@rdlabo/ionic-theme-ios27 1.1.0 → 1.2.0-0

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 (93) hide show
  1. package/README.md +8 -1
  2. package/dist/css/components/ion-list.css +1 -1
  3. package/dist/css/components/ion-menu.css +1 -1
  4. package/dist/css/ionic-theme-ios27.css +1 -1
  5. package/dist/css/vertical-bars.css +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +15 -8
  8. package/dist/native/components/index.d.ts +1 -0
  9. package/dist/native/components/index.d.ts.map +1 -1
  10. package/dist/native/components/index.js +8 -3
  11. package/dist/native/components/ion-back-button.d.ts.map +1 -1
  12. package/dist/native/components/ion-back-button.js +5 -2
  13. package/dist/native/components/ion-button.d.ts.map +1 -1
  14. package/dist/native/components/ion-button.js +10 -2
  15. package/dist/native/components/ion-buttons.d.ts.map +1 -1
  16. package/dist/native/components/ion-buttons.js +12 -4
  17. package/dist/native/components/ion-fab.js +4 -4
  18. package/dist/native/components/ion-menu-button.d.ts.map +1 -1
  19. package/dist/native/components/ion-menu-button.js +5 -4
  20. package/dist/native/components/searchable-tabs.d.ts.map +1 -1
  21. package/dist/native/components/searchable-tabs.js +4 -1
  22. package/dist/native/definitions.d.ts +29 -3
  23. package/dist/native/definitions.d.ts.map +1 -1
  24. package/dist/native/definitions.js +6 -1
  25. package/dist/native/index.d.ts +6 -2
  26. package/dist/native/index.d.ts.map +1 -1
  27. package/dist/native/index.js +154 -17
  28. package/dist/native/prehide.d.ts +5 -0
  29. package/dist/native/prehide.d.ts.map +1 -0
  30. package/dist/native/prehide.js +246 -0
  31. package/dist/native/runtime.d.ts +1 -1
  32. package/dist/native/runtime.d.ts.map +1 -1
  33. package/dist/native/runtime.js +162 -28
  34. package/dist/native/shared/candidate.d.ts.map +1 -1
  35. package/dist/native/shared/candidate.js +7 -3
  36. package/dist/native/shared/dom.d.ts +33 -2
  37. package/dist/native/shared/dom.d.ts.map +1 -1
  38. package/dist/native/shared/dom.js +171 -10
  39. package/dist/native/vertical-bars-web.d.ts +3 -0
  40. package/dist/native/vertical-bars-web.d.ts.map +1 -0
  41. package/dist/native/vertical-bars-web.js +341 -0
  42. package/dist/native-integration/index.d.ts +3 -2
  43. package/dist/native-integration/index.d.ts.map +1 -1
  44. package/dist/native-integration/index.js +11 -5
  45. package/dist/transition/index.d.ts +2 -2
  46. package/dist/transition/index.d.ts.map +1 -1
  47. package/dist/transition/index.js +1 -1
  48. package/dist/transition/ios.transition.d.ts.map +1 -1
  49. package/dist/transition/ios.transition.js +1 -0
  50. package/dist/utils.d.ts +3 -3
  51. package/dist/utils.d.ts.map +1 -1
  52. package/dist/utils.js +1 -1
  53. package/dist/vertical-bars.d.ts +4 -0
  54. package/dist/vertical-bars.d.ts.map +1 -0
  55. package/dist/vertical-bars.js +2 -0
  56. package/docs/iphone-duo.md +255 -0
  57. package/docs/migration.md +2 -2
  58. package/docs/native-ui-shell.md +15 -3
  59. package/docs/special-markup.md +6 -0
  60. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +15 -1
  61. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +358 -0
  62. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +215 -35
  63. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +10 -1
  64. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +29 -0
  65. package/package.json +6 -2
  66. package/src/index.ts +15 -8
  67. package/src/native/components/index.ts +12 -3
  68. package/src/native/components/ion-back-button.ts +8 -2
  69. package/src/native/components/ion-button.ts +12 -2
  70. package/src/native/components/ion-buttons.ts +9 -4
  71. package/src/native/components/ion-fab.ts +4 -4
  72. package/src/native/components/ion-menu-button.ts +5 -4
  73. package/src/native/components/searchable-tabs.ts +9 -1
  74. package/src/native/definitions.ts +40 -3
  75. package/src/native/index.ts +188 -20
  76. package/src/native/prehide.ts +255 -0
  77. package/src/native/runtime.ts +173 -27
  78. package/src/native/shared/candidate.ts +11 -3
  79. package/src/native/shared/dom.ts +212 -11
  80. package/src/native/vertical-bars-web.ts +374 -0
  81. package/src/native-integration/index.ts +13 -8
  82. package/src/styles/components/ion-list.scss +2 -2
  83. package/src/styles/components/ion-menu.scss +2 -2
  84. package/src/styles/ionic-theme-ios27.scss +1 -0
  85. package/src/styles/vertical-bars.scss +342 -0
  86. package/src/transition/index.ts +11 -6
  87. package/src/transition/ios.transition.ts +1 -0
  88. package/src/utils.ts +8 -8
  89. package/src/vertical-bars.ts +3 -0
  90. package/dist/native/lifecycle.d.ts +0 -4
  91. package/dist/native/lifecycle.d.ts.map +0 -1
  92. package/dist/native/lifecycle.js +0 -34
  93. package/src/native/lifecycle.ts +0 -38
@@ -2,7 +2,7 @@
2
2
  import type { Animation } from '@ionic/core';
3
3
 
4
4
  export interface NativeUIShellIntegration {
5
- suspend(scopes: HTMLElement[]): Promise<() => void>;
5
+ suspend(scopes: HTMLElement[]): Promise<(canceled?: boolean) => void>;
6
6
  search?(binding: NativeSearchBinding, active: boolean, focus?: boolean): Promise<boolean>;
7
7
  }
8
8
 
@@ -32,6 +32,7 @@ export const requestNativeSearch = async (binding: NativeSearchBinding, active:
32
32
  (await runtimes.get(binding.tabBar.ownerDocument)?.search?.(binding, active, focus)) ?? false;
33
33
 
34
34
  const runtimes = new WeakMap<Document, NativeUIShellIntegration>();
35
+ export const VERTICAL_BARS_TRANSITION_CANCELED = 'iosThemeVerticalBarsTransitionCanceled';
35
36
 
36
37
  export const setNativeUIShellIntegration = (doc: Document, runtime?: NativeUIShellIntegration) => {
37
38
  if (runtime) runtimes.set(doc, runtime);
@@ -40,11 +41,15 @@ export const setNativeUIShellIntegration = (doc: Document, runtime?: NativeUIShe
40
41
 
41
42
  export const isNativeUIShell = (element: HTMLElement) => element.hasAttribute('data-native-ui-shell');
42
43
 
43
- export const suspendNativeUIShell = async (scopes: HTMLElement[]): Promise<() => void> =>
44
+ export const suspendNativeUIShell = async (scopes: HTMLElement[]): Promise<(canceled?: boolean) => void> =>
44
45
  (await runtimes.get(scopes[0]?.ownerDocument)?.suspend(scopes)) ?? (() => {});
45
46
 
46
47
  /** Ionic write hooks cannot await the bridge. Gate playback, including interactive playback. */
47
48
  export const connectNativeUIShellTransition = (animation: Animation, entering: HTMLElement, leaving?: HTMLElement) => {
49
+ if (leaving)
50
+ animation.onFinish((step) => {
51
+ if (step === 0) leaving.dispatchEvent(new CustomEvent(VERTICAL_BARS_TRANSITION_CANCELED, { bubbles: true, detail: { entering } }));
52
+ });
48
53
  if (!runtimes.has(entering.ownerDocument)) return;
49
54
  const scopes = leaving ? [entering, leaving] : [entering];
50
55
  const play = animation.play.bind(animation);
@@ -52,7 +57,7 @@ export const connectNativeUIShellTransition = (animation: Animation, entering: H
52
57
  const progressStep = animation.progressStep.bind(animation);
53
58
  const progressEnd = animation.progressEnd.bind(animation);
54
59
  const destroy = animation.destroy.bind(animation);
55
- let release: (() => void) | undefined;
60
+ let release: ((canceled?: boolean) => void) | undefined;
56
61
  let preparation: Promise<void> | undefined;
57
62
  let disposed = false;
58
63
  let interactiveReady = false;
@@ -60,15 +65,15 @@ export const connectNativeUIShellTransition = (animation: Animation, entering: H
60
65
  let end: Parameters<Animation['progressEnd']> | undefined;
61
66
  const prepare = () =>
62
67
  (preparation ??= suspendNativeUIShell(scopes).then((resume) => {
63
- if (disposed) resume();
68
+ if (disposed) resume(true);
64
69
  else release = resume;
65
70
  }));
66
- const finish = () => {
67
- release?.();
71
+ const finish = (canceled = false) => {
72
+ release?.(canceled);
68
73
  release = undefined;
69
74
  preparation = undefined;
70
75
  };
71
- animation.onFinish(finish);
76
+ animation.onFinish((step) => finish(step === 0));
72
77
  animation.play = async (options) => {
73
78
  await prepare();
74
79
  if (disposed) return;
@@ -103,7 +108,7 @@ export const connectNativeUIShellTransition = (animation: Animation, entering: H
103
108
  };
104
109
  animation.destroy = (...args) => {
105
110
  disposed = true;
106
- finish();
111
+ finish(true);
107
112
  return destroy(...args);
108
113
  };
109
114
  };
@@ -34,11 +34,11 @@ ion-list.list-inset.ios:not(.ios-theme-disabled, .ios26-disabled) {
34
34
  --inner-padding-end: 0;
35
35
 
36
36
  &::part(native) {
37
- padding-right: calc(var(--ion-safe-area-right, 0px) + 18px);
37
+ padding-inline-end: calc(var(--ion-safe-area-right, 0px) + 18px);
38
38
  }
39
39
 
40
40
  &:dir(rtl)::part(native) {
41
- padding-left: calc(var(--ion-safe-area-left, 0px) + 18px);
41
+ padding-inline-end: calc(var(--ion-safe-area-left, 0px) + 18px);
42
42
  }
43
43
 
44
44
  --min-height: 52px;
@@ -4,7 +4,7 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled) {
4
4
 
5
5
  ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:has(ion-menu.menu-pane-visible) {
6
6
  .split-pane-main {
7
- ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
7
+ ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled):not(:where(.ios-theme-vertical-bars *)) {
8
8
  width: calc(
9
9
  100% - calc(18px + var(--ion-safe-area-left, 0px)) - calc(18px + var(--ion-safe-area-right, 0px)) - 60px -
10
10
  12px - var(--ios-theme-menu-width, var(--ios26-menu-width))
@@ -43,7 +43,7 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
43
43
  ion-footer ion-toolbar {
44
44
  --padding-start: var(--ios-theme-menu-width, var(--ios26-menu-width));
45
45
  }
46
- ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
46
+ ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled):not(:where(.ios-theme-vertical-bars *)) {
47
47
  &[slot='bottom'] {
48
48
  inset-inline-start: calc(16px + var(--ion-safe-area-left, 0px) + var(--ios-theme-menu-width, var(--ios26-menu-width)));
49
49
  &:dir(rtl) {
@@ -21,3 +21,4 @@
21
21
  @use 'components/ion-toast';
22
22
  @use 'components/ion-toggle';
23
23
  @use 'components/ion-toolbar';
24
+ @use 'vertical-bars';
@@ -0,0 +1,342 @@
1
+ @use 'utils/api';
2
+ @use 'utils/glass';
3
+
4
+ // iPhone Duo simulation: the system reserves 80pt at the physical right edge.
5
+ // This entry point does not load the iOS 27 theme or change ordinary Ionic UI.
6
+ // A split menu is independent of the vertical control area. Its width is
7
+ // selected by the application from the device posture, not the viewport width.
8
+ @property --ios-theme-split-pane-width {
9
+ syntax: '<length>';
10
+ inherits: false;
11
+ initial-value: 320px;
12
+ }
13
+
14
+ ion-split-pane.ios-theme-split-pane-half-open {
15
+ --ios-theme-split-pane-width: 50vw;
16
+ }
17
+
18
+ ion-app.ios-theme-vertical-bars {
19
+ --ios-theme-vertical-bars-safe-area-left-resolved: var(--ios-theme-vertical-bars-safe-area-left, 0px);
20
+ --ios-theme-vertical-bars-safe-area-right-resolved: var(
21
+ --ios-theme-vertical-bars-safe-area-right,
22
+ var(--ios-theme-vertical-bars-native-inset, max(80px, env(safe-area-inset-right, 0px)))
23
+ );
24
+ }
25
+
26
+ ion-app.ios-theme-vertical-bars.ios-theme-vertical-bars-left {
27
+ --ios-theme-vertical-bars-safe-area-left-resolved: var(
28
+ --ios-theme-vertical-bars-safe-area-left,
29
+ var(--ios-theme-vertical-bars-native-inset, max(80px, env(safe-area-inset-left, 0px)))
30
+ );
31
+ --ios-theme-vertical-bars-safe-area-right-resolved: var(--ios-theme-vertical-bars-safe-area-right, 0px);
32
+ }
33
+
34
+ // Supply material defaults only to Web projections, never to ordinary controls.
35
+ ion-app.ios-theme-vertical-bars.ios-theme-vertical-bars-toolbar-ready
36
+ > :is(ion-menu-button, ion-button).ios-theme-vertical-bars-toolbar-projection {
37
+ --ios26-glass-background-rgb: var(--ion-background-color-rgb, 255, 255, 255);
38
+ --ios26-glass-border-color-rgb: var(--ion-background-color-rgb, 255, 255, 255);
39
+ --ios26-glass-box-shadow-color-rgb: var(--ion-text-color-rgb, 0, 0, 0);
40
+ }
41
+
42
+ // The transition shade belongs to the Web page, never to the control area.
43
+ ion-app.ios-theme-vertical-bars .ios-transition-shade {
44
+ clip-path: inset(0 var(--ios-theme-vertical-bars-safe-area-right-resolved) 0 var(--ios-theme-vertical-bars-safe-area-left-resolved));
45
+ }
46
+
47
+ // Keep page backgrounds and the router full width; move only their foregrounds.
48
+ ion-app.ios-theme-vertical-bars
49
+ ion-content:is(.ios, .md):not(.ios-theme-disabled, .ios26-disabled):not(:where(ion-menu *, ion-modal *, ion-popover *)) {
50
+ --ion-safe-area-left: 0px;
51
+ --ion-safe-area-right: 0px;
52
+
53
+ &::part(scroll) {
54
+ padding-inline-start: calc(var(--padding-start, 0px) + var(--ios-theme-vertical-bars-safe-area-left-resolved));
55
+ padding-inline-end: calc(var(--padding-end, 0px) + var(--ios-theme-vertical-bars-safe-area-right-resolved));
56
+ }
57
+
58
+ &:dir(rtl)::part(scroll) {
59
+ padding-inline-start: calc(var(--padding-start, 0px) + var(--ios-theme-vertical-bars-safe-area-right-resolved));
60
+ padding-inline-end: calc(var(--padding-end, 0px) + var(--ios-theme-vertical-bars-safe-area-left-resolved));
61
+ }
62
+ }
63
+
64
+ ion-app.ios-theme-vertical-bars
65
+ ion-toolbar:is(.ios, .md):not(.ios-theme-disabled, .ios26-disabled):not(:where(ion-menu *, ion-modal *, ion-popover *)) {
66
+ --ion-safe-area-left: 0px;
67
+ --ion-safe-area-right: 0px;
68
+
69
+ // Collapse headers already sit inside the padded ion-content scroll area.
70
+ &:not(:where(ion-header.header-collapse-condense *, ion-footer.footer-collapse-fade *)) {
71
+ &::part(container) {
72
+ padding-inline-start: calc(var(--padding-start, 0px) + var(--ios-theme-vertical-bars-safe-area-left-resolved));
73
+ padding-inline-end: calc(var(--padding-end, 0px) + var(--ios-theme-vertical-bars-safe-area-right-resolved));
74
+ }
75
+
76
+ &:dir(rtl)::part(container) {
77
+ padding-inline-start: calc(var(--padding-start, 0px) + var(--ios-theme-vertical-bars-safe-area-right-resolved));
78
+ padding-inline-end: calc(var(--padding-end, 0px) + var(--ios-theme-vertical-bars-safe-area-left-resolved));
79
+ }
80
+ }
81
+ }
82
+
83
+ ion-app.ios-theme-vertical-bars
84
+ ion-fab:is(.ios, .md):not(.ios-theme-disabled, .ios26-disabled):not(:where(ion-menu *, ion-modal *, ion-popover *)) {
85
+ --ion-safe-area-left: 0px;
86
+ --ion-safe-area-right: 0px;
87
+ --ios-theme-vertical-bars-fab-edge-gap: 16px;
88
+ --ios-theme-vertical-bars-fab-start-offset: var(--ios-theme-menu-width, var(--ios26-menu-width, 0px));
89
+
90
+ &.fab-horizontal-start {
91
+ inset-inline-start: calc(
92
+ var(--ios-theme-vertical-bars-fab-edge-gap) + var(--ios-theme-vertical-bars-fab-start-offset) +
93
+ var(--ios-theme-vertical-bars-safe-area-left-resolved)
94
+ );
95
+ }
96
+ &.fab-horizontal-end {
97
+ inset-inline-end: calc(var(--ios-theme-vertical-bars-fab-edge-gap) + var(--ios-theme-vertical-bars-safe-area-right-resolved));
98
+ }
99
+ &:dir(rtl).fab-horizontal-start {
100
+ inset-inline-start: calc(
101
+ var(--ios-theme-vertical-bars-fab-edge-gap) + var(--ios-theme-vertical-bars-fab-start-offset) +
102
+ var(--ios-theme-vertical-bars-safe-area-right-resolved)
103
+ );
104
+ }
105
+ &:dir(rtl).fab-horizontal-end {
106
+ inset-inline-end: calc(var(--ios-theme-vertical-bars-fab-edge-gap) + var(--ios-theme-vertical-bars-safe-area-left-resolved));
107
+ }
108
+ &:has(.fab-button-small) {
109
+ --ios-theme-vertical-bars-fab-edge-gap: 10px;
110
+ }
111
+ }
112
+
113
+ // Menus retain their own safe-area handling and full-width animation host.
114
+ ion-app.ios-theme-vertical-bars {
115
+ ion-menu:is(.ios, .md).menu-side-end:not(:dir(rtl)):not(.ios-theme-disabled, .ios26-disabled),
116
+ ion-menu:is(.ios, .md).menu-side-start:dir(rtl):not(.ios-theme-disabled, .ios26-disabled) {
117
+ --ion-safe-area-right: 0px;
118
+ &::part(container) {
119
+ margin-right: var(--ios-theme-vertical-bars-safe-area-right-resolved);
120
+ }
121
+ }
122
+ ion-menu:is(.ios, .md).menu-side-start:not(:dir(rtl)):not(.ios-theme-disabled, .ios26-disabled),
123
+ ion-menu:is(.ios, .md).menu-side-end:dir(rtl):not(.ios-theme-disabled, .ios26-disabled) {
124
+ --ion-safe-area-left: 0px;
125
+ &::part(container) {
126
+ margin-left: var(--ios-theme-vertical-bars-safe-area-left-resolved);
127
+ }
128
+ }
129
+ }
130
+
131
+ ion-app.ios-theme-vertical-bars
132
+ ion-tabs:not(:where(ion-menu *, ion-modal *, ion-popover *))
133
+ > ion-tab-bar:is(.ios, .md):not(.ios-theme-disabled, .ios26-disabled) {
134
+ --ios-theme-side-tab-bar-width: 50px;
135
+ --ios-theme-side-tab-bar-gap: 8px;
136
+ --ios-theme-side-safe-area: var(--ios-theme-vertical-bars-safe-area-right-resolved);
137
+ contain: layout style;
138
+ position: absolute;
139
+ z-index: 2;
140
+ display: flex;
141
+ flex-direction: column;
142
+ width: 46px;
143
+ min-width: 46px;
144
+ max-width: 46px;
145
+ height: fit-content;
146
+ min-height: 0;
147
+ padding: 2px;
148
+ border-radius: 25px;
149
+ left: auto;
150
+ right: max(4px, calc((var(--ios-theme-side-safe-area) - var(--ios-theme-side-tab-bar-width)) / 2));
151
+ margin: 0;
152
+ top: auto;
153
+ bottom: calc(var(--ion-safe-area-bottom, 0px) + var(--ios-theme-side-tab-bar-gap));
154
+
155
+ > ion-tab-button:not(.ion-cloned-element) {
156
+ display: block;
157
+ flex: none;
158
+ width: 46px;
159
+ min-width: 46px;
160
+ height: 52px;
161
+ min-height: 52px;
162
+ border-radius: 23px;
163
+ &::part(native) {
164
+ justify-content: center;
165
+ }
166
+ ion-label {
167
+ position: absolute;
168
+ width: 1px;
169
+ height: 1px;
170
+ padding: 0;
171
+ margin: -1px;
172
+ overflow: hidden;
173
+ clip: rect(0, 0, 0, 0);
174
+ white-space: nowrap;
175
+ border: 0;
176
+ }
177
+ ion-icon {
178
+ margin: 0;
179
+ font-size: 28px;
180
+ }
181
+ }
182
+ > ion-tab-button:not(.ion-cloned-element) ~ ion-tab-button:not(.ion-cloned-element) {
183
+ margin-block-start: 0;
184
+ margin-inline-start: 0;
185
+ }
186
+ &:has(> ion-tab-button:is(.ion-activated, :active)) > ion-tab-button:not(.ion-cloned-element) {
187
+ &::part(native) {
188
+ flex-direction: column;
189
+ padding-block: 4px;
190
+ }
191
+ ion-label {
192
+ position: static;
193
+ width: auto;
194
+ height: auto;
195
+ max-width: 44px;
196
+ padding: 0;
197
+ margin: 1px 0 0;
198
+ overflow: hidden;
199
+ clip: auto;
200
+ font-size: 0.5rem;
201
+ line-height: 1;
202
+ text-overflow: ellipsis;
203
+ white-space: nowrap;
204
+ }
205
+ ion-icon {
206
+ margin: 0;
207
+ font-size: 22px;
208
+ }
209
+ }
210
+ }
211
+
212
+ ion-app.ios-theme-vertical-bars.ios-theme-vertical-bars-left
213
+ ion-tabs:not(:where(ion-menu *, ion-modal *, ion-popover *))
214
+ > ion-tab-bar:is(.ios, .md):not(.ios-theme-disabled, .ios26-disabled) {
215
+ --ios-theme-side-safe-area: var(--ios-theme-vertical-bars-safe-area-left-resolved);
216
+ right: auto;
217
+ left: max(4px, calc((var(--ios-theme-side-safe-area) - var(--ios-theme-side-tab-bar-width)) / 2));
218
+ }
219
+
220
+ .ios-theme-native-ui-shell-prehidden {
221
+ position: absolute !important;
222
+ visibility: hidden !important;
223
+ }
224
+
225
+ // Hide an entering back source until JS decides whether it belongs in the rail.
226
+ html.ios-theme-native-ui-shell-prehide
227
+ ion-app.ios-theme-vertical-bars
228
+ ion-back-button:not(
229
+ [icon],
230
+ [color],
231
+ .ios-theme-vertical-bars-back-web-owned,
232
+ .ionic-theme-disabled,
233
+ .ios-theme-disabled,
234
+ .ios26-disabled,
235
+ .ios-theme-vertical-bars-back-button-projection
236
+ ):not(
237
+ :where(
238
+ ion-menu *,
239
+ ion-modal *,
240
+ ion-popover *,
241
+ ion-header[collapse] *,
242
+ ion-footer[collapse] *,
243
+ ion-buttons.ios-theme-horizontal-only *,
244
+ .ios-theme-shell-disabled *,
245
+ .ionic-theme-disabled *,
246
+ .ios-theme-disabled *,
247
+ .ios26-disabled *
248
+ )
249
+ ) {
250
+ position: absolute !important;
251
+ visibility: hidden !important;
252
+ }
253
+
254
+ ion-app.ios-theme-vertical-bars.ios-theme-vertical-bars-toolbar-ready {
255
+ ion-toolbar:is(.ios, .md):not(.ios-theme-disabled, .ios26-disabled):not(:where(ion-menu *, ion-modal *, ion-popover *))
256
+ ion-back-button:is(.ios, .md)[data-native-ui-shell]:not(
257
+ .ios-theme-disabled,
258
+ .ios26-disabled,
259
+ .ios-theme-vertical-bars-back-button-projection
260
+ ) {
261
+ display: none;
262
+ }
263
+ ion-toolbar:is(.ios, .md):not(.ios-theme-disabled, .ios26-disabled):not(:where(ion-menu *, ion-modal *, ion-popover *))
264
+ ion-buttons:is(.ios, .md)
265
+ > :is(ion-button, ion-menu-button):is(.ios, .md)[data-native-ui-shell]:not(
266
+ .ios-theme-disabled,
267
+ .ios26-disabled,
268
+ .ios-theme-vertical-bars-toolbar-action
269
+ ) {
270
+ display: none;
271
+ }
272
+ ion-toolbar:is(.ios, .md):not(.ios-theme-disabled, .ios26-disabled):not(:where(ion-menu *, ion-modal *, ion-popover *))
273
+ ion-buttons:is(.ios, .md):has(> :is(ion-back-button, ion-button, ion-menu-button)[data-native-ui-shell]):not(
274
+ :has(> :is(ion-back-button, ion-button, ion-menu-button):not([data-native-ui-shell]))
275
+ ) {
276
+ display: none;
277
+ }
278
+ > ion-back-button.ios-theme-vertical-bars-back-button-projection {
279
+ position: fixed;
280
+ z-index: 1001;
281
+ top: var(--ios-theme-vertical-bars-toolbar-top, 220px);
282
+ right: max(4px, calc((var(--ios-theme-vertical-bars-safe-area-right-resolved) - 46px) / 2));
283
+ display: block;
284
+ width: 46px;
285
+ height: 46px;
286
+ margin: 0;
287
+ }
288
+ > :is(.ios, .md).ios-theme-vertical-bars-toolbar-projection {
289
+ position: fixed;
290
+ z-index: 1001;
291
+ top: calc(var(--ios-theme-vertical-bars-toolbar-top, 220px) + var(--ios-theme-vertical-bars-toolbar-offset, 0px));
292
+ right: max(4px, calc((var(--ios-theme-vertical-bars-safe-area-right-resolved) - 46px) / 2));
293
+ width: 46px;
294
+ min-width: 46px;
295
+ height: 46px;
296
+ min-height: 46px;
297
+ margin: 0;
298
+ :is(ion-icon, svg) {
299
+ width: 22px;
300
+ height: 22px;
301
+ margin: 0;
302
+ font-size: 22px;
303
+ }
304
+ }
305
+ > ion-menu-button:is(.ios, .md).ios-theme-vertical-bars-toolbar-projection {
306
+ @include api.glass-control-background;
307
+ border-radius: 50%;
308
+ }
309
+ > ion-button:is(.ios, .md).ios-theme-vertical-bars-toolbar-projection {
310
+ --background: rgba(var(--ios-theme-glass-background-rgb, var(--ios26-glass-background-rgb)), 0.72);
311
+ --border-radius: 50%;
312
+ @include glass.light-shadow('--box-shadow');
313
+ &::part(native) {
314
+ @include api.glass-control-background($include-background: false, $shadow: var(--box-shadow));
315
+ }
316
+ }
317
+ > ion-buttons:is(.ios, .md).ios-theme-vertical-bars-toolbar-projection {
318
+ display: flex;
319
+ flex-direction: column;
320
+ height: auto;
321
+ min-height: 0;
322
+ > :is(ion-button, ion-menu-button):is(.ios, .md).ios-theme-vertical-bars-toolbar-action {
323
+ display: block;
324
+ width: 44px;
325
+ min-width: 44px;
326
+ height: 46px;
327
+ min-height: 46px;
328
+ margin: 0 1px;
329
+ &::part(native) {
330
+ margin-inline: auto;
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ ion-app.ios-theme-vertical-bars.ios-theme-vertical-bars-left {
337
+ > ion-back-button.ios-theme-vertical-bars-back-button-projection,
338
+ > :is(.ios, .md).ios-theme-vertical-bars-toolbar-projection {
339
+ right: auto;
340
+ left: max(4px, calc((var(--ios-theme-vertical-bars-safe-area-left-resolved) - 46px) / 2));
341
+ }
342
+ }
@@ -170,7 +170,7 @@ const waitForReady = async (opts: TransitionOptions, defaultDeep: boolean) => {
170
170
  await notifyViewReady(opts.viewIsReady, opts.enteringEl);
171
171
  };
172
172
 
173
- const notifyViewReady = async (viewIsReady: undefined | ((enteringEl: HTMLElement) => Promise<any>), enteringEl: HTMLElement) => {
173
+ const notifyViewReady = async (viewIsReady: undefined | ((enteringEl: HTMLElement) => Promise<unknown>), enteringEl: HTMLElement) => {
174
174
  if (viewIsReady) {
175
175
  await viewIsReady(enteringEl);
176
176
  }
@@ -180,7 +180,7 @@ const playTransition = (trans: Animation, opts: TransitionOptions): Promise<bool
180
180
  const progressCallback = opts.progressCallback;
181
181
 
182
182
  const promise = new Promise<boolean>((resolve) => {
183
- trans.onFinish((currentStep: any) => resolve(currentStep === 1));
183
+ trans.onFinish((currentStep) => resolve(currentStep === 1));
184
184
  });
185
185
 
186
186
  // cool, let's do this, start the transition
@@ -236,8 +236,13 @@ export const waitForMount = (): Promise<void> => {
236
236
  return new Promise((resolve) => raf(() => raf(() => resolve())));
237
237
  };
238
238
 
239
- export const deepReady = async (el: any | undefined): Promise<void> => {
240
- const element = el as any;
239
+ type LazyLoadedElement = Element & {
240
+ componentOnReady?: () => Promise<unknown>;
241
+ __registerHost?: unknown;
242
+ };
243
+
244
+ export const deepReady = async (el: Element | undefined): Promise<void> => {
245
+ const element = el as LazyLoadedElement | undefined;
241
246
  if (element) {
242
247
  if (element.componentOnReady != null) {
243
248
  // eslint-disable-next-line custom-rules/no-component-on-ready-method
@@ -259,7 +264,7 @@ export const deepReady = async (el: any | undefined): Promise<void> => {
259
264
 
260
265
  return;
261
266
  }
262
- await Promise.all(Array.from(element.children).map(deepReady));
267
+ await Promise.all(Array.from(element.children).map((child) => deepReady(child)));
263
268
  }
264
269
  };
265
270
 
@@ -358,7 +363,7 @@ const getIosIonHeader = (opts: TransitionOptions): HTMLElement | null => {
358
363
 
359
364
  export interface TransitionOptions extends NavOptions {
360
365
  progressCallback?: (ani: Animation | undefined) => void;
361
- baseEl: any;
366
+ baseEl: HTMLElement;
362
367
  enteringEl: HTMLElement;
363
368
  leavingEl: HTMLElement | undefined;
364
369
  }
@@ -14,6 +14,7 @@ const transitionConfig = {
14
14
  offLeftPercent: 30,
15
15
  getIonPageElement,
16
16
  connectNativeUIShellTransition,
17
+ shouldAnimateFixedBackButton: (navEl: HTMLElement) => !navEl.closest('ion-app.ios-theme-vertical-bars'),
17
18
  radius: 0,
18
19
  };
19
20
 
package/src/utils.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { AnimationPosition } from './sheets-of-glass/interfaces';
2
2
  import { IonicConfig } from '@ionic/core';
3
3
 
4
- declare const __zone_symbol__requestAnimationFrame: any;
5
- declare const requestAnimationFrame: any;
4
+ declare const __zone_symbol__requestAnimationFrame: ((callback: FrameRequestCallback) => number) | undefined;
5
+ declare const requestAnimationFrame: ((callback: FrameRequestCallback) => number) | undefined;
6
6
 
7
7
  export const getElementRoot = (el: HTMLElement, fallback: HTMLElement = el) => {
8
8
  return el.shadowRoot || fallback;
@@ -58,15 +58,15 @@ export const changeSelectedElement = (
58
58
  };
59
59
 
60
60
  export class Config {
61
- private m = new Map<keyof IonicConfig, any>();
61
+ private m = new Map<keyof IonicConfig, unknown>();
62
62
 
63
63
  reset(configObj: IonicConfig) {
64
- this.m = new Map<keyof IonicConfig, any>(Object.entries(configObj) as any);
64
+ this.m = new Map<keyof IonicConfig, unknown>(Object.entries(configObj) as [keyof IonicConfig, unknown][]);
65
65
  }
66
66
 
67
- get(key: keyof IonicConfig, fallback?: any): any {
67
+ get<T>(key: keyof IonicConfig, fallback?: T): T {
68
68
  const value = this.m.get(key);
69
- return value !== undefined ? value : fallback;
69
+ return value !== undefined ? (value as T) : (fallback as T);
70
70
  }
71
71
 
72
72
  getBoolean(key: keyof IonicConfig, fallback = false): boolean {
@@ -81,11 +81,11 @@ export class Config {
81
81
  }
82
82
 
83
83
  getNumber(key: keyof IonicConfig, fallback?: number): number {
84
- const val = parseFloat(this.m.get(key));
84
+ const val = parseFloat(String(this.m.get(key)));
85
85
  return isNaN(val) ? (fallback !== undefined ? fallback : NaN) : val;
86
86
  }
87
87
 
88
- set(key: keyof IonicConfig, value: any) {
88
+ set(key: keyof IonicConfig, value: unknown) {
89
89
  this.m.set(key, value);
90
90
  }
91
91
  }
@@ -0,0 +1,3 @@
1
+ export { enableVerticalControlArea, setVerticalControlAreaPlacement, IonicNativeUIShell } from './native';
2
+ export { HingeStatus } from './native';
3
+ export type { DeviceLayout, NativeUIShellStatus, NativeUIShellSuspension, VerticalBarEdge, VerticalControlAreaHandle } from './native';
@@ -1,4 +0,0 @@
1
- import type { PluginListenerHandle } from '@capacitor/core';
2
- import type { NativeUIShellHandle } from './definitions';
3
- export declare const bindMetricsLifecycle: (runtime: NativeUIShellHandle, listen: () => Promise<PluginListenerHandle>, deactivate: () => void) => Promise<NativeUIShellHandle>;
4
- //# sourceMappingURL=lifecycle.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/native/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,eAAO,MAAM,oBAAoB,GAC/B,SAAS,mBAAmB,EAC5B,QAAQ,MAAM,OAAO,CAAC,oBAAoB,CAAC,EAC3C,YAAY,MAAM,IAAI,KACrB,OAAO,CAAC,mBAAmB,CA8B7B,CAAC"}
@@ -1,34 +0,0 @@
1
- export const bindMetricsLifecycle = async (runtime, listen, deactivate) => {
2
- let listener;
3
- try {
4
- listener = await listen();
5
- }
6
- catch (error) {
7
- await runtime.destroy();
8
- throw error;
9
- }
10
- let destroying;
11
- return {
12
- getStatus: runtime.getStatus,
13
- suspend: runtime.suspend,
14
- destroy() {
15
- return (destroying ?? (destroying = (async () => {
16
- let removalError;
17
- try {
18
- await listener.remove();
19
- }
20
- catch (error) {
21
- removalError = error;
22
- }
23
- try {
24
- await runtime.destroy();
25
- }
26
- finally {
27
- deactivate();
28
- }
29
- if (removalError)
30
- throw removalError;
31
- })()));
32
- },
33
- };
34
- };
@@ -1,38 +0,0 @@
1
- import type { PluginListenerHandle } from '@capacitor/core';
2
- import type { NativeUIShellHandle } from './definitions';
3
-
4
- export const bindMetricsLifecycle = async (
5
- runtime: NativeUIShellHandle,
6
- listen: () => Promise<PluginListenerHandle>,
7
- deactivate: () => void,
8
- ): Promise<NativeUIShellHandle> => {
9
- let listener: PluginListenerHandle;
10
- try {
11
- listener = await listen();
12
- } catch (error) {
13
- await runtime.destroy();
14
- throw error;
15
- }
16
-
17
- let destroying: Promise<void> | undefined;
18
- return {
19
- getStatus: runtime.getStatus,
20
- suspend: runtime.suspend,
21
- destroy() {
22
- return (destroying ??= (async () => {
23
- let removalError: unknown;
24
- try {
25
- await listener.remove();
26
- } catch (error) {
27
- removalError = error;
28
- }
29
- try {
30
- await runtime.destroy();
31
- } finally {
32
- deactivate();
33
- }
34
- if (removalError) throw removalError;
35
- })());
36
- },
37
- };
38
- };