@kawaiininja/layouts 2.0.0 → 3.0.0

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 (96) hide show
  1. package/dist/MobileLayout.js +95 -241
  2. package/dist/PcLayout.js +55 -201
  3. package/dist/animations/index.d.ts +1 -0
  4. package/dist/animations/index.js +1 -0
  5. package/dist/animations/layout.d.ts +4 -0
  6. package/dist/animations/layout.js +83 -0
  7. package/dist/components/Header.d.ts +9 -0
  8. package/dist/components/Header.js +7 -0
  9. package/dist/components/HorizontalTabs.d.ts +9 -0
  10. package/dist/components/HorizontalTabs.js +5 -0
  11. package/dist/components/QuickMenu.d.ts +12 -0
  12. package/dist/components/QuickMenu.js +11 -0
  13. package/dist/components/RailNavButton.d.ts +16 -0
  14. package/dist/components/RailNavButton.js +8 -0
  15. package/dist/components/RailNavIndicator.d.ts +6 -0
  16. package/dist/components/RailNavIndicator.js +2 -0
  17. package/dist/components/RailNavThemeToggle.d.ts +5 -0
  18. package/dist/components/RailNavThemeToggle.js +11 -0
  19. package/dist/{ThemeContext.d.ts → contexts/ThemeContext.d.ts} +0 -4
  20. package/dist/{ThemeContext.js → contexts/ThemeContext.js} +1 -7
  21. package/dist/hooks/useDrawerStack.d.ts +12 -0
  22. package/dist/hooks/useDrawerStack.js +39 -0
  23. package/dist/hooks/useOnyxNavigation.d.ts +27 -0
  24. package/dist/hooks/useOnyxNavigation.js +98 -0
  25. package/dist/hooks/useOverlayHistory.d.ts +14 -0
  26. package/dist/hooks/useOverlayHistory.js +151 -0
  27. package/dist/hooks/useRailNavScroll.d.ts +10 -0
  28. package/dist/hooks/useRailNavScroll.js +45 -0
  29. package/dist/hooks/useTheme.d.ts +4 -0
  30. package/dist/hooks/useTheme.js +8 -0
  31. package/dist/index.d.ts +10 -3
  32. package/dist/index.js +10 -3
  33. package/dist/library/Library.d.ts +3 -0
  34. package/dist/library/Library.js +145 -0
  35. package/dist/library/LibraryDetail.d.ts +17 -0
  36. package/dist/library/LibraryDetail.js +59 -0
  37. package/dist/library/LibraryHome.d.ts +20 -0
  38. package/dist/library/LibraryHome.js +136 -0
  39. package/dist/library/MediaPreview.d.ts +7 -0
  40. package/dist/library/MediaPreview.js +56 -0
  41. package/dist/library/details/DetailActions.d.ts +8 -0
  42. package/dist/library/details/DetailActions.js +11 -0
  43. package/dist/library/details/DetailApiReference.d.ts +7 -0
  44. package/dist/library/details/DetailApiReference.js +7 -0
  45. package/dist/library/details/DetailDocumentation.d.ts +8 -0
  46. package/dist/library/details/DetailDocumentation.js +9 -0
  47. package/dist/library/details/DetailFooterCTA.d.ts +10 -0
  48. package/dist/library/details/DetailFooterCTA.js +17 -0
  49. package/dist/library/details/DetailHero.d.ts +14 -0
  50. package/dist/library/details/DetailHero.js +69 -0
  51. package/dist/library/details/DetailInstallation.d.ts +9 -0
  52. package/dist/library/details/DetailInstallation.js +7 -0
  53. package/dist/library/details/DetailMetadata.d.ts +7 -0
  54. package/dist/library/details/DetailMetadata.js +25 -0
  55. package/dist/library/details/DetailNavbar.d.ts +13 -0
  56. package/dist/library/details/DetailNavbar.js +13 -0
  57. package/dist/library/details/DetailPolicy.d.ts +7 -0
  58. package/dist/library/details/DetailPolicy.js +10 -0
  59. package/dist/library/details/DetailSidebar.d.ts +9 -0
  60. package/dist/library/details/DetailSidebar.js +29 -0
  61. package/dist/library/details/DetailUsage.d.ts +7 -0
  62. package/dist/library/details/DetailUsage.js +8 -0
  63. package/dist/library/details/DetailUtils.d.ts +3 -0
  64. package/dist/library/details/DetailUtils.js +67 -0
  65. package/dist/library/index.d.ts +2 -0
  66. package/dist/library/index.js +2 -0
  67. package/dist/{asset-store → library}/types.d.ts +29 -4
  68. package/dist/library/ui/Badge.d.ts +8 -0
  69. package/dist/library/ui/Badge.js +11 -0
  70. package/dist/library/ui/GlyphDetailView.d.ts +14 -0
  71. package/dist/library/ui/GlyphDetailView.js +80 -0
  72. package/dist/library/ui/Skeleton.d.ts +1 -0
  73. package/dist/library/ui/Skeleton.js +2 -0
  74. package/dist/library/ui/SyntaxHighlighter.d.ts +6 -0
  75. package/dist/library/ui/SyntaxHighlighter.js +104 -0
  76. package/dist/types/drawer.d.ts +9 -0
  77. package/dist/types/index.d.ts +4 -0
  78. package/dist/types/index.js +4 -0
  79. package/dist/types/layout.d.ts +28 -0
  80. package/dist/types/layout.js +1 -0
  81. package/dist/types/navigation.d.ts +28 -0
  82. package/dist/types/navigation.js +1 -0
  83. package/dist/types/user.d.ts +5 -0
  84. package/dist/types/user.js +1 -0
  85. package/dist/utils/index.d.ts +1 -0
  86. package/dist/utils/index.js +1 -0
  87. package/dist/utils/layout.d.ts +19 -0
  88. package/dist/utils/layout.js +37 -0
  89. package/package.json +9 -5
  90. package/dist/asset-store/AssetStore.d.ts +0 -2
  91. package/dist/asset-store/AssetStore.js +0 -334
  92. package/dist/asset-store/index.d.ts +0 -2
  93. package/dist/asset-store/index.js +0 -2
  94. package/dist/types.d.ts +0 -63
  95. /package/dist/{asset-store → library}/types.js +0 -0
  96. /package/dist/{types.js → types/drawer.js} +0 -0
