@rdlabo/ionic-theme-ios27 0.1.0 → 0.2.1

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 (140) hide show
  1. package/README.md +10 -2
  2. package/dist/css/components/ion-button.css +1 -1
  3. package/dist/css/components/ion-fab.css +1 -1
  4. package/dist/css/components/ion-list.css +1 -1
  5. package/dist/css/components/ion-menu.css +1 -1
  6. package/dist/css/components/ion-searchbar.css +1 -1
  7. package/dist/css/components/ion-tabs.css +1 -1
  8. package/dist/css/components/ion-toolbar.css +1 -1
  9. package/dist/css/ionic-theme-ios27.css +1 -1
  10. package/dist/css/md-ion-list-inset.css +1 -1
  11. package/dist/css/utils/translucent.css +1 -1
  12. package/dist/index.d.ts +1 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -2
  15. package/dist/native/definitions.d.ts +19 -0
  16. package/dist/native/definitions.d.ts.map +1 -1
  17. package/dist/native/index.d.ts +4 -3
  18. package/dist/native/index.d.ts.map +1 -1
  19. package/dist/native/index.js +22 -9
  20. package/dist/native/lifecycle.d.ts +4 -0
  21. package/dist/native/lifecycle.d.ts.map +1 -0
  22. package/dist/native/lifecycle.js +34 -0
  23. package/dist/native/runtime.d.ts +2 -2
  24. package/dist/native/runtime.d.ts.map +1 -1
  25. package/dist/native/runtime.js +43 -4
  26. package/dist/tab-bar-searchable/animations/enter.d.ts +1 -1
  27. package/dist/tab-bar-searchable/animations/enter.d.ts.map +1 -1
  28. package/dist/tab-bar-searchable/animations/enter.js +1 -1
  29. package/dist/tab-bar-searchable/animations/leave.d.ts +1 -1
  30. package/dist/tab-bar-searchable/animations/leave.d.ts.map +1 -1
  31. package/dist/tab-bar-searchable/animations/leave.js +1 -1
  32. package/dist/tab-bar-searchable/index.d.ts +2 -2
  33. package/dist/tab-bar-searchable/index.d.ts.map +1 -1
  34. package/dist/tab-bar-searchable/index.js +2 -3
  35. package/dist/transition/ios.transition.d.ts +6 -1
  36. package/dist/transition/ios.transition.d.ts.map +1 -1
  37. package/dist/transition/ios.transition.js +10 -385
  38. package/docs/features.md +3 -1
  39. package/docs/native-ui-shell.md +113 -0
  40. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +28 -0
  41. package/node_modules/@rdlabo/ionic-theme-utils/LICENSE +21 -0
  42. package/node_modules/@rdlabo/ionic-theme-utils/README.md +28 -0
  43. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts +3 -0
  44. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts.map +1 -0
  45. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js +13 -0
  46. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js.map +1 -0
  47. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts +6 -0
  48. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts.map +1 -0
  49. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js +37 -0
  50. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js.map +1 -0
  51. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts +13 -0
  52. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts.map +1 -0
  53. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js +11 -0
  54. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js.map +1 -0
  55. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.enter.d.ts +0 -1
  56. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.d.ts.map +1 -0
  57. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.enter.js +8 -5
  58. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.js.map +1 -0
  59. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.leave.d.ts.map +1 -1
  60. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.leave.js +11 -5
  61. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.js.map +1 -0
  62. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/callout-surface.js +1 -0
  63. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.js.map +1 -0
  64. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js +2 -0
  65. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js.map +1 -0
  66. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/utils.d.ts +1 -0
  67. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.d.ts.map +1 -0
  68. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/utils.js +3 -2
  69. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.js.map +1 -0
  70. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts +16 -0
  71. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts.map +1 -0
  72. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js +2 -0
  73. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js.map +1 -0
  74. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/interfaces.js +1 -0
  75. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.js.map +1 -0
  76. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/utils.js +1 -0
  77. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.js.map +1 -0
  78. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts +15 -0
  79. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts.map +1 -0
  80. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js +49 -0
  81. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js.map +1 -0
  82. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts +18 -0
  83. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts.map +1 -0
  84. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js +412 -0
  85. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js.map +1 -0
  86. package/node_modules/@rdlabo/ionic-theme-utils/package.json +64 -0
  87. package/node_modules/@rdlabo/ionic-theme-utils/src/dom.spec.ts +19 -0
  88. package/node_modules/@rdlabo/ionic-theme-utils/src/dom.ts +16 -0
  89. package/node_modules/@rdlabo/ionic-theme-utils/src/focus-controller.ts +38 -0
  90. package/node_modules/@rdlabo/ionic-theme-utils/src/index.ts +59 -0
  91. package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/animations/ios.enter.ts +7 -5
  92. package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/animations/ios.leave.ts +14 -5
  93. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/utils.spec.ts +58 -0
  94. package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/utils.ts +4 -2
  95. package/node_modules/@rdlabo/ionic-theme-utils/src/sheets-of-glass.ts +17 -0
  96. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/index.scss +1 -0
  97. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/md-ion-list-inset.scss +20 -0
  98. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/structured-list.scss +310 -0
  99. package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/utils.spec.ts +56 -0
  100. package/node_modules/@rdlabo/ionic-theme-utils/src/theme.ts +51 -0
  101. package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.spec.ts +52 -0
  102. package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.ts +542 -0
  103. package/package.json +13 -5
  104. package/src/index.ts +4 -2
  105. package/src/native/definitions.ts +34 -0
  106. package/src/native/index.ts +38 -11
  107. package/src/native/lifecycle.ts +38 -0
  108. package/src/native/runtime.ts +49 -4
  109. package/src/styles/components/ion-button.scss +21 -10
  110. package/src/styles/components/ion-fab.scss +10 -4
  111. package/src/styles/components/ion-list.scss +12 -3
  112. package/src/styles/components/ion-menu.scss +17 -10
  113. package/src/styles/components/ion-searchbar.scss +1 -1
  114. package/src/styles/components/ion-tabs.scss +14 -10
  115. package/src/styles/components/ion-toolbar.scss +9 -10
  116. package/src/styles/md-ion-list-inset.scss +2 -2
  117. package/src/styles/utils/dark/ion-list.scss +1 -1
  118. package/src/styles/utils/translucent.scss +44 -0
  119. package/src/tab-bar-searchable/animations/enter.ts +1 -2
  120. package/src/tab-bar-searchable/animations/leave.ts +1 -2
  121. package/src/tab-bar-searchable/index.ts +12 -4
  122. package/src/transition/ios.transition.ts +14 -473
  123. package/dist/css/utils/structured-list.css +0 -1
  124. package/dist/popover/animations/ios.enter.d.ts.map +0 -1
  125. package/dist/popover/popover-interface.js +0 -1
  126. package/dist/popover/utils.d.ts.map +0 -1
  127. package/src/styles/utils/structured-list.scss +0 -223
  128. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.leave.d.ts +0 -0
  129. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/callout-surface.d.ts +0 -0
  130. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/callout-surface.d.ts.map +0 -0
  131. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/popover-interface.d.ts +0 -0
  132. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/popover-interface.d.ts.map +0 -0
  133. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/interfaces.d.ts +0 -0
  134. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/interfaces.d.ts.map +0 -0
  135. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/utils.d.ts +0 -0
  136. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/utils.d.ts.map +0 -0
  137. /package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/callout-surface.ts +0 -0
  138. /package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/popover-interface.ts +0 -0
  139. /package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/tab-bar-searchable/interfaces.ts +0 -0
  140. /package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/tab-bar-searchable/utils.ts +0 -0
