cursedbelt 5.77.0 → 5.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/react/canvas-hud/CanvasToolbar.d.ts +1 -1
  2. package/dist/react/components/DataTable/TableFilterBuilder.d.ts.map +1 -1
  3. package/dist/react/components/DataTable/TableFilterBuilder.js +2 -2
  4. package/dist/react/components/DataTable/TableFilterBuilder.js.map +1 -1
  5. package/dist/react/components/ListToolbar.d.ts +2 -2
  6. package/dist/react/components/ListToolbar.d.ts.map +1 -1
  7. package/dist/react/components/ListToolbar.js +1 -1
  8. package/dist/react/hooks/useOutsidePress.d.ts.map +1 -1
  9. package/dist/react/hooks/useOutsidePress.js +18 -3
  10. package/dist/react/hooks/useOutsidePress.js.map +1 -1
  11. package/dist/react/hooks/useScrollMemory.d.ts +4 -3
  12. package/dist/react/hooks/useScrollMemory.d.ts.map +1 -1
  13. package/dist/react/hooks/useScrollMemory.js +16 -2
  14. package/dist/react/hooks/useScrollMemory.js.map +1 -1
  15. package/dist/react/media/FocusViewer.d.ts +8 -1
  16. package/dist/react/media/FocusViewer.d.ts.map +1 -1
  17. package/dist/react/media/FocusViewer.js +174 -10
  18. package/dist/react/media/FocusViewer.js.map +1 -1
  19. package/dist/react/media/focusViewer.css +31 -0
  20. package/dist/react/media/gifClip.d.ts +46 -0
  21. package/dist/react/media/gifClip.d.ts.map +1 -0
  22. package/dist/react/media/gifClip.js +74 -0
  23. package/dist/react/media/gifClip.js.map +1 -0
  24. package/dist/react/media/gifEncode.d.ts +46 -0
  25. package/dist/react/media/gifEncode.d.ts.map +1 -0
  26. package/dist/react/media/gifEncode.js +288 -0
  27. package/dist/react/media/gifEncode.js.map +1 -0
  28. package/dist/react/media/gifRecord.d.ts +26 -0
  29. package/dist/react/media/gifRecord.d.ts.map +1 -0
  30. package/dist/react/media/gifRecord.js +93 -0
  31. package/dist/react/media/gifRecord.js.map +1 -0
  32. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  33. package/dist/styles-areas/analytics.css +9 -9
  34. package/dist/styles-areas/data-table.css +10 -10
  35. package/dist/styles-areas/media.css +2 -2
  36. package/package.json +3 -3
  37. package/scripts/guardrailsEnforce.spec.ts +10 -0
  38. package/scripts/guardrailsEnforce.ts +4 -1
  39. package/scripts/styleAreas.ts +3 -1
  40. package/src/react/canvas-hud/CanvasToolbar.tsx +1 -1
  41. package/src/react/components/DataTable/TableFilterBuilder.tsx +9 -13
  42. package/src/react/components/DataTable/tableSystem.spec.tsx +15 -7
  43. package/src/react/components/ListToolbar.tsx +2 -2
  44. package/src/react/hooks/useOutsidePress.spec.tsx +20 -0
  45. package/src/react/hooks/useOutsidePress.ts +17 -2
  46. package/src/react/hooks/useScrollMemory.spec.tsx +45 -2
  47. package/src/react/hooks/useScrollMemory.ts +20 -5
  48. package/src/react/media/FocusViewer.tsx +322 -25
  49. package/src/react/media/focusViewer.css +31 -0
  50. package/src/react/media/gifClip.ts +90 -0
  51. package/src/react/media/gifEncode.spec.ts +213 -0
  52. package/src/react/media/gifEncode.ts +298 -0
  53. package/src/react/media/gifRecord.ts +103 -0
  54. package/src/styles-areas/analytics.css +9 -9
  55. package/src/styles-areas/data-table.css +10 -10
  56. package/src/styles-areas/media.css +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gifEncode.js","sourceRoot":"","sources":["../../../src/react/media/gifEncode.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { type GifSection } from "./gifClip.js";
