react-native-terra-ui 0.10.2 → 0.11.1

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 (82) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +8 -4
  3. package/lib/module/components/badge/Badge.js +174 -0
  4. package/lib/module/components/badge/Badge.js.map +1 -0
  5. package/lib/module/components/badge/index.js +4 -0
  6. package/lib/module/components/badge/index.js.map +1 -0
  7. package/lib/module/components/badge/parts/BadgeIndicator.js +191 -0
  8. package/lib/module/components/badge/parts/BadgeIndicator.js.map +1 -0
  9. package/lib/module/components/badge/types.js +4 -0
  10. package/lib/module/components/badge/types.js.map +1 -0
  11. package/lib/module/components/badge/utils.js +25 -0
  12. package/lib/module/components/badge/utils.js.map +1 -0
  13. package/lib/module/components/index.js +1 -0
  14. package/lib/module/components/index.js.map +1 -1
  15. package/lib/module/context/ThemeProvider.js +5 -5
  16. package/lib/module/context/ThemeProvider.js.map +1 -1
  17. package/lib/module/theme/define-config.js +5 -5
  18. package/lib/module/theme/define-config.js.map +1 -1
  19. package/lib/module/theme/errors.js +1 -1
  20. package/lib/module/theme/index.js +2 -2
  21. package/lib/module/theme/index.js.map +1 -1
  22. package/lib/module/theme/registry.js +15 -21
  23. package/lib/module/theme/registry.js.map +1 -1
  24. package/lib/module/theme/resolve-config.js +68 -68
  25. package/lib/module/theme/resolve-config.js.map +1 -1
  26. package/lib/module/theme/runtime.js +11 -11
  27. package/lib/module/theme/runtime.js.map +1 -1
  28. package/lib/module/theme/selection-store.js +7 -7
  29. package/lib/module/theme/selection-store.js.map +1 -1
  30. package/lib/module/theme/unistyles-adapter.js +1 -1
  31. package/lib/module/utils/accent-utils.js +24 -24
  32. package/lib/module/utils/accent-utils.js.map +1 -1
  33. package/lib/typescript/src/components/badge/Badge.d.ts +16 -0
  34. package/lib/typescript/src/components/badge/Badge.d.ts.map +1 -0
  35. package/lib/typescript/src/components/badge/index.d.ts +3 -0
  36. package/lib/typescript/src/components/badge/index.d.ts.map +1 -0
  37. package/lib/typescript/src/components/badge/parts/BadgeIndicator.d.ts +19 -0
  38. package/lib/typescript/src/components/badge/parts/BadgeIndicator.d.ts.map +1 -0
  39. package/lib/typescript/src/components/badge/types.d.ts +48 -0
  40. package/lib/typescript/src/components/badge/types.d.ts.map +1 -0
  41. package/lib/typescript/src/components/badge/utils.d.ts +13 -0
  42. package/lib/typescript/src/components/badge/utils.d.ts.map +1 -0
  43. package/lib/typescript/src/components/index.d.ts +1 -0
  44. package/lib/typescript/src/components/index.d.ts.map +1 -1
  45. package/lib/typescript/src/context/ThemeProvider.d.ts +7 -7
  46. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  47. package/lib/typescript/src/theme/define-config.d.ts +11 -8
  48. package/lib/typescript/src/theme/define-config.d.ts.map +1 -1
  49. package/lib/typescript/src/theme/errors.d.ts +1 -1
  50. package/lib/typescript/src/theme/index.d.ts +3 -3
  51. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  52. package/lib/typescript/src/theme/registry.d.ts +5 -5
  53. package/lib/typescript/src/theme/registry.d.ts.map +1 -1
  54. package/lib/typescript/src/theme/resolve-config.d.ts +25 -25
  55. package/lib/typescript/src/theme/resolve-config.d.ts.map +1 -1
  56. package/lib/typescript/src/theme/runtime.d.ts +5 -5
  57. package/lib/typescript/src/theme/runtime.d.ts.map +1 -1
  58. package/lib/typescript/src/theme/selection-store.d.ts +5 -5
  59. package/lib/typescript/src/theme/selection-store.d.ts.map +1 -1
  60. package/lib/typescript/src/theme/types.d.ts +20 -47
  61. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  62. package/lib/typescript/src/theme/unistyles-adapter.d.ts +1 -1
  63. package/lib/typescript/src/utils/accent-utils.d.ts +6 -7
  64. package/lib/typescript/src/utils/accent-utils.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/src/components/badge/Badge.tsx +111 -0
  67. package/src/components/badge/index.ts +2 -0
  68. package/src/components/badge/parts/BadgeIndicator.tsx +166 -0
  69. package/src/components/badge/types.ts +58 -0
  70. package/src/components/badge/utils.ts +24 -0
  71. package/src/components/index.ts +1 -0
  72. package/src/context/ThemeProvider.tsx +10 -10
  73. package/src/theme/define-config.ts +12 -7
  74. package/src/theme/errors.ts +1 -1
  75. package/src/theme/index.ts +6 -8
  76. package/src/theme/registry.ts +15 -23
  77. package/src/theme/resolve-config.ts +88 -95
  78. package/src/theme/runtime.ts +11 -11
  79. package/src/theme/selection-store.ts +9 -9
  80. package/src/theme/types.ts +21 -47
  81. package/src/theme/unistyles-adapter.ts +1 -1
  82. package/src/utils/accent-utils.ts +25 -25
