@kawaiininja/layouts 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @onyx/layouts
1
+ # @kawaiininja/layouts
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
 
@@ -26,6 +26,12 @@ High-performance, premium mobile-first layouts for the Onyx Framework. Designed
26
26
  - **Flexible Side Drawer**: Fully configurable drawer menu with support for deep-linking (target specific tabs/sub-tabs) and custom action links.
27
27
  - **Panel System**: Register any number of custom drawers (Bottom Sheets) that can be triggered from anywhere in the app.
28
28
 
29
+ ### 🛣️ Integrated Routing Support
30
+
31
+ - **Full URL Sync**: Sync layout state with `react-router-dom` or any history-based router.
32
+ - **Controlled Navigation**: Pass `activeTab` and `activeSubTab` from your routing hook to drive the UI.
33
+ - **Path Mapping**: Define `path` strings for tabs and sub-tabs to automatically trigger the `onNavigate` callback.
34
+
29
35
  ### 🎨 Design System & Theme
30
36
 
31
37
  - **Premium Glassmorphism**: High-quality backdrop blurs and subtle gradients for a modern, high-end feel.
@@ -36,7 +42,7 @@ High-performance, premium mobile-first layouts for the Onyx Framework. Designed
36
42
  ## 🚀 Installation
37
43
 
38
44
  ```bash
39
- npm install @onyx/layouts
45
+ npm install @kawaiininja/layouts
40
46
  ```
41
47
 
42
48
  Note: This package requires `framer-motion`, `lucide-react`, and `react` >= 18.0.0.
@@ -45,42 +51,53 @@ Note: This package requires `framer-motion`, `lucide-react`, and `react` >= 18.0
45
51
 
46
52
  ## 📦 Usage
47
53
 
48
- ### Basic Example
54
+ ### Integrated with React Router
49
55
 
50
56
  ```jsx
51
- import { OnyxMobileLayout } from "@onyx/layouts";
52
- import { Home, Search, Bell, User, PenTool, Grid } from "lucide-react";
57
+ import { OnyxMobileLayout } from "@kawaiininja/layouts";
58
+ import { BrowserRouter, useNavigate, useLocation } from "react-router-dom";
59
+ import { Home, Grid, PenTool } from "lucide-react";
60
+
61
+ const App = () => {
62
+ const navigate = useNavigate();
63
+ const location = useLocation();
64
+ const currentPath = location.pathname;
53
65
 
54
- const MyApp = () => {
55
66
  const tabs = [
56
67
  {
57
68
  id: "home",
58
69
  label: "Home",
59
70
  icon: Home,
60
- navTitle: "Activity Feed",
71
+ path: "/home",
61
72
  subTabs: [
62
- { label: "Inbox", icon: Grid, view: () => <div>Your Messages</div> },
73
+ {
74
+ label: "Feed",
75
+ icon: Grid,
76
+ path: "feed",
77
+ view: () => <div>Main Feed</div>,
78
+ },
63
79
  ],
64
80
  quickActions: [
65
81
  {
66
82
  label: "New Post",
67
83
  icon: PenTool,
68
- onClick: ({ openDrawer }) => openDrawer("post_editor"),
84
+ onClick: ({ openDrawer }) => openDrawer("editor"),
69
85
  },
70
86
  ],
71
87
  },
72
88
  ];
73
89
 
90
+ // Derive active states from URL
91
+ const activeTabId = tabs.find((t) => currentPath.startsWith(t.path))?.id;
92
+
74
93
  return (
75
94
  <OnyxMobileLayout
76
95
  tabs={tabs}
77
- user={{
78
- name: "Alex Designer",
79
- handle: "@alex_ui",
80
- avatar: "https://example.com/avatar.png",
81
- }}
96
+ user={{ name: "Alex", handle: "@alex", avatar: "..." }}
97
+ activeTab={activeTabId}
98
+ onNavigate={(path) => navigate(path)}
82
99
  drawers={{
83
- post_editor: PostEditorPanel,
100
+ editor: MyEditorPanel,
84
101
  }}
85
102
  />
86
103
  );
@@ -93,51 +110,54 @@ const MyApp = () => {
93
110
 
94
111
  ### `OnyxMobileLayoutProps`
95
112
 
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). |
113
+ | Prop | Type | Description |
114
+ | :------------- | :-------------------------- | :------------------------------------------------------------ |
115
+ | `tabs` | `TabConfig[]` | **Required**. The main navigation stack (Rails on the right). |
116
+ | `user` | `UserConfig` | **Required**. Data for the side drawer's profile section. |
117
+ | `drawers` | `Record<string, Component>` | Dictionary of custom panel components. |
118
+ | `activeTab` | `string` | Controlled ID of the active tab. |
119
+ | `activeSubTab` | `string` | Controlled label of the active sub-tab. |
120
+ | `onNavigate` | `(path: string) => void` | Triggered on path-based navigation (Rails, Tabs, Drawers). |
121
+ | `drawerItems` | `DrawerItemConfig[]` | Custom links for the side menu. |
122
+ | `onSignOut` | `() => void` | Triggered when the "Sign Out" button is clicked. |
123
+ | `onRefresh` | `() => void` | Global pull-to-refresh handler. |
124
+ | `isRefreshing` | `boolean` | Global refresh state. |
125
+ | `initialTab` | `string` | Default: `home`. |
126
+ | `rightAction` | `ReactNode` | Global header action element. |
107
127
 
108
128
  ### `TabConfig`
109
129
 
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. |
130
+ | Field | Type | Description |
131
+ | :------------- | :-------------------- | :------------------------------ |
132
+ | `id` | `string` | Unique identifier. |
133
+ | `label` | `string` | Rail label. |
134
+ | `icon` | `LucideIcon` | Rail icon. |
135
+ | `path` | `string` | Absolute base path for routing. |
136
+ | `navTitle` | `string` | Optional header title. |
137
+ | `subTabs` | `SubTabConfig[]` | Nested horizontal navigation. |
138
+ | `quickActions` | `QuickActionConfig[]` | Radial menu items. |
139
+ | `onRefresh` | `() => void` | Tab-specific refresh handler. |
140
+ | `isRefreshing` | `boolean` | Tab-specific loading state. |
141
+ | `rightAction` | `ReactNode` | Tab-specific header action. |
121
142
 
122
143
  ### `SubTabConfig`
123
144
 
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. |
145
+ | Field | Type | Description |
146
+ | :------ | :----------- | :------------------------------------- |
147
+ | `label` | `string` | Pill label. |
148
+ | `icon` | `LucideIcon` | Pill icon. |
149
+ | `path` | `string` | Relative or absolute path for routing. |
150
+ | `view` | `Component` | The component to render. |
129
151
 
130
152
  ### `QuickActionConfig` & `DrawerItemConfig`
131
153
 
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 }`. |
154
+ | Prop | Type | Description |
155
+ | :------------- | :-------------- | :----------------------------------------------- |
156
+ | `label` | `string` | Item text. |
157
+ | `icon` | `LucideIcon` | Item icon. |
158
+ | `targetTab` | `string` | Target Tab ID for navigation. |
159
+ | `targetSubTab` | `string` | Target Sub-tab Label for deep-linking. |
160
+ | `onClick` | `(ctx) => void` | Context: `{ openDrawer: (id: string) => void }`. |
141
161
 
