@kawaiininja/layouts 1.0.3 → 1.0.5

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
@@ -2,108 +2,187 @@
2
2
 
3
3
  High-performance, premium mobile-first layouts for the Onyx Framework. Designed with a focus on fluid gestures, modularity, and state-of-the-art aesthetics.
4
4
 
5
+ [**📖 Read the Full Developer Guide**](./GUIDE.md) | [**🎨 Explore Examples**](./examples/)
6
+
7
+ ---
8
+
5
9
  ## ✨ Features
6
10
 
7
- - **Gesture-Driven Navigation**: Horizontal tab swiping and vertical pull-to-refresh logic.
8
- - **Radial Quick Actions**: Long-press navigation keys to trigger a radial menu for rapid access to actions.
9
- - **Dynamic Headers**: Auto-updating header titles based on navigation state, with support for custom actions.
10
- - **Modular Drawers**: Easily integrate side menus and custom bottom sheets/panels.
11
- - **Integrated Theme Support**: Built-in dark/light mode toggle with persistence and system-wide CSS variable injection.
12
- - **Hardware Back Button Support**: Sophisticated history management for closing overlays and panels seamlessly on mobile devices.
11
+ ### 🖐️ Gesture-Driven Navigation (Fluid Flow)
12
+
13
+ - **Horizontal Tab Swiping**: Seamlessly transition between sub-tabs with natural swipe gestures. Built on a custom gesture engine for zero-lag response.
14
+ - **Vertical Pull-to-Refresh**: Per-tab or global refresh logic with integrated spring physics and haptic feedback support.
15
+ - **Smart History management**: Integrated hardware back-button handling. Pressing "Back" on Android or using back gestures will close drawers and panels before navigating away from the page.
16
+
17
+ ### 🔘 Radial Quick Actions (Long-Press)
18
+
19
+ - **Zero-Friction Access**: Long-press any navigation rail item to reveal a radial selection menu.
20
+ - **Directional Selection**: Swipe towards an action item to select and execute it instantly without lifting your finger.
21
+ - **Programmable Logic**: Quick actions can trigger internal state changes, external redirects, or open custom layout drawers.
22
+
23
+ ### 🖼️ Modular Layout Engine
24
+
25
+ - **Dynamic Headers**: Auto-syncing header titles with current navigation state. Supports custom right-aligned actions (buttons, icons, etc.).
26
+ - **Flexible Side Drawer**: Fully configurable drawer menu with support for deep-linking (target specific tabs/sub-tabs) and custom action links.
27
+ - **Panel System**: Register any number of custom drawers (Bottom Sheets) that can be triggered from anywhere in the app.
28
+
29
+ ### 🎨 Design System & Theme
30
+
31
+ - **Premium Glassmorphism**: High-quality backdrop blurs and subtle gradients for a modern, high-end feel.
32
+ - **Integrated Theme Support**: Robust dark/light mode toggle with local storage persistence and system-aware defaults.
33
+
34
+ ---
13
35
 
14
36
  ## 🚀 Installation
15
37
 
16
38
  ```bash
17
- npm install @onyx/layouts
39
+ npm install @kawaiininja/layouts
18
40
  ```
19
41
 
20
42
  Note: This package requires `framer-motion`, `lucide-react`, and `react` >= 18.0.0.
21
43
 
44
+ ---
45
+
22
46
  ## 📦 Usage
23
47
 
48
+ ### Basic Example
49
+
24
50
  ```jsx
25
- import { OnyxMobileLayout } from "@onyx/layouts";
26
- import { Home, Settings, PenTool, RefreshCw } from "lucide-react";
51
+ import { OnyxMobileLayout } from "@kawaiininja/layouts";
52
+ import { Home, Search, Bell, User, PenTool, Grid } from "lucide-react";
27
53
 
28
- const MyLayout = () => {
54
+ const MyApp = () => {
29
55
  const tabs = [
30
56
  {
31
57
  id: "home",
32
- icon: Home,
33
58
  label: "Home",
34
- navTitle: "My Feed",
35
- subTabs: [{ label: "Overview", icon: Grid, view: MyViewComponent }],
59
+ icon: Home,
60
+ navTitle: "Activity Feed",
61
+ subTabs: [
62
+ { label: "Inbox", icon: Grid, view: () => <div>Your Messages</div> },
63
+ ],
36
64
  quickActions: [
37
65
  {
66
+ label: "New Post",
38
67
  icon: PenTool,
39
- label: "Create",
40
- onClick: ({ openDrawer }) => openDrawer("editor"),
68
+ onClick: ({ openDrawer }) => openDrawer("post_editor"),
41
69
  },
42
70
  ],
43
- onRefresh: () => fetchData(),
44
- isRefreshing: loadingState,
45
71
  },
46
72
  ];
47
73
 
48
74
  return (
49
75
  <OnyxMobileLayout
50
76
  tabs={tabs}
51
- user={{ name: "Alex", handle: "@alex", avatar: "url" }}
77
+ user={{
78
+ name: "Alex Designer",
79
+ handle: "@alex_ui",
80
+ avatar: "https://example.com/avatar.png",
81
+ }}
52
82
  drawers={{
53
- editor: MyEditorPanel,
83
+ post_editor: PostEditorPanel,
54
84
  }}
55
85
  />
56
86
  );
57
87
  };
58
88
  ```
