@untheme/core 0.1.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/.dist/index.d.mts +267 -0
- package/.dist/index.d.ts +267 -0
- package/.dist/index.mjs +261 -0
- package/README.md +88 -0
- package/package.json +28 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { SchemaError, Issue, Template, Theme, Input, Overrides, Layer, Schema, Modifier, Token, Binding, Values, Open, Type, Context, Patch, Contract } from '@untheme/schema';
|
|
2
|
+
import { Diff } from '@untheme/utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Raised when the base theme handed to {@link defineUntheme} violates its own
|
|
6
|
+
* contract. Extends {@link SchemaError}, so it carries the underlying
|
|
7
|
+
* {@link Issue}s while naming *which* boundary rejected the value.
|
|
8
|
+
*/
|
|
9
|
+
declare class InvalidThemeError extends SchemaError {
|
|
10
|
+
constructor(issues: Issue[]);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Raised when a layer — a registry seed, or an argument to {@link apply} /
|
|
14
|
+
* {@link create} — steps outside the contract. Carries the {@link Issue}s of
|
|
15
|
+
* the failed {@link SchemaError} it wraps.
|
|
16
|
+
*/
|
|
17
|
+
declare class InvalidLayerError extends SchemaError {
|
|
18
|
+
constructor(issues: Issue[]);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Raised when a patch handed to {@link update} steps outside the contract.
|
|
22
|
+
* Carries the {@link Issue}s of the failed {@link SchemaError} it wraps.
|
|
23
|
+
*/
|
|
24
|
+
declare class InvalidPatchError extends SchemaError {
|
|
25
|
+
constructor(issues: Issue[]);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Raised when {@link Untheme.select} is handed a key that names no theme in the
|
|
29
|
+
* registry. Unlike the validation errors above this carries no {@link Issue}s —
|
|
30
|
+
* a missing key is a lookup miss, not a contract violation — so it extends the
|
|
31
|
+
* plain {@link Error} while still giving the failure a semantic identity and
|
|
32
|
+
* the offending `key`.
|
|
33
|
+
*/
|
|
34
|
+
declare class UnknownThemeError extends Error {
|
|
35
|
+
readonly key: string;
|
|
36
|
+
constructor(key: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Raised when {@link Untheme.contexts} is handed a name the contract declares
|
|
40
|
+
* no modifier under. Like {@link UnknownThemeError} this is a lookup miss, not
|
|
41
|
+
* a contract violation, so it extends the plain {@link Error} while carrying
|
|
42
|
+
* the offending `modifier`.
|
|
43
|
+
*/
|
|
44
|
+
declare class UnknownModifierError extends Error {
|
|
45
|
+
readonly modifier: string;
|
|
46
|
+
constructor(modifier: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Raised when {@link Untheme.resolve} follows an alias chain that loops back on
|
|
50
|
+
* itself. Like {@link UnknownThemeError} it carries no {@link Issue}s — a cycle
|
|
51
|
+
* is a resolution failure, not a contract violation — so it extends the plain
|
|
52
|
+
* {@link Error}, carrying the `chain` of token names up to and including the
|
|
53
|
+
* repeat. Detected by tracking visited tokens, so the stack never overflows.
|
|
54
|
+
*/
|
|
55
|
+
declare class CircularAliasError extends Error {
|
|
56
|
+
readonly chain: string[];
|
|
57
|
+
constructor(chain: string[]);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Runs `fn`, and if it throws a {@link SchemaError}, re-throws it as the given
|
|
61
|
+
* semantic subclass carrying the same {@link Issue}s; anything else propagates
|
|
62
|
+
* untouched. Lets the service speak in {@link InvalidThemeError} /
|
|
63
|
+
* {@link InvalidLayerError} / {@link InvalidPatchError} while the schema layer
|
|
64
|
+
* stays generic.
|
|
65
|
+
*/
|
|
66
|
+
declare const reframe: <T>(Semantic: new (issues: Issue[]) => SchemaError, fn: () => T) => T;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The caller-owned live state an {@link Untheme} service reads and writes: the
|
|
70
|
+
* active theme definition, the active selection (one context per modifier), and
|
|
71
|
+
* the user override layer that `set` populates. Pass a plain object for inert
|
|
72
|
+
* state, or a reactive proxy to have reads and writes tracked.
|
|
73
|
+
*
|
|
74
|
+
* `theme` reads as the caller's own contract type but writes accept any
|
|
75
|
+
* complete {@link Theme} of that contract: `update` and `apply` store merged
|
|
76
|
+
* themes, which satisfy the contract without being the caller's exact type. A
|
|
77
|
+
* plain `{ theme, input, override }` object satisfies both sides.
|
|
78
|
+
*/
|
|
79
|
+
type Config<T extends Template> = {
|
|
80
|
+
get theme(): T;
|
|
81
|
+
set theme(value: Theme<T>);
|
|
82
|
+
input: Input<T>;
|
|
83
|
+
override: Overrides<T>;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Read/write middleware over the state container. Each slot intercepts the
|
|
87
|
+
* matching `config` field or catalog entry and transforms the value as it
|
|
88
|
+
* passes through — the integration's hook for instrumenting state without the
|
|
89
|
+
* service knowing about it. Omit a slot to pass the value through untouched.
|
|
90
|
+
*/
|
|
91
|
+
type Options<T extends Template> = {
|
|
92
|
+
get?: {
|
|
93
|
+
config?: {
|
|
94
|
+
theme?: (theme: T) => T;
|
|
95
|
+
input?: (input: Input<T>) => Input<T>;
|
|
96
|
+
override?: (override: Overrides<T>) => Overrides<T>;
|
|
97
|
+
};
|
|
98
|
+
themes?: {
|
|
99
|
+
[key: string]: (layer: Layer<T>) => Layer<T>;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
set?: {
|
|
103
|
+
config?: {
|
|
104
|
+
theme?: (theme: Theme<T>) => Theme<T>;
|
|
105
|
+
input?: (input: Input<T>) => Input<T>;
|
|
106
|
+
override?: (override: Overrides<T>) => Overrides<T>;
|
|
107
|
+
};
|
|
108
|
+
themes?: {
|
|
109
|
+
[key: string]: (layer: Layer<T>) => Layer<T>;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* A runtime theme service over a contract. Reads resolve the active selection
|
|
115
|
+
* with the user override on top; `set` writes the override; switching a context
|
|
116
|
+
* or applying a definition change updates the active state in `config`.
|
|
117
|
+
*/
|
|
118
|
+
interface Untheme<T extends Template> {
|
|
119
|
+
/**
|
|
120
|
+
* The caller-owned live state — the single place state is read or written raw.
|
|
121
|
+
*/
|
|
122
|
+
config: Config<T>;
|
|
123
|
+
/**
|
|
124
|
+
* The catalog of switchable layers `select` / `create` / `remove` operate on.
|
|
125
|
+
*/
|
|
126
|
+
themes: Record<string, Layer<T>>;
|
|
127
|
+
/**
|
|
128
|
+
* The validation bundle for the contract.
|
|
129
|
+
*/
|
|
130
|
+
schema: Schema<T>;
|
|
131
|
+
/**
|
|
132
|
+
* The modifiers (axes) the contract declares, in composition order.
|
|
133
|
+
*/
|
|
134
|
+
modifiers: () => Modifier<T>[];
|
|
135
|
+
/**
|
|
136
|
+
* The context names a modifier offers. Throws `UnknownModifierError` when
|
|
137
|
+
* the contract declares no modifier under the name.
|
|
138
|
+
*/
|
|
139
|
+
contexts: (modifier: Modifier<T>) => string[];
|
|
140
|
+
/**
|
|
141
|
+
* The flat token map for a selection (default: the active one), each token
|
|
142
|
+
* bound to its `$value`, with the user override applied on top. Does not
|
|
143
|
+
* change the active state.
|
|
144
|
+
*/
|
|
145
|
+
tokens: (input?: Input<T>) => {
|
|
146
|
+
[K in Token<T>]: Binding;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* A token's effective binding: the override if set, else the composed value.
|
|
150
|
+
*/
|
|
151
|
+
get: (token: Token<T>) => Binding;
|
|
152
|
+
/**
|
|
153
|
+
* A token's fully dereferenced value: whole-value references are followed to
|
|
154
|
+
* their target and references nested inside composite values resolve in
|
|
155
|
+
* place, so the result carries no references at any depth. Throws
|
|
156
|
+
* `CircularAliasError` on a reference loop.
|
|
157
|
+
*/
|
|
158
|
+
resolve: (token: Token<T>) => Values<Open>[Type];
|
|
159
|
+
/**
|
|
160
|
+
* Selects a context for a modifier — the cheap runtime swap. Throws
|
|
161
|
+
* `InvalidThemeError` when the context names no declared context of the
|
|
162
|
+
* modifier.
|
|
163
|
+
*/
|
|
164
|
+
swap: <M extends Modifier<T>, C extends Context<T, M>>(modifier: M, context: C) => void;
|
|
165
|
+
/**
|
|
166
|
+
* Writes a token into the user override layer. A write outside the contract —
|
|
167
|
+
* an unknown token, or a value invalid for that token's declared type — is a
|
|
168
|
+
* silent no-op. The override holds a detached copy of the value.
|
|
169
|
+
*/
|
|
170
|
+
set: (token: Token<T>, value: Binding) => void;
|
|
171
|
+
/**
|
|
172
|
+
* The effective drift from the baseline as a re-appliable patch: the active
|
|
173
|
+
* theme with the user override baked in, diffed against the theme the service
|
|
174
|
+
* was built on. Token and context bindings that match the baseline drop out;
|
|
175
|
+
* what remains is everything `set` / `update` / `apply` changed. Identity is
|
|
176
|
+
* not compared. Feeding the result back through `update` reproduces the drift.
|
|
177
|
+
*/
|
|
178
|
+
delta: () => Diff<T>;
|
|
179
|
+
/**
|
|
180
|
+
* Whether the user override holds any edits.
|
|
181
|
+
*/
|
|
182
|
+
dirty: () => boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Clears the user override.
|
|
185
|
+
*/
|
|
186
|
+
reset: () => void;
|
|
187
|
+
/**
|
|
188
|
+
* Merges a patch into the active theme; identity and the override are
|
|
189
|
+
* unchanged.
|
|
190
|
+
*/
|
|
191
|
+
update: (patch: Patch<T>) => void;
|
|
192
|
+
/**
|
|
193
|
+
* Becomes the layer resolved against the baseline, and clears the override.
|
|
194
|
+
*/
|
|
195
|
+
apply: (layer: Layer<T>) => void;
|
|
196
|
+
/**
|
|
197
|
+
* Switches to the catalog layer filed under `key`, and clears the override.
|
|
198
|
+
*/
|
|
199
|
+
select: (key: string) => void;
|
|
200
|
+
/**
|
|
201
|
+
* Files a layer in the catalog under its id and returns it resolved against
|
|
202
|
+
* the baseline as a complete theme. The active theme is not touched.
|
|
203
|
+
*/
|
|
204
|
+
create: (layer: Layer<T>) => Theme<T>;
|
|
205
|
+
/**
|
|
206
|
+
* Snapshots the active theme and override as a detached theme; not
|
|
207
|
+
* registered. Throws `InvalidThemeError` when the identity leaves the
|
|
208
|
+
* snapshot invalid.
|
|
209
|
+
*/
|
|
210
|
+
extract: (id: string, name: string) => Theme<T>;
|
|
211
|
+
/**
|
|
212
|
+
* Drops a layer from the catalog by id; the active theme is unaffected.
|
|
213
|
+
*/
|
|
214
|
+
remove: (id: string) => void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Builds the runtime {@link Untheme} service over a state container, for
|
|
219
|
+
* any complete theme of a contract — an authored contract via
|
|
220
|
+
* {@link defineUntheme}, or a machine-built theme (a `configure`-widened
|
|
221
|
+
* preset, a merged theme) whose slot bindings only the runtime schema can
|
|
222
|
+
* rule on. The theme is validated against its own contract up front either
|
|
223
|
+
* way.
|
|
224
|
+
*
|
|
225
|
+
* Every read and write goes through `proxy`, so the caller decides whether
|
|
226
|
+
* state is plain (tests, node) or a reactive proxy (Vue); `options` can
|
|
227
|
+
* intercept and transform each read and write on the way through. Reads resolve
|
|
228
|
+
* the active selection — base tokens overlaid with the selected context of each
|
|
229
|
+
* modifier in `order` — then apply the user override on top. `set` writes only
|
|
230
|
+
* the override; `swap` selects a modifier's context; `update` / `apply` /
|
|
231
|
+
* `select` change the definition. Switching theme (`apply` / `select`) clears
|
|
232
|
+
* the override.
|
|
233
|
+
*
|
|
234
|
+
* The baseline — a snapshot of the theme at construction — is what `apply` and
|
|
235
|
+
* `create` resolve layers against.
|
|
236
|
+
*
|
|
237
|
+
* @param config - The caller-owned container: active theme, selection, override.
|
|
238
|
+
* @param themes - The catalog of switchable layers, validated up front; the
|
|
239
|
+
* target of `select`, `create`, and `remove`.
|
|
240
|
+
* @param options - Read/write middleware over `config` and `themes`.
|
|
241
|
+
* @returns An {@link Untheme} service bound to the container.
|
|
242
|
+
* @throws InvalidThemeError when the theme or selection violates the contract.
|
|
243
|
+
* @throws InvalidLayerError when a registry layer steps outside the contract.
|
|
244
|
+
*/
|
|
245
|
+
declare const makeUntheme: <T extends Theme<T>>(config: Config<T>, themes?: Record<string, Layer<T>>, options?: Options<T>) => Untheme<T>;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Creates a runtime {@link Untheme} service from an authored contract — the
|
|
249
|
+
* front door for defining a theme inline. `Tok` is inferred from the token
|
|
250
|
+
* keys: token slots are discriminated by their declared `$type`, and modifier
|
|
251
|
+
* overrides suggest the contract's tokens at their key positions. Booting
|
|
252
|
+
* from a machine-built theme instead — a `configure`-widened preset, a merged
|
|
253
|
+
* theme — goes through {@link makeUntheme}, whose slots carry
|
|
254
|
+
* runtime-validated bindings.
|
|
255
|
+
*
|
|
256
|
+
* @param config - The caller-owned container: active theme, selection, override.
|
|
257
|
+
* @param themes - The catalog of switchable layers, validated up front; the
|
|
258
|
+
* target of `select`, `create`, and `remove`.
|
|
259
|
+
* @param options - Read/write middleware over `config` and `themes`.
|
|
260
|
+
* @returns An {@link Untheme} service bound to the container.
|
|
261
|
+
* @throws InvalidThemeError when the theme or selection violates the contract.
|
|
262
|
+
* @throws InvalidLayerError when a registry layer steps outside the contract.
|
|
263
|
+
*/
|
|
264
|
+
declare const defineUntheme: <Tok extends string, Mod extends Record<string, Record<string, Partial<Record<NoInfer<Tok>, Binding>>>>>(config: Config<Contract<Tok, Mod>>, themes?: Record<string, Layer<Contract<Tok, Mod>>>, options?: Options<Contract<Tok, Mod>>) => Untheme<Contract<Tok, Mod>>;
|
|
265
|
+
|
|
266
|
+
export { CircularAliasError, InvalidLayerError, InvalidPatchError, InvalidThemeError, UnknownModifierError, UnknownThemeError, defineUntheme, makeUntheme, reframe };
|
|
267
|
+
export type { Config, Options, Untheme };
|
package/.dist/index.d.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { SchemaError, Issue, Template, Theme, Input, Overrides, Layer, Schema, Modifier, Token, Binding, Values, Open, Type, Context, Patch, Contract } from '@untheme/schema';
|
|
2
|
+
import { Diff } from '@untheme/utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Raised when the base theme handed to {@link defineUntheme} violates its own
|
|
6
|
+
* contract. Extends {@link SchemaError}, so it carries the underlying
|
|
7
|
+
* {@link Issue}s while naming *which* boundary rejected the value.
|
|
8
|
+
*/
|
|
9
|
+
declare class InvalidThemeError extends SchemaError {
|
|
10
|
+
constructor(issues: Issue[]);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Raised when a layer — a registry seed, or an argument to {@link apply} /
|
|
14
|
+
* {@link create} — steps outside the contract. Carries the {@link Issue}s of
|
|
15
|
+
* the failed {@link SchemaError} it wraps.
|
|
16
|
+
*/
|
|
17
|
+
declare class InvalidLayerError extends SchemaError {
|
|
18
|
+
constructor(issues: Issue[]);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Raised when a patch handed to {@link update} steps outside the contract.
|
|
22
|
+
* Carries the {@link Issue}s of the failed {@link SchemaError} it wraps.
|
|
23
|
+
*/
|
|
24
|
+
declare class InvalidPatchError extends SchemaError {
|
|
25
|
+
constructor(issues: Issue[]);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Raised when {@link Untheme.select} is handed a key that names no theme in the
|
|
29
|
+
* registry. Unlike the validation errors above this carries no {@link Issue}s —
|
|
30
|
+
* a missing key is a lookup miss, not a contract violation — so it extends the
|
|
31
|
+
* plain {@link Error} while still giving the failure a semantic identity and
|
|
32
|
+
* the offending `key`.
|
|
33
|
+
*/
|
|
34
|
+
declare class UnknownThemeError extends Error {
|
|
35
|
+
readonly key: string;
|
|
36
|
+
constructor(key: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Raised when {@link Untheme.contexts} is handed a name the contract declares
|
|
40
|
+
* no modifier under. Like {@link UnknownThemeError} this is a lookup miss, not
|
|
41
|
+
* a contract violation, so it extends the plain {@link Error} while carrying
|
|
42
|
+
* the offending `modifier`.
|
|
43
|
+
*/
|
|
44
|
+
declare class UnknownModifierError extends Error {
|
|
45
|
+
readonly modifier: string;
|
|
46
|
+
constructor(modifier: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Raised when {@link Untheme.resolve} follows an alias chain that loops back on
|
|
50
|
+
* itself. Like {@link UnknownThemeError} it carries no {@link Issue}s — a cycle
|
|
51
|
+
* is a resolution failure, not a contract violation — so it extends the plain
|
|
52
|
+
* {@link Error}, carrying the `chain` of token names up to and including the
|
|
53
|
+
* repeat. Detected by tracking visited tokens, so the stack never overflows.
|
|
54
|
+
*/
|
|
55
|
+
declare class CircularAliasError extends Error {
|
|
56
|
+
readonly chain: string[];
|
|
57
|
+
constructor(chain: string[]);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Runs `fn`, and if it throws a {@link SchemaError}, re-throws it as the given
|
|
61
|
+
* semantic subclass carrying the same {@link Issue}s; anything else propagates
|
|
62
|
+
* untouched. Lets the service speak in {@link InvalidThemeError} /
|
|
63
|
+
* {@link InvalidLayerError} / {@link InvalidPatchError} while the schema layer
|
|
64
|
+
* stays generic.
|
|
65
|
+
*/
|
|
66
|
+
declare const reframe: <T>(Semantic: new (issues: Issue[]) => SchemaError, fn: () => T) => T;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The caller-owned live state an {@link Untheme} service reads and writes: the
|
|
70
|
+
* active theme definition, the active selection (one context per modifier), and
|
|
71
|
+
* the user override layer that `set` populates. Pass a plain object for inert
|
|
72
|
+
* state, or a reactive proxy to have reads and writes tracked.
|
|
73
|
+
*
|
|
74
|
+
* `theme` reads as the caller's own contract type but writes accept any
|
|
75
|
+
* complete {@link Theme} of that contract: `update` and `apply` store merged
|
|
76
|
+
* themes, which satisfy the contract without being the caller's exact type. A
|
|
77
|
+
* plain `{ theme, input, override }` object satisfies both sides.
|
|
78
|
+
*/
|
|
79
|
+
type Config<T extends Template> = {
|
|
80
|
+
get theme(): T;
|
|
81
|
+
set theme(value: Theme<T>);
|
|
82
|
+
input: Input<T>;
|
|
83
|
+
override: Overrides<T>;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Read/write middleware over the state container. Each slot intercepts the
|
|
87
|
+
* matching `config` field or catalog entry and transforms the value as it
|
|
88
|
+
* passes through — the integration's hook for instrumenting state without the
|
|
89
|
+
* service knowing about it. Omit a slot to pass the value through untouched.
|
|
90
|
+
*/
|
|
91
|
+
type Options<T extends Template> = {
|
|
92
|
+
get?: {
|
|
93
|
+
config?: {
|
|
94
|
+
theme?: (theme: T) => T;
|
|
95
|
+
input?: (input: Input<T>) => Input<T>;
|
|
96
|
+
override?: (override: Overrides<T>) => Overrides<T>;
|
|
97
|
+
};
|
|
98
|
+
themes?: {
|
|
99
|
+
[key: string]: (layer: Layer<T>) => Layer<T>;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
set?: {
|
|
103
|
+
config?: {
|
|
104
|
+
theme?: (theme: Theme<T>) => Theme<T>;
|
|
105
|
+
input?: (input: Input<T>) => Input<T>;
|
|
106
|
+
override?: (override: Overrides<T>) => Overrides<T>;
|
|
107
|
+
};
|
|
108
|
+
themes?: {
|
|
109
|
+
[key: string]: (layer: Layer<T>) => Layer<T>;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* A runtime theme service over a contract. Reads resolve the active selection
|
|
115
|
+
* with the user override on top; `set` writes the override; switching a context
|
|
116
|
+
* or applying a definition change updates the active state in `config`.
|
|
117
|
+
*/
|
|
118
|
+
interface Untheme<T extends Template> {
|
|
119
|
+
/**
|
|
120
|
+
* The caller-owned live state — the single place state is read or written raw.
|
|
121
|
+
*/
|
|
122
|
+
config: Config<T>;
|
|
123
|
+
/**
|
|
124
|
+
* The catalog of switchable layers `select` / `create` / `remove` operate on.
|
|
125
|
+
*/
|
|
126
|
+
themes: Record<string, Layer<T>>;
|
|
127
|
+
/**
|
|
128
|
+
* The validation bundle for the contract.
|
|
129
|
+
*/
|
|
130
|
+
schema: Schema<T>;
|
|
131
|
+
/**
|
|
132
|
+
* The modifiers (axes) the contract declares, in composition order.
|
|
133
|
+
*/
|
|
134
|
+
modifiers: () => Modifier<T>[];
|
|
135
|
+
/**
|
|
136
|
+
* The context names a modifier offers. Throws `UnknownModifierError` when
|
|
137
|
+
* the contract declares no modifier under the name.
|
|
138
|
+
*/
|
|
139
|
+
contexts: (modifier: Modifier<T>) => string[];
|
|
140
|
+
/**
|
|
141
|
+
* The flat token map for a selection (default: the active one), each token
|
|
142
|
+
* bound to its `$value`, with the user override applied on top. Does not
|
|
143
|
+
* change the active state.
|
|
144
|
+
*/
|
|
145
|
+
tokens: (input?: Input<T>) => {
|
|
146
|
+
[K in Token<T>]: Binding;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* A token's effective binding: the override if set, else the composed value.
|
|
150
|
+
*/
|
|
151
|
+
get: (token: Token<T>) => Binding;
|
|
152
|
+
/**
|
|
153
|
+
* A token's fully dereferenced value: whole-value references are followed to
|
|
154
|
+
* their target and references nested inside composite values resolve in
|
|
155
|
+
* place, so the result carries no references at any depth. Throws
|
|
156
|
+
* `CircularAliasError` on a reference loop.
|
|
157
|
+
*/
|
|
158
|
+
resolve: (token: Token<T>) => Values<Open>[Type];
|
|
159
|
+
/**
|
|
160
|
+
* Selects a context for a modifier — the cheap runtime swap. Throws
|
|
161
|
+
* `InvalidThemeError` when the context names no declared context of the
|
|
162
|
+
* modifier.
|
|
163
|
+
*/
|
|
164
|
+
swap: <M extends Modifier<T>, C extends Context<T, M>>(modifier: M, context: C) => void;
|
|
165
|
+
/**
|
|
166
|
+
* Writes a token into the user override layer. A write outside the contract —
|
|
167
|
+
* an unknown token, or a value invalid for that token's declared type — is a
|
|
168
|
+
* silent no-op. The override holds a detached copy of the value.
|
|
169
|
+
*/
|
|
170
|
+
set: (token: Token<T>, value: Binding) => void;
|
|
171
|
+
/**
|
|
172
|
+
* The effective drift from the baseline as a re-appliable patch: the active
|
|
173
|
+
* theme with the user override baked in, diffed against the theme the service
|
|
174
|
+
* was built on. Token and context bindings that match the baseline drop out;
|
|
175
|
+
* what remains is everything `set` / `update` / `apply` changed. Identity is
|
|
176
|
+
* not compared. Feeding the result back through `update` reproduces the drift.
|
|
177
|
+
*/
|
|
178
|
+
delta: () => Diff<T>;
|
|
179
|
+
/**
|
|
180
|
+
* Whether the user override holds any edits.
|
|
181
|
+
*/
|
|
182
|
+
dirty: () => boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Clears the user override.
|
|
185
|
+
*/
|
|
186
|
+
reset: () => void;
|
|
187
|
+
/**
|
|
188
|
+
* Merges a patch into the active theme; identity and the override are
|
|
189
|
+
* unchanged.
|
|
190
|
+
*/
|
|
191
|
+
update: (patch: Patch<T>) => void;
|
|
192
|
+
/**
|
|
193
|
+
* Becomes the layer resolved against the baseline, and clears the override.
|
|
194
|
+
*/
|
|
195
|
+
apply: (layer: Layer<T>) => void;
|
|
196
|
+
/**
|
|
197
|
+
* Switches to the catalog layer filed under `key`, and clears the override.
|
|
198
|
+
*/
|
|
199
|
+
select: (key: string) => void;
|
|
200
|
+
/**
|
|
201
|
+
* Files a layer in the catalog under its id and returns it resolved against
|
|
202
|
+
* the baseline as a complete theme. The active theme is not touched.
|
|
203
|
+
*/
|
|
204
|
+
create: (layer: Layer<T>) => Theme<T>;
|
|
205
|
+
/**
|
|
206
|
+
* Snapshots the active theme and override as a detached theme; not
|
|
207
|
+
* registered. Throws `InvalidThemeError` when the identity leaves the
|
|
208
|
+
* snapshot invalid.
|
|
209
|
+
*/
|
|
210
|
+
extract: (id: string, name: string) => Theme<T>;
|
|
211
|
+
/**
|
|
212
|
+
* Drops a layer from the catalog by id; the active theme is unaffected.
|
|
213
|
+
*/
|
|
214
|
+
remove: (id: string) => void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Builds the runtime {@link Untheme} service over a state container, for
|
|
219
|
+
* any complete theme of a contract — an authored contract via
|
|
220
|
+
* {@link defineUntheme}, or a machine-built theme (a `configure`-widened
|
|
221
|
+
* preset, a merged theme) whose slot bindings only the runtime schema can
|
|
222
|
+
* rule on. The theme is validated against its own contract up front either
|
|
223
|
+
* way.
|
|
224
|
+
*
|
|
225
|
+
* Every read and write goes through `proxy`, so the caller decides whether
|
|
226
|
+
* state is plain (tests, node) or a reactive proxy (Vue); `options` can
|
|
227
|
+
* intercept and transform each read and write on the way through. Reads resolve
|
|
228
|
+
* the active selection — base tokens overlaid with the selected context of each
|
|
229
|
+
* modifier in `order` — then apply the user override on top. `set` writes only
|
|
230
|
+
* the override; `swap` selects a modifier's context; `update` / `apply` /
|
|
231
|
+
* `select` change the definition. Switching theme (`apply` / `select`) clears
|
|
232
|
+
* the override.
|
|
233
|
+
*
|
|
234
|
+
* The baseline — a snapshot of the theme at construction — is what `apply` and
|
|
235
|
+
* `create` resolve layers against.
|
|
236
|
+
*
|
|
237
|
+
* @param config - The caller-owned container: active theme, selection, override.
|
|
238
|
+
* @param themes - The catalog of switchable layers, validated up front; the
|
|
239
|
+
* target of `select`, `create`, and `remove`.
|
|
240
|
+
* @param options - Read/write middleware over `config` and `themes`.
|
|
241
|
+
* @returns An {@link Untheme} service bound to the container.
|
|
242
|
+
* @throws InvalidThemeError when the theme or selection violates the contract.
|
|
243
|
+
* @throws InvalidLayerError when a registry layer steps outside the contract.
|
|
244
|
+
*/
|
|
245
|
+
declare const makeUntheme: <T extends Theme<T>>(config: Config<T>, themes?: Record<string, Layer<T>>, options?: Options<T>) => Untheme<T>;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Creates a runtime {@link Untheme} service from an authored contract — the
|
|
249
|
+
* front door for defining a theme inline. `Tok` is inferred from the token
|
|
250
|
+
* keys: token slots are discriminated by their declared `$type`, and modifier
|
|
251
|
+
* overrides suggest the contract's tokens at their key positions. Booting
|
|
252
|
+
* from a machine-built theme instead — a `configure`-widened preset, a merged
|
|
253
|
+
* theme — goes through {@link makeUntheme}, whose slots carry
|
|
254
|
+
* runtime-validated bindings.
|
|
255
|
+
*
|
|
256
|
+
* @param config - The caller-owned container: active theme, selection, override.
|
|
257
|
+
* @param themes - The catalog of switchable layers, validated up front; the
|
|
258
|
+
* target of `select`, `create`, and `remove`.
|
|
259
|
+
* @param options - Read/write middleware over `config` and `themes`.
|
|
260
|
+
* @returns An {@link Untheme} service bound to the container.
|
|
261
|
+
* @throws InvalidThemeError when the theme or selection violates the contract.
|
|
262
|
+
* @throws InvalidLayerError when a registry layer steps outside the contract.
|
|
263
|
+
*/
|
|
264
|
+
declare const defineUntheme: <Tok extends string, Mod extends Record<string, Record<string, Partial<Record<NoInfer<Tok>, Binding>>>>>(config: Config<Contract<Tok, Mod>>, themes?: Record<string, Layer<Contract<Tok, Mod>>>, options?: Options<Contract<Tok, Mod>>) => Untheme<Contract<Tok, Mod>>;
|
|
265
|
+
|
|
266
|
+
export { CircularAliasError, InvalidLayerError, InvalidPatchError, InvalidThemeError, UnknownModifierError, UnknownThemeError, defineUntheme, makeUntheme, reframe };
|
|
267
|
+
export type { Config, Options, Untheme };
|
package/.dist/index.mjs
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { SchemaError, defineSchema } from '@untheme/schema';
|
|
2
|
+
import { values, map, isRecord } from '@untheme/common';
|
|
3
|
+
import { clone, merge, copy, diff } from '@untheme/utils';
|
|
4
|
+
|
|
5
|
+
class InvalidThemeError extends SchemaError {
|
|
6
|
+
constructor(issues) {
|
|
7
|
+
super(issues);
|
|
8
|
+
this.name = "InvalidThemeError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
class InvalidLayerError extends SchemaError {
|
|
12
|
+
constructor(issues) {
|
|
13
|
+
super(issues);
|
|
14
|
+
this.name = "InvalidLayerError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class InvalidPatchError extends SchemaError {
|
|
18
|
+
constructor(issues) {
|
|
19
|
+
super(issues);
|
|
20
|
+
this.name = "InvalidPatchError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class UnknownThemeError extends Error {
|
|
24
|
+
key;
|
|
25
|
+
constructor(key) {
|
|
26
|
+
super(`no theme registered under "${key}"`);
|
|
27
|
+
this.name = "UnknownThemeError";
|
|
28
|
+
this.key = key;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
class UnknownModifierError extends Error {
|
|
32
|
+
modifier;
|
|
33
|
+
constructor(modifier) {
|
|
34
|
+
super(`no modifier declared under "${modifier}"`);
|
|
35
|
+
this.name = "UnknownModifierError";
|
|
36
|
+
this.modifier = modifier;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
class CircularAliasError extends Error {
|
|
40
|
+
chain;
|
|
41
|
+
constructor(chain) {
|
|
42
|
+
super(`alias chain loops: ${chain.join(" \u2192 ")}`);
|
|
43
|
+
this.name = "CircularAliasError";
|
|
44
|
+
this.chain = chain;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const reframe = (Semantic, fn) => {
|
|
48
|
+
try {
|
|
49
|
+
return fn();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof SchemaError) {
|
|
52
|
+
throw new Semantic(error.issues);
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const makeUntheme = (config, themes = {}, options = {}) => {
|
|
59
|
+
const proxy = {
|
|
60
|
+
get theme() {
|
|
61
|
+
const through = options.get?.config?.theme;
|
|
62
|
+
if (through) {
|
|
63
|
+
return through(config.theme);
|
|
64
|
+
}
|
|
65
|
+
return config.theme;
|
|
66
|
+
},
|
|
67
|
+
set theme(value) {
|
|
68
|
+
const through = options.set?.config?.theme;
|
|
69
|
+
if (through) {
|
|
70
|
+
config.theme = through(value);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
config.theme = value;
|
|
74
|
+
},
|
|
75
|
+
get input() {
|
|
76
|
+
const through = options.get?.config?.input;
|
|
77
|
+
if (through) {
|
|
78
|
+
return through(config.input);
|
|
79
|
+
}
|
|
80
|
+
return config.input;
|
|
81
|
+
},
|
|
82
|
+
set input(value) {
|
|
83
|
+
const through = options.set?.config?.input;
|
|
84
|
+
if (through) {
|
|
85
|
+
config.input = through(value);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
config.input = value;
|
|
89
|
+
},
|
|
90
|
+
get override() {
|
|
91
|
+
const through = options.get?.config?.override;
|
|
92
|
+
if (through) {
|
|
93
|
+
return through(config.override);
|
|
94
|
+
}
|
|
95
|
+
return config.override;
|
|
96
|
+
},
|
|
97
|
+
set override(value) {
|
|
98
|
+
const through = options.set?.config?.override;
|
|
99
|
+
if (through) {
|
|
100
|
+
config.override = through(value);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
config.override = value;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const registry = new Proxy(themes, {
|
|
107
|
+
get(target, key, receiver) {
|
|
108
|
+
const value = Reflect.get(target, key, receiver);
|
|
109
|
+
if (typeof key === "string") {
|
|
110
|
+
const through = options.get?.themes?.[key];
|
|
111
|
+
if (through) {
|
|
112
|
+
return through(value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
},
|
|
117
|
+
set(target, key, value, receiver) {
|
|
118
|
+
if (typeof key === "string") {
|
|
119
|
+
const through = options.set?.themes?.[key];
|
|
120
|
+
if (through) {
|
|
121
|
+
return Reflect.set(target, key, through(value), receiver);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return Reflect.set(target, key, value, receiver);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const schema = reframe(
|
|
128
|
+
InvalidThemeError,
|
|
129
|
+
() => defineSchema(clone(proxy.theme))
|
|
130
|
+
);
|
|
131
|
+
reframe(InvalidThemeError, () => schema.assert.input(proxy.input));
|
|
132
|
+
reframe(InvalidLayerError, () => {
|
|
133
|
+
for (const layer of values(registry)) {
|
|
134
|
+
schema.assert.layer(layer);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const tokens = (input = proxy.input) => {
|
|
138
|
+
const flat = map(proxy.theme.tokens, (slot) => slot.$value);
|
|
139
|
+
for (const modifier of proxy.theme.order) {
|
|
140
|
+
Object.assign(flat, proxy.theme.modifiers[modifier]?.[input[modifier]]);
|
|
141
|
+
}
|
|
142
|
+
Object.assign(flat, proxy.override);
|
|
143
|
+
return flat;
|
|
144
|
+
};
|
|
145
|
+
const get = (token) => {
|
|
146
|
+
return tokens()[token];
|
|
147
|
+
};
|
|
148
|
+
const set = (token, value) => {
|
|
149
|
+
if (!schema.check.overrides({ [token]: value })) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
proxy.override = { ...proxy.override, [token]: copy(value) };
|
|
153
|
+
};
|
|
154
|
+
const substitute = (value, chain) => {
|
|
155
|
+
if (schema.check.reference(value)) {
|
|
156
|
+
const inner = value.slice(1, -1);
|
|
157
|
+
if (schema.check.token(inner)) {
|
|
158
|
+
return follow(inner, chain);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return value.map((entry) => substitute(entry, chain));
|
|
163
|
+
}
|
|
164
|
+
if (isRecord(value)) {
|
|
165
|
+
return map(value, (entry) => substitute(entry, chain));
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
};
|
|
169
|
+
const follow = (token, chain) => {
|
|
170
|
+
if (chain.has(token)) {
|
|
171
|
+
throw new CircularAliasError([...chain, token]);
|
|
172
|
+
}
|
|
173
|
+
return substitute(get(token), new Set(chain).add(token));
|
|
174
|
+
};
|
|
175
|
+
const resolve = (token) => {
|
|
176
|
+
return schema.parse.value(follow(token, /* @__PURE__ */ new Set()));
|
|
177
|
+
};
|
|
178
|
+
const modifiers = () => proxy.theme.order;
|
|
179
|
+
const contexts = (modifier) => {
|
|
180
|
+
const axis = proxy.theme.modifiers[modifier];
|
|
181
|
+
if (!axis) {
|
|
182
|
+
throw new UnknownModifierError(modifier);
|
|
183
|
+
}
|
|
184
|
+
return Object.keys(axis);
|
|
185
|
+
};
|
|
186
|
+
const swap = (modifier, context) => {
|
|
187
|
+
const input = { ...proxy.input, [modifier]: context };
|
|
188
|
+
reframe(InvalidThemeError, () => schema.assert.input(input));
|
|
189
|
+
proxy.input = input;
|
|
190
|
+
};
|
|
191
|
+
const update = (patch) => {
|
|
192
|
+
reframe(InvalidPatchError, () => schema.assert.patch(patch));
|
|
193
|
+
proxy.theme = merge(proxy.theme, patch);
|
|
194
|
+
};
|
|
195
|
+
const apply = (layer) => {
|
|
196
|
+
reframe(InvalidLayerError, () => schema.assert.layer(layer));
|
|
197
|
+
proxy.theme = merge(schema.base, layer);
|
|
198
|
+
proxy.override = {};
|
|
199
|
+
};
|
|
200
|
+
const select = (key) => {
|
|
201
|
+
const layer = registry[key];
|
|
202
|
+
if (!layer) {
|
|
203
|
+
throw new UnknownThemeError(key);
|
|
204
|
+
}
|
|
205
|
+
apply(layer);
|
|
206
|
+
};
|
|
207
|
+
const create = (layer) => {
|
|
208
|
+
reframe(InvalidLayerError, () => schema.assert.layer(layer));
|
|
209
|
+
registry[layer.id] = copy(layer);
|
|
210
|
+
return merge(schema.base, layer);
|
|
211
|
+
};
|
|
212
|
+
const extract = (id, name) => {
|
|
213
|
+
const snapshot = merge(proxy.theme, {
|
|
214
|
+
id,
|
|
215
|
+
name,
|
|
216
|
+
tokens: proxy.override
|
|
217
|
+
});
|
|
218
|
+
reframe(InvalidThemeError, () => schema.assert.theme(snapshot));
|
|
219
|
+
return snapshot;
|
|
220
|
+
};
|
|
221
|
+
const remove = (id) => {
|
|
222
|
+
delete registry[id];
|
|
223
|
+
};
|
|
224
|
+
const delta = () => {
|
|
225
|
+
return diff(
|
|
226
|
+
schema.base,
|
|
227
|
+
merge(proxy.theme, { tokens: proxy.override })
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
const dirty = () => Object.keys(proxy.override).length > 0;
|
|
231
|
+
const reset = () => {
|
|
232
|
+
proxy.override = {};
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
config: proxy,
|
|
236
|
+
themes: registry,
|
|
237
|
+
schema,
|
|
238
|
+
modifiers,
|
|
239
|
+
contexts,
|
|
240
|
+
tokens,
|
|
241
|
+
get,
|
|
242
|
+
resolve,
|
|
243
|
+
swap,
|
|
244
|
+
set,
|
|
245
|
+
delta,
|
|
246
|
+
dirty,
|
|
247
|
+
reset,
|
|
248
|
+
update,
|
|
249
|
+
apply,
|
|
250
|
+
select,
|
|
251
|
+
create,
|
|
252
|
+
extract,
|
|
253
|
+
remove
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const defineUntheme = (config, themes = {}, options = {}) => {
|
|
258
|
+
return makeUntheme(config, themes, options);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export { CircularAliasError, InvalidLayerError, InvalidPatchError, InvalidThemeError, UnknownModifierError, UnknownThemeError, defineUntheme, makeUntheme, reframe };
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @untheme/core
|
|
2
|
+
|
|
3
|
+
Runtime theme service for the untheme design token system.
|
|
4
|
+
|
|
5
|
+
Provides the service that reads, resolves, and mutates tokens over a caller-owned state container. The token contract and its runtime guards live in [`@untheme/schema`](../schema); most apps depend on both transitively through the [`untheme`](../untheme) umbrella package.
|
|
6
|
+
|
|
7
|
+
## The token model
|
|
8
|
+
|
|
9
|
+
A theme is a flat map of tokens, each holding a `$type` and `$value`. On top of the base tokens, the contract declares any number of **modifiers** — independent axes like color scheme or density — each offering named **contexts** that override tokens for that axis. An **input** selects one context per modifier. Reading a token composes three layers in order: the base `$value`, then the selected context of each modifier (in the contract's `order`), then the user override last.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { defineUntheme } from "@untheme/core";
|
|
15
|
+
|
|
16
|
+
const ut = defineUntheme(
|
|
17
|
+
{ theme, input: { color: "dark" }, override: {} },
|
|
18
|
+
{ midnight },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
ut.resolve("primary"); // follow the alias chain to a raw value
|
|
22
|
+
ut.set("background", "blue"); // write to the override layer
|
|
23
|
+
ut.swap("color", "light"); // switch the color modifier's context
|
|
24
|
+
|
|
25
|
+
ut.dirty(); // true — the override holds edits
|
|
26
|
+
ut.reset(); // clears the override
|
|
27
|
+
|
|
28
|
+
ut.select("midnight"); // switch to a catalogued theme by key
|
|
29
|
+
ut.create(draftLayer); // resolve + file a new theme into the catalog
|
|
30
|
+
ut.remove("midnight"); // drop a theme from the catalog
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Presets built with [`@untheme/kit`](../kit) produce the config for you: `defineUntheme(preset.use({ color: "dark" }))`. A `configure`-widened preset's theme is machine-built rather than authored, so it boots through the factory instead: `makeUntheme(app.use({ color: "dark" }))` — the same service, accepting any complete theme of a contract.
|
|
34
|
+
|
|
35
|
+
## The state container
|
|
36
|
+
|
|
37
|
+
The service is pure behavior: every read and write goes through `config`, the caller-owned container holding the active `theme`, the `input` (one selected context per modifier), and the `override` layer that `set` populates. Pass a plain object for inert state (tests, node), or a reactive proxy (Vue) to have every service read and write tracked — reactivity threads through property access on the container, never through the service itself.
|
|
38
|
+
|
|
39
|
+
## The service
|
|
40
|
+
|
|
41
|
+
`defineUntheme(state, registry?, options?)` returns an `Untheme<T>`:
|
|
42
|
+
|
|
43
|
+
- `config` — the caller-owned container: `theme`, `input`, `override`.
|
|
44
|
+
- `themes` — the mutable catalog of theme layers (optional second argument; defaults to empty). `select` switches to an entry by key, `create` files new themes into it, and `remove` deletes them.
|
|
45
|
+
- `schema` — guard vocabulary for the theme's token contract, from [`defineSchema`](../schema).
|
|
46
|
+
- `modifiers()` — the modifier axes the contract declares, in composition order.
|
|
47
|
+
- `contexts(modifier)` — the context names a modifier offers.
|
|
48
|
+
- `tokens(input?)` — the flat token map for a selection (default: the active one): every token's effective binding, override included, without touching `config.input`.
|
|
49
|
+
- `get(token)` — a token's effective binding (alias name or raw value), unresolved.
|
|
50
|
+
- `resolve(token)` — a token's fully dereferenced value: a whole-value reference is followed to its target, and references nested inside composite values resolve in place too. Throws `CircularAliasError` on a looping chain.
|
|
51
|
+
- `swap(modifier, context)` — selects a context for a modifier.
|
|
52
|
+
- `set(token, value)` — writes a token into the user override layer; validated against the token's declared type. A write outside the contract — an unknown token, or a value invalid for that token's type — is a silent no-op.
|
|
53
|
+
- `delta()` — the drift from the baseline as a re-appliable patch: the active theme with the override baked in, diffed against the baseline.
|
|
54
|
+
- `dirty()` — whether the user override holds any edits.
|
|
55
|
+
- `reset()` — clears the user override.
|
|
56
|
+
- `update(patch)` — merges a patch into the active theme; identity and the override are unchanged. Throws `InvalidPatchError` outside the contract.
|
|
57
|
+
- `apply(layer)` — becomes that theme: the layer resolved against the baseline, and clears the override. Throws `InvalidLayerError` outside the contract.
|
|
58
|
+
- `select(key)` — switches to the catalog layer filed under `key` (`apply` addressed by name), and clears the override. Throws `UnknownThemeError` when nothing is registered under `key`.
|
|
59
|
+
- `create(layer)` — resolves a layer against the baseline into a complete theme and files it in the catalog under its id, where `select` can switch to it; the active theme is untouched. Throws `InvalidLayerError` outside the contract.
|
|
60
|
+
- `extract(id, name)` — snapshots the active theme, including unsaved edits, as a detached theme under a new identity; returned, not filed in the catalog.
|
|
61
|
+
- `remove(id)` — drops a theme from the catalog by id; a no-op when absent. The active theme is unaffected.
|
|
62
|
+
|
|
63
|
+
## Baselines
|
|
64
|
+
|
|
65
|
+
The service keeps exactly one baseline: a snapshot of the theme it was constructed with, captured once at creation. `apply` and `create` resolve layers against this baseline, so every theme produced this way carries the full token set, and `delta()` diffs the active theme against it. `dirty()` and `reset()` work directly on the user override — `dirty()` is true whenever the override holds any keys, `reset()` clears it — so edits made since the last `apply` / `select` / `create` are detectable and revertible.
|
|
66
|
+
|
|
67
|
+
## Errors
|
|
68
|
+
|
|
69
|
+
Every boundary throws a semantic error rather than a bare `Error`, in two families.
|
|
70
|
+
|
|
71
|
+
**Contract violations** — subclasses of [`SchemaError`](../schema), carrying the underlying `issues` so callers can react to each failure:
|
|
72
|
+
|
|
73
|
+
- `InvalidThemeError` — the baseline theme handed to `defineUntheme` violates its own contract.
|
|
74
|
+
- `InvalidLayerError` — a layer (registry seed, `apply`, `create`) steps outside the contract.
|
|
75
|
+
- `InvalidPatchError` — a patch handed to `update` steps outside the contract.
|
|
76
|
+
|
|
77
|
+
**Resolution failures** — subclasses of plain `Error`, carrying the offending lookup:
|
|
78
|
+
|
|
79
|
+
- `UnknownThemeError` — `select` was handed a `key` that names no theme in the catalog.
|
|
80
|
+
- `CircularAliasError` — `resolve` hit an alias `chain` that loops back on itself; detected by tracking visited tokens, so the stack never overflows.
|
|
81
|
+
|
|
82
|
+
`set` is the deliberate exception: as the hot-path interactive write it validates with the same guards but treats invalid writes as silent no-ops rather than throwing.
|
|
83
|
+
|
|
84
|
+
## Related
|
|
85
|
+
|
|
86
|
+
- [`@untheme/schema`](../schema) — token contract types and runtime guards.
|
|
87
|
+
- [`@untheme/kit`](../kit) — authoring tool for reusable presets.
|
|
88
|
+
- [`untheme`](../untheme) — umbrella package re-exporting core and schema.
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@untheme/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./.dist/index.mjs",
|
|
6
|
+
"types": "./.dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./.dist/index.d.ts",
|
|
10
|
+
"import": "./.dist/index.mjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
".dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@untheme/common": "0.1.0",
|
|
18
|
+
"@untheme/schema": "0.1.0",
|
|
19
|
+
"@untheme/utils": "0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "unbuild",
|
|
23
|
+
"stub": "unbuild --stub",
|
|
24
|
+
"lint": "eslint .",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run"
|
|
27
|
+
}
|
|
28
|
+
}
|