@rdlabo/ionic-theme-ios27 0.2.1 → 1.0.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.
@@ -25,7 +25,55 @@ interface SearchState {
25
25
  export const createSearchSupport = (doc: Document, id: (element: Element) => string, schedule: () => void) => {
26
26
  const states = new Map<NativeSearchBinding, SearchState>();
27
27
  let keyboardVisible = false;
28
+ let heldKeyboardResize: string | null = null;
29
+ let keyboardResizeGeneration = 0;
30
+ type KeyboardPlugin = {
31
+ setResizeMode?: (options: { mode: string }) => Promise<void>;
32
+ getResizeMode?: () => Promise<{ mode?: string }>;
33
+ };
34
+ const keyboardPlugin = (): KeyboardPlugin | undefined =>
35
+ (
36
+ doc.defaultView as Window & {
37
+ Capacitor?: { Plugins?: { Keyboard?: KeyboardPlugin } };
38
+ }
39
+ )?.Capacitor?.Plugins?.Keyboard;
28
40
  const projected = (binding: NativeSearchBinding) => binding.footer.hasAttribute(marker);
41
+ // Hold Cap resize at none while search is active so WebView shrink does not fight UISearchTab.
42
+ const holdKeyboardResize = () => {
43
+ const keyboard = keyboardPlugin();
44
+ if (!keyboard?.setResizeMode || heldKeyboardResize !== null) return;
45
+ const setResizeMode = keyboard.setResizeMode.bind(keyboard);
46
+ const generation = ++keyboardResizeGeneration;
47
+ heldKeyboardResize = 'native';
48
+ void (async () => {
49
+ let restoreMode = 'native';
50
+ try {
51
+ const current = await keyboard.getResizeMode?.();
52
+ if (typeof current?.mode === 'string') restoreMode = current.mode;
53
+ } catch {
54
+ /* keep default restore target */
55
+ }
56
+ if (generation !== keyboardResizeGeneration) return;
57
+ heldKeyboardResize = restoreMode;
58
+ try {
59
+ await setResizeMode({ mode: 'none' });
60
+ } catch {
61
+ if (generation === keyboardResizeGeneration) heldKeyboardResize = null;
62
+ return;
63
+ }
64
+ // Leave raced ahead of setResizeMode(none) — undo the stale none apply.
65
+ if (generation !== keyboardResizeGeneration) void setResizeMode({ mode: restoreMode }).catch(() => undefined);
66
+ })();
67
+ };
68
+ const hasActiveProjection = () => [...states.keys()].some((binding) => binding.active && projected(binding));
69
+ const releaseKeyboardResize = () => {
70
+ if (hasActiveProjection()) return;
71
+ keyboardResizeGeneration++;
72
+ const mode = heldKeyboardResize;
73
+ heldKeyboardResize = null;
74
+ if (!mode) return;
75
+ void keyboardPlugin()?.setResizeMode?.({ mode });
76
+ };
29
77
  const isCurrent = (state: SearchState) =>
30
78
  state.bar.isConnected &&
31
79
  state.binding.footer.querySelector('ion-searchbar') === state.bar &&
@@ -118,7 +166,10 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
118
166
  return {
119
167
  keyboard(visible: boolean) {
120
168
  keyboardVisible = visible;
121
- if (!visible) states.forEach((state) => (state.keyboardHidden = false));
169
+ if (!visible) {
170
+ states.forEach((state) => (state.keyboardHidden = false));
171
+ releaseKeyboardResize();
172
+ }
122
173
  schedule();
123
174
  },
124
175
  keepSearchTabsVisible() {
@@ -142,6 +193,13 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
142
193
  }
143
194
  }
144
195
  const inactive: [Candidate, ShellSearch][] = [];
196
+ const pageHidden = (element: HTMLElement) => !!element.closest('.ion-page-hidden, .ion-page-invisible');
197
+ const retainInactive = (candidate: Candidate, state?: SearchState, keepAvailable = false) => {
198
+ // Keep ShellSearchController across page transitions instead of demoting to UITabBar.
199
+ // While the searchable page is only blocked mid-transition (not yet hidden), keep
200
+ // available:true so native chrome does not swap to ordinary idleBar over album content.
201
+ if (state?.last) inactive.push([candidate, { ...state.last, active: false, available: keepAvailable, focused: false }]);
202
+ };
145
203
  for (const binding of bindings) {
146
204
  const candidate = candidates.find((c) => c.element === binding.tabBar);
147
205
  if (!candidate || candidate.control.search || binding.tabBar.getAttribute('slot') !== 'bottom') continue;
@@ -153,9 +211,37 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
153
211
  )
154
212
  continue;
155
213
  if (isShellDisabled(fab) || isShellDisabled(binding.footer)) continue;
156
- if (blocked(binding.footer) || !visible(binding.trigger) || binding.footer.closest(excluded)) {
157
- const state = states.get(binding);
158
- if (state?.last) inactive.push([candidate, { ...state.last, active: false, available: false, focused: false }]);
214
+ const pageUnavailable = blocked(binding.footer) || !!binding.footer.closest(excluded);
215
+ const existing = states.get(binding);
216
+ if (pageUnavailable) {
217
+ retainInactive(candidate, existing, !!existing?.last?.available && !pageHidden(binding.footer));
218
+ if (existing?.last) continue;
219
+ // First registration during a transition: fall through and measure available:false.
220
+ }
221
+ if (binding.active && existing && isCurrent(existing) && existing.last && !pageUnavailable) {
222
+ const back = binding.footer.querySelector<HTMLIonButtonElement>('ion-buttons[slot=start] ion-button');
223
+ // Close must stay eligible; otherwise fall through and demote instead of caching active.
224
+ if (back && !back.disabled && !back.closest(excluded)) {
225
+ candidate.control.search = {
226
+ ...existing.last,
227
+ active: true,
228
+ available: true,
229
+ focused: binding.focused,
230
+ value: existing.bar.value ?? '',
231
+ placeholder: existing.bar.placeholder ?? '',
232
+ disabled: existing.bar.disabled,
233
+ editSequence: existing.editSequence,
234
+ valueVersion: existing.valueVersion,
235
+ };
236
+ existing.last = candidate.control.search;
237
+ candidate.sources = [binding.tabBar, binding.trigger.closest<HTMLElement>('ion-fab') ?? binding.trigger, binding.footer];
238
+ candidate.actions.set(existing.last.trigger.id, binding.trigger);
239
+ candidate.actions.set(existing.last.closeId, back);
240
+ continue;
241
+ }
242
+ }
243
+ if (!visible(binding.trigger) && !pageUnavailable) {
244
+ retainInactive(candidate, existing);
159
245
  continue;
160
246
  }
161
247
  const bar = binding.footer.querySelector<HTMLIonSearchbarElement>('ion-searchbar');
@@ -168,11 +254,13 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
168
254
  !binding.footer.matches('ion-footer') ||
169
255
  !inFixedToolbar(bar) ||
170
256
  !inFixedToolbar(back) ||
171
- bar.closest(excluded) ||
257
+ (!pageUnavailable && bar.closest(excluded)) ||
172
258
  !bar.classList.contains('ios') ||
173
259
  bar.classList.contains('searchbar-classic')
174
- )
260
+ ) {
261
+ retainInactive(candidate, existing);
175
262
  continue;
263
+ }
176
264
  if (
177
265
  bar.color ||
178
266
  back.fill !== 'default' ||
@@ -187,14 +275,19 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
187
275
  bar.spellcheck ||
188
276
  bar.showClearButton !== 'always' ||
189
277
  bar.clearIcon
190
- )
278
+ ) {
279
+ retainInactive(candidate, existing);
191
280
  continue;
192
- if (bar.showCancelButton !== 'never' || bar.type !== 'search' || !getComputedStyle(input).backdropFilter.includes('blur')) continue;
281
+ }
282
+ if (bar.showCancelButton !== 'never' || bar.type !== 'search' || !getComputedStyle(input).backdropFilter.includes('blur')) {
283
+ retainInactive(candidate, existing);
284
+ continue;
285
+ }
193
286
  const fabButton = binding.trigger as HTMLIonFabButtonElement;
