zyzz 0.0.20 → 0.0.21

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 (131) hide show
  1. package/dist/Style.d.ts +7 -0
  2. package/dist/Style.d.ts.map +1 -1
  3. package/dist/Style.js +1 -5
  4. package/dist/Style.js.map +1 -1
  5. package/dist/compiler/Graph.d.ts +3 -0
  6. package/dist/compiler/Graph.d.ts.map +1 -1
  7. package/dist/compiler/Graph.js +44 -37
  8. package/dist/compiler/Graph.js.map +1 -1
  9. package/dist/compiler/Source.d.ts +4 -0
  10. package/dist/compiler/Source.d.ts.map +1 -1
  11. package/dist/compiler/Source.js +63 -6
  12. package/dist/compiler/Source.js.map +1 -1
  13. package/dist/compiler/Transform.d.ts +4 -1
  14. package/dist/compiler/Transform.d.ts.map +1 -1
  15. package/dist/compiler/Transform.js +32 -46
  16. package/dist/compiler/Transform.js.map +1 -1
  17. package/dist/compiler/internal/Catalogs.d.ts.map +1 -1
  18. package/dist/compiler/internal/Catalogs.js +6 -1
  19. package/dist/compiler/internal/Catalogs.js.map +1 -1
  20. package/dist/compiler/internal/Compositions.d.ts.map +1 -1
  21. package/dist/compiler/internal/Compositions.js +5 -0
  22. package/dist/compiler/internal/Compositions.js.map +1 -1
  23. package/dist/compiler/internal/Configurations.d.ts.map +1 -1
  24. package/dist/compiler/internal/Configurations.js +2 -1
  25. package/dist/compiler/internal/Configurations.js.map +1 -1
  26. package/dist/compiler/internal/Contract.d.ts +4 -1
  27. package/dist/compiler/internal/Contract.d.ts.map +1 -1
  28. package/dist/compiler/internal/Contract.js +56 -147
  29. package/dist/compiler/internal/Contract.js.map +1 -1
  30. package/dist/compiler/internal/Contributions.d.ts.map +1 -1
  31. package/dist/compiler/internal/Contributions.js +1 -3
  32. package/dist/compiler/internal/Contributions.js.map +1 -1
  33. package/dist/compiler/internal/Dynamic.d.ts +1 -1
  34. package/dist/compiler/internal/Dynamic.d.ts.map +1 -1
  35. package/dist/compiler/internal/Dynamic.js +5 -5
  36. package/dist/compiler/internal/Dynamic.js.map +1 -1
  37. package/dist/compiler/internal/Names.d.ts +5 -0
  38. package/dist/compiler/internal/Names.d.ts.map +1 -0
  39. package/dist/compiler/internal/Names.js +37 -0
  40. package/dist/compiler/internal/Names.js.map +1 -0
  41. package/dist/compiler/internal/Selectors.d.ts +1 -1
  42. package/dist/compiler/internal/Selectors.d.ts.map +1 -1
  43. package/dist/compiler/internal/Selectors.js +5 -2
  44. package/dist/compiler/internal/Selectors.js.map +1 -1
  45. package/dist/compiler/internal/Stylesheets.js +2 -2
  46. package/dist/compiler/internal/Stylesheets.js.map +1 -1
  47. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  48. package/dist/compiler/internal/Themes.js +2 -1
  49. package/dist/compiler/internal/Themes.js.map +1 -1
  50. package/dist/compiler/internal/Vars.d.ts +2 -0
  51. package/dist/compiler/internal/Vars.d.ts.map +1 -1
  52. package/dist/compiler/internal/Vars.js +7 -1
  53. package/dist/compiler/internal/Vars.js.map +1 -1
  54. package/dist/default.d.ts +4 -4
  55. package/dist/default.d.ts.map +1 -1
  56. package/dist/default.js +9 -9
  57. package/dist/default.js.map +2 -2
  58. package/dist/default.js.zyzz.json +1 -1
  59. package/dist/internal/Authoring.d.ts.map +1 -1
  60. package/dist/internal/Authoring.js +8 -3
  61. package/dist/internal/Authoring.js.map +1 -1
  62. package/dist/internal/Binding.d.ts +4 -0
  63. package/dist/internal/Binding.d.ts.map +1 -1
  64. package/dist/internal/Binding.js +2 -0
  65. package/dist/internal/Binding.js.map +1 -1
  66. package/dist/internal/Configuration.d.ts +2 -2
  67. package/dist/internal/Configuration.d.ts.map +1 -1
  68. package/dist/internal/Configuration.js +4 -2
  69. package/dist/internal/Configuration.js.map +1 -1
  70. package/dist/internal/Identity.d.ts +3 -3
  71. package/dist/internal/Identity.d.ts.map +1 -1
  72. package/dist/internal/Identity.js +11 -9
  73. package/dist/internal/Identity.js.map +1 -1
  74. package/dist/internal/Theme.js +2 -2
  75. package/dist/internal/Theme.js.map +1 -1
  76. package/dist/internal/Token.d.ts +5 -1
  77. package/dist/internal/Token.d.ts.map +1 -1
  78. package/dist/internal/Token.js +9 -2
  79. package/dist/internal/Token.js.map +1 -1
  80. package/dist/internal/VariableSets.js +1 -1
  81. package/dist/internal/VariableSets.js.map +1 -1
  82. package/dist/node/Host.d.ts.map +1 -1
  83. package/dist/node/Host.js +2 -0
  84. package/dist/node/Host.js.map +1 -1
  85. package/dist/variable.js +1 -1
  86. package/dist/variable.js.map +1 -1
  87. package/dist/vite/index.d.ts.map +1 -1
  88. package/dist/vite/index.js +36 -12
  89. package/dist/vite/index.js.map +1 -1
  90. package/dist/web/Css.d.ts +7 -2
  91. package/dist/web/Css.d.ts.map +1 -1
  92. package/dist/web/Css.js +46 -39
  93. package/dist/web/Css.js.map +1 -1
  94. package/dist/web/internal/ClassName.d.ts +13 -2
  95. package/dist/web/internal/ClassName.d.ts.map +1 -1
  96. package/dist/web/internal/ClassName.js +17 -30
  97. package/dist/web/internal/ClassName.js.map +1 -1
  98. package/dist/web/internal/Themes.d.ts +3 -0
  99. package/dist/web/internal/Themes.d.ts.map +1 -1
  100. package/dist/web/internal/Themes.js +48 -24
  101. package/dist/web/internal/Themes.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/Style.ts +4 -1
  104. package/src/compiler/Graph.ts +60 -46
  105. package/src/compiler/Source.ts +83 -5
  106. package/src/compiler/Transform.ts +42 -60
  107. package/src/compiler/internal/Catalogs.ts +9 -1
  108. package/src/compiler/internal/Compositions.ts +7 -0
  109. package/src/compiler/internal/Configurations.ts +2 -1
  110. package/src/compiler/internal/Contract.ts +75 -253
  111. package/src/compiler/internal/Contributions.ts +1 -5
  112. package/src/compiler/internal/Dynamic.ts +14 -9
  113. package/src/compiler/internal/Names.ts +48 -0
  114. package/src/compiler/internal/Selectors.ts +6 -1
  115. package/src/compiler/internal/Stylesheets.ts +2 -2
  116. package/src/compiler/internal/Themes.ts +6 -1
  117. package/src/compiler/internal/Vars.ts +9 -1
  118. package/src/default.ts +9 -10
  119. package/src/internal/Authoring.ts +8 -2
  120. package/src/internal/Binding.ts +5 -0
  121. package/src/internal/Configuration.ts +7 -4
  122. package/src/internal/Identity.ts +14 -12
  123. package/src/internal/Theme.ts +2 -2
  124. package/src/internal/Token.ts +13 -1
  125. package/src/internal/VariableSets.ts +1 -1
  126. package/src/node/Host.ts +3 -0
  127. package/src/variable.ts +1 -1
  128. package/src/vite/index.ts +43 -18
  129. package/src/web/Css.ts +83 -49
  130. package/src/web/internal/ClassName.ts +31 -36
  131. package/src/web/internal/Themes.ts +75 -29
