@multiplatform.one/theme 7.63.4 → 7.64.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/theme",
3
- "version": "7.63.4",
3
+ "version": "7.64.0",
4
4
  "description": "Tamagui theme system for multiplatform.one",
5
5
  "keywords": [
6
6
  "multiplatform",
@@ -72,8 +72,8 @@
72
72
  "access": "public"
73
73
  },
74
74
  "dependencies": {
75
- "@multiplatform.one/platform": "^7.63.4",
76
- "@multiplatform.one/store": "^7.63.4",
75
+ "@multiplatform.one/platform": "^7.64.0",
76
+ "@multiplatform.one/store": "^7.64.0",
77
77
  "@tamagui/animations-react-native": "2.7.6",
78
78
  "@tamagui/colors": "2.7.6",
79
79
  "@tamagui/config": "2.7.6",
@@ -89,8 +89,8 @@
89
89
  },
90
90
  "devDependencies": {
91
91
  "@expo/prebuild-config": "55.0.18",
92
- "@multiplatform.one/config": "7.63.4",
93
- "@multiplatform.one/test-utils": "7.63.4",
92
+ "@multiplatform.one/config": "7.64.0",
93
+ "@multiplatform.one/test-utils": "7.64.0",
94
94
  "@tamagui/animations-css": "2.7.6",
95
95
  "@tamagui/animations-reanimated": "2.7.6",
96
96
  "@tamagui/font-inter": "2.7.6",
@@ -0,0 +1,96 @@
1
+ /**
2
+ * FZ-7944 round 2, LC-71: an overlay's open focus paints a ring only when the
3
+ * open came from the keyboard. Any other open lands on the frame, except a
4
+ * mouse open with a first text field, which takes the caret.
5
+ */
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { ensureKeyboardModalityTracking, focusOnOpen } from "./keyboardFocusRing";
8
+
9
+ function pointer(pointerType: "mouse" | "touch" | "pen") {
10
+ const event = new Event("pointerdown", { bubbles: true });
11
+ Object.defineProperty(event, "pointerType", { value: pointerType });
12
+ document.body.dispatchEvent(event);
13
+ }
14
+
15
+ function key() {
16
+ document.body.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
17
+ }
18
+
19
+ function overlay(tag: "button" | "input", type?: string) {
20
+ const frame = document.createElement("div");
21
+ frame.tabIndex = -1;
22
+ const target = document.createElement(tag);
23
+ if (type) target.setAttribute("type", type);
24
+ frame.append(target);
25
+ document.body.append(frame);
26
+ const focus = { frame: vi.spyOn(frame, "focus"), target: vi.spyOn(target, "focus") };
27
+ return { frame, target, focus };
28
+ }
29
+
30
+ describe("focusOnOpen (LC-71, FZ-7944)", () => {
31
+ beforeEach(() => ensureKeyboardModalityTracking());
32
+ afterEach(() => {
33
+ document.body.replaceChildren();
34
+ });
35
+
36
+ it("a keyboard open focuses the first control and paints its ring", () => {
37
+ key();
38
+ const { frame, target, focus } = overlay("button");
39
+ expect(focusOnOpen(target, frame)).toBe(target);
40
+ expect(focus.target).toHaveBeenCalledWith({ preventScroll: true, focusVisible: true });
41
+ });
42
+
43
+ it("a keyboard open still puts the caret in the first field", () => {
44
+ key();
45
+ const { frame, target, focus } = overlay("input");
46
+ expect(focusOnOpen(target, frame)).toBe(target);
47
+ expect(focus.target).toHaveBeenCalledWith({ preventScroll: true, focusVisible: true });
48
+ });
49
+
50
+ it("a mouse open lands on the frame, so the first button never rings", () => {
51
+ pointer("mouse");
52
+ const { frame, target, focus } = overlay("button");
53
+ expect(focusOnOpen(target, frame)).toBe(frame);
54
+ expect(focus.target).not.toHaveBeenCalled();
55
+ expect(focus.frame).toHaveBeenCalledWith({ preventScroll: true, focusVisible: false });
56
+ });
57
+
58
+ it("a mouse open puts the caret in a first text field", () => {
59
+ pointer("mouse");
60
+ const { frame, target, focus } = overlay("input");
61
+ expect(focusOnOpen(target, frame)).toBe(target);
62
+ expect(focus.target).toHaveBeenCalledWith({ preventScroll: true, focusVisible: false });
63
+ });
64
+
65
+ it("a touch open lands on the frame, not a text field, so no keyboard rises", () => {
66
+ pointer("touch");
67
+ const { frame, target, focus } = overlay("input");
68
+ expect(focusOnOpen(target, frame)).toBe(frame);
69
+ expect(focus.target).not.toHaveBeenCalled();
70
+ expect(focus.frame).toHaveBeenCalledWith({ preventScroll: true, focusVisible: false });
71
+ });
72
+
73
+ it("a touch open lands on the frame for a non-text control too", () => {
74
+ pointer("touch");
75
+ const { frame, target, focus } = overlay("input", "checkbox");
76
+ expect(focusOnOpen(target, frame)).toBe(frame);
77
+ expect(focus.target).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it("a frame without a tabindex is made focusable, so it can take the open", () => {
81
+ pointer("mouse");
82
+ const { frame, target } = overlay("button");
83
+ frame.removeAttribute("tabindex");
84
+ expect(focusOnOpen(target, frame)).toBe(frame);
85
+ expect(frame.getAttribute("tabindex")).toBe("-1");
86
+ expect(document.activeElement).toBe(frame);
87
+ });
88
+
89
+ it("with nothing focusable the frame takes focus", () => {
90
+ pointer("mouse");
91
+ const frame = document.createElement("div");
92
+ frame.tabIndex = -1;
93
+ document.body.append(frame);
94
+ expect(focusOnOpen(null, frame)).toBe(frame);
95
+ });
96
+ });
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  let lastInputWasKeyboard = false;
14
+ let lastPointerWasTouch = false;
14
15
  let hooked = false;
15
16
 
16
17
  /**
@@ -22,7 +23,14 @@ export function ensureKeyboardModalityTracking() {
22
23
  if (hooked || typeof document === "undefined") return;
23
24
  hooked = true;
24
25
  document.addEventListener("keydown", () => (lastInputWasKeyboard = true), true);
25
- document.addEventListener("pointerdown", () => (lastInputWasKeyboard = false), true);
26
+ document.addEventListener(
27
+ "pointerdown",
28
+ (event) => {
29
+ lastInputWasKeyboard = false;
30
+ lastPointerWasTouch = event.pointerType === "touch";
31
+ },
32
+ true,
33
+ );
26
34
  }
27
35
 
28
36
  /**
@@ -36,6 +44,35 @@ export function wasKeyboardFocus(): boolean {
36
44
  return lastInputWasKeyboard;
37
45
  }
38
46
 
47
+ function isTextEntry(node: HTMLElement): boolean {
48
+ if (node.isContentEditable || node.tagName === "TEXTAREA") return true;
49
+ if (node.tagName !== "INPUT") return false;
50
+ const type = (node as HTMLInputElement).type;
51
+ return !["button", "checkbox", "color", "file", "radio", "range", "reset", "submit"].includes(
52
+ type,
53
+ );
54
+ }
55
+
56
+ /**
57
+ * An overlay's open focus (LC-71, FZ-7944). Only a keyboard open lands on a
58
+ * control and rings it. Any other open (pointer, touch, mounted open) lands on
59
+ * the frame, which paints no ring: Chromium ignores `focusVisible: false` and
60
+ * rings a control focused before any input. A mouse open still puts the caret
61
+ * in a first text field, which shows LC-133's border; a touch open does not,
62
+ * so the keyboard does not rise over a sheet the user only looked at. A frame
63
+ * without a tabindex gets -1, or its focus() is a no-op and the browser's
64
+ * own pick stays. Returns the node that took focus.
65
+ */
66
+ export function focusOnOpen(target: HTMLElement | null, frame: HTMLElement): HTMLElement {
67
+ ensureKeyboardModalityTracking();
68
+ const keyboard = lastInputWasKeyboard;
69
+ const caret = target !== null && isTextEntry(target) && !lastPointerWasTouch;
70
+ const node = target && (keyboard || caret) ? target : frame;
71
+ if (node === frame && !frame.hasAttribute("tabindex")) frame.tabIndex = -1;
72
+ node.focus({ preventScroll: true, focusVisible: keyboard } as FocusOptions);
73
+ return node;
74
+ }
75
+
39
76
  import { FOCUS_VISIBLE_RING_INSET } from "./theme/focusState";
40
77
 
41
78
  /**
@@ -700,6 +700,55 @@ const childrenHues = [
700
700
  "gray",
701
701
  ] as const;
702
702
 
703
+ /**
704
+ * FZ-7944 round 2, LC-71: the keyboard ring is the accent, never a white or
705
+ * black neutral. A hue scope rings in its own accent ink. The roots, whose
706
+ * house accent is the neutral inverse, ring in the blue scope's ink, the
707
+ * default accent on Apple platforms. Solid accent sub-themes keep the on-fill
708
+ * ring they paint for content inside the fill.
709
+ */
710
+ function holdFocusRingAccent(
711
+ themes: Record<string, Record<string, string>>,
712
+ ): Record<string, Record<string, string>> {
713
+ const out: Record<string, Record<string, string>> = { ...themes };
714
+ for (const scheme of ["light", "dark"] as const) {
715
+ const base = themes[scheme];
716
+ if (!base) continue;
717
+ const grounds = [base.color1, base.color2].map(luminanceOf);
718
+ if (grounds.some((lum) => lum === null)) continue;
719
+ const holds = (color: string | undefined): color is string => {
720
+ const lum = luminanceOf(color);
721
+ return (
722
+ lum !== null &&
723
+ isChromatic(color) &&
724
+ grounds.every((ground) => contrastRatio(lum, ground as number) >= minContrastRatio)
725
+ );
726
+ };
727
+ const rootRing = [base.accentColor, themes[`${scheme}_blue`]?.accentColor].find(holds);
728
+ for (const [name, theme] of Object.entries(themes)) {
729
+ if (name !== scheme && !name.startsWith(`${scheme}_`)) continue;
730
+ if (theme.outlineColor === undefined) continue;
731
+ const parts = name.split("_");
732
+ if (parts.includes("accent")) continue;
733
+ const hue = (childrenHues as readonly string[]).includes(parts[1] ?? "")
734
+ ? parts[1]
735
+ : undefined;
736
+ const scopeInk = hue ? themes[`${scheme}_${hue}`]?.accentColor : undefined;
737
+ const ring = holds(scopeInk) ? scopeInk : rootRing;
738
+ if (!ring || ring === theme.outlineColor) continue;
739
+ out[name] = { ...theme, outlineColor: ring };
740
+ }
741
+ }
742
+ return out;
743
+ }
744
+
745
+ function isChromatic(color: string | undefined): boolean {
746
+ const hex = color ? normalizeToHex(color) : null;
747
+ if (!hex) return false;
748
+ const channels = [1, 3, 5].map((index) => Number.parseInt(hex.slice(index, index + 2), 16));
749
+ return Math.max(...channels) - Math.min(...channels) >= 24;
750
+ }
751
+
703
752
  export function createThemesBuilder(
704
753
  baseTheme: BaseThemeBuilderDefinition,
705
754
  accentTheme: ThemeBuilderDefinition,
@@ -769,12 +818,14 @@ export function createThemesBuilder(
769
818
  const initialTheme = tamaguiCreateThemes(
770
819
  createThemesProps as Parameters<typeof tamaguiCreateThemes>[0],
771
820
  );
772
- const themes = holdColorThemeAccent(
773
- holdSwitchTrack(
774
- holdSyntaxFloor(
775
- holdNamedInkFloor(
776
- holdSelectedState(
777
- holdAccentFloor(neutralizeSemanticEntryText(neutralizeTintText(initialTheme))),
821
+ const themes = holdFocusRingAccent(
822
+ holdColorThemeAccent(
823
+ holdSwitchTrack(
824
+ holdSyntaxFloor(
825
+ holdNamedInkFloor(
826
+ holdSelectedState(
827
+ holdAccentFloor(neutralizeSemanticEntryText(neutralizeTintText(initialTheme))),
828
+ ),
778
829
  ),
779
830
  ),
780
831
  ),
@@ -28,13 +28,13 @@ import { defaultKnobs } from "./knobs";
28
28
  import { resolveKnobs } from "./resolveKnobs";
29
29
 
30
30
  describe("focusState tokens (DG-A11Y-01 / DG-STATE-01 / W12)", () => {
31
- it("FOCUS_VISIBLE_RING meets ≥2px solid outline contract at offset 0 (F1)", () => {
31
+ it("FOCUS_VISIBLE_RING meets ≥2px solid outline contract at offset +2 (FZ-7944)", () => {
32
32
  expect(FOCUS_VISIBLE_RING.outlineWidth).toBeGreaterThanOrEqual(FOCUS_RING_MIN_WIDTH);
33
33
  expect(FOCUS_VISIBLE_RING.outlineStyle).toBe("solid");
34
34
  expect(FOCUS_VISIBLE_RING.outlineColor).toBe("$outlineColor");
35
- // Board verify pass F1: offset 0 — not −2 (band inside the fill, where it
36
- // measured 1.17–1.31:1 on solid intents) and not +2 (a gapped halo).
37
- expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(0);
35
+ // Not −2 (band inside the fill, 1.17–1.31:1 on solid intents). FZ-7944
36
+ // round 2: a 2px gap, the macOS and iPadOS keyboard ring.
37
+ expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(2);
38
38
  expect(FOCUS_RING_MIN_CONTRAST).toBeGreaterThanOrEqual(3);
39
39
  });
40
40
 
@@ -121,14 +121,14 @@ describe("$outlineColor token contrast (DF-03 / DG-A11Y-01)", () => {
121
121
  }
122
122
 
123
123
  // MPO-41 — assert the RATIO on the surface the band actually draws on, not
124
- // just the declaration. At offset 0 the band paints OUTSIDE the border box,
124
+ // just the declaration. At offset +2 the band paints OUTSIDE the border box,
125
125
  // on the page ground behind the control; intent Buttons pin the parent-scope
126
126
  // `$outlineColor` literal on the ring (LC-51), so the pair that must clear
127
127
  // the floor is base `outlineColor` × base ground — for accent and error
128
128
  // controls alike, in both schemes.
129
129
  for (const scheme of ["light", "dark"] as const) {
130
130
  for (const intent of ["accent", "error"] as const) {
131
- it(`${scheme} ${intent}: offset-0 ring clears ≥3:1 on the page ground it draws on`, () => {
131
+ it(`${scheme} ${intent}: the ring clears ≥3:1 on the page ground it draws on`, () => {
132
132
  const base = themes[scheme];
133
133
  expect(themes[`${scheme}_${intent}`]).toBeTruthy();
134
134
  const ring = normalizeToHex(base.outlineColor);
@@ -175,21 +175,21 @@ describe("the ring still paints inside a clipped / fused group (LC-71 §4 inset
175
175
  expect(keyboardFocusRingProps).toEqual(FOCUS_VISIBLE_RING_INSET);
176
176
  });
177
177
 
178
- it("ensureFocusVisibleRing honors the explicit per-call-site inset and defaults to 0", () => {
178
+ it("ensureFocusVisibleRing honors the explicit per-call-site inset and defaults to +2", () => {
179
179
  expect(ensureFocusVisibleRing({ outlineOffset: -2 }).outlineOffset).toBe(-2);
180
- expect(ensureFocusVisibleRing({ outlineWidth: 3 }).outlineOffset).toBe(0);
181
- expect(ensureFocusVisibleRing().outlineOffset).toBe(0);
180
+ expect(ensureFocusVisibleRing({ outlineWidth: 3 }).outlineOffset).toBe(2);
181
+ expect(ensureFocusVisibleRing().outlineOffset).toBe(2);
182
182
  });
183
183
  });
184
184
 
185
185
  describe("LC-71: keyboard-only; measure the ring / text node, not the frame", () => {
186
- it("standalone ring paints a 2px band on the ring-carrying node's box at offset 0", () => {
186
+ it("standalone ring paints a 2px band 2px clear of the ring-carrying node's box", () => {
187
187
  // Measure the node that paints the outline (the ring carrier / text node),
188
188
  // never an ancestor Frame. Band = [offset, offset + width] from THAT box.
189
189
  const { outlineOffset, outlineWidth } = FOCUS_VISIBLE_RING;
190
190
  expect(outlineWidth).toBe(2);
191
- expect(outlineOffset).toBe(0);
192
- expect(outlineOffset + outlineWidth).toBe(2);
191
+ expect(outlineOffset).toBe(2);
192
+ expect(outlineOffset + outlineWidth).toBe(4);
193
193
  });
194
194
 
195
195
  it("chip dismiss rings the glyph node, not the 44px hit-box frame", () => {
@@ -287,10 +287,10 @@ describe("theme DEV warn (outline-none / positive-tabindex)", () => {
287
287
  * shipping and being found in a later audit.
288
288
  */
289
289
  describe("LC-71 §4: the sanctioned focus-ring offset set is closed (MPO-92)", () => {
290
- it("is exactly three offsets — clipping inset, default, shape halo", () => {
290
+ it("is exactly three offsets — clipping inset, flush, default", () => {
291
291
  expect([...SANCTIONED_FOCUS_RING_OFFSETS]).toEqual([-2, 0, 2]);
292
292
  expect(FOCUS_RING_CLIPPED_OFFSET).toBe(-2);
293
- expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(0);
293
+ expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(2);
294
294
  expect(FOCUS_RING_HALO_OFFSET).toBe(2);
295
295
  });
296
296
 
@@ -312,7 +312,7 @@ describe("LC-71 §4: the sanctioned focus-ring offset set is closed (MPO-92)", (
312
312
  });
313
313
  // The band spans [outlineOffset, outlineOffset + outlineWidth] from the
314
314
  // border edge — here [2, 4]. Inner edge ≥ 0 means it paints entirely on the
315
- // surface BEHIND the control, exactly like the default offset 0, so it
315
+ // surface BEHIND the control, exactly like the default ring, so it
316
316
  // measures against the page ground and cannot reproduce the 1.17–1.31:1
317
317
  // inside-the-fill failure MPO-41 retired.
318
318
  expect(FOCUS_VISIBLE_RING_HALO.outlineOffset).toBeGreaterThanOrEqual(0);
@@ -48,31 +48,26 @@ export function focusRingColorMeetsContrast(ringColor: string, surfaceColor: str
48
48
 
49
49
  /**
50
50
  * ONE ring geometry (DG-A11Y-01 / LC-71 §4): 2px solid `$outlineColor` at
51
- * offset **0** — the band hugs the border box and paints OUTSIDE it, on the
52
- * surface behind the control (reference §10; board F1 verify pass: offset 0,
53
- * not −2 and not +2). Offset 0 is what makes the ≥3:1 floor computable: the
54
- * ring contrasts against the page ground, not the control's own fill. The
55
- * old flat inset (−2) painted the band INSIDE solid intent fills and sank to
56
- * 1.17–1.31:1 (light accent 1.27, light error 1.17, dark error 1.31) where
57
- * the same ring on the page ground clears 5.78–6.50:1 (MPO-41 / MPO-19 X7).
51
+ * offset **+2**, a band clear of the border box with a 2px gap, painted on the
52
+ * surface behind the control. FZ-7944 round 2 moved the default from 0 to +2
53
+ * (the macOS and iPadOS keyboard ring); the ≥3:1 floor stays computable
54
+ * because the band still paints on the page ground, never inside a fill. The
55
+ * old flat inset (−2) painted INSIDE solid intent fills and sank to
56
+ * 1.17–1.31:1 (MPO-41 / MPO-19 X7).
58
57
  *
59
- * The inset did not die — it moved to `FOCUS_VISIBLE_RING_INSET`, an
60
- * explicit per-call-site carve-out for clipping ancestors. MPO-19 X7 names
61
- * the same offset `FOCUS_RING_CLIPPED_OFFSET` so call sites do not type `-2`.
62
- *
63
- * MPO-92 ratified a second, shape-scoped carve-out on the other side:
64
- * `FOCUS_VISIBLE_RING_HALO` (+2) for pill/circular thumbs and inline text
65
- * links. The F1 "not +2" reading was about the DEFAULT — a blanket +2 on
66
- * every control — and it stands. The halo is a named carve-out with a written
67
- * shape scope, exactly as the inset is; `SANCTIONED_FOCUS_RING_OFFSETS` below
68
- * is the closed set of all three.
58
+ * Two carve-outs stay named: `FOCUS_VISIBLE_RING_INSET` (−2) for clipping
59
+ * ancestors, and `FOCUS_RING_FLUSH_OFFSET` (0) for rows and cells that sit
60
+ * edge to edge inside a padded overlay, where a gapped band would collide
61
+ * with the neighbour. `SANCTIONED_FOCUS_RING_OFFSETS` is the closed set.
69
62
  */
70
63
  export const FOCUS_RING_CLIPPED_OFFSET = -2;
64
+ export const FOCUS_RING_FLUSH_OFFSET = 0;
65
+ export const FOCUS_RING_HALO_OFFSET = 2;
71
66
  export const FOCUS_VISIBLE_RING = {
72
67
  outlineWidth: FOCUS_RING_MIN_WIDTH,
73
68
  outlineStyle: "solid" as const,
74
69
  outlineColor: "$outlineColor",
75
- outlineOffset: 0,
70
+ outlineOffset: FOCUS_RING_HALO_OFFSET,
76
71
  } satisfies ControlStateProps;
77
72
 
78
73
  /**
@@ -94,29 +89,10 @@ export const FOCUS_VISIBLE_RING_INSET = {
94
89
  } satisfies ControlStateProps;
95
90
 
96
91
  /**
97
- * The LC-71 §4 halo carve-out (MPO-92, ratified 2026-09-02): the SAME ring,
98
- * pushed 2px clear of the border box, for the two shapes where a band hugging
99
- * the edge reads as part of the glyph instead of around it.
100
- *
101
- * Scope is by SHAPE, not by instance, and it is closed at two:
102
- * - **pill / circular thumbs** — a round handle the user drags (Slider
103
- * thumb, Rating star, TimeRangeScrubber handle). At offset 0 the band is
104
- * concentric with the glyph's own edge and reads as a thicker glyph.
105
- * - **inline text links** — a link inside a run of prose (markdown/mdx
106
- * `Link`, ErrorSummary's summary link, backoffice "View All"). At offset 0
107
- * the band crosses the line box and collides with ascenders/descenders of
108
- * the surrounding text.
109
- *
110
- * NOT for standalone rectangles on the page ground — cards, tiles, rows,
111
- * fields. Those are the default `FOCUS_VISIBLE_RING` (offset 0), which is
112
- * what makes the Axiom 12 ≥3:1 floor computable against the page ground.
113
- *
114
- * Contrast is unaffected: like offset 0 the halo paints OUTSIDE the border
115
- * box, on the surface behind the control, so it measures against the same
116
- * page ground the default does — the 1.17–1.31:1 failures MPO-41 retired were
117
- * the INSET painting inside solid intent fills, a defect the halo cannot have.
92
+ * The halo (MPO-92): pill and circular thumbs and inline text links. Since
93
+ * FZ-7944 round 2 it equals the default ring; the name stays so those call
94
+ * sites keep saying why they hold +2 if the default ever moves again.
118
95
  */
119
- export const FOCUS_RING_HALO_OFFSET = 2;
120
96
  export const FOCUS_VISIBLE_RING_HALO = {
121
97
  ...FOCUS_VISIBLE_RING,
122
98
  outlineOffset: FOCUS_RING_HALO_OFFSET,
@@ -124,7 +100,7 @@ export const FOCUS_VISIBLE_RING_HALO = {
124
100
 
125
101
  /**
126
102
  * The CLOSED set of sanctioned focus-ring offsets (LC-71 §4). Three, and only
127
- * three: the default 0, the clipping inset −2, the shape halo +2. A call site
103
+ * three: the clipping inset −2, the flush 0, the default +2. A call site
128
104
  * that wants any other number is a rule gap, not a preference — file it and
129
105
  * amend §4, do not type the number. `focusState.spec.ts` asserts this set is
130
106
  * exactly what the repo paints, so a fourth offset fails a test instead of
@@ -132,8 +108,8 @@ export const FOCUS_VISIBLE_RING_HALO = {
132
108
  */
133
109
  export const SANCTIONED_FOCUS_RING_OFFSETS = [
134
110
  FOCUS_RING_CLIPPED_OFFSET,
111
+ FOCUS_RING_FLUSH_OFFSET,
135
112
  FOCUS_VISIBLE_RING.outlineOffset,
136
- FOCUS_RING_HALO_OFFSET,
137
113
  ] as const;
138
114
 
139
115
  /** True when `offset` is one of the three offsets LC-71 §4 sanctions. */
@@ -50,7 +50,7 @@ describe("LC-148 HAIRLINE-TOKEN: borderWidth small is the theme's hairline", ()
50
50
  controlBorderOutlined: "$borderColorHairline",
51
51
  elevatedSurface: "$borderColorHairline",
52
52
  });
53
- expect(edges({ borderWidth: "small" }, "dark").elevatedSurface).toBe("$color5");
53
+ expect(edges({ borderWidth: "small" }, "dark").elevatedSurface).toBe("transparent");
54
54
  expect(edges({ borderWidth: "none", fillStyle: "outlined" }).borderRadius).toBe(
55
55
  "$borderColorHairline",
56
56
  );
@@ -64,7 +64,7 @@ describe("LC-148 HAIRLINE-TOKEN: borderWidth small is the theme's hairline", ()
64
64
  controlBorderOutlined: "$borderColor",
65
65
  elevatedSurface: "$borderColor",
66
66
  });
67
- expect(edges({ borderWidth }, "dark").elevatedSurface).toBe("$color6");
67
+ expect(edges({ borderWidth }, "dark").elevatedSurface).toBe("transparent");
68
68
  }
69
69
  });
70
70
 
@@ -1,6 +1,12 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
1
3
  import { describe, expect, it } from "vitest";
4
+ import { GAP_LG_KNOB } from "../audit/constraintAudit";
2
5
  import { animationConfig, animationDurations, motionStops } from "./animations/css";
6
+ import { transitionProps } from "./transitionProps";
7
+ import { transitionProps as nativeTransitionProps } from "./transitionProps.native";
3
8
  import {
9
+ motionExitFactor,
4
10
  motionFor,
5
11
  motionDurationMs,
6
12
  motionSeconds,
@@ -82,3 +88,134 @@ describe("motion durations", () => {
82
88
  expect(motionSettleMs(["quick", { opacity: "slow" }])).toBe(980);
83
89
  });
84
90
  });
91
+
92
+ const bouncyStops = ["bouncy", "superBouncy"];
93
+ const nonBouncy = motionStops.filter((stop) => !bouncyStops.includes(stop));
94
+ const ms = (stop: unknown) => animationDurations[stop as keyof typeof animationDurations];
95
+
96
+ function bezierYs(timing: string): number[] {
97
+ const match = timing.match(/cubic-bezier\(([^)]+)\)/);
98
+ if (!match) return [];
99
+ const [, y1, , y2] = match[1].split(",").map(Number);
100
+ return [y1, y2];
101
+ }
102
+
103
+ const publicRoot = path.resolve(__dirname, "../../..");
104
+
105
+ function sources(dir: string, out: string[] = []): string[] {
106
+ for (const name of readdirSync(dir)) {
107
+ if (name === "node_modules" || name === "dist" || name === "types") continue;
108
+ const file = path.join(dir, name);
109
+ if (statSync(file).isDirectory()) sources(file, out);
110
+ else if (/\.tsx?$/.test(name) && !/\.(spec|test|stories)\.tsx?$|\.d\.ts$/.test(name)) {
111
+ out.push(file);
112
+ }
113
+ }
114
+ return out;
115
+ }
116
+
117
+ function enterExitObjects(source: string): string[] {
118
+ const objects: string[] = [];
119
+ for (const match of source.matchAll(
120
+ /\b(?:enterStyle|exitStyle)\s*(?:=\{|:)\s*(?:[^{}?]*\?\s*)?(\{[^{}]*\}|[A-Za-z_$][\w$]*)/g,
121
+ )) {
122
+ const value = match[1];
123
+ if (value.startsWith("{")) objects.push(value);
124
+ else {
125
+ const declared = source.match(
126
+ new RegExp(`\\b(?:const|let)\\s+${value}\\s*=\\s*(\\{[^{}]*\\})`),
127
+ );
128
+ if (declared) objects.push(declared[1]);
129
+ }
130
+ }
131
+ return objects;
132
+ }
133
+
134
+ function numbersIn(expression: string, source: string): number[] {
135
+ const values = [...expression.matchAll(/-?\d+(?:\.\d+)?/g)].map((m) => Math.abs(Number(m[0])));
136
+ for (const [identifier] of expression.matchAll(/[A-Za-z_$][\w$]*/g)) {
137
+ const declared = source.match(new RegExp(`\\bconst\\s+${identifier}\\s*=\\s*([^;\\n]+)`));
138
+ if (declared && !/[{(]/.test(declared[1])) {
139
+ values.push(
140
+ ...[...declared[1].matchAll(/-?\d+(?:\.\d+)?/g)].map((m) => Math.abs(Number(m[0]))),
141
+ );
142
+ }
143
+ }
144
+ return values;
145
+ }
146
+
147
+ describe("LC-178 MOTION-CHARACTER: quick in, quicker out, short and still", () => {
148
+ it("exits every role faster than it enters, about two thirds of the enter duration", () => {
149
+ for (const stop of motionStops) {
150
+ for (const [name, role] of Object.entries(motionFor(stop))) {
151
+ const enter = ms(role.transition);
152
+ const exit = ms(role.exit);
153
+ const shorter = nonBouncy.filter((candidate) => ms(candidate) < enter);
154
+ if (shorter.length === 0) {
155
+ expect(role.exit, `${stop}.${name}`).toBe(role.transition);
156
+ continue;
157
+ }
158
+ expect(exit, `${stop}.${name} exit`).toBeLessThan(enter);
159
+ const target = enter * motionExitFactor;
160
+ const best = Math.min(...shorter.map((candidate) => Math.abs(ms(candidate) - target)));
161
+ expect(Math.abs(exit - target), `${stop}.${name} exit near 2/3`).toBe(best);
162
+ }
163
+ }
164
+ for (const role of Object.values(motionFor("none"))) expect(role.exit).toBeUndefined();
165
+ });
166
+
167
+ it("never picks a bouncy stop, so no role overshoots opacity", () => {
168
+ for (const stop of motionStops) {
169
+ for (const [name, role] of Object.entries(motionFor(stop))) {
170
+ const presetBounce = name === "control" && bouncyStops.includes(stop);
171
+ for (const leg of presetBounce ? [role.exit] : [role.transition, role.exit]) {
172
+ expect(bouncyStops, `${stop}.${name}`).not.toContain(leg);
173
+ for (const y of bezierYs(animationConfig[leg as keyof typeof animationConfig])) {
174
+ expect(y, `${stop}.${name} ${String(leg)} curve`).toBeGreaterThanOrEqual(0);
175
+ expect(y, `${stop}.${name} ${String(leg)} curve`).toBeLessThanOrEqual(1);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ });
181
+
182
+ it("hands the exit stop to the driver, and native springs clamp their overshoot", () => {
183
+ const { overlay } = motionFor("quick");
184
+ expect(transitionProps(overlay).transition).toEqual(["quicker", { exit: "quickest" }]);
185
+ expect(nativeTransitionProps(overlay).transition).toEqual([
186
+ "quicker",
187
+ { overshootClamping: true, exit: "quickest" },
188
+ ]);
189
+ const shortest = motionFor("quickestLessBouncy").control;
190
+ expect(nativeTransitionProps(shortest).transition).toEqual([
191
+ "quickestLessBouncy",
192
+ { overshootClamping: true },
193
+ ]);
194
+ });
195
+
196
+ it("keeps every enter and exit offset in public/* within gapLg and its opacity in [0, 1]", () => {
197
+ const limit = GAP_LG_KNOB.gapLg;
198
+ expect(limit).toBeGreaterThan(0);
199
+ const travel: string[] = [];
200
+ const opacity: string[] = [];
201
+ let seen = 0;
202
+ for (const file of sources(publicRoot)) {
203
+ const source = readFileSync(file, "utf8");
204
+ for (const object of enterExitObjects(source)) {
205
+ seen += 1;
206
+ const where = `${path.relative(publicRoot, file)}: ${object}`;
207
+ for (const [, axis, expression] of object.matchAll(/\b([xy])\s*:\s*([^,}]+)/g)) {
208
+ if (numbersIn(expression, source).some((value) => value > limit)) {
209
+ travel.push(`${where} (${axis} past gapLg ${limit})`);
210
+ }
211
+ }
212
+ for (const [, value] of object.matchAll(/\bopacity\s*:\s*(-?\d+(?:\.\d+)?)/g)) {
213
+ if (Number(value) < 0 || Number(value) > 1) opacity.push(where);
214
+ }
215
+ }
216
+ }
217
+ expect(seen).toBeGreaterThan(10);
218
+ expect(travel).toEqual([]);
219
+ expect(opacity).toEqual([]);
220
+ });
221
+ });
@@ -9,9 +9,12 @@ export type MotionRoleName = "control" | "overlay" | "scrim" | "reveal" | "slide
9
9
 
10
10
  export interface MotionRole {
11
11
  transition: TransitionProp | undefined;
12
+ exit?: TransitionProp | undefined;
12
13
  animateOnly: readonly string[];
13
14
  }
14
15
 
16
+ export const motionExitFactor = 2 / 3;
17
+
15
18
  export const motionRoleProperties = {
16
19
  control: ["backgroundColor", "borderColor", "color", "opacity", "transform"],
17
20
  overlay: ["opacity", "transform"],
@@ -22,8 +25,8 @@ export const motionRoleProperties = {
22
25
 
23
26
  const nonBouncyStops = motionStops.filter((stop) => stop !== "superBouncy" && stop !== "bouncy");
24
27
 
25
- function nearestStop(duration: number): MotionStop {
26
- return nonBouncyStops.reduce((nearest, candidate) =>
28
+ function nearestStop(duration: number, stops: readonly MotionStop[] = nonBouncyStops): MotionStop {
29
+ return stops.reduce((nearest, candidate) =>
27
30
  Math.abs(animationDurations[candidate] - duration) <
28
31
  Math.abs(animationDurations[nearest] - duration)
29
32
  ? candidate
@@ -31,18 +34,28 @@ function nearestStop(duration: number): MotionStop {
31
34
  );
32
35
  }
33
36
 
37
+ function exitStop(enter: MotionStop): MotionStop {
38
+ const enterMs = animationDurations[enter];
39
+ const shorter = nonBouncyStops.filter((stop) => animationDurations[stop] < enterMs);
40
+ return shorter.length === 0 ? enter : nearestStop(enterMs * motionExitFactor, shorter);
41
+ }
42
+
34
43
  export function motionFor(stop: Animation): Record<MotionRoleName, MotionRole> {
35
44
  const transition = stop === "none" ? undefined : stop;
36
45
  const duration = motionDurationMs(transition);
37
- const role = (name: MotionRoleName, factor: number): MotionRole => ({
38
- transition:
46
+ const role = (name: MotionRoleName, factor: number): MotionRole => {
47
+ const enter =
39
48
  transition === undefined
40
49
  ? undefined
41
50
  : name === "control"
42
51
  ? transition
43
- : nearestStop(duration * factor),
44
- animateOnly: motionRoleProperties[name],
45
- });
52
+ : nearestStop(duration * factor);
53
+ return {
54
+ transition: enter,
55
+ exit: enter === undefined ? undefined : exitStop(enter),
56
+ animateOnly: motionRoleProperties[name],
57
+ };
58
+ };
46
59
  return {
47
60
  control: role("control", 1),
48
61
  overlay: role("overlay", 0.8),
@@ -208,9 +208,9 @@ describe("MPO-19 X5/X6 — surfaces address semantic tokens, so sub-themes work"
208
208
  });
209
209
 
210
210
  describe("MPO-19 X7 — the focus ring is inset only where a clip demands it", () => {
211
- it("the default sits on the border-box edge, as Tamagui paints it", () => {
212
- expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(0);
213
- expect(ensureFocusVisibleRing().outlineOffset).toBe(0);
211
+ it("the default sits 2px clear of the border box (FZ-7944 round 2)", () => {
212
+ expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(2);
213
+ expect(ensureFocusVisibleRing().outlineOffset).toBe(2);
214
214
  });
215
215
 
216
216
  it("the ring shape itself is unchanged — 2px solid, non-removable", () => {
@@ -1,3 +1,4 @@
1
+ import { focusOnOpen, wasKeyboardFocus } from "../keyboardFocusRing";
1
2
  import { transitionProps } from "./transitionProps";
2
3
  import { Surface } from "./Surface";
3
4
  import { useResolvedKnobs } from "./useResolvedKnobs";
@@ -40,7 +41,9 @@ const overlayFrameFocusStyle = {
40
41
  // tamagui's deferred path still runs. Dialog content sits in a native
41
42
  // <dialog> whose show() puts focus inside before the check in browsers,
42
43
  // so there FocusScope never calls the handler; useDialogShowFocus does
43
- // when show() left focus on something that is not a tab stop (MPO-470).
44
+ // when show() left focus on something that is not a tab stop (MPO-470), or
45
+ // on any control after a non-keyboard open, which Chromium would ring
46
+ // (LC-71, FZ-7944).
44
47
 
45
48
  function isTabbableWithin(node: HTMLElement, container: HTMLElement): boolean {
46
49
  if (node.tabIndex < 0 || node.hidden || node.tagName === "A") return false;
@@ -67,8 +70,10 @@ function focusFirstTabbableOnOpen(event: Event): void {
67
70
  );
68
71
  if (!first) return;
69
72
  event.preventDefault();
70
- first.focus({ preventScroll: true });
71
- if (typeof HTMLInputElement !== "undefined" && first instanceof HTMLInputElement) first.select();
73
+ const focused = focusOnOpen(first, container);
74
+ if (typeof HTMLInputElement !== "undefined" && focused instanceof HTMLInputElement) {
75
+ focused.select();
76
+ }
72
77
  }
73
78
 
74
79
  type OpenAutoFocus = (event: Event) => void;
@@ -100,7 +105,7 @@ function useDialogShowFocus(onOpenAutoFocus: OpenAutoFocus): void {
100
105
  const content = document.getElementById(contentId);
101
106
  const focused = document.activeElement;
102
107
  if (!content || typeof HTMLElement === "undefined" || !(focused instanceof HTMLElement)) return;
103
- if (!content.contains(focused) || focused.tabIndex >= 0) return;
108
+ if (!content.contains(focused) || (focused.tabIndex >= 0 && wasKeyboardFocus())) return;
104
109
  content.addEventListener(DIALOG_SHOW_FOCUS_EVENT, onOpenAutoFocus, { once: true });
105
110
  content.dispatchEvent(new CustomEvent(DIALOG_SHOW_FOCUS_EVENT, { cancelable: true }));
106
111
  }, [open, contentId]);
@@ -315,7 +315,7 @@ describe("resolveKnobs", () => {
315
315
  scheme === "dark" ? "$color4" : "$background",
316
316
  );
317
317
  expect(knobProps.elevatedSurface.borderColor).toBe(
318
- scheme === "dark" ? "$color6" : "$borderColor",
318
+ scheme === "dark" ? "transparent" : "$borderColor",
319
319
  );
320
320
  if (!key) {
321
321
  expect(knobProps.elevationChrome).toEqual({});
@@ -456,10 +456,10 @@ function elevationChromeFor(key: SizeKey | undefined): ElevationChromeProps {
456
456
  };
457
457
  }
458
458
 
459
- // v5's surface2 template steps: fill and edge two palette steps above the
460
- // base `$background`/`$borderColor` (color2/color4), one above Card's surface1.
461
- // The fill alone would land on the base edge step and erase the border.
462
- const surface2 = { fill: "$color4", edge: "$color6" } as const;
459
+ // v5's surface2 template step: the fill two palette steps above the base
460
+ // `$background` (color2), one above Card's surface1. A dark overlay separates
461
+ // by this lighter fill and its shadow, with no light edge (HIG, FZ-7944).
462
+ const surface2 = { fill: "$color4" } as const;
463
463
 
464
464
  // `$textMuted` is the theme's secondary ink (LC-152): the most muted ramp
465
465
  // step that clears 7:1 on the page and every card tier in its own scheme. A
@@ -801,7 +801,7 @@ export function resolveKnobs(
801
801
  }
802
802
  : {
803
803
  backgroundColor: scheme === "dark" ? surface2.fill : "$background",
804
- borderColor: edge(scheme === "dark" ? surface2.edge : "$borderColor", borderWidthValue),
804
+ borderColor: scheme === "dark" ? "transparent" : edge("$borderColor", borderWidthValue),
805
805
  borderWidth: borderWidthValue,
806
806
  borderRadius: radiusToken,
807
807
  elevation: platformElevation(overlayElevationToken),
@@ -1,5 +1,6 @@
1
1
  import type { MotionRole } from "./motion";
2
2
  import type { TransitionProp } from "tamagui";
3
+ import { animationConfig as nativeAnimationConfig } from "./animations/reactNative";
3
4
 
4
5
  /**
5
6
  * Native twin of `transitionProps.ts`.
@@ -23,12 +24,26 @@ export function transitionProps(
23
24
  const properties = [...role.animateOnly, ...extra].flatMap(
24
25
  (property) => nativeLonghands[property] ?? [property],
25
26
  );
27
+ if (typeof transition !== "string") return { transition, animateOnly: properties };
28
+ const exit = typeof role.exit === "string" && role.exit !== transition ? role.exit : undefined;
29
+ const clamp = isSpring(transition) && !presetBounce.has(transition);
30
+ if (!exit && !clamp) return { transition, animateOnly: properties };
26
31
  return {
27
- transition,
32
+ transition: [
33
+ transition,
34
+ { ...(clamp ? { overshootClamping: true } : {}), ...(exit ? { exit } : {}) },
35
+ ],
28
36
  animateOnly: properties,
29
37
  };
30
38
  }
31
39
 
40
+ function isSpring(stop: string): boolean {
41
+ const config = nativeAnimationConfig[stop as keyof typeof nativeAnimationConfig] as
42
+ | { type?: string }
43
+ | undefined;
44
+ return config !== undefined && config.type !== "timing";
45
+ }
46
+
32
47
  /**
33
48
  * FZ-7919: tamagui expands `borderColor` into its four sides before the React
34
49
  * Native driver sees the style, and the driver matches `animateOnly` by exact
@@ -36,6 +51,8 @@ export function transitionProps(
36
51
  * (the bubbled focus of a field's text input) animated them. The animated
37
52
  * copy outranked the static one and held the previous focus or theme colour.
38
53
  */
54
+ const presetBounce = new Set(["bouncy", "superBouncy"]);
55
+
39
56
  const nativeLonghands: Record<string, readonly string[]> = {
40
57
  borderColor: [
41
58
  "borderColor",
@@ -24,7 +24,7 @@ describe("role property scopes", () => {
24
24
  });
25
25
  it("hyphenates the control scope and additional properties", () => {
26
26
  expect(transitionProps(motionFor("quick").control, ["maxHeight", "left"])).toEqual({
27
- transition: "quick",
27
+ transition: ["quick", { exit: "quickerLessBouncy" }],
28
28
  animateOnly: [
29
29
  "background-color",
30
30
  "border-color",
@@ -41,7 +41,7 @@ describe("role property scopes", () => {
41
41
  expect(nativeTransitionProps(undefined)).toEqual({});
42
42
  expect(nativeTransitionProps("quick")).toEqual({ transition: "quick" });
43
43
  expect(nativeTransitionProps(motionFor("quick").control, ["maxHeight"])).toEqual({
44
- transition: "quick",
44
+ transition: ["quick", { overshootClamping: true, exit: "quickerLessBouncy" }],
45
45
  animateOnly: [
46
46
  "backgroundColor",
47
47
  "borderColor",
@@ -22,13 +22,23 @@ export function transitionProps(
22
22
  if (!isRole) return { transition };
23
23
  const properties = [...role.animateOnly, ...extra];
24
24
  return {
25
- transition,
25
+ transition: withExit(transition, role.exit),
26
26
  animateOnly: properties.map((property) =>
27
27
  property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
28
28
  ),
29
29
  };
30
30
  }
31
31
 
32
+ function withExit(
33
+ transition: TransitionProp | undefined,
34
+ exit: TransitionProp | undefined,
35
+ ): TransitionProp | undefined {
36
+ if (typeof transition !== "string" || typeof exit !== "string" || exit === transition) {
37
+ return transition;
38
+ }
39
+ return [transition, { exit }];
40
+ }
41
+
32
42
  function isMotionRole(value: MotionRole | TransitionProp | undefined): value is MotionRole {
33
43
  return (
34
44
  typeof value === "object" &&
@@ -22,6 +22,17 @@ export declare function ensureKeyboardModalityTracking(): void;
22
22
  * false) and during SSR.
23
23
  */
24
24
  export declare function wasKeyboardFocus(): boolean;
25
+ /**
26
+ * An overlay's open focus (LC-71, FZ-7944). Only a keyboard open lands on a
27
+ * control and rings it. Any other open (pointer, touch, mounted open) lands on
28
+ * the frame, which paints no ring: Chromium ignores `focusVisible: false` and
29
+ * rings a control focused before any input. A mouse open still puts the caret
30
+ * in a first text field, which shows LC-133's border; a touch open does not,
31
+ * so the keyboard does not rise over a sheet the user only looked at. A frame
32
+ * without a tabindex gets -1, or its focus() is a no-op and the browser's
33
+ * own pick stays. Returns the node that took focus.
34
+ */
35
+ export declare function focusOnOpen(target: HTMLElement | null, frame: HTMLElement): HTMLElement;
25
36
  /**
26
37
  * Standard manual ring props — the INSET ring geometry
27
38
  * (`FOCUS_VISIBLE_RING_INSET`: 2px solid `$outlineColor`, offset −2), spread
@@ -1 +1 @@
1
- {"version":3,"file":"keyboardFocusRing.d.ts","sourceRoot":"","sources":["../src/keyboardFocusRing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH;;;;GAIG;AACH,wBAAgB,8BAA8B,SAK7C;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAG1C;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB;;;;;CAEzB,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,8uCA+BhC,CAAC;AAIH,uEAAuE;AACvE,wBAAgB,wBAAwB,IAAI,IAAI,CAO/C"}
1
+ {"version":3,"file":"keyboardFocusRing.d.ts","sourceRoot":"","sources":["../src/keyboardFocusRing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH;;;;GAIG;AACH,wBAAgB,8BAA8B,SAY7C;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAG1C;AAWD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,EAAE,KAAK,EAAE,WAAW,GAAG,WAAW,CAQvF;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB;;;;;CAEzB,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,8uCA+BhC,CAAC;AAIH,uEAAuE;AACvE,wBAAgB,wBAAwB,IAAI,IAAI,CAO/C"}
@@ -1 +1 @@
1
- {"version":3,"file":"createThemes.d.ts","sourceRoot":"","sources":["../../src/theme/createThemes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"createThemes.d.ts","sourceRoot":"","sources":["../../src/theme/createThemes.ts"],"names":[],"mappings":"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"}
@@ -16,26 +16,21 @@ export declare const FOCUS_RING_MIN_CONTRAST = 3;
16
16
  export declare function focusRingColorMeetsContrast(ringColor: string, surfaceColor: string): boolean;
17
17
  /**
18
18
  * ONE ring geometry (DG-A11Y-01 / LC-71 §4): 2px solid `$outlineColor` at
19
- * offset **0** — the band hugs the border box and paints OUTSIDE it, on the
20
- * surface behind the control (reference §10; board F1 verify pass: offset 0,
21
- * not −2 and not +2). Offset 0 is what makes the ≥3:1 floor computable: the
22
- * ring contrasts against the page ground, not the control's own fill. The
23
- * old flat inset (−2) painted the band INSIDE solid intent fills and sank to
24
- * 1.17–1.31:1 (light accent 1.27, light error 1.17, dark error 1.31) where
25
- * the same ring on the page ground clears 5.78–6.50:1 (MPO-41 / MPO-19 X7).
19
+ * offset **+2**, a band clear of the border box with a 2px gap, painted on the
20
+ * surface behind the control. FZ-7944 round 2 moved the default from 0 to +2
21
+ * (the macOS and iPadOS keyboard ring); the ≥3:1 floor stays computable
22
+ * because the band still paints on the page ground, never inside a fill. The
23
+ * old flat inset (−2) painted INSIDE solid intent fills and sank to
24
+ * 1.17–1.31:1 (MPO-41 / MPO-19 X7).
26
25
  *
27
- * The inset did not die — it moved to `FOCUS_VISIBLE_RING_INSET`, an
28
- * explicit per-call-site carve-out for clipping ancestors. MPO-19 X7 names
29
- * the same offset `FOCUS_RING_CLIPPED_OFFSET` so call sites do not type `-2`.
30
- *
31
- * MPO-92 ratified a second, shape-scoped carve-out on the other side:
32
- * `FOCUS_VISIBLE_RING_HALO` (+2) for pill/circular thumbs and inline text
33
- * links. The F1 "not +2" reading was about the DEFAULT — a blanket +2 on
34
- * every control — and it stands. The halo is a named carve-out with a written
35
- * shape scope, exactly as the inset is; `SANCTIONED_FOCUS_RING_OFFSETS` below
36
- * is the closed set of all three.
26
+ * Two carve-outs stay named: `FOCUS_VISIBLE_RING_INSET` (−2) for clipping
27
+ * ancestors, and `FOCUS_RING_FLUSH_OFFSET` (0) for rows and cells that sit
28
+ * edge to edge inside a padded overlay, where a gapped band would collide
29
+ * with the neighbour. `SANCTIONED_FOCUS_RING_OFFSETS` is the closed set.
37
30
  */
38
31
  export declare const FOCUS_RING_CLIPPED_OFFSET = -2;
32
+ export declare const FOCUS_RING_FLUSH_OFFSET = 0;
33
+ export declare const FOCUS_RING_HALO_OFFSET = 2;
39
34
  export declare const FOCUS_VISIBLE_RING: {
40
35
  outlineWidth: number;
41
36
  outlineStyle: "solid";
@@ -62,29 +57,10 @@ export declare const FOCUS_VISIBLE_RING_INSET: {
62
57
  outlineColor: string;
63
58
  };
64
59
  /**
65
- * The LC-71 §4 halo carve-out (MPO-92, ratified 2026-09-02): the SAME ring,
66
- * pushed 2px clear of the border box, for the two shapes where a band hugging
67
- * the edge reads as part of the glyph instead of around it.
68
- *
69
- * Scope is by SHAPE, not by instance, and it is closed at two:
70
- * - **pill / circular thumbs** — a round handle the user drags (Slider
71
- * thumb, Rating star, TimeRangeScrubber handle). At offset 0 the band is
72
- * concentric with the glyph's own edge and reads as a thicker glyph.
73
- * - **inline text links** — a link inside a run of prose (markdown/mdx
74
- * `Link`, ErrorSummary's summary link, backoffice "View All"). At offset 0
75
- * the band crosses the line box and collides with ascenders/descenders of
76
- * the surrounding text.
77
- *
78
- * NOT for standalone rectangles on the page ground — cards, tiles, rows,
79
- * fields. Those are the default `FOCUS_VISIBLE_RING` (offset 0), which is
80
- * what makes the Axiom 12 ≥3:1 floor computable against the page ground.
81
- *
82
- * Contrast is unaffected: like offset 0 the halo paints OUTSIDE the border
83
- * box, on the surface behind the control, so it measures against the same
84
- * page ground the default does — the 1.17–1.31:1 failures MPO-41 retired were
85
- * the INSET painting inside solid intent fills, a defect the halo cannot have.
60
+ * The halo (MPO-92): pill and circular thumbs and inline text links. Since
61
+ * FZ-7944 round 2 it equals the default ring; the name stays so those call
62
+ * sites keep saying why they hold +2 if the default ever moves again.
86
63
  */
87
- export declare const FOCUS_RING_HALO_OFFSET = 2;
88
64
  export declare const FOCUS_VISIBLE_RING_HALO: {
89
65
  outlineOffset: number;
90
66
  outlineWidth: number;
@@ -93,13 +69,13 @@ export declare const FOCUS_VISIBLE_RING_HALO: {
93
69
  };
94
70
  /**
95
71
  * The CLOSED set of sanctioned focus-ring offsets (LC-71 §4). Three, and only
96
- * three: the default 0, the clipping inset −2, the shape halo +2. A call site
72
+ * three: the clipping inset −2, the flush 0, the default +2. A call site
97
73
  * that wants any other number is a rule gap, not a preference — file it and
98
74
  * amend §4, do not type the number. `focusState.spec.ts` asserts this set is
99
75
  * exactly what the repo paints, so a fourth offset fails a test instead of
100
76
  * shipping (MPO-92 AC-3).
101
77
  */
102
- export declare const SANCTIONED_FOCUS_RING_OFFSETS: readonly [-2, number, 2];
78
+ export declare const SANCTIONED_FOCUS_RING_OFFSETS: readonly [-2, 0, number];
103
79
  /** True when `offset` is one of the three offsets LC-71 §4 sanctions. */
104
80
  export declare function isSanctionedFocusRingOffset(offset: number): boolean;
105
81
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"focusState.d.ts","sourceRoot":"","sources":["../../src/theme/focusState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAuBzC;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAO5F;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAC5C,eAAO,MAAM,kBAAkB;;;;;CAKF,CAAC;AAE9B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB;;;;;CAGR,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC,eAAO,MAAM,uBAAuB;;;;;CAGP,CAAC;AAE9B;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,0BAIhC,CAAC;AAEX,yEAAyE;AACzE,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;;CAED,CAAC;AAE9B,eAAO,MAAM,iBAAiB;;CAED,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,iBAAiB,EAChE,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,iBAAiB,CAAC,GAAG,IAAI,GACxC,CAAC,GAAG,iBAAiB,CAyBvB;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,iBAAiB,GACvB,iBAAiB,CAKnB"}
1
+ {"version":3,"file":"focusState.d.ts","sourceRoot":"","sources":["../../src/theme/focusState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAuBzC;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAO5F;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAC5C,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC,eAAO,MAAM,kBAAkB;;;;;CAKF,CAAC;AAE9B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB;;;;;CAGR,CAAC;AAE9B;;;;GAIG;AACH,eAAO,MAAM,uBAAuB;;;;;CAGP,CAAC;AAE9B;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,0BAIhC,CAAC;AAEX,yEAAyE;AACzE,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;;CAED,CAAC;AAE9B,eAAO,MAAM,iBAAiB;;CAED,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,iBAAiB,EAChE,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,iBAAiB,CAAC,GAAG,IAAI,GACxC,CAAC,GAAG,iBAAiB,CAyBvB;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,iBAAiB,GACvB,iBAAiB,CAKnB"}
@@ -4,8 +4,10 @@ export { motionDurationMs, motionHideDelayMs, motionSeconds, motionSettleMs } fr
4
4
  export type MotionRoleName = "control" | "overlay" | "scrim" | "reveal" | "slide";
5
5
  export interface MotionRole {
6
6
  transition: TransitionProp | undefined;
7
+ exit?: TransitionProp | undefined;
7
8
  animateOnly: readonly string[];
8
9
  }
10
+ export declare const motionExitFactor: number;
9
11
  export declare const motionRoleProperties: {
10
12
  readonly control: readonly ["backgroundColor", "borderColor", "color", "opacity", "transform"];
11
13
  readonly overlay: readonly ["opacity", "transform"];
@@ -1 +1 @@
1
- {"version":3,"file":"motion.d.ts","sourceRoot":"","sources":["../../src/theme/motion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAIzC,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEpG,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElF,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,cAAc,GAAG,SAAS,CAAC;IACvC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,oBAAoB;;;;;;CAM6B,CAAC;AAa/D,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC,cAAc,EAAE,UAAU,CAAC,CAmB7E"}
1
+ {"version":3,"file":"motion.d.ts","sourceRoot":"","sources":["../../src/theme/motion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAIzC,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEpG,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElF,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,cAAc,GAAG,SAAS,CAAC;IACvC,IAAI,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IAClC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,gBAAgB,QAAQ,CAAC;AAEtC,eAAO,MAAM,oBAAoB;;;;;;CAM6B,CAAC;AAmB/D,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC,cAAc,EAAE,UAAU,CAAC,CAuB7E"}
@@ -1 +1 @@
1
- {"version":3,"file":"overlaySurfaces.d.ts","sourceRoot":"","sources":["../../src/theme/overlaySurfaces.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAML,KAAK,kBAAkB,IAAI,yBAAyB,EAEpD,KAAK,UAAU,EACf,KAAK,cAAc,EAEpB,MAAM,SAAS,CAAC;AAyDjB,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAmC5C;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,eAAe,EAAE,EAAE;IAAE,eAAe,CAAC,EAAE,aAAa,CAAA;CAAE,GAAG,IAAI,CAG9F;AAID,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,mDAE9B,CAAC;AAEH,wBAAgB,sBAAsB,IAAI,wBAAwB,CAEjE;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAQD,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,yBAAyB,EAAE,MAAM,CAAC,GAAG;IACzE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,aAAa;eAJb,SAAS;WACb,UAAU;kDAcjB,CAAC;AA0CH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;;;;eAEiB,SAAS;kDAelD,CAAC;AAOH,eAAO,MAAM,kBAAkB;eAxFlB,SAAS;WACb,UAAU;kDAiGlB,CAAC;AAgCF,eAAO,MAAM,kBAAkB;;;;;;;;eAEY,SAAS;kDAelD,CAAC;AAiBH,eAAO,MAAM,cAAc;eALd,SAAS;IACpB,iEAAiE;sBAC/C,aAAa;kDA0BhC,CAAC"}
1
+ {"version":3,"file":"overlaySurfaces.d.ts","sourceRoot":"","sources":["../../src/theme/overlaySurfaces.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAML,KAAK,kBAAkB,IAAI,yBAAyB,EAEpD,KAAK,UAAU,EACf,KAAK,cAAc,EAEpB,MAAM,SAAS,CAAC;AA6DjB,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAmC5C;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,eAAe,EAAE,EAAE;IAAE,eAAe,CAAC,EAAE,aAAa,CAAA;CAAE,GAAG,IAAI,CAG9F;AAID,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,mDAE9B,CAAC;AAEH,wBAAgB,sBAAsB,IAAI,wBAAwB,CAEjE;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAQD,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,yBAAyB,EAAE,MAAM,CAAC,GAAG;IACzE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,aAAa;eAJb,SAAS;WACb,UAAU;kDAcjB,CAAC;AA0CH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;;;;eAEiB,SAAS;kDAelD,CAAC;AAOH,eAAO,MAAM,kBAAkB;eAxFlB,SAAS;WACb,UAAU;kDAiGlB,CAAC;AAgCF,eAAO,MAAM,kBAAkB;;;;;;;;eAEY,SAAS;kDAelD,CAAC;AAiBH,eAAO,MAAM,cAAc;eALd,SAAS;IACpB,iEAAiE;sBAC/C,aAAa;kDA0BhC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"transitionProps.native.d.ts","sourceRoot":"","sources":["../../src/theme/transitionProps.native.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,SAAS,EAC7C,KAAK,GAAE,SAAS,MAAM,EAAO,GAC5B;IACD,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAYA"}
1
+ {"version":3,"file":"transitionProps.native.d.ts","sourceRoot":"","sources":["../../src/theme/transitionProps.native.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,SAAS,EAC7C,KAAK,GAAE,SAAS,MAAM,EAAO,GAC5B;IACD,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAmBA"}