zyzz 0.0.14 → 0.0.16

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.
Files changed (42) hide show
  1. package/dist/compiler/Graph.d.ts +3 -1
  2. package/dist/compiler/Graph.d.ts.map +1 -1
  3. package/dist/compiler/Graph.js +58 -5
  4. package/dist/compiler/Graph.js.map +1 -1
  5. package/dist/compiler/Source.d.ts.map +1 -1
  6. package/dist/compiler/Source.js +4 -1
  7. package/dist/compiler/Source.js.map +1 -1
  8. package/dist/compiler/Transform.d.ts +5 -0
  9. package/dist/compiler/Transform.d.ts.map +1 -1
  10. package/dist/compiler/Transform.js +9 -2
  11. package/dist/compiler/Transform.js.map +1 -1
  12. package/dist/compiler/internal/Stylesheets.d.ts +2 -0
  13. package/dist/compiler/internal/Stylesheets.d.ts.map +1 -1
  14. package/dist/compiler/internal/Stylesheets.js +2 -0
  15. package/dist/compiler/internal/Stylesheets.js.map +1 -1
  16. package/dist/default.js +1 -1
  17. package/dist/default.js.map +1 -1
  18. package/dist/default.js.zyzz.json +1 -1
  19. package/dist/internal/Identity.d.ts +4 -0
  20. package/dist/internal/Identity.d.ts.map +1 -1
  21. package/dist/internal/Identity.js +25 -0
  22. package/dist/internal/Identity.js.map +1 -1
  23. package/dist/next/loader.d.ts.map +1 -1
  24. package/dist/next/loader.js +17 -4
  25. package/dist/next/loader.js.map +1 -1
  26. package/dist/web/Css.d.ts +5 -0
  27. package/dist/web/Css.d.ts.map +1 -1
  28. package/dist/web/Css.js +3 -2
  29. package/dist/web/Css.js.map +1 -1
  30. package/dist/web/internal/Themes.d.ts +33 -1
  31. package/dist/web/internal/Themes.d.ts.map +1 -1
  32. package/dist/web/internal/Themes.js +134 -29
  33. package/dist/web/internal/Themes.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/compiler/Graph.ts +70 -6
  36. package/src/compiler/Source.ts +6 -1
  37. package/src/compiler/Transform.ts +13 -2
  38. package/src/compiler/internal/Stylesheets.ts +3 -0
  39. package/src/internal/Identity.ts +32 -0
  40. package/src/next/loader.ts +17 -8
  41. package/src/web/Css.ts +7 -1
  42. package/src/web/internal/Themes.ts +190 -31
