@octane-xplat/ui 0.7.2 → 0.9.0

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 (219) hide show
  1. package/README.md +4 -3
  2. package/dist/native/Button.tsrx.js +1 -1
  3. package/dist/native/CommandPalette.tsrx.js +7 -1
  4. package/dist/native/Grid.tsrx.js +31 -2
  5. package/dist/native/KeyboardAvoiding.mobile.tsrx.js +2 -2
  6. package/dist/native/Markdown.tsrx.js +233 -0
  7. package/dist/native/Overlay.tsrx.js +15 -26
  8. package/dist/native/Pagination.tsrx.js +15 -12
  9. package/dist/native/PinInput.tsrx.js +25 -7
  10. package/dist/native/Popover.tsrx.js +33 -23
  11. package/dist/native/Pressable.tsrx.js +4 -1
  12. package/dist/native/Screen.tsrx.js +1 -1
  13. package/dist/native/SearchInput.tsrx.js +6 -3
  14. package/dist/native/Sheet.tsrx.js +23 -38
  15. package/dist/native/Slider.tsrx.js +23 -14
  16. package/dist/native/Text.tsrx.js +16 -0
  17. package/dist/native/TextArea.tsrx.js +6 -3
  18. package/dist/native/TextInput.tsrx.js +6 -3
  19. package/dist/native/VirtualList.tsrx.js +305 -121
  20. package/dist/native/WebView.tsrx.js +17 -2
  21. package/dist/native/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
  22. package/dist/native/escape-props.js +2 -0
  23. package/dist/native/grid-auto-placement.js +83 -0
  24. package/dist/native/grid-placement.js +20 -0
  25. package/dist/native/index.shared.js +7 -5
  26. package/dist/native/keyboard-inset.mobile.js +5 -5
  27. package/dist/native/navigation-request.js +26 -0
  28. package/dist/native/octane-client-build.json +1 -1
  29. package/dist/native/overlay-lifetime.mobile.js +65 -0
  30. package/dist/native/pan.tsrx.js +16 -2
  31. package/dist/native/route-table.js +90 -10
  32. package/dist/native/route.js +26 -16
  33. package/dist/native/shared.js +7 -5
  34. package/dist/native/sheet-service.js +2 -2
  35. package/dist/native/svg.mobile.js +1 -1
  36. package/dist/native/{tap-to-blur.mobile.js → tap-to-blur.ios.js} +20 -32
  37. package/dist/native/text-focus.js +10 -0
  38. package/dist/native/text-write.js +18 -11
  39. package/dist/native/toast-anchor.tsrx.js +1 -1
  40. package/dist/native/ui.js +7 -6
  41. package/dist/native/useMeasure.tsrx.js +17 -18
  42. package/dist/native/vendor/ui-svg/index.android.js +2 -2
  43. package/dist/native/vendor/ui-svg/index.ios.js +2 -2
  44. package/dist/native/vendor/ui-svg/{index.common.js → src/index.common.js} +1 -1
  45. package/dist/native/virtual-list-cell-pool.js +63 -0
  46. package/dist/web/Button.web.tsrx.js +1 -1
  47. package/dist/web/KeyboardAvoiding.web.tsrx.js +78 -0
  48. package/dist/web/Markdown.tsrx.js +98 -0
  49. package/dist/web/Overlay.web.tsrx.js +81 -32
  50. package/dist/web/PinInput.web.tsrx.js +24 -10
  51. package/dist/web/Pressable.web.tsrx.js +169 -53
  52. package/dist/web/SearchInput.web.tsrx.js +2 -2
  53. package/dist/web/Sheet.web.tsrx.js +20 -7
  54. package/dist/web/TextArea.web.tsrx.js +22 -17
  55. package/dist/web/TextInput.web.tsrx.js +2 -2
  56. package/dist/web/VirtualList.web.tsrx.js +220 -109
  57. package/dist/web/WebView.web.tsrx.js +3 -3
  58. package/dist/web/{index.shared.js → index.shared.web.js} +7 -5
  59. package/dist/web/modal-focus.web.js +95 -0
  60. package/dist/web/navigation-request.js +26 -0
  61. package/dist/web/octane-client-build.json +1 -1
  62. package/dist/web/pan.web.tsrx.js +27 -10
  63. package/dist/web/route-table.js +90 -10
  64. package/dist/web/route.web.js +88 -22
  65. package/dist/web/sheet-service.web.js +8 -0
  66. package/dist/web/theme/colorScheme.web.js +31 -4
  67. package/dist/web/ui.js +8 -6
  68. package/dist/web/virtual-list-cell-pool.js +63 -0
  69. package/package.json +21 -19
  70. package/platforms/android/native-api-usage.json +7 -7
  71. package/src/CommandPalette.tsrx +8 -1
  72. package/src/Grid.tsrx +32 -1
  73. package/src/Icon.macos.tsrx +1 -0
  74. package/src/KeyboardAvoiding.macos.tsrx +7 -1
  75. package/src/KeyboardAvoiding.mobile.tsrx +2 -2
  76. package/src/KeyboardAvoiding.tsrx +15 -0
  77. package/src/KeyboardAvoiding.web.tsrx +15 -0
  78. package/src/Markdown.tsrx +121 -0
  79. package/src/Meter.tsrx +1 -0
  80. package/src/Overlay.tsrx +15 -30
  81. package/src/Overlay.web.tsrx +12 -3
  82. package/src/Pagination.tsrx +6 -4
  83. package/src/PinInput.macos.tsrx +20 -6
  84. package/src/PinInput.mobile.test.ts +107 -0
  85. package/src/PinInput.tsrx +20 -6
  86. package/src/PinInput.web.test.tsrx +42 -0
  87. package/src/PinInput.web.tsrx +20 -6
  88. package/src/Popover.tsrx +36 -22
  89. package/src/Pressable.tsrx +1 -1
  90. package/src/Pressable.web.test.tsrx +36 -0
  91. package/src/Pressable.web.tsrx +37 -6
  92. package/src/RouteHost.macos.tsrx +1 -0
  93. package/src/Screen.tsrx +1 -1
  94. package/src/SearchInput.tsrx +4 -3
  95. package/src/SearchInput.web.tsrx +1 -1
  96. package/src/Sheet.tsrx +21 -42
  97. package/src/Sheet.web.tsrx +9 -1
  98. package/src/Slider.tsrx +13 -11
  99. package/src/Text.tsrx +36 -0
  100. package/src/TextArea.tsrx +6 -5
  101. package/src/TextArea.web.test.tsrx +39 -0
  102. package/src/TextArea.web.tsrx +7 -5
  103. package/src/TextInput.tsrx +6 -6
  104. package/src/TextInput.web.tsrx +1 -1
  105. package/src/VirtualList.macos.tsrx +93 -6
  106. package/src/VirtualList.mobile.test.tsrx +232 -0
  107. package/src/VirtualList.tsrx +285 -81
  108. package/src/VirtualList.web.tsrx +131 -25
  109. package/src/WebView.tsrx +20 -10
  110. package/src/WebView.web.tsrx +3 -0
  111. package/src/android/BottomNavigationView.android.tsrx +1 -0
  112. package/src/android/MaterialDialog.android.tsrx +1 -0
  113. package/src/anim.macos.tsrx +1 -0
  114. package/src/apply-native-modifiers.native.ts +4 -1
  115. package/src/child-array.ts +12 -7
  116. package/src/colorScheme.macos.ts +1 -0
  117. package/src/controls.macos.tsrx +4 -1
  118. package/src/data-driver.mobile.test.ts +120 -0
  119. package/src/data-lifecycle.mobile.test.ts +251 -0
  120. package/src/escape-props.ts +13 -1
  121. package/src/grid-auto-placement.ts +163 -0
  122. package/src/grid-placement.ts +29 -0
  123. package/src/hosted-unsupported.macos.tsrx +1 -0
  124. package/src/index.macos.ts +23 -3
  125. package/src/index.mobile.ts +0 -1
  126. package/src/index.shared.ts +22 -1
  127. package/src/index.shared.web.ts +291 -0
  128. package/src/index.web.ts +1 -1
  129. package/src/input-readiness.web.test.tsrx +82 -0
  130. package/src/ios/UITabBar.ios.tsrx +1 -0
  131. package/src/keyboard-inset.mobile.test.ts +62 -0
  132. package/src/keyboard-inset.mobile.ts +63 -43
  133. package/src/layout-shells.macos.tsrx +1 -5
  134. package/src/macos-extras.macos.tsrx +2 -0
  135. package/src/modal-focus.web.test.tsrx +63 -0
  136. package/src/modal-focus.web.ts +162 -0
  137. package/src/navigation-request.ts +37 -0
  138. package/src/overlay-lifecycle.mobile.test.ts +316 -0
  139. package/src/overlay-lifetime.mobile.ts +107 -0
  140. package/src/pan.mobile.test.tsrx +116 -0
  141. package/src/pan.tsrx +20 -2
  142. package/src/pan.web.tsrx +35 -7
  143. package/src/platformProps.ts +1 -0
  144. package/src/popover.mobile.test.ts +9 -1
  145. package/src/pressable-accessibility.mobile.test.ts +30 -0
  146. package/src/props.ts +163 -3
  147. package/src/refresh.ts +6 -1
  148. package/src/refresh.web.test.ts +77 -77
  149. package/src/route-android.test.ts +162 -0
  150. package/src/route-history.web.test.ts +80 -0
  151. package/src/route-navigation.web.test.ts +173 -0
  152. package/src/route-table.test.ts +188 -9
  153. package/src/route-table.ts +138 -10
  154. package/src/route.macos.ts +56 -13
  155. package/src/route.ts +54 -24
  156. package/src/route.web.ts +141 -24
  157. package/src/sheet-detents.ts +5 -3
  158. package/src/sheet-service.macos.ts +5 -1
  159. package/src/sheet-service.ts +1 -1
  160. package/src/sheet-service.web.ts +8 -0
  161. package/src/sheet-snap.test.ts +23 -23
  162. package/src/sheet-snap.ts +6 -1
  163. package/src/svg.mobile.ts +9 -3
  164. package/src/tap-to-blur.ios.ts +106 -0
  165. package/src/tap-to-blur.mobile.ts +21 -95
  166. package/src/text-focus.ts +12 -0
  167. package/src/text-input.mobile.test.ts +89 -0
  168. package/src/text-write.ts +21 -11
  169. package/src/theme/colorScheme.linux.ts +2 -4
  170. package/src/theme/colorScheme.web.ts +51 -4
  171. package/src/theme/theme-scheme.macos.ts +8 -2
  172. package/src/theme/tokens.css +1740 -1718
  173. package/src/translate.macos.ts +4 -1
  174. package/src/useMeasure.tsrx +20 -21
  175. package/src/virtual-list-cell-pool.test.ts +53 -0
  176. package/src/virtual-list-cell-pool.ts +81 -0
  177. package/src/virtual-list-layout.test.ts +11 -8
  178. package/src/virtual-list-layout.ts +4 -8
  179. package/src/windows.linux.ts +7 -2
  180. package/src/windows.macos.ts +1 -0
  181. package/types/generated/.tsrx-typegen-manifest.json +16 -3
  182. package/types/generated/Grid.d.ts +0 -1
  183. package/types/generated/KeyboardAvoiding.d.ts +3 -0
  184. package/types/generated/KeyboardAvoiding.web.d.ts +2 -0
  185. package/types/generated/Markdown.d.ts +59 -0
  186. package/types/generated/TextArea.d.ts +2 -2
  187. package/types/generated/TextArea.web.d.ts +1 -1
  188. package/types/generated/TextInput.d.ts +2 -3
  189. package/types/generated/VirtualList.d.ts +3 -3
  190. package/types/generated/escape-props.d.ts +4 -0
  191. package/types/generated/grid-auto-placement.d.ts +13 -0
  192. package/types/generated/grid-placement.d.ts +5 -0
  193. package/types/generated/index.mobile.d.ts +0 -1
  194. package/types/generated/index.shared.d.ts +6 -3
  195. package/types/generated/index.shared.web.d.ts +114 -0
  196. package/types/generated/index.web.d.ts +1 -1
  197. package/types/generated/modal-focus.web.d.ts +3 -0
  198. package/types/generated/navigation-request.d.ts +10 -0
  199. package/types/generated/overlay-lifetime.mobile.d.ts +10 -0
  200. package/types/generated/props.d.ts +111 -3
  201. package/types/generated/route-table.d.ts +14 -3
  202. package/types/generated/svg.mobile.d.ts +2 -2
  203. package/types/generated/tap-to-blur.ios.d.ts +13 -0
  204. package/types/generated/tap-to-blur.mobile.d.ts +1 -15
  205. package/types/generated/text-focus.d.ts +3 -0
  206. package/types/generated/text-write.d.ts +3 -1
  207. package/types/generated/theme/colorScheme.web.d.ts +5 -0
  208. package/types/generated/virtual-list-cell-pool.d.ts +18 -0
  209. package/types/index.macos.d.ts +15 -3
  210. /package/src/vendor/ui-svg/{index.android.ts → src/index.android.ts} +0 -0
  211. /package/src/vendor/ui-svg/{index.common.ts → src/index.common.ts} +0 -0
  212. /package/src/vendor/ui-svg/{index.ios.ts → src/index.ios.ts} +0 -0
  213. /package/src/vendor/ui-svg/{index.ts → src/index.ts} +0 -0
  214. /package/src/vendor/ui-svg/{typings → src/typings}/SVGKit.d.ts +0 -0
  215. /package/src/vendor/ui-svg/{typings → src/typings}/android.d.ts +0 -0
  216. /package/src/vendor/ui-svg/{typings → src/typings}/ios.d.ts +0 -0
  217. /package/types/generated/vendor/ui-svg/{index.common.d.ts → src/index.common.d.ts} +0 -0
  218. /package/types/generated/vendor/ui-svg/{index.d.ts → src/index.d.ts} +0 -0
  219. /package/types/generated/vendor/ui-svg/{index.ios.d.ts → src/index.ios.d.ts} +0 -0
