zyzz 0.0.23 → 0.0.24

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 (46) hide show
  1. package/README.md +6 -6
  2. package/dist/internal/Configuration.d.ts.map +1 -1
  3. package/dist/internal/Configuration.js +2 -1
  4. package/dist/internal/Configuration.js.map +1 -1
  5. package/dist/node/Host.d.ts.map +1 -1
  6. package/dist/node/Host.js +40 -12
  7. package/dist/node/Host.js.map +1 -1
  8. package/dist/react-native/internal/Device.js +90 -37
  9. package/dist/react-native/internal/Device.js.map +3 -3
  10. package/dist/react-native/internal/NativeZyzz.js.map +2 -2
  11. package/dist/react-native/internal/Store.d.ts +4 -0
  12. package/dist/react-native/internal/Store.d.ts.map +1 -1
  13. package/dist/react-native/internal/Store.js.map +1 -1
  14. package/dist/react-native/internal/Viewport.d.ts +6 -0
  15. package/dist/react-native/internal/Viewport.d.ts.map +1 -1
  16. package/dist/react-native/internal/Viewport.js +2 -0
  17. package/dist/react-native/internal/Viewport.js.map +1 -1
  18. package/dist/react-native/react.d.ts +5 -2
  19. package/dist/react-native/react.d.ts.map +1 -1
  20. package/dist/react-native/react.js +60 -8
  21. package/dist/react-native/react.js.map +1 -1
  22. package/dist/runtime/NativeContext.d.ts +6 -0
  23. package/dist/runtime/NativeContext.d.ts.map +1 -1
  24. package/dist/runtime/NativeContext.js +62 -9
  25. package/dist/runtime/NativeContext.js.map +1 -1
  26. package/dist/unplugin/index.d.ts.map +1 -1
  27. package/dist/unplugin/index.js +4 -1
  28. package/dist/unplugin/index.js.map +1 -1
  29. package/dist/variants.d.ts +3 -2
  30. package/dist/variants.d.ts.map +1 -1
  31. package/dist/variants.js.map +1 -1
  32. package/native/NativeZyzz.cpp +52 -65
  33. package/native/NativeZyzz.h +0 -4
  34. package/native/ZyzzSpecJSI.h +0 -10
  35. package/native/android/ZyzzSpec-generated.cpp +0 -6
  36. package/package.json +1 -1
  37. package/src/internal/Configuration.ts +2 -1
  38. package/src/node/Host.ts +64 -17
  39. package/src/react-native/internal/Device.ts +129 -48
  40. package/src/react-native/internal/NativeZyzz.ts +4 -6
  41. package/src/react-native/internal/Store.ts +4 -0
  42. package/src/react-native/internal/Viewport.ts +7 -0
  43. package/src/react-native/react.ts +81 -11
  44. package/src/runtime/NativeContext.ts +83 -9
  45. package/src/unplugin/index.ts +4 -1
  46. package/src/variants.ts +3 -2
@@ -7,6 +7,14 @@ const dependency = Symbol('zyzz.native.dependency')
7
7
 
8
8
  /** Render-local set and resolved appearance. */
