@rdlabo/ionic-theme-ios27 1.2.0-beta.pr239.sha6c1c1e255674 → 1.2.0-beta.pr243.shae972a0e49191

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 (30) hide show
  1. package/README.md +2 -1
  2. package/dist/native/components/ion-buttons.js +1 -1
  3. package/dist/native/components/ion-buttons.js.map +1 -1
  4. package/dist/native/components/ion-menu-button.d.ts +2 -1
  5. package/dist/native/components/ion-menu-button.d.ts.map +1 -1
  6. package/dist/native/components/ion-menu-button.js +2 -2
  7. package/dist/native/components/ion-menu-button.js.map +1 -1
  8. package/dist/native/definitions.d.ts +2 -0
  9. package/dist/native/definitions.d.ts.map +1 -1
  10. package/dist/native/index.d.ts.map +1 -1
  11. package/dist/native/index.js +4 -3
  12. package/dist/native/index.js.map +1 -1
  13. package/dist/native/shared/candidate.d.ts +1 -0
  14. package/dist/native/shared/candidate.d.ts.map +1 -1
  15. package/dist/native/shared/candidate.js +11 -4
  16. package/dist/native/shared/candidate.js.map +1 -1
  17. package/docs/iphone-duo-with-original-theme.md +16 -12
  18. package/docs/iphone-duo.md +8 -155
  19. package/docs/native-ui-shell.md +5 -5
  20. package/docs/special-markup.md +2 -0
  21. package/docs/vertical-bars.md +276 -0
  22. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +34 -14
  23. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +2 -0
  24. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +3 -0
  25. package/package.json +2 -2
  26. package/src/native/components/ion-buttons.ts +1 -1
  27. package/src/native/components/ion-menu-button.ts +3 -2
  28. package/src/native/definitions.ts +15 -4
  29. package/src/native/index.ts +5 -3
  30. package/src/native/shared/candidate.ts +26 -8
package/README.md CHANGED
@@ -134,7 +134,7 @@ npx cap sync
134
134
 
135
135
  Apply its fold state with `applyFoldStateClasses(root, fold)` and its bar placement with `setVerticalControlAreaPlacement({ edge: verticalBarEdge, nativeEdge: verticalBarEdge, inset })`. Device monitoring is not bundled with the theme.
136
136
 
137
- Follow [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme) for the browser preview and iOS setup. For shared layout rules and APIs, see [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo). Available in `1.2.0-0` as an experimental feature; APIs and supported behavior may change.
137
+ Follow [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme) for the browser preview and iOS setup. For control projection and the runtime API, see [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars). For device events and split panes, see [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo). Available in `1.2.0-0` as an experimental feature; APIs and supported behavior may change.
138
138
 
139
139
  ### Use only the iOS 27 theme
140
140
 
