@kitlangton/knobs 0.4.0-next.8 → 0.4.0-next.9

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/ui-solid.js CHANGED
@@ -4205,7 +4205,7 @@ function planPill(state, next, flags = {}) {
4205
4205
  if (t && t.x === next.x && t.y === next.y && t.w === next.w && t.h === next.h)
4206
4206
  return { kind: "stay" };
4207
4207
  const d = Math.hypot(next.x - state.at.x, next.y - state.at.y);
4208
- return { kind: "travel", box: next, spring: pillSpring(d) };
4208
+ return { kind: "travel", box: next, spring: pillSpring(d, flags.spring) };
4209
4209
  }
4210
4210
  function selectionPill(container, options) {
4211
4211
  const doc = container.ownerDocument;
@@ -4229,7 +4229,7 @@ function selectionPill(container, options) {
4229
4229
  container.prepend(pill);
4230
4230
  if (copyOn)
4231
4231
  container.append(copy);
4232
- const { omega, zeta } = springMotion(SPRINGS.default);
4232
+ const { omega, zeta } = springMotion(options.spring ?? SPRINGS.default);
4233
4233
  const springs = [0, 0, 0, 0].map(() => new Spring(0, omega, zeta, 0.01));
4234
4234
  const [sx, sy, sw, sh] = springs;
4235
4235
  let shown = false;
@@ -4330,7 +4330,8 @@ function selectionPill(container, options) {
4330
4330
  const plan = planPill({ shown, at: shown ? at() : null, target }, next, {
4331
4331
  first,
4332
4332
  reduced: reducedMotion(win),
4333
- ...o?.snap ? { snap: true } : {}
4333
+ ...o?.snap ? { snap: true } : {},
4334
+ ...options.spring ? { spring: options.spring } : {}
4334
4335
  });
4335
4336
  first = false;
4336
4337
  for (const n of clones.values())
@@ -11380,6 +11381,7 @@ function compare(container, options) {
11380
11381
  let loupeHeld = false;
11381
11382
  let pointer = null;
11382
11383
  let drag = null;
11384
+ let tap = null;
11383
11385
  const touches = new Map;
11384
11386
  let pinch = null;
11385
11387
  const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
@@ -12211,15 +12213,18 @@ function compare(container, options) {
12211
12213
  touches.set(e.pointerId, p);
12212
12214
  stage.setPointerCapture(e.pointerId);
12213
12215
  if (touches.size >= 2) {
12216
+ tap = null;
12214
12217
  e.preventDefault();
12215
12218
  return startPinch();
12216
12219
  }
12220
+ tap = e.target === handle ? null : { id: e.pointerId, x: p.x, y: p.y };
12217
12221
  if (mode === "wipe") {
12218
12222
  const wr = wipeRect(layoutNow().L.overlay);
12219
12223
  const inside = p.x >= wr.x - SLOP && p.x <= wr.x + wr.w + SLOP && p.y >= wr.y && p.y <= wr.y + wr.h;
12220
12224
  const grab = wipeGrab(p.x, wr, wipeLine.x, SLOP);
12221
12225
  if (inside && (grab.onLine || !zoomed())) {
12222
12226
  drag = { kind: "wipe", offset: grab.offset };
12227
+ tap = null;
12223
12228
  root.dataset.dragging = "";
12224
12229
  gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
12225
12230
  e.preventDefault();
@@ -12239,6 +12244,8 @@ function compare(container, options) {
12239
12244
  const onMove = (e) => {
12240
12245
  const p = local(e);
12241
12246
  pointer = p;
12247
+ if (tap && Math.hypot(p.x - tap.x, p.y - tap.y) > TAP_SLOP)
12248
+ tap = null;
12242
12249
  if (touches.has(e.pointerId))
12243
12250
  touches.set(e.pointerId, p);
12244
12251
  if (pinch && touches.size >= 2) {
@@ -12271,6 +12278,11 @@ function compare(container, options) {
12271
12278
  touches.delete(e.pointerId);
12272
12279
  if (touches.size < 2)
12273
12280
  pinch = null;
12281
+ if (tap?.id === e.pointerId) {
12282
+ tap = null;
12283
+ if (e.type === "pointerup" && sides.some((s) => s.kind === "video"))
12284
+ api.toggle();
12285
+ }
12274
12286
  if (!drag)
12275
12287
  return;
12276
12288
  if (drag.kind === "wipe") {
@@ -12672,7 +12684,7 @@ function compare(container, options) {
12672
12684
  wake();
12673
12685
  return api;
12674
12686
  }
12675
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12687
+ var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, TAP_SLOP = 4, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12676
12688
  var init_compare = __esm(() => {
12677
12689
  init_motion();
12678
12690
  init_page_hints();
@@ -12823,7 +12835,10 @@ function player(container, options) {
12823
12835
  return tl.time;
12824
12836
  return v.ended ? Number.POSITIVE_INFINITY : v.currentTime;
12825
12837
  });
12826
- const onClick = () => tl.focus();
12838
+ const onClick = () => {
12839
+ tl.focus();
12840
+ tl.toggle();
12841
+ };
12827
12842
  frame.addEventListener("click", onClick);
12828
12843
  v.src = options.src;
12829
12844
  const api = {
@@ -17298,1002 +17313,480 @@ function confirm(options) {
17298
17313
  // src/ui/walkthrough.ts
17299
17314
  init_icons();
17300
17315
  init_look();
17301
- init_selection_pill();
17302
17316
 
17303
- // src/ui/toast.ts
17304
- init_look();
17305
- init_layer();
17306
- var regions = new WeakMap;
17307
- function toast(message, options = {}) {
17308
- const doc = options.within?.ownerDocument ?? options.doc ?? document;
17309
- const win = doc.defaultView;
17310
- const key = options.within ?? doc;
17311
- let r = regions.get(key);
17312
- if (!r || !r.el.isConnected) {
17313
- const el = options.within ? Object.assign(doc.createElement("div"), { className: "knobs-root knobs-ui knobs-toast" }) : layerElement(doc, "knobs-layer knobs-toast", options.look);
17314
- if (options.within) {
17315
- injectUiStyles(doc);
17316
- options.within.append(el);
17317
+ // src/review/foundations.ts
17318
+ init_motion();
17319
+ var TRACKING = 0.03;
17320
+ var MEASURE = "66ch";
17321
+ var FONT = {
17322
+ sans: 'system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI Variable Text","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif',
17323
+ mono: 'ui-monospace,"SF Mono",SFMono-Regular,"Cascadia Mono","JetBrains Mono",Menlo,Consolas,monospace'
17324
+ };
17325
+ var FEATURES = {
17326
+ figures: "tabular-nums",
17327
+ data: '"tnum" 1,"ss01" 1',
17328
+ code: "none"
17329
+ };
17330
+ var MOTION2 = [
17331
+ {
17332
+ name: "hover",
17333
+ spring: null,
17334
+ ms: "0 in, 120 out",
17335
+ use: "A hover's fill: on at once, off softly"
17336
+ },
17337
+ {
17338
+ name: "quick",
17339
+ spring: FADE,
17340
+ use: "Replacing a whole thing in place: fade, scale .98, blur 3 px"
17341
+ },
17342
+ { name: "close", spring: CLOSE, use: "A popover closing" },
17343
+ { name: "snappy", spring: SPRINGS.snappy, use: "Controls: a toggle, a pick, a chapter row" },
17344
+ { name: "state", spring: SPRINGS.default, use: "The selection pill, a disclosure opening" },
17345
+ {
17346
+ name: "calm",
17347
+ spring: SPRINGS.calm,
17348
+ use: "Layout: a compare's height, the page scrolling to a step"
17349
+ },
17350
+ { name: "smooth", spring: SPRINGS.smooth, use: "A step opening to the full window" }
17351
+ ];
17352
+ var foundationCss = `
17353
+ :where(.knobs-walkthrough,.knobs-wt-slot,.knobs-review){
17354
+ --_kr-track:calc((var(--_k-type-body) - 1em) * ${TRACKING});
17355
+ --_kr-measure:var(--k-walkthrough-measure,${MEASURE});
17356
+ --_kr-block:calc(var(--_k-space-title) * 2);
17357
+ --_kr-data:${FEATURES.data};
17358
+ letter-spacing:var(--_kr-track);font-variant-numeric:${FEATURES.figures};font-optical-sizing:auto;font-kerning:normal}
17359
+ :where(.knobs-walkthrough,.knobs-wt-slot,.knobs-review) :where(*){letter-spacing:var(--_kr-track)}
17360
+ :where(.knobs-walkthrough,.knobs-wt-slot,.knobs-review) :where(code,pre,kbd,samp,.knobs-wt-mono){letter-spacing:0;font-variant-ligatures:${FEATURES.code}}
17361
+ `;
17362
+
17363
+ // src/themes/review.ts
17364
+ init_define();
17365
+
17366
+ // src/themes/void.ts
17367
+ init_define();
17368
+ init_knobs();
17369
+ var voidTheme = defineTheme({
17370
+ name: "void",
17371
+ extends: knobs,
17372
+ gray: { hue: 0, chroma: 0 },
17373
+ accent: { dark: "#ffffff", light: "#1c1c1c" },
17374
+ material: "flat",
17375
+ tokens: {
17376
+ dark: {
17377
+ "bg-page": "#000000",
17378
+ "bg-inset": "#0a0a0a",
17379
+ bg: "#000000",
17380
+ "bg-raised": "#1c1c1e",
17381
+ "bg-overlay": "#2c2c2e",
17382
+ fg: "#ededed",
17383
+ "fg-2": "#cecece",
17384
+ "fg-3": "#989898",
17385
+ "fg-4": "#7a7a7a",
17386
+ "compare-stage": "#000000"
17317
17387
  }
17318
- el.setAttribute("role", "status");
17319
- el.setAttribute("aria-live", "polite");
17320
- const text = doc.createElement("span");
17321
- text.className = "knobs-toast-text";
17322
- const button = doc.createElement("button");
17323
- button.type = "button";
17324
- button.className = "knobs-toast-action";
17325
- el.append(text, button);
17326
- const region = {
17327
- el,
17328
- text,
17329
- button,
17330
- motion: presence(el, { scale: 0.96, blur: 4, dy: 10, duration: 0.36 }),
17331
- timer: 0
17332
- };
17333
- r = region;
17334
- regions.set(key, region);
17335
- el.addEventListener("pointerenter", () => win.clearTimeout(region.timer));
17336
- el.addEventListener("pointerleave", () => {
17337
- if (region.motion.shown)
17338
- arm(region, 1600);
17339
- });
17340
17388
  }
17341
- const region = r;
17342
- const arm = (reg, ms) => {
17343
- win.clearTimeout(reg.timer);
17344
- reg.timer = win.setTimeout(() => void reg.motion.hide(), ms);
17345
- };
17346
- region.text.textContent = message;
17347
- region.el.dataset.tone = options.tone ?? "info";
17348
- const action = options.action;
17349
- region.button.hidden = !action;
17350
- region.button.textContent = action?.label ?? "";
17351
- region.button.onclick = action ? () => {
17352
- action.run();
17353
- region.motion.hide();
17354
- } : null;
17355
- region.motion.show();
17356
- arm(region, options.linger ?? (options.tone === "error" ? 6000 : 3200));
17357
- return {
17358
- dismiss() {
17359
- if (region.text.textContent !== message)
17360
- return;
17361
- win.clearTimeout(region.timer);
17362
- region.motion.hide();
17389
+ });
17390
+
17391
+ // src/themes/review.ts
17392
+ var reviewTheme = defineTheme({
17393
+ name: "review",
17394
+ extends: voidTheme,
17395
+ font: FONT,
17396
+ tokens: {
17397
+ dark: {
17398
+ fill: "#ededed0f",
17399
+ "fill-hover": "#ededed1a",
17400
+ "fill-selected": "#ededed24",
17401
+ "border-subtle": "#ededed47",
17402
+ border: "#ededed4f",
17403
+ "border-strong": "#585858"
17404
+ },
17405
+ light: {
17406
+ "bg-page": "#ffffff",
17407
+ "bg-inset": "#f7f7f7",
17408
+ bg: "#ffffff",
17409
+ "bg-raised": "#ffffff",
17410
+ "bg-overlay": "#ffffff",
17411
+ fill: "#1919190a",
17412
+ "fill-hover": "#19191912",
17413
+ "fill-selected": "#1919191c",
17414
+ "border-subtle": "#19191917",
17415
+ border: "#1919191d",
17416
+ "border-strong": "#dcdcdc"
17363
17417
  }
17364
- };
17418
+ }
17419
+ });
17420
+
17421
+ // src/ui/review-look.ts
17422
+ var MODES2 = { dark: "dark", light: "light", auto: "system" };
17423
+ var THEMED = '[data-knobs-theme]:not([data-knobs-theme="dark"],[data-knobs-theme="light"])';
17424
+ function reviewLook(options, container) {
17425
+ const t = options.theme;
17426
+ if (typeof t === "object")
17427
+ return options;
17428
+ if (!t && container?.closest(THEMED))
17429
+ return options;
17430
+ const appearance = options.appearance ?? (t ? MODES2[t] : undefined);
17431
+ return { ...options, theme: reviewTheme, ...appearance ? { appearance } : {} };
17365
17432
  }
17366
17433
 
17367
- // src/ui/walkthrough-annotations.ts
17368
- var EYE = "M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z";
17369
- var EYE_OFF = "M53.92,34.62A8,8,0,1,0,42.08,45.38L61.32,66.55C25,88.84,9.38,123.2,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208a127.11,127.11,0,0,0,52.07-10.83l22,24.21a8,8,0,1,0,11.84-10.76Zm47.33,75.84,41.67,45.85a32,32,0,0,1-41.67-45.85ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.16,133.16,0,0,1,25,128c4.69-8.79,19.66-33.39,47.35-49.38l18,19.75a48,48,0,0,0,63.66,70l14.73,16.2A112,112,0,0,1,128,192Zm6-95.43a8,8,0,0,1,3-15.72,48.16,48.16,0,0,1,38.77,42.64,8,8,0,0,1-7.22,8.71,6.39,6.39,0,0,1-.75,0,8,8,0,0,1-8-7.26A32.09,32.09,0,0,0,134,96.57Zm113.28,34.69c-.42.94-10.55,23.37-33.36,43.8a8,8,0,1,1-10.67-11.92A132.77,132.77,0,0,0,231.05,128a133.15,133.15,0,0,0-23.12-30.77C185.67,75.19,158.78,64,128,64a118.37,118.37,0,0,0-19.36,1.57A8,8,0,1,1,106,49.79,134,134,0,0,1,128,48c34.88,0,66.57,13.26,91.66,38.35,18.83,18.83,27.3,37.62,27.65,38.41A8,8,0,0,1,247.31,131.26Z";
17370
- var glyph = (d) => `<svg class="knobs-icon" viewBox="0 0 256 256" width="16" height="16" fill="currentColor" aria-hidden="true" focusable="false"><path d="${d}"/></svg>`;
17371
- var ARROW = '<svg class="knobs-wt-anno-arrow" viewBox="0 0 28 28" width="28" height="28" aria-hidden="true"><path d="M3 3 L24 24 M24 24 L24 13 M24 24 L13 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
17372
- var shapeOf = (a) => a.shape ?? (a.w !== undefined && a.h !== undefined ? "box" : "dot");
17373
- var showsAt = (a, time) => !a.at || time === undefined || time >= a.at[0] && time <= a.at[1];
17374
- function placeMark(a, rect, view = ALL) {
17375
- const w = a.w ?? 0;
17376
- const h = a.h ?? 0;
17377
- const x = (a.x - view.u) / view.w;
17378
- const y = (a.y - view.v) / view.h;
17379
- const ww = w / view.w;
17380
- const hh = h / view.h;
17381
- if (x + ww < 0 || y + hh < 0 || x > 1 || y > 1)
17382
- return null;
17383
- return { x: rect.x + x * rect.w, y: rect.y + y * rect.h, w: ww * rect.w, h: hh * rect.h };
17434
+ // src/ui/walkthrough-model.ts
17435
+ var DEFAULT_VERDICTS = ["ship", "change", "unsure"];
17436
+ function slug(text) {
17437
+ return text.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^(.{0,48})-.*$/, (m, head) => m.length > 48 && head ? head : m).slice(0, 48) || "item";
17384
17438
  }
17385
- var ALL = { u: 0, v: 0, w: 1, h: 1 };
17386
- var scopeOf = (el) => el.closest(".knobs-walkthrough") ?? el.closest(".knobs-wt-slot") ?? el;
17387
- var annotationsOn = (scope) => scope.dataset.annotations !== "off";
17388
- function setAnnotations(scope, on) {
17389
- if (on)
17390
- delete scope.dataset.annotations;
17391
- else
17392
- scope.dataset.annotations = "off";
17393
- for (const b of Array.from(scope.querySelectorAll(".knobs-wt-anno-toggle")))
17394
- syncToggle(b, on);
17439
+ function uniqueIds() {
17440
+ const seen = new Map;
17441
+ return (id) => {
17442
+ const n = (seen.get(id) ?? 0) + 1;
17443
+ seen.set(id, n);
17444
+ return n === 1 ? id : `${id}-${n}`;
17445
+ };
17395
17446
  }
17396
- function syncToggle(b, on) {
17397
- b.setAttribute("aria-pressed", String(on));
17398
- b.title = on ? "Hide annotations (a)" : "Show annotations (a)";
17399
- b.innerHTML = glyph(on ? EYE : EYE_OFF);
17447
+ var label = (id) => id.charAt(0).toUpperCase() + id.slice(1).replace(/[-_]+/g, " ");
17448
+ function resolveSrc(src, base) {
17449
+ if (!base || /^([a-z][a-z0-9+.-]*:|\/\/|\/)/i.test(src))
17450
+ return src;
17451
+ return base.replace(/\/?$/, "/") + src.replace(/^\.\//, "");
17400
17452
  }
17401
- function annotationLayer(host, list, place, corner = host) {
17402
- const doc = host.ownerDocument;
17403
- const layer = doc.createElement("div");
17404
- layer.className = "knobs-wt-anno";
17405
- layer.setAttribute("aria-hidden", "true");
17406
- host.append(layer);
17407
- const toggle = doc.createElement("button");
17408
- toggle.type = "button";
17409
- toggle.className = "knobs-wt-anno-toggle";
17410
- toggle.setAttribute("aria-label", "Annotations");
17411
- syncToggle(toggle, annotationsOn(scopeOf(host)));
17412
- toggle.addEventListener("click", (e) => {
17413
- e.stopPropagation();
17414
- const scope = scopeOf(host);
17415
- setAnnotations(scope, !annotationsOn(scope));
17416
- });
17417
- corner.append(toggle);
17418
- let marks = [];
17419
- const make = (a) => {
17420
- const m = doc.createElement("div");
17421
- const shape = shapeOf(a);
17422
- m.className = "knobs-wt-mark";
17423
- m.dataset.shape = shape;
17424
- m.dataset.hidden = "";
17425
- if (shape === "arrow")
17426
- m.innerHTML = ARROW;
17427
- if (a.label) {
17428
- const l = doc.createElement("span");
17429
- l.className = "knobs-wt-mark-label";
17430
- l.textContent = a.label;
17431
- m.append(l);
17432
- }
17433
- layer.append(m);
17434
- return m;
17435
- };
17436
- let settled = false;
17437
- const update = () => {
17438
- const p = place();
17439
- if (marks.length !== p.rects.length) {
17440
- for (const row of marks)
17441
- for (const m of row)
17442
- m.remove();
17443
- marks = p.rects.map(() => list.map(make));
17444
- }
17445
- p.rects.forEach((rect, i) => {
17446
- list.forEach((a, j) => {
17447
- const m = marks[i][j];
17448
- const box = placeMark(a, rect, p.view);
17449
- const show = box !== null && showsAt(a, p.time);
17450
- if (box) {
17451
- m.style.transform = `translate(${box.x.toFixed(2)}px,${box.y.toFixed(2)}px)`;
17452
- m.style.width = `${box.w.toFixed(2)}px`;
17453
- m.style.height = `${box.h.toFixed(2)}px`;
17454
- }
17455
- m.toggleAttribute("data-hidden", !show);
17456
- });
17457
- });
17458
- if (!settled) {
17459
- settled = true;
17460
- requestAnimationFrame(() => requestAnimationFrame(() => layer.dataset.settled = ""));
17453
+ var VIDEO2 = /\.(mp4|m4v|webm|mov|ogv)(\?|#|$)/i;
17454
+ var mediaOf = (src) => VIDEO2.test(src) ? { kind: "video", src } : { kind: "image", src };
17455
+ function parseAspect(aspect) {
17456
+ if (aspect === undefined)
17457
+ return;
17458
+ if (typeof aspect === "number")
17459
+ return aspect > 0 && Number.isFinite(aspect) ? aspect : undefined;
17460
+ const m = /^\s*([\d.]+)\s*(?:[/x:×]\s*([\d.]+))?\s*$/.exec(aspect);
17461
+ if (!m)
17462
+ return;
17463
+ const w = Number(m[1]);
17464
+ const h = m[2] === undefined ? 1 : Number(m[2]);
17465
+ return w > 0 && h > 0 ? w / h : undefined;
17466
+ }
17467
+ function source2(s, base) {
17468
+ const v = typeof s === "string" ? { src: s } : s;
17469
+ return { ...v, src: resolveSrc(v.src, base) };
17470
+ }
17471
+ function fillMedia(m, base) {
17472
+ if (typeof m === "string")
17473
+ return mediaOf(resolveSrc(m, base));
17474
+ switch (m.kind) {
17475
+ case "compare": {
17476
+ const { chapters, ...rest } = m;
17477
+ return {
17478
+ ...rest,
17479
+ before: source2(m.before, base),
17480
+ after: source2(m.after, base),
17481
+ ...chapters?.length ? { chapters: fillChapters(chapters) } : {}
17482
+ };
17461
17483
  }
17462
- };
17463
- const ro = new ResizeObserver(() => update());
17464
- ro.observe(host);
17465
- update();
17466
- return {
17467
- element: layer,
17468
- update,
17469
- dispose() {
17470
- ro.disconnect();
17471
- layer.remove();
17472
- toggle.remove();
17484
+ case "video": {
17485
+ const { chapters, captionsSrc, captions, ...rest } = m;
17486
+ const vtt = captions ?? captionsSrc;
17487
+ return {
17488
+ ...rest,
17489
+ src: resolveSrc(m.src, base),
17490
+ ...m.poster ? { poster: resolveSrc(m.poster, base) } : {},
17491
+ ...vtt ? { captions: resolveSrc(vtt, base) } : {},
17492
+ ...chapters?.length ? { chapters: fillChapters(chapters) } : {}
17493
+ };
17473
17494
  }
17474
- };
17475
- }
17476
-
17477
- // src/ui/walkthrough-blocks.ts
17478
- init_motion();
17479
-
17480
- // src/ui/history.ts
17481
- init_motion();
17482
- init_glide();
17483
-
17484
- // src/ui/history-css.ts
17485
- var historyCss = `
17486
- :where(.knobs-history,.knobs-history *){box-sizing:border-box}
17487
- .knobs-ui.knobs-history{--hy-gutter:128px;--hy-lane:24px;--hy-bar:16px;--hy-axis:20px;--hy-bandrow:0px;--hy-c:var(--_k-fg-3);
17488
- --hy-fade:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);
17489
- position:relative;display:flex;flex-direction:column;width:100%;min-width:0;outline:none;color:var(--_k-fg);
17490
- font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";
17491
- text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased;user-select:none;-webkit-user-select:none}
17492
- .knobs-history[data-bands]{--hy-bandrow:var(--_k-leading-small)}
17493
- .knobs-history[data-narrow]{--hy-gutter:0px}
17494
- .knobs-history [hidden]{display:none!important}
17495
- :where(.knobs-ui) :is(.knobs-hy-ev,.knobs-hy-span,.knobs-hy-jump,.knobs-hy-more){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
17496
- background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
17497
- .knobs-history [data-tone="good"]{--hy-c:var(--_k-ok)}
17498
- .knobs-history [data-tone="warn"]{--hy-c:var(--_k-warn)}
17499
- .knobs-history [data-tone="bad"]{--hy-c:var(--_k-danger)}
17500
- .knobs-history [data-tone="info"]{--hy-c:var(--_k-info)}
17501
- .knobs-history [data-tone="dim"]{--hy-c:var(--_k-fg-3)}
17502
-
17503
- /* ——— the chart: lane labels beside the plot (above each lane where it's narrow) ——— */
17504
- .knobs-hy-chart{position:relative;display:grid;grid-template-columns:var(--hy-gutter) minmax(0,1fr)}
17505
- .knobs-hy-axis{position:relative;grid-column:2;grid-row:1;height:calc(var(--hy-bandrow) + var(--hy-axis))}
17506
- :is(.knobs-hy-tick,.knobs-hy-now,.knobs-hy-blabel){position:absolute;white-space:nowrap;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17507
- .knobs-hy-tick{bottom:2px;padding-left:4px}
17508
- .knobs-hy-now{bottom:2px;color:var(--_k-accent-fg);font-weight:var(--_k-weight-label)}
17509
- .knobs-hy-blabel{top:0;padding-left:4px;overflow:hidden;color:var(--_k-fg-2)}
17510
- .knobs-hy-label{grid-column:1;align-self:center;min-width:0;margin-right:var(--_k-space-title);overflow:hidden;white-space:nowrap;
17511
- font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17512
- .knobs-history[data-narrow] .knobs-hy-label{grid-column:2;align-self:end;margin:0}
17513
- :is(.knobs-hy-label,.knobs-hy-blabel)[data-overflow]{-webkit-mask-image:var(--hy-fade);mask-image:var(--hy-fade)}
17514
- .knobs-hy-track{position:relative;grid-column:2;height:var(--hy-lane)}
17515
- .knobs-hy-layer{position:absolute;inset:0;grid-column:2;grid-row:1/-1;pointer-events:none}
17516
-
17517
- /* Bands and month lines behind the lanes; today, the selection and the pointer's line over them. */
17518
- .knobs-hy-band{position:absolute;top:0;bottom:0;background:color-mix(in oklch,var(--_k-fg) 3.5%,transparent);box-shadow:inset 1px 0 0 var(--_k-border-subtle)}
17519
- :is(.knobs-hy-grid,.knobs-hy-today,.knobs-hy-hover,.knobs-hy-sel){position:absolute;left:0;top:calc(var(--hy-bandrow) + var(--hy-axis));bottom:0;width:1px;margin-left:-.5px}
17520
- .knobs-hy-grid{background:var(--_k-border-subtle)}
17521
- .knobs-hy-today{top:calc(var(--hy-bandrow) + var(--hy-axis) - 4px);background:var(--_k-accent)}
17522
- .knobs-hy-hover{background:color-mix(in oklch,var(--_k-fg) 32%,transparent);opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
17523
- .knobs-hy-hover[data-on]{opacity:1;transition-duration:calc(var(--_k-hover-in) * 1ms)}
17524
- .knobs-hy-sel{background:var(--_k-fg-2);opacity:0}
17525
- .knobs-hy-sel[data-on]{opacity:.6}
17526
-
17527
- /* Spans: a soft bar in their state's tone, inferred ones hatched; the tag at the end. */
17528
- .knobs-hy-span{position:absolute;top:calc((var(--hy-lane) - var(--hy-bar)) / 2);height:var(--hy-bar);border-radius:var(--_k-radius-sm);cursor:pointer;
17529
- background:color-mix(in oklch,var(--hy-c) 24%,transparent);transition:background calc(var(--_k-hover-out) * 1ms),box-shadow calc(var(--_k-hover-out) * 1ms)}
17530
- .knobs-hy-span[data-inferred]{background:repeating-linear-gradient(135deg,color-mix(in oklch,var(--hy-c) 34%,transparent) 0 2px,color-mix(in oklch,var(--hy-c) 9%,transparent) 2px 6px)}
17531
- .knobs-hy-span[data-hover]{background-color:color-mix(in oklch,var(--hy-c) 34%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17532
- .knobs-hy-span[data-selected]{box-shadow:inset 0 0 0 1px var(--hy-c)}
17533
- .knobs-hy-span[data-clip-start]{border-top-left-radius:0;border-bottom-left-radius:0;-webkit-mask-image:linear-gradient(to right,transparent,#000 16px);mask-image:linear-gradient(to right,transparent,#000 16px)}
17534
- .knobs-hy-span[data-clip-end]{border-top-right-radius:0;border-bottom-right-radius:0;-webkit-mask-image:linear-gradient(to left,transparent,#000 16px);mask-image:linear-gradient(to left,transparent,#000 16px)}
17535
- .knobs-hy-tag{position:absolute;top:0;right:6px;font-size:var(--_k-type-small);line-height:var(--hy-bar);white-space:nowrap;pointer-events:none;
17536
- color:color-mix(in oklch,var(--hy-c) 55%,var(--_k-fg))}
17537
- .knobs-hy-tag[data-place="after"]{right:auto;left:calc(100% + 6px)}
17538
- .knobs-hy-tag[data-place="none"]{visibility:hidden}
17539
-
17540
- /* Event marks (their kind named in the tooltip): a dot for a commit, a diamond for a release, a cross for a deletion, a square for a
17541
- channel's switch, a triangle for a finding; inferred ones hollow. */
17542
- .knobs-hy-ev{position:absolute;top:0;width:16px;height:var(--hy-lane);margin-left:-8px;cursor:pointer;--hy-c:var(--_k-fg-2);--hy-s:9px}
17543
- .knobs-hy-ev::before{content:"";position:absolute;left:50%;top:50%;width:var(--hy-s);height:var(--hy-s);margin:calc(var(--hy-s) / -2) 0 0 calc(var(--hy-s) / -2);
17544
- border-radius:50%;background:var(--hy-c);transition:scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
17545
- .knobs-hy-ev[data-range]{width:calc(var(--hy-w) + 16px)}
17546
- .knobs-hy-ev[data-range]::before{left:calc(8px - var(--hy-s) / 2);right:calc(8px - var(--hy-s) / 2);width:auto;margin-left:0;border-radius:var(--_k-round)}
17547
- .knobs-hy-ev[data-type="release"]{--hy-s:8px}
17548
- .knobs-hy-ev[data-type="release"]::before{border-radius:1.5px;rotate:45deg}
17549
- .knobs-hy-ev[data-type="channel"]{--hy-s:8px}
17550
- .knobs-hy-ev[data-type="channel"]::before{border-radius:2px}
17551
- .knobs-hy-ev[data-type="audit"]{--hy-s:10px}
17552
- .knobs-hy-ev[data-type="audit"]::before{border-radius:0;clip-path:polygon(50% 4%,100% 92%,0 92%)}
17553
- .knobs-hy-ev[data-type="deletion"]::before{border-radius:0;rotate:45deg;
17554
- background:linear-gradient(var(--hy-c),var(--hy-c)) center/100% 1.75px no-repeat,linear-gradient(var(--hy-c),var(--hy-c)) center/1.75px 100% no-repeat}
17555
- .knobs-hy-ev[data-inferred]:not([data-type="audit"],[data-type="deletion"])::before{background:transparent;box-shadow:inset 0 0 0 1.5px var(--hy-c)}
17556
- .knobs-hy-ev[data-inferred]:is([data-type="audit"],[data-type="deletion"])::before{opacity:.5}
17557
- /* Held-back marks (commits, by default) come up while the pointer is on their lane, and stay with
17558
- "Show commits" or while selected. */
17559
- .knobs-hy-ev[data-quiet]{opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
17560
- .knobs-hy-track:hover .knobs-hy-ev[data-quiet]{opacity:.6;transition-duration:calc(var(--_k-motion-snappy-ms) * 1ms)}
17561
- :is(.knobs-history[data-all] .knobs-hy-ev[data-quiet],.knobs-hy-ev[data-quiet]:is([data-selected],[data-hover])){opacity:1}
17562
- .knobs-hy-ev[data-hover]::before{scale:1.25}
17563
- .knobs-hy-ev[data-selected]::before{scale:1.3}
17564
- .knobs-hy-ev[data-selected]::after{content:"";position:absolute;left:50%;top:50%;width:calc(var(--hy-s) + 9px);height:calc(var(--hy-s) + 9px);
17565
- margin:calc((var(--hy-s) + 9px) / -2) 0 0 calc((var(--hy-s) + 9px) / -2);border-radius:50%;box-shadow:0 0 0 1.5px var(--hy-c)}
17566
- .knobs-hy-ev[data-range][data-selected]::after{left:3px;right:3px;width:auto;margin-left:0;border-radius:var(--_k-round)}
17567
- .knobs-history:focus-visible .knobs-hy-ev[data-selected]::after{box-shadow:0 0 0 2px var(--_k-focus)}
17568
- .knobs-history:focus-visible .knobs-hy-span[data-selected]{box-shadow:inset 0 0 0 2px var(--_k-focus)}
17569
-
17570
- /* ——— under the chart: "Show commits", and a legend only when inferred and verified both appear ——— */
17571
- .knobs-hy-foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--_k-space-inside) var(--_k-space-title);margin-top:var(--_k-space-sibling);padding-left:var(--hy-gutter);
17572
- font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17573
- .knobs-hy-legend{display:flex;flex-wrap:wrap;gap:var(--_k-space-inside) var(--_k-space-title)}
17574
- .knobs-ui .knobs-hy-more{height:20px;margin-left:-6px;padding:0 6px;border-radius:var(--_k-radius-sm);color:var(--_k-fg-3);cursor:pointer;
17575
- transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
17576
- .knobs-ui .knobs-hy-more:is(:hover,:focus-visible){background:var(--_k-fill);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17577
- .knobs-hy-key{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
17578
- .knobs-hy-swatch{flex:none;width:14px;height:8px;border-radius:2px;background:color-mix(in oklch,var(--hy-c) 34%,transparent)}
17579
- .knobs-hy-swatch[data-inferred]{background:repeating-linear-gradient(135deg,color-mix(in oklch,var(--hy-c) 60%,transparent) 0 1.5px,transparent 1.5px 4px)}
17580
-
17581
- /* ——— the detail: one area under the chart, its height on a spring ——— */
17582
- .knobs-hy-detail{position:relative;overflow:hidden;user-select:text;-webkit-user-select:text}
17583
- .knobs-hy-detail[data-moving]{-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent)}
17584
- .knobs-hy-dc{padding:var(--_k-space-title) 0 0 var(--hy-gutter);transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
17585
- .knobs-hy-dc:is([data-enter],[data-leave]){opacity:0}
17586
- .knobs-hy-dc[data-leave]{position:absolute;left:0;right:0;top:0;transition-duration:calc(var(--_k-motion-snappy-ms) * .8ms)}
17587
- .knobs-hy-dmeta{display:flex;flex-wrap:wrap;gap:0 var(--_k-space-sibling);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17588
- .knobs-hy-dmeta>[data-tone]{color:var(--hy-c)}
17589
- .knobs-hy-dtitle{margin-top:var(--_k-space-inside);font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-weight:var(--_k-weight-title);color:var(--_k-fg);text-wrap:balance}
17590
- .knobs-hy-dtext{margin:var(--_k-space-inside) 0 0;max-width:68ch;font-size:var(--_k-type-body);line-height:var(--_k-leading-body);color:var(--_k-fg-2);text-wrap:pretty}
17591
- .knobs-hy-dtext code{font:calc(1em - 1px) var(--_k-mono);padding:1px 4px;border-radius:var(--_k-radius-sm);background:var(--_k-fill)}
17592
- .knobs-hy-dfoot{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:var(--_k-space-sibling)}
17593
- .knobs-hy-dfoot:empty{display:none}
17594
- .knobs-hy-evidence{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:var(--_k-radius-sm);background:var(--_k-fill);color:var(--_k-fg-2);
17595
- font:var(--_k-type-small)/1 var(--_k-mono);text-decoration:none;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
17596
- a.knobs-hy-evidence:hover{background:var(--_k-fill-hover);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17597
- .knobs-ui .knobs-hy-jump{display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 6px 0 8px;border-radius:var(--_k-radius-sm);color:var(--_k-fg-2);cursor:pointer;
17598
- font-size:var(--_k-type-small);transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
17599
- .knobs-ui .knobs-hy-jump:is(:hover,:focus-visible){background:var(--_k-fill);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17600
- .knobs-hy-jump svg{width:12px;height:12px}
17601
-
17602
- /* ——— the pointer's tooltip: title, date, one line ——— */
17603
- .knobs-hy-tip{position:absolute;left:0;top:0;max-width:280px;padding:6px 8px;border-radius:var(--_k-radius-md);background:var(--_k-bg-overlay);
17604
- box-shadow:var(--_k-shadow-overlay),0 0 0 1px var(--_k-border);pointer-events:none;opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
17605
- .knobs-hy-tip[data-on]{opacity:1;transition-duration:calc(var(--_k-hover-in) * 1ms)}
17606
- .knobs-hy-tt{font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight-label);color:var(--_k-fg);text-wrap:balance}
17607
- :is(.knobs-hy-td,.knobs-hy-tl){font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17608
- .knobs-hy-tl{margin-top:2px;color:var(--_k-fg-2);text-wrap:pretty}
17609
- .knobs-hy-tl:empty{display:none}
17610
-
17611
- @media(prefers-reduced-motion:reduce){
17612
- .knobs-history :where(*,::before,::after){transition-property:opacity,background,color,box-shadow!important}
17613
- .knobs-hy-dc{transition-duration:calc(var(--_k-hover-out) * 1ms)!important}
17614
- }
17615
- `;
17616
-
17617
- // src/ui/history-model.ts
17618
- var MS_DAY = 86400000;
17619
- var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
17620
- var dayOf = (y, m, d) => Math.floor(Date.UTC(y, m, d) / MS_DAY);
17621
- var parts = (day) => {
17622
- const t = new Date(day * MS_DAY);
17623
- return { y: t.getUTCFullYear(), m: t.getUTCMonth(), d: t.getUTCDate() };
17624
- };
17625
- var todayDay = (now = Date.now()) => {
17626
- const t = new Date(now);
17627
- return dayOf(t.getFullYear(), t.getMonth(), t.getDate());
17628
- };
17629
- var monthIndex = (name) => MONTHS.findIndex((m) => m.toLowerCase() === name.slice(0, 3).toLowerCase());
17630
- function parseDay(text, year) {
17631
- const s = text.trim();
17632
- const iso = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
17633
- if (iso) {
17634
- const y = Number(iso[1]);
17635
- const day = dayOf(y, Number(iso[2]) - 1, Number(iso[3]));
17636
- return { from: day, to: day, year: y };
17495
+ case "image":
17496
+ return { ...m, src: resolveSrc(m.src, base) };
17497
+ case "history":
17498
+ if ("data" in m)
17499
+ return m;
17500
+ return {
17501
+ kind: "history",
17502
+ data: historyData(m),
17503
+ ...m.label ? { label: m.label } : {},
17504
+ ...m.marks ? { marks: m.marks } : {}
17505
+ };
17506
+ case "stack":
17507
+ return {
17508
+ kind: "stack",
17509
+ ...m.base ? { base: m.base } : {},
17510
+ items: m.items.map(fillStackItem)
17511
+ };
17512
+ case "table":
17513
+ return fillTable(m);
17514
+ case "facts":
17515
+ return {
17516
+ kind: "facts",
17517
+ ...m.title ? { title: m.title } : {},
17518
+ groups: [
17519
+ ...m.facts ? [{ facts: fillFacts(m.facts) }] : [],
17520
+ ...(m.groups ?? []).map((g) => ({
17521
+ ...g.title ? { title: g.title } : {},
17522
+ facts: fillFacts(g.facts)
17523
+ }))
17524
+ ]
17525
+ };
17526
+ case "decision":
17527
+ return { kind: "decision", options: fillDecision(m.options, m.noun, base) };
17528
+ default:
17529
+ return m;
17637
17530
  }
17638
- const m = /^([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:\s*[–—-]\s*(?:([A-Za-z]{3,9})\.?\s+)?(\d{1,2}))?(?:,?\s+(\d{4}))?$/.exec(s);
17639
- if (!m)
17640
- return null;
17641
- const m1 = monthIndex(m[1]);
17642
- const m2 = m[3] ? monthIndex(m[3]) : m1;
17643
- if (m1 < 0 || m2 < 0)
17644
- return null;
17645
- const written = m[5] ? Number(m[5]) : undefined;
17646
- const y = written ?? year;
17647
- if (y === undefined)
17648
- return null;
17649
- const from = dayOf(y, m1, Number(m[2]));
17650
- let to = m[4] ? dayOf(y, m2, Number(m[4])) : from;
17651
- if (to < from)
17652
- to = dayOf(y + 1, m2, Number(m[4]));
17653
- return { from, to, ...written !== undefined ? { year: written } : {} };
17654
17531
  }
17655
- function formatDay(day) {
17656
- const { y, m, d } = parts(day);
17657
- return `${MONTHS[m]} ${d}, ${y}`;
17532
+ function fillFacts(facts) {
17533
+ const list = Array.isArray(facts) ? facts : Object.entries(facts).map(([term, value]) => ({ term, value }));
17534
+ return list.map((f) => {
17535
+ const code = f.code === true;
17536
+ const raw = typeof f.value === "string" ? [f.value] : [...f.value];
17537
+ const value = (code ? raw.flatMap((v) => v.split(/\s*,\s*/)) : raw).map((v) => v.trim()).filter(Boolean);
17538
+ return { term: f.term, value, code };
17539
+ });
17658
17540
  }
17659
- function formatRange(from, to, open = false) {
17660
- if (open)
17661
- return `${formatDay(from)} – today`;
17662
- if (to <= from)
17663
- return formatDay(from);
17664
- const a = parts(from);
17665
- const b = parts(to);
17666
- if (a.y === b.y && a.m === b.m)
17667
- return `${MONTHS[a.m]} ${a.d}–${b.d}, ${a.y}`;
17668
- if (a.y === b.y)
17669
- return `${MONTHS[a.m]} ${a.d} – ${MONTHS[b.m]} ${b.d}, ${a.y}`;
17670
- return `${formatDay(from)} – ${formatDay(to)}`;
17541
+ function optionColumns(count, width, min = 280, gap = 48) {
17542
+ for (let c = Math.min(count, 4);c > 1; c--)
17543
+ if (count % c === 0 && (width - gap * (c - 1)) / c >= min)
17544
+ return c;
17545
+ return 1;
17546
+ }
17547
+ function fillDecision(options, noun = "Option", base) {
17548
+ const unique = uniqueIds();
17549
+ return options.map((o, i) => {
17550
+ if ("number" in o)
17551
+ return o;
17552
+ const extra = [
17553
+ ...o.pros?.length ? [{ term: "For", value: o.pros }] : [],
17554
+ ...o.cons?.length ? [{ term: "Against", value: o.cons }] : []
17555
+ ];
17556
+ return {
17557
+ id: unique(o.id ?? slug(o.title)),
17558
+ number: `${noun} ${i + 1}`,
17559
+ title: o.title,
17560
+ ...o.summary ? { summary: o.summary } : {},
17561
+ ...o.media !== undefined ? { media: fillMedia(o.media, base) } : {},
17562
+ facts: [...o.facts ? fillFacts(o.facts) : [], ...fillFacts(extra)],
17563
+ recommended: o.recommended === true
17564
+ };
17565
+ });
17671
17566
  }
17672
- var formatDays = (n) => `${n} ${n === 1 ? "day" : "days"}`;
17673
- function guessTone(state) {
17674
- const s = state.toLowerCase();
17675
- if (/broken|bad|fail|missing|deleted|down/.test(s))
17676
- return "bad";
17677
- if (/warn|redirect|partial|beta|next|pre/.test(s))
17678
- return "warn";
17679
- if (/work|ok|good|fixed|live|green/.test(s))
17680
- return "good";
17681
- return "dim";
17567
+ function historyData(m) {
17568
+ const events = m.events ?? [];
17569
+ if (m.lanes || m.bands || m.states || m.axis)
17570
+ return {
17571
+ ...m.axis ? { axis: m.axis } : {},
17572
+ ...m.lanes ? { lanes: m.lanes } : {},
17573
+ ...events.length ? { events } : {},
17574
+ ...m.bands ? { bands: m.bands } : {},
17575
+ ...m.states ? { states: m.states } : {}
17576
+ };
17577
+ const groups = [];
17578
+ if (events.length)
17579
+ groups.push({ events });
17580
+ for (const g of m.groups ?? [])
17581
+ groups.push({
17582
+ ...g.heading ? { heading: g.heading } : {},
17583
+ ...g.summary ? { summary: g.summary } : {},
17584
+ events: g.events ?? g.rows ?? []
17585
+ });
17586
+ return { groups };
17682
17587
  }
17683
- var bandLabel = (heading) => heading.replace(/^\s*\d+\s*[·.:)\-–]\s*/, "").replace(/\s*\([^)]*\)\s*$/, "").trim();
17684
- function fromGroups(groups, today) {
17685
- const events = [];
17686
- const bands = [];
17687
- const all = groups.flatMap((g) => g.events ?? g.rows ?? []);
17688
- let year = all.map((e) => parseDay(e.date, 0)?.year).find((y) => y !== undefined) ?? parts(today).y;
17689
- for (const g of groups) {
17690
- let lo = Infinity;
17691
- let hi = -Infinity;
17692
- for (const e of g.events ?? g.rows ?? []) {
17693
- const d = parseDay(e.date, year);
17694
- if (!d)
17695
- continue;
17696
- if (d.year !== undefined)
17697
- year = d.year;
17698
- lo = Math.min(lo, d.from);
17699
- hi = Math.max(hi, d.to);
17700
- events.push({
17701
- ...e,
17702
- date: iso(d.from),
17703
- ...d.to > d.from ? { to: iso(d.to) } : {},
17704
- lane: e.lane ?? "code"
17705
- });
17706
- }
17707
- const label = g.heading ? bandLabel(g.heading) : "";
17708
- if (label && lo <= hi)
17709
- bands.push({ from: iso(lo), to: iso(hi), label });
17710
- }
17711
- return { events, bands };
17588
+ function parseBranch(branch) {
17589
+ if (!branch?.trim())
17590
+ return {};
17591
+ const [head, base] = branch.split(/\s*(?:→|->|=>|⇒)\s*/, 2).map((s) => s.trim());
17592
+ return { ...head ? { head } : {}, ...base ? { base } : {} };
17712
17593
  }
17713
- function iso(day) {
17714
- const { y, m, d } = parts(day);
17715
- return `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
17594
+ function parseDelta(delta) {
17595
+ if (delta === undefined || typeof delta === "object")
17596
+ return delta;
17597
+ const m = /\+\s*([\d,_]+)[^\d+]*?[-−–]\s*([\d,_]+)/.exec(delta);
17598
+ if (!m)
17599
+ return delta.trim() || undefined;
17600
+ const n = (s) => Number(s.replace(/[,_]/g, ""));
17601
+ return { add: n(m[1]), del: n(m[2]) };
17716
17602
  }
17717
- var startOfMonth = (day) => {
17718
- const { y, m } = parts(day);
17719
- return dayOf(y, m, 1);
17720
- };
17721
- var TYPE_TONE = { deletion: "bad" };
17722
- function normalizeHistory(data, now = todayDay()) {
17723
- const today = data.axis?.today && parseDay(data.axis.today, parts(now).y)?.from || now;
17724
- const converted = data.groups?.length ? fromGroups(data.groups, today) : { events: [], bands: [] };
17725
- const rawEvents = [...data.events ?? [], ...converted.events];
17726
- const rawBands = [...data.bands ?? [], ...converted.bands];
17727
- const year = parts(today).y;
17728
- const day = (s) => s ? parseDay(s, year) ?? null : null;
17729
- const states = new Map;
17730
- const stateOf = (key) => {
17731
- const known = states.get(key);
17732
- if (known)
17733
- return known;
17734
- const def = data.states?.[key];
17735
- const s = { key, label: def?.label ?? key, tone: def?.tone ?? guessTone(key) };
17736
- states.set(key, s);
17737
- return s;
17603
+ function fillStackItem(it) {
17604
+ const parsed = parseBranch(it.branch);
17605
+ const head = parsed.head ?? it.head;
17606
+ const base = parsed.base ?? it.base;
17607
+ const delta = parseDelta(it.delta);
17608
+ const step = it.step ?? it.stepId;
17609
+ const pr = String(it.pr).trim();
17610
+ return {
17611
+ pr: /^\d+$/.test(pr) ? `#${pr}` : pr,
17612
+ ...it.href ? { href: it.href } : {},
17613
+ title: it.title,
17614
+ ...head ? { head } : {},
17615
+ ...base ? { base } : {},
17616
+ ...delta !== undefined ? { delta } : {},
17617
+ ...it.status ? { status: it.status } : {},
17618
+ ...it.tone ? { tone: it.tone } : {},
17619
+ ...it.summary ? { summary: it.summary } : {},
17620
+ ...step ? { step } : {},
17621
+ ...it.order !== undefined && it.order !== "" ? { order: String(it.order) } : {}
17738
17622
  };
17739
- const items = new Map;
17740
- const laneOrder = [];
17741
- const lanes = new Map;
17742
- const lane = (id, label = id === "code" ? "Code" : id) => {
17743
- let l = lanes.get(id);
17744
- if (!l) {
17745
- l = { id, label, spans: [], events: [] };
17746
- lanes.set(id, l);
17747
- laneOrder.push(id);
17623
+ }
17624
+ function stackLayout(m) {
17625
+ const items = m.items;
17626
+ const byHead = new Map;
17627
+ items.forEach((it, i) => it.head && !byHead.has(it.head) && byHead.set(it.head, i));
17628
+ let trunk = m.base ?? null;
17629
+ if (!trunk) {
17630
+ const tally = new Map;
17631
+ for (const it of items)
17632
+ if (it.base && !byHead.has(it.base))
17633
+ tally.set(it.base, (tally.get(it.base) ?? 0) + 1);
17634
+ let best = 0;
17635
+ for (const [b, n] of tally)
17636
+ if (n > best)
17637
+ [trunk, best] = [b, n];
17638
+ }
17639
+ const parentOf = items.map((it, i) => {
17640
+ const p = it.base === undefined ? undefined : byHead.get(it.base);
17641
+ return p === undefined || p === i ? null : p;
17642
+ });
17643
+ items.forEach((_, i) => {
17644
+ const seen = new Set;
17645
+ for (let n = i;n !== null; n = parentOf[n]) {
17646
+ if (seen.has(n)) {
17647
+ const loop = [n];
17648
+ for (let k = parentOf[n];k !== n && k !== null; k = parentOf[k])
17649
+ loop.push(k);
17650
+ parentOf[Math.min(...loop)] = null;
17651
+ break;
17652
+ }
17653
+ seen.add(n);
17748
17654
  }
17749
- return l;
17750
- };
17751
- if (rawEvents.some((e) => !e.lane || e.lane === "code") && !data.lanes?.some((l) => l.id === "code"))
17752
- lane("code");
17753
- let inferred = false;
17754
- for (const l of data.lanes ?? []) {
17755
- const into = lane(l.id, l.label);
17756
- (l.spans ?? []).forEach((s, i) => {
17757
- const a = day(s.from);
17758
- if (!a)
17759
- return;
17760
- const open = !s.to;
17761
- const b = open ? null : day(s.to);
17762
- const to = open ? today : b?.to ?? a.to;
17763
- const st = stateOf(s.state);
17764
- const days = Math.max(0, to - a.from);
17765
- const own = s.label ?? data.states?.[s.state]?.tag ?? "{d}d";
17766
- const tags = own === "" ? [] : [...new Set([own, "{d}d"])].map((t) => t.replace(/\{d\}/g, String(days)));
17767
- const id = s.id ?? `${l.id}/${i}`;
17768
- const item = {
17769
- id,
17770
- kind: "span",
17771
- lane: l.id,
17772
- from: a.from,
17773
- to,
17774
- open,
17775
- title: s.title ?? `${l.label} · ${st.label}`,
17776
- ...s.detail ? { detail: s.detail } : {},
17777
- tone: st.tone,
17778
- inferred: s.provenance === "inferred",
17779
- ...s.provenance ? { provenance: s.provenance } : {},
17780
- ...s.source ? { source: s.source } : {},
17781
- evidence: s.evidence ?? [],
17782
- ...s.step ? { step: s.step } : {},
17783
- state: s.state,
17784
- tags
17785
- };
17786
- if (item.inferred)
17787
- inferred = true;
17788
- into.spans.push(item);
17789
- items.set(id, item);
17655
+ });
17656
+ const kids = items.map(() => []);
17657
+ const roots = [];
17658
+ parentOf.forEach((p, i) => (p === null ? roots : kids[p]).push(i));
17659
+ const rows = [];
17660
+ const walk = (list, depth, rails) => list.forEach((i, n) => {
17661
+ const it = items[i];
17662
+ const last = n === list.length - 1;
17663
+ const off = parentOf[i] === null && it.base && it.base !== trunk ? it.base : undefined;
17664
+ rows.push({
17665
+ item: it,
17666
+ index: i,
17667
+ depth,
17668
+ parent: parentOf[i],
17669
+ last,
17670
+ rails,
17671
+ children: kids[i].length,
17672
+ ...off ? { offBase: off } : {}
17790
17673
  });
17791
- }
17792
- const types = [];
17793
- const events = [];
17794
- rawEvents.forEach((e, index) => {
17795
- const a = day(e.date);
17796
- if (!a)
17797
- return;
17798
- const to = Math.max(a.to, day(e.to)?.to ?? a.to);
17799
- const into = lane(e.lane ?? "code");
17800
- const id = e.id ?? `e${index}`;
17801
- const item = {
17802
- id,
17803
- kind: "event",
17804
- lane: into.id,
17805
- from: a.from,
17806
- to,
17807
- open: false,
17808
- title: e.title,
17809
- ...e.detail ? { detail: e.detail } : {},
17810
- tone: e.tone ?? TYPE_TONE[e.type ?? ""] ?? "dim",
17811
- ...e.type ? { type: e.type } : {},
17812
- inferred: e.provenance === "inferred",
17813
- ...e.provenance ? { provenance: e.provenance } : {},
17814
- ...e.source ? { source: e.source } : {},
17815
- evidence: e.evidence ?? [],
17816
- ...e.step ? { step: e.step } : {},
17817
- tags: []
17818
- };
17819
- if (item.inferred)
17820
- inferred = true;
17821
- if (e.type && !types.includes(e.type))
17822
- types.push(e.type);
17823
- into.events.push(item);
17824
- items.set(id, item);
17825
- events.push({ item, index });
17674
+ walk(kids[i], depth + 1, [...rails, !last]);
17826
17675
  });
17827
- const bands = [];
17828
- for (const b of rawBands) {
17829
- const a = day(b.from);
17830
- if (!a)
17831
- continue;
17832
- const z = b.to ? day(b.to) : null;
17833
- bands.push({ from: a.from, to: z ? z.to : today, label: b.label });
17834
- }
17835
- bands.sort((x, y) => x.from - y.from);
17836
- const all = [...items.values()].flatMap((i) => [i.from, i.to]);
17837
- for (const b of bands)
17838
- all.push(b.from, b.to);
17839
- const hasOpen = [...items.values()].some((i) => i.open);
17840
- const lo = all.length ? Math.min(...all) : today - 90;
17841
- let hi = all.length ? Math.max(...all) : today;
17842
- if (hasOpen || data.axis?.today)
17843
- hi = Math.max(hi, today);
17844
- const from = day(data.axis?.from)?.from ?? startOfMonth(lo);
17845
- const to = day(data.axis?.to)?.to ?? hi + Math.max(6, Math.round((hi - from) * 0.05));
17846
- const laneIndex = (id) => laneOrder.indexOf(id);
17847
- const order = eventOrder(events.map((e) => ({ ...e, laneIndex: laneIndex(e.item.lane) })));
17848
- for (const l of lanes.values())
17849
- l.events.sort((a, b) => a.from - b.from);
17850
- return {
17851
- from,
17852
- to: Math.max(to, from + 1),
17853
- today,
17854
- lanes: laneOrder.map((id) => lanes.get(id)),
17855
- events: order,
17856
- bands,
17857
- states: [...states.values()],
17858
- types,
17859
- inferred,
17860
- verified: [...items.values()].some((i) => i.provenance === "verified"),
17861
- items
17862
- };
17676
+ walk(roots, 0, []);
17677
+ return { trunk, rows, flat: !trunk && rows.every((r) => r.depth === 0 && !r.children) };
17863
17678
  }
17864
- var DEFAULT_MARKS = ["release", "deletion"];
17865
- function quietEvents(events, marks) {
17866
- if (marks === "all")
17867
- return new Set;
17868
- const quiet = events.filter((e) => e.type && !marks.includes(e.type));
17869
- return quiet.length === events.length ? new Set : new Set(quiet.map((e) => e.id));
17679
+ var isNum = (c) => typeof c === "number" || typeof c === "object" && typeof c.text === "number";
17680
+ function fillCell(c) {
17681
+ if (typeof c !== "object")
17682
+ return { text: String(c) };
17683
+ return { ...c, text: String(c.text) };
17870
17684
  }
17871
- var PLURALS = {
17872
- commit: "commits",
17873
- release: "releases",
17874
- deletion: "deletions",
17875
- channel: "channel switches",
17876
- audit: "findings"
17877
- };
17878
- function quietLabel(events, quiet) {
17879
- const types = [];
17880
- for (const e of events)
17881
- if (quiet.has(e.id) && e.type && !types.includes(e.type))
17882
- types.push(e.type);
17883
- const names = types.map((t) => PLURALS[t] ?? `${t}s`);
17884
- return names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}` : names[0] ?? "";
17885
- }
17886
- function eventOrder(events) {
17887
- return [...events].sort((a, b) => a.item.from - b.item.from || a.laneIndex - b.laneIndex || a.index - b.index).map((e) => e.item);
17888
- }
17889
- function stepEvent(order, current, dir, at) {
17890
- if (!order.length)
17891
- return null;
17892
- const i = current === null ? -1 : order.findIndex((e) => e.id === current);
17893
- if (i < 0 && at !== undefined) {
17894
- const next = dir > 0 ? order.find((e) => e.from >= at) : [...order].reverse().find((e) => e.from <= at);
17895
- return next ?? (dir > 0 ? order[order.length - 1] : order[0]);
17896
- }
17897
- if (i < 0)
17898
- return dir > 0 ? order[0] : order[order.length - 1];
17899
- return order[Math.max(0, Math.min(order.length - 1, i + dir))];
17900
- }
17901
- function dayScale(from, to, width) {
17902
- const span = Math.max(1, to - from);
17903
- return {
17904
- x: (day) => (day - from) / span * width,
17905
- day: (x) => from + x / Math.max(1, width) * span,
17906
- f: (day) => (day - from) / span
17907
- };
17908
- }
17909
- var STEPS = [1, 2, 3, 6, 12];
17910
- function monthTicks(from, to, width, minGap = 44) {
17911
- const perMonth = width / Math.max(1, to - from) * 30.44;
17912
- const step = STEPS.find((s) => s * perMonth >= minGap) ?? 12;
17913
- const ticks = [];
17914
- const { y } = parts(from);
17915
- let { m } = parts(from);
17916
- let d = dayOf(y, m, 1);
17917
- if (d < from) {
17918
- m++;
17919
- d = dayOf(y, m, 1);
17920
- }
17921
- while (d <= to) {
17922
- const p = parts(d);
17923
- if (p.m % step === 0)
17924
- ticks.push({ day: d, label: p.m === 0 ? String(p.y) : MONTHS[p.m] });
17925
- m++;
17926
- d = dayOf(y, m, 1);
17927
- }
17928
- return ticks;
17929
- }
17930
- function fitLabels(labels, width, reserved = [], gap = 8) {
17931
- let end = -Infinity;
17932
- return labels.map(({ x, w }) => {
17933
- const x1 = x + w;
17934
- if (x < end + gap || x1 > width)
17935
- return false;
17936
- if (reserved.some((r) => x < r.x1 + gap && x1 > r.x0 - gap))
17937
- return false;
17938
- end = x1;
17939
- return true;
17685
+ function fillTable(m) {
17686
+ const wrapped = !m.columns.some((c) => c.key === "cells");
17687
+ const raw = m.rows.map((r) => {
17688
+ const keys = Object.keys(r);
17689
+ const cells = r.cells;
17690
+ return wrapped && keys.length === 1 && keys[0] === "cells" && cells && typeof cells === "object" ? cells : r;
17940
17691
  });
17941
- }
17942
- function clipSpan(from, to, axisFrom, axisTo) {
17943
- if (to < axisFrom || from > axisTo)
17944
- return null;
17692
+ const columns = m.columns.map((c) => {
17693
+ const present = raw.map((r) => r[c.key]).filter((v) => v !== undefined && v !== "");
17694
+ const numeric = present.length > 0 && present.every(isNum);
17695
+ return { ...c, align: c.align ?? (numeric ? "right" : "left") };
17696
+ });
17697
+ const rows = raw.map((r) => {
17698
+ const out = {};
17699
+ for (const [k, v] of Object.entries(r))
17700
+ if (v !== undefined && v !== null)
17701
+ out[k] = fillCell(v);
17702
+ return out;
17703
+ });
17704
+ const primary = columns.find((c) => c.key === m.primary)?.key ?? columns[0]?.key ?? "";
17945
17705
  return {
17946
- from: Math.max(from, axisFrom),
17947
- to: Math.min(to, axisTo),
17948
- clipStart: from < axisFrom,
17949
- clipEnd: to > axisTo
17706
+ kind: "table",
17707
+ columns,
17708
+ rows,
17709
+ ...m.caption ? { caption: m.caption } : {},
17710
+ stack: m.stack ?? "auto",
17711
+ primary
17950
17712
  };
17951
17713
  }
17952
- function fitTag(widths, spanWidth, roomAfter, pad = 6) {
17953
- for (let i = 0;i < widths.length; i++)
17954
- if (widths[i] + pad * 2 <= spanWidth)
17955
- return { index: i, place: "inside" };
17956
- for (let i = 0;i < widths.length; i++)
17957
- if (widths[i] + pad <= roomAfter)
17958
- return { index: i, place: "after" };
17959
- return null;
17960
- }
17961
- function bandRoom(bands, x) {
17962
- return bands.map((b, i) => {
17963
- const next = bands.slice(i + 1).find((n) => n.from > b.from);
17964
- const end = Math.min(b.to, next ? next.from : Infinity);
17965
- return Math.max(0, x(end) - x(b.from));
17714
+ function fillChapters(chapters) {
17715
+ const id = uniqueIds();
17716
+ const starts = chapters.map((c) => c.start).sort((a, b) => a - b);
17717
+ return chapters.map((c) => {
17718
+ const next = starts.find((s) => s > c.start);
17719
+ const end = c.end ?? (c.duration !== undefined ? Math.round((c.start + c.duration) * 1000) / 1000 : next);
17720
+ return {
17721
+ id: id(c.id ?? slug(c.label)),
17722
+ label: c.label,
17723
+ start: c.start,
17724
+ ...end !== undefined && end > c.start ? { end } : {}
17725
+ };
17966
17726
  });
17967
17727
  }
17968
- function nearestEvent(events, day, tolerance) {
17969
- let best = null;
17970
- let gap = tolerance;
17971
- for (const e of events) {
17972
- const d = day < e.from ? e.from - day : day > e.to ? day - e.to : 0;
17973
- if (d <= gap) {
17974
- best = e;
17975
- gap = d;
17976
- }
17977
- }
17978
- return best;
17979
- }
17980
- function firstSentence(text) {
17981
- if (!text)
17982
- return "";
17983
- const plain = text.replace(/`([^`]*)`/g, "$1").replace(/\*\*?([^*]+)\*\*?/g, "$1").trim();
17984
- const m = /^.*?[.!?](?=\s|$)/.exec(plain);
17985
- return (m ? m[0] : plain).trim();
17986
- }
17987
-
17988
- // src/ui/history.ts
17989
- init_look();
17990
-
17991
- // src/ui/walkthrough-model.ts
17992
- var DEFAULT_VERDICTS = ["ship", "change", "unsure"];
17993
- function slug(text) {
17994
- return text.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^(.{0,48})-.*$/, (m, head) => m.length > 48 && head ? head : m).slice(0, 48) || "item";
17995
- }
17996
- function uniqueIds() {
17997
- const seen = new Map;
17998
- return (id) => {
17999
- const n = (seen.get(id) ?? 0) + 1;
18000
- seen.set(id, n);
18001
- return n === 1 ? id : `${id}-${n}`;
18002
- };
17728
+ function chapterAt(chapters, time) {
17729
+ let at = -1;
17730
+ chapters.forEach((c, i) => {
17731
+ if (time >= c.start && time < (c.end ?? Infinity) && (at < 0 || c.start >= chapters[at].start))
17732
+ at = i;
17733
+ });
17734
+ return at;
18003
17735
  }
18004
- var label = (id) => id.charAt(0).toUpperCase() + id.slice(1).replace(/[-_]+/g, " ");
18005
- function resolveSrc(src, base) {
18006
- if (!base || /^([a-z][a-z0-9+.-]*:|\/\/|\/)/i.test(src))
18007
- return src;
18008
- return base.replace(/\/?$/, "/") + src.replace(/^\.\//, "");
17736
+ function clock(seconds) {
17737
+ const t = Math.max(0, Math.floor(seconds));
17738
+ const h = Math.floor(t / 3600);
17739
+ const m = Math.floor(t % 3600 / 60);
17740
+ const s = String(t % 60).padStart(2, "0");
17741
+ return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
18009
17742
  }
18010
- var VIDEO2 = /\.(mp4|m4v|webm|mov|ogv)(\?|#|$)/i;
18011
- var mediaOf = (src) => VIDEO2.test(src) ? { kind: "video", src } : { kind: "image", src };
18012
- function parseAspect(aspect) {
18013
- if (aspect === undefined)
18014
- return;
18015
- if (typeof aspect === "number")
18016
- return aspect > 0 && Number.isFinite(aspect) ? aspect : undefined;
18017
- const m = /^\s*([\d.]+)\s*(?:[/x:×]\s*([\d.]+))?\s*$/.exec(aspect);
18018
- if (!m)
17743
+ function findStep(steps, ref) {
17744
+ if (ref === undefined)
18019
17745
  return;
18020
- const w = Number(m[1]);
18021
- const h = m[2] === undefined ? 1 : Number(m[2]);
18022
- return w > 0 && h > 0 ? w / h : undefined;
17746
+ const r = ref.trim().replace(/^#/, "");
17747
+ return steps.find((s) => s.key === r || s.id === r || s.number === r);
18023
17748
  }
18024
- function source2(s, base) {
18025
- const v = typeof s === "string" ? { src: s } : s;
18026
- return { ...v, src: resolveSrc(v.src, base) };
17749
+ function linkLabel(href) {
17750
+ const pr = /\/(?:pull|issues)\/(\d+)/.exec(href);
17751
+ if (pr)
17752
+ return `#${pr[1]}`;
17753
+ const tail = href.replace(/[?#].*$/, "").replace(/\/+$/, "").split("/").pop();
17754
+ return tail || href;
18027
17755
  }
18028
- function fillMedia(m, base) {
18029
- if (typeof m === "string")
18030
- return mediaOf(resolveSrc(m, base));
18031
- switch (m.kind) {
18032
- case "compare": {
18033
- const { chapters, ...rest } = m;
18034
- return {
18035
- ...rest,
18036
- before: source2(m.before, base),
18037
- after: source2(m.after, base),
18038
- ...chapters?.length ? { chapters: fillChapters(chapters) } : {}
18039
- };
18040
- }
18041
- case "video": {
18042
- const { chapters, captionsSrc, captions, ...rest } = m;
18043
- const vtt = captions ?? captionsSrc;
18044
- return {
18045
- ...rest,
18046
- src: resolveSrc(m.src, base),
18047
- ...m.poster ? { poster: resolveSrc(m.poster, base) } : {},
18048
- ...vtt ? { captions: resolveSrc(vtt, base) } : {},
18049
- ...chapters?.length ? { chapters: fillChapters(chapters) } : {}
18050
- };
18051
- }
18052
- case "image":
18053
- return { ...m, src: resolveSrc(m.src, base) };
18054
- case "history":
18055
- if ("data" in m)
18056
- return m;
18057
- return {
18058
- kind: "history",
18059
- data: historyData(m),
18060
- ...m.label ? { label: m.label } : {},
18061
- ...m.marks ? { marks: m.marks } : {}
18062
- };
18063
- case "stack":
18064
- return {
18065
- kind: "stack",
18066
- ...m.base ? { base: m.base } : {},
18067
- items: m.items.map(fillStackItem)
18068
- };
18069
- case "table":
18070
- return fillTable(m);
18071
- default:
18072
- return m;
18073
- }
18074
- }
18075
- function historyData(m) {
18076
- const events = m.events ?? [];
18077
- if (m.lanes || m.bands || m.states || m.axis)
18078
- return {
18079
- ...m.axis ? { axis: m.axis } : {},
18080
- ...m.lanes ? { lanes: m.lanes } : {},
18081
- ...events.length ? { events } : {},
18082
- ...m.bands ? { bands: m.bands } : {},
18083
- ...m.states ? { states: m.states } : {}
18084
- };
18085
- const groups = [];
18086
- if (events.length)
18087
- groups.push({ events });
18088
- for (const g of m.groups ?? [])
18089
- groups.push({
18090
- ...g.heading ? { heading: g.heading } : {},
18091
- ...g.summary ? { summary: g.summary } : {},
18092
- events: g.events ?? g.rows ?? []
18093
- });
18094
- return { groups };
18095
- }
18096
- function parseBranch(branch) {
18097
- if (!branch?.trim())
18098
- return {};
18099
- const [head, base] = branch.split(/\s*(?:→|->|=>|⇒)\s*/, 2).map((s) => s.trim());
18100
- return { ...head ? { head } : {}, ...base ? { base } : {} };
18101
- }
18102
- function parseDelta(delta) {
18103
- if (delta === undefined || typeof delta === "object")
18104
- return delta;
18105
- const m = /\+\s*([\d,_]+)[^\d+]*?[-−–]\s*([\d,_]+)/.exec(delta);
18106
- if (!m)
18107
- return delta.trim() || undefined;
18108
- const n = (s) => Number(s.replace(/[,_]/g, ""));
18109
- return { add: n(m[1]), del: n(m[2]) };
18110
- }
18111
- function fillStackItem(it) {
18112
- const parsed = parseBranch(it.branch);
18113
- const head = parsed.head ?? it.head;
18114
- const base = parsed.base ?? it.base;
18115
- const delta = parseDelta(it.delta);
18116
- const step = it.step ?? it.stepId;
18117
- const pr = String(it.pr).trim();
18118
- return {
18119
- pr: /^\d+$/.test(pr) ? `#${pr}` : pr,
18120
- ...it.href ? { href: it.href } : {},
18121
- title: it.title,
18122
- ...head ? { head } : {},
18123
- ...base ? { base } : {},
18124
- ...delta !== undefined ? { delta } : {},
18125
- ...it.status ? { status: it.status } : {},
18126
- ...it.tone ? { tone: it.tone } : {},
18127
- ...it.summary ? { summary: it.summary } : {},
18128
- ...step ? { step } : {},
18129
- ...it.order !== undefined && it.order !== "" ? { order: String(it.order) } : {}
18130
- };
18131
- }
18132
- function stackLayout(m) {
18133
- const items = m.items;
18134
- const byHead = new Map;
18135
- items.forEach((it, i) => it.head && !byHead.has(it.head) && byHead.set(it.head, i));
18136
- let trunk = m.base ?? null;
18137
- if (!trunk) {
18138
- const tally = new Map;
18139
- for (const it of items)
18140
- if (it.base && !byHead.has(it.base))
18141
- tally.set(it.base, (tally.get(it.base) ?? 0) + 1);
18142
- let best = 0;
18143
- for (const [b, n] of tally)
18144
- if (n > best)
18145
- [trunk, best] = [b, n];
18146
- }
18147
- const parentOf = items.map((it, i) => {
18148
- const p = it.base === undefined ? undefined : byHead.get(it.base);
18149
- return p === undefined || p === i ? null : p;
18150
- });
18151
- items.forEach((_, i) => {
18152
- const seen = new Set;
18153
- for (let n = i;n !== null; n = parentOf[n]) {
18154
- if (seen.has(n)) {
18155
- const loop = [n];
18156
- for (let k = parentOf[n];k !== n && k !== null; k = parentOf[k])
18157
- loop.push(k);
18158
- parentOf[Math.min(...loop)] = null;
18159
- break;
18160
- }
18161
- seen.add(n);
18162
- }
18163
- });
18164
- const kids = items.map(() => []);
18165
- const roots = [];
18166
- parentOf.forEach((p, i) => (p === null ? roots : kids[p]).push(i));
18167
- const rows = [];
18168
- const walk = (list, depth, rails) => list.forEach((i, n) => {
18169
- const it = items[i];
18170
- const last = n === list.length - 1;
18171
- const off = parentOf[i] === null && it.base && it.base !== trunk ? it.base : undefined;
18172
- rows.push({
18173
- item: it,
18174
- index: i,
18175
- depth,
18176
- parent: parentOf[i],
18177
- last,
18178
- rails,
18179
- children: kids[i].length,
18180
- ...off ? { offBase: off } : {}
18181
- });
18182
- walk(kids[i], depth + 1, [...rails, !last]);
18183
- });
18184
- walk(roots, 0, []);
18185
- return { trunk, rows, flat: !trunk && rows.every((r) => r.depth === 0 && !r.children) };
18186
- }
18187
- var isNum = (c) => typeof c === "number" || typeof c === "object" && typeof c.text === "number";
18188
- function fillCell(c) {
18189
- if (typeof c !== "object")
18190
- return { text: String(c) };
18191
- return { ...c, text: String(c.text) };
18192
- }
18193
- function fillTable(m) {
18194
- const wrapped = !m.columns.some((c) => c.key === "cells");
18195
- const raw = m.rows.map((r) => {
18196
- const keys = Object.keys(r);
18197
- const cells = r.cells;
18198
- return wrapped && keys.length === 1 && keys[0] === "cells" && cells && typeof cells === "object" ? cells : r;
18199
- });
18200
- const columns = m.columns.map((c) => {
18201
- const present = raw.map((r) => r[c.key]).filter((v) => v !== undefined && v !== "");
18202
- const numeric = present.length > 0 && present.every(isNum);
18203
- return { ...c, align: c.align ?? (numeric ? "right" : "left") };
18204
- });
18205
- const rows = raw.map((r) => {
18206
- const out = {};
18207
- for (const [k, v] of Object.entries(r))
18208
- if (v !== undefined && v !== null)
18209
- out[k] = fillCell(v);
18210
- return out;
18211
- });
18212
- const primary = columns.find((c) => c.key === m.primary)?.key ?? columns[0]?.key ?? "";
18213
- return {
18214
- kind: "table",
18215
- columns,
18216
- rows,
18217
- ...m.caption ? { caption: m.caption } : {},
18218
- stack: m.stack ?? "auto",
18219
- primary
18220
- };
18221
- }
18222
- function fillChapters(chapters) {
18223
- const id = uniqueIds();
18224
- const starts = chapters.map((c) => c.start).sort((a, b) => a - b);
18225
- return chapters.map((c) => {
18226
- const next = starts.find((s) => s > c.start);
18227
- const end = c.end ?? (c.duration !== undefined ? Math.round((c.start + c.duration) * 1000) / 1000 : next);
18228
- return {
18229
- id: id(c.id ?? slug(c.label)),
18230
- label: c.label,
18231
- start: c.start,
18232
- ...end !== undefined && end > c.start ? { end } : {}
18233
- };
18234
- });
18235
- }
18236
- function chapterAt(chapters, time) {
18237
- let at = -1;
18238
- chapters.forEach((c, i) => {
18239
- if (time >= c.start && time < (c.end ?? Infinity) && (at < 0 || c.start >= chapters[at].start))
18240
- at = i;
18241
- });
18242
- return at;
18243
- }
18244
- function clock(seconds) {
18245
- const t = Math.max(0, Math.floor(seconds));
18246
- const h = Math.floor(t / 3600);
18247
- const m = Math.floor(t % 3600 / 60);
18248
- const s = String(t % 60).padStart(2, "0");
18249
- return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
18250
- }
18251
- function findStep(steps, ref) {
18252
- if (ref === undefined)
18253
- return;
18254
- const r = ref.trim().replace(/^#/, "");
18255
- return steps.find((s) => s.key === r || s.id === r || s.number === r);
18256
- }
18257
- function linkLabel(href) {
18258
- const pr = /\/(?:pull|issues)\/(\d+)/.exec(href);
18259
- if (pr)
18260
- return `#${pr[1]}`;
18261
- const tail = href.replace(/[?#].*$/, "").replace(/\/+$/, "").split("/").pop();
18262
- return tail || href;
18263
- }
18264
- function normalizeWalkthrough(doc) {
18265
- const sectionId = uniqueIds();
18266
- const steps = [];
18267
- const sections = doc.sections.map((s, si) => {
18268
- const id = sectionId(s.id ?? slug(s.title));
18269
- const stepId = uniqueIds();
18270
- const own = s.steps.map((step, i) => {
18271
- const sid = stepId(step.id ?? slug(step.title));
18272
- const optionId = uniqueIds();
18273
- const options = (step.options ?? []).map((o, oi) => ({
18274
- id: optionId(o.id ?? String.fromCharCode(97 + oi % 26)),
18275
- label: o.label,
18276
- letter: String.fromCharCode(65 + oi % 26),
18277
- ...o.caption ? { caption: o.caption } : {},
18278
- ...o.media !== undefined ? { media: fillMedia(o.media, doc.base) } : {},
18279
- suggested: o.suggested === true
18280
- }));
18281
- const list = step.media === undefined ? [] : Array.isArray(step.media) ? step.media : [step.media];
18282
- const asked = step.ask === "verdict" ? "note" : step.ask;
18283
- const ask = asked === "pick" && !options.length ? "note" : asked ?? (options.length ? "pick" : "note");
18284
- const walk = {
18285
- key: `${id}/${sid}`,
18286
- id: sid,
18287
- section: id,
18288
- number: `${si + 1}.${i + 1}`,
18289
- title: step.title,
18290
- ...step.caption ? { caption: step.caption } : {},
18291
- ...step.question ? { question: step.question } : {},
18292
- media: list.map((m) => fillMedia(m, doc.base)),
18293
- columns: Math.max(1, Math.min(3, Math.round(step.columns ?? 1))),
18294
- ...step.more?.trim() ? { more: step.more.trim() } : {},
18295
- options,
18296
- ask
17756
+ function normalizeWalkthrough(doc) {
17757
+ const sectionId = uniqueIds();
17758
+ const steps = [];
17759
+ const sections = doc.sections.map((s, si) => {
17760
+ const id = sectionId(s.id ?? slug(s.title));
17761
+ const stepId = uniqueIds();
17762
+ const own = s.steps.map((step, i) => {
17763
+ const sid = stepId(step.id ?? slug(step.title));
17764
+ const optionId = uniqueIds();
17765
+ const options = (step.options ?? []).map((o, oi) => ({
17766
+ id: optionId(o.id ?? String.fromCharCode(97 + oi % 26)),
17767
+ label: o.label,
17768
+ letter: String.fromCharCode(65 + oi % 26),
17769
+ ...o.caption ? { caption: o.caption } : {},
17770
+ ...o.media !== undefined ? { media: fillMedia(o.media, doc.base) } : {},
17771
+ ...o.facts ? { facts: fillFacts(o.facts) } : {},
17772
+ suggested: o.suggested === true || o.recommended === true
17773
+ }));
17774
+ const list = step.media === undefined ? [] : Array.isArray(step.media) ? step.media : [step.media];
17775
+ const asked = step.ask === "verdict" ? "note" : step.ask;
17776
+ const ask = asked === "pick" && !options.length ? "note" : asked ?? (options.length ? "pick" : "note");
17777
+ const walk = {
17778
+ key: `${id}/${sid}`,
17779
+ id: sid,
17780
+ section: id,
17781
+ number: `${si + 1}.${i + 1}`,
17782
+ title: step.title,
17783
+ ...step.caption ? { caption: step.caption } : {},
17784
+ ...step.question ? { question: step.question } : {},
17785
+ media: list.map((m) => fillMedia(m, doc.base)),
17786
+ columns: Math.max(1, Math.min(3, Math.round(step.columns ?? 1))),
17787
+ ...step.more?.trim() ? { more: step.more.trim() } : {},
17788
+ options,
17789
+ ask
18297
17790
  };
18298
17791
  steps.push(walk);
18299
17792
  return walk;
@@ -18362,398 +17855,1192 @@ function prStatus(status) {
18362
17855
  const state = PR_STATES.includes(first) ? first : first === "shipped" || first === "done" ? "merged" : null;
18363
17856
  return { state, checks: /\b(green|passing|passed|ci|checks? pass)/.test(words) };
18364
17857
  }
18365
- function knownPRs(doc, sections, steps = []) {
18366
- const out = {};
18367
- const repo = repoOf(doc);
18368
- const add = (n, pr) => {
18369
- const known = out[n] ?? {};
18370
- const next = { ...known };
18371
- for (const [k, v] of Object.entries(pr))
18372
- if (v !== undefined && next[k] === undefined)
18373
- next[k] = v;
18374
- out[n] = next;
18375
- };
18376
- for (const [k, pr] of Object.entries(doc.prs ?? {})) {
18377
- const n = k.replace(/^#/, "");
18378
- if (!/^\d+$/.test(n))
18379
- continue;
18380
- add(n, pr);
18381
- }
18382
- for (const step of steps)
18383
- for (const m of step.media) {
18384
- if (m.kind !== "stack")
18385
- continue;
18386
- for (const it of m.items) {
18387
- const n = it.pr.replace(/^#/, "");
18388
- if (!/^\d+$/.test(n))
18389
- continue;
18390
- const said = prStatus(it.status);
18391
- add(n, {
18392
- ...said.state ? { state: said.state } : {},
18393
- ...said.state && said.checks ? { checks: true } : {},
18394
- ...it.title ? { title: plainMarkdown(it.title) } : {},
18395
- ...it.href ? { href: it.href } : {}
18396
- });
18397
- }
18398
- }
18399
- for (const s of sections) {
18400
- const n = s.link && prNumber(s.link.href);
18401
- if (!n)
18402
- continue;
18403
- const said = prStatus(s.status);
18404
- add(n, {
18405
- href: s.link.href.replace(/(\/pull\/\d+).*$/, "$1"),
18406
- ...said.state ? { state: said.state } : {},
18407
- title: plainMarkdown(s.title)
18408
- });
18409
- }
18410
- if (repo) {
18411
- for (const n of Object.keys(out))
18412
- if (!out[n].href)
18413
- out[n] = { ...out[n], href: `https://github.com/${repo}/pull/${n}` };
17858
+ function knownPRs(doc, sections, steps = []) {
17859
+ const out = {};
17860
+ const repo = repoOf(doc);
17861
+ const add = (n, pr) => {
17862
+ const known = out[n] ?? {};
17863
+ const next = { ...known };
17864
+ for (const [k, v] of Object.entries(pr))
17865
+ if (v !== undefined && next[k] === undefined)
17866
+ next[k] = v;
17867
+ out[n] = next;
17868
+ };
17869
+ for (const [k, pr] of Object.entries(doc.prs ?? {})) {
17870
+ const n = k.replace(/^#/, "");
17871
+ if (!/^\d+$/.test(n))
17872
+ continue;
17873
+ add(n, pr);
17874
+ }
17875
+ for (const step of steps)
17876
+ for (const m of step.media) {
17877
+ if (m.kind !== "stack")
17878
+ continue;
17879
+ for (const it of m.items) {
17880
+ const n = it.pr.replace(/^#/, "");
17881
+ if (!/^\d+$/.test(n))
17882
+ continue;
17883
+ const said = prStatus(it.status);
17884
+ add(n, {
17885
+ ...said.state ? { state: said.state } : {},
17886
+ ...said.state && said.checks ? { checks: true } : {},
17887
+ ...it.title ? { title: plainMarkdown(it.title) } : {},
17888
+ ...it.href ? { href: it.href } : {}
17889
+ });
17890
+ }
17891
+ }
17892
+ for (const s of sections) {
17893
+ const n = s.link && prNumber(s.link.href);
17894
+ if (!n)
17895
+ continue;
17896
+ const said = prStatus(s.status);
17897
+ add(n, {
17898
+ href: s.link.href.replace(/(\/pull\/\d+).*$/, "$1"),
17899
+ ...said.state ? { state: said.state } : {},
17900
+ title: plainMarkdown(s.title)
17901
+ });
17902
+ }
17903
+ if (repo) {
17904
+ for (const n of Object.keys(out))
17905
+ if (!out[n].href)
17906
+ out[n] = { ...out[n], href: `https://github.com/${repo}/pull/${n}` };
17907
+ }
17908
+ return out;
17909
+ }
17910
+ function findPR(prs, n, repo, kind = "pr") {
17911
+ const entry = prs[n];
17912
+ const known = entry && (entry.kind ?? "pr") === kind ? entry : undefined;
17913
+ const path = kind === "issue" ? "issues" : "pull";
17914
+ const href = repo ? `https://github.com/${repo}/${path}/${n}` : undefined;
17915
+ const base = kind === "issue" ? { kind } : {};
17916
+ if (known)
17917
+ return known.href || !href ? { ...base, ...known } : { ...base, ...known, href };
17918
+ return href ? { ...base, href } : kind === "issue" ? { kind } : null;
17919
+ }
17920
+ var issueNumber = (href) => /^https?:\/\/[^/]+\/.+\/issues\/(\d+)\/?(?:[?#].*)?$/.exec(href)?.[1] ?? null;
17921
+ var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
17922
+ function withKickers(sections, source, kickers = "pr") {
17923
+ if (kickers === false)
17924
+ return [...sections];
17925
+ const noun = typeof kickers === "object" ? kickers.noun.trim() : "PR";
17926
+ const counted = typeof kickers === "object" ? sections : sections.filter((s) => s.link && isPullRequest(s.link.href));
17927
+ return sections.map((s, i) => {
17928
+ const own = source[i]?.kicker;
17929
+ if (own !== undefined || s.kicker)
17930
+ return s;
17931
+ const at = counted.indexOf(s);
17932
+ return at < 0 ? s : { ...s, kicker: `${noun} ${at + 1} of ${counted.length}`.trim() };
17933
+ });
17934
+ }
17935
+ function tableStacks(stack, room, natural) {
17936
+ if (stack !== "auto")
17937
+ return stack === "always";
17938
+ return room > 0 && natural > room + 1;
17939
+ }
17940
+ function noteBarReserve(bar, inset, gap) {
17941
+ return Math.max(0, Math.ceil(bar)) + inset + gap;
17942
+ }
17943
+ function parseAt(at) {
17944
+ if (!at)
17945
+ return null;
17946
+ const t = Date.parse(at);
17947
+ return Number.isFinite(t) ? t : null;
17948
+ }
17949
+ function sortFeed(items) {
17950
+ return items.map((item, i) => ({ item, i })).sort((a, b) => {
17951
+ const x = a.item.at ?? -Infinity;
17952
+ const y = b.item.at ?? -Infinity;
17953
+ return x !== y ? y - x : b.i - a.i;
17954
+ }).map((e) => e.item);
17955
+ }
17956
+ function ago(at, now) {
17957
+ const s = Math.max(0, (now - at) / 1000);
17958
+ if (s < 45)
17959
+ return "just now";
17960
+ if (s < 3600)
17961
+ return `${Math.round(s / 60)}m ago`;
17962
+ if (s < 86400)
17963
+ return `${Math.round(s / 3600)}h ago`;
17964
+ if (s < 7 * 86400)
17965
+ return `${Math.round(s / 86400)}d ago`;
17966
+ return new Date(at).toISOString().slice(0, 10);
17967
+ }
17968
+ function statusLook(status) {
17969
+ const id = slug(status);
17970
+ const tone = id === "ready" || id === "merged" || id === "shipped" ? "good" : id === "needs-taste" || id === "needs-review" || id === "review" ? "warn" : id === "blocked" || id === "broken" || id === "failing" ? "bad" : id === "draft" || id === "wip" ? "dim" : "info";
17971
+ return { label: label(status), tone };
17972
+ }
17973
+ function verdictTone(id, index) {
17974
+ if (/^(ship|yes|good|approve|keep|lgtm)/.test(id))
17975
+ return "good";
17976
+ if (/^(change|fix|revise|tweak|redo)/.test(id))
17977
+ return "warn";
17978
+ if (/^(no|reject|drop|revert|bad)/.test(id))
17979
+ return "bad";
17980
+ if (/^(unsure|maybe|skip|later)/.test(id))
17981
+ return "dim";
17982
+ return index === 0 ? "good" : "dim";
17983
+ }
17984
+ var EMPTY_STATE = { steps: {} };
17985
+ function setFeedback(state, key, patch) {
17986
+ const next = { ...state.steps[key] };
17987
+ for (const [k, v] of Object.entries(patch)) {
17988
+ if (v === undefined)
17989
+ continue;
17990
+ if (v === null || v.trim() === "")
17991
+ delete next[k];
17992
+ else
17993
+ next[k] = v;
17994
+ }
17995
+ const steps = { ...state.steps };
17996
+ if (Object.keys(next).length)
17997
+ steps[key] = next;
17998
+ else
17999
+ delete steps[key];
18000
+ return { ...state, steps };
18001
+ }
18002
+ var hasContent = (n) => Boolean(n.text?.trim() || n.voice || n.ink?.paths.length);
18003
+ function addNote(state, note) {
18004
+ if (!hasContent(note))
18005
+ return state;
18006
+ return { ...state, notes: [...state.notes ?? [], note] };
18007
+ }
18008
+ function removeNote(state, id) {
18009
+ return { ...state, notes: (state.notes ?? []).filter((n) => n.id !== id) };
18010
+ }
18011
+ var notesOn = (state, key) => (state.notes ?? []).filter((n) => n.step === key);
18012
+ function isAnswered(step, state) {
18013
+ if (step.ask === "none")
18014
+ return false;
18015
+ const fb = state.steps[step.key];
18016
+ if (step.ask === "pick")
18017
+ return fb?.pick !== undefined;
18018
+ return Boolean(fb?.verdict || fb?.note || notesOn(state, step.key).length);
18019
+ }
18020
+ function digitChoice(step, fb, key) {
18021
+ if (step.ask !== "pick" || !/^[1-9]$/.test(key))
18022
+ return;
18023
+ const id = step.options[Number(key) - 1]?.id;
18024
+ if (id === undefined)
18025
+ return;
18026
+ return fb?.pick === id ? null : id;
18027
+ }
18028
+ function walkthroughProgress(w, state) {
18029
+ const asked = w.steps.filter((s) => s.ask !== "none");
18030
+ const done = asked.filter((s) => isAnswered(s, state));
18031
+ const next = asked.find((s) => !isAnswered(s, state));
18032
+ return { done: done.length, total: asked.length, next: next?.key ?? null };
18033
+ }
18034
+ function adjacentStep(keys, current, dir) {
18035
+ if (!keys.length)
18036
+ return;
18037
+ const i = current === null ? -1 : keys.indexOf(current);
18038
+ if (i < 0)
18039
+ return dir > 0 ? keys[0] : keys[keys.length - 1];
18040
+ return keys[Math.max(0, Math.min(keys.length - 1, i + dir))];
18041
+ }
18042
+ var LOG_MAX = 40;
18043
+ function pushContext(log, event, max = LOG_MAX) {
18044
+ const last = log[log.length - 1];
18045
+ const merge = last && last.what === event.what && last.step === event.step && (event.what === "seek" || event.what === "step") && event.at - last.at < 1500;
18046
+ const next = merge ? [...log.slice(0, -1), event] : [...log, event];
18047
+ return next.slice(-max);
18048
+ }
18049
+ function recentContext(log, now, windowMs = 90000, min = 4) {
18050
+ const inWindow = log.filter((e) => now - e.at <= windowMs);
18051
+ return inWindow.length >= min ? inWindow : log.slice(-min);
18052
+ }
18053
+ function formatTime(seconds) {
18054
+ const t = Math.max(0, Math.round(seconds * 10) / 10);
18055
+ const m = Math.floor(t / 60);
18056
+ const s = (t - m * 60).toFixed(1).padStart(4, "0");
18057
+ return `${m}:${s}`;
18058
+ }
18059
+ function inkPath(points, w, h) {
18060
+ if (!points.length || w <= 0 || h <= 0)
18061
+ return "";
18062
+ const n = (v) => String(Math.round(Math.max(0, Math.min(1, v)) * 1000) / 1000);
18063
+ const kept = [points[0]];
18064
+ for (const p of points.slice(1)) {
18065
+ const l = kept[kept.length - 1];
18066
+ if (Math.hypot(p.x - l.x, p.y - l.y) >= 1)
18067
+ kept.push(p);
18068
+ }
18069
+ return kept.map((p, i) => `${i ? "L" : "M"}${n(p.x / w)} ${n(p.y / h)}`).join("");
18070
+ }
18071
+ function zoomWords(z) {
18072
+ const times = Math.round(1 / Math.max(0.001, Math.max(z.w, z.h)) * 10) / 10;
18073
+ return `${times}× at ${Math.round(z.u * 100)}%, ${Math.round(z.v * 100)}%`;
18074
+ }
18075
+ function describeMedia(m) {
18076
+ if (!m)
18077
+ return "";
18078
+ const parts = [];
18079
+ if (m.file)
18080
+ parts.push(`${m.kind} ${m.file}`);
18081
+ else if (m.time !== undefined)
18082
+ parts.push(`${m.kind} at ${formatTime(m.time)}`);
18083
+ else
18084
+ parts.push(m.kind);
18085
+ if (m.chapter)
18086
+ parts[0] += ` in “${m.chapter}”`;
18087
+ if (m.loop)
18088
+ parts.push(`loop ${formatTime(m.loop.start)}–${formatTime(m.loop.end)}`);
18089
+ if (m.zoom)
18090
+ parts.push(`zoomed ${zoomWords(m.zoom)}`);
18091
+ if (m.mode)
18092
+ parts.push(m.mode);
18093
+ if (m.full)
18094
+ parts.push("full window");
18095
+ return parts.join(", ");
18096
+ }
18097
+ function readState(raw) {
18098
+ if (!raw)
18099
+ return EMPTY_STATE;
18100
+ try {
18101
+ const v = JSON.parse(raw);
18102
+ if (!v || typeof v !== "object")
18103
+ return EMPTY_STATE;
18104
+ const o = v;
18105
+ const steps = {};
18106
+ if (o.steps && typeof o.steps === "object")
18107
+ for (const [key, fb] of Object.entries(o.steps)) {
18108
+ if (!fb || typeof fb !== "object")
18109
+ continue;
18110
+ const f = fb;
18111
+ const clean = {};
18112
+ for (const k of ["verdict", "pick", "note"])
18113
+ if (typeof f[k] === "string" && f[k].trim())
18114
+ clean[k] = f[k];
18115
+ if (Object.keys(clean).length)
18116
+ steps[key] = clean;
18117
+ }
18118
+ const notes = Array.isArray(o.notes) ? o.notes.filter(isNote) : [];
18119
+ return {
18120
+ steps,
18121
+ ...notes.length ? { notes } : {},
18122
+ ...typeof o.note === "string" && o.note.trim() ? { note: o.note } : {}
18123
+ };
18124
+ } catch {
18125
+ return EMPTY_STATE;
18126
+ }
18127
+ }
18128
+ function isNote(v) {
18129
+ if (!v || typeof v !== "object")
18130
+ return false;
18131
+ const n = v;
18132
+ const ctx = n.context;
18133
+ return typeof n.id === "string" && typeof n.at === "number" && (n.step === null || typeof n.step === "string") && (n.text === undefined || typeof n.text === "string") && !!ctx && Array.isArray(ctx.log) && hasContent(n);
18134
+ }
18135
+ var noteOut = (n) => ({
18136
+ at: new Date(n.at).toISOString(),
18137
+ ...n.text?.trim() ? { text: n.text.trim() } : {},
18138
+ ...n.voice ? { voice: n.voice } : {},
18139
+ ...n.ink?.paths.length ? { ink: n.ink } : {},
18140
+ ...n.context.media ? { media: n.context.media } : {},
18141
+ log: n.context.log.map(({ at, ...e }) => ({ ...e, ago: Math.round((n.at - at) / 100) / 10 }))
18142
+ });
18143
+ function collectFeedback(w, state) {
18144
+ const progress = walkthroughProgress(w, state);
18145
+ const sectionTitle = new Map(w.sections.map((s) => [s.id, s.link ? `${s.link.label} ${s.title}` : s.title]));
18146
+ const keys = new Set(w.steps.map((s) => s.key));
18147
+ const steps = [];
18148
+ for (const s of w.steps) {
18149
+ const fb = state.steps[s.key];
18150
+ const notes = notesOn(state, s.key).map(noteOut);
18151
+ if (s.ask === "none" && !notes.length)
18152
+ continue;
18153
+ const option = s.ask === "pick" ? s.options.find((o) => o.id === fb?.pick) : undefined;
18154
+ const suggested = s.options.find((o) => o.suggested);
18155
+ steps.push({
18156
+ key: s.key,
18157
+ number: s.number,
18158
+ section: sectionTitle.get(s.section) ?? s.section,
18159
+ title: s.title,
18160
+ ask: s.ask,
18161
+ ...option ? { pick: option.id, pickLabel: `${option.letter}: ${option.label}` } : {},
18162
+ ...option && suggested && suggested.id !== option.id ? { suggested: `${suggested.letter}: ${suggested.label}` } : {},
18163
+ notes,
18164
+ ...fb?.verdict ? { verdict: fb.verdict } : {},
18165
+ ...fb?.note ? { note: fb.note } : {}
18166
+ });
18167
+ }
18168
+ return {
18169
+ id: w.id,
18170
+ title: w.title,
18171
+ reviewed: progress.done,
18172
+ total: progress.total,
18173
+ ...state.note ? { note: state.note } : {},
18174
+ steps,
18175
+ general: (state.notes ?? []).filter((n) => !n.step || !keys.has(n.step)).map(noteOut)
18176
+ };
18177
+ }
18178
+ var quote = (text) => text.trim().replace(/\s*\n\s*/g, " ");
18179
+ function noteLine(n) {
18180
+ const bits = [];
18181
+ if (n.text)
18182
+ bits.push(`“${quote(n.text)}”`);
18183
+ if (n.voice)
18184
+ bits.push(`voice note ${formatTime(n.voice.duration)} (${n.voice.url})`);
18185
+ if (n.ink)
18186
+ bits.push(`ink, ${n.ink.paths.length} stroke${n.ink.paths.length === 1 ? "" : "s"}`);
18187
+ const seen = describeMedia(n.media);
18188
+ return `${bits.join(" + ")}${seen ? ` · ${seen}` : ""}`;
18189
+ }
18190
+ function feedbackMarkdown(fb, verdicts) {
18191
+ const name = (id) => verdicts?.find((v) => v.id === id)?.label ?? label(id);
18192
+ const lines = [
18193
+ `## Feedback: ${fb.title}`,
18194
+ "",
18195
+ `${fb.reviewed} of ${fb.total} reviewed · \`${fb.id}\``
18196
+ ];
18197
+ if (fb.note)
18198
+ lines.push("", quote(fb.note));
18199
+ if (fb.general.length) {
18200
+ lines.push("", "### General");
18201
+ for (const n of fb.general)
18202
+ lines.push(`- ${noteLine(n)}`);
18203
+ }
18204
+ let section = "";
18205
+ for (const s of fb.steps) {
18206
+ if (s.section !== section) {
18207
+ section = s.section;
18208
+ lines.push("", `### ${section}`);
18209
+ }
18210
+ const head = [];
18211
+ if (s.pickLabel)
18212
+ head.push(`**Picked ${s.pickLabel}**${s.suggested ? ` (suggested ${s.suggested})` : ""}`);
18213
+ if (s.verdict)
18214
+ head.push(`**${name(s.verdict)}**`);
18215
+ if (s.note)
18216
+ head.push(`“${quote(s.note)}”`);
18217
+ const open = !head.length && !s.notes.length;
18218
+ lines.push(`- ${s.number} ${s.title}: ${open ? "_open_" : head.join(" · ") || `${s.notes.length} note${s.notes.length === 1 ? "" : "s"}`}`);
18219
+ for (const n of s.notes)
18220
+ lines.push(` - ${noteLine(n)}`);
18221
+ }
18222
+ return lines.join(`
18223
+ `) + `
18224
+ `;
18225
+ }
18226
+ var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
18227
+ function plainMarkdown(text) {
18228
+ return text.replace(/\[([^\]\n]+)\]\([^)\s]+\)/g, "$1").replace(/`([^`\n]+)`/g, "$1").replace(/\*\*([^*\n]+)\*\*/g, "$1").replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, "$1$2").replace(/\s+/g, " ").trim();
18229
+ }
18230
+ function inlineMarkdown(text) {
18231
+ const codes = [];
18232
+ let s = text.replace(/`([^`\n]+)`/g, (_, c) => {
18233
+ codes.push(c);
18234
+ return `\x00${codes.length - 1}\x00`;
18235
+ });
18236
+ s = escapeHtml(s);
18237
+ s = s.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (m, t, href) => {
18238
+ const raw = href.replace(/&amp;/g, "&");
18239
+ if (!/^(https?:\/\/|\/|\.{0,2}\/|#)/i.test(raw) && /^[a-z][a-z0-9+.-]*:/i.test(raw))
18240
+ return m;
18241
+ return `<a href="${href}" target="_blank" rel="noreferrer">${t}</a>`;
18242
+ });
18243
+ s = s.replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>");
18244
+ s = s.replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, "$1<i>$2</i>");
18245
+ s = s.replace(/\n{2,}/g, "<br><br>").replace(/\n/g, " ");
18246
+ return s.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${escapeHtml(codes[Number(i)])}</code>`);
18247
+ }
18248
+
18249
+ // src/ui/review-options.ts
18250
+ var el = (doc, tag, cls) => {
18251
+ const n = doc.createElement(tag);
18252
+ if (cls)
18253
+ n.className = cls;
18254
+ return n;
18255
+ };
18256
+ function renderFacts(doc, groups) {
18257
+ const box = el(doc, "div", "knobs-wt-facts");
18258
+ for (const g of groups) {
18259
+ if (!g.facts.length && !g.title)
18260
+ continue;
18261
+ const group = el(doc, "section", "knobs-wt-fgroup");
18262
+ if (g.title) {
18263
+ const head = el(doc, "h3", "knobs-wt-fhead");
18264
+ head.innerHTML = inlineMarkdown(g.title);
18265
+ group.append(head);
18266
+ }
18267
+ if (g.facts.length)
18268
+ group.append(factList(doc, g.facts));
18269
+ box.append(group);
18270
+ }
18271
+ return box;
18272
+ }
18273
+ function factList(doc, facts) {
18274
+ const dl = el(doc, "dl", "knobs-wt-fterms");
18275
+ for (const f of facts) {
18276
+ const row = el(doc, "div", "knobs-wt-fact");
18277
+ const dt = el(doc, "dt");
18278
+ dt.innerHTML = inlineMarkdown(f.term);
18279
+ const dd = el(doc, "dd");
18280
+ if (f.code) {
18281
+ dd.dataset.code = "";
18282
+ for (const v of f.value) {
18283
+ const chip = el(doc, "code");
18284
+ chip.textContent = v;
18285
+ dd.append(chip);
18286
+ }
18287
+ } else
18288
+ for (const v of f.value) {
18289
+ const line = el(doc, "div");
18290
+ line.innerHTML = inlineMarkdown(v);
18291
+ dd.append(line);
18292
+ }
18293
+ row.append(dt, dd);
18294
+ dl.append(row);
18295
+ }
18296
+ return dl;
18297
+ }
18298
+ function optionTile(doc, o, options = {}) {
18299
+ const tile = el(doc, "div", "knobs-wt-option");
18300
+ const head = options.head ?? el(doc, "div");
18301
+ head.classList.add("knobs-wt-ohead");
18302
+ const kicker = el(doc, "span", "knobs-wt-okicker");
18303
+ const n = el(doc, "span");
18304
+ n.textContent = o.number;
18305
+ kicker.append(n);
18306
+ if (o.recommended) {
18307
+ const rec = el(doc, "span", "knobs-wt-orec");
18308
+ rec.textContent = "Recommended";
18309
+ kicker.append(rec);
18310
+ }
18311
+ const title = el(doc, "span", "knobs-wt-otitle");
18312
+ title.innerHTML = inlineMarkdown(o.title);
18313
+ head.append(kicker, title);
18314
+ tile.append(head);
18315
+ if (o.summary) {
18316
+ const p = el(doc, "p", "knobs-wt-osum");
18317
+ p.innerHTML = inlineMarkdown(o.summary);
18318
+ tile.append(p);
18319
+ }
18320
+ options.media?.(tile);
18321
+ if (o.facts?.length)
18322
+ tile.append(renderFacts(doc, [{ facts: o.facts }]));
18323
+ return { tile, head, kicker };
18324
+ }
18325
+ function layoutOptions(root, tiles) {
18326
+ let cols = 0;
18327
+ const layout = () => {
18328
+ const width = root.clientWidth;
18329
+ if (!width)
18330
+ return;
18331
+ const next = optionColumns(tiles.length, width);
18332
+ if (next === cols)
18333
+ return;
18334
+ cols = next;
18335
+ root.style.setProperty("--kw-cols", String(cols));
18336
+ const rows = Math.ceil(tiles.length / cols);
18337
+ tiles.forEach((t, i) => {
18338
+ const row = Math.floor(i / cols);
18339
+ t.dataset.col = String(i % cols);
18340
+ t.dataset.row = String(row);
18341
+ t.toggleAttribute("data-end", i % cols === cols - 1);
18342
+ t.toggleAttribute("data-last-row", row === rows - 1);
18343
+ });
18344
+ };
18345
+ layout();
18346
+ const win = root.ownerDocument.defaultView;
18347
+ if (!win || typeof win.ResizeObserver !== "function")
18348
+ return () => {};
18349
+ const ro = new win.ResizeObserver(layout);
18350
+ ro.observe(root);
18351
+ return () => ro.disconnect();
18352
+ }
18353
+
18354
+ // src/ui/walkthrough.ts
18355
+ init_selection_pill();
18356
+
18357
+ // src/ui/toast.ts
18358
+ init_look();
18359
+ init_layer();
18360
+ var regions = new WeakMap;
18361
+ function toast(message, options = {}) {
18362
+ const doc = options.within?.ownerDocument ?? options.doc ?? document;
18363
+ const win = doc.defaultView;
18364
+ const key = options.within ?? doc;
18365
+ let r = regions.get(key);
18366
+ if (!r || !r.el.isConnected) {
18367
+ const el = options.within ? Object.assign(doc.createElement("div"), { className: "knobs-root knobs-ui knobs-toast" }) : layerElement(doc, "knobs-layer knobs-toast", options.look);
18368
+ if (options.within) {
18369
+ injectUiStyles(doc);
18370
+ options.within.append(el);
18371
+ }
18372
+ el.setAttribute("role", "status");
18373
+ el.setAttribute("aria-live", "polite");
18374
+ const text = doc.createElement("span");
18375
+ text.className = "knobs-toast-text";
18376
+ const button = doc.createElement("button");
18377
+ button.type = "button";
18378
+ button.className = "knobs-toast-action";
18379
+ el.append(text, button);
18380
+ const region = {
18381
+ el,
18382
+ text,
18383
+ button,
18384
+ motion: presence(el, { scale: 0.96, blur: 4, dy: 10, duration: 0.36 }),
18385
+ timer: 0
18386
+ };
18387
+ r = region;
18388
+ regions.set(key, region);
18389
+ el.addEventListener("pointerenter", () => win.clearTimeout(region.timer));
18390
+ el.addEventListener("pointerleave", () => {
18391
+ if (region.motion.shown)
18392
+ arm(region, 1600);
18393
+ });
18394
+ }
18395
+ const region = r;
18396
+ const arm = (reg, ms) => {
18397
+ win.clearTimeout(reg.timer);
18398
+ reg.timer = win.setTimeout(() => void reg.motion.hide(), ms);
18399
+ };
18400
+ region.text.textContent = message;
18401
+ region.el.dataset.tone = options.tone ?? "info";
18402
+ const action = options.action;
18403
+ region.button.hidden = !action;
18404
+ region.button.textContent = action?.label ?? "";
18405
+ region.button.onclick = action ? () => {
18406
+ action.run();
18407
+ region.motion.hide();
18408
+ } : null;
18409
+ region.motion.show();
18410
+ arm(region, options.linger ?? (options.tone === "error" ? 6000 : 3200));
18411
+ return {
18412
+ dismiss() {
18413
+ if (region.text.textContent !== message)
18414
+ return;
18415
+ win.clearTimeout(region.timer);
18416
+ region.motion.hide();
18417
+ }
18418
+ };
18419
+ }
18420
+
18421
+ // src/ui/walkthrough-annotations.ts
18422
+ var EYE = "M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z";
18423
+ var EYE_OFF = "M53.92,34.62A8,8,0,1,0,42.08,45.38L61.32,66.55C25,88.84,9.38,123.2,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208a127.11,127.11,0,0,0,52.07-10.83l22,24.21a8,8,0,1,0,11.84-10.76Zm47.33,75.84,41.67,45.85a32,32,0,0,1-41.67-45.85ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.16,133.16,0,0,1,25,128c4.69-8.79,19.66-33.39,47.35-49.38l18,19.75a48,48,0,0,0,63.66,70l14.73,16.2A112,112,0,0,1,128,192Zm6-95.43a8,8,0,0,1,3-15.72,48.16,48.16,0,0,1,38.77,42.64,8,8,0,0,1-7.22,8.71,6.39,6.39,0,0,1-.75,0,8,8,0,0,1-8-7.26A32.09,32.09,0,0,0,134,96.57Zm113.28,34.69c-.42.94-10.55,23.37-33.36,43.8a8,8,0,1,1-10.67-11.92A132.77,132.77,0,0,0,231.05,128a133.15,133.15,0,0,0-23.12-30.77C185.67,75.19,158.78,64,128,64a118.37,118.37,0,0,0-19.36,1.57A8,8,0,1,1,106,49.79,134,134,0,0,1,128,48c34.88,0,66.57,13.26,91.66,38.35,18.83,18.83,27.3,37.62,27.65,38.41A8,8,0,0,1,247.31,131.26Z";
18424
+ var glyph = (d) => `<svg class="knobs-icon" viewBox="0 0 256 256" width="16" height="16" fill="currentColor" aria-hidden="true" focusable="false"><path d="${d}"/></svg>`;
18425
+ var ARROW = '<svg class="knobs-wt-anno-arrow" viewBox="0 0 28 28" width="28" height="28" aria-hidden="true"><path d="M3 3 L24 24 M24 24 L24 13 M24 24 L13 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
18426
+ var shapeOf = (a) => a.shape ?? (a.w !== undefined && a.h !== undefined ? "box" : "dot");
18427
+ var showsAt = (a, time) => !a.at || time === undefined || time >= a.at[0] && time <= a.at[1];
18428
+ function placeMark(a, rect, view = ALL) {
18429
+ const w = a.w ?? 0;
18430
+ const h = a.h ?? 0;
18431
+ const x = (a.x - view.u) / view.w;
18432
+ const y = (a.y - view.v) / view.h;
18433
+ const ww = w / view.w;
18434
+ const hh = h / view.h;
18435
+ if (x + ww < 0 || y + hh < 0 || x > 1 || y > 1)
18436
+ return null;
18437
+ return { x: rect.x + x * rect.w, y: rect.y + y * rect.h, w: ww * rect.w, h: hh * rect.h };
18438
+ }
18439
+ var ALL = { u: 0, v: 0, w: 1, h: 1 };
18440
+ var scopeOf = (el) => el.closest(".knobs-walkthrough") ?? el.closest(".knobs-wt-slot") ?? el;
18441
+ var annotationsOn = (scope) => scope.dataset.annotations !== "off";
18442
+ function setAnnotations(scope, on) {
18443
+ if (on)
18444
+ delete scope.dataset.annotations;
18445
+ else
18446
+ scope.dataset.annotations = "off";
18447
+ for (const b of Array.from(scope.querySelectorAll(".knobs-wt-anno-toggle")))
18448
+ syncToggle(b, on);
18449
+ }
18450
+ function syncToggle(b, on) {
18451
+ b.setAttribute("aria-pressed", String(on));
18452
+ b.title = on ? "Hide annotations (a)" : "Show annotations (a)";
18453
+ b.innerHTML = glyph(on ? EYE : EYE_OFF);
18454
+ }
18455
+ function annotationLayer(host, list, place, corner = host) {
18456
+ const doc = host.ownerDocument;
18457
+ const layer = doc.createElement("div");
18458
+ layer.className = "knobs-wt-anno";
18459
+ layer.setAttribute("aria-hidden", "true");
18460
+ host.append(layer);
18461
+ const toggle = doc.createElement("button");
18462
+ toggle.type = "button";
18463
+ toggle.className = "knobs-wt-anno-toggle";
18464
+ toggle.setAttribute("aria-label", "Annotations");
18465
+ syncToggle(toggle, annotationsOn(scopeOf(host)));
18466
+ toggle.addEventListener("click", (e) => {
18467
+ e.stopPropagation();
18468
+ const scope = scopeOf(host);
18469
+ setAnnotations(scope, !annotationsOn(scope));
18470
+ });
18471
+ corner.append(toggle);
18472
+ let marks = [];
18473
+ const make = (a) => {
18474
+ const m = doc.createElement("div");
18475
+ const shape = shapeOf(a);
18476
+ m.className = "knobs-wt-mark";
18477
+ m.dataset.shape = shape;
18478
+ m.dataset.hidden = "";
18479
+ if (shape === "arrow")
18480
+ m.innerHTML = ARROW;
18481
+ if (a.label) {
18482
+ const l = doc.createElement("span");
18483
+ l.className = "knobs-wt-mark-label";
18484
+ l.textContent = a.label;
18485
+ m.append(l);
18486
+ }
18487
+ layer.append(m);
18488
+ return m;
18489
+ };
18490
+ let settled = false;
18491
+ const update = () => {
18492
+ const p = place();
18493
+ if (marks.length !== p.rects.length) {
18494
+ for (const row of marks)
18495
+ for (const m of row)
18496
+ m.remove();
18497
+ marks = p.rects.map(() => list.map(make));
18498
+ }
18499
+ p.rects.forEach((rect, i) => {
18500
+ list.forEach((a, j) => {
18501
+ const m = marks[i][j];
18502
+ const box = placeMark(a, rect, p.view);
18503
+ const show = box !== null && showsAt(a, p.time);
18504
+ if (box) {
18505
+ m.style.transform = `translate(${box.x.toFixed(2)}px,${box.y.toFixed(2)}px)`;
18506
+ m.style.width = `${box.w.toFixed(2)}px`;
18507
+ m.style.height = `${box.h.toFixed(2)}px`;
18508
+ }
18509
+ m.toggleAttribute("data-hidden", !show);
18510
+ });
18511
+ });
18512
+ if (!settled) {
18513
+ settled = true;
18514
+ requestAnimationFrame(() => requestAnimationFrame(() => layer.dataset.settled = ""));
18515
+ }
18516
+ };
18517
+ const ro = new ResizeObserver(() => update());
18518
+ ro.observe(host);
18519
+ update();
18520
+ return {
18521
+ element: layer,
18522
+ update,
18523
+ dispose() {
18524
+ ro.disconnect();
18525
+ layer.remove();
18526
+ toggle.remove();
18527
+ }
18528
+ };
18529
+ }
18530
+
18531
+ // src/ui/walkthrough-blocks.ts
18532
+ init_motion();
18533
+
18534
+ // src/ui/history.ts
18535
+ init_motion();
18536
+ init_glide();
18537
+
18538
+ // src/ui/history-css.ts
18539
+ var historyCss = `
18540
+ :where(.knobs-history,.knobs-history *){box-sizing:border-box}
18541
+ .knobs-ui.knobs-history{--hy-gutter:128px;--hy-lane:24px;--hy-bar:16px;--hy-axis:20px;--hy-bandrow:0px;--hy-c:var(--_k-fg-3);
18542
+ --hy-fade:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);
18543
+ position:relative;display:flex;flex-direction:column;width:100%;min-width:0;outline:none;color:var(--_k-fg);
18544
+ font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";
18545
+ text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased;user-select:none;-webkit-user-select:none}
18546
+ .knobs-history[data-bands]{--hy-bandrow:var(--_k-leading-small)}
18547
+ .knobs-history[data-narrow]{--hy-gutter:0px}
18548
+ .knobs-history [hidden]{display:none!important}
18549
+ :where(.knobs-ui) :is(.knobs-hy-ev,.knobs-hy-span,.knobs-hy-jump,.knobs-hy-more){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
18550
+ background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
18551
+ .knobs-history [data-tone="good"]{--hy-c:var(--_k-ok)}
18552
+ .knobs-history [data-tone="warn"]{--hy-c:var(--_k-warn)}
18553
+ .knobs-history [data-tone="bad"]{--hy-c:var(--_k-danger)}
18554
+ .knobs-history [data-tone="info"]{--hy-c:var(--_k-info)}
18555
+ .knobs-history [data-tone="dim"]{--hy-c:var(--_k-fg-3)}
18556
+
18557
+ /* ——— the chart: lane labels beside the plot (above each lane where it's narrow) ——— */
18558
+ .knobs-hy-chart{position:relative;display:grid;grid-template-columns:var(--hy-gutter) minmax(0,1fr)}
18559
+ .knobs-hy-axis{position:relative;grid-column:2;grid-row:1;height:calc(var(--hy-bandrow) + var(--hy-axis))}
18560
+ :is(.knobs-hy-tick,.knobs-hy-now,.knobs-hy-blabel){position:absolute;white-space:nowrap;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
18561
+ .knobs-hy-tick{bottom:2px;padding-left:4px}
18562
+ .knobs-hy-now{bottom:2px;color:var(--_k-accent-fg);font-weight:var(--_k-weight-label)}
18563
+ .knobs-hy-blabel{top:0;padding-left:4px;overflow:hidden;color:var(--_k-fg-2)}
18564
+ .knobs-hy-label{grid-column:1;align-self:center;min-width:0;margin-right:var(--_k-space-title);overflow:hidden;white-space:nowrap;
18565
+ font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
18566
+ .knobs-history[data-narrow] .knobs-hy-label{grid-column:2;align-self:end;margin:0}
18567
+ :is(.knobs-hy-label,.knobs-hy-blabel)[data-overflow]{-webkit-mask-image:var(--hy-fade);mask-image:var(--hy-fade)}
18568
+ .knobs-hy-track{position:relative;grid-column:2;height:var(--hy-lane)}
18569
+ .knobs-hy-layer{position:absolute;inset:0;grid-column:2;grid-row:1/-1;pointer-events:none}
18570
+
18571
+ /* Bands and month lines behind the lanes; today, the selection and the pointer's line over them. */
18572
+ .knobs-hy-band{position:absolute;top:0;bottom:0;background:var(--_k-fill);box-shadow:inset 1px 0 0 var(--_k-border-subtle)}
18573
+ :is(.knobs-hy-grid,.knobs-hy-today,.knobs-hy-hover,.knobs-hy-sel){position:absolute;left:0;top:calc(var(--hy-bandrow) + var(--hy-axis));bottom:0;width:1px;margin-left:-.5px}
18574
+ .knobs-hy-grid{background:var(--_k-border-subtle)}
18575
+ .knobs-hy-today{top:calc(var(--hy-bandrow) + var(--hy-axis) - 4px);background:var(--_k-accent)}
18576
+ .knobs-hy-hover{background:color-mix(in oklch,var(--_k-fg) 32%,transparent);opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
18577
+ .knobs-hy-hover[data-on]{opacity:1;transition-duration:calc(var(--_k-hover-in) * 1ms)}
18578
+ .knobs-hy-sel{background:var(--_k-fg-2);opacity:0}
18579
+ .knobs-hy-sel[data-on]{opacity:.6}
18580
+
18581
+ /* Spans: a soft bar in their state's tone, inferred ones hatched; the tag at the end. */
18582
+ .knobs-hy-span{position:absolute;top:calc((var(--hy-lane) - var(--hy-bar)) / 2);height:var(--hy-bar);border-radius:var(--_k-radius-sm);cursor:pointer;
18583
+ background:color-mix(in oklch,var(--hy-c) 24%,transparent);transition:background calc(var(--_k-hover-out) * 1ms),box-shadow calc(var(--_k-hover-out) * 1ms)}
18584
+ .knobs-hy-span[data-inferred]{background:repeating-linear-gradient(135deg,color-mix(in oklch,var(--hy-c) 34%,transparent) 0 2px,color-mix(in oklch,var(--hy-c) 9%,transparent) 2px 6px)}
18585
+ .knobs-hy-span[data-hover]{background-color:color-mix(in oklch,var(--hy-c) 34%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18586
+ .knobs-hy-span[data-selected]{box-shadow:inset 0 0 0 1px var(--hy-c)}
18587
+ .knobs-hy-span[data-clip-start]{border-top-left-radius:0;border-bottom-left-radius:0;-webkit-mask-image:linear-gradient(to right,transparent,#000 16px);mask-image:linear-gradient(to right,transparent,#000 16px)}
18588
+ .knobs-hy-span[data-clip-end]{border-top-right-radius:0;border-bottom-right-radius:0;-webkit-mask-image:linear-gradient(to left,transparent,#000 16px);mask-image:linear-gradient(to left,transparent,#000 16px)}
18589
+ .knobs-hy-tag{position:absolute;top:0;right:6px;font-size:var(--_k-type-small);line-height:var(--hy-bar);white-space:nowrap;pointer-events:none;
18590
+ color:color-mix(in oklch,var(--hy-c) 55%,var(--_k-fg))}
18591
+ .knobs-hy-tag[data-place="after"]{right:auto;left:calc(100% + 6px)}
18592
+ .knobs-hy-tag[data-place="none"]{visibility:hidden}
18593
+
18594
+ /* Event marks (their kind named in the tooltip): a dot for a commit, a diamond for a release, a cross for a deletion, a square for a
18595
+ channel's switch, a triangle for a finding; inferred ones hollow. */
18596
+ .knobs-hy-ev{position:absolute;top:0;width:16px;height:var(--hy-lane);margin-left:-8px;cursor:pointer;--hy-c:var(--_k-fg-2);--hy-s:9px}
18597
+ .knobs-hy-ev::before{content:"";position:absolute;left:50%;top:50%;width:var(--hy-s);height:var(--hy-s);margin:calc(var(--hy-s) / -2) 0 0 calc(var(--hy-s) / -2);
18598
+ border-radius:50%;background:var(--hy-c);transition:scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
18599
+ .knobs-hy-ev[data-range]{width:calc(var(--hy-w) + 16px)}
18600
+ .knobs-hy-ev[data-range]::before{left:calc(8px - var(--hy-s) / 2);right:calc(8px - var(--hy-s) / 2);width:auto;margin-left:0;border-radius:var(--_k-round)}
18601
+ .knobs-hy-ev[data-type="release"]{--hy-s:8px}
18602
+ .knobs-hy-ev[data-type="release"]::before{border-radius:1.5px;rotate:45deg}
18603
+ .knobs-hy-ev[data-type="channel"]{--hy-s:8px}
18604
+ .knobs-hy-ev[data-type="channel"]::before{border-radius:2px}
18605
+ .knobs-hy-ev[data-type="audit"]{--hy-s:10px}
18606
+ .knobs-hy-ev[data-type="audit"]::before{border-radius:0;clip-path:polygon(50% 4%,100% 92%,0 92%)}
18607
+ .knobs-hy-ev[data-type="deletion"]::before{border-radius:0;rotate:45deg;
18608
+ background:linear-gradient(var(--hy-c),var(--hy-c)) center/100% 1.75px no-repeat,linear-gradient(var(--hy-c),var(--hy-c)) center/1.75px 100% no-repeat}
18609
+ .knobs-hy-ev[data-inferred]:not([data-type="audit"],[data-type="deletion"])::before{background:transparent;box-shadow:inset 0 0 0 1.5px var(--hy-c)}
18610
+ .knobs-hy-ev[data-inferred]:is([data-type="audit"],[data-type="deletion"])::before{opacity:.5}
18611
+ /* Held-back marks (commits, by default) come up while the pointer is on their lane, and stay with
18612
+ "Show commits" or while selected. */
18613
+ .knobs-hy-ev[data-quiet]{opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
18614
+ .knobs-hy-track:hover .knobs-hy-ev[data-quiet]{opacity:.6;transition-duration:calc(var(--_k-motion-snappy-ms) * 1ms)}
18615
+ :is(.knobs-history[data-all] .knobs-hy-ev[data-quiet],.knobs-hy-ev[data-quiet]:is([data-selected],[data-hover])){opacity:1}
18616
+ .knobs-hy-ev[data-hover]::before{scale:1.25}
18617
+ .knobs-hy-ev[data-selected]::before{scale:1.3}
18618
+ .knobs-hy-ev[data-selected]::after{content:"";position:absolute;left:50%;top:50%;width:calc(var(--hy-s) + 9px);height:calc(var(--hy-s) + 9px);
18619
+ margin:calc((var(--hy-s) + 9px) / -2) 0 0 calc((var(--hy-s) + 9px) / -2);border-radius:50%;box-shadow:0 0 0 1.5px var(--hy-c)}
18620
+ .knobs-hy-ev[data-range][data-selected]::after{left:3px;right:3px;width:auto;margin-left:0;border-radius:var(--_k-round)}
18621
+ .knobs-history:focus-visible .knobs-hy-ev[data-selected]::after{box-shadow:0 0 0 2px var(--_k-focus)}
18622
+ .knobs-history:focus-visible .knobs-hy-span[data-selected]{box-shadow:inset 0 0 0 2px var(--_k-focus)}
18623
+
18624
+ /* ——— under the chart: "Show commits", and a legend only when inferred and verified both appear ——— */
18625
+ .knobs-hy-foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--_k-space-inside) var(--_k-space-title);margin-top:var(--_k-space-sibling);padding-left:var(--hy-gutter);
18626
+ font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
18627
+ .knobs-hy-legend{display:flex;flex-wrap:wrap;gap:var(--_k-space-inside) var(--_k-space-title)}
18628
+ .knobs-ui .knobs-hy-more{height:20px;margin-left:-6px;padding:0 6px;border-radius:var(--_k-radius-sm);color:var(--_k-fg-3);cursor:pointer;
18629
+ transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
18630
+ .knobs-ui .knobs-hy-more:is(:hover,:focus-visible){background:var(--_k-fill);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18631
+ .knobs-hy-key{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
18632
+ .knobs-hy-swatch{flex:none;width:14px;height:8px;border-radius:2px;background:color-mix(in oklch,var(--hy-c) 34%,transparent)}
18633
+ .knobs-hy-swatch[data-inferred]{background:repeating-linear-gradient(135deg,color-mix(in oklch,var(--hy-c) 60%,transparent) 0 1.5px,transparent 1.5px 4px)}
18634
+
18635
+ /* ——— the detail: one area under the chart, its height on a spring ——— */
18636
+ .knobs-hy-detail{position:relative;overflow:hidden;user-select:text;-webkit-user-select:text}
18637
+ .knobs-hy-detail[data-moving]{-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent)}
18638
+ .knobs-hy-dc{padding:var(--_k-space-title) 0 0 var(--hy-gutter);transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
18639
+ .knobs-hy-dc:is([data-enter],[data-leave]){opacity:0}
18640
+ .knobs-hy-dc[data-leave]{position:absolute;left:0;right:0;top:0;transition-duration:calc(var(--_k-motion-snappy-ms) * .8ms)}
18641
+ .knobs-hy-dmeta{display:flex;flex-wrap:wrap;gap:0 var(--_k-space-sibling);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
18642
+ .knobs-hy-dmeta>[data-tone]{color:var(--hy-c)}
18643
+ .knobs-hy-dtitle{margin-top:var(--_k-space-inside);font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-weight:var(--_k-weight-title);color:var(--_k-fg);text-wrap:balance}
18644
+ .knobs-hy-dtext{margin:var(--_k-space-inside) 0 0;max-width:68ch;font-size:var(--_k-type-body);line-height:var(--_k-leading-body);color:var(--_k-fg-2);text-wrap:pretty}
18645
+ .knobs-hy-dtext code{font:calc(1em - 1px) var(--_k-mono);padding:1px 4px;border-radius:var(--_k-radius-sm);background:var(--_k-fill)}
18646
+ .knobs-hy-dfoot{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:var(--_k-space-sibling)}
18647
+ .knobs-hy-dfoot:empty{display:none}
18648
+ .knobs-hy-evidence{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:var(--_k-radius-sm);background:var(--_k-fill);color:var(--_k-fg-2);
18649
+ font:var(--_k-type-small)/1 var(--_k-mono);text-decoration:none;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
18650
+ a.knobs-hy-evidence:hover{background:var(--_k-fill-hover);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18651
+ .knobs-ui .knobs-hy-jump{display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 6px 0 8px;border-radius:var(--_k-radius-sm);color:var(--_k-fg-2);cursor:pointer;
18652
+ font-size:var(--_k-type-small);transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
18653
+ .knobs-ui .knobs-hy-jump:is(:hover,:focus-visible){background:var(--_k-fill);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18654
+ .knobs-hy-jump svg{width:12px;height:12px}
18655
+
18656
+ /* ——— the pointer's tooltip: title, date, one line ——— */
18657
+ .knobs-hy-tip{position:absolute;left:0;top:0;max-width:280px;padding:6px 8px;border-radius:var(--_k-radius-md);background:var(--_k-bg-overlay);
18658
+ box-shadow:var(--_k-shadow-overlay),0 0 0 1px var(--_k-border);pointer-events:none;opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
18659
+ .knobs-hy-tip[data-on]{opacity:1;transition-duration:calc(var(--_k-hover-in) * 1ms)}
18660
+ .knobs-hy-tt{font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight-label);color:var(--_k-fg);text-wrap:balance}
18661
+ :is(.knobs-hy-td,.knobs-hy-tl){font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
18662
+ .knobs-hy-tl{margin-top:2px;color:var(--_k-fg-2);text-wrap:pretty}
18663
+ .knobs-hy-tl:empty{display:none}
18664
+
18665
+ @media(prefers-reduced-motion:reduce){
18666
+ .knobs-history :where(*,::before,::after){transition-property:opacity,background,color,box-shadow!important}
18667
+ .knobs-hy-dc{transition-duration:calc(var(--_k-hover-out) * 1ms)!important}
18668
+ }
18669
+ `;
18670
+
18671
+ // src/ui/history-model.ts
18672
+ var MS_DAY = 86400000;
18673
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
18674
+ var dayOf = (y, m, d) => Math.floor(Date.UTC(y, m, d) / MS_DAY);
18675
+ var parts = (day) => {
18676
+ const t = new Date(day * MS_DAY);
18677
+ return { y: t.getUTCFullYear(), m: t.getUTCMonth(), d: t.getUTCDate() };
18678
+ };
18679
+ var todayDay = (now = Date.now()) => {
18680
+ const t = new Date(now);
18681
+ return dayOf(t.getFullYear(), t.getMonth(), t.getDate());
18682
+ };
18683
+ var monthIndex = (name) => MONTHS.findIndex((m) => m.toLowerCase() === name.slice(0, 3).toLowerCase());
18684
+ function parseDay(text, year) {
18685
+ const s = text.trim();
18686
+ const iso = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
18687
+ if (iso) {
18688
+ const y = Number(iso[1]);
18689
+ const day = dayOf(y, Number(iso[2]) - 1, Number(iso[3]));
18690
+ return { from: day, to: day, year: y };
18414
18691
  }
18415
- return out;
18416
- }
18417
- function findPR(prs, n, repo, kind = "pr") {
18418
- const entry = prs[n];
18419
- const known = entry && (entry.kind ?? "pr") === kind ? entry : undefined;
18420
- const path = kind === "issue" ? "issues" : "pull";
18421
- const href = repo ? `https://github.com/${repo}/${path}/${n}` : undefined;
18422
- const base = kind === "issue" ? { kind } : {};
18423
- if (known)
18424
- return known.href || !href ? { ...base, ...known } : { ...base, ...known, href };
18425
- return href ? { ...base, href } : kind === "issue" ? { kind } : null;
18426
- }
18427
- var issueNumber = (href) => /^https?:\/\/[^/]+\/.+\/issues\/(\d+)\/?(?:[?#].*)?$/.exec(href)?.[1] ?? null;
18428
- var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
18429
- function withKickers(sections, source, kickers = "pr") {
18430
- if (kickers === false)
18431
- return [...sections];
18432
- const noun = typeof kickers === "object" ? kickers.noun.trim() : "PR";
18433
- const counted = typeof kickers === "object" ? sections : sections.filter((s) => s.link && isPullRequest(s.link.href));
18434
- return sections.map((s, i) => {
18435
- const own = source[i]?.kicker;
18436
- if (own !== undefined || s.kicker)
18437
- return s;
18438
- const at = counted.indexOf(s);
18439
- return at < 0 ? s : { ...s, kicker: `${noun} ${at + 1} of ${counted.length}`.trim() };
18440
- });
18441
- }
18442
- function tableStacks(stack, room, natural) {
18443
- if (stack !== "auto")
18444
- return stack === "always";
18445
- return room > 0 && natural > room + 1;
18446
- }
18447
- function noteBarReserve(bar, inset, gap) {
18448
- return Math.max(0, Math.ceil(bar)) + inset + gap;
18449
- }
18450
- function parseAt(at) {
18451
- if (!at)
18692
+ const m = /^([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:\s*[–—-]\s*(?:([A-Za-z]{3,9})\.?\s+)?(\d{1,2}))?(?:,?\s+(\d{4}))?$/.exec(s);
18693
+ if (!m)
18452
18694
  return null;
18453
- const t = Date.parse(at);
18454
- return Number.isFinite(t) ? t : null;
18455
- }
18456
- function sortFeed(items) {
18457
- return items.map((item, i) => ({ item, i })).sort((a, b) => {
18458
- const x = a.item.at ?? -Infinity;
18459
- const y = b.item.at ?? -Infinity;
18460
- return x !== y ? y - x : b.i - a.i;
18461
- }).map((e) => e.item);
18462
- }
18463
- function ago(at, now) {
18464
- const s = Math.max(0, (now - at) / 1000);
18465
- if (s < 45)
18466
- return "just now";
18467
- if (s < 3600)
18468
- return `${Math.round(s / 60)}m ago`;
18469
- if (s < 86400)
18470
- return `${Math.round(s / 3600)}h ago`;
18471
- if (s < 7 * 86400)
18472
- return `${Math.round(s / 86400)}d ago`;
18473
- return new Date(at).toISOString().slice(0, 10);
18474
- }
18475
- function statusLook(status) {
18476
- const id = slug(status);
18477
- const tone = id === "ready" || id === "merged" || id === "shipped" ? "good" : id === "needs-taste" || id === "needs-review" || id === "review" ? "warn" : id === "blocked" || id === "broken" || id === "failing" ? "bad" : id === "draft" || id === "wip" ? "dim" : "info";
18478
- return { label: label(status), tone };
18479
- }
18480
- function verdictTone(id, index) {
18481
- if (/^(ship|yes|good|approve|keep|lgtm)/.test(id))
18482
- return "good";
18483
- if (/^(change|fix|revise|tweak|redo)/.test(id))
18484
- return "warn";
18485
- if (/^(no|reject|drop|revert|bad)/.test(id))
18486
- return "bad";
18487
- if (/^(unsure|maybe|skip|later)/.test(id))
18488
- return "dim";
18489
- return index === 0 ? "good" : "dim";
18490
- }
18491
- var EMPTY_STATE = { steps: {} };
18492
- function setFeedback(state, key, patch) {
18493
- const next = { ...state.steps[key] };
18494
- for (const [k, v] of Object.entries(patch)) {
18495
- if (v === undefined)
18496
- continue;
18497
- if (v === null || v.trim() === "")
18498
- delete next[k];
18499
- else
18500
- next[k] = v;
18501
- }
18502
- const steps = { ...state.steps };
18503
- if (Object.keys(next).length)
18504
- steps[key] = next;
18505
- else
18506
- delete steps[key];
18507
- return { ...state, steps };
18508
- }
18509
- var hasContent = (n) => Boolean(n.text?.trim() || n.voice || n.ink?.paths.length);
18510
- function addNote(state, note) {
18511
- if (!hasContent(note))
18512
- return state;
18513
- return { ...state, notes: [...state.notes ?? [], note] };
18514
- }
18515
- function removeNote(state, id) {
18516
- return { ...state, notes: (state.notes ?? []).filter((n) => n.id !== id) };
18517
- }
18518
- var notesOn = (state, key) => (state.notes ?? []).filter((n) => n.step === key);
18519
- function isAnswered(step, state) {
18520
- if (step.ask === "none")
18521
- return false;
18522
- const fb = state.steps[step.key];
18523
- if (step.ask === "pick")
18524
- return fb?.pick !== undefined;
18525
- return Boolean(fb?.verdict || fb?.note || notesOn(state, step.key).length);
18526
- }
18527
- function digitChoice(step, fb, key) {
18528
- if (step.ask !== "pick" || !/^[1-9]$/.test(key))
18529
- return;
18530
- const id = step.options[Number(key) - 1]?.id;
18531
- if (id === undefined)
18532
- return;
18533
- return fb?.pick === id ? null : id;
18534
- }
18535
- function walkthroughProgress(w, state) {
18536
- const asked = w.steps.filter((s) => s.ask !== "none");
18537
- const done = asked.filter((s) => isAnswered(s, state));
18538
- const next = asked.find((s) => !isAnswered(s, state));
18539
- return { done: done.length, total: asked.length, next: next?.key ?? null };
18540
- }
18541
- function adjacentStep(keys, current, dir) {
18542
- if (!keys.length)
18543
- return;
18544
- const i = current === null ? -1 : keys.indexOf(current);
18545
- if (i < 0)
18546
- return dir > 0 ? keys[0] : keys[keys.length - 1];
18547
- return keys[Math.max(0, Math.min(keys.length - 1, i + dir))];
18695
+ const m1 = monthIndex(m[1]);
18696
+ const m2 = m[3] ? monthIndex(m[3]) : m1;
18697
+ if (m1 < 0 || m2 < 0)
18698
+ return null;
18699
+ const written = m[5] ? Number(m[5]) : undefined;
18700
+ const y = written ?? year;
18701
+ if (y === undefined)
18702
+ return null;
18703
+ const from = dayOf(y, m1, Number(m[2]));
18704
+ let to = m[4] ? dayOf(y, m2, Number(m[4])) : from;
18705
+ if (to < from)
18706
+ to = dayOf(y + 1, m2, Number(m[4]));
18707
+ return { from, to, ...written !== undefined ? { year: written } : {} };
18548
18708
  }
18549
- var LOG_MAX = 40;
18550
- function pushContext(log, event, max = LOG_MAX) {
18551
- const last = log[log.length - 1];
18552
- const merge = last && last.what === event.what && last.step === event.step && (event.what === "seek" || event.what === "step") && event.at - last.at < 1500;
18553
- const next = merge ? [...log.slice(0, -1), event] : [...log, event];
18554
- return next.slice(-max);
18709
+ function formatDay(day) {
18710
+ const { y, m, d } = parts(day);
18711
+ return `${MONTHS[m]} ${d}, ${y}`;
18555
18712
  }
18556
- function recentContext(log, now, windowMs = 90000, min = 4) {
18557
- const inWindow = log.filter((e) => now - e.at <= windowMs);
18558
- return inWindow.length >= min ? inWindow : log.slice(-min);
18713
+ function formatRange(from, to, open = false) {
18714
+ if (open)
18715
+ return `${formatDay(from)} – today`;
18716
+ if (to <= from)
18717
+ return formatDay(from);
18718
+ const a = parts(from);
18719
+ const b = parts(to);
18720
+ if (a.y === b.y && a.m === b.m)
18721
+ return `${MONTHS[a.m]} ${a.d}–${b.d}, ${a.y}`;
18722
+ if (a.y === b.y)
18723
+ return `${MONTHS[a.m]} ${a.d} – ${MONTHS[b.m]} ${b.d}, ${a.y}`;
18724
+ return `${formatDay(from)} – ${formatDay(to)}`;
18559
18725
  }
18560
- function formatTime(seconds) {
18561
- const t = Math.max(0, Math.round(seconds * 10) / 10);
18562
- const m = Math.floor(t / 60);
18563
- const s = (t - m * 60).toFixed(1).padStart(4, "0");
18564
- return `${m}:${s}`;
18726
+ var formatDays = (n) => `${n} ${n === 1 ? "day" : "days"}`;
18727
+ function guessTone(state) {
18728
+ const s = state.toLowerCase();
18729
+ if (/broken|bad|fail|missing|deleted|down/.test(s))
18730
+ return "bad";
18731
+ if (/warn|redirect|partial|beta|next|pre/.test(s))
18732
+ return "warn";
18733
+ if (/work|ok|good|fixed|live|green/.test(s))
18734
+ return "good";
18735
+ return "dim";
18565
18736
  }
18566
- function inkPath(points, w, h) {
18567
- if (!points.length || w <= 0 || h <= 0)
18568
- return "";
18569
- const n = (v) => String(Math.round(Math.max(0, Math.min(1, v)) * 1000) / 1000);
18570
- const kept = [points[0]];
18571
- for (const p of points.slice(1)) {
18572
- const l = kept[kept.length - 1];
18573
- if (Math.hypot(p.x - l.x, p.y - l.y) >= 1)
18574
- kept.push(p);
18737
+ var bandLabel = (heading) => heading.replace(/^\s*\d+\s*[·.:)\-–]\s*/, "").replace(/\s*\([^)]*\)\s*$/, "").trim();
18738
+ function fromGroups(groups, today) {
18739
+ const events = [];
18740
+ const bands = [];
18741
+ const all = groups.flatMap((g) => g.events ?? g.rows ?? []);
18742
+ let year = all.map((e) => parseDay(e.date, 0)?.year).find((y) => y !== undefined) ?? parts(today).y;
18743
+ for (const g of groups) {
18744
+ let lo = Infinity;
18745
+ let hi = -Infinity;
18746
+ for (const e of g.events ?? g.rows ?? []) {
18747
+ const d = parseDay(e.date, year);
18748
+ if (!d)
18749
+ continue;
18750
+ if (d.year !== undefined)
18751
+ year = d.year;
18752
+ lo = Math.min(lo, d.from);
18753
+ hi = Math.max(hi, d.to);
18754
+ events.push({
18755
+ ...e,
18756
+ date: iso(d.from),
18757
+ ...d.to > d.from ? { to: iso(d.to) } : {},
18758
+ lane: e.lane ?? "code"
18759
+ });
18760
+ }
18761
+ const label = g.heading ? bandLabel(g.heading) : "";
18762
+ if (label && lo <= hi)
18763
+ bands.push({ from: iso(lo), to: iso(hi), label });
18575
18764
  }
18576
- return kept.map((p, i) => `${i ? "L" : "M"}${n(p.x / w)} ${n(p.y / h)}`).join("");
18577
- }
18578
- function zoomWords(z) {
18579
- const times = Math.round(1 / Math.max(0.001, Math.max(z.w, z.h)) * 10) / 10;
18580
- return `${times}× at ${Math.round(z.u * 100)}%, ${Math.round(z.v * 100)}%`;
18765
+ return { events, bands };
18581
18766
  }
18582
- function describeMedia(m) {
18583
- if (!m)
18584
- return "";
18585
- const parts = [];
18586
- if (m.file)
18587
- parts.push(`${m.kind} ${m.file}`);
18588
- else if (m.time !== undefined)
18589
- parts.push(`${m.kind} at ${formatTime(m.time)}`);
18590
- else
18591
- parts.push(m.kind);
18592
- if (m.chapter)
18593
- parts[0] += ` in “${m.chapter}”`;
18594
- if (m.loop)
18595
- parts.push(`loop ${formatTime(m.loop.start)}–${formatTime(m.loop.end)}`);
18596
- if (m.zoom)
18597
- parts.push(`zoomed ${zoomWords(m.zoom)}`);
18598
- if (m.mode)
18599
- parts.push(m.mode);
18600
- if (m.full)
18601
- parts.push("full window");
18602
- return parts.join(", ");
18767
+ function iso(day) {
18768
+ const { y, m, d } = parts(day);
18769
+ return `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
18603
18770
  }
18604
- function readState(raw) {
18605
- if (!raw)
18606
- return EMPTY_STATE;
18607
- try {
18608
- const v = JSON.parse(raw);
18609
- if (!v || typeof v !== "object")
18610
- return EMPTY_STATE;
18611
- const o = v;
18612
- const steps = {};
18613
- if (o.steps && typeof o.steps === "object")
18614
- for (const [key, fb] of Object.entries(o.steps)) {
18615
- if (!fb || typeof fb !== "object")
18616
- continue;
18617
- const f = fb;
18618
- const clean = {};
18619
- for (const k of ["verdict", "pick", "note"])
18620
- if (typeof f[k] === "string" && f[k].trim())
18621
- clean[k] = f[k];
18622
- if (Object.keys(clean).length)
18623
- steps[key] = clean;
18624
- }
18625
- const notes = Array.isArray(o.notes) ? o.notes.filter(isNote) : [];
18626
- return {
18627
- steps,
18628
- ...notes.length ? { notes } : {},
18629
- ...typeof o.note === "string" && o.note.trim() ? { note: o.note } : {}
18771
+ var startOfMonth = (day) => {
18772
+ const { y, m } = parts(day);
18773
+ return dayOf(y, m, 1);
18774
+ };
18775
+ var TYPE_TONE = { deletion: "bad" };
18776
+ function normalizeHistory(data, now = todayDay()) {
18777
+ const today = data.axis?.today && parseDay(data.axis.today, parts(now).y)?.from || now;
18778
+ const converted = data.groups?.length ? fromGroups(data.groups, today) : { events: [], bands: [] };
18779
+ const rawEvents = [...data.events ?? [], ...converted.events];
18780
+ const rawBands = [...data.bands ?? [], ...converted.bands];
18781
+ const year = parts(today).y;
18782
+ const day = (s) => s ? parseDay(s, year) ?? null : null;
18783
+ const states = new Map;
18784
+ const stateOf = (key) => {
18785
+ const known = states.get(key);
18786
+ if (known)
18787
+ return known;
18788
+ const def = data.states?.[key];
18789
+ const s = { key, label: def?.label ?? key, tone: def?.tone ?? guessTone(key) };
18790
+ states.set(key, s);
18791
+ return s;
18792
+ };
18793
+ const items = new Map;
18794
+ const laneOrder = [];
18795
+ const lanes = new Map;
18796
+ const lane = (id, label = id === "code" ? "Code" : id) => {
18797
+ let l = lanes.get(id);
18798
+ if (!l) {
18799
+ l = { id, label, spans: [], events: [] };
18800
+ lanes.set(id, l);
18801
+ laneOrder.push(id);
18802
+ }
18803
+ return l;
18804
+ };
18805
+ if (rawEvents.some((e) => !e.lane || e.lane === "code") && !data.lanes?.some((l) => l.id === "code"))
18806
+ lane("code");
18807
+ let inferred = false;
18808
+ for (const l of data.lanes ?? []) {
18809
+ const into = lane(l.id, l.label);
18810
+ (l.spans ?? []).forEach((s, i) => {
18811
+ const a = day(s.from);
18812
+ if (!a)
18813
+ return;
18814
+ const open = !s.to;
18815
+ const b = open ? null : day(s.to);
18816
+ const to = open ? today : b?.to ?? a.to;
18817
+ const st = stateOf(s.state);
18818
+ const days = Math.max(0, to - a.from);
18819
+ const own = s.label ?? data.states?.[s.state]?.tag ?? "{d}d";
18820
+ const tags = own === "" ? [] : [...new Set([own, "{d}d"])].map((t) => t.replace(/\{d\}/g, String(days)));
18821
+ const id = s.id ?? `${l.id}/${i}`;
18822
+ const item = {
18823
+ id,
18824
+ kind: "span",
18825
+ lane: l.id,
18826
+ from: a.from,
18827
+ to,
18828
+ open,
18829
+ title: s.title ?? `${l.label} · ${st.label}`,
18830
+ ...s.detail ? { detail: s.detail } : {},
18831
+ tone: st.tone,
18832
+ inferred: s.provenance === "inferred",
18833
+ ...s.provenance ? { provenance: s.provenance } : {},
18834
+ ...s.source ? { source: s.source } : {},
18835
+ evidence: s.evidence ?? [],
18836
+ ...s.step ? { step: s.step } : {},
18837
+ state: s.state,
18838
+ tags
18839
+ };
18840
+ if (item.inferred)
18841
+ inferred = true;
18842
+ into.spans.push(item);
18843
+ items.set(id, item);
18844
+ });
18845
+ }
18846
+ const types = [];
18847
+ const events = [];
18848
+ rawEvents.forEach((e, index) => {
18849
+ const a = day(e.date);
18850
+ if (!a)
18851
+ return;
18852
+ const to = Math.max(a.to, day(e.to)?.to ?? a.to);
18853
+ const into = lane(e.lane ?? "code");
18854
+ const id = e.id ?? `e${index}`;
18855
+ const item = {
18856
+ id,
18857
+ kind: "event",
18858
+ lane: into.id,
18859
+ from: a.from,
18860
+ to,
18861
+ open: false,
18862
+ title: e.title,
18863
+ ...e.detail ? { detail: e.detail } : {},
18864
+ tone: e.tone ?? TYPE_TONE[e.type ?? ""] ?? "dim",
18865
+ ...e.type ? { type: e.type } : {},
18866
+ inferred: e.provenance === "inferred",
18867
+ ...e.provenance ? { provenance: e.provenance } : {},
18868
+ ...e.source ? { source: e.source } : {},
18869
+ evidence: e.evidence ?? [],
18870
+ ...e.step ? { step: e.step } : {},
18871
+ tags: []
18630
18872
  };
18631
- } catch {
18632
- return EMPTY_STATE;
18873
+ if (item.inferred)
18874
+ inferred = true;
18875
+ if (e.type && !types.includes(e.type))
18876
+ types.push(e.type);
18877
+ into.events.push(item);
18878
+ items.set(id, item);
18879
+ events.push({ item, index });
18880
+ });
18881
+ const bands = [];
18882
+ for (const b of rawBands) {
18883
+ const a = day(b.from);
18884
+ if (!a)
18885
+ continue;
18886
+ const z = b.to ? day(b.to) : null;
18887
+ bands.push({ from: a.from, to: z ? z.to : today, label: b.label });
18633
18888
  }
18889
+ bands.sort((x, y) => x.from - y.from);
18890
+ const all = [...items.values()].flatMap((i) => [i.from, i.to]);
18891
+ for (const b of bands)
18892
+ all.push(b.from, b.to);
18893
+ const hasOpen = [...items.values()].some((i) => i.open);
18894
+ const lo = all.length ? Math.min(...all) : today - 90;
18895
+ let hi = all.length ? Math.max(...all) : today;
18896
+ if (hasOpen || data.axis?.today)
18897
+ hi = Math.max(hi, today);
18898
+ const from = day(data.axis?.from)?.from ?? startOfMonth(lo);
18899
+ const to = day(data.axis?.to)?.to ?? hi + Math.max(6, Math.round((hi - from) * 0.05));
18900
+ const laneIndex = (id) => laneOrder.indexOf(id);
18901
+ const order = eventOrder(events.map((e) => ({ ...e, laneIndex: laneIndex(e.item.lane) })));
18902
+ for (const l of lanes.values())
18903
+ l.events.sort((a, b) => a.from - b.from);
18904
+ return {
18905
+ from,
18906
+ to: Math.max(to, from + 1),
18907
+ today,
18908
+ lanes: laneOrder.map((id) => lanes.get(id)),
18909
+ events: order,
18910
+ bands,
18911
+ states: [...states.values()],
18912
+ types,
18913
+ inferred,
18914
+ verified: [...items.values()].some((i) => i.provenance === "verified"),
18915
+ items
18916
+ };
18634
18917
  }
18635
- function isNote(v) {
18636
- if (!v || typeof v !== "object")
18637
- return false;
18638
- const n = v;
18639
- const ctx = n.context;
18640
- return typeof n.id === "string" && typeof n.at === "number" && (n.step === null || typeof n.step === "string") && (n.text === undefined || typeof n.text === "string") && !!ctx && Array.isArray(ctx.log) && hasContent(n);
18918
+ var DEFAULT_MARKS = ["release", "deletion"];
18919
+ function quietEvents(events, marks) {
18920
+ if (marks === "all")
18921
+ return new Set;
18922
+ const quiet = events.filter((e) => e.type && !marks.includes(e.type));
18923
+ return quiet.length === events.length ? new Set : new Set(quiet.map((e) => e.id));
18641
18924
  }
18642
- var noteOut = (n) => ({
18643
- at: new Date(n.at).toISOString(),
18644
- ...n.text?.trim() ? { text: n.text.trim() } : {},
18645
- ...n.voice ? { voice: n.voice } : {},
18646
- ...n.ink?.paths.length ? { ink: n.ink } : {},
18647
- ...n.context.media ? { media: n.context.media } : {},
18648
- log: n.context.log.map(({ at, ...e }) => ({ ...e, ago: Math.round((n.at - at) / 100) / 10 }))
18649
- });
18650
- function collectFeedback(w, state) {
18651
- const progress = walkthroughProgress(w, state);
18652
- const sectionTitle = new Map(w.sections.map((s) => [s.id, s.link ? `${s.link.label} ${s.title}` : s.title]));
18653
- const keys = new Set(w.steps.map((s) => s.key));
18654
- const steps = [];
18655
- for (const s of w.steps) {
18656
- const fb = state.steps[s.key];
18657
- const notes = notesOn(state, s.key).map(noteOut);
18658
- if (s.ask === "none" && !notes.length)
18659
- continue;
18660
- const option = s.ask === "pick" ? s.options.find((o) => o.id === fb?.pick) : undefined;
18661
- const suggested = s.options.find((o) => o.suggested);
18662
- steps.push({
18663
- key: s.key,
18664
- number: s.number,
18665
- section: sectionTitle.get(s.section) ?? s.section,
18666
- title: s.title,
18667
- ask: s.ask,
18668
- ...option ? { pick: option.id, pickLabel: `${option.letter}: ${option.label}` } : {},
18669
- ...option && suggested && suggested.id !== option.id ? { suggested: `${suggested.letter}: ${suggested.label}` } : {},
18670
- notes,
18671
- ...fb?.verdict ? { verdict: fb.verdict } : {},
18672
- ...fb?.note ? { note: fb.note } : {}
18673
- });
18925
+ var PLURALS = {
18926
+ commit: "commits",
18927
+ release: "releases",
18928
+ deletion: "deletions",
18929
+ channel: "channel switches",
18930
+ audit: "findings"
18931
+ };
18932
+ function quietLabel(events, quiet) {
18933
+ const types = [];
18934
+ for (const e of events)
18935
+ if (quiet.has(e.id) && e.type && !types.includes(e.type))
18936
+ types.push(e.type);
18937
+ const names = types.map((t) => PLURALS[t] ?? `${t}s`);
18938
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}` : names[0] ?? "";
18939
+ }
18940
+ function eventOrder(events) {
18941
+ return [...events].sort((a, b) => a.item.from - b.item.from || a.laneIndex - b.laneIndex || a.index - b.index).map((e) => e.item);
18942
+ }
18943
+ function stepEvent(order, current, dir, at) {
18944
+ if (!order.length)
18945
+ return null;
18946
+ const i = current === null ? -1 : order.findIndex((e) => e.id === current);
18947
+ if (i < 0 && at !== undefined) {
18948
+ const next = dir > 0 ? order.find((e) => e.from >= at) : [...order].reverse().find((e) => e.from <= at);
18949
+ return next ?? (dir > 0 ? order[order.length - 1] : order[0]);
18674
18950
  }
18951
+ if (i < 0)
18952
+ return dir > 0 ? order[0] : order[order.length - 1];
18953
+ return order[Math.max(0, Math.min(order.length - 1, i + dir))];
18954
+ }
18955
+ function dayScale(from, to, width) {
18956
+ const span = Math.max(1, to - from);
18675
18957
  return {
18676
- id: w.id,
18677
- title: w.title,
18678
- reviewed: progress.done,
18679
- total: progress.total,
18680
- ...state.note ? { note: state.note } : {},
18681
- steps,
18682
- general: (state.notes ?? []).filter((n) => !n.step || !keys.has(n.step)).map(noteOut)
18958
+ x: (day) => (day - from) / span * width,
18959
+ day: (x) => from + x / Math.max(1, width) * span,
18960
+ f: (day) => (day - from) / span
18683
18961
  };
18684
18962
  }
18685
- var quote = (text) => text.trim().replace(/\s*\n\s*/g, " ");
18686
- function noteLine(n) {
18687
- const bits = [];
18688
- if (n.text)
18689
- bits.push(`“${quote(n.text)}”`);
18690
- if (n.voice)
18691
- bits.push(`voice note ${formatTime(n.voice.duration)} (${n.voice.url})`);
18692
- if (n.ink)
18693
- bits.push(`ink, ${n.ink.paths.length} stroke${n.ink.paths.length === 1 ? "" : "s"}`);
18694
- const seen = describeMedia(n.media);
18695
- return `${bits.join(" + ")}${seen ? ` · ${seen}` : ""}`;
18696
- }
18697
- function feedbackMarkdown(fb, verdicts) {
18698
- const name = (id) => verdicts?.find((v) => v.id === id)?.label ?? label(id);
18699
- const lines = [
18700
- `## Feedback: ${fb.title}`,
18701
- "",
18702
- `${fb.reviewed} of ${fb.total} reviewed · \`${fb.id}\``
18703
- ];
18704
- if (fb.note)
18705
- lines.push("", quote(fb.note));
18706
- if (fb.general.length) {
18707
- lines.push("", "### General");
18708
- for (const n of fb.general)
18709
- lines.push(`- ${noteLine(n)}`);
18963
+ var STEPS = [1, 2, 3, 6, 12];
18964
+ function monthTicks(from, to, width, minGap = 44) {
18965
+ const perMonth = width / Math.max(1, to - from) * 30.44;
18966
+ const step = STEPS.find((s) => s * perMonth >= minGap) ?? 12;
18967
+ const ticks = [];
18968
+ const { y } = parts(from);
18969
+ let { m } = parts(from);
18970
+ let d = dayOf(y, m, 1);
18971
+ if (d < from) {
18972
+ m++;
18973
+ d = dayOf(y, m, 1);
18710
18974
  }
18711
- let section = "";
18712
- for (const s of fb.steps) {
18713
- if (s.section !== section) {
18714
- section = s.section;
18715
- lines.push("", `### ${section}`);
18716
- }
18717
- const head = [];
18718
- if (s.pickLabel)
18719
- head.push(`**Picked ${s.pickLabel}**${s.suggested ? ` (suggested ${s.suggested})` : ""}`);
18720
- if (s.verdict)
18721
- head.push(`**${name(s.verdict)}**`);
18722
- if (s.note)
18723
- head.push(`“${quote(s.note)}”`);
18724
- const open = !head.length && !s.notes.length;
18725
- lines.push(`- ${s.number} ${s.title}: ${open ? "_open_" : head.join(" · ") || `${s.notes.length} note${s.notes.length === 1 ? "" : "s"}`}`);
18726
- for (const n of s.notes)
18727
- lines.push(` - ${noteLine(n)}`);
18975
+ while (d <= to) {
18976
+ const p = parts(d);
18977
+ if (p.m % step === 0)
18978
+ ticks.push({ day: d, label: p.m === 0 ? String(p.y) : MONTHS[p.m] });
18979
+ m++;
18980
+ d = dayOf(y, m, 1);
18728
18981
  }
18729
- return lines.join(`
18730
- `) + `
18731
- `;
18732
- }
18733
- var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
18734
- function plainMarkdown(text) {
18735
- return text.replace(/\[([^\]\n]+)\]\([^)\s]+\)/g, "$1").replace(/`([^`\n]+)`/g, "$1").replace(/\*\*([^*\n]+)\*\*/g, "$1").replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, "$1$2").replace(/\s+/g, " ").trim();
18982
+ return ticks;
18736
18983
  }
18737
- function inlineMarkdown(text) {
18738
- const codes = [];
18739
- let s = text.replace(/`([^`\n]+)`/g, (_, c) => {
18740
- codes.push(c);
18741
- return `\x00${codes.length - 1}\x00`;
18984
+ function fitLabels(labels, width, reserved = [], gap = 8) {
18985
+ let end = -Infinity;
18986
+ return labels.map(({ x, w }) => {
18987
+ const x1 = x + w;
18988
+ if (x < end + gap || x1 > width)
18989
+ return false;
18990
+ if (reserved.some((r) => x < r.x1 + gap && x1 > r.x0 - gap))
18991
+ return false;
18992
+ end = x1;
18993
+ return true;
18742
18994
  });
18743
- s = escapeHtml(s);
18744
- s = s.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (m, t, href) => {
18745
- const raw = href.replace(/&amp;/g, "&");
18746
- if (!/^(https?:\/\/|\/|\.{0,2}\/|#)/i.test(raw) && /^[a-z][a-z0-9+.-]*:/i.test(raw))
18747
- return m;
18748
- return `<a href="${href}" target="_blank" rel="noreferrer">${t}</a>`;
18995
+ }
18996
+ function clipSpan(from, to, axisFrom, axisTo) {
18997
+ if (to < axisFrom || from > axisTo)
18998
+ return null;
18999
+ return {
19000
+ from: Math.max(from, axisFrom),
19001
+ to: Math.min(to, axisTo),
19002
+ clipStart: from < axisFrom,
19003
+ clipEnd: to > axisTo
19004
+ };
19005
+ }
19006
+ function fitTag(widths, spanWidth, roomAfter, pad = 6) {
19007
+ for (let i = 0;i < widths.length; i++)
19008
+ if (widths[i] + pad * 2 <= spanWidth)
19009
+ return { index: i, place: "inside" };
19010
+ for (let i = 0;i < widths.length; i++)
19011
+ if (widths[i] + pad <= roomAfter)
19012
+ return { index: i, place: "after" };
19013
+ return null;
19014
+ }
19015
+ function bandRoom(bands, x) {
19016
+ return bands.map((b, i) => {
19017
+ const next = bands.slice(i + 1).find((n) => n.from > b.from);
19018
+ const end = Math.min(b.to, next ? next.from : Infinity);
19019
+ return Math.max(0, x(end) - x(b.from));
18749
19020
  });
18750
- s = s.replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>");
18751
- s = s.replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, "$1<i>$2</i>");
18752
- s = s.replace(/\n{2,}/g, "<br><br>").replace(/\n/g, " ");
18753
- return s.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${escapeHtml(codes[Number(i)])}</code>`);
19021
+ }
19022
+ function nearestEvent(events, day, tolerance) {
19023
+ let best = null;
19024
+ let gap = tolerance;
19025
+ for (const e of events) {
19026
+ const d = day < e.from ? e.from - day : day > e.to ? day - e.to : 0;
19027
+ if (d <= gap) {
19028
+ best = e;
19029
+ gap = d;
19030
+ }
19031
+ }
19032
+ return best;
19033
+ }
19034
+ function firstSentence(text) {
19035
+ if (!text)
19036
+ return "";
19037
+ const plain = text.replace(/`([^`]*)`/g, "$1").replace(/\*\*?([^*]+)\*\*?/g, "$1").trim();
19038
+ const m = /^.*?[.!?](?=\s|$)/.exec(plain);
19039
+ return (m ? m[0] : plain).trim();
18754
19040
  }
18755
19041
 
18756
19042
  // src/ui/history.ts
19043
+ init_look();
18757
19044
  var HEIGHT2 = springMotion(SPRINGS.default);
18758
19045
  var GO = svg('<path d="M5 5h6v6M11 5l-6 6"/>');
18759
19046
  var TYPE_NAMES = {
@@ -18765,14 +19052,14 @@ var TYPE_NAMES = {
18765
19052
  };
18766
19053
  var typeName = (t) => TYPE_NAMES[t] ?? t.charAt(0).toUpperCase() + t.slice(1);
18767
19054
  var REACH = 8;
18768
- var el = (doc, tag, cls) => {
19055
+ var el2 = (doc, tag, cls) => {
18769
19056
  const n = doc.createElement(tag);
18770
19057
  if (cls)
18771
19058
  n.className = cls;
18772
19059
  return n;
18773
19060
  };
18774
19061
  var text = (doc, tag, cls, value) => {
18775
- const n = el(doc, tag, cls);
19062
+ const n = el2(doc, tag, cls);
18776
19063
  n.textContent = value;
18777
19064
  return n;
18778
19065
  };
@@ -18788,22 +19075,22 @@ var when = (it) => it.kind === "span" ? `${formatRange(it.from, it.to, it.open)}
18788
19075
  function history(container, options) {
18789
19076
  const doc = container.ownerDocument;
18790
19077
  const win = doc.defaultView;
18791
- const root = el(doc, "div", "knobs-history");
19078
+ const root = el2(doc, "div", "knobs-history");
18792
19079
  applyLook2(root, options);
18793
19080
  injectHistoryStyles(doc);
18794
19081
  root.tabIndex = 0;
18795
19082
  root.setAttribute("role", "group");
18796
19083
  root.setAttribute("aria-label", options.label ?? "History");
18797
19084
  root.setAttribute("aria-roledescription", "timeline");
18798
- const chart = el(doc, "div", "knobs-hy-chart");
18799
- const foot = el(doc, "div", "knobs-hy-foot");
18800
- const detail = el(doc, "div", "knobs-hy-detail");
19085
+ const chart = el2(doc, "div", "knobs-hy-chart");
19086
+ const foot = el2(doc, "div", "knobs-hy-foot");
19087
+ const detail = el2(doc, "div", "knobs-hy-detail");
18801
19088
  detail.setAttribute("aria-live", "polite");
18802
- const tip = el(doc, "div", "knobs-hy-tip");
19089
+ const tip = el2(doc, "div", "knobs-hy-tip");
18803
19090
  tip.setAttribute("aria-hidden", "true");
18804
- const tipTitle = el(doc, "div", "knobs-hy-tt");
18805
- const tipDate = el(doc, "div", "knobs-hy-td");
18806
- const tipLine = el(doc, "div", "knobs-hy-tl");
19091
+ const tipTitle = el2(doc, "div", "knobs-hy-tt");
19092
+ const tipDate = el2(doc, "div", "knobs-hy-td");
19093
+ const tipLine = el2(doc, "div", "knobs-hy-tl");
18807
19094
  tip.append(tipTitle, tipDate, tipLine);
18808
19095
  root.append(chart, foot, detail, tip);
18809
19096
  container.append(root);
@@ -18839,7 +19126,7 @@ function history(container, options) {
18839
19126
  foot.textContent = "";
18840
19127
  root.toggleAttribute("data-all", showAll);
18841
19128
  if (quiet.size) {
18842
- const t = el(doc, "button", "knobs-hy-more");
19129
+ const t = el2(doc, "button", "knobs-hy-more");
18843
19130
  t.type = "button";
18844
19131
  t.setAttribute("aria-pressed", String(showAll));
18845
19132
  t.textContent = `${showAll ? "Hide" : "Show"} ${quietLabel(model.events, quiet)}`;
@@ -18852,13 +19139,13 @@ function history(container, options) {
18852
19139
  foot.append(t);
18853
19140
  }
18854
19141
  if (model.inferred && model.verified) {
18855
- const legend = el(doc, "span", "knobs-hy-legend");
19142
+ const legend = el2(doc, "span", "knobs-hy-legend");
18856
19143
  for (const [label, inferred] of [
18857
19144
  ["Verified", false],
18858
19145
  ["Inferred", true]
18859
19146
  ]) {
18860
- const k = el(doc, "span", "knobs-hy-key");
18861
- const sw = el(doc, "span", "knobs-hy-swatch");
19147
+ const k = el2(doc, "span", "knobs-hy-key");
19148
+ const sw = el2(doc, "span", "knobs-hy-swatch");
18862
19149
  sw.dataset.tone = "dim";
18863
19150
  if (inferred)
18864
19151
  sw.dataset.inferred = "";
@@ -18881,7 +19168,7 @@ function history(container, options) {
18881
19168
  tracks = new Map;
18882
19169
  let gutter = 0;
18883
19170
  if (!narrow) {
18884
- const probe = el(doc, "div", "knobs-hy-label");
19171
+ const probe = el2(doc, "div", "knobs-hy-label");
18885
19172
  chart.append(probe);
18886
19173
  const font = fontOf(probe);
18887
19174
  const gap = parseFloat(win.getComputedStyle(probe).marginRight) || 0;
@@ -18897,25 +19184,25 @@ function history(container, options) {
18897
19184
  for (let i = 0;i < model.lanes.length; i++)
18898
19185
  rows.push(...narrow ? ["auto", "var(--hy-lane)"] : ["var(--hy-lane)"]);
18899
19186
  chart.style.gridTemplateRows = rows.join(" ");
18900
- const behind = el(doc, "div", "knobs-hy-layer");
19187
+ const behind = el2(doc, "div", "knobs-hy-layer");
18901
19188
  behind.setAttribute("aria-hidden", "true");
18902
19189
  for (const b of model.bands) {
18903
19190
  const c = clipSpan(b.from, b.to, model.from, model.to);
18904
19191
  if (!c)
18905
19192
  continue;
18906
- const band = el(doc, "div", "knobs-hy-band");
19193
+ const band = el2(doc, "div", "knobs-hy-band");
18907
19194
  band.style.left = pct(c.from);
18908
19195
  band.style.width = pct(model.from + (c.to - c.from));
18909
19196
  behind.append(band);
18910
19197
  }
18911
19198
  const ticks = monthTicks(model.from, model.to, plotW);
18912
19199
  for (const t of ticks) {
18913
- const line = el(doc, "div", "knobs-hy-grid");
19200
+ const line = el2(doc, "div", "knobs-hy-grid");
18914
19201
  line.style.left = pct(t.day);
18915
19202
  behind.append(line);
18916
19203
  }
18917
19204
  chart.append(behind);
18918
- const axis = el(doc, "div", "knobs-hy-axis");
19205
+ const axis = el2(doc, "div", "knobs-hy-axis");
18919
19206
  axis.setAttribute("aria-hidden", "true");
18920
19207
  const tickEls = ticks.map((t) => {
18921
19208
  const n = text(doc, "span", "knobs-hy-tick", t.label);
@@ -18941,7 +19228,7 @@ function history(container, options) {
18941
19228
  const label = text(doc, "div", "knobs-hy-label", lane.label);
18942
19229
  label.style.gridRow = String(row);
18943
19230
  label.title = lane.label;
18944
- const track = el(doc, "div", "knobs-hy-track");
19231
+ const track = el2(doc, "div", "knobs-hy-track");
18945
19232
  track.style.gridRow = String(narrow ? row + 1 : row);
18946
19233
  track.dataset.lane = lane.id;
18947
19234
  tracks.set(lane.id, track);
@@ -18949,7 +19236,7 @@ function history(container, options) {
18949
19236
  const c = clipSpan(s.from, s.to, model.from, model.to);
18950
19237
  if (!c)
18951
19238
  continue;
18952
- const b = el(doc, "button", "knobs-hy-span");
19239
+ const b = el2(doc, "button", "knobs-hy-span");
18953
19240
  b.type = "button";
18954
19241
  b.tabIndex = -1;
18955
19242
  b.dataset.id = s.id;
@@ -18971,7 +19258,7 @@ function history(container, options) {
18971
19258
  for (const e of lane.events) {
18972
19259
  if (e.to < model.from || e.from > model.to)
18973
19260
  continue;
18974
- const b = el(doc, "button", "knobs-hy-ev");
19261
+ const b = el2(doc, "button", "knobs-hy-ev");
18975
19262
  b.type = "button";
18976
19263
  b.tabIndex = -1;
18977
19264
  b.dataset.id = e.id;
@@ -18993,15 +19280,15 @@ function history(container, options) {
18993
19280
  }
18994
19281
  chart.append(label, track);
18995
19282
  });
18996
- over = el(doc, "div", "knobs-hy-layer");
19283
+ over = el2(doc, "div", "knobs-hy-layer");
18997
19284
  over.setAttribute("aria-hidden", "true");
18998
19285
  if (showToday) {
18999
- const today = el(doc, "div", "knobs-hy-today");
19286
+ const today = el2(doc, "div", "knobs-hy-today");
19000
19287
  today.style.left = pct(model.today);
19001
19288
  over.append(today);
19002
19289
  }
19003
- selLine = el(doc, "div", "knobs-hy-sel");
19004
- hoverLine = el(doc, "div", "knobs-hy-hover");
19290
+ selLine = el2(doc, "div", "knobs-hy-sel");
19291
+ hoverLine = el2(doc, "div", "knobs-hy-hover");
19005
19292
  over.append(selLine, hoverLine);
19006
19293
  chart.append(over);
19007
19294
  const reserved = [];
@@ -19047,8 +19334,8 @@ function history(container, options) {
19047
19334
  return moving;
19048
19335
  });
19049
19336
  function buildDetail(it) {
19050
- const c = el(doc, "div", "knobs-hy-dc");
19051
- const meta = el(doc, "div", "knobs-hy-dmeta");
19337
+ const c = el2(doc, "div", "knobs-hy-dc");
19338
+ const meta = el2(doc, "div", "knobs-hy-dmeta");
19052
19339
  meta.append(text(doc, "time", "", when(it)));
19053
19340
  const lane = model.lanes.find((l) => l.id === it.lane);
19054
19341
  if (lane && (model.lanes.length > 1 || lane.id !== "code"))
@@ -19065,11 +19352,11 @@ function history(container, options) {
19065
19352
  }
19066
19353
  c.append(meta, text(doc, "div", "knobs-hy-dtitle", it.title));
19067
19354
  if (it.detail) {
19068
- const p = el(doc, "p", "knobs-hy-dtext");
19355
+ const p = el2(doc, "p", "knobs-hy-dtext");
19069
19356
  p.innerHTML = inlineMarkdown(it.detail);
19070
19357
  c.append(p);
19071
19358
  }
19072
- const foot = el(doc, "div", "knobs-hy-dfoot");
19359
+ const foot = el2(doc, "div", "knobs-hy-dfoot");
19073
19360
  for (const e of it.evidence) {
19074
19361
  const tag = text(doc, e.href ? "a" : "span", "knobs-hy-evidence", e.label);
19075
19362
  if (tag instanceof HTMLAnchorElement && e.href) {
@@ -19080,7 +19367,7 @@ function history(container, options) {
19080
19367
  foot.append(tag);
19081
19368
  }
19082
19369
  if (it.step && options.onJump) {
19083
- const go = el(doc, "button", "knobs-hy-jump");
19370
+ const go = el2(doc, "button", "knobs-hy-jump");
19084
19371
  go.type = "button";
19085
19372
  go.append(text(doc, "span", "", "Go to step"));
19086
19373
  go.insertAdjacentHTML("beforeend", GO);
@@ -19323,19 +19610,19 @@ init_selection_pill();
19323
19610
  var GLYPH = {
19324
19611
  caret: icon("caretRight", { small: true })
19325
19612
  };
19326
- var el2 = (doc, tag, cls) => {
19613
+ var el3 = (doc, tag, cls) => {
19327
19614
  const n = doc.createElement(tag);
19328
19615
  if (cls)
19329
19616
  n.className = cls;
19330
19617
  return n;
19331
19618
  };
19332
19619
  var text2 = (doc, tag, cls, value) => {
19333
- const n = el2(doc, tag, cls);
19620
+ const n = el3(doc, tag, cls);
19334
19621
  n.textContent = value;
19335
19622
  return n;
19336
19623
  };
19337
19624
  var prose = (doc, tag, cls, value) => {
19338
- const n = el2(doc, tag, cls);
19625
+ const n = el3(doc, tag, cls);
19339
19626
  n.innerHTML = inlineMarkdown(value);
19340
19627
  return n;
19341
19628
  };
@@ -19391,10 +19678,10 @@ function mountHistory(host, m, steps) {
19391
19678
  function mountStack(host, m, steps, findPR) {
19392
19679
  const doc = host.ownerDocument;
19393
19680
  const layout = stackLayout(m);
19394
- const root = el2(doc, "div", "knobs-wt-stack");
19681
+ const root = el3(doc, "div", "knobs-wt-stack");
19395
19682
  root.toggleAttribute("data-flat", layout.flat);
19396
19683
  const lane = (kind) => {
19397
- const s = el2(doc, "span", "knobs-wt-lane");
19684
+ const s = el3(doc, "span", "knobs-wt-lane");
19398
19685
  s.dataset.rail = kind;
19399
19686
  return s;
19400
19687
  };
@@ -19405,9 +19692,9 @@ function mountStack(host, m, steps, findPR) {
19405
19692
  };
19406
19693
  const from = layout.trunk ? 0 : 1;
19407
19694
  if (layout.trunk && layout.rows.length) {
19408
- const row = el2(doc, "div", "knobs-wt-srow");
19695
+ const row = el3(doc, "div", "knobs-wt-srow");
19409
19696
  row.dataset.trunk = "";
19410
- const lanes = el2(doc, "span", "knobs-wt-lanes");
19697
+ const lanes = el3(doc, "span", "knobs-wt-lanes");
19411
19698
  lanes.setAttribute("aria-hidden", "true");
19412
19699
  lanes.append(node(true));
19413
19700
  row.append(lanes, text2(doc, "span", "knobs-wt-trunk", layout.trunk));
@@ -19415,9 +19702,9 @@ function mountStack(host, m, steps, findPR) {
19415
19702
  }
19416
19703
  for (const r of layout.rows) {
19417
19704
  const it = r.item;
19418
- const row = el2(doc, "div", "knobs-wt-srow");
19705
+ const row = el3(doc, "div", "knobs-wt-srow");
19419
19706
  if (!layout.flat) {
19420
- const lanes = el2(doc, "span", "knobs-wt-lanes");
19707
+ const lanes = el3(doc, "span", "knobs-wt-lanes");
19421
19708
  lanes.setAttribute("aria-hidden", "true");
19422
19709
  for (let k = from;k < r.depth; k++)
19423
19710
  lanes.append(lane(r.rails[k] ? "pass" : "none"));
@@ -19426,8 +19713,8 @@ function mountStack(host, m, steps, findPR) {
19426
19713
  lanes.append(node(r.children > 0));
19427
19714
  row.append(lanes);
19428
19715
  }
19429
- const main = el2(doc, "div", "knobs-wt-smain");
19430
- const line = el2(doc, "div", "knobs-wt-sline");
19716
+ const main = el3(doc, "div", "knobs-wt-smain");
19717
+ const line = el3(doc, "div", "knobs-wt-sline");
19431
19718
  if (it.order)
19432
19719
  line.append(text2(doc, "span", "knobs-wt-sord", it.order));
19433
19720
  const known = findPR?.(it.pr.replace(/^#/, "")) ?? null;
@@ -19458,7 +19745,7 @@ function mountStack(host, m, steps, findPR) {
19458
19745
  main.append(line);
19459
19746
  if (it.summary)
19460
19747
  main.append(prose(doc, "p", "knobs-wt-ssummary", it.summary));
19461
- const meta = el2(doc, "div", "knobs-wt-smeta");
19748
+ const meta = el3(doc, "div", "knobs-wt-smeta");
19462
19749
  if (it.head) {
19463
19750
  const branch = r.offBase ? `${it.head} → ${r.offBase}` : it.head;
19464
19751
  meta.append(text2(doc, "span", "knobs-wt-sbranch", branch));
@@ -19483,7 +19770,7 @@ function mountStack(host, m, steps, findPR) {
19483
19770
  return { dispose: () => root.remove(), context: () => ({ kind: "stack" }) };
19484
19771
  }
19485
19772
  function delta(doc, d) {
19486
- const s = el2(doc, "span", "knobs-wt-delta");
19773
+ const s = el3(doc, "span", "knobs-wt-delta");
19487
19774
  if (typeof d === "string") {
19488
19775
  s.textContent = d;
19489
19776
  return s;
@@ -19523,13 +19810,13 @@ var cellAttrs = (td, c) => {
19523
19810
  function mountTable(host, m, steps) {
19524
19811
  const doc = host.ownerDocument;
19525
19812
  const win = doc.defaultView;
19526
- const box = el2(doc, "div", "knobs-wt-tablebox");
19527
- const scroller = el2(doc, "div", "knobs-wt-table");
19528
- const table = el2(doc, "table");
19813
+ const box = el3(doc, "div", "knobs-wt-tablebox");
19814
+ const scroller = el3(doc, "div", "knobs-wt-table");
19815
+ const table = el3(doc, "table");
19529
19816
  if (m.caption)
19530
19817
  table.append(text2(doc, "caption", "", m.caption));
19531
- const head = el2(doc, "thead");
19532
- const hr = el2(doc, "tr");
19818
+ const head = el3(doc, "thead");
19819
+ const hr = el3(doc, "tr");
19533
19820
  for (const c of m.columns) {
19534
19821
  const th = text2(doc, "th", "", c.label);
19535
19822
  th.scope = "col";
@@ -19537,11 +19824,11 @@ function mountTable(host, m, steps) {
19537
19824
  hr.append(th);
19538
19825
  }
19539
19826
  head.append(hr);
19540
- const body = el2(doc, "tbody");
19827
+ const body = el3(doc, "tbody");
19541
19828
  for (const r of m.rows) {
19542
- const tr = el2(doc, "tr");
19829
+ const tr = el3(doc, "tr");
19543
19830
  for (const c of m.columns) {
19544
- const td = el2(doc, "td");
19831
+ const td = el3(doc, "td");
19545
19832
  cellAttrs(td, c);
19546
19833
  const cell = r[c.key];
19547
19834
  if (cell) {
@@ -19556,24 +19843,24 @@ function mountTable(host, m, steps) {
19556
19843
  }
19557
19844
  table.append(head, body);
19558
19845
  scroller.append(table);
19559
- const stack = el2(doc, "div", "knobs-wt-tstack");
19846
+ const stack = el3(doc, "div", "knobs-wt-tstack");
19560
19847
  stack.setAttribute("role", "list");
19561
19848
  if (m.caption)
19562
19849
  stack.setAttribute("aria-label", m.caption);
19563
19850
  const primary = m.columns.find((c) => c.key === m.primary) ?? m.columns[0];
19564
19851
  for (const r of m.rows) {
19565
- const item = el2(doc, "div", "knobs-wt-trow");
19852
+ const item = el3(doc, "div", "knobs-wt-trow");
19566
19853
  item.setAttribute("role", "listitem");
19567
- const title = el2(doc, "div", "knobs-wt-ttitle");
19854
+ const title = el3(doc, "div", "knobs-wt-ttitle");
19568
19855
  if (primary) {
19569
19856
  cellAttrs(title, primary);
19570
19857
  const cell = r[primary.key];
19571
19858
  if (cell)
19572
19859
  cellContent(doc, title, cell);
19573
19860
  }
19574
- const line = el2(doc, "div", "knobs-wt-tline");
19861
+ const line = el3(doc, "div", "knobs-wt-tline");
19575
19862
  line.append(title);
19576
- const badges = el2(doc, "div", "knobs-wt-tbadges");
19863
+ const badges = el3(doc, "div", "knobs-wt-tbadges");
19577
19864
  for (const c of m.columns) {
19578
19865
  const cell = r[c.key];
19579
19866
  if (c !== primary && cell?.badge)
@@ -19582,14 +19869,14 @@ function mountTable(host, m, steps) {
19582
19869
  if (badges.childNodes.length)
19583
19870
  line.append(badges);
19584
19871
  item.append(line);
19585
- const terms = el2(doc, "dl", "knobs-wt-tterms");
19872
+ const terms = el3(doc, "dl", "knobs-wt-tterms");
19586
19873
  for (const c of m.columns) {
19587
19874
  if (c === primary)
19588
19875
  continue;
19589
19876
  const cell = r[c.key];
19590
19877
  if (!cell || cell.text === "" || cell.badge)
19591
19878
  continue;
19592
- const dd = el2(doc, "dd");
19879
+ const dd = el3(doc, "dd");
19593
19880
  cellAttrs(dd, c);
19594
19881
  dd.removeAttribute("data-align");
19595
19882
  cellContent(doc, dd, cell);
@@ -19655,12 +19942,12 @@ function mountTable(host, m, steps) {
19655
19942
  }
19656
19943
  function chapterPills(host, chapters, io) {
19657
19944
  const doc = host.ownerDocument;
19658
- const list = el2(doc, "div", "knobs-wt-chapters");
19945
+ const list = el3(doc, "div", "knobs-wt-chapters");
19659
19946
  list.setAttribute("role", "group");
19660
19947
  list.setAttribute("aria-label", "Chapters");
19661
19948
  let at = -2;
19662
19949
  const rows = chapters.map((c, i) => {
19663
- const b = el2(doc, "button", "knobs-wt-chapter");
19950
+ const b = el3(doc, "button", "knobs-wt-chapter");
19664
19951
  b.type = "button";
19665
19952
  b.append(text2(doc, "span", "knobs-wt-chapter-n", String(i + 1)));
19666
19953
  b.append(text2(doc, "span", "knobs-wt-chapter-label", c.label));
@@ -19676,7 +19963,8 @@ function chapterPills(host, chapters, io) {
19676
19963
  host.append(list);
19677
19964
  const pill = selectionPill(list, {
19678
19965
  selected: () => rows[at] ?? null,
19679
- rows: () => rows
19966
+ rows: () => rows,
19967
+ spring: SPRINGS.snappy
19680
19968
  });
19681
19969
  const select = (i) => {
19682
19970
  if (i === at)
@@ -19702,13 +19990,13 @@ function disclosure(host, options) {
19702
19990
  const doc = host.ownerDocument;
19703
19991
  const win = doc.defaultView;
19704
19992
  let open = options.open === true;
19705
- const root = el2(doc, "div", "knobs-wt-disc");
19706
- const toggle = el2(doc, "button", "knobs-wt-disc-toggle");
19993
+ const root = el3(doc, "div", "knobs-wt-disc");
19994
+ const toggle = el3(doc, "button", "knobs-wt-disc-toggle");
19707
19995
  toggle.type = "button";
19708
19996
  toggle.innerHTML = GLYPH.caret;
19709
19997
  toggle.append(text2(doc, "span", "", options.label));
19710
- const viewport = el2(doc, "div", "knobs-wt-disc-body");
19711
- const content = el2(doc, "div", "knobs-wt-disc-content");
19998
+ const viewport = el3(doc, "div", "knobs-wt-disc-body");
19999
+ const content = el3(doc, "div", "knobs-wt-disc-content");
19712
20000
  content.append(options.content);
19713
20001
  viewport.append(content);
19714
20002
  root.append(toggle, viewport);
@@ -19787,12 +20075,12 @@ var prGlyph = (state) => state === "merged" ? svg12(PR_PATHS.merge) : state ===
19787
20075
  var PR_STATE = { merged: "Merged", open: "Open", draft: "Draft", closed: "Closed" };
19788
20076
  var ISSUE = "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-88a16,16,0,1,1-16-16A16,16,0,0,1,144,128Z";
19789
20077
  function prStateLabel(doc, state, checks, title) {
19790
- const s = el2(doc, "span", "knobs-wt-prstate");
20078
+ const s = el3(doc, "span", "knobs-wt-prstate");
19791
20079
  s.dataset.state = state;
19792
20080
  s.innerHTML = prGlyph(state);
19793
20081
  s.append(text2(doc, "span", "", PR_STATE[state]));
19794
20082
  if (checks) {
19795
- const c = el2(doc, "span", "knobs-wt-prchecks");
20083
+ const c = el3(doc, "span", "knobs-wt-prchecks");
19796
20084
  c.innerHTML = svg12(CHECK3);
19797
20085
  c.title = "Checks pass";
19798
20086
  c.setAttribute("aria-label", "checks pass");
@@ -19803,7 +20091,7 @@ function prStateLabel(doc, state, checks, title) {
19803
20091
  return s;
19804
20092
  }
19805
20093
  function prChip(doc, n, pr) {
19806
- const chip = el2(doc, pr.href ? "a" : "span", "knobs-wt-pr");
20094
+ const chip = el3(doc, pr.href ? "a" : "span", "knobs-wt-pr");
19807
20095
  if (pr.href) {
19808
20096
  chip.setAttribute("href", pr.href);
19809
20097
  chip.setAttribute("target", "_blank");
@@ -19817,7 +20105,7 @@ function prChip(doc, n, pr) {
19817
20105
  chip.innerHTML = issue ? svg12(ISSUE) : prGlyph(pr.state);
19818
20106
  chip.append(text2(doc, "span", "", `#${n}`));
19819
20107
  if (pr.checks) {
19820
- const c = el2(doc, "span", "knobs-wt-prchecks");
20108
+ const c = el3(doc, "span", "knobs-wt-prchecks");
19821
20109
  c.innerHTML = svg12(CHECK3);
19822
20110
  chip.append(c);
19823
20111
  }
@@ -19900,21 +20188,25 @@ var calm = springCss(SPRINGS.calm);
19900
20188
  var state = springCss(SPRINGS.default);
19901
20189
  var quick = springCss(FADE);
19902
20190
  var fade2 = "-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 16px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 16px),transparent)";
19903
- var walkthroughCss = `
20191
+ var walkthroughCss = `${foundationCss}
19904
20192
  :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
19905
20193
  .knobs-ui:is(.knobs-walkthrough,.knobs-wt-slot){--k-glass:0;--k-glow:0;--kw-quick:${quick.easing};--kw-quick-ms:${quick.duration}ms}
19906
20194
  .knobs-ui.knobs-walkthrough{--kw-snappy:${snappy3.easing};--kw-snappy-ms:${snappy3.duration}ms;--kw-calm:${calm.easing};--kw-calm-ms:${calm.duration}ms;--kw-state:${state.easing};--kw-state-ms:${state.duration}ms;--kw-quick:${quick.easing};--kw-quick-ms:${quick.duration}ms;
19907
- --kw-measure:var(--k-walkthrough-measure,68ch);--kw-width:var(--k-walkthrough-width,960px);--kw-rail-w:220px;--kw-rail-gap:calc(var(--_k-space-group) + var(--_k-space-column) / 1.5);--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
20195
+ --kw-measure:var(--_kr-measure);--kw-width:var(--k-walkthrough-width,960px);--kw-rail-w:220px;--kw-rail-gap:calc(var(--_k-space-group) + var(--_k-space-column) / 1.5);--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
19908
20196
  --kw-card:var(--_ku-pane-bg);--kw-card-ring:var(--_ku-pane-ring);--kw-under:var(--_k-bg-page);--kw-ink:var(--k-walkthrough-ink,#ff375f);--kw-strong:var(--_k-weight-label);
19909
20197
  container:knobs-wt / inline-size;display:block;width:100%;color:var(--_k-fg);font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;text-align:left;
19910
- letter-spacing:normal;-webkit-font-smoothing:antialiased}
20198
+ letter-spacing:var(--_kr-track);-webkit-font-smoothing:antialiased}
19911
20199
  .knobs-walkthrough [hidden]{display:none!important}
20200
+ /* Prose: wrapped to look set by hand, quotes hung in the margin (Safari). Data: figures that line
20201
+ up and can't be misread. */
20202
+ :where(.knobs-wt-summary,.knobs-wt-ssum,.knobs-wt-caption,.knobs-wt-more-text,.knobs-wt-fi-text,.knobs-wt-fi-answer,.knobs-wt-ocap,.knobs-wt-text,.knobs-wt-callout,.knobs-wt-ssummary,.knobs-wt-osum){text-wrap:pretty;hanging-punctuation:first allow-end last}
20203
+ :where(.knobs-wt-table table,.knobs-wt-tterms,.knobs-wt-facts,.knobs-wt-smeta,.knobs-wt-metaline,.knobs-wt-meta,.knobs-wt-pr,.knobs-wt-prstate,.knobs-wt-sord){font-feature-settings:var(--_kr-data)}
19912
20204
  /* (0,0,1): a host's element rules stop here, and every class rule below wins over it. */
19913
- :where(.knobs-walkthrough) :is(h1,h2,h3,p,ol,ul,dl,dd){margin:0;font:inherit;letter-spacing:inherit}
20205
+ :where(.knobs-walkthrough) :is(h1,h2,h3,p,ol,ul,dl,dd){margin:0;font:inherit;letter-spacing:var(--_kr-track)}
19914
20206
  .knobs-walkthrough a{cursor:default;color:inherit;text-decoration:underline;text-decoration-color:color-mix(in srgb,currentColor 35%,transparent);text-underline-offset:2px;transition:text-decoration-color calc(var(--_k-hover-out) * 1ms)}
19915
20207
  .knobs-walkthrough a:hover{text-decoration-color:currentColor}
19916
20208
  .knobs-walkthrough code,.knobs-wt-slot code{font:var(--_k-type-label) var(--_k-mono);padding:1px 5px;border-radius:calc(var(--_k-radius) / 2);background:var(--_k-fill)}
19917
- :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-row,button.knobs-wt-stitle,.knobs-wt-chapter,.knobs-wt-disc-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
20209
+ :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-row,button.knobs-wt-stitle,.knobs-wt-chapter,.knobs-wt-disc-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:var(--_kr-track);
19918
20210
  background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
19919
20211
 
19920
20212
  /* ——— layout: the contents rail sticky at the left edge (right if asked), and the column centred
@@ -20072,7 +20364,7 @@ var walkthroughCss = `
20072
20364
  box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay)}
20073
20365
  .knobs-wt-on{padding-top:3px;color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);white-space:nowrap;overflow:hidden;${fade2}}
20074
20366
  .knobs-wt-row{display:flex;align-items:flex-end;gap:4px}
20075
- :where(.knobs-ui) :is(.knobs-wt-input,.knobs-wt-tool,.knobs-wt-x,.knobs-wt-back){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;
20367
+ :where(.knobs-ui) :is(.knobs-wt-input,.knobs-wt-tool,.knobs-wt-x,.knobs-wt-back){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:var(--_kr-track);background:transparent;border:0;
20076
20368
  border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
20077
20369
  .knobs-ui .knobs-wt-input{flex:1;min-width:0;height:32px;max-height:140px;padding:6px 0;resize:none;color:var(--_k-fg);caret-color:var(--_k-accent);line-height:var(--_k-leading-body);font-size:var(--_k-type-body)}
20078
20370
  .knobs-ui .knobs-wt-input::placeholder{color:var(--_k-fg-3)}
@@ -20124,27 +20416,53 @@ var walkthroughCss = `
20124
20416
  .knobs-wt-step[data-full] .knobs-wt-slot[data-kind="video"] .knobs-player{flex:1;min-height:0}
20125
20417
  .knobs-wt-step[data-full] .knobs-wt-slot[data-kind="video"] .knobs-player-frame{flex:1;min-height:0;aspect-ratio:auto}
20126
20418
 
20127
- /* ——— a taste test: the options side by side, Kit's pick checked and the rest stepping back. On
20128
- the step's card, so no fill of their own ——— */
20129
- .knobs-wt-options{display:grid;grid-template-columns:repeat(var(--kw-n,2),minmax(0,1fr));gap:var(--_k-space-group) var(--_k-space-column)}
20130
- @container knobs-wt-step (max-width:640px){.knobs-wt-options{grid-template-columns:minmax(0,1fr)}}
20131
- .knobs-wt-option{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0}
20132
- .knobs-wt-option>:not(.knobs-wt-pick){transition:opacity var(--kw-snappy-ms) var(--kw-snappy)}
20133
- .knobs-wt-options[data-has-pick] .knobs-wt-option:not([data-picked])>:not(.knobs-wt-pick){opacity:.55}
20134
- .knobs-ui .knobs-wt-pick{display:flex;align-items:center;gap:8px;width:100%;min-height:28px;margin:0 -6px;width:calc(100% + 12px);padding:0 6px;border-radius:var(--_k-radius);cursor:default;color:var(--_k-fg-2);text-align:left;
20135
- transition:background calc(var(--_k-hover-out) * 1ms)}
20136
- .knobs-ui .knobs-wt-pick:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_k-fill);transition-duration:calc(var(--_k-hover-in) * 1ms)}
20137
- .knobs-wt-letter{flex:none;font-weight:var(--kw-strong);color:var(--_k-fg)}
20138
- .knobs-wt-olabel{flex:1;min-width:0;font-weight:var(--kw-strong);color:var(--_k-fg)}
20139
- .knobs-wt-suggested{flex:none;font-size:var(--_k-type-small);color:var(--_k-fg-3)}
20140
- .knobs-wt-check{flex:none;display:flex;width:18px;height:18px;align-items:center;justify-content:center;border-radius:50%;background:var(--_k-fill);color:transparent;
20141
- transition:background var(--kw-snappy-ms) var(--kw-snappy),color var(--kw-snappy-ms) var(--kw-snappy)}
20419
+ /* ——— options: a decision, or a taste test when Kit picks. Columns that fill whole rows (4 → 4, 2
20420
+ or 1 across; 3 → 3 or 1), a hairline between neighbours and between rows, no card of their own.
20421
+ Each: a kicker ("Option 1 · Recommended", the mark a quiet word in text colour), its title, a
20422
+ sentence, its media and what changes as facts. A pick dims the rest ——— */
20423
+ .knobs-wt-options{--kw-ogap:var(--_k-space-group);display:grid;grid-template-columns:repeat(var(--kw-cols,1),minmax(0,1fr));gap:var(--kw-ogap);min-width:0}
20424
+ .knobs-wt-option{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0}
20425
+ .knobs-wt-option[data-col]:not([data-col="0"])::before{content:"";position:absolute;top:0;bottom:0;left:calc(var(--kw-ogap) / -2);width:1px;background:var(--_k-border-subtle);pointer-events:none}
20426
+ .knobs-wt-option[data-col]:not([data-row="0"])::before{top:calc(var(--kw-ogap) / -2)}
20427
+ .knobs-wt-option[data-col]:not([data-last-row])::before{bottom:calc(var(--kw-ogap) / -2)}
20428
+ .knobs-wt-option[data-col]:not([data-row="0"])::after{content:"";position:absolute;top:calc(var(--kw-ogap) / -2);left:calc(var(--kw-ogap) / -2);right:calc(var(--kw-ogap) / -2);height:1px;background:var(--_k-border-subtle);pointer-events:none}
20429
+ .knobs-wt-option[data-col="0"]::after{left:0}
20430
+ .knobs-wt-option[data-end]::after{right:0}
20431
+ .knobs-wt-option>:not(.knobs-wt-ohead){transition:opacity var(--kw-snappy-ms) var(--kw-snappy)}
20432
+ .knobs-wt-options[data-has-pick] .knobs-wt-option:not([data-picked])>:not(.knobs-wt-ohead){opacity:.55}
20433
+ .knobs-wt-ohead{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0;text-align:left}
20434
+ .knobs-wt-okicker{display:flex;align-items:center;gap:var(--_k-space-inside);min-width:0;height:var(--_k-leading-small);color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);white-space:nowrap}
20435
+ .knobs-wt-orec{color:var(--_k-fg);font-weight:var(--kw-strong)}
20436
+ .knobs-wt-orec::before{content:"·";margin-right:var(--_k-space-inside);color:var(--_k-fg-3);font-weight:var(--_k-weight)}
20437
+ .knobs-wt-otitle{color:var(--_k-fg);font-weight:var(--kw-strong);text-wrap:balance}
20438
+ .knobs-wt-osum{max-width:var(--kw-measure);color:var(--_k-fg-2)}
20439
+ /* Picking: the head is the radio, bled out so its words keep the column's edge; a fill under the
20440
+ pointer at once; a check at the kicker's end that fills when picked. */
20441
+ .knobs-ui .knobs-wt-pick{margin:-6px -8px;padding:6px 8px;border-radius:var(--_k-radius);cursor:default;transition:background calc(var(--_k-hover-out) * 1ms)}
20442
+ .knobs-ui .knobs-wt-pick:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
20443
+ .knobs-ui .knobs-wt-pick:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
20444
+ .knobs-wt-check{flex:none;display:flex;width:16px;height:16px;margin-left:auto;align-items:center;justify-content:center;border-radius:50%;box-shadow:inset 0 0 0 1px var(--_k-border-strong);color:transparent;
20445
+ transition:background var(--kw-snappy-ms) var(--kw-snappy),color var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
20142
20446
  .knobs-wt-check svg{width:12px;height:12px}
20143
- .knobs-wt-option[data-picked] .knobs-wt-check{background:var(--_k-accent);color:var(--_k-bg)}
20144
- .knobs-wt-key{display:inline-flex;align-items:center;justify-content:center;min-width:15px;height:15px;padding:0 3px;border-radius:4px;background:var(--_k-fill);color:var(--_k-fg-3);
20145
- font:var(--kw-strong) var(--_k-type-small)/1 var(--_k-font);opacity:.5;transition:opacity .15s}
20146
- .knobs-wt-step[data-current] .knobs-wt-key{opacity:1}
20147
- .knobs-wt-ocap{color:var(--_k-fg-2);max-width:var(--kw-measure)}
20447
+ .knobs-wt-option[data-picked] .knobs-wt-check{background:var(--_k-accent);box-shadow:none;color:var(--_k-on-accent)}
20448
+
20449
+ /* ——— facts: terms in a fixed gutter beside their values, sharing a baseline; groups under a
20450
+ heading, a hairline between them. Code and evidence are small mono chips. Too narrow for the
20451
+ gutter, each term sits over its value ——— */
20452
+ .knobs-wt-facts{--kw-term:144px;display:flex;flex-direction:column;gap:var(--_kr-block);min-width:0}
20453
+ .knobs-wt-fgroup{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0}
20454
+ .knobs-wt-fgroup+.knobs-wt-fgroup::before{content:"";position:absolute;left:0;right:0;top:calc(var(--_kr-block) / -2);height:1px;background:var(--_k-border-subtle);pointer-events:none}
20455
+ .knobs-wt-fhead{margin:0;color:var(--_k-fg);font-weight:var(--kw-strong);text-wrap:balance}
20456
+ .knobs-wt-fterms{display:grid;grid-template-columns:var(--kw-term) minmax(0,1fr);gap:var(--_k-space-sibling) var(--_kr-block);align-items:baseline;margin:0;min-width:0}
20457
+ .knobs-wt-fact{display:contents}
20458
+ .knobs-wt-fterms dt{margin:0;color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);text-wrap:balance}
20459
+ .knobs-wt-fterms dd{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0;margin:0;color:var(--_k-fg-2);overflow-wrap:anywhere;text-wrap:pretty}
20460
+ .knobs-wt-fterms dd[data-code]{flex-flow:row wrap}
20461
+ .knobs-wt-fterms dd[data-code]>code{padding:2px 6px;line-height:var(--_k-leading-label);color:var(--_k-fg)}
20462
+ @container knobs-wt-slot (max-width:400px){
20463
+ .knobs-wt-fterms{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
20464
+ .knobs-wt-fact{display:flex;flex-direction:column;gap:var(--_k-space-inside)}
20465
+ }
20148
20466
 
20149
20467
  /* ——— the rail: progress, contents, export ——— */
20150
20468
  .knobs-wt-rail{position:sticky;top:var(--_k-pad-page);display:flex;flex-direction:column;gap:var(--_k-space-title);max-height:calc(100vh - var(--_k-pad-page) * 2);min-height:0;font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
@@ -20198,11 +20516,11 @@ var walkthroughCss = `
20198
20516
  set apart by space, hairlines and their own colours. On a card (.knobs-surface) code drops its
20199
20517
  well for hairlines, so nothing draws a fill and a ring of its own there ——— */
20200
20518
  .knobs-ui.knobs-wt-slot{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0;container:knobs-wt-slot / inline-size;color:var(--_k-fg);
20201
- font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;--kw-measure:var(--k-walkthrough-measure,68ch);
20519
+ font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;--kw-measure:var(--_kr-measure);
20202
20520
  --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));--kw-frame-r:var(--_k-radius-panel)}
20203
20521
  .knobs-surface .knobs-wt-slot{--kw-frame-r:calc(var(--_k-radius-panel) - 4px)}
20204
20522
  /* A slot's label: sentence case as written, small and faint, never a heading. */
20205
- .knobs-wt-slot-label{display:block;min-width:0;overflow:hidden;white-space:nowrap;font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);letter-spacing:normal;text-transform:none;color:var(--_k-fg-3);${fade2}}
20523
+ .knobs-wt-slot-label{display:block;min-width:0;overflow:hidden;white-space:nowrap;font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);text-transform:none;color:var(--_k-fg-3);${fade2}}
20206
20524
  .knobs-wt-body{min-width:0}
20207
20525
  /* Media mounting after the first paint, and a picture still decoding, come into focus: a fade with
20208
20526
  a small blur on the quick spring, from the top; at rest, nothing. */
@@ -20221,14 +20539,15 @@ var walkthroughCss = `
20221
20539
  .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
20222
20540
  .knobs-wt-frame[data-natural]{aspect-ratio:auto;background:transparent}
20223
20541
  .knobs-wt-frame[data-natural]>img{position:static;height:auto}
20224
- /* Diff, code and terminal: one plain container (no glass): a flat inset fill, a hairline ring, the
20225
- control radius and room inside, so a diff's buttons and the code under them can breathe. */
20226
- .knobs-wt-slot[data-kind="diff"]>.knobs-wt-body{padding:var(--_k-pad-card);border-radius:var(--_k-radius-panel);background:var(--_k-bg-inset);box-shadow:inset 0 0 0 1px var(--_k-border-subtle)}
20542
+ /* Diff, code and terminal: one plain well, drawn by its hairline alone (a fill and a ring would be
20543
+ two edges for one box), the panel radius and room inside, so a diff's buttons and the code under
20544
+ them can breathe. */
20545
+ .knobs-wt-slot[data-kind="diff"]>.knobs-wt-body{padding:var(--_k-pad-card);border-radius:var(--_k-radius-panel);box-shadow:inset 0 0 0 1px var(--_k-border-subtle)}
20227
20546
  /* The diff inside it: its header row 28 high on the container's padding, a hairline under it, and
20228
20547
  no line of its own under the code (the container's ring closes it). */
20229
20548
  .knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"]){--kd-head:28px;--kd-x:0px}
20230
20549
  .knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"])::after{display:none}
20231
- .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--_k-radius-panel);background:var(--_k-bg-inset);box-shadow:inset 0 0 0 1px var(--_k-border-subtle);color:var(--_k-fg);
20550
+ .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--_k-radius-panel);box-shadow:inset 0 0 0 1px var(--_k-border-subtle);color:var(--_k-fg);
20232
20551
  font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
20233
20552
  .knobs-surface :is(.knobs-wt-code,.knobs-wt-slot[data-kind="diff"]>.knobs-wt-body){padding-inline:0;border-radius:0;background:transparent;box-shadow:inset 0 1px 0 var(--_k-border-subtle),inset 0 -1px 0 var(--_k-border-subtle)}
20234
20553
  .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
@@ -20237,17 +20556,17 @@ var walkthroughCss = `
20237
20556
  .knobs-wt-out{color:var(--_k-fg-2)}
20238
20557
  .knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
20239
20558
  .knobs-wt-mlabel{color:var(--_k-fg-2)}
20240
- .knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
20559
+ .knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-feature-settings:var(--_kr-data)}
20241
20560
  .knobs-wt-mfrom{color:var(--_k-fg-3)}
20242
20561
  .knobs-wt-mto{font-size:var(--_k-type-title);line-height:var(--_k-leading-title);font-weight:var(--_k-weight-label)}
20243
20562
  .knobs-wt-mof{color:var(--_k-fg-3)}
20244
20563
  .knobs-wt-mdelta{--kw-tone:var(--_k-fg-2);font-size:var(--_k-type-small);font-weight:var(--_k-weight-label);color:var(--kw-tone)}
20245
20564
  .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
20246
20565
  .knobs-wt-mdelta[data-tone="bad"]{--kw-tone:var(--_k-danger)}
20247
- .knobs-wt-mtrack{position:relative;height:4px;border-radius:2px;background:color-mix(in srgb,var(--_k-fg) 8%,transparent)}
20566
+ .knobs-wt-mtrack{position:relative;height:4px;border-radius:2px;background:var(--_k-fill-selected)}
20248
20567
  .knobs-wt-mtrack>span{position:absolute;top:0;bottom:0;left:0;border-radius:inherit}
20249
20568
  .knobs-wt-mtrack>.knobs-wt-mafter{background:var(--_k-accent)}
20250
- .knobs-wt-mtrack>.knobs-wt-mbefore{background:color-mix(in srgb,var(--_k-fg) 30%,transparent)}
20569
+ .knobs-wt-mtrack>.knobs-wt-mbefore{background:var(--_k-fg-4)}
20251
20570
  .knobs-wt-callout{--kw-tone:var(--kw-info);display:flex;gap:var(--_k-space-sibling);max-width:var(--kw-measure);color:var(--_k-fg)}
20252
20571
  .knobs-wt-callout::before{content:"";flex:none;width:6px;height:6px;margin-top:7px;border-radius:50%;background:var(--kw-tone)}
20253
20572
  .knobs-wt-callout[data-tone="good"]{--kw-tone:var(--kw-good)}
@@ -20313,7 +20632,7 @@ var walkthroughCss = `
20313
20632
  .knobs-ui .knobs-wt-stitle{font-weight:var(--_k-weight-label);line-height:var(--kw-line);color:var(--_k-fg);text-align:left}
20314
20633
  .knobs-ui button.knobs-wt-stitle{cursor:default;text-decoration:underline;text-decoration-color:transparent;text-underline-offset:3px;transition:text-decoration-color calc(var(--_k-hover-out) * 1ms)}
20315
20634
  .knobs-ui :is(.knobs-wt-srow[data-go]:hover button.knobs-wt-stitle,button.knobs-wt-stitle:focus-visible){text-decoration-color:color-mix(in oklch,currentColor 45%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
20316
- .knobs-ui .knobs-wt-ssummary{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
20635
+ .knobs-ui .knobs-wt-ssummary{margin:0;max-width:var(--kw-measure);color:var(--_k-fg-2)}
20317
20636
  .knobs-wt-smeta{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px;font:var(--_k-weight) var(--_k-type-small)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3)}
20318
20637
  .knobs-wt-delta{display:inline-flex;gap:6px}
20319
20638
  .knobs-wt-add{color:var(--_k-diff-add)}
@@ -20529,7 +20848,7 @@ async function mountCompare(host, m, look, alive, onEvent) {
20529
20848
  before: side(before, "Before"),
20530
20849
  after: side(after, "After"),
20531
20850
  mode,
20532
- timeline: true,
20851
+ timeline: mediaKind(before.src) === "video" || mediaKind(after.src) === "video",
20533
20852
  lazy: false,
20534
20853
  ...aspect ? { aspect } : {},
20535
20854
  ...m.strip ? { strip: true } : {},
@@ -20653,7 +20972,13 @@ async function mountVideo(host, m, look, alive, onEvent) {
20653
20972
  ...m.captions ? { captions: m.captions } : {},
20654
20973
  ...aspect ? { aspect } : {},
20655
20974
  ...m.label ? { label: m.label } : {},
20656
- ...m.chapters?.length ? { chapters: m.chapters.map((c) => ({ label: c.label, start: c.start, ...c.id ? { id: c.id } : {} })) } : {},
20975
+ ...m.chapters?.length ? {
20976
+ chapters: m.chapters.map((c) => ({
20977
+ label: c.label,
20978
+ start: c.start,
20979
+ ...c.id ? { id: c.id } : {}
20980
+ }))
20981
+ } : {},
20657
20982
  onPlay: (on, time) => {
20658
20983
  marking?.kick();
20659
20984
  onEvent?.({ what: on ? "play" : "pause", time: round2(time) });
@@ -20886,12 +21211,39 @@ function mountProse(host, m) {
20886
21211
  host.append(el);
20887
21212
  return { dispose: () => el.remove() };
20888
21213
  }
21214
+ function mountFacts(host, m) {
21215
+ const box = renderFacts(host.ownerDocument, m.groups);
21216
+ host.append(box);
21217
+ return { dispose: () => box.remove() };
21218
+ }
21219
+ function mountDecision(host, m, slot) {
21220
+ const root = div(host.ownerDocument, "knobs-wt-options");
21221
+ const slots = [];
21222
+ const tiles = m.options.map((o) => optionTile(host.ownerDocument, o, {
21223
+ media: (tile) => {
21224
+ if (o.media)
21225
+ slots.push(slot(tile, o.media));
21226
+ }
21227
+ }).tile);
21228
+ root.append(...tiles);
21229
+ host.append(root);
21230
+ const unlayout = layoutOptions(root, tiles);
21231
+ return {
21232
+ pause: () => slots.forEach((s) => s.pause()),
21233
+ dispose: () => {
21234
+ unlayout();
21235
+ slots.forEach((s) => s.dispose());
21236
+ root.remove();
21237
+ }
21238
+ };
21239
+ }
20889
21240
  var HEAVY = new Set(["compare", "video", "diff", "mount"]);
20890
21241
  var RELEASES = new Set(["compare", "video"]);
20891
- var labelOf2 = (m) => ("label" in m) && m.kind !== "metric" ? m.label : undefined;
20892
- function mediaSlot(container, options) {
21242
+ var labelOf2 = (m) => m.kind === "facts" ? m.title : ("label" in m) && m.kind !== "metric" ? m.label : undefined;
21243
+ function mediaSlot(container, given) {
20893
21244
  const doc = container.ownerDocument;
20894
21245
  injectWalkthroughStyles(doc);
21246
+ const options = reviewLook(given, container);
20895
21247
  const look = {
20896
21248
  ...options.preset ? { preset: options.preset } : {},
20897
21249
  ...options.theme ? { theme: options.theme } : {}
@@ -20901,7 +21253,7 @@ function mediaSlot(container, options) {
20901
21253
  root.dataset.kind = m.kind;
20902
21254
  applyLook2(root, options);
20903
21255
  const label = labelOf2(m);
20904
- if (label && (m.kind === "video" || m.kind === "image" || m.kind === "code" || m.kind === "terminal")) {
21256
+ if (label && (m.kind === "video" || m.kind === "image" || m.kind === "code" || m.kind === "terminal" || m.kind === "facts")) {
20905
21257
  const l = doc.createElement("span");
20906
21258
  l.className = "knobs-wt-slot-label";
20907
21259
  l.textContent = label;
@@ -20981,6 +21333,16 @@ function mediaSlot(container, options) {
20981
21333
  return done(mountStack(host, m, options.steps, options.findPR));
20982
21334
  case "table":
20983
21335
  return done(mountTable(host, m, options.steps));
21336
+ case "facts":
21337
+ return done(mountFacts(host, m));
21338
+ case "decision":
21339
+ return done(mountDecision(host, m, (tile, media) => mediaSlot(tile, {
21340
+ ...look,
21341
+ media,
21342
+ ...options.lazy === false ? { lazy: false } : {},
21343
+ ...options.steps ? { steps: options.steps } : {},
21344
+ ...options.onEvent ? { onEvent: options.onEvent } : {}
21345
+ })));
20984
21346
  case "mount": {
20985
21347
  const cleanup = m.mount(host);
20986
21348
  return done({ dispose: () => cleanup ? cleanup() : host.textContent = "" });
@@ -21035,14 +21397,14 @@ function mediaSlot(container, options) {
21035
21397
  var CHECK4 = icon("check", { small: true });
21036
21398
 
21037
21399
  // src/ui/walkthrough.ts
21038
- var el3 = (doc, tag, cls) => {
21400
+ var el4 = (doc, tag, cls) => {
21039
21401
  const n = doc.createElement(tag);
21040
21402
  if (cls)
21041
21403
  n.className = cls;
21042
21404
  return n;
21043
21405
  };
21044
21406
  var prose2 = (doc, tag, cls, text) => {
21045
- const n = el3(doc, tag, cls);
21407
+ const n = el4(doc, tag, cls);
21046
21408
  n.innerHTML = inlineMarkdown(text);
21047
21409
  return n;
21048
21410
  };
@@ -21067,9 +21429,10 @@ var DOCK_GAP = 12;
21067
21429
  var CALM2 = springMotion(SPRINGS.calm);
21068
21430
  var SMOOTH = springMotion(SPRINGS.smooth);
21069
21431
  var BACK_MS = 4500;
21070
- function tasteTest(container, options) {
21432
+ function tasteTest(container, given) {
21071
21433
  const doc = container.ownerDocument;
21072
21434
  injectWalkthroughStyles(doc);
21435
+ const options = reviewLook(given, container);
21073
21436
  const look = lookOf2(options);
21074
21437
  const list = options.options.map((o, i) => ("letter" in o) ? o : {
21075
21438
  id: o.id ?? String.fromCharCode(97 + i),
@@ -21077,66 +21440,55 @@ function tasteTest(container, options) {
21077
21440
  letter: String.fromCharCode(65 + i),
21078
21441
  ...o.caption ? { caption: o.caption } : {},
21079
21442
  ...o.media !== undefined ? { media: fillMedia(o.media) } : {},
21080
- suggested: o.suggested === true
21443
+ ...o.facts ? { facts: fillFacts(o.facts) } : {},
21444
+ suggested: o.suggested === true || o.recommended === true
21081
21445
  });
21082
21446
  const pickable = options.pickable !== false;
21083
21447
  let picked = pickable ? options.picked ?? null : null;
21084
- const root = el3(doc, "div", "knobs-wt-options");
21448
+ const root = el4(doc, "div", "knobs-wt-options");
21085
21449
  if (pickable) {
21086
21450
  root.setAttribute("role", "radiogroup");
21087
21451
  root.setAttribute("aria-label", "Options");
21088
21452
  } else
21089
21453
  root.dataset.static = "";
21090
- root.style.setProperty("--kw-n", String(Math.min(list.length, 3)));
21091
21454
  if (!container.closest(".knobs-ui"))
21092
21455
  applyLook2(root, options);
21093
21456
  const slots = [];
21094
21457
  const tiles = list.map((o, i) => {
21095
- const tile = el3(doc, "div", "knobs-wt-option");
21096
- if (!pickable) {
21097
- const name = el3(doc, "span", "knobs-wt-slot-label");
21098
- name.textContent = o.label;
21099
- tile.append(name);
21100
- }
21101
- const pick = el3(doc, "button", "knobs-wt-pick");
21102
- pick.type = "button";
21103
- pick.setAttribute("role", "radio");
21104
- if (i < 9)
21105
- pick.setAttribute("aria-keyshortcuts", String(i + 1));
21106
- const letter = el3(doc, "span", "knobs-wt-letter");
21107
- letter.textContent = o.letter;
21108
- const name = el3(doc, "span", "knobs-wt-olabel");
21109
- name.textContent = o.label;
21110
- pick.append(letter, name);
21111
- if (o.suggested) {
21112
- const s = el3(doc, "span", "knobs-wt-suggested");
21113
- s.textContent = "Suggested";
21114
- pick.append(s);
21115
- }
21116
- if (i < 9) {
21117
- const key = el3(doc, "span", "knobs-wt-key");
21118
- key.textContent = String(i + 1);
21119
- pick.append(key);
21120
- }
21121
- const check = el3(doc, "span", "knobs-wt-check");
21122
- check.innerHTML = CHECK4;
21123
- pick.append(check);
21124
- pick.addEventListener("click", () => {
21125
- const next = picked === o.id ? null : o.id;
21126
- api.pick(next);
21127
- options.onPick?.(next);
21458
+ const button = pickable ? el4(doc, "button", "knobs-wt-pick") : undefined;
21459
+ const { tile, kicker } = optionTile(doc, {
21460
+ number: `Option ${i + 1}`,
21461
+ title: o.label,
21462
+ ...o.caption ? { summary: o.caption } : {},
21463
+ recommended: o.suggested,
21464
+ ...o.facts ? { facts: o.facts } : {}
21465
+ }, {
21466
+ ...button ? { head: button } : {},
21467
+ media: (t) => {
21468
+ if (o.media)
21469
+ slots.push(mediaSlot(t, {
21470
+ ...look,
21471
+ media: o.media,
21472
+ ...options.lazy === false ? { lazy: false } : {},
21473
+ ...options.steps ? { steps: options.steps } : {}
21474
+ }));
21475
+ }
21128
21476
  });
21129
- if (pickable)
21130
- tile.append(pick);
21131
- if (o.media)
21132
- slots.push(mediaSlot(tile, {
21133
- ...look,
21134
- media: o.media,
21135
- ...options.lazy === false ? { lazy: false } : {},
21136
- ...options.steps ? { steps: options.steps } : {}
21137
- }));
21138
- if (o.caption)
21139
- tile.append(prose2(doc, "p", "knobs-wt-ocap", o.caption));
21477
+ const pick = button ?? el4(doc, "button");
21478
+ if (button) {
21479
+ button.type = "button";
21480
+ button.setAttribute("role", "radio");
21481
+ if (i < 9)
21482
+ button.setAttribute("aria-keyshortcuts", String(i + 1));
21483
+ const check = el4(doc, "span", "knobs-wt-check");
21484
+ check.innerHTML = CHECK4;
21485
+ kicker.append(check);
21486
+ button.addEventListener("click", () => {
21487
+ const next = picked === o.id ? null : o.id;
21488
+ api.pick(next);
21489
+ options.onPick?.(next);
21490
+ });
21491
+ }
21140
21492
  root.append(tile);
21141
21493
  return { tile, pick };
21142
21494
  });
@@ -21150,6 +21502,7 @@ function tasteTest(container, options) {
21150
21502
  };
21151
21503
  sync();
21152
21504
  container.append(root);
21505
+ const unlayout = layoutOptions(root, tiles.map((t) => t.tile));
21153
21506
  const api = {
21154
21507
  element: root,
21155
21508
  get picked() {
@@ -21162,6 +21515,7 @@ function tasteTest(container, options) {
21162
21515
  },
21163
21516
  pause: () => slots.forEach((s) => s.pause()),
21164
21517
  dispose() {
21518
+ unlayout();
21165
21519
  slots.forEach((s) => s.dispose());
21166
21520
  root.remove();
21167
21521
  }
@@ -21178,13 +21532,14 @@ function scrollerOf(el) {
21178
21532
  }
21179
21533
  return doc.scrollingElement ?? doc.documentElement;
21180
21534
  }
21181
- function walkthrough(container, options) {
21535
+ function walkthrough(container, given) {
21182
21536
  const doc = container.ownerDocument;
21183
21537
  const win = doc.defaultView;
21184
21538
  injectWalkthroughStyles(doc);
21539
+ const options = reviewLook(given, container);
21185
21540
  const look = lookOf2(options);
21186
21541
  const storage = options.storage === undefined ? browserStorage() : options.storage;
21187
- const root = el3(doc, "div", "knobs-walkthrough");
21542
+ const root = el4(doc, "div", "knobs-walkthrough");
21188
21543
  applyLook2(root, options);
21189
21544
  container.append(root);
21190
21545
  let page = normalizeWalkthrough(options.doc);
@@ -21397,7 +21752,7 @@ function walkthrough(container, options) {
21397
21752
  return true;
21398
21753
  return false;
21399
21754
  };
21400
- const scrim = el3(doc, "div", "knobs-wt-scrim");
21755
+ const scrim = el4(doc, "div", "knobs-wt-scrim");
21401
21756
  scrim.addEventListener("click", () => setFull(false));
21402
21757
  const fullRect = () => ({
21403
21758
  x: DOCK_INSET,
@@ -21447,7 +21802,7 @@ function walkthrough(container, options) {
21447
21802
  }
21448
21803
  if (!full) {
21449
21804
  const r = v.card.getBoundingClientRect();
21450
- const hold = el3(doc, "div", "knobs-wt-hold-card");
21805
+ const hold = el4(doc, "div", "knobs-wt-hold-card");
21451
21806
  hold.style.height = `${r.height}px`;
21452
21807
  v.card.before(hold);
21453
21808
  v.card.dataset.full = "";
@@ -21597,7 +21952,7 @@ function walkthrough(container, options) {
21597
21952
  toast("Couldn't reach the microphone", { tone: "warn", look, doc });
21598
21953
  }
21599
21954
  };
21600
- const backPill = el3(doc, "button", "knobs-wt-back");
21955
+ const backPill = el4(doc, "button", "knobs-wt-back");
21601
21956
  backPill.type = "button";
21602
21957
  let backTo = null;
21603
21958
  let backTimer = 0;
@@ -21617,7 +21972,7 @@ function walkthrough(container, options) {
21617
21972
  return;
21618
21973
  backTo = key;
21619
21974
  backPill.innerHTML = ICON.back;
21620
- const words = el3(doc, "span", "knobs-wt-back-label");
21975
+ const words = el4(doc, "span", "knobs-wt-back-label");
21621
21976
  words.textContent = `Back to ${plainMarkdown(v.step.title)}`;
21622
21977
  backPill.append(words);
21623
21978
  backPill.dataset.on = "";
@@ -21625,16 +21980,16 @@ function walkthrough(container, options) {
21625
21980
  backTimer = win.setTimeout(hideBack, BACK_MS);
21626
21981
  };
21627
21982
  const bar = (() => {
21628
- const dock = el3(doc, "div", "knobs-wt-dock");
21629
- const box = el3(doc, "div", "knobs-wt-notebar");
21630
- const on = el3(doc, "div", "knobs-wt-on");
21631
- const row = el3(doc, "div", "knobs-wt-row");
21632
- const input = el3(doc, "textarea", "knobs-wt-input");
21983
+ const dock = el4(doc, "div", "knobs-wt-dock");
21984
+ const box = el4(doc, "div", "knobs-wt-notebar");
21985
+ const on = el4(doc, "div", "knobs-wt-on");
21986
+ const row = el4(doc, "div", "knobs-wt-row");
21987
+ const input = el4(doc, "textarea", "knobs-wt-input");
21633
21988
  input.rows = 1;
21634
21989
  input.setAttribute("aria-label", "Note");
21635
- const attach = el3(doc, "div", "knobs-wt-attach");
21990
+ const attach = el4(doc, "div", "knobs-wt-attach");
21636
21991
  const btn = (icon, label, run) => {
21637
- const b = el3(doc, "button", "knobs-wt-tool");
21992
+ const b = el4(doc, "button", "knobs-wt-tool");
21638
21993
  b.type = "button";
21639
21994
  b.innerHTML = icon;
21640
21995
  b.setAttribute("aria-label", label);
@@ -21645,17 +22000,17 @@ function walkthrough(container, options) {
21645
22000
  const fullBtn = btn(ICON.full, "Full window · f", () => setFull(!full));
21646
22001
  const penBtn = btn(ICON.pen, "Draw on the media", () => setInk(!ink.on));
21647
22002
  const micBtn = btn(ICON.mic, "Record a voice note", () => void toggleVoice());
21648
- const micTime = el3(doc, "span", "knobs-wt-rec");
22003
+ const micTime = el4(doc, "span", "knobs-wt-rec");
21649
22004
  micBtn.append(micTime);
21650
22005
  const sendBtn = btn(ICON.send, "Send · Enter", () => send());
21651
22006
  sendBtn.dataset.send = "";
21652
22007
  row.append(input, attach, fullBtn, penBtn, micBtn, sendBtn);
21653
22008
  box.append(on, row);
21654
- const launch = el3(doc, "button", "knobs-wt-launch");
22009
+ const launch = el4(doc, "button", "knobs-wt-launch");
21655
22010
  launch.type = "button";
21656
22011
  launch.innerHTML = ICON.pen;
21657
22012
  launch.append("Note");
21658
- const kbd = el3(doc, "kbd");
22013
+ const kbd = el4(doc, "kbd");
21659
22014
  kbd.textContent = "n";
21660
22015
  launch.append(kbd);
21661
22016
  launch.setAttribute("aria-label", "Write a note (n)");
@@ -21707,9 +22062,9 @@ function walkthrough(container, options) {
21707
22062
  }
21708
22063
  });
21709
22064
  const chip = (text, clear) => {
21710
- const c = el3(doc, "span", "knobs-wt-chip-a");
22065
+ const c = el4(doc, "span", "knobs-wt-chip-a");
21711
22066
  c.append(text);
21712
- const x = el3(doc, "button", "knobs-wt-x");
22067
+ const x = el4(doc, "button", "knobs-wt-x");
21713
22068
  x.type = "button";
21714
22069
  x.innerHTML = ICON.close;
21715
22070
  x.setAttribute("aria-label", `Remove ${text}`);
@@ -21858,9 +22213,9 @@ function walkthrough(container, options) {
21858
22213
  }
21859
22214
  };
21860
22215
  const renderNote = (v, n, entering = false) => {
21861
- const row = el3(doc, "div", "knobs-wt-n");
22216
+ const row = el4(doc, "div", "knobs-wt-n");
21862
22217
  row.dataset.id = n.id;
21863
- const meta = el3(doc, "span", "knobs-wt-n-meta");
22218
+ const meta = el4(doc, "span", "knobs-wt-n-meta");
21864
22219
  const m = n.context.media;
21865
22220
  meta.textContent = m?.time !== undefined ? formatTime(m.time) : m?.file ? m.file.split("/").pop() : "·";
21866
22221
  if (m?.time !== undefined) {
@@ -21876,11 +22231,11 @@ function walkthrough(container, options) {
21876
22231
  });
21877
22232
  }
21878
22233
  row.append(meta);
21879
- const body = el3(doc, "div", "knobs-wt-n-body");
22234
+ const body = el4(doc, "div", "knobs-wt-n-body");
21880
22235
  if (n.text)
21881
22236
  body.append(prose2(doc, "p", "knobs-wt-n-text", n.text));
21882
22237
  if (n.voice) {
21883
- const a = el3(doc, "audio", "knobs-wt-n-voice");
22238
+ const a = el4(doc, "audio", "knobs-wt-n-voice");
21884
22239
  a.controls = true;
21885
22240
  a.preload = "none";
21886
22241
  a.src = n.voice.url;
@@ -21900,7 +22255,7 @@ function walkthrough(container, options) {
21900
22255
  body.append(thumb);
21901
22256
  }
21902
22257
  row.append(body);
21903
- const x = el3(doc, "button", "knobs-wt-x");
22258
+ const x = el4(doc, "button", "knobs-wt-x");
21904
22259
  x.type = "button";
21905
22260
  x.innerHTML = ICON.close;
21906
22261
  x.setAttribute("aria-label", "Delete this note");
@@ -21940,9 +22295,9 @@ function walkthrough(container, options) {
21940
22295
  row.addEventListener("transitionend", done, { once: true });
21941
22296
  win.setTimeout(done, 600);
21942
22297
  };
21943
- const metaLine = (cls) => el3(doc, "div", `knobs-wt-metaline ${cls}`);
22298
+ const metaLine = (cls) => el4(doc, "div", `knobs-wt-metaline ${cls}`);
21944
22299
  const metaText = (line, text, cls) => {
21945
- const s = el3(doc, "span", cls);
22300
+ const s = el4(doc, "span", cls);
21946
22301
  s.textContent = text;
21947
22302
  line.append(s);
21948
22303
  return s;
@@ -21956,7 +22311,7 @@ function walkthrough(container, options) {
21956
22311
  line.append(chip);
21957
22312
  return chip;
21958
22313
  }
21959
- const a = el3(doc, "a");
22314
+ const a = el4(doc, "a");
21960
22315
  a.href = href;
21961
22316
  a.target = "_blank";
21962
22317
  a.rel = "noreferrer";
@@ -21966,9 +22321,9 @@ function walkthrough(container, options) {
21966
22321
  return a;
21967
22322
  };
21968
22323
  const feedItem = (f, now) => {
21969
- const li = el3(doc, "li", "knobs-wt-fi");
22324
+ const li = el4(doc, "li", "knobs-wt-fi");
21970
22325
  li.dataset.kind = f.kind;
21971
- const q = el3(doc, "div", "knobs-wt-fi-q");
22326
+ const q = el4(doc, "div", "knobs-wt-fi-q");
21972
22327
  const meta = metaLine("knobs-wt-fi-meta");
21973
22328
  if (f.at !== null) {
21974
22329
  const when = metaText(meta, ago(f.at, now));
@@ -21977,7 +22332,7 @@ function walkthrough(container, options) {
21977
22332
  const step = f.step ? page.steps.find((s) => s.key === f.step) : undefined;
21978
22333
  if (step) {
21979
22334
  const section = page.sections.find((s) => s.id === step.section);
21980
- const b = el3(doc, "button", "knobs-wt-fi-step");
22335
+ const b = el4(doc, "button", "knobs-wt-fi-step");
21981
22336
  b.type = "button";
21982
22337
  b.textContent = section?.link?.label ?? plainMarkdown(step.title);
21983
22338
  b.title = `Go to “${plainMarkdown(step.title)}”`;
@@ -21999,19 +22354,19 @@ function walkthrough(container, options) {
21999
22354
  q.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
22000
22355
  li.append(q);
22001
22356
  if (f.q || f.trail.length) {
22002
- const a = el3(doc, "div", "knobs-wt-fi-a");
22357
+ const a = el4(doc, "div", "knobs-wt-fi-a");
22003
22358
  if (f.q)
22004
22359
  a.append(prose2(doc, "div", "knobs-wt-fi-answer", f.text));
22005
22360
  if (f.trail.length) {
22006
- const list = el3(doc, "ol", "knobs-wt-trail");
22361
+ const list = el4(doc, "ol", "knobs-wt-trail");
22007
22362
  for (const line of f.trail) {
22008
22363
  const i = line.indexOf(" · ");
22009
- const row = el3(doc, "li");
22010
- const words = el3(doc, "span", "knobs-wt-trail-text");
22364
+ const row = el4(doc, "li");
22365
+ const words = el4(doc, "span", "knobs-wt-trail-text");
22011
22366
  words.textContent = i < 0 ? line : line.slice(i + 3);
22012
22367
  row.append(words);
22013
22368
  if (i >= 0) {
22014
- const at = el3(doc, "span", "knobs-wt-trail-at");
22369
+ const at = el4(doc, "span", "knobs-wt-trail-at");
22015
22370
  at.textContent = line.slice(0, i);
22016
22371
  row.append(at);
22017
22372
  }
@@ -22045,11 +22400,11 @@ function walkthrough(container, options) {
22045
22400
  root.dataset.rail = options.rail === false ? "none" : options.rail === "right" ? "right" : "left";
22046
22401
  root.toggleAttribute("data-cards", options.cards === true);
22047
22402
  root.toggleAttribute("data-review", review);
22048
- const layout = el3(doc, "div", "knobs-wt-layout");
22049
- const main = el3(doc, "div", "knobs-wt-main");
22050
- const flow = el3(doc, "div", "knobs-wt-flow");
22051
- const head = el3(doc, "header", "knobs-wt-head");
22052
- const h1 = el3(doc, "h1", "knobs-wt-title");
22403
+ const layout = el4(doc, "div", "knobs-wt-layout");
22404
+ const main = el4(doc, "div", "knobs-wt-main");
22405
+ const flow = el4(doc, "div", "knobs-wt-flow");
22406
+ const head = el4(doc, "header", "knobs-wt-head");
22407
+ const h1 = el4(doc, "h1", "knobs-wt-title");
22053
22408
  h1.textContent = page.title;
22054
22409
  head.append(h1);
22055
22410
  if (page.summary)
@@ -22057,13 +22412,13 @@ function walkthrough(container, options) {
22057
22412
  if (page.more)
22058
22413
  head.append(more(page.more));
22059
22414
  if (page.meta.length) {
22060
- const meta = el3(doc, "div", "knobs-wt-meta");
22415
+ const meta = el4(doc, "div", "knobs-wt-meta");
22061
22416
  for (const m of page.meta) {
22062
- const s = el3(doc, "span");
22417
+ const s = el4(doc, "span");
22063
22418
  s.append(m.label);
22064
- const b = el3(doc, "b");
22419
+ const b = el4(doc, "b");
22065
22420
  if (m.href) {
22066
- const a = el3(doc, "a");
22421
+ const a = el4(doc, "a");
22067
22422
  a.href = m.href;
22068
22423
  a.target = "_blank";
22069
22424
  a.rel = "noreferrer";
@@ -22079,14 +22434,14 @@ function walkthrough(container, options) {
22079
22434
  flow.append(head);
22080
22435
  let feedEl = null;
22081
22436
  if (page.feed.length) {
22082
- feedEl = el3(doc, "section", "knobs-wt-feed");
22437
+ feedEl = el4(doc, "section", "knobs-wt-feed");
22083
22438
  feedEl.id = "wt-latest";
22084
22439
  feedEl.setAttribute("aria-label", "Updates and answers");
22085
- const fh = el3(doc, "header", "knobs-wt-feed-head");
22086
- const h = el3(doc, "h2", "knobs-wt-feed-title");
22440
+ const fh = el4(doc, "header", "knobs-wt-feed-head");
22441
+ const h = el4(doc, "h2", "knobs-wt-feed-title");
22087
22442
  h.textContent = page.feed.some((f) => f.kind === "qa") ? "Updates & Q&A" : "Updates";
22088
22443
  fh.append(h);
22089
- const list = el3(doc, "ol", "knobs-wt-feed-list");
22444
+ const list = el4(doc, "ol", "knobs-wt-feed-list");
22090
22445
  const now = Date.now();
22091
22446
  for (const f of page.feed)
22092
22447
  list.append(feedItem(f, now));
@@ -22096,21 +22451,21 @@ function walkthrough(container, options) {
22096
22451
  let rail = null;
22097
22452
  count = fill = tocEl = null;
22098
22453
  if (options.rail !== false) {
22099
- rail = el3(doc, "aside", "knobs-wt-rail");
22454
+ rail = el4(doc, "aside", "knobs-wt-rail");
22100
22455
  rail.setAttribute("aria-label", "Contents");
22101
- tocEl = el3(doc, "nav", "knobs-wt-toc");
22456
+ tocEl = el4(doc, "nav", "knobs-wt-toc");
22102
22457
  tocEl.setAttribute("aria-label", "Contents");
22103
22458
  if (feedEl) {
22104
- const group = el3(doc, "div", "knobs-wt-toc-group");
22459
+ const group = el4(doc, "div", "knobs-wt-toc-group");
22105
22460
  const target = feedEl;
22106
22461
  group.append(tocRow("Latest", false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
22107
22462
  tocEl.append(group);
22108
22463
  }
22109
22464
  if (review) {
22110
- const progress = el3(doc, "div", "knobs-wt-progress");
22111
- count = el3(doc, "div", "knobs-wt-count");
22112
- const meter = el3(doc, "div", "knobs-wt-bar");
22113
- fill = el3(doc, "div", "knobs-wt-fill");
22465
+ const progress = el4(doc, "div", "knobs-wt-progress");
22466
+ count = el4(doc, "div", "knobs-wt-count");
22467
+ const meter = el4(doc, "div", "knobs-wt-bar");
22468
+ fill = el4(doc, "div", "knobs-wt-fill");
22114
22469
  meter.append(fill);
22115
22470
  progress.append(count, meter);
22116
22471
  rail.append(progress);
@@ -22119,15 +22474,15 @@ function walkthrough(container, options) {
22119
22474
  if (review)
22120
22475
  rail.append(actions(true));
22121
22476
  if (review && options.keys !== false) {
22122
- const keys = el3(doc, "div", "knobs-wt-keys");
22477
+ const keys = el4(doc, "div", "knobs-wt-keys");
22123
22478
  keys.innerHTML = "<kbd>j</kbd> <kbd>k</kbd> steps · <kbd>←</kbd> <kbd>→</kbd> seek · <kbd>f</kbd> full · <kbd>n</kbd> note" + (page.steps.some((s) => s.ask === "pick") ? " · <kbd>1</kbd>–<kbd>9</kbd> pick" : "");
22124
22479
  rail.append(keys);
22125
22480
  }
22126
22481
  }
22127
22482
  for (const section of page.sections) {
22128
- const sec = el3(doc, "section", "knobs-wt-section");
22483
+ const sec = el4(doc, "section", "knobs-wt-section");
22129
22484
  sec.id = `wt-${section.id}`;
22130
- const sh = el3(doc, "header", "knobs-wt-shead");
22485
+ const sh = el4(doc, "header", "knobs-wt-shead");
22131
22486
  const status = review ? section.status : undefined;
22132
22487
  if (section.kicker || section.link || status) {
22133
22488
  const meta = metaLine("knobs-wt-kicker");
@@ -22141,10 +22496,10 @@ function walkthrough(container, options) {
22141
22496
  }
22142
22497
  sh.append(meta);
22143
22498
  }
22144
- const h2 = el3(doc, "h2", "knobs-wt-heading");
22499
+ const h2 = el4(doc, "h2", "knobs-wt-heading");
22145
22500
  h2.innerHTML = inlineMarkdown(section.title);
22146
22501
  sh.append(h2);
22147
- const intro = el3(doc, "div", "knobs-wt-sintro");
22502
+ const intro = el4(doc, "div", "knobs-wt-sintro");
22148
22503
  intro.append(sh);
22149
22504
  if (section.summary)
22150
22505
  intro.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
@@ -22153,7 +22508,7 @@ function walkthrough(container, options) {
22153
22508
  sec.append(intro);
22154
22509
  let group = null;
22155
22510
  if (tocEl) {
22156
- group = el3(doc, "div", "knobs-wt-toc-group");
22511
+ group = el4(doc, "div", "knobs-wt-toc-group");
22157
22512
  const first = section.steps[0]?.key;
22158
22513
  group.append(tocRow(plainMarkdown(section.title), false, () => first ? go(first) : undefined, section.id));
22159
22514
  tocEl.append(group);
@@ -22181,7 +22536,8 @@ function walkthrough(container, options) {
22181
22536
  if (tocEl)
22182
22537
  pill = selectionPill(tocEl, {
22183
22538
  selected: () => viewOf(current)?.toc ?? null,
22184
- rows: () => tocRows
22539
+ rows: () => tocRows,
22540
+ spring: SPRINGS.snappy
22185
22541
  });
22186
22542
  syncProgress();
22187
22543
  current = null;
@@ -22195,8 +22551,8 @@ function walkthrough(container, options) {
22195
22551
  }));
22196
22552
  };
22197
22553
  const more = (text) => {
22198
- const box = el3(doc, "div", "knobs-wt-more");
22199
- const body = el3(doc, "div", "knobs-wt-more-text");
22554
+ const box = el4(doc, "div", "knobs-wt-more");
22555
+ const body = el4(doc, "div", "knobs-wt-more-text");
22200
22556
  for (const para of text.split(/\n\s*\n/))
22201
22557
  if (para.trim())
22202
22558
  body.append(prose2(doc, "p", "", para.trim()));
@@ -22204,25 +22560,25 @@ function walkthrough(container, options) {
22204
22560
  return box;
22205
22561
  };
22206
22562
  const tocRow = (label, sub, run, section) => {
22207
- const b = el3(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
22563
+ const b = el4(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
22208
22564
  b.type = "button";
22209
22565
  if (section)
22210
22566
  b.dataset.section = section;
22211
- const t = el3(doc, "span", "knobs-wt-toc-label");
22567
+ const t = el4(doc, "span", "knobs-wt-toc-label");
22212
22568
  t.textContent = label;
22213
22569
  b.title = label;
22214
22570
  b.append(t);
22215
22571
  if (review)
22216
- b.append(el3(doc, "span", "knobs-wt-toc-end"));
22572
+ b.append(el4(doc, "span", "knobs-wt-toc-end"));
22217
22573
  b.addEventListener("mousedown", (ev) => ev.preventDefault());
22218
22574
  b.addEventListener("click", run);
22219
22575
  tocRows.push(b);
22220
22576
  return b;
22221
22577
  };
22222
22578
  const actions = (rail) => {
22223
- const row = el3(doc, "div", "knobs-wt-actions");
22579
+ const row = el4(doc, "div", "knobs-wt-actions");
22224
22580
  const btn = (text, run, primary = false) => {
22225
- const b = el3(doc, "button", "knobs-wt-btn");
22581
+ const b = el4(doc, "button", "knobs-wt-btn");
22226
22582
  b.type = "button";
22227
22583
  b.textContent = text;
22228
22584
  if (primary)
@@ -22260,13 +22616,13 @@ function walkthrough(container, options) {
22260
22616
  }
22261
22617
  };
22262
22618
  const stepView = (step, group) => {
22263
- const card = el3(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
22619
+ const card = el4(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
22264
22620
  card.id = `wt-${step.key.replace("/", "--")}`;
22265
22621
  card.tabIndex = -1;
22266
22622
  card.dataset.key = step.key;
22267
22623
  card.setAttribute("aria-label", plainMarkdown(step.title));
22268
- const head = el3(doc, "header", "knobs-wt-step-head");
22269
- const h3 = el3(doc, "h3", "knobs-wt-stepTitle");
22624
+ const head = el4(doc, "header", "knobs-wt-step-head");
22625
+ const h3 = el4(doc, "h3", "knobs-wt-stepTitle");
22270
22626
  h3.innerHTML = inlineMarkdown(step.title);
22271
22627
  head.append(h3);
22272
22628
  if (step.caption)
@@ -22278,7 +22634,7 @@ function walkthrough(container, options) {
22278
22634
  const onEvent = (e) => record({ ...e, step: step.key });
22279
22635
  let media = null;
22280
22636
  if (step.media.length) {
22281
- media = el3(doc, "div", "knobs-wt-media");
22637
+ media = el4(doc, "div", "knobs-wt-media");
22282
22638
  media.style.setProperty("--kw-n", String(Math.min(step.media.length, step.columns)));
22283
22639
  for (const m of step.media)
22284
22640
  slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks, findPR: findPRIn }));
@@ -22298,7 +22654,7 @@ function walkthrough(container, options) {
22298
22654
  });
22299
22655
  media ??= taste.element;
22300
22656
  }
22301
- const notes = el3(doc, "div", "knobs-wt-notes");
22657
+ const notes = el4(doc, "div", "knobs-wt-notes");
22302
22658
  if (review)
22303
22659
  card.append(notes);
22304
22660
  card.addEventListener("focusin", () => {
@@ -22312,7 +22668,7 @@ function walkthrough(container, options) {
22312
22668
  if (group) {
22313
22669
  toc = tocRow(plainMarkdown(step.title), true, () => go(step.key));
22314
22670
  if (review) {
22315
- glyph = el3(doc, "span", "knobs-wt-toc-glyph");
22671
+ glyph = el4(doc, "span", "knobs-wt-toc-glyph");
22316
22672
  toc.querySelector(".knobs-wt-toc-end").append(glyph);
22317
22673
  }
22318
22674
  group.append(toc);