drawnui-react 0.1.0-preview.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/PARITY.md +287 -0
  3. package/README.md +92 -0
  4. package/SKIPPED.md +107 -0
  5. package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
  6. package/dist/controls/AnimatedFramesRenderer.js +133 -0
  7. package/dist/controls/GridStructure.d.ts +79 -0
  8. package/dist/controls/GridStructure.js +403 -0
  9. package/dist/controls/RefreshIndicator.d.ts +35 -0
  10. package/dist/controls/RefreshIndicator.js +82 -0
  11. package/dist/controls/SkiaBackdrop.d.ts +25 -0
  12. package/dist/controls/SkiaBackdrop.js +80 -0
  13. package/dist/controls/SkiaButton.d.ts +58 -0
  14. package/dist/controls/SkiaButton.js +148 -0
  15. package/dist/controls/SkiaCarousel.d.ts +133 -0
  16. package/dist/controls/SkiaCarousel.js +603 -0
  17. package/dist/controls/SkiaCheckbox.d.ts +21 -0
  18. package/dist/controls/SkiaCheckbox.js +118 -0
  19. package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
  20. package/dist/controls/SkiaDecoratedGrid.js +46 -0
  21. package/dist/controls/SkiaDrawer.d.ts +46 -0
  22. package/dist/controls/SkiaDrawer.js +212 -0
  23. package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
  24. package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
  25. package/dist/controls/SkiaEditor.d.ts +163 -0
  26. package/dist/controls/SkiaEditor.js +803 -0
  27. package/dist/controls/SkiaGif.d.ts +54 -0
  28. package/dist/controls/SkiaGif.js +206 -0
  29. package/dist/controls/SkiaHotspot.d.ts +13 -0
  30. package/dist/controls/SkiaHotspot.js +36 -0
  31. package/dist/controls/SkiaImage.d.ts +75 -0
  32. package/dist/controls/SkiaImage.js +222 -0
  33. package/dist/controls/SkiaImageTiles.d.ts +27 -0
  34. package/dist/controls/SkiaImageTiles.js +76 -0
  35. package/dist/controls/SkiaLabel.d.ts +160 -0
  36. package/dist/controls/SkiaLabel.js +580 -0
  37. package/dist/controls/SkiaLayout.d.ts +163 -0
  38. package/dist/controls/SkiaLayout.js +723 -0
  39. package/dist/controls/SkiaLottie.d.ts +75 -0
  40. package/dist/controls/SkiaLottie.js +290 -0
  41. package/dist/controls/SkiaProgress.d.ts +40 -0
  42. package/dist/controls/SkiaProgress.js +80 -0
  43. package/dist/controls/SkiaRadioButton.d.ts +32 -0
  44. package/dist/controls/SkiaRadioButton.js +157 -0
  45. package/dist/controls/SkiaRichLabel.d.ts +77 -0
  46. package/dist/controls/SkiaRichLabel.js +274 -0
  47. package/dist/controls/SkiaScroll.d.ts +231 -0
  48. package/dist/controls/SkiaScroll.js +958 -0
  49. package/dist/controls/SkiaScrollBar.d.ts +56 -0
  50. package/dist/controls/SkiaScrollBar.js +158 -0
  51. package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
  52. package/dist/controls/SkiaShaderCarousel.js +246 -0
  53. package/dist/controls/SkiaShape.d.ts +63 -0
  54. package/dist/controls/SkiaShape.js +317 -0
  55. package/dist/controls/SkiaSlider.d.ts +74 -0
  56. package/dist/controls/SkiaSlider.js +243 -0
  57. package/dist/controls/SkiaSprite.d.ts +99 -0
  58. package/dist/controls/SkiaSprite.js +324 -0
  59. package/dist/controls/SkiaSpriteSet.d.ts +27 -0
  60. package/dist/controls/SkiaSpriteSet.js +75 -0
  61. package/dist/controls/SkiaSvg.d.ts +43 -0
  62. package/dist/controls/SkiaSvg.js +181 -0
  63. package/dist/controls/SkiaSwitch.d.ts +26 -0
  64. package/dist/controls/SkiaSwitch.js +147 -0
  65. package/dist/controls/SkiaToggle.d.ts +50 -0
  66. package/dist/controls/SkiaToggle.js +71 -0
  67. package/dist/controls/SnappingLayout.d.ts +78 -0
  68. package/dist/controls/SnappingLayout.js +217 -0
  69. package/dist/controls/TextSpan.d.ts +70 -0
  70. package/dist/controls/TextSpan.js +76 -0
  71. package/dist/core/Accessibility.d.ts +104 -0
  72. package/dist/core/Accessibility.js +150 -0
  73. package/dist/core/Animators.d.ts +82 -0
  74. package/dist/core/Animators.js +247 -0
  75. package/dist/core/Canvas.d.ts +89 -0
  76. package/dist/core/Canvas.js +328 -0
  77. package/dist/core/ControlStyle.d.ts +6 -0
  78. package/dist/core/ControlStyle.js +11 -0
  79. package/dist/core/Easing.d.ts +13 -0
  80. package/dist/core/Easing.js +15 -0
  81. package/dist/core/Gestures.d.ts +74 -0
  82. package/dist/core/Gestures.js +104 -0
  83. package/dist/core/ImageEffects.d.ts +29 -0
  84. package/dist/core/ImageEffects.js +55 -0
  85. package/dist/core/KeyboardManager.d.ts +25 -0
  86. package/dist/core/KeyboardManager.js +54 -0
  87. package/dist/core/ScrollAnimators.d.ts +90 -0
  88. package/dist/core/ScrollAnimators.js +244 -0
  89. package/dist/core/SkiaControl.d.ts +401 -0
  90. package/dist/core/SkiaControl.js +1239 -0
  91. package/dist/core/SkiaEffect.d.ts +216 -0
  92. package/dist/core/SkiaEffect.js +571 -0
  93. package/dist/core/SkiaImageManager.d.ts +43 -0
  94. package/dist/core/SkiaImageManager.js +122 -0
  95. package/dist/core/Super.d.ts +61 -0
  96. package/dist/core/Super.js +178 -0
  97. package/dist/core/TextInputProxy.d.ts +31 -0
  98. package/dist/core/TextInputProxy.js +144 -0
  99. package/dist/core/Types.d.ts +177 -0
  100. package/dist/core/Types.js +122 -0
  101. package/dist/core/ViewsAdapter.d.ts +41 -0
  102. package/dist/core/ViewsAdapter.js +113 -0
  103. package/dist/index.d.ts +48 -0
  104. package/dist/index.js +48 -0
  105. package/dist/react/SkiaShell.d.ts +144 -0
  106. package/dist/react/SkiaShell.js +490 -0
  107. package/dist/react/index.d.ts +110 -0
  108. package/dist/react/index.js +158 -0
  109. package/dist/react/reconciler.d.ts +14 -0
  110. package/dist/react/reconciler.js +160 -0
  111. package/package.json +60 -0
