@webview-native-ui/engine 1.0.0 → 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.
- package/README.md +48 -35
- package/WebViewNativeUIControls.podspec +17 -0
- package/dist/controls/activation.d.ts +15 -0
- package/dist/controls/activation.js +370 -0
- package/dist/controls/components/index.d.ts +16 -0
- package/dist/controls/components/index.js +38 -0
- package/dist/controls/components/ion-back-button.d.ts +4 -0
- package/dist/controls/components/ion-back-button.js +16 -0
- package/dist/controls/components/ion-button.d.ts +4 -0
- package/dist/controls/components/ion-button.js +26 -0
- package/dist/controls/components/ion-buttons.d.ts +5 -0
- package/dist/controls/components/ion-buttons.js +34 -0
- package/dist/controls/components/ion-fab.d.ts +4 -0
- package/dist/controls/components/ion-fab.js +164 -0
- package/dist/controls/components/ion-menu-button.d.ts +7 -0
- package/dist/controls/components/ion-menu-button.js +41 -0
- package/dist/controls/components/ion-segment.d.ts +5 -0
- package/dist/controls/components/ion-segment.js +24 -0
- package/dist/controls/components/ion-tab-bar.d.ts +5 -0
- package/dist/controls/components/ion-tab-bar.js +49 -0
- package/dist/controls/components/searchable-tabs.d.ts +20 -0
- package/dist/controls/components/searchable-tabs.js +525 -0
- package/dist/controls/definitions.d.ts +360 -0
- package/dist/controls/definitions.js +1 -0
- package/dist/controls/extensions.d.ts +44 -0
- package/dist/controls/extensions.js +164 -0
- package/dist/controls/foldable/fold-state.d.ts +4 -0
- package/dist/controls/foldable/fold-state.js +8 -0
- package/dist/controls/foldable/types.d.ts +9 -0
- package/dist/controls/foldable/types.js +1 -0
- package/dist/controls/html.d.ts +8 -0
- package/dist/controls/html.js +260 -0
- package/dist/controls/index.d.ts +7 -0
- package/dist/controls/index.js +4 -0
- package/dist/controls/ionic.d.ts +1 -0
- package/dist/controls/ionic.js +1 -0
- package/dist/controls/legacy-layout.d.ts +7 -0
- package/dist/controls/legacy-layout.js +23 -0
- package/dist/controls/overlays/content.d.ts +2 -0
- package/dist/controls/overlays/content.js +55 -0
- package/dist/controls/overlays/controller.d.ts +7 -0
- package/dist/controls/overlays/controller.js +369 -0
- package/dist/controls/overlays/covered-overlay.d.ts +4 -0
- package/dist/controls/overlays/covered-overlay.js +9 -0
- package/dist/controls/overlays/dialog-animation.d.ts +7 -0
- package/dist/controls/overlays/dialog-animation.js +49 -0
- package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
- package/dist/controls/overlays/dismiss-animation.js +14 -0
- package/dist/controls/overlays/focus.d.ts +2 -0
- package/dist/controls/overlays/focus.js +67 -0
- package/dist/controls/overlays/popover.d.ts +10 -0
- package/dist/controls/overlays/popover.js +117 -0
- package/dist/controls/overlays/styles.d.ts +9 -0
- package/dist/controls/overlays/styles.js +194 -0
- package/dist/controls/prehide.d.ts +6 -0
- package/dist/controls/prehide.js +280 -0
- package/dist/controls/runtime.d.ts +5 -0
- package/dist/controls/runtime.js +1685 -0
- package/dist/controls/shared/bridge.d.ts +4 -0
- package/dist/controls/shared/bridge.js +15 -0
- package/dist/controls/shared/candidate.d.ts +23 -0
- package/dist/controls/shared/candidate.js +128 -0
- package/dist/controls/shared/clipping.d.ts +1 -0
- package/dist/controls/shared/clipping.js +1 -0
- package/dist/controls/shared/connection.d.ts +21 -0
- package/dist/controls/shared/connection.js +112 -0
- package/dist/controls/shared/control-options.d.ts +4 -0
- package/dist/controls/shared/control-options.js +14 -0
- package/dist/controls/shared/covered-snapshot.d.ts +35 -0
- package/dist/controls/shared/covered-snapshot.js +41 -0
- package/dist/controls/shared/covered-source.d.ts +22 -0
- package/dist/controls/shared/covered-source.js +72 -0
- package/dist/controls/shared/crossfade.d.ts +11 -0
- package/dist/controls/shared/crossfade.js +71 -0
- package/dist/controls/shared/diagnostics.d.ts +2 -0
- package/dist/controls/shared/diagnostics.js +16 -0
- package/dist/controls/shared/dom.d.ts +45 -0
- package/dist/controls/shared/dom.js +258 -0
- package/dist/controls/shared/events.d.ts +12 -0
- package/dist/controls/shared/events.js +13 -0
- package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
- package/dist/controls/shared/horizontal-navigation.js +90 -0
- package/dist/controls/shared/icons.d.ts +5 -0
- package/dist/controls/shared/icons.js +66 -0
- package/dist/controls/shared/modal-animation.d.ts +21 -0
- package/dist/controls/shared/modal-animation.js +97 -0
- package/dist/controls/shared/modal.d.ts +26 -0
- package/dist/controls/shared/modal.js +261 -0
- package/dist/controls/shared/projection-id.d.ts +2 -0
- package/dist/controls/shared/projection-id.js +2 -0
- package/dist/controls/shared/scroll-placement.d.ts +1 -0
- package/dist/controls/shared/scroll-placement.js +1 -0
- package/dist/controls/shared/tab-bar-width.d.ts +7 -0
- package/dist/controls/shared/tab-bar-width.js +72 -0
- package/dist/controls/shared/toolbar-options.d.ts +10 -0
- package/dist/controls/shared/toolbar-options.js +38 -0
- package/dist/controls/shared/transition-identity.d.ts +3 -0
- package/dist/controls/shared/transition-identity.js +64 -0
- package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
- package/dist/controls/shared/vertical-bars-geometry.js +82 -0
- package/dist/controls/shared/vertical-navigation.d.ts +5 -0
- package/dist/controls/shared/vertical-navigation.js +39 -0
- package/dist/controls/startup.d.ts +7 -0
- package/dist/controls/startup.js +72 -0
- package/dist/controls/transport.d.ts +10 -0
- package/dist/controls/transport.js +1 -0
- package/dist/controls/vertical-bars-web-search.d.ts +8 -0
- package/dist/controls/vertical-bars-web-search.js +83 -0
- package/dist/controls/vertical-bars-web.d.ts +2 -0
- package/dist/controls/vertical-bars-web.js +609 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integration/index.d.ts +36 -0
- package/dist/integration/index.js +25 -0
- package/dist/native/bootstrap.js +1 -1
- package/dist/native/session.js +14 -8
- package/dist/projection/pause.d.ts +10 -0
- package/dist/projection/pause.js +58 -0
- package/dist/projection/styles.d.ts +1 -1
- package/dist/projection/styles.js +0 -1
- package/dist/projection/surfaces.js +11 -8
- package/dist/projection/web.js +48 -13
- package/dist/types.d.ts +5 -1
- package/docs/README.md +17 -10
- package/docs/concepts.md +39 -14
- package/docs/configuration/README.md +4 -2
- package/docs/configuration/ionic-overlays.md +36 -2
- package/docs/configuration/pause-resume.md +51 -0
- package/docs/controls.md +39 -0
- package/docs/getting-started/capacitor.md +3 -3
- package/docs/getting-started/cordova.md +3 -1
- package/docs/getting-started/initialization.md +18 -2
- package/docs/reference/platforms.md +2 -2
- package/docs/reference/projection.md +1 -1
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +6 -4
- package/ios/Sources/WebViewNativeUIEngine/ProjectionOrigins.swift +31 -0
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
- package/package.json +30 -5
- package/src/controls/activation.ts +427 -0
- package/src/controls/components/index.ts +45 -0
- package/src/controls/components/ion-back-button.ts +22 -0
- package/src/controls/components/ion-button.ts +28 -0
- package/src/controls/components/ion-buttons.ts +37 -0
- package/src/controls/components/ion-fab.ts +151 -0
- package/src/controls/components/ion-menu-button.ts +37 -0
- package/src/controls/components/ion-segment.ts +27 -0
- package/src/controls/components/ion-tab-bar.ts +55 -0
- package/src/controls/components/searchable-tabs.ts +534 -0
- package/src/controls/definitions.ts +338 -0
- package/src/controls/extensions.ts +198 -0
- package/src/controls/foldable/fold-state.ts +10 -0
- package/src/controls/foldable/types.ts +4 -0
- package/src/controls/html.ts +239 -0
- package/src/controls/index.ts +7 -0
- package/src/controls/ionic.ts +1 -0
- package/src/controls/legacy-layout.ts +21 -0
- package/src/controls/overlays/content.ts +51 -0
- package/src/controls/overlays/controller.ts +394 -0
- package/src/controls/overlays/covered-overlay.ts +11 -0
- package/src/controls/overlays/dialog-animation.ts +53 -0
- package/src/controls/overlays/dismiss-animation.ts +15 -0
- package/src/controls/overlays/focus.ts +64 -0
- package/src/controls/overlays/popover.ts +116 -0
- package/src/controls/overlays/styles.ts +195 -0
- package/src/controls/prehide.ts +285 -0
- package/src/controls/runtime.ts +1731 -0
- package/src/controls/shared/bridge.ts +18 -0
- package/src/controls/shared/candidate.ts +163 -0
- package/src/controls/shared/clipping.ts +1 -0
- package/src/controls/shared/connection.ts +105 -0
- package/src/controls/shared/control-options.ts +20 -0
- package/src/controls/shared/covered-snapshot.ts +52 -0
- package/src/controls/shared/covered-source.ts +78 -0
- package/src/controls/shared/crossfade.ts +70 -0
- package/src/controls/shared/diagnostics.ts +13 -0
- package/src/controls/shared/dom.ts +310 -0
- package/src/controls/shared/events.ts +14 -0
- package/src/controls/shared/horizontal-navigation.ts +106 -0
- package/src/controls/shared/icons.ts +64 -0
- package/src/controls/shared/modal-animation.ts +109 -0
- package/src/controls/shared/modal.ts +286 -0
- package/src/controls/shared/projection-id.ts +2 -0
- package/src/controls/shared/scroll-placement.ts +1 -0
- package/src/controls/shared/tab-bar-width.ts +66 -0
- package/src/controls/shared/toolbar-options.ts +51 -0
- package/src/controls/shared/transition-identity.ts +67 -0
- package/src/controls/shared/vertical-bars-geometry.ts +92 -0
- package/src/controls/shared/vertical-navigation.ts +34 -0
- package/src/controls/startup.ts +74 -0
- package/src/controls/transport.ts +11 -0
- package/src/controls/vertical-bars-web-search.ts +86 -0
- package/src/controls/vertical-bars-web.ts +674 -0
- package/src/dom/geometry.ts +24 -0
- package/src/dom/overlays.ts +196 -0
- package/src/dom/scroll-bindings.ts +66 -0
- package/src/dom/scroll-placement.ts +92 -0
- package/src/dom/visibility.ts +29 -0
- package/src/index.ts +19 -0
- package/src/integration/index.ts +64 -0
- package/src/ionic.ts +34 -0
- package/src/native/bootstrap.ts +132 -0
- package/src/native/bridge.ts +18 -0
- package/src/native/resource-url.ts +11 -0
- package/src/native/session.ts +708 -0
- package/src/native/updates.ts +103 -0
- package/src/native/values.ts +29 -0
- package/src/projection/events.ts +58 -0
- package/src/projection/pause.ts +58 -0
- package/src/projection/placement.ts +34 -0
- package/src/projection/scope.ts +13 -0
- package/src/projection/styles.ts +26 -0
- package/src/projection/surface.ts +88 -0
- package/src/projection/surfaces.ts +119 -0
- package/src/projection/web.ts +1013 -0
- package/src/types.ts +58 -0
|
@@ -5,10 +5,17 @@ above Engine-managed native content. Native registration handles capture and
|
|
|
5
5
|
restoration, but it cannot infer Ionic's presentation lifecycle from a native
|
|
6
6
|
view. This shared monitor supplies that coverage decision for all registrations.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
[
|
|
8
|
+
Applications use this adapter with an Engine-enabled plugin. Plugin authors first
|
|
9
|
+
integrate through the [Capacitor](../getting-started/capacitor.md) or
|
|
10
|
+
[Cordova](../getting-started/cordova.md) guide. Non-Ionic applications use their own
|
|
10
11
|
lifecycle notifications and do not install this adapter.
|
|
11
12
|
|
|
13
|
+
The `/ionic` entry point provides shared framework integration. It observes Ionic
|
|
14
|
+
overlays and determines which registered surfaces they cover. A plugin's explicit
|
|
15
|
+
presentation controls remain part of that plugin's API, connected through
|
|
16
|
+
[projection notifications](events.md) or its existing
|
|
17
|
+
[renderer API](../advanced/renderer.md).
|
|
18
|
+
|
|
12
19
|
For example, opening an Ionic settings modal should cover the background native
|
|
13
20
|
tab bar while leaving the modal's own native close button usable. A blanket hide
|
|
14
21
|
notification cannot express that relationship. The shared monitor evaluates each
|
|
@@ -32,6 +39,10 @@ One monitor is shared per document, including between the npm module and the run
|
|
|
32
39
|
|
|
33
40
|
The default covers Modal, Popover, Alert, Action Sheet, Loading, Picker, Toast and Menu lifecycle events. It compares each projection's DOM with the active overlay stack: content inside the front modal stays native, while covered content temporarily uses its prepared image. Toasts and inactive sheet backdrops do not cover a surface. Nested overlays keep it covered until the remaining stack permits restoration. Missing images retain the normal Web fallback. This temporary coverage never overwrites a plugin's explicit `web` or `none` destination. Projections registered later read the current state.
|
|
34
41
|
|
|
42
|
+
Here, Alert means Ionic's `ion-alert`. This adapter does not intercept
|
|
43
|
+
`window.alert()`, detect HTML `<dialog>` automatically, or observe native dialogs.
|
|
44
|
+
Custom Web overlays can supply lifecycle events through `additional`, below.
|
|
45
|
+
|
|
35
46
|
A popover displayed in a native window is different from a popover rendered in
|
|
36
47
|
the WebView: its DOM placeholder must not be treated as a covering Web overlay.
|
|
37
48
|
The `@rdlabo/capacitor-native-ui` plugin reports this native presentation ownership
|
|
@@ -42,6 +53,8 @@ explicit integration; it is not inferred from the popover's DOM alone.
|
|
|
42
53
|
## Customize unusual layouts
|
|
43
54
|
|
|
44
55
|
Optional settings are document-wide. Keep the defaults for ordinary Ionic overlays.
|
|
56
|
+
`IonicOverlayOptions`, exported from the same `/ionic` entry point, is the TypeScript
|
|
57
|
+
type for these settings; it is not another initialization function.
|
|
45
58
|
Use `exclude` for an application overlay that should not cover native content, and
|
|
46
59
|
`additional` when the same app also uses a custom dialog with its own lifecycle
|
|
47
60
|
events. Use `covers` only when DOM containment and backdrop state do not describe
|
|
@@ -63,3 +76,24 @@ enableSupportIonicOverlays({
|
|
|
63
76
|
```
|
|
64
77
|
|
|
65
78
|
Use `covers(overlay, target)` only when a registered DOM projection needs a custom coverage rule; return `undefined` for the default, or a boolean to override it. Exclusions and explicit native-window ownership take precedence. The standard relationship uses overlay order, DOM containment and backdrop state, rather than attempting to interpret arbitrary CSS stacking contexts. Configure unusual layouts explicitly.
|
|
79
|
+
|
|
80
|
+
## When a plugin needs to know which modal is behind another
|
|
81
|
+
|
|
82
|
+
For ordinary application setup, use `enableSupportIonicOverlays()` as shown above.
|
|
83
|
+
The Engine then handles Ionic overlay coverage without additional calls.
|
|
84
|
+
|
|
85
|
+
A plugin that places native controls inside Ionic modals may also need to know
|
|
86
|
+
which modal lies behind the one being opened. Two helpers from the same `/ionic`
|
|
87
|
+
entry point let the plugin read the modal order already tracked by the Engine.
|
|
88
|
+
|
|
89
|
+
For example, open modal A, then modal B above it:
|
|
90
|
+
|
|
91
|
+
- `ionicModalStack(document)` returns `[A, B]`, from back to front.
|
|
92
|
+
- `coveredIonicSurface(B)` returns A, the modal beneath B.
|
|
93
|
+
- If a popover opens above B, `coveredIonicSurface(popover)` returns B.
|
|
94
|
+
- `coveredIonicSurface(A)` returns `undefined`: the page behind A is not a modal.
|
|
95
|
+
|
|
96
|
+
A and B here are the actual `ion-modal` DOM elements. These helpers only read the
|
|
97
|
+
tracked modal order; they do not open or close overlays, switch projection, or
|
|
98
|
+
start monitoring. Call them after `enableSupportIonicOverlays()` has enabled the
|
|
99
|
+
monitor and the relevant overlay has entered its presentation lifecycle.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Cover native UI with a custom Web overlay
|
|
2
|
+
|
|
3
|
+
Use `pause()` before presenting a custom modal or `div` over native UI and
|
|
4
|
+
`resume()` when it closes. For Ionic overlays, use the automatic
|
|
5
|
+
[Ionic integration](ionic-overlays.md), which shares this coordination but only
|
|
6
|
+
covers surfaces behind the overlay.
|
|
7
|
+
|
|
8
|
+
## Plugin authors: re-export the Engine functions
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
export { pause, resume } from '@webview-native-ui/engine';
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The functions share one pause count across Engine-enabled plugins and bundles in
|
|
15
|
+
the same document. No wrapper or native plugin method is needed.
|
|
16
|
+
|
|
17
|
+
## Application authors: pair each pause with a resume
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { pause, resume } from 'your-plugin';
|
|
21
|
+
|
|
22
|
+
pause();
|
|
23
|
+
try {
|
|
24
|
+
await showCustomModal();
|
|
25
|
+
} finally {
|
|
26
|
+
resume();
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`showCustomModal()` must resolve when the modal closes. With event-driven modals,
|
|
31
|
+
call `resume()` on dismissal, cancellation or failure. Use one pair per overlay,
|
|
32
|
+
even with multiple plugins; nested overlays each need their own pair.
|
|
33
|
+
|
|
34
|
+
A pause replaces visible native surfaces with prepared Web images or their normal
|
|
35
|
+
Web fallback. Hidden surfaces stay hidden. The final `resume()` releases
|
|
36
|
+
the request, preserving each plugin's presentation intent and remaining Ionic
|
|
37
|
+
overlay coverage.
|
|
38
|
+
|
|
39
|
+
The counter does not track callers or pair individual calls. An extra `resume()`
|
|
40
|
+
while paused can release another overlay's request; only a `resume()` at zero has
|
|
41
|
+
no effect. Each owner must release exactly once for each pause it requested.
|
|
42
|
+
|
|
43
|
+
Both functions return immediately, without waiting for native acknowledgments or
|
|
44
|
+
painting. Returning from `pause()` does not mean native UI has already moved
|
|
45
|
+
behind the Web overlay; it may remain in front until the handoff completes.
|
|
46
|
+
The final `resume()` defers native restoration until the second animation frame,
|
|
47
|
+
allowing overlay removal to paint without guaranteeing compositor completion.
|
|
48
|
+
Call it after removing the overlay or completing
|
|
49
|
+
its dismissal animation. A new `pause()` cancels a pending restoration.
|
|
50
|
+
Calling `pause()` just before a blocking `window.alert()` therefore does
|
|
51
|
+
not guarantee a completed handoff. They do not detect custom overlays automatically.
|
package/docs/controls.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Shared controls for native adapters
|
|
2
|
+
|
|
3
|
+
The existing Engine package also owns the control implementation used by Capacitor Native UI.
|
|
4
|
+
No additional npm package or workspace is required.
|
|
5
|
+
|
|
6
|
+
- `@webview-native-ui/engine`: generic projection API; unchanged dependency boundary.
|
|
7
|
+
- `@webview-native-ui/engine/controls`: control runtime and transport contract.
|
|
8
|
+
- `@webview-native-ui/engine/controls/ionic`: optional Ionic overlay implementation.
|
|
9
|
+
- `@webview-native-ui/engine/integration`: the single shared transition/search integration state.
|
|
10
|
+
|
|
11
|
+
An adapter creates one `createNativeUI({ plugin, getPlatform, isAvailable })` instance and keeps it
|
|
12
|
+
for its lifetime. The plugin implements the `NativeUIPlugin` transport interface. The adapter owns
|
|
13
|
+
host registration, method dispatch, listener delivery, and lifecycle hooks; Engine owns rendering,
|
|
14
|
+
DOM ownership, extension acknowledgement, and teardown sequencing. Cordova can supply this same
|
|
15
|
+
transport without importing Capacitor. The Cordova Native UI plugin itself is a subsequent change.
|
|
16
|
+
|
|
17
|
+
SwiftPM exposes the optional `WebViewNativeUIControls` product, depending on `WebViewNativeUIEngine`.
|
|
18
|
+
CocoaPods exposes `WebViewNativeUIControls` with the same dependency. Android exposes the optional
|
|
19
|
+
`io.github.webview-native-ui:controls` artifact from `android/controls`; it depends on `engine` and
|
|
20
|
+
Compose. Generic Engine consumers keep their existing dependencies and Android minimum SDK 23.
|
|
21
|
+
Controls require Android API 24. Native adapters create `NativeUIController` and forward requests
|
|
22
|
+
and events. iOS receives JSON dictionaries and completion closures; Android receives JSONObjects.
|
|
23
|
+
|
|
24
|
+
The Ionic theme imports Engine integration directly. The Capacitor compatibility integration export
|
|
25
|
+
re-exports that module. Resolve a single Engine version in the application; do not bundle a private
|
|
26
|
+
copy of integration into the theme or adapter. The `/controls` entry does not load Ionic runtime code;
|
|
27
|
+
load `/controls/ionic` only when opting into Ionic overlays.
|
|
28
|
+
|
|
29
|
+
## Verification and release order
|
|
30
|
+
|
|
31
|
+
Run `npm run test:controls` for the migrated control tests and `npm test` for generic projection.
|
|
32
|
+
SwiftPM's `WebViewNativeUIEngine-Package` scheme covers both native targets. Android's
|
|
33
|
+
`:controls:testReleaseUnitTest` covers the migrated snapshot parser. Existing Capacitor transport
|
|
34
|
+
and hosted lifecycle tests remain in its repository, and the theme retains the real demo E2E suite.
|
|
35
|
+
|
|
36
|
+
Build and verify Engine first, including both signed Maven artifacts in the same Central bundle and
|
|
37
|
+
both npm-distributed podspecs. Release the Engine native artifacts and npm package at the same version
|
|
38
|
+
before refreshing the Capacitor and theme registry lockfiles. Candidate verification uses local npm
|
|
39
|
+
tarballs and a temporary SwiftPM path or local Maven repository; do not commit these temporary paths.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Capacitor plugin integration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Let an existing Capacitor plugin's native components coexist with Web layouts, navigation and overlays without changing its public `registerPlugin()` export. The plugin keeps its native renderer and application API; registration connects each native view to its DOM container using the [shared presentation model](../concepts.md).
|
|
4
4
|
|
|
5
5
|
This guide covers dependencies, registration, notifications and teardown. Start with
|
|
6
6
|
[native initialization](initialization.md) for the minimal call, required inputs
|
|
@@ -40,9 +40,9 @@ The primary native distribution is Swift Package Manager. Depend on the reposito
|
|
|
40
40
|
.product(name: "WebViewNativeUIEngine", package: "engine")
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Use the same Engine release across npm, SwiftPM, Gradle and CocoaPods, and upgrade deliberately. Applications should retain their dependency lockfiles for reproducible builds.
|
|
44
44
|
|
|
45
|
-
SwiftPM resolves this repository's version tags such as `
|
|
45
|
+
SwiftPM resolves this repository's version tags such as `v1.0.0`. For unreleased integration tests, use a local checkout or an explicit tested commit.
|
|
46
46
|
|
|
47
47
|
`import WebViewNativeUIEngine` requires no Capacitor registration. The npm companion supplies DOM measurement and Web image handoff; SwiftPM resolves the native dependency independently of npm hoisting.
|
|
48
48
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Cordova plugin integration
|
|
2
2
|
|
|
3
|
+
Let a Cordova plugin's native components coexist with Web layouts, navigation and overlays using the [same presentation model](../concepts.md) as Capacitor.
|
|
4
|
+
|
|
3
5
|
The same npm package, Swift library and Android AAR support Capacitor and Cordova. Engine is a library used by plugin maintainers, not a separately registered Cordova plugin. A Cordova application does not need `@capacitor/core` or a Capacitor native SDK.
|
|
4
6
|
|
|
5
7
|
The consuming plugin still owns its native view and the corresponding DOM, including placement, size and stacking order. Supply an existing selector such as `native-ad` or `#player`. The DOM may be added after initialization. Image preparation, scroll handling, overlays and restoration use the common Engine implementation.
|
|
@@ -32,7 +34,7 @@ frameworks report their own transition lifecycle through notifications.
|
|
|
32
34
|
Replace `<engine-version>` in every dependency below with the same published
|
|
33
35
|
release. Read that release tag’s documentation so the examples match its APIs.
|
|
34
36
|
|
|
35
|
-
Use the same Engine release for the Web package and native libraries.
|
|
37
|
+
Use the same Engine release for the Web package and native libraries. Pin matching versions and retain dependency lockfiles for reproducible builds.
|
|
36
38
|
|
|
37
39
|
```sh
|
|
38
40
|
npm install --save-exact '@webview-native-ui/engine@<engine-version>'
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Start with `NativeUIProjection` when a plugin owns a native view and a corresponding
|
|
4
4
|
Web container. Registration starts the runtime, binds that container and coordinates
|
|
5
|
-
|
|
5
|
+
presentation between the native view and the Web layout. This is the same starting
|
|
6
|
+
point for a component in a scrolling feed or beneath a Web overlay. You do not need
|
|
7
|
+
the renderer APIs for this route. See [the shared model](../concepts.md) for how
|
|
8
|
+
these situations relate and where platform limits apply.
|
|
6
9
|
|
|
7
10
|
Use the [Capacitor](capacitor.md) or [Cordova](cordova.md) guide for
|
|
8
11
|
dependency installation, plugin lifecycle hooks and notification forwarding.
|
|
@@ -46,7 +49,20 @@ projection = NativeUIProjection(
|
|
|
46
49
|
)
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
Java can use the same defaults without supplying every optional argument:
|
|
53
|
+
|
|
54
|
+
```java
|
|
55
|
+
NativeUIProjection.install(getBridge().getWebView());
|
|
56
|
+
projection = new NativeUIProjection(
|
|
57
|
+
getActivity().getWindow(),
|
|
58
|
+
getBridge().getWebView(),
|
|
59
|
+
getPluginHandle().getId(),
|
|
60
|
+
"#native-ui-fallback",
|
|
61
|
+
() -> nativeView
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
These snippets belong inside the integration guide's error boundary; retain the
|
|
50
66
|
existing Web fallback if initialization fails. `projection` and `nativeView` are
|
|
51
67
|
properties of your plugin. Dispose the registration with `destroy()` on iOS or
|
|
52
68
|
`close()` on Android when that renderer is retired. All native lifecycle calls
|
|
@@ -100,8 +100,8 @@ precision alone does not resolve this difference.
|
|
|
100
100
|
Left: **iPhone 15 Pro Max, iOS 27.2**. Right: **iPad mini (6th generation),
|
|
101
101
|
iPadOS 27.2**. Each device screenshot shows the captured **16-bit Display P3 PNG
|
|
102
102
|
in WKWebView above** and the **live UIKit tab bar below**, at the same size within
|
|
103
|
-
that device.
|
|
104
|
-
their shared color profile. These are physical-device results, not Simulator captures.
|
|
103
|
+
that device. For this comparison, the iPhone screenshot was resized to match the iPad
|
|
104
|
+
screenshot's height; their shared color profile was preserved. These are physical-device results, not Simulator captures.
|
|
105
105
|
|
|
106
106
|
The system screenshot preserves the live appearance, but the application-level
|
|
107
107
|
capture does not reproduce it completely. On the tested iPhone, the difference was
|
|
@@ -125,7 +125,7 @@ Listeners follow host reparenting and slot reassignment and are removed on teard
|
|
|
125
125
|
|
|
126
126
|
Scrolling is internal: iOS uses a uniquely matched native scroll binding when available; otherwise, including Android, scrolling temporarily uses the prepared image and restores native content after motion settles. Automatic scroll handling never overrides explicit `web` or `none`. Unsupported capture or geometry keeps the normal DOM fallback; it does not wait for a new capture on the transition path. The latest explicit destination survives WebView reload, and old-document replies cannot complete a new document's requests.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
The internal channel is limited to the main frame. Capacitor origins come from the bundled `capacitor.config.json`: `server.url` takes precedence over the local hostname/scheme. To override them, pass exact `allowedOrigins` (on Android, to `install` before navigation; registrations inherit them). Neither `allowNavigation` nor the current WebView URL expands this set. Android requires WebView's `WEB_MESSAGE_LISTENER` and `DOCUMENT_START_SCRIPT` features; registration fails explicitly on unsupported WebView implementations. Do not use wildcard origins for remotely navigable WebViews.
|
|
129
129
|
|
|
130
130
|
For capture modes, translucent surfaces, image color and private storage, see
|
|
131
131
|
[capture and platform limits](platforms.md). Custom renderer bridge and scroll
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
enum NativeUIButton {
|
|
4
|
+
private final class BadgeLabel: UILabel {}
|
|
5
|
+
private static let activationID = UIAction.Identifier("native-ui-activate")
|
|
6
|
+
|
|
7
|
+
/// Reuse an unambiguous presentation slot while keeping action IDs source-specific.
|
|
8
|
+
@MainActor
|
|
9
|
+
static func reassociate(_ nodes: [NativeUIControl], controls: inout [String: UIView],
|
|
10
|
+
fingerprints: inout [String: NativeUIControl]) -> Set<String> {
|
|
11
|
+
func identity(_ node: NativeUIControl) -> String? {
|
|
12
|
+
guard node.placement == nil, kinds.contains(node.kind), node.items.count == 1 else { return nil }
|
|
13
|
+
return node.items[0].content.transitionId
|
|
14
|
+
}
|
|
15
|
+
// swiftlint:disable:next force_unwrapping
|
|
16
|
+
let old = Dictionary(grouping: fingerprints.values.filter { identity($0) != nil }, by: { identity($0)! })
|
|
17
|
+
// swiftlint:disable:next force_unwrapping
|
|
18
|
+
let incoming = Dictionary(grouping: nodes.filter { identity($0) != nil }, by: { identity($0)! })
|
|
19
|
+
let retained = Set(nodes.map(\.id))
|
|
20
|
+
var reused = Set<String>()
|
|
21
|
+
for (key, matches) in incoming {
|
|
22
|
+
guard matches.count == 1, let node = matches.first, controls[node.id] == nil,
|
|
23
|
+
let previous = old[key], previous.count == 1, let source = previous.first,
|
|
24
|
+
!retained.contains(source.id), let button = controls[source.id] as? UIButton else { continue }
|
|
25
|
+
controls.removeValue(forKey: source.id)
|
|
26
|
+
fingerprints.removeValue(forKey: source.id)
|
|
27
|
+
controls[node.id] = button
|
|
28
|
+
reused.insert(node.id)
|
|
29
|
+
}
|
|
30
|
+
return reused
|
|
31
|
+
}
|
|
32
|
+
// These Ionic components share the same native UIButton presentation.
|
|
33
|
+
static let kinds: [NativeUIComponent] = [.button, .backButton, .menuButton]
|
|
34
|
+
|
|
35
|
+
@available(iOS 26.0, *)
|
|
36
|
+
static func make(_ node: NativeUIControl, existing: UIButton? = nil, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIButton? {
|
|
37
|
+
guard node.items.count == 1, let item = node.items.first else { return nil }
|
|
38
|
+
let control = render(item.content, glass: item.content.buttonFill != .clear, existing: existing, rendering: rendering, activate: activate)
|
|
39
|
+
control.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
|
|
40
|
+
return control
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@available(iOS 26.0, *)
|
|
44
|
+
static func render(_ item: NativeUIItemContent, glass: Bool, existing: UIButton? = nil, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIButton {
|
|
45
|
+
var configuration: UIButton.Configuration = glass ? .glass() : .plain()
|
|
46
|
+
configuration.title = item.label
|
|
47
|
+
configuration.image = rendering.image(item)
|
|
48
|
+
configuration.imagePadding = item.imagePadding ?? 4
|
|
49
|
+
configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: item.contentInsetLeading ?? 4,
|
|
50
|
+
bottom: 0, trailing: item.contentInsetTrailing ?? 4)
|
|
51
|
+
configuration.imagePlacement = item.iconPosition == .trailing ? .trailing : .leading
|
|
52
|
+
configuration.baseForegroundColor = rendering.color(item.color)
|
|
53
|
+
configuration.titleLineBreakMode = .byTruncatingTail
|
|
54
|
+
let size = item.fontSize
|
|
55
|
+
let weight = item.fontWeight
|
|
56
|
+
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
|
|
57
|
+
var outgoing = incoming
|
|
58
|
+
outgoing.font = UIFont.systemFont(ofSize: size, weight: weight >= 600 ? .semibold : weight >= 500 ? .medium : .regular)
|
|
59
|
+
return outgoing
|
|
60
|
+
}
|
|
61
|
+
let button = existing ?? UIButton(configuration: configuration)
|
|
62
|
+
if existing != nil { button.configuration = configuration }
|
|
63
|
+
button.titleLabel?.numberOfLines = 1
|
|
64
|
+
// Web and UIKit can differ by a fraction of a point in text measurement.
|
|
65
|
+
// Absorb that rounding without widening the projected button.
|
|
66
|
+
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
|
67
|
+
button.titleLabel?.minimumScaleFactor = 0.99
|
|
68
|
+
button.isEnabled = !item.disabled
|
|
69
|
+
button.accessibilityLabel = item.accessibilityLabel
|
|
70
|
+
button.accessibilityIdentifier = item.id
|
|
71
|
+
if item.selected { button.accessibilityTraits.insert(.selected) } else { button.accessibilityTraits.remove(.selected) }
|
|
72
|
+
if item.disabled { button.accessibilityTraits.insert(.notEnabled) } else { button.accessibilityTraits.remove(.notEnabled) }
|
|
73
|
+
button.subviews.filter { $0 is BadgeLabel }.forEach { $0.removeFromSuperview() }
|
|
74
|
+
button.accessibilityValue = item.badge?.value
|
|
75
|
+
if let badge = item.badge {
|
|
76
|
+
let label = BadgeLabel()
|
|
77
|
+
label.text = badge.value
|
|
78
|
+
label.font = .systemFont(ofSize: 11, weight: .semibold)
|
|
79
|
+
label.textColor = rendering.color(badge.textColor)
|
|
80
|
+
label.backgroundColor = rendering.color(badge.color)
|
|
81
|
+
label.textAlignment = .center
|
|
82
|
+
label.layer.cornerRadius = 8
|
|
83
|
+
label.clipsToBounds = true
|
|
84
|
+
label.isAccessibilityElement = false
|
|
85
|
+
label.translatesAutoresizingMaskIntoConstraints = false
|
|
86
|
+
button.addSubview(label)
|
|
87
|
+
NSLayoutConstraint.activate([
|
|
88
|
+
label.centerXAnchor.constraint(equalTo: button.centerXAnchor, constant: 14),
|
|
89
|
+
label.topAnchor.constraint(equalTo: button.topAnchor, constant: 1),
|
|
90
|
+
label.widthAnchor.constraint(greaterThanOrEqualToConstant: max(16, label.intrinsicContentSize.width + 6)),
|
|
91
|
+
label.heightAnchor.constraint(equalToConstant: 16)
|
|
92
|
+
])
|
|
93
|
+
}
|
|
94
|
+
button.removeAction(identifiedBy: activationID, for: .touchUpInside)
|
|
95
|
+
button.addAction(UIAction(identifier: activationID) { _ in activate(item.id) }, for: .touchUpInside)
|
|
96
|
+
return button
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
enum NativeUIButtons {
|
|
4
|
+
static let kind = NativeUIComponent.buttons
|
|
5
|
+
|
|
6
|
+
@available(iOS 26.0, *)
|
|
7
|
+
static func make(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIView {
|
|
8
|
+
let items = node.items
|
|
9
|
+
let rtl = node.rtl
|
|
10
|
+
let effect = UIGlassEffect(style: .regular)
|
|
11
|
+
let control = UIVisualEffectView(effect: effect)
|
|
12
|
+
control.layer.cornerRadius = node.frame.height * scale / 2
|
|
13
|
+
control.clipsToBounds = true
|
|
14
|
+
control.accessibilityIdentifier = node.id
|
|
15
|
+
for item in items {
|
|
16
|
+
let button = NativeUIButton.render(item.content, glass: false, rendering: rendering, activate: activate)
|
|
17
|
+
button.semanticContentAttribute = rtl ? .forceRightToLeft : .forceLeftToRight
|
|
18
|
+
let rect = item.frame.rect
|
|
19
|
+
button.frame = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale)
|
|
20
|
+
control.contentView.addSubview(button)
|
|
21
|
+
}
|
|
22
|
+
return control
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// Static composition of supported wire kinds. Component files own their names and rendering.
|
|
4
|
+
enum NativeUIComponents {
|
|
5
|
+
static let supported = Set(NativeUIButton.kinds + [NativeUIButtons.kind, NativeUITabBar.kind, NativeUISegment.kind, NativeUIFab.kind])
|
|
6
|
+
|
|
7
|
+
@available(iOS 26.0, *)
|
|
8
|
+
static func make(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering,
|
|
9
|
+
tabDelegate: UITabBarDelegate, activate: @escaping (String) -> Void) -> UIView? {
|
|
10
|
+
switch node.kind {
|
|
11
|
+
case .button, .backButton, .menuButton: return NativeUIButton.make(node, rendering: rendering, activate: activate)
|
|
12
|
+
case .buttons: return NativeUIButtons.make(node, scale: scale, rendering: rendering, activate: activate)
|
|
13
|
+
case .tabBar: return NativeUITabBar.make(node, rendering: rendering, delegate: tabDelegate)
|
|
14
|
+
case .segment: return NativeUISegment.make(node, scale: scale, rendering: rendering, activate: activate)
|
|
15
|
+
case .fab: return NativeUIFab()
|
|
16
|
+
case .extension: return nil // App-local SwiftUI content is hosted by the shell.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
final class NativeUIFab: UIView {
|
|
4
|
+
static let kind = NativeUIComponent.fab
|
|
5
|
+
var buttons: [String: UIButton] = [:]
|
|
6
|
+
var content: [String: NativeUIItemContent] = [:]
|
|
7
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
8
|
+
let hit = super.hitTest(point, with: event)
|
|
9
|
+
return hit === self ? nil : hit
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@available(iOS 26.0, *)
|
|
13
|
+
func apply(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) {
|
|
14
|
+
let items = node.items
|
|
15
|
+
let retained = Set(items.map(\.id))
|
|
16
|
+
var lostFocus = false
|
|
17
|
+
for id in Array(self.buttons.keys) where !retained.contains(id) {
|
|
18
|
+
lostFocus = lostFocus || self.buttons[id]?.accessibilityElementIsFocused() == true
|
|
19
|
+
self.buttons.removeValue(forKey: id)?.removeFromSuperview()
|
|
20
|
+
self.content.removeValue(forKey: id)
|
|
21
|
+
}
|
|
22
|
+
self.accessibilityIdentifier = node.id
|
|
23
|
+
for item in items {
|
|
24
|
+
let id = item.id
|
|
25
|
+
let artwork = item.content.fabArtwork
|
|
26
|
+
let previous = self.buttons[id]
|
|
27
|
+
let button = previous ?? NativeUIButton.render(artwork, glass: true, rendering: rendering, activate: activate)
|
|
28
|
+
if previous == nil {
|
|
29
|
+
self.buttons[id] = button
|
|
30
|
+
self.addSubview(button)
|
|
31
|
+
} else if self.content[id] != artwork {
|
|
32
|
+
let changedIcon = self.content[id]?.selected != item.content.selected
|
|
33
|
+
let duration = UIAccessibility.isReduceMotionEnabled ? 0 : min(item.content.iconTransition ?? 0, 1)
|
|
34
|
+
if changedIcon, let imageView = button.imageView, duration > 0 {
|
|
35
|
+
UIView.transition(with: imageView, duration: duration,
|
|
36
|
+
options: [.transitionCrossDissolve, .beginFromCurrentState, .allowUserInteraction]) {
|
|
37
|
+
_ = NativeUIButton.render(artwork, glass: true, existing: button, rendering: rendering, activate: activate)
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
UIView.performWithoutAnimation { _ = NativeUIButton.render(artwork, glass: true, existing: button, rendering: rendering, activate: activate) }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
self.content[id] = artwork
|
|
44
|
+
button.accessibilityTraits.remove(.selected)
|
|
45
|
+
if id == items.first?.id && items.count > 1 {
|
|
46
|
+
button.accessibilityValue = item.content.selected
|
|
47
|
+
? NSLocalizedString("Expanded", comment: "FAB accessibility state")
|
|
48
|
+
: NSLocalizedString("Collapsed", comment: "FAB accessibility state")
|
|
49
|
+
} else {
|
|
50
|
+
button.accessibilityValue = nil
|
|
51
|
+
}
|
|
52
|
+
let hidden = item.visible == false
|
|
53
|
+
if hidden && !button.isHidden && button.accessibilityElementIsFocused() { lostFocus = true }
|
|
54
|
+
button.isHidden = hidden
|
|
55
|
+
button.isUserInteractionEnabled = button.isEnabled && !hidden
|
|
56
|
+
button.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
|
|
57
|
+
let rect = item.frame.rect
|
|
58
|
+
let frame = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale)
|
|
59
|
+
if button.frame != frame { button.frame = frame }
|
|
60
|
+
}
|
|
61
|
+
let ordered = items.compactMap { self.buttons[$0.id] }.filter { !$0.isHidden }
|
|
62
|
+
self.accessibilityElements = ordered
|
|
63
|
+
if lostFocus, let main = ordered.first, main.isEnabled, self.window != nil {
|
|
64
|
+
UIAccessibility.post(notification: .layoutChanged, argument: main)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
extension NativeUIItemContent {
|
|
70
|
+
var fabArtwork: Self {
|
|
71
|
+
var artwork = self
|
|
72
|
+
if selected {
|
|
73
|
+
artwork.icon = closeIcon
|
|
74
|
+
artwork.iconWidth = closeIconWidth
|
|
75
|
+
artwork.iconHeight = closeIconHeight
|
|
76
|
+
artwork.label = ""
|
|
77
|
+
}
|
|
78
|
+
return artwork
|
|
79
|
+
}
|
|
80
|
+
}
|