@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
@@ -1,389 +1,14 @@
1
- import { createAnimation } from '@ionic/core';
1
+ import { createIosTransitionAnimation, shadow } from '@rdlabo/ionic-theme-utils';
2
2
  import { getIonPageElement } from './index';
3
3
  import { connectNativeUIShellTransition } from '../native-integration';
4
- const DURATION = 540;
5
- const getClonedElement = (tagName) => {
6
- return document.querySelector(`${tagName}.ion-cloned-element`);
4
+ export { shadow };
5
+ const transitionConfig = {
6
+ offLeftPercent: 30,
7
+ getIonPageElement,
8
+ connectNativeUIShellTransition,
9
+ radius: 0,
7
10
  };
8
- export const shadow = (el) => {
9
- return el.shadowRoot || el;
10
- };
11
- const animateFixedBackButton = (root, navEl, page, entering, interactive, otherPage) => {
12
- const button = page.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled) ion-back-button:not(.ios-theme-disabled, .ios26-disabled)');
13
- if (!button || button.offsetWidth === 0) {
14
- return;
15
- }
16
- const otherButton = otherPage?.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled) ion-back-button:not(.ios-theme-disabled, .ios26-disabled)');
17
- const persistent = !!otherButton && otherButton.offsetWidth > 0;
18
- const buttons = [button, ...(persistent ? [otherButton] : [])].map((element) => ({
19
- element,
20
- visibility: element.style.visibility,
21
- }));
22
- const rect = button.getBoundingClientRect();
23
- const width = button.offsetWidth;
24
- const height = button.offsetHeight;
25
- const clone = getClonedElement('ion-back-button');
26
- if (!clone) {
27
- return;
28
- }
29
- const cloneParent = clone.parentNode;
30
- const cloneNextSibling = clone.nextSibling;
31
- const cloneStyle = clone.getAttribute('style');
32
- clone.icon = button.icon;
33
- clone.text = button.text;
34
- clone.mode = button.mode;
35
- clone.color = button.color;
36
- clone.disabled = button.disabled;
37
- const icon = shadow(clone).querySelector('ion-icon');
38
- const animation = createAnimation().addElement(clone);
39
- const fadeStart = !entering && interactive ? 0.8 : 0.4;
40
- const fadeEnd = entering ? 0.96 : interactive ? 1 : 0.95;
41
- if (persistent) {
42
- animation.fromTo('transform', 'scale(1)', 'scale(1)').fromTo('opacity', 1, 1);
43
- }
44
- else if (entering) {
45
- animation.keyframes([
46
- { offset: 0, transform: 'scale(1.2)', opacity: 0 },
47
- { offset: fadeStart, transform: 'scale(1.2)', opacity: 0 },
48
- { offset: 0.65, transform: 'scale(1.12)', opacity: 0.15 },
49
- { offset: 0.9, transform: 'scale(1.02)', opacity: 0.75 },
50
- { offset: fadeEnd, transform: 'scale(1)', opacity: 1 },
51
- { offset: 1, transform: 'scale(1)', opacity: 1 },
52
- ]);
53
- }
54
- else {
55
- animation.keyframes([
56
- { offset: 0, transform: 'scale(1)', opacity: 1 },
57
- { offset: fadeStart, transform: 'scale(1)', opacity: 1 },
58
- { offset: fadeEnd, transform: 'scale(1.2)', opacity: 0 },
59
- { offset: 1, transform: 'scale(1.2)', opacity: 0 },
60
- ]);
61
- }
62
- if (icon && !persistent) {
63
- const from = entering ? 'blur(4px)' : 'blur(0px)';
64
- const to = entering ? 'blur(0px)' : 'blur(4px)';
65
- animation.addAnimation(createAnimation()
66
- .addElement(icon)
67
- .keyframes([
68
- { offset: 0, filter: from },
69
- { offset: fadeStart, filter: from },
70
- { offset: fadeEnd, filter: to },
71
- { offset: 1, filter: to },
72
- ]));
73
- }
74
- root.beforeAddWrite(() => {
75
- Object.assign(clone.style, {
76
- position: 'fixed',
77
- left: `${rect.left + (rect.width - width) / 2}px`,
78
- top: `${rect.top + (rect.height - height) / 2}px`,
79
- width: `${width}px`,
80
- height: `${height}px`,
81
- margin: '0',
82
- pointerEvents: 'none',
83
- visibility: 'visible',
84
- display: getComputedStyle(button).display,
85
- zIndex: '1000',
86
- });
87
- navEl.appendChild(clone);
88
- buttons.forEach(({ element }) => (element.style.visibility = 'hidden'));
89
- });
90
- root.afterAddWrite(() => {
91
- buttons.forEach(({ element, visibility }) => (element.style.visibility = visibility));
92
- cloneParent.insertBefore(clone, cloneNextSibling);
93
- if (cloneStyle === null) {
94
- clone.removeAttribute('style');
95
- }
96
- else {
97
- clone.setAttribute('style', cloneStyle);
98
- }
99
- });
100
- root.addAnimation(animation);
101
- };
102
- export const iosTransitionAnimation = (navEl, opts) => {
103
- try {
104
- const EASING = 'cubic-bezier(0.32,0.72,0,1)';
105
- const OPACITY = 'opacity';
106
- const TRANSFORM = 'transform';
107
- const CENTER = '0%';
108
- const OFF_OPACITY = 0.8;
109
- const isRTL = navEl.ownerDocument.dir === 'rtl';
110
- const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%';
111
- const OFF_LEFT = isRTL ? '33%' : '-33%';
112
- const enteringEl = opts.enteringEl;
113
- const leavingEl = opts.leavingEl;
114
- const backDirection = opts.direction === 'back';
115
- const contentEl = enteringEl.querySelector(':scope > ion-content');
116
- const headerEls = enteringEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');
117
- const enteringToolBarEls = enteringEl.querySelectorAll(':scope > ion-header > ion-toolbar');
118
- const rootAnimation = createAnimation();
119
- const enteringContentAnimation = createAnimation();
120
- rootAnimation
121
- .addElement(enteringEl)
122
- .duration((opts.duration ?? 0) || DURATION)
123
- .easing(opts.easing || EASING)
124
- .fill('both')
125
- .beforeRemoveClass('ion-page-invisible');
126
- const topPage = backDirection ? leavingEl : enteringEl;
127
- if (topPage?.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
128
- const shadow = topPage.style.boxShadow;
129
- rootAnimation.beforeAddWrite(() => {
130
- topPage.style.boxShadow = `${isRTL ? 4 : -4}px 0 24px rgba(0, 0, 0, 0.04)`;
131
- });
132
- rootAnimation.afterAddWrite(() => {
133
- topPage.style.boxShadow = shadow;
134
- });
135
- }
136
- if (leavingEl && navEl !== null && navEl !== undefined) {
137
- const navDecorAnimation = createAnimation();
138
- navDecorAnimation.addElement(navEl);
139
- rootAnimation.addAnimation(navDecorAnimation);
140
- }
141
- if (enteringEl.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
142
- enteringContentAnimation.addElement(enteringEl);
143
- }
144
- else if (!contentEl && enteringToolBarEls.length === 0 && headerEls.length === 0) {
145
- enteringContentAnimation.addElement(enteringEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs'));
146
- }
147
- else {
148
- enteringContentAnimation.addElement(contentEl);
149
- enteringContentAnimation.addElement(headerEls);
150
- }
151
- rootAnimation.addAnimation(enteringContentAnimation);
152
- if (backDirection) {
153
- enteringContentAnimation
154
- .beforeClearStyles([OPACITY])
155
- .fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`)
156
- .fromTo(OPACITY, OFF_OPACITY, 1);
157
- }
158
- else {
159
- enteringContentAnimation.beforeClearStyles([OPACITY]).fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
160
- }
161
- if (contentEl && !enteringEl.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
162
- const enteringTransitionEffectEl = shadow(contentEl).querySelector('.transition-effect');
163
- if (enteringTransitionEffectEl) {
164
- const enteringTransitionCoverEl = enteringTransitionEffectEl.querySelector('.transition-cover');
165
- const enteringTransitionShadowEl = enteringTransitionEffectEl.querySelector('.transition-shadow');
166
- const enteringTransitionEffect = createAnimation();
167
- const enteringTransitionCover = createAnimation();
168
- const enteringTransitionShadow = createAnimation();
169
- enteringTransitionEffect
170
- .addElement(enteringTransitionEffectEl)
171
- .beforeStyles({ opacity: '1', display: 'block' })
172
- .afterStyles({ opacity: '', display: '' });
173
- enteringTransitionCover
174
- .addElement(enteringTransitionCoverEl)
175
- .beforeClearStyles([OPACITY])
176
- .fromTo(OPACITY, 0, 0.1);
177
- enteringTransitionShadow
178
- .addElement(enteringTransitionShadowEl)
179
- .beforeClearStyles([OPACITY])
180
- .fromTo(OPACITY, 0.03, 0.7);
181
- enteringTransitionEffect.addAnimation([enteringTransitionCover, enteringTransitionShadow]);
182
- enteringContentAnimation.addAnimation([enteringTransitionEffect]);
183
- }
184
- }
185
- if (topPage) {
186
- animateFixedBackButton(rootAnimation, navEl, topPage, !backDirection, opts.progressCallback !== undefined, backDirection ? enteringEl : leavingEl);
187
- }
188
- const enteringContentHasLargeTitle = enteringEl.querySelector('ion-header.header-collapse-condense');
189
- enteringToolBarEls.forEach((enteringToolBarEl) => {
190
- if (enteringToolBarEl.closest('ion-header')?.matches('.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
191
- return;
192
- }
193
- const enteringToolBar = createAnimation();
194
- enteringToolBar.addElement(enteringToolBarEl);
195
- rootAnimation.addAnimation(enteringToolBar);
196
- const enteringTitle = createAnimation();
197
- enteringTitle.addElement(enteringToolBarEl.querySelector('ion-title'));
198
- const enteringToolBarButtons = createAnimation();
199
- const buttons = Array.from(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]'));
200
- const parentHeader = enteringToolBarEl.closest('ion-header');
201
- const inactiveHeader = parentHeader?.classList.contains('header-collapse-condense-inactive');
202
- let buttonsToAnimate;
203
- if (backDirection) {
204
- buttonsToAnimate = buttons.filter((button) => {
205
- const isCollapseButton = button.classList.contains('buttons-collapse');
206
- return (isCollapseButton && !inactiveHeader) || !isCollapseButton;
207
- });
208
- }
209
- else {
210
- buttonsToAnimate = buttons.filter((button) => !button.classList.contains('buttons-collapse'));
211
- }
212
- enteringToolBarButtons.addElement(buttonsToAnimate);
213
- const enteringToolBarItems = createAnimation();
214
- enteringToolBarItems.addElement(enteringToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])'));
215
- const enteringToolBarBg = createAnimation();
216
- enteringToolBarBg.addElement(shadow(enteringToolBarEl).querySelector('.toolbar-background'));
217
- const enteringBackButton = createAnimation();
218
- const backButtonEl = enteringToolBarEl.querySelector('ion-back-button');
219
- if (backButtonEl) {
220
- enteringBackButton.addElement(backButtonEl);
221
- }
222
- enteringToolBar.addAnimation([enteringTitle, enteringToolBarButtons, enteringToolBarItems, enteringToolBarBg, enteringBackButton]);
223
- enteringToolBarButtons.fromTo(OPACITY, 0.01, 1);
224
- enteringToolBarItems.fromTo(OPACITY, 0.01, 1);
225
- if (backDirection) {
226
- if (!inactiveHeader) {
227
- enteringTitle.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`).fromTo(OPACITY, 0.01, 1);
228
- }
229
- enteringToolBarItems.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`);
230
- enteringBackButton.fromTo(OPACITY, 0.01, 1);
231
- }
232
- else {
233
- if (!enteringContentHasLargeTitle) {
234
- enteringTitle.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`).fromTo(OPACITY, 0.01, 1);
235
- }
236
- enteringToolBarItems.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
237
- enteringToolBarBg.beforeClearStyles([OPACITY, 'transform']);
238
- const translucentHeader = parentHeader?.translucent;
239
- if (!translucentHeader) {
240
- enteringToolBarBg.fromTo(OPACITY, 0.01, 'var(--opacity)');
241
- }
242
- else {
243
- enteringToolBarBg.fromTo('transform', isRTL ? 'translateX(-100%)' : 'translateX(100%)', 'translateX(0px)');
244
- }
245
- enteringBackButton.fromTo(OPACITY, 0.01, 1);
246
- if (backButtonEl) {
247
- const enteringBackBtnText = createAnimation();
248
- enteringBackBtnText
249
- .addElement(shadow(backButtonEl).querySelector('.button-text'))
250
- .fromTo(`transform`, isRTL ? 'translateX(-100px)' : 'translateX(100px)', 'translateX(0px)');
251
- enteringToolBar.addAnimation(enteringBackBtnText);
252
- }
253
- }
254
- });
255
- if (leavingEl) {
256
- const leavingContent = createAnimation();
257
- const leavingContentEl = leavingEl.querySelector(':scope > ion-content');
258
- const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar');
259
- const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');
260
- if (leavingEl.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
261
- leavingContent.addElement(leavingEl);
262
- }
263
- else if (!leavingContentEl && leavingToolBarEls.length === 0 && leavingHeaderEls.length === 0) {
264
- leavingContent.addElement(leavingEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs'));
265
- }
266
- else {
267
- leavingContent.addElement(leavingContentEl);
268
- leavingContent.addElement(leavingHeaderEls);
269
- }
270
- rootAnimation.addAnimation(leavingContent);
271
- if (backDirection) {
272
- leavingContent
273
- .beforeClearStyles([OPACITY])
274
- .fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)');
275
- const leavingPage = getIonPageElement(leavingEl);
276
- rootAnimation.afterAddWrite(() => {
277
- if (rootAnimation.getDirection() === 'normal') {
278
- leavingPage.style.setProperty('display', 'none');
279
- }
280
- });
281
- }
282
- else {
283
- leavingContent.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`).fromTo(OPACITY, 1, OFF_OPACITY);
284
- }
285
- if (leavingContentEl &&
286
- !leavingEl.querySelector(':scope > ion-header.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
287
- const leavingTransitionEffectEl = shadow(leavingContentEl).querySelector('.transition-effect');
288
- if (leavingTransitionEffectEl) {
289
- const leavingTransitionCoverEl = leavingTransitionEffectEl.querySelector('.transition-cover');
290
- const leavingTransitionShadowEl = leavingTransitionEffectEl.querySelector('.transition-shadow');
291
- const leavingTransitionEffect = createAnimation();
292
- const leavingTransitionCover = createAnimation();
293
- const leavingTransitionShadow = createAnimation();
294
- leavingTransitionEffect
295
- .addElement(leavingTransitionEffectEl)
296
- .beforeStyles({ opacity: '1', display: 'block' })
297
- .afterStyles({ opacity: '', display: '' });
298
- leavingTransitionCover
299
- .addElement(leavingTransitionCoverEl)
300
- .beforeClearStyles([OPACITY])
301
- .fromTo(OPACITY, 0.1, 0);
302
- leavingTransitionShadow
303
- .addElement(leavingTransitionShadowEl)
304
- .beforeClearStyles([OPACITY])
305
- .fromTo(OPACITY, 0.7, 0.03);
306
- leavingTransitionEffect.addAnimation([leavingTransitionCover, leavingTransitionShadow]);
307
- leavingContent.addAnimation([leavingTransitionEffect]);
308
- }
309
- }
310
- leavingToolBarEls.forEach((leavingToolBarEl) => {
311
- if (leavingToolBarEl.closest('ion-header')?.matches('.header-translucent:not(.ios-theme-disabled, .ios26-disabled)')) {
312
- return;
313
- }
314
- const leavingToolBar = createAnimation();
315
- leavingToolBar.addElement(leavingToolBarEl);
316
- const leavingTitle = createAnimation();
317
- leavingTitle.addElement(leavingToolBarEl.querySelector('ion-title'));
318
- const leavingToolBarButtons = createAnimation();
319
- const buttons = leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]');
320
- const parentHeader = leavingToolBarEl.closest('ion-header');
321
- const inactiveHeader = parentHeader?.classList.contains('header-collapse-condense-inactive');
322
- const buttonsToAnimate = Array.from(buttons).filter((button) => {
323
- const isCollapseButton = button.classList.contains('buttons-collapse');
324
- return (isCollapseButton && !inactiveHeader) || !isCollapseButton;
325
- });
326
- leavingToolBarButtons.addElement(buttonsToAnimate);
327
- const leavingToolBarItems = createAnimation();
328
- const leavingToolBarItemEls = leavingToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])');
329
- if (leavingToolBarItemEls.length > 0) {
330
- leavingToolBarItems.addElement(leavingToolBarItemEls);
331
- }
332
- const leavingToolBarBg = createAnimation();
333
- leavingToolBarBg.addElement(shadow(leavingToolBarEl).querySelector('.toolbar-background'));
334
- const leavingBackButton = createAnimation();
335
- const backButtonEl = leavingToolBarEl.querySelector('ion-back-button');
336
- if (backButtonEl) {
337
- leavingBackButton.addElement(backButtonEl);
338
- }
339
- leavingToolBar.addAnimation([leavingTitle, leavingToolBarButtons, leavingToolBarItems, leavingBackButton, leavingToolBarBg]);
340
- rootAnimation.addAnimation(leavingToolBar);
341
- leavingBackButton.fromTo(OPACITY, 0.99, 0);
342
- leavingToolBarButtons.fromTo(OPACITY, 0.99, 0);
343
- leavingToolBarItems.fromTo(OPACITY, 0.99, 0);
344
- if (backDirection) {
345
- if (!inactiveHeader) {
346
- leavingTitle
347
- .fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)')
348
- .fromTo(OPACITY, 0.99, 0);
349
- }
350
- leavingToolBarItems.fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)');
351
- leavingToolBarBg.beforeClearStyles([OPACITY, 'transform']);
352
- const translucentHeader = parentHeader?.translucent;
353
- if (!translucentHeader) {
354
- leavingToolBarBg.fromTo(OPACITY, 'var(--opacity)', 0);
355
- }
356
- else {
357
- leavingToolBarBg.fromTo('transform', 'translateX(0px)', isRTL ? 'translateX(-100%)' : 'translateX(100%)');
358
- }
359
- if (backButtonEl) {
360
- const leavingBackBtnText = createAnimation();
361
- leavingBackBtnText
362
- .addElement(shadow(backButtonEl).querySelector('.button-text'))
363
- .fromTo('transform', `translateX(${CENTER})`, `translateX(${(isRTL ? -124 : 124) + 'px'})`);
364
- leavingToolBar.addAnimation(leavingBackBtnText);
365
- }
366
- }
367
- else {
368
- if (!inactiveHeader) {
369
- leavingTitle
370
- .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
371
- .fromTo(OPACITY, 0.99, 0)
372
- .afterClearStyles([TRANSFORM, OPACITY]);
373
- }
374
- leavingToolBarItems
375
- .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
376
- .afterClearStyles([TRANSFORM, OPACITY]);
377
- leavingBackButton.afterClearStyles([OPACITY]);
378
- leavingTitle.afterClearStyles([OPACITY]);
379
- leavingToolBarButtons.afterClearStyles([OPACITY]);
380
- }
381
- });
382
- }
383
- connectNativeUIShellTransition(rootAnimation, enteringEl, leavingEl);
384
- return rootAnimation;
385
- }
386
- catch (err) {
387
- throw err;
388
- }
11
+ export const setConfig = (config) => {
12
+ Object.assign(transitionConfig, config);
389
13
  };
14
+ export const iosTransitionAnimation = createIosTransitionAnimation(transitionConfig);
package/docs/features.md CHANGED
@@ -13,6 +13,8 @@ To customize the library's default styles to match your design, several CSS vari
13
13
 
14
14
  For menus, `--ios-theme-menu-background-rgb` sets the surface RGB channels (light: `225, 230, 240`; dark: `26, 31, 34`). `--ios-theme-menu-background-opacity` controls opacity and defaults to `0.96`. Both are public customization variables and can be set on `ion-menu`.
15
15
 
16
+ Button press scaling follows `:active` with a slight overshoot. Set `--ios-theme-button-press-duration` (default: `380ms`) to customize its duration instead of the shared activated-transition duration variables.
17
+
16
18
  ## Liquid Glass mixin
17
19
 
18
20
  Import the SCSS files from the main package to use the liquid glass mixin.
@@ -27,7 +29,7 @@ ion-textarea label.textarea-wrapper {
27
29
 
28
30
  ## Native UI Shell (Experimental)
29
31
 
30
- Capacitor iOS apps can use the optional, experimental [Native UI Shell](native-ui-shell.md) to render supported fixed Ionic controls with UIKit and system Liquid Glass. Content and application logic stay in the WebView, with Ionic owning routing and page transitions. The guide explains the hybrid approach's origins in Basecamp and Capacitor, setup, supported controls and Web fallback behavior.
32
+ Capacitor iOS apps can use the optional, experimental [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell) to render supported fixed Ionic controls with UIKit and system Liquid Glass. Content and application logic stay in the WebView, with Ionic owning routing and page transitions. The guide explains the hybrid approach's origins in Basecamp and Capacitor, setup, supported controls and Web fallback behavior.
31
33
 
32
34
  ## Selective component imports
33
35
 
@@ -24,6 +24,14 @@ import { enableNativeUIShell } from '@rdlabo/ionic-theme-ios27/native';
24
24
  void enableNativeUIShell();
25
25
  ```
26
26
 
27
+ `enableNativeUIShell()` also reads the WebView's effective top-left corner radius and applies it to page transitions. To configure only the transition without enabling native controls, call:
28
+
29
+ ```ts
30
+ import { configureNativeTransition } from '@rdlabo/ionic-theme-ios27/native';
31
+
32
+ await configureNativeTransition();
33
+ ```
34
+
27
35
  Keep the existing `navAnimation: iosTransitionAnimation` setting. No per-page registration, component list, native callback, or Swift view controller is required. Run `npx cap sync ios` after installing or updating the package. The native plugin uses Swift Package Manager (SPM). For an existing CocoaPods app, run `npx cap spm-migration-assistant` and link the generated `CapApp-SPM` package to the app target in Xcode. Build with Xcode 26 or later and Capacitor 8; native glass requires iOS 26 or later. Web, Android, SSR and older iOS keep the Web implementation.
28
36
 
29
37
  This is an opt-in feature. The ordinary package entry point does not import Capacitor, and `@capacitor/core` is an optional peer dependency. Native sources are still detected and built by Capacitor's sync when this package is installed in a Capacitor project, even if the application does not call `enableNativeUIShell()`.
@@ -122,8 +130,113 @@ console.log(shell.getStatus()); // state, projected control count, update count,
122
130
  await shell.destroy(); // restore DOM, remove native controls and release listeners/cache
123
131
  ```
124
132
 
133
+ Projection is globally enabled by default. Limit it to selected Ionic components when an application only wants part of the native shell, or disable it globally while retaining the same configuration path:
134
+
135
+ ```ts
136
+ const shell = await enableNativeUIShell({
137
+ enabled: true,
138
+ controls: {
139
+ tabs: true,
140
+ },
141
+ });
142
+
143
+ // Equivalent to leaving Native UI Shell off; all controls remain on the Web.
144
+ const disabledShell = await enableNativeUIShell({ enabled: false });
145
+ ```
146
+
147
+ Omitting `controls` enables every supported control for backward compatibility. When `controls` is present, only entries set to `true` are native-eligible. Available entries are `tabs`, `toolbar`, `segment`, and `fab`.
148
+
149
+ For a custom modal or overlay that Native UI Shell cannot detect, acquire a suspension before presenting it. The resolved suspension means projected controls have returned to Web rendering. Always release it after dismissal:
150
+
151
+ ```ts
152
+ const suspension = await shell.suspend();
153
+
154
+ try {
155
+ await modal.present();
156
+ await modal.onDidDismiss();
157
+ } finally {
158
+ await suspension.resume();
159
+ }
160
+ ```
161
+
162
+ Suspensions are nestable and `resume()` is idempotent. Native projection resumes only after every active suspension has been released, using the current DOM rather than a stale snapshot.
163
+
125
164
  The native material and control appearance follow the running iOS version; an iOS 26 device does not acquire iOS 27's appearance merely by installing this theme.
126
165
 
166
+ ## Native UI Shell API
167
+
168
+ The generated reference below documents the handle returned by `enableNativeUIShell()`. The underlying Capacitor bridge and its control-snapshot protocol are implementation details.
169
+
170
+ <docgen-index>
171
+
172
+ * [`getStatus()`](#getstatus)
173
+ * [`suspend()`](#suspend)
174
+ * [`destroy()`](#destroy)
175
+ * [Interfaces](#interfaces)
176
+
177
+ </docgen-index>
178
+
179
+ <docgen-api>
180
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
181
+
182
+ ### getStatus()
183
+
184
+ ```typescript
185
+ getStatus() => NativeUIShellStatus
186
+ ```
187
+
188
+ Returns the current Web/native projection state.
189
+
190
+ **Returns:** <code><a href="#nativeuishellstatus">NativeUIShellStatus</a></code>
191
+
192
+ --------------------
193
+
194
+
195
+ ### suspend()
196
+
197
+ ```typescript
198
+ suspend() => Promise<NativeUIShellSuspension>
199
+ ```
200
+
201
+ Restores projected controls to the Web until the returned lease is resumed.
202
+
203
+ **Returns:** <code>Promise&lt;<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>&gt;</code>
204
+
205
+ --------------------
206
+
207
+
208
+ ### destroy()
209
+
210
+ ```typescript
211
+ destroy() => Promise<void>
212
+ ```
213
+
214
+ Stops synchronization, restores Web controls and releases native resources.
215
+
216
+ --------------------
217
+
218
+
219
+ ### Interfaces
220
+
221
+
222
+ #### NativeUIShellStatus
223
+
224
+ | Prop | Type |
225
+ | --------------- | ------------------------------------------- |
226
+ | **`state`** | <code>'native' \| 'stopped' \| 'web'</code> |
227
+ | **`projected`** | <code>number</code> |
228
+ | **`updates`** | <code>number</code> |
229
+ | **`reason`** | <code>string</code> |
230
+
231
+
232
+ #### NativeUIShellSuspension
233
+
234
+ | Method | Signature | Description |
235
+ | ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
236
+ | **resume** | () =&gt; Promise&lt;void&gt; | Releases this suspension. Native projection resumes after all active suspensions are released. |
237
+
238
+ </docgen-api>
239
+
127
240
  ## Source layout
128
241
 
129
242
  Each TypeScript module in [`src/native/components`](../src/native/components) declares its Ionic tag and DOM reader. `components/index.ts` combines those exports into discovery selectors and the component type. Shared DOM measurements, item data and SVG rendering live in `src/native/shared`; `runtime.ts` owns synchronization, visibility handoffs and lifecycle events.
@@ -8,6 +8,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
8
8
  public let jsName = "IonicNativeUIShell"
9
9
  public let pluginMethods: [CAPPluginMethod] = [
10
10
  CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "getWebViewMetrics", returnType: CAPPluginReturnPromise),
11
12
  CAPPluginMethod(name: "update", returnType: CAPPluginReturnPromise),
12
13
  CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise)
13
14
  ]
@@ -45,6 +46,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
45
46
  }
46
47
  }
47
48
  if keyboard { self.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh") }
49
+ else if name == UIDevice.orientationDidChangeNotification { self.notifyWebViewMetricsChange() }
48
50
  })
49
51
  }
50
52
  for name in [UIApplication.didBecomeActiveNotification, UIResponder.keyboardDidHideNotification,
@@ -52,6 +54,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
52
54
  observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
53
55
  if name == UIResponder.keyboardDidHideNotification { self?.keyboardVisible = false }
54
56
  self?.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh", data: name == UIApplication.didBecomeActiveNotification ? "{\"retireSearch\":true}" : "{}")
57
+ if name == UIApplication.didBecomeActiveNotification { self?.notifyWebViewMetricsChange() }
55
58
  })
56
59
  }
57
60
  }
@@ -60,6 +63,31 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
60
63
  observers.forEach(NotificationCenter.default.removeObserver)
61
64
  }
62
65
 
66
+ private func webViewMetrics() -> JSObject? {
67
+ guard #available(iOS 26.0, *), let webView = bridge?.webView else { return nil }
68
+ webView.layoutIfNeeded()
69
+ return ["radius": Double(webView.effectiveRadius(corner: .topLeft))]
70
+ }
71
+
72
+ private func notifyWebViewMetricsChange() {
73
+ guard let metrics = webViewMetrics() else { return }
74
+ notifyListeners("webViewMetricsChange", data: metrics)
75
+ }
76
+
77
+ @objc func getWebViewMetrics(_ call: CAPPluginCall) {
78
+ DispatchQueue.main.async { [weak self] in
79
+ guard #available(iOS 26.0, *) else {
80
+ call.resolve(["radius": 0])
81
+ return
82
+ }
83
+ guard let metrics = self?.webViewMetrics() else {
84
+ call.reject("WebView unavailable")
85
+ return
86
+ }
87
+ call.resolve(metrics)
88
+ }
89
+ }
90
+
63
91
  @objc func configure(_ call: CAPPluginCall) {
64
92
  DispatchQueue.main.async { [weak self] in
65
93
  // A new JS context starts revision numbering again (live reload / navigation).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Masahiko Sakakibara
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # @rdlabo/ionic-theme-utils
2
+
3
+ Shared, theme-neutral utilities for Rdlabo Ionic themes.
4
+
5
+ TypeScript and Sass sources are exported directly. Consumers are expected to
6
+ compile them as part of an Angular/Ionic application build.
7
+
8
+ ```typescript
9
+ import { createIosTransitionAnimation, getPopoverPosition } from '@rdlabo/ionic-theme-utils';
10
+ ```
11
+
12
+ ```scss
13
+ @use 'pkg:@rdlabo/ionic-theme-utils/styles/structured-list';
14
+ ```
15
+
16
+ The package currently contains shared iOS transition construction, popover
17
+ positioning, searchable tab-bar utilities, and structured-list Sass mixins.
18
+
19
+ ## Development
20
+
21
+ ```shell
22
+ npm install
23
+ npm run build
24
+ npm run lint
25
+ ```
26
+
27
+ Use `npm link` while developing this package together with a theme repository.
28
+ Theme repositories should use a tagged Git dependency for reproducible installs.
@@ -0,0 +1,3 @@
1
+ export declare const getElementRoot: (el: HTMLElement, fallback?: HTMLElement) => HTMLElement | ShadowRoot;
2
+ export declare const raf: (h: FrameRequestCallback) => any;
3
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,GAAI,IAAI,WAAW,EAAE,WAAU,WAAgB,6BAEzE,CAAC;AAEF,eAAO,MAAM,GAAG,GAAI,GAAG,oBAAoB,QAQ1C,CAAC"}