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.
- package/dist/react/components/PaneSplit.d.ts +11 -2
- package/dist/react/components/PaneSplit.d.ts.map +1 -1
- package/dist/react/components/PaneSplit.js +12 -11
- package/dist/react/components/PaneSplit.js.map +1 -1
- package/dist/react/components/PanelGrid.d.ts +14 -8
- package/dist/react/components/PanelGrid.d.ts.map +1 -1
- package/dist/react/components/PanelGrid.js +10 -3
- package/dist/react/components/PanelGrid.js.map +1 -1
- package/dist/react/components/SplitPane.d.ts +3 -9
- package/dist/react/components/SplitPane.d.ts.map +1 -1
- package/dist/react/components/SplitPane.js +0 -3
- package/dist/react/components/SplitPane.js.map +1 -1
- package/dist/react/components/paneSplit.css +8 -0
- package/dist/react/filter-rail/railResize.d.ts +16 -5
- package/dist/react/filter-rail/railResize.d.ts.map +1 -1
- package/dist/react/filter-rail/railResize.js +27 -9
- package/dist/react/filter-rail/railResize.js.map +1 -1
- package/dist/react/filter-rail/railWidth.d.ts +10 -5
- package/dist/react/filter-rail/railWidth.d.ts.map +1 -1
- package/dist/react/filter-rail/railWidth.js +13 -8
- package/dist/react/filter-rail/railWidth.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/lib/paneReach.d.ts +36 -0
- package/dist/react/lib/paneReach.d.ts.map +1 -0
- package/dist/react/lib/paneReach.js +72 -0
- package/dist/react/lib/paneReach.js.map +1 -0
- package/dist/react/master-detail/MasterDetail.d.ts +6 -3
- package/dist/react/master-detail/MasterDetail.d.ts.map +1 -1
- package/dist/react/master-detail/MasterDetail.js +3 -2
- package/dist/react/master-detail/MasterDetail.js.map +1 -1
- package/dist/react/media/videoEnhance.d.ts +1 -1
- package/dist/react/media/videoEnhance.js +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
- package/dist/testing/phoneRules.js +3 -3
- package/dist/testing/phoneRules.js.map +1 -1
- package/package.json +2 -1
- package/scripts/guardrailsEnforce.spec.ts +25 -0
- package/scripts/guardrailsEnforce.ts +61 -0
- package/src/react/components/PaneSplit.spec.tsx +26 -3
- package/src/react/components/PaneSplit.tsx +26 -13
- package/src/react/components/PanelGrid.tsx +16 -11
- package/src/react/components/SplitPane.spec.tsx +10 -5
- package/src/react/components/SplitPane.tsx +3 -12
- package/src/react/components/paneSplit.css +8 -0
- package/src/react/filter-rail/FilterRail.spec.tsx +7 -3
- package/src/react/filter-rail/railResize.spec.tsx +27 -5
- package/src/react/filter-rail/railResize.tsx +46 -15
- package/src/react/filter-rail/railWidth.ts +13 -8
- package/src/react/index.ts +1 -0
- package/src/react/lib/paneReach.spec.ts +58 -0
- package/src/react/lib/paneReach.ts +65 -0
- package/src/react/master-detail/MasterDetail.tsx +10 -5
- package/src/react/media/videoEnhance.ts +1 -1
- 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
|
|
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
|
|
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
|
|
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"
|
|
324
|
-
|
|
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-
|
|
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
|
|
111
|
-
if (
|
|
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);
|