cursedbelt 5.96.1 → 5.97.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 (57) hide show
  1. package/dist/react/components/PaneSplit.d.ts +11 -2
  2. package/dist/react/components/PaneSplit.d.ts.map +1 -1
  3. package/dist/react/components/PaneSplit.js +12 -11
  4. package/dist/react/components/PaneSplit.js.map +1 -1
  5. package/dist/react/components/PanelGrid.d.ts +14 -8
  6. package/dist/react/components/PanelGrid.d.ts.map +1 -1
  7. package/dist/react/components/PanelGrid.js +10 -3
  8. package/dist/react/components/PanelGrid.js.map +1 -1
  9. package/dist/react/components/SplitPane.d.ts +3 -9
  10. package/dist/react/components/SplitPane.d.ts.map +1 -1
  11. package/dist/react/components/SplitPane.js +0 -3
  12. package/dist/react/components/SplitPane.js.map +1 -1
  13. package/dist/react/components/paneSplit.css +8 -0
  14. package/dist/react/filter-rail/railResize.d.ts +16 -5
  15. package/dist/react/filter-rail/railResize.d.ts.map +1 -1
  16. package/dist/react/filter-rail/railResize.js +27 -9
  17. package/dist/react/filter-rail/railResize.js.map +1 -1
  18. package/dist/react/filter-rail/railWidth.d.ts +10 -5
  19. package/dist/react/filter-rail/railWidth.d.ts.map +1 -1
  20. package/dist/react/filter-rail/railWidth.js +13 -8
  21. package/dist/react/filter-rail/railWidth.js.map +1 -1
  22. package/dist/react/index.d.ts +1 -0
  23. package/dist/react/index.d.ts.map +1 -1
  24. package/dist/react/index.js +1 -0
  25. package/dist/react/index.js.map +1 -1
  26. package/dist/react/lib/paneReach.d.ts +36 -0
  27. package/dist/react/lib/paneReach.d.ts.map +1 -0
  28. package/dist/react/lib/paneReach.js +72 -0
  29. package/dist/react/lib/paneReach.js.map +1 -0
  30. package/dist/react/master-detail/MasterDetail.d.ts +6 -3
  31. package/dist/react/master-detail/MasterDetail.d.ts.map +1 -1
  32. package/dist/react/master-detail/MasterDetail.js +3 -2
  33. package/dist/react/master-detail/MasterDetail.js.map +1 -1
  34. package/dist/react/media/videoEnhance.d.ts +1 -1
  35. package/dist/react/media/videoEnhance.js +1 -1
  36. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  37. package/dist/testing/phoneRules.js +3 -3
  38. package/dist/testing/phoneRules.js.map +1 -1
  39. package/package.json +2 -1
  40. package/scripts/guardrailsEnforce.spec.ts +25 -0
  41. package/scripts/guardrailsEnforce.ts +61 -0
  42. package/src/react/components/PaneSplit.spec.tsx +26 -3
  43. package/src/react/components/PaneSplit.tsx +26 -13
  44. package/src/react/components/PanelGrid.tsx +16 -11
  45. package/src/react/components/SplitPane.spec.tsx +10 -5
  46. package/src/react/components/SplitPane.tsx +3 -12
  47. package/src/react/components/paneSplit.css +8 -0
  48. package/src/react/filter-rail/FilterRail.spec.tsx +7 -3
  49. package/src/react/filter-rail/railResize.spec.tsx +27 -5
  50. package/src/react/filter-rail/railResize.tsx +46 -15
  51. package/src/react/filter-rail/railWidth.ts +13 -8
  52. package/src/react/index.ts +1 -0
  53. package/src/react/lib/paneReach.spec.ts +58 -0
  54. package/src/react/lib/paneReach.ts +65 -0
  55. package/src/react/master-detail/MasterDetail.tsx +10 -5
  56. package/src/react/media/videoEnhance.ts +1 -1
  57. package/src/testing/phoneRules.ts +2 -2
