@octane-xplat/ui 0.7.3 → 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 (195) hide show
  1. package/README.md +4 -3
  2. package/dist/native/Button.tsrx.js +1 -1
  3. package/dist/native/KeyboardAvoiding.mobile.tsrx.js +2 -2
  4. package/dist/native/Markdown.tsrx.js +233 -0
  5. package/dist/native/Overlay.tsrx.js +15 -26
  6. package/dist/native/PinInput.tsrx.js +25 -7
  7. package/dist/native/Popover.tsrx.js +17 -18
  8. package/dist/native/Pressable.tsrx.js +4 -1
  9. package/dist/native/SearchInput.tsrx.js +6 -3
  10. package/dist/native/Sheet.tsrx.js +22 -37
  11. package/dist/native/TextArea.tsrx.js +6 -3
  12. package/dist/native/TextInput.tsrx.js +6 -3
  13. package/dist/native/VirtualList.tsrx.js +305 -121
  14. package/dist/native/index.shared.js +7 -5
  15. package/dist/native/keyboard-inset.mobile.js +5 -5
  16. package/dist/native/navigation-request.js +26 -0
  17. package/dist/native/octane-client-build.json +1 -1
  18. package/dist/native/overlay-lifetime.mobile.js +65 -0
  19. package/dist/native/pan.tsrx.js +16 -2
  20. package/dist/native/route-table.js +90 -10
  21. package/dist/native/route.js +26 -16
  22. package/dist/native/shared.js +7 -5
  23. package/dist/native/sheet-service.js +1 -1
  24. package/dist/native/svg.mobile.js +1 -1
  25. package/dist/native/text-focus.js +10 -0
  26. package/dist/native/text-write.js +18 -11
  27. package/dist/native/toast-anchor.tsrx.js +1 -1
  28. package/dist/native/ui.js +7 -6
  29. package/dist/native/vendor/ui-svg/index.android.js +2 -2
  30. package/dist/native/vendor/ui-svg/index.ios.js +2 -2
  31. package/dist/native/vendor/ui-svg/{index.common.js → src/index.common.js} +1 -1
  32. package/dist/native/virtual-list-cell-pool.js +63 -0
  33. package/dist/web/Button.web.tsrx.js +1 -1
  34. package/dist/web/KeyboardAvoiding.web.tsrx.js +78 -0
  35. package/dist/web/Markdown.tsrx.js +98 -0
  36. package/dist/web/Overlay.web.tsrx.js +81 -32
  37. package/dist/web/PinInput.web.tsrx.js +24 -10
  38. package/dist/web/Pressable.web.tsrx.js +169 -53
  39. package/dist/web/SearchInput.web.tsrx.js +2 -2
  40. package/dist/web/Sheet.web.tsrx.js +20 -7
  41. package/dist/web/TextArea.web.tsrx.js +22 -17
  42. package/dist/web/TextInput.web.tsrx.js +2 -2
  43. package/dist/web/VirtualList.web.tsrx.js +220 -109
  44. package/dist/web/WebView.web.tsrx.js +3 -3
  45. package/dist/web/{index.shared.js → index.shared.web.js} +7 -5
  46. package/dist/web/modal-focus.web.js +95 -0
  47. package/dist/web/navigation-request.js +26 -0
  48. package/dist/web/octane-client-build.json +1 -1
  49. package/dist/web/pan.web.tsrx.js +27 -10
  50. package/dist/web/route-table.js +90 -10
  51. package/dist/web/route.web.js +88 -22
  52. package/dist/web/sheet-service.web.js +8 -0
  53. package/dist/web/theme/colorScheme.web.js +31 -4
  54. package/dist/web/ui.js +8 -6
  55. package/dist/web/virtual-list-cell-pool.js +63 -0
  56. package/package.json +21 -19
  57. package/platforms/android/native-api-usage.json +7 -7
  58. package/src/CommandPalette.tsrx +1 -0
  59. package/src/Grid.tsrx +2 -0
  60. package/src/Icon.macos.tsrx +1 -0
  61. package/src/KeyboardAvoiding.macos.tsrx +7 -1
  62. package/src/KeyboardAvoiding.mobile.tsrx +2 -2
  63. package/src/KeyboardAvoiding.tsrx +15 -0
  64. package/src/KeyboardAvoiding.web.tsrx +15 -0
  65. package/src/Markdown.tsrx +121 -0
  66. package/src/Meter.tsrx +1 -0
  67. package/src/Overlay.tsrx +15 -30
  68. package/src/Overlay.web.tsrx +12 -3
  69. package/src/PinInput.macos.tsrx +20 -6
  70. package/src/PinInput.mobile.test.ts +107 -0
  71. package/src/PinInput.tsrx +20 -6
  72. package/src/PinInput.web.test.tsrx +42 -0
  73. package/src/PinInput.web.tsrx +20 -6
  74. package/src/Popover.tsrx +17 -17
  75. package/src/Pressable.tsrx +1 -1
  76. package/src/Pressable.web.test.tsrx +36 -0
  77. package/src/Pressable.web.tsrx +37 -6
  78. package/src/RouteHost.macos.tsrx +1 -0
  79. package/src/SearchInput.tsrx +4 -3
  80. package/src/SearchInput.web.tsrx +1 -1
  81. package/src/Sheet.tsrx +20 -41
  82. package/src/Sheet.web.tsrx +9 -1
  83. package/src/Text.tsrx +2 -0
  84. package/src/TextArea.tsrx +6 -5
  85. package/src/TextArea.web.test.tsrx +39 -0
  86. package/src/TextArea.web.tsrx +7 -5
  87. package/src/TextInput.tsrx +6 -6
  88. package/src/TextInput.web.tsrx +1 -1
  89. package/src/VirtualList.macos.tsrx +93 -6
  90. package/src/VirtualList.mobile.test.tsrx +232 -0
  91. package/src/VirtualList.tsrx +285 -81
  92. package/src/VirtualList.web.tsrx +131 -25
  93. package/src/WebView.tsrx +1 -0
  94. package/src/WebView.web.tsrx +3 -0
  95. package/src/android/BottomNavigationView.android.tsrx +1 -0
  96. package/src/android/MaterialDialog.android.tsrx +1 -0
  97. package/src/anim.macos.tsrx +1 -0
  98. package/src/apply-native-modifiers.native.ts +4 -1
  99. package/src/child-array.ts +12 -7
  100. package/src/colorScheme.macos.ts +1 -0
  101. package/src/controls.macos.tsrx +4 -1
  102. package/src/data-driver.mobile.test.ts +120 -0
  103. package/src/data-lifecycle.mobile.test.ts +251 -0
  104. package/src/escape-props.ts +2 -0
  105. package/src/grid-auto-placement.ts +54 -12
  106. package/src/grid-placement.ts +1 -1
  107. package/src/hosted-unsupported.macos.tsrx +1 -0
  108. package/src/index.macos.ts +23 -3
  109. package/src/index.mobile.ts +0 -1
  110. package/src/index.shared.ts +22 -1
  111. package/src/index.shared.web.ts +291 -0
  112. package/src/index.web.ts +1 -1
  113. package/src/input-readiness.web.test.tsrx +82 -0
  114. package/src/ios/UITabBar.ios.tsrx +1 -0
  115. package/src/keyboard-inset.mobile.test.ts +62 -0
  116. package/src/keyboard-inset.mobile.ts +63 -43
  117. package/src/layout-shells.macos.tsrx +1 -5
  118. package/src/macos-extras.macos.tsrx +2 -0
  119. package/src/modal-focus.web.test.tsrx +63 -0
  120. package/src/modal-focus.web.ts +162 -0
  121. package/src/navigation-request.ts +37 -0
  122. package/src/overlay-lifecycle.mobile.test.ts +316 -0
  123. package/src/overlay-lifetime.mobile.ts +107 -0
  124. package/src/pan.mobile.test.tsrx +116 -0
  125. package/src/pan.tsrx +20 -2
  126. package/src/pan.web.tsrx +35 -7
  127. package/src/platformProps.ts +1 -0
  128. package/src/popover.mobile.test.ts +9 -1
  129. package/src/pressable-accessibility.mobile.test.ts +30 -0
  130. package/src/props.ts +163 -3
  131. package/src/refresh.ts +6 -1
  132. package/src/refresh.web.test.ts +77 -77
  133. package/src/route-android.test.ts +162 -0
  134. package/src/route-history.web.test.ts +80 -0
  135. package/src/route-navigation.web.test.ts +173 -0
  136. package/src/route-table.test.ts +188 -9
  137. package/src/route-table.ts +138 -10
  138. package/src/route.macos.ts +56 -13
  139. package/src/route.ts +54 -24
  140. package/src/route.web.ts +141 -24
  141. package/src/sheet-detents.ts +5 -3
  142. package/src/sheet-service.macos.ts +5 -1
  143. package/src/sheet-service.web.ts +8 -0
  144. package/src/sheet-snap.test.ts +23 -23
  145. package/src/sheet-snap.ts +6 -1
  146. package/src/svg.mobile.ts +9 -3
  147. package/src/tap-to-blur.ios.ts +48 -32
  148. package/src/tap-to-blur.mobile.ts +23 -15
  149. package/src/text-focus.ts +12 -0
  150. package/src/text-input.mobile.test.ts +89 -0
  151. package/src/text-write.ts +21 -11
  152. package/src/theme/colorScheme.linux.ts +2 -4
  153. package/src/theme/colorScheme.web.ts +51 -4
  154. package/src/theme/theme-scheme.macos.ts +8 -2
  155. package/src/theme/tokens.css +1736 -1730
  156. package/src/translate.macos.ts +4 -1
  157. package/src/virtual-list-cell-pool.test.ts +53 -0
  158. package/src/virtual-list-cell-pool.ts +81 -0
  159. package/src/virtual-list-layout.test.ts +11 -8
  160. package/src/virtual-list-layout.ts +4 -8
  161. package/src/windows.linux.ts +7 -2
  162. package/src/windows.macos.ts +1 -0
  163. package/types/generated/.tsrx-typegen-manifest.json +13 -3
  164. package/types/generated/KeyboardAvoiding.d.ts +3 -0
  165. package/types/generated/KeyboardAvoiding.web.d.ts +2 -0
  166. package/types/generated/Markdown.d.ts +59 -0
  167. package/types/generated/TextArea.d.ts +2 -2
  168. package/types/generated/TextArea.web.d.ts +1 -1
  169. package/types/generated/TextInput.d.ts +2 -3
  170. package/types/generated/VirtualList.d.ts +3 -3
  171. package/types/generated/index.mobile.d.ts +0 -1
  172. package/types/generated/index.shared.d.ts +6 -3
  173. package/types/generated/index.shared.web.d.ts +114 -0
  174. package/types/generated/index.web.d.ts +1 -1
  175. package/types/generated/modal-focus.web.d.ts +3 -0
  176. package/types/generated/navigation-request.d.ts +10 -0
  177. package/types/generated/overlay-lifetime.mobile.d.ts +10 -0
  178. package/types/generated/props.d.ts +111 -3
  179. package/types/generated/route-table.d.ts +14 -3
  180. package/types/generated/svg.mobile.d.ts +2 -2
  181. package/types/generated/text-focus.d.ts +3 -0
  182. package/types/generated/text-write.d.ts +3 -1
  183. package/types/generated/theme/colorScheme.web.d.ts +5 -0
  184. package/types/generated/virtual-list-cell-pool.d.ts +18 -0
  185. package/types/index.macos.d.ts +15 -3
  186. /package/src/vendor/ui-svg/{index.android.ts → src/index.android.ts} +0 -0
  187. /package/src/vendor/ui-svg/{index.common.ts → src/index.common.ts} +0 -0
  188. /package/src/vendor/ui-svg/{index.ios.ts → src/index.ios.ts} +0 -0
  189. /package/src/vendor/ui-svg/{index.ts → src/index.ts} +0 -0
  190. /package/src/vendor/ui-svg/{typings → src/typings}/SVGKit.d.ts +0 -0
  191. /package/src/vendor/ui-svg/{typings → src/typings}/android.d.ts +0 -0
  192. /package/src/vendor/ui-svg/{typings → src/typings}/ios.d.ts +0 -0
  193. /package/types/generated/vendor/ui-svg/{index.common.d.ts → src/index.common.d.ts} +0 -0
  194. /package/types/generated/vendor/ui-svg/{index.d.ts → src/index.d.ts} +0 -0
  195. /package/types/generated/vendor/ui-svg/{index.ios.d.ts → src/index.ios.d.ts} +0 -0
