outcrop 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,4107 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, statSync, watch, existsSync } from "node:fs";
5
+ import { join, dirname, basename, resolve, relative, isAbsolute } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { createHash } from "node:crypto";
8
+ import { createGzip } from "node:zlib";
9
+ import { createServer } from "node:http";
10
+ import { Readable } from "node:stream";
11
+
12
+ // src/settings.ts
13
+ import { readFileSync, writeFileSync } from "node:fs";
14
+ var SETTINGS_FILENAME = ".outcrop.json";
15
+ var DEFAULT_COLUMNS = ["backlog", "ready", "in-progress", "in-review", "done", "blocked"];
16
+ var ALL_VIEWS = ["board", "docs", "graph"];
17
+ function normalizeSettings(input, fallbackTitle) {
18
+ const o = input && typeof input === "object" ? input : {};
19
+ const columns = Array.isArray(o.columns) ? [
20
+ ...new Set(o.columns.map((c) => String(c).trim().toLowerCase().replace(/\s+/g, "-")).filter((c) => /^[a-z0-9][a-z0-9-]*$/.test(c)))
21
+ ] : [];
22
+ const title = typeof o.title === "string" && o.title.trim() ? o.title.trim() : fallbackTitle;
23
+ const wanted = Array.isArray(o.views) ? o.views.map(String) : [];
24
+ const views = ALL_VIEWS.filter((v) => wanted.includes(v));
25
+ return {
26
+ title,
27
+ views: views.length ? views : [...ALL_VIEWS],
28
+ columns: columns.length ? columns : [...DEFAULT_COLUMNS],
29
+ gitCommit: o.gitCommit === true
30
+ };
31
+ }
32
+ function readSettings(file, fallbackTitle) {
33
+ try {
34
+ return normalizeSettings(JSON.parse(readFileSync(file, "utf8")), fallbackTitle);
35
+ } catch {
36
+ return normalizeSettings(null, fallbackTitle);
37
+ }
38
+ }
39
+ function writeSettings(file, s) {
40
+ writeFileSync(file, JSON.stringify(s, null, 2) + `
41
+ `);
42
+ }
43
+
44
+ // src/styles.gen.css
45
+ var styles_gen_default = "*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:\"\"}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-ui);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.detail-body h1,.detail-body h2,.detail-body h3,.detail-body h4{margin-top:1.25rem;margin-bottom:.375rem;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.detail-body h1:first-child,.detail-body h2:first-child,.detail-body h3:first-child,.detail-body h4:first-child{margin-top:0}.detail-body h1:is(.dark *),.detail-body h2:is(.dark *),.detail-body h3:is(.dark *),.detail-body h4:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.detail-body p{margin-top:.375rem;margin-bottom:.375rem;font-size:13px;line-height:1.625;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.detail-body p:is(.dark *){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.detail-body ul{margin-top:.375rem;margin-bottom:.375rem}.detail-body ul>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.detail-body ul{padding-left:1rem;font-size:13px;line-height:1.625;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.detail-body ul:is(.dark *){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.detail-body ul:not(.ordered)>li{list-style-type:disc}.detail-body li.check{margin-left:-1rem;display:flex;list-style-type:none;align-items:flex-start;gap:.5rem}.detail-body li.check .box{margin-top:.125rem;display:flex;height:15px;width:15px;flex-shrink:0;cursor:pointer;align-items:center;justify-content:center;border-radius:.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity,1));font-size:10px;line-height:1;--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.detail-body li.check .box:hover{--tw-border-opacity:1;border-color:rgb(16 185 129/var(--tw-border-opacity,1))}.detail-body li.check .box:is(.dark *){--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.detail-body li.check .box:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(16 185 129/var(--tw-border-opacity,1))}.detail-body li.check.done{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1));text-decoration-line:line-through}.detail-body li.check.done:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.detail-body li.check.done .box{border-color:rgba(16,185,129,.5);--tw-bg-opacity:1;background-color:rgb(236 253 245/var(--tw-bg-opacity,1))}.detail-body li.check.done .box:is(.dark *){background-color:rgba(16,185,129,.1)}.detail-body ul.ordered>li{list-style-type:none}.detail-body code{border-radius:.25rem;--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1));padding:1px .25rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.detail-body code:is(.dark *){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.detail-body pre{margin-top:.5rem;margin-bottom:.5rem;overflow-x:auto;border-radius:.375rem;--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1));padding:.75rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px;line-height:1.625;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.detail-body pre:is(.dark *){background-color:rgba(39,39,42,.8);--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.detail-body .ref{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px;--tw-text-opacity:1;color:rgb(var(--accent-600)/var(--tw-text-opacity,1))}.detail-body .ref:hover{text-decoration-line:underline}.detail-body .ref:is(.dark *){--tw-text-opacity:1;color:rgb(var(--accent-400)/var(--tw-text-opacity,1))}.detail-body table{margin-top:.5rem;margin-bottom:.5rem;width:100%;border-collapse:collapse;font-size:12.5px}.detail-body th{border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity,1));padding-top:.25rem;padding-bottom:.25rem;padding-right:.75rem;text-align:left;font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.detail-body th:is(.dark *){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.detail-body td{border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(244 244 245/var(--tw-border-opacity,1));padding-top:.375rem;padding-bottom:.375rem;padding-right:.75rem;vertical-align:top;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.detail-body td:is(.dark *){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.detail-body a.mdlink{--tw-text-opacity:1;color:rgb(var(--accent-600)/var(--tw-text-opacity,1))}.detail-body a.mdlink:hover{text-decoration-line:underline}.detail-body a.mdlink:is(.dark *){--tw-text-opacity:1;color:rgb(var(--accent-400)/var(--tw-text-opacity,1))}.doc-body h1{margin-bottom:.75rem;margin-top:.5rem;font-size:21px;font-weight:600;letter-spacing:-.025em;--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.doc-body h1:is(.dark *){--tw-text-opacity:1;color:rgb(250 250 250/var(--tw-text-opacity,1))}.doc-body h2{margin-bottom:.5rem;margin-top:1.75rem;border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity,1));padding-bottom:.375rem;font-size:16px;font-weight:600;--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.doc-body h2:is(.dark *){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.doc-body h3{margin-bottom:.375rem;margin-top:1.25rem;font-size:13.5px;font-weight:600;--tw-text-opacity:1;color:rgb(39 39 42/var(--tw-text-opacity,1))}.doc-body h3:is(.dark *){--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.doc-body h4{margin-bottom:.25rem;margin-top:1rem;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.doc-body h4:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.doc-body p{margin-top:.5rem;margin-bottom:.5rem;font-size:13.5px;line-height:1.625;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.doc-body p:is(.dark *){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.doc-body ul{margin-top:.5rem;margin-bottom:.5rem}.doc-body ul>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.doc-body ul{padding-left:1.25rem;font-size:13.5px;line-height:1.625;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.doc-body ul:is(.dark *){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.doc-body ul:not(.ordered)>li{list-style-type:disc}.doc-body ul.ordered>li{list-style-type:none}.doc-body li.check{margin-left:-1.25rem;display:flex;list-style-type:none;align-items:flex-start;gap:.5rem}.doc-body li.check .box{margin-top:.25rem;display:flex;height:14px;width:14px;flex-shrink:0;align-items:center;justify-content:center;border-radius:.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity,1));font-size:9px;line-height:1;--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity,1))}.doc-body li.check .box:is(.dark *){--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.doc-body li.check.done{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1));text-decoration-line:line-through}.doc-body li.check.done:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.doc-body code{border-radius:.25rem;--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1));padding:1px .25rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.doc-body code:is(.dark *){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.doc-body pre{margin-top:.75rem;margin-bottom:.75rem;overflow-x:auto;border-radius:.375rem;--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1));padding:.75rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px;line-height:1.625;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.doc-body pre:is(.dark *){background-color:rgba(39,39,42,.8);--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.doc-body table{margin-top:.75rem;margin-bottom:.75rem;width:100%;border-collapse:collapse;font-size:13px}.doc-body th{border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity,1));padding-top:.375rem;padding-bottom:.375rem;padding-right:1rem;text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.doc-body th:is(.dark *){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.doc-body td{border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(244 244 245/var(--tw-border-opacity,1));padding-top:.5rem;padding-bottom:.5rem;padding-right:1rem;vertical-align:top;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.doc-body td:is(.dark *){border-color:rgba(39,39,42,.6);--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.doc-body img{margin-left:auto;margin-right:auto;display:inline-block;height:auto;max-width:100%}.doc-body [align=center]{text-align:center}.doc-body [align=right]{text-align:right}.doc-body .ref,.doc-body a.mdlink{--tw-text-opacity:1;color:rgb(var(--accent-600)/var(--tw-text-opacity,1))}.doc-body .ref:hover,.doc-body a.mdlink:hover{text-decoration-line:underline}.doc-body .ref:is(.dark *),.doc-body a.mdlink:is(.dark *){--tw-text-opacity:1;color:rgb(var(--accent-400)/var(--tw-text-opacity,1))}.doc-body .ref{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12.5px}.doc-body .mermaid{margin-top:1rem;margin-bottom:1rem;display:flex;justify-content:center;overflow-x:auto}.doc-body strong{font-weight:600;--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.doc-body strong:is(.dark *){--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.detail-body hr,.doc-body hr{margin-top:1.5rem;margin-bottom:1.5rem;--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity,1))}.detail-body hr:is(.dark *),.doc-body hr:is(.dark *){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.doc-folder>summary{display:flex;cursor:pointer;list-style-type:none;align-items:center;gap:.375rem;border-radius:.375rem;padding:.25rem .5rem;font-size:13.5px;font-weight:600;--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.doc-folder>summary:hover{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1))}.doc-folder>summary:is(.dark *){--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.doc-folder>summary:hover:is(.dark *){background-color:rgba(39,39,42,.7)}.doc-folder>summary::-webkit-details-marker{display:none}.doc-folder>summary:before{content:\"›\";display:inline-block;width:.5rem;flex-shrink:0;font-size:13px;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.doc-folder>summary:is(.dark *):before{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.doc-folder[open]>summary:before{transform:rotate(90deg)}.doc-folder-count{margin-left:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:10px;font-weight:400;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.doc-folder-count:is(.dark *){--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.doc-folder+.doc-folder{margin-top:.375rem}.doc-link{margin-left:9px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:.375rem;border-left-width:1px;--tw-border-opacity:1;border-color:rgb(244 244 245/var(--tw-border-opacity,1));padding:.25rem .5rem .25rem .625rem;font-size:12px;--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.doc-link:hover{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1))}.doc-link:is(.dark *){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.doc-link:hover:is(.dark *){background-color:rgba(39,39,42,.7)}.doc-link.active{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1));font-weight:500;--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.doc-link.active:is(.dark *){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}#doc-outline.empty{display:none}.outline-link{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-left-width:2px;border-color:transparent;padding:.25rem .25rem .25rem .625rem;font-size:12px;line-height:1.375;--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.outline-link:hover{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.outline-link:is(.dark *){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.outline-link:hover:is(.dark *){--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.outline-link.lvl-2{padding-left:1.25rem}.outline-link.lvl-3{padding-left:2rem;font-size:11.5px}.outline-link.active{--tw-border-opacity:1;border-color:rgb(var(--accent-500)/var(--tw-border-opacity,1));font-weight:500;--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.outline-link.active:is(.dark *){--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}#doc-sidebar.collapsed,#doc-sidebar.collapsed+.sidebar-resizer{display:none!important}#search-results mark{border-radius:.125rem;--tw-bg-opacity:1;background-color:rgb(var(--accent-200)/var(--tw-bg-opacity,1));padding-left:.125rem;padding-right:.125rem;font-weight:500;--tw-text-opacity:1;color:rgb(var(--accent-950)/var(--tw-text-opacity,1))}#search-results mark:is(.dark *){background-color:rgb(var(--accent-600)/.5);--tw-text-opacity:1;color:rgb(var(--accent-100)/var(--tw-text-opacity,1))}::highlight(doc-search){background-color:rgb(var(--accent-300));color:rgb(var(--accent-950))}.dark ::highlight(doc-search){background-color:rgb(var(--accent-600));color:#fff}::highlight(doc-match){background-color:rgb(var(--accent-500));color:#fff}.dark ::highlight(doc-match){background-color:rgb(var(--accent-300));color:rgb(var(--accent-950))}.swatch{height:1.5rem;width:1.5rem;border-radius:9999px;border-width:2px;border-color:transparent;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(228 228 231/var(--tw-ring-opacity,1));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.swatch:hover{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.swatch:is(.dark *){--tw-ring-opacity:1;--tw-ring-color:rgb(63 63 70/var(--tw-ring-opacity,1))}.swatch.active{--tw-border-opacity:1;border-color:rgb(24 24 27/var(--tw-border-opacity,1))}.swatch.active:is(.dark *){--tw-border-opacity:1;border-color:rgb(244 244 245/var(--tw-border-opacity,1))}.swatch-violet{background-color:#8b5cf6}.swatch-blue{background-color:#3b82f6}.swatch-emerald{background-color:#10b981}.swatch-amber{background-color:#f59e0b}.swatch-rose{background-color:#f43f5e}.pick{flex:1 1 0%;border-radius:.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity,1));padding:.25rem .5rem;font-size:12px;--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.pick:hover{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity,1))}.pick:is(.dark *){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.pick:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1))}.pick.active{--tw-border-opacity:1;border-color:rgb(var(--accent-400)/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(var(--accent-50)/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(var(--accent-700)/var(--tw-text-opacity,1))}.pick.active:is(.dark *){--tw-border-opacity:1;border-color:rgb(var(--accent-500)/var(--tw-border-opacity,1));background-color:rgb(var(--accent-500)/.1);--tw-text-opacity:1;color:rgb(var(--accent-300)/var(--tw-text-opacity,1))}.theme-opt.theme-active{--tw-border-opacity:1;border-color:rgb(var(--accent-400)/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(var(--accent-50)/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(var(--accent-700)/var(--tw-text-opacity,1))}.theme-opt.theme-active:is(.dark *){--tw-border-opacity:1;border-color:rgb(var(--accent-500)/var(--tw-border-opacity,1));background-color:rgb(var(--accent-500)/.1);--tw-text-opacity:1;color:rgb(var(--accent-300)/var(--tw-text-opacity,1))}.set-group{border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(244 244 245/var(--tw-border-opacity,1))}.set-group:is(.dark *){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.set-group>summary{display:flex;cursor:pointer;list-style-type:none;align-items:center;gap:.5rem;padding-top:.625rem;padding-bottom:.625rem;font-size:12.5px;font-weight:500;--tw-text-opacity:1;color:rgb(39 39 42/var(--tw-text-opacity,1))}.set-group>summary:is(.dark *){--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.set-group>summary::-webkit-details-marker{display:none}.set-group>summary:after{content:\"›\";margin-left:auto;font-size:15px;line-height:1;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.set-group>summary:is(.dark *):after{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.set-group[open]>summary:after{transform:rotate(90deg)}.set-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:400;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.set-sub:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.set-row{display:flex;align-items:flex-start;gap:.75rem;padding-top:.375rem;padding-bottom:.375rem}.set-label{width:72px;flex-shrink:0;padding-top:.375rem;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.set-label:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}#sheet.fullscreen{max-width:none}#sheet.fullscreen #sheet-content{margin-left:auto;margin-right:auto;max-width:42rem}.task-card{width:100%;cursor:pointer;border-radius:.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));padding:.625rem .75rem;text-align:left;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.task-card:hover{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity,1))}.task-card:focus-visible{outline-style:solid;outline-width:2px;outline-offset:1px;outline-color:rgb(var(--accent-500)/1)}.task-card:is(.dark *){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity,1))}.task-card:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1))}.card-id{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:11px;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.card-id:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.card-title{font-size:13px;font-weight:500;line-height:1.375;--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.card-title:is(.dark *){--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.card-progress{margin-left:auto;display:inline-flex;align-items:center;gap:.375rem;font-size:10px;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.card-progress:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.epic-chip{border-radius:.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity,1));padding:1px .375rem;font-size:10px;font-weight:500;--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.epic-chip:hover{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.epic-chip:is(.dark *){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.epic-chip:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.prio{display:inline-flex;align-items:center;gap:.25rem;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.prio:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.prio-label{font-size:10px;font-weight:500;letter-spacing:.025em}.dep-chip{display:inline-flex;align-items:center;gap:.25rem;border-radius:.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity,1));padding:1px .375rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:10px;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.dep-chip:hover{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.dep-chip:is(.dark *){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.dep-chip:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.dep-chip.done{border-color:transparent;--tw-bg-opacity:1;background-color:rgb(236 253 245/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity,1))}.dep-chip.done:hover{--tw-bg-opacity:1;background-color:rgb(209 250 229/var(--tw-bg-opacity,1))}.dep-chip.done:is(.dark *){background-color:rgba(16,185,129,.1);--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.dep-chip.done:hover:is(.dark *){background-color:rgba(16,185,129,.2)}.backlink-row{display:flex;width:100%;align-items:center;gap:.5rem;border-radius:.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(244 244 245/var(--tw-border-opacity,1));padding:.375rem .625rem;text-align:left;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.backlink-row:hover{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity,1))}.backlink-row:is(.dark *){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.backlink-row:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1))}.epic-row{display:flex;width:100%;cursor:pointer;align-items:center;gap:.5rem;border-radius:.375rem;padding:.375rem .5rem;text-align:left;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.epic-row:hover{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1))}.epic-row:hover:is(.dark *){background-color:rgba(39,39,42,.7)}.epic-row.epic-active{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1))}.epic-row.epic-active:is(.dark *){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1))}#sidebar>*{flex-shrink:0}#sidebar.collapsed,#sidebar.collapsed+.sidebar-resizer{display:none!important}section.drop-target{background-color:rgb(var(--accent-50)/.6);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-inset:inset;--tw-ring-opacity:1;--tw-ring-color:rgb(var(--accent-300)/var(--tw-ring-opacity,1))}section.drop-target:is(.dark *){background-color:rgb(var(--accent-500)/.05);--tw-ring-color:rgb(var(--accent-500)/0.4)}.task-card.\\!dragging,.task-card.dragging{opacity:.4}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-top-0\\.5{top:-.125rem}.bottom-3{bottom:.75rem}.bottom-5{bottom:1.25rem}.left-1{left:.25rem}.left-1\\/2{left:50%}.left-3{left:.75rem}.left-44{left:11rem}.right-0{right:0}.right-3{right:.75rem}.right-4{right:1rem}.right-5{right:1.25rem}.top-0{top:0}.top-4{top:1rem}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.z-\\[60\\]{z-index:60}.mx-auto{margin-left:auto;margin-right:auto}.-ml-0\\.5{margin-left:-.125rem}.mb-1{margin-bottom:.25rem}.mb-1\\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-2\\.5{margin-bottom:.625rem}.mb-3{margin-bottom:.75rem}.mb-8{margin-bottom:2rem}.ml-3{margin-left:.75rem}.ml-auto{margin-left:auto}.mr-1\\.5{margin-right:.375rem}.mr-16{margin-right:4rem}.mt-1{margin-top:.25rem}.mt-1\\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\\.5{height:.375rem}.h-12{height:3rem}.h-2{height:.5rem}.h-3\\.5{height:.875rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-\\[calc\\(\\(100vh\\/var\\(--zoom\\)\\)-3rem\\)\\]{height:calc(100vh/var(--zoom) - 3rem)}.h-\\[calc\\(100vh\\/var\\(--zoom\\)\\)\\]{height:calc(100vh/var(--zoom))}.h-full{height:100%}.min-h-\\[60px\\]{min-height:60px}.w-1{width:.25rem}.w-1\\.5{width:.375rem}.w-2{width:.5rem}.w-3\\.5{width:.875rem}.w-44{width:11rem}.w-56{width:14rem}.w-60{width:15rem}.w-\\[290px\\]{width:290px}.w-full{width:100%}.min-w-0{min-width:0}.max-w-3xl{max-width:48rem}.max-w-\\[460px\\]{max-width:460px}.max-w-full{max-width:100%}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-1\\/2{--tw-translate-x:-50%}.-translate-x-1\\/2,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x:100%}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-col-resize{cursor:col-resize}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\\[90px_1fr\\]{grid-template-columns:90px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-y-1\\.5{row-gap:.375rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-zinc-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(228 228 231/var(--tw-divide-opacity,1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.border{border-width:1px}.border-y{border-top-width:1px}.border-b,.border-y{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-rose-300{--tw-border-opacity:1;border-color:rgb(253 164 175/var(--tw-border-opacity,1))}.border-rose-500\\/70{border-color:rgba(244,63,94,.7)}.border-zinc-100{--tw-border-opacity:1;border-color:rgb(244 244 245/var(--tw-border-opacity,1))}.border-zinc-200{--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity,1))}.bg-accent-50{--tw-bg-opacity:1;background-color:rgb(var(--accent-50)/var(--tw-bg-opacity,1))}.bg-accent-600{--tw-bg-opacity:1;background-color:rgb(var(--accent-600)/var(--tw-bg-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-black\\/20{background-color:rgba(0,0,0,.2)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-rose-50{--tw-bg-opacity:1;background-color:rgb(255 241 242/var(--tw-bg-opacity,1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-zinc-100{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1))}.bg-zinc-300{--tw-bg-opacity:1;background-color:rgb(212 212 216/var(--tw-bg-opacity,1))}.bg-zinc-50{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity,1))}.bg-zinc-900{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity,1))}.p-0\\.5{padding:.125rem}.p-1{padding:.25rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-96{padding:24rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\\.5{padding-top:.875rem;padding-bottom:.875rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-1{padding-bottom:.25rem}.pb-3{padding-bottom:.75rem}.pl-2\\.5{padding-left:.625rem}.pr-1{padding-right:.25rem}.pr-3{padding-right:.75rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:var(--font-ui)}.font-serif{font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif}.text-\\[10\\.5px\\]{font-size:10.5px}.text-\\[10px\\]{font-size:10px}.text-\\[11px\\]{font-size:11px}.text-\\[12\\.5px\\]{font-size:12.5px}.text-\\[12px\\]{font-size:12px}.text-\\[13px\\]{font-size:13px}.text-\\[16px\\]{font-size:16px}.text-\\[9\\.5px\\]{font-size:9.5px}.text-\\[9px\\]{font-size:9px}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.normal-case{text-transform:none}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-accent-600{--tw-text-opacity:1;color:rgb(var(--accent-600)/var(--tw-text-opacity,1))}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-rose-500{--tw-text-opacity:1;color:rgb(244 63 94/var(--tw-text-opacity,1))}.text-rose-700{--tw-text-opacity:1;color:rgb(190 18 60/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-zinc-100{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.text-zinc-300{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.text-zinc-700{--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.text-zinc-800{--tw-text-opacity:1;color:rgb(39 39 42/var(--tw-text-opacity,1))}.text-zinc-900{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-accent-600{accent-color:rgb(var(--accent-600)/1)}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-sm,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}:root,html[data-accent=violet]{--accent-50:245 243 255;--accent-100:237 233 254;--accent-200:221 214 254;--accent-300:196 181 253;--accent-400:167 139 250;--accent-500:139 92 246;--accent-600:124 58 237;--accent-700:109 40 217;--accent-950:46 16 101}html[data-accent=blue]{--accent-50:239 246 255;--accent-100:219 234 254;--accent-200:191 219 254;--accent-300:147 197 253;--accent-400:96 165 250;--accent-500:59 130 246;--accent-600:37 99 235;--accent-700:29 78 216;--accent-950:23 37 84}html[data-accent=emerald]{--accent-50:236 253 245;--accent-100:209 250 229;--accent-200:167 243 208;--accent-300:110 231 183;--accent-400:52 211 153;--accent-500:16 185 129;--accent-600:5 150 105;--accent-700:4 120 87;--accent-950:2 44 34}html[data-accent=amber]{--accent-50:255 251 235;--accent-100:254 243 199;--accent-200:253 230 138;--accent-300:252 211 77;--accent-400:251 191 36;--accent-500:245 158 11;--accent-600:217 119 6;--accent-700:180 83 9;--accent-950:69 26 3}html[data-accent=rose]{--accent-50:255 241 242;--accent-100:255 228 230;--accent-200:254 205 211;--accent-300:253 164 175;--accent-400:251 113 133;--accent-500:244 63 94;--accent-600:225 29 72;--accent-700:190 18 60;--accent-950:76 5 25}:root,html[data-font=sans]{--font-ui:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif}html[data-font=serif]{--font-ui:ui-serif,Georgia,Cambria,\"Times New Roman\",Times,serif}html[data-font=mono]{--font-ui:ui-monospace,SFMono-Regular,\"SF Mono\",Menlo,Consolas,\"Liberation Mono\",monospace}:root{--zoom:1}html[data-size=compact]{--zoom:0.9}html[data-size=large]{--zoom:1.15}html[data-size=compact] body,html[data-size=large] body{zoom:var(--zoom)}@media (prefers-reduced-motion:no-preference){html.theme-anim,html.theme-anim *,html.theme-anim :after,html.theme-anim :before{transition:background-color .26s cubic-bezier(.4,0,.2,1),border-color .26s cubic-bezier(.4,0,.2,1),color .26s cubic-bezier(.4,0,.2,1),fill .26s cubic-bezier(.4,0,.2,1),stroke .26s cubic-bezier(.4,0,.2,1),box-shadow .26s cubic-bezier(.4,0,.2,1)!important;transition-delay:0s!important}}.placeholder\\:text-zinc-400::-moz-placeholder{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.placeholder\\:text-zinc-400::placeholder{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.hover\\:border-zinc-300:hover{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity,1))}.hover\\:bg-accent-400\\/70:hover{background-color:rgb(var(--accent-400)/.7)}.hover\\:bg-accent-500:hover{--tw-bg-opacity:1;background-color:rgb(var(--accent-500)/var(--tw-bg-opacity,1))}.hover\\:bg-zinc-100:hover{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1))}.hover\\:bg-zinc-200:hover{--tw-bg-opacity:1;background-color:rgb(228 228 231/var(--tw-bg-opacity,1))}.hover\\:bg-zinc-50:hover{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity,1))}.hover\\:text-zinc-600:hover{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.hover\\:text-zinc-900:hover{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.hover\\:underline:hover{text-decoration-line:underline}.focus\\:border-accent-400:focus{--tw-border-opacity:1;border-color:rgb(var(--accent-400)/var(--tw-border-opacity,1))}.focus\\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus-visible\\:opacity-100:focus-visible{opacity:1}.active\\:bg-accent-400\\/70:active{background-color:rgb(var(--accent-400)/.7)}.group:hover .group-hover\\:opacity-100{opacity:1}@media (prefers-reduced-motion:no-preference){.motion-safe\\:transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.motion-safe\\:duration-200{transition-duration:.2s}}.dark\\:divide-zinc-800:is(.dark *)>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(39 39 42/var(--tw-divide-opacity,1))}.dark\\:border-rose-500\\/40:is(.dark *){border-color:rgba(244,63,94,.4)}.dark\\:border-zinc-700:is(.dark *){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1))}.dark\\:border-zinc-800:is(.dark *){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.dark\\:bg-accent-500\\/10:is(.dark *){background-color:rgb(var(--accent-500)/.1)}.dark\\:bg-amber-500\\/10:is(.dark *){background-color:rgba(245,158,11,.1)}.dark\\:bg-black\\/50:is(.dark *){background-color:rgba(0,0,0,.5)}.dark\\:bg-rose-500\\/10:is(.dark *){background-color:rgba(244,63,94,.1)}.dark\\:bg-zinc-100:is(.dark *){--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity,1))}.dark\\:bg-zinc-800:is(.dark *){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1))}.dark\\:bg-zinc-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity,1))}.dark\\:bg-zinc-950:is(.dark *){--tw-bg-opacity:1;background-color:rgb(9 9 11/var(--tw-bg-opacity,1))}.dark\\:text-accent-400:is(.dark *){--tw-text-opacity:1;color:rgb(var(--accent-400)/var(--tw-text-opacity,1))}.dark\\:text-amber-400:is(.dark *){--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.dark\\:text-rose-300:is(.dark *){--tw-text-opacity:1;color:rgb(253 164 175/var(--tw-text-opacity,1))}.dark\\:text-zinc-100:is(.dark *){--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.dark\\:text-zinc-200:is(.dark *){--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.dark\\:text-zinc-300:is(.dark *){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.dark\\:text-zinc-400:is(.dark *){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.dark\\:text-zinc-50:is(.dark *){--tw-text-opacity:1;color:rgb(250 250 250/var(--tw-text-opacity,1))}.dark\\:text-zinc-500:is(.dark *){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.dark\\:text-zinc-600:is(.dark *){--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.dark\\:text-zinc-700:is(.dark *){--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity,1))}.dark\\:text-zinc-900:is(.dark *){--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.dark\\:brightness-125:is(.dark *){--tw-brightness:brightness(1.25);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.dark\\:placeholder\\:text-zinc-600:is(.dark *)::-moz-placeholder{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.dark\\:placeholder\\:text-zinc-600:is(.dark *)::placeholder{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.dark\\:hover\\:border-zinc-600:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1))}.dark\\:hover\\:bg-accent-500\\/60:hover:is(.dark *){background-color:rgb(var(--accent-500)/.6)}.dark\\:hover\\:bg-zinc-700:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity,1))}.dark\\:hover\\:bg-zinc-800:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1))}.dark\\:hover\\:bg-zinc-900:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity,1))}.dark\\:hover\\:text-zinc-100:hover:is(.dark *){--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.dark\\:hover\\:text-zinc-300:hover:is(.dark *){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.dark\\:focus\\:border-accent-500:focus:is(.dark *){--tw-border-opacity:1;border-color:rgb(var(--accent-500)/var(--tw-border-opacity,1))}.dark\\:active\\:bg-accent-500\\/60:active:is(.dark *){background-color:rgb(var(--accent-500)/.6)}@media (min-width:640px){.sm\\:block{display:block}}@media (min-width:768px){.md\\:block{display:block}.md\\:flex{display:flex}}@media (min-width:1024px){.lg\\:block{display:block}}.dark\\:\\[\\&\\>option\\]\\:bg-zinc-900>option:is(.dark *){--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity,1))}";
46
+
47
+ // assets/logo.svg
48
+ var logo_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="120 12 800 800">
49
+ <title>outcrop</title>
50
+ <desc>A rock outcrop: faceted strata breaking the surface.</desc>
51
+ <g id="outcrop-mark">
52
+ <path d="M140 640 L215 590 L275 500 L330 530 L385 400 L455 330 L515 170 L580 280 L650 245 L710 365 L780 430 L850 585 L900 640 Z" fill="#24282d"/>
53
+ <path d="M385 400 L455 330 L515 170 L525 410 L455 520 Z" fill="#e2e6ea"/>
54
+ <path d="M525 410 L515 170 L580 280 L650 245 L625 430 Z" fill="#59616a"/>
55
+ <path d="M625 430 L650 245 L710 365 L690 510 Z" fill="#30363d"/>
56
+ <path d="M455 520 L525 410 L625 430 L570 575 Z" fill="#414850"/>
57
+ <path d="M275 500 L330 530 L385 400 L410 525 L350 585 Z" fill="#7c858e"/>
58
+ <path d="M650 245 L710 365 L780 430 L720 465 L690 510 Z" fill="#747d86"/>
59
+ <path d="M140 640 L215 590 L275 600 L330 575 L350 585 L410 560 L455 590 L570 575 L630 610 L690 510 L720 465 L780 430 L850 585 L900 640 Z" fill="#343a41"/>
60
+ <path d="M215 590 L275 500 L275 600 Z" fill="#9aa1a8"/>
61
+ <path d="M350 585 L410 525 L455 590 Z" fill="#b9bec3"/>
62
+ <path d="M570 575 L625 430 L630 610 Z" fill="#8b939b"/>
63
+ <path d="M690 510 L720 465 L780 430 L760 550 Z" fill="#aeb4b9"/>
64
+ <path d="M140 640 L900 640 L850 655 L175 655 Z" fill="#15191d"/>
65
+ </g>
66
+ </svg>
67
+ `;
68
+
69
+ // src/generated/head.js
70
+ var head_default = `(() => {
71
+ // src/client/prefs.ts
72
+ var THEMES = ["light", "dark"];
73
+ var APPEARANCE = {
74
+ accent: ["violet", "blue", "emerald", "amber", "rose"],
75
+ font: ["sans", "serif", "mono"],
76
+ size: ["compact", "default", "large"]
77
+ };
78
+ var DEFAULTS = { accent: "violet", font: "sans", size: "default" };
79
+ function getPref(name) {
80
+ const stored = localStorage.getItem("outcrop." + name);
81
+ return stored && APPEARANCE[name].includes(stored) ? stored : DEFAULTS[name];
82
+ }
83
+ function setPref(name, value) {
84
+ localStorage.setItem("outcrop." + name, value);
85
+ applyAppearance();
86
+ }
87
+ function applyAppearance() {
88
+ for (const name of Object.keys(APPEARANCE)) {
89
+ document.documentElement.dataset[name] = getPref(name);
90
+ }
91
+ }
92
+ function currentTheme() {
93
+ const stored = localStorage.getItem("theme");
94
+ if (stored === "light" || stored === "dark")
95
+ return stored;
96
+ return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
97
+ }
98
+ function applyTheme() {
99
+ const theme = currentTheme();
100
+ document.documentElement.classList.toggle("dark", theme === "dark");
101
+ return theme;
102
+ }
103
+
104
+ // src/client/head.ts
105
+ applyAppearance();
106
+ applyTheme();
107
+ })();
108
+ `;
109
+
110
+ // src/generated/board.js
111
+ var board_default = `(() => {
112
+ // src/client/prefs.ts
113
+ var THEMES = ["light", "dark"];
114
+ var APPEARANCE = {
115
+ accent: ["violet", "blue", "emerald", "amber", "rose"],
116
+ font: ["sans", "serif", "mono"],
117
+ size: ["compact", "default", "large"]
118
+ };
119
+ var DEFAULTS = { accent: "violet", font: "sans", size: "default" };
120
+ function getPref(name) {
121
+ const stored = localStorage.getItem("outcrop." + name);
122
+ return stored && APPEARANCE[name].includes(stored) ? stored : DEFAULTS[name];
123
+ }
124
+ function setPref(name, value) {
125
+ localStorage.setItem("outcrop." + name, value);
126
+ applyAppearance();
127
+ }
128
+ function applyAppearance() {
129
+ for (const name of Object.keys(APPEARANCE)) {
130
+ document.documentElement.dataset[name] = getPref(name);
131
+ }
132
+ }
133
+ function currentTheme() {
134
+ const stored = localStorage.getItem("theme");
135
+ if (stored === "light" || stored === "dark")
136
+ return stored;
137
+ return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
138
+ }
139
+ function applyTheme() {
140
+ const theme = currentTheme();
141
+ document.documentElement.classList.toggle("dark", theme === "dark");
142
+ return theme;
143
+ }
144
+
145
+ // src/client/chrome.ts
146
+ var THEME_ANIM_MS = 260;
147
+ var hooks = {};
148
+ var sidebarWidthKey = "sidebarWidth";
149
+ function applySidebarWidth() {
150
+ const sb = document.querySelector(".sidebar-resizer")?.previousElementSibling;
151
+ const w = localStorage.getItem(sidebarWidthKey);
152
+ if (sb && w)
153
+ sb.style.width = w + "px";
154
+ }
155
+ function initResizer() {
156
+ applySidebarWidth();
157
+ document.addEventListener("pointerdown", (e) => {
158
+ const rz = e.target.closest?.(".sidebar-resizer");
159
+ if (!rz)
160
+ return;
161
+ e.preventDefault();
162
+ const sb = rz.previousElementSibling;
163
+ const left = sb.getBoundingClientRect().left;
164
+ rz.setPointerCapture(e.pointerId);
165
+ const onMove = (ev) => {
166
+ const w = Math.min(448, Math.max(176, Math.round(ev.clientX - left)));
167
+ sb.style.width = w + "px";
168
+ localStorage.setItem(sidebarWidthKey, String(w));
169
+ };
170
+ rz.addEventListener("pointermove", onMove);
171
+ rz.addEventListener("pointerup", () => rz.removeEventListener("pointermove", onMove), { once: true });
172
+ });
173
+ }
174
+ var THEME_ICONS = {
175
+ light: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.4"/><path d="M8 1v1.6M8 13.4V15M15 8h-1.6M2.6 8H1M12.9 3.1l-1.1 1.1M4.2 11.8l-1.1 1.1M12.9 12.9l-1.1-1.1M4.2 4.2 3.1 3.1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
176
+ dark: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M13.5 9.6A6 6 0 0 1 6.4 2.5a6 6 0 1 0 7.1 7.1Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>'
177
+ };
178
+ var themeBtn;
179
+ function renderTheme() {
180
+ const t = applyTheme();
181
+ themeBtn.innerHTML = THEME_ICONS[t];
182
+ themeBtn.title = "Switch to " + (t === "dark" ? "light" : "dark") + " mode";
183
+ for (const b of document.querySelectorAll("[data-set-theme]")) {
184
+ b.classList.toggle("theme-active", b.dataset.setTheme === t);
185
+ }
186
+ renderAppearance();
187
+ }
188
+ function renderAppearance() {
189
+ for (const b of document.querySelectorAll("[data-set-appearance]")) {
190
+ b.classList.toggle("active", getPref(b.dataset.setAppearance) === b.dataset.value);
191
+ }
192
+ }
193
+ var themeAnimTimer;
194
+ function setTheme(t) {
195
+ const changed = t !== currentTheme();
196
+ localStorage.setItem("theme", t);
197
+ if (changed) {
198
+ document.documentElement.classList.add("theme-anim");
199
+ clearTimeout(themeAnimTimer);
200
+ themeAnimTimer = setTimeout(() => document.documentElement.classList.remove("theme-anim"), THEME_ANIM_MS + 60);
201
+ }
202
+ renderTheme();
203
+ if (changed)
204
+ hooks.onAppearanceChange?.();
205
+ }
206
+ function initTheme() {
207
+ themeBtn = document.getElementById("theme-toggle");
208
+ themeBtn.addEventListener("click", () => setTheme(THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length]));
209
+ document.addEventListener("click", (e) => {
210
+ const target = e.target;
211
+ const theme = target.closest("[data-set-theme]");
212
+ if (theme) {
213
+ setTheme(theme.dataset.setTheme);
214
+ return;
215
+ }
216
+ const pref = target.closest("[data-set-appearance]");
217
+ if (!pref)
218
+ return;
219
+ setPref(pref.dataset.setAppearance, pref.dataset.value);
220
+ renderAppearance();
221
+ hooks.onAppearanceChange?.();
222
+ });
223
+ matchMedia("(prefers-color-scheme: dark)").addEventListener("change", renderTheme);
224
+ renderTheme();
225
+ }
226
+ var sheet;
227
+ var sheetContent;
228
+ var backdrop;
229
+ var lastFocus = null;
230
+ var toastTimer;
231
+ function showToast(msg) {
232
+ const toast = document.getElementById("toast");
233
+ toast.textContent = msg;
234
+ toast.classList.add("opacity-100");
235
+ clearTimeout(toastTimer);
236
+ toastTimer = setTimeout(() => toast.classList.remove("opacity-100"), 2500);
237
+ }
238
+ function sheetIsOpen() {
239
+ return !sheet.classList.contains("translate-x-full");
240
+ }
241
+ function showSheet(id) {
242
+ backdrop.classList.remove("hidden");
243
+ sheet.classList.remove("translate-x-full");
244
+ if (!sheet.contains(document.activeElement))
245
+ lastFocus = document.activeElement;
246
+ sheet.focus({ preventScroll: true });
247
+ sheet.scrollTop = 0;
248
+ history.replaceState(null, "", "#" + id);
249
+ }
250
+ function closeSheet() {
251
+ sheet.classList.add("translate-x-full");
252
+ backdrop.classList.add("hidden");
253
+ lastFocus?.focus({ preventScroll: true });
254
+ history.replaceState(null, "", location.pathname + location.search);
255
+ }
256
+ var sheetSeq = 0;
257
+ var paneCache = new Map;
258
+ function clearPaneCache() {
259
+ paneCache.clear();
260
+ }
261
+ async function openSheet(id) {
262
+ const seq = ++sheetSeq;
263
+ const tpl = document.getElementById("detail-" + id);
264
+ if (tpl) {
265
+ sheetContent.replaceChildren(tpl.content.cloneNode(true));
266
+ renderTheme();
267
+ const form = sheetContent.querySelector('.detail-root[data-id="new"]');
268
+ if (form) {
269
+ setKind(form, localStorage.getItem("newKind") || "task");
270
+ showSheet(id);
271
+ form.querySelector('[name="title"]')?.focus();
272
+ return;
273
+ }
274
+ showSheet(id);
275
+ return;
276
+ }
277
+ const cached = paneCache.get(id);
278
+ if (cached !== undefined) {
279
+ sheetContent.innerHTML = cached;
280
+ showSheet(id);
281
+ return;
282
+ }
283
+ sheetContent.innerHTML = '<div class="pt-1 text-[12px] text-zinc-400 dark:text-zinc-500">Loading…</div>';
284
+ showSheet(id);
285
+ let html = null;
286
+ try {
287
+ const res = await fetch("/api/items/" + encodeURIComponent(id) + "/pane");
288
+ if (res.ok)
289
+ html = await res.text();
290
+ } catch {}
291
+ if (seq !== sheetSeq)
292
+ return;
293
+ if (html === null) {
294
+ sheetContent.innerHTML = '<div class="pt-1 text-[12px] text-zinc-400">Not found.</div>';
295
+ return;
296
+ }
297
+ paneCache.set(id, html);
298
+ sheetContent.innerHTML = html;
299
+ }
300
+ async function saveSettings(root) {
301
+ const val = (n) => root.querySelector(\`[name="\${n}"]\`);
302
+ const err = root.querySelector("#settings-error");
303
+ const views = [...root.querySelectorAll('[name^="view-"]')].filter((c) => c.checked).map((c) => c.name.slice(5));
304
+ if (!views.length) {
305
+ err.textContent = "Keep at least one view on: with all of them off there is nothing to show.";
306
+ err.classList.remove("hidden");
307
+ root.querySelector(".set-group:has([name^='view-'])")?.setAttribute("open", "");
308
+ return;
309
+ }
310
+ err.classList.add("hidden");
311
+ try {
312
+ const res = await fetch("/api/settings", {
313
+ method: "PUT",
314
+ headers: { "content-type": "application/json" },
315
+ body: JSON.stringify({
316
+ title: val("title").value,
317
+ views,
318
+ columns: val("columns").value.split(","),
319
+ gitCommit: val("gitCommit").checked
320
+ })
321
+ });
322
+ if (!res.ok) {
323
+ const body = await res.json().catch(() => null);
324
+ err.textContent = body?.error ?? "Save failed.";
325
+ err.classList.remove("hidden");
326
+ return;
327
+ }
328
+ showToast("Settings saved to .outcrop.json");
329
+ const saved = await res.json();
330
+ if (root.dataset.views !== saved.views.join(","))
331
+ return location.reload();
332
+ closeSheet();
333
+ await hooks.refreshView?.();
334
+ } catch {
335
+ err.textContent = "Save failed: server unreachable.";
336
+ err.classList.remove("hidden");
337
+ }
338
+ }
339
+ async function toggleCheckbox(url, index, checked, rev) {
340
+ try {
341
+ const res = await fetch(url, {
342
+ method: "PATCH",
343
+ headers: { "content-type": "application/json" },
344
+ body: JSON.stringify({ checkbox: { index, checked }, rev })
345
+ });
346
+ if (res.status === 409) {
347
+ showToast("Changed on disk elsewhere: showing latest");
348
+ } else if (!res.ok) {
349
+ showToast("Update failed");
350
+ return;
351
+ }
352
+ await hooks.refreshView?.();
353
+ } catch {
354
+ showToast("Update failed: server unreachable");
355
+ }
356
+ }
357
+ function initCheckboxes() {
358
+ document.addEventListener("click", (e) => {
359
+ const cb = e.target.closest("[data-cb]");
360
+ if (!cb)
361
+ return;
362
+ const index = Number(cb.dataset.cb);
363
+ const checked = !cb.classList.contains("checked");
364
+ cb.classList.toggle("checked", checked);
365
+ cb.closest("li.check")?.classList.toggle("done", checked);
366
+ const item = cb.closest(".detail-root[data-id][data-rev]");
367
+ if (item) {
368
+ toggleCheckbox("/api/items/" + encodeURIComponent(item.dataset.id), index, checked, item.dataset.rev);
369
+ return;
370
+ }
371
+ const doc = cb.closest("#doc-content[data-file]");
372
+ if (doc) {
373
+ toggleCheckbox("/api/docs?file=" + encodeURIComponent(doc.dataset.file), index, checked, doc.dataset.rev);
374
+ }
375
+ });
376
+ }
377
+ var KIND_ENDPOINT = { task: "/api/tasks", epic: "/api/epics", doc: "/api/docs" };
378
+ function setKind(root, kind) {
379
+ root.dataset.kind = kind;
380
+ localStorage.setItem("newKind", kind);
381
+ for (const b of root.querySelectorAll("[data-kind]")) {
382
+ b.classList.toggle("active", b.dataset.kind === kind);
383
+ }
384
+ for (const g of root.querySelectorAll("[data-for]")) {
385
+ g.classList.toggle("hidden", g.dataset.for !== kind);
386
+ }
387
+ root.querySelector("#new-target").textContent = kind === "doc" ? "" : kind === "epic" ? "epics/" : "tasks/";
388
+ }
389
+ async function createItem(root) {
390
+ const kind = root.dataset.kind || "task";
391
+ const val = (n) => root.querySelector(\`[name="\${n}"]\`);
392
+ const err = root.querySelector("#new-error");
393
+ const fail = (msg) => {
394
+ err.textContent = msg;
395
+ err.classList.remove("hidden");
396
+ };
397
+ const title = val("title").value.trim();
398
+ if (!title)
399
+ return fail("Give it a title.");
400
+ const payload = { title, body: val("body").value };
401
+ if (kind === "task") {
402
+ payload.epic = val("epic").value;
403
+ payload.status = val("status").value;
404
+ payload.priority = val("priority").value;
405
+ payload.subtasks = val("subtasks").value;
406
+ }
407
+ if (kind === "doc") {
408
+ payload.path = val("path").value.trim() || title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
409
+ }
410
+ err.classList.add("hidden");
411
+ const btn = root.querySelector("#new-save");
412
+ btn.disabled = true;
413
+ try {
414
+ const res = await fetch(KIND_ENDPOINT[kind], {
415
+ method: "POST",
416
+ headers: { "content-type": "application/json" },
417
+ body: JSON.stringify(payload)
418
+ });
419
+ const out = await res.json().catch(() => null);
420
+ if (!res.ok)
421
+ return fail(out?.error ?? "Could not create that.");
422
+ if (kind === "doc" && out?.file) {
423
+ location.href = "/docs?file=" + encodeURIComponent(out.file);
424
+ return;
425
+ }
426
+ showToast(\`\${out?.id ?? "Item"} created\`);
427
+ closeSheet();
428
+ await hooks.refreshView?.();
429
+ } catch {
430
+ fail("Could not reach the server.");
431
+ } finally {
432
+ btn.disabled = false;
433
+ }
434
+ }
435
+ function initSheet() {
436
+ sheet = document.getElementById("sheet");
437
+ backdrop = document.getElementById("backdrop");
438
+ sheetContent = document.getElementById("sheet-content");
439
+ document.addEventListener("click", (e) => {
440
+ const target = e.target;
441
+ const save = target.closest("#settings-save");
442
+ if (save) {
443
+ saveSettings(save.closest(".detail-root"));
444
+ return;
445
+ }
446
+ const create = target.closest("#new-save");
447
+ if (create) {
448
+ createItem(create.closest(".detail-root"));
449
+ return;
450
+ }
451
+ const kind = target.closest("[data-kind]");
452
+ if (kind) {
453
+ setKind(kind.closest(".detail-root"), kind.dataset.kind);
454
+ return;
455
+ }
456
+ const open = target.closest("[data-open]");
457
+ if (open)
458
+ openSheet(open.dataset.open);
459
+ });
460
+ document.addEventListener("keydown", (e) => {
461
+ if (e.key === "Escape" && sheetIsOpen())
462
+ closeSheet();
463
+ });
464
+ backdrop.addEventListener("click", closeSheet);
465
+ document.getElementById("sheet-close").addEventListener("click", closeSheet);
466
+ document.getElementById("sheet-expand").addEventListener("click", () => sheet.classList.toggle("fullscreen"));
467
+ }
468
+ function connectLive(onChange, debounceMs = 150) {
469
+ const dot = document.getElementById("live-dot");
470
+ let timer;
471
+ let es = null;
472
+ let connectedOnce = false;
473
+ const open = () => {
474
+ if (es)
475
+ return;
476
+ es = new EventSource("/sse");
477
+ es.onmessage = () => {
478
+ clearTimeout(timer);
479
+ timer = setTimeout(onChange, debounceMs);
480
+ };
481
+ es.onerror = () => dot.classList.replace("bg-emerald-500", "bg-zinc-300");
482
+ es.onopen = () => {
483
+ dot.classList.replace("bg-zinc-300", "bg-emerald-500");
484
+ if (connectedOnce)
485
+ onChange();
486
+ connectedOnce = true;
487
+ };
488
+ };
489
+ addEventListener("pagehide", () => {
490
+ es?.close();
491
+ es = null;
492
+ clearTimeout(timer);
493
+ });
494
+ addEventListener("pageshow", (e) => {
495
+ if (e.persisted)
496
+ open();
497
+ });
498
+ open();
499
+ }
500
+ var macLike = /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
501
+ function initSearchShortcut(searchBox) {
502
+ searchBox.placeholder = macLike ? "Search ⌘K" : "Search Ctrl+K";
503
+ searchBox.title = macLike ? "Search (⌘K or /)" : "Search (Ctrl+K or /)";
504
+ document.addEventListener("keydown", (e) => {
505
+ if (e.key !== "k" && e.key !== "K")
506
+ return;
507
+ if (!(macLike ? e.metaKey : e.ctrlKey) || e.altKey)
508
+ return;
509
+ e.preventDefault();
510
+ searchBox.focus();
511
+ searchBox.select();
512
+ });
513
+ }
514
+ function slashShouldFocus(e) {
515
+ const el = document.activeElement;
516
+ return e.key === "/" && !e.metaKey && !e.ctrlKey && !e.altKey && !/INPUT|TEXTAREA|SELECT/.test(el?.tagName ?? "") && !el?.isContentEditable;
517
+ }
518
+ function initChrome() {
519
+ initResizer();
520
+ initTheme();
521
+ initSheet();
522
+ initCheckboxes();
523
+ }
524
+
525
+ // src/client/board.ts
526
+ initChrome();
527
+ var sidebarCollapsedKey = "sidebarCollapsed";
528
+ var searchEl = document.getElementById("search");
529
+ initSearchShortcut(searchEl);
530
+ var activeEpic = null;
531
+ function applyFilter(epic) {
532
+ if (epic && !document.querySelector(\`[data-filter-epic="\${epic}"]\`))
533
+ epic = null;
534
+ activeEpic = epic;
535
+ const q = searchEl.value.trim().toLowerCase();
536
+ for (const c of document.querySelectorAll(".task-card")) {
537
+ const miss = q && !c.dataset.search.includes(q);
538
+ c.classList.toggle("hidden", !!epic && c.dataset.epic !== epic || !!miss);
539
+ }
540
+ for (const r of document.querySelectorAll("[data-filter-epic]")) {
541
+ r.classList.toggle("epic-active", (r.dataset.filterEpic || null) === epic);
542
+ }
543
+ for (const s of document.querySelectorAll("section[data-col]")) {
544
+ s.querySelector(".col-count").textContent = String(s.querySelectorAll(".task-card:not(.hidden)").length);
545
+ }
546
+ }
547
+ searchEl.addEventListener("input", () => applyFilter(activeEpic));
548
+ function applySidebarState() {
549
+ document.getElementById("sidebar").classList.toggle("collapsed", localStorage.getItem(sidebarCollapsedKey) === "1");
550
+ applySidebarWidth();
551
+ }
552
+ async function patchItem(id, patch, rev) {
553
+ try {
554
+ const res = await fetch("/api/items/" + id, {
555
+ method: "PATCH",
556
+ headers: { "content-type": "application/json" },
557
+ body: JSON.stringify({ ...patch, rev })
558
+ });
559
+ if (res.status === 409) {
560
+ showToast("Changed on disk elsewhere: showing latest");
561
+ await refreshApp();
562
+ } else if (!res.ok) {
563
+ showToast("Update failed");
564
+ }
565
+ } catch {
566
+ showToast("Update failed: server unreachable");
567
+ }
568
+ }
569
+ var refreshing = false;
570
+ var etag = "";
571
+ async function refreshApp() {
572
+ if (refreshing)
573
+ return;
574
+ refreshing = true;
575
+ try {
576
+ const res = await fetch(location.pathname, {
577
+ cache: "no-store",
578
+ headers: etag ? { "if-none-match": etag } : {}
579
+ });
580
+ if (res.status === 304)
581
+ return;
582
+ etag = res.headers.get("etag") || "";
583
+ const doc = new DOMParser().parseFromString(await res.text(), "text/html");
584
+ document.getElementById("app").innerHTML = doc.getElementById("app").innerHTML;
585
+ document.getElementById("header-meta").innerHTML = doc.getElementById("header-meta").innerHTML;
586
+ applySidebarState();
587
+ applyFilter(activeEpic);
588
+ clearPaneCache();
589
+ if (sheetIsOpen() && location.hash.length > 1) {
590
+ const st = sheet.scrollTop;
591
+ await openSheet(location.hash.slice(1));
592
+ sheet.scrollTop = st;
593
+ }
594
+ } catch {
595
+ showToast("Refresh failed");
596
+ } finally {
597
+ refreshing = false;
598
+ }
599
+ }
600
+ hooks.refreshView = refreshApp;
601
+ document.addEventListener("click", (e) => {
602
+ const target = e.target;
603
+ if (target.closest("[data-cb]"))
604
+ return;
605
+ if (target.closest("[data-open]"))
606
+ return;
607
+ const row = target.closest("[data-filter-epic]");
608
+ if (row) {
609
+ applyFilter(row.dataset.filterEpic === activeEpic ? null : row.dataset.filterEpic || null);
610
+ return;
611
+ }
612
+ const card = target.closest("[data-task-id]");
613
+ if (card)
614
+ openSheet(card.dataset.taskId);
615
+ });
616
+ document.addEventListener("change", (e) => {
617
+ const target = e.target;
618
+ if (target.matches("[data-status-select]")) {
619
+ const root = target.closest(".detail-root");
620
+ patchItem(root.dataset.id, { status: target.value }, root.dataset.rev);
621
+ }
622
+ });
623
+ document.addEventListener("keydown", (e) => {
624
+ if (e.key === "Escape" && document.activeElement === searchEl) {
625
+ searchEl.value = "";
626
+ applyFilter(activeEpic);
627
+ searchEl.blur();
628
+ return;
629
+ }
630
+ if (slashShouldFocus(e)) {
631
+ e.preventDefault();
632
+ searchEl.focus();
633
+ return;
634
+ }
635
+ const target = e.target;
636
+ if ((e.key === "Enter" || e.key === " ") && target instanceof HTMLElement && target.getAttribute("role") === "button") {
637
+ e.preventDefault();
638
+ target.click();
639
+ }
640
+ });
641
+ var dragId = null;
642
+ var dragRev = null;
643
+ var dragStatus = null;
644
+ function clearDropTargets() {
645
+ for (const s of document.querySelectorAll("section.drop-target"))
646
+ s.classList.remove("drop-target");
647
+ }
648
+ document.addEventListener("dragstart", (e) => {
649
+ const card = e.target.closest?.("[data-task-id]");
650
+ if (!card)
651
+ return;
652
+ dragId = card.dataset.taskId;
653
+ dragRev = card.dataset.rev;
654
+ dragStatus = card.dataset.status;
655
+ e.dataTransfer.effectAllowed = "move";
656
+ card.classList.add("dragging");
657
+ });
658
+ document.addEventListener("dragend", (e) => {
659
+ e.target.closest?.("[data-task-id]")?.classList.remove("dragging");
660
+ clearDropTargets();
661
+ dragId = null;
662
+ });
663
+ document.addEventListener("dragover", (e) => {
664
+ const col = e.target.closest?.("section[data-col]");
665
+ if (col && dragId) {
666
+ e.preventDefault();
667
+ e.dataTransfer.dropEffect = "move";
668
+ if (!col.classList.contains("drop-target")) {
669
+ clearDropTargets();
670
+ col.classList.add("drop-target");
671
+ }
672
+ }
673
+ });
674
+ document.addEventListener("drop", (e) => {
675
+ const col = e.target.closest?.("section[data-col]");
676
+ if (col && dragId) {
677
+ e.preventDefault();
678
+ if (col.dataset.col !== dragStatus)
679
+ patchItem(dragId, { status: col.dataset.col }, dragRev);
680
+ }
681
+ clearDropTargets();
682
+ dragId = null;
683
+ });
684
+ document.getElementById("sidebar-toggle").addEventListener("click", () => {
685
+ const sb = document.getElementById("sidebar");
686
+ localStorage.setItem(sidebarCollapsedKey, sb.classList.toggle("collapsed") ? "1" : "0");
687
+ });
688
+ connectLive(() => void refreshApp(), 120);
689
+ applySidebarState();
690
+ if (location.hash.length > 1)
691
+ openSheet(location.hash.slice(1));
692
+ })();
693
+ `;
694
+
695
+ // src/generated/docs.js
696
+ var docs_default = `(() => {
697
+ // src/client/prefs.ts
698
+ var THEMES = ["light", "dark"];
699
+ var APPEARANCE = {
700
+ accent: ["violet", "blue", "emerald", "amber", "rose"],
701
+ font: ["sans", "serif", "mono"],
702
+ size: ["compact", "default", "large"]
703
+ };
704
+ var DEFAULTS = { accent: "violet", font: "sans", size: "default" };
705
+ function getPref(name) {
706
+ const stored = localStorage.getItem("outcrop." + name);
707
+ return stored && APPEARANCE[name].includes(stored) ? stored : DEFAULTS[name];
708
+ }
709
+ function setPref(name, value) {
710
+ localStorage.setItem("outcrop." + name, value);
711
+ applyAppearance();
712
+ }
713
+ function applyAppearance() {
714
+ for (const name of Object.keys(APPEARANCE)) {
715
+ document.documentElement.dataset[name] = getPref(name);
716
+ }
717
+ }
718
+ function currentTheme() {
719
+ const stored = localStorage.getItem("theme");
720
+ if (stored === "light" || stored === "dark")
721
+ return stored;
722
+ return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
723
+ }
724
+ function applyTheme() {
725
+ const theme = currentTheme();
726
+ document.documentElement.classList.toggle("dark", theme === "dark");
727
+ return theme;
728
+ }
729
+
730
+ // src/client/chrome.ts
731
+ var THEME_ANIM_MS = 260;
732
+ var hooks = {};
733
+ var sidebarWidthKey = "sidebarWidth";
734
+ function applySidebarWidth() {
735
+ const sb = document.querySelector(".sidebar-resizer")?.previousElementSibling;
736
+ const w = localStorage.getItem(sidebarWidthKey);
737
+ if (sb && w)
738
+ sb.style.width = w + "px";
739
+ }
740
+ function initResizer() {
741
+ applySidebarWidth();
742
+ document.addEventListener("pointerdown", (e) => {
743
+ const rz = e.target.closest?.(".sidebar-resizer");
744
+ if (!rz)
745
+ return;
746
+ e.preventDefault();
747
+ const sb = rz.previousElementSibling;
748
+ const left = sb.getBoundingClientRect().left;
749
+ rz.setPointerCapture(e.pointerId);
750
+ const onMove = (ev) => {
751
+ const w = Math.min(448, Math.max(176, Math.round(ev.clientX - left)));
752
+ sb.style.width = w + "px";
753
+ localStorage.setItem(sidebarWidthKey, String(w));
754
+ };
755
+ rz.addEventListener("pointermove", onMove);
756
+ rz.addEventListener("pointerup", () => rz.removeEventListener("pointermove", onMove), { once: true });
757
+ });
758
+ }
759
+ var THEME_ICONS = {
760
+ light: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.4"/><path d="M8 1v1.6M8 13.4V15M15 8h-1.6M2.6 8H1M12.9 3.1l-1.1 1.1M4.2 11.8l-1.1 1.1M12.9 12.9l-1.1-1.1M4.2 4.2 3.1 3.1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
761
+ dark: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M13.5 9.6A6 6 0 0 1 6.4 2.5a6 6 0 1 0 7.1 7.1Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>'
762
+ };
763
+ var themeBtn;
764
+ function renderTheme() {
765
+ const t = applyTheme();
766
+ themeBtn.innerHTML = THEME_ICONS[t];
767
+ themeBtn.title = "Switch to " + (t === "dark" ? "light" : "dark") + " mode";
768
+ for (const b of document.querySelectorAll("[data-set-theme]")) {
769
+ b.classList.toggle("theme-active", b.dataset.setTheme === t);
770
+ }
771
+ renderAppearance();
772
+ }
773
+ function renderAppearance() {
774
+ for (const b of document.querySelectorAll("[data-set-appearance]")) {
775
+ b.classList.toggle("active", getPref(b.dataset.setAppearance) === b.dataset.value);
776
+ }
777
+ }
778
+ var themeAnimTimer;
779
+ function setTheme(t) {
780
+ const changed = t !== currentTheme();
781
+ localStorage.setItem("theme", t);
782
+ if (changed) {
783
+ document.documentElement.classList.add("theme-anim");
784
+ clearTimeout(themeAnimTimer);
785
+ themeAnimTimer = setTimeout(() => document.documentElement.classList.remove("theme-anim"), THEME_ANIM_MS + 60);
786
+ }
787
+ renderTheme();
788
+ if (changed)
789
+ hooks.onAppearanceChange?.();
790
+ }
791
+ function initTheme() {
792
+ themeBtn = document.getElementById("theme-toggle");
793
+ themeBtn.addEventListener("click", () => setTheme(THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length]));
794
+ document.addEventListener("click", (e) => {
795
+ const target = e.target;
796
+ const theme = target.closest("[data-set-theme]");
797
+ if (theme) {
798
+ setTheme(theme.dataset.setTheme);
799
+ return;
800
+ }
801
+ const pref = target.closest("[data-set-appearance]");
802
+ if (!pref)
803
+ return;
804
+ setPref(pref.dataset.setAppearance, pref.dataset.value);
805
+ renderAppearance();
806
+ hooks.onAppearanceChange?.();
807
+ });
808
+ matchMedia("(prefers-color-scheme: dark)").addEventListener("change", renderTheme);
809
+ renderTheme();
810
+ }
811
+ var sheet;
812
+ var sheetContent;
813
+ var backdrop;
814
+ var lastFocus = null;
815
+ var toastTimer;
816
+ function showToast(msg) {
817
+ const toast = document.getElementById("toast");
818
+ toast.textContent = msg;
819
+ toast.classList.add("opacity-100");
820
+ clearTimeout(toastTimer);
821
+ toastTimer = setTimeout(() => toast.classList.remove("opacity-100"), 2500);
822
+ }
823
+ function sheetIsOpen() {
824
+ return !sheet.classList.contains("translate-x-full");
825
+ }
826
+ function showSheet(id) {
827
+ backdrop.classList.remove("hidden");
828
+ sheet.classList.remove("translate-x-full");
829
+ if (!sheet.contains(document.activeElement))
830
+ lastFocus = document.activeElement;
831
+ sheet.focus({ preventScroll: true });
832
+ sheet.scrollTop = 0;
833
+ history.replaceState(null, "", "#" + id);
834
+ }
835
+ function closeSheet() {
836
+ sheet.classList.add("translate-x-full");
837
+ backdrop.classList.add("hidden");
838
+ lastFocus?.focus({ preventScroll: true });
839
+ history.replaceState(null, "", location.pathname + location.search);
840
+ }
841
+ var sheetSeq = 0;
842
+ var paneCache = new Map;
843
+ function clearPaneCache() {
844
+ paneCache.clear();
845
+ }
846
+ async function openSheet(id) {
847
+ const seq = ++sheetSeq;
848
+ const tpl = document.getElementById("detail-" + id);
849
+ if (tpl) {
850
+ sheetContent.replaceChildren(tpl.content.cloneNode(true));
851
+ renderTheme();
852
+ const form = sheetContent.querySelector('.detail-root[data-id="new"]');
853
+ if (form) {
854
+ setKind(form, localStorage.getItem("newKind") || "task");
855
+ showSheet(id);
856
+ form.querySelector('[name="title"]')?.focus();
857
+ return;
858
+ }
859
+ showSheet(id);
860
+ return;
861
+ }
862
+ const cached = paneCache.get(id);
863
+ if (cached !== undefined) {
864
+ sheetContent.innerHTML = cached;
865
+ showSheet(id);
866
+ return;
867
+ }
868
+ sheetContent.innerHTML = '<div class="pt-1 text-[12px] text-zinc-400 dark:text-zinc-500">Loading…</div>';
869
+ showSheet(id);
870
+ let html = null;
871
+ try {
872
+ const res = await fetch("/api/items/" + encodeURIComponent(id) + "/pane");
873
+ if (res.ok)
874
+ html = await res.text();
875
+ } catch {}
876
+ if (seq !== sheetSeq)
877
+ return;
878
+ if (html === null) {
879
+ sheetContent.innerHTML = '<div class="pt-1 text-[12px] text-zinc-400">Not found.</div>';
880
+ return;
881
+ }
882
+ paneCache.set(id, html);
883
+ sheetContent.innerHTML = html;
884
+ }
885
+ async function saveSettings(root) {
886
+ const val = (n) => root.querySelector(\`[name="\${n}"]\`);
887
+ const err = root.querySelector("#settings-error");
888
+ const views = [...root.querySelectorAll('[name^="view-"]')].filter((c) => c.checked).map((c) => c.name.slice(5));
889
+ if (!views.length) {
890
+ err.textContent = "Keep at least one view on: with all of them off there is nothing to show.";
891
+ err.classList.remove("hidden");
892
+ root.querySelector(".set-group:has([name^='view-'])")?.setAttribute("open", "");
893
+ return;
894
+ }
895
+ err.classList.add("hidden");
896
+ try {
897
+ const res = await fetch("/api/settings", {
898
+ method: "PUT",
899
+ headers: { "content-type": "application/json" },
900
+ body: JSON.stringify({
901
+ title: val("title").value,
902
+ views,
903
+ columns: val("columns").value.split(","),
904
+ gitCommit: val("gitCommit").checked
905
+ })
906
+ });
907
+ if (!res.ok) {
908
+ const body = await res.json().catch(() => null);
909
+ err.textContent = body?.error ?? "Save failed.";
910
+ err.classList.remove("hidden");
911
+ return;
912
+ }
913
+ showToast("Settings saved to .outcrop.json");
914
+ const saved = await res.json();
915
+ if (root.dataset.views !== saved.views.join(","))
916
+ return location.reload();
917
+ closeSheet();
918
+ await hooks.refreshView?.();
919
+ } catch {
920
+ err.textContent = "Save failed: server unreachable.";
921
+ err.classList.remove("hidden");
922
+ }
923
+ }
924
+ async function toggleCheckbox(url, index, checked, rev) {
925
+ try {
926
+ const res = await fetch(url, {
927
+ method: "PATCH",
928
+ headers: { "content-type": "application/json" },
929
+ body: JSON.stringify({ checkbox: { index, checked }, rev })
930
+ });
931
+ if (res.status === 409) {
932
+ showToast("Changed on disk elsewhere: showing latest");
933
+ } else if (!res.ok) {
934
+ showToast("Update failed");
935
+ return;
936
+ }
937
+ await hooks.refreshView?.();
938
+ } catch {
939
+ showToast("Update failed: server unreachable");
940
+ }
941
+ }
942
+ function initCheckboxes() {
943
+ document.addEventListener("click", (e) => {
944
+ const cb = e.target.closest("[data-cb]");
945
+ if (!cb)
946
+ return;
947
+ const index = Number(cb.dataset.cb);
948
+ const checked = !cb.classList.contains("checked");
949
+ cb.classList.toggle("checked", checked);
950
+ cb.closest("li.check")?.classList.toggle("done", checked);
951
+ const item = cb.closest(".detail-root[data-id][data-rev]");
952
+ if (item) {
953
+ toggleCheckbox("/api/items/" + encodeURIComponent(item.dataset.id), index, checked, item.dataset.rev);
954
+ return;
955
+ }
956
+ const doc = cb.closest("#doc-content[data-file]");
957
+ if (doc) {
958
+ toggleCheckbox("/api/docs?file=" + encodeURIComponent(doc.dataset.file), index, checked, doc.dataset.rev);
959
+ }
960
+ });
961
+ }
962
+ var KIND_ENDPOINT = { task: "/api/tasks", epic: "/api/epics", doc: "/api/docs" };
963
+ function setKind(root, kind) {
964
+ root.dataset.kind = kind;
965
+ localStorage.setItem("newKind", kind);
966
+ for (const b of root.querySelectorAll("[data-kind]")) {
967
+ b.classList.toggle("active", b.dataset.kind === kind);
968
+ }
969
+ for (const g of root.querySelectorAll("[data-for]")) {
970
+ g.classList.toggle("hidden", g.dataset.for !== kind);
971
+ }
972
+ root.querySelector("#new-target").textContent = kind === "doc" ? "" : kind === "epic" ? "epics/" : "tasks/";
973
+ }
974
+ async function createItem(root) {
975
+ const kind = root.dataset.kind || "task";
976
+ const val = (n) => root.querySelector(\`[name="\${n}"]\`);
977
+ const err = root.querySelector("#new-error");
978
+ const fail = (msg) => {
979
+ err.textContent = msg;
980
+ err.classList.remove("hidden");
981
+ };
982
+ const title = val("title").value.trim();
983
+ if (!title)
984
+ return fail("Give it a title.");
985
+ const payload = { title, body: val("body").value };
986
+ if (kind === "task") {
987
+ payload.epic = val("epic").value;
988
+ payload.status = val("status").value;
989
+ payload.priority = val("priority").value;
990
+ payload.subtasks = val("subtasks").value;
991
+ }
992
+ if (kind === "doc") {
993
+ payload.path = val("path").value.trim() || title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
994
+ }
995
+ err.classList.add("hidden");
996
+ const btn = root.querySelector("#new-save");
997
+ btn.disabled = true;
998
+ try {
999
+ const res = await fetch(KIND_ENDPOINT[kind], {
1000
+ method: "POST",
1001
+ headers: { "content-type": "application/json" },
1002
+ body: JSON.stringify(payload)
1003
+ });
1004
+ const out = await res.json().catch(() => null);
1005
+ if (!res.ok)
1006
+ return fail(out?.error ?? "Could not create that.");
1007
+ if (kind === "doc" && out?.file) {
1008
+ location.href = "/docs?file=" + encodeURIComponent(out.file);
1009
+ return;
1010
+ }
1011
+ showToast(\`\${out?.id ?? "Item"} created\`);
1012
+ closeSheet();
1013
+ await hooks.refreshView?.();
1014
+ } catch {
1015
+ fail("Could not reach the server.");
1016
+ } finally {
1017
+ btn.disabled = false;
1018
+ }
1019
+ }
1020
+ function initSheet() {
1021
+ sheet = document.getElementById("sheet");
1022
+ backdrop = document.getElementById("backdrop");
1023
+ sheetContent = document.getElementById("sheet-content");
1024
+ document.addEventListener("click", (e) => {
1025
+ const target = e.target;
1026
+ const save = target.closest("#settings-save");
1027
+ if (save) {
1028
+ saveSettings(save.closest(".detail-root"));
1029
+ return;
1030
+ }
1031
+ const create = target.closest("#new-save");
1032
+ if (create) {
1033
+ createItem(create.closest(".detail-root"));
1034
+ return;
1035
+ }
1036
+ const kind = target.closest("[data-kind]");
1037
+ if (kind) {
1038
+ setKind(kind.closest(".detail-root"), kind.dataset.kind);
1039
+ return;
1040
+ }
1041
+ const open = target.closest("[data-open]");
1042
+ if (open)
1043
+ openSheet(open.dataset.open);
1044
+ });
1045
+ document.addEventListener("keydown", (e) => {
1046
+ if (e.key === "Escape" && sheetIsOpen())
1047
+ closeSheet();
1048
+ });
1049
+ backdrop.addEventListener("click", closeSheet);
1050
+ document.getElementById("sheet-close").addEventListener("click", closeSheet);
1051
+ document.getElementById("sheet-expand").addEventListener("click", () => sheet.classList.toggle("fullscreen"));
1052
+ }
1053
+ function connectLive(onChange, debounceMs = 150) {
1054
+ const dot = document.getElementById("live-dot");
1055
+ let timer;
1056
+ let es = null;
1057
+ let connectedOnce = false;
1058
+ const open = () => {
1059
+ if (es)
1060
+ return;
1061
+ es = new EventSource("/sse");
1062
+ es.onmessage = () => {
1063
+ clearTimeout(timer);
1064
+ timer = setTimeout(onChange, debounceMs);
1065
+ };
1066
+ es.onerror = () => dot.classList.replace("bg-emerald-500", "bg-zinc-300");
1067
+ es.onopen = () => {
1068
+ dot.classList.replace("bg-zinc-300", "bg-emerald-500");
1069
+ if (connectedOnce)
1070
+ onChange();
1071
+ connectedOnce = true;
1072
+ };
1073
+ };
1074
+ addEventListener("pagehide", () => {
1075
+ es?.close();
1076
+ es = null;
1077
+ clearTimeout(timer);
1078
+ });
1079
+ addEventListener("pageshow", (e) => {
1080
+ if (e.persisted)
1081
+ open();
1082
+ });
1083
+ open();
1084
+ }
1085
+ var macLike = /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
1086
+ function initSearchShortcut(searchBox) {
1087
+ searchBox.placeholder = macLike ? "Search ⌘K" : "Search Ctrl+K";
1088
+ searchBox.title = macLike ? "Search (⌘K or /)" : "Search (Ctrl+K or /)";
1089
+ document.addEventListener("keydown", (e) => {
1090
+ if (e.key !== "k" && e.key !== "K")
1091
+ return;
1092
+ if (!(macLike ? e.metaKey : e.ctrlKey) || e.altKey)
1093
+ return;
1094
+ e.preventDefault();
1095
+ searchBox.focus();
1096
+ searchBox.select();
1097
+ });
1098
+ }
1099
+ function slashShouldFocus(e) {
1100
+ const el = document.activeElement;
1101
+ return e.key === "/" && !e.metaKey && !e.ctrlKey && !e.altKey && !/INPUT|TEXTAREA|SELECT/.test(el?.tagName ?? "") && !el?.isContentEditable;
1102
+ }
1103
+ function initChrome() {
1104
+ initResizer();
1105
+ initTheme();
1106
+ initSheet();
1107
+ initCheckboxes();
1108
+ }
1109
+
1110
+ // src/client/docs.ts
1111
+ initChrome();
1112
+ var mermaidTheme = () => document.documentElement.classList.contains("dark") ? "dark" : "neutral";
1113
+ var hasMermaid = () => typeof globalThis.mermaid !== "undefined";
1114
+ var mermaidLoad = null;
1115
+ function ensureMermaid() {
1116
+ if (hasMermaid())
1117
+ return Promise.resolve();
1118
+ if (!mermaidLoad) {
1119
+ mermaidLoad = new Promise((resolve, reject) => {
1120
+ const s = document.createElement("script");
1121
+ s.src = "https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js";
1122
+ s.onload = () => {
1123
+ mermaid.initialize({ startOnLoad: false, theme: mermaidTheme() });
1124
+ resolve();
1125
+ };
1126
+ s.onerror = () => reject(new Error("mermaid failed to load"));
1127
+ document.head.appendChild(s);
1128
+ });
1129
+ }
1130
+ return mermaidLoad;
1131
+ }
1132
+ async function renderDiagrams() {
1133
+ if (!document.querySelector("#doc-main .mermaid:not([data-processed])"))
1134
+ return;
1135
+ try {
1136
+ await ensureMermaid();
1137
+ await mermaid.run({ querySelector: "#doc-main .mermaid:not([data-processed])" });
1138
+ } catch {}
1139
+ }
1140
+ if (hasMermaid())
1141
+ mermaid.initialize({ startOnLoad: false, theme: mermaidTheme() });
1142
+ renderDiagrams();
1143
+ hooks.onAppearanceChange = () => {
1144
+ if (!hasMermaid())
1145
+ return;
1146
+ mermaid.initialize({ startOnLoad: false, theme: mermaidTheme() });
1147
+ if (document.querySelector("#doc-main .mermaid")) {
1148
+ etag = "";
1149
+ refreshDoc();
1150
+ }
1151
+ };
1152
+ var foldKey = "docsCollapsedFolders";
1153
+ var servedTree = document.getElementById("doc-tree").innerHTML;
1154
+ function collapsed() {
1155
+ try {
1156
+ return new Set(JSON.parse(localStorage.getItem(foldKey) || "[]"));
1157
+ } catch {
1158
+ return new Set;
1159
+ }
1160
+ }
1161
+ var folders = () => [...document.querySelectorAll("#doc-tree .doc-folder")];
1162
+ var foldingProgrammatically = false;
1163
+ function setFolds(fn) {
1164
+ foldingProgrammatically = true;
1165
+ for (const d of folders())
1166
+ d.open = fn(d);
1167
+ foldingProgrammatically = false;
1168
+ syncFoldAllButton();
1169
+ }
1170
+ function applyFolds() {
1171
+ const shut = collapsed();
1172
+ setFolds((d) => !shut.has(d.dataset.folder));
1173
+ }
1174
+ function hideEmptyFolders() {
1175
+ for (const d of folders()) {
1176
+ const any = [...d.querySelectorAll(".doc-link")].some((a) => !a.classList.contains("hidden"));
1177
+ d.classList.toggle("hidden", !any);
1178
+ }
1179
+ }
1180
+ function syncFoldAllButton() {
1181
+ const all = folders();
1182
+ const allShut = all.length > 0 && all.every((d) => !d.open);
1183
+ const label = allShut ? "Expand all folders" : "Collapse all folders";
1184
+ foldAllBtn.title = label;
1185
+ foldAllBtn.setAttribute("aria-label", label);
1186
+ }
1187
+ var foldAllBtn = document.getElementById("fold-all");
1188
+ foldAllBtn.addEventListener("click", () => {
1189
+ const open = folders().some((d) => !d.open);
1190
+ setFolds(() => open);
1191
+ localStorage.setItem(foldKey, JSON.stringify(open ? [] : folders().map((d) => d.dataset.folder)));
1192
+ });
1193
+ document.addEventListener("toggle", (e) => {
1194
+ if (foldingProgrammatically)
1195
+ return;
1196
+ const d = e.target.closest("#doc-tree .doc-folder");
1197
+ if (!d)
1198
+ return;
1199
+ const shut = collapsed();
1200
+ if (d.open)
1201
+ shut.delete(d.dataset.folder);
1202
+ else
1203
+ shut.add(d.dataset.folder);
1204
+ localStorage.setItem(foldKey, JSON.stringify([...shut]));
1205
+ syncFoldAllButton();
1206
+ }, true);
1207
+ applyFolds();
1208
+ var outline = document.getElementById("outline-list");
1209
+ var outlineAside = document.getElementById("doc-outline");
1210
+ var docMain = document.getElementById("doc-main");
1211
+ var outlineHeads = [];
1212
+ function markOutline() {
1213
+ if (!outlineHeads.length)
1214
+ return;
1215
+ const line = docMain.getBoundingClientRect().top + docMain.clientHeight * 0.25;
1216
+ let active = outlineHeads[0].id;
1217
+ for (const h of outlineHeads) {
1218
+ if (h.getBoundingClientRect().top > line)
1219
+ break;
1220
+ active = h.id;
1221
+ }
1222
+ if (docMain.scrollTop + docMain.clientHeight >= docMain.scrollHeight - 2) {
1223
+ active = outlineHeads[outlineHeads.length - 1].id;
1224
+ }
1225
+ for (const a of outline.querySelectorAll(".outline-link")) {
1226
+ a.classList.toggle("active", a.getAttribute("href") === "#" + active);
1227
+ }
1228
+ }
1229
+ docMain.addEventListener("scroll", markOutline, { passive: true });
1230
+ addEventListener("resize", markOutline);
1231
+ function buildOutline() {
1232
+ outlineHeads = [
1233
+ ...document.querySelectorAll("#doc-content .doc-body h1[id], #doc-content .doc-body h2[id], #doc-content .doc-body h3[id]")
1234
+ ];
1235
+ outline.replaceChildren();
1236
+ outlineAside.classList.toggle("empty", outlineHeads.length < 2);
1237
+ if (outlineHeads.length < 2) {
1238
+ outlineHeads = [];
1239
+ return;
1240
+ }
1241
+ for (const h of outlineHeads) {
1242
+ const a = document.createElement("a");
1243
+ a.href = "#" + h.id;
1244
+ a.className = "outline-link lvl-" + h.tagName[1];
1245
+ a.textContent = h.textContent;
1246
+ a.addEventListener("click", (e) => {
1247
+ e.preventDefault();
1248
+ h.scrollIntoView({ behavior: "smooth", block: "start" });
1249
+ history.replaceState(null, "", location.pathname + location.search + "#" + h.id);
1250
+ });
1251
+ outline.append(a);
1252
+ }
1253
+ markOutline();
1254
+ }
1255
+ buildOutline();
1256
+ var refreshing = false;
1257
+ var etag = "";
1258
+ var etagFor = location.pathname + location.search;
1259
+ var navSeq = 0;
1260
+ async function loadDoc(url, { push = false, conditional = false } = {}) {
1261
+ const seq = ++navSeq;
1262
+ if (refreshing && !push)
1263
+ return;
1264
+ refreshing = true;
1265
+ try {
1266
+ const sameDoc = conditional && url === etagFor && etag;
1267
+ const res = await fetch(url, { cache: "no-store", headers: sameDoc ? { "if-none-match": etag } : {} });
1268
+ if (seq !== navSeq)
1269
+ return;
1270
+ if (res.status === 304)
1271
+ return;
1272
+ if (!res.ok) {
1273
+ location.href = url;
1274
+ return;
1275
+ }
1276
+ const text = await res.text();
1277
+ if (seq !== navSeq)
1278
+ return;
1279
+ etag = res.headers.get("etag") || "";
1280
+ etagFor = url;
1281
+ const doc = new DOMParser().parseFromString(text, "text/html");
1282
+ const tree = document.getElementById("doc-tree");
1283
+ const nextTree = doc.getElementById("doc-tree").innerHTML;
1284
+ if (servedTree !== nextTree) {
1285
+ const sidebar = document.getElementById("doc-sidebar");
1286
+ const top = sidebar.scrollTop;
1287
+ tree.innerHTML = nextTree;
1288
+ servedTree = nextTree;
1289
+ sidebar.scrollTop = top;
1290
+ applyFolds();
1291
+ }
1292
+ const main = document.getElementById("doc-main");
1293
+ main.innerHTML = doc.getElementById("doc-main").innerHTML;
1294
+ document.title = doc.title;
1295
+ if (push) {
1296
+ history.pushState(null, "", url);
1297
+ main.scrollTop = 0;
1298
+ }
1299
+ markActiveDoc();
1300
+ buildOutline();
1301
+ applyDocSearch();
1302
+ const jump = jumpQuery;
1303
+ jumpQuery = "";
1304
+ if (jump)
1305
+ jumpToMatch(jump);
1306
+ else
1307
+ endMatches();
1308
+ await renderDiagrams();
1309
+ } catch {} finally {
1310
+ if (seq === navSeq)
1311
+ refreshing = false;
1312
+ }
1313
+ }
1314
+ var refreshDoc = () => loadDoc(location.pathname + location.search, { conditional: true });
1315
+ hooks.refreshView = refreshDoc;
1316
+ function markActiveDoc() {
1317
+ const current = new URLSearchParams(location.search).get("file") || "README.md";
1318
+ for (const a of document.querySelectorAll("#doc-sidebar .doc-link")) {
1319
+ a.classList.toggle("active", a.dataset.file === current);
1320
+ }
1321
+ }
1322
+ markActiveDoc();
1323
+ document.addEventListener("click", (e) => {
1324
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
1325
+ return;
1326
+ const a = e.target.closest("a[href]");
1327
+ if (!a || a.target)
1328
+ return;
1329
+ const url = new URL(a.href, location.href);
1330
+ if (url.origin !== location.origin || url.pathname !== "/docs")
1331
+ return;
1332
+ const here = url.pathname + url.search;
1333
+ if (here === location.pathname + location.search)
1334
+ return;
1335
+ e.preventDefault();
1336
+ loadDoc(here, { push: true });
1337
+ });
1338
+ window.addEventListener("popstate", () => void loadDoc(location.pathname + location.search));
1339
+ var docSidebar = document.getElementById("doc-sidebar");
1340
+ var searchEl = document.getElementById("search");
1341
+ initSearchShortcut(searchEl);
1342
+ searchEl.value = sessionStorage.getItem("docSearch") || "";
1343
+ function matchRanges(q) {
1344
+ const ranges = [];
1345
+ if (!q)
1346
+ return ranges;
1347
+ const walker = document.createTreeWalker(document.getElementById("doc-content"), NodeFilter.SHOW_TEXT);
1348
+ let n;
1349
+ while (n = walker.nextNode()) {
1350
+ const t = n.textContent.toLowerCase();
1351
+ for (let i = t.indexOf(q);i !== -1; i = t.indexOf(q, i + q.length)) {
1352
+ const r = new Range;
1353
+ r.setStart(n, i);
1354
+ r.setEnd(n, i + q.length);
1355
+ ranges.push(r);
1356
+ }
1357
+ }
1358
+ return ranges;
1359
+ }
1360
+ function highlightDoc(q) {
1361
+ if (!("highlights" in CSS))
1362
+ return;
1363
+ CSS.highlights.delete("doc-search");
1364
+ const ranges = matchRanges(q);
1365
+ if (ranges.length)
1366
+ CSS.highlights.set("doc-search", new Highlight(...ranges));
1367
+ }
1368
+ var jumpQuery = "";
1369
+ var matches = [];
1370
+ var matchIdx = 0;
1371
+ var STEP_BUTTON = "rounded p-1 text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300";
1372
+ var matchBar = document.createElement("div");
1373
+ matchBar.className = "fixed bottom-5 right-5 z-40 hidden items-center gap-0.5 rounded-md border border-zinc-200 bg-white py-1 pl-2.5 pr-1 text-[12px] shadow-lg dark:border-zinc-800 dark:bg-zinc-900";
1374
+ matchBar.innerHTML = '<span id="match-count" class="mr-1.5 tabular-nums text-zinc-500 dark:text-zinc-400"></span>' + \`<button type="button" data-step="-1" class="\${STEP_BUTTON}" aria-label="Previous match" title="Previous match (Shift+Enter)">\` + '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 10 8 6 12 10" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' + \`<button type="button" data-step="1" class="\${STEP_BUTTON}" aria-label="Next match" title="Next match (Enter)">\` + '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 6 8 10 12 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' + \`<button type="button" data-step="0" class="\${STEP_BUTTON}" aria-label="Done" title="Done (Esc)">\` + '<svg width="13" height="13" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M3 3 L11 11 M11 3 L3 11" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>';
1375
+ document.body.append(matchBar);
1376
+ matchBar.addEventListener("click", (e) => {
1377
+ const step = e.target.closest("[data-step]")?.dataset.step;
1378
+ if (step === undefined)
1379
+ return;
1380
+ if (step === "0")
1381
+ endMatches();
1382
+ else
1383
+ stepMatch(Number(step));
1384
+ });
1385
+ function paintMatches() {
1386
+ if (!("highlights" in CSS))
1387
+ return;
1388
+ CSS.highlights.delete("doc-search");
1389
+ CSS.highlights.delete("doc-match");
1390
+ const rest = matches.filter((_, i) => i !== matchIdx);
1391
+ if (rest.length)
1392
+ CSS.highlights.set("doc-search", new Highlight(...rest));
1393
+ if (matches[matchIdx])
1394
+ CSS.highlights.set("doc-match", new Highlight(matches[matchIdx]));
1395
+ }
1396
+ function scrollToMatch() {
1397
+ const el = matches[matchIdx]?.startContainer.parentElement;
1398
+ if (el)
1399
+ el.scrollIntoView({ block: "center" });
1400
+ }
1401
+ function stepMatch(delta) {
1402
+ if (!matches.length)
1403
+ return;
1404
+ matchIdx = (matchIdx + delta + matches.length) % matches.length;
1405
+ document.getElementById("match-count").textContent = \`\${matchIdx + 1} of \${matches.length}\`;
1406
+ paintMatches();
1407
+ scrollToMatch();
1408
+ }
1409
+ function endMatches() {
1410
+ matches = [];
1411
+ matchBar.classList.replace("flex", "hidden");
1412
+ if ("highlights" in CSS) {
1413
+ CSS.highlights.delete("doc-search");
1414
+ CSS.highlights.delete("doc-match");
1415
+ }
1416
+ }
1417
+ function jumpToMatch(q) {
1418
+ document.getElementById("search-results").classList.add("hidden");
1419
+ document.getElementById("doc-content").classList.remove("hidden");
1420
+ matches = matchRanges(q);
1421
+ matchIdx = -1;
1422
+ if (!matches.length)
1423
+ return endMatches();
1424
+ matchBar.classList.replace("hidden", "flex");
1425
+ stepMatch(1);
1426
+ }
1427
+ var searchSeq = 0;
1428
+ var panelOpen = false;
1429
+ var escHtml = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1430
+ async function applyDocSearch() {
1431
+ const q = searchEl.value.trim().toLowerCase();
1432
+ sessionStorage.setItem("docSearch", searchEl.value);
1433
+ highlightDoc(q);
1434
+ const results = document.getElementById("search-results");
1435
+ const content = document.getElementById("doc-content");
1436
+ const links = [...docSidebar.querySelectorAll(".doc-link")];
1437
+ const seq = ++searchSeq;
1438
+ if (!q) {
1439
+ panelOpen = false;
1440
+ for (const a of links)
1441
+ a.classList.remove("hidden");
1442
+ applyFolds();
1443
+ hideEmptyFolders();
1444
+ results.classList.add("hidden");
1445
+ content.classList.remove("hidden");
1446
+ return;
1447
+ }
1448
+ const hits = await fetch("/api/docs-search?q=" + encodeURIComponent(q)).then((r) => r.json()).catch(() => null);
1449
+ if (!hits || seq !== searchSeq)
1450
+ return;
1451
+ const files = hits.map((h) => h.file);
1452
+ for (const a of links) {
1453
+ a.classList.toggle("hidden", !files.includes(new URL(a.href).searchParams.get("file")));
1454
+ }
1455
+ setFolds(() => true);
1456
+ hideEmptyFolders();
1457
+ if (!panelOpen)
1458
+ return;
1459
+ const markSnippet = (s) => {
1460
+ const low = s.toLowerCase();
1461
+ let out = "";
1462
+ let pos = 0;
1463
+ for (let i = low.indexOf(q);i !== -1; i = low.indexOf(q, i + q.length)) {
1464
+ out += escHtml(s.slice(pos, i)) + "<mark>" + escHtml(s.slice(i, i + q.length)) + "</mark>";
1465
+ pos = i + q.length;
1466
+ }
1467
+ return out + escHtml(s.slice(pos));
1468
+ };
1469
+ results.innerHTML = hits.length ? hits.map((h) => '<a href="/docs?file=' + encodeURIComponent(h.file) + '" class="block px-3 py-3.5 transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-900">' + '<div class="flex items-baseline justify-between gap-2">' + '<span class="truncate text-[13px] font-medium text-zinc-900 dark:text-zinc-100">' + escHtml(h.file) + "</span>" + '<span class="shrink-0 text-[11px] text-zinc-400 dark:text-zinc-600">' + h.count + (h.count === 1 ? " match" : " matches") + "</span>" + "</div>" + h.snippets.map((s) => '<p class="mt-1.5 text-[12.5px] leading-relaxed text-zinc-600 dark:text-zinc-400">' + markSnippet(s) + "</p>").join("") + "</a>").join("") : '<p class="px-3 py-6 text-[13px] text-zinc-400 dark:text-zinc-600">No matches</p>';
1470
+ results.classList.remove("hidden");
1471
+ content.classList.add("hidden");
1472
+ }
1473
+ document.addEventListener("click", (e) => {
1474
+ const hit = e.target.closest?.("#search-results a[href]");
1475
+ if (!hit)
1476
+ return;
1477
+ const q = searchEl.value.trim().toLowerCase();
1478
+ jumpQuery = q;
1479
+ panelOpen = false;
1480
+ searchEl.value = "";
1481
+ applyDocSearch();
1482
+ const url = new URL(hit.href, location.href);
1483
+ if (url.pathname + url.search === location.pathname + location.search) {
1484
+ e.preventDefault();
1485
+ jumpQuery = "";
1486
+ jumpToMatch(q);
1487
+ }
1488
+ }, true);
1489
+ var searchTimer;
1490
+ searchEl.addEventListener("input", () => {
1491
+ panelOpen = true;
1492
+ endMatches();
1493
+ clearTimeout(searchTimer);
1494
+ searchTimer = setTimeout(applyDocSearch, 120);
1495
+ });
1496
+ if (searchEl.value)
1497
+ applyDocSearch();
1498
+ document.addEventListener("keydown", (e) => {
1499
+ if (e.key === "Escape" && document.activeElement === searchEl) {
1500
+ searchEl.value = "";
1501
+ applyDocSearch();
1502
+ searchEl.blur();
1503
+ return;
1504
+ }
1505
+ if (matches.length && document.activeElement !== searchEl) {
1506
+ if (e.key === "Enter") {
1507
+ e.preventDefault();
1508
+ stepMatch(e.shiftKey ? -1 : 1);
1509
+ return;
1510
+ }
1511
+ if (e.key === "Escape") {
1512
+ endMatches();
1513
+ return;
1514
+ }
1515
+ }
1516
+ if (slashShouldFocus(e)) {
1517
+ e.preventDefault();
1518
+ searchEl.focus();
1519
+ }
1520
+ });
1521
+ if (localStorage.getItem("docsSidebarCollapsed") === "1")
1522
+ docSidebar.classList.add("collapsed");
1523
+ document.getElementById("sidebar-toggle").addEventListener("click", () => {
1524
+ localStorage.setItem("docsSidebarCollapsed", docSidebar.classList.toggle("collapsed") ? "1" : "0");
1525
+ });
1526
+ connectLive(() => void refreshDoc(), 150);
1527
+ })();
1528
+ `;
1529
+
1530
+ // src/generated/graph.js
1531
+ var graph_default = `(() => {
1532
+ // src/client/prefs.ts
1533
+ var THEMES = ["light", "dark"];
1534
+ var APPEARANCE = {
1535
+ accent: ["violet", "blue", "emerald", "amber", "rose"],
1536
+ font: ["sans", "serif", "mono"],
1537
+ size: ["compact", "default", "large"]
1538
+ };
1539
+ var DEFAULTS = { accent: "violet", font: "sans", size: "default" };
1540
+ function getPref(name) {
1541
+ const stored = localStorage.getItem("outcrop." + name);
1542
+ return stored && APPEARANCE[name].includes(stored) ? stored : DEFAULTS[name];
1543
+ }
1544
+ function setPref(name, value) {
1545
+ localStorage.setItem("outcrop." + name, value);
1546
+ applyAppearance();
1547
+ }
1548
+ function applyAppearance() {
1549
+ for (const name of Object.keys(APPEARANCE)) {
1550
+ document.documentElement.dataset[name] = getPref(name);
1551
+ }
1552
+ }
1553
+ function currentTheme() {
1554
+ const stored = localStorage.getItem("theme");
1555
+ if (stored === "light" || stored === "dark")
1556
+ return stored;
1557
+ return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
1558
+ }
1559
+ function applyTheme() {
1560
+ const theme = currentTheme();
1561
+ document.documentElement.classList.toggle("dark", theme === "dark");
1562
+ return theme;
1563
+ }
1564
+
1565
+ // src/client/chrome.ts
1566
+ var THEME_ANIM_MS = 260;
1567
+ var hooks = {};
1568
+ var sidebarWidthKey = "sidebarWidth";
1569
+ function applySidebarWidth() {
1570
+ const sb = document.querySelector(".sidebar-resizer")?.previousElementSibling;
1571
+ const w = localStorage.getItem(sidebarWidthKey);
1572
+ if (sb && w)
1573
+ sb.style.width = w + "px";
1574
+ }
1575
+ function initResizer() {
1576
+ applySidebarWidth();
1577
+ document.addEventListener("pointerdown", (e) => {
1578
+ const rz = e.target.closest?.(".sidebar-resizer");
1579
+ if (!rz)
1580
+ return;
1581
+ e.preventDefault();
1582
+ const sb = rz.previousElementSibling;
1583
+ const left = sb.getBoundingClientRect().left;
1584
+ rz.setPointerCapture(e.pointerId);
1585
+ const onMove = (ev) => {
1586
+ const w = Math.min(448, Math.max(176, Math.round(ev.clientX - left)));
1587
+ sb.style.width = w + "px";
1588
+ localStorage.setItem(sidebarWidthKey, String(w));
1589
+ };
1590
+ rz.addEventListener("pointermove", onMove);
1591
+ rz.addEventListener("pointerup", () => rz.removeEventListener("pointermove", onMove), { once: true });
1592
+ });
1593
+ }
1594
+ var THEME_ICONS = {
1595
+ light: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.4"/><path d="M8 1v1.6M8 13.4V15M15 8h-1.6M2.6 8H1M12.9 3.1l-1.1 1.1M4.2 11.8l-1.1 1.1M12.9 12.9l-1.1-1.1M4.2 4.2 3.1 3.1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
1596
+ dark: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M13.5 9.6A6 6 0 0 1 6.4 2.5a6 6 0 1 0 7.1 7.1Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>'
1597
+ };
1598
+ var themeBtn;
1599
+ function renderTheme() {
1600
+ const t = applyTheme();
1601
+ themeBtn.innerHTML = THEME_ICONS[t];
1602
+ themeBtn.title = "Switch to " + (t === "dark" ? "light" : "dark") + " mode";
1603
+ for (const b of document.querySelectorAll("[data-set-theme]")) {
1604
+ b.classList.toggle("theme-active", b.dataset.setTheme === t);
1605
+ }
1606
+ renderAppearance();
1607
+ }
1608
+ function renderAppearance() {
1609
+ for (const b of document.querySelectorAll("[data-set-appearance]")) {
1610
+ b.classList.toggle("active", getPref(b.dataset.setAppearance) === b.dataset.value);
1611
+ }
1612
+ }
1613
+ var themeAnimTimer;
1614
+ function setTheme(t) {
1615
+ const changed = t !== currentTheme();
1616
+ localStorage.setItem("theme", t);
1617
+ if (changed) {
1618
+ document.documentElement.classList.add("theme-anim");
1619
+ clearTimeout(themeAnimTimer);
1620
+ themeAnimTimer = setTimeout(() => document.documentElement.classList.remove("theme-anim"), THEME_ANIM_MS + 60);
1621
+ }
1622
+ renderTheme();
1623
+ if (changed)
1624
+ hooks.onAppearanceChange?.();
1625
+ }
1626
+ function initTheme() {
1627
+ themeBtn = document.getElementById("theme-toggle");
1628
+ themeBtn.addEventListener("click", () => setTheme(THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length]));
1629
+ document.addEventListener("click", (e) => {
1630
+ const target = e.target;
1631
+ const theme = target.closest("[data-set-theme]");
1632
+ if (theme) {
1633
+ setTheme(theme.dataset.setTheme);
1634
+ return;
1635
+ }
1636
+ const pref = target.closest("[data-set-appearance]");
1637
+ if (!pref)
1638
+ return;
1639
+ setPref(pref.dataset.setAppearance, pref.dataset.value);
1640
+ renderAppearance();
1641
+ hooks.onAppearanceChange?.();
1642
+ });
1643
+ matchMedia("(prefers-color-scheme: dark)").addEventListener("change", renderTheme);
1644
+ renderTheme();
1645
+ }
1646
+ var sheet;
1647
+ var sheetContent;
1648
+ var backdrop;
1649
+ var lastFocus = null;
1650
+ var toastTimer;
1651
+ function showToast(msg) {
1652
+ const toast = document.getElementById("toast");
1653
+ toast.textContent = msg;
1654
+ toast.classList.add("opacity-100");
1655
+ clearTimeout(toastTimer);
1656
+ toastTimer = setTimeout(() => toast.classList.remove("opacity-100"), 2500);
1657
+ }
1658
+ function sheetIsOpen() {
1659
+ return !sheet.classList.contains("translate-x-full");
1660
+ }
1661
+ function showSheet(id) {
1662
+ backdrop.classList.remove("hidden");
1663
+ sheet.classList.remove("translate-x-full");
1664
+ if (!sheet.contains(document.activeElement))
1665
+ lastFocus = document.activeElement;
1666
+ sheet.focus({ preventScroll: true });
1667
+ sheet.scrollTop = 0;
1668
+ history.replaceState(null, "", "#" + id);
1669
+ }
1670
+ function closeSheet() {
1671
+ sheet.classList.add("translate-x-full");
1672
+ backdrop.classList.add("hidden");
1673
+ lastFocus?.focus({ preventScroll: true });
1674
+ history.replaceState(null, "", location.pathname + location.search);
1675
+ }
1676
+ var sheetSeq = 0;
1677
+ var paneCache = new Map;
1678
+ function clearPaneCache() {
1679
+ paneCache.clear();
1680
+ }
1681
+ async function openSheet(id) {
1682
+ const seq = ++sheetSeq;
1683
+ const tpl = document.getElementById("detail-" + id);
1684
+ if (tpl) {
1685
+ sheetContent.replaceChildren(tpl.content.cloneNode(true));
1686
+ renderTheme();
1687
+ const form = sheetContent.querySelector('.detail-root[data-id="new"]');
1688
+ if (form) {
1689
+ setKind(form, localStorage.getItem("newKind") || "task");
1690
+ showSheet(id);
1691
+ form.querySelector('[name="title"]')?.focus();
1692
+ return;
1693
+ }
1694
+ showSheet(id);
1695
+ return;
1696
+ }
1697
+ const cached = paneCache.get(id);
1698
+ if (cached !== undefined) {
1699
+ sheetContent.innerHTML = cached;
1700
+ showSheet(id);
1701
+ return;
1702
+ }
1703
+ sheetContent.innerHTML = '<div class="pt-1 text-[12px] text-zinc-400 dark:text-zinc-500">Loading…</div>';
1704
+ showSheet(id);
1705
+ let html = null;
1706
+ try {
1707
+ const res = await fetch("/api/items/" + encodeURIComponent(id) + "/pane");
1708
+ if (res.ok)
1709
+ html = await res.text();
1710
+ } catch {}
1711
+ if (seq !== sheetSeq)
1712
+ return;
1713
+ if (html === null) {
1714
+ sheetContent.innerHTML = '<div class="pt-1 text-[12px] text-zinc-400">Not found.</div>';
1715
+ return;
1716
+ }
1717
+ paneCache.set(id, html);
1718
+ sheetContent.innerHTML = html;
1719
+ }
1720
+ async function saveSettings(root) {
1721
+ const val = (n) => root.querySelector(\`[name="\${n}"]\`);
1722
+ const err = root.querySelector("#settings-error");
1723
+ const views = [...root.querySelectorAll('[name^="view-"]')].filter((c) => c.checked).map((c) => c.name.slice(5));
1724
+ if (!views.length) {
1725
+ err.textContent = "Keep at least one view on: with all of them off there is nothing to show.";
1726
+ err.classList.remove("hidden");
1727
+ root.querySelector(".set-group:has([name^='view-'])")?.setAttribute("open", "");
1728
+ return;
1729
+ }
1730
+ err.classList.add("hidden");
1731
+ try {
1732
+ const res = await fetch("/api/settings", {
1733
+ method: "PUT",
1734
+ headers: { "content-type": "application/json" },
1735
+ body: JSON.stringify({
1736
+ title: val("title").value,
1737
+ views,
1738
+ columns: val("columns").value.split(","),
1739
+ gitCommit: val("gitCommit").checked
1740
+ })
1741
+ });
1742
+ if (!res.ok) {
1743
+ const body = await res.json().catch(() => null);
1744
+ err.textContent = body?.error ?? "Save failed.";
1745
+ err.classList.remove("hidden");
1746
+ return;
1747
+ }
1748
+ showToast("Settings saved to .outcrop.json");
1749
+ const saved = await res.json();
1750
+ if (root.dataset.views !== saved.views.join(","))
1751
+ return location.reload();
1752
+ closeSheet();
1753
+ await hooks.refreshView?.();
1754
+ } catch {
1755
+ err.textContent = "Save failed: server unreachable.";
1756
+ err.classList.remove("hidden");
1757
+ }
1758
+ }
1759
+ async function toggleCheckbox(url, index, checked, rev) {
1760
+ try {
1761
+ const res = await fetch(url, {
1762
+ method: "PATCH",
1763
+ headers: { "content-type": "application/json" },
1764
+ body: JSON.stringify({ checkbox: { index, checked }, rev })
1765
+ });
1766
+ if (res.status === 409) {
1767
+ showToast("Changed on disk elsewhere: showing latest");
1768
+ } else if (!res.ok) {
1769
+ showToast("Update failed");
1770
+ return;
1771
+ }
1772
+ await hooks.refreshView?.();
1773
+ } catch {
1774
+ showToast("Update failed: server unreachable");
1775
+ }
1776
+ }
1777
+ function initCheckboxes() {
1778
+ document.addEventListener("click", (e) => {
1779
+ const cb = e.target.closest("[data-cb]");
1780
+ if (!cb)
1781
+ return;
1782
+ const index = Number(cb.dataset.cb);
1783
+ const checked = !cb.classList.contains("checked");
1784
+ cb.classList.toggle("checked", checked);
1785
+ cb.closest("li.check")?.classList.toggle("done", checked);
1786
+ const item = cb.closest(".detail-root[data-id][data-rev]");
1787
+ if (item) {
1788
+ toggleCheckbox("/api/items/" + encodeURIComponent(item.dataset.id), index, checked, item.dataset.rev);
1789
+ return;
1790
+ }
1791
+ const doc = cb.closest("#doc-content[data-file]");
1792
+ if (doc) {
1793
+ toggleCheckbox("/api/docs?file=" + encodeURIComponent(doc.dataset.file), index, checked, doc.dataset.rev);
1794
+ }
1795
+ });
1796
+ }
1797
+ var KIND_ENDPOINT = { task: "/api/tasks", epic: "/api/epics", doc: "/api/docs" };
1798
+ function setKind(root, kind) {
1799
+ root.dataset.kind = kind;
1800
+ localStorage.setItem("newKind", kind);
1801
+ for (const b of root.querySelectorAll("[data-kind]")) {
1802
+ b.classList.toggle("active", b.dataset.kind === kind);
1803
+ }
1804
+ for (const g of root.querySelectorAll("[data-for]")) {
1805
+ g.classList.toggle("hidden", g.dataset.for !== kind);
1806
+ }
1807
+ root.querySelector("#new-target").textContent = kind === "doc" ? "" : kind === "epic" ? "epics/" : "tasks/";
1808
+ }
1809
+ async function createItem(root) {
1810
+ const kind = root.dataset.kind || "task";
1811
+ const val = (n) => root.querySelector(\`[name="\${n}"]\`);
1812
+ const err = root.querySelector("#new-error");
1813
+ const fail = (msg) => {
1814
+ err.textContent = msg;
1815
+ err.classList.remove("hidden");
1816
+ };
1817
+ const title = val("title").value.trim();
1818
+ if (!title)
1819
+ return fail("Give it a title.");
1820
+ const payload = { title, body: val("body").value };
1821
+ if (kind === "task") {
1822
+ payload.epic = val("epic").value;
1823
+ payload.status = val("status").value;
1824
+ payload.priority = val("priority").value;
1825
+ payload.subtasks = val("subtasks").value;
1826
+ }
1827
+ if (kind === "doc") {
1828
+ payload.path = val("path").value.trim() || title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
1829
+ }
1830
+ err.classList.add("hidden");
1831
+ const btn = root.querySelector("#new-save");
1832
+ btn.disabled = true;
1833
+ try {
1834
+ const res = await fetch(KIND_ENDPOINT[kind], {
1835
+ method: "POST",
1836
+ headers: { "content-type": "application/json" },
1837
+ body: JSON.stringify(payload)
1838
+ });
1839
+ const out = await res.json().catch(() => null);
1840
+ if (!res.ok)
1841
+ return fail(out?.error ?? "Could not create that.");
1842
+ if (kind === "doc" && out?.file) {
1843
+ location.href = "/docs?file=" + encodeURIComponent(out.file);
1844
+ return;
1845
+ }
1846
+ showToast(\`\${out?.id ?? "Item"} created\`);
1847
+ closeSheet();
1848
+ await hooks.refreshView?.();
1849
+ } catch {
1850
+ fail("Could not reach the server.");
1851
+ } finally {
1852
+ btn.disabled = false;
1853
+ }
1854
+ }
1855
+ function initSheet() {
1856
+ sheet = document.getElementById("sheet");
1857
+ backdrop = document.getElementById("backdrop");
1858
+ sheetContent = document.getElementById("sheet-content");
1859
+ document.addEventListener("click", (e) => {
1860
+ const target = e.target;
1861
+ const save = target.closest("#settings-save");
1862
+ if (save) {
1863
+ saveSettings(save.closest(".detail-root"));
1864
+ return;
1865
+ }
1866
+ const create = target.closest("#new-save");
1867
+ if (create) {
1868
+ createItem(create.closest(".detail-root"));
1869
+ return;
1870
+ }
1871
+ const kind = target.closest("[data-kind]");
1872
+ if (kind) {
1873
+ setKind(kind.closest(".detail-root"), kind.dataset.kind);
1874
+ return;
1875
+ }
1876
+ const open = target.closest("[data-open]");
1877
+ if (open)
1878
+ openSheet(open.dataset.open);
1879
+ });
1880
+ document.addEventListener("keydown", (e) => {
1881
+ if (e.key === "Escape" && sheetIsOpen())
1882
+ closeSheet();
1883
+ });
1884
+ backdrop.addEventListener("click", closeSheet);
1885
+ document.getElementById("sheet-close").addEventListener("click", closeSheet);
1886
+ document.getElementById("sheet-expand").addEventListener("click", () => sheet.classList.toggle("fullscreen"));
1887
+ }
1888
+ function connectLive(onChange, debounceMs = 150) {
1889
+ const dot = document.getElementById("live-dot");
1890
+ let timer;
1891
+ let es = null;
1892
+ let connectedOnce = false;
1893
+ const open = () => {
1894
+ if (es)
1895
+ return;
1896
+ es = new EventSource("/sse");
1897
+ es.onmessage = () => {
1898
+ clearTimeout(timer);
1899
+ timer = setTimeout(onChange, debounceMs);
1900
+ };
1901
+ es.onerror = () => dot.classList.replace("bg-emerald-500", "bg-zinc-300");
1902
+ es.onopen = () => {
1903
+ dot.classList.replace("bg-zinc-300", "bg-emerald-500");
1904
+ if (connectedOnce)
1905
+ onChange();
1906
+ connectedOnce = true;
1907
+ };
1908
+ };
1909
+ addEventListener("pagehide", () => {
1910
+ es?.close();
1911
+ es = null;
1912
+ clearTimeout(timer);
1913
+ });
1914
+ addEventListener("pageshow", (e) => {
1915
+ if (e.persisted)
1916
+ open();
1917
+ });
1918
+ open();
1919
+ }
1920
+ var macLike = /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
1921
+ function initSearchShortcut(searchBox) {
1922
+ searchBox.placeholder = macLike ? "Search ⌘K" : "Search Ctrl+K";
1923
+ searchBox.title = macLike ? "Search (⌘K or /)" : "Search (Ctrl+K or /)";
1924
+ document.addEventListener("keydown", (e) => {
1925
+ if (e.key !== "k" && e.key !== "K")
1926
+ return;
1927
+ if (!(macLike ? e.metaKey : e.ctrlKey) || e.altKey)
1928
+ return;
1929
+ e.preventDefault();
1930
+ searchBox.focus();
1931
+ searchBox.select();
1932
+ });
1933
+ }
1934
+ function slashShouldFocus(e) {
1935
+ const el = document.activeElement;
1936
+ return e.key === "/" && !e.metaKey && !e.ctrlKey && !e.altKey && !/INPUT|TEXTAREA|SELECT/.test(el?.tagName ?? "") && !el?.isContentEditable;
1937
+ }
1938
+ function initChrome() {
1939
+ initResizer();
1940
+ initTheme();
1941
+ initSheet();
1942
+ initCheckboxes();
1943
+ }
1944
+
1945
+ // src/client/graph.ts
1946
+ initChrome();
1947
+ var canvas = document.getElementById("graph-canvas");
1948
+ var ctx = canvas.getContext("2d");
1949
+ var emptyMsg = document.getElementById("graph-empty");
1950
+ var hintEl = document.getElementById("graph-hint");
1951
+ var nodes = [];
1952
+ var edges = [];
1953
+ var neighbours = new Map;
1954
+ var globe = localStorage.getItem("graphGlobe") !== "0";
1955
+ var CHARGE = 190;
1956
+ var LINK_DISTANCE = 58;
1957
+ var GRAVITY = 0.05;
1958
+ var VELOCITY_DECAY = 0.6;
1959
+ var MAX_SPEED = 22;
1960
+ var ALPHA_DECAY = 0.0225;
1961
+ var ALPHA_MIN = 0.002;
1962
+ var globeRadius = () => LINK_DISTANCE * Math.sqrt(Math.max(nodes.length, 4)) / 3.2;
1963
+ var alpha = 1;
1964
+ function reheat(to = 0.5) {
1965
+ alpha = Math.max(alpha, to);
1966
+ }
1967
+ function step() {
1968
+ if (alpha < ALPHA_MIN)
1969
+ return;
1970
+ const n = nodes.length;
1971
+ for (let i = 0;i < n; i++) {
1972
+ const a = nodes[i];
1973
+ for (let j = i + 1;j < n; j++) {
1974
+ const b = nodes[j];
1975
+ let dx = b.x - a.x;
1976
+ let dy = b.y - a.y;
1977
+ let dz = b.z - a.z;
1978
+ let d2 = dx * dx + dy * dy + dz * dz;
1979
+ if (d2 < 16) {
1980
+ dx = (i * 37 + j * 11) % 9 - 4 + 0.5;
1981
+ dy = (i * 17 + j * 29) % 9 - 4 + 0.5;
1982
+ dz = globe ? (i * 23 + j * 41) % 9 - 4 + 0.5 : 0;
1983
+ d2 = dx * dx + dy * dy + dz * dz;
1984
+ }
1985
+ const w = CHARGE * alpha / d2;
1986
+ a.vx -= dx * w;
1987
+ a.vy -= dy * w;
1988
+ a.vz -= dz * w;
1989
+ b.vx += dx * w;
1990
+ b.vy += dy * w;
1991
+ b.vz += dz * w;
1992
+ }
1993
+ }
1994
+ for (const e of edges) {
1995
+ const dx = e.b.x - e.a.x;
1996
+ const dy = e.b.y - e.a.y;
1997
+ const dz = e.b.z - e.a.z;
1998
+ const d = Math.hypot(dx, dy, dz) || 1;
1999
+ const f = (d - LINK_DISTANCE) / d * alpha * e.strength;
2000
+ e.b.vx -= dx * f * e.bias;
2001
+ e.b.vy -= dy * f * e.bias;
2002
+ e.b.vz -= dz * f * e.bias;
2003
+ e.a.vx += dx * f * (1 - e.bias);
2004
+ e.a.vy += dy * f * (1 - e.bias);
2005
+ e.a.vz += dz * f * (1 - e.bias);
2006
+ }
2007
+ const R = globeRadius();
2008
+ let cx = 0;
2009
+ let cy = 0;
2010
+ for (const nd of nodes) {
2011
+ if (nd === dragging) {
2012
+ nd.vx = nd.vy = nd.vz = 0;
2013
+ cx += nd.x;
2014
+ cy += nd.y;
2015
+ continue;
2016
+ }
2017
+ if (globe) {
2018
+ const len = Math.hypot(nd.x, nd.y, nd.z) || 1;
2019
+ const ux = nd.x / len;
2020
+ const uy = nd.y / len;
2021
+ const uz = nd.z / len;
2022
+ const radial = nd.vx * ux + nd.vy * uy + nd.vz * uz;
2023
+ nd.vx -= radial * ux;
2024
+ nd.vy -= radial * uy;
2025
+ nd.vz -= radial * uz;
2026
+ } else {
2027
+ nd.vx -= nd.x * GRAVITY * alpha;
2028
+ nd.vy -= nd.y * GRAVITY * alpha;
2029
+ }
2030
+ nd.vx *= VELOCITY_DECAY;
2031
+ nd.vy *= VELOCITY_DECAY;
2032
+ nd.vz *= VELOCITY_DECAY;
2033
+ const speed = Math.hypot(nd.vx, nd.vy, nd.vz);
2034
+ if (speed > MAX_SPEED) {
2035
+ nd.vx = nd.vx / speed * MAX_SPEED;
2036
+ nd.vy = nd.vy / speed * MAX_SPEED;
2037
+ nd.vz = nd.vz / speed * MAX_SPEED;
2038
+ }
2039
+ nd.x += nd.vx;
2040
+ nd.y += nd.vy;
2041
+ nd.z += nd.vz;
2042
+ if (globe) {
2043
+ const len = Math.hypot(nd.x, nd.y, nd.z) || 1;
2044
+ nd.x = nd.x / len * R;
2045
+ nd.y = nd.y / len * R;
2046
+ nd.z = nd.z / len * R;
2047
+ } else {
2048
+ nd.z = nd.vz = 0;
2049
+ }
2050
+ cx += nd.x;
2051
+ cy += nd.y;
2052
+ }
2053
+ if (!globe && n) {
2054
+ cx /= n;
2055
+ cy /= n;
2056
+ for (const nd of nodes) {
2057
+ nd.x -= cx;
2058
+ nd.y -= cy;
2059
+ }
2060
+ }
2061
+ alpha -= alpha * ALPHA_DECAY;
2062
+ }
2063
+ function settle() {
2064
+ const ticks = Math.round(Math.min(260, Math.max(60, 9000 / Math.max(nodes.length, 1))));
2065
+ for (let i = 0;i < ticks; i++)
2066
+ step();
2067
+ }
2068
+ function seedPosition(n, i, count) {
2069
+ if (globe) {
2070
+ const R = globeRadius();
2071
+ const y = count > 1 ? 1 - i / (count - 1) * 2 : 0;
2072
+ const ring = Math.sqrt(Math.max(0, 1 - y * y));
2073
+ const theta = i * 2.39996;
2074
+ n.x = Math.cos(theta) * ring * R;
2075
+ n.y = y * R;
2076
+ n.z = Math.sin(theta) * ring * R;
2077
+ } else {
2078
+ const theta = i * 2.39996;
2079
+ const radius = 26 * Math.sqrt(0.5 + i);
2080
+ n.x = Math.cos(theta) * radius;
2081
+ n.y = Math.sin(theta) * radius;
2082
+ n.z = 0;
2083
+ }
2084
+ n.vx = n.vy = n.vz = 0;
2085
+ }
2086
+ var CAMERA_DISTANCE = 2.6;
2087
+ var scale = 1;
2088
+ var panX = 0;
2089
+ var panY = 0;
2090
+ var yaw = 0.6;
2091
+ var pitch = -0.25;
2092
+ var spinning = true;
2093
+ var cameraTouched = false;
2094
+ var cosYaw = 1;
2095
+ var sinYaw = 0;
2096
+ var cosPitch = 1;
2097
+ var sinPitch = 0;
2098
+ var camDistance = 1;
2099
+ var halfW = 0;
2100
+ var halfH = 0;
2101
+ var globeFit = 1;
2102
+ function syncCamera() {
2103
+ cosYaw = Math.cos(yaw);
2104
+ sinYaw = Math.sin(yaw);
2105
+ cosPitch = Math.cos(pitch);
2106
+ sinPitch = Math.sin(pitch);
2107
+ camDistance = globeRadius() * CAMERA_DISTANCE;
2108
+ halfW = canvas.clientWidth / 2;
2109
+ halfH = canvas.clientHeight / 2;
2110
+ globeFit = Math.min(canvas.clientWidth, canvas.clientHeight) * 0.38 / silhouette();
2111
+ }
2112
+ function project(x, y, z) {
2113
+ if (!globe)
2114
+ return { x: x * scale + panX, y: y * scale + panY, k: 1, z: 0 };
2115
+ const x1 = x * cosYaw + z * sinYaw;
2116
+ const z1 = z * cosYaw - x * sinYaw;
2117
+ const y2 = y * cosPitch - z1 * sinPitch;
2118
+ const z2 = z1 * cosPitch + y * sinPitch;
2119
+ const k = camDistance / (camDistance - z2);
2120
+ const m = k * globeFit * scale;
2121
+ return { x: halfW + x1 * m, y: halfH + y2 * m, k, z: z2 };
2122
+ }
2123
+ function projectAll() {
2124
+ syncCamera();
2125
+ for (const n of nodes) {
2126
+ const p = project(n.x, n.y, n.z);
2127
+ n.sx = p.x;
2128
+ n.sy = p.y;
2129
+ n.sk = p.k;
2130
+ n.sz = p.z;
2131
+ }
2132
+ }
2133
+ function silhouette() {
2134
+ const R = globeRadius();
2135
+ const d = camDistance || R * CAMERA_DISTANCE;
2136
+ return d * R / Math.sqrt(Math.max(d * d - R * R, 1));
2137
+ }
2138
+ function fitView() {
2139
+ syncCamera();
2140
+ const w = canvas.clientWidth;
2141
+ const h = canvas.clientHeight;
2142
+ if (globe) {
2143
+ panX = panY = 0;
2144
+ scale = 1;
2145
+ return;
2146
+ }
2147
+ if (!nodes.length) {
2148
+ scale = 1;
2149
+ panX = w / 2;
2150
+ panY = h / 2;
2151
+ return;
2152
+ }
2153
+ let minX = Infinity;
2154
+ let minY = Infinity;
2155
+ let maxX = -Infinity;
2156
+ let maxY = -Infinity;
2157
+ for (const n of nodes) {
2158
+ minX = Math.min(minX, n.x);
2159
+ maxX = Math.max(maxX, n.x);
2160
+ minY = Math.min(minY, n.y);
2161
+ maxY = Math.max(maxY, n.y);
2162
+ }
2163
+ const pad = 90;
2164
+ scale = Math.min(2, Math.max(0.15, Math.min((w - pad) / Math.max(maxX - minX, 1), (h - pad) / Math.max(maxY - minY, 1))));
2165
+ panX = w / 2 - (minX + maxX) / 2 * scale;
2166
+ panY = h / 2 - (minY + maxY) / 2 * scale;
2167
+ }
2168
+ function resetView() {
2169
+ cameraTouched = false;
2170
+ if (globe) {
2171
+ yaw = 0.6;
2172
+ pitch = -0.25;
2173
+ spinning = true;
2174
+ }
2175
+ fitView();
2176
+ }
2177
+ function nodeAt(sx, sy) {
2178
+ projectAll();
2179
+ let best = null;
2180
+ for (const n of nodes) {
2181
+ const hit = Math.max(n.r * scale * n.sk, 6) + 4;
2182
+ if ((n.sx - sx) ** 2 + (n.sy - sy) ** 2 > hit * hit)
2183
+ continue;
2184
+ if (!best || n.sz > best.sz)
2185
+ best = n;
2186
+ }
2187
+ return best;
2188
+ }
2189
+ var cssVar = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
2190
+ var accent = (alphaValue) => \`rgba(\${cssVar("--accent-500").replace(/\\s+/g, ",")},\${alphaValue})\`;
2191
+ var isDark = () => document.documentElement.classList.contains("dark");
2192
+ var hovered = null;
2193
+ var query = "";
2194
+ function shortLabel(n) {
2195
+ if (n === hovered || n.label.length <= 24)
2196
+ return n.label;
2197
+ return n.label.slice(0, 23).trimEnd() + "…";
2198
+ }
2199
+ function focusOf(n) {
2200
+ if (hovered)
2201
+ return n === hovered || neighbours.get(hovered.id)?.has(n.id) ? 1 : 0.12;
2202
+ if (query)
2203
+ return n.label.toLowerCase().includes(query) || n.id.toLowerCase().includes(query) ? 1 : 0.12;
2204
+ return 1;
2205
+ }
2206
+ function depthOf(n) {
2207
+ if (!globe)
2208
+ return 1;
2209
+ return 0.25 + 0.75 * ((n.sz / globeRadius() + 1) / 2);
2210
+ }
2211
+ function strokeEdge(a, b) {
2212
+ ctx.beginPath();
2213
+ if (!globe) {
2214
+ ctx.moveTo(a.sx, a.sy);
2215
+ ctx.lineTo(b.sx, b.sy);
2216
+ ctx.stroke();
2217
+ return;
2218
+ }
2219
+ const R = globeRadius();
2220
+ const dot = (a.x * b.x + a.y * b.y + a.z * b.z) / (R * R);
2221
+ const omega = Math.acos(Math.min(1, Math.max(-1, dot)));
2222
+ const sinOmega = Math.sin(omega);
2223
+ if (sinOmega < 0.001) {
2224
+ ctx.moveTo(a.sx, a.sy);
2225
+ ctx.lineTo(b.sx, b.sy);
2226
+ ctx.stroke();
2227
+ return;
2228
+ }
2229
+ const steps = edges.length > 600 ? 4 : 8;
2230
+ ctx.moveTo(a.sx, a.sy);
2231
+ for (let i = 1;i <= steps; i++) {
2232
+ const t = i / steps;
2233
+ const wa = Math.sin((1 - t) * omega) / sinOmega;
2234
+ const wb = Math.sin(t * omega) / sinOmega;
2235
+ const p = project(a.x * wa + b.x * wb, a.y * wa + b.y * wb, a.z * wa + b.z * wb);
2236
+ ctx.lineTo(p.x, p.y);
2237
+ }
2238
+ ctx.stroke();
2239
+ }
2240
+ function draw() {
2241
+ const dpr = devicePixelRatio || 1;
2242
+ const w = canvas.clientWidth;
2243
+ const h = canvas.clientHeight;
2244
+ if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
2245
+ canvas.width = Math.round(w * dpr);
2246
+ canvas.height = Math.round(h * dpr);
2247
+ }
2248
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2249
+ ctx.clearRect(0, 0, w, h);
2250
+ projectAll();
2251
+ const dark = isDark();
2252
+ if (globe && nodes.length) {
2253
+ ctx.strokeStyle = dark ? "rgba(113,113,122,0.22)" : "rgba(161,161,170,0.3)";
2254
+ ctx.lineWidth = 1;
2255
+ ctx.beginPath();
2256
+ ctx.arc(halfW, halfH, silhouette() * globeFit * scale, 0, Math.PI * 2);
2257
+ ctx.stroke();
2258
+ }
2259
+ ctx.lineWidth = 1;
2260
+ for (const { a, b } of edges) {
2261
+ const lit = hovered ? a === hovered || b === hovered : Math.min(focusOf(a), focusOf(b)) === 1;
2262
+ ctx.globalAlpha = Math.min(depthOf(a), depthOf(b));
2263
+ ctx.strokeStyle = lit ? accent(0.55) : dark ? \`rgba(113,113,122,\${hovered || query ? 0.08 : 0.28})\` : \`rgba(161,161,170,\${hovered || query ? 0.12 : 0.4})\`;
2264
+ strokeEdge(a, b);
2265
+ }
2266
+ ctx.globalAlpha = 1;
2267
+ const order = globe ? [...nodes].sort((p, q) => p.sz - q.sz) : nodes;
2268
+ for (const n of order) {
2269
+ const f = focusOf(n) * depthOf(n);
2270
+ const r = Math.max(n.r * scale * n.sk, 2.5);
2271
+ ctx.globalAlpha = f;
2272
+ ctx.fillStyle = n === hovered ? accent(1) : accent(0.75);
2273
+ ctx.beginPath();
2274
+ ctx.arc(n.sx, n.sy, r, 0, Math.PI * 2);
2275
+ ctx.fill();
2276
+ const room = globe ? n.sz > 0 && scale * n.sk > 0.45 : scale > 0.55;
2277
+ if (room && (focusOf(n) === 1 || scale > 1.2)) {
2278
+ ctx.globalAlpha = f * (n === hovered ? 1 : 0.85);
2279
+ ctx.font = \`\${Math.min(13, 10 + n.deg * 0.3)}px ui-sans-serif, system-ui, sans-serif\`;
2280
+ ctx.textAlign = "center";
2281
+ ctx.textBaseline = "top";
2282
+ ctx.fillStyle = dark ? "#d4d4d8" : "#3f3f46";
2283
+ ctx.fillText(shortLabel(n), n.sx, n.sy + r + 3);
2284
+ }
2285
+ ctx.globalAlpha = 1;
2286
+ }
2287
+ }
2288
+ var frameCount = 0;
2289
+ function frame() {
2290
+ step();
2291
+ if (globe && spinning && !hovered && !dragging && !rotating)
2292
+ yaw += 0.0016;
2293
+ if (!globe && !cameraTouched && alpha > 0.02 && ++frameCount % 12 === 0)
2294
+ fitView();
2295
+ draw();
2296
+ requestAnimationFrame(frame);
2297
+ }
2298
+ async function load(initial) {
2299
+ const data = initial ?? await fetch("/api/graph").then((r) => r.json()).catch(() => null);
2300
+ if (!data)
2301
+ return;
2302
+ const previous = new Map(nodes.map((n) => [n.id, n]));
2303
+ const fresh = !previous.size;
2304
+ nodes = data.nodes.map((n) => {
2305
+ const old = previous.get(n.id);
2306
+ return {
2307
+ ...n,
2308
+ x: old?.x ?? 0,
2309
+ y: old?.y ?? 0,
2310
+ z: old?.z ?? 0,
2311
+ vx: 0,
2312
+ vy: 0,
2313
+ vz: 0,
2314
+ r: 3.5 + Math.min(9, Math.sqrt(n.deg) * 2.4),
2315
+ sx: 0,
2316
+ sy: 0,
2317
+ sk: 1,
2318
+ sz: 0
2319
+ };
2320
+ });
2321
+ nodes.forEach((n, i) => {
2322
+ if (!previous.has(n.id))
2323
+ seedPosition(n, i, nodes.length);
2324
+ });
2325
+ const byId = new Map(nodes.map((n) => [n.id, n]));
2326
+ const degree = new Map;
2327
+ const pairs = data.links.flatMap((l) => {
2328
+ const a = byId.get(l.source);
2329
+ const b = byId.get(l.target);
2330
+ if (!a || !b)
2331
+ return [];
2332
+ degree.set(a.id, (degree.get(a.id) ?? 0) + 1);
2333
+ degree.set(b.id, (degree.get(b.id) ?? 0) + 1);
2334
+ return [{ a, b }];
2335
+ });
2336
+ edges = pairs.map(({ a, b }) => {
2337
+ const da = degree.get(a.id);
2338
+ const db = degree.get(b.id);
2339
+ return { a, b, strength: 1 / Math.min(da, db), bias: da / (da + db) };
2340
+ });
2341
+ neighbours = new Map(nodes.map((n) => [n.id, new Set([n.id])]));
2342
+ for (const { a, b } of edges) {
2343
+ neighbours.get(a.id).add(b.id);
2344
+ neighbours.get(b.id).add(a.id);
2345
+ }
2346
+ hovered = null;
2347
+ emptyMsg.classList.toggle("hidden", nodes.length > 0);
2348
+ emptyMsg.classList.toggle("flex", nodes.length === 0);
2349
+ if (fresh) {
2350
+ alpha = 1;
2351
+ settle();
2352
+ if (!cameraTouched)
2353
+ fitView();
2354
+ } else {
2355
+ reheat(0.4);
2356
+ }
2357
+ }
2358
+ var shapeBtn = document.getElementById("graph-shape");
2359
+ function applyShape() {
2360
+ shapeBtn.textContent = globe ? "Flat" : "Globe";
2361
+ shapeBtn.title = globe ? "Lay the graph out on a plane" : "Wrap the graph onto a globe";
2362
+ hintEl.textContent = globe ? "Scroll to zoom · drag to turn the globe · click a note to open it" : "Scroll to zoom · drag to pan · click a note to open it";
2363
+ }
2364
+ shapeBtn.addEventListener("click", () => {
2365
+ globe = !globe;
2366
+ localStorage.setItem("graphGlobe", globe ? "1" : "0");
2367
+ nodes.forEach((n, i) => seedPosition(n, i, nodes.length));
2368
+ alpha = 1;
2369
+ settle();
2370
+ resetView();
2371
+ applyShape();
2372
+ });
2373
+ var dragging = null;
2374
+ var panning = false;
2375
+ var rotating = false;
2376
+ var moved = false;
2377
+ var lastX = 0;
2378
+ var lastY = 0;
2379
+ canvas.addEventListener("pointerdown", (e) => {
2380
+ canvas.setPointerCapture(e.pointerId);
2381
+ lastX = e.offsetX;
2382
+ lastY = e.offsetY;
2383
+ moved = false;
2384
+ dragging = nodeAt(e.offsetX, e.offsetY);
2385
+ rotating = globe && !dragging;
2386
+ panning = !globe && !dragging;
2387
+ canvas.style.cursor = "grabbing";
2388
+ });
2389
+ canvas.addEventListener("pointermove", (e) => {
2390
+ if (dragging && !globe) {
2391
+ dragging.x = (e.offsetX - panX) / scale;
2392
+ dragging.y = (e.offsetY - panY) / scale;
2393
+ dragging.vx = dragging.vy = 0;
2394
+ reheat(0.35);
2395
+ } else if (rotating) {
2396
+ spinning = false;
2397
+ yaw += (e.offsetX - lastX) * 0.006;
2398
+ pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch + (e.offsetY - lastY) * 0.006));
2399
+ } else if (panning) {
2400
+ cameraTouched = true;
2401
+ panX += e.offsetX - lastX;
2402
+ panY += e.offsetY - lastY;
2403
+ } else {
2404
+ const over = nodeAt(e.offsetX, e.offsetY);
2405
+ if (over !== hovered) {
2406
+ hovered = over;
2407
+ canvas.title = over ? over.id : "";
2408
+ canvas.style.cursor = over ? "pointer" : "grab";
2409
+ }
2410
+ }
2411
+ if (Math.abs(e.offsetX - lastX) + Math.abs(e.offsetY - lastY) > 3)
2412
+ moved = true;
2413
+ lastX = e.offsetX;
2414
+ lastY = e.offsetY;
2415
+ });
2416
+ canvas.addEventListener("pointerup", (e) => {
2417
+ if (dragging && !moved)
2418
+ location.href = "/docs?file=" + encodeURIComponent(dragging.id);
2419
+ dragging = null;
2420
+ panning = false;
2421
+ rotating = false;
2422
+ canvas.style.cursor = nodeAt(e.offsetX, e.offsetY) ? "pointer" : "grab";
2423
+ });
2424
+ canvas.addEventListener("wheel", (e) => {
2425
+ e.preventDefault();
2426
+ const factor = Math.exp(-e.deltaY * 0.0015);
2427
+ cameraTouched = true;
2428
+ const next = Math.min(4, Math.max(0.15, scale * factor));
2429
+ if (globe) {
2430
+ scale = next;
2431
+ return;
2432
+ }
2433
+ panX = e.offsetX - (e.offsetX - panX) * next / scale;
2434
+ panY = e.offsetY - (e.offsetY - panY) * next / scale;
2435
+ scale = next;
2436
+ }, { passive: false });
2437
+ document.getElementById("graph-reset").addEventListener("click", resetView);
2438
+ addEventListener("resize", () => {
2439
+ if (!cameraTouched)
2440
+ fitView();
2441
+ });
2442
+ var searchEl = document.getElementById("search");
2443
+ initSearchShortcut(searchEl);
2444
+ searchEl.addEventListener("input", () => {
2445
+ query = searchEl.value.trim().toLowerCase();
2446
+ });
2447
+ document.addEventListener("keydown", (e) => {
2448
+ if (e.key === "Escape" && document.activeElement === searchEl) {
2449
+ searchEl.value = "";
2450
+ query = "";
2451
+ searchEl.blur();
2452
+ return;
2453
+ }
2454
+ if (slashShouldFocus(e)) {
2455
+ e.preventDefault();
2456
+ searchEl.focus();
2457
+ }
2458
+ });
2459
+ hooks.refreshView = load;
2460
+ connectLive(() => void load(), 200);
2461
+ function seeded() {
2462
+ try {
2463
+ return JSON.parse(document.getElementById("graph-data")?.textContent || "");
2464
+ } catch {
2465
+ return;
2466
+ }
2467
+ }
2468
+ applyShape();
2469
+ fitView();
2470
+ load(seeded());
2471
+ requestAnimationFrame(frame);
2472
+ })();
2473
+ `;
2474
+
2475
+ // src/cli.ts
2476
+ var dirArg = "";
2477
+ var portArg = 0;
2478
+ var argv = process.argv.slice(2);
2479
+ for (let i = 0;i < argv.length; i++) {
2480
+ const a = argv[i];
2481
+ if (a === "-p" || a === "--port")
2482
+ portArg = Number(argv[++i]);
2483
+ else if (a === "-h" || a === "--help") {
2484
+ console.log("Usage: npx outcrop [workspace-dir] [-p|--port N] [-v|--version]");
2485
+ process.exit(0);
2486
+ } else if (a === "-v" || a === "--version") {
2487
+ console.log(JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version);
2488
+ process.exit(0);
2489
+ } else if (!a.startsWith("-"))
2490
+ dirArg = a;
2491
+ }
2492
+ var PLANNER_DIR = resolve(dirArg || process.cwd());
2493
+ if (!existsSync(PLANNER_DIR) || !statSync(PLANNER_DIR).isDirectory()) {
2494
+ console.error(`Not a directory: ${PLANNER_DIR}
2495
+ Usage: npx outcrop [workspace-dir] [-p|--port N] [-v|--version]`);
2496
+ process.exit(1);
2497
+ }
2498
+ var WORKSPACE_NAME = basename(PLANNER_DIR);
2499
+ var TASKS_DIR = join(PLANNER_DIR, "tasks");
2500
+ var EPICS_DIR = join(PLANNER_DIR, "epics");
2501
+ var PORT = portArg || Number(process.env.PORT) || 4311;
2502
+ var PRIORITIES = ["P1", "P2", "P3"];
2503
+ var PRIORITY_ORDER = { P1: 0, P2: 1, P3: 2 };
2504
+ var GIT_COMMIT_ENV = process.env.BOARD_GIT_COMMIT === "1";
2505
+ var SETTINGS_FILE = join(PLANNER_DIR, SETTINGS_FILENAME);
2506
+ var DOC_EXCLUDE = new Set([".git", ".claude", "board", "node_modules"]);
2507
+ var ROOT_DOC_ORDER = ["README.md", "PRD.md", "ROADMAP.md", "TIMELINE.md", "ARCHITECTURE.md"];
2508
+ var settings = readSettings(SETTINGS_FILE, WORKSPACE_NAME);
2509
+ var gitEnabled = () => GIT_COMMIT_ENV || settings.gitCommit;
2510
+ var QUIET = process.env.OUTCROP_QUIET === "1";
2511
+ function log(msg) {
2512
+ if (!QUIET)
2513
+ console.log(`${new Date().toTimeString().slice(0, 8)} ${msg}`);
2514
+ }
2515
+ function parseFrontmatter(raw) {
2516
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
2517
+ if (!match)
2518
+ return null;
2519
+ const fm = {};
2520
+ for (const line of match[1].split(`
2521
+ `)) {
2522
+ const idx = line.indexOf(":");
2523
+ if (idx === -1)
2524
+ continue;
2525
+ fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
2526
+ }
2527
+ return { fm, body: match[2].trim() };
2528
+ }
2529
+ function parseInlineList(value) {
2530
+ const inner = value.replace(/^\[/, "").replace(/\]$/, "").trim();
2531
+ if (!inner)
2532
+ return [];
2533
+ return inner.split(",").map((s) => s.trim()).filter(Boolean);
2534
+ }
2535
+ function fileRev(path) {
2536
+ return String(statSync(path).mtimeMs);
2537
+ }
2538
+ var workspaceVersion = 0;
2539
+ var parseCache = new Map;
2540
+ function parseCached(kind, path, parse) {
2541
+ let mtime;
2542
+ try {
2543
+ mtime = statSync(path).mtimeMs;
2544
+ } catch {
2545
+ return null;
2546
+ }
2547
+ const key = `${kind}:${path}`;
2548
+ const hit = parseCache.get(key);
2549
+ if (hit && hit.mtime === mtime)
2550
+ return hit.value;
2551
+ let value = null;
2552
+ try {
2553
+ value = parse(readFileSync2(path, "utf8"));
2554
+ } catch {}
2555
+ parseCache.set(key, { mtime, value });
2556
+ return value;
2557
+ }
2558
+ function memoByVersion(compute) {
2559
+ let version = -1;
2560
+ let value;
2561
+ return () => {
2562
+ if (version !== workspaceVersion) {
2563
+ value = compute();
2564
+ version = workspaceVersion;
2565
+ }
2566
+ return value;
2567
+ };
2568
+ }
2569
+ function parseTask(file) {
2570
+ const path = join(TASKS_DIR, file);
2571
+ return parseCached("task", path, (raw) => {
2572
+ const parsed = parseFrontmatter(raw);
2573
+ if (!parsed)
2574
+ return null;
2575
+ const { fm, body } = parsed;
2576
+ const subtasksSection = body.split(/^## /m).find((s) => s.startsWith("Subtasks")) ?? "";
2577
+ const subtasksTotal = (subtasksSection.match(/- \[[ x]\]/g) ?? []).length;
2578
+ const subtasksDone = (subtasksSection.match(/- \[x\]/g) ?? []).length;
2579
+ return {
2580
+ id: fm.id ?? file,
2581
+ title: fm.title ?? "(untitled)",
2582
+ epic: fm.epic ?? "",
2583
+ status: fm.status ?? "backlog",
2584
+ priority: fm.priority ?? "P3",
2585
+ depends_on: parseInlineList(fm.depends_on ?? ""),
2586
+ created: fm.created ?? "",
2587
+ body,
2588
+ file,
2589
+ rev: fileRev(path),
2590
+ subtasksDone,
2591
+ subtasksTotal
2592
+ };
2593
+ });
2594
+ }
2595
+ function parseEpic(file) {
2596
+ const path = join(EPICS_DIR, file);
2597
+ return parseCached("epic", path, (raw) => {
2598
+ const parsed = parseFrontmatter(raw);
2599
+ if (!parsed)
2600
+ return null;
2601
+ const { fm, body } = parsed;
2602
+ return {
2603
+ id: fm.id ?? file,
2604
+ title: fm.title ?? "(untitled)",
2605
+ milestone: fm.milestone ?? "",
2606
+ status: fm.status ?? "",
2607
+ created: fm.created ?? "",
2608
+ body,
2609
+ file,
2610
+ rev: fileRev(path)
2611
+ };
2612
+ });
2613
+ }
2614
+ var allTasks = memoByVersion(() => loadDir(TASKS_DIR, parseTask));
2615
+ var allEpics = memoByVersion(() => loadDir(EPICS_DIR, parseEpic));
2616
+ function loadDir(dir, parse) {
2617
+ let files = [];
2618
+ try {
2619
+ files = readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
2620
+ } catch {
2621
+ return [];
2622
+ }
2623
+ return files.map(parse).filter((t) => t !== null);
2624
+ }
2625
+ function findItemPath(id) {
2626
+ if (!/^[A-Za-z0-9-]+$/.test(id))
2627
+ return null;
2628
+ for (const dir of [TASKS_DIR, EPICS_DIR]) {
2629
+ try {
2630
+ for (const f of readdirSync(dir)) {
2631
+ if (f === `${id}.md` || f.startsWith(`${id}-`))
2632
+ return join(dir, f);
2633
+ }
2634
+ } catch {}
2635
+ }
2636
+ return null;
2637
+ }
2638
+ function setFrontmatterValue(raw, key, value) {
2639
+ const lines = raw.split(`
2640
+ `);
2641
+ if (lines[0] !== "---")
2642
+ return raw;
2643
+ for (let i = 1;i < lines.length; i++) {
2644
+ if (lines[i] === "---") {
2645
+ lines.splice(i, 0, `${key}: ${value}`);
2646
+ return lines.join(`
2647
+ `);
2648
+ }
2649
+ if (lines[i].startsWith(`${key}:`)) {
2650
+ lines[i] = `${key}: ${value}`;
2651
+ return lines.join(`
2652
+ `);
2653
+ }
2654
+ }
2655
+ return raw;
2656
+ }
2657
+ function getFrontmatterValue(raw, key) {
2658
+ return parseFrontmatter(raw)?.fm[key] ?? "";
2659
+ }
2660
+ function toggleCheckboxLine(raw, index, checked) {
2661
+ const m = raw.match(/^(---\n[\s\S]*?\n---\n?)([\s\S]*)$/);
2662
+ const head = m ? m[1] : "";
2663
+ const lines = (m ? m[2] : raw).split(`
2664
+ `);
2665
+ let inCode = false;
2666
+ let count = 0;
2667
+ for (let i = 0;i < lines.length; i++) {
2668
+ if (lines[i].startsWith("```")) {
2669
+ inCode = !inCode;
2670
+ continue;
2671
+ }
2672
+ if (inCode)
2673
+ continue;
2674
+ if (/^\s*- \[[ x]\] /.test(lines[i])) {
2675
+ if (count === index) {
2676
+ lines[i] = lines[i].replace(/- \[[ x]\]/, checked ? "- [x]" : "- [ ]");
2677
+ return head + lines.join(`
2678
+ `);
2679
+ }
2680
+ count++;
2681
+ }
2682
+ }
2683
+ return null;
2684
+ }
2685
+ function gitCommitFile(path, message) {
2686
+ if (!gitEnabled())
2687
+ return;
2688
+ try {
2689
+ spawnSync("git", ["-C", PLANNER_DIR, "add", path]);
2690
+ spawnSync("git", ["-C", PLANNER_DIR, "commit", "-m", message, "--", path]);
2691
+ } catch {}
2692
+ }
2693
+ function esc(s) {
2694
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2695
+ }
2696
+ var HTML_TAGS = new Set(("a b blockquote br center code del details div em h1 h2 h3 h4 h5 h6 hr i img kbd li ol p picture " + "pre s small source span strong sub summary sup table tbody td th thead tr u ul").split(" "));
2697
+ var HTML_BLOCK = new Set(("a blockquote br center details div h1 h2 h3 h4 h5 h6 hr img li ol p picture summary table tbody " + "td th thead tr ul").split(" "));
2698
+ var HTML_ATTRS = new Set("align alt colspan height href open rowspan src srcset start title type width".split(" "));
2699
+ var UNSAFE_URL = /^\s*(javascript|data|vbscript)\s*:/i;
2700
+ var RAW_TAG = /&lt;(\/?)([a-zA-Z][a-zA-Z0-9]{0,9})((?:(?!&lt;|&gt;)[\s\S])*)&gt;/g;
2701
+ var RAW_ATTR = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*&quot;((?:(?!&quot;)[\s\S])*)&quot;)?/g;
2702
+ function rawHtml(escaped, resolveSrc = (v) => v) {
2703
+ return escaped.replace(RAW_TAG, (whole, closing, name, attrs) => {
2704
+ const tag = name.toLowerCase();
2705
+ if (!HTML_TAGS.has(tag))
2706
+ return whole;
2707
+ if (closing)
2708
+ return `</${tag}>`;
2709
+ let kept = "";
2710
+ for (const [, key, value] of attrs.matchAll(RAW_ATTR)) {
2711
+ const attr = key.toLowerCase();
2712
+ if (!HTML_ATTRS.has(attr))
2713
+ continue;
2714
+ if (value === undefined) {
2715
+ kept += ` ${attr}`;
2716
+ continue;
2717
+ }
2718
+ if (/^(href|src|srcset)$/.test(attr) && UNSAFE_URL.test(value))
2719
+ continue;
2720
+ kept += ` ${attr}="${/^(href|src)$/.test(attr) ? resolveSrc(value) : value}"`;
2721
+ }
2722
+ return `<${tag}${kept}${/\/\s*$/.test(attrs) ? " /" : ""}>`;
2723
+ });
2724
+ }
2725
+ function fromDocDir(baseDir, href) {
2726
+ const stack = [];
2727
+ for (const p of (baseDir + "/" + href).split("/").filter((s) => s && s !== ".")) {
2728
+ if (p === "..")
2729
+ stack.pop();
2730
+ else
2731
+ stack.push(p);
2732
+ }
2733
+ return stack.join("/");
2734
+ }
2735
+ function mdToHtml(md, opts = {}) {
2736
+ const resolveLink = (href) => {
2737
+ if (/^https?:/.test(href))
2738
+ return { href, ext: true };
2739
+ if (href.startsWith("#") || href.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(href))
2740
+ return { href, ext: false };
2741
+ const rel = fromDocDir(opts.baseDir ?? "", href);
2742
+ if (!href.endsWith(".md"))
2743
+ return { href: "/raw/" + encodeURI(rel), ext: false };
2744
+ return { href: "/docs?file=" + encodeURIComponent(rel), ext: false };
2745
+ };
2746
+ const rawSrc = (value) => resolveLink(value).href;
2747
+ const refTag = (id) => opts.docMode ? `<a class="ref" href="/#${id}">${id}</a>` : `<button type="button" data-open="${id}" class="ref">${id}</button>`;
2748
+ const inline = (s) => {
2749
+ const spans = [];
2750
+ return s.replace(/`([^`]+)`/g, (_, c) => `${spans.push(c) - 1}`).replace(RAW_TAG, (m) => rawHtml(m, rawSrc)).replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_, alt, src) => {
2751
+ const r = resolveLink(src);
2752
+ return `<img src="${r.href}" alt="${alt}" class="max-w-full rounded-md">`;
2753
+ }).replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, text, href) => {
2754
+ const r = resolveLink(href);
2755
+ return `<a href="${r.href}"${r.ext ? ' target="_blank" rel="noopener"' : ""} class="mdlink">${text}</a>`;
2756
+ }).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>").replace(/(^|[\s(:,])(ST-\d{3}|E\d{2})\b/g, (_, pre, id) => pre + refTag(id)).replace(/\ue000(\d+)\ue000/g, (_, i) => `<code>${spans[+i]}</code>`);
2757
+ };
2758
+ const lines = esc(md).split(`
2759
+ `);
2760
+ const out = [];
2761
+ let inCode = false;
2762
+ let codeLang = "";
2763
+ let inList = false;
2764
+ let cbIndex = 0;
2765
+ let tableBuf = [];
2766
+ const closeList = () => {
2767
+ if (inList) {
2768
+ out.push("</ul>");
2769
+ inList = false;
2770
+ }
2771
+ };
2772
+ const flushTable = () => {
2773
+ if (!tableBuf.length)
2774
+ return;
2775
+ const buf = tableBuf;
2776
+ tableBuf = [];
2777
+ const parseRow = (l) => l.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => inline(c.trim()));
2778
+ if (buf.length >= 2 && /^\s*\|[\s:|-]+\|?\s*$/.test(buf[1])) {
2779
+ const head = parseRow(buf[0]);
2780
+ const rows = buf.slice(2).map(parseRow);
2781
+ out.push("<table><thead><tr>" + head.map((h) => `<th>${h}</th>`).join("") + "</tr></thead><tbody>" + rows.map((r) => "<tr>" + r.map((c) => `<td>${c}</td>`).join("") + "</tr>").join("") + "</tbody></table>");
2782
+ } else {
2783
+ for (const l of buf)
2784
+ out.push(`<p>${inline(l)}</p>`);
2785
+ }
2786
+ };
2787
+ for (const line of lines) {
2788
+ if (line.startsWith("```")) {
2789
+ flushTable();
2790
+ closeList();
2791
+ if (!inCode) {
2792
+ codeLang = line.slice(3).trim();
2793
+ inCode = true;
2794
+ out.push(opts.docMode && codeLang === "mermaid" ? '<div class="mermaid">' : "<pre>");
2795
+ } else {
2796
+ out.push(opts.docMode && codeLang === "mermaid" ? "</div>" : "</pre>");
2797
+ inCode = false;
2798
+ }
2799
+ continue;
2800
+ }
2801
+ if (inCode) {
2802
+ out.push(line);
2803
+ continue;
2804
+ }
2805
+ if (/^\s*\|/.test(line)) {
2806
+ closeList();
2807
+ tableBuf.push(line);
2808
+ continue;
2809
+ }
2810
+ flushTable();
2811
+ const rawBlock = line.match(/^\s*&lt;\/?([a-zA-Z][a-zA-Z0-9]{0,9})/);
2812
+ if (rawBlock && HTML_BLOCK.has(rawBlock[1].toLowerCase())) {
2813
+ closeList();
2814
+ out.push(rawHtml(line, rawSrc));
2815
+ continue;
2816
+ }
2817
+ if (/^#{1,6} /.test(line)) {
2818
+ closeList();
2819
+ const level = Math.min(line.match(/^#+/)[0].length, 4);
2820
+ const text = line.replace(/^#+ /, "");
2821
+ const slug = text.toLowerCase().replace(/&[a-z]+;/g, "").replace(/`/g, "").replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-");
2822
+ out.push(`<h${level}${opts.docMode && slug ? ` id="${slug}"` : ""}>${inline(text)}</h${level}>`);
2823
+ } else if (/^\s*-{3,}\s*$/.test(line)) {
2824
+ closeList();
2825
+ out.push("<hr>");
2826
+ } else if (/^\s*- \[[ x]\] /.test(line)) {
2827
+ if (!inList) {
2828
+ out.push("<ul>");
2829
+ inList = true;
2830
+ }
2831
+ const checked = /- \[x\]/.test(line);
2832
+ out.push(`<li class="check${checked ? " done" : ""}"><button type="button" data-cb="${cbIndex}" class="box${checked ? " checked" : ""}" title="Toggle" aria-label="Toggle subtask">${checked ? "&#10003;" : ""}</button><span>${inline(line.replace(/^\s*- \[[ x]\] /, ""))}</span></li>`);
2833
+ cbIndex++;
2834
+ } else if (/^\s*[-*] /.test(line)) {
2835
+ if (!inList) {
2836
+ out.push("<ul>");
2837
+ inList = true;
2838
+ }
2839
+ out.push(`<li>${inline(line.replace(/^\s*[-*] /, ""))}</li>`);
2840
+ } else if (/^\s*\d+\. /.test(line)) {
2841
+ if (!inList) {
2842
+ out.push("<ul class='ordered'>");
2843
+ inList = true;
2844
+ }
2845
+ out.push(`<li>${inline(line)}</li>`);
2846
+ } else if (line.trim() === "") {
2847
+ closeList();
2848
+ } else {
2849
+ closeList();
2850
+ out.push(`<p>${inline(line)}</p>`);
2851
+ }
2852
+ }
2853
+ flushTable();
2854
+ closeList();
2855
+ if (inCode)
2856
+ out.push("</pre>");
2857
+ return out.join(`
2858
+ `);
2859
+ }
2860
+ var STATUS_COLOR = {
2861
+ backlog: "#a1a1aa",
2862
+ ready: "#71717a",
2863
+ "in-progress": "#eab308",
2864
+ "in-review": "#8b5cf6",
2865
+ done: "#22c55e",
2866
+ blocked: "#ef4444"
2867
+ };
2868
+ function statusIcon(status, size = 14) {
2869
+ const c = STATUS_COLOR[status] ?? "#a1a1aa";
2870
+ const base = `width="${size}" height="${size}" viewBox="0 0 14 14" fill="none" aria-hidden="true" class="shrink-0"`;
2871
+ switch (status) {
2872
+ case "backlog":
2873
+ return `<svg ${base}><circle cx="7" cy="7" r="5.5" stroke="${c}" stroke-width="1.6" stroke-dasharray="2.2 2.4"/></svg>`;
2874
+ case "ready":
2875
+ return `<svg ${base}><circle cx="7" cy="7" r="5.5" stroke="${c}" stroke-width="1.6"/></svg>`;
2876
+ case "in-progress":
2877
+ return `<svg ${base}><circle cx="7" cy="7" r="5.5" stroke="${c}" stroke-width="1.6"/><path d="M7 3.4 A3.6 3.6 0 0 1 7 10.6 Z" fill="${c}"/></svg>`;
2878
+ case "in-review":
2879
+ return `<svg ${base}><circle cx="7" cy="7" r="5.5" stroke="${c}" stroke-width="1.6"/><circle cx="7" cy="7" r="2.4" fill="${c}"/></svg>`;
2880
+ case "done":
2881
+ return `<svg ${base}><circle cx="7" cy="7" r="6" fill="${c}"/><path d="M4.4 7.2 L6.2 9 L9.6 5.4" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
2882
+ case "blocked":
2883
+ return `<svg ${base}><circle cx="7" cy="7" r="5.5" stroke="${c}" stroke-width="1.6"/><path d="M3.5 10.5 L10.5 3.5" stroke="${c}" stroke-width="1.6" stroke-linecap="round"/></svg>`;
2884
+ default:
2885
+ return `<svg ${base}><circle cx="7" cy="7" r="5.5" stroke="${c}" stroke-width="1.6"/></svg>`;
2886
+ }
2887
+ }
2888
+ function progressRing(done, total, size = 12) {
2889
+ const dash = total > 0 ? 28.9 * done / total : 0;
2890
+ return `<svg width="${size}" height="${size}" viewBox="0 0 12 12" aria-hidden="true" class="shrink-0"><circle cx="6" cy="6" r="4.6" stroke="currentColor" stroke-width="1.4" opacity="0.25" fill="none"/><circle cx="6" cy="6" r="4.6" stroke="${done === total && total > 0 ? "#22c55e" : "currentColor"}" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-dasharray="${dash.toFixed(1)} 28.9" transform="rotate(-90 6 6)"/></svg>`;
2891
+ }
2892
+ function priorityIcon(priority) {
2893
+ const filled = priority === "P1" ? 3 : priority === "P2" ? 2 : 1;
2894
+ const active = priority === "P1" ? "#f97316" : "currentColor";
2895
+ const bars = [
2896
+ { x: 1, y: 8, h: 4 },
2897
+ { x: 5.5, y: 5, h: 7 },
2898
+ { x: 10, y: 2, h: 10 }
2899
+ ].map((b, i) => `<rect x="${b.x}" y="${b.y}" width="3" height="${b.h}" rx="1" fill="${i < filled ? active : "currentColor"}" opacity="${i < filled ? 1 : 0.25}"/>`).join("");
2900
+ return `<span class="prio" title="Priority ${priority}"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">${bars}</svg><span class="prio-label">${esc(priority)}</span></span>`;
2901
+ }
2902
+ function depChip(id, done) {
2903
+ return `<button type="button" data-open="${esc(id)}" class="dep-chip${done ? " done" : ""}" title="depends on ${esc(id)}${done ? " (done)" : " (not done)"}: click to open">${done ? "&#10003; " : "&#8598; "}${esc(id)}</button>`;
2904
+ }
2905
+ function normalizeRel(baseDir, href) {
2906
+ const parts = (baseDir + "/" + href).split("/").filter((p) => p && p !== ".");
2907
+ const stack = [];
2908
+ for (const p of parts) {
2909
+ if (p === "..")
2910
+ stack.pop();
2911
+ else
2912
+ stack.push(p);
2913
+ }
2914
+ return stack.join("/");
2915
+ }
2916
+ var buildBacklinks = memoByVersion(() => {
2917
+ const map = new Map;
2918
+ for (const g of listDocs()) {
2919
+ for (const rel of g.files) {
2920
+ const parsed = parseCached("fm", join(PLANNER_DIR, ...rel.split("/")), (raw) => parseFrontmatter(raw) ?? { fm: {}, body: raw });
2921
+ if (!parsed)
2922
+ continue;
2923
+ const { fm, body } = parsed;
2924
+ const deps = new Set(parseInlineList(fm.depends_on ?? ""));
2925
+ const targets = new Set;
2926
+ for (const m of body.matchAll(/\b(ST-\d{3}|E\d{2})\b/g))
2927
+ targets.add(m[1]);
2928
+ if (fm.epic)
2929
+ targets.add(fm.epic);
2930
+ for (const d of deps)
2931
+ targets.add(d);
2932
+ const baseDir = dirname(rel) === "." ? "" : dirname(rel);
2933
+ for (const m of body.matchAll(/\]\(([^)\s]+\.md)\)/g)) {
2934
+ if (!/^https?:/.test(m[1]))
2935
+ targets.add(normalizeRel(baseDir, m[1]));
2936
+ }
2937
+ targets.delete(fm.id ?? "");
2938
+ targets.delete(rel);
2939
+ for (const t of targets) {
2940
+ const arr = map.get(t) ?? [];
2941
+ arr.push({ file: rel, id: fm.id, title: fm.title ?? basename(rel), isDep: deps.has(t) });
2942
+ map.set(t, arr);
2943
+ }
2944
+ }
2945
+ }
2946
+ return map;
2947
+ });
2948
+ var buildGraph = memoByVersion(() => {
2949
+ const nodes = new Map;
2950
+ const byItemId = new Map;
2951
+ for (const g of listDocs()) {
2952
+ for (const rel of g.files) {
2953
+ const parsed = parseCached("fm", join(PLANNER_DIR, ...rel.split("/")), (raw) => parseFrontmatter(raw) ?? { fm: {}, body: raw });
2954
+ const fm = parsed?.fm ?? {};
2955
+ nodes.set(rel, { id: rel, label: fm.title || basename(rel).replace(/\.md$/i, ""), group: g.group, deg: 0 });
2956
+ if (fm.id)
2957
+ byItemId.set(fm.id, rel);
2958
+ }
2959
+ }
2960
+ const links = [];
2961
+ const seen = new Set;
2962
+ for (const [target, sources] of buildBacklinks()) {
2963
+ const to = nodes.has(target) ? target : byItemId.get(target);
2964
+ if (!to)
2965
+ continue;
2966
+ for (const s of sources) {
2967
+ if (s.file === to || !nodes.has(s.file))
2968
+ continue;
2969
+ const key = s.file + "\x00" + to;
2970
+ if (seen.has(key))
2971
+ continue;
2972
+ seen.add(key);
2973
+ links.push({ source: s.file, target: to });
2974
+ nodes.get(s.file).deg++;
2975
+ nodes.get(to).deg++;
2976
+ }
2977
+ }
2978
+ return { nodes: [...nodes.values()], links };
2979
+ });
2980
+ function backlinksHtml(sources, heading, docMode) {
2981
+ if (!sources.length)
2982
+ return "";
2983
+ const rowClass = "backlink-row";
2984
+ const rows = sources.map((s) => {
2985
+ const idPart = s.id ? `<span class="font-mono text-[11px] text-zinc-400 dark:text-zinc-500">${esc(s.id)}</span>` : `<span class="font-mono text-[10.5px] text-zinc-400 dark:text-zinc-500">${esc(s.file)}</span>`;
2986
+ const badge = s.isDep ? `<span class="ml-auto shrink-0 rounded bg-amber-50 px-1.5 py-px text-[9.5px] font-medium uppercase tracking-wide text-amber-600 dark:bg-amber-500/10 dark:text-amber-400">depends on this</span>` : "";
2987
+ const inner = `${idPart}<span class="truncate text-[12.5px] text-zinc-700 dark:text-zinc-300">${esc(s.title)}</span>${badge}`;
2988
+ if (s.id && !docMode)
2989
+ return `<button type="button" data-open="${esc(s.id)}" class="${rowClass}">${inner}</button>`;
2990
+ const href = s.id ? `/#${esc(s.id)}` : `/docs?file=${encodeURIComponent(s.file)}`;
2991
+ return `<a href="${href}" class="${rowClass}">${inner}</a>`;
2992
+ }).join("");
2993
+ return `<div class="mt-6 border-t border-zinc-100 pt-4 dark:border-zinc-800">
2994
+ <div class="mb-2 text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">${heading} <span class="font-mono text-[10px] normal-case text-zinc-300 dark:text-zinc-600">${sources.length}</span></div>
2995
+ <div class="space-y-1.5">${rows}</div>
2996
+ </div>`;
2997
+ }
2998
+ function cardHtml(t, byId) {
2999
+ const deps = t.depends_on.map((d) => depChip(d, byId.get(d)?.status === "done")).join("");
3000
+ const progress = t.subtasksTotal > 0 ? `<span class="card-progress" title="${t.subtasksDone} of ${t.subtasksTotal} subtasks done">
3001
+ ${progressRing(t.subtasksDone, t.subtasksTotal)}
3002
+ ${t.subtasksDone}/${t.subtasksTotal}</span>` : "";
3003
+ const search = esc(`${t.id} ${t.title} ${t.epic} ${t.body}`.toLowerCase().replace(/\s+/g, " ").slice(0, 2000));
3004
+ return `<div role="button" tabindex="0" draggable="true" data-task-id="${esc(t.id)}" data-epic="${esc(t.epic)}" data-status="${esc(t.status)}" data-rev="${t.rev}" data-search="${search}"
3005
+ class="task-card">
3006
+ <div class="mb-1 flex items-center justify-between gap-2">
3007
+ <span class="card-id">${esc(t.id)}</span>
3008
+ ${priorityIcon(t.priority)}
3009
+ </div>
3010
+ <div class="card-title">${esc(t.title)}</div>
3011
+ <div class="mt-2 flex flex-wrap items-center gap-1.5">
3012
+ <button type="button" data-open="${esc(t.epic)}" class="epic-chip" title="open epic ${esc(t.epic)}">${esc(t.epic)}</button>
3013
+ ${deps}${progress}
3014
+ </div>
3015
+ </div>`;
3016
+ }
3017
+ function statusSelect(current) {
3018
+ const options = [...new Set([...settings.columns, current])].map((s) => `<option value="${esc(s)}" ${s === current ? "selected" : ""}>${esc(s.replace("-", " "))}</option>`).join("");
3019
+ return `<select data-status-select aria-label="Status"
3020
+ class="cursor-pointer rounded border border-zinc-200 bg-transparent px-1 py-0.5 text-[12px] capitalize text-zinc-600 hover:border-zinc-300 focus:outline-none dark:border-zinc-700 dark:text-zinc-300 dark:hover:border-zinc-600 dark:[&>option]:bg-zinc-900">${options}</select>`;
3021
+ }
3022
+ function taskDetailHtml(t, byId, backlinks) {
3023
+ const deps = t.depends_on.length ? t.depends_on.map((d) => depChip(d, byId.get(d)?.status === "done")).join(" ") : '<span class="text-zinc-400 dark:text-zinc-500">none</span>';
3024
+ return `<div class="detail-root" data-id="${esc(t.id)}" data-rev="${t.rev}">
3025
+ <div class="flex items-center gap-2 text-[12px] text-zinc-500 dark:text-zinc-400">
3026
+ ${statusIcon(t.status)}${statusSelect(t.status)}
3027
+ <span class="font-mono text-[11px]">${esc(t.id)}</span>
3028
+ <span class="ml-auto mr-16">${priorityIcon(t.priority)}</span>
3029
+ </div>
3030
+ <h2 class="mt-2 text-[16px] font-semibold leading-snug text-zinc-900 dark:text-zinc-50">${esc(t.title)}</h2>
3031
+ <dl class="mt-4 grid grid-cols-[90px_1fr] items-baseline gap-y-1.5 border-y border-zinc-100 py-3 text-[12px] dark:border-zinc-800">
3032
+ <dt class="text-zinc-400 dark:text-zinc-500">Epic</dt><dd><button type="button" data-open="${esc(t.epic)}" class="font-mono text-accent-600 hover:underline dark:text-accent-400">${esc(t.epic)}</button></dd>
3033
+ <dt class="text-zinc-400 dark:text-zinc-500">Depends on</dt><dd class="flex flex-wrap gap-1">${deps}</dd>
3034
+ <dt class="text-zinc-400 dark:text-zinc-500">Created</dt><dd class="text-zinc-700 dark:text-zinc-300">${esc(t.created)}</dd>
3035
+ <dt class="text-zinc-400 dark:text-zinc-500">File</dt><dd class="font-mono text-[11px] text-zinc-500 dark:text-zinc-400"><a class="hover:underline" href="/docs?file=tasks/${esc(t.file)}">tasks/${esc(t.file)}</a></dd>
3036
+ </dl>
3037
+ <div class="detail-body mt-4">${mdToHtml(t.body)}</div>
3038
+ ${backlinksHtml(backlinks.get(t.id) ?? [], "Referenced by", false)}
3039
+ </div>`;
3040
+ }
3041
+ function epicDetailHtml(e, tasks, backlinks) {
3042
+ const stories = tasks.filter((t) => t.epic === e.id);
3043
+ const storyList = stories.length ? stories.map((t) => `<button type="button" data-open="${esc(t.id)}" class="backlink-row">
3044
+ ${statusIcon(t.status, 12)}
3045
+ <span class="font-mono text-[11px] text-zinc-400 dark:text-zinc-500">${esc(t.id)}</span>
3046
+ <span class="truncate text-[12.5px] text-zinc-700 dark:text-zinc-300">${esc(t.title)}</span>
3047
+ </button>`).join("") : '<p class="text-[12px] text-zinc-400">No stories yet.</p>';
3048
+ const done = stories.filter((t) => t.status === "done").length;
3049
+ return `<div class="detail-root" data-id="${esc(e.id)}" data-rev="${e.rev}">
3050
+ <div class="flex items-center gap-2 text-[12px] text-zinc-500 dark:text-zinc-400">
3051
+ <span class="rounded bg-zinc-100 px-1.5 py-px text-[10px] font-medium uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">Epic</span>
3052
+ <span class="font-mono text-[11px]">${esc(e.id)}</span>
3053
+ <span class="text-zinc-300 dark:text-zinc-600">&middot;</span>
3054
+ <span>${esc(e.milestone)}</span>
3055
+ <span class="ml-auto mr-16 inline-flex items-center gap-1.5 text-zinc-400 dark:text-zinc-500">${progressRing(done, stories.length)}<span class="font-mono text-[11px]">${done}/${stories.length} done</span></span>
3056
+ </div>
3057
+ <h2 class="mt-2 text-[16px] font-semibold leading-snug text-zinc-900 dark:text-zinc-50">${esc(e.title)}</h2>
3058
+ <div class="mt-4 space-y-1.5">${storyList}</div>
3059
+ <div class="detail-body mt-5 border-t border-zinc-100 pt-4 dark:border-zinc-800">${mdToHtml(e.body)}</div>
3060
+ ${backlinksHtml((backlinks.get(e.id) ?? []).filter((s) => !stories.some((t) => t.id === s.id)), "Referenced by", false)}
3061
+ </div>`;
3062
+ }
3063
+ function itemPaneHtml(id) {
3064
+ const tasks = allTasks();
3065
+ const backlinks = buildBacklinks();
3066
+ const task = tasks.find((t) => t.id === id);
3067
+ if (task)
3068
+ return taskDetailHtml(task, new Map(tasks.map((t) => [t.id, t])), backlinks);
3069
+ const epic = allEpics().find((e) => e.id === id);
3070
+ return epic ? epicDetailHtml(epic, tasks, backlinks) : null;
3071
+ }
3072
+ function sidebarHtml(epics, tasks) {
3073
+ const rows = epics.sort((a, b) => a.id.localeCompare(b.id)).map((e) => {
3074
+ const stories = tasks.filter((t) => t.epic === e.id);
3075
+ const done = stories.filter((t) => t.status === "done").length;
3076
+ return `<div role="button" tabindex="0" data-filter-epic="${esc(e.id)}" class="epic-row group" title="${esc(e.title)}: click to filter the board">
3077
+ <span class="text-zinc-400 dark:text-zinc-500">${progressRing(done, stories.length)}</span>
3078
+ <span class="min-w-0 flex-1 truncate text-[12.5px] text-zinc-700 dark:text-zinc-300"><span class="font-mono text-[10.5px] text-zinc-400 dark:text-zinc-500">${esc(e.id)}</span> ${esc(e.title)}</span>
3079
+ <span class="font-mono text-[10px] text-zinc-400 dark:text-zinc-600">${done}/${stories.length}</span>
3080
+ <button type="button" data-open="${esc(e.id)}" aria-label="Open epic ${esc(e.id)}"
3081
+ class="rounded p-0.5 text-zinc-300 opacity-0 transition-opacity hover:bg-zinc-200 hover:text-zinc-600 group-hover:opacity-100 focus-visible:opacity-100 dark:text-zinc-600 dark:hover:bg-zinc-700 dark:hover:text-zinc-300">
3082
+ <svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M4.5 2.5 H9.5 V7.5 M9.5 2.5 L2.5 9.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
3083
+ </button>
3084
+ </div>`;
3085
+ }).join("");
3086
+ return `<aside id="sidebar" class="hidden w-60 shrink-0 flex-col gap-0.5 overflow-y-auto border-r border-zinc-200 bg-white px-2.5 py-3 dark:border-zinc-800 dark:bg-zinc-950 md:flex">
3087
+ <div class="mb-1 px-2 text-[10.5px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">Epics</div>
3088
+ <div role="button" tabindex="0" data-filter-epic=""
3089
+ class="epic-row epic-active">
3090
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true" class="text-zinc-400"><rect x="1" y="1" width="4.4" height="4.4" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="6.6" y="1" width="4.4" height="4.4" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="1" y="6.6" width="4.4" height="4.4" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="6.6" y="6.6" width="4.4" height="4.4" rx="1" stroke="currentColor" stroke-width="1.2"/></svg>
3091
+ <span class="flex-1 text-[12.5px] text-zinc-700 dark:text-zinc-300">All epics</span>
3092
+ <span class="font-mono text-[10px] text-zinc-400 dark:text-zinc-600">${tasks.length}</span>
3093
+ </div>
3094
+ ${rows}
3095
+ </aside>`;
3096
+ }
3097
+ function boardColumns(tasks) {
3098
+ const used = new Set(tasks.map((t) => t.status));
3099
+ return [...new Set([...settings.columns.filter((c) => c !== "blocked" || used.has("blocked")), ...used])];
3100
+ }
3101
+ function settingsPanelHtml() {
3102
+ const field = "w-full rounded-md border border-zinc-200 bg-white px-2 py-1 text-[12.5px] outline-none focus:border-accent-400 dark:border-zinc-700 dark:bg-zinc-900 dark:focus:border-accent-500";
3103
+ const hint = "mt-1 text-[11px] leading-relaxed text-zinc-400 dark:text-zinc-500";
3104
+ const row = (name, control, note = "") => `<div class="set-row"><span class="set-label">${name}</span><div class="min-w-0 flex-1">${control}${note ? `<p class="${hint}">${note}</p>` : ""}</div></div>`;
3105
+ const group = (title, sub, body, open = false) => `<details class="set-group" name="settings-group"${open ? " open" : ""}>
3106
+ <summary>${title}<span class="set-sub">${sub}</span></summary>
3107
+ <div class="pb-3">${body}</div>
3108
+ </details>`;
3109
+ const appearance = row("Theme", `<div class="flex gap-1">${["light", "dark"].map((t) => `<button type="button" data-set-theme="${t}"
3110
+ class="theme-opt pick capitalize">${t}</button>`).join("")}</div>`) + row("Accent", `<div class="flex flex-wrap gap-1.5">${[
3111
+ ["violet", "swatch swatch-violet"],
3112
+ ["blue", "swatch swatch-blue"],
3113
+ ["emerald", "swatch swatch-emerald"],
3114
+ ["amber", "swatch swatch-amber"],
3115
+ ["rose", "swatch swatch-rose"]
3116
+ ].map(([id, cls]) => `<button type="button" data-set-appearance="accent" data-value="${id}" title="${id}" class="${cls}" aria-label="Accent ${id}"></button>`).join("")}</div>`) + row("Typeface", `<div class="flex gap-1">${[
3117
+ ["sans", "Sans", "pick font-sans"],
3118
+ ["serif", "Serif", "pick font-serif"],
3119
+ ["mono", "Mono", "pick font-mono"]
3120
+ ].map(([v, name, cls]) => `<button type="button" data-set-appearance="font" data-value="${v}" class="${cls}">${name}</button>`).join("")}</div>`) + row("Size", `<div class="flex gap-1">${[
3121
+ ["compact", "Compact"],
3122
+ ["default", "Default"],
3123
+ ["large", "Large"]
3124
+ ].map(([v, name]) => `<button type="button" data-set-appearance="size" data-value="${v}" class="pick">${name}</button>`).join("")}</div>`, "Stored in this browser, not in the workspace.");
3125
+ const board = row("Title", `<input id="set-title" name="title" class="${field}" value="${esc(settings.title)}" placeholder="${esc(WORKSPACE_NAME)}">`) + row("Views", `<div class="space-y-1">${[
3126
+ ["board", "Board", "Kanban of tasks/"],
3127
+ ["docs", "Docs", "Markdown viewer and search"],
3128
+ ["graph", "Graph", "Force-directed map of links"]
3129
+ ].map(([v, name, why]) => `
3130
+ <label class="flex items-center gap-2 text-[12.5px] text-zinc-700 dark:text-zinc-300">
3131
+ <input type="checkbox" name="view-${v}" class="h-3.5 w-3.5 accent-accent-600" ${settings.views.includes(v) ? "checked" : ""}>
3132
+ ${name} <span class="text-[11px] text-zinc-400 dark:text-zinc-500">${why}</span>
3133
+ </label>`).join("")}</div>`, "Turn off whatever this workspace does not need. At least one has to stay on.") + row("Columns", `<input id="set-columns" name="columns" class="${field}" value="${esc(settings.columns.join(", "))}">`, "Comma separated, in board order. A status used by a task always gets a column even if listed nowhere here, so nothing goes missing.");
3134
+ const git = row("Commits", `<label class="flex items-center gap-2 text-[12.5px] text-zinc-700 dark:text-zinc-300">
3135
+ <input type="checkbox" name="gitCommit" class="h-3.5 w-3.5 accent-accent-600" ${settings.gitCommit ? "checked" : ""} ${GIT_COMMIT_ENV ? "disabled" : ""}>
3136
+ Commit every board mutation
3137
+ </label>`, GIT_COMMIT_ENV ? 'Forced on by <code class="font-mono">BOARD_GIT_COMMIT=1</code>.' : "Each status change or subtask toggle becomes its own commit.");
3138
+ return `<template id="detail-settings"><div class="detail-root" data-id="settings" data-views="${settings.views.join(",")}">
3139
+ <h2 class="text-[16px] font-semibold leading-snug text-zinc-900 dark:text-zinc-50">Settings</h2>
3140
+ <p class="${hint}">Workspace settings live in <code class="font-mono">${SETTINGS_FILENAME}</code> and are meant to be committed. Appearance is per-browser.</p>
3141
+
3142
+ <div class="mt-4 border-t border-zinc-100 dark:border-zinc-800">
3143
+ ${group("Appearance", "Theme, accent, typeface, size", appearance, true)}
3144
+ ${group("Board", "Title, views, columns", board)}
3145
+ ${group("Git", GIT_COMMIT_ENV || settings.gitCommit ? "Auto-commit on" : "Auto-commit off", git)}
3146
+ </div>
3147
+
3148
+ <p id="settings-error" class="mt-4 hidden rounded-md border border-rose-300 bg-rose-50 px-2.5 py-1.5 text-[12px] text-rose-700 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-300" role="alert"></p>
3149
+
3150
+ <div class="mt-4 flex items-center gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
3151
+ <button id="settings-save" type="button"
3152
+ class="rounded-md bg-accent-600 px-3 py-1.5 text-[12.5px] font-medium text-white transition-colors hover:bg-accent-500">Save</button>
3153
+ <span class="font-mono text-[10.5px] text-zinc-400 dark:text-zinc-600">${esc(SETTINGS_FILENAME)}</span>
3154
+ </div>
3155
+ </div></template>`;
3156
+ }
3157
+ function appHtml(tasks, epics) {
3158
+ const byId = new Map(tasks.map((t) => [t.id, t]));
3159
+ const columns = boardColumns(tasks).map((col) => {
3160
+ const cards = tasks.filter((t) => t.status === col).sort((a, b) => (PRIORITY_ORDER[a.priority] ?? 9) - (PRIORITY_ORDER[b.priority] ?? 9) || a.id.localeCompare(b.id));
3161
+ return `<section data-col="${esc(col)}" class="flex w-[290px] shrink-0 flex-col rounded-lg p-1 transition-colors">
3162
+ <header class="mb-2.5 flex items-center gap-2 px-1 pt-1">
3163
+ ${statusIcon(col)}
3164
+ <h2 class="text-[13px] font-medium capitalize text-zinc-700 dark:text-zinc-300">${esc(col.replace("-", " "))}</h2>
3165
+ <span class="col-count font-mono text-[11px] text-zinc-400 dark:text-zinc-600">${cards.length}</span>
3166
+ </header>
3167
+ <div class="flex min-h-[60px] flex-1 flex-col gap-2">
3168
+ ${cards.map((t) => cardHtml(t, byId)).join(`
3169
+ `) || '<div class="rounded-md border border-dashed border-zinc-200 py-6 text-center text-[12px] text-zinc-300 dark:border-zinc-800 dark:text-zinc-700">No tasks</div>'}
3170
+ </div>
3171
+ </section>`;
3172
+ }).join(`
3173
+ `);
3174
+ return `${sidebarHtml(epics, tasks)}
3175
+ ${RESIZER_HTML}
3176
+ <main class="flex flex-1 gap-3 overflow-x-auto px-4 py-3">
3177
+ ${columns}
3178
+ </main>
3179
+ ${settingsPanelHtml()}
3180
+ ${newItemPanelHtml()}`;
3181
+ }
3182
+ function headerMetaHtml(tasks) {
3183
+ const reviewCount = tasks.filter((t) => t.status === "in-review").length;
3184
+ const reviewChip = reviewCount > 0 ? `<span class="inline-flex items-center gap-1.5 rounded-full bg-accent-50 px-2.5 py-0.5 text-[11px] font-medium text-accent-600 dark:bg-accent-500/10 dark:text-accent-400">${statusIcon("in-review", 11)}${reviewCount} waiting for your review</span>` : "";
3185
+ return `${reviewChip}<span class="text-[11px] text-zinc-400 dark:text-zinc-600">${tasks.length} tasks</span>`;
3186
+ }
3187
+ var VIEW_HREF = { board: "/", docs: "/docs", graph: "/graph" };
3188
+ var VIEW_LABEL = { board: "Board", docs: "Docs", graph: "Graph" };
3189
+ function navTabs(active) {
3190
+ const tab = (on) => on ? "rounded-md bg-zinc-100 px-2.5 py-1 text-[12px] font-medium text-zinc-900 dark:bg-zinc-800 dark:text-zinc-100" : "rounded-md px-2.5 py-1 text-[12px] text-zinc-500 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100";
3191
+ if (settings.views.length < 2)
3192
+ return "";
3193
+ return `<nav class="ml-3 flex items-center gap-1">${settings.views.map((v) => `<a href="${VIEW_HREF[v]}" class="${tab(active === v)}">${VIEW_LABEL[v]}</a>`).join("")}</nav>`;
3194
+ }
3195
+ function firstEnabledView() {
3196
+ return VIEW_HREF[settings.views[0] ?? "board"];
3197
+ }
3198
+ var ICON_BUTTON = "rounded p-1 text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300";
3199
+ function headerHtml(active, meta, showSidebarToggle) {
3200
+ return `<header class="flex h-12 items-center gap-3 border-b border-zinc-200 bg-white px-4 dark:border-zinc-800 dark:bg-zinc-950">
3201
+ ${showSidebarToggle ? `<button id="sidebar-toggle" type="button" aria-label="Toggle sidebar" title="Toggle sidebar"
3202
+ class="hidden ${ICON_BUTTON} md:block">
3203
+ <svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><rect x="1.5" y="2.5" width="13" height="11" rx="2" stroke="currentColor" stroke-width="1.4"/><path d="M6 2.5 V13.5" stroke="currentColor" stroke-width="1.4"/></svg>
3204
+ </button>` : ""}
3205
+ <span class="flex items-center gap-2 text-[13px] font-semibold tracking-tight">
3206
+ <img src="/logo.svg" width="17" height="17" alt="" class="shrink-0 dark:brightness-125">${esc(settings.title)}
3207
+ </span>
3208
+ ${navTabs(active)}
3209
+ <span id="live-dot" class="h-1.5 w-1.5 rounded-full bg-emerald-500" title="Live: file changes appear automatically"></span>
3210
+ <div class="ml-auto flex items-center gap-2.5">
3211
+ <input id="search" type="search" placeholder="Search" autocomplete="off" spellcheck="false"
3212
+ class="hidden w-44 rounded-md border border-zinc-200 bg-zinc-50 px-2.5 py-1 text-[12px] outline-none transition-colors placeholder:text-zinc-400 focus:border-accent-400 dark:border-zinc-800 dark:bg-zinc-900 dark:placeholder:text-zinc-600 dark:focus:border-accent-500 sm:block">
3213
+ <div id="header-meta" class="flex items-center gap-2.5">${meta}</div>
3214
+ <button type="button" data-open="new" aria-label="New item" title="New epic, story or document" class="${ICON_BUTTON}">
3215
+ <svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 3.5 V12.5 M3.5 8 H12.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
3216
+ </button>
3217
+ <button id="theme-toggle" type="button" aria-label="Change theme" class="${ICON_BUTTON}"></button>
3218
+ <button type="button" data-open="settings" aria-label="Settings" title="Settings" class="${ICON_BUTTON}">
3219
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
3220
+ </button>
3221
+ </div>
3222
+ </header>`;
3223
+ }
3224
+ var STYLE_HASH = createHash("sha1").update(styles_gen_default).digest("base64url").slice(0, 12);
3225
+ var HEAD_LINKS = `<link rel="stylesheet" href="/styles.css?v=${STYLE_HASH}">` + `<link rel="icon" href="/logo.svg">`;
3226
+ var SHEET_HTML = `<div id="backdrop" class="fixed inset-0 z-40 hidden bg-black/20 dark:bg-black/50"></div>
3227
+ <aside id="sheet" tabindex="-1" role="dialog" aria-modal="true" aria-label="Details"
3228
+ class="fixed inset-y-0 right-0 z-50 w-full max-w-[460px] translate-x-full overflow-y-auto border-l border-zinc-200 bg-white p-6 shadow-xl outline-none motion-safe:transition-transform motion-safe:duration-200 dark:border-zinc-800 dark:bg-zinc-900">
3229
+ <div class="absolute right-4 top-4 flex items-center gap-0.5">
3230
+ <button id="sheet-expand" type="button" aria-label="Toggle fullscreen" title="Toggle fullscreen" class="${ICON_BUTTON}">
3231
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M8.5 1.5 H12.5 V5.5 M12.5 1.5 L8 6 M5.5 12.5 H1.5 V8.5 M1.5 12.5 L6 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
3232
+ </button>
3233
+ <button id="sheet-close" type="button" aria-label="Close" title="Close (Esc)" class="${ICON_BUTTON}">
3234
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M3 3 L11 11 M11 3 L3 11" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
3235
+ </button>
3236
+ </div>
3237
+ <div id="sheet-content"></div>
3238
+ </aside>
3239
+
3240
+ <div id="toast" class="pointer-events-none fixed bottom-5 left-1/2 z-[60] -translate-x-1/2 rounded-md bg-zinc-900 px-3 py-1.5 text-[12px] text-zinc-100 opacity-0 shadow-lg transition-opacity duration-200 dark:bg-zinc-100 dark:text-zinc-900"></div>`;
3241
+ var RESIZER_HTML = `<div class="sidebar-resizer -ml-0.5 hidden w-1 shrink-0 cursor-col-resize transition-colors hover:bg-accent-400/70 active:bg-accent-400/70 dark:hover:bg-accent-500/60 dark:active:bg-accent-500/60 md:block"></div>`;
3242
+ function pageHtml(tasks, epics) {
3243
+ return `<!doctype html>
3244
+ <html lang="en"><head><meta charset="utf-8">
3245
+ <title>${esc(settings.title)} Board</title>
3246
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3247
+ ${HEAD_LINKS}
3248
+ <script>${head_default}</script>
3249
+ </head>
3250
+ <body class="h-[calc(100vh/var(--zoom))] overflow-hidden bg-zinc-50 font-sans text-zinc-900 antialiased dark:bg-zinc-950 dark:text-zinc-100">
3251
+
3252
+ ${headerHtml("board", headerMetaHtml(tasks), true)}
3253
+
3254
+ <div id="app" class="flex h-[calc((100vh/var(--zoom))-3rem)]">
3255
+ ${appHtml(tasks, epics)}
3256
+ </div>
3257
+
3258
+ ${SHEET_HTML}
3259
+
3260
+ <script>${board_default}</script>
3261
+ </body></html>`;
3262
+ }
3263
+ function listDocs() {
3264
+ const groups = [];
3265
+ const rootFiles = readdirSync(PLANNER_DIR).filter((f) => f.endsWith(".md")).sort((a, b) => {
3266
+ const ia = ROOT_DOC_ORDER.indexOf(a);
3267
+ const ib = ROOT_DOC_ORDER.indexOf(b);
3268
+ return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib) || a.localeCompare(b);
3269
+ });
3270
+ if (rootFiles.length)
3271
+ groups.push({ group: "planner", files: rootFiles });
3272
+ const dirs = readdirSync(PLANNER_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && !DOC_EXCLUDE.has(d.name)).map((d) => d.name).sort();
3273
+ for (const dir of dirs) {
3274
+ const files = readdirSync(join(PLANNER_DIR, dir)).filter((f) => f.endsWith(".md")).sort();
3275
+ if (files.length)
3276
+ groups.push({ group: dir, files: files.map((f) => `${dir}/${f}`) });
3277
+ }
3278
+ return groups;
3279
+ }
3280
+ var RAW_TYPES = {
3281
+ svg: "image/svg+xml",
3282
+ png: "image/png",
3283
+ jpg: "image/jpeg",
3284
+ jpeg: "image/jpeg",
3285
+ gif: "image/gif",
3286
+ webp: "image/webp",
3287
+ avif: "image/avif",
3288
+ ico: "image/x-icon",
3289
+ pdf: "application/pdf",
3290
+ txt: "text/plain; charset=utf-8"
3291
+ };
3292
+ function safePath(rel) {
3293
+ const parts = rel.split("/").filter((p) => p && p !== ".");
3294
+ if (parts.some((p) => p === ".." || p.startsWith(".")))
3295
+ return null;
3296
+ const abs = join(PLANNER_DIR, ...parts);
3297
+ try {
3298
+ if (!statSync(abs).isFile())
3299
+ return null;
3300
+ } catch {
3301
+ return null;
3302
+ }
3303
+ return abs;
3304
+ }
3305
+ function safeDocPath(rel) {
3306
+ if (!rel.endsWith(".md"))
3307
+ return null;
3308
+ const parts = rel.split("/").filter((p) => p && p !== ".");
3309
+ if (parts.length > 1 && DOC_EXCLUDE.has(parts[0]))
3310
+ return null;
3311
+ return safePath(rel);
3312
+ }
3313
+ function parseTimeline(body) {
3314
+ const lines = body.split(`
3315
+ `);
3316
+ const parseRow = (l) => l.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
3317
+ for (let i = 0;i < lines.length - 1; i++) {
3318
+ if (!/^\s*\|/.test(lines[i]) || !/^\s*\|[\s:|-]+\|?\s*$/.test(lines[i + 1] ?? ""))
3319
+ continue;
3320
+ const head = parseRow(lines[i]).map((h) => h.toLowerCase());
3321
+ if (!head.includes("start") || !head.includes("end"))
3322
+ continue;
3323
+ const rows = [];
3324
+ for (let j = i + 2;j < lines.length && /^\s*\|/.test(lines[j]); j++) {
3325
+ const cells = parseRow(lines[j]);
3326
+ const get = (k) => head.indexOf(k) === -1 ? "" : cells[head.indexOf(k)] ?? "";
3327
+ const start = new Date(get("start"));
3328
+ const end = new Date(get("end"));
3329
+ if (isNaN(+start) || isNaN(+end))
3330
+ continue;
3331
+ rows.push({
3332
+ id: get("id") || get("milestone"),
3333
+ title: get("title") || get("milestone") || get("id"),
3334
+ start,
3335
+ end,
3336
+ status: get("status").toLowerCase(),
3337
+ epic: get("epic")
3338
+ });
3339
+ }
3340
+ return rows.length ? rows : null;
3341
+ }
3342
+ return null;
3343
+ }
3344
+ var TIMELINE_COLOR = { done: "#22c55e", active: "#8b5cf6", pending: "#a1a1aa" };
3345
+ function timelineHtml(rows) {
3346
+ const DAY = 86400000;
3347
+ const min = Math.min(...rows.map((r) => +r.start)) - 2 * DAY;
3348
+ const max = Math.max(...rows.map((r) => +r.end)) + 2 * DAY;
3349
+ const span = max - min;
3350
+ const pct = (d) => (d - min) / span * 100;
3351
+ const today = Date.now();
3352
+ const months = [];
3353
+ const cursor = new Date(min);
3354
+ cursor.setDate(1);
3355
+ cursor.setMonth(cursor.getMonth() + 1);
3356
+ while (+cursor < max) {
3357
+ const p = pct(+cursor);
3358
+ months.push(`<div class="absolute inset-y-0 border-l border-zinc-200 dark:border-zinc-800" style="left:${p.toFixed(2)}%"></div>
3359
+ <div class="absolute top-0 -translate-x-1/2 text-[9.5px] font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-600" style="left:${p.toFixed(2)}%">${cursor.toLocaleDateString("en", { month: "short" })}</div>`);
3360
+ cursor.setMonth(cursor.getMonth() + 1);
3361
+ }
3362
+ const bars = rows.map((r) => {
3363
+ const status = TIMELINE_COLOR[r.status] ? r.status : today > +r.end ? "done" : today >= +r.start ? "active" : "pending";
3364
+ const color = TIMELINE_COLOR[status];
3365
+ const left = pct(+r.start);
3366
+ const width = Math.max(pct(+r.end + DAY) - left, 1.5);
3367
+ const label = `${r.id} ${r.title}`.trim();
3368
+ const dates = `${r.start.toISOString().slice(0, 10)} to ${r.end.toISOString().slice(0, 10)}`;
3369
+ return `<div class="flex h-8 items-center">
3370
+ <div class="w-44 shrink-0 truncate pr-3 text-[12px] text-zinc-600 dark:text-zinc-400" title="${esc(label)}">
3371
+ ${r.epic ? `<a href="/#${esc(r.epic)}" class="font-mono text-[10.5px] text-accent-600 hover:underline dark:text-accent-400">${esc(r.epic)}</a> ` : ""}<span class="font-medium text-zinc-800 dark:text-zinc-200">${esc(r.id)}</span> ${esc(r.title)}
3372
+ </div>
3373
+ <div class="relative h-5 flex-1">
3374
+ <div class="track absolute inset-y-0 hidden"></div>
3375
+ <div class="absolute inset-y-0 overflow-hidden whitespace-nowrap rounded-md px-2 text-[10px] font-medium leading-5 text-white shadow-sm" style="left:${left.toFixed(2)}%;width:${width.toFixed(2)}%;background:${color}" title="${esc(label)}: ${dates} (${status})">${esc(r.title)}</div>
3376
+ </div>
3377
+ </div>`;
3378
+ }).join("");
3379
+ const todayMark = today >= min && today <= max ? `<div class="absolute inset-y-0 z-10 border-l-2 border-rose-500/70" style="left:calc(11rem + (100% - 11rem) * ${(pct(today) / 100).toFixed(4)})">
3380
+ <span class="absolute -top-0.5 left-1 text-[9px] font-semibold uppercase tracking-wide text-rose-500">today</span>
3381
+ </div>` : "";
3382
+ return `<div class="mb-8 rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">
3383
+ <div class="mb-3 flex items-baseline justify-between">
3384
+ <span class="text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">Timeline</span>
3385
+ <span class="text-[10.5px] text-zinc-400 dark:text-zinc-600">${new Date(min + 2 * DAY).toISOString().slice(0, 10)} to ${new Date(max - 2 * DAY).toISOString().slice(0, 10)}</span>
3386
+ </div>
3387
+ <div class="relative pt-4">
3388
+ <div class="absolute inset-y-0 left-44 right-0">${months.join("")}</div>
3389
+ ${todayMark}
3390
+ <div class="relative flex flex-col gap-1">${bars}</div>
3391
+ </div>
3392
+ <div class="mt-3 flex gap-4 text-[10px] text-zinc-400 dark:text-zinc-500">
3393
+ <span class="inline-flex items-center gap-1.5"><span class="h-2 w-2 rounded-sm" style="background:${TIMELINE_COLOR.done}"></span>done</span>
3394
+ <span class="inline-flex items-center gap-1.5"><span class="h-2 w-2 rounded-sm" style="background:${TIMELINE_COLOR.active}"></span>active</span>
3395
+ <span class="inline-flex items-center gap-1.5"><span class="h-2 w-2 rounded-sm" style="background:${TIMELINE_COLOR.pending}"></span>pending</span>
3396
+ </div>
3397
+ </div>`;
3398
+ }
3399
+ function docsNavHtml(groups) {
3400
+ return groups.map((g) => `<details class="doc-folder" open data-folder="${esc(g.group)}">
3401
+ <summary>${esc(g.group)}<span class="doc-folder-count">${g.files.length}</span></summary>
3402
+ ${g.files.map((f) => {
3403
+ const name = basename(f);
3404
+ return `<a href="/docs?file=${encodeURIComponent(f)}" class="doc-link" data-file="${esc(f)}" title="${esc(f)}">${esc(name)}</a>`;
3405
+ }).join("")}
3406
+ </details>`).join("");
3407
+ }
3408
+ function docsPageHtml(relFile) {
3409
+ const groups = listDocs();
3410
+ const abs = safeDocPath(relFile);
3411
+ const nav = docsNavHtml(groups);
3412
+ let main;
3413
+ let hasMermaid = false;
3414
+ if (!abs) {
3415
+ main = `<p class="text-[13px] text-zinc-400">Document not found. Pick one from the sidebar.</p>`;
3416
+ } else {
3417
+ const raw = readFileSync2(abs, "utf8");
3418
+ const parsed = parseFrontmatter(raw);
3419
+ const body = parsed ? parsed.body : raw;
3420
+ const fm = parsed?.fm ?? {};
3421
+ hasMermaid = /^```mermaid/m.test(body);
3422
+ const isTimeline = fm.render === "timeline" || basename(relFile).toUpperCase() === "TIMELINE.MD";
3423
+ const timeline = isTimeline ? parseTimeline(body) : null;
3424
+ const baseDir = dirname(relFile) === "." ? "" : dirname(relFile);
3425
+ const backlinks = buildBacklinks();
3426
+ const sources = [...backlinks.get(relFile) ?? [], ...fm.id ? backlinks.get(fm.id) ?? [] : []];
3427
+ const seen = new Set;
3428
+ const unique = sources.filter((s) => seen.has(s.file) ? false : (seen.add(s.file), true));
3429
+ main = `${timeline ? timelineHtml(timeline) : ""}<article class="doc-body">${mdToHtml(body, { docMode: true, baseDir })}</article>
3430
+ ${backlinksHtml(unique, "Linked mentions", true)}
3431
+ <div class="mt-10 border-t border-zinc-100 pt-3 font-mono text-[10.5px] text-zinc-400 dark:border-zinc-800 dark:text-zinc-600">${esc(relFile)}</div>`;
3432
+ }
3433
+ return `<!doctype html>
3434
+ <html lang="en"><head><meta charset="utf-8">
3435
+ <title>${esc(settings.title)} Docs: ${esc(basename(relFile))}</title>
3436
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3437
+ ${HEAD_LINKS}
3438
+ ${hasMermaid ? '<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"></script>' : ""}
3439
+ <script>${head_default}</script>
3440
+ </head>
3441
+ <body class="h-[calc(100vh/var(--zoom))] overflow-hidden bg-zinc-50 font-sans text-zinc-900 antialiased dark:bg-zinc-950 dark:text-zinc-100">
3442
+
3443
+ ${headerHtml("docs", "", true)}
3444
+
3445
+ <div class="flex h-[calc((100vh/var(--zoom))-3rem)]">
3446
+ <aside id="doc-sidebar" class="hidden w-60 shrink-0 flex-col overflow-y-auto border-r border-zinc-200 bg-white px-2.5 py-2 dark:border-zinc-800 dark:bg-zinc-950 md:flex">
3447
+ <div class="mb-1 flex items-center gap-1 px-2 pb-1">
3448
+ <span class="text-[10.5px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">Files</span>
3449
+ <button id="fold-all" type="button" class="${ICON_BUTTON} ml-auto" aria-label="Collapse all folders" title="Collapse all folders">
3450
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 6.5 8 2.5 12 6.5 M4 9.5 8 13.5 12 9.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
3451
+ </button>
3452
+ </div>
3453
+ <div id="doc-tree">${nav}</div>
3454
+ </aside>
3455
+ ${RESIZER_HTML}
3456
+ <main id="doc-main" class="flex-1 overflow-y-auto">
3457
+ <div id="search-results" class="mx-auto hidden max-w-3xl divide-y divide-zinc-200 px-8 py-5 dark:divide-zinc-800"></div>
3458
+ <div id="doc-content" class="mx-auto max-w-3xl px-8 py-8"${abs ? ` data-file="${esc(relFile)}" data-rev="${esc(fileRev(abs))}"` : ""}>${main}</div>
3459
+ </main>
3460
+ <aside id="doc-outline" class="hidden w-56 shrink-0 overflow-y-auto border-l border-zinc-200 bg-white px-3 py-3 dark:border-zinc-800 dark:bg-zinc-950 lg:block">
3461
+ <div class="mb-1.5 px-2 text-[10.5px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">On this page</div>
3462
+ <nav id="outline-list"></nav>
3463
+ </aside>
3464
+ </div>
3465
+
3466
+ ${settingsPanelHtml()}
3467
+ ${newItemPanelHtml()}
3468
+ ${SHEET_HTML}
3469
+
3470
+ <script>${docs_default}</script>
3471
+ </body></html>`;
3472
+ }
3473
+ function newItemPanelHtml() {
3474
+ const field = "w-full rounded-md border border-zinc-200 bg-white px-2 py-1 text-[12.5px] outline-none focus:border-accent-400 dark:border-zinc-700 dark:bg-zinc-900 dark:focus:border-accent-500";
3475
+ const label = "mb-1 block text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500";
3476
+ const hint = "mt-1 text-[11px] leading-relaxed text-zinc-400 dark:text-zinc-500";
3477
+ const epics = allEpics().sort((a, b) => a.id.localeCompare(b.id));
3478
+ const columns = boardColumns(allTasks());
3479
+ const opt = (v, text) => `<option value="${esc(v)}">${esc(text)}</option>`;
3480
+ return `<template id="detail-new"><div class="detail-root" data-id="new">
3481
+ <h2 class="text-[16px] font-semibold leading-snug text-zinc-900 dark:text-zinc-50">New</h2>
3482
+ <p class="${hint}">Writes a markdown file into the workspace. Ids and filenames are assigned for you.</p>
3483
+
3484
+ <div class="mt-4 flex gap-1" data-kind-picker>
3485
+ ${[
3486
+ ["task", "User story"],
3487
+ ["epic", "Epic"],
3488
+ ["doc", "Document"]
3489
+ ].map(([k, name]) => `<button type="button" class="pick" data-kind="${k}">${name}</button>`).join("")}
3490
+ </div>
3491
+
3492
+ <div class="mt-4"><label class="${label}" for="new-title">Title</label>
3493
+ <input id="new-title" name="title" class="${field}" maxlength="200" placeholder="Short imperative title">
3494
+ </div>
3495
+
3496
+ <div class="mt-3" data-for="doc"><label class="${label}" for="new-path">File path</label>
3497
+ <input id="new-path" name="path" class="${field}" placeholder="notes/my-document.md">
3498
+ <p class="${hint}">Relative to the workspace root. Folders are created as needed.</p>
3499
+ </div>
3500
+
3501
+ <div class="mt-3 grid grid-cols-2 gap-3" data-for="task">
3502
+ <div><label class="${label}" for="new-epic">Epic</label>
3503
+ <select id="new-epic" name="epic" class="${field}">
3504
+ ${opt("", "(none)")}${epics.map((e) => opt(e.id, `${e.id} ${e.title}`)).join("")}
3505
+ </select>
3506
+ </div>
3507
+ <div><label class="${label}" for="new-priority">Priority</label>
3508
+ <select id="new-priority" name="priority" class="${field}">
3509
+ ${PRIORITIES.map((p) => opt(p, p)).join("")}
3510
+ </select>
3511
+ </div>
3512
+ <div><label class="${label}" for="new-status">Status</label>
3513
+ <select id="new-status" name="status" class="${field}">${columns.map((c) => opt(c, c.replace(/-/g, " "))).join("")}</select>
3514
+ </div>
3515
+ </div>
3516
+
3517
+ <div class="mt-3"><label class="${label}" for="new-body">Description</label>
3518
+ <textarea id="new-body" name="body" rows="5" class="${field} resize-y" placeholder="Markdown. Optional."></textarea>
3519
+ </div>
3520
+
3521
+ <div class="mt-3" data-for="task"><label class="${label}" for="new-subtasks">Subtasks</label>
3522
+ <textarea id="new-subtasks" name="subtasks" rows="3" class="${field} resize-y" placeholder="One per line"></textarea>
3523
+ </div>
3524
+
3525
+ <p id="new-error" class="mt-4 hidden rounded-md border border-rose-300 bg-rose-50 px-2.5 py-1.5 text-[12px] text-rose-700 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-300" role="alert"></p>
3526
+
3527
+ <div class="mt-4 flex items-center gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
3528
+ <button id="new-save" type="button"
3529
+ class="rounded-md bg-accent-600 px-3 py-1.5 text-[12.5px] font-medium text-white transition-colors hover:bg-accent-500">Create</button>
3530
+ <span id="new-target" class="font-mono text-[10.5px] text-zinc-400 dark:text-zinc-600"></span>
3531
+ </div>
3532
+ </div></template>`;
3533
+ }
3534
+ var GRAPH_BUTTON = "rounded-md border border-zinc-200 bg-white px-2 py-1 text-[11px] text-zinc-500 transition-colors hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100";
3535
+ function graphPageHtml() {
3536
+ const { nodes, links } = buildGraph();
3537
+ return `<!doctype html>
3538
+ <html lang="en"><head><meta charset="utf-8">
3539
+ <title>${esc(settings.title)} Graph</title>
3540
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3541
+ ${HEAD_LINKS}
3542
+ <script>${head_default}</script>
3543
+ </head>
3544
+ <body class="h-[calc(100vh/var(--zoom))] overflow-hidden bg-zinc-50 font-sans text-zinc-900 antialiased dark:bg-zinc-950 dark:text-zinc-100">
3545
+
3546
+ ${headerHtml("graph", `<span class="text-[11px] text-zinc-400 dark:text-zinc-600">${nodes.length} notes, ${links.length} links</span>`, false)}
3547
+
3548
+ <div class="relative h-[calc((100vh/var(--zoom))-3rem)]">
3549
+ <canvas id="graph-canvas" class="block h-full w-full cursor-grab"></canvas>
3550
+ <div id="graph-empty" class="pointer-events-none absolute inset-0 hidden items-center justify-center text-[13px] text-zinc-400 dark:text-zinc-600">No documents to graph yet.</div>
3551
+ <div id="graph-hint" class="pointer-events-none absolute bottom-3 left-3 text-[10.5px] text-zinc-400 dark:text-zinc-600">Scroll to zoom · drag to pan · click a note to open it</div>
3552
+ <div class="absolute bottom-3 right-3 flex items-center gap-1.5">
3553
+ <button id="graph-shape" type="button" class="${GRAPH_BUTTON}">Globe</button>
3554
+ <button id="graph-reset" type="button" title="Reset view" class="${GRAPH_BUTTON}">Reset</button>
3555
+ </div>
3556
+ </div>
3557
+
3558
+ ${settingsPanelHtml()}
3559
+ ${newItemPanelHtml()}
3560
+ ${SHEET_HTML}
3561
+
3562
+ <!-- The graph ships with the page instead of as a second request. It is already built here for the
3563
+ header count, and one round trip on a cold connection is the slowest part of this view. -->
3564
+ <script type="application/json" id="graph-data">${JSON.stringify({ nodes, links }).replace(/</g, "\\u003c")}</script>
3565
+ <script>${graph_default}</script>
3566
+ </body></html>`;
3567
+ }
3568
+ var SSE_MAX = 24;
3569
+ var sseClients = new Set;
3570
+ var broadcastTimer = null;
3571
+ function scheduleBroadcast() {
3572
+ workspaceVersion++;
3573
+ if (broadcastTimer)
3574
+ clearTimeout(broadcastTimer);
3575
+ broadcastTimer = setTimeout(() => {
3576
+ for (const c of sseClients) {
3577
+ try {
3578
+ c.enqueue(`data: change
3579
+
3580
+ `);
3581
+ } catch {
3582
+ sseClients.delete(c);
3583
+ }
3584
+ }
3585
+ }, 120);
3586
+ }
3587
+ try {
3588
+ watch(PLANNER_DIR, { recursive: true }, (_event, filename) => {
3589
+ if (filename && !String(filename).endsWith(".md") && basename(String(filename)) !== SETTINGS_FILENAME)
3590
+ return;
3591
+ if (filename && /(^|\/)(\.git|board|node_modules)(\/|$)/.test(String(filename)))
3592
+ return;
3593
+ scheduleBroadcast();
3594
+ });
3595
+ } catch {
3596
+ for (const dir of [TASKS_DIR, EPICS_DIR]) {
3597
+ try {
3598
+ watch(dir, scheduleBroadcast);
3599
+ } catch {}
3600
+ }
3601
+ }
3602
+ setInterval(() => {
3603
+ for (const c of sseClients) {
3604
+ try {
3605
+ c.enqueue(`: ping
3606
+
3607
+ `);
3608
+ } catch {
3609
+ sseClients.delete(c);
3610
+ }
3611
+ }
3612
+ }, 25000);
3613
+ function json(data, status = 200) {
3614
+ return new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json" } });
3615
+ }
3616
+ var pageCache = new Map;
3617
+ function htmlPage(key, req, render) {
3618
+ let hit = pageCache.get(key);
3619
+ if (!hit || hit.version !== workspaceVersion) {
3620
+ if (pageCache.size > 32)
3621
+ pageCache.clear();
3622
+ const html = render();
3623
+ hit = {
3624
+ version: workspaceVersion,
3625
+ html,
3626
+ etag: `W/"${createHash("sha1").update(html).digest("base64url").slice(0, 20)}"`
3627
+ };
3628
+ pageCache.set(key, hit);
3629
+ }
3630
+ if (req.headers.get("if-none-match") === hit.etag) {
3631
+ return new Response(null, { status: 304, headers: { etag: hit.etag } });
3632
+ }
3633
+ return new Response(hit.html, {
3634
+ headers: { "content-type": "text/html; charset=utf-8", etag: hit.etag, "cache-control": "no-cache" }
3635
+ });
3636
+ }
3637
+ async function handlePatch(id, req, ctx) {
3638
+ const path = findItemPath(id);
3639
+ if (!path)
3640
+ return json({ error: `unknown item ${id}` }, 404);
3641
+ let patch;
3642
+ try {
3643
+ patch = await req.json();
3644
+ } catch {
3645
+ return json({ error: "invalid JSON" }, 400);
3646
+ }
3647
+ if (patch.rev && fileRev(path) !== String(patch.rev)) {
3648
+ return json({ error: "stale: file changed since it was rendered" }, 409);
3649
+ }
3650
+ let raw = readFileSync2(path, "utf8");
3651
+ const messages = [];
3652
+ if (patch.status !== undefined) {
3653
+ if (!boardColumns(allTasks()).includes(patch.status)) {
3654
+ return json({ error: `invalid status ${patch.status}` }, 400);
3655
+ }
3656
+ const old = getFrontmatterValue(raw, "status");
3657
+ if (old !== patch.status) {
3658
+ raw = setFrontmatterValue(raw, "status", patch.status);
3659
+ messages.push(`${id}: ${old} -> ${patch.status} via board`);
3660
+ }
3661
+ }
3662
+ if (patch.priority !== undefined) {
3663
+ if (!PRIORITIES.includes(patch.priority))
3664
+ return json({ error: `invalid priority ${patch.priority}` }, 400);
3665
+ if (getFrontmatterValue(raw, "priority") !== patch.priority) {
3666
+ raw = setFrontmatterValue(raw, "priority", patch.priority);
3667
+ messages.push(`${id}: priority ${patch.priority} via board`);
3668
+ }
3669
+ }
3670
+ if (patch.checkbox !== undefined) {
3671
+ const { index, checked } = patch.checkbox;
3672
+ const updated = toggleCheckboxLine(raw, Number(index), Boolean(checked));
3673
+ if (updated === null)
3674
+ return json({ error: `checkbox ${index} not found` }, 400);
3675
+ raw = updated;
3676
+ messages.push(`${id}: toggle subtask via board`);
3677
+ }
3678
+ const rel = relative(PLANNER_DIR, path);
3679
+ if (messages.length === 0) {
3680
+ ctx.detail = `${rel} unchanged`;
3681
+ return json({ ok: true, rev: fileRev(path), unchanged: true });
3682
+ }
3683
+ writeFileSync2(path, raw);
3684
+ gitCommitFile(path, messages.join("; "));
3685
+ scheduleBroadcast();
3686
+ ctx.detail = `${rel}: ${messages.join("; ")}${gitEnabled() ? " [committed]" : ""}`;
3687
+ return json({ ok: true, rev: fileRev(path) });
3688
+ }
3689
+ var MAX_TITLE = 200;
3690
+ function cleanTitle(v) {
3691
+ if (typeof v !== "string")
3692
+ return null;
3693
+ const t = v.replace(/\s+/g, " ").trim();
3694
+ if (!t || t.length > MAX_TITLE)
3695
+ return null;
3696
+ return [...t].some((c) => c < " " || c === "") ? null : t;
3697
+ }
3698
+ function slugify(s) {
3699
+ const slug = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "");
3700
+ return slug || "untitled";
3701
+ }
3702
+ function nextId(prefix, width, ids) {
3703
+ const re = new RegExp(`^${prefix}(\\d+)$`);
3704
+ let max = 0;
3705
+ for (const id of ids) {
3706
+ const m = re.exec(id);
3707
+ if (m)
3708
+ max = Math.max(max, Number(m[1]));
3709
+ }
3710
+ return prefix + String(max + 1).padStart(width, "0");
3711
+ }
3712
+ function cleanBody(v) {
3713
+ if (typeof v !== "string")
3714
+ return "";
3715
+ const text = v.replace(/\r\n?/g, `
3716
+ `).slice(0, 1e5);
3717
+ return [...text].filter((c) => c >= " " || c === `
3718
+ ` || c === "\t").join("").trim();
3719
+ }
3720
+ function safeNewDocPath(rel) {
3721
+ if (typeof rel !== "string" || !/\.md$/i.test(rel))
3722
+ return null;
3723
+ if (/^[/\\]/.test(rel) || /^[A-Za-z]:/.test(rel))
3724
+ return null;
3725
+ const parts = rel.split("/").filter((p) => p && p !== ".");
3726
+ if (!parts.length || parts.length > 4)
3727
+ return null;
3728
+ if (parts.some((p) => !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(p)))
3729
+ return null;
3730
+ if (DOC_EXCLUDE.has(parts[0]))
3731
+ return null;
3732
+ const abs = join(PLANNER_DIR, ...parts);
3733
+ const back = relative(PLANNER_DIR, abs);
3734
+ if (!back || back.startsWith("..") || isAbsolute(back))
3735
+ return null;
3736
+ return abs;
3737
+ }
3738
+ function createItem(kind, input, ctx) {
3739
+ const title = cleanTitle(input.title);
3740
+ if (!title)
3741
+ return json({ error: "a title is required, on one line and under 200 characters" }, 400);
3742
+ const body = cleanBody(input.body);
3743
+ const today = new Date().toISOString().slice(0, 10);
3744
+ let abs;
3745
+ let id = "";
3746
+ let text;
3747
+ if (kind === "epic") {
3748
+ id = nextId("E", 2, allEpics().map((e) => e.id));
3749
+ abs = join(EPICS_DIR, `${id}-${slugify(title)}.md`);
3750
+ text = `---
3751
+ id: ${id}
3752
+ title: ${title}
3753
+ status: planned
3754
+ created: ${today}
3755
+ ---
3756
+
3757
+ ` + `## Description
3758
+
3759
+ ${body || "TBD."}
3760
+ `;
3761
+ } else if (kind === "task") {
3762
+ const status = input.status === undefined ? boardColumns(allTasks())[0] : String(input.status);
3763
+ if (!boardColumns(allTasks()).includes(status))
3764
+ return json({ error: `invalid status ${status}` }, 400);
3765
+ const priority = input.priority === undefined ? "P2" : String(input.priority);
3766
+ if (!PRIORITIES.includes(priority))
3767
+ return json({ error: `invalid priority ${priority}` }, 400);
3768
+ const epic = input.epic ? String(input.epic) : "";
3769
+ if (epic && !allEpics().some((e) => e.id === epic))
3770
+ return json({ error: `unknown epic ${epic}` }, 400);
3771
+ id = nextId("ST-", 3, allTasks().map((t) => t.id));
3772
+ abs = join(TASKS_DIR, `${id}-${slugify(title)}.md`);
3773
+ const subtasks = cleanBody(input.subtasks).split(`
3774
+ `).map((l) => l.replace(/^\s*[-*]\s*(\[[ x]\]\s*)?/, "").trim()).filter(Boolean);
3775
+ text = `---
3776
+ id: ${id}
3777
+ title: ${title}
3778
+ epic: ${epic}
3779
+ status: ${status}
3780
+ priority: ${priority}
3781
+ ` + `depends_on: []
3782
+ created: ${today}
3783
+ ---
3784
+
3785
+ ` + `## Description
3786
+
3787
+ ${body || "TBD."}
3788
+ ` + (subtasks.length ? `
3789
+ ## Subtasks
3790
+
3791
+ ${subtasks.map((s) => `- [ ] ${s}`).join(`
3792
+ `)}
3793
+ ` : "");
3794
+ } else if (kind === "doc") {
3795
+ const resolved = safeNewDocPath(input.path);
3796
+ if (!resolved)
3797
+ return json({ error: "path must be a plain .md file inside the workspace" }, 400);
3798
+ abs = resolved;
3799
+ text = `# ${title}
3800
+
3801
+ ${body || "TBD."}
3802
+ `;
3803
+ } else {
3804
+ return json({ error: `unknown kind ${kind}` }, 400);
3805
+ }
3806
+ if (existsSync(abs))
3807
+ return json({ error: `${relative(PLANNER_DIR, abs)} already exists` }, 409);
3808
+ try {
3809
+ mkdirSync(dirname(abs), { recursive: true });
3810
+ writeFileSync2(abs, text, { flag: "wx" });
3811
+ } catch {
3812
+ return json({ error: "could not write the file" }, 500);
3813
+ }
3814
+ const file = relative(PLANNER_DIR, abs);
3815
+ gitCommitFile(abs, `${id || file}: created via board`);
3816
+ scheduleBroadcast();
3817
+ ctx.detail = `created ${file}${gitEnabled() ? " [committed]" : ""}`;
3818
+ return json({ ok: true, id, file }, 201);
3819
+ }
3820
+ async function handleDocPatch(rel, req, ctx) {
3821
+ const path = safeDocPath(rel);
3822
+ if (!path)
3823
+ return json({ error: `unknown document ${rel}` }, 404);
3824
+ let patch;
3825
+ try {
3826
+ patch = await req.json();
3827
+ } catch {
3828
+ return json({ error: "invalid JSON" }, 400);
3829
+ }
3830
+ if (patch.rev && fileRev(path) !== String(patch.rev)) {
3831
+ return json({ error: "stale: file changed since it was rendered" }, 409);
3832
+ }
3833
+ if (!patch.checkbox)
3834
+ return json({ error: "nothing to change" }, 400);
3835
+ const raw = readFileSync2(path, "utf8");
3836
+ const updated = toggleCheckboxLine(raw, Number(patch.checkbox.index), Boolean(patch.checkbox.checked));
3837
+ if (updated === null)
3838
+ return json({ error: `checkbox ${patch.checkbox.index} not found` }, 400);
3839
+ writeFileSync2(path, updated);
3840
+ gitCommitFile(path, `${rel}: toggle checkbox via board`);
3841
+ scheduleBroadcast();
3842
+ ctx.detail = `${rel}: toggle checkbox${gitEnabled() ? " [committed]" : ""}`;
3843
+ return json({ ok: true, rev: fileRev(path) });
3844
+ }
3845
+ function settingsDiff(before, after) {
3846
+ const keys = Object.keys(after);
3847
+ const changed = keys.filter((k) => String(before[k]) !== String(after[k])).map((k) => `${k}: ${JSON.stringify(before[k])} -> ${JSON.stringify(after[k])}`);
3848
+ return changed.length ? changed.join(", ") : "no change";
3849
+ }
3850
+ function redirect(to) {
3851
+ return new Response(null, { status: 302, headers: { location: to } });
3852
+ }
3853
+ async function handle(req, ctx) {
3854
+ {
3855
+ const url = new URL(req.url);
3856
+ settings = readSettings(SETTINGS_FILE, WORKSPACE_NAME);
3857
+ try {
3858
+ if (url.pathname === "/sse") {
3859
+ let ctrl;
3860
+ const stream = new ReadableStream({
3861
+ start(c) {
3862
+ ctrl = c;
3863
+ while (sseClients.size >= SSE_MAX) {
3864
+ const oldest = sseClients.values().next().value;
3865
+ if (!oldest)
3866
+ break;
3867
+ sseClients.delete(oldest);
3868
+ try {
3869
+ oldest.close();
3870
+ } catch {}
3871
+ }
3872
+ sseClients.add(c);
3873
+ c.enqueue(`: connected
3874
+
3875
+ `);
3876
+ },
3877
+ cancel() {
3878
+ sseClients.delete(ctrl);
3879
+ log(`SSE client disconnected (${sseClients.size} live)`);
3880
+ }
3881
+ });
3882
+ ctx.detail = `${sseClients.size} client(s) live`;
3883
+ return new Response(stream.pipeThrough(new TextEncoderStream), {
3884
+ headers: { "content-type": "text/event-stream", "cache-control": "no-cache" }
3885
+ });
3886
+ }
3887
+ if (url.pathname === "/styles.css") {
3888
+ return new Response(styles_gen_default, {
3889
+ headers: {
3890
+ "content-type": "text/css; charset=utf-8",
3891
+ "cache-control": url.searchParams.get("v") ? "public, max-age=31536000, immutable" : "no-cache"
3892
+ }
3893
+ });
3894
+ }
3895
+ if (url.pathname === "/logo.svg") {
3896
+ return new Response(logo_default, {
3897
+ headers: { "content-type": "image/svg+xml; charset=utf-8", "cache-control": "public, max-age=86400" }
3898
+ });
3899
+ }
3900
+ if (url.pathname.startsWith("/raw/") && req.method === "GET") {
3901
+ const rel = decodeURIComponent(url.pathname.slice(5));
3902
+ const abs = safePath(rel);
3903
+ if (!abs) {
3904
+ ctx.detail = `${rel} (not found)`;
3905
+ return new Response("Not found", { status: 404 });
3906
+ }
3907
+ ctx.detail = rel;
3908
+ return new Response(readFileSync2(abs), {
3909
+ headers: {
3910
+ "content-type": RAW_TYPES[rel.slice(rel.lastIndexOf(".") + 1).toLowerCase()] ?? "application/octet-stream",
3911
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
3912
+ "x-content-type-options": "nosniff",
3913
+ "cache-control": "no-cache"
3914
+ }
3915
+ });
3916
+ }
3917
+ if (url.pathname === "/api/settings") {
3918
+ if (req.method === "GET")
3919
+ return json(settings);
3920
+ if (req.method === "PUT") {
3921
+ let body;
3922
+ try {
3923
+ body = await req.json();
3924
+ } catch {
3925
+ return json({ error: "invalid JSON" }, 400);
3926
+ }
3927
+ const wanted = body?.views;
3928
+ if (Array.isArray(wanted) && !ALL_VIEWS.some((v) => wanted.includes(v)))
3929
+ return json({ error: "Keep at least one view on: with all of them off there is nothing to show." }, 400);
3930
+ const before = settings;
3931
+ settings = normalizeSettings(body, WORKSPACE_NAME);
3932
+ writeSettings(SETTINGS_FILE, settings);
3933
+ scheduleBroadcast();
3934
+ ctx.detail = `${SETTINGS_FILENAME}: ${settingsDiff(before, settings)}`;
3935
+ return json(settings);
3936
+ }
3937
+ }
3938
+ const paneMatch = url.pathname.match(/^\/api\/items\/([A-Za-z0-9-]+)\/pane$/);
3939
+ if (paneMatch && req.method === "GET") {
3940
+ const html = itemPaneHtml(paneMatch[1]);
3941
+ ctx.detail = html ? paneMatch[1] : `${paneMatch[1]} (unknown)`;
3942
+ return html ? new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }) : new Response("Not found", { status: 404 });
3943
+ }
3944
+ const apiMatch = url.pathname.match(/^\/api\/items\/([A-Za-z0-9-]+)$/);
3945
+ if (apiMatch && req.method === "PATCH")
3946
+ return handlePatch(apiMatch[1], req, ctx);
3947
+ if (url.pathname === "/api/tasks" && req.method === "GET") {
3948
+ const tasks = allTasks();
3949
+ ctx.detail = `${tasks.length} tasks`;
3950
+ return json(tasks);
3951
+ }
3952
+ if (url.pathname === "/api/epics" && req.method === "GET") {
3953
+ const epics = allEpics();
3954
+ ctx.detail = `${epics.length} epics`;
3955
+ return json(epics);
3956
+ }
3957
+ const itemMatch = url.pathname.match(/^\/api\/(?:tasks|items)\/([A-Za-z0-9-]+)$/);
3958
+ if (itemMatch && req.method === "GET") {
3959
+ const item = allTasks().find((t) => t.id === itemMatch[1]) ?? allEpics().find((e) => e.id === itemMatch[1]);
3960
+ return item ? json(item) : json({ error: `unknown item ${itemMatch[1]}` }, 404);
3961
+ }
3962
+ if (url.pathname === "/api/docs-search" && req.method === "GET") {
3963
+ const q = (url.searchParams.get("q") ?? "").trim().toLowerCase();
3964
+ const hits = [];
3965
+ if (q) {
3966
+ for (const g of listDocs()) {
3967
+ for (const f of g.files) {
3968
+ const abs = safeDocPath(f);
3969
+ if (!abs)
3970
+ continue;
3971
+ const text = readFileSync2(abs, "utf8");
3972
+ const lower = text.toLowerCase();
3973
+ let count = 0;
3974
+ const snippets = [];
3975
+ for (let i = lower.indexOf(q);i !== -1; i = lower.indexOf(q, i + q.length)) {
3976
+ count++;
3977
+ if (snippets.length < 3) {
3978
+ const start = Math.max(0, i - 50);
3979
+ const end = Math.min(text.length, i + q.length + 90);
3980
+ snippets.push((start > 0 ? "…" : "") + text.slice(start, end).replace(/\s+/g, " ").trim() + (end < text.length ? "…" : ""));
3981
+ }
3982
+ }
3983
+ if (count || f.toLowerCase().includes(q))
3984
+ hits.push({ file: f, count, snippets });
3985
+ }
3986
+ }
3987
+ hits.sort((a, b) => b.count - a.count);
3988
+ }
3989
+ ctx.detail = `"${q}": ${hits.length} file(s)`;
3990
+ return Response.json(hits);
3991
+ }
3992
+ if (url.pathname === "/api/docs" && req.method === "PATCH") {
3993
+ return handleDocPatch(url.searchParams.get("file") ?? "", req, ctx);
3994
+ }
3995
+ const createMatch = url.pathname.match(/^\/api\/(tasks|epics|docs)$/);
3996
+ if (createMatch && req.method === "POST") {
3997
+ let body;
3998
+ try {
3999
+ body = await req.json();
4000
+ } catch {
4001
+ return json({ error: "invalid JSON" }, 400);
4002
+ }
4003
+ if (!body || typeof body !== "object")
4004
+ return json({ error: "expected a JSON object" }, 400);
4005
+ const kind = { tasks: "task", epics: "epic", docs: "doc" }[createMatch[1]];
4006
+ return createItem(kind, body, ctx);
4007
+ }
4008
+ if (url.pathname === "/api/graph" && req.method === "GET") {
4009
+ const g = buildGraph();
4010
+ ctx.detail = `${g.nodes.length} nodes, ${g.links.length} links`;
4011
+ return json(g);
4012
+ }
4013
+ if (url.pathname === "/docs" && req.method === "GET") {
4014
+ if (!settings.views.includes("docs"))
4015
+ return redirect(firstEnabledView());
4016
+ const rel = url.searchParams.get("file") ?? "README.md";
4017
+ ctx.detail = safeDocPath(rel) ? rel : `${rel} (not found)`;
4018
+ return htmlPage(`docs:${rel}`, req, () => docsPageHtml(rel));
4019
+ }
4020
+ if (url.pathname === "/graph" && req.method === "GET") {
4021
+ if (!settings.views.includes("graph"))
4022
+ return redirect(firstEnabledView());
4023
+ return htmlPage("graph", req, graphPageHtml);
4024
+ }
4025
+ if (url.pathname === "/" && req.method === "GET") {
4026
+ if (!settings.views.includes("board"))
4027
+ return redirect(firstEnabledView());
4028
+ ctx.detail = `${allTasks().length} tasks, ${allEpics().length} epics`;
4029
+ return htmlPage("board", req, () => pageHtml(allTasks(), allEpics()));
4030
+ }
4031
+ return new Response("Not found", { status: 404 });
4032
+ } catch (e) {
4033
+ return new Response(`Board error: ${e}`, { status: 500 });
4034
+ }
4035
+ }
4036
+ }
4037
+ var server = createServer(async (nodeReq, nodeRes) => {
4038
+ const req = new Request(new URL(nodeReq.url ?? "/", "http://localhost").toString(), {
4039
+ method: nodeReq.method,
4040
+ headers: nodeReq.headers,
4041
+ body: nodeReq.method === "GET" || nodeReq.method === "HEAD" ? undefined : Readable.toWeb(nodeReq),
4042
+ duplex: "half"
4043
+ });
4044
+ const ctx = { detail: "" };
4045
+ const res = await handle(req, ctx);
4046
+ const method = (nodeReq.method ?? "GET").padEnd(5);
4047
+ const where = res.headers.get("location") ? `-> ${res.headers.get("location")}` : ctx.detail;
4048
+ log(`${method} ${(nodeReq.url ?? "/").padEnd(34)} ${res.status}${where ? " " + where : ""}`);
4049
+ const headers = Object.fromEntries(res.headers);
4050
+ const type = headers["content-type"] ?? "";
4051
+ const compressible = /text\/(html|css)|application\/json/.test(type) && res.status === 200;
4052
+ const gzip = compressible && /\bgzip\b/.test(String(nodeReq.headers["accept-encoding"] ?? ""));
4053
+ if (gzip) {
4054
+ headers["content-encoding"] = "gzip";
4055
+ headers["vary"] = "accept-encoding";
4056
+ delete headers["content-length"];
4057
+ }
4058
+ nodeRes.writeHead(res.status, headers);
4059
+ if (gzip) {
4060
+ const out = createGzip({ level: 6 });
4061
+ out.pipe(nodeRes);
4062
+ out.end(Buffer.from(await res.arrayBuffer()));
4063
+ await new Promise((r) => nodeRes.on("close", r));
4064
+ return;
4065
+ }
4066
+ if (res.body) {
4067
+ const reader = res.body.getReader();
4068
+ nodeRes.on("close", () => reader.cancel().catch(() => {}));
4069
+ try {
4070
+ for (;; ) {
4071
+ const { done, value } = await reader.read();
4072
+ if (done)
4073
+ break;
4074
+ nodeRes.write(value);
4075
+ }
4076
+ } catch {}
4077
+ }
4078
+ nodeRes.end();
4079
+ });
4080
+ var PORT_SCAN = 20;
4081
+ var portAttempt = 0;
4082
+ server.on("error", (err) => {
4083
+ if (err.code !== "EADDRINUSE")
4084
+ throw err;
4085
+ if (++portAttempt > PORT_SCAN) {
4086
+ console.error(`Ports ${PORT}-${PORT + PORT_SCAN} are all in use. Free one or pass -p.`);
4087
+ process.exit(1);
4088
+ }
4089
+ server.listen(PORT + portAttempt);
4090
+ });
4091
+ server.on("listening", () => {
4092
+ const addr = server.address();
4093
+ const port = typeof addr === "object" && addr ? addr.port : PORT;
4094
+ banner(port, port !== PORT);
4095
+ });
4096
+ function banner(port, moved) {
4097
+ console.log(`${settings.title}: http://localhost:${port}`);
4098
+ console.log(` workspace ${PLANNER_DIR}`);
4099
+ console.log(` settings ${existsSync(SETTINGS_FILE) ? SETTINGS_FILE : `${SETTINGS_FILENAME} (absent, using defaults)`}`);
4100
+ console.log(` views ${settings.views.join(", ")}${existsSync(TASKS_DIR) ? "" : " (no tasks/ directory)"}`);
4101
+ console.log(` git ${gitEnabled() ? `auto-commit on${GIT_COMMIT_ENV ? " via BOARD_GIT_COMMIT" : ""}` : "off"}`);
4102
+ if (moved)
4103
+ console.log(` port ${PORT} was busy, using ${port}`);
4104
+ if (!QUIET)
4105
+ console.log(` log on (OUTCROP_QUIET=1 to silence)`);
4106
+ }
4107
+ server.listen(PORT);