@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
@@ -0,0 +1,90 @@
1
+ /** Tap-outside-to-blur: iOS keeps a focused UITextField/UITextView as first
2
+ * responder until something else takes it, so a tap on blank chrome never
3
+ * dismisses the keyboard on its own.
4
+ *
5
+ * iOS: a single window-level UITapGestureRecognizer (ref-counted across
6
+ * attach calls) sees every touch in the app window — popups/sheets included —
7
+ * where a recognizer on an intermediate container proved unreliable. On each
8
+ * tap we hit-test and resign first responder unless the hit view is an
9
+ * editable text view (otherwise the field would focus-then-blur in the same
10
+ * tap — recognizers fire on touch-up, after the field's touch-down focus).
11
+ * Keyboard taps live in a separate UIWindow, so they never reach it.
12
+ * `cancelsTouchesInView` stays false so taps still reach buttons. */
13
+ export function attachTapToBlur(view: any): () => void {
14
+ const marker = '__xplatTapToBlur';
15
+ if (!view || view[marker]) {return () => {};}
16
+ view[marker] = true;
17
+
18
+ iosAttach();
19
+ return () => {
20
+ view[marker] = false;
21
+ iosDetach();
22
+ };
23
+ }
24
+
25
+ let iosCount = 0;
26
+ let iosTarget: any = null;
27
+ let iosRecognizer: any = null;
28
+ let iosWindow: any = null;
29
+
30
+ function iosAttach() {
31
+ iosCount++;
32
+ if (iosRecognizer) {return;}
33
+ const appWin = appWindow();
34
+ if (!appWin) {return;}
35
+ iosWindow = appWin;
36
+ // Resolve Objective-C bridge globals only when the iOS view is attached.
37
+ @NativeClass
38
+ class TapToBlurRecognizerTarget extends NSObject implements UIGestureRecognizerDelegate {
39
+ static ObjCProtocols = [UIGestureRecognizerDelegate]
40
+ static ObjCExposedMethods = {
41
+ tap: { returns: interop.types.void, params: [UITapGestureRecognizer] },
42
+ };
43
+
44
+ gestureRecognizerShouldReceiveTouch(_recognizer: any, touch: any) {
45
+ let hit = touch.view
46
+ while (hit) {
47
+ const textField = hit.isKindOfClass(UITextField.class())
48
+ const textView = hit.isKindOfClass(UITextView.class())
49
+ if (textField || textView) {
50
+ return false
51
+ }
52
+ hit = hit.superview
53
+ }
54
+ return true
55
+ }
56
+
57
+ tap(_recognizer: any) {
58
+ iosWindow.endEditing(true);
59
+ }
60
+ }
61
+
62
+ iosTarget = TapToBlurRecognizerTarget.new();
63
+ iosRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(iosTarget, 'tap');
64
+ iosRecognizer.delegate = iosTarget;
65
+ iosRecognizer.cancelsTouchesInView = false;
66
+ appWin.addGestureRecognizer(iosRecognizer);
67
+ }
68
+
69
+ function iosDetach() {
70
+ iosCount--;
71
+ if (iosCount > 0 || !iosRecognizer) {return;}
72
+ iosWindow?.removeGestureRecognizer(iosRecognizer);
73
+ iosRecognizer = null;
74
+ iosTarget = null;
75
+ iosWindow = null;
76
+ }
77
+
78
+ function appWindow() {
79
+ const scenes = UIApplication.sharedApplication.connectedScenes;
80
+ // connectedScenes is an NSSet — `allObjects` marshals to a JS array.
81
+ for (const scene of (scenes as any).allObjects ?? []) {
82
+ if (scene instanceof UIWindowScene) {
83
+ for (const w of (scene.windows as any)?.allObjects ?? scene.windows ?? []) {
84
+ if (w.isKeyWindow) {return w;}
85
+ }
86
+ }
87
+ }
88
+
89
+ return UIApplication.sharedApplication.keyWindow;
90
+ }
@@ -1,108 +1,26 @@
1
1
  import { Application } from '@nativescript/core';
2
2
 
