@octane-xplat/ui 0.7.2 → 0.7.3

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 (43) hide show
  1. package/dist/native/CommandPalette.tsrx.js +7 -1
  2. package/dist/native/Grid.tsrx.js +31 -2
  3. package/dist/native/Pagination.tsrx.js +15 -12
  4. package/dist/native/Popover.tsrx.js +18 -7
  5. package/dist/native/Screen.tsrx.js +1 -1
  6. package/dist/native/Sheet.tsrx.js +1 -1
  7. package/dist/native/Slider.tsrx.js +23 -14
  8. package/dist/native/Text.tsrx.js +16 -0
  9. package/dist/native/WebView.tsrx.js +17 -2
  10. package/dist/native/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
  11. package/dist/native/escape-props.js +2 -0
  12. package/dist/native/grid-auto-placement.js +83 -0
  13. package/dist/native/grid-placement.js +20 -0
  14. package/dist/native/octane-client-build.json +1 -1
  15. package/dist/native/sheet-service.js +1 -1
  16. package/dist/native/{tap-to-blur.mobile.js → tap-to-blur.ios.js} +20 -32
  17. package/dist/native/useMeasure.tsrx.js +17 -18
  18. package/dist/web/octane-client-build.json +1 -1
  19. package/package.json +1 -1
  20. package/src/CommandPalette.tsrx +7 -1
  21. package/src/Grid.tsrx +30 -1
  22. package/src/Pagination.tsrx +6 -4
  23. package/src/Popover.tsrx +20 -6
  24. package/src/Screen.tsrx +1 -1
  25. package/src/Sheet.tsrx +1 -1
  26. package/src/Slider.tsrx +13 -11
  27. package/src/Text.tsrx +34 -0
  28. package/src/WebView.tsrx +19 -10
  29. package/src/escape-props.ts +11 -1
  30. package/src/grid-auto-placement.ts +121 -0
  31. package/src/grid-placement.ts +29 -0
  32. package/src/sheet-service.ts +1 -1
  33. package/src/tap-to-blur.ios.ts +90 -0
  34. package/src/tap-to-blur.mobile.ts +12 -94
  35. package/src/theme/tokens.css +24 -8
  36. package/src/useMeasure.tsrx +20 -21
  37. package/types/generated/.tsrx-typegen-manifest.json +3 -0
  38. package/types/generated/Grid.d.ts +0 -1
  39. package/types/generated/escape-props.d.ts +4 -0
  40. package/types/generated/grid-auto-placement.d.ts +13 -0
  41. package/types/generated/grid-placement.d.ts +5 -0
  42. package/types/generated/tap-to-blur.ios.d.ts +13 -0
  43. package/types/generated/tap-to-blur.mobile.d.ts +1 -15
@@ -1,4 +1,4 @@
1
- import { hookSlots, useCallback, useEffect, useState } from "@nativescript-community/octane";
1
+ import { hookSlots, useCallback, useEffect, useRef, useState } from "@nativescript-community/octane";
2
2
  //#region src/useMeasure.tsrx
3
3
  var _hs$ = /* @__PURE__ */ hookSlots(4);
4
4
  var _h$0 = /* @__PURE__ */ Symbol(_hs$);
@@ -17,44 +17,43 @@ function readBounds(view) {
17
17
  };
18
18
  }
