@pitlane/theme 0.2.0 → 0.3.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.
- package/CHANGELOG.md +127 -0
- package/README.md +66 -28
- package/dist/default.d.mts +605 -0
- package/dist/default.mjs +584 -0
- package/dist/dtcg.d.mts +127 -0
- package/dist/dtcg.mjs +468 -0
- package/dist/index.d.mts +76 -348
- package/dist/index.mjs +54 -458
- package/dist/schema-CRP607Pg.d.mts +320 -0
- package/dist/schema-JmfNnMzr.mjs +572 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +2 -0
- package/dist/theme-CaHfWYnM.d.mts +280 -0
- package/dist/theme-iDUjQhE0.mjs +253 -0
- package/package.json +58 -48
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { A as DimensionToken, B as UntypedToken, D as BrandByType, F as NumberToken, R as TokenType, T as AnyToken, a as TAG, i as SchemaTag, j as DurationToken, t as SELF } from "./schema-CRP607Pg.mjs";
|
|
2
|
+
import { Handle, RemixElement } from "remix/ui";
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A token value: the CSS it becomes. A string, a number, or an array
|
|
6
|
+
* of either. Anything else is a group.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link Tokens}
|
|
9
|
+
*/
|
|
10
|
+
type TokenValue = number | readonly (number | string)[] | string;
|
|
11
|
+
/**
|
|
12
|
+
* The token tree {@link createTheme} accepts: nested records whose
|
|
13
|
+
* leaves are {@link TokenValue}s. No reserved keys, so it is plain
|
|
14
|
+
* JSON and a designer can diff it.
|
|
15
|
+
*/
|
|
16
|
+
interface Tokens {
|
|
17
|
+
[key: string]: Tokens | TokenValue;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The accessor leaf for a token declared with `s.scale()`: callable
|
|
21
|
+
* for steps, with the base itself as `.token`.
|
|
22
|
+
*
|
|
23
|
+
* `.token` is what a projection re-roots and what `raw` resolves, so
|
|
24
|
+
* it is load-bearing rather than decorative.
|
|
25
|
+
*
|
|
26
|
+
* @see {@link TokenTree}
|
|
27
|
+
*/
|
|
28
|
+
interface ScaleFn {
|
|
29
|
+
(steps: number): DimensionToken;
|
|
30
|
+
readonly token: DimensionToken;
|
|
31
|
+
}
|
|
32
|
+
/** A theme's mode: a condition plus the values it overrides. */
|
|
33
|
+
interface ThemeMode<T> {
|
|
34
|
+
/**
|
|
35
|
+
* The media query this mode applies under. Defaults to
|
|
36
|
+
* `(prefers-color-scheme: <name>)` for the names `light` and
|
|
37
|
+
* `dark`, and is required for any other name.
|
|
38
|
+
*/
|
|
39
|
+
media?: string;
|
|
40
|
+
/**
|
|
41
|
+
* A selector this mode also applies under, for a user-selectable
|
|
42
|
+
* toggle. An attribute selector outranks the media block on
|
|
43
|
+
* specificity, so an explicit choice beats the OS preference.
|
|
44
|
+
*/
|
|
45
|
+
selector?: string;
|
|
46
|
+
/** The values this mode overrides. Structure only, never types. */
|
|
47
|
+
tokens: DeepPartialTokens<T>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The argument {@link createTheme} accepts: a schema tree, the token
|
|
51
|
+
* tree it describes, and any modes.
|
|
52
|
+
*
|
|
53
|
+
* The members are typed as bare objects on purpose. A `const` type
|
|
54
|
+
* parameter constrained to an index-signature type widens the literal
|
|
55
|
+
* it was inferred from, which would erase every brand; the shapes are
|
|
56
|
+
* enforced by the schema factories being typed values, by an
|
|
57
|
+
* undeclared leaf resolving to `unknown`, and by validation at module
|
|
58
|
+
* load.
|
|
59
|
+
*/
|
|
60
|
+
interface ThemeInit {
|
|
61
|
+
schema: object;
|
|
62
|
+
tokens: object;
|
|
63
|
+
modes?: object;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The argument `extend` accepts. `schema` is optional, because a patch
|
|
67
|
+
* that only adds tokens to an existing namespace needs no new entry.
|
|
68
|
+
*/
|
|
69
|
+
interface ThemePatch {
|
|
70
|
+
schema?: object;
|
|
71
|
+
tokens: object;
|
|
72
|
+
modes?: object;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The mode-override shape for a token tree: every group optional, every
|
|
76
|
+
* leaf a value. A mode overrides values, never structure or types.
|
|
77
|
+
*/
|
|
78
|
+
type DeepPartialTokens<T> = 0 extends 1 & T ? unknown : { [K in keyof T]?: T[K] extends Leaf ? TokenValue : DeepPartialTokens<T[K]>; };
|
|
79
|
+
type Leaf = number | readonly (number | string)[] | string;
|
|
80
|
+
type BrandOf<tag> = [tag] extends [never] ? unknown : [tag] extends ["any"] ? UntypedToken : [tag] extends ["scale"] ? ScaleFn : tag extends TokenType ? BrandByType[tag] : unknown;
|
|
81
|
+
/** The tag a schema node declares for itself and its unlabelled children. */
|
|
82
|
+
type NodeTag<S, Inherited> = S extends {
|
|
83
|
+
readonly [TAG]: infer tag extends SchemaTag;
|
|
84
|
+
} ? tag : S extends {
|
|
85
|
+
readonly [SELF]: {
|
|
86
|
+
readonly [TAG]: infer tag extends SchemaTag;
|
|
87
|
+
};
|
|
88
|
+
} ? tag : Inherited;
|
|
89
|
+
type Child<S, K> = K extends keyof S ? S[K] : undefined;
|
|
90
|
+
type LeafTag<S, Inherited> = S extends {
|
|
91
|
+
readonly [TAG]: infer tag extends SchemaTag;
|
|
92
|
+
} ? tag : Inherited;
|
|
93
|
+
type TreeOf<Tok, Sch, Inherited> = { [K in keyof Tok]: Tok[K] extends Leaf ? BrandOf<LeafTag<Child<Sch, K>, Inherited>> : TreeOf<Tok[K], Child<Sch, K>, NodeTag<Child<Sch, K>, Inherited>>; };
|
|
94
|
+
/**
|
|
95
|
+
* The accessor shape for an init `T`: the same nesting as its token
|
|
96
|
+
* tree, with every leaf replaced by the branded `var(--…)` reference
|
|
97
|
+
* its schema declares. Numeric keys index with brackets
|
|
98
|
+
* (`t.color.gray[900]`).
|
|
99
|
+
*
|
|
100
|
+
* A leaf whose schema entry is missing resolves to `unknown`, which is
|
|
101
|
+
* unusable in `css()`. The compiler also throws for it at module load.
|
|
102
|
+
*
|
|
103
|
+
* @see {@link ThemeResult}
|
|
104
|
+
*/
|
|
105
|
+
type TokenTree<T> = 0 extends 1 & T ? unknown : T extends {
|
|
106
|
+
schema: infer Sch;
|
|
107
|
+
tokens: infer Tok;
|
|
108
|
+
} ? TreeOf<Tok, Sch, never> : unknown;
|
|
109
|
+
/**
|
|
110
|
+
* Deep-merges an `extend` patch onto the tree it extends. A leaf
|
|
111
|
+
* replaces wholesale; every other node recurses.
|
|
112
|
+
*
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
type DeepMerge<A, B> = { [K in keyof A | keyof B]: K extends keyof B ? K extends keyof A ? B[K] extends Leaf ? B[K] : A[K] extends Leaf ? B[K] : DeepMerge<A[K], B[K]> : B[K] : K extends keyof A ? A[K] : never; };
|
|
116
|
+
/**
|
|
117
|
+
* The init an `extend` produces: both trees merged.
|
|
118
|
+
*
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
interface Merged<T, E> {
|
|
122
|
+
schema: DeepMerge<T extends {
|
|
123
|
+
schema: infer S;
|
|
124
|
+
} ? S : {}, E extends {
|
|
125
|
+
schema: infer S;
|
|
126
|
+
} ? S : {}>;
|
|
127
|
+
tokens: DeepMerge<T extends {
|
|
128
|
+
tokens: infer S;
|
|
129
|
+
} ? S : {}, E extends {
|
|
130
|
+
tokens: infer S;
|
|
131
|
+
} ? S : {}>;
|
|
132
|
+
}
|
|
133
|
+
/** The bases {@link scale} accepts. @internal */
|
|
134
|
+
type ScalableToken = DimensionToken | DurationToken | NumberToken;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/theme.d.ts
|
|
137
|
+
/**
|
|
138
|
+
* Props for the {@link ThemeComponent}. `nonce` sets the `nonce`
|
|
139
|
+
* attribute on the emitted `<style>` element for Content Security
|
|
140
|
+
* Policy setups.
|
|
141
|
+
*/
|
|
142
|
+
type ThemeProps = {
|
|
143
|
+
nonce?: string;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* The `<Theme />` component {@link createTheme} returns. Render it
|
|
147
|
+
* once near the document root to install the custom properties.
|
|
148
|
+
*
|
|
149
|
+
* It is an ordinary component with one extra property, `$theme`,
|
|
150
|
+
* holding the init it was compiled from, so a published theme can be
|
|
151
|
+
* handed straight back to {@link createTheme}.
|
|
152
|
+
*/
|
|
153
|
+
interface ThemeComponent<T> {
|
|
154
|
+
(handle: Handle<ThemeProps>): () => RemixElement;
|
|
155
|
+
readonly $theme: T;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The object {@link createTheme} returns: the typed token accessor,
|
|
159
|
+
* the `raw` resolver, the `<Theme />` component, and the two
|
|
160
|
+
* derivation methods.
|
|
161
|
+
*/
|
|
162
|
+
interface ThemeResult<T> {
|
|
163
|
+
/**
|
|
164
|
+
* The compiled CSS text `<Theme />` installs. Exposed for tests
|
|
165
|
+
* and for a build step that emits it as an asset.
|
|
166
|
+
*
|
|
167
|
+
* @internal
|
|
168
|
+
*/
|
|
169
|
+
readonly cssText: string;
|
|
170
|
+
/**
|
|
171
|
+
* A typed mirror of the token tree whose leaves are branded
|
|
172
|
+
* `var()` reference strings. A `s.scale()` leaf is callable.
|
|
173
|
+
*/
|
|
174
|
+
readonly token: TokenTree<T>;
|
|
175
|
+
/**
|
|
176
|
+
* Resolves a token reference to its concrete base value, following
|
|
177
|
+
* references to the end.
|
|
178
|
+
*
|
|
179
|
+
* @param ref - A token reference minted by this theme
|
|
180
|
+
* @returns The serialized base value
|
|
181
|
+
* @throws ThemeError when the reference was not minted here
|
|
182
|
+
*/
|
|
183
|
+
raw(ref: AnyToken | UntypedToken): string;
|
|
184
|
+
/** The component that installs the custom properties. */
|
|
185
|
+
readonly Theme: ThemeComponent<T>;
|
|
186
|
+
/**
|
|
187
|
+
* Deep-merges a patch onto this theme and returns a new one. A
|
|
188
|
+
* leaf replaces wholesale; every other node recurses.
|
|
189
|
+
*
|
|
190
|
+
* The callback form receives this theme's accessor, so a patch can
|
|
191
|
+
* reference what it extends.
|
|
192
|
+
*
|
|
193
|
+
* @param patch - The tokens and schema to merge, or a callback returning them
|
|
194
|
+
* @returns A new theme
|
|
195
|
+
* @throws ThemeError when the patch holds no `tokens` group
|
|
196
|
+
*/
|
|
197
|
+
extend<const schema extends object, const tokens extends object>(patch: ExtendPatch<T, schema, tokens> | ((base: TokenTree<T>) => ExtendPatch<T, schema, tokens>)): ThemeResult<Merged<T, {
|
|
198
|
+
schema: schema;
|
|
199
|
+
tokens: tokens;
|
|
200
|
+
}>>;
|
|
201
|
+
/**
|
|
202
|
+
* Replaces this theme with a projection of it. The callback
|
|
203
|
+
* receives this theme's accessor; every value in the projection is
|
|
204
|
+
* a reference into it, and the new path decides the new custom
|
|
205
|
+
* property name, so a projection may also reshape and rename.
|
|
206
|
+
*
|
|
207
|
+
* @param projection - A callback returning the schema and tokens to keep
|
|
208
|
+
* @returns A new theme
|
|
209
|
+
* @throws ThemeError when the projection holds no `tokens` group
|
|
210
|
+
*/
|
|
211
|
+
select<const P extends ThemeInit>(projection: (base: TokenTree<T>) => P): ThemeResult<P>;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* An `extend` patch. Its modes are checked against the merged token tree rather
|
|
215
|
+
* than the patch's own, because a layer whose only job is to override a base
|
|
216
|
+
* token in a mode declares no tokens of its own.
|
|
217
|
+
*/
|
|
218
|
+
type ExtendPatch<T, schema, tokens> = {
|
|
219
|
+
schema?: schema;
|
|
220
|
+
tokens: tokens;
|
|
221
|
+
modes?: Record<string, ThemeMode<T extends {
|
|
222
|
+
tokens: infer base;
|
|
223
|
+
} ? DeepMerge<base, tokens> : tokens>>;
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Compiles a theme from a schema tree and the token tree it describes.
|
|
227
|
+
*
|
|
228
|
+
* Values are the CSS they become: a color is any CSS color, a
|
|
229
|
+
* dimension is any CSS length, and a composite is the shorthand text.
|
|
230
|
+
* The schema names each token's type, which is what the accessor's
|
|
231
|
+
* compile-time brands are read from and what `css()` enforces.
|
|
232
|
+
*
|
|
233
|
+
* A reference to another token is a property access on the layer below,
|
|
234
|
+
* so it needs an {@link ThemeResult.extend} layer. The accessor leaf is
|
|
235
|
+
* already a `var()` string, which keeps its indirection in the emitted
|
|
236
|
+
* CSS so a mode override cascades through it. There is no string syntax
|
|
237
|
+
* for a reference; a leftover `"{a.b.c}"` raises {@link ThemeError}.
|
|
238
|
+
*
|
|
239
|
+
* All validation and serialization happen eagerly: a malformed theme
|
|
240
|
+
* throws at module load rather than emitting broken CSS. Bad values
|
|
241
|
+
* raise `ValidationError` from `remix/data-schema`, carrying one issue
|
|
242
|
+
* per bad token with its path. Structural problems raise
|
|
243
|
+
* {@link ThemeError}.
|
|
244
|
+
*
|
|
245
|
+
* @param init - The schema, tokens, and modes, or a `<Theme />` to re-derive from
|
|
246
|
+
* @returns The {@link ThemeResult}
|
|
247
|
+
* @throws ThemeError on a structural failure
|
|
248
|
+
* @throws ValidationError on one or more invalid values
|
|
249
|
+
*
|
|
250
|
+
* @see {@link ThemeInit} for the accepted shape.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* import { createTheme } from "@pitlane/theme";
|
|
255
|
+
* import * as s from "@pitlane/theme/schema";
|
|
256
|
+
*
|
|
257
|
+
* export let { token: t, raw, Theme } = createTheme({
|
|
258
|
+
* schema: { color: s.color(), spacing: s.scale() },
|
|
259
|
+
* tokens: { color: { white: "#fff" }, spacing: "0.25rem" },
|
|
260
|
+
* }).extend(base => ({
|
|
261
|
+
* schema: { color: s.color() },
|
|
262
|
+
* tokens: { color: { page: base.color.white } },
|
|
263
|
+
* }));
|
|
264
|
+
*
|
|
265
|
+
* t.color.page; // "var(--color-page)"
|
|
266
|
+
* raw(t.color.page); // "#fff"
|
|
267
|
+
* t.spacing(4); // "calc(var(--spacing) * 4)"
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
declare function createTheme<const schema extends object, const tokens extends object>(init: {
|
|
271
|
+
schema: schema;
|
|
272
|
+
tokens: tokens;
|
|
273
|
+
modes?: Record<string, ThemeMode<tokens>>;
|
|
274
|
+
}): ThemeResult<{
|
|
275
|
+
schema: schema;
|
|
276
|
+
tokens: tokens;
|
|
277
|
+
}>;
|
|
278
|
+
declare function createTheme<T>(theme: ThemeComponent<T>): ThemeResult<T>;
|
|
279
|
+
//#endregion
|
|
280
|
+
export { DeepPartialTokens as a, ScaleFn as c, ThemePatch as d, TokenTree as f, createTheme as i, ThemeInit as l, Tokens as m, ThemeProps as n, Merged as o, TokenValue as p, ThemeResult as r, ScalableToken as s, ThemeComponent as t, ThemeMode as u };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { C as assertNoStringReference, E as mentionedVarNames, S as ThemeError, T as kebabSegment, _ as selfSchema, a as childSchema, h as pathKey, p as isTokenSchema, w as collectTokens, x as serializeValue } from "./schema-JmfNnMzr.mjs";
|
|
2
|
+
import { createElement } from "remix/ui";
|
|
3
|
+
import { createSchema, fail, object, parse } from "remix/data-schema";
|
|
4
|
+
//#region src/validate.ts
|
|
5
|
+
/**
|
|
6
|
+
* Composes the sparse schema tree and the token tree into one
|
|
7
|
+
* `remix/data-schema` object schema, so a single `parse` validates
|
|
8
|
+
* every token and reports each failure with its own path.
|
|
9
|
+
*
|
|
10
|
+
* A token with no schema entry gets a schema that always fails, so a
|
|
11
|
+
* missing declaration is reported alongside any bad values rather than
|
|
12
|
+
* short-circuiting them.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
function composeSchema(tokens, schema) {
|
|
17
|
+
return compose(tokens, schema, void 0);
|
|
18
|
+
}
|
|
19
|
+
function compose(tokens, schema, inherited) {
|
|
20
|
+
let shape = {};
|
|
21
|
+
for (let [key, value] of Object.entries(tokens)) {
|
|
22
|
+
let child = childSchema(schema, key);
|
|
23
|
+
let own = selfSchema(child) ?? inherited;
|
|
24
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) shape[key] = compose(value, child, own);
|
|
25
|
+
else if (own === void 0) shape[key] = createSchema((_, context) => fail(`"${pathKey(context.path)}" has no schema entry`, context.path));
|
|
26
|
+
else shape[key] = own;
|
|
27
|
+
}
|
|
28
|
+
return object(shape);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/theme.ts
|
|
32
|
+
const REF_RE = /^var\((--[a-z0-9-]+)\)$/;
|
|
33
|
+
function createTheme(input) {
|
|
34
|
+
let init = typeof input === "function" ? input.$theme : input;
|
|
35
|
+
assertTokensGroup(init, "createTheme()", "createTheme({ schema: { … }, tokens: { … } })");
|
|
36
|
+
return compile(init);
|
|
37
|
+
}
|
|
38
|
+
function compile(init) {
|
|
39
|
+
parse(composeSchema(init.tokens, init.schema), init.tokens, { abortEarly: false });
|
|
40
|
+
let entries = collectTokens(init.tokens, init.schema);
|
|
41
|
+
let byKey = new Map(entries.map((entry) => [entry.key, entry]));
|
|
42
|
+
let byVarName = new Map(entries.map((entry) => [entry.varName, entry]));
|
|
43
|
+
let ctx = referenceContext(byKey);
|
|
44
|
+
let cssText = buildCssText(entries.map((entry) => [entry.varName, declare(entry, ctx)]), modeBlocks(init.modes, byKey, ctx));
|
|
45
|
+
return {
|
|
46
|
+
cssText,
|
|
47
|
+
token: buildAccessor(entries),
|
|
48
|
+
Theme: createThemeComponent(cssText, init),
|
|
49
|
+
raw(ref) {
|
|
50
|
+
return resolveRaw(ref, byVarName, byKey, []);
|
|
51
|
+
},
|
|
52
|
+
extend(patch) {
|
|
53
|
+
let accessor = buildAccessor(entries);
|
|
54
|
+
let next = typeof patch === "function" ? patch(accessor) : patch;
|
|
55
|
+
assertTokensGroup(next, "extend()", ".extend(base => ({ schema: { … }, tokens: { … } }))");
|
|
56
|
+
return compile({
|
|
57
|
+
schema: mergeDeep(init.schema, next.schema ?? {}),
|
|
58
|
+
tokens: mergeDeep(init.tokens, next.tokens),
|
|
59
|
+
modes: {
|
|
60
|
+
...init.modes,
|
|
61
|
+
...next.modes
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
select(projection) {
|
|
66
|
+
let next = projection(buildAccessor(entries));
|
|
67
|
+
assertTokensGroup(next, "select()", ".select(base => ({ schema: { … }, tokens: { … } }))");
|
|
68
|
+
let projected = reroot(next.tokens, byVarName);
|
|
69
|
+
assertNoDroppedReferences(projected, byVarName);
|
|
70
|
+
return compile({
|
|
71
|
+
schema: next.schema,
|
|
72
|
+
tokens: projected,
|
|
73
|
+
modes: next.modes
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function referenceContext(byKey) {
|
|
79
|
+
return { varRefFor(key, from, expected) {
|
|
80
|
+
let target = byKey.get(key);
|
|
81
|
+
if (target === void 0) throw new ThemeError(`"${from}" references unknown token "${key}"`);
|
|
82
|
+
if (target.kind === "untyped") throw new ThemeError(`"${from}" references untyped token "${key}"`);
|
|
83
|
+
let type = target.kind === "scale" ? "dimension" : target.type;
|
|
84
|
+
if (type !== expected) throw new ThemeError(`"${from}" references "${key}" of type "${type}" where "${expected}" is required`);
|
|
85
|
+
return `var(${target.varName})`;
|
|
86
|
+
} };
|
|
87
|
+
}
|
|
88
|
+
function declare(entry, ctx) {
|
|
89
|
+
if (entry.kind === "untyped") return entry.value;
|
|
90
|
+
if (entry.kind === "scale") return serializeValue("dimension", entry.value, ctx, entry.key);
|
|
91
|
+
if (entry.aliasOf !== void 0) return ctx.varRefFor(entry.aliasOf, entry.key, entry.type);
|
|
92
|
+
return serializeValue(entry.type, entry.value, ctx, entry.key);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A schema tree, a patch, a projection, and a mode all carry their token tree
|
|
96
|
+
* under a `tokens` key, and leaving the wrapper off is a plausible mistake:
|
|
97
|
+
* 0.2.0 took a bare document, and the wrapper is what the 0.3.0 migration
|
|
98
|
+
* teaches for modes. Without this the bare tree reaches the compiler, where it
|
|
99
|
+
* fails as `Object.entries(undefined)` from inside a bundled chunk.
|
|
100
|
+
*/
|
|
101
|
+
function assertTokensGroup(node, label, example) {
|
|
102
|
+
if (!isPlainRecord(node) || !("tokens" in node)) throw new ThemeError(`${label} is missing its "tokens" group: ${example}`);
|
|
103
|
+
if (!isPlainRecord(node.tokens)) throw new ThemeError(`${label} has a "tokens" that is not a group`);
|
|
104
|
+
}
|
|
105
|
+
function modeBlocks(modes, byKey, ctx) {
|
|
106
|
+
if (modes === void 0) return [];
|
|
107
|
+
let selectorBlocks = [];
|
|
108
|
+
let mediaBlocks = [];
|
|
109
|
+
for (let [name, mode] of Object.entries(modes)) {
|
|
110
|
+
assertTokensGroup(mode, `Mode "${name}"`, `modes: { ${name}: { tokens: { … } } }`);
|
|
111
|
+
let overrides = [];
|
|
112
|
+
walkMode(mode.tokens, [], name, byKey, ctx, overrides);
|
|
113
|
+
if (overrides.length === 0) continue;
|
|
114
|
+
if (mode.selector !== void 0) {
|
|
115
|
+
let lines = overrides.map(([varName, value]) => ` ${varName}: ${value};`);
|
|
116
|
+
selectorBlocks.push(`${mode.selector} {\n${lines.join("\n")}\n}`);
|
|
117
|
+
}
|
|
118
|
+
let media = mode.media ?? `(prefers-color-scheme: ${name})`;
|
|
119
|
+
let lines = overrides.map(([varName, value]) => ` ${varName}: ${value};`);
|
|
120
|
+
mediaBlocks.push(`@media ${media} {\n :root {\n${lines.join("\n")}\n }\n}`);
|
|
121
|
+
}
|
|
122
|
+
return [...selectorBlocks, ...mediaBlocks];
|
|
123
|
+
}
|
|
124
|
+
function walkMode(node, path, mode, byKey, ctx, out) {
|
|
125
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) throw new ThemeError(`Mode "${mode}" override at "${path.join(".")}" is not a group`);
|
|
126
|
+
for (let [key, value] of Object.entries(node)) {
|
|
127
|
+
let childPath = [...path, key];
|
|
128
|
+
let childKey = childPath.join(".");
|
|
129
|
+
let entry = byKey.get(childKey);
|
|
130
|
+
if (entry === void 0) {
|
|
131
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
132
|
+
walkMode(value, childPath, mode, byKey, ctx, out);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
throw new ThemeError(`Mode "${mode}" overrides unknown token "${childKey}"`);
|
|
136
|
+
}
|
|
137
|
+
if (entry.kind === "untyped") {
|
|
138
|
+
out.push([entry.varName, String(value)]);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
assertNoStringReference(value, childKey);
|
|
142
|
+
let type = entry.kind === "scale" ? "dimension" : entry.type;
|
|
143
|
+
out.push([entry.varName, serializeValue(type, value, ctx, childKey)]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function resolveRaw(ref, byVarName, byKey, chain) {
|
|
147
|
+
let match = REF_RE.exec(ref);
|
|
148
|
+
let entry = match === null ? void 0 : byVarName.get(match[1]);
|
|
149
|
+
if (entry === void 0) throw new ThemeError(`"${ref}" was not minted by this theme`);
|
|
150
|
+
if (chain.includes(entry.key)) throw new ThemeError(`Reference cycle: ${[...chain, entry.key].join(" → ")}`);
|
|
151
|
+
if (entry.kind === "typed" && entry.aliasOf !== void 0) {
|
|
152
|
+
let target = byKey.get(entry.aliasOf);
|
|
153
|
+
if (target === void 0) throw new ThemeError(`"${entry.key}" references unknown token "${entry.aliasOf}"`);
|
|
154
|
+
return resolveRaw(`var(${target.varName})`, byVarName, byKey, [...chain, entry.key]);
|
|
155
|
+
}
|
|
156
|
+
if (entry.kind === "untyped") return entry.value;
|
|
157
|
+
return serializeValue(entry.kind === "scale" ? "dimension" : entry.type, entry.value, { varRefFor: (key, from, expected) => {
|
|
158
|
+
let target = byKey.get(key);
|
|
159
|
+
if (target === void 0) throw new ThemeError(`"${from}" references unknown token "${key}"`);
|
|
160
|
+
return resolveRaw(`var(${target.varName})`, byVarName, byKey, [...chain, entry.key]);
|
|
161
|
+
} }, entry.key);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A projected value may itself be a `var()` reference to a token the projection
|
|
165
|
+
* left behind, which the cascade would resolve to nothing. The reference is
|
|
166
|
+
* detectable because its variable belonged to the theme being projected from.
|
|
167
|
+
*/
|
|
168
|
+
function assertNoDroppedReferences(tokens, sourceByVarName) {
|
|
169
|
+
let kept = /* @__PURE__ */ new Set();
|
|
170
|
+
let walkValues = (node, path) => {
|
|
171
|
+
if (typeof node === "object" && node !== null && !Array.isArray(node)) {
|
|
172
|
+
for (let [key, value] of Object.entries(node)) walkValues(value, [...path, key]);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (let varName of mentionedVarNames(node)) {
|
|
176
|
+
let source = sourceByVarName.get(varName);
|
|
177
|
+
if (source !== void 0 && !kept.has(source.varName)) throw new ThemeError(`"${path.join(".")}" references "${source.key}", which the projection dropped`);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
let record = (node, path) => {
|
|
181
|
+
if (typeof node === "object" && node !== null && !Array.isArray(node)) {
|
|
182
|
+
for (let [key, value] of Object.entries(node)) record(value, [...path, key]);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
kept.add(`--${path.map(kebabSegment).join("-")}`);
|
|
186
|
+
};
|
|
187
|
+
record(tokens, []);
|
|
188
|
+
walkValues(tokens, []);
|
|
189
|
+
}
|
|
190
|
+
function buildAccessor(entries) {
|
|
191
|
+
let root = Object.create(null);
|
|
192
|
+
for (let entry of entries) {
|
|
193
|
+
let node = root;
|
|
194
|
+
for (let segment of entry.path.slice(0, -1)) {
|
|
195
|
+
let next = node[segment];
|
|
196
|
+
if (next === void 0) {
|
|
197
|
+
next = Object.create(null);
|
|
198
|
+
node[segment] = next;
|
|
199
|
+
}
|
|
200
|
+
node = next;
|
|
201
|
+
}
|
|
202
|
+
let ref = `var(${entry.varName})`;
|
|
203
|
+
node[entry.path.at(-1)] = entry.kind === "scale" ? scaleLeaf(ref) : ref;
|
|
204
|
+
}
|
|
205
|
+
return root;
|
|
206
|
+
}
|
|
207
|
+
function scaleLeaf(ref) {
|
|
208
|
+
let fn = (steps) => `calc(${ref} * ${steps})`;
|
|
209
|
+
return Object.assign(fn, { token: ref });
|
|
210
|
+
}
|
|
211
|
+
function buildCssText(declarations, blocks) {
|
|
212
|
+
let lines = declarations.map(([name, value]) => ` ${name}: ${value};`);
|
|
213
|
+
if (declarations.some(([, value]) => value.includes("light-dark("))) lines.unshift(" color-scheme: light dark;");
|
|
214
|
+
return [`:root {\n${lines.join("\n")}\n}`, ...blocks].join("\n\n");
|
|
215
|
+
}
|
|
216
|
+
function createThemeComponent(cssText, init) {
|
|
217
|
+
let escaped = cssText.replaceAll("</style", "<\\/style");
|
|
218
|
+
let component = (handle) => () => createElement("style", {
|
|
219
|
+
nonce: handle.props.nonce,
|
|
220
|
+
"data-pitlane-theme": "",
|
|
221
|
+
innerHTML: escaped
|
|
222
|
+
});
|
|
223
|
+
return Object.assign(component, { $theme: init });
|
|
224
|
+
}
|
|
225
|
+
function mergeDeep(a, b) {
|
|
226
|
+
if (!isPlainRecord(a) || !isPlainRecord(b)) return b;
|
|
227
|
+
let out = { ...a };
|
|
228
|
+
for (let [key, value] of Object.entries(b)) out[key] = mergeDeep(out[key], value);
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
function isPlainRecord(value) {
|
|
232
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !isTokenSchema(value);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* A projection holds accessor references, so each one resolves back to
|
|
236
|
+
* the value its token holds. A scale leaf is projected through its
|
|
237
|
+
* `.token`, which is an ordinary reference by the time it arrives.
|
|
238
|
+
*/
|
|
239
|
+
function reroot(node, byVarName) {
|
|
240
|
+
if (typeof node === "object" && node !== null && !Array.isArray(node)) {
|
|
241
|
+
let out = {};
|
|
242
|
+
for (let [key, value] of Object.entries(node)) out[key] = reroot(value, byVarName);
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
if (typeof node !== "string") return node;
|
|
246
|
+
let match = REF_RE.exec(node);
|
|
247
|
+
if (match === null) return node;
|
|
248
|
+
let source = byVarName.get(match[1]);
|
|
249
|
+
if (source === void 0) throw new ThemeError(`"${node}" was not minted by the theme being selected from`);
|
|
250
|
+
return source.value;
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
export { createTheme as t };
|
package/package.json
CHANGED
|
@@ -1,53 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
"name": "@pitlane/theme",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Type-safe styling with W3C design tokens for Remix 3.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"css-variables",
|
|
7
|
+
"design-tokens",
|
|
8
|
+
"dtcg",
|
|
9
|
+
"pitlane",
|
|
10
|
+
"remix",
|
|
11
|
+
"theme"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://pitlane.tools/package/theme/",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/pitlane-tools/pitlane/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Mark Malstrom <mark@malstrom.me>",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/pitlane-tools/pitlane.git",
|
|
22
|
+
"directory": "packages/theme"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"CHANGELOG.md"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.mts",
|
|
33
|
+
"import": "./dist/index.mjs"
|
|
16
34
|
},
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/pitlane-tools/pitlane.git",
|
|
22
|
-
"directory": "packages/theme"
|
|
35
|
+
"./schema": {
|
|
36
|
+
"types": "./dist/schema.d.mts",
|
|
37
|
+
"import": "./dist/schema.mjs"
|
|
23
38
|
},
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
],
|
|
28
|
-
"type": "module",
|
|
29
|
-
"types": "./dist/index.d.mts",
|
|
30
|
-
"exports": {
|
|
31
|
-
".": {
|
|
32
|
-
"types": "./dist/index.d.mts",
|
|
33
|
-
"import": "./dist/index.mjs"
|
|
34
|
-
}
|
|
39
|
+
"./default": {
|
|
40
|
+
"types": "./dist/default.d.mts",
|
|
41
|
+
"import": "./dist/default.mjs"
|
|
35
42
|
},
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"csstype": "^3.2.3"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"remix": "3.0.0-beta.10",
|
|
44
|
-
"typescript": "^7.0.2",
|
|
45
|
-
"vite-plus": "^0.2.6"
|
|
46
|
-
},
|
|
47
|
-
"peerDependencies": {
|
|
48
|
-
"remix": "^3.0.0-beta.10"
|
|
49
|
-
},
|
|
50
|
-
"engines": {
|
|
51
|
-
"node": "^20.19.0 || >=22.12.0"
|
|
43
|
+
"./dtcg": {
|
|
44
|
+
"types": "./dist/dtcg.d.mts",
|
|
45
|
+
"import": "./dist/dtcg.mjs"
|
|
52
46
|
}
|
|
53
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"csstype": "^3.2.3"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"remix": "3.0.0-beta.10",
|
|
53
|
+
"typescript": "^7.0.2",
|
|
54
|
+
"vite-plus": "^0.2.6"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"remix": "^3.0.0-beta.10"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {}
|
|
63
|
+
}
|