zyzz 0.0.22 → 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 (96) hide show
  1. package/README.md +6 -6
  2. package/dist/babel/NativeJsx.d.ts.map +1 -1
  3. package/dist/babel/NativeJsx.js +14 -4
  4. package/dist/babel/NativeJsx.js.map +1 -1
  5. package/dist/compiler/Graph.js +78 -35
  6. package/dist/compiler/Graph.js.map +1 -1
  7. package/dist/compiler/internal/Dynamic.d.ts.map +1 -1
  8. package/dist/compiler/internal/Dynamic.js +3 -0
  9. package/dist/compiler/internal/Dynamic.js.map +1 -1
  10. package/dist/compiler/internal/Expression.js +2 -2
  11. package/dist/compiler/internal/Expression.js.map +1 -1
  12. package/dist/compiler/internal/NativeBindings.d.ts.map +1 -1
  13. package/dist/compiler/internal/NativeBindings.js +1 -0
  14. package/dist/compiler/internal/NativeBindings.js.map +1 -1
  15. package/dist/compiler/internal/NativeVars.d.ts.map +1 -1
  16. package/dist/compiler/internal/NativeVars.js +3 -0
  17. package/dist/compiler/internal/NativeVars.js.map +1 -1
  18. package/dist/compiler/internal/Themes.d.ts +2 -0
  19. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  20. package/dist/compiler/internal/Themes.js +24 -22
  21. package/dist/compiler/internal/Themes.js.map +1 -1
  22. package/dist/internal/Configuration.d.ts.map +1 -1
  23. package/dist/internal/Configuration.js +2 -1
  24. package/dist/internal/Configuration.js.map +1 -1
  25. package/dist/metro/Graph.d.ts.map +1 -1
  26. package/dist/metro/Graph.js +11 -2
  27. package/dist/metro/Graph.js.map +1 -1
  28. package/dist/node/Host.d.ts.map +1 -1
  29. package/dist/node/Host.js +40 -12
  30. package/dist/node/Host.js.map +1 -1
  31. package/dist/react-native/StyleSheet.d.ts +7 -3
  32. package/dist/react-native/StyleSheet.d.ts.map +1 -1
  33. package/dist/react-native/StyleSheet.js +8 -0
  34. package/dist/react-native/StyleSheet.js.map +1 -1
  35. package/dist/react-native/internal/Device.js +118 -38
  36. package/dist/react-native/internal/Device.js.map +3 -3
  37. package/dist/react-native/internal/NativeZyzz.js.map +2 -2
  38. package/dist/react-native/internal/Scalar.d.ts +16 -0
  39. package/dist/react-native/internal/Scalar.d.ts.map +1 -1
  40. package/dist/react-native/internal/Scalar.js +53 -2
  41. package/dist/react-native/internal/Scalar.js.map +1 -1
  42. package/dist/react-native/internal/Store.d.ts +4 -0
  43. package/dist/react-native/internal/Store.d.ts.map +1 -1
  44. package/dist/react-native/internal/Store.js.map +1 -1
  45. package/dist/react-native/internal/Styles.d.ts +19 -0
  46. package/dist/react-native/internal/Styles.d.ts.map +1 -0
  47. package/dist/react-native/internal/Styles.js +35 -0
  48. package/dist/react-native/internal/Styles.js.map +1 -0
  49. package/dist/react-native/internal/Viewport.d.ts +6 -0
  50. package/dist/react-native/internal/Viewport.d.ts.map +1 -1
  51. package/dist/react-native/internal/Viewport.js +2 -0
  52. package/dist/react-native/internal/Viewport.js.map +1 -1
  53. package/dist/react-native/react.d.ts +44 -16
  54. package/dist/react-native/react.d.ts.map +1 -1
  55. package/dist/react-native/react.js +68 -47
  56. package/dist/react-native/react.js.map +1 -1
  57. package/dist/runtime/NativeContext.d.ts +6 -0
  58. package/dist/runtime/NativeContext.d.ts.map +1 -1
  59. package/dist/runtime/NativeContext.js +62 -9
  60. package/dist/runtime/NativeContext.js.map +1 -1
  61. package/dist/runtime/NativeDynamic.d.ts.map +1 -1
  62. package/dist/runtime/NativeDynamic.js +5 -0
  63. package/dist/runtime/NativeDynamic.js.map +1 -1
  64. package/dist/unplugin/index.d.ts.map +1 -1
  65. package/dist/unplugin/index.js +4 -1
  66. package/dist/unplugin/index.js.map +1 -1
  67. package/dist/variants.d.ts +3 -2
  68. package/dist/variants.d.ts.map +1 -1
  69. package/dist/variants.js.map +1 -1
  70. package/native/NativeZyzz.cpp +52 -65
  71. package/native/NativeZyzz.h +0 -4
  72. package/native/ZyzzSpecJSI.h +0 -10
  73. package/native/android/ZyzzSpec-generated.cpp +0 -6
  74. package/package.json +1 -1
  75. package/src/babel/NativeJsx.ts +17 -8
  76. package/src/compiler/Graph.ts +89 -38
  77. package/src/compiler/internal/Dynamic.ts +4 -0
  78. package/src/compiler/internal/Expression.ts +2 -2
  79. package/src/compiler/internal/NativeBindings.ts +1 -0
  80. package/src/compiler/internal/NativeVars.ts +3 -0
  81. package/src/compiler/internal/Themes.ts +29 -23
  82. package/src/internal/Configuration.ts +2 -1
  83. package/src/metro/Graph.ts +17 -2
  84. package/src/node/Host.ts +64 -17
  85. package/src/react-native/StyleSheet.ts +32 -9
  86. package/src/react-native/internal/Device.ts +196 -49
  87. package/src/react-native/internal/NativeZyzz.ts +4 -6
  88. package/src/react-native/internal/Scalar.ts +73 -2
  89. package/src/react-native/internal/Store.ts +4 -0
  90. package/src/react-native/internal/Styles.ts +77 -0
  91. package/src/react-native/internal/Viewport.ts +7 -0
  92. package/src/react-native/react.ts +142 -72
  93. package/src/runtime/NativeContext.ts +83 -9
  94. package/src/runtime/NativeDynamic.ts +7 -0
  95. package/src/unplugin/index.ts +4 -1
  96. package/src/variants.ts +3 -2
