zyzz 0.0.19 → 0.0.20

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 (80) hide show
  1. package/README.md +9 -9
  2. package/dist/Config.d.ts +1 -0
  3. package/dist/Config.d.ts.map +1 -1
  4. package/dist/Config.js.map +1 -1
  5. package/dist/Vars.d.ts +22 -15
  6. package/dist/Vars.d.ts.map +1 -1
  7. package/dist/Vars.js +26 -0
  8. package/dist/Vars.js.map +1 -1
  9. package/dist/cli/index.js +5 -0
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/compiler/Source.d.ts.map +1 -1
  12. package/dist/compiler/Source.js +3 -0
  13. package/dist/compiler/Source.js.map +1 -1
  14. package/dist/compiler/internal/Configurations.d.ts.map +1 -1
  15. package/dist/compiler/internal/Configurations.js +3 -0
  16. package/dist/compiler/internal/Configurations.js.map +1 -1
  17. package/dist/compiler/internal/Contract.d.ts.map +1 -1
  18. package/dist/compiler/internal/Contract.js +30 -4
  19. package/dist/compiler/internal/Contract.js.map +1 -1
  20. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  21. package/dist/compiler/internal/Themes.js +83 -23
  22. package/dist/compiler/internal/Themes.js.map +1 -1
  23. package/dist/default.d.ts +7 -11
  24. package/dist/default.d.ts.map +1 -1
  25. package/dist/default.js +4 -5
  26. package/dist/default.js.map +2 -2
  27. package/dist/default.js.zyzz.json +1 -1
  28. package/dist/index.d.ts +15 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +15 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/internal/Token.d.ts +11 -2
  33. package/dist/internal/Token.d.ts.map +1 -1
  34. package/dist/internal/Token.js +2 -0
  35. package/dist/internal/Token.js.map +1 -1
  36. package/dist/internal/VariableSets.d.ts.map +1 -1
  37. package/dist/internal/VariableSets.js +18 -1
  38. package/dist/internal/VariableSets.js.map +1 -1
  39. package/dist/node/Host.d.ts +2 -0
  40. package/dist/node/Host.d.ts.map +1 -1
  41. package/dist/node/Host.js +7 -0
  42. package/dist/node/Host.js.map +1 -1
  43. package/dist/react-native/StyleSheet.js +2 -0
  44. package/dist/react-native/StyleSheet.js.map +1 -1
  45. package/dist/runtime/Composition.d.ts +1 -1
  46. package/dist/runtime/ConditionalRecipe.d.ts +1 -1
  47. package/dist/runtime/PayloadRecipe.d.ts +1 -1
  48. package/dist/runtime/Recipe.d.ts +1 -1
  49. package/dist/styleFunction.d.ts +1 -1
  50. package/dist/styleFunction.d.ts.map +1 -1
  51. package/dist/styleFunction.js.map +1 -1
  52. package/dist/variants.d.ts +14 -6
  53. package/dist/variants.d.ts.map +1 -1
  54. package/dist/variants.js.map +1 -1
  55. package/dist/vite/index.d.ts +2 -0
  56. package/dist/vite/index.d.ts.map +1 -1
  57. package/dist/vite/index.js +75 -30
  58. package/dist/vite/index.js.map +1 -1
  59. package/dist/web/internal/Themes.d.ts.map +1 -1
  60. package/dist/web/internal/Themes.js +4 -0
  61. package/dist/web/internal/Themes.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/Config.ts +5 -0
  64. package/src/Vars.ts +88 -29
  65. package/src/cli/index.ts +5 -0
  66. package/src/compiler/Source.ts +3 -0
  67. package/src/compiler/internal/Configurations.ts +3 -0
  68. package/src/compiler/internal/Contract.ts +47 -4
  69. package/src/compiler/internal/Themes.ts +109 -34
  70. package/src/default.ts +8 -10
  71. package/src/index.ts +15 -0
  72. package/src/internal/Authoring.ts +1 -1
  73. package/src/internal/Token.ts +39 -11
  74. package/src/internal/VariableSets.ts +25 -1
  75. package/src/node/Host.ts +19 -0
  76. package/src/react-native/StyleSheet.ts +6 -0
  77. package/src/styleFunction.ts +1 -3
  78. package/src/variants.ts +38 -16
  79. package/src/vite/index.ts +94 -33
  80. package/src/web/internal/Themes.ts +4 -0
