elit 3.5.6 → 3.5.7

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 (113) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +1 -1
  3. package/desktop/build.rs +83 -0
  4. package/desktop/icon.rs +106 -0
  5. package/desktop/lib.rs +2 -0
  6. package/desktop/main.rs +235 -0
  7. package/desktop/native_main.rs +128 -0
  8. package/desktop/native_renderer/action_widgets.rs +184 -0
  9. package/desktop/native_renderer/app_models.rs +171 -0
  10. package/desktop/native_renderer/app_runtime.rs +140 -0
  11. package/desktop/native_renderer/container_rendering.rs +610 -0
  12. package/desktop/native_renderer/content_widgets.rs +634 -0
  13. package/desktop/native_renderer/css_models.rs +371 -0
  14. package/desktop/native_renderer/embedded_surfaces.rs +414 -0
  15. package/desktop/native_renderer/form_controls.rs +516 -0
  16. package/desktop/native_renderer/interaction_dispatch.rs +89 -0
  17. package/desktop/native_renderer/runtime_support.rs +135 -0
  18. package/desktop/native_renderer/utilities.rs +495 -0
  19. package/desktop/native_renderer/vector_drawing.rs +491 -0
  20. package/desktop/native_renderer.rs +4122 -0
  21. package/desktop/runtime/external.rs +422 -0
  22. package/desktop/runtime/mod.rs +67 -0
  23. package/desktop/runtime/quickjs.rs +106 -0
  24. package/desktop/window.rs +383 -0
  25. package/package.json +6 -3
  26. package/dist/build.d.mts +0 -20
  27. package/dist/chokidar.d.mts +0 -134
  28. package/dist/cli.d.mts +0 -81
  29. package/dist/config.d.mts +0 -254
  30. package/dist/coverage.d.mts +0 -85
  31. package/dist/database.d.mts +0 -52
  32. package/dist/desktop.d.mts +0 -68
  33. package/dist/dom.d.mts +0 -87
  34. package/dist/el.d.mts +0 -208
  35. package/dist/fs.d.mts +0 -255
  36. package/dist/hmr.d.mts +0 -38
  37. package/dist/http.d.mts +0 -169
  38. package/dist/https.d.mts +0 -108
  39. package/dist/index.d.mts +0 -13
  40. package/dist/mime-types.d.mts +0 -48
  41. package/dist/native.d.mts +0 -136
  42. package/dist/path.d.mts +0 -163
  43. package/dist/router.d.mts +0 -49
  44. package/dist/runtime.d.mts +0 -97
  45. package/dist/server-D0Dp4R5z.d.mts +0 -449
  46. package/dist/server.d.mts +0 -7
  47. package/dist/state.d.mts +0 -117
  48. package/dist/style.d.mts +0 -232
  49. package/dist/test-reporter.d.mts +0 -77
  50. package/dist/test-runtime.d.mts +0 -122
  51. package/dist/test.d.mts +0 -39
  52. package/dist/types.d.mts +0 -586
  53. package/dist/universal.d.mts +0 -21
  54. package/dist/ws.d.mts +0 -200
  55. package/dist/wss.d.mts +0 -108
  56. package/src/build.ts +0 -362
  57. package/src/chokidar.ts +0 -427
  58. package/src/cli.ts +0 -1162
  59. package/src/config.ts +0 -509
  60. package/src/coverage.ts +0 -1479
  61. package/src/database.ts +0 -1410
  62. package/src/desktop-auto-render.ts +0 -317
  63. package/src/desktop-cli.ts +0 -1533
  64. package/src/desktop.ts +0 -99
  65. package/src/dev-build.ts +0 -340
  66. package/src/dom.ts +0 -901
  67. package/src/el.ts +0 -183
  68. package/src/fs.ts +0 -609
  69. package/src/hmr.ts +0 -149
  70. package/src/http.ts +0 -856
  71. package/src/https.ts +0 -411
  72. package/src/index.ts +0 -16
  73. package/src/mime-types.ts +0 -222
  74. package/src/mobile-cli.ts +0 -2313
  75. package/src/native-background.ts +0 -444
  76. package/src/native-border.ts +0 -343
  77. package/src/native-canvas.ts +0 -260
  78. package/src/native-cli.ts +0 -414
  79. package/src/native-color.ts +0 -904
  80. package/src/native-estimation.ts +0 -194
  81. package/src/native-grid.ts +0 -590
  82. package/src/native-interaction.ts +0 -1289
  83. package/src/native-layout.ts +0 -568
  84. package/src/native-link.ts +0 -76
  85. package/src/native-render-support.ts +0 -361
  86. package/src/native-spacing.ts +0 -231
  87. package/src/native-state.ts +0 -318
  88. package/src/native-strings.ts +0 -46
  89. package/src/native-transform.ts +0 -120
  90. package/src/native-types.ts +0 -439
  91. package/src/native-typography.ts +0 -254
  92. package/src/native-units.ts +0 -441
  93. package/src/native-vector.ts +0 -910
  94. package/src/native.ts +0 -5606
  95. package/src/path.ts +0 -493
  96. package/src/pm-cli.ts +0 -2498
  97. package/src/preview-build.ts +0 -294
  98. package/src/render-context.ts +0 -138
  99. package/src/router.ts +0 -260
  100. package/src/runtime.ts +0 -97
  101. package/src/server.ts +0 -2294
  102. package/src/state.ts +0 -556
  103. package/src/style.ts +0 -1790
  104. package/src/test-globals.d.ts +0 -184
  105. package/src/test-reporter.ts +0 -609
  106. package/src/test-runtime.ts +0 -1359
  107. package/src/test.ts +0 -368
  108. package/src/types.ts +0 -381
  109. package/src/universal.ts +0 -81
  110. package/src/wapk-cli.ts +0 -3213
  111. package/src/workspace-package.ts +0 -102
  112. package/src/ws.ts +0 -648
  113. package/src/wss.ts +0 -241