59
89
 
90
+ ---
91
+
60
92
  ## 🛠️ Configuration API
61
93
 
62
- ### OnyxMobileLayoutProps
63
-
64
- | Prop | Type | Description |
65
- | :------------- | :-------------------------- | :------------------------------------------------------ |
66
- | `tabs` | `TabConfig[]` | Array of main navigation tabs (right rail). |
67
- | `user` | `UserConfig` | User profile data for the side drawer. |
68
- | `drawers` | `Record<string, Component>` | Dictionary of custom panels/bottom sheets. |
69
- | `drawerItems` | `DrawerItemConfig[]` | Custom links for the main side drawer. |
70
- | `onSignOut` | `() => void` | Callback triggered when the sign-out button is clicked. |
71
- | `onRefresh` | `() => void` | (Global) Callback for pull-to-refresh. |
72
- | `isRefreshing` | `boolean` | (Global) Refreshing state. |
73
- | `initialTab` | `string` | ID of the tab to show on mount (default: 'home'). |
74
-
75
- ### TabConfig
76
-
77
- | Field | Type | Description |
78
- | :------------- | :-------------------- | :------------------------------------------------ |
79
- | `id` | `string` | Unique identifier for navigation. |
80
- | `icon` | `LucideIcon` | Icon for the navigation rail. |
81
- | `label` | `string` | Label for the navigation rail. |
82
- | `navTitle` | `string` | (Optional) Title shown in the header when active. |
83
- | `subTabs` | `SubTabConfig[]` | Array of nested horizontal tabs. |
84
- | `quickActions` | `QuickActionConfig[]` | Actions triggered by long-press on the nav key. |
85
- | `onRefresh` | `() => void` | Tab-specific refresh callback. |
86
- | `isRefreshing` | `boolean` | Tab-specific refresh state. |
87
-
88
- ### QuickActionConfig / DrawerItemConfig
89
-
90
- Quick actions and Drawer items support an `onClick` callback that receives a context object:
91
-
92
- ```typescript
93
- onClick: ({ openDrawer: (id: string) => void }) => void
94
- ```
94
+ ### `OnyxMobileLayoutProps`
95
+
96
+ | Prop | Type | Description |
97
+ | :------------- | :-------------------------- | :---------------------------------------------------------------------------- |
98
+ | `tabs` | `TabConfig[]` | **Required**. The main navigation stack (Rails on the right). |
99
+ | `user` | `UserConfig` | **Required**. Data for the side drawer's profile section. |
100
+ | `drawers` | `Record<string, Component>` | Dictionary of custom panel components. Keys are used as IDs for `openDrawer`. |
101
+ | `drawerItems` | `DrawerItemConfig[]` | Custom links for the side menu. If empty, defaults to first tab's subtabs. |
102
+ | `onSignOut` | `() => void` | Triggered when the "Sign Out" button in the drawer is clicked. |
103
+ | `onRefresh` | `() => void` | Global pull-to-refresh handler. Overridden if a tab has its own `onRefresh`. |
104
+ | `isRefreshing` | `boolean` | Global refresh state. |
105
+ | `initialTab` | `string` | The ID of the tab to show initially. Default: `home`. |
106
+ | `rightAction` | `ReactNode` | Global action element shown in the header (e.g., Settings icon). |
107
+
108
+ ### `TabConfig`
95
109
 
