@rdlabo/ionic-theme-ios27 0.1.0-beta.pr154.shac3e35365120e → 0.1.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 (95) hide show
  1. package/README.md +3 -3
  2. package/dist/css/components/ion-action-sheet.css +1 -1
  3. package/dist/css/components/ion-alert.css +1 -1
  4. package/dist/css/components/ion-button.css +1 -1
  5. package/dist/css/components/ion-card.css +1 -1
  6. package/dist/css/components/ion-fab.css +1 -1
  7. package/dist/css/components/ion-list.css +1 -1
  8. package/dist/css/components/ion-menu.css +1 -1
  9. package/dist/css/components/ion-modal.css +1 -1
  10. package/dist/css/components/ion-popover.css +1 -1
  11. package/dist/css/components/ion-range.css +1 -1
  12. package/dist/css/components/ion-segment.css +1 -1
  13. package/dist/css/components/ion-tabs.css +1 -1
  14. package/dist/css/components/ion-toast.css +1 -1
  15. package/dist/css/components/ion-toolbar.css +1 -1
  16. package/dist/css/default-variables.css +1 -1
  17. package/dist/css/ionic-theme-ios27-dark-always.css +1 -1
  18. package/dist/css/ionic-theme-ios27-dark-class.css +1 -1
  19. package/dist/css/ionic-theme-ios27-dark-system.css +1 -1
  20. package/dist/css/ionic-theme-ios27.css +1 -1
  21. package/dist/css/md-ion-list-inset.css +1 -1
  22. package/dist/css/utils/dark/ion-list.css +1 -0
  23. package/dist/css/utils/group-heading.css +1 -0
  24. package/dist/native/components/searchable-tabs.d.ts.map +1 -1
  25. package/dist/native/components/searchable-tabs.js +9 -1
  26. package/dist/native/definitions.d.ts +4 -0
  27. package/dist/native/definitions.d.ts.map +1 -1
  28. package/dist/native/runtime.d.ts.map +1 -1
  29. package/dist/native/runtime.js +51 -27
  30. package/dist/native/shared/candidate.d.ts.map +1 -1
  31. package/dist/native/shared/candidate.js +17 -0
  32. package/dist/native/shared/crossfade.d.ts +8 -0
  33. package/dist/native/shared/crossfade.d.ts.map +1 -0
  34. package/dist/native/shared/crossfade.js +46 -0
  35. package/dist/native/shared/dom.d.ts.map +1 -1
  36. package/dist/native/shared/dom.js +2 -1
  37. package/dist/popover/animations/ios.enter.d.ts.map +1 -1
  38. package/dist/popover/animations/ios.enter.js +105 -9
  39. package/dist/popover/animations/ios.leave.d.ts.map +1 -1
  40. package/dist/popover/animations/ios.leave.js +15 -1
  41. package/dist/popover/callout-surface.d.ts +2 -0
  42. package/dist/popover/callout-surface.d.ts.map +1 -0
  43. package/dist/popover/callout-surface.js +44 -0
  44. package/dist/sheets-of-glass/index.js +2 -1
  45. package/dist/transition/ios.transition.d.ts.map +1 -1
  46. package/dist/transition/ios.transition.js +0 -223
  47. package/docs/experimental-animation.md +3 -3
  48. package/docs/features.md +7 -2
  49. package/docs/native-ui-shell.md +9 -5
  50. package/docs/special-markup.md +17 -0
  51. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButton.swift +7 -2
  52. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +10 -0
  53. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSegment.swift +47 -15
  54. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +42 -7
  55. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +13 -6
  56. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellCrossfade.swift +35 -0
  57. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +6 -1
  58. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +59 -0
  59. package/package.json +1 -1
  60. package/src/native/components/searchable-tabs.ts +12 -1
  61. package/src/native/definitions.ts +4 -0
  62. package/src/native/runtime.ts +54 -31
  63. package/src/native/shared/candidate.ts +19 -0
  64. package/src/native/shared/crossfade.ts +49 -0
  65. package/src/native/shared/dom.ts +4 -1
  66. package/src/popover/animations/ios.enter.ts +123 -10
  67. package/src/popover/animations/ios.leave.ts +15 -1
  68. package/src/popover/callout-surface.ts +54 -0
  69. package/src/sheets-of-glass/index.ts +2 -2
  70. package/src/styles/components/ion-action-sheet.scss +35 -17
  71. package/src/styles/components/ion-alert.scss +51 -31
  72. package/src/styles/components/ion-button.scss +4 -7
  73. package/src/styles/components/ion-card.scss +64 -3
  74. package/src/styles/components/ion-fab.scss +3 -5
  75. package/src/styles/components/ion-list.scss +14 -1
  76. package/src/styles/components/ion-menu.scss +19 -7
  77. package/src/styles/components/ion-modal.scss +63 -14
  78. package/src/styles/components/ion-popover.scss +52 -9
  79. package/src/styles/components/ion-range.scss +49 -50
  80. package/src/styles/components/ion-segment.scss +15 -16
  81. package/src/styles/components/ion-tabs.scss +73 -13
  82. package/src/styles/components/ion-toast.scss +16 -25
  83. package/src/styles/components/ion-toolbar.scss +17 -1
  84. package/src/styles/default-variables.scss +4 -0
  85. package/src/styles/utils/_glass.scss +96 -0
  86. package/src/styles/utils/api.scss +46 -4
  87. package/src/styles/utils/dark/ion-button.scss +13 -0
  88. package/src/styles/utils/dark/ion-fab.scss +9 -0
  89. package/src/styles/utils/dark/ion-list.scss +7 -0
  90. package/src/styles/utils/dark/ion-segment.scss +8 -3
  91. package/src/styles/utils/dark/ion-tabs.scss +20 -0
  92. package/src/styles/utils/group-heading.scss +5 -0
  93. package/src/styles/utils/structured-list.scss +68 -15
  94. package/src/styles/utils/theme-dark.scss +23 -0
  95. package/src/transition/ios.transition.ts +0 -487
@@ -2,6 +2,7 @@ import { createAnimation } from '@ionic/core';
2
2
  import type { Animation } from '@ionic/core';
3
3
  import { getElementRoot } from '../../utils';
4
4
  import { calculateWindowAdjustment, getPopoverDimensions, getPopoverPosition } from '../utils';
5
+ import { createCalloutSurface } from '../callout-surface';
5
6
 
6
7
  const POPOVER_IOS_BODY_PADDING = 5;
7
8
  export const POPOVER_IOS_BODY_MARGIN = 8;
@@ -19,12 +20,41 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
19
20
 
20
21
  const root = getElementRoot(baseEl);
21
22
  const contentEl = root.querySelector('.popover-content') as HTMLElement;
23
+ const arrowEl = root.querySelector<HTMLElement>('.popover-arrow');
22
24
 
23
25
  const referenceSizeEl = trigger || ev?.detail?.ionShadowTarget || ev?.target;
26
+ const anchorBounds = referenceSizeEl?.getBoundingClientRect();
27
+ // Overlays live outside the page, but must stay in the pane that opened them.
28
+ let paneAnchor = referenceSizeEl as HTMLElement | undefined;
29
+ let pane: HTMLElement | null = null;
30
+ while (paneAnchor && !pane) {
31
+ pane = paneAnchor.closest('ion-content, .ion-page, ion-menu');
32
+ paneAnchor = (paneAnchor.getRootNode() as ShadowRoot).host as HTMLElement | undefined;
33
+ }
34
+ const paneBounds = pane?.getBoundingClientRect();
35
+ const scroll = pane?.shadowRoot?.querySelector<HTMLElement>('[part="scroll"]');
36
+ const scrollStyle = scroll ? getComputedStyle(scroll) : null;
37
+ let paneLeft = Math.max(0, (paneBounds?.left ?? 0) + (parseFloat(scrollStyle?.paddingLeft ?? '0') || 0));
38
+ let paneRight = Math.min(bodyWidth, (paneBounds?.right ?? bodyWidth) - (parseFloat(scrollStyle?.paddingRight ?? '0') || 0));
39
+ // Toolbars may be siblings of ion-content; exclude the visible menu there too.
40
+ const splitPane = pane?.closest('ion-split-pane.split-pane-visible');
41
+ if (splitPane && pane?.closest('.split-pane-main')) {
42
+ for (const menu of Array.from(splitPane.querySelectorAll<HTMLElement>(':scope > ion-menu.menu-pane-visible'))) {
43
+ const rect = menu.getBoundingClientRect();
44
+ if (rect.left <= paneLeft && rect.right > paneLeft) paneLeft = rect.right;
45
+ if (rect.right >= paneRight && rect.left < paneRight) paneRight = rect.left;
46
+ }
47
+ }
48
+ const paneMargin = size === 'cover' ? 0 : POPOVER_IOS_BODY_MARGIN;
49
+ const availableWidth = Math.max(0, paneRight - paneLeft - paneMargin * 2);
50
+ if (pane && availableWidth > 0 && contentEl.getBoundingClientRect().width > availableWidth) {
51
+ contentEl.dataset['previousMaxWidth'] = contentEl.style.maxWidth;
52
+ contentEl.style.maxWidth = `${availableWidth}px`;
53
+ }
24
54
  const { contentWidth, contentHeight } = getPopoverDimensions(size, contentEl, referenceSizeEl);
25
55
 
26
56
  const isReplace = ((): boolean => {
27
- if (!['ion-button', 'ion-buttons'].includes(referenceSizeEl.localName)) {
57
+ if (reference === 'event' || !referenceSizeEl || !['ion-button', 'ion-buttons'].includes(referenceSizeEl.localName)) {
28
58
  return false;
29
59
  }
30
60
  if (referenceSizeEl.matches('.ios-theme-disabled, .ios26-disabled')) {
@@ -42,10 +72,22 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
42
72
 
43
73
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, reference, side, align, defaultPosition, trigger, ev);
44
74
 
75
+ // Use the same reference for placement, the callout and the animation origin.
76
+ const anchor = reference === 'event' ? results.referenceCoordinates : anchorBounds;
77
+
45
78
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
46
79
  const margin = size === 'cover' ? 0 : POPOVER_IOS_BODY_MARGIN;
47
80
 
48
- const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, addPopoverBottomClass } = calculateWindowAdjustment(
81
+ const {
82
+ originX,
83
+ originY,
84
+ top,
85
+ left: windowLeft,
86
+ bottom,
87
+ checkSafeAreaLeft,
88
+ checkSafeAreaRight,
89
+ addPopoverBottomClass,
90
+ } = calculateWindowAdjustment(
49
91
  side,
50
92
  results.top,
51
93
  results.left,
@@ -58,14 +100,59 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
58
100
  results.originX,
59
101
  results.originY,
60
102
  results.referenceCoordinates,
61
- referenceSizeEl.getBoundingClientRect(),
103
+ referenceSizeEl?.getBoundingClientRect(),
62
104
  isReplace,
63
105
  );
106
+ // A replacing surface grows inward from the button's edge, not its center.
107
+ const preferredLeft =
108
+ isReplace && anchorBounds
109
+ ? anchorBounds.left + anchorBounds.width / 2 <= (paneLeft + paneRight) / 2
110
+ ? anchorBounds.left
111
+ : anchorBounds.right - contentWidth
112
+ : windowLeft;
113
+ const left = pane ? Math.max(paneLeft + paneMargin, Math.min(paneRight - paneMargin - contentWidth, preferredLeft)) : preferredLeft;
114
+ const physicalSide = side === 'start' ? (isRTL ? 'right' : 'left') : side === 'end' ? (isRTL ? 'left' : 'right') : side;
115
+ const horizontal = physicalSide === 'left' || physicalSide === 'right';
116
+ const above = addPopoverBottomClass || physicalSide === 'top';
117
+ const hasCallout = !!arrowEl && !isReplace && !!anchor && size !== 'cover' && bottom === undefined;
118
+ const surfaceTop = hasCallout && !horizontal ? top + (above ? -5 : 5) : top;
119
+ const contentOrigin = anchor
120
+ ? `${anchor.left + anchor.width / 2 - left}px ${anchor.top + anchor.height / 2 - surfaceTop}px`
121
+ : `${originX} ${originY}`;
64
122
 
65
123
  const baseAnimation = createAnimation();
66
124
  const backdropAnimation = createAnimation();
67
125
  const contentAnimation = createAnimation();
68
126
  const targetAnimation = createAnimation();
127
+ const arrowAnimation = createAnimation();
128
+ const surfaceAnimation = createAnimation();
129
+ if (arrowEl) {
130
+ arrowAnimation.addElement(arrowEl).delay(300).duration(200).fromTo('opacity', 0, 1);
131
+ }
132
+ if (hasCallout && anchor) {
133
+ const length = horizontal ? contentHeight : contentWidth;
134
+ const inset = Math.min(48, length / 2);
135
+ const center = Math.max(
136
+ inset,
137
+ Math.min(length - inset, horizontal ? anchor.top + anchor.height / 2 - top : anchor.left + anchor.width / 2 - left),
138
+ );
139
+ const arrowSide = horizontal ? (physicalSide === 'left' ? 'right' : 'left') : above ? 'bottom' : 'top';
140
+ const layers = createCalloutSurface(root, contentWidth, contentHeight, arrowSide, center);
141
+ baseEl.classList.add('ios-theme-callout');
142
+ for (const layer of layers) {
143
+ layer.style.left = `calc(${left - 32}px + var(--offset-x, 0))`;
144
+ layer.style.top = `calc(${surfaceTop - 32}px + var(--offset-y, 0))`;
145
+ const [originLeft, originTop] = contentOrigin.split(' ').map(parseFloat);
146
+ layer.style.transformOrigin = `${originLeft + 32}px ${originTop + 32}px`;
147
+ }
148
+ surfaceAnimation
149
+ .addElement(layers)
150
+ .delay(100)
151
+ .duration(400)
152
+ .easing('cubic-bezier(0, 1, 0.22, 1)')
153
+ .fromTo('transform', 'scale(0)', 'scale(1)')
154
+ .fromTo('opacity', 0.01, 1);
155
+ }
69
156
 
70
157
  backdropAnimation
71
158
  .delay(100)
@@ -86,12 +173,12 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
86
173
  .delay(100)
87
174
  .duration(400)
88
175
  .addElement(root.querySelector('.popover-content')!)
89
- .beforeStyles({ 'transform-origin': `${originY} ${originX}` })
176
+ .beforeStyles({ 'transform-origin': contentOrigin })
90
177
  .beforeAddWrite(() => {
91
178
  /**
92
179
  * 'transformOrigin' use for leave animation.
93
180
  */
94
- root.querySelector<HTMLElement>('.popover-content')!.dataset['transformOrigin'] = `${originY} ${originX}`;
181
+ root.querySelector<HTMLElement>('.popover-content')!.dataset['transformOrigin'] = contentOrigin;
95
182
  })
96
183
  .fromTo('transform', 'scale(0)', 'scale(1)')
97
184
  .fromTo('opacity', 0.01, 1);
@@ -129,16 +216,42 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
129
216
 
130
217
  let leftValue = `${left}px`;
131
218
 
132
- if (checkSafeAreaLeft) {
219
+ if (checkSafeAreaLeft && !pane) {
133
220
  leftValue = `${left}px${safeAreaLeft}`;
134
221
  }
135
- if (checkSafeAreaRight) {
222
+ if (checkSafeAreaRight && !pane) {
136
223
  leftValue = `${left}px${safeAreaRight}`;
137
224
  }
138
225
 
139
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
226
+ contentEl.style.setProperty('top', `calc(${surfaceTop}px + var(--offset-y, 0))`);
140
227
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
141
- contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
228
+ contentEl.style.setProperty('transform-origin', contentOrigin);
229
+
230
+ // Morphing buttons replace their anchor; ordinary anchored popovers point to it.
231
+ if (arrowEl) {
232
+ arrowEl.style.display = 'none';
233
+ if (hasCallout && anchor) {
234
+ const inset = Math.min(48, (horizontal ? contentHeight : contentWidth) / 2);
235
+ const clamp = (value: number, length: number) => Math.max(inset, Math.min(length - inset, value));
236
+ let arrowLeft: number;
237
+ let arrowTop: number;
238
+ let rotation: number;
239
+ if (horizontal) {
240
+ arrowLeft = physicalSide === 'left' ? left + contentWidth - 10 : left - 24;
241
+ arrowTop = top + clamp(anchor.top + anchor.height / 2 - top, contentHeight) - 7;
242
+ rotation = physicalSide === 'left' ? 90 : -90;
243
+ } else {
244
+ arrowLeft = left + clamp(anchor.left + anchor.width / 2 - left, contentWidth) - 17;
245
+ arrowTop = above ? surfaceTop + contentHeight - 1 : surfaceTop - 13;
246
+ rotation = above ? 180 : 0;
247
+ }
248
+ arrowEl.style.setProperty('display', 'block');
249
+ arrowEl.style.setProperty('top', `calc(${arrowTop}px + var(--offset-y, 0))`);
250
+ arrowEl.style.setProperty('left', `calc(${arrowLeft}px + var(--offset-x, 0))`);
251
+ arrowEl.style.setProperty('bottom', 'auto');
252
+ arrowEl.style.setProperty('transform', `rotate(${rotation}deg)`);
253
+ }
254
+ }
142
255
  })
143
- .addAnimation([backdropAnimation, contentAnimation, targetAnimation]);
256
+ .addAnimation([backdropAnimation, contentAnimation, targetAnimation, arrowAnimation, surfaceAnimation]);
144
257
  };
@@ -15,6 +15,12 @@ export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
15
15
  const backdropAnimation = createAnimation();
16
16
  const contentAnimation = createAnimation();
17
17
  const targetAnimation = createAnimation();
18
+ const surfaceAnimation = createAnimation()
19
+ .addElement(Array.from(root.querySelectorAll<HTMLElement>('.ios-theme-callout-layer')))
20
+ .duration(400)
21
+ .easing('ease')
22
+ .fromTo('opacity', 0.99, 0)
23
+ .fromTo('transform', 'scale(1)', 'scale(0)');
18
24
 
19
25
  const doc = baseEl.ownerDocument as any;
20
26
  const replaceElement = doc.querySelector('.ios27-replace-element') as HTMLElement | null;
@@ -59,18 +65,26 @@ export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
59
65
  .afterAddWrite(() => {
60
66
  baseEl.style.removeProperty('--width');
61
67
  baseEl.classList.remove('popover-bottom');
68
+ baseEl.classList.remove('ios-theme-callout');
69
+ root.querySelectorAll('.ios-theme-callout-layer').forEach((layer) => layer.remove());
62
70
 
63
71
  contentEl.style.removeProperty('top');
64
72
  contentEl.style.removeProperty('left');
65
73
  contentEl.style.removeProperty('bottom');
66
74
  contentEl.style.removeProperty('transform-origin');
75
+ if (contentEl.dataset['previousMaxWidth'] !== undefined) {
76
+ contentEl.style.maxWidth = contentEl.dataset['previousMaxWidth'];
77
+ delete contentEl.dataset['previousMaxWidth'];
78
+ }
67
79
 
68
80
  if (arrowEl) {
69
81
  arrowEl.style.removeProperty('top');
70
82
  arrowEl.style.removeProperty('left');
71
83
  arrowEl.style.removeProperty('display');
84
+ arrowEl.style.removeProperty('bottom');
85
+ arrowEl.style.removeProperty('transform');
72
86
  }
73
87
  })
74
88
  .duration(300)
75
- .addAnimation([backdropAnimation, contentAnimation, targetAnimation]);
89
+ .addAnimation([backdropAnimation, contentAnimation, targetAnimation, surfaceAnimation]);
76
90
  };
@@ -0,0 +1,54 @@
1
+ /** Draw one glass silhouette so the arrow cannot sample the body's own border/shadow. */
2
+ export const createCalloutSurface = (
3
+ root: ShadowRoot | HTMLElement,
4
+ width: number,
5
+ height: number,
6
+ side: 'top' | 'right' | 'bottom' | 'left',
7
+ center: number,
8
+ ): HTMLElement[] => {
9
+ const inset = 32;
10
+ const x = inset;
11
+ const y = inset;
12
+ const right = x + width;
13
+ const bottom = y + height;
14
+ const radius = Math.max(0, Math.min(34, width / 2, height / 2, (side === 'left' || side === 'right' ? height : width) / 2 - 17));
15
+ const point = (u: number, v: number): string => {
16
+ switch (side) {
17
+ case 'top':
18
+ return `${x + center - 17 + u} ${y - 13 + v}`;
19
+ case 'right':
20
+ return `${right + 13 - v} ${y + center - 17 + u}`;
21
+ case 'bottom':
22
+ return `${x + center + 17 - u} ${bottom + 13 - v}`;
23
+ case 'left':
24
+ return `${x - 13 + v} ${y + center + 17 - u}`;
25
+ }
26
+ };
27
+ const arrow = `L ${point(0, 13)} C ${point(3, 13)} ${point(4.5, 12)} ${point(6.5, 10)} L ${point(14.5, 1.5)} C ${point(16, -0.5)} ${point(18, -0.5)} ${point(19.5, 1.5)} L ${point(27.5, 10)} C ${point(29.5, 12)} ${point(31, 13)} ${point(34, 13)}`;
28
+ const path = `M ${x + radius} ${y} ${side === 'top' ? arrow : ''}
29
+ L ${right - radius} ${y} A ${radius} ${radius} 0 0 1 ${right} ${y + radius} ${side === 'right' ? arrow : ''}
30
+ L ${right} ${bottom - radius} A ${radius} ${radius} 0 0 1 ${right - radius} ${bottom} ${side === 'bottom' ? arrow : ''}
31
+ L ${x + radius} ${bottom} A ${radius} ${radius} 0 0 1 ${x} ${bottom - radius} ${side === 'left' ? arrow : ''}
32
+ L ${x} ${y + radius} A ${radius} ${radius} 0 0 1 ${x + radius} ${y} Z`;
33
+ const svg = (body: string) =>
34
+ `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${width + inset * 2}" height="${height + inset * 2}">${body}</svg>`)}")`;
35
+ const fill = svg(`<path d="${path}" fill="white"/>`);
36
+ const stroke = svg(`<path d="${path}" fill="none" stroke="white" stroke-width="0.5"/>`);
37
+ const shadow = svg(
38
+ `<defs><filter id="s" x="-50%" y="-100%" width="200%" height="300%"><feGaussianBlur stdDeviation="4"/><feOffset dy="2"/><feComposite in2="SourceGraphic" operator="out"/></filter></defs><path d="${path}" fill="white" filter="url(#s)"/>`,
39
+ );
40
+ const wrapper = root.querySelector('.popover-wrapper')!;
41
+ return [
42
+ ['callout-shadow', shadow],
43
+ ['callout-glass', fill],
44
+ ['callout-border', stroke],
45
+ ].map(([part, mask]) => {
46
+ const element = document.createElement('div');
47
+ element.setAttribute('part', part);
48
+ element.className = 'ios-theme-callout-layer';
49
+ element.setAttribute('aria-hidden', 'true');
50
+ element.style.cssText = `position:absolute;pointer-events:none;z-index:9;width:${width + inset * 2}px;height:${height + inset * 2}px;mask-image:${mask};-webkit-mask-image:${mask};mask-repeat:no-repeat;`;
51
+ wrapper.append(element);
52
+ return element;
53
+ });
54
+ };
@@ -307,14 +307,14 @@ export const registerEffect = (
307
307
  stopTabPress();
308
308
  moveAnimation.destroy();
309
309
  moveAnimation = undefined;
310
+ const tabs = Array.from(targetElement.querySelectorAll<HTMLElement>(effectTagName)).filter((tab) => tab.offsetWidth > 0);
310
311
  const releasing = createTabBarReleaseAnimation(
311
312
  effectElement,
312
313
  currentTouchedElement,
313
314
  tapElapsed !== undefined && tapElapsed < 200
314
315
  ? {
315
316
  elapsed: tapElapsed,
316
- distance:
317
- Math.abs(currentTouchedElement.offsetLeft - selectedElementBeforeGesture!.offsetLeft) / currentTouchedElement.offsetWidth,
317
+ distance: Math.abs(tabs.indexOf(currentTouchedElement) - tabs.indexOf(selectedElementBeforeGesture!)),
318
318
  }
319
319
  : undefined,
320
320
  );
@@ -1,6 +1,7 @@
1
1
  @use '../utils/api';
2
2
 
3
3
  ion-action-sheet.ios:not(.ios-theme-disabled, .ios26-disabled) {
4
+ @include api.glass-overlay-variables;
4
5
  --backdrop-opacity: 0.2;
5
6
  --color: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
6
7
 
@@ -10,52 +11,69 @@ ion-action-sheet.ios:not(.ios-theme-disabled, .ios26-disabled) {
10
11
  }
11
12
 
12
13
  .action-sheet-container {
13
- @include api.glass-background-overlay;
14
- transition: transform 200ms ease;
15
- &:has(.ion-activated) {
16
- transform: scale(1.016);
17
- @include api.glass-background-overlay-activated;
18
- }
14
+ @include api.glass-overlay-surface;
19
15
  margin: 0 12px;
20
- border-radius: 32px;
16
+ border-radius: 36px;
17
+ padding: 16px;
21
18
 
22
19
  .action-sheet-title,
23
20
  .action-sheet-has-sub-title {
24
21
  background: none;
25
22
  }
26
23
  .action-sheet-title {
27
- padding-top: 8px;
24
+ padding: 6px 14px 20px;
25
+ text-align: start;
26
+ font-size: 1rem;
27
+ line-height: 1.21;
28
+ font-weight: 600;
29
+ color: var(--ion-text-color, #000);
28
30
  }
29
- .action-sheet-has-sub-title {
31
+ .action-sheet-sub-title {
32
+ padding-top: 8px;
33
+ font-size: 0.94rem;
34
+ line-height: 1.19;
35
+ color: var(--ion-text-color-step-600, var(--ion-color-step-400, #999999));
30
36
  }
31
37
 
32
38
  .action-sheet-group {
33
39
  background: transparent;
40
+ margin: 0;
41
+ border-radius: 0;
34
42
 
35
43
  button {
36
44
  @include api.glass-background-overlay-button-variable;
37
45
  &::after {
38
46
  content: none;
39
47
  }
40
- &:not(.action-sheet-destructive):not(.action-sheet-selected) {
48
+ &:not(.action-sheet-destructive):not(.action-sheet-selected):not(.action-sheet-preferred) {
41
49
  --button-color: var(--ion-text-color, #000);
42
50
  --button-color-hover: var(--ion-text-color, #000);
43
51
  --button-color-activated: var(--ion-text-color, #000);
44
52
  }
53
+ &.action-sheet-preferred {
54
+ @include api.glass-overlay-preferred-button(--button-background, --button-color);
55
+ }
45
56
  &.action-sheet-destructive {
46
- --button-color: var(--ion-color-danger, #c5000f);
47
- --button-color-hover: var(--ion-color-danger, #c5000f);
57
+ --button-color: var(--ios-theme-destructive-color, var(--ion-color-danger, #ea292d));
58
+ }
59
+ &.action-sheet-destructive,
60
+ &.action-sheet-preferred {
61
+ color: var(--button-color);
62
+ --button-color-hover: var(--button-color);
63
+ --button-color-activated: var(--button-color);
64
+ --button-color-focused: var(--button-color);
48
65
  }
49
- font-weight: 550;
66
+ font-weight: 500;
50
67
  &.action-sheet-cancel {
51
- margin-top: 12px;
52
- font-weight: 550;
68
+ margin-top: 8px;
69
+ font-weight: 600;
53
70
  }
54
- border-radius: 32px;
71
+ border-radius: 24px;
55
72
  padding: 0 14px;
56
- min-height: 52px;
73
+ min-height: 48px;
57
74
  span.action-sheet-button-inner {
58
75
  font-size: 1rem;
76
+ line-height: 1.19;
59
77
  ion-icon {
60
78
  font-size: 1.2rem;
61
79
  }
@@ -1,35 +1,43 @@
1
1
  @use '../utils/api';
2
2
 
3
3
  ion-alert.ios:not(.ios-theme-disabled, .ios26-disabled) {
4
+ @include api.glass-overlay-variables;
4
5
  --min-width: 280px;
6
+ --max-width: 320px;
5
7
  --backdrop-opacity: 0.2;
6
8
 
7
- // --max-width: clamp(270px, 16.875rem, 324px);
8
- --max-width: 322px;
9
+ .alert-wrapper {
10
+ @include api.glass-overlay-surface;
11
+ border-radius: 36px;
12
+ // The dialog receives programmatic focus; focus feedback belongs to its controls.
13
+ outline: none;
9
14
 
10
- transition: transform 200ms ease;
11
- &:has(.ion-activated) {
12
- transform: scale(1.016);
13
- }
15
+ .alert-head {
16
+ padding: 22px 30px 7px;
17
+ text-align: start;
18
+ }
14
19
 
15
- .alert-wrapper {
16
- @include api.glass-background-overlay;
17
- &:has(.ion-activated) {
18
- @include api.glass-background-overlay-activated;
20
+ .alert-title {
21
+ margin: 0;
22
+ font-size: 1rem;
23
+ line-height: 1.21;
24
+ font-weight: 600;
19
25
  }
20
- border-radius: 32px;
21
26
 
22
- .alert-head,
27
+ .alert-sub-title,
23
28
  .alert-message {
24
- text-align: left;
25
- margin-left: 14px;
26
- margin-right: 14px;
29
+ font-size: 0.94rem;
30
+ line-height: 1.19;
31
+ color: var(--ion-text-color-step-600, var(--ion-color-step-400, #999999));
27
32
  }
28
33
 
29
34
  .alert-message {
30
- font-size: 0.98rem;
31
- line-height: 1.2rem;
32
- color: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
35
+ padding: 0 30px 20px;
36
+ text-align: start;
37
+
38
+ &:empty {
39
+ padding: 0 0 12px;
40
+ }
33
41
  }
34
42
 
35
43
  .alert-radio-group {
@@ -41,26 +49,38 @@ ion-alert.ios:not(.ios-theme-disabled, .ios26-disabled) {
41
49
  }
42
50
 
43
51
  .alert-button-group {
44
- padding: 0 8px 12px;
45
- gap: 8px 0;
52
+ padding: 0 16px 16px;
53
+ gap: 8px;
46
54
  .alert-button {
47
55
  @include api.glass-background-overlay-button;
48
56
  border: none;
49
- border-radius: 32px;
50
- margin: 0 6px;
51
- min-width: calc(50% - 12px);
52
- // height: max(44px, 2.75rem);
53
- height: 48px;
57
+ border-radius: 24px;
58
+ margin: 0;
59
+ min-width: calc(50% - 4px);
60
+ min-height: 48px;
61
+ height: auto;
62
+ padding: 12px;
63
+ font-size: 1rem;
64
+ line-height: 1.19;
65
+ font-weight: 600;
66
+
67
+ .alert-button-inner {
68
+ min-height: 0;
69
+ height: auto;
70
+ }
71
+
72
+ &.alert-button-role-destructive {
73
+ color: var(--ios-theme-destructive-color, var(--ion-color-danger, #ea292d));
74
+ }
54
75
 
55
- &:not(.alert-button-role-destructive) {
76
+ &.alert-button-role-preferred {
77
+ @include api.glass-overlay-preferred-button;
78
+ }
79
+
80
+ &:not(.alert-button-role-destructive):not(.alert-button-role-preferred) {
56
81
  color: var(--ion-text-color, #000);
57
82
  }
58
- font-weight: 550;
59
83
  }
60
84
  }
61
-
62
- button.action-sheet-cancel {
63
- margin-top: 12px;
64
- }
65
85
  }
66
86
  }
@@ -1,4 +1,5 @@
1
1
  @use '../utils/api';
2
+ @use '../utils/glass';
2
3
 
3
4
  /**
4
5
  * Note: ion-back-button should not put inner ion-button.
@@ -13,7 +14,7 @@ $scaleup-large: 1.12;
13
14
  $scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
14
15
 
15
16
  @mixin theme-buttons {
16
- @include api.glass-background($shadow-opacity: 0.08);
17
+ @include api.glass-control-background;
17
18
  border-radius: 25px;
18
19
  overflow: hidden;
19
20
  &:not(:has(.ion-activated)):hover {
@@ -299,11 +300,7 @@ $scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
299
300
  --background: rgba(var(--ios-theme-glass-background-rgb, var(--ios26-glass-background-rgb)), 0.72);
300
301
  --background-hover: transparent;
301
302
  --background-activated: transparent;
302
- @if not $is-back-button {
303
- --box-shadow:
304
- inset 0 0 8px 0 rgba(var(--ios-theme-glass-box-shadow-color-rgb, var(--ios26-glass-box-shadow-color-rgb)), 0.2),
305
- 0 0.5px 1px 0 rgba(var(--ios-theme-glass-box-shadow-color-rgb, var(--ios26-glass-box-shadow-color-rgb)), 0.08);
306
- }
303
+ @include glass.light-shadow('--box-shadow');
307
304
 
308
305
  &:not(.button-has-icon-only):not(.back-button-has-icon-only) {
309
306
  &:not(.button-small):not(.button-large) {
@@ -312,7 +309,7 @@ $scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
312
309
  }
313
310
 
314
311
  &::part(native) {
315
- @include api.glass-background($include-background: false, $include-box-shadow: $is-back-button, $shadow-opacity: 0.08);
312
+ @include api.glass-control-background($include-background: false, $shadow: var(--box-shadow));
316
313
  }
317
314
 
318
315
  &:not(.button-disabled) {
@@ -1,7 +1,68 @@
1
- ion-card.ios:not(.ios-theme-disabled, .ios26-disabled) {
2
- border-radius: 24px;
1
+ @use '../utils/group-heading';
3
2
 
4
- ion-list ion-item {
3
+ ion-card.ios:not(.ios-theme-disabled, .ios26-disabled) {
4
+ ion-list:not(.ios-theme-disabled, .ios26-disabled) ion-item:not(.ios-theme-disabled, .ios26-disabled) {
5
5
  --padding-start: 0;
6
6
  }
7
+
8
+ // A standalone grouped surface, aligned with an inset list.
9
+ &:not(.ion-color) {
10
+ --background: var(--ion-card-background, var(--ion-item-background, var(--ion-background-color, #fff)));
11
+ --color: var(--ion-card-color, var(--ion-text-color, #000));
12
+ }
13
+ border-radius: 28px;
14
+ margin: 16px;
15
+ box-shadow: none;
16
+ font-size: 1rem;
17
+ line-height: 1.29;
18
+
19
+ > ion-card-header:not(.ios-theme-disabled, .ios26-disabled) {
20
+ flex-direction: column;
21
+ gap: 4px;
22
+ padding: 16px 20px;
23
+
24
+ &:has(+ ion-card-content:not(.ios-theme-disabled, .ios26-disabled)) {
25
+ padding-bottom: 4px;
26
+ }
27
+
28
+ > ion-card-title:not(.ios-theme-disabled, .ios26-disabled) {
29
+ @include group-heading.typography;
30
+ }
31
+
32
+ > ion-card-subtitle:not(.ios-theme-disabled, .ios26-disabled) {
33
+ margin: 0;
34
+ font-size: 1rem;
35
+ font-weight: 400;
36
+ line-height: 1.29;
37
+ letter-spacing: normal;
38
+ text-transform: none;
39
+ }
40
+ }
41
+
42
+ > ion-card-content:not(.ios-theme-disabled, .ios26-disabled) {
43
+ padding: 16px 20px;
44
+ font-size: 1rem;
45
+ line-height: 1.29;
46
+
47
+ p {
48
+ font-size: inherit;
49
+ }
50
+
51
+ > ion-list:not(.ios-theme-disabled, .ios26-disabled) {
52
+ background: transparent;
53
+
54
+ ion-item:not(.ios-theme-disabled, .ios26-disabled):not(.ion-color) {
55
+ --background: transparent;
56
+ }
57
+ }
58
+ }
59
+
60
+ > ion-card-header:not(.ios-theme-disabled, .ios26-disabled) + ion-card-content:not(.ios-theme-disabled, .ios26-disabled) {
61
+ padding-top: 0;
62
+ }
63
+ &:not(.ion-color)
64
+ > ion-card-header:not(.ios-theme-disabled, .ios26-disabled)
65
+ > ion-card-subtitle:not(.ios-theme-disabled, .ios26-disabled):not(.ion-color) {
66
+ --color: rgba(var(--ion-color-medium-rgb, 146, 148, 156), 0.9);
67
+ }
7
68
  }
@@ -1,4 +1,5 @@
1
1
  @use '../utils/api';
2
+ @use '../utils/glass';
2
3
 
3
4
  $scaleup-small-icon-only: api.$button-scaleup-small-icon-only;
4
5
  $scaleup-default-icon-only: api.$button-scaleup-default-icon-only;
@@ -12,9 +13,7 @@ ion-fab.ios:not(.ios-theme-disabled, .ios26-disabled) {
12
13
 
13
14
  ion-fab-button {
14
15
  --background: rgba(var(--ios-theme-glass-background-rgb, var(--ios26-glass-background-rgb)), 0.72);
15
- --box-shadow:
16
- inset 0 0 8px 0 rgba(var(--ios-theme-glass-box-shadow-color-rgb, var(--ios26-glass-box-shadow-color-rgb)), 0.2),
17
- 0 0.5px 1px 0 rgba(var(--ios-theme-glass-box-shadow-color-rgb, var(--ios26-glass-box-shadow-color-rgb)), 0.08);
16
+ @include glass.light-shadow('--box-shadow');
18
17
  --transition:
19
18
  var(--ios-theme-activated-transition-duration, var(--ios26-activated-transition-duration)) transform
20
19
  cubic-bezier(0.25, 1.11, 0.78, 1.59),
@@ -62,8 +61,7 @@ ion-fab.ios:not(.ios-theme-disabled, .ios26-disabled) {
62
61
  }
63
62
 
64
63
  &::part(native) {
65
- background-clip: border-box;
66
- @include api.glass-background($include-background: false, $include-box-shadow: false);
64
+ @include api.glass-control-background($include-background: false, $shadow: var(--box-shadow));
67
65
  }
68
66
  }
69
67