@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.
@@ -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 };
@@ -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 };
@@ -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
+ }