194
287
  const nativeTrigger = fabButton.shadowRoot?.querySelector<HTMLElement>('[part=native]');
195
288
  if (
196
- binding.trigger.closest(excluded) ||
197
- back.closest(excluded) ||
289
+ (!pageUnavailable && binding.trigger.closest(excluded)) ||
290
+ (!pageUnavailable && back.closest(excluded)) ||
198
291
  back.disabled ||
199
292
  fabButton.disabled ||
200
293
  fabButton.color ||
@@ -203,15 +296,20 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
203
296
  !nativeTrigger ||
204
297
  getComputedStyle(fabButton).backgroundColor !== 'rgba(0, 0, 0, 0)' ||
205
298
  !getComputedStyle(nativeTrigger).backdropFilter.includes('blur')
206
- )
299
+ ) {
300
+ retainInactive(candidate, existing);
207
301
  continue;
302
+ }
303
+ const state = existing ?? install(binding, bar, input);
208
304
  const trigger = readIcon(binding.trigger, binding.trigger, candidate);
209
305
  const searchIcon = bar.querySelector<HTMLElement>('.searchbar-search-icon');
210
306
  const field = searchIcon && readIcon(searchIcon, bar, candidate);
211
- if (!trigger || !field) continue;
307
+ if (!trigger || !field) {
308
+ retainInactive(candidate, state);
309
+ continue;
310
+ }
212
311
  field.label = '';
