@cdx-ui/styles 0.0.1-beta.9 → 0.0.1-beta.91
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 +116 -20
- package/css/theme.css +201 -85
- package/css/vanilla.css +124 -54
- package/lib/commonjs/applyThemeOverride.js +154 -0
- package/lib/commonjs/applyThemeOverride.js.map +1 -0
- package/lib/commonjs/index.js +82 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/palette.js +262 -0
- package/lib/commonjs/palette.js.map +1 -0
- package/lib/commonjs/theming.js +255 -0
- package/lib/commonjs/theming.js.map +1 -0
- package/lib/commonjs/types.js +75 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useCdxFonts.js +4 -231
- package/lib/commonjs/useCdxFonts.js.map +1 -1
- package/lib/commonjs/useForgeFonts.js +237 -0
- package/lib/commonjs/useForgeFonts.js.map +1 -0
- package/lib/module/applyThemeOverride.js +149 -0
- package/lib/module/applyThemeOverride.js.map +1 -0
- package/lib/module/index.js +11 -20
- package/lib/module/index.js.map +1 -1
- package/lib/module/palette.js +257 -0
- package/lib/module/palette.js.map +1 -0
- package/lib/module/theming.js +239 -0
- package/lib/module/theming.js.map +1 -0
- package/lib/module/types.js +71 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useCdxFonts.js +3 -219
- package/lib/module/useCdxFonts.js.map +1 -1
- package/lib/module/useForgeFonts.js +223 -0
- package/lib/module/useForgeFonts.js.map +1 -0
- package/lib/runtime/prestige-vs-default.json +1 -0
- package/lib/runtime/pulse-vs-default.json +1 -0
- package/lib/runtime/token-to-css-var.json +672 -0
- package/lib/typescript/applyThemeOverride.d.ts +26 -0
- package/lib/typescript/applyThemeOverride.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -57
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/palette.d.ts +60 -0
- package/lib/typescript/palette.d.ts.map +1 -0
- package/lib/typescript/theming.d.ts +40 -0
- package/lib/typescript/theming.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +90 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/useCdxFonts.d.ts +3 -11
- package/lib/typescript/useCdxFonts.d.ts.map +1 -1
- package/lib/typescript/useForgeFonts.d.ts +12 -0
- package/lib/typescript/useForgeFonts.d.ts.map +1 -0
- package/package.json +27 -8
- package/runtime/prestige-vs-default.json +1 -0
- package/runtime/pulse-vs-default.json +1 -0
- package/runtime/token-to-css-var.json +672 -0
- package/src/__tests__/applyThemeOverride.test.ts +552 -0
- package/src/__tests__/generateColorScale.test.ts +296 -0
- package/src/__tests__/presetJson.test.ts +120 -0
- package/src/__tests__/sd.config.test.ts +856 -0
- package/src/__tests__/theming.test.ts +647 -0
- package/src/__tests__/types.test.ts +72 -0
- package/src/__tests__/useCdxFonts.test.ts +27 -0
- package/src/__tests__/useForgeFonts.test.ts +226 -0
- package/src/applyThemeOverride.ts +139 -0
- package/src/index.ts +36 -60
- package/src/palette.ts +307 -0
- package/src/theming.ts +268 -0
- package/src/types.ts +112 -0
- package/src/useCdxFonts.ts +3 -229
- package/src/useForgeFonts.ts +230 -0
- package/tokens/presets/.manifest.json +3 -3
- package/tokens/presets/poise.json +319 -39
- package/tokens/presets/prestige.json +1 -1
- package/tokens/presets/pulse.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OVERRIDE_SCHEMA_VERSION,
|
|
3
|
+
SUPPORTED_OVERRIDE_SCHEMA_VERSIONS,
|
|
4
|
+
INPUT_TOKEN_MAP,
|
|
5
|
+
presetFonts,
|
|
6
|
+
} from '../types';
|
|
7
|
+
import type { Preset } from '../types';
|
|
8
|
+
|
|
9
|
+
describe('OVERRIDE_SCHEMA_VERSION', () => {
|
|
10
|
+
it('is the current 1.0.0 schema version', () => {
|
|
11
|
+
expect(OVERRIDE_SCHEMA_VERSION).toBe('1.0.0');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('matches semver MAJOR.MINOR.PATCH shape', () => {
|
|
15
|
+
expect(OVERRIDE_SCHEMA_VERSION).toMatch(/^\d+\.\d+\.\d+$/);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('SUPPORTED_OVERRIDE_SCHEMA_VERSIONS', () => {
|
|
20
|
+
it('includes the current OVERRIDE_SCHEMA_VERSION', () => {
|
|
21
|
+
expect(SUPPORTED_OVERRIDE_SCHEMA_VERSIONS).toContain(OVERRIDE_SCHEMA_VERSION);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('is a non-empty array of semver strings', () => {
|
|
25
|
+
expect(Array.isArray(SUPPORTED_OVERRIDE_SCHEMA_VERSIONS)).toBe(true);
|
|
26
|
+
expect(SUPPORTED_OVERRIDE_SCHEMA_VERSIONS.length).toBeGreaterThan(0);
|
|
27
|
+
for (const v of SUPPORTED_OVERRIDE_SCHEMA_VERSIONS) {
|
|
28
|
+
expect(v).toMatch(/^\d+\.\d+\.\d+$/);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('INPUT_TOKEN_MAP', () => {
|
|
34
|
+
it('maps every known input key to a dotted token path', () => {
|
|
35
|
+
expect(INPUT_TOKEN_MAP).toEqual({
|
|
36
|
+
brandPrimary: 'color.brand',
|
|
37
|
+
accentPrimary: 'color.accent',
|
|
38
|
+
basePrimary: 'color.base',
|
|
39
|
+
displayFont: 'font.display',
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('uses dotted-path token namespaces for every value', () => {
|
|
44
|
+
for (const value of Object.values(INPUT_TOKEN_MAP)) {
|
|
45
|
+
expect(value).toMatch(/^[a-z]+(\.[a-z]+)+$/);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('presetFonts', () => {
|
|
51
|
+
const expectedPresets: Preset[] = ['poise', 'prestige', 'pulse'];
|
|
52
|
+
|
|
53
|
+
it('exposes exactly the three built-in presets as keys', () => {
|
|
54
|
+
expect(Object.keys(presetFonts).sort()).toEqual([...expectedPresets].sort());
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it.each(expectedPresets)('declares 3 display font families for %s', (preset) => {
|
|
58
|
+
expect(presetFonts[preset]).toHaveLength(3);
|
|
59
|
+
for (const family of presetFonts[preset]) {
|
|
60
|
+
expect(typeof family).toBe('string');
|
|
61
|
+
expect(family.length).toBeGreaterThan(0);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('matches the canonical preset → families map', () => {
|
|
66
|
+
expect(presetFonts).toEqual({
|
|
67
|
+
poise: ['Crimson Pro', 'Bitter', 'DM Sans'],
|
|
68
|
+
prestige: ['Libre Caslon Text', 'Cormorant', 'Libre Franklin'],
|
|
69
|
+
pulse: ['Outfit', 'Manrope', 'Public Sans'],
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-deprecated -- this entire test exercises the deprecated alias intentionally */
|
|
2
|
+
import { useFonts } from 'expo-font';
|
|
3
|
+
import * as useCdxFontsModule from '../useCdxFonts';
|
|
4
|
+
import * as useForgeFontsModule from '../useForgeFonts';
|
|
5
|
+
|
|
6
|
+
jest.mock('react-native', () => ({
|
|
7
|
+
Platform: { OS: 'ios' },
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
jest.mock('expo-font', () => ({
|
|
11
|
+
useFonts: jest.fn(() => [true, null]),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockedUseFonts = useFonts as jest.MockedFunction<typeof useFonts>;
|
|
15
|
+
|
|
16
|
+
describe('useCdxFonts', () => {
|
|
17
|
+
it('re-exports useForgeFonts as the exact same function (deprecated alias)', () => {
|
|
18
|
+
expect(useCdxFontsModule.useCdxFonts).toBe(useForgeFontsModule.useForgeFonts);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('invokes the underlying useFonts when called', () => {
|
|
22
|
+
mockedUseFonts.mockClear();
|
|
23
|
+
const result = useCdxFontsModule.useCdxFonts();
|
|
24
|
+
expect(mockedUseFonts).toHaveBeenCalledTimes(1);
|
|
25
|
+
expect(result).toEqual({ loaded: true, error: null });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useForgeFonts` decides which web-only fonts to bundle at *module evaluation*
|
|
3
|
+
* time (the `webFonts` const reads `Platform.OS` once). To exercise both
|
|
4
|
+
* branches we re-require the module under `jest.isolateModules` with a fresh
|
|
5
|
+
* `react-native` mock per case.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
interface CallResult {
|
|
13
|
+
hookResult: { loaded: boolean; error: Error | null };
|
|
14
|
+
capturedMap: Record<string, unknown>;
|
|
15
|
+
mockUseFonts: jest.Mock;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setupAndCall(opts: {
|
|
19
|
+
platformOS: 'ios' | 'android' | 'web';
|
|
20
|
+
result?: [boolean, Error | null];
|
|
21
|
+
}): CallResult {
|
|
22
|
+
const result: [boolean, Error | null] = opts.result ?? [true, null];
|
|
23
|
+
const mockUseFonts = jest.fn((_map: Record<string, unknown>) => result);
|
|
24
|
+
|
|
25
|
+
let hookResult: CallResult['hookResult'] = { loaded: false, error: null };
|
|
26
|
+
|
|
27
|
+
jest.isolateModules(() => {
|
|
28
|
+
jest.doMock('react-native', () => ({ Platform: { OS: opts.platformOS } }));
|
|
29
|
+
jest.doMock('expo-font', () => ({ useFonts: mockUseFonts }));
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
const mod = require('../useForgeFonts') as typeof import('../useForgeFonts');
|
|
33
|
+
hookResult = mod.useForgeFonts();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const capturedMap: Record<string, unknown> = mockUseFonts.mock.calls[0][0];
|
|
37
|
+
|
|
38
|
+
return { hookResult, capturedMap, mockUseFonts };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Expected display font keys — every preset, every weight, every italic variant.
|
|
42
|
+
// Mirrors the `displayFonts` map in `useForgeFonts.ts`.
|
|
43
|
+
const POISE_DISPLAY_FONT_KEYS = [
|
|
44
|
+
'Crimson Pro',
|
|
45
|
+
'Crimson Pro Medium',
|
|
46
|
+
'Crimson Pro SemiBold',
|
|
47
|
+
'Crimson Pro Bold',
|
|
48
|
+
'Crimson Pro Italic',
|
|
49
|
+
'Crimson Pro Medium Italic',
|
|
50
|
+
'Crimson Pro SemiBold Italic',
|
|
51
|
+
'Crimson Pro Bold Italic',
|
|
52
|
+
'Bitter',
|
|
53
|
+
'Bitter Medium',
|
|
54
|
+
'Bitter SemiBold',
|
|
55
|
+
'Bitter Bold',
|
|
56
|
+
'Bitter Italic',
|
|
57
|
+
'Bitter Medium Italic',
|
|
58
|
+
'Bitter SemiBold Italic',
|
|
59
|
+
'Bitter Bold Italic',
|
|
60
|
+
'DM Sans',
|
|
61
|
+
'DM Sans Medium',
|
|
62
|
+
'DM Sans SemiBold',
|
|
63
|
+
'DM Sans Bold',
|
|
64
|
+
'DM Sans Italic',
|
|
65
|
+
'DM Sans Medium Italic',
|
|
66
|
+
'DM Sans SemiBold Italic',
|
|
67
|
+
'DM Sans Bold Italic',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const PRESTIGE_DISPLAY_FONT_KEYS = [
|
|
71
|
+
'Libre Caslon Text',
|
|
72
|
+
'Libre Caslon Text Medium',
|
|
73
|
+
'Libre Caslon Text SemiBold',
|
|
74
|
+
'Libre Caslon Text Bold',
|
|
75
|
+
'Libre Caslon Text Italic',
|
|
76
|
+
'Libre Caslon Text Medium Italic',
|
|
77
|
+
'Libre Caslon Text SemiBold Italic',
|
|
78
|
+
'Libre Caslon Text Bold Italic',
|
|
79
|
+
'Cormorant',
|
|
80
|
+
'Cormorant Medium',
|
|
81
|
+
'Cormorant SemiBold',
|
|
82
|
+
'Cormorant Bold',
|
|
83
|
+
'Cormorant Italic',
|
|
84
|
+
'Cormorant Medium Italic',
|
|
85
|
+
'Cormorant SemiBold Italic',
|
|
86
|
+
'Cormorant Bold Italic',
|
|
87
|
+
'Libre Franklin',
|
|
88
|
+
'Libre Franklin Medium',
|
|
89
|
+
'Libre Franklin SemiBold',
|
|
90
|
+
'Libre Franklin Bold',
|
|
91
|
+
'Libre Franklin Italic',
|
|
92
|
+
'Libre Franklin Medium Italic',
|
|
93
|
+
'Libre Franklin SemiBold Italic',
|
|
94
|
+
'Libre Franklin Bold Italic',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const PULSE_DISPLAY_FONT_KEYS = [
|
|
98
|
+
'Outfit',
|
|
99
|
+
'Outfit Medium',
|
|
100
|
+
'Outfit SemiBold',
|
|
101
|
+
'Outfit Bold',
|
|
102
|
+
'Outfit Italic',
|
|
103
|
+
'Outfit Medium Italic',
|
|
104
|
+
'Outfit SemiBold Italic',
|
|
105
|
+
'Outfit Bold Italic',
|
|
106
|
+
'Manrope',
|
|
107
|
+
'Manrope Medium',
|
|
108
|
+
'Manrope SemiBold',
|
|
109
|
+
'Manrope Bold',
|
|
110
|
+
'Manrope Italic',
|
|
111
|
+
'Manrope Medium Italic',
|
|
112
|
+
'Manrope SemiBold Italic',
|
|
113
|
+
'Manrope Bold Italic',
|
|
114
|
+
'Public Sans',
|
|
115
|
+
'Public Sans Medium',
|
|
116
|
+
'Public Sans SemiBold',
|
|
117
|
+
'Public Sans Bold',
|
|
118
|
+
'Public Sans Italic',
|
|
119
|
+
'Public Sans Medium Italic',
|
|
120
|
+
'Public Sans SemiBold Italic',
|
|
121
|
+
'Public Sans Bold Italic',
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const ALL_DISPLAY_FONT_KEYS = [
|
|
125
|
+
...POISE_DISPLAY_FONT_KEYS,
|
|
126
|
+
...PRESTIGE_DISPLAY_FONT_KEYS,
|
|
127
|
+
...PULSE_DISPLAY_FONT_KEYS,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const WEB_ONLY_FONT_KEYS = [
|
|
131
|
+
'Inter',
|
|
132
|
+
'Inter Medium',
|
|
133
|
+
'Inter SemiBold',
|
|
134
|
+
'Inter Bold',
|
|
135
|
+
'Inter Italic',
|
|
136
|
+
'Inter Medium Italic',
|
|
137
|
+
'Inter SemiBold Italic',
|
|
138
|
+
'Inter Bold Italic',
|
|
139
|
+
'IBM Plex Mono',
|
|
140
|
+
'IBM Plex Mono Medium',
|
|
141
|
+
'IBM Plex Mono SemiBold',
|
|
142
|
+
'IBM Plex Mono Bold',
|
|
143
|
+
'IBM Plex Mono Italic',
|
|
144
|
+
'IBM Plex Mono Medium Italic',
|
|
145
|
+
'IBM Plex Mono SemiBold Italic',
|
|
146
|
+
'IBM Plex Mono Bold Italic',
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Tests
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
describe('useForgeFonts', () => {
|
|
154
|
+
describe('return shape', () => {
|
|
155
|
+
it('returns { loaded, error } in the loaded state', () => {
|
|
156
|
+
const { hookResult } = setupAndCall({ platformOS: 'ios', result: [true, null] });
|
|
157
|
+
expect(hookResult).toEqual({ loaded: true, error: null });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns { loaded: false, error: null } while still loading', () => {
|
|
161
|
+
const { hookResult } = setupAndCall({ platformOS: 'ios', result: [false, null] });
|
|
162
|
+
expect(hookResult).toEqual({ loaded: false, error: null });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('propagates the error from useFonts', () => {
|
|
166
|
+
const err = new Error('boom');
|
|
167
|
+
const { hookResult } = setupAndCall({ platformOS: 'ios', result: [false, err] });
|
|
168
|
+
expect(hookResult.loaded).toBe(false);
|
|
169
|
+
expect(hookResult.error).toBe(err);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('font map (display fonts)', () => {
|
|
174
|
+
it.each(['ios', 'android', 'web'] as const)(
|
|
175
|
+
'passes every display font (Poise + Prestige + Pulse) when Platform.OS is %s',
|
|
176
|
+
(platformOS) => {
|
|
177
|
+
const { capturedMap } = setupAndCall({ platformOS });
|
|
178
|
+
for (const key of ALL_DISPLAY_FONT_KEYS) {
|
|
179
|
+
expect(capturedMap).toHaveProperty(key);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
it('passes 72 display font keys (24 per preset × 3 presets)', () => {
|
|
185
|
+
const { capturedMap } = setupAndCall({ platformOS: 'ios' });
|
|
186
|
+
const displayKeys = Object.keys(capturedMap).filter((k) => !WEB_ONLY_FONT_KEYS.includes(k));
|
|
187
|
+
expect(displayKeys).toHaveLength(ALL_DISPLAY_FONT_KEYS.length);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('web-only fonts', () => {
|
|
192
|
+
it('excludes Inter and IBM Plex Mono on ios', () => {
|
|
193
|
+
const { capturedMap } = setupAndCall({ platformOS: 'ios' });
|
|
194
|
+
for (const key of WEB_ONLY_FONT_KEYS) {
|
|
195
|
+
expect(capturedMap).not.toHaveProperty(key);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('excludes Inter and IBM Plex Mono on android', () => {
|
|
200
|
+
const { capturedMap } = setupAndCall({ platformOS: 'android' });
|
|
201
|
+
for (const key of WEB_ONLY_FONT_KEYS) {
|
|
202
|
+
expect(capturedMap).not.toHaveProperty(key);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('includes every Inter and IBM Plex Mono weight/italic on web', () => {
|
|
207
|
+
const { capturedMap } = setupAndCall({ platformOS: 'web' });
|
|
208
|
+
for (const key of WEB_ONLY_FONT_KEYS) {
|
|
209
|
+
expect(capturedMap).toHaveProperty(key);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('still passes display fonts alongside web fonts when on web', () => {
|
|
214
|
+
const { capturedMap } = setupAndCall({ platformOS: 'web' });
|
|
215
|
+
const totalKeys = Object.keys(capturedMap);
|
|
216
|
+
expect(totalKeys).toHaveLength(ALL_DISPLAY_FONT_KEYS.length + WEB_ONLY_FONT_KEYS.length);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('useFonts invocation', () => {
|
|
221
|
+
it('calls useFonts exactly once per hook call', () => {
|
|
222
|
+
const { mockUseFonts } = setupAndCall({ platformOS: 'ios' });
|
|
223
|
+
expect(mockUseFonts).toHaveBeenCalledTimes(1);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import prestigePatch from '../runtime/prestige-vs-default.json';
|
|
2
|
+
import pulsePatch from '../runtime/pulse-vs-default.json';
|
|
3
|
+
import defaultRuntimeMap from '../runtime/token-to-css-var.json';
|
|
4
|
+
import type { Platform, Preset, ThemeOverride, TokenGroup } from './types';
|
|
5
|
+
import {
|
|
6
|
+
isSchemaVersionSupported,
|
|
7
|
+
presetPatchToUniwindMaps,
|
|
8
|
+
themeOverrideToUniwindMaps,
|
|
9
|
+
type CssVariableMaps,
|
|
10
|
+
type RuntimeMap,
|
|
11
|
+
} from './theming';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Constants
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** The build-time default preset. Apps ship with Poise baked into CSS. */
|
|
18
|
+
export const DEFAULT_PRESET: Preset = 'poise';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Preset patch lookup
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const PRESET_PATCHES: Record<Preset, TokenGroup | null> = {
|
|
25
|
+
poise: null,
|
|
26
|
+
prestige: prestigePatch,
|
|
27
|
+
pulse: pulsePatch,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Platform detection
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function detectPlatform(): Platform {
|
|
35
|
+
try {
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const { Platform: RNPlatform } = require('react-native');
|
|
38
|
+
const os = RNPlatform.OS as string;
|
|
39
|
+
if (os === 'ios' || os === 'android') return os;
|
|
40
|
+
return 'web';
|
|
41
|
+
} catch {
|
|
42
|
+
return 'web';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Result type
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export type ApplyThemeOverrideResult =
|
|
51
|
+
| { applied: true; light: Record<string, string>; dark: Record<string, string> }
|
|
52
|
+
| { applied: false; reason: 'unsupported_schema_version' | 'no_theme_changes' };
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Options
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
export interface ApplyThemeOverrideOptions {
|
|
59
|
+
runtimeMap?: RuntimeMap;
|
|
60
|
+
runtimePlatform?: Platform;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Orchestrator
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply a `ThemeOverride` end-to-end: schema version gate, preset patch
|
|
69
|
+
* selection, palette generation, and `Uniwind.updateCSSVariables` calls.
|
|
70
|
+
*
|
|
71
|
+
* Encapsulates the full runtime theme application sequence so consuming
|
|
72
|
+
* apps don't need to orchestrate lower-level utilities or import Uniwind
|
|
73
|
+
* directly.
|
|
74
|
+
*/
|
|
75
|
+
export function applyThemeOverride(
|
|
76
|
+
override: ThemeOverride,
|
|
77
|
+
options: ApplyThemeOverrideOptions = {},
|
|
78
|
+
): ApplyThemeOverrideResult {
|
|
79
|
+
const { runtimeMap = defaultRuntimeMap, runtimePlatform = detectPlatform() } = options;
|
|
80
|
+
|
|
81
|
+
// --- Schema version gate ---
|
|
82
|
+
if (!isSchemaVersionSupported(override)) {
|
|
83
|
+
return { applied: false, reason: 'unsupported_schema_version' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const basePreset = override.$extensions['com.forge.ui.themeOverride'].basePreset;
|
|
87
|
+
const hasInputs = override.inputs !== undefined && Object.keys(override.inputs).length > 0;
|
|
88
|
+
const hasOverrides =
|
|
89
|
+
override.overrides !== undefined && Object.keys(override.overrides).length > 0;
|
|
90
|
+
|
|
91
|
+
// --- Metadata-only check ---
|
|
92
|
+
const isDefaultPreset = basePreset === DEFAULT_PRESET;
|
|
93
|
+
if (isDefaultPreset && !hasInputs && !hasOverrides) {
|
|
94
|
+
return { applied: false, reason: 'no_theme_changes' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Preset patch step ---
|
|
98
|
+
let presetMaps: CssVariableMaps = { light: {}, dark: {} };
|
|
99
|
+
if (!isDefaultPreset) {
|
|
100
|
+
const patch = PRESET_PATCHES[basePreset];
|
|
101
|
+
if (patch) {
|
|
102
|
+
presetMaps = presetPatchToUniwindMaps(patch, runtimeMap, runtimePlatform);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- FI override step ---
|
|
107
|
+
let overrideMaps: CssVariableMaps = { light: {}, dark: {} };
|
|
108
|
+
if (hasInputs || hasOverrides) {
|
|
109
|
+
overrideMaps = themeOverrideToUniwindMaps(override, runtimeMap);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Merge per mode (FI wins on collision) ---
|
|
113
|
+
const mergedLight = { ...presetMaps.light, ...overrideMaps.light };
|
|
114
|
+
const mergedDark = { ...presetMaps.dark, ...overrideMaps.dark };
|
|
115
|
+
|
|
116
|
+
// --- Non-default preset with no FI changes still needs patch application ---
|
|
117
|
+
if (Object.keys(mergedLight).length === 0 && Object.keys(mergedDark).length === 0) {
|
|
118
|
+
return { applied: false, reason: 'no_theme_changes' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Apply via Uniwind ---
|
|
122
|
+
try {
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
124
|
+
const { Uniwind } = require('uniwind');
|
|
125
|
+
if (Object.keys(mergedLight).length > 0) {
|
|
126
|
+
Uniwind.updateCSSVariables('light', mergedLight);
|
|
127
|
+
}
|
|
128
|
+
if (Object.keys(mergedDark).length > 0) {
|
|
129
|
+
Uniwind.updateCSSVariables('dark', mergedDark);
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'applyThemeOverride requires "uniwind" to be installed. ' +
|
|
134
|
+
'Add it as a dependency in your app.',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { applied: true, light: mergedLight, dark: mergedDark };
|
|
139
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,73 +1,49 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Types
|
|
2
|
+
// Types & Constants
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
5
|
+
export {
|
|
6
|
+
OVERRIDE_SCHEMA_VERSION,
|
|
7
|
+
SUPPORTED_OVERRIDE_SCHEMA_VERSIONS,
|
|
8
|
+
INPUT_TOKEN_MAP,
|
|
9
|
+
presetFonts,
|
|
10
|
+
} from './types';
|
|
11
|
+
export type {
|
|
12
|
+
TokenValue,
|
|
13
|
+
TokenGroup,
|
|
14
|
+
Preset,
|
|
15
|
+
ThemeMetadata,
|
|
16
|
+
ThemeOverrideMetadata,
|
|
17
|
+
ThemeObject,
|
|
18
|
+
ThemeOverride,
|
|
19
|
+
Mode,
|
|
20
|
+
Platform,
|
|
21
|
+
} from './types';
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Theme metadata stored under `$extensions.com.candescent.theme`. */
|
|
18
|
-
export interface ThemeMetadata {
|
|
19
|
-
name: string;
|
|
20
|
-
preset: string;
|
|
21
|
-
schemaVersion: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Override metadata stored under `$extensions.com.candescent.themeOverride`. */
|
|
25
|
-
export interface ThemeOverrideMetadata {
|
|
26
|
-
basePreset: string;
|
|
27
|
-
fiId: string;
|
|
28
|
-
fiName: string;
|
|
29
|
-
schemaVersion: string;
|
|
30
|
-
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Palette generation
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
* Presets (Poise, Prestige, Pulse) are full theme objects. At runtime the
|
|
36
|
-
* build-time default preset is augmented by FI overrides via
|
|
37
|
-
* `applyThemeOverrides`.
|
|
38
|
-
*/
|
|
39
|
-
export type ThemeObject = TokenGroup & {
|
|
40
|
-
$extensions: {
|
|
41
|
-
'com.candescent.theme': ThemeMetadata;
|
|
42
|
-
[key: string]: unknown;
|
|
43
|
-
};
|
|
44
|
-
modes: {
|
|
45
|
-
light: TokenGroup;
|
|
46
|
-
dark: TokenGroup;
|
|
47
|
-
};
|
|
48
|
-
platform: {
|
|
49
|
-
web: TokenGroup;
|
|
50
|
-
ios: TokenGroup;
|
|
51
|
-
android: TokenGroup;
|
|
52
|
-
};
|
|
53
|
-
};
|
|
27
|
+
export { generateColorScale, generatePalettesFromInputs, deriveBaseColorKey } from './palette';
|
|
28
|
+
export type { PaletteCategory, PaletteScale, PaletteStep, PaletteTokenMap } from './palette';
|
|
54
29
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
export type ThemeOverride = TokenGroup & {
|
|
60
|
-
$extensions?: {
|
|
61
|
-
'com.candescent.themeOverride'?: ThemeOverrideMetadata;
|
|
62
|
-
[key: string]: unknown;
|
|
63
|
-
};
|
|
64
|
-
};
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Theming utilities
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
65
33
|
|
|
66
|
-
export
|
|
67
|
-
|
|
34
|
+
export {
|
|
35
|
+
isSchemaVersionSupported,
|
|
36
|
+
presetPatchToUniwindMaps,
|
|
37
|
+
themeOverrideToUniwindMaps,
|
|
38
|
+
applyThemeOverride,
|
|
39
|
+
DEFAULT_PRESET,
|
|
40
|
+
} from './theming';
|
|
41
|
+
export type { CssVariableMaps, RuntimeMap } from './theming';
|
|
42
|
+
export type { ApplyThemeOverrideOptions, ApplyThemeOverrideResult } from './applyThemeOverride';
|
|
68
43
|
|
|
69
44
|
// ---------------------------------------------------------------------------
|
|
70
45
|
// Hooks
|
|
71
46
|
// ---------------------------------------------------------------------------
|
|
72
47
|
|
|
48
|
+
export { useForgeFonts } from './useForgeFonts';
|
|
73
49
|
export { useCdxFonts } from './useCdxFonts';
|