@@ -0,0 +1,542 @@
1
+ import type { Animation } from '@ionic/core';
2
+ import { createAnimation } from '@ionic/core';
3
+
4
+ /** Options consumed by the shared iOS page transition animation. */
5
+ export interface IosTransitionAnimationOptions {
6
+ enteringEl: HTMLElement;
7
+ leavingEl?: HTMLElement;
8
+ direction?: string;
9
+ duration?: number;
10
+ easing?: string;
11
+ progressCallback?: (ani: Animation | undefined) => void;
12
+ }
13
+
14
+ /** Theme-specific knobs for {@link createIosTransitionAnimation}. */
15
+ export interface IosTransitionAnimationConfig {
16
+ /**
17
+ * Previous-page retreat as a positive percent of the viewport width.
18
+ * ios26 uses 30; ios27 uses 33.
19
+ */
20
+ offLeftPercent: number;
21
+ /** Radius applied to the foreground page while transitioning. */
22
+ radius?: number;
23
+ /** Resolves the ion-page (or fallback) element for a leaving view. */
24
+ getIonPageElement: (element: HTMLElement) => Element;
25
+ /**
26
+ * Optional hook invoked after the root animation is fully composed
27
+ * (e.g. ios27 `connectNativeUIShellTransition`).
28
+ */
29
+ connectNativeUIShellTransition?: (animation: Animation, entering: HTMLElement, leaving?: HTMLElement) => void;
30
+ }
31
+
32
+ const DURATION = 540;
33
+
34
+ const getClonedElement = <T extends HTMLIonBackButtonElement | HTMLIonTitleElement>(tagName: string) => {
35
+ return document.querySelector<T>(`${tagName}.ion-cloned-element`);
36
+ };
37
+
38
+ export const shadow = <T extends Element>(el: T): ShadowRoot | T => {
39
+ return el.shadowRoot || el;
40
+ };
41
+
42
+ const animateFixedBackButton = (
43
+ root: Animation,
44
+ navEl: HTMLElement,
45
+ page: HTMLElement,
46
+ entering: boolean,
47
+ interactive: boolean,
48
+ otherPage?: HTMLElement,
49
+ ) => {
50
+ const button = page.querySelector<HTMLIonBackButtonElement>(
51
+ ':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled) ion-back-button:not(.ios-theme-disabled, .ios26-disabled)',
52
+ );
53
+ if (!button || button.offsetWidth === 0) {
54
+ return;
55
+ }
56
+
57
+ const otherButton = otherPage?.querySelector<HTMLIonBackButtonElement>(
58
+ ':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled) ion-back-button:not(.ios-theme-disabled, .ios26-disabled)',
59
+ );
60
+ const persistent = !!otherButton && otherButton.offsetWidth > 0;
61
+ const buttons = [button, ...(persistent ? [otherButton!] : [])].map((element) => ({
62
+ element,
63
+ visibility: element.style.visibility,
64
+ }));
65
+ const rect = button.getBoundingClientRect();
66
+ const width = button.offsetWidth;
67
+ const height = button.offsetHeight;
68
+ const clone = getClonedElement<HTMLIonBackButtonElement>('ion-back-button');
69
+ if (!clone) {
70
+ return;
71
+ }
72
+ const cloneParent = clone.parentNode!;
73
+ const cloneNextSibling = clone.nextSibling;
74
+ const cloneStyle = clone.getAttribute('style');
75
+ clone.icon = button.icon;
76
+ clone.text = button.text;
77
+ clone.mode = button.mode;
78
+ clone.color = button.color;
79
+ clone.disabled = button.disabled;
80
+ const icon = shadow(clone).querySelector('ion-icon');
81
+ const animation = createAnimation().addElement(clone);
82
+ const fadeStart = !entering && interactive ? 0.8 : 0.4;
83
+ const fadeEnd = entering ? 0.96 : interactive ? 1 : 0.95;
84
+ if (persistent) {
85
+ animation.fromTo('transform', 'scale(1)', 'scale(1)').fromTo('opacity', 1, 1);
86
+ } else if (entering) {
87
+ animation.keyframes([
88
+ { offset: 0, transform: 'scale(1.2)', opacity: 0 },
89
+ { offset: fadeStart, transform: 'scale(1.2)', opacity: 0 },
90
+ { offset: 0.65, transform: 'scale(1.12)', opacity: 0.15 },
91
+ { offset: 0.9, transform: 'scale(1.02)', opacity: 0.75 },
92
+ { offset: fadeEnd, transform: 'scale(1)', opacity: 1 },
93
+ { offset: 1, transform: 'scale(1)', opacity: 1 },
94
+ ]);
95
+ } else {
96
+ animation.keyframes([
97
+ { offset: 0, transform: 'scale(1)', opacity: 1 },
98
+ { offset: fadeStart, transform: 'scale(1)', opacity: 1 },
99
+ { offset: fadeEnd, transform: 'scale(1.2)', opacity: 0 },
100
+ { offset: 1, transform: 'scale(1.2)', opacity: 0 },
101
+ ]);
102
+ }
103
+ if (icon && !persistent) {
104
+ const from = entering ? 'blur(4px)' : 'blur(0px)';
105
+ const to = entering ? 'blur(0px)' : 'blur(4px)';
106
+ animation.addAnimation(
107
+ createAnimation()
108
+ .addElement(icon)
109
+ .keyframes([
110
+ { offset: 0, filter: from },
111
+ { offset: fadeStart, filter: from },
112
+ { offset: fadeEnd, filter: to },
113
+ { offset: 1, filter: to },
114
+ ]),
115
+ );
116
+ }
117
+ root.beforeAddWrite(() => {
118
+ Object.assign(clone.style, {
119
+ position: 'fixed',
120
+ left: `${rect.left + (rect.width - width) / 2}px`,
121
+ top: `${rect.top + (rect.height - height) / 2}px`,
122
+ width: `${width}px`,
123
+ height: `${height}px`,
124
+ margin: '0',
125
+ pointerEvents: 'none',
126
+ visibility: 'visible',
127
+ display: getComputedStyle(button).display,
128
+ zIndex: '1000',
129
+ });
130
+ navEl.appendChild(clone);
131
+ buttons.forEach(({ element }) => (element.style.visibility = 'hidden'));
132
+ });
133
+ root.afterAddWrite(() => {
134
+ buttons.forEach(({ element, visibility }) => (element.style.visibility = visibility));
135
+ cloneParent.insertBefore(clone, cloneNextSibling);
136
+ if (cloneStyle === null) {
137
+ clone.removeAttribute('style');
138
+ } else {
139
+ clone.setAttribute('style', cloneStyle);
140
+ }
141
+ });
142
+ root.addAnimation(animation);
143
+ };
144
+
145
+ /**
146
+ * Builds an `iosTransitionAnimation` function parameterized by theme config
147
+ * (OFF_LEFT percent and an optional native-shell hook).
148
+ */
149
+ export const createIosTransitionAnimation = <TOpts extends IosTransitionAnimationOptions>(
150
+ config: IosTransitionAnimationConfig,
151
+ ): ((navEl: HTMLElement, opts: TOpts) => Animation) => {
152
+ const { offLeftPercent, getIonPageElement, connectNativeUIShellTransition } = config;
153
+
154
+ return (navEl: HTMLElement, opts: TOpts): Animation => {
155
+ try {
156
+ const EASING = 'cubic-bezier(0.32,0.72,0,1)';
157
+ const OPACITY = 'opacity';
158
+ const TRANSFORM = 'transform';
159
+ const CENTER = '0%';
160
+ const OFF_OPACITY = 0.8;
161
+
162
+ const isRTL = navEl.ownerDocument.dir === 'rtl';
163
+ const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%';
164
+ const OFF_LEFT = isRTL ? `${offLeftPercent}%` : `-${offLeftPercent}%`;
165
+
166
+ const enteringEl = opts.enteringEl;
167
+ const leavingEl = opts.leavingEl;
168
+
169
+ const backDirection = opts.direction === 'back';
170
+ const contentEl = enteringEl.querySelector(':scope > ion-content');
171
+ const headerEls = enteringEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');
172
+ const enteringToolBarEls = enteringEl.querySelectorAll(':scope > ion-header > ion-toolbar');
173
+
174
+ const rootAnimation = createAnimation();
175
+ const enteringContentAnimation = createAnimation();
176
+
177
+ rootAnimation
178
+ .addElement(enteringEl)
179
+ .duration((opts.duration ?? 0) || DURATION)
180
+ .easing(opts.easing || EASING)
181
+ .fill('both')
182
+ .beforeRemoveClass('ion-page-invisible');
183
+
184
+ const topPage = backDirection ? leavingEl : enteringEl;
185
+ const translucentTop = topPage?.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)');
186
+ if (topPage && translucentTop) {
187
+ // UIKit dims the opaque page underneath, including its header, with 10% black.
188
+ const shade = navEl.ownerDocument.createElement('div');
189
+ shade.className = 'ios-transition-shade';
190
+ shade.setAttribute('aria-hidden', 'true');
191
+ const clipPath = topPage.style.clipPath;
192
+ const corner = config.radius ?? 0;
193
+ Object.assign(shade.style, {
194
+ position: 'absolute',
195
+ inset: '0',
196
+ zIndex: topPage.style.zIndex,
197
+ pointerEvents: 'none',
198
+ background: 'rgba(0, 0, 0, 0.1)',
199
+ });
200
+ rootAnimation.beforeAddWrite(() => {
201
+ topPage.before(shade);
202
+ if (corner > 0) {
203
+ topPage.style.clipPath = `inset(0 round ${corner}px)`;
204
+ }
205
+ });
206
+ rootAnimation.afterAddWrite(() => {
207
+ shade.remove();
208
+ topPage.style.clipPath = clipPath;
209
+ });
210
+ rootAnimation.addAnimation(
211
+ createAnimation()
212
+ .addElement(shade)
213
+ .fromTo(OPACITY, backDirection ? 1 : 0, backDirection ? 0 : 1),
214
+ );
215
+ }
216
+
217
+ if (leavingEl && navEl !== null && navEl !== undefined) {
218
+ const navDecorAnimation = createAnimation();
219
+ navDecorAnimation.addElement(navEl);
220
+ rootAnimation.addAnimation(navDecorAnimation);
221
+ }
222
+
223
+ if (enteringEl.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
224
+ enteringContentAnimation.addElement(enteringEl);
225
+ } else if (!contentEl && enteringToolBarEls.length === 0 && headerEls.length === 0) {
226
+ enteringContentAnimation.addElement(enteringEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs')!); // REVIEW
227
+ } else {
228
+ enteringContentAnimation.addElement(contentEl!); // REVIEW
229
+ enteringContentAnimation.addElement(headerEls);
230
+ }
231
+
232
+ rootAnimation.addAnimation(enteringContentAnimation);
233
+
234
+ if (backDirection) {
235
+ enteringContentAnimation
236
+ .beforeClearStyles([OPACITY])
237
+ .fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`)
238
+ .fromTo(OPACITY, translucentTop ? 1 : OFF_OPACITY, 1);
239
+ } else {
240
+ // entering content, forward direction
241
+ enteringContentAnimation.beforeClearStyles([OPACITY]).fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
242
+ }
243
+
244
+ if (contentEl && !enteringEl.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
245
+ const enteringTransitionEffectEl = shadow(contentEl).querySelector('.transition-effect');
246
+ if (enteringTransitionEffectEl) {
247
+ const enteringTransitionCoverEl = enteringTransitionEffectEl.querySelector('.transition-cover');
248
+ const enteringTransitionShadowEl = enteringTransitionEffectEl.querySelector('.transition-shadow');
249
+
250
+ const enteringTransitionEffect = createAnimation();
251
+ const enteringTransitionCover = createAnimation();
252
+ const enteringTransitionShadow = createAnimation();
253
+
254
+ enteringTransitionEffect
255
+ .addElement(enteringTransitionEffectEl)
256
+ .beforeStyles({ opacity: '1', display: 'block' })
257
+ .afterStyles({ opacity: '', display: '' });
258
+
259
+ enteringTransitionCover
260
+ .addElement(enteringTransitionCoverEl!) // REVIEW
261
+ .beforeClearStyles([OPACITY])
262
+ .fromTo(OPACITY, 0, 0.1);
263
+
264
+ enteringTransitionShadow
265
+ .addElement(enteringTransitionShadowEl!) // REVIEW
266
+ .beforeClearStyles([OPACITY])
267
+ .fromTo(OPACITY, 0.03, 0.7);
268
+
269
+ enteringTransitionEffect.addAnimation([enteringTransitionCover, enteringTransitionShadow]);
270
+ enteringContentAnimation.addAnimation([enteringTransitionEffect]);
271
+ }
272
+ }
273
+
274
+ if (topPage) {
275
+ animateFixedBackButton(
276
+ rootAnimation,
277
+ navEl,
278
+ topPage,
279
+ !backDirection,
280
+ opts.progressCallback !== undefined,
281
+ backDirection ? enteringEl : leavingEl,
282
+ );
283
+ }
284
+
285
+ const enteringContentHasLargeTitle = enteringEl.querySelector('ion-header.header-collapse-condense');
286
+
287
+ enteringToolBarEls.forEach((enteringToolBarEl) => {
288
+ if (enteringToolBarEl.closest('ion-header')?.matches('.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
289
+ return;
290
+ }
291
+ const enteringToolBar = createAnimation();
292
+ enteringToolBar.addElement(enteringToolBarEl);
293
+ rootAnimation.addAnimation(enteringToolBar);
294
+
295
+ const enteringTitle = createAnimation();
296
+ enteringTitle.addElement(enteringToolBarEl.querySelector('ion-title')!); // REVIEW
297
+
298
+ const enteringToolBarButtons = createAnimation();
299
+ const buttons = Array.from(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]'));
300
+
301
+ const parentHeader = enteringToolBarEl.closest('ion-header');
302
+ const inactiveHeader = parentHeader?.classList.contains('header-collapse-condense-inactive');
303
+
304
+ let buttonsToAnimate;
305
+ if (backDirection) {
306
+ buttonsToAnimate = buttons.filter((button) => {
307
+ const isCollapseButton = button.classList.contains('buttons-collapse');
308
+ return (isCollapseButton && !inactiveHeader) || !isCollapseButton;
309
+ });
310
+ } else {
311
+ buttonsToAnimate = buttons.filter((button) => !button.classList.contains('buttons-collapse'));
312
+ }
313
+
314
+ enteringToolBarButtons.addElement(buttonsToAnimate);
315
+
316
+ const enteringToolBarItems = createAnimation();
317
+ enteringToolBarItems.addElement(enteringToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])'));
318
+
319
+ const enteringToolBarBg = createAnimation();
320
+ enteringToolBarBg.addElement(shadow(enteringToolBarEl).querySelector('.toolbar-background')!); // REVIEW
321
+
322
+ const enteringBackButton = createAnimation();
323
+ const backButtonEl = enteringToolBarEl.querySelector('ion-back-button');
324
+
325
+ if (backButtonEl) {
326
+ enteringBackButton.addElement(backButtonEl);
327
+ }
328
+
329
+ enteringToolBar.addAnimation([enteringTitle, enteringToolBarButtons, enteringToolBarItems, enteringToolBarBg, enteringBackButton]);
330
+ enteringToolBarButtons.fromTo(OPACITY, 0.01, 1);
331
+ enteringToolBarItems.fromTo(OPACITY, 0.01, 1);
332
+
333
+ if (backDirection) {
334
+ if (!inactiveHeader) {
335
+ enteringTitle.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`).fromTo(OPACITY, 0.01, 1);
336
+ }
337
+
338
+ enteringToolBarItems.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`);
339
+
340
+ // back direction, entering page has a back button
341
+ enteringBackButton.fromTo(OPACITY, 0.01, 1);
342
+ } else {
343
+ // entering toolbar, forward direction
344
+ if (!enteringContentHasLargeTitle) {
345
+ enteringTitle.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`).fromTo(OPACITY, 0.01, 1);
346
+ }
347
+
348
+ enteringToolBarItems.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
349
+ enteringToolBarBg.beforeClearStyles([OPACITY, 'transform']);
350
+
351
+ const translucentHeader = parentHeader?.translucent;
352
+ if (!translucentHeader) {
353
+ enteringToolBarBg.fromTo(OPACITY, 0.01, 'var(--opacity)');
354
+ } else {
355
+ enteringToolBarBg.fromTo('transform', isRTL ? 'translateX(-100%)' : 'translateX(100%)', 'translateX(0px)');
356
+ }
357
+
358
+ // forward direction, entering page has a back button
359
+ enteringBackButton.fromTo(OPACITY, 0.01, 1);
360
+
361
+ if (backButtonEl) {
362
+ const enteringBackBtnText = createAnimation();
363
+ enteringBackBtnText
364
+ .addElement(shadow(backButtonEl).querySelector('.button-text')!) // REVIEW
365
+ .fromTo(`transform`, isRTL ? 'translateX(-100px)' : 'translateX(100px)', 'translateX(0px)');
366
+
367
+ enteringToolBar.addAnimation(enteringBackBtnText);
368
+ }
369
+ }
370
+ });
371
+
372
+ // setup leaving view
373
+ if (leavingEl) {
374
+ const leavingContent = createAnimation();
375
+ const leavingContentEl = leavingEl.querySelector(':scope > ion-content');
376
+ const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar');
377
+ const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');
378
+
379
+ if (leavingEl.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
380
+ leavingContent.addElement(leavingEl);
381
+ } else if (!leavingContentEl && leavingToolBarEls.length === 0 && leavingHeaderEls.length === 0) {
382
+ leavingContent.addElement(leavingEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs')!); // REVIEW
383
+ } else {
384
+ leavingContent.addElement(leavingContentEl!); // REVIEW
385
+ leavingContent.addElement(leavingHeaderEls);
386
+ }
387
+
388
+ rootAnimation.addAnimation(leavingContent);
389
+
390
+ if (backDirection) {
391
+ // leaving content, back direction
392
+ leavingContent
393
+ .beforeClearStyles([OPACITY])
394
+ .fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)');
395
+
396
+ const leavingPage = getIonPageElement(leavingEl) as HTMLElement;
397
+ rootAnimation.afterAddWrite(() => {
398
+ if (rootAnimation.getDirection() === 'normal') {
399
+ leavingPage.style.setProperty('display', 'none');
400
+ }
401
+ });
402
+ } else {
403
+ // leaving content, forward direction
404
+ leavingContent
405
+ .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
406
+ .fromTo(OPACITY, 1, translucentTop ? 1 : OFF_OPACITY);
407
+ }
408
+
409
+ if (
410
+ leavingContentEl &&
411
+ !leavingEl.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')
412
+ ) {
413
+ const leavingTransitionEffectEl = shadow(leavingContentEl).querySelector('.transition-effect');
414
+
415
+ if (leavingTransitionEffectEl) {
416
+ const leavingTransitionCoverEl = leavingTransitionEffectEl.querySelector('.transition-cover');
417
+ const leavingTransitionShadowEl = leavingTransitionEffectEl.querySelector('.transition-shadow');
418
+
419
+ const leavingTransitionEffect = createAnimation();
420
+ const leavingTransitionCover = createAnimation();
421
+ const leavingTransitionShadow = createAnimation();
422
+
423
+ leavingTransitionEffect
424
+ .addElement(leavingTransitionEffectEl)
425
+ .beforeStyles({ opacity: '1', display: 'block' })
426
+ .afterStyles({ opacity: '', display: '' });
427
+
428
+ leavingTransitionCover
429
+ .addElement(leavingTransitionCoverEl!) // REVIEW
430
+ .beforeClearStyles([OPACITY])
431
+ .fromTo(OPACITY, 0.1, 0);
432
+
433
+ leavingTransitionShadow
434
+ .addElement(leavingTransitionShadowEl!) // REVIEW
435
+ .beforeClearStyles([OPACITY])
436
+ .fromTo(OPACITY, 0.7, 0.03);
437
+
438
+ leavingTransitionEffect.addAnimation([leavingTransitionCover, leavingTransitionShadow]);
439
+ leavingContent.addAnimation([leavingTransitionEffect]);
440
+ }
441
+ }
442
+
443
+ leavingToolBarEls.forEach((leavingToolBarEl) => {
444
+ if (leavingToolBarEl.closest('ion-header')?.matches('.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
445
+ return;
446
+ }
447
+ const leavingToolBar = createAnimation();
448
+ leavingToolBar.addElement(leavingToolBarEl);
449
+
450
+ const leavingTitle = createAnimation();
451
+ leavingTitle.addElement(leavingToolBarEl.querySelector('ion-title')!); // REVIEW
452
+
453
+ const leavingToolBarButtons = createAnimation();
454
+ const buttons = leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]');
455
+
456
+ const parentHeader = leavingToolBarEl.closest('ion-header');
457
+ const inactiveHeader = parentHeader?.classList.contains('header-collapse-condense-inactive');
458
+
459
+ const buttonsToAnimate = Array.from(buttons).filter((button) => {
460
+ const isCollapseButton = button.classList.contains('buttons-collapse');
461
+ return (isCollapseButton && !inactiveHeader) || !isCollapseButton;
462
+ });
463
+
464
+ leavingToolBarButtons.addElement(buttonsToAnimate);
465
+
466
+ const leavingToolBarItems = createAnimation();
467
+ const leavingToolBarItemEls = leavingToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])');
468
+ if (leavingToolBarItemEls.length > 0) {
469
+ leavingToolBarItems.addElement(leavingToolBarItemEls);
470
+ }
471
+
472
+ const leavingToolBarBg = createAnimation();
473
+ leavingToolBarBg.addElement(shadow(leavingToolBarEl).querySelector('.toolbar-background')!); // REVIEW
474
+
475
+ const leavingBackButton = createAnimation();
476
+ const backButtonEl = leavingToolBarEl.querySelector('ion-back-button');
477
+ if (backButtonEl) {
478
+ leavingBackButton.addElement(backButtonEl);
479
+ }
480
+
481
+ leavingToolBar.addAnimation([leavingTitle, leavingToolBarButtons, leavingToolBarItems, leavingBackButton, leavingToolBarBg]);
482
+ rootAnimation.addAnimation(leavingToolBar);
483
+
484
+ // fade out leaving toolbar items
485
+ leavingBackButton.fromTo(OPACITY, 0.99, 0);
486
+
487
+ leavingToolBarButtons.fromTo(OPACITY, 0.99, 0);
488
+ leavingToolBarItems.fromTo(OPACITY, 0.99, 0);
489
+
490
+ if (backDirection) {
491
+ if (!inactiveHeader) {
492
+ // leaving toolbar, back direction
493
+ leavingTitle
494
+ .fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)')
495
+ .fromTo(OPACITY, 0.99, 0);
496
+ }
497
+
498
+ leavingToolBarItems.fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)');
499
+ leavingToolBarBg.beforeClearStyles([OPACITY, 'transform']);
500
+ // leaving toolbar, back direction, and there's no entering toolbar
501
+ // should just slide out, no fading out
502
+ const translucentHeader = parentHeader?.translucent;
503
+ if (!translucentHeader) {
504
+ leavingToolBarBg.fromTo(OPACITY, 'var(--opacity)', 0);
505
+ } else {
506
+ leavingToolBarBg.fromTo('transform', 'translateX(0px)', isRTL ? 'translateX(-100%)' : 'translateX(100%)');
507
+ }
508
+
509
+ if (backButtonEl) {
510
+ const leavingBackBtnText = createAnimation();
511
+ leavingBackBtnText
512
+ .addElement(shadow(backButtonEl).querySelector('.button-text')!) // REVIEW
513
+ .fromTo('transform', `translateX(${CENTER})`, `translateX(${(isRTL ? -124 : 124) + 'px'})`);
514
+ leavingToolBar.addAnimation(leavingBackBtnText);
515
+ }
516
+ } else {
517
+ // leaving toolbar, forward direction
518
+ if (!inactiveHeader) {
519
+ leavingTitle
520
+ .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
521
+ .fromTo(OPACITY, 0.99, 0)
522
+ .afterClearStyles([TRANSFORM, OPACITY]);
523
+ }
524
+
525
+ leavingToolBarItems
526
+ .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
527
+ .afterClearStyles([TRANSFORM, OPACITY]);
528
+
529
+ leavingBackButton.afterClearStyles([OPACITY]);
530
+ leavingTitle.afterClearStyles([OPACITY]);
531
+ leavingToolBarButtons.afterClearStyles([OPACITY]);
532
+ }
533
+ });
534
+ }
535
+
536
+ connectNativeUIShellTransition?.(rootAnimation, enteringEl, leavingEl);
537
+ return rootAnimation;
538
+ } catch (err) {
539
+ throw err;
540
+ }
541
+ };
542
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rdlabo/ionic-theme-ios27",
3
3
  "private": false,
