@kitlangton/knobs 0.4.0-next.6 → 0.4.0-next.7

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
@@ -575,13 +575,14 @@ var compareCss = `
575
575
  picked: the pill's bright copy is. */
576
576
  .knobs-cmp-modes{position:relative;display:inline-flex;height:var(--_k-control-h);border-radius:var(--_k-radius);background:var(--_k-fill)}
577
577
  .knobs-cmp-modes[hidden]{display:none}
578
- .knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
579
- transition:color calc(var(--_k-hover-out) * 1ms)}
580
- .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
578
+ /* Hover is a quiet fill under the arrow cursor (no hand, no brightening), there at once. */
579
+ .knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:default;white-space:nowrap;
580
+ transition:background calc(var(--_k-hover-out) * 1ms)}
581
+ .knobs-ui .knobs-cmp-mode:not([aria-checked="true"]):is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
581
582
  /* Toggles: icon buttons, the accent's wash when on. */
582
- .knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-3);cursor:pointer;
583
- transition:color calc(var(--_k-hover-out) * 1ms),background calc(var(--_k-hover-out) * 1ms)}
584
- .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
583
+ .knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:default;
584
+ transition:background calc(var(--_k-hover-out) * 1ms)}
585
+ .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
585
586
  .knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}
586
587
  .knobs-cmp-toggle svg{display:block}
587
588
  .knobs-cmp-fill{flex:1}
@@ -595,11 +596,10 @@ var compareCss = `
595
596
  .knobs-cmp-stage{position:relative;flex:1;min-height:96px;overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_ku-cmp-stage);touch-action:pan-y;outline:none}
596
597
  .knobs-compare[data-frame="flat"] .knobs-cmp-stage{border-radius:0}
597
598
  .knobs-compare[data-mode="wipe"] .knobs-cmp-stage{cursor:ew-resize}
598
- /* Videos: a click on the stage plays and pauses. */
599
- .knobs-compare[data-video]:not([data-mode="wipe"],[data-zoomed]) .knobs-cmp-stage{cursor:pointer}
600
599
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
601
- .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
602
- .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
600
+ /* Zoomed, it pans: a move cursor, never a hand. */
601
+ .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:move;touch-action:none}
602
+ .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:move!important}
603
603
  /* Wiping, the grip stands in for the cursor. */
604
604
  .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:none!important}
605
605
  .knobs-cmp-sizer{position:absolute;left:0;right:0;top:0;height:0;visibility:hidden;pointer-events:none}
@@ -646,8 +646,8 @@ var compareCss = `
646
646
  opacity:0;scale:.96;pointer-events:none;transition:opacity .16s cubic-bezier(.4,0,1,1),scale .16s cubic-bezier(.4,0,1,1)}
647
647
  .knobs-cmp-zoom[data-on]{opacity:1;scale:1;pointer-events:auto;transition:opacity .2s cubic-bezier(.2,.8,.2,1),scale .2s cubic-bezier(.2,.8,.2,1)}
648
648
  :where(.knobs-ui) .knobs-cmp-zoom-reset{all:revert;box-sizing:border-box;font:inherit;letter-spacing:inherit;margin:0;border:0;outline:none;box-shadow:none;
649
- display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
650
- :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){color:var(--_k-fg);background:color-mix(in srgb,var(--_k-fg) 12%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
649
+ display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:default;transition:background calc(var(--_k-hover-out) * 1ms)}
650
+ :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
651
651
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);font-size:var(--_k-text-meta);pointer-events:none}
652
652
  .knobs-cmp-note:empty{display:none}
653
653
  .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
@@ -665,23 +665,29 @@ var playerCss = `
665
665
  .knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;
666
666
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
667
667
  :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-player,.knobs-ui.knobs-player[data-nested]{--_kp-stage:var(--k-compare-stage,transparent)}
668
- .knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage);cursor:pointer}
668
+ .knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage)}
669
669
  .knobs-player[data-frame="flat"] .knobs-player-frame{border-radius:0}
670
670
  .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
671
671
  opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
672
672
  .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
673
- .knobs-player-glyph{position:absolute;left:50%;top:50%;display:grid;place-items:center;width:44px;height:44px;margin:-22px 0 0 -22px;border-radius:50%;color:var(--_k-fg);pointer-events:none;
674
- opacity:0;filter:blur(4px);scale:.96;
675
- background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
676
- box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim);
677
- transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),filter calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
678
- .knobs-player-glyph svg{width:16px;height:16px}
679
- .knobs-player-glyph[data-flash]{opacity:1;filter:none;scale:1}
680
673
  .knobs-player-line{flex:none;min-height:0}
681
- /* Hover in is immediate (0 ms), out eases. The timeline's own stylesheet is look.ts's; its hover
682
- states are set here, after it, so the player's line and every timeline share them. */
674
+ /* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share
675
+ about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it
676
+ resizes); hover is a quiet fill (fill-hover), in at once (hover-in, 0 ms), out easing. */
677
+ .knobs-ui .knobs-tl-play,.knobs-ui.knobs-timeline :is(.knobs-tl-pin,.knobs-tl-rlabel),
678
+ .knobs-ui.knobs-timeline[data-dragging] .knobs-tl-marker :is(.knobs-tl-pin,.knobs-tl-mlabel){cursor:default}
683
679
  .knobs-ui .knobs-tl-play:hover,.knobs-tl-edge:hover::after,.knobs-tl-rlabel:hover,.knobs-tl-track:hover .knobs-tl-hover,