19
19
  function useMeasure(options = {}) {
20
- const [view, setView] = useState(null, _h$0);
21
- const [bounds, setBounds] = useState(null, _h$1);
20
+ const [bounds, setBounds] = useState(null, _h$0);
21
+ const detach = useRef(null, _h$1);
22
22
  const observe = options.observe ?? true;
23
- const bind = useCallback((next) => {
24
- setView((current) => current === next ? current : next);
25
- }, [], _h$2);
26
- useEffect(() => {
23
+ const bind = useCallback((view) => {
24
+ detach.current?.();
25
+ detach.current = null;
27
26
  if (!view) {
28
27
  setBounds(null);
29
28
  return;
30
29
  }
31
30
  const update = () => {
32
31
  const next = readBounds(view);
33
- setBounds((prev) => prev && next && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height ? prev : next);
32
+ setBounds((prev) => {
33
+ return prev && next && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height ? prev : next;
34
+ });
34
35
  };
35
- const onLoaded = update;
36
- view.on?.("loaded", onLoaded);
36
+ view.on?.("loaded", update);
37
37
  update();
38
+ const scrollParents = [];
38
39
  if (observe) {
39
40
  view.on?.("layoutChanged", update);
40
41
  let parent = view.parent;
41
42
  while (parent) {
42
43
  parent.on?.("scroll", update);
44
+ scrollParents.push(parent);
43
45
  parent = parent.parent;
44
46
  }
45
47
  }
46
- return () => {
47
- view.off?.("loaded", onLoaded);
48
+ detach.current = () => {
49
+ view.off?.("loaded", update);
48
50
  if (observe) {
49
51
  view.off?.("layoutChanged", update);
50
- let parent = view.parent;
51
- while (parent) {
52
- parent.off?.("scroll", update);
53
- parent = parent.parent;
54
- }
52
+ for (const parent of scrollParents) parent.off?.("scroll", update);
55
53
  }
56
54
  };
57
- }, [view, observe], _h$3);
55
+ }, [observe], _h$2);
56
+ useEffect(() => () => detach.current?.(), [], _h$3);
58
57
  return {
59
58
  bind,
60
59
  bounds
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "b6a64d5b-cfff-418e-921b-443efbc3bad2",
3
+ "buildId": "0ba2ee91-551d-42cf-a6c9-dedc87a8a53b",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/ui",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Cross-platform UI for Octane xplat — shared primitives, stacks, routes and theming across web (DOM), iOS and Android (NativeScript)",
5
5
  "keywords": [
6
6
  "octane",
@@ -1,5 +1,6 @@
1
1
  /** @jsxImportSource @nativescript-community/octane */
2
2
  import { useState } from 'octane';
3
+ import { Screen } from '@nativescript/core';
3
4
  import type { CommandPaletteProps, MenuItem } from "./props";
4
5
  import { cx } from "./cx";
5
6
  import { Overlay } from './Overlay.tsrx';
@@ -19,6 +20,10 @@ export function CommandPalette(props: CommandPaletteProps) @{
19
20
  const shown = query
20
21
  ? props.items.filter((i) => (i.label ?? i.key).toLowerCase().includes(query.toLowerCase()))
21
22
  : props.items;
23
+ const panelStyle = {
24
+ ...props.style,
25
+ maxWidth: props.style?.maxWidth ?? Screen.mainScreen.widthDIPs * 0.9,
26
+ };
22
27
 
23
28
  const select = (item: MenuItem) => {
24
29
  if (item.disabled) return;
@@ -29,8 +34,9 @@ export function CommandPalette(props: CommandPaletteProps) @{
29
34
  <Overlay open={props.open} shadeCover={true} onDismiss={() => setOpen(false)} className="vx-cmdk-layer">
30
35
  <flexboxlayout
31
36
  className={cx('vx-commandpalette', props.className)}
32
- style={props.style}
37
+ style={panelStyle}
33
38
  id={props.id}
39
+ flexDirection="column"
34
40
  horizontalAlignment="center"
35
41
  verticalAlignment="top"
36
42
  marginTop={80}
package/src/Grid.tsrx CHANGED
@@ -1,20 +1,49 @@
1
1
  /** @jsxImportSource @nativescript-community/octane */
2
+ import { useEffect, useRef } from 'octane';
2
3
  import type { GridProps } from './props';
3
4
  import { cx } from './cx';
4
5
  import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from './escape-props';
6
+ import { autoPlaceGridChildren, ensureGridTracks } from './grid-auto-placement';
7
+ import { gridChildPlacement } from './grid-placement';
5
8
 
6
9
  let warnedAboutGap = false;
7
10
 
8
11
  export function Grid(props: GridProps) @{
12
+ const grid = useRef<any>(null);
9
13
  const deprecatedGap = (props as GridProps & { gap?: number | string }).gap;
10
14
  if (deprecatedGap != null && !warnedAboutGap) {
11
15
  warnedAboutGap = true;
12
16
  console.warn('[Grid] gap was removed; use child margins');
13
17
  }
14
18
 
19
+ useEffect(() => {
20
+ const host = grid.current;
21
+ if (!host) return;
22
+
23
+ const views: any[] = [];
24
+ host.eachChildView((child: any) => {
25
+ views.push(child);
26
+ return true;
27
+ });
28
+ const placement = autoPlaceGridChildren(
29
+ views.map(gridChildPlacement),
30
+ props.rows,
31
+ props.columns,
32
+ );
33
+ host.rows = ensureGridTracks(props.rows, placement.rowCount);
34
+ host.columns = ensureGridTracks(props.columns, placement.columnCount);
35
+ for (let index = 0; index < views.length; index++) {
36
+ views[index].row = placement.cells[index].row;
37
+ views[index].col = placement.cells[index].col;
38
+ }
39
+ }, [props.children, props.rows, props.columns]);
40
+
15
41
  <gridlayout
16
42
  id={props.id}
17
- ref={(view: any) => applyEscapeProps(view, props)}
43
+ ref={(view: any) => {
44
+ grid.current = view;
45
+ applyEscapeProps(view, props);
46
+ }}
18
47
  rows={props.rows}
19
48
  columns={props.columns}
20
49
  className={cx('vx-grid', props.className)}
@@ -36,11 +36,13 @@ export function Pagination(props: PaginationProps) @{
36
36
  >
37
37
  <flexboxlayout className="vx-page-btn" onTap={() => go(current - 1)}><label text="‹" /></flexboxlayout>
38
38
  @for (const e of pages; key e.key) {
39
- {e.page == null
40
- ? <label className="vx-page-ellipsis" text="…" />
41
- : <flexboxlayout className={cx('vx-page-btn', e.page === current && 'vx-page-btn--on')} onTap={() => go(e.page!)}>
39
+ @if (e.page == null) {
40
+ <label className="vx-page-ellipsis" text="…" />
41
+ } @else {
42
+ <flexboxlayout className={cx('vx-page-btn', e.page === current && 'vx-page-btn--on')} onTap={() => go(e.page!)}>
42
43
  <label text={String(e.page)} />
43
- </flexboxlayout>}
44
+ </flexboxlayout>
45
+ }
44
46
  }
45
47
  <flexboxlayout className="vx-page-btn" onTap={() => go(current + 1)}><label text="›" /></flexboxlayout>
46
48
  </flexboxlayout>
package/src/Popover.tsrx CHANGED
@@ -1,6 +1,6 @@
1
1
  /** @jsxImportSource @nativescript-community/octane */
2
2
  import { useEffect, useRef, useState } from 'octane';
3
- import { GridLayout } from '@nativescript/core';
3
+ import { GridLayout, isIOS, Screen } from '@nativescript/core';
4
4
  import { createNativeScriptRoot } from '@nativescript-community/octane';
5
5
  import type { UniversalComponent } from 'octane/universal';
6
6
  import type { PopoverProps } from './props';
@@ -38,13 +38,19 @@ function PopoverLayer(props: PopoverProps & { anchorView: any }) @{
38
38
  },
39
39
  props.placement ?? 'bottom',
40
40
  );
41
-
41
+ const insets = isIOS ? panelRef.current?.parent?.getSafeAreaInsets?.() : undefined;
42
+ const scale = isIOS ? Number(Screen.mainScreen.scale) || 1 : 1;
43
+ const panelPosition = {
44
+ ...next,
45
+ left: next.left - Number(insets?.left ?? 0) / scale,
46
+ top: next.top - Number(insets?.top ?? 0) / scale,
47
+ };
42
48
  setPosition((current) =>
43
- current?.left === next.left &&
44
- current?.top === next.top &&
45
- current?.placement === next.placement
49
+ current?.left === panelPosition.left &&
50
+ current?.top === panelPosition.top &&
51
+ current?.placement === panelPosition.placement
46
52
  ? current
47
- : next,
53
+ : panelPosition,
48
54
  );
49
55
 
50
56
  if (panelSize?.width > 0 && panelSize?.height > 0) setSettled(true);
@@ -73,6 +79,14 @@ function PopoverLayer(props: PopoverProps & { anchorView: any }) @{
73
79
  };
74
80
  }, [props.open, props.anchorView, props.placement, props.children, props.style]);
75
81
 
82
+ useEffect(() => {
83
+ if (position) {
84
+ const panel = panelRef.current;
85
+ const parent = panel?.parent;
86
+ parent?.requestLayout?.();
87
+ }
88
+ }, [position]);
89
+
76
90
  useEffect(() => {
77
91
  applyNativeProps(panelRef.current, props);
78
92
  }, [props.ios, props.android, settled]);
package/src/Screen.tsrx CHANGED
@@ -3,7 +3,7 @@ import type { ScreenProps } from "./props";
3
3
  import { cx } from "./cx";
4
4
  import { applyEscapeProps } from "./escape-props";
5
5
  import { registerRootLayout, unregisterRootLayout } from "./root-layout.mobile";
6
- import { attachTapToBlur } from "./tap-to-blur.mobile";
6
+ import { attachTapToBlur } from "./tap-to-blur";
7
7
  // The screen shell is a RootLayout so RootLayout.open() can layer overlays
8
8
  // over it (decision #22). Self-registers on loaded/unloaded — overlay and
9
9
  // sheet services mount on the most recently mounted shell (topRootLayout).
package/src/Sheet.tsrx CHANGED
@@ -9,7 +9,7 @@ import { applyThemeClasses } from './theme/theme-scheme';
9
9
  import { rootLayoutFor } from './root-layout.mobile';
10
10
  import { attachSheetDetents } from './sheet-detents';
11
11
  import { bindBottomInsetToKeyboard } from './keyboard-inset.mobile';
12
- import { attachTapToBlur } from './tap-to-blur.mobile';
12
+ import { attachTapToBlur } from './tap-to-blur';
13
13
 
14
14
  function SheetBody(props: { children?: any }) @{
15
15
  <>{props.children}</>
package/src/Slider.tsrx CHANGED
@@ -22,14 +22,20 @@ export function Slider(props: SliderProps) @{
22
22
  const ratio = Math.min(1, Math.max(0, (value - min) / (max - min)));
23
23
  const width = measure.bounds?.width ?? 0;
24
24
 
25
- // Stable ref identity — an inline ref closure re-fires ref(null)/ref(el)
26
- // per render, which bounces useMeasure's setView and loops forever.
25
+ // Keep one ref identity so native measurement listeners stay attached to
26
+ // the same host across rerenders.
27
27
  const host = useRef<any>(null);
28
28
  const bindHost = useCallback((el: any) => {
29
29
  host.current = el;
30
30
  measure.bind(el);
31
31
  }, []);
32
32
 
33
+ const trackHeight = 4;
34
+ const thumbSize = 20;
35
+ const trackTop = (28 - trackHeight) / 2;
36
+ const thumbTop = (28 - thumbSize) / 2;
37
+ const thumbLeft = Math.min(Math.max(ratio * width - thumbSize / 2, 0), Math.max(width - thumbSize, 0));
38
+
33
39
  useEffect(() => {
34
40
  applyNativeProps(host.current, props);
35
41
  });
@@ -52,7 +58,7 @@ export function Slider(props: SliderProps) @{
52
58
  }
53
59
  });
54
60
 
55
- <gridlayout
61
+ <absolutelayout
56
62
  id={props.id}
57
63
  className={cx('vx-slider', props.className)}
58
64
  style={props.style}
@@ -60,12 +66,8 @@ export function Slider(props: SliderProps) @{
60
66
  onPan={onPan}
61
67
  ref={bindHost}
62
68
  >
63
- <flexboxlayout className="vx-slider-track" />
64
- <flexboxlayout className="vx-slider-fill" horizontalAlignment="left" style={{ width: ratio * width }} />
65
- <flexboxlayout
66
- className="vx-slider-thumb"
67
- horizontalAlignment="left"
68
- style={{ marginLeft: Math.min(Math.max(ratio * width - 10, 0), Math.max(width - 20, 0)) }}
69
- />
70
- </gridlayout>
69
+ <flexboxlayout className="vx-slider-track" style={{ width, translateY: trackTop }} />
70
+ <flexboxlayout className="vx-slider-fill" style={{ width: ratio * width, translateY: trackTop }} />
71
+ <flexboxlayout className="vx-slider-thumb" style={{ translateX: thumbLeft, translateY: thumbTop }} />
72
+ </absolutelayout>
71
73
  }
package/src/Text.tsrx CHANGED
@@ -1,5 +1,6 @@
1
1
  /** @jsxImportSource @nativescript-community/octane */
2
2
  import { createContext, useContext } from 'octane';
3
+ import { Application } from '@nativescript/core';
3
4
  import type { TextProps } from "./props";
4
5
  import { cx } from "./cx";
5
6
  import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from "./escape-props";
@@ -24,8 +25,41 @@ function TextScope(props: { children?: any }) {
24
25
 
25
26
  export function Text(props: TextProps) @{
26
27
  const inText = useContext(InText);
28
+ const fontWeightValue = String(props.style?.fontWeight ?? '').toLowerCase();
29
+ const namedWeights: Record<string, number> = {
30
+ normal: 400,
31
+ bold: 700,
32
+ thin: 100,
33
+ 'extra-light': 200,
34
+ light: 300,
35
+ medium: 500,
36
+ 'semi-bold': 600,
37
+ 'extra-bold': 800,
38
+ black: 900,
39
+ };
40
+ const fontWeight = /^\d+$/.test(fontWeightValue)
41
+ ? Number(fontWeightValue)
42
+ : namedWeights[fontWeightValue];
43
+ const lineHeight = props.style?.lineHeight;
44
+ // UIFontDescriptor resolves variable fonts to the regular axis default on
45
+ // iOS. Set wght explicitly so CSS fontWeight selects the matching instance.
27
46
  const textStyle = {
28
47
  ...props.style,
48
+ ...((Application.ios || Application.android) &&
49
+ typeof lineHeight === 'number' &&
50
+ Number.isFinite(lineHeight) &&
51
+ lineHeight > 0 &&
52
+ props.style?.height == null &&
53
+ props.style?.minHeight == null
54
+ ? { minHeight: lineHeight }
55
+ : {}),
56
+ ...(Application.ios &&
57
+ props.style?.fontVariationSettings == null &&
58
+ fontWeight !== undefined &&
59
+ fontWeight >= 100 &&
60
+ fontWeight <= 900
61
+ ? { fontVariationSettings: `'wght' ${fontWeight}` }
62
+ : {}),
29
63
  // Clamped text must be allowed to shrink — the shared shrink:0 default
30
64
  // would let the label overflow its row instead of truncating.
31
65
  ...(props.ellipsize || props.numberOfLines ? { flexShrink: 1 } : {}),
package/src/WebView.tsrx CHANGED
@@ -24,6 +24,22 @@ function reportContentLayout(view: any, props: WebViewProps) {
24
24
  }
25
25
  }
26
26
 
27
+ function applyNativeScrollEnabled(view: any, enabled: boolean | undefined) {
28
+ if (isIOS) {
29
+ const scrollView = view?.ios?.scrollView
30
+ if (scrollView) scrollView.scrollEnabled = enabled !== false
31
+ } else if (isAndroid) {
32
+ const nativeView = view?.android
33
+ if (!nativeView?.setOnTouchListener) return
34
+ nativeView.setOnTouchListener(enabled === false
35
+ ? new android.view.View.OnTouchListener({
36
+ onTouch: (_view: any, event: any) =>
37
+ event.getActionMasked() === android.view.MotionEvent.ACTION_MOVE,
38
+ })
39
+ : null)
40
+ }
41
+ }
42
+
27
43
  /** Embedded web document — NS <webview> (WKWebView / android.webkit.WebView).
28
44
  * `html` flows through the `src` property: values that aren't http/file
29
45
  * URLs take its `_loadData` path. NS reports failures as `loadFinished`
@@ -39,16 +55,7 @@ export function WebView(props: WebViewProps) @{
39
55
  useEffect(() => {
40
56
  const v = el.current;
41
57
  if (!v) return;
42
- if (isIOS) {
43
- v.ios.scrollView.scrollEnabled = props.scrollEnabled !== false;
44
- } else if (isAndroid) {
45
- v.android.setOnTouchListener(props.scrollEnabled === false
46
- ? new android.view.View.OnTouchListener({
47
- onTouch: (_view: any, event: any) =>
48
- event.getActionMasked() === android.view.MotionEvent.ACTION_MOVE,
49
- })
50
- : null);
51
- }
58
+ applyNativeScrollEnabled(v, props.scrollEnabled)
52
59
  if (props.matchContents && loaded.current) reportContentLayout(v, props);
53
60
  }, [props.scrollEnabled, props.matchContents]);
54
61
 
@@ -70,7 +77,9 @@ export function WebView(props: WebViewProps) @{
70
77
  className={cx('vx-webview', props.className)}
71
78
  style={props.style}
72
79
  src={props.html !== undefined ? props.html : props.src}
80
+ onLoaded={() => applyNativeScrollEnabled(el.current, props.scrollEnabled)}
73
81
  onLoadFinished={(e: any) => {
82
+ applyNativeScrollEnabled(e.object ?? el.current, props.scrollEnabled)
74
83
  if (e.error) props.onError?.({ url: e.url, error: e.error });
75
84
  else {
76
85
  loaded.current = true;
@@ -1,15 +1,25 @@
1
1
  import { isAndroid, isIOS } from '@nativescript/core'
2
2
  import type { NativeModifier, Role } from './props'
3
3
  import { applyNativeModifiers } from './apply-native-modifiers.native'
4
+ import { rememberGridChildPlacement } from './grid-placement'
4
5
 
5
6
  /** Apply only the active device's escape bag after the primitive's own props. */
6
7
  export function applyEscapeProps(
7
8
  view: any,
8
- props: { ios?: any; android?: any; modifiers?: readonly NativeModifier[] },
9
+ props: {
10
+ ios?: any
11
+ android?: any
12
+ modifiers?: readonly NativeModifier[]
13
+ row?: number
14
+ col?: number
15
+ rowSpan?: number
16
+ colSpan?: number
17
+ },
9
18
  ): void {
10
19
  if (!view) {
11
20
  return
12
21
  }
22
+ rememberGridChildPlacement(view, props)
13
23
 
14
24
  const bag = isIOS ? props.ios : isAndroid ? props.android : undefined
15
25
  if (bag) {
@@ -0,0 +1,121 @@
1
+ import type { GridChildPlacement } from './grid-placement'
2
+
3
+ export interface GridCell {
4
+ row: number
5
+ col: number
6
+ }
7
+
8
+ export interface GridAutoPlacement {
9
+ cells: GridCell[]
10
+ rowCount: number
11
+ columnCount: number
12
+ }
13
+
14
+ function gridIndex(value: unknown): number | null {
15
+ if (value == null || value === '') return null
16
+ const index = Number(value)
17
+ return Number.isInteger(index) && index >= 0 ? index : null
18
+ }
19
+
20
+ function gridSpan(value: unknown): number {
21
+ const span = Number(value)
22
+ return Number.isInteger(span) && span > 0 ? span : 1
23
+ }
24
+
25
+ function trackCount(spec?: string): number {
26
+ return spec?.split(',').filter((track) => track.trim().length > 0).length ?? 0
27
+ }
28
+
29
+ /** Match CSS Grid's row-flow placement for NativeScript GridLayout children. */
30
+ export function autoPlaceGridChildren(
31
+ items: readonly GridChildPlacement[],
32
+ rowTracks?: string,
33
+ columnTracks?: string,
34
+ ): GridAutoPlacement {
35
+ const placements = items.map((item) => ({
36
+ row: gridIndex(item.row),
37
+ col: gridIndex(item.col),
38
+ rowSpan: gridSpan(item.rowSpan),
39
+ colSpan: gridSpan(item.colSpan),
40
+ }))
41
+ const columnCount = Math.max(
42
+ 1,
43
+ trackCount(columnTracks),
44
+ ...placements
45
+ .filter((placement) => placement.col != null)
46
+ .map((placement) => placement.col! + placement.colSpan),
47
+ ...placements.map((placement) => placement.colSpan),
48
+ )
49
+ const occupied = new Set<string>()
50
+ const isFree = (row: number, col: number, rowSpan: number, colSpan: number) => {
51
+ for (let y = row; y < row + rowSpan; y++) {
52
+ for (let x = col; x < col + colSpan; x++) {
53
+ if (occupied.has(`${y},${x}`)) return false
54
+ }
55
+ }
56
+ return true
57
+ }
58
+ const occupy = (row: number, col: number, rowSpan: number, colSpan: number) => {
59
+ for (let y = row; y < row + rowSpan; y++) {
60
+ for (let x = col; x < col + colSpan; x++) occupied.add(`${y},${x}`)
61
+ }
62
+ }
63
+
64
+ // Definite placements reserve their cells before automatic items flow.
65
+ for (const placement of placements) {
66
+ if (placement.row != null && placement.col != null) {
67
+ occupy(placement.row, placement.col, placement.rowSpan, placement.colSpan)
68
+ }
69
+ }
70
+
71
+ for (const placement of placements) {
72
+ if (placement.row == null || placement.col != null) continue
73
+ let col = 0
74
+ while (!isFree(placement.row, col, placement.rowSpan, placement.colSpan)) col++
75
+ placement.col = col
76
+ occupy(placement.row, col, placement.rowSpan, placement.colSpan)
77
+ }
78
+
79
+ for (const placement of placements) {
80
+ if (placement.col == null || placement.row != null) continue
81
+ let row = 0
82
+ while (!isFree(row, placement.col, placement.rowSpan, placement.colSpan)) row++
83
+ placement.row = row
84
+ occupy(row, placement.col, placement.rowSpan, placement.colSpan)
85
+ }
86
+
87
+ let cursorRow = 0
88
+ let cursorCol = 0
89
+ for (const placement of placements) {
90
+ if (placement.row != null && placement.col != null) continue
91
+ while (true) {
92
+ if (cursorCol + placement.colSpan > columnCount) {
93
+ cursorRow++
94
+ cursorCol = 0
95
+ continue
96
+ }
97
+ if (isFree(cursorRow, cursorCol, placement.rowSpan, placement.colSpan)) break
98
+ cursorCol++
99
+ }
100
+ placement.row = cursorRow
101
+ placement.col = cursorCol
102
+ occupy(cursorRow, cursorCol, placement.rowSpan, placement.colSpan)
103
+ cursorCol += placement.colSpan
104
+ if (cursorCol >= columnCount) {
105
+ cursorRow++
106
+ cursorCol = 0
107
+ }
108
+ }
109
+
110
+ return {
111
+ cells: placements.map(({ row, col }) => ({ row: row!, col: col! })),
112
+ rowCount: Math.max(trackCount(rowTracks), ...placements.map((item) => item.row! + item.rowSpan)),
113
+ columnCount: Math.max(columnCount, ...placements.map((item) => item.col! + item.colSpan)),
114
+ }
115
+ }
116
+
117
+ export function ensureGridTracks(spec: string | undefined, requiredCount: number): string {
118
+ const tracks = spec?.split(',').map((track) => track.trim()).filter(Boolean) ?? []
119
+ while (tracks.length < requiredCount) tracks.push('auto')
120
+ return tracks.join(',')
121
+ }
@@ -0,0 +1,29 @@
1
+ import type { LayoutChildProps } from './props'
2
+
3
+ export type GridChildPlacement = Pick<LayoutChildProps, 'row' | 'col' | 'rowSpan' | 'colSpan'>
4
+
5
+ const placements = new WeakMap<object, GridChildPlacement>()
6
+
7
+ /** Keep the authored Grid placement next to its mounted NativeScript view. */
8
+ export function rememberGridChildPlacement(view: unknown, props: GridChildPlacement): void {
9
+ if ((typeof view !== 'object' || view === null) && typeof view !== 'function') {
10
+ return
11
+ }
12
+
13
+ const placement: GridChildPlacement = {}
14
+ for (const key of ['row', 'col', 'rowSpan', 'colSpan'] as const) {
15
+ if (props[key] !== undefined) {
16
+ placement[key] = props[key]
17
+ }
18
+ }
19
+
20
+ if (Object.keys(placement).length) {
21
+ placements.set(view as object, placement)
22
+ } else {
23
+ placements.delete(view as object)
24
+ }
25
+ }
26
+
27
+ export function gridChildPlacement(view: unknown): GridChildPlacement {
28
+ return typeof view === 'object' && view !== null ? placements.get(view) ?? {} : {}
29
+ }
@@ -6,7 +6,7 @@ import { topRootLayout } from './root-layout.mobile'
6
6
  import { applyThemeClasses } from './theme/theme-scheme'
7
7
  import { attachSheetDetents } from './sheet-detents'
8
8
  import { bindBottomInsetToKeyboard } from './keyboard-inset.mobile'
9
- import { attachTapToBlur } from './tap-to-blur.mobile'
9
+ import { attachTapToBlur } from './tap-to-blur'
10
10
 
11
11
  interface ActiveSheet {
12
12
  host: GridLayout