@kitlangton/knobs 0.4.0-next.1 → 0.4.0-next.2

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.js CHANGED
@@ -18970,7 +18970,7 @@ var walkthroughCss = `
18970
18970
  .knobs-walkthrough a{color:inherit;text-decoration:underline;text-decoration-color:color-mix(in srgb,currentColor 35%,transparent);text-underline-offset:2px;transition:text-decoration-color .15s}
18971
18971
  .knobs-walkthrough a:hover{text-decoration-color:currentColor}
18972
18972
  .knobs-walkthrough code,.knobs-wt-slot code{font:calc(1em - 1px) var(--_k-mono);padding:1px 5px;border-radius:5px;background:var(--_k-fill)}
18973
- :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-step){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
18973
+ :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-step,button.knobs-wt-stitle,.knobs-wt-chapter){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
18974
18974
  background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
18975
18975
 
18976
18976
  /* ——— layout: the rail beside the column; one column where it's narrow ——— */
@@ -19029,9 +19029,9 @@ var walkthroughCss = `
19029
19029
  .knobs-wt-fi-body{max-width:var(--kw-measure);color:var(--_k-fg)}
19030
19030
  .knobs-wt-fi-q{color:var(--_k-fg-2)}
19031
19031
  .knobs-wt-fi-q::before{content:"You asked: ";color:var(--_k-fg-3)}
