@rdlabo/capacitor-native-ui 1.2.1 → 2.0.0-beta.pr304.sha56daf151ad80

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 (65) hide show
  1. package/README.md +38 -26
  2. package/dist/core.d.ts +2 -2
  3. package/dist/native/activation.d.ts.map +1 -1
  4. package/dist/native/activation.js +19 -19
  5. package/dist/native/activation.js.map +1 -1
  6. package/dist/native/definitions.d.ts +22 -40
  7. package/dist/native/definitions.d.ts.map +1 -1
  8. package/dist/native/html.d.ts.map +1 -1
  9. package/dist/native/html.js +0 -1
  10. package/dist/native/html.js.map +1 -1
  11. package/dist/native/index.d.ts +2 -2
  12. package/dist/native/index.d.ts.map +1 -1
  13. package/dist/native/index.js +2 -11
  14. package/dist/native/index.js.map +1 -1
  15. package/dist/native/overlays/controller.js +1 -1
  16. package/dist/native/overlays/controller.js.map +1 -1
  17. package/dist/native/prehide.js +1 -1
  18. package/dist/native/prehide.js.map +1 -1
  19. package/dist/native/runtime.d.ts +1 -1
  20. package/dist/native/runtime.d.ts.map +1 -1
  21. package/dist/native/runtime.js +8 -20
  22. package/dist/native/runtime.js.map +1 -1
  23. package/dist/native/shared/candidate.d.ts +2 -2
  24. package/dist/native/shared/candidate.d.ts.map +1 -1
  25. package/dist/native/shared/candidate.js +2 -2
  26. package/dist/native/shared/candidate.js.map +1 -1
  27. package/dist/native/shared/control-options.d.ts +6 -0
  28. package/dist/native/shared/control-options.d.ts.map +1 -0
  29. package/dist/native/shared/control-options.js +17 -0
  30. package/dist/native/shared/control-options.js.map +1 -0
  31. package/dist/native/shared/toolbar-options.d.ts +6 -2
  32. package/dist/native/shared/toolbar-options.d.ts.map +1 -1
  33. package/dist/native/shared/toolbar-options.js +7 -15
  34. package/dist/native/shared/toolbar-options.js.map +1 -1
  35. package/dist/native/startup.d.ts.map +1 -1
  36. package/dist/native/startup.js +0 -16
  37. package/dist/native/startup.js.map +1 -1
  38. package/dist/native/vertical-bars-web.d.ts.map +1 -1
  39. package/dist/native/vertical-bars-web.js +3 -2
  40. package/dist/native/vertical-bars-web.js.map +1 -1
  41. package/dist/vertical-bars.d.ts +2 -2
  42. package/docs/configuration.md +94 -72
  43. package/docs/framework-independent.md +11 -5
  44. package/docs/migration.md +62 -84
  45. package/docs/native-ui-extensions.md +31 -28
  46. package/docs/native-ui.md +34 -50
  47. package/docs/setup.md +34 -22
  48. package/docs/vertical-bars.md +59 -65
  49. package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +1 -1
  50. package/package.json +1 -1
  51. package/src/core.ts +1 -2
  52. package/src/native/activation.ts +13 -22
  53. package/src/native/definitions.ts +40 -90
  54. package/src/native/html.ts +0 -5
  55. package/src/native/index.ts +2 -11
  56. package/src/native/overlays/controller.ts +1 -1
  57. package/src/native/prehide.ts +1 -1
  58. package/src/native/runtime.ts +7 -16
  59. package/src/native/shared/candidate.ts +4 -4
  60. package/src/native/shared/control-options.ts +20 -0
  61. package/src/native/shared/toolbar-options.ts +8 -12
  62. package/src/native/startup.ts +0 -16
  63. package/src/native/vertical-bars-web.ts +4 -2
  64. package/src/vertical-bars.ts +2 -1
  65. package/docs/native-mini-player.md +0 -47
@@ -17,104 +17,61 @@ export interface NativeUIStatus {
17
17
  reason?: string;
18
18
  }