@@ -1,7 +1,7 @@
1
1
  import type { TerraConfig } from "./types";
2
2
 
3
- /** The literal accent names declared in `T.accents`, or `never` when none are declared. */
4
- type DeclaredAccentNames<T> = T extends { accents: infer A }
3
+ /** The literal theme names declared in `T.themes`, or `never` when none are declared. */
4
+ type DeclaredThemeNames<T> = T extends { themes: infer A }
5
5
  ? A extends Record<string, unknown>
6
6
  ? keyof A & string
7
7
  : never
@@ -9,19 +9,24 @@ type DeclaredAccentNames<T> = T extends { accents: infer A }
9
9
 
10
10
  /**
11
11
  * Identity helper for authoring a {@link TerraConfig} with contextual typing.
12
- * Has no side effects. With literal `accents`, `defaultAccent` must name one of
13
- * them; a dynamic `Record<string, Accent>` keeps `defaultAccent` as `string`
12
+ * Has no side effects. With literal `themes`, `defaultTheme` must name one of
13
+ * them; a dynamic `Record<string, NamedTheme>` keeps `defaultTheme` as `string`
14
14
  * and defers to runtime validation.
15
15
  *
16
16
  * @example
17
17
  * const config = defineTerraConfig({
18
- * accents: { indigo: { light: "#4f46e5", dark: "#818cf8" } },
19
- * defaultAccent: "indigo",
18
+ * themes: { indigo: { light: "#4f46e5", dark: "#818cf8" } },
19
+ * defaultTheme: "indigo",
20
20
  * });
21
21
  * configureTerraUI(config);
22
22
  */
