@kennycorrea/ui 0.1.1 → 0.2.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/README.md +73 -10
- package/dist/atoms/Badge/Badge.js +1 -1
- package/dist/atoms/Button/Button.js +1 -1
- package/dist/atoms/Button/Button.types.d.ts +1 -1
- package/dist/atoms/Card/Card.d.ts +1 -1
- package/dist/atoms/Card/Card.js +1 -1
- package/dist/atoms/Dialog/Dialog.d.ts +1 -1
- package/dist/atoms/Dialog/Dialog.js +1 -1
- package/dist/utils/theme.d.ts +81 -13
- package/dist/utils/theme.js +110 -47
- package/package.json +1 -1
- package/tokens.css +3 -3
package/README.md
CHANGED
|
@@ -63,6 +63,58 @@ And in the app's own `index.css`:
|
|
|
63
63
|
every utility class used *inside* the atoms — `w-full`, `inline-flex` — is missing from the
|
|
64
64
|
generated CSS. The components come out subtly unstyled and nothing appears in the console.
|
|
65
65
|
|
|
66
|
+
## Theming — 0.2.0
|
|
67
|
+
|
|
68
|
+
**A product configures its own accents, default and storage namespace.** Until 0.2.0 all three
|
|
69
|
+
were hardcoded to the first app that used this package: the fallback accent was jade, the accent
|
|
70
|
+
list was six fixed colours, and preferences were stored under `upfront.theme` / `upfront.accent`.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// Once, in the app.
|
|
74
|
+
export const theming = createTheming({
|
|
75
|
+
accents: ['teal', 'crimson'],
|
|
76
|
+
defaultAccent: 'teal',
|
|
77
|
+
storagePrefix: 'myapp',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
theming.readAccent(); // 'teal' | 'crimson'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Any Radix accent name works, and a product with one brand colour passes one. The list is the
|
|
84
|
+
order a picker should render.
|
|
85
|
+
|
|
86
|
+
**`defaultAccent` is typed as a member of `accents`**, so naming one that is not on the list is
|
|
87
|
+
`TS2322` rather than a colourless first paint — verified by compiling the mistake.
|
|
88
|
+
|
|
89
|
+
**The literal array needs no `as const`**; the `const` type parameter infers the tuple. A list
|
|
90
|
+
declared separately does, because `const list = ['teal']` widens to `string[]` and is rejected.
|
|
91
|
+
|
|
92
|
+
⚠ **`storagePrefix` is required and has no default.** A default would be one product's name
|
|
93
|
+
silently storing another's preferences, and because separate origins never collide, nothing would
|
|
94
|
+
ever reveal it.
|
|
95
|
+
|
|
96
|
+
⚠ **Every accent needs its Radix colour scale imported** in the app's `main.tsx` — see Colour
|
|
97
|
+
scales below. A missing scale is not an error; it is a colourless app.
|
|
98
|
+
|
|
99
|
+
**A factory, not a `configure()` call.** Module-level mutable state would have to be written
|
|
100
|
+
before the first read, making correctness depend on import order — the same class of silent bug as
|
|
101
|
+
the CSS import order this package already has to warn about. An instance cannot be read before it
|
|
102
|
+
is made.
|
|
103
|
+
|
|
104
|
+
`applyTheme` stays a free function, because `data-theme` is the contract between it and
|
|
105
|
+
`tokens.css` rather than a preference: two apps disagreeing about the attribute name would each
|
|
106
|
+
need their own stylesheet.
|
|
107
|
+
|
|
108
|
+
### What still works unchanged
|
|
109
|
+
|
|
110
|
+
`readTheme`, `storeTheme`, `readAccent` and `storeAccent` are still exported as free functions,
|
|
111
|
+
bound to an instance with the six default accents, jade, and the `upfront.*` keys. **One
|
|
112
|
+
implementation, not two** — they are literally a pre-made instance, so no call site had to change
|
|
113
|
+
and nobody who already picked a theme loses it. `ACCENTS` remains as a deprecated alias for
|
|
114
|
+
`DEFAULT_ACCENTS`, the same array.
|
|
115
|
+
|
|
116
|
+
A new product should call `createTheming` with its own name rather than inherit those.
|
|
117
|
+
|
|
66
118
|
## What is deliberately not here
|
|
67
119
|
|
|
68
120
|
- **Dictionaries.** Words belong to an app. The two strings the atoms say on their own behalf —
|
|
@@ -160,17 +212,28 @@ That was the state when the workflow was first added.
|
|
|
160
212
|
|
|
161
213
|
### Where it is consumed from today
|
|
162
214
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
Once this is published, each consumer's dependency becomes a version instead of a path:
|
|
215
|
+
✅ **All three apps depend on a version from the registry**, as of 2026-09-30. The
|
|
216
|
+
`file:../ui` arrangement is gone, and each app now builds on Vercel from its own repository.
|
|
167
217
|
|
|
168
218
|
```jsonc
|
|
169
|
-
//
|
|
170
|
-
"@kennycorrea/ui": "^0.
|
|
219
|
+
// a new consumer
|
|
220
|
+
"@kennycorrea/ui": "^0.2.0"
|
|
221
|
+
|
|
222
|
+
// web, site and admin, still — see the note below
|
|
223
|
+
"@kennycorrea/ui": "^0.1.1"
|
|
171
224
|
```
|
|
172
225
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
226
|
+
⚠ **Never floor a range at 0.1.0** — its published output cannot be imported by Node. `^0.1.0`
|
|
227
|
+
resolves correctly today because npm picks the newest match, but a range whose floor is unusable
|
|
228
|
+
documents nothing and would let a lockfile built from scratch land back on it.
|
|
229
|
+
|
|
230
|
+
⚠ **Under `0.x`, `^` does not cross a minor.** `^0.1.1` will **not** take 0.2.0, so moving a
|
|
231
|
+
consumer onto the new theming API is a `package.json` change in each app, not just a lockfile
|
|
232
|
+
refresh. The free functions still work, so an app that does not need `createTheming` can stay
|
|
233
|
+
where it is — and the three existing apps deliberately do, for now. Moving them is a one-line
|
|
234
|
+
change each plus a lockfile, worth doing to stop three consumers drifting a minor behind, and
|
|
235
|
+
worth doing as its own commit rather than inside this one.
|
|
236
|
+
|
|
237
|
+
⚠ **A `file:` install was a symlink; a version install is a copy.** Edits here no longer appear in
|
|
238
|
+
a consumer. An app that needs an unreleased change wants `npm link` or a temporary `file:`
|
|
239
|
+
override — never a copied file, which is the drift this package exists to end.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Badge as ThemedBadge } from '@radix-ui/themes';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* The five meanings this library names, in Themes' colour vocabulary.
|
|
5
5
|
*
|
|
6
6
|
* **`brand` names no colour on purpose.** Themes falls back to whatever accent the theme
|
|
7
7
|
* carries, so a badge that means "this is ours" follows the business's chosen colour while
|
|
@@ -3,7 +3,7 @@ import { Button as ThemedButton } from '@radix-ui/themes';
|
|
|
3
3
|
import { forwardRef } from 'react';
|
|
4
4
|
import { cn } from '../../utils/cn.js';
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* The house intents, expressed in Radix Themes' variant-and-colour vocabulary.
|
|
7
7
|
*
|
|
8
8
|
* The mapping exists so three apps keep saying `variant="danger"` rather than
|
|
9
9
|
* `variant="solid" color="red"` at sixty call sites — and so that when the house style changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Button as ThemedButton } from '@radix-ui/themes';
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* The house button intents.
|
|
5
5
|
*
|
|
6
6
|
* **`outline` is new, and it is how the disagreement was resolved.** The dashboard's
|
|
7
7
|
* `secondary` mapped to Themes' `soft` and the marketing site's mapped to `surface` — the same
|
|
@@ -6,7 +6,7 @@ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
6
6
|
/**
|
|
7
7
|
* Radix Themes' Card, with the padding taken off.
|
|
8
8
|
*
|
|
9
|
-
* Themes pads its card for you;
|
|
9
|
+
* Themes pads its card for you; the house card is built from a header and a body with a
|
|
10
10
|
* rule between them, so the padding belongs to those rather than to the card. `p-0` is the
|
|
11
11
|
* whole of the customisation.
|
|
12
12
|
*/
|
package/dist/atoms/Card/Card.js
CHANGED
|
@@ -4,7 +4,7 @@ import { cn } from '../../utils/cn.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Radix Themes' Card, with the padding taken off.
|
|
6
6
|
*
|
|
7
|
-
* Themes pads its card for you;
|
|
7
|
+
* Themes pads its card for you; the house card is built from a header and a body with a
|
|
8
8
|
* rule between them, so the padding belongs to those rather than to the card. `p-0` is the
|
|
9
9
|
* whole of the customisation.
|
|
10
10
|
*/
|
|
@@ -10,7 +10,7 @@ export interface DialogProps {
|
|
|
10
10
|
className?: string;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Themes' dialog, laid out the way
|
|
13
|
+
* Themes' dialog, laid out the way the house ones are: a titled header, a body, and a rule above
|
|
14
14
|
* the actions.
|
|
15
15
|
*
|
|
16
16
|
* The padding is taken off the content so those three bands can own it — Themes pads the
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Dialog as ThemedDialog } from '@radix-ui/themes';
|
|
3
3
|
import { cn } from '../../utils/cn.js';
|
|
4
4
|
/**
|
|
5
|
-
* Themes' dialog, laid out the way
|
|
5
|
+
* Themes' dialog, laid out the way the house ones are: a titled header, a body, and a rule above
|
|
6
6
|
* the actions.
|
|
7
7
|
*
|
|
8
8
|
* The padding is taken off the content so those three bands can own it — Themes pads the
|
package/dist/utils/theme.d.ts
CHANGED
|
@@ -7,27 +7,95 @@ export type Theme = (typeof THEMES)[number];
|
|
|
7
7
|
* "System" deliberately stamps *nothing*: the CSS falls through to `prefers-color-scheme`
|
|
8
8
|
* on its own, so following the operating system needs no listener here and keeps working
|
|
9
9
|
* when the user changes it mid-session.
|
|
10
|
+
*
|
|
11
|
+
* **Outside `createTheming` on purpose.** `data-theme` is the contract between this function
|
|
12
|
+
* and `tokens.css`, not a preference an app gets to set — two apps disagreeing about the
|
|
13
|
+
* attribute name would each need their own stylesheet, which is the thing this package exists
|
|
14
|
+
* to prevent.
|
|
10
15
|
*/
|
|
11
16
|
export declare function applyTheme(theme: Theme): void;
|
|
12
|
-
export declare function readTheme(): Theme;
|
|
13
|
-
export declare function storeTheme(theme: Theme): void;
|
|
14
17
|
/**
|
|
15
|
-
*
|
|
18
|
+
* A reasonable set of accents for an app that wants a picker and has no opinion.
|
|
16
19
|
*
|
|
17
20
|
* Six of Radix's twenty-six, not all of them: every one costs a colour scale in the bundle —
|
|
18
|
-
* see the note in
|
|
19
|
-
* choice than six that are obviously different from across a room.
|
|
20
|
-
* is Upfront's own, and the default for anyone who never opens the picker.
|
|
21
|
+
* see the `main.tsx` note in the README — and a page of near-identical greens is a worse
|
|
22
|
+
* choice than six that are obviously different from across a room.
|
|
21
23
|
*
|
|
22
|
-
* **
|
|
23
|
-
*
|
|
24
|
-
* they liked orange would have lost something. It does mean picking red or jade makes the
|
|
25
|
-
* accent and one semantic colour look alike, which is a fair trade for letting people choose.
|
|
24
|
+
* **It is a default, not the list.** Pass your own to `createTheming`; any Radix accent name
|
|
25
|
+
* works, and a product with one brand colour should pass just that one.
|
|
26
26
|
*
|
|
27
27
|
* **The labels are not here.** They are words, and words belong to an app's own dictionary —
|
|
28
28
|
* the same rule that keeps `UiStrings` down to two entries.
|
|
29
29
|
*/
|
|
30
|
+
export declare const DEFAULT_ACCENTS: readonly ["jade", "blue", "purple", "red", "orange", "yellow"];
|
|
31
|
+
/**
|
|
32
|
+
* @deprecated Prefer `DEFAULT_ACCENTS`, or your own list passed to `createTheming`. Kept
|
|
33
|
+
* because the old name is imported by name in existing apps; it is the same array.
|
|
34
|
+
*/
|
|
30
35
|
export declare const ACCENTS: readonly ["jade", "blue", "purple", "red", "orange", "yellow"];
|
|
31
|
-
|
|
32
|
-
export
|
|
33
|
-
export
|
|
36
|
+
/** The accents `DEFAULT_ACCENTS` offers. An app with its own list gets its own union. */
|
|
37
|
+
export type Accent = (typeof DEFAULT_ACCENTS)[number];
|
|
38
|
+
export interface ThemingConfig<A extends readonly string[]> {
|
|
39
|
+
/**
|
|
40
|
+
* The accents this product offers, in the order a picker should show them.
|
|
41
|
+
*
|
|
42
|
+
* ⚠ **Each one needs its Radix colour scale imported in the app's `main.tsx`**, or it
|
|
43
|
+
* resolves to nothing — a missing scale is not an error, it is a colourless app.
|
|
44
|
+
*/
|
|
45
|
+
accents: A;
|
|
46
|
+
/** The accent for anyone who has never opened the picker. Must be one of `accents`. */
|
|
47
|
+
defaultAccent: A[number];
|
|
48
|
+
/**
|
|
49
|
+
* Namespace for this product's `localStorage` keys, giving `<prefix>.theme` and
|
|
50
|
+
* `<prefix>.accent`.
|
|
51
|
+
*
|
|
52
|
+
* **Required, with no default, deliberately.** A default here would be some other
|
|
53
|
+
* product's name silently storing this one's preferences — and because it would work
|
|
54
|
+
* perfectly on a separate origin, nothing would ever reveal it.
|
|
55
|
+
*/
|
|
56
|
+
storagePrefix: string;
|
|
57
|
+
}
|
|
58
|
+
export interface Theming<A extends readonly string[]> {
|
|
59
|
+
/** The configured list, so a picker can render it without being handed it twice. */
|
|
60
|
+
readonly accents: A;
|
|
61
|
+
readonly defaultAccent: A[number];
|
|
62
|
+
readTheme(): Theme;
|
|
63
|
+
storeTheme(theme: Theme): void;
|
|
64
|
+
readAccent(): A[number];
|
|
65
|
+
storeAccent(accent: A[number]): void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Binds the theme and accent helpers to one product's accents, default and storage namespace.
|
|
69
|
+
*
|
|
70
|
+
* Three things used to be hardcoded here — the fallback accent, the list of accents, and the
|
|
71
|
+
* `localStorage` prefix — and all three were the same mistake: a shared package holding one
|
|
72
|
+
* product's identity. They move together rather than one at a time, because a product that can
|
|
73
|
+
* choose its default and not its palette has not really chosen anything.
|
|
74
|
+
*
|
|
75
|
+
* **A factory rather than a `configure()` call.** Module-level mutable state would have to be
|
|
76
|
+
* written before the first read, which makes correctness depend on import order — the same
|
|
77
|
+
* class of bug as this library's load-bearing CSS import order, and just as silent. An instance
|
|
78
|
+
* cannot be read before it is made.
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* export const theming = createTheming({
|
|
82
|
+
* accents: ['teal', 'crimson'],
|
|
83
|
+
* defaultAccent: 'teal',
|
|
84
|
+
* storagePrefix: 'myapp',
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* theming.readAccent(); // 'teal' | 'crimson'
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* `defaultAccent` is typed as a member of `accents`, so naming one that is not on the list is a
|
|
91
|
+
* compile error (`TS2322`) rather than a colourless first paint.
|
|
92
|
+
*
|
|
93
|
+
* The literal array needs no `as const` — the `const` type parameter infers the tuple. A list
|
|
94
|
+
* declared separately *does*, because `const list = ['teal']` widens to `string[]`, which is not
|
|
95
|
+
* a tuple and will be rejected: either inline it or write `as const` there.
|
|
96
|
+
*/
|
|
97
|
+
export declare function createTheming<const A extends readonly [string, ...string[]]>(config: ThemingConfig<A>): Theming<A>;
|
|
98
|
+
export declare const readTheme: () => Theme;
|
|
99
|
+
export declare const storeTheme: (theme: Theme) => void;
|
|
100
|
+
export declare const readAccent: () => "jade" | "red" | "yellow" | "orange" | "purple" | "blue";
|
|
101
|
+
export declare const storeAccent: (accent: "jade" | "red" | "yellow" | "orange" | "purple" | "blue") => void;
|
package/dist/utils/theme.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export const THEMES = ['system', 'light', 'dark'];
|
|
2
|
-
const STORAGE_KEY = 'upfront.theme';
|
|
3
2
|
/**
|
|
4
3
|
* Applies a theme by stamping the root element, or clearing it for "system".
|
|
5
4
|
*
|
|
@@ -7,6 +6,11 @@ const STORAGE_KEY = 'upfront.theme';
|
|
|
7
6
|
* "System" deliberately stamps *nothing*: the CSS falls through to `prefers-color-scheme`
|
|
8
7
|
* on its own, so following the operating system needs no listener here and keeps working
|
|
9
8
|
* when the user changes it mid-session.
|
|
9
|
+
*
|
|
10
|
+
* **Outside `createTheming` on purpose.** `data-theme` is the contract between this function
|
|
11
|
+
* and `tokens.css`, not a preference an app gets to set — two apps disagreeing about the
|
|
12
|
+
* attribute name would each need their own stylesheet, which is the thing this package exists
|
|
13
|
+
* to prevent.
|
|
10
14
|
*/
|
|
11
15
|
export function applyTheme(theme) {
|
|
12
16
|
const root = document.documentElement;
|
|
@@ -15,58 +19,117 @@ export function applyTheme(theme) {
|
|
|
15
19
|
else
|
|
16
20
|
root.setAttribute('data-theme', theme);
|
|
17
21
|
}
|
|
18
|
-
export function readTheme() {
|
|
19
|
-
try {
|
|
20
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
21
|
-
return THEMES.includes(stored) ? stored : 'system';
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
// Private windows and blocked site data both throw. A theme preference is not worth a
|
|
25
|
-
// crash, so the system default stands.
|
|
26
|
-
return 'system';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export function storeTheme(theme) {
|
|
30
|
-
try {
|
|
31
|
-
localStorage.setItem(STORAGE_KEY, theme);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
// See readTheme. It applies for this session either way.
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
22
|
/**
|
|
38
|
-
*
|
|
23
|
+
* A reasonable set of accents for an app that wants a picker and has no opinion.
|
|
39
24
|
*
|
|
40
25
|
* Six of Radix's twenty-six, not all of them: every one costs a colour scale in the bundle —
|
|
41
|
-
* see the note in
|
|
42
|
-
* choice than six that are obviously different from across a room.
|
|
43
|
-
* is Upfront's own, and the default for anyone who never opens the picker.
|
|
26
|
+
* see the `main.tsx` note in the README — and a page of near-identical greens is a worse
|
|
27
|
+
* choice than six that are obviously different from across a room.
|
|
44
28
|
*
|
|
45
|
-
* **
|
|
46
|
-
*
|
|
47
|
-
* they liked orange would have lost something. It does mean picking red or jade makes the
|
|
48
|
-
* accent and one semantic colour look alike, which is a fair trade for letting people choose.
|
|
29
|
+
* **It is a default, not the list.** Pass your own to `createTheming`; any Radix accent name
|
|
30
|
+
* works, and a product with one brand colour should pass just that one.
|
|
49
31
|
*
|
|
50
32
|
* **The labels are not here.** They are words, and words belong to an app's own dictionary —
|
|
51
33
|
* the same rule that keeps `UiStrings` down to two entries.
|
|
52
34
|
*/
|
|
53
|
-
export const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
35
|
+
export const DEFAULT_ACCENTS = ['jade', 'blue', 'purple', 'red', 'orange', 'yellow'];
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated Prefer `DEFAULT_ACCENTS`, or your own list passed to `createTheming`. Kept
|
|
38
|
+
* because the old name is imported by name in existing apps; it is the same array.
|
|
39
|
+
*/
|
|
40
|
+
export const ACCENTS = DEFAULT_ACCENTS;
|
|
41
|
+
/**
|
|
42
|
+
* Binds the theme and accent helpers to one product's accents, default and storage namespace.
|
|
43
|
+
*
|
|
44
|
+
* Three things used to be hardcoded here — the fallback accent, the list of accents, and the
|
|
45
|
+
* `localStorage` prefix — and all three were the same mistake: a shared package holding one
|
|
46
|
+
* product's identity. They move together rather than one at a time, because a product that can
|
|
47
|
+
* choose its default and not its palette has not really chosen anything.
|
|
48
|
+
*
|
|
49
|
+
* **A factory rather than a `configure()` call.** Module-level mutable state would have to be
|
|
50
|
+
* written before the first read, which makes correctness depend on import order — the same
|
|
51
|
+
* class of bug as this library's load-bearing CSS import order, and just as silent. An instance
|
|
52
|
+
* cannot be read before it is made.
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* export const theming = createTheming({
|
|
56
|
+
* accents: ['teal', 'crimson'],
|
|
57
|
+
* defaultAccent: 'teal',
|
|
58
|
+
* storagePrefix: 'myapp',
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* theming.readAccent(); // 'teal' | 'crimson'
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* `defaultAccent` is typed as a member of `accents`, so naming one that is not on the list is a
|
|
65
|
+
* compile error (`TS2322`) rather than a colourless first paint.
|
|
66
|
+
*
|
|
67
|
+
* The literal array needs no `as const` — the `const` type parameter infers the tuple. A list
|
|
68
|
+
* declared separately *does*, because `const list = ['teal']` widens to `string[]`, which is not
|
|
69
|
+
* a tuple and will be rejected: either inline it or write `as const` there.
|
|
70
|
+
*/
|
|
71
|
+
export function createTheming(config) {
|
|
72
|
+
const { accents, defaultAccent, storagePrefix } = config;
|
|
73
|
+
const themeKey = `${storagePrefix}.theme`;
|
|
74
|
+
const accentKey = `${storagePrefix}.accent`;
|
|
75
|
+
return {
|
|
76
|
+
accents,
|
|
77
|
+
defaultAccent,
|
|
78
|
+
readTheme() {
|
|
79
|
+
try {
|
|
80
|
+
const stored = localStorage.getItem(themeKey);
|
|
81
|
+
return THEMES.includes(stored) ? stored : 'system';
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Private windows and blocked site data both throw. A theme preference is not worth a
|
|
85
|
+
// crash, so the system default stands.
|
|
86
|
+
return 'system';
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
storeTheme(theme) {
|
|
90
|
+
try {
|
|
91
|
+
localStorage.setItem(themeKey, theme);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// See readTheme. It applies for this session either way.
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
readAccent() {
|
|
98
|
+
try {
|
|
99
|
+
const stored = localStorage.getItem(accentKey);
|
|
100
|
+
// Validated against the configured list, so a stored accent that was removed from a
|
|
101
|
+
// later release falls back rather than being handed to Radix as an unknown colour.
|
|
102
|
+
return accents.includes(stored) ? stored : defaultAccent;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Same as readTheme: a private window throws, and a colour is not worth a crash.
|
|
106
|
+
return defaultAccent;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
storeAccent(accent) {
|
|
110
|
+
try {
|
|
111
|
+
localStorage.setItem(accentKey, accent);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// It applies for this session either way.
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
72
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* The instance the original free functions are bound to: the six default accents, jade, and the
|
|
121
|
+
* `upfront.*` storage keys the apps that predate `createTheming` already wrote.
|
|
122
|
+
*
|
|
123
|
+
* **One implementation, not two.** These exports exist so no call site had to change when the
|
|
124
|
+
* factory landed, and keeping the prefix is what stops an existing user losing the theme they
|
|
125
|
+
* picked. A new product should call `createTheming` with its own name instead.
|
|
126
|
+
*/
|
|
127
|
+
const legacy = createTheming({
|
|
128
|
+
accents: DEFAULT_ACCENTS,
|
|
129
|
+
defaultAccent: 'jade',
|
|
130
|
+
storagePrefix: 'upfront',
|
|
131
|
+
});
|
|
132
|
+
export const readTheme = legacy.readTheme;
|
|
133
|
+
export const storeTheme = legacy.storeTheme;
|
|
134
|
+
export const readAccent = legacy.readAccent;
|
|
135
|
+
export const storeAccent = legacy.storeAccent;
|
package/package.json
CHANGED
package/tokens.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The house token bridge — the shared half of every consuming app's stylesheet.
|
|
3
3
|
*
|
|
4
4
|
* Two apps said `text-ink-muted` and `bg-brand-700/8` and each carried its own copy of what
|
|
5
5
|
* those meant. The values below are the dashboard's, which was the more complete of the two.
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* The house token names, registered with Tailwind.
|
|
40
40
|
*
|
|
41
41
|
* The values here are static fallbacks, not the live palette — the real assignments are in
|
|
42
42
|
* the `.radix-themes` rule below. Two blocks rather than one because of how custom
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* The live bridge:
|
|
91
|
+
* The live bridge: the house names, pointed at Radix Themes' palette.
|
|
92
92
|
*
|
|
93
93
|
* Every colour in the app now comes from Themes. This is the only place that names a Radix
|
|
94
94
|
* variable — the components keep saying `brand-700`, `ink-muted`, `surface`, and nothing
|