Files
backlog/webapp/index-style-v2.html
T
2026-08-28 19:47:46 +02:00

9927 lines
471 KiB
HTML

<!doctype html>
<html lang="en" data-density="comfortable" data-show-ids="false" data-paper="true">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Personal Backlog</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* Personal Backlog — paper-like editor aesthetic */
:root {
--accent-hue: 35;
--accent: oklch(0.62 0.13 var(--accent-hue));
--accent-soft: oklch(0.62 0.13 var(--accent-hue) / 0.10);
--accent-edge: oklch(0.62 0.13 var(--accent-hue) / 0.35);
--accent-deep: oklch(0.48 0.14 var(--accent-hue));
--bg: oklch(0.985 0.005 80);
--bg-2: oklch(0.97 0.006 80);
--paper: oklch(0.99 0.004 80);
--ink: oklch(0.22 0.012 60);
--ink-2: oklch(0.42 0.012 60);
--ink-3: oklch(0.62 0.012 60);
--rule: oklch(0.91 0.008 70);
--rule-2: oklch(0.95 0.006 70);
--status-open: oklch(0.55 0.005 60);
--status-progress: oklch(0.55 0.13 220);
--status-blocked: oklch(0.58 0.16 25);
--status-postponed: oklch(0.68 0.15 60);
--status-done: oklch(0.55 0.12 145);
--status-cancelled: oklch(0.5 0.012 60);
--warn: oklch(0.7 0.13 80);
--warn-bg: oklch(0.94 0.05 85);
--ok: oklch(0.6 0.12 145);
--danger: oklch(0.58 0.16 25);
--danger-soft: oklch(0.58 0.16 25 / 0.08);
--font-sans: "Inter", system-ui, -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
--radius: 6px;
--radius-lg: 10px;
--shadow-1: 0 1px 0 oklch(0.91 0.008 70);
--shadow-2: 0 1px 2px oklch(0.22 0.012 60 / 0.06), 0 4px 14px oklch(0.22 0.012 60 / 0.05);
--shadow-pop: 0 8px 24px oklch(0.22 0.012 60 / 0.14), 0 2px 6px oklch(0.22 0.012 60 / 0.08);
--row-h: 36px;
--row-pad-y: 7px;
}
html[data-density="compact"] { --row-h: 28px; --row-pad-y: 4px; }
/* ---------- Dark theme ---------- */
html[data-theme="dark"] {
--bg: oklch(0.19 0.008 60);
--bg-2: oklch(0.23 0.009 60);
--paper: oklch(0.215 0.008 60);
--ink: oklch(0.93 0.006 70);
--ink-2: oklch(0.74 0.006 70);
--ink-3: oklch(0.56 0.008 70);
--rule: oklch(0.32 0.012 70);
--rule-2: oklch(0.27 0.010 70);
--status-open: oklch(0.72 0.008 60);
--status-progress: oklch(0.68 0.13 220);
--status-blocked: oklch(0.66 0.16 25);
--status-postponed: oklch(0.74 0.14 60);
--status-done: oklch(0.66 0.12 145);
--status-cancelled: oklch(0.55 0.012 60);
--warn: oklch(0.78 0.13 80);
--warn-bg: oklch(0.30 0.06 80);
--ok: oklch(0.7 0.12 145);
--danger: oklch(0.66 0.16 25);
--danger-soft: oklch(0.66 0.16 25 / 0.14);
--shadow-1: 0 1px 0 oklch(0 0 0 / 0.4);
--shadow-2: 0 1px 2px oklch(0 0 0 / 0.4), 0 4px 14px oklch(0 0 0 / 0.35);
--shadow-pop: 0 8px 24px oklch(0 0 0 / 0.55), 0 2px 6px oklch(0 0 0 / 0.45);
}
html[data-theme="dark"] .header { background: oklch(0.19 0.008 60 / 0.86); }
html[data-theme="dark"] .row:hover { background: oklch(0.27 0.010 60 / 0.6); }
html[data-theme="dark"] .btn-primary:hover:not(:disabled),
html[data-theme="dark"] .row-btn.primary:hover { background: oklch(0.85 0.008 70); color: var(--bg); }
html[data-theme="dark"] .banner.info { background: oklch(0.30 0.06 220); color: oklch(0.85 0.04 220); }
html[data-theme="dark"][data-paper="true"] body {
background-image: radial-gradient(oklch(0.55 0.008 70 / 0.18) 1px, transparent 1px);
}
html[data-theme="dark"] .restore-warn {
background: oklch(0.28 0.06 80);
border-color: oklch(0.45 0.10 80 / 0.6);
color: oklch(0.85 0.06 75);
}
html[data-theme="dark"] .restore-warn svg { color: oklch(0.78 0.13 70); }
html[data-theme="dark"] .restore-warn-title { color: oklch(0.92 0.05 75); }
html[data-theme="dark"] .restore-warn-body { color: oklch(0.78 0.04 70); }
html[data-theme="dark"] .restore-warn-body code { background: oklch(0 0 0 / 0.30); }
html[data-theme="dark"] .drag-handle { color: oklch(0.50 0.005 60); }
html[data-theme="dark"] .rte-toolbar { background: oklch(0.25 0.010 60); }
html[data-theme="dark"] .rte-code-block,
html[data-theme="dark"] .rte-inline-code { background: oklch(0.25 0.010 60); }
html[data-theme="dark"] .rte-table th { background: oklch(0.25 0.010 60); }
html[data-theme="dark"] .rte-table td { background: oklch(0.22 0.008 60); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
body {
font-family: var(--font-sans);
color: var(--ink);
background: var(--bg);
font-size: 14px;
line-height: 1.45;
-webkit-font-smoothing: antialiased;
}
html[data-paper="true"] body {
background-image: radial-gradient(oklch(0.91 0.008 70 / 0.35) 1px, transparent 1px);
background-size: 22px 22px;
}
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
button:focus-visible { outline: 2px solid var(--accent-edge); outline-offset: 2px; border-radius: 3px; }
button:disabled { cursor: not-allowed; opacity: 0.4; }
input { font: inherit; color: inherit; }
.mono { font-family: var(--font-mono); }
.small { font-size: 12px; }
.muted { color: var(--ink-3); }
/* ---------- App shell ---------- */
.app { min-height: 100vh; height: 100vh; display: flex; flex-direction: column; }
/* ---------- Header ---------- */
.header {
display: grid;
grid-template-columns: auto auto 1fr auto;
gap: 24px;
align-items: center;
padding: 14px 24px;
border-bottom: 1px solid var(--rule);
background: oklch(0.985 0.005 80 / 0.86);
backdrop-filter: saturate(1.4) blur(8px);
flex-shrink: 0;
z-index: 50;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-glyph { display: inline-flex; align-items: center; color: var(--accent); }
.brand-name { font-weight: 600; font-size: 15px; letter-spacing: -0.01em; }
.brand-sub {
font-size: 12px; color: var(--ink-3);
padding-left: 10px; border-left: 1px solid var(--rule); margin-left: 4px;
}
.tabs { display: flex; gap: 2px; }
.tab {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 12px; border-radius: var(--radius);
font-size: 13px; color: var(--ink-2); font-weight: 500;
}
.tab:hover { background: var(--bg-2); color: var(--ink); }
.tab.active { background: var(--ink); color: var(--bg); }
.search-wrap {
display: flex; align-items: center; gap: 8px;
padding: 7px 12px;
background: var(--paper);
border: 1px solid var(--rule); border-radius: var(--radius);
color: var(--ink-3);
max-width: 480px; width: 100%; justify-self: start;
}
.search-wrap:focus-within {
border-color: var(--accent-edge);
box-shadow: 0 0 0 3px var(--accent-soft);
color: var(--ink);
}
.search-input { flex: 1; border: 0; background: transparent; outline: 0; color: var(--ink); font-size: 13px; }
.search-input::placeholder { color: var(--ink-3); }
.search-clear {
width: 18px; height: 18px;
display: grid; place-items: center;
border-radius: 50%; color: var(--ink-3); font-size: 14px; line-height: 1;
}
.search-clear:hover { background: var(--bg-2); color: var(--ink); }
.header-right { display: flex; align-items: center; gap: 14px; }
.header-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 10px; border-radius: var(--radius);
font-size: 12.5px; color: var(--ink-2);
border: 1px solid var(--rule); background: var(--paper);
}
.header-btn:hover { background: var(--bg-2); color: var(--ink); }
.save-indicator { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ink-3); font-family: var(--font-mono); }
.save-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.save-dot.saving { background: var(--accent); animation: pulse 1.2s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: 0.4; } }
/* ---------- Banner ---------- */
.banner { display: flex; align-items: center; gap: 10px; padding: 9px 24px; font-size: 13px; border-bottom: 1px solid var(--rule); }
.banner.warn { background: var(--warn-bg); color: oklch(0.35 0.12 60); }
.banner.info { background: oklch(0.96 0.01 220); color: oklch(0.35 0.05 220); }
.banner-close { margin-left: auto; font-size: 12px; color: inherit; text-decoration: underline; text-underline-offset: 3px; }
/* ---------- Main layout ---------- */
.main { display: grid; grid-template-columns: 240px 1fr; flex: 1; min-height: 0; }
/* ---------- Filter panel ---------- */
.filter-panel {
border-right: 1px solid var(--rule);
padding: 22px 18px;
display: flex; flex-direction: column; gap: 22px;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
}
.filter-section { display: flex; flex-direction: column; gap: 8px; }
.filter-label {
font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
color: var(--ink-3); font-weight: 600;
}
.filter-list { display: flex; flex-direction: column; gap: 2px; }
.filter-list-row { flex-direction: row; gap: 6px; }
.filter-list-row.wrap { flex-wrap: wrap; }
.filter-chip {
display: flex; align-items: center; gap: 8px;
padding: 5px 8px; border-radius: 5px;
font-size: 13px; color: var(--ink-2); text-align: left;
}
.filter-chip:hover { background: var(--bg-2); color: var(--ink); }
.filter-chip.active { background: var(--accent-soft); color: var(--accent-deep); }
.filter-chip .filter-name { flex: 1; }
.filter-chip .filter-count { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); }
.filter-chip.active .filter-count { color: var(--accent-deep); }
.filter-chip.mini {
padding: 4px 10px; font-family: var(--font-mono); font-size: 12px; font-weight: 500;
border: 1px solid var(--rule); background: var(--paper);
}
.filter-chip.mini.active { border-color: var(--accent); background: var(--accent); color: var(--bg); }
.filter-chip.mini.pri-P0.active { background: var(--accent-deep); border-color: var(--accent-deep); }
.tag-chip {
font-family: var(--font-mono); font-size: 11.5px;
padding: 3px 8px; border-radius: 99px;
background: var(--bg-2); color: var(--ink-2);
border: 1px solid var(--rule);
display: inline-flex; align-items: center; gap: 3px;
}
.tag-chip:hover { background: var(--paper); color: var(--ink); }
.tag-chip.active { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.tag-chip .hl { color: var(--accent-deep); font-weight: 600; }
.tag-chip.active .hl { color: var(--bg); }
.tag-x { font-family: var(--font-sans); font-size: 13px; line-height: 1; opacity: 0.6; margin-left: 1px; }
.tag-chip:hover .tag-x { opacity: 1; }
.tag-total {
font-size: 10.5px; font-weight: 500;
color: var(--ink-3);
padding: 1px 6px; border-radius: 99px;
background: var(--bg-2); border: 1px solid var(--rule);
margin-left: 4px;
letter-spacing: 0;
}
.tag-selected { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 2px; }
.tag-search {
display: flex; align-items: center; gap: 6px;
padding: 6px 9px;
border: 1px solid var(--rule); border-radius: var(--radius);
background: var(--bg-2); color: var(--ink-3);
margin-bottom: 6px;
}
.tag-search.focused { border-color: var(--accent-edge); background: var(--paper); box-shadow: 0 0 0 3px var(--accent-soft); color: var(--ink); }
.tag-search-input { flex: 1; border: 0; background: transparent; outline: 0; font-size: 12.5px; color: var(--ink); padding: 0; min-width: 0; }
.tag-search-input::placeholder { color: var(--ink-3); }
.tag-search-clear { width: 16px; height: 16px; display: grid; place-items: center; border-radius: 50%; color: var(--ink-3); font-size: 14px; line-height: 1; flex-shrink: 0; }
.tag-search-clear:hover { background: var(--paper); color: var(--ink); }
.tag-results { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; max-height: 220px; overflow-y: auto; padding: 2px 0; }
.tag-empty { font-size: 11.5px; color: var(--ink-3); font-style: italic; padding: 4px 0; }
.tag-more { font-size: 10.5px; color: var(--ink-3); padding: 2px 4px; }
.clear-btn { padding: 6px 0; font-size: 12px; color: var(--accent-deep); text-align: left; text-decoration: underline; text-underline-offset: 3px; }
/* ---------- Content ---------- */
.content { min-width: 0; height: 100%; display: flex; flex-direction: column; overflow: hidden; }
.content-sticky {
flex-shrink: 0;
padding: 28px 32px 14px;
background: var(--bg);
border-bottom: 1px solid var(--rule-2);
}
.content-scroll {
flex: 1;
overflow-y: auto;
padding: 18px 32px 80px;
min-height: 0;
}
.content-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; margin-bottom: 14px; }
.content-head-actions { display: flex; align-items: center; gap: 12px; }
.eyebrow { font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-3); font-weight: 600; margin-bottom: 4px; }
.content-title { font-size: 26px; font-weight: 600; letter-spacing: -0.015em; margin: 0; }
.btn-primary, .btn-secondary, .btn-danger {
display: inline-flex; align-items: center; gap: 6px;
padding: 8px 14px; border-radius: var(--radius);
font-size: 13px; font-weight: 500;
}
.btn-primary { background: var(--ink); color: var(--bg); }
.btn-primary:hover:not(:disabled) { background: oklch(0.15 0.012 60); }
.btn-secondary { background: var(--paper); color: var(--ink); border: 1px solid var(--rule); }
.btn-secondary:hover { background: var(--bg-2); }
.btn-danger { background: var(--danger); color: white; }
.btn-danger:hover { background: oklch(0.5 0.16 25); }
/* segmented control */
.seg { display: inline-flex; padding: 2px; background: var(--bg-2); border: 1px solid var(--rule); border-radius: var(--radius); }
.seg-mini { padding: 2px; }
.seg-btn {
padding: 5px 10px; border-radius: 4px;
font-size: 12px; color: var(--ink-2);
font-family: var(--font-mono); font-weight: 500;
transition: background 0.12s, color 0.12s;
}
.seg-btn:hover { color: var(--ink); }
.seg-btn.active { background: var(--paper); color: var(--ink); box-shadow: 0 1px 2px oklch(0.22 0.012 60 / 0.08); }
/* in dialogs the segmented control should fill the row + have a stronger active state */
.field .seg { width: 100%; height: 38px; padding: 3px; align-items: stretch; }
.field .seg .seg-btn { flex: 1; padding: 0 12px; display: inline-flex; align-items: center; justify-content: center; gap: 5px; }
.field .seg .seg-btn.active {
background: var(--paper);
color: var(--ink);
box-shadow: 0 0 0 1px var(--ink) inset, 0 1px 2px oklch(0.22 0.012 60 / 0.08);
font-weight: 600;
}
.field .seg .seg-btn.pri-P0.active {
background: var(--accent);
color: var(--bg);
box-shadow: 0 0 0 1px var(--accent) inset;
}
.field .seg .seg-btn.pri-P0.active .p0-dot { background: var(--bg); box-shadow: 0 0 0 2px oklch(0.62 0.13 var(--accent-hue) / 0.4); }
.legend {
display: flex; flex-wrap: wrap; align-items: center;
gap: 14px; padding: 10px 12px; margin-bottom: 0;
font-size: 12px; color: var(--ink-3);
border: 1px dashed var(--rule); border-radius: var(--radius);
background: var(--paper);
}
/* Saved view chips */
.view-chips {
display: flex; flex-wrap: wrap; gap: 4px;
margin: 12px 0 10px;
}
.view-chip {
padding: 6px 12px;
border-radius: 99px;
font-size: 12.5px; font-weight: 500;
color: var(--ink-2);
border: 1px solid var(--rule);
background: var(--paper);
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.view-chip:hover { background: var(--bg-2); color: var(--ink); border-color: oklch(0.85 0.008 70); }
.view-chip.active {
background: var(--ink);
color: var(--bg);
border-color: var(--ink);
}
.legend-cap { font-weight: 600; color: var(--ink-2); letter-spacing: 0.04em; }
.legend-item { display: inline-flex; align-items: center; gap: 5px; color: var(--ink-2); }
.legend-sep { color: var(--rule); }
.legend-hint { margin-left: auto; }
.legend kbd {
font-family: var(--font-mono); font-size: 11px;
padding: 1px 6px;
border: 1px solid var(--rule); border-bottom-width: 2px; border-radius: 4px;
background: var(--paper); color: var(--ink-2);
}
/* ---------- Status icons (3 styles) ---------- */
.glyph-flat { display: inline-block; vertical-align: middle; }
.glyph-flat.status-open { color: var(--status-open); }
.glyph-flat.status-in-progress { color: var(--status-progress); }
.glyph-flat.status-blocked { color: var(--status-blocked); }
.glyph-flat.status-postponed { color: var(--status-postponed); }
.glyph-flat.status-done { color: var(--status-done); }
.glyph-flat.status-cancelled { color: var(--status-cancelled); }
.glyph-ascii {
font-family: var(--font-mono); font-size: 13px; font-weight: 500;
letter-spacing: -0.04em; display: inline-block; min-width: 26px; text-align: center;
}
.glyph-ascii.status-open { color: var(--status-open); }
.glyph-ascii.status-in-progress { color: var(--status-progress); font-weight: 600; }
.glyph-ascii.status-blocked { color: var(--status-blocked); font-weight: 600; }
.glyph-ascii.status-postponed { color: var(--status-postponed); }
.glyph-ascii.status-done { color: var(--status-done); }
.glyph-ascii.status-cancelled { color: var(--status-cancelled); text-decoration: line-through; text-decoration-thickness: 1px; }
.glyph-emoji {
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
line-height: 1;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
/* normalize size of the variation-selector pictographs */
font-variant-emoji: emoji;
}
.glyph-color { display: inline-block; vertical-align: middle; }
.restore-warn {
display: flex; gap: 10px;
margin-top: 12px;
padding: 10px 12px;
border-radius: var(--radius);
border: 1px solid oklch(0.82 0.13 85 / 0.6);
background: oklch(0.97 0.05 85);
color: oklch(0.36 0.10 70);
}
.restore-warn svg { flex-shrink: 0; margin-top: 2px; color: oklch(0.62 0.16 70); }
.restore-warn-title { font-weight: 600; font-size: 12.5px; margin-bottom: 3px; color: oklch(0.32 0.08 70); }
.restore-warn-body { font-size: 12px; line-height: 1.45; color: oklch(0.40 0.06 70); }
.restore-warn-body code { background: oklch(1 0 0 / 0.55); padding: 0 4px; border-radius: 3px; }
/* ---------- Tree rows ---------- */
.tree {
background: var(--paper);
border: 1px solid var(--rule);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-2);
}
.row {
display: flex; align-items: center; gap: 10px;
padding: var(--row-pad-y) 16px;
min-height: var(--row-h);
border-bottom: 1px solid var(--rule-2);
position: relative;
font-size: 13.5px;
}
.row:last-child { border-bottom: 0; }
.row:hover { background: oklch(0.985 0.005 80 / 0.7); }
.row.dimmed { opacity: 0.45; }
.row.drop-target { box-shadow: inset 0 2px 0 var(--accent); background: var(--accent-soft); }
.row.p0 { background: linear-gradient(90deg, var(--accent-soft) 0%, transparent 60%); }
.row.p0::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: var(--accent); }
.level-0 .title { font-weight: 600; font-size: 14.5px; }
.level-1 .title { font-weight: 500; }
.level-2 .title, .level-3 .title { color: var(--ink-2); }
.row-rail { align-self: stretch; flex-shrink: 0; }
.twisty { width: 16px; height: 16px; display: grid; place-items: center; color: var(--ink-3); border-radius: 3px; flex-shrink: 0; }
.twisty:hover { background: var(--bg-2); color: var(--ink); }
.twisty.empty { visibility: hidden; }
.drag-handle { width: 14px; color: oklch(0.85 0.005 60); cursor: grab; flex-shrink: 0; opacity: 0; transition: opacity 0.15s; }
.row:hover .drag-handle { opacity: 1; }
/* status button + popover */
.status-wrap { position: relative; display: inline-flex; }
.status-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 24px;
border-radius: 5px;
flex-shrink: 0;
}
.status-btn:hover { background: var(--bg-2); }
.status-pop {
position: absolute; top: calc(100% + 4px); left: 0;
z-index: 30; min-width: 180px;
padding: 4px;
background: var(--paper); border: 1px solid var(--rule);
border-radius: var(--radius); box-shadow: var(--shadow-pop);
}
.status-pop-item {
width: 100%; display: flex; align-items: center; gap: 8px;
padding: 6px 8px; border-radius: 4px;
font-size: 13px; color: var(--ink); text-align: left;
}
.status-pop-item:hover { background: var(--bg-2); }
.status-pop-item.active { background: var(--accent-soft); color: var(--accent-deep); }
/* priority button + popover */
.pri-wrap { position: relative; display: inline-flex; }
.pri {
font-family: var(--font-mono);
font-size: 11px; font-weight: 600;
color: var(--ink-3); letter-spacing: 0.04em;
flex-shrink: 0;
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 7px; border-radius: 4px;
border: 1px solid var(--rule); background: var(--paper);
}
.pri:hover { background: var(--bg-2); border-color: oklch(0.85 0.008 70); }
.pri-P0 { color: var(--accent-deep); border-color: var(--accent-edge); background: var(--accent-soft); }
.pri-P1 { color: var(--ink); }
.pri-P2 { color: var(--ink-2); }
.pri-P3 { color: var(--ink-3); }
.p0-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.pri-pop {
position: absolute; top: calc(100% + 4px); left: 0;
z-index: 30; min-width: 160px; padding: 4px;
background: var(--paper); border: 1px solid var(--rule);
border-radius: var(--radius); box-shadow: var(--shadow-pop);
}
.pri-pop-item {
width: 100%;
display: grid; grid-template-columns: 14px 32px 1fr; align-items: center; gap: 8px;
padding: 6px 8px; border-radius: 4px;
font-family: var(--font-mono); font-size: 12px;
color: var(--ink); text-align: left;
}
.pri-pop-item:hover { background: var(--bg-2); }
.pri-pop-item.active { background: var(--accent-soft); color: var(--accent-deep); }
.pri-pop-item .p0-dot { grid-column: 1; }
.pri-pop-hint { font-family: var(--font-sans); font-size: 11.5px; color: var(--ink-3); justify-self: end; }
.title { flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: text; }
.title.done { color: var(--ink-3); text-decoration: line-through; text-decoration-thickness: 1px; }
.title .hl { background: var(--accent-soft); color: var(--accent-deep); padding: 0 1px; border-radius: 2px; }
/* ---------- Progress gauge ---------- */
.gauge {
display: inline-flex; align-items: center; gap: 4px;
flex-shrink: 0;
padding: 1px 6px 1px 4px;
border-radius: 99px;
background: var(--bg-2);
border: 1px solid var(--rule);
user-select: none;
line-height: 1;
}
.gauge-seg {
display: inline-block;
width: 4px; height: 8px;
border-radius: 1px;
background: oklch(0.88 0.005 60);
transition: background 0.12s;
}
.gauge-seg.on { background: var(--status-progress); }
.gauge-num {
font-family: var(--font-mono);
font-size: 10.5px;
font-variant-numeric: tabular-nums;
color: var(--ink-3);
padding-left: 3px;
min-width: 26px;
text-align: right;
}
/* state-coloring: 100 = green, 0 = neutral, mid = blue (default) */
.gauge.gauge-v-100 .gauge-seg.on { background: var(--status-done); }
.gauge.gauge-v-100 .gauge-num { color: var(--status-done); }
.gauge.gauge-v-0 { border-style: dashed; }
.gauge.gauge-v-0 .gauge-num { color: var(--ink-3); }
/* row gauge: hover-emphasize when interactive */
.gauge-interactive { cursor: pointer; transition: background 0.12s, border-color 0.12s; }
.gauge-interactive:hover { background: var(--paper); border-color: var(--accent-edge); }
.gauge-interactive:focus-visible { outline: 2px solid var(--accent-edge); outline-offset: 2px; }
/* larger size for dialog */
.gauge-md { padding: 4px 10px 4px 7px; gap: 5px; }
.gauge-md .gauge-seg { width: 5px; height: 12px; }
.gauge-md .gauge-num { font-size: 12px; min-width: 32px; padding-left: 4px; }
/* dialog progress field */
.progress-row { display: flex; align-items: center; gap: 12px; }
.progress-seg { flex: 1; }
.progress-seg .seg-btn { font-family: var(--font-mono); }
.progress-seg.locked { opacity: 0.55; }
/* row dim adjustment: lower opacity for blocked-ish stuck rows */
.row .gauge { margin-left: 2px; }
.due { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-3); padding: 1px 6px; border-radius: 99px; background: var(--bg-2); flex-shrink: 0; }
.due.overdue { color: oklch(0.95 0.005 60); background: var(--status-blocked); font-weight: 500; }
.tags { display: inline-flex; gap: 4px; flex-shrink: 0; }
.tag { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-2); padding: 1px 6px; border-radius: 99px; border: 1px solid var(--rule); background: var(--paper); }
.reason { font-style: italic; font-size: 12px; color: var(--status-blocked); flex-shrink: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 240px; }
.row-spacer { flex: 1; min-width: 0; }
/* row action toolbar — bigger, distinct, labeled on hover */
.row-actions { display: flex; gap: 2px; flex-shrink: 0; opacity: 0; transition: opacity 0.15s; align-items: center; }
.row:hover .row-actions, .row:focus-within .row-actions { opacity: 1; }
.action-sep { width: 1px; height: 16px; background: var(--rule); margin: 0 4px; }
/* custom tooltip on icon buttons (instant, branded) */
.icon-btn { position: relative; }
.icon-btn[data-tip]::after {
content: attr(data-tip);
position: absolute;
top: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
background: var(--ink);
color: var(--bg);
font-family: var(--font-sans);
font-size: 11.5px;
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
pointer-events: none;
opacity: 0;
transition: opacity 0.12s 0s;
z-index: 100;
box-shadow: 0 4px 12px oklch(0.22 0.012 60 / 0.2);
}
.icon-btn[data-tip]::before {
content: "";
position: absolute;
top: calc(100% + 1px);
left: 50%;
transform: translateX(-50%);
border: 4px solid transparent;
border-bottom-color: var(--ink);
pointer-events: none;
opacity: 0;
transition: opacity 0.12s 0s;
z-index: 100;
}
.icon-btn[data-tip]:hover::after,
.icon-btn[data-tip]:hover::before,
.icon-btn[data-tip]:focus-visible::after,
.icon-btn[data-tip]:focus-visible::before {
opacity: 1;
transition-delay: 0.35s;
}
.icon-btn {
width: 28px; height: 28px;
display: inline-grid; place-items: center;
border-radius: 5px;
color: var(--ink-2);
border: 1px solid transparent;
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.icon-btn:hover:not(:disabled) {
background: var(--paper);
color: var(--ink);
border-color: var(--rule);
}
.icon-btn.danger:hover:not(:disabled) {
color: var(--danger);
background: var(--danger-soft);
border-color: oklch(0.58 0.16 25 / 0.3);
}
.empty-state { text-align: center; padding: 60px 20px; color: var(--ink-3); background: var(--paper); border: 1px dashed var(--rule); border-radius: var(--radius-lg); }
.empty-glyph { font-family: var(--font-mono); font-size: 36px; margin-bottom: 8px; color: var(--rule); }
/* ---------- Admin ---------- */
.admin { padding: 28px 32px 80px; max-width: 1280px; margin: 0 auto; width: 100%; overflow-y: auto; height: 100%; }
.admin-head { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 24px; }
.admin-head h2 { font-size: 26px; font-weight: 600; letter-spacing: -0.015em; margin: 0; }
.admin-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.card { background: var(--paper); border: 1px solid var(--rule); border-radius: var(--radius-lg); padding: 18px 20px; box-shadow: var(--shadow-2); }
.card.span-2 { grid-column: span 2; }
.card.span-3 { grid-column: span 3; }
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; gap: 10px; }
.card-head h3 { font-size: 14px; font-weight: 600; margin: 0; letter-spacing: -0.005em; }
.card-sub { font-size: 11.5px; color: var(--ink-3); }
.pill { display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 99px; border: 1px solid var(--rule); }
.pill.mini { font-size: 10.5px; padding: 1px 6px; }
.pill.ok { color: var(--ok); border-color: var(--ok); background: oklch(0.95 0.04 145); }
.pill.warn { color: oklch(0.45 0.12 60); border-color: var(--warn); background: var(--warn-bg); }
.pill.neutral { color: var(--ink-2); background: var(--bg-2); }
.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px; margin: 0; }
.kv > div { display: flex; flex-direction: column; gap: 1px; padding: 6px 0; border-top: 1px dashed var(--rule); }
.kv > div:first-child, .kv > div:nth-child(2) { border-top: 0; padding-top: 0; }
.kv dt { font-size: 11px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; margin: 0; }
.kv dd { margin: 0; font-size: 13px; color: var(--ink); }
.action-list { display: flex; flex-direction: column; gap: 6px; }
.action-btn { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; border-radius: var(--radius); border: 1px solid var(--rule); background: var(--bg); font-size: 13px; text-align: left; line-height: 1.35; }
.action-btn:hover { background: var(--bg-2); border-color: oklch(0.85 0.008 70); }
.action-btn svg { flex-shrink: 0; margin-top: 2px; color: var(--ink-2); }
.action-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.action-label { font-weight: 500; color: var(--ink); font-size: 13px; }
.action-meta { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-3); letter-spacing: 0.01em; }
/* ---- Stats overview ---- */
.stats-card { display: flex; flex-direction: column; gap: 16px; }
.stats-head { flex-wrap: wrap; gap: 12px; }
.range-switch {
display: inline-flex; flex-wrap: wrap;
background: var(--bg);
border: 1px solid var(--rule);
border-radius: var(--radius);
padding: 2px;
gap: 1px;
}
.range-btn {
padding: 5px 10px;
font-size: 11.5px; font-weight: 500;
color: var(--ink-3);
border-radius: 4px;
white-space: nowrap;
transition: background 0.12s, color 0.12s;
}
.range-btn:hover { color: var(--ink-2); background: var(--bg-2); }
.range-btn.active { background: var(--ink); color: var(--bg); }
.kpi-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1px;
background: var(--rule);
border: 1px solid var(--rule);
border-radius: var(--radius);
overflow: hidden;
}
.kpi {
background: var(--paper);
padding: 12px 14px;
display: flex; flex-direction: column; gap: 6px;
min-width: 0;
}
.kpi-accent { background: oklch(from var(--accent) l c h / 0.04); }
.kpi-label { font-size: 10.5px; font-weight: 600; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; }
.kpi-value { font-family: var(--font-mono); font-size: 24px; font-weight: 500; letter-spacing: -0.02em; line-height: 1; color: var(--ink); }
.kpi-unit { font-size: 13px; color: var(--ink-3); margin-left: 2px; font-weight: 400; }
.kpi-delta { font-family: var(--font-mono); font-size: 10.5px; display: flex; align-items: center; gap: 4px; }
.kpi-delta.up { color: oklch(0.55 0.13 145); }
.kpi-delta.down { color: oklch(0.58 0.16 30); }
.kpi-delta.flat { color: var(--ink-3); }
.kpi-arrow { font-size: 9px; }
.kpi-vs { color: var(--ink-3); font-weight: 400; }
.chart-head {
display: flex; justify-content: space-between; align-items: center;
gap: 12px; flex-wrap: wrap;
padding-top: 6px;
border-top: 1px dashed var(--rule);
padding-top: 14px;
}
.chart-legend { display: flex; flex-wrap: wrap; gap: 4px; }
.legend-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 8px;
font-size: 11px; color: var(--ink-2);
border: 1px solid transparent;
border-radius: 99px;
transition: opacity 0.12s, border-color 0.12s;
}
.legend-chip:hover { border-color: var(--rule); }
.legend-chip.off { opacity: 0.4; }
.legend-chip.off .lg-swatch { filter: grayscale(0.8); }
.lg-swatch { display: inline-block; width: 9px; height: 9px; border-radius: 2px; }
.mode-switch {
display: inline-flex;
background: var(--bg);
border: 1px solid var(--rule);
border-radius: var(--radius);
padding: 2px;
gap: 1px;
}
.mode-btn {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 9px;
font-size: 11.5px; font-weight: 500;
color: var(--ink-3);
border-radius: 4px;
}
.mode-btn:hover { color: var(--ink-2); background: var(--bg-2); }
.mode-btn.active { background: var(--paper); color: var(--ink); box-shadow: 0 0 0 1px var(--rule); }
.mode-btn svg { color: currentColor; }
.stack-bars {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 4px;
align-items: end;
height: 160px;
padding: 0 2px;
}
.stack-col { display: flex; flex-direction: column; gap: 6px; height: 100%; min-width: 0; }
.stack-stack {
position: relative;
flex: 1;
min-height: 0;
border-radius: 3px 3px 0 0;
overflow: hidden;
align-self: stretch;
}
.stack-seg {
position: absolute;
left: 0; right: 0;
width: 100%;
min-height: 1px;
transition: height 0.25s ease, bottom 0.25s ease;
box-sizing: border-box;
}
/* round the topmost segment of each stack */
.stack-seg.top { border-radius: 3px 3px 0 0; }
.cumulative-stack { border-radius: 3px 3px 0 0; }
.stack-seg.cumulative {
background: var(--accent);
border-radius: 3px 3px 0 0;
box-shadow: none;
}
.legend-chip.static { cursor: default; opacity: 1; padding: 3px 8px; }
.stack-cap {
font-size: 10.5px; color: var(--ink-3);
font-family: var(--font-mono);
text-align: center;
display: flex; flex-direction: column; align-items: center; gap: 1px;
min-height: 16px;
line-height: 1;
}
.stack-total { font-size: 9.5px; color: var(--ink-3); opacity: 0.7; }
/* dense buckets — collapse columns to fit */
.stack-bars.buckets-30 { gap: 2px; }
.stack-bars.buckets-30 .stack-cap { font-size: 9.5px; }
/* legacy stat block styles kept for any external refs but no longer used */
.stats-row, .stat-block, .bar-row, .bar-col, .bar { /* retained */ }
.stat-block { padding: 12px 14px; border-radius: var(--radius); background: var(--bg); border: 1px solid var(--rule); }
.stat-num { font-family: var(--font-mono); font-size: 28px; font-weight: 500; letter-spacing: -0.03em; line-height: 1; }
.stat-num.small { font-family: var(--font-sans); font-size: 14px; font-weight: 600; }
.stat-unit { font-size: 14px; color: var(--ink-3); margin-left: 2px; }
.stat-cap { font-size: 11px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 6px; }
.bar-row { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; align-items: end; padding-top: 16px; border-top: 1px dashed var(--rule); }
.bar-col { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.bar { width: 100%; background: var(--accent); border-radius: 3px 3px 0 0; min-height: 4px; }
.bar-cap { font-size: 11px; color: var(--ink-3); }
.bar-num { font-family: var(--font-mono); font-size: 11px; color: var(--ink-2); }
.status-mix { display: flex; flex-direction: column; gap: 7px; }
.mix-row { display: grid; grid-template-columns: 30px 1fr 80px 30px; align-items: center; gap: 8px; font-size: 12.5px; }
.mix-label { color: var(--ink-2); }
.mix-bar { height: 6px; background: var(--bg-2); border-radius: 99px; overflow: hidden; }
.mix-fill { display: block; height: 100%; background: var(--ink-2); border-radius: 99px; }
.mix-num { font-family: var(--font-mono); font-size: 11.5px; text-align: right; color: var(--ink-2); }
.backup-table, .history-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.backup-table th, .history-table th { text-align: left; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3); padding: 6px 8px; border-bottom: 1px solid var(--rule); }
.backup-table td, .history-table td { padding: 9px 8px; border-bottom: 1px solid var(--rule-2); }
.backup-table tr.active { background: var(--accent-soft); }
.row-actions-cell { display: flex; gap: 4px; justify-content: flex-end; }
.row-btn { display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 4px; font-size: 11.5px; color: var(--ink-2); border: 1px solid var(--rule); background: var(--paper); }
.row-btn:hover { background: var(--bg-2); }
.row-btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.row-btn.primary:hover { background: oklch(0.15 0.012 60); }
.backup-preview { margin-top: 12px; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--bg); overflow: hidden; }
.preview-head { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--rule); background: var(--bg-2); }
.preview-body { margin: 0; padding: 14px 16px; font-family: var(--font-mono); font-size: 11.5px; line-height: 1.55; color: var(--ink-2); white-space: pre-wrap; max-height: 320px; overflow: auto; }
/* ---------- Dialog system (centered, properly scrolled) ---------- */
.dlg-backdrop {
position: fixed; inset: 0;
display: flex; align-items: center; justify-content: center;
background: oklch(0.22 0.012 60 / 0.42);
backdrop-filter: blur(3px);
z-index: 200;
padding: 24px;
animation: dlg-fade 0.15s ease;
}
@keyframes dlg-fade { from { opacity: 0; } }
.dlg {
width: 100%;
max-height: calc(100vh - 48px);
background: var(--paper);
border: 1px solid var(--rule);
border-radius: 12px;
box-shadow: 0 30px 80px oklch(0.22 0.012 60 / 0.28), 0 4px 12px oklch(0.22 0.012 60 / 0.12);
display: flex; flex-direction: column;
overflow: hidden;
animation: dlg-pop 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.1);
}
@keyframes dlg-pop { from { opacity: 0; transform: translateY(8px) scale(0.985); } }
.dlg form { display: contents; }
.dlg-head {
display: flex; align-items: flex-start; justify-content: space-between;
padding: 18px 22px 14px;
border-bottom: 1px solid var(--rule-2);
gap: 16px;
}
.dlg-title { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; margin: 0; color: var(--ink); }
.dlg-close {
width: 28px; height: 28px;
display: grid; place-items: center;
border-radius: 6px;
color: var(--ink-3);
flex-shrink: 0;
}
.dlg-close:hover { background: var(--bg-2); color: var(--ink); }
.dlg-body {
padding: 18px 22px;
overflow-y: auto;
flex: 1;
min-height: 0;
}
.dlg-msg { font-size: 14px; color: var(--ink); margin: 0 0 8px; }
.dlg-detail { font-size: 12.5px; color: var(--ink-3); padding: 8px 12px; background: var(--bg-2); border: 1px dashed var(--rule); border-radius: 6px; }
.dlg-foot {
display: flex; align-items: center; justify-content: space-between;
gap: 12px;
padding: 14px 22px;
border-top: 1px solid var(--rule-2);
background: var(--bg-2);
}
.dlg-foot-actions { display: flex; gap: 8px; }
.dlg-hint { font-size: 11.5px; color: var(--ink-3); font-family: var(--font-mono); }
/* dialog form fields */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.field:last-child { margin-bottom: 0; }
.field-row { display: grid; grid-template-columns: 1.5fr 1fr; gap: 16px; margin-bottom: 16px; }
.field-row .field { margin-bottom: 0; }
.field-label {
font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
color: var(--ink-3); font-weight: 600;
display: flex; align-items: baseline; gap: 8px;
}
.field-hint {
font-size: 11px; letter-spacing: 0; text-transform: none;
font-weight: 400; color: var(--ink-3); font-style: italic;
}
.text-input {
width: 100%;
height: 38px;
padding: 0 12px;
font-size: 13.5px;
border: 1px solid var(--rule);
border-radius: var(--radius);
background: var(--bg);
color: var(--ink);
outline: 0;
transition: border-color 0.12s, box-shadow 0.12s;
}
.text-input[type="date"] { font-family: var(--font-mono); font-size: 13px; }
.text-input::placeholder { color: var(--ink-3); }
.text-input:focus { border-color: var(--accent-edge); box-shadow: 0 0 0 3px var(--accent-soft); background: var(--paper); }
/* status grid in dialog */
.status-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
}
.status-card {
display: flex; align-items: center; gap: 8px;
padding: 9px 11px; border-radius: var(--radius);
border: 1px solid var(--rule); background: var(--bg);
font-size: 12.5px; color: var(--ink-2);
text-align: left;
transition: background 0.12s, border-color 0.12s;
}
.status-card:hover { background: var(--paper); color: var(--ink); }
.status-card.active { background: var(--paper); border-color: var(--ink); color: var(--ink); box-shadow: 0 0 0 1px var(--ink) inset; }
.status-card-icon { display: inline-flex; flex-shrink: 0; }
.status-card-label { font-weight: 500; }
/* tag input */
.tag-input {
display: flex; flex-wrap: wrap; gap: 4px;
padding: 6px 8px;
border: 1px solid var(--rule);
border-radius: var(--radius);
background: var(--bg);
min-height: 38px;
cursor: text;
}
.tag-input:hover { border-color: oklch(0.85 0.008 70); }
.tag-input:focus-within { border-color: var(--accent-edge); box-shadow: 0 0 0 3px var(--accent-soft); background: var(--paper); }
.tag-pill {
display: inline-flex; align-items: center; gap: 4px;
font-family: var(--font-mono); font-size: 11.5px;
padding: 2px 4px 2px 8px;
background: var(--paper); border: 1px solid var(--rule); border-radius: 99px;
color: var(--ink);
}
.tag-pill button {
width: 16px; height: 16px;
display: grid; place-items: center;
border-radius: 50%;
color: var(--ink-3);
font-size: 14px; line-height: 1;
}
.tag-pill button:hover { background: var(--bg-2); color: var(--danger); }
.tag-input-field { flex: 1; min-width: 80px; border: 0; outline: 0; background: transparent; padding: 4px; font-size: 13px; }
.tag-input-field::placeholder { color: var(--ink-3); }
.tag-suggest { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; align-items: center; }
.tag-suggest-cap { font-size: 11px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; margin-right: 4px; }
.tag-suggest-chip {
font-family: var(--font-mono); font-size: 11px;
padding: 2px 8px; border-radius: 99px;
background: var(--bg); border: 1px dashed var(--rule); color: var(--ink-2);
}
.tag-suggest-chip:hover { background: var(--paper); color: var(--ink); border-style: solid; border-color: var(--accent-edge); }
/* Tag autocomplete dropdown */
.tag-ac { position: relative; }
.tag-ac-pop {
position: absolute;
top: calc(100% + 4px); left: 0; right: 0;
background: var(--paper);
border: 1px solid var(--rule);
border-radius: var(--radius);
box-shadow: var(--shadow-pop);
padding: 4px;
z-index: 50;
max-height: 280px;
overflow-y: auto;
}
.tag-ac-cap {
font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
color: var(--ink-3); font-weight: 600;
padding: 6px 8px 4px;
}
.tag-ac-item {
display: flex; align-items: center; gap: 6px;
width: 100%; text-align: left;
padding: 6px 9px; border-radius: 4px;
font-family: var(--font-mono); font-size: 12.5px;
color: var(--ink); background: transparent;
}
.tag-ac-item.highlighted { background: var(--accent-soft); color: var(--accent-deep); }
.tag-ac-item .hl { color: var(--accent-deep); font-weight: 600; }
.tag-ac-item.highlighted .hl { color: var(--accent-deep); background: oklch(0.62 0.13 var(--accent-hue) / 0.18); padding: 0 1px; border-radius: 2px; }
.tag-ac-item.create { color: var(--accent-deep); }
.tag-ac-item.create strong { font-weight: 600; }
.tag-ac-foot {
display: flex; gap: 12px; justify-content: flex-end;
padding: 6px 9px 4px;
border-top: 1px dashed var(--rule);
margin-top: 4px;
font-size: 10.5px; color: var(--ink-3);
}
.tag-ac-foot kbd {
font-family: var(--font-mono); font-size: 10px;
padding: 1px 4px; margin-right: 2px;
border: 1px solid var(--rule); border-bottom-width: 2px; border-radius: 3px;
background: var(--bg-2); color: var(--ink-2);
}
/* ---------- Rich Text Editor ---------- */
.rte-wrap {
border: 1px solid var(--rule);
border-radius: var(--radius);
background: var(--bg);
overflow: hidden;
transition: border-color 0.12s, box-shadow 0.12s;
}
.rte-wrap:focus-within {
border-color: var(--accent-edge);
box-shadow: 0 0 0 3px var(--accent-soft);
background: var(--paper);
}
.rte-toolbar {
display: flex;
align-items: center;
gap: 2px;
padding: 4px 6px;
border-bottom: 1px solid var(--rule);
background: var(--bg-2);
flex-wrap: wrap;
}
.rte-toolbar button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
background: transparent;
transition: background 0.1s, color 0.1s;
}
.rte-toolbar button:hover {
background: var(--paper);
color: var(--ink);
}
.rte-toolbar-sep {
width: 1px;
height: 18px;
background: var(--rule);
margin: 0 4px;
}
.rte-copy-btn {
width: auto !important;
padding: 0 6px;
gap: 3px;
font-size: 11px;
font-weight: 500;
}
.rte-copy-menu {
position: fixed;
background: var(--paper);
border: 1px solid var(--rule);
border-radius: var(--radius);
box-shadow: var(--shadow-pop);
padding: 4px;
z-index: 100;
min-width: 160px;
}
.rte-copy-menu button {
display: block;
width: 100%;
text-align: left;
padding: 7px 10px;
border-radius: 4px;
font-size: 12.5px;
color: var(--ink);
}
.rte-copy-menu button:hover {
background: var(--accent-soft);
color: var(--accent-deep);
}
.rte-editor {
min-height: 80px;
max-height: 240px;
overflow-y: auto;
padding: 10px 12px;
font-size: 13px;
line-height: 1.55;
color: var(--ink);
outline: none;
}
.rte-editor:empty::before {
content: attr(data-placeholder);
color: var(--ink-3);
pointer-events: none;
}
.rte-editor p { margin: 0 0 0.75em; }
.rte-editor p:last-child { margin-bottom: 0; }
.rte-editor h1, .rte-editor h2, .rte-editor h3 {
margin: 0.8em 0 0.4em;
font-weight: 600;
line-height: 1.3;
}
.rte-editor h1:first-child,
.rte-editor h2:first-child,
.rte-editor h3:first-child { margin-top: 0; }
.rte-editor h1 { font-size: 1.3em; }
.rte-editor h2 { font-size: 1.15em; }
.rte-editor h3 { font-size: 1em; }
.rte-editor ul, .rte-editor ol {
margin: 0.5em 0;
padding-left: 1.5em;
}
.rte-editor li { margin: 0.25em 0; }
.rte-editor a {
color: var(--accent-deep);
text-decoration: underline;
text-underline-offset: 2px;
}
.rte-editor a:hover { color: var(--accent); }
.rte-editor strong { font-weight: 600; }
.rte-editor em { font-style: italic; }
.rte-inline-code {
font-family: var(--font-mono);
font-size: 0.9em;
padding: 1px 5px;
background: var(--bg-2);
border-radius: 3px;
color: var(--ink);
}
.rte-code-block {
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
padding: 10px 12px;
background: var(--bg-2);
border-radius: var(--radius);
margin: 0.75em 0;
overflow-x: auto;
white-space: pre;
}
.rte-code-block code {
background: none;
padding: 0;
font-size: inherit;
}
.rte-table {
width: 100%;
border-collapse: collapse;
margin: 0.75em 0;
font-size: 12.5px;
}
.rte-table th, .rte-table td {
border: 1px solid var(--rule);
padding: 6px 10px;
text-align: left;
}
.rte-table th {
background: var(--bg-2);
font-weight: 600;
}
.rte-table td { background: var(--paper); }
.rte-img {
max-width: 100%;
height: auto;
border-radius: var(--radius);
margin: 0.5em 0;
}
/* Collapsed state */
.rte-collapsed {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
min-height: 40px;
border: 1px dashed var(--rule);
border-radius: var(--radius);
background: var(--bg);
cursor: pointer;
transition: border-color 0.12s, background 0.12s;
}
.rte-collapsed:hover {
border-color: var(--accent-edge);
background: var(--paper);
}
.rte-collapsed-placeholder {
font-size: 13px;
color: var(--ink-3);
}
/* ---------- Import / Export dialog ---------- */
.ie-tabs { display: flex; gap: 4px; margin-bottom: 12px; border-bottom: 1px solid var(--rule); align-items: stretch; }
.ie-tab {
display: inline-flex; align-items: baseline; gap: 6px;
padding: 7px 14px;
font-size: 13px; font-weight: 500; color: var(--ink-3);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.ie-tab:hover { color: var(--ink); }
.ie-tab.active { color: var(--ink); border-bottom-color: var(--ink); }
.ie-tab-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-3); }
.ie-copy-btn {
margin-left: auto;
display: inline-flex; align-items: center; gap: 5px;
padding: 5px 10px;
font-size: 12px; font-weight: 500; color: var(--ink-2);
border: 1px solid var(--rule); border-radius: var(--radius);
background: var(--paper);
align-self: center;
margin-bottom: 4px;
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.ie-copy-btn:hover { background: var(--bg-2); color: var(--ink); border-color: oklch(0.85 0.008 70); }
.ie-copy-btn.copied { background: var(--ok); color: white; border-color: var(--ok); }
.export-pre {
background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius);
padding: 14px 16px;
font-family: var(--font-mono); font-size: 11.5px; line-height: 1.55;
color: var(--ink-2);
max-height: 360px; overflow: auto;
white-space: pre;
margin: 0;
}
/* ---------- Toast ---------- */
.toast {
position: fixed; bottom: 24px; left: 50%;
transform: translateX(-50%);
background: var(--ink); color: var(--bg);
padding: 9px 16px; border-radius: 99px;
font-size: 13px;
display: inline-flex; align-items: center; gap: 8px;
box-shadow: 0 10px 30px oklch(0.22 0.012 60 / 0.3);
z-index: 250;
animation: toast-in 0.2s ease;
}
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 8px); } }
@media (max-width: 900px) {
.main { grid-template-columns: 1fr; }
.filter-panel { position: static; max-height: none; border-right: 0; border-bottom: 1px solid var(--rule); }
.admin-grid { grid-template-columns: 1fr; }
.card.span-2, .card.span-3 { grid-column: span 1; }
.header { grid-template-columns: 1fr; gap: 10px; }
.stats-row { grid-template-columns: repeat(2, 1fr); }
.field-row { grid-template-columns: 1fr; }
.status-grid { grid-template-columns: repeat(2, 1fr); }
}
/* ---------- Archive Manager ---------- */
.archive-section { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--rule-2); }
.archive-section:first-of-type { margin-top: 8px; padding-top: 0; border-top: 0; }
.archive-toolbar {
display: flex; align-items: center; gap: 10px;
margin-bottom: 12px; flex-wrap: wrap;
}
.archive-label { font-size: 12px; font-weight: 600; color: var(--ink-2); }
.archive-filter {
padding: 4px 8px; font-size: 12px;
border: 1px solid var(--rule); border-radius: var(--radius);
background: var(--paper); color: var(--ink);
}
.archive-search {
padding: 4px 10px; font-size: 12px;
border: 1px solid var(--rule); border-radius: var(--radius);
background: var(--paper); color: var(--ink);
width: 160px;
}
.archive-search::placeholder { color: var(--ink-3); }
.archive-empty {
padding: 20px; text-align: center;
color: var(--ink-3); font-size: 13px;
}
.archive-list {
display: flex; flex-direction: column; gap: 4px;
max-height: 280px; overflow-y: auto;
}
.archive-item {
display: flex; align-items: center; gap: 8px;
padding: 6px 10px; background: var(--bg); border-radius: var(--radius);
font-size: 13px; cursor: pointer;
transition: background 0.12s;
}
.archive-item:hover { background: var(--bg-2); }
.archive-item.archived { cursor: default; }
.archive-item input[type="checkbox"] { flex-shrink: 0; }
.archive-item .glyph { flex-shrink: 0; width: 18px; text-align: center; }
.archive-item-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.archive-item-children {
font-size: 11px; color: var(--ink-3);
padding: 2px 6px; background: var(--rule-2); border-radius: 3px;
}
.archive-item-date { font-size: 11px; color: var(--ink-3); font-family: var(--font-mono); }
.archive-more { padding: 8px; text-align: center; font-size: 12px; color: var(--ink-3); }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-xs { padding: 2px 8px; font-size: 11px; }
/* ---------- Archive Search Results ---------- */
.archive-search-section {
margin-top: 16px; padding-top: 16px;
border-top: 1px dashed var(--rule);
}
.archive-search-toggle {
display: flex; align-items: center; gap: 6px;
padding: 6px 10px;
font-size: 12px; font-weight: 500; color: var(--ink-2);
background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius);
cursor: pointer;
transition: background 0.12s, color 0.12s;
}
.archive-search-toggle:hover { background: var(--bg-2); color: var(--ink); }
.archive-loading { color: var(--ink-3); font-style: italic; }
.archive-count { color: var(--ink-3); }
.archive-search-results {
margin-top: 8px; padding: 8px;
background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius);
}
.archive-search-empty {
padding: 12px; text-align: center;
color: var(--ink-3); font-size: 13px;
}
.archive-search-item {
display: flex; align-items: center; gap: 8px;
padding: 6px 8px;
border-radius: var(--radius);
transition: background 0.12s;
}
.archive-search-item:hover { background: var(--bg-2); }
.archive-search-item .glyph { flex-shrink: 0; width: 18px; text-align: center; }
.archive-search-title { flex: 1; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.archive-search-date { font-size: 11px; color: var(--ink-3); font-family: var(--font-mono); }
.archive-search-more { padding: 8px; text-align: center; font-size: 12px; color: var(--ink-3); }
.archive-search-header {
font-size: 12px; font-weight: 600; color: var(--ink-2);
padding: 4px 0; margin-bottom: 8px;
}
.archive-match-hint { font-size: 14px; color: var(--ink-3); font-weight: 400; }
.rte-readonly {
padding: 10px 12px; background: var(--bg); border: 1px solid var(--rule);
border-radius: var(--radius); font-size: 13px; line-height: 1.5; color: var(--ink-2);
min-height: 60px; max-height: 200px; overflow-y: auto;
}
.rte-readonly:empty::before { content: 'No details'; color: var(--ink-3); }
.tag-list-readonly { display: flex; flex-wrap: wrap; gap: 6px; }
/* ---------- Project sections (multi-project api mode) ---------- */
.project-sections { display: flex; flex-direction: column; gap: 16px; }
.project-section {
background: var(--paper);
border: 1px solid var(--rule);
border-radius: var(--radius-lg);
overflow: hidden;
}
.project-section-header {
display: flex; align-items: center; gap: 8px; padding: 10px 14px;
background: var(--bg-2); cursor: pointer; user-select: none;
border-bottom: 1px solid transparent; transition: background 0.15s;
}
.project-section-header:hover { background: var(--accent-soft); }
.project-section-header.expanded { border-bottom-color: var(--rule); }
.project-section-header.missing .project-section-name { color: var(--ink-3); }
.project-section-chevron { color: var(--ink-3); width: 14px; display: inline-flex; }
.project-section-name { font-weight: 600; font-size: 14px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.project-section-count { font-size: 12px; color: var(--ink-3); margin-left: 4px; white-space: nowrap; }
.project-section-actions {
margin-left: auto; display: flex; gap: 2px; opacity: 0; transition: opacity 0.15s;
}
.project-section-header:hover .project-section-actions,
.project-section-header:focus-within .project-section-actions { opacity: 1; }
.project-section-btn {
padding: 4px 6px; border: none; background: transparent;
color: var(--ink-3); cursor: pointer; border-radius: var(--radius); transition: background 0.15s, color 0.15s;
}
.project-section-btn:hover { background: var(--accent-soft); color: var(--accent); }
.project-section-btn-danger:hover { background: var(--danger-soft); color: var(--danger); }
.project-section-content { padding: 8px 0; }
.project-section-content.drop-target {
outline: 2px dashed var(--accent-edge);
outline-offset: -4px;
background: var(--accent-soft);
}
.project-empty { padding: 20px; text-align: center; color: var(--ink-3); font-size: 13px; }
.project-empty .mono { font-family: var(--font-mono); font-size: 12px; }
.project-missing-badge {
font-size: 11px; line-height: 1; padding: 3px 8px; border-radius: 999px;
color: var(--warn); background: var(--warn-bg);
border: 1px solid var(--warn);
white-space: nowrap;
}
.dlg-checkbox {
display: flex; align-items: center; gap: 8px; margin-top: 12px;
font-size: 13px; color: var(--ink-2); cursor: pointer;
}
.dlg-checkbox input { width: 16px; height: 16px; accent-color: var(--danger); cursor: pointer; }
/* ---- Admin Projects card + import/export target select (multi-project) ---- */
.projects-table .projects-path {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.projects-register {
display: flex; align-items: center; gap: 8px;
margin-top: 14px; flex-wrap: wrap;
}
.projects-input {
flex: 1 1 auto; min-width: 0;
font-family: var(--font-mono); font-size: 12.5px;
padding: 7px 10px;
border: 1px solid var(--rule); border-radius: var(--radius);
background: var(--paper); color: var(--ink);
}
.projects-input::placeholder { color: var(--ink-3); }
.projects-input:focus { outline: none; border-color: var(--accent-edge); }
.projects-input-name { flex: 0 1 200px; font-family: inherit; }
.ie-target-row {
display: flex; align-items: center; gap: 10px;
margin-bottom: 12px;
}
.ie-target-label {
font-size: 11px; font-weight: 600;
color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em;
white-space: nowrap;
}
.ie-target {
font: inherit; font-size: 13px;
padding: 6px 10px;
border: 1px solid var(--rule); border-radius: var(--radius);
background: var(--paper); color: var(--ink);
cursor: pointer;
}
/* ---------- Row context menu ---------- */
.ctx-menu {
position: fixed; z-index: 60;
width: 200px; padding: 4px;
background: var(--paper); border: 1px solid var(--rule);
border-radius: var(--radius-lg); box-shadow: var(--shadow-pop);
}
.ctx-menu-label {
padding: 6px 8px 4px;
font-size: 11px; font-weight: 600;
color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em;
}
.ctx-menu-item {
width: 100%; display: flex; align-items: center; gap: 8px;
padding: 6px 8px; border-radius: 4px;
font-size: 13px; color: var(--ink); text-align: left;
}
.ctx-menu-item:hover { background: var(--bg-2); }
.ctx-menu-item.active { background: var(--accent-soft); color: var(--accent-deep); }
.ctx-menu-item.danger { color: var(--danger); }
.ctx-menu-item.danger:hover { background: var(--danger-soft); }
.ctx-menu-sep { height: 1px; margin: 4px 6px; background: var(--rule-2); }
</style>
</head>
<body>
<template id="__bundler_thumbnail">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f7f3ec"/>
<g transform="translate(60,60)" stroke="#3a3530" stroke-width="3" fill="none" stroke-linecap="round">
<rect x="0" y="0" width="80" height="80" rx="4" fill="#ffffff"/>
<line x1="14" y1="22" x2="66" y2="22"/>
<line x1="14" y1="40" x2="56" y2="40"/>
<line x1="14" y1="58" x2="60" y2="58"/>
<circle cx="8" cy="22" r="2.5" fill="#3a3530"/>
<circle cx="8" cy="40" r="2.5" fill="#3a3530"/>
<circle cx="8" cy="58" r="2.5" fill="#3a3530"/>
</g>
</svg>
</template>
<div id="root"></div>
<script>
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
(function(){'use strict';(function(c,x){"object"===typeof exports&&"undefined"!==typeof module?x(exports):"function"===typeof define&&define.amd?define(["exports"],x):(c=c||self,x(c.React={}))})(this,function(c){function x(a){if(null===a||"object"!==typeof a)return null;a=V&&a[V]||a["@@iterator"];return"function"===typeof a?a:null}function w(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Y(){}function K(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Z(a,b,
e){var m,d={},c=null,h=null;if(null!=b)for(m in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(c=""+b.key),b)aa.call(b,m)&&!ba.hasOwnProperty(m)&&(d[m]=b[m]);var l=arguments.length-2;if(1===l)d.children=e;else if(1<l){for(var f=Array(l),k=0;k<l;k++)f[k]=arguments[k+2];d.children=f}if(a&&a.defaultProps)for(m in l=a.defaultProps,l)void 0===d[m]&&(d[m]=l[m]);return{$$typeof:y,type:a,key:c,ref:h,props:d,_owner:L.current}}function oa(a,b){return{$$typeof:y,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}
function M(a){return"object"===typeof a&&null!==a&&a.$$typeof===y}function pa(a){var b={"=":"=0",":":"=2"};return"$"+a.replace(/[=:]/g,function(a){return b[a]})}function N(a,b){return"object"===typeof a&&null!==a&&null!=a.key?pa(""+a.key):b.toString(36)}function B(a,b,e,m,d){var c=typeof a;if("undefined"===c||"boolean"===c)a=null;var h=!1;if(null===a)h=!0;else switch(c){case "string":case "number":h=!0;break;case "object":switch(a.$$typeof){case y:case qa:h=!0}}if(h)return h=a,d=d(h),a=""===m?"."+
N(h,0):m,ca(d)?(e="",null!=a&&(e=a.replace(da,"$&/")+"/"),B(d,b,e,"",function(a){return a})):null!=d&&(M(d)&&(d=oa(d,e+(!d.key||h&&h.key===d.key?"":(""+d.key).replace(da,"$&/")+"/")+a)),b.push(d)),1;h=0;m=""===m?".":m+":";if(ca(a))for(var l=0;l<a.length;l++){c=a[l];var f=m+N(c,l);h+=B(c,b,e,f,d)}else if(f=x(a),"function"===typeof f)for(a=f.call(a),l=0;!(c=a.next()).done;)c=c.value,f=m+N(c,l++),h+=B(c,b,e,f,d);else if("object"===c)throw b=String(a),Error("Objects are not valid as a React child (found: "+
("[object Object]"===b?"object with keys {"+Object.keys(a).join(", ")+"}":b)+"). If you meant to render a collection of children, use an array instead.");return h}function C(a,b,e){if(null==a)return a;var c=[],d=0;B(a,c,"","",function(a){return b.call(e,a,d++)});return c}function ra(a){if(-1===a._status){var b=a._result;b=b();b.then(function(b){if(0===a._status||-1===a._status)a._status=1,a._result=b},function(b){if(0===a._status||-1===a._status)a._status=2,a._result=b});-1===a._status&&(a._status=
0,a._result=b)}if(1===a._status)return a._result.default;throw a._result;}function O(a,b){var e=a.length;a.push(b);a:for(;0<e;){var c=e-1>>>1,d=a[c];if(0<D(d,b))a[c]=b,a[e]=d,e=c;else break a}}function p(a){return 0===a.length?null:a[0]}function E(a){if(0===a.length)return null;var b=a[0],e=a.pop();if(e!==b){a[0]=e;a:for(var c=0,d=a.length,k=d>>>1;c<k;){var h=2*(c+1)-1,l=a[h],f=h+1,g=a[f];if(0>D(l,e))f<d&&0>D(g,l)?(a[c]=g,a[f]=e,c=f):(a[c]=l,a[h]=e,c=h);else if(f<d&&0>D(g,e))a[c]=g,a[f]=e,c=f;else break a}}return b}
function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function P(a){for(var b=p(r);null!==b;){if(null===b.callback)E(r);else if(b.startTime<=a)E(r),b.sortIndex=b.expirationTime,O(q,b);else break;b=p(r)}}function Q(a){z=!1;P(a);if(!u)if(null!==p(q))u=!0,R(S);else{var b=p(r);null!==b&&T(Q,b.startTime-a)}}function S(a,b){u=!1;z&&(z=!1,ea(A),A=-1);F=!0;var c=k;try{P(b);for(n=p(q);null!==n&&(!(n.expirationTime>b)||a&&!fa());){var m=n.callback;if("function"===typeof m){n.callback=null;
k=n.priorityLevel;var d=m(n.expirationTime<=b);b=v();"function"===typeof d?n.callback=d:n===p(q)&&E(q);P(b)}else E(q);n=p(q)}if(null!==n)var g=!0;else{var h=p(r);null!==h&&T(Q,h.startTime-b);g=!1}return g}finally{n=null,k=c,F=!1}}function fa(){return v()-ha<ia?!1:!0}function R(a){G=a;H||(H=!0,I())}function T(a,b){A=ja(function(){a(v())},b)}function ka(a){throw Error("act(...) is not supported in production builds of React.");}var y=Symbol.for("react.element"),qa=Symbol.for("react.portal"),sa=Symbol.for("react.fragment"),
ta=Symbol.for("react.strict_mode"),ua=Symbol.for("react.profiler"),va=Symbol.for("react.provider"),wa=Symbol.for("react.context"),xa=Symbol.for("react.forward_ref"),ya=Symbol.for("react.suspense"),za=Symbol.for("react.memo"),Aa=Symbol.for("react.lazy"),V=Symbol.iterator,X={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,c){},enqueueReplaceState:function(a,b,c,m){},enqueueSetState:function(a,b,c,m){}},la=Object.assign,W={};w.prototype.isReactComponent={};w.prototype.setState=function(a,
b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,a,b,"setState")};w.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};Y.prototype=w.prototype;var t=K.prototype=new Y;t.constructor=K;la(t,w.prototype);t.isPureReactComponent=!0;var ca=Array.isArray,aa=Object.prototype.hasOwnProperty,L={current:null},
ba={key:!0,ref:!0,__self:!0,__source:!0},da=/\/+/g,g={current:null},J={transition:null};if("object"===typeof performance&&"function"===typeof performance.now){var Ba=performance;var v=function(){return Ba.now()}}else{var ma=Date,Ca=ma.now();v=function(){return ma.now()-Ca}}var q=[],r=[],Da=1,n=null,k=3,F=!1,u=!1,z=!1,ja="function"===typeof setTimeout?setTimeout:null,ea="function"===typeof clearTimeout?clearTimeout:null,na="undefined"!==typeof setImmediate?setImmediate:null;"undefined"!==typeof navigator&&
void 0!==navigator.scheduling&&void 0!==navigator.scheduling.isInputPending&&navigator.scheduling.isInputPending.bind(navigator.scheduling);var H=!1,G=null,A=-1,ia=5,ha=-1,U=function(){if(null!==G){var a=v();ha=a;var b=!0;try{b=G(!0,a)}finally{b?I():(H=!1,G=null)}}else H=!1};if("function"===typeof na)var I=function(){na(U)};else if("undefined"!==typeof MessageChannel){t=new MessageChannel;var Ea=t.port2;t.port1.onmessage=U;I=function(){Ea.postMessage(null)}}else I=function(){ja(U,0)};t={ReactCurrentDispatcher:g,
ReactCurrentOwner:L,ReactCurrentBatchConfig:J,Scheduler:{__proto__:null,unstable_ImmediatePriority:1,unstable_UserBlockingPriority:2,unstable_NormalPriority:3,unstable_IdlePriority:5,unstable_LowPriority:4,unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3}var c=k;k=a;try{return b()}finally{k=c}},unstable_next:function(a){switch(k){case 1:case 2:case 3:var b=3;break;default:b=k}var c=k;k=b;try{return a()}finally{k=c}},unstable_scheduleCallback:function(a,
b,c){var e=v();"object"===typeof c&&null!==c?(c=c.delay,c="number"===typeof c&&0<c?e+c:e):c=e;switch(a){case 1:var d=-1;break;case 2:d=250;break;case 5:d=1073741823;break;case 4:d=1E4;break;default:d=5E3}d=c+d;a={id:Da++,callback:b,priorityLevel:a,startTime:c,expirationTime:d,sortIndex:-1};c>e?(a.sortIndex=c,O(r,a),null===p(q)&&a===p(r)&&(z?(ea(A),A=-1):z=!0,T(Q,c-e))):(a.sortIndex=d,O(q,a),u||F||(u=!0,R(S)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=
k;return function(){var c=k;k=b;try{return a.apply(this,arguments)}finally{k=c}}},unstable_getCurrentPriorityLevel:function(){return k},unstable_shouldYield:fa,unstable_requestPaint:function(){},unstable_continueExecution:function(){u||F||(u=!0,R(S))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return p(q)},get unstable_now(){return v},unstable_forceFrameRate:function(a){0>a||125<a?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):
ia=0<a?Math.floor(1E3/a):5},unstable_Profiling:null}};c.Children={map:C,forEach:function(a,b,c){C(a,function(){b.apply(this,arguments)},c)},count:function(a){var b=0;C(a,function(){b++});return b},toArray:function(a){return C(a,function(a){return a})||[]},only:function(a){if(!M(a))throw Error("React.Children.only expected to receive a single React element child.");return a}};c.Component=w;c.Fragment=sa;c.Profiler=ua;c.PureComponent=K;c.StrictMode=ta;c.Suspense=ya;c.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=
t;c.act=ka;c.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var e=la({},a.props),d=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=L.current);void 0!==b.key&&(d=""+b.key);if(a.type&&a.type.defaultProps)var l=a.type.defaultProps;for(f in b)aa.call(b,f)&&!ba.hasOwnProperty(f)&&(e[f]=void 0===b[f]&&void 0!==l?l[f]:b[f])}var f=arguments.length-2;if(1===f)e.children=c;else if(1<f){l=
Array(f);for(var g=0;g<f;g++)l[g]=arguments[g+2];e.children=l}return{$$typeof:y,type:a.type,key:d,ref:k,props:e,_owner:h}};c.createContext=function(a){a={$$typeof:wa,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:va,_context:a};return a.Consumer=a};c.createElement=Z;c.createFactory=function(a){var b=Z.bind(null,a);b.type=a;return b};c.createRef=function(){return{current:null}};c.forwardRef=function(a){return{$$typeof:xa,
render:a}};c.isValidElement=M;c.lazy=function(a){return{$$typeof:Aa,_payload:{_status:-1,_result:a},_init:ra}};c.memo=function(a,b){return{$$typeof:za,type:a,compare:void 0===b?null:b}};c.startTransition=function(a,b){b=J.transition;J.transition={};try{a()}finally{J.transition=b}};c.unstable_act=ka;c.useCallback=function(a,b){return g.current.useCallback(a,b)};c.useContext=function(a){return g.current.useContext(a)};c.useDebugValue=function(a,b){};c.useDeferredValue=function(a){return g.current.useDeferredValue(a)};
c.useEffect=function(a,b){return g.current.useEffect(a,b)};c.useId=function(){return g.current.useId()};c.useImperativeHandle=function(a,b,c){return g.current.useImperativeHandle(a,b,c)};c.useInsertionEffect=function(a,b){return g.current.useInsertionEffect(a,b)};c.useLayoutEffect=function(a,b){return g.current.useLayoutEffect(a,b)};c.useMemo=function(a,b){return g.current.useMemo(a,b)};c.useReducer=function(a,b,c){return g.current.useReducer(a,b,c)};c.useRef=function(a){return g.current.useRef(a)};
c.useState=function(a){return g.current.useState(a)};c.useSyncExternalStore=function(a,b,c){return g.current.useSyncExternalStore(a,b,c)};c.useTransition=function(){return g.current.useTransition()};c.version="18.3.1"});
})();
</script>
<script>
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
(function(){/*
Modernizr 3.0.0pre (Custom Build) | MIT
*/
'use strict';(function(Q,zb){"object"===typeof exports&&"undefined"!==typeof module?zb(exports,require("react")):"function"===typeof define&&define.amd?define(["exports","react"],zb):(Q=Q||self,zb(Q.ReactDOM={},Q.React))})(this,function(Q,zb){function m(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
function mb(a,b){Ab(a,b);Ab(a+"Capture",b)}function Ab(a,b){$b[a]=b;for(a=0;a<b.length;a++)cg.add(b[a])}function bj(a){if(Zd.call(dg,a))return!0;if(Zd.call(eg,a))return!1;if(cj.test(a))return dg[a]=!0;eg[a]=!0;return!1}function dj(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case "function":case "symbol":return!0;case "boolean":if(d)return!1;if(null!==c)return!c.acceptsBooleans;a=a.toLowerCase().slice(0,5);return"data-"!==a&&"aria-"!==a;default:return!1}}function ej(a,b,c,d){if(null===
b||"undefined"===typeof b||dj(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function Y(a,b,c,d,e,f,g){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f;this.removeEmptyString=g}function $d(a,b,c,d){var e=R.hasOwnProperty(b)?R[b]:null;if(null!==e?0!==e.type:d||!(2<b.length)||"o"!==
b[0]&&"O"!==b[0]||"n"!==b[1]&&"N"!==b[1])ej(b,c,e,d)&&(c=null),d||null===e?bj(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,""+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:"":c:(b=e.attributeName,d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?"":""+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c)))}function ac(a){if(null===a||"object"!==typeof a)return null;a=fg&&a[fg]||a["@@iterator"];return"function"===typeof a?a:null}function bc(a,b,
c){if(void 0===ae)try{throw Error();}catch(d){ae=(b=d.stack.trim().match(/\n( *(at )?)/))&&b[1]||""}return"\n"+ae+a}function be(a,b){if(!a||ce)return"";ce=!0;var c=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(b)if(b=function(){throw Error();},Object.defineProperty(b.prototype,"props",{set:function(){throw Error();}}),"object"===typeof Reflect&&Reflect.construct){try{Reflect.construct(b,[])}catch(n){var d=n}Reflect.construct(a,[],b)}else{try{b.call()}catch(n){d=n}a.call(b.prototype)}else{try{throw Error();
}catch(n){d=n}a()}}catch(n){if(n&&d&&"string"===typeof n.stack){for(var e=n.stack.split("\n"),f=d.stack.split("\n"),g=e.length-1,h=f.length-1;1<=g&&0<=h&&e[g]!==f[h];)h--;for(;1<=g&&0<=h;g--,h--)if(e[g]!==f[h]){if(1!==g||1!==h){do if(g--,h--,0>h||e[g]!==f[h]){var k="\n"+e[g].replace(" at new "," at ");a.displayName&&k.includes("<anonymous>")&&(k=k.replace("<anonymous>",a.displayName));return k}while(1<=g&&0<=h)}break}}}finally{ce=!1,Error.prepareStackTrace=c}return(a=a?a.displayName||a.name:"")?bc(a):
""}function fj(a){switch(a.tag){case 5:return bc(a.type);case 16:return bc("Lazy");case 13:return bc("Suspense");case 19:return bc("SuspenseList");case 0:case 2:case 15:return a=be(a.type,!1),a;case 11:return a=be(a.type.render,!1),a;case 1:return a=be(a.type,!0),a;default:return""}}function de(a){if(null==a)return null;if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case Bb:return"Fragment";case Cb:return"Portal";case ee:return"Profiler";case fe:return"StrictMode";
case ge:return"Suspense";case he:return"SuspenseList"}if("object"===typeof a)switch(a.$$typeof){case gg:return(a.displayName||"Context")+".Consumer";case hg:return(a._context.displayName||"Context")+".Provider";case ie:var b=a.render;a=a.displayName;a||(a=b.displayName||b.name||"",a=""!==a?"ForwardRef("+a+")":"ForwardRef");return a;case je:return b=a.displayName||null,null!==b?b:de(a.type)||"Memo";case Ta:b=a._payload;a=a._init;try{return de(a(b))}catch(c){}}return null}function gj(a){var b=a.type;
switch(a.tag){case 24:return"Cache";case 9:return(b.displayName||"Context")+".Consumer";case 10:return(b._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return a=b.render,a=a.displayName||a.name||"",b.displayName||(""!==a?"ForwardRef("+a+")":"ForwardRef");case 7:return"Fragment";case 5:return b;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return de(b);case 8:return b===fe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";
case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if("function"===typeof b)return b.displayName||b.name||null;if("string"===typeof b)return b}return null}function Ua(a){switch(typeof a){case "boolean":case "number":case "string":case "undefined":return a;case "object":return a;default:return""}}function ig(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"===
b)}function hj(a){var b=ig(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker=
null;delete a[b]}}}}function Pc(a){a._valueTracker||(a._valueTracker=hj(a))}function jg(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=ig(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Qc(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function ke(a,b){var c=b.checked;return E({},b,{defaultChecked:void 0,defaultValue:void 0,
value:void 0,checked:null!=c?c:a._wrapperState.initialChecked})}function kg(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=Ua(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function lg(a,b){b=b.checked;null!=b&&$d(a,"checked",b,!1)}function le(a,b){lg(a,b);var c=Ua(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!=
c)a.value=""+c}else a.value!==""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?me(a,b.type,c):b.hasOwnProperty("defaultValue")&&me(a,b.type,Ua(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function mg(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue;
c||b===a.value||(a.value=b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function me(a,b,c){if("number"!==b||Qc(a.ownerDocument)!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function Db(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e<c.length;e++)b["$"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty("$"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=
!0)}else{c=""+Ua(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function ne(a,b){if(null!=b.dangerouslySetInnerHTML)throw Error(m(91));return E({},b,{value:void 0,defaultValue:void 0,children:""+a._wrapperState.initialValue})}function ng(a,b){var c=b.value;if(null==c){c=b.children;b=b.defaultValue;if(null!=c){if(null!=b)throw Error(m(92));if(cc(c)){if(1<c.length)throw Error(m(93));
c=c[0]}b=c}null==b&&(b="");c=b}a._wrapperState={initialValue:Ua(c)}}function og(a,b){var c=Ua(b.value),d=Ua(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d)}function pg(a,b){b=a.textContent;b===a._wrapperState.initialValue&&""!==b&&null!==b&&(a.value=b)}function qg(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}
function oe(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?qg(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a}function rg(a,b,c){return null==b||"boolean"===typeof b||""===b?"":c||"number"!==typeof b||0===b||dc.hasOwnProperty(a)&&dc[a]?(""+b).trim():b+"px"}function sg(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf("--"),e=rg(c,b[c],d);"float"===c&&(c="cssFloat");d?a.setProperty(c,e):a[c]=e}}function pe(a,b){if(b){if(ij[a]&&
(null!=b.children||null!=b.dangerouslySetInnerHTML))throw Error(m(137,a));if(null!=b.dangerouslySetInnerHTML){if(null!=b.children)throw Error(m(60));if("object"!==typeof b.dangerouslySetInnerHTML||!("__html"in b.dangerouslySetInnerHTML))throw Error(m(61));}if(null!=b.style&&"object"!==typeof b.style)throw Error(m(62));}}function qe(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;
default:return!0}}function re(a){a=a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:a}function tg(a){if(a=ec(a)){if("function"!==typeof se)throw Error(m(280));var b=a.stateNode;b&&(b=Rc(b),se(a.stateNode,a.type,b))}}function ug(a){Eb?Fb?Fb.push(a):Fb=[a]:Eb=a}function vg(){if(Eb){var a=Eb,b=Fb;Fb=Eb=null;tg(a);if(b)for(a=0;a<b.length;a++)tg(b[a])}}function wg(a,b,c){if(te)return a(b,c);te=!0;try{return xg(a,b,c)}finally{if(te=
!1,null!==Eb||null!==Fb)yg(),vg()}}function fc(a,b){var c=a.stateNode;if(null===c)return null;var d=Rc(c);if(null===d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":case "onMouseEnter":(d=!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1}if(a)return null;
if(c&&"function"!==typeof c)throw Error(m(231,b,typeof c));return c}function jj(a,b,c,d,e,f,g,h,k){gc=!1;Sc=null;kj.apply(lj,arguments)}function mj(a,b,c,d,e,f,g,h,k){jj.apply(this,arguments);if(gc){if(gc){var n=Sc;gc=!1;Sc=null}else throw Error(m(198));Tc||(Tc=!0,ue=n)}}function nb(a){var b=a,c=a;if(a.alternate)for(;b.return;)b=b.return;else{a=b;do b=a,0!==(b.flags&4098)&&(c=b.return),a=b.return;while(a)}return 3===b.tag?c:null}function zg(a){if(13===a.tag){var b=a.memoizedState;null===b&&(a=a.alternate,
null!==a&&(b=a.memoizedState));if(null!==b)return b.dehydrated}return null}function Ag(a){if(nb(a)!==a)throw Error(m(188));}function nj(a){var b=a.alternate;if(!b){b=nb(a);if(null===b)throw Error(m(188));return b!==a?null:a}for(var c=a,d=b;;){var e=c.return;if(null===e)break;var f=e.alternate;if(null===f){d=e.return;if(null!==d){c=d;continue}break}if(e.child===f.child){for(f=e.child;f;){if(f===c)return Ag(e),a;if(f===d)return Ag(e),b;f=f.sibling}throw Error(m(188));}if(c.return!==d.return)c=e,d=f;
else{for(var g=!1,h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}if(!g)throw Error(m(189));}}if(c.alternate!==d)throw Error(m(190));}if(3!==c.tag)throw Error(m(188));return c.stateNode.current===c?a:b}function Bg(a){a=nj(a);return null!==a?Cg(a):null}function Cg(a){if(5===a.tag||6===a.tag)return a;for(a=a.child;null!==a;){var b=Cg(a);if(null!==b)return b;a=a.sibling}return null}
function oj(a,b){if(Ca&&"function"===typeof Ca.onCommitFiberRoot)try{Ca.onCommitFiberRoot(Uc,a,void 0,128===(a.current.flags&128))}catch(c){}}function pj(a){a>>>=0;return 0===a?32:31-(qj(a)/rj|0)|0}function hc(a){switch(a&-a){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a&
4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return a&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return a}}function Vc(a,b){var c=a.pendingLanes;if(0===c)return 0;var d=0,e=a.suspendedLanes,f=a.pingedLanes,g=c&268435455;if(0!==g){var h=g&~e;0!==h?d=hc(h):(f&=g,0!==f&&(d=hc(f)))}else g=c&~e,0!==g?d=hc(g):0!==f&&(d=hc(f));if(0===d)return 0;if(0!==b&&b!==d&&0===(b&e)&&
(e=d&-d,f=b&-b,e>=f||16===e&&0!==(f&4194240)))return b;0!==(d&4)&&(d|=c&16);b=a.entangledLanes;if(0!==b)for(a=a.entanglements,b&=d;0<b;)c=31-ta(b),e=1<<c,d|=a[c],b&=~e;return d}function sj(a,b){switch(a){case 1:case 2:case 4:return b+250;case 8:case 16:case 32:case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return b+5E3;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return-1;
case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function tj(a,b){for(var c=a.suspendedLanes,d=a.pingedLanes,e=a.expirationTimes,f=a.pendingLanes;0<f;){var g=31-ta(f),h=1<<g,k=e[g];if(-1===k){if(0===(h&c)||0!==(h&d))e[g]=sj(h,b)}else k<=b&&(a.expiredLanes|=h);f&=~h}}function ve(a){a=a.pendingLanes&-1073741825;return 0!==a?a:a&1073741824?1073741824:0}function Dg(){var a=Wc;Wc<<=1;0===(Wc&4194240)&&(Wc=64);return a}function we(a){for(var b=[],c=0;31>c;c++)b.push(a);
return b}function ic(a,b,c){a.pendingLanes|=b;536870912!==b&&(a.suspendedLanes=0,a.pingedLanes=0);a=a.eventTimes;b=31-ta(b);a[b]=c}function uj(a,b){var c=a.pendingLanes&~b;a.pendingLanes=b;a.suspendedLanes=0;a.pingedLanes=0;a.expiredLanes&=b;a.mutableReadLanes&=b;a.entangledLanes&=b;b=a.entanglements;var d=a.eventTimes;for(a=a.expirationTimes;0<c;){var e=31-ta(c),f=1<<e;b[e]=0;d[e]=-1;a[e]=-1;c&=~f}}function xe(a,b){var c=a.entangledLanes|=b;for(a=a.entanglements;c;){var d=31-ta(c),e=1<<d;e&b|a[d]&
b&&(a[d]|=b);c&=~e}}function Eg(a){a&=-a;return 1<a?4<a?0!==(a&268435455)?16:536870912:4:1}function Fg(a,b){switch(a){case "focusin":case "focusout":Va=null;break;case "dragenter":case "dragleave":Wa=null;break;case "mouseover":case "mouseout":Xa=null;break;case "pointerover":case "pointerout":jc.delete(b.pointerId);break;case "gotpointercapture":case "lostpointercapture":kc.delete(b.pointerId)}}function lc(a,b,c,d,e,f){if(null===a||a.nativeEvent!==f)return a={blockedOn:b,domEventName:c,eventSystemFlags:d,
nativeEvent:f,targetContainers:[e]},null!==b&&(b=ec(b),null!==b&&Gg(b)),a;a.eventSystemFlags|=d;b=a.targetContainers;null!==e&&-1===b.indexOf(e)&&b.push(e);return a}function vj(a,b,c,d,e){switch(b){case "focusin":return Va=lc(Va,a,b,c,d,e),!0;case "dragenter":return Wa=lc(Wa,a,b,c,d,e),!0;case "mouseover":return Xa=lc(Xa,a,b,c,d,e),!0;case "pointerover":var f=e.pointerId;jc.set(f,lc(jc.get(f)||null,a,b,c,d,e));return!0;case "gotpointercapture":return f=e.pointerId,kc.set(f,lc(kc.get(f)||null,a,b,
c,d,e)),!0}return!1}function Hg(a){var b=ob(a.target);if(null!==b){var c=nb(b);if(null!==c)if(b=c.tag,13===b){if(b=zg(c),null!==b){a.blockedOn=b;wj(a.priority,function(){xj(c)});return}}else if(3===b&&c.stateNode.current.memoizedState.isDehydrated){a.blockedOn=3===c.tag?c.stateNode.containerInfo:null;return}}a.blockedOn=null}function Xc(a){if(null!==a.blockedOn)return!1;for(var b=a.targetContainers;0<b.length;){var c=ye(a.domEventName,a.eventSystemFlags,b[0],a.nativeEvent);if(null===c){c=a.nativeEvent;
var d=new c.constructor(c.type,c);ze=d;c.target.dispatchEvent(d);ze=null}else return b=ec(c),null!==b&&Gg(b),a.blockedOn=c,!1;b.shift()}return!0}function Ig(a,b,c){Xc(a)&&c.delete(b)}function yj(){Ae=!1;null!==Va&&Xc(Va)&&(Va=null);null!==Wa&&Xc(Wa)&&(Wa=null);null!==Xa&&Xc(Xa)&&(Xa=null);jc.forEach(Ig);kc.forEach(Ig)}function mc(a,b){a.blockedOn===b&&(a.blockedOn=null,Ae||(Ae=!0,Jg(Kg,yj)))}function nc(a){if(0<Yc.length){mc(Yc[0],a);for(var b=1;b<Yc.length;b++){var c=Yc[b];c.blockedOn===a&&(c.blockedOn=
null)}}null!==Va&&mc(Va,a);null!==Wa&&mc(Wa,a);null!==Xa&&mc(Xa,a);b=function(b){return mc(b,a)};jc.forEach(b);kc.forEach(b);for(b=0;b<Ya.length;b++)c=Ya[b],c.blockedOn===a&&(c.blockedOn=null);for(;0<Ya.length&&(b=Ya[0],null===b.blockedOn);)Hg(b),null===b.blockedOn&&Ya.shift()}function zj(a,b,c,d){var e=z,f=Gb.transition;Gb.transition=null;try{z=1,Be(a,b,c,d)}finally{z=e,Gb.transition=f}}function Aj(a,b,c,d){var e=z,f=Gb.transition;Gb.transition=null;try{z=4,Be(a,b,c,d)}finally{z=e,Gb.transition=
f}}function Be(a,b,c,d){if(Zc){var e=ye(a,b,c,d);if(null===e)Ce(a,b,d,$c,c),Fg(a,d);else if(vj(e,a,b,c,d))d.stopPropagation();else if(Fg(a,d),b&4&&-1<Bj.indexOf(a)){for(;null!==e;){var f=ec(e);null!==f&&Cj(f);f=ye(a,b,c,d);null===f&&Ce(a,b,d,$c,c);if(f===e)break;e=f}null!==e&&d.stopPropagation()}else Ce(a,b,d,null,c)}}function ye(a,b,c,d){$c=null;a=re(d);a=ob(a);if(null!==a)if(b=nb(a),null===b)a=null;else if(c=b.tag,13===c){a=zg(b);if(null!==a)return a;a=null}else if(3===c){if(b.stateNode.current.memoizedState.isDehydrated)return 3===
b.tag?b.stateNode.containerInfo:null;a=null}else b!==a&&(a=null);$c=a;return null}function Lg(a){switch(a){case "cancel":case "click":case "close":case "contextmenu":case "copy":case "cut":case "auxclick":case "dblclick":case "dragend":case "dragstart":case "drop":case "focusin":case "focusout":case "input":case "invalid":case "keydown":case "keypress":case "keyup":case "mousedown":case "mouseup":case "paste":case "pause":case "play":case "pointercancel":case "pointerdown":case "pointerup":case "ratechange":case "reset":case "resize":case "seeked":case "submit":case "touchcancel":case "touchend":case "touchstart":case "volumechange":case "change":case "selectionchange":case "textInput":case "compositionstart":case "compositionend":case "compositionupdate":case "beforeblur":case "afterblur":case "beforeinput":case "blur":case "fullscreenchange":case "focus":case "hashchange":case "popstate":case "select":case "selectstart":return 1;
case "drag":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "mousemove":case "mouseout":case "mouseover":case "pointermove":case "pointerout":case "pointerover":case "scroll":case "toggle":case "touchmove":case "wheel":case "mouseenter":case "mouseleave":case "pointerenter":case "pointerleave":return 4;case "message":switch(Dj()){case De:return 1;case Mg:return 4;case ad:case Ej:return 16;case Ng:return 536870912;default:return 16}default:return 16}}function Og(){if(bd)return bd;
var a,b=Ee,c=b.length,d,e="value"in Za?Za.value:Za.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return bd=e.slice(a,1<d?1-d:void 0)}function cd(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function dd(){return!0}function Pg(){return!1}function ka(a){function b(b,d,e,f,g){this._reactName=b;this._targetInst=e;this.type=d;this.nativeEvent=f;this.target=g;this.currentTarget=null;
for(var c in a)a.hasOwnProperty(c)&&(b=a[c],this[c]=b?b(f):f[c]);this.isDefaultPrevented=(null!=f.defaultPrevented?f.defaultPrevented:!1===f.returnValue)?dd:Pg;this.isPropagationStopped=Pg;return this}E(b.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():"unknown"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=dd)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():
"unknown"!==typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=dd)},persist:function(){},isPersistent:dd});return b}function Fj(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Gj[a])?!!b[a]:!1}function Fe(a){return Fj}function Qg(a,b){switch(a){case "keyup":return-1!==Hj.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "focusout":return!0;default:return!1}}function Rg(a){a=a.detail;return"object"===typeof a&&
"data"in a?a.data:null}function Ij(a,b){switch(a){case "compositionend":return Rg(b);case "keypress":if(32!==b.which)return null;Sg=!0;return Tg;case "textInput":return a=b.data,a===Tg&&Sg?null:a;default:return null}}function Jj(a,b){if(Hb)return"compositionend"===a||!Ge&&Qg(a,b)?(a=Og(),bd=Ee=Za=null,Hb=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;
case "compositionend":return Ug&&"ko"!==b.locale?null:b.data;default:return null}}function Vg(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return"input"===b?!!Kj[a.type]:"textarea"===b?!0:!1}function Lj(a){if(!Ia)return!1;a="on"+a;var b=a in document;b||(b=document.createElement("div"),b.setAttribute(a,"return;"),b="function"===typeof b[a]);return b}function Wg(a,b,c,d){ug(d);b=ed(b,"onChange");0<b.length&&(c=new He("onChange","change",null,c,d),a.push({event:c,listeners:b}))}function Mj(a){Xg(a,
0)}function fd(a){var b=Ib(a);if(jg(b))return a}function Nj(a,b){if("change"===a)return b}function Yg(){oc&&(oc.detachEvent("onpropertychange",Zg),pc=oc=null)}function Zg(a){if("value"===a.propertyName&&fd(pc)){var b=[];Wg(b,pc,a,re(a));wg(Mj,b)}}function Oj(a,b,c){"focusin"===a?(Yg(),oc=b,pc=c,oc.attachEvent("onpropertychange",Zg)):"focusout"===a&&Yg()}function Pj(a,b){if("selectionchange"===a||"keyup"===a||"keydown"===a)return fd(pc)}function Qj(a,b){if("click"===a)return fd(b)}function Rj(a,b){if("input"===
a||"change"===a)return fd(b)}function Sj(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function qc(a,b){if(ua(a,b))return!0;if("object"!==typeof a||null===a||"object"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++){var e=c[d];if(!Zd.call(b,e)||!ua(a[e],b[e]))return!1}return!0}function $g(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function ah(a,b){var c=$g(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;
if(a<=b&&d>=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=$g(c)}}function bh(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?bh(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function ch(){for(var a=window,b=Qc();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;
b=Qc(a.document)}return b}function Ie(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function Tj(a){var b=ch(),c=a.focusedElem,d=a.selectionRange;if(b!==c&&c&&c.ownerDocument&&bh(c.ownerDocument.documentElement,c)){if(null!==d&&Ie(c))if(b=d.start,a=d.end,void 0===a&&(a=b),"selectionStart"in c)c.selectionStart=b,c.selectionEnd=Math.min(a,c.value.length);
else if(a=(b=c.ownerDocument||document)&&b.defaultView||window,a.getSelection){a=a.getSelection();var e=c.textContent.length,f=Math.min(d.start,e);d=void 0===d.end?f:Math.min(d.end,e);!a.extend&&f>d&&(e=d,d=f,f=e);e=ah(c,f);var g=ah(c,d);e&&g&&(1!==a.rangeCount||a.anchorNode!==e.node||a.anchorOffset!==e.offset||a.focusNode!==g.node||a.focusOffset!==g.offset)&&(b=b.createRange(),b.setStart(e.node,e.offset),a.removeAllRanges(),f>d?(a.addRange(b),a.extend(g.node,g.offset)):(b.setEnd(g.node,g.offset),
a.addRange(b)))}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});"function"===typeof c.focus&&c.focus();for(c=0;c<b.length;c++)a=b[c],a.element.scrollLeft=a.left,a.element.scrollTop=a.top}}function dh(a,b,c){var d=c.window===c?c.document:9===c.nodeType?c:c.ownerDocument;Je||null==Jb||Jb!==Qc(d)||(d=Jb,"selectionStart"in d&&Ie(d)?d={start:d.selectionStart,end:d.selectionEnd}:(d=(d.ownerDocument&&d.ownerDocument.defaultView||window).getSelection(),d=
{anchorNode:d.anchorNode,anchorOffset:d.anchorOffset,focusNode:d.focusNode,focusOffset:d.focusOffset}),rc&&qc(rc,d)||(rc=d,d=ed(Ke,"onSelect"),0<d.length&&(b=new He("onSelect","select",null,b,c),a.push({event:b,listeners:d}),b.target=Jb)))}function gd(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}function hd(a){if(Le[a])return Le[a];if(!Kb[a])return a;var b=Kb[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in eh)return Le[a]=b[c];return a}function $a(a,
b){fh.set(a,b);mb(b,[a])}function gh(a,b,c){var d=a.type||"unknown-event";a.currentTarget=c;mj(d,b,void 0,a);a.currentTarget=null}function Xg(a,b){b=0!==(b&4);for(var c=0;c<a.length;c++){var d=a[c],e=d.event;d=d.listeners;a:{var f=void 0;if(b)for(var g=d.length-1;0<=g;g--){var h=d[g],k=h.instance,n=h.currentTarget;h=h.listener;if(k!==f&&e.isPropagationStopped())break a;gh(e,h,n);f=k}else for(g=0;g<d.length;g++){h=d[g];k=h.instance;n=h.currentTarget;h=h.listener;if(k!==f&&e.isPropagationStopped())break a;
gh(e,h,n);f=k}}}if(Tc)throw a=ue,Tc=!1,ue=null,a;}function B(a,b){var c=b[Me];void 0===c&&(c=b[Me]=new Set);var d=a+"__bubble";c.has(d)||(hh(b,a,2,!1),c.add(d))}function Ne(a,b,c){var d=0;b&&(d|=4);hh(c,a,d,b)}function sc(a){if(!a[id]){a[id]=!0;cg.forEach(function(b){"selectionchange"!==b&&(Uj.has(b)||Ne(b,!1,a),Ne(b,!0,a))});var b=9===a.nodeType?a:a.ownerDocument;null===b||b[id]||(b[id]=!0,Ne("selectionchange",!1,b))}}function hh(a,b,c,d,e){switch(Lg(b)){case 1:e=zj;break;case 4:e=Aj;break;default:e=
Be}c=e.bind(null,b,c,a);e=void 0;!Oe||"touchstart"!==b&&"touchmove"!==b&&"wheel"!==b||(e=!0);d?void 0!==e?a.addEventListener(b,c,{capture:!0,passive:e}):a.addEventListener(b,c,!0):void 0!==e?a.addEventListener(b,c,{passive:e}):a.addEventListener(b,c,!1)}function Ce(a,b,c,d,e){var f=d;if(0===(b&1)&&0===(b&2)&&null!==d)a:for(;;){if(null===d)return;var g=d.tag;if(3===g||4===g){var h=d.stateNode.containerInfo;if(h===e||8===h.nodeType&&h.parentNode===e)break;if(4===g)for(g=d.return;null!==g;){var k=g.tag;
if(3===k||4===k)if(k=g.stateNode.containerInfo,k===e||8===k.nodeType&&k.parentNode===e)return;g=g.return}for(;null!==h;){g=ob(h);if(null===g)return;k=g.tag;if(5===k||6===k){d=f=g;continue a}h=h.parentNode}}d=d.return}wg(function(){var d=f,e=re(c),g=[];a:{var h=fh.get(a);if(void 0!==h){var k=He,m=a;switch(a){case "keypress":if(0===cd(c))break a;case "keydown":case "keyup":k=Vj;break;case "focusin":m="focus";k=Pe;break;case "focusout":m="blur";k=Pe;break;case "beforeblur":case "afterblur":k=Pe;break;
case "click":if(2===c.button)break a;case "auxclick":case "dblclick":case "mousedown":case "mousemove":case "mouseup":case "mouseout":case "mouseover":case "contextmenu":k=ih;break;case "drag":case "dragend":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "dragstart":case "drop":k=Wj;break;case "touchcancel":case "touchend":case "touchmove":case "touchstart":k=Xj;break;case jh:case kh:case lh:k=Yj;break;case mh:k=Zj;break;case "scroll":k=ak;break;case "wheel":k=bk;break;case "copy":case "cut":case "paste":k=
ck;break;case "gotpointercapture":case "lostpointercapture":case "pointercancel":case "pointerdown":case "pointermove":case "pointerout":case "pointerover":case "pointerup":k=nh}var l=0!==(b&4),p=!l&&"scroll"===a,w=l?null!==h?h+"Capture":null:h;l=[];for(var A=d,t;null!==A;){t=A;var M=t.stateNode;5===t.tag&&null!==M&&(t=M,null!==w&&(M=fc(A,w),null!=M&&l.push(tc(A,M,t))));if(p)break;A=A.return}0<l.length&&(h=new k(h,m,null,c,e),g.push({event:h,listeners:l}))}}if(0===(b&7)){a:{h="mouseover"===a||"pointerover"===
a;k="mouseout"===a||"pointerout"===a;if(h&&c!==ze&&(m=c.relatedTarget||c.fromElement)&&(ob(m)||m[Ja]))break a;if(k||h){h=e.window===e?e:(h=e.ownerDocument)?h.defaultView||h.parentWindow:window;if(k){if(m=c.relatedTarget||c.toElement,k=d,m=m?ob(m):null,null!==m&&(p=nb(m),m!==p||5!==m.tag&&6!==m.tag))m=null}else k=null,m=d;if(k!==m){l=ih;M="onMouseLeave";w="onMouseEnter";A="mouse";if("pointerout"===a||"pointerover"===a)l=nh,M="onPointerLeave",w="onPointerEnter",A="pointer";p=null==k?h:Ib(k);t=null==
m?h:Ib(m);h=new l(M,A+"leave",k,c,e);h.target=p;h.relatedTarget=t;M=null;ob(e)===d&&(l=new l(w,A+"enter",m,c,e),l.target=t,l.relatedTarget=p,M=l);p=M;if(k&&m)b:{l=k;w=m;A=0;for(t=l;t;t=Lb(t))A++;t=0;for(M=w;M;M=Lb(M))t++;for(;0<A-t;)l=Lb(l),A--;for(;0<t-A;)w=Lb(w),t--;for(;A--;){if(l===w||null!==w&&l===w.alternate)break b;l=Lb(l);w=Lb(w)}l=null}else l=null;null!==k&&oh(g,h,k,l,!1);null!==m&&null!==p&&oh(g,p,m,l,!0)}}}a:{h=d?Ib(d):window;k=h.nodeName&&h.nodeName.toLowerCase();if("select"===k||"input"===
k&&"file"===h.type)var ma=Nj;else if(Vg(h))if(ph)ma=Rj;else{ma=Pj;var va=Oj}else(k=h.nodeName)&&"input"===k.toLowerCase()&&("checkbox"===h.type||"radio"===h.type)&&(ma=Qj);if(ma&&(ma=ma(a,d))){Wg(g,ma,c,e);break a}va&&va(a,h,d);"focusout"===a&&(va=h._wrapperState)&&va.controlled&&"number"===h.type&&me(h,"number",h.value)}va=d?Ib(d):window;switch(a){case "focusin":if(Vg(va)||"true"===va.contentEditable)Jb=va,Ke=d,rc=null;break;case "focusout":rc=Ke=Jb=null;break;case "mousedown":Je=!0;break;case "contextmenu":case "mouseup":case "dragend":Je=
!1;dh(g,c,e);break;case "selectionchange":if(dk)break;case "keydown":case "keyup":dh(g,c,e)}var ab;if(Ge)b:{switch(a){case "compositionstart":var da="onCompositionStart";break b;case "compositionend":da="onCompositionEnd";break b;case "compositionupdate":da="onCompositionUpdate";break b}da=void 0}else Hb?Qg(a,c)&&(da="onCompositionEnd"):"keydown"===a&&229===c.keyCode&&(da="onCompositionStart");da&&(Ug&&"ko"!==c.locale&&(Hb||"onCompositionStart"!==da?"onCompositionEnd"===da&&Hb&&(ab=Og()):(Za=e,Ee=
"value"in Za?Za.value:Za.textContent,Hb=!0)),va=ed(d,da),0<va.length&&(da=new qh(da,a,null,c,e),g.push({event:da,listeners:va}),ab?da.data=ab:(ab=Rg(c),null!==ab&&(da.data=ab))));if(ab=ek?Ij(a,c):Jj(a,c))d=ed(d,"onBeforeInput"),0<d.length&&(e=new fk("onBeforeInput","beforeinput",null,c,e),g.push({event:e,listeners:d}),e.data=ab)}Xg(g,b)})}function tc(a,b,c){return{instance:a,listener:b,currentTarget:c}}function ed(a,b){for(var c=b+"Capture",d=[];null!==a;){var e=a,f=e.stateNode;5===e.tag&&null!==
f&&(e=f,f=fc(a,c),null!=f&&d.unshift(tc(a,f,e)),f=fc(a,b),null!=f&&d.push(tc(a,f,e)));a=a.return}return d}function Lb(a){if(null===a)return null;do a=a.return;while(a&&5!==a.tag);return a?a:null}function oh(a,b,c,d,e){for(var f=b._reactName,g=[];null!==c&&c!==d;){var h=c,k=h.alternate,n=h.stateNode;if(null!==k&&k===d)break;5===h.tag&&null!==n&&(h=n,e?(k=fc(c,f),null!=k&&g.unshift(tc(c,k,h))):e||(k=fc(c,f),null!=k&&g.push(tc(c,k,h))));c=c.return}0!==g.length&&a.push({event:b,listeners:g})}function rh(a){return("string"===
typeof a?a:""+a).replace(gk,"\n").replace(hk,"")}function jd(a,b,c,d){b=rh(b);if(rh(a)!==b&&c)throw Error(m(425));}function kd(){}function Qe(a,b){return"textarea"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function ik(a){setTimeout(function(){throw a;})}function Re(a,b){var c=b,d=0;do{var e=c.nextSibling;a.removeChild(c);if(e&&8===e.nodeType)if(c=
e.data,"/$"===c){if(0===d){a.removeChild(e);nc(b);return}d--}else"$"!==c&&"$?"!==c&&"$!"!==c||d++;c=e}while(c);nc(b)}function Ka(a){for(;null!=a;a=a.nextSibling){var b=a.nodeType;if(1===b||3===b)break;if(8===b){b=a.data;if("$"===b||"$!"===b||"$?"===b)break;if("/$"===b)return null}}return a}function sh(a){a=a.previousSibling;for(var b=0;a;){if(8===a.nodeType){var c=a.data;if("$"===c||"$!"===c||"$?"===c){if(0===b)return a;b--}else"/$"===c&&b++}a=a.previousSibling}return null}function ob(a){var b=a[Da];
if(b)return b;for(var c=a.parentNode;c;){if(b=c[Ja]||c[Da]){c=b.alternate;if(null!==b.child||null!==c&&null!==c.child)for(a=sh(a);null!==a;){if(c=a[Da])return c;a=sh(a)}return b}a=c;c=a.parentNode}return null}function ec(a){a=a[Da]||a[Ja];return!a||5!==a.tag&&6!==a.tag&&13!==a.tag&&3!==a.tag?null:a}function Ib(a){if(5===a.tag||6===a.tag)return a.stateNode;throw Error(m(33));}function Rc(a){return a[uc]||null}function bb(a){return{current:a}}function v(a,b){0>Mb||(a.current=Se[Mb],Se[Mb]=null,Mb--)}
function y(a,b,c){Mb++;Se[Mb]=a.current;a.current=b}function Nb(a,b){var c=a.type.contextTypes;if(!c)return cb;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function ea(a){a=a.childContextTypes;return null!==a&&void 0!==a}function th(a,b,c){if(J.current!==cb)throw Error(m(168));
y(J,b);y(S,c)}function uh(a,b,c){var d=a.stateNode;b=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in b))throw Error(m(108,gj(a)||"Unknown",e));return E({},c,d)}function ld(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||cb;pb=J.current;y(J,a);y(S,S.current);return!0}function vh(a,b,c){var d=a.stateNode;if(!d)throw Error(m(169));c?(a=uh(a,b,pb),d.__reactInternalMemoizedMergedChildContext=a,v(S),v(J),y(J,a)):v(S);
y(S,c)}function wh(a){null===La?La=[a]:La.push(a)}function jk(a){md=!0;wh(a)}function db(){if(!Te&&null!==La){Te=!0;var a=0,b=z;try{var c=La;for(z=1;a<c.length;a++){var d=c[a];do d=d(!0);while(null!==d)}La=null;md=!1}catch(e){throw null!==La&&(La=La.slice(a+1)),xh(De,db),e;}finally{z=b,Te=!1}}return null}function qb(a,b){Ob[Pb++]=nd;Ob[Pb++]=od;od=a;nd=b}function yh(a,b,c){na[oa++]=Ma;na[oa++]=Na;na[oa++]=rb;rb=a;var d=Ma;a=Na;var e=32-ta(d)-1;d&=~(1<<e);c+=1;var f=32-ta(b)+e;if(30<f){var g=e-e%5;
f=(d&(1<<g)-1).toString(32);d>>=g;e-=g;Ma=1<<32-ta(b)+e|c<<e|d;Na=f+a}else Ma=1<<f|c<<e|d,Na=a}function Ue(a){null!==a.return&&(qb(a,1),yh(a,1,0))}function Ve(a){for(;a===od;)od=Ob[--Pb],Ob[Pb]=null,nd=Ob[--Pb],Ob[Pb]=null;for(;a===rb;)rb=na[--oa],na[oa]=null,Na=na[--oa],na[oa]=null,Ma=na[--oa],na[oa]=null}function zh(a,b){var c=pa(5,null,null,0);c.elementType="DELETED";c.stateNode=b;c.return=a;b=a.deletions;null===b?(a.deletions=[c],a.flags|=16):b.push(c)}function Ah(a,b){switch(a.tag){case 5:var c=
a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,la=a,fa=Ka(b.firstChild),!0):!1;case 6:return b=""===a.pendingProps||3!==b.nodeType?null:b,null!==b?(a.stateNode=b,la=a,fa=null,!0):!1;case 13:return b=8!==b.nodeType?null:b,null!==b?(c=null!==rb?{id:Ma,overflow:Na}:null,a.memoizedState={dehydrated:b,treeContext:c,retryLane:1073741824},c=pa(18,null,null,0),c.stateNode=b,c.return=a,a.child=c,la=a,fa=null,!0):!1;default:return!1}}function We(a){return 0!==
(a.mode&1)&&0===(a.flags&128)}function Xe(a){if(D){var b=fa;if(b){var c=b;if(!Ah(a,b)){if(We(a))throw Error(m(418));b=Ka(c.nextSibling);var d=la;b&&Ah(a,b)?zh(d,c):(a.flags=a.flags&-4097|2,D=!1,la=a)}}else{if(We(a))throw Error(m(418));a.flags=a.flags&-4097|2;D=!1;la=a}}}function Bh(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&13!==a.tag;)a=a.return;la=a}function pd(a){if(a!==la)return!1;if(!D)return Bh(a),D=!0,!1;var b;(b=3!==a.tag)&&!(b=5!==a.tag)&&(b=a.type,b="head"!==b&&"body"!==b&&!Qe(a.type,
a.memoizedProps));if(b&&(b=fa)){if(We(a)){for(a=fa;a;)a=Ka(a.nextSibling);throw Error(m(418));}for(;b;)zh(a,b),b=Ka(b.nextSibling)}Bh(a);if(13===a.tag){a=a.memoizedState;a=null!==a?a.dehydrated:null;if(!a)throw Error(m(317));a:{a=a.nextSibling;for(b=0;a;){if(8===a.nodeType){var c=a.data;if("/$"===c){if(0===b){fa=Ka(a.nextSibling);break a}b--}else"$"!==c&&"$!"!==c&&"$?"!==c||b++}a=a.nextSibling}fa=null}}else fa=la?Ka(a.stateNode.nextSibling):null;return!0}function Qb(){fa=la=null;D=!1}function Ye(a){null===
wa?wa=[a]:wa.push(a)}function vc(a,b,c){a=c.ref;if(null!==a&&"function"!==typeof a&&"object"!==typeof a){if(c._owner){c=c._owner;if(c){if(1!==c.tag)throw Error(m(309));var d=c.stateNode}if(!d)throw Error(m(147,a));var e=d,f=""+a;if(null!==b&&null!==b.ref&&"function"===typeof b.ref&&b.ref._stringRef===f)return b.ref;b=function(a){var b=e.refs;null===a?delete b[f]:b[f]=a};b._stringRef=f;return b}if("string"!==typeof a)throw Error(m(284));if(!c._owner)throw Error(m(290,a));}return a}function qd(a,b){a=
Object.prototype.toString.call(b);throw Error(m(31,"[object Object]"===a?"object with keys {"+Object.keys(b).join(", ")+"}":a));}function Ch(a){var b=a._init;return b(a._payload)}function Dh(a){function b(b,c){if(a){var d=b.deletions;null===d?(b.deletions=[c],b.flags|=16):d.push(c)}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,b),b=b.sibling;return a}function e(a,b){a=eb(a,b);a.index=
0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return b.flags|=1048576,c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.flags|=2,c):d;b.flags|=2;return c}function g(b){a&&null===b.alternate&&(b.flags|=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=Ze(c,a.mode,d),b.return=a,b;b=e(b,c);b.return=a;return b}function k(a,b,c,d){var f=c.type;if(f===Bb)return l(a,b,c.props.children,d,c.key);if(null!==b&&(b.elementType===f||"object"===typeof f&&null!==f&&f.$$typeof===Ta&&
Ch(f)===b.type))return d=e(b,c.props),d.ref=vc(a,b,c),d.return=a,d;d=rd(c.type,c.key,c.props,null,a.mode,d);d.ref=vc(a,b,c);d.return=a;return d}function n(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==c.containerInfo||b.stateNode.implementation!==c.implementation)return b=$e(c,a.mode,d),b.return=a,b;b=e(b,c.children||[]);b.return=a;return b}function l(a,b,c,d,f){if(null===b||7!==b.tag)return b=sb(c,a.mode,d,f),b.return=a,b;b=e(b,c);b.return=a;return b}function u(a,b,c){if("string"===
typeof b&&""!==b||"number"===typeof b)return b=Ze(""+b,a.mode,c),b.return=a,b;if("object"===typeof b&&null!==b){switch(b.$$typeof){case sd:return c=rd(b.type,b.key,b.props,null,a.mode,c),c.ref=vc(a,null,b),c.return=a,c;case Cb:return b=$e(b,a.mode,c),b.return=a,b;case Ta:var d=b._init;return u(a,d(b._payload),c)}if(cc(b)||ac(b))return b=sb(b,a.mode,c,null),b.return=a,b;qd(a,b)}return null}function r(a,b,c,d){var e=null!==b?b.key:null;if("string"===typeof c&&""!==c||"number"===typeof c)return null!==
e?null:h(a,b,""+c,d);if("object"===typeof c&&null!==c){switch(c.$$typeof){case sd:return c.key===e?k(a,b,c,d):null;case Cb:return c.key===e?n(a,b,c,d):null;case Ta:return e=c._init,r(a,b,e(c._payload),d)}if(cc(c)||ac(c))return null!==e?null:l(a,b,c,d,null);qd(a,c)}return null}function p(a,b,c,d,e){if("string"===typeof d&&""!==d||"number"===typeof d)return a=a.get(c)||null,h(b,a,""+d,e);if("object"===typeof d&&null!==d){switch(d.$$typeof){case sd:return a=a.get(null===d.key?c:d.key)||null,k(b,a,d,
e);case Cb:return a=a.get(null===d.key?c:d.key)||null,n(b,a,d,e);case Ta:var f=d._init;return p(a,b,c,f(d._payload),e)}if(cc(d)||ac(d))return a=a.get(c)||null,l(b,a,d,e,null);qd(b,d)}return null}function x(e,g,h,k){for(var n=null,m=null,l=g,t=g=0,q=null;null!==l&&t<h.length;t++){l.index>t?(q=l,l=null):q=l.sibling;var A=r(e,l,h[t],k);if(null===A){null===l&&(l=q);break}a&&l&&null===A.alternate&&b(e,l);g=f(A,g,t);null===m?n=A:m.sibling=A;m=A;l=q}if(t===h.length)return c(e,l),D&&qb(e,t),n;if(null===l){for(;t<
h.length;t++)l=u(e,h[t],k),null!==l&&(g=f(l,g,t),null===m?n=l:m.sibling=l,m=l);D&&qb(e,t);return n}for(l=d(e,l);t<h.length;t++)q=p(l,e,t,h[t],k),null!==q&&(a&&null!==q.alternate&&l.delete(null===q.key?t:q.key),g=f(q,g,t),null===m?n=q:m.sibling=q,m=q);a&&l.forEach(function(a){return b(e,a)});D&&qb(e,t);return n}function I(e,g,h,k){var n=ac(h);if("function"!==typeof n)throw Error(m(150));h=n.call(h);if(null==h)throw Error(m(151));for(var l=n=null,q=g,t=g=0,A=null,w=h.next();null!==q&&!w.done;t++,w=
h.next()){q.index>t?(A=q,q=null):A=q.sibling;var x=r(e,q,w.value,k);if(null===x){null===q&&(q=A);break}a&&q&&null===x.alternate&&b(e,q);g=f(x,g,t);null===l?n=x:l.sibling=x;l=x;q=A}if(w.done)return c(e,q),D&&qb(e,t),n;if(null===q){for(;!w.done;t++,w=h.next())w=u(e,w.value,k),null!==w&&(g=f(w,g,t),null===l?n=w:l.sibling=w,l=w);D&&qb(e,t);return n}for(q=d(e,q);!w.done;t++,w=h.next())w=p(q,e,t,w.value,k),null!==w&&(a&&null!==w.alternate&&q.delete(null===w.key?t:w.key),g=f(w,g,t),null===l?n=w:l.sibling=
w,l=w);a&&q.forEach(function(a){return b(e,a)});D&&qb(e,t);return n}function v(a,d,f,h){"object"===typeof f&&null!==f&&f.type===Bb&&null===f.key&&(f=f.props.children);if("object"===typeof f&&null!==f){switch(f.$$typeof){case sd:a:{for(var k=f.key,n=d;null!==n;){if(n.key===k){k=f.type;if(k===Bb){if(7===n.tag){c(a,n.sibling);d=e(n,f.props.children);d.return=a;a=d;break a}}else if(n.elementType===k||"object"===typeof k&&null!==k&&k.$$typeof===Ta&&Ch(k)===n.type){c(a,n.sibling);d=e(n,f.props);d.ref=vc(a,
n,f);d.return=a;a=d;break a}c(a,n);break}else b(a,n);n=n.sibling}f.type===Bb?(d=sb(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=rd(f.type,f.key,f.props,null,a.mode,h),h.ref=vc(a,d,f),h.return=a,a=h)}return g(a);case Cb:a:{for(n=f.key;null!==d;){if(d.key===n)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=$e(f,a.mode,h);d.return=a;
a=d}return g(a);case Ta:return n=f._init,v(a,d,n(f._payload),h)}if(cc(f))return x(a,d,f,h);if(ac(f))return I(a,d,f,h);qd(a,f)}return"string"===typeof f&&""!==f||"number"===typeof f?(f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=Ze(f,a.mode,h),d.return=a,a=d),g(a)):c(a,d)}return v}function af(){bf=Rb=td=null}function cf(a,b){b=ud.current;v(ud);a._currentValue=b}function df(a,b,c){for(;null!==a;){var d=a.alternate;(a.childLanes&b)!==b?(a.childLanes|=b,null!==d&&(d.childLanes|=
b)):null!==d&&(d.childLanes&b)!==b&&(d.childLanes|=b);if(a===c)break;a=a.return}}function Sb(a,b){td=a;bf=Rb=null;a=a.dependencies;null!==a&&null!==a.firstContext&&(0!==(a.lanes&b)&&(ha=!0),a.firstContext=null)}function qa(a){var b=a._currentValue;if(bf!==a)if(a={context:a,memoizedValue:b,next:null},null===Rb){if(null===td)throw Error(m(308));Rb=a;td.dependencies={lanes:0,firstContext:a}}else Rb=Rb.next=a;return b}function ef(a){null===tb?tb=[a]:tb.push(a)}function Eh(a,b,c,d){var e=b.interleaved;
null===e?(c.next=c,ef(b)):(c.next=e.next,e.next=c);b.interleaved=c;return Oa(a,d)}function Oa(a,b){a.lanes|=b;var c=a.alternate;null!==c&&(c.lanes|=b);c=a;for(a=a.return;null!==a;)a.childLanes|=b,c=a.alternate,null!==c&&(c.childLanes|=b),c=a,a=a.return;return 3===c.tag?c.stateNode:null}function ff(a){a.updateQueue={baseState:a.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Fh(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue=
{baseState:a.baseState,firstBaseUpdate:a.firstBaseUpdate,lastBaseUpdate:a.lastBaseUpdate,shared:a.shared,effects:a.effects})}function Pa(a,b){return{eventTime:a,lane:b,tag:0,payload:null,callback:null,next:null}}function fb(a,b,c){var d=a.updateQueue;if(null===d)return null;d=d.shared;if(0!==(p&2)){var e=d.pending;null===e?b.next=b:(b.next=e.next,e.next=b);d.pending=b;return kk(a,c)}e=d.interleaved;null===e?(b.next=b,ef(d)):(b.next=e.next,e.next=b);d.interleaved=b;return Oa(a,c)}function vd(a,b,c){b=
b.updateQueue;if(null!==b&&(b=b.shared,0!==(c&4194240))){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function Gh(a,b){var c=a.updateQueue,d=a.alternate;if(null!==d&&(d=d.updateQueue,c===d)){var e=null,f=null;c=c.firstBaseUpdate;if(null!==c){do{var g={eventTime:c.eventTime,lane:c.lane,tag:c.tag,payload:c.payload,callback:c.callback,next:null};null===f?e=f=g:f=f.next=g;c=c.next}while(null!==c);null===f?e=f=b:f=f.next=b}else e=f=b;c={baseState:d.baseState,firstBaseUpdate:e,lastBaseUpdate:f,
shared:d.shared,effects:d.effects};a.updateQueue=c;return}a=c.lastBaseUpdate;null===a?c.firstBaseUpdate=b:a.next=b;c.lastBaseUpdate=b}function wd(a,b,c,d){var e=a.updateQueue;gb=!1;var f=e.firstBaseUpdate,g=e.lastBaseUpdate,h=e.shared.pending;if(null!==h){e.shared.pending=null;var k=h,n=k.next;k.next=null;null===g?f=n:g.next=n;g=k;var l=a.alternate;null!==l&&(l=l.updateQueue,h=l.lastBaseUpdate,h!==g&&(null===h?l.firstBaseUpdate=n:h.next=n,l.lastBaseUpdate=k))}if(null!==f){var m=e.baseState;g=0;l=
n=k=null;h=f;do{var r=h.lane,p=h.eventTime;if((d&r)===r){null!==l&&(l=l.next={eventTime:p,lane:0,tag:h.tag,payload:h.payload,callback:h.callback,next:null});a:{var x=a,v=h;r=b;p=c;switch(v.tag){case 1:x=v.payload;if("function"===typeof x){m=x.call(p,m,r);break a}m=x;break a;case 3:x.flags=x.flags&-65537|128;case 0:x=v.payload;r="function"===typeof x?x.call(p,m,r):x;if(null===r||void 0===r)break a;m=E({},m,r);break a;case 2:gb=!0}}null!==h.callback&&0!==h.lane&&(a.flags|=64,r=e.effects,null===r?e.effects=
[h]:r.push(h))}else p={eventTime:p,lane:r,tag:h.tag,payload:h.payload,callback:h.callback,next:null},null===l?(n=l=p,k=m):l=l.next=p,g|=r;h=h.next;if(null===h)if(h=e.shared.pending,null===h)break;else r=h,h=r.next,r.next=null,e.lastBaseUpdate=r,e.shared.pending=null}while(1);null===l&&(k=m);e.baseState=k;e.firstBaseUpdate=n;e.lastBaseUpdate=l;b=e.shared.interleaved;if(null!==b){e=b;do g|=e.lane,e=e.next;while(e!==b)}else null===f&&(e.shared.lanes=0);ra|=g;a.lanes=g;a.memoizedState=m}}function Hh(a,
b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;b<a.length;b++){var d=a[b],e=d.callback;if(null!==e){d.callback=null;d=c;if("function"!==typeof e)throw Error(m(191,e));e.call(d)}}}function ub(a){if(a===wc)throw Error(m(174));return a}function gf(a,b){y(xc,b);y(yc,a);y(Ea,wc);a=b.nodeType;switch(a){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:oe(null,"");break;default:a=8===a?b.parentNode:b,b=a.namespaceURI||null,a=a.tagName,b=oe(b,a)}v(Ea);y(Ea,b)}function Tb(a){v(Ea);v(yc);v(xc)}function Ih(a){ub(xc.current);
var b=ub(Ea.current);var c=oe(b,a.type);b!==c&&(y(yc,a),y(Ea,c))}function hf(a){yc.current===a&&(v(Ea),v(yc))}function xd(a){for(var b=a;null!==b;){if(13===b.tag){var c=b.memoizedState;if(null!==c&&(c=c.dehydrated,null===c||"$?"===c.data||"$!"===c.data))return b}else if(19===b.tag&&void 0!==b.memoizedProps.revealOrder){if(0!==(b.flags&128))return b}else if(null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return null;b=b.return}b.sibling.return=
b.return;b=b.sibling}return null}function jf(){for(var a=0;a<kf.length;a++)kf[a]._workInProgressVersionPrimary=null;kf.length=0}function V(){throw Error(m(321));}function lf(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!ua(a[c],b[c]))return!1;return!0}function mf(a,b,c,d,e,f){vb=f;C=b;b.memoizedState=null;b.updateQueue=null;b.lanes=0;yd.current=null===a||null===a.memoizedState?lk:mk;a=c(d,e);if(zc){f=0;do{zc=!1;Ac=0;if(25<=f)throw Error(m(301));f+=1;N=K=null;b.updateQueue=null;
yd.current=nk;a=c(d,e)}while(zc)}yd.current=zd;b=null!==K&&null!==K.next;vb=0;N=K=C=null;Ad=!1;if(b)throw Error(m(300));return a}function nf(){var a=0!==Ac;Ac=0;return a}function Fa(){var a={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};null===N?C.memoizedState=N=a:N=N.next=a;return N}function sa(){if(null===K){var a=C.alternate;a=null!==a?a.memoizedState:null}else a=K.next;var b=null===N?C.memoizedState:N.next;if(null!==b)N=b,K=a;else{if(null===a)throw Error(m(310));K=a;
a={memoizedState:K.memoizedState,baseState:K.baseState,baseQueue:K.baseQueue,queue:K.queue,next:null};null===N?C.memoizedState=N=a:N=N.next=a}return N}function Bc(a,b){return"function"===typeof b?b(a):b}function of(a,b,c){b=sa();c=b.queue;if(null===c)throw Error(m(311));c.lastRenderedReducer=a;var d=K,e=d.baseQueue,f=c.pending;if(null!==f){if(null!==e){var g=e.next;e.next=f.next;f.next=g}d.baseQueue=e=f;c.pending=null}if(null!==e){f=e.next;d=d.baseState;var h=g=null,k=null,n=f;do{var l=n.lane;if((vb&
l)===l)null!==k&&(k=k.next={lane:0,action:n.action,hasEagerState:n.hasEagerState,eagerState:n.eagerState,next:null}),d=n.hasEagerState?n.eagerState:a(d,n.action);else{var u={lane:l,action:n.action,hasEagerState:n.hasEagerState,eagerState:n.eagerState,next:null};null===k?(h=k=u,g=d):k=k.next=u;C.lanes|=l;ra|=l}n=n.next}while(null!==n&&n!==f);null===k?g=d:k.next=h;ua(d,b.memoizedState)||(ha=!0);b.memoizedState=d;b.baseState=g;b.baseQueue=k;c.lastRenderedState=d}a=c.interleaved;if(null!==a){e=a;do f=
e.lane,C.lanes|=f,ra|=f,e=e.next;while(e!==a)}else null===e&&(c.lanes=0);return[b.memoizedState,c.dispatch]}function pf(a,b,c){b=sa();c=b.queue;if(null===c)throw Error(m(311));c.lastRenderedReducer=a;var d=c.dispatch,e=c.pending,f=b.memoizedState;if(null!==e){c.pending=null;var g=e=e.next;do f=a(f,g.action),g=g.next;while(g!==e);ua(f,b.memoizedState)||(ha=!0);b.memoizedState=f;null===b.baseQueue&&(b.baseState=f);c.lastRenderedState=f}return[f,d]}function Jh(a,b,c){}function Kh(a,b,c){c=C;var d=sa(),
e=b(),f=!ua(d.memoizedState,e);f&&(d.memoizedState=e,ha=!0);d=d.queue;qf(Lh.bind(null,c,d,a),[a]);if(d.getSnapshot!==b||f||null!==N&&N.memoizedState.tag&1){c.flags|=2048;Cc(9,Mh.bind(null,c,d,e,b),void 0,null);if(null===O)throw Error(m(349));0!==(vb&30)||Nh(c,b,e)}return e}function Nh(a,b,c){a.flags|=16384;a={getSnapshot:b,value:c};b=C.updateQueue;null===b?(b={lastEffect:null,stores:null},C.updateQueue=b,b.stores=[a]):(c=b.stores,null===c?b.stores=[a]:c.push(a))}function Mh(a,b,c,d){b.value=c;b.getSnapshot=
d;Oh(b)&&Ph(a)}function Lh(a,b,c){return c(function(){Oh(b)&&Ph(a)})}function Oh(a){var b=a.getSnapshot;a=a.value;try{var c=b();return!ua(a,c)}catch(d){return!0}}function Ph(a){var b=Oa(a,1);null!==b&&xa(b,a,1,-1)}function Qh(a){var b=Fa();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:Bc,lastRenderedState:a};b.queue=a;a=a.dispatch=ok.bind(null,C,a);return[b.memoizedState,a]}function Cc(a,b,c,d){a={tag:a,create:b,
destroy:c,deps:d,next:null};b=C.updateQueue;null===b?(b={lastEffect:null,stores:null},C.updateQueue=b,b.lastEffect=a.next=a):(c=b.lastEffect,null===c?b.lastEffect=a.next=a:(d=c.next,c.next=a,a.next=d,b.lastEffect=a));return a}function Rh(a){return sa().memoizedState}function Bd(a,b,c,d){var e=Fa();C.flags|=a;e.memoizedState=Cc(1|b,c,void 0,void 0===d?null:d)}function Cd(a,b,c,d){var e=sa();d=void 0===d?null:d;var f=void 0;if(null!==K){var g=K.memoizedState;f=g.destroy;if(null!==d&&lf(d,g.deps)){e.memoizedState=
Cc(b,c,f,d);return}}C.flags|=a;e.memoizedState=Cc(1|b,c,f,d)}function Sh(a,b){return Bd(8390656,8,a,b)}function qf(a,b){return Cd(2048,8,a,b)}function Th(a,b){return Cd(4,2,a,b)}function Uh(a,b){return Cd(4,4,a,b)}function Vh(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function Wh(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Cd(4,4,Vh.bind(null,b,a),c)}function rf(a,b){}function Xh(a,b){var c=
sa();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&lf(b,d[1]))return d[0];c.memoizedState=[a,b];return a}function Yh(a,b){var c=sa();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&lf(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a}function Zh(a,b,c){if(0===(vb&21))return a.baseState&&(a.baseState=!1,ha=!0),a.memoizedState=c;ua(c,b)||(c=Dg(),C.lanes|=c,ra|=c,a.baseState=!0);return b}function pk(a,b,c){c=z;z=0!==c&&4>c?c:4;a(!0);var d=sf.transition;sf.transition=
{};try{a(!1),b()}finally{z=c,sf.transition=d}}function $h(){return sa().memoizedState}function qk(a,b,c){var d=hb(a);c={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ai(a))bi(b,c);else if(c=Eh(a,b,c,d),null!==c){var e=Z();xa(c,a,d,e);ci(c,b,d)}}function ok(a,b,c){var d=hb(a),e={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ai(a))bi(b,e);else{var f=a.alternate;if(0===a.lanes&&(null===f||0===f.lanes)&&(f=b.lastRenderedReducer,null!==f))try{var g=b.lastRenderedState,
h=f(g,c);e.hasEagerState=!0;e.eagerState=h;if(ua(h,g)){var k=b.interleaved;null===k?(e.next=e,ef(b)):(e.next=k.next,k.next=e);b.interleaved=e;return}}catch(n){}finally{}c=Eh(a,b,e,d);null!==c&&(e=Z(),xa(c,a,d,e),ci(c,b,d))}}function ai(a){var b=a.alternate;return a===C||null!==b&&b===C}function bi(a,b){zc=Ad=!0;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b}function ci(a,b,c){if(0!==(c&4194240)){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function ya(a,b){if(a&&
a.defaultProps){b=E({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return b}function tf(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:E({},b,c);a.memoizedState=c;0===a.lanes&&(a.updateQueue.baseState=c)}function di(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,f,g):b.prototype&&b.prototype.isPureReactComponent?!qc(c,d)||!qc(e,f):!0}function ei(a,b,c){var d=!1,e=cb;var f=b.contextType;"object"===typeof f&&
null!==f?f=qa(f):(e=ea(b)?pb:J.current,d=b.contextTypes,f=(d=null!==d&&void 0!==d)?Nb(a,e):cb);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=Dd;a.stateNode=b;b._reactInternals=a;d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=e,a.__reactInternalMemoizedMaskedChildContext=f);return b}function fi(a,b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&&
b.UNSAFE_componentWillReceiveProps(c,d);b.state!==a&&Dd.enqueueReplaceState(b,b.state,null)}function uf(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs={};ff(a);var f=b.contextType;"object"===typeof f&&null!==f?e.context=qa(f):(f=ea(b)?pb:J.current,e.context=Nb(a,f));e.state=a.memoizedState;f=b.getDerivedStateFromProps;"function"===typeof f&&(tf(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!==
typeof e.UNSAFE_componentWillMount&&"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&Dd.enqueueReplaceState(e,e.state,null),wd(a,c,e,d),e.state=a.memoizedState);"function"===typeof e.componentDidMount&&(a.flags|=4194308)}function Ub(a,b){try{var c="",d=b;do c+=fj(d),d=d.return;while(d);var e=c}catch(f){e="\nError generating stack: "+f.message+
"\n"+f.stack}return{value:a,source:b,stack:e,digest:null}}function vf(a,b,c){return{value:a,source:null,stack:null!=c?c:null,digest:null!=b?b:null}}function wf(a,b){try{console.error(b.value)}catch(c){setTimeout(function(){throw c;})}}function gi(a,b,c){c=Pa(-1,c);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){Ed||(Ed=!0,xf=d);wf(a,b)};return c}function hi(a,b,c){c=Pa(-1,c);c.tag=3;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){return d(e)};
c.callback=function(){wf(a,b)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){wf(a,b);"function"!==typeof d&&(null===ib?ib=new Set([this]):ib.add(this));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:""})});return c}function ii(a,b,c){var d=a.pingCache;if(null===d){d=a.pingCache=new rk;var e=new Set;d.set(b,e)}else e=d.get(b),void 0===e&&(e=new Set,d.set(b,e));e.has(c)||(e.add(c),a=sk.bind(null,a,b,c),b.then(a,a))}function ji(a){do{var b;
if(b=13===a.tag)b=a.memoizedState,b=null!==b?null!==b.dehydrated?!0:!1:!0;if(b)return a;a=a.return}while(null!==a);return null}function ki(a,b,c,d,e){if(0===(a.mode&1))return a===b?a.flags|=65536:(a.flags|=128,c.flags|=131072,c.flags&=-52805,1===c.tag&&(null===c.alternate?c.tag=17:(b=Pa(-1,1),b.tag=2,fb(c,b,1))),c.lanes|=1),a;a.flags|=65536;a.lanes=e;return a}function aa(a,b,c,d){b.child=null===a?li(b,null,c,d):Vb(b,a.child,c,d)}function mi(a,b,c,d,e){c=c.render;var f=b.ref;Sb(b,e);d=mf(a,b,c,d,f,
e);c=nf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&c&&Ue(b);b.flags|=1;aa(a,b,d,e);return b.child}function ni(a,b,c,d,e){if(null===a){var f=c.type;if("function"===typeof f&&!yf(f)&&void 0===f.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=f,oi(a,b,f,d,e);a=rd(c.type,null,d,b,b.mode,e);a.ref=b.ref;a.return=b;return b.child=a}f=a.child;if(0===(a.lanes&e)){var g=f.memoizedProps;c=c.compare;c=null!==c?c:qc;if(c(g,d)&&a.ref===
b.ref)return Qa(a,b,e)}b.flags|=1;a=eb(f,d);a.ref=b.ref;a.return=b;return b.child=a}function oi(a,b,c,d,e){if(null!==a){var f=a.memoizedProps;if(qc(f,d)&&a.ref===b.ref)if(ha=!1,b.pendingProps=d=f,0!==(a.lanes&e))0!==(a.flags&131072)&&(ha=!0);else return b.lanes=a.lanes,Qa(a,b,e)}return zf(a,b,c,d,e)}function pi(a,b,c){var d=b.pendingProps,e=d.children,f=null!==a?a.memoizedState:null;if("hidden"===d.mode)if(0===(b.mode&1))b.memoizedState={baseLanes:0,cachePool:null,transitions:null},y(Ga,ba),ba|=c;
else{if(0===(c&1073741824))return a=null!==f?f.baseLanes|c:c,b.lanes=b.childLanes=1073741824,b.memoizedState={baseLanes:a,cachePool:null,transitions:null},b.updateQueue=null,y(Ga,ba),ba|=a,null;b.memoizedState={baseLanes:0,cachePool:null,transitions:null};d=null!==f?f.baseLanes:c;y(Ga,ba);ba|=d}else null!==f?(d=f.baseLanes|c,b.memoizedState=null):d=c,y(Ga,ba),ba|=d;aa(a,b,e,c);return b.child}function qi(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.flags|=512,b.flags|=2097152}function zf(a,
b,c,d,e){var f=ea(c)?pb:J.current;f=Nb(b,f);Sb(b,e);c=mf(a,b,c,d,f,e);d=nf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&d&&Ue(b);b.flags|=1;aa(a,b,c,e);return b.child}function ri(a,b,c,d,e){if(ea(c)){var f=!0;ld(b)}else f=!1;Sb(b,e);if(null===b.stateNode)Fd(a,b),ei(b,c,d),uf(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var k=g.context,n=c.contextType;"object"===typeof n&&null!==n?n=qa(n):(n=ea(c)?pb:J.current,n=Nb(b,
n));var l=c.getDerivedStateFromProps,m="function"===typeof l||"function"===typeof g.getSnapshotBeforeUpdate;m||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||k!==n)&&fi(b,g,d,n);gb=!1;var r=b.memoizedState;g.state=r;wd(b,d,g,e);k=b.memoizedState;h!==d||r!==k||S.current||gb?("function"===typeof l&&(tf(b,c,l,d),k=b.memoizedState),(h=gb||di(b,c,h,d,r,k,n))?(m||"function"!==typeof g.UNSAFE_componentWillMount&&"function"!==typeof g.componentWillMount||
("function"===typeof g.componentWillMount&&g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.flags|=4194308)):("function"===typeof g.componentDidMount&&(b.flags|=4194308),b.memoizedProps=d,b.memoizedState=k),g.props=d,g.state=k,g.context=n,d=h):("function"===typeof g.componentDidMount&&(b.flags|=4194308),d=!1)}else{g=b.stateNode;Fh(a,b);h=b.memoizedProps;n=b.type===b.elementType?h:ya(b.type,h);g.props=
n;m=b.pendingProps;r=g.context;k=c.contextType;"object"===typeof k&&null!==k?k=qa(k):(k=ea(c)?pb:J.current,k=Nb(b,k));var p=c.getDerivedStateFromProps;(l="function"===typeof p||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==m||r!==k)&&fi(b,g,d,k);gb=!1;r=b.memoizedState;g.state=r;wd(b,d,g,e);var x=b.memoizedState;h!==m||r!==x||S.current||gb?("function"===typeof p&&(tf(b,c,p,d),x=b.memoizedState),
(n=gb||di(b,c,n,d,r,x,k)||!1)?(l||"function"!==typeof g.UNSAFE_componentWillUpdate&&"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,x,k),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,x,k)),"function"===typeof g.componentDidUpdate&&(b.flags|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.flags|=1024)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=
4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),b.memoizedProps=d,b.memoizedState=x),g.props=d,g.state=x,g.context=k,d=n):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),d=!1)}return Af(a,b,c,d,f,e)}function Af(a,b,c,d,e,f){qi(a,b);var g=0!==(b.flags&128);if(!d&&!g)return e&&vh(b,c,!1),
Qa(a,b,f);d=b.stateNode;tk.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?null:d.render();b.flags|=1;null!==a&&g?(b.child=Vb(b,a.child,null,f),b.child=Vb(b,null,h,f)):aa(a,b,h,f);b.memoizedState=d.state;e&&vh(b,c,!0);return b.child}function si(a){var b=a.stateNode;b.pendingContext?th(a,b.pendingContext,b.pendingContext!==b.context):b.context&&th(a,b.context,!1);gf(a,b.containerInfo)}function ti(a,b,c,d,e){Qb();Ye(e);b.flags|=256;aa(a,b,c,d);return b.child}function Bf(a){return{baseLanes:a,
cachePool:null,transitions:null}}function ui(a,b,c){var d=b.pendingProps,e=F.current,f=!1,g=0!==(b.flags&128),h;(h=g)||(h=null!==a&&null===a.memoizedState?!1:0!==(e&2));if(h)f=!0,b.flags&=-129;else if(null===a||null!==a.memoizedState)e|=1;y(F,e&1);if(null===a){Xe(b);a=b.memoizedState;if(null!==a&&(a=a.dehydrated,null!==a))return 0===(b.mode&1)?b.lanes=1:"$!"===a.data?b.lanes=8:b.lanes=1073741824,null;g=d.children;a=d.fallback;return f?(d=b.mode,f=b.child,g={mode:"hidden",children:g},0===(d&1)&&null!==
f?(f.childLanes=0,f.pendingProps=g):f=Gd(g,d,0,null),a=sb(a,d,c,null),f.return=b,a.return=b,f.sibling=a,b.child=f,b.child.memoizedState=Bf(c),b.memoizedState=Cf,a):Df(b,g)}e=a.memoizedState;if(null!==e&&(h=e.dehydrated,null!==h))return uk(a,b,g,d,h,e,c);if(f){f=d.fallback;g=b.mode;e=a.child;h=e.sibling;var k={mode:"hidden",children:d.children};0===(g&1)&&b.child!==e?(d=b.child,d.childLanes=0,d.pendingProps=k,b.deletions=null):(d=eb(e,k),d.subtreeFlags=e.subtreeFlags&14680064);null!==h?f=eb(h,f):(f=
sb(f,g,c,null),f.flags|=2);f.return=b;d.return=b;d.sibling=f;b.child=d;d=f;f=b.child;g=a.child.memoizedState;g=null===g?Bf(c):{baseLanes:g.baseLanes|c,cachePool:null,transitions:g.transitions};f.memoizedState=g;f.childLanes=a.childLanes&~c;b.memoizedState=Cf;return d}f=a.child;a=f.sibling;d=eb(f,{mode:"visible",children:d.children});0===(b.mode&1)&&(d.lanes=c);d.return=b;d.sibling=null;null!==a&&(c=b.deletions,null===c?(b.deletions=[a],b.flags|=16):c.push(a));b.child=d;b.memoizedState=null;return d}
function Df(a,b,c){b=Gd({mode:"visible",children:b},a.mode,0,null);b.return=a;return a.child=b}function Hd(a,b,c,d){null!==d&&Ye(d);Vb(b,a.child,null,c);a=Df(b,b.pendingProps.children);a.flags|=2;b.memoizedState=null;return a}function uk(a,b,c,d,e,f,g){if(c){if(b.flags&256)return b.flags&=-257,d=vf(Error(m(422))),Hd(a,b,g,d);if(null!==b.memoizedState)return b.child=a.child,b.flags|=128,null;f=d.fallback;e=b.mode;d=Gd({mode:"visible",children:d.children},e,0,null);f=sb(f,e,g,null);f.flags|=2;d.return=
b;f.return=b;d.sibling=f;b.child=d;0!==(b.mode&1)&&Vb(b,a.child,null,g);b.child.memoizedState=Bf(g);b.memoizedState=Cf;return f}if(0===(b.mode&1))return Hd(a,b,g,null);if("$!"===e.data){d=e.nextSibling&&e.nextSibling.dataset;if(d)var h=d.dgst;d=h;f=Error(m(419));d=vf(f,d,void 0);return Hd(a,b,g,d)}h=0!==(g&a.childLanes);if(ha||h){d=O;if(null!==d){switch(g&-g){case 4:e=2;break;case 16:e=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:e=
32;break;case 536870912:e=268435456;break;default:e=0}e=0!==(e&(d.suspendedLanes|g))?0:e;0!==e&&e!==f.retryLane&&(f.retryLane=e,Oa(a,e),xa(d,a,e,-1))}Ef();d=vf(Error(m(421)));return Hd(a,b,g,d)}if("$?"===e.data)return b.flags|=128,b.child=a.child,b=vk.bind(null,a),e._reactRetry=b,null;a=f.treeContext;fa=Ka(e.nextSibling);la=b;D=!0;wa=null;null!==a&&(na[oa++]=Ma,na[oa++]=Na,na[oa++]=rb,Ma=a.id,Na=a.overflow,rb=b);b=Df(b,d.children);b.flags|=4096;return b}function vi(a,b,c){a.lanes|=b;var d=a.alternate;
null!==d&&(d.lanes|=b);df(a.return,b,c)}function Ff(a,b,c,d,e){var f=a.memoizedState;null===f?a.memoizedState={isBackwards:b,rendering:null,renderingStartTime:0,last:d,tail:c,tailMode:e}:(f.isBackwards=b,f.rendering=null,f.renderingStartTime=0,f.last=d,f.tail=c,f.tailMode=e)}function wi(a,b,c){var d=b.pendingProps,e=d.revealOrder,f=d.tail;aa(a,b,d.children,c);d=F.current;if(0!==(d&2))d=d&1|2,b.flags|=128;else{if(null!==a&&0!==(a.flags&128))a:for(a=b.child;null!==a;){if(13===a.tag)null!==a.memoizedState&&
vi(a,c,b);else if(19===a.tag)vi(a,c,b);else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===b)break a;for(;null===a.sibling;){if(null===a.return||a.return===b)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}d&=1}y(F,d);if(0===(b.mode&1))b.memoizedState=null;else switch(e){case "forwards":c=b.child;for(e=null;null!==c;)a=c.alternate,null!==a&&null===xd(a)&&(e=c),c=c.sibling;c=e;null===c?(e=b.child,b.child=null):(e=c.sibling,c.sibling=null);Ff(b,!1,e,c,f);break;case "backwards":c=
null;e=b.child;for(b.child=null;null!==e;){a=e.alternate;if(null!==a&&null===xd(a)){b.child=e;break}a=e.sibling;e.sibling=c;c=e;e=a}Ff(b,!0,c,null,f);break;case "together":Ff(b,!1,null,null,void 0);break;default:b.memoizedState=null}return b.child}function Fd(a,b){0===(b.mode&1)&&null!==a&&(a.alternate=null,b.alternate=null,b.flags|=2)}function Qa(a,b,c){null!==a&&(b.dependencies=a.dependencies);ra|=b.lanes;if(0===(c&b.childLanes))return null;if(null!==a&&b.child!==a.child)throw Error(m(153));if(null!==
b.child){a=b.child;c=eb(a,a.pendingProps);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=eb(a,a.pendingProps),c.return=b;c.sibling=null}return b.child}function wk(a,b,c){switch(b.tag){case 3:si(b);Qb();break;case 5:Ih(b);break;case 1:ea(b.type)&&ld(b);break;case 4:gf(b,b.stateNode.containerInfo);break;case 10:var d=b.type._context,e=b.memoizedProps.value;y(ud,d._currentValue);d._currentValue=e;break;case 13:d=b.memoizedState;if(null!==d){if(null!==d.dehydrated)return y(F,F.current&
1),b.flags|=128,null;if(0!==(c&b.child.childLanes))return ui(a,b,c);y(F,F.current&1);a=Qa(a,b,c);return null!==a?a.sibling:null}y(F,F.current&1);break;case 19:d=0!==(c&b.childLanes);if(0!==(a.flags&128)){if(d)return wi(a,b,c);b.flags|=128}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null,e.lastEffect=null);y(F,F.current);if(d)break;else return null;case 22:case 23:return b.lanes=0,pi(a,b,c)}return Qa(a,b,c)}function Dc(a,b){if(!D)switch(a.tailMode){case "hidden":b=a.tail;for(var c=null;null!==
b;)null!==b.alternate&&(c=b),b=b.sibling;null===c?a.tail=null:c.sibling=null;break;case "collapsed":c=a.tail;for(var d=null;null!==c;)null!==c.alternate&&(d=c),c=c.sibling;null===d?b||null===a.tail?a.tail=null:a.tail.sibling=null:d.sibling=null}}function W(a){var b=null!==a.alternate&&a.alternate.child===a.child,c=0,d=0;if(b)for(var e=a.child;null!==e;)c|=e.lanes|e.childLanes,d|=e.subtreeFlags&14680064,d|=e.flags&14680064,e.return=a,e=e.sibling;else for(e=a.child;null!==e;)c|=e.lanes|e.childLanes,
d|=e.subtreeFlags,d|=e.flags,e.return=a,e=e.sibling;a.subtreeFlags|=d;a.childLanes=c;return b}function xk(a,b,c){var d=b.pendingProps;Ve(b);switch(b.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return W(b),null;case 1:return ea(b.type)&&(v(S),v(J)),W(b),null;case 3:d=b.stateNode;Tb();v(S);v(J);jf();d.pendingContext&&(d.context=d.pendingContext,d.pendingContext=null);if(null===a||null===a.child)pd(b)?b.flags|=4:null===a||a.memoizedState.isDehydrated&&0===(b.flags&
256)||(b.flags|=1024,null!==wa&&(Gf(wa),wa=null));xi(a,b);W(b);return null;case 5:hf(b);var e=ub(xc.current);c=b.type;if(null!==a&&null!=b.stateNode)yk(a,b,c,d,e),a.ref!==b.ref&&(b.flags|=512,b.flags|=2097152);else{if(!d){if(null===b.stateNode)throw Error(m(166));W(b);return null}a=ub(Ea.current);if(pd(b)){d=b.stateNode;c=b.type;var f=b.memoizedProps;d[Da]=b;d[uc]=f;a=0!==(b.mode&1);switch(c){case "dialog":B("cancel",d);B("close",d);break;case "iframe":case "object":case "embed":B("load",d);break;
case "video":case "audio":for(e=0;e<Ec.length;e++)B(Ec[e],d);break;case "source":B("error",d);break;case "img":case "image":case "link":B("error",d);B("load",d);break;case "details":B("toggle",d);break;case "input":kg(d,f);B("invalid",d);break;case "select":d._wrapperState={wasMultiple:!!f.multiple};B("invalid",d);break;case "textarea":ng(d,f),B("invalid",d)}pe(c,f);e=null;for(var g in f)if(f.hasOwnProperty(g)){var h=f[g];"children"===g?"string"===typeof h?d.textContent!==h&&(!0!==f.suppressHydrationWarning&&
jd(d.textContent,h,a),e=["children",h]):"number"===typeof h&&d.textContent!==""+h&&(!0!==f.suppressHydrationWarning&&jd(d.textContent,h,a),e=["children",""+h]):$b.hasOwnProperty(g)&&null!=h&&"onScroll"===g&&B("scroll",d)}switch(c){case "input":Pc(d);mg(d,f,!0);break;case "textarea":Pc(d);pg(d);break;case "select":case "option":break;default:"function"===typeof f.onClick&&(d.onclick=kd)}d=e;b.updateQueue=d;null!==d&&(b.flags|=4)}else{g=9===e.nodeType?e:e.ownerDocument;"http://www.w3.org/1999/xhtml"===
a&&(a=qg(c));"http://www.w3.org/1999/xhtml"===a?"script"===c?(a=g.createElement("div"),a.innerHTML="<script>\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(c,{is:d.is}):(a=g.createElement(c),"select"===c&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,c);a[Da]=b;a[uc]=d;zk(a,b,!1,!1);b.stateNode=a;a:{g=qe(c,d);switch(c){case "dialog":B("cancel",a);B("close",a);e=d;break;case "iframe":case "object":case "embed":B("load",a);e=d;break;
case "video":case "audio":for(e=0;e<Ec.length;e++)B(Ec[e],a);e=d;break;case "source":B("error",a);e=d;break;case "img":case "image":case "link":B("error",a);B("load",a);e=d;break;case "details":B("toggle",a);e=d;break;case "input":kg(a,d);e=ke(a,d);B("invalid",a);break;case "option":e=d;break;case "select":a._wrapperState={wasMultiple:!!d.multiple};e=E({},d,{value:void 0});B("invalid",a);break;case "textarea":ng(a,d);e=ne(a,d);B("invalid",a);break;default:e=d}pe(c,e);h=e;for(f in h)if(h.hasOwnProperty(f)){var k=
h[f];"style"===f?sg(a,k):"dangerouslySetInnerHTML"===f?(k=k?k.__html:void 0,null!=k&&yi(a,k)):"children"===f?"string"===typeof k?("textarea"!==c||""!==k)&&Fc(a,k):"number"===typeof k&&Fc(a,""+k):"suppressContentEditableWarning"!==f&&"suppressHydrationWarning"!==f&&"autoFocus"!==f&&($b.hasOwnProperty(f)?null!=k&&"onScroll"===f&&B("scroll",a):null!=k&&$d(a,f,k,g))}switch(c){case "input":Pc(a);mg(a,d,!1);break;case "textarea":Pc(a);pg(a);break;case "option":null!=d.value&&a.setAttribute("value",""+Ua(d.value));
break;case "select":a.multiple=!!d.multiple;f=d.value;null!=f?Db(a,!!d.multiple,f,!1):null!=d.defaultValue&&Db(a,!!d.multiple,d.defaultValue,!0);break;default:"function"===typeof e.onClick&&(a.onclick=kd)}switch(c){case "button":case "input":case "select":case "textarea":d=!!d.autoFocus;break a;case "img":d=!0;break a;default:d=!1}}d&&(b.flags|=4)}null!==b.ref&&(b.flags|=512,b.flags|=2097152)}W(b);return null;case 6:if(a&&null!=b.stateNode)Ak(a,b,a.memoizedProps,d);else{if("string"!==typeof d&&null===
b.stateNode)throw Error(m(166));c=ub(xc.current);ub(Ea.current);if(pd(b)){d=b.stateNode;c=b.memoizedProps;d[Da]=b;if(f=d.nodeValue!==c)if(a=la,null!==a)switch(a.tag){case 3:jd(d.nodeValue,c,0!==(a.mode&1));break;case 5:!0!==a.memoizedProps.suppressHydrationWarning&&jd(d.nodeValue,c,0!==(a.mode&1))}f&&(b.flags|=4)}else d=(9===c.nodeType?c:c.ownerDocument).createTextNode(d),d[Da]=b,b.stateNode=d}W(b);return null;case 13:v(F);d=b.memoizedState;if(null===a||null!==a.memoizedState&&null!==a.memoizedState.dehydrated){if(D&&
null!==fa&&0!==(b.mode&1)&&0===(b.flags&128)){for(f=fa;f;)f=Ka(f.nextSibling);Qb();b.flags|=98560;f=!1}else if(f=pd(b),null!==d&&null!==d.dehydrated){if(null===a){if(!f)throw Error(m(318));f=b.memoizedState;f=null!==f?f.dehydrated:null;if(!f)throw Error(m(317));f[Da]=b}else Qb(),0===(b.flags&128)&&(b.memoizedState=null),b.flags|=4;W(b);f=!1}else null!==wa&&(Gf(wa),wa=null),f=!0;if(!f)return b.flags&65536?b:null}if(0!==(b.flags&128))return b.lanes=c,b;d=null!==d;d!==(null!==a&&null!==a.memoizedState)&&
d&&(b.child.flags|=8192,0!==(b.mode&1)&&(null===a||0!==(F.current&1)?0===L&&(L=3):Ef()));null!==b.updateQueue&&(b.flags|=4);W(b);return null;case 4:return Tb(),xi(a,b),null===a&&sc(b.stateNode.containerInfo),W(b),null;case 10:return cf(b.type._context),W(b),null;case 17:return ea(b.type)&&(v(S),v(J)),W(b),null;case 19:v(F);f=b.memoizedState;if(null===f)return W(b),null;d=0!==(b.flags&128);g=f.rendering;if(null===g)if(d)Dc(f,!1);else{if(0!==L||null!==a&&0!==(a.flags&128))for(a=b.child;null!==a;){g=
xd(a);if(null!==g){b.flags|=128;Dc(f,!1);d=g.updateQueue;null!==d&&(b.updateQueue=d,b.flags|=4);b.subtreeFlags=0;d=c;for(c=b.child;null!==c;)f=c,a=d,f.flags&=14680066,g=f.alternate,null===g?(f.childLanes=0,f.lanes=a,f.child=null,f.subtreeFlags=0,f.memoizedProps=null,f.memoizedState=null,f.updateQueue=null,f.dependencies=null,f.stateNode=null):(f.childLanes=g.childLanes,f.lanes=g.lanes,f.child=g.child,f.subtreeFlags=0,f.deletions=null,f.memoizedProps=g.memoizedProps,f.memoizedState=g.memoizedState,
f.updateQueue=g.updateQueue,f.type=g.type,a=g.dependencies,f.dependencies=null===a?null:{lanes:a.lanes,firstContext:a.firstContext}),c=c.sibling;y(F,F.current&1|2);return b.child}a=a.sibling}null!==f.tail&&P()>Hf&&(b.flags|=128,d=!0,Dc(f,!1),b.lanes=4194304)}else{if(!d)if(a=xd(g),null!==a){if(b.flags|=128,d=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.flags|=4),Dc(f,!0),null===f.tail&&"hidden"===f.tailMode&&!g.alternate&&!D)return W(b),null}else 2*P()-f.renderingStartTime>Hf&&1073741824!==c&&(b.flags|=
128,d=!0,Dc(f,!1),b.lanes=4194304);f.isBackwards?(g.sibling=b.child,b.child=g):(c=f.last,null!==c?c.sibling=g:b.child=g,f.last=g)}if(null!==f.tail)return b=f.tail,f.rendering=b,f.tail=b.sibling,f.renderingStartTime=P(),b.sibling=null,c=F.current,y(F,d?c&1|2:c&1),b;W(b);return null;case 22:case 23:return ba=Ga.current,v(Ga),d=null!==b.memoizedState,null!==a&&null!==a.memoizedState!==d&&(b.flags|=8192),d&&0!==(b.mode&1)?0!==(ba&1073741824)&&(W(b),b.subtreeFlags&6&&(b.flags|=8192)):W(b),null;case 24:return null;
case 25:return null}throw Error(m(156,b.tag));}function Bk(a,b,c){Ve(b);switch(b.tag){case 1:return ea(b.type)&&(v(S),v(J)),a=b.flags,a&65536?(b.flags=a&-65537|128,b):null;case 3:return Tb(),v(S),v(J),jf(),a=b.flags,0!==(a&65536)&&0===(a&128)?(b.flags=a&-65537|128,b):null;case 5:return hf(b),null;case 13:v(F);a=b.memoizedState;if(null!==a&&null!==a.dehydrated){if(null===b.alternate)throw Error(m(340));Qb()}a=b.flags;return a&65536?(b.flags=a&-65537|128,b):null;case 19:return v(F),null;case 4:return Tb(),
null;case 10:return cf(b.type._context),null;case 22:case 23:return ba=Ga.current,v(Ga),null;case 24:return null;default:return null}}function Wb(a,b){var c=a.ref;if(null!==c)if("function"===typeof c)try{c(null)}catch(d){G(a,b,d)}else c.current=null}function If(a,b,c){try{c()}catch(d){G(a,b,d)}}function Ck(a,b){Jf=Zc;a=ch();if(Ie(a)){if("selectionStart"in a)var c={start:a.selectionStart,end:a.selectionEnd};else a:{c=(c=a.ownerDocument)&&c.defaultView||window;var d=c.getSelection&&c.getSelection();
if(d&&0!==d.rangeCount){c=d.anchorNode;var e=d.anchorOffset,f=d.focusNode;d=d.focusOffset;try{c.nodeType,f.nodeType}catch(M){c=null;break a}var g=0,h=-1,k=-1,n=0,q=0,u=a,r=null;b:for(;;){for(var p;;){u!==c||0!==e&&3!==u.nodeType||(h=g+e);u!==f||0!==d&&3!==u.nodeType||(k=g+d);3===u.nodeType&&(g+=u.nodeValue.length);if(null===(p=u.firstChild))break;r=u;u=p}for(;;){if(u===a)break b;r===c&&++n===e&&(h=g);r===f&&++q===d&&(k=g);if(null!==(p=u.nextSibling))break;u=r;r=u.parentNode}u=p}c=-1===h||-1===k?null:
{start:h,end:k}}else c=null}c=c||{start:0,end:0}}else c=null;Kf={focusedElem:a,selectionRange:c};Zc=!1;for(l=b;null!==l;)if(b=l,a=b.child,0!==(b.subtreeFlags&1028)&&null!==a)a.return=b,l=a;else for(;null!==l;){b=l;try{var x=b.alternate;if(0!==(b.flags&1024))switch(b.tag){case 0:case 11:case 15:break;case 1:if(null!==x){var v=x.memoizedProps,z=x.memoizedState,w=b.stateNode,A=w.getSnapshotBeforeUpdate(b.elementType===b.type?v:ya(b.type,v),z);w.__reactInternalSnapshotBeforeUpdate=A}break;case 3:var t=
b.stateNode.containerInfo;1===t.nodeType?t.textContent="":9===t.nodeType&&t.documentElement&&t.removeChild(t.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(m(163));}}catch(M){G(b,b.return,M)}a=b.sibling;if(null!==a){a.return=b.return;l=a;break}l=b.return}x=zi;zi=!1;return x}function Gc(a,b,c){var d=b.updateQueue;d=null!==d?d.lastEffect:null;if(null!==d){var e=d=d.next;do{if((e.tag&a)===a){var f=e.destroy;e.destroy=void 0;void 0!==f&&If(b,c,f)}e=e.next}while(e!==d)}}
function Id(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.create;c.destroy=d()}c=c.next}while(c!==b)}}function Lf(a){var b=a.ref;if(null!==b){var c=a.stateNode;switch(a.tag){case 5:a=c;break;default:a=c}"function"===typeof b?b(a):b.current=a}}function Ai(a){var b=a.alternate;null!==b&&(a.alternate=null,Ai(b));a.child=null;a.deletions=null;a.sibling=null;5===a.tag&&(b=a.stateNode,null!==b&&(delete b[Da],delete b[uc],delete b[Me],delete b[Dk],
delete b[Ek]));a.stateNode=null;a.return=null;a.dependencies=null;a.memoizedProps=null;a.memoizedState=null;a.pendingProps=null;a.stateNode=null;a.updateQueue=null}function Bi(a){return 5===a.tag||3===a.tag||4===a.tag}function Ci(a){a:for(;;){for(;null===a.sibling;){if(null===a.return||Bi(a.return))return null;a=a.return}a.sibling.return=a.return;for(a=a.sibling;5!==a.tag&&6!==a.tag&&18!==a.tag;){if(a.flags&2)continue a;if(null===a.child||4===a.tag)continue a;else a.child.return=a,a=a.child}if(!(a.flags&
2))return a.stateNode}}function Mf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=kd));else if(4!==d&&(a=a.child,null!==a))for(Mf(a,b,c),a=a.sibling;null!==a;)Mf(a,b,c),a=a.sibling}function Nf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?c.insertBefore(a,b):c.appendChild(a);
else if(4!==d&&(a=a.child,null!==a))for(Nf(a,b,c),a=a.sibling;null!==a;)Nf(a,b,c),a=a.sibling}function jb(a,b,c){for(c=c.child;null!==c;)Di(a,b,c),c=c.sibling}function Di(a,b,c){if(Ca&&"function"===typeof Ca.onCommitFiberUnmount)try{Ca.onCommitFiberUnmount(Uc,c)}catch(h){}switch(c.tag){case 5:X||Wb(c,b);case 6:var d=T,e=za;T=null;jb(a,b,c);T=d;za=e;null!==T&&(za?(a=T,c=c.stateNode,8===a.nodeType?a.parentNode.removeChild(c):a.removeChild(c)):T.removeChild(c.stateNode));break;case 18:null!==T&&(za?
(a=T,c=c.stateNode,8===a.nodeType?Re(a.parentNode,c):1===a.nodeType&&Re(a,c),nc(a)):Re(T,c.stateNode));break;case 4:d=T;e=za;T=c.stateNode.containerInfo;za=!0;jb(a,b,c);T=d;za=e;break;case 0:case 11:case 14:case 15:if(!X&&(d=c.updateQueue,null!==d&&(d=d.lastEffect,null!==d))){e=d=d.next;do{var f=e,g=f.destroy;f=f.tag;void 0!==g&&(0!==(f&2)?If(c,b,g):0!==(f&4)&&If(c,b,g));e=e.next}while(e!==d)}jb(a,b,c);break;case 1:if(!X&&(Wb(c,b),d=c.stateNode,"function"===typeof d.componentWillUnmount))try{d.props=
c.memoizedProps,d.state=c.memoizedState,d.componentWillUnmount()}catch(h){G(c,b,h)}jb(a,b,c);break;case 21:jb(a,b,c);break;case 22:c.mode&1?(X=(d=X)||null!==c.memoizedState,jb(a,b,c),X=d):jb(a,b,c);break;default:jb(a,b,c)}}function Ei(a){var b=a.updateQueue;if(null!==b){a.updateQueue=null;var c=a.stateNode;null===c&&(c=a.stateNode=new Fk);b.forEach(function(b){var d=Gk.bind(null,a,b);c.has(b)||(c.add(b),b.then(d,d))})}}function Aa(a,b,c){c=b.deletions;if(null!==c)for(var d=0;d<c.length;d++){var e=
c[d];try{var f=a,g=b,h=g;a:for(;null!==h;){switch(h.tag){case 5:T=h.stateNode;za=!1;break a;case 3:T=h.stateNode.containerInfo;za=!0;break a;case 4:T=h.stateNode.containerInfo;za=!0;break a}h=h.return}if(null===T)throw Error(m(160));Di(f,g,e);T=null;za=!1;var k=e.alternate;null!==k&&(k.return=null);e.return=null}catch(n){G(e,b,n)}}if(b.subtreeFlags&12854)for(b=b.child;null!==b;)Fi(b,a),b=b.sibling}function Fi(a,b,c){var d=a.alternate;c=a.flags;switch(a.tag){case 0:case 11:case 14:case 15:Aa(b,a);
Ha(a);if(c&4){try{Gc(3,a,a.return),Id(3,a)}catch(I){G(a,a.return,I)}try{Gc(5,a,a.return)}catch(I){G(a,a.return,I)}}break;case 1:Aa(b,a);Ha(a);c&512&&null!==d&&Wb(d,d.return);break;case 5:Aa(b,a);Ha(a);c&512&&null!==d&&Wb(d,d.return);if(a.flags&32){var e=a.stateNode;try{Fc(e,"")}catch(I){G(a,a.return,I)}}if(c&4&&(e=a.stateNode,null!=e)){var f=a.memoizedProps,g=null!==d?d.memoizedProps:f,h=a.type,k=a.updateQueue;a.updateQueue=null;if(null!==k)try{"input"===h&&"radio"===f.type&&null!=f.name&&lg(e,f);
qe(h,g);var n=qe(h,f);for(g=0;g<k.length;g+=2){var q=k[g],u=k[g+1];"style"===q?sg(e,u):"dangerouslySetInnerHTML"===q?yi(e,u):"children"===q?Fc(e,u):$d(e,q,u,n)}switch(h){case "input":le(e,f);break;case "textarea":og(e,f);break;case "select":var r=e._wrapperState.wasMultiple;e._wrapperState.wasMultiple=!!f.multiple;var p=f.value;null!=p?Db(e,!!f.multiple,p,!1):r!==!!f.multiple&&(null!=f.defaultValue?Db(e,!!f.multiple,f.defaultValue,!0):Db(e,!!f.multiple,f.multiple?[]:"",!1))}e[uc]=f}catch(I){G(a,a.return,
I)}}break;case 6:Aa(b,a);Ha(a);if(c&4){if(null===a.stateNode)throw Error(m(162));e=a.stateNode;f=a.memoizedProps;try{e.nodeValue=f}catch(I){G(a,a.return,I)}}break;case 3:Aa(b,a);Ha(a);if(c&4&&null!==d&&d.memoizedState.isDehydrated)try{nc(b.containerInfo)}catch(I){G(a,a.return,I)}break;case 4:Aa(b,a);Ha(a);break;case 13:Aa(b,a);Ha(a);e=a.child;e.flags&8192&&(f=null!==e.memoizedState,e.stateNode.isHidden=f,!f||null!==e.alternate&&null!==e.alternate.memoizedState||(Of=P()));c&4&&Ei(a);break;case 22:q=
null!==d&&null!==d.memoizedState;a.mode&1?(X=(n=X)||q,Aa(b,a),X=n):Aa(b,a);Ha(a);if(c&8192){n=null!==a.memoizedState;if((a.stateNode.isHidden=n)&&!q&&0!==(a.mode&1))for(l=a,q=a.child;null!==q;){for(u=l=q;null!==l;){r=l;p=r.child;switch(r.tag){case 0:case 11:case 14:case 15:Gc(4,r,r.return);break;case 1:Wb(r,r.return);var x=r.stateNode;if("function"===typeof x.componentWillUnmount){c=r;b=r.return;try{d=c,x.props=d.memoizedProps,x.state=d.memoizedState,x.componentWillUnmount()}catch(I){G(c,b,I)}}break;
case 5:Wb(r,r.return);break;case 22:if(null!==r.memoizedState){Gi(u);continue}}null!==p?(p.return=r,l=p):Gi(u)}q=q.sibling}a:for(q=null,u=a;;){if(5===u.tag){if(null===q){q=u;try{e=u.stateNode,n?(f=e.style,"function"===typeof f.setProperty?f.setProperty("display","none","important"):f.display="none"):(h=u.stateNode,k=u.memoizedProps.style,g=void 0!==k&&null!==k&&k.hasOwnProperty("display")?k.display:null,h.style.display=rg("display",g))}catch(I){G(a,a.return,I)}}}else if(6===u.tag){if(null===q)try{u.stateNode.nodeValue=
n?"":u.memoizedProps}catch(I){G(a,a.return,I)}}else if((22!==u.tag&&23!==u.tag||null===u.memoizedState||u===a)&&null!==u.child){u.child.return=u;u=u.child;continue}if(u===a)break a;for(;null===u.sibling;){if(null===u.return||u.return===a)break a;q===u&&(q=null);u=u.return}q===u&&(q=null);u.sibling.return=u.return;u=u.sibling}}break;case 19:Aa(b,a);Ha(a);c&4&&Ei(a);break;case 21:break;default:Aa(b,a),Ha(a)}}function Ha(a){var b=a.flags;if(b&2){try{a:{for(var c=a.return;null!==c;){if(Bi(c)){var d=c;
break a}c=c.return}throw Error(m(160));}switch(d.tag){case 5:var e=d.stateNode;d.flags&32&&(Fc(e,""),d.flags&=-33);var f=Ci(a);Nf(a,f,e);break;case 3:case 4:var g=d.stateNode.containerInfo,h=Ci(a);Mf(a,h,g);break;default:throw Error(m(161));}}catch(k){G(a,a.return,k)}a.flags&=-3}b&4096&&(a.flags&=-4097)}function Hk(a,b,c){l=a;Hi(a,b,c)}function Hi(a,b,c){for(var d=0!==(a.mode&1);null!==l;){var e=l,f=e.child;if(22===e.tag&&d){var g=null!==e.memoizedState||Jd;if(!g){var h=e.alternate,k=null!==h&&null!==
h.memoizedState||X;h=Jd;var n=X;Jd=g;if((X=k)&&!n)for(l=e;null!==l;)g=l,k=g.child,22===g.tag&&null!==g.memoizedState?Ii(e):null!==k?(k.return=g,l=k):Ii(e);for(;null!==f;)l=f,Hi(f,b,c),f=f.sibling;l=e;Jd=h;X=n}Ji(a,b,c)}else 0!==(e.subtreeFlags&8772)&&null!==f?(f.return=e,l=f):Ji(a,b,c)}}function Ji(a,b,c){for(;null!==l;){b=l;if(0!==(b.flags&8772)){c=b.alternate;try{if(0!==(b.flags&8772))switch(b.tag){case 0:case 11:case 15:X||Id(5,b);break;case 1:var d=b.stateNode;if(b.flags&4&&!X)if(null===c)d.componentDidMount();
else{var e=b.elementType===b.type?c.memoizedProps:ya(b.type,c.memoizedProps);d.componentDidUpdate(e,c.memoizedState,d.__reactInternalSnapshotBeforeUpdate)}var f=b.updateQueue;null!==f&&Hh(b,f,d);break;case 3:var g=b.updateQueue;if(null!==g){c=null;if(null!==b.child)switch(b.child.tag){case 5:c=b.child.stateNode;break;case 1:c=b.child.stateNode}Hh(b,g,c)}break;case 5:var h=b.stateNode;if(null===c&&b.flags&4){c=h;var k=b.memoizedProps;switch(b.type){case "button":case "input":case "select":case "textarea":k.autoFocus&&
c.focus();break;case "img":k.src&&(c.src=k.src)}}break;case 6:break;case 4:break;case 12:break;case 13:if(null===b.memoizedState){var n=b.alternate;if(null!==n){var q=n.memoizedState;if(null!==q){var p=q.dehydrated;null!==p&&nc(p)}}}break;case 19:case 17:case 21:case 22:case 23:case 25:break;default:throw Error(m(163));}X||b.flags&512&&Lf(b)}catch(r){G(b,b.return,r)}}if(b===a){l=null;break}c=b.sibling;if(null!==c){c.return=b.return;l=c;break}l=b.return}}function Gi(a){for(;null!==l;){var b=l;if(b===
a){l=null;break}var c=b.sibling;if(null!==c){c.return=b.return;l=c;break}l=b.return}}function Ii(a){for(;null!==l;){var b=l;try{switch(b.tag){case 0:case 11:case 15:var c=b.return;try{Id(4,b)}catch(k){G(b,c,k)}break;case 1:var d=b.stateNode;if("function"===typeof d.componentDidMount){var e=b.return;try{d.componentDidMount()}catch(k){G(b,e,k)}}var f=b.return;try{Lf(b)}catch(k){G(b,f,k)}break;case 5:var g=b.return;try{Lf(b)}catch(k){G(b,g,k)}}}catch(k){G(b,b.return,k)}if(b===a){l=null;break}var h=b.sibling;
if(null!==h){h.return=b.return;l=h;break}l=b.return}}function Hc(){Hf=P()+500}function Z(){return 0!==(p&6)?P():-1!==Kd?Kd:Kd=P()}function hb(a){if(0===(a.mode&1))return 1;if(0!==(p&2)&&0!==U)return U&-U;if(null!==Ik.transition)return 0===Ld&&(Ld=Dg()),Ld;a=z;if(0!==a)return a;a=window.event;a=void 0===a?16:Lg(a.type);return a}function xa(a,b,c,d){if(50<Ic)throw Ic=0,Pf=null,Error(m(185));ic(a,c,d);if(0===(p&2)||a!==O)a===O&&(0===(p&2)&&(Md|=c),4===L&&kb(a,U)),ia(a,d),1===c&&0===p&&0===(b.mode&1)&&
(Hc(),md&&db())}function ia(a,b){var c=a.callbackNode;tj(a,b);var d=Vc(a,a===O?U:0);if(0===d)null!==c&&Ki(c),a.callbackNode=null,a.callbackPriority=0;else if(b=d&-d,a.callbackPriority!==b){null!=c&&Ki(c);if(1===b)0===a.tag?jk(Li.bind(null,a)):wh(Li.bind(null,a)),Jk(function(){0===(p&6)&&db()}),c=null;else{switch(Eg(d)){case 1:c=De;break;case 4:c=Mg;break;case 16:c=ad;break;case 536870912:c=Ng;break;default:c=ad}c=Mi(c,Ni.bind(null,a))}a.callbackPriority=b;a.callbackNode=c}}function Ni(a,b){Kd=-1;
Ld=0;if(0!==(p&6))throw Error(m(327));var c=a.callbackNode;if(Xb()&&a.callbackNode!==c)return null;var d=Vc(a,a===O?U:0);if(0===d)return null;if(0!==(d&30)||0!==(d&a.expiredLanes)||b)b=Nd(a,d);else{b=d;var e=p;p|=2;var f=Oi();if(O!==a||U!==b)Ra=null,Hc(),wb(a,b);do try{Kk();break}catch(h){Pi(a,h)}while(1);af();Od.current=f;p=e;null!==H?b=0:(O=null,U=0,b=L)}if(0!==b){2===b&&(e=ve(a),0!==e&&(d=e,b=Qf(a,e)));if(1===b)throw c=Jc,wb(a,0),kb(a,d),ia(a,P()),c;if(6===b)kb(a,d);else{e=a.current.alternate;
if(0===(d&30)&&!Lk(e)&&(b=Nd(a,d),2===b&&(f=ve(a),0!==f&&(d=f,b=Qf(a,f))),1===b))throw c=Jc,wb(a,0),kb(a,d),ia(a,P()),c;a.finishedWork=e;a.finishedLanes=d;switch(b){case 0:case 1:throw Error(m(345));case 2:xb(a,ja,Ra);break;case 3:kb(a,d);if((d&130023424)===d&&(b=Of+500-P(),10<b)){if(0!==Vc(a,0))break;e=a.suspendedLanes;if((e&d)!==d){Z();a.pingedLanes|=a.suspendedLanes&e;break}a.timeoutHandle=Rf(xb.bind(null,a,ja,Ra),b);break}xb(a,ja,Ra);break;case 4:kb(a,d);if((d&4194240)===d)break;b=a.eventTimes;
for(e=-1;0<d;){var g=31-ta(d);f=1<<g;g=b[g];g>e&&(e=g);d&=~f}d=e;d=P()-d;d=(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*Mk(d/1960))-d;if(10<d){a.timeoutHandle=Rf(xb.bind(null,a,ja,Ra),d);break}xb(a,ja,Ra);break;case 5:xb(a,ja,Ra);break;default:throw Error(m(329));}}}ia(a,P());return a.callbackNode===c?Ni.bind(null,a):null}function Qf(a,b){var c=Kc;a.current.memoizedState.isDehydrated&&(wb(a,b).flags|=256);a=Nd(a,b);2!==a&&(b=ja,ja=c,null!==b&&Gf(b));return a}function Gf(a){null===
ja?ja=a:ja.push.apply(ja,a)}function Lk(a){for(var b=a;;){if(b.flags&16384){var c=b.updateQueue;if(null!==c&&(c=c.stores,null!==c))for(var d=0;d<c.length;d++){var e=c[d],f=e.getSnapshot;e=e.value;try{if(!ua(f(),e))return!1}catch(g){return!1}}}c=b.child;if(b.subtreeFlags&16384&&null!==c)c.return=b,b=c;else{if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return!0;b=b.return}b.sibling.return=b.return;b=b.sibling}}return!0}function kb(a,b){b&=~Sf;b&=~Md;a.suspendedLanes|=b;a.pingedLanes&=
~b;for(a=a.expirationTimes;0<b;){var c=31-ta(b),d=1<<c;a[c]=-1;b&=~d}}function Li(a){if(0!==(p&6))throw Error(m(327));Xb();var b=Vc(a,0);if(0===(b&1))return ia(a,P()),null;var c=Nd(a,b);if(0!==a.tag&&2===c){var d=ve(a);0!==d&&(b=d,c=Qf(a,d))}if(1===c)throw c=Jc,wb(a,0),kb(a,b),ia(a,P()),c;if(6===c)throw Error(m(345));a.finishedWork=a.current.alternate;a.finishedLanes=b;xb(a,ja,Ra);ia(a,P());return null}function Tf(a,b){var c=p;p|=1;try{return a(b)}finally{p=c,0===p&&(Hc(),md&&db())}}function yb(a){null!==
lb&&0===lb.tag&&0===(p&6)&&Xb();var b=p;p|=1;var c=ca.transition,d=z;try{if(ca.transition=null,z=1,a)return a()}finally{z=d,ca.transition=c,p=b,0===(p&6)&&db()}}function wb(a,b){a.finishedWork=null;a.finishedLanes=0;var c=a.timeoutHandle;-1!==c&&(a.timeoutHandle=-1,Nk(c));if(null!==H)for(c=H.return;null!==c;){var d=c;Ve(d);switch(d.tag){case 1:d=d.type.childContextTypes;null!==d&&void 0!==d&&(v(S),v(J));break;case 3:Tb();v(S);v(J);jf();break;case 5:hf(d);break;case 4:Tb();break;case 13:v(F);break;
case 19:v(F);break;case 10:cf(d.type._context);break;case 22:case 23:ba=Ga.current,v(Ga)}c=c.return}O=a;H=a=eb(a.current,null);U=ba=b;L=0;Jc=null;Sf=Md=ra=0;ja=Kc=null;if(null!==tb){for(b=0;b<tb.length;b++)if(c=tb[b],d=c.interleaved,null!==d){c.interleaved=null;var e=d.next,f=c.pending;if(null!==f){var g=f.next;f.next=e;d.next=g}c.pending=d}tb=null}return a}function Pi(a,b){do{var c=H;try{af();yd.current=zd;if(Ad){for(var d=C.memoizedState;null!==d;){var e=d.queue;null!==e&&(e.pending=null);d=d.next}Ad=
!1}vb=0;N=K=C=null;zc=!1;Ac=0;Uf.current=null;if(null===c||null===c.return){L=1;Jc=b;H=null;break}a:{var f=a,g=c.return,h=c,k=b;b=U;h.flags|=32768;if(null!==k&&"object"===typeof k&&"function"===typeof k.then){var n=k,l=h,p=l.tag;if(0===(l.mode&1)&&(0===p||11===p||15===p)){var r=l.alternate;r?(l.updateQueue=r.updateQueue,l.memoizedState=r.memoizedState,l.lanes=r.lanes):(l.updateQueue=null,l.memoizedState=null)}var v=ji(g);if(null!==v){v.flags&=-257;ki(v,g,h,f,b);v.mode&1&&ii(f,n,b);b=v;k=n;var x=b.updateQueue;
if(null===x){var z=new Set;z.add(k);b.updateQueue=z}else x.add(k);break a}else{if(0===(b&1)){ii(f,n,b);Ef();break a}k=Error(m(426))}}else if(D&&h.mode&1){var y=ji(g);if(null!==y){0===(y.flags&65536)&&(y.flags|=256);ki(y,g,h,f,b);Ye(Ub(k,h));break a}}f=k=Ub(k,h);4!==L&&(L=2);null===Kc?Kc=[f]:Kc.push(f);f=g;do{switch(f.tag){case 3:f.flags|=65536;b&=-b;f.lanes|=b;var w=gi(f,k,b);Gh(f,w);break a;case 1:h=k;var A=f.type,t=f.stateNode;if(0===(f.flags&128)&&("function"===typeof A.getDerivedStateFromError||
null!==t&&"function"===typeof t.componentDidCatch&&(null===ib||!ib.has(t)))){f.flags|=65536;b&=-b;f.lanes|=b;var B=hi(f,h,b);Gh(f,B);break a}}f=f.return}while(null!==f)}Qi(c)}catch(ma){b=ma;H===c&&null!==c&&(H=c=c.return);continue}break}while(1)}function Oi(){var a=Od.current;Od.current=zd;return null===a?zd:a}function Ef(){if(0===L||3===L||2===L)L=4;null===O||0===(ra&268435455)&&0===(Md&268435455)||kb(O,U)}function Nd(a,b){var c=p;p|=2;var d=Oi();if(O!==a||U!==b)Ra=null,wb(a,b);do try{Ok();break}catch(e){Pi(a,
e)}while(1);af();p=c;Od.current=d;if(null!==H)throw Error(m(261));O=null;U=0;return L}function Ok(){for(;null!==H;)Ri(H)}function Kk(){for(;null!==H&&!Pk();)Ri(H)}function Ri(a){var b=Qk(a.alternate,a,ba);a.memoizedProps=a.pendingProps;null===b?Qi(a):H=b;Uf.current=null}function Qi(a){var b=a;do{var c=b.alternate;a=b.return;if(0===(b.flags&32768)){if(c=xk(c,b,ba),null!==c){H=c;return}}else{c=Bk(c,b);if(null!==c){c.flags&=32767;H=c;return}if(null!==a)a.flags|=32768,a.subtreeFlags=0,a.deletions=null;
else{L=6;H=null;return}}b=b.sibling;if(null!==b){H=b;return}H=b=a}while(null!==b);0===L&&(L=5)}function xb(a,b,c){var d=z,e=ca.transition;try{ca.transition=null,z=1,Rk(a,b,c,d)}finally{ca.transition=e,z=d}return null}function Rk(a,b,c,d){do Xb();while(null!==lb);if(0!==(p&6))throw Error(m(327));c=a.finishedWork;var e=a.finishedLanes;if(null===c)return null;a.finishedWork=null;a.finishedLanes=0;if(c===a.current)throw Error(m(177));a.callbackNode=null;a.callbackPriority=0;var f=c.lanes|c.childLanes;
uj(a,f);a===O&&(H=O=null,U=0);0===(c.subtreeFlags&2064)&&0===(c.flags&2064)||Pd||(Pd=!0,Mi(ad,function(){Xb();return null}));f=0!==(c.flags&15990);if(0!==(c.subtreeFlags&15990)||f){f=ca.transition;ca.transition=null;var g=z;z=1;var h=p;p|=4;Uf.current=null;Ck(a,c);Fi(c,a);Tj(Kf);Zc=!!Jf;Kf=Jf=null;a.current=c;Hk(c,a,e);Sk();p=h;z=g;ca.transition=f}else a.current=c;Pd&&(Pd=!1,lb=a,Qd=e);f=a.pendingLanes;0===f&&(ib=null);oj(c.stateNode,d);ia(a,P());if(null!==b)for(d=a.onRecoverableError,c=0;c<b.length;c++)e=
b[c],d(e.value,{componentStack:e.stack,digest:e.digest});if(Ed)throw Ed=!1,a=xf,xf=null,a;0!==(Qd&1)&&0!==a.tag&&Xb();f=a.pendingLanes;0!==(f&1)?a===Pf?Ic++:(Ic=0,Pf=a):Ic=0;db();return null}function Xb(){if(null!==lb){var a=Eg(Qd),b=ca.transition,c=z;try{ca.transition=null;z=16>a?16:a;if(null===lb)var d=!1;else{a=lb;lb=null;Qd=0;if(0!==(p&6))throw Error(m(331));var e=p;p|=4;for(l=a.current;null!==l;){var f=l,g=f.child;if(0!==(l.flags&16)){var h=f.deletions;if(null!==h){for(var k=0;k<h.length;k++){var n=
h[k];for(l=n;null!==l;){var q=l;switch(q.tag){case 0:case 11:case 15:Gc(8,q,f)}var u=q.child;if(null!==u)u.return=q,l=u;else for(;null!==l;){q=l;var r=q.sibling,v=q.return;Ai(q);if(q===n){l=null;break}if(null!==r){r.return=v;l=r;break}l=v}}}var x=f.alternate;if(null!==x){var y=x.child;if(null!==y){x.child=null;do{var C=y.sibling;y.sibling=null;y=C}while(null!==y)}}l=f}}if(0!==(f.subtreeFlags&2064)&&null!==g)g.return=f,l=g;else b:for(;null!==l;){f=l;if(0!==(f.flags&2048))switch(f.tag){case 0:case 11:case 15:Gc(9,
f,f.return)}var w=f.sibling;if(null!==w){w.return=f.return;l=w;break b}l=f.return}}var A=a.current;for(l=A;null!==l;){g=l;var t=g.child;if(0!==(g.subtreeFlags&2064)&&null!==t)t.return=g,l=t;else b:for(g=A;null!==l;){h=l;if(0!==(h.flags&2048))try{switch(h.tag){case 0:case 11:case 15:Id(9,h)}}catch(ma){G(h,h.return,ma)}if(h===g){l=null;break b}var B=h.sibling;if(null!==B){B.return=h.return;l=B;break b}l=h.return}}p=e;db();if(Ca&&"function"===typeof Ca.onPostCommitFiberRoot)try{Ca.onPostCommitFiberRoot(Uc,
a)}catch(ma){}d=!0}return d}finally{z=c,ca.transition=b}}return!1}function Si(a,b,c){b=Ub(c,b);b=gi(a,b,1);a=fb(a,b,1);b=Z();null!==a&&(ic(a,1,b),ia(a,b))}function G(a,b,c){if(3===a.tag)Si(a,a,c);else for(;null!==b;){if(3===b.tag){Si(b,a,c);break}else if(1===b.tag){var d=b.stateNode;if("function"===typeof b.type.getDerivedStateFromError||"function"===typeof d.componentDidCatch&&(null===ib||!ib.has(d))){a=Ub(c,a);a=hi(b,a,1);b=fb(b,a,1);a=Z();null!==b&&(ic(b,1,a),ia(b,a));break}}b=b.return}}function sk(a,
b,c){var d=a.pingCache;null!==d&&d.delete(b);b=Z();a.pingedLanes|=a.suspendedLanes&c;O===a&&(U&c)===c&&(4===L||3===L&&(U&130023424)===U&&500>P()-Of?wb(a,0):Sf|=c);ia(a,b)}function Ti(a,b){0===b&&(0===(a.mode&1)?b=1:(b=Rd,Rd<<=1,0===(Rd&130023424)&&(Rd=4194304)));var c=Z();a=Oa(a,b);null!==a&&(ic(a,b,c),ia(a,c))}function vk(a){var b=a.memoizedState,c=0;null!==b&&(c=b.retryLane);Ti(a,c)}function Gk(a,b){var c=0;switch(a.tag){case 13:var d=a.stateNode;var e=a.memoizedState;null!==e&&(c=e.retryLane);
break;case 19:d=a.stateNode;break;default:throw Error(m(314));}null!==d&&d.delete(b);Ti(a,c)}function Mi(a,b){return xh(a,b)}function Tk(a,b,c,d){this.tag=a;this.key=c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.subtreeFlags=this.flags=0;this.deletions=null;this.childLanes=this.lanes=0;this.alternate=null}function yf(a){a=
a.prototype;return!(!a||!a.isReactComponent)}function Uk(a){if("function"===typeof a)return yf(a)?1:0;if(void 0!==a&&null!==a){a=a.$$typeof;if(a===ie)return 11;if(a===je)return 14}return 2}function eb(a,b){var c=a.alternate;null===c?(c=pa(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.type=a.type,c.flags=0,c.subtreeFlags=0,c.deletions=null);c.flags=a.flags&14680064;c.childLanes=a.childLanes;c.lanes=a.lanes;c.child=
a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;b=a.dependencies;c.dependencies=null===b?null:{lanes:b.lanes,firstContext:b.firstContext};c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function rd(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)yf(a)&&(g=1);else if("string"===typeof a)g=5;else a:switch(a){case Bb:return sb(c.children,e,f,b);case fe:g=8;e|=8;break;case ee:return a=pa(12,c,b,e|2),a.elementType=ee,a.lanes=f,a;case ge:return a=
pa(13,c,b,e),a.elementType=ge,a.lanes=f,a;case he:return a=pa(19,c,b,e),a.elementType=he,a.lanes=f,a;case Ui:return Gd(c,e,f,b);default:if("object"===typeof a&&null!==a)switch(a.$$typeof){case hg:g=10;break a;case gg:g=9;break a;case ie:g=11;break a;case je:g=14;break a;case Ta:g=16;d=null;break a}throw Error(m(130,null==a?a:typeof a,""));}b=pa(g,c,b,e);b.elementType=a;b.type=d;b.lanes=f;return b}function sb(a,b,c,d){a=pa(7,a,d,b);a.lanes=c;return a}function Gd(a,b,c,d){a=pa(22,a,d,b);a.elementType=
Ui;a.lanes=c;a.stateNode={isHidden:!1};return a}function Ze(a,b,c){a=pa(6,a,null,b);a.lanes=c;return a}function $e(a,b,c){b=pa(4,null!==a.children?a.children:[],a.key,b);b.lanes=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Vk(a,b,c,d,e){this.tag=b;this.containerInfo=a;this.finishedWork=this.pingCache=this.current=this.pendingChildren=null;this.timeoutHandle=-1;this.callbackNode=this.pendingContext=this.context=null;this.callbackPriority=
0;this.eventTimes=we(0);this.expirationTimes=we(-1);this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0;this.entanglements=we(0);this.identifierPrefix=d;this.onRecoverableError=e;this.mutableSourceEagerHydrationData=null}function Vf(a,b,c,d,e,f,g,h,k,l){a=new Vk(a,b,c,h,k);1===b?(b=1,!0===f&&(b|=8)):b=0;f=pa(3,null,null,b);a.current=f;f.stateNode=a;f.memoizedState={element:d,isDehydrated:c,cache:null,transitions:null,
pendingSuspenseBoundaries:null};ff(f);return a}function Wk(a,b,c){var d=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:Cb,key:null==d?null:""+d,children:a,containerInfo:b,implementation:c}}function Vi(a){if(!a)return cb;a=a._reactInternals;a:{if(nb(a)!==a||1!==a.tag)throw Error(m(170));var b=a;do{switch(b.tag){case 3:b=b.stateNode.context;break a;case 1:if(ea(b.type)){b=b.stateNode.__reactInternalMemoizedMergedChildContext;break a}}b=b.return}while(null!==b);throw Error(m(171));
}if(1===a.tag){var c=a.type;if(ea(c))return uh(a,c,b)}return b}function Wi(a,b,c,d,e,f,g,h,k,l){a=Vf(c,d,!0,a,e,f,g,h,k);a.context=Vi(null);c=a.current;d=Z();e=hb(c);f=Pa(d,e);f.callback=void 0!==b&&null!==b?b:null;fb(c,f,e);a.current.lanes=e;ic(a,e,d);ia(a,d);return a}function Sd(a,b,c,d){var e=b.current,f=Z(),g=hb(e);c=Vi(c);null===b.context?b.context=c:b.pendingContext=c;b=Pa(f,g);b.payload={element:a};d=void 0===d?null:d;null!==d&&(b.callback=d);a=fb(e,b,g);null!==a&&(xa(a,e,g,f),vd(a,e,g));return g}
function Td(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Xi(a,b){a=a.memoizedState;if(null!==a&&null!==a.dehydrated){var c=a.retryLane;a.retryLane=0!==c&&c<b?c:b}}function Wf(a,b){Xi(a,b);(a=a.alternate)&&Xi(a,b)}function Xk(a){a=Bg(a);return null===a?null:a.stateNode}function Yk(a){return null}function Xf(a){this._internalRoot=a}function Ud(a){this._internalRoot=a}function Yf(a){return!(!a||1!==a.nodeType&&9!==
a.nodeType&&11!==a.nodeType)}function Vd(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||" react-mount-point-unstable "!==a.nodeValue))}function Yi(){}function Zk(a,b,c,d,e){if(e){if("function"===typeof d){var f=d;d=function(){var a=Td(g);f.call(a)}}var g=Wi(b,d,a,0,null,!1,!1,"",Yi);a._reactRootContainer=g;a[Ja]=g.current;sc(8===a.nodeType?a.parentNode:a);yb();return g}for(;e=a.lastChild;)a.removeChild(e);if("function"===typeof d){var h=d;d=function(){var a=Td(k);
h.call(a)}}var k=Vf(a,0,!1,null,null,!1,!1,"",Yi);a._reactRootContainer=k;a[Ja]=k.current;sc(8===a.nodeType?a.parentNode:a);yb(function(){Sd(b,k,c,d)});return k}function Wd(a,b,c,d,e){var f=c._reactRootContainer;if(f){var g=f;if("function"===typeof e){var h=e;e=function(){var a=Td(g);h.call(a)}}Sd(b,g,a,e)}else g=Zk(c,b,a,e,d);return Td(g)}var cg=new Set,$b={},Ia=!("undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement),Zd=Object.prototype.hasOwnProperty,
cj=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,eg={},dg={},R={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){R[a]=
new Y(a,0,!1,a,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];R[b]=new Y(b,1,!1,a[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(a){R[a]=new Y(a,2,!1,a.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){R[a]=new Y(a,2,!1,a,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){R[a]=
new Y(a,3,!1,a.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(a){R[a]=new Y(a,3,!0,a,null,!1,!1)});["capture","download"].forEach(function(a){R[a]=new Y(a,4,!1,a,null,!1,!1)});["cols","rows","size","span"].forEach(function(a){R[a]=new Y(a,6,!1,a,null,!1,!1)});["rowSpan","start"].forEach(function(a){R[a]=new Y(a,5,!1,a.toLowerCase(),null,!1,!1)});var Zf=/[\-:]([a-z])/g,$f=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=
a.replace(Zf,$f);R[b]=new Y(b,1,!1,a,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(Zf,$f);R[b]=new Y(b,1,!1,a,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(Zf,$f);R[b]=new Y(b,1,!1,a,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(a){R[a]=new Y(a,1,!1,a.toLowerCase(),null,!1,!1)});R.xlinkHref=new Y("xlinkHref",
1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(a){R[a]=new Y(a,1,!1,a.toLowerCase(),null,!0,!0)});var Sa=zb.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,sd=Symbol.for("react.element"),Cb=Symbol.for("react.portal"),Bb=Symbol.for("react.fragment"),fe=Symbol.for("react.strict_mode"),ee=Symbol.for("react.profiler"),hg=Symbol.for("react.provider"),gg=Symbol.for("react.context"),ie=Symbol.for("react.forward_ref"),ge=Symbol.for("react.suspense"),
he=Symbol.for("react.suspense_list"),je=Symbol.for("react.memo"),Ta=Symbol.for("react.lazy");Symbol.for("react.scope");Symbol.for("react.debug_trace_mode");var Ui=Symbol.for("react.offscreen");Symbol.for("react.legacy_hidden");Symbol.for("react.cache");Symbol.for("react.tracing_marker");var fg=Symbol.iterator,E=Object.assign,ae,ce=!1,cc=Array.isArray,Xd,yi=function(a){return"undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,
c,d,e)})}:a}(function(a,b){if("http://www.w3.org/2000/svg"!==a.namespaceURI||"innerHTML"in a)a.innerHTML=b;else{Xd=Xd||document.createElement("div");Xd.innerHTML="<svg>"+b.valueOf().toString()+"</svg>";for(b=Xd.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),Fc=function(a,b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},dc={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,
borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,
strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},$k=["Webkit","ms","Moz","O"];Object.keys(dc).forEach(function(a){$k.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);dc[b]=dc[a]})});var ij=E({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),ze=null,se=null,Eb=null,Fb=null,xg=function(a,b){return a(b)},yg=function(){},te=!1,Oe=!1;if(Ia)try{var Lc={};Object.defineProperty(Lc,
"passive",{get:function(){Oe=!0}});window.addEventListener("test",Lc,Lc);window.removeEventListener("test",Lc,Lc)}catch(a){Oe=!1}var kj=function(a,b,c,d,e,f,g,h,k){var l=Array.prototype.slice.call(arguments,3);try{b.apply(c,l)}catch(q){this.onError(q)}},gc=!1,Sc=null,Tc=!1,ue=null,lj={onError:function(a){gc=!0;Sc=a}},Ba=zb.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Jg=Ba.unstable_scheduleCallback,Kg=Ba.unstable_NormalPriority,xh=Jg,Ki=Ba.unstable_cancelCallback,Pk=Ba.unstable_shouldYield,
Sk=Ba.unstable_requestPaint,P=Ba.unstable_now,Dj=Ba.unstable_getCurrentPriorityLevel,De=Ba.unstable_ImmediatePriority,Mg=Ba.unstable_UserBlockingPriority,ad=Kg,Ej=Ba.unstable_LowPriority,Ng=Ba.unstable_IdlePriority,Uc=null,Ca=null,ta=Math.clz32?Math.clz32:pj,qj=Math.log,rj=Math.LN2,Wc=64,Rd=4194304,z=0,Ae=!1,Yc=[],Va=null,Wa=null,Xa=null,jc=new Map,kc=new Map,Ya=[],Bj="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset submit".split(" "),
Gb=Sa.ReactCurrentBatchConfig,Zc=!0,$c=null,Za=null,Ee=null,bd=null,Yb={eventPhase:0,bubbles:0,cancelable:0,timeStamp:function(a){return a.timeStamp||Date.now()},defaultPrevented:0,isTrusted:0},He=ka(Yb),Mc=E({},Yb,{view:0,detail:0}),ak=ka(Mc),ag,bg,Nc,Yd=E({},Mc,{screenX:0,screenY:0,clientX:0,clientY:0,pageX:0,pageY:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,getModifierState:Fe,button:0,buttons:0,relatedTarget:function(a){return void 0===a.relatedTarget?a.fromElement===a.srcElement?a.toElement:a.fromElement:
a.relatedTarget},movementX:function(a){if("movementX"in a)return a.movementX;a!==Nc&&(Nc&&"mousemove"===a.type?(ag=a.screenX-Nc.screenX,bg=a.screenY-Nc.screenY):bg=ag=0,Nc=a);return ag},movementY:function(a){return"movementY"in a?a.movementY:bg}}),ih=ka(Yd),al=E({},Yd,{dataTransfer:0}),Wj=ka(al),bl=E({},Mc,{relatedTarget:0}),Pe=ka(bl),cl=E({},Yb,{animationName:0,elapsedTime:0,pseudoElement:0}),Yj=ka(cl),dl=E({},Yb,{clipboardData:function(a){return"clipboardData"in a?a.clipboardData:window.clipboardData}}),
ck=ka(dl),el=E({},Yb,{data:0}),qh=ka(el),fk=qh,fl={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},gl={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",
112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"},Gj={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"},hl=E({},Mc,{key:function(a){if(a.key){var b=fl[a.key]||a.key;if("Unidentified"!==b)return b}return"keypress"===a.type?(a=cd(a),13===a?"Enter":String.fromCharCode(a)):"keydown"===a.type||"keyup"===a.type?gl[a.keyCode]||"Unidentified":""},code:0,location:0,ctrlKey:0,shiftKey:0,altKey:0,
metaKey:0,repeat:0,locale:0,getModifierState:Fe,charCode:function(a){return"keypress"===a.type?cd(a):0},keyCode:function(a){return"keydown"===a.type||"keyup"===a.type?a.keyCode:0},which:function(a){return"keypress"===a.type?cd(a):"keydown"===a.type||"keyup"===a.type?a.keyCode:0}}),Vj=ka(hl),il=E({},Yd,{pointerId:0,width:0,height:0,pressure:0,tangentialPressure:0,tiltX:0,tiltY:0,twist:0,pointerType:0,isPrimary:0}),nh=ka(il),jl=E({},Mc,{touches:0,targetTouches:0,changedTouches:0,altKey:0,metaKey:0,
ctrlKey:0,shiftKey:0,getModifierState:Fe}),Xj=ka(jl),kl=E({},Yb,{propertyName:0,elapsedTime:0,pseudoElement:0}),Zj=ka(kl),ll=E({},Yd,{deltaX:function(a){return"deltaX"in a?a.deltaX:"wheelDeltaX"in a?-a.wheelDeltaX:0},deltaY:function(a){return"deltaY"in a?a.deltaY:"wheelDeltaY"in a?-a.wheelDeltaY:"wheelDelta"in a?-a.wheelDelta:0},deltaZ:0,deltaMode:0}),bk=ka(ll),Hj=[9,13,27,32],Ge=Ia&&"CompositionEvent"in window,Oc=null;Ia&&"documentMode"in document&&(Oc=document.documentMode);var ek=Ia&&"TextEvent"in
window&&!Oc,Ug=Ia&&(!Ge||Oc&&8<Oc&&11>=Oc),Tg=String.fromCharCode(32),Sg=!1,Hb=!1,Kj={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},oc=null,pc=null,ph=!1;Ia&&(ph=Lj("input")&&(!document.documentMode||9<document.documentMode));var ua="function"===typeof Object.is?Object.is:Sj,dk=Ia&&"documentMode"in document&&11>=document.documentMode,Jb=null,Ke=null,rc=null,Je=!1,Kb={animationend:gd("Animation","AnimationEnd"),
animationiteration:gd("Animation","AnimationIteration"),animationstart:gd("Animation","AnimationStart"),transitionend:gd("Transition","TransitionEnd")},Le={},eh={};Ia&&(eh=document.createElement("div").style,"AnimationEvent"in window||(delete Kb.animationend.animation,delete Kb.animationiteration.animation,delete Kb.animationstart.animation),"TransitionEvent"in window||delete Kb.transitionend.transition);var jh=hd("animationend"),kh=hd("animationiteration"),lh=hd("animationstart"),mh=hd("transitionend"),
fh=new Map,Zi="abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");
(function(){for(var a=0;a<Zi.length;a++){var b=Zi[a],c=b.toLowerCase();b=b[0].toUpperCase()+b.slice(1);$a(c,"on"+b)}$a(jh,"onAnimationEnd");$a(kh,"onAnimationIteration");$a(lh,"onAnimationStart");$a("dblclick","onDoubleClick");$a("focusin","onFocus");$a("focusout","onBlur");$a(mh,"onTransitionEnd")})();Ab("onMouseEnter",["mouseout","mouseover"]);Ab("onMouseLeave",["mouseout","mouseover"]);Ab("onPointerEnter",["pointerout","pointerover"]);Ab("onPointerLeave",["pointerout","pointerover"]);mb("onChange",
"change click focusin focusout input keydown keyup selectionchange".split(" "));mb("onSelect","focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange".split(" "));mb("onBeforeInput",["compositionend","keypress","textInput","paste"]);mb("onCompositionEnd","compositionend focusout keydown keypress keyup mousedown".split(" "));mb("onCompositionStart","compositionstart focusout keydown keypress keyup mousedown".split(" "));mb("onCompositionUpdate","compositionupdate focusout keydown keypress keyup mousedown".split(" "));
var Ec="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),Uj=new Set("cancel close invalid load scroll toggle".split(" ").concat(Ec)),id="_reactListening"+Math.random().toString(36).slice(2),gk=/\r\n?/g,hk=/\u0000|\uFFFD/g,Jf=null,Kf=null,Rf="function"===typeof setTimeout?setTimeout:void 0,Nk="function"===typeof clearTimeout?
clearTimeout:void 0,$i="function"===typeof Promise?Promise:void 0,Jk="function"===typeof queueMicrotask?queueMicrotask:"undefined"!==typeof $i?function(a){return $i.resolve(null).then(a).catch(ik)}:Rf,Zb=Math.random().toString(36).slice(2),Da="__reactFiber$"+Zb,uc="__reactProps$"+Zb,Ja="__reactContainer$"+Zb,Me="__reactEvents$"+Zb,Dk="__reactListeners$"+Zb,Ek="__reactHandles$"+Zb,Se=[],Mb=-1,cb={},J=bb(cb),S=bb(!1),pb=cb,La=null,md=!1,Te=!1,Ob=[],Pb=0,od=null,nd=0,na=[],oa=0,rb=null,Ma=1,Na="",la=
null,fa=null,D=!1,wa=null,Ik=Sa.ReactCurrentBatchConfig,Vb=Dh(!0),li=Dh(!1),ud=bb(null),td=null,Rb=null,bf=null,tb=null,kk=Oa,gb=!1,wc={},Ea=bb(wc),yc=bb(wc),xc=bb(wc),F=bb(0),kf=[],yd=Sa.ReactCurrentDispatcher,sf=Sa.ReactCurrentBatchConfig,vb=0,C=null,K=null,N=null,Ad=!1,zc=!1,Ac=0,ml=0,zd={readContext:qa,useCallback:V,useContext:V,useEffect:V,useImperativeHandle:V,useInsertionEffect:V,useLayoutEffect:V,useMemo:V,useReducer:V,useRef:V,useState:V,useDebugValue:V,useDeferredValue:V,useTransition:V,
useMutableSource:V,useSyncExternalStore:V,useId:V,unstable_isNewReconciler:!1},lk={readContext:qa,useCallback:function(a,b){Fa().memoizedState=[a,void 0===b?null:b];return a},useContext:qa,useEffect:Sh,useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Bd(4194308,4,Vh.bind(null,b,a),c)},useLayoutEffect:function(a,b){return Bd(4194308,4,a,b)},useInsertionEffect:function(a,b){return Bd(4,2,a,b)},useMemo:function(a,b){var c=Fa();b=void 0===b?null:b;a=a();c.memoizedState=
[a,b];return a},useReducer:function(a,b,c){var d=Fa();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};d.queue=a;a=a.dispatch=qk.bind(null,C,a);return[d.memoizedState,a]},useRef:function(a){var b=Fa();a={current:a};return b.memoizedState=a},useState:Qh,useDebugValue:rf,useDeferredValue:function(a){return Fa().memoizedState=a},useTransition:function(){var a=Qh(!1),b=a[0];a=pk.bind(null,a[1]);Fa().memoizedState=
a;return[b,a]},useMutableSource:function(a,b,c){},useSyncExternalStore:function(a,b,c){var d=C,e=Fa();if(D){if(void 0===c)throw Error(m(407));c=c()}else{c=b();if(null===O)throw Error(m(349));0!==(vb&30)||Nh(d,b,c)}e.memoizedState=c;var f={value:c,getSnapshot:b};e.queue=f;Sh(Lh.bind(null,d,f,a),[a]);d.flags|=2048;Cc(9,Mh.bind(null,d,f,c,b),void 0,null);return c},useId:function(){var a=Fa(),b=O.identifierPrefix;if(D){var c=Na;var d=Ma;c=(d&~(1<<32-ta(d)-1)).toString(32)+c;b=":"+b+"R"+c;c=Ac++;0<c&&
(b+="H"+c.toString(32));b+=":"}else c=ml++,b=":"+b+"r"+c.toString(32)+":";return a.memoizedState=b},unstable_isNewReconciler:!1},mk={readContext:qa,useCallback:Xh,useContext:qa,useEffect:qf,useImperativeHandle:Wh,useInsertionEffect:Th,useLayoutEffect:Uh,useMemo:Yh,useReducer:of,useRef:Rh,useState:function(a){return of(Bc)},useDebugValue:rf,useDeferredValue:function(a){var b=sa();return Zh(b,K.memoizedState,a)},useTransition:function(){var a=of(Bc)[0],b=sa().memoizedState;return[a,b]},useMutableSource:Jh,
useSyncExternalStore:Kh,useId:$h,unstable_isNewReconciler:!1},nk={readContext:qa,useCallback:Xh,useContext:qa,useEffect:qf,useImperativeHandle:Wh,useInsertionEffect:Th,useLayoutEffect:Uh,useMemo:Yh,useReducer:pf,useRef:Rh,useState:function(a){return pf(Bc)},useDebugValue:rf,useDeferredValue:function(a){var b=sa();return null===K?b.memoizedState=a:Zh(b,K.memoizedState,a)},useTransition:function(){var a=pf(Bc)[0],b=sa().memoizedState;return[a,b]},useMutableSource:Jh,useSyncExternalStore:Kh,useId:$h,
unstable_isNewReconciler:!1},Dd={isMounted:function(a){return(a=a._reactInternals)?nb(a)===a:!1},enqueueSetState:function(a,b,c){a=a._reactInternals;var d=Z(),e=hb(a),f=Pa(d,e);f.payload=b;void 0!==c&&null!==c&&(f.callback=c);b=fb(a,f,e);null!==b&&(xa(b,a,e,d),vd(b,a,e))},enqueueReplaceState:function(a,b,c){a=a._reactInternals;var d=Z(),e=hb(a),f=Pa(d,e);f.tag=1;f.payload=b;void 0!==c&&null!==c&&(f.callback=c);b=fb(a,f,e);null!==b&&(xa(b,a,e,d),vd(b,a,e))},enqueueForceUpdate:function(a,b){a=a._reactInternals;
var c=Z(),d=hb(a),e=Pa(c,d);e.tag=2;void 0!==b&&null!==b&&(e.callback=b);b=fb(a,e,d);null!==b&&(xa(b,a,d,c),vd(b,a,d))}},rk="function"===typeof WeakMap?WeakMap:Map,tk=Sa.ReactCurrentOwner,ha=!1,Cf={dehydrated:null,treeContext:null,retryLane:0};var zk=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=
c.return;c=c.sibling}};var xi=function(a,b){};var yk=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){a=b.stateNode;ub(Ea.current);e=null;switch(c){case "input":f=ke(a,f);d=ke(a,d);e=[];break;case "select":f=E({},f,{value:void 0});d=E({},d,{value:void 0});e=[];break;case "textarea":f=ne(a,f);d=ne(a,d);e=[];break;default:"function"!==typeof f.onClick&&"function"===typeof d.onClick&&(a.onclick=kd)}pe(c,d);var g;c=null;for(l in f)if(!d.hasOwnProperty(l)&&f.hasOwnProperty(l)&&null!=f[l])if("style"===
l){var h=f[l];for(g in h)h.hasOwnProperty(g)&&(c||(c={}),c[g]="")}else"dangerouslySetInnerHTML"!==l&&"children"!==l&&"suppressContentEditableWarning"!==l&&"suppressHydrationWarning"!==l&&"autoFocus"!==l&&($b.hasOwnProperty(l)?e||(e=[]):(e=e||[]).push(l,null));for(l in d){var k=d[l];h=null!=f?f[l]:void 0;if(d.hasOwnProperty(l)&&k!==h&&(null!=k||null!=h))if("style"===l)if(h){for(g in h)!h.hasOwnProperty(g)||k&&k.hasOwnProperty(g)||(c||(c={}),c[g]="");for(g in k)k.hasOwnProperty(g)&&h[g]!==k[g]&&(c||
(c={}),c[g]=k[g])}else c||(e||(e=[]),e.push(l,c)),c=k;else"dangerouslySetInnerHTML"===l?(k=k?k.__html:void 0,h=h?h.__html:void 0,null!=k&&h!==k&&(e=e||[]).push(l,k)):"children"===l?"string"!==typeof k&&"number"!==typeof k||(e=e||[]).push(l,""+k):"suppressContentEditableWarning"!==l&&"suppressHydrationWarning"!==l&&($b.hasOwnProperty(l)?(null!=k&&"onScroll"===l&&B("scroll",a),e||h===k||(e=[])):(e=e||[]).push(l,k))}c&&(e=e||[]).push("style",c);var l=e;if(b.updateQueue=l)b.flags|=4}};var Ak=function(a,
b,c,d){c!==d&&(b.flags|=4)};var Jd=!1,X=!1,Fk="function"===typeof WeakSet?WeakSet:Set,l=null,zi=!1,T=null,za=!1,Mk=Math.ceil,Od=Sa.ReactCurrentDispatcher,Uf=Sa.ReactCurrentOwner,ca=Sa.ReactCurrentBatchConfig,p=0,O=null,H=null,U=0,ba=0,Ga=bb(0),L=0,Jc=null,ra=0,Md=0,Sf=0,Kc=null,ja=null,Of=0,Hf=Infinity,Ra=null,Ed=!1,xf=null,ib=null,Pd=!1,lb=null,Qd=0,Ic=0,Pf=null,Kd=-1,Ld=0;var Qk=function(a,b,c){if(null!==a)if(a.memoizedProps!==b.pendingProps||S.current)ha=!0;else{if(0===(a.lanes&c)&&0===(b.flags&
128))return ha=!1,wk(a,b,c);ha=0!==(a.flags&131072)?!0:!1}else ha=!1,D&&0!==(b.flags&1048576)&&yh(b,nd,b.index);b.lanes=0;switch(b.tag){case 2:var d=b.type;Fd(a,b);a=b.pendingProps;var e=Nb(b,J.current);Sb(b,c);e=mf(null,b,d,a,e,c);var f=nf();b.flags|=1;"object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof?(b.tag=1,b.memoizedState=null,b.updateQueue=null,ea(d)?(f=!0,ld(b)):f=!1,b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null,ff(b),e.updater=Dd,b.stateNode=
e,e._reactInternals=b,uf(b,d,a,c),b=Af(null,b,d,!0,f,c)):(b.tag=0,D&&f&&Ue(b),aa(null,b,e,c),b=b.child);return b;case 16:d=b.elementType;a:{Fd(a,b);a=b.pendingProps;e=d._init;d=e(d._payload);b.type=d;e=b.tag=Uk(d);a=ya(d,a);switch(e){case 0:b=zf(null,b,d,a,c);break a;case 1:b=ri(null,b,d,a,c);break a;case 11:b=mi(null,b,d,a,c);break a;case 14:b=ni(null,b,d,ya(d.type,a),c);break a}throw Error(m(306,d,""));}return b;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ya(d,e),zf(a,b,d,e,c);
case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ya(d,e),ri(a,b,d,e,c);case 3:a:{si(b);if(null===a)throw Error(m(387));d=b.pendingProps;f=b.memoizedState;e=f.element;Fh(a,b);wd(b,d,null,c);var g=b.memoizedState;d=g.element;if(f.isDehydrated)if(f={element:d,isDehydrated:!1,cache:g.cache,pendingSuspenseBoundaries:g.pendingSuspenseBoundaries,transitions:g.transitions},b.updateQueue.baseState=f,b.memoizedState=f,b.flags&256){e=Ub(Error(m(423)),b);b=ti(a,b,d,c,e);break a}else if(d!==e){e=
Ub(Error(m(424)),b);b=ti(a,b,d,c,e);break a}else for(fa=Ka(b.stateNode.containerInfo.firstChild),la=b,D=!0,wa=null,c=li(b,null,d,c),b.child=c;c;)c.flags=c.flags&-3|4096,c=c.sibling;else{Qb();if(d===e){b=Qa(a,b,c);break a}aa(a,b,d,c)}b=b.child}return b;case 5:return Ih(b),null===a&&Xe(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:null,g=e.children,Qe(d,e)?g=null:null!==f&&Qe(d,f)&&(b.flags|=32),qi(a,b),aa(a,b,g,c),b.child;case 6:return null===a&&Xe(b),null;case 13:return ui(a,b,c);case 4:return gf(b,
b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=Vb(b,null,d,c):aa(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ya(d,e),mi(a,b,d,e,c);case 7:return aa(a,b,b.pendingProps,c),b.child;case 8:return aa(a,b,b.pendingProps.children,c),b.child;case 12:return aa(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;f=b.memoizedProps;g=e.value;y(ud,d._currentValue);d._currentValue=g;if(null!==f)if(ua(f.value,g)){if(f.children===
e.children&&!S.current){b=Qa(a,b,c);break a}}else for(f=b.child,null!==f&&(f.return=b);null!==f;){var h=f.dependencies;if(null!==h){g=f.child;for(var k=h.firstContext;null!==k;){if(k.context===d){if(1===f.tag){k=Pa(-1,c&-c);k.tag=2;var l=f.updateQueue;if(null!==l){l=l.shared;var p=l.pending;null===p?k.next=k:(k.next=p.next,p.next=k);l.pending=k}}f.lanes|=c;k=f.alternate;null!==k&&(k.lanes|=c);df(f.return,c,b);h.lanes|=c;break}k=k.next}}else if(10===f.tag)g=f.type===b.type?null:f.child;else if(18===
f.tag){g=f.return;if(null===g)throw Error(m(341));g.lanes|=c;h=g.alternate;null!==h&&(h.lanes|=c);df(g,c,b);g=f.sibling}else g=f.child;if(null!==g)g.return=f;else for(g=f;null!==g;){if(g===b){g=null;break}f=g.sibling;if(null!==f){f.return=g.return;g=f;break}g=g.return}f=g}aa(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,d=b.pendingProps.children,Sb(b,c),e=qa(e),d=d(e),b.flags|=1,aa(a,b,d,c),b.child;case 14:return d=b.type,e=ya(d,b.pendingProps),e=ya(d.type,e),ni(a,b,d,e,c);case 15:return oi(a,
b,b.type,b.pendingProps,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ya(d,e),Fd(a,b),b.tag=1,ea(d)?(a=!0,ld(b)):a=!1,Sb(b,c),ei(b,d,e),uf(b,d,e,c),Af(null,b,d,!0,a,c);case 19:return wi(a,b,c);case 22:return pi(a,b,c)}throw Error(m(156,b.tag));};var pa=function(a,b,c,d){return new Tk(a,b,c,d)},aj="function"===typeof reportError?reportError:function(a){console.error(a)};Ud.prototype.render=Xf.prototype.render=function(a){var b=this._internalRoot;if(null===b)throw Error(m(409));
Sd(a,b,null,null)};Ud.prototype.unmount=Xf.prototype.unmount=function(){var a=this._internalRoot;if(null!==a){this._internalRoot=null;var b=a.containerInfo;yb(function(){Sd(null,a,null,null)});b[Ja]=null}};Ud.prototype.unstable_scheduleHydration=function(a){if(a){var b=nl();a={blockedOn:null,target:a,priority:b};for(var c=0;c<Ya.length&&0!==b&&b<Ya[c].priority;c++);Ya.splice(c,0,a);0===c&&Hg(a)}};var Cj=function(a){switch(a.tag){case 3:var b=a.stateNode;if(b.current.memoizedState.isDehydrated){var c=
hc(b.pendingLanes);0!==c&&(xe(b,c|1),ia(b,P()),0===(p&6)&&(Hc(),db()))}break;case 13:yb(function(){var b=Oa(a,1);if(null!==b){var c=Z();xa(b,a,1,c)}}),Wf(a,1)}};var Gg=function(a){if(13===a.tag){var b=Oa(a,134217728);if(null!==b){var c=Z();xa(b,a,134217728,c)}Wf(a,134217728)}};var xj=function(a){if(13===a.tag){var b=hb(a),c=Oa(a,b);if(null!==c){var d=Z();xa(c,a,b,d)}Wf(a,b)}};var nl=function(){return z};var wj=function(a,b){var c=z;try{return z=a,b()}finally{z=c}};se=function(a,b,c){switch(b){case "input":le(a,
c);b=c.name;if("radio"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll("input[name="+JSON.stringify(""+b)+'][type="radio"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=Rc(d);if(!e)throw Error(m(90));jg(d);le(d,e)}}}break;case "textarea":og(a,c);break;case "select":b=c.value,null!=b&&Db(a,!!c.multiple,b,!1)}};(function(a,b,c){xg=a;yg=c})(Tf,function(a,b,c,d,e){var f=z,g=ca.transition;try{return ca.transition=null,z=1,a(b,c,d,e)}finally{z=f,ca.transition=
g,0===p&&Hc()}},yb);var ol={usingClientEntryPoint:!1,Events:[ec,Ib,Rc,ug,vg,Tf]};(function(a){a={bundleType:a.bundleType,version:a.version,rendererPackageName:a.rendererPackageName,rendererConfig:a.rendererConfig,overrideHookState:null,overrideHookStateDeletePath:null,overrideHookStateRenamePath:null,overrideProps:null,overridePropsDeletePath:null,overridePropsRenamePath:null,setErrorHandler:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:Sa.ReactCurrentDispatcher,findHostInstanceByFiber:Xk,
findFiberByHostInstance:a.findFiberByHostInstance||Yk,findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null,reconcilerVersion:"18.3.1"};if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)a=!1;else{var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)a=!0;else{try{Uc=b.inject(a),Ca=b}catch(c){}a=b.checkDCE?!0:!1}}return a})({findFiberByHostInstance:ob,bundleType:0,version:"18.3.1-next-f1338f8080-20240426",
rendererPackageName:"react-dom"});Q.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=ol;Q.createPortal=function(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;if(!Yf(b))throw Error(m(200));return Wk(a,b,null,c)};Q.createRoot=function(a,b){if(!Yf(a))throw Error(m(299));var c=!1,d="",e=aj;null!==b&&void 0!==b&&(!0===b.unstable_strictMode&&(c=!0),void 0!==b.identifierPrefix&&(d=b.identifierPrefix),void 0!==b.onRecoverableError&&(e=b.onRecoverableError));b=Vf(a,1,!1,null,null,
c,!1,d,e);a[Ja]=b.current;sc(8===a.nodeType?a.parentNode:a);return new Xf(b)};Q.findDOMNode=function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternals;if(void 0===b){if("function"===typeof a.render)throw Error(m(188));a=Object.keys(a).join(",");throw Error(m(268,a));}a=Bg(b);a=null===a?null:a.stateNode;return a};Q.flushSync=function(a){return yb(a)};Q.hydrate=function(a,b,c){if(!Vd(b))throw Error(m(200));return Wd(null,a,b,!0,c)};Q.hydrateRoot=function(a,b,c){if(!Yf(a))throw Error(m(405));
var d=null!=c&&c.hydratedSources||null,e=!1,f="",g=aj;null!==c&&void 0!==c&&(!0===c.unstable_strictMode&&(e=!0),void 0!==c.identifierPrefix&&(f=c.identifierPrefix),void 0!==c.onRecoverableError&&(g=c.onRecoverableError));b=Wi(b,null,a,1,null!=c?c:null,e,!1,f,g);a[Ja]=b.current;sc(a);if(d)for(a=0;a<d.length;a++)c=d[a],e=c._getVersion,e=e(c._source),null==b.mutableSourceEagerHydrationData?b.mutableSourceEagerHydrationData=[c,e]:b.mutableSourceEagerHydrationData.push(c,e);return new Ud(b)};Q.render=
function(a,b,c){if(!Vd(b))throw Error(m(200));return Wd(null,a,b,!1,c)};Q.unmountComponentAtNode=function(a){if(!Vd(a))throw Error(m(40));return a._reactRootContainer?(yb(function(){Wd(null,null,a,!1,function(){a._reactRootContainer=null;a[Ja]=null})}),!0):!1};Q.unstable_batchedUpdates=Tf;Q.unstable_renderSubtreeIntoContainer=function(a,b,c,d){if(!Vd(c))throw Error(m(200));if(null==a||void 0===a._reactInternals)throw Error(m(38));return Wd(a,b,c,!1,d)};Q.version="18.3.1-next-f1338f8080-20240426"});
})();
</script>
<!-- data.jsx: test-only seed data — not loaded by default.
To run with sample data, uncomment the line below:
<script type="text/babel" src="data.jsx"></script> -->
<script>
// Shared helpers, status definitions, filter engine, icons.
const STATUSES = [{
key: "open",
glyph: "[ ]",
label: "Open"
}, {
key: "in-progress",
glyph: "[/]",
label: "In progress"
}, {
key: "blocked",
glyph: "[!]",
label: "Blocked"
}, {
key: "postponed",
glyph: "[>]",
label: "Postponed"
}, {
key: "done",
glyph: "[x]",
label: "Done"
}, {
key: "cancelled",
glyph: "[-]",
label: "Cancelled"
}];
const STATUS_BY_KEY = Object.fromEntries(STATUSES.map(s => [s.key, s]));
const STATUS_ORDER = STATUSES.map(s => s.key);
const PRIORITIES = ["P0", "P1", "P2", "P3"];
// ---- date helpers ----
function parseDate(s) {
return s ? new Date(s + "T12:00:00Z") : null;
}
function daysBetween(a, b) {
return Math.round((a - b) / 86400000);
}
function isOverdue(item) {
if (!item.due || item.status === "done" || item.status === "cancelled") return false;
return parseDate(item.due) < new Date();
}
function dueRelative(item) {
if (!item.due) return null;
const d = daysBetween(parseDate(item.due), new Date());
if (d < 0) return `${-d}d overdue`;
if (d === 0) return "today";
if (d === 1) return "tomorrow";
if (d < 7) return `in ${d}d`;
if (d < 30) return `in ${Math.round(d / 7)}w`;
return item.due;
}
function fmtTimestamp(iso) {
if (!iso) return "—";
const d = new Date(iso);
const diff = (Date.now() - d.getTime()) / 1000;
if (diff < 60) return "just now";
if (diff < 3600) return `${Math.round(diff / 60)}m ago`;
if (diff < 86400) return `${Math.round(diff / 3600)}h ago`;
if (diff < 86400 * 7) return `${Math.round(diff / 86400)}d ago`;
return d.toISOString().slice(0, 10);
}
function fmtBytes(b) {
if (b < 1024) return `${b} B`;
if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
return `${(b / 1024 / 1024).toFixed(2)} MB`;
}
const fmtRelative = fmtTimestamp;
function fmtShortDate(iso) {
if (!iso) return "—";
const d = new Date(iso);
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
}
// ---- tree helpers ----
function walkTree(items, fn, depth = 0, parent = null) {
for (const it of items) {
fn(it, depth, parent);
if (it.children?.length) walkTree(it.children, fn, depth + 1, it);
}
}
function findItem(items, id) {
for (const it of items) {
if (it.id === id) return it;
if (it.children?.length) {
const f = findItem(it.children, id);
if (f) return f;
}
}
return null;
}
function allTags(items) {
const counts = new Map();
walkTree(items, it => (it.tags || []).forEach(t => counts.set(t, (counts.get(t) || 0) + 1)));
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([t]) => t);
}
function countAll(items) {
let n = 0;
walkTree(items, () => n++);
return n;
}
function countByStatus(items) {
const c = Object.fromEntries(STATUSES.map(s => [s.key, 0]));
walkTree(items, it => {
c[it.status] = (c[it.status] || 0) + 1;
});
return c;
}
// ---- filter engine with parent-visibility rule ----
function filterTree(items, f) {
function matches(it) {
if (f.statuses?.length && !f.statuses.includes(it.status)) return false;
if (f.priorities?.length && !f.priorities.includes(it.priority)) return false;
if (f.tags?.length && !(it.tags || []).some(t => f.tags.includes(t))) return false;
if (f.dueRange) {
const overdue = isOverdue(it);
const d = it.due ? daysBetween(parseDate(it.due), new Date()) : null;
if (f.dueRange === "overdue" && !overdue) return false;
if (f.dueRange === "today" && !(d === 0)) return false;
if (f.dueRange === "week" && !(d !== null && d >= 0 && d <= 7)) return false;
if (f.dueRange === "month" && !(d !== null && d >= 0 && d <= 30)) return false;
}
if (f.text) {
const q = f.text.toLowerCase();
const hay = [it.title, it.body, it.reason, ...(it.tags || [])].filter(Boolean).join(" ").toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
}
function recur(list) {
const out = [];
for (const it of list) {
const kids = it.children?.length ? recur(it.children) : [];
const self = matches(it);
if (self || kids.length) {
out.push({
...it,
children: kids,
_matches: self
});
}
}
return out;
}
return recur(items);
}
// ---- status visuals (4 styles, switchable via Tweaks) ----
// styles: "flat" (minimal monochrome SVG, inherits status color),
// "ascii" (original [/], [x] etc),
// "color" (filled colored icons, ChatGPT-style — green check, red blocked, etc),
// "emoji" (expressive native emoji — ✅ 🚧 ⏸ etc)
// Self-contained color palette for the "color" style — does NOT inherit currentColor.
const STATUS_COLORS = {
"open": {
stroke: "oklch(0.62 0.01 70)",
fill: "transparent"
},
"in-progress": {
stroke: "oklch(0.55 0.16 230)",
fill: "oklch(0.62 0.16 230)"
},
"blocked": {
stroke: "oklch(0.50 0.20 28)",
fill: "oklch(0.62 0.20 28)"
},
"postponed": {
stroke: "oklch(0.55 0.13 290)",
fill: "oklch(0.65 0.13 290)"
},
"done": {
stroke: "oklch(0.48 0.16 145)",
fill: "oklch(0.60 0.16 145)"
},
"cancelled": {
stroke: "oklch(0.55 0.01 70)",
fill: "oklch(0.65 0.01 70)"
}
};
// Expressive emoji pack — chosen for clarity at small sizes (avoid round colored dots).
const STATUS_EMOJI = {
"open": "⬜",
"in-progress": "✏️",
"blocked": "🚧",
"postponed": "⏸️",
"done": "✅",
"cancelled": "❌"
};
// Context so every StatusIcon in the tree picks up the current style automatically.
const StatusStyleContext = React.createContext("flat");
function StatusIcon({
status,
style,
size = 14
}) {
const ctx = React.useContext(StatusStyleContext);
style = style || ctx || "flat";
if (style === "ascii") {
return /*#__PURE__*/React.createElement("span", {
className: `glyph-ascii status-${status}`
}, STATUS_BY_KEY[status].glyph);
}
if (style === "color") {
return /*#__PURE__*/React.createElement(ColorStatusIcon, {
status: status,
size: size
});
}
if (style === "emoji") {
return /*#__PURE__*/React.createElement("span", {
className: "glyph-emoji",
style: {
fontSize: size + 2,
lineHeight: 1
}
}, STATUS_EMOJI[status]);
}
// flat — minimalistic SVG icons (square family, no circles)
const c = "currentColor";
const wrap = children => /*#__PURE__*/React.createElement("svg", {
width: size + 2,
height: size + 2,
viewBox: "0 0 16 16",
className: `glyph-flat status-${status}`
}, children);
switch (status) {
case "open":
// empty square outline
return wrap(/*#__PURE__*/React.createElement("rect", {
x: "2.5",
y: "2.5",
width: "11",
height: "11",
rx: "2",
fill: "none",
stroke: c,
strokeWidth: "1.4"
}));
case "in-progress":
// half-filled square (left filled)
return wrap(/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
x: "2.5",
y: "2.5",
width: "11",
height: "11",
rx: "2",
fill: "none",
stroke: c,
strokeWidth: "1.4"
}), /*#__PURE__*/React.createElement("path", {
d: "M3.5 3.5 H8 V12.5 H3.5 Z",
fill: c
})));
case "blocked":
// filled square with horizontal bar (no entry)
return wrap(/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
x: "2.5",
y: "2.5",
width: "11",
height: "11",
rx: "2",
fill: c
}), /*#__PURE__*/React.createElement("rect", {
x: "4.5",
y: "7.2",
width: "7",
height: "1.6",
rx: "0.5",
fill: "white"
})));
case "postponed":
// square with right-pointing chevron (deferred)
return wrap(/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
x: "2.5",
y: "2.5",
width: "11",
height: "11",
rx: "2",
fill: "none",
stroke: c,
strokeWidth: "1.4"
}), /*#__PURE__*/React.createElement("polyline", {
points: "6,5.5 9,8 6,10.5",
fill: "none",
stroke: c,
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
})));
case "done":
// filled square with checkmark
return wrap(/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
x: "2.5",
y: "2.5",
width: "11",
height: "11",
rx: "2",
fill: c
}), /*#__PURE__*/React.createElement("polyline", {
points: "5,8 7,10.2 11,5.8",
fill: "none",
stroke: "white",
strokeWidth: "1.7",
strokeLinecap: "round",
strokeLinejoin: "round"
})));
case "cancelled":
// outlined square with single clean diagonal slash
return wrap(/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
x: "2.5",
y: "2.5",
width: "11",
height: "11",
rx: "2",
fill: "none",
stroke: c,
strokeWidth: "1.4"
}), /*#__PURE__*/React.createElement("line", {
x1: "3.5",
y1: "12.5",
x2: "12.5",
y2: "3.5",
stroke: c,
strokeWidth: "1.6",
strokeLinecap: "round"
})));
default:
return null;
}
}
// ---- color status icons (ChatGPT / Linear / GitHub style) ----
// Filled circles with semantic colors and white glyphs. Self-coloring,
// not affected by currentColor — looks good on any background.
function ColorStatusIcon({
status,
size = 14
}) {
const c = STATUS_COLORS[status] || STATUS_COLORS.open;
const w = size + 2;
const cx = 8,
cy = 8,
r = 6.4;
switch (status) {
case "open":
// empty circle — thin gray ring
return /*#__PURE__*/React.createElement("svg", {
width: w,
height: w,
viewBox: "0 0 16 16",
className: `glyph-color status-${status}`
}, /*#__PURE__*/React.createElement("circle", {
cx: cx,
cy: cy,
r: r,
fill: "none",
stroke: c.stroke,
strokeWidth: "1.5"
}));
case "in-progress":
// half-filled pie (clockwise 50%) — Linear-style "in progress"
return /*#__PURE__*/React.createElement("svg", {
width: w,
height: w,
viewBox: "0 0 16 16",
className: `glyph-color status-${status}`
}, /*#__PURE__*/React.createElement("circle", {
cx: cx,
cy: cy,
r: r,
fill: "none",
stroke: c.fill,
strokeWidth: "1.5"
}), /*#__PURE__*/React.createElement("path", {
d: `M 8 1.6 A 6.4 6.4 0 0 1 8 14.4 Z`,
fill: c.fill
}));
case "blocked":
// red filled circle with white horizontal bar
return /*#__PURE__*/React.createElement("svg", {
width: w,
height: w,
viewBox: "0 0 16 16",
className: `glyph-color status-${status}`
}, /*#__PURE__*/React.createElement("circle", {
cx: cx,
cy: cy,
r: r,
fill: c.fill
}), /*#__PURE__*/React.createElement("rect", {
x: "4.6",
y: "7.2",
width: "6.8",
height: "1.6",
rx: "0.8",
fill: "white"
}));
case "postponed":
// purple filled circle with white right-chevron
return /*#__PURE__*/React.createElement("svg", {
width: w,
height: w,
viewBox: "0 0 16 16",
className: `glyph-color status-${status}`
}, /*#__PURE__*/React.createElement("circle", {
cx: cx,
cy: cy,
r: r,
fill: c.fill
}), /*#__PURE__*/React.createElement("polyline", {
points: "6.4,5.4 9.4,8 6.4,10.6",
fill: "none",
stroke: "white",
strokeWidth: "1.6",
strokeLinecap: "round",
strokeLinejoin: "round"
}));
case "done":
// green filled circle with white check
return /*#__PURE__*/React.createElement("svg", {
width: w,
height: w,
viewBox: "0 0 16 16",
className: `glyph-color status-${status}`
}, /*#__PURE__*/React.createElement("circle", {
cx: cx,
cy: cy,
r: r,
fill: c.fill
}), /*#__PURE__*/React.createElement("polyline", {
points: "5,8.2 7.2,10.3 11,5.9",
fill: "none",
stroke: "white",
strokeWidth: "1.7",
strokeLinecap: "round",
strokeLinejoin: "round"
}));
case "cancelled":
// gray filled circle with white X
return /*#__PURE__*/React.createElement("svg", {
width: w,
height: w,
viewBox: "0 0 16 16",
className: `glyph-color status-${status}`
}, /*#__PURE__*/React.createElement("circle", {
cx: cx,
cy: cy,
r: r,
fill: c.fill
}), /*#__PURE__*/React.createElement("g", {
stroke: "white",
strokeWidth: "1.6",
strokeLinecap: "round"
}, /*#__PURE__*/React.createElement("line", {
x1: "5.6",
y1: "5.6",
x2: "10.4",
y2: "10.4"
}), /*#__PURE__*/React.createElement("line", {
x1: "10.4",
y1: "5.6",
x2: "5.6",
y2: "10.4"
})));
default:
return null;
}
}
// ---- icons (inline SVG) ----
const Icon = ({
name,
size = 14
}) => {
const paths = {
chevron: /*#__PURE__*/React.createElement("polyline", {
points: "5,8 10,13 15,8",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.6",
strokeLinecap: "round",
strokeLinejoin: "round"
}),
chevronRight: /*#__PURE__*/React.createElement("polyline", {
points: "8,5 13,10 8,15",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.6",
strokeLinecap: "round",
strokeLinejoin: "round"
}),
plus: /*#__PURE__*/React.createElement("g", {
stroke: "currentColor",
strokeWidth: "1.6",
strokeLinecap: "round"
}, /*#__PURE__*/React.createElement("line", {
x1: "10",
y1: "4",
x2: "10",
y2: "16"
}), /*#__PURE__*/React.createElement("line", {
x1: "4",
y1: "10",
x2: "16",
y2: "10"
})),
search: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.6",
strokeLinecap: "round"
}, /*#__PURE__*/React.createElement("circle", {
cx: "9",
cy: "9",
r: "5"
}), /*#__PURE__*/React.createElement("line", {
x1: "13",
y1: "13",
x2: "17",
y2: "17"
})),
drag: /*#__PURE__*/React.createElement("g", {
fill: "currentColor"
}, /*#__PURE__*/React.createElement("circle", {
cx: "7",
cy: "5",
r: "1.2"
}), /*#__PURE__*/React.createElement("circle", {
cx: "13",
cy: "5",
r: "1.2"
}), /*#__PURE__*/React.createElement("circle", {
cx: "7",
cy: "10",
r: "1.2"
}), /*#__PURE__*/React.createElement("circle", {
cx: "13",
cy: "10",
r: "1.2"
}), /*#__PURE__*/React.createElement("circle", {
cx: "7",
cy: "15",
r: "1.2"
}), /*#__PURE__*/React.createElement("circle", {
cx: "13",
cy: "15",
r: "1.2"
})),
trash: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round"
}, /*#__PURE__*/React.createElement("path", {
d: "M5 6h10M8 6V4h4v2M6 6l1 10h6l1-10"
})),
cog: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5"
}, /*#__PURE__*/React.createElement("circle", {
cx: "10",
cy: "10",
r: "2.6"
}), /*#__PURE__*/React.createElement("path", {
d: "M10 2v2M10 16v2M2 10h2M16 10h2M4.3 4.3l1.4 1.4M14.3 14.3l1.4 1.4M4.3 15.7l1.4-1.4M14.3 5.7l1.4-1.4"
})),
list: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.6",
strokeLinecap: "round"
}, /*#__PURE__*/React.createElement("line", {
x1: "6",
y1: "6",
x2: "16",
y2: "6"
}), /*#__PURE__*/React.createElement("line", {
x1: "6",
y1: "10",
x2: "16",
y2: "10"
}), /*#__PURE__*/React.createElement("line", {
x1: "6",
y1: "14",
x2: "16",
y2: "14"
}), /*#__PURE__*/React.createElement("circle", {
cx: "3.5",
cy: "6",
r: "0.6",
fill: "currentColor"
}), /*#__PURE__*/React.createElement("circle", {
cx: "3.5",
cy: "10",
r: "0.6",
fill: "currentColor"
}), /*#__PURE__*/React.createElement("circle", {
cx: "3.5",
cy: "14",
r: "0.6",
fill: "currentColor"
})),
check: /*#__PURE__*/React.createElement("polyline", {
points: "4,10 8,14 16,5",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.8",
strokeLinecap: "round",
strokeLinejoin: "round"
}),
download: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("path", {
d: "M10 3v10M5 9l5 5 5-5M3 17h14"
})),
upload: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("path", {
d: "M10 16V4M5 9l5-5 5 5M3 17h14"
})),
save: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("path", {
d: "M4 4h9l3 3v9H4z"
}), /*#__PURE__*/React.createElement("path", {
d: "M7 4v4h6V4M7 16v-5h6v5"
})),
refresh: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("path", {
d: "M3 10a7 7 0 0 1 12-5l2 2"
}), /*#__PURE__*/React.createElement("path", {
d: "M17 4v3h-3"
}), /*#__PURE__*/React.createElement("path", {
d: "M17 10a7 7 0 0 1-12 5l-2-2"
}), /*#__PURE__*/React.createElement("path", {
d: "M3 16v-3h3"
})),
warn: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("path", {
d: "M10 3l8 14H2z"
}), /*#__PURE__*/React.createElement("line", {
x1: "10",
y1: "8",
x2: "10",
y2: "12"
}), /*#__PURE__*/React.createElement("circle", {
cx: "10",
cy: "14.5",
r: "0.6",
fill: "currentColor"
})),
eye: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5"
}, /*#__PURE__*/React.createElement("path", {
d: "M2 10s3-5 8-5 8 5 8 5-3 5-8 5-8-5-8-5z"
}), /*#__PURE__*/React.createElement("circle", {
cx: "10",
cy: "10",
r: "2"
})),
archive: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("rect", {
x: "3",
y: "5",
width: "14",
height: "3"
}), /*#__PURE__*/React.createElement("path", {
d: "M4 8v9h12V8"
}), /*#__PURE__*/React.createElement("line", {
x1: "8",
y1: "11",
x2: "12",
y2: "11"
})),
edit: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("path", {
d: "M4 14v2h2l8-8-2-2-8 8z"
}), /*#__PURE__*/React.createElement("path", {
d: "M12 4l2 2"
})),
addChild: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("path", {
d: "M5 4v6a3 3 0 0 0 3 3h4"
}), /*#__PURE__*/React.createElement("line", {
x1: "14",
y1: "13",
x2: "14",
y2: "17"
}), /*#__PURE__*/React.createElement("line", {
x1: "12",
y1: "15",
x2: "16",
y2: "15"
})),
copy: /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("rect", {
x: "7",
y: "3",
width: "10",
height: "12",
rx: "1.5"
}), /*#__PURE__*/React.createElement("path", {
d: "M13 17H4a1 1 0 0 1-1-1V6"
})),
statusDots: /*#__PURE__*/React.createElement("g", {
fill: "currentColor"
}, /*#__PURE__*/React.createElement("circle", {
cx: "5",
cy: "10",
r: "1.6"
}), /*#__PURE__*/React.createElement("circle", {
cx: "10",
cy: "10",
r: "1.6"
}), /*#__PURE__*/React.createElement("circle", {
cx: "15",
cy: "10",
r: "1.6"
}))
};
return /*#__PURE__*/React.createElement("svg", {
width: size,
height: size,
viewBox: "0 0 20 20",
style: {
display: "inline-block",
verticalAlign: "middle",
flexShrink: 0
}
}, paths[name]);
};
// --- Progress (completion %) ---
// Quantized to 0/25/50/75/100 — coarse on purpose; tasks are rarely "73% done".
const PROGRESS_STEPS = [0, 25, 50, 75, 100];
function snapProgress(p) {
if (p == null || isNaN(p)) return 0;
if (p <= 0) return 0;
if (p >= 100) return 100;
// round to nearest 25
return Math.round(p / 25) * 25;
}
// Effective progress: done is always 100 regardless of stored value;
// cancelled returns null (don't render).
function progressFor(item) {
if (item.status === "done") return 100;
if (item.status === "cancelled") return null;
return snapProgress(item.progress ?? 0);
}
// When progress should be visible inline. Done shows nothing (line-through is enough).
// Open shows only if user explicitly set >0.
function shouldShowProgress(item) {
if (item.status === "cancelled") return false;
if (item.status === "done") return false;
if (item.status === "open") return (item.progress ?? 0) > 0;
return true; // in-progress, blocked, postponed (always visible, including at 100%)
}
// Click to advance through 0/25/50/75/100, wrapping.
function cycleProgress(p) {
const i = PROGRESS_STEPS.indexOf(snapProgress(p));
return PROGRESS_STEPS[(i + 1) % PROGRESS_STEPS.length];
}
// 4-segment gauge (visual representation of 0/25/50/75/100).
// 0 = empty, 100 = all 4 filled.
function ProgressGauge({
value,
onClick,
interactive = false,
size = "sm"
}) {
const v = snapProgress(value);
const filled = v / 25; // 0..4
const segs = [0, 1, 2, 3].map(i => i < filled);
return /*#__PURE__*/React.createElement("span", {
className: `gauge ${interactive ? "gauge-interactive" : ""} gauge-${size} gauge-v-${v}`,
onClick: onClick,
role: interactive ? "button" : undefined,
tabIndex: interactive ? 0 : undefined,
title: `${v}% complete${interactive ? " — click to change" : ""}`,
"aria-label": `Progress ${v} percent`
}, segs.map((on, i) => /*#__PURE__*/React.createElement("span", {
key: i,
className: `gauge-seg ${on ? "on" : ""}`
})), /*#__PURE__*/React.createElement("span", {
className: "gauge-num"
}, v, "%"));
}
// ---- Markdown ↔ HTML conversion (lightweight, no external deps) ----
const MarkdownUtils = {
// Convert markdown to HTML for rendering in editor
toHtml(md) {
if (!md) return '';
let html = md;
// Escape HTML entities first
html = html.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// Code blocks (``` ... ```) - must be before inline code
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => `<pre class="rte-code-block" data-lang="${lang}"><code>${code.trim()}</code></pre>`);
// Tables
html = html.replace(/^\|(.+)\|\s*\n\|[-:| ]+\|\s*\n((?:\|.+\|\s*\n?)+)/gm, (_, header, body) => {
const headers = header.split('|').map(c => c.trim()).filter(Boolean);
const rows = body.trim().split('\n').map(row => row.split('|').map(c => c.trim()).filter(Boolean));
return `<table class="rte-table"><thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead><tbody>${rows.map(r => `<tr>${r.map(c => `<td>${c}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
});
// Images (base64 or URL)
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" class="rte-img">');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
// Bold **text** or __text__
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
// Italic *text* or _text_ (not inside words)
html = html.replace(/(?<![*\w])\*([^*\n]+)\*(?![*\w])/g, '<em>$1</em>');
html = html.replace(/(?<![_\w])_([^_\n]+)_(?![_\w])/g, '<em>$1</em>');
// Inline code `code`
html = html.replace(/`([^`]+)`/g, '<code class="rte-inline-code">$1</code>');
// Headings
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// Lists - unordered
html = html.replace(/^[-*] (.+)$/gm, '<li data-list="ul">$1</li>');
// Lists - ordered
html = html.replace(/^\d+\. (.+)$/gm, '<li data-list="ol">$1</li>');
// Wrap consecutive list items
html = html.replace(/((?:<li data-list="ul">.+<\/li>\s*)+)/g, '<ul>$1</ul>');
html = html.replace(/((?:<li data-list="ol">.+<\/li>\s*)+)/g, '<ol>$1</ol>');
html = html.replace(/ data-list="[uo]l"/g, '');
// Paragraphs - wrap remaining text blocks
html = html.split('\n\n').map(block => {
block = block.trim();
if (!block) return '';
if (/^<(h[1-3]|ul|ol|pre|table|p)/.test(block)) return block;
if (block.includes('<')) return block; // Already has HTML
return `<p>${block.replace(/\n/g, '<br>')}</p>`;
}).join('\n');
return html;
},
// Convert HTML from editor back to markdown
toMarkdown(html) {
if (!html) return '';
const doc = new DOMParser().parseFromString(html, 'text/html');
return this._nodeToMd(doc.body).trim();
},
_nodeToMd(node) {
let md = '';
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
md += child.textContent;
} else if (child.nodeType === Node.ELEMENT_NODE) {
const tag = child.tagName.toLowerCase();
const inner = this._nodeToMd(child);
switch (tag) {
case 'strong':
case 'b':
md += `**${inner}**`;
break;
case 'em':
case 'i':
md += `*${inner}*`;
break;
case 'code':
if (child.parentElement?.tagName === 'PRE') md += inner;else md += `\`${inner}\``;
break;
case 'pre':
md += `\`\`\`\n${inner}\n\`\`\`\n\n`;
break;
case 'a':
md += `[${inner}](${child.getAttribute('href') || ''})`;
break;
case 'img':
md += `![${child.getAttribute('alt') || ''}](${child.getAttribute('src') || ''})`;
break;
case 'h1':
md += `# ${inner}\n\n`;
break;
case 'h2':
md += `## ${inner}\n\n`;
break;
case 'h3':
md += `### ${inner}\n\n`;
break;
case 'p':
md += `${inner}\n\n`;
break;
case 'br':
md += '\n';
break;
case 'li':
md += `- ${inner}\n`;
break;
case 'ul':
case 'ol':
md += inner + '\n';
break;
case 'table':
md += this._tableToMd(child) + '\n\n';
break;
case 'thead':
case 'tbody':
case 'tr':
case 'th':
case 'td':
md += inner;
break;
case 'div':
md += inner + '\n';
break;
default:
md += inner;
}
}
}
return md;
},
_tableToMd(table) {
const rows = Array.from(table.querySelectorAll('tr'));
if (!rows.length) return '';
const lines = [];
rows.forEach((row, i) => {
const cells = Array.from(row.querySelectorAll('th, td')).map(c => c.textContent.trim());
lines.push('| ' + cells.join(' | ') + ' |');
if (i === 0) lines.push('|' + cells.map(() => '---').join('|') + '|');
});
return lines.join('\n');
},
// Convert HTML to plain text
toPlainText(html) {
if (!html) return '';
const doc = new DOMParser().parseFromString(html, 'text/html');
return doc.body.textContent || '';
}
};
// ---- Clipboard utilities for multiple formats ----
const ClipboardUtils = {
// Copy to clipboard with multiple formats (HTML primary, plain text also)
async copyHtml(html, markdown) {
const plain = MarkdownUtils.toPlainText(html);
try {
// Modern API: write multiple formats
await navigator.clipboard.write([new ClipboardItem({
'text/html': new Blob([html], {
type: 'text/html'
}),
'text/plain': new Blob([plain], {
type: 'text/plain'
})
})]);
return true;
} catch {
// Fallback: plain text only
await navigator.clipboard.writeText(plain);
return true;
}
},
async copyMarkdown(markdown) {
await navigator.clipboard.writeText(markdown);
return true;
},
async copyPlainText(html) {
const plain = MarkdownUtils.toPlainText(html);
await navigator.clipboard.writeText(plain);
return true;
}
};
// ---- Archive Helpers ----
function getRestorePath(entries, itemId) {
// Build path of parent IDs from root to item's parent
const path = [];
function find(items, targetId, currentPath) {
for (const it of items) {
if (it.id === targetId) return currentPath;
if (it.children?.length) {
const result = find(it.children, targetId, [...currentPath, it.id]);
if (result) return result;
}
}
return null;
}
return find(entries, itemId, [])?.join('/') || '';
}
function removeItems(entries, itemIds) {
// Remove items by ID, returns removed items with their children
const removed = [];
const idsSet = new Set(itemIds);
function remove(items) {
const kept = [];
for (const it of items) {
if (idsSet.has(it.id)) {
removed.push(it);
} else {
if (it.children?.length) it.children = remove(it.children);
kept.push(it);
}
}
return kept;
}
const newEntries = remove(entries);
return {
entries: newEntries,
removed
};
}
function insertAtPath(entries, item, restorePath) {
// Insert item at the path specified by parent IDs
if (!restorePath) {
entries.push(item);
return true;
}
const pathIds = restorePath.split('/').filter(Boolean);
let current = entries;
for (const pid of pathIds) {
const parent = current.find(it => it.id === pid);
if (!parent) {
// Parent not found - insert at root with warning
entries.push(item);
return false;
}
if (!parent.children) parent.children = [];
current = parent.children;
}
current.push(item);
return true;
}
Object.assign(window, {
STATUSES,
STATUS_BY_KEY,
STATUS_ORDER,
PRIORITIES,
parseDate,
daysBetween,
isOverdue,
dueRelative,
fmtTimestamp,
fmtBytes,
fmtRelative,
fmtShortDate,
walkTree,
findItem,
allTags,
countAll,
countByStatus,
filterTree,
Icon,
StatusIcon,
ColorStatusIcon,
STATUS_COLORS,
STATUS_EMOJI,
StatusStyleContext,
PROGRESS_STEPS,
snapProgress,
progressFor,
shouldShowProgress,
cycleProgress,
ProgressGauge,
MarkdownUtils,
ClipboardUtils,
getRestorePath,
removeItems,
insertAtPath
});
</script>
<script>
// Storage layer: Parser, ApiBackend, DirectBackend, Storage detection, SyncPoller.
// Compatible with web/index.html and server/server.py file format.
//
// File format (backlog.md):
// # Backlog
// <!-- SECTION: ENTRIES -->
// - [ ] [P1] Title *(due: ..., reason: ..., tags: tag1 tag2, progress: 50)*
// - [x] [P0] Child *(progress: 100)*
// <!-- SECTION: HISTORY -->
// | Timestamp | Item ID | Action | Details |
// <!-- SECTION: INTEGRITY -->
// <!-- saved: ... | checksum: sha256:... | entries: N | history: N -->
// ---- Parser ----
const Parser = {
async parse(text) {
if (!text || !text.trim()) {
return {
entries: [],
history: [],
meta: null,
checksumOk: false
};
}
const entStart = text.indexOf('<!-- SECTION: ENTRIES -->');
const histStart = text.indexOf('<!-- SECTION: HISTORY -->');
const intStart = text.indexOf('<!-- SECTION: INTEGRITY -->');
const entriesText = entStart !== -1 ? text.slice(entStart, histStart !== -1 ? histStart : intStart !== -1 ? intStart : text.length) : text;
const historyText = histStart !== -1 ? text.slice(histStart, intStart !== -1 ? intStart : text.length) : '';
// Parse integrity marker
let meta = null;
let checksumOk = false;
if (intStart !== -1) {
const m = text.slice(intStart).match(/saved:\s*([^|]+?)\s*\|\s*checksum:\s*([^|]+?)\s*\|\s*entries:\s*(\d+)\s*\|\s*history:\s*(\d+)/);
if (m) {
meta = {
saved: m[1].trim(),
checksum: m[2].trim(),
entryCount: +m[3],
historyCount: +m[4]
};
// Strip section-marker prefix + surrounding whitespace to get raw content,
// matching server.py's compute_checksum() which uses raw (marker-free) payload.
const rawEnt = entriesText.slice('<!-- SECTION: ENTRIES -->'.length).trim();
const rawHist = historyText.slice('<!-- SECTION: HISTORY -->'.length).trim();
const payload = rawEnt + '\n' + rawHist;
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload));
const hash = 'sha256:' + Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
checksumOk = hash === meta.checksum;
}
}
// Parse history table
const history = [];
for (const line of historyText.split('\n')) {
if (!line.trim().startsWith('|')) continue;
const parts = line.split('|').map(s => s.trim()).filter(Boolean);
if (parts.length >= 4 && parts[0] !== 'Timestamp' && !parts[0].match(/^-+$/)) {
history.push({
timestamp: parts[0],
itemId: parts[1],
action: parts[2],
details: parts[3]
});
}
}
// Parse entries (indented markdown list)
const GLYPH_MAP = {
' ': 'open',
'x': 'done',
'!': 'blocked',
'>': 'postponed',
'/': 'in-progress',
'-': 'cancelled'
};
let idSeq = Date.now();
const uid = () => 'i-' + (++idSeq).toString(36);
const entries = [];
const stack = [{
children: entries,
depth: -1,
level: 0,
item: null
}];
const lines = entriesText.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for body line (starts with whitespace + "> ")
const bodyMatch = line.match(/^(\s*)>\s?(.*)$/);
if (bodyMatch) {
// Body line - attach to the most recent item in the stack
// Body indent is one level deeper than its parent item (e.g., item at depth 0 has body at depth 1)
const bodyIndent = bodyMatch[1].length / 2;
const bodyLine = bodyMatch[2];
// Find the item whose depth is bodyIndent - 1 (body is indented under its item)
for (let j = stack.length - 1; j >= 0; j--) {
if (stack[j].item && stack[j].depth === bodyIndent - 1) {
if (!stack[j].item.body) stack[j].item.body = '';else stack[j].item.body += '\n';
stack[j].item.body += bodyLine;
break;
}
}
continue;
}
const m = line.match(/^(\s*)[-*]\s*\[([ x!>/\-])\]\s*(.*)$/);
if (!m) continue;
const depth = m[1].length / 2;
const status = GLYPH_MAP[m[2]] || 'open';
const raw = m[3].trim();
let title = raw,
due = null,
reason = null,
tags = [],
priority = 'P1',
progress = 0;
let archived = null,
restorePath = null;
const metaM = raw.match(/^(.*?)\s*\*\((.*)\)\*\s*$/);
if (metaM) {
title = metaM[1].trim();
for (const p of metaM[2].split(',').map(s => s.trim())) {
if (p.startsWith('due:')) due = p.slice(4).trim();else if (p.startsWith('reason:')) reason = p.slice(7).trim();else if (p.startsWith('tags:')) tags = p.slice(5).trim().split(/\s+/).filter(Boolean);else if (p.startsWith('priority:')) priority = p.slice(9).trim().toUpperCase();else if (p.startsWith('progress:')) progress = parseInt(p.slice(9).trim(), 10) || 0;else if (p.startsWith('archived:')) archived = p.slice(9).trim();else if (p.startsWith('restore-path:')) restorePath = p.slice(13).trim();
}
}
// Priority prefix [P0] embedded in title
const pm = title.match(/^\[(P\d)\]\s*/);
if (pm) {
priority = pm[1];
title = title.slice(pm[0].length);
}
while (stack.length > 1 && stack[stack.length - 1].depth >= depth) stack.pop();
const parentLevel = stack[stack.length - 1].level;
const item = {
id: uid(),
level: parentLevel + 1,
title,
status,
priority,
due,
reason,
tags,
progress: status === 'done' ? 100 : progress,
collapsed: false,
children: [],
body: null,
archived: archived || null,
restorePath: restorePath || null
};
stack[stack.length - 1].children.push(item);
stack.push({
children: item.children,
depth,
level: item.level,
item
});
}
return {
entries,
history,
meta,
checksumOk
};
},
async serialize(data) {
const entriesText = this._serializeEntries(data.entries);
const historyText = this._serializeHistory(data.history);
const payload = entriesText + '\n' + historyText;
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload));
const hash = 'sha256:' + Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
const saved = new Date().toISOString().slice(0, 19) + 'Z';
const nEntries = this._countItems(data.entries);
const nHistory = data.history.length;
const marker = `<!-- saved: ${saved} | checksum: ${hash} | entries: ${nEntries} | history: ${nHistory} -->`;
return `# Backlog\n\n<!-- SECTION: ENTRIES -->\n\n${entriesText}\n\n<!-- SECTION: HISTORY -->\n\n${historyText}\n\n<!-- SECTION: INTEGRITY -->\n\n${marker}\n`;
},
_serializeEntries(items, depth = 0) {
const indent = ' '.repeat(depth);
const GLYPH = {
open: ' ',
'in-progress': '/',
blocked: '!',
postponed: '>',
done: 'x',
cancelled: '-'
};
const lines = [];
for (const it of items) {
const meta = [];
if (it.due) meta.push(`due: ${it.due}`);
if (it.reason) meta.push(`reason: ${it.reason}`);
if (it.tags?.length) meta.push(`tags: ${it.tags.join(' ')}`);
if (it.priority && it.priority !== 'P1') meta.push(`priority: ${it.priority}`);
if ((it.progress ?? 0) > 0) meta.push(`progress: ${it.progress}`);
if (it.archived) meta.push(`archived: ${it.archived}`);
if (it.restorePath) meta.push(`restore-path: ${it.restorePath}`);
const metaStr = meta.length ? ` *(${meta.join(', ')})*` : '';
const prefix = it.priority ? `[${it.priority}] ` : '';
lines.push(`${indent}- [${GLYPH[it.status] || ' '}] ${prefix}${it.title}${metaStr}`);
// Serialize body as indented blockquote lines (one level deeper than item)
if (it.body) {
const bodyIndent = ' '.repeat(depth + 1);
for (const bodyLine of it.body.split('\n')) {
lines.push(`${bodyIndent}> ${bodyLine}`);
}
}
if (it.children?.length) {
lines.push(...this._serializeEntries(it.children, depth + 1).split('\n'));
}
}
return lines.join('\n');
},
_serializeHistory(rows) {
const lines = ['| Timestamp | Item ID | Action | Details |', '|-----------|---------|--------|---------|'];
for (const r of rows) lines.push(`| ${r.timestamp} | ${r.itemId} | ${r.action} | ${r.details} |`);
return lines.join('\n');
},
_countItems(items) {
let n = 0;
for (const it of items) {
n++;
n += this._countItems(it.children || []);
}
return n;
}
};
// ---- API Backend (python server.py) ----
const ApiBackend = {
async detect() {
if (location.protocol === 'file:') return false; // no server in file:// context
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 800);
try {
const r = await fetch('/api/health', {
signal: ctrl.signal
});
clearTimeout(t);
return r.ok;
} catch {
clearTimeout(t);
return false;
}
},
async load() {
const r = await fetch('/api/backlog');
if (!r.ok) throw new Error('API load failed: ' + r.status);
return r.json(); // { content, checksum }
},
async save(content) {
const r = await fetch('/api/backlog', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
content
})
});
if (!r.ok) throw new Error('API save failed: ' + r.status);
return r.json(); // { ok, checksum, saved }
},
async listBackups() {
try {
const r = await fetch('/api/backups');
if (!r.ok) return [];
return (await r.json()).backups || [];
} catch {
return [];
}
},
async restoreBackup(name) {
const r = await fetch('/api/backups/restore', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name
})
});
return r.ok ? r.json() : {
ok: false
};
},
async getHealthInfo() {
try {
const r = await fetch('/api/health');
if (!r.ok) return {};
const j = await r.json();
return {
masterSize: j.masterSize || 0,
statsSize: 0,
masterPath: j.masterPath || '',
backupsPath: j.backupsPath || '',
archiveExists: j.archiveExists || false,
archiveSize: j.archiveSize || 0,
archivePath: j.archivePath || ''
};
} catch {
return {};
}
},
async loadArchive(project = null) {
const url = project ? `/api/projects/${encodeURIComponent(project)}/archive` : '/api/archive';
const r = await fetch(url);
if (!r.ok) throw new Error('API archive load failed: ' + r.status);
return r.json(); // { content, checksum, exists }
},
async saveArchive(backlogContent, archiveContent, project = null) {
const url = project ? `/api/projects/${encodeURIComponent(project)}/archive` : '/api/archive';
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
backlog: backlogContent,
archive: archiveContent
})
});
if (!r.ok) throw new Error('API archive save failed: ' + r.status);
return r.json();
},
async restoreFromArchive(backlogContent, archiveContent, project = null) {
const url = project ? `/api/projects/${encodeURIComponent(project)}/archive/restore` : '/api/archive/restore';
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
backlog: backlogContent,
archive: archiveContent
})
});
if (!r.ok) throw new Error('API archive restore failed: ' + r.status);
return r.json();
},
// ---- Project-scoped methods (multi-project server API) ----
async listProjects() {
const r = await fetch('/api/projects');
if (!r.ok) throw new Error('API projects failed: ' + r.status);
return (await r.json()).projects || [];
},
async registerProject(path, name = null) {
const r = await fetch('/api/projects', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path,
name
})
});
return r.json(); // { ok, name, path }
},
async unregisterProject(name, deleteFile = false) {
const r = await fetch(`/api/projects/${encodeURIComponent(name)}${deleteFile ? '?delete_file=true' : ''}`, {
method: 'DELETE'
});
return r.json(); // { ok, name, deleted, warning? }
},
async renameProject(name, newName) {
const r = await fetch(`/api/projects/${encodeURIComponent(name)}/rename`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
newName
})
});
return r.json(); // { ok, name }
},
async loadProject(name) {
const r = await fetch(`/api/projects/${encodeURIComponent(name)}/backlog`);
if (!r.ok) throw new Error('API project load failed: ' + r.status);
return r.json(); // { content, checksum }
},
async saveProject(name, content) {
const r = await fetch(`/api/projects/${encodeURIComponent(name)}/backlog`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
content
})
});
if (!r.ok) throw new Error('API project save failed: ' + r.status);
return r.json(); // { ok, checksum, saved }
},
async loadProjectBackups(name) {
try {
const r = await fetch(`/api/projects/${encodeURIComponent(name)}/backups`);
if (!r.ok) return [];
return (await r.json()).backups || [];
} catch {
return [];
}
},
async loadAllProjects() {
const list = await this.listProjects();
return Promise.all(list.map(async p => {
if (p.missing) {
return {
id: p.name,
name: p.name,
path: p.path,
entries: [],
history: [],
meta: null,
checksumOk: true,
missing: true
};
}
try {
const raw = await this.loadProject(p.name);
const parsed = await Parser.parse(raw.content);
return {
id: p.name,
name: p.name,
path: p.path,
entries: parsed.entries,
history: parsed.history,
meta: parsed.meta,
checksumOk: parsed.checksumOk
};
} catch (e) {
return {
id: p.name,
name: p.name,
path: p.path,
entries: [],
history: [],
meta: null,
checksumOk: false,
error: String(e.message || e)
};
}
}));
}
};
// ---- Direct File Backend (File System Access API) ----
const DirectBackend = {
dirHandle: null,
_DB_NAME: 'pb-storage-v2',
_STORE: 'handles',
_KEY: 'root',
async detect() {
return typeof window.showDirectoryPicker === 'function';
},
// Silent reconnect — no user gesture. Returns true if the stored handle
// is still accessible and permission is already granted.
async tryAutoConnect() {
this.dirHandle = await this._loadHandle();
if (!this.dirHandle) return false;
try {
return (await this.dirHandle.queryPermission({
mode: 'readwrite'
})) === 'granted';
} catch {
return false;
}
},
// Explicit connect — requires a user gesture (button click).
// Shows the directory picker on first use; re-prompts for permission on subsequent uses.
async connect() {
let handle = await this._loadHandle();
if (!handle) {
handle = await window.showDirectoryPicker({
mode: 'readwrite'
});
await this._saveHandle(handle);
}
const perm = await handle.requestPermission({
mode: 'readwrite'
});
if (perm !== 'granted') throw new Error('Permission denied');
this.dirHandle = handle;
},
_loadHandle() {
return new Promise(res => {
const req = indexedDB.open(this._DB_NAME, 1);
req.onupgradeneeded = e => e.target.result.createObjectStore(this._STORE);
req.onsuccess = e => {
const get = e.target.result.transaction(this._STORE, 'readonly').objectStore(this._STORE).get(this._KEY);
get.onsuccess = () => res(get.result || null);
get.onerror = () => res(null);
};
req.onerror = () => res(null);
});
},
_saveHandle(handle) {
return new Promise((res, rej) => {
const req = indexedDB.open(this._DB_NAME, 1);
req.onsuccess = e => {
const tx = e.target.result.transaction(this._STORE, 'readwrite');
tx.objectStore(this._STORE).put(handle, this._KEY);
tx.oncomplete = () => res();
tx.onerror = () => rej(tx.error);
};
});
},
async _readFile(name) {
try {
const h = await this.dirHandle.getFileHandle(name);
return (await h.getFile()).text();
} catch {
return null;
}
},
async _writeFile(name, content, dirHandle) {
const dir = dirHandle || this.dirHandle;
const h = await dir.getFileHandle(name, {
create: true
});
const w = await h.createWritable();
await w.write(content);
await w.close();
},
async load() {
const content = await this._readFile('backlog.md');
return {
content: content || '',
checksum: ''
};
},
async save(content) {
await this._writeFile('backlog.md', content);
// Create backup
try {
const ts = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
const dir = await this.dirHandle.getDirectoryHandle('backups', {
create: true
});
await this._writeFile(`backlog_${ts}.md`, content, dir);
} catch {/* backup failure is non-fatal */}
return {
ok: true
};
},
async listBackups() {
try {
const dir = await this.dirHandle.getDirectoryHandle('backups', {
create: true
});
const backups = [];
for await (const [name, h] of dir.entries()) {
if (!name.startsWith('backlog_') || !name.endsWith('.md')) continue;
const f = await h.getFile();
const text = await f.text();
backups.push({
name,
size: f.size,
timestamp: new Date(f.lastModified).toISOString(),
valid: text.includes('<!-- SECTION: INTEGRITY -->')
});
}
return backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
} catch {
return [];
}
},
async restoreBackup(name) {
try {
const dir = await this.dirHandle.getDirectoryHandle('backups');
const h = await dir.getFileHandle(name);
const text = await (await h.getFile()).text();
await this._writeFile('backlog.md', text);
return {
ok: true
};
} catch (e) {
return {
ok: false,
error: e.message
};
}
},
async getHealthInfo() {
try {
const f = await (await this.dirHandle.getFileHandle('backlog.md')).getFile();
let archiveExists = false,
archiveSize = 0;
try {
const af = await (await this.dirHandle.getFileHandle('archive.md')).getFile();
archiveExists = true;
archiveSize = af.size;
} catch {}
return {
masterSize: f.size,
statsSize: 0,
dirName: this.dirHandle?.name ?? null,
archiveExists,
archiveSize
};
} catch {
return {
masterSize: 0,
statsSize: 0,
dirName: this.dirHandle?.name ?? null,
archiveExists: false,
archiveSize: 0
};
}
},
async loadArchive() {
const content = await this._readFile('archive.md');
return {
content: content || '',
checksum: '',
exists: !!content
};
},
async saveArchive(backlogContent, archiveContent) {
await this._writeFile('backlog.md', backlogContent);
await this._writeFile('archive.md', archiveContent);
return {
ok: true
};
},
async restoreFromArchive(backlogContent, archiveContent) {
await this._writeFile('backlog.md', backlogContent);
await this._writeFile('archive.md', archiveContent);
return {
ok: true
};
}
};
// ---- Browser Storage Backend (IndexedDB + Storage Persistence API) ----
// Fallback for browsers without File System Access API (Firefox, Safari)
const BrowserStorageBackend = {
_DB_NAME: 'pb-browser-storage',
_STORE: 'data',
_CONTENT_KEY: 'backlog',
_BACKUPS_KEY: 'backups',
_MAX_BACKUPS: 10,
_persistenceRequested: false,
_persistenceGranted: null,
async detect() {
return typeof indexedDB !== 'undefined';
},
_openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(this._DB_NAME, 1);
req.onupgradeneeded = e => e.target.result.createObjectStore(this._STORE);
req.onsuccess = e => resolve(e.target.result);
req.onerror = () => reject(req.error);
});
},
async _get(key) {
const db = await this._openDB();
return new Promise(resolve => {
const tx = db.transaction(this._STORE, 'readonly');
const get = tx.objectStore(this._STORE).get(key);
get.onsuccess = () => resolve(get.result);
get.onerror = () => resolve(null);
});
},
async _set(key, value) {
const db = await this._openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this._STORE, 'readwrite');
tx.objectStore(this._STORE).put(value, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
},
async load() {
try {
const content = (await this._get(this._CONTENT_KEY)) || '';
return {
content,
checksum: ''
};
} catch {
return {
content: '',
checksum: ''
};
}
},
async save(content) {
// Request persistence on first save (triggered by user action = valid gesture)
if (!this._persistenceRequested) {
this._persistenceRequested = true;
this._requestPersistence();
}
try {
// Create backup before saving
const oldContent = await this._get(this._CONTENT_KEY);
if (oldContent) {
await this._addBackup(oldContent);
}
await this._set(this._CONTENT_KEY, content);
return {
ok: true
};
} catch (e) {
return {
ok: false,
error: e.message
};
}
},
async _requestPersistence() {
if (navigator.storage?.persist) {
try {
const alreadyPersisted = await navigator.storage.persisted();
if (alreadyPersisted) {
this._persistenceGranted = true;
return;
}
// This will prompt the user in Firefox
this._persistenceGranted = await navigator.storage.persist();
} catch {
this._persistenceGranted = false;
}
}
},
async _addBackup(content) {
try {
const backups = (await this._get(this._BACKUPS_KEY)) || [];
const ts = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
backups.unshift({
name: `backlog_${ts}.md`,
content,
timestamp: new Date().toISOString(),
size: new TextEncoder().encode(content).length
});
// Keep only last N backups
if (backups.length > this._MAX_BACKUPS) {
backups.length = this._MAX_BACKUPS;
}
await this._set(this._BACKUPS_KEY, backups);
} catch {/* backup failure is non-fatal */}
},
async listBackups() {
try {
const backups = (await this._get(this._BACKUPS_KEY)) || [];
return backups.map(b => ({
name: b.name,
size: b.size,
timestamp: b.timestamp,
valid: b.content?.includes('<!-- SECTION: INTEGRITY -->')
}));
} catch {
return [];
}
},
async restoreBackup(name) {
try {
const backups = (await this._get(this._BACKUPS_KEY)) || [];
const backup = backups.find(b => b.name === name);
if (!backup) return {
ok: false,
error: 'Backup not found'
};
await this._set(this._CONTENT_KEY, backup.content);
return {
ok: true
};
} catch (e) {
return {
ok: false,
error: e.message
};
}
},
async getHealthInfo() {
try {
const content = (await this._get(this._CONTENT_KEY)) || '';
const archiveContent = (await this._get('archive')) || '';
const estimate = (await navigator.storage?.estimate?.()) || {};
const persisted = (await navigator.storage?.persisted?.()) || false;
return {
masterSize: new TextEncoder().encode(content).length,
statsSize: 0,
browserStorage: true,
persisted,
quota: estimate.quota,
usage: estimate.usage,
archiveExists: !!archiveContent,
archiveSize: new TextEncoder().encode(archiveContent).length
};
} catch {
return {
masterSize: 0,
statsSize: 0,
browserStorage: true,
persisted: false,
archiveExists: false,
archiveSize: 0
};
}
},
async loadArchive() {
try {
const content = (await this._get('archive')) || '';
return {
content,
checksum: '',
exists: !!content
};
} catch {
return {
content: '',
checksum: '',
exists: false
};
}
},
async saveArchive(backlogContent, archiveContent) {
try {
await this._set(this._CONTENT_KEY, backlogContent);
await this._set('archive', archiveContent);
return {
ok: true
};
} catch (e) {
return {
ok: false,
error: e.message
};
}
},
async restoreFromArchive(backlogContent, archiveContent) {
try {
await this._set(this._CONTENT_KEY, backlogContent);
await this._set('archive', archiveContent);
return {
ok: true
};
} catch (e) {
return {
ok: false,
error: e.message
};
}
}
};
// ---- Storage — detect backend and delegate ----
const Storage = {
backend: null,
mode: 'local',
// 'api' | 'direct' | 'local'
async detect() {
if (await ApiBackend.detect()) {
this.backend = ApiBackend;
this.mode = 'api';
return 'api';
}
if (await DirectBackend.detect()) {
this.backend = DirectBackend;
this.mode = 'direct';
return 'direct';
}
if (await BrowserStorageBackend.detect()) {
this.backend = BrowserStorageBackend;
this.mode = 'browser';
return 'browser';
}
this.mode = 'local';
return 'local';
},
// Try to reconnect silently (no user gesture). Returns true if connected.
// For API mode this is always true; for Direct it checks the stored handle.
async tryAutoConnect() {
if (this.backend === DirectBackend) return DirectBackend.tryAutoConnect();
return true;
},
// Explicit connect — call from a user-gesture handler (button click).
async connect() {
if (this.backend === DirectBackend) await DirectBackend.connect();
},
// True only when a backend is detected AND fully initialised (dirHandle set for Direct).
isConnected() {
if (!this.backend) return false;
if (this.backend === DirectBackend) return !!DirectBackend.dirHandle;
return true;
},
async load() {
return this.backend?.load() ?? null;
},
async save(content) {
return this.backend?.save(content) ?? null;
},
async listBackups() {
return this.backend?.listBackups() ?? [];
},
async restoreBackup(name) {
return this.backend?.restoreBackup(name) ?? {
ok: false
};
},
async getHealthInfo() {
return this.backend?.getHealthInfo?.() ?? {};
},
async loadArchive() {
return this.backend?.loadArchive?.() ?? {
content: '',
exists: false
};
},
async saveArchive(backlog, archive) {
return this.backend?.saveArchive?.(backlog, archive) ?? {
ok: false
};
},
async restoreFromArchive(backlog, archive) {
return this.backend?.restoreFromArchive?.(backlog, archive) ?? {
ok: false
};
}
};
// ---- SyncPoller — detect external file changes (5s interval) ----
const SyncPoller = {
_timer: null,
lastChecksum: '',
lastChecksums: {},
// multi-project mode: { projectName: checksum }
_onExternalChange: null,
_isDirty: null,
_multi: false,
start({
onExternalChange,
isDirty
}) {
this._onExternalChange = onExternalChange;
this._isDirty = isDirty;
this._multi = false;
this._timer = setInterval(() => this._tick(), 5000);
},
// Multi-project mode (api only): poll GET /api/projects — ONE round trip for
// all checksums. A changed project is reloaded individually; a changed
// project SET (registered/renamed/unregistered externally) triggers a full
// reload via the 'set-changed' callback.
startMulti({
onExternalChange,
isDirty
}) {
this._onExternalChange = onExternalChange;
this._isDirty = isDirty;
this._multi = true;
this.lastChecksums = {};
this.syncChecksums(); // baseline; a baseline never fires callbacks
this._timer = setInterval(() => this._tickMulti(), 5000);
},
stop() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
this._multi = false;
},
// Re-baseline checksums after our own writes that bypass saveProject (e.g.
// per-project archive saves), so the next poll does not mistake them for
// external changes.
async syncChecksums() {
if (!this._multi) return;
try {
const projects = await ApiBackend.listProjects();
for (const p of projects) this.lastChecksums[p.name] = p.checksum || '';
} catch {/* silent on poll errors */}
},
async _tick() {
if (Storage.mode === 'local' || !Storage.backend) return;
try {
const raw = await Storage.load();
if (!raw?.content) return;
const parsed = await Parser.parse(raw.content);
const cs = parsed.meta?.checksum || '';
if (!cs || cs === this.lastChecksum) return;
this.lastChecksum = cs;
if (this._isDirty?.()) {
this._onExternalChange?.('warn', null);
} else {
this._onExternalChange?.('reload', parsed);
}
} catch {/* silent on poll errors */}
},
async _tickMulti() {
if (!this._multi) return;
let projects;
try {
projects = await ApiBackend.listProjects();
} catch {
return;
}
if (!projects?.length) return;
const prev = this.lastChecksums;
const known = Object.keys(prev);
const names = projects.map(p => p.name);
if (known.length > 0 && (names.length !== known.length || names.some(n => !known.includes(n)))) {
if (this._isDirty?.()) {
// Leave the baseline stale so the next tick retries after the
// pending save lands — reloading now would clobber unsaved edits.
this._onExternalChange?.('warn', null);
} else {
this.lastChecksums = Object.fromEntries(projects.map(p => [p.name, p.checksum || '']));
this._onExternalChange?.('set-changed', null);
}
return;
}
for (const p of projects) {
const cs = p.checksum || '';
if (!(p.name in prev)) {
prev[p.name] = cs;
continue;
} // baseline a latecomer silently
if (!cs || cs === prev[p.name]) continue;
prev[p.name] = cs;
if (this._isDirty?.()) {
// Checksum already stored — the warn fires exactly once per change.
this._onExternalChange?.('warn', null);
} else {
try {
const raw = await ApiBackend.loadProject(p.name);
const parsed = await Parser.parse(raw.content);
walkTree(parsed.entries, e => {
e._projectId = p.name;
});
for (const h of parsed.history) h._projectId = p.name;
this._onExternalChange?.('project-reloaded', {
name: p.name,
parsed
});
} catch {/* unreadable project — retry on the next checksum change */}
}
}
}
};
// ---- buildDataFromStorage — assemble the full App data object from parsed content ----
async function buildDataFromStorage(parsed, backups, storageMode, sizeInfo = {}) {
const {
entries,
history,
meta,
checksumOk
} = parsed;
// Assign level field (derived from tree depth, not stored in file)
function setLevels(items, level) {
for (const it of items) {
it.level = level;
if (it.children?.length) setLevels(it.children, level + 1);
}
}
setLevels(entries, 1);
// Migrate: ensure progress field is always a number
const progressDefaults = {
done: 100,
cancelled: 0,
'in-progress': 50,
blocked: 25,
postponed: 25,
open: 0
};
walkTree(entries, it => {
if (typeof it.progress !== 'number') it.progress = progressDefaults[it.status] ?? 0;else if (it.status === 'done') it.progress = 100;
});
// Status counts
const statusMix = {
open: 0,
'in-progress': 0,
blocked: 0,
postponed: 0,
done: 0,
cancelled: 0
};
walkTree(entries, it => {
statusMix[it.status] = (statusMix[it.status] || 0) + 1;
});
// Most active item by history event count
const actCounts = {};
walkTree(entries, it => {
actCounts[it.id] = 0;
});
for (const h of history) {
if (h.itemId in actCounts) actCounts[h.itemId]++;
}
const topId = Object.entries(actCounts).sort((a, b) => b[1] - a[1])[0]?.[0];
const topItem = topId ? findItem(entries, topId) : null;
const mostActiveProject = topItem?.title || entries[0]?.title || '—';
// Recent activity (last 7 days)
const sevenAgo = new Date(Date.now() - 7 * 86400000).toISOString();
const recentH = history.filter(h => h.timestamp >= sevenAgo);
const createdThisWeek = recentH.filter(h => h.action === 'item_created').length;
const completedThisWeek = recentH.filter(h => h.action === 'status_changed' && h.details.includes('→ done')).length;
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const completionByDay = dayNames.map(day => ({
day,
count: 0
}));
const createdByDay = dayNames.map(day => ({
day,
count: 0
}));
for (const h of recentH) {
const dow = (new Date(h.timestamp).getDay() + 6) % 7; // 0=Mon
if (h.action === 'status_changed' && h.details.includes('→ done')) completionByDay[dow].count++;
if (h.action === 'item_created') createdByDay[dow].count++;
}
// Average days spent in-progress: pair "→ in-progress" with next "in-progress →" per item.
const ipStart = {}; // itemId → start timestamp ms
const ipDurations = [];
for (const h of [...history].reverse()) {
// chronological order
if (h.action !== 'status_changed') continue;
if (h.details.includes('→ in-progress')) ipStart[h.itemId] = new Date(h.timestamp).getTime();else if (h.details.startsWith('in-progress →') && ipStart[h.itemId]) {
const days = (new Date(h.timestamp).getTime() - ipStart[h.itemId]) / 86400000;
if (days >= 0 && days < 365) ipDurations.push(days);
delete ipStart[h.itemId];
}
}
const avgInProgressDays = ipDurations.length > 0 ? +(ipDurations.reduce((a, b) => a + b, 0) / ipDurations.length).toFixed(1) : null;
const historyOldest = history.length > 0 ? history[history.length - 1].timestamp : null;
const backupDirSize = backups.reduce((s, b) => s + (b.size || 0), 0);
const historySize = new TextEncoder().encode(JSON.stringify(history)).length;
const modeLabel = storageMode === 'api' ? 'API server' : storageMode === 'direct' ? 'Direct (File System API)' : storageMode === 'browser' ? 'Browser storage (IndexedDB)' : 'localStorage only';
return {
entries,
history,
meta: meta || {
saved: null,
checksum: '—',
entryCount: countAll(entries),
historyCount: history.length
},
health: {
integrityOk: checksumOk,
lastSave: meta?.saved || null,
lastBackup: backups[0]?.timestamp || null,
masterSize: sizeInfo.masterSize || 0,
backupDirSize,
backupCount: backups.length,
statsSize: sizeInfo.statsSize || 0,
historySize,
historyOldest,
mode: modeLabel,
masterPath: sizeInfo.masterPath || (sizeInfo.dirName ? `${sizeInfo.dirName}/backlog.md` : null),
backupsPath: sizeInfo.backupsPath || (sizeInfo.dirName ? `${sizeInfo.dirName}/backups/` : null)
},
stats: {
createdThisWeek,
completedThisWeek,
avgInProgressDays,
mostActiveProject,
completionByDay,
createdByDay,
statusMix
},
backups
};
}
Object.assign(window, {
Parser,
ApiBackend,
DirectBackend,
BrowserStorageBackend,
Storage,
SyncPoller,
buildDataFromStorage
});
</script>
<script>
// tweaks-panel.jsx
// Reusable Tweaks shell + form-control helpers.
//
// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode,
// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so
// individual prototypes don't re-roll it. Ships a consistent set of controls so you
// don't hand-draw <input type="range">, segmented radios, steppers, etc.
//
// Usage (in an HTML file that loads React + Babel):
//
// const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
// "primaryColor": "#D97757",
// "fontSize": 16,
// "density": "regular",
// "dark": false
// }/*EDITMODE-END*/;
//
// function App() {
// const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
// return (
// <div style={{ fontSize: t.fontSize, color: t.primaryColor }}>
// Hello
// <TweaksPanel>
// <TweakSection label="Typography" />
// <TweakSlider label="Font size" value={t.fontSize} min={10} max={32} unit="px"
// onChange={(v) => setTweak('fontSize', v)} />
// <TweakRadio label="Density" value={t.density}
// options={['compact', 'regular', 'comfy']}
// onChange={(v) => setTweak('density', v)} />
// <TweakSection label="Theme" />
// <TweakColor label="Primary" value={t.primaryColor}
// onChange={(v) => setTweak('primaryColor', v)} />
// <TweakToggle label="Dark mode" value={t.dark}
// onChange={(v) => setTweak('dark', v)} />
// </TweaksPanel>
// </div>
// );
// }
//
// ─────────────────────────────────────────────────────────────────────────────
const __TWEAKS_STYLE = `
.twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
max-height:calc(100vh - 32px);display:flex;flex-direction:column;
background:rgba(250,249,247,.78);color:#29261b;
-webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
border:.5px solid rgba(255,255,255,.6);border-radius:14px;
box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
.twk-hd{display:flex;align-items:center;justify-content:space-between;
padding:10px 8px 10px 14px;cursor:move;user-select:none}
.twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
.twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
.twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
.twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
overflow-y:auto;overflow-x:hidden;min-height:0;
scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
.twk-body::-webkit-scrollbar{width:8px}
.twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
.twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
border:2px solid transparent;background-clip:content-box}
.twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
border:2px solid transparent;background-clip:content-box}
.twk-row{display:flex;flex-direction:column;gap:5px}
.twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
.twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
color:rgba(41,38,27,.72)}
.twk-lbl>span:first-child{font-weight:500}
.twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}
.twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
color:rgba(41,38,27,.45);padding:10px 0 0}
.twk-sect:first-child{padding-top:0}
.twk-field{appearance:none;width:100%;height:26px;padding:0 8px;
border:.5px solid rgba(0,0,0,.1);border-radius:7px;
background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
.twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
select.twk-field{padding-right:22px;
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='rgba(0,0,0,.5)' d='M0 0h10L5 6z'/></svg>");
background-repeat:no-repeat;background-position:right 8px center}
.twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
border-radius:999px;background:rgba(0,0,0,.12);outline:none}
.twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
width:14px;height:14px;border-radius:50%;background:#fff;
border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
.twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
.twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
background:rgba(0,0,0,.06);user-select:none}
.twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
.twk-seg.dragging .twk-seg-thumb{transition:none}
.twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
overflow-wrap:anywhere}
.twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
.twk-toggle[data-on="1"]{background:#34c759}
.twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
.twk-toggle[data-on="1"] i{transform:translateX(14px)}
.twk-num{display:flex;align-items:center;height:26px;padding:0 0 0 8px;
border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
.twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
user-select:none;padding-right:8px}
.twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
outline:none;color:inherit;-moz-appearance:textfield}
.twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
-webkit-appearance:none;margin:0}
.twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}
.twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
.twk-btn:hover{background:rgba(0,0,0,.88)}
.twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
.twk-btn.secondary:hover{background:rgba(0,0,0,.1)}
.twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
background:transparent;flex-shrink:0}
.twk-swatch::-webkit-color-swatch-wrapper{padding:0}
.twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
.twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}
`;
// ── useTweaks ───────────────────────────────────────────────────────────────
// Single source of truth for tweak values. setTweak persists via the host
// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk).
function useTweaks(defaults) {
const [values, setValues] = React.useState(defaults);
// Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a
// useState-style call doesn't write a "[object Object]" key into the persisted
// JSON block.
const setTweak = React.useCallback((keyOrEdits, val) => {
const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null ? keyOrEdits : {
[keyOrEdits]: val
};
setValues(prev => ({
...prev,
...edits
}));
window.parent.postMessage({
type: '__edit_mode_set_keys',
edits
}, '*');
}, []);
return [values, setTweak];
}
// ── TweaksPanel ─────────────────────────────────────────────────────────────
// Floating shell. Registers the protocol listener BEFORE announcing
// availability — if the announce ran first, the host's activate could land
// before our handler exists and the toolbar toggle would silently no-op.
// The close button posts __edit_mode_dismissed so the host's toolbar toggle
// flips off in lockstep; the host echoes __deactivate_edit_mode back which
// is what actually hides the panel.
function TweaksPanel({
title = 'Tweaks',
children
}) {
const [open, setOpen] = React.useState(false);
const dragRef = React.useRef(null);
const offsetRef = React.useRef({
x: 16,
y: 16
});
const PAD = 16;
const clampToViewport = React.useCallback(() => {
const panel = dragRef.current;
if (!panel) return;
const w = panel.offsetWidth,
h = panel.offsetHeight;
const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
offsetRef.current = {
x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y))
};
panel.style.right = offsetRef.current.x + 'px';
panel.style.bottom = offsetRef.current.y + 'px';
}, []);
React.useEffect(() => {
if (!open) return;
clampToViewport();
if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', clampToViewport);
return () => window.removeEventListener('resize', clampToViewport);
}
const ro = new ResizeObserver(clampToViewport);
ro.observe(document.documentElement);
return () => ro.disconnect();
}, [open, clampToViewport]);
React.useEffect(() => {
const onMsg = e => {
const t = e?.data?.type;
if (t === '__activate_edit_mode') setOpen(true);else if (t === '__deactivate_edit_mode') setOpen(false);
};
window.addEventListener('message', onMsg);
window.parent.postMessage({
type: '__edit_mode_available'
}, '*');
return () => window.removeEventListener('message', onMsg);
}, []);
const dismiss = () => {
setOpen(false);
window.parent.postMessage({
type: '__edit_mode_dismissed'
}, '*');
};
const onDragStart = e => {
const panel = dragRef.current;
if (!panel) return;
const r = panel.getBoundingClientRect();
const sx = e.clientX,
sy = e.clientY;
const startRight = window.innerWidth - r.right;
const startBottom = window.innerHeight - r.bottom;
const move = ev => {
offsetRef.current = {
x: startRight - (ev.clientX - sx),
y: startBottom - (ev.clientY - sy)
};
clampToViewport();
};
const up = () => {
window.removeEventListener('mousemove', move);
window.removeEventListener('mouseup', up);
};
window.addEventListener('mousemove', move);
window.addEventListener('mouseup', up);
};
if (!open) return null;
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("style", null, __TWEAKS_STYLE), /*#__PURE__*/React.createElement("div", {
ref: dragRef,
className: "twk-panel",
"data-noncommentable": "",
style: {
right: offsetRef.current.x,
bottom: offsetRef.current.y
}
}, /*#__PURE__*/React.createElement("div", {
className: "twk-hd",
onMouseDown: onDragStart
}, /*#__PURE__*/React.createElement("b", null, title), /*#__PURE__*/React.createElement("button", {
className: "twk-x",
"aria-label": "Close tweaks",
onMouseDown: e => e.stopPropagation(),
onClick: dismiss
}, "\u2715")), /*#__PURE__*/React.createElement("div", {
className: "twk-body"
}, children)));
}
// ── Layout helpers ──────────────────────────────────────────────────────────
function TweakSection({
label,
children
}) {
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
className: "twk-sect"
}, label), children);
}
function TweakRow({
label,
value,
children,
inline = false
}) {
return /*#__PURE__*/React.createElement("div", {
className: inline ? 'twk-row twk-row-h' : 'twk-row'
}, /*#__PURE__*/React.createElement("div", {
className: "twk-lbl"
}, /*#__PURE__*/React.createElement("span", null, label), value != null && /*#__PURE__*/React.createElement("span", {
className: "twk-val"
}, value)), children);
}
// ── Controls ────────────────────────────────────────────────────────────────
function TweakSlider({
label,
value,
min = 0,
max = 100,
step = 1,
unit = '',
onChange
}) {
return /*#__PURE__*/React.createElement(TweakRow, {
label: label,
value: `${value}${unit}`
}, /*#__PURE__*/React.createElement("input", {
type: "range",
className: "twk-slider",
min: min,
max: max,
step: step,
value: value,
onChange: e => onChange(Number(e.target.value))
}));
}
function TweakToggle({
label,
value,
onChange
}) {
return /*#__PURE__*/React.createElement("div", {
className: "twk-row twk-row-h"
}, /*#__PURE__*/React.createElement("div", {
className: "twk-lbl"
}, /*#__PURE__*/React.createElement("span", null, label)), /*#__PURE__*/React.createElement("button", {
type: "button",
className: "twk-toggle",
"data-on": value ? '1' : '0',
role: "switch",
"aria-checked": !!value,
onClick: () => onChange(!value)
}, /*#__PURE__*/React.createElement("i", null)));
}
function TweakRadio({
label,
value,
options,
onChange
}) {
const trackRef = React.useRef(null);
const [dragging, setDragging] = React.useState(false);
const opts = options.map(o => typeof o === 'object' ? o : {
value: o,
label: o
});
const idx = Math.max(0, opts.findIndex(o => o.value === value));
const n = opts.length;
// The active value is read by pointer-move handlers attached for the lifetime
// of a drag — ref it so a stale closure doesn't fire onChange for every move.
const valueRef = React.useRef(value);
valueRef.current = value;
const segAt = clientX => {
const r = trackRef.current.getBoundingClientRect();
const inner = r.width - 4;
const i = Math.floor((clientX - r.left - 2) / inner * n);
return opts[Math.max(0, Math.min(n - 1, i))].value;
};
const onPointerDown = e => {
setDragging(true);
const v0 = segAt(e.clientX);
if (v0 !== valueRef.current) onChange(v0);
const move = ev => {
if (!trackRef.current) return;
const v = segAt(ev.clientX);
if (v !== valueRef.current) onChange(v);
};
const up = () => {
setDragging(false);
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
};
return /*#__PURE__*/React.createElement(TweakRow, {
label: label
}, /*#__PURE__*/React.createElement("div", {
ref: trackRef,
role: "radiogroup",
onPointerDown: onPointerDown,
className: dragging ? 'twk-seg dragging' : 'twk-seg'
}, /*#__PURE__*/React.createElement("div", {
className: "twk-seg-thumb",
style: {
left: `calc(2px + ${idx} * (100% - 4px) / ${n})`,
width: `calc((100% - 4px) / ${n})`
}
}), opts.map(o => /*#__PURE__*/React.createElement("button", {
key: o.value,
type: "button",
role: "radio",
"aria-checked": o.value === value
}, o.label))));
}
function TweakSelect({
label,
value,
options,
onChange
}) {
return /*#__PURE__*/React.createElement(TweakRow, {
label: label
}, /*#__PURE__*/React.createElement("select", {
className: "twk-field",
value: value,
onChange: e => onChange(e.target.value)
}, options.map(o => {
const v = typeof o === 'object' ? o.value : o;
const l = typeof o === 'object' ? o.label : o;
return /*#__PURE__*/React.createElement("option", {
key: v,
value: v
}, l);
})));
}
function TweakText({
label,
value,
placeholder,
onChange
}) {
return /*#__PURE__*/React.createElement(TweakRow, {
label: label
}, /*#__PURE__*/React.createElement("input", {
className: "twk-field",
type: "text",
value: value,
placeholder: placeholder,
onChange: e => onChange(e.target.value)
}));
}
function TweakNumber({
label,
value,
min,
max,
step = 1,
unit = '',
onChange
}) {
const clamp = n => {
if (min != null && n < min) return min;
if (max != null && n > max) return max;
return n;
};
const startRef = React.useRef({
x: 0,
val: 0
});
const onScrubStart = e => {
e.preventDefault();
startRef.current = {
x: e.clientX,
val: value
};
const decimals = (String(step).split('.')[1] || '').length;
const move = ev => {
const dx = ev.clientX - startRef.current.x;
const raw = startRef.current.val + dx * step;
const snapped = Math.round(raw / step) * step;
onChange(clamp(Number(snapped.toFixed(decimals))));
};
const up = () => {
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
};
return /*#__PURE__*/React.createElement("div", {
className: "twk-num"
}, /*#__PURE__*/React.createElement("span", {
className: "twk-num-lbl",
onPointerDown: onScrubStart
}, label), /*#__PURE__*/React.createElement("input", {
type: "number",
value: value,
min: min,
max: max,
step: step,
onChange: e => onChange(clamp(Number(e.target.value)))
}), unit && /*#__PURE__*/React.createElement("span", {
className: "twk-num-unit"
}, unit));
}
function TweakColor({
label,
value,
onChange
}) {
return /*#__PURE__*/React.createElement("div", {
className: "twk-row twk-row-h"
}, /*#__PURE__*/React.createElement("div", {
className: "twk-lbl"
}, /*#__PURE__*/React.createElement("span", null, label)), /*#__PURE__*/React.createElement("input", {
type: "color",
className: "twk-swatch",
value: value,
onChange: e => onChange(e.target.value)
}));
}
function TweakButton({
label,
onClick,
secondary = false
}) {
return /*#__PURE__*/React.createElement("button", {
type: "button",
className: secondary ? 'twk-btn secondary' : 'twk-btn',
onClick: onClick
}, label);
}
Object.assign(window, {
useTweaks,
TweaksPanel,
TweakSection,
TweakRow,
TweakSlider,
TweakToggle,
TweakRadio,
TweakSelect,
TweakText,
TweakNumber,
TweakColor,
TweakButton
});
</script>
<script>
// Filter sidebar: status, priority, due, tags + scope.
function FilterPanel({
filters,
setFilters,
tagsList,
counts
}) {
const toggle = (key, val) => {
const cur = new Set(filters[key] || []);
cur.has(val) ? cur.delete(val) : cur.add(val);
setFilters({
...filters,
[key]: [...cur]
});
};
const isActive = (key, val) => (filters[key] || []).includes(val);
return /*#__PURE__*/React.createElement("aside", {
className: "filter-panel"
}, /*#__PURE__*/React.createElement("div", {
className: "filter-section"
}, /*#__PURE__*/React.createElement("div", {
className: "filter-label"
}, "Status"), /*#__PURE__*/React.createElement("div", {
className: "filter-list"
}, STATUSES.map(s => /*#__PURE__*/React.createElement("button", {
key: s.key,
className: `filter-chip ${isActive("statuses", s.key) ? "active" : ""}`,
onClick: () => toggle("statuses", s.key)
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: s.key,
size: 13
}), /*#__PURE__*/React.createElement("span", {
className: "filter-name"
}, s.label), /*#__PURE__*/React.createElement("span", {
className: "filter-count"
}, counts[s.key] || 0))))), /*#__PURE__*/React.createElement("div", {
className: "filter-section"
}, /*#__PURE__*/React.createElement("div", {
className: "filter-label"
}, "Priority"), /*#__PURE__*/React.createElement("div", {
className: "filter-list filter-list-row"
}, PRIORITIES.map(p => /*#__PURE__*/React.createElement("button", {
key: p,
className: `filter-chip mini ${isActive("priorities", p) ? "active" : ""} pri-${p}`,
onClick: () => toggle("priorities", p)
}, p === "P0" && /*#__PURE__*/React.createElement("span", {
className: "p0-dot"
}), p)))), /*#__PURE__*/React.createElement("div", {
className: "filter-section"
}, /*#__PURE__*/React.createElement("div", {
className: "filter-label"
}, "Due"), /*#__PURE__*/React.createElement("div", {
className: "filter-list"
}, [{
k: "overdue",
n: "Overdue"
}, {
k: "today",
n: "Today"
}, {
k: "week",
n: "This week"
}, {
k: "month",
n: "This month"
}].map(o => /*#__PURE__*/React.createElement("button", {
key: o.k,
className: `filter-chip ${filters.dueRange === o.k ? "active" : ""}`,
onClick: () => setFilters({
...filters,
dueRange: filters.dueRange === o.k ? null : o.k
})
}, /*#__PURE__*/React.createElement("span", {
className: "filter-name"
}, o.n))))), /*#__PURE__*/React.createElement(TagFilter, {
tagsList: tagsList,
selected: filters.tags || [],
onToggle: t => toggle("tags", t)
}), /*#__PURE__*/React.createElement("div", {
className: "filter-section"
}, /*#__PURE__*/React.createElement("div", {
className: "filter-label"
}, "Scope"), /*#__PURE__*/React.createElement("div", {
className: "filter-list"
}, [{
k: "all",
n: "All items"
}, {
k: "top",
n: "Top-level only"
}].map(o => /*#__PURE__*/React.createElement("button", {
key: o.k,
className: `filter-chip ${(filters.scope || "all") === o.k ? "active" : ""}`,
onClick: () => setFilters({
...filters,
scope: o.k
})
}, /*#__PURE__*/React.createElement("span", {
className: "filter-name"
}, o.n))))), (filters.statuses?.length || filters.priorities?.length || filters.tags?.length || filters.dueRange) && /*#__PURE__*/React.createElement("button", {
className: "clear-btn",
onClick: () => setFilters({
statuses: [],
priorities: [],
tags: [],
dueRange: null,
scope: filters.scope,
text: filters.text
})
}, "Clear filters"));
}
// ---- Tag filter with autocomplete (handles hundreds of tags) ----
function TagFilter({
tagsList,
selected,
onToggle
}) {
const [q, setQ] = React.useState("");
const [focused, setFocused] = React.useState(false);
const inputRef = React.useRef(null);
if (!tagsList.length) return null;
const ql = q.trim().toLowerCase();
const filtered = ql ? tagsList.filter(t => t.toLowerCase().includes(ql)).slice(0, 50) : tagsList.slice(0, 12); // top dozen by frequency when not searching
const exact = ql && tagsList.includes(ql);
const TOTAL = tagsList.length;
return /*#__PURE__*/React.createElement("div", {
className: "filter-section"
}, /*#__PURE__*/React.createElement("div", {
className: "filter-label"
}, "Tags ", /*#__PURE__*/React.createElement("span", {
className: "tag-total mono"
}, TOTAL)), selected.length > 0 && /*#__PURE__*/React.createElement("div", {
className: "tag-selected"
}, selected.map(t => /*#__PURE__*/React.createElement("button", {
key: t,
className: "tag-chip active",
onClick: () => onToggle(t),
title: "Remove"
}, "#", t, /*#__PURE__*/React.createElement("span", {
className: "tag-x"
}, "\xD7")))), /*#__PURE__*/React.createElement("div", {
className: `tag-search ${focused ? "focused" : ""}`
}, /*#__PURE__*/React.createElement(Icon, {
name: "search",
size: 12
}), /*#__PURE__*/React.createElement("input", {
ref: inputRef,
className: "tag-search-input",
placeholder: `Search ${TOTAL} tags…`,
value: q,
onChange: e => setQ(e.target.value),
onFocus: () => setFocused(true),
onBlur: () => setTimeout(() => setFocused(false), 120),
onKeyDown: e => {
if (e.key === "Enter" && filtered.length > 0) {
onToggle(filtered[0]);
setQ("");
} else if (e.key === "Escape") {
setQ("");
e.currentTarget.blur();
}
}
}), q && /*#__PURE__*/React.createElement("button", {
className: "tag-search-clear",
onClick: () => {
setQ("");
inputRef.current?.focus();
}
}, "\xD7")), /*#__PURE__*/React.createElement("div", {
className: "tag-results"
}, filtered.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "tag-empty"
}, "No tags match \"", q, "\"") : /*#__PURE__*/React.createElement(React.Fragment, null, filtered.map(t => {
const isSelected = selected.includes(t);
return /*#__PURE__*/React.createElement("button", {
key: t,
className: `tag-chip ${isSelected ? "active" : ""}`,
onClick: () => onToggle(t),
title: isSelected ? "Remove from filter" : "Add to filter"
}, ql && !isSelected ? /*#__PURE__*/React.createElement(TagHighlight, {
text: t,
match: ql
}) : `#${t}`);
}), !ql && tagsList.length > 12 && /*#__PURE__*/React.createElement("span", {
className: "tag-more mono"
}, "+", tagsList.length - 12, " more \u2014 type to find"))));
}
function TagHighlight({
text,
match
}) {
const i = text.toLowerCase().indexOf(match);
if (i < 0) return `#${text}`;
return /*#__PURE__*/React.createElement(React.Fragment, null, "#", text.slice(0, i), /*#__PURE__*/React.createElement("span", {
className: "hl"
}, text.slice(i, i + match.length)), text.slice(i + match.length));
}
window.FilterPanel = FilterPanel;
</script>
<script>
// Backlog tree view: rows, expand/collapse, status cycling, drag handle, add/delete.
const {
useState,
useRef,
useEffect,
useMemo
} = React;
// Inline priority popover
function PriorityPicker({
value,
onChange,
onClose
}) {
const ref = useRef(null);
useEffect(() => {
const close = e => {
if (!ref.current?.contains(e.target)) onClose();
};
document.addEventListener("mousedown", close);
return () => document.removeEventListener("mousedown", close);
}, [onClose]);
return /*#__PURE__*/React.createElement("div", {
className: "pri-pop",
ref: ref,
onClick: e => e.stopPropagation()
}, PRIORITIES.map(p => /*#__PURE__*/React.createElement("button", {
key: p,
className: `pri-pop-item pri-${p} ${value === p ? "active" : ""}`,
onClick: () => {
onChange(p);
onClose();
}
}, p === "P0" && /*#__PURE__*/React.createElement("span", {
className: "p0-dot"
}), /*#__PURE__*/React.createElement("span", null, p), /*#__PURE__*/React.createElement("span", {
className: "pri-pop-hint"
}, p === "P0" ? "Burning" : p === "P1" ? "High" : p === "P2" ? "Normal" : "Low"))));
}
// Inline status popover (replaces ●●● menu)
function StatusPopover({
value,
style,
onChange,
onClose,
anchorRect
}) {
const ref = useRef(null);
useEffect(() => {
const close = e => {
if (!ref.current?.contains(e.target)) onClose();
};
document.addEventListener("mousedown", close);
return () => document.removeEventListener("mousedown", close);
}, [onClose]);
return /*#__PURE__*/React.createElement("div", {
className: "status-pop",
ref: ref,
onClick: e => e.stopPropagation()
}, STATUSES.map(s => /*#__PURE__*/React.createElement("button", {
key: s.key,
className: `status-pop-item ${value === s.key ? "active" : ""}`,
onClick: () => {
onChange(s.key);
onClose();
}
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: s.key,
style: style,
size: 14
}), /*#__PURE__*/React.createElement("span", null, s.label))));
}
// Right-click context menu: set status / archive / delete, fixed at the cursor.
function ItemContextMenu({
item,
statusStyle,
anchor,
onSetStatus,
onArchive,
onDelete,
onClose
}) {
const ref = useRef(null);
const [pos, setPos] = useState({
left: anchor.x,
top: anchor.y
});
// Viewport clamp (upstream ItemPeek pattern): flip left / above when overflowing.
useEffect(() => {
const el = ref.current;
if (!el) return;
const r = el.getBoundingClientRect();
const pad = 12,
gap = 14;
let left = anchor.x + gap,
top = anchor.y + gap;
if (left + r.width > window.innerWidth - pad) left = Math.max(pad, anchor.x - r.width - gap);
if (top + r.height > window.innerHeight - pad) top = Math.max(pad, anchor.y - r.height - gap);
setPos({
left,
top
});
}, [anchor.x, anchor.y, item]);
// Capture-phase scroll/wheel: any scrollable container must close the cursor-anchored menu.
useEffect(() => {
const onPointerDown = e => {
if (!ref.current?.contains(e.target)) onClose();
};
const onKeyDown = e => {
if (e.key === "Escape") onClose();
};
const onMove = () => onClose();
document.addEventListener("pointerdown", onPointerDown);
document.addEventListener("keydown", onKeyDown);
document.addEventListener("scroll", onMove, true);
document.addEventListener("wheel", onMove, true);
window.addEventListener("resize", onMove);
return () => {
document.removeEventListener("pointerdown", onPointerDown);
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("scroll", onMove, true);
document.removeEventListener("wheel", onMove, true);
window.removeEventListener("resize", onMove);
};
}, [onClose]);
return /*#__PURE__*/React.createElement("div", {
ref: ref,
className: "ctx-menu",
style: {
left: pos.left,
top: pos.top
},
onContextMenu: e => e.preventDefault()
}, /*#__PURE__*/React.createElement("div", {
className: "ctx-menu-label"
}, "Set status"), STATUSES.map(s => /*#__PURE__*/React.createElement("button", {
key: s.key,
className: `ctx-menu-item ${s.key === item.status ? "active" : ""}`,
onClick: () => {
onSetStatus(s.key);
onClose();
}
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: s.key,
style: statusStyle,
size: 13
}), /*#__PURE__*/React.createElement("span", null, s.label))), /*#__PURE__*/React.createElement("div", {
className: "ctx-menu-sep"
}), /*#__PURE__*/React.createElement("button", {
className: "ctx-menu-item",
onClick: () => {
onArchive();
onClose();
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "archive"
}), /*#__PURE__*/React.createElement("span", null, "Archive")), /*#__PURE__*/React.createElement("button", {
className: "ctx-menu-item danger",
onClick: () => {
onDelete();
onClose();
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "trash"
}), /*#__PURE__*/React.createElement("span", null, "Delete")));
}
// Single tree row
function ItemRow({
item,
depth,
expanded,
hasChildren,
statusStyle,
onToggle,
onSetStatus,
onAddChild,
onDelete,
onArchive,
onEdit,
onSetPriority,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
onDragStart,
onDragOver,
onDrop,
dimmed,
query,
dropTarget
}) {
const [statusOpen, setStatusOpen] = useState(false);
const [priOpen, setPriOpen] = useState(false);
const [ctxMenu, setCtxMenu] = useState(null);
const overdue = isOverdue(item);
const isP0 = item.priority === "P0" && item.status !== "done" && item.status !== "cancelled";
const isDone = item.status === "done" || item.status === "cancelled";
const renderTitle = () => {
if (!query) return item.title;
const i = item.title.toLowerCase().indexOf(query.toLowerCase());
if (i < 0) return item.title;
return /*#__PURE__*/React.createElement(React.Fragment, null, item.title.slice(0, i), /*#__PURE__*/React.createElement("mark", {
className: "hl"
}, item.title.slice(i, i + query.length)), item.title.slice(i + query.length));
};
return /*#__PURE__*/React.createElement("div", {
className: `row level-${depth} ${dimmed ? "dimmed" : ""} ${isP0 ? "p0" : ""} ${dropTarget ? "drop-target" : ""}`,
draggable: true,
onDragStart: e => {
setCtxMenu(null);
onDragStart?.(e, item);
},
onDragOver: e => onDragOver?.(e, item),
onDrop: e => onDrop?.(e, item),
onContextMenu: e => {
e.preventDefault();
setStatusOpen(false);
setPriOpen(false);
setCtxMenu({
x: e.clientX,
y: e.clientY
});
}
}, ctxMenu && ReactDOM.createPortal(/*#__PURE__*/React.createElement(ItemContextMenu, {
item: item,
statusStyle: statusStyle,
anchor: ctxMenu,
onSetStatus: onSetStatus,
onArchive: onArchive,
onDelete: onDelete,
onClose: () => setCtxMenu(null)
}), document.body), /*#__PURE__*/React.createElement("span", {
className: "row-rail",
style: {
width: depth * 18
}
}), /*#__PURE__*/React.createElement("button", {
className: `twisty ${hasChildren ? "" : "empty"}`,
onClick: hasChildren ? onToggle : undefined,
"aria-label": expanded ? "Collapse" : "Expand",
tabIndex: hasChildren ? 0 : -1
}, hasChildren && /*#__PURE__*/React.createElement(Icon, {
name: expanded ? "chevron" : "chevronRight",
size: 12
})), /*#__PURE__*/React.createElement("span", {
className: "drag-handle",
title: "Drag to reorder within priority"
}, /*#__PURE__*/React.createElement(Icon, {
name: "drag",
size: 14
})), /*#__PURE__*/React.createElement("span", {
className: "status-wrap"
}, /*#__PURE__*/React.createElement("button", {
className: "status-btn",
onClick: () => setStatusOpen(v => !v),
title: `${STATUS_BY_KEY[item.status].label} — click to change`
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: item.status,
style: statusStyle,
size: 14
})), statusOpen && /*#__PURE__*/React.createElement(StatusPopover, {
value: item.status,
style: statusStyle,
onChange: onSetStatus,
onClose: () => setStatusOpen(false)
})), /*#__PURE__*/React.createElement("span", {
className: "pri-wrap"
}, /*#__PURE__*/React.createElement("button", {
className: `pri pri-${item.priority}`,
onClick: () => setPriOpen(v => !v),
title: "Change priority"
}, item.priority === "P0" && /*#__PURE__*/React.createElement("span", {
className: "p0-dot"
}), item.priority), priOpen && /*#__PURE__*/React.createElement(PriorityPicker, {
value: item.priority,
onChange: p => onSetPriority(p),
onClose: () => setPriOpen(false)
})), /*#__PURE__*/React.createElement("span", {
className: `title ${isDone ? "done" : ""}`,
onDoubleClick: onEdit
}, renderTitle()), shouldShowProgress(item) && /*#__PURE__*/React.createElement(ProgressGauge, {
value: progressFor(item)
}), item.due && /*#__PURE__*/React.createElement("span", {
className: `due ${overdue ? "overdue" : ""}`
}, dueRelative(item)), item.tags?.length > 0 && /*#__PURE__*/React.createElement("span", {
className: "tags"
}, item.tags.map(t => /*#__PURE__*/React.createElement("span", {
key: t,
className: "tag"
}, "#", t))), item.status === "blocked" && item.reason && /*#__PURE__*/React.createElement("span", {
className: "reason",
title: item.reason
}, "\u21B3 ", item.reason), /*#__PURE__*/React.createElement("span", {
className: "row-spacer"
}), /*#__PURE__*/React.createElement("span", {
className: "row-actions"
}, /*#__PURE__*/React.createElement("button", {
className: "icon-btn",
onClick: onMoveUp,
disabled: !canMoveUp,
"data-tip": "Move up",
"aria-label": "Move up"
}, /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 16 16"
}, /*#__PURE__*/React.createElement("polyline", {
points: "4,10 8,5 12,10",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.6",
strokeLinecap: "round",
strokeLinejoin: "round"
}))), /*#__PURE__*/React.createElement("button", {
className: "icon-btn",
onClick: onMoveDown,
disabled: !canMoveDown,
"data-tip": "Move down",
"aria-label": "Move down"
}, /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 16 16"
}, /*#__PURE__*/React.createElement("polyline", {
points: "4,6 8,11 12,6",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.6",
strokeLinecap: "round",
strokeLinejoin: "round"
}))), /*#__PURE__*/React.createElement("span", {
className: "action-sep"
}), depth < 3 && /*#__PURE__*/React.createElement("button", {
className: "icon-btn",
onClick: onAddChild,
"data-tip": "Add sub-item",
"aria-label": "Add sub-item"
}, /*#__PURE__*/React.createElement(Icon, {
name: "addChild",
size: 14
})), /*#__PURE__*/React.createElement("button", {
className: "icon-btn",
onClick: onEdit,
"data-tip": "Edit item",
"aria-label": "Edit item"
}, /*#__PURE__*/React.createElement(Icon, {
name: "edit",
size: 13
})), /*#__PURE__*/React.createElement("button", {
className: "icon-btn danger",
onClick: onDelete,
"data-tip": "Delete",
"aria-label": "Delete"
}, /*#__PURE__*/React.createElement(Icon, {
name: "trash",
size: 13
}))));
}
// Expanded-state map kept in parent so filtering doesn't lose it.
function BacklogTree({
items,
expandedMap,
setExpanded,
onMutate,
query,
statusStyle,
manualOrder
}) {
const [draggedId, setDraggedId] = useState(null);
const [dropTargetId, setDropTargetId] = useState(null);
const onDragStart = (e, item) => {
setDraggedId(item.id);
e.dataTransfer.effectAllowed = "move";
try {
e.dataTransfer.setData("text/plain", item.id);
} catch (_) {}
};
const onDragOver = (e, item) => {
if (!draggedId || draggedId === item.id) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setDropTargetId(item.id);
};
const onDrop = (e, item) => {
e.preventDefault();
if (draggedId && draggedId !== item.id) {
onMutate.reorder(draggedId, item.id);
}
setDraggedId(null);
setDropTargetId(null);
};
function renderList(list, depth, parentList) {
return list.map((item, idx) => {
const expanded = expandedMap[item.id] ?? !item.collapsed;
const hasChildren = item.children?.length > 0;
const forceOpen = query && item._matches !== true && hasChildren;
const showKids = (expanded || forceOpen) && hasChildren;
// can-move logic: only swap with adjacent same-priority sibling
const samePri = parentList.map((x, i) => ({
x,
i
})).filter(({
x
}) => x.priority === item.priority);
const myIdx = samePri.findIndex(({
x
}) => x.id === item.id);
const canMoveUp = myIdx > 0;
const canMoveDown = myIdx >= 0 && myIdx < samePri.length - 1;
return /*#__PURE__*/React.createElement(React.Fragment, {
key: item.id
}, /*#__PURE__*/React.createElement(ItemRow, {
item: item,
depth: depth,
expanded: expanded || forceOpen,
hasChildren: hasChildren,
statusStyle: statusStyle,
dimmed: query && item._matches === false,
dropTarget: dropTargetId === item.id && draggedId !== item.id,
query: query,
onToggle: () => setExpanded(item.id, !expanded),
onSetStatus: s => onMutate.setStatus(item.id, s),
onSetPriority: p => onMutate.setPriority(item.id, p),
onAddChild: () => onMutate.addChild(item.id),
onDelete: () => onMutate.deleteItem(item.id),
onArchive: () => onMutate.archiveItem(item.id),
onEdit: () => onMutate.editItem(item.id),
onMoveUp: () => onMutate.moveWithinPriority(item.id, -1),
onMoveDown: () => onMutate.moveWithinPriority(item.id, +1),
canMoveUp: canMoveUp,
canMoveDown: canMoveDown,
onDragStart: onDragStart,
onDragOver: onDragOver,
onDrop: onDrop
}), showKids && renderList(item.children, depth + 1, item.children));
});
}
return /*#__PURE__*/React.createElement("div", {
className: "tree"
}, renderList(items, 0, items));
}
window.BacklogTree = BacklogTree;
</script>
<script>
// Stylish dialog primitives + concrete dialogs (Edit/Add Item, Confirm).
const {
useState: useStateD,
useEffect: useEffectD,
useRef: useRefD,
useCallback: useCallbackD
} = React;
// ---- Rich Text Editor (lightweight WYSIWYG using contenteditable) ----
function RichTextEditor({
value,
onChange,
placeholder = "Add details...",
collapsed = false,
onExpand
}) {
const editorRef = useRefD(null);
const [isExpanded, setIsExpanded] = useStateD(!collapsed || !!value);
const [showCopyMenu, setShowCopyMenu] = useStateD(false);
const [copyMenuPos, setCopyMenuPos] = useStateD({
x: 0,
y: 0
});
const toolbarRef = useRefD(null);
const isEditingRef = useRefD(false);
const prevValueRef = useRefD(value);
// Initialize editor content only when value changes externally (not from our own edits)
useEffectD(() => {
if (editorRef.current && !isEditingRef.current) {
// Update if value changed from previous or editor is empty
if (prevValueRef.current !== value || editorRef.current.innerHTML === '') {
const html = MarkdownUtils.toHtml(value || '');
editorRef.current.innerHTML = html;
prevValueRef.current = value;
}
}
}, [value]);
const handleInput = useCallbackD(() => {
if (editorRef.current) {
isEditingRef.current = true;
const md = MarkdownUtils.toMarkdown(editorRef.current.innerHTML);
onChange?.(md || null);
// Reset editing flag after a short delay to allow state updates
setTimeout(() => {
isEditingRef.current = false;
}, 100);
}
}, [onChange]);
const execCmd = useCallbackD((cmd, val = null) => {
editorRef.current?.focus();
document.execCommand(cmd, false, val);
handleInput();
}, [handleInput]);
const handleKeyDown = useCallbackD(e => {
if (e.key === 'Tab') {
e.preventDefault();
execCmd('insertText', ' ');
}
// Auto-convert "- " or "* " at line start to bullet list
if (e.key === ' ') {
const sel = window.getSelection();
if (sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
const node = range.startContainer;
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
const offset = range.startOffset;
// Check if cursor is right after "- " or "* " at line start
if (offset === 1 && (text[0] === '-' || text[0] === '*')) {
e.preventDefault();
// Remove the "-" or "*" character
node.textContent = text.slice(1);
// Convert to bullet list
execCmd('insertUnorderedList');
return;
}
// Also check for "1. " to convert to numbered list
if (offset === 2 && text[0] === '1' && text[1] === '.') {
e.preventDefault();
node.textContent = text.slice(2);
execCmd('insertOrderedList');
return;
}
}
}
}
// Ctrl/Cmd + B/I/K shortcuts
if (e.ctrlKey || e.metaKey) {
if (e.key === 'b') {
e.preventDefault();
execCmd('bold');
}
if (e.key === 'i') {
e.preventDefault();
execCmd('italic');
}
if (e.key === 'k') {
e.preventDefault();
insertLink();
}
}
}, [execCmd]);
const insertLink = useCallbackD(() => {
const url = prompt('Enter URL:');
if (url) execCmd('createLink', url);
}, [execCmd]);
const insertImage = useCallbackD(() => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async e => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
execCmd('insertHTML', `<img src="${reader.result}" alt="${file.name}" class="rte-img">`);
};
reader.readAsDataURL(file);
};
input.click();
}, [execCmd]);
const insertTable = useCallbackD(() => {
const html = '<table class="rte-table"><thead><tr><th>Header 1</th><th>Header 2</th></tr></thead><tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody></table><p><br></p>';
execCmd('insertHTML', html);
}, [execCmd]);
const insertCodeBlock = useCallbackD(() => {
execCmd('insertHTML', '<pre class="rte-code-block"><code>code here</code></pre><p><br></p>');
}, [execCmd]);
const handleCopyMenu = useCallbackD(e => {
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
setCopyMenuPos({
x: rect.left,
y: rect.bottom + 4
});
setShowCopyMenu(true);
}, []);
const copyAs = useCallbackD(async format => {
setShowCopyMenu(false);
const html = editorRef.current?.innerHTML || '';
const md = MarkdownUtils.toMarkdown(html);
if (format === 'html') await ClipboardUtils.copyHtml(html, md);else if (format === 'markdown') await ClipboardUtils.copyMarkdown(md);else if (format === 'text') await ClipboardUtils.copyPlainText(html);
}, []);
const expand = useCallbackD(() => {
setIsExpanded(true);
onExpand?.();
setTimeout(() => editorRef.current?.focus(), 50);
}, [onExpand]);
// Close copy menu on outside click
useEffectD(() => {
if (!showCopyMenu) return;
const close = () => setShowCopyMenu(false);
document.addEventListener('click', close);
return () => document.removeEventListener('click', close);
}, [showCopyMenu]);
if (!isExpanded) {
return /*#__PURE__*/React.createElement("div", {
className: "rte-collapsed",
onClick: expand
}, /*#__PURE__*/React.createElement("span", {
className: "rte-collapsed-placeholder"
}, placeholder), /*#__PURE__*/React.createElement(Icon, {
name: "plus",
size: 12
}));
}
return /*#__PURE__*/React.createElement("div", {
className: "rte-wrap"
}, /*#__PURE__*/React.createElement("div", {
className: "rte-toolbar",
ref: toolbarRef
}, /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => execCmd('bold'),
title: "Bold (Ctrl+B)"
}, /*#__PURE__*/React.createElement("strong", null, "B")), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => execCmd('italic'),
title: "Italic (Ctrl+I)"
}, /*#__PURE__*/React.createElement("em", null, "I")), /*#__PURE__*/React.createElement("span", {
className: "rte-toolbar-sep"
}), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: insertLink,
title: "Link (Ctrl+K)"
}, /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 20 20"
}, /*#__PURE__*/React.createElement("path", {
d: "M8 12l4-4M6 10l-1 1a3 3 0 0 0 4.24 4.24l1-1M14 10l1-1a3 3 0 0 0-4.24-4.24l-1 1",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round"
}))), /*#__PURE__*/React.createElement("span", {
className: "rte-toolbar-sep"
}), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => execCmd('formatBlock', 'h1'),
title: "Heading 1"
}, "H1"), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => execCmd('formatBlock', 'h2'),
title: "Heading 2"
}, "H2"), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => execCmd('formatBlock', 'h3'),
title: "Heading 3"
}, "H3"), /*#__PURE__*/React.createElement("span", {
className: "rte-toolbar-sep"
}), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => execCmd('insertUnorderedList'),
title: "Bullet list"
}, /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 20 20"
}, /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round"
}, /*#__PURE__*/React.createElement("line", {
x1: "8",
y1: "6",
x2: "17",
y2: "6"
}), /*#__PURE__*/React.createElement("line", {
x1: "8",
y1: "10",
x2: "17",
y2: "10"
}), /*#__PURE__*/React.createElement("line", {
x1: "8",
y1: "14",
x2: "17",
y2: "14"
}), /*#__PURE__*/React.createElement("circle", {
cx: "4",
cy: "6",
r: "1",
fill: "currentColor"
}), /*#__PURE__*/React.createElement("circle", {
cx: "4",
cy: "10",
r: "1",
fill: "currentColor"
}), /*#__PURE__*/React.createElement("circle", {
cx: "4",
cy: "14",
r: "1",
fill: "currentColor"
})))), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => execCmd('insertOrderedList'),
title: "Numbered list"
}, /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 20 20"
}, /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round"
}, /*#__PURE__*/React.createElement("line", {
x1: "8",
y1: "6",
x2: "17",
y2: "6"
}), /*#__PURE__*/React.createElement("line", {
x1: "8",
y1: "10",
x2: "17",
y2: "10"
}), /*#__PURE__*/React.createElement("line", {
x1: "8",
y1: "14",
x2: "17",
y2: "14"
}), /*#__PURE__*/React.createElement("text", {
x: "3",
y: "7",
fontSize: "5",
fill: "currentColor",
stroke: "none"
}, "1"), /*#__PURE__*/React.createElement("text", {
x: "3",
y: "11",
fontSize: "5",
fill: "currentColor",
stroke: "none"
}, "2"), /*#__PURE__*/React.createElement("text", {
x: "3",
y: "15",
fontSize: "5",
fill: "currentColor",
stroke: "none"
}, "3")))), /*#__PURE__*/React.createElement("span", {
className: "rte-toolbar-sep"
}), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: insertCodeBlock,
title: "Code block"
}, /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 20 20"
}, /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("polyline", {
points: "7,5 3,10 7,15"
}), /*#__PURE__*/React.createElement("polyline", {
points: "13,5 17,10 13,15"
})))), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: insertTable,
title: "Table"
}, /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 20 20"
}, /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5"
}, /*#__PURE__*/React.createElement("rect", {
x: "3",
y: "4",
width: "14",
height: "12",
rx: "1.5"
}), /*#__PURE__*/React.createElement("line", {
x1: "3",
y1: "8",
x2: "17",
y2: "8"
}), /*#__PURE__*/React.createElement("line", {
x1: "10",
y1: "8",
x2: "10",
y2: "16"
})))), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: insertImage,
title: "Image"
}, /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 20 20"
}, /*#__PURE__*/React.createElement("g", {
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinejoin: "round"
}, /*#__PURE__*/React.createElement("rect", {
x: "3",
y: "4",
width: "14",
height: "12",
rx: "1.5"
}), /*#__PURE__*/React.createElement("circle", {
cx: "7",
cy: "8",
r: "1.5"
}), /*#__PURE__*/React.createElement("path", {
d: "M3 14l4-4 3 3 4-4 3 3"
})))), /*#__PURE__*/React.createElement("span", {
className: "rte-toolbar-sep"
}), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: handleCopyMenu,
title: "Copy as...",
className: "rte-copy-btn"
}, /*#__PURE__*/React.createElement(Icon, {
name: "copy",
size: 12
}), /*#__PURE__*/React.createElement("svg", {
width: "8",
height: "8",
viewBox: "0 0 10 10"
}, /*#__PURE__*/React.createElement("polyline", {
points: "2,4 5,7 8,4",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round"
})))), showCopyMenu && /*#__PURE__*/React.createElement("div", {
className: "rte-copy-menu",
style: {
left: copyMenuPos.x,
top: copyMenuPos.y
}
}, /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => copyAs('html')
}, "Copy for Docs/Word"), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => copyAs('markdown')
}, "Copy as Markdown"), /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => copyAs('text')
}, "Copy as Plain Text")), /*#__PURE__*/React.createElement("div", {
ref: editorRef,
className: "rte-editor",
contentEditable: true,
onInput: handleInput,
onKeyDown: handleKeyDown,
"data-placeholder": placeholder,
onContextMenu: e => {
// Allow default context menu but add copy options via showCopyMenu
}
}));
}
function Dialog({
open,
onClose,
children,
width = 460,
labelledBy
}) {
useEffectD(() => {
if (!open) return;
const onKey = e => {
if (e.key === "Escape") onClose?.();
};
document.addEventListener("keydown", onKey);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = "";
};
}, [open, onClose]);
if (!open) return null;
return /*#__PURE__*/React.createElement("div", {
className: "dlg-backdrop",
onMouseDown: e => {
if (e.target === e.currentTarget) onClose?.();
}
}, /*#__PURE__*/React.createElement("div", {
className: "dlg",
role: "dialog",
"aria-modal": "true",
"aria-labelledby": labelledBy,
style: {
maxWidth: width
}
}, children));
}
function DialogHeader({
eyebrow,
title,
onClose,
id
}) {
return /*#__PURE__*/React.createElement("div", {
className: "dlg-head"
}, /*#__PURE__*/React.createElement("div", null, eyebrow && /*#__PURE__*/React.createElement("div", {
className: "eyebrow"
}, eyebrow), /*#__PURE__*/React.createElement("h3", {
id: id,
className: "dlg-title"
}, title)), onClose && /*#__PURE__*/React.createElement("button", {
className: "dlg-close",
onClick: onClose,
"aria-label": "Close"
}, /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 16 16"
}, /*#__PURE__*/React.createElement("path", {
d: "M3 3l10 10M13 3L3 13",
stroke: "currentColor",
strokeWidth: "1.6",
strokeLinecap: "round"
}))));
}
// --- Item editor (used for both edit and add) ---
function ItemDialog({
open,
mode,
initial,
onClose,
onSubmit,
recentTags = [],
isMultiProject = false,
projects = [],
defaultProjectId = null
}) {
const [title, setTitle] = useStateD("");
const [body, setBody] = useStateD(null);
const [priority, setPriority] = useStateD("P2");
const [status, setStatus] = useStateD("open");
const [due, setDue] = useStateD("");
const [tags, setTags] = useStateD([]);
const [tagInput, setTagInput] = useStateD("");
const [reason, setReason] = useStateD("");
const [progress, setProgress] = useStateD(0);
const [createAsProject, setCreateAsProject] = useStateD(false);
const [projectPath, setProjectPath] = useStateD("");
const [selectedProjectId, setSelectedProjectId] = useStateD(null);
const titleRef = useRefD(null);
const showProjectOption = isMultiProject && mode === "add";
const showPathInput = showProjectOption && createAsProject;
const showProjectSelect = showProjectOption && !createAsProject && projects.length > 0;
useEffectD(() => {
if (!open) return;
setTitle(initial?.title || "");
setBody(initial?.body || null);
setPriority(initial?.priority || "P2");
setStatus(initial?.status || "open");
setDue(initial?.due || "");
setTags(initial?.tags || []);
setTagInput("");
setReason(initial?.reason || "");
setProgress(initial?.progress ?? 0);
setCreateAsProject(false);
setProjectPath("");
setSelectedProjectId(defaultProjectId && projects.some(p => p.id === defaultProjectId) ? defaultProjectId : projects[0] ? projects[0].id : null);
setTimeout(() => titleRef.current?.focus(), 50);
}, [open, initial]);
const addTag = t => {
const v = t.trim().replace(/^#/, "");
if (!v) return;
if (tags.includes(v)) return;
setTags([...tags, v]);
};
const removeTag = t => setTags(tags.filter(x => x !== t));
const submit = e => {
e?.preventDefault();
// Item type requires a title; Project type only requires a path (title is an optional display name).
if (showPathInput ? !projectPath.trim() : !title.trim()) return;
// Reconcile progress with status before submitting.
let outProgress = snapProgress(progress);
if (status === "done") outProgress = 100;
const payload = {
title: title.trim(),
body: body || null,
priority,
status,
due: due || null,
tags,
reason: status === "blocked" ? reason.trim() || null : null,
progress: outProgress
};
if (isMultiProject) {
const rawPath = projectPath.trim();
payload.createAsProject = showPathInput;
payload.projectPath = showPathInput ? rawPath.endsWith('.md') ? rawPath : rawPath.replace(/\/$/, '') + '/backlog.md' : null;
payload.projectId = showProjectSelect ? selectedProjectId : null;
}
onSubmit(payload);
};
const tagSuggestions = recentTags.filter(t => !tags.includes(t)).slice(0, 8);
return /*#__PURE__*/React.createElement(Dialog, {
open: open,
onClose: onClose,
width: 520,
labelledBy: "dlg-item-title"
}, /*#__PURE__*/React.createElement("form", {
onSubmit: submit,
onKeyDown: e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
submit();
}
}
}, /*#__PURE__*/React.createElement(DialogHeader, {
id: "dlg-item-title",
eyebrow: mode === "view" ? "Archived item" : mode === "edit" ? "Edit item" : mode === "add-child" ? "Add sub-item" : "New item",
title: mode === "view" ? "View details (read-only)" : mode === "edit" ? "Edit details" : "Create item",
onClose: onClose
}), /*#__PURE__*/React.createElement("div", {
className: "dlg-body"
}, /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Title"), /*#__PURE__*/React.createElement("input", {
ref: titleRef,
className: "text-input",
value: title,
onChange: e => setTitle(e.target.value),
placeholder: showPathInput ? "Optional display name" : "What needs to happen?",
required: !showPathInput,
readOnly: mode === "view"
})), /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Details ", /*#__PURE__*/React.createElement("span", {
className: "field-hint"
}, mode === "view" ? "read-only" : "rich text, supports markdown")), mode === "view" ? /*#__PURE__*/React.createElement("div", {
className: "rte-readonly",
dangerouslySetInnerHTML: {
__html: MarkdownUtils.toHtml(body || '')
}
}) : /*#__PURE__*/React.createElement(RichTextEditor, {
value: body,
onChange: setBody,
placeholder: "Add notes, context, or details...",
collapsed: !body
})), /*#__PURE__*/React.createElement("div", {
className: "field-row"
}, /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Priority"), /*#__PURE__*/React.createElement("div", {
className: "seg"
}, PRIORITIES.map(p => /*#__PURE__*/React.createElement("button", {
type: "button",
key: p,
className: `seg-btn pri-${p} ${priority === p ? "active" : ""}`,
onClick: () => mode !== "view" && setPriority(p),
disabled: mode === "view"
}, p === "P0" && /*#__PURE__*/React.createElement("span", {
className: "p0-dot"
}), p)))), /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Due"), /*#__PURE__*/React.createElement("input", {
className: "text-input",
type: "date",
value: due,
onChange: e => setDue(e.target.value),
readOnly: mode === "view"
}))), /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Status"), /*#__PURE__*/React.createElement("div", {
className: "status-grid"
}, STATUSES.map(s => /*#__PURE__*/React.createElement("button", {
type: "button",
key: s.key,
className: `status-card ${status === s.key ? "active" : ""}`,
onClick: () => mode !== "view" && setStatus(s.key),
disabled: mode === "view"
}, /*#__PURE__*/React.createElement("span", {
className: `status-card-icon status-${s.key}`
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: s.key,
size: 16
})), /*#__PURE__*/React.createElement("span", {
className: "status-card-label"
}, s.label))))), status !== "cancelled" && /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Completion", /*#__PURE__*/React.createElement("span", {
className: "field-hint"
}, mode === "view" ? "read-only" : status === "done" ? "locked at 100% while marked done" : "rough estimate — set 100% to mark done")), /*#__PURE__*/React.createElement("div", {
className: "progress-row"
}, /*#__PURE__*/React.createElement("div", {
className: `seg progress-seg ${status === "done" || mode === "view" ? "locked" : ""}`
}, PROGRESS_STEPS.map(p => /*#__PURE__*/React.createElement("button", {
type: "button",
key: p,
className: `seg-btn ${progress === p ? "active" : ""}`,
onClick: () => setProgress(p),
disabled: status === "done" || mode === "view"
}, p, "%"))), /*#__PURE__*/React.createElement(ProgressGauge, {
value: status === "done" ? 100 : progress,
size: "md"
}))), status === "blocked" && /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Block reason"), /*#__PURE__*/React.createElement("input", {
className: "text-input",
value: reason,
onChange: e => setReason(e.target.value),
placeholder: "Waiting for\u2026",
readOnly: mode === "view"
})), /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Tags ", mode !== "view" && /*#__PURE__*/React.createElement("span", {
className: "field-hint"
}, "type a name, press Enter or comma to add")), mode === "view" ? /*#__PURE__*/React.createElement("div", {
className: "tag-list-readonly"
}, tags.length === 0 ? /*#__PURE__*/React.createElement("span", {
className: "muted"
}, "No tags") : tags.map(t => /*#__PURE__*/React.createElement("span", {
key: t,
className: "tag"
}, t))) : /*#__PURE__*/React.createElement(TagAutocompleteInput, {
tags: tags,
setTags: setTags,
tagInput: tagInput,
setTagInput: setTagInput,
addTag: addTag,
removeTag: removeTag,
allTags: recentTags
})), showProjectOption && /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Type"), /*#__PURE__*/React.createElement("div", {
className: "status-grid",
style: {
gridTemplateColumns: '1fr 1fr'
}
}, /*#__PURE__*/React.createElement("button", {
type: "button",
className: `status-card ${!createAsProject ? 'active' : ''}`,
onClick: () => setCreateAsProject(false)
}, /*#__PURE__*/React.createElement("span", {
className: "status-card-icon",
style: {
background: 'var(--bg-2)'
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "check",
size: 16
})), /*#__PURE__*/React.createElement("span", {
className: "status-card-label"
}, "Item")), /*#__PURE__*/React.createElement("button", {
type: "button",
className: `status-card ${createAsProject ? 'active' : ''}`,
onClick: () => setCreateAsProject(true)
}, /*#__PURE__*/React.createElement("span", {
className: "status-card-icon",
style: {
background: 'var(--bg-2)'
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "folder",
size: 16
})), /*#__PURE__*/React.createElement("span", {
className: "status-card-label"
}, "Project")))), showProjectSelect && /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "Project ", /*#__PURE__*/React.createElement("span", {
className: "field-hint"
}, "where this item will live")), /*#__PURE__*/React.createElement("select", {
className: "text-input",
value: selectedProjectId ?? "",
onChange: e => setSelectedProjectId(e.target.value)
}, projects.map(p => /*#__PURE__*/React.createElement("option", {
key: p.id,
value: p.id
}, p.name)))), showPathInput && /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement("label", {
className: "field-label"
}, "File path ", /*#__PURE__*/React.createElement("span", {
className: "field-hint"
}, "created if missing; backlog.md appended for directories")), /*#__PURE__*/React.createElement("input", {
className: "text-input",
value: projectPath,
onChange: e => setProjectPath(e.target.value),
placeholder: "/home/user/project/backlog.md"
}))), /*#__PURE__*/React.createElement("div", {
className: "dlg-foot"
}, mode === "view" ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
className: "dlg-hint"
}, "Read-only \xB7 archived ", initial?.archived || ''), /*#__PURE__*/React.createElement("div", {
className: "dlg-foot-actions"
}, /*#__PURE__*/React.createElement("button", {
type: "button",
className: "btn-secondary",
onClick: onClose
}, "Close"))) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
className: "dlg-hint"
}, "\u2318\u21A9 to save \xB7 Esc to cancel"), /*#__PURE__*/React.createElement("div", {
className: "dlg-foot-actions"
}, /*#__PURE__*/React.createElement("button", {
type: "button",
className: "btn-secondary",
onClick: onClose
}, "Cancel"), /*#__PURE__*/React.createElement("button", {
type: "submit",
className: "btn-primary",
disabled: showPathInput ? !projectPath.trim() : !title.trim()
}, mode === "edit" ? "Save changes" : showPathInput ? "Register project" : "Create item"))))));
}
// --- Confirm dialog ---
function ConfirmDialog({
open,
title,
message,
detail,
checkbox,
confirmLabel = "Confirm",
danger = false,
onCancel,
onConfirm
}) {
const [checked, setChecked] = useStateD(false);
useEffectD(() => {
if (open) setChecked(false);
}, [open]);
return /*#__PURE__*/React.createElement(Dialog, {
open: open,
onClose: onCancel,
width: 420,
labelledBy: "dlg-confirm-title"
}, /*#__PURE__*/React.createElement(DialogHeader, {
id: "dlg-confirm-title",
eyebrow: "Confirm",
title: title,
onClose: onCancel
}), /*#__PURE__*/React.createElement("div", {
className: "dlg-body"
}, /*#__PURE__*/React.createElement("p", {
className: "dlg-msg"
}, message), detail && /*#__PURE__*/React.createElement("div", {
className: "dlg-detail"
}, detail), checkbox && /*#__PURE__*/React.createElement("label", {
className: "dlg-checkbox"
}, /*#__PURE__*/React.createElement("input", {
type: "checkbox",
checked: checked,
onChange: e => setChecked(e.target.checked)
}), /*#__PURE__*/React.createElement("span", null, checkbox))), /*#__PURE__*/React.createElement("div", {
className: "dlg-foot"
}, /*#__PURE__*/React.createElement("span", null), /*#__PURE__*/React.createElement("div", {
className: "dlg-foot-actions"
}, /*#__PURE__*/React.createElement("button", {
type: "button",
className: "btn-secondary",
onClick: onCancel
}, "Cancel"), /*#__PURE__*/React.createElement("button", {
type: "button",
className: danger ? "btn-danger" : "btn-primary",
onClick: () => onConfirm(checked),
autoFocus: true
}, confirmLabel))));
}
// --- Prompt dialog (styled input) ---
function PromptDialog({
open,
title,
message,
placeholder = "",
confirmLabel = "OK",
onCancel,
onConfirm
}) {
const [value, setValue] = useStateD("");
const inputRef = useRefD(null);
useEffectD(() => {
if (open) {
setValue("");
setTimeout(() => inputRef.current?.focus(), 50);
}
}, [open]);
const submit = e => {
e?.preventDefault();
if (!value.trim()) return;
onConfirm(value.trim());
};
return /*#__PURE__*/React.createElement(Dialog, {
open: open,
onClose: onCancel,
width: 480,
labelledBy: "dlg-prompt-title"
}, /*#__PURE__*/React.createElement("form", {
onSubmit: submit
}, /*#__PURE__*/React.createElement(DialogHeader, {
id: "dlg-prompt-title",
eyebrow: "Input",
title: title,
onClose: onCancel
}), /*#__PURE__*/React.createElement("div", {
className: "dlg-body"
}, /*#__PURE__*/React.createElement("p", {
className: "dlg-msg"
}, message), /*#__PURE__*/React.createElement("input", {
ref: inputRef,
className: "text-input",
value: value,
onChange: e => setValue(e.target.value),
placeholder: placeholder,
style: {
marginTop: 12
}
})), /*#__PURE__*/React.createElement("div", {
className: "dlg-foot"
}, /*#__PURE__*/React.createElement("span", null), /*#__PURE__*/React.createElement("div", {
className: "dlg-foot-actions"
}, /*#__PURE__*/React.createElement("button", {
type: "button",
className: "btn-secondary",
onClick: onCancel
}, "Cancel"), /*#__PURE__*/React.createElement("button", {
type: "submit",
className: "btn-primary",
disabled: !value.trim()
}, confirmLabel)))));
}
// --- Tag autocomplete (typed input + filtered dropdown) ---
function TagAutocompleteInput({
tags,
setTags,
tagInput,
setTagInput,
addTag,
removeTag,
allTags
}) {
const [focused, setFocused] = useStateD(false);
const [highlighted, setHighlighted] = useStateD(0);
const inputRef = useRefD(null);
const wrapRef = useRefD(null);
const q = tagInput.trim().toLowerCase().replace(/^#/, "");
// suggestions: filter out already-applied; rank exact prefix > contains
const pool = (allTags || []).filter(t => !tags.includes(t));
let suggestions;
if (q) {
const starts = pool.filter(t => t.toLowerCase().startsWith(q));
const contains = pool.filter(t => !t.toLowerCase().startsWith(q) && t.toLowerCase().includes(q));
suggestions = [...starts, ...contains].slice(0, 8);
} else {
suggestions = pool.slice(0, 8); // recents
}
const exactMatch = q && pool.some(t => t.toLowerCase() === q);
const showCreate = q && !exactMatch && !tags.includes(q);
// index 0 = "create new" (if shown), then suggestions
const items = [...(showCreate ? [{
kind: "create",
value: q
}] : []), ...suggestions.map(t => ({
kind: "suggestion",
value: t
}))];
useEffectD(() => {
setHighlighted(0);
}, [tagInput, focused]);
const pick = it => {
if (!it) return;
addTag(it.value);
setTagInput("");
setHighlighted(0);
};
const onKey = e => {
if (e.key === "ArrowDown") {
e.preventDefault();
if (items.length) setHighlighted(h => (h + 1) % items.length);
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (items.length) setHighlighted(h => (h - 1 + items.length) % items.length);
} else if (e.key === "Enter") {
e.preventDefault();
if (items[highlighted]) pick(items[highlighted]);else if (tagInput.trim()) {
addTag(tagInput);
setTagInput("");
}
} else if (e.key === "," || e.key === "Tab") {
if (tagInput.trim()) {
e.preventDefault();
if (items[highlighted]) pick(items[highlighted]);else {
addTag(tagInput);
setTagInput("");
}
}
} else if (e.key === "Backspace" && !tagInput && tags.length) {
removeTag(tags[tags.length - 1]);
} else if (e.key === "Escape") {
if (tagInput) {
setTagInput("");
e.preventDefault();
} else inputRef.current?.blur();
}
};
return /*#__PURE__*/React.createElement("div", {
className: "tag-ac",
ref: wrapRef
}, /*#__PURE__*/React.createElement("div", {
className: `tag-input ${focused ? "focused" : ""}`,
onClick: e => {
if (e.target === e.currentTarget) inputRef.current?.focus();
}
}, tags.map(t => /*#__PURE__*/React.createElement("span", {
key: t,
className: "tag-pill"
}, "#", t, /*#__PURE__*/React.createElement("button", {
type: "button",
onClick: () => removeTag(t),
"aria-label": `Remove ${t}`
}, "\xD7"))), /*#__PURE__*/React.createElement("input", {
ref: inputRef,
className: "tag-input-field",
placeholder: tags.length ? "+ add tag" : "e.g. urgent, frontend, errand",
value: tagInput,
onChange: e => setTagInput(e.target.value),
onFocus: () => setFocused(true),
onBlur: () => setTimeout(() => setFocused(false), 150),
onKeyDown: onKey,
autoComplete: "off",
"aria-autocomplete": "list",
"aria-expanded": focused && items.length > 0
})), focused && items.length > 0 && /*#__PURE__*/React.createElement("div", {
className: "tag-ac-pop",
role: "listbox"
}, !q && /*#__PURE__*/React.createElement("div", {
className: "tag-ac-cap"
}, "Recent"), items.map((it, i) => /*#__PURE__*/React.createElement("button", {
key: `${it.kind}-${it.value}`,
type: "button",
role: "option",
"aria-selected": i === highlighted,
className: `tag-ac-item ${i === highlighted ? "highlighted" : ""} ${it.kind === "create" ? "create" : ""}`,
onMouseEnter: () => setHighlighted(i),
onMouseDown: e => {
e.preventDefault();
pick(it);
}
}, it.kind === "create" ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Icon, {
name: "plus",
size: 11
}), /*#__PURE__*/React.createElement("span", null, "Create ", /*#__PURE__*/React.createElement("strong", null, "#", it.value))) : /*#__PURE__*/React.createElement("span", null, q ? /*#__PURE__*/React.createElement(TagMatchHL, {
text: it.value,
match: q
}) : `#${it.value}`))), /*#__PURE__*/React.createElement("div", {
className: "tag-ac-foot"
}, /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement("kbd", null, "\u2191"), /*#__PURE__*/React.createElement("kbd", null, "\u2193"), " to navigate"), /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement("kbd", null, "\u21B5"), " to add"), /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement("kbd", null, "esc"), " to dismiss"))));
}
function TagMatchHL({
text,
match
}) {
const i = text.toLowerCase().indexOf(match);
if (i < 0) return `#${text}`;
return /*#__PURE__*/React.createElement(React.Fragment, null, "#", text.slice(0, i), /*#__PURE__*/React.createElement("span", {
className: "hl"
}, text.slice(i, i + match.length)), text.slice(i + match.length));
}
window.Dialog = Dialog;
window.ItemDialog = ItemDialog;
window.ConfirmDialog = ConfirmDialog;
window.PromptDialog = PromptDialog;
window.RichTextEditor = RichTextEditor;
</script>
<script>
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
// Admin / Maintenance page: health, storage stats, backup browser, stats, manual actions.
// ---- Range-aware stats engine -----------------------------------------------
// Generates deterministic per-bucket counts based on seed numbers so switching
// range feels real (each range animates between fixed shapes, not random noise).
const RANGE_OPTIONS = [{
key: "week",
label: "This week",
buckets: 7,
bucketLabel: "d",
granularity: "day"
}, {
key: "month",
label: "This month",
buckets: 5,
bucketLabel: "w",
granularity: "week"
}, {
key: "year",
label: "This year",
buckets: 12,
bucketLabel: "m",
granularity: "month"
}, {
key: "7d",
label: "Last 7d",
buckets: 7,
bucketLabel: "d",
granularity: "day"
}, {
key: "30d",
label: "Last 30d",
buckets: 30,
bucketLabel: "d",
granularity: "day"
}, {
key: "365d",
label: "Last 365d",
buckets: 12,
bucketLabel: "m",
granularity: "month"
}];
// Returns [{start, end}] bucket boundaries matching bucketLabelsFor(rangeKey)
function bucketBounds(rangeKey, refDate) {
const d = new Date(refDate);
if (rangeKey === 'week') {
const dow = (d.getDay() + 6) % 7;
const mon = new Date(d.getFullYear(), d.getMonth(), d.getDate() - dow);
return Array.from({
length: 7
}, (_, i) => {
const s = new Date(mon);
s.setDate(mon.getDate() + i);
const e = new Date(s);
e.setDate(s.getDate() + 1);
return {
start: s,
end: e
};
});
}
if (rangeKey === '7d') {
return Array.from({
length: 7
}, (_, i) => {
const offset = 6 - i;
const s = new Date(d.getFullYear(), d.getMonth(), d.getDate() - offset);
const e = new Date(s);
e.setDate(s.getDate() + 1);
return {
start: s,
end: e
};
});
}
if (rangeKey === '30d') {
return Array.from({
length: 30
}, (_, i) => {
const offset = 29 - i;
const s = new Date(d.getFullYear(), d.getMonth(), d.getDate() - offset);
const e = new Date(s);
e.setDate(s.getDate() + 1);
return {
start: s,
end: e
};
});
}
if (rangeKey === 'month') {
const y = d.getFullYear(),
mo = d.getMonth();
return Array.from({
length: 5
}, (_, i) => ({
start: new Date(y, mo, 1 + i * 7),
end: i < 4 ? new Date(y, mo, 1 + (i + 1) * 7) : new Date(y, mo + 1, 1)
}));
}
if (rangeKey === 'year') {
const y = d.getFullYear();
return Array.from({
length: 12
}, (_, i) => ({
start: new Date(y, i, 1),
end: new Date(y, i + 1, 1)
}));
}
if (rangeKey === '365d') {
return Array.from({
length: 12
}, (_, i) => {
const offset = 11 - i;
const y = d.getFullYear(),
mo = d.getMonth();
return {
start: new Date(y, mo - offset, 1),
end: new Date(y, mo - offset + 1, 1)
};
});
}
return [];
}
function countInBuckets(history, buckets) {
const keys = ['created', 'in-progress', 'blocked', 'postponed', 'done', 'cancelled'];
const out = Object.fromEntries(keys.map(k => [k, Array(buckets.length).fill(0)]));
for (const h of history) {
const ts = new Date(h.timestamp);
const idx = buckets.findIndex(b => ts >= b.start && ts < b.end);
if (idx < 0) continue;
if (h.action === 'item_created') {
out.created[idx]++;
} else if (h.action === 'status_changed') {
const m = h.details.match(/→\s*(\S[\s\S]*?)$/);
if (m) {
const ns = m[1].trim();
if (ns in out) out[ns][idx]++;
}
}
}
return out;
}
function bucketLabelsFor(rangeKey) {
const today = new Date();
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
if (rangeKey === "week") {
return days;
}
if (rangeKey === "7d") {
const out = [];
for (let i = 6; i >= 0; i--) {
const d = new Date(today);
d.setDate(today.getDate() - i);
out.push(days[(d.getDay() + 6) % 7]);
}
return out;
}
if (rangeKey === "30d") {
const out = [];
for (let i = 29; i >= 0; i--) {
const d = new Date(today);
d.setDate(today.getDate() - i);
out.push(String(d.getDate()));
}
return out;
}
if (rangeKey === "month") {
return ["W1", "W2", "W3", "W4", "W5"];
}
if (rangeKey === "year") {
return months;
}
if (rangeKey === "365d") {
const out = [];
for (let i = 11; i >= 0; i--) {
const d = new Date(today);
d.setMonth(today.getMonth() - i);
out.push(months[d.getMonth()]);
}
return out;
}
return [];
}
// Status keys in the order we want them stacked (bottom → top).
// "created" is a synthetic bucket: items that entered the system in that period.
const CHART_STATUSES = [{
key: "created",
label: "Created",
color: "oklch(0.78 0.13 220)"
},
// sky cyan-blue
{
key: "in-progress",
label: "In progress",
color: "oklch(0.72 0.14 270)"
},
// lavender
{
key: "blocked",
label: "Blocked",
color: "oklch(0.68 0.20 28)"
},
// red — fixed (0° HSL)
{
key: "postponed",
label: "Postponed",
color: "oklch(0.83 0.16 90)"
},
// bright golden yellow — bisector of red↔green (≈70° HSL)
{
key: "done",
label: "Done",
color: "oklch(0.68 0.16 150)"
},
// green — fixed (140° HSL)
{
key: "cancelled",
label: "Cancelled",
color: "oklch(0.62 0.04 280)"
} // slate
];
function computeRangeStats(rangeKey, dataStats, history) {
const opt = RANGE_OPTIONS.find(r => r.key === rangeKey);
const labels = bucketLabelsFor(rangeKey);
const now = new Date();
// Prior-period reference: shift back by one equivalent period
const priorRef = new Date(now);
if (rangeKey === 'week' || rangeKey === '7d') priorRef.setDate(priorRef.getDate() - 7);else if (rangeKey === 'month' || rangeKey === '30d') priorRef.setMonth(priorRef.getMonth() - 1);else if (rangeKey === 'year' || rangeKey === '365d') priorRef.setFullYear(priorRef.getFullYear() - 1);
const curr = countInBuckets(history, bucketBounds(rangeKey, now));
const prior = countInBuckets(history, bucketBounds(rangeKey, priorRef));
const series = {
created: curr.created,
'in-progress': curr['in-progress'],
blocked: curr.blocked,
postponed: curr.postponed,
done: curr.done,
cancelled: curr.cancelled
};
const sum = arr => arr.reduce((a, b) => a + b, 0);
const totalCreated = sum(curr.created);
const totalCompleted = sum(curr.done);
const priorCreated = sum(prior.created);
const priorCompleted = sum(prior.done);
const pct = (c, p) => p === 0 ? c > 0 ? 100 : 0 : Math.round((c - p) / p * 100);
const periodWeeks = {
week: 1,
'7d': 1,
month: 4.3,
'30d': 4.3,
year: 52,
'365d': 52
}[rangeKey];
const throughput = +(totalCompleted / periodWeeks).toFixed(1);
const priorThroughput = +(priorCompleted / periodWeeks).toFixed(1);
const completionRate = totalCreated > 0 ? Math.round(totalCompleted / totalCreated * 100) : 0;
const priorCompletionRate = priorCreated > 0 ? Math.round(priorCompleted / priorCreated * 100) : 0;
const cycle = dataStats.avgInProgressDays ?? 0;
return {
opt,
labels,
series,
totals: {
created: totalCreated,
completed: totalCompleted
},
kpis: {
created: {
value: totalCreated,
delta: pct(totalCreated, priorCreated)
},
completed: {
value: totalCompleted,
delta: pct(totalCompleted, priorCompleted)
},
throughput: {
value: throughput,
delta: pct(throughput, priorThroughput),
unit: '/wk'
},
cycle: {
value: cycle,
delta: 0,
unit: 'd',
deltaInverse: true
},
rate: {
value: completionRate,
delta: completionRate - priorCompletionRate,
unit: '%'
}
}
};
}
function StatsOverview({
dataStats,
mostActiveProject,
history
}) {
const [range, setRange] = useState("week");
const [chartMode, setChartMode] = useState("cumulative"); // cumulative | breakdown
const [hiddenStatuses, setHiddenStatuses] = useState(new Set());
const stats = useMemo(() => computeRangeStats(range, dataStats, history), [range, dataStats, history]);
const visibleStatuses = CHART_STATUSES.filter(s => !hiddenStatuses.has(s.key));
// Both modes show stacked totals; max is bucket sum across visible series.
const maxBar = useMemo(() => {
let m = 1;
for (let i = 0; i < stats.labels.length; i++) {
const s = visibleStatuses.reduce((acc, st) => acc + stats.series[st.key][i], 0);
if (s > m) m = s;
}
return m;
}, [stats, visibleStatuses]);
const dense = stats.labels.length > 12;
const labelStride = dense ? Math.ceil(stats.labels.length / 8) : 1;
const toggleStatus = key => {
setHiddenStatuses(prev => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);else next.add(key);
return next;
});
};
return /*#__PURE__*/React.createElement("section", {
className: "card span-2 stats-card"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head stats-head"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("h3", null, "Stats overview"), /*#__PURE__*/React.createElement("span", {
className: "card-sub"
}, stats.opt.label.toLowerCase(), " \xB7 most active in ", /*#__PURE__*/React.createElement("strong", null, mostActiveProject))), /*#__PURE__*/React.createElement("div", {
className: "range-switch",
role: "tablist",
"aria-label": "Stats range"
}, RANGE_OPTIONS.map(opt => /*#__PURE__*/React.createElement("button", {
key: opt.key,
role: "tab",
"aria-selected": range === opt.key,
className: `range-btn ${range === opt.key ? "active" : ""}`,
onClick: () => setRange(opt.key)
}, opt.label)))), /*#__PURE__*/React.createElement("div", {
className: "kpi-grid"
}, /*#__PURE__*/React.createElement(Kpi, _extends({
label: "Created"
}, stats.kpis.created)), /*#__PURE__*/React.createElement(Kpi, _extends({
label: "Completed"
}, stats.kpis.completed)), /*#__PURE__*/React.createElement(Kpi, _extends({
label: "Throughput"
}, stats.kpis.throughput)), /*#__PURE__*/React.createElement(Kpi, _extends({
label: "Avg cycle"
}, stats.kpis.cycle)), /*#__PURE__*/React.createElement(Kpi, _extends({
label: "Completion rate"
}, stats.kpis.rate))), /*#__PURE__*/React.createElement("div", {
className: "chart-head"
}, /*#__PURE__*/React.createElement("div", {
className: "chart-legend",
role: "group",
"aria-label": "Toggle status series"
}, chartMode === "breakdown" ? CHART_STATUSES.map(s => {
const off = hiddenStatuses.has(s.key);
return /*#__PURE__*/React.createElement("button", {
key: s.key,
className: `legend-chip ${off ? "off" : ""}`,
onClick: () => toggleStatus(s.key),
title: off ? `Show ${s.label}` : `Hide ${s.label}`
}, /*#__PURE__*/React.createElement("span", {
className: "lg-swatch",
style: {
background: s.color
}
}), s.label);
}) : /*#__PURE__*/React.createElement("span", {
className: "legend-chip static"
}, /*#__PURE__*/React.createElement("span", {
className: "lg-swatch",
style: {
background: "var(--accent)"
}
}), "Total activity")), /*#__PURE__*/React.createElement("div", {
className: "mode-switch",
role: "tablist",
"aria-label": "Chart mode"
}, /*#__PURE__*/React.createElement("button", {
className: `mode-btn ${chartMode === "cumulative" ? "active" : ""}`,
onClick: () => setChartMode("cumulative"),
"aria-selected": chartMode === "cumulative",
role: "tab",
title: "Cumulative \u2014 single bar per bucket"
}, /*#__PURE__*/React.createElement(ModeIconCumulative, null), " Cumulative"), /*#__PURE__*/React.createElement("button", {
className: `mode-btn ${chartMode === "breakdown" ? "active" : ""}`,
onClick: () => setChartMode("breakdown"),
"aria-selected": chartMode === "breakdown",
role: "tab",
title: "Breakdown \u2014 stacked segments per status"
}, /*#__PURE__*/React.createElement(ModeIconBreakdown, null), " Breakdown"))), /*#__PURE__*/React.createElement("div", {
className: `stack-bars buckets-${stats.labels.length} mode-${chartMode}`
}, stats.labels.map((lbl, i) => {
const showLabel = i % labelStride === 0;
const bucketTotal = visibleStatuses.reduce((acc, st) => acc + stats.series[st.key][i], 0);
const tip = chartMode === "breakdown" ? visibleStatuses.map(st => `${st.label} ${stats.series[st.key][i]}`).join(" · ") : `total ${bucketTotal}`;
// Pre-compute % heights and bottoms for breakdown so segments stack
// without flex rounding errors. Bottom = sum of segments below.
let acc = 0;
const segs = chartMode === "breakdown" ? visibleStatuses.map(st => {
const v = stats.series[st.key][i];
if (v === 0) return null;
const heightPct = v / maxBar * 100;
const bottomPct = acc / maxBar * 100;
acc += v;
return {
st,
v,
heightPct,
bottomPct
};
}).filter(Boolean) : [];
return /*#__PURE__*/React.createElement("div", {
key: i,
className: "stack-col",
title: `${lbl} — ${tip}`
}, chartMode === "breakdown" ? /*#__PURE__*/React.createElement("div", {
className: "stack-stack"
}, segs.map((s, idx) => /*#__PURE__*/React.createElement("div", {
key: s.st.key,
className: `stack-seg ${idx === segs.length - 1 ? "top" : ""}`,
style: {
background: s.st.color,
height: `${s.heightPct}%`,
bottom: `${s.bottomPct}%`
},
"aria-label": `${s.st.label} ${s.v}`
}))) : /*#__PURE__*/React.createElement("div", {
className: "stack-stack cumulative-stack"
}, /*#__PURE__*/React.createElement("div", {
className: "stack-seg cumulative top",
style: {
height: `${bucketTotal / maxBar * 100}%`,
bottom: 0
},
"aria-label": `total ${bucketTotal}`
})), /*#__PURE__*/React.createElement("div", {
className: "stack-cap"
}, showLabel ? lbl : "", showLabel && bucketTotal > 0 && /*#__PURE__*/React.createElement("span", {
className: "stack-total"
}, bucketTotal)));
})));
}
const ModeIconCumulative = () => /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "12",
viewBox: "0 0 14 12",
fill: "none",
"aria-hidden": true
}, /*#__PURE__*/React.createElement("rect", {
x: "0.5",
y: "6",
width: "3",
height: "5",
fill: "currentColor"
}), /*#__PURE__*/React.createElement("rect", {
x: "5.5",
y: "3",
width: "3",
height: "8",
fill: "currentColor"
}), /*#__PURE__*/React.createElement("rect", {
x: "10.5",
y: "5",
width: "3",
height: "6",
fill: "currentColor"
}));
const ModeIconBreakdown = () => /*#__PURE__*/React.createElement("svg", {
width: "14",
height: "12",
viewBox: "0 0 14 12",
fill: "none",
"aria-hidden": true
}, /*#__PURE__*/React.createElement("rect", {
x: "1",
y: "6",
width: "3",
height: "5",
fill: "currentColor",
opacity: "0.4"
}), /*#__PURE__*/React.createElement("rect", {
x: "1",
y: "2",
width: "3",
height: "4",
fill: "currentColor"
}), /*#__PURE__*/React.createElement("rect", {
x: "5.5",
y: "4",
width: "3",
height: "7",
fill: "currentColor",
opacity: "0.4"
}), /*#__PURE__*/React.createElement("rect", {
x: "5.5",
y: "1",
width: "3",
height: "3",
fill: "currentColor"
}), /*#__PURE__*/React.createElement("rect", {
x: "10",
y: "7",
width: "3",
height: "4",
fill: "currentColor",
opacity: "0.4"
}), /*#__PURE__*/React.createElement("rect", {
x: "10",
y: "3",
width: "3",
height: "4",
fill: "currentColor"
}));
function Kpi({
label,
value,
delta,
unit,
accent,
deltaInverse
}) {
// For metrics where lower is better (cycle time), invert sign
const positive = deltaInverse ? delta < 0 : delta > 0;
const negative = deltaInverse ? delta > 0 : delta < 0;
const arrow = delta === 0 ? "→" : positive ? "▲" : "▼";
const cls = delta === 0 ? "flat" : positive ? "up" : "down";
const formatted = Math.abs(delta) === 0 ? "0" : `${Math.abs(delta)}${unit === "%" ? "pp" : "%"}`;
return /*#__PURE__*/React.createElement("div", {
className: `kpi ${accent ? "kpi-accent" : ""}`
}, /*#__PURE__*/React.createElement("div", {
className: "kpi-label"
}, label), /*#__PURE__*/React.createElement("div", {
className: "kpi-value"
}, value, unit && /*#__PURE__*/React.createElement("span", {
className: "kpi-unit"
}, unit)), /*#__PURE__*/React.createElement("div", {
className: `kpi-delta ${cls}`
}, /*#__PURE__*/React.createElement("span", {
className: "kpi-arrow"
}, arrow), " ", formatted, " ", /*#__PURE__*/React.createElement("span", {
className: "kpi-vs"
}, "vs prior")));
}
const ICON_STYLES = [{
value: 'color',
label: 'Color',
hint: 'Filled circles with semantic colors'
}, {
value: 'flat',
label: 'Flat',
hint: 'Minimal monochrome SVG squares'
}, {
value: 'emoji',
label: 'Emoji',
hint: 'Native emoji glyphs'
}, {
value: 'ascii',
label: 'ASCII',
hint: 'Plain text glyphs: [ ] [/] [!] …'
}];
const THEMES = [{
value: 'system',
label: 'System',
hint: 'Follow OS light / dark setting'
}, {
value: 'light',
label: 'Light',
hint: 'Always light'
}, {
value: 'dark',
label: 'Dark',
hint: 'Always dark'
}];
function AppearanceCard({
tweaks,
setTweak
}) {
const current = tweaks?.status_style || 'color';
const theme = tweaks?.theme || 'system';
return /*#__PURE__*/React.createElement("section", {
className: "card"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "Appearance")), /*#__PURE__*/React.createElement("div", {
style: {
fontSize: 11.5,
fontWeight: 600,
color: 'var(--ink-3)',
textTransform: 'uppercase',
letterSpacing: '0.04em',
margin: '4px 0 6px'
}
}, "Theme"), /*#__PURE__*/React.createElement("div", {
className: "seg",
style: {
marginBottom: 14
}
}, THEMES.map(t => /*#__PURE__*/React.createElement("button", {
key: t.value,
className: `seg-btn ${theme === t.value ? 'active' : ''}`,
title: t.hint,
onClick: () => setTweak('theme', t.value)
}, t.label))), /*#__PURE__*/React.createElement("div", {
style: {
fontSize: 11.5,
fontWeight: 600,
color: 'var(--ink-3)',
textTransform: 'uppercase',
letterSpacing: '0.04em',
margin: '4px 0 6px'
}
}, "Status icons"), /*#__PURE__*/React.createElement("div", {
style: {
display: 'flex',
flexDirection: 'column',
gap: 6
}
}, ICON_STYLES.map(opt => /*#__PURE__*/React.createElement("label", {
key: opt.value,
style: {
display: 'flex',
alignItems: 'center',
gap: 10,
padding: '7px 10px',
borderRadius: 8,
cursor: 'pointer',
background: current === opt.value ? 'var(--bg-2)' : 'transparent',
border: `1px solid ${current === opt.value ? 'var(--rule)' : 'transparent'}`
}
}, /*#__PURE__*/React.createElement("input", {
type: "radio",
name: "icon-style",
value: opt.value,
checked: current === opt.value,
onChange: () => setTweak('status_style', opt.value),
style: {
accentColor: 'var(--accent)',
flexShrink: 0
}
}), /*#__PURE__*/React.createElement("div", {
style: {
display: 'flex',
gap: 5,
flexShrink: 0
}
}, ['open', 'in-progress', 'blocked', 'postponed', 'done', 'cancelled'].map(s => /*#__PURE__*/React.createElement(StatusStyleContext.Provider, {
key: s,
value: opt.value
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: s,
size: 14
})))), /*#__PURE__*/React.createElement("div", {
style: {
display: 'flex',
flexDirection: 'column',
gap: 1
}
}, /*#__PURE__*/React.createElement("span", {
style: {
fontSize: 13,
fontWeight: 500,
color: 'var(--ink)'
}
}, opt.label), /*#__PURE__*/React.createElement("span", {
style: {
fontSize: 11.5,
color: 'var(--ink-3)'
}
}, opt.hint))))));
}
// ---- Projects card (api mode only, todo 8) ----
// Registry manager: rows come straight from the server (authoritative size /
// missing flags). Rename/remove reuse the SAME app-level handlers the backlog
// sections use, so the ConfirmDialog-with-checkbox and PromptDialog flows have
// exactly one implementation.
function ProjectsCard({
projects,
onRegisterProject,
onRenameProject,
onRemoveProject
}) {
const propRows = () => (projects || []).map(p => ({
name: p.name,
path: p.path,
size: undefined,
missing: !!p.missing
}));
const [rows, setRows] = useState(propRows);
const [regPath, setRegPath] = useState('');
const [regName, setRegName] = useState('');
const [registering, setRegistering] = useState(false);
// Re-fetch the registry whenever the app-level project set changes
// (register/rename/remove/poller reloads); on failure fall back to the
// app's list so the card never blanks out.
useEffect(() => {
let cancelled = false;
ApiBackend.listProjects().then(list => {
if (!cancelled && Array.isArray(list)) setRows(list);
}).catch(() => {
if (!cancelled) setRows(propRows());
});
return () => {
cancelled = true;
};
}, [projects]);
const submitRegister = e => {
e.preventDefault();
const path = regPath.trim();
if (!path || registering) return;
setRegistering(true);
// onRegisterProject returns a promise; the app toasts success/error.
Promise.resolve(onRegisterProject(path, regName.trim() || null)).finally(() => {
setRegistering(false);
setRegPath('');
setRegName('');
});
};
return /*#__PURE__*/React.createElement("section", {
className: "card span-2"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "Projects"), /*#__PURE__*/React.createElement("span", {
className: "card-sub"
}, rows.length, " registered \xB7 rename and remove act on the registry, not the file")), /*#__PURE__*/React.createElement("table", {
className: "backup-table projects-table"
}, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "Name"), /*#__PURE__*/React.createElement("th", null, "Path"), /*#__PURE__*/React.createElement("th", null, "Size"), /*#__PURE__*/React.createElement("th", null, "Status"), /*#__PURE__*/React.createElement("th", null))), /*#__PURE__*/React.createElement("tbody", null, rows.map(r => /*#__PURE__*/React.createElement("tr", {
key: r.name
}, /*#__PURE__*/React.createElement("td", {
className: "mono"
}, r.name), /*#__PURE__*/React.createElement("td", {
className: "mono projects-path",
title: r.path
}, r.path), /*#__PURE__*/React.createElement("td", null, typeof r.size === 'number' ? fmtBytes(r.size) : /*#__PURE__*/React.createElement("span", {
className: "muted"
}, "\u2014")), /*#__PURE__*/React.createElement("td", null, r.missing ? /*#__PURE__*/React.createElement("span", {
className: "project-missing-badge"
}, "missing") : /*#__PURE__*/React.createElement("span", {
className: "pill mini ok"
}, "\u2713 ok")), /*#__PURE__*/React.createElement("td", {
className: "row-actions-cell"
}, /*#__PURE__*/React.createElement("button", {
className: "row-btn",
disabled: !!r.missing,
onClick: () => onRenameProject(r.name)
}, /*#__PURE__*/React.createElement(Icon, {
name: "edit",
size: 12
}), " Rename"), /*#__PURE__*/React.createElement("button", {
className: "row-btn",
onClick: () => onRemoveProject(r.name)
}, /*#__PURE__*/React.createElement(Icon, {
name: "trash",
size: 12
}), " Remove")))))), /*#__PURE__*/React.createElement("form", {
className: "projects-register",
onSubmit: submitRegister
}, /*#__PURE__*/React.createElement("input", {
type: "text",
className: "projects-input",
placeholder: "/path/to/project \u2014 directory or .md file",
value: regPath,
onChange: e => setRegPath(e.target.value)
}), /*#__PURE__*/React.createElement("input", {
type: "text",
className: "projects-input projects-input-name",
placeholder: "Display name (optional)",
value: regName,
onChange: e => setRegName(e.target.value)
}), /*#__PURE__*/React.createElement("button", {
className: "btn-primary",
type: "submit",
disabled: !regPath.trim() || registering
}, /*#__PURE__*/React.createElement(Icon, {
name: "plus",
size: 12
}), " Register")));
}
function AdminPage({
data,
onClose,
history,
tweaks,
setTweak,
onForceSave,
onForceBackup,
onCompact,
onRestore,
onDownloadBackup,
archiveData,
onArchiveItems,
onRestoreItems,
onViewItem,
storageMode,
projects,
onRegisterProject,
onRenameProject,
onRemoveProject
}) {
const [previewBackup, setPreviewBackup] = useState(null);
const counts = countByStatus(data.entries);
const total = countAll(data.entries);
const maxStat = Math.max(1, ...Object.values(data.stats.statusMix));
return /*#__PURE__*/React.createElement("div", {
className: "admin"
}, /*#__PURE__*/React.createElement("div", {
className: "admin-head"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
className: "eyebrow"
}, "Admin \xB7 Maintenance"), /*#__PURE__*/React.createElement("h2", null, "System overview")), /*#__PURE__*/React.createElement("button", {
className: "btn-secondary",
onClick: onClose
}, "\u2190 Back to backlog")), /*#__PURE__*/React.createElement("div", {
className: "admin-grid"
}, /*#__PURE__*/React.createElement("section", {
className: "card span-2"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "System health"), /*#__PURE__*/React.createElement("span", {
className: `pill ${data.health.integrityOk ? "ok" : "warn"}`
}, data.health.integrityOk ? "✓ integrity ok" : "checksum mismatch")), /*#__PURE__*/React.createElement("dl", {
className: "kv"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "Last save"), /*#__PURE__*/React.createElement("dd", null, fmtTimestamp(data.health.lastSave))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "Last backup"), /*#__PURE__*/React.createElement("dd", null, fmtTimestamp(data.health.lastBackup))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "Storage mode"), /*#__PURE__*/React.createElement("dd", null, data.health.mode)), data.health.masterPath && /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "File path"), /*#__PURE__*/React.createElement("dd", {
className: "mono small"
}, data.health.masterPath)), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "Checksum"), /*#__PURE__*/React.createElement("dd", {
className: "mono small"
}, data.meta.checksum.slice(0, 32), "\u2026")), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "Saved at"), /*#__PURE__*/React.createElement("dd", {
className: "mono small"
}, data.meta.saved)), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "Entries / history"), /*#__PURE__*/React.createElement("dd", null, data.meta.entryCount, " \xB7 ", data.meta.historyCount)))), /*#__PURE__*/React.createElement("section", {
className: "card"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "Storage"), /*#__PURE__*/React.createElement("span", {
className: "card-sub mono"
}, fmtBytes(data.health.masterSize + data.health.backupDirSize + data.health.statsSize + data.health.historySize), " total")), /*#__PURE__*/React.createElement("dl", {
className: "kv"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "backlog.md"), /*#__PURE__*/React.createElement("dd", null, fmtBytes(data.health.masterSize), " \xB7 ", data.meta.entryCount, " entries")), data.health.masterPath && /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "Path"), /*#__PURE__*/React.createElement("dd", {
className: "mono small"
}, data.health.masterPath)), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "history.jsonl"), /*#__PURE__*/React.createElement("dd", null, fmtBytes(data.health.historySize), " \xB7 ", data.meta.historyCount.toLocaleString(), " events")), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "backups/"), /*#__PURE__*/React.createElement("dd", null, fmtBytes(data.health.backupDirSize), " \xB7 ", data.health.backupCount, " files")), data.health.backupsPath && /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "Backups"), /*#__PURE__*/React.createElement("dd", {
className: "mono small"
}, data.health.backupsPath)), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "stats.jsonl"), /*#__PURE__*/React.createElement("dd", null, fmtBytes(data.health.statsSize), " \xB7 90-day rollup")), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("dt", null, "Oldest event"), /*#__PURE__*/React.createElement("dd", {
className: "mono small"
}, fmtTimestamp(data.health.historyOldest))))), storageMode === 'api' && /*#__PURE__*/React.createElement(ProjectsCard, {
projects: projects,
onRegisterProject: onRegisterProject,
onRenameProject: onRenameProject,
onRemoveProject: onRemoveProject
}), /*#__PURE__*/React.createElement("section", {
className: "card"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "Manual actions")), /*#__PURE__*/React.createElement("div", {
className: "action-list"
}, /*#__PURE__*/React.createElement("button", {
className: "action-btn",
onClick: onForceSave
}, /*#__PURE__*/React.createElement(Icon, {
name: "save",
size: 14
}), /*#__PURE__*/React.createElement("span", {
className: "action-text"
}, /*#__PURE__*/React.createElement("span", {
className: "action-label"
}, "Force save"), /*#__PURE__*/React.createElement("span", {
className: "action-meta"
}, "last save ", fmtRelative(data.health.lastSave)))), /*#__PURE__*/React.createElement("button", {
className: "action-btn",
onClick: onForceBackup
}, /*#__PURE__*/React.createElement(Icon, {
name: "archive",
size: 14
}), /*#__PURE__*/React.createElement("span", {
className: "action-text"
}, /*#__PURE__*/React.createElement("span", {
className: "action-label"
}, "Force backup"), /*#__PURE__*/React.createElement("span", {
className: "action-meta"
}, data.health.backupCount, " files \xB7 ", fmtBytes(data.health.backupDirSize)))), /*#__PURE__*/React.createElement("button", {
className: "action-btn",
onClick: onCompact
}, /*#__PURE__*/React.createElement(Icon, {
name: "refresh",
size: 14
}), /*#__PURE__*/React.createElement("span", {
className: "action-text"
}, /*#__PURE__*/React.createElement("span", {
className: "action-label"
}, "Compact history"), /*#__PURE__*/React.createElement("span", {
className: "action-meta"
}, data.meta.historyCount.toLocaleString(), " events \xB7 ", fmtBytes(data.health.historySize), " \xB7 since ", fmtShortDate(data.health.historyOldest)))))), /*#__PURE__*/React.createElement(StatsOverview, {
dataStats: data.stats,
mostActiveProject: data.stats.mostActiveProject,
history: history
}), /*#__PURE__*/React.createElement(AppearanceCard, {
tweaks: tweaks,
setTweak: setTweak
}), /*#__PURE__*/React.createElement("section", {
className: "card"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "Status mix")), /*#__PURE__*/React.createElement("div", {
className: "status-mix"
}, STATUSES.map(s => {
const v = data.stats.statusMix[s.key] || 0;
return /*#__PURE__*/React.createElement("div", {
key: s.key,
className: "mix-row"
}, /*#__PURE__*/React.createElement("span", {
className: `glyph status-${s.key}`
}, s.glyph), /*#__PURE__*/React.createElement("span", {
className: "mix-label"
}, s.label), /*#__PURE__*/React.createElement("span", {
className: "mix-bar"
}, /*#__PURE__*/React.createElement("span", {
className: "mix-fill",
style: {
width: `${v / maxStat * 100}%`
}
})), /*#__PURE__*/React.createElement("span", {
className: "mix-num"
}, v));
}))), /*#__PURE__*/React.createElement("section", {
className: "card span-3"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "Backup browser"), /*#__PURE__*/React.createElement("span", {
className: "card-sub"
}, data.backups.length, " files \xB7 keeping \u22657 days, \u226530 days of dailies")), /*#__PURE__*/React.createElement("table", {
className: "backup-table"
}, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "Filename"), /*#__PURE__*/React.createElement("th", null, "Timestamp"), /*#__PURE__*/React.createElement("th", null, "Size"), /*#__PURE__*/React.createElement("th", null, "Entries"), /*#__PURE__*/React.createElement("th", null, "Integrity"), /*#__PURE__*/React.createElement("th", null))), /*#__PURE__*/React.createElement("tbody", null, data.backups.map(b => /*#__PURE__*/React.createElement("tr", {
key: b.name,
className: previewBackup === b.name ? "active" : ""
}, /*#__PURE__*/React.createElement("td", {
className: "mono"
}, b.name), /*#__PURE__*/React.createElement("td", null, fmtTimestamp(b.timestamp)), /*#__PURE__*/React.createElement("td", null, fmtBytes(b.size)), /*#__PURE__*/React.createElement("td", null, b.entries ?? /*#__PURE__*/React.createElement("span", {
className: "muted"
}, "\u2014")), /*#__PURE__*/React.createElement("td", null, b.valid ? /*#__PURE__*/React.createElement("span", {
className: "pill mini ok"
}, "\u2713 valid") : /*#__PURE__*/React.createElement("span", {
className: "pill mini warn"
}, /*#__PURE__*/React.createElement(Icon, {
name: "warn",
size: 10
}), " mismatch")), /*#__PURE__*/React.createElement("td", {
className: "row-actions-cell"
}, /*#__PURE__*/React.createElement("button", {
className: "row-btn",
onClick: () => setPreviewBackup(previewBackup === b.name ? null : b.name)
}, /*#__PURE__*/React.createElement(Icon, {
name: "eye",
size: 12
}), " Preview"), /*#__PURE__*/React.createElement("button", {
className: "row-btn",
onClick: () => onDownloadBackup(b.name)
}, /*#__PURE__*/React.createElement(Icon, {
name: "download",
size: 12
}), " Download"), /*#__PURE__*/React.createElement("button", {
className: "row-btn primary",
onClick: () => onRestore(b)
}, "Restore")))))), previewBackup && /*#__PURE__*/React.createElement("div", {
className: "backup-preview"
}, /*#__PURE__*/React.createElement("div", {
className: "preview-head"
}, /*#__PURE__*/React.createElement("span", {
className: "mono"
}, previewBackup), /*#__PURE__*/React.createElement("span", {
className: "card-sub"
}, "read-only preview")), /*#__PURE__*/React.createElement("pre", {
className: "preview-body"
}, `# Backlog
<!-- SECTION: ENTRIES -->
## [P0] 🔥 Personal backlog app
- [x] Markdown parser + integrity marker *(done: 2026-04-29)*
- [x] Tokenize three sections
- [x] SHA-256 over entries+history
- [/] Tree renderer with parent-visibility filter *(due: 2026-05-04)*
- [ ] Expand / collapse animations
- [ ] Drag handle within priority group *(due: 2026-05-06)*
- [!] File System Access API direct mode *(reason: Need to test on Edge — no machine yet)*
…
<!-- SECTION: HISTORY -->
| Timestamp | Item ID | Action | Details |
…
<!-- SECTION: INTEGRITY -->
<!-- saved: 2026-04-30T19:44:02Z | checksum: sha256:8a1c3e… | entries: 27 | history: 10 -->`))), /*#__PURE__*/React.createElement(ArchiveCard, {
entries: data.entries,
archiveData: archiveData,
onArchiveItems: onArchiveItems,
onRestoreItems: onRestoreItems,
onViewItem: onViewItem,
projects: storageMode === 'api' ? projects : []
}), /*#__PURE__*/React.createElement("section", {
className: "card span-3"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "Audit log"), /*#__PURE__*/React.createElement("span", {
className: "card-sub"
}, /*#__PURE__*/React.createElement("strong", null, data.meta.historyCount.toLocaleString()), " events \xB7", " ", fmtBytes(data.health.historySize), " \xB7", " ", "since ", fmtShortDate(data.health.historyOldest), " \xB7", " ", "showing latest 10")), /*#__PURE__*/React.createElement("table", {
className: "history-table"
}, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "Timestamp"), /*#__PURE__*/React.createElement("th", null, "Item ID"), /*#__PURE__*/React.createElement("th", null, "Action"), /*#__PURE__*/React.createElement("th", null, "Details"))), /*#__PURE__*/React.createElement("tbody", null, history.slice(0, 10).map((h, i) => /*#__PURE__*/React.createElement("tr", {
key: i
}, /*#__PURE__*/React.createElement("td", {
className: "mono small"
}, h.timestamp), /*#__PURE__*/React.createElement("td", {
className: "mono small"
}, h.itemId), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("span", {
className: "pill mini neutral"
}, h.action.replace(/_/g, " "))), /*#__PURE__*/React.createElement("td", null, h.details))))))));
}
// ---- Archive Manager Card ----
// In multi-project api mode the card scopes to ONE project (select at the
// top): both lists and every count reflect the selection. Writes already
// route per-project in app.jsx (archive/restore group by _projectId), so the
// select is a read-side filter — restore of an item from project X rewrites
// only X's backlog.md + archive.md. Single-project mode renders exactly as
// before (no select).
function ArchiveCard({
entries,
archiveData,
onArchiveItems,
onRestoreItems,
onViewItem,
projects = []
}) {
const [selectedIds, setSelectedIds] = useState([]);
const [filter, setFilter] = useState('done'); // 'done', 'cancelled', 'all'
const [searchTerm, setSearchTerm] = useState('');
const [archiveSearchTerm, setArchiveSearchTerm] = useState('');
const [expandedArchive, setExpandedArchive] = useState(false);
const isMulti = projects.length > 1;
const [archivePid, setArchivePid] = useState(projects[0]?.id ?? null);
// Keep the selection valid across register/rename/remove reloads.
useEffect(() => {
if (isMulti && !projects.some(p => p.id === archivePid)) {
setArchivePid(projects[0]?.id ?? null);
}
}, [projects]);
const pidOf = x => x._projectId ?? projects[0]?.id;
const scopedBacklog = isMulti ? entries.filter(e => pidOf(e) === archivePid) : entries;
const scopedArchive = isMulti ? (archiveData?.entries || []).filter(e => pidOf(e) === archivePid) : archiveData?.entries || [];
// Find archivable items (done or cancelled)
const archivableItems = [];
walkTree(scopedBacklog, item => {
if (filter === 'all' || item.status === filter) {
if (item.status === 'done' || item.status === 'cancelled') {
archivableItems.push(item);
}
}
});
// Filter by search
const filteredItems = archivableItems.filter(it => !searchTerm || it.title.toLowerCase().includes(searchTerm.toLowerCase()));
// Archive entries
const archiveEntries = scopedArchive;
const filteredArchiveItems = [];
walkTree(archiveEntries, item => {
if (!archiveSearchTerm || item.title.toLowerCase().includes(archiveSearchTerm.toLowerCase())) {
filteredArchiveItems.push(item);
}
});
const toggleSelect = id => {
setSelectedIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
};
const selectAll = () => {
setSelectedIds(filteredItems.map(it => it.id));
};
const handleArchive = () => {
if (selectedIds.length > 0) {
onArchiveItems(selectedIds);
setSelectedIds([]);
}
};
return /*#__PURE__*/React.createElement("section", {
className: "card span-3"
}, /*#__PURE__*/React.createElement("div", {
className: "card-head"
}, /*#__PURE__*/React.createElement("h3", null, "Archive manager"), /*#__PURE__*/React.createElement("div", {
className: "card-head-right",
style: {
display: 'flex',
alignItems: 'center',
gap: 10
}
}, isMulti && /*#__PURE__*/React.createElement("select", {
value: archivePid ?? '',
onChange: e => setArchivePid(e.target.value),
className: "archive-filter",
title: "Archive source project",
"aria-label": "Archive source project"
}, projects.map(p => /*#__PURE__*/React.createElement("option", {
key: p.id,
value: p.id
}, p.name))), /*#__PURE__*/React.createElement("span", {
className: "card-sub"
}, archivableItems.length, " archivable \xB7 ", archiveEntries.length, " archived", isMulti ? ` · ${archivePid}` : ''))), /*#__PURE__*/React.createElement("div", {
className: "archive-section"
}, /*#__PURE__*/React.createElement("div", {
className: "archive-toolbar"
}, /*#__PURE__*/React.createElement("span", {
className: "archive-label"
}, "Archivable items"), /*#__PURE__*/React.createElement("select", {
value: filter,
onChange: e => setFilter(e.target.value),
className: "archive-filter"
}, /*#__PURE__*/React.createElement("option", {
value: "done"
}, "Done only"), /*#__PURE__*/React.createElement("option", {
value: "cancelled"
}, "Cancelled only"), /*#__PURE__*/React.createElement("option", {
value: "all"
}, "Done + Cancelled")), /*#__PURE__*/React.createElement("input", {
type: "text",
placeholder: "Search...",
value: searchTerm,
onChange: e => setSearchTerm(e.target.value),
className: "archive-search"
}), /*#__PURE__*/React.createElement("button", {
className: "btn-secondary btn-sm",
onClick: selectAll,
disabled: filteredItems.length === 0
}, "Select all"), /*#__PURE__*/React.createElement("button", {
className: "btn-primary btn-sm",
onClick: handleArchive,
disabled: selectedIds.length === 0
}, "Archive ", selectedIds.length > 0 ? `(${selectedIds.length})` : '')), filteredItems.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "archive-empty"
}, "No archivable items found") : /*#__PURE__*/React.createElement("div", {
className: "archive-list"
}, filteredItems.slice(0, 20).map(it => /*#__PURE__*/React.createElement("label", {
key: it.id,
className: "archive-item"
}, /*#__PURE__*/React.createElement("input", {
type: "checkbox",
checked: selectedIds.includes(it.id),
onChange: () => toggleSelect(it.id)
}), /*#__PURE__*/React.createElement(StatusIcon, {
status: it.status,
size: 14
}), /*#__PURE__*/React.createElement("span", {
className: "archive-item-title"
}, it.title), it.children?.length > 0 && /*#__PURE__*/React.createElement("span", {
className: "archive-item-children"
}, "+", it.children.length, " children"))), filteredItems.length > 20 && /*#__PURE__*/React.createElement("div", {
className: "archive-more"
}, "...and ", filteredItems.length - 20, " more"))), /*#__PURE__*/React.createElement("div", {
className: "archive-section"
}, /*#__PURE__*/React.createElement("div", {
className: "archive-toolbar"
}, /*#__PURE__*/React.createElement("span", {
className: "archive-label"
}, "Archived items (", archiveEntries.length, ")"), /*#__PURE__*/React.createElement("button", {
className: "btn-secondary btn-sm",
onClick: () => setExpandedArchive(!expandedArchive)
}, expandedArchive ? 'Collapse' : 'Expand'), expandedArchive && /*#__PURE__*/React.createElement("input", {
type: "text",
placeholder: "Search archive...",
value: archiveSearchTerm,
onChange: e => setArchiveSearchTerm(e.target.value),
className: "archive-search"
})), expandedArchive && (filteredArchiveItems.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "archive-empty"
}, archiveEntries.length === 0 ? 'Archive is empty' : 'No matches found') : /*#__PURE__*/React.createElement("div", {
className: "archive-list"
}, filteredArchiveItems.slice(0, 30).map(it => /*#__PURE__*/React.createElement("div", {
key: it.id,
className: "archive-item archived"
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: it.status,
size: 14
}), /*#__PURE__*/React.createElement("span", {
className: "archive-item-title",
onDoubleClick: () => onViewItem?.(it)
}, it.title), it.archived && /*#__PURE__*/React.createElement("span", {
className: "archive-item-date"
}, "archived ", it.archived), /*#__PURE__*/React.createElement("button", {
className: "btn-secondary btn-xs",
onClick: () => onRestoreItems([it.id])
}, "Restore"))), filteredArchiveItems.length > 30 && /*#__PURE__*/React.createElement("div", {
className: "archive-more"
}, "...and ", filteredArchiveItems.length - 30, " more")))));
}
window.AdminPage = AdminPage;
window.ArchiveCard = ArchiveCard;
window.ProjectsCard = ProjectsCard;
</script>
<script>
// Main App — top shell, state management, real storage load/save flow.
const {
useState: useStateMain,
useEffect: useEffectMain,
useMemo: useMemoMain,
useRef: useRefMain
} = React;
const LS_KEY = "personal-backlog-state-v1";
function loadLocalState() {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch {
return null;
}
}
// Only UI state (expanded rows, project section collapse, tweaks) is persisted locally.
// Backlog data always lives in backlog.md — never in localStorage.
function saveLocalState(expandedMap, tweaks, projectExpandedMap = {}) {
try {
localStorage.setItem(LS_KEY, JSON.stringify({
expandedMap,
tweaks,
projectExpandedMap
}));
} catch {/* quota / private mode */}
}
// Empty starting state — used when there is no saved data and no seed data loaded.
function buildEmptyData() {
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(day => ({
day,
count: 0
}));
return {
entries: [],
history: [],
meta: {
saved: null,
checksum: '—',
entryCount: 0,
historyCount: 0
},
health: {
integrityOk: true,
lastSave: null,
lastBackup: null,
masterSize: 0,
backupDirSize: 0,
backupCount: 0,
statsSize: 0,
historySize: 0,
historyOldest: null,
mode: 'localStorage only'
},
stats: {
createdThisWeek: 0,
completedThisWeek: 0,
avgInProgressDays: null,
mostActiveProject: '—',
completionByDay: days.map(d => ({
...d
})),
createdByDay: days.map(d => ({
...d
})),
statusMix: {
open: 0,
'in-progress': 0,
blocked: 0,
postponed: 0,
done: 0,
cancelled: 0
}
},
backups: []
};
}
// Build ONE merged data object from per-project loads (api mode). Every entry
// and history row is tagged with _projectId; stats derive through the same
// buildDataFromStorage pipeline as single-project mode. A single registered
// project degenerates cleanly — the merged model is the ONLY api data path.
async function buildMergedDataFromProjects(projectsData, {
sizeInfo = {},
backups = []
} = {}) {
// Parser ids derive from a Date.now() counter, so loadAllProjects' parallel
// per-project parses can mint IDENTICAL ids (same-millisecond start).
// De-collide deterministically per project — stable across reloads — and
// remap that project's history rows to the new id.
const seenIds = new Set();
const allEntries = [];
const taggedHistory = [];
for (const proj of projectsData) {
walkTree(proj.entries, it => {
if (seenIds.has(it.id)) {
const nid = `${it.id}~${proj.id}`;
for (const h of proj.history) if (h.itemId === it.id) h.itemId = nid;
it.id = nid;
}
seenIds.add(it.id);
it._projectId = proj.id;
});
allEntries.push(...proj.entries);
for (const h of proj.history) taggedHistory.push({
...h,
_projectId: proj.id
});
}
taggedHistory.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
const metas = projectsData.map(p => p.meta).filter(Boolean);
const mergedMeta = metas.length ? {
saved: metas.map(m => m.saved).filter(Boolean).sort().pop() ?? null,
checksum: '—',
entryCount: countAll(allEntries),
historyCount: taggedHistory.length
} : null;
const merged = await buildDataFromStorage({
entries: allEntries,
history: taggedHistory,
meta: mergedMeta,
checksumOk: projectsData.every(p => p.checksumOk)
}, backups, 'api', sizeInfo);
// Stats above derive from the merged tree; mostActiveProject is the project
// with the most entries.
let mostActive = '—',
maxCount = -1;
for (const proj of projectsData) {
const n = countAll(proj.entries);
if (n > maxCount) {
maxCount = n;
mostActive = proj.name;
}
}
merged.stats.mostActiveProject = mostActive;
return merged;
}
// Which projects have local changes to persist? An explicit id wins (or an
// array of ids for cross-project moves); otherwise derive from _projectId tags
// on entries AND history rows, with untagged rows falling back to the first
// registered project.
function computeDirtyProjectIds(data, explicitProjectId, fallbackFirst) {
if (Array.isArray(explicitProjectId)) return explicitProjectId.filter(Boolean);
if (explicitProjectId) return [explicitProjectId];
const ids = new Set();
for (const e of data.entries) ids.add(e._projectId ?? fallbackFirst);
for (const h of data.history) ids.add(h._projectId ?? fallbackFirst);
ids.delete(null);
ids.delete(undefined);
return [...ids];
}
// Re-tag a node and all its descendants as belonging to `projectId`.
// Used by cross-project drag&drop (todo 6 drop zones) and project rename.
function retagSubtree(node, projectId) {
walkTree([node], it => {
it._projectId = projectId;
});
}
// Disk counterpart of the in-memory de-collision in buildMergedDataFromProjects:
// strip the merge-only `~<projectId>` suffix from entry ids, history itemIds and
// restore-path segments of THIS project's subset before serializing, so every
// project file stays a standard upstream backlog.md (upstream ids never contain
// `~`). Only an exact `~<projectId>` ending is stripped — other projects'
// suffixes or natural `~` pass through untouched. PURE: the in-memory tree keeps
// its suffixed ids for de-collision; deep-safe copies are returned.
function toDiskIds(entries, history, projectId) {
if (!projectId) return {
entries,
history
};
const suffix = '~' + projectId;
const strip = id => typeof id === 'string' && id.endsWith(suffix) ? id.slice(0, id.length - suffix.length) : id;
const mapEntry = it => ({
...it,
id: strip(it.id),
restorePath: typeof it.restorePath === 'string' && it.restorePath ? it.restorePath.split('/').map(strip).join('/') : it.restorePath,
children: it.children ? it.children.map(mapEntry) : it.children
});
return {
entries: entries.map(mapEntry),
history: history.map(h => ({
...h,
itemId: strip(h.itemId)
}))
};
}
function App() {
const [data, setData] = useStateMain(buildEmptyData);
const [storageMode, setStorageMode] = useStateMain('local'); // 'api' | 'direct' | 'local'
const [isLoading, setIsLoading] = useStateMain(true);
const [view, setView] = useStateMain('backlog');
const [filters, setFilters] = useStateMain({
statuses: [],
priorities: [],
tags: [],
dueRange: null,
scope: 'all',
text: ''
});
const [expandedMap, setExpandedMap] = useStateMain(() => loadLocalState()?.expandedMap ?? {});
const [saveState, setSaveState] = useStateMain({
status: 'idle',
lastSaved: data.health?.lastSave || null
});
const [toast, setToast] = useStateMain(null);
const [showWarning, setShowWarning] = useStateMain(false);
const [importExportOpen, setImportExportOpen] = useStateMain(false);
const [itemDialog, setItemDialog] = useStateMain(null);
const [confirm, setConfirm] = useStateMain(null);
const [needsConnect, setNeedsConnect] = useStateMain(false);
const [archiveData, setArchiveData] = useStateMain({
entries: [],
history: []
});
const [projects, setProjects] = useStateMain([]);
const [projectExpandedMap, setProjectExpandedMap] = useStateMain(() => loadLocalState()?.projectExpandedMap ?? {});
const [prompt, setPrompt] = useStateMain(null); // PromptDialog state {title, message, placeholder, onConfirm}
const TWEAK_DEFAULTS = {
accent_hue: 35,
density: 'comfortable',
show_ids: false,
paper_texture: true,
status_style: 'color',
sort_mode: 'priority',
theme: 'system'
};
const [tweaks, setTweak] = useTweaks({
...TWEAK_DEFAULTS,
...(loadLocalState()?.tweaks ?? {})
});
// Refs for async callbacks that need latest state without stale closures.
const latestData = useRefMain(data);
const latestExpandedMap = useRefMain(expandedMap);
const latestProjects = useRefMain(projects);
const latestProjectExpandedMap = useRefMain(projectExpandedMap);
const isDirtyRef = useRefMain(false);
const saveTimerRef = useRefMain(null);
const saveInFlightRef = useRefMain(null); // promise of the performSave currently running
const pendingSaveArgsRef = useRefMain(null); // { label, projectId } of the debounced save
useEffectMain(() => {
latestData.current = data;
}, [data]);
useEffectMain(() => {
latestExpandedMap.current = expandedMap;
}, [expandedMap]);
useEffectMain(() => {
latestProjects.current = projects;
}, [projects]);
useEffectMain(() => {
latestProjectExpandedMap.current = projectExpandedMap;
}, [projectExpandedMap]);
useEffectMain(() => {
document.documentElement.style.setProperty('--accent-hue', tweaks.accent_hue);
document.documentElement.dataset.density = tweaks.density;
document.documentElement.dataset.showIds = tweaks.show_ids ? 'true' : 'false';
document.documentElement.dataset.paper = tweaks.paper_texture ? 'true' : 'false';
}, [tweaks]);
useEffectMain(() => {
const apply = () => {
const t = tweaks.theme === 'system' ? window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' : tweaks.theme;
document.documentElement.dataset.theme = t;
};
apply();
if (tweaks.theme !== 'system') return;
const mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener('change', apply);
return () => mq.removeEventListener('change', apply);
}, [tweaks.theme]);
// ---- Multi-project loading (api mode) ----
// Full reload of every registered project + merged data rebuild. Used by
// init, 'set-changed' polls, restores, and the admin flows (todos 6-8).
const reloadProjects = async ({
seedExpanded = false
} = {}) => {
// Drain pending/in-flight saves first: replacing state from disk while
// edits sit in the 600ms debounce (or a save POST is mid-flight) would
// silently discard them. On flush failure abort — never clobber state.
try {
await flushPendingSave();
} catch (e) {
showToast('Save failed: ' + e.message, 'err');
return null;
}
const [projs, sizeInfo, backups] = await Promise.all([ApiBackend.loadAllProjects(), Storage.getHealthInfo(), Storage.listBackups()]);
const newData = await buildMergedDataFromProjects(projs, {
sizeInfo,
backups
});
latestProjects.current = projs;
setProjects(projs);
setData(newData);
if (seedExpanded && !loadLocalState()?.expandedMap) {
const em = {};
walkTree(newData.entries, it => {
em[it.id] = !it.collapsed;
});
setExpandedMap(em);
}
return {
data: newData,
projects: projs
};
};
// SyncPoller callback for multi-project polling (api mode).
const handleExternalChangeMulti = async (kind, payload) => {
if (kind === 'warn') {
showToast('File changed externally — you have unsaved edits', 'warn');
} else if (kind === 'set-changed') {
try {
await reloadProjects({
seedExpanded: false
});
showToast('Projects changed on server — reloaded');
} catch (e) {
console.error('Project set reload failed:', e); // keep current state; next poll retries
}
} else if (kind === 'project-reloaded' && payload) {
const {
name,
parsed
} = payload;
const cur = latestProjects.current;
const idx = cur.findIndex(p => p.id === name);
if (idx === -1) return; // unknown project — a set-changed poll will pick it up
const newProjs = cur.map(p => p.id === name ? {
...p,
entries: parsed.entries,
history: parsed.history,
meta: parsed.meta,
checksumOk: parsed.checksumOk,
missing: false,
error: null
} : p);
latestProjects.current = newProjs;
setProjects(newProjs);
const merged = await buildMergedDataFromProjects(newProjs, {
sizeInfo: latestData.current.health,
backups: latestData.current.backups
});
setData(merged);
showToast(`Reloaded ${name}`);
}
};
// Register a project and refresh every derived piece of state. Shared by
// the ItemDialog "Project" type (todo 7) and the admin Projects card
// register form (todo 8) — one implementation, two callers.
const registerProjectAndRefresh = async (path, name = null) => {
const result = await ApiBackend.registerProject(path, name);
if (!result.ok) throw new Error(result.error || 'Register failed');
await reloadProjects({
seedExpanded: false
});
await SyncPoller.syncChecksums();
return result;
};
// ---- Storage initialisation (runs once on mount) ----
useEffectMain(() => {
let cancelled = false;
async function initStorage() {
try {
const mode = await Storage.detect();
setStorageMode(mode);
if (mode === 'local') {
setIsLoading(false);
return;
}
if (mode === 'browser') {
if (cancelled) return;
await applyStorageData(mode, () => cancelled);
return;
}
if (mode === 'direct') {
// Try silent reconnect — works if permission is still active from a previous session.
const ok = await Storage.tryAutoConnect();
if (!ok) {
// Can't connect without a user gesture — show the connect button instead of blocking.
setIsLoading(false);
setNeedsConnect(true);
return;
}
}
// API mode needs no init — server is already running.
if (cancelled) return;
if (mode === 'api') {
await applyProjectsData(() => cancelled);
} else {
await applyStorageData(mode, () => cancelled);
}
} catch (e) {
if (cancelled) return;
setStorageMode('local');
showToast('Storage init failed: ' + e.message, 'err');
} finally {
if (!cancelled) setIsLoading(false);
}
}
async function applyProjectsData(isCancelled) {
const {
data: newData,
projects: projs
} = await reloadProjects({
seedExpanded: true
});
if (isCancelled?.()) return;
const saved = loadLocalState();
const pem = {};
for (const p of projs) pem[p.id] = saved?.projectExpandedMap?.[p.id] ?? true;
setProjectExpandedMap(pem);
if (projs.some(p => !p.checksumOk && p.meta)) setShowWarning(true);
SyncPoller.startMulti({
isDirty: () => isDirtyRef.current,
onExternalChange: handleExternalChangeMulti
});
}
async function applyStorageData(mode, isCancelled) {
const [raw, backups, sizeInfo] = await Promise.all([Storage.load(), Storage.listBackups(), Storage.getHealthInfo()]);
if (isCancelled?.()) return;
const parsed = await Parser.parse(raw?.content || '');
const newData = await buildDataFromStorage(parsed, backups, mode || Storage.mode, sizeInfo);
if (isCancelled?.()) return;
setData(newData);
if (!loadLocalState()?.expandedMap) {
const em = {};
walkTree(newData.entries, it => {
em[it.id] = !it.collapsed;
});
setExpandedMap(em);
}
if (!parsed.checksumOk && parsed.meta) setShowWarning(true);
SyncPoller.lastChecksum = parsed.meta?.checksum || '';
SyncPoller.start({
isDirty: () => isDirtyRef.current,
onExternalChange: async (kind, freshParsed) => {
if (kind === 'warn') {
showToast('File changed externally — you have unsaved edits', 'warn');
} else if (kind === 'reload' && freshParsed) {
const [nb, ns] = await Promise.all([Storage.listBackups(), Storage.getHealthInfo()]);
setData(await buildDataFromStorage(freshParsed, nb, Storage.mode, ns));
showToast('Reloaded from disk');
}
}
});
}
initStorage();
return () => {
cancelled = true;
SyncPoller.stop();
};
}, []);
// Persist expanded/collapsed row state and tweaks locally. Data itself lives in backlog.md.
useEffectMain(() => {
saveLocalState(latestExpandedMap.current, tweaks, latestProjectExpandedMap.current);
}, [expandedMap, projectExpandedMap, tweaks]);
const showToast = (msg, kind = 'ok') => {
setToast({
msg,
kind,
t: Date.now()
});
setTimeout(() => setToast(t => t && Date.now() - t.t >= 2400 ? null : t), 2500);
};
// ---- Connect to backlog.md (user-triggered, called from the connect banner) ----
const handleConnect = async () => {
try {
await Storage.connect();
const [raw, backups, sizeInfo] = await Promise.all([Storage.load(), Storage.listBackups(), Storage.getHealthInfo()]);
const parsed = await Parser.parse(raw?.content || '');
const newData = await buildDataFromStorage(parsed, backups, Storage.mode, sizeInfo);
setData(newData);
if (!loadLocalState()?.expandedMap) {
const em = {};
walkTree(newData.entries, it => {
em[it.id] = !it.collapsed;
});
setExpandedMap(em);
}
if (!parsed.checksumOk && parsed.meta) setShowWarning(true);
SyncPoller.lastChecksum = parsed.meta?.checksum || '';
SyncPoller.start({
isDirty: () => isDirtyRef.current,
onExternalChange: async (kind, freshParsed) => {
if (kind === 'warn') {
showToast('File changed externally — you have unsaved edits', 'warn');
} else if (kind === 'reload' && freshParsed) {
const [nb, ns] = await Promise.all([Storage.listBackups(), Storage.getHealthInfo()]);
setData(await buildDataFromStorage(freshParsed, nb, Storage.mode, ns));
showToast('Reloaded from disk');
}
}
});
setNeedsConnect(false);
} catch (e) {
showToast('Connect failed: ' + e.message, 'err');
}
};
// ---- Real async save ----
// projectId (string or array) restricts the save to that project(s);
// omitted = derive the dirty set from _projectId tags.
// Returns null on success or the caught error (dirty stays set on failure).
const performSave = async (label, projectId = null) => {
const d = latestData.current;
const em = latestExpandedMap.current;
try {
if (Storage.mode === 'api') {
// Unified api save path: serialize every DIRTY project separately.
// A single registered project is the same loop with one element.
const fallbackFirst = latestProjects.current[0]?.id ?? null;
const dirtyIds = computeDirtyProjectIds(d, projectId, fallbackFirst);
for (const pId of dirtyIds) {
const projectEntries = d.entries.filter(e => (e._projectId ?? fallbackFirst) === pId);
const projectHistory = d.history.filter(h => (h._projectId ?? fallbackFirst) === pId);
const content = await Parser.serialize(toDiskIds(projectEntries, projectHistory, pId));
const result = await ApiBackend.saveProject(pId, content);
// Keep SyncPoller in sync with our own save to avoid false external-change triggers.
if (result?.checksum) SyncPoller.lastChecksums[pId] = result.checksum;
}
} else if (Storage.isConnected()) {
const content = await Parser.serialize({
entries: d.entries,
history: d.history
});
await Storage.save(content);
// Keep SyncPoller in sync with our own save to avoid false external-change triggers.
const cm = content.match(/checksum:\s*(sha256:[a-f0-9]+)/);
if (cm) SyncPoller.lastChecksum = cm[1];
}
saveLocalState(em, tweaks, latestProjectExpandedMap.current);
isDirtyRef.current = false;
const now = new Date().toISOString();
setSaveState({
status: 'saved',
lastSaved: now
});
setData(prev => ({
...prev,
health: {
...prev.health,
lastSave: now
},
meta: {
...prev.meta,
saved: now
}
}));
if (label) showToast(label);
return null;
} catch (e) {
setSaveState(prev => ({
...prev,
status: 'error'
}));
showToast('Save failed: ' + e.message, 'err');
return e;
}
};
// Run performSave under in-flight tracking: the ref holds the promise
// while it runs and clears on settle (only if still the same promise).
const startTrackedSave = (label, projectId = null) => {
const p = performSave(label, projectId).finally(() => {
if (saveInFlightRef.current === p) saveInFlightRef.current = null;
});
saveInFlightRef.current = p;
return p;
};
const triggerSave = (label, projectId = null) => {
isDirtyRef.current = true;
setSaveState(prev => ({
...prev,
status: 'saving'
}));
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
pendingSaveArgsRef.current = {
label,
projectId
};
saveTimerRef.current = setTimeout(() => {
saveTimerRef.current = null;
const {
label,
projectId
} = pendingSaveArgsRef.current ?? {};
pendingSaveArgsRef.current = null;
startTrackedSave(label, projectId);
}, 600);
};
// Drain pending edits into disk before a full state reload: fire the
// debounced save now, wait out any save already in flight, and retry once
// if edits landed meanwhile. No-op when clean. Mirrors the guard
// SyncPoller._tickMulti implements for its own path. Rethrows the error
// on failure — dirty is left set so the pending edits survive.
const flushPendingSave = async () => {
let err = null;
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = null;
const args = pendingSaveArgsRef.current;
pendingSaveArgsRef.current = null;
err = await startTrackedSave(args?.label ?? null, args?.projectId ?? null);
}
if (saveInFlightRef.current) {
const e = await saveInFlightRef.current;
err = err ?? e;
}
if (isDirtyRef.current) {
err = await startTrackedSave(null, null);
}
if (err) throw err;
};
const mutate = fn => {
isDirtyRef.current = true;
setData(d => {
const c = structuredClone(d);
fn(c);
return c;
});
};
// ---- Helpers ----
function findParentList(items, id, parent = null) {
for (const it of items) {
if (it.id === id) return {
list: items,
parent
};
if (it.children?.length) {
const r = findParentList(it.children, id, it);
if (r) return r;
}
}
return null;
}
const recentTags = useMemoMain(() => {
const counts = {};
walkTree(data.entries, it => (it.tags || []).forEach(t => counts[t] = (counts[t] || 0) + 1));
return Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([t]) => t);
}, [data]);
// ---- Mutations ----
const onMutate = {
setStatus: (id, status) => {
const item = findItem(data.entries, id);
mutate(d => {
const it = findItem(d.entries, id);
if (!it || it.status === status) return;
const histEntry = {
timestamp: new Date().toISOString(),
itemId: id,
action: 'status_changed',
details: `${it.status} → ${status}`
};
if (it._projectId) histEntry._projectId = it._projectId;
d.history.unshift(histEntry);
const wasDone = it.status === 'done';
it.status = status;
if (status !== 'blocked') it.reason = null;
if (status === 'done') it.progress = 100;else if (wasDone && (it.progress ?? 0) >= 100) it.progress = 75;
});
triggerSave(null, item?._projectId);
},
setPriority: (id, priority) => {
const item = findItem(data.entries, id);
mutate(d => {
const it = findItem(d.entries, id);
if (!it || it.priority === priority) return;
const histEntry = {
timestamp: new Date().toISOString(),
itemId: id,
action: 'priority_changed',
details: `${it.priority} → ${priority}`
};
if (it._projectId) histEntry._projectId = it._projectId;
d.history.unshift(histEntry);
it.priority = priority;
});
triggerSave(null, item?._projectId);
},
setProgress: (id, progress) => {
const item = findItem(data.entries, id);
mutate(d => {
const it = findItem(d.entries, id);
if (!it) return;
const v = snapProgress(progress);
if ((it.progress ?? 0) === v) return;
const before = it.progress ?? 0;
it.progress = v;
const histEntry = {
timestamp: new Date().toISOString(),
itemId: id,
action: 'progress_changed',
details: `${before}% → ${v}%`
};
if (it._projectId) histEntry._projectId = it._projectId;
d.history.unshift(histEntry);
});
triggerSave(null, item?._projectId);
},
moveWithinPriority: (id, dir) => {
const item = findItem(data.entries, id);
mutate(d => {
const r = findParentList(d.entries, id);
if (!r) return;
const i = r.list.findIndex(x => x.id === id);
if (i < 0) return;
const me = r.list[i];
let j = i + dir;
while (j >= 0 && j < r.list.length && r.list[j].priority !== me.priority) j += dir;
if (j < 0 || j >= r.list.length) return;
[r.list[i], r.list[j]] = [r.list[j], r.list[i]];
const histEntry = {
timestamp: new Date().toISOString(),
itemId: id,
action: 'item_reordered',
details: `moved ${dir < 0 ? 'up' : 'down'}`
};
if (me._projectId) histEntry._projectId = me._projectId;
d.history.unshift(histEntry);
});
triggerSave(null, item?._projectId);
},
reorder: (draggedId, targetId) => {
const item = findItem(data.entries, draggedId);
mutate(d => {
const src = findParentList(d.entries, draggedId);
const tgt = findParentList(d.entries, targetId);
if (!src || !tgt || src.list !== tgt.list) return;
const draggedIdx = src.list.findIndex(x => x.id === draggedId);
const dragged = src.list[draggedIdx];
src.list.splice(draggedIdx, 1);
const newTargetIdx = tgt.list.findIndex(x => x.id === targetId);
tgt.list.splice(newTargetIdx, 0, dragged);
const histEntry = {
timestamp: new Date().toISOString(),
itemId: draggedId,
action: 'item_reordered',
details: `dropped before ${targetId}`
};
if (dragged._projectId) histEntry._projectId = dragged._projectId;
d.history.unshift(histEntry);
});
triggerSave('Reordered', item?._projectId);
},
// Cross-project drag&drop (todo 6 section drop zones call this): move the
// node to the destination project's root, re-tagging the whole subtree.
// Both source and destination files change, so both are saved.
moveToProject: (draggedId, projectId) => {
const dragged = findItem(data.entries, draggedId);
if (!dragged || !projectId || dragged._projectId === projectId) return;
const srcProjectId = dragged._projectId ?? latestProjects.current[0]?.id ?? null;
mutate(d => {
const r = findParentList(d.entries, draggedId);
if (!r) return;
const i = r.list.findIndex(x => x.id === draggedId);
if (i < 0) return;
const [node] = r.list.splice(i, 1);
retagSubtree(node, projectId);
(function setLevel(it, lv) {
it.level = lv;
(it.children || []).forEach(c => setLevel(c, lv + 1));
})(node, 1);
d.entries.push(node);
const histEntry = {
timestamp: new Date().toISOString(),
itemId: draggedId,
action: 'item_reordered',
details: `moved to project ${projectId}`
};
histEntry._projectId = projectId;
d.history.unshift(histEntry);
});
triggerSave('Moved', [srcProjectId, projectId]);
},
addChild: parentId => setItemDialog({
mode: 'add-child',
parentId,
initial: null
}),
addRoot: (projectId = null) => setItemDialog({
mode: 'add',
parentId: null,
initial: null,
projectId
}),
editItem: id => {
const it = findItem(data.entries, id);
if (!it) return;
setItemDialog({
mode: 'edit',
itemId: id,
initial: it
});
},
deleteItem: id => {
const it = findItem(data.entries, id);
if (!it) return;
const childCount = (() => {
let n = 0;
walkTree([it], () => n++);
return n - 1;
})();
const projectId = it._projectId ?? null;
setConfirm({
title: 'Delete this item?',
message: /*#__PURE__*/React.createElement(React.Fragment, null, "Delete ", /*#__PURE__*/React.createElement("strong", null, it.title), childCount > 0 ? ` and ${childCount} sub-item${childCount > 1 ? 's' : ''}` : '', "?"),
detail: /*#__PURE__*/React.createElement("span", {
className: "muted"
}, "This will be recorded in the history log."),
confirmLabel: 'Delete',
danger: true,
onConfirm: () => {
mutate(d => {
function remove(list) {
const i = list.findIndex(x => x.id === id);
if (i >= 0) {
list.splice(i, 1);
return true;
}
for (const x of list) if (x.children?.length && remove(x.children)) return true;
return false;
}
remove(d.entries);
const histEntry = {
timestamp: new Date().toISOString(),
itemId: id,
action: 'item_deleted',
details: `final: ${it.status}`
};
if (projectId) histEntry._projectId = projectId;
d.history.unshift(histEntry);
});
setConfirm(null);
triggerSave('Deleted', projectId);
}
});
},
archiveItem: async id => {
try {
await flushPendingSave();
} catch (e) {
showToast('Save failed: ' + e.message, 'err');
return;
}
handleArchiveItems([id]);
}
};
// Which project does a NEW item belong to? An explicit hint (dialog select
// or section context — todos 6-7) wins; otherwise the first registered
// project. Non-api modes have no projects → null → item stays untagged.
const resolveTargetProjectId = (hint = null) => {
if (hint) return hint;
const first = latestProjects.current[0];
return first ? first.id : null;
};
const submitItemDialog = vals => {
if (vals.createAsProject && vals.projectPath) {
const path = vals.projectPath;
setItemDialog(null);
registerProjectAndRefresh(path, vals.title || null).then(result => showToast(`Project registered: ${result.name}`)).catch(e => showToast('Register failed: ' + e.message, 'err'));
return;
}
if (itemDialog.mode === 'edit') {
const editedItem = findItem(data.entries, itemDialog.itemId);
mutate(d => {
const x = findItem(d.entries, itemDialog.itemId);
if (!x) return;
const before = {
priority: x.priority,
status: x.status
};
Object.assign(x, vals);
if (before.status !== vals.status) {
const histEntry = {
timestamp: new Date().toISOString(),
itemId: x.id,
action: 'status_changed',
details: `${before.status} → ${vals.status}`
};
if (x._projectId) histEntry._projectId = x._projectId;
d.history.unshift(histEntry);
}
if (before.priority !== vals.priority) {
const histEntry = {
timestamp: new Date().toISOString(),
itemId: x.id,
action: 'priority_changed',
details: `${before.priority} → ${vals.priority}`
};
if (x._projectId) histEntry._projectId = x._projectId;
d.history.unshift(histEntry);
}
});
triggerSave('Saved', editedItem?._projectId);
} else {
const newId = 'n-' + Math.random().toString(36).slice(2, 8);
const parentForTarget = itemDialog.mode === 'add-child' && itemDialog.parentId ? findItem(data.entries, itemDialog.parentId) : null;
const targetProjectId = resolveTargetProjectId(vals.projectId || itemDialog.projectId || parentForTarget?._projectId || null);
mutate(d => {
const node = {
id: newId,
level: 1,
...vals,
children: [],
collapsed: false
};
if (targetProjectId) node._projectId = targetProjectId;
if (parentForTarget) {
const parent = findItem(d.entries, itemDialog.parentId);
if (parent) {
parent.children = parent.children || [];
node.level = (parent.level || 1) + 1;
parent.children.push(node);
setExpandedMap(m => ({
...m,
[parent.id]: true
}));
}
} else {
d.entries.push(node);
}
const histEntry = {
timestamp: new Date().toISOString(),
itemId: newId,
action: 'item_created',
details: vals.title
};
if (targetProjectId) histEntry._projectId = targetProjectId;
d.history.unshift(histEntry);
});
triggerSave('Added', targetProjectId);
}
setItemDialog(null);
};
const setExpanded = (id, val) => setExpandedMap(m => ({
...m,
[id]: val
}));
// ---- Project section actions (multi-project api mode, todo 6) ----
const toggleProjectExpanded = projectId => {
setProjectExpandedMap(m => {
const isExpanded = m[projectId] !== false;
return {
...m,
[projectId]: !isExpanded
};
});
};
// Cross-project drag&drop: tree rows only accept drops from their OWN tree
// (draggedId is local state per BacklogTree), so a drag that lands anywhere
// else in a section falls through to this drop zone. The dragged id travels
// in the text/plain payload set by the row's dragstart.
const sectionDragOver = e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
e.currentTarget.classList.add('drop-target');
};
const sectionDragLeave = e => {
if (e.target === e.currentTarget) e.currentTarget.classList.remove('drop-target');
};
const handleSectionDrop = (e, projectId) => {
e.preventDefault();
e.currentTarget.classList.remove('drop-target');
let draggedId = null;
try {
draggedId = e.dataTransfer.getData('text/plain') || null;
} catch {
draggedId = null;
} // protected drag data — nothing sensible to move
if (!draggedId) return;
const dragged = findItem(data.entries, draggedId);
if (!dragged) return;
const fromId = dragged._projectId ?? latestProjects.current[0]?.id;
if (fromId === projectId) return; // intra-project drops stay the tree's own reorder
onMutate.moveToProject(draggedId, projectId);
};
const handleRenameProject = projectId => {
const proj = projects.find(p => p.id === projectId);
if (!proj) return;
setPrompt({
title: 'Rename project',
message: /*#__PURE__*/React.createElement(React.Fragment, null, "New name for ", /*#__PURE__*/React.createElement("strong", null, proj.name), "?"),
placeholder: proj.name,
confirmLabel: 'Rename',
onConfirm: async newName => {
if (!newName || newName === proj.name) return;
try {
// Flush pending edits under the CURRENT id first — after the
// rename POST a save tagged with the old id would 404 forever.
// (reloadProjects' own flush cannot help: it runs too late here.)
await flushPendingSave();
const result = await ApiBackend.renameProject(projectId, newName);
if (!result.ok) throw new Error(result.error || 'Rename failed');
setProjectExpandedMap(prev => {
const next = {
...prev
};
const was = next[projectId] ?? true;
delete next[projectId];
next[result.name] = was;
return next;
});
await reloadProjects({
seedExpanded: false
});
await SyncPoller.syncChecksums();
showToast(`Renamed to ${result.name}`);
} catch (e) {
showToast('Rename failed: ' + e.message, 'err');
}
}
});
};
const handleRemoveProject = projectId => {
const proj = projects.find(p => p.id === projectId);
if (!proj) return;
const itemCount = countAll(proj.entries);
setConfirm({
title: 'Delete project?',
message: /*#__PURE__*/React.createElement(React.Fragment, null, "Delete ", /*#__PURE__*/React.createElement("strong", null, proj.name), " from the registry?"),
detail: /*#__PURE__*/React.createElement("span", {
className: "muted"
}, `The file contains ${itemCount} item${itemCount !== 1 ? 's' : ''}. `, "Unregistering keeps the file on disk."),
checkbox: 'Also delete the file from disk',
confirmLabel: 'Delete',
danger: true,
onConfirm: async deleteFromDisk => {
const del = deleteFromDisk === true;
setConfirm(null);
try {
const result = await ApiBackend.unregisterProject(projectId, del);
if (!result.ok) throw new Error(result.error || 'Delete failed');
// Optimistic removal of every trace — a partial clean would resurrect
// the section on the next reloadProjects.
latestProjects.current = latestProjects.current.filter(p => p.id !== projectId);
setProjects(prev => prev.filter(p => p.id !== projectId));
setData(prev => ({
...prev,
entries: prev.entries.filter(e => e._projectId !== projectId),
history: prev.history.filter(h => h._projectId !== projectId)
}));
setProjectExpandedMap(prev => {
const n = {
...prev
};
delete n[projectId];
return n;
});
await SyncPoller.syncChecksums();
showToast(result.warning ? `Removed ${proj.name} — file not deleted: ${result.warning}` : del ? `Deleted from disk: ${proj.name}` : `Removed: ${proj.name}`);
} catch (e) {
showToast('Delete failed: ' + e.message, 'err');
}
}
});
};
const tagsList = useMemoMain(() => allTags(data.entries), [data]);
const counts = useMemoMain(() => countByStatus(data.entries), [data]);
const filtered = useMemoMain(() => {
const f = {
...filters,
text: filters.text?.trim() || ''
};
const noFilters = !f.statuses?.length && !f.priorities?.length && !f.tags?.length && !f.dueRange && !f.text;
let tree = noFilters ? data.entries : filterTree(data.entries, f);
tree = structuredClone(tree);
if (tweaks.sort_mode === 'priority') {
function sortRecur(list) {
list.sort((a, b) => PRIORITIES.indexOf(a.priority) - PRIORITIES.indexOf(b.priority));
list.forEach(it => it.children?.length && sortRecur(it.children));
}
sortRecur(tree);
}
if (f.scope === 'top') tree = tree.map(it => ({
...it,
children: []
}));
return tree;
}, [data, filters, tweaks.sort_mode]);
const filteredCount = useMemoMain(() => countAll(filtered), [filtered]);
const totalCount = useMemoMain(() => countAll(data.entries), [data]);
const archiveMatchCount = useMemoMain(() => {
if (!filters.text?.trim()) return 0;
const q = filters.text.trim().toLowerCase();
let count = 0;
walkTree(archiveData.entries, item => {
if (item.title.toLowerCase().includes(q) || item.body?.toLowerCase().includes(q) || item.tags?.some(t => t.toLowerCase().includes(q))) {
count++;
}
});
return count;
}, [filters.text, archiveData.entries]);
// ---- Keyboard shortcuts ----
useEffectMain(() => {
const onKey = e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === '/' && !e.metaKey) {
e.preventDefault();
document.querySelector('.search-input')?.focus();
}
if (e.key === 'g') setView('backlog');
if (e.key === 'a') setView('admin');
if (e.key === 'n') onMutate.addRoot();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
// ---- Restore from backup ----
const handleRestore = b => {
setConfirm({
title: 'Restore this backup?',
danger: false,
message: /*#__PURE__*/React.createElement(React.Fragment, null, "Overwrite ", /*#__PURE__*/React.createElement("code", {
className: "mono"
}, "backlog.md"), " with the contents of this backup."),
detail: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
className: "mono small"
}, b.name), !b.valid && /*#__PURE__*/React.createElement("div", {
className: "restore-warn"
}, /*#__PURE__*/React.createElement(Icon, {
name: "warn",
size: 14
}), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
className: "restore-warn-title"
}, "Checksum mismatch on this backup"), /*#__PURE__*/React.createElement("div", {
className: "restore-warn-body"
}, "The file may be partially written or edited externally.")))),
confirmLabel: 'Restore',
onConfirm: async () => {
setConfirm(null);
try {
if (Storage.isConnected()) {
const result = await Storage.restoreBackup(b.name);
if (!result.ok) throw new Error(result.error || 'Restore failed');
if (Storage.mode === 'api') {
// The legacy backup route restored the default project — refresh everything.
// Null = flush failed ('Save failed' toast shown there); keep dirty state, skip success toast.
if (!(await reloadProjects({
seedExpanded: false
}))) return;
await SyncPoller.syncChecksums();
} else {
const [raw, backups, sizeInfo] = await Promise.all([Storage.load(), Storage.listBackups(), Storage.getHealthInfo()]);
const parsed = await Parser.parse(raw?.content || '');
const newData = await buildDataFromStorage(parsed, backups, storageMode, sizeInfo);
setData(newData);
SyncPoller.lastChecksum = parsed.meta?.checksum || '';
}
isDirtyRef.current = false;
}
showToast(`Restored from ${b.name.slice(0, 28)}…`);
} catch (e) {
showToast('Restore failed: ' + e.message, 'err');
}
}
});
};
// ---- Import entries from parsed content ----
const handleImport = ({
entries,
history,
targetProjectId
}) => {
// Multi-project target (todo 8): the dialog's "Target project" select
// routes the file into ONE project. Its backlog.md is written directly
// (merge-only id suffixes stripped via toDiskIds), other projects'
// files are untouched, then everything reloads from the server.
if (Storage.mode === 'api' && targetProjectId) {
if (!entries.length && !(history || []).length) {
showToast('Import failed: no entries found in file', 'err');
return;
}
(async () => {
try {
const content = await Parser.serialize(toDiskIds(entries, history || [], targetProjectId));
const result = await ApiBackend.saveProject(targetProjectId, content);
if (!result.ok) throw new Error(result.error || 'Import failed');
if (result.checksum) SyncPoller.lastChecksums[targetProjectId] = result.checksum;
setImportExportOpen(false);
await reloadProjects({
seedExpanded: false
});
await SyncPoller.syncChecksums();
showToast(`Imported into ${targetProjectId}`);
} catch (e) {
showToast('Import failed: ' + e.message, 'err');
}
})();
return;
}
// Single-project path: imported rows land in the first project (api
// mode with one registered project) or in the single backlog file.
const pid = Storage.mode === 'api' ? resolveTargetProjectId() : null;
mutate(d => {
if (pid) {
for (const e of entries) e._projectId = pid;
}
d.entries = entries;
if (history?.length) d.history = [...history.map(h => pid ? {
...h,
_projectId: pid
} : h), ...d.history];
const histEntry = {
timestamp: new Date().toISOString(),
itemId: 'system',
action: 'imported',
details: `${entries.length} top-level entries`
};
if (pid) histEntry._projectId = pid;
d.history.unshift(histEntry);
});
setImportExportOpen(false);
triggerSave('Imported', pid);
};
// ---- Archive functions ----
const loadArchive = async () => {
try {
if (Storage.mode === 'api') {
// Each project has its own archive.md — merge them, tagged per project.
const projs = latestProjects.current;
const parsedAll = await Promise.all(projs.map(async p => {
try {
const raw = await ApiBackend.loadArchive(p.id);
if (raw.exists && raw.content) return await Parser.parse(raw.content);
} catch (e) {
console.error(`Failed to load archive for ${p.id}:`, e);
}
return {
entries: [],
history: []
};
}));
const entries = [],
history = [];
projs.forEach((p, i) => {
for (const e of parsedAll[i].entries) e._projectId = p.id;
for (const h of parsedAll[i].history) h._projectId = p.id;
entries.push(...parsedAll[i].entries);
history.push(...parsedAll[i].history);
});
setArchiveData({
entries,
history
});
} else {
const raw = await Storage.loadArchive();
if (raw.exists && raw.content) {
const parsed = await Parser.parse(raw.content);
setArchiveData({
entries: parsed.entries || [],
history: parsed.history || []
});
}
}
} catch (e) {
console.error('Failed to load archive:', e);
}
};
// Load archive when switching to admin view
useEffectMain(() => {
if (view === 'admin') loadArchive();
}, [view]);
const handleArchiveItems = async itemIds => {
const now = new Date().toISOString().slice(0, 10);
const toArchive = [];
// Build items to archive with restore-path metadata
for (const id of itemIds) {
const item = findItem(data.entries, id);
if (!item) continue;
const restorePath = getRestorePath(data.entries, id);
// Deep clone and add archive metadata
const cloned = JSON.parse(JSON.stringify(item));
cloned.archived = now;
cloned.restorePath = restorePath || null;
toArchive.push(cloned);
}
if (toArchive.length === 0) return;
// Remove from backlog
const {
entries: newEntries
} = removeItems([...data.entries], itemIds);
// Add history entries
const newHistory = [...data.history];
for (const item of toArchive) {
const histEntry = {
timestamp: new Date().toISOString(),
itemId: item.id,
action: 'item_archived',
details: `moved to archive.md`
};
if (item._projectId) histEntry._projectId = item._projectId;
newHistory.unshift(histEntry);
}
// Add to archive
const newArchiveEntries = [...archiveData.entries, ...toArchive];
try {
if (Storage.mode === 'api') {
// Per-project archive: group the batch by _projectId, then for each
// affected project write its remaining backlog + its own archive.
const fallbackFirst = latestProjects.current[0]?.id ?? null;
const pidOf = x => x._projectId ?? fallbackFirst;
const groups = new Map();
for (const item of toArchive) {
const pid = pidOf(item);
if (!groups.has(pid)) groups.set(pid, []);
groups.get(pid).push(item);
}
const existing = await Promise.all([...groups.keys()].map(async pid => {
try {
const raw = await ApiBackend.loadArchive(pid);
if (raw.exists && raw.content) {
const parsed = await Parser.parse(raw.content);
return {
pid,
entries: parsed.entries || [],
history: parsed.history || []
};
}
} catch (e) {
console.error(`Failed to load archive for ${pid}:`, e);
}
return {
pid,
entries: [],
history: []
};
}));
for (const {
pid,
entries: archEntries,
history: archHistory
} of existing) {
const backlogContent = await Parser.serialize(toDiskIds(newEntries.filter(e => pidOf(e) === pid), newHistory.filter(h => pidOf(h) === pid), pid));
const archiveContent = await Parser.serialize(toDiskIds([...archEntries, ...groups.get(pid)], archHistory, pid));
const result = await ApiBackend.saveArchive(backlogContent, archiveContent, pid);
if (!result.ok) throw new Error('Archive failed');
}
await SyncPoller.syncChecksums();
} else {
// Serialize both files
const backlogContent = await Parser.serialize({
entries: newEntries,
history: newHistory
});
const archiveContent = await Parser.serialize({
entries: newArchiveEntries,
history: archiveData.history
});
// Save both atomically
const result = await Storage.saveArchive(backlogContent, archiveContent);
if (!result.ok) throw new Error('Archive failed');
}
// Update local state
setData(d => ({
...d,
entries: newEntries,
history: newHistory
}));
if (Storage.mode === 'api') {
await loadArchive(); // refresh the merged per-project archive view from disk
} else {
setArchiveData({
entries: newArchiveEntries,
history: archiveData.history
});
}
isDirtyRef.current = false;
showToast(`Archived ${toArchive.length} item${toArchive.length > 1 ? 's' : ''}`);
} catch (e) {
showToast('Archive failed: ' + e.message, 'err');
}
};
const handleRestoreItems = async itemIds => {
const toRestore = [];
const remainingArchive = [];
// Find items to restore
for (const item of archiveData.entries) {
if (itemIds.includes(item.id)) {
toRestore.push(item);
} else {
remainingArchive.push(item);
}
}
if (toRestore.length === 0) return;
// Restore to backlog
const newEntries = [...data.entries];
const warnings = [];
for (const item of toRestore) {
const restorePath = item.restorePath;
// Clear archive metadata
delete item.archived;
delete item.restorePath;
// Try to insert at original location
const inserted = insertAtPath(newEntries, item, restorePath);
if (!inserted) {
warnings.push(item.title);
}
}
// Add history entries
const newHistory = [...data.history];
for (const item of toRestore) {
const histEntry = {
timestamp: new Date().toISOString(),
itemId: item.id,
action: 'item_restored',
details: `restored from archive`
};
if (item._projectId) histEntry._projectId = item._projectId;
newHistory.unshift(histEntry);
}
try {
if (Storage.mode === 'api') {
// Mirror of the archive flow per project: only projects whose items
// were restored get their backlog + archive rewritten.
const fallbackFirst = latestProjects.current[0]?.id ?? null;
const pidOf = x => x._projectId ?? fallbackFirst;
for (const pid of [...new Set(toRestore.map(pidOf))]) {
const backlogContent = await Parser.serialize(toDiskIds(newEntries.filter(e => pidOf(e) === pid), newHistory.filter(h => pidOf(h) === pid), pid));
const archiveContent = await Parser.serialize(toDiskIds(remainingArchive.filter(e => pidOf(e) === pid), archiveData.history.filter(h => pidOf(h) === pid), pid));
const result = await ApiBackend.restoreFromArchive(backlogContent, archiveContent, pid);
if (!result.ok) throw new Error('Restore failed');
}
await SyncPoller.syncChecksums();
} else {
// Serialize both files
const backlogContent = await Parser.serialize({
entries: newEntries,
history: newHistory
});
const archiveContent = await Parser.serialize({
entries: remainingArchive,
history: archiveData.history
});
// Save both atomically
const result = await Storage.restoreFromArchive(backlogContent, archiveContent);
if (!result.ok) throw new Error('Restore failed');
}
// Update local state
setData(d => ({
...d,
entries: newEntries,
history: newHistory
}));
if (Storage.mode === 'api') {
await loadArchive(); // refresh the merged per-project archive view from disk
} else {
setArchiveData({
entries: remainingArchive,
history: archiveData.history
});
}
isDirtyRef.current = false;
let msg = `Restored ${toRestore.length} item${toRestore.length > 1 ? 's' : ''}`;
if (warnings.length) msg += ` (${warnings.length} to root - parent not found)`;
showToast(msg);
} catch (e) {
showToast('Restore failed: ' + e.message, 'err');
}
};
const saveLabel = saveState.status === 'saving' ? 'Saving…' : saveState.status === 'error' ? 'Save failed' : `Saved · ${fmtTimestamp(saveState.lastSaved)}`;
const hasFilters = filters.text || filters.statuses?.length || filters.priorities?.length || filters.tags?.length || filters.dueRange;
const isMulti = projects.length > 1; // UI gating only — the merged model is the single data path
if (isLoading) {
return /*#__PURE__*/React.createElement("div", {
className: "app",
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh'
}
}, /*#__PURE__*/React.createElement("div", {
style: {
textAlign: 'center',
color: 'var(--ink-3)'
}
}, /*#__PURE__*/React.createElement("div", {
style: {
fontSize: 22,
marginBottom: 8
}
}, "Loading\u2026"), /*#__PURE__*/React.createElement("div", {
style: {
fontSize: 13
}
}, "Connecting to storage")));
}
return /*#__PURE__*/React.createElement("div", {
className: "app"
}, /*#__PURE__*/React.createElement(StatusStyleContext.Provider, {
value: tweaks.status_style
}, /*#__PURE__*/React.createElement(Header, {
view: view,
setView: setView,
saveState: saveState,
saveLabel: saveLabel,
storageMode: storageMode,
searchValue: filters.text,
onSearch: v => setFilters({
...filters,
text: v
}),
onForceSave: () => triggerSave('Saved manually'),
onOpenImportExport: () => setImportExportOpen(true)
}), storageMode === 'local' && /*#__PURE__*/React.createElement("div", {
className: "banner warn",
style: {
gap: 8
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "warn",
size: 14
}), /*#__PURE__*/React.createElement("span", null, "This browser can't access local files directly \u2014 ", /*#__PURE__*/React.createElement("strong", null, "changes won't be saved."), ' ', "Open in Chrome or Edge to save without a server, or run", ' ', /*#__PURE__*/React.createElement("code", {
className: "mono"
}, "python3 server/server.py"), " for any browser.")), storageMode === 'browser' && /*#__PURE__*/React.createElement("div", {
className: "banner info",
style: {
gap: 8
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "folder",
size: 14
}), /*#__PURE__*/React.createElement("span", null, "Data saved in ", /*#__PURE__*/React.createElement("strong", null, "browser storage"), " \u2014 persists until you clear site data.", ' ', "Use ", /*#__PURE__*/React.createElement("em", null, "Import/Export"), " to back up to a file.")), needsConnect && /*#__PURE__*/React.createElement("div", {
className: "banner info"
}, /*#__PURE__*/React.createElement(Icon, {
name: "folder",
size: 14
}), "No ", /*#__PURE__*/React.createElement("code", {
className: "mono"
}, "backlog.md"), " connected \u2014 grant one-time folder access, then reloads are silent.", /*#__PURE__*/React.createElement("button", {
className: "btn-primary",
style: {
marginLeft: 'auto',
flexShrink: 0,
fontSize: 12,
padding: '3px 10px'
},
onClick: handleConnect
}, "Open folder\u2026")), showWarning && /*#__PURE__*/React.createElement("div", {
className: "banner warn"
}, /*#__PURE__*/React.createElement(Icon, {
name: "warn",
size: 14
}), "File was edited outside the app \u2014 checksum mismatch. The next save will rewrite a correct marker.", /*#__PURE__*/React.createElement("button", {
className: "banner-close",
onClick: () => setShowWarning(false)
}, "dismiss")), view === 'backlog' ? /*#__PURE__*/React.createElement("div", {
className: "main"
}, /*#__PURE__*/React.createElement(FilterPanel, {
filters: filters,
setFilters: setFilters,
tagsList: tagsList,
counts: counts
}), /*#__PURE__*/React.createElement("section", {
className: "content"
}, /*#__PURE__*/React.createElement("div", {
className: "content-sticky"
}, /*#__PURE__*/React.createElement("div", {
className: "content-head"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
className: "eyebrow"
}, "Backlog"), /*#__PURE__*/React.createElement("h1", {
className: "content-title"
}, hasFilters ? /*#__PURE__*/React.createElement(React.Fragment, null, filteredCount, " of ", totalCount, " items", archiveMatchCount > 0 && /*#__PURE__*/React.createElement(React.Fragment, null, " \xB7 ", archiveMatchCount, " in archive")) : /*#__PURE__*/React.createElement(React.Fragment, null, totalCount, " items"))), /*#__PURE__*/React.createElement("div", {
className: "content-head-actions"
}, /*#__PURE__*/React.createElement("div", {
className: "seg seg-mini",
title: "Order"
}, /*#__PURE__*/React.createElement("button", {
className: `seg-btn ${tweaks.sort_mode === 'priority' ? 'active' : ''}`,
onClick: () => setTweak('sort_mode', 'priority')
}, "By priority"), /*#__PURE__*/React.createElement("button", {
className: `seg-btn ${tweaks.sort_mode === 'manual' ? 'active' : ''}`,
onClick: () => setTweak('sort_mode', 'manual')
}, "Manual")), /*#__PURE__*/React.createElement("button", {
className: "btn-primary",
onClick: onMutate.addRoot
}, /*#__PURE__*/React.createElement(Icon, {
name: "plus",
size: 12
}), " New item"))), /*#__PURE__*/React.createElement(ViewChips, {
filters: filters,
setFilters: setFilters
}), /*#__PURE__*/React.createElement("div", {
className: "legend"
}, /*#__PURE__*/React.createElement("span", {
className: "legend-cap"
}, "Status:"), STATUSES.map(s => /*#__PURE__*/React.createElement("span", {
key: s.key,
className: "legend-item"
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: s.key,
size: 13
}), /*#__PURE__*/React.createElement("span", null, s.label))), /*#__PURE__*/React.createElement("span", {
className: "legend-sep"
}, "\xB7"), /*#__PURE__*/React.createElement("span", {
className: "legend-hint"
}, tweaks.sort_mode === 'manual' ? /*#__PURE__*/React.createElement(React.Fragment, null, "Drag rows to reorder \xB7 ") : /*#__PURE__*/React.createElement(React.Fragment, null, "Auto-sorted by priority \xB7 drag/arrows reorder within priority \xB7 "), /*#__PURE__*/React.createElement("kbd", null, "/"), " search \xB7 ", /*#__PURE__*/React.createElement("kbd", null, "n"), " new"))), /*#__PURE__*/React.createElement("div", {
className: "content-scroll"
}, isMulti && projects.length > 0 ? /*#__PURE__*/React.createElement("div", {
className: "project-sections"
}, projects.map(p => {
const pidOf = e => e._projectId ?? projects[0]?.id;
const projEntries = filtered.filter(e => pidOf(e) === p.id);
const itemCount = countAll(data.entries.filter(e => pidOf(e) === p.id));
const isExpanded = projectExpandedMap[p.id] !== false;
const unhealthy = p.missing || p.error;
return /*#__PURE__*/React.createElement("div", {
key: p.id,
className: "project-section"
}, /*#__PURE__*/React.createElement("div", {
className: `project-section-header ${isExpanded ? 'expanded' : 'collapsed'}${unhealthy ? ' missing' : ''}`,
onClick: () => toggleProjectExpanded(p.id),
title: p.path
}, /*#__PURE__*/React.createElement("span", {
className: "project-section-chevron"
}, /*#__PURE__*/React.createElement(Icon, {
name: isExpanded ? 'chevron' : 'chevronRight',
size: 12
})), /*#__PURE__*/React.createElement("span", {
className: "project-section-name"
}, p.name), unhealthy && /*#__PURE__*/React.createElement("span", {
className: "project-missing-badge"
}, p.missing ? 'missing' : 'error'), /*#__PURE__*/React.createElement("span", {
className: "project-section-count"
}, itemCount, " item", itemCount !== 1 ? 's' : ''), /*#__PURE__*/React.createElement("div", {
className: "project-section-actions"
}, /*#__PURE__*/React.createElement("button", {
className: "project-section-btn",
title: `Add item to ${p.name}`,
onClick: e => {
e.stopPropagation();
onMutate.addRoot(p.id);
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "plus",
size: 11
})), /*#__PURE__*/React.createElement("button", {
className: "project-section-btn",
title: "Rename project",
onClick: e => {
e.stopPropagation();
handleRenameProject(p.id);
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "edit",
size: 11
})), /*#__PURE__*/React.createElement("button", {
className: "project-section-btn project-section-btn-danger",
title: "Remove project from registry",
onClick: e => {
e.stopPropagation();
handleRemoveProject(p.id);
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "trash",
size: 11
})))), isExpanded && /*#__PURE__*/React.createElement("div", {
className: "project-section-content",
onDragOver: sectionDragOver,
onDragLeave: sectionDragLeave,
onDrop: e => handleSectionDrop(e, p.id)
}, unhealthy ? /*#__PURE__*/React.createElement("div", {
className: "project-empty"
}, p.missing ? /*#__PURE__*/React.createElement(React.Fragment, null, "File not found on disk \u2014 restore it, or remove this project. ", /*#__PURE__*/React.createElement("span", {
className: "mono"
}, p.path)) : /*#__PURE__*/React.createElement(React.Fragment, null, "Failed to load this project: ", p.error)) : projEntries.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "project-empty"
}, hasFilters ? 'No items match these filters.' : 'No items in this project yet.') : /*#__PURE__*/React.createElement(BacklogTree, {
items: projEntries,
expandedMap: expandedMap,
setExpanded: setExpanded,
onMutate: onMutate,
query: filters.text?.trim() || '',
statusStyle: tweaks.status_style,
manualOrder: tweaks.sort_mode === 'manual'
})));
})) : filtered.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "empty-state"
}, /*#__PURE__*/React.createElement("div", {
className: "empty-glyph"
}, "\u2205"), /*#__PURE__*/React.createElement("div", null, hasFilters ? 'No items match these filters.' : 'No items yet. Press n or click "+ New item" to start.')) : /*#__PURE__*/React.createElement(BacklogTree, {
items: filtered,
expandedMap: expandedMap,
setExpanded: setExpanded,
onMutate: onMutate,
query: filters.text?.trim() || '',
statusStyle: tweaks.status_style,
manualOrder: tweaks.sort_mode === 'manual'
}), filters.text?.trim() && /*#__PURE__*/React.createElement(ArchiveSearchResults, {
query: filters.text.trim(),
archiveEntries: archiveData.entries,
onRestore: handleRestoreItems,
onLoadArchive: loadArchive,
onView: item => setItemDialog({
mode: 'view',
initial: item
})
})))) : /*#__PURE__*/React.createElement(AdminPage, {
data: data,
history: data.history,
tweaks: tweaks,
setTweak: setTweak,
onClose: () => setView('backlog'),
onForceSave: () => triggerSave('Force-saved'),
onForceBackup: () => triggerSave('Force-backed up'),
onCompact: () => {
mutate(d => {
if (d.history.length > 200) d.history = d.history.slice(0, 200);
});
triggerSave('History compacted');
},
onRestore: handleRestore,
onDownloadBackup: name => {
if (Storage.mode === 'api') {
const a = document.createElement('a');
a.href = `/api/backups/${encodeURIComponent(name)}`;
a.download = name;
a.click();
} else {
showToast(`${name.slice(0, 28)}… — download not available in direct mode`);
}
},
archiveData: archiveData,
onArchiveItems: handleArchiveItems,
onRestoreItems: handleRestoreItems,
onViewItem: item => setItemDialog({
mode: 'view',
initial: item
}),
storageMode: storageMode,
projects: projects,
onRegisterProject: (path, name) => registerProjectAndRefresh(path, name).then(result => showToast(`Project registered: ${result.name}`)).catch(e => showToast('Register failed: ' + e.message, 'err')),
onRenameProject: handleRenameProject,
onRemoveProject: handleRemoveProject
}), /*#__PURE__*/React.createElement(ItemDialog, {
open: !!itemDialog,
mode: itemDialog?.mode,
initial: itemDialog?.initial,
recentTags: recentTags,
isMultiProject: isMulti && storageMode === 'api',
projects: projects,
defaultProjectId: itemDialog?.projectId ?? projects[0]?.id,
onClose: () => setItemDialog(null),
onSubmit: submitItemDialog
}), /*#__PURE__*/React.createElement(ConfirmDialog, {
open: !!confirm,
title: confirm?.title,
message: confirm?.message,
detail: confirm?.detail,
checkbox: confirm?.checkbox,
confirmLabel: confirm?.confirmLabel,
danger: confirm?.danger,
onCancel: () => setConfirm(null),
onConfirm: confirm?.onConfirm
}), prompt && /*#__PURE__*/React.createElement(window.PromptDialog, {
open: true,
title: prompt.title,
message: prompt.message,
placeholder: prompt.placeholder,
confirmLabel: prompt.confirmLabel,
onCancel: () => setPrompt(null),
onConfirm: value => {
const cb = prompt.onConfirm;
setPrompt(null);
cb?.(value);
}
}), /*#__PURE__*/React.createElement(ImportExportDialog, {
open: importExportOpen,
data: data,
storageMode: storageMode,
projects: projects,
onClose: () => setImportExportOpen(false),
onImport: handleImport
}), toast && /*#__PURE__*/React.createElement("div", {
className: `toast ${toast.kind}`
}, /*#__PURE__*/React.createElement(Icon, {
name: "check",
size: 12
}), " ", toast.msg), /*#__PURE__*/React.createElement(TweaksPanel, {
title: "Tweaks"
}, /*#__PURE__*/React.createElement(TweakSection, {
title: "Status icons"
}, /*#__PURE__*/React.createElement(TweakRadio, {
label: "Style",
value: tweaks.status_style,
options: [{
value: 'flat',
label: 'Flat'
}, {
value: 'ascii',
label: 'ASCII'
}, {
value: 'color',
label: 'Color'
}, {
value: 'emoji',
label: 'Emoji'
}],
onChange: v => setTweak('status_style', v)
})), /*#__PURE__*/React.createElement(TweakSection, {
title: "Order"
}, /*#__PURE__*/React.createElement(TweakRadio, {
label: "Sort",
value: tweaks.sort_mode,
options: [{
value: 'priority',
label: 'By priority'
}, {
value: 'manual',
label: 'Manual'
}],
onChange: v => setTweak('sort_mode', v)
})), /*#__PURE__*/React.createElement(TweakSection, {
title: "Accent"
}, /*#__PURE__*/React.createElement(TweakSlider, {
label: "Hue",
value: tweaks.accent_hue,
min: 0,
max: 360,
step: 1,
onChange: v => setTweak('accent_hue', v),
formatValue: v => `${v}°`
})), /*#__PURE__*/React.createElement(TweakSection, {
title: "Layout"
}, /*#__PURE__*/React.createElement(TweakRadio, {
label: "Density",
value: tweaks.density,
options: [{
value: 'comfortable',
label: 'Comfortable'
}, {
value: 'compact',
label: 'Compact'
}],
onChange: v => setTweak('density', v)
}), /*#__PURE__*/React.createElement(TweakToggle, {
label: "Paper texture",
checked: tweaks.paper_texture,
onChange: v => setTweak('paper_texture', v)
})), /*#__PURE__*/React.createElement(TweakSection, {
title: "Demo"
}, /*#__PURE__*/React.createElement(TweakButton, {
label: "Show checksum-mismatch banner",
onClick: () => setShowWarning(true)
}), /*#__PURE__*/React.createElement(TweakButton, {
label: "Simulate external file change",
onClick: () => showToast('File reloaded from disk')
}), window.SEED_DATA && /*#__PURE__*/React.createElement(TweakButton, {
label: "Load sample / test data",
onClick: () => {
const seed = structuredClone(window.SEED_DATA);
const defaults = {
done: 100,
cancelled: 0,
'in-progress': 50,
blocked: 25,
postponed: 25,
open: 0
};
walkTree(seed.entries, it => {
if (typeof it.progress !== 'number') it.progress = defaults[it.status] ?? 0;else if (it.status === 'done') it.progress = 100;
});
setData(seed);
const em = {};
walkTree(seed.entries, it => {
em[it.id] = !it.collapsed;
});
setExpandedMap(em);
showToast('Sample data loaded');
}
})))));
}
// ----- View chips (saved searches) -----
const VIEW_CHIPS = [{
key: 'all',
label: 'All',
statuses: []
}, {
key: 'open',
label: 'Open',
statuses: ['open']
}, {
key: 'wip',
label: 'In progress',
statuses: ['in-progress']
}, {
key: 'active',
label: 'Active',
statuses: ['open', 'in-progress', 'blocked', 'postponed'],
hint: 'everything not done or cancelled'
}, {
key: 'blocked',
label: 'Blocked',
statuses: ['blocked']
}, {
key: 'closed',
label: 'Closed',
statuses: ['done', 'cancelled'],
hint: 'done + cancelled'
}];
function ViewChips({
filters,
setFilters
}) {
const cur = filters.statuses || [];
const same = (a, b) => a.length === b.length && a.every(x => b.includes(x));
const activeKey = VIEW_CHIPS.find(v => same(v.statuses, cur))?.key;
return /*#__PURE__*/React.createElement("div", {
className: "view-chips",
role: "tablist",
"aria-label": "Saved views"
}, VIEW_CHIPS.map(v => /*#__PURE__*/React.createElement("button", {
key: v.key,
role: "tab",
"aria-selected": activeKey === v.key,
className: `view-chip ${activeKey === v.key ? 'active' : ''}`,
title: v.hint || v.label,
onClick: () => setFilters({
...filters,
statuses: v.statuses
})
}, v.label)));
}
// ----- Header -----
function Header({
view,
setView,
saveState,
saveLabel,
storageMode,
searchValue,
onSearch,
onOpenImportExport
}) {
const modeIcon = storageMode === 'api' ? '⚡' : storageMode === 'direct' ? '📁' : storageMode === 'browser' ? '🌐' : '💾';
return /*#__PURE__*/React.createElement("header", {
className: "header"
}, /*#__PURE__*/React.createElement("div", {
className: "brand"
}, /*#__PURE__*/React.createElement("span", {
className: "brand-glyph"
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: "in-progress",
size: 16
})), /*#__PURE__*/React.createElement("span", {
className: "brand-name"
}, "backlog"), /*#__PURE__*/React.createElement("span", {
className: "brand-sub mono",
title: `Storage: ${storageMode}`
}, "backlog.md ", modeIcon)), /*#__PURE__*/React.createElement("nav", {
className: "tabs"
}, /*#__PURE__*/React.createElement("button", {
className: `tab ${view === 'backlog' ? 'active' : ''}`,
onClick: () => setView('backlog')
}, /*#__PURE__*/React.createElement(Icon, {
name: "list",
size: 13
}), " Backlog"), /*#__PURE__*/React.createElement("button", {
className: `tab ${view === 'admin' ? 'active' : ''}`,
onClick: () => setView('admin')
}, /*#__PURE__*/React.createElement(Icon, {
name: "cog",
size: 13
}), " Admin")), /*#__PURE__*/React.createElement("div", {
className: "search-wrap"
}, /*#__PURE__*/React.createElement(Icon, {
name: "search",
size: 14
}), /*#__PURE__*/React.createElement("input", {
className: "search-input",
placeholder: "Search titles, tags, reasons\u2026 ( / )",
value: searchValue,
onChange: e => onSearch(e.target.value)
}), searchValue && /*#__PURE__*/React.createElement("button", {
className: "search-clear",
onClick: () => onSearch('')
}, "\xD7")), /*#__PURE__*/React.createElement("div", {
className: "header-right"
}, /*#__PURE__*/React.createElement("button", {
className: "header-btn",
onClick: onOpenImportExport,
title: "Import / Export"
}, /*#__PURE__*/React.createElement(Icon, {
name: "archive",
size: 13
}), " Import/Export"), /*#__PURE__*/React.createElement("div", {
className: `save-indicator ${saveState.status}`
}, /*#__PURE__*/React.createElement("span", {
className: `save-dot ${saveState.status}`
}), saveLabel)));
}
// ----- Import / Export dialog -----
// Multi-project api mode (projects.length > 1) adds a "Target project"
// select: export serializes only the selected project's entries/history (as a
// plain upstream file — merge-only id suffixes stripped), import writes only
// that project's backlog.md. Single-project mode is unchanged.
function ImportExportDialog({
open,
data,
storageMode,
projects = [],
onClose,
onImport
}) {
const {
useState: useStateD,
useEffect: useEffectD,
useRef: useRefD
} = React;
const [tab, setTab] = useStateD('md');
const [copied, setCopied] = useStateD(false);
const [mdContent, setMdContent] = useStateD('');
const [targetId, setTargetId] = useStateD(null);
const fileInputRef = useRefD(null);
const isMulti = storageMode === 'api' && projects.length > 1;
// Default the target to the first registered project on every open.
useEffectD(() => {
if (open) setTargetId(projects[0]?.id ?? null);
}, [open]);
// Export scope: full data in single mode; the selected project's rows in
// multi mode (toDiskIds keeps exported ids free of `~project` suffixes).
const exportScope = React.useMemo(() => {
if (!isMulti || !targetId) {
return {
entries: data?.entries || [],
history: data?.history || [],
meta: data?.meta
};
}
const pidOf = x => x._projectId ?? projects[0]?.id;
const scoped = toDiskIds((data?.entries || []).filter(e => pidOf(e) === targetId), (data?.history || []).filter(h => pidOf(h) === targetId), targetId);
return {
...scoped,
meta: data?.meta
};
}, [isMulti, targetId, data, projects]);
// Async-generate markdown when dialog opens or data changes.
useEffectD(() => {
if (!open || !data) return;
let cancelled = false;
Parser.serialize({
entries: exportScope.entries,
history: exportScope.history
}).then(md => {
if (!cancelled) setMdContent(md);
}).catch(() => {});
return () => {
cancelled = true;
};
}, [open, data, exportScope]);
const jsonContent = React.useMemo(() => {
if (!data) return '';
return JSON.stringify({
entries: exportScope.entries,
history: exportScope.history,
meta: exportScope.meta
}, null, 2);
}, [data, exportScope]);
const currentContent = tab === 'md' ? mdContent : jsonContent;
const filename = tab === 'md' ? 'backlog.md' : 'backlog_export.json';
const mimeType = tab === 'md' ? 'text/markdown' : 'application/json';
const copyContent = async () => {
try {
await navigator.clipboard.writeText(currentContent);
} catch {
const ta = Object.assign(document.createElement('textarea'), {
value: currentContent,
style: 'position:fixed;opacity:0'
});
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} catch {}
document.body.removeChild(ta);
}
setCopied(true);
setTimeout(() => setCopied(false), 1400);
};
const downloadFile = () => {
const blob = new Blob([currentContent], {
type: mimeType
});
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), {
href: url,
download: filename
});
a.click();
URL.revokeObjectURL(url);
};
const handleFileInput = async e => {
const file = e.target.files?.[0];
if (!file) return;
e.target.value = '';
try {
const text = await file.text();
let entries, history;
if (file.name.endsWith('.json')) {
const obj = JSON.parse(text);
entries = obj.entries || [];
history = obj.history || [];
} else {
const parsed = await Parser.parse(text);
entries = parsed.entries;
history = parsed.history;
}
onImport({
entries,
history,
targetProjectId: isMulti ? targetId : undefined
});
} catch (err) {
alert('Import failed: ' + err.message);
}
};
return /*#__PURE__*/React.createElement(Dialog, {
open: open,
onClose: onClose,
width: 760,
labelledBy: "dlg-ie-title"
}, /*#__PURE__*/React.createElement(DialogHeader, {
id: "dlg-ie-title",
eyebrow: "Backup & sync",
title: "Import / Export",
onClose: onClose
}), /*#__PURE__*/React.createElement("div", {
className: "dlg-body"
}, isMulti && /*#__PURE__*/React.createElement("div", {
className: "ie-target-row"
}, /*#__PURE__*/React.createElement("label", {
className: "ie-target-label",
htmlFor: "ie-target-select"
}, "Target project"), /*#__PURE__*/React.createElement("select", {
id: "ie-target-select",
className: "ie-target",
value: targetId ?? '',
onChange: e => setTargetId(e.target.value)
}, projects.map(p => /*#__PURE__*/React.createElement("option", {
key: p.id,
value: p.id
}, p.name))), /*#__PURE__*/React.createElement("span", {
className: "card-sub"
}, "export and import apply to this project only")), /*#__PURE__*/React.createElement("div", {
className: "ie-tabs"
}, /*#__PURE__*/React.createElement("button", {
className: `ie-tab ${tab === 'md' ? 'active' : ''}`,
onClick: () => setTab('md')
}, "Markdown ", /*#__PURE__*/React.createElement("span", {
className: "ie-tab-sub"
}, ".md")), /*#__PURE__*/React.createElement("button", {
className: `ie-tab ${tab === 'json' ? 'active' : ''}`,
onClick: () => setTab('json')
}, "JSON ", /*#__PURE__*/React.createElement("span", {
className: "ie-tab-sub"
}, "structured")), /*#__PURE__*/React.createElement("button", {
className: `ie-copy-btn ${copied ? 'copied' : ''}`,
onClick: copyContent,
title: `Copy ${tab === 'md' ? 'Markdown' : 'JSON'} to clipboard`,
"aria-live": "polite"
}, copied ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Icon, {
name: "check",
size: 13
}), " ", /*#__PURE__*/React.createElement("span", null, "Copied")) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Icon, {
name: "copy",
size: 13
}), " ", /*#__PURE__*/React.createElement("span", null, "Copy")))), /*#__PURE__*/React.createElement("pre", {
className: "export-pre"
}, currentContent || '…generating…')), /*#__PURE__*/React.createElement("div", {
className: "dlg-foot"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("input", {
ref: fileInputRef,
type: "file",
accept: ".md,.json",
style: {
display: 'none'
},
onChange: handleFileInput
}), /*#__PURE__*/React.createElement("button", {
className: "btn-secondary",
onClick: () => fileInputRef.current?.click()
}, /*#__PURE__*/React.createElement(Icon, {
name: "upload",
size: 12
}), " Import file\u2026")), /*#__PURE__*/React.createElement("div", {
className: "dlg-foot-actions"
}, /*#__PURE__*/React.createElement("button", {
className: "btn-secondary",
onClick: onClose
}, "Close"), /*#__PURE__*/React.createElement("button", {
className: "btn-primary",
onClick: downloadFile
}, /*#__PURE__*/React.createElement(Icon, {
name: "download",
size: 12
}), " Download ", tab === 'md' ? '.md' : '.json'))));
}
// ----- Archive Search Results -----
function ArchiveSearchResults({
query,
archiveEntries,
onRestore,
onLoadArchive,
onView
}) {
const [loading, setLoading] = useStateMain(false);
const loadedRef = useRefMain(false);
// Auto-load archive on first render
useEffectMain(() => {
if (!loadedRef.current && archiveEntries.length === 0) {
loadedRef.current = true;
setLoading(true);
onLoadArchive().finally(() => setLoading(false));
}
}, []);
// Filter archive entries by query
const matches = [];
if (query) {
const q = query.toLowerCase();
walkTree(archiveEntries, item => {
if (item.title.toLowerCase().includes(q) || item.body?.toLowerCase().includes(q) || item.tags?.some(t => t.toLowerCase().includes(q))) {
matches.push(item);
}
});
}
if (loading) {
return /*#__PURE__*/React.createElement("div", {
className: "archive-search-section"
}, /*#__PURE__*/React.createElement("div", {
className: "archive-search-header"
}, "Archive \u2014 loading..."));
}
if (matches.length === 0) return null;
return /*#__PURE__*/React.createElement("div", {
className: "archive-search-section"
}, /*#__PURE__*/React.createElement("div", {
className: "archive-search-header"
}, "Archive (", matches.length, ")"), /*#__PURE__*/React.createElement("div", {
className: "archive-search-results"
}, matches.slice(0, 10).map(item => /*#__PURE__*/React.createElement("div", {
key: item.id,
className: "archive-search-item"
}, /*#__PURE__*/React.createElement(StatusIcon, {
status: item.status,
size: 14
}), /*#__PURE__*/React.createElement("span", {
className: "archive-search-title",
onDoubleClick: () => onView(item)
}, item.title), item.archived && /*#__PURE__*/React.createElement("span", {
className: "archive-search-date"
}, item.archived), /*#__PURE__*/React.createElement("button", {
className: "btn-secondary btn-xs",
onClick: () => onRestore([item.id])
}, "Restore"))), matches.length > 10 && /*#__PURE__*/React.createElement("div", {
className: "archive-search-more"
}, "...and ", matches.length - 10, " more (see Admin \u2192 Archive)")));
}
// Multi-project data-layer helpers — consumed by todos 6-8 (sections UI,
// dialogs, admin) and the todo-5 verification harness.
Object.assign(window, {
buildMergedDataFromProjects,
computeDirtyProjectIds,
retagSubtree,
toDiskIds
});
window.App = App;
</script>
<script>
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(/*#__PURE__*/React.createElement(App, null));
</script>
</body>
</html>