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
package/src/node/Host.ts CHANGED
@@ -13,6 +13,7 @@ import * as Graph from '../compiler/Graph.js'
13
13
  import * as AtRules from '../compiler/internal/AtRules.js'
14
14
  import * as Catalogs from '../compiler/internal/Catalogs.js'
15
15
  import * as Relative from '../compiler/internal/Relative.js'
16
+ import * as Scheme from '../internal/Scheme.js'
16
17
  import * as Snapshot from './internal/Snapshot.js'
17
18
  import * as Syntax from '../compiler/internal/Syntax.js'
18
19
  import * as Source from '../compiler/Source.js'
@@ -620,7 +621,10 @@ export async function create(options: create.Options): Promise<Runtime> {
620
621
  artifacts.set('zyzz.shared.css.map', shared.map)
621
622
  }
622
623
 
623
- const moduleStylesheets = new Map<string, Stylesheet>()
624
+ const moduleStylesheets = new Map<
625
+ string,
626
+ { name: string; stylesheet: Stylesheet }
627
+ >()
624
628
 
625
629
  for (const name of Object.keys(sources)) {
626
630
  const output = graph.modules[`${options.packageId}/${name}`]!
@@ -661,23 +665,40 @@ export async function create(options: create.Options): Promise<Runtime> {
661
665
 
662
666
  artifacts.set(`${name}.css`, stylesheet.code)
663
667
  artifacts.set(`${name}.css.map`, stylesheet.map)
664
- // Module URLs resolve beside the module file; the complete stylesheet at
665
- // the output root carries a copy with those URLs rebased.
666
- moduleStylesheets.set(
667
- `${options.packageId}/${name}`,
668
- rebase(stylesheet, name, css === false ? false : css.minify),
669
- )
668
+ moduleStylesheets.set(`${options.packageId}/${name}`, {
669
+ name,
670
+ stylesheet,
671
+ })
670
672
  }
671
673
 
672
674
  // The complete stylesheet follows the module graph so a consumer's rules
673
675
  // cascade over the rules of the modules it imports.
674
676
  if (shared || moduleStylesheets.size) {
675
- const complete = concatenate([
676
- ...(shared ? [shared] : []),
677
- ...order(graph.dependencies, [...moduleStylesheets.keys()]).map(
678
- (id) => moduleStylesheets.get(id)!,
679
- ),
680
- ])
677
+ const parts = shared ? [shared] : []
678
+ let schemes = false
679
+
680
+ // Each module stylesheet declares the scheme classes to load alone. The
681
+ // complete stylesheet keeps the first declaration.
682
+ for (const id of order(graph.dependencies, [
683
+ ...moduleStylesheets.keys(),
684
+ ])) {
685
+ const { name, stylesheet } = moduleStylesheets.get(id)!
686
+ const declares = Object.values(Scheme.classes).some((className) =>
687
+ stylesheet.code.includes(`.${className}`),
688
+ )
689
+
690
+ parts.push(
691
+ rebase(
692
+ stylesheet,
693
+ name,
694
+ css === false ? false : css.minify,
695
+ schemes && declares,
696
+ ),
697
+ )
698
+ schemes ||= declares
699
+ }
700
+
701
+ const complete = concatenate(parts)
681
702
 
682
703
  artifacts.set('zyzz.css', complete.code)
683
704
  artifacts.set('zyzz.css.map', complete.map)
@@ -1025,7 +1046,7 @@ function concatenate(parts: readonly { code: string; map: string }[]) {
1025
1046
  let offset = 0
1026
1047
 
1027
1048
  for (const part of parts) {
1028
- if (!part.code) continue
1049
+ if (!part.code.trim()) continue
1029
1050
 
1030
1051
  const code = part.code.endsWith('\n') ? part.code : `${part.code}\n`
1031
1052
  const traced = Mapping.fromMap(part.map)
@@ -1169,15 +1190,22 @@ async function read(
1169
1190
  }
1170
1191
  }
1171
1192
 
1172
- /** Rewrites a nested module stylesheet's relative URLs against the output root. */
1193
+ /**
1194
+ * Copies a module stylesheet into the complete stylesheet at the output root,
1195
+ * rebasing nested relative URLs and optionally dropping the scheme rules.
1196
+ */
1173
1197
  function rebase(
1174
1198
  stylesheet: { code: string; map: string },
1175
1199
  name: string,
1176
1200
  minify: boolean,
1201
+ schemes: boolean,
1177
1202
  ) {
1178
1203
  const directory = Path.posix.dirname(name)
1179
1204
 
1180
- if (directory === '.' || !stylesheet.code.includes('url(')) return stylesheet
1205
+ if (!schemes && (directory === '.' || !stylesheet.code.includes('url(')))
1206
+ return stylesheet
1207
+
1208
+ const classes = Object.values(Scheme.classes)
1181
1209
 
1182
1210
  const result = AtRules.transform({
1183
1211
  code: Buffer.from(stylesheet.code),
@@ -1186,8 +1214,27 @@ function rebase(
1186
1214
  minify,
1187
1215
  sourceMap: true,
1188
1216
  visitor: {
1217
+ Rule: {
1218
+ style(rule) {
1219
+ const [selector, ...others] = rule.value.selectors
1220
+ const [component, ...rest] = selector ?? []
1221
+
1222
+ if (
1223
+ schemes &&
1224
+ !others.length &&
1225
+ !rest.length &&
1226
+ component?.type === 'class' &&
1227
+ classes.includes(component.name)
1228
+ )
1229
+ return []
1230
+ },
1231
+ },
1189
1232
  Url(url) {
1190
- if (/^(?:\/|[?#]|[a-z][a-z\d+.-]*:)/i.test(url.url)) return url
1233
+ if (
1234
+ directory === '.' ||
1235
+ /^(?:\/|[?#]|[a-z][a-z\d+.-]*:)/i.test(url.url)
1236
+ )
1237
+ return url
1191
1238
 
1192
1239
  return { ...url, url: Path.posix.join(directory, url.url) }
1193
1240
  },
@@ -14,10 +14,17 @@ export function defineConfig<const options extends Config.create.Options = {}>(
14
14
  options: options & Parameters<typeof Config.create<options>>[0] = {} as never,
15
15
  ): Subscription.defineConfig.ReturnType<options> {
16
16
  const config = Subscription.defineConfig<options>(options)
17
- return Object.freeze({
18
- ...config,
19
- Provider: dimensions(config.Provider),
20
- }) as Subscription.defineConfig.ReturnType<options>
17
+ // Copy descriptors rather than spreading, so deferred helpers stay deferred.
18
+ return Object.freeze(
19
+ Object.defineProperties(
20
+ { Provider: dimensions(config.Provider) } as Record<string, unknown>,
21
+ Object.fromEntries(
22
+ Object.entries(Object.getOwnPropertyDescriptors(config)).filter(
23
+ ([name]) => name !== 'Provider',
24
+ ),
25
+ ),
26
+ ),
27
+ ) as unknown as Subscription.defineConfig.ReturnType<options>
21
28
  }
22
29
 
23
30
  /** Provides selected variables, appearance, and automatic native dimensions. */
@@ -95,18 +102,40 @@ function dimensions<props extends Subscription.Provider.Props>(
95
102
  ): React.FunctionComponent<props> {
96
103
  return function WindowProvider(props: props) {
97
104
  const window = ReactNative.useWindowDimensions()
105
+ const scheme = ReactNative.useColorScheme()
98
106
  const viewport = React.useMemo(
99
107
  () => ({ height: window.height, width: window.width }),
100
108
  [window.height, window.width],
101
109
  )
110
+ const appearance = React.useMemo(
111
+ () => ({
112
+ adaptive,
113
+ colorScheme: scheme === 'dark' ? ('dark' as const) : ('light' as const),
114
+ }),
115
+ [scheme],
116
+ )
102
117
  return React.createElement(
103
118
  Viewport.context.Provider,
104
119
  { value: viewport },
105
- React.createElement(Component, props),
120
+ React.createElement(
121
+ Viewport.appearance.Provider,
122
+ { value: appearance },
123
+ React.createElement(Component, props),
124
+ ),
106
125
  )
107
126
  }
108
127
  }
109
128
 
129
+ // iOS resolves these colors from the window's appearance, so scheme changes need no Fabric update.
130
+ const adaptive =
131
+ ReactNative.Platform.OS === 'ios'
132
+ ? (light: string | number, dark: string | number) =>
133
+ ReactNative.DynamicColorIOS({
134
+ dark: dark as ReactNative.ColorValue,
135
+ light: light as ReactNative.ColorValue,
136
+ })
137
+ : undefined
138
+
110
139
  /** Compiler-only binding path, with React subscriptions for Expo Go. */
111
140
  export const useNativeStyles = useBindings
112
141
 
@@ -121,55 +150,68 @@ type Instance = {
121
150
 
122
151
  function useBindings() {
123
152
  const owner = React.useContext(Store.context)
124
- const [fallback] = React.useState(() =>
125
- Store.create({
153
+ // Views inside a Provider never read the fallback, so it is created only outside one.
154
+ const fallbackRef = React.useRef<ReturnType<typeof Store.create> | undefined>(
155
+ undefined,
156
+ )
157
+ if (!owner)
158
+ fallbackRef.current ??= Store.create({
126
159
  colorScheme: 'light',
127
160
  viewport: ReactNative.Dimensions.get('window'),
128
- }),
129
- )
130
- const store = owner ?? fallback
161
+ })
162
+ const fallback = fallbackRef.current
163
+ const store = (owner ?? fallback)!
131
164
  const [, render] = React.useReducer((count: number) => count + 1, 0)
132
- const [refs] = React.useState(
133
- () =>
134
- new Map<
135
- Instance,
136
- {
137
- active: boolean
138
- ref: React.Ref<Instance> | undefined
139
- release: () => void
140
- }
141
- >(),
142
- )
143
- React.useLayoutEffect(() => {
144
- for (const [instance, entry] of refs) {
145
- if (entry.active) continue
146
- entry.release()
147
- refs.delete(instance)
165
+ const [refs] = React.useState(() => {
166
+ const entries = new Map<
167
+ Instance,
168
+ {
169
+ active: boolean
170
+ ref: React.Ref<Instance> | undefined
171
+ release: () => void
172
+ }
173
+ >()
174
+ let scheduled = false
175
+ return {
176
+ entries,
177
+ // Runs after the commit, once every cleanup and re-attachment has settled, without a layout effect per render.
178
+ sweep() {
179
+ if (scheduled) return
180
+ scheduled = true
181
+ queueMicrotask(() => {
182
+ scheduled = false
183
+ for (const [instance, entry] of entries) {
184
+ if (entry.active) continue
185
+ entry.release()
186
+ entries.delete(instance)
187
+ }
188
+ })
189
+ },
148
190
  }
149
191
  })
150
- React.useLayoutEffect(
151
- () => () => {
152
- for (const entry of refs.values()) entry.release()
153
- refs.clear()
154
- },
155
- [refs],
156
- )
157
192
  return {
158
193
  style: NativeContext.application,
159
194
  view: (props: Record<string, unknown>) => {
160
195
  const snapshot = store.getSnapshot()
161
196
  const selected = NativeContext.resolve(props.style, snapshot)
162
197
  const initial = NativeContext.key(props.style, snapshot)
198
+ // A context-free style never needs a binding, so React keeps a stable element without a ref wrapper.
199
+ if (
200
+ !initial.length &&
201
+ typeof selected !== 'function' &&
202
+ props.ref == null
203
+ )
204
+ return { ...props, style: selected }
163
205
 
164
206
  return {
165
207
  ...props,
166
208
  ref: (value: Instance | null) => {
167
209
  const ref = props.ref as React.Ref<Instance> | undefined
168
210
  if (!value) return
169
- let entry = refs.get(value)
211
+ let entry = refs.entries.get(value)
170
212
  if (entry && entry.ref !== ref) {
171
213
  entry.release()
172
- refs.delete(value)
214
+ refs.entries.delete(value)
173
215
  entry = undefined
174
216
  }
175
217
  if (!entry) {
@@ -184,7 +226,7 @@ function useBindings() {
184
226
  else if (ref) ref.current = null
185
227
  },
186
228
  }
187
- refs.set(value, entry)
229
+ refs.entries.set(value, entry)
188
230
  }
189
231
  entry.active = true
190
232
  const attached = entry
@@ -199,7 +241,9 @@ function useBindings() {
199
241
  if (store.getSnapshot() !== snapshot) render()
200
242
  return unsubscribe
201
243
  }
202
- const id = NativeZyzz!.attach(node, nativeProps(selected))
244
+ // Most views never change selection, so they bind on their first patch.
245
+ let id: number | undefined
246
+ let bound = false
203
247
  let previous = initial
204
248
  const read = (context: NativeContext.Context) => {
205
249
  const keys = NativeContext.key(props.style, context)
@@ -208,30 +252,38 @@ function useBindings() {
208
252
  keys.every((key, index) => Object.is(key, previous[index]))
209
253
  )
210
254
  return { commit: () => {} }
211
- const selected = NativeContext.resolve(props.style, context)
212
- if (!selective(selected)) return { commit: render }
255
+ const next = NativeContext.resolve(props.style, context)
256
+ if (!selective(next)) return { commit: render }
257
+ id ??= ++bindings
213
258
  return {
214
259
  commit: () => {
215
260
  previous = keys
261
+ bound = true
216
262
  },
217
- patch: {
218
- id,
219
- props: nativeProps(selected),
220
- },
263
+ patch: bound
264
+ ? { id, props: nativeProps(next) }
265
+ : {
266
+ id,
267
+ node,
268
+ props: nativeProps(next),
269
+ rendered: nativeProps(selected),
270
+ },
221
271
  }
222
272
  }
223
273
  // Context can change between render and committing the host ref.
224
- const current = read(store.getSnapshot())
225
- if (current.patch) write([current.patch])
226
- current.commit()
274
+ if (store.getSnapshot() !== snapshot) {
275
+ const current = read(store.getSnapshot())
276
+ if (current.patch) write([current.patch])
277
+ current.commit()
278
+ }
227
279
  const unbind = store.bind(read, write)
228
280
  return () => {
229
281
  unbind()
230
- NativeZyzz!.detach(id)
282
+ if (bound) NativeZyzz!.detach(id!)
231
283
  }
232
284
  })()
233
285
  const update = () =>
234
- fallback.update({
286
+ fallback!.update({
235
287
  colorScheme: 'light',
236
288
  viewport: ReactNative.Dimensions.get('window'),
237
289
  })
@@ -246,6 +298,7 @@ function useBindings() {
246
298
  detach()
247
299
  subscription?.remove()
248
300
  attached.active = false
301
+ refs.sweep()
249
302
  }
250
303
  },
251
304
  style: selected,
@@ -256,6 +309,21 @@ function useBindings() {
256
309
 
257
310
  function selective(style: unknown): boolean {
258
311
  if (typeof style === 'function') return false
312
+ const shared =
313
+ typeof style === 'object' &&
314
+ style !== null &&
315
+ !Array.isArray(style) &&
316
+ Object.isFrozen(style)
317
+ const cached = shared ? patchable.get(style) : undefined
318
+ if (cached !== undefined) return cached
319
+ const result = writable(style)
320
+ if (shared) patchable.set(style, result)
321
+ return result
322
+ }
323
+
324
+ const patchable = new WeakMap<object, boolean>()
325
+
326
+ function writable(style: unknown): boolean {
259
327
  const value = ReactNative.StyleSheet.flatten(
260
328
  style as ReactNative.StyleProp<ReactNative.ViewStyle>,
261
329
  )
@@ -279,7 +347,16 @@ function selective(style: unknown): boolean {
279
347
  )
280
348
  }
281
349
 
350
+ // Compiled styles are frozen and shared by every view that selects them.
351
+ const converted = new WeakMap<object, Record<string, unknown>>()
282
352
  function nativeProps(style: unknown): Record<string, unknown> {
353
+ const shared =
354
+ typeof style === 'object' &&
355
+ style !== null &&
356
+ !Array.isArray(style) &&
357
+ Object.isFrozen(style)
358
+ const cached = shared ? converted.get(style) : undefined
359
+ if (cached) return cached
283
360
  const flattened =
284
361
  ReactNative.StyleSheet.flatten(
285
362
  style as ReactNative.StyleProp<ReactNative.ViewStyle>,
@@ -289,9 +366,13 @@ function nativeProps(style: unknown): Record<string, unknown> {
289
366
  if (name === 'color' || name.endsWith('Color'))
290
367
  result[name] = ReactNative.processColor(value as ReactNative.ColorValue)
291
368
  }
369
+ if (shared) converted.set(style, result)
292
370
  return result
293
371
  }
294
372
 
373
+ // Binding identifiers are unique per JavaScript runtime, like the native module.
374
+ let bindings = 0
375
+
295
376
  function write(patches: readonly Store.Patch[]) {
296
377
  NativeZyzz!.update(patches)
297
378
  }
@@ -4,16 +4,14 @@ import { TurboModuleRegistry } from 'react-native'
4
4
 
5
5
  /** Codegen contract, consumed only by the native adapter. */
6
6
  export interface Spec extends TurboModule {
7
- /** Links a mounted Fabric node and its rendered style. */
8
- readonly attach: (
9
- node: CodegenTypes.UnsafeObject,
10
- props: CodegenTypes.UnsafeObject,
11
- ) => number
12
7
  /** Releases a view binding. */
13
8
  readonly detach: (id: number) => void
14
9
  /** Reports live bindings and applied update batches. */
15
10
  readonly inspect: () => CodegenTypes.UnsafeObject
16
- /** Applies one batch of changed native styles. */
11
+ /**
12
+ * Applies one batch of changed native styles. An update with `node` and
13
+ * `rendered` first binds that mounted Fabric node to its rendered style.
14
+ */
17
15
  readonly update: (updates: ReadonlyArray<CodegenTypes.UnsafeObject>) => void
18
16
  }
19
17
 
@@ -6,8 +6,12 @@ import * as React from 'react'
6
6
  export type Patch = {
7
7
  /** Runtime-local mounted view identifier. */
8
8
  readonly id: number
9
+ /** Mounted Fabric node, sent until the binding's first patch commits. */
10
+ readonly node?: object | undefined
9
11
  /** Processed native style properties. */
10
12
  readonly props: Record<string, unknown>
13
+ /** Processed style React rendered, sent with `node`. */
14
+ readonly rendered?: Record<string, unknown> | undefined
11
15
  }
12
16
 
13
17
  type Selection = {
@@ -1,4 +1,5 @@
1
1
  /** Connects the native device adapter to framework subscriptions. @module */
2
+ import type * as NativeContext from '../../runtime/NativeContext.js'
2
3
  import type * as Media from '../../runtime/internal/NativeMedia.js'
3
4
  import * as React from 'react'
4
5
 
@@ -6,3 +7,9 @@ import * as React from 'react'
6
7
  export const context = React.createContext<Media.Viewport | undefined>(
7
8
  undefined,
8
9
  )
10
+
11
+ /** Device appearance for `colorScheme: 'system'`, with the platform's appearance-following color when it has one. */
12
+ export const appearance = React.createContext<{
13
+ readonly adaptive?: NativeContext.Context['adaptive']
14
+ readonly colorScheme: 'dark' | 'light'
15
+ }>({ colorScheme: 'light' })
@@ -12,7 +12,6 @@ import * as Viewport from './internal/Viewport.js'
12
12
  export function defineConfig<const options extends Config.create.Options = {}>(
13
13
  options: options & Parameters<typeof Config.create<options>>[0] = {} as never,
14
14
  ): defineConfig.ReturnType<options> {
15
- const config = Config.create<options>(options)
16
15
  const defaultVars = (options as Config.VariableOptions).defaultVars
17
16
  const names =
18
17
  defaultVars === undefined
@@ -29,10 +28,71 @@ export function defineConfig<const options extends Config.create.Options = {}>(
29
28
  })
30
29
  }
31
30
 
31
+ if ((options as Config.VariableOptions).vars === undefined)
32
+ return Object.freeze({
33
+ ...Config.create<options>(options),
34
+ Provider: BoundProvider,
35
+ }) as defineConfig.ReturnType<options>
36
+
37
+ // Compiled native styles and `useVars` read precompiled tables, so variable
38
+ // configurations build their authoring helpers on first runtime use.
39
+ let config: Helpers | undefined
40
+ const create = () =>
41
+ (config ??= (Config.create as (options: object) => Helpers)(options))
32
42
  return Object.freeze({
33
- ...config,
43
+ get appearance() {
44
+ return create().appearance
45
+ },
34
46
  Provider: BoundProvider,
35
- }) as defineConfig.ReturnType<options>
47
+ get script() {
48
+ return create().script
49
+ },
50
+ style: (...args: never[]) => create().style(...args),
51
+ variants: (...args: never[]) => create().variants(...args),
52
+ vars: deferred(() => create().vars),
53
+ }) as unknown as defineConfig.ReturnType<options>
54
+ }
55
+
56
+ /** Untyped view of a variable configuration's helpers. */
57
+ type Helpers = {
58
+ readonly appearance: unknown
59
+ readonly script: unknown
60
+ readonly style: (...args: never[]) => unknown
61
+ readonly variants: (...args: never[]) => unknown
62
+ readonly vars: object
63
+ }
64
+
65
+ /** Forwards every operation to a value built on first use, mirroring it onto the proxy target so invariants hold. */
66
+ function deferred<value extends object>(create: () => value): value {
67
+ const target = (() => {}) as unknown as value
68
+ let ready = false
69
+ function resolve() {
70
+ if (ready) return target
71
+ const value = create()
72
+ Object.defineProperties(target, Object.getOwnPropertyDescriptors(value))
73
+ Object.setPrototypeOf(target, Object.getPrototypeOf(value))
74
+ if (Object.isFrozen(value)) Object.freeze(target)
75
+ ready = true
76
+ return target
77
+ }
78
+ return new Proxy(target, {
79
+ apply: (_, self, args) =>
80
+ Reflect.apply(create() as (...args: unknown[]) => unknown, self, args),
81
+ defineProperty: (_, key, descriptor) =>
82
+ Reflect.defineProperty(resolve(), key, descriptor),
83
+ deleteProperty: (_, key) => Reflect.deleteProperty(resolve(), key),
84
+ get: (_, key) => Reflect.get(resolve(), key),
85
+ getOwnPropertyDescriptor: (_, key) =>
86
+ Reflect.getOwnPropertyDescriptor(resolve(), key),
87
+ getPrototypeOf: () => Reflect.getPrototypeOf(resolve()),
88
+ has: (_, key) => Reflect.has(resolve(), key),
89
+ isExtensible: () => Reflect.isExtensible(resolve()),
90
+ ownKeys: () => Reflect.ownKeys(resolve()),
91
+ preventExtensions: () => Reflect.preventExtensions(resolve()),
92
+ set: (_, key, value) => Reflect.set(resolve(), key, value),
93
+ setPrototypeOf: (_, prototype) =>
94
+ Reflect.setPrototypeOf(resolve(), prototype),
95
+ })
36
96
  }
37
97
 
38
98
  /** Native configuration helpers and inferred Provider props. */
@@ -62,23 +122,30 @@ export function Provider(props: Provider.Props) {
62
122
  if (Object.hasOwn(props, 'set'))
63
123
  throw new Error('Provider uses vars instead of set.')
64
124
  if (
65
- (props.colorScheme !== 'light' && props.colorScheme !== 'dark') ||
125
+ (props.colorScheme !== 'light' &&
126
+ props.colorScheme !== 'dark' &&
127
+ props.colorScheme !== 'system') ||
66
128
  (props.vars !== undefined &&
67
129
  (typeof props.vars !== 'string' || !props.vars.trim()))
68
130
  )
69
131
  throw new Error(
70
- 'Native appearance requires a resolved light/dark scheme and a nonempty vars name.',
132
+ 'Native appearance requires a light, dark, or system scheme and a nonempty vars name.',
71
133
  )
72
134
 
73
135
  const viewport = React.useContext(Viewport.context)
136
+ const system = React.useContext(Viewport.appearance)
137
+ const colorScheme =
138
+ props.colorScheme === 'system' ? system.colorScheme : props.colorScheme
139
+ const adaptive = props.colorScheme === 'system' ? system.adaptive : undefined
74
140
  const [store] = React.useState(() =>
75
- Store.create({ colorScheme: props.colorScheme, set: props.vars, viewport }),
141
+ Store.create({ adaptive, colorScheme, set: props.vars, viewport }),
76
142
  )
77
143
  const [, publish] = React.useReducer((version: number) => version + 1, 0)
78
144
  const committed = React.useRef(props.children)
79
145
  const snapshot = store.getSnapshot()
80
146
  const pending =
81
- snapshot.colorScheme !== props.colorScheme ||
147
+ snapshot.adaptive !== adaptive ||
148
+ snapshot.colorScheme !== colorScheme ||
82
149
  snapshot.set !== props.vars ||
83
150
  snapshot.viewport?.width !== viewport?.width ||
84
151
  snapshot.viewport?.height !== viewport?.height
@@ -89,9 +156,9 @@ export function Provider(props: Provider.Props) {
89
156
  // Keep the committed subtree until its selection is published. This avoids
90
157
  // rendering new application props with the previous store snapshot, without
91
158
  // mutating an external store during a potentially abandoned React render.
92
- store.update({ colorScheme: props.colorScheme, set: props.vars, viewport })
159
+ store.update({ adaptive, colorScheme, set: props.vars, viewport })
93
160
  publish()
94
- }, [children, pending, props.colorScheme, props.vars, store, viewport])
161
+ }, [adaptive, children, colorScheme, pending, props.vars, store, viewport])
95
162
 
96
163
  return React.createElement(Store.context.Provider, { value: store }, children)
97
164
  }
@@ -102,8 +169,11 @@ export declare namespace Provider {
102
169
  type Props = {
103
170
  /** Components that consume compiled native styling or values. */
104
171
  readonly children?: React.ReactNode | undefined
105
- /** Resolved device scheme or an application override. */
106
- readonly colorScheme: 'dark' | 'light'
172
+ /**
173
+ * Resolved device scheme, an application override, or `system` to follow the device.
174
+ * On iOS, `system` styles whose schemes differ only in color switch without updates.
175
+ */
176
+ readonly colorScheme: 'dark' | 'light' | 'system'
107
177
  /** Selected variables. Omission uses each configuration's default. */
108
178
  readonly vars?: string | undefined
109
179
  }