@@ -5,6 +5,7 @@ import NativeZyzz from './NativeZyzz.js'
5
5
  import * as React from 'react'
6
6
  import * as ReactNative from 'react-native'
7
7
  import * as Store from './Store.js'
8
+ import * as Styles from './Styles.js'
8
9
  import * as Subscription from '../react.js'
9
10
  import * as Viewport from './Viewport.js'
10
11
 
@@ -13,34 +14,128 @@ export function defineConfig<const options extends Config.create.Options = {}>(
13
14
  options: options & Parameters<typeof Config.create<options>>[0] = {} as never,
14
15
  ): Subscription.defineConfig.ReturnType<options> {
15
16
  const config = Subscription.defineConfig<options>(options)
16
- return Object.freeze({
17
- ...config,
18
- Provider: dimensions(config.Provider),
19
- }) 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>
20
28
  }
21
29
 
22
30
  /** Provides selected variables, appearance, and automatic native dimensions. */
23
31
  export const Provider = dimensions(Subscription.Provider)
24
32
 
25
- export { useStyles, useVars, withStyles } from '../react.js'
33
+ /**
34
+ * Compiler-inserted subscription for resolving native style applications.
35
+ * Outside a Provider, selections use default variables, the light scheme, and window dimensions.
36
+ */
37
+ export function useStyles(): Subscription.useStyles.ReturnType {
38
+ const owner = React.useContext(Store.context)
39
+ const [fallback] = React.useState(() =>
40
+ Store.create({
41
+ colorScheme: 'light',
42
+ viewport: ReactNative.Dimensions.get('window'),
43
+ }),
44
+ )
45
+ React.useLayoutEffect(() => {
46
+ if (owner) return
47
+ const update = () =>
48
+ fallback.update({
49
+ colorScheme: 'light',
50
+ viewport: ReactNative.Dimensions.get('window'),
51
+ })
52
+ // Dimensions can change between render and commit.
53
+ update()
54
+ const subscription = ReactNative.Dimensions.addEventListener(
55
+ 'change',
56
+ update,
57
+ )
58
+ return () => subscription.remove()
59
+ }, [fallback, owner])
60
+
61
+ return Styles.use(owner ?? fallback)
62
+ }
63
+
64
+ export { useVars } from '../react.js'
65
+
66
+ /**
67
+ * Resolves compiled style props for function components using React updates.
68
+ * @param Component - Function, memo, or ref-forwarding component receiving native styles.
69
+ * @param options - Additional style-bearing prop names.
70
+ * @returns A component with the original call signature and ref. Type parameters remain when `styleProps` is omitted.
71
+ * @throws For invalid style prop names.
72
+ */
73
+ export function withStyles<
74
+ props extends object,
75
+ result extends React.ReactNode,
76
+ >(
77
+ Component: (props: props) => result,
78
+ options?: NoInfer<Subscription.withStyles.Options<(props: props) => result>>,
79
+ ): (props: props) => result
80
+ /**
81
+ * Resolves compiled style props for class and host components using React updates.
82
+ * @param Component - Class component or host element type receiving native styles.
83
+ * @param options - Additional style-bearing prop names.
84
+ * @returns A component preserving the original props and instance ref.
85
+ * @throws For invalid style prop names.
86
+ */
87
+ export function withStyles<const component extends React.ElementType>(
88
+ Component: component,
89
+ options?: Subscription.withStyles.Options<component>,
90
+ ): React.ForwardRefExoticComponent<React.ComponentPropsWithRef<component>>
91
+ export function withStyles(
92
+ Component: React.ElementType,
93
+ options: Subscription.withStyles.Options<
94
+ (props: Readonly<Record<string, unknown>>) => React.ReactNode
95
+ > = {},
96
+ ): React.ElementType {
97
+ return Styles.wrap(Component, options, useStyles)
98
+ }
26
99
 