23
23
  export function defineTerraConfig<const T extends TerraConfig>(
24
- config: T & { defaultAccent?: DeclaredAccentNames<T> }
24
+ config: T & {
25
+ defaultTheme?: DeclaredThemeNames<T>;
26
+ schemes?: never;
27
+ accents?: never;
28
+ defaultAccent?: never;
29
+ }
25
30
  ): T {
26
31
  return config;
27
32
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Thrown for invalid pure configuration input — before any registry, selection,
3
3
  * or native state is touched. `path` points at the offending config entry, e.g.
4
- * `accents.indigo.dark.seed` or `defaultAccent`.
4
+ * `themes.indigo.dark.seed` or `defaultTheme`.
5
5
  */
6
6
  export class TerraConfigError extends Error {
7
7
  readonly path: string;
@@ -33,23 +33,23 @@ export { defineTerraConfig } from "./define-config";
33
33
  export { TerraConfigError } from "./errors";
34
34
  export {
35
35
  configureTerraUI,
36
- getAccentNames,
37
36
  getDefaultRadius,
38
37
  getIcon,
39
38
  getImageComponent,
40
39
  getIsConfigured,
41
40
  getRequiredIcon,
42
41
  getSurfaceElevation,
42
+ getThemeNames,
43
43
  resolveTheme,
44
44
  } from "./registry";
45
45
  export { createTerraThemes } from "./resolve-config";
46
46
  export type { ThemeSelection } from "./runtime";
47
47
  export {
48
- applyAccent,
49
48
  applyScheme,
50
- getCurrentAccent,
49
+ getCurrentTheme,
51
50
  getThemeMode,
52
51
  getThemeSelection,
52
+ setTheme,
53
53
  setThemeMode,
54
54
  } from "./runtime";
55
55
  export type { ResolvedScreenMargin } from "./screen-margin";
@@ -60,12 +60,7 @@ export { resolveScreenPadding } from "./screen-padding";
60
60
  export { defaultDarkTheme, defaultLightTheme } from "./theme";
61
61
  // ─── Types ────────────────────────────────────────────────────────────────────
62
62
  export type {
63
- Accent,
64
63
  AccentColorToken,
65
- AccentOverride,
66
- AccentSchemeInput,
67
- AccentSeed,
68
- AccentShorthand,
69
64
  ActionColorToken,
70
65
  BackgroundColorToken,
71
66
  BorderColorToken,
@@ -81,6 +76,7 @@ export type {
81
76
  ElevationStyle,
82
77
  FontWeightToken,
83
78
  LayoutTokens,
79
+ NamedTheme,
84
80
  NeutralColorToken,
85
81
  OpacityTokens,
86
82
  RadiusComponent,
@@ -110,6 +106,8 @@ export type {
110
106
  ThemeColor,
111
107
  ThemeMode,
112
108
  ThemeRadiusOverride,
109
+ ThemeSchemeInput,
110
+ ThemeSeed,
113
111
  ThemeSpacingOverride,
114
112
  TypeStyle,
115
113
  Typography,
@@ -23,7 +23,6 @@ import type {
23
23
  TerraImageComponent,
24
24
  TerraSemanticIconName,
25
25
  TerraTheme,
26
- TerraThemeInput,
27
26
  ThemeMode,
28
27
  } from "./types";
29
28
  import { applyMode, configureUnistyles, hasConfiguredUnistyles, updateThemes } from "./unistyles-adapter";
@@ -81,8 +80,8 @@ let installation: "unconfigured" | "fallback" | "app" = "unconfigured";
81
80
 
82
81
  export const getIsConfigured = (): boolean => installation !== "unconfigured";
83
82
 
84
- /** Names of the registered accents, in declaration order. Returns a fresh array. */
85
- export const getAccentNames = (): string[] => [...getInstalledConfig().accentNames];
83
+ /** Names of the registered themes, in declaration order. Returns a fresh array. */
84
+ export const getThemeNames = (): string[] => [...getInstalledConfig().themeNames];
86
85
 
87
86
  export const getImageComponent = (): TerraImageComponent => settings.imageComponent;
88
87
 
@@ -107,22 +106,22 @@ export const getSurfaceElevation = (): ElevationKey => settings.surfaceElevation
107
106
 
108
107
  /**
109
108
  * Resolves one scheme's theme from the installed configuration with the named
110
- * accent overlaid. Omitting `accentName` selects the base theme (no accent
111
- * not the configured `defaultAccent`). An unknown name warns in development
109
+ * theme selected. Omitting `name` selects the base theme (defaults + shared
110
+ * not the configured `defaultTheme`). An unknown name warns in development
112
111
  * and falls back to the base theme. Pure with respect to runtime state: it
113
112
  * never changes the applied selection.
114
113
  */
115
- export function resolveTheme(scheme: Scheme, accentName?: string): TerraTheme {
114
+ export function resolveTheme(scheme: Scheme, name?: string): TerraTheme {
116
115
  const config = getInstalledConfig();
117
- let accent: string | null = null;
118
- if (accentName !== undefined) {
119
- if (config.accentNames.includes(accentName)) {
120
- accent = accentName;
116
+ let themeName: string | null = null;
117
+ if (name !== undefined) {
118
+ if (config.themeNames.includes(name)) {
119
+ themeName = name;
121
120
  } else if (__DEV__) {
122
- console.warn(`[react-native-terra-ui] Unknown accent "${accentName}"; resolving the base theme.`);
121
+ console.warn(`[react-native-terra-ui] Unknown theme "${name}"; resolving the base theme.`);
123
122
  }
124
123
  }
125
- return resolveThemeScheme(config, scheme, accent).theme;
124
+ return resolveThemeScheme(config, scheme, themeName).theme;
126
125
  }
127
126
 
128
127
  // ─── Configuration ───────────────────────────────────────────────────────────
@@ -189,22 +188,15 @@ function deriveMode(config: TerraConfig): ThemeMode {
189
188
  return "system";
190
189
  }
191
190
 
192
- const pickThemeInput = (config: TerraConfig): TerraThemeInput => ({
193
- shared: config.shared,
194
- schemes: config.schemes,
195
- accents: config.accents,
196
- defaultAccent: config.defaultAccent,
197
- });
198
-
199
191
  function applyConfig(config: TerraConfig, kind: "fallback" | "app"): void {
200
192
  // (a) Pure: validate and build everything. Throws leave ALL state untouched.
201
- const themeConfig = normalizeThemeInput(pickThemeInput(config));
193
+ const themeConfig = normalizeThemeInput(config);
202
194
  const mode = deriveMode(config);
203
- const { themes, diagnostics } = resolveThemes(themeConfig, themeConfig.defaultAccent);
195
+ const { themes, diagnostics } = resolveThemes(themeConfig, themeConfig.defaultTheme);
204
196
  const nextSettings = buildSettings(config);
205
197
 
206
198
  // (b) Native. `StyleSheet.configure` runs once per app; after that the
207
- // rebuilt themes reach the runtime the same way an accent change does, so an
199
+ // rebuilt themes reach the runtime the same way a theme change does, so an
208
200
  // app's config is never stranded behind the fallback's defaults.
209
201
  if (hasConfiguredUnistyles()) {
210
202
  updateThemes(themes);
@@ -217,7 +209,7 @@ function applyConfig(config: TerraConfig, kind: "fallback" | "app"): void {
217
209
  installation = kind;
218
210
  settings = nextSettings;
219
211
  setInstalledConfig(themeConfig);
220
- publishThemeSelection({ accent: themeConfig.defaultAccent ?? undefined, mode });
212
+ publishThemeSelection({ themeName: themeConfig.defaultTheme ?? undefined, mode });
221
213
 
222
214
  warnContrastDiagnostics(diagnostics);
223
215
  }
@@ -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
  }