@untheme/utils 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 +181 -0
- package/.dist/index.d.ts +181 -0
- package/.dist/index.mjs +133 -0
- package/README.md +86 -0
- package/package.json +27 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import * as _untheme_schema from '@untheme/schema';
|
|
2
|
+
import { Template, Overrides, Modifier, Context, Binding, Authored, Theme, Contract, Definition, Modifiers } from '@untheme/schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deep copy of a theme, facet by facet: identity, tokens, modifiers, and order
|
|
6
|
+
* are each rebuilt through {@link copy}, so no definition object, nested
|
|
7
|
+
* `$value` structure, or override map is shared with the source. Mutating the
|
|
8
|
+
* clone at any depth never reaches the original.
|
|
9
|
+
*
|
|
10
|
+
* Because {@link copy} reaches every value through plain property access,
|
|
11
|
+
* cloning a reactive proxy yields an inert, plain snapshot.
|
|
12
|
+
*/
|
|
13
|
+
declare const clone: <T extends Template>(theme: T) => T;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Deep copy of plain data: records and arrays are rebuilt from fresh
|
|
17
|
+
* containers, so mutating the copy never reaches the source. Every value is
|
|
18
|
+
* reached through plain property access, so copying a reactive proxy yields an
|
|
19
|
+
* inert, plain snapshot detached from the proxy.
|
|
20
|
+
*
|
|
21
|
+
* Non-plain values — functions and class instances, such as those a token's
|
|
22
|
+
* `$extensions` may carry — pass through by reference rather than being
|
|
23
|
+
* duplicated. `structuredClone` is never used: it throws on functions and
|
|
24
|
+
* detaches proxies through a mechanism this copy deliberately avoids.
|
|
25
|
+
*/
|
|
26
|
+
declare const copy: <T>(value: T) => T;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The entries of `to` that deviate from `from`: every key `to` holds whose
|
|
30
|
+
* value is not structurally equal to the one `from` holds. Emitted values are
|
|
31
|
+
* copies, so the result shares no structure with `to`. Keys with structurally
|
|
32
|
+
* equal values drop out; two objects that bind identically yield an empty
|
|
33
|
+
* result.
|
|
34
|
+
*/
|
|
35
|
+
declare const delta: <T extends object>(from: T, to: T) => Partial<T>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A contract extension for {@link extend}: new tokens (`XTok`) and new modifiers
|
|
39
|
+
* (`XMod`), plus optional overrides of the base's tokens and of its existing
|
|
40
|
+
* modifier contexts.
|
|
41
|
+
*
|
|
42
|
+
* An existing base token accepts an optional bare binding — a value or a
|
|
43
|
+
* `{reference}` that rebinds its `$value` only, never its `$type`. A new token
|
|
44
|
+
* requires a full {@link Authored} definition. Every value position draws its
|
|
45
|
+
* reference suggestions from the union of the base tokens and the extension's
|
|
46
|
+
* own new tokens, so a binding may reference either set.
|
|
47
|
+
*
|
|
48
|
+
* `Tok` is inferred from the base contract and `XTok` from the new-token keys
|
|
49
|
+
* alone; the reference arm of every {@link Binding} carries `NoInfer`, so
|
|
50
|
+
* a reference sitting in a value position never drives inference and never
|
|
51
|
+
* collapses the token union.
|
|
52
|
+
*
|
|
53
|
+
* `XTok` infers as every key present in `tokens`, base overrides included, since
|
|
54
|
+
* a mapped type cannot subtract the known `Tok` keys during inference. The
|
|
55
|
+
* `K extends Tok` guard routes those leaked base keys back to a bare binding, so
|
|
56
|
+
* only genuinely new keys demand a full definition. The arm stays optional: a
|
|
57
|
+
* required arm stops `XTok` from inferring when `tokens` is empty, and the
|
|
58
|
+
* fallback to `string` disables every token-name check.
|
|
59
|
+
*
|
|
60
|
+
* `modifiers` admits overrides of an existing axis's contexts and new axes
|
|
61
|
+
* carrying their complete context maps.
|
|
62
|
+
*/
|
|
63
|
+
type Extension<Tok extends string, Mod extends Record<string, Record<string, object>>, XTok extends string, XMod extends Record<string, Record<string, object>>> = {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
tokens: {
|
|
67
|
+
[K in NoInfer<Tok>]?: Binding;
|
|
68
|
+
} & {
|
|
69
|
+
[K in XTok]?: K extends Tok ? Binding : Authored;
|
|
70
|
+
};
|
|
71
|
+
modifiers: {
|
|
72
|
+
[M in keyof Mod]?: {
|
|
73
|
+
[C in keyof Mod[M]]?: {
|
|
74
|
+
[K in NoInfer<Tok | XTok>]?: Binding;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
} & {
|
|
78
|
+
[M in keyof XMod]: {
|
|
79
|
+
[C in keyof XMod[M]]: {
|
|
80
|
+
[K in NoInfer<Tok | XTok>]?: Binding;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
order: ((keyof Mod | keyof XMod) & (string & {}))[];
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The deviation between two themes, as produced by {@link diff}: a token
|
|
88
|
+
* override map and per-modifier, per-context override maps, all present (empty
|
|
89
|
+
* when nothing deviates) so consumers can inspect them without guards.
|
|
90
|
+
*/
|
|
91
|
+
type Diff<T extends Template> = {
|
|
92
|
+
tokens: Overrides<T>;
|
|
93
|
+
modifiers: {
|
|
94
|
+
[M in Modifier<T>]: {
|
|
95
|
+
[C in Context<T, M>]: Overrides<T>;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* A partial overlay of a theme: any subset of identity, tokens, modifiers, and
|
|
101
|
+
* order. Both a `Layer` (identity plus partial overrides) and a `Patch`
|
|
102
|
+
* (anonymous overrides) fit this shape, so one merge serves them all.
|
|
103
|
+
*/
|
|
104
|
+
type Overlay<T extends Template> = {
|
|
105
|
+
id?: string;
|
|
106
|
+
name?: string;
|
|
107
|
+
tokens?: Overrides<T>;
|
|
108
|
+
modifiers?: {
|
|
109
|
+
[M in Modifier<T>]?: {
|
|
110
|
+
[C in Context<T, M>]?: Overrides<T>;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
order?: NoInfer<Modifier<T>>[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Computes the patch that turns `from` into `to`: every binding `to` holds that
|
|
118
|
+
* deviates from `from`, token by token and context by context. At the token
|
|
119
|
+
* level the bound `$value` is compared and emitted — a token's metadata cannot
|
|
120
|
+
* drift through the patch pipeline, so only the binding is carried. Identity and
|
|
121
|
+
* order are not compared. Empty maps mean the themes bind identically; applying
|
|
122
|
+
* the result to `from` via `merge` restores every binding `to` carries. A
|
|
123
|
+
* binding `from` holds that `to` dropped is not restored — a patch can add and
|
|
124
|
+
* override, never remove.
|
|
125
|
+
*/
|
|
126
|
+
declare const diff: <T extends Template>(from: Theme<T>, to: Theme<T>) => Diff<T>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Widens a base contract with an {@link Extension} into a fresh contract over
|
|
130
|
+
* the union token and modifier sets. Neither input is mutated.
|
|
131
|
+
*
|
|
132
|
+
* Tokens compose two ways: an extension key that already names a base token
|
|
133
|
+
* rebinds that token's `$value`, preserving its `$type` and metadata; an
|
|
134
|
+
* extension key beyond the base inserts its full definition as a new token.
|
|
135
|
+
* Modifiers spread the new axes over the base's, then deep-merge each context
|
|
136
|
+
* the two share, the extension's bindings winning per token.
|
|
137
|
+
*
|
|
138
|
+
* The base may be an authored contract or any complete {@link Theme} of one —
|
|
139
|
+
* a machine-built theme (a prior extension's output) widens the same way.
|
|
140
|
+
*/
|
|
141
|
+
declare const extend: <Tok extends string, Mod extends Record<string, Record<string, object>>, XTok extends string, XMod extends Record<string, Record<string, object>>>(base: Contract<Tok, Mod> | Theme<Contract<Tok, Mod>>, extension: Extension<Tok, Mod, XTok, XMod>) => {
|
|
142
|
+
id: string;
|
|
143
|
+
name: string;
|
|
144
|
+
tokens: { [K in Tok | XTok | (NoInfer<Tok> & string)]: Definition; };
|
|
145
|
+
modifiers: { [M in keyof Mod]?: { [C in keyof Mod[M]]?: { [K_1 in NoInfer<Tok | XTok>]?: Binding | undefined; } | undefined; } | undefined; } & { [M_1 in keyof XMod]: { [C in keyof XMod[M_1]]: { [K_1 in NoInfer<Tok | XTok>]?: Binding | undefined; }; }; } & { [M_2 in _untheme_schema.Modifier<Contract<Tok, Mod>>]: { [C in _untheme_schema.Context<Contract<Tok, Mod>, M_2>]: Overrides<Contract<Tok, Mod>>; }; };
|
|
146
|
+
order: ((keyof Mod | keyof XMod) & string)[];
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
declare const guard: (v: unknown) => v is Template;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Merges overlays over a complete theme into a fresh theme, left to right:
|
|
153
|
+
* later overlays win where they bind the same token, token by token and context
|
|
154
|
+
* by context. Identity and order transfer from the last overlay that carries
|
|
155
|
+
* them. No input is mutated; with no overlays the result is a plain copy.
|
|
156
|
+
*
|
|
157
|
+
* A token override rebinds a slot's `$value` and nothing else — the slot's
|
|
158
|
+
* `$type`, description, and other metadata are preserved, and the incoming
|
|
159
|
+
* binding replaces the old `$value` whole rather than merging into it. An
|
|
160
|
+
* overlay key with no matching base slot is skipped: there is no `$type` to
|
|
161
|
+
* inherit, so no definition can be fabricated for it.
|
|
162
|
+
*
|
|
163
|
+
* A `Layer` overlay adopts its identity (apply semantics); a `Patch` overlay
|
|
164
|
+
* has none, so the prevailing identity is preserved (update semantics).
|
|
165
|
+
*/
|
|
166
|
+
declare const merge: <T extends Template>(theme: Theme<T>, ...overlays: Overlay<T>[]) => Theme<T>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Rebuilds a modifiers structure leaf by leaf: every context of every modifier
|
|
170
|
+
* mapped through the callback, each modifier keeping its own context keys. The
|
|
171
|
+
* callback's `at` indexes any other modifiers structure at the leaf's own
|
|
172
|
+
* coordinates, so a leaf can be combined with its counterpart elsewhere; a
|
|
173
|
+
* sparse structure — a layer's or patch's partial modifier map — may hold
|
|
174
|
+
* nothing there, so `at` yields the overrides or `undefined`. The
|
|
175
|
+
* modifier/context pairing is carried generically here, checked once; callers
|
|
176
|
+
* supply only the leaf operation.
|
|
177
|
+
*/
|
|
178
|
+
declare const traverse: <T extends Template, R>(modifiers: Modifiers<T>, fn: (overrides: Overrides<T>, at: (other: { [M in Modifier<T>]?: { [C in Context<T, M>]?: Overrides<T>; }; }) => Overrides<T> | undefined) => R) => { [M in Modifier<T>]: { [C in Context<T, M>]: R; }; };
|
|
179
|
+
|
|
180
|
+
export { clone, copy, delta, diff, extend, guard, merge, traverse };
|
|
181
|
+
export type { Diff, Extension, Overlay };
|
package/.dist/index.d.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import * as _untheme_schema from '@untheme/schema';
|
|
2
|
+
import { Template, Overrides, Modifier, Context, Binding, Authored, Theme, Contract, Definition, Modifiers } from '@untheme/schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deep copy of a theme, facet by facet: identity, tokens, modifiers, and order
|
|
6
|
+
* are each rebuilt through {@link copy}, so no definition object, nested
|
|
7
|
+
* `$value` structure, or override map is shared with the source. Mutating the
|
|
8
|
+
* clone at any depth never reaches the original.
|
|
9
|
+
*
|
|
10
|
+
* Because {@link copy} reaches every value through plain property access,
|
|
11
|
+
* cloning a reactive proxy yields an inert, plain snapshot.
|
|
12
|
+
*/
|
|
13
|
+
declare const clone: <T extends Template>(theme: T) => T;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Deep copy of plain data: records and arrays are rebuilt from fresh
|
|
17
|
+
* containers, so mutating the copy never reaches the source. Every value is
|
|
18
|
+
* reached through plain property access, so copying a reactive proxy yields an
|
|
19
|
+
* inert, plain snapshot detached from the proxy.
|
|
20
|
+
*
|
|
21
|
+
* Non-plain values — functions and class instances, such as those a token's
|
|
22
|
+
* `$extensions` may carry — pass through by reference rather than being
|
|
23
|
+
* duplicated. `structuredClone` is never used: it throws on functions and
|
|
24
|
+
* detaches proxies through a mechanism this copy deliberately avoids.
|
|
25
|
+
*/
|
|
26
|
+
declare const copy: <T>(value: T) => T;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The entries of `to` that deviate from `from`: every key `to` holds whose
|
|
30
|
+
* value is not structurally equal to the one `from` holds. Emitted values are
|
|
31
|
+
* copies, so the result shares no structure with `to`. Keys with structurally
|
|
32
|
+
* equal values drop out; two objects that bind identically yield an empty
|
|
33
|
+
* result.
|
|
34
|
+
*/
|
|
35
|
+
declare const delta: <T extends object>(from: T, to: T) => Partial<T>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A contract extension for {@link extend}: new tokens (`XTok`) and new modifiers
|
|
39
|
+
* (`XMod`), plus optional overrides of the base's tokens and of its existing
|
|
40
|
+
* modifier contexts.
|
|
41
|
+
*
|
|
42
|
+
* An existing base token accepts an optional bare binding — a value or a
|
|
43
|
+
* `{reference}` that rebinds its `$value` only, never its `$type`. A new token
|
|
44
|
+
* requires a full {@link Authored} definition. Every value position draws its
|
|
45
|
+
* reference suggestions from the union of the base tokens and the extension's
|
|
46
|
+
* own new tokens, so a binding may reference either set.
|
|
47
|
+
*
|
|
48
|
+
* `Tok` is inferred from the base contract and `XTok` from the new-token keys
|
|
49
|
+
* alone; the reference arm of every {@link Binding} carries `NoInfer`, so
|
|
50
|
+
* a reference sitting in a value position never drives inference and never
|
|
51
|
+
* collapses the token union.
|
|
52
|
+
*
|
|
53
|
+
* `XTok` infers as every key present in `tokens`, base overrides included, since
|
|
54
|
+
* a mapped type cannot subtract the known `Tok` keys during inference. The
|
|
55
|
+
* `K extends Tok` guard routes those leaked base keys back to a bare binding, so
|
|
56
|
+
* only genuinely new keys demand a full definition. The arm stays optional: a
|
|
57
|
+
* required arm stops `XTok` from inferring when `tokens` is empty, and the
|
|
58
|
+
* fallback to `string` disables every token-name check.
|
|
59
|
+
*
|
|
60
|
+
* `modifiers` admits overrides of an existing axis's contexts and new axes
|
|
61
|
+
* carrying their complete context maps.
|
|
62
|
+
*/
|
|
63
|
+
type Extension<Tok extends string, Mod extends Record<string, Record<string, object>>, XTok extends string, XMod extends Record<string, Record<string, object>>> = {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
tokens: {
|
|
67
|
+
[K in NoInfer<Tok>]?: Binding;
|
|
68
|
+
} & {
|
|
69
|
+
[K in XTok]?: K extends Tok ? Binding : Authored;
|
|
70
|
+
};
|
|
71
|
+
modifiers: {
|
|
72
|
+
[M in keyof Mod]?: {
|
|
73
|
+
[C in keyof Mod[M]]?: {
|
|
74
|
+
[K in NoInfer<Tok | XTok>]?: Binding;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
} & {
|
|
78
|
+
[M in keyof XMod]: {
|
|
79
|
+
[C in keyof XMod[M]]: {
|
|
80
|
+
[K in NoInfer<Tok | XTok>]?: Binding;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
order: ((keyof Mod | keyof XMod) & (string & {}))[];
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The deviation between two themes, as produced by {@link diff}: a token
|
|
88
|
+
* override map and per-modifier, per-context override maps, all present (empty
|
|
89
|
+
* when nothing deviates) so consumers can inspect them without guards.
|
|
90
|
+
*/
|
|
91
|
+
type Diff<T extends Template> = {
|
|
92
|
+
tokens: Overrides<T>;
|
|
93
|
+
modifiers: {
|
|
94
|
+
[M in Modifier<T>]: {
|
|
95
|
+
[C in Context<T, M>]: Overrides<T>;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* A partial overlay of a theme: any subset of identity, tokens, modifiers, and
|
|
101
|
+
* order. Both a `Layer` (identity plus partial overrides) and a `Patch`
|
|
102
|
+
* (anonymous overrides) fit this shape, so one merge serves them all.
|
|
103
|
+
*/
|
|
104
|
+
type Overlay<T extends Template> = {
|
|
105
|
+
id?: string;
|
|
106
|
+
name?: string;
|
|
107
|
+
tokens?: Overrides<T>;
|
|
108
|
+
modifiers?: {
|
|
109
|
+
[M in Modifier<T>]?: {
|
|
110
|
+
[C in Context<T, M>]?: Overrides<T>;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
order?: NoInfer<Modifier<T>>[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Computes the patch that turns `from` into `to`: every binding `to` holds that
|
|
118
|
+
* deviates from `from`, token by token and context by context. At the token
|
|
119
|
+
* level the bound `$value` is compared and emitted — a token's metadata cannot
|
|
120
|
+
* drift through the patch pipeline, so only the binding is carried. Identity and
|
|
121
|
+
* order are not compared. Empty maps mean the themes bind identically; applying
|
|
122
|
+
* the result to `from` via `merge` restores every binding `to` carries. A
|
|
123
|
+
* binding `from` holds that `to` dropped is not restored — a patch can add and
|
|
124
|
+
* override, never remove.
|
|
125
|
+
*/
|
|
126
|
+
declare const diff: <T extends Template>(from: Theme<T>, to: Theme<T>) => Diff<T>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Widens a base contract with an {@link Extension} into a fresh contract over
|
|
130
|
+
* the union token and modifier sets. Neither input is mutated.
|
|
131
|
+
*
|
|
132
|
+
* Tokens compose two ways: an extension key that already names a base token
|
|
133
|
+
* rebinds that token's `$value`, preserving its `$type` and metadata; an
|
|
134
|
+
* extension key beyond the base inserts its full definition as a new token.
|
|
135
|
+
* Modifiers spread the new axes over the base's, then deep-merge each context
|
|
136
|
+
* the two share, the extension's bindings winning per token.
|
|
137
|
+
*
|
|
138
|
+
* The base may be an authored contract or any complete {@link Theme} of one —
|
|
139
|
+
* a machine-built theme (a prior extension's output) widens the same way.
|
|
140
|
+
*/
|
|
141
|
+
declare const extend: <Tok extends string, Mod extends Record<string, Record<string, object>>, XTok extends string, XMod extends Record<string, Record<string, object>>>(base: Contract<Tok, Mod> | Theme<Contract<Tok, Mod>>, extension: Extension<Tok, Mod, XTok, XMod>) => {
|
|
142
|
+
id: string;
|
|
143
|
+
name: string;
|
|
144
|
+
tokens: { [K in Tok | XTok | (NoInfer<Tok> & string)]: Definition; };
|
|
145
|
+
modifiers: { [M in keyof Mod]?: { [C in keyof Mod[M]]?: { [K_1 in NoInfer<Tok | XTok>]?: Binding | undefined; } | undefined; } | undefined; } & { [M_1 in keyof XMod]: { [C in keyof XMod[M_1]]: { [K_1 in NoInfer<Tok | XTok>]?: Binding | undefined; }; }; } & { [M_2 in _untheme_schema.Modifier<Contract<Tok, Mod>>]: { [C in _untheme_schema.Context<Contract<Tok, Mod>, M_2>]: Overrides<Contract<Tok, Mod>>; }; };
|
|
146
|
+
order: ((keyof Mod | keyof XMod) & string)[];
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
declare const guard: (v: unknown) => v is Template;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Merges overlays over a complete theme into a fresh theme, left to right:
|
|
153
|
+
* later overlays win where they bind the same token, token by token and context
|
|
154
|
+
* by context. Identity and order transfer from the last overlay that carries
|
|
155
|
+
* them. No input is mutated; with no overlays the result is a plain copy.
|
|
156
|
+
*
|
|
157
|
+
* A token override rebinds a slot's `$value` and nothing else — the slot's
|
|
158
|
+
* `$type`, description, and other metadata are preserved, and the incoming
|
|
159
|
+
* binding replaces the old `$value` whole rather than merging into it. An
|
|
160
|
+
* overlay key with no matching base slot is skipped: there is no `$type` to
|
|
161
|
+
* inherit, so no definition can be fabricated for it.
|
|
162
|
+
*
|
|
163
|
+
* A `Layer` overlay adopts its identity (apply semantics); a `Patch` overlay
|
|
164
|
+
* has none, so the prevailing identity is preserved (update semantics).
|
|
165
|
+
*/
|
|
166
|
+
declare const merge: <T extends Template>(theme: Theme<T>, ...overlays: Overlay<T>[]) => Theme<T>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Rebuilds a modifiers structure leaf by leaf: every context of every modifier
|
|
170
|
+
* mapped through the callback, each modifier keeping its own context keys. The
|
|
171
|
+
* callback's `at` indexes any other modifiers structure at the leaf's own
|
|
172
|
+
* coordinates, so a leaf can be combined with its counterpart elsewhere; a
|
|
173
|
+
* sparse structure — a layer's or patch's partial modifier map — may hold
|
|
174
|
+
* nothing there, so `at` yields the overrides or `undefined`. The
|
|
175
|
+
* modifier/context pairing is carried generically here, checked once; callers
|
|
176
|
+
* supply only the leaf operation.
|
|
177
|
+
*/
|
|
178
|
+
declare const traverse: <T extends Template, R>(modifiers: Modifiers<T>, fn: (overrides: Overrides<T>, at: (other: { [M in Modifier<T>]?: { [C in Context<T, M>]?: Overrides<T>; }; }) => Overrides<T> | undefined) => R) => { [M in Modifier<T>]: { [C in Context<T, M>]: R; }; };
|
|
179
|
+
|
|
180
|
+
export { clone, copy, delta, diff, extend, guard, merge, traverse };
|
|
181
|
+
export type { Diff, Extension, Overlay };
|
package/.dist/index.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { isEqual, isRecord, entries, remap, map, isDefinition, isObject } from '@untheme/common';
|
|
2
|
+
|
|
3
|
+
const copy = (value) => {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
const result = value.map((entry) => copy(entry));
|
|
6
|
+
if (!isEqual(value, result)) {
|
|
7
|
+
throw new TypeError("could not copy array");
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
if (isRecord(value)) {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const key of Object.keys(value)) {
|
|
14
|
+
result[key] = copy(value[key]);
|
|
15
|
+
}
|
|
16
|
+
if (!isEqual(value, result)) {
|
|
17
|
+
throw new TypeError("could not copy record");
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const clone = (theme) => {
|
|
25
|
+
const result = {
|
|
26
|
+
id: theme.id,
|
|
27
|
+
name: theme.name,
|
|
28
|
+
tokens: copy(theme.tokens),
|
|
29
|
+
modifiers: copy(theme.modifiers),
|
|
30
|
+
order: copy(theme.order)
|
|
31
|
+
};
|
|
32
|
+
if (!isEqual(theme, result)) {
|
|
33
|
+
throw new TypeError("unable to clone a theme");
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const delta = (from, to) => {
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [key, value] of entries(to)) {
|
|
41
|
+
if (!isEqual(from[key], value)) {
|
|
42
|
+
result[key] = copy(value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const traverse = (modifiers, fn) => remap(
|
|
49
|
+
modifiers,
|
|
50
|
+
(contexts, modifier) => remap(
|
|
51
|
+
contexts,
|
|
52
|
+
(overrides, context) => fn(overrides, (other) => other[modifier]?.[context])
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const diff = (from, to) => {
|
|
57
|
+
const tokens = delta(
|
|
58
|
+
map(from.tokens, (slot) => slot.$value),
|
|
59
|
+
map(to.tokens, (slot) => slot.$value)
|
|
60
|
+
);
|
|
61
|
+
const modifiers = traverse(
|
|
62
|
+
to.modifiers,
|
|
63
|
+
(overrides, at) => delta(at(from.modifiers) ?? {}, overrides)
|
|
64
|
+
);
|
|
65
|
+
return { tokens, modifiers };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const extend = (base, extension) => {
|
|
69
|
+
const source = Object.assign({}, extension.tokens, base.tokens);
|
|
70
|
+
const tokens = map(source, (slot, token) => {
|
|
71
|
+
if (slot === void 0 || !isDefinition(slot)) {
|
|
72
|
+
throw new TypeError(`token "${token}" is not a definition`);
|
|
73
|
+
}
|
|
74
|
+
const definition = slot;
|
|
75
|
+
const incoming = extension.tokens[token];
|
|
76
|
+
if (incoming === void 0 || isDefinition(incoming)) {
|
|
77
|
+
return copy(definition);
|
|
78
|
+
}
|
|
79
|
+
const rebound = {
|
|
80
|
+
...copy(definition),
|
|
81
|
+
$value: copy(incoming)
|
|
82
|
+
};
|
|
83
|
+
return rebound;
|
|
84
|
+
});
|
|
85
|
+
const modifiers = {
|
|
86
|
+
...copy(extension.modifiers),
|
|
87
|
+
...traverse(base.modifiers, (overrides, at) => ({
|
|
88
|
+
...copy(overrides),
|
|
89
|
+
...copy(at(extension.modifiers) ?? {})
|
|
90
|
+
}))
|
|
91
|
+
};
|
|
92
|
+
const listed = new Set(extension.order);
|
|
93
|
+
const order = [
|
|
94
|
+
...base.order.filter((modifier) => !listed.has(modifier)),
|
|
95
|
+
...extension.order
|
|
96
|
+
];
|
|
97
|
+
const result = {
|
|
98
|
+
id: extension.id,
|
|
99
|
+
name: extension.name,
|
|
100
|
+
tokens,
|
|
101
|
+
modifiers,
|
|
102
|
+
order
|
|
103
|
+
};
|
|
104
|
+
return result;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const guard = (v) => {
|
|
108
|
+
return typeof v === "object" && v !== null && "id" in v && typeof v["id"] === "string" && "name" in v && typeof v["name"] === "string" && "tokens" in v && isObject(v["tokens"]) && "modifiers" in v && isRecord(v["modifiers"]) && "order" in v && Array.isArray(v["order"]);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const merge = (theme, ...overlays) => {
|
|
112
|
+
return overlays.reduce(
|
|
113
|
+
(acc, overlay) => ({
|
|
114
|
+
id: overlay.id ?? acc.id,
|
|
115
|
+
name: overlay.name ?? acc.name,
|
|
116
|
+
tokens: map(acc.tokens, (slot, token) => {
|
|
117
|
+
const binding = overlay.tokens?.[token];
|
|
118
|
+
if (binding === void 0) {
|
|
119
|
+
return slot;
|
|
120
|
+
}
|
|
121
|
+
return { ...slot, $value: copy(binding) };
|
|
122
|
+
}),
|
|
123
|
+
modifiers: traverse(acc.modifiers, (overrides, at) => ({
|
|
124
|
+
...overrides,
|
|
125
|
+
...copy(at(overlay.modifiers ?? {}) ?? {})
|
|
126
|
+
})),
|
|
127
|
+
order: copy(overlay.order ?? acc.order)
|
|
128
|
+
}),
|
|
129
|
+
clone(theme)
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export { clone, copy, delta, diff, extend, guard, merge, traverse };
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @untheme/utils
|
|
2
|
+
|
|
3
|
+
Common structural helpers for untheme's theme shape. Types come from [`@untheme/schema`](../schema); guards and generic helpers come from [`@untheme/common`](../common).
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
### `merge(theme, ...overlays)`
|
|
8
|
+
|
|
9
|
+
Merges overlays over a complete theme into a fresh theme, left to right: later overlays win where they bind the same token, token by token and context by context. Identity and order transfer from the last overlay that carries them. No input is mutated; with no overlays the result is a plain copy.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { merge } from "@untheme/utils";
|
|
13
|
+
|
|
14
|
+
merge(theme, patch); // patch has no identity → identity preserved (update)
|
|
15
|
+
merge(theme, layer); // layer carries id/name → identity adopted (apply)
|
|
16
|
+
merge(theme, layer, patch); // layer applied, then patched — last binding wins
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### `extend(base, extension)`
|
|
20
|
+
|
|
21
|
+
Widens a base contract with an extension into a fresh contract over the union token and modifier sets. New tokens and new modifiers join; existing tokens are overridden; existing modifier contexts are deep-merged leaf by leaf, so the base's other tokens in that context are kept. The `order` the extension lists wins — any modifier it names is dropped from the base order, then the extension's order is appended. Neither input is mutated.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { extend } from "@untheme/utils";
|
|
25
|
+
|
|
26
|
+
const wide = extend(base, {
|
|
27
|
+
id: "wide",
|
|
28
|
+
name: "Wide",
|
|
29
|
+
tokens: { accent: "#0090ff", bg: "{accent}" }, // new token + override existing
|
|
30
|
+
modifiers: {
|
|
31
|
+
color: { dark: { fg: "{accent}" } }, // override an existing context (leaf)
|
|
32
|
+
density: { compact: {}, cozy: {} }, // a new modifier
|
|
33
|
+
},
|
|
34
|
+
order: ["density", "color"],
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Unlike `merge`, which stays within one contract, `extend` is the widening primitive: the result's token and modifier sets grow to include the extension's.
|
|
39
|
+
|
|
40
|
+
### `diff(from, to)`
|
|
41
|
+
|
|
42
|
+
Computes the patch that turns `from` into `to`: every binding `to` holds that deviates from `from`, token by token and context by context. At the token level only the bound `$value` is compared and emitted — a token's metadata cannot drift through the patch pipeline. Identity and order are not compared. Empty maps mean the themes bind identically; applying the result to `from` via `merge` restores every binding `to` carries. A patch can add and override, never remove — a context override `from` holds that `to` dropped survives the restoration, and identity, order, and slot metadata are not part of it.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { diff, merge } from "@untheme/utils";
|
|
46
|
+
|
|
47
|
+
const deviation = diff(pristine, edited);
|
|
48
|
+
merge(pristine, deviation); // ≅ edited
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Primitives
|
|
52
|
+
|
|
53
|
+
Supporting helpers that `merge`, `extend`, and `diff` are built on.
|
|
54
|
+
|
|
55
|
+
### `clone(theme)`
|
|
56
|
+
|
|
57
|
+
Deep copy of a theme, facet by facet: identity, tokens, modifiers, and order are each rebuilt through `copy`, so no definition object, nested `$value` structure, or override map is shared with the source. Because `copy` reaches every value through plain property access, cloning a reactive proxy yields an inert, plain snapshot. `clone` throws a `TypeError` if the rebuilt theme isn't structurally equal to the source.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { clone } from "@untheme/utils";
|
|
61
|
+
|
|
62
|
+
const snapshot = clone(theme); // detached from any reactive proxy
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `copy(value)`
|
|
66
|
+
|
|
67
|
+
Deep copy of plain data: records and arrays are rebuilt from fresh containers, so mutating the copy never reaches the source. Non-plain values — functions and class instances, such as those a token's `$extensions` may carry — pass through by reference rather than being duplicated. `structuredClone` is never used: it throws on functions and detaches proxies through a mechanism `copy` deliberately avoids.
|
|
68
|
+
|
|
69
|
+
### `delta(from, to)`
|
|
70
|
+
|
|
71
|
+
The entries of `to` that deviate from `from`: every key `to` holds whose value is not structurally equal to the one `from` holds. Emitted values are copies, so the result shares no structure with `to`. Keys with structurally equal values drop out; two objects that bind identically yield an empty result.
|
|
72
|
+
|
|
73
|
+
### `traverse(modifiers, fn)`
|
|
74
|
+
|
|
75
|
+
Rebuilds a modifiers structure leaf by leaf: every context of every modifier is mapped through the callback, each modifier keeping its own context keys. The callback's `at` accessor indexes another (possibly sparse) modifiers structure at the same modifier/context coordinates, so a leaf can be combined with its counterpart elsewhere. This is the primitive `diff`, `merge`, and `extend` are all built on.
|
|
76
|
+
|
|
77
|
+
## Types
|
|
78
|
+
|
|
79
|
+
- `Overlay<T>` — the shape `merge` accepts: any subset of identity, tokens, modifiers, and order. Both a `Layer` and a `Patch` from [`@untheme/schema`](../schema) fit it.
|
|
80
|
+
- `Extension<Tok, Mod, XTok, XMod>` — the shape `extend` accepts: new tokens (`XTok`) and new modifiers (`XMod`) over a base contract's tokens (`Tok`) and modifiers (`Mod`). An existing base token accepts an optional bare binding that rebinds its `$value` only; a new token requires a full slot definition. New modifier axes carry their complete context maps; existing axes take optional overrides.
|
|
81
|
+
- `Diff<T>` — the shape `diff` returns: token and per-context override maps, always present (empty when nothing deviates).
|
|
82
|
+
|
|
83
|
+
## Related
|
|
84
|
+
|
|
85
|
+
- [`@untheme/schema`](../schema) — token contract types and runtime validation.
|
|
86
|
+
- [`@untheme/core`](../core) — the runtime theme service.
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@untheme/utils",
|
|
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
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "unbuild",
|
|
22
|
+
"stub": "unbuild --stub",
|
|
23
|
+
"lint": "eslint .",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "vitest run"
|
|
26
|
+
}
|
|
27
|
+
}
|