19
19
 
20
- export interface ToolbarProjectionOptions {
21
- /** Group selected actions; never selects children or back buttons. Defaults to true. */
22
- buttons?: boolean;
23
- /** Automatically select ordinary actions, inside or outside groups. Defaults to true. */
24
- button?: boolean;
25
- /** Automatically select back navigation independently of groups. Defaults to true. */
26
- backButton?: boolean;
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;
27
34
  }
28
35
 
29
- export interface VerticalControlAreaOptions {
30
- /** App-local DOM readers with matching SwiftUI or Compose registrations.
31
- * @experimental API and behavior may change in future releases.
32
- */
33
- extensions?: readonly NativeUIExtension[];
34
- /** Opt-in popovers anchored to native Vertical Bars buttons. */
35
- controls?: Pick<NativeUIControls, 'popover'>;
36
- /** Ordinary horizontal toolbar selection/grouping. true enables all three options; false disables them.
37
- * Object fields default to true. Local data-horizontal-bars overrides still apply.
38
- * Only enableNativeUI projects horizontal controls.
39
- */
40
- horizontal?: boolean | ToolbarProjectionOptions;
41
- /** Vertical toolbar selection/grouping. true enables all three options; false disables them.
42
- * Object fields default to true. Local data-vertical-bars overrides still apply.
43
- * Neither layout setting controls tabs, segments or FABs.
44
- */
45
- vertical?: boolean | ToolbarProjectionOptions;
46
- /** Appearance of native vertical ion-button and ion-menu-button actions.
47
- * `system` (default) uses SwiftUI styling and template icons; `source` projects supported Ionic fill and colors.
48
- * Local data-projection="source|system" or ios-theme-projection-source / ios-theme-projection-system take precedence:
49
- * the button itself, then its nearest ion-buttons, then this option. On the same element, a valid attribute wins;
50
- * otherwise system wins if both classes are present. Invalid attribute values are ignored.
51
- * Local settings update live; removing them restores inheritance. Actions, disabled state and grouping are preserved.
52
- * Does not affect back buttons, tabs, FABs, horizontal controls, source elements or Web clones.
53
- */
54
- buttonProjection?: 'source' | 'system';
55
- /** Default fill for native vertical ion-button actions resolved to `source`, including local overrides.
56
- * Use `solid` for Ionic's default design, or `null` (also the omitted default) for the iOS theme's glass design.
57
- * Applies when fill is omitted or `default`, outside ion-buttons. Inside ion-buttons the default is clear.
58
- * Explicit clear, solid and outline take precedence; clear and outline cannot be configured as defaults.
59
- * Source elements and Web clones are unchanged.
60
- */
61
- buttonDefaultFill?: 'solid' | null;
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
+ };
62
47
  }
63
48
 
64
- export interface NativeUIOptions extends VerticalControlAreaOptions {
65
- /** Log Web fallback reasons and their source elements once per reason during this activation. Defaults to false. */
66
- debug?: boolean;
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;
67
54
  verticalBars?: NativeUIVerticalBarsOptions;
68
- /** Ionic adapter settings. Explicit data-native-ui sources and extensions remain independent. */
55
+ /** App-local projection definitions, independent of controls.
56
+ * @experimental API and behavior may change.
57
+ */
58
+ extensions?: readonly NativeUIExtension[];
69
59
  ionic?: IonicProjectionOptions;
70
- /** Source subtree. Defaults to document.body. */
71
- root?: HTMLElement;
72
- /** @deprecated Call destroy() on the running handle to stop the shell. */
73
- enabled?: boolean;
74
- /** @deprecated Use ionic.auto for automatic Ionic projection and verticalBars.popover for popovers. */
75
- controls?: NativeUIControls;
76
- /** @deprecated Use verticalBars.only. */
77
- verticalBarsOnly?: boolean;
60
+ /** Log Web fallback reasons. Defaults to false. */
61
+ debug?: boolean;
78
62
  }
79
63
 
