@rdlabo/ionic-theme-ios27 1.2.0-beta.pr242.sha99c4a2ec4d6a → 1.2.0-beta.pr246.sha237333ceac21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +8 -7
  2. package/dist/css/ionic-theme-ios27.css +1 -1
  3. package/dist/css/vertical-bars.css +1 -1
  4. package/dist/native/components/ion-buttons.js +1 -1
  5. package/dist/native/components/ion-buttons.js.map +1 -1
  6. package/dist/native/components/ion-menu-button.d.ts +2 -1
  7. package/dist/native/components/ion-menu-button.d.ts.map +1 -1
  8. package/dist/native/components/ion-menu-button.js +2 -2
  9. package/dist/native/components/ion-menu-button.js.map +1 -1
  10. package/dist/native/definitions.d.ts +1 -0
  11. package/dist/native/definitions.d.ts.map +1 -1
  12. package/dist/native/index.d.ts.map +1 -1
  13. package/dist/native/index.js +4 -3
  14. package/dist/native/index.js.map +1 -1
  15. package/dist/native/prehide.js +2 -0
  16. package/dist/native/prehide.js.map +1 -1
  17. package/dist/native/shared/candidate.d.ts +1 -0
  18. package/dist/native/shared/candidate.d.ts.map +1 -1
  19. package/dist/native/shared/candidate.js +7 -2
  20. package/dist/native/shared/candidate.js.map +1 -1
  21. package/dist/native/shared/dom.d.ts +1 -1
  22. package/dist/native/shared/dom.js +4 -4
  23. package/dist/native/shared/dom.js.map +1 -1
  24. package/docs/features.md +2 -2
  25. package/docs/iphone-duo-with-original-theme.md +20 -18
  26. package/docs/iphone-duo.md +11 -158
  27. package/docs/native-ui-shell.md +15 -13
  28. package/docs/special-markup.md +4 -2
  29. package/docs/vertical-bars.md +283 -0
  30. package/package.json +2 -2
  31. package/src/native/components/ion-buttons.ts +1 -1
  32. package/src/native/components/ion-menu-button.ts +3 -2
  33. package/src/native/definitions.ts +14 -6
  34. package/src/native/index.ts +5 -3
  35. package/src/native/prehide.ts +3 -1
  36. package/src/native/shared/candidate.ts +21 -2
  37. package/src/native/shared/dom.ts +4 -4
  38. package/src/styles/vertical-bars.scss +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"dom.js","names":[],"sources":["../../../src/native/shared/dom.ts"],"sourcesContent":["import { fadeMarker } from './crossfade';\nimport { modalUsesVerticalBars } from './modal';\n\nimport type { Frame } from '../definitions';\n\nexport const marker = 'data-native-ui-shell';\n// Scoped runtimes (Stencil slot emulation) patch child accessors on upgraded\n// elements and may report none before hydration; the prototype getter still\n// reports the real light DOM.\nconst childNodesGet = typeof Node === 'undefined' ? undefined : Object.getOwnPropertyDescriptor(Node.prototype, 'childNodes')?.get;\nexport const childNodesOf = (element: Element): Node[] =>\n childNodesGet ? Array.from(childNodesGet.call(element)) : Array.from(element.childNodes);\nexport const childElements = (element: Element): HTMLElement[] =>\n childNodesOf(element).filter((node) => node.nodeType === 1) as HTMLElement[];\nexport const prehiddenClass = 'ios-theme-native-ui-shell-prehidden';\nexport const prehideRootClass = 'ios-theme-native-ui-shell-prehide';\nexport const rejectedClass = 'ios-theme-native-ui-shell-rejected';\nexport const verticalBarsBackWebClass = 'ios-theme-vertical-bars-back-web-owned';\nconst prehideClasses = new Set([prehiddenClass, prehideRootClass]);\nexport const prehideOnlyMutation = (record: MutationRecord): boolean => {\n if (record.attributeName !== 'class' || record.oldValue === null) return false;\n const withoutPrehide = (value: string) =>\n value\n .split(/\\s+/)\n .filter((name) => name && !prehideClasses.has(name))\n .join(' ');\n return withoutPrehide(record.oldValue) === withoutPrehide((record.target as Element).getAttribute('class') ?? '');\n};\n\nexport const withoutPrehide = <T>(element: HTMLElement, read: () => T): T => {\n const changed: HTMLElement[] = [];\n for (let current: HTMLElement | null = element; current; current = current.parentElement) {\n if (current.classList.contains(prehiddenClass)) {\n current.classList.remove(prehiddenClass);\n changed.push(current);\n }\n }\n const root = element.ownerDocument.documentElement;\n if (element.matches('ion-back-button') && root.classList.contains(prehideRootClass)) {\n root.classList.remove(prehideRootClass);\n changed.push(root);\n }\n try {\n return read();\n } finally {\n changed.forEach((current) => current.classList.add(current === root ? prehideRootClass : prehiddenClass));\n }\n};\nexport const isDark = (style: CSSStyleDeclaration): boolean => {\n const themeScheme = style.getPropertyValue('--ios27-color-scheme').trim();\n if (themeScheme) return themeScheme === 'dark';\n const background = style.getPropertyValue('--ion-background-color-rgb').match(/\\d+/g)?.slice(0, 3).map(Number);\n return !!background && background.length === 3 && background[0] * 0.2126 + background[1] * 0.7152 + background[2] * 0.0722 < 128;\n};\nconst permanentlyExcluded = '.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled, .ion-cloned-element, [hidden], [inert]';\nexport const excluded = `${permanentlyExcluded}, .ion-page-hidden, .ion-page-invisible`;\nconst enteringPages = new WeakSet<HTMLElement>();\nexport const setVerticalBarsEnteringPage = (page: HTMLElement, entering: boolean): void => {\n if (entering) enteringPages.add(page);\n else enteringPages.delete(page);\n};\nexport const verticalBarsEnteringPage = (element: HTMLElement): HTMLElement | undefined => {\n const page = element.closest<HTMLElement>('.ion-page-invisible');\n return page && enteringPages.has(page) && page.closest('ion-app.ios-theme-vertical-bars') ? page : undefined;\n};\nexport const createVerticalBarsPageState = () => {\n const departed = new WeakSet<HTMLElement>();\n return {\n isDeparted(element: HTMLElement): boolean {\n const page = element.closest<HTMLElement>('.ion-page');\n return !!page && departed.has(page) && !!element.closest('ion-app.ios-theme-vertical-bars');\n },\n lifecycle(event: Event): void {\n // Avoid instanceof so stale listeners stay safe after global teardown.\n const page = event.target && (event.target as Node).nodeType === 1 ? (event.target as HTMLElement) : undefined;\n if (!page?.matches('.ion-page')) return;\n const entering = event.type === 'ionViewWillEnter';\n if (entering || event.type === 'ionViewDidEnter') departed.delete(page);\n else if (event.type === 'ionViewWillLeave' || event.type === 'ionViewDidLeave') departed.add(page);\n setVerticalBarsEnteringPage(page, entering);\n },\n cancel(entering?: HTMLElement, leaving?: HTMLElement): void {\n if (entering) {\n departed.add(entering);\n setVerticalBarsEnteringPage(entering, false);\n }\n if (leaving) departed.delete(leaving);\n },\n };\n};\nconst disabledButtonGroup = 'ion-buttons:is(.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled)';\nconst shellDisabledSelector = '.ios-theme-shell-disabled';\n\nexport const isDisabledButtonGroupChild = (element: HTMLElement): boolean =>\n element.matches('ion-button') && element.parentElement?.matches(disabledButtonGroup) === true;\n\nconst verticalBarsTags = new Set(['ion-button', 'ion-back-button', 'ion-buttons', 'ion-menu-button', 'ion-tab-bar']);\n\nexport const isVerticalBarsSource = (element: HTMLElement): boolean =>\n verticalBarsTags.has(element.localName) &&\n (element.matches('ion-tab-bar') || verticalBarsOwned(element)) &&\n !element.closest('ion-menu, ion-popover') &&\n modalUsesVerticalBars(element) &&\n !!element.closest('ion-app.ios-theme-vertical-bars');\n\nexport const isVerticalBarsBackPosition = (element: HTMLElement): boolean => {\n if (element.closest('ion-header[collapse], ion-footer[collapse]')) return false;\n const page = element.closest('.ion-page');\n if (!page) return true;\n const candidates = Array.from(page.querySelectorAll<HTMLElement>('ion-back-button')).filter(\n (back) =>\n back.closest('.ion-page') === page &&\n !back.matches('.ion-cloned-element') &&\n !back.closest('ion-header[collapse], ion-footer[collapse], ion-menu, ion-popover') &&\n modalUsesVerticalBars(back),\n );\n const rank = (back: HTMLElement) =>\n inFixedToolbar(back) ? 0 : back.closest('ion-header ion-toolbar, ion-footer ion-toolbar') ? 1 : back.closest('ion-toolbar') ? 3 : 2;\n const best = candidates.reduce<HTMLElement | undefined>(\n (winner, back) => (!winner || rank(back) < rank(winner) ? back : winner),\n undefined,\n );\n return best === element;\n};\n\nexport const preferredVerticalBarsBack = <T extends HTMLElement>(elements: T[], doc: Document): T | undefined => {\n const pages = Array.from(doc.querySelectorAll('.ion-page'));\n const pageOrder = (element: Element) => pages.indexOf(element.closest('.ion-page')!);\n return elements.sort((a, b) => pageOrder(b) - pageOrder(a) || Number(!!b.closest('ion-header')) - Number(!!a.closest('ion-header')))[0];\n};\n\nconst excludedBy = (element: HTMLElement, selector: string): boolean => {\n const owner = element.closest<HTMLElement>(selector);\n return !!owner && !(element.parentElement === owner && isDisabledButtonGroupChild(element));\n};\n\nexport const isPermanentlyExcluded = (element: HTMLElement): boolean => excludedBy(element, permanentlyExcluded);\nexport const isExcluded = (element: HTMLElement, enteringPage?: HTMLElement): boolean =>\n excludedBy(element, `${permanentlyExcluded}, .ion-page-hidden`) || (!!element.closest('.ion-page-invisible') && !enteringPage);\n\n// A shared native surface must not cover an opted-out descendant either.\nexport const isShellDisabled = (element: Element): boolean =>\n !!element.closest(shellDisabledSelector) || !!element.querySelector(shellDisabledSelector);\n\nexport const isVerticalBarsToolbarActionShape = (element: HTMLElement): boolean =>\n element.matches('ion-menu-button') ||\n (element.matches('ion-button') && !!element.querySelector('ion-icon[slot=\"icon-only\"], svg[slot=\"icon-only\"]'));\n\n// Placement belongs to the DOM identity for one routed-page epoch. Changes to\n// content/disabled state affect rendering, never its chosen surface.\nconst verticalBarsPlacement = new WeakMap<HTMLElement, boolean>();\nexport const setVerticalBarsPlacement = (element: HTMLElement, rail: boolean): void => {\n verticalBarsPlacement.set(element, rail);\n};\nexport const clearVerticalBarsPlacement = (element: HTMLElement): void => {\n verticalBarsPlacement.delete(element);\n};\nexport const verticalBarsOwned = (element: HTMLElement): boolean => verticalBarsPlacement.get(element) === true;\n\nexport const isVerticalBarsToolbarAction = (element: HTMLElement): boolean =>\n verticalBarsOwned(element) && !isExcluded(element, verticalBarsEnteringPage(element)) && !isShellDisabled(element);\n\nconst overlays = 'ion-menu, ion-popover';\n\n/** Shared eligibility for a toolbar action candidate, before placement is decided (prehide) or consumed (projection). */\nexport const verticalBarsActionCandidate = (element: HTMLElement): boolean =>\n inFixedToolbar(element) &&\n !element.closest('ion-buttons.ios-theme-horizontal-only, ion-button.ios-theme-horizontal-only') &&\n !isPermanentlyExcluded(element) &&\n !isShellDisabled(element) &&\n !element.closest(overlays) &&\n modalUsesVerticalBars(element);\n\n/** Shared eligibility for a back-button candidate; the back button may sit outside a fixed toolbar. */\nexport const verticalBarsBackCandidate = (element: HTMLElement): boolean =>\n isVerticalBarsBackPosition(element) &&\n !element.closest('ion-buttons.ios-theme-horizontal-only') &&\n !isPermanentlyExcluded(element) &&\n !isShellDisabled(element) &&\n !element.closest(overlays) &&\n modalUsesVerticalBars(element);\n\nexport const verticalBarsToolbarActions = (element: HTMLElement): HTMLElement[] =>\n // Back buttons use the dedicated rail slot; they are never toolbar actions.\n childElements(element).filter((child) => !child.matches('ion-back-button') && isVerticalBarsToolbarAction(child));\n\nexport const isVerticalBarsToolbarGroup = (element: HTMLElement): boolean => {\n return (\n element.matches('ion-buttons') &&\n verticalBarsOwned(element) &&\n !element.matches('.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled') &&\n !isShellDisabled(element)\n );\n};\n\nexport const activateProjectedElement = (element: HTMLElement): void => {\n const target = element.matches('ion-button, ion-back-button, ion-menu-button')\n ? element.shadowRoot?.querySelector<HTMLElement>('[part~=\"native\"]')\n : undefined;\n (target ?? element).click();\n};\n\nexport const unprojected = <T>(elements: Iterable<HTMLElement>, read: () => T): T => {\n const hidden = Array.from(elements).filter((element) => element.hasAttribute(marker));\n hidden.forEach((element) => element.removeAttribute(marker));\n try {\n return read();\n } finally {\n hidden.forEach((element) => element.setAttribute(marker, ''));\n }\n};\n\nconst readVisible = (element: HTMLElement, allowOutsideViewport: boolean): boolean => {\n const enteringPage = isVerticalBarsSource(element) ? verticalBarsEnteringPage(element) : undefined;\n if (!element.isConnected || isExcluded(element, enteringPage) || isShellDisabled(element)) return false;\n for (let current: HTMLElement | null = element; current; current = current.parentElement) {\n const style = getComputedStyle(current);\n if (\n style.display === 'none' ||\n style.visibility !== 'visible' ||\n (Number(style.opacity) === 0 && !current.hasAttribute(fadeMarker) && current !== enteringPage)\n )\n return false;\n // Ordinary controls on moving/collapsing/custom transformed surfaces stay in Web coordinates.\n // VerticalBars rail controls are placed independently of their Web coordinates and must remain\n // owned while Ionic transforms the content behind an open menu.\n if (\n !allowOutsideViewport &&\n style.transform !== 'none' &&\n !new DOMMatrixReadOnly(style.transform).isIdentity &&\n current !== element &&\n !current.matches('ion-tab-bar')\n )\n return false;\n }\n const rect = element.getBoundingClientRect();\n return (\n rect.width > 0 &&\n rect.height > 0 &&\n (allowOutsideViewport || (rect.left >= -1 && rect.top >= -1 && rect.right <= innerWidth + 1 && rect.bottom <= innerHeight + 1))\n );\n};\n\nexport const visible = (element: HTMLElement, allowOutsideViewport = false): boolean =>\n withoutPrehide(element, () => readVisible(element, allowOutsideViewport));\n\nexport const frame = (rect: DOMRect, origin?: DOMRect): Frame => ({\n x: rect.x - (origin?.x ?? 0),\n y: rect.y - (origin?.y ?? 0),\n width: rect.width,\n height: rect.height,\n});\n\nexport const text = (element: Element): string => {\n if (element.matches('ion-icon, svg, ion-badge, .ios27-segment-lens, .ion-cloned-element')) return '';\n return childNodesOf(element)\n .map((node) => (node.nodeType === 3 ? node.textContent : node.nodeType === 1 ? text(node as Element) : ''))\n .join('')\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nexport const inFixedToolbar = (element: Element): boolean => {\n const edge = element.closest('ion-toolbar')?.parentElement;\n const verticalBars = !!element.closest('ion-app.ios-theme-vertical-bars');\n return (\n !!edge?.matches('ion-header, ion-footer') &&\n !element.closest('ion-content') &&\n !edge.hasAttribute('collapse') &&\n (verticalBars || !edge.matches('.header-collapse-main, .header-collapse-condense'))\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"dom.js","names":[],"sources":["../../../src/native/shared/dom.ts"],"sourcesContent":["import { fadeMarker } from './crossfade';\nimport { modalUsesVerticalBars } from './modal';\n\nimport type { Frame } from '../definitions';\n\nexport const marker = 'data-native-ui-shell';\n// Scoped runtimes (Stencil slot emulation) patch child accessors on upgraded\n// elements and may report none before hydration; the prototype getter still\n// reports the real light DOM.\nconst childNodesGet = typeof Node === 'undefined' ? undefined : Object.getOwnPropertyDescriptor(Node.prototype, 'childNodes')?.get;\nexport const childNodesOf = (element: Element): Node[] =>\n childNodesGet ? Array.from(childNodesGet.call(element)) : Array.from(element.childNodes);\nexport const childElements = (element: Element): HTMLElement[] =>\n childNodesOf(element).filter((node) => node.nodeType === 1) as HTMLElement[];\nexport const prehiddenClass = 'ios-theme-native-ui-shell-prehidden';\nexport const prehideRootClass = 'ios-theme-native-ui-shell-prehide';\nexport const rejectedClass = 'ios-theme-native-ui-shell-rejected';\nexport const verticalBarsBackWebClass = 'ios-theme-vertical-bars-back-web-owned';\nconst prehideClasses = new Set([prehiddenClass, prehideRootClass]);\nexport const prehideOnlyMutation = (record: MutationRecord): boolean => {\n if (record.attributeName !== 'class' || record.oldValue === null) return false;\n const withoutPrehide = (value: string) =>\n value\n .split(/\\s+/)\n .filter((name) => name && !prehideClasses.has(name))\n .join(' ');\n return withoutPrehide(record.oldValue) === withoutPrehide((record.target as Element).getAttribute('class') ?? '');\n};\n\nexport const withoutPrehide = <T>(element: HTMLElement, read: () => T): T => {\n const changed: HTMLElement[] = [];\n for (let current: HTMLElement | null = element; current; current = current.parentElement) {\n if (current.classList.contains(prehiddenClass)) {\n current.classList.remove(prehiddenClass);\n changed.push(current);\n }\n }\n const root = element.ownerDocument.documentElement;\n if (element.matches('ion-back-button') && root.classList.contains(prehideRootClass)) {\n root.classList.remove(prehideRootClass);\n changed.push(root);\n }\n try {\n return read();\n } finally {\n changed.forEach((current) => current.classList.add(current === root ? prehideRootClass : prehiddenClass));\n }\n};\nexport const isDark = (style: CSSStyleDeclaration): boolean => {\n const themeScheme = style.getPropertyValue('--ios27-color-scheme').trim();\n if (themeScheme) return themeScheme === 'dark';\n const background = style.getPropertyValue('--ion-background-color-rgb').match(/\\d+/g)?.slice(0, 3).map(Number);\n return !!background && background.length === 3 && background[0] * 0.2126 + background[1] * 0.7152 + background[2] * 0.0722 < 128;\n};\nconst permanentlyExcluded = '.ios-theme-disabled, .ios26-disabled, .ion-cloned-element, [hidden], [inert]';\nexport const excluded = `${permanentlyExcluded}, .ion-page-hidden, .ion-page-invisible`;\nconst enteringPages = new WeakSet<HTMLElement>();\nexport const setVerticalBarsEnteringPage = (page: HTMLElement, entering: boolean): void => {\n if (entering) enteringPages.add(page);\n else enteringPages.delete(page);\n};\nexport const verticalBarsEnteringPage = (element: HTMLElement): HTMLElement | undefined => {\n const page = element.closest<HTMLElement>('.ion-page-invisible');\n return page && enteringPages.has(page) && page.closest('ion-app.ios-theme-vertical-bars') ? page : undefined;\n};\nexport const createVerticalBarsPageState = () => {\n const departed = new WeakSet<HTMLElement>();\n return {\n isDeparted(element: HTMLElement): boolean {\n const page = element.closest<HTMLElement>('.ion-page');\n return !!page && departed.has(page) && !!element.closest('ion-app.ios-theme-vertical-bars');\n },\n lifecycle(event: Event): void {\n // Avoid instanceof so stale listeners stay safe after global teardown.\n const page = event.target && (event.target as Node).nodeType === 1 ? (event.target as HTMLElement) : undefined;\n if (!page?.matches('.ion-page')) return;\n const entering = event.type === 'ionViewWillEnter';\n if (entering || event.type === 'ionViewDidEnter') departed.delete(page);\n else if (event.type === 'ionViewWillLeave' || event.type === 'ionViewDidLeave') departed.add(page);\n setVerticalBarsEnteringPage(page, entering);\n },\n cancel(entering?: HTMLElement, leaving?: HTMLElement): void {\n if (entering) {\n departed.add(entering);\n setVerticalBarsEnteringPage(entering, false);\n }\n if (leaving) departed.delete(leaving);\n },\n };\n};\nconst disabledButtonGroup = 'ion-buttons:is(.ios-theme-disabled, .ios26-disabled)';\nconst shellDisabledSelector = '.ios-theme-shell-disabled, [data-shell=\"disabled\"]';\n\nexport const isDisabledButtonGroupChild = (element: HTMLElement): boolean =>\n element.matches('ion-button') && element.parentElement?.matches(disabledButtonGroup) === true;\n\nconst verticalBarsTags = new Set(['ion-button', 'ion-back-button', 'ion-buttons', 'ion-menu-button', 'ion-tab-bar']);\n\nexport const isVerticalBarsSource = (element: HTMLElement): boolean =>\n verticalBarsTags.has(element.localName) &&\n (element.matches('ion-tab-bar') || verticalBarsOwned(element)) &&\n !element.closest('ion-menu, ion-popover') &&\n modalUsesVerticalBars(element) &&\n !!element.closest('ion-app.ios-theme-vertical-bars');\n\nexport const isVerticalBarsBackPosition = (element: HTMLElement): boolean => {\n if (element.closest('ion-header[collapse], ion-footer[collapse]')) return false;\n const page = element.closest('.ion-page');\n if (!page) return true;\n const candidates = Array.from(page.querySelectorAll<HTMLElement>('ion-back-button')).filter(\n (back) =>\n back.closest('.ion-page') === page &&\n !back.matches('.ion-cloned-element') &&\n !back.closest('ion-header[collapse], ion-footer[collapse], ion-menu, ion-popover') &&\n modalUsesVerticalBars(back),\n );\n const rank = (back: HTMLElement) =>\n inFixedToolbar(back) ? 0 : back.closest('ion-header ion-toolbar, ion-footer ion-toolbar') ? 1 : back.closest('ion-toolbar') ? 3 : 2;\n const best = candidates.reduce<HTMLElement | undefined>(\n (winner, back) => (!winner || rank(back) < rank(winner) ? back : winner),\n undefined,\n );\n return best === element;\n};\n\nexport const preferredVerticalBarsBack = <T extends HTMLElement>(elements: T[], doc: Document): T | undefined => {\n const pages = Array.from(doc.querySelectorAll('.ion-page'));\n const pageOrder = (element: Element) => pages.indexOf(element.closest('.ion-page')!);\n return elements.sort((a, b) => pageOrder(b) - pageOrder(a) || Number(!!b.closest('ion-header')) - Number(!!a.closest('ion-header')))[0];\n};\n\nconst excludedBy = (element: HTMLElement, selector: string): boolean => {\n const owner = element.closest<HTMLElement>(selector);\n return !!owner && !(element.parentElement === owner && isDisabledButtonGroupChild(element));\n};\n\nexport const isPermanentlyExcluded = (element: HTMLElement): boolean => excludedBy(element, permanentlyExcluded);\nexport const isExcluded = (element: HTMLElement, enteringPage?: HTMLElement): boolean =>\n excludedBy(element, `${permanentlyExcluded}, .ion-page-hidden`) || (!!element.closest('.ion-page-invisible') && !enteringPage);\n\n// A shared native surface must not cover an opted-out descendant either.\nexport const isShellDisabled = (element: Element): boolean =>\n !!element.closest(shellDisabledSelector) || !!element.querySelector(shellDisabledSelector);\n\nexport const isVerticalBarsToolbarActionShape = (element: HTMLElement): boolean =>\n element.matches('ion-menu-button') ||\n (element.matches('ion-button') && !!element.querySelector('ion-icon[slot=\"icon-only\"], svg[slot=\"icon-only\"]'));\n\n// Placement belongs to the DOM identity for one routed-page epoch. Changes to\n// content/disabled state affect rendering, never its chosen surface.\nconst verticalBarsPlacement = new WeakMap<HTMLElement, boolean>();\nexport const setVerticalBarsPlacement = (element: HTMLElement, rail: boolean): void => {\n verticalBarsPlacement.set(element, rail);\n};\nexport const clearVerticalBarsPlacement = (element: HTMLElement): void => {\n verticalBarsPlacement.delete(element);\n};\nexport const verticalBarsOwned = (element: HTMLElement): boolean => verticalBarsPlacement.get(element) === true;\n\nexport const isVerticalBarsToolbarAction = (element: HTMLElement): boolean =>\n verticalBarsOwned(element) && !isExcluded(element, verticalBarsEnteringPage(element)) && !isShellDisabled(element);\n\nconst overlays = 'ion-menu, ion-popover';\n\n/** Shared eligibility for a toolbar action candidate, before placement is decided (prehide) or consumed (projection). */\nexport const verticalBarsActionCandidate = (element: HTMLElement): boolean =>\n inFixedToolbar(element) &&\n !element.closest('ion-buttons.ios-theme-horizontal-only, ion-button.ios-theme-horizontal-only') &&\n !isPermanentlyExcluded(element) &&\n !isShellDisabled(element) &&\n !element.closest(overlays) &&\n modalUsesVerticalBars(element);\n\n/** Shared eligibility for a back-button candidate; the back button may sit outside a fixed toolbar. */\nexport const verticalBarsBackCandidate = (element: HTMLElement): boolean =>\n isVerticalBarsBackPosition(element) &&\n !element.closest('ion-buttons.ios-theme-horizontal-only') &&\n !isPermanentlyExcluded(element) &&\n !isShellDisabled(element) &&\n !element.closest(overlays) &&\n modalUsesVerticalBars(element);\n\nexport const verticalBarsToolbarActions = (element: HTMLElement): HTMLElement[] =>\n // Back buttons use the dedicated rail slot; they are never toolbar actions.\n childElements(element).filter((child) => !child.matches('ion-back-button') && isVerticalBarsToolbarAction(child));\n\nexport const isVerticalBarsToolbarGroup = (element: HTMLElement): boolean => {\n return (\n element.matches('ion-buttons') &&\n verticalBarsOwned(element) &&\n !element.matches('.ios-theme-disabled, .ios26-disabled') &&\n !isShellDisabled(element)\n );\n};\n\nexport const activateProjectedElement = (element: HTMLElement): void => {\n const target = element.matches('ion-button, ion-back-button, ion-menu-button')\n ? element.shadowRoot?.querySelector<HTMLElement>('[part~=\"native\"]')\n : undefined;\n (target ?? element).click();\n};\n\nexport const unprojected = <T>(elements: Iterable<HTMLElement>, read: () => T): T => {\n const hidden = Array.from(elements).filter((element) => element.hasAttribute(marker));\n hidden.forEach((element) => element.removeAttribute(marker));\n try {\n return read();\n } finally {\n hidden.forEach((element) => element.setAttribute(marker, ''));\n }\n};\n\nconst readVisible = (element: HTMLElement, allowOutsideViewport: boolean): boolean => {\n const enteringPage = isVerticalBarsSource(element) ? verticalBarsEnteringPage(element) : undefined;\n if (!element.isConnected || isExcluded(element, enteringPage) || isShellDisabled(element)) return false;\n for (let current: HTMLElement | null = element; current; current = current.parentElement) {\n const style = getComputedStyle(current);\n if (\n style.display === 'none' ||\n style.visibility !== 'visible' ||\n (Number(style.opacity) === 0 && !current.hasAttribute(fadeMarker) && current !== enteringPage)\n )\n return false;\n // Ordinary controls on moving/collapsing/custom transformed surfaces stay in Web coordinates.\n // VerticalBars rail controls are placed independently of their Web coordinates and must remain\n // owned while Ionic transforms the content behind an open menu.\n if (\n !allowOutsideViewport &&\n style.transform !== 'none' &&\n !new DOMMatrixReadOnly(style.transform).isIdentity &&\n current !== element &&\n !current.matches('ion-tab-bar')\n )\n return false;\n }\n const rect = element.getBoundingClientRect();\n return (\n rect.width > 0 &&\n rect.height > 0 &&\n (allowOutsideViewport || (rect.left >= -1 && rect.top >= -1 && rect.right <= innerWidth + 1 && rect.bottom <= innerHeight + 1))\n );\n};\n\nexport const visible = (element: HTMLElement, allowOutsideViewport = false): boolean =>\n withoutPrehide(element, () => readVisible(element, allowOutsideViewport));\n\nexport const frame = (rect: DOMRect, origin?: DOMRect): Frame => ({\n x: rect.x - (origin?.x ?? 0),\n y: rect.y - (origin?.y ?? 0),\n width: rect.width,\n height: rect.height,\n});\n\nexport const text = (element: Element): string => {\n if (element.matches('ion-icon, svg, ion-badge, .ios27-segment-lens, .ion-cloned-element')) return '';\n return childNodesOf(element)\n .map((node) => (node.nodeType === 3 ? node.textContent : node.nodeType === 1 ? text(node as Element) : ''))\n .join('')\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nexport const inFixedToolbar = (element: Element): boolean => {\n const edge = element.closest('ion-toolbar')?.parentElement;\n const verticalBars = !!element.closest('ion-app.ios-theme-vertical-bars');\n return (\n !!edge?.matches('ion-header, ion-footer') &&\n !element.closest('ion-content') &&\n !edge.hasAttribute('collapse') &&\n (verticalBars || !edge.matches('.header-collapse-main, .header-collapse-condense'))\n );\n};\n"],"mappings":"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"}
package/docs/features.md CHANGED
@@ -27,9 +27,9 @@ ion-textarea label.textarea-wrapper {
27
27
  }
28
28
  ```
29
29
 
30
- ## Native UI Shell (Experimental)
30
+ ## Native UI Shell (Preview)
31
31
 
32
- Capacitor iOS apps can use the optional, experimental [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell) to render supported fixed Ionic controls with UIKit and system Liquid Glass. Content and application logic stay in the WebView, with Ionic owning routing and page transitions. The guide explains the hybrid approach's origins in Basecamp and Capacitor, setup, supported controls and Web fallback behavior.
32
+ Capacitor iOS apps can use the optional [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell) to render supported fixed Ionic controls with UIKit and system Liquid Glass. It is available as a preview in `1.2.0`. Content and application logic stay in the WebView, with Ionic owning routing and page transitions. The guide explains the hybrid approach's origins in Basecamp and Capacitor, setup, supported controls and Web fallback behavior.
33
33
 
34
34
  ## Selective component imports
35
35
 
@@ -1,14 +1,14 @@
1
1
  ---
2
- title: iPhone Duo with your existing theme (experimental)
2
+ title: iPhone Duo with your existing theme (preview)
3
3
  ---
4
4
 
5
- # iPhone Duo with your existing theme (experimental)
5
+ # iPhone Duo with your existing theme (preview)
6
6
 
7
7
  Add a vertical navigation area to your Ionic app while keeping its existing theme. Tabs and supported toolbar actions move to the side of the screen; your content and horizontal controls keep their current appearance. Both Ionic `ios` and `md` modes are supported.
8
8
 
9
9
  **Try it in Chrome first.** You can preview the layout with Web controls before setting up an iPhone Duo or an iOS build. On supported Capacitor iOS, the same Ionic markup supplies native SwiftUI controls in the system rail.
10
10
 
11
- Available in `1.2.0-0` as an **experimental** feature. APIs and supported behavior may change.
11
+ Available in `1.2.0` as a **preview** feature. APIs and supported behavior may change.
12
12
 
13
13
  ## Try it in your existing Ionic app
14
14
 
@@ -17,7 +17,7 @@ Available in `1.2.0-0` as an **experimental** feature. APIs and supported behavi
17
17
  This guide assumes an existing Ionic app with Ionic `>=8.8.1 <10` and Capacitor Core `>=8 <9`. Keep your existing Capacitor 8 installation. If your app uses another Capacitor major, migrate its Core, CLI, and platform packages together before following this guide. For a Web-only app without Capacitor, also install `@capacitor/core@^8`; the JavaScript entry point needs it even in Chrome.
18
18
 
19
19
  ```bash
20
- npm install @rdlabo/ionic-theme-ios27@1.2.0-1
20
+ npm install @rdlabo/ionic-theme-ios27@1.2.0
21
21
  ```
22
22
 
23
23
  Keep your existing theme imports. Add this to your global Sass file:
@@ -94,8 +94,6 @@ The adapter returns the original `Animation`, preserving its effects, duration,
94
94
 
95
95
  The adapter keeps the builder's animation targets, including any horizontal back-button effect. If you need the iOS 27 transition with that effect excluded in vertical layouts, use `iosTransitionAnimation` from `@rdlabo/ionic-theme-ios27` as your `navAnimation` instead. It already includes the adapter, so no wrapper is needed.
96
96
 
97
- `withNativeUIShellTransition()` is available in `1.2.0-1` and later.
98
-
99
97
  ### 4. Start the controls after the app root is mounted
100
98
 
101
99
  Call this once from your application startup after `ion-app` exists in the DOM:
@@ -106,27 +104,31 @@ import { enableVerticalControlArea } from '@rdlabo/ionic-theme-ios27/vertical-ba
106
104
  const rail = await enableVerticalControlArea();
107
105
  ```
108
106
 
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
107
  **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
108
 
119
109
  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
110
 
121
111
  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
112
 
113
+ ### Optional: choose native button appearance
114
+
115
+ `buttonProjection` and local projection settings are available in `1.2.0`. See [Choose button appearance](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#choose-button-appearance) for availability and migration details.
116
+
117
+ The new default is `system`: SwiftUI styles vertical buttons and tints their icons. If your existing theme should supply their fill and colors, use:
118
+
119
+ ```ts
120
+ const rail = await enableVerticalControlArea({ buttonProjection: 'source', buttonDefaultFill: 'solid' });
121
+ ```
122
+
123
+ 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 settings](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.
124
+
123
125
  ### If the preview does not appear
124
126
 
125
127
  | What you see | What to check |
126
128
  | --- | --- |
127
129
  | No space at the side | Load `vertical-bars.css` and put the class on `ion-app`. |
128
130
  | Space appears, but controls stay horizontal | Start `enableVerticalControlArea()` after mounting the app root. Use existing tabs or `slot="icon-only"` actions in a fixed header/footer toolbar. |
129
- | One action stays horizontal | Check for `slot="icon-only"` on the icon and a fixed toolbar outside scrolling content. Explicitly excluded controls and controls in centered modals stay horizontal; `fill` and `type="submit"` do not prevent movement. See [control requirements](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#toolbar-actions). |
131
+ | 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
132
 
131
133
  ## Connect an iPhone Duo
132
134
 
@@ -141,9 +143,9 @@ Use Capacitor 8.5 or later and build with Xcode 27.1 or newer for actual rail pl
141
143
 
142
144
  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
145
 
144
- Pass both the requested and native edge with Foldable's measured inset. The placement API resolves RTL. A null edge restores the ordinary layout. Keep `buttonDefaultFill: 'solid'` on `enableVerticalControlArea()` if your existing theme needs CSS background projection.
146
+ 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
147
 
146
- On supported iOS, controls in the rail use the system SwiftUI appearance; your custom Web styling still applies to ordinary content and horizontal controls. Web and Android use Web clones.
148
+ 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
149
 
148
150
  ## Use hinge posture without projecting controls
149
151
 
@@ -153,4 +155,4 @@ See [Read the device layout](https://docs.rdlabo.dev/projects/ionic-theme-ios27/
153
155
 
154
156
  ## Shared layout rules and API
155
157
 
156
- Safe-area handling, overlays, RTL, control eligibility, Web simulation, and the handle API are documented in [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo). Those rules apply to this standalone setup too.
158
+ 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.
@@ -1,14 +1,14 @@
1
1
  ---
2
- title: iPhone Duo support (experimental)
2
+ title: iPhone Duo support (preview)
3
3
  ---
4
4
 
5
- # iPhone Duo support (experimental)
5
+ # iPhone Duo support (preview)
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, split panes, and the API.
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
- 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.
11
+ Available in `1.2.0` as a **preview** feature alongside [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell). APIs and supported behavior may change before stable status. Stable status is planned after the official release of Xcode 27.1. 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
 
13
13
  This package provides two independent pieces for that hardware. Each works **without the iOS 27 theme stylesheets** and **without the full Native UI Shell**:
14
14
 
@@ -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 and API.
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
- 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:
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 above without connecting device placement.
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. Back navigation can come from outside a fixed toolbar; menu buttons and other toolbar actions still require one.
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
- 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`.
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
- 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.
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 generated reference below documents the handle returned by `enableVerticalControlArea()`.
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&lt;<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>&gt;</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** | () =&gt; Promise&lt;void&gt; | 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).
@@ -1,10 +1,10 @@
1
1
  ---
2
- title: Native UI Shell (Experimental)
2
+ title: Native UI Shell (Preview)
3
3
  ---
4
4
 
5
- # Native UI Shell (Experimental)
5
+ # Native UI Shell (Preview)
6
6
 
7
- Native UI Shell is experimental. Its API and supported controls may change.
7
+ Native UI Shell is available as a **preview** in `1.2.0`. Its API and supported controls may change before stable status. Stable status is planned after the official release of Xcode 27.1.
8
8
 
9
9
  Native UI Shell gives an Ionic app native navigation and action controls around its Web content. The optional Capacitor iOS plugin renders supported fixed Ionic controls with UIKit or SwiftUI and the system's Liquid Glass material. Page content, scrolling, application state and routing remain in Ionic's WebView.
10
10
 
@@ -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/iphone-duo#toolbar-actions).
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
  | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
@@ -52,12 +52,14 @@ The table below describes the ordinary Native UI Shell. Vertical Bars uses the s
52
52
  | `ion-segment` | Fixed toolbar, non-scrollable, text **or** one icon per item | `UISegmentedControl` |
53
53
  | `ion-fab` / `ion-fab-button` / `ion-fab-list` | Glass FAB in an `ion-content` fixed slot; one main button and optional directional lists | Persistent glass `UIButton` per button; one FAB synchronization group |
54
54
 
55
- For the ordinary Native UI Shell, only iOS-mode components with the theme variables installed are eligible. Explicitly enabled Vertical Bars is mode-independent as described below. `ionic-theme-disabled`, `ios-theme-disabled`, and the legacy `ios26-disabled` on an element or ancestor always exclude it. A disabled theme on one tab/segment item keeps its whole group on the Web.
55
+ For the ordinary Native UI Shell, only iOS-mode components with the theme variables installed are eligible. Explicitly enabled Vertical Bars is mode-independent as described below. `ios-theme-disabled` and the legacy `ios26-disabled` on an element or ancestor always exclude it. A disabled theme on one tab/segment item keeps its whole group on the Web.
56
56
 
57
- Use `ios-theme-shell-disabled` to disable only the iOS Native UI Shell while keeping the Web theme. It excludes the element and all its descendants. Adding or removing the class at runtime automatically restores Web rendering or re-evaluates native eligibility.
57
+ Use `data-shell="disabled"` (or the equivalent `ios-theme-shell-disabled` class) to disable only the iOS Native UI Shell while keeping the Web theme. It excludes the element and all its descendants. Adding or removing the attribute or class at runtime automatically restores Web rendering or re-evaluates native eligibility.
58
+
59
+ Only the exact value `disabled` opts out; an empty or unknown value is ignored. Remove the attribute to re-enable projection. If the class is also present, remove both to re-enable projection. This does not disable clicks or change the Web theme. The data attribute is available in `1.2.0`.
58
60
 
59
61
  ```html
60
- <ion-toolbar class="ios-theme-shell-disabled">
62
+ <ion-toolbar data-shell="disabled">
61
63
  <ion-button>Web glass button</ion-button>
62
64
  </ion-toolbar>
63
65
  ```
@@ -84,7 +86,7 @@ The DOM owns labels, SVG content, placement, selected values and application beh
84
86
 
85
87
  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
88
 
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, preserving their colors. Tab SVGs that follow text color use native template rendering, so icons and labels change selection color together without waiting for another bridge image. Explicit multicolor 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.
89
+ 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
90
 
89
91
  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
92
 
@@ -171,17 +173,17 @@ Suspensions are nestable and `resume()` is idempotent. Native projection resumes
171
173
 
172
174
  The native material and control appearance follow the running iOS version; an iOS 26 device does not acquire iOS 27's appearance merely by installing this theme.
173
175
 
174
- ## Support iPhone Duo (experimental)
176
+ ## Support iPhone Duo (preview)
175
177
 
176
- iPhone Duo support is experimental, including standalone use without Native UI Shell. It is available in the `1.2.0-0` prerelease alongside Native UI Shell. Its APIs and supported behavior may change.
178
+ iPhone Duo support, including standalone vertical bars without Native UI Shell, is available as a **preview** in `1.2.0` alongside Native UI Shell. Its APIs and supported behavior may change.
177
179
 
178
180
  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
181
 
180
- 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.
182
+ 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
183
 
182
- 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. A topmost full-width modal supplies its own eligible toolbar actions and rail bounds, including when it is a sheet. Centered modals, menus, and popovers keep their own toolbar layout. Modal placement follows the visible dialog width rather than its type or hinge posture.
184
+ 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
185
 
184
- In Vertical Bars, fixed-toolbar `ion-button` actions use an `ion-icon` or SVG with `slot="icon-only"`. All fills (`default`, `clear`, `solid`, and `outline`) and Ionic colors are eligible; submit buttons follow the same placement rule. Clear actions have no native glass background, including the default inside `ion-buttons`. Solid actions retain their background color and outline actions retain their border color and width. Actions without `icon-only` stay horizontal. Scrolling and explicitly opted-out controls retain their Web presentation. Add `.ios-theme-horizontal-only` to an `ion-buttons` group or individual `ion-button` to keep it in the horizontal Web toolbar. On Web, Android, older iOS, or when native projection is unavailable during setup, the Web projection remains the fallback.
186
+ **Native button appearance:** `1.2.0` defaults native vertical buttons to `buttonProjection: 'system'`. Use `source` to project Ionic fill and colors, with `data-projection="source|system"` or the equivalent `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
187
 
186
188
  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
189
 
@@ -47,12 +47,14 @@ Add one of `tab-bar-position-start`, `tab-bar-position-center`, or `tab-bar-posi
47
47
 
48
48
  These classes do not reposition a separate `ion-fab`; leave room for it when choosing the bar's position.
49
49
 
50
- ## Support iPhone Duo (experimental)
50
+ ## Support iPhone Duo (preview)
51
51
 
52
- iPhone Duo support is experimental. It is available in the `1.2.0-0` prerelease alongside Native UI Shell. Its APIs and supported behavior may change.
52
+ iPhone Duo support, including standalone vertical bars, is available as a **preview** in `1.2.0` alongside Native UI Shell. Its APIs and supported behavior may change.
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