@rdlabo/ionic-theme-ios27 1.2.0 → 1.2.1-beta.pr273.shae866fb1dca9f

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 (121) hide show
  1. package/dist/css/components/ion-button.css +1 -1
  2. package/dist/css/components/ion-toolbar.css +1 -1
  3. package/dist/css/ionic-theme-ios27.css +1 -1
  4. package/dist/css/vertical-bars.css +1 -1
  5. package/dist/native/components/index.js +2 -2
  6. package/dist/native/components/index.js.map +1 -1
  7. package/dist/native/definitions.d.ts +32 -0
  8. package/dist/native/definitions.d.ts.map +1 -1
  9. package/dist/native/index.d.ts.map +1 -1
  10. package/dist/native/index.js +23 -6
  11. package/dist/native/index.js.map +1 -1
  12. package/dist/native/overlays/content.d.ts +4 -0
  13. package/dist/native/overlays/content.d.ts.map +1 -0
  14. package/dist/native/overlays/content.js +51 -0
  15. package/dist/native/overlays/content.js.map +1 -0
  16. package/dist/native/overlays/controller.d.ts +8 -0
  17. package/dist/native/overlays/controller.d.ts.map +1 -0
  18. package/dist/native/overlays/controller.js +266 -0
  19. package/dist/native/overlays/controller.js.map +1 -0
  20. package/dist/native/overlays/dialog-animation.d.ts +8 -0
  21. package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
  22. package/dist/native/overlays/dialog-animation.js +31 -0
  23. package/dist/native/overlays/dialog-animation.js.map +1 -0
  24. package/dist/native/overlays/dismiss-animation.d.ts +4 -0
  25. package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
  26. package/dist/native/overlays/dismiss-animation.js +18 -0
  27. package/dist/native/overlays/dismiss-animation.js.map +1 -0
  28. package/dist/native/overlays/focus.d.ts +5 -0
  29. package/dist/native/overlays/focus.d.ts.map +1 -0
  30. package/dist/native/overlays/focus.js +59 -0
  31. package/dist/native/overlays/focus.js.map +1 -0
  32. package/dist/native/overlays/popover.d.ts +11 -0
  33. package/dist/native/overlays/popover.d.ts.map +1 -0
  34. package/dist/native/overlays/popover.js +78 -0
  35. package/dist/native/overlays/popover.js.map +1 -0
  36. package/dist/native/overlays/styles.d.ts +9 -0
  37. package/dist/native/overlays/styles.d.ts.map +1 -0
  38. package/dist/native/overlays/styles.js +82 -0
  39. package/dist/native/overlays/styles.js.map +1 -0
  40. package/dist/native/prehide.d.ts.map +1 -1
  41. package/dist/native/prehide.js +6 -2
  42. package/dist/native/prehide.js.map +1 -1
  43. package/dist/native/runtime.d.ts +4 -1
  44. package/dist/native/runtime.d.ts.map +1 -1
  45. package/dist/native/runtime.js +29 -8
  46. package/dist/native/runtime.js.map +1 -1
  47. package/dist/native/shared/dom.d.ts +2 -0
  48. package/dist/native/shared/dom.d.ts.map +1 -1
  49. package/dist/native/shared/dom.js +5 -1
  50. package/dist/native/shared/dom.js.map +1 -1
  51. package/dist/native/shared/projection-id.d.ts +4 -0
  52. package/dist/native/shared/projection-id.d.ts.map +1 -0
  53. package/dist/native/shared/projection-id.js +7 -0
  54. package/dist/native/shared/projection-id.js.map +1 -0
  55. package/dist/native/vertical-bars-web.d.ts.map +1 -1
  56. package/dist/native/vertical-bars-web.js +4 -2
  57. package/dist/native/vertical-bars-web.js.map +1 -1
  58. package/dist/native-integration/index.d.ts +1 -1
  59. package/dist/native-integration/index.js.map +1 -1
  60. package/dist/native-integration/transition.d.ts +1 -1
  61. package/dist/native-integration/transition.js.map +1 -1
  62. package/dist/sheets-of-glass/animations.d.ts +1 -1
  63. package/dist/sheets-of-glass/animations.js +1 -1
  64. package/dist/sheets-of-glass/animations.js.map +1 -1
  65. package/dist/sheets-of-glass/index.js +1 -1
  66. package/dist/sheets-of-glass/index.js.map +1 -1
  67. package/dist/tab-bar-searchable/animations/enter.d.ts +1 -1
  68. package/dist/tab-bar-searchable/animations/enter.d.ts.map +1 -1
  69. package/dist/tab-bar-searchable/animations/enter.js +1 -1
  70. package/dist/tab-bar-searchable/animations/enter.js.map +1 -1
  71. package/dist/tab-bar-searchable/animations/leave.d.ts +1 -1
  72. package/dist/tab-bar-searchable/animations/leave.d.ts.map +1 -1
  73. package/dist/tab-bar-searchable/animations/leave.js +1 -1
  74. package/dist/tab-bar-searchable/animations/leave.js.map +1 -1
  75. package/dist/tab-bar-searchable/index.js +1 -1
  76. package/dist/tab-bar-searchable/index.js.map +1 -1
  77. package/dist/transition/index.d.ts +1 -1
  78. package/dist/transition/index.js +1 -1
  79. package/dist/transition/index.js.map +1 -1
  80. package/dist/transition/ios.transition.d.ts +1 -1
  81. package/dist/transition/ios.transition.js.map +1 -1
  82. package/dist/utils.d.ts +1 -1
  83. package/dist/utils.js.map +1 -1
  84. package/docs/native-ui-shell.md +20 -4
  85. package/docs/vertical-bars.md +3 -1
  86. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +65 -91
  87. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +9 -3
  88. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +79 -3
  89. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +57 -1
  90. package/ios/Sources/IonicNativeUIShellPlugin/Overlays/ShellOverlayController.swift +116 -0
  91. package/ios/Sources/IonicNativeUIShellPlugin/Overlays/ShellOverlayHost.swift +36 -0
  92. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +27 -0
  93. package/package.json +2 -2
  94. package/src/native/components/index.ts +3 -2
  95. package/src/native/definitions.ts +27 -1
  96. package/src/native/index.ts +23 -6
  97. package/src/native/overlays/content.ts +51 -0
  98. package/src/native/overlays/controller.ts +322 -0
  99. package/src/native/overlays/dialog-animation.ts +30 -0
  100. package/src/native/overlays/dismiss-animation.ts +15 -0
  101. package/src/native/overlays/focus.ts +64 -0
  102. package/src/native/overlays/popover.ts +80 -0
  103. package/src/native/overlays/styles.ts +80 -0
  104. package/src/native/prehide.ts +6 -1
  105. package/src/native/runtime.ts +39 -15
  106. package/src/native/shared/dom.ts +14 -0
  107. package/src/native/shared/projection-id.ts +2 -0
  108. package/src/native/vertical-bars-web.ts +11 -1
  109. package/src/native-integration/index.ts +1 -1
  110. package/src/native-integration/transition.ts +1 -1
  111. package/src/sheets-of-glass/animations.ts +2 -2
  112. package/src/sheets-of-glass/index.ts +2 -2
  113. package/src/styles/components/ion-button.scss +47 -0
  114. package/src/styles/components/ion-toolbar.scss +62 -7
  115. package/src/styles/vertical-bars.scss +23 -0
  116. package/src/tab-bar-searchable/animations/enter.ts +2 -1
  117. package/src/tab-bar-searchable/animations/leave.ts +2 -1
  118. package/src/tab-bar-searchable/index.ts +1 -1
  119. package/src/transition/index.ts +3 -3
  120. package/src/transition/ios.transition.ts +1 -1
  121. package/src/utils.ts +1 -1
@@ -1,6 +1,7 @@
1
+ import { projectionIds } from './shared/projection-id';
1
2
  import { inVerticalBarsSurface, topModal, modalUsesVerticalBars, modalVerticalBarFrame } from './shared/modal';
2
3
  import type { PluginListenerHandle } from '@capacitor/core';
3
- import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from '@ionic/core';
4
+ import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from '@ionic/core/components/index.js';
4
5
  import { VERTICAL_BARS_TRANSITION_CANCELED, getNativeSearchBindings, setNativeUIShellIntegration } from '../native-integration';
5
6
  import { createSearchSupport } from './components/searchable-tabs';
6
7
  import type {
@@ -18,6 +19,8 @@ import {
18
19
  isVerticalBarsSource,
19
20
  preferredVerticalBarsBack,
20
21
  marker,
22
+ syncToolbarText,
23
+ toolbarTextMarker,
21
24
  prehideOnlyMutation,
22
25
  rejectedClass,
23
26
  unprojected,
@@ -33,7 +36,7 @@ const verticalBarsMarker = 'data-native-ui-shell-vertical-bars';
33
36
  const verticalBarsMemberMarker = 'data-native-ui-shell-vertical-bars-member';
34
37
 
35
38
  // A failed bridge must not leave the source inaccessible indefinitely.
36
- const bounded = <T>(promise: Promise<T>): Promise<T> =>
39
+ export const bounded = <T>(promise: Promise<T>): Promise<T> =>
37
40
  new Promise((resolve, reject) => {
38
41
  const timer = setTimeout(() => reject(new Error('Native UI Shell bridge timed out')), 5000);
39
42
  promise.then(resolve, reject).finally(() => clearTimeout(timer));
@@ -45,7 +48,7 @@ export const createRuntime = async (
45
48
  options: NativeUIShellOptions = {},
46
49
  nativeVerticalBars: () => boolean = () => true,
47
50
  verticalBarsOnly = false,
48
- ): Promise<NativeUIShellHandle> => {
51
+ ): Promise<NativeUIShellHandle & { retain: (deferActivation?: (id: string) => boolean) => () => void }> => {
49
52
  const win = doc.defaultView!;
50
53
  const icons = createIconRenderer();
51
54
  const crossfade = createCrossfade(win);
@@ -75,6 +78,7 @@ export const createRuntime = async (
75
78
  let dirty = false;
76
79
  let pending = false;
77
80
  let stopped = false;
81
+ const retentions = new Set<{ accepts?: (id: string) => boolean; activation?: ShellActivation }>();
78
82
  /** True while a tab-switch handoff should skip crossfade. */
79
83
  let tabSwitchHandoff = false;
80
84
  /** Keep instant updates until this time — DidLeave often precedes the retire sync. */
@@ -106,6 +110,7 @@ export const createRuntime = async (
106
110
  if (!value) {
107
111
  value = `shell-${++nextId}`;
108
112
  ids.set(element, value);
113
+ projectionIds.set(element, value);
109
114
  }
110
115
  return value;
111
116
  };
@@ -299,9 +304,13 @@ export const createRuntime = async (
299
304
  frame = 0;
300
305
  dirty = false;
301
306
  pending = true;
307
+ syncToolbarText(doc);
302
308
  handoffInstant = tabSwitchHandoff || handoffAcrossPending || modalPresentHandoff || win.performance.now() < handoffUntil;
303
- modalPresentHandoff = false;
304
309
  try {
310
+ // A retained sync performs no update; keep the modal handoff armed so the
311
+ // deferred retirement still runs instantly after the overlay releases.
312
+ if (retentions.size && !manualSuspensions.size) return;
313
+ modalPresentHandoff = false;
305
314
  const size = `${win.innerWidth}:${win.innerHeight}`;
306
315
  // WebKit can resize before Ionic's fixed DOM positions catch up.
307
316
  // Measure on the next frame instead of retiring a valid native search.
@@ -323,7 +332,7 @@ export const createRuntime = async (
323
332
  );
324
333
  }
325
334
  }
326
- if (stopped || dirty) return;
335
+ if (stopped || dirty || (retentions.size && !manualSuspensions.size)) return;
327
336
  const retained = new Set(candidates.flatMap(candidateSources));
328
337
  const removed = Array.from(sources.keys()).filter((element) => !retained.has(element));
329
338
  if (removed.length) {
@@ -358,7 +367,7 @@ export const createRuntime = async (
358
367
  updates++;
359
368
  updatingRevision = snapshot.revision;
360
369
  const result = await bounded(plugin.update(snapshot));
361
- if (stopped) return;
370
+ if (stopped || (retentions.size && !manualSuspensions.size)) return;
362
371
  if (result.revision !== snapshot.revision) throw new Error('Native UI Shell revision mismatch');
363
372
  if (`${win.innerWidth}:${win.innerHeight}` !== size) {
364
373
  lastSnapshot = '';
@@ -466,6 +475,7 @@ export const createRuntime = async (
466
475
  records.some(
467
476
  (record) =>
468
477
  record.attributeName !== marker &&
478
+ record.attributeName !== toolbarTextMarker &&
469
479
  record.attributeName !== fadeMarker &&
470
480
  !prehideOnlyMutation(record) &&
471
481
  !(record.attributeName === 'style' && measuringPointerPages.has(record.target as HTMLElement)) &&
@@ -633,7 +643,20 @@ export const createRuntime = async (
633
643
  on(win.visualViewport, 'resize', refreshLayout);
634
644
  on(win.visualViewport, 'scroll');
635
645
  }
636
- const handle: NativeUIShellHandle = {
646
+ const handle: NativeUIShellHandle & { retain: (deferActivation?: (id: string) => boolean) => () => void } = {
647
+ // A native overlay covers this surface; keep its controls and Web ownership intact.
648
+ retain(deferActivation) {
649
+ const retention: { accepts?: (id: string) => boolean; activation?: ShellActivation } = { accepts: deferActivation };
650
+ retentions.add(retention);
651
+ return () => {
652
+ if (!retentions.delete(retention)) return;
653
+ // UIKit may already expose the anchor while Ionic is still dismissing.
654
+ // Replay at most one validated input after the live content is restored.
655
+ const activation = retention.activation;
656
+ if (activation && retention.accepts?.(activation.id)) win.queueMicrotask(() => activate(activation));
657
+ schedule();
658
+ };
659
+ },
637
660
  getStatus: (): NativeUIShellStatus => ({
638
661
  state: stopped ? 'stopped' : sources.size ? 'native' : 'web',
639
662
  projected: sources.size,
@@ -691,15 +714,16 @@ export const createRuntime = async (
691
714
  },
692
715
  };
693
716
  const activate = (event: ShellActivation) => {
694
- if (
695
- stopped ||
696
- doc.hidden ||
697
- event.revision < acceptedRevision ||
698
- event.revision > revision ||
699
- event.sequence <= lastSequence ||
700
- overlayOpen(false, true)
701
- )
717
+ if (stopped || doc.hidden || event.revision < acceptedRevision || event.revision > revision || event.sequence <= lastSequence) return;
718
+ if (overlayOpen(false, true)) {
719
+ if (!manualSuspensions.size) {
720
+ for (const retention of retentions) {
721
+ if (retention.accepts?.(event.id) && (!retention.activation || event.sequence > retention.activation.sequence))
722
+ retention.activation = event;
723
+ }
724
+ }
702
725
  return;
726
+ }
703
727
  // Native may send input before update() resolves on the JS bridge.
704
728
  // Revalidate it after ownership is accepted, using the same action path.
705
729
  if (event.revision > acceptedRevision && event.revision === updatingRevision) {
@@ -193,6 +193,20 @@ export const isVerticalBarsToolbarGroup = (element: HTMLElement): boolean => {
193
193
  );
194
194
  };
195
195
 
196
+ export const toolbarTextMarker = 'data-native-ui-shell-toolbar-text';
197
+
198
+ export const syncToolbarText = (doc: Document): void => {
199
+ for (const toolbar of Array.from(
200
+ doc.querySelectorAll<HTMLElement>(`ion-app.ios-theme-vertical-bars ion-toolbar, ion-toolbar[${toolbarTextMarker}]`),
201
+ )) {
202
+ toolbar.toggleAttribute(
203
+ toolbarTextMarker,
204
+ !!toolbar.closest('ion-app.ios-theme-vertical-bars') &&
205
+ childNodesOf(toolbar).some((node) => node.nodeType === Node.TEXT_NODE && !!node.textContent?.trim()),
206
+ );
207
+ }
208
+ };
209
+
196
210
  export const activateProjectedElement = (element: HTMLElement): void => {
197
211
  const target = element.matches('ion-button, ion-back-button, ion-menu-button')
198
212
  ? element.shadowRoot?.querySelector<HTMLElement>('[part~="native"]')
@@ -0,0 +1,2 @@
1
+ /** Internal identity used to anchor overlays to their native control. */
2
+ export const projectionIds = new WeakMap<Element, string>();
@@ -15,6 +15,8 @@ import {
15
15
  preferredVerticalBarsBack,
16
16
  verticalBarsOwned,
17
17
  marker,
18
+ syncToolbarText,
19
+ toolbarTextMarker,
18
20
  prehideOnlyMutation,
19
21
  prehiddenClass,
20
22
  unprojected,
@@ -271,6 +273,7 @@ export const createVerticalBarsWebProjection = (
271
273
  };
272
274
  const performUpdate = () => {
273
275
  frame = 0;
276
+ syncToolbarText(doc);
274
277
  const currentRoot = verticalBarsRoot();
275
278
  if (observingVerticalBars !== !!currentRoot) {
276
279
  observingVerticalBars = !!currentRoot;
@@ -278,6 +281,7 @@ export const createVerticalBarsWebProjection = (
278
281
  observer.observe(doc.documentElement, {
279
282
  subtree: true,
280
283
  childList: true,
284
+ characterData: observingVerticalBars,
281
285
  attributes: true,
282
286
  attributeOldValue: true,
283
287
  attributeFilter: observingVerticalBars ? undefined : ['class'],
@@ -335,7 +339,13 @@ export const createVerticalBarsWebProjection = (
335
339
  );
336
340
  if (
337
341
  (observingVerticalBars &&
338
- records.some((record) => record.attributeName !== marker && !prehideOnlyMutation(record) && !insideProjection(record.target))) ||
342
+ records.some(
343
+ (record) =>
344
+ record.attributeName !== marker &&
345
+ record.attributeName !== toolbarTextMarker &&
346
+ !prehideOnlyMutation(record) &&
347
+ !insideProjection(record.target),
348
+ )) ||
339
349
  (!observingVerticalBars && verticalBarsChanged && records.some((record) => !prehideOnlyMutation(record)))
340
350
  )
341
351
  schedule();
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="es2021.weakref" />
2
- import type { Animation } from '@ionic/core';
2
+ import type { Animation } from '@ionic/core/components';
3
3
 
4
4
  export interface NativeUIShellIntegration {
5
5
  suspend(scopes: HTMLElement[]): Promise<(canceled?: boolean) => void>;
@@ -1,4 +1,4 @@
1
- import type { AnimationBuilder } from '@ionic/core';
1
+ import type { AnimationBuilder } from '@ionic/core/components';
2
2
  import { connectNativeUIShellTransition } from './index';
3
3
 
4
4
  /**
@@ -1,6 +1,6 @@
1
- import type { Animation, AnimationKeyFrames } from '@ionic/core';
1
+ import type { Animation, AnimationKeyFrames, GestureDetail } from '@ionic/core/components';
2
2
  import { AnimationPosition, EffectScales } from './interfaces';
3
- import { createAnimation, GestureDetail } from '@ionic/core';
3
+ import { createAnimation } from '@ionic/core/components/index.js';
4
4
  import { getStep } from '../utils';
5
5
 
6
6
  // Work in main/cross-axis coordinates so native horizontal samples also drive the rail.
@@ -1,6 +1,6 @@
1
1
  import { AnimationPosition, EffectScales, registeredEffect } from './interfaces';
2
- import { createAnimation, createGesture } from '@ionic/core';
3
- import type { Animation, Gesture, GestureDetail } from '@ionic/core';
2
+ import { createAnimation, createGesture } from '@ionic/core/components/index.js';
3
+ import type { Animation, Gesture, GestureDetail } from '@ionic/core/components';
4
4
  import { changeSelectedElement, cloneElement, getStep } from '../utils';
5
5
  import { isNativeUIShell } from '../native-integration';
6
6
  import {
@@ -436,3 +436,50 @@ ion-button.ios:not(.ios-theme-disabled, .ios26-disabled) {
436
436
  ion-back-button.ios:not(.ios-theme-disabled, .ios26-disabled) {
437
437
  @include theme-button($is-back-button: true);
438
438
  }
439
+
440
+ // Toolbar button typography and spacing follow system toolbar controls.
441
+ ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled) {
442
+ ion-button.ios:not(.ios-theme-disabled, .ios26-disabled):not(.button-small, .button-large):not(
443
+ :where(ion-buttons.ios-theme-disabled *, ion-buttons.ios26-disabled *)
444
+ ) {
445
+ font-size: 1rem;
446
+ font-weight: 400;
447
+ // TODO(v2.0.0): Remove .back-button-has-icon-only; Ionic only adds it to ion-back-button.
448
+ &:not(.button-has-icon-only, .back-button-has-icon-only) {
449
+ --padding-start: 16px;
450
+ --padding-end: 16px;
451
+ }
452
+ }
453
+
454
+ ion-buttons.ios:not(.ios-theme-disabled, .ios26-disabled):has(ion-button ~ ion-button):not(
455
+ :has(
456
+ ion-back-button,
457
+ ion-button:not(.button-clear),
458
+ ion-menu-button,
459
+ .ios-theme-disabled,
460
+ .ios26-disabled,
461
+ .button-small,
462
+ .button-large
463
+ )
464
+ ) {
465
+ height: 44px;
466
+ padding-inline: 3.5px;
467
+ // SwiftUI toolbar items share glass with 16pt between their inner controls.
468
+ gap: 16px;
469
+
470
+ ion-button.ios:not(.ios-theme-disabled, .ios26-disabled).button-clear {
471
+ // Keep the action's hit area through the group's height, while the
472
+ // inner plain control follows the system's 36px visual layout.
473
+ height: 100%;
474
+ display: inline-flex;
475
+ align-items: center;
476
+ --padding-start: 7px;
477
+ --padding-end: 7px;
478
+ &::part(native) {
479
+ width: auto;
480
+ height: 36px;
481
+ min-height: 36px;
482
+ }
483
+ }
484
+ }
485
+ }
@@ -13,22 +13,27 @@ ion-header.ios:not(.ios-theme-disabled, .ios26-disabled) ion-toolbar:not(.ios-th
13
13
  ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled).modal-card,
14
14
  ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled).modal-sheet {
15
15
  ion-header.ios:not(.ios-theme-disabled, .ios26-disabled) ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled):first-of-type {
16
- padding-top: 0;
16
+ // The system's modal safe area places the 44px control 16px below the
17
+ // content edge, with or without a sheet drag indicator.
18
+ padding-top: 6px;
17
19
  padding-bottom: 0;
18
20
  --min-height: 64px;
19
- --padding-start: 3px;
20
- --padding-end: 3px;
21
21
  }
22
22
  }
23
23
 
24
24
  @media (min-width: 768px) and (min-height: 600px) {
25
- ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled) {
25
+ ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled):not(.modal-sheet) {
26
26
  ion-header.ios:not(.ios-theme-disabled, .ios26-disabled) ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled):first-of-type {
27
- padding-top: 0;
27
+ // Center a 44px control at 10px, retaining the safe area in fullscreen modals.
28
+ padding-top: max(calc(var(--ion-safe-area-top, 0px) - 10px), 0px);
28
29
  padding-bottom: 0;
29
30
  --min-height: 64px;
30
- --padding-start: 3px;
31
- --padding-end: 3px;
31
+ }
32
+ }
33
+ // Ionic keeps the root safe area in card modals, including centered dialogs.
34
+ ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled).modal-card {
35
+ ion-header.ios:not(.ios-theme-disabled, .ios26-disabled) ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled):first-of-type {
36
+ padding-top: 0;
32
37
  }
33
38
  }
34
39
  }
@@ -106,4 +111,54 @@ ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled) {
106
111
  ion-buttons > ion-back-button {
107
112
  margin-inline: 0;
108
113
  }
114
+
115
+ // Keep the outer inset for individual actions too. Adjacent actions in the
116
+ // same slot share a 12px gap rather than adding two outer margins together.
117
+ > ion-button:not(.ios-theme-disabled, .ios26-disabled) {
118
+ margin-inline: 12px;
119
+ }
120
+ // Rail sources leave toolbar layout; ordinary native projection retains it.
121
+ $visible: ':not([hidden], .ios-theme-native-ui-shell-prehidden, [data-native-ui-shell][data-native-ui-shell-vertical-bars], ion-menu-button.menu-button-hidden, ion-back-button:not(.show-back-button))';
122
+ $button: ':is(ion-button, ion-back-button)#{$visible}';
123
+ $group: 'ion-buttons#{$visible}';
124
+ $action: ':is(#{$button}, #{$group}:has(> #{$visible}))';
125
+ $optOut: ':is(.ios-theme-disabled, .ios26-disabled)';
126
+
127
+ > ion-buttons:not(:has(> #{$visible})) {
128
+ display: none;
129
+ }
130
+
131
+ @each $slot in start, end, primary, secondary {
132
+ // Keep standalone margins when the slot includes a visible opted-out action.
133
+ &:not(.toolbar-searchbar):not(:has(> #{$button}#{$optOut}[slot='#{$slot}'], > #{$group}#{$optOut}[slot='#{$slot}'] > #{$visible})) {
134
+ > #{$action}[slot='#{$slot}'] {
135
+ margin-inline: 6px;
136
+ }
137
+ > :nth-child(1 of #{$action}[slot='#{$slot}']) {
138
+ margin-inline-start: 12px;
139
+ }
140
+ > :nth-last-child(1 of #{$action}[slot='#{$slot}']) {
141
+ margin-inline-end: 12px;
142
+ }
143
+ }
144
+ }
145
+ // Ionic orders these slots together, independently of their DOM order.
146
+ @each $first, $second in (start, secondary), (primary, end) {
147
+ &:not(.toolbar-searchbar):has(> #{$button}[slot='#{$first}'], > #{$group}[slot='#{$first}'] > #{$visible}):has(
148
+ > #{$button}[slot='#{$second}'],
149
+ > #{$group}[slot='#{$second}'] > #{$visible}
150
+ ):not(
151
+ :has(
152
+ > #{$button}#{$optOut}:is([slot='#{$first}'], [slot='#{$second}']),
153
+ > #{$group}#{$optOut}:is([slot='#{$first}'], [slot='#{$second}']) > #{$visible}
154
+ )
155
+ ) {
156
+ > :nth-last-child(1 of #{$action}[slot='#{$first}']) {
157
+ margin-inline-end: 6px;
158
+ }
159
+ > :nth-child(1 of #{$action}[slot='#{$second}']) {
160
+ margin-inline-start: 6px;
161
+ }
162
+ }
163
+ }
109
164
  }
@@ -92,8 +92,31 @@ ion-app.ios-theme-vertical-bars
92
92
  --ion-safe-area-left: 0px;
93
93
  --ion-safe-area-right: 0px;
94
94
 
95
+ // Reserve the final layout before a page transition, using the same prehide
96
+ // decision as its rail controls. Horizontal native projection keeps its space.
97
+ // Titles, custom content and horizontal-only controls keep the toolbar visible.
98
+ &:not([data-native-ui-shell-toolbar-text]):has(
99
+ > .ios-theme-native-ui-shell-prehidden,
100
+ > ion-buttons > .ios-theme-native-ui-shell-prehidden
101
+ ):not(:has(> :not(ion-buttons, .ios-theme-native-ui-shell-prehidden))):not(
102
+ :has(> ion-buttons:not(.ios-theme-native-ui-shell-prehidden) > :not(.ios-theme-native-ui-shell-prehidden))
103
+ ) {
104
+ display: none;
105
+ }
106
+
95
107
  // Collapse headers already sit inside the padded ion-content scroll area.
96
108
  &:not(:where(ion-header.header-collapse-condense *, ion-footer.footer-collapse-fade *)) {
109
+ // Ionic's centered iOS title is absolute and does not follow container padding.
110
+ // Center it within the same foreground width as the page content.
111
+ > ion-title.ios.title-default:not(.ios-theme-disabled, .ios26-disabled) {
112
+ inset-inline-start: var(--ios-theme-vertical-bars-safe-area-left-resolved);
113
+ width: calc(100% - var(--ios-theme-vertical-bars-safe-area-left-resolved) - var(--ios-theme-vertical-bars-safe-area-right-resolved));
114
+
115
+ &:dir(rtl) {
116
+ inset-inline-start: var(--ios-theme-vertical-bars-safe-area-right-resolved);
117
+ }
118
+ }
119
+
97
120
  &::part(container) {
98
121
  padding-inline-start: calc(var(--padding-start, 0px) + var(--ios-theme-vertical-bars-safe-area-left-resolved));
99
122
  padding-inline-end: calc(var(--padding-end, 0px) + var(--ios-theme-vertical-bars-safe-area-right-resolved));
@@ -1,4 +1,5 @@
1
- import { Animation, createAnimation } from '@ionic/core';
1
+ import type { Animation } from '@ionic/core/components';
2
+ import { createAnimation } from '@ionic/core/components/index.js';
2
3
  import { ANIMATION_DELAY_CLOSE_BUTTONS, OPACITY_TRANSITION, type ElementReferences, type ElementSizes } from '@rdlabo/ionic-theme-utils';
3
4
  import { cloneElement } from '../../utils';
4
5
 
@@ -1,4 +1,5 @@
1
- import { Animation, createAnimation } from '@ionic/core';
1
+ import type { Animation } from '@ionic/core/components';
2
+ import { createAnimation } from '@ionic/core/components/index.js';
2
3
  import { ANIMATION_DELAY_CLOSE_BUTTONS, OPACITY_TRANSITION, type ElementReferences, type ElementSizes } from '@rdlabo/ionic-theme-utils';
3
4
  import { cloneElement } from '../../utils';
4
5
 
@@ -1,4 +1,4 @@
1
- import { createAnimation } from '@ionic/core';
1
+ import { createAnimation } from '@ionic/core/components/index.js';
2
2
  import { addSearchDismissButton } from './dismiss-button';
3
3
  import {
4
4
  ANIMATION_DELAY_BASE,
@@ -1,9 +1,9 @@
1
1
  import { config } from '../utils';
2
2
  import { Build, writeTask } from '@stencil/core';
3
3
 
4
- import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core';
5
- import type { NavOptions, NavDirection } from '@ionic/core';
6
- import type { Animation, AnimationBuilder } from '@ionic/core';
4
+ import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core/components/index.js';
5
+ import type { NavOptions, NavDirection } from '@ionic/core/components';
6
+ import type { Animation, AnimationBuilder } from '@ionic/core/components';
7
7
  import { createFocusController } from '../focus-controller';
8
8
  import { raf } from '../utils';
9
9
 
@@ -1,5 +1,5 @@
1
1
  import { createIosTransitionAnimation, shadow } from '@rdlabo/ionic-theme-utils';
2
- import type { Animation } from '@ionic/core';
2
+ import type { Animation } from '@ionic/core/components';
3
3
  import type { TransitionOptions } from './index';
4
4
  import { getIonPageElement } from './index';
5
5
  import { withNativeUIShellTransition } from '../native-integration/transition';
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AnimationPosition } from './sheets-of-glass/interfaces';
2
- import { IonicConfig } from '@ionic/core';
2
+ import type { IonicConfig } from '@ionic/core/components';
3
3
 
4
4
  declare const __zone_symbol__requestAnimationFrame: ((callback: FrameRequestCallback) => number) | undefined;
5
5
  declare const requestAnimationFrame: ((callback: FrameRequestCallback) => number) | undefined;