@@ -222,6 +222,7 @@ For Ionic 9 Angular, import `isPlatform` and `provideIonicAngular` from `@ionic/
222
222
  - [ESLint](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/eslint) — check list structure with ESLint rules.
223
223
  - [Features](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/features) — CSS variables, Liquid Glass, selective imports, and dark mode.
224
224
  - [Native UI Shell (Experimental)](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell) — project supported Ionic controls, text, and icons into UIKit.
225
+ - [Vertical Bars (experimental)](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars) — side-rail layout, control eligibility, native button appearance and runtime API.
225
226
  - [iPhone Duo support (experimental)](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo) — vertical system rail, hinge posture, and split-pane layout; usable without the theme or the shell.
226
227
  - [iPhone Duo with your existing theme (experimental)](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme) — standalone setup that keeps your existing Web theme.
227
228
  - [Animation](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/animation) — tab, segment, and searchable effects.
@@ -14,7 +14,7 @@ const tracksMotion = true;
14
14
  const read = (element, id, options = {}) => {
15
15
  if (!inFixedToolbar(element)) return;
16
16
  let children = childElements(element);
17
- if (children.length === 1) return read$1(element, id);
17
+ if (children.length === 1) return read$1(element, id, options);
18
18
  const verticalBars = !element.closest("ion-menu, ion-popover") && modalUsesVerticalBars(element) && !!element.closest("ion-app.ios-theme-vertical-bars");
19
19
  if (verticalBars && !isVerticalBarsToolbarGroup(element)) return;
20
20
  if (verticalBars) children = verticalBarsToolbarActions(element);
@@ -1 +1 @@
1
- {"version":3,"file":"ion-buttons.js","names":["menuButton.read","menuButton.append"],"sources":["../../../src/native/components/ion-buttons.ts"],"sourcesContent":["import type { VerticalControlAreaOptions } from '../definitions';\nimport { modalUsesVerticalBars } from '../shared/modal';\nimport { createCandidate, appendItem } from '../shared/candidate';\nimport type { Candidate, Identify } from '../shared/candidate';\nimport { childElements, verticalBarsToolbarActions, inFixedToolbar, isVerticalBarsToolbarGroup } from '../shared/dom';\nimport * as menuButton from './ion-menu-button';\n\nexport const tag = 'ion-buttons';\nexport const tracksMotion = true;\n\nexport const read = (element: HTMLElement, id: Identify, options: VerticalControlAreaOptions = {}): Candidate | undefined => {\n if (!inFixedToolbar(element)) return;\n let children = childElements(element);\n if (children.length === 1) return menuButton.read(element, id);\n const verticalBars =\n !element.closest('ion-menu, ion-popover') && modalUsesVerticalBars(element) && !!element.closest('ion-app.ios-theme-vertical-bars');\n if (verticalBars && !isVerticalBarsToolbarGroup(element)) return;\n if (verticalBars) children = verticalBarsToolbarActions(element);\n if (\n !children.length ||\n children.some(\n (child) =>\n !child.matches(`${menuButton.tag}${verticalBars ? '' : '.ios'}`) &&\n (!child.matches(`ion-button${verticalBars ? '' : '.ios.button-clear'}`) ||\n (!verticalBars && (child as HTMLIonButtonElement).fill !== 'clear')),\n )\n )\n return;\n const candidate = createCandidate(element, tag, id, options);\n if (verticalBars && children.length !== childElements(element).length) candidate.sources = children;\n for (const child of children) {\n const supported = child.matches(menuButton.tag)\n ? menuButton.append(candidate, child as HTMLIonMenuButtonElement, id)\n : appendItem(candidate, child, id);\n if (!supported) return;\n }\n return candidate;\n};\n"],"mappings":";;;;;;;;;;;AAOA,MAAa,MAAM;AACnB,MAAa,eAAe;AAE5B,MAAa,QAAQ,SAAsB,IAAc,UAAsC,CAAC,MAA6B;CAC3H,IAAI,CAAC,eAAe,OAAO,GAAG;CAC9B,IAAI,WAAW,cAAc,OAAO;CACpC,IAAI,SAAS,WAAW,GAAG,OAAOA,OAAgB,SAAS,EAAE;CAC7D,MAAM,eACJ,CAAC,QAAQ,QAAQ,uBAAuB,KAAK,sBAAsB,OAAO,KAAK,CAAC,CAAC,QAAQ,QAAQ,iCAAiC;CACpI,IAAI,gBAAgB,CAAC,2BAA2B,OAAO,GAAG;CAC1D,IAAI,cAAc,WAAW,2BAA2B,OAAO;CAC/D,IACE,CAAC,SAAS,UACV,SAAS,MACN,UACC,CAAC,MAAM,QAAQ,kBAAoB,eAAe,KAAK,QAAQ,MAC9D,CAAC,MAAM,QAAQ,aAAa,eAAe,KAAK,qBAAqB,KACnE,CAAC,gBAAiB,MAA+B,SAAS,QACjE,GAEA;CACF,MAAM,YAAY,gBAAgB,SAAS,KAAK,IAAI,OAAO;CAC3D,IAAI,gBAAgB,SAAS,WAAW,cAAc,OAAO,CAAC,CAAC,QAAQ,UAAU,UAAU;CAC3F,KAAK,MAAM,SAAS,UAIlB,IAAI,EAHc,MAAM,QAAQ,iBAAc,IAC1CC,OAAkB,WAAW,OAAmC,EAAE,IAClE,WAAW,WAAW,OAAO,EAAE,IACnB;CAElB,OAAO;AACT"}
1
+ {"version":3,"file":"ion-buttons.js","names":["menuButton.read","menuButton.append"],"sources":["../../../src/native/components/ion-buttons.ts"],"sourcesContent":["import type { VerticalControlAreaOptions } from '../definitions';\nimport { modalUsesVerticalBars } from '../shared/modal';\nimport { createCandidate, appendItem } from '../shared/candidate';\nimport type { Candidate, Identify } from '../shared/candidate';\nimport { childElements, verticalBarsToolbarActions, inFixedToolbar, isVerticalBarsToolbarGroup } from '../shared/dom';\nimport * as menuButton from './ion-menu-button';\n\nexport const tag = 'ion-buttons';\nexport const tracksMotion = true;\n\nexport const read = (element: HTMLElement, id: Identify, options: VerticalControlAreaOptions = {}): Candidate | undefined => {\n if (!inFixedToolbar(element)) return;\n let children = childElements(element);\n if (children.length === 1) return menuButton.read(element, id, options);\n const verticalBars =\n !element.closest('ion-menu, ion-popover') && modalUsesVerticalBars(element) && !!element.closest('ion-app.ios-theme-vertical-bars');\n if (verticalBars && !isVerticalBarsToolbarGroup(element)) return;\n if (verticalBars) children = verticalBarsToolbarActions(element);\n if (\n !children.length ||\n children.some(\n (child) =>\n !child.matches(`${menuButton.tag}${verticalBars ? '' : '.ios'}`) &&\n (!child.matches(`ion-button${verticalBars ? '' : '.ios.button-clear'}`) ||\n (!verticalBars && (child as HTMLIonButtonElement).fill !== 'clear')),\n )\n )\n return;\n const candidate = createCandidate(element, tag, id, options);\n if (verticalBars && children.length !== childElements(element).length) candidate.sources = children;\n for (const child of children) {\n const supported = child.matches(menuButton.tag)\n ? menuButton.append(candidate, child as HTMLIonMenuButtonElement, id)\n : appendItem(candidate, child, id);\n if (!supported) return;\n }\n return candidate;\n};\n"],"mappings":";;;;;;;;;;;AAOA,MAAa,MAAM;AACnB,MAAa,eAAe;AAE5B,MAAa,QAAQ,SAAsB,IAAc,UAAsC,CAAC,MAA6B;CAC3H,IAAI,CAAC,eAAe,OAAO,GAAG;CAC9B,IAAI,WAAW,cAAc,OAAO;CACpC,IAAI,SAAS,WAAW,GAAG,OAAOA,OAAgB,SAAS,IAAI,OAAO;CACtE,MAAM,eACJ,CAAC,QAAQ,QAAQ,uBAAuB,KAAK,sBAAsB,OAAO,KAAK,CAAC,CAAC,QAAQ,QAAQ,iCAAiC;CACpI,IAAI,gBAAgB,CAAC,2BAA2B,OAAO,GAAG;CAC1D,IAAI,cAAc,WAAW,2BAA2B,OAAO;CAC/D,IACE,CAAC,SAAS,UACV,SAAS,MACN,UACC,CAAC,MAAM,QAAQ,kBAAoB,eAAe,KAAK,QAAQ,MAC9D,CAAC,MAAM,QAAQ,aAAa,eAAe,KAAK,qBAAqB,KACnE,CAAC,gBAAiB,MAA+B,SAAS,QACjE,GAEA;CACF,MAAM,YAAY,gBAAgB,SAAS,KAAK,IAAI,OAAO;CAC3D,IAAI,gBAAgB,SAAS,WAAW,cAAc,OAAO,CAAC,CAAC,QAAQ,UAAU,UAAU;CAC3F,KAAK,MAAM,SAAS,UAIlB,IAAI,EAHc,MAAM,QAAQ,iBAAc,IAC1CC,OAAkB,WAAW,OAAmC,EAAE,IAClE,WAAW,WAAW,OAAO,EAAE,IACnB;CAElB,OAAO;AACT"}
@@ -1,3 +1,4 @@
1
+ import { VerticalControlAreaOptions } from "../definitions.js";
1
2
  import { Candidate, Identify } from "../shared/candidate.js";
2
3
  declare namespace ion_menu_button_d_exports {
3
4
  export { append, read, selector, shadowSelector, tag };
@@ -6,7 +7,7 @@ export declare const tag = "ion-menu-button";
6
7
  export declare const selector = "";
7
8
  export declare const shadowSelector = "ion-menu-button";
8
9
  export declare const append: (candidate: Candidate, button: HTMLIonMenuButtonElement, id: Identify) => boolean;
9
- export declare const read: (group: HTMLElement, id: Identify) => Candidate | undefined;
10
+ export declare const read: (group: HTMLElement, id: Identify, options?: VerticalControlAreaOptions) => Candidate | undefined;
10
11
  //#endregion
11
12
  export { ion_menu_button_d_exports };
12
13
  //# sourceMappingURL=ion-menu-button.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ion-menu-button.d.ts","names":[],"sources":["../../../src/native/components/ion-menu-button.ts"],"mappings":";;;;qBAIa;qBAEA;qBACA;qBAEA,SAAU,WAAW,WAAW,QAAQ,0BAA0B,IAAI;qBAMtE,OAAQ,OAAO,aAAa,IAAI,aAAW"}
1
+ {"version":3,"file":"ion-menu-button.d.ts","names":[],"sources":["../../../src/native/components/ion-menu-button.ts"],"mappings":";;;;;qBAKa;qBAEA;qBACA;qBAEA,SAAU,WAAW,WAAW,QAAQ,0BAA0B,IAAI;qBAMtE,OAAQ,OAAO,aAAa,IAAI,UAAU,UAAS,+BAAkC"}
@@ -17,12 +17,12 @@ const append = (candidate, button, id) => {
17
17
  const content = !button.shadowRoot?.querySelector("slot")?.assignedNodes().length ? button.shadowRoot : button;
18
18
  return !!content && !!appendItem(candidate, button, id, content);
19
19
  };
20
- const read = (group, id) => {
20
+ const read = (group, id, options = {}) => {
21
21
  const children = group.matches("ion-buttons") ? childElements(group) : [];
22
22
  if (!inFixedToolbar(group) || children.length !== 1) return;
23
23
  const button = children[0];
24
24
  if (!button?.matches(`ion-menu-button${group.closest("ion-app.ios-theme-vertical-bars") ? "" : ".ios"}`)) return;
25
- const candidate = createCandidate(group, tag, id);
25
+ const candidate = createCandidate(group, tag, id, options);
26
26
  return append(candidate, button, id) ? candidate : void 0;
27
27
  };
28
28
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"ion-menu-button.js","names":[],"sources":["../../../src/native/components/ion-menu-button.ts"],"sourcesContent":["import { createCandidate, appendItem } from '../shared/candidate';\nimport type { Candidate, Identify } from '../shared/candidate';\nimport { childElements, inFixedToolbar } from '../shared/dom';\n\nexport const tag = 'ion-menu-button';\n// The glass and projection ownership belong to ion-buttons, even for one menu button.\nexport const selector = '';\nexport const shadowSelector = tag;\n\nexport const append = (candidate: Candidate, button: HTMLIonMenuButtonElement, id: Identify): boolean => {\n if (button.type !== 'button') return false;\n const content = !button.shadowRoot?.querySelector('slot')?.assignedNodes().length ? button.shadowRoot : button;\n return !!content && !!appendItem(candidate, button, id, content);\n};\n\nexport const read = (group: HTMLElement, id: Identify): Candidate | undefined => {\n const children = group.matches('ion-buttons') ? childElements(group) : [];\n if (!inFixedToolbar(group) || children.length !== 1) return;\n const button = children[0];\n if (!button?.matches(`${tag}${group.closest('ion-app.ios-theme-vertical-bars') ? '' : '.ios'}`)) return;\n const candidate = createCandidate(group, tag, id);\n return append(candidate, button as HTMLIonMenuButtonElement, id) ? candidate : undefined;\n};\n"],"mappings":";;;;;;;;;;;AAIA,MAAa,MAAM;AAEnB,MAAa,WAAW;AACxB,MAAa,iBAAiB;AAE9B,MAAa,UAAU,WAAsB,QAAkC,OAA0B;CACvG,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,MAAM,UAAU,CAAC,OAAO,YAAY,cAAc,MAAM,CAAC,EAAE,cAAc,CAAC,CAAC,SAAS,OAAO,aAAa;CACxG,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,WAAW,WAAW,QAAQ,IAAI,OAAO;AACjE;AAEA,MAAa,QAAQ,OAAoB,OAAwC;CAC/E,MAAM,WAAW,MAAM,QAAQ,aAAa,IAAI,cAAc,KAAK,IAAI,CAAC;CACxE,IAAI,CAAC,eAAe,KAAK,KAAK,SAAS,WAAW,GAAG;CACrD,MAAM,SAAS,SAAS;CACxB,IAAI,CAAC,QAAQ,QAAQ,kBAAS,MAAM,QAAQ,iCAAiC,IAAI,KAAK,QAAQ,GAAG;CACjG,MAAM,YAAY,gBAAgB,OAAO,KAAK,EAAE;CAChD,OAAO,OAAO,WAAW,QAAoC,EAAE,IAAI,YAAY,KAAA;AACjF"}
1
+ {"version":3,"file":"ion-menu-button.js","names":[],"sources":["../../../src/native/components/ion-menu-button.ts"],"sourcesContent":["import { createCandidate, appendItem } from '../shared/candidate';\nimport type { Candidate, Identify } from '../shared/candidate';\nimport type { VerticalControlAreaOptions } from '../definitions';\nimport { childElements, inFixedToolbar } from '../shared/dom';\n\nexport const tag = 'ion-menu-button';\n// The glass and projection ownership belong to ion-buttons, even for one menu button.\nexport const selector = '';\nexport const shadowSelector = tag;\n\nexport const append = (candidate: Candidate, button: HTMLIonMenuButtonElement, id: Identify): boolean => {\n if (button.type !== 'button') return false;\n const content = !button.shadowRoot?.querySelector('slot')?.assignedNodes().length ? button.shadowRoot : button;\n return !!content && !!appendItem(candidate, button, id, content);\n};\n\nexport const read = (group: HTMLElement, id: Identify, options: VerticalControlAreaOptions = {}): Candidate | undefined => {\n const children = group.matches('ion-buttons') ? childElements(group) : [];\n if (!inFixedToolbar(group) || children.length !== 1) return;\n const button = children[0];\n if (!button?.matches(`${tag}${group.closest('ion-app.ios-theme-vertical-bars') ? '' : '.ios'}`)) return;\n const candidate = createCandidate(group, tag, id, options);\n return append(candidate, button as HTMLIonMenuButtonElement, id) ? candidate : undefined;\n};\n"],"mappings":";;;;;;;;;;;AAKA,MAAa,MAAM;AAEnB,MAAa,WAAW;AACxB,MAAa,iBAAiB;AAE9B,MAAa,UAAU,WAAsB,QAAkC,OAA0B;CACvG,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,MAAM,UAAU,CAAC,OAAO,YAAY,cAAc,MAAM,CAAC,EAAE,cAAc,CAAC,CAAC,SAAS,OAAO,aAAa;CACxG,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,WAAW,WAAW,QAAQ,IAAI,OAAO;AACjE;AAEA,MAAa,QAAQ,OAAoB,IAAc,UAAsC,CAAC,MAA6B;CACzH,MAAM,WAAW,MAAM,QAAQ,aAAa,IAAI,cAAc,KAAK,IAAI,CAAC;CACxE,IAAI,CAAC,eAAe,KAAK,KAAK,SAAS,WAAW,GAAG;CACrD,MAAM,SAAS,SAAS;CACxB,IAAI,CAAC,QAAQ,QAAQ,kBAAS,MAAM,QAAQ,iCAAiC,IAAI,KAAK,QAAQ,GAAG;CACjG,MAAM,YAAY,gBAAgB,OAAO,KAAK,IAAI,OAAO;CACzD,OAAO,OAAO,WAAW,QAAoC,EAAE,IAAI,YAAY,KAAA;AACjF"}
@@ -8,6 +8,7 @@ export interface NativeUIShellStatus {
8
8
  reason?: string;
9
9
  }
10
10
  export interface VerticalControlAreaOptions {
11
+ buttonProjection?: 'source' | 'system';
11
12
  buttonDefaultFill?: 'solid' | null;
12
13
  }
13
14
  export interface NativeUIShellOptions extends VerticalControlAreaOptions {
@@ -58,6 +59,7 @@ export interface ShellItem extends Frame {
58
59
  fontSize: number;
59
60
  fontWeight: number;
60
61
  color: string;
62
+ buttonFill?: 'clear' | 'solid' | 'outline';
61
63
  backgroundColor?: string;
62
64
  borderColor?: string;
63
65
  borderWidth?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","names":[],"sources":["../../src/native/definitions.ts"],"mappings":";;;iBAKiB;EACf;EACA;EACA;EACA;;iBAGe;EAMf;;iBAGe,6BAA6B;EAE5C;EAEA,WAAW;EAEX;;iBAGe;EAEf;EAEA;EAEA;EAEA;;iBAGe;EAEf,aAAa;EAEb,WAAW,QAAQ;EAEnB,WAAW;;YAID;iBAEK;EACf,MAAM;EAEN;EAEA,aAAa;;iBAGE,kCAAkC;EAEjD,aAAa,WAAW,kBAAkB,sBAAsB;;iBAGjD;EAEf,UAAU;;iBAGK;EACf;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;;iBAGe,kBAAkB;EACjC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe,qBAAqB;EACpC;EACA,MAAM;EAEN;EAEA;EACA,OAAO;EACP;EACA;EACA;IAAiB;IAAgB;;EACjC,SAAS;;iBAGM;EACf;EACA,OAAO;EACP,SAAS;EACT;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe,yBAAyB;EACxC;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EAEA;EAEA,mBAAmB;EACnB,UAAU;;iBAGK;EACf;EACA;EACA;;iBAGe;EAEf;;iBAGe;EACf,UAAU;IAAY;MAA+B;IAAU;;EAC/D,qBAAqB,QAAQ;EAC7B,OAAO,UAAU,gBAAgB;IAAU;IAAkB;IAA6B;;EAC1F,MAAM;IAAW;MAAqB;EACtC,YAAY,kBAAkB,WAAW,OAAO,2BAA2B,QAAQ;EACnF,YAAY,gBAAgB,WAAW,OAAO,4BAA4B,QAAQ;EAClF,YAAY,8BAA8B,WAAW,OAAO,0BAA0B,QAAQ"}
1
+ {"version":3,"file":"definitions.d.ts","names":[],"sources":["../../src/native/definitions.ts"],"mappings":";;;iBAKiB;EACf;EACA;EACA;EACA;;iBAGe;EAQf;EAOA;;iBAGe,6BAA6B;EAE5C;EAEA,WAAW;EAEX;;iBAGe;EAEf;EAEA;EAEA;EAEA;;iBAGe;EAEf,aAAa;EAEb,WAAW,QAAQ;EAEnB,WAAW;;YAID;iBAEK;EACf,MAAM;EAEN;EAEA,aAAa;;iBAGE,kCAAkC;EAEjD,aAAa,WAAW,kBAAkB,sBAAsB;;iBAGjD;EAEf,UAAU;;iBAGK;EACf;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;;iBAGe,kBAAkB;EACjC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe,qBAAqB;EACpC;EACA,MAAM;EAEN;EAEA;EACA,OAAO;EACP;EACA;EACA;IAAiB;IAAgB;;EACjC,SAAS;;iBAGM;EACf;EACA,OAAO;EACP,SAAS;EACT;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe,yBAAyB;EACxC;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EAEA;EAEA,mBAAmB;EACnB,UAAU;;iBAGK;EACf;EACA;EACA;;iBAGe;EAEf;;iBAGe;EACf,UAAU;IAAY;MAA+B;IAAU;;EAC/D,qBAAqB,QAAQ;EAC7B,OAAO,UAAU,gBAAgB;IAAU;IAAkB;IAA6B;;EAC1F,MAAM;IAAW;MAAqB;EACtC,YAAY,kBAAkB,WAAW,OAAO,2BAA2B,QAAQ;EACnF,YAAY,gBAAgB,WAAW,OAAO,4BAA4B,QAAQ;EAClF,YAAY,8BAA8B,WAAW,OAAO,0BAA0B,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/native/index.ts"],"mappings":";;;qBA+Ba,oBAAkB;qBAoElB,iCAAsC,QAAQ;qBAiD9C,kCAAmC,WAAW,kBAAkB,sBAAsB;qBAsBtF,4BAAmC,UAAS,+BAAkC,QAAQ;qBAetF,sBAAuB,UAAS,yBAA4B,QAAQ"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/native/index.ts"],"mappings":";;;qBA+Ba,oBAAkB;qBAoElB,iCAAsC,QAAQ;qBAiD9C,kCAAmC,WAAW,kBAAkB,sBAAsB;qBAqBtF,4BAAmC,UAAS,+BAAkC,QAAQ;qBAgBtF,sBAAuB,UAAS,yBAA4B,QAAQ"}
@@ -130,12 +130,12 @@ const setVerticalControlAreaPlacement = (placement, rtl) => {
130
130
  };
131
131
  /**
132
132
  * Call once after ion-app is mounted. Ionic markup remains the source of truth.
133
- * Set buttonDefaultFill to `solid` for Ionic's default design, or `null` for this
134
- * theme's glass design. Omitting it is equivalent to `null` (existing behavior).
135
- * This default affects native vertical projection only; explicit button fills win.
133
+ * Native vertical buttons default to SwiftUI appearance.
134
+ * See VerticalControlAreaOptions for source styling and local overrides.
136
135
  */
137
136
  const enableVerticalControlArea = async (options = {}) => {
138
137
  const handle = await enableNativeUIShell({
138
+ buttonProjection: options.buttonProjection,
139
139
  buttonDefaultFill: options.buttonDefaultFill,
140
140
  controls: {
141
141
  tabs: true,
@@ -165,6 +165,7 @@ const enableNativeUIShell = (options = {}) => {
165
165
  const controls = options.controls;
166
166
  const configuration = JSON.stringify([
167
167
  options.verticalBarsOnly === true,
168
+ options.buttonProjection ?? "system",
168
169
  options.buttonDefaultFill ?? null,
169
170
  ...[
170
171
  "tabs",
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/native/index.ts"],"sourcesContent":["import { Capacitor, registerPlugin } from '@capacitor/core';\nimport { setConfig } from '../transition/ios.transition';\nimport type {\n NativeUIShellHandle,\n NativeUIShellOptions,\n NativeUIShellPlugin,\n VerticalBarEdge,\n VerticalBarPlacement,\n VerticalControlAreaHandle,\n VerticalControlAreaOptions,\n WebViewMetrics,\n} from './definitions';\nimport { createRuntime } from './runtime';\nimport { createVerticalBarsWebProjection } from './vertical-bars-web';\nimport { prehideVerticalBarsToolbarSources } from './prehide';\nimport { observeVerticalBarsModals } from './shared/modal';\nexport type {\n NativeUIShellComponent,\n NativeUIShellControls,\n NativeUIShellHandle,\n NativeUIShellOptions,\n NativeUIShellStatus,\n NativeUIShellSuspension,\n VerticalBarEdge,\n VerticalBarPlacement,\n VerticalControlAreaHandle,\n VerticalControlAreaOptions,\n WebViewMetrics,\n} from './definitions';\n\nconst plugin = registerPlugin<NativeUIShellPlugin>('IonicNativeUIShell');\nexport const IonicNativeUIShell = plugin;\nlet active: Promise<NativeUIShellHandle> | undefined;\nlet activeConfiguration: string | undefined;\nconst web = (reason: string): NativeUIShellHandle => ({\n getStatus: () => ({ state: 'web', projected: 0, updates: 0, reason }),\n suspend: async () => ({ resume: async () => {} }),\n destroy: async () => {},\n});\n\nconst combine = (native: NativeUIShellHandle, fallback: NativeUIShellHandle): NativeUIShellHandle => ({\n getStatus() {\n const nativeStatus = native.getStatus();\n const fallbackStatus = fallback.getStatus();\n return {\n ...nativeStatus,\n state:\n nativeStatus.state === 'stopped' && fallbackStatus.state === 'stopped' ? 'stopped' : nativeStatus.projected > 0 ? 'native' : 'web',\n projected: nativeStatus.projected + fallbackStatus.projected,\n updates: nativeStatus.updates + fallbackStatus.updates,\n };\n },\n async suspend() {\n const [nativeLease, fallbackLease] = await Promise.all([native.suspend(), fallback.suspend()]);\n return { resume: async () => void (await Promise.all([nativeLease.resume(), fallbackLease.resume()])) };\n },\n async destroy() {\n await Promise.all([native.destroy(), fallback.destroy()]);\n },\n});\n\n/** Wraps a runtime handle with the shared enable-lifecycle: reason, suspend hooks and idempotent destroy. */\nconst manage = (\n handle: NativeUIShellHandle,\n lifecycle: {\n reason?: string;\n suspend?: () => (() => void) | undefined;\n /** Runs after teardown; clears the shared slot only while this activation owns it. */\n release?: () => void;\n destroy?: () => void | Promise<void>;\n } = {},\n): NativeUIShellHandle => {\n let destroyed = false;\n return {\n getStatus: () => (lifecycle.reason ? { ...handle.getStatus(), reason: lifecycle.reason } : handle.getStatus()),\n async suspend() {\n const lease = await handle.suspend();\n const resume = lifecycle.suspend?.();\n return {\n async resume() {\n await lease.resume();\n resume?.();\n },\n };\n },\n async destroy() {\n if (destroyed) return;\n destroyed = true;\n try {\n await handle.destroy();\n } finally {\n await lifecycle.destroy?.();\n lifecycle.release?.();\n }\n },\n };\n};\n\n/** Reads the current native WebView geometry and applies it to page transitions. */\nexport const configureNativeTransition = async (): Promise<WebViewMetrics> => {\n const metrics = typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' ? await plugin.getWebViewMetrics() : { radius: 0 };\n setConfig({ radius: metrics.radius });\n return metrics;\n};\n\n/** Resolves a logical vertical-bar edge to the physical side for the given direction. */\nconst physicalVerticalBarEdge = (edge: Exclude<VerticalBarEdge, null>, rtl: boolean): 'left' | 'right' =>\n (edge === 'leading') !== rtl ? 'left' : 'right';\n\nconst elementRtl = (element: Element): boolean => element.closest('[dir]')?.getAttribute('dir') === 'rtl';\n\n// Device facts are supplied by the application; the theme only compares placement.\nconst nativePlacements = new WeakMap<HTMLElement, { edge: VerticalBarEdge; rtl?: boolean }>();\nconst horizontalFallbackAttribute = 'data-native-ui-shell-vertical-bars-suspended';\n\n// Keep the requested rail while ordinary Native UI Shell temporarily uses its\n// horizontal layout. Removing the effective class restores normal measurements,\n// toolbar ownership and safe areas throughout the existing rendering pipeline.\nconst observeNativeVerticalBarsLayout = (doc: Document): (() => void) => {\n const reconcile = () => {\n const app = doc.querySelector<HTMLElement>('ion-app');\n if (!app) return;\n const requested = app.classList.contains('ios-theme-vertical-bars') || app.hasAttribute(horizontalFallbackAttribute);\n const suspended = requested && nativePlacements.get(app)?.edge == null;\n if (app.hasAttribute(horizontalFallbackAttribute) !== suspended) app.toggleAttribute(horizontalFallbackAttribute, suspended);\n if (app.classList.contains('ios-theme-vertical-bars') !== (requested && !suspended)) {\n app.classList.toggle('ios-theme-vertical-bars', requested && !suspended);\n }\n };\n const observer = new MutationObserver(reconcile);\n observer.observe(doc.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });\n doc.defaultView?.addEventListener('nativeUIShellRefresh', reconcile);\n reconcile();\n return () => {\n observer.disconnect();\n doc.defaultView?.removeEventListener('nativeUIShellRefresh', reconcile);\n const app = doc.querySelector<HTMLElement>(`ion-app[${horizontalFallbackAttribute}]`);\n if (app) {\n app.removeAttribute(horizontalFallbackAttribute);\n app.classList.add('ios-theme-vertical-bars');\n }\n };\n};\n\n/**\n * Applies one placement to the CSS layout and both Web/native projections.\n * Pass `rtl` when the document direction is known; otherwise the nearest `dir` attribute is used.\n */\nexport const setVerticalControlAreaPlacement = (placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean): void => {\n if (typeof document === 'undefined') return;\n const app = document.querySelector<HTMLElement>('ion-app');\n if (!app) throw new Error('Vertical Control Area requires ion-app');\n const { edge, inset = 0 } = placement && typeof placement === 'object' ? placement : { edge: placement, inset: 0 };\n if (placement && typeof placement === 'object' && placement.nativeEdge !== undefined) {\n nativePlacements.set(app, { edge: placement.nativeEdge, rtl });\n }\n app.removeAttribute(horizontalFallbackAttribute);\n app.classList.toggle('ios-theme-vertical-bars', edge !== null);\n app.classList.toggle('ios-theme-vertical-bars-left', edge !== null && physicalVerticalBarEdge(edge, rtl ?? elementRtl(app)) === 'left');\n if (edge && Number.isFinite(inset) && inset > 0) app.style.setProperty('--ios-theme-vertical-bars-native-inset', `${inset}px`);\n else app.style.removeProperty('--ios-theme-vertical-bars-native-inset');\n document.defaultView?.dispatchEvent(new Event('nativeUIShellRefresh'));\n};\n\n/**\n * Call once after ion-app is mounted. Ionic markup remains the source of truth.\n * Set buttonDefaultFill to `solid` for Ionic's default design, or `null` for this\n * theme's glass design. Omitting it is equivalent to `null` (existing behavior).\n * This default affects native vertical projection only; explicit button fills win.\n */\nexport const enableVerticalControlArea = async (options: VerticalControlAreaOptions = {}): Promise<VerticalControlAreaHandle> => {\n const handle = await enableNativeUIShell({\n buttonDefaultFill: options.buttonDefaultFill,\n controls: { tabs: true, toolbar: true },\n verticalBarsOnly: true,\n });\n return {\n getStatus: () => handle.getStatus(),\n suspend: () => handle.suspend(),\n destroy: () => handle.destroy(),\n setPlacement: setVerticalControlAreaPlacement,\n };\n};\n\n/** Call once at application startup. Ionic markup remains the source of truth. */\nexport const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise<NativeUIShellHandle> => {\n if (options.enabled === false) {\n const current = active;\n active = undefined;\n activeConfiguration = undefined;\n return current\n ? current.then(async (handle) => {\n await handle.destroy();\n return web('Disabled');\n })\n : Promise.resolve(web('Disabled'));\n }\n if (typeof document === 'undefined') return Promise.resolve(web('Requires a document'));\n const controls = options.controls;\n const configuration = JSON.stringify([\n options.verticalBarsOnly === true,\n options.buttonDefaultFill ?? null,\n ...(['tabs', 'toolbar', 'segment', 'fab'] as const).map((component) => !controls || controls[component] === true),\n ]);\n if (active && activeConfiguration !== configuration)\n return Promise.reject(\n new Error('Native UI Shell is already running with different controls; destroy it before changing configuration.'),\n );\n if (active) return active;\n activeConfiguration = configuration;\n const stopModals = observeVerticalBarsModals(document);\n const prehide =\n options.controls === undefined || options.controls.toolbar === true ? prehideVerticalBarsToolbarSources(document) : undefined;\n // A slow destroy of a superseded activation must not release a newer one.\n let start: Promise<NativeUIShellHandle>;\n const release = () => {\n if (active === start) {\n active = undefined;\n activeConfiguration = undefined;\n }\n };\n const fallback = (reason: string) =>\n manage(createVerticalBarsWebProjection(document, options), {\n reason,\n suspend: () => prehide?.suspend(),\n destroy: () => {\n prehide?.stop();\n stopModals();\n },\n release,\n });\n return (start = active =\n (async () => {\n if (Capacitor.getPlatform() !== 'ios') return fallback('Requires Capacitor iOS');\n let runtime: NativeUIShellHandle | undefined;\n let metricsListener: Awaited<ReturnType<typeof plugin.addListener>> | undefined;\n let stopVerticalBarsLayout: (() => void) | undefined;\n try {\n const capabilities = await plugin.configure({ verticalBarsOnly: options.verticalBarsOnly === true });\n if (!capabilities.supported) return fallback('Requires iOS 26 or later');\n if (!options.verticalBarsOnly) stopVerticalBarsLayout = observeNativeVerticalBarsLayout(document);\n // The application owns device state and selects the rail through placement classes.\n const nativeVerticalBars = () => {\n const app = document.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');\n if (!app) return false;\n const reported = nativePlacements.get(app);\n if (!reported?.edge) return false;\n const physicalEdge = app.classList.contains('ios-theme-vertical-bars-left') ? 'left' : 'right';\n return physicalEdge === physicalVerticalBarEdge(reported.edge, reported.rtl ?? elementRtl(app));\n };\n if (!options.verticalBarsOnly) {\n metricsListener = await plugin.addListener('webViewMetricsChange', (metrics) => {\n setConfig({ radius: metrics.radius });\n });\n await configureNativeTransition().catch(() => undefined);\n }\n const native = await createRuntime(document, plugin, options, nativeVerticalBars, options.verticalBarsOnly === true);\n runtime = combine(\n native,\n createVerticalBarsWebProjection(document, options, () => !nativeVerticalBars() || native.getStatus().state === 'stopped'),\n );\n return manage(runtime, {\n suspend: () => prehide?.suspend(),\n release,\n destroy: async () => {\n await metricsListener?.remove().catch(() => {});\n stopVerticalBarsLayout?.();\n prehide?.stop();\n stopModals();\n },\n });\n } catch (error) {\n await runtime?.destroy();\n await metricsListener?.remove().catch(() => {});\n stopVerticalBarsLayout?.();\n return fallback(error instanceof Error ? error.message : String(error));\n }\n })());\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/native/index.ts"],"sourcesContent":["import { Capacitor, registerPlugin } from '@capacitor/core';\nimport { setConfig } from '../transition/ios.transition';\nimport type {\n NativeUIShellHandle,\n NativeUIShellOptions,\n NativeUIShellPlugin,\n VerticalBarEdge,\n VerticalBarPlacement,\n VerticalControlAreaHandle,\n VerticalControlAreaOptions,\n WebViewMetrics,\n} from './definitions';\nimport { createRuntime } from './runtime';\nimport { createVerticalBarsWebProjection } from './vertical-bars-web';\nimport { prehideVerticalBarsToolbarSources } from './prehide';\nimport { observeVerticalBarsModals } from './shared/modal';\nexport type {\n NativeUIShellComponent,\n NativeUIShellControls,\n NativeUIShellHandle,\n NativeUIShellOptions,\n NativeUIShellStatus,\n NativeUIShellSuspension,\n VerticalBarEdge,\n VerticalBarPlacement,\n VerticalControlAreaHandle,\n VerticalControlAreaOptions,\n WebViewMetrics,\n} from './definitions';\n\nconst plugin = registerPlugin<NativeUIShellPlugin>('IonicNativeUIShell');\nexport const IonicNativeUIShell = plugin;\nlet active: Promise<NativeUIShellHandle> | undefined;\nlet activeConfiguration: string | undefined;\nconst web = (reason: string): NativeUIShellHandle => ({\n getStatus: () => ({ state: 'web', projected: 0, updates: 0, reason }),\n suspend: async () => ({ resume: async () => {} }),\n destroy: async () => {},\n});\n\nconst combine = (native: NativeUIShellHandle, fallback: NativeUIShellHandle): NativeUIShellHandle => ({\n getStatus() {\n const nativeStatus = native.getStatus();\n const fallbackStatus = fallback.getStatus();\n return {\n ...nativeStatus,\n state:\n nativeStatus.state === 'stopped' && fallbackStatus.state === 'stopped' ? 'stopped' : nativeStatus.projected > 0 ? 'native' : 'web',\n projected: nativeStatus.projected + fallbackStatus.projected,\n updates: nativeStatus.updates + fallbackStatus.updates,\n };\n },\n async suspend() {\n const [nativeLease, fallbackLease] = await Promise.all([native.suspend(), fallback.suspend()]);\n return { resume: async () => void (await Promise.all([nativeLease.resume(), fallbackLease.resume()])) };\n },\n async destroy() {\n await Promise.all([native.destroy(), fallback.destroy()]);\n },\n});\n\n/** Wraps a runtime handle with the shared enable-lifecycle: reason, suspend hooks and idempotent destroy. */\nconst manage = (\n handle: NativeUIShellHandle,\n lifecycle: {\n reason?: string;\n suspend?: () => (() => void) | undefined;\n /** Runs after teardown; clears the shared slot only while this activation owns it. */\n release?: () => void;\n destroy?: () => void | Promise<void>;\n } = {},\n): NativeUIShellHandle => {\n let destroyed = false;\n return {\n getStatus: () => (lifecycle.reason ? { ...handle.getStatus(), reason: lifecycle.reason } : handle.getStatus()),\n async suspend() {\n const lease = await handle.suspend();\n const resume = lifecycle.suspend?.();\n return {\n async resume() {\n await lease.resume();\n resume?.();\n },\n };\n },\n async destroy() {\n if (destroyed) return;\n destroyed = true;\n try {\n await handle.destroy();\n } finally {\n await lifecycle.destroy?.();\n lifecycle.release?.();\n }\n },\n };\n};\n\n/** Reads the current native WebView geometry and applies it to page transitions. */\nexport const configureNativeTransition = async (): Promise<WebViewMetrics> => {\n const metrics = typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' ? await plugin.getWebViewMetrics() : { radius: 0 };\n setConfig({ radius: metrics.radius });\n return metrics;\n};\n\n/** Resolves a logical vertical-bar edge to the physical side for the given direction. */\nconst physicalVerticalBarEdge = (edge: Exclude<VerticalBarEdge, null>, rtl: boolean): 'left' | 'right' =>\n (edge === 'leading') !== rtl ? 'left' : 'right';\n\nconst elementRtl = (element: Element): boolean => element.closest('[dir]')?.getAttribute('dir') === 'rtl';\n\n// Device facts are supplied by the application; the theme only compares placement.\nconst nativePlacements = new WeakMap<HTMLElement, { edge: VerticalBarEdge; rtl?: boolean }>();\nconst horizontalFallbackAttribute = 'data-native-ui-shell-vertical-bars-suspended';\n\n// Keep the requested rail while ordinary Native UI Shell temporarily uses its\n// horizontal layout. Removing the effective class restores normal measurements,\n// toolbar ownership and safe areas throughout the existing rendering pipeline.\nconst observeNativeVerticalBarsLayout = (doc: Document): (() => void) => {\n const reconcile = () => {\n const app = doc.querySelector<HTMLElement>('ion-app');\n if (!app) return;\n const requested = app.classList.contains('ios-theme-vertical-bars') || app.hasAttribute(horizontalFallbackAttribute);\n const suspended = requested && nativePlacements.get(app)?.edge == null;\n if (app.hasAttribute(horizontalFallbackAttribute) !== suspended) app.toggleAttribute(horizontalFallbackAttribute, suspended);\n if (app.classList.contains('ios-theme-vertical-bars') !== (requested && !suspended)) {\n app.classList.toggle('ios-theme-vertical-bars', requested && !suspended);\n }\n };\n const observer = new MutationObserver(reconcile);\n observer.observe(doc.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });\n doc.defaultView?.addEventListener('nativeUIShellRefresh', reconcile);\n reconcile();\n return () => {\n observer.disconnect();\n doc.defaultView?.removeEventListener('nativeUIShellRefresh', reconcile);\n const app = doc.querySelector<HTMLElement>(`ion-app[${horizontalFallbackAttribute}]`);\n if (app) {\n app.removeAttribute(horizontalFallbackAttribute);\n app.classList.add('ios-theme-vertical-bars');\n }\n };\n};\n\n/**\n * Applies one placement to the CSS layout and both Web/native projections.\n * Pass `rtl` when the document direction is known; otherwise the nearest `dir` attribute is used.\n */\nexport const setVerticalControlAreaPlacement = (placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean): void => {\n if (typeof document === 'undefined') return;\n const app = document.querySelector<HTMLElement>('ion-app');\n if (!app) throw new Error('Vertical Control Area requires ion-app');\n const { edge, inset = 0 } = placement && typeof placement === 'object' ? placement : { edge: placement, inset: 0 };\n if (placement && typeof placement === 'object' && placement.nativeEdge !== undefined) {\n nativePlacements.set(app, { edge: placement.nativeEdge, rtl });\n }\n app.removeAttribute(horizontalFallbackAttribute);\n app.classList.toggle('ios-theme-vertical-bars', edge !== null);\n app.classList.toggle('ios-theme-vertical-bars-left', edge !== null && physicalVerticalBarEdge(edge, rtl ?? elementRtl(app)) === 'left');\n if (edge && Number.isFinite(inset) && inset > 0) app.style.setProperty('--ios-theme-vertical-bars-native-inset', `${inset}px`);\n else app.style.removeProperty('--ios-theme-vertical-bars-native-inset');\n document.defaultView?.dispatchEvent(new Event('nativeUIShellRefresh'));\n};\n\n/**\n * Call once after ion-app is mounted. Ionic markup remains the source of truth.\n * Native vertical buttons default to SwiftUI appearance.\n * See VerticalControlAreaOptions for source styling and local overrides.\n */\nexport const enableVerticalControlArea = async (options: VerticalControlAreaOptions = {}): Promise<VerticalControlAreaHandle> => {\n const handle = await enableNativeUIShell({\n buttonProjection: options.buttonProjection,\n buttonDefaultFill: options.buttonDefaultFill,\n controls: { tabs: true, toolbar: true },\n verticalBarsOnly: true,\n });\n return {\n getStatus: () => handle.getStatus(),\n suspend: () => handle.suspend(),\n destroy: () => handle.destroy(),\n setPlacement: setVerticalControlAreaPlacement,\n };\n};\n\n/** Call once at application startup. Ionic markup remains the source of truth. */\nexport const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise<NativeUIShellHandle> => {\n if (options.enabled === false) {\n const current = active;\n active = undefined;\n activeConfiguration = undefined;\n return current\n ? current.then(async (handle) => {\n await handle.destroy();\n return web('Disabled');\n })\n : Promise.resolve(web('Disabled'));\n }\n if (typeof document === 'undefined') return Promise.resolve(web('Requires a document'));\n const controls = options.controls;\n const configuration = JSON.stringify([\n options.verticalBarsOnly === true,\n options.buttonProjection ?? 'system',\n // Local source overrides can use this even when the startup projection is system.\n options.buttonDefaultFill ?? null,\n ...(['tabs', 'toolbar', 'segment', 'fab'] as const).map((component) => !controls || controls[component] === true),\n ]);\n if (active && activeConfiguration !== configuration)\n return Promise.reject(\n new Error('Native UI Shell is already running with different controls; destroy it before changing configuration.'),\n );\n if (active) return active;\n activeConfiguration = configuration;\n const stopModals = observeVerticalBarsModals(document);\n const prehide =\n options.controls === undefined || options.controls.toolbar === true ? prehideVerticalBarsToolbarSources(document) : undefined;\n // A slow destroy of a superseded activation must not release a newer one.\n let start: Promise<NativeUIShellHandle>;\n const release = () => {\n if (active === start) {\n active = undefined;\n activeConfiguration = undefined;\n }\n };\n const fallback = (reason: string) =>\n manage(createVerticalBarsWebProjection(document, options), {\n reason,\n suspend: () => prehide?.suspend(),\n destroy: () => {\n prehide?.stop();\n stopModals();\n },\n release,\n });\n return (start = active =\n (async () => {\n if (Capacitor.getPlatform() !== 'ios') return fallback('Requires Capacitor iOS');\n let runtime: NativeUIShellHandle | undefined;\n let metricsListener: Awaited<ReturnType<typeof plugin.addListener>> | undefined;\n let stopVerticalBarsLayout: (() => void) | undefined;\n try {\n const capabilities = await plugin.configure({ verticalBarsOnly: options.verticalBarsOnly === true });\n if (!capabilities.supported) return fallback('Requires iOS 26 or later');\n if (!options.verticalBarsOnly) stopVerticalBarsLayout = observeNativeVerticalBarsLayout(document);\n // The application owns device state and selects the rail through placement classes.\n const nativeVerticalBars = () => {\n const app = document.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');\n if (!app) return false;\n const reported = nativePlacements.get(app);\n if (!reported?.edge) return false;\n const physicalEdge = app.classList.contains('ios-theme-vertical-bars-left') ? 'left' : 'right';\n return physicalEdge === physicalVerticalBarEdge(reported.edge, reported.rtl ?? elementRtl(app));\n };\n if (!options.verticalBarsOnly) {\n metricsListener = await plugin.addListener('webViewMetricsChange', (metrics) => {\n setConfig({ radius: metrics.radius });\n });\n await configureNativeTransition().catch(() => undefined);\n }\n const native = await createRuntime(document, plugin, options, nativeVerticalBars, options.verticalBarsOnly === true);\n runtime = combine(\n native,\n createVerticalBarsWebProjection(document, options, () => !nativeVerticalBars() || native.getStatus().state === 'stopped'),\n );\n return manage(runtime, {\n suspend: () => prehide?.suspend(),\n release,\n destroy: async () => {\n await metricsListener?.remove().catch(() => {});\n stopVerticalBarsLayout?.();\n prehide?.stop();\n stopModals();\n },\n });\n } catch (error) {\n await runtime?.destroy();\n await metricsListener?.remove().catch(() => {});\n stopVerticalBarsLayout?.();\n return fallback(error instanceof Error ? error.message : String(error));\n }\n })());\n};\n"],"mappings":";;;;;;;AA8BA,MAAM,SAAS,eAAoC,oBAAoB;AACvE,MAAa,qBAAqB;AAClC,IAAI;AACJ,IAAI;AACJ,MAAM,OAAO,YAAyC;CACpD,kBAAkB;EAAE,OAAO;EAAO,WAAW;EAAG,SAAS;EAAG;CAAO;CACnE,SAAS,aAAa,EAAE,QAAQ,YAAY,CAAC,EAAE;CAC/C,SAAS,YAAY,CAAC;AACxB;AAEA,MAAM,WAAW,QAA6B,cAAwD;CACpG,YAAY;EACV,MAAM,eAAe,OAAO,UAAU;EACtC,MAAM,iBAAiB,SAAS,UAAU;EAC1C,OAAO;GACL,GAAG;GACH,OACE,aAAa,UAAU,aAAa,eAAe,UAAU,YAAY,YAAY,aAAa,YAAY,IAAI,WAAW;GAC/H,WAAW,aAAa,YAAY,eAAe;GACnD,SAAS,aAAa,UAAU,eAAe;EACjD;CACF;CACA,MAAM,UAAU;EACd,MAAM,CAAC,aAAa,iBAAiB,MAAM,QAAQ,IAAI,CAAC,OAAO,QAAQ,GAAG,SAAS,QAAQ,CAAC,CAAC;EAC7F,OAAO,EAAE,QAAQ,YAAY,KAAM,MAAM,QAAQ,IAAI,CAAC,YAAY,OAAO,GAAG,cAAc,OAAO,CAAC,CAAC,EAAG;CACxG;CACA,MAAM,UAAU;EACd,MAAM,QAAQ,IAAI,CAAC,OAAO,QAAQ,GAAG,SAAS,QAAQ,CAAC,CAAC;CAC1D;AACF;;AAGA,MAAM,UACJ,QACA,YAMI,CAAC,MACmB;CACxB,IAAI,YAAY;CAChB,OAAO;EACL,iBAAkB,UAAU,SAAS;GAAE,GAAG,OAAO,UAAU;GAAG,QAAQ,UAAU;EAAO,IAAI,OAAO,UAAU;EAC5G,MAAM,UAAU;GACd,MAAM,QAAQ,MAAM,OAAO,QAAQ;GACnC,MAAM,SAAS,UAAU,UAAU;GACnC,OAAO,EACL,MAAM,SAAS;IACb,MAAM,MAAM,OAAO;IACnB,SAAS;GACX,EACF;EACF;EACA,MAAM,UAAU;GACd,IAAI,WAAW;GACf,YAAY;GACZ,IAAI;IACF,MAAM,OAAO,QAAQ;GACvB,UAAU;IACR,MAAM,UAAU,UAAU;IAC1B,UAAU,UAAU;GACtB;EACF;CACF;AACF;;AAGA,MAAa,4BAA4B,YAAqC;CAC5E,MAAM,UAAU,OAAO,aAAa,eAAe,UAAU,YAAY,MAAM,QAAQ,MAAM,OAAO,kBAAkB,IAAI,EAAE,QAAQ,EAAE;CACtI,UAAU,EAAE,QAAQ,QAAQ,OAAO,CAAC;CACpC,OAAO;AACT;;AAGA,MAAM,2BAA2B,MAAsC,QACpE,SAAS,cAAe,MAAM,SAAS;AAE1C,MAAM,cAAc,YAA8B,QAAQ,QAAQ,OAAO,CAAC,EAAE,aAAa,KAAK,MAAM;AAGpG,MAAM,mCAAmB,IAAI,QAA+D;AAC5F,MAAM,8BAA8B;AAKpC,MAAM,mCAAmC,QAAgC;CACvE,MAAM,kBAAkB;EACtB,MAAM,MAAM,IAAI,cAA2B,SAAS;EACpD,IAAI,CAAC,KAAK;EACV,MAAM,YAAY,IAAI,UAAU,SAAS,yBAAyB,KAAK,IAAI,aAAa,2BAA2B;EACnH,MAAM,YAAY,aAAa,iBAAiB,IAAI,GAAG,CAAC,EAAE,QAAQ;EAClE,IAAI,IAAI,aAAa,2BAA2B,MAAM,WAAW,IAAI,gBAAgB,6BAA6B,SAAS;EAC3H,IAAI,IAAI,UAAU,SAAS,yBAAyB,OAAO,aAAa,CAAC,YACvE,IAAI,UAAU,OAAO,2BAA2B,aAAa,CAAC,SAAS;CAE3E;CACA,MAAM,WAAW,IAAI,iBAAiB,SAAS;CAC/C,SAAS,QAAQ,IAAI,iBAAiB;EAAE,WAAW;EAAM,SAAS;EAAM,YAAY;EAAM,iBAAiB,CAAC,OAAO;CAAE,CAAC;CACtH,IAAI,aAAa,iBAAiB,wBAAwB,SAAS;CACnE,UAAU;CACV,aAAa;EACX,SAAS,WAAW;EACpB,IAAI,aAAa,oBAAoB,wBAAwB,SAAS;EACtE,MAAM,MAAM,IAAI,cAA2B,WAAW,4BAA4B,EAAE;EACpF,IAAI,KAAK;GACP,IAAI,gBAAgB,2BAA2B;GAC/C,IAAI,UAAU,IAAI,yBAAyB;EAC7C;CACF;AACF;;;;;AAMA,MAAa,mCAAmC,WAAmD,QAAwB;CACzH,IAAI,OAAO,aAAa,aAAa;CACrC,MAAM,MAAM,SAAS,cAA2B,SAAS;CACzD,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,wCAAwC;CAClE,MAAM,EAAE,MAAM,QAAQ,MAAM,aAAa,OAAO,cAAc,WAAW,YAAY;EAAE,MAAM;EAAW,OAAO;CAAE;CACjH,IAAI,aAAa,OAAO,cAAc,YAAY,UAAU,eAAe,KAAA,GACzE,iBAAiB,IAAI,KAAK;EAAE,MAAM,UAAU;EAAY;CAAI,CAAC;CAE/D,IAAI,gBAAgB,2BAA2B;CAC/C,IAAI,UAAU,OAAO,2BAA2B,SAAS,IAAI;CAC7D,IAAI,UAAU,OAAO,gCAAgC,SAAS,QAAQ,wBAAwB,MAAM,OAAO,WAAW,GAAG,CAAC,MAAM,MAAM;CACtI,IAAI,QAAQ,OAAO,SAAS,KAAK,KAAK,QAAQ,GAAG,IAAI,MAAM,YAAY,0CAA0C,GAAG,MAAM,GAAG;MACxH,IAAI,MAAM,eAAe,wCAAwC;CACtE,SAAS,aAAa,cAAc,IAAI,MAAM,sBAAsB,CAAC;AACvE;;;;;;AAOA,MAAa,4BAA4B,OAAO,UAAsC,CAAC,MAA0C;CAC/H,MAAM,SAAS,MAAM,oBAAoB;EACvC,kBAAkB,QAAQ;EAC1B,mBAAmB,QAAQ;EAC3B,UAAU;GAAE,MAAM;GAAM,SAAS;EAAK;EACtC,kBAAkB;CACpB,CAAC;CACD,OAAO;EACL,iBAAiB,OAAO,UAAU;EAClC,eAAe,OAAO,QAAQ;EAC9B,eAAe,OAAO,QAAQ;EAC9B,cAAc;CAChB;AACF;;AAGA,MAAa,uBAAuB,UAAgC,CAAC,MAAoC;CACvG,IAAI,QAAQ,YAAY,OAAO;EAC7B,MAAM,UAAU;EAChB,SAAS,KAAA;EACT,sBAAsB,KAAA;EACtB,OAAO,UACH,QAAQ,KAAK,OAAO,WAAW;GAC7B,MAAM,OAAO,QAAQ;GACrB,OAAO,IAAI,UAAU;EACvB,CAAC,IACD,QAAQ,QAAQ,IAAI,UAAU,CAAC;CACrC;CACA,IAAI,OAAO,aAAa,aAAa,OAAO,QAAQ,QAAQ,IAAI,qBAAqB,CAAC;CACtF,MAAM,WAAW,QAAQ;CACzB,MAAM,gBAAgB,KAAK,UAAU;EACnC,QAAQ,qBAAqB;EAC7B,QAAQ,oBAAoB;EAE5B,QAAQ,qBAAqB;EAC7B,GAAI;GAAC;GAAQ;GAAW;GAAW;EAAK,CAAC,CAAW,KAAK,cAAc,CAAC,YAAY,SAAS,eAAe,IAAI;CAClH,CAAC;CACD,IAAI,UAAU,wBAAwB,eACpC,OAAO,QAAQ,uBACb,IAAI,MAAM,uGAAuG,CACnH;CACF,IAAI,QAAQ,OAAO;CACnB,sBAAsB;CACtB,MAAM,aAAa,0BAA0B,QAAQ;CACrD,MAAM,UACJ,QAAQ,aAAa,KAAA,KAAa,QAAQ,SAAS,YAAY,OAAO,kCAAkC,QAAQ,IAAI,KAAA;CAEtH,IAAI;CACJ,MAAM,gBAAgB;EACpB,IAAI,WAAW,OAAO;GACpB,SAAS,KAAA;GACT,sBAAsB,KAAA;EACxB;CACF;CACA,MAAM,YAAY,WAChB,OAAO,gCAAgC,UAAU,OAAO,GAAG;EACzD;EACA,eAAe,SAAS,QAAQ;EAChC,eAAe;GACb,SAAS,KAAK;GACd,WAAW;EACb;EACA;CACF,CAAC;CACH,OAAQ,QAAQ,UACb,YAAY;EACX,IAAI,UAAU,YAAY,MAAM,OAAO,OAAO,SAAS,wBAAwB;EAC/E,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;GAEF,IAAI,EAAC,MADsB,OAAO,UAAU,EAAE,kBAAkB,QAAQ,qBAAqB,KAAK,CAAC,EAAA,CACjF,WAAW,OAAO,SAAS,0BAA0B;GACvE,IAAI,CAAC,QAAQ,kBAAkB,yBAAyB,gCAAgC,QAAQ;GAEhG,MAAM,2BAA2B;IAC/B,MAAM,MAAM,SAAS,cAA2B,iCAAiC;IACjF,IAAI,CAAC,KAAK,OAAO;IACjB,MAAM,WAAW,iBAAiB,IAAI,GAAG;IACzC,IAAI,CAAC,UAAU,MAAM,OAAO;IAE5B,QADqB,IAAI,UAAU,SAAS,8BAA8B,IAAI,SAAS,aAC/D,wBAAwB,SAAS,MAAM,SAAS,OAAO,WAAW,GAAG,CAAC;GAChG;GACA,IAAI,CAAC,QAAQ,kBAAkB;IAC7B,kBAAkB,MAAM,OAAO,YAAY,yBAAyB,YAAY;KAC9E,UAAU,EAAE,QAAQ,QAAQ,OAAO,CAAC;IACtC,CAAC;IACD,MAAM,0BAA0B,CAAC,CAAC,YAAY,KAAA,CAAS;GACzD;GACA,MAAM,SAAS,MAAM,cAAc,UAAU,QAAQ,SAAS,oBAAoB,QAAQ,qBAAqB,IAAI;GACnH,UAAU,QACR,QACA,gCAAgC,UAAU,eAAe,CAAC,mBAAmB,KAAK,OAAO,UAAU,CAAC,CAAC,UAAU,SAAS,CAC1H;GACA,OAAO,OAAO,SAAS;IACrB,eAAe,SAAS,QAAQ;IAChC;IACA,SAAS,YAAY;KACnB,MAAM,iBAAiB,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;KAC9C,yBAAyB;KACzB,SAAS,KAAK;KACd,WAAW;IACb;GACF,CAAC;EACH,SAAS,OAAO;GACd,MAAM,SAAS,QAAQ;GACvB,MAAM,iBAAiB,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;GAC9C,yBAAyB;GACzB,OAAO,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;EACxE;CACF,EAAA,CAAG;AACP"}
@@ -2,6 +2,7 @@ import { ShellControl, ShellItem, VerticalControlAreaOptions } from "../definiti
2
2
  import { NativeUIShellComponent } from "../components/index.js";
3
3
  //#region src/native/shared/candidate.d.ts
4
4
  export interface Candidate {
5
+ buttonProjection?: VerticalControlAreaOptions['buttonProjection'];
5
6
  buttonDefaultFill?: VerticalControlAreaOptions['buttonDefaultFill'];
6
7
  element: HTMLElement;
7
8
  sources?: HTMLElement[];
@@ -1 +1 @@
1
- {"version":3,"file":"candidate.d.ts","names":[],"sources":["../../../src/native/shared/candidate.ts"],"mappings":";;;iBAIiB;EACf,oBAAoB;EACpB,SAAS;EACT,UAAU;EACV,SAAS;EACT,SAAS,YAAY;EACrB;IAAS,MAAM;IAAW;IAAgB;;;YAGhC,YAAY,SAAS;qBAGpB,kBACX,SAAS,aACT,MAAM,wBACN,IAAI,UACJ,UAAS,+BACR;qBAoBU,aACX,WAAW,WACX,OAAO,aACP,IAAI,UACJ,UAAS,YACT,mBACC"}
1
+ {"version":3,"file":"candidate.d.ts","names":[],"sources":["../../../src/native/shared/candidate.ts"],"mappings":";;;iBAIiB;EACf,mBAAmB;EACnB,oBAAoB;EACpB,SAAS;EACT,UAAU;EACV,SAAS;EACT,SAAS,YAAY;EACrB;IAAS,MAAM;IAAW;IAAgB;;;YAGhC,YAAY,SAAS;qBAGpB,kBACX,SAAS,aACT,MAAM,wBACN,IAAI,UACJ,UAAS,+BACR;qBAqBU,aACX,WAAW,WACX,OAAO,aACP,IAAI,UACJ,UAAS,YACT,mBACC"}
@@ -6,6 +6,7 @@ const createCandidate = (element, kind, id, options = {}) => {
6
6
  const style = getComputedStyle(element);
7
7
  return {
8
8
  element,
9
+ buttonProjection: options.buttonProjection,
9
10
  buttonDefaultFill: options.buttonDefaultFill,
10
11
  control: {
11
12
  id: id(element),
@@ -32,8 +33,12 @@ const appendItem = (candidate, child, id, content = child, label = text(child))
32
33
  const badge = child.querySelector("ion-badge");
33
34
  const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : void 0;
34
35
  const sourceFill = child.fill;
35
- const fill = !sourceFill || sourceFill === "default" ? candidate.buttonDefaultFill : sourceFill;
36
- const outline = isVerticalBarsSource(child) && child.matches("ion-button") && fill === "outline" ? getComputedStyle(native ?? child) : void 0;
36
+ const projectionOwner = [child, child.closest("ion-buttons")].find((element) => element?.matches(".ios-theme-projection-source, .ios-theme-projection-system"));
37
+ const systemButton = (projectionOwner ? projectionOwner.classList.contains("ios-theme-projection-system") ? "system" : "source" : candidate.buttonProjection) !== "source" && isVerticalBarsSource(child) && child.matches("ion-button, ion-menu-button");
38
+ const defaultFill = child.closest("ion-buttons") ? "clear" : candidate.buttonDefaultFill;
39
+ const fill = !sourceFill || sourceFill === "default" ? defaultFill : sourceFill;
40
+ const buttonFill = !systemButton && isVerticalBarsSource(child) && child.matches("ion-button") && (fill === "clear" || fill === "solid" || fill === "outline") ? fill : void 0;
41
+ const outline = buttonFill === "outline" ? getComputedStyle(native ?? child) : void 0;
37
42
  const item = {
38
43
  id: id(child),
39
44
  ...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),
@@ -43,8 +48,10 @@ const appendItem = (candidate, child, id, content = child, label = text(child))
43
48
  selected: !!child.selected,
44
49
  fontSize: parseFloat(labelStyle.fontSize),
45
50
  fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,
46
- color: getComputedStyle(native ?? child).color,
47
- backgroundColor: isVerticalBarsSource(child) && child.matches("ion-button") && fill === "solid" ? getComputedStyle(native ?? child).backgroundColor : void 0,
51
+ color: systemButton ? "currentColor" : getComputedStyle(native ?? child).color,
52
+ iconTemplate: systemButton ? true : void 0,
53
+ buttonFill,
54
+ backgroundColor: buttonFill === "solid" ? getComputedStyle(native ?? child).backgroundColor : void 0,
48
55
  borderColor: outline?.borderTopColor,
49
56
  borderWidth: outline ? parseFloat(outline.borderTopWidth) : void 0,
50
57
  badge: badgeStyle ? {
@@ -1 +1 @@
1
- {"version":3,"file":"candidate.js","names":[],"sources":["../../../src/native/shared/candidate.ts"],"sourcesContent":["import type { ShellControl, ShellItem, NativeUIShellComponent, VerticalControlAreaOptions } from '../definitions';\nimport { frame, isDark, isVerticalBarsSource, text, visible } from './dom';\nimport { iconSource } from './icons';\n\nexport interface Candidate {\n buttonDefaultFill?: VerticalControlAreaOptions['buttonDefaultFill'];\n element: HTMLElement;\n sources?: HTMLElement[];\n control: ShellControl;\n actions: Map<string, HTMLElement>;\n icons: { item: ShellItem; source: string; field?: 'closeIcon' }[];\n}\n\nexport type Identify = (element: HTMLElement) => string;\ntype ItemElement = HTMLElement & { disabled?: boolean; selected?: boolean };\n\nexport const createCandidate = (\n element: HTMLElement,\n kind: NativeUIShellComponent,\n id: Identify,\n options: VerticalControlAreaOptions = {},\n): Candidate => {\n const rect = element.getBoundingClientRect();\n const style = getComputedStyle(element);\n return {\n element,\n buttonDefaultFill: options.buttonDefaultFill,\n control: {\n id: id(element),\n kind,\n ...frame(rect),\n items: [],\n dark: isDark(style),\n rtl: style.direction === 'rtl',\n },\n actions: new Map(),\n icons: [],\n };\n};\n\n// Shared text, artwork, geometry and action data; component rules stay with their readers.\nexport const appendItem = (\n candidate: Candidate,\n child: HTMLElement,\n id: Identify,\n content: ParentNode = child,\n label = text(child),\n): ShellItem | undefined => {\n if (\n !visible(child, isVerticalBarsSource(candidate.element)) ||\n child.querySelector('input, button, a, img, canvas, video, ion-spinner, ion-avatar')\n )\n return;\n const icons = Array.from(content.querySelectorAll<HTMLElement>('ion-icon'));\n const directSVGs = Array.from(content.querySelectorAll<SVGElement>('svg'));\n if (icons.length + directSVGs.length > 1) return undefined;\n const svg = directSVGs[0] ?? icons[0]?.shadowRoot?.querySelector<SVGElement>('svg');\n if (icons.length && !svg) return undefined; // ion-icon has not finished loading.\n const labelElement = content.querySelector('[part=\"text\"], ion-label') ?? child;\n const native = child.shadowRoot?.querySelector('[part=\"native\"]');\n const labelStyle = getComputedStyle(labelElement);\n const badge = child.querySelector<HTMLElement>('ion-badge');\n const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : undefined;\n const sourceFill = (child as HTMLIonButtonElement).fill;\n const fill = !sourceFill || sourceFill === 'default' ? candidate.buttonDefaultFill : sourceFill;\n const outline =\n isVerticalBarsSource(child) && child.matches('ion-button') && fill === 'outline' ? getComputedStyle(native ?? child) : undefined;\n const item: ShellItem = {\n id: id(child),\n ...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),\n label,\n accessibilityLabel:\n child.getAttribute('aria-label') ??\n native?.getAttribute('aria-label') ??\n (icons[0]?.getAttribute('aria-hidden') === 'true' ? null : icons[0]?.getAttribute('aria-label')) ??\n label,\n disabled:\n !!(child as ItemElement).disabled ||\n !!(candidate.element as ItemElement).disabled ||\n getComputedStyle(child).pointerEvents === 'none',\n selected: !!(child as ItemElement).selected,\n fontSize: parseFloat(labelStyle.fontSize),\n fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,\n color: getComputedStyle(native ?? child).color,\n backgroundColor:\n isVerticalBarsSource(child) && child.matches('ion-button') && fill === 'solid'\n ? getComputedStyle(native ?? child).backgroundColor\n : undefined,\n borderColor: outline?.borderTopColor,\n borderWidth: outline ? parseFloat(outline.borderTopWidth) : undefined,\n badge: badgeStyle\n ? { value: badge!.textContent?.trim() ?? '', color: badgeStyle.backgroundColor, textColor: badgeStyle.color }\n : undefined,\n };\n if (svg) {\n const source = iconSource(svg);\n if (!source) return undefined;\n const size = (icons[0] ?? svg).getBoundingClientRect();\n item.iconWidth = size.width;\n item.iconHeight = size.height;\n if (!size.width || !size.height) return undefined;\n item.iconPosition = (icons[0] ?? svg).getAttribute('slot') === 'end' ? 'trailing' : 'leading';\n candidate.icons.push({ item, source });\n }\n if (native && child.matches('ion-button, ion-back-button')) {\n const style = getComputedStyle(native);\n const icon = icons[0] ?? svg;\n const iconStyle = icon && getComputedStyle(icon);\n const leadingMargin = parseFloat(iconStyle?.marginInlineStart ?? '') || 0;\n const trailingMargin = parseFloat(iconStyle?.marginInlineEnd ?? '') || 0;\n const trailing = item.iconPosition === 'trailing';\n const inner = native.querySelector('.button-inner');\n const gap = inner ? parseFloat(getComputedStyle(inner).columnGap) || 0 : 0;\n // UIKit centers the title/image pair using its insets. Include Ionic's\n // border and negative outer icon margin instead of applying a fixed offset.\n item.contentInsetLeading =\n (parseFloat(style.paddingInlineStart) || 0) +\n (parseFloat(style.borderInlineStartWidth) || 0) +\n (!trailing || !label ? leadingMargin : 0);\n item.contentInsetTrailing =\n (parseFloat(style.paddingInlineEnd) || 0) + (parseFloat(style.borderInlineEndWidth) || 0) + (trailing || !label ? trailingMargin : 0);\n item.imagePadding = label && icon ? gap + (trailing ? leadingMargin : trailingMargin) : 0;\n }\n if (!label && !svg) return undefined;\n candidate.control.items.push(item);\n candidate.actions.set(item.id, child);\n return item;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"candidate.js","names":[],"sources":["../../../src/native/shared/candidate.ts"],"sourcesContent":["import type { ShellControl, ShellItem, NativeUIShellComponent, VerticalControlAreaOptions } from '../definitions';\nimport { frame, isDark, isVerticalBarsSource, text, visible } from './dom';\nimport { iconSource } from './icons';\n\nexport interface Candidate {\n buttonProjection?: VerticalControlAreaOptions['buttonProjection'];\n buttonDefaultFill?: VerticalControlAreaOptions['buttonDefaultFill'];\n element: HTMLElement;\n sources?: HTMLElement[];\n control: ShellControl;\n actions: Map<string, HTMLElement>;\n icons: { item: ShellItem; source: string; field?: 'closeIcon' }[];\n}\n\nexport type Identify = (element: HTMLElement) => string;\ntype ItemElement = HTMLElement & { disabled?: boolean; selected?: boolean };\n\nexport const createCandidate = (\n element: HTMLElement,\n kind: NativeUIShellComponent,\n id: Identify,\n options: VerticalControlAreaOptions = {},\n): Candidate => {\n const rect = element.getBoundingClientRect();\n const style = getComputedStyle(element);\n return {\n element,\n buttonProjection: options.buttonProjection,\n buttonDefaultFill: options.buttonDefaultFill,\n control: {\n id: id(element),\n kind,\n ...frame(rect),\n items: [],\n dark: isDark(style),\n rtl: style.direction === 'rtl',\n },\n actions: new Map(),\n icons: [],\n };\n};\n\n// Shared text, artwork, geometry and action data; component rules stay with their readers.\nexport const appendItem = (\n candidate: Candidate,\n child: HTMLElement,\n id: Identify,\n content: ParentNode = child,\n label = text(child),\n): ShellItem | undefined => {\n if (\n !visible(child, isVerticalBarsSource(candidate.element)) ||\n child.querySelector('input, button, a, img, canvas, video, ion-spinner, ion-avatar')\n )\n return;\n const icons = Array.from(content.querySelectorAll<HTMLElement>('ion-icon'));\n const directSVGs = Array.from(content.querySelectorAll<SVGElement>('svg'));\n if (icons.length + directSVGs.length > 1) return undefined;\n const svg = directSVGs[0] ?? icons[0]?.shadowRoot?.querySelector<SVGElement>('svg');\n if (icons.length && !svg) return undefined; // ion-icon has not finished loading.\n const labelElement = content.querySelector('[part=\"text\"], ion-label') ?? child;\n const native = child.shadowRoot?.querySelector('[part=\"native\"]');\n const labelStyle = getComputedStyle(labelElement);\n const badge = child.querySelector<HTMLElement>('ion-badge');\n const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : undefined;\n const sourceFill = (child as HTMLIonButtonElement).fill;\n const projectionOwner = [child, child.closest('ion-buttons')].find((element) =>\n element?.matches('.ios-theme-projection-source, .ios-theme-projection-system'),\n );\n const projection = projectionOwner\n ? projectionOwner.classList.contains('ios-theme-projection-system')\n ? 'system'\n : 'source'\n : candidate.buttonProjection;\n const systemButton = projection !== 'source' && isVerticalBarsSource(child) && child.matches('ion-button, ion-menu-button');\n // Ionic defaults buttons inside ion-buttons to clear, even when the app defaults to solid.\n const defaultFill = child.closest('ion-buttons') ? 'clear' : candidate.buttonDefaultFill;\n const fill = !sourceFill || sourceFill === 'default' ? defaultFill : sourceFill;\n const buttonFill =\n !systemButton &&\n isVerticalBarsSource(child) &&\n child.matches('ion-button') &&\n (fill === 'clear' || fill === 'solid' || fill === 'outline')\n ? fill\n : undefined;\n const outline = buttonFill === 'outline' ? getComputedStyle(native ?? child) : undefined;\n const item: ShellItem = {\n id: id(child),\n ...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),\n label,\n accessibilityLabel:\n child.getAttribute('aria-label') ??\n native?.getAttribute('aria-label') ??\n (icons[0]?.getAttribute('aria-hidden') === 'true' ? null : icons[0]?.getAttribute('aria-label')) ??\n label,\n disabled:\n !!(child as ItemElement).disabled ||\n !!(candidate.element as ItemElement).disabled ||\n getComputedStyle(child).pointerEvents === 'none',\n selected: !!(child as ItemElement).selected,\n fontSize: parseFloat(labelStyle.fontSize),\n fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,\n color: systemButton ? 'currentColor' : getComputedStyle(native ?? child).color,\n iconTemplate: systemButton ? true : undefined,\n buttonFill,\n backgroundColor: buttonFill === 'solid' ? getComputedStyle(native ?? child).backgroundColor : undefined,\n borderColor: outline?.borderTopColor,\n borderWidth: outline ? parseFloat(outline.borderTopWidth) : undefined,\n badge: badgeStyle\n ? { value: badge!.textContent?.trim() ?? '', color: badgeStyle.backgroundColor, textColor: badgeStyle.color }\n : undefined,\n };\n if (svg) {\n const source = iconSource(svg);\n if (!source) return undefined;\n const size = (icons[0] ?? svg).getBoundingClientRect();\n item.iconWidth = size.width;\n item.iconHeight = size.height;\n if (!size.width || !size.height) return undefined;\n item.iconPosition = (icons[0] ?? svg).getAttribute('slot') === 'end' ? 'trailing' : 'leading';\n candidate.icons.push({ item, source });\n }\n if (native && child.matches('ion-button, ion-back-button')) {\n const style = getComputedStyle(native);\n const icon = icons[0] ?? svg;\n const iconStyle = icon && getComputedStyle(icon);\n const leadingMargin = parseFloat(iconStyle?.marginInlineStart ?? '') || 0;\n const trailingMargin = parseFloat(iconStyle?.marginInlineEnd ?? '') || 0;\n const trailing = item.iconPosition === 'trailing';\n const inner = native.querySelector('.button-inner');\n const gap = inner ? parseFloat(getComputedStyle(inner).columnGap) || 0 : 0;\n // UIKit centers the title/image pair using its insets. Include Ionic's\n // border and negative outer icon margin instead of applying a fixed offset.\n item.contentInsetLeading =\n (parseFloat(style.paddingInlineStart) || 0) +\n (parseFloat(style.borderInlineStartWidth) || 0) +\n (!trailing || !label ? leadingMargin : 0);\n item.contentInsetTrailing =\n (parseFloat(style.paddingInlineEnd) || 0) + (parseFloat(style.borderInlineEndWidth) || 0) + (trailing || !label ? trailingMargin : 0);\n item.imagePadding = label && icon ? gap + (trailing ? leadingMargin : trailingMargin) : 0;\n }\n if (!label && !svg) return undefined;\n candidate.control.items.push(item);\n candidate.actions.set(item.id, child);\n return item;\n};\n"],"mappings":"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"}
@@ -106,27 +106,31 @@ import { enableVerticalControlArea } from '@rdlabo/ionic-theme-ios27/vertical-ba
106
106
  const rail = await enableVerticalControlArea();
107
107
  ```