27
100
  function dimensions<props extends Subscription.Provider.Props>(
28
101
  Component: React.FunctionComponent<props>,
29
102
  ): React.FunctionComponent<props> {
30
103
  return function WindowProvider(props: props) {
31
104
  const window = ReactNative.useWindowDimensions()
105
+ const scheme = ReactNative.useColorScheme()
32
106
  const viewport = React.useMemo(
33
107
  () => ({ height: window.height, width: window.width }),
34
108
  [window.height, window.width],
35
109
  )
110
+ const appearance = React.useMemo(
111
+ () => ({
112
+ adaptive,
113
+ colorScheme: scheme === 'dark' ? ('dark' as const) : ('light' as const),
114
+ }),
115
+ [scheme],
116
+ )
36
117
  return React.createElement(
37
118
  Viewport.context.Provider,
38
119
  { value: viewport },
39
- React.createElement(Component, props),
120
+ React.createElement(
121
+ Viewport.appearance.Provider,
122
+ { value: appearance },
123
+ React.createElement(Component, props),
124
+ ),
40
125
  )
41
126
  }
42
127
  }
43
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
+
44
139
  /** Compiler-only binding path, with React subscriptions for Expo Go. */
45
140
  export const useNativeStyles = useBindings
46
141
 
@@ -55,55 +150,68 @@ type Instance = {
55
150
 
56
151
  function useBindings() {
57
152
  const owner = React.useContext(Store.context)
58
- const [fallback] = React.useState(() =>
59
- 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({
60
159
  colorScheme: 'light',
61
160
  viewport: ReactNative.Dimensions.get('window'),
62
- }),
63
- )
64
- const store = owner ?? fallback
161
+ })
162
+ const fallback = fallbackRef.current
163
+ const store = (owner ?? fallback)!
65
164
  const [, render] = React.useReducer((count: number) => count + 1, 0)
66
- const [refs] = React.useState(
67
- () =>
68
- new Map<
69
- Instance,
70
- {
71
- active: boolean
72
- ref: React.Ref<Instance> | undefined
73
- release: () => void
74
- }
75
- >(),
76
- )
77
- React.useLayoutEffect(() => {
78
- for (const [instance, entry] of refs) {
79
- if (entry.active) continue
80
- entry.release()
81
- 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
+ },
82
190
  }
83
191
  })
