@multiplatform.one/theme 7.45.1 → 7.47.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 +33 -0
- package/expo.cjs +3 -0
- package/package.json +22 -6
- package/src/audit/constraintAudit.ts +2 -1
- package/src/audit/themeMatrix.ts +1 -0
- package/src/expo.spec.ts +83 -0
- package/src/expo.ts +57 -0
- package/src/expoConfig.spec.ts +67 -0
- package/src/figma/figmaTokens.spec.ts +4 -3
- package/src/theme/FontKnobStyles.tsx +13 -1
- package/src/theme/Heading.tsx +70 -0
- package/src/theme/Intent.stories.tsx +2 -2
- package/src/theme/Preset.stories.tsx +2 -2
- package/src/theme/Preset.tsx +4 -2
- package/src/theme/Surface.stories.tsx +2 -1
- package/src/theme/Surface.tsx +4 -2
- package/src/theme/Text.tsx +152 -0
- package/src/theme/ThemeProvider.stories.tsx +2 -1
- package/src/theme/Tint.stories.tsx +2 -1
- package/src/theme/accentFloor.spec.ts +5 -3
- package/src/theme/accentOnSurface.palette.spec.tsx +75 -0
- package/src/theme/accentOnSurface.spec.tsx +149 -0
- package/src/theme/colorThemeAccent.spec.ts +5 -2
- package/src/theme/cookies.spec.ts +2 -2
- package/src/theme/createDefaultThemeConfig.spec.ts +2 -2
- package/src/theme/createThemes.ts +91 -11
- package/src/theme/defaults/builderOptions.ts +18 -0
- package/src/theme/devtools/ColorLineVisualizer.tsx +3 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +2 -0
- package/src/theme/hairline.native.ts +3 -0
- package/src/theme/hairline.ts +9 -0
- package/src/theme/hairlineToken.spec.ts +110 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/knobs.ts +2 -2
- package/src/theme/mpo19Proof.spec.ts +4 -1
- package/src/theme/overlaySurfaces.tsx +9 -3
- package/src/theme/pageGutter.tsx +61 -0
- package/src/theme/preset.spec.tsx +29 -1
- package/src/theme/presets.spec.ts +1 -1
- package/src/theme/presets.ts +2 -2
- package/src/theme/ramps.spec.ts +53 -0
- package/src/theme/ramps.ts +75 -0
- package/src/theme/readableColor.ts +111 -70
- package/src/theme/recipes.ts +51 -2
- package/src/theme/resolveFontTokenFamily.spec.ts +44 -0
- package/src/theme/resolveKnobs.spec.ts +30 -20
- package/src/theme/resolveKnobs.ts +87 -20
- package/src/theme/selectedState.spec.ts +127 -0
- package/types/audit/constraintAudit.d.ts.map +1 -1
- package/types/audit/themeMatrix.d.ts.map +1 -1
- package/types/expo.d.ts +29 -0
- package/types/expo.d.ts.map +1 -0
- package/types/theme/FontKnobStyles.d.ts +7 -0
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Heading.d.ts +10 -0
- package/types/theme/Heading.d.ts.map +1 -0
- package/types/theme/Preset.d.ts.map +1 -1
- package/types/theme/Surface.d.ts +2 -1
- package/types/theme/Surface.d.ts.map +1 -1
- package/types/theme/Text.d.ts +13 -0
- package/types/theme/Text.d.ts.map +1 -0
- package/types/theme/colorRules.d.ts +1 -1
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/fonts.d.ts +30 -30
- package/types/theme/devtools/ColorLineVisualizer.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/hairline.d.ts +8 -0
- package/types/theme/hairline.d.ts.map +1 -1
- package/types/theme/hairline.native.d.ts +2 -0
- package/types/theme/hairline.native.d.ts.map +1 -1
- package/types/theme/index.d.ts +4 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +1 -1
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/overlaySurfaces.d.ts.map +1 -1
- package/types/theme/pageGutter.d.ts +41 -0
- package/types/theme/pageGutter.d.ts.map +1 -0
- package/types/theme/ramps.d.ts +20 -0
- package/types/theme/ramps.d.ts.map +1 -0
- package/types/theme/readableColor.d.ts +24 -12
- package/types/theme/readableColor.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +44 -4
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +10 -4
- package/types/theme/resolveKnobs.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -89,6 +89,39 @@ is the same filter as a pure function over a built matrix.
|
|
|
89
89
|
Full standard: `docs/standards/frontend/knobs-system.md` in the
|
|
90
90
|
[multiplatform.one repo](https://git.corp.bitspur.com/multiplatform.one/mpo).
|
|
91
91
|
|
|
92
|
+
## Expo launch backgrounds
|
|
93
|
+
|
|
94
|
+
Expo evaluates app configuration in Node before a React provider exists. Use the
|
|
95
|
+
build-time entry to read the same background tokens as the app's initial preset:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
const { createExpoThemeColors } = require("@multiplatform.one/theme/expo");
|
|
99
|
+
const colors = createExpoThemeColors({ preset: "default" });
|
|
100
|
+
const splash = {
|
|
101
|
+
image: "./assets/splash.png",
|
|
102
|
+
resizeMode: "contain",
|
|
103
|
+
...colors.splash,
|
|
104
|
+
dark: { ...colors.splash.dark, image: "./assets/splash.png" },
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Pass `splash` to the `expo-splash-screen` plugin, or to both `ios.splash` and
|
|
109
|
+
`android.splash` when using legacy Expo splash configuration. Top-level
|
|
110
|
+
`splash` alone does not configure the iOS dark appearance. Spread
|
|
111
|
+
`colors.adaptiveIcon` into `android.adaptiveIcon`, alongside the foreground image.
|
|
112
|
+
|
|
113
|
+
Built-in presets are `default`, `bold`, `hero`, `dense` and `roomy`. For custom
|
|
114
|
+
presets, pass `presets: { name: { theme: "themeName" } }` and the actual built
|
|
115
|
+
`themes` table. Each supplied table replaces its default. A blank theme name
|
|
116
|
+
uses `light.background` and `dark.background`; a named theme uses
|
|
117
|
+
`light_themeName.background` and `dark_themeName.background`. Raw strings and
|
|
118
|
+
Tamagui `{ val: string }` tokens are accepted. Unknown presets, missing tokens
|
|
119
|
+
and non-opaque colors fail configuration.
|
|
120
|
+
|
|
121
|
+
Adaptive icons have one background. `adaptiveIconScheme: "dark"` selects the
|
|
122
|
+
dark token; the default is light. No runtime setting can change these baked-in
|
|
123
|
+
assets: changing tokens or the launch preset requires a new native build.
|
|
124
|
+
|
|
92
125
|
## License
|
|
93
126
|
|
|
94
127
|
Apache-2.0
|
package/expo.cjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/theme",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.47.0",
|
|
4
4
|
"description": "Tamagui theme system for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multiplatform",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"src",
|
|
21
21
|
"!src/**/__snapshots__",
|
|
22
|
+
"expo.cjs",
|
|
22
23
|
"types",
|
|
23
24
|
"!types/.tsbuildinfo"
|
|
24
25
|
],
|
|
@@ -49,32 +50,47 @@
|
|
|
49
50
|
"types": "./types/audit/designTokens.d.ts",
|
|
50
51
|
"default": "./src/audit/designTokens.ts"
|
|
51
52
|
},
|
|
53
|
+
"./ramps": {
|
|
54
|
+
"source": "./src/theme/ramps.ts",
|
|
55
|
+
"types": "./types/theme/ramps.d.ts",
|
|
56
|
+
"default": "./src/theme/ramps.ts"
|
|
57
|
+
},
|
|
52
58
|
"./motion": {
|
|
53
59
|
"source": "./src/theme/motionTiming.ts",
|
|
54
60
|
"types": "./types/theme/motionTiming.d.ts",
|
|
55
61
|
"default": "./src/theme/motionTiming.ts"
|
|
62
|
+
},
|
|
63
|
+
"./expo": {
|
|
64
|
+
"source": "./src/expo.ts",
|
|
65
|
+
"types": "./types/expo.d.ts",
|
|
66
|
+
"react-native": "./src/expo.ts",
|
|
67
|
+
"browser": "./src/expo.ts",
|
|
68
|
+
"default": "./expo.cjs"
|
|
56
69
|
}
|
|
57
70
|
},
|
|
58
71
|
"publishConfig": {
|
|
59
72
|
"access": "public"
|
|
60
73
|
},
|
|
61
74
|
"dependencies": {
|
|
62
|
-
"@multiplatform.one/platform": "^7.
|
|
63
|
-
"@multiplatform.one/store": "^7.
|
|
75
|
+
"@multiplatform.one/platform": "^7.47.0",
|
|
76
|
+
"@multiplatform.one/store": "^7.47.0",
|
|
64
77
|
"@tamagui/animations-react-native": "2.7.6",
|
|
65
78
|
"@tamagui/colors": "2.7.6",
|
|
66
79
|
"@tamagui/config": "2.7.6",
|
|
67
80
|
"@tamagui/get-token": "2.7.6",
|
|
81
|
+
"@tamagui/normalize-css-color": "2.7.6",
|
|
68
82
|
"@tamagui/shorthands": "2.7.6",
|
|
69
83
|
"@tamagui/theme-builder": "2.7.6",
|
|
70
84
|
"@tamagui/themes": "2.7.6",
|
|
71
85
|
"@tamagui/toast": "2.7.6",
|
|
72
86
|
"@tamagui/web": "2.7.6",
|
|
87
|
+
"jiti": "^2.7.0",
|
|
73
88
|
"react-cookie": "^8.1.2"
|
|
74
89
|
},
|
|
75
90
|
"devDependencies": {
|
|
76
|
-
"@
|
|
77
|
-
"@multiplatform.one/
|
|
91
|
+
"@expo/prebuild-config": "55.0.18",
|
|
92
|
+
"@multiplatform.one/config": "7.47.0",
|
|
93
|
+
"@multiplatform.one/test-utils": "7.47.0",
|
|
78
94
|
"@tamagui/animations-css": "2.7.6",
|
|
79
95
|
"@tamagui/animations-reanimated": "2.7.6",
|
|
80
96
|
"@tamagui/font-inter": "2.7.6",
|
|
@@ -112,6 +128,6 @@
|
|
|
112
128
|
"scripts": {
|
|
113
129
|
"test": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=lcov --coverage.reporter=html",
|
|
114
130
|
"typecheck": "tsgo --noEmit",
|
|
115
|
-
"build": "
|
|
131
|
+
"build": "tsc -p tsconfig.build.json"
|
|
116
132
|
}
|
|
117
133
|
}
|
|
@@ -321,6 +321,7 @@ function reverseMapFontFamily(computed: string): { knob: string; offScale: boole
|
|
|
321
321
|
* Returns an AuditReport object describing all structural CSS violations.
|
|
322
322
|
*/
|
|
323
323
|
export function runConstraintAudit(rootSelector?: string): AuditReport {
|
|
324
|
+
if (typeof document === "undefined") throw new Error("runConstraintAudit needs a DOM");
|
|
324
325
|
// ── Inline token definitions (must be self-contained for page.evaluate) ──
|
|
325
326
|
// The page's published scale wins; the second argument is the default
|
|
326
327
|
// scale's value, for pages that publish none.
|
|
@@ -709,7 +710,7 @@ export function runConstraintAudit(rootSelector?: string): AuditReport {
|
|
|
709
710
|
const scope = rootSelector ? document.querySelector(rootSelector) : document;
|
|
710
711
|
if (!scope) throw new Error(`Constraint audit scope not found: ${rootSelector}`);
|
|
711
712
|
const allElements = [
|
|
712
|
-
...(scope instanceof Element ? [scope] : []),
|
|
713
|
+
...(typeof Element !== "undefined" && scope instanceof Element ? [scope] : []),
|
|
713
714
|
...Array.from(scope.querySelectorAll("*")),
|
|
714
715
|
];
|
|
715
716
|
const candidates = allElements.filter((el) => {
|
package/src/audit/themeMatrix.ts
CHANGED
|
@@ -256,6 +256,7 @@ export interface TextContrastResult {
|
|
|
256
256
|
* `page.evaluate(captureMatrixSnapshot)`.
|
|
257
257
|
*/
|
|
258
258
|
export function captureMatrixSnapshot(): MatrixSnapshot {
|
|
259
|
+
if (typeof document === "undefined") throw new Error("captureMatrixSnapshot needs a DOM");
|
|
259
260
|
const SKIP_TAGS = new Set([
|
|
260
261
|
"script",
|
|
261
262
|
"style",
|
package/src/expo.spec.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createExpoThemeColors } from "./expo";
|
|
3
|
+
import { createThemesBuilder } from "./theme/createThemes";
|
|
4
|
+
import { defaultBaseTheme } from "./theme/defaults/base";
|
|
5
|
+
import { defaultAccentTheme } from "./theme/defaults/accent";
|
|
6
|
+
import { defaultBuilderOptions } from "./theme/defaults/builderOptions";
|
|
7
|
+
import * as presets from "./theme/presets";
|
|
8
|
+
import { normalizeToHex } from "./theme/colorRules";
|
|
9
|
+
|
|
10
|
+
const themes = createThemesBuilder(
|
|
11
|
+
defaultBaseTheme,
|
|
12
|
+
defaultAccentTheme,
|
|
13
|
+
defaultBuilderOptions,
|
|
14
|
+
).themes();
|
|
15
|
+
|
|
16
|
+
describe("Expo theme colors", () => {
|
|
17
|
+
it.each(["purple", "blue", "orange", "gray"])("supports the app preset hue %s", (theme) => {
|
|
18
|
+
const result = createExpoThemeColors({ preset: "app", presets: { app: { theme } } });
|
|
19
|
+
expect(result.splash.backgroundColor).toBe(normalizeToHex(themes[`light_${theme}`].background));
|
|
20
|
+
expect(result.splash.dark.backgroundColor).toBe(
|
|
21
|
+
normalizeToHex(themes[`dark_${theme}`].background),
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects an invalid adaptive-icon scheme from JavaScript configs", () => {
|
|
26
|
+
expect(() =>
|
|
27
|
+
createExpoThemeColors({
|
|
28
|
+
adaptiveIconScheme: "system" as "light",
|
|
29
|
+
}),
|
|
30
|
+
).toThrow(/adaptiveIconScheme/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each(Object.entries(presets))("resolves both background tokens for %s", (name, preset) => {
|
|
34
|
+
const presetName = name.replace(/Preset$/, "");
|
|
35
|
+
const suffix = preset.theme ? `_${preset.theme}` : "";
|
|
36
|
+
const result = createExpoThemeColors({ preset: presetName });
|
|
37
|
+
expect(result.splash.backgroundColor).toBe(normalizeToHex(themes[`light${suffix}`].background));
|
|
38
|
+
expect(result.splash.dark.backgroundColor).toBe(
|
|
39
|
+
normalizeToHex(themes[`dark${suffix}`].background),
|
|
40
|
+
);
|
|
41
|
+
expect(result.adaptiveIcon.backgroundColor).toBe(result.splash.backgroundColor);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("reads changed custom tokens on every call", () => {
|
|
45
|
+
const custom = {
|
|
46
|
+
light_brand: { background: "rgb(240, 230, 220)" },
|
|
47
|
+
dark_brand: { background: { val: "hsl(210, 50%, 20%)" } },
|
|
48
|
+
};
|
|
49
|
+
const options = {
|
|
50
|
+
preset: "brand",
|
|
51
|
+
presets: { brand: { theme: "brand" } },
|
|
52
|
+
themes: custom,
|
|
53
|
+
};
|
|
54
|
+
expect(createExpoThemeColors(options).splash).toEqual({
|
|
55
|
+
backgroundColor: "#f0e6dc",
|
|
56
|
+
dark: { backgroundColor: "#19334d" },
|
|
57
|
+
});
|
|
58
|
+
custom.light_brand.background = "#abcdef";
|
|
59
|
+
expect(createExpoThemeColors(options).splash.backgroundColor).toBe("#abcdef");
|
|
60
|
+
custom.dark_brand.background.val = "#123456";
|
|
61
|
+
expect(createExpoThemeColors({ ...options, adaptiveIconScheme: "dark" }).adaptiveIcon).toEqual({
|
|
62
|
+
backgroundColor: "#123456",
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("rejects an unknown preset or missing scheme instead of baking in a fallback", () => {
|
|
67
|
+
expect(() => createExpoThemeColors({ preset: "typo" })).toThrow(/preset.*typo/i);
|
|
68
|
+
expect(() => createExpoThemeColors({ themes: { light: { background: "#fff" } } })).toThrow(
|
|
69
|
+
/dark.*background/i,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it.each(["transparent", "rgba(1, 2, 3, 0.5)", "var(--background)", "$color2", "#xyz"])(
|
|
74
|
+
"rejects an unresolved or non-opaque background: %s",
|
|
75
|
+
(background) => {
|
|
76
|
+
expect(() =>
|
|
77
|
+
createExpoThemeColors({
|
|
78
|
+
themes: { light: { background }, dark: { background: "#000" } },
|
|
79
|
+
}),
|
|
80
|
+
).toThrow(/light.*background/i);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
});
|
package/src/expo.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { normalizeCSSColor } from "@tamagui/normalize-css-color";
|
|
2
|
+
import { createThemesBuilder } from "./theme/createThemes";
|
|
3
|
+
import { defaultAccentTheme } from "./theme/defaults/accent";
|
|
4
|
+
import { defaultBaseTheme } from "./theme/defaults/base";
|
|
5
|
+
import { defaultBuilderOptions } from "./theme/defaults/builderOptions";
|
|
6
|
+
import { boldPreset, defaultPreset, densePreset, heroPreset, roomyPreset } from "./theme/presets";
|
|
7
|
+
|
|
8
|
+
export { createThemesBuilder, defaultBaseTheme, defaultAccentTheme, defaultBuilderOptions };
|
|
9
|
+
|
|
10
|
+
export interface ExpoThemeColorOptions {
|
|
11
|
+
preset?: string;
|
|
12
|
+
presets?: Record<string, { theme: string }>;
|
|
13
|
+
themes?: Record<string, { background?: string | { val: string } }>;
|
|
14
|
+
adaptiveIconScheme?: "light" | "dark";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const builtinPresets = {
|
|
18
|
+
default: defaultPreset,
|
|
19
|
+
bold: boldPreset,
|
|
20
|
+
dense: densePreset,
|
|
21
|
+
hero: heroPreset,
|
|
22
|
+
roomy: roomyPreset,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createExpoThemeColors({
|
|
26
|
+
preset = "default",
|
|
27
|
+
presets = builtinPresets,
|
|
28
|
+
themes = createThemesBuilder(
|
|
29
|
+
defaultBaseTheme,
|
|
30
|
+
defaultAccentTheme,
|
|
31
|
+
defaultBuilderOptions,
|
|
32
|
+
).themes(),
|
|
33
|
+
adaptiveIconScheme = "light",
|
|
34
|
+
}: ExpoThemeColorOptions = {}) {
|
|
35
|
+
if (adaptiveIconScheme !== "light" && adaptiveIconScheme !== "dark") {
|
|
36
|
+
throw new Error('Expo adaptiveIconScheme must be "light" or "dark".');
|
|
37
|
+
}
|
|
38
|
+
const selected = Object.hasOwn(presets, preset) ? presets[preset] : undefined;
|
|
39
|
+
if (!selected) throw new Error(`Unknown Expo theme preset "${preset}".`);
|
|
40
|
+
const suffix = selected.theme ? `_${selected.theme}` : "";
|
|
41
|
+
const background = (scheme: "light" | "dark") => {
|
|
42
|
+
const name = `${scheme}${suffix}`;
|
|
43
|
+
const token = themes[name]?.background;
|
|
44
|
+
const value = typeof token === "string" ? token : token?.val;
|
|
45
|
+
const color = typeof value === "string" ? normalizeCSSColor(value) : null;
|
|
46
|
+
if (color == null || (color & 0xff) !== 0xff) {
|
|
47
|
+
throw new Error(`Expo theme "${name}" needs an opaque, resolved background token.`);
|
|
48
|
+
}
|
|
49
|
+
return `#${(color >>> 8).toString(16).padStart(6, "0")}`;
|
|
50
|
+
};
|
|
51
|
+
const light = background("light");
|
|
52
|
+
const dark = background("dark");
|
|
53
|
+
return {
|
|
54
|
+
splash: { backgroundColor: light, dark: { backgroundColor: dark } },
|
|
55
|
+
adaptiveIcon: { backgroundColor: adaptiveIconScheme === "dark" ? dark : light },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { runInNewContext } from "node:vm";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { createExpoThemeColors } from "./expo";
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const {
|
|
11
|
+
getIosSplashConfig,
|
|
12
|
+
} = require("@expo/prebuild-config/build/plugins/unversioned/expo-splash-screen/getIosSplashConfig");
|
|
13
|
+
const {
|
|
14
|
+
getAndroidSplashConfig,
|
|
15
|
+
getAndroidDarkSplashConfig,
|
|
16
|
+
} = require("@expo/prebuild-config/build/plugins/unversioned/expo-splash-screen/getAndroidSplashConfig");
|
|
17
|
+
|
|
18
|
+
it("loads the published CommonJS entry in plain Node without React", () => {
|
|
19
|
+
const result = JSON.parse(
|
|
20
|
+
execFileSync(
|
|
21
|
+
process.execPath,
|
|
22
|
+
[
|
|
23
|
+
"-e",
|
|
24
|
+
`
|
|
25
|
+
const { createExpoThemeColors } = require("@multiplatform.one/theme/expo");
|
|
26
|
+
const colors = createExpoThemeColors();
|
|
27
|
+
const runtime = Object.keys(require.cache).filter(path =>
|
|
28
|
+
/node_modules\\/(react|react-native|tamagui|@tamagui\\/web)\\//.test(path));
|
|
29
|
+
process.stdout.write(JSON.stringify({ colors, runtime }));
|
|
30
|
+
`,
|
|
31
|
+
],
|
|
32
|
+
{ cwd: resolve(__dirname, ".."), encoding: "utf8" },
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
expect(result.colors).toEqual(createExpoThemeColors());
|
|
36
|
+
expect(result.runtime).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe.each(["one", "storybook-expo"])("%s Expo config", (app) => {
|
|
40
|
+
it("passes light/dark tokens and the original image to native prebuild", () => {
|
|
41
|
+
const filename = resolve(__dirname, "../../../apps", app, "app.config.js");
|
|
42
|
+
const appRequire = createRequire(filename);
|
|
43
|
+
const module = { exports: {} as { expo: Record<string, any> } };
|
|
44
|
+
runInNewContext(readFileSync(filename, "utf8"), {
|
|
45
|
+
module,
|
|
46
|
+
__dirname: resolve(filename, ".."),
|
|
47
|
+
process: { env: {} },
|
|
48
|
+
require: (name: string) => (name === "dotenv" ? { config() {} } : appRequire(name)),
|
|
49
|
+
});
|
|
50
|
+
const { expo } = module.exports;
|
|
51
|
+
const colors = createExpoThemeColors();
|
|
52
|
+
const ios = getIosSplashConfig(expo);
|
|
53
|
+
const android = getAndroidSplashConfig(expo);
|
|
54
|
+
const androidDark = getAndroidDarkSplashConfig(expo);
|
|
55
|
+
expect(ios.backgroundColor).toBe(colors.splash.backgroundColor);
|
|
56
|
+
expect(ios.dark.backgroundColor).toBe(colors.splash.dark.backgroundColor);
|
|
57
|
+
expect(android.backgroundColor).toBe(colors.splash.backgroundColor);
|
|
58
|
+
expect(androidDark.backgroundColor).toBe(colors.splash.dark.backgroundColor);
|
|
59
|
+
expect(ios.image).toBe("./assets/splash.png");
|
|
60
|
+
expect(android.image).toBe(ios.image);
|
|
61
|
+
expect(androidDark.image).toBe(ios.image);
|
|
62
|
+
expect(expo.android.adaptiveIcon).toEqual({
|
|
63
|
+
foregroundImage: "./assets/adaptive-icon.png",
|
|
64
|
+
...colors.adaptiveIcon,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -6,6 +6,7 @@ import { normalizeToHex } from "../theme/colorRules";
|
|
|
6
6
|
import { defaultAccentTheme } from "../theme/defaults/accent";
|
|
7
7
|
import { defaultBaseTheme } from "../theme/defaults/base";
|
|
8
8
|
import { interTrackingPx } from "../theme/defaults/fonts";
|
|
9
|
+
import { hairlineBorderWidth } from "../theme/hairline";
|
|
9
10
|
import { FIGMA_THEME_MODES, buildFigmaTokens, figmaTokenName } from "./figmaTokens";
|
|
10
11
|
|
|
11
12
|
const fontFamilies = {
|
|
@@ -148,9 +149,9 @@ describe("buildFigmaTokens", () => {
|
|
|
148
149
|
expect(result.sizing["space-1-5"]).toBe((tokens.space as Record<string, number>)["$1.5"]);
|
|
149
150
|
});
|
|
150
151
|
|
|
151
|
-
it("exports knob-shaped borderWidth values
|
|
152
|
+
it("exports knob-shaped borderWidth values with the web hairline token (LC-148)", () => {
|
|
152
153
|
expect(result.sizing["borderWidth-none"]).toBe(0);
|
|
153
|
-
expect(result.sizing["borderWidth-small"]).toBe(
|
|
154
|
+
expect(result.sizing["borderWidth-small"]).toBe(hairlineBorderWidth);
|
|
154
155
|
expect(result.sizing["borderWidth-medium"]).toBe(1);
|
|
155
156
|
expect(result.sizing["borderWidth-large"]).toBe(2);
|
|
156
157
|
});
|
|
@@ -174,7 +175,7 @@ describe("buildFigmaTokens", () => {
|
|
|
174
175
|
it("exports borderWidth knob modes", () => {
|
|
175
176
|
expect(result.knobs.borderWidth.modes).toEqual({
|
|
176
177
|
none: 0,
|
|
177
|
-
small:
|
|
178
|
+
small: hairlineBorderWidth,
|
|
178
179
|
medium: 1,
|
|
179
180
|
large: 2,
|
|
180
181
|
});
|
|
@@ -76,14 +76,26 @@ export interface FontScopeTables {
|
|
|
76
76
|
* `--f-family` — live SHC console had the style tag and skipped the family).
|
|
77
77
|
*/
|
|
78
78
|
export function getConfiguredFontFamily(category: FontCategory): string | undefined {
|
|
79
|
+
return resolveFontTokenFamily(`$${category}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The family stack a resolved font token names in the Tamagui config, for
|
|
84
|
+
* text no Tamagui font reaches: `resolveFontTokenFamily(knobProps.body.fontFamily)`
|
|
85
|
+
* hands a mermaid diagram's SVG labels the face the bodyFont knob picked.
|
|
86
|
+
* Undefined outside a configured runtime or for a token the config lacks.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveFontTokenFamily(token: unknown): string | undefined {
|
|
89
|
+
if (typeof token !== "string" || !token) return undefined;
|
|
79
90
|
let config: ReturnType<typeof getConfig> | undefined;
|
|
80
91
|
try {
|
|
81
92
|
config = getConfig();
|
|
82
93
|
} catch {
|
|
83
94
|
return undefined;
|
|
84
95
|
}
|
|
96
|
+
const key = token.startsWith("$") ? token.slice(1) : token;
|
|
85
97
|
const fontsParsed = (config as { fontsParsed?: Record<string, unknown> }).fontsParsed;
|
|
86
|
-
const font = (fontsParsed?.[`$${
|
|
98
|
+
const font = (fontsParsed?.[`$${key}`] ?? config.fonts?.[key]) as
|
|
87
99
|
| { family?: string | { val?: string } }
|
|
88
100
|
| undefined;
|
|
89
101
|
const family = font?.family;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every heading takes headingFont on every platform. Regular keeps the heading
|
|
3
|
+
* weight table; bold writes 700. Consumer weight wins and rides last.
|
|
4
|
+
* H1 alone takes pageTitle: moderate $10 (40 px), display $12 (52 px), one
|
|
5
|
+
* token less below $sm (LC-143).
|
|
6
|
+
*/
|
|
7
|
+
import { useResolvedKnobs } from "./useResolvedKnobs";
|
|
8
|
+
import {
|
|
9
|
+
H1 as RawH1,
|
|
10
|
+
H2 as RawH2,
|
|
11
|
+
H3 as RawH3,
|
|
12
|
+
H4 as RawH4,
|
|
13
|
+
H5 as RawH5,
|
|
14
|
+
H6 as RawH6,
|
|
15
|
+
Heading as RawHeading,
|
|
16
|
+
type HeadingProps,
|
|
17
|
+
} from "tamagui";
|
|
18
|
+
|
|
19
|
+
export type { HeadingProps };
|
|
20
|
+
|
|
21
|
+
type Weight = HeadingProps["fontWeight"];
|
|
22
|
+
|
|
23
|
+
function useHeadingVoice(fontWeight: Weight) {
|
|
24
|
+
const { knobProps } = useResolvedKnobs();
|
|
25
|
+
const { fontWeight: knobWeight, ...heading } = knobProps.heading;
|
|
26
|
+
const resolvedWeight = fontWeight ?? (knobWeight as Weight);
|
|
27
|
+
return {
|
|
28
|
+
heading,
|
|
29
|
+
pageTitle: knobProps.pageTitle,
|
|
30
|
+
weight: resolvedWeight === undefined ? {} : { fontWeight: resolvedWeight },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Heading({ fontWeight, ...props }: HeadingProps) {
|
|
35
|
+
const { heading, weight } = useHeadingVoice(fontWeight);
|
|
36
|
+
return <RawHeading {...heading} {...props} {...weight} />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function H1({ fontWeight, ...props }: HeadingProps) {
|
|
40
|
+
const { heading, pageTitle, weight } = useHeadingVoice(fontWeight);
|
|
41
|
+
// An explicit size ejects the whole step. Spread anyway, the knob's compact
|
|
42
|
+
// `$max-sm` entry would still override that size on a phone.
|
|
43
|
+
const step = props.size === undefined ? pageTitle : null;
|
|
44
|
+
return <RawH1 {...step} {...heading} {...props} {...weight} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function H2({ fontWeight, ...props }: HeadingProps) {
|
|
48
|
+
const { heading, weight } = useHeadingVoice(fontWeight);
|
|
49
|
+
return <RawH2 {...heading} {...props} {...weight} />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function H3({ fontWeight, ...props }: HeadingProps) {
|
|
53
|
+
const { heading, weight } = useHeadingVoice(fontWeight);
|
|
54
|
+
return <RawH3 {...heading} {...props} {...weight} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function H4({ fontWeight, ...props }: HeadingProps) {
|
|
58
|
+
const { heading, weight } = useHeadingVoice(fontWeight);
|
|
59
|
+
return <RawH4 {...heading} {...props} {...weight} />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function H5({ fontWeight, ...props }: HeadingProps) {
|
|
63
|
+
const { heading, weight } = useHeadingVoice(fontWeight);
|
|
64
|
+
return <RawH5 {...heading} {...props} {...weight} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function H6({ fontWeight, ...props }: HeadingProps) {
|
|
68
|
+
const { heading, weight } = useHeadingVoice(fontWeight);
|
|
69
|
+
return <RawH6 {...heading} {...props} {...weight} />;
|
|
70
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { XStack, YStack } from "tamagui";
|
|
2
2
|
import { Intent, type IntentProps } from "./Intent";
|
|
3
3
|
import { useIntentContext, useResolvedKnobs } from "./useResolvedKnobs";
|
|
4
|
+
import { SizableText } from "./Text";
|
|
4
5
|
|
|
5
6
|
export default {
|
|
6
7
|
title: "Theme/Intent",
|
|
@@ -31,7 +32,6 @@ function Probe({ label }: { label: string }) {
|
|
|
31
32
|
{...knobProps.surface}
|
|
32
33
|
{...knobProps.borderRadius}
|
|
33
34
|
backgroundColor={knobProps.outlined ? "transparent" : "$background"}
|
|
34
|
-
borderColor="$borderColor"
|
|
35
35
|
padding="$3"
|
|
36
36
|
minWidth={140}
|
|
37
37
|
testID={`intent-probe-${label}`}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { XStack, YStack } from "tamagui";
|
|
2
2
|
import { Preset } from "./Preset";
|
|
3
3
|
import { useResolvedKnobs } from "./useResolvedKnobs";
|
|
4
|
+
import { SizableText } from "./Text";
|
|
4
5
|
|
|
5
6
|
export default {
|
|
6
7
|
title: "Theme/Preset",
|
|
@@ -28,7 +29,6 @@ function Probe({ label }: { label: string }) {
|
|
|
28
29
|
{...knobProps.surface}
|
|
29
30
|
{...knobProps.borderRadius}
|
|
30
31
|
backgroundColor={knobProps.outlined ? "transparent" : "$background"}
|
|
31
|
-
borderColor="$borderColor"
|
|
32
32
|
elevation={knobProps.elevation}
|
|
33
33
|
padding="$3"
|
|
34
34
|
minWidth={160}
|
package/src/theme/Preset.tsx
CHANGED
|
@@ -114,9 +114,11 @@ export function Preset({
|
|
|
114
114
|
let element = <PresetContext.Provider value={ctxValue}>{children}</PresetContext.Provider>;
|
|
115
115
|
|
|
116
116
|
// An explicit colour wraps in its Tamagui theme; `""` re-applies the preset's own.
|
|
117
|
+
// A Preset that takes `theme` keeps the wrapper even while it names nothing:
|
|
118
|
+
// adding it when a colour is picked re-parents the subtree and remounts it.
|
|
117
119
|
const wrap = theme ? theme : theme === "" ? ctxValue.preset.theme : undefined;
|
|
118
|
-
if (
|
|
119
|
-
element = <TamaguiTheme name={wrap}>{element}</TamaguiTheme>;
|
|
120
|
+
if (theme !== undefined) {
|
|
121
|
+
element = <TamaguiTheme name={wrap || undefined}>{element}</TamaguiTheme>;
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
return element;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { XStack, YStack } from "tamagui";
|
|
2
2
|
import { Surface } from "./Surface";
|
|
3
3
|
import { useResolvedKnobs } from "./useResolvedKnobs";
|
|
4
|
+
import { SizableText } from "./Text";
|
|
4
5
|
|
|
5
6
|
export default {
|
|
6
7
|
title: "Theme/Surface",
|
package/src/theme/Surface.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { createStyledContext, type SizeTokens } from "tamagui";
|
|
3
|
+
import { PageGutter } from "./pageGutter";
|
|
3
4
|
import { SIZE_KEYS } from "./scale/scaleInputs";
|
|
4
5
|
|
|
5
6
|
/** A Tamagui size key ("$3", "$true") or `unset` for no ceiling. */
|
|
@@ -92,7 +93,8 @@ export function clampSurfaceSize(requested: SurfaceSize, parent: SurfaceSize): S
|
|
|
92
93
|
* Forwards a size token to a subtree: every knob reader under it renders its
|
|
93
94
|
* controls at `size` unless it asks for a smaller one (LC-78 ceiling).
|
|
94
95
|
* `painted` marks (true) or resets (false) the painted-ancestor flag; omitted
|
|
95
|
-
* inherits it.
|
|
96
|
+
* inherits it. Either way a painted value opens a new surface with its own
|
|
97
|
+
* inset, so the page gutter does not reach inside it (LC-146).
|
|
96
98
|
*
|
|
97
99
|
* @example A table cell's editors at the row's control height
|
|
98
100
|
* <Surface size="$3">{cell}</Surface>
|
|
@@ -111,7 +113,7 @@ export function Surface({
|
|
|
111
113
|
const nextPainted = painted ?? parent.painted ?? false;
|
|
112
114
|
return (
|
|
113
115
|
<SurfaceContext.Provider size={nextSize} painted={nextPainted}>
|
|
114
|
-
{children}
|
|
116
|
+
{painted === undefined ? children : <PageGutter inset={null}>{children}</PageGutter>}
|
|
115
117
|
</SurfaceContext.Provider>
|
|
116
118
|
);
|
|
117
119
|
}
|