4
- "version": "0.1.0",
4
+ "version": "0.2.1",
5
5
  "description": "iOS27 Theme for Ionic Framework",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -27,9 +27,10 @@
27
27
  "Package.swift"
28
28
  ],
29
29
  "scripts": {
30
- "build": "npm run build:css && npm run build:ts",
31
- "build:css": "rm -rf dist/css && sass src/styles:dist/css --style=compressed --no-source-map",
30
+ "build": "npm run docgen && npm run build:css && npm run build:ts",
31
+ "build:css": "rm -rf dist/css && sass src/styles:dist/css --style=compressed --no-source-map --pkg-importer=node",
32
32
  "build:ts": "tsc",
33
+ "docgen": "docgen --project tsconfig.docgen.json --api NativeUIShellHandle --output-readme docs/native-ui-shell.md",
33
34
  "build:demo": "npm run build && cd demo && npm install && npm run build -- --configuration=production",
34
35
  "lint": "prettier --check \"./**/*.{scss,ts}\" && prettier --parser angular --check \"./**/*.html\"",
35
36
  "fmt": "prettier --write \"./**/*.{scss,ts}\" && prettier --parser angular --write \"./**/*.html\"",
@@ -53,9 +54,16 @@
53
54
  "url": "https://github.com/rdlabo-dev/ionic-theme-ios27/issues"
54
55
  },
55
56
  "homepage": "https://docs.rdlabo.dev/projects/ionic-theme-ios27",
57
+ "dependencies": {
58
+ "@rdlabo/ionic-theme-utils": "git+https://github.com/rdlabo-dev/ionic-theme-utils.git#66eb2fccca10646ebfccdfb3b6c2f3b4c9b00bf6"
59
+ },
60
+ "bundleDependencies": [
61
+ "@rdlabo/ionic-theme-utils"
62
+ ],
56
63
  "devDependencies": {
57
64
  "@capacitor/core": "^8.5.2",
58
65
  "@ionic/angular": "^9.0.0",
66
+ "@rdlabo/capacitor-docgen": "^0.4.1",
59
67
  "husky": "^9.1.7",
60
68
  "lint-staged": "^16.2.1",
61
69
  "np": "^10.2.0",
@@ -65,8 +73,8 @@
65
73
  "wrangler": "^4.125.0"
66
74
  },
67
75
  "peerDependencies": {
68
- "@ionic/core": ">=8.8.1 <10",
69
- "@capacitor/core": ">=8.0.0 <9"
76
+ "@capacitor/core": ">=8.0.0 <9",
77
+ "@ionic/core": ">=8.8.1 <10"
70
78
  },
71
79
  "peerDependenciesMeta": {
72
80
  "@capacitor/core": {
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { registeredEffect } from './sheets-of-glass/interfaces';
2
2
  import { registerEffect } from './sheets-of-glass';
3
3
  export * from './sheets-of-glass/interfaces';
4
- export { iosEnterAnimation as popoverEnterAnimation } from './popover/animations/ios.enter';
5
- export { iosLeaveAnimation as popoverLeaveAnimation } from './popover/animations/ios.leave';
4
+ export {
5
+ iosPopoverEnterAnimation as popoverEnterAnimation,
6
+ iosPopoverLeaveAnimation as popoverLeaveAnimation,
7
+ } from '@rdlabo/ionic-theme-utils';
6
8
  export * from './tab-bar-searchable';
7
9
  export * from './searchbar';
8
10
  export * from './transition/ios.transition';
@@ -10,11 +10,38 @@ export interface NativeUIShellStatus {
10
10
  reason?: string;
11
11
  }
12
12
 
13
+ export interface NativeUIShellOptions {
14
+ /** Enables Native UI Shell globally. Defaults to `true`. */
15
+ enabled?: boolean;
16
+ /** Controls eligible for native projection. Omit to enable every control; when present, only `true` controls are enabled. */
17
+ controls?: NativeUIShellControls;
18
+ }
19
+
20
+ export interface NativeUIShellControls {
21
+ /** Projects tab bars and their native search presentation. */
22
+ tabs?: boolean;
23
+ /** Projects toolbar buttons, including back and menu buttons. */
24
+ toolbar?: boolean;
25
+ /** Projects segments. */
26
+ segment?: boolean;
27
+ /** Projects floating action buttons. */
28
+ fab?: boolean;
29
+ }
30
+
13
31
  export interface NativeUIShellHandle {
32
+ /** Returns the current Web/native projection state. */
14
33
  getStatus(): NativeUIShellStatus;
34
+ /** Restores projected controls to the Web until the returned lease is resumed. */
35
+ suspend(): Promise<NativeUIShellSuspension>;
36
+ /** Stops synchronization, restores Web controls and releases native resources. */
15
37
  destroy(): Promise<void>;
16
38
  }
17
39
 
40
+ export interface NativeUIShellSuspension {
41
+ /** Releases this suspension. Native projection resumes after all active suspensions are released. */
42
+ resume(): Promise<void>;
43
+ }
44
+
18
45
  export interface Frame {
19
46
  x: number;
20
47
  y: number;
@@ -98,10 +125,17 @@ export interface ShellActivation {
98
125
  sequence: number;
99
126
  }
100
127
 
128
+ export interface WebViewMetrics {
129
+ /** The WebView's effective top-left corner radius in points, or `0` when unavailable. */
130
+ radius: number;
131
+ }
132
+
101
133
  export interface NativeUIShellPlugin {
102
134
  configure(): Promise<{ supported: boolean }>;
135
+ getWebViewMetrics(): Promise<WebViewMetrics>;
103
136
  update(snapshot: ShellSnapshot): Promise<{ revision: number; rejectedSearches?: string[]; rejectedControls?: string[] }>;
104
137
  clear(options: { revision: number }): Promise<void>;
105
138
  addListener(name: 'activate', listener: (event: ShellActivation) => void): Promise<PluginListenerHandle>;
106
139
  addListener(name: 'search', listener: (event: ShellSearchEvent) => void): Promise<PluginListenerHandle>;
140
+ addListener(name: 'webViewMetricsChange', listener: (event: WebViewMetrics) => void): Promise<PluginListenerHandle>;
107
141
  }