@rdlabo/ionic-theme-ios27 1.2.0-beta.pr239.sha6c1c1e255674 → 1.2.0-beta.pr242.sha99c4a2ec4d6a
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/dist/native/definitions.d.ts +1 -0
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/index.js +1 -1
- package/dist/native/index.js.map +1 -1
- package/dist/native/shared/candidate.js +6 -3
- package/dist/native/shared/candidate.js.map +1 -1
- package/docs/iphone-duo-with-original-theme.md +1 -1
- package/docs/iphone-duo.md +2 -2
- package/docs/native-ui-shell.md +1 -1
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +34 -14
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +2 -0
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +3 -0
- package/package.json +1 -1
- package/src/native/definitions.ts +5 -1
- package/src/native/index.ts +1 -1
- package/src/native/shared/candidate.ts +10 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.d.ts","names":[],"sources":["../../src/native/definitions.ts"],"mappings":";;;iBAKiB;EACf;EACA;EACA;EACA;;iBAGe;
|
|
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"}
|
package/dist/native/index.js
CHANGED
|
@@ -131,7 +131,7 @@ const setVerticalControlAreaPlacement = (placement, rtl) => {
|
|
|
131
131
|
/**
|
|
132
132
|
* Call once after ion-app is mounted. Ionic markup remains the source of truth.
|
|
133
133
|
* Set buttonDefaultFill to `solid` for Ionic's default design, or `null` for this
|
|
134
|
-
* theme's glass design. Omitting it is equivalent to `null
|
|
134
|
+
* theme's glass design. Omitting it is equivalent to `null`.
|
|
135
135
|
* This default affects native vertical projection only; explicit button fills win.
|
|
136
136
|
*/
|
|
137
137
|
const enableVerticalControlArea = async (options = {}) => {
|
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` (existing behavior).\n * This default affects native vertical projection only; explicit button fills win.\n */\nexport const enableVerticalControlArea = async (options: VerticalControlAreaOptions = {}): Promise<VerticalControlAreaHandle> => {\n const handle = await enableNativeUIShell({\n buttonDefaultFill: options.buttonDefaultFill,\n controls: { tabs: true, toolbar: true },\n verticalBarsOnly: true,\n });\n return {\n getStatus: () => handle.getStatus(),\n suspend: () => handle.suspend(),\n destroy: () => handle.destroy(),\n setPlacement: setVerticalControlAreaPlacement,\n };\n};\n\n/** Call once at application startup. Ionic markup remains the source of truth. */\nexport const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise<NativeUIShellHandle> => {\n if (options.enabled === false) {\n const current = active;\n active = undefined;\n activeConfiguration = undefined;\n return current\n ? current.then(async (handle) => {\n await handle.destroy();\n return web('Disabled');\n })\n : Promise.resolve(web('Disabled'));\n }\n if (typeof document === 'undefined') return Promise.resolve(web('Requires a document'));\n const controls = options.controls;\n const configuration = JSON.stringify([\n options.verticalBarsOnly === true,\n options.buttonDefaultFill ?? null,\n ...(['tabs', 'toolbar', 'segment', 'fab'] as const).map((component) => !controls || controls[component] === true),\n ]);\n if (active && activeConfiguration !== configuration)\n return Promise.reject(\n new Error('Native UI Shell is already running with different controls; destroy it before changing configuration.'),\n );\n if (active) return active;\n activeConfiguration = configuration;\n const stopModals = observeVerticalBarsModals(document);\n const prehide =\n options.controls === undefined || options.controls.toolbar === true ? prehideVerticalBarsToolbarSources(document) : undefined;\n // A slow destroy of a superseded activation must not release a newer one.\n let start: Promise<NativeUIShellHandle>;\n const release = () => {\n if (active === start) {\n active = undefined;\n activeConfiguration = undefined;\n }\n };\n const fallback = (reason: string) =>\n manage(createVerticalBarsWebProjection(document, options), {\n reason,\n suspend: () => prehide?.suspend(),\n destroy: () => {\n prehide?.stop();\n stopModals();\n },\n release,\n });\n return (start = active =\n (async () => {\n if (Capacitor.getPlatform() !== 'ios') return fallback('Requires Capacitor iOS');\n let runtime: NativeUIShellHandle | undefined;\n let metricsListener: Awaited<ReturnType<typeof plugin.addListener>> | undefined;\n let stopVerticalBarsLayout: (() => void) | undefined;\n try {\n const capabilities = await plugin.configure({ verticalBarsOnly: options.verticalBarsOnly === true });\n if (!capabilities.supported) return fallback('Requires iOS 26 or later');\n if (!options.verticalBarsOnly) stopVerticalBarsLayout = observeNativeVerticalBarsLayout(document);\n // The application owns device state and selects the rail through placement classes.\n const nativeVerticalBars = () => {\n const app = document.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');\n if (!app) return false;\n const reported = nativePlacements.get(app);\n if (!reported?.edge) return false;\n const physicalEdge = app.classList.contains('ios-theme-vertical-bars-left') ? 'left' : 'right';\n return physicalEdge === physicalVerticalBarEdge(reported.edge, reported.rtl ?? elementRtl(app));\n };\n if (!options.verticalBarsOnly) {\n metricsListener = await plugin.addListener('webViewMetricsChange', (metrics) => {\n setConfig({ radius: metrics.radius });\n });\n await configureNativeTransition().catch(() => undefined);\n }\n const native = await createRuntime(document, plugin, options, nativeVerticalBars, options.verticalBarsOnly === true);\n runtime = combine(\n native,\n createVerticalBarsWebProjection(document, options, () => !nativeVerticalBars() || native.getStatus().state === 'stopped'),\n );\n return manage(runtime, {\n suspend: () => prehide?.suspend(),\n release,\n destroy: async () => {\n await metricsListener?.remove().catch(() => {});\n stopVerticalBarsLayout?.();\n prehide?.stop();\n stopModals();\n },\n });\n } catch (error) {\n await runtime?.destroy();\n await metricsListener?.remove().catch(() => {});\n stopVerticalBarsLayout?.();\n return fallback(error instanceof Error ? error.message : String(error));\n }\n })());\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/native/index.ts"],"sourcesContent":["import { Capacitor, registerPlugin } from '@capacitor/core';\nimport { setConfig } from '../transition/ios.transition';\nimport type {\n NativeUIShellHandle,\n NativeUIShellOptions,\n NativeUIShellPlugin,\n VerticalBarEdge,\n VerticalBarPlacement,\n VerticalControlAreaHandle,\n VerticalControlAreaOptions,\n WebViewMetrics,\n} from './definitions';\nimport { createRuntime } from './runtime';\nimport { createVerticalBarsWebProjection } from './vertical-bars-web';\nimport { prehideVerticalBarsToolbarSources } from './prehide';\nimport { observeVerticalBarsModals } from './shared/modal';\nexport type {\n NativeUIShellComponent,\n NativeUIShellControls,\n NativeUIShellHandle,\n NativeUIShellOptions,\n NativeUIShellStatus,\n NativeUIShellSuspension,\n VerticalBarEdge,\n VerticalBarPlacement,\n VerticalControlAreaHandle,\n VerticalControlAreaOptions,\n WebViewMetrics,\n} from './definitions';\n\nconst plugin = registerPlugin<NativeUIShellPlugin>('IonicNativeUIShell');\nexport const IonicNativeUIShell = plugin;\nlet active: Promise<NativeUIShellHandle> | undefined;\nlet activeConfiguration: string | undefined;\nconst web = (reason: string): NativeUIShellHandle => ({\n getStatus: () => ({ state: 'web', projected: 0, updates: 0, reason }),\n suspend: async () => ({ resume: async () => {} }),\n destroy: async () => {},\n});\n\nconst combine = (native: NativeUIShellHandle, fallback: NativeUIShellHandle): NativeUIShellHandle => ({\n getStatus() {\n const nativeStatus = native.getStatus();\n const fallbackStatus = fallback.getStatus();\n return {\n ...nativeStatus,\n state:\n nativeStatus.state === 'stopped' && fallbackStatus.state === 'stopped' ? 'stopped' : nativeStatus.projected > 0 ? 'native' : 'web',\n projected: nativeStatus.projected + fallbackStatus.projected,\n updates: nativeStatus.updates + fallbackStatus.updates,\n };\n },\n async suspend() {\n const [nativeLease, fallbackLease] = await Promise.all([native.suspend(), fallback.suspend()]);\n return { resume: async () => void (await Promise.all([nativeLease.resume(), fallbackLease.resume()])) };\n },\n async destroy() {\n await Promise.all([native.destroy(), fallback.destroy()]);\n },\n});\n\n/** Wraps a runtime handle with the shared enable-lifecycle: reason, suspend hooks and idempotent destroy. */\nconst manage = (\n handle: NativeUIShellHandle,\n lifecycle: {\n reason?: string;\n suspend?: () => (() => void) | undefined;\n /** Runs after teardown; clears the shared slot only while this activation owns it. */\n release?: () => void;\n destroy?: () => void | Promise<void>;\n } = {},\n): NativeUIShellHandle => {\n let destroyed = false;\n return {\n getStatus: () => (lifecycle.reason ? { ...handle.getStatus(), reason: lifecycle.reason } : handle.getStatus()),\n async suspend() {\n const lease = await handle.suspend();\n const resume = lifecycle.suspend?.();\n return {\n async resume() {\n await lease.resume();\n resume?.();\n },\n };\n },\n async destroy() {\n if (destroyed) return;\n destroyed = true;\n try {\n await handle.destroy();\n } finally {\n await lifecycle.destroy?.();\n lifecycle.release?.();\n }\n },\n };\n};\n\n/** Reads the current native WebView geometry and applies it to page transitions. */\nexport const configureNativeTransition = async (): Promise<WebViewMetrics> => {\n const metrics = typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' ? await plugin.getWebViewMetrics() : { radius: 0 };\n setConfig({ radius: metrics.radius });\n return metrics;\n};\n\n/** Resolves a logical vertical-bar edge to the physical side for the given direction. */\nconst physicalVerticalBarEdge = (edge: Exclude<VerticalBarEdge, null>, rtl: boolean): 'left' | 'right' =>\n (edge === 'leading') !== rtl ? 'left' : 'right';\n\nconst elementRtl = (element: Element): boolean => element.closest('[dir]')?.getAttribute('dir') === 'rtl';\n\n// Device facts are supplied by the application; the theme only compares placement.\nconst nativePlacements = new WeakMap<HTMLElement, { edge: VerticalBarEdge; rtl?: boolean }>();\nconst horizontalFallbackAttribute = 'data-native-ui-shell-vertical-bars-suspended';\n\n// Keep the requested rail while ordinary Native UI Shell temporarily uses its\n// horizontal layout. Removing the effective class restores normal measurements,\n// toolbar ownership and safe areas throughout the existing rendering pipeline.\nconst observeNativeVerticalBarsLayout = (doc: Document): (() => void) => {\n const reconcile = () => {\n const app = doc.querySelector<HTMLElement>('ion-app');\n if (!app) return;\n const requested = app.classList.contains('ios-theme-vertical-bars') || app.hasAttribute(horizontalFallbackAttribute);\n const suspended = requested && nativePlacements.get(app)?.edge == null;\n if (app.hasAttribute(horizontalFallbackAttribute) !== suspended) app.toggleAttribute(horizontalFallbackAttribute, suspended);\n if (app.classList.contains('ios-theme-vertical-bars') !== (requested && !suspended)) {\n app.classList.toggle('ios-theme-vertical-bars', requested && !suspended);\n }\n };\n const observer = new MutationObserver(reconcile);\n observer.observe(doc.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });\n doc.defaultView?.addEventListener('nativeUIShellRefresh', reconcile);\n reconcile();\n return () => {\n observer.disconnect();\n doc.defaultView?.removeEventListener('nativeUIShellRefresh', reconcile);\n const app = doc.querySelector<HTMLElement>(`ion-app[${horizontalFallbackAttribute}]`);\n if (app) {\n app.removeAttribute(horizontalFallbackAttribute);\n app.classList.add('ios-theme-vertical-bars');\n }\n };\n};\n\n/**\n * Applies one placement to the CSS layout and both Web/native projections.\n * Pass `rtl` when the document direction is known; otherwise the nearest `dir` attribute is used.\n */\nexport const setVerticalControlAreaPlacement = (placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean): void => {\n if (typeof document === 'undefined') return;\n const app = document.querySelector<HTMLElement>('ion-app');\n if (!app) throw new Error('Vertical Control Area requires ion-app');\n const { edge, inset = 0 } = placement && typeof placement === 'object' ? placement : { edge: placement, inset: 0 };\n if (placement && typeof placement === 'object' && placement.nativeEdge !== undefined) {\n nativePlacements.set(app, { edge: placement.nativeEdge, rtl });\n }\n app.removeAttribute(horizontalFallbackAttribute);\n app.classList.toggle('ios-theme-vertical-bars', edge !== null);\n app.classList.toggle('ios-theme-vertical-bars-left', edge !== null && physicalVerticalBarEdge(edge, rtl ?? elementRtl(app)) === 'left');\n if (edge && Number.isFinite(inset) && inset > 0) app.style.setProperty('--ios-theme-vertical-bars-native-inset', `${inset}px`);\n else app.style.removeProperty('--ios-theme-vertical-bars-native-inset');\n document.defaultView?.dispatchEvent(new Event('nativeUIShellRefresh'));\n};\n\n/**\n * Call once after ion-app is mounted. Ionic markup remains the source of truth.\n * 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":";;;;;;;AA8BA,MAAM,SAAS,eAAoC,oBAAoB;AACvE,MAAa,qBAAqB;AAClC,IAAI;AACJ,IAAI;AACJ,MAAM,OAAO,YAAyC;CACpD,kBAAkB;EAAE,OAAO;EAAO,WAAW;EAAG,SAAS;EAAG;CAAO;CACnE,SAAS,aAAa,EAAE,QAAQ,YAAY,CAAC,EAAE;CAC/C,SAAS,YAAY,CAAC;AACxB;AAEA,MAAM,WAAW,QAA6B,cAAwD;CACpG,YAAY;EACV,MAAM,eAAe,OAAO,UAAU;EACtC,MAAM,iBAAiB,SAAS,UAAU;EAC1C,OAAO;GACL,GAAG;GACH,OACE,aAAa,UAAU,aAAa,eAAe,UAAU,YAAY,YAAY,aAAa,YAAY,IAAI,WAAW;GAC/H,WAAW,aAAa,YAAY,eAAe;GACnD,SAAS,aAAa,UAAU,eAAe;EACjD;CACF;CACA,MAAM,UAAU;EACd,MAAM,CAAC,aAAa,iBAAiB,MAAM,QAAQ,IAAI,CAAC,OAAO,QAAQ,GAAG,SAAS,QAAQ,CAAC,CAAC;EAC7F,OAAO,EAAE,QAAQ,YAAY,KAAM,MAAM,QAAQ,IAAI,CAAC,YAAY,OAAO,GAAG,cAAc,OAAO,CAAC,CAAC,EAAG;CACxG;CACA,MAAM,UAAU;EACd,MAAM,QAAQ,IAAI,CAAC,OAAO,QAAQ,GAAG,SAAS,QAAQ,CAAC,CAAC;CAC1D;AACF;;AAGA,MAAM,UACJ,QACA,YAMI,CAAC,MACmB;CACxB,IAAI,YAAY;CAChB,OAAO;EACL,iBAAkB,UAAU,SAAS;GAAE,GAAG,OAAO,UAAU;GAAG,QAAQ,UAAU;EAAO,IAAI,OAAO,UAAU;EAC5G,MAAM,UAAU;GACd,MAAM,QAAQ,MAAM,OAAO,QAAQ;GACnC,MAAM,SAAS,UAAU,UAAU;GACnC,OAAO,EACL,MAAM,SAAS;IACb,MAAM,MAAM,OAAO;IACnB,SAAS;GACX,EACF;EACF;EACA,MAAM,UAAU;GACd,IAAI,WAAW;GACf,YAAY;GACZ,IAAI;IACF,MAAM,OAAO,QAAQ;GACvB,UAAU;IACR,MAAM,UAAU,UAAU;IAC1B,UAAU,UAAU;GACtB;EACF;CACF;AACF;;AAGA,MAAa,4BAA4B,YAAqC;CAC5E,MAAM,UAAU,OAAO,aAAa,eAAe,UAAU,YAAY,MAAM,QAAQ,MAAM,OAAO,kBAAkB,IAAI,EAAE,QAAQ,EAAE;CACtI,UAAU,EAAE,QAAQ,QAAQ,OAAO,CAAC;CACpC,OAAO;AACT;;AAGA,MAAM,2BAA2B,MAAsC,QACpE,SAAS,cAAe,MAAM,SAAS;AAE1C,MAAM,cAAc,YAA8B,QAAQ,QAAQ,OAAO,CAAC,EAAE,aAAa,KAAK,MAAM;AAGpG,MAAM,mCAAmB,IAAI,QAA+D;AAC5F,MAAM,8BAA8B;AAKpC,MAAM,mCAAmC,QAAgC;CACvE,MAAM,kBAAkB;EACtB,MAAM,MAAM,IAAI,cAA2B,SAAS;EACpD,IAAI,CAAC,KAAK;EACV,MAAM,YAAY,IAAI,UAAU,SAAS,yBAAyB,KAAK,IAAI,aAAa,2BAA2B;EACnH,MAAM,YAAY,aAAa,iBAAiB,IAAI,GAAG,CAAC,EAAE,QAAQ;EAClE,IAAI,IAAI,aAAa,2BAA2B,MAAM,WAAW,IAAI,gBAAgB,6BAA6B,SAAS;EAC3H,IAAI,IAAI,UAAU,SAAS,yBAAyB,OAAO,aAAa,CAAC,YACvE,IAAI,UAAU,OAAO,2BAA2B,aAAa,CAAC,SAAS;CAE3E;CACA,MAAM,WAAW,IAAI,iBAAiB,SAAS;CAC/C,SAAS,QAAQ,IAAI,iBAAiB;EAAE,WAAW;EAAM,SAAS;EAAM,YAAY;EAAM,iBAAiB,CAAC,OAAO;CAAE,CAAC;CACtH,IAAI,aAAa,iBAAiB,wBAAwB,SAAS;CACnE,UAAU;CACV,aAAa;EACX,SAAS,WAAW;EACpB,IAAI,aAAa,oBAAoB,wBAAwB,SAAS;EACtE,MAAM,MAAM,IAAI,cAA2B,WAAW,4BAA4B,EAAE;EACpF,IAAI,KAAK;GACP,IAAI,gBAAgB,2BAA2B;GAC/C,IAAI,UAAU,IAAI,yBAAyB;EAC7C;CACF;AACF;;;;;AAMA,MAAa,mCAAmC,WAAmD,QAAwB;CACzH,IAAI,OAAO,aAAa,aAAa;CACrC,MAAM,MAAM,SAAS,cAA2B,SAAS;CACzD,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,wCAAwC;CAClE,MAAM,EAAE,MAAM,QAAQ,MAAM,aAAa,OAAO,cAAc,WAAW,YAAY;EAAE,MAAM;EAAW,OAAO;CAAE;CACjH,IAAI,aAAa,OAAO,cAAc,YAAY,UAAU,eAAe,KAAA,GACzE,iBAAiB,IAAI,KAAK;EAAE,MAAM,UAAU;EAAY;CAAI,CAAC;CAE/D,IAAI,gBAAgB,2BAA2B;CAC/C,IAAI,UAAU,OAAO,2BAA2B,SAAS,IAAI;CAC7D,IAAI,UAAU,OAAO,gCAAgC,SAAS,QAAQ,wBAAwB,MAAM,OAAO,WAAW,GAAG,CAAC,MAAM,MAAM;CACtI,IAAI,QAAQ,OAAO,SAAS,KAAK,KAAK,QAAQ,GAAG,IAAI,MAAM,YAAY,0CAA0C,GAAG,MAAM,GAAG;MACxH,IAAI,MAAM,eAAe,wCAAwC;CACtE,SAAS,aAAa,cAAc,IAAI,MAAM,sBAAsB,CAAC;AACvE;;;;;;;AAQA,MAAa,4BAA4B,OAAO,UAAsC,CAAC,MAA0C;CAC/H,MAAM,SAAS,MAAM,oBAAoB;EACvC,mBAAmB,QAAQ;EAC3B,UAAU;GAAE,MAAM;GAAM,SAAS;EAAK;EACtC,kBAAkB;CACpB,CAAC;CACD,OAAO;EACL,iBAAiB,OAAO,UAAU;EAClC,eAAe,OAAO,QAAQ;EAC9B,eAAe,OAAO,QAAQ;EAC9B,cAAc;CAChB;AACF;;AAGA,MAAa,uBAAuB,UAAgC,CAAC,MAAoC;CACvG,IAAI,QAAQ,YAAY,OAAO;EAC7B,MAAM,UAAU;EAChB,SAAS,KAAA;EACT,sBAAsB,KAAA;EACtB,OAAO,UACH,QAAQ,KAAK,OAAO,WAAW;GAC7B,MAAM,OAAO,QAAQ;GACrB,OAAO,IAAI,UAAU;EACvB,CAAC,IACD,QAAQ,QAAQ,IAAI,UAAU,CAAC;CACrC;CACA,IAAI,OAAO,aAAa,aAAa,OAAO,QAAQ,QAAQ,IAAI,qBAAqB,CAAC;CACtF,MAAM,WAAW,QAAQ;CACzB,MAAM,gBAAgB,KAAK,UAAU;EACnC,QAAQ,qBAAqB;EAC7B,QAAQ,qBAAqB;EAC7B,GAAI;GAAC;GAAQ;GAAW;GAAW;EAAK,CAAC,CAAW,KAAK,cAAc,CAAC,YAAY,SAAS,eAAe,IAAI;CAClH,CAAC;CACD,IAAI,UAAU,wBAAwB,eACpC,OAAO,QAAQ,uBACb,IAAI,MAAM,uGAAuG,CACnH;CACF,IAAI,QAAQ,OAAO;CACnB,sBAAsB;CACtB,MAAM,aAAa,0BAA0B,QAAQ;CACrD,MAAM,UACJ,QAAQ,aAAa,KAAA,KAAa,QAAQ,SAAS,YAAY,OAAO,kCAAkC,QAAQ,IAAI,KAAA;CAEtH,IAAI;CACJ,MAAM,gBAAgB;EACpB,IAAI,WAAW,OAAO;GACpB,SAAS,KAAA;GACT,sBAAsB,KAAA;EACxB;CACF;CACA,MAAM,YAAY,WAChB,OAAO,gCAAgC,UAAU,OAAO,GAAG;EACzD;EACA,eAAe,SAAS,QAAQ;EAChC,eAAe;GACb,SAAS,KAAK;GACd,WAAW;EACb;EACA;CACF,CAAC;CACH,OAAQ,QAAQ,UACb,YAAY;EACX,IAAI,UAAU,YAAY,MAAM,OAAO,OAAO,SAAS,wBAAwB;EAC/E,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;GAEF,IAAI,EAAC,MADsB,OAAO,UAAU,EAAE,kBAAkB,QAAQ,qBAAqB,KAAK,CAAC,EAAA,CACjF,WAAW,OAAO,SAAS,0BAA0B;GACvE,IAAI,CAAC,QAAQ,kBAAkB,yBAAyB,gCAAgC,QAAQ;GAEhG,MAAM,2BAA2B;IAC/B,MAAM,MAAM,SAAS,cAA2B,iCAAiC;IACjF,IAAI,CAAC,KAAK,OAAO;IACjB,MAAM,WAAW,iBAAiB,IAAI,GAAG;IACzC,IAAI,CAAC,UAAU,MAAM,OAAO;IAE5B,QADqB,IAAI,UAAU,SAAS,8BAA8B,IAAI,SAAS,aAC/D,wBAAwB,SAAS,MAAM,SAAS,OAAO,WAAW,GAAG,CAAC;GAChG;GACA,IAAI,CAAC,QAAQ,kBAAkB;IAC7B,kBAAkB,MAAM,OAAO,YAAY,yBAAyB,YAAY;KAC9E,UAAU,EAAE,QAAQ,QAAQ,OAAO,CAAC;IACtC,CAAC;IACD,MAAM,0BAA0B,CAAC,CAAC,YAAY,KAAA,CAAS;GACzD;GACA,MAAM,SAAS,MAAM,cAAc,UAAU,QAAQ,SAAS,oBAAoB,QAAQ,qBAAqB,IAAI;GACnH,UAAU,QACR,QACA,gCAAgC,UAAU,eAAe,CAAC,mBAAmB,KAAK,OAAO,UAAU,CAAC,CAAC,UAAU,SAAS,CAC1H;GACA,OAAO,OAAO,SAAS;IACrB,eAAe,SAAS,QAAQ;IAChC;IACA,SAAS,YAAY;KACnB,MAAM,iBAAiB,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;KAC9C,yBAAyB;KACzB,SAAS,KAAK;KACd,WAAW;IACb;GACF,CAAC;EACH,SAAS,OAAO;GACd,MAAM,SAAS,QAAQ;GACvB,MAAM,iBAAiB,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;GAC9C,yBAAyB;GACzB,OAAO,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;EACxE;CACF,EAAA,CAAG;AACP"}
|
|
@@ -32,8 +32,10 @@ const appendItem = (candidate, child, id, content = child, label = text(child))
|
|
|
32
32
|
const badge = child.querySelector("ion-badge");
|
|
33
33
|
const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : void 0;
|
|
34
34
|
const sourceFill = child.fill;
|
|
35
|
-
const
|
|
36
|
-
const
|
|
35
|
+
const defaultFill = child.closest("ion-buttons") ? "clear" : candidate.buttonDefaultFill;
|
|
36
|
+
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;
|
|
38
|
+
const outline = buttonFill === "outline" ? getComputedStyle(native ?? child) : void 0;
|
|
37
39
|
const item = {
|
|
38
40
|
id: id(child),
|
|
39
41
|
...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),
|
|
@@ -44,7 +46,8 @@ const appendItem = (candidate, child, id, content = child, label = text(child))
|
|
|
44
46
|
fontSize: parseFloat(labelStyle.fontSize),
|
|
45
47
|
fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,
|
|
46
48
|
color: getComputedStyle(native ?? child).color,
|
|
47
|
-
|
|
49
|
+
buttonFill,
|
|
50
|
+
backgroundColor: buttonFill === "solid" ? getComputedStyle(native ?? child).backgroundColor : void 0,
|
|
48
51
|
borderColor: outline?.borderTopColor,
|
|
49
52
|
borderWidth: outline ? parseFloat(outline.borderTopWidth) : void 0,
|
|
50
53
|
badge: badgeStyle ? {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"candidate.js","names":[],"sources":["../../../src/native/shared/candidate.ts"],"sourcesContent":["import type { ShellControl, ShellItem, NativeUIShellComponent, VerticalControlAreaOptions } from '../definitions';\nimport { frame, isDark, isVerticalBarsSource, text, visible } from './dom';\nimport { iconSource } from './icons';\n\nexport interface Candidate {\n buttonDefaultFill?: VerticalControlAreaOptions['buttonDefaultFill'];\n element: HTMLElement;\n sources?: HTMLElement[];\n control: ShellControl;\n actions: Map<string, HTMLElement>;\n icons: { item: ShellItem; source: string; field?: 'closeIcon' }[];\n}\n\nexport type Identify = (element: HTMLElement) => string;\ntype ItemElement = HTMLElement & { disabled?: boolean; selected?: boolean };\n\nexport const createCandidate = (\n element: HTMLElement,\n kind: NativeUIShellComponent,\n id: Identify,\n options: VerticalControlAreaOptions = {},\n): Candidate => {\n const rect = element.getBoundingClientRect();\n const style = getComputedStyle(element);\n return {\n element,\n buttonDefaultFill: options.buttonDefaultFill,\n control: {\n id: id(element),\n kind,\n ...frame(rect),\n items: [],\n dark: isDark(style),\n rtl: style.direction === 'rtl',\n },\n actions: new Map(),\n icons: [],\n };\n};\n\n// Shared text, artwork, geometry and action data; component rules stay with their readers.\nexport const appendItem = (\n candidate: Candidate,\n child: HTMLElement,\n id: Identify,\n content: ParentNode = child,\n label = text(child),\n): ShellItem | undefined => {\n if (\n !visible(child, isVerticalBarsSource(candidate.element)) ||\n child.querySelector('input, button, a, img, canvas, video, ion-spinner, ion-avatar')\n )\n return;\n const icons = Array.from(content.querySelectorAll<HTMLElement>('ion-icon'));\n const directSVGs = Array.from(content.querySelectorAll<SVGElement>('svg'));\n if (icons.length + directSVGs.length > 1) return undefined;\n const svg = directSVGs[0] ?? icons[0]?.shadowRoot?.querySelector<SVGElement>('svg');\n if (icons.length && !svg) return undefined; // ion-icon has not finished loading.\n const labelElement = content.querySelector('[part=\"text\"], ion-label') ?? child;\n const native = child.shadowRoot?.querySelector('[part=\"native\"]');\n const labelStyle = getComputedStyle(labelElement);\n const badge = child.querySelector<HTMLElement>('ion-badge');\n const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : undefined;\n const sourceFill = (child as HTMLIonButtonElement).fill;\n const fill = !sourceFill || sourceFill === 'default' ?
|
|
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) && child.matches('ion-button') && (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: getComputedStyle(native ?? child).color,\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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"}
|
|
@@ -112,7 +112,7 @@ If your existing theme gives buttons a CSS background without an explicit `fill=
|
|
|
112
112
|
const rail = await enableVerticalControlArea({ buttonDefaultFill: 'solid' });
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
`buttonDefaultFill` accepts `'solid'` (Ionic's default design) or `null` (the iOS theme's glass design). Omitting it is equivalent to `null`. It applies only to native vertical button projection when Ionic's `fill` is omitted or `default`. Explicit `clear`, `solid`, and `outline` values take precedence. Solid projection reads the existing computed foreground and background colors; outline projection reads the computed border. Source buttons and Web clones are unchanged.
|
|
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
116
|
|
|
117
117
|
**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
118
|
|
package/docs/iphone-duo.md
CHANGED
|
@@ -131,7 +131,7 @@ An `ion-button` moves into the rail when it contains an `ion-icon` or SVG with `
|
|
|
131
131
|
| `slot="start"`, `slot="end"`, or no slot | Original horizontal toolbar |
|
|
132
132
|
| No icon | Original horizontal toolbar |
|
|
133
133
|
|
|
134
|
-
This rule applies to `fill="default"`, `clear`, `solid`, and `outline`, including buttons with an Ionic `color`. 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.
|
|
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
135
|
|
|
136
136
|
```html
|
|
137
137
|
<ion-header>
|
|
@@ -145,7 +145,7 @@ This rule applies to `fill="default"`, `clear`, `solid`, and `outline`, includin
|
|
|
145
145
|
</ion-header>
|
|
146
146
|
```
|
|
147
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`. Explicit button fills take precedence. It affects native vertical projection only and never changes source buttons or Web clones.
|
|
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
149
|
|
|
150
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
151
|
|
package/docs/native-ui-shell.md
CHANGED
|
@@ -181,7 +181,7 @@ On supported iOS versions, adding `.ios-theme-vertical-bars` changes only contro
|
|
|
181
181
|
|
|
182
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.
|
|
183
183
|
|
|
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. 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.
|
|
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.
|
|
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
|
|
|
@@ -17,6 +17,7 @@ final class ShellVerticalBarsModel: ObservableObject {
|
|
|
17
17
|
let label: String
|
|
18
18
|
let accessibilityLabel: String
|
|
19
19
|
let image: UIImage?
|
|
20
|
+
let clear: Bool
|
|
20
21
|
let background: Color?
|
|
21
22
|
let borderColor: Color?
|
|
22
23
|
let borderWidth: CGFloat
|
|
@@ -47,6 +48,7 @@ final class ShellVerticalBarsModel: ObservableObject {
|
|
|
47
48
|
Item(id: source.id, label: source.content.label,
|
|
48
49
|
accessibilityLabel: source.content.accessibilityLabel,
|
|
49
50
|
image: rendering.image(source.content),
|
|
51
|
+
clear: source.content.buttonFill == .clear,
|
|
50
52
|
background: source.content.backgroundColor.map { Color(uiColor: rendering.color($0)) },
|
|
51
53
|
borderColor: source.content.borderColor.map { Color(uiColor: rendering.color($0)) },
|
|
52
54
|
borderWidth: source.content.borderWidth ?? 0,
|
|
@@ -125,6 +127,21 @@ private func verticalBarsButton(_ item: ShellVerticalBarsModel.Item, model: Shel
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
@available(iOS 26.0, *)
|
|
131
|
+
private struct ShellVerticalBarsButton: ToolbarContent {
|
|
132
|
+
@ObservedObject var model: ShellVerticalBarsModel
|
|
133
|
+
let id: String
|
|
134
|
+
let placement: ToolbarItemPlacement
|
|
135
|
+
|
|
136
|
+
// Observe here so same-ID updates also refresh the shared toolbar background.
|
|
137
|
+
var body: some ToolbarContent {
|
|
138
|
+
if let item = model.groups.lazy.flatMap(\.items).first(where: { $0.id == id }) {
|
|
139
|
+
ToolbarItem(placement: placement) { verticalBarsButton(item, model: model) }
|
|
140
|
+
.sharedBackgroundVisibility(item.clear ? .hidden : .automatic)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
128
145
|
@available(iOS 26.0, *)
|
|
129
146
|
private func verticalBarsBackButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
|
|
130
147
|
Button { model.activate(item.id) } label: { Image(systemName: "chevron.backward") }
|
|
@@ -238,20 +255,22 @@ private struct ShellVerticalBarsToolbar: ViewModifier {
|
|
|
238
255
|
.axisBehavior(.verticalPreferred)
|
|
239
256
|
}
|
|
240
257
|
ForEach(model.groups.filter { $0.slot == .start }) { group in
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
258
|
+
if group.id != model.groups.first(where: { $0.slot == .start })?.id {
|
|
259
|
+
ToolbarSpacer(.fixed, placement: .topBarLeading)
|
|
260
|
+
}
|
|
261
|
+
ForEach(group.items) { item in
|
|
262
|
+
ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarLeading)
|
|
263
|
+
.axisBehavior(.verticalPreferred)
|
|
245
264
|
}
|
|
246
|
-
.axisBehavior(.verticalPreferred)
|
|
247
265
|
}
|
|
248
266
|
ForEach(model.groups.filter { $0.slot != .start }) { group in
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
267
|
+
if group.id != model.groups.first(where: { $0.slot != .start })?.id {
|
|
268
|
+
ToolbarSpacer(.fixed, placement: .topBarTrailing)
|
|
269
|
+
}
|
|
270
|
+
ForEach(group.items) { item in
|
|
271
|
+
ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarTrailing)
|
|
272
|
+
.axisBehavior(.verticalPreferred)
|
|
253
273
|
}
|
|
254
|
-
.axisBehavior(.verticalPreferred)
|
|
255
274
|
}
|
|
256
275
|
}
|
|
257
276
|
}
|
|
@@ -270,10 +289,11 @@ private struct ShellVerticalBarsLegacyToolbar: ViewModifier {
|
|
|
270
289
|
}
|
|
271
290
|
}
|
|
272
291
|
ForEach(model.groups) { group in
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
292
|
+
if group.id != model.groups.first?.id {
|
|
293
|
+
ToolbarSpacer(.fixed, placement: .primaryAction)
|
|
294
|
+
}
|
|
295
|
+
ForEach(group.items) { item in
|
|
296
|
+
ShellVerticalBarsButton(model: model, id: item.id, placement: .primaryAction)
|
|
277
297
|
}
|
|
278
298
|
}
|
|
279
299
|
}
|
|
@@ -99,6 +99,7 @@ struct ShellBadge: Decodable, Equatable {
|
|
|
99
99
|
|
|
100
100
|
struct ShellItemContent: Decodable, Equatable {
|
|
101
101
|
enum IconPosition: String, Decodable { case leading, trailing, top }
|
|
102
|
+
enum ButtonFill: String, Decodable { case clear, solid, outline }
|
|
102
103
|
let id: String
|
|
103
104
|
var label: String
|
|
104
105
|
let accessibilityLabel: String
|
|
@@ -107,6 +108,7 @@ struct ShellItemContent: Decodable, Equatable {
|
|
|
107
108
|
let fontSize: Double
|
|
108
109
|
let fontWeight: Double
|
|
109
110
|
let color: String
|
|
111
|
+
let buttonFill: ButtonFill?
|
|
110
112
|
let backgroundColor: String?
|
|
111
113
|
let borderColor: String?
|
|
112
114
|
let borderWidth: CGFloat?
|
|
@@ -53,16 +53,19 @@ final class ShellSnapshotTests: XCTestCase {
|
|
|
53
53
|
guard #available(iOS 26.0, *) else { throw XCTSkip("Requires SwiftUI adaptive controls") }
|
|
54
54
|
let controls = try decode([control(["kind": "ion-buttons", "items": [
|
|
55
55
|
item(["backgroundColor": "rgb(0, 122, 255)"]),
|
|
56
|
+
item(["id": "clear", "buttonFill": "clear"]),
|
|
56
57
|
item(["id": "outline", "borderColor": "rgb(255, 0, 0)", "borderWidth": 2.0])
|
|
57
58
|
]])]).controls
|
|
58
59
|
XCTAssertEqual(controls.first?.items.first?.content.backgroundColor, "rgb(0, 122, 255)")
|
|
59
60
|
let model = ShellVerticalBarsModel()
|
|
60
61
|
model.apply(controls, rendering: ShellRendering())
|
|
61
62
|
XCTAssertNotNil(model.groups.first?.items.first?.background)
|
|
63
|
+
XCTAssertEqual(model.groups.first?.items.map(\.clear), [false, true, false])
|
|
62
64
|
XCTAssertNotNil(model.groups.first?.items.last?.borderColor)
|
|
63
65
|
XCTAssertEqual(model.groups.first?.items.last?.borderWidth, 2)
|
|
64
66
|
model.apply(try decode([control(["kind": "ion-button"])]).controls, rendering: ShellRendering())
|
|
65
67
|
XCTAssertNil(model.groups.first?.items.first?.background)
|
|
68
|
+
XCTAssertEqual(model.groups.first?.items.first?.clear, false)
|
|
66
69
|
XCTAssertNil(model.groups.first?.items.first?.borderColor)
|
|
67
70
|
XCTAssertEqual(model.groups.first?.items.first?.borderWidth, 0)
|
|
68
71
|
}
|
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.pr242.sha99c4a2ec4d6a",
|
|
5
5
|
"description": "iOS27 Theme for Ionic Framework",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -13,7 +13,9 @@ export interface NativeUIShellStatus {
|
|
|
13
13
|
export interface VerticalControlAreaOptions {
|
|
14
14
|
/** Default fill for native vertical button projection when Ionic's fill is omitted or `default`.
|
|
15
15
|
* Use `solid` for Ionic's default design, or `null` for the iOS theme's glass design.
|
|
16
|
-
* Omitted is equivalent to `null
|
|
16
|
+
* Omitted is equivalent to `null`.
|
|
17
|
+
* Clear buttons have no native glass background, including the default inside ion-buttons.
|
|
18
|
+
* Explicit solid is required to project a background from buttons inside ion-buttons.
|
|
17
19
|
* Explicit button fills take precedence. Source buttons and Web clones are unchanged.
|
|
18
20
|
*/
|
|
19
21
|
buttonDefaultFill?: 'solid' | null;
|
|
@@ -91,6 +93,8 @@ export interface ShellItem extends Frame {
|
|
|
91
93
|
fontSize: number;
|
|
92
94
|
fontWeight: number;
|
|
93
95
|
color: string;
|
|
96
|
+
/** Resolved ion-button fill for vertical projection; clear omits the native glass background. */
|
|
97
|
+
buttonFill?: 'clear' | 'solid' | 'outline';
|
|
94
98
|
backgroundColor?: string;
|
|
95
99
|
borderColor?: string;
|
|
96
100
|
borderWidth?: number;
|
package/src/native/index.ts
CHANGED
|
@@ -165,7 +165,7 @@ export const setVerticalControlAreaPlacement = (placement: VerticalBarEdge | Ver
|
|
|
165
165
|
/**
|
|
166
166
|
* Call once after ion-app is mounted. Ionic markup remains the source of truth.
|
|
167
167
|
* Set buttonDefaultFill to `solid` for Ionic's default design, or `null` for this
|
|
168
|
-
* theme's glass design. Omitting it is equivalent to `null
|
|
168
|
+
* theme's glass design. Omitting it is equivalent to `null`.
|
|
169
169
|
* This default affects native vertical projection only; explicit button fills win.
|
|
170
170
|
*/
|
|
171
171
|
export const enableVerticalControlArea = async (options: VerticalControlAreaOptions = {}): Promise<VerticalControlAreaHandle> => {
|
|
@@ -62,9 +62,14 @@ export const appendItem = (
|
|
|
62
62
|
const badge = child.querySelector<HTMLElement>('ion-badge');
|
|
63
63
|
const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : undefined;
|
|
64
64
|
const sourceFill = (child as HTMLIonButtonElement).fill;
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
65
|
+
// Ionic defaults buttons inside ion-buttons to clear, even when the app defaults to solid.
|
|
66
|
+
const defaultFill = child.closest('ion-buttons') ? 'clear' : candidate.buttonDefaultFill;
|
|
67
|
+
const fill = !sourceFill || sourceFill === 'default' ? defaultFill : sourceFill;
|
|
68
|
+
const buttonFill =
|
|
69
|
+
isVerticalBarsSource(child) && child.matches('ion-button') && (fill === 'clear' || fill === 'solid' || fill === 'outline')
|
|
70
|
+
? fill
|
|
71
|
+
: undefined;
|
|
72
|
+
const outline = buttonFill === 'outline' ? getComputedStyle(native ?? child) : undefined;
|
|
68
73
|
const item: ShellItem = {
|
|
69
74
|
id: id(child),
|
|
70
75
|
...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),
|
|
@@ -82,10 +87,8 @@ export const appendItem = (
|
|
|
82
87
|
fontSize: parseFloat(labelStyle.fontSize),
|
|
83
88
|
fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,
|
|
84
89
|
color: getComputedStyle(native ?? child).color,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
? getComputedStyle(native ?? child).backgroundColor
|
|
88
|
-
: undefined,
|
|
90
|
+
buttonFill,
|
|
91
|
+
backgroundColor: buttonFill === 'solid' ? getComputedStyle(native ?? child).backgroundColor : undefined,
|
|
89
92
|
borderColor: outline?.borderTopColor,
|
|
90
93
|
borderWidth: outline ? parseFloat(outline.borderTopWidth) : undefined,
|
|
91
94
|
badge: badgeStyle
|