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.
Files changed (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +8 -4
  3. package/lib/module/context/ThemeProvider.js +5 -5
  4. package/lib/module/context/ThemeProvider.js.map +1 -1
  5. package/lib/module/theme/define-config.js +5 -5
  6. package/lib/module/theme/define-config.js.map +1 -1
  7. package/lib/module/theme/errors.js +1 -1
  8. package/lib/module/theme/index.js +2 -2
  9. package/lib/module/theme/index.js.map +1 -1
  10. package/lib/module/theme/registry.js +15 -21
  11. package/lib/module/theme/registry.js.map +1 -1
  12. package/lib/module/theme/resolve-config.js +68 -68
  13. package/lib/module/theme/resolve-config.js.map +1 -1
  14. package/lib/module/theme/runtime.js +11 -11
  15. package/lib/module/theme/runtime.js.map +1 -1
  16. package/lib/module/theme/selection-store.js +7 -7
  17. package/lib/module/theme/selection-store.js.map +1 -1
  18. package/lib/module/theme/unistyles-adapter.js +1 -1
  19. package/lib/module/utils/accent-utils.js +24 -24
  20. package/lib/module/utils/accent-utils.js.map +1 -1
  21. package/lib/typescript/src/context/ThemeProvider.d.ts +7 -7
  22. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  23. package/lib/typescript/src/theme/define-config.d.ts +11 -8
  24. package/lib/typescript/src/theme/define-config.d.ts.map +1 -1
  25. package/lib/typescript/src/theme/errors.d.ts +1 -1
  26. package/lib/typescript/src/theme/index.d.ts +3 -3
  27. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  28. package/lib/typescript/src/theme/registry.d.ts +5 -5
  29. package/lib/typescript/src/theme/registry.d.ts.map +1 -1
  30. package/lib/typescript/src/theme/resolve-config.d.ts +25 -25
  31. package/lib/typescript/src/theme/resolve-config.d.ts.map +1 -1
  32. package/lib/typescript/src/theme/runtime.d.ts +5 -5
  33. package/lib/typescript/src/theme/runtime.d.ts.map +1 -1
  34. package/lib/typescript/src/theme/selection-store.d.ts +5 -5
  35. package/lib/typescript/src/theme/selection-store.d.ts.map +1 -1
  36. package/lib/typescript/src/theme/types.d.ts +20 -47
  37. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  38. package/lib/typescript/src/theme/unistyles-adapter.d.ts +1 -1
  39. package/lib/typescript/src/utils/accent-utils.d.ts +6 -7
  40. package/lib/typescript/src/utils/accent-utils.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/context/ThemeProvider.tsx +10 -10
  43. package/src/theme/define-config.ts +12 -7
  44. package/src/theme/errors.ts +1 -1
  45. package/src/theme/index.ts +6 -8
  46. package/src/theme/registry.ts +15 -23
  47. package/src/theme/resolve-config.ts +88 -95
  48. package/src/theme/runtime.ts +11 -11
  49. package/src/theme/selection-store.ts +9 -9
  50. package/src/theme/types.ts +21 -47
  51. package/src/theme/unistyles-adapter.ts +1 -1
  52. 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 accent changes and
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 → schemes[scheme]
9
- * generated selected accent selected accent explicit overrides
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
- ThemeColor,
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, scheme, generated accent, accent overrides, or a full accent
91
- * patch — goes through here, so they all follow the same rules:
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 NormalizedAccentScheme =
142
+ export type NormalizedThemeScheme =
144
143
  | { readonly kind: "none" }
145
- | { readonly kind: "seed"; readonly seed: string; readonly overrides?: Partial<ThemeColor> }
144
+ | { readonly kind: "seed"; readonly seed: string; readonly patch?: TerraThemeOverride }
146
145
  | { readonly kind: "patch"; readonly patch: TerraThemeOverride };
147
146
 
148
- interface NormalizedAccentEntry {
149
- readonly light: NormalizedAccentScheme;
150
- readonly dark: NormalizedAccentScheme;
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 → scheme` state per scheme; every accent selection is
156
- * re-resolved from it rather than from a previously resolved accent.
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 accentNames: readonly string[];
160
- readonly defaultAccent: string | null;
161
- /** @internal Immutable `defaults → shared → scheme` state per scheme. */
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-accent, per-scheme normalized inputs. */
164
- readonly accents: Readonly<Record<string, NormalizedAccentEntry>>;
162
+ /** @internal Per-theme, per-scheme normalized inputs. */
163
+ readonly themes: Readonly<Record<string, NormalizedThemeEntry>>;
165
164
  }
166
165
 
167
- export function normalizeAccentScheme(
168
- input: AccentSchemeInput | undefined,
166
+ export function normalizeThemeScheme(
167
+ input: ThemeSchemeInput | undefined,
169
168
  path: string
170
- ): NormalizedAccentScheme {
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, overrides }`, or a theme override object."
185
+ "Expected a color string, `{ seed, color }`, or a theme override object."
187
186
  );
188
187
  }
