@multiplatform.one/theme 6.7.0 → 7.1.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 +17 -17
- package/src/dates.spec.ts +344 -1
- package/src/dates.ts +50 -3
- package/src/devWarn.ts +29 -1
- package/src/figma/figmaTokens.spec.ts +3 -3
- package/src/figma/figmaTokens.ts +6 -3
- package/src/keyboardFocusRing.ts +61 -5
- package/src/menuRow.spec.ts +2 -2
- package/src/menuRow.ts +5 -4
- package/src/theme/Surface.spec.tsx +146 -0
- package/src/theme/Surface.tsx +73 -0
- package/src/theme/createDefaultThemeConfig.spec.ts +152 -0
- package/src/theme/createDefaultThemeConfig.ts +47 -3
- package/src/theme/createThemes.ts +27 -4
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +6 -2
- package/src/theme/focusState.ts +9 -1
- package/src/theme/groupGeometry.spec.ts +108 -0
- package/src/theme/groupGeometry.ts +119 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/intent.spec.tsx +17 -0
- package/src/theme/knobs.ts +2 -2
- package/src/theme/layoutTokens.spec.ts +14 -0
- package/src/theme/layoutTokens.ts +29 -0
- package/src/theme/recipeInputs.ts +368 -0
- package/src/theme/recipes.ts +34 -0
- package/src/theme/resolveKnobs.spec.ts +91 -9
- package/src/theme/resolveKnobs.ts +55 -5
- package/src/theme/shared.tsx +11 -0
- package/src/theme/sizeRecipes.spec.ts +296 -0
- package/src/theme/sizeRecipes.ts +198 -0
- package/src/theme/useResolvedKnobs.ts +76 -24
- package/src/theme/useResolvedKnobsBehavior.spec.tsx +11 -3
- package/types/dates.d.ts +36 -2
- package/types/dates.d.ts.map +1 -1
- package/types/devWarn.d.ts +16 -1
- package/types/devWarn.d.ts.map +1 -1
- package/types/figma/figmaTokens.d.ts +1 -1
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/keyboardFocusRing.d.ts +19 -4
- package/types/keyboardFocusRing.d.ts.map +1 -1
- package/types/menuRow.d.ts +5 -4
- package/types/menuRow.d.ts.map +1 -1
- package/types/theme/Surface.d.ts +37 -0
- package/types/theme/Surface.d.ts.map +1 -0
- package/types/theme/createDefaultThemeConfig.d.ts +14 -3
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/focusState.d.ts +8 -0
- package/types/theme/focusState.d.ts.map +1 -1
- package/types/theme/groupGeometry.d.ts +69 -0
- package/types/theme/groupGeometry.d.ts.map +1 -0
- package/types/theme/index.d.ts +3 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +2 -2
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +22 -0
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/recipeInputs.d.ts +110 -0
- package/types/theme/recipeInputs.d.ts.map +1 -0
- package/types/theme/recipes.d.ts +47 -0
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +7 -3
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/shared.d.ts +9 -0
- package/types/theme/shared.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +68 -0
- package/types/theme/sizeRecipes.d.ts.map +1 -0
- package/types/theme/useResolvedKnobs.d.ts +10 -6
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
- package/src/audit/baselines/home-pokemon.json +0 -355
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getGroupPosition, stackEdgeRadius, stackRadiusProps } from "./groupGeometry";
|
|
3
|
+
|
|
4
|
+
describe("getGroupPosition", () => {
|
|
5
|
+
it("returns only for singleton or empty groups", () => {
|
|
6
|
+
expect(getGroupPosition(0, 1)).toBe("only");
|
|
7
|
+
expect(getGroupPosition(0, 0)).toBe("only");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("maps first/middle/last by index", () => {
|
|
11
|
+
expect(getGroupPosition(0, 3)).toBe("first");
|
|
12
|
+
expect(getGroupPosition(1, 3)).toBe("middle");
|
|
13
|
+
expect(getGroupPosition(2, 3)).toBe("last");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("a two-item group has no middle", () => {
|
|
17
|
+
expect(getGroupPosition(0, 2)).toBe("first");
|
|
18
|
+
expect(getGroupPosition(1, 2)).toBe("last");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("stackRadiusProps (vertical)", () => {
|
|
23
|
+
const r = "$4";
|
|
24
|
+
|
|
25
|
+
it("first rounds only the top corners", () => {
|
|
26
|
+
expect(stackRadiusProps("first", r)).toEqual({
|
|
27
|
+
borderStartStartRadius: r,
|
|
28
|
+
borderStartEndRadius: r,
|
|
29
|
+
borderEndStartRadius: 0,
|
|
30
|
+
borderEndEndRadius: 0,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("last rounds only the bottom corners", () => {
|
|
35
|
+
expect(stackRadiusProps("last", r)).toEqual({
|
|
36
|
+
borderStartStartRadius: 0,
|
|
37
|
+
borderStartEndRadius: 0,
|
|
38
|
+
borderEndStartRadius: r,
|
|
39
|
+
borderEndEndRadius: r,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("middle is square on every corner", () => {
|
|
44
|
+
expect(stackRadiusProps("middle", r)).toEqual({
|
|
45
|
+
borderStartStartRadius: 0,
|
|
46
|
+
borderStartEndRadius: 0,
|
|
47
|
+
borderEndStartRadius: 0,
|
|
48
|
+
borderEndEndRadius: 0,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("only rounds all four corners", () => {
|
|
53
|
+
expect(stackRadiusProps("only", r)).toEqual({
|
|
54
|
+
borderStartStartRadius: r,
|
|
55
|
+
borderStartEndRadius: r,
|
|
56
|
+
borderEndStartRadius: r,
|
|
57
|
+
borderEndEndRadius: r,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("stackRadiusProps (horizontal)", () => {
|
|
63
|
+
const r = "$4";
|
|
64
|
+
|
|
65
|
+
it("first rounds only the inline-start corners", () => {
|
|
66
|
+
expect(stackRadiusProps("first", r, "horizontal")).toEqual({
|
|
67
|
+
borderStartStartRadius: r,
|
|
68
|
+
borderEndStartRadius: r,
|
|
69
|
+
borderStartEndRadius: 0,
|
|
70
|
+
borderEndEndRadius: 0,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("last rounds only the inline-end corners", () => {
|
|
75
|
+
expect(stackRadiusProps("last", r, "horizontal")).toEqual({
|
|
76
|
+
borderStartStartRadius: 0,
|
|
77
|
+
borderEndStartRadius: 0,
|
|
78
|
+
borderStartEndRadius: r,
|
|
79
|
+
borderEndEndRadius: r,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("stackEdgeRadius", () => {
|
|
85
|
+
it("supports conditional edges (open accordion trigger: last item, bottom square)", () => {
|
|
86
|
+
expect(stackEdgeRadius("$4", { start: false, end: false })).toEqual({
|
|
87
|
+
borderStartStartRadius: 0,
|
|
88
|
+
borderStartEndRadius: 0,
|
|
89
|
+
borderEndStartRadius: 0,
|
|
90
|
+
borderEndEndRadius: 0,
|
|
91
|
+
});
|
|
92
|
+
expect(stackEdgeRadius("$4", { start: true })).toEqual({
|
|
93
|
+
borderStartStartRadius: "$4",
|
|
94
|
+
borderStartEndRadius: "$4",
|
|
95
|
+
borderEndStartRadius: 0,
|
|
96
|
+
borderEndEndRadius: 0,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("treats undefined radius as 0 (radius:none world stays square)", () => {
|
|
101
|
+
expect(stackEdgeRadius(undefined, { start: true, end: true })).toEqual({
|
|
102
|
+
borderStartStartRadius: 0,
|
|
103
|
+
borderStartEndRadius: 0,
|
|
104
|
+
borderEndStartRadius: 0,
|
|
105
|
+
borderEndEndRadius: 0,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stacked-group corner geometry — the one canonical mapping from a row's
|
|
3
|
+
* position in a flush stack to its corner radii (Axiom 1 R-OUTER: a nested
|
|
4
|
+
* rounded child directly against the frame rounds only the outer corners;
|
|
5
|
+
* inner boundaries stay square).
|
|
6
|
+
*
|
|
7
|
+
* A "stacked group" is any run of flush siblings composing one visual unit —
|
|
8
|
+
* segmented buttons, grouped inputs, accordion/sidebar rows, list rows. The
|
|
9
|
+
* FIRST item rounds only its leading corners, the LAST only its trailing
|
|
10
|
+
* corners, everything between is square, and a lone item keeps the plain
|
|
11
|
+
* radius. State highlights (hover/press/selection) paint on the row surface
|
|
12
|
+
* itself, so they inherit exactly this geometry — a uniformly rounded
|
|
13
|
+
* highlight against a straight interior seam is the defect this module
|
|
14
|
+
* exists to prevent. Web focus outlines follow the element's border-radius,
|
|
15
|
+
* so the focus ring inherits the same geometry for free.
|
|
16
|
+
*
|
|
17
|
+
* It lives in `@multiplatform.one/theme` because both `components`
|
|
18
|
+
* (Accordion, List) and `forms` (ToggleGroup, ControlGroup) consume it and
|
|
19
|
+
* the dependency direction is components → forms → theme (same reasoning as
|
|
20
|
+
* menuRow.ts). Corners are LOGICAL (start/end) so RTL flips the rounding
|
|
21
|
+
* with the items — physical corners would land rounded edges on interior
|
|
22
|
+
* seams (the ControlGroup/ToggleGroup precedent).
|
|
23
|
+
*
|
|
24
|
+
* Overlay-clipped stacks (DropdownMenu/Select rows via `menuRowFrame`,
|
|
25
|
+
* table frames) are the CONTAINER-CLIP arm of the same rule: rows stay flat
|
|
26
|
+
* and the rounded overflow-hidden container cuts the outer corners. Use that
|
|
27
|
+
* arm when a rounded clipping container already exists; use this arm when
|
|
28
|
+
* the rows themselves are the outermost painted surface.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { createContext, useContext } from "react";
|
|
32
|
+
|
|
33
|
+
/** A row's position within its stacked group. */
|
|
34
|
+
export type GroupPosition = "only" | "first" | "middle" | "last";
|
|
35
|
+
|
|
36
|
+
/** Stacking direction: vertical stacks round top/bottom, horizontal start/end. */
|
|
37
|
+
export type GroupOrientation = "horizontal" | "vertical";
|
|
38
|
+
|
|
39
|
+
/** Resolve a row index to its group position (count <= 1 is "only"). */
|
|
40
|
+
export function getGroupPosition(index: number, count: number): GroupPosition {
|
|
41
|
+
if (count <= 1) return "only";
|
|
42
|
+
if (index <= 0) return "first";
|
|
43
|
+
if (index >= count - 1) return "last";
|
|
44
|
+
return "middle";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A type alias (not interface) so the fragment stays assignable to
|
|
48
|
+
// Record<string, unknown>-typed style bags.
|
|
49
|
+
export type StackRadiusFragment = {
|
|
50
|
+
borderStartStartRadius: string | number;
|
|
51
|
+
borderStartEndRadius: string | number;
|
|
52
|
+
borderEndStartRadius: string | number;
|
|
53
|
+
borderEndEndRadius: string | number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Corner fragment for a row whose leading/trailing edge may or may not be
|
|
58
|
+
* the group's outer edge. `start` is the top edge (vertical) or inline-start
|
|
59
|
+
* edge (horizontal); `end` the opposite. This is the low-level arm for rows
|
|
60
|
+
* whose outer-edge status is conditional — an open accordion trigger's
|
|
61
|
+
* bottom edge abuts its own content, so its bottom corners go square even on
|
|
62
|
+
* the last item.
|
|
63
|
+
*/
|
|
64
|
+
export function stackEdgeRadius(
|
|
65
|
+
radius: string | number | undefined,
|
|
66
|
+
edges: { start?: boolean; end?: boolean },
|
|
67
|
+
orientation: GroupOrientation = "vertical",
|
|
68
|
+
): StackRadiusFragment {
|
|
69
|
+
const r = radius ?? 0;
|
|
70
|
+
const start = edges.start ? r : 0;
|
|
71
|
+
const end = edges.end ? r : 0;
|
|
72
|
+
if (orientation === "horizontal") {
|
|
73
|
+
return {
|
|
74
|
+
borderStartStartRadius: start,
|
|
75
|
+
borderEndStartRadius: start,
|
|
76
|
+
borderStartEndRadius: end,
|
|
77
|
+
borderEndEndRadius: end,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
borderStartStartRadius: start,
|
|
82
|
+
borderStartEndRadius: start,
|
|
83
|
+
borderEndStartRadius: end,
|
|
84
|
+
borderEndEndRadius: end,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The complete corner fragment for a row at `position` in a stacked group.
|
|
90
|
+
* Spread it onto the row surface (the element that paints hover/press/
|
|
91
|
+
* selection) — never cherry-pick single corners out of it.
|
|
92
|
+
*/
|
|
93
|
+
export function stackRadiusProps(
|
|
94
|
+
position: GroupPosition,
|
|
95
|
+
radius: string | number | undefined,
|
|
96
|
+
orientation: GroupOrientation = "vertical",
|
|
97
|
+
): StackRadiusFragment {
|
|
98
|
+
return stackEdgeRadius(
|
|
99
|
+
radius,
|
|
100
|
+
{
|
|
101
|
+
start: position === "first" || position === "only",
|
|
102
|
+
end: position === "last" || position === "only",
|
|
103
|
+
},
|
|
104
|
+
orientation,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Group containers provide each item's position so nested compound parts
|
|
110
|
+
* (an Accordion.Trigger inside an Accordion.Item) resolve their geometry
|
|
111
|
+
* without prop drilling. `undefined` means "not inside a stacked group" —
|
|
112
|
+
* consumers treat that as "only".
|
|
113
|
+
*/
|
|
114
|
+
export const GroupPositionContext = createContext<GroupPosition | undefined>(undefined);
|
|
115
|
+
|
|
116
|
+
/** The current row's position within its stacked group, if any. */
|
|
117
|
+
export function useGroupPosition(): GroupPosition | undefined {
|
|
118
|
+
return useContext(GroupPositionContext);
|
|
119
|
+
}
|
package/src/theme/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./defaults/index";
|
|
|
9
9
|
export * from "./devtools/ThemeDevtoolsPanel";
|
|
10
10
|
export * from "./focusState";
|
|
11
11
|
export * from "./FontKnobStyles";
|
|
12
|
+
export * from "./groupGeometry";
|
|
12
13
|
export * from "./hairline";
|
|
13
14
|
export * from "./Intent";
|
|
14
15
|
export * from "./intents";
|
|
@@ -23,6 +24,8 @@ export * from "./readableColor";
|
|
|
23
24
|
export * from "./recipes";
|
|
24
25
|
export * from "./resolveKnobs";
|
|
25
26
|
export * from "./shared";
|
|
27
|
+
export * from "./sizeRecipes";
|
|
28
|
+
export * from "./Surface";
|
|
26
29
|
|
|
27
30
|
export * from "./theme";
|
|
28
31
|
export * from "./Tint";
|
|
@@ -7,6 +7,23 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
7
7
|
vi.mock("tamagui", () => ({
|
|
8
8
|
Theme: ({ name, children }: { name: string; children: ReactNode }) =>
|
|
9
9
|
createElement("div", { "data-tamagui-theme": name }, children),
|
|
10
|
+
createStyledContext: (defaults: { size: string; density: string }) => ({
|
|
11
|
+
Provider: ({
|
|
12
|
+
children,
|
|
13
|
+
size,
|
|
14
|
+
density,
|
|
15
|
+
}: {
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
size?: string;
|
|
18
|
+
density?: string;
|
|
19
|
+
}) =>
|
|
20
|
+
createElement("div", {
|
|
21
|
+
"data-surface-size": size ?? defaults.size,
|
|
22
|
+
"data-surface-density": density ?? defaults.density,
|
|
23
|
+
children,
|
|
24
|
+
}),
|
|
25
|
+
useStyledContext: () => defaults,
|
|
26
|
+
}),
|
|
10
27
|
}));
|
|
11
28
|
|
|
12
29
|
import type { ThemeName } from "@tamagui/web";
|
package/src/theme/knobs.ts
CHANGED
|
@@ -58,7 +58,7 @@ export const Size = {
|
|
|
58
58
|
} as const;
|
|
59
59
|
export type Size = (typeof Size)[keyof typeof Size];
|
|
60
60
|
|
|
61
|
-
/** Global density mode — compact steps
|
|
61
|
+
/** Global density mode — compact steps space down one level; size is independent. */
|
|
62
62
|
export const Density = {
|
|
63
63
|
Comfortable: "comfortable",
|
|
64
64
|
Compact: "compact",
|
|
@@ -236,7 +236,7 @@ export interface Knobs {
|
|
|
236
236
|
elevation: Elevation;
|
|
237
237
|
space: Space;
|
|
238
238
|
size: Size;
|
|
239
|
-
/** Propagating density mode. Default comfortable; compact steps
|
|
239
|
+
/** Propagating density mode. Default comfortable; compact steps space only. */
|
|
240
240
|
density: Density;
|
|
241
241
|
textAccent: TextAccent;
|
|
242
242
|
headingFont: HeadingFont;
|
|
@@ -2,12 +2,14 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
MIN_PRESS_TARGET,
|
|
4
4
|
NARROW_BREAKPOINT,
|
|
5
|
+
OVERLAY_ANCHOR_GAP,
|
|
5
6
|
OVERLAY_BREAKPOINT,
|
|
6
7
|
READING_WIDTH_CH,
|
|
7
8
|
READING_WIDTH_MAX_CH,
|
|
8
9
|
READING_WIDTH_MIN_CH,
|
|
9
10
|
defaultMaxPanes,
|
|
10
11
|
getLayoutSizeClass,
|
|
12
|
+
isTouchSurface,
|
|
11
13
|
layoutBreakpoints,
|
|
12
14
|
layoutSizeClassAtLeast,
|
|
13
15
|
pressTargetHitSlop,
|
|
@@ -65,6 +67,12 @@ describe("readingWidthStyle (DG-LAY-05)", () => {
|
|
|
65
67
|
});
|
|
66
68
|
});
|
|
67
69
|
|
|
70
|
+
describe("overlay anchoring", () => {
|
|
71
|
+
it("pins the shared trigger↔overlay gap at 4px", () => {
|
|
72
|
+
expect(OVERLAY_ANCHOR_GAP).toBe(4);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
68
76
|
describe("press target (DG-LAY-04)", () => {
|
|
69
77
|
it("exports 44px floor + hitSlop expansion", () => {
|
|
70
78
|
expect(MIN_PRESS_TARGET).toBe(44);
|
|
@@ -73,3 +81,9 @@ describe("press target (DG-LAY-04)", () => {
|
|
|
73
81
|
expect(pressTargetHitSlop(44)).toEqual({ top: 0, bottom: 0, left: 0, right: 0 });
|
|
74
82
|
});
|
|
75
83
|
});
|
|
84
|
+
|
|
85
|
+
describe("isTouchSurface (LC-66)", () => {
|
|
86
|
+
it("is false in the default test environment", () => {
|
|
87
|
+
expect(isTouchSurface()).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* (840→860). Fluent sm=640 matches medium; Fluent lg=1024 matches large.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
import { isTouchable, isWebTouchable } from "@multiplatform.one/platform";
|
|
32
33
|
import { useEffect, useState } from "react";
|
|
33
34
|
import { isWeb, useWindowDimensions } from "tamagui";
|
|
34
35
|
import { useResolvedKnobs } from "./useResolvedKnobs";
|
|
@@ -170,6 +171,11 @@ export function readingWidthStyle(
|
|
|
170
171
|
/** Platform floor for interactive targets (Fluent/HIG web+iOS 44; M3 48 on Android). */
|
|
171
172
|
export const MIN_PRESS_TARGET = 44;
|
|
172
173
|
|
|
174
|
+
/** Touch surfaces — native touchable or web touchable (LC-67). */
|
|
175
|
+
export function isTouchSurface(): boolean {
|
|
176
|
+
return isTouchable || isWebTouchable;
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
export type PressTargetStyle = {
|
|
174
180
|
minWidth: number;
|
|
175
181
|
minHeight: number;
|
|
@@ -195,6 +201,29 @@ export function pressTargetHitSlop(
|
|
|
195
201
|
return { top: pad, bottom: pad, left: pad, right: pad };
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
// ── Overlay anchoring (house rule) ────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* House convention for every anchored overlay (Select/Combobox listboxes,
|
|
208
|
+
* DropdownMenu, Popover, Tooltip, FloatingPanel):
|
|
209
|
+
*
|
|
210
|
+
* - Anchored MENUS and SELECT LISTBOXES match the trigger width EXACTLY
|
|
211
|
+
* (min-width = trigger width — no oversize fudge). They may grow only
|
|
212
|
+
* wider when option content genuinely overflows, never narrower, and
|
|
213
|
+
* stay start-aligned (flush with the trigger's leading edge).
|
|
214
|
+
* - Pointer-positioned CONTEXT MENUS and free POPOVERS/TOOLTIPS size to
|
|
215
|
+
* content, start-aligned to their anchor.
|
|
216
|
+
* - All of them keep `OVERLAY_ANCHOR_GAP` px between trigger edge and
|
|
217
|
+
* overlay (floating-ui `offset` / tamagui Popover `offset`), and
|
|
218
|
+
* shift/flip at viewport edges instead of overflowing.
|
|
219
|
+
* - ONE SURFACE OWNER per overlay: the overlay frame draws the single
|
|
220
|
+
* border + radius. When a panel intentionally covers its trigger
|
|
221
|
+
* (Select's macOS-style aligned listbox), the covered trigger must not
|
|
222
|
+
* paint focus decorations — a focus ring outside the trigger's border
|
|
223
|
+
* box would ghost around the overlay's corners as a doubled edge.
|
|
224
|
+
*/
|
|
225
|
+
export const OVERLAY_ANCHOR_GAP = 4;
|
|
226
|
+
|
|
198
227
|
// ── Semantic gaps (DG-LAY-03) ─────────────────────────────────────────────
|
|
199
228
|
|
|
200
229
|
/**
|