@@ -11,35 +11,44 @@
11
11
  * Keyboard taps live in a separate UIWindow, so they never reach it.
12
12
  * `cancelsTouchesInView` stays false so taps still reach buttons. */
13
13
  export function attachTapToBlur(view: any): () => void {
14
- const marker = '__xplatTapToBlur';
15
- if (!view || view[marker]) {return () => {};}
16
- view[marker] = true;
14
+ const marker = '__xplatTapToBlur'
15
+ if (!view || view[marker]) {
16
+ return () => {}
17
+ }
18
+
19
+ view[marker] = true
17
20
 
18
- iosAttach();
21
+ iosAttach()
19
22
  return () => {
20
- view[marker] = false;
21
- iosDetach();
22
- };
23
+ view[marker] = false
24
+ iosDetach()
25
+ }
23
26
  }
24
27
 
25
- let iosCount = 0;
26
- let iosTarget: any = null;
27
- let iosRecognizer: any = null;
28
- let iosWindow: any = null;
28
+ let iosCount = 0
29
+ let iosTarget: any = null
30
+ let iosRecognizer: any = null
31
+ let iosWindow: any = null
29
32
 
30
33
  function iosAttach() {
31
- iosCount++;
32
- if (iosRecognizer) {return;}
33
- const appWin = appWindow();
34
- if (!appWin) {return;}
35
- iosWindow = appWin;
34
+ iosCount++
35
+ if (iosRecognizer) {
36
+ return
37
+ }
38
+
39
+ const appWin = appWindow()
40
+ if (!appWin) {
41
+ return
42
+ }
43
+
44
+ iosWindow = appWin
36
45
  // Resolve Objective-C bridge globals only when the iOS view is attached.
37
46
  @NativeClass
38
47
  class TapToBlurRecognizerTarget extends NSObject implements UIGestureRecognizerDelegate {
39
48
  static ObjCProtocols = [UIGestureRecognizerDelegate]
40
49
  static ObjCExposedMethods = {
41
50
  tap: { returns: interop.types.void, params: [UITapGestureRecognizer] },
42
- };
51
+ }
43
52
 
44
53
  gestureRecognizerShouldReceiveTouch(_recognizer: any, touch: any) {
45
54
  let hit = touch.view
@@ -49,42 +58,49 @@ function iosAttach() {
49
58
  if (textField || textView) {
50
59
  return false
51
60
  }
61
+
52
62
  hit = hit.superview
53
63
  }
64
+
54
65
  return true
55
66
  }
56
67
 
57
68
  tap(_recognizer: any) {
58
- iosWindow.endEditing(true);
69
+ iosWindow.endEditing(true)
59
70
  }
60
71
  }