213
312
  field.accessibilityLabel = bar.getAttribute('aria-label') ?? input.getAttribute('aria-label') ?? 'Search';
214
- const state = states.get(binding) ?? install(binding, bar, input);
215
313
  state.layout = JSON.stringify([
216
314
  innerWidth,
217
315
  innerHeight,
@@ -231,15 +329,19 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
231
329
  bar.placeholder,
232
330
  bar.disabled,
233
331
  ]);
234
- if (state.rejectedLayout === state.layout) continue;
332
+ if (state.rejectedLayout === state.layout) {
333
+ retainInactive(candidate, state);
334
+ continue;
335
+ }
336
+ const available = !pageUnavailable && visible(binding.trigger);
235
337
  candidate.control.search = {
236
338
  id: id(input),
237
339
  field,
238
340
  trigger,
239
341
  closeId: id(back),
240
- active: binding.active,
241
- available: true,
242
- focused: binding.focused,
342
+ active: available && binding.active,
343
+ available,
344
+ focused: available && binding.focused,
243
345
  value: bar.value ?? '',
244
346
  placeholder: bar.placeholder ?? '',
245
347
  disabled: bar.disabled,
@@ -247,19 +349,36 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
247
349
  valueVersion: state.valueVersion,
248
350
  };
249
351
  state.last = candidate.control.search;
250
- candidate.sources = [binding.tabBar, binding.trigger.closest<HTMLElement>('ion-fab') ?? binding.trigger, binding.footer];
251
- candidate.actions.set(id(binding.trigger), binding.trigger);
252
- candidate.actions.set(id(back), back);
352
+ if (available) {
353
+ candidate.sources = [binding.tabBar, binding.trigger.closest<HTMLElement>('ion-fab') ?? binding.trigger, binding.footer];
354
+ candidate.actions.set(id(binding.trigger), binding.trigger);
355
+ candidate.actions.set(id(back), back);
356
+ }
253
357
  }