684
680
  .knobs-timeline[data-variant="line"] .knobs-tl-track:hover .knobs-tl-segs{transition-duration:calc(var(--_k-hover-in) * 1ms)}
681
+ /* The line's row: a minimal play/pause (the filled pair, a 28px target, no fill at rest), then
682
+ the line, centred on it. */
683
+ .knobs-ui.knobs-timeline[data-variant="line"]{gap:4px;align-items:center}
684
+ .knobs-timeline[data-variant="line"] .knobs-tl-transport{display:flex}
685
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-play{width:28px;height:28px;min-width:28px;min-height:28px;border-radius:var(--_k-radius);background:transparent;color:var(--_k-fg-2);
686
+ transition:background calc(var(--_k-hover-out) * 1ms),transform .15s var(--_ku-ease)}
687
+ .knobs-ui.knobs-timeline[data-variant="line"][data-playing] .knobs-tl-play{background:transparent;color:var(--_k-fg-2)}
688
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-play:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
689
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-track{cursor:default;transition:background calc(var(--_k-hover-out) * 1ms)}
690
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-track:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
685
691
  `;
686
692
 
687
693
  // src/ui/overlay-css.ts
@@ -8177,196 +8183,6 @@ var init_compare_strip = __esm(() => {
8177
8183
  DIFF = { w: 32, h: 18 };
8178
8184
  });
8179
8185
 
8180
- // src/ui/player.ts
8181
- function player(container, options) {
8182
- const doc = container.ownerDocument;
8183
- const el = (tag, cls) => {
8184
- const n = doc.createElement(tag);
8185
- n.className = cls;
8186
- return n;
8187
- };
8188
- const look = {
8189
- ...options.preset ? { preset: options.preset } : {},
8190
- ...options.theme ? { theme: options.theme } : {},
8191
- ...options.appearance ? { appearance: options.appearance } : {}
8192
- };
8193
- const root = el("div", "knobs-player");
8194
- root.setAttribute("role", "group");
8195
- root.setAttribute("aria-label", options.label ?? "Video");
8196
- applyLook2(root, options);
8197
- const frame = el("div", "knobs-player-frame");
8198
- frame.style.setProperty("--kp-aspect", String(options.aspect ?? 16 / 9));
8199
- const v = el("video", "knobs-player-media");
8200
- v.muted = options.muted ?? true;
8201
- v.defaultMuted = v.muted;
8202
- v.playsInline = true;
8203
- v.preload = "metadata";
8204
- v.setAttribute("playsinline", "");
8205
- if (options.poster)
8206
- v.poster = options.poster;
8207
- if (options.captions) {
8208
- const track = doc.createElement("track");
8209
- track.kind = "captions";
8210
- track.label = "Captions";
8211
- track.srclang = doc.documentElement.lang || "en";
8212
- track.default = true;
8213
- track.src = options.captions;
8214
- v.append(track);
8215
- }
8216
- const glyph = playGlyph(doc);
8217
- frame.append(v, glyph.element);
8218
- const host = el("div", "knobs-player-line");
8219
- root.append(frame, host);
8220
- container.append(root);
8221
- const markers = (options.chapters ?? []).map((c, i) => ({
8222
- id: c.id ?? `c${i}`,
8223
- time: c.start,
8224
- label: c.label
8225
- }));
8226
- const tl = timeline(host, {
8227
- ...look,
8228
- variant: "line",
8229
- duration: 1,
8230
- fps: options.fps ?? 30,
8231
- markers,
8232
- repeat: options.loop ?? true,
8233
- arrows: { step: 5, shift: 10 },
8234
- label: options.label ? `${options.label} time` : "Video time",
8235
- ...options.hints === false ? { hints: false } : {}
8236
- });
8237
- const onFrame = () => root.setAttribute("data-ready", "");
8238
- let pending = null;
8239
- const seekVideo = (t) => {
8240
- if (v.readyState < 1)
8241
- return;
8242
- if (v.seeking)
8243
- pending = t;
8244
- else
8245
- v.currentTime = t;
8246
- };
8247
- let reported = 0;
8248
- const onSeeked = () => {
8249
- if (pending !== null) {
8250
- const t = pending;
8251
- pending = null;
8252
- v.currentTime = t;
8253
- return;
8254
- }
8255
- if (!v.paused || tl.playing || Math.abs(v.currentTime - reported) < 0.1)
8256
- return;
8257
- reported = v.currentTime;
8258
- options.onSeek?.(reported);
8259
- };
8260
- const onMeta = () => {
8261
- onFrame();
8262
- if (v.videoWidth)
8263
- frame.style.setProperty("--kp-aspect", String(v.videoWidth / v.videoHeight));
8264
- if (Number.isFinite(v.duration))
8265
- tl.update({ duration: v.duration });
8266
- };
8267
- v.addEventListener("loadedmetadata", onMeta);
8268
- v.addEventListener("loadeddata", onFrame);
8269
- v.addEventListener("seeked", onSeeked);
8270
- const unlisten = tl.listen({
8271
- onSeek(t, source) {
8272
- if (source === "play") {
8273
- if (Math.abs(v.currentTime - t) > 0.25)
8274
- v.currentTime = t;
8275
- return;
8276
- }
8277
- if (Math.abs(v.currentTime - t) < 1 / (options.fps ?? 30))
8278
- return;
8279
- seekVideo(t);
8280
- },
8281
- onPlay(on) {
8282
- if (on)
8283
- v.play().catch(() => tl.pause());
8284
- else {
8285
- v.pause();
8286
- reported = tl.time;
8287
- }
8288
- root.toggleAttribute("data-playing", on);
8289
- options.onPlay?.(on, tl.time);
8290
- },
8291
- onScrub(on) {
8292
- if (on)
8293
- v.pause();
8294
- }
8295
- });
8296
- tl.drive(() => {
8297
- if (v.readyState < 1)
8298
- return Number.NaN;
8299
- if (v.seeking)
8300
- return tl.time;
8301
- return v.ended ? Number.POSITIVE_INFINITY : v.currentTime;
8302
- });
8303
- const onClick = (e) => {
8304
- if (e.button !== 0)
8305
- return;
8306
- tl.toggle();
8307
- glyph.flash(tl.playing);
8308
- tl.focus();
8309
- };
8310
- frame.addEventListener("click", onClick);
8311
- v.src = options.src;
8312
- const api = {
8313
- element: root,
8314
- video: v,
8315
- timeline: tl,
8316
- get time() {
8317
- return tl.time;
8318
- },
8319
- get playing() {
8320
- return tl.playing;
8321
- },
8322
- get duration() {
8323
- return Number.isFinite(v.duration) ? v.duration : 0;
8324
- },
8325
- play: () => tl.play(),
8326
- pause: () => tl.pause(),
8327
- toggle: () => tl.toggle(),
8328
- seek: (t, o) => tl.seek(t, o?.glide ? { glide: true } : undefined),
8329
- seekBy: (d) => tl.seek(Math.min(tl.duration, Math.max(0, tl.time + d)), { glide: true }),
8330
- focus: () => tl.focus(),
8331
- dispose() {
8332
- unlisten();
8333
- glyph.dispose();
8334
- tl.drive(null);
8335
- tl.dispose();
8336
- v.pause();
8337
- v.removeEventListener("loadedmetadata", onMeta);
8338
- v.removeEventListener("loadeddata", onFrame);
8339
- v.removeEventListener("seeked", onSeeked);
8340
- v.removeAttribute("src");
8341
- v.load();
8342
- root.remove();
8343
- }
8344
- };
8345
- return api;
8346
- }
8347
- function playGlyph(doc) {
8348
- const element = doc.createElement("span");
8349
- element.className = "knobs-player-glyph";
8350
- element.setAttribute("aria-hidden", "true");
8351
- let timer = 0;
8352
- const win = doc.defaultView;
8353
- return {
8354
- element,
8355
- flash(playing) {
8356
- element.innerHTML = playing ? PLAY : PAUSE;
8357
- element.setAttribute("data-flash", "");
8358
- win.clearTimeout(timer);
8359
- timer = win.setTimeout(() => element.removeAttribute("data-flash"), FLASH_MS);
8360
- },
8361
- dispose: () => win.clearTimeout(timer)
8362
- };
8363
- }
8364
- var FLASH_MS = 450;
8365
- var init_player = __esm(() => {
8366
- init_look();
8367
- init_timeline();
8368
- });
8369
-
8370
8186
  // src/ui/compare.ts
8371
8187
  function compare(container, options) {
8372
8188
  const doc = container.ownerDocument;
@@ -8485,8 +8301,7 @@ function compare(container, options) {
8485
8301
  grip.setAttribute("aria-hidden", "true");
8486
8302
  handle.append(grip);
8487
8303
  const note = el("div", "knobs-cmp-note");
8488
- const glyph = playGlyph(doc);
8489
- stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note, glyph.element);
8304
+ stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note);
8490
8305
  root.append(bar, stage);
8491
8306
  const wantsStrip = options.strip === true;
8492
8307
  let tl = bound;
@@ -8528,7 +8343,6 @@ function compare(container, options) {
8528
8343
  let drag = null;
8529
8344
  const touches = new Map;
8530
8345
  let pinch = null;
8531
- let press = null;
8532
8346
  const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
8533
8347
  const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
8534
8348
  const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
@@ -9357,10 +9171,8 @@ function compare(container, options) {
9357
9171
  root.focus({ preventScroll: true });
9358
9172
  touches.set(e.pointerId, p);
9359
9173
  stage.setPointerCapture(e.pointerId);
9360
- press = touches.size === 1 && mode !== "wipe" && master() ? p : null;
9361
9174
  if (touches.size >= 2) {
9362
9175
  e.preventDefault();
9363
- press = null;
9364
9176
  return startPinch();
9365
9177
  }
9366
9178
  if (mode === "wipe") {
@@ -9388,8 +9200,6 @@ function compare(container, options) {
9388
9200
  const onMove = (e) => {
9389
9201
  const p = local(e);
9390
9202
  pointer = p;
9391
- if (press && Math.hypot(p.x - press.x, p.y - press.y) > 4)
9392
- press = null;
9393
9203
  if (touches.has(e.pointerId))
9394
9204
  touches.set(e.pointerId, p);
9395
9205
  if (pinch && touches.size >= 2) {
@@ -9419,11 +9229,6 @@ function compare(container, options) {
9419
9229
  drawNow();
9420
9230
  };
9421
9231
  const endDrag = (e) => {
9422
- if (press && e.type === "pointerup") {
9423
- api.toggle();
9424
- glyph.flash(isPlaying());
9425
- }
9426
- press = null;
9427
9232
  touches.delete(e.pointerId);
9428
9233
  if (touches.size < 2)
9429
9234
  pinch = null;
@@ -9585,7 +9390,6 @@ function compare(container, options) {
9585
9390
  old.el.replaceWith(s.el);
9586
9391
  old.preview.replaceWith(s.preview);
9587
9392
  sides[index] = s;
9588
- root.toggleAttribute("data-video", sides.some((x) => x.kind === "video"));
9589
9393
  note.textContent = "";
9590
9394
  setLabel(index, labelOf(index));
9591
9395
  if (near)
@@ -9600,7 +9404,6 @@ function compare(container, options) {
9600
9404
  stage.insertBefore(sides[0].preview, labels[0]);
9601
9405
  setLabel(0, labelOf(0));
9602
9406
  setLabel(1, labelOf(1));
9603
- root.toggleAttribute("data-video", sides.some((s) => s.kind === "video"));
9604
9407
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
9605
9408
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
9606
9409
  const modePill = modeList.length > 1 ? selectionPill(modes, {
@@ -9801,7 +9604,6 @@ function compare(container, options) {
9801
9604
  disposed = true;
9802
9605
  loop.stop();
9803
9606
  modePill?.dispose();
9804
- glyph.dispose();
9805
9607
  win.clearTimeout(releaseTimer);
9806
9608
  ro.disconnect();
9807
9609
  nearIo?.disconnect();
@@ -9842,7 +9644,6 @@ var init_compare = __esm(() => {
9842
9644
  init_icons();
9843
9645
  init_look();
9844
9646
  init_rgba();
9845
- init_player();
9846
9647
  init_selection_pill();
9847
9648
  init_timeline();
9848
9649
  init_timeline_model();
@@ -10935,16 +10736,17 @@ var init_diff_css = __esm(() => {
10935
10736
  .knobs-diff-nav>span{min-width:3.6em;text-align:center}
10936
10737
  :where(.knobs-ui) :is(.knobs-diff-btn,.knobs-diff-choice,.knobs-diff-viewed,.knobs-diff-filter>input,.knobs-diff-filter>button){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;border-radius:0;
10937
10738
  margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
10938
- .knobs-ui .knobs-diff-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;
10939
- transition:background .15s,color .15s,opacity .15s}
10940
- .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
10739
+ .knobs-ui .knobs-diff-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-2);cursor:default;
10740
+ transition:background calc(var(--_k-hover-out) * 1ms),opacity .15s}
10741
+ /* Hover is a quiet fill under the arrow cursor (no hand, no brightening), there at once. */
10742
+ .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
10941
10743
  .knobs-ui .knobs-diff-btn[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
10942
10744
  .knobs-ui .knobs-diff-btn:disabled{opacity:.35;cursor:default;background:transparent}
10943
10745
  .knobs-diff-btn svg{width:16px;height:16px}
10944
10746
  /* Viewed: a check with its word, as on GitHub; the path dims once it's ticked. */
10945
10747
  .knobs-ui .knobs-diff-viewed{flex:none;display:inline-flex;align-items:center;gap:5px;height:var(--_k-control-h);padding:0 8px 0 6px;border-radius:var(--kd-ctl);color:var(--_k-fg-2);
10946
- font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
10947
- .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
10748
+ font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:default;transition:background calc(var(--_k-hover-out) * 1ms),color .15s}
10749
+ .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
10948
10750
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
10949
10751
  .knobs-diff-viewed svg{width:16px;height:16px}
10950
10752
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
@@ -10963,8 +10765,8 @@ var init_diff_css = __esm(() => {
10963
10765
  choice isn't brighter for being picked. Choices are styled by class, as the copy holds them too. */
10964
10766
  .knobs-diff-seg{position:relative;flex:none;display:grid;grid-template-columns:1fr 1fr;height:var(--_k-control-h);padding:2px;border-radius:var(--kd-ctl);background:var(--_k-fill)}
10965
10767
  .knobs-ui .knobs-diff-choice{padding:0 9px;border-radius:max(0px,calc(var(--kd-ctl) - 2px));color:var(--_k-fg-2);font-size:var(--_k-text-meta);
10966
- font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
10967
- .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
10768
+ font-weight:var(--_k-weight-label);white-space:nowrap;cursor:default;transition:background calc(var(--_k-hover-out) * 1ms)}
10769
+ .knobs-ui .knobs-diff-choice:not([aria-checked="true"]):is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
10968
10770
  /* The diff: one layer per file or layout on show, the old fading under the new. */
10969
10771
  .knobs-diff-body{position:relative;flex:1;min-height:0}
10970
10772
  .knobs-diff-probe{position:absolute;left:0;top:0;width:0;height:var(--kd-line);visibility:hidden;pointer-events:none}
@@ -11060,8 +10862,8 @@ var init_diff_css = __esm(() => {
11060
10862
  .knobs-diff-filter svg{width:12px;height:12px;flex:none}
11061
10863
  .knobs-ui .knobs-diff-filter>input{flex:1;min-width:0;height:100%;font:var(--_k-weight) var(--_k-font-size)/1 var(--_k-font);color:var(--_k-fg);caret-color:var(--_k-accent);background:transparent}
11062
10864
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
11063
- .knobs-ui .knobs-diff-filter>button{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;color:var(--_k-fg-3);cursor:pointer;transition:background .12s,color .12s,opacity .12s}
11064
- .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
10865
+ .knobs-ui .knobs-diff-filter>button{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;color:var(--_k-fg-3);cursor:default;transition:background calc(var(--_k-hover-out) * 1ms),opacity .12s}
10866
+ .knobs-ui .knobs-diff-filter>button:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
11065
10867
  .knobs-diff-filter>button svg{width:12px;height:12px}
11066
10868
  .knobs-diff-list{position:relative;flex:1;min-height:0}
11067
10869
  .knobs-diff-files .knobs-diff-counts{font-size:var(--_k-text-meta);align-items:center}
@@ -16577,8 +16379,169 @@ init_timeline();
16577
16379
  init_timeline_model();
16578
16380
  init_compare();
16579
16381
  init_compare_strip();
16580
- init_player();
16581
16382
 
16383
+ // src/ui/player.ts
16384
+ init_look();
16385
+ init_timeline();
16386
+ function player(container, options) {
16387
+ const doc = container.ownerDocument;
16388
+ const el = (tag, cls) => {
16389
+ const n = doc.createElement(tag);
16390
+ n.className = cls;
16391
+ return n;
16392
+ };
16393
+ const look = {
16394
+ ...options.preset ? { preset: options.preset } : {},
16395
+ ...options.theme ? { theme: options.theme } : {},
16396
+ ...options.appearance ? { appearance: options.appearance } : {}
16397
+ };
16398
+ const root = el("div", "knobs-player");
16399
+ root.setAttribute("role", "group");
16400
+ root.setAttribute("aria-label", options.label ?? "Video");
16401
+ applyLook2(root, options);
16402
+ const frame = el("div", "knobs-player-frame");
16403
+ frame.style.setProperty("--kp-aspect", String(options.aspect ?? 16 / 9));
16404
+ const v = el("video", "knobs-player-media");
16405
+ v.muted = options.muted ?? true;
16406
+ v.defaultMuted = v.muted;
16407
+ v.playsInline = true;
16408
+ v.preload = "metadata";
16409
+ v.setAttribute("playsinline", "");
16410
+ if (options.poster)
16411
+ v.poster = options.poster;
16412
+ if (options.captions) {
16413
+ const track = doc.createElement("track");
16414
+ track.kind = "captions";
16415
+ track.label = "Captions";
16416
+ track.srclang = doc.documentElement.lang || "en";
16417
+ track.default = true;
16418
+ track.src = options.captions;
16419
+ v.append(track);
16420
+ }
16421
+ frame.append(v);
16422
+ const host = el("div", "knobs-player-line");
16423
+ root.append(frame, host);
16424
+ container.append(root);
16425
+ const markers = (options.chapters ?? []).map((c, i) => ({
16426
+ id: c.id ?? `c${i}`,
16427
+ time: c.start,
16428
+ label: c.label
16429
+ }));
16430
+ const tl = timeline(host, {
16431
+ ...look,
16432
+ variant: "line",
16433
+ duration: 1,
16434
+ fps: options.fps ?? 30,
16435
+ markers,
16436
+ repeat: options.loop ?? true,
16437
+ arrows: { step: 5, shift: 10 },
16438
+ label: options.label ? `${options.label} time` : "Video time",
16439
+ ...options.hints === false ? { hints: false } : {}
16440
+ });
16441
+ const onFrame = () => root.setAttribute("data-ready", "");
16442
+ let pending = null;
16443
+ const seekVideo = (t) => {
16444
+ if (v.readyState < 1)
16445
+ return;
16446
+ if (v.seeking)
16447
+ pending = t;
16448
+ else
16449
+ v.currentTime = t;
16450
+ };
16451
+ let reported = 0;
16452
+ const onSeeked = () => {
16453
+ if (pending !== null) {
16454
+ const t = pending;
16455
+ pending = null;
16456
+ v.currentTime = t;
16457
+ return;
16458
+ }
16459
+ if (!v.paused || tl.playing || Math.abs(v.currentTime - reported) < 0.1)
16460
+ return;
16461
+ reported = v.currentTime;
16462
+ options.onSeek?.(reported);
16463
+ };
16464
+ const onMeta = () => {
16465
+ onFrame();
16466
+ if (v.videoWidth)
16467
+ frame.style.setProperty("--kp-aspect", String(v.videoWidth / v.videoHeight));
16468
+ if (Number.isFinite(v.duration))
16469
+ tl.update({ duration: v.duration });
16470
+ };
16471
+ v.addEventListener("loadedmetadata", onMeta);
16472
+ v.addEventListener("loadeddata", onFrame);
16473
+ v.addEventListener("seeked", onSeeked);
16474
+ const unlisten = tl.listen({
16475
+ onSeek(t, source) {
16476
+ if (source === "play") {
16477
+ if (Math.abs(v.currentTime - t) > 0.25)
16478
+ v.currentTime = t;
16479
+ return;
16480
+ }
16481
+ if (Math.abs(v.currentTime - t) < 1 / (options.fps ?? 30))
16482
+ return;
16483
+ seekVideo(t);
16484
+ },
16485
+ onPlay(on) {
16486
+ if (on)
16487
+ v.play().catch(() => tl.pause());
16488
+ else {
16489
+ v.pause();
16490
+ reported = tl.time;
16491
+ }
16492
+ root.toggleAttribute("data-playing", on);
16493
+ options.onPlay?.(on, tl.time);
16494
+ },
16495
+ onScrub(on) {
16496
+ if (on)
16497
+ v.pause();
16498
+ }
16499
+ });
16500
+ tl.drive(() => {
16501
+ if (v.readyState < 1)
16502
+ return Number.NaN;
16503
+ if (v.seeking)
16504
+ return tl.time;
16505
+ return v.ended ? Number.POSITIVE_INFINITY : v.currentTime;
16506
+ });
16507
+ const onClick = () => tl.focus();
16508
+ frame.addEventListener("click", onClick);
16509
+ v.src = options.src;
16510
+ const api = {
16511
+ element: root,
16512
+ video: v,
16513
+ timeline: tl,
16514
+ get time() {
16515
+ return tl.time;
16516
+ },
16517
+ get playing() {
16518
+ return tl.playing;
16519
+ },
16520
+ get duration() {
16521
+ return Number.isFinite(v.duration) ? v.duration : 0;
16522
+ },
16523
+ play: () => tl.play(),
16524
+ pause: () => tl.pause(),
16525
+ toggle: () => tl.toggle(),
16526
+ seek: (t, o) => tl.seek(t, o?.glide ? { glide: true } : undefined),
16527
+ seekBy: (d) => tl.seek(Math.min(tl.duration, Math.max(0, tl.time + d)), { glide: true }),
16528
+ focus: () => tl.focus(),
16529
+ dispose() {
16530
+ unlisten();
16531
+ tl.drive(null);
16532
+ tl.dispose();
16533
+ v.pause();
16534
+ frame.removeEventListener("click", onClick);
16535
+ v.removeEventListener("loadedmetadata", onMeta);
16536
+ v.removeEventListener("loadeddata", onFrame);
16537
+ v.removeEventListener("seeked", onSeeked);
16538
+ v.removeAttribute("src");
16539
+ v.load();
16540
+ root.remove();
16541
+ }
16542
+ };
16543
+ return api;
16544
+ }
16582
16545
  // src/ui/history.ts
16583
16546
  init_motion();
16584
16547
  init_glide();
@@ -17480,9 +17443,10 @@ function knownPRs(doc, sections) {
17480
17443
  }
17481
17444
  function findPR(prs, n, repo) {
17482
17445
  const known = prs[n];
17446
+ const href = repo ? `https://github.com/${repo}/pull/${n}` : undefined;
17483
17447
  if (known)