19032
- :where(.knobs-ui) .knobs-wt-fi-step{all:revert;box-sizing:border-box;font:var(--_k-weight) calc(var(--kw-text) - 2.5px)/1 var(--_k-mono);color:var(--_k-fg-2);background:transparent;border:0;
19032
+ :where(.knobs-ui) :is(.knobs-wt-fi-step,.knobs-wt-steplink){all:revert;box-sizing:border-box;font:var(--_k-weight) calc(var(--k-walkthrough-text,14px) - 2.5px)/1 var(--_k-mono);color:var(--_k-fg-2);background:transparent;border:0;
19033
19033
  padding:3px 6px;margin:0;border-radius:6px;cursor:pointer;outline:none;transition:background .15s,color .15s}
19034
- .knobs-ui .knobs-wt-fi-step:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
19034
+ .knobs-ui :is(.knobs-wt-fi-step,.knobs-wt-steplink):is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
19035
19035
 
19036
19036
  /* ——— a step's sent notes ——— */
19037
19037
  .knobs-wt-notes{display:flex;flex-direction:column}
@@ -19167,7 +19167,8 @@ var walkthroughCss = `
19167
19167
  }
19168
19168
 
19169
19169
  /* ——— media slots ——— */
19170
- .knobs-ui.knobs-wt-slot{position:relative;min-width:0;container:knobs-wt-slot / inline-size;color:var(--_k-fg);font:var(--_k-weight) var(--k-walkthrough-text,14px)/1.5 var(--_k-font)}
19170
+ .knobs-ui.knobs-wt-slot{position:relative;min-width:0;container:knobs-wt-slot / inline-size;color:var(--_k-fg);font:var(--_k-weight) var(--k-walkthrough-text,14px)/1.5 var(--_k-font);
19171
+ --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info))}
19171
19172
  .knobs-wt-slot-label{display:block;margin:0 0 6px 2px;font:var(--_k-weight-label) calc(var(--k-walkthrough-text,14px) - 3px)/1.3 var(--_k-font);color:var(--_k-fg-2);letter-spacing:.04em;text-transform:uppercase}
19172
19173
  /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
19173
19174
  sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
@@ -19207,9 +19208,141 @@ var walkthroughCss = `
19207
19208
  .knobs-wt-text{max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
19208
19209
  .knobs-wt-hold{border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
19209
19210
  .knobs-wt-failed{display:flex;align-items:center;justify-content:center;min-height:80px;color:var(--_k-fg-3);font-size:calc(var(--k-walkthrough-text,14px) - 2px)}
19211
+
19212
+ /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
19213
+ hairlines and fills only. Tones read the status tokens; hover fades are the panel's own. ——— */
19214
+ :where(.knobs-wt-history,.knobs-wt-stack,.knobs-wt-table,.knobs-wt-chapters){--kw-t:var(--k-walkthrough-text,14px);--kw-line:calc(var(--kw-t) + 6px);--kw-rail:var(--_k-border-strong)}
19215
+ :where(.knobs-wt-slot) [data-tone="good"]{--kw-tone:var(--kw-good)}
19216
+ :where(.knobs-wt-slot) [data-tone="warn"]{--kw-tone:var(--_k-warn)}
19217
+ :where(.knobs-wt-slot) [data-tone="bad"]{--kw-tone:var(--_k-danger)}
19218
+ :where(.knobs-wt-slot) [data-tone="info"]{--kw-tone:var(--kw-info)}
19219
+ :where(.knobs-wt-slot) [data-tone="dim"]{--kw-tone:var(--_k-fg-3)}
19220
+ .knobs-wt-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
19221
+ .knobs-ui .knobs-wt-tag{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-2);font:var(--_k-weight) calc(var(--kw-t) - 3px)/1 var(--_k-mono);
19222
+ text-decoration:none;white-space:nowrap;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
19223
+ .knobs-ui a.knobs-wt-tag:is(:hover,:focus-visible){background:var(--_k-fill-selected);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
19224
+
19225
+ /* A history: dates down the left, a spine of tone dots joined by a hairline (broken around each
19226
+ dot), the event beside it. Inferred events take a ring for a dot and a dashed tag. */
19227
+ .knobs-wt-history{--kw-hy:calc(5px + var(--kw-line) / 2);display:flex;flex-direction:column;gap:18px;font-size:calc(var(--kw-t) - 1px);line-height:var(--kw-line);color:var(--_k-fg)}
19228
+ .knobs-wt-hgroup{display:flex;flex-direction:column;gap:6px;min-width:0}
19229
+ .knobs-wt-hhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px}
19230
+ .knobs-ui .knobs-wt-hheading{margin:0;font:var(--_k-weight-label) calc(var(--kw-t) - 3px)/1.3 var(--_k-font);letter-spacing:.04em;text-transform:uppercase;color:var(--_k-fg-2)}
19231
+ .knobs-wt-hsum{color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 2px);font-variant-numeric:tabular-nums}
19232
+ .knobs-wt-hlist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
19233
+ .knobs-wt-hev{display:grid;grid-template-columns:var(--kw-hw,11ch) 14px minmax(0,1fr);grid-template-areas:"when spine main";column-gap:10px;align-items:baseline;padding:5px 0}
19234
+ .knobs-wt-hwhen{grid-area:when;display:flex;flex-direction:column;min-width:0;font:var(--_k-weight) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
19235
+ .knobs-wt-hage{color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 3px);line-height:1.35}
19236
+ .knobs-wt-hspine{grid-area:spine;position:relative;align-self:stretch;margin-block:-5px}
19237
+ .knobs-wt-hspine::before,.knobs-wt-hspine::after{content:"";position:absolute;left:6.5px;width:1px;background:var(--kw-rail)}
19238
+ .knobs-wt-hspine::before{top:0;height:calc(var(--kw-hy) - 6px)}
19239
+ .knobs-wt-hspine::after{top:calc(var(--kw-hy) + 6px);bottom:0}
19240
+ .knobs-wt-hev:first-child .knobs-wt-hspine::before,.knobs-wt-hev:last-child .knobs-wt-hspine::after{content:none}
19241
+ .knobs-wt-hdot{position:absolute;left:3.5px;top:calc(var(--kw-hy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--kw-tone,var(--_k-fg-3))}
19242
+ .knobs-wt-hev[data-type="release"] .knobs-wt-hdot{border-radius:1.5px;rotate:45deg;scale:.9}
19243
+ .knobs-wt-hev[data-type="audit"] .knobs-wt-hdot{border-radius:1.5px}
19244
+ .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-hdot{background:transparent;box-shadow:inset 0 0 0 1.5px var(--kw-tone,var(--_k-fg-3))}
19245
+ .knobs-wt-hmain{grid-area:main;display:flex;flex-direction:column;gap:2px;min-width:0}
19246
+ .knobs-wt-htitle{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
19247
+ .knobs-wt-prov{padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-3);font:var(--_k-weight) calc(var(--kw-t) - 3.5px)/18px var(--_k-mono);white-space:nowrap}
19248
+ .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-prov{background:transparent;color:var(--_k-warn);outline:1px dashed color-mix(in oklch,var(--_k-warn) 50%,transparent);outline-offset:-1px}
19249
+ .knobs-ui .knobs-wt-hdetail{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
19250
+ @container knobs-wt-slot (max-width:520px){
19251
+ .knobs-wt-hev{grid-template-columns:14px minmax(0,1fr);grid-template-areas:"spine when" "spine main"}
19252
+ .knobs-wt-hwhen{flex-direction:row;flex-wrap:wrap;align-items:baseline;column-gap:8px}
19253
+ }
19254
+
19255
+ /* A stack: the trunk at the top, each PR hanging off the branch it merges into, rails drawn as a
19256
+ git graph (solid, so where two meet they don't darken). A PR with a step goes there on a click. */
19257
+ .knobs-wt-stack{--kw-spt:7px;--kw-spb:7px;--kw-sy:calc(var(--kw-spt) + var(--kw-line) / 2);display:flex;flex-direction:column;font-size:calc(var(--kw-t) - 1px);line-height:var(--kw-line);color:var(--_k-fg)}
19258
+ .knobs-wt-srow{display:grid;grid-template-columns:auto minmax(0,1fr) auto;column-gap:10px;align-items:start;margin:0 -8px;padding:var(--kw-spt) 8px var(--kw-spb);
19259
+ border-radius:max(0px,calc(var(--_k-radius-panel) - 8px));transition:background calc(var(--_k-hover-out) * 1ms)}
19260
+ .knobs-wt-srow[data-trunk]{--kw-spb:0px;grid-template-columns:auto minmax(0,1fr)}
19261
+ .knobs-wt-srow[data-go]{cursor:pointer}
19262
+ .knobs-wt-srow[data-go]:hover{background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
19263
+ .knobs-wt-lanes{display:flex;align-self:stretch;margin-block:calc(var(--kw-spt) * -1) calc(var(--kw-spb) * -1)}
19264
+ .knobs-wt-lane{position:relative;flex:none;width:16px}
19265
+ .knobs-wt-lane:is([data-rail="pass"],[data-rail="tee"],[data-down])::before{content:"";position:absolute;left:7.5px;top:0;bottom:0;width:1px;background:var(--kw-rail)}
19266
+ .knobs-wt-lane[data-down]::before{top:calc(var(--kw-sy) + 3.5px)}
19267
+ .knobs-wt-lane:is([data-rail="tee"],[data-rail="end"])::after{content:"";position:absolute;left:7.5px;top:0;width:13px;height:calc(var(--kw-sy) + .5px);border:solid var(--kw-rail);border-width:0 0 1px 1px;border-bottom-left-radius:6px}
19268
+ .knobs-wt-lane[data-rail="node"]::after{content:"";position:absolute;left:4.5px;top:calc(var(--kw-sy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--_k-fg-3)}
19269
+ .knobs-wt-srow[data-trunk] .knobs-wt-lane[data-rail="node"]::after{background:var(--_k-fg-2)}
19270
+ .knobs-wt-trunk{font:var(--_k-weight-label) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2)}
19271
+ .knobs-wt-smain{display:flex;flex-direction:column;gap:1px;min-width:0}
19272
+ .knobs-wt-sline{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 8px}
19273
+ .knobs-wt-sord{font:var(--_k-weight) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3);white-space:nowrap}
19274
+ .knobs-ui .knobs-wt-spr{font:var(--_k-weight-label) calc(var(--kw-t) - 1.5px)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2);text-decoration:none;white-space:nowrap}
19275
+ .knobs-ui a.knobs-wt-spr:is(:hover,:focus-visible){color:var(--_k-fg);text-decoration:underline}
19276
+ .knobs-ui .knobs-wt-stitle{font-weight:var(--_k-weight-label);line-height:var(--kw-line);color:var(--_k-fg);text-align:left}
19277
+ .knobs-ui button.knobs-wt-stitle{cursor:pointer;text-decoration:underline;text-decoration-color:transparent;text-underline-offset:3px;transition:text-decoration-color calc(var(--_k-hover-out) * 1ms)}
19278
+ .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)}
19279
+ .knobs-ui .knobs-wt-ssummary{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
19280
+ .knobs-wt-smeta{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px;font:var(--_k-weight) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3)}
19281
+ .knobs-wt-delta{display:inline-flex;gap:6px}
19282
+ .knobs-wt-add{color:var(--_k-diff-add)}
19283
+ .knobs-wt-del{color:var(--_k-diff-del)}
19284
+ .knobs-wt-sstatus{margin-top:calc((var(--kw-line) - 20px) / 2)}
19285
+ .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr) auto}
19286
+ .knobs-wt-stack[data-flat] .knobs-wt-srow+.knobs-wt-srow{box-shadow:inset 0 1px 0 var(--_k-border)}
19287
+ @container knobs-wt-slot (max-width:520px){
19288
+ .knobs-wt-srow:not([data-trunk]){grid-template-columns:auto minmax(0,1fr)}
19289
+ .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr)}
19290
+ .knobs-wt-sstatus{grid-column:-2;justify-self:start;margin-top:4px}
19291
+ }
19292
+
19293
+ /* A table: a real one, its head stuck while it scrolls, both ways; edges fade where there's more
19294
+ (the top one under the head), and thin bars show on hover. */
19295
+ @property --kw-fade-l{syntax:"<number>";inherits:false;initial-value:1}
19296
+ @property --kw-fade-r{syntax:"<number>";inherits:false;initial-value:1}
19297
+ @keyframes kw-fade-l{from{--kw-fade-l:1}to{--kw-fade-l:0}}
19298
+ @keyframes kw-fade-r{from{--kw-fade-r:0}to{--kw-fade-r:1}}
19299
+ .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
19300
+ transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
19301
+ .knobs-wt-table:is(:hover,:focus-visible){scrollbar-color:color-mix(in oklch,var(--_k-fg) 28%,transparent) transparent;transition-duration:calc(var(--_k-hover-in) * 1ms)}
19302
+ .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:calc(var(--_k-radius-panel) - 4px)}
19303
+ @supports (animation-timeline:scroll()){
19304
+ .knobs-wt-table{animation:knobs-fade-a linear both,knobs-fade-b linear both,kw-fade-l linear both,kw-fade-r linear both;animation-duration:auto;
19305
+ animation-timeline:scroll(self block),scroll(self block),scroll(self inline),scroll(self inline);animation-range:0 24px,calc(100% - 24px) 100%,0 8px,calc(100% - 8px) 100%;
19306
+ --kw-table-mask-y:linear-gradient(to bottom,#000 var(--kw-thead,0px),rgb(0 0 0/var(--knobs-fade-a)) var(--kw-thead,0px),#000 calc(var(--kw-thead,0px) + 20px),#000 calc(100% - 20px),rgb(0 0 0/var(--knobs-fade-b)));
19307
+ --kw-table-mask-x:linear-gradient(to right,rgb(0 0 0/var(--kw-fade-l)),#000 24px,#000 calc(100% - 24px),rgb(0 0 0/var(--kw-fade-r)));
19308
+ -webkit-mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);-webkit-mask-composite:source-in;mask-composite:intersect}
19309
+ }
19310
+ .knobs-wt-table table{width:100%;margin:0;border-collapse:separate;border-spacing:0;font-size:calc(var(--kw-t) - 1.5px);line-height:1.45;font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
19311
+ .knobs-wt-table caption{caption-side:bottom;padding-top:8px;text-align:left;color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 2px)}
19312
+ .knobs-wt-table th{position:sticky;top:0;padding:0 12px 7px;text-align:left;vertical-align:bottom;font-weight:var(--_k-weight-label);font-size:calc(var(--kw-t) - 2.5px);color:var(--_k-fg-3);white-space:nowrap;
19313
+ background:var(--_k-pane-bg);box-shadow:inset 0 -1px 0 var(--_k-border)}
19314
+ .knobs-wt-table td{padding:8px 12px;vertical-align:baseline;box-shadow:inset 0 -1px 0 var(--_k-border)}
19315
+ .knobs-wt-table tr:last-child td{box-shadow:none}
19316
+ .knobs-wt-table :is(th,td):first-child{padding-left:0}
19317
+ .knobs-wt-table :is(th,td):last-child{padding-right:0}
19318
+ .knobs-wt-table td:first-child{color:var(--_k-fg)}
19319
+ .knobs-wt-table [data-align="right"]{text-align:right}
19320
+ .knobs-wt-table [data-align="center"]{text-align:center}
19321
+ .knobs-wt-table td[data-mono]{font-family:var(--_k-mono);font-size:calc(var(--kw-t) - 2.5px);color:var(--_k-fg);white-space:nowrap}
19322
+ .knobs-wt-table td[data-long]{min-width:24ch}
19323
+ .knobs-wt-table td>.knobs-wt-steplink{margin-left:4px}
19324
+ .knobs-ui .knobs-wt-table a.knobs-wt-chip{text-decoration:none}
19325
+ .knobs-wt-ctone{color:var(--kw-tone)}
19326
+
19327
+ /* Chapters: timestamp pills under a video; one highlight springs to the one playing. */
19328
+ .knobs-wt-chapters{position:relative;display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}
19329
+ .knobs-wt-chapter-mark{position:absolute;left:0;top:0;width:0;border-radius:calc(var(--_k-radius) + 2px);background:var(--_k-fill-selected);opacity:0;pointer-events:none;will-change:transform;
19330
+ transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms)}
19331
+ .knobs-wt-chapter-mark[data-on]{opacity:1}
19332
+ .knobs-ui .knobs-wt-chapter{position:relative;display:inline-flex;align-items:baseline;gap:6px;height:26px;padding:0 10px 0 8px;border-radius:calc(var(--_k-radius) + 2px);color:var(--_k-fg-2);
19333
+ font-size:calc(var(--kw-t) - 2px);line-height:26px;white-space:nowrap;cursor:pointer;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
19334
+ .knobs-ui .knobs-wt-chapter:hover{background:var(--_ku-wash);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
19335
+ .knobs-ui .knobs-wt-chapter[aria-current]{background:transparent;color:var(--_k-fg)}
19336
+ .knobs-ui .knobs-wt-chapter:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus)}
19337
+ .knobs-wt-chapter-time{font:var(--_k-weight) calc(var(--kw-t) - 3px)/1 var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3)}
19338
+ .knobs-wt-chapter[aria-current] .knobs-wt-chapter-time{color:var(--_k-fg-2)}
19210
19339
  @media(prefers-reduced-motion:reduce){:is(.knobs-walkthrough,.knobs-wt-slot) :where(*,::before,::after){transition-duration:.15s!important;transition-property:opacity,background,color!important}}