package/src/default.ts CHANGED
@@ -41,11 +41,10 @@ void [spin, ping, pulse, bounce]
41
41
  */
42
42
  export const tokens = {
43
43
  animate: {
44
- spin: 'z-kid-7a-79-7a-7a-2d-73-70-69-6e 1s linear infinite',
45
- ping: 'z-kid-7a-79-7a-7a-2d-70-69-6e-67 1s cubic-bezier(0, 0, 0.2, 1) infinite',
46
- pulse:
47
- 'z-kid-7a-79-7a-7a-2d-70-75-6c-73-65 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
48
- bounce: 'z-kid-7a-79-7a-7a-2d-62-6f-75-6e-63-65 1s infinite',
44
+ spin: 'z-kid-zyzz_2d_spin 1s linear infinite',
45
+ ping: 'z-kid-zyzz_2d_ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
46
+ pulse: 'z-kid-zyzz_2d_pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
47
+ bounce: 'z-kid-zyzz_2d_bounce 1s infinite',
49
48
  },
50
49
  aspect: {
51
50
  video: '16 / 9',
@@ -680,13 +679,13 @@ export const tokens = {
680
679
  /** Default appearance, restoration script, and authoring helpers with bundled light/dark tokens. */
681
680
  // Generated from tokens by scripts/default-theme.ts; edit tokens and regenerate.
682
681
  const config = Config.create({
682
+ id: 'default',
683
683
  vars: {
684
684
  animate: {
685
- spin: 'z-kid-7a-79-7a-7a-2d-73-70-69-6e 1s linear infinite',
686
- ping: 'z-kid-7a-79-7a-7a-2d-70-69-6e-67 1s cubic-bezier(0, 0, 0.2, 1) infinite',
687
- pulse:
688
- 'z-kid-7a-79-7a-7a-2d-70-75-6c-73-65 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
689
- bounce: 'z-kid-7a-79-7a-7a-2d-62-6f-75-6e-63-65 1s infinite',
685
+ spin: 'z-kid-zyzz_2d_spin 1s linear infinite',
686
+ ping: 'z-kid-zyzz_2d_ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
687
+ pulse: 'z-kid-zyzz_2d_pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
688
+ bounce: 'z-kid-zyzz_2d_bounce 1s infinite',
690
689
  },
691
690
  aspect: {
692
691
  video: '16 / 9',
@@ -3,6 +3,7 @@ import type { style } from '../styleFunction.js'
3
3
  import * as Html from '../runtime/CompositionHtml.js'
4
4
  import * as Identity from './Identity.js'
5
5
  import * as Props from '../runtime/Props.js'
6
+ import * as Token from './Token.js'
6
7
  import * as Style from '../Style.js'
7
8
  import type * as Theme from './Theme.js'
8
9
 
@@ -66,10 +67,12 @@ export function create(
66
67
  options: Options = {},
67
68
  ): style.ReturnType {
68
69
  const dynamic = typeof input === 'function'
70
+ const identity = options.theme?.[Token.definition].contract[Token.identity]
71
+ const namespace = identity?.startsWith('id-') ? identity.slice(3) : undefined
69
72
  const id =
70
73
  options.id === undefined
71
74
  ? undefined
72
- : Identity.requireId(options.id, 'style')
75
+ : `id-${namespace ? `${namespace}-` : ''}${Identity.requireId(options.id, 'style').slice(3)}`
73
76
  if (dynamic && !id) Identity.requireId(undefined, 'Dynamic style')
74
77
  if (!dynamic && !id && Object.keys(input as object).length === 0)
75
78
  Identity.requireId(undefined, 'Empty style')
@@ -90,6 +93,7 @@ export function create(
90
93
  { style: body(input as Record<string, unknown>) },
91
94
  { vars: options.theme },
92
95
  ).styles[0]!,
96
+ namespace,
93
97
  )
94
98
  const props = Props.create({ className })
95
99
  const apply = (values?: style.Options & Record<string, unknown>) => {
@@ -128,7 +132,9 @@ export function variants(
128
132
  input: Record<string, unknown>,
129
133
  options: Options = {},
130
134
  ): unknown {
131
- const id = Identity.requireId(options.id, 'variants')
135
+ const identity = options.theme?.[Token.definition].contract[Token.identity]
136
+ const namespace = identity?.startsWith('id-') ? identity.slice(3) : undefined
137
+ const id = `id-${namespace ? `${namespace}-` : ''}${Identity.requireId(options.id, 'variants').slice(3)}`
132
138
  if (options.theme) {
133
139
  const styles = [
134
140
  input.base,
@@ -15,10 +15,15 @@ export type Kind =
15
15
  | 'signedLength'
16
16
  | 'signedPercentage'
17
17
 
18
+ /** Marks callback inputs that require an explicit custom token escape. */
19
+ export const dynamic = Symbol('zyzz.binding.dynamic')
20
+
18
21
  /** A compiler-assigned web custom property with its scalar domain. */
19
22
  export type Reference<kind extends Domain = Domain> = {
20
23
  /** Fixed CSS custom-property name. */
21
24
  readonly name: `--${string}`
25
+ /** Internal callback provenance, independent of the emitted variable name. */
26
+ readonly [dynamic]?: true | undefined
22
27
  /** Scalar domain used by authoring types and runtime primitive checks. */
23
28
  readonly type: kind
24
29
  /** Distinguishes slots from theme references. */
@@ -163,6 +163,9 @@ export function create(
163
163
  )
164
164
  throw new InvalidError(`Unknown configuration option: ${key}`)
165
165
 
166
+ if (input.id !== undefined && (typeof input.id !== 'string' || !input.id))
167
+ throw new InvalidError('id must be a nonempty string.')
168
+
166
169
  if (
167
170
  input.storageKey !== undefined &&
168
171
  (typeof input.storageKey !== 'string' || !input.storageKey)
@@ -291,7 +294,7 @@ export function create(
291
294
  function handle(theme: Theme.Definition, name: string) {
292
295
  const original = Token.bind(theme, contract)
293
296
  const className = () =>
294
- `z_theme-${Identity.requireId(typeof input.id === 'string' ? input.id : undefined, 'Config.create')}-${name.replace(/[^a-zA-Z0-9-]/g, (character) => `_${character.charCodeAt(0).toString(16)}_`)}`
297
+ `z-theme-${Identity.requireId(typeof input.id === 'string' ? input.id : undefined, 'Config.create').slice(3)}-${Identity.name(name)}`
295
298
  const select = (options: { colorScheme?: string } = {}) => {
296
299
  const scheme = options.colorScheme
297
300
  const selection =
@@ -433,7 +436,7 @@ export function create(
433
436
  }
434
437
 
435
438
  const theme =
436
- shorthands || input.defaultLayer !== undefined
439
+ shorthands || input.defaultLayer !== undefined || input.id !== undefined
437
440
  ? Token.bind(Theme.define({}), contract)
438
441
  : undefined
439
442
  return finish({
@@ -452,7 +455,7 @@ export declare namespace create {
452
455
  readonly cssOutput?: 'atomic' | 'grouped' | undefined
453
456
  /** Fallback CSS layer for bound styles and variants; unlayered when omitted. */
454
457
  readonly defaultLayer?: string | undefined
455
- /** Stable theme identity required without source rewriting. */
458
+ /** Consumer-owned namespace; required for uncompiled variable configs. */
456
459
  readonly id?: string | undefined
457
460
  /** Explicit ordered property aliases; none are installed by default. */
458
461
  readonly shorthands?: Shorthands.Map | undefined
@@ -839,7 +842,7 @@ export type VariableOptions = {
839
842
  readonly defaultLayer?: string | undefined
840
843
  /** Required default key when vars contains named sets. */
841
844
  readonly defaultVars?: string | undefined
842
- /** Stable identity required without source rewriting. */
845
+ /** Consumer-owned namespace; required for uncompiled variable configs. */
843
846
  readonly id?: string | undefined
844
847
  /** Ordered CSS layer names. */
845
848
  readonly layers?: readonly string[] | undefined
@@ -25,13 +25,6 @@ export function compact(value: string): string {
25
25
  return result
26
26
  }
27
27
 
28
- /** Encodes an explicit identity without losing punctuation or Unicode distinctions. */
29
- export function encode(value: string): string {
30
- return Array.from(value, (character) =>
31
- character.codePointAt(0)!.toString(16),
32
- ).join('-')
33
- }
34
-
35
28
  /** Keeps an authored label readable while its separate hash disambiguates spelling. */
36
29
  export function label(value: string): string {
37
30
  return (
@@ -42,18 +35,27 @@ export function label(value: string): string {
42
35
  )
43
36
  }
44
37
 
38
+ /** Encodes one name segment, reserving hyphens for path boundaries. */
39
+ export function name(value: string): string {
40
+ return Array.from(value, (character) =>
41
+ /^[a-zA-Z0-9]$/.test(character)
42
+ ? character
43
+ : `_${character.codePointAt(0)!.toString(16)}_`,
44
+ ).join('')
45
+ }
46
+
45
47
  /** Requires an explicit identity at an uncompiled authoring boundary. */
46
48
  export function requireId(id: string | undefined, kind: string): string {
47
49
  if (!id)
48
50
  throw new Error(
49
51
  `${kind} requires an explicit id without the compiler plugin.`,
50
52
  )
51
- return `id-${encode(id)}`
53
+ return `id-${name(id)}`
52
54
  }
53
55
 
54
56
  /** Names one callback input independently of its declaration order. */
55
57
  export function slot(id: string, field: string): `--${string}` {
56
- return `--z-d${id}-${encode(field)}`
58
+ return `--z-${id.startsWith('id-') ? id.slice(3) : id}-${name(field)}`
57
59
  }
58
60
 
59
61
  /** Hashes a canonical value using two independent 32-bit accumulators. */
@@ -69,7 +71,7 @@ export function hash(value: string): string {
69
71
  }
70
72
 
71
73
  /** Names ordered style data consistently in runtime and extraction. */
72
- export function style(input: Style.NamedStyle): string {
74
+ export function style(input: Style.NamedStyle, namespace?: string): string {
73
75
  function value(input: Style.Declaration['value']): unknown {
74
76
  if (Binding.is(input)) return ['variable', input.name]
75
77
  if (Token.is(input))
@@ -105,12 +107,12 @@ export function style(input: Style.NamedStyle): string {
105
107
  !!declaration.important,
106
108
  ])
107
109
  }
108
- return `z-content-${hash(JSON.stringify(shape(input)))}`
110
+ return `z-${namespace ? `${namespace}-` : ''}content-${name(JSON.stringify(shape(input)))}`
109
111
  }
110
112
 
111
113
  /** Names a complete ordered composition, including repeated definitions. */
112
114
  export function composition(names: readonly string[]): string {
113
- return `z-compose-${hash(JSON.stringify(names))}`
115
+ return `z-compose-${name(JSON.stringify(names))}`
114
116
  }
115
117
 
116
118
  /** Names a stylesheet declaration with a caller-owned identity. */
@@ -599,10 +599,10 @@ function build(
599
599
  Object.defineProperty(
600
600
  {
601
601
  get className(): string {
602
- if (scope) return `z_theme-${scope}`
602
+ if (scope) return `z-theme-${scope}`
603
603
  const id = contract[Token.identity]
604
604
  if (!id?.startsWith('id-')) throw new MissingTransformError()
605
- return `z_theme-${id}-${Identity.hash(JSON.stringify(values))}`
605
+ return `z-theme-${id.slice(3)}`
606
606
  },
607
607
  style: (styles?: unknown, options: style.DefinitionOptions = {}) => {
608
608
  if (!contract[Token.identity])
@@ -333,6 +333,7 @@ export function acceptsReference(
333
333
  export function bind<tokens extends Theme.Tokens>(
334
334
  original: Theme.Definition<tokens>,
335
335
  contract: Contract,
336
+ cssName?: string,
336
337
  ): Theme.Definition<tokens> {
337
338
  type Tree = { [key: string]: Reference | Tree }
338
339
 
@@ -371,7 +372,14 @@ export function bind<tokens extends Theme.Tokens>(
371
372
  vars: variables(tokens),
372
373
  },
373
374
  definition,
374
- { value: Object.freeze({ ...metadata, contract, values }) },
375
+ {
376
+ value: Object.freeze({
377
+ ...metadata,
378
+ ...(cssName !== undefined ? { cssName } : {}),
379
+ contract,
380
+ values,
381
+ }),
382
+ },
375
383
  ),
376
384
  ) as Theme.Definition<tokens>
377
385
  }
@@ -383,6 +391,8 @@ export const complete = Symbol('zyzz.contract.complete')
383
391
  export type Contract = {
384
392
  /** Whether values belong to independent variables rather than fixed theme categories. */
385
393
  readonly variableSet?: boolean | undefined
394
+ /** Custom-property spelling retained by older packed producers. */
395
+ readonly variableNaming?: 'legacy' | 'hashed' | undefined
386
396
  /** Configuration-local category-to-property mappings. */
387
397
  readonly mappings?: VariableSets.Mappings | false | undefined
388
398
  /** Web emission mode retained by configuration-bound theme handles. */
@@ -520,6 +530,8 @@ export function is(value: unknown): value is Reference {
520
530
 
521
531
  /** Immutable theme data carried directly by each definition. */
522
532
  export type Metadata = {
533
+ /** Authored scope name, independent of private graph ownership. */
534
+ readonly cssName?: string | undefined
523
535
  /** Original segments for typography paths containing query punctuation. */
524
536
  readonly paths?: Readonly<Record<string, readonly string[]>> | undefined
525
537
  readonly queries?: Query.Metadata | undefined
@@ -33,7 +33,7 @@ export function theme(
33
33
  get: () => {
34
34
  const identity = metadata.contract[Token.identity]
35
35
  return identity
36
- ? `z_theme-${identity}-${Identity.hash(JSON.stringify(metadata.values))}`
36
+ ? `z-theme-${identity.startsWith('id-') ? identity.slice(3) : Identity.name(identity)}`
37
37
  : original.className
38
38
  },
39
39
  enumerable: true,
package/src/node/Host.ts CHANGED
@@ -17,6 +17,7 @@ import * as Snapshot from './internal/Snapshot.js'
17
17
  import * as Syntax from '../compiler/internal/Syntax.js'
18
18
  import * as Source from '../compiler/Source.js'
19
19
  import * as Transform from '../compiler/Transform.js'
20
+ import * as ThemeRules from '../web/internal/Themes.js'
20
21
 
21
22
  /** A successful publication; paths are relative to the output directory. */
22
23
  export type Build = {
@@ -458,6 +459,8 @@ export async function create(options: create.Options): Promise<Runtime> {
458
459
  }
459
460
 
460
461
  const graph = compiler.compile({
462
+ [ThemeRules.shared]:
463
+ options.modules === false && !native ? 'all' : undefined,
461
464
  [Syntax.cache]: snapshot.programs(modules),
462
465
  compiler: options.compiler,
463
466
  native,
package/src/variable.ts CHANGED
@@ -38,7 +38,7 @@ export function variable(
38
38
  ): unknown {
39
39
  const id = typeof kind === 'object' ? kind.id : options?.id
40
40
  return Variable.create({
41
- name: `--z-v${Identity.requireId(id, 'variable')}`,
41
+ name: `--z-${Identity.requireId(id, 'variable').slice(3)}`,
42
42
  type: typeof kind === 'string' ? kind : '*',
43
43
  variable: true,
44
44
  })
package/src/vite/index.ts CHANGED
@@ -1391,9 +1391,14 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1391
1391
  await updateDiscovery(this.environment, file, change.event)
1392
1392
  },
1393
1393
  async hotUpdate({ file, modules, timestamp, type, server }) {
1394
- await updateDiscovery(this.environment, file, type)
1395
-
1396
1394
  const state = entries(this.environment)
1395
+ const previous = [...state.values()].map((entry) => ({
1396
+ code: entry.code,
1397
+ file: entry.file,
1398
+ imports: entry.imports,
1399
+ }))
1400
+
1401
+ await updateDiscovery(this.environment, file, type)
1397
1402
  const first = [...state.values()].find(
1398
1403
  (entry) => entry.files.has(file) || eager(file, this.environment),
1399
1404
  )
@@ -1417,17 +1422,41 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1417
1422
  true,
1418
1423
  ).catch(() => undefined)
1419
1424
  : undefined
1420
- const affected = new Set(
1421
- modules.filter((module) => {
1422
- const entry = module.id && state.get(module.id)
1423
- return (
1424
- !output ||
1425
- !entry ||
1426
- entry.code !== output.modules[sourceId(entry.file)]?.code ||
1427
- entry.imports !==
1425
+ const javascriptChanged = await (async () => {
1426
+ if (!output) return true
1427
+
1428
+ // Portable build adapters share this module and do not require Vite.
1429
+ const { transformWithOxc } = await import('vite')
1430
+ const changed = await Promise.all(
1431
+ previous.map(async (entry) => {
1432
+ const code = output.modules[sourceId(entry.file)]?.code
1433
+ if (
1434
+ entry.imports !==
1428
1435
  JSON.stringify(output.imports[sourceId(entry.file)])
1429
- )
1430
- }),
1436
+ )
1437
+ return true
1438
+ if (entry.code === code) return false
1439
+ if (entry.code === undefined || code === undefined) return true
1440
+
1441
+ // Generated type assertions can change without changing runtime bindings.
1442
+ const [before, after] = await Promise.all([
1443
+ transformWithOxc(entry.code, entry.file, { sourcemap: false }),
1444
+ transformWithOxc(code, entry.file, { sourcemap: false }),
1445
+ ])
1446
+ return before.code !== after.code
1447
+ }),
1448
+ )
1449
+ return changed.some(Boolean)
1450
+ })()
1451
+ // Compiler-only inputs have watch edges, but their stylesheets update separately.
1452
+ const affected = new Set(
1453
+ modules.filter(
1454
+ (module) =>
1455
+ javascriptChanged ||
1456
+ (module.id
1457
+ ? !output?.modules[sourceId(module.id)]
1458
+ : module.type !== 'asset'),
1459
+ ),
1431
1460
  )
1432
1461
  const ids = new Set<string>()
1433
1462
  if (type === 'delete') ids.add(cssId(file))
@@ -1436,12 +1465,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1436
1465
  if (!entry.files.has(file) && !eager(file, this.environment)) continue
1437
1466
 
1438
1467
  // Theme scopes affect CSS even when Vite's JavaScript import was erased.
1439
- if (
1440
- !output ||
1441
- entry.code !== output.modules[sourceId(entry.file)]?.code ||
1442
- entry.imports !== JSON.stringify(output.imports[sourceId(entry.file)])
1443
- )
1444
- ids.add(entry.file)
1468
+ // Rewritten importers can lose a source edge, so changed bindings must update together.
1469
+ if (javascriptChanged) ids.add(entry.file)
1445
1470
  ids.add(cssId(entry.file))
1446
1471
  ids.add(sharedId)
1447
1472
  }
package/src/web/Css.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import * as Targets from '../internal/Targets.js'
6
6
  import * as ClassName from './internal/ClassName.js'
7
+ import * as Identity from '../internal/Identity.js'
7
8
  import * as Contributions from './internal/Contributions.js'
8
9
  import * as Binding from '../internal/Binding.js'
9
10
  import * as Cascade from '../internal/Cascade.js'
@@ -33,6 +34,7 @@ export function compile<
33
34
  >(
34
35
  options: compile.Options<name, themeName>,
35
36
  ): compile.ReturnType<name, themeName> {
37
+ const labels = options[ClassName.labels] ?? options.styles[ClassName.labels]
36
38
  options = {
37
39
  ...options,
38
40
  styles: { styles: options.styles.styles.map(Targets.web) },
@@ -417,6 +419,8 @@ export function compile<
417
419
 
418
420
  const identities = new Map<string, string>()
419
421
  const rules = new Map<string, string>()
422
+ type ConditionalRule = { conditions: readonly string[]; value: string }
423
+ const conditionalRules = new Map<string, ConditionalRule>()
420
424
  const identical = new Map<string, string>()
421
425
  const explicitBodies = new Map<string, string>()
422
426
  const selectors = new Map<string, string>()
@@ -470,12 +474,18 @@ export function compile<
470
474
 
471
475
  const names: string[] = explicit === undefined ? [] : [explicit]
472
476
  let ordinal = 0
473
- const slots = new Map<string, number>()
474
- const developmentName = options.scope
475
- ? `definition-${styleIndex}`
476
- : style.name
477
-
478
- function emit(body: string, label: string, shared: boolean, output = mode) {
477
+ const cssName = labels?.[style.name]?.name ?? encode(style.name)
478
+ const cssNamespace =
479
+ labels?.[style.name]?.namespace ??
480
+ (options.scope ? Identity.name(options.scope) : undefined)
481
+
482
+ function emit(
483
+ body: string,
484
+ label: string,
485
+ shared: boolean,
486
+ output = mode,
487
+ conditional?: ConditionalRule,
488
+ ) {
479
489
  if (!body) return
480
490
 
481
491
  const independent =
@@ -483,12 +493,8 @@ export function compile<
483
493
  !options.development &&
484
494
  output === mode &&
485
495
  options.composition === 'independent'
486
- const key = `${output}:${body}`
487
- // Development slots belong to each style even when their initial values match.
488
- const reusable =
489
- explicit === undefined &&
490
- !options.development &&
491
- (shared || independent)
496
+ const key = JSON.stringify([cssNamespace, output, body])
497
+ const reusable = explicit === undefined && (shared || independent)
492
498
  const previous = reusable ? identical.get(key) : undefined
493
499
  if (previous && !names.includes(previous)) {
494
500
  names.push(previous)
@@ -498,43 +504,23 @@ export function compile<
498
504
  if (previous) shared = false
499
505
 
500
506
  // Contextual slots preserve authored ordering; development names survive value edits.
501
- const ordinalSlot = ordinal++
502
- const slot = options.development ? (slots.get(label) ?? 0) : ordinalSlot
503
- slots.set(label, slot + 1)
507
+ const slot = ordinal++
504
508
  const identity = (() => {
505
509
  if (explicit !== undefined)
506
510
  return `${explicit}-${mode}-${encode(label)}-${slot}`
507
- if (
508
- output === 'grouped' &&
509
- mode === 'grouped' &&
510
- !options.development &&
511
- options.composition === 'independent'
512
- )
513
- return `g_${rules.size.toString(36)}`
514
511
  if (output === 'grouped' && mode === 'grouped') {
515
- const name = options.development ? developmentName : style.name
516
- const scope =
517
- options.development && options.scope
518
- ? `_m${encode(options.scope)}`
519
- : ''
520
- return `g-${encode(name)}${scope}${slot ? `_s${slot}` : ''}`
512
+ return `z-${cssNamespace ? `${cssNamespace}-` : ''}${cssName}${slot ? `__${slot}` : ''}`
521
513
  }
522
514
  return ClassName.create({
523
515
  body,
524
- context:
525
- !shared || options.development
526
- ? JSON.stringify([
527
- options.scope,
528
- mode,
529
- options.development ? developmentName : style.name,
530
- ])
531
- : options.scope,
516
+ context: !shared ? cssName : undefined,
517
+ namespace: cssNamespace,
532
518
  property: label,
533
- slot: !shared || options.development ? slot : undefined,
534
- stable: options.development,
519
+ slot: !shared ? slot : undefined,
535
520
  })
536
521
  })()
537
- const owner = mode === 'atomic' ? `${style.name}:${slot}` : undefined
522
+ const owner =
523
+ mode === 'atomic' && !shared ? `${style.name}:${slot}` : undefined
538
524
  if (
539
525
  (rules.has(identity) && rules.get(identity) !== body) ||
540
526
  (owner !== undefined &&
@@ -543,12 +529,19 @@ export function compile<
543
529
  )
544
530
  diagnostics.push({
545
531
  code: 'identity_collision',
546
- message: 'Distinct rules produced the same class identifier.',
532
+ message:
533
+ 'Generated class names conflict. Supply distinct config or style ids.',
547
534
  path: [style.name],
548
535
  })
549
536
 
550
537
  if (owner !== undefined) identities.set(identity, owner)
551
538
  rules.set(identity, body)
539
+ if (
540
+ conditional?.conditions.some((condition) =>
541
+ /^@(media|supports)\b/.test(condition),
542
+ )
543
+ )
544
+ conditionalRules.set(identity, conditional)
552
545
  if (explicit !== undefined) selectors.set(identity, explicit)
553
546
  else {
554
547
  if (reusable) identical.set(key, identity)
@@ -611,7 +604,7 @@ export function compile<
611
604
  !nestedComposition &&
612
605
  !conditions.length &&
613
606
  groups.get(conflict(property)) !== false
614
- emit(body, property, shared)
607
+ emit(body, property, shared, mode, { conditions, value })
615
608
  }
616
609
  }
617
610
 
@@ -673,7 +666,7 @@ export function compile<
673
666
  options.schemes ?? false,
674
667
  options[Themes.shared],
675
668
  )
676
- : { classes: Object.freeze({}), css: '', resources: [] }
669
+ : { classes: Object.freeze({}), css: '', resources: [], rules: [] }
677
670
  } catch (error) {
678
671
  throw new CompileError([
679
672
  {
@@ -686,20 +679,57 @@ export function compile<
686
679
 
687
680
  const scopedCss = [
688
681
  scopes.css,
689
- ...[...rules].map(
690
- ([name, body]) => `.${selectors.get(name) ?? name}{${body}}`,
691
- ),
682
+ ...[...rules].map((entry) => {
683
+ const [name, body] = entry
684
+ const selector = `.${ClassName.selector(selectors.get(name) ?? name)}`
685
+ const conditional = conditionalRules.get(name)
686
+ if (
687
+ !conditional ||
688
+ conditional.conditions.some(
689
+ (condition) =>
690
+ condition.startsWith('@') &&
691
+ !/^@(media|supports)\b/.test(condition),
692
+ )
693
+ )
694
+ return `${selector}{${body}}`
695
+
696
+ // Inactive conditions must surround the class rather than leave a matched empty wrapper.
697
+ const conditions = conditional.conditions.filter((condition) =>
698
+ condition.startsWith('@'),
699
+ )
700
+ const nested = conditional.conditions
701
+ .filter((condition) => !condition.startsWith('@'))
702
+ .reduceRight(
703
+ (body, condition) => `${condition}{${body}}`,
704
+ conditional.value,
705
+ )
706
+ return conditions.reduceRight(
707
+ (body, condition) => `${condition}{${body}}`,
708
+ `${selector}{${nested}}`,
709
+ )
710
+ }),
692
711
  ]
693
712
  .filter(Boolean)
694
713
  .join('\n')
695
714
 
696
- return Object.freeze({
715
+ const result = {
697
716
  ...(scopes.resources.length ? { [Themes.shared]: scopes.resources } : {}),
698
717
  ...(contributionCss ? { contributionCss, scopedCss } : {}),
718
+ [ClassName.rules]: Object.freeze({
719
+ ...Object.fromEntries(rules),
720
+ ...Object.fromEntries(
721
+ scopes.rules.map((rule) => [
722
+ `@scope:${rule.selector.slice(1)}:${Identity.name(rule.property)}:${Identity.name(JSON.stringify(rule.conditions))}`,
723
+ rule.value,
724
+ ]),
725
+ ),
726
+ }),
699
727
  classes: Object.freeze(classes),
700
728
  css: [contributionCss, scopedCss].filter(Boolean).join('\n'),
701
729
  vars: scopes.classes as Readonly<Record<themeName, string>>,
702
- })
730
+ }
731
+ Object.defineProperty(result, ClassName.rules, { enumerable: false })
732
+ return Object.freeze(result)
703
733
  }
704
734
 
705
735
  /** Input and output contracts for literal compilation. */
@@ -714,6 +744,8 @@ export declare namespace compile {
714
744
  > = {
715
745
  /** Separates generated token rules for hosts with independent CSS resources. */
716
746
  readonly [Themes.shared]?: 'all' | 'defaults' | undefined
747
+ /** Readable source bindings and consumer-owned configuration namespaces. */
748
+ readonly [ClassName.labels]?: ClassName.Labels | undefined
717
749
  /** Fixed class identities used by CSS-only consumers. */
718
750
  readonly names?: Readonly<Record<string, string>> | undefined
719
751
  /**
@@ -726,14 +758,14 @@ export declare namespace compile {
726
758
  /** CSS representation; atomic declarations are the default. */
727
759
  readonly cssOutput?: 'atomic' | 'grouped' | undefined
728
760
  readonly composition?: 'independent' | 'ordered' | undefined
729
- /** Stable declaration names for CSS-only development updates. */
761
+ /** Retains live definitions for development updates. */
730
762
  readonly development?: boolean | undefined
731
763
  /**
732
764
  * Emit the `color-scheme` selection classes. Set by modules whose runtime
733
765
  * helpers can apply a scheme, so lowered `light-dark()` resolves there.
734
766
  */
735
767
  readonly schemes?: boolean | undefined
736
- /** Optional module scope for independently delivered stylesheets. */
768
+ /** Explicit consumer-owned namespace for independently delivered stylesheets. */
737
769
  readonly scope?: string | undefined
738
770
  /** Ordered definitions; no themes or source adapter is required. */
739
771
  readonly styles: Style.Definition<name>
@@ -748,6 +780,8 @@ export declare namespace compile {
748
780
  name extends string = string,
749
781
  themeName extends string = string,
750
782
  > = {
783
+ /** Exact emitted rule bodies retained for graph-wide collision diagnostics. */
784
+ readonly [ClassName.rules]: Readonly<Record<string, string>>
751
785
  /** Shared token resources omitted from this module's stylesheet. */
752
786
  readonly [Themes.shared]?: readonly Themes.Resource[] | undefined
753
787
  /** Contribution text separated for graph-wide hoisting. */