96
- ## 🎨 Theme Variables
110
+ | Field | Type | Description |
111
+ | :------------- | :-------------------- | :-------------------------------------------------------------------------- |
112
+ | `id` | `string` | Unique identifier used for internal routing. |
113
+ | `label` | `string` | Text label shown in the Rail. |
114
+ | `icon` | `LucideIcon` | Icon shown in the Rail. |
115
+ | `navTitle` | `string` | Optional. Text shown in the top header when this tab is active. |
116
+ | `subTabs` | `SubTabConfig[]` | Horizontal navigation nested under this tab. |
117
+ | `quickActions` | `QuickActionConfig[]` | Radial menu items triggered by long-press. |
118
+ | `onRefresh` | `() => void` | Tab-specific refresh handler. If provided, disables the global `onRefresh`. |
119
+ | `isRefreshing` | `boolean` | Tab-specific loading state. |
120
+ | `rightAction` | `ReactNode` | Tab-specific header action. |
97
121
 
98
- The layout relies on the following CSS variables for its design system:
122
+ ### `SubTabConfig`
123
+
124
+ | Field | Type | Description |
125
+ | :------ | :----------- | :--------------------------------------------------- |
126
+ | `label` | `string` | Text shown in the horizontal pill. |
127
+ | `icon` | `LucideIcon` | Icon next to the label. |
128
+ | `view` | `Component` | The component to render when this sub-tab is active. |
129
+
130
+ ### `QuickActionConfig` & `DrawerItemConfig`
131
+
132
+ Both configurations share an `onClick` context provider:
133
+
134
+ | Prop | Type | Description |
135
+ | :------------- | :-------------- | :------------------------------------------------------------------------ |
136
+ | `label` | `string` | Item text. |
137
+ | `icon` | `LucideIcon` | Item icon. |
138
+ | `targetTab` | `string` | (Drawer only) Target Tab ID for navigation. |
139
+ | `targetSubTab` | `string` | (Drawer only) Target Sub-tab Label for deep-linking. |
140
+ | `onClick` | `(ctx) => void` | Custom callback. Context contains `{ openDrawer: (id: string) => void }`. |
141
+
142
+ ---
143
+
144
+ ## 🌊 Gesture Interactions Guide
145
+
146
+ ### Pull-to-Refresh
147
+
148
+ To enable the refresh indicator, simply provide an `onRefresh` callback. The indicator will appear when the user pulls down from the top of the scroll container.
149
+
150
+ - **Threshold**: 60px pull triggers the refresh.
151
+ - **State Control**: Use `isRefreshing` to let the layout know when the operation is complete.
152
+
153
+ ### Quick Action Menu (Radial Selection)
154
+
155
+ 1. **Trigger**: Press and hold any item in the right navigation rail for 400ms.
156
+ 2. **Selection**: While holding, move your finger up or down. The selection indicator will follow.
157
+ 3. **Execution**: Release your finger to execute the highlighted action.
158
+
159
+ ---
160
+
161
+ ## 🎨 Design System (CSS Tokens)
162
+
163
+ The layout is highly skinable using CSS variables. Override these in your root CSS:
164
+
165
+ ```css
166
+ :root {
167
+ /* Backgrounds */
168
+ --bg-main: 0, 0, 0; /* Pure black */
169
+ --bg-surface: 18, 18, 18; /* Elevate surface */
170
+ --bg-elevated: 28, 28, 28; /* Drawer/Menus */
171
+
172
+ /* Brand */
173
+ --color-accent: 99, 102, 241; /* Primary brand color (Indigo) */
174
+ --color-secondary: 236, 72, 153;
175
+
176
+ /* Text */
177
+ --text-primary: 255, 255, 255;
178
+ --text-muted: 156, 163, 175;
179
+
180
+ /* Borders */
181
+ --color-border-subtle: 255, 255, 255, 0.1;
182
+ }
183
+ ```
99
184
 
100
- - `--bg-main`: Page background
101
- - `--bg-surface`: Header/Tab background
102
- - `--bg-elevated`: Drawer/Overlay background
103
- - `--color-accent`: Highlight/Active color
104
- - `--text-primary`: Main text
105
- - `--text-muted`: Secondary text
106
- - `--color-border-subtle`: Borders and dividers
185
+ ---
107
186
 
108
187
  ## ⚖️ License
109
188
 
@@ -103,17 +103,43 @@ function useRailNavScroll(active, navKeys) {
103
103
  };
104
104
  }
105
105
  // --- Main Layout Component ---