19211
19340
  `;
19212
19341
 
19342
+ // src/ui/walkthrough-blocks.ts
19343
+ init_motion();
19344
+ init_look();
19345
+
19213
19346
  // src/ui/walkthrough-model.ts
19214
19347
  var DEFAULT_VERDICTS = ["ship", "change", "unsure"];
19215
19348
  function slug(text) {
@@ -19251,20 +19384,207 @@ function fillMedia(m, base) {
19251
19384
  if (typeof m === "string")
19252
19385
  return mediaOf(resolveSrc(m, base));
19253
19386
  switch (m.kind) {
19254
- case "compare":
19255
- return { ...m, before: source2(m.before, base), after: source2(m.after, base) };
19256
- case "video":
19387
+ case "compare": {
19388
+ const { chapters, ...rest } = m;
19257
19389
  return {
19258
- ...m,
19390
+ ...rest,
19391
+ before: source2(m.before, base),
19392
+ after: source2(m.after, base),
19393
+ ...chapters?.length ? { chapters: fillChapters(chapters) } : {}
19394
+ };
19395
+ }
19396
+ case "video": {
19397
+ const { chapters, captionsSrc, captions, ...rest } = m;
19398
+ const vtt = captions ?? captionsSrc;
19399
+ return {
19400
+ ...rest,
19259
19401
  src: resolveSrc(m.src, base),
19260
- ...m.poster ? { poster: resolveSrc(m.poster, base) } : {}
19402
+ ...m.poster ? { poster: resolveSrc(m.poster, base) } : {},
19403
+ ...vtt ? { captions: resolveSrc(vtt, base) } : {},
19404
+ ...chapters?.length ? { chapters: fillChapters(chapters) } : {}
19261
19405
  };
19406
+ }
19262
19407
  case "image":
19263
19408
  return { ...m, src: resolveSrc(m.src, base) };
19409
+ case "history":
19410
+ return { kind: "history", groups: fillHistory(m) };
19411
+ case "stack":
19412
+ return {
19413
+ kind: "stack",
19414
+ ...m.base ? { base: m.base } : {},
19415
+ items: m.items.map(fillStackItem)
19416
+ };
19417
+ case "table":
19418
+ return fillTable(m);
19264
19419
  default:
19265
19420
  return m;
19266
19421
  }
19267
19422
  }
19423
+ function fillHistory(m) {
19424
+ const groups = [];
19425
+ if (m.events?.length)
19426
+ groups.push({ events: m.events });
19427
+ for (const g of m.groups ?? [])
19428
+ groups.push({
19429
+ ...g.heading ? { heading: g.heading } : {},
19430
+ ...g.summary ? { summary: g.summary } : {},
19431
+ events: g.events ?? g.rows ?? []
19432
+ });
19433
+ return groups;
19434
+ }
19435
+ function parseBranch(branch) {
19436
+ if (!branch?.trim())
19437
+ return {};
19438
+ const [head, base] = branch.split(/\s*(?:→|->|=>|⇒)\s*/, 2).map((s) => s.trim());
19439
+ return { ...head ? { head } : {}, ...base ? { base } : {} };
19440
+ }
19441
+ function parseDelta(delta) {
19442
+ if (delta === undefined || typeof delta === "object")
19443
+ return delta;
19444
+ const m = /\+\s*([\d,_]+)[^\d+]*?[-−–]\s*([\d,_]+)/.exec(delta);
19445
+ if (!m)
19446
+ return delta.trim() || undefined;
19447
+ const n = (s) => Number(s.replace(/[,_]/g, ""));
19448
+ return { add: n(m[1]), del: n(m[2]) };
19449
+ }
19450
+ function fillStackItem(it) {
19451
+ const parsed = parseBranch(it.branch);
19452
+ const head = parsed.head ?? it.head;
19453
+ const base = parsed.base ?? it.base;
19454
+ const delta = parseDelta(it.delta);
19455
+ const step = it.step ?? it.stepId;
19456
+ const pr = String(it.pr).trim();
19457
+ return {
19458
+ pr: /^\d+$/.test(pr) ? `#${pr}` : pr,
19459
+ ...it.href ? { href: it.href } : {},
19460
+ title: it.title,
19461
+ ...head ? { head } : {},
19462
+ ...base ? { base } : {},
19463
+ ...delta !== undefined ? { delta } : {},
19464
+ ...it.status ? { status: it.status } : {},
19465
+ ...it.tone ? { tone: it.tone } : {},
19466
+ ...it.summary ? { summary: it.summary } : {},
19467
+ ...step ? { step } : {},
19468
+ ...it.order !== undefined && it.order !== "" ? { order: String(it.order) } : {}
19469
+ };
19470
+ }
19471
+ function stackLayout(m) {
19472
+ const items = m.items;
19473
+ const byHead = new Map;
19474
+ items.forEach((it, i) => it.head && !byHead.has(it.head) && byHead.set(it.head, i));
19475
+ let trunk = m.base ?? null;
19476
+ if (!trunk) {
19477
+ const tally = new Map;
19478
+ for (const it of items)
19479
+ if (it.base && !byHead.has(it.base))
19480
+ tally.set(it.base, (tally.get(it.base) ?? 0) + 1);
19481
+ let best = 0;
19482
+ for (const [b, n] of tally)
19483
+ if (n > best)
19484
+ [trunk, best] = [b, n];
19485
+ }
19486
+ const parentOf = items.map((it, i) => {
19487
+ const p = it.base === undefined ? undefined : byHead.get(it.base);
19488
+ return p === undefined || p === i ? null : p;
19489
+ });
19490
+ items.forEach((_, i) => {
19491
+ const seen = new Set;
19492
+ for (let n = i;n !== null; n = parentOf[n]) {
19493
+ if (seen.has(n)) {
19494
+ const loop = [n];
19495
+ for (let k = parentOf[n];k !== n && k !== null; k = parentOf[k])
19496
+ loop.push(k);
19497
+ parentOf[Math.min(...loop)] = null;
19498
+ break;
19499
+ }
19500
+ seen.add(n);
19501
+ }
19502
+ });
19503
+ const kids = items.map(() => []);
19504
+ const roots = [];
19505
+ parentOf.forEach((p, i) => (p === null ? roots : kids[p]).push(i));
19506
+ const rows = [];
19507
+ const walk = (list, depth, rails) => list.forEach((i, n) => {
19508
+ const it = items[i];
19509
+ const last = n === list.length - 1;
19510
+ const off = parentOf[i] === null && it.base && it.base !== trunk ? it.base : undefined;
19511
+ rows.push({
19512
+ item: it,
19513
+ index: i,
19514
+ depth,
19515
+ parent: parentOf[i],
19516
+ last,
19517
+ rails,
19518
+ children: kids[i].length,
19519
+ ...off ? { offBase: off } : {}
19520
+ });
19521
+ walk(kids[i], depth + 1, [...rails, !last]);
19522
+ });
19523
+ walk(roots, 0, []);
19524
+ return { trunk, rows, flat: !trunk && rows.every((r) => r.depth === 0 && !r.children) };
19525
+ }
19526
+ var isNum = (c) => typeof c === "number" || typeof c === "object" && typeof c.text === "number";
19527
+ function fillCell(c) {
19528
+ if (typeof c !== "object")
19529
+ return { text: String(c) };
19530
+ return { ...c, text: String(c.text) };
19531
+ }
19532
+ function fillTable(m) {
19533
+ const wrapped = !m.columns.some((c) => c.key === "cells");
19534
+ const raw = m.rows.map((r) => {
19535
+ const keys = Object.keys(r);
19536
+ const cells = r.cells;
19537
+ return wrapped && keys.length === 1 && keys[0] === "cells" && cells && typeof cells === "object" ? cells : r;
19538
+ });
19539
+ const columns = m.columns.map((c) => {
19540
+ const present = raw.map((r) => r[c.key]).filter((v) => v !== undefined && v !== "");
19541
+ const numeric = present.length > 0 && present.every(isNum);
19542
+ return { ...c, align: c.align ?? (numeric ? "right" : "left") };
19543
+ });
19544
+ const rows = raw.map((r) => {
19545
+ const out = {};
19546
+ for (const [k, v] of Object.entries(r))
19547
+ if (v !== undefined && v !== null)
19548
+ out[k] = fillCell(v);
19549
+ return out;
19550
+ });
19551
+ return { kind: "table", columns, rows, ...m.caption ? { caption: m.caption } : {} };
19552
+ }
19553
+ function fillChapters(chapters) {
19554
+ const id = uniqueIds();
19555
+ const starts = chapters.map((c) => c.start).sort((a, b) => a - b);
19556
+ return chapters.map((c) => {
19557
+ const next = starts.find((s) => s > c.start);
19558
+ const end = c.end ?? (c.duration !== undefined ? Math.round((c.start + c.duration) * 1000) / 1000 : next);
19559
+ return {
19560
+ id: id(c.id ?? slug(c.label)),
19561
+ label: c.label,
19562
+ start: c.start,
19563
+ ...end !== undefined && end > c.start ? { end } : {}
19564
+ };
19565
+ });
19566
+ }
19567
+ function chapterAt(chapters, time) {
19568
+ let at = -1;
19569
+ chapters.forEach((c, i) => {
19570
+ if (time >= c.start && time < (c.end ?? Infinity) && (at < 0 || c.start >= chapters[at].start))
19571
+ at = i;
19572
+ });
19573
+ return at;
19574
+ }
19575
+ function clock(seconds) {
19576
+ const t = Math.max(0, Math.floor(seconds));
19577
+ const h = Math.floor(t / 3600);
19578
+ const m = Math.floor(t % 3600 / 60);
19579
+ const s = String(t % 60).padStart(2, "0");
19580
+ return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
19581
+ }
19582
+ function findStep(steps, ref) {
19583
+ if (ref === undefined)
19584
+ return;
19585
+ const r = ref.trim().replace(/^#/, "");
19586
+ return steps.find((s) => s.key === r || s.id === r || s.number === r);
19587
+ }
19268
19588
  function linkLabel(href) {
19269
19589
  const pr = /\/(?:pull|issues)\/(\d+)/.exec(href);
19270
19590
  if (pr)
@@ -19321,7 +19641,7 @@ function normalizeWalkthrough(doc) {
19321
19641
  label: k,
19322
19642
  value
19323
19643
  }));