@@ -1,139 +1,38 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { AnimatePresence, motion } from "framer-motion";
3
- import { Loader2, LogOut, Menu, Moon, Sun } from "lucide-react";
4
- import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
5
- import { ThemeProvider, useTheme } from "./ThemeContext";
6
- // --- Sub-components (Generic Versions) ---
7
- const RailNavIndicator = ({ height, offset, }) => (_jsx("div", { className: "absolute left-2 right-2 rounded-[24px] bg-[rgb(var(--bg-elevated))] transition-all duration-300 ease-out z-0 pointer-events-none border border-[rgb(var(--color-border-subtle))]/30", style: { height: `${height}px`, transform: `translateY(${offset}px)` } }));
8
- const RailNavButton = ({ title, icon: Icon, active, onClick, onRegisterRef, onTouchStart, onTouchMove, onTouchEnd, }) => (_jsxs("button", { ref: onRegisterRef, type: "button", role: "tab", "aria-label": title, onClick: onClick, onTouchStart: onTouchStart, onTouchMove: onTouchMove, onTouchEnd: onTouchEnd, onContextMenu: (e) => e.preventDefault(), className: `flex flex-col shrink-0 items-center justify-center w-full py-8 px-2 min-h-[120px] gap-4 transition-colors relative z-10 select-none ${active
9
- ? "text-[rgb(var(--color-accent))]"
10
- : "text-[rgb(var(--text-secondary))] hover:text-[rgb(var(--text-primary))]"}`, children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-[0.2em] [writing-mode:vertical-rl] rotate-180 transition-all duration-300 pointer-events-none ${active ? "scale-110 opacity-100" : "scale-100 opacity-60"}`, children: title }), Icon && (_jsx(Icon, { size: 20, className: `transition-all duration-300 -rotate-90 pointer-events-none ${active ? "scale-110 opacity-100" : "scale-100 opacity-60"}` }))] }));
11
- const RailNavThemeToggle = () => {
12
- const { isDark, toggleTheme } = useTheme();
13
- return (_jsx("button", { onClick: toggleTheme, className: "mt-auto mb-4 mx-auto p-2 rounded-xl text-[rgb(var(--text-tertiary))] hover:text-[rgb(var(--text-primary))] hover:bg-[rgb(var(--bg-tertiary))] transition-all active:scale-90 group", children: isDark ? (_jsx(Sun, { size: 20, className: "duration-500 group-hover:rotate-180 text-[rgb(var(--color-accent))]" })) : (_jsx(Moon, { size: 20, className: "duration-300 group-hover:-rotate-12 text-[rgb(var(--color-secondary))]" })) }));
14
- };
15
- const QuickMenu = ({ visible, items, positionY, selectedIndex }) => {
16
- if (!visible || !items || items.length === 0)
17
- return null;
18
- return (_jsxs("aside", { className: "fixed right-20 z-[100] min-w-[180px] bg-[rgb(var(--bg-elevated))]/95 backdrop-blur-xl rounded-2xl shadow-2xl p-2 border border-[rgb(var(--color-border))] flex flex-col gap-1 transition-all duration-200 ease-out origin-right", style: {
19
- top: positionY,
20
- transform: "translateY(-50%) scale(1)",
21
- animation: "popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275)",
22
- }, children: [items.map((item, idx) => (_jsxs("button", { className: `flex items-center gap-3 p-3 rounded-xl transition-all duration-150 w-full text-left ${selectedIndex === idx
23
- ? "bg-[rgb(var(--color-accent))] text-white scale-105 shadow-lg"
24
- : "text-[rgb(var(--text-secondary))] hover:bg-[rgb(var(--bg-tertiary))]"}`, children: [item.icon && _jsx(item.icon, { size: 18 }), _jsx("span", { className: "text-sm font-medium", children: item.label })] }, idx))), _jsx("div", { className: "absolute top-1/2 -right-2 w-4 h-4 bg-[rgb(var(--bg-elevated))]/95 rotate-45 border-r border-t border-[rgb(var(--color-border))] -translate-y-1/2" }), _jsx("style", { children: `
25
- @keyframes popIn {
26
- from { opacity: 0; transform: translateY(-50%) scale(0.8) translateX(20px); }
27
- to { opacity: 1; transform: translateY(-50%) scale(1) translateX(0); }
28
- }
29
- ` })] }));
30
- };
31
- const Header = ({ title, onMenuClick, rightAction }) => (_jsxs("header", { className: "sticky top-0 z-20 bg-[rgb(var(--bg-surface))]/90 backdrop-blur-md border-b border-[rgb(var(--color-border-subtle))] px-4 pt-[calc(env(safe-area-inset-top,0px)+12px)] pb-3 flex justify-between items-center shadow-sm select-none", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("button", { onClick: onMenuClick, className: "p-1 -ml-1 text-[rgb(var(--text-secondary))] active:text-[rgb(var(--text-primary))] touch-manipulation", children: _jsx(Menu, { size: 24 }) }), _jsx("h1", { className: "text-xl font-bold text-[rgb(var(--text-primary))] tracking-tight", children: title })] }), rightAction] }));
32
- const HorizontalTabs = ({ tabs, active, onChange }) => (_jsx("div", { className: "w-full bg-[rgb(var(--bg-main))] border-b border-[rgb(var(--color-border-subtle))] z-10 shrink-0", children: _jsx("div", { className: "flex px-4 gap-6 overflow-x-auto no-scrollbar", children: tabs.map((tab) => (_jsxs("button", { onClick: () => onChange(tab.label), className: `relative py-3 flex items-center gap-2 text-sm font-medium transition-colors whitespace-nowrap ${active === tab.label
33
- ? "text-[rgb(var(--text-primary))]"
34
- : "text-[rgb(var(--text-muted))]"}`, children: [tab.icon && _jsx(tab.icon, { size: 16 }), tab.label, active === tab.label && (_jsx(motion.div, { layoutId: "activeTabIndicator", className: "absolute bottom-0 left-0 right-0 h-0.5 bg-[rgb(var(--color-accent))]", transition: { type: "spring", stiffness: 300, damping: 30 } }))] }, tab.label))) }) }));
35
- // --- Hook: useRailNavScroll ---
36
- function useRailNavScroll(active, navKeys) {
37
- const activeIndex = Math.max(0, navKeys.indexOf(active));
38
- const scrollContainerRef = useRef(null);
39
- const buttonRefs = useRef([]);
40
- const [measurements, setMeasurements] = useState({
41
- heights: [],
42
- offsets: [],
43
- });
44
- useLayoutEffect(() => {
45
- const heights = buttonRefs.current.map((btn) => btn?.offsetHeight || 0);
46
- let currentOffset = 0;
47
- const offsets = [];
48
- for (let i = 0; i < heights.length; i++) {
49
- offsets.push(currentOffset);
50
- currentOffset += heights[i];
51
- }
52
- setMeasurements({ heights, offsets });
53
- }, [navKeys]);
54
- useEffect(() => {
55
- if (!scrollContainerRef.current ||
56
- measurements.offsets.length === 0 ||
57
- measurements.offsets.length <= activeIndex)
58
- return;
59
- const container = scrollContainerRef.current;
60
- const targetY = measurements.offsets[activeIndex];
61
- const targetHeight = measurements.heights[activeIndex];
62
- const paddingOffset = 24;
63
- const absoluteTargetY = targetY + paddingOffset;
64
- const scrollTarget = absoluteTargetY - container.clientHeight / 2 + targetHeight / 2;
65
- container.scrollTo({ top: scrollTarget, behavior: "smooth" });
66
- }, [activeIndex, measurements]);
67
- const registerButtonRef = (index, el) => {
68
- buttonRefs.current[index] = el;
69
- };
70
- return {
71
- scrollContainerRef,
72
- activeHeight: measurements.heights[activeIndex] || 0,
73
- activeOffset: measurements.offsets[activeIndex] || 0,
74
- registerButtonRef,
75
- };
76
- }
3
+ import { Loader2, LogOut } from "lucide-react";
4
+ import React, { useEffect, useRef, useState } from "react";
5
+ import { mobileLayoutVariants } from "./animations";
6
+ import { Header } from "./components/Header";
7
+ import { HorizontalTabs } from "./components/HorizontalTabs";
8
+ import { QuickMenu } from "./components/QuickMenu";
9
+ import { RailNavButton } from "./components/RailNavButton";
10
+ import { RailNavIndicator } from "./components/RailNavIndicator";
11
+ import { RailNavThemeToggle } from "./components/RailNavThemeToggle";
12
+ import { ThemeProvider } from "./contexts/ThemeContext";
13
+ import { useDrawerStack } from "./hooks/useDrawerStack";
14
+ import { useOnyxNavigation } from "./hooks/useOnyxNavigation";
15
+ import { useOverlayHistory } from "./hooks/useOverlayHistory";
16
+ import { useRailNavScroll } from "./hooks/useRailNavScroll";
17
+ import { checkRefreshing, clamp, getUiZoom, validateLayoutProps, } from "./utils";
77
18
  // --- Main Layout Component ---
78
19
  const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh, isRefreshing: externalRefreshing, rightAction, initialTab = "home", activeTab: externalActiveTab, activeSubTab: externalActiveSubTab, onNavigate, drawerItems = [], }) => {
79
20
  // --- Safety Validations ---
80
- if (!tabs || !Array.isArray(tabs) || tabs.length === 0) {
81
- throw new Error("[OnyxMobileLayout] Critical Error: 'tabs' prop is required and must be a non-empty array.");
82
- }
83
- if (!user) {
84
- throw new Error("[OnyxMobileLayout] Critical Error: 'user' prop is required to display profile information.");
85
- }
86
- const [internalActiveTab, setInternalActiveTab] = useState(initialTab);
87
- const activeTab = externalActiveTab || internalActiveTab;
21
+ validateLayoutProps(tabs, user, "OnyxMobileLayout");
22
+ const { drawerStack, activeDrawer, handleOpenDrawer, handleBackDrawer, handleCloseDrawers, setDrawerStack, } = useDrawerStack();
23
+ const { activeTab, activeSubTab, currentTabConfig, subTabs, navigateTo, motionData, navKeys, } = useOnyxNavigation({
24
+ tabs,
25
+ initialTab,
26
+ externalActiveTab,
27
+ externalActiveSubTab,
28
+ onNavigate,
29
+ });
88
30
  const [isOpen, setIsOpen] = useState(false);
89
31
  const [isDragging, setIsDragging] = useState(false);
90
- const [activeDrawer, setActiveDrawer] = useState(null);
91
32
  const [pullY, setPullY] = useState(0);
92
33
  const [internalRefreshing, setInternalRefreshing] = useState(false);
93
- const currentTabConfig = tabs.find((t) => t.id === activeTab);
94
- const subTabs = currentTabConfig?.subTabs ?? [];
95
- const [internalSubTab, setInternalSubTab] = useState(subTabs[0]?.label || "");
96
- const subTab = externalActiveSubTab || internalSubTab;
97
- const navigateTo = (tabId, subTabLabel) => {
98
- let nextTab = tabId || activeTab;
99
- let nextSub = subTabLabel || subTab;
100
- if (tabId) {
101
- setInternalActiveTab(tabId);
102
- const targetTab = tabs.find((t) => t.id === tabId);
103
- const isValid = (targetTab?.subTabs || []).some((st) => st.label === nextSub);
104
- if (!isValid && targetTab?.subTabs?.[0]) {
105
- nextSub = targetTab.subTabs[0].label;
106
- setInternalSubTab(nextSub);
107
- }
108
- }
109
- if (subTabLabel) {
110
- nextSub = subTabLabel;
111
- setInternalSubTab(subTabLabel);
112
- }
113
- if (onNavigate) {
114
- const targetTab = tabs.find((t) => t.id === nextTab);
115
- let path = targetTab?.path || "";
116
- const st = (targetTab?.subTabs || []).find((s) => s.label === nextSub);
117
- if (st?.path) {
118
- if (st.path.startsWith("/")) {
119
- path = st.path;
120
- }
121
- else {
122
- const base = path.endsWith("/") ? path : path + "/";
123
- path = base + st.path;
124
- }
125
- }
126
- if (path)
127
- onNavigate(path);
128
- }
129
- };
130
- const currentSubTabConfig = subTabs.find((st) => st.label === subTab);
131
- const isRefreshing = !!(currentSubTabConfig?.isRefreshing ||
132
- currentTabConfig?.isRefreshing ||
133
- externalRefreshing ||
134
- internalRefreshing);
135
- const navKeys = useMemo(() => tabs.map((t) => t.id), [tabs]);
136
- const { scrollContainerRef, activeHeight, activeOffset, registerButtonRef } = useRailNavScroll(activeTab, navKeys);
34
+ const isRefreshing = checkRefreshing(subTabs, activeSubTab, currentTabConfig, externalRefreshing, internalRefreshing);
35
+ const { scrollContainerRef, activeHeight, activeOffset, registerButtonRef } = useRailNavScroll(activeTab, navKeys, 24);
137
36
  const DRAWER_WIDTH = 220;
138
37
  const startX = useRef(0);
139
38
  const startY = useRef(0);
@@ -156,112 +55,47 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
156
55
  const holdTimer = useRef(null);
157
56
  const startYRef = useRef(0);
158
57
  const isDraggingMenu = useRef(false);
159
- const isClosingViaBack = useRef(false);
160
58
  const isAnyOverlayOpen = isOpen || !!activeDrawer;
59
+ console.log("[OnyxMobileLayout] Render State:", {
60
+ isOpen,
61
+ activeDrawer,
62
+ isAnyOverlayOpen,
63
+ stackLength: drawerStack.length,
64
+ activeTab,
65
+ activeSubTab,
66
+ });
161
67
  // Handle Hardware Back Button for Drawer and Active Panels
162
- useEffect(() => {
163
- if (!isAnyOverlayOpen)
68
+ const lastBackTime = useRef(0);
69
+ const handleOverlayBack = React.useCallback(() => {
70
+ const now = Date.now();
71
+ const timeSinceLastCall = now - lastBackTime.current;
72
+ // Debounce: Ignore rapid duplicate calls (e.g., from event propagation)
73
+ if (timeSinceLastCall < 150 && lastBackTime.current !== 0) {
74
+ console.warn(`[OnyxMobileLayout] ⚠️ Ignoring duplicate onBack (${timeSinceLastCall}ms since last call)`, "\nTip: Add e.stopPropagation() to your drawer close button click handler");
164
75
  return;
165
- const marker = "onyx-overlay-" + Math.random().toString(36).substring(7);
166
- window.history.pushState({ onyxMarker: marker }, "");
167
- const onPopState = () => {
168
- isClosingViaBack.current = true;
169
- setIsOpen(false);
170
- setActiveDrawer(null);
171
- };
172
- window.addEventListener("popstate", onPopState);
173
- return () => {
174
- window.removeEventListener("popstate", onPopState);
175
- if (!isClosingViaBack.current) {
176
- if (window.history.state?.onyxMarker === marker) {
177
- window.history.back();
178
- }
179
- }
180
- isClosingViaBack.current = false;
181
- };
182
- }, [isAnyOverlayOpen]);
183
- // Navigation Logic
184
- useEffect(() => {
185
- if (currentTabConfig && currentTabConfig.subTabs.length > 0) {
186
- const isValid = currentTabConfig.subTabs.some((st) => st.label === subTab);
187
- if (!isValid && currentTabConfig.subTabs.length > 0)
188
- navigateTo(undefined, currentTabConfig.subTabs[0].label);
189
76
  }
190
- }, [activeTab, tabs, subTab, currentTabConfig]);
191
- const [motionData, setMotionData] = useState({
192
- tab: activeTab,
193
- sub: subTab,
194
- dir: 0,
195
- type: "tab",
196
- });
197
- // Critical Logic: determine animation style and direction in render
198
- if (activeTab !== motionData.tab) {
199
- const prevIdx = navKeys.indexOf(motionData.tab);
200
- const nextIdx = navKeys.indexOf(activeTab);
201
- setMotionData({
202
- tab: activeTab,
203
- sub: subTab,
204
- dir: nextIdx > prevIdx ? 1 : -1,
205
- type: "tab",
77
+ lastBackTime.current = now;
78
+ console.log("[OnyxMobileLayout] Overlay onBack triggered (Hardware/PopState)");
79
+ setDrawerStack((prev) => {
80
+ if (prev.length > 0) {
81
+ console.log("[OnyxMobileLayout] Popping drawer stack, new length:", prev.length - 1);
82
+ return prev.slice(0, -1);
83
+ }
84
+ console.log("[OnyxMobileLayout] No drawers left, closing main drawer");
85
+ setIsOpen(false);
86
+ return prev;
206
87
  });
207
- }
208
- else if (subTab !== motionData.sub) {
209
- const subLabels = subTabs.map((st) => st.label);
210
- const prevIdx = subLabels.indexOf(motionData.sub);
211
- const nextIdx = subLabels.indexOf(subTab);
212
- // Only animate subtabs if they both exist in the CURRENT tab's context
213
- if (prevIdx !== -1 && nextIdx !== -1) {
214
- setMotionData({
215
- tab: activeTab,
216
- sub: subTab,
217
- dir: nextIdx > prevIdx ? 1 : -1,
218
- type: "subtab",
219
- });
220
- }
221
- else {
222
- // Just sync without directional animation if it's an jump/reset
223
- setMotionData({ ...motionData, sub: subTab, dir: 0 });
224
- }
225
- }
226
- // Animation variants
227
- const variants = {
228
- enter: (d) => ({
229
- x: d.type === "subtab" ? (d.dir > 0 ? 80 : -80) : 0,
230
- y: d.type === "tab" ? (d.dir > 0 ? "100%" : "-100%") : 0,
231
- scale: d.type === "subtab" ? 0.95 : 1,
232
- scaleY: d.type === "tab" ? 0.7 : 1,
233
- skewY: d.type === "tab" ? (d.dir > 0 ? 15 : -15) : 0,
234
- opacity: 0,
235
- }),
236
- center: {
237
- x: 0,
238
- y: 0,
239
- scale: 1,
240
- scaleY: 1,
241
- skewY: 0,
242
- opacity: 1,
243
- transition: {
244
- duration: 0.5,
245
- ease: [0.16, 1, 0.3, 1],
246
- scaleY: {
247
- values: [0.7, 1.15, 1],
248
- times: [0, 0.5, 1],
249
- duration: 0.5,
250
- ease: [0.16, 1, 0.3, 1],
251
- },
252
- skewY: { duration: 0.4, ease: [0.16, 1, 0.3, 1] },
253
- },
254
- },
255
- exit: (d) => ({
256
- x: d.type === "subtab" ? (d.dir > 0 ? -80 : 80) : 0,
257
- y: d.type === "tab" ? (d.dir > 0 ? "-100%" : "100%") : 0,
258
- scale: d.type === "subtab" ? 1.05 : 1,
259
- scaleY: d.type === "tab" ? 1.2 : 1,
260
- skewY: d.type === "tab" ? (d.dir > 0 ? -10 : 10) : 0,
261
- opacity: 0,
262
- transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] },
263
- }),
264
- };
88
+ }, [setDrawerStack]);
89
+ const handleOverlayClose = React.useCallback(() => {
90
+ console.log("[OnyxMobileLayout] Overlay onClose triggered (Direct Close)");
91
+ setIsOpen(false);
92
+ }, []);
93
+ const depth = (isOpen ? 1 : 0) + drawerStack.length;
94
+ useOverlayHistory({
95
+ depth,
96
+ onBack: handleOverlayBack,
97
+ platform: "mobile",
98
+ });
265
99
  const Drawer = () => (_jsxs("aside", { className: "fixed inset-y-0 left-0 w-[220px] bg-[var(--drawer-bg,rgb(18,18,18))] text-[var(--drawer-text,rgb(255,255,255))] z-0 flex flex-col pt-12 pb-6 px-6 overflow-hidden", children: [_jsxs("div", { className: "mb-8 pl-2", children: [_jsx("div", { className: "w-16 h-16 rounded-full bg-gradient-to-tr from-[rgb(var(--color-secondary))] to-[rgb(var(--color-accent))] mb-4 p-0.5", children: _jsx("img", { src: user.avatar, className: "w-full h-full rounded-full object-cover border-2 border-[rgb(var(--bg-main))]" }) }), _jsx("h2", { className: "text-xl font-bold whitespace-nowrap", children: user.name }), _jsx("p", { className: "text-sm text-[rgb(var(--text-muted))]", children: user.handle })] }), _jsx("div", { className: "flex-1 space-y-1", children: (drawerItems.length > 0
266
100
  ? drawerItems
267
101
  : (tabs[0]?.subTabs || []).map((st) => ({
@@ -273,12 +107,12 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
273
107
  navigateTo(item.targetTab, item.targetSubTab);
274
108
  if (item.onClick)
275
109
  item.onClick({
276
- openDrawer: (id) => setActiveDrawer(id),
110
+ openDrawer: (id) => handleOpenDrawer(id),
277
111
  });
278
112
  setIsOpen(false);
279
113
  }, className: "w-full flex items-center gap-4 p-3 text-[rgb(var(--text-muted))] hover:text-[rgb(var(--text-primary))] hover:bg-[rgb(var(--bg-tertiary))] rounded-xl transition-all group font-medium", children: [_jsx(item.icon, { size: 20, className: "group-hover:scale-110 transition-transform" }), _jsx("span", { children: item.label })] }, item.label))) }), _jsx("div", { className: "mt-auto", children: _jsxs("button", { onClick: onSignOut, className: "flex items-center gap-3 text-[rgb(var(--status-error))] font-medium p-3 w-full hover:bg-[rgb(var(--bg-tertiary))] rounded-xl transition-colors", children: [_jsx(LogOut, { size: 20 }), _jsx("span", { children: "Sign Out" })] }) })] }));
280
114
  const handleTouchStart = (e) => {
281
- const zoom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--ui-zoom") || "1");
115
+ const zoom = getUiZoom();
282
116
  startX.current = e.touches[0].clientX / zoom;
283
117
  startY.current = e.touches[0].clientY / zoom;
284
118
  dragXRef.current = isOpen ? DRAWER_WIDTH : 0;
@@ -291,13 +125,13 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
291
125
  const handleTouchMove = (e) => {
292
126
  if (!isGesturing.current || quickMenu.visible || isDraggingMenu.current)
293
127
  return;
294
- const zoom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--ui-zoom") || "1");
128
+ const zoom = getUiZoom();
295
129
  const dx = e.touches[0].clientX / zoom - startX.current;
296
130
  const dy = e.touches[0].clientY / zoom - startY.current;
297
131
  lastDxRef.current = dx;
298
132
  if (!isHorizontal.current && !isVerticalPull.current && !isDragging) {
299
133
  if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 10) {
300
- if (!isOpen && dx > 0 && subTabs[0]?.label === subTab) {
134
+ if (!isOpen && dx > 0 && subTabs[0]?.label === activeSubTab) {
301
135
  isHorizontal.current = true;
302
136
  setIsDragging(true);
303
137
  }
@@ -313,7 +147,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
313
147
  }
314
148
  }
315
149
  if (isHorizontal.current) {
316
- const newX = Math.max(0, Math.min((isOpen ? DRAWER_WIDTH : 0) + dx, DRAWER_WIDTH));
150
+ const newX = clamp((isOpen ? DRAWER_WIDTH : 0) + dx, 0, DRAWER_WIDTH);
317
151
  dragXRef.current = newX;
318
152
  setRenderDragX(newX);
319
153
  }
@@ -335,7 +169,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
335
169
  if (isTabSwipe.current) {
336
170
  isTabSwipe.current = false;
337
171
  if (Math.abs(lastDxRef.current) > 50) {
338
- const idx = subTabs.findIndex((t) => t.label === subTab);
172
+ const idx = subTabs.findIndex((t) => t.label === activeSubTab);
339
173
  if (lastDxRef.current < 0 && idx < subTabs.length - 1)
340
174
  navigateTo(undefined, subTabs[idx + 1].label);
341
175
  else if (lastDxRef.current > 0 && idx > 0)
@@ -344,7 +178,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
344
178
  }
345
179
  if (isVerticalPull.current) {
346
180
  if (pullY > 60) {
347
- const currentSubTab = subTabs.find((st) => st.label === subTab);
181
+ const currentSubTab = subTabs.find((st) => st.label === activeSubTab);
348
182
  const refreshHandler = currentSubTab?.onRefresh || currentTabConfig?.onRefresh || onRefresh;
349
183
  if (refreshHandler) {
350
184
  const result = refreshHandler();
@@ -429,7 +263,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
429
263
  const ITEM_HEIGHT = 48;
430
264
  const offset = (quickMenu.items.length - 1) / 2;
431
265
  const idx = Math.round(diff / ITEM_HEIGHT + offset);
432
- const clampedIdx = Math.max(0, Math.min(idx, quickMenu.items.length - 1));
266
+ const clampedIdx = clamp(idx, 0, quickMenu.items.length - 1);
433
267
  setQuickMenu((prev) => ({ ...prev, selectedIndex: clampedIdx }));
434
268
  };
435
269
  const handleNavTouchEnd = (tabId) => {
@@ -439,7 +273,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
439
273
  if (quickMenu.selectedIndex !== -1 &&
440
274
  quickMenu.items[quickMenu.selectedIndex]?.onClick) {
441
275
  quickMenu.items[quickMenu.selectedIndex].onClick({
442
- openDrawer: (id) => setActiveDrawer(id),
276
+ openDrawer: (id) => handleOpenDrawer(id),
443
277
  });
444
278
  }
445
279
  setQuickMenu((prev) => ({ ...prev, visible: false, selectedIndex: -1 }));
@@ -460,7 +294,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
460
294
  ? "none"
461
295
  : "transform 0.3s cubic-bezier(0.32, 0.72, 0, 1), border-radius 0.3s",
462
296
  overflow: "hidden",
463
- }, children: [isOpen && !isDragging && (_jsx("div", { className: "absolute inset-0 z-50 bg-black/20", onClick: () => setIsOpen(false) })), _jsxs("div", { className: "flex-1 flex flex-col h-full mr-12 relative bg-[rgb(var(--bg-main))]", children: [_jsx(Header, { title: currentTabConfig?.navTitle || currentTabConfig?.label || "App", onMenuClick: () => setIsOpen(!isOpen), rightAction: currentTabConfig?.rightAction || rightAction }), _jsx(HorizontalTabs, { tabs: subTabs, active: subTab, onChange: (label) => navigateTo(undefined, label) }), _jsxs("main", { ref: mainScrollRef, className: "flex-1 overflow-y-auto no-scrollbar overscroll-contain relative touch-pan-y", children: [_jsx("div", { className: "absolute top-0 left-0 right-0 flex justify-center items-center pointer-events-none z-0", style: {
297
+ }, children: [isOpen && !isDragging && (_jsx("div", { className: "absolute inset-0 z-50 bg-black/20", onClick: () => setIsOpen(false) })), _jsxs("div", { className: "flex-1 flex flex-col h-full mr-12 relative bg-[rgb(var(--bg-main))]", children: [_jsx(Header, { title: currentTabConfig?.navTitle || currentTabConfig?.label || "App", onMenuClick: () => setIsOpen(!isOpen), rightAction: currentTabConfig?.rightAction || rightAction, isMobile: true }), _jsx(HorizontalTabs, { tabs: subTabs, active: activeSubTab, onChange: (label) => navigateTo(undefined, label), isMobile: true }), _jsxs("main", { ref: mainScrollRef, className: "flex-1 overflow-y-auto no-scrollbar overscroll-contain relative touch-pan-y", children: [_jsx("div", { className: "absolute top-0 left-0 right-0 flex justify-center items-center pointer-events-none z-0", style: {
464
298
  height: `${pullY}px`,
465
299
  opacity: Math.min(pullY / 40, 1),
466
300
  transition: isVerticalPull.current
@@ -468,7 +302,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
468
302
  : "height 0.4s cubic-bezier(0.19, 1, 0.22, 1), opacity 0.4s",
469
303
  }, children: _jsx("div", { className: `p-2 bg-[rgb(var(--bg-surface))] rounded-full shadow-md border border-[rgb(var(--color-border-subtle))] ${isRefreshing || (pullY > 20 && !isGesturing.current)
470
304
  ? "animate-spin text-[rgb(var(--color-accent))]"
471
- : "text-[rgb(var(--text-secondary))]"}`, children: _jsx(Loader2, { size: 20 }) }) }), _jsx(AnimatePresence, { mode: "popLayout", custom: motionData, initial: false, children: _jsx(motion.div, { custom: motionData, variants: variants, initial: "enter", animate: "center", exit: "exit", className: "w-full min-h-full origin-center", style: {
305
+ : "text-[rgb(var(--text-secondary))]"}`, children: _jsx(Loader2, { size: 20 }) }) }), _jsx(AnimatePresence, { mode: "popLayout", custom: motionData, initial: false, children: _jsx(motion.div, { custom: motionData, variants: mobileLayoutVariants, initial: "enter", animate: "center", exit: "exit", className: "w-full min-h-full origin-center", style: {
472
306
  width: "100%",
473
307
  willChange: "transform, opacity, filter",
474
308
  }, children: _jsx("div", { style: {
@@ -478,8 +312,28 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
478
312
  transition: isVerticalPull.current
479
313
  ? "none"
480
314
  : "transform 0.3s ease-out",
481
- }, children: subTabs.find((st) => st.label === subTab)?.view &&
482
- React.createElement(subTabs.find((st) => st.label === subTab).view, { onOpenDrawer: setActiveDrawer }) }) }, `${activeTab}-${subTab}`) })] })] }), _jsxs("nav", { className: "absolute right-0 top-0 bottom-0 w-12 bg-[rgb(var(--bg-tertiary))] border-l border-[rgb(var(--color-border-subtle))] flex flex-col z-50 pb-[env(safe-area-inset-bottom,0px)]", children: [_jsxs("div", { ref: scrollContainerRef, className: "flex-1 overflow-y-auto no-scrollbar py-4 w-full relative", children: [_jsx(RailNavIndicator, { height: activeHeight, offset: activeOffset }), tabs.map((tab, idx) => (_jsx(RailNavButton, { title: tab.label, icon: tab.icon, active: activeTab === tab.id, onClick: () => handleNavTouchEnd(tab.id), onTouchStart: (e) => handleNavTouchStart(e, tab), onTouchMove: handleNavTouchMove, onTouchEnd: () => handleNavTouchEnd(tab.id), onRegisterRef: (el) => registerButtonRef(idx, el) }, tab.id)))] }), _jsx(RailNavThemeToggle, {})] })] }), _jsx(QuickMenu, { ...quickMenu }), Object.entries(drawers || {}).map(([key, DrawerComp]) => (_jsx(DrawerComp, { isOpen: activeDrawer === key, onClose: () => setActiveDrawer(null) }, key))), _jsx("style", { children: `
315
+ }, children: subTabs.find((st) => st.label === activeSubTab)?.view &&
316
+ React.createElement(subTabs.find((st) => st.label === activeSubTab)
317
+ .view, {
318
+ onOpenDrawer: handleOpenDrawer,
319
+ openDrawer: handleOpenDrawer,
320
+ }) }) }, `${activeTab}-${activeSubTab}`) })] })] }), _jsxs("nav", { className: "absolute right-0 top-0 bottom-0 w-12 bg-[rgb(var(--bg-tertiary))] border-l border-[rgb(var(--color-border-subtle))] flex flex-col z-50 pb-[env(safe-area-inset-bottom,0px)]", children: [_jsxs("div", { ref: scrollContainerRef, className: "flex-1 overflow-y-auto no-scrollbar py-4 w-full relative", children: [_jsx(RailNavIndicator, { height: activeHeight, offset: activeOffset }), tabs.map((tab, idx) => (_jsx(RailNavButton, { title: tab.label, icon: tab.icon, active: activeTab === tab.id, onClick: () => handleNavTouchEnd(tab.id), onTouchStart: (e) => handleNavTouchStart(e, tab), onTouchMove: handleNavTouchMove, onTouchEnd: () => handleNavTouchEnd(tab.id), onRegisterRef: (el) => registerButtonRef(idx, el), isMobile: true }, tab.id)))] }), _jsx(RailNavThemeToggle, {})] })] }), _jsx(QuickMenu, { ...quickMenu, isMobile: true }), drawerStack.map((id, index) => {
321
+ const DrawerComp = drawers[id];
322
+ if (!DrawerComp)
323
+ return null;
324
+ const isTop = index === drawerStack.length - 1;
325
+ return (_jsx(DrawerComp, { isOpen: isTop, onClose: () => {
326
+ if (isTop) {
327
+ console.log("[OnyxMobileLayout] Drawer onClose Event:", id);
328
+ handleBackDrawer();
329
+ }
330
+ }, onBack: () => {
331
+ if (isTop) {
332
+ console.log("[OnyxMobileLayout] Drawer onBack Event:", id);
333
+ handleOverlayBack();
334
+ }
335
+ }, onOpenDrawer: handleOpenDrawer, openDrawer: handleOpenDrawer }, `${id}-${index}`));
336
+ }), _jsx("style", { children: `
483
337
  .no-scrollbar::-webkit-scrollbar { display: none; }
484
338
  .touch-manipulation { touch-action: manipulation; }
485
339
  .touch-pan-y { touch-action: pan-y; }