9
9
  export type Context = {
10
+ /**
11
+ * Builds a color that follows the platform appearance. When present, styles whose
12
+ * light and dark selections differ only in color use it and need no scheme update.
13
+ * Must be stable, since adapted styles are cached per selection pair.
14
+ */
15
+ readonly adaptive?:
16
+ | ((light: string | number, dark: string | number) => unknown)
17
+ | undefined
10
18
  /** Resolved system appearance or an explicit override. */
11
19
  readonly colorScheme: 'dark' | 'light'
12
20
  /** Named compiled set; omission uses each configuration default. */
@@ -37,7 +45,15 @@ export function create<const tables extends Tables>(
37
45
  const set = context.set ?? defaultVars
38
46
  const table = tables[set] ?? fallback
39
47
  if (!table) throw new Error(`Unknown native set: ${set}.`)
40
- return table[context.colorScheme](input!).style
48
+ const style = table[context.colorScheme](input!).style
49
+ if (!context.adaptive) return style
50
+ return (
51
+ adapt(
52
+ table.light(input!).style,
53
+ table.dark(input!).style,
54
+ context.adaptive,
55
+ ) ?? style
56
+ )
41
57
  }
42
58
  return bind(select, (context) => {
43
59
  const table = tables[context.set ?? defaultVars] ?? fallback
@@ -94,10 +110,10 @@ export function responsive<
94
110
  export function application(value: unknown, input?: unknown): unknown {
95
111
  return {
96
112
  [binding]: (context: Context) => resolve(value, context, input),
97
- [dependency]: (context: Context) => [
98
- ...key(value, context),
99
- ...key(input, context),
100
- ],
113
+ [dependency]: (context: Context) =>
114
+ input === undefined
115
+ ? key(value, context)
116
+ : [...key(value, context), ...key(input, context)],
101
117
  }
102
118
  }
103
119
 
@@ -117,6 +133,64 @@ export function key(value: unknown, context: Context): readonly unknown[] {
117
133
  return []
118
134
  }
119
135
 
136
+ const adapted = new WeakMap<object, WeakMap<object, object | null>>()
137
+
138
+ /** Merges selections that differ only in color, or returns undefined. Results keep their identity across schemes. */
139
+ function adapt(
140
+ light: unknown,
141
+ dark: unknown,
142
+ color: NonNullable<Context['adaptive']>,
143
+ ): unknown {
144
+ if (light === dark) return light
145
+ if (!record(light) || !record(dark)) return undefined
146
+ let pairs = adapted.get(light)
147
+ if (!pairs) adapted.set(light, (pairs = new WeakMap()))
148
+ if (pairs.has(dark)) return pairs.get(dark) ?? undefined
149
+ const result: Record<string, unknown> | null = (() => {
150
+ const keys = Object.keys(light)
151
+ if (keys.length !== Object.keys(dark).length) return null
152
+ const merged: Record<string, unknown> = {}
153
+ for (const key of keys) {
154
+ if (!Object.hasOwn(dark, key)) return null
155
+ const [first, second] = [light[key], dark[key]]
156
+ if (equal(first, second)) merged[key] = first
157
+ else if (
158
+ (key === 'color' || key.endsWith('Color')) &&
159
+ (typeof first === 'string' || typeof first === 'number') &&
160
+ (typeof second === 'string' || typeof second === 'number')
161
+ )
162
+ merged[key] = color(first, second)
163
+ else return null
164
+ }
165
+ return merged
166
+ })()
167
+ const value = result && Object.freeze(result)
168
+ pairs.set(dark, value)
169
+ return value ?? undefined
170
+ }
171
+
172
+ function equal(left: unknown, right: unknown): boolean {
173
+ if (Object.is(left, right)) return true
174
+ if (Array.isArray(left))
175
+ return (
176
+ Array.isArray(right) &&
177
+ left.length === right.length &&
178
+ left.every((value, index) => equal(value, right[index]))
179
+ )
180
+ if (!record(left) || !record(right)) return false
181
+ const keys = Object.keys(left)
182
+ return (
183
+ keys.length === Object.keys(right).length &&
184
+ keys.every(
185
+ (key) => Object.hasOwn(right, key) && equal(left[key], right[key]),
186
+ )
187
+ )
188
+ }
189
+
190
+ function record(value: unknown): value is Readonly<Record<string, unknown>> {
191
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
192
+ }
193
+
120
194
  function bind(
121
195
  select: (context: Context, input?: never) => unknown,
122
196
  read: (context: Context) => readonly unknown[],
@@ -125,10 +199,10 @@ function bind(
125
199
  return {
126
200
  style: {
127
201
  [binding]: (context: Context) => select(context, input),
128
- [dependency]: (context: Context) => [
129
- ...read(context),
130
- ...key(input, context),
131
- ],
202
+ [dependency]: (context: Context) =>
203
+ input === undefined
204
+ ? read(context)
205
+ : [...read(context), ...key(input, context)],
132
206
  },
133
207
  }
134
208
  }
@@ -13,6 +13,7 @@ import * as AtRules from '../compiler/internal/AtRules.js'
13
13
  import * as Catalogs from '../compiler/internal/Catalogs.js'
14
14
  import * as Contract from '../compiler/internal/Contract.js'
15
15
  import * as Vite from '../vite/index.js'
16
+ import * as ThemeRules from '../web/internal/Themes.js'
16
17
 
17
18
  /** Portable build configuration. Vite uses the options from `zyzz/vite` instead. */
18
19
  export type Options = {
@@ -284,7 +285,9 @@ const portable = createUnplugin<Options | undefined, false>(
284
285
  }
285
286
  }
286
287
 
288
+ // Only the combined stylesheet is emitted, so generated theme rules are shared once.
287
289
  const result = compiler.compile({
290
+ [ThemeRules.shared]: 'all',
288
291
  [Syntax.cache]: snapshot.programs(modules),
289
292
  compiler: options.compiler,
290
293
  reset: options.reset ? Reset.read() : undefined,
@@ -460,7 +463,7 @@ const portable = createUnplugin<Options | undefined, false>(
460
463
  let line = 0
461
464
  css = ''
462
465
  for (const chunk of chunks) {
463
- if (!chunk.code) continue
466
+ if (!chunk.code.trim()) continue
464
467
  const traced = Mapping.fromMap(chunk.map)
465
468
  for (const mapping of Mapping.allMappings(traced)) {
466
469
  const generated = {
package/src/variants.ts CHANGED
@@ -26,8 +26,9 @@ type ChoiceName<choices> = keyof choices extends infer key
26
26
  ? key
27
27
  : never
28
28
  : never
29
+ /** `void` targets skip dynamic choice return types, which resolve through `Checked` and cycle when selections are read first. */
29
30
  type DynamicKeys<choices> = {
30
- [key in keyof choices]: choices[key] extends (...args: never[]) => unknown
31
+ [key in keyof choices]: choices[key] extends (...args: never[]) => void
31
32
  ? key
32
33
  : never
33
34
  }[keyof choices]
@@ -36,7 +37,7 @@ type Choice<choices> =
36
37
  | {
37
38
  [key in DynamicKeys<choices>]: choices[key] extends (
38
39
  values: infer values,
39
- ) => unknown
40
+ ) => void
40
41
  ? { readonly [name in key]: values } & {
41
42
  readonly [name in Exclude<keyof choices, key>]?: never
42
43
  }