@rdlabo/ionic-theme-ios27 2.0.0-beta.pr313.shae1c646a70d58 → 2.0.0-beta.pr317.sha707be106d55b

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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Ionic Theme iOS27
2
2
 
3
3
  Install with `npm install @rdlabo/ionic-theme-ios27 @erkamyaman/capacitor-foldable`. For 1.x, use the [v1.2.1 guide](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/ios27-v1.2.1).
4
- A theme for Ionic apps that brings iOS 27 Liquid Glass and motion to the Web. Capacitor iOS apps can also opt into a preview Native UI for supported controls.
4
+ A theme for Ionic apps that brings iOS 27 Liquid Glass and motion to the Web. Capacitor iOS apps can also opt into Native UI for supported controls, officially supported starting with 2.0.0.
5
5
 
6
6
  **[Ionic 9 demo](https://ionic-theme-ios27.rdlabo.dev/) · [Ionic 8 demo](https://ionic8-theme-ios27.rdlabo.dev/) · [Setup guide](docs/setup.md)**
7
7
 
@@ -1 +1 @@
1
- {"version":3,"file":"ios.transition.d.ts","names":[],"sources":["../../src/transition/ios.transition.ts"],"mappings":";;;;iBASiB;EACf;;qBAYW,YAAa,QAAQ,QAAQ;qBAI7B,yBAAyB,OAAO,aAAa,MAAM,sBAAsB"}
1
+ {"version":3,"file":"ios.transition.d.ts","names":[],"sources":["../../src/transition/ios.transition.ts"],"mappings":";;;;iBAUiB;EACf;;qBAYW,YAAa,QAAQ,QAAQ;qBAK7B,yBAAyB,OAAO,aAAa,MAAM,sBAAsB"}
@@ -1,5 +1,6 @@
1
1
  import { getIonPageElement } from "./index.js";
2
2
  import { withNativeUITransition } from "./native-ui.js";
3
+ import { createAnimation } from "@ionic/core/components/index.js";
3
4
  import { nativeWebViewMetrics } from "@rdlabo/capacitor-native-ui/integration";
4
5
  import { createIosTransitionAnimation, shadow } from "@rdlabo/ionic-theme-utils";
5
6
  //#region src/transition/ios.transition.ts
@@ -14,7 +15,46 @@ const transitionConfig = {
14
15
  const setConfig = (config) => {
15
16
  if (config.radius !== void 0) nativeWebViewMetrics.radius = config.radius;
16
17
  };
17
- const iosTransitionAnimation = withNativeUITransition(createIosTransitionAnimation(transitionConfig));
18
+ const buildTransition = createIosTransitionAnimation(transitionConfig);
19
+ const iosTransitionAnimation = withNativeUITransition((navEl, opts) => {
20
+ const animation = buildTransition(navEl, opts);
21
+ const backing = "var(--ion-background-color, #fff)";
22
+ let ownsBacking = false;
23
+ animation.beforeAddWrite(() => {
24
+ if (navEl.style.backgroundColor) return;
25
+ const style = navEl.ownerDocument.defaultView?.getComputedStyle(navEl);
26
+ if (!style || !["transparent", "rgba(0, 0, 0, 0)"].includes(style.backgroundColor)) return;
27
+ if (style.backgroundImage && style.backgroundImage !== "none") return;
28
+ navEl.style.backgroundColor = backing;
29
+ ownsBacking = true;
30
+ });
31
+ const restoreBacking = () => {
32
+ if (ownsBacking && navEl.style.backgroundColor === backing && !navEl.style.getPropertyPriority("background-color")) navEl.style.removeProperty("background-color");
33
+ ownsBacking = false;
34
+ };
35
+ animation.afterAddWrite(restoreBacking);
36
+ const destroy = animation.destroy.bind(animation);
37
+ animation.destroy = (...args) => {
38
+ restoreBacking();
39
+ return destroy(...args);
40
+ };
41
+ const buttons = opts.leavingEl?.querySelectorAll(":scope > ion-header.header-translucent ion-buttons, :scope > ion-header.header-translucent ion-toolbar > ion-button");
42
+ if (buttons?.length) animation.addAnimation(createAnimation().addElement(buttons).keyframes([
43
+ {
44
+ offset: 0,
45
+ opacity: 1
46
+ },
47
+ {
48
+ offset: .5,
49
+ opacity: 0
50
+ },
51
+ {
52
+ offset: 1,
53
+ opacity: 0
54
+ }
55
+ ]));
56
+ return animation;
57
+ });
18
58
  //#endregion
19
59
  export { iosTransitionAnimation, setConfig, shadow };
20
60
 
@@ -1 +1 @@
1
- {"version":3,"file":"ios.transition.js","names":[],"sources":["../../src/transition/ios.transition.ts"],"sourcesContent":["import { createIosTransitionAnimation, shadow } from '@rdlabo/ionic-theme-utils';\nimport type { Animation } from '@ionic/core/components';\nimport type { TransitionOptions } from './index';\nimport { getIonPageElement } from './index';\nimport { nativeWebViewMetrics } from '@rdlabo/capacitor-native-ui/integration';\nimport { withNativeUITransition } from './native-ui';\n\nexport { shadow };\n\nexport interface IosTransitionConfig {\n radius: number;\n}\n\nconst transitionConfig = {\n offLeftPercent: 30,\n getIonPageElement,\n shouldAnimateFixedBackButton: (navEl: HTMLElement) => !navEl.closest('ion-app.ios-theme-vertical-bars'),\n get radius() {\n return nativeWebViewMetrics.radius;\n },\n};\n\nexport const setConfig = (config: Partial<IosTransitionConfig>): void => {\n if (config.radius !== undefined) nativeWebViewMetrics.radius = config.radius;\n};\n\nexport const iosTransitionAnimation: (navEl: HTMLElement, opts: TransitionOptions) => Animation = withNativeUITransition(\n createIosTransitionAnimation<TransitionOptions>(transitionConfig),\n);\n"],"mappings":";;;;;AAaA,MAAM,mBAAmB;CACvB,gBAAgB;CAChB;CACA,+BAA+B,UAAuB,CAAC,MAAM,QAAQ,iCAAiC;CACtG,IAAI,SAAS;EACX,OAAO,qBAAqB;CAC9B;AACF;AAEA,MAAa,aAAa,WAA+C;CACvE,IAAI,OAAO,WAAW,KAAA,GAAW,qBAAqB,SAAS,OAAO;AACxE;AAEA,MAAa,yBAAqF,uBAChG,6BAAgD,gBAAgB,CAClE"}
1
+ {"version":3,"file":"ios.transition.js","names":[],"sources":["../../src/transition/ios.transition.ts"],"sourcesContent":["import { createIosTransitionAnimation, shadow } from '@rdlabo/ionic-theme-utils';\nimport { createAnimation } from '@ionic/core/components/index.js';\nimport type { Animation } from '@ionic/core/components';\nimport type { TransitionOptions } from './index';\nimport { getIonPageElement } from './index';\nimport { nativeWebViewMetrics } from '@rdlabo/capacitor-native-ui/integration';\nimport { withNativeUITransition } from './native-ui';\n\nexport { shadow };\n\nexport interface IosTransitionConfig {\n radius: number;\n}\n\nconst transitionConfig = {\n offLeftPercent: 30,\n getIonPageElement,\n shouldAnimateFixedBackButton: (navEl: HTMLElement) => !navEl.closest('ion-app.ios-theme-vertical-bars'),\n get radius() {\n return nativeWebViewMetrics.radius;\n },\n};\n\nexport const setConfig = (config: Partial<IosTransitionConfig>): void => {\n if (config.radius !== undefined) nativeWebViewMetrics.radius = config.radius;\n};\n\nconst buildTransition = createIosTransitionAnimation<TransitionOptions>(transitionConfig);\nexport const iosTransitionAnimation: (navEl: HTMLElement, opts: TransitionOptions) => Animation = withNativeUITransition((navEl, opts) => {\n const animation = buildTransition(navEl, opts);\n const backing = 'var(--ion-background-color, #fff)';\n let ownsBacking = false;\n // Translucent pages expose the outlet as they move. Keep that backing surface\n // in the page theme instead of exposing WKWebView's transparent native backing.\n animation.beforeAddWrite(() => {\n if (navEl.style.backgroundColor) return;\n const style = navEl.ownerDocument.defaultView?.getComputedStyle(navEl);\n if (!style || !['transparent', 'rgba(0, 0, 0, 0)'].includes(style.backgroundColor)) return;\n if (style.backgroundImage && style.backgroundImage !== 'none') return;\n navEl.style.backgroundColor = backing;\n ownsBacking = true;\n });\n const restoreBacking = () => {\n if (ownsBacking && navEl.style.backgroundColor === backing && !navEl.style.getPropertyPriority('background-color'))\n navEl.style.removeProperty('background-color');\n ownsBacking = false;\n };\n animation.afterAddWrite(restoreBacking);\n // Ionic destroy does not run after-write hooks when playback is interrupted.\n const destroy = animation.destroy.bind(animation);\n animation.destroy = (...args) => {\n restoreBacking();\n return destroy(...args);\n };\n // Translucent pages move as a whole, but outgoing toolbar actions still need\n // the same exit fade as opaque headers when they return from native to Web.\n const buttons = opts.leavingEl?.querySelectorAll(\n ':scope > ion-header.header-translucent ion-buttons, :scope > ion-header.header-translucent ion-toolbar > ion-button',\n );\n if (buttons?.length)\n animation.addAnimation(\n createAnimation()\n .addElement(buttons)\n .keyframes([\n { offset: 0, opacity: 1 },\n // Finish the exit before the incoming title occupies this toolbar.\n { offset: 0.5, opacity: 0 },\n { offset: 1, opacity: 0 },\n ]),\n );\n return animation;\n});\n"],"mappings":";;;;;;AAcA,MAAM,mBAAmB;CACvB,gBAAgB;CAChB;CACA,+BAA+B,UAAuB,CAAC,MAAM,QAAQ,iCAAiC;CACtG,IAAI,SAAS;EACX,OAAO,qBAAqB;CAC9B;AACF;AAEA,MAAa,aAAa,WAA+C;CACvE,IAAI,OAAO,WAAW,KAAA,GAAW,qBAAqB,SAAS,OAAO;AACxE;AAEA,MAAM,kBAAkB,6BAAgD,gBAAgB;AACxF,MAAa,yBAAqF,wBAAwB,OAAO,SAAS;CACxI,MAAM,YAAY,gBAAgB,OAAO,IAAI;CAC7C,MAAM,UAAU;CAChB,IAAI,cAAc;CAGlB,UAAU,qBAAqB;EAC7B,IAAI,MAAM,MAAM,iBAAiB;EACjC,MAAM,QAAQ,MAAM,cAAc,aAAa,iBAAiB,KAAK;EACrE,IAAI,CAAC,SAAS,CAAC,CAAC,eAAe,kBAAkB,CAAC,CAAC,SAAS,MAAM,eAAe,GAAG;EACpF,IAAI,MAAM,mBAAmB,MAAM,oBAAoB,QAAQ;EAC/D,MAAM,MAAM,kBAAkB;EAC9B,cAAc;CAChB,CAAC;CACD,MAAM,uBAAuB;EAC3B,IAAI,eAAe,MAAM,MAAM,oBAAoB,WAAW,CAAC,MAAM,MAAM,oBAAoB,kBAAkB,GAC/G,MAAM,MAAM,eAAe,kBAAkB;EAC/C,cAAc;CAChB;CACA,UAAU,cAAc,cAAc;CAEtC,MAAM,UAAU,UAAU,QAAQ,KAAK,SAAS;CAChD,UAAU,WAAW,GAAG,SAAS;EAC/B,eAAe;EACf,OAAO,QAAQ,GAAG,IAAI;CACxB;CAGA,MAAM,UAAU,KAAK,WAAW,iBAC9B,qHACF;CACA,IAAI,SAAS,QACX,UAAU,aACR,gBAAgB,CAAC,CACd,WAAW,OAAO,CAAC,CACnB,UAAU;EACT;GAAE,QAAQ;GAAG,SAAS;EAAE;EAExB;GAAE,QAAQ;GAAK,SAAS;EAAE;EAC1B;GAAE,QAAQ;GAAG,SAAS;EAAE;CAC1B,CAAC,CACL;CACF,OAAO;AACT,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { Animation, AnimationBuilder } from "@ionic/core/components";
2
2
  //#region src/transition/native-ui.d.ts
3
3
  export declare const withNativeUITransition: (builder: AnimationBuilder) => AnimationBuilder;
4
- export declare const connectNativeUITransition: (animation: Animation, entering: HTMLElement, leaving?: HTMLElement) => void;
4
+ export declare const connectNativeUITransition: (animation: Animation, entering: HTMLElement, leaving?: HTMLElement, direction?: "forward" | "back" | "root") => void;
5
5
  //#endregion
6
6
  //# sourceMappingURL=native-ui.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"native-ui.d.ts","names":[],"sources":["../../src/transition/native-ui.ts"],"mappings":";;qBASa,yBACV,SAAS,qBAAmB;qBAWlB,4BAA6B,WAAW,WAAW,UAAU,aAAa,UAAU"}
1
+ {"version":3,"file":"native-ui.d.ts","names":[],"sources":["../../src/transition/native-ui.ts"],"mappings":";;qBAUa,yBACV,SAAS,qBAAmB;qBAWlB,4BACX,WAAW,WACX,UAAU,aACV,UAAU,aACV"}
@@ -7,13 +7,13 @@ import { VERTICAL_BARS_TRANSITION_CANCELED, hasNativeUIIntegration, suspendNativ
7
7
  */
8
8
  const withNativeUITransition = (builder) => (baseEl, opts) => {
9
9
  const animation = builder(baseEl, opts);
10
- if (opts?.enteringEl) connectNativeUITransition(animation, opts.enteringEl, opts.leavingEl);
10
+ if (opts?.enteringEl) connectNativeUITransition(animation, opts.enteringEl, opts.leavingEl, opts.direction);
11
11
  return animation;
12
12
  };
13
13
  const connectedTransitions = /* @__PURE__ */ new WeakSet();
14
14
  const cancellationTransitions = /* @__PURE__ */ new WeakSet();
15
15
  /** Ionic write hooks cannot await the bridge. Gate playback, including interactive playback. */
16
- const connectNativeUITransition = (animation, entering, leaving) => {
16
+ const connectNativeUITransition = (animation, entering, leaving, direction) => {
17
17
  if (leaving && !cancellationTransitions.has(animation)) {
18
18
  cancellationTransitions.add(animation);
19
19
  animation.onFinish((step) => {
@@ -32,16 +32,35 @@ const connectNativeUITransition = (animation, entering, leaving) => {
32
32
  const progressEnd = animation.progressEnd.bind(animation);
33
33
  const destroy = animation.destroy.bind(animation);
34
34
  let release;
35
+ let frame = 0;
36
+ const win = entering.ownerDocument.defaultView;
37
+ const webAnimations = (value) => [...value.getWebAnimations(), ...value.childAnimations.flatMap(webAnimations)];
38
+ const track = () => {
39
+ if (!release?.progress || !win) return;
40
+ const sample = () => {
41
+ const progress = webAnimations(animation).map((value) => value.effect?.getComputedTiming().progress).find((value) => value != null);
42
+ if (progress != null) release?.progress?.(progress);
43
+ frame = win.requestAnimationFrame(sample);
44
+ };
45
+ frame = win.requestAnimationFrame(sample);
46
+ };
35
47
  let preparation;
36
48
  let disposed = false;
37
49
  let interactiveReady = false;
38
50
  let step;
39
51
  let end;
40
- const prepare = () => preparation ?? (preparation = suspendNativeUI(scopes).then((resume) => {
52
+ const prepare = (interactive = false) => preparation ?? (preparation = suspendNativeUI(scopes, {
53
+ navigation: true,
54
+ ...direction ? { direction } : {},
55
+ ...interactive ? { interactive: true } : {}
56
+ }).then((resume) => {
41
57
  if (disposed) resume(true);
42
58
  else release = resume;
43
59
  }));
44
60
  const finish = (canceled = false) => {
61
+ if (frame) win?.cancelAnimationFrame(frame);
62
+ frame = 0;
63
+ release?.progress?.(canceled ? 0 : 1);
45
64
  release?.(canceled);
46
65
  release = void 0;
47
66
  preparation = void 0;
@@ -51,7 +70,9 @@ const connectNativeUITransition = (animation, entering, leaving) => {
51
70
  await prepare();
52
71
  if (disposed) return;
53
72
  try {
54
- await play(options);
73
+ const playing = play(options);
74
+ track();
75
+ await playing;
55
76
  } finally {
56
77
  finish();
57
78
  }
@@ -60,23 +81,36 @@ const connectNativeUITransition = (animation, entering, leaving) => {
60
81
  interactiveReady = false;
61
82
  step = void 0;
62
83
  end = void 0;
63
- prepare().then(() => {
84
+ prepare(true).then(() => {
64
85
  if (disposed) return;
65
86
  progressStart(...args);
87
+ track();
66
88
  interactiveReady = true;
67
- if (step !== void 0) progressStep(step);
68
- if (end) progressEnd(...end);
89
+ if (step !== void 0) {
90
+ progressStep(step);
91
+ release?.progress?.(step);
92
+ }
93
+ if (end) {
94
+ release?.finishGesture?.(end[0] === 1);
95
+ progressEnd(...end);
96
+ }
69
97
  });
70
98
  return animation;
71
99
  };
72
100
  animation.progressStep = (value) => {
73
101
  step = value;
74
- if (interactiveReady) progressStep(value);
102
+ if (interactiveReady) {
103
+ progressStep(value);
104
+ release?.progress?.(value);
105
+ }
75
106
  return animation;
76
107
  };
77
108
  animation.progressEnd = (...args) => {
78
109
  end = args;
79
- if (interactiveReady) progressEnd(...args);
110
+ if (interactiveReady) {
111
+ release?.finishGesture?.(args[0] === 1);
112
+ progressEnd(...args);
113
+ }
80
114
  return animation;
81
115
  };
82
116
  animation.destroy = (...args) => {
@@ -1 +1 @@
1
- {"version":3,"file":"native-ui.js","names":[],"sources":["../../src/transition/native-ui.ts"],"sourcesContent":["import type { Animation } from '@ionic/core/components';\nimport { hasNativeUIIntegration, suspendNativeUI, VERTICAL_BARS_TRANSITION_CANCELED } from '@rdlabo/capacitor-native-ui/integration';\nimport type { AnimationBuilder } from '@ionic/core/components';\n\n/**\n * Keeps an existing Ionic navigation animation and coordinates native control handoff.\n * Register the returned builder as navAnimation. Do not use it for overlay animations.\n * The builder must return a fresh Animation for each navigation; Ionic destroys it afterward.\n */\nexport const withNativeUITransition =\n (builder: AnimationBuilder): AnimationBuilder =>\n (baseEl, opts) => {\n const animation = builder(baseEl, opts);\n if (opts?.enteringEl) connectNativeUITransition(animation, opts.enteringEl, opts.leavingEl);\n return animation;\n };\n\nconst connectedTransitions = new WeakSet<Animation>();\nconst cancellationTransitions = new WeakSet<Animation>();\n\n/** Ionic write hooks cannot await the bridge. Gate playback, including interactive playback. */\nexport const connectNativeUITransition = (animation: Animation, entering: HTMLElement, leaving?: HTMLElement) => {\n if (leaving && !cancellationTransitions.has(animation)) {\n cancellationTransitions.add(animation);\n animation.onFinish((step) => {\n if (step === 0) leaving.dispatchEvent(new CustomEvent(VERTICAL_BARS_TRANSITION_CANCELED, { bubbles: true, detail: { entering } }));\n });\n }\n if (connectedTransitions.has(animation) || !hasNativeUIIntegration(entering.ownerDocument)) return;\n connectedTransitions.add(animation);\n const scopes = leaving ? [entering, leaving] : [entering];\n const play = animation.play.bind(animation);\n const progressStart = animation.progressStart.bind(animation);\n const progressStep = animation.progressStep.bind(animation);\n const progressEnd = animation.progressEnd.bind(animation);\n const destroy = animation.destroy.bind(animation);\n let release: ((canceled?: boolean) => void) | undefined;\n let preparation: Promise<void> | undefined;\n let disposed = false;\n let interactiveReady = false;\n let step: number | undefined;\n let end: Parameters<Animation['progressEnd']> | undefined;\n const prepare = () =>\n (preparation ??= suspendNativeUI(scopes).then((resume) => {\n if (disposed) resume(true);\n else release = resume;\n }));\n const finish = (canceled = false) => {\n release?.(canceled);\n release = undefined;\n preparation = undefined;\n };\n animation.onFinish((step) => finish(step === 0));\n animation.play = async (options) => {\n await prepare();\n if (disposed) return;\n try {\n await play(options);\n } finally {\n finish();\n }\n };\n animation.progressStart = (...args) => {\n interactiveReady = false;\n step = undefined;\n end = undefined;\n void prepare().then(() => {\n if (disposed) return;\n progressStart(...args);\n interactiveReady = true;\n if (step !== undefined) progressStep(step);\n if (end) progressEnd(...end);\n });\n return animation;\n };\n animation.progressStep = (value) => {\n step = value;\n if (interactiveReady) progressStep(value);\n return animation;\n };\n animation.progressEnd = (...args) => {\n end = args;\n if (interactiveReady) progressEnd(...args);\n return animation;\n };\n animation.destroy = (...args) => {\n disposed = true;\n finish(true);\n return destroy(...args);\n };\n};\n"],"mappings":";;;;;;;AASA,MAAa,0BACV,aACA,QAAQ,SAAS;CAChB,MAAM,YAAY,QAAQ,QAAQ,IAAI;CACtC,IAAI,MAAM,YAAY,0BAA0B,WAAW,KAAK,YAAY,KAAK,SAAS;CAC1F,OAAO;AACT;AAEF,MAAM,uCAAuB,IAAI,QAAmB;AACpD,MAAM,0CAA0B,IAAI,QAAmB;;AAGvD,MAAa,6BAA6B,WAAsB,UAAuB,YAA0B;CAC/G,IAAI,WAAW,CAAC,wBAAwB,IAAI,SAAS,GAAG;EACtD,wBAAwB,IAAI,SAAS;EACrC,UAAU,UAAU,SAAS;GAC3B,IAAI,SAAS,GAAG,QAAQ,cAAc,IAAI,YAAY,mCAAmC;IAAE,SAAS;IAAM,QAAQ,EAAE,SAAS;GAAE,CAAC,CAAC;EACnI,CAAC;CACH;CACA,IAAI,qBAAqB,IAAI,SAAS,KAAK,CAAC,uBAAuB,SAAS,aAAa,GAAG;CAC5F,qBAAqB,IAAI,SAAS;CAClC,MAAM,SAAS,UAAU,CAAC,UAAU,OAAO,IAAI,CAAC,QAAQ;CACxD,MAAM,OAAO,UAAU,KAAK,KAAK,SAAS;CAC1C,MAAM,gBAAgB,UAAU,cAAc,KAAK,SAAS;CAC5D,MAAM,eAAe,UAAU,aAAa,KAAK,SAAS;CAC1D,MAAM,cAAc,UAAU,YAAY,KAAK,SAAS;CACxD,MAAM,UAAU,UAAU,QAAQ,KAAK,SAAS;CAChD,IAAI;CACJ,IAAI;CACJ,IAAI,WAAW;CACf,IAAI,mBAAmB;CACvB,IAAI;CACJ,IAAI;CACJ,MAAM,gBACH,gBAAA,cAAgB,gBAAgB,MAAM,CAAC,CAAC,MAAM,WAAW;EACxD,IAAI,UAAU,OAAO,IAAI;OACpB,UAAU;CACjB,CAAC;CACH,MAAM,UAAU,WAAW,UAAU;EACnC,UAAU,QAAQ;EAClB,UAAU,KAAA;EACV,cAAc,KAAA;CAChB;CACA,UAAU,UAAU,SAAS,OAAO,SAAS,CAAC,CAAC;CAC/C,UAAU,OAAO,OAAO,YAAY;EAClC,MAAM,QAAQ;EACd,IAAI,UAAU;EACd,IAAI;GACF,MAAM,KAAK,OAAO;EACpB,UAAU;GACR,OAAO;EACT;CACF;CACA,UAAU,iBAAiB,GAAG,SAAS;EACrC,mBAAmB;EACnB,OAAO,KAAA;EACP,MAAM,KAAA;EACN,QAAa,CAAC,CAAC,WAAW;GACxB,IAAI,UAAU;GACd,cAAc,GAAG,IAAI;GACrB,mBAAmB;GACnB,IAAI,SAAS,KAAA,GAAW,aAAa,IAAI;GACzC,IAAI,KAAK,YAAY,GAAG,GAAG;EAC7B,CAAC;EACD,OAAO;CACT;CACA,UAAU,gBAAgB,UAAU;EAClC,OAAO;EACP,IAAI,kBAAkB,aAAa,KAAK;EACxC,OAAO;CACT;CACA,UAAU,eAAe,GAAG,SAAS;EACnC,MAAM;EACN,IAAI,kBAAkB,YAAY,GAAG,IAAI;EACzC,OAAO;CACT;CACA,UAAU,WAAW,GAAG,SAAS;EAC/B,WAAW;EACX,OAAO,IAAI;EACX,OAAO,QAAQ,GAAG,IAAI;CACxB;AACF"}
1
+ {"version":3,"file":"native-ui.js","names":[],"sources":["../../src/transition/native-ui.ts"],"sourcesContent":["import type { NativeUITransitionRelease } from '@rdlabo/capacitor-native-ui/integration';\nimport type { Animation } from '@ionic/core/components';\nimport { hasNativeUIIntegration, suspendNativeUI, VERTICAL_BARS_TRANSITION_CANCELED } from '@rdlabo/capacitor-native-ui/integration';\nimport type { AnimationBuilder } from '@ionic/core/components';\n\n/**\n * Keeps an existing Ionic navigation animation and coordinates native control handoff.\n * Register the returned builder as navAnimation. Do not use it for overlay animations.\n * The builder must return a fresh Animation for each navigation; Ionic destroys it afterward.\n */\nexport const withNativeUITransition =\n (builder: AnimationBuilder): AnimationBuilder =>\n (baseEl, opts) => {\n const animation = builder(baseEl, opts);\n if (opts?.enteringEl) connectNativeUITransition(animation, opts.enteringEl, opts.leavingEl, opts.direction);\n return animation;\n };\n\nconst connectedTransitions = new WeakSet<Animation>();\nconst cancellationTransitions = new WeakSet<Animation>();\n\n/** Ionic write hooks cannot await the bridge. Gate playback, including interactive playback. */\nexport const connectNativeUITransition = (\n animation: Animation,\n entering: HTMLElement,\n leaving?: HTMLElement,\n direction?: 'forward' | 'back' | 'root',\n) => {\n if (leaving && !cancellationTransitions.has(animation)) {\n cancellationTransitions.add(animation);\n animation.onFinish((step) => {\n if (step === 0) leaving.dispatchEvent(new CustomEvent(VERTICAL_BARS_TRANSITION_CANCELED, { bubbles: true, detail: { entering } }));\n });\n }\n if (connectedTransitions.has(animation) || !hasNativeUIIntegration(entering.ownerDocument)) return;\n connectedTransitions.add(animation);\n const scopes = leaving ? [entering, leaving] : [entering];\n const play = animation.play.bind(animation);\n const progressStart = animation.progressStart.bind(animation);\n const progressStep = animation.progressStep.bind(animation);\n const progressEnd = animation.progressEnd.bind(animation);\n const destroy = animation.destroy.bind(animation);\n let release: NativeUITransitionRelease | undefined;\n let frame = 0;\n const win = entering.ownerDocument.defaultView;\n const webAnimations = (value: Animation): globalThis.Animation[] => [\n ...value.getWebAnimations(),\n ...value.childAnimations.flatMap(webAnimations),\n ];\n const track = () => {\n if (!release?.progress || !win) return;\n const sample = () => {\n const progress = webAnimations(animation)\n .map((value) => value.effect?.getComputedTiming().progress)\n .find((value) => value != null);\n if (progress != null) release?.progress?.(progress);\n frame = win.requestAnimationFrame(sample);\n };\n frame = win.requestAnimationFrame(sample);\n };\n let preparation: Promise<void> | undefined;\n let disposed = false;\n let interactiveReady = false;\n let step: number | undefined;\n let end: Parameters<Animation['progressEnd']> | undefined;\n const prepare = (interactive = false) =>\n (preparation ??= suspendNativeUI(scopes, {\n navigation: true,\n ...(direction ? { direction } : {}),\n ...(interactive ? { interactive: true } : {}),\n }).then((resume) => {\n if (disposed) resume(true);\n else release = resume;\n }));\n const finish = (canceled = false) => {\n if (frame) win?.cancelAnimationFrame(frame);\n frame = 0;\n release?.progress?.(canceled ? 0 : 1);\n release?.(canceled);\n release = undefined;\n preparation = undefined;\n };\n animation.onFinish((step) => finish(step === 0));\n animation.play = async (options) => {\n await prepare();\n if (disposed) return;\n try {\n const playing = play(options);\n track();\n await playing;\n } finally {\n finish();\n }\n };\n animation.progressStart = (...args) => {\n interactiveReady = false;\n step = undefined;\n end = undefined;\n void prepare(true).then(() => {\n if (disposed) return;\n progressStart(...args);\n track();\n interactiveReady = true;\n if (step !== undefined) {\n progressStep(step);\n release?.progress?.(step);\n }\n if (end) {\n release?.finishGesture?.(end[0] === 1);\n progressEnd(...end);\n }\n });\n return animation;\n };\n animation.progressStep = (value) => {\n step = value;\n if (interactiveReady) {\n progressStep(value);\n release?.progress?.(value);\n }\n return animation;\n };\n animation.progressEnd = (...args) => {\n end = args;\n if (interactiveReady) {\n release?.finishGesture?.(args[0] === 1);\n progressEnd(...args);\n }\n return animation;\n };\n animation.destroy = (...args) => {\n disposed = true;\n finish(true);\n return destroy(...args);\n };\n};\n"],"mappings":"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"}
package/docs/features.md CHANGED
@@ -31,9 +31,9 @@ ion-textarea label.textarea-wrapper {
31
31
  }
32
32
  ```
33
33
 
34
- ## Native UI (Preview)
34
+ ## Native UI
35
35
 
36
- Once your theme is working, [add Native UI](add-native-ui.md) to render supported fixed controls with UIKit and SwiftUI on iOS. Keep theme styles for Web content and fallback controls. Native UI is an optional preview; adding styles alone does not enable it.
36
+ Once your theme is working, [add Native UI](add-native-ui.md) to render supported fixed controls with UIKit and SwiftUI on iOS. Keep theme styles for Web content and fallback controls. Native UI is officially supported starting with 2.0.0 and remains opt-in; adding styles alone does not enable it.
37
37
 
38
38
  ## Selective component imports
39
39
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rdlabo/ionic-theme-ios27",
3
3
  "private": false,
4
- "version": "2.0.0-beta.pr313.shae1c646a70d58",
4
+ "version": "2.0.0-beta.pr317.sha707be106d55b",
5
5
  "description": "iOS27 Theme for Ionic Framework",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "homepage": "https://docs.rdlabo.dev/projects/ionic-theme-ios27",
48
48
  "dependencies": {
49
- "@rdlabo/capacitor-native-ui": "2.0.0-beta.pr313.shae1c646a70d58",
49
+ "@rdlabo/capacitor-native-ui": "2.0.0-beta.pr317.sha707be106d55b",
50
50
  "@rdlabo/ionic-theme-utils": "0.1.5",
51
51
  "@stencil/core": "^4.43.5",
52
52
  "ionicons": "^8.1.0"
@@ -1,4 +1,5 @@
1
1
  import { createIosTransitionAnimation, shadow } from '@rdlabo/ionic-theme-utils';
2
+ import { createAnimation } from '@ionic/core/components/index.js';
2
3
  import type { Animation } from '@ionic/core/components';
3
4
  import type { TransitionOptions } from './index';
4
5
  import { getIonPageElement } from './index';
@@ -24,6 +25,48 @@ export const setConfig = (config: Partial<IosTransitionConfig>): void => {
24
25
  if (config.radius !== undefined) nativeWebViewMetrics.radius = config.radius;
25
26
  };
26
27
 
27
- export const iosTransitionAnimation: (navEl: HTMLElement, opts: TransitionOptions) => Animation = withNativeUITransition(
28
- createIosTransitionAnimation<TransitionOptions>(transitionConfig),
29
- );
28
+ const buildTransition = createIosTransitionAnimation<TransitionOptions>(transitionConfig);
29
+ export const iosTransitionAnimation: (navEl: HTMLElement, opts: TransitionOptions) => Animation = withNativeUITransition((navEl, opts) => {
30
+ const animation = buildTransition(navEl, opts);
31
+ const backing = 'var(--ion-background-color, #fff)';
32
+ let ownsBacking = false;
33
+ // Translucent pages expose the outlet as they move. Keep that backing surface
34
+ // in the page theme instead of exposing WKWebView's transparent native backing.
35
+ animation.beforeAddWrite(() => {
36
+ if (navEl.style.backgroundColor) return;
37
+ const style = navEl.ownerDocument.defaultView?.getComputedStyle(navEl);
38
+ if (!style || !['transparent', 'rgba(0, 0, 0, 0)'].includes(style.backgroundColor)) return;
39
+ if (style.backgroundImage && style.backgroundImage !== 'none') return;
40
+ navEl.style.backgroundColor = backing;
41
+ ownsBacking = true;
42
+ });
43
+ const restoreBacking = () => {
44
+ if (ownsBacking && navEl.style.backgroundColor === backing && !navEl.style.getPropertyPriority('background-color'))
45
+ navEl.style.removeProperty('background-color');
46
+ ownsBacking = false;
47
+ };
48
+ animation.afterAddWrite(restoreBacking);
49
+ // Ionic destroy does not run after-write hooks when playback is interrupted.
50
+ const destroy = animation.destroy.bind(animation);
51
+ animation.destroy = (...args) => {
52
+ restoreBacking();
53
+ return destroy(...args);
54
+ };
55
+ // Translucent pages move as a whole, but outgoing toolbar actions still need
56
+ // the same exit fade as opaque headers when they return from native to Web.
57
+ const buttons = opts.leavingEl?.querySelectorAll(
58
+ ':scope > ion-header.header-translucent ion-buttons, :scope > ion-header.header-translucent ion-toolbar > ion-button',
59
+ );
60
+ if (buttons?.length)
61
+ animation.addAnimation(
62
+ createAnimation()
63
+ .addElement(buttons)
64
+ .keyframes([
65
+ { offset: 0, opacity: 1 },
66
+ // Finish the exit before the incoming title occupies this toolbar.
67
+ { offset: 0.5, opacity: 0 },
68
+ { offset: 1, opacity: 0 },
69
+ ]),
70
+ );
71
+ return animation;
72
+ });
@@ -1,3 +1,4 @@
1
+ import type { NativeUITransitionRelease } from '@rdlabo/capacitor-native-ui/integration';
1
2
  import type { Animation } from '@ionic/core/components';
2
3
  import { hasNativeUIIntegration, suspendNativeUI, VERTICAL_BARS_TRANSITION_CANCELED } from '@rdlabo/capacitor-native-ui/integration';
3
4
  import type { AnimationBuilder } from '@ionic/core/components';
@@ -11,7 +12,7 @@ export const withNativeUITransition =
11
12
  (builder: AnimationBuilder): AnimationBuilder =>
12
13
  (baseEl, opts) => {
13
14
  const animation = builder(baseEl, opts);
14
- if (opts?.enteringEl) connectNativeUITransition(animation, opts.enteringEl, opts.leavingEl);
15
+ if (opts?.enteringEl) connectNativeUITransition(animation, opts.enteringEl, opts.leavingEl, opts.direction);
15
16
  return animation;
16
17
  };
17
18
 
@@ -19,7 +20,12 @@ const connectedTransitions = new WeakSet<Animation>();
19
20
  const cancellationTransitions = new WeakSet<Animation>();
20
21
 
21
22
  /** Ionic write hooks cannot await the bridge. Gate playback, including interactive playback. */
22
- export const connectNativeUITransition = (animation: Animation, entering: HTMLElement, leaving?: HTMLElement) => {
23
+ export const connectNativeUITransition = (
24
+ animation: Animation,
25
+ entering: HTMLElement,
26
+ leaving?: HTMLElement,
27
+ direction?: 'forward' | 'back' | 'root',
28
+ ) => {
23
29
  if (leaving && !cancellationTransitions.has(animation)) {
24
30
  cancellationTransitions.add(animation);
25
31
  animation.onFinish((step) => {
@@ -34,18 +40,42 @@ export const connectNativeUITransition = (animation: Animation, entering: HTMLEl
34
40
  const progressStep = animation.progressStep.bind(animation);
35
41
  const progressEnd = animation.progressEnd.bind(animation);
36
42
  const destroy = animation.destroy.bind(animation);
37
- let release: ((canceled?: boolean) => void) | undefined;
43
+ let release: NativeUITransitionRelease | undefined;
44
+ let frame = 0;
45
+ const win = entering.ownerDocument.defaultView;
46
+ const webAnimations = (value: Animation): globalThis.Animation[] => [
47
+ ...value.getWebAnimations(),
48
+ ...value.childAnimations.flatMap(webAnimations),
49
+ ];
50
+ const track = () => {
51
+ if (!release?.progress || !win) return;
52
+ const sample = () => {
53
+ const progress = webAnimations(animation)
54
+ .map((value) => value.effect?.getComputedTiming().progress)
55
+ .find((value) => value != null);
56
+ if (progress != null) release?.progress?.(progress);
57
+ frame = win.requestAnimationFrame(sample);
58
+ };
59
+ frame = win.requestAnimationFrame(sample);
60
+ };
38
61
  let preparation: Promise<void> | undefined;
39
62
  let disposed = false;
40
63
  let interactiveReady = false;
41
64
  let step: number | undefined;
42
65
  let end: Parameters<Animation['progressEnd']> | undefined;
43
- const prepare = () =>
44
- (preparation ??= suspendNativeUI(scopes).then((resume) => {
66
+ const prepare = (interactive = false) =>
67
+ (preparation ??= suspendNativeUI(scopes, {
68
+ navigation: true,
69
+ ...(direction ? { direction } : {}),
70
+ ...(interactive ? { interactive: true } : {}),
71
+ }).then((resume) => {
45
72
  if (disposed) resume(true);
46
73
  else release = resume;
47
74
  }));
48
75
  const finish = (canceled = false) => {
76
+ if (frame) win?.cancelAnimationFrame(frame);
77
+ frame = 0;
78
+ release?.progress?.(canceled ? 0 : 1);
49
79
  release?.(canceled);
50
80
  release = undefined;
51
81
  preparation = undefined;
@@ -55,7 +85,9 @@ export const connectNativeUITransition = (animation: Animation, entering: HTMLEl
55
85
  await prepare();
56
86
  if (disposed) return;
57
87
  try {
58
- await play(options);
88
+ const playing = play(options);
89
+ track();
90
+ await playing;
59
91
  } finally {
60
92
  finish();
61
93
  }
@@ -64,23 +96,36 @@ export const connectNativeUITransition = (animation: Animation, entering: HTMLEl
64
96
  interactiveReady = false;
65
97
  step = undefined;
66
98
  end = undefined;
67
- void prepare().then(() => {
99
+ void prepare(true).then(() => {
68
100
  if (disposed) return;
69
101
  progressStart(...args);
102
+ track();
70
103
  interactiveReady = true;
71
- if (step !== undefined) progressStep(step);
72
- if (end) progressEnd(...end);
104
+ if (step !== undefined) {
105
+ progressStep(step);
106
+ release?.progress?.(step);
107
+ }
108
+ if (end) {
109
+ release?.finishGesture?.(end[0] === 1);
110
+ progressEnd(...end);
111
+ }
73
112
  });
74
113
  return animation;
75
114
  };
76
115
  animation.progressStep = (value) => {
77
116
  step = value;
78
- if (interactiveReady) progressStep(value);
117
+ if (interactiveReady) {
118
+ progressStep(value);
119
+ release?.progress?.(value);
120
+ }
79
121
  return animation;
80
122
  };
81
123
  animation.progressEnd = (...args) => {
82
124
  end = args;
83
- if (interactiveReady) progressEnd(...args);
125
+ if (interactiveReady) {
126
+ release?.finishGesture?.(args[0] === 1);
127
+ progressEnd(...args);
128
+ }
84
129
  return animation;
85
130
  };
86
131
  animation.destroy = (...args) => {