@rdlabo/capacitor-native-ui 0.0.0-stage → 2.0.0-1
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.
- package/LICENSE +21 -0
- package/Package.swift +23 -0
- package/README.md +66 -2
- package/RdlaboCapacitorNativeUi.podspec +17 -0
- package/android/build.gradle +69 -0
- package/android/proguard-rules.pro +0 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUI.kt +51 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUIPlugin.kt +192 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUISnapshot.kt +57 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +3 -0
- package/dist/native/activation.d.ts +14 -0
- package/dist/native/activation.d.ts.map +1 -0
- package/dist/native/activation.js +305 -0
- package/dist/native/activation.js.map +1 -0
- package/dist/native/components/index.d.ts +19 -0
- package/dist/native/components/index.d.ts.map +1 -0
- package/dist/native/components/index.js +38 -0
- package/dist/native/components/index.js.map +1 -0
- package/dist/native/components/ion-back-button.d.ts +10 -0
- package/dist/native/components/ion-back-button.d.ts.map +1 -0
- package/dist/native/components/ion-back-button.js +21 -0
- package/dist/native/components/ion-back-button.js.map +1 -0
- package/dist/native/components/ion-button.d.ts +10 -0
- package/dist/native/components/ion-button.d.ts.map +1 -0
- package/dist/native/components/ion-button.js +25 -0
- package/dist/native/components/ion-button.js.map +1 -0
- package/dist/native/components/ion-buttons.d.ts +11 -0
- package/dist/native/components/ion-buttons.d.ts.map +1 -0
- package/dist/native/components/ion-buttons.js +30 -0
- package/dist/native/components/ion-buttons.js.map +1 -0
- package/dist/native/components/ion-fab.d.ts +10 -0
- package/dist/native/components/ion-fab.d.ts.map +1 -0
- package/dist/native/components/ion-fab.js +132 -0
- package/dist/native/components/ion-fab.js.map +1 -0
- package/dist/native/components/ion-menu-button.d.ts +13 -0
- package/dist/native/components/ion-menu-button.d.ts.map +1 -0
- package/dist/native/components/ion-menu-button.js +44 -0
- package/dist/native/components/ion-menu-button.js.map +1 -0
- package/dist/native/components/ion-segment.d.ts +11 -0
- package/dist/native/components/ion-segment.d.ts.map +1 -0
- package/dist/native/components/ion-segment.js +30 -0
- package/dist/native/components/ion-segment.js.map +1 -0
- package/dist/native/components/ion-tab-bar.d.ts +11 -0
- package/dist/native/components/ion-tab-bar.d.ts.map +1 -0
- package/dist/native/components/ion-tab-bar.js +50 -0
- package/dist/native/components/ion-tab-bar.js.map +1 -0
- package/dist/native/components/searchable-tabs.d.ts +20 -0
- package/dist/native/components/searchable-tabs.d.ts.map +1 -0
- package/dist/native/components/searchable-tabs.js +413 -0
- package/dist/native/components/searchable-tabs.js.map +1 -0
- package/dist/native/definitions.d.ts +237 -0
- package/dist/native/definitions.d.ts.map +1 -0
- package/dist/native/definitions.js +0 -0
- package/dist/native/extensions.d.ts +31 -0
- package/dist/native/extensions.d.ts.map +1 -0
- package/dist/native/extensions.js +122 -0
- package/dist/native/extensions.js.map +1 -0
- package/dist/native/foldable/fold-state.d.ts +5 -0
- package/dist/native/foldable/fold-state.d.ts.map +1 -0
- package/dist/native/foldable/fold-state.js +15 -0
- package/dist/native/foldable/fold-state.js.map +1 -0
- package/dist/native/foldable/types.d.ts +12 -0
- package/dist/native/foldable/types.d.ts.map +1 -0
- package/dist/native/foldable/types.js +0 -0
- package/dist/native/html.d.ts +9 -0
- package/dist/native/html.d.ts.map +1 -0
- package/dist/native/html.js +197 -0
- package/dist/native/html.js.map +1 -0
- package/dist/native/index.d.ts +10 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +19 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/legacy-layout.d.ts +5 -0
- package/dist/native/legacy-layout.d.ts.map +1 -0
- package/dist/native/legacy-layout.js +27 -0
- package/dist/native/legacy-layout.js.map +1 -0
- package/dist/native/overlays/content.d.ts +4 -0
- package/dist/native/overlays/content.d.ts.map +1 -0
- package/dist/native/overlays/content.js +51 -0
- package/dist/native/overlays/content.js.map +1 -0
- package/dist/native/overlays/controller.d.ts +8 -0
- package/dist/native/overlays/controller.d.ts.map +1 -0
- package/dist/native/overlays/controller.js +285 -0
- package/dist/native/overlays/controller.js.map +1 -0
- package/dist/native/overlays/dialog-animation.d.ts +9 -0
- package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
- package/dist/native/overlays/dialog-animation.js +53 -0
- package/dist/native/overlays/dialog-animation.js.map +1 -0
- package/dist/native/overlays/dismiss-animation.d.ts +4 -0
- package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
- package/dist/native/overlays/dismiss-animation.js +18 -0
- package/dist/native/overlays/dismiss-animation.js.map +1 -0
- package/dist/native/overlays/focus.d.ts +5 -0
- package/dist/native/overlays/focus.d.ts.map +1 -0
- package/dist/native/overlays/focus.js +59 -0
- package/dist/native/overlays/focus.js.map +1 -0
- package/dist/native/overlays/popover.d.ts +12 -0
- package/dist/native/overlays/popover.d.ts.map +1 -0
- package/dist/native/overlays/popover.js +110 -0
- package/dist/native/overlays/popover.js.map +1 -0
- package/dist/native/overlays/styles.d.ts +11 -0
- package/dist/native/overlays/styles.d.ts.map +1 -0
- package/dist/native/overlays/styles.js +167 -0
- package/dist/native/overlays/styles.js.map +1 -0
- package/dist/native/prehide.d.ts +8 -0
- package/dist/native/prehide.d.ts.map +1 -0
- package/dist/native/prehide.js +238 -0
- package/dist/native/prehide.js.map +1 -0
- package/dist/native/runtime.d.ts +7 -0
- package/dist/native/runtime.d.ts.map +1 -0
- package/dist/native/runtime.js +776 -0
- package/dist/native/runtime.js.map +1 -0
- package/dist/native/shared/bridge.d.ts +6 -0
- package/dist/native/shared/bridge.d.ts.map +1 -0
- package/dist/native/shared/bridge.js +18 -0
- package/dist/native/shared/bridge.js.map +1 -0
- package/dist/native/shared/candidate.d.ts +25 -0
- package/dist/native/shared/candidate.d.ts.map +1 -0
- package/dist/native/shared/candidate.js +106 -0
- package/dist/native/shared/candidate.js.map +1 -0
- package/dist/native/shared/clipping.d.ts +5 -0
- package/dist/native/shared/clipping.d.ts.map +1 -0
- package/dist/native/shared/clipping.js +26 -0
- package/dist/native/shared/clipping.js.map +1 -0
- package/dist/native/shared/control-options.d.ts +6 -0
- package/dist/native/shared/control-options.d.ts.map +1 -0
- package/dist/native/shared/control-options.js +17 -0
- package/dist/native/shared/control-options.js.map +1 -0
- package/dist/native/shared/crossfade.d.ts +12 -0
- package/dist/native/shared/crossfade.d.ts.map +1 -0
- package/dist/native/shared/crossfade.js +67 -0
- package/dist/native/shared/crossfade.js.map +1 -0
- package/dist/native/shared/diagnostics.d.ts +4 -0
- package/dist/native/shared/diagnostics.d.ts.map +1 -0
- package/dist/native/shared/diagnostics.js +18 -0
- package/dist/native/shared/diagnostics.js.map +1 -0
- package/dist/native/shared/dom.d.ts +46 -0
- package/dist/native/shared/dom.d.ts.map +1 -0
- package/dist/native/shared/dom.js +180 -0
- package/dist/native/shared/dom.js.map +1 -0
- package/dist/native/shared/events.d.ts +15 -0
- package/dist/native/shared/events.d.ts.map +1 -0
- package/dist/native/shared/events.js +17 -0
- package/dist/native/shared/events.js.map +1 -0
- package/dist/native/shared/icons.d.ts +8 -0
- package/dist/native/shared/icons.d.ts.map +1 -0
- package/dist/native/shared/icons.js +61 -0
- package/dist/native/shared/icons.js.map +1 -0
- package/dist/native/shared/modal.d.ts +10 -0
- package/dist/native/shared/modal.d.ts.map +1 -0
- package/dist/native/shared/modal.js +160 -0
- package/dist/native/shared/modal.js.map +1 -0
- package/dist/native/shared/projection-id.d.ts +4 -0
- package/dist/native/shared/projection-id.d.ts.map +1 -0
- package/dist/native/shared/projection-id.js +7 -0
- package/dist/native/shared/projection-id.js.map +1 -0
- package/dist/native/shared/toolbar-options.d.ts +13 -0
- package/dist/native/shared/toolbar-options.d.ts.map +1 -0
- package/dist/native/shared/toolbar-options.js +28 -0
- package/dist/native/shared/toolbar-options.js.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.js +85 -0
- package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
- package/dist/native/startup.d.ts +8 -0
- package/dist/native/startup.d.ts.map +1 -0
- package/dist/native/startup.js +70 -0
- package/dist/native/startup.js.map +1 -0
- package/dist/native/vertical-bars-web-search.d.ts +10 -0
- package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
- package/dist/native/vertical-bars-web-search.js +90 -0
- package/dist/native/vertical-bars-web-search.js.map +1 -0
- package/dist/native/vertical-bars-web.d.ts +5 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -0
- package/dist/native/vertical-bars-web.js +422 -0
- package/dist/native/vertical-bars-web.js.map +1 -0
- package/dist/native-integration/index.d.ts +25 -0
- package/dist/native-integration/index.d.ts.map +1 -0
- package/dist/native-integration/index.js +33 -0
- package/dist/native-integration/index.js.map +1 -0
- package/dist/vertical-bars.d.ts +6 -0
- package/dist/vertical-bars.js +5 -0
- package/docs/appearance.md +45 -0
- package/docs/configuration.md +219 -0
- package/docs/framework-independent.md +107 -0
- package/docs/ionic-vertical-bars.md +173 -0
- package/docs/migration.md +184 -0
- package/docs/native-ui-extensions.md +394 -0
- package/docs/native-ui.md +528 -0
- package/docs/setup.md +224 -0
- package/docs/vertical-bars.md +431 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
- package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
- package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
- package/package.json +89 -4
- package/src/core.ts +14 -0
- package/src/native/activation.ts +385 -0
- package/src/native/components/index.ts +45 -0
- package/src/native/components/ion-back-button.ts +22 -0
- package/src/native/components/ion-button.ts +25 -0
- package/src/native/components/ion-buttons.ts +37 -0
- package/src/native/components/ion-fab.ts +151 -0
- package/src/native/components/ion-menu-button.ts +37 -0
- package/src/native/components/ion-segment.ts +27 -0
- package/src/native/components/ion-tab-bar.ts +50 -0
- package/src/native/components/searchable-tabs.ts +499 -0
- package/src/native/definitions.ts +274 -0
- package/src/native/extensions.ts +176 -0
- package/src/native/foldable/fold-state.ts +10 -0
- package/src/native/foldable/types.ts +4 -0
- package/src/native/html.ts +199 -0
- package/src/native/index.ts +32 -0
- package/src/native/legacy-layout.ts +21 -0
- package/src/native/overlays/content.ts +51 -0
- package/src/native/overlays/controller.ts +349 -0
- package/src/native/overlays/dialog-animation.ts +53 -0
- package/src/native/overlays/dismiss-animation.ts +15 -0
- package/src/native/overlays/focus.ts +64 -0
- package/src/native/overlays/popover.ts +120 -0
- package/src/native/overlays/styles.ts +181 -0
- package/src/native/prehide.ts +285 -0
- package/src/native/runtime.ts +979 -0
- package/src/native/shared/bridge.ts +18 -0
- package/src/native/shared/candidate.ts +162 -0
- package/src/native/shared/clipping.ts +24 -0
- package/src/native/shared/control-options.ts +20 -0
- package/src/native/shared/crossfade.ts +67 -0
- package/src/native/shared/diagnostics.ts +13 -0
- package/src/native/shared/dom.ts +304 -0
- package/src/native/shared/events.ts +14 -0
- package/src/native/shared/icons.ts +57 -0
- package/src/native/shared/modal.ts +157 -0
- package/src/native/shared/projection-id.ts +2 -0
- package/src/native/shared/toolbar-options.ts +49 -0
- package/src/native/shared/vertical-bars-geometry.ts +80 -0
- package/src/native/startup.ts +68 -0
- package/src/native/vertical-bars-web-search.ts +86 -0
- package/src/native/vertical-bars-web.ts +500 -0
- package/src/native-integration/index.ts +49 -0
- package/src/vertical-bars.ts +12 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { NativeUIExtension } from './extensions';
|
|
2
|
+
import type { PluginListenerHandle } from '@capacitor/core';
|
|
3
|
+
|
|
4
|
+
export type NativeUIComponent =
|
|
5
|
+
| 'ion-button'
|
|
6
|
+
| 'ion-buttons'
|
|
7
|
+
| 'ion-back-button'
|
|
8
|
+
| 'ion-menu-button'
|
|
9
|
+
| 'ion-tab-bar'
|
|
10
|
+
| 'ion-segment'
|
|
11
|
+
| 'ion-fab';
|
|
12
|
+
|
|
13
|
+
export interface NativeUIStatus {
|
|
14
|
+
state: 'web' | 'native' | 'stopped';
|
|
15
|
+
projected: number;
|
|
16
|
+
updates: number;
|
|
17
|
+
reason?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Default selection in ordinary (horizontal) or rail (vertical) placement.
|
|
21
|
+
* Explicit source settings override these defaults. This does not choose placement.
|
|
22
|
+
*/
|
|
23
|
+
export type ControlProjection = boolean | 'horizontalOnly' | 'verticalOnly';
|
|
24
|
+
|
|
25
|
+
export interface NativeUIControls {
|
|
26
|
+
/** Group selected ordinary buttons. Does not select children or include back buttons. */
|
|
27
|
+
buttonGroup?: ControlProjection;
|
|
28
|
+
/** Select ordinary actions, including menu buttons. */
|
|
29
|
+
button?: ControlProjection;
|
|
30
|
+
backButton?: ControlProjection;
|
|
31
|
+
tabs?: ControlProjection;
|
|
32
|
+
segment?: ControlProjection;
|
|
33
|
+
fab?: ControlProjection;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IonicProjectionOptions {
|
|
37
|
+
/** Discover supported Ionic controls automatically. Explicit sources remain independent. Defaults to true. */
|
|
38
|
+
auto?: boolean;
|
|
39
|
+
verticalBars?: {
|
|
40
|
+
/** Native appearance of vertical Ionic buttons. Local source settings take precedence. Defaults to system. */
|
|
41
|
+
buttonProjection?: 'system' | 'source';
|
|
42
|
+
/** Source-style standalone ion-button default; ion-buttons still defaults to clear. Defaults to glass. */
|
|
43
|
+
buttonDefaultFill?: 'glass' | 'solid';
|
|
44
|
+
/** Relay Ionic popovers anchored to native rail buttons. Defaults to false. */
|
|
45
|
+
popover?: boolean;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface NativeUIOptions {
|
|
50
|
+
/** Connected source subtree. Defaults to document.body. */
|
|
51
|
+
root?: HTMLElement;
|
|
52
|
+
/** Default selection and grouping. Scalars apply to every kind; omitted fields default to true. */
|
|
53
|
+
controls?: ControlProjection | NativeUIControls;
|
|
54
|
+
verticalBars?: NativeUIVerticalBarsOptions;
|
|
55
|
+
/** App-local projection definitions, independent of controls.
|
|
56
|
+
* @experimental API and behavior may change.
|
|
57
|
+
*/
|
|
58
|
+
extensions?: readonly NativeUIExtension[];
|
|
59
|
+
ionic?: IonicProjectionOptions;
|
|
60
|
+
/** Log Web fallback reasons. Defaults to false. */
|
|
61
|
+
debug?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Options for the standalone rail entry. */
|
|
65
|
+
export type VerticalControlAreaOptions = NativeUIOptions;
|
|
66
|
+
|
|
67
|
+
/** @internal Resolved adapter state; not an application configuration API. */
|
|
68
|
+
export interface NativeUIRuntimeOptions extends NativeUIOptions {
|
|
69
|
+
source?: 'ionic' | 'html' | 'auto' | 'extensions';
|
|
70
|
+
nativeState?: { edge?: 'left' | 'right' };
|
|
71
|
+
unavailableExtensions?: ReadonlyMap<string, string>;
|
|
72
|
+
rail?: { host: HTMLElement; edge: 'left' | 'right' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A running shell. Placement changes preserve all other startup settings. */
|
|
76
|
+
export interface NativeUIHandle extends NativeUIRuntimeHandle {
|
|
77
|
+
/** Enable rail projection when available, preserving other settings. Rejects after destroy. */
|
|
78
|
+
enableVerticalControlArea(): Promise<void>;
|
|
79
|
+
/** Restore rail sources to the Web without stopping ordinary native controls. Rejects after destroy. */
|
|
80
|
+
disableVerticalControlArea(): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface NativeUIVerticalBarsOptions {
|
|
84
|
+
/** Defaults to true. Availability is always determined separately. */
|
|
85
|
+
enabled?: boolean;
|
|
86
|
+
/** Placement preference, not a declaration of device capability. */
|
|
87
|
+
placement?: { edge: Exclude<VerticalBarEdge, null>; webHost?: HTMLElement };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface NativeUIRuntimeHandle {
|
|
91
|
+
/** Schedule a source recheck. Calls coalesce; unchanged snapshots are not sent. No-op after destroy or without a runtime. */
|
|
92
|
+
markForCheck(): void;
|
|
93
|
+
/** Returns the current Web/native projection state. */
|
|
94
|
+
getStatus(): NativeUIStatus;
|
|
95
|
+
/** Restores projected controls to the Web until the returned lease is resumed. */
|
|
96
|
+
suspend(): Promise<NativeUISuspension>;
|
|
97
|
+
/** Stops synchronization, restores Web controls and releases native resources. */
|
|
98
|
+
destroy(): Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Logical edge in the reading direction, matching UIVerticalBarEdge and capacitor-foldable. */
|
|
102
|
+
export type VerticalBarEdge = 'leading' | 'trailing' | null;
|
|
103
|
+
|
|
104
|
+
export interface VerticalBarPlacement {
|
|
105
|
+
edge: VerticalBarEdge;
|
|
106
|
+
/** Explicit rail width in CSS pixels; omitted to use the stylesheet's safe-area rules. */
|
|
107
|
+
inset?: number;
|
|
108
|
+
/** Ignored. Native availability is read from the plugin’s own device metrics.
|
|
109
|
+
* @deprecated This input no longer reports native availability.
|
|
110
|
+
*/
|
|
111
|
+
nativeEdge?: VerticalBarEdge;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface VerticalControlAreaHandle extends NativeUIHandle {
|
|
115
|
+
/** Legacy Web CSS adapter. Use the theme setVerticalBarsLayout helper instead.
|
|
116
|
+
* @deprecated Web layout belongs to the theme or application.
|
|
117
|
+
*/
|
|
118
|
+
setPlacement(placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean): void;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface NativeUISuspension {
|
|
122
|
+
/** Releases this suspension. Native projection resumes after all active suspensions are released. */
|
|
123
|
+
resume(): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface Frame {
|
|
127
|
+
x: number;
|
|
128
|
+
y: number;
|
|
129
|
+
width: number;
|
|
130
|
+
height: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface NativeUIBadge {
|
|
134
|
+
value: string;
|
|
135
|
+
color: string;
|
|
136
|
+
textColor: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface NativeUIItem extends Frame {
|
|
140
|
+
id: string;
|
|
141
|
+
label: string;
|
|
142
|
+
accessibilityLabel: string;
|
|
143
|
+
disabled: boolean;
|
|
144
|
+
selected: boolean;
|
|
145
|
+
fontSize: number;
|
|
146
|
+
fontWeight: number;
|
|
147
|
+
color: string;
|
|
148
|
+
/** Resolved ion-button fill for vertical projection; clear omits the native glass background. */
|
|
149
|
+
buttonFill?: 'clear' | 'solid' | 'outline';
|
|
150
|
+
backgroundColor?: string;
|
|
151
|
+
borderColor?: string;
|
|
152
|
+
borderWidth?: number;
|
|
153
|
+
badge?: NativeUIBadge;
|
|
154
|
+
icon?: string;
|
|
155
|
+
iconWidth?: number;
|
|
156
|
+
iconHeight?: number;
|
|
157
|
+
iconPosition?: 'leading' | 'trailing' | 'top';
|
|
158
|
+
imagePadding?: number;
|
|
159
|
+
contentInsetLeading?: number;
|
|
160
|
+
contentInsetTrailing?: number;
|
|
161
|
+
iconTemplate?: boolean;
|
|
162
|
+
visible?: boolean;
|
|
163
|
+
closeIcon?: string;
|
|
164
|
+
closeIconWidth?: number;
|
|
165
|
+
closeIconHeight?: number;
|
|
166
|
+
iconTransition?: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Standard native renderers, independent of the source Web framework. */
|
|
170
|
+
export type NativeUIControlKind =
|
|
171
|
+
| 'button'
|
|
172
|
+
| 'button-group'
|
|
173
|
+
| 'back-button'
|
|
174
|
+
| 'menu-button'
|
|
175
|
+
| 'tab-bar'
|
|
176
|
+
| 'segment'
|
|
177
|
+
| 'floating-action-button';
|
|
178
|
+
|
|
179
|
+
export interface NativeUIControl extends Frame {
|
|
180
|
+
id: string;
|
|
181
|
+
kind: NativeUIControlKind | 'extension';
|
|
182
|
+
extension?: { name: string; props: string; actions: Record<string, string>; tabBarId?: string };
|
|
183
|
+
/** Lets the native host own adaptive placement instead of mirroring the DOM frame. */
|
|
184
|
+
placement?: 'vertical-bars';
|
|
185
|
+
/** Logical Ionic toolbar slot, preserved when projecting to the vertical rail. */
|
|
186
|
+
toolbarSlot?: 'start' | 'end';
|
|
187
|
+
items: NativeUIItem[];
|
|
188
|
+
dark: boolean;
|
|
189
|
+
rtl: boolean;
|
|
190
|
+
tabBarAnchor?: { x: 0 | 0.5 | 1; y: 0 | 1 };
|
|
191
|
+
search?: NativeUISearch;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface NativeUISearch {
|
|
195
|
+
id: string;
|
|
196
|
+
field: NativeUIItem;
|
|
197
|
+
trigger: NativeUIItem;
|
|
198
|
+
closeId: string;
|
|
199
|
+
active: boolean;
|
|
200
|
+
available: boolean;
|
|
201
|
+
focused: boolean;
|
|
202
|
+
value: string;
|
|
203
|
+
placeholder: string;
|
|
204
|
+
disabled: boolean;
|
|
205
|
+
editSequence: number;
|
|
206
|
+
valueVersion: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface NativeUISearchEvent extends NativeUIActivation {
|
|
210
|
+
valueVersion: number;
|
|
211
|
+
phase: 'input' | 'focus' | 'blur' | 'clear' | 'commit';
|
|
212
|
+
value: string;
|
|
213
|
+
composing: boolean;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface NativeUISnapshot {
|
|
217
|
+
revision: number;
|
|
218
|
+
transitionDuration?: number;
|
|
219
|
+
viewportWidth: number;
|
|
220
|
+
/** Physical side the native rail is drawn on; the runtime resolves the logical edge through the document direction. */
|
|
221
|
+
verticalBarEdge?: 'left' | 'right';
|
|
222
|
+
/** Visible foreground modal bounds; omitted for the full page. */
|
|
223
|
+
verticalBarFrame?: Frame;
|
|
224
|
+
controls: NativeUIControl[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface NativeUIActivation {
|
|
228
|
+
revision: number;
|
|
229
|
+
id: string;
|
|
230
|
+
sequence: number;
|
|
231
|
+
/** Native extension action awaiting a Web dispatch acknowledgement. */
|
|
232
|
+
requestId?: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface WebViewMetrics {
|
|
236
|
+
/** The WebView's effective top-left corner radius in points, or `0` when unavailable. */
|
|
237
|
+
radius: number;
|
|
238
|
+
/** Device-reported logical rail edge; null when unavailable. */
|
|
239
|
+
verticalBarEdge?: VerticalBarEdge;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface NativeUIPlugin {
|
|
243
|
+
configure(options?: { suppressScrollEdgeEffect?: boolean }): Promise<{ supported: boolean; extensions?: string[] }>;
|
|
244
|
+
completeExtensionAction(options: { requestId: string; accepted: boolean }): Promise<void>;
|
|
245
|
+
getWebViewMetrics(): Promise<WebViewMetrics>;
|
|
246
|
+
getVerticalBarsGeometry(options: { viewportWidth: number; ids: string[] }): Promise<{ revision?: number; items: Record<string, Frame> }>;
|
|
247
|
+
prepareOverlay(options: { id: string }): Promise<void>;
|
|
248
|
+
presentOverlay(options: { id: string; presentation?: NativeUIPopoverPresentation }): Promise<void>;
|
|
249
|
+
dismissOverlay(options: { id: string; animated: boolean }): Promise<void>;
|
|
250
|
+
closeOverlay(options: { id: string }): Promise<void>;
|
|
251
|
+
stopOverlays(): Promise<void>;
|
|
252
|
+
update(snapshot: NativeUISnapshot): Promise<{ revision: number; rejectedSearches?: string[]; rejectedControls?: string[] }>;
|
|
253
|
+
clear(options: { revision: number }): Promise<void>;
|
|
254
|
+
addListener(name: 'activate', listener: (event: NativeUIActivation) => void): Promise<PluginListenerHandle>;
|
|
255
|
+
addListener(name: 'search', listener: (event: NativeUISearchEvent) => void): Promise<PluginListenerHandle>;
|
|
256
|
+
addListener(name: 'webViewMetricsChange', listener: (event: WebViewMetrics) => void): Promise<PluginListenerHandle>;
|
|
257
|
+
addListener(name: 'overlay', listener: (event: NativeUIOverlayEvent) => void): Promise<PluginListenerHandle>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface NativeUIPopoverPresentation {
|
|
261
|
+
kind: 'popover';
|
|
262
|
+
animated: boolean;
|
|
263
|
+
dark: boolean;
|
|
264
|
+
anchorId: string;
|
|
265
|
+
width: number;
|
|
266
|
+
height: number;
|
|
267
|
+
backgroundColor: string;
|
|
268
|
+
backdropDismiss: boolean;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface NativeUIOverlayEvent {
|
|
272
|
+
id: string;
|
|
273
|
+
action: 'dismiss';
|
|
274
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { fitsClip } from './shared/clipping';
|
|
2
|
+
export { projectionAncestors as extensionAncestors } from './shared/clipping';
|
|
3
|
+
import type { Candidate, Identify } from './shared/candidate';
|
|
4
|
+
import { frame, isDark, visible } from './shared/dom';
|
|
5
|
+
import { iconSource, type createIconRenderer } from './shared/icons';
|
|
6
|
+
|
|
7
|
+
export type NativeUIValue = string | number | boolean | null | NativeUIValue[] | { [key: string]: NativeUIValue };
|
|
8
|
+
|
|
9
|
+
/** Opaque image reference. Return it in props; the shell replaces it with NativeUIImage data before transmission. */
|
|
10
|
+
export type NativeUIImageReference = { readonly $nativeUIImage: string };
|
|
11
|
+
type NativeUIImage = { data: string; width: number; height: number; template: boolean };
|
|
12
|
+
|
|
13
|
+
export interface NativeUIReadContext {
|
|
14
|
+
/** Capture a local SVG or resolved ion-icon using the standard renderer. Unsupported artwork returns null. */
|
|
15
|
+
image(element: SVGElement | HTMLElement | null, options?: { template?: boolean }): NativeUIImageReference | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** An app-local DOM projection. Swift or Kotlin registers its native content under the same name.
|
|
19
|
+
* @experimental API and behavior may change in future releases.
|
|
20
|
+
*/
|
|
21
|
+
export interface NativeUIExtension {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly selector: string;
|
|
24
|
+
/** Defaults to the source element's frame. Unsupported hosts keep the Web source visible. */
|
|
25
|
+
readonly placement?: 'element' | 'tab-accessory';
|
|
26
|
+
/** Return null to keep this instance on the Web. Read current source state without side effects. Notify property-only changes with shell.markForCheck(). */
|
|
27
|
+
readonly read: (
|
|
28
|
+
element: HTMLElement,
|
|
29
|
+
context: NativeUIReadContext,
|
|
30
|
+
) => {
|
|
31
|
+
props: { [key: string]: NativeUIValue };
|
|
32
|
+
/** Click targets must belong to the source element. Null targets are unavailable. */
|
|
33
|
+
actions?: Record<string, HTMLElement | null>;
|
|
34
|
+
} | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Defines a projection locally; it does not start native work or register a Capacitor plugin.
|
|
38
|
+
* @experimental API and behavior may change in future releases.
|
|
39
|
+
*/
|
|
40
|
+
export const defineNativeUI = <T extends NativeUIExtension>(extension: T): Readonly<T> => Object.freeze({ ...extension });
|
|
41
|
+
|
|
42
|
+
export const validateExtensions = (extensions: readonly NativeUIExtension[], doc: Document): void => {
|
|
43
|
+
const names = new Set<string>();
|
|
44
|
+
for (const extension of extensions) {
|
|
45
|
+
if (!extension.name.trim() || names.has(extension.name)) throw new Error('Native UI extension names must be nonempty and unique');
|
|
46
|
+
names.add(extension.name);
|
|
47
|
+
if (!extension.selector.trim()) throw new Error('Native UI extensions require a selector');
|
|
48
|
+
doc.querySelector(extension.selector);
|
|
49
|
+
if (extension.placement && !['element', 'tab-accessory'].includes(extension.placement)) throw new Error('Invalid native UI placement');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const json = (value: unknown, images: Map<string, NativeUIImage>): string => {
|
|
54
|
+
if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype)
|
|
55
|
+
throw new Error('Native UI props must be a JSON object');
|
|
56
|
+
const result = JSON.stringify(value, (_key, item: unknown) => {
|
|
57
|
+
if (
|
|
58
|
+
item === undefined ||
|
|
59
|
+
typeof item === 'function' ||
|
|
60
|
+
typeof item === 'symbol' ||
|
|
61
|
+
(typeof item === 'number' && !Number.isFinite(item)) ||
|
|
62
|
+
(item !== null && typeof item === 'object' && !Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype)
|
|
63
|
+
)
|
|
64
|
+
throw new Error('Native UI props must contain only JSON values');
|
|
65
|
+
if (item && typeof item === 'object' && '$nativeUIImage' in item) {
|
|
66
|
+
const image = images.get(item.$nativeUIImage as string);
|
|
67
|
+
if (!image || Object.keys(item).length !== 1) throw new Error('Invalid native image reference');
|
|
68
|
+
return image;
|
|
69
|
+
}
|
|
70
|
+
return item;
|
|
71
|
+
});
|
|
72
|
+
if (!result || result.length > 65536) throw new Error('Native UI props exceed 64K characters');
|
|
73
|
+
return result;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Follow slot assignment too: Ionic scroll containers live inside shadow roots. */
|
|
77
|
+
|
|
78
|
+
export const readExtension = (
|
|
79
|
+
element: HTMLElement,
|
|
80
|
+
extensions: readonly NativeUIExtension[],
|
|
81
|
+
id: Identify,
|
|
82
|
+
diagnose: (reason: string) => void = () => {},
|
|
83
|
+
): Candidate | undefined => {
|
|
84
|
+
const skip = (reason: string): undefined => {
|
|
85
|
+
diagnose(reason);
|
|
86
|
+
return undefined;
|
|
87
|
+
};
|
|
88
|
+
const matches = extensions.filter((extension) => element.matches(extension.selector));
|
|
89
|
+
if (matches.length !== 1) return skip('overlapping-selectors: exactly one extension must own this source.');
|
|
90
|
+
if (!visible(element)) return skip('source-hidden: the source must be visible and inside the viewport.');
|
|
91
|
+
if (element.closest('ion-modal, ion-popover, ion-menu'))
|
|
92
|
+
return skip('unsupported-overlay: extension sources inside Web overlays stay on the Web.');
|
|
93
|
+
if (element.contains(element.ownerDocument.activeElement)) return skip('source-focused: keep the focused Web source until focus leaves.');
|
|
94
|
+
const extension = matches[0];
|
|
95
|
+
if (extension.placement !== 'tab-accessory' && !fitsClip(element))
|
|
96
|
+
return skip('source-clipped: the source must fit its clipping ancestors without transformed ancestors.');
|
|
97
|
+
try {
|
|
98
|
+
const images: { value: NativeUIImage; source: string }[] = [];
|
|
99
|
+
const imageValues = new Map<string, NativeUIImage>();
|
|
100
|
+
let state: ReturnType<NativeUIExtension['read']>;
|
|
101
|
+
try {
|
|
102
|
+
state = extension.read(element, {
|
|
103
|
+
image(owner, options) {
|
|
104
|
+
if (!owner || !(element.contains(owner) || element.shadowRoot?.contains(owner))) return null;
|
|
105
|
+
const svg = owner.matches('svg') ? (owner as SVGElement) : owner.shadowRoot?.querySelector<SVGElement>('svg');
|
|
106
|
+
if (!svg) return null;
|
|
107
|
+
const source = iconSource(svg);
|
|
108
|
+
const { width, height } = owner.getBoundingClientRect();
|
|
109
|
+
if (!source || !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
|
|
110
|
+
const value: NativeUIImage = { data: '', width, height, template: options?.template ?? false };
|
|
111
|
+
const key = String(images.length);
|
|
112
|
+
images.push({ value, source });
|
|
113
|
+
imageValues.set(key, value);
|
|
114
|
+
return { $nativeUIImage: key };
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
return skip('reader-error: the extension reader threw; inspect its implementation.');
|
|
119
|
+
}
|
|
120
|
+
if (!state) return skip('reader-opt-out: the reader returned null.');
|
|
121
|
+
const props = json(state.props, imageValues);
|
|
122
|
+
const actions = new Map<string, HTMLElement>();
|
|
123
|
+
const targets: Record<string, string> = {};
|
|
124
|
+
for (const [name, target] of Object.entries(state.actions ?? {})) {
|
|
125
|
+
if (!name || (target && !element.contains(target))) return skip('invalid-actions: use named targets inside the source element.');
|
|
126
|
+
if (
|
|
127
|
+
!target ||
|
|
128
|
+
!visible(target) ||
|
|
129
|
+
(target as HTMLElement & { disabled?: boolean }).disabled ||
|
|
130
|
+
target.matches(':disabled, [disabled]') ||
|
|
131
|
+
target.closest('[aria-disabled="true"]') ||
|
|
132
|
+
getComputedStyle(target).pointerEvents === 'none'
|
|
133
|
+
)
|
|
134
|
+
continue;
|
|
135
|
+
targets[name] = id(target);
|
|
136
|
+
actions.set(id(target), target);
|
|
137
|
+
}
|
|
138
|
+
if (Object.keys(targets).length > 30) return skip('invalid-actions: a source can expose at most 30 actions.');
|
|
139
|
+
const tabBar =
|
|
140
|
+
extension.placement === 'tab-accessory'
|
|
141
|
+
? Array.from(element.closest('ion-tabs')?.querySelectorAll<HTMLElement>('ion-tab-bar') ?? []).find(
|
|
142
|
+
(bar) => bar.closest('ion-tabs') === element.closest('ion-tabs'),
|
|
143
|
+
)
|
|
144
|
+
: undefined;
|
|
145
|
+
if (extension.placement === 'tab-accessory' && !tabBar)
|
|
146
|
+
return skip('missing-tab-owner: a tab accessory needs an owning ion-tabs with ion-tab-bar.');
|
|
147
|
+
const style = getComputedStyle(element);
|
|
148
|
+
const control = {
|
|
149
|
+
id: id(element),
|
|
150
|
+
kind: 'extension' as const,
|
|
151
|
+
...frame(element.getBoundingClientRect()),
|
|
152
|
+
items: [],
|
|
153
|
+
dark: isDark(style),
|
|
154
|
+
rtl: style.direction === 'rtl',
|
|
155
|
+
extension: { name: extension.name, props, actions: targets, tabBarId: tabBar ? id(tabBar) : undefined },
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
element,
|
|
159
|
+
actions,
|
|
160
|
+
icons: [],
|
|
161
|
+
control,
|
|
162
|
+
assets: images.length
|
|
163
|
+
? {
|
|
164
|
+
signature: JSON.stringify(images),
|
|
165
|
+
async render(renderer: ReturnType<typeof createIconRenderer>) {
|
|
166
|
+
for (const { value, source } of images) value.data = await renderer.render(source, value.width, value.height);
|
|
167
|
+
control.extension.props = json(state.props, imageValues);
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
: undefined,
|
|
171
|
+
};
|
|
172
|
+
} catch {
|
|
173
|
+
// Keep the source usable without exposing props or application exception data.
|
|
174
|
+
return skip('invalid-props: return a JSON object with finite values, valid image references, and at most 64K characters.');
|
|
175
|
+
}
|
|
176
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FoldState } from './types';
|
|
2
|
+
|
|
3
|
+
/** @deprecated Import applyFoldStateClasses from @rdlabo/ionic-theme-ios27 instead.
|
|
4
|
+
* Keep posture on ion-app so descendants can follow it through CSS. */
|
|
5
|
+
export function applyFoldStateClasses(root: HTMLElement, fold: FoldState): void {
|
|
6
|
+
for (const state of ['flat', 'half-opened', 'closed']) {
|
|
7
|
+
root.classList.toggle(`ios-theme-fold-${state}`, fold.state === state);
|
|
8
|
+
}
|
|
9
|
+
root.classList.toggle('ios-theme-fold-expanded', fold.state === 'half-opened' || (fold.state === 'flat' && !!fold.hingeBounds));
|
|
10
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { fitsClip } from './shared/clipping';
|
|
2
|
+
import type { NativeUIRuntimeOptions, NativeUIRuntimeHandle } from './definitions';
|
|
3
|
+
import { appendItem, createCandidate, type Candidate, type Identify } from './shared/candidate';
|
|
4
|
+
import { marker, unprojected, visible } from './shared/dom';
|
|
5
|
+
|
|
6
|
+
/** Standard HTML sources use standard native renderers, not app-local Swift extensions. */
|
|
7
|
+
export const htmlSelector = '[data-native-ui="button"], [data-native-ui="back-button"]';
|
|
8
|
+
export const htmlRailSource = (element: Element) =>
|
|
9
|
+
element.matches(htmlSelector) && element.getAttribute('data-native-ui-placement') === 'rail';
|
|
10
|
+
|
|
11
|
+
export const readHTMLControl = (element: HTMLElement, id: Identify, options: NativeUIRuntimeOptions): Candidate | undefined => {
|
|
12
|
+
if (
|
|
13
|
+
!(options.root ?? element.ownerDocument.body).contains(element) ||
|
|
14
|
+
!element.matches(htmlSelector) ||
|
|
15
|
+
element.closest('[data-native-ui-proxy]')
|
|
16
|
+
)
|
|
17
|
+
return;
|
|
18
|
+
// Explicit opt-in; custom elements can implement click() and expose role=button.
|
|
19
|
+
if (!element.matches('button, ion-button, [role="button"]') || !visible(element, htmlRailSource(element))) return;
|
|
20
|
+
if (element.contains(element.ownerDocument.activeElement)) return;
|
|
21
|
+
if (!htmlRailSource(element) && !fitsClip(element)) return;
|
|
22
|
+
if (htmlRailSource(element) && options.verticalBars?.enabled === false) return;
|
|
23
|
+
const kind = element.getAttribute('data-native-ui') === 'back-button' ? 'back-button' : 'button';
|
|
24
|
+
const candidate = createCandidate(element, kind, id, options);
|
|
25
|
+
const item = appendItem(candidate, element, id);
|
|
26
|
+
if (!item) return;
|
|
27
|
+
const labelledBy = element
|
|
28
|
+
.getAttribute('aria-labelledby')
|
|
29
|
+
?.trim()
|
|
30
|
+
.split(/\s+/)
|
|
31
|
+
.map((key) => element.ownerDocument.getElementById(key)?.textContent?.trim() ?? '')
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(' ');
|
|
34
|
+
item.accessibilityLabel = labelledBy || element.getAttribute('aria-label') || item.label;
|
|
35
|
+
if (!item.accessibilityLabel) return;
|
|
36
|
+
item.disabled ||= element.matches(':disabled, [disabled]') || !!element.closest('[aria-disabled="true"]');
|
|
37
|
+
if (htmlRailSource(element)) candidate.control.placement = 'vertical-bars';
|
|
38
|
+
return candidate;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** A rail host is supplied and styled by the application, never by the plugin. */
|
|
42
|
+
export const createHTMLWebProjection = (doc: Document, options: NativeUIRuntimeOptions): NativeUIRuntimeHandle => {
|
|
43
|
+
const host = options.rail?.host;
|
|
44
|
+
if (!host) {
|
|
45
|
+
let stopped = false;
|
|
46
|
+
return {
|
|
47
|
+
markForCheck: () => {},
|
|
48
|
+
getStatus: () => ({ state: stopped ? 'stopped' : 'web', projected: 0, updates: 0 }),
|
|
49
|
+
suspend: async () => ({ resume: async () => {} }),
|
|
50
|
+
destroy: async () => {
|
|
51
|
+
stopped = true;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const win = doc.defaultView!;
|
|
56
|
+
const owned = new Map<HTMLElement, { clone: HTMLButtonElement; aria: string | null; signature: string }>();
|
|
57
|
+
const style = doc.createElement('style');
|
|
58
|
+
style.textContent = `[data-native-ui-web-source] { visibility: hidden !important; pointer-events: none !important; }`;
|
|
59
|
+
doc.head.append(style);
|
|
60
|
+
let frame = 0;
|
|
61
|
+
let stopped = false;
|
|
62
|
+
let suspended = 0;
|
|
63
|
+
let updates = 0;
|
|
64
|
+
const restore = (source: HTMLElement) => {
|
|
65
|
+
const state = owned.get(source);
|
|
66
|
+
if (!state) return;
|
|
67
|
+
const focused = state.clone.contains(doc.activeElement);
|
|
68
|
+
state.clone.remove();
|
|
69
|
+
source.removeAttribute('data-native-ui-web-source');
|
|
70
|
+
if (focused && source.isConnected && !source.hasAttribute(marker)) source.focus();
|
|
71
|
+
owned.delete(source);
|
|
72
|
+
};
|
|
73
|
+
const measure = <T>(source: HTMLElement, read: () => T): T => {
|
|
74
|
+
const hidden = source.hasAttribute('data-native-ui-web-source');
|
|
75
|
+
if (hidden) source.removeAttribute('data-native-ui-web-source');
|
|
76
|
+
try {
|
|
77
|
+
return unprojected([source], read);
|
|
78
|
+
} finally {
|
|
79
|
+
if (hidden) source.setAttribute('data-native-ui-web-source', '');
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const render = () => {
|
|
83
|
+
frame = 0;
|
|
84
|
+
if (stopped) return;
|
|
85
|
+
observer.disconnect();
|
|
86
|
+
observer.observe(doc.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
87
|
+
const wanted = new Set<HTMLElement>();
|
|
88
|
+
if (!suspended && options.verticalBars?.enabled !== false && host?.isConnected && options.root?.isConnected) {
|
|
89
|
+
const sources = Array.from(options.root.querySelectorAll<HTMLElement>(htmlSelector));
|
|
90
|
+
let back = false;
|
|
91
|
+
for (const source of sources) {
|
|
92
|
+
// An accepted native surface owns its source and descendants. Rejected sources
|
|
93
|
+
// lose the marker and can return to the Web rail on the next render.
|
|
94
|
+
if (!htmlRailSource(source) || host.contains(source) || source.closest(`[${marker}]`)) continue;
|
|
95
|
+
const iconRoot = source.querySelector('ion-icon')?.shadowRoot;
|
|
96
|
+
if (iconRoot) observer.observe(iconRoot, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
97
|
+
const candidate = measure(source, () => readHTMLControl(source, () => 'web', options));
|
|
98
|
+
if (!candidate) continue;
|
|
99
|
+
if (candidate.control.kind === 'back-button') {
|
|
100
|
+
if (back) continue;
|
|
101
|
+
back = true;
|
|
102
|
+
}
|
|
103
|
+
wanted.add(source);
|
|
104
|
+
const item = candidate.control.items[0];
|
|
105
|
+
const svg = source.querySelector('svg') ?? iconRoot?.querySelector('svg');
|
|
106
|
+
const signature = JSON.stringify([item, source.innerHTML, svg?.outerHTML]);
|
|
107
|
+
let state = owned.get(source);
|
|
108
|
+
if (!state) {
|
|
109
|
+
const clone = doc.createElement('button');
|
|
110
|
+
clone.type = 'button';
|
|
111
|
+
clone.setAttribute('data-native-ui-proxy', candidate.control.kind);
|
|
112
|
+
state = { clone, aria: source.getAttribute('aria-hidden'), signature: '' };
|
|
113
|
+
owned.set(source, state);
|
|
114
|
+
clone.addEventListener('click', () => {
|
|
115
|
+
const current = !stopped && !suspended && measure(source, () => readHTMLControl(source, () => 'web', options));
|
|
116
|
+
if (
|
|
117
|
+
options.verticalBars?.enabled === false ||
|
|
118
|
+
!current ||
|
|
119
|
+
!htmlRailSource(source) ||
|
|
120
|
+
!host.isConnected ||
|
|
121
|
+
current.control.items[0].disabled ||
|
|
122
|
+
source.closest(`[${marker}]`)
|
|
123
|
+
)
|
|
124
|
+
return;
|
|
125
|
+
source.click(); // Source owns submission, custom handlers and back navigation.
|
|
126
|
+
schedule();
|
|
127
|
+
});
|
|
128
|
+
host.append(clone);
|
|
129
|
+
}
|
|
130
|
+
if (state.signature !== signature) {
|
|
131
|
+
state.signature = signature;
|
|
132
|
+
state.clone.replaceChildren();
|
|
133
|
+
if (svg) {
|
|
134
|
+
const icon = svg.cloneNode(true) as SVGElement;
|
|
135
|
+
icon.querySelectorAll('[id]').forEach((node) => node.removeAttribute('id'));
|
|
136
|
+
icon.removeAttribute('id');
|
|
137
|
+
for (const node of [icon, ...Array.from(icon.querySelectorAll('*'))])
|
|
138
|
+
for (const attr of Array.from(node.attributes)) if (attr.name.startsWith('on')) node.removeAttribute(attr.name);
|
|
139
|
+
state.clone.append(icon);
|
|
140
|
+
}
|
|
141
|
+
state.clone.append(doc.createTextNode(item.label));
|
|
142
|
+
state.clone.setAttribute('aria-label', item.accessibilityLabel);
|
|
143
|
+
state.clone.disabled = item.disabled;
|
|
144
|
+
updates++;
|
|
145
|
+
}
|
|
146
|
+
if (!source.hasAttribute('data-native-ui-web-source')) source.setAttribute('data-native-ui-web-source', '');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const source of owned.keys()) if (!wanted.has(source)) restore(source);
|
|
150
|
+
const clones = Array.from(wanted, (source) => owned.get(source)!.clone);
|
|
151
|
+
const order = Array.from(host.children).filter((child): child is HTMLButtonElement => clones.includes(child as HTMLButtonElement));
|
|
152
|
+
const focused = clones.find((clone) => clone === doc.activeElement);
|
|
153
|
+
clones.forEach((clone, index) => {
|
|
154
|
+
if (order[index] === clone) return;
|
|
155
|
+
host.insertBefore(clone, order[index]);
|
|
156
|
+
order.splice(order.indexOf(clone), 1);
|
|
157
|
+
order.splice(index, 0, clone);
|
|
158
|
+
});
|
|
159
|
+
// Moving an existing button can blur it; retain keyboard focus across source reordering.
|
|
160
|
+
if (focused && doc.activeElement !== focused) focused.focus({ preventScroll: true });
|
|
161
|
+
// Ignore our own synchronous DOM writes; later application mutations still schedule a refresh.
|
|
162
|
+
observer.takeRecords();
|
|
163
|
+
};
|
|
164
|
+
const schedule = () => {
|
|
165
|
+
if (!stopped && !frame) frame = win.requestAnimationFrame(render);
|
|
166
|
+
};
|
|
167
|
+
const observer = new MutationObserver(schedule);
|
|
168
|
+
win.addEventListener('resize', schedule);
|
|
169
|
+
win.addEventListener('nativeUIRefresh', schedule);
|
|
170
|
+
render();
|
|
171
|
+
return {
|
|
172
|
+
markForCheck: schedule,
|
|
173
|
+
getStatus: () => ({ state: stopped ? 'stopped' : 'web', projected: owned.size, updates }),
|
|
174
|
+
async suspend() {
|
|
175
|
+
suspended++;
|
|
176
|
+
for (const source of owned.keys()) restore(source);
|
|
177
|
+
let resumed = false;
|
|
178
|
+
return {
|
|
179
|
+
async resume() {
|
|
180
|
+
if (!resumed) {
|
|
181
|
+
resumed = true;
|
|
182
|
+
suspended--;
|
|
183
|
+
render();
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
async destroy() {
|
|
189
|
+
if (stopped) return;
|
|
190
|
+
stopped = true;
|
|
191
|
+
observer.disconnect();
|
|
192
|
+
win.cancelAnimationFrame(frame);
|
|
193
|
+
win.removeEventListener('resize', schedule);
|
|
194
|
+
win.removeEventListener('nativeUIRefresh', schedule);
|
|
195
|
+
for (const source of owned.keys()) restore(source);
|
|
196
|
+
style.remove();
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
};
|