84
- React.useLayoutEffect(
85
- () => () => {
86
- for (const entry of refs.values()) entry.release()
87
- refs.clear()
88
- },
89
- [refs],
90
- )
91
192
  return {
92
193
  style: NativeContext.application,
93
194
  view: (props: Record<string, unknown>) => {
94
195
  const snapshot = store.getSnapshot()
95
196
  const selected = NativeContext.resolve(props.style, snapshot)
96
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 }
97
205
 
98
206
  return {
99
207
  ...props,
100
208
  ref: (value: Instance | null) => {
101
209
  const ref = props.ref as React.Ref<Instance> | undefined
102
210
  if (!value) return
103
- let entry = refs.get(value)
211
+ let entry = refs.entries.get(value)
104
212
  if (entry && entry.ref !== ref) {
105
213
  entry.release()
106
- refs.delete(value)
214
+ refs.entries.delete(value)
107
215
  entry = undefined
108
216
  }
109
217
  if (!entry) {
@@ -118,7 +226,7 @@ function useBindings() {
118
226
  else if (ref) ref.current = null
119
227
  },
120
228
  }
121
- refs.set(value, entry)
229
+ refs.entries.set(value, entry)
122
230
  }
123
231
  entry.active = true
124
232
  const attached = entry
@@ -133,7 +241,9 @@ function useBindings() {
133
241
  if (store.getSnapshot() !== snapshot) render()
134
242
  return unsubscribe
135
243
  }
136
- 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
137
247
  let previous = initial
138
248
  const read = (context: NativeContext.Context) => {
139
249
  const keys = NativeContext.key(props.style, context)
@@ -142,30 +252,38 @@ function useBindings() {
142
252
  keys.every((key, index) => Object.is(key, previous[index]))
143
253
  )
144
254
  return { commit: () => {} }
145
- const selected = NativeContext.resolve(props.style, context)
146
- if (!selective(selected)) return { commit: render }
255
+ const next = NativeContext.resolve(props.style, context)
256
+ if (!selective(next)) return { commit: render }
257
+ id ??= ++bindings
147
258
  return {
148
259
  commit: () => {
149
260
  previous = keys
261
+ bound = true
150
262
  },
151
- patch: {
152
- id,
153
- props: nativeProps(selected),
154
- },
263
+ patch: bound
264
+ ? { id, props: nativeProps(next) }
265
+ : {
266
+ id,
267
+ node,
268
+ props: nativeProps(next),
269
+ rendered: nativeProps(selected),
270
+ },
155
271
  }
156
272
  }
157
273
  // Context can change between render and committing the host ref.
158
- const current = read(store.getSnapshot())
159
- if (current.patch) write([current.patch])
160
- current.commit()
274
+ if (store.getSnapshot() !== snapshot) {
275
+ const current = read(store.getSnapshot())
276
+ if (current.patch) write([current.patch])
277
+ current.commit()
278
+ }
161
279
  const unbind = store.bind(read, write)
162
280
  return () => {
163
281
  unbind()
164
- NativeZyzz!.detach(id)
282
+ if (bound) NativeZyzz!.detach(id!)
165
283
  }
166
284
  })()
167
285
  const update = () =>