2
+ export interface RecordGifOptions extends GifSection {
3
+ fps: number;
4
+ /** Requested output width; see {@link gifSize}. */
5
+ width: number;
6
+ /** The source rectangle, in video pixels — what is on screen in a zoomed viewer. Whole frame when omitted. */
7
+ crop?: {
8
+ sx: number;
9
+ sy: number;
10
+ sw: number;
11
+ sh: number;
12
+ } | null;
13
+ /** A CSS filter baked into every frame, the same string the preview shows. */
14
+ filter?: string;
15
+ onProgress?: (done: number, total: number) => void;
16
+ signal?: AbortSignal;
17
+ }
18
+ /**
19
+ * Seek `video` through the section and encode what it shows as a looping GIF.
20
+ *
21
+ * The element is paused for the capture and handed back where it was, paused or playing.
22
+ * Rejects with an `AbortError` when `signal` aborts, and with a sentence when a frame cannot
23
+ * be read (a cross-origin source without CORS taints the canvas) or a seek never lands.
24
+ */
25
+ export declare function recordGifClip(video: HTMLVideoElement, options: RecordGifOptions): Promise<Blob>;
26
+ //# sourceMappingURL=gifRecord.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gifRecord.d.ts","sourceRoot":"","sources":["../../../src/react/media/gifRecord.ts"],"names":[],"mappings":"AASA,OAAO,EAAiB,KAAK,UAAU,EAA0B,MAAM,cAAc,CAAC;AAGtF,MAAM,WAAW,gBAAiB,SAAQ,UAAU;IAClD,GAAG,EAAE,MAAM,CAAC;IACZ,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,8GAA8G;IAC9G,IAAI,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCrG"}
@@ -0,0 +1,93 @@
1
+ // ── Making the GIF: seek, draw, encode ────────────────────────────────────────────────
2
+ // Loaded on demand by the focus viewer (see `gifClip.ts`'s header) — it and `gifEncode.ts` are
3
+ // the bytes of this feature, and a gallery that never makes a GIF should not pay for them.
4
+ //
5
+ // 🔴 Frames are taken by SEEKING the element to each moment, never by playing it and
6
+ // sampling what goes past. Playback drops and repeats frames whenever the machine is busy —
7
+ // and encoding is what keeps it busy — so a played capture stutters exactly as much as the
8
+ // encoder is slow. A seek lands on the frame for that time however long the last one took.
9
+ import { formatGifTime, gifFrameTimes, gifSize } from "./gifClip.js";
10
+ import { GifWriter } from "./gifEncode.js";
11
+ /**
12
+ * Seek `video` through the section and encode what it shows as a looping GIF.
13
+ *
14
+ * The element is paused for the capture and handed back where it was, paused or playing.
15
+ * Rejects with an `AbortError` when `signal` aborts, and with a sentence when a frame cannot
16
+ * be read (a cross-origin source without CORS taints the canvas) or a seek never lands.
17
+ */
18
+ export async function recordGifClip(video, options) {
19
+ if (!video.videoWidth || !video.videoHeight)
20
+ throw new Error("The video has not decoded a frame yet.");
21
+ const crop = options.crop ?? { sx: 0, sy: 0, sw: video.videoWidth, sh: video.videoHeight };
22
+ const size = gifSize(crop, options.width);
23
+ const times = gifFrameTimes(options, options.fps);
24
+ const canvas = document.createElement("canvas");
25
+ canvas.width = size.width;
26
+ canvas.height = size.height;
27
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
28
+ if (!ctx)
29
+ throw new Error("This browser cannot capture a frame.");
30
+ ctx.imageSmoothingEnabled = true;
31
+ ctx.imageSmoothingQuality = "high";
32
+ const was = { at: video.currentTime, playing: !video.paused };
33
+ video.pause();
34
+ const writer = new GifWriter(size.width, size.height);
35
+ try {
36
+ for (const [i, at] of times.entries()) {
37
+ if (options.signal?.aborted)
38
+ throw new DOMException("The GIF was cancelled.", "AbortError");
39
+ await seekForCapture(video, at);
40
+ if (options.filter && "filter" in ctx)
41
+ ctx.filter = options.filter;
42
+ ctx.drawImage(video, crop.sx, crop.sy, crop.sw, crop.sh, 0, 0, size.width, size.height);
43
+ let frame;
44
+ try {
45
+ frame = ctx.getImageData(0, 0, size.width, size.height);
46
+ }
47
+ catch {
48
+ throw new Error("This video's frames cannot be read — it may be cross-origin.");
49
+ }
50
+ writer.addFrame(frame.data, 1000 / options.fps);
51
+ options.onProgress?.(i + 1, times.length);
52
+ }
53
+ return new Blob([writer.finish()], { type: "image/gif" });
54
+ }
55
+ finally {
56
+ video.currentTime = was.at;
57
+ if (was.playing)
58
+ void video.play().catch(() => undefined);
59
+ }
60
+ }
61
+ /** How long one seek may take before the capture gives up and says so. */
62
+ const SEEK_TIMEOUT_MS = 8_000;
63
+ /**
64
+ * Move to `seconds` and wait until that frame is the one a canvas will read.
65
+ *
66
+ * `seeked` says the element has the data; on WebKit the PAINTED frame can still be the old one
67
+ * for a tick after it, so where `requestVideoFrameCallback` exists the capture also waits for
68
+ * the new frame to be presented — bounded, because a seek to the frame already showing
69
+ * presents nothing new.
70
+ */
71
+ async function seekForCapture(video, seconds) {
72
+ if (Math.abs(video.currentTime - seconds) < 1e-4 && !video.seeking && video.readyState >= 2)
73
+ return;
74
+ await new Promise((resolve, reject) => {
75
+ const timer = setTimeout(() => {
76
+ video.removeEventListener("seeked", done);
77
+ reject(new Error(`The video stopped answering at ${formatGifTime(seconds)} — try a shorter section.`));
78
+ }, SEEK_TIMEOUT_MS);
79
+ function done() {
80
+ clearTimeout(timer);
81
+ resolve();
82
+ }
83
+ video.addEventListener("seeked", done, { once: true });
84
+ video.currentTime = seconds;
85
+ });
86
+ const presented = video.requestVideoFrameCallback;
87
+ if (typeof presented === "function")
88
+ await Promise.race([
89
+ new Promise((resolve) => presented.call(video, () => resolve())),
90
+ new Promise((resolve) => setTimeout(resolve, 150)),
91
+ ]);
92
+ }
93
+ //# sourceMappingURL=gifRecord.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gifRecord.js","sourceRoot":"","sources":["../../../src/react/media/gifRecord.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"guardrailsEnforce.d.ts","sourceRoot":"","sources":["../../scripts/guardrailsEnforce.ts"],"names":[],"mappings":";AA6PA,UAAU,SAAS;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;;;sEAGkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,IAAI;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;CACzD;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CA6B9D;AAgwCD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAE3D;AAoHD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAIpE;AAg4BD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,CAEpF;AAID,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAoBhE;AAyKD,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE;AAqDD,UAAU,MAAM;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AA0BD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBpD;AAID,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5E;AA0FD,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAiCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAiE,CAAC;AAEtH,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,SAAS,MAAM,EAAwE,CAAC;AAShH,eAAO,MAAM,uBAAuB,aAAkD,CAAC;AAGvF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA+C3F;AAED,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAmGhC,wBAAgB,0BAA0B,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,EAAE,CAyBxF"}
1
+ {"version":3,"file":"guardrailsEnforce.d.ts","sourceRoot":"","sources":["../../scripts/guardrailsEnforce.ts"],"names":[],"mappings":";AA6PA,UAAU,SAAS;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;;;sEAGkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,IAAI;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;CACzD;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CA6B9D;AAgwCD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAE3D;AAoHD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAIpE;AAm4BD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,CAEpF;AAID,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAoBhE;AAyKD,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE;AAqDD,UAAU,MAAM;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AA0BD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBpD;AAID,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5E;AA0FD,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAiCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAiE,CAAC;AAEtH,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,SAAS,MAAM,EAAwE,CAAC;AAShH,eAAO,MAAM,uBAAuB,aAAkD,CAAC;AAGvF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA+C3F;AAED,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAmGhC,wBAAgB,0BAA0B,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,EAAE,CAyBxF"}
@@ -18,7 +18,7 @@
18
18
  * after an app's responsive variant (task 2124). An app that imports every area gets exactly