189
188
 
190
- if (!("seed" in input)) {
191
- return { kind: "patch", patch: sanitizeOverride(input as TerraThemeOverride) };
189
+ if ("overrides" in input) {
190
+ throw new TerraConfigError(`${path}.overrides`, "Use `color` for explicit colors alongside `seed`.");
192
191
  }
193
-
194
- const extra = Object.keys(input).filter((key) => key !== "seed" && key !== "overrides");
195
- if (extra.length > 0) {
196
- throw new TerraConfigError(
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
- if (overrides !== undefined && !isPlainObject(overrides)) {
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 normalizeAccentEntry(name: string, accent: Accent): NormalizedAccentEntry {
225
- const path = `accents.${name}`;
226
- if (!isPlainObject(accent)) {
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: normalizeAccentScheme(accent.light, `${path}.light`),
231
- dark: normalizeAccentScheme(accent.dark, `${path}.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 → scheme`). Throws {@link TerraConfigError} for invalid
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
- assertOverrideObject(input.schemes, "schemes");
254
- assertOverrideObject(input.schemes?.light, "schemes.light");
255
- assertOverrideObject(input.schemes?.dark, "schemes.dark");
256
- if (input.accents !== undefined && !isPlainObject(input.accents)) {
257
- throw new TerraConfigError("accents", "Expected a record of named accents.");
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 accents: Record<string, NormalizedAccentEntry> = {};
261
- for (const [name, accent] of Object.entries(input.accents ?? {})) {
262
- accents[name] = normalizeAccentEntry(name, accent);
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 accentNames = Object.keys(accents);
259
+ const themeNames = Object.keys(themes);
265
260
 
266
- let defaultAccent: string | null = null;
267
- if (input.defaultAccent !== undefined && input.defaultAccent !== null) {
268
- if (typeof input.defaultAccent !== "string" || !Object.hasOwn(accents, input.defaultAccent)) {
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
- "defaultAccent",
271
- `Unknown accent ${JSON.stringify(input.defaultAccent)}; declared accents: ${
272
- accentNames.length > 0 ? accentNames.map((n) => `"${n}"`).join(", ") : "(none)"
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
- defaultAccent = input.defaultAccent;
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 { accentNames, defaultAccent, base, accents };
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 getAccentEntry(
292
+ function getThemeEntry(
299
293
  config: NormalizedThemeConfig,
300
- accent: string | null
301
- ): NormalizedAccentEntry | undefined {
302
- if (accent === null) return undefined;
294
+ themeName: string | null
295
+ ): NormalizedThemeEntry | undefined {
296
+ if (themeName === null) return undefined;
303
297
 
304
- const entry = config.accents[accent];
305
- if (entry) return entry;
298
+ const entry = config.themes[themeName];
299
+ if (Object.hasOwn(config.themes, themeName)) return entry;
306
300
 
307
301
  throw new TerraConfigError(
308
- "accent",
309
- `Unknown accent ${JSON.stringify(accent)}; declared accents: ${
310
- config.accentNames.length > 0 ? config.accentNames.map((name) => `"${name}"`).join(", ") : "(none)"
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: NormalizedAccentScheme
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
- const context = accentContext(scheme, state.source);
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
- if (accent.overrides) {
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 `accent` from the config's immutable base. `null`
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, accent: string | null): ResolvedThemes {
365
- const entry = getAccentEntry(config, accent);
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
- accent: string | null
375
+ themeName: string | null
383
376
  ): { theme: TerraTheme; diagnostics: ContrastDiagnostic[] } {
384
- const entry = getAccentEntry(config, accent);
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.accent` omitted → the input's `defaultAccent`; `null` → the base
391
- * themes. Invalid input and unknown accents throw {@link TerraConfigError}.
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 accent = options?.accent === undefined ? config.defaultAccent : options.accent;
401
- const { themes, diagnostics } = resolveThemes(config, accent);
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
 
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Runtime selection commands — accent and theme mode — plus the subscribed
2
+ * Runtime selection commands — theme and theme mode — plus the subscribed
3
3
  * selection snapshot consumed by `useTheme()`.
4
4
  *
5
- * Every accent change re-resolves both schemes from the installed, immutable
6
- * configuration (never from the previously resolved accent), hands them to
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 accent (`null` restores the base themes) by updating both
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
- * `applyAccent(getCurrentAccent() ?? null)` rewrites both themes.
25
+ * `setTheme(getCurrentTheme() ?? null)` rewrites both themes.
26
26
  */
27
- export function applyAccent(name: string | null): void {
27
+ export function setTheme(name: string | null): void {
28
28
  const config = getInstalledConfig();
29
- if (name !== null && !config.accentNames.includes(name)) {
29
+ if (name !== null && !config.themeNames.includes(name)) {
30
30
  if (__DEV__) {
31
- console.warn(`[react-native-terra-ui] Unknown accent "${name}".`);
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({ accent: name ?? undefined });
38
+ publishThemeSelection({ themeName: name ?? undefined });
39
39
  warnContrastDiagnostics(diagnostics);
40
40
  }
41
41
 
42
- /** The applied accent name, or `undefined` for the base themes. */
43
- export const getCurrentAccent = (): string | undefined => getThemeSelection().accent;
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 accent
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 (`accent`, `mode`) behind a `useSyncExternalStore`
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 accent is applied and how the scheme is chosen. */
36
+ /** The current runtime selection: which theme is applied and how the scheme is chosen. */
37
37
  export interface ThemeSelection {
38
- /** Applied accent name, or `undefined` for the base themes. */
39
- readonly accent: string | undefined;
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({ accent: undefined, mode: "system" as ThemeMode });
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 accent = "accent" in next ? next.accent : selection.accent;
68
+ const themeName = "themeName" in next ? next.themeName : selection.themeName;
69
69
  const mode = next.mode ?? selection.mode;
70
- if (accent === selection.accent && mode === selection.mode) return;
70
+ if (themeName === selection.themeName && mode === selection.mode) return;
71
71
 
72
- selection = Object.freeze({ accent, mode });
72
+ selection = Object.freeze({ themeName, mode });
73
73
  for (const listener of listeners) {
74
74
  listener();
75
75
  }
@@ -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` / `schemes` / accent overrides, `theme.color`, and
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 both `schemes`.
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 / accents ─────────────────────────────────────────────────
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
- * A seed-generated accent with local exceptions. The seed is a starting color
324
- * for the accent family, not a promise that `accent.solid.bg` equals it — pin
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
- * One scheme's accent input:
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, runtime-switchable overlay on each scheme. A missing scheme leaves
348
- * that scheme unmodified; the other scheme's seed is never copied implicitly.
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 Accent = Partial<Record<Scheme, AccentSchemeInput>>;
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
- /** @deprecated Use {@link Accent} a {@link TerraThemeOverride} per scheme. */
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
- /** Per-scheme token overrides, applied after `shared`. */
478
- schemes?: { light?: TerraThemeOverride; dark?: TerraThemeOverride };
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 accent (from `accents`) to apply by default. */
494
- defaultAccent?: string;
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" | "schemes" | "accents" | "defaultAccent">;
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
- * Accent to overlay. Omitted → `defaultAccent`; `null` → the base themes
516
- * with no accent. An unknown name throws.
489
+ * Named theme to select. Omitted → `defaultTheme`; `null` → the base themes
490
+ * with no named theme. An unknown name throws.
517
491
  */
518
- accent?: string | null;
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. `applyAccent(getCurrentAccent() ?? null)`) rewrites
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 {