3
- /** Tap-outside-to-blur: iOS keeps a focused UITextField/UITextView as first
4
- * responder until something else takes it, so a tap on blank chrome never
5
- * dismisses the keyboard on its own.
6
- *
7
- * iOS: a single window-level UITapGestureRecognizer (ref-counted across
8
- * attach calls) sees every touch in the app window — popups/sheets included —
9
- * where a recognizer on an intermediate container proved unreliable. On each
10
- * tap we hit-test and resign first responder unless the hit view is an
11
- * editable text view (otherwise the field would focus-then-blur in the same
12
- * tap — recognizers fire on touch-up, after the field's touch-down focus).
13
- * Keyboard taps live in a separate UIWindow, so they never reach it.
14
- * `cancelsTouchesInView` stays false so taps still reach buttons.
15
- *
16
- * Android: hides the soft keyboard from the currently focused view when a
17
- * tap lands on the attached container. */
3
+ /** Hide the Android soft keyboard when the user taps the attached view. */
18
4
  export function attachTapToBlur(view: any): () => void {
19
5
  const marker = '__xplatTapToBlur';
20
6
  if (!view || view[marker]) {return () => {};}
21
7
  view[marker] = true;
22
8
 
23
- if (Application.ios) {
24
- iosAttach();
25
- return () => {
26
- view[marker] = false;
27
- iosDetach();
28
- };
9
+ if (!Application.android) {
10
+ return () => { view[marker] = false; };
29
11
  }
30
12
 
31
- if (Application.android) {
32
- const handler = (_args: any) => {
33
- const activity = Application.android.foregroundActivity;
34
- const focused = activity?.currentFocus;
35
- if (!focused) {return;}
36
- const imm = activity.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
37
- imm?.hideSoftInputFromWindow(focused.getWindowToken(), 0);
38
- };
39
-
40
- view.on('tap', handler);
41
- return () => {
42
- view[marker] = false;
43
- view.off('tap', handler);
44
- };
45
- }
13
+ const handler = (_args: any) => {
14
+ const activity = Application.android.foregroundActivity;
15
+ const focused = activity?.currentFocus;
16
+ if (!focused) {return;}
17
+ const imm = activity.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
18
+ imm?.hideSoftInputFromWindow(focused.getWindowToken(), 0);
19
+ };
46
20
 
21
+ view.on('tap', handler);
47
22
  return () => {
48
23
  view[marker] = false;
24
+ view.off('tap', handler);
49
25
  };
50
26
  }
51
-
52
- let iosCount = 0;
53
- let iosTarget: any = null;
54
- let iosRecognizer: any = null;
55
- let iosWindow: any = null;
56
-
57
- function iosAttach() {
58
- iosCount++;
59
- if (iosRecognizer) {return;}
60
- const appWin = appWindow();
61
- if (!appWin) {return;}
62
- iosWindow = appWin;
63
-
64
- const Owner = class extends NSObject {
65
- static ObjCExposedMethods = {
66
- tap: { returns: interop.types.void, params: [UITapGestureRecognizer] },
67
- };
68
- tap(recognizer: any) {
69
- const point = recognizer.locationInView(iosWindow);
70
- // hitTest can land on a text field's internal subview — walk up.
71
- let hit = iosWindow.hitTest(point, null);
72
- while (hit) {
73
- if (hit instanceof UITextField || hit instanceof UITextView) {return;}
74
- hit = hit.superview;
75
- }
76
-
77
- iosWindow.endEditing(true);
78
- }
79
- };
80
-
81
- iosTarget = Owner.new();
82
- iosRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(iosTarget, 'tap');
83
- iosRecognizer.cancelsTouchesInView = false;
84
- appWin.addGestureRecognizer(iosRecognizer);
85
- }
86
-
87
- function iosDetach() {
88
- iosCount--;
89
- if (iosCount > 0 || !iosRecognizer) {return;}
90
- iosWindow?.removeGestureRecognizer(iosRecognizer);
91
- iosRecognizer = null;
92
- iosTarget = null;
93
- iosWindow = null;
94
- }
95
-
96
- function appWindow() {
97
- const scenes = UIApplication.sharedApplication.connectedScenes;
98
- // connectedScenes is an NSSet — `allObjects` marshals to a JS array.
99
- for (const scene of (scenes as any).allObjects ?? []) {
100
- if (scene instanceof UIWindowScene) {
101
- for (const w of (scene.windows as any)?.allObjects ?? scene.windows ?? []) {
102
- if (w.isKeyWindow) {return w;}
103
- }
104
- }
105
- }
106
-
107
- return UIApplication.sharedApplication.keyWindow;
108
- }
@@ -168,6 +168,8 @@ img {
168
168
 
169
169
  .vx-stack > * {
170
170
  grid-area: 1 / 1;
171
+ horizontal-align: left;
172
+ vertical-align: top;
171
173
  }
172
174
 
173
175
  /* xplat-web-only:start — Absolute's native leaf is absolutelayout; CSS
@@ -316,6 +318,11 @@ img {
316
318
  background-color: #000;
317
319
  }
318
320
 
321
+ .vx-video > * {
322
+ horizontal-align: stretch;
323
+ vertical-align: stretch;
324
+ }
325
+
319
326
  .vx-video-centerbtn {
320
327
  width: 56px;
321
328
  height: 56px;
@@ -617,6 +624,9 @@ button {
617
624
  line-height: 20px;
618
625
  color: var(--color-onprimary);
619
626
  }
627
+ .vx-checkbox--on .vx-checkbox-check {
628
+ color: var(--color-onprimary);
629
+ }
620
630
  .vx-checkbox-label {
621
631
  margin-left: 8px;
622
632
  }
@@ -1212,10 +1222,6 @@ input.vx-search-field::-webkit-search-cancel-button {
1212
1222
  /* CommandPalette — panel centered by the overlay layer. */
1213
1223
  .vx-commandpalette {
1214
1224
  width: 360px;
1215
- /* vw, not %: the overlay-content wrapper shrink-wraps the palette, so a
1216
- * plain % would resolve against the palette's own width (clamping an
1217
- * explicit style.width to 90% of itself). */
1218
- max-width: 90vw;
1219
1225
  padding: 8px;
1220
1226
  border-width: 1px;
1221
1227
  border-color: var(--color-border);
@@ -1224,6 +1230,13 @@ input.vx-search-field::-webkit-search-cancel-button {
1224
1230
  background-color: var(--color-surface);
1225
1231
  color: var(--color-text);
1226
1232
  }
1233
+ /* xplat-web-only:start — native uses Screen.mainScreen.widthDIPs */
1234
+ .vx-commandpalette {
1235
+ /* The overlay-content wrapper shrink-wraps the palette, so % would resolve
1236
+ * against the palette itself instead of the available viewport. */
1237
+ max-width: 90vw;
1238
+ }
1239
+ /* xplat-web-only:end */
1227
1240
  .vx-cmdk-search {
1228
1241
  margin-bottom: 4px;
1229
1242
  }
@@ -1615,25 +1628,28 @@ input.vx-search-field::-webkit-search-cancel-button {
1615
1628
  gap: 24px;
1616
1629
  }
1617
1630
 
1618
- /* line-height is REQUIRED on native: without it a Label's measured height
1619
- misses the font's line metrics, and Android clips the descenders/bottom
1620
- of the glyphs (observed: section-labels 21–29px bounds for 12sp text at
1621
- 3.25x). Tailwind pairs every text-* size with a leading, same here. */
1631
+ /* Native Labels keep their natural font height even with line-height set, so
1632
+ min-height aligns utilities whose natural metrics fall below their leading.
1633
+ Tailwind pairs every text-* size with a leading, same here. */
1622
1634
  .text-2xl {
1623
1635
  font-size: 28px;
1624
1636
  line-height: 36px;
1637
+ min-height: 36px;
1625
1638
  }
1626
1639
  .text-xl {
1627
1640
  font-size: 20px;
1628
1641
  line-height: 28px;
1642
+ min-height: 28px;
1629
1643
  }
1630
1644
  .text-lg {
1631
1645
  font-size: 18px;
1632
1646
  line-height: 28px;
1647
+ min-height: 28px;
1633
1648
  }
1634
1649
  .text-sm {
1635
1650
  font-size: 13px;
1636
1651
  line-height: 20px;
1652
+ min-height: 20px;
1637
1653
  }
1638
1654
  .font-bold {
1639
1655
  font-weight: bold;
@@ -1,5 +1,5 @@
1
1
  /** @jsxImportSource @nativescript-community/octane */
2
- import { useCallback, useEffect, useState } from 'octane';
2
+ import { useCallback, useEffect, useRef, useState } from 'octane';
3
3
  import type { MeasureBounds, MeasureResult, UseMeasureOptions } from './props';
4
4
 
5
5
  function readBounds(view: any): MeasureBounds | null {
@@ -11,14 +11,12 @@ function readBounds(view: any): MeasureBounds | null {
11
11
 
12
12
  /** Binds to a NativeScript view and observes screen-relative DIP bounds by default. */
13
13
  export function useMeasure(options: UseMeasureOptions = {}): MeasureResult {
14
- const [view, setView] = useState<any>(null);
15
14
  const [bounds, setBounds] = useState<MeasureBounds | null>(null);
15
+ const detach = useRef<(() => void) | null>(null);
16
16
  const observe = options.observe ?? true;
17
- const bind = useCallback((next: any) => {
18
- setView((current: any) => current === next ? current : next);
19
- }, []);
20
-
21
- useEffect(() => {
17
+ const bind = useCallback((view: any) => {
18
+ detach.current?.();
19
+ detach.current = null;
22
20
  if (!view) {
23
21
  setBounds(null);
24
22
  return;
@@ -28,39 +26,40 @@ export function useMeasure(options: UseMeasureOptions = {}): MeasureResult {
28
26
  const next = readBounds(view);
29
27
  // Fresh object each read — skip the setState when nothing moved
30
28
  // or the re-render → layoutChanged → update loop never ends.
31
- setBounds((prev: MeasureBounds | null) =>
32
- prev && next &&
33
- prev.x === next.x && prev.y === next.y &&
34
- prev.width === next.width && prev.height === next.height
29
+ setBounds((prev: MeasureBounds | null) => {
30
+ return prev && next &&
31
+ prev.x === next.x && prev.y === next.y &&
32
+ prev.width === next.width && prev.height === next.height
35
33
  ? prev
36
- : next,
37
- );
34
+ : next;
35
+ });
38
36
  };
39
37
 
40
- const onLoaded = update;
41
- view.on?.('loaded', onLoaded);
38
+ view.on?.('loaded', update);
42
39
  update();
40
+ const scrollParents: any[] = [];
43
41
  if (observe) {
44
42
  view.on?.('layoutChanged', update);
45
43
  let parent = view.parent;
46
44
  while (parent) {
47
45
  parent.on?.('scroll', update);
46
+ scrollParents.push(parent);
48
47
  parent = parent.parent;
49
48
  }
50
49
  }
51
50
 
52
- return () => {
53
- view.off?.('loaded', onLoaded);
51
+ detach.current = () => {
52
+ view.off?.('loaded', update);
54
53
  if (observe) {
55
54
  view.off?.('layoutChanged', update);
56
- let parent = view.parent;
57
- while (parent) {
55
+ for (const parent of scrollParents) {
58
56
  parent.off?.('scroll', update);
59
- parent = parent.parent;
60
57
  }
61
58
  }
62
59
  };
63
- }, [view, observe]);
60
+ }, [observe]);
61
+
62
+ useEffect(() => () => detach.current?.(), []);
64
63
 
65
64
  return { bind, bounds };
66
65
  }
@@ -169,6 +169,8 @@
169
169
  "cx.d.ts",
170
170
  "escape-props.d.ts",
171
171
  "escape-props.web.d.ts",
172
+ "grid-auto-placement.d.ts",
173
+ "grid-placement.d.ts",
172
174
  "icons.d.ts",
173
175
  "index.d.ts",
174
176
  "index.linux.d.ts",
@@ -223,6 +225,7 @@
223
225
  "styled.d.ts",
224
226
  "styled.web.d.ts",
225
227
  "svg.mobile.d.ts",
228
+ "tap-to-blur.ios.d.ts",
226
229
  "tap-to-blur.mobile.d.ts",
227
230
  "text-write.d.ts",
228
231
  "text-write.web.d.ts",
@@ -1,3 +1,2 @@
1
- /** @jsxImportSource @nativescript-community/octane */
2
1
  import type { GridProps } from './props.js';
3
2
  export declare function Grid(props: GridProps): unknown;
@@ -4,6 +4,10 @@ export declare function applyEscapeProps(view: any, props: {
4
4
  ios?: any;
5
5
  android?: any;
6
6
  modifiers?: readonly NativeModifier[];
7
+ row?: number;
8
+ col?: number;
9
+ rowSpan?: number;
10
+ colSpan?: number;
7
11
  }): void;
8
12
  /** Translate the shared/ARIA spelling to NativeScript's narrower role enum. */
9
13
  export declare function nativeAccessibilityRole(role?: Role): string | undefined;
@@ -0,0 +1,13 @@
1
+ import type { GridChildPlacement } from './grid-placement.js';
2
+ export interface GridCell {
3
+ row: number;
4
+ col: number;
5
+ }
6
+ export interface GridAutoPlacement {
7
+ cells: GridCell[];
8
+ rowCount: number;
9
+ columnCount: number;
10
+ }
11
+ /** Match CSS Grid's row-flow placement for NativeScript GridLayout children. */
12
+ export declare function autoPlaceGridChildren(items: readonly GridChildPlacement[], rowTracks?: string, columnTracks?: string): GridAutoPlacement;
13
+ export declare function ensureGridTracks(spec: string | undefined, requiredCount: number): string;
@@ -0,0 +1,5 @@
1
+ import type { LayoutChildProps } from './props.js';
2
+ export type GridChildPlacement = Pick<LayoutChildProps, 'row' | 'col' | 'rowSpan' | 'colSpan'>;
3
+ /** Keep the authored Grid placement next to its mounted NativeScript view. */
4
+ export declare function rememberGridChildPlacement(view: unknown, props: GridChildPlacement): void;
5
+ export declare function gridChildPlacement(view: unknown): GridChildPlacement;
@@ -0,0 +1,13 @@
1
+ /** Tap-outside-to-blur: iOS keeps a focused UITextField/UITextView as first
2
+ * responder until something else takes it, so a tap on blank chrome never
3
+ * dismisses the keyboard on its own.
4
+ *
5
+ * iOS: a single window-level UITapGestureRecognizer (ref-counted across
6
+ * attach calls) sees every touch in the app window — popups/sheets included —
7
+ * where a recognizer on an intermediate container proved unreliable. On each
8
+ * tap we hit-test and resign first responder unless the hit view is an
9
+ * editable text view (otherwise the field would focus-then-blur in the same
10
+ * tap — recognizers fire on touch-up, after the field's touch-down focus).
11
+ * Keyboard taps live in a separate UIWindow, so they never reach it.
12
+ * `cancelsTouchesInView` stays false so taps still reach buttons. */
13
+ export declare function attachTapToBlur(view: any): () => void;
@@ -1,16 +1,2 @@
1
- /** Tap-outside-to-blur: iOS keeps a focused UITextField/UITextView as first
2
- * responder until something else takes it, so a tap on blank chrome never
3
- * dismisses the keyboard on its own.
4
- *
5
- * iOS: a single window-level UITapGestureRecognizer (ref-counted across
6
- * attach calls) sees every touch in the app window — popups/sheets included —
7
- * where a recognizer on an intermediate container proved unreliable. On each
8
- * tap we hit-test and resign first responder unless the hit view is an
9
- * editable text view (otherwise the field would focus-then-blur in the same
10
- * tap — recognizers fire on touch-up, after the field's touch-down focus).
11
- * Keyboard taps live in a separate UIWindow, so they never reach it.
12
- * `cancelsTouchesInView` stays false so taps still reach buttons.
13
- *
14
- * Android: hides the soft keyboard from the currently focused view when a
15
- * tap lands on the attached container. */
1
+ /** Hide the Android soft keyboard when the user taps the attached view. */
16
2
  export declare function attachTapToBlur(view: any): () => void;