@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,32 +1,59 @@
1
1
  import { Capacitor, registerPlugin } from '@capacitor/core';
2
- import type { NativeUIShellHandle, NativeUIShellPlugin } from './definitions';
2
+ import { setConfig } from '../transition/ios.transition';
3
+ import type { NativeUIShellHandle, NativeUIShellOptions, NativeUIShellPlugin, WebViewMetrics } from './definitions';
4
+ import { bindMetricsLifecycle } from './lifecycle';
3
5
  import { createRuntime } from './runtime';
4
- export type { NativeUIShellComponent, NativeUIShellHandle, NativeUIShellStatus } from './definitions';
6
+ export type {
7
+ NativeUIShellComponent,
8
+ NativeUIShellControls,
9
+ NativeUIShellHandle,
10
+ NativeUIShellOptions,
11
+ NativeUIShellStatus,
12
+ NativeUIShellSuspension,
13
+ WebViewMetrics,
14
+ } from './definitions';
5
15
 
6
16
  const plugin = registerPlugin<NativeUIShellPlugin>('IonicNativeUIShell');
7
17
  let active: Promise<NativeUIShellHandle> | undefined;
8
18
  const web = (reason: string): NativeUIShellHandle => ({
9
19
  getStatus: () => ({ state: 'web', projected: 0, updates: 0, reason }),
20
+ suspend: async () => ({ resume: async () => {} }),
10
21
  destroy: async () => {},
11
22
  });
12
23
 
24
+ /** Reads the current native WebView geometry and applies it to page transitions. */
25
+ export const configureNativeTransition = async (): Promise<WebViewMetrics> => {
26
+ const metrics = typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' ? await plugin.getWebViewMetrics() : { radius: 0 };
27
+ setConfig({ radius: metrics.radius });
28
+ return metrics;
29
+ };
30
+
13
31
  /** Call once at application startup. Ionic markup remains the source of truth. */