108
108
 
109
- If your existing theme gives buttons a CSS background without an explicit `fill="solid"`, opt into background projection:
110
-
111
- ```ts
112
- const rail = await enableVerticalControlArea({ buttonDefaultFill: 'solid' });
113
- ```
114
-
115
- `buttonDefaultFill` accepts `'solid'` (Ionic's default design) or `null` (the iOS theme's glass design). Omitting it is equivalent to `null`. It applies only to native vertical button projection when Ionic's `fill` is omitted or `default`. Explicit `clear`, `solid`, and `outline` values take precedence. Solid projection reads the existing computed foreground and background colors; outline projection reads the computed border. Source buttons and Web clones are unchanged. Omitting the option preserves the existing behavior. Native Liquid Glass tinting may differ visually from the CSS background, especially for translucent colors.
116
-
117
109
  **What you should see:** your existing tab bar moves to the side, and fixed-toolbar buttons with an `ion-icon` or SVG using `slot="icon-only"` appear there too. Content keeps its existing theme and leaves room for the controls. The Web tab rail displays icons; pressing and dragging reveals tab labels.
118
110
 
119
111
  Use your existing Ionic click handlers, routing, and form associations. All button fills (`default`, `clear`, `solid`, and `outline`) use the same `icon-only` rule, including submit buttons. Actions without that slot remain horizontal. Add `.ios-theme-horizontal-only` to an `ion-buttons` group or individual `ion-button` to keep an action in the horizontal toolbar.