64
+ /** Options for the standalone rail entry. */
65
+ export type VerticalControlAreaOptions = NativeUIOptions;
66
+
80
67
  /** @internal Resolved adapter state; not an application configuration API. */
81
68
  export interface NativeUIRuntimeOptions extends NativeUIOptions {
82
69
  source?: 'ionic' | 'html' | 'auto' | 'extensions';
83
- /** @internal Preserves legacy controls.buttons behavior for explicit sources. */
84
- explicitControls?: boolean;
85
70
  nativeState?: { edge?: 'left' | 'right' };
86
- /** @internal Keep unavailable definitions discoverable for diagnostics without calling their readers. */
87
71
  unavailableExtensions?: ReadonlyMap<string, string>;
88
72
  rail?: { host: HTMLElement; edge: 'left' | 'right' };
89
73
  }
90
74
 
91
- export interface IonicProjectionOptions {
92
- /** Automatic Ionic projection. Defaults to true; partial objects keep omitted kinds enabled. */
93
- auto?: boolean | AutoIonicControls;
94
- }
95
-
96
- export interface AutoIonicControls {
97
- buttons?: boolean;
98
- tabs?: boolean;
99
- segment?: boolean;
100
- fab?: boolean;
101
- }
102
-
103
- export interface NativeUIControls {
104
- /** Toolbar actions and explicitly marked HTML buttons. Defaults to true. */
105
- buttons?: boolean;
106
- /** Projects tab bars and their native search presentation. */
107
- tabs?: boolean;
108
- /** @deprecated Use buttons. Projects toolbar buttons, including back and menu buttons. */
109
- toolbar?: boolean;
110
- /** Projects segments. */
111
- segment?: boolean;
112
- /** Projects floating action buttons. */
113
- fab?: boolean;
114
- /** Relays popovers anchored to native Vertical Bars buttons. Defaults to false. */
115
- popover?: boolean;
116
- }
117
-
118
75
  /** A running shell. Placement changes preserve all other startup settings. */
