feat: polish life os cards and voice stack
This commit is contained in:
parent
66362c7176
commit
0edf8c3fef
21 changed files with 3681 additions and 502 deletions
|
|
@ -235,6 +235,8 @@
|
|||
letter-spacing: 0.005em;
|
||||
color: #624d40;
|
||||
opacity: 0.95;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
|
@ -324,175 +326,43 @@
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
.task-card__editor-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10001;
|
||||
padding:
|
||||
max(18px, env(safe-area-inset-top))
|
||||
max(16px, env(safe-area-inset-right))
|
||||
max(18px, env(safe-area-inset-bottom))
|
||||
max(16px, env(safe-area-inset-left));
|
||||
background: rgba(38, 27, 21, 0.42);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
box-sizing: border-box;
|
||||
.task-card__title--editing,
|
||||
.task-card__body--editing {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.task-card__editor-sheet {
|
||||
.task-card__inline-editor {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 14px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid rgba(87, 65, 50, 0.16);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 255, 255, 0.74), transparent 28%),
|
||||
linear-gradient(160deg, rgba(254, 246, 237, 0.985), rgba(240, 226, 210, 0.985));
|
||||
color: var(--task-ink);
|
||||
box-shadow:
|
||||
0 22px 48px rgba(48, 32, 24, 0.24),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
padding: 18px 16px 16px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-card__editor-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-card__editor-kicker {
|
||||
font-size: 0.66rem;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--task-muted);
|
||||
}
|
||||
|
||||
.task-card__editor-title {
|
||||
margin-top: 3px;
|
||||
font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.04;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.012em;
|
||||
color: var(--task-ink);
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-card__editor-close {
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--task-muted);
|
||||
font: 700 0.86rem/1 'M-1m Code', var(--card-font, 'SF Mono', ui-monospace, Menlo, Consolas, monospace);
|
||||
letter-spacing: 0.02em;
|
||||
padding: 6px 4px;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.task-card__editor-fields {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-card__editor-group {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-card__editor-label {
|
||||
font-size: 0.66rem;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--task-muted);
|
||||
}
|
||||
|
||||
.task-card__editor-input,
|
||||
.task-card__editor-textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(87, 65, 50, 0.14);
|
||||
background: rgba(255, 251, 246, 0.92);
|
||||
color: var(--task-ink);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.task-card__editor-input {
|
||||
min-height: 52px;
|
||||
padding: 12px 13px;
|
||||
font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
|
||||
font-size: 1.02rem;
|
||||
line-height: 1.1;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.012em;
|
||||
}
|
||||
|
||||
.task-card__editor-textarea {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 12px 13px;
|
||||
resize: none;
|
||||
font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.36;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.004em;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.task-card__editor-input:focus,
|
||||
.task-card__editor-textarea:focus {
|
||||
border-color: rgba(88, 112, 111, 0.48);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.82),
|
||||
0 0 0 3px rgba(88, 112, 111, 0.12);
|
||||
.task-card__inline-editor::placeholder {
|
||||
color: rgba(98, 77, 64, 0.6);
|
||||
opacity: 1;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.task-card__editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
.task-card__inline-editor--title {
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
.task-card__editor-action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.task-card__editor-action-row > .task-card__button {
|
||||
flex: 0 0 auto;
|
||||
.task-card__inline-editor--body {
|
||||
min-height: 1.34em;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -759,49 +629,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
const editorOverlayEl = doc.createElement('div');
|
||||
editorOverlayEl.className = 'task-card__editor-overlay';
|
||||
editorOverlayEl.innerHTML = `
|
||||
<div class="task-card__editor-sheet" role="dialog" aria-modal="true" aria-labelledby="task-editor-title">
|
||||
<div class="task-card__editor-head">
|
||||
<div>
|
||||
<div class="task-card__editor-kicker">${laneLabels[lane] || 'Task'}</div>
|
||||
<div id="task-editor-title" class="task-card__editor-title">Edit task</div>
|
||||
</div>
|
||||
<button type="button" class="task-card__editor-close" data-task-editor-close>Close</button>
|
||||
</div>
|
||||
<form class="task-card__editor-fields" data-task-editor-form>
|
||||
<div class="task-card__editor-group">
|
||||
<label class="task-card__editor-label" for="task-editor-title-input">Title</label>
|
||||
<input id="task-editor-title-input" data-task-editor-title-input class="task-card__editor-input" type="text" maxlength="240" />
|
||||
</div>
|
||||
<div class="task-card__editor-group" style="min-height:0;">
|
||||
<label class="task-card__editor-label" for="task-editor-body-input">Description</label>
|
||||
<textarea id="task-editor-body-input" data-task-editor-body-input class="task-card__editor-textarea" placeholder="Add notes, context, or next steps"></textarea>
|
||||
</div>
|
||||
<div class="task-card__editor-actions">
|
||||
<div class="task-card__editor-action-row">
|
||||
<button type="button" class="task-card__button task-card__button--secondary" data-task-editor-cancel>Cancel</button>
|
||||
<button type="submit" class="task-card__button" data-task-editor-save>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
const editorFormEl = editorOverlayEl.querySelector('[data-task-editor-form]');
|
||||
const editorTitleInputEl = editorOverlayEl.querySelector('[data-task-editor-title-input]');
|
||||
const editorBodyInputEl = editorOverlayEl.querySelector('[data-task-editor-body-input]');
|
||||
const editorCloseEl = editorOverlayEl.querySelector('[data-task-editor-close]');
|
||||
const editorCancelEl = editorOverlayEl.querySelector('[data-task-editor-cancel]');
|
||||
const editorSaveEl = editorOverlayEl.querySelector('[data-task-editor-save]');
|
||||
if (
|
||||
!(editorFormEl instanceof HTMLFormElement) ||
|
||||
!(editorTitleInputEl instanceof HTMLInputElement) ||
|
||||
!(editorBodyInputEl instanceof HTMLTextAreaElement) ||
|
||||
!(editorCloseEl instanceof HTMLButtonElement) ||
|
||||
!(editorCancelEl instanceof HTMLButtonElement) ||
|
||||
!(editorSaveEl instanceof HTMLButtonElement)
|
||||
) return;
|
||||
let activeInlineEdit = null;
|
||||
|
||||
const setBusy = (busy) => {
|
||||
laneToggleEl.disabled = busy || !taskPath;
|
||||
|
|
@ -811,11 +639,11 @@
|
|||
for (const button of tagsEl.querySelectorAll('button')) {
|
||||
if (button instanceof HTMLButtonElement) button.disabled = busy;
|
||||
}
|
||||
editorTitleInputEl.disabled = busy;
|
||||
editorBodyInputEl.disabled = busy;
|
||||
editorCloseEl.disabled = busy;
|
||||
editorCancelEl.disabled = busy;
|
||||
editorSaveEl.disabled = busy;
|
||||
summaryEl.style.pointerEvents = busy ? 'none' : '';
|
||||
descriptionEl.style.pointerEvents = busy ? 'none' : '';
|
||||
if (activeInlineEdit?.input instanceof HTMLTextAreaElement) {
|
||||
activeInlineEdit.input.disabled = busy;
|
||||
}
|
||||
};
|
||||
|
||||
const closeMoveMenu = () => {
|
||||
|
|
@ -858,26 +686,97 @@
|
|||
window.dispatchEvent(new Event('nanobot:cards-refresh'));
|
||||
};
|
||||
|
||||
const closeEditor = () => {
|
||||
editorOverlayEl.style.display = 'none';
|
||||
const autosizeInlineEditor = (editor, minHeight = 0) => {
|
||||
editor.style.height = '0px';
|
||||
const nextHeight = Math.max(Math.ceil(minHeight), editor.scrollHeight);
|
||||
editor.style.height = `${Math.max(nextHeight, 20)}px`;
|
||||
};
|
||||
|
||||
const openEditor = (focusField = 'title') => {
|
||||
if (!taskPath) return;
|
||||
const beginInlineEdit = (field) => {
|
||||
if (!taskPath || activeInlineEdit) return;
|
||||
closeMoveMenu();
|
||||
editorTitleInputEl.value = title;
|
||||
editorBodyInputEl.value = body;
|
||||
if (editorOverlayEl.parentElement !== doc.body) {
|
||||
doc.body.appendChild(editorOverlayEl);
|
||||
}
|
||||
editorOverlayEl.style.display = 'block';
|
||||
view.requestAnimationFrame(() => {
|
||||
const target = focusField === 'description' ? editorBodyInputEl : editorTitleInputEl;
|
||||
target.focus();
|
||||
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
|
||||
const end = target.value.length;
|
||||
target.setSelectionRange(end, end);
|
||||
const host = field === 'title' ? summaryEl : descriptionEl;
|
||||
const currentValue = field === 'title' ? title : body;
|
||||
const editor = document.createElement('textarea');
|
||||
const minHeight = host.getBoundingClientRect().height;
|
||||
|
||||
editor.className = `task-card__inline-editor ${
|
||||
field === 'title' ? 'task-card__inline-editor--title' : 'task-card__inline-editor--body'
|
||||
}`;
|
||||
editor.rows = 1;
|
||||
editor.value = currentValue;
|
||||
editor.placeholder = field === 'description' ? 'Add description' : '';
|
||||
editor.setAttribute('aria-label', field === 'title' ? 'Edit task title' : 'Edit task description');
|
||||
|
||||
host.textContent = '';
|
||||
host.classList.remove('task-card__body--placeholder');
|
||||
host.classList.add(field === 'title' ? 'task-card__title--editing' : 'task-card__body--editing');
|
||||
host.appendChild(editor);
|
||||
autosizeInlineEditor(editor, minHeight);
|
||||
|
||||
const cancel = () => {
|
||||
if (activeInlineEdit?.input !== editor) return;
|
||||
activeInlineEdit = null;
|
||||
render();
|
||||
};
|
||||
|
||||
const commit = async () => {
|
||||
if (activeInlineEdit?.input !== editor) return;
|
||||
const nextValue = editor.value.trim();
|
||||
if (field === 'title' && !nextValue) {
|
||||
editor.focus();
|
||||
return;
|
||||
}
|
||||
activeInlineEdit = null;
|
||||
if (nextValue === currentValue) {
|
||||
render();
|
||||
return;
|
||||
}
|
||||
const ok = await runTaskEdit(field === 'title' ? { title: nextValue } : { description: nextValue });
|
||||
if (!ok) render();
|
||||
};
|
||||
|
||||
activeInlineEdit = {
|
||||
field,
|
||||
input: editor,
|
||||
cancel,
|
||||
commit,
|
||||
};
|
||||
|
||||
editor.addEventListener('input', () => {
|
||||
autosizeInlineEditor(editor, minHeight);
|
||||
});
|
||||
|
||||
editor.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
editor.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (field === 'title' && event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
editor.blur();
|
||||
return;
|
||||
}
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
editor.blur();
|
||||
}
|
||||
});
|
||||
|
||||
editor.addEventListener('blur', () => {
|
||||
if (activeInlineEdit?.input !== editor) return;
|
||||
void commit();
|
||||
});
|
||||
|
||||
view.requestAnimationFrame(() => {
|
||||
editor.focus();
|
||||
const end = editor.value.length;
|
||||
editor.setSelectionRange(end, end);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -958,13 +857,14 @@
|
|||
if (payload && typeof payload === 'object' && payload.error) {
|
||||
throw new Error(String(payload.error));
|
||||
}
|
||||
closeEditor();
|
||||
refreshCards();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Task edit failed', error);
|
||||
setBusy(false);
|
||||
setStatus('Unavailable', '#8e3023', '#f3d3cc');
|
||||
publishLiveContent(lane, true, String(error));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1094,48 +994,16 @@
|
|||
doc.body.appendChild(moveMenuEl);
|
||||
}
|
||||
|
||||
editorCloseEl.addEventListener('click', () => {
|
||||
closeEditor();
|
||||
});
|
||||
|
||||
editorCancelEl.addEventListener('click', () => {
|
||||
closeEditor();
|
||||
});
|
||||
|
||||
editorOverlayEl.addEventListener('pointerdown', (event) => {
|
||||
if (event.target === editorOverlayEl) {
|
||||
closeEditor();
|
||||
}
|
||||
});
|
||||
|
||||
editorFormEl.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const nextTitle = editorTitleInputEl.value.trim();
|
||||
const nextDescription = editorBodyInputEl.value.trim();
|
||||
if (!nextTitle) {
|
||||
editorTitleInputEl.focus();
|
||||
return;
|
||||
}
|
||||
if (nextTitle === title && nextDescription === body) {
|
||||
closeEditor();
|
||||
return;
|
||||
}
|
||||
void runTaskEdit({
|
||||
title: nextTitle,
|
||||
description: nextDescription,
|
||||
});
|
||||
});
|
||||
|
||||
summaryEl.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openEditor('title');
|
||||
beginInlineEdit('title');
|
||||
});
|
||||
|
||||
descriptionEl.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openEditor('description');
|
||||
beginInlineEdit('description');
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue