@rdlabo/ionic-theme-ios27 1.2.0-beta.pr242.sha99c4a2ec4d6a → 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.
- package/README.md +2 -1
- package/dist/native/components/ion-buttons.js +1 -1
- package/dist/native/components/ion-buttons.js.map +1 -1
- package/dist/native/components/ion-menu-button.d.ts +2 -1
- package/dist/native/components/ion-menu-button.d.ts.map +1 -1
- package/dist/native/components/ion-menu-button.js +2 -2
- package/dist/native/components/ion-menu-button.js.map +1 -1
- package/dist/native/definitions.d.ts +1 -0
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +4 -3
- package/dist/native/index.js.map +1 -1
- package/dist/native/shared/candidate.d.ts +1 -0
- package/dist/native/shared/candidate.d.ts.map +1 -1
- package/dist/native/shared/candidate.js +6 -2
- package/dist/native/shared/candidate.js.map +1 -1
- package/docs/iphone-duo-with-original-theme.md +16 -12
- package/docs/iphone-duo.md +8 -155
- package/docs/native-ui-shell.md +5 -5
- package/docs/special-markup.md +2 -0
- package/docs/vertical-bars.md +276 -0
- package/package.json +2 -2
- package/src/native/components/ion-buttons.ts +1 -1
- package/src/native/components/ion-menu-button.ts +3 -2
- package/src/native/definitions.ts +13 -6
- package/src/native/index.ts +5 -3
- package/src/native/shared/candidate.ts +17 -2
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
|
|
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,
|
|
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":"
|
|
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":";;;;;;;;;;;
|
|
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 {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.d.ts","names":[],"sources":["../../src/native/definitions.ts"],"mappings":";;;iBAKiB;EACf;EACA;EACA;EACA;;iBAGe;EAQf;;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
|
+
{"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;
|
|
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"}
|
package/dist/native/index.js
CHANGED
|
@@ -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
|
-
*
|
|
134
|
-
*
|
|
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",
|
package/dist/native/index.js.map
CHANGED
|
@@ -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`.\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":"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"}
|
|
@@ -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;
|
|
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,9 +33,11 @@ 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;
|
|
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");
|
|
35
38
|
const defaultFill = child.closest("ion-buttons") ? "clear" : candidate.buttonDefaultFill;
|
|
36
39
|
const fill = !sourceFill || sourceFill === "default" ? defaultFill : sourceFill;
|
|
37
|
-
const buttonFill = isVerticalBarsSource(child) && child.matches("ion-button") && (fill === "clear" || fill === "solid" || fill === "outline") ? fill : void 0;
|
|
40
|
+
const buttonFill = !systemButton && isVerticalBarsSource(child) && child.matches("ion-button") && (fill === "clear" || fill === "solid" || fill === "outline") ? fill : void 0;
|
|
38
41
|
const outline = buttonFill === "outline" ? getComputedStyle(native ?? child) : void 0;
|
|
39
42
|
const item = {
|
|
40
43
|
id: id(child),
|
|
@@ -45,7 +48,8 @@ const appendItem = (candidate, child, id, content = child, label = text(child))
|
|
|
45
48
|
selected: !!child.selected,
|
|
46
49
|
fontSize: parseFloat(labelStyle.fontSize),
|
|
47
50
|
fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,
|
|
48
|
-
color: getComputedStyle(native ?? child).color,
|
|
51
|
+
color: systemButton ? "currentColor" : getComputedStyle(native ?? child).color,
|
|
52
|
+
iconTemplate: systemButton ? true : void 0,
|
|
49
53
|
buttonFill,
|
|
50
54
|
backgroundColor: buttonFill === "solid" ? getComputedStyle(native ?? child).backgroundColor : void 0,
|
|
51
55
|
borderColor: outline?.borderTopColor,
|
|
@@ -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 // 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 isVerticalBarsSource(child)
|
|
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`. Buttons inside `ion-buttons` default to `clear`, even when `buttonDefaultFill` is `solid`. Clear actions have no native glass background. To project a button background, explicitly set `fill="solid"`; CSS backgrounds on clear buttons are not projected. 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. 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/
|
|
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.
|
|
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
|
|
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 [
|
|
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.
|
package/docs/iphone-duo.md
CHANGED
|
@@ -6,7 +6,7 @@ title: iPhone Duo support (experimental)
|
|
|
6
6
|
|
|
7
7
|
Adapt your Ionic app to iPhone Duo: place navigation and actions in its vertical system rail, and adjust your split pane as the device opens and closes. Existing Ionic markup remains the source of labels, icons, routing, and click handlers.
|
|
8
8
|
|
|
9
|
-
**New here?** Start with [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme) to preview the side layout in Chrome. This page explains device events, placement
|
|
9
|
+
**New here?** Start with [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme) to preview the side layout in Chrome. This page explains device events, placement and split panes. [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars) documents control projection and the runtime API.
|
|
10
10
|
|
|
11
11
|
Available in `1.2.0-0` as an **experimental** feature alongside [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell). APIs and supported behavior may change. The real system rail and hinge reporting require iOS 27.1 or later and an app built with Xcode 27.1 or newer.
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ Device state belongs to [`@erkamyaman/capacitor-foldable`](https://github.com/er
|
|
|
19
19
|
|
|
20
20
|
## Choose what to adopt
|
|
21
21
|
|
|
22
|
-
To keep your existing theme and add only the standalone support, follow [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme). This page covers the shared device-layout rules
|
|
22
|
+
To keep your existing theme and add only the standalone support, follow [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme). This page covers the shared device-layout rules.
|
|
23
23
|
|
|
24
24
|
| Goal | Stylesheet | Runtime |
|
|
25
25
|
| ------------------------------------------------ | ------------------- | ------------------------------------------------------------------ |
|
|
@@ -76,21 +76,7 @@ WebView corner radius remains a rendering concern: `configureNativeTransition()`
|
|
|
76
76
|
|
|
77
77
|
## Reserve the vertical rail
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```html
|
|
82
|
-
<ion-app class="ios-theme-vertical-bars">...</ion-app>
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
The classes are physical — `-left` always means the physical left edge — because CSS and the native renderer work in physical coordinates. `setVerticalControlAreaPlacement` (below) applies the logical `verticalBarEdge` reported by `Foldable` and resolves it through the document's direction, so an RTL app does not need its own conversion.
|
|
86
|
-
|
|
87
|
-
For Chrome development, no native plugin is needed — the class alone reserves `80px` to simulate iPhone Duo. When `setVerticalControlAreaPlacement` receives `{ edge, nativeEdge, inset }`, the inset replaces the fallback width, even when it is less than `80px`. Override `--ios-theme-vertical-bars-safe-area-left` or `--ios-theme-vertical-bars-safe-area-right` when simulating a different layout.
|
|
88
|
-
|
|
89
|
-
This keeps routers and component backgrounds full-viewport. `ion-content` moves its scroll foreground, `ion-toolbar` moves its container foreground, and `ion-fab` adjusts only when placed beside the system UI. The corresponding Ionic safe-area variable is reset inside those foreground components so descendants do not add the inset again.
|
|
90
|
-
|
|
91
|
-
`ion-modal` applies the same foreground correction when its visible dialog spans the viewport width. With the Vertical Control Area runtime enabled, the topmost full-width modal also projects eligible toolbar buttons into its own rail; centered dialogs keep their toolbar buttons and receive no page-rail inset. This includes full-width sheet modals: their rail follows the visible sheet bounds as the breakpoint changes. Eligibility follows the visible dialog width, not the hinge posture or modal type. `ion-menu` and `ion-popover` are handled as separate surfaces: their internal foreground components do not receive the main-page conversion and retain Ionic's standard safe-area handling. A menu presented beside the system UI keeps Ionic's full-viewport animation host and offsets only its visible container by the corresponding inset; a menu from the opposite side is unchanged. Left and right remain physical coordinates in RTL, while Ionic's `side="start"` and `side="end"` values remain logical.
|
|
92
|
-
|
|
93
|
-
The mode is component-mode independent: an app can keep Ionic `mode: 'md'` on iOS and still enable Vertical Bars. No component needs `mode="ios"`.
|
|
79
|
+
Load `vertical-bars.css` as shown above. For class-based browser simulation, safe-area handling, RTL and overlay layout, see [Reserve the vertical rail](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#reserve-the-vertical-rail). On device, the placement helper below applies the layout classes and measured inset.
|
|
94
80
|
|
|
95
81
|
## Project controls into the rail
|
|
96
82
|
|
|
@@ -115,45 +101,19 @@ await listener.remove();
|
|
|
115
101
|
await rail.destroy();
|
|
116
102
|
```
|
|
117
103
|
|
|
118
|
-
Supply `nativeEdge` on both the initial read and each event so the renderer knows which rail the system actually provides. The measured `inset` is passed through instead of assuming a fixed width. Devices without a reported rail, including Web and Android, return a null edge and retain the ordinary layout. For browser simulation, use the class-based preview
|
|
104
|
+
Supply `nativeEdge` on both the initial read and each event so the renderer knows which rail the system actually provides. The measured `inset` is passed through instead of assuming a fixed width. Devices without a reported rail, including Web and Android, return a null edge and retain the ordinary layout. For browser simulation, use the [class-based preview](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#reserve-the-vertical-rail) without connecting device placement.
|
|
119
105
|
|
|
120
106
|
Start either `enableVerticalControlArea()` or the full `enableNativeUIShell()` — not both. If the app already uses Native UI Shell, keep that runtime and use the same `setVerticalControlAreaPlacement` callback. The application owner removes its listeners and destroys its runtime on teardown.
|
|
121
107
|
|
|
122
|
-
On supported iOS versions the runtime hands eligible tabs, back navigation, menu buttons, and fixed-toolbar actions to a native SwiftUI `TabView` and toolbar; on Web, Android, or when native projection is unavailable, Web clones remain the fallback.
|
|
108
|
+
On supported iOS versions the runtime hands eligible tabs, back navigation, menu buttons, and fixed-toolbar actions to a native SwiftUI `TabView` and toolbar; on Web, Android, or when native projection is unavailable, Web clones remain the fallback. See [Toolbar actions](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions) for control eligibility.
|
|
123
109
|
|
|
124
110
|
### Toolbar actions
|
|
125
111
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
| Icon markup | Placement |
|
|
129
|
-
| --- | --- |
|
|
130
|
-
| `slot="icon-only"` | Vertical rail |
|
|
131
|
-
| `slot="start"`, `slot="end"`, or no slot | Original horizontal toolbar |
|
|
132
|
-
| No icon | Original horizontal toolbar |
|
|
133
|
-
|
|
134
|
-
This rule applies to `fill="default"`, `clear`, `solid`, and `outline`, including buttons with an Ionic `color`. Clear actions have no native glass background, including buttons implicitly resolved to clear inside `ion-buttons`. Solid actions retain their background color through a prominent native button; outline actions retain their border color and width. The runtime preserves the original click or form-submit behavior. `type="submit"` and `.button-submit` do not select a different placement.
|
|
135
|
-
|
|
136
|
-
```html
|
|
137
|
-
<ion-header>
|
|
138
|
-
<ion-toolbar>
|
|
139
|
-
<ion-buttons slot="end">
|
|
140
|
-
<ion-button type="button" fill="outline" color="primary" aria-label="Done">
|
|
141
|
-
<ion-icon name="checkmark-outline" slot="icon-only"></ion-icon>
|
|
142
|
-
</ion-button>
|
|
143
|
-
</ion-buttons>
|
|
144
|
-
</ion-toolbar>
|
|
145
|
-
</ion-header>
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
For existing themes that style a button background through CSS while leaving `fill` omitted or `default`, use `enableVerticalControlArea({ buttonDefaultFill: 'solid' })`. The option accepts `'solid'` (Ionic's default design) or `null` (the iOS theme's glass design); omission is equivalent to `null`. Buttons inside `ion-buttons` default to `clear`, even with this option set to `solid`. To project their background, explicitly set `fill="solid"`; CSS backgrounds on clear buttons are not projected. With the iOS theme, ordinary `ion-buttons` retain group projection, while `ion-buttons.ios-theme-disabled` projects eligible buttons individually. Explicit button fills take precedence. It affects native vertical projection only and never changes source buttons or Web clones. The full `enableNativeUIShell()` also accepts this option for its vertical buttons. Restart the runtime before changing the option.
|
|
149
|
-
|
|
150
|
-
Give icon-only actions an accessible name, such as `aria-label="Done"`. Keep the original Ionic event handler or form association on the source button.
|
|
151
|
-
|
|
152
|
-
The same rule applies to individual buttons and buttons inside `ion-buttons`, on ordinary pages and in the topmost full-width modal. Centered modals, menus, and popovers keep their own toolbar layout. Add `.ios-theme-horizontal-only` to a group or individual button to keep it horizontal. Placement is chosen when a routed page enters; changing an existing button's content or icon slot does not move it between the toolbar and rail until the page leaves and re-enters.
|
|
112
|
+
See [Vertical Bars: Toolbar actions](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions) for eligible markup, placement, button appearance and local overrides.
|
|
153
113
|
|
|
154
114
|
### Tab bar
|
|
155
115
|
|
|
156
|
-
|
|
116
|
+
See [Vertical Bars: Tab bar](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#tab-bar) for navigation, labels and Web fallback behavior.
|
|
157
117
|
|
|
158
118
|
## Adapt the split pane
|
|
159
119
|
|
|
@@ -187,111 +147,4 @@ Ionic's `when` still controls whether the menu is persistent. The example choose
|
|
|
187
147
|
|
|
188
148
|
## Vertical Control Area API
|
|
189
149
|
|
|
190
|
-
The
|
|
191
|
-
|
|
192
|
-
<docgen-index>
|
|
193
|
-
|
|
194
|
-
* [`setPlacement(...)`](#setplacement)
|
|
195
|
-
* [`getStatus()`](#getstatus)
|
|
196
|
-
* [`suspend()`](#suspend)
|
|
197
|
-
* [`destroy()`](#destroy)
|
|
198
|
-
* [Interfaces](#interfaces)
|
|
199
|
-
* [Type Aliases](#type-aliases)
|
|
200
|
-
|
|
201
|
-
</docgen-index>
|
|
202
|
-
|
|
203
|
-
<docgen-api>
|
|
204
|
-
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
205
|
-
|
|
206
|
-
### setPlacement(...)
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
setPlacement(placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean | undefined) => void
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Applies the application's chosen placement to both Web and native controls.
|
|
213
|
-
|
|
214
|
-
| Param | Type |
|
|
215
|
-
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
216
|
-
| **`placement`** | <code><a href="#verticalbaredge">VerticalBarEdge</a> \| <a href="#verticalbarplacement">VerticalBarPlacement</a></code> |
|
|
217
|
-
| **`rtl`** | <code>boolean</code> |
|
|
218
|
-
|
|
219
|
-
--------------------
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
### getStatus()
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
getStatus() => NativeUIShellStatus
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
Returns the current Web/native projection state.
|
|
229
|
-
|
|
230
|
-
**Returns:** <code><a href="#nativeuishellstatus">NativeUIShellStatus</a></code>
|
|
231
|
-
|
|
232
|
-
--------------------
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
### suspend()
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
suspend() => Promise<NativeUIShellSuspension>
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Restores projected controls to the Web until the returned lease is resumed.
|
|
242
|
-
|
|
243
|
-
**Returns:** <code>Promise<<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>></code>
|
|
244
|
-
|
|
245
|
-
--------------------
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
### destroy()
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
destroy() => Promise<void>
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
Stops synchronization, restores Web controls and releases native resources.
|
|
255
|
-
|
|
256
|
-
--------------------
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
### Interfaces
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
#### VerticalBarPlacement
|
|
263
|
-
|
|
264
|
-
| Prop | Type | Description |
|
|
265
|
-
| ---------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
266
|
-
| **`edge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | |
|
|
267
|
-
| **`inset`** | <code>number</code> | Explicit rail width in CSS pixels; omitted to use the stylesheet's safe-area rules. |
|
|
268
|
-
| **`nativeEdge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | Native logical edge reported by the application's device plugin. Null or an unregistered edge uses a Web rail in verticalBarsOnly mode, or the ordinary Native UI Shell layout otherwise. Omission keeps the last supplied value. |
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
#### NativeUIShellStatus
|
|
272
|
-
|
|
273
|
-
| Prop | Type |
|
|
274
|
-
| --------------- | ------------------------------------------- |
|
|
275
|
-
| **`state`** | <code>'native' \| 'stopped' \| 'web'</code> |
|
|
276
|
-
| **`projected`** | <code>number</code> |
|
|
277
|
-
| **`updates`** | <code>number</code> |
|
|
278
|
-
| **`reason`** | <code>string</code> |
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
#### NativeUIShellSuspension
|
|
282
|
-
|
|
283
|
-
| Method | Signature | Description |
|
|
284
|
-
| ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
285
|
-
| **resume** | () => Promise<void> | Releases this suspension. Native projection resumes after all active suspensions are released. |
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
### Type Aliases
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
#### VerticalBarEdge
|
|
292
|
-
|
|
293
|
-
Logical edge in the reading direction, matching UIVerticalBarEdge and capacitor-foldable.
|
|
294
|
-
|
|
295
|
-
<code>'leading' | 'trailing' | null</code>
|
|
296
|
-
|
|
297
|
-
</docgen-api>
|
|
150
|
+
The runtime handle reference is maintained in [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#vertical-control-area-api).
|
package/docs/native-ui-shell.md
CHANGED
|
@@ -40,7 +40,7 @@ Native appearance follows the applied class, system or always-dark theme CSS. Sy
|
|
|
40
40
|
|
|
41
41
|
## Supported markup
|
|
42
42
|
|
|
43
|
-
The table below describes the ordinary Native UI Shell. Vertical Bars uses the separate [toolbar action rules](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/
|
|
43
|
+
The table below describes the ordinary Native UI Shell. Vertical Bars uses the separate [toolbar action rules](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions).
|
|
44
44
|
|
|
45
45
|
| Ionic component | Supported appearance and placement | Native rendering |
|
|
46
46
|
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
@@ -84,7 +84,7 @@ The DOM owns labels, SVG content, placement, selected values and application beh
|
|
|
84
84
|
|
|
85
85
|
For forms, retain `ion-button type="submit"` and the form's existing submit handler. An external form is still passed as `[form]="formRef"`. The plugin does not call `form.submit()`, add a second submit path or change Angular form ownership. Segment values retain their original type because the original `ion-segment-button` is clicked; programmatic value changes do not emit a synthetic `ionChange`.
|
|
86
86
|
|
|
87
|
-
Labels use native text. Local static SVGs and resolved `ion-icon` SVGs (including `name` and changes to `name`) are rasterized at the display scale and cached
|
|
87
|
+
Labels use native text. Local static SVGs and resolved `ion-icon` SVGs (including `name` and changes to `name`) are rasterized at the display scale and cached. Source projection preserves their colors; system projection of vertical toolbar buttons uses template icons tinted by SwiftUI. Tab SVGs that follow text color use native template rendering, so icons and labels change selection color together without waiting for another bridge image. Multicolor tab artwork retains its original colors. External references, `<use>`, animation, embedded HTML/images, SVG text and stylesheets are excluded. Web fonts and arbitrary slot layouts are not reproduced exactly.
|
|
88
88
|
|
|
89
89
|
The native host accepts input only within native controls. Tab interaction and accessibility are provided by the standard [UITabBar](https://developer.apple.com/documentation/uikit/uitabbar) control. Empty areas pass touches to the WebView. Ionic iOS hides empty badges by default. An empty visible `ion-badge` becomes a native notification dot; a non-empty badge displays its text. Badge background and text colors come from computed DOM styles, including Ionic `color` palettes. Hidden or removed badges clear the native badge. Tabs use UIKit’s standard tab control for selection, touch behavior, badges and accessibility, retaining item identity across selection updates. UIKit owns the item layout within the measured bar, so arbitrary CSS item placement is not reproduced. Native controls expose names, disabled/selected traits and badges to accessibility; the source is hidden from Web accessibility while projected. This is not a guarantee of identical VoiceOver traversal between Web and UIKit.
|
|
90
90
|
|
|
@@ -177,11 +177,11 @@ iPhone Duo support is experimental, including standalone use without Native UI S
|
|
|
177
177
|
|
|
178
178
|
The standalone Vertical Control Area entry point (`@rdlabo/ionic-theme-ios27/vertical-bars`) and `dist/css/vertical-bars.css` work without loading the iOS 27 theme. Call `enableVerticalControlArea()` for this use case; it projects only controls placed in the vertical area. Apps already calling `enableNativeUIShell()` should keep that single runtime rather than starting both. See [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo) for the complete setup, including hinge posture and the split-pane layout for apps that do not use this shell at all.
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
Vertical Bars uses a SwiftUI `TabView` and toolbar in the system rail. Ionic remains the source of labels, icons, selected/disabled state and actions. See [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars) for rail layout, native fallback and supported overlays.
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
In Vertical Bars, fixed-toolbar `ion-button` actions need an `ion-icon` or SVG with `slot="icon-only"`. All fills and Ionic colors are eligible; submit buttons follow the same placement rule. Add `.ios-theme-horizontal-only` to a button or its `ion-buttons` group to keep it horizontal. See [Toolbar actions](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions) for placement and exclusion rules.
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
**Unreleased button appearance options:** the next release defaults native vertical buttons to `buttonProjection: 'system'`. Use `source` to project Ionic fill and colors, with `ios-theme-projection-source` / `ios-theme-projection-system` classes for local exceptions. These settings affect native vertical `ion-button` and `ion-menu-button` actions only. Horizontal controls and Web clones keep their existing behavior. See [Choose button appearance](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#choose-button-appearance) for migration, priority and fill rules.
|
|
185
185
|
|
|
186
186
|
For standalone setup while keeping your existing theme, see [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme).
|
|
187
187
|
|
package/docs/special-markup.md
CHANGED
|
@@ -53,6 +53,8 @@ iPhone Duo support is experimental. It is available in the `1.2.0-0` prerelease
|
|
|
53
53
|
|
|
54
54
|
iPhone Duo support — the vertical system rail, hinge posture, and the posture-driven split-pane layout — is independent of the iOS 27 theme and the full Native UI Shell. See [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo) for the complete setup, including device-layout monitoring without a projection runtime.
|
|
55
55
|
|
|
56
|
+
See [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars) for rail layout classes, control eligibility and native button appearance.
|
|
57
|
+
|
|
56
58
|
For standalone setup while keeping your existing theme, see [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme).
|
|
57
59
|
|
|
58
60
|
## Two-line inset list items
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Vertical Bars (experimental)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Vertical Bars (experimental)
|
|
6
|
+
|
|
7
|
+
Vertical Bars moves eligible Ionic navigation and actions into a side rail while keeping the original components as the source of labels, icons and behavior. It works with this theme or an existing Ionic theme, independently of hinge posture and the full Native UI Shell.
|
|
8
|
+
|
|
9
|
+
Use this page for layout, control eligibility, native button appearance and the runtime API. For device events and split panes, see [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo). For a step-by-step browser preview and native setup, start with [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme).
|
|
10
|
+
|
|
11
|
+
Vertical Bars is experimental; APIs and supported behavior may change. The button appearance options below are unreleased.
|
|
12
|
+
|
|
13
|
+
## Enable Vertical Bars
|
|
14
|
+
|
|
15
|
+
Load the opt-in stylesheet:
|
|
16
|
+
|
|
17
|
+
```scss
|
|
18
|
+
@use '@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
After `ion-app` is mounted, start one runtime:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { enableVerticalControlArea } from '@rdlabo/ionic-theme-ios27/vertical-bars';
|
|
25
|
+
|
|
26
|
+
const rail = await enableVerticalControlArea();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Add the layout class below for browser simulation, or [apply device placement](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#project-controls-into-the-rail) for an iPhone Duo. The application owns placement and cleanup: call `await rail.destroy()` when its owner is disposed. If you already use `enableNativeUIShell()`, keep that runtime; it includes Vertical Bars. Do not start both.
|
|
30
|
+
|
|
31
|
+
The `/vertical-bars` entry point requires `@capacitor/core`, including in browser builds. CSS-only use needs no runtime. For native setup and navigation transitions, follow the [existing-theme guide](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme).
|
|
32
|
+
|
|
33
|
+
On supported Capacitor iOS, eligible controls use native SwiftUI rendering. Web, Android and unavailable native projection use Web clones. A browser preview can verify layout and actions, but cannot compare native button appearance.
|
|
34
|
+
|
|
35
|
+
## Reserve the vertical rail
|
|
36
|
+
|
|
37
|
+
Add `.ios-theme-vertical-bars` to `ion-app` to reserve the rail region on the physical right, or add `.ios-theme-vertical-bars-left` as well to use the physical left:
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<ion-app class="ios-theme-vertical-bars">...</ion-app>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The classes are physical — `-left` always means the physical left edge — because CSS and the native renderer work in physical coordinates. `setVerticalControlAreaPlacement` applies the logical `verticalBarEdge` reported by the device plugin and resolves it through the document's direction, so an RTL app does not need its own conversion.
|
|
44
|
+
|
|
45
|
+
For Chrome development, no native plugin is needed — the class alone reserves `80px` to simulate iPhone Duo. When `setVerticalControlAreaPlacement` receives `{ edge, nativeEdge, inset }`, the inset replaces the fallback width, even when it is less than `80px`. Override `--ios-theme-vertical-bars-safe-area-left` or `--ios-theme-vertical-bars-safe-area-right` when simulating a different layout.
|
|
46
|
+
|
|
47
|
+
This keeps routers and component backgrounds full-viewport. `ion-content` moves its scroll foreground, `ion-toolbar` moves its container foreground, and `ion-fab` adjusts only when placed beside the system UI. The corresponding Ionic safe-area variable is reset inside those foreground components so descendants do not add the inset again.
|
|
48
|
+
|
|
49
|
+
`ion-modal` applies the same foreground correction when its visible dialog spans the viewport width. With the Vertical Control Area runtime enabled, the topmost full-width modal also projects eligible toolbar buttons into its own rail; centered dialogs keep their toolbar buttons and receive no page-rail inset. This includes full-width sheet modals: their rail follows the visible sheet bounds as the breakpoint changes. Eligibility follows the visible dialog width, not the hinge posture or modal type. `ion-menu` and `ion-popover` are handled as separate surfaces: their internal foreground components do not receive the main-page conversion and retain Ionic's standard safe-area handling. A menu presented beside the system UI keeps Ionic's full-viewport animation host and offsets only its visible container by the corresponding inset; a menu from the opposite side is unchanged. Left and right remain physical coordinates in RTL, while Ionic's `side="start"` and `side="end"` values remain logical.
|
|
50
|
+
|
|
51
|
+
The mode is component-mode independent: an app can keep Ionic `mode: 'md'` on iOS and still enable Vertical Bars. No component needs `mode="ios"`.
|
|
52
|
+
|
|
53
|
+
## Native rendering
|
|
54
|
+
|
|
55
|
+
On supported iOS versions, adding `.ios-theme-vertical-bars` changes only controls that the system relocates into the physical side rail. Native UI Shell presents eligible tabs, back navigation, menu buttons, and toolbar actions through a SwiftUI `TabView` and toolbar once the class is applied. When the OS reports a rail edge — on iPhone Duo linked against iOS 27.1 or later — it must agree with the applied placement; a disagreeing report keeps the rail on the Web. Older toolchains that cannot report an edge trust the DOM placement directly. SwiftUI owns their adaptive placement and Liquid Glass appearance; Ionic remains the source of labels, icons, selected/disabled state, routing, form submission, and click handlers.
|
|
56
|
+
|
|
57
|
+
The SwiftUI surface is clipped and hit-tested to the system rail. Web content remains visible and interactive outside that physical region. The runtime optimistically updates tab selection before forwarding the action to the original `ion-tab-button`, using the same event and stale-revision protection as the other native controls. Overlay layout follows the [rail reservation rules](#reserve-the-vertical-rail).
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## Toolbar actions
|
|
61
|
+
|
|
62
|
+
A standard `ion-back-button` can project from outside a fixed toolbar, including routed content or a persistent app shell. `ion-menu-button` and other toolbar actions require a fixed toolbar.
|
|
63
|
+
|
|
64
|
+
An `ion-button` moves into the rail when it contains an `ion-icon` or SVG with `slot="icon-only"`. The button must be in a fixed `ion-toolbar` directly inside `ion-header` or `ion-footer`, outside scrolling `ion-content`.
|
|
65
|
+
|
|
66
|
+
| Icon markup | Placement |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `slot="icon-only"` | Vertical rail |
|
|
69
|
+
| `slot="start"`, `slot="end"`, or no slot | Original horizontal toolbar |
|
|
70
|
+
| No icon | Original horizontal toolbar |
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<ion-header>
|
|
74
|
+
<ion-toolbar>
|
|
75
|
+
<ion-buttons slot="end">
|
|
76
|
+
<ion-button aria-label="Done">
|
|
77
|
+
<ion-icon name="checkmark-outline" slot="icon-only"></ion-icon>
|
|
78
|
+
</ion-button>
|
|
79
|
+
</ion-buttons>
|
|
80
|
+
</ion-toolbar>
|
|
81
|
+
</ion-header>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
All fills (`default`, `clear`, `solid`, and `outline`) and Ionic colors follow this placement rule. Keep accessible names and the original click or form-submit handlers on the source buttons. `type="submit"` and `.button-submit` do not select a different placement.
|
|
85
|
+
|
|
86
|
+
The rule applies to individual buttons and buttons inside `ion-buttons`, on ordinary pages and in the topmost full-width modal. Centered modals, menus, and popovers keep their own toolbar layout. Add `.ios-theme-horizontal-only` to a group or individual button to keep it horizontal. Placement is chosen when a routed page enters; changing an existing button's content or icon slot does not move it between the toolbar and rail until the page leaves and re-enters.
|
|
87
|
+
|
|
88
|
+
### Choose button appearance
|
|
89
|
+
|
|
90
|
+
**Unreleased:** `buttonProjection` and the local projection classes below are part of the next release. They are not available in the `1.2.0-1` package used by the introductory guide. The following rules describe this branch.
|
|
91
|
+
|
|
92
|
+
For native vertical `ion-button` and `ion-menu-button` actions, choose who controls appearance:
|
|
93
|
+
|
|
94
|
+
| `buttonProjection` | Appearance |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `'system'` (default) | SwiftUI styles the buttons and tints their icons. Ionic fill, colors and borders are not applied. |
|
|
97
|
+
| `'source'` | Projects the supported Ionic fill and computed colors described in [Source fill rules](#source-fill-rules). |
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const rail = await enableVerticalControlArea({ buttonProjection: 'source' });
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Both `enableVerticalControlArea()` and `enableNativeUIShell()` accept the option. Use one runtime, and destroy it before restarting with different options. Either mode preserves actions, disabled state and grouping. These appearance settings do not affect horizontal controls, source elements or Web fallback clones, so compare the native appearance on supported iOS.
|
|
104
|
+
|
|
105
|
+
**Migration:** the default changes from source styling to `system`. Set `buttonProjection: 'source'` to retain the previous projection behavior. This does not change the feature's experimental status.
|
|
106
|
+
|
|
107
|
+
### Override individual buttons or groups
|
|
108
|
+
|
|
109
|
+
Use `ios-theme-projection-source` or `ios-theme-projection-system` for local exceptions:
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<ion-buttons class="ios-theme-projection-source">
|
|
113
|
+
<ion-button fill="solid" aria-label="Add">
|
|
114
|
+
<ion-icon name="add-outline" slot="icon-only"></ion-icon>
|
|
115
|
+
</ion-button>
|
|
116
|
+
<ion-button class="ios-theme-projection-system" aria-label="Search">
|
|
117
|
+
<ion-icon name="search-outline" slot="icon-only"></ion-icon>
|
|
118
|
+
</ion-button>
|
|
119
|
+
</ion-buttons>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The first matching setting wins:
|
|
123
|
+
|
|
124
|
+
1. The button's projection class.
|
|
125
|
+
2. Its nearest `ion-buttons` projection class.
|
|
126
|
+
3. The startup `buttonProjection` option, or `system` if omitted.
|
|
127
|
+
|
|
128
|
+
If both classes are present on the same element, `system` wins. Removing the classes restores the inherited setting. Class changes apply without restarting the runtime; grouping and placement stay unchanged. Only native vertical `ion-button` and `ion-menu-button` actions interpret these classes. Other ancestors, back buttons, tabs and FABs do not.
|
|
129
|
+
|
|
130
|
+
### Source fill rules
|
|
131
|
+
|
|
132
|
+
For an `ion-button` resolved to `source`, fill is chosen separately from the projection mode:
|
|
133
|
+
|
|
134
|
+
| Button | Effective fill |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| Explicit `fill="clear"`, `"solid"` or `"outline"` | The explicit value |
|
|
137
|
+
| Omitted fill or `fill="default"` inside `ion-buttons` | `clear` |
|
|
138
|
+
| Omitted fill or `fill="default"` outside `ion-buttons` | `buttonDefaultFill` |
|
|
139
|
+
|
|
140
|
+
`buttonDefaultFill` accepts only `'solid'` or `null`; omission is equivalent to `null`. Use `'solid'` for Ionic's default button design, or `null` for this theme's default glass design. It applies to every button resolved to `source`, including a local exception under a global `system` setting.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// Keep system styling globally; local source buttons use Ionic's default fill.
|
|
144
|
+
const rail = await enableVerticalControlArea({ buttonDefaultFill: 'solid' });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<!-- In a fixed toolbar, outside ion-buttons: omitted fill resolves to solid. -->
|
|
149
|
+
<ion-button class="ios-theme-projection-source" aria-label="Add">
|
|
150
|
+
<ion-icon name="add-outline" slot="icon-only"></ion-icon>
|
|
151
|
+
</ion-button>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
| Effective fill | Native appearance in `source` mode |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| `clear` | Icon color, without a glass background; CSS backgrounds are ignored |
|
|
157
|
+
| `solid` | Computed background color tints a prominent glass button |
|
|
158
|
+
| `outline` | Computed border color and width, with native glass |
|
|
159
|
+
| `null` | Native glass with source icon colors |
|
|
160
|
+
|
|
161
|
+
Inside `ion-buttons`, explicitly set `fill="solid"` to project a background; `buttonDefaultFill: 'solid'` does not override the group's clear default. Native Liquid Glass tinting can differ from the CSS color, especially for translucent backgrounds. With the iOS theme, ordinary `ion-buttons` retain group projection; `ion-buttons.ios-theme-disabled` projects eligible buttons individually. Projection classes do not change that grouping rule.
|
|
162
|
+
|
|
163
|
+
## Tab bar
|
|
164
|
+
|
|
165
|
+
When the app contains `ion-tabs`, its tab bar moves into the reserved region and uses the native Duo edge spacing; the Ionic `slot` value does not select a different position. Without native projection, the stable Web rail is icon-only, matching the native resting presentation. While the user presses and drags across that rail, every icon-and-label tab reveals its label so the pending destination stays identifiable. The Web tab bar receives pointer input in the simulated system region. Native tabs and a restored Web tab bar fade in over 180ms; disappearance remains immediate. Reduced motion disables this fade. Use `ion-menu` when navigation should become a sidebar; this mode does not convert tabs into a menu. Web clones also work when no `ion-tabs` exists. Disabling the mode or leaving the page removes the native ownership or Web clones and restores their sources. Override `--ios-theme-vertical-bars-toolbar-top` when the simulated system controls use a different vertical layout.
|
|
166
|
+
|
|
167
|
+
## Vertical Control Area API
|
|
168
|
+
|
|
169
|
+
The generated reference below documents the handle returned by `enableVerticalControlArea()`.
|
|
170
|
+
|
|
171
|
+
<docgen-index>
|
|
172
|
+
|
|
173
|
+
* [`setPlacement(...)`](#setplacement)
|
|
174
|
+
* [`getStatus()`](#getstatus)
|
|
175
|
+
* [`suspend()`](#suspend)
|
|
176
|
+
* [`destroy()`](#destroy)
|
|
177
|
+
* [Interfaces](#interfaces)
|
|
178
|
+
* [Type Aliases](#type-aliases)
|
|
179
|
+
|
|
180
|
+
</docgen-index>
|
|
181
|
+
|
|
182
|
+
<docgen-api>
|
|
183
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
184
|
+
|
|
185
|
+
### setPlacement(...)
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
setPlacement(placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean | undefined) => void
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Applies the application's chosen placement to both Web and native controls.
|
|
192
|
+
|
|
193
|
+
| Param | Type |
|
|
194
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
+
| **`placement`** | <code><a href="#verticalbaredge">VerticalBarEdge</a> \| <a href="#verticalbarplacement">VerticalBarPlacement</a></code> |
|
|
196
|
+
| **`rtl`** | <code>boolean</code> |
|
|
197
|
+
|
|
198
|
+
--------------------
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
### getStatus()
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
getStatus() => NativeUIShellStatus
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Returns the current Web/native projection state.
|
|
208
|
+
|
|
209
|
+
**Returns:** <code><a href="#nativeuishellstatus">NativeUIShellStatus</a></code>
|
|
210
|
+
|
|
211
|
+
--------------------
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
### suspend()
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
suspend() => Promise<NativeUIShellSuspension>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Restores projected controls to the Web until the returned lease is resumed.
|
|
221
|
+
|
|
222
|
+
**Returns:** <code>Promise<<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>></code>
|
|
223
|
+
|
|
224
|
+
--------------------
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
### destroy()
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
destroy() => Promise<void>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Stops synchronization, restores Web controls and releases native resources.
|
|
234
|
+
|
|
235
|
+
--------------------
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
### Interfaces
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
#### VerticalBarPlacement
|
|
242
|
+
|
|
243
|
+
| Prop | Type | Description |
|
|
244
|
+
| ---------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
245
|
+
| **`edge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | |
|
|
246
|
+
| **`inset`** | <code>number</code> | Explicit rail width in CSS pixels; omitted to use the stylesheet's safe-area rules. |
|
|
247
|
+
| **`nativeEdge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | Native logical edge reported by the application's device plugin. Null or an unregistered edge uses a Web rail in verticalBarsOnly mode, or the ordinary Native UI Shell layout otherwise. Omission keeps the last supplied value. |
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
#### NativeUIShellStatus
|
|
251
|
+
|
|
252
|
+
| Prop | Type |
|
|
253
|
+
| --------------- | ------------------------------------------- |
|
|
254
|
+
| **`state`** | <code>'native' \| 'stopped' \| 'web'</code> |
|
|
255
|
+
| **`projected`** | <code>number</code> |
|
|
256
|
+
| **`updates`** | <code>number</code> |
|
|
257
|
+
| **`reason`** | <code>string</code> |
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
#### NativeUIShellSuspension
|
|
261
|
+
|
|
262
|
+
| Method | Signature | Description |
|
|
263
|
+
| ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
264
|
+
| **resume** | () => Promise<void> | Releases this suspension. Native projection resumes after all active suspensions are released. |
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
### Type Aliases
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
#### VerticalBarEdge
|
|
271
|
+
|
|
272
|
+
Logical edge in the reading direction, matching UIVerticalBarEdge and capacitor-foldable.
|
|
273
|
+
|
|
274
|
+
<code>'leading' | 'trailing' | null</code>
|
|
275
|
+
|
|
276
|
+
</docgen-api>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rdlabo/ionic-theme-ios27",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.2.0-beta.
|
|
4
|
+
"version": "1.2.0-beta.pr243.shae972a0e49191",
|
|
5
5
|
"description": "iOS27 Theme for Ionic Framework",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"prebuild:css": "rdlabo-copy-structured-list src/styles/utils/structured-list.scss",
|
|
36
36
|
"build:css": "rm -rf dist/css && sass src/styles:dist/css --style=compressed --no-source-map",
|
|
37
37
|
"build:ts": "tsc --noEmit && rdlabo-build-theme",
|
|
38
|
-
"docgen": "docgen --project tsconfig.docgen.json --api NativeUIShellHandle --output-readme docs/native-ui-shell.md && docgen --project tsconfig.docgen.json --api VerticalControlAreaHandle --output-readme docs/
|
|
38
|
+
"docgen": "docgen --project tsconfig.docgen.json --api NativeUIShellHandle --output-readme docs/native-ui-shell.md && docgen --project tsconfig.docgen.json --api VerticalControlAreaHandle --output-readme docs/vertical-bars.md",
|
|
39
39
|
"build:demo": "npm run build && cd demo && npm install && npm run build -- --configuration=production",
|
|
40
40
|
"lint": "prettier --check \"./**/*.{scss,ts}\" && prettier --parser angular --check \"./**/*.html\"",
|
|
41
41
|
"fmt": "prettier --write \"./**/*.{scss,ts}\" && prettier --parser angular --write \"./**/*.html\"",
|
|
@@ -11,7 +11,7 @@ export const tracksMotion = true;
|
|
|
11
11
|
export const read = (element: HTMLElement, id: Identify, options: VerticalControlAreaOptions = {}): Candidate | undefined => {
|
|
12
12
|
if (!inFixedToolbar(element)) return;
|
|
13
13
|
let children = childElements(element);
|
|
14
|
-
if (children.length === 1) return menuButton.read(element, id);
|
|
14
|
+
if (children.length === 1) return menuButton.read(element, id, options);
|
|
15
15
|
const verticalBars =
|
|
16
16
|
!element.closest('ion-menu, ion-popover') && modalUsesVerticalBars(element) && !!element.closest('ion-app.ios-theme-vertical-bars');
|
|
17
17
|
if (verticalBars && !isVerticalBarsToolbarGroup(element)) return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createCandidate, appendItem } from '../shared/candidate';
|
|
2
2
|
import type { Candidate, Identify } from '../shared/candidate';
|
|
3
|
+
import type { VerticalControlAreaOptions } from '../definitions';
|
|
3
4
|
import { childElements, inFixedToolbar } from '../shared/dom';
|
|
4
5
|
|
|
5
6
|
export const tag = 'ion-menu-button';
|
|
@@ -13,11 +14,11 @@ export const append = (candidate: Candidate, button: HTMLIonMenuButtonElement, i
|
|
|
13
14
|
return !!content && !!appendItem(candidate, button, id, content);
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
export const read = (group: HTMLElement, id: Identify): Candidate | undefined => {
|
|
17
|
+
export const read = (group: HTMLElement, id: Identify, options: VerticalControlAreaOptions = {}): Candidate | undefined => {
|
|
17
18
|
const children = group.matches('ion-buttons') ? childElements(group) : [];
|
|
18
19
|
if (!inFixedToolbar(group) || children.length !== 1) return;
|
|
19
20
|
const button = children[0];
|
|
20
21
|
if (!button?.matches(`${tag}${group.closest('ion-app.ios-theme-vertical-bars') ? '' : '.ios'}`)) return;
|
|
21
|
-
const candidate = createCandidate(group, tag, id);
|
|
22
|
+
const candidate = createCandidate(group, tag, id, options);
|
|
22
23
|
return append(candidate, button as HTMLIonMenuButtonElement, id) ? candidate : undefined;
|
|
23
24
|
};
|
|
@@ -11,12 +11,19 @@ export interface NativeUIShellStatus {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface VerticalControlAreaOptions {
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
/** Appearance of native vertical ion-button and ion-menu-button actions.
|
|
15
|
+
* `system` (default) uses SwiftUI styling and template icons; `source` projects supported Ionic fill and colors.
|
|
16
|
+
* Local ios-theme-projection-source / ios-theme-projection-system classes take precedence:
|
|
17
|
+
* the button itself, then its nearest ion-buttons, then this option. On the same element, system wins.
|
|
18
|
+
* Classes update live; removing them restores inheritance. Actions, disabled state and grouping are preserved.
|
|
19
|
+
* Does not affect back buttons, tabs, FABs, horizontal controls, source elements or Web clones.
|
|
20
|
+
*/
|
|
21
|
+
buttonProjection?: 'source' | 'system';
|
|
22
|
+
/** Default fill for native vertical ion-button actions resolved to `source`, including local overrides.
|
|
23
|
+
* Use `solid` for Ionic's default design, or `null` (also the omitted default) for the iOS theme's glass design.
|
|
24
|
+
* Applies when fill is omitted or `default`, outside ion-buttons. Inside ion-buttons the default is clear.
|
|
25
|
+
* Explicit clear, solid and outline take precedence; clear and outline cannot be configured as defaults.
|
|
26
|
+
* Source elements and Web clones are unchanged.
|
|
20
27
|
*/
|
|
21
28
|
buttonDefaultFill?: 'solid' | null;
|
|
22
29
|
}
|
package/src/native/index.ts
CHANGED
|
@@ -164,12 +164,12 @@ export const setVerticalControlAreaPlacement = (placement: VerticalBarEdge | Ver
|
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
166
|
* Call once after ion-app is mounted. Ionic markup remains the source of truth.
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* This default affects native vertical projection only; explicit button fills win.
|
|
167
|
+
* Native vertical buttons default to SwiftUI appearance.
|
|
168
|
+
* See VerticalControlAreaOptions for source styling and local overrides.
|
|
170
169
|
*/
|
|
171
170
|
export const enableVerticalControlArea = async (options: VerticalControlAreaOptions = {}): Promise<VerticalControlAreaHandle> => {
|
|
172
171
|
const handle = await enableNativeUIShell({
|
|
172
|
+
buttonProjection: options.buttonProjection,
|
|
173
173
|
buttonDefaultFill: options.buttonDefaultFill,
|
|
174
174
|
controls: { tabs: true, toolbar: true },
|
|
175
175
|
verticalBarsOnly: true,
|
|
@@ -199,6 +199,8 @@ export const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise
|
|
|
199
199
|
const controls = options.controls;
|
|
200
200
|
const configuration = JSON.stringify([
|
|
201
201
|
options.verticalBarsOnly === true,
|
|
202
|
+
options.buttonProjection ?? 'system',
|
|
203
|
+
// Local source overrides can use this even when the startup projection is system.
|
|
202
204
|
options.buttonDefaultFill ?? null,
|
|
203
205
|
...(['tabs', 'toolbar', 'segment', 'fab'] as const).map((component) => !controls || controls[component] === true),
|
|
204
206
|
]);
|
|
@@ -3,6 +3,7 @@ import { frame, isDark, isVerticalBarsSource, text, visible } from './dom';
|
|
|
3
3
|
import { iconSource } from './icons';
|
|
4
4
|
|
|
5
5
|
export interface Candidate {
|
|
6
|
+
buttonProjection?: VerticalControlAreaOptions['buttonProjection'];
|
|
6
7
|
buttonDefaultFill?: VerticalControlAreaOptions['buttonDefaultFill'];
|
|
7
8
|
element: HTMLElement;
|
|
8
9
|
sources?: HTMLElement[];
|
|
@@ -24,6 +25,7 @@ export const createCandidate = (
|
|
|
24
25
|
const style = getComputedStyle(element);
|
|
25
26
|
return {
|
|
26
27
|
element,
|
|
28
|
+
buttonProjection: options.buttonProjection,
|
|
27
29
|
buttonDefaultFill: options.buttonDefaultFill,
|
|
28
30
|
control: {
|
|
29
31
|
id: id(element),
|
|
@@ -62,11 +64,23 @@ export const appendItem = (
|
|
|
62
64
|
const badge = child.querySelector<HTMLElement>('ion-badge');
|
|
63
65
|
const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : undefined;
|
|
64
66
|
const sourceFill = (child as HTMLIonButtonElement).fill;
|
|
67
|
+
const projectionOwner = [child, child.closest('ion-buttons')].find((element) =>
|
|
68
|
+
element?.matches('.ios-theme-projection-source, .ios-theme-projection-system'),
|
|
69
|
+
);
|
|
70
|
+
const projection = projectionOwner
|
|
71
|
+
? projectionOwner.classList.contains('ios-theme-projection-system')
|
|
72
|
+
? 'system'
|
|
73
|
+
: 'source'
|
|
74
|
+
: candidate.buttonProjection;
|
|
75
|
+
const systemButton = projection !== 'source' && isVerticalBarsSource(child) && child.matches('ion-button, ion-menu-button');
|
|
65
76
|
// Ionic defaults buttons inside ion-buttons to clear, even when the app defaults to solid.
|
|
66
77
|
const defaultFill = child.closest('ion-buttons') ? 'clear' : candidate.buttonDefaultFill;
|
|
67
78
|
const fill = !sourceFill || sourceFill === 'default' ? defaultFill : sourceFill;
|
|
68
79
|
const buttonFill =
|
|
69
|
-
|
|
80
|
+
!systemButton &&
|
|
81
|
+
isVerticalBarsSource(child) &&
|
|
82
|
+
child.matches('ion-button') &&
|
|
83
|
+
(fill === 'clear' || fill === 'solid' || fill === 'outline')
|
|
70
84
|
? fill
|
|
71
85
|
: undefined;
|
|
72
86
|
const outline = buttonFill === 'outline' ? getComputedStyle(native ?? child) : undefined;
|
|
@@ -86,7 +100,8 @@ export const appendItem = (
|
|
|
86
100
|
selected: !!(child as ItemElement).selected,
|
|
87
101
|
fontSize: parseFloat(labelStyle.fontSize),
|
|
88
102
|
fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,
|
|
89
|
-
color: getComputedStyle(native ?? child).color,
|
|
103
|
+
color: systemButton ? 'currentColor' : getComputedStyle(native ?? child).color,
|
|
104
|
+
iconTemplate: systemButton ? true : undefined,
|
|
90
105
|
buttonFill,
|
|
91
106
|
backgroundColor: buttonFill === 'solid' ? getComputedStyle(native ?? child).backgroundColor : undefined,
|
|
92
107
|
borderColor: outline?.borderTopColor,
|