254
358
  for (const [candidate, configuration] of inactive) candidate.control.search ??= configuration;
255
359
  const groups = candidates.filter((c) => c.control.search);
256
360
  return candidates.filter((c) => !groups.some((group) => group !== c && group.sources?.some((source) => source.contains(c.element))));
257
361
  },
258
362
  projected,
363
+ hasActive() {
364
+ for (const [binding] of states) if (binding.active && projected(binding)) return true;
365
+ return false;
366
+ },
259
367
  begin(binding: NativeSearchBinding, revision: number) {
260
368
  const state = states.get(binding);
261
369
  if (state) state.minimumRevision = revision;
262
370
  },
371
+ /** Enter / focus / leave. `focused` is ignored when leaving. */
372
+ setStatus(binding: NativeSearchBinding, active: boolean, focused = false) {
373
+ if (!active) {
374
+ this.retire(binding);
375
+ return;
376
+ }
377
+ binding.active = true;
378
+ // Projection intent only. Ionic focus/blur events still wait for the native phase.
379
+ binding.focused = focused;
380
+ holdKeyboardResize();
381
+ },
263
382
  release(element: HTMLElement) {
264
383
  for (const binding of states.keys()) if (binding.footer === element) this.retire(binding);
265
384
  },
@@ -267,8 +386,7 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
267
386
  states.forEach((state) => {
268
387
  if (ids.includes(id(state.binding.tabBar))) {
269
388
  state.rejectedLayout = state.layout;
270
- // A rejected search surface has been removed by UIKit. Do not reuse its
271
- // cached configuration on an inactive page; project ordinary tabs instead.
389
+ // UIKit removed the search surface — do not reuse its cache; project ordinary tabs.
272
390
  state.last = undefined;
273
391
  }
274
392
  });
@@ -319,6 +437,7 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
319
437
  state.input.dispatchEvent(new FocusEvent('blur'));
320
438
  }
321
439
  binding.active = binding.focused = false;
440
+ releaseKeyboardResize();
322
441
  },
323
442
  destroy() {
324
443
  states.forEach((state) => {
@@ -326,6 +445,7 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
326
445
  state.restore();
327
446
  });
328
447
  states.clear();
448
+ releaseKeyboardResize();
329
449
  },
330
450
  };
331
451
  };