19
19
  * what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
20
20
  * src/stylesAreas.spec.ts, which is also what fails when this file is stale.
21
- * 155 candidate(s).
21
+ * 153 candidate(s).
22
22
  *
23
23
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
24
24
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -31,13 +31,13 @@
31
31
  @source inline("@max-4xl:p-0! @max-4xl:text-[length:0] @max-4xl:w-0! @max-6xl:[&>*]:hidden @max-6xl:border-0");
32
32
  @source inline("@max-6xl:invisible @max-6xl:min-w-0! @max-6xl:p-0! @max-6xl:text-[length:0] @max-6xl:w-0!");
33
33
  @source inline("@max-xl:contents @xl:basis-auto @xl:block @xl:hidden [&>*]:hidden [&>*]:max-w-none");
34
- @source inline("[&>svg]:size-3.5 [&_svg]:size-3 [overflow-wrap:anywhere] active:cursor-grabbing appearance-none");
35
- @source inline("basis basis-full bg-[var(--cb-frozen-bg)] bg-card/95 bg-gradient-to-t bg-muted/30 bg-muted/50");
36
- @source inline("border-accent-solid border-accent/60 border-field-error border-solid border-t-2 col collapse");
37
- @source inline("cursor-grab data-[ending-style]:opacity-0 data-[ending-style]:scale-95");
38
- @source inline("data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong");
39
- @source inline("data-[starting-style]:opacity-0 data-[starting-style]:scale-95 disabled:opacity-40 duration-100");
40
- @source inline("fade-in focus-visible:ring-field-ring/40 focus-within:border-field-ring focus-within:ring-2");
34
+ @source inline("[&>svg]:size-3.5 [&_svg]:size-3 [overflow-wrap:anywhere] active:cursor-grabbing basis basis-full");
35
+ @source inline("bg-[var(--cb-frozen-bg)] bg-card/95 bg-gradient-to-t bg-muted/30 bg-muted/50 border-accent-solid");
36
+ @source inline("border-accent/60 border-field-error border-solid border-t-2 col collapse cursor-grab");
37
+ @source inline("data-[ending-style]:opacity-0 data-[ending-style]:scale-95 data-[highlighted]:bg-accent/10");
38
+ @source inline("data-[highlighted]:text-accent-strong data-[starting-style]:opacity-0");
39
+ @source inline("data-[starting-style]:scale-95 disabled:opacity-40 duration-100 fade-in");
40
+ @source inline("focus-visible:ring-field-ring/40 focus-within:border-field-ring focus-within:ring-2");
41
41
  @source inline("focus-within:ring-field-ring/30 from-card gap-x-6 h-105");
42
42
  @source inline("has-[[aria-invalid=true]]:border-field-error hover:bg-background hover:bg-muted/40");
43
43
  @source inline("hover:border-field-ring hover:text-warning italic justify-self-end last:border-0 left-11");
@@ -45,7 +45,7 @@
45
45
  @source inline("max-sm:min-w-24 max-sm:min-w-32 max-sm:ml-13.5 max-sm:w-auto max-w-32 max-w-36 max-w-45 max-w-48");
46
46
  @source inline("max-w-60 min-h-20 min-h-dvh min-w-0! min-w-20 min-w-24 min-w-52 min-w-[var(--anchor-width)]");
47
47
  @source inline("min-w-max ml-1.5 mr-2 mt-1.5 mt-3 mt-4 not-italic ordinal p-0! pb-1.5 pl-1.5 pl-2 pr-0.5 pr-1");
48
- @source inline("pr-10 pr-16 pr-3 pr-9 pt-1 resize-none resize-y right-3 ring-field-error/30 self self-stretch");
48
+ @source inline("pr-10 pr-16 pr-3 pt-1 resize-none resize-y right-3 ring-field-error/30 self self-stretch");
49
49
  @source inline("shadow-[1px_0_0_0_var(--color-border)] shrink-[2] size-7 slide-in-from-top-1 table-fixed");