142
162
  ---
143
163
 
@@ -145,39 +165,28 @@ Both configurations share an `onClick` context provider:
145
165
 
146
166
  ### Pull-to-Refresh
147
167
 
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
168
  - **Threshold**: 60px pull triggers the refresh.
151
- - **State Control**: Use `isRefreshing` to let the layout know when the operation is complete.
169
+ - **Implementation**: Provide `onRefresh` and sync `isRefreshing`.
152
170
 
153
171
  ### Quick Action Menu (Radial Selection)
154
172
 
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.
173
+ 1. **Trigger**: Long-press (400ms) any Navigation Rail item.
174
+ 2. **Selection**: Drag towards an action to highlight.
175
+ 3. **Execution**: Release to trigger.
158
176
 
159
177
  ---
160
178
 
161
179
  ## 🎨 Design System (CSS Tokens)
162
180
 
163
- The layout is highly skinable using CSS variables. Override these in your root CSS:
164
-
165
181
  ```css
166
182
  :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) */
183
+ --bg-main: 0, 0, 0;
184
+ --bg-surface: 18, 18, 18;
185
+ --bg-elevated: 28, 28, 28;
186
+ --color-accent: 99, 102, 241;
174
187
  --color-secondary: 236, 72, 153;
175
-
176
- /* Text */
177
188
  --text-primary: 255, 255, 255;
178
189
  --text-muted: 156, 163, 175;
179
-
180
- /* Borders */
181
190
  --color-border-subtle: 255, 255, 255, 0.1;
182
191
  }
183
192
  ```
@@ -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,6 +3,7 @@ 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;
@@ -16,6 +17,7 @@ export interface TabConfig {
16
17
  label: string;
17
18
  icon: ComponentType<any>;
18
19
  navTitle?: string;
20
+ path?: string;
19
21
  subTabs: SubTabConfig[];
20
22
  quickActions: QuickActionConfig[];
21
23
  rightAction?: ReactNode;
@@ -50,5 +52,8 @@ export interface OnyxMobileLayoutProps {
50
52
  isRefreshing?: boolean;
51
53
  rightAction?: ReactNode;
52
54
  initialTab?: string;
55
+ activeTab?: string;
56
+ activeSubTab?: string;
57
+ onNavigate?: (path: string) => void;
53
58
  drawerItems?: DrawerItemConfig[];
54
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kawaiininja/layouts",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",