@multiplatform.one/theme 6.1.0 → 6.3.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/README.md +73 -0
- package/package.json +22 -9
- package/src/dates.spec.ts +75 -0
- package/src/dates.ts +130 -0
- package/src/devWarn.spec.ts +144 -0
- package/src/devWarn.ts +235 -0
- package/src/figma/figmaTokens.spec.ts +244 -0
- package/src/figma/figmaTokens.ts +503 -0
- package/src/font.ts +6 -8
- package/src/fonts/createFontLoader.web.ts +15 -4
- package/src/index.ts +7 -0
- package/src/keyboardFocusRing.ts +45 -0
- package/src/menuRow.spec.ts +32 -0
- package/src/menuRow.ts +31 -0
- package/src/theme/FontKnobStyles.native.tsx +24 -0
- package/src/theme/FontKnobStyles.tsx +256 -0
- package/src/theme/animations/reactNative.ts +6 -1
- package/src/theme/chartPalette.spec.ts +159 -0
- package/src/theme/chartPalette.ts +170 -0
- package/src/theme/colorRules.ts +33 -1
- package/src/theme/cookies.spec.ts +32 -0
- package/src/theme/cookiesPersistence.spec.ts +86 -0
- package/src/theme/cornerSmoothing.native.ts +16 -0
- package/src/theme/cornerSmoothing.spec.ts +27 -0
- package/src/theme/cornerSmoothing.ts +46 -0
- package/src/theme/createDefaultThemeConfig.ts +32 -8
- package/src/theme/createThemeConfig.spec.ts +137 -0
- package/src/theme/createThemes.ts +77 -2
- package/src/theme/defaults/accent.ts +39 -0
- package/src/theme/defaults/base.ts +63 -0
- package/src/theme/defaults/builderOptions.ts +174 -0
- package/src/theme/defaults/fonts.spec.ts +87 -0
- package/src/theme/defaults/fonts.ts +89 -0
- package/src/theme/defaults/index.ts +4 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +196 -2
- package/src/theme/devtools/buildSnapshot.ts +11 -0
- package/src/theme/devtools/knobIcons.tsx +141 -0
- package/src/theme/devtools/vizConstants.ts +1 -1
- package/src/theme/focusState.spec.ts +174 -0
- package/src/theme/focusState.ts +112 -0
- package/src/theme/fontKnobStyles.spec.ts +98 -0
- package/src/theme/hairline.native.ts +43 -0
- package/src/theme/hairline.spec.ts +63 -0
- package/src/theme/hairline.ts +101 -0
- package/src/theme/index.ts +8 -0
- package/src/theme/intent.spec.tsx +2 -2
- package/src/theme/intents.ts +6 -2
- package/src/theme/knobs.ts +134 -2
- package/src/theme/layoutTokens.spec.ts +75 -0
- package/src/theme/layoutTokens.ts +213 -0
- package/src/theme/layoutTokensHooks.spec.tsx +157 -0
- package/src/theme/presets.spec.ts +17 -0
- package/src/theme/presets.ts +22 -2
- package/src/theme/readableColor.ts +101 -0
- package/src/theme/recipes.ts +130 -7
- package/src/theme/resolveKnobs.spec.ts +560 -21
- package/src/theme/resolveKnobs.ts +267 -23
- package/src/theme/shared.tsx +26 -2
- package/src/theme/useResolvedKnobs.ts +67 -26
- package/src/theme/useResolvedKnobsBehavior.spec.tsx +166 -0
- package/types/audit/constraintAudit.d.ts +83 -0
- package/types/audit/constraintAudit.d.ts.map +1 -0
- package/types/audit/diffReport.d.ts +45 -0
- package/types/audit/diffReport.d.ts.map +1 -0
- package/types/audit/index.d.ts +5 -0
- package/types/audit/index.d.ts.map +1 -0
- package/types/dates.d.ts +52 -0
- package/types/dates.d.ts.map +1 -0
- package/types/devWarn.d.ts +104 -0
- package/types/devWarn.d.ts.map +1 -0
- package/types/figma/figmaTokens.d.ts +168 -0
- package/types/figma/figmaTokens.d.ts.map +1 -0
- package/types/font.d.ts +5 -0
- package/types/font.d.ts.map +1 -0
- package/types/fonts/createFontLoader.d.ts +3 -0
- package/types/fonts/createFontLoader.d.ts.map +1 -0
- package/types/fonts/createFontLoader.web.d.ts +3 -0
- package/types/fonts/createFontLoader.web.d.ts.map +1 -0
- package/types/fonts/index.d.ts +3 -0
- package/types/fonts/index.d.ts.map +1 -0
- package/types/fonts/types.d.ts +12 -0
- package/types/fonts/types.d.ts.map +1 -0
- package/types/index.d.ts +9 -0
- package/types/index.d.ts.map +1 -0
- package/types/keyboardFocusRing.d.ts +32 -0
- package/types/keyboardFocusRing.d.ts.map +1 -0
- package/types/menuRow.d.ts +32 -0
- package/types/menuRow.d.ts.map +1 -0
- package/types/theme/FontKnobStyles.d.ts +49 -0
- package/types/theme/FontKnobStyles.d.ts.map +1 -0
- package/types/theme/FontKnobStyles.native.d.ts +12 -0
- package/types/theme/FontKnobStyles.native.d.ts.map +1 -0
- package/types/theme/Intent.d.ts +20 -0
- package/types/theme/Intent.d.ts.map +1 -0
- package/types/theme/Preset.d.ts +15 -0
- package/types/theme/Preset.d.ts.map +1 -0
- package/types/theme/PresetContext.d.ts +18 -0
- package/types/theme/PresetContext.d.ts.map +1 -0
- package/types/theme/Tint.d.ts +19 -0
- package/types/theme/Tint.d.ts.map +1 -0
- package/types/theme/animations/css.d.ts +24 -0
- package/types/theme/animations/css.d.ts.map +1 -0
- package/types/theme/animations/index.d.ts +2 -0
- package/types/theme/animations/index.d.ts.map +1 -0
- package/types/theme/animations/index.web.d.ts +2 -0
- package/types/theme/animations/index.web.d.ts.map +1 -0
- package/types/theme/animations/reactNative.d.ts +104 -0
- package/types/theme/animations/reactNative.d.ts.map +1 -0
- package/types/theme/chartPalette.d.ts +62 -0
- package/types/theme/chartPalette.d.ts.map +1 -0
- package/types/theme/colorRules.d.ts +69 -0
- package/types/theme/colorRules.d.ts.map +1 -0
- package/types/theme/cookies.d.ts +76 -0
- package/types/theme/cookies.d.ts.map +1 -0
- package/types/theme/cornerSmoothing.d.ts +31 -0
- package/types/theme/cornerSmoothing.d.ts.map +1 -0
- package/types/theme/cornerSmoothing.native.d.ts +14 -0
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -0
- package/types/theme/createDefaultThemeConfig.d.ts +37 -0
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -0
- package/types/theme/createThemes.d.ts +38 -0
- package/types/theme/createThemes.d.ts.map +1 -0
- package/types/theme/defaults/accent.d.ts +3 -0
- package/types/theme/defaults/accent.d.ts.map +1 -0
- package/types/theme/defaults/base.d.ts +3 -0
- package/types/theme/defaults/base.d.ts.map +1 -0
- package/types/theme/defaults/builderOptions.d.ts +3 -0
- package/types/theme/defaults/builderOptions.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +7 -0
- package/types/theme/defaults/fonts.d.ts.map +1 -0
- package/types/theme/defaults/index.d.ts +5 -0
- package/types/theme/defaults/index.d.ts.map +1 -0
- package/types/theme/devtools/ColorLineVisualizer.d.ts +17 -0
- package/types/theme/devtools/ColorLineVisualizer.d.ts.map +1 -0
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts +87 -0
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -0
- package/types/theme/devtools/buildSnapshot.d.ts +12 -0
- package/types/theme/devtools/buildSnapshot.d.ts.map +1 -0
- package/types/theme/devtools/knobIcons.d.ts +33 -0
- package/types/theme/devtools/knobIcons.d.ts.map +1 -0
- package/types/theme/devtools/vizConstants.d.ts +5 -0
- package/types/theme/devtools/vizConstants.d.ts.map +1 -0
- package/types/theme/focusState.d.ts +42 -0
- package/types/theme/focusState.d.ts.map +1 -0
- package/types/theme/hairline.d.ts +88 -0
- package/types/theme/hairline.d.ts.map +1 -0
- package/types/theme/hairline.native.d.ts +42 -0
- package/types/theme/hairline.native.d.ts.map +1 -0
- package/types/theme/index.d.ts +29 -0
- package/types/theme/index.d.ts.map +1 -0
- package/types/theme/intents.d.ts +3 -0
- package/types/theme/intents.d.ts.map +1 -0
- package/types/theme/knobs.d.ts +236 -0
- package/types/theme/knobs.d.ts.map +1 -0
- package/types/theme/layoutTokens.d.ts +104 -0
- package/types/theme/layoutTokens.d.ts.map +1 -0
- package/types/theme/preset.types.d.ts +21 -0
- package/types/theme/preset.types.d.ts.map +1 -0
- package/types/theme/presets.d.ts +4 -0
- package/types/theme/presets.d.ts.map +1 -0
- package/types/theme/readableColor.d.ts +17 -0
- package/types/theme/readableColor.d.ts.map +1 -0
- package/types/theme/recipes.d.ts +200 -0
- package/types/theme/recipes.d.ts.map +1 -0
- package/types/theme/resolveKnobs.d.ts +22 -0
- package/types/theme/resolveKnobs.d.ts.map +1 -0
- package/types/theme/shared.d.ts +40 -0
- package/types/theme/shared.d.ts.map +1 -0
- package/types/theme/theme.d.ts +19 -0
- package/types/theme/theme.d.ts.map +1 -0
- package/types/theme/theme.native.d.ts +10 -0
- package/types/theme/theme.native.d.ts.map +1 -0
- package/types/theme/useColorScale.d.ts +7 -0
- package/types/theme/useColorScale.d.ts.map +1 -0
- package/types/theme/useColorScale.native.d.ts +6 -0
- package/types/theme/useColorScale.native.d.ts.map +1 -0
- package/types/theme/useResolvedKnobs.d.ts +32 -0
- package/types/theme/useResolvedKnobs.d.ts.map +1 -0
- package/types/theme/useTheme.d.ts +31 -0
- package/types/theme/useTheme.d.ts.map +1 -0
- package/types/theme/useTheme.native.d.ts +10 -0
- package/types/theme/useTheme.native.d.ts.map +1 -0
- package/CHANGELOG.md +0 -53
- package/src/theme/__snapshots__/resolveKnobs.spec.ts.snap +0 -69
- package/src/theme/pokemonScreen.spec.ts +0 -109
- package/tsconfig.json +0 -11
- package/vitest.config.mjs +0 -9
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout hook specs — the live half of layoutTokens (DG-LAY-01/03): the
|
|
3
|
+
* viewport-driven `useLayoutSizeClass`, the pane budget `useMultiPane`, and
|
|
4
|
+
* the semantic gap roles `useSemanticGaps`. Locks down: widths resolve to the
|
|
5
|
+
* canonical size classes at mount, one matchMedia listener per breakpoint is
|
|
6
|
+
* registered and removed, and gap roles ride the space-knob recipes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { act, renderHook } from "@testing-library/react";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
|
|
12
|
+
// Controllable native-dimensions stub: the web path under test ignores it, but
|
|
13
|
+
// tests may mutate `width` to exercise the native fallback deterministically.
|
|
14
|
+
const nativeWindowDimensions = vi.hoisted(() => ({
|
|
15
|
+
width: 1024,
|
|
16
|
+
height: 768,
|
|
17
|
+
scale: 2,
|
|
18
|
+
fontScale: 1,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Partial mock: real tamagui exports pass through so future imports in
|
|
22
|
+
// layoutTokens don't break the suite; only the members the spec must control
|
|
23
|
+
// are stubbed.
|
|
24
|
+
vi.mock(import("tamagui"), async (importOriginal) => ({
|
|
25
|
+
...(await importOriginal()),
|
|
26
|
+
// Pin the web path — these specs drive size class via matchMedia listeners.
|
|
27
|
+
isWeb: true,
|
|
28
|
+
useWindowDimensions: () => nativeWindowDimensions,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
layoutBreakpoints,
|
|
33
|
+
useLayoutSizeClass,
|
|
34
|
+
useMultiPane,
|
|
35
|
+
useSemanticGaps,
|
|
36
|
+
} from "./layoutTokens";
|
|
37
|
+
import { useResolvedKnobs } from "./useResolvedKnobs";
|
|
38
|
+
|
|
39
|
+
interface FakeMq {
|
|
40
|
+
media: string;
|
|
41
|
+
matches: boolean;
|
|
42
|
+
addEventListener: ReturnType<typeof vi.fn>;
|
|
43
|
+
removeEventListener: ReturnType<typeof vi.fn>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let createdQueries: FakeMq[];
|
|
47
|
+
let originalMatchMedia: typeof window.matchMedia;
|
|
48
|
+
let originalInnerWidth: number;
|
|
49
|
+
|
|
50
|
+
function setViewportWidth(width: number) {
|
|
51
|
+
Object.defineProperty(window, "innerWidth", {
|
|
52
|
+
value: width,
|
|
53
|
+
writable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
createdQueries = [];
|
|
60
|
+
originalMatchMedia = window.matchMedia;
|
|
61
|
+
originalInnerWidth = window.innerWidth;
|
|
62
|
+
window.matchMedia = vi.fn((media: string): MediaQueryList => {
|
|
63
|
+
const mq: FakeMq = {
|
|
64
|
+
media,
|
|
65
|
+
matches: false,
|
|
66
|
+
addEventListener: vi.fn(),
|
|
67
|
+
removeEventListener: vi.fn(),
|
|
68
|
+
};
|
|
69
|
+
createdQueries.push(mq);
|
|
70
|
+
return mq as unknown as MediaQueryList;
|
|
71
|
+
}) as typeof window.matchMedia;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
window.matchMedia = originalMatchMedia;
|
|
76
|
+
setViewportWidth(originalInnerWidth);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("useLayoutSizeClass", () => {
|
|
80
|
+
it.each([
|
|
81
|
+
[500, "compact"],
|
|
82
|
+
[layoutBreakpoints.medium, "medium"],
|
|
83
|
+
[layoutBreakpoints.expanded, "expanded"],
|
|
84
|
+
[layoutBreakpoints.large, "large"],
|
|
85
|
+
[layoutBreakpoints.xl, "xl"],
|
|
86
|
+
] as const)("resolves width %dpx to %s at mount", (width, expected) => {
|
|
87
|
+
setViewportWidth(width);
|
|
88
|
+
const { result } = renderHook(() => useLayoutSizeClass());
|
|
89
|
+
expect(result.current).toBe(expected);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("registers a matchMedia listener per breakpoint and removes them on unmount", () => {
|
|
93
|
+
setViewportWidth(900);
|
|
94
|
+
const { unmount } = renderHook(() => useLayoutSizeClass());
|
|
95
|
+
expect(createdQueries).toHaveLength(4);
|
|
96
|
+
const medias = createdQueries.map((mq) => mq.media);
|
|
97
|
+
expect(medias).toEqual(
|
|
98
|
+
expect.arrayContaining([
|
|
99
|
+
`(min-width: ${layoutBreakpoints.medium}px)`,
|
|
100
|
+
`(min-width: ${layoutBreakpoints.expanded}px)`,
|
|
101
|
+
`(min-width: ${layoutBreakpoints.large}px)`,
|
|
102
|
+
`(min-width: ${layoutBreakpoints.xl}px)`,
|
|
103
|
+
]),
|
|
104
|
+
);
|
|
105
|
+
for (const mq of createdQueries) {
|
|
106
|
+
expect(mq.addEventListener).toHaveBeenCalledWith("change", expect.any(Function));
|
|
107
|
+
}
|
|
108
|
+
unmount();
|
|
109
|
+
for (const mq of createdQueries) {
|
|
110
|
+
expect(mq.removeEventListener).toHaveBeenCalledWith("change", expect.any(Function));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("re-syncs from the viewport when a breakpoint query fires", () => {
|
|
115
|
+
setViewportWidth(500);
|
|
116
|
+
const { result } = renderHook(() => useLayoutSizeClass());
|
|
117
|
+
expect(result.current).toBe("compact");
|
|
118
|
+
// Simulate crossing into expanded: update the viewport and fire any
|
|
119
|
+
// registered breakpoint listener.
|
|
120
|
+
setViewportWidth(1000);
|
|
121
|
+
const handler = createdQueries[0].addEventListener.mock.calls[0][1] as () => void;
|
|
122
|
+
act(() => handler());
|
|
123
|
+
expect(result.current).toBe("expanded");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("useMultiPane", () => {
|
|
128
|
+
it("allows two panes at expanded and wider", () => {
|
|
129
|
+
setViewportWidth(layoutBreakpoints.expanded);
|
|
130
|
+
const { result } = renderHook(() => useMultiPane());
|
|
131
|
+
expect(result.current).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("forces a single pane below expanded", () => {
|
|
135
|
+
setViewportWidth(700);
|
|
136
|
+
const { result } = renderHook(() => useMultiPane());
|
|
137
|
+
expect(result.current).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("honors an explicit single-pane budget even on wide viewports", () => {
|
|
141
|
+
setViewportWidth(1400);
|
|
142
|
+
const { result } = renderHook(() => useMultiPane(1));
|
|
143
|
+
expect(result.current).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("useSemanticGaps", () => {
|
|
148
|
+
it("maps within-group to knob gap and between-groups to the large gap", () => {
|
|
149
|
+
setViewportWidth(1000);
|
|
150
|
+
const { result: knobs } = renderHook(() => useResolvedKnobs());
|
|
151
|
+
const { result } = renderHook(() => useSemanticGaps());
|
|
152
|
+
expect(result.current.withinGroup).toEqual(knobs.current.knobProps.gap);
|
|
153
|
+
expect(result.current.betweenGroups).toEqual(knobs.current.knobProps.gapLg);
|
|
154
|
+
// The roles must differ, or grouping hierarchy is invisible.
|
|
155
|
+
expect(result.current.withinGroup.gap).not.toBe(result.current.betweenGroups.gap);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -20,6 +20,17 @@ import { defaultIntents } from "./intents";
|
|
|
20
20
|
// Preset fixtures
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
|
|
23
|
+
const houseKnobDefaults = {
|
|
24
|
+
fieldLabelPlacement: "top",
|
|
25
|
+
requiredMarking: "minority",
|
|
26
|
+
tableZebra: "off",
|
|
27
|
+
bulkBarPlacement: "top",
|
|
28
|
+
selectAllScope: "page",
|
|
29
|
+
timestampStyle: "absolute",
|
|
30
|
+
disabledStyle: "keepLabel",
|
|
31
|
+
formAutofocus: "off",
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
23
34
|
const minimalPreset: Preset = {
|
|
24
35
|
theme: "gray" as ThemeName,
|
|
25
36
|
knobs: {
|
|
@@ -29,11 +40,13 @@ const minimalPreset: Preset = {
|
|
|
29
40
|
elevation: "none",
|
|
30
41
|
space: "small",
|
|
31
42
|
size: "small",
|
|
43
|
+
density: "comfortable",
|
|
32
44
|
textAccent: "low",
|
|
33
45
|
headingFont: "sans-serif",
|
|
34
46
|
bodyFont: "sans-serif",
|
|
35
47
|
fontWeight: "regular",
|
|
36
48
|
animation: "none",
|
|
49
|
+
...houseKnobDefaults,
|
|
37
50
|
hover: {},
|
|
38
51
|
press: {},
|
|
39
52
|
focus: {},
|
|
@@ -52,11 +65,13 @@ const boldPreset: Preset = {
|
|
|
52
65
|
elevation: "medium",
|
|
53
66
|
space: "large",
|
|
54
67
|
size: "large",
|
|
68
|
+
density: "comfortable",
|
|
55
69
|
textAccent: "high",
|
|
56
70
|
headingFont: "sans-serif",
|
|
57
71
|
bodyFont: "sans-serif",
|
|
58
72
|
fontWeight: "bold",
|
|
59
73
|
animation: "bouncy",
|
|
74
|
+
...houseKnobDefaults,
|
|
60
75
|
hover: {},
|
|
61
76
|
press: {},
|
|
62
77
|
focus: {},
|
|
@@ -75,11 +90,13 @@ const roundedPreset: Preset = {
|
|
|
75
90
|
elevation: "small",
|
|
76
91
|
space: "medium",
|
|
77
92
|
size: "medium",
|
|
93
|
+
density: "comfortable",
|
|
78
94
|
textAccent: "high",
|
|
79
95
|
headingFont: "sans-serif",
|
|
80
96
|
bodyFont: "sans-serif",
|
|
81
97
|
fontWeight: "regular",
|
|
82
98
|
animation: "quick",
|
|
99
|
+
...houseKnobDefaults,
|
|
83
100
|
hover: {},
|
|
84
101
|
press: {},
|
|
85
102
|
focus: {},
|
package/src/theme/presets.ts
CHANGED
|
@@ -7,15 +7,25 @@ export const defaultPreset: Preset = {
|
|
|
7
7
|
knobs: {
|
|
8
8
|
fillStyle: "filled",
|
|
9
9
|
borderRadius: "medium",
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
cornerSmoothing: "round",
|
|
11
|
+
borderWidth: "medium",
|
|
12
|
+
elevation: "small",
|
|
12
13
|
space: "medium",
|
|
13
14
|
size: "medium",
|
|
15
|
+
density: "comfortable",
|
|
14
16
|
textAccent: "high",
|
|
15
17
|
headingFont: "sans-serif",
|
|
16
18
|
bodyFont: "sans-serif",
|
|
17
19
|
fontWeight: "regular",
|
|
18
20
|
animation: "quick",
|
|
21
|
+
fieldLabelPlacement: "top",
|
|
22
|
+
requiredMarking: "minority",
|
|
23
|
+
tableZebra: "off",
|
|
24
|
+
bulkBarPlacement: "top",
|
|
25
|
+
selectAllScope: "page",
|
|
26
|
+
timestampStyle: "absolute",
|
|
27
|
+
disabledStyle: "keepLabel",
|
|
28
|
+
formAutofocus: "off",
|
|
19
29
|
hover: {},
|
|
20
30
|
press: {},
|
|
21
31
|
focus: {},
|
|
@@ -30,15 +40,25 @@ export const boldPreset: Preset = {
|
|
|
30
40
|
knobs: {
|
|
31
41
|
fillStyle: "filled",
|
|
32
42
|
borderRadius: "full",
|
|
43
|
+
cornerSmoothing: "round",
|
|
33
44
|
borderWidth: "none",
|
|
34
45
|
elevation: "none",
|
|
35
46
|
space: "small",
|
|
36
47
|
size: "medium",
|
|
48
|
+
density: "comfortable",
|
|
37
49
|
textAccent: "high",
|
|
38
50
|
headingFont: "sans-serif",
|
|
39
51
|
bodyFont: "sans-serif",
|
|
40
52
|
fontWeight: "bold",
|
|
41
53
|
animation: "bouncy",
|
|
54
|
+
fieldLabelPlacement: "top",
|
|
55
|
+
requiredMarking: "minority",
|
|
56
|
+
tableZebra: "off",
|
|
57
|
+
bulkBarPlacement: "top",
|
|
58
|
+
selectAllScope: "page",
|
|
59
|
+
timestampStyle: "absolute",
|
|
60
|
+
disabledStyle: "keepLabel",
|
|
61
|
+
formAutofocus: "off",
|
|
42
62
|
hover: {},
|
|
43
63
|
press: {},
|
|
44
64
|
focus: {},
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useTheme as useTamaguiTheme } from "tamagui";
|
|
3
|
+
import {
|
|
4
|
+
aaTextContrastRatio,
|
|
5
|
+
contrastRatio,
|
|
6
|
+
normalizeToHex,
|
|
7
|
+
pickReadableForeground,
|
|
8
|
+
relativeLuminance,
|
|
9
|
+
} from "./colorRules";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Luminance-computed readable text/glyph colors (Axiom 15 OPTICS + Axiom 12
|
|
13
|
+
* LEGIBLE FLOOR).
|
|
14
|
+
*
|
|
15
|
+
* Two catalog-wide contrast traps this module closes:
|
|
16
|
+
*
|
|
17
|
+
* 1. Text sitting ON an accent/solid FILL (`useReadableTextOn`). Components
|
|
18
|
+
* that paint a selected/active row, chip or avatar with `$accentBackground`
|
|
19
|
+
* (or any solid step) and then let the label inherit the scheme's `$color`
|
|
20
|
+
* render ink-on-accent (~2.6:1) or, worse, near-white-on-bright-tint
|
|
21
|
+
* (~1.2:1). The readable foreground is whichever scheme anchor
|
|
22
|
+
* (paper `$color1` / ink `$color12`) carries more contrast on the ACTUAL
|
|
23
|
+
* fill — never assumed.
|
|
24
|
+
*
|
|
25
|
+
* 2. Accent-hued text on the PAGE (`useAccentOnSurface`). `$accentColor` is the
|
|
26
|
+
* on-fill foreground; used as a link/emphasis color on the neutral page it
|
|
27
|
+
* reads ~1.9:1 in dark. The readable accent is the most chromatic step of
|
|
28
|
+
* the theme's own accent ramp that still clears AA (4.5:1) on both page
|
|
29
|
+
* surfaces — a mid accent in light, a pale accent in dark.
|
|
30
|
+
*
|
|
31
|
+
* Both read live theme values so they stay correct across schemes, tints and
|
|
32
|
+
* custom accent palettes; they mirror the runtime pattern already used by the
|
|
33
|
+
* Switch thumb, Toast and Calendar chip.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
function resolveTokenValue(
|
|
37
|
+
theme: Record<string, { val?: unknown } | undefined>,
|
|
38
|
+
token: string,
|
|
39
|
+
): string | undefined {
|
|
40
|
+
const key = token.startsWith("$") ? token.slice(1) : token;
|
|
41
|
+
const val = theme[key]?.val;
|
|
42
|
+
return typeof val === "string" ? val : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Readable text/glyph color to sit ON `fillToken` (e.g. "$accentBackground",
|
|
47
|
+
* "$color8"): the scheme's paper (`$color1`) or ink (`$color12`) anchor,
|
|
48
|
+
* whichever contrasts more on the resolved fill, returned as a concrete value
|
|
49
|
+
* so no nested sub-theme can flip it. Returns `undefined` when any color fails
|
|
50
|
+
* to resolve/parse — callers keep their own default in that case.
|
|
51
|
+
*/
|
|
52
|
+
export function useReadableTextOn(fillToken: string): string | undefined {
|
|
53
|
+
const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
|
|
54
|
+
const fill = resolveTokenValue(theme, fillToken);
|
|
55
|
+
const paper = resolveTokenValue(theme, "color1");
|
|
56
|
+
const ink = resolveTokenValue(theme, "color12");
|
|
57
|
+
return useMemo(() => {
|
|
58
|
+
if (!fill || !paper || !ink) return undefined;
|
|
59
|
+
if (!normalizeToHex(fill) || !normalizeToHex(paper) || !normalizeToHex(ink)) return undefined;
|
|
60
|
+
return pickReadableForeground(fill, paper, ink);
|
|
61
|
+
}, [fill, paper, ink]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Accent-hued text/link color readable on the page surfaces (color1/color2).
|
|
66
|
+
* Scans the theme's own accent ramp for the most chromatic step that clears
|
|
67
|
+
* the AA text floor against BOTH page surfaces; falls back to readable neutral
|
|
68
|
+
* ink (`$color12`) when no accent step qualifies or the ramp is absent (e.g.
|
|
69
|
+
* tint sub-themes, where neutral text is the house default anyway).
|
|
70
|
+
*/
|
|
71
|
+
export function useAccentOnSurface(): string {
|
|
72
|
+
const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
|
|
73
|
+
const c1 = resolveTokenValue(theme, "color1");
|
|
74
|
+
const c2 = resolveTokenValue(theme, "color2");
|
|
75
|
+
const ink = resolveTokenValue(theme, "color12") ?? "$color12";
|
|
76
|
+
const steps: (string | undefined)[] = [];
|
|
77
|
+
for (let i = 1; i <= 12; i++) steps.push(resolveTokenValue(theme, `accent${i}`));
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
return useMemo(() => {
|
|
80
|
+
const bg1 = c1 ? normalizeToHex(c1) : null;
|
|
81
|
+
const bg2 = c2 ? normalizeToHex(c2) : null;
|
|
82
|
+
if (!bg1 || !bg2) return ink;
|
|
83
|
+
const l1 = relativeLuminance(bg1);
|
|
84
|
+
const l2 = relativeLuminance(bg2);
|
|
85
|
+
let best: string | undefined;
|
|
86
|
+
let bestMin = Number.POSITIVE_INFINITY;
|
|
87
|
+
for (const step of steps) {
|
|
88
|
+
const hex = step ? normalizeToHex(step) : null;
|
|
89
|
+
if (!step || !hex) continue;
|
|
90
|
+
const stepLum = relativeLuminance(hex);
|
|
91
|
+
const min = Math.min(contrastRatio(stepLum, l1), contrastRatio(stepLum, l2));
|
|
92
|
+
// Most chromatic passing step = the lowest contrast that still clears AA
|
|
93
|
+
// (accent steps get paler/whiter as contrast climbs).
|
|
94
|
+
if (min >= aaTextContrastRatio && min < bestMin) {
|
|
95
|
+
bestMin = min;
|
|
96
|
+
best = step;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return best ?? ink;
|
|
100
|
+
}, [c1, c2, ink, steps.join("|")]);
|
|
101
|
+
}
|
package/src/theme/recipes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TransitionProp } from "tamagui";
|
|
1
|
+
import type { ColorTokens, TransitionProp } from "tamagui";
|
|
2
2
|
import type { Knobs } from "./knobs";
|
|
3
3
|
|
|
4
4
|
// ── State knob prop types (by concern) ───────────────────────────────────
|
|
@@ -8,6 +8,11 @@ export interface ControlStateProps {
|
|
|
8
8
|
borderWidth?: number;
|
|
9
9
|
backgroundColor?: string;
|
|
10
10
|
padding?: string;
|
|
11
|
+
/** Focus-visible ring (DG-A11Y-01); always ≥2 via `ensureFocusVisibleRing`. */
|
|
12
|
+
outlineWidth?: number;
|
|
13
|
+
outlineStyle?: "solid" | "none";
|
|
14
|
+
outlineColor?: string;
|
|
15
|
+
outlineOffset?: number;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export interface TextStateProps {
|
|
@@ -18,7 +23,65 @@ export interface TextStateProps {
|
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export interface ElevationStateProps {
|
|
21
|
-
|
|
26
|
+
/** Size token on web ("$1"); numeric tokens.size value on native (RN-safe). */
|
|
27
|
+
elevation?: string | number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Disabled state recipe (LC-40 DISABLED-VISIBLE) ───────────────────────
|
|
31
|
+
|
|
32
|
+
/** Text-bearing control chrome (Button, Chip, Input.Box): muted fill/border, no opacity. */
|
|
33
|
+
export interface DisabledSurfaceProps {
|
|
34
|
+
backgroundColor?: string;
|
|
35
|
+
borderColor?: string;
|
|
36
|
+
cursor?: "not-allowed";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Text-free control chrome (switch track, checkbox box, radio ring, stars): opacity dim. */
|
|
40
|
+
export interface DisabledChromeProps {
|
|
41
|
+
opacity?: number;
|
|
42
|
+
cursor?: "not-allowed";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Label/value text and glyphs riding inside disabled surface chrome (stays ≥ AA). */
|
|
46
|
+
export interface DisabledTextProps {
|
|
47
|
+
color?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whole field assembly (FieldLayout wrapper / standalone control frame). */
|
|
51
|
+
export interface DisabledAssemblyProps {
|
|
52
|
+
opacity?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolved disabled treatment (LC-40 DISABLED-VISIBLE / DG-ST-05). A control
|
|
57
|
+
* rendered disabled MUST be visually distinguishable from its enabled twin
|
|
58
|
+
* under BOTH `disabledStyle` values. Fragments by anatomy — spread whole,
|
|
59
|
+
* never cherry-pick, and only when `disabled` (read-only is a different
|
|
60
|
+
* state and never dims):
|
|
61
|
+
*
|
|
62
|
+
* - `surfaceKnobProps` — chrome that CONTAINS text (Button, Chip, Input.Box):
|
|
63
|
+
* fill/border wash to the muted tier without opacity so the inner
|
|
64
|
+
* label/value text (via `textKnobProps`, `$color11`) keeps ≥ 4.5:1.
|
|
65
|
+
* - `selectedSurfaceKnobProps` — a SELECTED segment inside disabled
|
|
66
|
+
* text-bearing chrome (ToggleGroup / segmented controls): one wash step
|
|
67
|
+
* stronger than `surfaceKnobProps` so which option is chosen stays
|
|
68
|
+
* visible while disabled (Axiom 6) — components drop their live active
|
|
69
|
+
* fill when disabled (the `active` variant beats the wash), so the
|
|
70
|
+
* recipe owns the selected tier under BOTH disabledStyle values.
|
|
71
|
+
* - `chromeKnobProps` — chrome with no text of its own (switch track,
|
|
72
|
+
* checkbox box, radio ring, slider, rating stars): opacity dim.
|
|
73
|
+
* - `assemblyKnobProps` — the whole assembly; populated only under
|
|
74
|
+
* `dimWhole` (FieldLayout wrapper; standalone controls spread it after
|
|
75
|
+
* their surface fragment since they are their own assembly).
|
|
76
|
+
*/
|
|
77
|
+
export interface DisabledRecipe {
|
|
78
|
+
surfaceKnobProps: DisabledSurfaceProps;
|
|
79
|
+
selectedSurfaceKnobProps: DisabledSurfaceProps;
|
|
80
|
+
chromeKnobProps: DisabledChromeProps;
|
|
81
|
+
textKnobProps: DisabledTextProps;
|
|
82
|
+
assemblyKnobProps: DisabledAssemblyProps;
|
|
83
|
+
/** Effective `disabledStyle` knob value ("keepLabel" | "dimWhole"). */
|
|
84
|
+
style: string;
|
|
22
85
|
}
|
|
23
86
|
|
|
24
87
|
/**
|
|
@@ -34,18 +97,65 @@ export interface KnobRecipe<T> {
|
|
|
34
97
|
|
|
35
98
|
// ── KnobProps: resolved style objects from knobs ──────────────────────────
|
|
36
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Corner smoothing carrier (Axiom 15 OPTICS): radius-bearing fragments gain
|
|
102
|
+
* `className: "mp-corner-smooth"` when `cornerSmoothing: "smooth"` (web
|
|
103
|
+
* only) — the mounted stylesheet in `cornerSmoothing.ts` resolves it to
|
|
104
|
+
* `corner-shape: squircle`, a Chromium 139+ progressive enhancement other
|
|
105
|
+
* engines ignore. Absent at `round` and on native.
|
|
106
|
+
*/
|
|
107
|
+
export interface CornerSmoothingCarrier {
|
|
108
|
+
className?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
37
111
|
export interface KnobProps {
|
|
38
|
-
borderRadius: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
112
|
+
borderRadius: {
|
|
113
|
+
borderRadius: string;
|
|
114
|
+
borderColor: string;
|
|
115
|
+
borderWidth: number;
|
|
116
|
+
} & CornerSmoothingCarrier;
|
|
117
|
+
borderRadiusOuter: { borderRadius: string } & CornerSmoothingCarrier;
|
|
118
|
+
/**
|
|
119
|
+
* Axiom 1 child-clip cap for padded containers (Card/Panel/DialogContent):
|
|
120
|
+
* the radius token while it fits, or the padding px once it would exceed
|
|
121
|
+
* the container's own inset. Spread after the radius-bearing fragment.
|
|
122
|
+
*/
|
|
123
|
+
containerRadius: { borderRadius: string | number } & CornerSmoothingCarrier;
|
|
124
|
+
/** Size token on web ("$1"); numeric tokens.size value on native (RN-safe). */
|
|
125
|
+
elevation: string | number | undefined;
|
|
126
|
+
surface: { backgroundColor: string; borderColor: string; borderWidth: number };
|
|
127
|
+
/** Content tier (Bento content card): padded frame, no border, no shadow. Radius carries the container cap. */
|
|
128
|
+
cardSurface: {
|
|
129
|
+
backgroundColor: string;
|
|
130
|
+
borderRadius: string | number;
|
|
131
|
+
padding: string;
|
|
132
|
+
} & CornerSmoothingCarrier;
|
|
133
|
+
/** Elevated/overlay tier (popovers, dropdowns): border + elevation. */
|
|
134
|
+
elevatedSurface: {
|
|
135
|
+
backgroundColor: string;
|
|
136
|
+
borderColor: string;
|
|
137
|
+
borderWidth: number;
|
|
138
|
+
borderRadius: string;
|
|
139
|
+
elevation: string | number | undefined;
|
|
140
|
+
} & CornerSmoothingCarrier;
|
|
141
|
+
/** Feature/marketing tier (auth, hero cards): big radius + ultra-soft wide shadow. */
|
|
142
|
+
featureSurface: {
|
|
143
|
+
backgroundColor: string;
|
|
144
|
+
borderRadius: string;
|
|
145
|
+
padding: string;
|
|
146
|
+
shadowColor: string;
|
|
147
|
+
shadowOffset: { width: number; height: number };
|
|
148
|
+
shadowOpacity: number;
|
|
149
|
+
shadowRadius: number;
|
|
150
|
+
} & CornerSmoothingCarrier;
|
|
42
151
|
inputSurface: { borderWidth: number };
|
|
43
152
|
panelPadding: { padding: string };
|
|
44
153
|
gap: { gap: string };
|
|
45
154
|
gapLg: { gap: string };
|
|
46
155
|
sizeToken: string;
|
|
47
156
|
heading: { fontFamily: string; fontWeight: string };
|
|
48
|
-
|
|
157
|
+
/** color is set only for solid semantic-intent Buttons (error/warning/success). */
|
|
158
|
+
body: { fontFamily: string; fontWeight: string; color?: ColorTokens };
|
|
49
159
|
textWeight: { fontWeight: string };
|
|
50
160
|
transition: TransitionProp | undefined;
|
|
51
161
|
outlined: boolean;
|
|
@@ -55,6 +165,17 @@ export interface KnobProps {
|
|
|
55
165
|
inputBackground: string;
|
|
56
166
|
space: string;
|
|
57
167
|
size: string;
|
|
168
|
+
/** Effective density mode after compact override resolution. */
|
|
169
|
+
density: string;
|
|
170
|
+
/** House decision knobs — pass-through for consumers (forms/tables). */
|
|
171
|
+
fieldLabelPlacement: string;
|
|
172
|
+
requiredMarking: string;
|
|
173
|
+
tableZebra: string;
|
|
174
|
+
bulkBarPlacement: string;
|
|
175
|
+
selectAllScope: string;
|
|
176
|
+
timestampStyle: string;
|
|
177
|
+
disabledStyle: string;
|
|
178
|
+
formAutofocus: string;
|
|
58
179
|
}
|
|
59
180
|
|
|
60
181
|
// ── Return type ───────────────────────────────────────────────────────────
|
|
@@ -64,6 +185,8 @@ export interface ResolvedKnobs {
|
|
|
64
185
|
control: KnobRecipe<ControlStateProps>;
|
|
65
186
|
text: KnobRecipe<TextStateProps>;
|
|
66
187
|
elevation: KnobRecipe<ElevationStateProps>;
|
|
188
|
+
/** LC-40: resolved disabled treatment (named to avoid `disabled` prop clashes). */
|
|
189
|
+
disabledState: DisabledRecipe;
|
|
67
190
|
}
|
|
68
191
|
|
|
69
192
|
export type KnobPropsOverride = (knobProps: KnobProps, knobs: Knobs) => Partial<KnobProps>;
|