19324
- const stepRef = (ref) => ref === undefined ? null : steps.find((s) => s.key === ref || s.id === ref || s.number === ref)?.key ?? null;
19644
+ const stepRef = (ref) => findStep(steps, ref)?.key ?? null;
19325
19645
  const feed = sortFeed([
19326
19646
  ...(doc.updates ?? []).map((u) => ({ kind: "update", at: parseAt(u.at), text: u.text, step: stepRef(u.step) })),
19327
19647
  ...(doc.qa ?? []).map((x) => ({
@@ -19488,6 +19808,8 @@ function describeMedia(m) {
19488
19808
  parts.push(`${m.kind} at ${formatTime(m.time)}`);
19489
19809
  else
19490
19810
  parts.push(m.kind);
19811
+ if (m.chapter)
19812
+ parts[0] += ` in “${m.chapter}”`;
19491
19813
  if (m.loop)
19492
19814
  parts.push(`loop ${formatTime(m.loop.start)}–${formatTime(m.loop.end)}`);
19493
19815
  if (m.zoom)
@@ -19647,6 +19969,388 @@ function inlineMarkdown(text) {
19647
19969
  return s.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${escapeHtml(codes[Number(i)])}</code>`);
19648
19970
  }
19649
19971
 
19972
+ // src/ui/walkthrough-blocks.ts
19973
+ var el = (doc, tag, cls) => {
19974
+ const n = doc.createElement(tag);
19975
+ if (cls)
19976
+ n.className = cls;
19977
+ return n;
19978
+ };
19979
+ var text = (doc, tag, cls, value) => {
19980
+ const n = el(doc, tag, cls);
19981
+ n.textContent = value;
19982
+ return n;
19983
+ };
19984
+ var prose = (doc, tag, cls, value) => {
19985
+ const n = el(doc, tag, cls);
19986
+ n.innerHTML = inlineMarkdown(value);
19987
+ return n;
19988
+ };
19989
+ var link = (doc, cls, label, href) => {
19990
+ if (!href)
19991
+ return text(doc, "span", cls, label);
19992
+ const a = text(doc, "a", cls, label);
19993
+ a.href = href;
19994
+ a.target = "_blank";
19995
+ a.rel = "noreferrer";
19996
+ return a;
19997
+ };
19998
+ function stepButton(doc, ref, steps) {
19999
+ const s = ref && steps?.find(ref);
20000
+ if (!s || !steps)
20001
+ return null;
20002
+ const b = text(doc, "button", "knobs-wt-steplink", s.number);
20003
+ b.type = "button";
20004
+ b.title = `Go to ${s.number} ${s.title}`;
20005
+ b.setAttribute("aria-label", b.title);
20006
+ b.addEventListener("click", (e) => {
20007
+ e.stopPropagation();
20008
+ steps.go(s.key);
20009
+ });
20010
+ return b;
20011
+ }
20012
+ var tags = (doc, list) => {
20013
+ if (!list?.length)
20014
+ return null;
20015
+ const row = el(doc, "div", "knobs-wt-tags");
20016
+ for (const e of list)
20017
+ row.append(link(doc, "knobs-wt-tag", e.label, e.href));
20018
+ return row;
20019
+ };
20020
+ function mountHistory(host, m, steps) {
20021
+ const doc = host.ownerDocument;
20022
+ const root = el(doc, "div", "knobs-wt-history");
20023
+ const longest = Math.max(6, ...m.groups.flatMap((g) => g.events.flatMap((e) => [e.date.length, e.age?.length ?? 0])));
20024
+ root.style.setProperty("--kw-hw", `${Math.min(16, longest + 1)}ch`);
20025
+ for (const g of m.groups) {
20026
+ const group = el(doc, "section", "knobs-wt-hgroup");
20027
+ if (g.heading || g.summary) {
20028
+ const head = el(doc, "header", "knobs-wt-hhead");
20029
+ if (g.heading)
20030
+ head.append(text(doc, "h4", "knobs-wt-hheading", g.heading));
20031
+ if (g.summary)
20032
+ head.append(text(doc, "span", "knobs-wt-hsum", g.summary));
20033
+ group.append(head);
20034
+ }
20035
+ const list = el(doc, "ol", "knobs-wt-hlist");
20036
+ for (const e of g.events)
20037
+ list.append(historyEvent(doc, e, steps));
20038
+ group.append(list);
20039
+ root.append(group);
20040
+ }
20041
+ host.append(root);
20042
+ return { dispose: () => root.remove(), context: () => ({ kind: "history" }) };
20043
+ }
20044
+ function historyEvent(doc, e, steps) {
20045
+ const li = el(doc, "li", "knobs-wt-hev");
20046
+ if (e.tone)
20047
+ li.dataset.tone = e.tone;
20048
+ if (e.type)
20049
+ li.dataset.type = e.type;
20050
+ if (e.provenance)
20051
+ li.dataset.provenance = e.provenance;
20052
+ const when = el(doc, "div", "knobs-wt-hwhen");
20053
+ when.append(text(doc, "time", "knobs-wt-hdate", e.date));
20054
+ if (e.age)
20055
+ when.append(text(doc, "span", "knobs-wt-hage", e.age));
20056
+ const spine = el(doc, "span", "knobs-wt-hspine");
20057
+ spine.setAttribute("aria-hidden", "true");
20058
+ spine.append(el(doc, "span", "knobs-wt-hdot"));
20059
+ const main = el(doc, "div", "knobs-wt-hmain");
20060
+ const title = el(doc, "div", "knobs-wt-htitle");
20061
+ title.append(text(doc, "span", "knobs-wt-htext", e.title));
20062
+ if (e.provenance || e.source) {
20063
+ const p = text(doc, "span", "knobs-wt-prov", [e.provenance, e.source].filter(Boolean).join(" · "));
20064
+ if (e.provenance === "inferred")
20065
+ p.title = "Inferred from behaviour, not seen in the record";
20066
+ else if (e.provenance === "verified")
20067
+ p.title = "Seen in the record";
20068
+ title.append(p);
20069
+ }
20070
+ const go = stepButton(doc, e.step, steps);
20071
+ if (go)
20072
+ title.append(go);
20073
+ main.append(title);
20074
+ if (e.detail)
20075
+ main.append(prose(doc, "p", "knobs-wt-hdetail", e.detail));
20076
+ const t = tags(doc, e.evidence);
20077
+ if (t)
20078
+ main.append(t);
20079
+ li.append(when, spine, main);
20080
+ return li;
20081
+ }
20082
+ function mountStack(host, m, steps) {
20083
+ const doc = host.ownerDocument;
20084
+ const layout = stackLayout(m);
20085
+ const root = el(doc, "div", "knobs-wt-stack");
20086
+ root.toggleAttribute("data-flat", layout.flat);
20087
+ const lane = (kind) => {
20088
+ const s = el(doc, "span", "knobs-wt-lane");
20089
+ s.dataset.rail = kind;
20090
+ return s;
20091
+ };
20092
+ const node = (down) => {
20093
+ const s = lane("node");
20094
+ s.toggleAttribute("data-down", down);
20095
+ return s;
20096
+ };
20097
+ const from = layout.trunk ? 0 : 1;
20098
+ if (layout.trunk && layout.rows.length) {
20099
+ const row = el(doc, "div", "knobs-wt-srow");
20100
+ row.dataset.trunk = "";
20101
+ const lanes = el(doc, "span", "knobs-wt-lanes");
20102
+ lanes.setAttribute("aria-hidden", "true");
20103
+ lanes.append(node(true));
20104
+ row.append(lanes, text(doc, "span", "knobs-wt-trunk", layout.trunk));
20105
+ root.append(row);
20106
+ }
20107
+ for (const r of layout.rows) {
20108
+ const it = r.item;
20109
+ const row = el(doc, "div", "knobs-wt-srow");
20110
+ if (!layout.flat) {
20111
+ const lanes = el(doc, "span", "knobs-wt-lanes");
20112
+ lanes.setAttribute("aria-hidden", "true");
20113
+ for (let k = from;k < r.depth; k++)
20114
+ lanes.append(lane(r.rails[k] ? "pass" : "none"));
20115
+ if (r.depth >= from)
20116
+ lanes.append(lane(r.last ? "end" : "tee"));
20117
+ lanes.append(node(r.children > 0));
20118
+ row.append(lanes);
20119
+ }
20120
+ const main = el(doc, "div", "knobs-wt-smain");
20121
+ const line = el(doc, "div", "knobs-wt-sline");
20122
+ if (it.order)
20123
+ line.append(text(doc, "span", "knobs-wt-sord", it.order));
20124
+ line.append(link(doc, "knobs-wt-spr", it.pr, it.href));
20125
+ const target = it.step ? steps?.find(it.step) : null;
20126
+ if (target && steps) {
20127
+ const title = text(doc, "button", "knobs-wt-stitle", it.title);
20128
+ title.type = "button";
20129
+ title.title = `Go to ${target.number} ${target.title}`;
20130
+ title.addEventListener("click", (e) => {
20131
+ e.stopPropagation();
20132
+ steps.go(target.key);
20133
+ });
20134
+ line.append(title);
20135
+ const go = stepButton(doc, it.step, steps);
20136
+ if (go)
20137
+ line.append(go);
20138
+ row.dataset.go = "";
20139
+ row.addEventListener("click", (e) => {
20140
+ const t = e.target;
20141
+ if (t?.closest("a,button"))
20142
+ return;
20143
+ if (doc.getSelection()?.toString())
20144
+ return;
20145
+ steps.go(target.key);
20146
+ });
20147
+ } else
20148
+ line.append(text(doc, "span", "knobs-wt-stitle", it.title));
20149
+ main.append(line);
20150
+ if (it.summary)
20151
+ main.append(prose(doc, "p", "knobs-wt-ssummary", it.summary));
20152
+ const meta = el(doc, "div", "knobs-wt-smeta");
20153
+ if (it.head) {
20154
+ const branch = r.offBase ? `${it.head} → ${r.offBase}` : it.head;
20155
+ meta.append(text(doc, "span", "knobs-wt-sbranch", branch));
20156
+ }
20157
+ if (it.delta !== undefined)
20158
+ meta.append(delta(doc, it.delta));
20159
+ if (meta.childNodes.length)
20160
+ main.append(meta);
20161
+ row.append(main);
20162
+ if (it.status) {
20163
+ const chip = text(doc, "span", "knobs-wt-chip knobs-wt-sstatus", it.status);
20164
+ chip.dataset.tone = it.tone ?? "dim";
20165
+ row.append(chip);
20166
+ }
20167
+ root.append(row);
20168
+ }
20169
+ host.append(root);
20170
+ return { dispose: () => root.remove(), context: () => ({ kind: "stack" }) };
20171
+ }
20172
+ function delta(doc, d) {
20173
+ const s = el(doc, "span", "knobs-wt-delta");
20174
+ if (typeof d === "string") {
20175
+ s.textContent = d;
20176
+ return s;
20177
+ }
20178
+ const n = (v) => v.toLocaleString("en-US");
20179
+ s.append(text(doc, "span", "knobs-wt-add", `+${n(d.add)}`));
20180
+ s.append(text(doc, "span", "knobs-wt-del", `−${n(d.del)}`));
20181
+ s.setAttribute("aria-label", `${d.add} added, ${d.del} removed`);
20182
+ return s;
20183
+ }
20184
+ function mountTable(host, m, steps) {
20185
+ const doc = host.ownerDocument;
20186
+ const win = doc.defaultView;
20187
+ const scroller = el(doc, "div", "knobs-wt-table");
20188
+ const table = el(doc, "table");
20189
+ if (m.caption)
20190
+ table.append(text(doc, "caption", "", m.caption));
20191
+ const head = el(doc, "thead");
20192
+ const hr = el(doc, "tr");
20193
+ for (const c of m.columns) {
20194
+ const th = text(doc, "th", "", c.label);
20195
+ th.scope = "col";
20196
+ th.dataset.align = c.align;
20197
+ hr.append(th);
20198
+ }
20199
+ head.append(hr);
20200
+ const body = el(doc, "tbody");
20201
+ for (const r of m.rows) {
20202
+ const tr = el(doc, "tr");
20203
+ for (const c of m.columns) {
20204
+ const td = el(doc, "td");
20205
+ td.dataset.align = c.align;
20206
+ if (c.mono)
20207
+ td.dataset.mono = "";
20208
+ const cell = r[c.key];
20209
+ if (cell) {
20210
+ if (cell.text.length > 40 && !c.mono && !cell.code)
20211
+ td.dataset.long = "";
20212
+ let content;
20213
+ if (cell.badge) {
20214
+ content = link(doc, "knobs-wt-chip", cell.text, cell.href);
20215
+ content.dataset.tone = cell.badge;
20216
+ } else {
20217
+ const inner = cell.code ? text(doc, "code", "", cell.text) : null;
20218
+ if (cell.href) {
20219
+ content = link(doc, "knobs-wt-clink", inner ? "" : cell.text, cell.href);
20220
+ if (inner)
20221
+ content.append(inner);
20222
+ } else
20223
+ content = inner ?? text(doc, "span", "", cell.text);
20224
+ if (cell.tone) {
20225
+ content.classList.add("knobs-wt-ctone");
20226
+ content.dataset.tone = cell.tone;
20227
+ }
20228
+ }
20229
+ td.append(content);
20230
+ const go = stepButton(doc, cell.step, steps);
20231
+ if (go)
20232
+ td.append(go);
20233
+ }
20234
+ tr.append(td);
20235
+ }
20236
+ body.append(tr);
20237
+ }
20238
+ table.append(head, body);
20239
+ scroller.append(table);
20240
+ host.append(scroller);
20241
+ const measure = () => {
20242
+ scroller.style.setProperty("--kw-thead", `${head.offsetHeight}px`);
20243
+ const scrolls = scroller.scrollWidth > scroller.clientWidth + 1 || scroller.scrollHeight > scroller.clientHeight + 1;
20244
+ if (scrolls) {
20245
+ scroller.tabIndex = 0;
20246
+ scroller.setAttribute("role", "region");
20247
+ scroller.setAttribute("aria-label", m.caption ?? "Table");
20248
+ } else {
20249
+ scroller.removeAttribute("tabindex");
20250
+ scroller.removeAttribute("role");
20251
+ scroller.removeAttribute("aria-label");
20252
+ }
20253
+ };
20254
+ const ro = win && "ResizeObserver" in win ? new win.ResizeObserver(measure) : null;
20255
+ ro?.observe(scroller);
20256
+ measure();
20257
+ return {
20258
+ dispose: () => {
20259
+ ro?.disconnect();
20260
+ scroller.remove();
20261
+ },
20262
+ context: () => ({ kind: "table" })
20263
+ };
20264
+ }
20265
+ var grid = (v) => Math.round(v * 64) / 64;
20266
+ function chapterPills(host, chapters, io) {
20267
+ const doc = host.ownerDocument;
20268
+ const win = doc.defaultView;
20269
+ const bar = el(doc, "div", "knobs-wt-chapters");
20270
+ bar.setAttribute("role", "group");
20271
+ bar.setAttribute("aria-label", "Chapters");
20272
+ const mark = el(doc, "span", "knobs-wt-chapter-mark");
20273
+ mark.setAttribute("aria-hidden", "true");
20274
+ bar.append(mark);
20275
+ const pills = chapters.map((c) => {
20276
+ const b = el(doc, "button", "knobs-wt-chapter");
20277
+ b.type = "button";
20278
+ b.append(text(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
20279
+ b.append(text(doc, "span", "knobs-wt-chapter-label", c.label));
20280
+ b.addEventListener("click", () => {
20281
+ io.onEvent?.({ what: "seek", time: Math.round(c.start * 100) / 100, detail: c.label });
20282
+ io.seek(c.start);
20283
+ sync();
20284
+ });
20285
+ bar.append(b);
20286
+ return b;
20287
+ });
20288
+ host.append(bar);
20289
+ const x = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
20290
+ const y = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
20291
+ const w = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
20292
+ const draw = () => {
20293
+ const left = grid(x.x);
20294
+ mark.style.transform = `translate(${left}px,${grid(y.x)}px)`;
20295
+ mark.style.width = `${grid(x.x + w.x) - left}px`;
20296
+ };
20297
+ const loop = frameLoop(win, (dt) => {
20298
+ x.step(dt);
20299
+ y.step(dt);
20300
+ w.step(dt);
20301
+ draw();
20302
+ return !x.resting || !y.resting || !w.resting;
20303
+ });
20304
+ let at = -2;
20305
+ let shown = false;
20306
+ const place = (snap) => {
20307
+ const p = pills[at];
20308
+ if (!p) {
20309
+ mark.removeAttribute("data-on");
20310
+ pills.forEach((b) => b.removeAttribute("aria-current"));
20311
+ shown = false;
20312
+ return;
20313
+ }
20314
+ pills.forEach((b, i) => i === at ? b.setAttribute("aria-current", "true") : b.removeAttribute("aria-current"));
20315
+ const to = [p.offsetLeft, p.offsetTop, p.offsetWidth];
20316
+ if (snap || !shown || reducedMotion(win)) {
20317
+ x.snap(to[0]);
20318
+ y.snap(to[1]);
20319
+ w.snap(to[2]);
20320
+ draw();
20321
+ } else {
20322
+ x.set(to[0]);
20323
+ y.set(to[1]);
20324
+ w.set(to[2]);
20325
+ loop.wake();
20326
+ }
20327
+ mark.style.height = `${p.offsetHeight}px`;
20328
+ mark.dataset.on = "";
20329
+ shown = true;
20330
+ };
20331
+ const sync = () => {
20332
+ const next = chapterAt(chapters, io.now());
20333
+ if (next === at)
20334
+ return;
20335
+ at = next;
20336
+ place(false);
20337
+ };
20338
+ sync();
20339
+ const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => place(true)) : null;
20340
+ ro?.observe(bar);
20341
+ return {
20342
+ sync,
20343
+ get current() {
20344
+ return chapters[at] ?? null;
20345
+ },
20346
+ dispose() {
20347
+ loop.stop();
20348
+ ro?.disconnect();
20349
+ bar.remove();
20350
+ }
20351
+ };
20352
+ }
20353
+
19650
20354
  // src/ui/walkthrough-media.ts
19651
20355
  function injectWalkthroughStyles(doc) {
19652
20356
  if (doc.querySelector("style[data-knobs-ui-walkthrough]"))
@@ -19779,8 +20483,25 @@ async function mountCompare(host, m, look, alive, onEvent) {
19779
20483
  onEvent?.({ what: "loop", detail: lastLoop = loopKey });
19780
20484
  }
19781
20485
  });
20486
+ const chapters = m.chapters?.length ? m.chapters : null;
20487
+ if (chapters)
20488
+ c.timeline?.update({
20489
+ markers: chapters.map((ch) => ({ id: ch.id, time: ch.start, label: ch.label }))
20490
+ });
20491
+ const pills = chapters ? chapterPills(host, chapters, {
20492
+ now: () => c.time,
20493
+ seek: (t) => {
20494
+ c.seek(t);
20495
+ c.play();
20496
+ },
20497
+ onEvent
20498
+ }) : null;
19782
20499
  const unlisten = c.timeline?.listen({
19783
- onSeek: (time, source) => source !== "play" && onEvent?.({ what: "seek", time: round2(time) }),
20500
+ onSeek: (time, source) => {
20501
+ pills?.sync();
20502
+ if (source !== "play")
20503
+ onEvent?.({ what: "seek", time: round2(time) });
20504
+ },
19784
20505
  onPlay: (on) => onEvent?.({ what: on ? "play" : "pause", time: round2(c.time) })
19785
20506
  });
19786
20507
  requestAnimationFrame(() => requestAnimationFrame(() => fit.dataset.settled = ""));
@@ -19802,12 +20523,14 @@ async function mountCompare(host, m, look, alive, onEvent) {
19802
20523
  duration: round2(c.duration),
19803
20524
  playing: c.playing,
19804
20525
  mode: c.mode,
20526
+ ...pills?.current ? { chapter: pills.current.label } : {},
19805
20527
  ...loop ? { loop: { start: round2(loop.start), end: round2(loop.end) } } : {},
19806
20528
  ...zoom ? { zoom: { u: round2(zoom.u), v: round2(zoom.v), w: round2(zoom.w), h: round2(zoom.h) } } : {}
19807
20529
  };
19808
20530
  },
19809
20531
  dispose: () => {
19810
20532
  unlisten?.();
20533
+ pills?.dispose();
19811
20534
  c.dispose();
19812
20535
  fit.remove();
19813
20536
  }
@@ -19831,6 +20554,15 @@ function mountVideo(host, m, onEvent) {
19831
20554
  v.poster = m.poster;
19832
20555
  if (!aspect)
19833
20556
  v.addEventListener("loadedmetadata", () => v.videoWidth && frame.style.setProperty("--kw-aspect", String(v.videoWidth / v.videoHeight)), { once: true });
20557
+ if (m.captions) {
20558
+ const track = doc.createElement("track");
20559
+ track.kind = "captions";
20560
+ track.label = "Captions";
20561
+ track.srclang = doc.documentElement.lang || "en";
20562
+ track.default = true;
20563
+ track.src = m.captions;
20564
+ v.append(track);
20565
+ }
19834
20566
  v.src = m.src;
19835
20567
  v.addEventListener("play", () => onEvent?.({ what: "play", time: round2(v.currentTime) }));
19836
20568
  v.addEventListener("pause", () => onEvent?.({ what: "pause", time: round2(v.currentTime) }));
@@ -19840,6 +20572,18 @@ function mountVideo(host, m, onEvent) {
19840
20572
  });
19841
20573
  frame.append(v);
19842
20574
  host.append(frame);
20575
+ const pills = m.chapters?.length ? chapterPills(host, m.chapters, {
20576
+ now: () => v.currentTime,
20577
+ seek: (t) => {
20578
+ v.currentTime = t;
20579
+ v.play().catch(() => {});
20580
+ },
20581
+ onEvent
20582
+ }) : null;
20583
+ const sync = () => pills?.sync();
20584
+ if (pills)
20585
+ for (const type of ["timeupdate", "seeking", "seeked"])
20586
+ v.addEventListener(type, sync);
19843
20587
  return {
19844
20588
  pause: () => v.pause(),
19845
20589
  seekBy: (d) => {
@@ -19849,9 +20593,11 @@ function mountVideo(host, m, onEvent) {
19849
20593
  kind: "video",
19850
20594
  time: round2(v.currentTime),
19851
20595
  ...Number.isFinite(v.duration) ? { duration: round2(v.duration) } : {},
19852
- playing: !v.paused
20596
+ playing: !v.paused,
20597
+ ...pills?.current ? { chapter: pills.current.label } : {}
19853
20598
  }),
19854
20599
  dispose: () => {
20600
+ pills?.dispose();
19855
20601
  v.pause();
19856
20602
  v.removeAttribute("src");
19857
20603
  v.load();
@@ -20075,6 +20821,12 @@ function mediaSlot(container, options) {
20075
20821
  case "callout":
20076
20822
  case "text":
20077
20823
  return done(mountProse(host, m));
20824
+ case "history":
20825
+ return done(mountHistory(host, m, options.steps));
20826
+ case "stack":
20827
+ return done(mountStack(host, m, options.steps));
20828
+ case "table":
20829
+ return done(mountTable(host, m, options.steps));
20078
20830
  case "mount": {
20079
20831
  const cleanup = m.mount(host);
20080
20832
  return done({ dispose: () => cleanup ? cleanup() : host.textContent = "" });
@@ -20129,14 +20881,14 @@ function mediaSlot(container, options) {
20129
20881
  var CHECK3 = svg('<path d="m3.5 8.4 3 3 6-6.6"/>');
20130
20882
 
20131
20883
  // src/ui/walkthrough.ts
20132
- var el = (doc, tag, cls) => {
20884
+ var el2 = (doc, tag, cls) => {
20133
20885
  const n = doc.createElement(tag);
20134
20886
  if (cls)
20135
20887
  n.className = cls;
20136
20888
  return n;
20137
20889
  };
20138
- var prose = (doc, tag, cls, text) => {
20139
- const n = el(doc, tag, cls);
20890
+ var prose2 = (doc, tag, cls, text) => {
20891
+ const n = el2(doc, tag, cls);
20140
20892
  n.innerHTML = inlineMarkdown(text);
20141
20893
  return n;
20142
20894
  };
@@ -20144,7 +20896,7 @@ var lookOf2 = (o) => ({
20144
20896
  ...o.preset ? { preset: o.preset } : {},
20145
20897
  ...o.theme ? { theme: o.theme } : {}
20146
20898
  });
20147
- var grid = (v) => Math.round(v * 64) / 64;
20899
+ var grid2 = (v) => Math.round(v * 64) / 64;
20148
20900
  var ICON = {
20149
20901
  full: svg('<path d="M3 6.5V3h3.5M13 6.5V3H9.5M3 9.5V13h3.5M13 9.5V13H9.5"/>'),
20150
20902
  exit: svg('<path d="M6.5 3v3.5H3M9.5 3v3.5H13M6.5 13V9.5H3M9.5 13V9.5H13"/>'),
@@ -20170,7 +20922,7 @@ function tasteTest(container, options) {
20170
20922
  suggested: o.suggested === true
20171
20923
  });
20172
20924
  let picked = options.picked ?? null;
20173
- const root = el(doc, "div", "knobs-wt-options");
20925
+ const root = el2(doc, "div", "knobs-wt-options");
20174
20926
  root.setAttribute("role", "radiogroup");
20175
20927
  root.setAttribute("aria-label", "Options");
20176
20928
  root.style.setProperty("--kw-n", String(Math.min(list.length, 3)));
@@ -20178,28 +20930,28 @@ function tasteTest(container, options) {
20178
20930
  applyLook2(root, options);
20179
20931
  const slots = [];
20180
20932
  const tiles = list.map((o, i) => {
20181
- const tile = el(doc, "div", "knobs-wt-option");
20182
- const pick = el(doc, "button", "knobs-wt-pick");
20933
+ const tile = el2(doc, "div", "knobs-wt-option");
20934
+ const pick = el2(doc, "button", "knobs-wt-pick");
20183
20935
  pick.type = "button";
20184
20936
  pick.setAttribute("role", "radio");
20185
20937
  if (i < 9)
20186
20938
  pick.setAttribute("aria-keyshortcuts", String(i + 1));
20187
- const letter = el(doc, "span", "knobs-wt-letter");
20939
+ const letter = el2(doc, "span", "knobs-wt-letter");
20188
20940
  letter.textContent = o.letter;
20189
- const name = el(doc, "span", "knobs-wt-olabel");
20941
+ const name = el2(doc, "span", "knobs-wt-olabel");
20190
20942
  name.textContent = o.label;
20191
20943
  pick.append(letter, name);
20192
20944
  if (o.suggested) {
20193
- const s = el(doc, "span", "knobs-wt-suggested");
20945
+ const s = el2(doc, "span", "knobs-wt-suggested");
20194
20946
  s.textContent = "Suggested";
20195
20947
  pick.append(s);
20196
20948
  }
20197
20949
  if (i < 9) {
20198
- const key = el(doc, "span", "knobs-wt-key");
20950
+ const key = el2(doc, "span", "knobs-wt-key");
20199
20951
  key.textContent = String(i + 1);
20200
20952
  pick.append(key);
20201
20953
  }
20202
- const check = el(doc, "span", "knobs-wt-check");
20954
+ const check = el2(doc, "span", "knobs-wt-check");
20203
20955
  check.innerHTML = CHECK3;
20204
20956
  pick.append(check);
20205
20957
  pick.addEventListener("click", () => {
@@ -20212,10 +20964,11 @@ function tasteTest(container, options) {
20212
20964
  slots.push(mediaSlot(tile, {
20213
20965
  ...look,
20214
20966
  media: o.media,
20215
- ...options.lazy === false ? { lazy: false } : {}
20967
+ ...options.lazy === false ? { lazy: false } : {},
20968
+ ...options.steps ? { steps: options.steps } : {}
20216
20969
  }));
20217
20970
  if (o.caption)
20218
- tile.append(prose(doc, "p", "knobs-wt-ocap", o.caption));
20971
+ tile.append(prose2(doc, "p", "knobs-wt-ocap", o.caption));
20219
20972
  root.append(tile);
20220
20973
  return { tile, pick };
20221
20974
  });
@@ -20263,7 +21016,7 @@ function walkthrough(container, options) {
20263
21016
  injectWalkthroughStyles(doc);
20264
21017
  const look = lookOf2(options);
20265
21018
  const storage = options.storage === undefined ? browserStorage() : options.storage;
20266
- const root = el(doc, "div", "knobs-walkthrough");
21019
+ const root = el2(doc, "div", "knobs-walkthrough");
20267
21020
  applyLook2(root, options);
20268
21021
  container.append(root);
20269
21022
  let page = normalizeWalkthrough(options.doc);
@@ -20323,8 +21076,8 @@ function walkthrough(container, options) {
20323
21076
  if (mark) {
20324
21077
  pillY.step(dt);
20325
21078
  pillH.step(dt);
20326
- mark.style.transform = `translateY(${grid(pillY.x)}px)`;
20327
- mark.style.height = `${grid(pillY.x + pillH.x) - grid(pillY.x)}px`;
21079
+ mark.style.transform = `translateY(${grid2(pillY.x)}px)`;
21080
+ mark.style.height = `${grid2(pillY.x + pillH.x) - grid2(pillY.x)}px`;
20328
21081
  more ||= !pillY.resting || !pillH.resting;
20329
21082
  }
20330
21083
  if (scroll.to && scroll.el) {
@@ -20459,6 +21212,10 @@ function walkthrough(container, options) {
20459
21212
  scrollToCard(v);
20460
21213
  v.card.focus({ preventScroll: true });
20461
21214
  };
21215
+ const stepLinks = {
21216
+ find: (ref) => findStep(page.steps, ref) ?? null,
21217
+ go: (key) => go(key)
21218
+ };
20462
21219
  const slotsOf = (v) => v ? [...v.slots, ...v.taste?.slots ?? []] : [];
20463
21220
  const mediaContext = (v) => {
20464
21221
  const slots = slotsOf(v);
@@ -20473,17 +21230,17 @@ function walkthrough(container, options) {
20473
21230
  return true;
20474
21231
  return false;
20475
21232
  };
20476
- const scrim = el(doc, "div", "knobs-wt-scrim");
21233
+ const scrim = el2(doc, "div", "knobs-wt-scrim");
20477
21234
  scrim.addEventListener("click", () => setFull(false));
20478
21235
  const fullRect = () => ({ x: 12, y: 12, w: win.innerWidth - 24, h: win.innerHeight - 24 });
20479
21236
  const placeFull = (f) => {
20480
21237
  const a = f.hold.getBoundingClientRect();
20481
21238
  const b = fullRect();
20482
21239
  const k = reduced() ? Math.round(f.p.target) : f.p.x;
20483
- const left = grid(a.left + (b.x - a.left) * k);
20484
- const top = grid(a.top + (b.y - a.top) * k);
20485
- const right = grid(a.right + (b.x + b.w - a.right) * k);
20486
- const bottom = grid(a.bottom + (b.y + b.h - a.bottom) * k);
21240
+ const left = grid2(a.left + (b.x - a.left) * k);
21241
+ const top = grid2(a.top + (b.y - a.top) * k);
21242
+ const right = grid2(a.right + (b.x + b.w - a.right) * k);
21243
+ const bottom = grid2(a.bottom + (b.y + b.h - a.bottom) * k);
20487
21244
  const s = f.v.card.style;
20488
21245
  s.left = `${left}px`;
20489
21246
  s.top = `${top}px`;
@@ -20518,7 +21275,7 @@ function walkthrough(container, options) {
20518
21275
  }
20519
21276
  if (!full) {
20520
21277
  const r = v.card.getBoundingClientRect();
20521
- const hold = el(doc, "div", "knobs-wt-hold-card");
21278
+ const hold = el2(doc, "div", "knobs-wt-hold-card");
20522
21279
  hold.style.height = `${r.height}px`;
20523
21280
  v.card.before(hold);
20524
21281
  v.card.dataset.full = "";
@@ -20666,7 +21423,7 @@ function walkthrough(container, options) {
20666
21423
  toast("Couldn't reach the microphone", { tone: "warn", look, doc });
20667
21424
  }
20668
21425
  };
20669
- const backPill = el(doc, "button", "knobs-wt-back");
21426
+ const backPill = el2(doc, "button", "knobs-wt-back");
20670
21427
  backPill.type = "button";
20671
21428
  let backTo = null;
20672
21429
  let backTimer = 0;
@@ -20691,16 +21448,16 @@ function walkthrough(container, options) {
20691
21448
  backTimer = win.setTimeout(hideBack, BACK_MS);
20692
21449
  };
20693
21450
  const bar = (() => {
20694
- const dock = el(doc, "div", "knobs-wt-dock");
20695
- const box = el(doc, "div", "knobs-wt-notebar");
20696
- const on = el(doc, "div", "knobs-wt-on");
20697
- const row = el(doc, "div", "knobs-wt-row");
20698
- const input = el(doc, "textarea", "knobs-wt-input");
21451
+ const dock = el2(doc, "div", "knobs-wt-dock");
21452
+ const box = el2(doc, "div", "knobs-wt-notebar");
21453
+ const on = el2(doc, "div", "knobs-wt-on");
21454
+ const row = el2(doc, "div", "knobs-wt-row");
21455
+ const input = el2(doc, "textarea", "knobs-wt-input");
20699
21456
  input.rows = 1;
20700
21457
  input.setAttribute("aria-label", "Note");
20701
- const attach = el(doc, "div", "knobs-wt-attach");
21458
+ const attach = el2(doc, "div", "knobs-wt-attach");
20702
21459
  const btn = (icon, label, run) => {
20703
- const b = el(doc, "button", "knobs-wt-tool");
21460
+ const b = el2(doc, "button", "knobs-wt-tool");
20704
21461
  b.type = "button";
20705
21462
  b.innerHTML = icon;
20706
21463
  b.setAttribute("aria-label", label);
@@ -20711,7 +21468,7 @@ function walkthrough(container, options) {
20711
21468
  const fullBtn = btn(ICON.full, "Full window · f", () => setFull(!full));
20712
21469
  const penBtn = btn(ICON.pen, "Draw on the media", () => setInk(!ink.on));
20713
21470
  const micBtn = btn(ICON.mic, "Record a voice note", () => void toggleVoice());
20714
- const micTime = el(doc, "span", "knobs-wt-rec");
21471
+ const micTime = el2(doc, "span", "knobs-wt-rec");
20715
21472
  micBtn.append(micTime);
20716
21473
  const sendBtn = btn(ICON.send, "Send · Enter", () => send());
20717
21474
  sendBtn.dataset.send = "";
@@ -20737,9 +21494,9 @@ function walkthrough(container, options) {
20737
21494
  }
20738
21495
  });
20739
21496
  const chip = (text, clear) => {
20740
- const c = el(doc, "span", "knobs-wt-chip-a");
21497
+ const c = el2(doc, "span", "knobs-wt-chip-a");
20741
21498
  c.append(text);
20742
- const x = el(doc, "button", "knobs-wt-x");
21499
+ const x = el2(doc, "button", "knobs-wt-x");
20743
21500
  x.type = "button";
20744
21501
  x.innerHTML = ICON.close;
20745
21502
  x.setAttribute("aria-label", `Remove ${text}`);
@@ -20852,9 +21609,9 @@ function walkthrough(container, options) {
20852
21609
  }
20853
21610
  };
20854
21611
  const renderNote = (v, n, entering = false) => {
20855
- const row = el(doc, "div", "knobs-wt-n");
21612
+ const row = el2(doc, "div", "knobs-wt-n");
20856
21613
  row.dataset.id = n.id;
20857
- const meta = el(doc, "span", "knobs-wt-n-meta");
21614
+ const meta = el2(doc, "span", "knobs-wt-n-meta");
20858
21615
  const m = n.context.media;
20859
21616
  meta.textContent = m?.time !== undefined ? formatTime(m.time) : m?.file ? m.file.split("/").pop() : "·";
20860
21617
  if (m?.time !== undefined) {
@@ -20870,11 +21627,11 @@ function walkthrough(container, options) {
20870
21627
  });
20871
21628
  }
20872
21629
  row.append(meta);
20873
- const body = el(doc, "div", "knobs-wt-n-body");
21630
+ const body = el2(doc, "div", "knobs-wt-n-body");
20874
21631
  if (n.text)
20875
- body.append(prose(doc, "p", "knobs-wt-n-text", n.text));
21632
+ body.append(prose2(doc, "p", "knobs-wt-n-text", n.text));
20876
21633
  if (n.voice) {
20877
- const a = el(doc, "audio", "knobs-wt-n-voice");
21634
+ const a = el2(doc, "audio", "knobs-wt-n-voice");
20878
21635
  a.controls = true;
20879
21636
  a.preload = "none";
20880
21637
  a.src = n.voice.url;
@@ -20894,7 +21651,7 @@ function walkthrough(container, options) {
20894
21651
  body.append(thumb);
20895
21652
  }
20896
21653
  row.append(body);
20897
- const x = el(doc, "button", "knobs-wt-x");
21654
+ const x = el2(doc, "button", "knobs-wt-x");
20898
21655
  x.type = "button";
20899
21656
  x.innerHTML = ICON.close;
20900
21657
  x.setAttribute("aria-label", "Delete this note");
@@ -20948,22 +21705,22 @@ function walkthrough(container, options) {
20948
21705
  pillPlaced = false;
20949
21706
  root.textContent = "";
20950
21707
  root.removeAttribute("data-settled");
20951
- const layout = el(doc, "div", "knobs-wt-layout");
20952
- const main = el(doc, "div", "knobs-wt-main");
20953
- const head = el(doc, "header", "knobs-wt-head");
20954
- const h1 = el(doc, "h1", "knobs-wt-title");
21708
+ const layout = el2(doc, "div", "knobs-wt-layout");
21709
+ const main = el2(doc, "div", "knobs-wt-main");
21710
+ const head = el2(doc, "header", "knobs-wt-head");
21711
+ const h1 = el2(doc, "h1", "knobs-wt-title");
20955
21712
  h1.textContent = page.title;
20956
21713
  head.append(h1);
20957
21714
  if (page.summary)
20958
- head.append(prose(doc, "p", "knobs-wt-summary", page.summary));
21715
+ head.append(prose2(doc, "p", "knobs-wt-summary", page.summary));
20959
21716
  if (page.meta.length) {
20960
- const meta = el(doc, "div", "knobs-wt-meta");
21717
+ const meta = el2(doc, "div", "knobs-wt-meta");
20961
21718
  for (const m of page.meta) {
20962
- const s = el(doc, "span");
21719
+ const s = el2(doc, "span");
20963
21720
  s.append(m.label);
20964
- const b = el(doc, "b");
21721
+ const b = el2(doc, "b");
20965
21722
  if (m.href) {
20966
- const a = el(doc, "a");
21723
+ const a = el2(doc, "a");
20967
21724
  a.href = m.href;
20968
21725
  a.target = "_blank";
20969
21726
  a.rel = "noreferrer";
@@ -20978,27 +21735,27 @@ function walkthrough(container, options) {
20978
21735
  }
20979
21736
  main.append(head);
20980
21737
  if (page.feed.length) {
20981
- const feed = el(doc, "section", "knobs-wt-feed");
21738
+ const feed = el2(doc, "section", "knobs-wt-feed");
20982
21739
  feed.setAttribute("aria-label", "Latest");
20983
- const list = el(doc, "ol", "knobs-wt-feed-list knobs-scroll-fade");
21740
+ const list = el2(doc, "ol", "knobs-wt-feed-list knobs-scroll-fade");
20984
21741
  const now = Date.now();
20985
21742
  for (const f of page.feed) {
20986
- const li = el(doc, "li", "knobs-wt-fi");
21743
+ const li = el2(doc, "li", "knobs-wt-fi");
20987
21744
  li.dataset.kind = f.kind;
20988
- const when = el(doc, "span", "knobs-wt-fi-when");
21745
+ const when = el2(doc, "span", "knobs-wt-fi-when");
20989
21746
  when.textContent = f.at === null ? "" : ago(f.at, now);
20990
21747
  if (f.at !== null)
20991
21748
  when.title = new Date(f.at).toLocaleString();
20992
- const kind = el(doc, "span", "knobs-wt-fi-kind");
21749
+ const kind = el2(doc, "span", "knobs-wt-fi-kind");
20993
21750
  kind.textContent = f.kind === "qa" ? "Answer" : "Update";
20994
- const body = el(doc, "div", "knobs-wt-fi-body");
21751
+ const body = el2(doc, "div", "knobs-wt-fi-body");
20995
21752
  if (f.q)
20996
- body.append(prose(doc, "p", "knobs-wt-fi-q", f.q));
20997
- body.append(prose(doc, "p", "knobs-wt-fi-text", f.text));
21753
+ body.append(prose2(doc, "p", "knobs-wt-fi-q", f.q));
21754
+ body.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
20998
21755
  li.append(when, kind, body);
20999
21756
  if (f.step) {
21000
21757
  const target = f.step;
21001
- const link = el(doc, "button", "knobs-wt-fi-step");
21758
+ const link = el2(doc, "button", "knobs-wt-fi-step");
21002
21759
  link.type = "button";
21003
21760
  link.textContent = page.steps.find((s) => s.key === target)?.number ?? "";
21004
21761
  link.title = "Go to the step";
@@ -21013,33 +21770,33 @@ function walkthrough(container, options) {
21013
21770
  let rail = null;
21014
21771
  count = fill = tocEl = mark = null;
21015
21772
  if (options.rail !== false) {
21016
- rail = el(doc, "aside", "knobs-wt-rail");
21773
+ rail = el2(doc, "aside", "knobs-wt-rail");
21017
21774
  rail.setAttribute("aria-label", "Progress");
21018
- const progress = el(doc, "div", "knobs-wt-progress");
21019
- count = el(doc, "div", "knobs-wt-count");
21020
- const meter = el(doc, "div", "knobs-wt-bar");
21021
- fill = el(doc, "div", "knobs-wt-fill");
21775
+ const progress = el2(doc, "div", "knobs-wt-progress");
21776
+ count = el2(doc, "div", "knobs-wt-count");
21777
+ const meter = el2(doc, "div", "knobs-wt-bar");
21778
+ fill = el2(doc, "div", "knobs-wt-fill");
21022
21779
  meter.append(fill);
21023
21780
  progress.append(count, meter);
21024
- tocEl = el(doc, "nav", "knobs-wt-toc");
21781
+ tocEl = el2(doc, "nav", "knobs-wt-toc");
21025
21782
  tocEl.setAttribute("aria-label", "Contents");
21026
- mark = el(doc, "div", "knobs-wt-toc-mark");
21783
+ mark = el2(doc, "div", "knobs-wt-toc-mark");
21027
21784
  tocEl.append(mark);
21028
21785
  rail.append(progress, tocEl, actions(true));
21029
21786
  if (options.keys !== false) {
21030
- const keys = el(doc, "div", "knobs-wt-keys");
21787
+ const keys = el2(doc, "div", "knobs-wt-keys");
21031
21788
  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" : "");
21032
21789
  rail.append(keys);
21033
21790
  }
21034
21791
  }
21035
21792
  for (const section of page.sections) {
21036
- const sec = el(doc, "section", "knobs-wt-section");
21793
+ const sec = el2(doc, "section", "knobs-wt-section");
21037
21794
  sec.id = `wt-${section.id}`;
21038
- const sh = el(doc, "header", "knobs-wt-shead");
21795
+ const sh = el2(doc, "header", "knobs-wt-shead");
21039
21796
  if (section.link || section.status) {
21040
- const kicker = el(doc, "div", "knobs-wt-kicker");
21797
+ const kicker = el2(doc, "div", "knobs-wt-kicker");
21041
21798
  if (section.link) {
21042
- const a = el(doc, "a");
21799
+ const a = el2(doc, "a");
21043
21800
  a.href = section.link.href;
21044
21801
  a.target = "_blank";
21045
21802
  a.rel = "noreferrer";
@@ -21048,21 +21805,21 @@ function walkthrough(container, options) {
21048
21805
  }
21049
21806
  if (section.status) {
21050
21807
  const s = statusLook(section.status);
21051
- const chip = el(doc, "span", "knobs-wt-chip");
21808
+ const chip = el2(doc, "span", "knobs-wt-chip");
21052
21809
  chip.dataset.tone = s.tone;
21053
21810
  chip.textContent = s.label;
21054
21811
  kicker.append(chip);
21055
21812
  }
21056
21813
  sh.append(kicker);
21057
21814
  }
21058
- const h2 = el(doc, "h2", "knobs-wt-stitle");
21815
+ const h2 = el2(doc, "h2", "knobs-wt-stitle");
21059
21816
  h2.textContent = section.title;
21060
21817
  sh.append(h2);
21061
21818
  if (section.summary)
21062
- sh.append(prose(doc, "p", "knobs-wt-ssum", section.summary));
21819
+ sh.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
21063
21820
  sec.append(sh);
21064
21821
  if (tocEl) {
21065
- const t = el(doc, "div", "knobs-wt-toc-section");
21822
+ const t = el2(doc, "div", "knobs-wt-toc-section");
21066
21823
  t.textContent = section.link ? `${section.link.label} ${section.title}` : section.title;
21067
21824
  t.title = section.title;
21068
21825
  tocEl.append(t);
@@ -21095,9 +21852,9 @@ function walkthrough(container, options) {
21095
21852
  }));
21096
21853
  };
21097
21854
  const actions = (rail) => {
21098
- const row = el(doc, "div", "knobs-wt-actions");
21855
+ const row = el2(doc, "div", "knobs-wt-actions");
21099
21856
  const btn = (text, run, primary = false) => {
21100
- const b = el(doc, "button", "knobs-wt-btn");
21857
+ const b = el2(doc, "button", "knobs-wt-btn");
21101
21858
  b.type = "button";
21102
21859
  b.textContent = text;
21103
21860
  if (primary)
@@ -21135,43 +21892,44 @@ function walkthrough(container, options) {
21135
21892
  }
21136
21893
  };
21137
21894
  const stepView = (step) => {
21138
- const card = el(doc, "article", "knobs-wt-step knobs-surface");
21895
+ const card = el2(doc, "article", "knobs-wt-step knobs-surface");
21139
21896
  card.id = `wt-${step.key.replace("/", "--")}`;
21140
21897
  card.tabIndex = -1;
21141
21898
  card.dataset.key = step.key;
21142
21899
  card.setAttribute("aria-label", `${step.number} ${step.title}`);
21143
- const head = el(doc, "header", "knobs-wt-step-head");
21144
- const num = el(doc, "span", "knobs-wt-num");
21900
+ const head = el2(doc, "header", "knobs-wt-step-head");
21901
+ const num = el2(doc, "span", "knobs-wt-num");
21145
21902
  num.textContent = step.number;
21146
- const h3 = el(doc, "h3", "knobs-wt-stepTitle");
21903
+ const h3 = el2(doc, "h3", "knobs-wt-stepTitle");
21147
21904
  h3.textContent = step.title;
21148
21905
  head.append(num, h3);
21149
21906
  card.append(head);
21150
21907
  if (step.caption)
21151
- card.append(prose(doc, "p", "knobs-wt-caption", step.caption));
21908
+ card.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
21152
21909
  const slots = [];
21153
21910
  const onEvent = (e) => record({ ...e, step: step.key });
21154
21911
  let media = null;
21155
21912
  if (step.media.length) {
21156
- media = el(doc, "div", "knobs-wt-media");
21913
+ media = el2(doc, "div", "knobs-wt-media");
21157
21914
  media.style.setProperty("--kw-n", String(Math.min(step.media.length, 3)));
21158
21915
  for (const m of step.media)
21159
- slots.push(mediaSlot(media, { ...look, media: m, onEvent }));
21916
+ slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks }));
21160
21917
  card.append(media);
21161
21918
  }
21162
21919
  let taste = null;
21163
21920
  if (step.question)
21164
- card.append(prose(doc, "p", "knobs-wt-question", step.question));
21921
+ card.append(prose2(doc, "p", "knobs-wt-question", step.question));
21165
21922
  if (step.ask === "pick") {
21166
21923
  taste = tasteTest(card, {
21167
21924
  ...look,
21168
21925
  options: step.options,
21169
21926
  picked: state.steps[step.key]?.pick ?? null,
21170
- onPick: (id) => pick(step.key, id)
21927
+ onPick: (id) => pick(step.key, id),
21928
+ steps: stepLinks
21171
21929
  });
21172
21930
  media ??= taste.element;
21173
21931
  }
21174
- const notes = el(doc, "div", "knobs-wt-notes");
21932
+ const notes = el2(doc, "div", "knobs-wt-notes");
21175
21933
  card.append(notes);
21176
21934
  card.addEventListener("focusin", () => {
21177
21935
  if (scroll.to)
@@ -21182,10 +21940,10 @@ function walkthrough(container, options) {
21182
21940
  let toc = null;
21183
21941
  let glyph = null;
21184
21942
  if (tocEl) {
21185
- toc = el(doc, "button", "knobs-wt-toc-step");
21943
+ toc = el2(doc, "button", "knobs-wt-toc-step");
21186
21944
  toc.type = "button";
21187
- glyph = el(doc, "span", "knobs-wt-toc-glyph");
21188
- const label = el(doc, "span", "knobs-wt-toc-label");
21945
+ glyph = el2(doc, "span", "knobs-wt-toc-glyph");
21946
+ const label = el2(doc, "span", "knobs-wt-toc-label");
21189
21947
  label.textContent = step.title;
21190
21948
  toc.title = `${step.number} ${step.title}`;
21191
21949
  toc.append(glyph, label);
@@ -21445,6 +22203,7 @@ export {
21445
22203
  solve2 as solveSplit,
21446
22204
  split,
21447
22205
  splitPath,
22206
+ stackLayout,
21448
22207
  stackRows,
21449
22208
  stepFile,
21450
22209
  stepItem,