npm package: @kawaiininja/layouts
Description: High-performance, premium mobile-first layouts for the Onyx Framework, featuring gesture-driven navigation, radial quick actions, and integrated theme support.
Author: Tristan
Latest version: 3.1.0

# @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**](./GUIDE.md) | [**🎨 Explore Examples**](./examples/)

---

## ✨ Features

### 💻 Responsive Engine (Adaptive Mode)

- **Layout Swapping**: Automatically switches between `OnyxMobileLayout` and `OnyxPcLayout` at 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-dom` or any history-based router.
- **Controlled Navigation**: Pass `activeTab` and `activeSubTab` from your routing hook to drive the UI.
- **Path Mapping**: Define `path` strings for tabs and sub-tabs to automatically trigger the `onNavigate` callback.

### 🎨 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

```bash
npm install @kawaiininja/layouts
```

Note: This package requires `framer-motion`, `lucide-react`, and `react` >= 18.0.0.

---

## 📦 Usage

### Integrated Routing Support (Responsive)

```jsx
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:

1. **Wrap in `<Suspense>`**: If your views are lazy-loaded, this is **mandatory**.
2. **Use `100dvh`**: Desktop `100vh` causes "bounce" and sizing bugs on mobile. Always use `100dvh` (Dynamic Viewport Height) on your main app container.
3. **Sync `isRefreshing`**: Ensure your refresh handler always toggles the boolean, or the spinner will never disappear (though the framework has a 12s safety fallback).
4. **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`
- **Default Views**: Use an empty string `""` for the path of your primary sub-tab.
  - _Resulting Path_: `/settings` (shows the first tab by default).

### 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()`:

```js
// ✅ 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.

```jsx
<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.

```javascript
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`.

### 5. Triggering Drawers from Views

Every component rendered inside a sub-tab `view` automatically receives the `openDrawer` prop (and its alias `onOpenDrawer`). This allows you to trigger your registered drawers from any button or interaction within your content.

**New**: You can now pass a data object as the second argument. This data will be passed as props to the drawer component.

```jsx
// Inside a component used as a sub-tab 'view'
const MyFeedView = ({ openDrawer }) => {
  return (
    <div className="p-6">
      <h1>Welcome to the Feed</h1>
      {/* Pass custom data to the drawer */}
      <button onClick={() => openDrawer("editor", { initialTitle: "Hello World" })}>
        Compose New Post
      </button>
    </div>
  );
};

// The Drawer component then receives it as props
const MyEditorDrawer = ({ initialTitle, isOpen, onClose }) => {
  return (
    <AdaptiveDrawer isOpen={isOpen} onClose={onClose} title="New Post">
      <input defaultValue={initialTitle} />
    </AdaptiveDrawer>
  );
};
```

---

## 🔄 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 `onRefresh` at 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.

```javascript
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 `isRefreshing` stays `true` for 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 `onRefresh` and sync `isRefreshing`.

### Quick Action Menu (Radial Selection)

1. **Trigger**: Long-press (400ms) any Navigation Rail item.
2. **Selection**: Drag towards an action to highlight.
3. **Execution**: Release to trigger.

---

## 🎨 Design System (CSS Tokens)

```css
: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