Files
workspace/docs/mockups/2026-05-14-task-form-dialog-mockup.html
Roberto e1d15b3edd docs: add task form dialog keyboard polish design
Spec for porting AddEventDialog UX patterns into TaskFormDialog: new
header (title + description, no separator), full keyboard navigation
(roving focus on pills, arrow nav, Enter to open popover, arrow nav
inside list popovers and calendar), and date+time keyboard entry via
extended DateField (withTime + flat props) and parseDate time suffix.

Includes interactive HTML mockup demonstrating the keyboard flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:47:57 +02:00

853 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Task Form Dialog — keyboard-driven mockup</title>
<style>
:root {
--bg: #f4edf3;
--canvas: #ebe4ea;
--card: #ffffff;
--card-soft: #fbf7fa;
--border: #c8c3cd;
--border-soft: #d8d4dc;
--text: #1a1a1a;
--muted: #6e6a73;
--primary: #fbc881;
--primary-fg: #4a3210;
--accent: #e9e5ee;
--ring: #8a8ea9;
--danger: #c4423a;
--green: #5a8a55;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0c0c0c;
--canvas: #161616;
--card: #1a1a1a;
--card-soft: #202020;
--border: #323232;
--border-soft: #2a2a2a;
--text: #f5f5f5;
--muted: #9a9a9a;
--primary: #fbc881;
--primary-fg: #4a3210;
--accent: #2a2a2a;
--ring: #8a8ea9;
}
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Geist", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
-webkit-font-smoothing: antialiased;
letter-spacing: -0.01em;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
padding: 40px 20px;
}
.page-hint {
position: fixed; left: 16px; top: 16px;
background: var(--card); border: 1px solid var(--border);
border-radius: 8px; padding: 10px 12px;
font-size: 12px; max-width: 280px; line-height: 1.5;
color: var(--muted);
}
.page-hint strong { color: var(--text); }
.page-hint kbd {
font-family: ui-monospace, SFMono-Regular, monospace;
font-size: 11px;
background: var(--accent); border: 1px solid var(--border-soft);
border-radius: 4px; padding: 1px 5px;
}
/* Dialog */
.overlay {
width: 580px; max-width: 100%;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(20px);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 20px 50px -10px rgba(0,0,0,0.18), 0 0 0 1px rgba(255,255,255,0.4) inset;
overflow: hidden;
}
@media (prefers-color-scheme: dark) {
.overlay { background: rgba(26,26,26,0.92); }
}
/* Header — AddEventDialog style: title + description, no separator */
.dlg-header {
padding: 18px 22px 8px;
}
.dlg-title {
font-size: 16px; font-weight: 600; margin: 0;
letter-spacing: -0.02em;
}
.dlg-desc {
margin: 4px 0 0; font-size: 13px; color: var(--muted);
line-height: 1.45;
}
/* Body */
.dlg-body { padding: 18px 22px 12px; }
.title-input {
width: 100%;
border: none; outline: none; background: transparent;
font: inherit; color: inherit;
font-size: 22px; font-weight: 500;
letter-spacing: -0.02em;
line-height: 1.2;
}
.title-input::placeholder { color: var(--muted); opacity: 0.7; }
.desc-input {
margin-top: 8px;
width: 100%;
border: none; outline: none; background: transparent;
font: inherit; color: inherit;
font-size: 13px;
resize: none;
line-height: 1.5;
}
.desc-input::placeholder { color: var(--muted); opacity: 0.7; }
/* Properties section */
.props-label {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
color: var(--muted); margin: 14px 0 8px;
}
.pills { display: flex; flex-wrap: wrap; gap: 6px; }
/* Pill */
.pill {
display: inline-flex; align-items: center; gap: 6px;
height: 28px; padding: 0 10px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--card-soft);
font-size: 12px; color: var(--text);
cursor: pointer;
transition: background 120ms, border-color 120ms, box-shadow 120ms;
position: relative;
}
.pill[data-empty="true"] {
border-style: dashed;
color: var(--muted);
background: transparent;
}
.pill:focus-visible,
.pill[data-focused="true"] {
outline: none;
border-color: var(--ring);
box-shadow: 0 0 0 3px rgba(138,142,169,0.25);
}
.pill .pill-label { color: var(--muted); }
.pill .pill-value { font-weight: 500; }
.pill .pill-sep { color: var(--muted); opacity: 0.5; }
.pill-icon { font-size: 11px; line-height: 1; }
.pill .pi-up { color: #c4423a; }
.pill .pi-mid { color: #b97a14; }
.pill .pi-down { color: var(--muted); }
/* Footer */
.dlg-footer {
display: flex; align-items: center; justify-content: space-between;
gap: 8px;
padding: 12px 22px;
border-top: 1px solid var(--border-soft);
background: rgba(0,0,0,0.015);
}
@media (prefers-color-scheme: dark) {
.dlg-footer { background: rgba(255,255,255,0.02); }
}
.kbd-hint { font-size: 11px; color: var(--muted); }
.kbd-hint kbd {
font-family: ui-monospace, SFMono-Regular, monospace;
font-size: 10px;
background: var(--accent); border: 1px solid var(--border-soft);
border-radius: 4px; padding: 1px 5px;
}
.btn {
height: 30px; padding: 0 14px; border-radius: 8px;
font: inherit; font-size: 13px; font-weight: 500;
border: 1px solid var(--border);
background: transparent; color: var(--text);
cursor: pointer;
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(138,142,169,0.25);
border-color: var(--ring);
}
.btn-primary {
background: var(--primary);
color: var(--primary-fg);
border-color: transparent;
}
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.footer-actions { display: flex; gap: 6px; }
/* Popover */
.popover {
position: absolute;
z-index: 100;
min-width: 220px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 12px 32px -8px rgba(0,0,0,0.2);
padding: 4px;
display: none;
}
.popover[data-open="true"] { display: block; }
.pop-item {
display: flex; align-items: center; gap: 8px;
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
outline: none;
}
.pop-item:hover,
.pop-item:focus,
.pop-item[data-active="true"] { background: var(--accent); }
.pop-item:focus { box-shadow: inset 0 0 0 1px var(--ring); }
.pop-item .check { width: 14px; color: var(--muted); }
.pop-item[data-selected="true"] .check::before { content: "✓"; color: var(--text); }
/* DateField segments */
.datefield {
display: inline-flex; align-items: center;
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px 8px;
background: var(--card-soft);
font-family: ui-monospace, SFMono-Regular, monospace;
font-size: 13px;
}
.datefield:focus-within {
border-color: var(--ring);
box-shadow: 0 0 0 3px rgba(138,142,169,0.25);
}
.segment {
min-width: 1.8ch; text-align: center; padding: 2px 1px;
border-radius: 3px; outline: none; cursor: text;
color: var(--text);
}
.segment[data-placeholder="true"] { color: var(--muted); opacity: 0.6; }
.segment:focus { background: var(--accent); }
.seg-sep { color: var(--muted); padding: 0 1px; user-select: none; }
.date-pop {
padding: 12px;
min-width: 280px;
}
.date-pop .field-label {
font-size: 11px; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.06em;
margin-bottom: 6px;
}
.date-pop .cal {
margin-top: 12px;
border-top: 1px solid var(--border-soft);
padding-top: 10px;
}
.cal-head {
display: flex; align-items: center; justify-content: space-between;
font-size: 12px; margin-bottom: 6px;
}
.cal-head .month { font-weight: 600; }
.cal-head button {
border: 1px solid var(--border); background: transparent;
border-radius: 6px; width: 22px; height: 22px;
color: var(--text); cursor: pointer;
}
.cal-grid {
display: grid; grid-template-columns: repeat(7, 1fr);
gap: 2px; font-size: 11px;
}
.cal-dow {
color: var(--muted); text-align: center;
padding: 4px 0; font-weight: 500;
}
.cal-day {
text-align: center; padding: 5px 0;
border-radius: 5px; cursor: pointer; outline: none;
color: var(--text);
}
.cal-day:focus,
.cal-day[data-active="true"] { background: var(--accent); }
.cal-day[data-selected="true"] {
background: var(--primary); color: var(--primary-fg);
}
.cal-day[data-other-month="true"] { color: var(--muted); opacity: 0.4; }
.pop-anchor { position: relative; display: inline-flex; }
</style>
</head>
<body>
<div class="page-hint">
<strong>Keyboard demo</strong><br>
<kbd>Tab</kbd>/<kbd>Shift+Tab</kbd> cycles fields + pills.<br>
<kbd>Enter</kbd> opens focused pill.<br>
<kbd></kbd>/<kbd></kbd> inside popovers and calendar.<br>
<kbd>Esc</kbd> closes popover.<br>
Due pill: type date directly (segment edit).
<hr style="border:none; border-top:1px solid var(--border-soft); margin:8px 0;">
<label style="font-size:11px;">FormatPrefs.dateFormat:
<select id="fmt-pref" style="margin-top:4px; width:100%; padding:4px; font: inherit; font-size:11px;">
<option value="dd/MM/yyyy">dd/MM/yyyy</option>
<option value="MM/dd/yyyy">MM/dd/yyyy</option>
<option value="yyyy-MM-dd">yyyy-MM-dd</option>
</select>
</label>
</div>
<div class="overlay" role="dialog" aria-modal="true" aria-labelledby="dlg-title">
<header class="dlg-header">
<h2 id="dlg-title" class="dlg-title">New task</h2>
<p class="dlg-desc">Capture what needs doing. Set properties below or skip and refine later.</p>
</header>
<div class="dlg-body">
<input class="title-input" id="f-title" placeholder="What needs to be done?" autofocus />
<textarea class="desc-input" id="f-desc" rows="3" placeholder="Add a description…"></textarea>
<div class="props-label">Properties</div>
<div class="pills" id="pills">
<!-- Project pill -->
<span class="pop-anchor">
<button type="button" class="pill" data-pill="project" data-empty="true" tabindex="0">
<span class="pill-icon">📁</span>
<span class="pill-label">Project</span>
</button>
<div class="popover" data-popover="project" role="listbox">
<div class="pop-item" data-active="true" data-value="">
<span class="check"></span>No project
</div>
<div class="pop-item" data-value="acme-comm">
<span class="check"></span>Acme · Communications
</div>
<div class="pop-item" data-value="testing-bot">
<span class="check"></span>Testing · AI ChatBot
</div>
<div class="pop-item" data-value="adiuvai-app">
<span class="check"></span>AdiuvAI · App
</div>
</div>
</span>
<!-- Priority pill -->
<span class="pop-anchor">
<button type="button" class="pill" data-pill="priority" tabindex="0">
<span class="pill-icon pi-mid"></span>
<span class="pill-label">Priority</span>
<span class="pill-sep">·</span>
<span class="pill-value">Medium</span>
</button>
<div class="popover" data-popover="priority" role="listbox" style="min-width:160px;">
<div class="pop-item" data-value="high"><span class="check"></span>High</div>
<div class="pop-item" data-active="true" data-selected="true" data-value="medium"><span class="check"></span>Medium</div>
<div class="pop-item" data-value="low"><span class="check"></span>Low</div>
</div>
</span>
<!-- Status pill -->
<span class="pop-anchor">
<button type="button" class="pill" data-pill="status" tabindex="0">
<span class="pill-icon"></span>
<span class="pill-label">Status</span>
<span class="pill-sep">·</span>
<span class="pill-value">To do</span>
</button>
<div class="popover" data-popover="status" role="listbox" style="min-width:170px;">
<div class="pop-item" data-active="true" data-selected="true" data-value="todo"><span class="check"></span>To do</div>
<div class="pop-item" data-value="in_progress"><span class="check"></span>In progress</div>
<div class="pop-item" data-value="done"><span class="check"></span>Done</div>
</div>
</span>
<!-- Due pill -->
<span class="pop-anchor">
<button type="button" class="pill" data-pill="due" data-empty="true" tabindex="0">
<span class="pill-icon">📅</span>
<span class="pill-label">Due</span>
</button>
<div class="popover date-pop" data-popover="due" role="dialog" style="min-width:300px;">
<div class="field-label">Date</div>
<div class="datefield" id="datefield" tabindex="-1"><!-- segments injected by JS --></div>
<div class="cal" id="calendar">
<div class="cal-head">
<button type="button" data-nav="-1"></button>
<span class="month" id="cal-month">May 2026</span>
<button type="button" data-nav="1"></button>
</div>
<div class="cal-grid" id="cal-grid"><!-- filled by JS --></div>
</div>
</div>
</span>
<!-- Assignees pill -->
<span class="pop-anchor">
<button type="button" class="pill" data-pill="assignees" data-empty="true" tabindex="0">
<span class="pill-icon"></span>
<span class="pill-label">Add assignees</span>
</button>
<div class="popover" data-popover="assignees" role="listbox">
<div class="pop-item" data-active="true" data-value="alex"><span class="check"></span>Alex Morgan</div>
<div class="pop-item" data-value="priya"><span class="check"></span>Priya Shah</div>
<div class="pop-item" data-value="yo"><span class="check"></span>You</div>
</div>
</span>
</div>
</div>
<footer class="dlg-footer">
<div></div>
<div class="footer-actions">
<button type="button" class="btn">Cancel</button>
<button type="button" class="btn btn-primary">Create task</button>
</div>
</footer>
</div>
<script>
/* ---------- popover open/close + arrow nav ---------- */
const pills = document.querySelectorAll('.pill');
const popovers = document.querySelectorAll('.popover');
function closeAllPopovers() {
popovers.forEach((p) => p.setAttribute('data-open', 'false'));
}
const pillArr = Array.from(pills);
pills.forEach((pill) => {
pill.addEventListener('click', (e) => openPopoverFor(pill));
pill.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openPopoverFor(pill);
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
const idx = pillArr.indexOf(pill);
const next = pillArr[Math.min(idx + 1, pillArr.length - 1)];
next && next.focus();
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
const idx = pillArr.indexOf(pill);
const prev = pillArr[Math.max(idx - 1, 0)];
prev && prev.focus();
}
});
});
function openPopoverFor(pill) {
const which = pill.dataset.pill;
const pop = document.querySelector(`.popover[data-popover="${which}"]`);
if (!pop) return;
closeAllPopovers();
pop.setAttribute('data-open', 'true');
if (which === 'due') {
// focus first date segment
const firstSeg = pop.querySelector('.segment');
if (firstSeg) firstSeg.focus();
} else {
const items = pop.querySelectorAll('.pop-item');
items.forEach((i) => i.setAttribute('tabindex', '-1'));
const active = pop.querySelector('.pop-item[data-active="true"]') || items[0];
if (active) {
active.setAttribute('tabindex', '0');
active.focus();
}
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const open = document.querySelector('.popover[data-open="true"]');
if (open) {
e.preventDefault();
closePopover(open);
}
}
});
/* ---------- list popover keyboard ---------- */
popovers.forEach((pop) => {
if (pop.dataset.popover === 'due') return;
const items = Array.from(pop.querySelectorAll('.pop-item'));
items.forEach((it) => {
it.setAttribute('tabindex', '-1');
it.addEventListener('click', () => selectPopItem(pop, it));
it.addEventListener('keydown', (e) => onPopItemKey(e, pop, items, it));
});
});
function onPopItemKey(e, pop, items, item) {
const idx = items.indexOf(item);
if (e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
moveFocus(items, Math.min(idx + 1, items.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
moveFocus(items, Math.max(idx - 1, 0));
} else if (e.key === 'Home') {
e.preventDefault(); moveFocus(items, 0);
} else if (e.key === 'End') {
e.preventDefault(); moveFocus(items, items.length - 1);
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
selectPopItem(pop, item);
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
closePopover(pop);
} else if (e.key === 'Tab') {
closePopover(pop);
}
}
function moveFocus(items, target) {
items.forEach((i) => i.setAttribute('tabindex', '-1'));
const el = items[target];
el.setAttribute('tabindex', '0');
el.focus();
}
function closePopover(pop) {
pop.setAttribute('data-open', 'false');
const pill = document.querySelector(`.pill[data-pill="${pop.dataset.popover}"]`);
pill && pill.focus();
}
function selectPopItem(pop, item) {
const which = pop.dataset.popover;
if (which === 'assignees') {
item.toggleAttribute('data-selected');
} else {
pop.querySelectorAll('.pop-item').forEach((i) => i.removeAttribute('data-selected'));
item.setAttribute('data-selected', 'true');
}
updatePillFrom(pop);
if (which !== 'assignees') closePopover(pop);
}
function updatePillFrom(pop) {
const which = pop.dataset.popover;
const pill = document.querySelector(`.pill[data-pill="${which}"]`);
if (!pill) return;
if (which === 'assignees') {
const sel = Array.from(pop.querySelectorAll('.pop-item[data-selected="true"]'));
if (sel.length === 0) {
pill.setAttribute('data-empty', 'true');
pill.innerHTML = '<span class="pill-icon"></span><span class="pill-label">Add assignees</span>';
} else {
pill.removeAttribute('data-empty');
const names = sel.map((s) => s.textContent.trim());
pill.innerHTML = `<span class="pill-icon">👤</span><span class="pill-label">Assignees</span><span class="pill-sep">·</span><span class="pill-value">${names.join(', ')}</span>`;
}
return;
}
const cur = pop.querySelector('.pop-item[data-selected="true"]');
if (which === 'project') {
if (!cur || cur.dataset.value === '') {
pill.setAttribute('data-empty', 'true');
pill.innerHTML = '<span class="pill-icon">📁</span><span class="pill-label">Project</span>';
} else {
pill.removeAttribute('data-empty');
pill.innerHTML = `<span class="pill-icon">📁</span><span class="pill-label">Project</span><span class="pill-sep">·</span><span class="pill-value">${cur.textContent.trim()}</span>`;
}
} else if (which === 'priority') {
const v = cur.dataset.value;
const icon = v === 'high' ? '<span class="pill-icon pi-up">↑</span>'
: v === 'low' ? '<span class="pill-icon pi-down">↓</span>'
: '<span class="pill-icon pi-mid">→</span>';
const label = v[0].toUpperCase() + v.slice(1);
pill.innerHTML = `${icon}<span class="pill-label">Priority</span><span class="pill-sep">·</span><span class="pill-value">${label}</span>`;
} else if (which === 'status') {
const v = cur.dataset.value;
const icon = v === 'done' ? '✓' : v === 'in_progress' ? '◐' : '○';
const label = v === 'in_progress' ? 'In progress' : v === 'todo' ? 'To do' : 'Done';
pill.innerHTML = `<span class="pill-icon">${icon}</span><span class="pill-label">Status</span><span class="pill-sep">·</span><span class="pill-value">${label}</span>`;
}
}
/* ---------- DateField — format-aware segments ---------- */
const SEG_DEFS = {
day: { len: 2, min: 1, max: 31, ph: 'DD' },
month: { len: 2, min: 1, max: 12, ph: 'MM' },
year: { len: 4, min: 1900, max: 2100, ph: 'YYYY' },
hour: { len: 2, min: 0, max: 23, ph: 'HH' },
minute: { len: 2, min: 0, max: 59, ph: 'MM' },
};
const FMT_LAYOUT = {
'dd/MM/yyyy': [['day','/'],['month','/'],['year',null]],
'MM/dd/yyyy': [['month','/'],['day','/'],['year',null]],
'yyyy-MM-dd': [['year','-'],['month','-'],['day',null]],
};
let currentFmt = 'dd/MM/yyyy';
function renderDateField() {
const df = document.getElementById('datefield');
const cur = readDateField();
df.innerHTML = '';
const layout = FMT_LAYOUT[currentFmt].concat([null, ['hour',':'], ['minute', null]]);
layout.forEach((entry) => {
if (entry === null) {
const sp = document.createElement('span');
sp.className = 'seg-sep'; sp.innerHTML = '&nbsp;&nbsp;';
df.appendChild(sp); return;
}
const [key, sep] = entry;
const def = SEG_DEFS[key];
const seg = document.createElement('span');
seg.className = 'segment';
seg.contentEditable = 'true';
seg.dataset.seg = key;
seg.dataset.len = def.len; seg.dataset.min = def.min; seg.dataset.max = def.max;
const v = cur[key];
if (v == null) {
seg.dataset.placeholder = 'true';
seg.textContent = def.ph;
} else {
seg.dataset.placeholder = 'false';
seg.textContent = String(v).padStart(def.len, '0');
}
df.appendChild(seg);
if (sep) {
const s = document.createElement('span');
s.className = 'seg-sep'; s.textContent = sep;
df.appendChild(s);
}
});
bindDateSegments();
}
function bindDateSegments() {
const dfSegments = Array.from(document.querySelectorAll('.segment'));
dfSegments.forEach((seg, idx) => {
seg.addEventListener('focus', () => {
if (seg.dataset.placeholder === 'true') {
seg.textContent = '';
}
// select all
const range = document.createRange();
range.selectNodeContents(seg);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
});
seg.addEventListener('blur', () => {
const len = parseInt(seg.dataset.len, 10);
const min = parseInt(seg.dataset.min, 10);
const max = parseInt(seg.dataset.max, 10);
let v = seg.textContent.replace(/\D/g, '');
if (!v) {
seg.dataset.placeholder = 'true';
seg.textContent = seg.dataset.seg.toUpperCase().slice(0,len).padEnd(len, seg.dataset.seg[0].toUpperCase());
// reset to nice placeholder
const ph = { day:'DD', month:'MM', year:'YYYY', hour:'HH', minute:'MM' }[seg.dataset.seg];
seg.textContent = ph;
return;
}
let n = parseInt(v, 10);
if (n < min) n = min;
if (n > max) n = max;
seg.dataset.placeholder = 'false';
seg.textContent = String(n).padStart(len, '0');
refreshSelectedDay();
});
seg.addEventListener('keydown', (e) => {
const len = parseInt(seg.dataset.len, 10);
if (e.key === 'ArrowRight' || (e.key === '/' || e.key === ':') ) {
e.preventDefault();
const next = dfSegments[idx + 1];
if (next) next.focus();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
const prev = dfSegments[idx - 1];
if (prev) prev.focus();
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
const min = parseInt(seg.dataset.min, 10);
const max = parseInt(seg.dataset.max, 10);
const cur = parseInt(seg.textContent.replace(/\D/g,''), 10);
const base = isNaN(cur) ? min : cur;
let n = base + (e.key === 'ArrowUp' ? 1 : -1);
if (n < min) n = max;
if (n > max) n = min;
seg.dataset.placeholder = 'false';
seg.textContent = String(n).padStart(len, '0');
refreshSelectedDay();
} else if (/^\d$/.test(e.key)) {
const cur = seg.textContent.replace(/\D/g,'');
if (cur.length >= len) {
e.preventDefault();
seg.textContent = e.key;
// place caret at end
}
// when reaching len, advance to next segment after this char
setTimeout(() => {
if ((seg.textContent || '').replace(/\D/g,'').length >= len) {
const next = dfSegments[idx + 1];
if (next) next.focus();
}
}, 0);
} else if (e.key === 'Backspace' && seg.textContent === '') {
const prev = dfSegments[idx - 1];
if (prev) { e.preventDefault(); prev.focus(); }
} else if (e.key === 'Enter') {
e.preventDefault();
seg.blur();
const pop = document.querySelector('.popover[data-popover="due"]');
closePopover(pop);
updateDuePill();
} else if (e.key === 'Escape') {
e.preventDefault();
const pop = document.querySelector('.popover[data-popover="due"]');
closePopover(pop);
}
});
});
}
function readDateFieldFromDOM() {
return readDateField();
}
renderDateField();
document.getElementById('fmt-pref').addEventListener('change', (e) => {
currentFmt = e.target.value;
renderDateField();
updateDuePill();
});
function readDateField() {
const get = (k) => {
const s = document.querySelector(`.segment[data-seg="${k}"]`);
if (!s || s.dataset.placeholder === 'true') return null;
const v = s.textContent.replace(/\D/g,'');
return v ? parseInt(v, 10) : null;
};
return {
day: get('day'), month: get('month'), year: get('year'),
hour: get('hour'), minute: get('minute'),
};
}
function formatDateValue(d) {
const day = String(d.day).padStart(2,'0');
const month = String(d.month).padStart(2,'0');
const year = String(d.year);
switch (currentFmt) {
case 'MM/dd/yyyy': return `${month}/${day}/${year}`;
case 'yyyy-MM-dd': return `${year}-${month}-${day}`;
default: return `${day}/${month}/${year}`;
}
}
function updateDuePill() {
const d = readDateField();
const pill = document.querySelector('.pill[data-pill="due"]');
if (d.day && d.month && d.year) {
pill.removeAttribute('data-empty');
const time = d.hour != null && d.minute != null
? ` ${String(d.hour).padStart(2,'0')}:${String(d.minute).padStart(2,'0')}` : '';
pill.innerHTML = `<span class="pill-icon">📅</span><span class="pill-label">Due</span><span class="pill-sep">·</span><span class="pill-value">${formatDateValue(d)}${time}</span>`;
} else {
pill.setAttribute('data-empty', 'true');
pill.innerHTML = '<span class="pill-icon">📅</span><span class="pill-label">Due</span>';
}
}
/* ---------- Mini calendar ---------- */
let calYear = 2026, calMonth = 5; // May 2026
function renderCalendar() {
const grid = document.getElementById('cal-grid');
document.getElementById('cal-month').textContent =
new Date(calYear, calMonth - 1, 1).toLocaleString(undefined, { month: 'long', year: 'numeric' });
grid.innerHTML = '';
const dows = ['Mo','Tu','We','Th','Fr','Sa','Su'];
dows.forEach((d) => {
const el = document.createElement('div');
el.className = 'cal-dow'; el.textContent = d;
grid.appendChild(el);
});
const first = new Date(calYear, calMonth - 1, 1);
const offset = (first.getDay() + 6) % 7; // Mon-first
const daysInMonth = new Date(calYear, calMonth, 0).getDate();
const daysPrev = new Date(calYear, calMonth - 1, 0).getDate();
for (let i = offset - 1; i >= 0; i--) {
const el = document.createElement('div');
el.className = 'cal-day';
el.dataset.otherMonth = 'true';
el.textContent = daysPrev - i;
grid.appendChild(el);
}
for (let d = 1; d <= daysInMonth; d++) {
const el = document.createElement('div');
el.className = 'cal-day';
el.tabIndex = 0;
el.textContent = d;
el.dataset.day = d;
el.addEventListener('click', () => pickCalDay(d));
el.addEventListener('keydown', (e) => onCalKey(e, d));
grid.appendChild(el);
}
refreshSelectedDay();
}
function refreshSelectedDay() {
const d = readDateField();
const days = document.querySelectorAll('.cal-day[data-day]');
days.forEach((el) => el.removeAttribute('data-selected'));
if (d.day && d.month === calMonth && d.year === calYear) {
const tgt = document.querySelector(`.cal-day[data-day="${d.day}"]`);
if (tgt) tgt.setAttribute('data-selected', 'true');
}
}
function pickCalDay(d) {
const segDay = document.querySelector('.segment[data-seg="day"]');
const segMonth = document.querySelector('.segment[data-seg="month"]');
const segYear = document.querySelector('.segment[data-seg="year"]');
segDay.dataset.placeholder = 'false'; segDay.textContent = String(d).padStart(2,'0');
segMonth.dataset.placeholder = 'false'; segMonth.textContent = String(calMonth).padStart(2,'0');
segYear.dataset.placeholder = 'false'; segYear.textContent = String(calYear);
refreshSelectedDay();
updateDuePill();
}
function onCalKey(e, d) {
const grid = document.getElementById('cal-grid');
const days = Array.from(grid.querySelectorAll('.cal-day[data-day]'));
const idx = days.findIndex((el) => parseInt(el.dataset.day,10) === d);
let target = null;
if (e.key === 'ArrowRight') target = days[idx + 1];
else if (e.key === 'ArrowLeft') target = days[idx - 1];
else if (e.key === 'ArrowDown') target = days[idx + 7];
else if (e.key === 'ArrowUp') target = days[idx - 7];
else if (e.key === 'Enter') { e.preventDefault(); pickCalDay(d); return; }
if (target) { e.preventDefault(); target.focus(); }
}
document.querySelectorAll('[data-nav]').forEach((b) => {
b.addEventListener('click', () => {
const dir = parseInt(b.dataset.nav, 10);
calMonth += dir;
if (calMonth < 1) { calMonth = 12; calYear--; }
if (calMonth > 12) { calMonth = 1; calYear++; }
renderCalendar();
});
});
renderCalendar();
/* ---------- click outside closes popovers ---------- */
document.addEventListener('mousedown', (e) => {
const inPop = e.target.closest('.popover');
const inPill = e.target.closest('.pill');
if (!inPop && !inPill) closeAllPopovers();
});
</script>
</body>
</html>