50
50
  @source inline("text-[length:0] text-accent-solid-foreground text-field-placeholder text-success text-xl");
51
51
  @source inline("to-transparent top-3 transition-[transform,opacity] w-0 w-0! w-11 w-12 w-20 w-32 w-36 w-40 w-88");
@@ -18,7 +18,7 @@
18
18
  * after an app's responsive variant (task 2124). An app that imports every area gets exactly
19
19
  * what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
20
20
  * src/stylesAreas.spec.ts, which is also what fails when this file is stale.
21
- * 150 candidate(s).
21
+ * 148 candidate(s).
22
22
  *
23
23
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
24
24
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -31,21 +31,21 @@
31
31
  @source inline("@max-4xl:p-0! @max-4xl:text-[length:0] @max-4xl:w-0! @max-6xl:[&>*]:hidden @max-6xl:border-0");
32
32
  @source inline("@max-6xl:invisible @max-6xl:min-w-0! @max-6xl:p-0! @max-6xl:text-[length:0] @max-6xl:w-0!");
33
33
  @source inline("@max-xl:contents @xl:basis-auto @xl:block @xl:hidden [&>*]:hidden [&>*]:max-w-none");
34
- @source inline("[&>svg]:size-3.5 [&_svg]:size-3 [overflow-wrap:anywhere] active:cursor-grabbing appearance-none");
35
- @source inline("basis-full bg-[var(--cb-frozen-bg)] bg-card/95 bg-gradient-to-t bg-muted/30 bg-muted/50");
36
- @source inline("border-accent-solid border-accent/60 border-field-error border-solid border-t-2 col collapse");
37
- @source inline("cursor-grab data-[ending-style]:opacity-0 data-[ending-style]:scale-95");
38
- @source inline("data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong");
39
- @source inline("data-[starting-style]:opacity-0 data-[starting-style]:scale-95 disabled:opacity-40 duration-100");
40
- @source inline("fade-in focus-visible:ring-field-ring/40 focus-within:border-field-ring focus-within:ring-2");
34
+ @source inline("[&>svg]:size-3.5 [&_svg]:size-3 [overflow-wrap:anywhere] active:cursor-grabbing basis-full");
35
+ @source inline("bg-[var(--cb-frozen-bg)] bg-card/95 bg-gradient-to-t bg-muted/30 bg-muted/50 border-accent-solid");
36
+ @source inline("border-accent/60 border-field-error border-solid border-t-2 col collapse cursor-grab");
37
+ @source inline("data-[ending-style]:opacity-0 data-[ending-style]:scale-95 data-[highlighted]:bg-accent/10");
38
+ @source inline("data-[highlighted]:text-accent-strong data-[starting-style]:opacity-0");
39
+ @source inline("data-[starting-style]:scale-95 disabled:opacity-40 duration-100 fade-in");
40
+ @source inline("focus-visible:ring-field-ring/40 focus-within:border-field-ring focus-within:ring-2");
41
41
  @source inline("focus-within:ring-field-ring/30 from-card gap-x-6 h-105");
42
42
  @source inline("has-[[aria-invalid=true]]:border-field-error hover:bg-background hover:bg-muted/40");
43
43
  @source inline("hover:border-field-ring hover:text-warning justify-self-end last:border-0 left-11 left-2.5");
44
44
  @source inline("list-none max-h-64 max-h-88 max-sm:flex-1 max-sm:flex-wrap max-sm:min-w-0 max-sm:min-w-24");
45
45
  @source inline("max-sm:min-w-32 max-sm:ml-13.5 max-sm:w-auto max-w-32 max-w-36 max-w-45 max-w-48 max-w-60");
46
46
  @source inline("min-h-20 min-h-dvh min-w-0! min-w-24 min-w-52 min-w-[var(--anchor-width)] min-w-max ml-1.5 mr-2");
47
- @source inline("mt-1.5 mt-3 mt-4 not-italic ordinal p-0! pb-1.5 pl-1.5 pl-2 pr-0.5 pr-1 pr-10 pr-16 pr-3 pr-9");
48
- @source inline("pt-1 resize-none resize-y right-3 ring-field-error/30 self self-stretch");
47
+ @source inline("mt-1.5 mt-3 mt-4 not-italic ordinal p-0! pb-1.5 pl-1.5 pl-2 pr-0.5 pr-1 pr-10 pr-16 pr-3 pt-1");
48
+ @source inline("resize-none resize-y right-3 ring-field-error/30 self self-stretch");
49
49
  @source inline("shadow-[1px_0_0_0_var(--color-border)] size-7 slide-in-from-top-1 table-fixed text-[length:0]");
50
50
  @source inline("text-accent-solid-foreground text-field-placeholder text-success text-xl to-transparent top-3");
51
51
  @source inline("transition-[transform,opacity] w-0! w-11 w-12 w-20 w-32 w-36 w-40 w-88");
@@ -18,7 +18,7 @@
18
18
  * after an app's responsive variant (task 2124). An app that imports every area gets exactly
19
19
  * what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
20
20
  * src/stylesAreas.spec.ts, which is also what fails when this file is stale.
21
- * 80 candidate(s).
21
+ * 78 candidate(s).
22
22
  *
23
23
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
24
24
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -32,7 +32,7 @@
32
32
  @source inline("focus-within:opacity-100 from-black/80 g grayscale group-focus-within:opacity-100 h-11");