61
72
 
62
- iosTarget = TapToBlurRecognizerTarget.new();
63
- iosRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(iosTarget, 'tap');
64
- iosRecognizer.delegate = iosTarget;
65
- iosRecognizer.cancelsTouchesInView = false;
66
- appWin.addGestureRecognizer(iosRecognizer);
73
+ iosTarget = TapToBlurRecognizerTarget.new()
74
+ iosRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(iosTarget, 'tap')
75
+ iosRecognizer.delegate = iosTarget
76
+ iosRecognizer.cancelsTouchesInView = false
77
+ appWin.addGestureRecognizer(iosRecognizer)
67
78
  }
68
79
 
69
80
  function iosDetach() {
70
- iosCount--;
71
- if (iosCount > 0 || !iosRecognizer) {return;}
72
- iosWindow?.removeGestureRecognizer(iosRecognizer);
73
- iosRecognizer = null;
74
- iosTarget = null;
75
- iosWindow = null;
81
+ iosCount--
82
+ if (iosCount > 0 || !iosRecognizer) {
83
+ return
84
+ }
85
+
86
+ iosWindow?.removeGestureRecognizer(iosRecognizer)
87
+ iosRecognizer = null
88
+ iosTarget = null
89
+ iosWindow = null
76
90
  }
77
91
 
