react-native-terra-ui 0.10.2 → 0.11.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/CHANGELOG.md +22 -0
- package/README.md +8 -4
- package/lib/module/context/ThemeProvider.js +5 -5
- package/lib/module/context/ThemeProvider.js.map +1 -1
- package/lib/module/theme/define-config.js +5 -5
- package/lib/module/theme/define-config.js.map +1 -1
- package/lib/module/theme/errors.js +1 -1
- package/lib/module/theme/index.js +2 -2
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/registry.js +15 -21
- package/lib/module/theme/registry.js.map +1 -1
- package/lib/module/theme/resolve-config.js +68 -68
- package/lib/module/theme/resolve-config.js.map +1 -1
- package/lib/module/theme/runtime.js +11 -11
- package/lib/module/theme/runtime.js.map +1 -1
- package/lib/module/theme/selection-store.js +7 -7
- package/lib/module/theme/selection-store.js.map +1 -1
- package/lib/module/theme/unistyles-adapter.js +1 -1
- package/lib/module/utils/accent-utils.js +24 -24
- package/lib/module/utils/accent-utils.js.map +1 -1
- package/lib/typescript/src/context/ThemeProvider.d.ts +7 -7
- package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
- package/lib/typescript/src/theme/define-config.d.ts +11 -8
- package/lib/typescript/src/theme/define-config.d.ts.map +1 -1
- package/lib/typescript/src/theme/errors.d.ts +1 -1
- package/lib/typescript/src/theme/index.d.ts +3 -3
- package/lib/typescript/src/theme/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/registry.d.ts +5 -5
- package/lib/typescript/src/theme/registry.d.ts.map +1 -1
- package/lib/typescript/src/theme/resolve-config.d.ts +25 -25
- package/lib/typescript/src/theme/resolve-config.d.ts.map +1 -1
- package/lib/typescript/src/theme/runtime.d.ts +5 -5
- package/lib/typescript/src/theme/runtime.d.ts.map +1 -1
- package/lib/typescript/src/theme/selection-store.d.ts +5 -5
- package/lib/typescript/src/theme/selection-store.d.ts.map +1 -1
- package/lib/typescript/src/theme/types.d.ts +20 -47
- package/lib/typescript/src/theme/types.d.ts.map +1 -1
- package/lib/typescript/src/theme/unistyles-adapter.d.ts +1 -1
- package/lib/typescript/src/utils/accent-utils.d.ts +6 -7
- package/lib/typescript/src/utils/accent-utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/context/ThemeProvider.tsx +10 -10
- package/src/theme/define-config.ts +12 -7
- package/src/theme/errors.ts +1 -1
- package/src/theme/index.ts +6 -8
- package/src/theme/registry.ts +15 -23
- package/src/theme/resolve-config.ts +88 -95
- package/src/theme/runtime.ts +11 -11
- package/src/theme/selection-store.ts +9 -9
- package/src/theme/types.ts +21 -47
- package/src/theme/unistyles-adapter.ts +1 -1
- package/src/utils/accent-utils.ts +25 -25
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure theme resolution — normalization, layer processing, derived values and
|
|
3
|
-
* theme creation. Shared by startup configuration, runtime
|
|
3
|
+
* theme creation. Shared by startup configuration, runtime theme changes and
|
|
4
4
|
* previews so every consumer sees the same colors for the same input.
|
|
5
5
|
*
|
|
6
6
|
* Precedence, identical for both schemes:
|
|
7
7
|
*
|
|
8
|
-
* default tokens → shared →
|
|
9
|
-
*
|
|
8
|
+
* default tokens → shared → generated accent → selected theme tokens
|
|
9
|
+
* Generation uses the selected theme's resolved surfaces and pressed opacity.
|
|
10
10
|
*
|
|
11
11
|
* This module must stay free of registry, provider, Unistyles and renderer
|
|
12
12
|
* imports so `createTerraThemes` works without any native configuration.
|
|
@@ -33,15 +33,14 @@ import {
|
|
|
33
33
|
type ThemeSource,
|
|
34
34
|
} from "./theme";
|
|
35
35
|
import type {
|
|
36
|
-
Accent,
|
|
37
|
-
AccentSchemeInput,
|
|
38
36
|
CreateTerraThemesOptions,
|
|
39
37
|
ElevationKey,
|
|
38
|
+
NamedTheme,
|
|
40
39
|
Scheme,
|
|
41
40
|
TerraTheme,
|
|
42
41
|
TerraThemeInput,
|
|
43
42
|
TerraThemeOverride,
|
|
44
|
-
|
|
43
|
+
ThemeSchemeInput,
|
|
45
44
|
} from "./types";
|
|
46
45
|
|
|
47
46
|
const SCHEMES: readonly Scheme[] = ["light", "dark"];
|
|
@@ -87,8 +86,8 @@ function sanitizeOverride<T>(value: T): T {
|
|
|
87
86
|
|
|
88
87
|
/**
|
|
89
88
|
* Applies one override layer onto `state` and returns the next state. Every
|
|
90
|
-
* layer — shared,
|
|
91
|
-
*
|
|
89
|
+
* layer — shared, generated accent, or selected theme tokens — goes through
|
|
90
|
+
* here, so they all follow the same rules:
|
|
92
91
|
* - deprecated names are normalized within the layer (canonical wins);
|
|
93
92
|
* - `undefined`/`null` leaves are no override; arrays replace; objects merge;
|
|
94
93
|
* - `spacing.base` / `radius.base` regenerate their scale, then that layer's
|
|
@@ -140,34 +139,34 @@ export function applyThemeLayer(state: ThemeLayerState, override?: TerraThemeOve
|
|
|
140
139
|
|
|
141
140
|
// ─── Normalized configuration ────────────────────────────────────────────────
|
|
142
141
|
|
|
143
|
-
export type
|
|
142
|
+
export type NormalizedThemeScheme =
|
|
144
143
|
| { readonly kind: "none" }
|
|
145
|
-
| { readonly kind: "seed"; readonly seed: string; readonly
|
|
144
|
+
| { readonly kind: "seed"; readonly seed: string; readonly patch?: TerraThemeOverride }
|
|
146
145
|
| { readonly kind: "patch"; readonly patch: TerraThemeOverride };
|
|
147
146
|
|
|
148
|
-
interface
|
|
149
|
-
readonly light:
|
|
150
|
-
readonly dark:
|
|
147
|
+
interface NormalizedThemeEntry {
|
|
148
|
+
readonly light: NormalizedThemeScheme;
|
|
149
|
+
readonly dark: NormalizedThemeScheme;
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
/**
|
|
154
153
|
* A validated, immutable theme configuration. `base` is the fully layered
|
|
155
|
-
* `defaults → shared
|
|
156
|
-
* re-resolved from it rather than from a previously resolved
|
|
154
|
+
* `defaults → shared` state per scheme; every theme selection is
|
|
155
|
+
* re-resolved from it rather than from a previously resolved theme.
|
|
157
156
|
*/
|
|
158
157
|
export interface NormalizedThemeConfig {
|
|
159
|
-
readonly
|
|
160
|
-
readonly
|
|
161
|
-
/** @internal Immutable `defaults → shared
|
|
158
|
+
readonly themeNames: readonly string[];
|
|
159
|
+
readonly defaultTheme: string | null;
|
|
160
|
+
/** @internal Immutable `defaults → shared` state per scheme. */
|
|
162
161
|
readonly base: Readonly<Record<Scheme, ThemeLayerState>>;
|
|
163
|
-
/** @internal Per-
|
|
164
|
-
readonly
|
|
162
|
+
/** @internal Per-theme, per-scheme normalized inputs. */
|
|
163
|
+
readonly themes: Readonly<Record<string, NormalizedThemeEntry>>;
|
|
165
164
|
}
|
|
166
165
|
|
|
167
|
-
export function
|
|
168
|
-
input:
|
|
166
|
+
export function normalizeThemeScheme(
|
|
167
|
+
input: ThemeSchemeInput | undefined,
|
|
169
168
|
path: string
|
|
170
|
-
):
|
|
169
|
+
): NormalizedThemeScheme {
|
|
171
170
|
if (input === undefined || input === null) return { kind: "none" };
|
|
172
171
|
|
|
173
172
|
if (typeof input === "string") {
|
|
@@ -183,52 +182,46 @@ export function normalizeAccentScheme(
|
|
|
183
182
|
if (!isPlainObject(input)) {
|
|
184
183
|
throw new TerraConfigError(
|
|
185
184
|
path,
|
|
186
|
-
"Expected a color string, `{ seed,
|
|
185
|
+
"Expected a color string, `{ seed, color }`, or a theme override object."
|
|
187
186
|
);
|
|
188
187
|
}
|
|
189
188
|
|
|
190
|
-
if (
|
|
191
|
-
|
|
189
|
+
if ("overrides" in input) {
|
|
190
|
+
throw new TerraConfigError(`${path}.overrides`, "Use `color` for explicit colors alongside `seed`.");
|
|
192
191
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
path,
|
|
198
|
-
`A seeded accent accepts only \`seed\` and \`overrides\`; found ${extra
|
|
199
|
-
.map((key) => `\`${key}\``)
|
|
200
|
-
.join(
|
|
201
|
-
", "
|
|
202
|
-
)}. Use \`{ seed, overrides: { ... } }\` for color exceptions, or drop \`seed\` for a full theme override.`
|
|
203
|
-
);
|
|
192
|
+
const { seed, ...patch } = input as TerraThemeOverride & { seed?: unknown };
|
|
193
|
+
assertOverrideObject(patch.color, `${path}.color`);
|
|
194
|
+
if (!("seed" in input)) {
|
|
195
|
+
return { kind: "patch", patch: sanitizeOverride(patch) };
|
|
204
196
|
}
|
|
205
|
-
|
|
206
|
-
const { seed, overrides } = input as { seed: unknown; overrides?: unknown };
|
|
207
197
|
if (!isSupportedAccentSeed(seed)) {
|
|
208
198
|
throw new TerraConfigError(
|
|
209
199
|
`${path}.seed`,
|
|
210
200
|
`Unsupported accent seed ${JSON.stringify(seed)}; expected an opaque six-digit hex color (#rrggbb).`
|
|
211
201
|
);
|
|
212
202
|
}
|
|
213
|
-
|
|
214
|
-
throw new TerraConfigError(`${path}.overrides`, "Expected a flat semantic color patch object.");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
kind: "seed",
|
|
219
|
-
seed: seed.trim(),
|
|
220
|
-
...(overrides ? { overrides: sanitizeOverride(overrides as Partial<ThemeColor>) } : {}),
|
|
221
|
-
};
|
|
203
|
+
return { kind: "seed", seed: seed.trim(), patch: sanitizeOverride(patch) };
|
|
222
204
|
}
|
|
223
205
|
|
|
224
|
-
function
|
|
225
|
-
const path = `
|
|
226
|
-
if (!isPlainObject(
|
|
206
|
+
function normalizeThemeEntry(name: string, theme: NamedTheme): NormalizedThemeEntry {
|
|
207
|
+
const path = `themes.${name}`;
|
|
208
|
+
if (!isPlainObject(theme)) {
|
|
227
209
|
throw new TerraConfigError(path, "Expected an object with `light` and/or `dark` scheme inputs.");
|
|
228
210
|
}
|
|
211
|
+
// An empty entry is a valid theme that adds nothing, but tokens sitting
|
|
212
|
+
// outside a scheme — `{ seed, color }` lifted straight out of the old
|
|
213
|
+
// `accents` shape — would otherwise be accepted and silently render defaults.
|
|
214
|
+
const keys = Object.keys(theme);
|
|
215
|
+
if (keys.length > 0 && !("light" in theme) && !("dark" in theme)) {
|
|
216
|
+
throw new TerraConfigError(
|
|
217
|
+
path,
|
|
218
|
+
`Unexpected ${keys.map((key) => `\`${key}\``).join(", ")} outside a scheme; ` +
|
|
219
|
+
"put `seed` and token overrides under `light` and/or `dark`."
|
|
220
|
+
);
|
|
221
|
+
}
|
|
229
222
|
return {
|
|
230
|
-
light:
|
|
231
|
-
dark:
|
|
223
|
+
light: normalizeThemeScheme(theme.light, `${path}.light`),
|
|
224
|
+
dark: normalizeThemeScheme(theme.dark, `${path}.dark`),
|
|
232
225
|
};
|
|
233
226
|
}
|
|
234
227
|
|
|
@@ -240,7 +233,7 @@ function assertOverrideObject(value: unknown, path: string): void {
|
|
|
240
233
|
|
|
241
234
|
/**
|
|
242
235
|
* Validates a theme input and pre-computes the immutable per-scheme base
|
|
243
|
-
* (`defaults → shared
|
|
236
|
+
* (`defaults → shared`). Throws {@link TerraConfigError} for invalid
|
|
244
237
|
* input — before any state exists to mutate. Never mutates `input` or the
|
|
245
238
|
* exported defaults.
|
|
246
239
|
*/
|
|
@@ -250,41 +243,42 @@ export function normalizeThemeInput(input: TerraThemeInput = {}): NormalizedThem
|
|
|
250
243
|
}
|
|
251
244
|
|
|
252
245
|
assertOverrideObject(input.shared, "shared");
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
246
|
+
for (const removed of ["schemes", "accents", "defaultAccent"]) {
|
|
247
|
+
if (removed in input) {
|
|
248
|
+
throw new TerraConfigError(removed, "Use `themes.<name>.light/dark` and `defaultTheme` instead.");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (input.themes !== undefined && !isPlainObject(input.themes)) {
|
|
252
|
+
throw new TerraConfigError("themes", "Expected a record of named themes.");
|
|
258
253
|
}
|
|
259
254
|
|
|
260
|
-
const
|
|
261
|
-
for (const [name,
|
|
262
|
-
|
|
255
|
+
const themes: Record<string, NormalizedThemeEntry> = Object.create(null);
|
|
256
|
+
for (const [name, theme] of Object.entries(input.themes ?? {})) {
|
|
257
|
+
themes[name] = normalizeThemeEntry(name, theme);
|
|
263
258
|
}
|
|
264
|
-
const
|
|
259
|
+
const themeNames = Object.keys(themes);
|
|
265
260
|
|
|
266
|
-
let
|
|
267
|
-
if (input.
|
|
268
|
-
if (typeof input.
|
|
261
|
+
let defaultTheme: string | null = null;
|
|
262
|
+
if (input.defaultTheme !== undefined && input.defaultTheme !== null) {
|
|
263
|
+
if (typeof input.defaultTheme !== "string" || !Object.hasOwn(themes, input.defaultTheme)) {
|
|
269
264
|
throw new TerraConfigError(
|
|
270
|
-
"
|
|
271
|
-
`Unknown
|
|
272
|
-
|
|
265
|
+
"defaultTheme",
|
|
266
|
+
`Unknown theme ${JSON.stringify(input.defaultTheme)}; declared themes: ${
|
|
267
|
+
themeNames.length > 0 ? themeNames.map((n) => `"${n}"`).join(", ") : "(none)"
|
|
273
268
|
}.`
|
|
274
269
|
);
|
|
275
270
|
}
|
|
276
|
-
|
|
271
|
+
defaultTheme = input.defaultTheme;
|
|
277
272
|
}
|
|
278
273
|
|
|
279
274
|
const base = {} as Record<Scheme, ThemeLayerState>;
|
|
280
275
|
for (const scheme of SCHEMES) {
|
|
281
276
|
let state: ThemeLayerState = { source: createDefaultThemeSource(scheme), pinnedShadowColors: {} };
|
|
282
277
|
state = applyThemeLayer(state, input.shared);
|
|
283
|
-
state = applyThemeLayer(state, input.schemes?.[scheme]);
|
|
284
278
|
base[scheme] = state;
|
|
285
279
|
}
|
|
286
280
|
|
|
287
|
-
return {
|
|
281
|
+
return { themeNames, defaultTheme, base, themes };
|
|
288
282
|
}
|
|
289
283
|
|
|
290
284
|
// ─── Resolution ──────────────────────────────────────────────────────────────
|
|
@@ -295,19 +289,19 @@ export interface ResolvedThemes {
|
|
|
295
289
|
diagnostics: ContrastDiagnostic[];
|
|
296
290
|
}
|
|
297
291
|
|
|
298
|
-
function
|
|
292
|
+
function getThemeEntry(
|
|
299
293
|
config: NormalizedThemeConfig,
|
|
300
|
-
|
|
301
|
-
):
|
|
302
|
-
if (
|
|
294
|
+
themeName: string | null
|
|
295
|
+
): NormalizedThemeEntry | undefined {
|
|
296
|
+
if (themeName === null) return undefined;
|
|
303
297
|
|
|
304
|
-
const entry = config.
|
|
305
|
-
if (
|
|
298
|
+
const entry = config.themes[themeName];
|
|
299
|
+
if (Object.hasOwn(config.themes, themeName)) return entry;
|
|
306
300
|
|
|
307
301
|
throw new TerraConfigError(
|
|
308
|
-
"
|
|
309
|
-
`Unknown
|
|
310
|
-
config.
|
|
302
|
+
"theme",
|
|
303
|
+
`Unknown theme ${JSON.stringify(themeName)}; declared themes: ${
|
|
304
|
+
config.themeNames.length > 0 ? config.themeNames.map((name) => `"${name}"`).join(", ") : "(none)"
|
|
311
305
|
}.`
|
|
312
306
|
);
|
|
313
307
|
}
|
|
@@ -331,7 +325,7 @@ function pickGeneratedRoles(color: ThemeSource["color"]): Record<GeneratedAccent
|
|
|
331
325
|
function resolveScheme(
|
|
332
326
|
scheme: Scheme,
|
|
333
327
|
base: ThemeLayerState,
|
|
334
|
-
accent:
|
|
328
|
+
accent: NormalizedThemeScheme
|
|
335
329
|
): { theme: TerraTheme; diagnostics: ContrastDiagnostic[] } {
|
|
336
330
|
let state = base;
|
|
337
331
|
let diagnostics: ContrastDiagnostic[] = [];
|
|
@@ -339,12 +333,11 @@ function resolveScheme(
|
|
|
339
333
|
if (accent.kind === "patch") {
|
|
340
334
|
state = applyThemeLayer(state, accent.patch);
|
|
341
335
|
} else if (accent.kind === "seed") {
|
|
342
|
-
|
|
336
|
+
// Generate against the final ordinary surfaces, then let explicit tokens win.
|
|
337
|
+
const context = accentContext(scheme, applyThemeLayer(base, accent.patch).source);
|
|
343
338
|
const generated = generateAccentColors(accent.seed, context);
|
|
344
339
|
state = applyThemeLayer(state, { color: generated.colors } as TerraThemeOverride);
|
|
345
|
-
|
|
346
|
-
state = applyThemeLayer(state, { color: accent.overrides });
|
|
347
|
-
}
|
|
340
|
+
state = applyThemeLayer(state, accent.patch);
|
|
348
341
|
// The post-override check is authoritative: it evaluates the colours that
|
|
349
342
|
// actually ship, so an override that fixes a pair leaves no stale report.
|
|
350
343
|
diagnostics = checkAccentContrast(
|
|
@@ -357,12 +350,12 @@ function resolveScheme(
|
|
|
357
350
|
}
|
|
358
351
|
|
|
359
352
|
/**
|
|
360
|
-
* Resolves both schemes for `
|
|
353
|
+
* Resolves both schemes for `themeName` from the config's immutable base. `null`
|
|
361
354
|
* selects the base themes; an unknown name throws {@link TerraConfigError}.
|
|
362
355
|
* Returned themes are freshly built and independently owned.
|
|
363
356
|
*/
|
|
364
|
-
export function resolveThemes(config: NormalizedThemeConfig,
|
|
365
|
-
const entry =
|
|
357
|
+
export function resolveThemes(config: NormalizedThemeConfig, themeName: string | null): ResolvedThemes {
|
|
358
|
+
const entry = getThemeEntry(config, themeName);
|
|
366
359
|
|
|
367
360
|
const themes = {} as Record<Scheme, TerraTheme>;
|
|
368
361
|
const diagnostics: ContrastDiagnostic[][] = [];
|
|
@@ -379,16 +372,16 @@ export function resolveThemes(config: NormalizedThemeConfig, accent: string | nu
|
|
|
379
372
|
export function resolveThemeScheme(
|
|
380
373
|
config: NormalizedThemeConfig,
|
|
381
374
|
scheme: Scheme,
|
|
382
|
-
|
|
375
|
+
themeName: string | null
|
|
383
376
|
): { theme: TerraTheme; diagnostics: ContrastDiagnostic[] } {
|
|
384
|
-
const entry =
|
|
377
|
+
const entry = getThemeEntry(config, themeName);
|
|
385
378
|
return resolveScheme(scheme, config.base[scheme], entry?.[scheme] ?? { kind: "none" });
|
|
386
379
|
}
|
|
387
380
|
|
|
388
381
|
/**
|
|
389
382
|
* Pure theme creation — no Unistyles configuration or registry involved.
|
|
390
|
-
* `options.
|
|
391
|
-
* themes. Invalid input and unknown
|
|
383
|
+
* `options.theme` omitted → the input's `defaultTheme`; `null` → the base
|
|
384
|
+
* themes. Invalid input and unknown themes throw {@link TerraConfigError}.
|
|
392
385
|
* In development, contrast diagnostics are reported once each via
|
|
393
386
|
* `console.warn`; requested colors are never altered.
|
|
394
387
|
*/
|
|
@@ -397,8 +390,8 @@ export function createTerraThemes(
|
|
|
397
390
|
options?: CreateTerraThemesOptions
|
|
398
391
|
): Record<Scheme, TerraTheme> {
|
|
399
392
|
const config = normalizeThemeInput(input);
|
|
400
|
-
const
|
|
401
|
-
const { themes, diagnostics } = resolveThemes(config,
|
|
393
|
+
const themeName = options?.theme === undefined ? config.defaultTheme : options.theme;
|
|
394
|
+
const { themes, diagnostics } = resolveThemes(config, themeName);
|
|
402
395
|
|
|
403
396
|
warnContrastDiagnostics(diagnostics);
|
|
404
397
|
|
package/src/theme/runtime.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime selection commands —
|
|
2
|
+
* Runtime selection commands — theme and theme mode — plus the subscribed
|
|
3
3
|
* selection snapshot consumed by `useTheme()`.
|
|
4
4
|
*
|
|
5
|
-
* Every
|
|
6
|
-
* configuration (never from the previously resolved
|
|
5
|
+
* Every theme change re-resolves both schemes from the installed, immutable
|
|
6
|
+
* configuration (never from the previously resolved theme), hands them to
|
|
7
7
|
* Unistyles, and only then publishes the new selection. A rejected or failed
|
|
8
8
|
* change leaves both the native themes and the published selection untouched.
|
|
9
9
|
*/
|
|
@@ -17,30 +17,30 @@ export type { ThemeSelection } from "./selection-store";
|
|
|
17
17
|
export { getThemeSelection, subscribeThemeSelection } from "./selection-store";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Apply a registered
|
|
20
|
+
* Apply a registered theme (`null` restores the base themes) by updating both
|
|
21
21
|
* registered themes. An unknown name warns in development and changes nothing.
|
|
22
22
|
*
|
|
23
23
|
* If the native update fails part-way (see `updateThemes` in the adapter), the
|
|
24
24
|
* error propagates and the previous selection stays published; calling
|
|
25
|
-
* `
|
|
25
|
+
* `setTheme(getCurrentTheme() ?? null)` rewrites both themes.
|
|
26
26
|
*/
|
|
27
|
-
export function
|
|
27
|
+
export function setTheme(name: string | null): void {
|
|
28
28
|
const config = getInstalledConfig();
|
|
29
|
-
if (name !== null && !config.
|
|
29
|
+
if (name !== null && !config.themeNames.includes(name)) {
|
|
30
30
|
if (__DEV__) {
|
|
31
|
-
console.warn(`[react-native-terra-ui] Unknown
|
|
31
|
+
console.warn(`[react-native-terra-ui] Unknown theme "${name}".`);
|
|
32
32
|
}
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const { themes, diagnostics } = resolveThemes(config, name);
|
|
37
37
|
updateThemes(themes);
|
|
38
|
-
publishThemeSelection({
|
|
38
|
+
publishThemeSelection({ themeName: name ?? undefined });
|
|
39
39
|
warnContrastDiagnostics(diagnostics);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/** The applied
|
|
43
|
-
export const
|
|
42
|
+
/** The applied theme name, or `undefined` for the base themes. */
|
|
43
|
+
export const getCurrentTheme = (): string | undefined => getThemeSelection().themeName;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Choose how the active scheme is selected: `system` follows the OS appearance;
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* those has to import the other.
|
|
5
5
|
*
|
|
6
6
|
* Holds two things, both private to `theme/`:
|
|
7
|
-
* - the installed {@link NormalizedThemeConfig} — immutable; every
|
|
7
|
+
* - the installed {@link NormalizedThemeConfig} — immutable; every theme
|
|
8
8
|
* selection is re-resolved from it, never layered on a previous result;
|
|
9
|
-
* - the selection snapshot (`
|
|
9
|
+
* - the selection snapshot (`themeName`, `mode`) behind a `useSyncExternalStore`
|
|
10
10
|
* compatible subscribe/get pair. The snapshot object keeps its identity until
|
|
11
11
|
* a value actually changes, so subscribers only re-render on real changes.
|
|
12
12
|
*
|
|
@@ -33,15 +33,15 @@ export function setInstalledConfig(config: NormalizedThemeConfig): void {
|
|
|
33
33
|
|
|
34
34
|
// ─── Selection snapshot ──────────────────────────────────────────────────────
|
|
35
35
|
|
|
36
|
-
/** The current runtime selection: which
|
|
36
|
+
/** The current runtime selection: which theme is applied and how the scheme is chosen. */
|
|
37
37
|
export interface ThemeSelection {
|
|
38
|
-
/** Applied
|
|
39
|
-
readonly
|
|
38
|
+
/** Applied theme name, or `undefined` for the base themes. */
|
|
39
|
+
readonly themeName: string | undefined;
|
|
40
40
|
/** `system` follows the OS appearance; `light` / `dark` pin a scheme. */
|
|
41
41
|
readonly mode: ThemeMode;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
let selection: ThemeSelection = Object.freeze({
|
|
44
|
+
let selection: ThemeSelection = Object.freeze({ themeName: undefined, mode: "system" as ThemeMode });
|
|
45
45
|
|
|
46
46
|
const listeners = new Set<() => void>();
|
|
47
47
|
|
|
@@ -65,11 +65,11 @@ export function subscribeThemeSelection(listener: () => void): () => void {
|
|
|
65
65
|
* A no-op — same snapshot identity, no notification — when nothing changed.
|
|
66
66
|
*/
|
|
67
67
|
export function publishThemeSelection(next: Partial<ThemeSelection>): void {
|
|
68
|
-
const
|
|
68
|
+
const themeName = "themeName" in next ? next.themeName : selection.themeName;
|
|
69
69
|
const mode = next.mode ?? selection.mode;
|
|
70
|
-
if (
|
|
70
|
+
if (themeName === selection.themeName && mode === selection.mode) return;
|
|
71
71
|
|
|
72
|
-
selection = Object.freeze({
|
|
72
|
+
selection = Object.freeze({ themeName, mode });
|
|
73
73
|
for (const listener of listeners) {
|
|
74
74
|
listener();
|
|
75
75
|
}
|
package/src/theme/types.ts
CHANGED
|
@@ -203,10 +203,10 @@ declare global {
|
|
|
203
203
|
namespace TerraUI {
|
|
204
204
|
/**
|
|
205
205
|
* Augment to declare app-specific semantic color keys. Declared keys become
|
|
206
|
-
* valid in `shared` /
|
|
206
|
+
* valid in `shared` / named theme tokens, `theme.color`, and
|
|
207
207
|
* component color props. Keys must not collide with built-in canonical or
|
|
208
208
|
* deprecated names. Type augmentation alone cannot prove a value exists at
|
|
209
|
-
* runtime — provide every declared color through `shared` or
|
|
209
|
+
* runtime — provide every declared color through `shared` or each theme's light/dark entries.
|
|
210
210
|
*
|
|
211
211
|
* @example
|
|
212
212
|
* declare global {
|
|
@@ -309,7 +309,7 @@ export type ContentColorToken = TextColorToken;
|
|
|
309
309
|
*/
|
|
310
310
|
export type ColorToken = keyof ThemeColor | (string & {});
|
|
311
311
|
|
|
312
|
-
// ─── Configuration /
|
|
312
|
+
// ─── Configuration / themes ─────────────────────────────────────────────────
|
|
313
313
|
|
|
314
314
|
export type Scheme = "light" | "dark";
|
|
315
315
|
|
|
@@ -319,46 +319,22 @@ export type Scheme = "light" | "dark";
|
|
|
319
319
|
*/
|
|
320
320
|
export type ThemeMode = "system" | "light" | "dark";
|
|
321
321
|
|
|
322
|
-
/**
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
* `accent.solid.bg` in `overrides` when an exact fill is required.
|
|
326
|
-
*/
|
|
327
|
-
export interface AccentSeed {
|
|
328
|
-
/** Opaque six-digit sRGB hex (`#rrggbb`). Other formats are rejected at configure time. */
|
|
322
|
+
/** Explicit theme tokens plus a seed-generated accent palette. */
|
|
323
|
+
export type ThemeSeed = TerraThemeOverride & {
|
|
324
|
+
/** Opaque six-digit hex color. Explicit `color` values win over generation. */
|
|
329
325
|
seed: string;
|
|
330
|
-
|
|
331
|
-
* Flat semantic color patch applied after generation. Within the patch a
|
|
332
|
-
* canonical name beats its deprecated alias; across layers an explicit
|
|
333
|
-
* override beats every generated value.
|
|
334
|
-
*/
|
|
335
|
-
overrides?: Partial<ThemeColor>;
|
|
336
|
-
}
|
|
326
|
+
};
|
|
337
327
|
|
|
338
|
-
/**
|
|
339
|
-
|
|
340
|
-
* - a color string — equivalent to `{ seed: value }`;
|
|
341
|
-
* - `{ seed, overrides }` — generation with explicit exceptions;
|
|
342
|
-
* - a full {@link TerraThemeOverride} — skips generation (expert path).
|
|
343
|
-
*/
|
|
344
|
-
export type AccentSchemeInput = string | AccentSeed | TerraThemeOverride;
|
|
328
|
+
/** A seed shorthand, explicit theme tokens, or both via `{ seed, color, ... }`. */
|
|
329
|
+
export type ThemeSchemeInput = string | ThemeSeed | TerraThemeOverride;
|
|
345
330
|
|
|
346
331
|
/**
|
|
347
|
-
* A named
|
|
348
|
-
*
|
|
332
|
+
* A complete named light/dark theme. Missing schemes inherit defaults + shared;
|
|
333
|
+
* seeds are never copied from the other scheme.
|
|
349
334
|
*/
|
|
350
|
-
export type
|
|
351
|
-
|
|
352
|
-
/** @deprecated Use {@link Accent} — a color string per scheme. */
|
|
353
|
-
export type AccentShorthand = Record<Scheme, string>;
|
|
335
|
+
export type NamedTheme = Partial<Record<Scheme, ThemeSchemeInput>>;
|
|
354
336
|
|
|
355
|
-
/**
|
|
356
|
-
export type AccentOverride = Partial<Record<Scheme, TerraThemeOverride>>;
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* The compatibility output of `normalizeAccent`: per-scheme partial-theme
|
|
360
|
-
* overrides generated against the default themes.
|
|
361
|
-
*/
|
|
337
|
+
/** Output of the legacy normalizeAccent helper. */
|
|
362
338
|
export interface NormalizedAccent {
|
|
363
339
|
light: DeepPartial<TerraTheme>;
|
|
364
340
|
dark: DeepPartial<TerraTheme>;
|
|
@@ -474,10 +450,8 @@ export interface ComponentDefaults {
|
|
|
474
450
|
export interface TerraConfig {
|
|
475
451
|
/** Scheme-agnostic token overrides, applied to both light and dark themes. */
|
|
476
452
|
shared?: TerraThemeOverride;
|
|
477
|
-
/**
|
|
478
|
-
|
|
479
|
-
/** Runtime-switchable named overrides (hue shorthand or full partial). */
|
|
480
|
-
accents?: Record<string, Accent>;
|
|
453
|
+
/** Named light/dark themes applied over defaults + shared. */
|
|
454
|
+
themes?: Record<string, NamedTheme>;
|
|
481
455
|
/**
|
|
482
456
|
* App-provided icon overrides. Semantic icons (`navigation.*`, `status.*`)
|
|
483
457
|
* fall back to the built-in defaults when omitted; app icons (`add`, etc.)
|
|
@@ -490,8 +464,8 @@ export interface TerraConfig {
|
|
|
490
464
|
* `resizeMode`. Wrap third-party components to match `TerraImageProps`.
|
|
491
465
|
*/
|
|
492
466
|
image?: TerraImageComponent;
|
|
493
|
-
/** Name of the
|
|
494
|
-
|
|
467
|
+
/** Name of the theme (from `themes`) to apply by default. */
|
|
468
|
+
defaultTheme?: string;
|
|
495
469
|
/** Per-component default props (e.g. `{ button: { radius: 'full' } }`). */
|
|
496
470
|
components?: ComponentDefaults;
|
|
497
471
|
/**
|
|
@@ -507,13 +481,13 @@ export interface TerraConfig {
|
|
|
507
481
|
}
|
|
508
482
|
|
|
509
483
|
/** The subset of {@link TerraConfig} that affects resolved theme tokens. */
|
|
510
|
-
export type TerraThemeInput = Pick<TerraConfig, "shared" | "
|
|
484
|
+
export type TerraThemeInput = Pick<TerraConfig, "shared" | "themes" | "defaultTheme">;
|
|
511
485
|
|
|
512
486
|
/** Options for pure theme creation. */
|
|
513
487
|
export interface CreateTerraThemesOptions {
|
|
514
488
|
/**
|
|
515
|
-
*
|
|
516
|
-
* with no
|
|
489
|
+
* Named theme to select. Omitted → `defaultTheme`; `null` → the base themes
|
|
490
|
+
* with no named theme. An unknown name throws.
|
|
517
491
|
*/
|
|
518
|
-
|
|
492
|
+
theme?: string | null;
|
|
519
493
|
}
|
|
@@ -52,7 +52,7 @@ export function configureUnistyles({
|
|
|
52
52
|
* runtime holds the new `light` theme and the previous `dark` theme. Callers
|
|
53
53
|
* publish registry/selection state only after this returns, so on a throw Terra
|
|
54
54
|
* still reports the previous selection; calling the same command again with the
|
|
55
|
-
* previous selection (e.g. `
|
|
55
|
+
* previous selection (e.g. `setTheme(getCurrentTheme() ?? null)`) rewrites
|
|
56
56
|
* both themes and restores consistency. No automatic rollback is attempted.
|
|
57
57
|
*/
|
|
58
58
|
export function updateThemes(themes: Record<Scheme, TerraTheme>): void {
|