17484
- return known;
17485
- return repo ? { href: `https://github.com/${repo}/pull/${n}` } : null;
17448
+ return known.href || !href ? known : { ...known, href };
17449
+ return href ? { href } : null;
17486
17450
  }
17487
17451
  var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
17488
17452
  function withKickers(sections, source, kickers = "pr") {
@@ -21979,7 +21943,7 @@ function mountHistory(host, m, steps) {
21979
21943
  context: () => ({ kind: "history" })
21980
21944
  };
21981
21945
  }
21982
- function mountStack(host, m, steps) {
21946
+ function mountStack(host, m, steps, findPR) {
21983
21947
  const doc = host.ownerDocument;
21984
21948
  const layout = stackLayout(m);
21985
21949
  const root = el2(doc, "div", "knobs-wt-stack");
@@ -22021,7 +21985,10 @@ function mountStack(host, m, steps) {
22021
21985
  const line = el2(doc, "div", "knobs-wt-sline");
22022
21986
  if (it.order)
22023
21987
  line.append(text2(doc, "span", "knobs-wt-sord", it.order));
22024
- line.append(link(doc, "knobs-wt-spr", it.pr, it.href));
21988
+ const known = findPR?.(it.pr.replace(/^#/, "")) ?? null;
21989
+ const pr = link(doc, "knobs-wt-spr", it.pr, it.href ?? known?.href);
21990
+ pr.addEventListener("click", (e) => e.stopPropagation());
21991
+ line.append(pr);
22025
21992
  const target = it.step ? steps?.find(it.step) : null;
22026
21993
  if (target && steps) {
22027
21994
  const title = text2(doc, "button", "knobs-wt-stitle", it.title);
@@ -22056,7 +22023,11 @@ function mountStack(host, m, steps) {
22056
22023
  if (meta.childNodes.length)
22057
22024
  main.append(meta);
22058
22025
  row.append(main);
22059
- if (it.status) {
22026
+ const said = prStatus(it.status);
22027
+ const state = known?.state ?? said.state;
22028
+ if (state)
22029
+ row.append(prStateLabel(doc, state, said.checks, it.status));
22030
+ else if (it.status) {
22060
22031
  const chip = text2(doc, "span", "knobs-wt-chip knobs-wt-sstatus", it.status);
22061
22032
  chip.dataset.tone = it.tone ?? "dim";
22062
22033
  row.append(chip);
@@ -22363,8 +22334,34 @@ var PR_PATHS = {
22363
22334
  pull: "M104,64A32,32,0,1,0,64,95v66a32,32,0,1,0,16,0V95A32.06,32.06,0,0,0,104,64ZM56,64A16,16,0,1,1,72,80,16,16,0,0,1,56,64ZM88,192a16,16,0,1,1-16-16A16,16,0,0,1,88,192Zm120-31V110.63a23.85,23.85,0,0,0-7-17L163.31,56H192a8,8,0,0,0,0-16H144a8,8,0,0,0-8,8V96a8,8,0,0,0,16,0V67.31L189.66,105a8,8,0,0,1,2.34,5.66V161a32,32,0,1,0,16,0Zm-8,47a16,16,0,1,1,16-16A16,16,0,0,1,200,208Z",
22364
22335
  merge: "M208,112a32.05,32.05,0,0,0-30.69,23l-42.21-6a8,8,0,0,1-4.95-2.71L94.43,84.55A32,32,0,1,0,72,87v82a32,32,0,1,0,16,0V101.63l30,35a24,24,0,0,0,14.83,8.14l44,6.28A32,32,0,1,0,208,112ZM64,56A16,16,0,1,1,80,72,16,16,0,0,1,64,56ZM96,200a16,16,0,1,1-16-16A16,16,0,0,1,96,200Zm112-40a16,16,0,1,1,16-16A16,16,0,0,1,208,160Z"
22365
22336
  };
22366
- var prGlyph = (merged) => `<svg class="knobs-icon" viewBox="0 0 256 256" width="12" height="12" fill="currentColor" aria-hidden="true" focusable="false"><path d="${merged ? PR_PATHS.merge : PR_PATHS.pull}"/></svg>`;
22337
+ var DRAFT = "M104,64A32,32,0,1,0,64,95v66a32,32,0,1,0,16,0V95A32.06,32.06,0,0,0,104,64ZM56,64A16,16,0,1,1,72,80,16,16,0,0,1,56,64ZM88,192a16,16,0,1,1-16-16A16,16,0,0,1,88,192ZM232,192a32,32,0,1,1-32-32A32,32,0,0,1,232,192ZM216,192a16,16,0,1,0-16,16A16,16,0,0,0,216,192ZM212,120a12,12,0,1,1-12-12A12,12,0,0,1,212,120ZM212,60a12,12,0,1,1-12-12A12,12,0,0,1,212,60Z";
22338
+ var X = "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z";
22339
+ var CHECK3 = "M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z";
22340
+ var svg12 = (d, evenodd = false) => `<svg class="knobs-icon" viewBox="0 0 256 256" width="12" height="12" fill="currentColor"${evenodd ? ' fill-rule="evenodd"' : ""} aria-hidden="true" focusable="false"><path d="${d}"/></svg>`;
22341
+ var prGlyph = (state) => state === "merged" ? svg12(PR_PATHS.merge) : state === "draft" ? svg12(DRAFT, true) : state === "closed" ? svg12(X) : svg12(PR_PATHS.pull);
22367
22342
  var PR_STATE = { merged: "Merged", open: "Open", draft: "Draft", closed: "Closed" };
22343
+ function prStatus(status) {
22344
+ const words = (status ?? "").trim().toLowerCase();
22345
+ const first = /^[a-z]+/.exec(words)?.[0] ?? "";
22346
+ const state = first in PR_STATE ? first : null;
22347
+ return { state, checks: /\b(green|passing|passed|ci|checks? pass)/.test(words) };
22348
+ }
22349
+ function prStateLabel(doc, state, checks, title) {
22350
+ const s = el2(doc, "span", "knobs-wt-prstate");
22351
+ s.dataset.state = state;
22352
+ s.innerHTML = prGlyph(state);
22353
+ s.append(text2(doc, "span", "", PR_STATE[state]));
22354
+ if (checks) {
22355
+ const c = el2(doc, "span", "knobs-wt-prchecks");
22356
+ c.innerHTML = svg12(CHECK3);
22357
+ c.title = "Checks pass";
22358
+ c.setAttribute("aria-label", "checks pass");
22359
+ s.append(c);
22360
+ }
22361
+ if (title)
22362
+ s.title = title;
22363
+ return s;
22364
+ }
22368
22365
  function prChip(doc, n, pr) {
22369
22366
  const chip = el2(doc, pr.href ? "a" : "span", "knobs-wt-pr");
22370
22367
  if (pr.href) {
@@ -22374,7 +22371,7 @@ function prChip(doc, n, pr) {
22374
22371
  }
22375
22372
  if (pr.state)
22376
22373
  chip.dataset.state = pr.state;
22377
- chip.innerHTML = prGlyph(pr.state === "merged");
22374
+ chip.innerHTML = prGlyph(pr.state);
22378
22375
  chip.append(text2(doc, "span", "", `#${n}`));
22379
22376
  const tip = [`#${n}`, pr.title, pr.state ? PR_STATE[pr.state] : ""].filter(Boolean).join(" · ");
22380
22377
  chip.title = tip;
@@ -22851,12 +22848,21 @@ var walkthroughCss = `
22851
22848
  .knobs-wt-add{color:var(--_k-diff-add)}
22852
22849
  .knobs-wt-del{color:var(--_k-diff-del)}
22853
22850
  .knobs-wt-sstatus{margin-top:calc((var(--kw-line) - 20px) / 2)}
22851
+ /* A PR's state the GitHub way: its glyph and word, open and merged quietly tinted (as PR chips),
22852
+ draft and closed faint; no fill, no dot. A check after it where the checks pass, faint too. One
22853
+ width for every row, so the glyphs and words line up. */
22854
+ .knobs-ui .knobs-wt-prstate{--kw-pr:var(--_k-fg-3);display:inline-flex;align-items:center;gap:4px;min-width:calc(7ch + 32px);height:var(--kw-line);color:var(--kw-pr);
22855
+ font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);white-space:nowrap}
22856
+ .knobs-ui .knobs-wt-prstate[data-state="open"]{--kw-pr:color-mix(in oklch,var(--_k-ok) 60%,var(--_k-fg-2))}
22857
+ .knobs-ui .knobs-wt-prstate[data-state="merged"]{--kw-pr:color-mix(in oklch,var(--_k-series-5) 60%,var(--_k-fg-2))}
22858
+ .knobs-wt-prstate svg{flex:none}
22859
+ .knobs-ui .knobs-wt-prchecks{display:inline-flex;margin-left:2px;color:var(--_k-fg-3)}
22854
22860
  .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr) auto}
22855
22861
  .knobs-wt-stack[data-flat] .knobs-wt-srow+.knobs-wt-srow{box-shadow:inset 0 1px 0 var(--_k-border)}
22856
22862
  @container knobs-wt-slot (max-width:520px){
22857
22863
  .knobs-wt-srow:not([data-trunk]){grid-template-columns:auto minmax(0,1fr)}
22858
22864
  .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr)}
22859
- .knobs-wt-sstatus{grid-column:-2;justify-self:start;margin-top:4px}
22865
+ :is(.knobs-wt-sstatus,.knobs-wt-prstate){grid-column:-2;justify-self:start;margin-top:4px}
22860
22866
  }
22861
22867
 
22862
22868
  /* A table: a real one, its head stuck while it scrolls, both ways; edges fade where there's more
@@ -23502,7 +23508,7 @@ function mediaSlot(container, options) {
23502
23508
  case "history":
23503
23509
  return done(mountHistory(host, m, options.steps));
23504
23510
  case "stack":
23505
- return done(mountStack(host, m, options.steps));
23511
+ return done(mountStack(host, m, options.steps, options.findPR));
23506
23512
  case "table":
23507
23513
  return done(mountTable(host, m, options.steps));
23508
23514
  case "mount": {
@@ -23556,7 +23562,7 @@ function mediaSlot(container, options) {
23556
23562
  }
23557
23563
  };
23558
23564
  }
23559
- var CHECK3 = icon("check", { small: true });
23565
+ var CHECK4 = icon("check", { small: true });
23560
23566
 
23561
23567
  // src/ui/walkthrough.ts
23562
23568
  var el3 = (doc, tag, cls) => {
@@ -23643,7 +23649,7 @@ function tasteTest(container, options) {
23643
23649
  pick.append(key);
23644
23650
  }
23645
23651
  const check = el3(doc, "span", "knobs-wt-check");
23646
- check.innerHTML = CHECK3;
23652
+ check.innerHTML = CHECK4;
23647
23653
  pick.append(check);
23648
23654
  pick.addEventListener("click", () => {
23649
23655
  const next = picked === o.id ? null : o.id;
@@ -24804,7 +24810,7 @@ function walkthrough(container, options) {
24804
24810
  media = el3(doc, "div", "knobs-wt-media");
24805
24811
  media.style.setProperty("--kw-n", String(Math.min(step.media.length, step.columns)));
24806
24812
  for (const m of step.media)
24807
- slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks }));
24813
+ slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks, findPR: findPRIn }));
24808
24814
  card.append(media);
24809
24815
  }
24810
24816
  let taste = null;