33
33
  @source inline("hover:bg-black/75 hover:bg-destructive hover:bg-white/20 hover:border-accent-solid invert left-1");
34
34
  @source inline("max-h-[40vh] max-h-[var(--media-max-h)] max-h-full max-w-50 min-h-64 min-h-dvh min-w-10 min-w-28");
35
- @source inline("min-w-40 min-w-48 o op opacity-100 opacity-80 pr-9 pt-8 right-1 right-1.5 right-3 sh");
35
+ @source inline("min-w-40 min-w-48 o op opacity-100 opacity-80 pt-8 right-1 right-1.5 sh");
36
36
  @source inline("shadow-[0_0_0_1px_rgba(0,0,0,0.55)] size-7 size-9 sm:gap-3 sm:grid-cols-3");
37
37
  @source inline("text-[color:var(--accent)] text-xl to-transparent top-1 top-1.5 translate translate-x-px w-12");
38
38
  @source inline("w-3 zoom zoom-in");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.77.0",
3
+ "version": "5.78.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
@@ -75,7 +75,7 @@
75
75
  "demo:check": "tsc --noEmit --project demo/tsconfig.json",
76
76
  "demo:paths": "bun run demo/generatePaths.ts --write",
77
77
  "demo:coverage": "bun run scripts/demoCoverage.ts",
78
- "guardrails:report": "bun run scripts/guardrailsEnforce.ts no-raw-action-button,raw-button-resets-face,no-raw-overflow,no-raw-inputs,no-hardcoded-colors,no-raw-modal-footer,no-unguarded-editing-modal,control-height-tokens,no-raw-zindex,no-raw-data-table,no-hand-rolled-property-list,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-dialog,no-fast-tooltip,no-raw-password,no-raw-lazy,no-raw-popover,no-hand-rolled-scroll-memory,no-hand-rolled-surface,no-inline-style,no-arbitrary-length,opt-out-count,opt-out-needs-cause --scope cursedbelt src/react src/styles.css",
78
+ "guardrails:report": "bun run scripts/guardrailsEnforce.ts no-raw-action-button,raw-button-resets-face,no-raw-overflow,no-raw-inputs,no-hardcoded-colors,no-raw-modal-footer,no-unguarded-editing-modal,control-height-tokens,no-raw-zindex,no-raw-data-table,no-hand-rolled-property-list,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-select,no-native-dialog,no-fast-tooltip,no-raw-password,no-raw-lazy,no-raw-popover,no-hand-rolled-scroll-memory,no-hand-rolled-surface,no-inline-style,no-arbitrary-length,opt-out-count,opt-out-needs-cause --scope cursedbelt src/react src/styles.css",
79
79
  "lint": "biome lint --error-on-warnings src e2e demo scripts",
80
80
  "styles": "bun run styles:static && bun run styles:utilities && bun run styles:areas",
81
81
  "styles:static": "bun run scripts/generateStaticStyles.ts",
@@ -922,7 +922,7 @@
922
922
  "@vidstack/react": "1.15.6",
923
923
  "@vitejs/plugin-react": "^5.2.0",
924
924
  "cursedbelt-server": "^4.43.1",
925
- "cursedops": "^0.15.0",
925
+ "cursedops": "^0.16.1",
926
926
  "diff": "^9.0.0",
927
927
  "emoji-mart": "^5.6.0",
928
928
  "hls.js": "^1.5.0",