106
- const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh, isRefreshing: externalRefreshing, rightAction, initialTab = "home", drawerItems = [], }) => {
107
- const [activeTab, setActiveTab] = useState(initialTab);
106
+ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh, isRefreshing: externalRefreshing, rightAction, initialTab = "home", activeTab: externalActiveTab, activeSubTab: externalActiveSubTab, onNavigate, drawerItems = [], }) => {
107
+ const [internalActiveTab, setInternalActiveTab] = useState(initialTab);
108
+ const activeTab = externalActiveTab || internalActiveTab;
108
109
  const [isOpen, setIsOpen] = useState(false);
109
110
  const [isDragging, setIsDragging] = useState(false);
110
111
  const [activeDrawer, setActiveDrawer] = useState(null);
111
112
  const [pullY, setPullY] = useState(0);
112
113
  const [internalRefreshing, setInternalRefreshing] = useState(false);
113
114
  const currentTabConfig = tabs.find((t) => t.id === activeTab);
114
- const isRefreshing = currentTabConfig?.isRefreshing ?? externalRefreshing ?? internalRefreshing;
115
115
  const subTabs = currentTabConfig?.subTabs || [];
116
- const [subTab, setSubTab] = useState(subTabs[0]?.label || "");
116
+ const [internalSubTab, setInternalSubTab] = useState(subTabs[0]?.label || "");
117
+ const subTab = externalActiveSubTab || internalSubTab;
118
+ const navigateTo = (tabId, subTabLabel) => {
119
+ if (tabId)
120
+ setInternalActiveTab(tabId);
121
+ if (subTabLabel)
122
+ setInternalSubTab(subTabLabel);
123
+ if (onNavigate) {
124
+ const targetTab = tabs.find((t) => t.id === (tabId || activeTab));
125
+ let path = targetTab?.path || "";
126
+ if (subTabLabel) {
127
+ const st = targetTab?.subTabs.find((s) => s.label === subTabLabel);
128
+ if (st?.path) {
129
+ if (st.path.startsWith("/")) {
130
+ path = st.path;
131
+ }
132
+ else {
133
+ const base = path.endsWith("/") ? path : path + "/";
134
+ path = base + st.path;
135
+ }
136
+ }
137
+ }
138
+ if (path)
139
+ onNavigate(path);
140
+ }
141
+ };
142
+ const isRefreshing = currentTabConfig?.isRefreshing ?? externalRefreshing ?? internalRefreshing;
117
143
  const navKeys = useMemo(() => tabs.map((t) => t.id), [tabs]);
118
144
  const { scrollContainerRef, activeHeight, activeOffset, registerButtonRef } = useRailNavScroll(activeTab, navKeys);
119
145
  const DRAWER_WIDTH = 220;
@@ -165,8 +191,8 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
165
191
  useEffect(() => {
166
192
  if (currentTabConfig && currentTabConfig.subTabs.length > 0) {
167
193
  const isValid = currentTabConfig.subTabs.some((st) => st.label === subTab);
168
- if (!isValid)
169
- setSubTab(currentTabConfig.subTabs[0].label);
194
+ if (!isValid && currentTabConfig.subTabs.length > 0)
195
+ navigateTo(undefined, currentTabConfig.subTabs[0].label);
170
196
  }
171
197
  }, [activeTab, tabs, subTab, currentTabConfig]);
172
198
  const [[prevActive, direction], setDir] = useState([activeTab, 0]);
@@ -210,10 +236,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
210
236
  targetTab: tabs[0].id,
211
237
  targetSubTab: st.label,
212
238
  }))).map((item) => (_jsxs("button", { onClick: () => {
213
- if (item.targetTab)
214
- setActiveTab(item.targetTab);
215
- if (item.targetSubTab)
216
- setSubTab(item.targetSubTab);
239
+ navigateTo(item.targetTab, item.targetSubTab);
217
240
  if (item.onClick)
218
241
  item.onClick({
219
242
  openDrawer: (id) => setActiveDrawer(id),
@@ -280,9 +303,9 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
280
303
  if (Math.abs(lastDxRef.current) > 50) {
281
304
  const idx = subTabs.findIndex((t) => t.label === subTab);
282
305
  if (lastDxRef.current < 0 && idx < subTabs.length - 1)
283
- setSubTab(subTabs[idx + 1].label);
306
+ navigateTo(undefined, subTabs[idx + 1].label);
284
307
  else if (lastDxRef.current > 0 && idx > 0)
285
- setSubTab(subTabs[idx - 1].label);
308
+ navigateTo(undefined, subTabs[idx - 1].label);
286
309
  }
287
310
  }
288
311
  if (isVerticalPull.current) {
@@ -365,7 +388,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
365
388
  setQuickMenu((prev) => ({ ...prev, visible: false, selectedIndex: -1 }));
366
389
  }
367
390
  else {
368
- setActiveTab(tabId);
391
+ navigateTo(tabId);
369
392
  }
370
393
  };
371
394
  const currentTranslate = isDragging ? renderDragX : isOpen ? DRAWER_WIDTH : 0;
@@ -377,7 +400,7 @@ const OnyxMobileLayoutBase = ({ tabs, user, drawers = {}, onSignOut, onRefresh,
377
400
  ? "none"
378
401
  : "transform 0.3s cubic-bezier(0.32, 0.72, 0, 1), border-radius 0.3s",
379
402
  overflow: "hidden",
380
- }, 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: setSubTab }), _jsxs("main", { ref: mainScrollRef, className: "flex-1 overflow-y-auto no-scrollbar overscroll-contain relative mr-4", children: [_jsx("div", { className: "absolute top-0 left-0 right-0 flex justify-center items-center pointer-events-none z-0", style: {
403
+ }, 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 mr-4", children: [_jsx("div", { className: "absolute top-0 left-0 right-0 flex justify-center items-center pointer-events-none z-0", style: {
381
404
  height: `${pullY}px`,
382
405
  opacity: Math.min(pullY / 40, 1),
383
406
  }, children: _jsx("div", { className: `p-2 bg-[rgb(var(--bg-surface))] rounded-full shadow-md border border-[rgb(var(--color-border-subtle))] ${isRefreshing
package/dist/types.d.ts CHANGED
@@ -3,17 +3,21 @@ export interface SubTabConfig {
3
3
  label: string;
4
4
  icon: ComponentType<any>;
5
5
  view: ComponentType<any>;
6
+ path?: string;
6
7
  }
7
8
  export interface QuickActionConfig {
8
9
  label: string;
9
10
  icon: ComponentType<any>;
10
- onClick?: () => void;
11
+ onClick?: (ctx: {
12
+ openDrawer: (id: string) => void;
13
+ }) => void;
11
14
  }
12
15
  export interface TabConfig {
13
16
  id: string;
14
17
  label: string;
15
18
  icon: ComponentType<any>;
16
19
  navTitle?: string;
20
+ path?: string;
17
21
  subTabs: SubTabConfig[];
18
22
  quickActions: QuickActionConfig[];
19
23
  rightAction?: ReactNode;
@@ -25,7 +29,9 @@ export interface DrawerItemConfig {
25
29
  icon: any;
26
30
  targetTab?: string;
27
31
  targetSubTab?: string;
28
- onClick?: () => void;
32
+ onClick?: (ctx: {
33
+ openDrawer: (id: string) => void;
34
+ }) => void;
29
35
  }
30
36
  export interface UserConfig {
31
37
  name: string;
@@ -46,5 +52,8 @@ export interface OnyxMobileLayoutProps {
46
52
  isRefreshing?: boolean;
47
53
  rightAction?: ReactNode;
48
54
  initialTab?: string;
55
+ activeTab?: string;
56
+ activeSubTab?: string;
57
+ onNavigate?: (path: string) => void;
49
58
  drawerItems?: DrawerItemConfig[];
50
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kawaiininja/layouts",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "High-performance, premium mobile-first layouts for the Onyx Framework, featuring gesture-driven navigation, radial quick actions, and integrated theme support.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -26,13 +26,13 @@
26
26
  "license": "MIT",
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/kawaiininja/onyx-framework.git",
29
+ "url": "https://github.com/4kawaiininja/onyx-framework.git",
30
30
  "directory": "layouts"
31
31
  },
32
32
  "bugs": {
33
- "url": "https://github.com/kawaiininja/onyx-framework/issues"
33
+ "url": "https://github.com/4kawaiininja/onyx-framework/issues"
34
34
  },
35
- "homepage": "https://github.com/kawaiininja/onyx-framework#readme",
35
+ "homepage": "https://github.com/4kawaiininja/onyx-framework/blob/main/layouts/README.md",
36
36
  "exports": {
37
37
  ".": {
38
38
  "types": "./dist/index.d.ts",