cursedbelt 5.68.0 → 5.69.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/looks.css +41 -0
- package/dist/react/components/ActionBar.d.ts.map +1 -1
- package/dist/react/components/ActionBar.js +9 -1
- package/dist/react/components/ActionBar.js.map +1 -1
- package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
- package/dist/react/components/DataTable/DataTable.js +3 -3
- package/dist/react/components/DataTable/DataTable.js.map +1 -1
- package/dist/react/components/Grid.d.ts.map +1 -1
- package/dist/react/components/Grid.js +4 -2
- package/dist/react/components/Grid.js.map +1 -1
- package/dist/react/components/ScrollArea.d.ts +1 -1
- package/dist/react/components/ScrollArea.js +1 -1
- package/dist/react/components/SecretInput.d.ts.map +1 -1
- package/dist/react/components/SecretInput.js +8 -2
- package/dist/react/components/SecretInput.js.map +1 -1
- package/dist/react/components/SegmentedControl.d.ts.map +1 -1
- package/dist/react/components/SegmentedControl.js +5 -1
- package/dist/react/components/SegmentedControl.js.map +1 -1
- package/dist/react/hooks/useKeyboardOpen.d.ts +13 -0
- package/dist/react/hooks/useKeyboardOpen.d.ts.map +1 -0
- package/dist/react/hooks/useKeyboardOpen.js +30 -0
- package/dist/react/hooks/useKeyboardOpen.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/look/AppearanceMenu.d.ts +29 -0
- package/dist/react/look/AppearanceMenu.d.ts.map +1 -0
- package/dist/react/look/AppearanceMenu.js +39 -0
- package/dist/react/look/AppearanceMenu.js.map +1 -0
- package/dist/react/look/AppearancePanel.d.ts +18 -0
- package/dist/react/look/AppearancePanel.d.ts.map +1 -0
- package/dist/react/look/AppearancePanel.js +20 -0
- package/dist/react/look/AppearancePanel.js.map +1 -0
- package/dist/react/look/LookPicker.d.ts +6 -1
- package/dist/react/look/LookPicker.d.ts.map +1 -1
- package/dist/react/look/LookPicker.js +2 -2
- package/dist/react/look/LookPicker.js.map +1 -1
- package/dist/react/media/denseGallery.css +2 -0
- package/dist/styles-areas/core.css +40 -39
- package/dist/styles-utilities.css +10 -8
- package/dist/testing/appleDevices.d.ts +68 -0
- package/dist/testing/appleDevices.d.ts.map +1 -0
- package/dist/testing/appleDevices.js +65 -0
- package/dist/testing/appleDevices.js.map +1 -0
- package/dist/testing/appleWalk.d.ts +72 -0
- package/dist/testing/appleWalk.d.ts.map +1 -0
- package/dist/testing/appleWalk.js +209 -0
- package/dist/testing/appleWalk.js.map +1 -0
- package/dist/testing/designAudit.d.ts +1 -1
- package/dist/testing/designAudit.d.ts.map +1 -1
- package/dist/testing/designAudit.js +12 -0
- package/dist/testing/designAudit.js.map +1 -1
- package/dist/testing/designBaseline.d.ts +14 -0
- package/dist/testing/designBaseline.d.ts.map +1 -1
- package/dist/testing/designBaseline.js +26 -28
- package/dist/testing/designBaseline.js.map +1 -1
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +9 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/lookPrePaint.d.ts +19 -0
- package/dist/testing/lookPrePaint.d.ts.map +1 -0
- package/dist/testing/lookPrePaint.js +56 -0
- package/dist/testing/lookPrePaint.js.map +1 -0
- package/dist/testing/phoneRules.d.ts +98 -0
- package/dist/testing/phoneRules.d.ts.map +1 -0
- package/dist/testing/phoneRules.js +282 -0
- package/dist/testing/phoneRules.js.map +1 -0
- package/dist/testing/safariDriver.d.ts +103 -0
- package/dist/testing/safariDriver.d.ts.map +1 -0
- package/dist/testing/safariDriver.js +288 -0
- package/dist/testing/safariDriver.js.map +1 -0
- package/dist/testing/safariRules.d.ts +57 -0
- package/dist/testing/safariRules.d.ts.map +1 -0
- package/dist/testing/safariRules.js +191 -0
- package/dist/testing/safariRules.js.map +1 -0
- package/package.json +2 -1
- package/src/fonts/inter-OFL.txt +93 -0
- package/src/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/src/looks.css +41 -0
- package/src/looks.spec.ts +59 -1
- package/src/react/components/ActionBar.spec.tsx +49 -1
- package/src/react/components/ActionBar.tsx +10 -2
- package/src/react/components/DataTable/DataTable.tsx +6 -1
- package/src/react/components/Grid.tsx +4 -2
- package/src/react/components/ScrollArea.tsx +1 -1
- package/src/react/components/SecretInput.spec.tsx +14 -0
- package/src/react/components/SecretInput.tsx +13 -1
- package/src/react/components/SegmentedControl.spec.tsx +1 -1
- package/src/react/components/SegmentedControl.tsx +5 -1
- package/src/react/components/tooltipWarm.spec.tsx +15 -0
- package/src/react/hooks/useKeyboardOpen.ts +29 -0
- package/src/react/index.ts +2 -0
- package/src/react/look/AppearanceMenu.spec.tsx +90 -0
- package/src/react/look/AppearanceMenu.tsx +64 -0
- package/src/react/look/AppearancePanel.tsx +54 -0
- package/src/react/look/LookPicker.tsx +14 -2
- package/src/react/media/denseGallery.css +2 -0
- package/src/styles-areas/core.css +40 -39
- package/src/styles-utilities.css +10 -8
- package/src/testing/appleDevices.spec.ts +36 -0
- package/src/testing/appleDevices.ts +97 -0
- package/src/testing/appleWalk.ts +232 -0
- package/src/testing/designAudit.ts +12 -0
- package/src/testing/designBaseline.spec.ts +9 -0
- package/src/testing/designBaseline.ts +26 -34
- package/src/testing/index.ts +18 -0
- package/src/testing/lookPrePaint.spec.ts +43 -0
- package/src/testing/lookPrePaint.ts +60 -0
- package/src/testing/phoneRules.spec.ts +75 -0
- package/src/testing/phoneRules.ts +332 -0
- package/src/testing/safariDriver.ts +331 -0
- package/src/testing/safariRules.spec.ts +92 -0
- package/src/testing/safariRules.ts +195 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
16
16
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
17
17
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
18
|
-
*
|
|
18
|
+
* 905 candidate(s).
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
@source inline("*:border-l *:border-rule *:px-[calc(var(--col-gap)/2)] -bottom-1.5 -bottom-px -left-1.5 -left-px");
|
|
25
25
|
@source inline("-mb-px -ml-1 -ml-2 -ml-3 -ml-4 -ml-[calc(var(--col-gap)/2+1px)] -mr-[calc(var(--col-gap)/2)]");
|
|
26
26
|
@source inline("-right-1.5 -right-px -rotate-90 -top-1.5 -top-px -translate-x-1/2 -translate-y-1/2 @container");
|
|
27
|
-
@source inline("@xl:grid-cols-2 [
|
|
28
|
-
@source inline("[&>:first-child]:w-[var(--cb-sidebar-side)] [&>:last-child]:flex-1
|
|
29
|
-
@source inline("[&>button:first-child]:rounded-l-md [&>button:focus-visible]:relative");
|
|
27
|
+
@source inline("@xl:grid-cols-2 [&>*>button]:w-full [&>*]:w-full [&>:first-child]:min-w-0");
|
|
28
|
+
@source inline("[&>:first-child]:shrink-0 [&>:first-child]:w-[var(--cb-sidebar-side)] [&>:last-child]:flex-1");
|
|
29
|
+
@source inline("[&>:last-child]:min-w-0 [&>button:first-child]:rounded-l-md [&>button:focus-visible]:relative");
|
|
30
30
|
@source inline("[&>button:focus-visible]:z-10 [&>button:hover]:relative [&>button:hover]:z-10");
|
|
31
31
|
@source inline("[&>button:last-child]:rounded-r-md [&>button:not(:first-child)]:-ml-px");
|
|
32
32
|
@source inline("[&>button:not(:first-child)]:border-border [&>button:not(:first-child)]:border-l");
|
|
@@ -38,31 +38,30 @@
|
|
|
38
38
|
@source inline("[&_[data-icon]]:text-accent [&_[data-icon]]:text-destructive [&_[data-icon]]:text-success");
|
|
39
39
|
@source inline("[&_[data-icon]]:text-warning [&_sup]:leading-none [&_svg]:shrink-0 [&_svg]:size-12");
|
|
40
40
|
@source inline("[&_svg]:size-4 [&_svg]:size-5 [&_svg]:size-6 [--cbfk-sticky-top:-1rem]");
|
|
41
|
-
@source inline("[-webkit-touch-callout:none]
|
|
42
|
-
@source inline("active:
|
|
43
|
-
@source inline("
|
|
44
|
-
@source inline("animate-
|
|
45
|
-
@source inline("aria-
|
|
46
|
-
@source inline("aria-disabled:active:scale-100 aria-disabled:cursor-not-allowed");
|
|
41
|
+
@source inline("[-webkit-touch-callout:none] a absolute act active active:bg-accent active:scale-100");
|
|
42
|
+
@source inline("active:scale-[var(--press-scale)] active:scale-[var(--press-scale-surface)] alert alert-icon");
|
|
43
|
+
@source inline("align align-middle an animate animate-in animate-progress-indeterminate animate-pulse");
|
|
44
|
+
@source inline("animate-spin aria aria-[invalid=true]:border-field-error aria-[invalid=true]:ring-field-error/30");
|
|
45
|
+
@source inline("aria-disabled aria-disabled:active:scale-100 aria-disabled:cursor-not-allowed");
|
|
47
46
|
@source inline("aria-disabled:hover:bg-transparent aria-disabled:opacity-30 aria-disabled:opacity-50");
|
|
48
47
|
@source inline("aria-disabled:pointer-events-none aria-pressed aria-pressed:bg-accent/10 aria-pressed:border");
|
|
49
|
-
@source inline("aria-pressed:border-accent aria-pressed:text-accent auto auto-cols-
|
|
50
|
-
@source inline("backdrop-blur backdrop-blur-md backdrop-blur-sm bg bg-accent bg-accent-solid
|
|
51
|
-
@source inline("bg-accent/5 bg-background bg-background/80 bg-background/90 bg-background/95
|
|
52
|
-
@source inline("bg-border bg-border-strong bg-card bg-card-raised bg-current bg-destructive
|
|
53
|
-
@source inline("bg-destructive/15 bg-destructive/5 bg-destructive/8 bg-emerald-100
|
|
54
|
-
@source inline("bg-
|
|
55
|
-
@source inline("bg-muted-foreground/60 bg-muted/20 bg-muted/40 bg-popover bg-popover!
|
|
56
|
-
@source inline("bg-success-solid bg-success/15 bg-success/5 bg-surface bg-surface-sunken
|
|
57
|
-
@source inline("bg-tone-amber-
|
|
58
|
-
@source inline("bg-tone-fuchsia-bg bg-tone-fuchsia-fg bg-tone-lime-bg bg-tone-lime-fg
|
|
59
|
-
@source inline("bg-tone-orange-
|
|
60
|
-
@source inline("bg-tone-sky-bg bg-tone-sky-fg bg-tone-slate-bg bg-tone-slate-fg bg-tone-teal-bg
|
|
61
|
-
@source inline("bg-tone-violet-bg bg-tone-violet-fg bg-transparent bg-warning bg-warning/15
|
|
62
|
-
@source inline("bg-warning/8 bg-white block blur border border-0 border-accent border-accent/40
|
|
63
|
-
@source inline("border-b border-b-0 border-b-2 border-b-[length:var(--status-glyph-h)]
|
|
64
|
-
@source inline("border-border border-border! border-border-strong border-border/60
|
|
65
|
-
@source inline("border-current border-destructive/30 border-destructive/40 border-destructive/50");
|
|
48
|
+
@source inline("aria-pressed:border-accent aria-pressed:text-accent auto auto-cols-[minmax(min-content,1fr)] b");
|
|
49
|
+
@source inline("back backdrop backdrop-blur backdrop-blur-md backdrop-blur-sm bg bg-accent bg-accent-solid");
|
|
50
|
+
@source inline("bg-accent/10 bg-accent/5 bg-background bg-background/80 bg-background/90 bg-background/95");
|
|
51
|
+
@source inline("bg-black/50 bg-border bg-border-strong bg-card bg-card-raised bg-current bg-destructive");
|
|
52
|
+
@source inline("bg-destructive-solid bg-destructive/15 bg-destructive/5 bg-destructive/8 bg-emerald-100");
|
|
53
|
+
@source inline("bg-field-bg bg-foreground/70 bg-muted bg-muted-foreground bg-muted-foreground/40");
|
|
54
|
+
@source inline("bg-muted-foreground/50 bg-muted-foreground/60 bg-muted/20 bg-muted/40 bg-popover bg-popover!");
|
|
55
|
+
@source inline("bg-success bg-success-solid bg-success/15 bg-success/5 bg-surface bg-surface-sunken");
|
|
56
|
+
@source inline("bg-tone-amber-bg bg-tone-amber-fg bg-tone-cyan-bg bg-tone-cyan-fg bg-tone-emerald-bg");
|
|
57
|
+
@source inline("bg-tone-emerald-fg bg-tone-fuchsia-bg bg-tone-fuchsia-fg bg-tone-lime-bg bg-tone-lime-fg");
|
|
58
|
+
@source inline("bg-tone-orange-bg bg-tone-orange-fg bg-tone-purple-bg bg-tone-purple-fg bg-tone-rose-bg");
|
|
59
|
+
@source inline("bg-tone-rose-fg bg-tone-sky-bg bg-tone-sky-fg bg-tone-slate-bg bg-tone-slate-fg bg-tone-teal-bg");
|
|
60
|
+
@source inline("bg-tone-teal-fg bg-tone-violet-bg bg-tone-violet-fg bg-transparent bg-warning bg-warning/15");
|
|
61
|
+
@source inline("bg-warning/5 bg-warning/8 bg-white block blur border border-0 border-accent border-accent/40");
|
|
62
|
+
@source inline("border-accent/50 border-b border-b-0 border-b-2 border-b-[length:var(--status-glyph-h)]");
|
|
63
|
+
@source inline("border-b-current border-border border-border! border-border-strong border-border/60");
|
|
64
|
+
@source inline("border-collapse border-current border-destructive/30 border-destructive/40 border-destructive/50");
|
|
66
65
|
@source inline("border-field-border border-rule border-success/30 border-success/40 border-success/50 border-t");
|
|
67
66
|
@source inline("border-tone-sky-fg/30 border-tone-slate-fg/30 border-transparent border-warning/30");
|
|
68
67
|
@source inline("border-warning/40 border-warning/50 border-x-4 border-x-transparent bot bottom bottom-0 break");
|
|
@@ -120,7 +119,7 @@
|
|
|
120
119
|
@source inline("group-data-[state=open]:rotate-90 group-focus-visible/sep:opacity-100 group-hover");
|
|
121
120
|
@source inline("group-hover/path:opacity-100 group-hover/sep:opacity-100 group-hover:opacity-100 group/path");
|
|
122
121
|
@source inline("group/properties group/sep grow h h-0.5 h-1 h-1.5 h-10 h-14 h-16 h-2 h-2.5 h-24 h-3 h-32 h-4 h-5");
|
|
123
|
-
@source inline("h-6 h-7 h-8 h-9 h-[
|
|
122
|
+
@source inline("h-6 h-7 h-8 h-9 h-[min(var(--region-h-lg),var(--region-h-phone))]");
|
|
124
123
|
@source inline("h-[min(var(--region-h-md),var(--region-h-phone))]");
|
|
125
124
|
@source inline("h-[min(var(--region-h-sm),var(--region-h-phone))] h-[var(--control-h-lg)]");
|
|
126
125
|
@source inline("h-[var(--control-h-md)] h-[var(--control-h-sm)] h-[var(--indicator-w)] h-auto h-full h-px hi hid");
|
|
@@ -172,17 +171,19 @@
|
|
|
172
171
|
@source inline("sm:max-w-[var(--modal-w-lg)] sm:max-w-[var(--modal-w-md)] sm:max-w-[var(--modal-w-sm)] sm:p-4");
|
|
173
172
|
@source inline("sm:p-5 sm:p-8 sm:pointer-fine:text-sm sm:px-6 sm:rounded-xl sm:text-sm sm:top-1/2");
|
|
174
173
|
@source inline("sm:w-[calc(100vw-2rem)] space space-y-1 space-y-3 sr-only stat static sticky stroke");
|
|
175
|
-
@source inline("stroke-accent-solid stroke-destructive stroke-muted stroke-warning
|
|
176
|
-
@source inline("
|
|
177
|
-
@source inline("
|
|
178
|
-
@source inline("text-
|
|
179
|
-
@source inline("text-
|
|
180
|
-
@source inline("text-
|
|
181
|
-
@source inline("text-
|
|
182
|
-
@source inline("text-
|
|
183
|
-
@source inline("text-tone-
|
|
184
|
-
@source inline("text-
|
|
185
|
-
@source inline("
|
|
174
|
+
@source inline("stroke-accent-solid stroke-destructive stroke-muted stroke-warning support supports");
|
|
175
|
+
@source inline("supports-[overflow-clip-margin:1px]:[overflow-clip-margin:calc(var(--spacing)*1)] t ta tab table");
|
|
176
|
+
@source inline("tabular tabular-nums text text-2xl text-[length:var(--text-glyph-relative)]");
|
|
177
|
+
@source inline("text-[var(--foreground)] text-[var(--muted-foreground)] text-accent text-accent-strong text-base");
|
|
178
|
+
@source inline("text-caption text-center text-current text-data text-destructive text-destructive-foreground");
|
|
179
|
+
@source inline("text-destructive-strong text-emerald-700 text-field-error text-field-fg text-foreground text-h1");
|
|
180
|
+
@source inline("text-h3 text-inherit text-left text-lg text-meta text-muted-foreground text-muted-foreground!");
|
|
181
|
+
@source inline("text-popover-foreground text-right text-sm text-success-strong text-tone-amber-fg");
|
|
182
|
+
@source inline("text-tone-cyan-fg text-tone-emerald-fg text-tone-fuchsia-fg text-tone-lime-fg");
|
|
183
|
+
@source inline("text-tone-orange-fg text-tone-purple-fg text-tone-rose-fg text-tone-sky-fg text-tone-slate-fg");
|
|
184
|
+
@source inline("text-tone-teal-fg text-tone-violet-fg text-warning text-warning-strong text-white text-xs to top");
|
|
185
|
+
@source inline("top-0 top-1/2 top-2 touch touch-manipulation touch-none track tracking tracking-tight");
|
|
186
|
+
@source inline("tracking-wide tracking-wider transform transition");
|
|
186
187
|
@source inline("transition-[color,background-color,border-color,box-shadow,scale] transition-[stroke-dashoffset]");
|
|
187
188
|
@source inline("transition-[transform,width] transition-[width] transition-all transition-colors");
|
|
188
189
|
@source inline("transition-opacity transition-transform translate-x-1/2 translate-y-1/2 truncate under underline");
|
|
@@ -716,9 +716,6 @@
|
|
|
716
716
|
.h-105 {
|
|
717
717
|
height: calc(var(--spacing) * 105);
|
|
718
718
|
}
|
|
719
|
-
.h-\[calc\(100dvh-…\)\] {
|
|
720
|
-
height: calc(100dvh - …);
|
|
721
|
-
}
|
|
722
719
|
.h-\[min\(var\(--region-h-lg\)\,var\(--region-h-phone\)\)\] {
|
|
723
720
|
height: min(var(--region-h-lg), var(--region-h-phone));
|
|
724
721
|
}
|
|
@@ -1335,8 +1332,8 @@
|
|
|
1335
1332
|
.appearance-none {
|
|
1336
1333
|
appearance: none;
|
|
1337
1334
|
}
|
|
1338
|
-
.auto-cols-
|
|
1339
|
-
grid-auto-columns: minmax(
|
|
1335
|
+
.auto-cols-\[minmax\(min-content\,1fr\)\] {
|
|
1336
|
+
grid-auto-columns: minmax(min-content,1fr);
|
|
1340
1337
|
}
|
|
1341
1338
|
.grid-flow-col {
|
|
1342
1339
|
grid-auto-flow: column;
|
|
@@ -3485,9 +3482,6 @@
|
|
|
3485
3482
|
.\[background\:var\(--cb-swatch-fill\,var\(--muted\)\)\] {
|
|
3486
3483
|
background: var(--cb-swatch-fill,var(--muted));
|
|
3487
3484
|
}
|
|
3488
|
-
.\[overflow-clip-margin\:calc\(var\(--spacing\)\*1\)\] {
|
|
3489
|
-
overflow-clip-margin: calc(var(--spacing) * 1);
|
|
3490
|
-
}
|
|
3491
3485
|
.fade-in {
|
|
3492
3486
|
--tw-enter-opacity: 0;
|
|
3493
3487
|
}
|
|
@@ -4453,6 +4447,11 @@
|
|
|
4453
4447
|
.data-\[state\=unchecked\]\:bg-muted[data-state="unchecked"] {
|
|
4454
4448
|
background-color: var(--muted);
|
|
4455
4449
|
}
|
|
4450
|
+
@supports (overflow-clip-margin:1px) {
|
|
4451
|
+
.supports-\[overflow-clip-margin\:1px\]\:\[overflow-clip-margin\:calc\(var\(--spacing\)\*1\)\] {
|
|
4452
|
+
overflow-clip-margin: calc(var(--spacing) * 1);
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4456
4455
|
@media (prefers-reduced-motion: reduce) {
|
|
4457
4456
|
.motion-reduce\:animate-none {
|
|
4458
4457
|
animation: none;
|
|
@@ -5346,6 +5345,9 @@
|
|
|
5346
5345
|
display: none;
|
|
5347
5346
|
}
|
|
5348
5347
|
}
|
|
5348
|
+
.\[\&\>\*\>button\]\:w-full > * > button {
|
|
5349
|
+
width: 100%;
|
|
5350
|
+
}
|
|
5349
5351
|
.\[\&\>\:first-child\]\:w-\[var\(--cb-sidebar-side\)\] > :first-child {
|
|
5350
5352
|
width: var(--cb-sidebar-side);
|
|
5351
5353
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Apple screens every app is held to (forge task apple-device-testing, 2026-10-06): the
|
|
3
|
+
* smallest iPhone still sold, today's common one and that one on its side, an iPad both ways,
|
|
4
|
+
* and the Mac's Safari — all in Playwright's WebKit, the engine Apple ships to every one of them.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 WebKit, not Chromium in a phone's clothes. Each app's browser gate already audits at 390 and
|
|
7
|
+
* 1440 in Chromium (`designBaseline.ts`, REV-9); what this roster adds is the engine the owner's
|
|
8
|
+
* devices actually run, with a real touchscreen (`hasTouch`, so `(pointer: coarse)` applies and a
|
|
9
|
+
* tap is a tap, not a mouse) and the viewport Safari leaves once its own chrome has taken its share.
|
|
10
|
+
* On 2026-10-06 orbgem measured the gap this closes: Playwright's WebKit reported itself as 26.6
|
|
11
|
+
* while the Mac's Safari was 27.0.1 — close enough to find layout faults, and `safariRules.ts` is
|
|
12
|
+
* for the styles only the shipped release can rule on.
|
|
13
|
+
*
|
|
14
|
+
* No Playwright import, on purpose (see `designAudit.ts`'s header): the caller hands in
|
|
15
|
+
* Playwright's `devices` registry and gets back the options for `browser.newContext`.
|
|
16
|
+
*/
|
|
17
|
+
export type AppleDeviceKind = 'phone' | 'tablet' | 'desktop';
|
|
18
|
+
export interface AppleDevice {
|
|
19
|
+
/** The name a report prints and a baseline label carries — Playwright's, so it is also the registry key. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** `name`, as a file and label stem: `iphone-se-3rd-gen`. */
|
|
22
|
+
slug: string;
|
|
23
|
+
kind: AppleDeviceKind;
|
|
24
|
+
landscape: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The on-screen keyboard's height in CSS px when a field takes focus, suggestion row included,
|
|
27
|
+
* or `null` where the keyboard rule does not run: on its side (the keyboard covers nearly all
|
|
28
|
+
* of a landscape phone, and iOS scrolls the field itself) and on a desktop (a hardware keyboard).
|
|
29
|
+
*/
|
|
30
|
+
keyboard: number | null;
|
|
31
|
+
}
|
|
32
|
+
export declare const KEYBOARD_PHONE = 336;
|
|
33
|
+
export declare const KEYBOARD_TABLET = 408;
|
|
34
|
+
/** The whole roster: what `bun run test:browser` walks. */
|
|
35
|
+
export declare const APPLE_DEVICES: readonly AppleDevice[];
|
|
36
|
+
/**
|
|
37
|
+
* The gate's subset — one of each kind, the strictest of each: the narrowest phone, the iPad in
|
|
38
|
+
* the orientation that shares the desktop's layout breakpoints, and the Mac. A gate has a budget
|
|
39
|
+
* (`$FORGE/tools/check-gate-time.ts`); the rest of the roster runs in the app's full browser suite.
|
|
40
|
+
*/
|
|
41
|
+
export declare const APPLE_GATE_DEVICES: readonly AppleDevice[];
|
|
42
|
+
/** Finds a device by name or slug; throws naming the roster, so a typo fails loud. */
|
|
43
|
+
export declare function appleDevice(nameOrSlug: string): AppleDevice;
|
|
44
|
+
/** The slice of a Playwright device descriptor a context is made from. */
|
|
45
|
+
export interface DeviceDescriptorLike {
|
|
46
|
+
viewport?: {
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
};
|
|
50
|
+
userAgent?: string;
|
|
51
|
+
deviceScaleFactor?: number;
|
|
52
|
+
isMobile?: boolean;
|
|
53
|
+
hasTouch?: boolean;
|
|
54
|
+
defaultBrowserType?: string;
|
|
55
|
+
}
|
|
56
|
+
/** The laptop REV-2 names (1440×900), in place of Playwright's 1280×720 for 'Desktop Safari'. */
|
|
57
|
+
export declare const DESKTOP_SAFARI_VIEWPORT: {
|
|
58
|
+
readonly width: 1440;
|
|
59
|
+
readonly height: 900;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* `browser.newContext` options for `device`, from Playwright's registry (`import { devices } from
|
|
63
|
+
* 'playwright'`). The desktop takes the review protocol's laptop size; everything else is the
|
|
64
|
+
* device's own screen, scale and touch. Throws when the registry lacks the device — a Playwright
|
|
65
|
+
* too old to know it would otherwise open a bare desktop window and call it an iPhone.
|
|
66
|
+
*/
|
|
67
|
+
export declare function appleContext<T extends DeviceDescriptorLike>(registry: Record<string, T>, device: AppleDevice): T;
|
|
68
|
+
//# sourceMappingURL=appleDevices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appleDevices.d.ts","sourceRoot":"","sources":["../../src/testing/appleDevices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC1B,4GAA4G;IAC5G,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,eAAe,MAAM,CAAC;AAWnC,2DAA2D;AAC3D,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAO/C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,WAAW,EAEpD,CAAC;AAEF,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW,CAI3D;AAED,0EAA0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,iGAAiG;AACjG,eAAO,MAAM,uBAAuB;;;CAAwC,CAAC;AAE7E;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,oBAAoB,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,CAAC,CAKhH"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Apple screens every app is held to (forge task apple-device-testing, 2026-10-06): the
|
|
3
|
+
* smallest iPhone still sold, today's common one and that one on its side, an iPad both ways,
|
|
4
|
+
* and the Mac's Safari — all in Playwright's WebKit, the engine Apple ships to every one of them.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 WebKit, not Chromium in a phone's clothes. Each app's browser gate already audits at 390 and
|
|
7
|
+
* 1440 in Chromium (`designBaseline.ts`, REV-9); what this roster adds is the engine the owner's
|
|
8
|
+
* devices actually run, with a real touchscreen (`hasTouch`, so `(pointer: coarse)` applies and a
|
|
9
|
+
* tap is a tap, not a mouse) and the viewport Safari leaves once its own chrome has taken its share.
|
|
10
|
+
* On 2026-10-06 orbgem measured the gap this closes: Playwright's WebKit reported itself as 26.6
|
|
11
|
+
* while the Mac's Safari was 27.0.1 — close enough to find layout faults, and `safariRules.ts` is
|
|
12
|
+
* for the styles only the shipped release can rule on.
|
|
13
|
+
*
|
|
14
|
+
* No Playwright import, on purpose (see `designAudit.ts`'s header): the caller hands in
|
|
15
|
+
* Playwright's `devices` registry and gets back the options for `browser.newContext`.
|
|
16
|
+
*/
|
|
17
|
+
export const KEYBOARD_PHONE = 336;
|
|
18
|
+
export const KEYBOARD_TABLET = 408;
|
|
19
|
+
const slugOf = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
20
|
+
const device = (name, kind, landscape, keyboard) => ({
|
|
21
|
+
name,
|
|
22
|
+
slug: slugOf(name),
|
|
23
|
+
kind,
|
|
24
|
+
landscape,
|
|
25
|
+
keyboard,
|
|
26
|
+
});
|
|
27
|
+
/** The whole roster: what `bun run test:browser` walks. */
|
|
28
|
+
export const APPLE_DEVICES = [
|
|
29
|
+
device('iPhone SE (3rd gen)', 'phone', false, KEYBOARD_PHONE),
|
|
30
|
+
device('iPhone 17', 'phone', false, KEYBOARD_PHONE),
|
|
31
|
+
device('iPhone 17 landscape', 'phone', true, null),
|
|
32
|
+
device('iPad (gen 7)', 'tablet', false, KEYBOARD_TABLET),
|
|
33
|
+
device('iPad (gen 7) landscape', 'tablet', true, null),
|
|
34
|
+
device('Desktop Safari', 'desktop', false, null),
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* The gate's subset — one of each kind, the strictest of each: the narrowest phone, the iPad in
|
|
38
|
+
* the orientation that shares the desktop's layout breakpoints, and the Mac. A gate has a budget
|
|
39
|
+
* (`$FORGE/tools/check-gate-time.ts`); the rest of the roster runs in the app's full browser suite.
|
|
40
|
+
*/
|
|
41
|
+
export const APPLE_GATE_DEVICES = APPLE_DEVICES.filter((d) => ['iPhone SE (3rd gen)', 'iPad (gen 7) landscape', 'Desktop Safari'].includes(d.name));
|
|
42
|
+
/** Finds a device by name or slug; throws naming the roster, so a typo fails loud. */
|
|
43
|
+
export function appleDevice(nameOrSlug) {
|
|
44
|
+
const found = APPLE_DEVICES.find((d) => d.name === nameOrSlug || d.slug === nameOrSlug);
|
|
45
|
+
if (!found)
|
|
46
|
+
throw new Error(`no Apple device "${nameOrSlug}" — the roster: ${APPLE_DEVICES.map((d) => d.name).join(', ')}`);
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
/** The laptop REV-2 names (1440×900), in place of Playwright's 1280×720 for 'Desktop Safari'. */
|
|
50
|
+
export const DESKTOP_SAFARI_VIEWPORT = { width: 1440, height: 900 };
|
|
51
|
+
/**
|
|
52
|
+
* `browser.newContext` options for `device`, from Playwright's registry (`import { devices } from
|
|
53
|
+
* 'playwright'`). The desktop takes the review protocol's laptop size; everything else is the
|
|
54
|
+
* device's own screen, scale and touch. Throws when the registry lacks the device — a Playwright
|
|
55
|
+
* too old to know it would otherwise open a bare desktop window and call it an iPhone.
|
|
56
|
+
*/
|
|
57
|
+
export function appleContext(registry, device) {
|
|
58
|
+
const found = registry[device.name];
|
|
59
|
+
if (!found?.viewport)
|
|
60
|
+
throw new Error(`Playwright's devices registry has no "${device.name}" — upgrade playwright, or pick from: ${APPLE_DEVICES.map((d) => d.name).join(', ')}`);
|
|
61
|
+
if (device.kind === 'desktop')
|
|
62
|
+
return { ...found, viewport: { ...DESKTOP_SAFARI_VIEWPORT } };
|
|
63
|
+
return { ...found };
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=appleDevices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appleDevices.js","sourceRoot":"","sources":["../../src/testing/appleDevices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAmBH,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAClC,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAEnC,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AACtG,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,IAAqB,EAAE,SAAkB,EAAE,QAAuB,EAAe,EAAE,CAAC,CAAC;IACjH,IAAI;IACJ,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC;IAClB,IAAI;IACJ,SAAS;IACT,QAAQ;CACT,CAAC,CAAC;AAEH,2DAA2D;AAC3D,MAAM,CAAC,MAAM,aAAa,GAA2B;IACnD,MAAM,CAAC,qBAAqB,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,CAAC;IAC7D,MAAM,CAAC,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,CAAC;IACnD,MAAM,CAAC,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;IAClD,MAAM,CAAC,cAAc,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,CAAC;IACxD,MAAM,CAAC,wBAAwB,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC;IACtD,MAAM,CAAC,gBAAgB,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC;CACjD,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA2B,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACnF,CAAC,qBAAqB,EAAE,wBAAwB,EAAE,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CACrF,CAAC;AAEF,sFAAsF;AACtF,MAAM,UAAU,WAAW,CAAC,UAAkB;IAC5C,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;IACxF,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,UAAU,mBAAmB,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5H,OAAO,KAAK,CAAC;AACf,CAAC;AAYD,iGAAiG;AACjG,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAW,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAiC,QAA2B,EAAE,MAAmB;IAC3G,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,KAAK,EAAE,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,MAAM,CAAC,IAAI,yCAAyC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClL,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,uBAAuB,EAAE,EAAE,CAAC;IAC7F,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { AppleDevice } from './appleDevices.js';
|
|
2
|
+
import { type AuditDesignOptions, type DesignViolation, type EvaluatingPage } from './designAudit.js';
|
|
3
|
+
import { type KeyboardView } from './phoneRules.js';
|
|
4
|
+
/**
|
|
5
|
+
* Installed once per page, BEFORE the app runs (`context.addInitScript`), so the app subscribes to
|
|
6
|
+
* the stand-in. It speaks on the real viewport as well, for a harness that could only install it
|
|
7
|
+
* afterwards (the shipped Safari over WebDriver): a keyboard nobody hears passes every rule.
|
|
8
|
+
*/
|
|
9
|
+
export declare const KEYBOARD_STANDIN = "(() => {\n const real = window.visualViewport;\n if (!real || window.__keyboard) return;\n const fake = new EventTarget();\n const cover = { px: 0, pan: 0 };\n for (const key of [\"width\", \"offsetLeft\", \"pageLeft\", \"pageTop\", \"scale\"]) Object.defineProperty(fake, key, { get: () => real[key] });\n Object.defineProperty(fake, \"height\", { get: () => real.height - cover.px });\n Object.defineProperty(fake, \"offsetTop\", { get: () => real.offsetTop + cover.pan });\n real.addEventListener(\"resize\", () => fake.dispatchEvent(new Event(\"resize\")));\n Object.defineProperty(window, \"visualViewport\", { get: () => fake });\n window.__keyboard = (px, pan = 0) => { cover.px = px; cover.pan = pan; for (const target of [fake, real]) { target.dispatchEvent(new Event(\"resize\")); target.dispatchEvent(new Event(\"scroll\")); } };\n})()";
|
|
10
|
+
/**
|
|
11
|
+
* Waits for the page to settle: three 100ms polls in a row with no DOM mutation, no finite animation
|
|
12
|
+
* running and no visible loading indicator (cb's Spinner is `.animate-spin`; anything else may say
|
|
13
|
+
* `aria-busy`), capped at 5s so a ticking clock or a spinner that never stops cannot stall a gate.
|
|
14
|
+
* A resize or a navigation can swap a whole layout — family's phone shell remounts its page behind a
|
|
15
|
+
* Spinner and fills it ~450ms later, with no DOM mutation in between — and a frame audited before
|
|
16
|
+
* that is the empty shell. Shared by the Chromium audit (`designBaseline.ts`) and the Apple pass.
|
|
17
|
+
*/
|
|
18
|
+
export declare function settleInPage(): Promise<void>;
|
|
19
|
+
/** Scrolls the page's main pane to `to` and says how far it can go. */
|
|
20
|
+
export declare function scrollMainInPage(to: number): {
|
|
21
|
+
at: number;
|
|
22
|
+
end: number;
|
|
23
|
+
};
|
|
24
|
+
/** Finishes every finite animation on the document's timeline; endless motion and scroll-driven motion are left alone. */
|
|
25
|
+
export declare function atRestInPage(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Focuses the field at `css` the way a tap would land on it — scrolled into view, the caret on a
|
|
28
|
+
* visible line of an editor — and answers whether it took focus. In-page rather than a Playwright
|
|
29
|
+
* tap: WebDriver calls an editor's document "not interactable", and a focus is no less true.
|
|
30
|
+
*/
|
|
31
|
+
export declare function focusFieldInPage(css: string): boolean;
|
|
32
|
+
/** With the focused field tapped, raises the stood-in keyboard of `keyboard` px over it and reports where the line being typed on ended up. */
|
|
33
|
+
export declare function keyboardViewInPage([keyboard, what]: readonly [number, string]): Promise<KeyboardView>;
|
|
34
|
+
/** Installs the stand-in from its source when the context did not (a page `setContent` made); a no-op once it is there. */
|
|
35
|
+
export declare function installKeyboardStandinInPage(source: string): boolean;
|
|
36
|
+
/** Lowers the stood-in keyboard. */
|
|
37
|
+
export declare function keyboardDownInPage(): void;
|
|
38
|
+
/** The slice of a Playwright `Page` the walk needs: `evaluate`, and a screenshot where one is wanted. */
|
|
39
|
+
export interface ApplePage extends EvaluatingPage {
|
|
40
|
+
viewportSize(): {
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
} | null;
|
|
44
|
+
screenshot?(options: {
|
|
45
|
+
path: string;
|
|
46
|
+
}): Promise<unknown>;
|
|
47
|
+
waitForTimeout?(ms: number): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export interface AuditAppleOptions extends AuditDesignOptions {
|
|
50
|
+
/** A page that scrolls: read to its end a screen at a time, so what arrives on scroll is judged where it arrives. */
|
|
51
|
+
long?: boolean;
|
|
52
|
+
/** A field to type into with the keyboard up, as a selector; on a device with a keyboard height. */
|
|
53
|
+
keyboard?: string;
|
|
54
|
+
/** Where to write screenshots (`<shots>/<device slug>/<route slug>[-NN|-keyboard].png`); none when unset. */
|
|
55
|
+
shots?: string;
|
|
56
|
+
/** Frames a long page is read through at most. */
|
|
57
|
+
maxFrames?: number;
|
|
58
|
+
/** Whether the pointer is a finger; the device's kind decides unless said. The shipped Safari at a phone's width has a mouse. */
|
|
59
|
+
touch?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** The same fault seen on two screens of one read-through is one fault. */
|
|
62
|
+
export declare function dedupe(findings: DesignViolation[]): DesignViolation[];
|
|
63
|
+
/**
|
|
64
|
+
* Judges one screen on one Apple device: the design audit (less LAY-13, which `measurePhone`
|
|
65
|
+
* owns here by the reach a finger gets rather than the box) plus the phone rules, at rest, a
|
|
66
|
+
* screen at a time for a long page, and with the keyboard up over `options.keyboard`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function auditApple(page: ApplePage, route: string, device: AppleDevice, options?: AuditAppleOptions): Promise<{
|
|
69
|
+
violations: DesignViolation[];
|
|
70
|
+
textNodes: number;
|
|
71
|
+
}>;
|
|
72
|
+
//# sourceMappingURL=appleWalk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appleWalk.d.ts","sourceRoot":"","sources":["../../src/testing/appleWalk.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,KAAK,kBAAkB,EAAe,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACnH,OAAO,EAA6B,KAAK,YAAY,EAA6C,MAAM,iBAAiB,CAAC;AAE1H;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,61BAWxB,CAAC;AAEN;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAqB5C;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAQxE;AAED,0HAA0H;AAC1H,wBAAgB,YAAY,IAAI,IAAI,CAKnC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAarD;AAED,+IAA+I;AAC/I,wBAAsB,kBAAkB,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CA0B3G;AAED,2HAA2H;AAC3H,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGpE;AAED,oCAAoC;AACpC,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,yGAAyG;AACzG,MAAM,WAAW,SAAU,SAAQ,cAAc;IAC/C,YAAY,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,UAAU,CAAC,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,cAAc,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IAC3D,qHAAqH;IACrH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oGAAoG;IACpG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6GAA6G;IAC7G,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iIAAiI;IACjI,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAKD,2EAA2E;AAC3E,wBAAgB,MAAM,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAErE;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,eAAe,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA+CpL"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* Walking one screen the way a thumb does, on an Apple device (forge task apple-device-testing,
|
|
4
|
+
* 2026-10-06, after orbgem's phone-screens script): the functions here run INSIDE the page
|
|
5
|
+
* (handed to Playwright's `evaluate` or WebDriver's execute), so each stands alone — no imports,
|
|
6
|
+
* no closures — and `auditApple` strings them together for the ledger (`designBaseline.ts`).
|
|
7
|
+
*
|
|
8
|
+
* - `KEYBOARD_STANDIN` is iOS's keyboard, stood in: the visual viewport loses `px` from the
|
|
9
|
+
* bottom and Safari pans it. Playwright's WebKit raises no keyboard, so the rule about the line
|
|
10
|
+
* being typed on (LAY-15) has nothing to measure without it.
|
|
11
|
+
* - `atRestInPage` brings every finite animation to its end before a screen is judged: the rules
|
|
12
|
+
* are about the screen at rest, and a fade still running is not a control that only appears on hover.
|
|
13
|
+
* - `scrollMainInPage` scrolls the tallest pane that scrolls (an app shell scrolls its main region,
|
|
14
|
+
* not the window) and says how far it can go, so a long page is read through a screen at a time.
|
|
15
|
+
* - `keyboardViewInPage` raises the stood-in keyboard over the focused field and reports where the
|
|
16
|
+
* line with the caret ended up against the strip the keyboard leaves.
|
|
17
|
+
*/
|
|
18
|
+
import { mkdirSync } from 'node:fs';
|
|
19
|
+
import { dirname } from 'node:path';
|
|
20
|
+
import { auditDesign } from './designAudit.js';
|
|
21
|
+
import { judgeKeyboard, judgePhone, measurePhone, PHONE_FIELDS, PHONE_TARGETS } from './phoneRules.js';
|
|
22
|
+
/**
|
|
23
|
+
* Installed once per page, BEFORE the app runs (`context.addInitScript`), so the app subscribes to
|
|
24
|
+
* the stand-in. It speaks on the real viewport as well, for a harness that could only install it
|
|
25
|
+
* afterwards (the shipped Safari over WebDriver): a keyboard nobody hears passes every rule.
|
|
26
|
+
*/
|
|
27
|
+
export const KEYBOARD_STANDIN = `(() => {
|
|
28
|
+
const real = window.visualViewport;
|
|
29
|
+
if (!real || window.__keyboard) return;
|
|
30
|
+
const fake = new EventTarget();
|
|
31
|
+
const cover = { px: 0, pan: 0 };
|
|
32
|
+
for (const key of ["width", "offsetLeft", "pageLeft", "pageTop", "scale"]) Object.defineProperty(fake, key, { get: () => real[key] });
|
|
33
|
+
Object.defineProperty(fake, "height", { get: () => real.height - cover.px });
|
|
34
|
+
Object.defineProperty(fake, "offsetTop", { get: () => real.offsetTop + cover.pan });
|
|
35
|
+
real.addEventListener("resize", () => fake.dispatchEvent(new Event("resize")));
|
|
36
|
+
Object.defineProperty(window, "visualViewport", { get: () => fake });
|
|
37
|
+
window.__keyboard = (px, pan = 0) => { cover.px = px; cover.pan = pan; for (const target of [fake, real]) { target.dispatchEvent(new Event("resize")); target.dispatchEvent(new Event("scroll")); } };
|
|
38
|
+
})()`;
|
|
39
|
+
/**
|
|
40
|
+
* Waits for the page to settle: three 100ms polls in a row with no DOM mutation, no finite animation
|
|
41
|
+
* running and no visible loading indicator (cb's Spinner is `.animate-spin`; anything else may say
|
|
42
|
+
* `aria-busy`), capped at 5s so a ticking clock or a spinner that never stops cannot stall a gate.
|
|
43
|
+
* A resize or a navigation can swap a whole layout — family's phone shell remounts its page behind a
|
|
44
|
+
* Spinner and fills it ~450ms later, with no DOM mutation in between — and a frame audited before
|
|
45
|
+
* that is the empty shell. Shared by the Chromium audit (`designBaseline.ts`) and the Apple pass.
|
|
46
|
+
*/
|
|
47
|
+
export function settleInPage() {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
let changed = false;
|
|
50
|
+
let calm = 0;
|
|
51
|
+
const observer = new MutationObserver(() => {
|
|
52
|
+
changed = true;
|
|
53
|
+
});
|
|
54
|
+
observer.observe(document.body, { subtree: true, childList: true, characterData: true, attributes: true });
|
|
55
|
+
const started = performance.now();
|
|
56
|
+
const poll = setInterval(() => {
|
|
57
|
+
const animating = document.getAnimations().some((a) => a.playState === 'running' && a.effect?.getComputedTiming().endTime !== Infinity);
|
|
58
|
+
const loading = [...document.querySelectorAll('.animate-spin, [aria-busy="true"]')].some((el) => el.checkVisibility({ opacityProperty: true, visibilityProperty: true }));
|
|
59
|
+
calm = changed || animating || loading ? 0 : calm + 1;
|
|
60
|
+
changed = false;
|
|
61
|
+
if (calm >= 3 || performance.now() - started > 5000) {
|
|
62
|
+
clearInterval(poll);
|
|
63
|
+
observer.disconnect();
|
|
64
|
+
requestAnimationFrame(() => resolve());
|
|
65
|
+
}
|
|
66
|
+
}, 100);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Scrolls the page's main pane to `to` and says how far it can go. */
|
|
70
|
+
export function scrollMainInPage(to) {
|
|
71
|
+
const panes = [document.scrollingElement ?? document.documentElement, ...document.querySelectorAll('*')].filter((el) => el.scrollHeight > el.clientHeight + 1 && (el === document.scrollingElement || /(auto|scroll)/.test(getComputedStyle(el).overflowY)));
|
|
72
|
+
const pane = panes.sort((a, b) => b.clientHeight * b.clientWidth - a.clientHeight * a.clientWidth)[0];
|
|
73
|
+
if (!pane)
|
|
74
|
+
return { at: 0, end: 0 };
|
|
75
|
+
pane.scrollTop = to;
|
|
76
|
+
return { at: pane.scrollTop, end: pane.scrollHeight - pane.clientHeight };
|
|
77
|
+
}
|
|
78
|
+
/** Finishes every finite animation on the document's timeline; endless motion and scroll-driven motion are left alone. */
|
|
79
|
+
export function atRestInPage() {
|
|
80
|
+
for (const motion of document.getAnimations()) {
|
|
81
|
+
if (motion.timeline !== document.timeline || !Number.isFinite(motion.effect?.getComputedTiming().endTime ?? Number.POSITIVE_INFINITY))
|
|
82
|
+
continue;
|
|
83
|
+
motion.finish();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Focuses the field at `css` the way a tap would land on it — scrolled into view, the caret on a
|
|
88
|
+
* visible line of an editor — and answers whether it took focus. In-page rather than a Playwright
|
|
89
|
+
* tap: WebDriver calls an editor's document "not interactable", and a focus is no less true.
|
|
90
|
+
*/
|
|
91
|
+
export function focusFieldInPage(css) {
|
|
92
|
+
const el = document.querySelector(css);
|
|
93
|
+
if (!(el instanceof HTMLElement))
|
|
94
|
+
return false;
|
|
95
|
+
el.scrollIntoView({ block: 'center' });
|
|
96
|
+
el.focus();
|
|
97
|
+
if (el.isContentEditable) {
|
|
98
|
+
const lines = [...el.querySelectorAll('.cm-line, p, div')].filter((line) => {
|
|
99
|
+
const r = line.getBoundingClientRect();
|
|
100
|
+
return r.height > 0 && r.top >= 0 && r.bottom <= window.innerHeight;
|
|
101
|
+
});
|
|
102
|
+
window.getSelection()?.collapse(lines[Math.floor(lines.length / 2)] ?? el, 0);
|
|
103
|
+
}
|
|
104
|
+
return document.activeElement === el || el.contains(document.activeElement);
|
|
105
|
+
}
|
|
106
|
+
/** With the focused field tapped, raises the stood-in keyboard of `keyboard` px over it and reports where the line being typed on ended up. */
|
|
107
|
+
export async function keyboardViewInPage([keyboard, what]) {
|
|
108
|
+
const H = window.innerHeight;
|
|
109
|
+
const el = document.activeElement ?? document.body;
|
|
110
|
+
// In an editor the field is the whole document; what must stay in sight is the line with the caret.
|
|
111
|
+
// A caret on an empty line has no box of its own in WebKit: then it is the line it is on, never the
|
|
112
|
+
// whole editor, whose top is scrolled away in anything longer than the strip.
|
|
113
|
+
const caret = () => {
|
|
114
|
+
const range = el instanceof HTMLElement && el.isContentEditable ? window.getSelection()?.getRangeAt(0) : null;
|
|
115
|
+
const line = range?.getClientRects()[0] ?? range?.getBoundingClientRect();
|
|
116
|
+
if (line && line.height > 0)
|
|
117
|
+
return line;
|
|
118
|
+
const node = range?.startContainer === el ? (el.childNodes[range.startOffset] ?? el.lastChild) : range?.startContainer;
|
|
119
|
+
const holder = node instanceof Element ? node : node?.parentElement;
|
|
120
|
+
const row = holder && holder !== el && el.contains(holder) ? (holder.closest('.cm-line, p, div') ?? holder) : null;
|
|
121
|
+
return (row ?? el).getBoundingClientRect();
|
|
122
|
+
};
|
|
123
|
+
const first = caret();
|
|
124
|
+
// Safari pans just far enough to show the line with the caret, a little above the keys.
|
|
125
|
+
const pan = Math.max(0, Math.min(keyboard, Math.round(first.top + Math.min(first.height, 44) + 12 - (H - keyboard))));
|
|
126
|
+
window.__keyboard(keyboard, pan);
|
|
127
|
+
await new Promise((done) => setTimeout(done, 450));
|
|
128
|
+
const now = caret();
|
|
129
|
+
const lineY = now.top + Math.min(now.height, 44) / 2;
|
|
130
|
+
const hit = document.elementFromPoint(now.left + Math.min(now.width / 2, 40), Math.min(Math.max(lineY, 0), H - 1));
|
|
131
|
+
const mine = hit && (hit === document.activeElement || document.activeElement?.contains(hit) || hit.contains(document.activeElement));
|
|
132
|
+
const label = hit ? `${hit.tagName.toLowerCase()}${hit.getAttribute('aria-label') ? ` "${hit.getAttribute('aria-label')}"` : ''}` : 'nothing';
|
|
133
|
+
return { field: what, top: now.top, bottom: now.bottom, stripTop: pan, stripBottom: pan + H - keyboard, coveredBy: mine ? null : label };
|
|
134
|
+
}
|
|
135
|
+
/** Installs the stand-in from its source when the context did not (a page `setContent` made); a no-op once it is there. */
|
|
136
|
+
export function installKeyboardStandinInPage(source) {
|
|
137
|
+
if (!window.__keyboard)
|
|
138
|
+
new Function(source)();
|
|
139
|
+
return typeof window.__keyboard === 'function';
|
|
140
|
+
}
|
|
141
|
+
/** Lowers the stood-in keyboard. */
|
|
142
|
+
export function keyboardDownInPage() {
|
|
143
|
+
window.__keyboard?.(0, 0);
|
|
144
|
+
}
|
|
145
|
+
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'root';
|
|
146
|
+
const pause = (page, ms) => (page.waitForTimeout ? page.waitForTimeout(ms) : new Promise((r) => setTimeout(r, ms)));
|
|
147
|
+
/** The same fault seen on two screens of one read-through is one fault. */
|
|
148
|
+
export function dedupe(findings) {
|
|
149
|
+
return findings.filter((f, i) => findings.findIndex((g) => g.rule === f.rule && g.selector === f.selector && g.detail === f.detail) === i);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Judges one screen on one Apple device: the design audit (less LAY-13, which `measurePhone`
|
|
153
|
+
* owns here by the reach a finger gets rather than the box) plus the phone rules, at rest, a
|
|
154
|
+
* screen at a time for a long page, and with the keyboard up over `options.keyboard`.
|
|
155
|
+
*/
|
|
156
|
+
export async function auditApple(page, route, device, options = {}) {
|
|
157
|
+
const { long, keyboard, shots, maxFrames = 12, touch = device.kind !== 'desktop', ...audit } = options;
|
|
158
|
+
const shot = (suffix) => {
|
|
159
|
+
if (!shots || !page.screenshot)
|
|
160
|
+
return Promise.resolve();
|
|
161
|
+
const path = `${shots}/${device.slug}/${slug(route)}${suffix}.png`;
|
|
162
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
163
|
+
return page.screenshot({ path }).then(() => undefined);
|
|
164
|
+
};
|
|
165
|
+
const ignore = { ...(audit.ignore ?? {}), 'LAY-13': 'measurePhone judges the reach a finger gets, halo included, in place of the box' };
|
|
166
|
+
let textNodes = 0;
|
|
167
|
+
const judgeNow = async () => {
|
|
168
|
+
await page.evaluate(settleInPage, undefined);
|
|
169
|
+
await page.evaluate(atRestInPage, undefined);
|
|
170
|
+
const design = await auditDesign(page, { ...audit, ignore, route, width: page.viewportSize()?.width });
|
|
171
|
+
textNodes = Math.max(textNodes, design.census.textNodes);
|
|
172
|
+
const phone = judgePhone(await page.evaluate(measurePhone, { targets: PHONE_TARGETS, fields: PHONE_FIELDS }), { touch });
|
|
173
|
+
return [...design.violations, ...phone];
|
|
174
|
+
};
|
|
175
|
+
const found = [];
|
|
176
|
+
// A pause, not a condition: overlays rise over ~200ms, and a measurement mid-rise is of a moving thing.
|
|
177
|
+
await pause(page, 300);
|
|
178
|
+
if (long) {
|
|
179
|
+
const view = page.viewportSize()?.height ?? 600;
|
|
180
|
+
for (let frame = 0, y = 0; frame < maxFrames; frame++, y += Math.round(view * 0.85)) {
|
|
181
|
+
const { at, end } = await page.evaluate(scrollMainInPage, y);
|
|
182
|
+
// A section that fades up once scrolled to needs its moment before `atRest` can finish it.
|
|
183
|
+
await pause(page, 350);
|
|
184
|
+
found.push(...(await judgeNow()));
|
|
185
|
+
await shot(`-${String(frame).padStart(2, '0')}`);
|
|
186
|
+
if (at >= end - 1)
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
await page.evaluate(scrollMainInPage, 0);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
found.push(...(await judgeNow()));
|
|
193
|
+
await shot('');
|
|
194
|
+
}
|
|
195
|
+
if (keyboard && device.keyboard) {
|
|
196
|
+
const focused = await page.evaluate(focusFieldInPage, keyboard);
|
|
197
|
+
if (!focused)
|
|
198
|
+
found.push({ rule: 'LAY-15', selector: keyboard, detail: 'is not there, or would not take focus, so the keyboard could not be raised over it' });
|
|
199
|
+
else if (!(await page.evaluate(installKeyboardStandinInPage, KEYBOARD_STANDIN)))
|
|
200
|
+
throw new Error('auditApple: the page has no visualViewport to stand a keyboard in on');
|
|
201
|
+
else {
|
|
202
|
+
found.push(...judgeKeyboard(await page.evaluate(keyboardViewInPage, [device.keyboard, keyboard])));
|
|
203
|
+
await shot('-keyboard');
|
|
204
|
+
await page.evaluate(keyboardDownInPage, undefined);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { violations: dedupe(found), textNodes };
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=appleWalk.js.map
|