@@ -0,0 +1,144 @@
1
+ import { type ReactNode } from "react";
2
+ import { Thickness } from "../core/Types";
3
+ import type { SkiaControl } from "../core/SkiaControl";
4
+ /** Arguments passed to a page (C# GoToAsync arguments / query string), e.g. `GoToAsync("detail", true, { id: 42 })` or `GoToAsync("detail?id=42")`. */
5
+ export type ShellArguments = Record<string, unknown>;
6
+ /** Route name -> page factory receiving the navigation arguments. Pages are plain JSX, rendered inside the shell when navigated to. */
7
+ export type ShellRoutes = Record<string, (args: ShellArguments) => ReactNode>;
8
+ /** C# NavigationSource. */
9
+ export type NavigationSource = "Push" | "Pop";
10
+ /** C# SkiaShellNavigatingArgs: set Cancel to stop the navigation. */
11
+ export interface SkiaShellNavigatingArgs {
12
+ Route: string;
13
+ Source: NavigationSource;
14
+ Cancel: boolean;
15
+ View?: SkiaControl;
16
+ Previous?: SkiaControl;
17
+ }
18
+ /** C# SkiaShellNavigatedArgs. */
19
+ export interface SkiaShellNavigatedArgs {
20
+ Route: string;
21
+ Source: NavigationSource;
22
+ View?: SkiaControl;
23
+ }
24
+ /** "name?a=1&b=2" -> name + arguments (query values are strings). */
25
+ export declare function SplitRoute(route: string): {
26
+ name: string;
27
+ args: ShellArguments;
28
+ };
29
+ /** name + arguments -> "name?a=1" (only string / number / boolean values travel in the route, the rest stays in memory). */
30
+ export declare function BuildRoute(name: string, args?: ShellArguments): string;
31
+ /** C# OpenPopupAsync parameters. */
32
+ export interface PopupOptions {
33
+ animated?: boolean;
34
+ /** A tap outside the content closes the popup (default true). */
35
+ closeWhenBackgroundTapped?: boolean;
36
+ /** Dim + blur the page behind (default true). */
37
+ showOverlay?: boolean;
38
+ backgroundColor?: string;
39
+ }
40
+ /** C# PushModalAsync parameters. */
41
+ export interface ModalOptions {
42
+ /** The modal can be dragged down to close (default false). */
43
+ useGestures?: boolean;
44
+ animated?: boolean;
45
+ /** Dim + blur the page behind (C# freezeBackground; the frozen screenshot is not ported). */
46
+ freezeBackground?: boolean;
47
+ }
48
+ /** A shell tab: its root route and the label shown in the tab bar. */
49
+ export interface ShellTab {
50
+ route: string;
51
+ title: string;
52
+ }
53
+ /** Navigation API, same verbs as DrawnUi SkiaShell. */
54
+ export interface ShellNavigation {
55
+ /** Pushes a registered route; `route` may carry a query ("detail?id=42") and/or explicit arguments (C# GoToAsync(route, animate, arguments)). */
56
+ GoToAsync(route: string, animated?: boolean, args?: ShellArguments): Promise<void>;
57
+ /** C# GoBack: closes the top popup, else the top modal, else pops the page. */
58
+ GoBackAsync(animated?: boolean): Promise<void>;
59
+ PopToRootAsync(): Promise<void>;
60
+ /** Routes below the current page of the current tab, root excluded. */
61
+ NavigationStack: readonly string[];
62
+ CanGoBack: boolean;
63
+ /** Current route (with its query), "" for the root content. */
64
+ Route: string;
65
+ /** Arguments of the current page (query + explicit). */
66
+ Arguments: ShellArguments;
67
+ OpenPopupAsync(content: ReactNode, options?: PopupOptions): Promise<void>;
68
+ ClosePopupAsync(animated?: boolean): Promise<void>;
69
+ CloseAllPopups(): Promise<void>;
70
+ PushModalAsync(content: ReactNode, options?: ModalOptions): Promise<void>;
71
+ PopModalAsync(animated?: boolean): Promise<void>;
72
+ ShowToast(content: string | ReactNode, msShowTime?: number): void;
73
+ CloseAllToasts(): Promise<void>;
74
+ PopupsCount: number;
75
+ ModalsCount: number;
76
+ ToastsCount: number;
77
+ /** Tabs (C# SkiaViewSwitcher.SelectedIndex): each tab keeps its own page stack. */
78
+ SelectedTab: number;
79
+ SelectTabAsync(index: number): Promise<void>;
80
+ /** Pops every page of the current tab (C# PopTabToRoot). */
81
+ PopTabToRootAsync(): Promise<void>;
82
+ }
83
+ /** Navigation for pages and buttons rendered inside a SkiaShell. */
84
+ export declare function useShell(): ShellNavigation;
85
+ export interface SkiaShellProps {
86
+ Routes: ShellRoutes;
87
+ /** Root content shown when the stack is empty (ignored when Tabs are set: the selected tab's route is the root). */
88
+ children?: ReactNode;
89
+ /** Nav bar height in points when a page is open. */
90
+ NavBarHeight?: number;
91
+ NavBarColor?: string;
92
+ /** Title shown in the nav bar; defaults to the route name. */
93
+ Titles?: Record<string, string>;
94
+ /** Page push/pop slide duration in ms (C# SkiaViewSwitcher.PagesAnimationSpeed). */
95
+ PagesAnimationSpeed?: number;
96
+ /** Opaque background of pushed pages (they slide over the page below). */
97
+ PageBackgroundColor?: string;
98
+ /** Bottom tab bar: one root route per tab, per-tab navigation stacks (C# SkiaViewSwitcher tabs). */
99
+ Tabs?: ShellTab[];
100
+ TabBarHeight?: number;
101
+ TabBarColor?: string;
102
+ /** Mirror pages in the URL hash (#/route/route) and honour the browser back button (React extension, default true). */
103
+ UseBrowserHistory?: boolean;
104
+ /** Slide + fade between tabs (C# SkiaViewSwitcher.AnimateTabs, default false). */
105
+ AnimateTabs?: boolean;
106
+ /** Tab switch duration in ms (C# TabsAnimationSpeed). */
107
+ TabsAnimationSpeed?: number;
108
+ /** Before a page / popup / modal is pushed or popped; set e.Cancel = true to stop it (C# Navigating). */
109
+ Navigating?: (e: SkiaShellNavigatingArgs) => void;
110
+ /** After a page / popup / modal went upfront or was removed (C# Navigated). */
111
+ Navigated?: (e: SkiaShellNavigatedArgs) => void;
112
+ /** The current route changed (C# RouteChanged). */
113
+ RouteChanged?: (route: string) => void;
114
+ /** Safe-area insets (points) reserved above the nav bar and below the tab bar; default Super.Insets (C# Super.Insets). */
115
+ Insets?: Thickness;
116
+ }
117
+ /** C# SkiaShell static defaults (mutable). */
118
+ export declare const ShellDefaults: {
119
+ ToastBackgroundColor: string;
120
+ ToastTextColor: "#FFFFFF";
121
+ ToastTextFont: string;
122
+ ToastTextSize: number;
123
+ ToastTextMargins: number;
124
+ /** Default overlay tint behind popups / modals. */
125
+ PopupBackgroundColor: string;
126
+ /** Backdrop blur behind popups / modals, points. */
127
+ PopupsBackgroundBlur: number;
128
+ PopupsAnimationSpeed: number;
129
+ PopupsCancelAnimationsAfterMs: number;
130
+ ZIndexModals: number;
131
+ ZIndexPopups: number;
132
+ ZIndexToasts: number;
133
+ /** Accent of the selected tab. */
134
+ TabSelectedColor: string;
135
+ TabColor: string;
136
+ };
137
+ /**
138
+ * Mirrors DrawnUi SkiaShell at the React level: a root page (or tabs) plus per-tab stacks of routed pages drawn
139
+ * inside one SkiaLayer with a nav bar (pages slide in/out like SkiaViewSwitcher), popups (blurred + dimmed
140
+ * backdrop, scale + fade, tap outside to close), modals (full-screen SkiaDrawer from the bottom, optionally
141
+ * draggable), toasts (bottom banner, slide + fade, auto close). Pages live in the URL hash and the browser back
142
+ * button behaves like C# GoBack (popup, then modal, then page). Navigation via ref or useShell().
143
+ */
144
+ export declare const SkiaShell: import("react").ForwardRefExoticComponent<SkiaShellProps & import("react").RefAttributes<ShellNavigation>>;
@@ -0,0 +1,490 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
3
+ import { Colors, Thickness } from "../core/Types";
4
+ import { Super } from "../core/Super";
5
+ import { Easing } from "../core/Easing";
6
+ import { SkiaBackdrop, SkiaButton, SkiaDrawer, SkiaGrid, SkiaLabel, SkiaLayer, SkiaRichLabel, SkiaShape } from "./index";
7
+ /** "name?a=1&b=2" -> name + arguments (query values are strings). */
8
+ export function SplitRoute(route) {
9
+ const q = route.indexOf("?");
10
+ if (q < 0)
11
+ return { name: route, args: {} };
12
+ const args = {};
13
+ new URLSearchParams(route.slice(q + 1)).forEach((v, k) => { args[k] = v; });
14
+ return { name: route.slice(0, q), args };
15
+ }
16
+ /** name + arguments -> "name?a=1" (only string / number / boolean values travel in the route, the rest stays in memory). */
17
+ export function BuildRoute(name, args) {
18
+ if (!args)
19
+ return name;
20
+ const p = new URLSearchParams();
21
+ for (const [k, v] of Object.entries(args))
22
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean")
23
+ p.set(k, String(v));
24
+ const q = p.toString();
25
+ return q ? `${name}?${q}` : name;
26
+ }
27
+ const ShellContext = createContext(null);
28
+ /** Navigation for pages and buttons rendered inside a SkiaShell. */
29
+ export function useShell() {
30
+ const shell = useContext(ShellContext);
31
+ if (!shell)
32
+ throw new Error("DrawnUi: useShell() must be used inside <SkiaShell>");
33
+ return shell;
34
+ }
35
+ /** C# SkiaViewSwitcher.Custom easing (back-ease, side coefficient 0.55) used for tab switches. */
36
+ const TabsEasing = new Easing((x) => (x - 1) * (x - 1) * ((0.55 + 1) * (x - 1) + 0.55) + 1);
37
+ /** C# SkiaShell static defaults (mutable). */
38
+ export const ShellDefaults = {
39
+ ToastBackgroundColor: "#CC000000",
40
+ ToastTextColor: Colors.White,
41
+ ToastTextFont: "",
42
+ ToastTextSize: 16,
43
+ ToastTextMargins: 24,
44
+ /** Default overlay tint behind popups / modals. */
45
+ PopupBackgroundColor: "#66000000",
46
+ /** Backdrop blur behind popups / modals, points. */
47
+ PopupsBackgroundBlur: 6,
48
+ PopupsAnimationSpeed: 250,
49
+ PopupsCancelAnimationsAfterMs: 1500,
50
+ ZIndexModals: 1000,
51
+ ZIndexPopups: 2000,
52
+ ZIndexToasts: 3000,
53
+ /** Accent of the selected tab. */
54
+ TabSelectedColor: "#6EA8FE",
55
+ TabColor: "#ADB5BD",
56
+ };
57
+ /** Canvas width in points for a control (page slide distance); falls back to the window. */
58
+ function CanvasWidthPts(ctrl) {
59
+ const el = ctrl.Superview?.Element;
60
+ return el ? el.clientWidth : window.innerWidth;
61
+ }
62
+ /** Resolves once pred() is truthy (a ref set by the React commit) or after timeout ms. */
63
+ async function WaitFor(pred, timeout = 1000) {
64
+ const started = performance.now();
65
+ for (;;) {
66
+ const v = pred();
67
+ if (v)
68
+ return v;
69
+ if (performance.now() - started > timeout)
70
+ return undefined;
71
+ await new Promise((r) => setTimeout(r, 16));
72
+ }
73
+ }
74
+ function AnimateWithTimeout(promise, ms = ShellDefaults.PopupsCancelAnimationsAfterMs) {
75
+ return Promise.race([promise.catch(() => undefined), new Promise((r) => setTimeout(r, ms))]).then(() => undefined);
76
+ }
77
+ const HISTORY_KEY = "drawnui-shell";
78
+ /**
79
+ * Mirrors DrawnUi SkiaShell at the React level: a root page (or tabs) plus per-tab stacks of routed pages drawn
80
+ * inside one SkiaLayer with a nav bar (pages slide in/out like SkiaViewSwitcher), popups (blurred + dimmed
81
+ * backdrop, scale + fade, tap outside to close), modals (full-screen SkiaDrawer from the bottom, optionally
82
+ * draggable), toasts (bottom banner, slide + fade, auto close). Pages live in the URL hash and the browser back
83
+ * button behaves like C# GoBack (popup, then modal, then page). Navigation via ref or useShell().
84
+ */
85
+ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBarHeight = 56, NavBarColor = "#212529", Titles, PagesAnimationSpeed = 200, PageBackgroundColor = "#212529", Tabs, TabBarHeight = 56, TabBarColor = "#212529", UseBrowserHistory = true, AnimateTabs = false, TabsAnimationSpeed = 150, Navigating, Navigated, RouteChanged, Insets }, ref) {
86
+ const hasTabs = !!Tabs && Tabs.length > 0;
87
+ const [systemInsets, setSystemInsets] = useState(() => Super.Insets);
88
+ useEffect(() => Super.OnInsetsChanged(() => setSystemInsets(Super.Insets)), []);
89
+ const insets = Insets ?? systemInsets;
90
+ const argsInMemory = useRef(new Map()); // arguments that cannot travel in the route string
91
+ const RenderRoute = useCallback((r) => {
92
+ const { name, args } = SplitRoute(r);
93
+ const factory = Routes[name];
94
+ return factory ? factory({ ...args, ...argsInMemory.current.get(r) }) : null;
95
+ }, [Routes]);
96
+ const RouteName = (r) => SplitRoute(r).name;
97
+ const events = useRef({ Navigating, Navigated, RouteChanged });
98
+ events.current = { Navigating, Navigated, RouteChanged };
99
+ /** C# NotifyAndCheckCanNavigate: raises Navigating, false when cancelled. */
100
+ const canNavigate = useCallback((r, source, view, previous) => {
101
+ const e = { Route: r ?? "", Source: source, Cancel: false, View: view, Previous: previous };
102
+ events.current.Navigating?.(e);
103
+ return !e.Cancel;
104
+ }, []);
105
+ const navigated = useCallback((r, source, view) => { events.current.Navigated?.({ Route: r ?? "", Source: source, View: view }); }, []);
106
+ const [selectedTab, setSelectedTab] = useState(0);
107
+ /** The tab root leaving during an animated switch (C# PreviousVisibleView) and the slide direction. */
108
+ const [tabTransition, setTabTransition] = useState(null);
109
+ const tabRoots = useRef(new Map());
110
+ const [stacks, setStacks] = useState(() => [[]]);
111
+ const stack = stacks[selectedTab] ?? [];
112
+ const setStack = useCallback((update) => setStacks((all) => { const next = all.slice(); next[selectedTab] = update(next[selectedTab] ?? []); return next; }), [selectedTab]);
113
+ const [leaving, setLeaving] = useState(null); // page sliding out during a pop
114
+ const [transitions, setTransitions] = useState(0); // pages sliding in: the page below stays visible meanwhile
115
+ const [popups, setPopups] = useState([]);
116
+ const [modals, setModals] = useState([]);
117
+ const [toasts, setToasts] = useState([]);
118
+ const nextId = useRef(1);
119
+ const pageCtrls = useRef(new Map());
120
+ const route = stack[stack.length - 1] ?? "";
121
+ useEffect(() => { events.current.RouteChanged?.(route); }, [route]);
122
+ // latest values for callbacks fired from history events
123
+ const live = useRef({ stack, popups, modals, selectedTab });
124
+ live.current = { stack, popups, modals, selectedTab };
125
+ // ---- browser history: one entry per pushed page / popup / modal, popped in C# GoBack order ----
126
+ const history = useRef([]);
127
+ const historyOn = UseBrowserHistory && typeof window !== "undefined";
128
+ const hashFor = (s) => (s.length ? "#/" + s.map(encodeURIComponent).join("/") : window.location.pathname + window.location.search);
129
+ const pushHistory = useCallback((kind, s) => {
130
+ if (!historyOn)
131
+ return;
132
+ history.current.push(kind);
133
+ window.history.pushState({ [HISTORY_KEY]: history.current.length }, "", kind === "page" ? hashFor(s) : undefined);
134
+ }, [historyOn]);
135
+ /** A programmatic close of something that has a history entry goes through history.back(), so the URL stays in sync. */
136
+ const backThroughHistory = useCallback((kind) => {
137
+ if (!historyOn || history.current[history.current.length - 1] !== kind)
138
+ return false;
139
+ window.history.back();
140
+ return true;
141
+ }, [historyOn]);
142
+ // ---- pages ----
143
+ const slideOut = useCallback(async (current, animated) => {
144
+ const ctrl = pageCtrls.current.get(current);
145
+ if (animated && ctrl) {
146
+ setLeaving(current);
147
+ await AnimateWithTimeout(ctrl.TranslateToAsync(CanvasWidthPts(ctrl), 0, PagesAnimationSpeed), PagesAnimationSpeed + 500);
148
+ setLeaving(null);
149
+ }
150
+ }, [PagesAnimationSpeed]);
151
+ /** Pops the top page; false when Navigating cancelled it (C# NavigationSource.Pop). */
152
+ const popPage = useCallback(async (animated) => {
153
+ const s = live.current.stack;
154
+ const current = s[s.length - 1];
155
+ if (!current)
156
+ return false;
157
+ const below = s[s.length - 2] ?? "";
158
+ if (!canNavigate(below, "Pop", pageCtrls.current.get(current), pageCtrls.current.get(current)))
159
+ return false;
160
+ await slideOut(current, animated);
161
+ setStack((x) => x.slice(0, -1));
162
+ argsInMemory.current.delete(current);
163
+ navigated(below, "Pop", pageCtrls.current.get(below));
164
+ return true;
165
+ }, [slideOut, setStack, canNavigate, navigated]);
166
+ const GoToAsync = useCallback(async (r, animated = true, args) => {
167
+ const { name, args: fromRoute } = SplitRoute(r);
168
+ if (!Routes[name])
169
+ throw new Error(`DrawnUi: route '${name}' is not registered in SkiaShell.Routes`);
170
+ const full = BuildRoute(name, { ...fromRoute, ...args });
171
+ if (!canNavigate(full, "Push", undefined, pageCtrls.current.get(live.current.stack[live.current.stack.length - 1] ?? "")))
172
+ return;
173
+ if (args)
174
+ argsInMemory.current.set(full, { ...fromRoute, ...args });
175
+ const next = [...live.current.stack, full];
176
+ setStack(() => next);
177
+ pushHistory("page", next);
178
+ if (animated) {
179
+ setTransitions((t) => t + 1);
180
+ await new Promise((res) => setTimeout(res, PagesAnimationSpeed + 50));
181
+ setTransitions((t) => t - 1);
182
+ }
183
+ navigated(full, "Push", await WaitFor(() => pageCtrls.current.get(full)));
184
+ }, [Routes, PagesAnimationSpeed, setStack, pushHistory, canNavigate, navigated]);
185
+ // ---- popups ----
186
+ const closePopupNow = useCallback(async (p, animated) => {
187
+ if (p.closing)
188
+ return false;
189
+ if (!canNavigate(live.current.stack[live.current.stack.length - 1] ?? "", "Pop", p.content, p.content))
190
+ return false;
191
+ p.closing = true;
192
+ if (animated && p.animated !== false && p.ctrl && p.content) {
193
+ await AnimateWithTimeout(Promise.all([p.ctrl.FadeToAsync(0, ShellDefaults.PopupsAnimationSpeed), p.content.ScaleToAsync(0, 0, ShellDefaults.PopupsAnimationSpeed)]));
194
+ }
195
+ setPopups((list) => list.filter((x) => x !== p));
196
+ navigated(live.current.stack[live.current.stack.length - 1] ?? "", "Pop");
197
+ return true;
198
+ }, [canNavigate, navigated]);
199
+ const closePopup = useCallback(async (p, animated) => {
200
+ if (p.closing)
201
+ return;
202
+ if (p.history && backThroughHistory("popup")) {
203
+ p.history = false;
204
+ return;
205
+ } // popstate closes it
206
+ await closePopupNow(p, animated);
207
+ }, [backThroughHistory, closePopupNow]);
208
+ const OpenPopupAsync = useCallback(async (content, options) => {
209
+ const animated = options?.animated ?? true;
210
+ if (!canNavigate(live.current.stack[live.current.stack.length - 1] ?? "", "Push"))
211
+ return;
212
+ const entry = { id: nextId.current++, node: null, animated };
213
+ const closeWhenBackgroundTapped = options?.closeWhenBackgroundTapped ?? true;
214
+ const showOverlay = options?.showOverlay ?? true;
215
+ const overlay = options?.backgroundColor ?? ShellDefaults.PopupBackgroundColor;
216
+ entry.node = (_jsxs(SkiaLayer, { VerticalOptions: "Fill", HorizontalOptions: "Fill", ZIndex: ShellDefaults.ZIndexPopups + entry.id, BlockGesturesBelow: true, Opacity: animated ? 0.1 : 1, ref: (c) => { if (c)
217
+ entry.ctrl = c; }, Tapped: (_, e) => {
218
+ // C# PopupWrapper: a tap outside the content closes it
219
+ const c = entry.content;
220
+ const l = e.ProcessingInfo.MappedLocation, o = e.ProcessingInfo.ChildOffset;
221
+ if (closeWhenBackgroundTapped && c && !c.HitIsInside(l.X + o.X, l.Y + o.Y))
222
+ void closePopup(entry, animated);
223
+ }, children: [showOverlay && _jsx(SkiaBackdrop, { Blur: ShellDefaults.PopupsBackgroundBlur, BackgroundColor: overlay, InputTransparent: true }), _jsx(SkiaLayer, { HorizontalOptions: "Center", VerticalOptions: "Center", Scale: animated ? 0.5 : 1, ref: (c) => { if (c)
224
+ entry.content = c; }, children: content })] }, entry.id));
225
+ setPopups((list) => [...list, entry]);
226
+ entry.history = true;
227
+ pushHistory("popup", live.current.stack);
228
+ await WaitFor(() => entry.ctrl && entry.content); // mounted after the React commit
229
+ if (animated && entry.ctrl && entry.content)
230
+ await AnimateWithTimeout(Promise.all([entry.ctrl.FadeToAsync(1, ShellDefaults.PopupsAnimationSpeed), entry.content.ScaleToAsync(1, 1, ShellDefaults.PopupsAnimationSpeed)]));
231
+ navigated(live.current.stack[live.current.stack.length - 1] ?? "", "Push", entry.content);
232
+ }, [closePopup, pushHistory, canNavigate, navigated]);
233
+ const ClosePopupAsync = useCallback(async (animated = true) => { const list = live.current.popups; const top = list[list.length - 1]; if (top)
234
+ await closePopup(top, animated); }, [closePopup]);
235
+ const CloseAllPopups = useCallback(async () => { for (const p of [...live.current.popups].reverse())
236
+ await closePopup(p, false); }, [closePopup]);
237
+ // ---- modals ----
238
+ const removeModal = useCallback((m) => setModals((list) => list.filter((x) => x !== m)), []);
239
+ const popModalNow = useCallback(async (top, animated) => {
240
+ if (top.closing)
241
+ return false;
242
+ if (!canNavigate(live.current.stack[live.current.stack.length - 1] ?? "", "Pop", top.drawer, top.drawer))
243
+ return false;
244
+ top.closing = true;
245
+ if (top.drawer && top.drawer.IsOpen) {
246
+ top.drawer.Animated = animated;
247
+ const done = new Promise((r) => { const prev = top.drawer.StateTransitionComplete; top.drawer.StateTransitionComplete = (d, o) => { prev?.(d, o); if (!o)
248
+ r(); }; });
249
+ top.drawer.IsOpen = false;
250
+ if (animated)
251
+ await AnimateWithTimeout(done);
252
+ }
253
+ removeModal(top);
254
+ navigated(live.current.stack[live.current.stack.length - 1] ?? "", "Pop");
255
+ return true;
256
+ }, [removeModal, canNavigate, navigated]);
257
+ const PushModalAsync = useCallback(async (content, options) => {
258
+ const animated = options?.animated ?? true;
259
+ if (!canNavigate(live.current.stack[live.current.stack.length - 1] ?? "", "Push"))
260
+ return;
261
+ const entry = { id: nextId.current++, node: null };
262
+ const overlay = (options?.freezeBackground ?? true) ? ShellDefaults.PopupBackgroundColor : undefined;
263
+ let opened = () => { };
264
+ const openedPromise = new Promise((r) => { opened = r; });
265
+ // the user closed it (drag down / IsOpen = false): leave through history when it owns an entry, else remove
266
+ const userClosed = () => { if (entry.closing)
267
+ return; if (entry.history && backThroughHistory("modal"))
268
+ entry.history = false;
269
+ else {
270
+ entry.closing = true;
271
+ removeModal(entry);
272
+ } };
273
+ entry.node = (_jsxs(SkiaLayer, { VerticalOptions: "Fill", HorizontalOptions: "Fill", ZIndex: ShellDefaults.ZIndexModals + entry.id, BlockGesturesBelow: true, children: [overlay && _jsx(SkiaBackdrop, { Blur: ShellDefaults.PopupsBackgroundBlur, BackgroundColor: overlay, InputTransparent: true }), _jsx(SkiaDrawer, { ref: (d) => { if (d)
274
+ entry.drawer = d; }, Direction: "FromBottom", HeaderSize: 0, HorizontalOptions: "Fill", VerticalOptions: "Fill", RespondsToGestures: options?.useGestures ?? false, Animated: animated, Bounces: false, BlockGesturesBelow: true, StateTransitionComplete: (d, isOpen) => { if (isOpen)
275
+ opened();
276
+ else if (!d.IsOpen)
277
+ userClosed(); }, IsOpenChanged: (_, isOpen) => { if (!isOpen && !animated)
278
+ userClosed(); }, children: content })] }, entry.id));
279
+ setModals((list) => [...list, entry]);
280
+ entry.history = true;
281
+ pushHistory("modal", live.current.stack);
282
+ const drawer = await WaitFor(() => entry.drawer);
283
+ await WaitFor(() => drawer && drawer.DrawingRect.Height > 0); // first draw: the drawer measured its travel
284
+ if (drawer) {
285
+ drawer.IsOpen = true;
286
+ if (!animated)
287
+ opened();
288
+ }
289
+ await AnimateWithTimeout(openedPromise);
290
+ navigated(live.current.stack[live.current.stack.length - 1] ?? "", "Push", drawer);
291
+ }, [removeModal, pushHistory, backThroughHistory, canNavigate, navigated]);
292
+ const PopModalAsync = useCallback(async (animated = true) => {
293
+ const list = live.current.modals;
294
+ const top = list[list.length - 1];
295
+ if (!top || top.closing)
296
+ return;
297
+ if (top.history && backThroughHistory("modal")) {
298
+ top.history = false;
299
+ return;
300
+ }
301
+ await popModalNow(top, animated);
302
+ }, [popModalNow, backThroughHistory]);
303
+ // ---- toasts ----
304
+ const closeToast = useCallback(async (t, animated) => {
305
+ if (t.closing)
306
+ return;
307
+ t.closing = true;
308
+ const c = t.ctrl;
309
+ if (animated && c)
310
+ await AnimateWithTimeout(Promise.all([c.TranslateToAsync(0, c.DrawingRect.Height / c.RenderingScale, 250), c.FadeToAsync(0, 250)]));
311
+ setToasts((list) => list.filter((x) => x !== t));
312
+ }, []);
313
+ const toastsRef = useRef([]);
314
+ toastsRef.current = toasts;
315
+ const CloseAllToasts = useCallback(async () => { await Promise.all(toastsRef.current.map((t) => closeToast(t, false))); }, [closeToast]);
316
+ const ShowToast = useCallback((content, msShowTime = 4000) => {
317
+ void (async () => {
318
+ await CloseAllToasts();
319
+ const entry = { id: nextId.current++, node: null };
320
+ const body = typeof content === "string"
321
+ ? _jsx(SkiaRichLabel, { Text: content, TextColor: ShellDefaults.ToastTextColor, FontFamily: ShellDefaults.ToastTextFont || undefined, FontSize: ShellDefaults.ToastTextSize, Margin: new Thickness(ShellDefaults.ToastTextMargins), HorizontalOptions: "Fill" })
322
+ : content;
323
+ entry.node = (_jsx(SkiaLayer, { HorizontalOptions: "Fill", VerticalOptions: "End", ZIndex: ShellDefaults.ZIndexToasts + entry.id, BackgroundColor: ShellDefaults.ToastBackgroundColor, BlockGesturesBelow: true, UseCache: "Operations", Opacity: 0, ref: (c) => { if (c)
324
+ entry.ctrl = c; }, children: _jsx(SkiaLayer, { HorizontalOptions: "Fill", children: body }) }, entry.id));
325
+ setToasts((list) => [...list, entry]);
326
+ const c = await WaitFor(() => entry.ctrl);
327
+ await WaitFor(() => c && c.DrawingRect.Height > 0); // laid out: height known
328
+ if (c) {
329
+ c.TranslationY = c.DrawingRect.Height / c.RenderingScale;
330
+ void c.TranslateToAsync(0, 0, 300);
331
+ void c.FadeToAsync(1, 300);
332
+ }
333
+ setTimeout(() => void closeToast(entry, true), msShowTime);
334
+ })();
335
+ }, [CloseAllToasts, closeToast]);
336
+ // ---- back (C# GoBackDefault order) ----
337
+ const GoBackAsync = useCallback(async (animated = true) => {
338
+ const { popups: ps, modals: ms, stack: s } = live.current;
339
+ if (ps.length) {
340
+ await closePopup(ps[ps.length - 1], animated);
341
+ return;
342
+ }
343
+ if (ms.length) {
344
+ await PopModalAsync(animated);
345
+ return;
346
+ }
347
+ if (!s.length)
348
+ return;
349
+ if (backThroughHistory("page")) {
350
+ await WaitFor(() => live.current.stack.length < s.length, PagesAnimationSpeed + 800);
351
+ return;
352
+ }
353
+ await popPage(animated);
354
+ }, [closePopup, PopModalAsync, backThroughHistory, popPage, PagesAnimationSpeed]);
355
+ const PopToRootAsync = useCallback(async () => {
356
+ setLeaving(null);
357
+ if (historyOn) {
358
+ const pages = history.current.filter((k) => k === "page").length;
359
+ history.current = history.current.filter((k) => k !== "page");
360
+ if (pages)
361
+ window.history.replaceState({ [HISTORY_KEY]: history.current.length }, "", hashFor([]));
362
+ }
363
+ setStacks((all) => all.map(() => []));
364
+ }, [historyOn]);
365
+ const PopTabToRootAsync = useCallback(async () => { setLeaving(null); setStack(() => []); }, [setStack]);
366
+ const SelectTabAsync = useCallback(async (index) => {
367
+ if (!hasTabs)
368
+ return;
369
+ const to = Math.max(0, Math.min(Tabs.length - 1, index));
370
+ const from = live.current.selectedTab;
371
+ setLeaving(null);
372
+ if (to === from)
373
+ return;
374
+ if (!AnimateTabs) {
375
+ setSelectedTab(to);
376
+ return;
377
+ }
378
+ // C# SelectRightTab / SelectLeftTab: the new root slides in from 0.75 width with a fade, the old one slides out
379
+ const dir = to > from ? 1 : -1;
380
+ setTabTransition({ from, dir });
381
+ setSelectedTab(to);
382
+ const next = await WaitFor(() => tabRoots.current.get(to));
383
+ const prev = tabRoots.current.get(from);
384
+ if (next) {
385
+ const w = CanvasWidthPts(next);
386
+ await AnimateWithTimeout(Promise.all([
387
+ next.TranslateToAsync(0, 0, TabsAnimationSpeed, TabsEasing),
388
+ next.FadeToAsync(1, TabsAnimationSpeed, Easing.Linear),
389
+ prev ? prev.TranslateToAsync(-dir * w, 0, TabsAnimationSpeed, TabsEasing) : Promise.resolve(),
390
+ ]), TabsAnimationSpeed + 500);
391
+ }
392
+ setTabTransition(null);
393
+ }, [hasTabs, Tabs, AnimateTabs, TabsAnimationSpeed]);
394
+ // popstate: the browser popped OUR last entry (back) — or moved forward to a hash we can rebuild
395
+ useEffect(() => {
396
+ if (!historyOn)
397
+ return;
398
+ const parseHash = () => window.location.hash.replace(/^#\/?/, "").split("/").filter(Boolean).map(decodeURIComponent).filter((r) => !!Routes[RouteName(r)]);
399
+ // initial deep link
400
+ const initial = parseHash();
401
+ if (initial.length) {
402
+ setStacks((all) => { const next = all.slice(); next[live.current.selectedTab] = initial; return next; });
403
+ history.current = initial.map(() => "page");
404
+ window.history.replaceState({ [HISTORY_KEY]: history.current.length }, "", hashFor(initial));
405
+ }
406
+ const onPop = () => {
407
+ const depth = window.history.state?.[HISTORY_KEY] ?? 0;
408
+ if (depth < history.current.length) {
409
+ // back: unwind our entries down to depth
410
+ void (async () => {
411
+ while (history.current.length > depth) {
412
+ const kind = history.current.pop();
413
+ const { popups: ps, modals: ms } = live.current;
414
+ let done = true;
415
+ if (kind === "popup") {
416
+ const top = ps[ps.length - 1];
417
+ if (top) {
418
+ top.history = false;
419
+ done = await closePopupNow(top, true);
420
+ if (!done)
421
+ top.history = true;
422
+ }
423
+ }
424
+ else if (kind === "modal") {
425
+ const top = ms[ms.length - 1];
426
+ if (top) {
427
+ top.history = false;
428
+ done = await popModalNow(top, true);
429
+ if (!done)
430
+ top.history = true;
431
+ }
432
+ }
433
+ else if (kind === "page")
434
+ done = await popPage(true);
435
+ if (!done && kind) { // Navigating cancelled: put the browser entry back
436
+ history.current.push(kind);
437
+ window.history.pushState({ [HISTORY_KEY]: history.current.length }, "", kind === "page" ? hashFor(live.current.stack) : undefined);
438
+ break;
439
+ }
440
+ }
441
+ })();
442
+ }
443
+ else if (depth > history.current.length) {
444
+ // forward: the hash names the pages, rebuild the stack without animation (overlays cannot be restored)
445
+ const target = parseHash();
446
+ history.current = target.map(() => "page");
447
+ window.history.replaceState({ [HISTORY_KEY]: history.current.length }, "", hashFor(target));
448
+ setStacks((all) => { const next = all.slice(); next[live.current.selectedTab] = target; return next; });
449
+ }
450
+ };
451
+ window.addEventListener("popstate", onPop);
452
+ return () => window.removeEventListener("popstate", onPop);
453
+ }, [historyOn, Routes, closePopupNow, popModalNow, popPage]);
454
+ const nav = useMemo(() => ({
455
+ GoToAsync, GoBackAsync, PopToRootAsync, NavigationStack: stack, CanGoBack: stack.length > 0 || popups.length > 0 || modals.length > 0, Route: route, Arguments: { ...SplitRoute(route).args, ...argsInMemory.current.get(route) },
456
+ OpenPopupAsync, ClosePopupAsync, CloseAllPopups, PushModalAsync, PopModalAsync, ShowToast, CloseAllToasts,
457
+ PopupsCount: popups.length, ModalsCount: modals.length, ToastsCount: toasts.length,
458
+ SelectedTab: selectedTab, SelectTabAsync, PopTabToRootAsync,
459
+ }), [GoToAsync, GoBackAsync, PopToRootAsync, stack, route, OpenPopupAsync, ClosePopupAsync, CloseAllPopups, PushModalAsync, PopModalAsync, ShowToast, CloseAllToasts, popups.length, modals.length, toasts.length, selectedTab, SelectTabAsync, PopTabToRootAsync]);
460
+ useImperativeHandle(ref, () => nav, [nav]);
461
+ const visiblePages = leaving && !stack.includes(leaving) ? [...stack, leaving] : stack;
462
+ const topRoute = visiblePages[visiblePages.length - 1] ?? "";
463
+ const transitioning = transitions > 0 || leaving !== null;
464
+ const rootBottom = hasTabs ? TabBarHeight + insets.Bottom : 0;
465
+ const navTop = NavBarHeight + insets.Top;
466
+ const rootNode = hasTabs ? RenderRoute(Tabs[selectedTab]?.route ?? "") : children;
467
+ const entering = tabTransition ? { TranslationX: tabTransition.dir * 0.75 * (typeof window !== "undefined" ? window.innerWidth : 0), Opacity: 0.001, ZIndex: 1 } : {};
468
+ return (_jsx(ShellContext.Provider, { value: nav, children: _jsxs(SkiaLayer, { VerticalOptions: "Fill", children: [tabTransition && (_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, 0, 0, rootBottom), ZIndex: 0, BackgroundColor: PageBackgroundColor, ref: (c) => { if (c)
469
+ tabRoots.current.set(tabTransition.from, c); }, children: RenderRoute(Tabs[tabTransition.from]?.route ?? "") }, `tab-leaving-${tabTransition.from}`)), _jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, 0, 0, rootBottom), IsVisible: visiblePages.length === 0 || transitioning, BackgroundColor: hasTabs ? PageBackgroundColor : undefined, ...entering, ref: (c) => { if (c)
470
+ tabRoots.current.set(selectedTab, c); }, children: rootNode }, hasTabs ? `tab-${selectedTab}` : "root"), visiblePages.map((r, i) => (_jsxs(PageHost, { route: r, speed: PagesAnimationSpeed, visible: r === topRoute || transitioning, background: PageBackgroundColor, bottom: rootBottom, register: (c) => { if (c)
471
+ pageCtrls.current.set(r, c);
472
+ else
473
+ pageCtrls.current.delete(r); }, children: [_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, navTop, 0, 0), children: RenderRoute(r) }), _jsxs(SkiaLayer, { HeightRequest: navTop, BackgroundColor: NavBarColor, Padding: new Thickness(0, insets.Top, 0, 0), children: [_jsx(SkiaButton, { Text: "\u2039 Back", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, VerticalOptions: "Center", Margin: new Thickness(8, 0), ApplyEffect: "Ripple", Tapped: () => void GoBackAsync(), AccessibilityRole: "button", AccessibilityLabel: "Back" }), _jsx(SkiaLabel, { Text: Titles?.[RouteName(r)] ?? RouteName(r), FontSize: 18, FontFamily: "FontTextBold", TextColor: Colors.White, HorizontalOptions: "Fill", HorizontalTextAlignment: "Center", VerticalOptions: "Center", MaxLines: 1, Margin: new Thickness(96, 0), AccessibilityRole: "heading" }), _jsx(SkiaButton, { Text: "Home", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, HorizontalOptions: "End", VerticalOptions: "Center", Margin: new Thickness(0, 0, 8, 0), ApplyEffect: "Ripple", Tapped: () => void PopToRootAsync(), AccessibilityRole: "button", AccessibilityLabel: "Home" }), _jsx(SkiaLayer, { HeightRequest: 1, VerticalOptions: "End", BackgroundColor: "#343A40" })] })] }, `${selectedTab}:${r}#${i}`))), hasTabs && (_jsxs(SkiaLayer, { HeightRequest: TabBarHeight + insets.Bottom, VerticalOptions: "End", BackgroundColor: TabBarColor, ZIndex: ShellDefaults.ZIndexModals - 1, BlockGesturesBelow: true, Padding: new Thickness(0, 0, 0, insets.Bottom), children: [_jsx(SkiaLayer, { HeightRequest: 1, BackgroundColor: "#343A40" }), _jsx(SkiaGrid, { ColumnDefinitions: Tabs.map(() => "*").join(","), HorizontalOptions: "Fill", VerticalOptions: "Fill", children: Tabs.map((t, i) => (_jsxs(SkiaLayer, { Column: i, HorizontalOptions: "Fill", VerticalOptions: "Fill", AnimationTapped: "Ripple", Tapped: () => void SelectTabAsync(i), AccessibilityRole: "button", AccessibilityLabel: t.title, children: [_jsx(SkiaLabel, { Text: t.title, FontSize: 13, FontFamily: i === selectedTab ? "FontTextBold" : undefined, TextColor: i === selectedTab ? ShellDefaults.TabSelectedColor : ShellDefaults.TabColor, HorizontalOptions: "Center", VerticalOptions: "Center", AccessibilityRole: "presentation" }), i === selectedTab && _jsx(SkiaShape, { Type: "Rectangle", HeightRequest: 3, WidthRequest: 36, CornerRadius: 2, BackgroundColor: ShellDefaults.TabSelectedColor, HorizontalOptions: "Center", VerticalOptions: "End" })] }, t.route))) })] })), modals.map((m) => m.node), popups.map((p) => p.node), toasts.map((t) => t.node)] }) }));
474
+ });
475
+ /** A pushed page: slides in from the right on mount (C# SkiaViewSwitcher PushView), covers what is below. */
476
+ function PageHost({ route, speed, visible, background, bottom, register, children }) {
477
+ const ctrl = useRef(null);
478
+ useEffect(() => {
479
+ const c = ctrl.current;
480
+ if (!c)
481
+ return;
482
+ register(c);
483
+ if (speed > 0) {
484
+ c.TranslationX = CanvasWidthPts(c);
485
+ void c.TranslateToAsync(0, 0, speed);
486
+ }
487
+ return () => register(null);
488
+ }, [route]); // eslint-disable-line react-hooks/exhaustive-deps
489
+ return (_jsx(SkiaLayer, { ref: (c) => { ctrl.current = c; }, VerticalOptions: "Fill", HorizontalOptions: "Fill", Margin: new Thickness(0, 0, 0, bottom), BackgroundColor: background, IsVisible: visible, BlockGesturesBelow: true, children: children }));
490
+ }