78
92
  function appWindow() {
79
- const scenes = UIApplication.sharedApplication.connectedScenes;
93
+ const scenes = UIApplication.sharedApplication.connectedScenes
80
94
  // connectedScenes is an NSSet — `allObjects` marshals to a JS array.
81
95
  for (const scene of (scenes as any).allObjects ?? []) {
82
96
  if (scene instanceof UIWindowScene) {
83
97
  for (const w of (scene.windows as any)?.allObjects ?? scene.windows ?? []) {
84
- if (w.isKeyWindow) {return w;}
98
+ if (w.isKeyWindow) {
99
+ return w
100
+ }
85
101
  }
86
102
  }
87
103
  }
88
104
 
89
- return UIApplication.sharedApplication.keyWindow;
105
+ return UIApplication.sharedApplication.keyWindow
90
106
  }
@@ -1,26 +1,34 @@
1
- import { Application } from '@nativescript/core';
1
+ import { Application } from '@nativescript/core'
2
2
 
3
3
  /** Hide the Android soft keyboard when the user taps the attached view. */
4
4
  export function attachTapToBlur(view: any): () => void {
5
- const marker = '__xplatTapToBlur';
6
- if (!view || view[marker]) {return () => {};}
7
- view[marker] = true;
5
+ const marker = '__xplatTapToBlur'
6
+ if (!view || view[marker]) {
7
+ return () => {}
8
+ }
9
+
10
+ view[marker] = true
8
11
 
9
12
  if (!Application.android) {
10
- return () => { view[marker] = false; };
13
+ return () => {
14
+ view[marker] = false
15
+ }
11
16
  }
12
17
 
13
18
  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
- };
19
+ const activity = Application.android.foregroundActivity
20
+ const focused = activity?.getCurrentFocus()
21
+ if (!focused) {
22
+ return
23
+ }
20
24
 