@@ -1064,6 +1064,16 @@ describe('no-native-select — the OS draws its list (owner, 2026-10-10)', () =>
1064
1064
  <NativeSelect value={relation} onChange={pick} />`;
1065
1065
  expect(check('no-native-select', source)).toHaveLength(0);
1066
1066
  });
1067
+
1068
+ // forge task 3063: the rule used to skip all of cursedbelt's components, so the kit's own
1069
+ // DataTable filter builder drew OS menus into apps whose count read 0.
1070
+ test('holds the kit itself to it — only NativeSelect.tsx may render one', () => {
1071
+ const source = `<NativeSelect size='sm' aria-label='Column' />`;
1072
+ const rule = RULES['no-native-select'];
1073
+ expect(rule.check('libs/cursedbelt/src/react/components/DataTable/TableFilterBuilder.tsx', source)).toHaveLength(1);
1074
+ expect(rule.check('libs/cursedbelt/src/react/media/FocusViewer.tsx', source)).toHaveLength(1);
1075
+ expect(rule.check('libs/cursedbelt/src/react/components/NativeSelect.tsx', source)).toHaveLength(0);
1076
+ });
1067
1077
  });
1068
1078
 
1069
1079
  describe('typed-field-outside-a-form — Enter submits (owner, 2026-10-11; forge task 3043)', () => {
@@ -2054,7 +2054,10 @@ const noNativeSelect: Rule = {
2054
2054
  "Owner, 2026-10-10: unstyled dropdowns break the app's cohesiveness. A `NativeSelect`'s list is drawn by the OS, outside the page, where no stylesheet reaches it. Use `SelectField` / `ComboboxField` for a form field, `PropertyPicker` for a record's property (Type, Folder, Tags), or `ChoiceMenu` to choose-and-act. Where the OS picker is the better control, say why on the line above: `// guardrails-ignore no-native-select: <why>`.",
2055
2055
  check(filePath, source) {
2056
2056
  const unix = filePath.replace(/\\/g, "/");
2057
- if (/(^|\/)(?:packages|libs)\/cursedbelt\/src\/react\/components\//.test(unix)) return [];
2057
+ // The kit is held to its own law (forge task 3063: its DataTable filter builder and
2058
+ // FocusViewer drew OS menus into every app whose count read 0). Only the control itself
2059
+ // may render one.
2060
+ if (/(^|\/)(?:packages|libs)\/cursedbelt\/src\/react\/components\/NativeSelect\.tsx$/.test(unix)) return [];
2058
2061
  if (/\.(?:spec|test)\.[tj]sx?$/.test(unix)) return [];
2059
2062
  const scannable = blankComments(source);
2060
2063
  const lines = source.split("\n");
@@ -299,7 +299,9 @@ export const AREAS: readonly Area[] = [
299
299
  {
300
300
  name: 'media',
301
301
  why: 'the video/audio player, the upload client and the player strip',
302
- roots: ['media', 'media/FrameGrabber.tsx', 'media/poster.ts'],
302
+ // `media/gifRecord.ts` is the focus viewer's `import()`ed GIF recorder (it draws nothing;
303
+ // it is named so the closure walk, which reads static specifiers only, finds it at all).
304
+ roots: ['media', 'media/FrameGrabber.tsx', 'media/poster.ts', 'media/gifRecord.ts'],
303
305
  },
304
306
  {
305
307
  name: 'media-gallery',
@@ -6,7 +6,7 @@ export interface CanvasToolbarProps {
6
6
  /** The left half: what the canvas shows (a reach, a layer switch). Hidden while the bar is
7
7
  * under 768px wide when `compactFilters` is given — five stops beside the tools would scroll out of a thumb's sight. */
8
8
  filters?: ReactNode;
9
- /** The same choice as a phone wants it, usually the OS picker (`NativeSelect`): shown only while
9
+ /** The same choice as a phone wants it, usually one `SelectField`: shown only while
10
10
  * the bar is under 768px wide, on row two beside the legend. */
11
11
  compactFilters?: ReactNode;
12
12
  /** A key or chip strip (houses, layers, tags): ONE line that scrolls inside itself, its ends
@@ -3,8 +3,8 @@ import { useState } from 'react';
3
3
  import { cn } from '../../lib/cn.js';
4
4
  import { Button } from '../Button.js';
5
5
  import { Input } from '../Input.js';
6
- import { NativeSelect } from '../NativeSelect.js';
7
6
  import { Popover } from '../Popover.js';
7
+ import { SelectField } from '../SelectField.js';
8
8
  import {
9
9
  countActiveConditions,
10
10
  describeFilterQuery,
@@ -181,31 +181,27 @@ export function TableFilterBuilder({
181
181
  <span className='w-12 shrink-0 text-right text-caption font-semibold uppercase tracking-wide text-muted-foreground'>
182
182
  {conditionIndex === 0 ? 'Where' : 'and'}
183
183
  </span>
184
- <NativeSelect
184
+ <SelectField
185
185
  size='sm'
186
186
  aria-label='Column'
187
- className='min-w-0 flex-1'
188
- wrapperClassName='min-w-0 flex-1 max-sm:min-w-24'
187
+ className='min-w-0 flex-1 max-sm:min-w-24'
189
188
  value={condition.column}
190
189
  options={columns.map((c) => ({ value: c.key, label: c.label }))}
191
- onChange={(e) =>
190
+ onValueChange={(column) =>
192
191
  patchGroup(group.id, (list) =>
193
- list.map((c) => (c.id === condition.id ? { ...c, column: e.target.value } : c)),
192
+ list.map((c) => (c.id === condition.id ? { ...c, column } : c)),
194
193
  )
195
194
  }
196
195
  />
197
- <NativeSelect
196
+ <SelectField
198
197
  size='sm'
199
198
  aria-label='Condition'
200
- className='min-w-0'
201
- wrapperClassName='w-40 shrink-0 max-sm:w-auto max-sm:min-w-32 max-sm:flex-1'
199
+ className='w-40 min-w-0 shrink-0 max-sm:w-auto max-sm:min-w-32 max-sm:flex-1'
202
200
  value={condition.op}
203
201
  options={OP_OPTIONS}
204
- onChange={(e) =>
202
+ onValueChange={(op) =>
205
203
  patchGroup(group.id, (list) =>
206
- list.map((c) =>
207
- c.id === condition.id ? { ...c, op: e.target.value as FilterOp } : c,
208
- ),
204
+ list.map((c) => (c.id === condition.id ? { ...c, op: op as FilterOp } : c)),
209
205
  )
210
206
  }
211
207
  />
@@ -238,6 +238,14 @@ describe('search AND the filter builder AND the sort stack compose', () => {
238
238
  });
239
239
 
240
240
  describe('the filter builder, driven the way a person drives it', () => {
241
+ /** Picks `label` in one of the builder's `SelectField`s as a keyboard does: open the trigger,
242
+ * then take the option. Its Column and Condition were OS menus until forge task 3063. */
243
+ const choose = (trigger: HTMLElement, label: string) => {
244
+ expect(trigger.getAttribute('role')).toBe('combobox');
245
+ fireEvent.keyDown(trigger, { key: 'Enter' });
246
+ fireEvent.click(screen.getByRole('option', { name: label }));
247
+ };
248
+
241
249
  /**
242
250
  * 🔴 The acceptance test for item 3 is the owner's own last clause — *"easy
243
251
  * enough that I don't give up on trying it"* — so this walks the WHOLE
@@ -255,22 +263,22 @@ describe('the filter builder, driven the way a person drives it', () => {
255
263
  // A condition with no value yet must NOT empty the table.
256
264
  expect(bodyTitles()).toHaveLength(4);
257
265
 
258
- const columnSelects = () => screen.getAllByLabelText('Column') as HTMLSelectElement[];
259
- fireEvent.change(columnSelects()[0], { target: { value: 'artist' } });
260
- fireEvent.change(screen.getAllByLabelText('Condition')[0], { target: { value: 'equals' } });
266
+ const columnSelects = () => screen.getAllByLabelText('Column');
267
+ choose(columnSelects()[0], 'Artist');
268
+ choose(screen.getAllByLabelText('Condition')[0], 'is');
261
269
  fireEvent.change(screen.getAllByLabelText('Value')[0], { target: { value: 'alpha' } });
262
270
  expect(bodyTitles()).toEqual(['bravo', 'delta']);
263
271
 
264
272
  // AND: a second condition inside the same group.
265
273
  fireEvent.click(screen.getByRole('button', { name: /^And$/ }));
266
- fireEvent.change(columnSelects()[1], { target: { value: 'title' } });
274
+ choose(columnSelects()[1], 'Title');
267
275
  fireEvent.change(screen.getAllByLabelText('Value')[1], { target: { value: 'bra' } });
268
276
  expect(bodyTitles()).toEqual(['bravo']);
269
277
 
270
278
  // OR: a second GROUP.
271
279
  fireEvent.click(screen.getByRole('button', { name: /Or…/ }));
272
- fireEvent.change(columnSelects()[2], { target: { value: 'artist' } });
273
- fireEvent.change(screen.getAllByLabelText('Condition')[2], { target: { value: 'equals' } });
280
+ choose(columnSelects()[2], 'Artist');
281
+ choose(screen.getAllByLabelText('Condition')[2], 'is');
274
282
  fireEvent.change(screen.getAllByLabelText('Value')[2], { target: { value: 'beta' } });
275
283
  expect(bodyTitles()).toEqual(['alpha', 'bravo', 'charlie']);
276
284
 
@@ -297,7 +305,7 @@ describe('the filter builder, driven the way a person drives it', () => {
297
305
  render(<DataTable columns={COLUMNS} data={TRACKS} getRowId={(r) => r.id} hidePagination />);
298
306
  fireEvent.click(screen.getByRole('button', { name: /^Filter/ }));
299
307
  fireEvent.click(screen.getByRole('button', { name: /Add a condition/ }));
300
- fireEvent.change(screen.getAllByLabelText('Condition')[0], { target: { value: 'isEmpty' } });
308
+ choose(screen.getAllByLabelText('Condition')[0], 'is empty');
301
309
  expect(screen.queryAllByLabelText('Value')).toHaveLength(0);
302
310
  // Every fixture row has a title, so "title is empty" keeps nothing.
303
311
  expect(bodyTitles()).toHaveLength(0);
@@ -34,7 +34,7 @@ export interface ListToolbarProps {
34
34
  chipsAllLabel?: string;
35
35
  /** Any further FILTER controls — they sit left, after the chips. */
36
36
  filters?: ReactNode;
37
- /** The sort control (a `NativeSelect`, a `ChoiceMenu`, …). Right-hand cluster. */
37
+ /** The sort control (a `SelectField`, a `ChoiceMenu`, …). Right-hand cluster. */
38
38
  sort?: ReactNode;
39
39
  /** The density/view-mode control — normally a `TableViewToggle`. Right-hand cluster. */
40
40
  view?: ReactNode;
@@ -95,7 +95,7 @@ export interface ListToolbarProps {
95
95
  * chips={buckets}
96
96
  * chipValue={filter}
97
97
  * onChipChange={(v) => setFilter(typeof v === 'string' ? v : null)}
98
- * sort={<NativeSelect size='sm' aria-label='Sort the queue' … />}
98
+ * sort={<SelectField size='sm' aria-label='Sort the queue' … />}
99
99
  * view={<TableViewToggle value={view.mode} onChange={view.setMode} />}
100
100
  * />
101
101
  */
@@ -110,6 +110,26 @@ describe('useOutsidePress', () => {
110
110
  expect(isOpen()).toBe(true);
111
111
  });
112
112
 
113
+ /**
114
+ * 🔴 Forge task 3064: a Radix Select opens on the POINTERDOWN and makes the page inert, so the
115
+ * same press's pointerup — and the click it makes — land on `<html>`. Read as a press outside,
116
+ * that click shut the filter builder's popover the moment its Column picker opened (music's
117
+ * browser test, cursedbelt 5.77.1). A click is the end of the press that began it: one that
118
+ * began inside is inside, wherever the release fell.
119
+ */
120
+ test('🔴 a press that begins inside and is released outside keeps it open', async () => {
121
+ render(<Panel />);
122
+ await press(screen.getByRole('button', { name: 'folders' }));
123
+ fireEvent.pointerDown(screen.getByRole('button', { name: 'open nested' }));
124
+ fireEvent.click(document.documentElement);
125
+ await act(() => new Promise((r) => setTimeout(r, 5)));
126
+ expect(isOpen()).toBe(true);
127
+ // The next press, begun outside, still closes it.
128
+ fireEvent.pointerDown(screen.getByRole('button', { name: 'row' }));
129
+ await press(screen.getByRole('button', { name: 'row' }));
130
+ expect(isOpen()).toBe(false);
131
+ });
132
+
113
133
  test('Escape closes it', async () => {
114
134
  render(<Panel />);
115
135
  await press(screen.getByRole('button', { name: 'folders' }));
@@ -125,8 +125,23 @@ export function useOutsidePress({
125
125
  if (outside(target)) swallowUntil = Date.now() + 1000;
126
126
  decideLater(target);
127
127
  };
128
+ // `click` mode: where the press BEGAN. A Radix Select opens on the pointerdown and makes the
129
+ // page inert, so that same press's release — and the click it makes — land on `<html>`; read
130
+ // as outside, it shut the filter builder the moment its Column picker opened (forge task
131
+ // 3064). A click whose press began inside (in the DOM, or this panel's React tree — the
132
+ // panel's `onPointerDownCapture` runs after this capture listener) is inside.
133
+ let pressBegan: Node | null = null;
134
+ const onPressBegins = (event: PointerEvent) => {
135
+ pressInsideRef.current = false;
136
+ pressBegan = event.target instanceof Node ? event.target : null;
137
+ };
128
138
  const onClick = (event: MouseEvent) => {
129
139
  const target = event.target instanceof Node ? event.target : null;
140
+ if (decideOn === 'click') {
141
+ const began = pressBegan;
142
+ pressBegan = null;
143
+ if (began && (pressInsideRef.current || isInside(began) || onAnotherLayer(began))) return;
144
+ }
130
145
  if (decideOn === 'pointerdown') {
131
146
  if (Date.now() < swallowUntil) {
132
147
  swallowUntil = 0;
@@ -149,11 +164,11 @@ export function useOutsidePress({
149
164
  };
150
165
 
151
166
  document.addEventListener('click', onClick, true);
152
- if (decideOn === 'pointerdown') document.addEventListener('pointerdown', onPointerDown, true);
167
+ document.addEventListener('pointerdown', decideOn === 'pointerdown' ? onPointerDown : onPressBegins, true);
153
168
  if (closeOnEscape) document.addEventListener('keydown', onKeyDown);
154
169
  return () => {
155
170
  document.removeEventListener('click', onClick, true);
156
- document.removeEventListener('pointerdown', onPointerDown, true);
171
+ document.removeEventListener('pointerdown', decideOn === 'pointerdown' ? onPointerDown : onPressBegins, true);
157
172
  document.removeEventListener('keydown', onKeyDown);
158
173
  for (const timer of timers) clearTimeout(timer);
159
174
  };
@@ -41,8 +41,8 @@ function fakeScroller(height: number, viewport = 800) {
41
41
  };
42
42
  }
43
43
 
44
- function Shell({ at, el, enabled }: { at: string; el: HTMLElement; enabled?: boolean }) {
45
- useScrollMemory(at, { scroller: () => el, enabled });
44
+ function Shell({ at, el, enabled, deadlineMs }: { at: string; el: HTMLElement; enabled?: boolean; deadlineMs?: number }) {
45
+ useScrollMemory(at, { scroller: () => el, enabled, deadlineMs });
46
46
  return null;
47
47
  }
48
48
 
@@ -103,6 +103,49 @@ describe('useScrollMemory', () => {
103
103
  expect(s.el.scrollTop).toBe(15_000);
104
104
  });
105
105
 
106
+ /**
107
+ * 🔴 forge task 3069: flix's phone grid, reloaded, came back to 29,731px instead of 30,000 — its
108
+ * pages were still arriving one round trip at a time when the 3s deadline, counted from the
109
+ * START, ran out. A page that is still growing is still being followed.
110
+ */
111
+ test('a list growing page by page is followed past the deadline, as long as each page lands within it', async () => {
112
+ const s = fakeScroller(20_000);
113
+ const { rerender } = render(<Shell at='/' el={s.el} deadlineMs={60} />);
114
+ s.scrollTo(15_000);
115
+ const list = { state: window.history.state, path: '/' };
116
+ push('/item/7');
117
+ rerender(<Shell at='/item/7' el={s.el} deadlineMs={60} />);
118
+
119
+ s.grow(3_000);
120
+ back(list);
121
+ rerender(<Shell at='/' el={s.el} deadlineMs={60} />);
122
+ // Five pages, 40ms apart: 200ms in all — well past 60ms from the start, never 60ms without a page.
123
+ for (const height of [6_000, 9_000, 12_000, 15_000, 20_000]) {
124
+ await act(() => Bun.sleep(40));
125
+ s.grow(height);
126
+ }
127
+ await frames(2);
128
+ expect(s.el.scrollTop).toBe(15_000);
129
+ });
130
+
131
+ test('a list that STOPS growing is given up on once the deadline passes with no new page', async () => {
132
+ const s = fakeScroller(20_000);
133
+ const { rerender } = render(<Shell at='/' el={s.el} deadlineMs={60} />);
134
+ s.scrollTo(15_000);
135
+ const list = { state: window.history.state, path: '/' };
136
+ push('/item/7');
137
+ rerender(<Shell at='/item/7' el={s.el} deadlineMs={60} />);
138
+
139
+ s.grow(3_000);
140
+ back(list);
141
+ rerender(<Shell at='/' el={s.el} deadlineMs={60} />);
142
+ await act(() => Bun.sleep(150));
143
+ await frames(2);
144
+ s.grow(20_000); // too late: the restore settled where the page ended
145
+ await frames(3);
146
+ expect(s.el.scrollTop).toBe(2_200);
147
+ });
148
+
106
149
  test('the viewer taking the page back cancels a restore that is still waiting', async () => {
107
150
  const s = fakeScroller(20_000);
108
151
  const { rerender } = render(<Shell at='/' el={s.el} />);