@@ -1,12 +1,39 @@
1
1
  import { hookSlots, useSyncExternalStore } from "octane";
2
2
  //#region src/theme/colorScheme.web.ts
3
- function getColorScheme() {
3
+ var hostTransport = () => typeof window === "undefined" ? null : window.__xplatHostTransport ?? null;
4
+ var hostedScheme = typeof window === "undefined" ? "light" : window.__xplatHostSnapshot?.colorScheme ?? window.__xplatColorScheme ?? "light";
5
+ function getSystemScheme() {
4
6
  return typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
5
7
  }
8
+ function getColorScheme() {
9
+ return hostTransport() ? hostedScheme : getSystemScheme();
10
+ }
6
11
  function subscribeSystemScheme(cb) {
7
- const mq = matchMedia("(prefers-color-scheme: dark)");
8
- mq.addEventListener("change", cb);
9
- return () => mq.removeEventListener("change", cb);
12
+ const transport = hostTransport();
13
+ if (!transport) {
14
+ const mq = matchMedia("(prefers-color-scheme: dark)");
15
+ mq.addEventListener("change", cb);
16
+ return () => mq.removeEventListener("change", cb);
17
+ }
18
+ const onTransportMessage = (message) => {
19
+ try {
20
+ const packet = JSON.parse(message);
21
+ if (packet.type === "event" && packet.name === "appearance.change") {
22
+ hostedScheme = packet.payload === "dark" ? "dark" : "light";
23
+ cb();
24
+ }
25
+ } catch {}
26
+ };
27
+ const onCompatEvent = (event) => {
28
+ hostedScheme = event.detail === "dark" ? "dark" : "light";
29
+ cb();
30
+ };
31
+ const offTransport = transport.listen(onTransportMessage);
32
+ window.addEventListener("xplat:appearance.change", onCompatEvent);
33
+ return () => {
34
+ offTransport();
35
+ window.removeEventListener("xplat:appearance.change", onCompatEvent);
36
+ };
10
37
  }
11
38
  function useColorScheme() {
12
39
  return useSyncExternalStore(subscribeSystemScheme, getColorScheme, _h$0);
package/dist/web/ui.js CHANGED
@@ -6,9 +6,12 @@ import { Absolute } from "./Absolute.web.tsrx.js";
6
6
  import { setTranslate } from "./translate.web.js";
7
7
  import { Spacer } from "./Spacer.web.tsrx.js";
8
8
  import { Text } from "./Text.web.tsrx.js";
9
+ import { Screen } from "./Screen.web.tsrx.js";
10
+ import { ActivityIndicator } from "./ActivityIndicator.web.tsrx.js";
11
+ import { ScrollView } from "./ScrollView.web.tsrx.js";
12
+ import { Markdown, MarkdownScreen } from "./Markdown.tsrx.js";
9
13
  import { RichText, RichTextSpan } from "./RichText.web.tsrx.js";
10
14
  import { Pressable } from "./Pressable.web.tsrx.js";
11
- import { ActivityIndicator } from "./ActivityIndicator.web.tsrx.js";
12
15
  import { Button } from "./Button.web.tsrx.js";
13
16
  import { Collapsible } from "./Collapsible.web.tsrx.js";
14
17
  import { Accordion } from "./Accordion.web.tsrx.js";
@@ -56,13 +59,12 @@ import { User } from "./User.web.tsrx.js";
56
59
  import { Meter } from "./Meter.web.tsrx.js";
57
60
  import { ProgressGroup } from "./ProgressGroup.web.tsrx.js";
58
61
  import { Link } from "./Link.web.tsrx.js";
59
- import { defineRoutes, deriveRouteManifest, mergeRouteManifests } from "./route-table.js";
62
+ import { defineRoutes, deriveRouteManifest, manifestToJson, mergeRouteManifests } from "./route-table.js";
60
63
  import { addBackInterceptor, addRoutes, canGoBack, currentModalRoute, currentRoute, hrefFor, layoutsFor, popRoute, pushDeepLink, pushRoute, redirect, registerRoutes, registerScreens, routeFor, routeStacks, screenFor, useCanGoBack, useModalRoute, useRoute } from "./route.web.js";
61
64
  import { NavLink } from "./NavLink.web.tsrx.js";
62
65
  import { TextArea } from "./TextArea.web.tsrx.js";
63
66
  import { SearchInput } from "./SearchInput.web.tsrx.js";
64
67
  import { SegmentedControl } from "./SegmentedControl.web.tsrx.js";
65
- import { ScrollView } from "./ScrollView.web.tsrx.js";
66
68
  import { ScrollBox } from "./ScrollBox.web.tsrx.js";
67
69
  import { VirtualList } from "./VirtualList.web.tsrx.js";
68
70
  import { Image } from "./Image.web.tsrx.js";
@@ -73,11 +75,11 @@ import { showToast } from "./toast-anchor.web.tsrx.js";
73
75
  import { useAnimation } from "./anim.web.tsrx.js";
74
76
  import { styled } from "./styled.web.tsrx.js";
75
77
  import { openWindow } from "./windows.web.js";
76
- import { Screen } from "./Screen.web.tsrx.js";
77
78
  import { Tabs } from "./Tabs.web.tsrx.js";
78
79
  import { Switch } from "./Switch.web.tsrx.js";
79
80
  import { useSafeAreaInsets } from "./safeAreaInsets.web.tsrx.js";
80
81
  import { SafeArea } from "./SafeArea.web.tsrx.js";
82
+ import { KeyboardAvoiding } from "./KeyboardAvoiding.web.tsrx.js";
81
83
  import { Drawer } from "./Drawer.web.tsrx.js";
82
84
  import { useMeasure } from "./useMeasure.web.tsrx.js";
83
85
  import { Slider } from "./Slider.web.tsrx.js";
@@ -89,5 +91,5 @@ import { createStore } from "./store.js";
89
91
  import { useStore } from "./use-store.web.tsrx.js";
90
92
  import { Sheet } from "./Sheet.web.tsrx.js";
91
93
  import { closeSheet, openSheet } from "./sheet-service.web.js";
92
- import "./index.shared.js";
93
- export { Absolute, Accordion, ActivityIndicator, Alert, Avatar, AvatarGroup, Badge, Banner, Breadcrumb, Button, Card, Checkbox, CheckboxGroup, Chip, Collapsible, View as Column, Combobox, CommandPalette, ContextMenu, Drawer, DropdownMenu, Empty, FieldGroup, FormField, Grid, Heading, Hoverable, Icon, Image, InputMenu, InputNumber, InputRating, InputTags, Kbd, Link, ListItem, Meter, NavLink, NavigationMenu, Overlay, Pagination, PinInput, Popover, Pressable, ProgressGroup, RadioGroup, RichText, RichTextSpan, Row, SafeArea, Screen, ScrollBox, ScrollView, SearchInput, SegmentedControl, Select, SelectMenu, Separator, Sheet, Skeleton, Slider, Spacer, Stack, Stepper, Switch, Table, Tabs, Text, TextArea, TextInput, Timeline, Tooltip, Tree, User, View, VirtualList, WebView, addBackInterceptor, addRoutes, applyThemeClasses, canGoBack, closeSheet, createStore, currentModalRoute, currentRoute, defineRoutes, deriveRouteManifest, getColorScheme, getStack, getThemePreference, getThemeScheme, hrefFor, isNative, layoutsFor as layoutsForRoute, mergeRouteManifests, onThemeSchemeChange, openSheet, openWindow, popRoute, pushDeepLink, pushRoute, redirect, registerIcon, registerIcons, registerRoutes, registerScreens, registerStack, routeFor, routeStacks, screenFor, setThemePreference, setTranslate, showToast, stackEntries, styled, themeSchemeClasses, useAnimation, useBackInterceptor, useCanGoBack, useColorScheme, useMeasure, useModalRoute, useRoute, useSafeAreaInsets, useStore, useThemeScheme };
94
+ import "./index.shared.web.js";
95
+ export { Absolute, Accordion, ActivityIndicator, Alert, Avatar, AvatarGroup, Badge, Banner, Breadcrumb, Button, Card, Checkbox, CheckboxGroup, Chip, Collapsible, View as Column, Combobox, CommandPalette, ContextMenu, Drawer, DropdownMenu, Empty, FieldGroup, FormField, Grid, Heading, Hoverable, Icon, Image, InputMenu, InputNumber, InputRating, InputTags, Kbd, KeyboardAvoiding, Link, ListItem, Markdown, MarkdownScreen, Meter, NavLink, NavigationMenu, Overlay, Pagination, PinInput, Popover, Pressable, ProgressGroup, RadioGroup, RichText, RichTextSpan, Row, SafeArea, Screen, ScrollBox, ScrollView, SearchInput, SegmentedControl, Select, SelectMenu, Separator, Sheet, Skeleton, Slider, Spacer, Stack, Stepper, Switch, Table, Tabs, Text, TextArea, TextInput, Timeline, Tooltip, Tree, User, View, VirtualList, WebView, addBackInterceptor, addRoutes, applyThemeClasses, canGoBack, closeSheet, createStore, currentModalRoute, currentRoute, defineRoutes, deriveRouteManifest, getColorScheme, getStack, getThemePreference, getThemeScheme, hrefFor, isNative, layoutsFor as layoutsForRoute, manifestToJson, mergeRouteManifests, onThemeSchemeChange, openSheet, openWindow, popRoute, pushDeepLink, pushRoute, redirect, registerIcon, registerIcons, registerRoutes, registerScreens, registerStack, routeFor, routeStacks, screenFor, setThemePreference, setTranslate, showToast, stackEntries, styled, themeSchemeClasses, useAnimation, useBackInterceptor, useCanGoBack, useColorScheme, useMeasure, useModalRoute, useRoute, useSafeAreaInsets, useStore, useThemeScheme };
@@ -0,0 +1,63 @@
1
+ //#region src/virtual-list-cell-pool.ts
2
+ /** Reuse physical hosts without retaining an off-window logical row subtree. */
3
+ var VirtualListCellPool = class {
4
+ constructor() {
5
+ this.cells = [];
6
+ this.byId = /* @__PURE__ */ new Map();
7
+ this.nextId = 0;
8
+ }
9
+ update(entries) {
10
+ if (entries.length === 0) {
11
+ this.clear();
12
+ return this.cells;
13
+ }
14
+ const wanted = new Set(entries.map((entry) => entry.rowKey));
15
+ let cells = this.cells.map((cell) => cell.entry && !wanted.has(cell.entry.rowKey) ? {
16
+ ...cell,
17
+ entry: null,
18
+ rows: [],
19
+ generation: cell.generation + 1
20
+ } : cell);
21
+ const retained = new Map(cells.filter((cell) => cell.entry).map((cell) => [cell.entry.rowKey, cell.id]));
22
+ for (const entry of entries) {
23
+ const retainedId = retained.get(entry.rowKey);
24
+ let index = retainedId === void 0 ? cells.findIndex((cell) => !cell.entry && cell.typeKey === entry.typeKey) : cells.findIndex((cell) => cell.id === retainedId);
25
+ if (index === -1) {
26
+ index = cells.length;
27
+ cells.push({
28
+ id: this.nextId++,
29
+ typeKey: entry.typeKey,
30
+ generation: 0,
31
+ entry: null,
32
+ rows: []
33
+ });
34
+ }
35
+ const cell = cells[index];
36
+ if (cell.entry !== entry) cells[index] = {
37
+ ...cell,
38
+ entry,
39
+ rows: [entry],
40
+ generation: cell.generation + 1
41
+ };
42
+ }
43
+ let spare = 0;
44
+ cells = cells.filter((cell) => cell.entry || spare++ < 8);
45
+ cells.sort((a, b) => (a.entry?.index ?? Infinity) - (b.entry?.index ?? Infinity) || a.id - b.id);
46
+ this.cells = cells;
47
+ this.byId = new Map(cells.map((cell) => [cell.id, cell]));
48
+ return cells;
49
+ }
50
+ get(id) {
51
+ return this.byId.get(id);
52
+ }
53
+ isCurrent(id, rowKey, generation) {
54
+ const cell = this.byId.get(id);
55
+ return cell?.entry?.rowKey === rowKey && cell.generation === generation;
56
+ }
57
+ clear() {
58
+ this.cells = [];
59
+ this.byId.clear();
60
+ }
61
+ };
62
+ //#endregion
63
+ export { VirtualListCellPool };
package/package.json CHANGED
@@ -1,22 +1,27 @@
1
1
  {
2
2
  "name": "@octane-xplat/ui",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
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
- "octane",
7
- "xplat",
6
+ "android",
8
7
  "cross-platform",
9
- "ui",
10
- "ui-components",
11
- "primitives",
12
8
  "design-system",
13
- "nativescript",
14
9
  "ios",
15
- "android",
16
- "web"
10
+ "nativescript",
11
+ "octane",
12
+ "primitives",
13
+ "ui",
14
+ "ui-components",
15
+ "web",
16
+ "xplat"
17
17
  ],
18
- "license": "MIT",
19
18
  "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/ui",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/octane-xplat/octane-xplat",
23
+ "directory": "packages/ui"
24
+ },
20
25
  "files": [
21
26
  "dist",
22
27
  "types",
@@ -60,7 +65,7 @@
60
65
  "./theme/tokens.css": "./src/theme/tokens.css"
61
66
  },
62
67
  "devDependencies": {
63
- "@nativescript-community/octane": "0.2.1",
68
+ "@nativescript-community/octane": "0.2.4",
64
69
  "@nativescript-community/ui-drawer": "0.1.32",
65
70
  "@nativescript/core": "9.1.2",
66
71
  "@nativescript/types": "9.0.0",
@@ -68,7 +73,7 @@
68
73
  "@tsrx/typescript-plugin": "0.4.11",
69
74
  "jsdom": "30.1.1",
70
75
  "octane": "0.6.3",
71
- "tsrx-typegen": "0.1.0",
76
+ "tsrx-typegen": "0.2.1",
72
77
  "typescript": "5.9.3",
73
78
  "vite": "8.3.0",
74
79
  "vitest": "5.0.1"
@@ -100,14 +105,11 @@
100
105
  "android": "9.1.0"
101
106
  }
102
107
  },
