@webview-native-ui/engine 1.0.1 → 1.0.2-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 (230) hide show
  1. package/README.md +14 -0
  2. package/WebViewNativeUIControls.podspec +17 -0
  3. package/dist/controls/activation.d.ts +15 -0
  4. package/dist/controls/activation.js +370 -0
  5. package/dist/controls/components/index.d.ts +16 -0
  6. package/dist/controls/components/index.js +38 -0
  7. package/dist/controls/components/ion-back-button.d.ts +4 -0
  8. package/dist/controls/components/ion-back-button.js +16 -0
  9. package/dist/controls/components/ion-button.d.ts +4 -0
  10. package/dist/controls/components/ion-button.js +26 -0
  11. package/dist/controls/components/ion-buttons.d.ts +5 -0
  12. package/dist/controls/components/ion-buttons.js +34 -0
  13. package/dist/controls/components/ion-fab.d.ts +4 -0
  14. package/dist/controls/components/ion-fab.js +164 -0
  15. package/dist/controls/components/ion-menu-button.d.ts +7 -0
  16. package/dist/controls/components/ion-menu-button.js +41 -0
  17. package/dist/controls/components/ion-segment.d.ts +5 -0
  18. package/dist/controls/components/ion-segment.js +24 -0
  19. package/dist/controls/components/ion-tab-bar.d.ts +5 -0
  20. package/dist/controls/components/ion-tab-bar.js +49 -0
  21. package/dist/controls/components/searchable-tabs.d.ts +20 -0
  22. package/dist/controls/components/searchable-tabs.js +525 -0
  23. package/dist/controls/definitions.d.ts +360 -0
  24. package/dist/controls/definitions.js +1 -0
  25. package/dist/controls/extensions.d.ts +44 -0
  26. package/dist/controls/extensions.js +164 -0
  27. package/dist/controls/foldable/fold-state.d.ts +4 -0
  28. package/dist/controls/foldable/fold-state.js +8 -0
  29. package/dist/controls/foldable/types.d.ts +9 -0
  30. package/dist/controls/foldable/types.js +1 -0
  31. package/dist/controls/html.d.ts +8 -0
  32. package/dist/controls/html.js +260 -0
  33. package/dist/controls/index.d.ts +7 -0
  34. package/dist/controls/index.js +4 -0
  35. package/dist/controls/ionic.d.ts +1 -0
  36. package/dist/controls/ionic.js +1 -0
  37. package/dist/controls/legacy-layout.d.ts +7 -0
  38. package/dist/controls/legacy-layout.js +23 -0
  39. package/dist/controls/overlays/content.d.ts +2 -0
  40. package/dist/controls/overlays/content.js +55 -0
  41. package/dist/controls/overlays/controller.d.ts +7 -0
  42. package/dist/controls/overlays/controller.js +369 -0
  43. package/dist/controls/overlays/covered-overlay.d.ts +4 -0
  44. package/dist/controls/overlays/covered-overlay.js +9 -0
  45. package/dist/controls/overlays/dialog-animation.d.ts +7 -0
  46. package/dist/controls/overlays/dialog-animation.js +49 -0
  47. package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
  48. package/dist/controls/overlays/dismiss-animation.js +14 -0
  49. package/dist/controls/overlays/focus.d.ts +2 -0
  50. package/dist/controls/overlays/focus.js +67 -0
  51. package/dist/controls/overlays/popover.d.ts +10 -0
  52. package/dist/controls/overlays/popover.js +117 -0
  53. package/dist/controls/overlays/styles.d.ts +9 -0
  54. package/dist/controls/overlays/styles.js +194 -0
  55. package/dist/controls/prehide.d.ts +6 -0
  56. package/dist/controls/prehide.js +280 -0
  57. package/dist/controls/runtime.d.ts +5 -0
  58. package/dist/controls/runtime.js +1685 -0
  59. package/dist/controls/shared/bridge.d.ts +4 -0
  60. package/dist/controls/shared/bridge.js +15 -0
  61. package/dist/controls/shared/candidate.d.ts +23 -0
  62. package/dist/controls/shared/candidate.js +128 -0
  63. package/dist/controls/shared/clipping.d.ts +1 -0
  64. package/dist/controls/shared/clipping.js +1 -0
  65. package/dist/controls/shared/connection.d.ts +21 -0
  66. package/dist/controls/shared/connection.js +112 -0
  67. package/dist/controls/shared/control-options.d.ts +4 -0
  68. package/dist/controls/shared/control-options.js +14 -0
  69. package/dist/controls/shared/covered-snapshot.d.ts +35 -0
  70. package/dist/controls/shared/covered-snapshot.js +41 -0
  71. package/dist/controls/shared/covered-source.d.ts +22 -0
  72. package/dist/controls/shared/covered-source.js +72 -0
  73. package/dist/controls/shared/crossfade.d.ts +11 -0
  74. package/dist/controls/shared/crossfade.js +71 -0
  75. package/dist/controls/shared/diagnostics.d.ts +2 -0
  76. package/dist/controls/shared/diagnostics.js +16 -0
  77. package/dist/controls/shared/dom.d.ts +45 -0
  78. package/dist/controls/shared/dom.js +258 -0
  79. package/dist/controls/shared/events.d.ts +12 -0
  80. package/dist/controls/shared/events.js +13 -0
  81. package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
  82. package/dist/controls/shared/horizontal-navigation.js +90 -0
  83. package/dist/controls/shared/icons.d.ts +5 -0
  84. package/dist/controls/shared/icons.js +66 -0
  85. package/dist/controls/shared/modal-animation.d.ts +21 -0
  86. package/dist/controls/shared/modal-animation.js +97 -0
  87. package/dist/controls/shared/modal.d.ts +26 -0
  88. package/dist/controls/shared/modal.js +261 -0
  89. package/dist/controls/shared/projection-id.d.ts +2 -0
  90. package/dist/controls/shared/projection-id.js +2 -0
  91. package/dist/controls/shared/scroll-placement.d.ts +1 -0
  92. package/dist/controls/shared/scroll-placement.js +1 -0
  93. package/dist/controls/shared/tab-bar-width.d.ts +7 -0
  94. package/dist/controls/shared/tab-bar-width.js +72 -0
  95. package/dist/controls/shared/toolbar-options.d.ts +10 -0
  96. package/dist/controls/shared/toolbar-options.js +38 -0
  97. package/dist/controls/shared/transition-identity.d.ts +3 -0
  98. package/dist/controls/shared/transition-identity.js +64 -0
  99. package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
  100. package/dist/controls/shared/vertical-bars-geometry.js +82 -0
  101. package/dist/controls/shared/vertical-navigation.d.ts +5 -0
  102. package/dist/controls/shared/vertical-navigation.js +39 -0
  103. package/dist/controls/startup.d.ts +7 -0
  104. package/dist/controls/startup.js +72 -0
  105. package/dist/controls/transport.d.ts +10 -0
  106. package/dist/controls/transport.js +1 -0
  107. package/dist/controls/vertical-bars-web-search.d.ts +8 -0
  108. package/dist/controls/vertical-bars-web-search.js +83 -0
  109. package/dist/controls/vertical-bars-web.d.ts +2 -0
  110. package/dist/controls/vertical-bars-web.js +609 -0
  111. package/dist/index.d.ts +1 -0
  112. package/dist/index.js +1 -0
  113. package/dist/integration/index.d.ts +36 -0
  114. package/dist/integration/index.js +25 -0
  115. package/dist/native/session.js +14 -8
  116. package/dist/projection/pause.d.ts +10 -0
  117. package/dist/projection/pause.js +58 -0
  118. package/dist/projection/styles.d.ts +1 -1
  119. package/dist/projection/styles.js +0 -1
  120. package/dist/projection/surfaces.js +2 -1
  121. package/dist/projection/web.js +39 -5
  122. package/dist/types.d.ts +5 -1
  123. package/docs/README.md +1 -0
  124. package/docs/configuration/README.md +3 -1
  125. package/docs/configuration/ionic-overlays.md +36 -2
  126. package/docs/configuration/pause-resume.md +51 -0
  127. package/docs/controls.md +39 -0
  128. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
  129. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
  130. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
  131. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
  132. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
  133. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
  134. package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
  135. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
  136. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
  137. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
  138. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
  139. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
  140. package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
  141. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
  142. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
  143. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
  144. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
  145. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
  146. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
  147. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
  148. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
  149. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
  150. package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
  151. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +1 -1
  152. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
  153. package/package.json +30 -5
  154. package/src/controls/activation.ts +427 -0
  155. package/src/controls/components/index.ts +45 -0
  156. package/src/controls/components/ion-back-button.ts +22 -0
  157. package/src/controls/components/ion-button.ts +28 -0
  158. package/src/controls/components/ion-buttons.ts +37 -0
  159. package/src/controls/components/ion-fab.ts +151 -0
  160. package/src/controls/components/ion-menu-button.ts +37 -0
  161. package/src/controls/components/ion-segment.ts +27 -0
  162. package/src/controls/components/ion-tab-bar.ts +55 -0
  163. package/src/controls/components/searchable-tabs.ts +534 -0
  164. package/src/controls/definitions.ts +338 -0
  165. package/src/controls/extensions.ts +198 -0
  166. package/src/controls/foldable/fold-state.ts +10 -0
  167. package/src/controls/foldable/types.ts +4 -0
  168. package/src/controls/html.ts +239 -0
  169. package/src/controls/index.ts +7 -0
  170. package/src/controls/ionic.ts +1 -0
  171. package/src/controls/legacy-layout.ts +21 -0
  172. package/src/controls/overlays/content.ts +51 -0
  173. package/src/controls/overlays/controller.ts +394 -0
  174. package/src/controls/overlays/covered-overlay.ts +11 -0
  175. package/src/controls/overlays/dialog-animation.ts +53 -0
  176. package/src/controls/overlays/dismiss-animation.ts +15 -0
  177. package/src/controls/overlays/focus.ts +64 -0
  178. package/src/controls/overlays/popover.ts +116 -0
  179. package/src/controls/overlays/styles.ts +195 -0
  180. package/src/controls/prehide.ts +285 -0
  181. package/src/controls/runtime.ts +1731 -0
  182. package/src/controls/shared/bridge.ts +18 -0
  183. package/src/controls/shared/candidate.ts +163 -0
  184. package/src/controls/shared/clipping.ts +1 -0
  185. package/src/controls/shared/connection.ts +105 -0
  186. package/src/controls/shared/control-options.ts +20 -0
  187. package/src/controls/shared/covered-snapshot.ts +52 -0
  188. package/src/controls/shared/covered-source.ts +78 -0
  189. package/src/controls/shared/crossfade.ts +70 -0
  190. package/src/controls/shared/diagnostics.ts +13 -0
  191. package/src/controls/shared/dom.ts +310 -0
  192. package/src/controls/shared/events.ts +14 -0
  193. package/src/controls/shared/horizontal-navigation.ts +106 -0
  194. package/src/controls/shared/icons.ts +64 -0
  195. package/src/controls/shared/modal-animation.ts +109 -0
  196. package/src/controls/shared/modal.ts +286 -0
  197. package/src/controls/shared/projection-id.ts +2 -0
  198. package/src/controls/shared/scroll-placement.ts +1 -0
  199. package/src/controls/shared/tab-bar-width.ts +66 -0
  200. package/src/controls/shared/toolbar-options.ts +51 -0
  201. package/src/controls/shared/transition-identity.ts +67 -0
  202. package/src/controls/shared/vertical-bars-geometry.ts +92 -0
  203. package/src/controls/shared/vertical-navigation.ts +34 -0
  204. package/src/controls/startup.ts +74 -0
  205. package/src/controls/transport.ts +11 -0
  206. package/src/controls/vertical-bars-web-search.ts +86 -0
  207. package/src/controls/vertical-bars-web.ts +674 -0
  208. package/src/dom/geometry.ts +24 -0
  209. package/src/dom/overlays.ts +196 -0
  210. package/src/dom/scroll-bindings.ts +66 -0
  211. package/src/dom/scroll-placement.ts +92 -0
  212. package/src/dom/visibility.ts +29 -0
  213. package/src/index.ts +19 -0
  214. package/src/integration/index.ts +64 -0
  215. package/src/ionic.ts +34 -0
  216. package/src/native/bootstrap.ts +132 -0
  217. package/src/native/bridge.ts +18 -0
  218. package/src/native/resource-url.ts +11 -0
  219. package/src/native/session.ts +708 -0
  220. package/src/native/updates.ts +103 -0
  221. package/src/native/values.ts +29 -0
  222. package/src/projection/events.ts +58 -0
  223. package/src/projection/pause.ts +58 -0
  224. package/src/projection/placement.ts +34 -0
  225. package/src/projection/scope.ts +13 -0
  226. package/src/projection/styles.ts +26 -0
  227. package/src/projection/surface.ts +88 -0
  228. package/src/projection/surfaces.ts +119 -0
  229. package/src/projection/web.ts +1013 -0
  230. package/src/types.ts +58 -0