@@ -26,7 +26,7 @@ export function read(
26
26
  if (
27
27
  ![
28
28
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
29
- 22, 23, 24, 25, 26, 27, 28,
29
+ 22, 23, 24, 25, 26, 27, 28, 29,
30
30
  ].includes(data.version as number)
31
31
  )
32
32
  throw new Error('Unsupported Zyzz contract version.')
@@ -69,6 +69,26 @@ export function read(
69
69
  decode(value),
70
70
  ]),
71
71
  )
72
+ if (Object.hasOwn(fields, '$composition')) {
73
+ if ((data.version as number) < 29)
74
+ throw new Error(
75
+ 'Composed variables require contract version 29 or later.',
76
+ )
77
+ const composition = record(fields.$composition)
78
+ if (
79
+ (composition.group !== 'color' && composition.group !== 'spacing') ||
80
+ !Array.isArray(composition.parts)
81
+ )
82
+ throw new Error('Invalid packed variable composition.')
83
+ return Vars.compose(
84
+ composition.group,
85
+ composition.parts.map(decode) as readonly (
86
+ | string
87
+ | number
88
+ | Vars.Reference
89
+ )[],
90
+ )
91
+ }
72
92
  if (Object.hasOwn(fields, '$variable')) {
73
93
  const reference = record(fields.$variable)
74
94
  const identity = string(reference.identity)
@@ -294,7 +314,7 @@ export function read(
294
314
  if (
295
315
  ![
296
316
  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
297
- 27, 28,
317
+ 27, 28, 29,
298
318
  ].includes(data.version as number) ||
299
319
  ![
300
320
  'cssFunction',
@@ -590,7 +610,8 @@ function tokens(tree: Theme.References<Theme.Tokens>): Record<string, unknown> {
590
610
  }
591
611
 
592
612
  function type(value: unknown): string {
593
- if (Token.is(value)) return Configurations.type(value)
613
+ if (Token.is(value) || Token.isExpression(value))
614
+ return Configurations.type(value)
594
615
  if (Array.isArray(value)) return `readonly [${value.map(type).join(',')}]`
595
616
  if (!value || typeof value !== 'object') return JSON.stringify(value)
596
617
 
@@ -727,6 +748,22 @@ export function write(
727
748
  ]),
728
749
  ),
729
750
  version: (() => {
751
+ function composed(value: unknown): boolean {
752
+ if (Token.isExpression(value)) return true
753
+ if (Token.is(value)) return composed(value.value)
754
+ return (
755
+ !!value &&
756
+ typeof value === 'object' &&
757
+ Object.values(value).some(composed)
758
+ )
759
+ }
760
+ if (
761
+ Object.values(themes).some((theme) =>
762
+ composed(theme[Token.definition].values),
763
+ )
764
+ )
765
+ return 29
766
+
730
767
  if (
731
768
  Object.values(themes).some(
732
769
  (theme) =>
@@ -1031,6 +1068,10 @@ function extended(signature: NonNullable<Themes.Call['function']>): boolean {
1031
1068
  }
1032
1069
 
1033
1070
  function encode(value: unknown): unknown {
1071
+ if (Token.isExpression(value) && 'group' in value)
1072
+ return {
1073
+ $composition: { group: value.group, parts: value.parts.map(encode) },
1074
+ }
1034
1075
  if (Token.is(value))
1035
1076
  return {
1036
1077
  $variable: {
@@ -1044,7 +1085,9 @@ function encode(value: unknown): unknown {
1044
1085
  const fields = Object.fromEntries(
1045
1086
  Object.entries(value).map(([key, value]) => [key, encode(value)]),
1046
1087
  )
1047
- return Object.hasOwn(fields, '$variable') || Object.hasOwn(fields, '$object')
1088
+ return Object.hasOwn(fields, '$composition') ||
1089
+ Object.hasOwn(fields, '$variable') ||
1090
+ Object.hasOwn(fields, '$object')
1048
1091
  ? { $object: fields }
1049
1092
  : fields
1050
1093
  }
@@ -144,6 +144,7 @@ export function collect(program: Ast.Program, options: collect.Options) {
144
144
  const imports = new Set<number>()
145
145
  const variableImports = new Set<number>()
146
146
  const configImports = new Set<number>()
147
+ const factoryImports = new Map<number, 'create' | 'define' | 'extend'>()
147
148
  const configs = new Map<string, Link>()
148
149
  const configBindings = new Map<number, Link>()
149
150
  const factoryReferences = new Set<number>()
@@ -188,11 +189,31 @@ export function collect(program: Ast.Program, options: collect.Options) {
188
189
  : specifier.imported.value) === 'Config'
189
190
  )
190
191
  configImports.add(specifier.start)
192
+ else if (
193
+ specifier.type === 'ImportSpecifier' &&
194
+ specifier.importKind !== 'type' &&
195
+ ['defineConfig', 'defineVars', 'extendVars'].includes(
196
+ specifier.imported.type === 'Identifier'
197
+ ? specifier.imported.name
198
+ : specifier.imported.value,
199
+ )
200
+ ) {
201
+ const name =
202
+ specifier.imported.type === 'Identifier'
203
+ ? specifier.imported.name
204
+ : specifier.imported.value
205
+ if (name === 'defineConfig')
206
+ factoryImports.set(specifier.start, 'create')
207
+ else if (name === 'defineVars')
208
+ factoryImports.set(specifier.start, 'define')
209
+ else factoryImports.set(specifier.start, 'extend')
210
+ }
191
211
  }
192
212
 
193
213
  if (
194
214
  !imports.size &&
195
215
  !configImports.size &&
216
+ !factoryImports.size &&
196
217
  !Object.keys(options.links ?? {}).length
197
218
  )
198
219
  return undefined
@@ -251,6 +272,7 @@ export function collect(program: Ast.Program, options: collect.Options) {
251
272
 
252
273
  const namespaces = new Set<string>()
253
274
  const configNamespaces = new Set<string>()
275
+ const factoryFunctions = new Map<string, 'create' | 'define' | 'extend'>()
254
276
  const variableNamespaces = new Set<string>()
255
277
  for (const node of program.body)
256
278
  if (node.type === 'ImportDeclaration')
@@ -264,6 +286,11 @@ export function collect(program: Ast.Program, options: collect.Options) {
264
286
  if (imports.has(specifier.start)) namespaces.add(specifier.local.name)
265
287
  else if (configImports.has(specifier.start))
266
288
  configNamespaces.add(specifier.local.name)
289
+ else if (factoryImports.has(specifier.start))
290
+ factoryFunctions.set(
291
+ specifier.local.name,
292
+ factoryImports.get(specifier.start)!,
293
+ )
267
294
 
268
295
  function resolve(node: Ast.Node): Link | undefined {
269
296
  if (node.type === 'Identifier') {
@@ -426,6 +453,35 @@ export function collect(program: Ast.Program, options: collect.Options) {
426
453
  if (node.type === 'TSAsExpression' || node.type === 'TSSatisfiesExpression')
427
454
  return data(node.expression, derived)
428
455
 
456
+ if (node.type === 'CallExpression' && !node.optional) {
457
+ const callee = node.callee
458
+ if (
459
+ callee.type === 'MemberExpression' &&
460
+ !callee.computed &&
461
+ !callee.optional &&
462
+ callee.object.type === 'Identifier' &&
463
+ variableNamespaces.has(callee.object.name) &&
464
+ derived?.name !== callee.object.name &&
465
+ callee.property.type === 'Identifier' &&
466
+ callee.property.name === 'compose'
467
+ ) {
468
+ if (node.arguments.length !== 2)
469
+ return fail(
470
+ 'Vars.compose requires a literal domain and parts array.',
471
+ node,
472
+ )
473
+ const group = data(node.arguments[0]!, derived)
474
+ const parts = data(node.arguments[1]!, derived)
475
+ if ((group !== 'color' && group !== 'spacing') || !Array.isArray(parts))
476
+ return fail(
477
+ 'Vars.compose requires a literal domain and parts array.',
478
+ node,
479
+ )
480
+ factoryReferences.add(callee.start)
481
+ return Vars.compose(group, parts)
482
+ }
483
+ }
484
+
429
485
  if (
430
486
  node.type === 'Literal' &&
431
487
  (typeof node.value === 'string' || typeof node.value === 'number')
@@ -536,37 +592,52 @@ export function collect(program: Ast.Program, options: collect.Options) {
536
592
  registerAlias({ declaration, statement, variable })
537
593
 
538
594
  const expression = variable.init
539
- if (
540
- expression?.type !== 'CallExpression' ||
541
- expression.callee.type !== 'MemberExpression' ||
542
- expression.callee.object.type !== 'Identifier' ||
543
- (!namespaces.has(expression.callee.object.name) &&
544
- !configNamespaces.has(expression.callee.object.name))
545
- )
546
- continue
595
+ if (expression?.type !== 'CallExpression' || expression.optional) continue
547
596
 
548
597
  const member = expression.callee
598
+ const directFactory =
599
+ member.type === 'Identifier'
600
+ ? factoryFunctions.get(member.name)
601
+ : undefined
549
602
  if (
550
- member.computed ||
551
- member.optional ||
552
- expression.optional ||
553
- member.property.type !== 'Identifier' ||
554
- !(
555
- configNamespaces.has(expression.callee.object.name)
556
- ? ['create']
557
- : ['define', 'extend']
558
- ).includes(member.property.name)
603
+ !directFactory &&
604
+ (member.type !== 'MemberExpression' ||
605
+ member.object.type !== 'Identifier' ||
606
+ (!namespaces.has(member.object.name) &&
607
+ !configNamespaces.has(member.object.name)) ||
608
+ member.computed ||
609
+ member.optional ||
610
+ member.property.type !== 'Identifier' ||
611
+ !(
612
+ configNamespaces.has(member.object.name)
613
+ ? ['create']
614
+ : ['define', 'extend']
615
+ ).includes(member.property.name))
559
616
  )
560
617
  continue
561
618
 
619
+ const config =
620
+ directFactory === 'create' ||
621
+ (member.type === 'MemberExpression' &&
622
+ member.object.type === 'Identifier' &&
623
+ configNamespaces.has(member.object.name))
624
+ const method =
625
+ member.type === 'MemberExpression' &&
626
+ member.property.type === 'Identifier'
627
+ ? member.property.name
628
+ : directFactory
629
+ const variableSet =
630
+ directFactory === 'define' ||
631
+ directFactory === 'extend' ||
632
+ (member.type === 'MemberExpression' &&
633
+ member.object.type === 'Identifier' &&
634
+ variableNamespaces.has(member.object.name))
635
+
562
636
  if (
563
637
  (statement.type === 'ExportNamedDeclaration' && !options.linked) ||
564
638
  declaration.kind !== 'const' ||
565
639
  (variable.id.type !== 'Identifier' &&
566
- !(
567
- configNamespaces.has(expression.callee.object.name) &&
568
- variable.id.type === 'ObjectPattern'
569
- ))
640
+ !(config && variable.id.type === 'ObjectPattern'))
570
641
  )
571
642
  fail(
572
643
  'Define local themes with a module-level const; exported themes require source linking.',
@@ -601,10 +672,7 @@ export function collect(program: Ast.Program, options: collect.Options) {
601
672
  fail('Configuration destructuring requires a binding.', variable)
602
673
 
603
674
  let name = `${options.namespace}-${binding}`
604
- if (
605
- !configNamespaces.has(expression.callee.object.name) &&
606
- member.property.name === 'define'
607
- ) {
675
+ if (!config && method === 'define') {
608
676
  const argument = expression.arguments[1]
609
677
  const callback = argument && Expression.unwrap(argument)
610
678
  const id = Identifiers.explicit(
@@ -617,7 +685,7 @@ export function collect(program: Ast.Program, options: collect.Options) {
617
685
  if (id !== undefined) name = Identity.requireId(id, 'Vars.define')
618
686
  }
619
687
 
620
- if (configNamespaces.has(expression.callee.object.name)) {
688
+ if (config) {
621
689
  try {
622
690
  const link = Configurations.collect({
623
691
  data,
@@ -772,7 +840,7 @@ export function collect(program: Ast.Program, options: collect.Options) {
772
840
  let output: Call['output']
773
841
 
774
842
  try {
775
- if (member.property.name === 'define') {
843
+ if (method === 'define') {
776
844
  if (
777
845
  expression.arguments.length < 1 ||
778
846
  expression.arguments.length > 3
@@ -786,9 +854,6 @@ export function collect(program: Ast.Program, options: collect.Options) {
786
854
 
787
855
  tokenType = type(expression.arguments[0]!)
788
856
 
789
- const variableSet = variableNamespaces.has(
790
- expression.callee.object.name,
791
- )
792
857
  const argument = expression.arguments[1]
793
858
  const callback = argument && Expression.unwrap(argument)
794
859
  if (
@@ -860,7 +925,7 @@ export function collect(program: Ast.Program, options: collect.Options) {
860
925
  )
861
926
 
862
927
  factoryReferences.add(base!.start)
863
- definition = variableNamespaces.has(expression.callee.object.name)
928
+ definition = variableSet
864
929
  ? VariableSets.theme(
865
930
  Vars.extend(
866
931
  VariableSets.from(themes[parent.name]![Token.definition]),
@@ -892,7 +957,7 @@ export function collect(program: Ast.Program, options: collect.Options) {
892
957
  }
893
958
 
894
959
  const call = Object.freeze({
895
- ...(variableNamespaces.has(expression.callee.object.name)
960
+ ...(variableSet
896
961
  ? {
897
962
  variableSet: true,
898
963
  directVariables: true,
@@ -1286,12 +1351,22 @@ export function collect(program: Ast.Program, options: collect.Options) {
1286
1351
 
1287
1352
  if (
1288
1353
  binding?.type === 'Import' &&
1289
- (imports.has(binding.node.start) || configImports.has(binding.node.start))
1354
+ (imports.has(binding.node.start) ||
1355
+ configImports.has(binding.node.start) ||
1356
+ factoryImports.has(binding.node.start))
1290
1357
  ) {
1358
+ if (
1359
+ factoryImports.has(binding.node.start) &&
1360
+ parent.type === 'CallExpression' &&
1361
+ factories.has(parent.start)
1362
+ )
1363
+ return true
1364
+
1291
1365
  if (
1292
1366
  parent.type === 'MemberExpression' &&
1293
1367
  grandparent?.type === 'CallExpression' &&
1294
- factories.has(grandparent.start)
1368
+ (factories.has(grandparent.start) ||
1369
+ factoryReferences.has(parent.start))
1295
1370
  )
1296
1371
  return true
1297
1372
 
package/src/default.ts CHANGED
@@ -80,8 +80,8 @@ export const tokens = {
80
80
  1000: { dark: '#fef3dc', light: '#4e2009' },
81
81
  },
82
82
  background: {
83
- 100: { dark: '#0a0a0a', light: '#fff' },
84
- 200: { dark: '#000', light: '#fafafa' },
83
+ primary: { dark: '#000', light: '#fafafa' },
84
+ surface: { dark: '#0a0a0a', light: '#fff' },
85
85
  },
86
86
  black: '#000',
87
87
  blue: {
@@ -169,7 +169,6 @@ export const tokens = {
169
169
  900: { dark: '#ff6166', light: '#cb2a2f' },
170
170
  1000: { dark: '#feecee', light: '#391417' },
171
171
  },
172
- surface: { dark: '#0a0a0a', light: '#fff' },
173
172
  teal: {
174
173
  100: { dark: '#04201b', light: '#eefcf9' },
175
174
  200: { dark: '#062822', light: '#e5faf6' },
@@ -428,14 +427,14 @@ export const tokens = {
428
427
  fontSize: '20px',
429
428
  fontWeight: 400,
430
429
  letterSpacing: '0px',
431
- lineHeight: '36px',
430
+ lineHeight: '28px',
432
431
  strong: {
433
432
  fontFamily:
434
433
  'Geist, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
435
434
  fontSize: '20px',
436
435
  fontWeight: 550,
437
436
  letterSpacing: '0px',
438
- lineHeight: '36px',
437
+ lineHeight: '28px',
439
438
  },
440
439
  },
441
440
  '24': {
@@ -722,8 +721,8 @@ const config = Config.create({
722
721
  1000: { dark: '#fef3dc', light: '#4e2009' },
723
722
  },
724
723
  background: {
725
- 100: { dark: '#0a0a0a', light: '#fff' },
726
- 200: { dark: '#000', light: '#fafafa' },
724
+ primary: { dark: '#000', light: '#fafafa' },
725
+ surface: { dark: '#0a0a0a', light: '#fff' },
727
726
  },
728
727
  black: '#000',
729
728
  blue: {
@@ -811,7 +810,6 @@ const config = Config.create({
811
810
  900: { dark: '#ff6166', light: '#cb2a2f' },
812
811
  1000: { dark: '#feecee', light: '#391417' },
813
812
  },
814
- surface: { dark: '#0a0a0a', light: '#fff' },
815
813
  teal: {
816
814
  100: { dark: '#04201b', light: '#eefcf9' },
817
815
  200: { dark: '#062822', light: '#e5faf6' },
@@ -1070,14 +1068,14 @@ const config = Config.create({
1070
1068
  fontSize: '20px',
1071
1069
  fontWeight: 400,
1072
1070
  letterSpacing: '0px',
1073
- lineHeight: '36px',
1071
+ lineHeight: '28px',
1074
1072
  strong: {
1075
1073
  fontFamily:
1076
1074
  'Geist, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
1077
1075
  fontSize: '20px',
1078
1076
  fontWeight: 550,
1079
1077
  letterSpacing: '0px',
1080
- lineHeight: '36px',
1078
+ lineHeight: '28px',
1081
1079
  },
1082
1080
  },
1083
1081
  '24': {
package/src/index.ts CHANGED
@@ -8,6 +8,21 @@ export * as Config from './Config.js'
8
8
  export { style } from './styleFunction.js'
9
9
  /** Ordered composition of applied styling props. */
10
10
  export { cx } from './cx.js'
11
+ /**
12
+ * Creates isolated authoring with optional vars and named alternatives.
13
+ * @alias Config.create
14
+ */
15
+ export { create as defineConfig } from './Config.js'
16
+ /**
17
+ * Creates immutable typed variable sets without emitting CSS.
18
+ * @alias Vars.define
19
+ */
20
+ export { define as defineVars } from './Vars.js'
21
+ /**
22
+ * Overrides variable values while preserving their shared contract.
23
+ * @alias Vars.extend
24
+ */
25
+ export { extend as extendVars } from './Vars.js'
11
26
  /** Inferred component inputs from callable style definitions. */
12
27
  export type * as Props from './Props.js'
13
28
  /** Typed literal style definitions and validation diagnostics. */
@@ -157,7 +157,7 @@ export function variants(
157
157
  return (values: Record<string, unknown> & style.Options = {}) => {
158
158
  const result = { ...props(values as style.Options) } as {
159
159
  className: string
160
- style?: style.Props['style']
160
+ style?: NonNullable<style.Props['style']>
161
161
  [key: `data-${string}`]: string | undefined
162
162
  }
163
163
  const bindings: Record<string, string | number> = {}
@@ -262,6 +262,11 @@ export function acceptsReference(
262
262
  if (property.startsWith('--')) return true
263
263
  function check(value: Value): boolean {
264
264
  if (is(value)) return acceptsReference(value, property)
265
+ if (isExpression(value))
266
+ return Binding.accepts(
267
+ value.group === 'color' ? 'color' : 'length',
268
+ property,
269
+ )
265
270
  if (typeof value === 'object') return Object.values(value).every(check)
266
271
  if (typeof value === 'number') {
267
272
  const rule = Literal.rule(property)
@@ -403,6 +408,18 @@ export type Expression = {
403
408
  readonly parts: readonly (string | Reference | Binding.Reference)[]
404
409
  }
405
410
 
411
+ /** A composed variable value with an explicit scalar domain. */
412
+ export type Composition<
413
+ group extends 'color' | 'spacing' = 'color' | 'spacing',
414
+ > = {
415
+ /** Structured expression discriminator. */
416
+ readonly [expression]: true
417
+ /** Domain retained by variable references and compatible overrides. */
418
+ readonly group: group
419
+ /** CSS text and references to portable variable values. */
420
+ readonly parts: readonly (string | Reference)[]
421
+ }
422
+
406
423
  /** Identifies structured web expressions independently of literal CSS text. */
407
424
  export const expression = Symbol('zyzz.expression')
408
425
 
@@ -558,17 +575,27 @@ type Paths<tree, property extends keyof Literal.Properties = never> = [
558
575
  > extends Value
559
576
  ? [property] extends [never]
560
577
  ? PathKey<key>
561
- : VariableSets.Scalar<
562
- NonNullable<tree[key]>
563
- > extends Literal.Properties[property] &
564
- ValueSyntax.Checked<
565
- Record<
566
- property,
567
- VariableSets.Scalar<NonNullable<tree[key]>>
568
- >
569
- >[property]
570
- ? PathKey<key>
571
- : never
578
+ : Literal.Color extends VariableSets.Scalar<NonNullable<tree[key]>>
579
+ ? property extends Properties<'color'>
580
+ ? PathKey<key>
581
+ : never
582
+ : Literal.Length extends VariableSets.Scalar<
583
+ NonNullable<tree[key]>
584
+ >
585
+ ? Binding.Property<'length', property> extends never
586
+ ? never
587
+ : PathKey<key>
588
+ : VariableSets.Scalar<
589
+ NonNullable<tree[key]>
590
+ > extends Literal.Properties[property] &
591
+ ValueSyntax.Checked<
592
+ Record<
593
+ property,
594
+ VariableSets.Scalar<NonNullable<tree[key]>>
595
+ >
596
+ >[property]
597
+ ? PathKey<key>
598
+ : never
572
599
  : `${key}.${Paths<NonNullable<tree[key]>, property>}`
573
600
  }[Extract<keyof tree, number | string>]
574
601
 
@@ -794,6 +821,7 @@ export type Value =
794
821
  | number
795
822
  | string
796
823
  | Reference
824
+ | Composition
797
825
  | { readonly dark: Value; readonly light: Value }
798
826
  | Conditions
799
827
 
@@ -75,6 +75,7 @@ export function build(
75
75
  typeof input === 'string' ||
76
76
  typeof input === 'number' ||
77
77
  Token.is(input) ||
78
+ Token.isExpression(input) ||
78
79
  (path.length > 0 &&
79
80
  input &&
80
81
  typeof input === 'object' &&
@@ -244,6 +245,17 @@ function read(
244
245
  active = new Set<object>(),
245
246
  ): Token.Value {
246
247
  if (Token.is(input)) return input
248
+ if (Token.isExpression(input)) {
249
+ if (
250
+ !('group' in input) ||
251
+ (input.group !== 'color' && input.group !== 'spacing')
252
+ )
253
+ throw new Vars.InvalidError(path, 'Expected a composed variable value.')
254
+ return Vars.compose(
255
+ input.group,
256
+ input.parts as readonly (string | Token.Reference)[],
257
+ )
258
+ }
247
259
  if (typeof input === 'number' && Number.isFinite(input)) return input
248
260
  if (typeof input === 'string' && input.trim() && !/[;{}]/.test(input))
249
261
  return input
@@ -307,7 +319,7 @@ function read(
307
319
 
308
320
  /** Infers the shared declaration domain after following defaults and references. */
309
321
  export function domain(value: Token.Value): Token.Group {
310
- if (Token.is(value)) return value.group
322
+ if (Token.is(value) || Token.isExpression(value)) return value.group
311
323
  if (typeof value === 'object')
312
324
  return 'default' in value ? domain(value.default) : 'color'
313
325
  if (
@@ -383,6 +395,7 @@ export function merge(
383
395
  typeof entry !== 'object' ||
384
396
  Array.isArray(entry) ||
385
397
  Token.is(entry) ||
398
+ Token.isExpression(entry) ||
386
399
  'default' in entry ||
387
400
  ('light' in entry && 'dark' in entry) ||
388
401
  (Object.keys(entry).length <= 2 &&
@@ -424,6 +437,17 @@ export function rebind(
424
437
  value: visit(value.path),
425
438
  })
426
439
  }
440
+ if (Token.isExpression(value))
441
+ return Vars.compose(
442
+ value.group,
443
+ value.parts.map((part) => {
444
+ if (typeof part === 'string') return part
445
+ const reference = resolve(part)
446
+ if (!Token.is(reference))
447
+ throw new Vars.InvalidError([], 'Expected a variable reference.')
448
+ return reference
449
+ }),
450
+ )
427
451
  if (typeof value !== 'object') return value
428
452
 
429
453
  return Object.freeze(
package/src/node/Host.ts CHANGED
@@ -49,6 +49,14 @@ export async function create(options: create.Options): Promise<Runtime> {
49
49
  targets: { ...options.css?.targets },
50
50
  }
51
51
 
52
+ const externals = [...(options.external ?? [])]
53
+ if (
54
+ externals.some((pattern) => !pattern || pattern.slice(0, -1).includes('*'))
55
+ )
56
+ throw new Error(
57
+ 'External imports require exact names or a trailing * prefix.',
58
+ )
59
+
52
60
  const native = options.native
53
61
  ? Object.freeze({
54
62
  ...options.native,
@@ -404,6 +412,15 @@ export async function create(options: create.Options): Promise<Runtime> {
404
412
  (!specifier.startsWith('#') && specifier.includes('#'))
405
413
  )
406
414
  continue
415
+ if (
416
+ externals.some((pattern) =>
417
+ pattern.endsWith('*')
418
+ ? specifier.startsWith(pattern.slice(0, -1))
419
+ : specifier === pattern,
420
+ )
421
+ )
422
+ continue
423
+
407
424
  try {
408
425
  if (specifier.startsWith('.')) {
409
426
  const target = Relative.resolve({ moduleId, modules, specifier })
@@ -951,6 +968,8 @@ export declare namespace create {
951
968
  readonly targets?: Readonly<LightningCss.Targets> | undefined
952
969
  }
953
970
  | undefined
971
+ /** Imports left for downstream tooling. Matches exact names or prefixes ending in *. */
972
+ readonly external?: readonly string[] | undefined
954
973
  /** Native context captured at creation. Emits modules and maps without CSS or web initialization. */
955
974
  readonly native?: Graph.compile.Options['native']
956
975
  /** Output directory exclusively locked until close; may be nested under root. Defaults to `dist`. */
@@ -785,6 +785,12 @@ function resolve(
785
785
  resolved = resolved.value
786
786
  continue
787
787
  }
788
+ if (Token.isExpression(resolved))
789
+ fail(
790
+ 'unsupported_feature',
791
+ 'Composed variables require a web target.',
792
+ path,
793
+ )
788
794
  if ('default' in resolved)
789
795
  fail(
790
796
  'unsupported_feature',
@@ -114,9 +114,7 @@ export declare namespace style {
114
114
  /** Compiled and supplied class names. */
115
115
  readonly className: string
116
116
  /** Supplied inline styling overrides when present. */
117
- readonly style?:
118
- | Readonly<Record<string, string | number | undefined>>
119
- | undefined
117
+ readonly style?: Readonly<Record<string, string | number | undefined>>
120
118
  }
121
119
 
122
120
  /** Callable definition; source rewriting supplies its implementation. */