103
- "repository": {
104
- "type": "git",
105
- "url": "git+https://github.com/octane-xplat/octane-xplat.git",
106
- "directory": "packages/ui"
107
- },
108
108
  "scripts": {
109
- "build": "vite build && vite build --mode native && tsrx-typegen --target ui && node ../../scripts/check-native-dist.mjs",
109
+ "build": "node ../../scripts/sync-ui-barrels.mjs --check && vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
110
110
  "test:packed": "node tests/packed-consumer.mjs",
111
- "test:native": "vitest run --config vitest.native.config.mts"
111
+ "test:native": "vitest run --config vitest.native.config.mts",
112
+ "typegen": "tsrx-typegen --target ui",
113
+ "pack:check": "tsrx-typegen --pack-check"
112
114
  }
113
115
  }
@@ -1,8 +1,8 @@
1
1
  {
2
- "uses": [
3
- "com.caverock.androidsvg:SVG",
4
- "com.caverock.androidsvg:SVGExternalFileResolver",
5
- "com.caverock.androidsvg:PreserveAspectRatio",
6
- "com.caverock.androidsvg:RenderOptions"
7
- ]
8
- }
2
+ "uses": [
3
+ "com.caverock.androidsvg:SVG",
4
+ "com.caverock.androidsvg:SVGExternalFileResolver",
5
+ "com.caverock.androidsvg:PreserveAspectRatio",
6
+ "com.caverock.androidsvg:RenderOptions"
7
+ ]
8
+ }
@@ -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';
@@ -20,6 +21,11 @@ export function CommandPalette(props: CommandPaletteProps) @{
20
21
  ? props.items.filter((i) => (i.label ?? i.key).toLowerCase().includes(query.toLowerCase()))
21
22
  : props.items;
22
23
 
24
+ const panelStyle = {
25
+ ...props.style,
26
+ maxWidth: props.style?.maxWidth ?? Screen.mainScreen.widthDIPs * 0.9,
27
+ };
28
+
23
29
  const select = (item: MenuItem) => {
24
30
  if (item.disabled) return;
25
31
  setOpen(false);
@@ -29,8 +35,9 @@ export function CommandPalette(props: CommandPaletteProps) @{
29
35
  <Overlay open={props.open} shadeCover={true} onDismiss={() => setOpen(false)} className="vx-cmdk-layer">
30
36
  <flexboxlayout
31
37
  className={cx('vx-commandpalette', props.className)}
32
- style={props.style}
38
+ style={panelStyle}
33
39
  id={props.id}
40
+ flexDirection="column"
34
41
  horizontalAlignment="center"
35
42
  verticalAlignment="top"
36
43
  marginTop={80}
package/src/Grid.tsrx CHANGED
@@ -1,20 +1,51 @@
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
+
29
+ const placement = autoPlaceGridChildren(
30
+ views.map(gridChildPlacement),
31
+ props.rows,
32
+ props.columns,
33
+ );
34
+
35
+ host.rows = ensureGridTracks(props.rows, placement.rowCount);
36
+ host.columns = ensureGridTracks(props.columns, placement.columnCount);
37
+ for (let index = 0; index < views.length; index++) {
38
+ views[index].row = placement.cells[index].row;
39
+ views[index].col = placement.cells[index].col;
40
+ }
41
+ }, [props.children, props.rows, props.columns]);
42
+
15
43
  <gridlayout
16
44
  id={props.id}
17
- ref={(view: any) => applyEscapeProps(view, props)}
45
+ ref={(view: any) => {
46
+ grid.current = view;
47
+ applyEscapeProps(view, props);
48
+ }}
18
49
  rows={props.rows}
19
50
  columns={props.columns}
20
51
  className={cx('vx-grid', props.className)}
@@ -8,6 +8,7 @@ import { getIcon } from './icons'
8
8
  export function Icon(props: IconProps) @{
9
9
  const size = props.size ?? 24
10
10
  const glyph = getIcon(props.name)
11
+
11
12
  <label
12
13
  className={props.className}
13
14
  style={{ width: size, height: size, fontSize: size * 0.75, ...props.style }}
@@ -1,8 +1,14 @@
1
1
  /** @jsxImportSource @xplat/macos/renderer */
2
2
  import type { KeyboardAvoidingProps } from './props'
3
+ import { cx } from './cx'
3
4
 
4
5
  export function KeyboardAvoiding(props: KeyboardAvoidingProps) @{
5
- <flexboxlayout flexDirection="column" className={props.className} style={props.style}>
6
+ <flexboxlayout
7
+ id={props.id}
8
+ flexDirection="column"
9
+ className={cx('vx-keyboard-avoiding', props.className)}
10
+ style={props.style}
11
+ >
6
12
  {props.children}
7
13
  </flexboxlayout>
8
14
  }
@@ -32,10 +32,10 @@ function useKeyboardInset() {
32
32
 
33
33
  function useAndroidResize() {
34
34
  useEffect(() => {
35
- const nativeWindow = Application.android?.foregroundActivity?.window;
35
+ const nativeWindow = Application.android?.foregroundActivity?.getWindow();
36
36
  if (!nativeWindow) return;
37
37
  const params = android.view.WindowManager.LayoutParams;
38
- const previous = nativeWindow.attributes.softInputMode;
38
+ const previous = nativeWindow.getAttributes().softInputMode;
39
39
  const adjusted = (previous & ~params.SOFT_INPUT_MASK_ADJUST) | params.SOFT_INPUT_ADJUST_RESIZE;
40
40
  nativeWindow.setSoftInputMode(adjusted);
41
41
  return () => nativeWindow.setSoftInputMode(previous);
@@ -0,0 +1,15 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import type { KeyboardAvoidingProps } from './props'
3
+ import { cx } from './cx'
4
+
5
+ // Targets without mobile keyboard insets retain the shared column layout.
6
+ export function KeyboardAvoiding(props: KeyboardAvoidingProps) @{
7
+ <flexboxlayout
8
+ id={props.id}
9
+ flexDirection="column"
10
+ className={cx('vx-keyboard-avoiding', props.className)}
11
+ style={props.style}
12
+ >
13
+ {props.children}
14
+ </flexboxlayout>
15
+ }
@@ -0,0 +1,15 @@
1
+ /** @jsxImportSource octane */
2
+ import type { KeyboardAvoidingProps } from './props'
3
+ import { cx } from './cx'
4
+
5
+ // Web keeps the wrapper's layout and styling, without claiming keyboard avoidance.
6
+ export function KeyboardAvoiding(props: KeyboardAvoidingProps) @{
7
+ <div
8
+ id={props.id}
9
+ className={cx('vx-keyboard-avoiding', props.className)}
10
+ style={props.style}
11
+ {...props.web}
12
+ >
13
+ {props.children}
14
+ </div>
15
+ }
@@ -0,0 +1,121 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { Screen } from './Screen';
3
+ import { ScrollView } from './ScrollView';
4
+ import { Text } from './Text';
5
+ import { View } from './View';
6
+
7
+ /** Markdown AST — the serialized shape `xplat routes` bakes for `.md`
8
+ * routes (parse happens at codegen, not runtime). `MdNode` covers block
9
+ * level; `MdInline` covers text runs inside paragraphs/lists/quotes. */
10
+ export type MdInline =
11
+ | { t: 'text'; text: string }
12
+ | { t: 'code'; text: string }
13
+ | { t: 'bold'; children: MdInline[] }
14
+ | { t: 'em'; children: MdInline[] }
15
+ | { t: 'link'; text: string; href: string };
16
+
17
+ export type MdNode =
18
+ | { t: 'h'; depth: 1 | 2 | 3 | 4 | 5 | 6; children: MdInline[] }
19
+ | { t: 'p'; children: MdInline[] }
20
+ | { t: 'code'; lang?: string; text: string }
21
+ | { t: 'list'; ordered: boolean; items: MdInline[][] }
22
+ | { t: 'quote'; children: MdInline[] }
23
+ | { t: 'hr' };
24
+
25
+ export interface MdDoc {
26
+ kind: 'octane-xplat/md';
27
+ v: 1;
28
+ children: MdNode[];
29
+ }
30
+
31
+ const headingClass = ['text-2xl font-bold', 'text-xl font-semibold', 'text-lg font-semibold'];
32
+
33
+ function Inlines(props: { nodes: MdInline[] }) {
34
+ const out = [];
35
+ for (const n of props.nodes ?? []) {
36
+ switch (n.t) {
37
+ case 'code':
38
+ out.push(
39
+ <Text style={{ fontFamily: 'ui-monospace' }}>{n.text}</Text>,
40
+ );
41
+
42
+ break;
43
+ case 'bold':
44
+ out.push(<Text className="font-bold"><Inlines nodes={n.children} /></Text>);
45
+
46
+ break;
47
+ case 'em':
48
+ out.push(<Text style={{ fontStyle: 'italic' }}><Inlines nodes={n.children} /></Text>);
49
+
50
+ break;
51
+ case 'link':
52
+ out.push(<Text style={{ textDecorationLine: 'underline' }}>{n.text}</Text>);
53
+
54
+ break;
55
+ default:
56
+ out.push(n.text);
57
+ }
58
+ }
59
+
60
+ return out;
61
+ }
62
+
63
+ /** Renders a baked markdown doc with the shared vocabulary — Text
64
+ * blocks on every platform, no parser at runtime. `data` is the AST the
65
+ * codegen baked into routes.gen.data (or a doc built programmatically). */
66
+ export function Markdown(props: { data?: MdDoc }) @{
67
+ <View className="gap-3">
68
+ {(props.data?.children ?? []).map((node) => {
69
+ switch (node.t) {
70
+ case 'h':
71
+ return (
72
+ <Text className={headingClass[node.depth - 1] ?? 'font-semibold'}>
73
+ <Inlines nodes={node.children} />
74
+ </Text>
75
+ );
76
+ case 'p':
77
+ return (
78
+ <Text>
79
+ <Inlines nodes={node.children} />
80
+ </Text>
81
+ );
82
+ case 'code':
83
+ return (
84
+ <Text className="text-sm" style={{ fontFamily: 'ui-monospace' }}>
85
+ {node.text}
86
+ </Text>
87
+ );
88
+ case 'list':
89
+ return node.items.map((item, i) => (
90
+ <Text>
91
+ {(node.ordered ? `${i + 1}. ` : '• ') + ''}
92
+ <Inlines nodes={item} />
93
+ </Text>
94
+ ));
95
+ case 'quote':
96
+ return (
97
+ <Text className="text-muted" style={{ fontStyle: 'italic' }}>
98
+ <Inlines nodes={node.children} />
99
+ </Text>
100
+ );
101
+ case 'hr':
102
+ return (
103
+ <View style={{ height: 1, backgroundColor: 'var(--color-border)' }} />
104
+ );
105
+ default:
106
+ return null;
107
+ }
108
+ })}
109
+ </View>
110
+ }
111
+
112
+ /** Route screen for `.md` files — the codegen emits specs with this as
113
+ * `screen`, so a markdown file in the route dir is a route end-to-end.
114
+ * Receives baked data like any route (`props.data`). */
115
+ export function MarkdownScreen(props: { data?: MdDoc; error?: unknown }) @{
116
+ <Screen className="vx-app">
117
+ <ScrollView style={{ padding: 16 }}>
118
+ <Markdown data={props.data} />
119
+ </ScrollView>
120
+ </Screen>
121
+ }
package/src/Meter.tsrx CHANGED
@@ -17,6 +17,7 @@ function ringSvg(size: number, strokeWidth: number, ratio: number, color: string
17
17
  const arc = ratio > 0 && r > 0
18
18
  ? `<circle cx="${c}" cy="${c}" r="${r}" fill="none" stroke="${color}" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-dasharray="${ratio * circumference} ${circumference}" transform="rotate(-90 ${c} ${c})"/>`
19
19
  : '';
20
+
20
21
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}">${track}${arc}</svg>`;
21
22
  }
22
23
 
package/src/Overlay.tsrx CHANGED
@@ -7,12 +7,13 @@ import type { OverlayProps } from './props';
7
7
  import { cx } from './cx';
8
8
  import { applyThemeClasses } from './theme/theme-scheme';
9
9
  import { rootLayoutFor } from './root-layout.mobile';
10
+ import { createOverlayLifetime } from './overlay-lifetime.mobile';
10
11
 
11
12
  function OverlayBody(props: { children?: any }) @{
12
13
  <>{props.children}</>
13
14
  }
14
15
 
15
- type OpenOverlay = { host: GridLayout; root: any; dismissOnClose: boolean; unbindTheme?: () => void };
16
+ type OpenOverlay = { host: GridLayout; root: any; lifetime: ReturnType<typeof createOverlayLifetime>; unbindTheme?: () => void };
16
17
 
17
18
  /** Renders children in a separate RootLayout root above the current screen. */
18
19
  export function Overlay(props: OverlayProps) @{
@@ -26,22 +27,13 @@ export function Overlay(props: OverlayProps) @{
26
27
  useEffect(() => {
27
28
  const current = shown.current;
28
29
  if (!props.open) {
29
- if (!current) return;
30
- current.dismissOnClose = false;
31
- shown.current = null;
32
- const rl = rootLayout();
33
- if (rl && (rl as any)?.hasChild?.(current.host)) {
34
- rl.close(current.host).then(
35
- () => current.root.unmount?.(),
36
- (error) => { console.error('[Overlay] close failed', error); current.root.unmount?.(); },
37
- );
38
- } else current.root.unmount?.();
39
-
30
+ current?.lifetime.close();
40
31
  return;
41
32
  }
42
33
 
43
34
  if (current) {
44
35
  current.root.render(OverlayBody as unknown as UniversalComponent, { children: props.children });
36
+ current.unbindTheme?.();
45
37
  current.unbindTheme = applyThemeClasses(current.host, cx('vx-overlay-host', props.className));
46
38
  if (props.style) Object.assign(current.host.style, props.style);
47
39
  Object.assign(current.host, isIOS ? props.ios : isAndroid ? props.android : undefined);
@@ -57,28 +49,21 @@ export function Overlay(props: OverlayProps) @{
57
49
  if (props.style) Object.assign(host.style, props.style);
58
50
  Object.assign(host, isIOS ? props.ios : isAndroid ? props.android : undefined);
59
51
  const root = createNativeScriptRoot(host);
60
- const opened = { host, root, dismissOnClose: true, unbindTheme };
61
- shown.current = opened;
62
- root.render(OverlayBody as unknown as UniversalComponent, { children: props.children });
63
- host.on('closed', () => {
64
- if (shown.current === opened) shown.current = null;
65
- opened.unbindTheme?.();
66
- root.unmount?.();
67
- if (opened.dismissOnClose) dismiss.current?.();
52
+ const lifetime = createOverlayLifetime(rl, host, root, 'Overlay', (dismissed) => {
53
+ if (shown.current?.host === host) shown.current = null;
54
+ if (dismissed) dismiss.current?.();
68
55
  });
69
56
 
70
- const open = () => rl.open(host, props.shadeCover ? {
57
+ const opened = { host, root, lifetime, unbindTheme };
58
+ lifetime.own(() => opened.unbindTheme?.());
59
+ shown.current = opened;
60
+ root.render(OverlayBody as unknown as UniversalComponent, { children: props.children });
61
+ lifetime.open(props.shadeCover ? {
71
62
  shadeCover: { opacity: 0.45, color: '#000000', tapToClose: true },
72
- } : {}).catch((error: unknown) => {
73
- if (shown.current === opened) shown.current = null;
74
- console.error('[Overlay] open failed', error);
75
- root.unmount?.();
76
- });
77
-
78
- if ((rl as any).hasChild?.(host)) {
79
- rl.close(host).then(open, (error) => console.error('[Overlay] stale host close failed', error));
80
- } else open();
63
+ } : {});
81
64
  }, [props.open, props.children, props.shadeCover, props.className, props.style, props.ios, props.android]);
82
65
 
66
+ useEffect(() => () => shown.current?.lifetime.close(), []);
67
+
83
68
  <stacklayout ref={(v: any) => { hostView.current = v; }} style={{ width: 0, height: 0 }} />
84
69
  }
@@ -1,19 +1,28 @@
1
- import { createPortal } from 'octane';
1
+ import { createPortal, useEffect, useRef } from 'octane';
2
2
  import type { OverlayProps } from './props';
3
3
  import { useThemeScheme } from './theme/theme-scheme';
4
4
  import { cx } from './cx';
5
+ import { isolateModalFocus } from './modal-focus.web';
5
6
 
6
7
  /** Renders an in-window overlay through the DOM renderer's body portal. */
7
8
  export function Overlay(props: OverlayProps) @{
8
9
  const dark = useThemeScheme() === 'dark';
10
+ const layer = useRef<HTMLDivElement | null>(null);
11
+ const panel = useRef<HTMLDivElement | null>(null);
12
+ const dismiss = useRef(props.onDismiss);
13
+ dismiss.current = props.onDismiss;
14
+ useEffect(() => {
15
+ if (!props.open || !props.shadeCover || !layer.current || !panel.current) return;
16
+ return isolateModalFocus(layer.current, panel.current, () => dismiss.current?.());
17
+ }, [props.open, props.shadeCover]);
9
18
 
10
19
  @if (props.open) {
11
20
  <>{createPortal(
12
- <div className={cx('vx-overlay', dark && 'dark', props.className)} style={{ ...props.style, position: 'fixed', inset: 0 }} {...props.web}>
21
+ <div ref={layer} className={cx('vx-overlay', dark && 'dark', props.className)} style={{ ...props.style, position: 'fixed', inset: 0 }} {...props.web}>
13
22
  @if (props.shadeCover) {
14
23
  <div className="vx-overlay-shade" onClick={props.onDismiss} />
15
24
  }
16
- <div className="vx-overlay-content" onClick={(event: any) => event.stopPropagation()}>
25
+ <div ref={panel} role={props.shadeCover ? 'dialog' : undefined} aria-modal={props.shadeCover ? true : undefined} className="vx-overlay-content" onClick={(event: any) => event.stopPropagation()}>
17
26
  {props.children}
18
27
  </div>
19
28
  </div>,