@@ -1,1289 +0,0 @@
1
- import { type NativeStyleResolveOptions } from './style';
2
- import { getNativeStyleResolveOptions, parsePlainNumericValue } from './native-units';
3
- import { parseCssColor, toComposeColorLiteral } from './native-color';
4
- import { isExternalDestination, resolveNativeLinkHint } from './native-link';
5
- import { flattenTextContent, quoteKotlinString, quoteSwiftString } from './native-strings';
6
- import { prependComposeModifierCall } from './native-canvas';
7
- import { buildComposeTextStyleLiteralFromStyle } from './native-typography';
8
- import type {
9
- NativeBindingReference,
10
- NativeControlEventExpressionOptions,
11
- NativeElementNode,
12
- NativeNode,
13
- NativePickerOption,
14
- NativePropValue,
15
- } from './native-types';
16
-
17
- const NATIVE_PATTERN_MAX_LENGTH = 500;
18
- const REDOS_NESTED_QUANTIFIER = /\([^()]*[+*][^()]*\)[+*]/;
19
- const IMAGE_FALLBACK_STOP_WORDS = new Set([
20
- 'image',
21
- 'icon',
22
- 'public',
23
- 'assets',
24
- 'asset',
25
- 'favicon',
26
- 'svg',
27
- 'png',
28
- 'jpg',
29
- 'jpeg',
30
- 'webp',
31
- ]);
32
-
33
- function resolveNativeInputTypeValue(sourceTag: string, props: Record<string, NativePropValue>): string | undefined {
34
- if (sourceTag === 'textarea') {
35
- return 'textarea';
36
- }
37
-
38
- if (sourceTag !== 'input') {
39
- return undefined;
40
- }
41
-
42
- return typeof props.type === 'string' && props.type.trim()
43
- ? props.type.trim().toLowerCase()
44
- : 'text';
45
- }
46
-
47
- export function isCheckboxInput(sourceTag: string, props: Record<string, NativePropValue>): boolean {
48
- return resolveNativeInputTypeValue(sourceTag, props) === 'checkbox';
49
- }
50
-
51
- export function isRangeInput(sourceTag: string, props: Record<string, NativePropValue>): boolean {
52
- return resolveNativeInputTypeValue(sourceTag, props) === 'range';
53
- }
54
-
55
- export function toNativeBoolean(value: NativePropValue | undefined): boolean {
56
- if (typeof value === 'boolean') return value;
57
- if (typeof value === 'number') return value !== 0;
58
- if (typeof value === 'string') {
59
- const normalized = value.trim().toLowerCase();
60
- return normalized === 'true' || normalized === '1' || normalized === 'on' || normalized === 'yes';
61
- }
62
- return false;
63
- }
64
-
65
- export function buildComposeButtonModifier(
66
- modifier: string,
67
- onClickExpression?: string,
68
- enabled = true,
69
- interactionSourceName?: string,
70
- ): string {
71
- if (!onClickExpression || !enabled) {
72
- return modifier;
73
- }
74
-
75
- return prependComposeModifierCall(
76
- modifier,
77
- interactionSourceName
78
- ? `clickable(interactionSource = ${interactionSourceName}, indication = LocalIndication.current) { ${onClickExpression} }`
79
- : `clickable { ${onClickExpression} }`,
80
- );
81
- }
82
-
83
- export function buildComposeTextInputArgsFromStyle(
84
- node: NativeElementNode,
85
- style: Record<string, NativePropValue> | undefined,
86
- submitActionExpression?: string,
87
- styleResolveOptions: NativeStyleResolveOptions = getNativeStyleResolveOptions('generic'),
88
- ): string[] {
89
- const args: string[] = [];
90
- if (style) {
91
- const textStyle = buildComposeTextStyleLiteralFromStyle(style, styleResolveOptions);
92
- if (textStyle) {
93
- args.push(`textStyle = ${textStyle}`);
94
- }
95
-
96
- const color = parseCssColor(style.color);
97
- if (color) {
98
- args.push(`cursorBrush = SolidColor(${toComposeColorLiteral(color)})`);
99
- }
100
- }
101
-
102
- if (isNativeDisabled(node)) {
103
- args.push('enabled = false');
104
- }
105
-
106
- if (isNativeReadOnly(node)) {
107
- args.push('readOnly = true');
108
- }
109
-
110
- const keyboardType = resolveComposeKeyboardType(node);
111
- if (keyboardType || submitActionExpression) {
112
- const keyboardArgs: string[] = [];
113
- if (keyboardType) {
114
- keyboardArgs.push(`keyboardType = ${keyboardType}`);
115
- }
116
- if (submitActionExpression) {
117
- keyboardArgs.push('imeAction = androidx.compose.ui.text.input.ImeAction.Done');
118
- }
119
- args.push(`keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(${keyboardArgs.join(', ')})`);
120
- }
121
-
122
- if (submitActionExpression) {
123
- args.push(`keyboardActions = androidx.compose.foundation.text.KeyboardActions(onDone = { ${submitActionExpression} })`);
124
- }
125
-
126
- if (resolveNativeTextInputType(node) === 'password') {
127
- args.push('visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation()');
128
- }
129
-
130
- args.push(`singleLine = ${node.sourceTag === 'textarea' ? 'false' : 'true'}`);
131
- if (node.sourceTag === 'textarea') {
132
- args.push('minLines = 4');
133
- }
134
-
135
- return args;
136
- }
137
-
138
- export function buildSwiftUIButtonModifiersFromStyle(
139
- node: NativeElementNode,
140
- modifiers: string[],
141
- style: Record<string, NativePropValue> | undefined,
142
- ): string[] {
143
- const interactiveModifiers = [
144
- ...(node.sourceTag === 'button' && isNativeDisabled(node) ? ['.disabled(true)'] : []),
145
- ...modifiers,
146
- ];
147
- return style ? ['.buttonStyle(.plain)', ...interactiveModifiers] : interactiveModifiers;
148
- }
149
-
150
- export function isNativeDisabled(node: NativeElementNode): boolean {
151
- return toNativeBoolean(node.props.disabled) || toNativeBoolean(node.props['aria-disabled']);
152
- }
153
-
154
- export function isNativeFormControl(node: NativeElementNode): boolean {
155
- return node.component === 'TextInput' || node.component === 'Toggle' || node.component === 'Picker' || node.component === 'Slider';
156
- }
157
-
158
- export function isNativeEnabled(node: NativeElementNode): boolean {
159
- return (node.component === 'Button' || isNativeFormControl(node)) && !isNativeDisabled(node);
160
- }
161
-
162
- export function isNativeChecked(node: NativeElementNode): boolean {
163
- return toNativeBoolean(node.props.checked) || toNativeBoolean(node.props['aria-checked']);
164
- }
165
-
166
- export function isNativeSelected(node: NativeElementNode): boolean {
167
- return toNativeBoolean(node.props.selected)
168
- || toNativeBoolean(node.props['aria-selected'])
169
- || typeof node.props['aria-current'] === 'string';
170
- }
171
-
172
- export function hasNativePressedAccessibilityState(node: NativeElementNode): boolean {
173
- return node.props['aria-pressed'] !== undefined;
174
- }
175
-
176
- export function isNativePressed(node: NativeElementNode): boolean {
177
- return toNativeBoolean(node.props['aria-pressed'])
178
- || toNativeBoolean(node.props.pressed)
179
- || toNativeBoolean(node.props.active);
180
- }
181
-
182
- export function isNativeActive(node: NativeElementNode): boolean {
183
- return !isNativeDisabled(node) && isNativePressed(node);
184
- }
185
-
186
- export function isNativeRequired(node: NativeElementNode): boolean {
187
- return toNativeBoolean(node.props.required) || toNativeBoolean(node.props['aria-required']);
188
- }
189
-
190
- export function isNativeMultiple(node: NativeElementNode): boolean {
191
- return node.component === 'Picker' && toNativeBoolean(node.props.multiple);
192
- }
193
-
194
- function canNativeParticipateInValidation(node: NativeElementNode): boolean {
195
- return isNativeFormControl(node) && !isNativeDisabled(node);
196
- }
197
-
198
- export function resolveNativeTextInputValue(node: NativeElementNode): string {
199
- return typeof node.props.value === 'string' || typeof node.props.value === 'number'
200
- ? String(node.props.value)
201
- : '';
202
- }
203
-
204
- function parseNativeNonNegativeIntegerConstraint(value: NativePropValue | undefined): number | undefined {
205
- const parsed = parsePlainNumericValue(value);
206
- return parsed !== undefined && Number.isInteger(parsed) && parsed >= 0
207
- ? parsed
208
- : undefined;
209
- }
210
-
211
- function resolveNativeTextInputMinLength(node: NativeElementNode): number | undefined {
212
- return parseNativeNonNegativeIntegerConstraint(node.props.minLength ?? node.props.minlength);
213
- }
214
-
215
- function resolveNativeTextInputMaxLength(node: NativeElementNode): number | undefined {
216
- return parseNativeNonNegativeIntegerConstraint(node.props.maxLength ?? node.props.maxlength);
217
- }
218
-
219
- function resolveNativePatternExpression(node: NativeElementNode): RegExp | undefined {
220
- if (node.component !== 'TextInput' || typeof node.props.pattern !== 'string' || !node.props.pattern.trim()) {
221
- return undefined;
222
- }
223
-
224
- const pattern = node.props.pattern.trim();
225
- if (pattern.length > NATIVE_PATTERN_MAX_LENGTH || REDOS_NESTED_QUANTIFIER.test(pattern)) {
226
- return undefined;
227
- }
228
-
229
- try {
230
- return new RegExp(`^(?:${pattern})$`);
231
- } catch {
232
- return undefined;
233
- }
234
- }
235
-
236
- function resolveNativeNumericConstraint(value: NativePropValue | undefined): number | undefined {
237
- return parsePlainNumericValue(value);
238
- }
239
-
240
- export function resolveNativeStepConstraint(node: NativeElementNode): number | undefined {
241
- if (node.props.step === undefined) {
242
- return undefined;
243
- }
244
-
245
- if (typeof node.props.step === 'string' && node.props.step.trim().toLowerCase() === 'any') {
246
- return undefined;
247
- }
248
-
249
- const parsed = resolveNativeNumericConstraint(node.props.step);
250
- return parsed !== undefined && parsed > 0 ? parsed : undefined;
251
- }
252
-
253
- export function resolveNativeTextInputType(node: NativeElementNode): 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search' | 'textarea' {
254
- const inputType = resolveNativeInputTypeValue(node.sourceTag, node.props);
255
-
256
- switch (inputType) {
257
- case 'password':
258
- case 'email':
259
- case 'number':
260
- case 'tel':
261
- case 'url':
262
- case 'search':
263
- return inputType;
264
- default:
265
- return 'text';
266
- }
267
- }
268
-
269
- function supportsNativePatternValidation(node: NativeElementNode): boolean {
270
- switch (resolveNativeTextInputType(node)) {
271
- case 'text':
272
- case 'password':
273
- case 'email':
274
- case 'tel':
275
- case 'url':
276
- case 'search':
277
- return true;
278
- default:
279
- return false;
280
- }
281
- }
282
-
283
- function isNativeEmailValue(value: string): boolean {
284
- return /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value);
285
- }
286
-
287
- function isNativeUrlValue(value: string): boolean {
288
- try {
289
- const parsed = new URL(value);
290
- return Boolean(parsed.protocol && parsed.hostname);
291
- } catch {
292
- return false;
293
- }
294
- }
295
-
296
- export function hasNativeValidationConstraint(node: NativeElementNode): boolean {
297
- if (node.props['aria-invalid'] !== undefined) {
298
- return true;
299
- }
300
-
301
- if (node.component === 'TextInput') {
302
- return isNativeRequired(node)
303
- || resolveNativeTextInputMinLength(node) !== undefined
304
- || resolveNativeTextInputMaxLength(node) !== undefined
305
- || resolveNativePatternExpression(node) !== undefined
306
- || resolveNativeNumericConstraint(node.props.min) !== undefined
307
- || resolveNativeNumericConstraint(node.props.max) !== undefined
308
- || resolveNativeStepConstraint(node) !== undefined
309
- || resolveNativeTextInputType(node) === 'email'
310
- || resolveNativeTextInputType(node) === 'url'
311
- || resolveNativeTextInputType(node) === 'number';
312
- }
313
-
314
- return isNativeRequired(node);
315
- }
316
-
317
- function isNativeTextInputConstraintInvalid(node: NativeElementNode): boolean {
318
- const value = resolveNativeTextInputValue(node);
319
- const trimmedValue = value.trim();
320
-
321
- if (isNativeRequired(node) && trimmedValue.length === 0) {
322
- return true;
323
- }
324
-
325
- if (trimmedValue.length === 0) {
326
- return false;
327
- }
328
-
329
- const inputType = resolveNativeTextInputType(node);
330
- if (inputType === 'email' && !isNativeEmailValue(trimmedValue)) {
331
- return true;
332
- }
333
-
334
- if (inputType === 'url' && !isNativeUrlValue(trimmedValue)) {
335
- return true;
336
- }
337
-
338
- const minLength = resolveNativeTextInputMinLength(node);
339
- if (minLength !== undefined && value.length < minLength) {
340
- return true;
341
- }
342
-
343
- const maxLength = resolveNativeTextInputMaxLength(node);
344
- if (maxLength !== undefined && value.length > maxLength) {
345
- return true;
346
- }
347
-
348
- const patternExpression = supportsNativePatternValidation(node)
349
- ? resolveNativePatternExpression(node)
350
- : undefined;
351
- if (patternExpression && !patternExpression.test(value)) {
352
- return true;
353
- }
354
-
355
- if (inputType === 'number') {
356
- const numericValue = Number(trimmedValue);
357
- if (!Number.isFinite(numericValue)) {
358
- return true;
359
- }
360
-
361
- const min = resolveNativeNumericConstraint(node.props.min);
362
- if (min !== undefined && numericValue < min) {
363
- return true;
364
- }
365
-
366
- const max = resolveNativeNumericConstraint(node.props.max);
367
- if (max !== undefined && numericValue > max) {
368
- return true;
369
- }
370
-
371
- const step = resolveNativeStepConstraint(node);
372
- if (step !== undefined) {
373
- const stepBase = resolveNativeNumericConstraint(node.props.min) ?? 0;
374
- const steps = (numericValue - stepBase) / step;
375
- if (Math.abs(steps - Math.round(steps)) > 1e-7) {
376
- return true;
377
- }
378
- }
379
- }
380
-
381
- return false;
382
- }
383
-
384
- export function isNativePlaceholderShown(node: NativeElementNode): boolean {
385
- return node.component === 'TextInput'
386
- && typeof node.props.placeholder === 'string'
387
- && node.props.placeholder.length > 0
388
- && resolveNativeTextInputValue(node).length === 0;
389
- }
390
-
391
- export function isNativeReadOnlyState(node: NativeElementNode): boolean {
392
- return node.component === 'TextInput' && (isNativeReadOnly(node) || isNativeDisabled(node));
393
- }
394
-
395
- export function isNativeReadWrite(node: NativeElementNode): boolean {
396
- return node.component === 'TextInput' && !isNativeReadOnlyState(node);
397
- }
398
-
399
- export function isNativeElementEmpty(node: NativeElementNode): boolean {
400
- return node.children.every((child) => child.kind === 'text' && child.value.length === 0);
401
- }
402
-
403
- export function isNativeFocusWithin(node: NativeElementNode): boolean {
404
- if (isNativePseudoFocused(node)) {
405
- return true;
406
- }
407
-
408
- return node.children.some((child) => child.kind === 'element' && isNativeFocusWithin(child));
409
- }
410
-
411
- function isNativeAriaInvalid(node: NativeElementNode): boolean {
412
- const value = node.props['aria-invalid'];
413
- if (value === undefined || value === null || value === false) {
414
- return false;
415
- }
416
-
417
- if (typeof value === 'string') {
418
- const normalized = value.trim().toLowerCase();
419
- return normalized.length > 0 && normalized !== 'false';
420
- }
421
-
422
- return toNativeBoolean(value);
423
- }
424
-
425
- export function isNativeInvalid(node: NativeElementNode): boolean {
426
- if (isNativeAriaInvalid(node)) {
427
- return true;
428
- }
429
-
430
- if (!isNativeFormControl(node)) {
431
- return false;
432
- }
433
-
434
- if (!canNativeParticipateInValidation(node)) {
435
- return false;
436
- }
437
-
438
- if (node.component === 'TextInput') {
439
- return isNativeTextInputConstraintInvalid(node);
440
- }
441
-
442
- if (!isNativeRequired(node)) {
443
- return false;
444
- }
445
-
446
- if (node.component === 'Toggle') {
447
- return !isNativeChecked(node);
448
- }
449
-
450
- if (node.component === 'Picker') {
451
- const options = resolveNativePickerOptions(node);
452
- return isNativeMultiple(node)
453
- ? resolveNativePickerInitialSelections(node, options).length === 0
454
- : resolveNativePickerInitialSelection(node, options).trim().length === 0;
455
- }
456
-
457
- return false;
458
- }
459
-
460
- export function isNativeValid(node: NativeElementNode): boolean {
461
- return canNativeParticipateInValidation(node) && !isNativeInvalid(node);
462
- }
463
-
464
- export function isNativeOptional(node: NativeElementNode): boolean {
465
- return isNativeFormControl(node) && !isNativeRequired(node);
466
- }
467
-
468
- export function isNativeReadOnly(node: NativeElementNode): boolean {
469
- return toNativeBoolean(node.props.readOnly) || toNativeBoolean(node.props.readonly);
470
- }
471
-
472
- function parseNativeTabIndex(node: NativeElementNode): number | undefined {
473
- const rawValue = node.props.tabIndex ?? node.props.tabindex;
474
- if (typeof rawValue === 'number' && Number.isInteger(rawValue)) {
475
- return rawValue;
476
- }
477
-
478
- if (typeof rawValue === 'string' && /^-?\d+$/.test(rawValue.trim())) {
479
- return Number(rawValue.trim());
480
- }
481
-
482
- return undefined;
483
- }
484
-
485
- function hasNativeExplicitFocusSignal(node: NativeElementNode): boolean {
486
- return toNativeBoolean(node.props.autoFocus)
487
- || toNativeBoolean(node.props.autofocus)
488
- || toNativeBoolean(node.props.focused)
489
- || toNativeBoolean(node.props['aria-focused']);
490
- }
491
-
492
- function isNativeFocusableRole(node: NativeElementNode): boolean {
493
- const role = typeof node.props.role === 'string'
494
- ? node.props.role.trim().toLowerCase()
495
- : undefined;
496
-
497
- return role === 'button'
498
- || role === 'link'
499
- || role === 'checkbox'
500
- || role === 'switch'
501
- || role === 'tab'
502
- || role === 'textbox'
503
- || role === 'combobox';
504
- }
505
-
506
- function isNativeFocusableElement(node: NativeElementNode): boolean {
507
- if (isNativeDisabled(node)) {
508
- return false;
509
- }
510
-
511
- const tabIndex = parseNativeTabIndex(node);
512
- if (tabIndex !== undefined) {
513
- return tabIndex >= 0;
514
- }
515
-
516
- if (toNativeBoolean(node.props.contentEditable) || toNativeBoolean(node.props.contenteditable)) {
517
- return true;
518
- }
519
-
520
- if (node.component === 'TextInput' || node.component === 'Button' || node.component === 'Link' || node.component === 'Toggle' || node.component === 'Picker' || node.component === 'Slider') {
521
- return true;
522
- }
523
-
524
- return isNativeFocusableRole(node);
525
- }
526
-
527
- export function isNativePseudoFocused(node: NativeElementNode): boolean {
528
- return hasNativeExplicitFocusSignal(node) && isNativeFocusableElement(node);
529
- }
530
-
531
- export function shouldNativeAutoFocus(node: NativeElementNode): boolean {
532
- return node.component === 'TextInput' && hasNativeExplicitFocusSignal(node) && !isNativeDisabled(node);
533
- }
534
-
535
- export function isNativeMuted(node: NativeElementNode): boolean {
536
- return toNativeBoolean(node.props.muted);
537
- }
538
-
539
- export function shouldNativeShowVideoControls(node: NativeElementNode): boolean {
540
- return node.sourceTag === 'video' && toNativeBoolean(node.props.controls);
541
- }
542
-
543
- export function resolveNativeVideoPoster(node: NativeElementNode): string | undefined {
544
- return node.sourceTag === 'video' && typeof node.props.poster === 'string' && node.props.poster.trim()
545
- ? node.props.poster.trim()
546
- : undefined;
547
- }
548
-
549
- export function shouldNativePlayInline(node: NativeElementNode): boolean {
550
- return node.sourceTag === 'video' && (
551
- toNativeBoolean(node.props.playsInline)
552
- || toNativeBoolean(node.props.playsinline)
553
- );
554
- }
555
-
556
- export function resolveNativeExplicitAccessibilityLabel(node: NativeElementNode): string | undefined {
557
- const explicitLabel = typeof node.props['aria-label'] === 'string' && node.props['aria-label'].trim()
558
- ? node.props['aria-label'].trim()
559
- : typeof node.props.title === 'string' && node.props.title.trim()
560
- ? node.props.title.trim()
561
- : undefined;
562
-
563
- if (explicitLabel) {
564
- return explicitLabel;
565
- }
566
-
567
- if (typeof node.props.alt === 'string' && node.props.alt.trim()) {
568
- return node.props.alt.trim();
569
- }
570
-
571
- return undefined;
572
- }
573
-
574
- export function resolveNativeMediaLabel(node: NativeElementNode): string {
575
- const explicitLabel = typeof node.props['aria-label'] === 'string' && node.props['aria-label'].trim()
576
- ? node.props['aria-label'].trim()
577
- : typeof node.props.title === 'string' && node.props.title.trim()
578
- ? node.props.title.trim()
579
- : undefined;
580
-
581
- if (explicitLabel) {
582
- return explicitLabel;
583
- }
584
-
585
- const textContent = flattenTextContent(node.children).trim();
586
- if (textContent) {
587
- return textContent;
588
- }
589
-
590
- return node.sourceTag === 'audio' ? 'Audio' : 'Video';
591
- }
592
-
593
- function tokenizeImageFallbackWords(value: string): string[] {
594
- return value
595
- .split(/[^a-zA-Z0-9]+/)
596
- .map((token) => token.trim())
597
- .filter((token) => token.length > 0)
598
- .filter((token) => !/^\d+$/.test(token))
599
- .filter((token) => !IMAGE_FALLBACK_STOP_WORDS.has(token.toLowerCase()));
600
- }
601
-
602
- export function resolveImageFallbackLabel(source: string, alt?: string): string {
603
- const altTokens = alt ? tokenizeImageFallbackWords(alt) : [];
604
- const sourceTokens = tokenizeImageFallbackWords(source.replace(/\.[a-z0-9]+$/i, ''));
605
- const tokens = altTokens.length > 0 ? altTokens : sourceTokens;
606
-
607
- if (tokens.length === 0) {
608
- return 'IMG';
609
- }
610
-
611
- if (tokens.length === 1) {
612
- return tokens[0]!.slice(0, 2).toUpperCase();
613
- }
614
-
615
- const initials = tokens
616
- .slice(0, 2)
617
- .map((token) => token[0]!.toUpperCase())
618
- .join('');
619
-
620
- return initials || 'IMG';
621
- }
622
-
623
- export function resolveNativeAccessibilityLabel(node: NativeElementNode): string | undefined {
624
- const explicitLabel = resolveNativeExplicitAccessibilityLabel(node);
625
-
626
- if (explicitLabel) {
627
- return explicitLabel;
628
- }
629
-
630
- if (node.component === 'Picker') {
631
- if (typeof node.props.placeholder === 'string' && node.props.placeholder.trim()) {
632
- return node.props.placeholder.trim();
633
- }
634
-
635
- return isNativeMultiple(node) ? 'Selection list' : 'Select';
636
- }
637
-
638
- const textContent = flattenTextContent(node.children).trim();
639
- if (textContent) {
640
- return textContent;
641
- }
642
-
643
- if (typeof node.props.placeholder === 'string' && node.props.placeholder.trim()) {
644
- return node.props.placeholder.trim();
645
- }
646
-
647
- if (node.component === 'Media') {
648
- return resolveNativeMediaLabel(node);
649
- }
650
-
651
- if (node.component === 'WebView') {
652
- return 'Web content';
653
- }
654
-
655
- return undefined;
656
- }
657
-
658
- export function resolveNativeAccessibilityHint(node: NativeElementNode): string | undefined {
659
- const parts: string[] = [];
660
-
661
- if (typeof node.props['aria-description'] === 'string' && node.props['aria-description'].trim()) {
662
- parts.push(node.props['aria-description'].trim());
663
- }
664
-
665
- const linkHint = resolveNativeLinkHint(node);
666
- if (linkHint) {
667
- parts.push(linkHint);
668
- }
669
-
670
- return parts.length > 0 ? parts.join(', ') : undefined;
671
- }
672
-
673
- export function resolveNativeAccessibilityRole(node: NativeElementNode): 'button' | 'link' | 'checkbox' | 'switch' | 'tab' | 'image' | 'heading' | undefined {
674
- const explicitRole = typeof node.props.role === 'string'
675
- ? node.props.role.trim().toLowerCase()
676
- : undefined;
677
-
678
- switch (explicitRole) {
679
- case 'button':
680
- case 'link':
681
- case 'checkbox':
682
- case 'switch':
683
- case 'tab':
684
- case 'image':
685
- case 'heading':
686
- return explicitRole;
687
- case 'img':
688
- return 'image';
689
- default:
690
- break;
691
- }
692
-
693
- return undefined;
694
- }
695
-
696
- export function hasExplicitNativeAccessibilitySignal(node: NativeElementNode): boolean {
697
- return Boolean(
698
- resolveNativeExplicitAccessibilityLabel(node)
699
- || resolveNativeAccessibilityHint(node)
700
- || resolveNativeLinkHint(node)
701
- || (typeof node.props.role === 'string' && node.props.role.trim())
702
- || node.props['aria-selected'] !== undefined
703
- || node.props['aria-checked'] !== undefined
704
- || node.props['aria-pressed'] !== undefined
705
- || node.props['aria-disabled'] !== undefined
706
- || node.props['aria-expanded'] !== undefined
707
- || node.props['aria-invalid'] !== undefined
708
- || node.props['aria-current'] !== undefined
709
- || node.props['aria-valuetext'] !== undefined
710
- || node.props['aria-required'] !== undefined
711
- || toNativeBoolean(node.props.required)
712
- || isNativeMultiple(node)
713
- );
714
- }
715
-
716
- export function shouldEmitNativeAccessibilityLabel(node: NativeElementNode): boolean {
717
- return hasExplicitNativeAccessibilitySignal(node);
718
- }
719
-
720
- export function resolveNativeAccessibilityStateParts(node: NativeElementNode): string[] {
721
- const parts: string[] = [];
722
- const role = resolveNativeAccessibilityRole(node);
723
- const hasSelectedState = node.props['aria-selected'] !== undefined || typeof node.props['aria-current'] === 'string' || role === 'tab';
724
- const hasCheckedState = node.component === 'Toggle' || node.props['aria-checked'] !== undefined || role === 'checkbox' || role === 'switch';
725
- const hasPressedState = hasNativePressedAccessibilityState(node);
726
-
727
- if (isNativeRequired(node)) {
728
- parts.push('Required');
729
- }
730
-
731
- if (isNativeInvalid(node)) {
732
- parts.push('Invalid');
733
- }
734
-
735
- if (!isNativeInvalid(node) && hasNativeValidationConstraint(node) && isNativeValid(node)) {
736
- parts.push('Valid');
737
- }
738
-
739
- if (isNativeDisabled(node)) {
740
- parts.push('Disabled');
741
- }
742
-
743
- if (hasSelectedState) {
744
- parts.push(isNativeSelected(node) ? 'Selected' : 'Not selected');
745
- }
746
-
747
- if (hasCheckedState) {
748
- parts.push(isNativeChecked(node) ? 'Checked' : 'Unchecked');
749
- }
750
-
751
- if (hasPressedState) {
752
- parts.push(isNativePressed(node) ? 'Pressed' : 'Not pressed');
753
- }
754
-
755
- if (node.props['aria-expanded'] !== undefined) {
756
- parts.push(toNativeBoolean(node.props['aria-expanded']) ? 'Expanded' : 'Collapsed');
757
- }
758
-
759
- if (typeof node.props['aria-valuetext'] === 'string' && node.props['aria-valuetext'].trim()) {
760
- parts.push(node.props['aria-valuetext'].trim());
761
- }
762
-
763
- return [...new Set(parts)];
764
- }
765
-
766
- function resolveComposeAccessibilityRoleExpression(node: NativeElementNode): string | undefined {
767
- switch (resolveNativeAccessibilityRole(node)) {
768
- case 'button':
769
- case 'link':
770
- return 'Role.Button';
771
- case 'checkbox':
772
- return 'Role.Checkbox';
773
- case 'switch':
774
- return 'Role.Switch';
775
- case 'tab':
776
- return 'Role.Tab';
777
- case 'image':
778
- return 'Role.Image';
779
- default:
780
- return undefined;
781
- }
782
- }
783
-
784
- export function buildComposeAccessibilityModifier(node: NativeElementNode): string | undefined {
785
- if (!hasExplicitNativeAccessibilitySignal(node)) {
786
- return undefined;
787
- }
788
-
789
- const statements: string[] = [];
790
- const label = shouldEmitNativeAccessibilityLabel(node) ? resolveNativeAccessibilityLabel(node) : undefined;
791
- const hint = resolveNativeAccessibilityHint(node);
792
- const stateParts = resolveNativeAccessibilityStateParts(node);
793
- const stateDescription = [hint, ...stateParts].filter((value): value is string => Boolean(value)).join(', ');
794
- const roleExpression = resolveComposeAccessibilityRoleExpression(node);
795
- const role = resolveNativeAccessibilityRole(node);
796
-
797
- if (label) {
798
- statements.push(`contentDescription = ${quoteKotlinString(label)}`);
799
- }
800
-
801
- if (roleExpression) {
802
- statements.push(`role = ${roleExpression}`);
803
- }
804
-
805
- if ((node.props['aria-selected'] !== undefined || typeof node.props['aria-current'] === 'string' || role === 'tab') && isNativeSelected(node)) {
806
- statements.push('selected = true');
807
- }
808
-
809
- if (stateDescription) {
810
- statements.push(`stateDescription = ${quoteKotlinString(stateDescription)}`);
811
- }
812
-
813
- if (role === 'heading') {
814
- statements.push('heading()');
815
- }
816
-
817
- if (node.props['aria-disabled'] !== undefined) {
818
- statements.push('disabled()');
819
- }
820
-
821
- return statements.length > 0
822
- ? `semantics(mergeDescendants = true) { ${statements.join('; ')} }`
823
- : undefined;
824
- }
825
-
826
- export function buildSwiftAccessibilityModifiers(node: NativeElementNode): string[] {
827
- if (!hasExplicitNativeAccessibilitySignal(node)) {
828
- return [];
829
- }
830
-
831
- const modifiers: string[] = [];
832
- const label = shouldEmitNativeAccessibilityLabel(node) ? resolveNativeAccessibilityLabel(node) : undefined;
833
- const hint = resolveNativeAccessibilityHint(node);
834
- const value = resolveNativeAccessibilityStateParts(node).join(', ');
835
- const role = resolveNativeAccessibilityRole(node);
836
-
837
- if (label) {
838
- modifiers.push(`.accessibilityLabel(${quoteSwiftString(label)})`);
839
- }
840
-
841
- if (hint) {
842
- modifiers.push(`.accessibilityHint(${quoteSwiftString(hint)})`);
843
- }
844
-
845
- if (value) {
846
- modifiers.push(`.accessibilityValue(${quoteSwiftString(value)})`);
847
- }
848
-
849
- if (role === 'button') {
850
- modifiers.push('.accessibilityAddTraits(.isButton)');
851
- } else if (role === 'link') {
852
- modifiers.push('.accessibilityAddTraits(.isLink)');
853
- } else if (role === 'image') {
854
- modifiers.push('.accessibilityAddTraits(.isImage)');
855
- } else if (role === 'heading') {
856
- modifiers.push('.accessibilityAddTraits(.isHeader)');
857
- }
858
-
859
- if ((node.props['aria-selected'] !== undefined || typeof node.props['aria-current'] === 'string' || role === 'tab') && isNativeSelected(node)) {
860
- modifiers.push('.accessibilityAddTraits(.isSelected)');
861
- }
862
-
863
- return modifiers;
864
- }
865
-
866
- export function resolveNativeRangeMin(node: NativeElementNode): number {
867
- return resolveNativeNumericConstraint(node.props.min) ?? 0;
868
- }
869
-
870
- export function resolveNativeRangeMax(node: NativeElementNode): number {
871
- const min = resolveNativeRangeMin(node);
872
- const max = resolveNativeNumericConstraint(node.props.max);
873
- return max !== undefined && max > min ? max : min + 100;
874
- }
875
-
876
- export function resolveNativeRangeInitialValue(node: NativeElementNode): number {
877
- const min = resolveNativeRangeMin(node);
878
- const max = resolveNativeRangeMax(node);
879
- const value = resolveNativeNumericConstraint(node.props.value);
880
- const candidate = value !== undefined ? value : min;
881
- return Math.min(max, Math.max(min, candidate));
882
- }
883
-
884
- export function resolveComposeSliderSteps(node: NativeElementNode): number | undefined {
885
- const step = resolveNativeStepConstraint(node);
886
- if (step === undefined) {
887
- return undefined;
888
- }
889
-
890
- const intervals = (resolveNativeRangeMax(node) - resolveNativeRangeMin(node)) / step;
891
- if (!Number.isFinite(intervals)) {
892
- return undefined;
893
- }
894
-
895
- const roundedIntervals = Math.round(intervals);
896
- if (roundedIntervals < 1 || Math.abs(intervals - roundedIntervals) > 1e-7) {
897
- return undefined;
898
- }
899
-
900
- return Math.max(0, roundedIntervals - 1);
901
- }
902
-
903
- export function resolveComposeKeyboardType(node: NativeElementNode): string | undefined {
904
- switch (resolveNativeTextInputType(node)) {
905
- case 'email':
906
- return 'androidx.compose.ui.text.input.KeyboardType.Email';
907
- case 'number':
908
- return 'androidx.compose.ui.text.input.KeyboardType.Decimal';
909
- case 'password':
910
- return 'androidx.compose.ui.text.input.KeyboardType.Password';
911
- case 'tel':
912
- return 'androidx.compose.ui.text.input.KeyboardType.Phone';
913
- case 'url':
914
- return 'androidx.compose.ui.text.input.KeyboardType.Uri';
915
- case 'search':
916
- return 'androidx.compose.ui.text.input.KeyboardType.Text';
917
- default:
918
- return undefined;
919
- }
920
- }
921
-
922
- export function resolveSwiftKeyboardTypeModifier(node: NativeElementNode): string | undefined {
923
- switch (resolveNativeTextInputType(node)) {
924
- case 'email':
925
- return '.keyboardType(.emailAddress)';
926
- case 'number':
927
- return '.keyboardType(.decimalPad)';
928
- case 'tel':
929
- return '.keyboardType(.phonePad)';
930
- case 'url':
931
- return '.keyboardType(.URL)';
932
- case 'search':
933
- return '.keyboardType(.webSearch)';
934
- default:
935
- return undefined;
936
- }
937
- }
938
-
939
- export function shouldDisableNativeTextCapitalization(node: NativeElementNode): boolean {
940
- const inputType = resolveNativeTextInputType(node);
941
- return inputType === 'email' || inputType === 'password' || inputType === 'url';
942
- }
943
-
944
- export function serializeNativePayload(value: NativePropValue | undefined): string | undefined {
945
- if (value === undefined) return undefined;
946
- if (typeof value === 'string') return value;
947
- return JSON.stringify(value);
948
- }
949
-
950
- export function resolveNativeAction(node: NativeElementNode): string | undefined {
951
- return typeof node.props.nativeAction === 'string' && node.props.nativeAction.trim()
952
- ? node.props.nativeAction
953
- : undefined;
954
- }
955
-
956
- export function resolveNativeRoute(node: NativeElementNode): string | undefined {
957
- if (typeof node.props.nativeRoute === 'string' && node.props.nativeRoute.trim()) {
958
- return node.props.nativeRoute;
959
- }
960
-
961
- const destination = typeof node.props.destination === 'string' ? node.props.destination : undefined;
962
- if (destination && !isExternalDestination(destination)) {
963
- return destination;
964
- }
965
-
966
- return undefined;
967
- }
968
-
969
- export function buildComposeBridgeInvocation(action?: string, route?: string, payloadJson?: string): string | undefined {
970
- const args: string[] = [];
971
-
972
- if (action) args.push(`action = ${quoteKotlinString(action)}`);
973
- if (route) args.push(`route = ${quoteKotlinString(route)}`);
974
- if (payloadJson) args.push(`payloadJson = ${quoteKotlinString(payloadJson)}`);
975
-
976
- return args.length > 0 ? `ElitNativeBridge.dispatch(${args.join(', ')})` : undefined;
977
- }
978
-
979
- export function buildSwiftBridgeInvocation(action?: string, route?: string, payloadJson?: string): string | undefined {
980
- const args: string[] = [];
981
-
982
- if (action) args.push(`action: ${quoteSwiftString(action)}`);
983
- if (route) args.push(`route: ${quoteSwiftString(route)}`);
984
- if (payloadJson) args.push(`payloadJson: ${quoteSwiftString(payloadJson)}`);
985
-
986
- return args.length > 0 ? `ElitNativeBridge.dispatch(${args.join(', ')})` : undefined;
987
- }
988
-
989
- function resolveNativeControlEventInputType(node: NativeElementNode): string | undefined {
990
- if (node.component === 'Picker') {
991
- return isNativeMultiple(node) ? 'select-multiple' : 'select-one';
992
- }
993
-
994
- if (node.component === 'Toggle') {
995
- return typeof node.props.type === 'string' && node.props.type.trim()
996
- ? node.props.type.trim().toLowerCase()
997
- : 'checkbox';
998
- }
999
-
1000
- if (node.component === 'Slider') {
1001
- return 'range';
1002
- }
1003
-
1004
- return resolveNativeInputTypeValue(node.sourceTag, node.props);
1005
- }
1006
-
1007
- function shouldDispatchNativeControlEvent(node: NativeElementNode, eventName: 'input' | 'change' | 'submit'): boolean {
1008
- if (!node.events.includes(eventName)) {
1009
- return false;
1010
- }
1011
-
1012
- return !(eventName === 'input' && getNativeBindingReference(node) && node.events.every((candidate) => candidate === 'input'));
1013
- }
1014
-
1015
- function resolveNativeControlEventAction(node: NativeElementNode, eventName: 'input' | 'change' | 'submit'): string {
1016
- return resolveNativeAction(node) ?? `elit.event.${eventName}`;
1017
- }
1018
-
1019
- function buildComposeControlEventPayloadInvocation(
1020
- node: NativeElementNode,
1021
- eventName: 'input' | 'change' | 'submit',
1022
- options: NativeControlEventExpressionOptions = {},
1023
- ): string {
1024
- const args = [
1025
- `event = ${quoteKotlinString(eventName)}`,
1026
- `sourceTag = ${quoteKotlinString(node.sourceTag)}`,
1027
- ];
1028
- const inputType = resolveNativeControlEventInputType(node);
1029
- const detailJson = serializeNativePayload(node.props.nativePayload);
1030
-
1031
- if (inputType) {
1032
- args.push(`inputType = ${quoteKotlinString(inputType)}`);
1033
- }
1034
- if (options.valueExpression) {
1035
- args.push(`value = ${options.valueExpression}`);
1036
- }
1037
- if (options.valuesExpression) {
1038
- args.push(`values = ${options.valuesExpression}`);
1039
- }
1040
- if (options.checkedExpression) {
1041
- args.push(`checked = ${options.checkedExpression}`);
1042
- }
1043
- if (detailJson) {
1044
- args.push(`detailJson = ${quoteKotlinString(detailJson)}`);
1045
- }
1046
-
1047
- return `ElitNativeBridge.controlEventPayload(${args.join(', ')})`;
1048
- }
1049
-
1050
- export function buildComposeControlEventDispatchInvocation(
1051
- node: NativeElementNode,
1052
- eventName: 'input' | 'change' | 'submit',
1053
- options: NativeControlEventExpressionOptions = {},
1054
- ): string {
1055
- return `ElitNativeBridge.dispatch(action = ${quoteKotlinString(resolveNativeControlEventAction(node, eventName))}, payloadJson = ${buildComposeControlEventPayloadInvocation(node, eventName, options)})`;
1056
- }
1057
-
1058
- export function buildComposeControlEventDispatchStatements(
1059
- node: NativeElementNode,
1060
- options: NativeControlEventExpressionOptions = {},
1061
- ): string[] {
1062
- const statements: string[] = [];
1063
- if (shouldDispatchNativeControlEvent(node, 'input')) {
1064
- statements.push(buildComposeControlEventDispatchInvocation(node, 'input', options));
1065
- }
1066
- if (shouldDispatchNativeControlEvent(node, 'change')) {
1067
- statements.push(buildComposeControlEventDispatchInvocation(node, 'change', options));
1068
- }
1069
- return statements;
1070
- }
1071
-
1072
- function buildSwiftControlEventPayloadInvocation(
1073
- node: NativeElementNode,
1074
- eventName: 'input' | 'change' | 'submit',
1075
- options: NativeControlEventExpressionOptions = {},
1076
- ): string {
1077
- const args = [
1078
- `event: ${quoteSwiftString(eventName)}`,
1079
- `sourceTag: ${quoteSwiftString(node.sourceTag)}`,
1080
- ];
1081
- const inputType = resolveNativeControlEventInputType(node);
1082
- const detailJson = serializeNativePayload(node.props.nativePayload);
1083
-
1084
- if (inputType) {
1085
- args.push(`inputType: ${quoteSwiftString(inputType)}`);
1086
- }
1087
- if (options.valueExpression) {
1088
- args.push(`value: ${options.valueExpression}`);
1089
- }
1090
- if (options.valuesExpression) {
1091
- args.push(`values: ${options.valuesExpression}`);
1092
- }
1093
- if (options.checkedExpression) {
1094
- args.push(`checked: ${options.checkedExpression}`);
1095
- }
1096
- if (detailJson) {
1097
- args.push(`detailJson: ${quoteSwiftString(detailJson)}`);
1098
- }
1099
-
1100
- return `ElitNativeBridge.controlEventPayload(${args.join(', ')})`;
1101
- }
1102
-
1103
- export function buildSwiftControlEventDispatchInvocation(
1104
- node: NativeElementNode,
1105
- eventName: 'input' | 'change' | 'submit',
1106
- options: NativeControlEventExpressionOptions = {},
1107
- ): string {
1108
- return `ElitNativeBridge.dispatch(action: ${quoteSwiftString(resolveNativeControlEventAction(node, eventName))}, payloadJson: ${buildSwiftControlEventPayloadInvocation(node, eventName, options)})`;
1109
- }
1110
-
1111
- export function buildSwiftControlEventDispatchStatements(
1112
- node: NativeElementNode,
1113
- options: NativeControlEventExpressionOptions = {},
1114
- ): string[] {
1115
- const statements: string[] = [];
1116
- if (shouldDispatchNativeControlEvent(node, 'input')) {
1117
- statements.push(buildSwiftControlEventDispatchInvocation(node, 'input', options));
1118
- }
1119
- if (shouldDispatchNativeControlEvent(node, 'change')) {
1120
- statements.push(buildSwiftControlEventDispatchInvocation(node, 'change', options));
1121
- }
1122
- return statements;
1123
- }
1124
-
1125
- export function getNativeBindingReference(node: NativeElementNode): NativeBindingReference | undefined {
1126
- const binding = node.props.nativeBinding;
1127
- if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
1128
- return undefined;
1129
- }
1130
-
1131
- const id = typeof binding.id === 'string' ? binding.id : undefined;
1132
- const kind = binding.kind === 'value' || binding.kind === 'checked' ? binding.kind : undefined;
1133
- const valueType = binding.valueType === 'boolean' || binding.valueType === 'number' || binding.valueType === 'string' || binding.valueType === 'string-array'
1134
- ? binding.valueType
1135
- : undefined;
1136
-
1137
- if (!id || !kind || !valueType) {
1138
- return undefined;
1139
- }
1140
-
1141
- return { id, kind, valueType };
1142
- }
1143
-
1144
- function collectNativePickerOptionNodes(nodes: NativeNode[]): NativeElementNode[] {
1145
- const options: NativeElementNode[] = [];
1146
-
1147
- for (const node of nodes) {
1148
- if (node.kind !== 'element') {
1149
- continue;
1150
- }
1151
-
1152
- if (node.component === 'Option') {
1153
- options.push(node);
1154
- continue;
1155
- }
1156
-
1157
- if (node.sourceTag === 'optgroup') {
1158
- options.push(...collectNativePickerOptionNodes(node.children));
1159
- }
1160
- }
1161
-
1162
- return options;
1163
- }
1164
-
1165
- export function resolveNativePickerOptionLabel(node: NativeElementNode): string {
1166
- if (typeof node.props.label === 'string' && node.props.label.trim()) {
1167
- return node.props.label;
1168
- }
1169
-
1170
- const textContent = flattenTextContent(node.children).trim();
1171
- if (textContent) {
1172
- return textContent;
1173
- }
1174
-
1175
- if (typeof node.props.value === 'string' || typeof node.props.value === 'number' || typeof node.props.value === 'boolean') {
1176
- return String(node.props.value);
1177
- }
1178
-
1179
- return 'Option';
1180
- }
1181
-
1182
- function resolveNativePickerOptionValue(node: NativeElementNode): string {
1183
- if (typeof node.props.value === 'string' || typeof node.props.value === 'number' || typeof node.props.value === 'boolean') {
1184
- return String(node.props.value);
1185
- }
1186
-
1187
- return resolveNativePickerOptionLabel(node);
1188
- }
1189
-
1190
- export function resolveNativePickerOptions(node: NativeElementNode): NativePickerOption[] {
1191
- return collectNativePickerOptionNodes(node.children).map((optionNode) => ({
1192
- label: resolveNativePickerOptionLabel(optionNode),
1193
- value: resolveNativePickerOptionValue(optionNode),
1194
- selected: isNativeSelected(optionNode),
1195
- disabled: isNativeDisabled(optionNode),
1196
- }));
1197
- }
1198
-
1199
- export function resolveNativePickerInitialSelection(node: NativeElementNode, options: NativePickerOption[]): string {
1200
- if (isNativeMultiple(node)) {
1201
- return resolveNativePickerInitialSelections(node, options)[0] ?? '';
1202
- }
1203
-
1204
- const explicitValue = typeof node.props.value === 'string' || typeof node.props.value === 'number' || typeof node.props.value === 'boolean'
1205
- ? String(node.props.value)
1206
- : undefined;
1207
-
1208
- if (explicitValue && options.some((option) => option.value === explicitValue)) {
1209
- return explicitValue;
1210
- }
1211
-
1212
- const selectedOption = options.find((option) => option.selected);
1213
-
1214
- if (selectedOption) {
1215
- return selectedOption.value;
1216
- }
1217
-
1218
- if (isNativeRequired(node)) {
1219
- return '';
1220
- }
1221
-
1222
- return options[0]?.value ?? '';
1223
- }
1224
-
1225
- export function resolveNativePickerInitialSelections(node: NativeElementNode, options: NativePickerOption[]): string[] {
1226
- if (Array.isArray(node.props.value)) {
1227
- const explicitValues = node.props.value
1228
- .filter((value): value is string | number | boolean => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
1229
- .map((value) => String(value));
1230
-
1231
- return explicitValues.filter((value, index) => explicitValues.indexOf(value) === index && options.some((option) => option.value === value));
1232
- }
1233
-
1234
- const explicitValue = typeof node.props.value === 'string' || typeof node.props.value === 'number' || typeof node.props.value === 'boolean'
1235
- ? String(node.props.value)
1236
- : undefined;
1237
-
1238
- if (explicitValue && options.some((option) => option.value === explicitValue)) {
1239
- return [explicitValue];
1240
- }
1241
-
1242
- return options
1243
- .filter((option) => option.selected)
1244
- .map((option) => option.value);
1245
- }
1246
-
1247
- export function resolveNativePickerDisplayLabel(value: string, options: NativePickerOption[]): string {
1248
- return options.find((option) => option.value === value)?.label ?? value;
1249
- }
1250
-
1251
- export function buildComposePickerLabelExpression(selectionExpression: string, options: NativePickerOption[], placeholder?: string): string {
1252
- const fallbackLabel = placeholder ? quoteKotlinString(placeholder) : undefined;
1253
-
1254
- if (options.length === 0 || options.every((option) => option.value === option.label)) {
1255
- return fallbackLabel
1256
- ? `if (${selectionExpression}.isEmpty()) ${fallbackLabel} else ${selectionExpression}`
1257
- : selectionExpression;
1258
- }
1259
-
1260
- const branches = options.map((option) => `${quoteKotlinString(option.value)} -> ${quoteKotlinString(option.label)}`).join('; ');
1261
- return fallbackLabel
1262
- ? `when (${selectionExpression}) { "" -> ${fallbackLabel}; ${branches}; else -> ${selectionExpression} }`
1263
- : `when (${selectionExpression}) { ${branches}; else -> ${selectionExpression} }`;
1264
- }
1265
-
1266
- export function resolveNativeProgressFraction(props: Record<string, NativePropValue>): number | undefined {
1267
- const value = parsePlainNumericValue(props.value);
1268
- if (value === undefined) {
1269
- return undefined;
1270
- }
1271
-
1272
- const max = parsePlainNumericValue(props.max);
1273
- const denominator = max !== undefined && max > 0 ? max : 1;
1274
- return Math.max(0, Math.min(1, value / denominator));
1275
- }
1276
-
1277
- export function resolveNativeSurfaceSource(node: NativeElementNode): string | undefined {
1278
- const source = typeof node.props.source === 'string' && node.props.source.trim()
1279
- ? node.props.source.trim()
1280
- : typeof node.props.src === 'string' && node.props.src.trim()
1281
- ? node.props.src.trim()
1282
- : typeof node.props.data === 'string' && node.props.data.trim()
1283
- ? node.props.data.trim()
1284
- : typeof node.props.destination === 'string' && node.props.destination.trim()
1285
- ? node.props.destination.trim()
1286
- : undefined;
1287
-
1288
- return source && source.length > 0 ? source : undefined;
1289
- }