21
- view.on('tap', handler);
25
+ const imm = activity.getSystemService(android.content.Context.INPUT_METHOD_SERVICE)
26
+ imm?.hideSoftInputFromWindow(focused.getWindowToken(), 0)
27
+ }
28
+
29
+ view.on('tap', handler)
22
30
  return () => {
23
- view[marker] = false;
24
- view.off('tap', handler);
25
- };
31
+ view[marker] = false
32
+ view.off('tap', handler)
33
+ }
26
34
  }
@@ -0,0 +1,12 @@
1
+ /** Dismiss the keyboard and release native editing focus. NativeScript's
2
+ * dismissSoftInput returns void; its return value is not a fallback signal. */
3
+ export function blurText(view: any): void {
4
+ if (typeof view.dismissSoftInput === 'function') {
5
+ view.dismissSoftInput()
6
+ } else {
7
+ view.blur?.()
8
+ }
9
+
10
+ // Android dismissSoftInput hides the IME but retains EditText focus.
11
+ view.android?.clearFocus?.()
12
+ }
@@ -0,0 +1,89 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ createObjectContainer,
4
+ createObjectDriver,
5
+ createUniversalRoot,
6
+ flushUniversalSync,
7
+ } from 'octane/universal/native'
8
+
9
+ vi.mock('@nativescript/core', () => ({
10
+ isIOS: false,
11
+ isAndroid: true,
12
+ Utils: { layout: { toDeviceIndependentPixels: (value: number) => value } },
13
+ }))
14
+
15
+ vi.mock('./escape-props', () => ({ applyEscapeProps: () => {} }))
16
+ vi.mock('./Icon.tsrx', async () => {
17
+ const { defineUniversalComponent } = await import('octane/universal/native')
18
+ return { Icon: defineUniversalComponent('nativescript', () => null) }
19
+ })
20
+
21
+ import { TextInput } from './TextInput.tsrx'
22
+ import { TextArea } from './TextArea.tsrx'
23
+ import { SearchInput } from './SearchInput.tsrx'
24
+
25
+ // NativeScript Property raises a synchronous textChange on script writes.
26
+ // Model that event here, through the real compiled leaves and event dispatch.
27
+ // This is regression coverage, not keyboard/IME/device evidence.
28
+ describe('native controlled input writes', () => {
29
+ for (const [name, Component] of [
30
+ ['TextInput', TextInput],
31
+ ['TextArea', TextArea],
32
+ ['SearchInput', SearchInput],
33
+ ] as const) {
34
+ it(`${name} does not report controlled writes as user edits`, async () => {
35
+ const container = createObjectContainer('nativescript')
36
+ const root = createUniversalRoot(container, createObjectDriver('nativescript'))
37
+ const change = vi.fn()
38
+ let node: any
39
+ let inputHandle: any
40
+ let text = ''
41
+ let selection = [2, 4]
42
+ const bind = (handle: any) => {
43
+ inputHandle = handle
44
+ node = handle.native
45
+ node.style = {}
46
+ node.android = {
47
+ getSelectionStart: () => selection[0],
48
+ getSelectionEnd: () => selection[1],
49
+ setSelection: (start: number, end: number) => {
50
+ selection = [start, end]
51
+ },
52
+ }
53
+
54
+ Object.defineProperty(node, 'text', {
55
+ configurable: true,
56
+ get: () => text,
57
+ set: (value) => {
58
+ text = value
59
+ container.dispatchEvent(node, 'textchange', { object: node, value })
60
+ },
61
+ })
62
+ }
63
+
64
+ root.render(Component as any, { value: 'hello', onChange: change, bind, icon: false })
65
+ await vi.waitFor(() => expect(text).toBe('hello'))
66
+ expect(change).not.toHaveBeenCalled()
67
+ expect(selection).toEqual([2, 4])
68
+ root.render(Component as any, { value: 'x', onChange: change, bind, icon: false })
69
+ await vi.waitFor(() => expect(text).toBe('x'))
70
+ expect(change).not.toHaveBeenCalled()
71
+ expect(selection).toEqual([1, 1])
72
+ // A native edit still reaches the callback exactly once.
73
+ text = 'xy'
74
+ flushUniversalSync(() =>
75
+ container.dispatchEvent(node, 'textchange', { object: node, value: text }),
76
+ )
77
+
78
+ expect(change).toHaveBeenCalledExactlyOnceWith('xy')
79
+ const dismiss = vi.fn()
80
+ const clearFocus = vi.fn()
81
+ node.android.clearFocus = clearFocus
82
+ node.dismissSoftInput = dismiss // NativeScript returns void and has no blur method.
83
+ expect(() => inputHandle.blur()).not.toThrow()
84
+ expect(dismiss).toHaveBeenCalledTimes(1)
85
+ expect(clearFocus).toHaveBeenCalledTimes(1)
86
+ root.unmount()
87
+ })
88
+ }
89
+ })
package/src/text-write.ts CHANGED
@@ -1,4 +1,11 @@
1
- /** Controlled `text` write for TextInput/TextArea leaves. The driver's
1
+ const writing = new WeakSet<object>()
2
+
3
+ /** Whether a textChange is the synchronous echo of our controlled write. */
4
+ export function isWritingText(view: object): boolean {
5
+ return writing.has(view)
6
+ }
7
+
8
+ /** Controlled `text` write for TextInput/TextArea/SearchInput leaves. The driver's
2
9
  * generic prop path writes view.text verbatim; on Android the EditText
3
10
  * setText resets the cursor to 0 on every controlled write. Park and
4
11
  * restore the selection around the write (clamped to the new length so an
@@ -13,16 +20,19 @@ export function writeText(view: any, value: string | undefined | null): void {
13
20
  }
14
21
 
15
22
  const et = view.android
16
- if (!et) {
23
+ const start = et?.getSelectionStart?.() ?? -1
24
+ const end = et?.getSelectionEnd?.() ?? -1
25
+ const nested = writing.has(view)
26
+ writing.add(view)
27
+ try {
17
28
  view.text = next
18
- return
19
- }
20
-
21
- const start = et.getSelectionStart?.() ?? -1
22
- const end = et.getSelectionEnd?.() ?? -1
23
- view.text = next
24
- if (start >= 0) {
25
- const len = next.length
26
- et.setSelection(Math.min(start, len), Math.min(end >= 0 ? end : start, len))
29
+ if (start >= 0) {
30
+ const len = next.length
31
+ et.setSelection(Math.min(start, len), Math.min(end >= 0 ? end : start, len))
32
+ }
33
+ } finally {
34
+ if (!nested) {
35
+ writing.delete(view)
36
+ }
27
37
  }
28
38
  }
@@ -13,15 +13,13 @@ export type { ColorScheme } from '../props'
13
13
  import type { ColorScheme } from '../props'
14
14
 
15
15
  const bridged = () =>
16
- typeof window !== 'undefined' &&
17
- (window as any).webkit?.messageHandlers?.xplat !== undefined
16
+ typeof window !== 'undefined' && (window as any).webkit?.messageHandlers?.xplat !== undefined
18
17
 
19
18
  let lastScheme: ColorScheme =
20
19
  ((window as any).__xplatColorScheme as ColorScheme | undefined) ?? 'light'
21
20
 
22
21
  function getSystemScheme(): ColorScheme {
23
- return typeof matchMedia === 'function' &&
24
- matchMedia('(prefers-color-scheme: dark)').matches
22
+ return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches
25
23
  ? 'dark'
26
24
  : 'light'
27
25
  }
@@ -3,16 +3,63 @@ import { useSyncExternalStore } from 'octane'
3
3
  export type { ColorScheme } from '../props'
4
4
  import type { ColorScheme } from '../props'
5
5
 
6
- export function getColorScheme(): ColorScheme {
6
+ type HostTransport = {
7
+ listen(listener: (message: string) => void): () => void
8
+ }
9
+
10
+ const hostTransport = () =>
11
+ typeof window === 'undefined'
12
+ ? null
13
+ : (((window as any).__xplatHostTransport as HostTransport | undefined) ?? null)
14
+
15
+ let hostedScheme: ColorScheme =
16
+ typeof window === 'undefined'
17
+ ? 'light'
18
+ : ((window as any).__xplatHostSnapshot?.colorScheme ??
19
+ (window as any).__xplatColorScheme ??
20
+ 'light')
21
+
22
+ function getSystemScheme(): ColorScheme {
7
23
  return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches
8
24
  ? 'dark'
9
25
  : 'light'
10
26
  }
11
27
 
28
+ export function getColorScheme(): ColorScheme {
29
+ return hostTransport() ? hostedScheme : getSystemScheme()
30
+ }
31
+
12
32
  export function subscribeSystemScheme(cb: () => void): () => void {
13
- const mq = matchMedia('(prefers-color-scheme: dark)')
14
- mq.addEventListener('change', cb)
15
- return () => mq.removeEventListener('change', cb)
33
+ const transport = hostTransport()
34
+ if (!transport) {
35
+ const mq = matchMedia('(prefers-color-scheme: dark)')
36
+ mq.addEventListener('change', cb)
37
+ return () => mq.removeEventListener('change', cb)
38
+ }
39
+
40
+ const onTransportMessage = (message: string) => {
41
+ try {
42
+ const packet = JSON.parse(message) as { type?: string; name?: string; payload?: unknown }
43
+ if (packet.type === 'event' && packet.name === 'appearance.change') {
44
+ hostedScheme = packet.payload === 'dark' ? 'dark' : 'light'
45
+ cb()
46
+ }
47
+ } catch {
48
+ // Host messages are strings; unrelated malformed traffic is ignored.
49
+ }
50
+ }
51
+
52
+ const onCompatEvent = (event: Event) => {
53
+ hostedScheme = (event as CustomEvent).detail === 'dark' ? 'dark' : 'light'
54
+ cb()
55
+ }
56
+
57
+ const offTransport = transport.listen(onTransportMessage)
58
+ window.addEventListener('xplat:appearance.change', onCompatEvent)
59
+ return () => {
60
+ offTransport()
61
+ window.removeEventListener('xplat:appearance.change', onCompatEvent)
62
+ }
16
63
  }
17
64
 
18
65
  export function useColorScheme(): ColorScheme {
@@ -9,7 +9,10 @@ const listeners = new Set<() => void>()
9
9
  const emit = () => listeners.forEach((listener) => listener())
10
10
 
11
11
  export function setThemePreference(value: ThemePreference): void {
12
- if (preference === value) {return}
12
+ if (preference === value) {
13
+ return
14
+ }
15
+
13
16
  preference = value
14
17
  emit()
15
18
  }
@@ -44,7 +47,10 @@ export function onThemeSchemeChange(callback: () => void): () => void {
44
47
  }
45
48
 
46
49
  export function applyThemeClasses(view: any, base = ''): () => void {
47
- const apply = () => { view.className = cx(base, themeSchemeClasses()) }
50
+ const apply = () => {
51
+ view.className = cx(base, themeSchemeClasses())
52
+ }
53
+
48
54
  apply()
49
55
  return onThemeSchemeChange(apply)
50
56
  }