120
112
 
121
113
  When the application owner is disposed, call `await rail.destroy()` to restore the original controls and release the runtime. If you already use `enableNativeUIShell()`, keep that runtime and follow the [shared placement guide](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#project-controls-into-the-rail).
122
114
 
115
+ ### Optional: choose native button appearance (unreleased)
116
+
117
+ The next release adds `buttonProjection` and local projection classes. These options are not available in the `1.2.0-1` installation above. See [Choose button appearance](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#choose-button-appearance) for availability and migration details.
118
+
119
+ The new default is `system`: SwiftUI styles vertical buttons and tints their icons. If your existing theme should supply their fill and colors, use:
120
+
121
+ ```ts
122
+ const rail = await enableVerticalControlArea({ buttonProjection: 'source', buttonDefaultFill: 'solid' });
123
+ ```
124
+
125
+ The `solid` default suits ordinary Ionic buttons. Buttons inside `ion-buttons` still default to clear; set `fill="solid"` explicitly to project their background. For one-off exceptions, use the [local projection classes](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#override-individual-buttons-or-groups). These settings affect native vertical buttons only; Web clones keep their existing appearance.
126
+
123
127
  ### If the preview does not appear
124
128
 
125
129
  | What you see | What to check |
126
130
  | --- | --- |
127
131
  | No space at the side | Load `vertical-bars.css` and put the class on `ion-app`. |
128
132
  | Space appears, but controls stay horizontal | Start `enableVerticalControlArea()` after mounting the app root. Use existing tabs or `slot="icon-only"` actions in a fixed header/footer toolbar. |
129
- | One action stays horizontal | Check for `slot="icon-only"` on the icon and a fixed toolbar outside scrolling content. Explicitly excluded controls and controls in centered modals stay horizontal; `fill` and `type="submit"` do not prevent movement. See [control requirements](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#toolbar-actions). |
133
+ | One action stays horizontal | Check for `slot="icon-only"` on the icon and a fixed toolbar outside scrolling content. Explicitly excluded controls and controls in centered modals stay horizontal; `fill` and `type="submit"` do not prevent movement. See [control requirements](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions). |
130
134
 
131
135
  ## Connect an iPhone Duo
132
136
 
@@ -141,9 +145,9 @@ Use Capacitor 8.5 or later and build with Xcode 27.1 or newer for actual rail pl
141
145
 
142
146
  Replace the browser-only startup with the [device placement setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#project-controls-into-the-rail). That setup sends initial values and `barPlacementChange` events to `setVerticalControlAreaPlacement({ edge: verticalBarEdge, nativeEdge: verticalBarEdge, inset })`, with subscriptions and cleanup kept in your application.
143
147
 
144
- Pass both the requested and native edge with Foldable's measured inset. The placement API resolves RTL. A null edge restores the ordinary layout. Keep `buttonDefaultFill: 'solid'` on `enableVerticalControlArea()` if your existing theme needs CSS background projection.
148
+ Pass both the requested and native edge with Foldable's measured inset. The placement API resolves RTL. A null edge restores the ordinary layout.
145
149
 
146
- On supported iOS, controls in the rail use the system SwiftUI appearance; your custom Web styling still applies to ordinary content and horizontal controls. Web and Android use Web clones.
150
+ On supported iOS, controls in the rail use native SwiftUI rendering; your custom Web styling still applies to ordinary content and horizontal controls. Web and Android use Web clones.
147
151
 
148
152
  ## Use hinge posture without projecting controls
149
153
 
@@ -153,4 +157,4 @@ See [Read the device layout](https://docs.rdlabo.dev/projects/ionic-theme-ios27/
153
157
 
154
158
  ## Shared layout rules and API
155
159
 
156
- Safe-area handling, overlays, RTL, control eligibility, Web simulation, and the handle API are documented in [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo). Those rules apply to this standalone setup too.
160
+ Safe-area handling, overlays, RTL, control eligibility, Web simulation, and the handle API are documented in [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars). Those rules apply to this standalone setup too.