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.
- package/README.md +6 -6
- package/dist/internal/Configuration.d.ts.map +1 -1
- package/dist/internal/Configuration.js +2 -1
- package/dist/internal/Configuration.js.map +1 -1
- package/dist/node/Host.d.ts.map +1 -1
- package/dist/node/Host.js +40 -12
- package/dist/node/Host.js.map +1 -1
- package/dist/react-native/internal/Device.js +90 -37
- package/dist/react-native/internal/Device.js.map +3 -3
- package/dist/react-native/internal/NativeZyzz.js.map +2 -2
- package/dist/react-native/internal/Store.d.ts +4 -0
- package/dist/react-native/internal/Store.d.ts.map +1 -1
- package/dist/react-native/internal/Store.js.map +1 -1
- package/dist/react-native/internal/Viewport.d.ts +6 -0
- package/dist/react-native/internal/Viewport.d.ts.map +1 -1
- package/dist/react-native/internal/Viewport.js +2 -0
- package/dist/react-native/internal/Viewport.js.map +1 -1
- package/dist/react-native/react.d.ts +5 -2
- package/dist/react-native/react.d.ts.map +1 -1
- package/dist/react-native/react.js +60 -8
- package/dist/react-native/react.js.map +1 -1
- package/dist/runtime/NativeContext.d.ts +6 -0
- package/dist/runtime/NativeContext.d.ts.map +1 -1
- package/dist/runtime/NativeContext.js +62 -9
- package/dist/runtime/NativeContext.js.map +1 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +4 -1
- package/dist/unplugin/index.js.map +1 -1
- package/dist/variants.d.ts +3 -2
- package/dist/variants.d.ts.map +1 -1
- package/dist/variants.js.map +1 -1
- package/native/NativeZyzz.cpp +52 -65
- package/native/NativeZyzz.h +0 -4
- package/native/ZyzzSpecJSI.h +0 -10
- package/native/android/ZyzzSpec-generated.cpp +0 -6
- package/package.json +1 -1
- package/src/internal/Configuration.ts +2 -1
- package/src/node/Host.ts +64 -17
- package/src/react-native/internal/Device.ts +129 -48
- package/src/react-native/internal/NativeZyzz.ts +4 -6
- package/src/react-native/internal/Store.ts +4 -0
- package/src/react-native/internal/Viewport.ts +7 -0
- package/src/react-native/react.ts +81 -11
- package/src/runtime/NativeContext.ts +83 -9
- package/src/unplugin/index.ts +4 -1
- 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<
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
/**
|
|
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('))
|
|
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 (
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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(
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
212
|
-
if (!selective(
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
43
|
+
get appearance() {
|
|
44
|
+
return create().appearance
|
|
45
|
+
},
|
|
34
46
|
Provider: BoundProvider,
|
|
35
|
-
|
|
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' &&
|
|
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
|
|
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({
|
|
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.
|
|
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({
|
|
159
|
+
store.update({ adaptive, colorScheme, set: props.vars, viewport })
|
|
93
160
|
publish()
|
|
94
|
-
}, [
|
|
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
|
-
/**
|
|
106
|
-
|
|
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
|
}
|