@@ -0,0 +1,58 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { reachSize, stepSize } from './paneReach.js';
3
+
4
+ // The owner, 2026-10-27: every divider goes as far as the hand takes it, shutting a pane included.
5
+ describe('reachSize — where a pointer drag lands', () => {
6
+ test('has no maximum: the first pane can take the whole axis', () => {
7
+ expect(reachSize(1000, 160, 1000)).toBe(1000);
8
+ expect(reachSize(1400, 160, 1000)).toBe(1000);
9
+ expect(reachSize(0.99, 0.12, 1)).toBe(1);
10
+ });
11
+
12
+ test('shuts a pane dragged below half its minimum, either side of the seam', () => {
13
+ expect(reachSize(79, 160, 1000)).toBe(0);
14
+ expect(reachSize(-40, 160, 1000)).toBe(0);
15
+ expect(reachSize(921, 160, 1000)).toBe(1000);
16
+ });
17
+
18
+ test('holds at the minimum between half and all of it, so no pane is a sliver', () => {
19
+ expect(reachSize(100, 160, 1000)).toBe(160);
20
+ expect(reachSize(900, 160, 1000)).toBe(840);
21
+ });
22
+
23
+ test('passes every size between the two minimums through untouched', () => {
24
+ expect(reachSize(500, 160, 1000)).toBe(500);
25
+ expect(reachSize(160, 160, 1000)).toBe(160);
26
+ });
27
+
28
+ test('a minimum larger than half the axis shrinks to half, so both panes still fit', () => {
29
+ expect(reachSize(300, 800, 1000)).toBe(500);
30
+ });
31
+
32
+ test('a zero-width axis gives nothing rather than NaN or Infinity', () => {
33
+ expect(reachSize(10, 160, 0)).toBe(0);
34
+ expect(reachSize(Number.NaN, 160, 1000)).toBe(0);
35
+ });
36
+ });
37
+
38
+ describe('stepSize — where an arrow key lands', () => {
39
+ test('a step that would cross the minimum stops on it, and the next one shuts', () => {
40
+ expect(stepSize(170, -16, 160, 1000)).toBe(160);
41
+ expect(stepSize(160, -16, 160, 1000)).toBe(0);
42
+ });
43
+
44
+ test('the first step out of shut opens at the minimum, not a sliver', () => {
45
+ expect(stepSize(0, 16, 160, 1000)).toBe(160);
46
+ });
47
+
48
+ test('the far end shuts the other pane the same way, and opens it the same way', () => {
49
+ expect(stepSize(830, 16, 160, 1000)).toBe(840);
50
+ expect(stepSize(840, 16, 160, 1000)).toBe(1000);
51
+ expect(stepSize(1000, -16, 160, 1000)).toBe(840);
52
+ });
53
+
54
+ test('works in shares as well as pixels, through float noise', () => {
55
+ expect(stepSize(0.12000000001, -0.02, 0.12, 1)).toBe(0);
56
+ expect(stepSize(0.5, 0.02, 0.12, 1)).toBeCloseTo(0.52);
57
+ });
58
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * How far a divider may be dragged — the ONE rule every resizable split in cursedbelt obeys.
3
+ *
4
+ * The owner, 2026-10-27: *"For all dividers … that I can drag to size the panes, let me drag them
5
+ * as far as I want including shutting a pane completely … I don't want any panes to block me."*
6
+ * Station's Queue was the worked example: its task list stopped at 36% of the page, so the titles
7
+ * it was opened to read stayed cut off however hard the divider was pulled.
8
+ *
9
+ * So a pane has no MAXIMUM, ever — a pane grows until its neighbor is shut — and its minimum is
10
+ * not a wall but a snap, VS Code's sash and `react-resizable-panels`' collapsible panel alike:
11
+ *
12
+ * - dragged below HALF its minimum, a pane SHUTS (size 0);
13
+ * - between half and the whole minimum it HOLDS at the minimum, so a pane is never drawn as an
14
+ * unreadable sliver and a shut is a deliberate pull, not a twitch;
15
+ * - the same at the far end, for the pane on the other side of the seam.
16
+ *
17
+ * A shut pane opens again the way it closed: drag the seam back out (it is still there, at the
18
+ * edge), or step it with an arrow key. `PanelGrid`/`SplitPane` get the same rule from the library
19
+ * (every leaf is `collapsible`); `PaneSplit` and `useRailResize` draw their own seams and call
20
+ * these. An app that draws a seam of its own calls them too — guardrails' `no-hand-rolled-divider`
21
+ * refuses one that does not.
22
+ *
23
+ * Units are the caller's — pixels or a share of 1 — as long as all three agree.
24
+ */
25
+
26
+ /** Below this share of `total`, two sizes are the same size (a float's last bits are not a gap). */
27
+ const EPSILON = 1e-4;
28
+
29
+ /** The snap a pane gets, never more than half the axis — two minimums must fit side by side. */
30
+ function snapOf(min: number, total: number): number {
31
+ return Math.max(0, Math.min(min, total / 2));
32
+ }
33
+
34
+ /**
35
+ * Where a POINTER drag lands: the size the pointer asks for, with the snap applied at both ends.
36
+ * `size` is the first pane's (the one before the seam); the second pane is `total - size`.
37
+ */
38
+ export function reachSize(size: number, min: number, total: number): number {
39
+ if (!(total > 0) || !Number.isFinite(size)) return 0;
40
+ const snap = snapOf(min, total);
41
+ if (size < snap / 2) return 0;
42
+ if (size < snap) return snap;
43
+ if (size > total - snap / 2) return total;
44
+ if (size > total - snap) return total - snap;
45
+ return size;
46
+ }
47
+
48
+ /**
49
+ * Where a KEYBOARD step lands. A step that would cross a minimum stops ON it, and the next step
50
+ * the same way shuts the pane — so every size, including shut, is reachable from the keys, and
51
+ * a shut pane's first step out opens it at its minimum, not a sliver.
52
+ */
53
+ export function stepSize(size: number, delta: number, min: number, total: number): number {
54
+ if (!(total > 0) || !Number.isFinite(size) || delta === 0) return size;
55
+ const snap = snapOf(min, total);
56
+ const near = (a: number, b: number) => Math.abs(a - b) <= total * EPSILON;
57
+ if (delta < 0) {
58
+ if (near(size, total)) return total - snap;
59
+ if (size <= snap || near(size, snap)) return 0;
60
+ return Math.max(snap, size + delta);
61
+ }
62
+ if (near(size, 0)) return snap;
63
+ if (size >= total - snap || near(size, total - snap)) return total;
64
+ return Math.min(total - snap, size + delta);
65
+ }
@@ -85,13 +85,17 @@ export interface MasterDetailProps {
85
85
  /** Distinct per page — SplitPane persists pane widths under it. */
86
86
  storageKey?: string;
87
87
  /**
88
- * The LIST pane's share of the width — default 30% between 16rem and 44%. A page whose detail is
88
+ * The LIST pane's share of the width — default 30%, snapping at 16rem. A page whose detail is
89
89
  * the thing being read (a task's text, an app's charts) gives the list less: station's Queue at
90
90
  * 1440 drew a 30% list of one-line titles beside a detail pane of four facts, so the text the
91
91
  * owner opened the page for had nowhere to go (forge task station-evolution, 2026-10-20). Any
92
- * CSS length or percentage; the reader can still drag the divider inside `min`/`max`.
92
+ * CSS length or percentage.
93
+ *
94
+ * 🔴 There is no `max`: the reader drags the divider as far as they like, shutting either pane
95
+ * (the owner, 2026-10-27 — station's Queue capped its list at 36% and cut its titles off). `min`
96
+ * is the snap `SplitPane` gives it, not a floor.
93
97
  */
94
- masterSize?: { default?: string; min?: string; max?: string };
98
+ masterSize?: { default?: string; min?: string };
95
99
  /** Rendered above the filters (a "New…" button, a count, a refresh). */
96
100
  listHeader?: ReactNode;
97
101
  className?: string;
@@ -320,8 +324,9 @@ export function MasterDetail({
320
324
  {...(storageKey ? { storageKey } : {})}
321
325
  separatorVariant="line"
322
326
  panels={[
323
- { id: "master", content: list, defaultSize: masterSize?.default ?? "30%", minSize: masterSize?.min ?? "16rem", maxSize: masterSize?.max ?? "44%" },
324
- { id: "detail", content: pane },
327
+ { id: "master", content: list, defaultSize: masterSize?.default ?? "30%", minSize: masterSize?.min ?? "16rem" },
328
+ // The detail snaps too, so the list can take the whole width without a sliver beside it.
329
+ { id: "detail", content: pane, minSize: "16rem" },
325
330
  ]}
326
331
  />
327
332
  </div>
@@ -19,7 +19,7 @@
19
19
  * ── 🔴 Upscaling is HONEST here, and deliberately modest ───────────────────────────────
20
20
  * There is no detail in a video frame that was not decoded, so nothing here can invent any.
21
21
  * What upscaling buys is real but narrow: a 300×200 crop of a 4K frame saved at 1× is a tiny
22
- * picture that every later viewer scales up with nearest-neighbour-ish defaults, whereas
22
+ * picture that every later viewer scales up with nearest-neighbor-ish defaults, whereas
23
23
  * resampling it once, at capture, with `imageSmoothingQuality: 'high'`, produces a smoother
24
24
  * enlargement than each viewer would make for itself. So the ceiling is low on purpose — 4×
25
25
  * is already past the point of adding anything, and beyond that it is only file size.
@@ -107,8 +107,8 @@ export function judgePhone(s: PhoneSnapshot, { touch }: JudgePhoneOptions): Desi
107
107
  if (!touch) return found;
108
108
  for (const f of s.fields.filter((f) => f.size < ZOOM_FLOOR)) push('FRM-11', f.what, `${f.size}px text: iOS zooms the page into a field under ${ZOOM_FLOOR}px`);
109
109
  for (const t of s.targets.filter((t) => !t.inline && !t.cut && Math.min(t.w, t.h) < TARGET_FLOOR)) {
110
- const neighbour = crowded(t, s.targets);
111
- if (neighbour) push('LAY-13', t.what, `${Math.round(t.w)}×${Math.round(t.h)}: under ${TARGET_FLOOR}px and too close to ${neighbour}`);
110
+ const neighbor = crowded(t, s.targets);
111
+ if (neighbor) push('LAY-13', t.what, `${Math.round(t.w)}×${Math.round(t.h)}: under ${TARGET_FLOOR}px and too close to ${neighbor}`);
112
112
  }
113
113
  // The probe steps a pixel at a time from a center that may sit on a half pixel, so a 44px halo can read 43.
114
114
  const cramped = s.targets.filter((t) => !t.inline && !t.cut && Math.min(t.w, t.h) < THUMB - 1.5 && Math.min(t.w, t.h) >= TARGET_FLOOR - 0.5);