14
- export const enableNativeUIShell = (): Promise<NativeUIShellHandle> => {
32
+ export const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise<NativeUIShellHandle> => {
33
+ if (options.enabled === false) {
34
+ const current = active;
35
+ active = undefined;
36
+ return current
37
+ ? current.then(async (handle) => {
38
+ await handle.destroy();
39
+ return web('Disabled');
40
+ })
41
+ : Promise.resolve(web('Disabled'));
42
+ }
15
43
  if (typeof document === 'undefined' || Capacitor.getPlatform() !== 'ios') return Promise.resolve(web('Requires Capacitor iOS'));
16
44
  return (active ??= (async () => {
17
45
  try {
46
+ await configureNativeTransition().catch(() => undefined);
18
47
  if (!(await plugin.configure()).supported) {
19
48
  active = undefined;
20
49
  return web('Requires iOS 26 or later');
21
50
  }
22
- const runtime = await createRuntime(document, plugin);
23
- return {
24
- getStatus: runtime.getStatus,
25
- async destroy() {
26
- await runtime.destroy();
27
- active = undefined;
28
- },
29
- };
51
+ const runtime = await createRuntime(document, plugin, options);
52
+ return await bindMetricsLifecycle(
53
+ runtime,
54
+ () => plugin.addListener('webViewMetricsChange', (metrics) => setConfig({ radius: metrics.radius })),
55
+ () => (active = undefined),
56
+ );
30
57
  } catch (error) {
31
58
  active = undefined;
32
59
  return web(error instanceof Error ? error.message : String(error));
@@ -0,0 +1,38 @@
1
+ import type { PluginListenerHandle } from '@capacitor/core';
2
+ import type { NativeUIShellHandle } from './definitions';
3
+
4
+ export const bindMetricsLifecycle = async (
5
+ runtime: NativeUIShellHandle,
6
+ listen: () => Promise<PluginListenerHandle>,
7
+ deactivate: () => void,
8
+ ): Promise<NativeUIShellHandle> => {
9
+ let listener: PluginListenerHandle;
10
+ try {
11
+ listener = await listen();
12
+ } catch (error) {
13
+ await runtime.destroy();
14
+ throw error;
15
+ }
16
+
17
+ let destroying: Promise<void> | undefined;
18
+ return {
19
+ getStatus: runtime.getStatus,
20
+ suspend: runtime.suspend,
21
+ destroy() {
22
+ return (destroying ??= (async () => {
23
+ let removalError: unknown;
24
+ try {
25
+ await listener.remove();
26
+ } catch (error) {
27
+ removalError = error;
28
+ }
29
+ try {
30
+ await runtime.destroy();
31
+ } finally {
32
+ deactivate();
33
+ }
34
+ if (removalError) throw removalError;
35
+ })());
36
+ },
37
+ };
38
+ };
@@ -2,7 +2,14 @@ import type { PluginListenerHandle } from '@capacitor/core';
2
2
  import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from '@ionic/core';
3
3
  import { getNativeSearchBindings, setNativeUIShellIntegration } from '../native-integration';
4
4
  import { createSearchSupport } from './components/searchable-tabs';
5
- import type { ShellActivation, ShellSnapshot, NativeUIShellHandle, NativeUIShellPlugin, NativeUIShellStatus } from './definitions';
5
+ import type {
6
+ ShellActivation,
7
+ ShellSnapshot,
8
+ NativeUIShellHandle,
9
+ NativeUIShellOptions,
10
+ NativeUIShellPlugin,
11
+ NativeUIShellStatus,
12
+ } from './definitions';
6
13
  import { readCandidate, selector, shadowSelector, motionSelector } from './components';
7
14
  import { marker, unprojected } from './shared/dom';
8
15
  import { createIconRenderer } from './shared/icons';
@@ -20,7 +27,11 @@ const bounded = <T>(promise: Promise<T>): Promise<T> =>
20
27
  promise.then(resolve, reject).finally(() => clearTimeout(timer));
21
28
  });
22
29
 
23
- export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin): Promise<NativeUIShellHandle> => {
30
+ export const createRuntime = async (
31
+ doc: Document,
32
+ plugin: NativeUIShellPlugin,
33
+ options: NativeUIShellOptions = {},
34
+ ): Promise<NativeUIShellHandle> => {
24
35
  const win = doc.defaultView!;
25
36
  const icons = createIconRenderer();
26
37
  const crossfade = createCrossfade(win);
@@ -30,6 +41,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
30
41
  const suspended = new Set<HTMLElement[]>();
31
42
  const pages = new Set<HTMLElement>();
32
43
  const presented = new Set<HTMLElement>();
44
+ const manualSuspensions = new Set<symbol>();
33
45
  const moving = new Map<HTMLElement, Set<string>>();
34
46
  const observed = new Set<Element | ShadowRoot>();
35
47
  const listeners = new AbortController();
@@ -52,6 +64,16 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
52
64
  let listener: PluginListenerHandle | undefined;
53
65
  let searchListener: PluginListenerHandle | undefined;
54
66
  let waiters: (() => void)[] = [];
67
+ const control = (kind: Candidate['control']['kind']): keyof NonNullable<NativeUIShellOptions['controls']> => {
68
+ if (kind === 'ion-tab-bar') return 'tabs';
69
+ if (kind === 'ion-segment') return 'segment';
70
+ if (kind === 'ion-fab') return 'fab';
71
+ return 'toolbar';
72
+ };
73
+ const controlEnabled = (candidate: Candidate) => {
74
+ const controls = options.controls;
75
+ return controls === undefined || controls[control(candidate.control.kind)] === true;
76
+ };
55
77
  const id = (element: Element) => {
56
78
  let value = ids.get(element);
57
79
  if (!value) {
@@ -91,6 +113,10 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
91
113
  };
92
114
  const search = createSearchSupport(doc, id, schedule);
93
115
  const candidateSources = (candidate: Candidate) => candidate.sources ?? [candidate.element];
116
+ const readEnabledCandidate = (element: HTMLElement): Candidate | undefined => {
117
+ const candidate = readCandidate(element, id);
118
+ return candidate && controlEnabled(candidate) ? candidate : undefined;
119
+ };
94
120
  const flush = async () => {
95
121
  if (stopped) return;
96
122
  schedule();
@@ -108,6 +134,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
108
134
  const overlayOpen = () => {
109
135
  for (const element of presented) if (!element.isConnected) presented.delete(element);
110
136
  return (
137
+ manualSuspensions.size > 0 ||
111
138
  presented.size > 0 ||
112
139
  Array.from(doc.querySelectorAll(overlays)).some(
113
140
  (element) => (element as Element & { presented?: boolean }).presented || element.classList.contains('show-menu'),
@@ -128,7 +155,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
128
155
  .decorate(
129
156
  Array.from(doc.querySelectorAll<HTMLElement>(selector))
130
157
  .filter((element) => !blocked(element))
131
- .map((element) => readCandidate(element, id))
158
+ .map(readEnabledCandidate)
132
159
  .filter((candidate): candidate is Candidate => !!candidate),
133
160
  blocked,
134
161
  )
@@ -375,6 +402,23 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
375
402
  updates,
376
403
  reason,
377
404
  }),
405
+ async suspend() {
406
+ const token = Symbol();
407
+ let resumed = false;
408
+ if (!stopped) {
409
+ manualSuspensions.add(token);
410
+ getNativeSearchBindings(doc).forEach((binding) => search.retire(binding));
411
+ await flush();
412
+ }
413
+ return {
414
+ async resume() {
415
+ if (resumed) return;
416
+ resumed = true;
417
+ if (stopped || !manualSuspensions.delete(token)) return;
418
+ await flush();
419
+ },
420
+ };
421
+ },
378
422
  async destroy() {
379
423
  if (stopped) return;
380
424
  stopped = true;
@@ -399,6 +443,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
399
443
  icons.clear();
400
444
  pages.clear();
401
445
  presented.clear();
446
+ manualSuspensions.clear();
402
447
  moving.clear();
403
448
  suspended.clear();
404
449
  await listener?.remove().catch(() => {});
@@ -427,7 +472,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
427
472
  if (!element) return;
428
473
  const owner = Array.from(sources.keys()).find((source) => source === element || source.contains(element));
429
474
  if (!owner) return;
430
- const direct = !blocked(owner) && unprojected(sources.keys(), () => readCandidate(owner, id));
475
+ const direct = !blocked(owner) && unprojected(sources.keys(), () => readEnabledCandidate(owner));
431
476
  const candidate = direct || read().find((candidate) => candidate.actions.has(event.id));
432
477
  const item = candidate?.control.items.find((item) => item.id === event.id);
433
478
  const searchAction =
@@ -13,6 +13,9 @@ $scaleup-default-icon-only: api.$button-scaleup-default-icon-only;
13
13
  $scaleup-large: 1.12;
14
14
  $scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
15
15
 
16
+ // Fit to the iOS 27 glass button's expansion: a small overshoot, settling at 380ms.
17
+ $press-transition: transform var(--ios-theme-button-press-duration, 380ms) cubic-bezier(0.37, 0.36, 0.03, 1.34);
18
+
16
19
  @mixin theme-buttons {
17
20
  @include api.glass-control-background;
18
21
  border-radius: 25px;
@@ -24,14 +27,17 @@ $scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
24
27
  /**
25
28
  * compare .header-collapse-main ion-toolbar.in-toolbar ion-title, .header-collapse-main ion-toolbar.in-toolbar ion-buttons
26
29
  */
27
- transition: transform var(--ios-theme-activated-transition-duration, var(--ios26-activated-transition-duration)) ease-out;
30
+ transition: $press-transition;
28
31
 
29
32
  z-index: 0;
30
33
 
31
34
  &:has(.ion-activated) {
32
35
  z-index: 5;
33
36
  @include api.glass-background-button-activated;
37
+ }
34
38
 
39
+ // Follow the actual press so a short tap reverses before reaching full scale.
40
+ &:has(ion-button:active:not(.button-disabled), ion-menu-button:active:not([disabled])) {
35
41
  // Hardware acceleration optimization for scale transforms
36
42
  transform: scale($scaleup-default-icon-only) translateZ(0);
37
43
  -webkit-transform: scale($scaleup-default-icon-only) translateZ(0);
@@ -141,7 +147,7 @@ $scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
141
147
  }
142
148
 
143
149
  transition:
144
- transform var(--ios-theme-activated-transition-duration, var(--ios26-activated-transition-duration)) ease-out,
150
+ $press-transition,
145
151
  color 50ms ease;
146
152
 
147
153
  &.in-toolbar:not(.ion-color):not(.in-toolbar-color) {
@@ -247,8 +253,10 @@ $scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
247
253
  inset 0 -0.5px 0 rgba(255, 255, 255, 0.48),
248
254
  0 0.5px 1px rgba(0, 0, 0, 0.12);
249
255
  }
250
- &.ion-activated.button-has-icon-only,
251
- &.ion-activated.back-button-has-icon-only {
256
+ }
257
+ &:active:not(.button-disabled) {
258
+ &.button-has-icon-only,
259
+ &.back-button-has-icon-only {
252
260
  transform: scale($scaleup-default-icon-only);
253
261
  &.button-small {
254
262
  transform: scale($scaleup-small-icon-only);
@@ -332,8 +340,10 @@ $scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
332
340
  &.ion-color {
333
341
  --color: rgba(var(--ion-color-base-rgb, var(--ion-text-color-rgb, 0, 0, 0)), 0.1);
334
342
  }
335
- &.ion-activated.button-has-icon-only,
336
- &.ion-activated.back-button-has-icon-only {
343
+ }
344
+ &:active:not(.button-disabled) {
345
+ &.button-has-icon-only,
346
+ &.back-button-has-icon-only {
337
347
  transform: scale($scaleup-default-icon-only);
338
348
  &.button-small {
339
349
  transform: scale($scaleup-small-icon-only);
@@ -401,17 +411,18 @@ ion-buttons.ios:not(.ios-theme-disabled, .ios26-disabled):not(
401
411
  ion-buttons.ios:not(.ios-theme-disabled, .ios26-disabled):not(
402
412
  :has(ion-back-button, ion-button:not(.button-clear), ion-menu-button.menu-button-hidden)
403
413
  ) {
404
- transition: transform var(--ios-theme-activated-transition-duration, var(--ios26-activated-transition-duration)) ease-out;
414
+ transition: $press-transition;
405
415
  }
406
416
 
407
417
  ion-buttons.ios:not(.ios-theme-disabled, .ios26-disabled):not(:has(ion-back-button, ion-button.button-clear)) {
408
418
  ion-button {
409
- margin: 0 6px;
419
+ margin-block: 0;
420
+ margin-inline: 6px;
410
421
  &:first-child {
411
- margin-left: 0;
422
+ margin-inline-start: 0;
412
423
  }
413
424
  &:last-child {
414
- margin-right: 0;
425
+ margin-inline-end: 0;
415
426
  }
416
427
  &:only-child {
417
428
  margin: 0;
@@ -6,7 +6,13 @@ $scaleup-default-icon-only: api.$button-scaleup-default-icon-only;
6
6
  $scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
7
7
 
8
8
  ion-fab.ios:not(.ios-theme-disabled, .ios26-disabled) {
9
+ --ios-theme-safe-area-inline-start: var(--ion-safe-area-left, 0px);
10
+ --ios-theme-safe-area-inline-end: var(--ion-safe-area-right, 0px);
9
11
  --transform-value: 2.5px;
12
+ &:dir(rtl) {
13
+ --ios-theme-safe-area-inline-start: var(--ion-safe-area-right, 0px);
14
+ --ios-theme-safe-area-inline-end: var(--ion-safe-area-left, 0px);
15
+ }
10
16
  &:has(> ion-fab-button.fab-button-small) {
11
17
  --transform-value: 4px;
12
18
  }
@@ -85,20 +91,20 @@ ion-fab.ios:not(.ios-theme-disabled, .ios26-disabled) {
85
91
  }
86
92
 
87
93
  &.fab-horizontal-start {
88
- left: calc(16px + var(--ios-theme-menu-width, var(--ios26-menu-width, var(--ion-safe-area-left), 0px)));
94
+ inset-inline-start: calc(16px + var(--ios-theme-menu-width, var(--ios26-menu-width, var(--ios-theme-safe-area-inline-start))));
89
95
  }
90
96
 
91
97
  &.fab-horizontal-end {
92
- right: calc(16px + var(--ion-safe-area-right, 0px));
98
+ inset-inline-end: calc(16px + var(--ios-theme-safe-area-inline-end));
93
99
  }
94
100
 
95
101
  &:has(.fab-button-small) {
96
102
  &.fab-horizontal-start {
97
- left: calc(10px + var(--ios-theme-menu-width, var(--ios26-menu-width, var(--ion-safe-area-left), 0px)));
103
+ inset-inline-start: calc(10px + var(--ios-theme-menu-width, var(--ios26-menu-width, var(--ios-theme-safe-area-inline-start))));
98
104
  }
99
105
 
100
106
  &.fab-horizontal-end {
101
- right: calc(10px + var(--ion-safe-area-right, 0px));
107
+ inset-inline-end: calc(10px + var(--ios-theme-safe-area-inline-end));
102
108
  }
103
109
  }
104
110
  }
@@ -1,4 +1,4 @@
1
- @use '../utils/structured-list';
1
+ @use 'pkg:@rdlabo/ionic-theme-utils/styles/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));
@@ -11,7 +11,7 @@ ion-list.ios:not(.ios-theme-disabled, .ios26-disabled) {
11
11
  }
12
12
 
13
13
  ion-list.list-inset.ios:not(.ios-theme-disabled, .ios26-disabled) {
14
- @include structured-list.layout(20px, 28px);
14
+ @include structured-list.layout(20px, 28px, $variant: ios27);
15
15
 
16
16
  ion-radio-group,
17
17
  ion-reorder-group {
@@ -23,13 +23,22 @@ ion-list.list-inset.ios:not(.ios-theme-disabled, .ios26-disabled) {
23
23
  padding-inline-end: calc(var(--ion-safe-area-right, 0) + 20px);
24
24
  }
25
25
 
26
+ &:dir(rtl) ion-radio-group > .radio-group-top {
27
+ padding-inline-start: calc(var(--ion-safe-area-right, 0) + 20px);
28
+ padding-inline-end: calc(var(--ion-safe-area-left, 0) + 20px);
29
+ }
30
+
26
31
  > :is(#{structured-list.$groups}) {
27
32
  ion-item {
28
33
  // To draw lines inside
29
34
  --inner-padding-end: 0;
30
35
 
31
36
  &::part(native) {
32
- padding-right: calc(var(--ion-safe-area-right, 0px) + 18px);
37
+ padding-inline-end: calc(var(--ion-safe-area-right, 0px) + 18px);
38
+ }
39
+
40
+ &:dir(rtl)::part(native) {
41
+ padding-inline-end: calc(var(--ion-safe-area-left, 0px) + 18px);
33
42
  }
34
43
 
35
44
  --min-height: 52px;
@@ -6,13 +6,13 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
6
6
  .split-pane-main {
7
7
  ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
8
8
  width: calc(
9
- 100% - calc(18px + var(--ion-safe-area-left, 0px)) - calc(18px + var(--ion-safe-area-left, 0px)) - 60px -
9
+ 100% - calc(18px + var(--ion-safe-area-left, 0px)) - calc(18px + var(--ion-safe-area-right, 0px)) - 60px -
10
10
  12px - var(--ios-theme-menu-width, var(--ios26-menu-width))
11
11
  );
12
12
 
13
13
  &:has(:nth-child(5)) {
14
14
  width: calc(
15
- 100% - calc(18px + var(--ion-safe-area-left, 0px)) - calc(18px + var(--ion-safe-area-left, 0px)) - var(
15
+ 100% - calc(18px + var(--ion-safe-area-left, 0px)) - calc(18px + var(--ion-safe-area-right, 0px)) - var(
16
16
  --ios-theme-menu-width,
17
17
  var(--ios26-menu-width)
18
18
  )
@@ -31,7 +31,7 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
31
31
  /*
32
32
  * 90px: https://github.com/ionic-team/ionic-framework/blob/main/core/src/components/title/title.ios.scss#L8
33
33
  */
34
- padding-left: calc(90px + var(--ios-theme-menu-width, var(--ios26-menu-width)));
34
+ padding-inline-start: calc(90px + var(--ios-theme-menu-width, var(--ios26-menu-width)));
35
35
  }
36
36
  }
37
37
  ion-content {
@@ -45,7 +45,10 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
45
45
  }
46
46
  ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
47
47
  &[slot='bottom'] {
48
- left: calc(16px + var(--ion-safe-area-left, 0px) + var(--ios-theme-menu-width, var(--ios26-menu-width)));
48
+ inset-inline-start: calc(16px + var(--ion-safe-area-left, 0px) + var(--ios-theme-menu-width, var(--ios26-menu-width)));
49
+ &:dir(rtl) {
50
+ inset-inline-start: calc(16px + var(--ion-safe-area-right, 0px) + var(--ios-theme-menu-width, var(--ios26-menu-width)));
51
+ }
49
52
  }
50
53
  }
51
54
  }
@@ -62,7 +65,7 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
62
65
  /*
63
66
  * 90px: https://github.com/ionic-team/ionic-framework/blob/main/core/src/components/title/title.ios.scss#L8
64
67
  */
65
- padding-right: calc(90px + var(--ios-theme-menu-width, var(--ios26-menu-width)));
68
+ padding-inline-end: calc(90px + var(--ios-theme-menu-width, var(--ios26-menu-width)));
66
69
  }
67
70
  }
68
71
  ion-content {
@@ -88,10 +91,10 @@ ion-menu.ios:not(.ios-theme-disabled, .ios26-disabled) {
88
91
  z-index: 999;
89
92
 
90
93
  &.menu-side-start {
91
- left: 0;
94
+ inset-inline-start: 0;
92
95
  }
93
96
  &.menu-side-end {
94
- left: calc(100vw - var(--ios-theme-menu-width, var(--ios26-menu-width)));
97
+ inset-inline-end: 0;
95
98
  }
96
99
 
97
100
  &::part(backdrop) {
@@ -119,9 +122,13 @@ ion-menu.ios:not(.ios-theme-disabled, .ios26-disabled) {
119
122
  --ios-theme-content-box-shadow-rgb: var(--ios26-content-box-shadow-rgb);
120
123
 
121
124
  &::part(scroll) {
122
- margin: calc(var(--ios-theme-floating-safe-area-top, var(--ios26-floating-safe-area-top)) + 6px)
123
- calc(16px + var(--ion-safe-area-right, 0px)) var(--ios-theme-floating-safe-area-bottom, var(--ios26-floating-safe-area-bottom))
124
- calc(12px + var(--ion-safe-area-left, 0px));
125
+ margin-block: calc(var(--ios-theme-floating-safe-area-top, var(--ios26-floating-safe-area-top)) + 6px)
126
+ var(--ios-theme-floating-safe-area-bottom, var(--ios26-floating-safe-area-bottom));
127
+ margin-inline: calc(12px + var(--ion-safe-area-left, 0px)) calc(16px + var(--ion-safe-area-right, 0px));
128
+ }
129
+
130
+ &:dir(rtl)::part(scroll) {
131
+ margin-inline: calc(12px + var(--ion-safe-area-right, 0px)) calc(16px + var(--ion-safe-area-left, 0px));
125
132
  }
126
133
  }
127
134
  }
@@ -274,6 +274,6 @@ ion-searchbar.ios:not(.ios-theme-disabled, .ios26-disabled).searchbar-classic {
274
274
  width: 1.18rem;
275
275
  }
276
276
  button.searchbar-clear-button {
277
- right: 8px;
277
+ inset-inline-end: 8px;
278
278
  }
279
279
  }
@@ -1,6 +1,12 @@
1
1
  @use '../utils/api';
2
2
 
3
3
  ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
4
+ --ios-theme-safe-area-inline-start: var(--ion-safe-area-left, 0px);
5
+ --ios-theme-safe-area-inline-end: var(--ion-safe-area-right, 0px);
6
+ &:dir(rtl) {
7
+ --ios-theme-safe-area-inline-start: var(--ion-safe-area-right, 0px);
8
+ --ios-theme-safe-area-inline-end: var(--ion-safe-area-left, 0px);
9
+ }
4
10
  // Ionic forces content-box on the host. Keep the Glass border in a
5
11
  // separate layer so its physical-pixel rounding cannot resize the bar.
6
12
  border: 0;
@@ -29,26 +35,24 @@ ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
29
35
  &[slot='top'] {
30
36
  position: absolute;
31
37
  top: var(--ios-theme-floating-safe-area-top, var(--ios26-floating-safe-area-top));
32
- right: calc(16px + var(--ion-safe-area-right, 0px));
38
+ inset-inline-end: calc(16px + var(--ios-theme-safe-area-inline-end));
33
39
  }
34
40
 
35
41
  &[slot='bottom'] {
36
42
  position: absolute;
37
43
  bottom: var(--ios-theme-floating-safe-area-bottom, var(--ios26-floating-safe-area-bottom));
38
- left: calc(16px + var(--ion-safe-area-left, 0px));
44
+ inset-inline-start: calc(16px + var(--ios-theme-safe-area-inline-start));
39
45
  }
40
46
 
41
- &.tab-bar-position-start,
42
- &.tab-bar-position-end:dir(rtl) {
43
- left: calc(16px + var(--ion-safe-area-left, 0px));
44
- right: auto;
47
+ &.tab-bar-position-start {
48
+ inset-inline-start: calc(16px + var(--ios-theme-safe-area-inline-start));
49
+ inset-inline-end: auto;
45
50
  margin-inline: 0;
46
51
  }
47
52
 
48
- &.tab-bar-position-end,
49
- &.tab-bar-position-start:dir(rtl) {
50
- left: auto;
51
- right: calc(16px + var(--ion-safe-area-right, 0px));
53
+ &.tab-bar-position-end {
54
+ inset-inline-start: auto;
55
+ inset-inline-end: calc(16px + var(--ios-theme-safe-area-inline-end));
52
56
  margin-inline: 0;
53
57
  }
54
58
 
@@ -57,27 +57,29 @@ ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled).toolbar-searchbar {
57
57
  }
58
58
  &:has(ion-buttons[slot='start']) {
59
59
  ion-buttons[slot='start'] {
60
- margin-right: 6px;
60
+ margin-inline-end: 6px;
61
61
  }
62
62
  ion-searchbar.ios:not(.ios-theme-disabled, .ios26-disabled) {
63
63
  &:not(.searchbar-classic) {
64
64
  padding-inline: 6px 12px;
65
65
  }
66
66
  &.searchbar-classic .searchbar-input-container {
67
- margin: 0 12px 0 6px;
67
+ margin-block: 0;
68
+ margin-inline: 6px 12px;
68
69
  }
69
70
  }
70
71
  }
71
72
  &:has(ion-buttons[slot='end']) {
72
73
  ion-buttons[slot='end'] {
73
- margin-left: 6px;
74
+ margin-inline-start: 6px;
74
75
  }
75
76
  ion-searchbar.ios:not(.ios-theme-disabled, .ios26-disabled) {
76
77
  &:not(.searchbar-classic) {
77
78
  padding-inline: 12px 6px;
78
79
  }
79
80
  &.searchbar-classic .searchbar-input-container {
80
- margin: 0 6px 0 12px;
81
+ margin-block: 0;
82
+ margin-inline: 12px 6px;
81
83
  }
82
84
  }
83
85
  }
@@ -85,15 +87,12 @@ ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled).toolbar-searchbar {
85
87
 
86
88
  ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled) {
87
89
  ion-buttons {
88
- margin-left: 12px;
89
- margin-right: 12px;
90
+ margin-inline: 12px;
90
91
  }
91
92
  ion-back-button {
92
- margin-left: 12px;
93
- margin-right: 12px;
93
+ margin-inline: 12px;
94
94
  }
95
95
  ion-buttons > ion-back-button {
96
- margin-left: 0;
97
- margin-right: 0;
96
+ margin-inline: 0;
98
97
  }
99
98
  }
@@ -1,10 +1,10 @@
1
- @use 'utils/structured-list';
1
+ @use 'pkg:@rdlabo/ionic-theme-utils/styles/structured-list';
2
2
 
3
3
  ion-list.md {
4
4
  @include structured-list.support-text;
5
5
 
6
6
  &.list-inset {
7
- @include structured-list.layout(8px, 16px);
7
+ @include structured-list.layout(8px, 16px, $variant: ios27);
8
8
  :is(#{structured-list.$groups}) ion-item {
9
9
  & > ion-input[labelplacement='floating'] {
10
10
  transition: transform 200ms ease;
@@ -1,4 +1,4 @@
1
- @use '../structured-list';
1
+ @use 'pkg:@rdlabo/ionic-theme-utils/styles/structured-list';
2
2
 
3
3
  @mixin ion-list {
4
4
  ion-list.ios:not(.ios-theme-disabled, .ios26-disabled) {
@@ -20,6 +20,18 @@ ion-footer.ios:not(.ios-theme-disabled, .ios26-disabled).footer-translucent {
20
20
  }
21
21
  }
22
22
 
23
+ .ion-page:has(> ion-content.ios.content-fullscreen) > ion-header.ios:not(.ios-theme-disabled, .ios26-disabled):not(.header-translucent),
24
+ .ion-page:has(> ion-content.ios.content-fullscreen) > ion-footer.ios:not(.ios-theme-disabled, .ios26-disabled):not(.footer-translucent) {
25
+ ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled) {
26
+ --background: transparent;
27
+ --opacity: 1;
28
+
29
+ &::part(background) {
30
+ --opacity: 0;
31
+ }
32
+ }
33
+ }
34
+
23
35
  ion-header.ios:not(.ios-theme-disabled, .ios26-disabled).header-translucent:not(.header-collapse-condense) {
24
36
  &:has(> ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled) > *)::before {
25
37
  height: 100%;
@@ -40,6 +52,27 @@ ion-header.ios:not(.ios-theme-disabled, .ios26-disabled).header-translucent:not(
40
52
  }
41
53
  }
42
54
 
55
+ .ion-page:has(> ion-content.ios.content-fullscreen) > ion-footer.ios:not(.ios-theme-disabled, .ios26-disabled).footer-translucent {
56
+ &:has(> ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled) > *)::before {
57
+ bottom: 0;
58
+ height: 100%;
59
+ background: rgba(
60
+ var(--ion-color-base-rgb, var(--ios-theme-content-box-shadow-rgb, var(--ios26-content-box-shadow-rgb))),
61
+ calc(0.2 * var(--opacity-scale, 1))
62
+ );
63
+ backdrop-filter: blur(8px);
64
+ }
65
+
66
+ &:not(.ion-no-border) > ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled):has(> *):first-of-type {
67
+ --border-width: 0.5px 0 0;
68
+ --border-color: var(--ion-toolbar-border-color, rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.08));
69
+ &::part(background) {
70
+ --opacity: 1;
71
+ background: transparent;
72
+ }
73
+ }
74
+ }
75
+
43
76
  ion-header.ios:not(.ios-theme-disabled, .ios26-disabled).header-collapse-main ion-toolbar.in-toolbar ion-title {
44
77
  transition:
45
78
  opacity 0.5s ease-in-out,
@@ -58,6 +91,8 @@ ion-header.ios:not(.ios-theme-disabled, .ios26-disabled).header-collapse-condens
58
91
 
59
92
  ion-header.ios:not(.ios-theme-disabled, .ios26-disabled).header-translucent::before,
60
93
  ion-header.ios:not(.ios-theme-disabled, .ios26-disabled).header-translucent::after,
94
+ .ion-page:has(> ion-content.ios.content-fullscreen) > ion-footer.ios:not(.ios-theme-disabled, .ios26-disabled).footer-translucent::before,
95
+ .ion-page:has(> ion-content.ios.content-fullscreen) > ion-footer.ios:not(.ios-theme-disabled, .ios26-disabled).footer-translucent::after,
61
96
  ion-content.ios:not(.ios-theme-disabled, .ios26-disabled).content-fullscreen::part(background)::before {
62
97
  content: '';
63
98
  position: absolute;
@@ -84,6 +119,15 @@ ion-header.ios:not(.ios-theme-disabled, .ios26-disabled).header-translucent::aft
84
119
  mask-composite: intersect;
85
120
  }
86
121
 
122
+ .ion-page:has(> ion-content.ios.content-fullscreen) > ion-footer.ios:not(.ios-theme-disabled, .ios26-disabled).footer-translucent::after {
123
+ z-index: 2;
124
+ bottom: 0;
125
+ height: 100%;
126
+ backdrop-filter: blur(2px);
127
+ mask-image: linear-gradient(rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 20%);
128
+ mask-composite: intersect;
129
+ }
130
+
87
131
  /**
88
132
  * ここから下は、ion-content::beforeが content:'' を持たない限り表示されない
89
133
  */