@@ -17,7 +17,7 @@ export function create() {
17
17
  { identity: string | undefined; paths: Map<string, string> }
18
18
  >()
19
19
 
20
- const defaults = new Map<string, string>()
20
+ const defaults = new Map<string, readonly Rule[]>()
21
21
 
22
22
  function serialize(token: Token.Reference): string {
23
23
  const value = token.value
@@ -41,15 +41,18 @@ export function create() {
41
41
  contract.paths.set(token.path, name)
42
42
  }
43
43
 
44
- return `var(${name},${literal(value)})`
44
+ const label = contract.identity?.startsWith('src-') ? token.path : undefined
45
+ return `var(${name},${literal(value, label)})`
45
46
  }
46
47
 
47
48
  function emit(
48
49
  themes: Readonly<Record<string, Theme.Definition | Vars.Definition>>,
49
50
  schemes = false,
51
+ separate?: 'all' | 'defaults',
50
52
  ) {
51
53
  const classes: Record<string, string> = Object.create(null)
52
- const rules: string[] = []
54
+ const empty: string[] = []
55
+ const rules: Rule[] = []
53
56
 
54
57
  for (const [name, theme] of Object.entries(themes)) {
55
58
  if (!name) throw new Error('Theme names must be nonempty.')
@@ -94,55 +97,134 @@ export function create() {
94
97
  }
95
98
 
96
99
  if (!contract) continue
100
+ if (!contract.paths.size) empty.push(`.${className}{}`)
97
101
 
98
- const body = [...contract.paths]
99
- .map(([path, name]) => {
100
- const value = data.values[path]
101
- if (value === undefined)
102
- throw new Error('Theme scope is missing a live token.')
102
+ const declarations: Rule[] = []
103
+ // Resolving a value can add references to this contract.
104
+ const paths = [...contract.paths]
105
+ for (const [path, name] of paths) {
106
+ const value = data.values[path]
107
+ if (value === undefined)
108
+ throw new Error('Theme scope is missing a live token.')
103
109
 
104
- return `${name}:${literal(value)};`
110
+ const label = contract.identity?.startsWith('src-') ? path : undefined
111
+ declarations.push({
112
+ conditions: [],
113
+ property: name,
114
+ selector: `.${className}`,
115
+ value: literal(value, label),
105
116
  })
106
- .join('')
117
+ }
107
118
 
108
- rules.push(`.${className}{${body}}`)
119
+ rules.push(...declarations)
109
120
  for (const [path, name] of contract.paths)
110
- conditional(data.values[path]!, name, `.${className}`, rules)
121
+ conditional(
122
+ data.values[path]!,
123
+ name,
124
+ `.${className}`,
125
+ rules,
126
+ contract.identity?.startsWith('src-') ? path : undefined,
127
+ )
111
128
  }
112
129
 
113
- // Scheme rules travel with the module that can apply them, so lowered
114
- // light-dark() resolves wherever selection helpers load.
115
- if (schemes) rules.push(Scheme.css)
130
+ const scopes = new Map<string, Rule[]>()
131
+ for (const rule of rules) {
132
+ const entries = scopes.get(rule.property) ?? []
133
+ entries.push(rule)
134
+ scopes.set(rule.property, entries)
135
+ }
116
136
 
117
137
  return {
118
138
  classes: Object.freeze(classes),
119
- css: [...defaults.values(), ...rules].join('\n'),
139
+ css:
140
+ separate === 'all'
141
+ ? ''
142
+ : [
143
+ ...empty,
144
+ render([
145
+ ...(separate ? [] : [...defaults.values()].flat()),
146
+ ...rules,
147
+ ]),
148
+ ...(schemes ? [Scheme.css] : []),
149
+ ]
150
+ .filter(Boolean)
151
+ .join('\n'),
152
+ resources: separate
153
+ ? [
154
+ ...[...defaults].map(([id, rules]) => ({
155
+ css: render(rules),
156
+ id,
157
+ rules,
158
+ })),
159
+ // Keep every scope of a property together so overlapping themes retain precedence.
160
+ ...(separate === 'all'
161
+ ? [...scopes].map(([property, rules]) => ({
162
+ css: render(rules),
163
+ id: JSON.stringify([classes, property]),
164
+ rules,
165
+ }))
166
+ : []),
167
+ ...(separate === 'all' && schemes
168
+ ? [{ css: Scheme.css, id: 'zyzz-color-scheme' }]
169
+ : []),
170
+ ]
171
+ : [],
120
172
  }
121
173
  }
122
174
 
123
- function literal(value: Token.Value): string {
175
+ function literal(value: Token.Value, label?: string): string {
124
176
  if (Token.is(value)) return serialize(value)
125
177
  if (typeof value !== 'object') return String(value)
126
178
  if ('default' in value) {
127
179
  // Separate fallback properties preserve extensions and resolve references within each scope.
128
- const base = literal(value.default)
129
- const rules = [`:where(*){--fallback:${base};}`]
130
- conditional(value, '--fallback', ':where(*)', rules)
131
- const css = rules.join('')
132
- const name = `--z-f${Identity.hash(css)}`
133
- const emitted = [`:where(*){${name}:${base};}`]
134
- conditional(value, name, ':where(*)', emitted)
135
- defaults.set(name, emitted.join(''))
180
+ const base = literal(value.default, label)
181
+ // Rule consolidation must not change existing variable identities.
182
+ const css = `:where(*){--fallback:${base};}${conditionalCss(value, '--fallback', ':where(*)', label)}`
183
+ const name = label
184
+ ? `--z-${Identity.label(label)}-fallback-${Identity.compact(css)}`
185
+ : `--z-f${Identity.hash(css)}`
186
+ const emitted: Rule[] = [
187
+ { conditions: [], property: name, selector: ':where(*)', value: base },
188
+ ]
189
+ conditional(value, name, ':where(*)', emitted, label)
190
+ defaults.set(name, emitted)
136
191
  return `var(${name})`
137
192
  }
138
- return `light-dark(${literal(value.light)},${literal(value.dark)})`
193
+ return `light-dark(${literal(value.light, label)},${literal(value.dark, label)})`
194
+ }
195
+
196
+ function conditionalCss(
197
+ value: Token.Value,
198
+ property: string,
199
+ selector: string,
200
+ label?: string,
201
+ ): string {
202
+ if (
203
+ !value ||
204
+ typeof value !== 'object' ||
205
+ Token.is(value) ||
206
+ !('default' in value)
207
+ )
208
+ return ''
209
+ return (
210
+ conditionalCss(value.default, property, selector, label) +
211
+ Object.entries(value)
212
+ .filter(([query]) => query !== 'default')
213
+ .map(
214
+ ([query, entry]) =>
215
+ `${query}{${selector}{${property}:${literal(entry, label)};}${conditionalCss(entry, property, selector, label)}}`,
216
+ )
217
+ .join('')
218
+ )
139
219
  }
140
220
 
141
221
  function conditional(
142
222
  value: Token.Value,
143
223
  name: string,
144
224
  selector: string,
145
- rules: string[],
225
+ rules: Rule[],
226
+ label?: string,
227
+ conditions: readonly string[] = [],
146
228
  ) {
147
229
  if (
148
230
  !value ||
@@ -151,12 +233,17 @@ export function create() {
151
233
  !('default' in value)
152
234
  )
153
235
  return
154
- conditional(value.default, name, selector, rules)
236
+ conditional(value.default, name, selector, rules, label, conditions)
155
237
  for (const [query, entry] of Object.entries(value)) {
156
238
  if (query === 'default') continue
157
- const nested = [`${selector}{${name}:${literal(entry)};}`]
158
- conditional(entry, name, selector, nested)
159
- rules.push(`${query}{${nested.join('')}}`)
239
+ const nested = [...conditions, query]
240
+ rules.push({
241
+ conditions: nested,
242
+ property: name,
243
+ selector,
244
+ value: literal(entry, label),
245
+ })
246
+ conditional(entry, name, selector, rules, label, nested)
160
247
  }
161
248
  }
162
249
 
@@ -171,6 +258,78 @@ export function encode(value: string): string {
171
258
  )
172
259
  }
173
260
 
261
+ /** Combines generated declarations without crossing competing declarations of the same property. */
262
+ export function render(rules: readonly Rule[]): string {
263
+ const groups: {
264
+ conditions: readonly string[]
265
+ declarations: Rule[]
266
+ selector: string
267
+ }[] = []
268
+ for (const rule of rules) {
269
+ let target: (typeof groups)[number] | undefined
270
+ for (let index = groups.length - 1; index >= 0; index--) {
271
+ const group = groups[index]!
272
+ if (
273
+ group.selector === rule.selector &&
274
+ JSON.stringify(group.conditions) === JSON.stringify(rule.conditions)
275
+ ) {
276
+ target = group
277
+ break
278
+ }
279
+ if (group.declarations.some((entry) => entry.property === rule.property))
280
+ break
281
+ }
282
+ if (!target) {
283
+ target = {
284
+ conditions: rule.conditions,
285
+ declarations: [],
286
+ selector: rule.selector,
287
+ }
288
+ groups.push(target)
289
+ }
290
+ const previous = target.declarations.findLast(
291
+ (entry) => entry.property === rule.property,
292
+ )
293
+ if (previous?.value !== rule.value) target.declarations.push(rule)
294
+ }
295
+ return groups
296
+ .map(({ conditions, declarations, selector }) => {
297
+ let css = `${selector}{${declarations.map(({ property, value }) => `${property}:${value};`).join('')}}`
298
+ for (const condition of [...conditions].reverse())
299
+ css = `${condition}{${css}}`
300
+ return css
301
+ })
302
+ .join('\n')
303
+ }
304
+
305
+ /** One generated definition whose scope order must remain intact. */
306
+ export type Resource = {
307
+ /** Complete fallback or scoped token definition. */
308
+ readonly css: string
309
+ /** Stable identity for stylesheet replacement during development. */
310
+ readonly id: string
311
+ /** Generated declarations eligible for consolidation. */
312
+ readonly rules?: readonly Rule[] | undefined
313
+ }
314
+
315
+ /** One generated custom-property declaration and its ordered conditional scope. */
316
+ export type Rule = {
317
+ /** Outer-to-inner conditional rules. */
318
+ readonly conditions: readonly string[]
319
+ /** Generated custom-property name. */
320
+ readonly property: string
321
+ /** Theme or universal fallback selector. */
322
+ readonly selector: string
323
+ /** Serialized custom-property value. */
324
+ readonly value: string
325
+ }
326
+
327
+ /** Selects independently shareable defaults or all token rules. */
328
+ export const shared = Symbol('shared token rules')
329
+
174
330
  function variable(index: number | string, path: string): string {
331
+ // Packed declarations retain the spelling assigned by their compiler.
332
+ if (typeof index === 'string' && index.startsWith('src-'))
333
+ return `--z-${Identity.label(path)}-${Identity.compact(JSON.stringify([index, path]))}`
175
334
  return `--z-t${encode(String(index))}-${encode(path)}`
176
335
  }