@kawaiininja/layouts
v1.2.0
Published
High-performance, premium mobile-first layouts for the Onyx Framework, featuring gesture-driven navigation, radial quick actions, and integrated theme support.
Maintainers
Readme
@kawaiininja/layouts
High-performance, premium responsive layouts for the Onyx Framework. Designed with a focus on fluid gestures, adaptive sidebar navigation, and state-of-the-art aesthetics that switch seamlessly between Mobile and PC.
📖 Read the Full Developer Guide | 🎨 Explore Examples
✨ Features
💻 Responsive Engine (Adaptive Mode)
- Layout Swapping: Automatically switches between
OnyxMobileLayoutandOnyxPcLayoutat the 1024px breakpoint. - Adaptive Navigation: Mobile uses a bottom rail and popup drawer; PC uses a fixed sidebar and hover-based quick menus.
- Single Config: Define your navigation once—the framework handles the physics and layout changes for all screen sizes.
🖐️ Gesture & Mouse Interactions
- Horizontal Tab Swiping: Seamlessly transition between sub-tabs with natural swipe gestures. Built on a custom gesture engine for zero-lag response.
- Vertical Pull-to-Refresh: Granular refresh logic at the global, tab, or individual sub-tab level with integrated spring physics and haptic feedback support.
- Async State Management: Refresh handlers now support Promises, allowing the framework to automatically manage the loading spinner lifecycle during async operations.
🔘 Radial Quick Actions (Long-Press)
- Zero-Friction Access: Long-press any navigation rail item to reveal a radial selection menu.
- Directional Selection: Swipe towards an action item to select and execute it instantly without lifting your finger.
- Programmable Logic: Quick actions can trigger internal state changes, external redirects, or open custom layout drawers.
🖼️ Modular Layout Engine
- Dynamic Headers: Auto-syncing header titles with current navigation state. Supports custom right-aligned actions (buttons, icons, etc.).
- Flexible Side Drawer: Fully configurable drawer menu with support for deep-linking (target specific tabs/sub-tabs) and custom action links.
- Panel System: Register any number of custom drawers (Bottom Sheets) that can be triggered from anywhere in the app.
🛣️ Integrated Routing Support
- Full URL Sync: Sync layout state with
react-router-domor any history-based router. - Controlled Navigation: Pass
activeTabandactiveSubTabfrom your routing hook to drive the UI. - Path Mapping: Define
pathstrings for tabs and sub-tabs to automatically trigger theonNavigatecallback.
🎨 Design System & Theme
- Premium Glassmorphism: High-quality backdrop blurs and subtle gradients for a modern, high-end feel.
- Integrated Theme Support: Robust dark/light mode toggle with local storage persistence and system-aware defaults.
🚀 Installation
npm install @kawaiininja/layoutsNote: This package requires framer-motion, lucide-react, and react >= 18.0.0.
📦 Usage
Integrated Routing Support (Responsive)
import { OnyxResponsiveLayout } from "@kawaiininja/layouts";
import { BrowserRouter, useNavigate, useLocation } from "react-router-dom";
import { Home, Grid, PenTool } from "lucide-react";
const App = () => {
const navigate = useNavigate();
const location = useLocation();
const currentPath = location.pathname;
const tabs = [
{
id: "home",
label: "Home",
icon: Home,
path: "/home",
subTabs: [
{
label: "Feed",
icon: Grid,
path: "feed",
view: () => <div>Main Feed</div>,
},
],
quickActions: [
{
label: "New Post",
icon: PenTool,
onClick: ({ openDrawer }) => openDrawer("editor"),
},
],
},
];
// Derive active states from URL
const activeTabId = tabs.find((t) => currentPath.startsWith(t.path))?.id;
const [isRefreshing, setIsRefreshing] = useState(false);
return (
<OnyxResponsiveLayout
tabs={tabs}
user={{ name: "Alex", handle: "@alex", avatar: "..." }}
activeTab={activeTabId}
onNavigate={(path) => navigate(path)}
isRefreshing={isRefreshing}
drawers={{
editor: MyEditorPanel,
}}
/>
);
};⚠️ Critical Integration Checklist
Do not skip these steps, especially for Native/Capacitor builds:
- Wrap in
<Suspense>: If your views are lazy-loaded, this is mandatory. - Use
100dvh: Desktop100vhcauses "bounce" and sizing bugs on mobile. Always use100dvh(Dynamic Viewport Height) on your main app container. - Sync
isRefreshing: Ensure your refresh handler always toggles the boolean, or the spinner will never disappear (though the framework has a 12s safety fallback). - Absolute Parent Paths: Ensure tab paths start with
/to avoid navigation state confusion.
🧭 Mastering Navigation & Routing
To ensure your application feels premium and works correctly on native devices, follow these core principles for pathing and state.
1. The Pathing Hierarchy
The framework intelligently joins paths. To maintain a clean structure, follow these rules:
- Parent Tabs: Always use Absolute Paths (e.g.,
/settings). - Sub-Tabs: Use Relative Paths (e.g.,
security).- Resulting Path:
/settings/security
- Resulting Path:
- Default Views: Use an empty string
""for the path of your primary sub-tab.- Resulting Path:
/settings(shows the first tab by default).
- Resulting Path:
2. Deep-Linking Safety
When using a router (like React Router), your activeTab logic should handle sub-paths. Instead of a direct equality check, use .startsWith():
// ✅ CORRECT: Supports sub-paths like /settings/security
const activeTabId = tabs.find((t) => location.pathname.startsWith(t.path))?.id;3. Native Stability (Suspense)
If your view components are Lazy Loaded (using React.lazy), you must wrap the OnyxMobileLayout in a <Suspense> boundary. Without this, the app may crash on native devices when transitioning to a new tab.
<Suspense fallback={<MyLoader />}>
<OnyxResponsiveLayout ... />
</Suspense>4. Sidemenu Deep-Linking (Global Navigation)
The sidebar (PC) and Drawer (Mobile) can navigate to specific tabs and sub-tabs using the drawerItems prop. This is the preferred way to create a "Global Menu" that crosses category boundaries.
const drawerItems = [
{
label: "Inventory Hub",
icon: Activity,
targetTab: "search", // The ID of the parent tab
targetSubTab: "Inventory", // The Label of the specific sub-tab
},
{
label: "Account Settings",
icon: User,
targetTab: "settings", // Defaults to first sub-tab of settings
},
];
// ... inside render
<OnyxResponsiveLayout tabs={tabs} drawerItems={drawerItems} />;The framework will automatically calculate the correct path from your TabConfig and trigger onNavigate.
🔄 Data Refresh & Safety
The framework includes a sophisticated gesture-based refresh system with built-in safety mechanisms.
1. Granular Hierarchy
You can implement refresh logic at three distinct levels:
- Global Refresh: Pass
onRefreshat the root. Best for app-wide syncs. - Tab-Specific Refresh: Defined in
TabConfig. Refreshes all data within a main category. - Sub-Tab Refresh (Granular): Defined in
SubTabConfig. The most efficient pattern; refreshes only the specific view the user is looking at.
2. Async Handler Support
Handers can now return a Promise. If a promise is returned, the layout will stay in its refresh state (and keep spinning) until the promise resolves or rejects.
onRefresh: async () => {
await syncMyData(); // Framework stays in loading state automatically
};3. Built-in 12s Safety Cutoff
To prevent your UI from getting "stuck" due to forgotten state updates or hanging API calls, the framework includes an automatic safety timeout:
- If
isRefreshingstaystruefor more than 12 seconds, the layout will automatically reset the pull-to-refresh UI to its idle state. - This ensures that your users are never locked out of the application by a loading spinner.
🛠️ Configuration API
OnyxMobileLayoutProps & OnyxPcLayoutProps
Both versions of the layout use almost identical properties. For automatic detection, use OnyxResponsiveLayout.
| Prop | Type | Description |
| :------------- | :---------------------------- | :------------------------------------------------------------ |
| tabs | TabConfig[] | Required. The main navigation stack (Rails on the right). |
| user | UserConfig | Required. Data for the side drawer's profile section. |
| drawers | Record<string, Component> | Dictionary of custom panel components. |
| activeTab | string | Controlled ID of the active tab. |
| activeSubTab | string | Controlled label of the active sub-tab. |
| onNavigate | (path: string) => void | Triggered on path-based navigation (Rails, Tabs, Drawers). |
| drawerItems | DrawerItemConfig[] | Custom links for the side menu. |
| onSignOut | () => void | Triggered when the "Sign Out" button is clicked. |
| onRefresh | () => void \| Promise<void> | Global pull-to-refresh handler. |
| isRefreshing | boolean | Global refresh state. |
| initialTab | string | Default: home. |
| rightAction | ReactNode | Global header action element. |
TabConfig
| Field | Type | Description |
| :------------- | :---------------------------- | :------------------------------ |
| id | string | Unique identifier. |
| label | string | Rail label. |
| Field | Type | Description |
| :------------- | :---------------------------- | :------------------------------ |
| id | string | Unique identifier. |
| label | string | Rail label. |
| icon | LucideIcon | Rail icon. |
| path | string | Absolute base path for routing. |
| navTitle | string | Optional header title. |
| subTabs | SubTabConfig[] | Nested horizontal navigation. |
| quickActions | QuickActionConfig[] | Radial menu items. |
| onRefresh | () => void \| Promise<void> | Tab-specific refresh handler. |
| isRefreshing | boolean | Tab-specific loading state. |
| rightAction | ReactNode | Tab-specific header action. |
SubTabConfig
| Field | Type | Description |
| :------------- | :---------------------------- | :------------------------------------- |
| label | string | Pill label. |
| icon | LucideIcon | Pill icon. |
| path | string | Relative or absolute path for routing. |
| view | Component | The component to render. |
| onRefresh | () => void \| Promise<void> | Sub-tab specific refresh handler. |
| isRefreshing | boolean | Sub-tab specific loading state. |
QuickActionConfig & DrawerItemConfig
| Prop | Type | Description |
| :------------- | :-------------- | :----------------------------------------------- |
| label | string | Item text. |
| icon | LucideIcon | Item icon. |
| targetTab | string | Target Tab ID for navigation. |
| targetSubTab | string | Target Sub-tab Label for deep-linking. |
| onClick | (ctx) => void | Context: { openDrawer: (id: string) => void }. |
🌊 Gesture Interactions Guide
Pull-to-Refresh
- Threshold: 60px pull triggers the refresh.
- Implementation: Provide
onRefreshand syncisRefreshing.
Quick Action Menu (Radial Selection)
- Trigger: Long-press (400ms) any Navigation Rail item.
- Selection: Drag towards an action to highlight.
- Execution: Release to trigger.
🎨 Design System (CSS Tokens)
:root {
--bg-main: 0, 0, 0;
--bg-surface: 18, 18, 18;
--bg-elevated: 28, 28, 28;
--color-accent: 99, 102, 241;
--color-secondary: 236, 72, 153;
--text-primary: 255, 255, 255;
--text-muted: 156, 163, 175;
--color-border-subtle: 255, 255, 255, 0.1;
}⚖️ License
MIT © Tristan