168
- fallback.update({
286
+ fallback!.update({
169
287
  colorScheme: 'light',
170
288
  viewport: ReactNative.Dimensions.get('window'),
171
289
  })
@@ -180,6 +298,7 @@ function useBindings() {
180
298
  detach()
181
299
  subscription?.remove()
182
300
  attached.active = false
301
+ refs.sweep()
183
302
  }
184
303
  },
185
304
  style: selected,
@@ -190,6 +309,21 @@ function useBindings() {
190
309
 
191
310
  function selective(style: unknown): boolean {
192
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 {
193
327
  const value = ReactNative.StyleSheet.flatten(
194
328
  style as ReactNative.StyleProp<ReactNative.ViewStyle>,
195
329
  )
@@ -213,7 +347,16 @@ function selective(style: unknown): boolean {
213
347
  )
214
348
  }
215
349
 
350
+ // Compiled styles are frozen and shared by every view that selects them.
351
+ const converted = new WeakMap<object, Record<string, unknown>>()
216
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
217
360
  const flattened =
218
361
  ReactNative.StyleSheet.flatten(
219
362
  style as ReactNative.StyleProp<ReactNative.ViewStyle>,
@@ -223,9 +366,13 @@ function nativeProps(style: unknown): Record<string, unknown> {
223
366
  if (name === 'color' || name.endsWith('Color'))
224
367
  result[name] = ReactNative.processColor(value as ReactNative.ColorValue)
225
368
  }
369
+ if (shared) converted.set(style, result)
226
370
  return result
227
371
  }
228
372
 
373
+ // Binding identifiers are unique per JavaScript runtime, like the native module.
374
+ let bindings = 0
375
+
229
376
  function write(patches: readonly Store.Patch[]) {
230
377
  NativeZyzz!.update(patches)
231
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
 
@@ -48,6 +48,7 @@ export const properties = {
48
48
  columnGap: 'length',
49
49
  direction: ['ltr', 'rtl'],
50
50
  display: ['contents', 'flex', 'none'],
51
+ flex: 'flex',
51
52
  flexBasis: 'size',
52
53
  flexDirection: ['column', 'column-reverse', 'row', 'row-reverse'],
53
54
  flexGrow: 'number',
@@ -60,6 +61,7 @@ export const properties = {
60
61
  fontWeight: 'weight',
61
62
  gap: 'length',
62
63
  height: 'size',
64
+ inset: 'boxOffset',
63
65
  justifyContent: [
64
66
  'center',
65
67
  'flex-end',
@@ -206,18 +208,87 @@ export function convert(
206
208
  return value
207
209
  fail('unsupported_value', 'Invalid native percentage.', path)
208
210
  }
209
- if (kind === 'box' || kind === 'boxSigned') {
211
+ if (kind === 'flex') {
212
+ flex(value, options, path)
213
+ return 0
214
+ }
215
+ if (kind === 'box' || kind === 'boxOffset' || kind === 'boxSigned') {
210
216
  const values =
211
217
  typeof value === 'string' ? Calculation.parts(value) : [value]
212
218
  if (!values.length || values.length > 4)
213
219
  fail('unsupported_value', 'Use one to four scalar lengths.', path)
214
220
  for (const value of values)
215
- length(value, options, kind === 'boxSigned', path)
221
+ if (kind === 'boxOffset') convert('offset', value, options, path)
222
+ else length(value, options, kind === 'boxSigned', path)
216
223
  return 0
217
224
  }
218
225
  return length(value, options, kind === 'signed' || kind === 'offset', path)
219
226
  }
220
227
 
228
+ /**
229
+ * Expands the CSS `flex` shorthand to native longhands with CSS semantics.
230
+ * Omitted factors default to 1 and an omitted basis to 0, unlike the React Native `flex` prop.
231
+ */
232
+ export function flex(
233
+ value: number | string,
234
+ options: Options,
235
+ path: readonly string[],
236
+ ): flex.ReturnType {
237
+ const keyword = typeof value === 'string' ? value.trim() : undefined
238
+ if (keyword === 'none')
239
+ return { flexBasis: 'auto', flexGrow: 0, flexShrink: 0 }
240
+ if (keyword === 'auto')
241
+ return { flexBasis: 'auto', flexGrow: 1, flexShrink: 1 }
242
+ if (keyword === 'initial')
243
+ return { flexBasis: 'auto', flexGrow: 0, flexShrink: 1 }
244
+
245
+ const parts = typeof value === 'string' ? Calculation.parts(value) : [value]
246
+ const kinds: ('basis' | 'factor')[] = []
247
+ for (const part of parts) {
248
+ const numeric =
249
+ typeof part === 'number' ||
250
+ /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(part)
251
+ // A unitless zero after two factors is the basis, as in CSS.
252
+ const basis =
253
+ !numeric || (kinds.join(' ') === 'factor factor' && Number(part) === 0)
254
+ kinds.push(basis ? 'basis' : 'factor')
255
+ }
256
+ // Factors stay adjacent, and the basis may precede or follow them.
257
+ if (
258
+ ![
259
+ 'basis',
260
+ 'basis factor',
261
+ 'basis factor factor',
262
+ 'factor',
263
+ 'factor basis',
264
+ 'factor factor',
265
+ 'factor factor basis',
266
+ ].includes(kinds.join(' '))
267
+ )
268
+ fail('unsupported_value', 'Invalid flex shorthand.', path)
269
+
270
+ const factors = parts
271
+ .filter((_, index) => kinds[index] === 'factor')
272
+ .map((part) => convert('number', Number(part), options, path) as number)
273
+ const basis = parts.find((_, index) => kinds[index] === 'basis')
274
+
275
+ return {
276
+ flexBasis: basis === undefined ? 0 : convert('size', basis, options, path),
277
+ flexGrow: factors[0] ?? 1,
278
+ flexShrink: factors[1] ?? 1,
279
+ }
280
+ }
281
+
282
+ /** Native longhands produced by the `flex` shorthand. */
283
+ export declare namespace flex {
284
+ /** Grow and shrink factors with a converted basis. */
285
+ type ReturnType = {
286
+ readonly flexBasis: number | string
287
+ readonly flexGrow: number
288
+ readonly flexShrink: number
289
+ }
290
+ }
291
+
221
292
  /** Converts a portable length to logical units. */
222
293
  export function length(
223
294
  value: number | string,
@@ -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 = {
@@ -0,0 +1,77 @@
1
+ /** Resolves compiled style applications for consumers outside native view bindings. @module */
2
+ import * as NativeContext from '../../runtime/NativeContext.js'
3
+ import * as React from 'react'
4
+ import type { useStyles } from '../react.js'
5
+ import type * as Store from './Store.js'
6
+
7
+ /** Subscribes to a selection and returns its style resolvers. */
8
+ export function use(
9
+ store: ReturnType<typeof Store.create>,
10
+ ): useStyles.ReturnType {
11
+ const value = React.useSyncExternalStore(
12
+ store.subscribe,
13
+ store.getSnapshot,
14
+ store.getSnapshot,
15
+ )
16
+ return React.useMemo(
17
+ () => ({
18
+ props: (props) =>
19
+ props && Object.hasOwn(props, 'style')
20
+ ? { ...props, style: NativeContext.resolve(props.style, value) }
21
+ : props,
22
+ style: <style>(style: style, input?: unknown) =>
23
+ NativeContext.resolve(style, value, input) as useStyles.Style<style>,
24
+ }),
25
+ [value],
26
+ )
27
+ }
28
+
29
+ /**
30
+ * Wraps a component so its style props resolve through an entrypoint's selection hook.
31
+ * @throws For invalid style prop names.
32
+ */
33
+ export function wrap<const component extends React.ElementType>(
34
+ Component: component,
35
+ options: wrap.Options,
36
+ useStyles: () => useStyles.ReturnType,
37
+ ) {
38
+ const additional = options.styleProps ?? []
39
+ if (
40
+ !Array.isArray(additional) ||
41
+ additional.some(
42
+ (name) =>
43
+ typeof name !== 'string' || !name || name === 'key' || name === 'ref',
44
+ )
45
+ )
46
+ throw new Error(
47
+ 'withStyles requires style prop names excluding key and ref.',
48
+ )
49
+
50
+ const names = [...new Set(['style', 'contentContainerStyle', ...additional])]
51
+ const Wrapped = React.forwardRef<
52
+ React.ComponentRef<component>,
53
+ React.ComponentPropsWithoutRef<component>
54
+ >((props, ref) => {
55
+ const selected = useStyles()
56
+ const resolved: Record<string, unknown> = { ...props, ref }
57
+ for (const name of names)
58
+ if (Object.hasOwn(props, name))
59
+ resolved[name] = selected.style(
60
+ (props as Record<string, unknown>)[name],
61
+ )
62
+
63
+ return React.createElement(Component, resolved)
64
+ })
65
+ Wrapped.displayName = `withStyles(${typeof Component === 'string' ? Component : Component.displayName || Component.name || 'Component'})`
66
+
67
+ return Wrapped
68
+ }
69
+
70
+ /** Style-bearing prop names accepted by a wrapper. */
71
+ export declare namespace wrap {
72
+ /** Entrypoints narrow these names to the wrapped component's props. */
73
+ type Options = {
74
+ /** Additional style props declared by the wrapped component. */
75
+ readonly styleProps?: readonly string[] | undefined
76
+ }
77
+ }
@@ -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' })