@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,4 @@
1
+ import type { NativeUIListenerHandle as PluginListenerHandle } from '../transport.js';
2
+ export declare const bounded: <T>(promise: Promise<T>) => Promise<T>;
3
+ /** A timed-out registration still owns a native resource if it eventually succeeds. */
4
+ export declare const boundedListener: (registration: Promise<PluginListenerHandle>) => Promise<PluginListenerHandle>;
@@ -0,0 +1,15 @@
1
+ // A failed bridge must not leave the source inaccessible indefinitely.
2
+ export const bounded = (promise) => new Promise((resolve, reject) => {
3
+ const timer = setTimeout(() => reject(new Error('Native UI bridge timed out')), 5000);
4
+ promise.then(resolve, reject).finally(() => clearTimeout(timer));
5
+ });
6
+ /** A timed-out registration still owns a native resource if it eventually succeeds. */
7
+ export const boundedListener = async (registration) => {
8
+ try {
9
+ return await bounded(registration);
10
+ }
11
+ catch (error) {
12
+ void registration.then((listener) => listener.remove()).catch(() => { });
13
+ throw error;
14
+ }
15
+ };
@@ -0,0 +1,23 @@
1
+ import type { NativeUIControl, NativeUIItem, NativeUIControlKind, NativeUIComponent, VerticalControlAreaOptions } from '../definitions.js';
2
+ import { type createIconRenderer } from './icons.js';
3
+ export interface Candidate {
4
+ scrollElement?: HTMLElement;
5
+ assets?: {
6
+ signature: string;
7
+ render(renderer: ReturnType<typeof createIconRenderer>): Promise<void>;
8
+ };
9
+ buttonProjection?: 'system' | 'source';
10
+ buttonDefaultFill?: 'solid' | null;
11
+ element: HTMLElement;
12
+ sources?: HTMLElement[];
13
+ control: NativeUIControl;
14
+ actions: Map<string, HTMLElement>;
15
+ icons: {
16
+ item: NativeUIItem;
17
+ source: string;
18
+ field?: 'closeIcon';
19
+ }[];
20
+ }
21
+ export type Identify = (element: HTMLElement) => string;
22
+ export declare const createCandidate: (element: HTMLElement, kind: NativeUIComponent | NativeUIControlKind, id: Identify, options?: VerticalControlAreaOptions) => Candidate;
23
+ export declare const appendItem: (candidate: Candidate, child: HTMLElement, id: Identify, content?: ParentNode, label?: string) => NativeUIItem | undefined;
@@ -0,0 +1,128 @@
1
+ import { frame, isDark, isVerticalBarsSource, text, visible } from './dom.js';
2
+ import { iconSource } from './icons.js';
3
+ export const createCandidate = (element, kind, id, options = {}) => {
4
+ const rect = element.getBoundingClientRect();
5
+ const style = getComputedStyle(element);
6
+ return {
7
+ element,
8
+ buttonProjection: options.ionic?.verticalBars?.buttonProjection ?? 'system',
9
+ buttonDefaultFill: options.ionic?.verticalBars?.buttonDefaultFill === 'solid' ? 'solid' : null,
10
+ control: {
11
+ id: id(element),
12
+ kind: {
13
+ 'ion-button': 'button',
14
+ 'ion-buttons': 'button-group',
15
+ 'ion-back-button': 'back-button',
16
+ 'ion-menu-button': 'menu-button',
17
+ 'ion-tab-bar': 'tab-bar',
18
+ 'ion-segment': 'segment',
19
+ 'ion-fab': 'floating-action-button',
20
+ }[kind] ?? kind,
21
+ ...frame(rect),
22
+ items: [],
23
+ dark: isDark(style),
24
+ rtl: style.direction === 'rtl',
25
+ },
26
+ actions: new Map(),
27
+ icons: [],
28
+ };
29
+ };
30
+ // Shared text, artwork, geometry and action data; component rules stay with their readers.
31
+ export const appendItem = (candidate, child, id, content = child, label = text(child)) => {
32
+ if (!visible(child, isVerticalBarsSource(candidate.element)) ||
33
+ child.querySelector('input, button, a, img:not([data-native-ui-projection-node]), canvas, video, ion-spinner, ion-avatar'))
34
+ return;
35
+ const icons = Array.from(content.querySelectorAll('ion-icon'));
36
+ const directSVGs = Array.from(content.querySelectorAll('svg'));
37
+ if (icons.length + directSVGs.length > 1)
38
+ return undefined;
39
+ const svg = directSVGs[0] ?? icons[0]?.shadowRoot?.querySelector('svg');
40
+ if (icons.length && !svg)
41
+ return undefined; // ion-icon has not finished loading.
42
+ const labelElement = content.querySelector('[part="text"], ion-label') ?? child;
43
+ const native = child.shadowRoot?.querySelector('[part="native"]');
44
+ const labelStyle = getComputedStyle(labelElement);
45
+ const badge = child.querySelector('ion-badge');
46
+ const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : undefined;
47
+ const sourceFill = child.fill;
48
+ const projectionOwner = [child, child.closest('ion-buttons')].find((element) => element?.matches('[data-projection="source"], [data-projection="system"], .ios-theme-projection-source, .ios-theme-projection-system'));
49
+ const attributeProjection = projectionOwner?.getAttribute('data-projection');
50
+ const projection = attributeProjection === 'source' || attributeProjection === 'system'
51
+ ? attributeProjection
52
+ : projectionOwner
53
+ ? projectionOwner.classList.contains('ios-theme-projection-system')
54
+ ? 'system'
55
+ : 'source'
56
+ : candidate.buttonProjection;
57
+ const systemButton = projection !== 'source' && isVerticalBarsSource(child) && child.matches('ion-button, ion-menu-button');
58
+ // Ionic defaults buttons inside ion-buttons to clear, even when the app defaults to solid.
59
+ const defaultFill = child.closest('ion-buttons') ? 'clear' : candidate.buttonDefaultFill;
60
+ const fill = !sourceFill || sourceFill === 'default' ? defaultFill : sourceFill;
61
+ const buttonFill = !systemButton &&
62
+ isVerticalBarsSource(child) &&
63
+ child.matches('ion-button') &&
64
+ (fill === 'clear' || fill === 'solid' || fill === 'outline')
65
+ ? fill
66
+ : undefined;
67
+ const outline = buttonFill === 'outline' ? getComputedStyle(native ?? child) : undefined;
68
+ const item = {
69
+ id: id(child),
70
+ ...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),
71
+ label,
72
+ accessibilityLabel: child.getAttribute('aria-label') ??
73
+ native?.getAttribute('aria-label') ??
74
+ (icons[0]?.getAttribute('aria-hidden') === 'true' ? null : icons[0]?.getAttribute('aria-label')) ??
75
+ label,
76
+ disabled: !!child.disabled ||
77
+ !!candidate.element.disabled ||
78
+ getComputedStyle(child).pointerEvents === 'none',
79
+ selected: !!child.selected,
80
+ fontSize: parseFloat(labelStyle.fontSize),
81
+ fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,
82
+ color: systemButton ? 'currentColor' : getComputedStyle(native ?? child).color,
83
+ iconTemplate: systemButton ? true : undefined,
84
+ buttonFill,
85
+ backgroundColor: buttonFill === 'solid' ? getComputedStyle(native ?? child).backgroundColor : undefined,
86
+ borderColor: outline?.borderTopColor,
87
+ borderWidth: outline ? parseFloat(outline.borderTopWidth) : undefined,
88
+ badge: badgeStyle
89
+ ? { value: badge.textContent?.trim() ?? '', color: badgeStyle.backgroundColor, textColor: badgeStyle.color }
90
+ : undefined,
91
+ };
92
+ if (svg) {
93
+ const source = iconSource(svg);
94
+ if (!source)
95
+ return undefined;
96
+ const size = (icons[0] ?? svg).getBoundingClientRect();
97
+ item.iconWidth = size.width;
98
+ item.iconHeight = size.height;
99
+ if (!size.width || !size.height)
100
+ return undefined;
101
+ item.iconPosition = (icons[0] ?? svg).getAttribute('slot') === 'end' ? 'trailing' : 'leading';
102
+ candidate.icons.push({ item, source });
103
+ }
104
+ if (native && child.matches('ion-button, ion-back-button')) {
105
+ const style = getComputedStyle(native);
106
+ const icon = icons[0] ?? svg;
107
+ const iconStyle = icon && getComputedStyle(icon);
108
+ const leadingMargin = parseFloat(iconStyle?.marginInlineStart ?? '') || 0;
109
+ const trailingMargin = parseFloat(iconStyle?.marginInlineEnd ?? '') || 0;
110
+ const trailing = item.iconPosition === 'trailing';
111
+ const inner = native.querySelector('.button-inner');
112
+ const gap = inner ? parseFloat(getComputedStyle(inner).columnGap) || 0 : 0;
113
+ // UIKit centers the title/image pair using its insets. Include Ionic's
114
+ // border and negative outer icon margin instead of applying a fixed offset.
115
+ item.contentInsetLeading =
116
+ (parseFloat(style.paddingInlineStart) || 0) +
117
+ (parseFloat(style.borderInlineStartWidth) || 0) +
118
+ (!trailing || !label ? leadingMargin : 0);
119
+ item.contentInsetTrailing =
120
+ (parseFloat(style.paddingInlineEnd) || 0) + (parseFloat(style.borderInlineEndWidth) || 0) + (trailing || !label ? trailingMargin : 0);
121
+ item.imagePadding = label && icon ? gap + (trailing ? leadingMargin : trailingMargin) : 0;
122
+ }
123
+ if (!label && !svg)
124
+ return undefined;
125
+ candidate.control.items.push(item);
126
+ candidate.actions.set(item.id, child);
127
+ return item;
128
+ };
@@ -0,0 +1 @@
1
+ export { projectionAncestors, fitsClip } from '../../index.js';
@@ -0,0 +1 @@
1
+ export { projectionAncestors, fitsClip } from '../../index.js';
@@ -0,0 +1,21 @@
1
+ import type { NativeUIListenerHandle as PluginListenerHandle } from '../transport.js';
2
+ import type { NativeUIConnectionState, NativeUIPlugin } from '../definitions.js';
3
+ type Check = (state: NativeUIConnectionState, restored: boolean, requestedAt: number) => void | Promise<void>;
4
+ type Failure = (error: unknown) => void | Promise<void>;
5
+ /** One connection check for the shell, independent of projection retention. */
6
+ export declare const createNativeConnection: (doc: Document, plugin: NativeUIPlugin) => {
7
+ listen: {
8
+ (name: "snapshotInteraction", listener: (event: {
9
+ active: boolean;
10
+ }) => void): Promise<PluginListenerHandle>;
11
+ (name: "activate", listener: (event: import("../definitions.js").NativeUIActivation) => void): Promise<PluginListenerHandle>;
12
+ (name: "search", listener: (event: import("../definitions.js").NativeUISearchEvent) => void): Promise<PluginListenerHandle>;
13
+ (name: "webViewMetricsChange", listener: (event: import("../definitions.js").WebViewMetrics) => void): Promise<PluginListenerHandle>;
14
+ (name: "overlay", listener: (event: import("../definitions.js").NativeUIOverlayEvent) => void): Promise<PluginListenerHandle>;
15
+ };
16
+ observe(callback: Check, failure?: Failure): () => void;
17
+ start(): void;
18
+ stop(): void;
19
+ };
20
+ export type NativeUIConnection = ReturnType<typeof createNativeConnection>;
21
+ export {};
@@ -0,0 +1,112 @@
1
+ import { bounded, boundedListener } from './bridge.js';
2
+ const failureLimit = 3;
3
+ /** One connection check for the shell, independent of projection retention. */
4
+ export const createNativeConnection = (doc, plugin) => {
5
+ const registrations = new Set();
6
+ const checks = new Set();
7
+ const failures = new Map();
8
+ let stopped = false;
9
+ let timer;
10
+ let session;
11
+ let failed = false;
12
+ let consecutiveFailures = 0;
13
+ const install = async (registration) => {
14
+ const add = plugin.addListener;
15
+ const handle = await boundedListener(add.call(plugin, registration.name, registration.callback));
16
+ if (stopped || !registrations.has(registration))
17
+ await bounded(handle.remove());
18
+ else
19
+ registration.handle = handle;
20
+ };
21
+ const listen = (async (name, callback) => {
22
+ const registration = { name, callback };
23
+ registrations.add(registration);
24
+ try {
25
+ await install(registration);
26
+ }
27
+ catch (error) {
28
+ registrations.delete(registration);
29
+ throw error;
30
+ }
31
+ return {
32
+ async remove() {
33
+ registrations.delete(registration);
34
+ await registration.handle?.remove();
35
+ },
36
+ };
37
+ });
38
+ const check = async () => {
39
+ if (stopped)
40
+ return;
41
+ if (!doc.hidden) {
42
+ const requestedAt = doc.defaultView.performance.now();
43
+ const registered = Array.from(registrations);
44
+ const observed = Array.from(checks);
45
+ try {
46
+ const state = await bounded(plugin.getConnectionState());
47
+ if (stopped)
48
+ return;
49
+ let restored = failed || (session !== undefined && session !== state.session);
50
+ session = state.session;
51
+ for (const registration of registered) {
52
+ if (state.listeners.includes(registration.name) || !registrations.has(registration))
53
+ continue;
54
+ // Remove our previous callback before installing another; never replay events.
55
+ await bounded(Promise.resolve(registration.handle?.remove()));
56
+ if (stopped || !registrations.has(registration))
57
+ continue;
58
+ await install(registration);
59
+ restored = true;
60
+ }
61
+ if (stopped)
62
+ return;
63
+ for (const callback of observed) {
64
+ if (checks.has(callback))
65
+ await callback(state, restored, requestedAt);
66
+ if (stopped)
67
+ return;
68
+ }
69
+ failed = false;
70
+ consecutiveFailures = 0;
71
+ }
72
+ catch (error) {
73
+ if (stopped)
74
+ return;
75
+ // A later successful handshake repairs subscriptions and resynchronizes
76
+ // current state. A timeout ends this logical check, not the underlying request.
77
+ failed = true;
78
+ if (++consecutiveFailures >= failureLimit) {
79
+ for (const callback of observed) {
80
+ if (checks.has(callback))
81
+ await failures.get(callback)?.(error);
82
+ if (stopped)
83
+ return;
84
+ }
85
+ }
86
+ }
87
+ }
88
+ if (!stopped)
89
+ timer = setTimeout(() => void check(), 1000);
90
+ };
91
+ return {
92
+ listen,
93
+ observe(callback, failure) {
94
+ checks.add(callback);
95
+ if (failure)
96
+ failures.set(callback, failure);
97
+ return () => {
98
+ checks.delete(callback);
99
+ failures.delete(callback);
100
+ };
101
+ },
102
+ start() {
103
+ timer = setTimeout(() => void check(), 1000);
104
+ },
105
+ stop() {
106
+ stopped = true;
107
+ clearTimeout(timer);
108
+ checks.clear();
109
+ failures.clear();
110
+ },
111
+ };
112
+ };
@@ -0,0 +1,4 @@
1
+ import type { NativeUIControls, NativeUIOptions } from '../definitions.js';
2
+ /** Resolve one default without changing discovery, explicit selection, or placement. */
3
+ export declare const controlDefault: (options: NativeUIOptions, kind: keyof NativeUIControls, vertical: boolean) => boolean;
4
+ export declare const controlSelected: (element: HTMLElement, options: NativeUIOptions, kind: keyof NativeUIControls, vertical: boolean) => boolean;
@@ -0,0 +1,14 @@
1
+ /** Resolve one default without changing discovery, explicit selection, or placement. */
2
+ export const controlDefault = (options, kind, vertical) => {
3
+ const controls = options.controls;
4
+ const value = typeof controls === 'object' ? controls[kind] : controls;
5
+ if (value === undefined || value === true)
6
+ return true;
7
+ if (value === false)
8
+ return false;
9
+ return value === (vertical ? 'verticalOnly' : 'horizontalOnly');
10
+ };
11
+ export const controlSelected = (element, options, kind, vertical) => {
12
+ const local = element.getAttribute(vertical ? 'data-vertical-bars' : 'data-horizontal-bars');
13
+ return local === 'true' ? true : local === 'false' ? false : controlDefault(options, kind, vertical);
14
+ };
@@ -0,0 +1,35 @@
1
+ import type { NativeUIPlugin } from '../definitions.js';
2
+ /** Ionic supplies source ownership and proxy placement; Engine owns image handoff. */
3
+ export declare const createCoveredSnapshots: (doc: Document, plugin: NativeUIPlugin, listen: NativeUIPlugin["addListener"], callbacks: {
4
+ wake: () => void;
5
+ blocked: () => boolean;
6
+ modal?: HTMLIonModalElement;
7
+ revision?: () => number;
8
+ }) => Promise<Omit<{
9
+ readonly owners: readonly HTMLElement[];
10
+ readonly visibleOwners: readonly HTMLElement[];
11
+ readonly preparedOwners: readonly HTMLElement[];
12
+ consider: (revision: number, candidates: import("../../types.js").ScopedWebProjectionSource[]) => void;
13
+ changed: (scrollOwner?: EventTarget) => void;
14
+ moved: () => void;
15
+ readonly token: string | undefined;
16
+ begin: () => void;
17
+ readonly restoring: boolean;
18
+ readonly handoffPending: boolean;
19
+ retireRestoration: () => void;
20
+ end: () => void;
21
+ updated: (token: string | undefined, restoredIDs?: string[], revision?: number) => void;
22
+ invalidate: (nativeHidden?: boolean) => void;
23
+ destroy(): Promise<void>;
24
+ }, "consider"> & {
25
+ consider(revision: number, sources: import("../../types.js").ScopedWebProjectionSource[]): void;
26
+ } & {
27
+ consider(revision: number, sources: import("../../types.js").WebProjectionSource[]): void;
28
+ begin(layer?: HTMLElement): void;
29
+ updated(token: string | undefined, ids?: string[], revision?: number): void;
30
+ invalidate(nativeHidden?: boolean): void;
31
+ destroy(): Promise<void>;
32
+ } & {
33
+ destroy(): Promise<void>;
34
+ }>;
35
+ export type CoveredSnapshots = Awaited<ReturnType<typeof createCoveredSnapshots>>;
@@ -0,0 +1,41 @@
1
+ import { createWebProjectionSurface } from '../../index.js';
2
+ import { getProjectionState } from './covered-source.js';
3
+ /** Ionic supplies source ownership and proxy placement; Engine owns image handoff. */
4
+ export const createCoveredSnapshots = async (doc, plugin, listen, callbacks) => {
5
+ const state = getProjectionState(doc);
6
+ const { modal, wake, blocked, revision } = callbacks;
7
+ const projection = await createWebProjectionSurface({
8
+ send: (request) => plugin.coveredSnapshot(request),
9
+ listen: (listener) => listen('snapshotInteraction', listener),
10
+ }, {
11
+ namespace: 'native-ui',
12
+ target: ({ element: source, control }) => {
13
+ const proxy = state.getTarget(source);
14
+ return control.placement === 'vertical-bars' &&
15
+ (proxy || source.matches('ion-button, ion-buttons, ion-back-button, ion-menu-button'))
16
+ ? proxy
17
+ : source;
18
+ },
19
+ canRelocate: (target) => target.matches('.ion-cloned-element, [data-native-ui-proxy]'),
20
+ wake,
21
+ blocked,
22
+ revision,
23
+ transient: !!modal,
24
+ eligible: (source) => modal
25
+ ? source.element.closest('ion-modal') === modal && source.control.placement === 'vertical-bars'
26
+ : !source.element.closest('ion-modal'),
27
+ current: (sources) => !modal || sources.every((source) => source.closest('ion-modal') === modal),
28
+ });
29
+ const unregister = state.register(projection);
30
+ const destroy = projection.destroy;
31
+ return Object.assign(projection, {
32
+ async destroy() {
33
+ try {
34
+ await destroy();
35
+ }
36
+ finally {
37
+ unregister();
38
+ }
39
+ },
40
+ });
41
+ };
@@ -0,0 +1,22 @@
1
+ import type { WebProjectionBatch } from '../../index.js';
2
+ declare const createState: () => {
3
+ imageMarker: string;
4
+ nativeOwns: (source: HTMLElement) => boolean;
5
+ registerNativeOwner(owns: (source: HTMLElement) => boolean): () => boolean;
6
+ readonly snapshotsEnabled: boolean;
7
+ sourcesChangedEvent: string;
8
+ targetsChangedEvent: string;
9
+ register(manager: WebProjectionBatch): () => boolean;
10
+ readonly owners: readonly HTMLElement[];
11
+ readonly visibleOwners: readonly HTMLElement[];
12
+ readonly preparedOwners: readonly HTMLElement[];
13
+ owns: (element: HTMLElement) => boolean;
14
+ isVisible: (element: HTMLElement) => boolean;
15
+ getTarget: (source: HTMLElement) => HTMLElement | undefined;
16
+ setTarget: (source: HTMLElement, target: HTMLElement) => void;
17
+ deleteTarget: (source: HTMLElement) => boolean;
18
+ isProjectionMutation(record: MutationRecord): boolean;
19
+ };
20
+ export type NativeUIProjectionState = ReturnType<typeof createState>;
21
+ export declare const getProjectionState: (doc: Document) => NativeUIProjectionState;
22
+ export {};
@@ -0,0 +1,72 @@
1
+ /** Ionic's cross-surface ownership and source/proxy mapping, not a projection DOM scope. */
2
+ const states = new WeakMap();
3
+ const createState = () => {
4
+ const managers = new Set();
5
+ const nativeOwners = new Set();
6
+ const targets = new WeakMap();
7
+ const imageMarker = 'data-native-ui-snapshot';
8
+ const collect = (key) => [...new Set([...managers].flatMap((manager) => manager[key]))];
9
+ return {
10
+ imageMarker,
11
+ nativeOwns: (source) => [...nativeOwners].some((owns) => owns(source)),
12
+ registerNativeOwner(owns) {
13
+ nativeOwners.add(owns);
14
+ return () => nativeOwners.delete(owns);
15
+ },
16
+ get snapshotsEnabled() {
17
+ return managers.size > 0;
18
+ },
19
+ sourcesChangedEvent: 'native-ui:sources-change',
20
+ targetsChangedEvent: 'native-ui:targets-change',
21
+ register(manager) {
22
+ managers.add(manager);
23
+ return () => managers.delete(manager);
24
+ },
25
+ get owners() {
26
+ return collect('owners');
27
+ },
28
+ get visibleOwners() {
29
+ return collect('visibleOwners');
30
+ },
31
+ get preparedOwners() {
32
+ return collect('preparedOwners');
33
+ },
34
+ owns: (element) => collect('owners').some((owner) => owner === element || owner.contains(element)),
35
+ isVisible: (element) => collect('visibleOwners').some((owner) => owner === element || owner.contains(element)),
36
+ getTarget: (source) => targets.get(source),
37
+ setTarget: (source, target) => {
38
+ targets.set(source, target);
39
+ },
40
+ deleteTarget: (source) => targets.delete(source),
41
+ isProjectionMutation(record) {
42
+ const target = record.target instanceof Element ? record.target : record.target.parentElement;
43
+ if (target?.closest('[data-native-ui-projection-node]'))
44
+ return true;
45
+ if (target?.closest('.ion-cloned-element, [data-native-ui-proxy]'))
46
+ return true;
47
+ if (record.type === 'attributes' &&
48
+ [
49
+ imageMarker,
50
+ 'data-native-ui-proxy-prepared',
51
+ 'data-native-ui-snapshot-prepared',
52
+ 'data-native-ui-snapshot-source',
53
+ 'data-native-ui-snapshot-visible',
54
+ 'data-native-ui-snapshot-content',
55
+ 'data-native-ui-snapshot-positioned',
56
+ ].includes(record.attributeName ?? ''))
57
+ return true;
58
+ const image = (node) => node instanceof Element &&
59
+ node.matches(`style[${imageMarker}], style[data-native-ui-snapshot-base], [data-native-ui-proxy-prepared], [data-native-ui-snapshot-prepared], [data-native-ui-projection-node]`);
60
+ const nodes = [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)];
61
+ return image(record.target) || (record.type === 'childList' && nodes.length > 0 && nodes.every(image));
62
+ },
63
+ };
64
+ };
65
+ export const getProjectionState = (doc) => {
66
+ let state = states.get(doc);
67
+ if (!state) {
68
+ state = createState();
69
+ states.set(doc, state);
70
+ }
71
+ return state;
72
+ };
@@ -0,0 +1,11 @@
1
+ export declare const handoffDuration = 180;
2
+ export declare const fadeMarker = "data-native-ui-fading";
3
+ /** Only visual opacity animates; the runtime transfers input/accessibility ownership once. */
4
+ export declare const createCrossfade: (win: Window) => {
5
+ duration: (instant?: boolean) => 0 | 180;
6
+ play(element: HTMLElement, toNative: boolean, instant?: boolean): void;
7
+ retire(element: HTMLElement, instant?: boolean): void;
8
+ finish: () => void;
9
+ settled(): Promise<void>;
10
+ destroy(): void;
11
+ };
@@ -0,0 +1,71 @@
1
+ export const handoffDuration = 180;
2
+ export const fadeMarker = 'data-native-ui-fading';
3
+ /** Only visual opacity animates; the runtime transfers input/accessibility ownership once. */
4
+ export const createCrossfade = (win) => {
5
+ const active = new Map();
6
+ const retired = new Set();
7
+ const reduced = win.matchMedia('(prefers-reduced-motion: reduce)');
8
+ const finish = (element, animation) => {
9
+ if (active.get(element) !== animation)
10
+ return;
11
+ active.delete(element);
12
+ element.removeAttribute(fadeMarker);
13
+ animation.cancel();
14
+ if (retired.delete(element))
15
+ element.remove();
16
+ };
17
+ const cancelAll = () => {
18
+ for (const [element, animation] of active)
19
+ finish(element, animation);
20
+ };
21
+ reduced.addEventListener('change', cancelAll);
22
+ const api = {
23
+ duration: (instant = false) => (reduced.matches || instant ? 0 : handoffDuration),
24
+ play(element, toNative, instant = false) {
25
+ const previous = active.get(element);
26
+ // Instant handoffs must not force layout before retiring an outgoing tab.
27
+ if (reduced.matches || instant || !element.isConnected) {
28
+ if (previous)
29
+ finish(element, previous);
30
+ element.removeAttribute(fadeMarker);
31
+ return;
32
+ }
33
+ const current = Number(win.getComputedStyle(element).opacity);
34
+ previous?.cancel();
35
+ active.delete(element);
36
+ // Measure the Web appearance after lifting ownership concealment.
37
+ element.setAttribute(fadeMarker, '');
38
+ const opacity = Number(win.getComputedStyle(element).opacity);
39
+ const animation = element.animate([{ opacity: previous ? current : toNative ? opacity : 0 }, { opacity: toNative ? 0 : opacity }], {
40
+ duration: handoffDuration,
41
+ easing: 'ease-in-out',
42
+ fill: 'both',
43
+ });
44
+ active.set(element, animation);
45
+ void animation.finished.then(() => finish(element, animation), () => finish(element, animation));
46
+ },
47
+ retire(element, 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)
54
+ finish(element, previous);
55
+ element.remove();
56
+ return;
57
+ }
58
+ retired.add(element);
59
+ api.play(element, true);
60
+ },
61
+ finish: cancelAll,
62
+ async settled() {
63
+ await Promise.all(Array.from(active.values(), (animation) => animation.finished.catch(() => { })));
64
+ },
65
+ destroy() {
66
+ reduced.removeEventListener('change', cancelAll);
67
+ cancelAll();
68
+ },
69
+ };
70
+ return api;
71
+ };
@@ -0,0 +1,2 @@
1
+ /** Opt-in diagnostics. Never log props, action data, or application exception messages. */
2
+ export declare const createDiagnostics: (enabled?: boolean) => (reason: string, element?: HTMLElement) => void;
@@ -0,0 +1,16 @@
1
+ /** Opt-in diagnostics. Never log props, action data, or application exception messages. */
2
+ export const createDiagnostics = (enabled = false) => {
3
+ const elements = new WeakMap();
4
+ const global = new Set();
5
+ return (reason, element) => {
6
+ if (!enabled)
7
+ return;
8
+ const seen = element ? (elements.get(element) ?? new Set()) : global;
9
+ if (seen.has(reason))
10
+ return;
11
+ seen.add(reason);
12
+ if (element)
13
+ elements.set(element, seen);
14
+ console.info(`[Native UI] ${reason}`, ...(element ? [element] : []));
15
+ };
16
+ };