feat: snapshot fullscreen todo card editor
This commit is contained in:
parent
980dfb9e0e
commit
66362c7176
1 changed files with 628 additions and 112 deletions
|
|
@ -87,57 +87,111 @@
|
||||||
|
|
||||||
.task-card__topline {
|
.task-card__topline {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
position: relative;
|
||||||
|
z-index: 4;
|
||||||
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-card__lane-button {
|
||||||
|
appearance: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.task-card__lane {
|
.task-card__lane {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 0.69rem;
|
font-size: 0.64rem;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.11em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--task-muted);
|
color: var(--task-muted);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card__lane-caret {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-family: 'M-1m Code', var(--card-font, 'SF Mono', ui-monospace, Menlo, Consolas, monospace);
|
||||||
|
font-size: 0.66rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--task-muted);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
transition: transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card__lane-button[data-open='1'] .task-card__lane-caret {
|
||||||
|
transform: translateY(-1px) rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card__lane-button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card__stamp {
|
.task-card__stamp {
|
||||||
display: none;
|
display: none;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-end;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-radius: 999px;
|
padding: 0;
|
||||||
padding: 5px 8px;
|
font-size: 0.72rem;
|
||||||
font-size: 0.68rem;
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--task-muted);
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card__title {
|
.task-card__title {
|
||||||
font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
|
font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
|
||||||
font-size: 1.08rem;
|
font-size: 0.96rem;
|
||||||
line-height: 1.1;
|
line-height: 1.06;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.008em;
|
||||||
color: var(--task-ink);
|
color: var(--task-ink);
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card__tags {
|
.task-card__tags {
|
||||||
display: none;
|
display: none;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card__tags::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card__tag {
|
.task-card__tag {
|
||||||
|
appearance: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
max-width: 100%;
|
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 4px 9px;
|
padding: 4px 9px;
|
||||||
background: var(--task-accent-soft);
|
background: var(--task-accent-soft);
|
||||||
|
|
@ -148,6 +202,28 @@
|
||||||
border: 1px solid rgba(0, 0, 0, 0.035);
|
border: 1px solid rgba(0, 0, 0, 0.035);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card__tag:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card__tag--action {
|
||||||
|
border-style: dashed;
|
||||||
|
background: rgba(255, 248, 239, 0.74);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card__tag--holding {
|
||||||
|
background: rgba(165, 95, 75, 0.18);
|
||||||
|
color: #7b2f20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card__body {
|
.task-card__body {
|
||||||
|
|
@ -159,6 +235,13 @@
|
||||||
letter-spacing: 0.005em;
|
letter-spacing: 0.005em;
|
||||||
color: #624d40;
|
color: #624d40;
|
||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card__body--placeholder {
|
||||||
|
opacity: 0.62;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card__meta {
|
.task-card__meta {
|
||||||
|
|
@ -213,28 +296,221 @@
|
||||||
|
|
||||||
.task-card__move {
|
.task-card__move {
|
||||||
display: none;
|
display: none;
|
||||||
flex-wrap: wrap;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
min-width: 150px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 248, 239, 0.96);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 24px rgba(79, 56, 43, 0.12),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card__move-button {
|
.task-card__move-button {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 10px;
|
||||||
padding: 7px 10px;
|
padding: 8px 10px;
|
||||||
background: rgba(255, 248, 239, 0.78);
|
background: rgba(255, 248, 239, 0.78);
|
||||||
color: var(--task-button-ink);
|
color: var(--task-button-ink);
|
||||||
font: 700 0.7rem/1 'M-1m Code', var(--card-font, 'SF Mono', ui-monospace, Menlo, Consolas, monospace);
|
font: 700 0.7rem/1 'M-1m Code', var(--card-font, 'SF Mono', ui-monospace, Menlo, Consolas, monospace);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
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__editor-sheet {
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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__editor-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div data-task-item-card class="task-card">
|
<div data-task-item-card class="task-card">
|
||||||
<div class="task-card__inner">
|
<div class="task-card__inner">
|
||||||
<div class="task-card__topline">
|
<div class="task-card__topline">
|
||||||
<div data-task-subtitle class="task-card__lane">Task</div>
|
<button
|
||||||
|
data-task-lane-toggle
|
||||||
|
type="button"
|
||||||
|
class="task-card__lane-button"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-open="0"
|
||||||
|
>
|
||||||
|
<span data-task-subtitle class="task-card__lane">Task</span>
|
||||||
|
<span class="task-card__lane-caret">▾</span>
|
||||||
|
</button>
|
||||||
<span data-task-status class="task-card__stamp"></span>
|
<span data-task-status class="task-card__stamp"></span>
|
||||||
|
<div data-task-move-menu class="task-card__move"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-task-summary class="task-card__title">Loading...</div>
|
<div data-task-summary class="task-card__title">Loading...</div>
|
||||||
|
|
@ -243,15 +519,8 @@
|
||||||
|
|
||||||
<div class="task-card__meta">
|
<div class="task-card__meta">
|
||||||
<div data-task-due class="task-card__meta-chip"></div>
|
<div data-task-due class="task-card__meta-chip"></div>
|
||||||
<div data-task-age class="task-card__meta-chip"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="task-card__actions">
|
|
||||||
<button data-task-primary type="button" class="task-card__button" style="display:none;">Start</button>
|
|
||||||
<button data-task-move-toggle type="button" class="task-card__button task-card__button--secondary" style="display:none;">Move</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-task-move-menu class="task-card__move"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -261,29 +530,27 @@
|
||||||
const root = script?.closest('[data-nanobot-card-root]');
|
const root = script?.closest('[data-nanobot-card-root]');
|
||||||
const state = window.__nanobotGetCardState?.(script) || {};
|
const state = window.__nanobotGetCardState?.(script) || {};
|
||||||
if (!(root instanceof HTMLElement)) return;
|
if (!(root instanceof HTMLElement)) return;
|
||||||
|
const doc = root.ownerDocument || document;
|
||||||
|
const view = doc.defaultView || window;
|
||||||
|
|
||||||
const cardEl = root.querySelector('[data-task-item-card]');
|
const cardEl = root.querySelector('[data-task-item-card]');
|
||||||
|
const laneToggleEl = root.querySelector('[data-task-lane-toggle]');
|
||||||
const subtitleEl = root.querySelector('[data-task-subtitle]');
|
const subtitleEl = root.querySelector('[data-task-subtitle]');
|
||||||
const statusEl = root.querySelector('[data-task-status]');
|
const statusEl = root.querySelector('[data-task-status]');
|
||||||
const summaryEl = root.querySelector('[data-task-summary]');
|
const summaryEl = root.querySelector('[data-task-summary]');
|
||||||
const tagsEl = root.querySelector('[data-task-tags]');
|
const tagsEl = root.querySelector('[data-task-tags]');
|
||||||
const dueEl = root.querySelector('[data-task-due]');
|
const dueEl = root.querySelector('[data-task-due]');
|
||||||
const ageEl = root.querySelector('[data-task-age]');
|
|
||||||
const descriptionEl = root.querySelector('[data-task-description]');
|
const descriptionEl = root.querySelector('[data-task-description]');
|
||||||
const primaryEl = root.querySelector('[data-task-primary]');
|
|
||||||
const moveToggleEl = root.querySelector('[data-task-move-toggle]');
|
|
||||||
const moveMenuEl = root.querySelector('[data-task-move-menu]');
|
const moveMenuEl = root.querySelector('[data-task-move-menu]');
|
||||||
if (
|
if (
|
||||||
!(cardEl instanceof HTMLElement) ||
|
!(cardEl instanceof HTMLElement) ||
|
||||||
|
!(laneToggleEl instanceof HTMLButtonElement) ||
|
||||||
!(subtitleEl instanceof HTMLElement) ||
|
!(subtitleEl instanceof HTMLElement) ||
|
||||||
!(statusEl instanceof HTMLElement) ||
|
!(statusEl instanceof HTMLElement) ||
|
||||||
!(summaryEl instanceof HTMLElement) ||
|
!(summaryEl instanceof HTMLElement) ||
|
||||||
!(tagsEl instanceof HTMLElement) ||
|
!(tagsEl instanceof HTMLElement) ||
|
||||||
!(dueEl instanceof HTMLElement) ||
|
!(dueEl instanceof HTMLElement) ||
|
||||||
!(ageEl instanceof HTMLElement) ||
|
|
||||||
!(descriptionEl instanceof HTMLElement) ||
|
!(descriptionEl instanceof HTMLElement) ||
|
||||||
!(primaryEl instanceof HTMLButtonElement) ||
|
|
||||||
!(moveToggleEl instanceof HTMLButtonElement) ||
|
|
||||||
!(moveMenuEl instanceof HTMLElement)
|
!(moveMenuEl instanceof HTMLElement)
|
||||||
) return;
|
) return;
|
||||||
|
|
||||||
|
|
@ -351,18 +618,9 @@
|
||||||
canceled: 'Cancel',
|
canceled: 'Cancel',
|
||||||
};
|
};
|
||||||
|
|
||||||
const primaryActionForLane = (value) => {
|
|
||||||
if (value === 'backlog') return { lane: 'in-progress', label: 'Start' };
|
|
||||||
if (value === 'in-progress') return { lane: 'done', label: 'Done' };
|
|
||||||
if (value === 'blocked') return { lane: 'in-progress', label: 'Resume' };
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveOptionsForLane = (value) => {
|
const moveOptionsForLane = (value) => {
|
||||||
const primary = primaryActionForLane(value);
|
|
||||||
return ['backlog', 'in-progress', 'blocked', 'done', 'canceled']
|
return ['backlog', 'in-progress', 'blocked', 'done', 'canceled']
|
||||||
.filter((targetLane) => targetLane !== value)
|
.filter((targetLane) => targetLane !== value)
|
||||||
.filter((targetLane) => !primary || targetLane !== primary.lane)
|
|
||||||
.map((targetLane) => ({
|
.map((targetLane) => ({
|
||||||
lane: targetLane,
|
lane: targetLane,
|
||||||
label: actionLabels[targetLane] || targetLane,
|
label: actionLabels[targetLane] || targetLane,
|
||||||
|
|
@ -379,8 +637,10 @@
|
||||||
|
|
||||||
const setStatus = (label, fg, bg) => {
|
const setStatus = (label, fg, bg) => {
|
||||||
statusEl.textContent = label;
|
statusEl.textContent = label;
|
||||||
statusEl.style.color = fg;
|
statusEl.style.color = fg || 'var(--task-muted)';
|
||||||
statusEl.style.background = bg;
|
statusEl.style.background = bg && bg !== 'transparent' ? bg : 'transparent';
|
||||||
|
statusEl.style.border = bg && bg !== 'transparent' ? '1px solid rgba(0, 0, 0, 0.04)' : '0';
|
||||||
|
statusEl.style.padding = bg && bg !== 'transparent' ? '5px 8px' : '0';
|
||||||
statusEl.style.display = label ? 'inline-flex' : 'none';
|
statusEl.style.display = label ? 'inline-flex' : 'none';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -400,7 +660,7 @@
|
||||||
const computeScore = () => {
|
const computeScore = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const dueMs = parseDueTimeMs();
|
const dueMs = parseDueTimeMs();
|
||||||
let score = 70;
|
let score = 54;
|
||||||
if (Number.isFinite(dueMs)) {
|
if (Number.isFinite(dueMs)) {
|
||||||
const hoursUntilDue = (dueMs - now) / (60 * 60 * 1000);
|
const hoursUntilDue = (dueMs - now) / (60 * 60 * 1000);
|
||||||
if (hoursUntilDue <= 0) score = 100;
|
if (hoursUntilDue <= 0) score = 100;
|
||||||
|
|
@ -413,12 +673,12 @@
|
||||||
const createdMs = parseCreatedTimeMs();
|
const createdMs = parseCreatedTimeMs();
|
||||||
if (Number.isFinite(createdMs)) {
|
if (Number.isFinite(createdMs)) {
|
||||||
const ageDays = Math.max(0, (now - createdMs) / (24 * 60 * 60 * 1000));
|
const ageDays = Math.max(0, (now - createdMs) / (24 * 60 * 60 * 1000));
|
||||||
if (ageDays >= 30) score = 94;
|
if (ageDays >= 30) score = 80;
|
||||||
else if (ageDays >= 21) score = 90;
|
else if (ageDays >= 21) score = 76;
|
||||||
else if (ageDays >= 14) score = 86;
|
else if (ageDays >= 14) score = 72;
|
||||||
else if (ageDays >= 7) score = 82;
|
else if (ageDays >= 7) score = 68;
|
||||||
else if (ageDays >= 3) score = 78;
|
else if (ageDays >= 3) score = 62;
|
||||||
else if (ageDays >= 1) score = 74;
|
else if (ageDays >= 1) score = 58;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lane === 'blocked') return Math.min(100, score + 4);
|
if (lane === 'blocked') return Math.min(100, score + 4);
|
||||||
|
|
@ -437,16 +697,6 @@
|
||||||
return `Due ${parsed.toLocaleDateString([], { month: 'short', day: 'numeric' })}`;
|
return `Due ${parsed.toLocaleDateString([], { month: 'short', day: 'numeric' })}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAge = () => {
|
|
||||||
const createdMs = parseCreatedTimeMs();
|
|
||||||
if (!Number.isFinite(createdMs)) return '';
|
|
||||||
const days = Math.floor((Date.now() - createdMs) / (24 * 60 * 60 * 1000));
|
|
||||||
if (days <= 0) return 'New today';
|
|
||||||
if (days < 7) return `${days}d old`;
|
|
||||||
if (days < 30) return `${Math.floor(days / 7)}w old`;
|
|
||||||
return `${Math.floor(days / 30)}mo old`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const summarizeBody = () => {
|
const summarizeBody = () => {
|
||||||
if (!body) return '';
|
if (!body) return '';
|
||||||
const trimmed = body.trim();
|
const trimmed = body.trim();
|
||||||
|
|
@ -496,21 +746,141 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setBusy = (busy) => {
|
const parseToolPayload = (result) => {
|
||||||
primaryEl.disabled = busy;
|
if (result && typeof result === 'object' && result.parsed && typeof result.parsed === 'object') {
|
||||||
moveToggleEl.disabled = busy;
|
return result.parsed;
|
||||||
for (const button of moveMenuEl.querySelectorAll('button')) {
|
}
|
||||||
if (button instanceof HTMLButtonElement) button.disabled = busy;
|
const raw = typeof result?.content === 'string' ? result.content : '';
|
||||||
|
if (!raw.trim()) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshCards = () => {
|
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;
|
||||||
|
|
||||||
|
const setBusy = (busy) => {
|
||||||
|
laneToggleEl.disabled = busy || !taskPath;
|
||||||
|
for (const button of moveMenuEl.querySelectorAll('button')) {
|
||||||
|
if (button instanceof HTMLButtonElement) button.disabled = busy;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeMoveMenu = () => {
|
||||||
moveMenuEl.style.display = 'none';
|
moveMenuEl.style.display = 'none';
|
||||||
moveToggleEl.textContent = 'Move';
|
laneToggleEl.setAttribute('aria-expanded', 'false');
|
||||||
moveToggleEl.setAttribute('aria-expanded', 'false');
|
laneToggleEl.dataset.open = '0';
|
||||||
|
};
|
||||||
|
|
||||||
|
const positionMoveMenu = () => {
|
||||||
|
if (moveMenuEl.style.display !== 'flex') return;
|
||||||
|
const rect = laneToggleEl.getBoundingClientRect();
|
||||||
|
const menuRect = moveMenuEl.getBoundingClientRect();
|
||||||
|
const gutter = 12;
|
||||||
|
let left = rect.left;
|
||||||
|
let top = rect.bottom + 6;
|
||||||
|
if (left + menuRect.width > view.innerWidth - gutter) {
|
||||||
|
left = Math.max(gutter, view.innerWidth - gutter - menuRect.width);
|
||||||
|
}
|
||||||
|
if (top + menuRect.height > view.innerHeight - gutter) {
|
||||||
|
top = Math.max(gutter, rect.top - menuRect.height - 6);
|
||||||
|
}
|
||||||
|
moveMenuEl.style.left = `${Math.round(left)}px`;
|
||||||
|
moveMenuEl.style.top = `${Math.round(top)}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openMoveMenu = () => {
|
||||||
|
if (laneToggleEl.disabled) return;
|
||||||
|
moveMenuEl.style.display = 'flex';
|
||||||
|
moveMenuEl.style.visibility = 'hidden';
|
||||||
|
moveMenuEl.style.left = '0px';
|
||||||
|
moveMenuEl.style.top = '0px';
|
||||||
|
positionMoveMenu();
|
||||||
|
moveMenuEl.style.visibility = 'visible';
|
||||||
|
laneToggleEl.setAttribute('aria-expanded', 'true');
|
||||||
|
laneToggleEl.dataset.open = '1';
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshCards = () => {
|
||||||
|
closeMoveMenu();
|
||||||
window.dispatchEvent(new Event('nanobot:cards-refresh'));
|
window.dispatchEvent(new Event('nanobot:cards-refresh'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeEditor = () => {
|
||||||
|
editorOverlayEl.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditor = (focusField = 'title') => {
|
||||||
|
if (!taskPath) 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 runTransition = async (targetLane) => {
|
const runTransition = async (targetLane) => {
|
||||||
if (!taskPath) return;
|
if (!taskPath) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
|
|
@ -549,25 +919,132 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runTagMutation = async (action, tagValue) => {
|
||||||
|
if (!taskPath) return;
|
||||||
|
const cleanedTag = String(tagValue || '').trim();
|
||||||
|
if (!cleanedTag) return;
|
||||||
|
setBusy(true);
|
||||||
|
setStatus('Saving', '#6e5b4d', 'rgba(255, 244, 227, 0.9)');
|
||||||
|
try {
|
||||||
|
const result = await window.__nanobotCallTool?.('task_board', {
|
||||||
|
action,
|
||||||
|
task: taskPath,
|
||||||
|
tags: [cleanedTag],
|
||||||
|
});
|
||||||
|
const payload = parseToolPayload(result);
|
||||||
|
if (payload && typeof payload === 'object' && payload.error) {
|
||||||
|
throw new Error(String(payload.error));
|
||||||
|
}
|
||||||
|
refreshCards();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Task tag mutation failed (${action})`, error);
|
||||||
|
setBusy(false);
|
||||||
|
setStatus('Unavailable', '#8e3023', '#f3d3cc');
|
||||||
|
publishLiveContent(lane, true, String(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runTaskEdit = async (changes) => {
|
||||||
|
if (!taskPath) return;
|
||||||
|
setBusy(true);
|
||||||
|
setStatus('Saving', '#6e5b4d', 'rgba(255, 244, 227, 0.9)');
|
||||||
|
try {
|
||||||
|
const result = await window.__nanobotCallTool?.('task_board', {
|
||||||
|
action: 'edit',
|
||||||
|
task: taskPath,
|
||||||
|
...changes,
|
||||||
|
});
|
||||||
|
const payload = parseToolPayload(result);
|
||||||
|
if (payload && typeof payload === 'object' && payload.error) {
|
||||||
|
throw new Error(String(payload.error));
|
||||||
|
}
|
||||||
|
closeEditor();
|
||||||
|
refreshCards();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Task edit failed', error);
|
||||||
|
setBusy(false);
|
||||||
|
setStatus('Unavailable', '#8e3023', '#f3d3cc');
|
||||||
|
publishLiveContent(lane, true, String(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const promptForTag = async () => {
|
||||||
|
if (!taskPath) return;
|
||||||
|
const value = window.prompt('Add tag to task', '');
|
||||||
|
if (value == null) return;
|
||||||
|
const cleaned = value.trim();
|
||||||
|
if (!cleaned) return;
|
||||||
|
await runTagMutation('add_tag', cleaned);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindTagRemoval = (button, tagValue) => {
|
||||||
|
let holdTimer = null;
|
||||||
|
let holdTriggered = false;
|
||||||
|
const clearHold = () => {
|
||||||
|
if (holdTimer !== null) {
|
||||||
|
window.clearTimeout(holdTimer);
|
||||||
|
holdTimer = null;
|
||||||
|
}
|
||||||
|
button.classList.remove('task-card__tag--holding');
|
||||||
|
};
|
||||||
|
|
||||||
|
button.addEventListener('pointerdown', (event) => {
|
||||||
|
if (!taskPath || button.disabled) return;
|
||||||
|
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
||||||
|
holdTriggered = false;
|
||||||
|
button.classList.add('task-card__tag--holding');
|
||||||
|
holdTimer = window.setTimeout(async () => {
|
||||||
|
holdTimer = null;
|
||||||
|
holdTriggered = true;
|
||||||
|
button.classList.remove('task-card__tag--holding');
|
||||||
|
const confirmed = window.confirm(`Remove ${tagValue} from this task?`);
|
||||||
|
if (!confirmed) return;
|
||||||
|
await runTagMutation('remove_tag', tagValue);
|
||||||
|
}, 650);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const eventName of ['pointerup', 'pointerleave', 'pointercancel']) {
|
||||||
|
button.addEventListener(eventName, clearHold);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener('contextmenu', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
button.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (holdTriggered) {
|
||||||
|
holdTriggered = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const renderTags = () => {
|
const renderTags = () => {
|
||||||
tagsEl.innerHTML = '';
|
tagsEl.innerHTML = '';
|
||||||
if (!tags.length) {
|
for (const tag of tags) {
|
||||||
tagsEl.style.display = 'none';
|
const chip = document.createElement('button');
|
||||||
return;
|
chip.type = 'button';
|
||||||
}
|
|
||||||
const visibleTags = tags.slice(0, 4);
|
|
||||||
for (const tag of visibleTags) {
|
|
||||||
const chip = document.createElement('span');
|
|
||||||
chip.className = 'task-card__tag';
|
chip.className = 'task-card__tag';
|
||||||
chip.textContent = tag;
|
chip.textContent = tag;
|
||||||
|
chip.title = `Hold to remove ${tag}`;
|
||||||
|
bindTagRemoval(chip, tag);
|
||||||
tagsEl.appendChild(chip);
|
tagsEl.appendChild(chip);
|
||||||
}
|
}
|
||||||
if (tags.length > visibleTags.length) {
|
|
||||||
const overflow = document.createElement('span');
|
const addTagButton = document.createElement('button');
|
||||||
overflow.className = 'task-card__tag';
|
addTagButton.type = 'button';
|
||||||
overflow.textContent = `+${tags.length - visibleTags.length}`;
|
addTagButton.className = 'task-card__tag task-card__tag--action';
|
||||||
tagsEl.appendChild(overflow);
|
addTagButton.textContent = '+';
|
||||||
}
|
addTagButton.title = 'Add tag';
|
||||||
|
addTagButton.disabled = !taskPath;
|
||||||
|
addTagButton.addEventListener('click', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
await promptForTag();
|
||||||
|
});
|
||||||
|
tagsEl.appendChild(addTagButton);
|
||||||
|
|
||||||
tagsEl.style.display = 'flex';
|
tagsEl.style.display = 'flex';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -575,10 +1052,11 @@
|
||||||
moveMenuEl.innerHTML = '';
|
moveMenuEl.innerHTML = '';
|
||||||
const options = moveOptionsForLane(lane);
|
const options = moveOptionsForLane(lane);
|
||||||
if (!options.length || !taskPath) {
|
if (!options.length || !taskPath) {
|
||||||
moveToggleEl.style.display = 'none';
|
laneToggleEl.disabled = true;
|
||||||
|
closeMoveMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
moveToggleEl.style.display = 'inline-flex';
|
laneToggleEl.disabled = false;
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
|
|
@ -593,56 +1071,94 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPrimaryAction = () => {
|
laneToggleEl.addEventListener('click', (event) => {
|
||||||
const primary = primaryActionForLane(lane);
|
|
||||||
if (!primary || !taskPath) {
|
|
||||||
primaryEl.style.display = 'none';
|
|
||||||
primaryEl.disabled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
primaryEl.textContent = primary.label;
|
|
||||||
primaryEl.style.display = 'inline-flex';
|
|
||||||
primaryEl.disabled = false;
|
|
||||||
primaryEl.onclick = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
runTransition(primary.lane);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
moveToggleEl.addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const open = moveMenuEl.style.display === 'flex';
|
const open = moveMenuEl.style.display === 'flex';
|
||||||
moveMenuEl.style.display = open ? 'none' : 'flex';
|
if (open) closeMoveMenu();
|
||||||
moveToggleEl.textContent = open ? 'Move' : 'Close';
|
else openMoveMenu();
|
||||||
moveToggleEl.setAttribute('aria-expanded', open ? 'false' : 'true');
|
});
|
||||||
|
|
||||||
|
doc.addEventListener('pointerdown', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof Node)) return;
|
||||||
|
if (moveMenuEl.style.display !== 'flex') return;
|
||||||
|
if (moveMenuEl.contains(target) || laneToggleEl.contains(target)) return;
|
||||||
|
closeMoveMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
view.addEventListener('resize', closeMoveMenu);
|
||||||
|
view.addEventListener('scroll', closeMoveMenu, true);
|
||||||
|
|
||||||
|
if (moveMenuEl.parentElement !== doc.body) {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
descriptionEl.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
openEditor('description');
|
||||||
});
|
});
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
applyTheme();
|
applyTheme();
|
||||||
subtitleEl.textContent = laneLabels[lane] || 'Task';
|
subtitleEl.textContent = laneLabels[lane] || 'Task';
|
||||||
summaryEl.textContent = title || '(Untitled task)';
|
summaryEl.textContent = title || '(Untitled task)';
|
||||||
|
summaryEl.title = taskPath ? 'Tap to edit title' : '';
|
||||||
|
|
||||||
const dueText = formatDue();
|
const dueText = formatDue();
|
||||||
dueEl.textContent = dueText;
|
dueEl.textContent = dueText;
|
||||||
dueEl.style.display = dueText ? 'inline-flex' : 'none';
|
dueEl.style.display = dueText ? 'inline-flex' : 'none';
|
||||||
|
|
||||||
const ageText = formatAge();
|
setStatus('', 'var(--task-muted)', 'transparent');
|
||||||
ageEl.textContent = ageText;
|
|
||||||
ageEl.style.display = ageText ? 'inline-flex' : 'none';
|
|
||||||
|
|
||||||
const bodySummary = summarizeBody();
|
const bodySummary = summarizeBody();
|
||||||
descriptionEl.textContent = bodySummary;
|
descriptionEl.textContent = bodySummary || 'Add description';
|
||||||
descriptionEl.style.display = bodySummary ? 'block' : 'none';
|
descriptionEl.title = taskPath ? 'Tap to edit description' : '';
|
||||||
|
descriptionEl.style.display = taskPath ? 'block' : 'none';
|
||||||
|
descriptionEl.classList.toggle('task-card__body--placeholder', !bodySummary);
|
||||||
|
|
||||||
renderTags();
|
renderTags();
|
||||||
setStatus('', '', 'transparent');
|
|
||||||
renderPrimaryAction();
|
|
||||||
renderMoveMenu();
|
renderMoveMenu();
|
||||||
moveMenuEl.style.display = 'none';
|
closeMoveMenu();
|
||||||
moveToggleEl.textContent = 'Move';
|
|
||||||
moveToggleEl.setAttribute('aria-expanded', 'false');
|
|
||||||
publishLiveContent(lane, true, '');
|
publishLiveContent(lane, true, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue