@rdlabo/ionic-theme-ios26 0.3.2 → 0.3.4

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.
@@ -1,52 +1,63 @@
1
1
  @use '../utils/api';
2
2
 
3
- ion-toolbar.ios:not(.ios26-disabled) {
4
- ion-segment {
5
- @include api.glass-background;
6
- min-height: 48px;
7
- border-radius: 25px;
8
-
9
- transition: transform var(--ios26-activated-transition-duration) ease-out;
10
- will-change: transform;
11
-
12
- &.in-toolbar-color:not(.in-segment-color) {
13
- ion-segment-button:not(.segment-button-checked)::part(native) {
14
- color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 1) !important;
15
- }
3
+ ion-segment.ios:not(.ios26-disabled) {
4
+ @include api.glass-background;
5
+ min-height: 48px;
6
+ border-radius: 25px;
7
+
8
+ &.segment-expand {
9
+ min-height: 24px;
10
+ width: calc(100% - var(--ion-safe-area-left, 0) - var(--ion-safe-area-left, 0) - 24px);
11
+
12
+ &.segment-activated {
13
+ transform: scale(1);
16
14
  }
17
15
 
18
16
  ion-segment-button {
19
- --border-width: 0;
20
- --ion-color-base: var(--ion-text-color, #000);
21
- --padding-start: 8px;
22
- --padding-end: 8px;
23
- min-width: 60px;
24
- margin: 3px 2px;
25
- font-size: 14.5px;
17
+ min-height: 24px;
18
+ }
19
+ }
26
20
 
27
- &::part(indicator-background) {
28
- border-radius: 25px;
29
- box-shadow: none;
30
- transition: background 0.2s ease;
31
- background: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.06);
32
- }
21
+ transition: transform var(--ios26-activated-transition-duration) ease-out;
22
+ will-change: transform;
23
+
24
+ &.in-toolbar-color:not(.in-segment-color) {
25
+ ion-segment-button:not(.segment-button-checked)::part(native) {
26
+ color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 1) !important;
33
27
  }
28
+ }
34
29
 
35
- &.segment-activated {
36
- transform: scale(1.1);
37
-
38
- ion-segment-button {
39
- transition: transform 100ms ease-out;
40
- &.segment-button-checked::part(native) {
41
- transform: scale(1.08);
42
- }
43
- &::part(indicator-background) {
44
- position: relative;
45
- z-index: 1;
46
- background: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0);
47
- transform: scale(1.1);
48
- transform-origin: center center;
49
- }
30
+ ion-segment-button {
31
+ --border-width: 0;
32
+ --ion-color-base: var(--ion-text-color, #000);
33
+ --padding-start: 8px;
34
+ --padding-end: 8px;
35
+ min-width: 60px;
36
+ margin: 3px 2px;
37
+ font-size: 14.5px;
38
+
39
+ &::part(indicator-background) {
40
+ border-radius: 25px;
41
+ box-shadow: none;
42
+ transition: background 0.2s ease;
43
+ background: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.06);
44
+ }
45
+ }
46
+
47
+ &.segment-activated {
48
+ transform: scale(1.1);
49
+
50
+ ion-segment-button {
51
+ transition: transform 100ms ease-out;
52
+ &.segment-button-checked::part(native) {
53
+ transform: scale(1.08);
54
+ }
55
+ &::part(indicator-background) {
56
+ position: relative;
57
+ z-index: 1;
58
+ background: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0);
59
+ transform: scale(1.1);
60
+ transform-origin: center center;
50
61
  }
51
62
  }
52
63
  }
@@ -43,44 +43,75 @@ ion-tab-bar.ios:not(.ios26-disabled) {
43
43
  }
44
44
  }
45
45
 
46
- ion-tab-button {
47
- ion-icon {
48
- font-size: 26px;
46
+ /**
47
+ * effectが有効な場合、effectでスタイリングするため不要
48
+ */
49
+ &.tab-bar-ios26-effect:has(ion-tab-button.ion-activated) {
50
+ ion-tab-button.tab-selected::part(native) {
51
+ background: rgba(var(--ios26-button-color-selected-rgb), 0);
52
+ }
53
+ ion-tab-button.ion-activated {
54
+ ion-label,
55
+ ion-icon {
56
+ filter: brightness(100%);
57
+ }
49
58
  }
59
+ }
60
+ }
61
+
62
+ ion-tab-button.ios:not(.ios26-disabled) {
63
+ ion-icon {
64
+ font-size: 26px;
65
+ }
66
+ ion-label,
67
+ ion-icon {
68
+ transition:
69
+ filter var(--ios26-activated-transition-duration) ease,
70
+ color var(--ios26-activated-transition-duration) ease;
71
+ }
72
+ background: rgba(var(--ios26-glass-background-rgb), 0);
73
+ height: auto;
74
+ transition: transform var(--ios26-activated-transition-duration) ease;
75
+ &::part(native) {
76
+ overflow: visible;
77
+ min-height: 56px;
78
+ border-radius: 32px;
79
+ }
80
+
81
+ &.ion-activated {
82
+ transform: scale(1.1);
83
+ position: relative;
84
+ color: var(--color-selected);
50
85
  ion-label,
51
86
  ion-icon {
52
- transition:
53
- filter var(--ios26-activated-transition-duration) ease,
54
- color var(--ios26-activated-transition-duration) ease;
87
+ filter: brightness(104%);
55
88
  }
56
- background: rgba(var(--ios26-glass-background-rgb), 0);
57
- height: auto;
58
- transition: transform var(--ios26-activated-transition-duration) ease;
89
+ }
90
+
91
+ &.tab-selected {
59
92
  &::part(native) {
60
- overflow: visible;
61
- min-height: 56px;
62
- border-radius: 32px;
93
+ background: rgba(var(--ios26-button-color-selected-rgb), 0.095);
94
+ transition: background var(--ios26-activated-transition-duration) ease;
63
95
  }
64
96
 
65
- &.ion-activated {
66
- transform: scale(1.1);
67
- position: relative;
68
- color: var(--color-selected);
69
- ion-label,
70
- ion-icon {
71
- filter: brightness(104%);
72
- }
97
+ &.ion-activated::part(native) {
98
+ background: rgba(var(--ios26-glass-box-shadow-color-rgb), 0.02);
73
99
  }
100
+ }
74
101
 
75
- &.tab-selected {
76
- &::part(native) {
77
- background: rgba(var(--ios26-button-color-selected-rgb), 0.095);
78
- transition: background var(--ios26-activated-transition-duration) ease;
79
- }
80
-
81
- &.ion-activated::part(native) {
82
- background: rgba(var(--ios26-glass-box-shadow-color-rgb), 0.02);
83
- }
102
+ &.ion-cloned-element {
103
+ &::part(native) {
104
+ @include api.glass-background(1, 2px, 104%);
105
+ background: transparent;
106
+ }
107
+ color: var(--color-selected, var(--ion-color-primary, #0054e9));
108
+ pointer-events: none;
109
+ position: absolute;
110
+ left: 0;
111
+ top: 0;
112
+ transform-origin: center;
113
+ & > * {
114
+ visibility: hidden;
84
115
  }
85
116
  }
86
117
  }
@@ -21,7 +21,7 @@ ion-header.ios.header-collapse-condense:not(.ios26-disabled) ion-toolbar:not(.io
21
21
  padding-top: 0;
22
22
  }
23
23
 
24
- ion-toolbar.ios:not(.toolbar-title-large):not(.ios26-disabled) {
24
+ ion-toolbar.ios:not(.toolbar-title-large):not(.ios26-disabled):not(:has(ion-segment.segment-expand)) {
25
25
  --min-height: 68px;
26
26
  }
27
27
 
package/src/index.ts ADDED
@@ -0,0 +1,244 @@
1
+ import { createGesture, GestureDetail, createAnimation } from '@ionic/core';
2
+ import type { Animation } from '@ionic/core/dist/types/utils/animation/animation-interface';
3
+ import { Gesture } from '@ionic/core/dist/types/utils/gesture';
4
+
5
+ export const registerTabBarEffect = (ionTabBar: HTMLElement): Gesture => {
6
+ let gesture!: Gesture;
7
+ let currentTouchedButton: HTMLIonTabButtonElement | null;
8
+ let gestureMoveStartTime: number | null;
9
+ let tabEffectElY: number | null;
10
+
11
+ const tabEffectEl = cloneElement('ion-tab-button');
12
+ const GestureName = 'tab-bar-gesture';
13
+ const MinScale = 'scale(1.1)';
14
+ const MiddleScale = 'scale(1.2)';
15
+ const MaxScale = 'scale(1.3)';
16
+ const OverScale = 'scale(1.4)';
17
+
18
+ ionTabBar.addEventListener('pointerdown', () => clearActivated());
19
+
20
+ const createTabButtonGesture = () => {
21
+ ionTabBar.classList.add('tab-bar-ios26-effect');
22
+ gesture = createGesture({
23
+ el: ionTabBar,
24
+ threshold: 0,
25
+ gestureName: GestureName,
26
+ onStart: (event) => {
27
+ enterTabButtonAnimation(event)?.play();
28
+ },
29
+ onMove: (event) => {
30
+ moveTabButtonAnimation(event)?.play();
31
+ },
32
+ onEnd: (event) => {
33
+ leaveTabButtonAnimation(event).then((animation) => animation?.play());
34
+ },
35
+ });
36
+ gesture.enable(true);
37
+ };
38
+ createTabButtonGesture();
39
+
40
+ const clearActivated = (isAfterAddWrite = false) => {
41
+ if (currentTouchedButton) {
42
+ tabEffectEl.style.display = 'none';
43
+ tabEffectEl.innerHTML = '';
44
+ tabEffectEl.style.top = 'auto';
45
+ tabEffectEl.style.top = 'left';
46
+ tabEffectEl.style.transform = 'none';
47
+
48
+ currentTouchedButton!.classList.remove('ion-activated');
49
+ if (isAfterAddWrite) {
50
+ currentTouchedButton.click();
51
+ }
52
+
53
+ /**
54
+ * もしこの処理がafterAddWrite以外で走る場合、正常に終了していない
55
+ */
56
+ if (!isAfterAddWrite) {
57
+ gesture.destroy();
58
+ createTabButtonGesture();
59
+ }
60
+ currentTouchedButton = null;
61
+ }
62
+ };
63
+
64
+ const getTransform = (detailCurrentX: number, tabSelectedActual: Element): string => {
65
+ const diff = -2;
66
+ const currentX = detailCurrentX - tabSelectedActual.clientWidth / 2;
67
+ const maxLeft = tabSelectedActual.getBoundingClientRect().left + diff;
68
+ const maxRight = tabSelectedActual.getBoundingClientRect().right - diff - tabSelectedActual.clientWidth;
69
+
70
+ if (maxLeft < currentX && currentX < maxRight) {
71
+ return `translate3d(${currentX}px, ${tabEffectElY}px, 0)`;
72
+ }
73
+ if (maxLeft > currentX) {
74
+ return `translate3d(${maxLeft}px, ${tabEffectElY}px, 0)`;
75
+ }
76
+ return `translate3d(${maxRight}px, ${tabEffectElY}px, 0)`;
77
+ };
78
+
79
+ const enterTabButtonAnimation = (detail: GestureDetail): Animation | undefined => {
80
+ currentTouchedButton = (detail.event.target as HTMLElement).closest('ion-tab-button');
81
+ const tabSelectedActual = ionTabBar.querySelector('ion-tab-button.tab-selected');
82
+ if (tabSelectedActual === null || currentTouchedButton === null) {
83
+ return undefined;
84
+ }
85
+
86
+ tabEffectElY = tabSelectedActual.getBoundingClientRect().top;
87
+ const startTransform = getTransform(
88
+ tabSelectedActual.getBoundingClientRect().left + tabSelectedActual.clientWidth / 2,
89
+ tabSelectedActual,
90
+ );
91
+ const middleTransform = getTransform(
92
+ (tabSelectedActual.getBoundingClientRect().left + tabSelectedActual.clientWidth / 2 + detail.currentX) / 2,
93
+ currentTouchedButton,
94
+ );
95
+ const endTransform = getTransform(detail.currentX, currentTouchedButton);
96
+ const tabButtonAnimation = createAnimation();
97
+ tabButtonAnimation
98
+ .addElement(tabEffectEl)
99
+ .delay(70)
100
+ .beforeStyles({
101
+ width: `${tabSelectedActual.clientWidth}px`,
102
+ height: `${tabSelectedActual.clientHeight}px`,
103
+ display: 'block',
104
+ })
105
+ .beforeAddWrite(() => {
106
+ tabSelectedActual.childNodes.forEach((node) => {
107
+ tabEffectEl.appendChild(node.cloneNode(true));
108
+ });
109
+ currentTouchedButton!.classList.add('ion-activated');
110
+ });
111
+
112
+ if (currentTouchedButton === tabSelectedActual) {
113
+ tabButtonAnimation
114
+ .keyframes([
115
+ {
116
+ transform: `${startTransform} ${MinScale}`,
117
+ opacity: 1,
118
+ offset: 0,
119
+ },
120
+ {
121
+ transform: `${middleTransform} ${MiddleScale}`,
122
+ opacity: 1,
123
+ offset: 0.6,
124
+ },
125
+ {
126
+ transform: `${endTransform} ${MaxScale}`,
127
+ opacity: 1,
128
+ offset: 1,
129
+ },
130
+ ])
131
+ .duration(120);
132
+ gestureMoveStartTime = detail.currentTime + 120;
133
+ } else {
134
+ tabButtonAnimation
135
+ .keyframes([
136
+ {
137
+ transform: `${startTransform} ${MinScale}`,
138
+ opacity: 1,
139
+ offset: 0,
140
+ },
141
+ {
142
+ transform: `${middleTransform} ${MiddleScale}`,
143
+ opacity: 1,
144
+ offset: 0.4,
145
+ },
146
+ {
147
+ transform: `${endTransform} ${MaxScale}`,
148
+ opacity: 1,
149
+ offset: 0.55,
150
+ },
151
+ {
152
+ transform: `${endTransform} ${OverScale}`,
153
+ opacity: 1,
154
+ offset: 0.75,
155
+ },
156
+ {
157
+ transform: `${endTransform} ${MaxScale}`,
158
+ opacity: 1,
159
+ offset: 1,
160
+ },
161
+ ])
162
+ .duration(480);
163
+ gestureMoveStartTime = detail.currentTime + 480;
164
+ }
165
+
166
+ return tabButtonAnimation;
167
+ };
168
+
169
+ const moveTabButtonAnimation = (detail: GestureDetail): Animation | undefined => {
170
+ if (gestureMoveStartTime) {
171
+ if (detail.currentTime < gestureMoveStartTime) {
172
+ return undefined;
173
+ }
174
+ }
175
+ const tabSelectedActual = ionTabBar.querySelector('ion-tab-button.tab-selected');
176
+ if (tabSelectedActual === null || currentTouchedButton === null) {
177
+ return undefined;
178
+ }
179
+
180
+ const transform = getTransform(detail.currentX, currentTouchedButton);
181
+
182
+ const tabButtonAnimation = createAnimation();
183
+ tabButtonAnimation.addElement(tabEffectEl);
184
+ tabButtonAnimation.duration(50).keyframes([
185
+ {
186
+ transform: `${transform} ${MaxScale}`,
187
+ },
188
+ {
189
+ transform: `${transform} ${MaxScale}`,
190
+ },
191
+ ]);
192
+ return tabButtonAnimation;
193
+ };
194
+
195
+ const leaveTabButtonAnimation = async (detail: GestureDetail): Promise<Animation | undefined> => {
196
+ if (gestureMoveStartTime) {
197
+ if (detail.currentTime < gestureMoveStartTime) {
198
+ await new Promise((resolve) => setTimeout(resolve, gestureMoveStartTime! - detail.currentTime));
199
+ }
200
+ }
201
+ const tabSelectedActual = ionTabBar.querySelector('ion-tab-button.tab-selected');
202
+ if (tabSelectedActual === null || currentTouchedButton === null) {
203
+ return undefined;
204
+ }
205
+
206
+ const endTransform = getTransform(
207
+ currentTouchedButton.getBoundingClientRect().left + currentTouchedButton.clientWidth / 2,
208
+ currentTouchedButton,
209
+ );
210
+
211
+ const tabButtonAnimation = createAnimation();
212
+ tabButtonAnimation.addElement(tabEffectEl);
213
+ tabButtonAnimation
214
+ .onFinish(() => clearActivated(true))
215
+ .duration(50)
216
+ .keyframes([
217
+ {
218
+ transform: `${endTransform} ${MaxScale}`,
219
+ opacity: 1,
220
+ },
221
+ {
222
+ transform: `${endTransform} ${MinScale}`,
223
+ opacity: 0,
224
+ },
225
+ ]);
226
+ return tabButtonAnimation;
227
+ };
228
+
229
+ return gesture;
230
+ };
231
+
232
+ const cloneElement = (tagName: string): HTMLElement => {
233
+ const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
234
+ if (getCachedEl !== null) {
235
+ return getCachedEl as HTMLElement;
236
+ }
237
+
238
+ const clonedEl = document.createElement(tagName) as HTMLElement;
239
+ clonedEl.classList.add('ion-cloned-element');
240
+ clonedEl.style.setProperty('display', 'none');
241
+ document.body.appendChild(clonedEl);
242
+
243
+ return clonedEl;
244
+ };