@@ -148,8 +148,8 @@ export const createRuntime = async (
148
148
  search.keepSearchTabsVisible();
149
149
  for (const page of pages) if (!page.isConnected) pages.delete(page);
150
150
  for (const surface of moving.keys()) if (!surface.isConnected) moving.delete(surface);
151
- if (doc.hidden || overlayOpen() || (win.visualViewport && (win.visualViewport.scale !== 1 || win.visualViewport.offsetTop !== 0)))
152
- return [];
151
+ if (doc.hidden || overlayOpen()) return [];
152
+ if (win.visualViewport && (win.visualViewport.scale !== 1 || win.visualViewport.offsetTop !== 0) && !search.hasActive()) return [];
153
153
  return unprojected(sources.keys(), () =>
154
154
  search
155
155
  .decorate(
@@ -246,6 +246,7 @@ export const createRuntime = async (
246
246
  }
247
247
  if (result.rejectedSearches?.length) {
248
248
  search.reject(result.rejectedSearches);
249
+ lastSnapshot = '';
249
250
  dirty = true;
250
251
  }
251
252
  for (const candidate of candidates) {
@@ -496,10 +497,14 @@ export const createRuntime = async (
496
497
  setNativeUIShellIntegration(doc, {
497
498
  async search(binding, active, focus) {
498
499
  if (!getNativeSearchBindings(doc).includes(binding) || !search.projected(binding)) return false;
499
- if (active && !binding.active) search.begin(binding, revision + 1);
500
- binding.active = active;
501
- binding.focused = focus ?? (active && binding.focused);
502
- if (!active) search.retire(binding);
500
+ if (!active) {
501
+ search.setStatus(binding, false);
502
+ } else {
503
+ const entering = !binding.active;
504
+ if (entering) search.begin(binding, revision + 1);
505
+ const nextFocused = focus === true ? true : focus === false ? false : entering ? false : binding.focused;
506
+ search.setStatus(binding, true, nextFocused);
507
+ }
503
508
  await flush();
504
509
  return !stopped && search.projected(binding);
505
510
  },
@@ -1,4 +1,4 @@
1
- @use 'pkg:@rdlabo/ionic-theme-utils/styles/structured-list';
1
+ @use '../utils/structured-list';
2
2
 
3
3
  ion-list.ios:not(.ios-theme-disabled, .ios26-disabled) {
4
4
  --ion-border-color: var(--ion-color-step-100, var(--ion-background-color-step-100, #e8e8e8));
@@ -1,4 +1,4 @@
1
- @use 'pkg:@rdlabo/ionic-theme-utils/styles/structured-list';
1
+ @use './utils/structured-list';
2
2
 
3
3
  ion-list.md {
4
4
  @include structured-list.support-text;
@@ -1,4 +1,4 @@
1
- @use 'pkg:@rdlabo/ionic-theme-utils/styles/structured-list';
1
+ @use '../structured-list';
2
2
 
3
3
  @mixin ion-list {
4
4
  ion-list.ios:not(.ios-theme-disabled, .ios26-disabled) {
@@ -0,0 +1,310 @@
1
+ $groups: ion-item-group, ion-reorder-group, ion-accordion-group, ion-radio-group;
2
+
3
+ @function _disabled-suffix($disabled-selector) {
4
+ @if $disabled-selector {
5
+ @return ':not(#{$disabled-selector})';
6
+ }
7
+
8
+ @return '';
9
+ }
10
+
11
+ // Derive neutral list colors from the application's Ionic medium palette.
12
+ @mixin colors($supporting-opacity, $detail-opacity) {
13
+ ion-item {
14
+ --detail-icon-color: rgb(var(--ion-color-medium-rgb));
15
+ --detail-icon-opacity: #{$detail-opacity};
16
+ }
17
+
18
+ &.list-inset {
19
+ ion-list-header ion-label:not(.ion-color) {
20
+ color: rgba(var(--ion-color-medium-rgb), $supporting-opacity);
21
+ }
22
+
23
+ ion-list-header ion-note:not(.ion-color),
24
+ > ion-note:not(.ion-color) {
25
+ --color: rgba(var(--ion-color-medium-rgb), #{$supporting-opacity});
26
+ }
27
+ }
28
+ }
29
+
30
+ @mixin support-text($disabled-selector: null) {
31
+ $d: _disabled-suffix($disabled-selector);
32
+
33
+ ion-item#{$d} {
34
+ &:has(ion-input.input-fill-outline),
35
+ &:has(ion-textarea.textarea-fill-outline),
36
+ &:has(ion-select:is([helpertext]:not([helpertext='']), [errortext]:not([errortext='']))),
37
+ &:has(ion-input .input-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty))),
38
+ &:has(ion-textarea .textarea-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty))) {
39
+ --inner-border-width: 0;
40
+ --inner-padding-bottom: 4px;
41
+ }
42
+
43
+ ion-input#{$d} .input-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty)))),
44
+ ion-textarea#{$d} .textarea-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty)))) {
45
+ display: none;
46
+ }
47
+ }
48
+ }
49
+
50
+ @mixin layout($outer-note-inset, $group-border-radius, $variant: classic, $disabled-selector: null) {
51
+ $d: _disabled-suffix($disabled-selector);
52
+ $is-ios27: $variant == ios27;
53
+
54
+ background: transparent;
55
+
56
+ ion-list-header#{$d} {
57
+ display: flex;
58
+ align-items: center;
59
+
60
+ ion-label#{$d} {
61
+ @if $is-ios27 {
62
+ font-size: 1rem;
63
+ line-height: 1.29rem;
64
+ font-weight: 600;
65
+ } @else {
66
+ font-size: 0.9rem;
67
+ }
68
+ margin: 8px 4px 4px;
69
+
70
+ &:not(.ion-color) {
71
+ color: var(--ion-color-medium-tint);
72
+ }
73
+ }
74
+
75
+ ion-button#{$d} {
76
+ margin-top: 8px;
77
+ }
78
+
79
+ ion-note#{$d} {
80
+ @if $is-ios27 {
81
+ font-size: 0.76rem;
82
+ line-height: 1.06rem;
83
+ font-weight: 400;
84
+ margin-block: 8px 4px;
85
+ margin-inline: 0 16px;
86
+
87
+ &:not(.ion-color) {
88
+ --color: var(--ion-color-medium-tint);
89
+ }
90
+ } @else {
91
+ font-size: 0.9rem;
92
+ margin-block: 8px 4px;
93
+ margin-inline: 0 16px;
94
+ }
95
+ }
96
+ }
97
+
98
+ @if $is-ios27 {
99
+ ion-item-group.item-group-header#{$d} {
100
+ border-radius: $group-border-radius;
101
+
102
+ > ion-item#{$d} {
103
+ --inner-padding-end: 0;
104
+ --inner-border-width: 0;
105
+
106
+ &::part(native) {
107
+ padding-inline: 0;
108
+ }
109
+ }
110
+
111
+ & > ion-item#{$d} > ion-label#{$d} {
112
+ width: 100%;
113
+ margin: 0;
114
+ text-align: start;
115
+ white-space: normal;
116
+ padding: 16px 20px;
117
+
118
+ /**
119
+ * ion-header.header-collapse-condenseを使って、スクロールによってHeaderを変化させるトリック
120
+ */
121
+ ion-header.header-collapse-condense > ion-toolbar {
122
+ --min-height: 0;
123
+ visibility: hidden;
124
+ }
125
+
126
+ ion-icon#{$d} {
127
+ font-size: 2.82rem;
128
+ border-radius: 20%;
129
+ padding: 6px;
130
+ vertical-align: top;
131
+
132
+ &:not(.ion-color) {
133
+ color: #ffffff;
134
+ }
135
+ }
136
+
137
+ h2 {
138
+ font-size: 1.29rem;
139
+ font-weight: 700;
140
+ line-height: 1.53rem;
141
+ margin: 20px 0 8px;
142
+ }
143
+
144
+ ion-text#{$d} {
145
+ display: block;
146
+ font-size: 1rem;
147
+ line-height: 1.29rem;
148
+
149
+ &:not(.ion-color) {
150
+ color: var(--ion-color-step-500, var(--ion-text-color-step-500, #808080));
151
+ }
152
+ }
153
+ }
154
+ }
155
+ } @else {
156
+ ion-item-group.item-group-header#{$d} {
157
+ & > ion-item#{$d} > ion-label#{$d} {
158
+ width: 100%;
159
+ text-align: center;
160
+ padding: 32px 16px 16px;
161
+ transform: translateX(5px);
162
+
163
+ &:dir(rtl) {
164
+ transform: translateX(-5px);
165
+ }
166
+
167
+ /**
168
+ * ion-header.header-collapse-condenseを使って、スクロールによってHeaderを変化させるトリック
169
+ */
170
+ ion-header.header-collapse-condense > ion-toolbar {
171
+ --min-height: 0;
172
+ visibility: hidden;
173
+ }
174
+
175
+ ion-icon#{$d} {
176
+ font-size: 2.8rem;
177
+ border-radius: 20%;
178
+ padding: 6px;
179
+
180
+ &:not(.ion-color) {
181
+ color: #ffffff;
182
+ }
183
+ }
184
+
185
+ h2 {
186
+ font-size: 1.2rem;
187
+ font-weight: bold;
188
+ margin-top: 8px;
189
+ }
190
+
191
+ ion-text#{$d} {
192
+ font-size: 0.9rem;
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ > :is(#{$groups})#{$d} {
199
+ display: block;
200
+ border-radius: $group-border-radius;
201
+ overflow: hidden;
202
+
203
+ &:has(> .radio-group-top) {
204
+ display: flex;
205
+ flex-direction: column;
206
+ border-radius: 0;
207
+ overflow: visible;
208
+
209
+ > .radio-group-top {
210
+ order: 1;
211
+ font-size: 0.9rem;
212
+ padding-block-start: 8px;
213
+ }
214
+
215
+ > ion-item#{$d}:first-of-type {
216
+ --border-radius: #{$group-border-radius} #{$group-border-radius} 0 0;
217
+ }
218
+
219
+ > ion-item#{$d}:last-of-type {
220
+ --border-radius: 0 0 #{$group-border-radius} #{$group-border-radius};
221
+ }
222
+ }
223
+
224
+ &:not(:first-of-type) {
225
+ margin-top: 16px;
226
+ }
227
+
228
+ ion-item#{$d} {
229
+ &:has(> ion-label:not([slot])):has(> ion-note:not([slot])) {
230
+ &::part(container) {
231
+ flex-direction: column;
232
+ justify-content: center;
233
+ padding-top: 14px;
234
+ padding-bottom: 14px;
235
+ min-height: 64px;
236
+ }
237
+ & > ion-label#{$d},
238
+ & > ion-note#{$d} {
239
+ width: auto;
240
+ align-self: flex-start;
241
+ }
242
+ & > ion-label#{$d} {
243
+ margin: 0;
244
+ font-weight: 500;
245
+ @if $is-ios27 {
246
+ font-size: 1rem;
247
+ line-height: 1.29rem;
248
+ } @else {
249
+ font-size: 1.15rem;
250
+ line-height: 0.9rem;
251
+ }
252
+ }
253
+ & > ion-note#{$d} {
254
+ @if $is-ios27 {
255
+ font-size: 0.88rem;
256
+ line-height: 1.18rem;
257
+ } @else {
258
+ font-size: 0.85rem;
259
+ }
260
+ }
261
+ }
262
+
263
+ ion-text[slot='end']#{$d} {
264
+ padding-inline-start: 8px;
265
+ }
266
+
267
+ &.item-disabled {
268
+ --detail-icon-opacity: 0.1;
269
+ opacity: 1;
270
+
271
+ & > * {
272
+ opacity: 0.4;
273
+ }
274
+ }
275
+
276
+ ion-button[slot='end']#{$d} {
277
+ &.ion-align-self-end {
278
+ transform: translateY(-7px);
279
+ }
280
+ ion-icon[slot='icon-only']#{$d} {
281
+ font-size: 1.2rem;
282
+ transform: translateY(4px);
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ & > ion-note#{$d} {
289
+ margin-block: 8px;
290
+ margin-inline: calc(var(--ion-safe-area-left, 0) + $outer-note-inset) calc(var(--ion-safe-area-right, 0) + $outer-note-inset);
291
+
292
+ &:dir(rtl) {
293
+ margin-inline: calc(var(--ion-safe-area-right, 0) + $outer-note-inset) calc(var(--ion-safe-area-left, 0) + $outer-note-inset);
294
+ }
295
+
296
+ @if $is-ios27 {
297
+ font-size: 0.76rem;
298
+ line-height: 1.06rem;
299
+ font-weight: 400;
300
+ display: block;
301
+ &:not(.ion-color) {
302
+ --color: var(--ion-color-medium-tint);
303
+ }
304
+ } @else {
305
+ --color: var(--ion-color-medium-tint);
306
+ font-size: 0.9rem;
307
+ display: block;
308
+ }
309
+ }
310
+ }