@@ -0,0 +1,18 @@
1
+ import type { NativeUIListenerHandle as PluginListenerHandle } from '../transport.js';
2
+
3
+ // A failed bridge must not leave the source inaccessible indefinitely.
4
+ export const bounded = <T>(promise: Promise<T>): Promise<T> =>
5
+ new Promise((resolve, reject) => {
6
+ const timer = setTimeout(() => reject(new Error('Native UI bridge timed out')), 5000);
7
+ promise.then(resolve, reject).finally(() => clearTimeout(timer));
8
+ });
9
+
10
+ /** A timed-out registration still owns a native resource if it eventually succeeds. */
11
+ export const boundedListener = async (registration: Promise<PluginListenerHandle>): Promise<PluginListenerHandle> => {
12
+ try {
13
+ return await bounded(registration);
14
+ } catch (error) {
15
+ void registration.then((listener) => listener.remove()).catch(() => {});
16
+ throw error;
17
+ }
18
+ };
@@ -0,0 +1,163 @@
1
+ import type { NativeUIControl, NativeUIItem, NativeUIControlKind, NativeUIComponent, VerticalControlAreaOptions } from '../definitions.js';
2
+ import { frame, isDark, isVerticalBarsSource, text, visible } from './dom.js';
3
+ import { iconSource, type createIconRenderer } from './icons.js';
4
+
5
+ export interface Candidate {
6
+ scrollElement?: HTMLElement;
7
+ assets?: { signature: string; render(renderer: ReturnType<typeof createIconRenderer>): Promise<void> };
8
+ buttonProjection?: 'system' | 'source';
9
+ buttonDefaultFill?: 'solid' | null;
10
+ element: HTMLElement;
11
+ sources?: HTMLElement[];
12
+ control: NativeUIControl;
13
+ actions: Map<string, HTMLElement>;
14
+ icons: { item: NativeUIItem; source: string; field?: 'closeIcon' }[];
15
+ }
16
+
17
+ export type Identify = (element: HTMLElement) => string;
18
+ type ItemElement = HTMLElement & { disabled?: boolean; selected?: boolean };
19
+
20
+ export const createCandidate = (
21
+ element: HTMLElement,
22
+ kind: NativeUIComponent | NativeUIControlKind,
23
+ id: Identify,
24
+ options: VerticalControlAreaOptions = {},
25
+ ): Candidate => {
26
+ const rect = element.getBoundingClientRect();
27
+ const style = getComputedStyle(element);
28
+ return {
29
+ element,
30
+ buttonProjection: options.ionic?.verticalBars?.buttonProjection ?? 'system',
31
+ buttonDefaultFill: options.ionic?.verticalBars?.buttonDefaultFill === 'solid' ? 'solid' : null,
32
+ control: {
33
+ id: id(element),
34
+ kind:
35
+ (
36
+ {
37
+ 'ion-button': 'button',
38
+ 'ion-buttons': 'button-group',
39
+ 'ion-back-button': 'back-button',
40
+ 'ion-menu-button': 'menu-button',
41
+ 'ion-tab-bar': 'tab-bar',
42
+ 'ion-segment': 'segment',
43
+ 'ion-fab': 'floating-action-button',
44
+ } as Partial<Record<string, NativeUIControlKind>>
45
+ )[kind] ?? (kind as NativeUIControlKind),
46
+ ...frame(rect),
47
+ items: [],
48
+ dark: isDark(style),
49
+ rtl: style.direction === 'rtl',
50
+ },
51
+ actions: new Map(),
52
+ icons: [],
53
+ };
54
+ };
55
+
56
+ // Shared text, artwork, geometry and action data; component rules stay with their readers.
57
+ export const appendItem = (
58
+ candidate: Candidate,
59
+ child: HTMLElement,
60
+ id: Identify,
61
+ content: ParentNode = child,
62
+ label = text(child),
63
+ ): NativeUIItem | undefined => {
64
+ if (
65
+ !visible(child, isVerticalBarsSource(candidate.element)) ||
66
+ child.querySelector('input, button, a, img:not([data-native-ui-projection-node]), canvas, video, ion-spinner, ion-avatar')
67
+ )
68
+ return;
69
+ const icons = Array.from(content.querySelectorAll<HTMLElement>('ion-icon'));
70
+ const directSVGs = Array.from(content.querySelectorAll<SVGElement>('svg'));
71
+ if (icons.length + directSVGs.length > 1) return undefined;
72
+ const svg = directSVGs[0] ?? icons[0]?.shadowRoot?.querySelector<SVGElement>('svg');
73
+ if (icons.length && !svg) return undefined; // ion-icon has not finished loading.
74
+ const labelElement = content.querySelector('[part="text"], ion-label') ?? child;
75
+ const native = child.shadowRoot?.querySelector('[part="native"]');
76
+ const labelStyle = getComputedStyle(labelElement);
77
+ const badge = child.querySelector<HTMLElement>('ion-badge');
78
+ const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : undefined;
79
+ const sourceFill = (child as HTMLIonButtonElement).fill;
80
+ const projectionOwner = [child, child.closest('ion-buttons')].find((element) =>
81
+ element?.matches('[data-projection="source"], [data-projection="system"], .ios-theme-projection-source, .ios-theme-projection-system'),
82
+ );
83
+ const attributeProjection = projectionOwner?.getAttribute('data-projection');
84
+ const projection =
85
+ attributeProjection === 'source' || attributeProjection === 'system'
86
+ ? attributeProjection
87
+ : projectionOwner
88
+ ? projectionOwner.classList.contains('ios-theme-projection-system')
89
+ ? 'system'
90
+ : 'source'
91
+ : candidate.buttonProjection;
92
+ const systemButton = projection !== 'source' && isVerticalBarsSource(child) && child.matches('ion-button, ion-menu-button');
93
+ // Ionic defaults buttons inside ion-buttons to clear, even when the app defaults to solid.
94
+ const defaultFill = child.closest('ion-buttons') ? 'clear' : candidate.buttonDefaultFill;
95
+ const fill = !sourceFill || sourceFill === 'default' ? defaultFill : sourceFill;
96
+ const buttonFill =
97
+ !systemButton &&
98
+ isVerticalBarsSource(child) &&
99
+ child.matches('ion-button') &&
100
+ (fill === 'clear' || fill === 'solid' || fill === 'outline')
101
+ ? fill
102
+ : undefined;
103
+ const outline = buttonFill === 'outline' ? getComputedStyle(native ?? child) : undefined;
104
+ const item: NativeUIItem = {
105
+ id: id(child),
106
+ ...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),
107
+ label,
108
+ accessibilityLabel:
109
+ child.getAttribute('aria-label') ??
110
+ native?.getAttribute('aria-label') ??
111
+ (icons[0]?.getAttribute('aria-hidden') === 'true' ? null : icons[0]?.getAttribute('aria-label')) ??
112
+ label,
113
+ disabled:
114
+ !!(child as ItemElement).disabled ||
115
+ !!(candidate.element as ItemElement).disabled ||
116
+ getComputedStyle(child).pointerEvents === 'none',
117
+ selected: !!(child as ItemElement).selected,
118
+ fontSize: parseFloat(labelStyle.fontSize),
119
+ fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,
120
+ color: systemButton ? 'currentColor' : getComputedStyle(native ?? child).color,
121
+ iconTemplate: systemButton ? true : undefined,
122
+ buttonFill,
123
+ backgroundColor: buttonFill === 'solid' ? getComputedStyle(native ?? child).backgroundColor : undefined,
124
+ borderColor: outline?.borderTopColor,
125
+ borderWidth: outline ? parseFloat(outline.borderTopWidth) : undefined,
126
+ badge: badgeStyle
127
+ ? { value: badge!.textContent?.trim() ?? '', color: badgeStyle.backgroundColor, textColor: badgeStyle.color }
128
+ : undefined,
129
+ };
130
+ if (svg) {
131
+ const source = iconSource(svg);
132
+ if (!source) return undefined;
133
+ const size = (icons[0] ?? svg).getBoundingClientRect();
134
+ item.iconWidth = size.width;
135
+ item.iconHeight = size.height;
136
+ if (!size.width || !size.height) return undefined;
137
+ item.iconPosition = (icons[0] ?? svg).getAttribute('slot') === 'end' ? 'trailing' : 'leading';
138
+ candidate.icons.push({ item, source });
139
+ }
140
+ if (native && child.matches('ion-button, ion-back-button')) {
141
+ const style = getComputedStyle(native);
142
+ const icon = icons[0] ?? svg;
143
+ const iconStyle = icon && getComputedStyle(icon);
144
+ const leadingMargin = parseFloat(iconStyle?.marginInlineStart ?? '') || 0;
145
+ const trailingMargin = parseFloat(iconStyle?.marginInlineEnd ?? '') || 0;
146
+ const trailing = item.iconPosition === 'trailing';
147
+ const inner = native.querySelector('.button-inner');
148
+ const gap = inner ? parseFloat(getComputedStyle(inner).columnGap) || 0 : 0;
149
+ // UIKit centers the title/image pair using its insets. Include Ionic's
150
+ // border and negative outer icon margin instead of applying a fixed offset.
151
+ item.contentInsetLeading =
152
+ (parseFloat(style.paddingInlineStart) || 0) +
153
+ (parseFloat(style.borderInlineStartWidth) || 0) +
154
+ (!trailing || !label ? leadingMargin : 0);
155
+ item.contentInsetTrailing =
156
+ (parseFloat(style.paddingInlineEnd) || 0) + (parseFloat(style.borderInlineEndWidth) || 0) + (trailing || !label ? trailingMargin : 0);
157
+ item.imagePadding = label && icon ? gap + (trailing ? leadingMargin : trailingMargin) : 0;
158
+ }
159
+ if (!label && !svg) return undefined;
160
+ candidate.control.items.push(item);
161
+ candidate.actions.set(item.id, child);
162
+ return item;
163
+ };
@@ -0,0 +1 @@
1
+ export { projectionAncestors, fitsClip } from '../../index.js';
@@ -0,0 +1,105 @@
1
+ import type { NativeUIListenerHandle as PluginListenerHandle } from '../transport.js';
2
+ import type { NativeUIConnectionState, NativeUIPlugin } from '../definitions.js';
3
+ import { bounded, boundedListener } from './bridge.js';
4
+
5
+ type Check = (state: NativeUIConnectionState, restored: boolean, requestedAt: number) => void | Promise<void>;
6
+ type Failure = (error: unknown) => void | Promise<void>;
7
+ const failureLimit = 3;
8
+
9
+ type Registration = { name: string; callback: (event: never) => void; handle?: PluginListenerHandle };
10
+
11
+ /** One connection check for the shell, independent of projection retention. */
12
+ export const createNativeConnection = (doc: Document, plugin: NativeUIPlugin) => {
13
+ const registrations = new Set<Registration>();
14
+ const checks = new Set<Check>();
15
+ const failures = new Map<Check, Failure>();
16
+ let stopped = false;
17
+ let timer: ReturnType<typeof setTimeout>;
18
+ let session: string | undefined;
19
+ let failed = false;
20
+ let consecutiveFailures = 0;
21
+ const install = async (registration: Registration) => {
22
+ const add = plugin.addListener as (name: string, callback: (event: never) => void) => Promise<PluginListenerHandle>;
23
+ const handle = await boundedListener(add.call(plugin, registration.name, registration.callback));
24
+ if (stopped || !registrations.has(registration)) await bounded(handle.remove());
25
+ else registration.handle = handle;
26
+ };
27
+ const listen = (async (name: string, callback: (event: never) => void) => {
28
+ const registration: Registration = { name, callback };
29
+ registrations.add(registration);
30
+ try {
31
+ await install(registration);
32
+ } catch (error) {
33
+ registrations.delete(registration);
34
+ throw error;
35
+ }
36
+ return {
37
+ async remove() {
38
+ registrations.delete(registration);
39
+ await registration.handle?.remove();
40
+ },
41
+ };
42
+ }) as NativeUIPlugin['addListener'];
43
+ const check = async () => {
44
+ if (stopped) return;
45
+ if (!doc.hidden) {
46
+ const requestedAt = doc.defaultView!.performance.now();
47
+ const registered = Array.from(registrations);
48
+ const observed = Array.from(checks);
49
+ try {
50
+ const state = await bounded(plugin.getConnectionState());
51
+ if (stopped) return;
52
+ let restored = failed || (session !== undefined && session !== state.session);
53
+ session = state.session;
54
+ for (const registration of registered) {
55
+ if (state.listeners.includes(registration.name) || !registrations.has(registration)) continue;
56
+ // Remove our previous callback before installing another; never replay events.
57
+ await bounded(Promise.resolve(registration.handle?.remove()));
58
+ if (stopped || !registrations.has(registration)) continue;
59
+ await install(registration);
60
+ restored = true;
61
+ }
62
+ if (stopped) return;
63
+ for (const callback of observed) {
64
+ if (checks.has(callback)) await callback(state, restored, requestedAt);
65
+ if (stopped) return;
66
+ }
67
+ failed = false;
68
+ consecutiveFailures = 0;
69
+ } catch (error) {
70
+ if (stopped) return;
71
+ // A later successful handshake repairs subscriptions and resynchronizes
72
+ // current state. A timeout ends this logical check, not the underlying request.
73
+ failed = true;
74
+ if (++consecutiveFailures >= failureLimit) {
75
+ for (const callback of observed) {
76
+ if (checks.has(callback)) await failures.get(callback)?.(error);
77
+ if (stopped) return;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ if (!stopped) timer = setTimeout(() => void check(), 1000);
83
+ };
84
+ return {
85
+ listen,
86
+ observe(callback: Check, failure?: Failure) {
87
+ checks.add(callback);
88
+ if (failure) failures.set(callback, failure);
89
+ return () => {
90
+ checks.delete(callback);
91
+ failures.delete(callback);
92
+ };
93
+ },
94
+ start() {
95
+ timer = setTimeout(() => void check(), 1000);
96
+ },
97
+ stop() {
98
+ stopped = true;
99
+ clearTimeout(timer);
100
+ checks.clear();
101
+ failures.clear();
102
+ },
103
+ };
104
+ };
105
+ export type NativeUIConnection = ReturnType<typeof createNativeConnection>;
@@ -0,0 +1,20 @@
1
+ import type { NativeUIControls, NativeUIOptions } from '../definitions.js';
2
+
3
+ /** Resolve one default without changing discovery, explicit selection, or placement. */
4
+ export const controlDefault = (options: NativeUIOptions, kind: keyof NativeUIControls, vertical: boolean): boolean => {
5
+ const controls = options.controls;
6
+ const value = typeof controls === 'object' ? controls[kind] : controls;
7
+ if (value === undefined || value === true) return true;
8
+ if (value === false) return false;
9
+ return value === (vertical ? 'verticalOnly' : 'horizontalOnly');
10
+ };
11
+
12
+ export const controlSelected = (
13
+ element: HTMLElement,
14
+ options: NativeUIOptions,
15
+ kind: keyof NativeUIControls,
16
+ vertical: boolean,
17
+ ): boolean => {
18
+ const local = element.getAttribute(vertical ? 'data-vertical-bars' : 'data-horizontal-bars');
19
+ return local === 'true' ? true : local === 'false' ? false : controlDefault(options, kind, vertical);
20
+ };
@@ -0,0 +1,52 @@
1
+ import { createWebProjectionSurface } from '../../index.js';
2
+ import type { NativeUIPlugin } from '../definitions.js';
3
+ import { getProjectionState } from './covered-source.js';
4
+
5
+ /** Ionic supplies source ownership and proxy placement; Engine owns image handoff. */
6
+ export const createCoveredSnapshots = async (
7
+ doc: Document,
8
+ plugin: NativeUIPlugin,
9
+ listen: NativeUIPlugin['addListener'],
10
+ callbacks: { wake: () => void; blocked: () => boolean; modal?: HTMLIonModalElement; revision?: () => number },
11
+ ) => {
12
+ const state = getProjectionState(doc);
13
+ const { modal, wake, blocked, revision } = callbacks;
14
+ const projection = await createWebProjectionSurface(
15
+ {
16
+ send: (request) => plugin.coveredSnapshot!(request),
17
+ listen: (listener) => listen('snapshotInteraction', listener),
18
+ },
19
+ {
20
+ namespace: 'native-ui',
21
+ target: ({ element: source, control }) => {
22
+ const proxy = state.getTarget(source);
23
+ return control.placement === 'vertical-bars' &&
24
+ (proxy || source.matches('ion-button, ion-buttons, ion-back-button, ion-menu-button'))
25
+ ? proxy
26
+ : source;
27
+ },
28
+ canRelocate: (target) => target.matches('.ion-cloned-element, [data-native-ui-proxy]'),
29
+ wake,
30
+ blocked,
31
+ revision,
32
+ transient: !!modal,
33
+ eligible: (source) =>
34
+ modal
35
+ ? source.element.closest('ion-modal') === modal && source.control.placement === 'vertical-bars'
36
+ : !source.element.closest('ion-modal'),
37
+ current: (sources) => !modal || sources.every((source) => source.closest('ion-modal') === modal),
38
+ },
39
+ );
40
+ const unregister = state.register(projection);
41
+ const destroy = projection.destroy;
42
+ return Object.assign(projection, {
43
+ async destroy() {
44
+ try {
45
+ await destroy();
46
+ } finally {
47
+ unregister();
48
+ }
49
+ },
50
+ });
51
+ };
52
+ export type CoveredSnapshots = Awaited<ReturnType<typeof createCoveredSnapshots>>;
@@ -0,0 +1,78 @@
1
+ import type { WebProjectionBatch } from '../../index.js';
2
+
3
+ /** Ionic's cross-surface ownership and source/proxy mapping, not a projection DOM scope. */
4
+ const states = new WeakMap<Document, NativeUIProjectionState>();
5
+ const createState = () => {
6
+ const managers = new Set<WebProjectionBatch>();
7
+ const nativeOwners = new Set<(source: HTMLElement) => boolean>();
8
+ const targets = new WeakMap<HTMLElement, HTMLElement>();
9
+ const imageMarker = 'data-native-ui-snapshot';
10
+ const collect = (key: 'owners' | 'visibleOwners' | 'preparedOwners') => [...new Set([...managers].flatMap((manager) => manager[key]))];
11
+ return {
12
+ imageMarker,
13
+ nativeOwns: (source: HTMLElement) => [...nativeOwners].some((owns) => owns(source)),
14
+ registerNativeOwner(owns: (source: HTMLElement) => boolean) {
15
+ nativeOwners.add(owns);
16
+ return () => nativeOwners.delete(owns);
17
+ },
18
+ get snapshotsEnabled() {
19
+ return managers.size > 0;
20
+ },
21
+ sourcesChangedEvent: 'native-ui:sources-change',
22
+ targetsChangedEvent: 'native-ui:targets-change',
23
+ register(manager: WebProjectionBatch) {
24
+ managers.add(manager);
25
+ return () => managers.delete(manager);
26
+ },
27
+ get owners(): readonly HTMLElement[] {
28
+ return collect('owners');
29
+ },
30
+ get visibleOwners(): readonly HTMLElement[] {
31
+ return collect('visibleOwners');
32
+ },
33
+ get preparedOwners(): readonly HTMLElement[] {
34
+ return collect('preparedOwners');
35
+ },
36
+ owns: (element: HTMLElement) => collect('owners').some((owner) => owner === element || owner.contains(element)),
37
+ isVisible: (element: HTMLElement) => collect('visibleOwners').some((owner) => owner === element || owner.contains(element)),
38
+ getTarget: (source: HTMLElement) => targets.get(source),
39
+ setTarget: (source: HTMLElement, target: HTMLElement) => {
40
+ targets.set(source, target);
41
+ },
42
+ deleteTarget: (source: HTMLElement) => targets.delete(source),
43
+ isProjectionMutation(record: MutationRecord) {
44
+ const target = record.target instanceof Element ? record.target : record.target.parentElement;
45
+ if (target?.closest('[data-native-ui-projection-node]')) return true;
46
+ if (target?.closest('.ion-cloned-element, [data-native-ui-proxy]')) return true;
47
+ if (
48
+ record.type === 'attributes' &&
49
+ [
50
+ imageMarker,
51
+ 'data-native-ui-proxy-prepared',
52
+ 'data-native-ui-snapshot-prepared',
53
+ 'data-native-ui-snapshot-source',
54
+ 'data-native-ui-snapshot-visible',
55
+ 'data-native-ui-snapshot-content',
56
+ 'data-native-ui-snapshot-positioned',
57
+ ].includes(record.attributeName ?? '')
58
+ )
59
+ return true;
60
+ const image = (node: Node) =>
61
+ node instanceof Element &&
62
+ node.matches(
63
+ `style[${imageMarker}], style[data-native-ui-snapshot-base], [data-native-ui-proxy-prepared], [data-native-ui-snapshot-prepared], [data-native-ui-projection-node]`,
64
+ );
65
+ const nodes = [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)];
66
+ return image(record.target) || (record.type === 'childList' && nodes.length > 0 && nodes.every(image));
67
+ },
68
+ };
69
+ };
70
+ export type NativeUIProjectionState = ReturnType<typeof createState>;
71
+ export const getProjectionState = (doc: Document): NativeUIProjectionState => {
72
+ let state = states.get(doc);
73
+ if (!state) {
74
+ state = createState();
75
+ states.set(doc, state);
76
+ }
77
+ return state;
78
+ };
@@ -0,0 +1,70 @@
1
+ export const handoffDuration = 180;
2
+ export const fadeMarker = 'data-native-ui-fading';
3
+
4
+ /** Only visual opacity animates; the runtime transfers input/accessibility ownership once. */
5
+ export const createCrossfade = (win: Window) => {
6
+ const active = new Map<HTMLElement, Animation>();
7
+ const retired = new Set<HTMLElement>();
8
+ const reduced = win.matchMedia('(prefers-reduced-motion: reduce)');
9
+ const finish = (element: HTMLElement, animation: Animation) => {
10
+ if (active.get(element) !== animation) return;
11
+ active.delete(element);
12
+ element.removeAttribute(fadeMarker);
13
+ animation.cancel();
14
+ if (retired.delete(element)) element.remove();
15
+ };
16
+ const cancelAll = () => {
17
+ for (const [element, animation] of active) finish(element, animation);
18
+ };
19
+ reduced.addEventListener('change', cancelAll);
20
+ const api = {
21
+ duration: (instant = false) => (reduced.matches || instant ? 0 : handoffDuration),
22
+ play(element: HTMLElement, toNative: boolean, instant = false) {
23
+ const previous = active.get(element);
24
+ // Instant handoffs must not force layout before retiring an outgoing tab.
25
+ if (reduced.matches || instant || !element.isConnected) {
26
+ if (previous) finish(element, previous);
27
+ element.removeAttribute(fadeMarker);
28
+ return;
29
+ }
30
+ const current = Number(win.getComputedStyle(element).opacity);
31
+ previous?.cancel();
32
+ active.delete(element);
33
+ // Measure the Web appearance after lifting ownership concealment.
34
+ element.setAttribute(fadeMarker, '');
35
+ const opacity = Number(win.getComputedStyle(element).opacity);
36
+ const animation = element.animate([{ opacity: previous ? current : toNative ? opacity : 0 }, { opacity: toNative ? 0 : opacity }], {
37
+ duration: handoffDuration,
38
+ easing: 'ease-in-out',
39
+ fill: 'both',
40
+ });
41
+ active.set(element, animation);
42
+ void animation.finished.then(
43
+ () => finish(element, animation),
44
+ () => finish(element, animation),
45
+ );
46
+ },
47
+ retire(element: HTMLElement, instant = false) {
48
+ element.inert = true;
49
+ element.setAttribute('aria-hidden', 'true');
50
+ element.style.setProperty('pointer-events', 'none', 'important');
51
+ if (!api.duration(instant) || !element.isConnected) {
52
+ const previous = active.get(element);
53
+ if (previous) finish(element, previous);
54
+ element.remove();
55
+ return;
56
+ }
57
+ retired.add(element);
58
+ api.play(element, true);
59
+ },
60
+ finish: cancelAll,
61
+ async settled() {
62
+ await Promise.all(Array.from(active.values(), (animation) => animation.finished.catch(() => {})));
63
+ },
64
+ destroy() {
65
+ reduced.removeEventListener('change', cancelAll);
66
+ cancelAll();
67
+ },
68
+ };
69
+ return api;
70
+ };
@@ -0,0 +1,13 @@
1
+ /** Opt-in diagnostics. Never log props, action data, or application exception messages. */
2
+ export const createDiagnostics = (enabled = false) => {
3
+ const elements = new WeakMap<HTMLElement, Set<string>>();
4
+ const global = new Set<string>();
5
+ return (reason: string, element?: HTMLElement): void => {
6
+ if (!enabled) return;
7
+ const seen = element ? (elements.get(element) ?? new Set<string>()) : global;
8
+ if (seen.has(reason)) return;
9
+ seen.add(reason);
10
+ if (element) elements.set(element, seen);
11
+ console.info(`[Native UI] ${reason}`, ...(element ? [element] : []));
12
+ };
13
+ };