@sigx/lynx-zero 0.4.9 → 0.5.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/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/styles/tokens.css +98 -98
- package/dist/theme/ThemeProvider.d.ts +25 -1
- package/dist/theme/ThemeProvider.d.ts.map +1 -1
- package/dist/theme/ThemeProvider.js +17 -0
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/package.json +13 -8
- package/src/components/SwiperIndicator.tsx +519 -519
- package/src/contract.ts +136 -136
- package/src/index.ts +101 -101
- package/src/layout/Center.tsx +41 -41
- package/src/layout/Col.tsx +53 -53
- package/src/layout/Row.tsx +53 -53
- package/src/layout/ScrollView.tsx +38 -38
- package/src/layout/Spacer.tsx +18 -18
- package/src/preset/index.ts +77 -77
- package/src/shared/press.ts +6 -6
- package/src/shared/styles.ts +82 -82
- package/src/shared/tabs-selection.ts +57 -57
- package/src/styles/tokens.css +98 -98
- package/src/theme/StatusBarSync.tsx +104 -104
- package/src/theme/ThemeProvider.tsx +532 -492
- package/src/theme/color-mix.ts +68 -68
- package/src/theme/registry.ts +290 -290
- package/src/theme/theme-state.ts +112 -112
- package/src/theme/use-screen-theme.ts +42 -42
- package/src/theme/use-theme-colors.ts +99 -99
package/src/theme/registry.ts
CHANGED
|
@@ -1,290 +1,290 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme registry — the single source of truth for registered themes.
|
|
3
|
-
*
|
|
4
|
-
* A theme is *data*: a name, a light/dark variant, and a full color palette
|
|
5
|
-
* (plus an optional toggle `pair` and roundness overrides). Both rendering and
|
|
6
|
-
* any DS-specific consumers (e.g. icon tinting) read from here —
|
|
7
|
-
* `<ThemeProvider>` applies a theme's `colors` as inline CSS custom properties
|
|
8
|
-
* on its host view (Lynx inherits custom properties to descendants, so
|
|
9
|
-
* component classes resolve `var(--color-*)`). There is no per-theme CSS or
|
|
10
|
-
* parallel JS palette to keep in sync.
|
|
11
|
-
*
|
|
12
|
-
* The registry starts **empty** — design-system packages seed it at module
|
|
13
|
-
* load (e.g. `@sigx/lynx-daisyui` registers its six built-ins on import).
|
|
14
|
-
* Register more — including tenant themes fetched at runtime — with
|
|
15
|
-
* `registerTheme()`, or `extendTheme()` to derive one from a base. Order
|
|
16
|
-
* matters for `pickThemeFor()`: the first theme of a given variant is the
|
|
17
|
-
* follow-system default for that variant.
|
|
18
|
-
*
|
|
19
|
-
* Structural tokens (radius, sizing, component dimensions) are theme-agnostic
|
|
20
|
-
* and ship once in the bundled `.lynx-zero` base class (`styles/tokens.css`);
|
|
21
|
-
* a theme may override roundness via `radius` and base size units via `sizes`.
|
|
22
|
-
*
|
|
23
|
-
* Colors are engine-safe strings — hex or `rgb()`. Lynx's CSS engine does not
|
|
24
|
-
* parse `oklch()`, so convert before registering.
|
|
25
|
-
*/
|
|
26
|
-
import {
|
|
27
|
-
COLOR_VARIANT_LIST,
|
|
28
|
-
type ColorToken,
|
|
29
|
-
type CoreColorToken,
|
|
30
|
-
type SoftColorToken,
|
|
31
|
-
} from '../contract.js';
|
|
32
|
-
import { mixColors } from './color-mix.js';
|
|
33
|
-
|
|
34
|
-
export type ThemeVariant = 'light' | 'dark';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Full *registered* color palette — every semantic token including the
|
|
38
|
-
* `*-soft` tints, no holes. This is what `colorsOf()` returns.
|
|
39
|
-
*/
|
|
40
|
-
export type ThemePalette = Record<ColorToken, string>;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* What a theme *author* writes: every core token, with the `*-soft` tints
|
|
44
|
-
* optional — any omitted soft is computed at registration (`softMix` of the
|
|
45
|
-
* variant color into `base-100`).
|
|
46
|
-
*/
|
|
47
|
-
export type ThemePaletteInput =
|
|
48
|
-
& Record<CoreColorToken, string>
|
|
49
|
-
& Partial<Record<SoftColorToken, string>>;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Roundness token overrides. Emitted as `--radius-selector` /
|
|
53
|
-
* `--radius-field` / `--radius-box`. Defaults live in the bundled
|
|
54
|
-
* `.lynx-zero` base.
|
|
55
|
-
*/
|
|
56
|
-
export interface ThemeRadius {
|
|
57
|
-
/** Small selectable controls — checkbox, toggle, badge. */
|
|
58
|
-
selector?: string;
|
|
59
|
-
/** Fields — button, input, select, textarea. */
|
|
60
|
-
field?: string;
|
|
61
|
-
/** Boxes — card, modal, alert. */
|
|
62
|
-
box?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Base size-unit overrides. Emitted as `--size-selector` / `--size-field`;
|
|
67
|
-
* component dimensions are integer multiples of these. Defaults live in the
|
|
68
|
-
* bundled `.lynx-zero` base.
|
|
69
|
-
*/
|
|
70
|
-
export interface ThemeSizes {
|
|
71
|
-
/** Base unit for selector controls (checkbox, toggle, badge). */
|
|
72
|
-
selector?: string;
|
|
73
|
-
/** Base unit for fields (button, input, select). */
|
|
74
|
-
field?: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface ThemeInput {
|
|
78
|
-
/** Unique id — also the value of `theme.name`. */
|
|
79
|
-
name: string;
|
|
80
|
-
/** Light or dark — drives follow-system selection and status-bar tint. */
|
|
81
|
-
variant: ThemeVariant;
|
|
82
|
-
/** Color palette — core tokens required, `*-soft` tints optional. */
|
|
83
|
-
colors: ThemePaletteInput;
|
|
84
|
-
/**
|
|
85
|
-
* Which theme `toggle()` flips to. Defaults to the first registered theme of
|
|
86
|
-
* the opposite variant.
|
|
87
|
-
*/
|
|
88
|
-
pair?: string;
|
|
89
|
-
/** Optional roundness overrides; unspecified tokens fall back to the base. */
|
|
90
|
-
radius?: ThemeRadius;
|
|
91
|
-
/** Optional base size-unit overrides; unspecified tokens fall back to the base. */
|
|
92
|
-
sizes?: ThemeSizes;
|
|
93
|
-
/**
|
|
94
|
-
* Whether this theme ships a build-time CSS class named after it (the DS
|
|
95
|
-
* package generates `.theme-name { --color-*: … }` at build time, e.g. via
|
|
96
|
-
* daisyui's `gen-theme-css.mjs`). Such themes paint correctly on the very
|
|
97
|
-
* first frame; themes without it apply via the runtime `setProperty` path
|
|
98
|
-
* post-mount, with their variant's static theme class as the first-frame
|
|
99
|
-
* fallback.
|
|
100
|
-
*/
|
|
101
|
-
staticCss?: boolean;
|
|
102
|
-
/**
|
|
103
|
-
* How strong the computed `*-soft` tints are: the ratio of the variant
|
|
104
|
-
* color mixed into `base-100` for any soft token the palette doesn't set
|
|
105
|
-
* explicitly. Design-system flavor, carried as theme data — daisy's
|
|
106
|
-
* built-ins use `0.08` (daisyUI v5's ~8% tints), hero's use `0.2`
|
|
107
|
-
* (HeroUI's `color/20`). Default `0.16`.
|
|
108
|
-
*/
|
|
109
|
-
softMix?: number;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** A registered theme — same shape as the input, with the palette completed. */
|
|
113
|
-
export interface Theme extends Omit<ThemeInput, 'colors'> {
|
|
114
|
-
colors: ThemePalette;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const DEFAULT_SOFT_MIX = 0.16;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Complete a theme's palette: any `*-soft` token the author didn't set is
|
|
121
|
-
* computed as `softMix` of the variant color mixed into `base-100` (in JS —
|
|
122
|
-
* Lynx CSS can't alpha-compose `var()` colors, so the tints are materialized
|
|
123
|
-
* in the palette). Idempotent; explicitly provided softs are kept verbatim.
|
|
124
|
-
*
|
|
125
|
-
* DS packages run their builtin arrays through this before exporting them so
|
|
126
|
-
* build scripts (gen-theme-css) see the same palette the registry serves.
|
|
127
|
-
*/
|
|
128
|
-
export function completeTheme(input: ThemeInput): Theme {
|
|
129
|
-
const mix = input.softMix ?? DEFAULT_SOFT_MIX;
|
|
130
|
-
const colors = { ...input.colors } as ThemePalette;
|
|
131
|
-
for (const variant of COLOR_VARIANT_LIST) {
|
|
132
|
-
const soft: SoftColorToken = `${variant}-soft`;
|
|
133
|
-
if (colors[soft] === undefined) {
|
|
134
|
-
colors[soft] = mixColors(colors[variant], colors['base-100'], mix);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return { ...input, colors };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const registry: Theme[] = [];
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Whether `name` is a registered theme that ships a build-time CSS class —
|
|
144
|
-
* i.e. it paints correctly on the first frame. Themes registered without
|
|
145
|
-
* `staticCss` return `false`; `<ThemeProvider>` falls back to their variant's
|
|
146
|
-
* static class for first paint and swaps in the exact palette via
|
|
147
|
-
* `setProperty`.
|
|
148
|
-
*/
|
|
149
|
-
export function hasStaticCss(name: string | undefined): boolean {
|
|
150
|
-
return findTheme(name)?.staticCss === true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Resolve a `theme.name` to its registered `Theme`. Supports multi-class names
|
|
155
|
-
* like `'daisy-light daisy-rounded'` by matching the first registered id found.
|
|
156
|
-
*/
|
|
157
|
-
function findTheme(name: string | undefined): Theme | undefined {
|
|
158
|
-
if (!name) return undefined;
|
|
159
|
-
for (const part of name.split(/\s+/)) {
|
|
160
|
-
const hit = registry.find((t) => t.name === part);
|
|
161
|
-
if (hit) return hit;
|
|
162
|
-
}
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* All registered themes in insertion order. Returns a shallow copy so callers
|
|
168
|
-
* can't mutate the internal registry — re-registration goes through
|
|
169
|
-
* `registerTheme()`. Each entry is a full `Theme` (name, variant, palette),
|
|
170
|
-
* so consumers can render swatches in a picker.
|
|
171
|
-
*/
|
|
172
|
-
export function listThemes(): readonly Theme[] {
|
|
173
|
-
return registry.slice();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Register (or replace, by `name`) a theme. Call at module-load time before
|
|
178
|
-
* mounting `<ThemeProvider>` so it shows up in `listThemes()` / `pickThemeFor()`.
|
|
179
|
-
* The palette is completed on the way in (`completeTheme`): any `*-soft`
|
|
180
|
-
* token the author didn't set is computed from `softMix`.
|
|
181
|
-
*/
|
|
182
|
-
export function registerTheme(theme: ThemeInput): void {
|
|
183
|
-
const complete = completeTheme(theme);
|
|
184
|
-
const i = registry.findIndex((t) => t.name === complete.name);
|
|
185
|
-
if (i >= 0) registry[i] = complete;
|
|
186
|
-
else registry.push(complete);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Derive a new theme from a registered base, overriding any colors / roundness.
|
|
191
|
-
* Ergonomic for "tenant tweaks a few tokens": the result is a full `Theme` you
|
|
192
|
-
* pass to `registerTheme()`. Throws if `base` isn't registered.
|
|
193
|
-
*
|
|
194
|
-
* ```ts
|
|
195
|
-
* registerTheme(extendTheme('daisy-dark', {
|
|
196
|
-
* name: 'acme-dark',
|
|
197
|
-
* colors: { primary: '#fb7185' },
|
|
198
|
-
* }));
|
|
199
|
-
* ```
|
|
200
|
-
*/
|
|
201
|
-
export function extendTheme(
|
|
202
|
-
base: string,
|
|
203
|
-
patch: {
|
|
204
|
-
name: string;
|
|
205
|
-
variant?: ThemeVariant;
|
|
206
|
-
pair?: string;
|
|
207
|
-
colors?: Partial<ThemePalette>;
|
|
208
|
-
radius?: ThemeRadius;
|
|
209
|
-
sizes?: ThemeSizes;
|
|
210
|
-
softMix?: number;
|
|
211
|
-
},
|
|
212
|
-
): Theme {
|
|
213
|
-
const src = findTheme(base);
|
|
214
|
-
if (!src) {
|
|
215
|
-
throw new Error(
|
|
216
|
-
`[lynx-zero] extendTheme: unknown base theme "${base}". `
|
|
217
|
-
+ `Register it first, or extend a theme your design system registered.`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
// Merge core tokens, then RECOMPUTE every soft tint the patch didn't set
|
|
221
|
-
// explicitly — patching `primary` must not leave the base's stale
|
|
222
|
-
// `primary-soft` behind. (A soft the base author set explicitly is
|
|
223
|
-
// indistinguishable from a computed one post-registration; explicit softs
|
|
224
|
-
// therefore live in the patch when extending.)
|
|
225
|
-
const merged: Record<string, string> = { ...src.colors };
|
|
226
|
-
for (const variant of COLOR_VARIANT_LIST) delete merged[`${variant}-soft`];
|
|
227
|
-
Object.assign(merged, patch.colors);
|
|
228
|
-
return completeTheme({
|
|
229
|
-
name: patch.name,
|
|
230
|
-
variant: patch.variant ?? src.variant,
|
|
231
|
-
pair: patch.pair ?? src.pair,
|
|
232
|
-
colors: merged as ThemePaletteInput,
|
|
233
|
-
radius: patch.radius ?? src.radius,
|
|
234
|
-
sizes: patch.sizes ?? src.sizes,
|
|
235
|
-
softMix: patch.softMix ?? src.softMix,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** The variant of a registered theme, or `undefined` if not registered. */
|
|
240
|
-
export function variantOf(name: string | undefined): ThemeVariant | undefined {
|
|
241
|
-
return findTheme(name)?.variant;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** The color palette of a registered theme, or `undefined` if not registered. */
|
|
245
|
-
export function colorsOf(name: string | undefined): ThemePalette | undefined {
|
|
246
|
-
return findTheme(name)?.colors;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** The roundness overrides of a registered theme, if any. */
|
|
250
|
-
export function radiusOf(name: string | undefined): ThemeRadius | undefined {
|
|
251
|
-
return findTheme(name)?.radius;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** The base size-unit overrides of a registered theme, if any. */
|
|
255
|
-
export function sizesOf(name: string | undefined): ThemeSizes | undefined {
|
|
256
|
-
return findTheme(name)?.sizes;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* The first registered palette — the engine's last-resort fallback when an
|
|
261
|
-
* active theme name isn't registered. `undefined` only when no design system
|
|
262
|
-
* has seeded the registry yet.
|
|
263
|
-
* @internal
|
|
264
|
-
*/
|
|
265
|
-
export function fallbackPalette(): ThemePalette | undefined {
|
|
266
|
-
return registry[0]?.colors;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Pick a default theme for a given system color scheme — the first registered
|
|
271
|
-
* theme of that variant. Falls back to the first registered theme of any
|
|
272
|
-
* variant, or `''` while the registry is empty (a design-system package seeds
|
|
273
|
-
* it at module load, so this is only reachable before any DS import).
|
|
274
|
-
*/
|
|
275
|
-
export function pickThemeFor(scheme: ThemeVariant): string {
|
|
276
|
-
const hit = registry.find((t) => t.variant === scheme);
|
|
277
|
-
return hit?.name ?? registry[0]?.name ?? '';
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Resolve the paired theme of a given name — used by `theme.toggle()`. Follows
|
|
282
|
-
* `pair` if set, otherwise the first theme of the opposite variant. Returns the
|
|
283
|
-
* input unchanged when the theme isn't registered.
|
|
284
|
-
*/
|
|
285
|
-
export function pairOf(name: string): string {
|
|
286
|
-
const hit = findTheme(name);
|
|
287
|
-
if (!hit) return name;
|
|
288
|
-
if (hit.pair) return hit.pair;
|
|
289
|
-
return pickThemeFor(hit.variant === 'light' ? 'dark' : 'light');
|
|
290
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Theme registry — the single source of truth for registered themes.
|
|
3
|
+
*
|
|
4
|
+
* A theme is *data*: a name, a light/dark variant, and a full color palette
|
|
5
|
+
* (plus an optional toggle `pair` and roundness overrides). Both rendering and
|
|
6
|
+
* any DS-specific consumers (e.g. icon tinting) read from here —
|
|
7
|
+
* `<ThemeProvider>` applies a theme's `colors` as inline CSS custom properties
|
|
8
|
+
* on its host view (Lynx inherits custom properties to descendants, so
|
|
9
|
+
* component classes resolve `var(--color-*)`). There is no per-theme CSS or
|
|
10
|
+
* parallel JS palette to keep in sync.
|
|
11
|
+
*
|
|
12
|
+
* The registry starts **empty** — design-system packages seed it at module
|
|
13
|
+
* load (e.g. `@sigx/lynx-daisyui` registers its six built-ins on import).
|
|
14
|
+
* Register more — including tenant themes fetched at runtime — with
|
|
15
|
+
* `registerTheme()`, or `extendTheme()` to derive one from a base. Order
|
|
16
|
+
* matters for `pickThemeFor()`: the first theme of a given variant is the
|
|
17
|
+
* follow-system default for that variant.
|
|
18
|
+
*
|
|
19
|
+
* Structural tokens (radius, sizing, component dimensions) are theme-agnostic
|
|
20
|
+
* and ship once in the bundled `.lynx-zero` base class (`styles/tokens.css`);
|
|
21
|
+
* a theme may override roundness via `radius` and base size units via `sizes`.
|
|
22
|
+
*
|
|
23
|
+
* Colors are engine-safe strings — hex or `rgb()`. Lynx's CSS engine does not
|
|
24
|
+
* parse `oklch()`, so convert before registering.
|
|
25
|
+
*/
|
|
26
|
+
import {
|
|
27
|
+
COLOR_VARIANT_LIST,
|
|
28
|
+
type ColorToken,
|
|
29
|
+
type CoreColorToken,
|
|
30
|
+
type SoftColorToken,
|
|
31
|
+
} from '../contract.js';
|
|
32
|
+
import { mixColors } from './color-mix.js';
|
|
33
|
+
|
|
34
|
+
export type ThemeVariant = 'light' | 'dark';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Full *registered* color palette — every semantic token including the
|
|
38
|
+
* `*-soft` tints, no holes. This is what `colorsOf()` returns.
|
|
39
|
+
*/
|
|
40
|
+
export type ThemePalette = Record<ColorToken, string>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What a theme *author* writes: every core token, with the `*-soft` tints
|
|
44
|
+
* optional — any omitted soft is computed at registration (`softMix` of the
|
|
45
|
+
* variant color into `base-100`).
|
|
46
|
+
*/
|
|
47
|
+
export type ThemePaletteInput =
|
|
48
|
+
& Record<CoreColorToken, string>
|
|
49
|
+
& Partial<Record<SoftColorToken, string>>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Roundness token overrides. Emitted as `--radius-selector` /
|
|
53
|
+
* `--radius-field` / `--radius-box`. Defaults live in the bundled
|
|
54
|
+
* `.lynx-zero` base.
|
|
55
|
+
*/
|
|
56
|
+
export interface ThemeRadius {
|
|
57
|
+
/** Small selectable controls — checkbox, toggle, badge. */
|
|
58
|
+
selector?: string;
|
|
59
|
+
/** Fields — button, input, select, textarea. */
|
|
60
|
+
field?: string;
|
|
61
|
+
/** Boxes — card, modal, alert. */
|
|
62
|
+
box?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Base size-unit overrides. Emitted as `--size-selector` / `--size-field`;
|
|
67
|
+
* component dimensions are integer multiples of these. Defaults live in the
|
|
68
|
+
* bundled `.lynx-zero` base.
|
|
69
|
+
*/
|
|
70
|
+
export interface ThemeSizes {
|
|
71
|
+
/** Base unit for selector controls (checkbox, toggle, badge). */
|
|
72
|
+
selector?: string;
|
|
73
|
+
/** Base unit for fields (button, input, select). */
|
|
74
|
+
field?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ThemeInput {
|
|
78
|
+
/** Unique id — also the value of `theme.name`. */
|
|
79
|
+
name: string;
|
|
80
|
+
/** Light or dark — drives follow-system selection and status-bar tint. */
|
|
81
|
+
variant: ThemeVariant;
|
|
82
|
+
/** Color palette — core tokens required, `*-soft` tints optional. */
|
|
83
|
+
colors: ThemePaletteInput;
|
|
84
|
+
/**
|
|
85
|
+
* Which theme `toggle()` flips to. Defaults to the first registered theme of
|
|
86
|
+
* the opposite variant.
|
|
87
|
+
*/
|
|
88
|
+
pair?: string;
|
|
89
|
+
/** Optional roundness overrides; unspecified tokens fall back to the base. */
|
|
90
|
+
radius?: ThemeRadius;
|
|
91
|
+
/** Optional base size-unit overrides; unspecified tokens fall back to the base. */
|
|
92
|
+
sizes?: ThemeSizes;
|
|
93
|
+
/**
|
|
94
|
+
* Whether this theme ships a build-time CSS class named after it (the DS
|
|
95
|
+
* package generates `.theme-name { --color-*: … }` at build time, e.g. via
|
|
96
|
+
* daisyui's `gen-theme-css.mjs`). Such themes paint correctly on the very
|
|
97
|
+
* first frame; themes without it apply via the runtime `setProperty` path
|
|
98
|
+
* post-mount, with their variant's static theme class as the first-frame
|
|
99
|
+
* fallback.
|
|
100
|
+
*/
|
|
101
|
+
staticCss?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* How strong the computed `*-soft` tints are: the ratio of the variant
|
|
104
|
+
* color mixed into `base-100` for any soft token the palette doesn't set
|
|
105
|
+
* explicitly. Design-system flavor, carried as theme data — daisy's
|
|
106
|
+
* built-ins use `0.08` (daisyUI v5's ~8% tints), hero's use `0.2`
|
|
107
|
+
* (HeroUI's `color/20`). Default `0.16`.
|
|
108
|
+
*/
|
|
109
|
+
softMix?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A registered theme — same shape as the input, with the palette completed. */
|
|
113
|
+
export interface Theme extends Omit<ThemeInput, 'colors'> {
|
|
114
|
+
colors: ThemePalette;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const DEFAULT_SOFT_MIX = 0.16;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Complete a theme's palette: any `*-soft` token the author didn't set is
|
|
121
|
+
* computed as `softMix` of the variant color mixed into `base-100` (in JS —
|
|
122
|
+
* Lynx CSS can't alpha-compose `var()` colors, so the tints are materialized
|
|
123
|
+
* in the palette). Idempotent; explicitly provided softs are kept verbatim.
|
|
124
|
+
*
|
|
125
|
+
* DS packages run their builtin arrays through this before exporting them so
|
|
126
|
+
* build scripts (gen-theme-css) see the same palette the registry serves.
|
|
127
|
+
*/
|
|
128
|
+
export function completeTheme(input: ThemeInput): Theme {
|
|
129
|
+
const mix = input.softMix ?? DEFAULT_SOFT_MIX;
|
|
130
|
+
const colors = { ...input.colors } as ThemePalette;
|
|
131
|
+
for (const variant of COLOR_VARIANT_LIST) {
|
|
132
|
+
const soft: SoftColorToken = `${variant}-soft`;
|
|
133
|
+
if (colors[soft] === undefined) {
|
|
134
|
+
colors[soft] = mixColors(colors[variant], colors['base-100'], mix);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { ...input, colors };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const registry: Theme[] = [];
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Whether `name` is a registered theme that ships a build-time CSS class —
|
|
144
|
+
* i.e. it paints correctly on the first frame. Themes registered without
|
|
145
|
+
* `staticCss` return `false`; `<ThemeProvider>` falls back to their variant's
|
|
146
|
+
* static class for first paint and swaps in the exact palette via
|
|
147
|
+
* `setProperty`.
|
|
148
|
+
*/
|
|
149
|
+
export function hasStaticCss(name: string | undefined): boolean {
|
|
150
|
+
return findTheme(name)?.staticCss === true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve a `theme.name` to its registered `Theme`. Supports multi-class names
|
|
155
|
+
* like `'daisy-light daisy-rounded'` by matching the first registered id found.
|
|
156
|
+
*/
|
|
157
|
+
function findTheme(name: string | undefined): Theme | undefined {
|
|
158
|
+
if (!name) return undefined;
|
|
159
|
+
for (const part of name.split(/\s+/)) {
|
|
160
|
+
const hit = registry.find((t) => t.name === part);
|
|
161
|
+
if (hit) return hit;
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* All registered themes in insertion order. Returns a shallow copy so callers
|
|
168
|
+
* can't mutate the internal registry — re-registration goes through
|
|
169
|
+
* `registerTheme()`. Each entry is a full `Theme` (name, variant, palette),
|
|
170
|
+
* so consumers can render swatches in a picker.
|
|
171
|
+
*/
|
|
172
|
+
export function listThemes(): readonly Theme[] {
|
|
173
|
+
return registry.slice();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Register (or replace, by `name`) a theme. Call at module-load time before
|
|
178
|
+
* mounting `<ThemeProvider>` so it shows up in `listThemes()` / `pickThemeFor()`.
|
|
179
|
+
* The palette is completed on the way in (`completeTheme`): any `*-soft`
|
|
180
|
+
* token the author didn't set is computed from `softMix`.
|
|
181
|
+
*/
|
|
182
|
+
export function registerTheme(theme: ThemeInput): void {
|
|
183
|
+
const complete = completeTheme(theme);
|
|
184
|
+
const i = registry.findIndex((t) => t.name === complete.name);
|
|
185
|
+
if (i >= 0) registry[i] = complete;
|
|
186
|
+
else registry.push(complete);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Derive a new theme from a registered base, overriding any colors / roundness.
|
|
191
|
+
* Ergonomic for "tenant tweaks a few tokens": the result is a full `Theme` you
|
|
192
|
+
* pass to `registerTheme()`. Throws if `base` isn't registered.
|
|
193
|
+
*
|
|
194
|
+
* ```ts
|
|
195
|
+
* registerTheme(extendTheme('daisy-dark', {
|
|
196
|
+
* name: 'acme-dark',
|
|
197
|
+
* colors: { primary: '#fb7185' },
|
|
198
|
+
* }));
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function extendTheme(
|
|
202
|
+
base: string,
|
|
203
|
+
patch: {
|
|
204
|
+
name: string;
|
|
205
|
+
variant?: ThemeVariant;
|
|
206
|
+
pair?: string;
|
|
207
|
+
colors?: Partial<ThemePalette>;
|
|
208
|
+
radius?: ThemeRadius;
|
|
209
|
+
sizes?: ThemeSizes;
|
|
210
|
+
softMix?: number;
|
|
211
|
+
},
|
|
212
|
+
): Theme {
|
|
213
|
+
const src = findTheme(base);
|
|
214
|
+
if (!src) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`[lynx-zero] extendTheme: unknown base theme "${base}". `
|
|
217
|
+
+ `Register it first, or extend a theme your design system registered.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
// Merge core tokens, then RECOMPUTE every soft tint the patch didn't set
|
|
221
|
+
// explicitly — patching `primary` must not leave the base's stale
|
|
222
|
+
// `primary-soft` behind. (A soft the base author set explicitly is
|
|
223
|
+
// indistinguishable from a computed one post-registration; explicit softs
|
|
224
|
+
// therefore live in the patch when extending.)
|
|
225
|
+
const merged: Record<string, string> = { ...src.colors };
|
|
226
|
+
for (const variant of COLOR_VARIANT_LIST) delete merged[`${variant}-soft`];
|
|
227
|
+
Object.assign(merged, patch.colors);
|
|
228
|
+
return completeTheme({
|
|
229
|
+
name: patch.name,
|
|
230
|
+
variant: patch.variant ?? src.variant,
|
|
231
|
+
pair: patch.pair ?? src.pair,
|
|
232
|
+
colors: merged as ThemePaletteInput,
|
|
233
|
+
radius: patch.radius ?? src.radius,
|
|
234
|
+
sizes: patch.sizes ?? src.sizes,
|
|
235
|
+
softMix: patch.softMix ?? src.softMix,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The variant of a registered theme, or `undefined` if not registered. */
|
|
240
|
+
export function variantOf(name: string | undefined): ThemeVariant | undefined {
|
|
241
|
+
return findTheme(name)?.variant;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The color palette of a registered theme, or `undefined` if not registered. */
|
|
245
|
+
export function colorsOf(name: string | undefined): ThemePalette | undefined {
|
|
246
|
+
return findTheme(name)?.colors;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The roundness overrides of a registered theme, if any. */
|
|
250
|
+
export function radiusOf(name: string | undefined): ThemeRadius | undefined {
|
|
251
|
+
return findTheme(name)?.radius;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The base size-unit overrides of a registered theme, if any. */
|
|
255
|
+
export function sizesOf(name: string | undefined): ThemeSizes | undefined {
|
|
256
|
+
return findTheme(name)?.sizes;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The first registered palette — the engine's last-resort fallback when an
|
|
261
|
+
* active theme name isn't registered. `undefined` only when no design system
|
|
262
|
+
* has seeded the registry yet.
|
|
263
|
+
* @internal
|
|
264
|
+
*/
|
|
265
|
+
export function fallbackPalette(): ThemePalette | undefined {
|
|
266
|
+
return registry[0]?.colors;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Pick a default theme for a given system color scheme — the first registered
|
|
271
|
+
* theme of that variant. Falls back to the first registered theme of any
|
|
272
|
+
* variant, or `''` while the registry is empty (a design-system package seeds
|
|
273
|
+
* it at module load, so this is only reachable before any DS import).
|
|
274
|
+
*/
|
|
275
|
+
export function pickThemeFor(scheme: ThemeVariant): string {
|
|
276
|
+
const hit = registry.find((t) => t.variant === scheme);
|
|
277
|
+
return hit?.name ?? registry[0]?.name ?? '';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Resolve the paired theme of a given name — used by `theme.toggle()`. Follows
|
|
282
|
+
* `pair` if set, otherwise the first theme of the opposite variant. Returns the
|
|
283
|
+
* input unchanged when the theme isn't registered.
|
|
284
|
+
*/
|
|
285
|
+
export function pairOf(name: string): string {
|
|
286
|
+
const hit = findTheme(name);
|
|
287
|
+
if (!hit) return name;
|
|
288
|
+
if (hit.pair) return hit.pair;
|
|
289
|
+
return pickThemeFor(hit.variant === 'light' ? 'dark' : 'light');
|
|
290
|
+
}
|