@craft-native/android 0.0.204 → 0.0.206

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 CHANGED
@@ -106,16 +106,21 @@ Native mode maps STX `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Icon`,
106
106
  `Pressable`, `TouchableOpacity`, `TouchableHighlight`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Modal`, `Switch`, `Slider`, `ActivityIndicator`, `Image`, `ImageBackground`, `Video`, `Svg`, `G`, `SvgText`, `ScrollView`,
107
107
  `Map`, `SectionList`, and `FlatList` to Android views. `Map` uses the Google
108
108
  Maps SDK for trail polylines and marker taps; its overlays carry flat
109
- latitude/longitude `geometry` arrays and can fit the visible region to their
110
- bounds. Set `mapsApiKey` in the Android config to emit the SDK manifest key;
111
- Coordinates are range-checked at the native boundary, and invalid points or
112
- pole-crossing regions are ignored.
109
+ latitude/longitude `geometry` arrays, `strokeWidth` is expressed in logical
110
+ points, and the camera supports controlled `region`, one-time `initialRegion`,
111
+ and `fitToBounds`. Set `mapsApiKey` in the Android config to emit the SDK manifest key;
112
+ Coordinates must be JSON numbers (numeric strings are not coerced), are
113
+ range-checked at the native boundary, and invalid points or pole-crossing
114
+ regions are ignored. `fitToBounds` waits for the map viewport to
115
+ be measured before caching a successful camera fit, so an early map callback
116
+ does not lose the initial route framing.
113
117
  without it, the native map remains an explicit opt-in integration that needs
114
118
  the app's own Maps credentials. `SectionList` uses the same
115
119
  native scroll host for ordinary `.stx` slot children; grouped section data
116
120
  should use `FlatList` until the section-data protocol is available. The
117
121
  generated host routes both list names through `CraftNativeScrollView` for this
118
- slot-based contract. `Picker` renders `<select>`/`<option>` values
122
+ slot-based contract. `ScrollView` also supports `stickyHeaderIndices` and
123
+ `scrollTarget` for vertical native content. `Picker` renders `<select>`/`<option>` values
119
124
  with `onValueChange` or `onChange`. `DatePicker` uses the platform `DatePicker`
120
125
  and `TimePicker` controls for date, time, datetime, and countdown modes and
121
126
  emits local `value`, ISO, and millisecond `timestamp` fields. `Modal` is parent-scoped and supports
@@ -125,7 +130,10 @@ and `thumbColor`, while `Slider` supports track/thumb tint props and `step`
125
130
  snapping without collapsing stepped ranges to a fixed pixel-like precision.
126
131
  `ActivityIndicator` keeps a visible stopped indicator genuinely
127
132
  non-animated when `animating={false}` and `hidesWhenStopped={false}`. `Image`
128
- reports load lifecycle events and structured failures. `Video` uses the native
133
+ reports load lifecycle events and structured failures. Native image sources
134
+ accept HTTPS everywhere and loopback HTTP for local development (`localhost`,
135
+ `127.0.0.1`, `10.0.2.2`, `::1`, and `*.localhost`); public HTTP and unsupported
136
+ schemes are rejected. `Video` uses the native
129
137
  `VideoView` for URI playback with autoplay, pause, looping, mute, controls, and
130
138
  resize-mode props.
131
139
  `ImageBackground` loads an image behind ordinary native children while
@@ -146,8 +154,9 @@ become checked on their first user toggle.
146
154
  horizontal and inverted layouts, list chrome, and end-reached pagination.
147
155
  Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric or percentage
148
156
  `flexBasis` for main-axis space distribution while preserving gaps and margins.
149
- Shared transforms, z-order, view shadows, text shadows, and Android `elevation`
150
- are applied with the same style keys as the iOS renderer.
157
+ Shared transforms, z-order, view shadows, text shadows, per-edge border
158
+ styles, and linear gradients are applied with the same style keys as the iOS
159
+ renderer. Android `elevation` remains an additional native shadow input.
151
160
  `gap`, `rowGap`, and `columnGap` retain fractional density-independent values
152
161
  through line distribution and round only the final native frames.
153
162
  Numeric lengths and percentage widths/heights resolve against the native parent
@@ -234,11 +243,18 @@ With `nativeScreens` (paths to the screens that draw them), `tabs` and
234
243
  `nativeApp` in craft.config.json, the same keys as the iOS builder, the
235
244
  native renderer is a whole app rather than one bundle route:
236
245
 
246
+ `craft android init` and `craft android build` validate every tab and the
247
+ configured start path against `nativeScreens`; invalid native route tables are
248
+ rejected before a project is generated.
249
+
237
250
  - a tab bar and a navigation bar (`craft.navigation.setOptions`: title, large
238
251
  title, right buttons with badges, `tabBarHidden`), a stack per tab, Android's
239
252
  Back key popping it; completed left/right swipes switch between tab roots
240
253
  interactively, yield to a horizontal scroll view, and can be disabled per
241
254
  screen with `craft.navigation.setOptions({ tabSwipeEnabled: false })`;
255
+ each screen may also set `backgroundColor`; omitting it or passing `null`
256
+ follows the app's light/dark background resource, and the navigation bar and
257
+ large title use the same resolved color;
242
258
  - `craft.navigation.open(path)` matched in the iOS shell's order (fixed segments
243
259
  before parameters), a tab's path selecting its tab, a path no screen draws
244
260
  opening the site in a Custom Tab; deep links and tapped notifications route
package/dist/index.js CHANGED
@@ -380,6 +380,94 @@ function validateAndroidConfig(config) {
380
380
  throw new Error("Android targetSdk must not exceed compileSdk");
381
381
  }
382
382
  }
383
+ function normalizeNativeRoutePath(raw) {
384
+ if (typeof raw !== "string")
385
+ return null;
386
+ let path = raw;
387
+ const cut = path.search(/[?#]/);
388
+ if (cut >= 0)
389
+ path = path.slice(0, cut);
390
+ if (!path.startsWith("/"))
391
+ return null;
392
+ path = path.replace(/\/{2,}/g, "/");
393
+ if (path.length > 1 && path.endsWith("/"))
394
+ path = path.slice(0, -1);
395
+ return path;
396
+ }
397
+ function nativeRouteSegments(path) {
398
+ return path === "/" ? [] : path.slice(1).split("/");
399
+ }
400
+ function matchesNativeRoute(nativeScreens, raw) {
401
+ const path = normalizeNativeRoutePath(raw);
402
+ if (path === null)
403
+ return false;
404
+ const parts = nativeRouteSegments(path);
405
+ return Object.entries(nativeScreens ?? {}).some(([pattern, screen]) => {
406
+ if (!screen)
407
+ return false;
408
+ const normalized = normalizeNativeRoutePath(pattern);
409
+ if (normalized === null)
410
+ return false;
411
+ const route = nativeRouteSegments(normalized);
412
+ for (let index = 0;index < route.length; index++) {
413
+ const segment = route[index];
414
+ if (segment === "*")
415
+ return index === route.length - 1;
416
+ if (index >= parts.length)
417
+ return false;
418
+ if (segment.startsWith(":")) {
419
+ if (!parts[index])
420
+ return false;
421
+ } else if (segment !== parts[index])
422
+ return false;
423
+ }
424
+ return route.length === parts.length;
425
+ });
426
+ }
427
+ function nativeStartPath(config) {
428
+ if (!config.devServerURL)
429
+ return "/";
430
+ try {
431
+ const url = new URL(config.devServerURL);
432
+ return `${url.pathname || "/"}${url.search}`;
433
+ } catch {
434
+ return "/";
435
+ }
436
+ }
437
+ function validateNativeRouteConfig(config) {
438
+ const screens = Object.entries(config.nativeScreens ?? {});
439
+ for (const [pattern, screen] of screens) {
440
+ if (normalizeNativeRoutePath(pattern) !== pattern || /[?#]/.test(pattern))
441
+ throw new Error(`nativeScreens path "${pattern}" must be an absolute path with no query, hash or trailing slash.`);
442
+ if (nativeRouteSegments(normalizeNativeRoutePath(pattern) ?? "/").slice(0, -1).includes("*"))
443
+ throw new Error(`nativeScreens path "${pattern}" may only end in *.`);
444
+ if (typeof screen !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(screen))
445
+ throw new Error(`nativeScreens screen "${String(screen)}" for "${pattern}" must be a screen name from the native bundle.`);
446
+ }
447
+ const ids = new Set;
448
+ for (const tab of config.tabs ?? []) {
449
+ if (!tab || typeof tab.id !== "string" || !tab.id || typeof tab.title !== "string" || !tab.title)
450
+ throw new Error("Each tab needs an id (its root path) and a title.");
451
+ if (ids.has(tab.id))
452
+ throw new Error(`Tab "${tab.id}" is listed twice.`);
453
+ ids.add(tab.id);
454
+ }
455
+ if (config.renderer === "native" && screens.length > 0) {
456
+ for (const id of ids) {
457
+ if (!matchesNativeRoute(config.nativeScreens, id))
458
+ throw new Error(`Tab "${id}" is not a nativeScreens path; with renderer: "native" every tab must be a native screen.`);
459
+ }
460
+ const start = nativeStartPath(config);
461
+ if (!matchesNativeRoute(config.nativeScreens, start))
462
+ throw new Error(`The start path "${start}" (from devServerURL) is not a nativeScreens path; with renderer: "native" the app must start on a native screen.`);
463
+ }
464
+ if (config.nativeApp !== undefined) {
465
+ if (typeof config.nativeApp !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(config.nativeApp))
466
+ throw new Error(`nativeApp "${String(config.nativeApp)}" must be a route name from the native bundle.`);
467
+ if (config.renderer !== "native" && screens.length === 0)
468
+ throw new Error('nativeApp runs in the native bundle; it needs nativeScreens or renderer: "native".');
469
+ }
470
+ }
383
471
  function writeAndroidConfig(output, config) {
384
472
  writeFileSync(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
385
473
  const runtimeConfig = { ...config };
@@ -601,6 +689,7 @@ async function init(options) {
601
689
  }
602
690
  normalizeAndroidNetworkConfig(config);
603
691
  validateAndroidConfig(config);
692
+ validateNativeRouteConfig(config);
604
693
  if (config.enablePushNotifications && !config.googleServicesFile) {
605
694
  throw new Error("Android push notifications require a googleServicesFile");
606
695
  }
@@ -861,6 +950,7 @@ async function build(options) {
861
950
  }
862
951
  const config = JSON.parse(readFileSync2(configPath, "utf-8"));
863
952
  validateAndroidConfig(config);
953
+ validateNativeRouteConfig(config);
864
954
  if (config.renderer === "native") {
865
955
  if (htmlPath || devServer)
866
956
  throw new Error("A native Android screen cannot use --html-path or --dev-server.");
@@ -990,4 +1080,4 @@ export {
990
1080
  ANDROID_MIN_SDK
991
1081
  };
992
1082
 
993
- //# debugId=12217629CA4DCE5764756E2164756E21
1083
+ //# debugId=D95CCF804E0EBBCF64756E2164756E21