craft-native 0.0.209 → 0.0.211
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/dist/android/templates/CraftNativeShell.kt.template +5 -1
- package/dist/android/templates/MainActivityNative.kt.template +11 -8
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +35 -12
- package/dist/index.cjs +14 -5
- package/dist/index.js +14 -5
- package/dist/ios/templates/CraftApp.swift +16 -11
- package/dist/ios/templates/CraftLiveActivityWidget.swift.template +32 -8
- package/dist/ios/templates/CraftNativeScreen.swift +20 -4
- package/dist/ios/templates/testing/DriverTests.swift.template +8 -2
- package/package.json +1 -1
|
@@ -389,6 +389,10 @@ internal object CraftNativePrelude {
|
|
|
389
389
|
routePrelude: String,
|
|
390
390
|
): String = """
|
|
391
391
|
$routePrelude
|
|
392
|
+
// JavaScriptSandbox has no DOM window, but browser-safe SDK bridges
|
|
393
|
+
// address the host through `window.craft`. Keep native screens
|
|
394
|
+
// compatible without pretending a document exists.
|
|
395
|
+
if (typeof globalThis.window === 'undefined') globalThis.window = globalThis;
|
|
392
396
|
(function() {
|
|
393
397
|
var forward = null;
|
|
394
398
|
var queued = [];
|
|
@@ -567,7 +571,7 @@ internal object CraftNativePrelude {
|
|
|
567
571
|
* The bar above a screen (back, title, the screen's buttons) and the tab bar
|
|
568
572
|
* below it, in plain Android views. A screen sets its bar with
|
|
569
573
|
* `craft.navigation.setOptions` (title, largeTitle, hidden, rightButtons,
|
|
570
|
-
* tabBarHidden); the tabs come from the app's `tabs`.
|
|
574
|
+
* tabAccessory, tabBarHidden); the tabs come from the app's `tabs`.
|
|
571
575
|
*/
|
|
572
576
|
internal class CraftNativeChrome(context: Context, private val tabs: List<CraftNativeTab>) : LinearLayout(context) {
|
|
573
577
|
val content = FrameLayout(context)
|
|
@@ -142,7 +142,7 @@ import kotlin.math.pow
|
|
|
142
142
|
|
|
143
143
|
private const val CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS = 30_000L
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
internal fun craftNativeOptionalString(value: Any?): String? = value
|
|
146
146
|
?.takeUnless { it == JSONObject.NULL }
|
|
147
147
|
?.toString()
|
|
148
148
|
?.takeUnless { it == "null" || it == "undefined" }
|
|
@@ -718,12 +718,15 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
718
718
|
val explicitRegion = region(pendingProps.optJSONObject("region"))
|
|
719
719
|
val initialRegion = region(pendingProps.optJSONObject("initialRegion"))
|
|
720
720
|
if (explicitRegion != null) {
|
|
721
|
-
|
|
721
|
+
// A controlled region is an exact camera request. The padding
|
|
722
|
+
// used for fitToBounds is framing chrome, not part of the region
|
|
723
|
+
// contract, and would otherwise zoom every Android screen out.
|
|
724
|
+
moveCamera(explicitRegion, padding = 0)
|
|
722
725
|
initialCameraKey = null
|
|
723
726
|
} else if (initialRegion != null) {
|
|
724
727
|
val key = "region:${pendingProps.optJSONObject("initialRegion")}"
|
|
725
728
|
if (initialCameraKey != key) {
|
|
726
|
-
moveCamera(initialRegion)
|
|
729
|
+
moveCamera(initialRegion, padding = 0)
|
|
727
730
|
initialCameraKey = key
|
|
728
731
|
}
|
|
729
732
|
} else if (craftNativeBoolean(pendingProps.opt("fitToBounds"))) {
|
|
@@ -745,9 +748,9 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
745
748
|
}
|
|
746
749
|
}
|
|
747
750
|
|
|
748
|
-
private fun moveCamera(bounds: LatLngBounds) {
|
|
751
|
+
private fun moveCamera(bounds: LatLngBounds, padding: Int = 24) {
|
|
749
752
|
post {
|
|
750
|
-
if (width > 0 && height > 0) googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds,
|
|
753
|
+
if (width > 0 && height > 0) googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, padding))
|
|
751
754
|
}
|
|
752
755
|
}
|
|
753
756
|
|
|
@@ -2512,7 +2515,7 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2512
2515
|
}
|
|
2513
2516
|
|
|
2514
2517
|
scroller.setOnScrollChangeListener { _, x, y, _, _ ->
|
|
2515
|
-
if (
|
|
2518
|
+
if (horizontalMode != true) updateStickyHeaders()
|
|
2516
2519
|
onScroll?.invoke(x, y)
|
|
2517
2520
|
}
|
|
2518
2521
|
scroller.setOnTouchListener { _, event ->
|
|
@@ -6219,7 +6222,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6219
6222
|
else -> 400
|
|
6220
6223
|
}
|
|
6221
6224
|
view.typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
6222
|
-
Typeface.create("sans-serif", weight, false)
|
|
6225
|
+
Typeface.create(Typeface.create("sans-serif", Typeface.NORMAL), weight, false)
|
|
6223
6226
|
} else {
|
|
6224
6227
|
Typeface.create("sans-serif", if (weight >= 600) Typeface.BOLD else Typeface.NORMAL)
|
|
6225
6228
|
}
|
|
@@ -6275,7 +6278,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6275
6278
|
private fun configureTextStyle(view: TextView, style: JSONObject) {
|
|
6276
6279
|
view.setTextColor(color(craftNativeOptionalString(style.opt("color")).orEmpty(), defaultTextColor))
|
|
6277
6280
|
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, craftNativeFiniteFloat(style, "fontSize", 16f))
|
|
6278
|
-
val fontWeight = style.opt("fontWeight")
|
|
6281
|
+
val fontWeight = craftNativeFontWeight(style.opt("fontWeight"))
|
|
6279
6282
|
val weight = when (fontWeight) {
|
|
6280
6283
|
"bold" -> 700
|
|
6281
6284
|
"100", "200", "300", "400", "500", "600", "700", "800", "900" -> style.optInt("fontWeight")
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.211";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -245,7 +245,7 @@ export interface TextStyleProps {
|
|
|
245
245
|
/** Font style */
|
|
246
246
|
fontStyle?: 'normal' | 'italic';
|
|
247
247
|
/** Font weight */
|
|
248
|
-
fontWeight?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
|
|
248
|
+
fontWeight?: number | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
|
|
249
249
|
/** Letter spacing */
|
|
250
250
|
letterSpacing?: number;
|
|
251
251
|
/** Line height */
|
|
@@ -287,6 +287,8 @@ export interface ImageStyleProps {
|
|
|
287
287
|
* Combined Image style type.
|
|
288
288
|
*/
|
|
289
289
|
export type ImageStyle = ViewStyle & ImageStyleProps;
|
|
290
|
+
/** A native style may be nested or conditionally omitted in any component's `style` prop. */
|
|
291
|
+
export type StyleValue<T> = T | StyleValue<T>[] | null | false | undefined;
|
|
290
292
|
/**
|
|
291
293
|
* Base props for all components.
|
|
292
294
|
*/
|
|
@@ -320,7 +322,7 @@ export interface AccessibilityState {
|
|
|
320
322
|
*/
|
|
321
323
|
export interface ViewProps extends BaseProps {
|
|
322
324
|
/** Style object or array */
|
|
323
|
-
style?: ViewStyle
|
|
325
|
+
style?: StyleValue<ViewStyle>;
|
|
324
326
|
/** Press handler */
|
|
325
327
|
onPress?: () => void;
|
|
326
328
|
/** DOM-compatible press alias accepted by native hosts. */
|
|
@@ -356,19 +358,23 @@ export interface RefreshControlProps extends BaseProps {
|
|
|
356
358
|
*/
|
|
357
359
|
export interface TextProps extends BaseProps {
|
|
358
360
|
/** Style object or array */
|
|
359
|
-
style?: TextStyle
|
|
361
|
+
style?: StyleValue<TextStyle>;
|
|
360
362
|
/** Number of lines before truncation */
|
|
361
363
|
numberOfLines?: number;
|
|
362
364
|
/** Ellipsize mode */
|
|
363
365
|
ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
|
|
364
366
|
/** Press handler */
|
|
365
367
|
onPress?: () => void;
|
|
368
|
+
/** DOM-compatible press alias accepted by native hosts. */
|
|
369
|
+
onClick?: () => void;
|
|
366
370
|
/** Long press handler */
|
|
367
371
|
onLongPress?: () => void;
|
|
368
372
|
}
|
|
369
373
|
/** Native button/link action props. */
|
|
370
374
|
export interface ButtonProps extends BaseProps {
|
|
371
375
|
title?: string;
|
|
376
|
+
/** Text and layout styling applied by the native button hosts. */
|
|
377
|
+
style?: StyleValue<TextStyle>;
|
|
372
378
|
onPress?: () => void;
|
|
373
379
|
onClick?: () => void;
|
|
374
380
|
disabled?: boolean;
|
|
@@ -393,10 +399,17 @@ export interface TouchableHighlightProps extends ViewProps {
|
|
|
393
399
|
/**
|
|
394
400
|
* Image source type.
|
|
395
401
|
*/
|
|
396
|
-
export type ImageSource = {
|
|
402
|
+
export type ImageSource = string | {
|
|
397
403
|
uri: string;
|
|
398
404
|
headers?: Record<string, string>;
|
|
399
405
|
};
|
|
406
|
+
export interface ImageErrorEvent {
|
|
407
|
+
nativeEvent: {
|
|
408
|
+
error: {
|
|
409
|
+
message: string;
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
}
|
|
400
413
|
/**
|
|
401
414
|
* Image component props.
|
|
402
415
|
*/
|
|
@@ -404,7 +417,7 @@ export interface ImageProps extends BaseProps {
|
|
|
404
417
|
/** Image source */
|
|
405
418
|
source: ImageSource;
|
|
406
419
|
/** Style object or array */
|
|
407
|
-
style?: ImageStyle
|
|
420
|
+
style?: StyleValue<ImageStyle>;
|
|
408
421
|
/** Resize mode */
|
|
409
422
|
resizeMode?: 'cover' | 'contain' | 'stretch' | 'center';
|
|
410
423
|
/** Load start handler */
|
|
@@ -413,19 +426,21 @@ export interface ImageProps extends BaseProps {
|
|
|
413
426
|
onLoad?: () => void;
|
|
414
427
|
/** Load end handler */
|
|
415
428
|
onLoadEnd?: () => void;
|
|
416
|
-
/** Error handler */
|
|
417
|
-
onError?: (
|
|
429
|
+
/** Error handler; native failures are exposed as nativeEvent.error.message. */
|
|
430
|
+
onError?: (event: ImageErrorEvent) => void;
|
|
418
431
|
/** Default source while loading */
|
|
419
432
|
defaultSource?: ImageSource;
|
|
420
433
|
/** Alt text for accessibility */
|
|
421
434
|
alt?: string;
|
|
422
435
|
}
|
|
423
436
|
export interface IconProps extends BaseProps {
|
|
437
|
+
style?: StyleValue<TextStyle>;
|
|
424
438
|
name?: string;
|
|
425
439
|
symbol?: string;
|
|
426
440
|
color?: string;
|
|
427
441
|
}
|
|
428
442
|
export interface VideoProps extends BaseProps {
|
|
443
|
+
style?: StyleValue<ViewStyle>;
|
|
429
444
|
source?: string | {
|
|
430
445
|
uri: string;
|
|
431
446
|
};
|
|
@@ -472,6 +487,7 @@ export interface MapRegion {
|
|
|
472
487
|
};
|
|
473
488
|
}
|
|
474
489
|
export interface MapProps extends BaseProps {
|
|
490
|
+
style?: StyleValue<ViewStyle>;
|
|
475
491
|
region?: MapRegion;
|
|
476
492
|
/** Initial camera region; applied once, then controlled by subsequent props. */
|
|
477
493
|
initialRegion?: MapRegion;
|
|
@@ -512,7 +528,7 @@ export interface ImageBackgroundProps extends ImageProps {
|
|
|
512
528
|
}
|
|
513
529
|
/** Shared paint and accessibility props for DOM-free native SVG nodes. */
|
|
514
530
|
export interface SvgPaintProps extends BaseProps {
|
|
515
|
-
style?: ViewStyle
|
|
531
|
+
style?: StyleValue<ViewStyle>;
|
|
516
532
|
fill?: string;
|
|
517
533
|
stroke?: string;
|
|
518
534
|
strokeWidth?: number;
|
|
@@ -629,7 +645,7 @@ export interface PressableProps extends ViewProps {
|
|
|
629
645
|
*/
|
|
630
646
|
export interface TextInputProps extends BaseProps {
|
|
631
647
|
/** Style object or array */
|
|
632
|
-
style?: TextStyle
|
|
648
|
+
style?: StyleValue<TextStyle>;
|
|
633
649
|
/** Current value */
|
|
634
650
|
value?: string;
|
|
635
651
|
/** Default value */
|
|
@@ -680,6 +696,7 @@ export interface TextInputProps extends BaseProps {
|
|
|
680
696
|
export type KeyboardType = 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'number-pad' | 'decimal-pad' | 'url' | 'web-search' | 'visible-password';
|
|
681
697
|
export type ReturnKeyType = 'done' | 'go' | 'next' | 'search' | 'send' | 'join' | 'route' | 'continue' | 'emergency-call' | 'default';
|
|
682
698
|
export interface SwitchProps extends BaseProps {
|
|
699
|
+
style?: StyleValue<ViewStyle>;
|
|
683
700
|
/** Controlled boolean state; malformed native bridge values stay off. */
|
|
684
701
|
value?: boolean;
|
|
685
702
|
onValueChange?: (value: boolean) => void;
|
|
@@ -693,6 +710,7 @@ export interface SwitchProps extends BaseProps {
|
|
|
693
710
|
ios_backgroundColor?: string;
|
|
694
711
|
}
|
|
695
712
|
export interface CheckboxProps extends BaseProps {
|
|
713
|
+
style?: StyleValue<ViewStyle>;
|
|
696
714
|
/** Controlled boolean state; malformed native bridge values stay off. */
|
|
697
715
|
value?: boolean;
|
|
698
716
|
checked?: boolean;
|
|
@@ -704,6 +722,7 @@ export interface CheckboxProps extends BaseProps {
|
|
|
704
722
|
color?: string;
|
|
705
723
|
}
|
|
706
724
|
export interface SliderProps extends BaseProps {
|
|
725
|
+
style?: StyleValue<ViewStyle>;
|
|
707
726
|
value?: number;
|
|
708
727
|
minimumValue?: number;
|
|
709
728
|
maximumValue?: number;
|
|
@@ -722,6 +741,7 @@ export interface PickerOption {
|
|
|
722
741
|
value: string | number;
|
|
723
742
|
}
|
|
724
743
|
export interface PickerProps extends BaseProps {
|
|
744
|
+
style?: StyleValue<ViewStyle>;
|
|
725
745
|
/** Typed options; child options remain supported when omitted. */
|
|
726
746
|
items?: PickerOption[];
|
|
727
747
|
selectedValue?: string | number;
|
|
@@ -735,7 +755,8 @@ export interface DatePickerChange {
|
|
|
735
755
|
timestamp: number;
|
|
736
756
|
}
|
|
737
757
|
export interface DatePickerProps extends BaseProps {
|
|
738
|
-
|
|
758
|
+
style?: StyleValue<ViewStyle>;
|
|
759
|
+
mode?: 'date' | 'time' | 'datetime' | 'datetime-local' | 'countdown';
|
|
739
760
|
value?: string | number;
|
|
740
761
|
minimumDate?: string | number;
|
|
741
762
|
maximumDate?: string | number;
|
|
@@ -745,6 +766,7 @@ export interface DatePickerProps extends BaseProps {
|
|
|
745
766
|
disabled?: boolean;
|
|
746
767
|
}
|
|
747
768
|
export interface ActivityIndicatorProps extends BaseProps {
|
|
769
|
+
style?: StyleValue<ViewStyle>;
|
|
748
770
|
color?: string;
|
|
749
771
|
animating?: boolean;
|
|
750
772
|
hidesWhenStopped?: boolean;
|
|
@@ -782,6 +804,7 @@ export interface SectionListSection<T> {
|
|
|
782
804
|
export interface SectionListProps<T> extends ScrollViewProps {
|
|
783
805
|
}
|
|
784
806
|
export interface ModalProps extends BaseProps {
|
|
807
|
+
style?: StyleValue<ViewStyle>;
|
|
785
808
|
visible?: boolean;
|
|
786
809
|
transparent?: boolean;
|
|
787
810
|
animationType?: 'none' | 'fade' | 'slide';
|
|
@@ -893,9 +916,9 @@ export declare const StyleSheet: {
|
|
|
893
916
|
*/
|
|
894
917
|
create<T extends Record<string, ViewStyle | TextStyle | ImageStyle>>(styles: T): T;
|
|
895
918
|
/**
|
|
896
|
-
* Flatten
|
|
919
|
+
* Flatten nested style arrays and ignore falsy conditional entries.
|
|
897
920
|
*/
|
|
898
|
-
flatten<T>(style: T
|
|
921
|
+
flatten<T>(style: StyleValue<T>): T | undefined;
|
|
899
922
|
/**
|
|
900
923
|
* Get the absolute fill style (full parent).
|
|
901
924
|
*/
|
package/dist/index.cjs
CHANGED
|
@@ -309,7 +309,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
309
309
|
`);
|
|
310
310
|
}
|
|
311
311
|
// package.json
|
|
312
|
-
var version = "0.0.
|
|
312
|
+
var version = "0.0.211";
|
|
313
313
|
|
|
314
314
|
// src/sdk-version.ts
|
|
315
315
|
var SDK_VERSION = version;
|
|
@@ -8625,10 +8625,19 @@ var StyleSheet = {
|
|
|
8625
8625
|
flatten(style) {
|
|
8626
8626
|
if (!style)
|
|
8627
8627
|
return;
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8628
|
+
const flattened = {};
|
|
8629
|
+
const merge = (value) => {
|
|
8630
|
+
if (!value)
|
|
8631
|
+
return;
|
|
8632
|
+
if (Array.isArray(value)) {
|
|
8633
|
+
value.forEach(merge);
|
|
8634
|
+
return;
|
|
8635
|
+
}
|
|
8636
|
+
if (typeof value === "object")
|
|
8637
|
+
Object.assign(flattened, value);
|
|
8638
|
+
};
|
|
8639
|
+
merge(style);
|
|
8640
|
+
return flattened;
|
|
8632
8641
|
},
|
|
8633
8642
|
absoluteFill: {
|
|
8634
8643
|
position: "absolute",
|
package/dist/index.js
CHANGED
|
@@ -57,7 +57,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
57
57
|
`);
|
|
58
58
|
}
|
|
59
59
|
// package.json
|
|
60
|
-
var version = "0.0.
|
|
60
|
+
var version = "0.0.211";
|
|
61
61
|
|
|
62
62
|
// src/sdk-version.ts
|
|
63
63
|
var SDK_VERSION = version;
|
|
@@ -8384,10 +8384,19 @@ var StyleSheet = {
|
|
|
8384
8384
|
flatten(style) {
|
|
8385
8385
|
if (!style)
|
|
8386
8386
|
return;
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8387
|
+
const flattened = {};
|
|
8388
|
+
const merge = (value) => {
|
|
8389
|
+
if (!value)
|
|
8390
|
+
return;
|
|
8391
|
+
if (Array.isArray(value)) {
|
|
8392
|
+
value.forEach(merge);
|
|
8393
|
+
return;
|
|
8394
|
+
}
|
|
8395
|
+
if (typeof value === "object")
|
|
8396
|
+
Object.assign(flattened, value);
|
|
8397
|
+
};
|
|
8398
|
+
merge(style);
|
|
8399
|
+
return flattened;
|
|
8391
8400
|
},
|
|
8392
8401
|
absoluteFill: {
|
|
8393
8402
|
position: "absolute",
|
|
@@ -928,10 +928,12 @@ struct CraftTabBarView: View {
|
|
|
928
928
|
bar
|
|
929
929
|
if let accessory = chrome.tabAccessory {
|
|
930
930
|
Button { CraftHybrid.shared.open(accessory.id) } label: {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
931
|
+
surface {
|
|
932
|
+
Image(systemName: accessory.symbol)
|
|
933
|
+
.font(.system(size: iconSize))
|
|
934
|
+
.foregroundStyle(Color.primary)
|
|
935
|
+
.frame(width: 62, height: 62)
|
|
936
|
+
}
|
|
935
937
|
.overlay(alignment: .topTrailing) {
|
|
936
938
|
if let badge = accessory.badge {
|
|
937
939
|
Text(badge)
|
|
@@ -967,24 +969,27 @@ struct CraftTabBarView: View {
|
|
|
967
969
|
chrome.layoutChanged(max(0, screen - proxy.frame(in: .global).minY))
|
|
968
970
|
}
|
|
969
971
|
|
|
970
|
-
|
|
972
|
+
private var bar: some View {
|
|
973
|
+
surface { items.padding(4) }
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/// Both the tab capsule and its circular accessory use the same material,
|
|
977
|
+
/// outline and shadow. A square capsule forms the accessory's circle.
|
|
978
|
+
@ViewBuilder private func surface<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
|
971
979
|
#if compiler(>=6.2)
|
|
972
980
|
if #available(iOS 26.0, *) {
|
|
973
981
|
GlassEffectContainer {
|
|
974
|
-
|
|
975
|
-
.padding(4)
|
|
982
|
+
content()
|
|
976
983
|
.glassEffect(.regular.interactive(), in: .capsule)
|
|
977
984
|
}
|
|
978
985
|
} else {
|
|
979
|
-
|
|
980
|
-
.padding(4)
|
|
986
|
+
content()
|
|
981
987
|
.background(.ultraThinMaterial, in: Capsule())
|
|
982
988
|
.overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
|
|
983
989
|
.shadow(color: .black.opacity(0.12), radius: 18, y: 6)
|
|
984
990
|
}
|
|
985
991
|
#else
|
|
986
|
-
|
|
987
|
-
.padding(4)
|
|
992
|
+
content()
|
|
988
993
|
.background(.ultraThinMaterial, in: Capsule())
|
|
989
994
|
.overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
|
|
990
995
|
.shadow(color: .black.opacity(0.12), radius: 18, y: 6)
|
|
@@ -11,9 +11,9 @@ struct CraftLiveActivityWidget: Widget {
|
|
|
11
11
|
var body: some WidgetConfiguration {
|
|
12
12
|
ActivityConfiguration(for: CraftActivityAttributes.self) { context in
|
|
13
13
|
HStack(spacing: 12) {
|
|
14
|
-
|
|
14
|
+
activityIcon(context).font(.title2)
|
|
15
15
|
VStack(alignment: .leading, spacing: 4) {
|
|
16
|
-
Text(context.attributes.title).font(.headline).lineLimit(
|
|
16
|
+
Text(context.attributes.title).font(.headline).lineLimit(2)
|
|
17
17
|
if let detail = context.state.detail, !detail.isEmpty {
|
|
18
18
|
Text(detail).font(.subheadline).lineLimit(1)
|
|
19
19
|
}
|
|
@@ -33,14 +33,14 @@ struct CraftLiveActivityWidget: Widget {
|
|
|
33
33
|
} dynamicIsland: { context in
|
|
34
34
|
DynamicIsland {
|
|
35
35
|
DynamicIslandExpandedRegion(.leading) {
|
|
36
|
-
|
|
36
|
+
activityIcon(context).font(.title2)
|
|
37
37
|
}
|
|
38
38
|
DynamicIslandExpandedRegion(.trailing) {
|
|
39
39
|
timer(context).font(.title2.monospacedDigit().weight(.semibold))
|
|
40
40
|
}
|
|
41
41
|
DynamicIslandExpandedRegion(.center) {
|
|
42
42
|
VStack(spacing: 3) {
|
|
43
|
-
Text(context.attributes.title).font(.headline).lineLimit(
|
|
43
|
+
Text(context.attributes.title).font(.headline).lineLimit(2)
|
|
44
44
|
Text(context.isStale ? context.state.staleStatus ?? context.state.status : context.state.status).font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -59,17 +59,26 @@ struct CraftLiveActivityWidget: Widget {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
} compactLeading: {
|
|
62
|
-
|
|
62
|
+
activityIcon(context)
|
|
63
63
|
} compactTrailing: {
|
|
64
|
-
timer(context).monospacedDigit().frame(maxWidth: 64)
|
|
64
|
+
timer(context).monospacedDigit().frame(maxWidth: 64, alignment: .trailing)
|
|
65
65
|
} minimal: {
|
|
66
|
-
|
|
66
|
+
activityIcon(context)
|
|
67
67
|
}
|
|
68
68
|
.widgetURL(context.attributes.deepLink.flatMap(URL.init(string:)))
|
|
69
69
|
.keylineTint(tint(context))
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
private func activityIcon(_ context: ActivityViewContext<CraftActivityAttributes>) -> some View {
|
|
74
|
+
// The pause glyph uses the active symbol's footprint, keeping the
|
|
75
|
+
// title's leading edge steady through state changes.
|
|
76
|
+
Image(systemName: context.attributes.symbol ?? "timer")
|
|
77
|
+
.hidden()
|
|
78
|
+
.overlay { Image(systemName: symbol(context)) }
|
|
79
|
+
.foregroundStyle(tint(context))
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
private func symbol(_ context: ActivityViewContext<CraftActivityAttributes>) -> String {
|
|
74
83
|
context.state.paused == true ? "pause.fill" : context.attributes.symbol ?? "timer"
|
|
75
84
|
}
|
|
@@ -82,8 +91,23 @@ struct CraftLiveActivityWidget: Widget {
|
|
|
82
91
|
|
|
83
92
|
private func date(_ milliseconds: Double) -> Date { Date(timeIntervalSince1970: milliseconds / 1000) }
|
|
84
93
|
|
|
85
|
-
@ViewBuilder
|
|
86
94
|
private func timer(_ context: ActivityViewContext<CraftActivityAttributes>) -> some View {
|
|
95
|
+
// Measure the hour format in the inherited native font. A widget's
|
|
96
|
+
// ticking Text is horizontally flexible; an overlay bounds it to this
|
|
97
|
+
// footprint so names retain the same width when the timer pauses.
|
|
98
|
+
Text(duration(max(3600, context.state.durationSeconds, context.state.remainingSeconds ?? 0)))
|
|
99
|
+
.lineLimit(1)
|
|
100
|
+
.hidden()
|
|
101
|
+
.overlay(alignment: .trailing) {
|
|
102
|
+
timerContent(context)
|
|
103
|
+
.multilineTextAlignment(.trailing)
|
|
104
|
+
.lineLimit(1)
|
|
105
|
+
.minimumScaleFactor(0.8)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@ViewBuilder
|
|
110
|
+
private func timerContent(_ context: ActivityViewContext<CraftActivityAttributes>) -> some View {
|
|
87
111
|
if context.state.paused == true {
|
|
88
112
|
Text(duration(context.state.remainingSeconds ?? context.state.durationSeconds))
|
|
89
113
|
} else if let end = context.state.timerEndsAt {
|
|
@@ -4151,6 +4151,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4151
4151
|
let appVersion = Self.jsonString(info?["CFBundleShortVersionString"] as? String ?? "")
|
|
4152
4152
|
let appBuild = Self.jsonString(info?["CFBundleVersion"] as? String ?? "")
|
|
4153
4153
|
jsContext.evaluateScript("""
|
|
4154
|
+
// JavaScriptCore has no DOM window, but browser-safe SDK bridges
|
|
4155
|
+
// address the host through `window.craft`. Keep native screens
|
|
4156
|
+
// compatible without pretending a document exists.
|
|
4157
|
+
if (typeof globalThis.window === "undefined") { globalThis.window = globalThis; }
|
|
4154
4158
|
globalThis.__stxNativeCallback = null;
|
|
4155
4159
|
globalThis.__stxNativeBridge = {
|
|
4156
4160
|
platform: "ios",
|
|
@@ -4279,7 +4283,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4279
4283
|
/// `craft.secureStorage.set` wrote.
|
|
4280
4284
|
/// - `craft.navigation.setOptions({ title, largeTitle, hidden, backTitle,
|
|
4281
4285
|
/// tabBarHidden, swipeBack, contentSwipeBack, tabSwipeEnabled,
|
|
4282
|
-
/// backgroundColor, navigationBarSolid, rightButtons })`;
|
|
4286
|
+
/// backgroundColor, navigationBarSolid, rightButtons, tabAccessory })`;
|
|
4283
4287
|
/// a right button's tap is the event `navButton`.
|
|
4284
4288
|
///
|
|
4285
4289
|
/// A runtime that assigns `craft.storage` (or the others) afterwards keeps
|
|
@@ -5731,7 +5735,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5731
5735
|
? nil : number(props["activeOpacity"]) ?? 0.5
|
|
5732
5736
|
pressable.pressBaseOpacity = number(style["opacity"]) ?? 1
|
|
5733
5737
|
pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
|
|
5734
|
-
pressable.pressBaseBackgroundColor = pressable.backgroundColor
|
|
5735
5738
|
case "Switch":
|
|
5736
5739
|
let toggle = result as! UISwitch
|
|
5737
5740
|
toggle.isOn = props["value"] as? Bool ?? props["checked"] as? Bool ?? false
|
|
@@ -6689,6 +6692,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6689
6692
|
updateDimension(width, constraint: &node.widthConstraint, anchor: view.widthAnchor)
|
|
6690
6693
|
updateDimension(height, constraint: &node.heightConstraint, anchor: view.heightAnchor)
|
|
6691
6694
|
view.backgroundColor = color(style["backgroundColor"]) ?? .clear
|
|
6695
|
+
// Every flow container can receive press feedback, including ordinary
|
|
6696
|
+
// Views. Capture its current authored fill before feedback cleanup or
|
|
6697
|
+
// release restores it; otherwise avatars and button wrappers go clear.
|
|
6698
|
+
(view as? CraftNativeFlowView)?.pressBaseBackgroundColor = view.backgroundColor
|
|
6692
6699
|
// A field the style dresses itself (`border-0`, a border or a
|
|
6693
6700
|
// background, as a search field inside its own pill is) draws no
|
|
6694
6701
|
// system box of its own; a bare one keeps the rounded rectangle.
|
|
@@ -6940,7 +6947,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6940
6947
|
private func textFont(_ style: [String: Any], default fallback: UIFont) -> UIFont {
|
|
6941
6948
|
let size = number(style["fontSize"]) ?? fallback.pointSize
|
|
6942
6949
|
let weight: UIFont.Weight
|
|
6943
|
-
switch style["fontWeight"]
|
|
6950
|
+
switch craftNativeFontWeight(style["fontWeight"]) {
|
|
6944
6951
|
case "100": weight = .ultraLight
|
|
6945
6952
|
case "200": weight = .thin
|
|
6946
6953
|
case "300": weight = .light
|
|
@@ -6962,7 +6969,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6962
6969
|
?? .systemFont(ofSize: scaledSize, weight: weight)
|
|
6963
6970
|
if style["fontStyle"] as? String == "italic",
|
|
6964
6971
|
let descriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
|
6965
|
-
font = UIFont(descriptor: descriptor, size:
|
|
6972
|
+
font = UIFont(descriptor: descriptor, size: scaledSize)
|
|
6966
6973
|
}
|
|
6967
6974
|
return font
|
|
6968
6975
|
}
|
|
@@ -7162,7 +7169,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7162
7169
|
|| controlledVideo
|
|
7163
7170
|
view.isAccessibilityElement = !container && role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
|
|
7164
7171
|
view.accessibilityContainerType = container && (view.accessibilityLabel != nil || controlledVideo) ? .semanticGroup : .none
|
|
7172
|
+
// Keep the interaction trait native controls expose. Rebuilding this
|
|
7173
|
+
// set from the STX role alone otherwise turns a Slider, Picker,
|
|
7174
|
+
// DatePicker, or Switch into a static VoiceOver item whenever the
|
|
7175
|
+
// screen omits redundant accessibility metadata.
|
|
7165
7176
|
var traits: UIAccessibilityTraits = []
|
|
7177
|
+
switch type {
|
|
7178
|
+
case "Slider", "Picker", "DatePicker": traits.insert(.adjustable)
|
|
7179
|
+
case "Switch": traits.insert(.button)
|
|
7180
|
+
default: break
|
|
7181
|
+
}
|
|
7166
7182
|
switch role ?? type.lowercased() {
|
|
7167
7183
|
case "tablist": traits.insert(.tabBar)
|
|
7168
7184
|
case "button", "checkbox": traits.insert(.button)
|
|
@@ -167,7 +167,7 @@ final class DriverTests: XCTestCase {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
func testScript() throws {
|
|
170
|
-
continueAfterFailure =
|
|
170
|
+
continueAfterFailure = false
|
|
171
171
|
let data: Data?
|
|
172
172
|
if let resource = Bundle(for: DriverTests.self).url(forResource: "journey", withExtension: "txt") {
|
|
173
173
|
bundledJourney = true
|
|
@@ -219,7 +219,13 @@ final class DriverTests: XCTestCase {
|
|
|
219
219
|
check(element(containing: arg, in: springboard) == nil, "systemabsent \(arg)")
|
|
220
220
|
case "systemtap":
|
|
221
221
|
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
|
222
|
-
|
|
222
|
+
// Widget text lives in a remote accessibility host. Its frame can
|
|
223
|
+
// be visible while XCTest cannot resolve an activation point. Tap
|
|
224
|
+
// the containing system button (the actual Live Activity card).
|
|
225
|
+
let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR identifier == %@", arg, arg)
|
|
226
|
+
let card = springboard.buttons.containing(predicate).firstMatch
|
|
227
|
+
if card.exists && card.isHittable { press(card); note("systemtap \(arg): ok") }
|
|
228
|
+
else if let target = poll(10, { element(containing: arg, in: springboard) }) { press(target); note("systemtap \(arg): ok") }
|
|
223
229
|
else { XCTFail("System element not found"); note("FAIL systemtap \(arg): NOT FOUND") }
|
|
224
230
|
case "systemtree":
|
|
225
231
|
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|