119
76
  export interface NativeUIHandle extends NativeUIRuntimeHandle {
120
77
  /** Enable rail projection when available, preserving other settings. Rejects after destroy. */
@@ -126,13 +83,6 @@ export interface NativeUIHandle extends NativeUIRuntimeHandle {
126
83
  export interface NativeUIVerticalBarsOptions {
127
84
  /** Defaults to true. Availability is always determined separately. */
128
85
  enabled?: boolean;
129
- /** Project only rail controls; unavailable or disabled rails leave controls on the Web. */
130
- only?: boolean;
131
- buttonProjection?: 'system' | 'source';
132
- /** Source-style standalone buttons with no explicit fill. Defaults to glass. */
133
- buttonDefaultFill?: 'glass' | 'solid';
134
- /** Ionic popover relay. Defaults to false. */
135
- popover?: boolean;
136
86
  /** Placement preference, not a declaration of device capability. */
137
87
  placement?: { edge: Exclude<VerticalBarEdge, null>; webHost?: HTMLElement };
138
88
  }
@@ -290,7 +240,7 @@ export interface WebViewMetrics {
290
240
  }
291
241
 
292
242
  export interface NativeUIPlugin {
293
- configure(options?: { verticalBarsOnly?: boolean }): Promise<{ supported: boolean; extensions?: string[] }>;
243
+ configure(options?: { suppressScrollEdgeEffect?: boolean }): Promise<{ supported: boolean; extensions?: string[] }>;
294
244
  completeExtensionAction(options: { requestId: string; accepted: boolean }): Promise<void>;
295
245
  getWebViewMetrics(): Promise<WebViewMetrics>;
296
246
  getVerticalBarsGeometry(options: { viewportWidth: number; ids: string[] }): Promise<{ revision?: number; items: Record<string, Frame> }>;
@@ -19,11 +19,6 @@ export const readHTMLControl = (element: HTMLElement, id: Identify, options: Nat
19
19
  if (!element.matches('button, ion-button, [role="button"]') || !visible(element, htmlRailSource(element))) return;
20
20
  if (element.contains(element.ownerDocument.activeElement)) return;
21
21
  if (!htmlRailSource(element) && !fitsClip(element)) return;
22
- if (
23
- options.explicitControls === false ||
24
- (options.explicitControls === undefined && (options.controls?.toolbar === false || options.controls?.buttons === false))
25
- )
26
- return;
27
22
  if (htmlRailSource(element) && options.verticalBars?.enabled === false) return;
28
23
  const kind = element.getAttribute('data-native-ui') === 'back-button' ? 'back-button' : 'button';
29
24
  const candidate = createCandidate(element, kind, id, options);
@@ -10,7 +10,7 @@ export type {
10
10
  NativeUIVerticalBarsOptions,
11
11
  NativeUIComponent,
12
12
  NativeUIControls,
13
- AutoIonicControls,
13
+ ControlProjection,
14
14
  IonicProjectionOptions,
15
15
  NativeUIHandle,
16
16
  NativeUIOptions,
@@ -19,7 +19,6 @@ export type {
19
19
  VerticalBarEdge,
20
20
  VerticalBarPlacement,
21
21
  VerticalControlAreaHandle,
22
- ToolbarProjectionOptions,
23
22
  VerticalControlAreaOptions,
24
23
  WebViewMetrics,
25
24
  } from './definitions';
@@ -28,14 +27,6 @@ export const enableNativeUI = (options: NativeUIOptions = {}) =>
28
27
  activate(options, async (...args) => (await import('./overlays/controller')).createOverlayController(...args));
29
28
 
30
29
  export const enableVerticalControlArea = async (options: VerticalControlAreaOptions = {}): Promise<VerticalControlAreaHandle> => {
31
- const handle = await enableNativeUI({
32
- extensions: options.extensions,
33
- horizontal: options.horizontal,
34
- vertical: options.vertical,
35
- buttonProjection: options.buttonProjection,
36
- buttonDefaultFill: options.buttonDefaultFill,
37
- controls: { tabs: true, toolbar: true, popover: options.controls?.popover === true },
38
- verticalBarsOnly: true,
39
- });
30
+ const handle = await enableNativeUI({ ...options, controls: options.controls ?? 'verticalOnly' });
40
31
  return { ...handle, setPlacement: setVerticalControlAreaPlacement };
41
32
  };
@@ -227,7 +227,7 @@ export const createOverlayController = async (
227
227
  overlay.localName === 'ion-popover' &&
228
228
  overlay.mode !== 'md' &&
229
229
  !overlay.classList.contains('md') &&
230
- options.controls?.popover === true &&
230
+ options.ionic?.verticalBars?.popover === true &&
231
231
  nativeVerticalBars() &&
232
232
  verticalBarsPopoverAnchor(overlay) !== undefined;
233
233
  if (stopped || suspensions.size || !enabled || excluded(overlay)) {
@@ -99,7 +99,7 @@ export const prehideVerticalBarsToolbarSources = (
99
99
  const children = childElements(element);
100
100
  const actions = children.filter((child) => eligible(child) && selectedAction(child, options));
101
101
  const group =
102
- (localPlacement(element) ?? toolbarOptions(options, true).buttons) &&
102
+ (localPlacement(element) ?? toolbarOptions(options, true).buttonGroup) &&
103
103
  eligible(element) &&
104
104
  actions.length === children.length &&
105
105
  (actions.length > 1 || (actions.length === 1 && actions[0].matches('ion-menu-button')));
@@ -1,3 +1,4 @@
1
+ import { controlSelected } from './shared/control-options';
1
2
  import { createDiagnostics } from './shared/diagnostics';
2
3
  import { htmlSelector, readHTMLControl } from './html';
3
4
  import { extensionAncestors, readExtension } from './extensions';
@@ -53,7 +54,6 @@ export const createRuntime = async (
53
54
  plugin: NativeUIPlugin,
54
55
  options: NativeUIRuntimeOptions = {},
55
56
  nativeVerticalBars: () => boolean = () => true,
56
- verticalBarsOnly = false,
57
57
  onStop?: (cause: 'failure' | 'pagehide') => void,
58
58
  ): Promise<NativeUIRuntimeHandle & { retain: (deferActivation?: (id: string) => boolean) => () => void }> => {
59
59
  const win = doc.defaultView!;
@@ -118,16 +118,12 @@ export const createRuntime = async (
118
118
  let listener: PluginListenerHandle | undefined;
119
119
  let searchListener: PluginListenerHandle | undefined;
120
120
  let waiters: (() => void)[] = [];
121
- const control = (kind: Candidate['control']['kind']): keyof NonNullable<NativeUIRuntimeOptions['controls']> => {
122
- if (kind === 'tab-bar') return 'tabs';
123
- if (kind === 'segment') return 'segment';
124
- if (kind === 'floating-action-button') return 'fab';
125
- return 'toolbar';
126
- };
127
121
  const controlEnabled = (candidate: Candidate) => {
128
- if (candidate.control.kind === 'extension') return true;
129
- const controls = options.controls;
130
- return controls === undefined || controls[control(candidate.control.kind)] === true;
122
+ const kind = candidate.control.kind;
123
+ // Button readers apply selection/grouping per child; explicit HTML and extensions
124
+ // are independent opt-ins. Only other automatic kinds need this final selection.
125
+ const key = kind === 'tab-bar' ? 'tabs' : kind === 'segment' ? 'segment' : kind === 'floating-action-button' ? 'fab' : undefined;
126
+ return !key || controlSelected(candidate.element, options, key, candidate.control.placement === 'vertical-bars');
131
127
  };
132
128
  const id = (element: Element) => {
133
129
  let value = ids.get(element);
@@ -200,10 +196,6 @@ export const createRuntime = async (
200
196
  return;
201
197
  }
202
198
  const candidate = readExtension(element, extensions, id, (reason) => diagnose(`${extension.name}: ${reason}`, element));
203
- if (verticalBarsOnly && (!candidate?.control.extension?.tabBarId || !nativeVerticalBars())) {
204
- if (candidate) diagnose('rail-only: element extensions stay on the Web when verticalBars.only is true.', element);
205
- return;
206
- }
207
199
  return candidate;
208
200
  }
209
201
  if (options.source === 'extensions') return;
@@ -217,12 +209,11 @@ export const createRuntime = async (
217
209
  return;
218
210
  }
219
211
  const rail = candidate.control.placement === 'vertical-bars';
220
- if (verticalBarsOnly && !rail) return;
221
212
  if (rail && !nativeVerticalBars()) return;
222
213
  return candidate;
223
214
  }
224
215
  if (element.querySelector(htmlSelector)) return; // A shared automatic surface must not cover explicitly selected descendants.
225
- if (verticalBarsOnly && (!isVerticalBarsCandidate(element) || element.closest('[data-native-ui-placement="element"]'))) return;
216
+ if (options.ionic?.auto === false) return;
226
217
  if (options.verticalBars?.enabled === false && isVerticalBarsCandidate(element)) return;
227
218
  const pointerPage = isVerticalBarsCandidate(element) ? element.closest<HTMLElement>('.ion-page') : undefined;
228
219
  let candidate: Candidate | undefined;
@@ -4,8 +4,8 @@ import { iconSource, type createIconRenderer } from './icons';
4
4
 
5
5
  export interface Candidate {
6
6
  assets?: { signature: string; render(renderer: ReturnType<typeof createIconRenderer>): Promise<void> };
7
- buttonProjection?: VerticalControlAreaOptions['buttonProjection'];
8
- buttonDefaultFill?: VerticalControlAreaOptions['buttonDefaultFill'];
7
+ buttonProjection?: 'system' | 'source';
8
+ buttonDefaultFill?: 'solid' | null;
9
9
  element: HTMLElement;
10
10
  sources?: HTMLElement[];
11
11
  control: NativeUIControl;
@@ -26,8 +26,8 @@ export const createCandidate = (
26
26
  const style = getComputedStyle(element);
27
27
  return {
28
28
  element,
29
- buttonProjection: options.buttonProjection,
30
- buttonDefaultFill: options.buttonDefaultFill,
29
+ buttonProjection: options.ionic?.verticalBars?.buttonProjection ?? 'system',
30
+ buttonDefaultFill: options.ionic?.verticalBars?.buttonDefaultFill === 'solid' ? 'solid' : null,
31
31
  control: {
32
32
  id: id(element),
33
33
  kind:
@@ -0,0 +1,20 @@
1
+ import type { NativeUIControls, NativeUIOptions } from '../definitions';
2
+
3
+ /** Resolve one default without changing discovery, explicit selection, or placement. */
4
+ export const controlDefault = (options: NativeUIOptions, kind: keyof NativeUIControls, vertical: boolean): boolean => {
5
+ const controls = options.controls;
6
+ const value = typeof controls === 'object' ? controls[kind] : controls;
7
+ if (value === undefined || value === true) return true;
8
+ if (value === false) return false;
9
+ return value === (vertical ? 'verticalOnly' : 'horizontalOnly');
10
+ };
11
+
12
+ export const controlSelected = (
13
+ element: HTMLElement,
14
+ options: NativeUIOptions,
15
+ kind: keyof NativeUIControls,
16
+ vertical: boolean,
17
+ ): boolean => {
18
+ const local = element.getAttribute(vertical ? 'data-vertical-bars' : 'data-horizontal-bars');
19
+ return local === 'true' ? true : local === 'false' ? false : controlDefault(options, kind, vertical);
20
+ };
@@ -1,17 +1,13 @@
1
1
  import { modalUsesVerticalBars } from './modal';
2
- import type { ToolbarProjectionOptions, VerticalControlAreaOptions } from '../definitions';
2
+ import type { VerticalControlAreaOptions } from '../definitions';
3
+ import { controlDefault } from './control-options';
3
4
  import { childElements, inFixedToolbar, isExcluded, isNativeUIDisabled } from './dom';
4
5
 
5
- export const toolbarOptions = (options: VerticalControlAreaOptions, vertical: boolean): Required<ToolbarProjectionOptions> => {
6
- const layout = vertical ? options.vertical : options.horizontal;
7
- if (typeof layout === 'boolean') return { buttons: layout, button: layout, backButton: layout };
8
- const settings = layout ?? {};
9
- return {
10
- buttons: settings.buttons ?? true,
11
- button: settings.button ?? true,
12
- backButton: settings.backButton ?? true,
13
- };
14
- };
6
+ export const toolbarOptions = (options: VerticalControlAreaOptions, vertical: boolean) => ({
7
+ buttonGroup: controlDefault(options, 'buttonGroup', vertical),
8
+ button: controlDefault(options, 'button', vertical),
9
+ backButton: controlDefault(options, 'backButton', vertical),
10
+ });
15
11
 
16
12
  export const localToolbarOption = (element: HTMLElement, vertical: boolean): boolean | undefined => {
17
13
  const value = element.getAttribute(vertical ? 'data-vertical-bars' : 'data-horizontal-bars');
@@ -29,7 +25,7 @@ export const horizontalSelected = (element: HTMLElement, options: VerticalContro
29
25
  export const horizontalToolbarGroup = (element: HTMLElement, options: VerticalControlAreaOptions): boolean => {
30
26
  if (
31
27
  !element.matches('ion-buttons.ios') ||
32
- !(localToolbarOption(element, false) ?? toolbarOptions(options, false).buttons) ||
28
+ !(localToolbarOption(element, false) ?? toolbarOptions(options, false).buttonGroup) ||
33
29
  !inFixedToolbar(element) ||
34
30
  isExcluded(element) ||
35
31
  isNativeUIDisabled(element)
@@ -13,27 +13,11 @@ export const startNativeUI = async (input: NativeUIOptions = {}, overlayFactory?
13
13
  throw new Error('Native UI requires a connected root in the current document');
14
14
  if (host && (!host.isConnected || host.ownerDocument !== document || root?.contains(host) || host.contains(root!)))
15
15
  throw new Error('The Web rail host must be connected in the current document and outside the source root');
16
- const legacy = input.controls;
17
- const automatic = input.ionic?.auto;
18
- const controls = automatic === undefined ? legacy : typeof automatic === 'object' ? automatic : {};
19
- const defaultEnabled = automatic !== false;
20
16
  const options: NativeUIRuntimeOptions = {
21
17
  ...input,
22
18
  root,
23
19
  source: 'auto',
24
- explicitControls: automatic !== undefined || (legacy?.buttons ?? legacy?.toolbar ?? true),
25
20
  verticalBars: bars,
26
- controls: {
27
- ...controls,
28
- tabs: controls?.tabs ?? defaultEnabled,
29
- segment: controls?.segment ?? defaultEnabled,
30
- fab: controls?.fab ?? defaultEnabled,
31
- toolbar: controls?.buttons ?? (automatic === undefined ? legacy?.toolbar : undefined) ?? defaultEnabled,
32
- popover: bars.popover ?? legacy?.popover ?? false,
33
- },
34
- verticalBarsOnly: bars.only ?? input.verticalBarsOnly ?? false,
35
- buttonProjection: bars.buttonProjection ?? input.buttonProjection ?? 'system',
36
- buttonDefaultFill: bars.buttonDefaultFill === 'glass' ? null : (bars.buttonDefaultFill ?? input.buttonDefaultFill ?? null),
37
21
  rail: host ? { host, edge: bars.placement?.edge === 'leading' ? 'left' : 'right' } : undefined,
38
22
  };
39
23
  const runtime = await activate(options, overlayFactory);
@@ -1,3 +1,4 @@
1
+ import { controlSelected } from './shared/control-options';
1
2
  import { createCrossfade, fadeMarker } from './shared/crossfade';
2
3
  import { alignVerticalBarClone, verticalBarFrame } from './shared/vertical-bars-geometry';
3
4
  import { createVerticalBarsWebSearch, searchProjectionClass } from './vertical-bars-web-search';
@@ -50,7 +51,7 @@ export const createVerticalBarsWebProjection = (
50
51
  const win = doc.defaultView!;
51
52
  const crossfade = createCrossfade(win);
52
53
  let incomingInstant: boolean | undefined;
53
- const toolbarEnabled = options.controls === undefined || options.controls.toolbar === true;
54
+ const toolbarEnabled = options.ionic?.auto !== false;
54
55
  let root: HTMLElement | undefined;
55
56
  let backSource: HTMLIonBackButtonElement | undefined;
56
57
  let backProjection: HTMLIonBackButtonElement | undefined;
@@ -79,7 +80,8 @@ export const createVerticalBarsWebProjection = (
79
80
  const search = createVerticalBarsWebSearch(
80
81
  doc,
81
82
  (element) =>
82
- (options.controls === undefined || options.controls.tabs === true) &&
83
+ options.ionic?.auto !== false &&
84
+ controlSelected(element, options, 'tabs', true) &&
83
85
  !!verticalBarsRoot()?.contains(element) &&
84
86
  (!options.root || options.root.contains(element)) &&
85
87
  !element.closest('[data-native-ui-placement="element"]') &&
@@ -5,7 +5,8 @@ export type {
5
5
  VerticalBarEdge,
6
6
  VerticalControlAreaHandle,
7
7
  VerticalControlAreaOptions,
8
- ToolbarProjectionOptions,
8
+ ControlProjection,
9
+ NativeUIControls,
9
10
  } from './native';
10
11
 
11
12
  export { applyFoldStateClasses } from './native/foldable/fold-state';
@@ -1,47 +0,0 @@
1
- ---
2
- title: Native UI Extension: Now Playing
3
- ---
4
-
5
- # Native UI Extension: Now Playing
6
-
7
- This example uses the experimental extension API, which may change in future releases.
8
-
9
- Use the [app-local extension API](native-ui-extensions.md) to add a SwiftUI mini-player with a Web fallback. The demo's Index → Native UI Extension page provides a sample you can copy into your application.
10
-
11
- - [Web component](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/src/app/mini-player/mini-player.component.ts): owns playback state and the Now Playing modal. Play/pause is separate from opening the player. Tap or swipe up on the track opens the same modal.
12
- - [Web renderer + DOM reader](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/src/native-ui/mini-player.ts): renders the Web UI from attributes, sends title, artist and playback state to SwiftUI, and maps `open` / `toggle` to the original Web buttons.
13
- - [SwiftUI View](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/ios/App/App/NativeMiniPlayer.swift): uses `Button`, SF Symbols, `DragGesture`, and a capsule glass background. It awaits `NativeUIActions.activate()` rather than duplicating playback state or presenting its own modal.
14
-
15
- The SwiftUI sample uses `NativeUIContext.sourceFrame.width` for compact spacing and `actions.isAvailable(...)` to disable actions whose Web targets are unavailable. See [projection context](native-ui-extensions.md#read-source-geometry-and-presentation-state) for coordinate units and system placement.
16
-
17
- Register the View once in the app delegate and pass `miniPlayer` in `enableNativeUI({ extensions: [miniPlayer] })`. Copy these app-local files to adapt the sample to your own playback state and artwork. The original sunset artwork is drawn with SwiftUI shapes, with a matching local SVG for the Web. The compact player shows only artwork, track metadata and playback; elapsed time stays in Now Playing.
18
-
19
- The sample uses the default **element projection**: SwiftUI follows the Web element’s position and size on iOS 26+, with ordinary tabs or Vertical Bars. Browsers and unsupported environments retain the Web player. Use the **Native extension** toggle to switch native projection on or off. The player starts in Web mode; disabling the extension restores its Web UI. It is shown only on this demo page; leaving the page hides it without resetting playback state.
20
-
21
- The Web fallback reserves its own bottom-slot space and keeps play state across tab changes. Opening Now Playing retires the native player through the shell's existing overlay policy; dismissing it restores the current Web state.
22
-
23
- This is a UI example: elapsed time is fixed and no audio engine is started. Applications own playback and reflect current values into the Web DOM. Connect your application's audio engine to the sample's playback events before using it as a player.
24
-
25
- ## Use the Web + SwiftUI set in your application
26
-
27
- The local TypeScript file includes both a renderer for an ordinary DOM element and its native reader. Copy it together with `NativeMiniPlayer.swift`; in your app, replace the demo’s relative `defineNativeUI` import with `@rdlabo/capacitor-native-ui`. The Web renderer works without Angular, Capacitor or an enabled Native UI; SwiftUI replaces it only after native accepts the projection.
28
-
29
- ```ts
30
- import { miniPlayer, mountNowPlaying } from './native-ui/mini-player';
31
- import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
32
-
33
- const player = document.querySelector<HTMLElement>('[data-mini-player]')!;
34
- const destroyWeb = mountNowPlaying(player); // Run on every platform, including the Web.
35
- player.addEventListener('togglePlayback', () => {
36
- player.setAttribute('data-playing', String(player.getAttribute('data-playing') !== 'true'));
37
- });
38
- player.addEventListener('openNowPlaying', () => openNowPlaying());
39
- await enableNativeUI({ extensions: [miniPlayer] });
40
- // Call destroyWeb() when the application's source element is destroyed.
41
- ```
42
-
43
- ```html
44
- <section data-mini-player data-title="Midnight Drive" data-artist="The Coast" data-playing="true"></section>
45
- ```
46
-
47
- The sample does not specify `placement`; the default is `'element'`. Applications can opt into `placement: 'tab-accessory'` when they want a system tab accessory; see the [placement options and host requirements](native-ui-extensions.md#content-and-placement-are-independent). Its surrounding Web layout belongs to the application. The same attributes feed the Web view and SwiftUI; both dispatch the same events. The application updates attributes after its playback state changes. Mount into an empty element. The Web renderer owns only its generated children and releases its observer and handlers on teardown. Frameworks own the host's attributes, not those children. Text is rendered as text, never interpreted as HTML.