harebear-ui 0.6.0 → 0.8.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.
- package/AGENTS.md +32 -12
- package/README.md +109 -36
- package/bin/harebear-ui.mjs +66 -3
- package/dist/components/admin/app-nav.d.ts +57 -0
- package/dist/components/admin/app-nav.js +172 -0
- package/dist/components/admin/focus-layout.d.ts +14 -0
- package/dist/components/admin/focus-layout.js +36 -0
- package/dist/components/admin/settings.d.ts +31 -0
- package/dist/components/admin/settings.js +78 -0
- package/dist/components/ai/action-receipt.d.ts +16 -0
- package/dist/components/ai/action-receipt.js +47 -0
- package/dist/components/ai/ask-box.d.ts +27 -0
- package/dist/components/ai/ask-box.js +85 -0
- package/dist/components/ai/assistant.d.ts +94 -0
- package/dist/components/ai/assistant.js +229 -0
- package/dist/components/ai/dictation.d.ts +57 -0
- package/dist/components/ai/dictation.js +148 -0
- package/dist/components/data/sectioned-list.d.ts +37 -0
- package/dist/components/data/sectioned-list.js +139 -0
- package/dist/components/data/task-row.d.ts +22 -0
- package/dist/components/data/task-row.js +60 -0
- package/dist/components/layout/collapse.d.ts +14 -0
- package/dist/components/layout/collapse.js +24 -0
- package/dist/components/layout/media-item.d.ts +13 -0
- package/dist/components/layout/media-item.js +54 -0
- package/dist/components/layout/metric.d.ts +15 -0
- package/dist/components/layout/metric.js +51 -0
- package/dist/components/layout/show-more.d.ts +17 -0
- package/dist/components/layout/show-more.js +30 -0
- package/dist/components/layout/stack.d.ts +39 -0
- package/dist/components/layout/stack.js +114 -0
- package/dist/components/layout/text.d.ts +37 -0
- package/dist/components/layout/text.js +52 -0
- package/dist/components/theme/theme-toggle.d.ts +4 -1
- package/dist/components/theme/theme-toggle.js +11 -6
- package/dist/components/ui/check-circle.d.ts +16 -0
- package/dist/components/ui/check-circle.js +39 -0
- package/dist/components/ui/sonner.d.ts +15 -2
- package/dist/components/ui/sonner.js +20 -3
- package/dist/index.d.ts +18 -2
- package/dist/index.js +18 -2
- package/dist/styles.css +75 -0
- package/docs/COMPONENTS.md +160 -21
- package/docs/ROADMAP.md +95 -0
- package/docs/guides/agents.md +75 -0
- package/docs/guides/getting-started.md +217 -0
- package/docs/guides/theming.md +99 -0
- package/llms.txt +11 -3
- package/package.json +2 -1
- package/skills/harebear-ui/SKILL.md +2 -1
package/AGENTS.md
CHANGED
|
@@ -14,6 +14,7 @@ Consumers get compiled ESM + types from `dist/` and a Tailwind stylesheet (`hare
|
|
|
14
14
|
| `pnpm gen` | Regenerate `src/index.ts` from component export lists. Run after adding or renaming exports. |
|
|
15
15
|
| `pnpm format` | Biome format + safe fixes. |
|
|
16
16
|
| `pnpm shadcn add <name>` | Pull a new shadcn component into `src/components/ui/`. |
|
|
17
|
+
| `pnpm shots [filter]` | Regenerate README images in `.github/assets/` (needs `pnpm dev` running, Chrome and `cwebp`). |
|
|
17
18
|
|
|
18
19
|
## Layout
|
|
19
20
|
|
|
@@ -24,27 +25,46 @@ src/
|
|
|
24
25
|
index.ts Generated barrel. Do not edit by hand.
|
|
25
26
|
components/ui/ shadcn primitives (radix-nova style), restyled. Keep upstream structure.
|
|
26
27
|
components/theme/ ThemeProvider, ThemeToggle, ThemeScript.
|
|
27
|
-
components/admin/ Layout and dashboard composites (AppShell,
|
|
28
|
-
components/data/ DataTable, TrendChart, Sparkline, BarList, StatusBars.
|
|
29
|
-
components/ai/ Chat composites (ChatThread, ChatMessage, PromptInput,
|
|
28
|
+
components/admin/ Layout and dashboard composites (AppShell, AppNav, FocusLayout, SettingsSection, StatCard...).
|
|
29
|
+
components/data/ DataTable, TrendChart, Sparkline, BarList, StatusBars, TaskRow, SectionedList.
|
|
30
|
+
components/ai/ Chat composites (ChatThread, ChatMessage, PromptInput...) and the assistant layer (AssistantProvider, AssistantDock, AskBox, DictationButton, ActionReceipt).
|
|
30
31
|
components/auth/ AuthLayout, LoginForm, SignupForm (parameterized shadcn login/signup blocks).
|
|
32
|
+
components/layout/ Prop-driven layout and text (Stack, Text, Metric, ShowMore, Collapse).
|
|
31
33
|
hooks/, lib/utils.ts Shared hooks and cn().
|
|
32
|
-
playground/screens/ Reference screens that use only public exports (#admin, #chat, #records, #home, #auth, #catalog). Visual spec and usage examples.
|
|
33
|
-
docs/COMPONENTS.md Consumer-facing catalog. Shipped. Test-enforced to mention every export.
|
|
34
|
+
playground/screens/ Reference screens that use only public exports (#admin, #chat, #agent, #records, #home, #household, #settings, #auth, #catalog). Visual spec and usage examples.
|
|
35
|
+
docs/COMPONENTS.md Consumer-facing catalog. Shipped. Test-enforced to mention every export. "Agent-first apps" is the recipe for household/assistant apps.
|
|
36
|
+
docs/ROADMAP.md Proposed and built components for household and agent-first apps. Mark items built when they land.
|
|
37
|
+
docs/guides/ Getting started (per framework), theming, working with agents. Shipped.
|
|
34
38
|
llms.txt, skills/ Consumer-facing agent docs. Shipped.
|
|
35
|
-
bin/harebear-ui.mjs `npx harebear-ui init|docs` for consuming apps.
|
|
39
|
+
bin/harebear-ui.mjs `npx harebear-ui init|docs [name...]` for consuming apps. Zero dependencies.
|
|
40
|
+
scripts/screenshots.mjs `pnpm shots`: captures playground screens and renders .github/assets/art/*.html to images.
|
|
41
|
+
.github/assets/ README imagery (not shipped): logo.svg, screens/*.webp, hero/light-dark/agents art, social.png.
|
|
36
42
|
test/ Vitest: API snapshot, docs coverage, token integrity, behavior.
|
|
37
43
|
```
|
|
38
44
|
|
|
39
45
|
## Adding a component
|
|
40
46
|
|
|
41
|
-
1. Pick the folder by purpose (`admin`, `data`, `ai`, `auth`; `ui` only for shadcn primitives).
|
|
47
|
+
1. Pick the folder by purpose (`admin`, `layout`, `data`, `ai`, `auth`; `ui` only for shadcn primitives and small harebear primitives such as `CheckCircle`).
|
|
42
48
|
2. Follow the existing files: `"use client"` first line, named function component, props interface exported as `<Name>Props`, `data-slot="<kebab-name>"` on the root, `className` merged last with `cn()`, rest props spread on the root.
|
|
43
|
-
3. Style only with tokens (`bg-card`, `text-muted-foreground`, `bg-primary-soft`, `shadow-card`, `rounded-xl`). No hex, no palette colors. Status takes a `tone` (`success | warning | danger | info | neutral`).
|
|
44
|
-
4.
|
|
45
|
-
5.
|
|
46
|
-
6.
|
|
47
|
-
7. `pnpm
|
|
49
|
+
3. Style only with tokens (`bg-card`, `text-muted-foreground`, `bg-primary-soft`, `shadow-card`, `rounded-xl`). No hex, no palette colors. Status takes a `tone` (`success | warning | danger | info | neutral`); categories (members, lists) use `category-1..6`. A box around a bare field uses `focus-box`. Motion is CSS only and off under `prefers-reduced-motion` (`motion-reduce:`; `Collapse` for height changes).
|
|
50
|
+
4. Floors: checks 24px, phone touch targets 44px, no text under 12px, icon-only buttons have `aria-label`.
|
|
51
|
+
5. `pnpm gen`, then add the component to `docs/COMPONENTS.md` (the docs test fails otherwise) and to the index in `llms.txt`. Use a heading like ``### `Name` `` or a table row with the name in backticks, so `npx harebear-ui docs Name` finds it.
|
|
52
|
+
6. Use it in a playground screen if it is visual, and look at it in light and dark mode and at 375px width.
|
|
53
|
+
7. Add a behavior test in `test/` if it has logic (`components.test.tsx`, `building-blocks.test.tsx`, `household.test.tsx` for the household and assistant layer). `pnpm test -u` to accept the new export in the snapshot.
|
|
54
|
+
8. `pnpm check`.
|
|
55
|
+
|
|
56
|
+
## Looking at your work
|
|
57
|
+
|
|
58
|
+
- `pnpm dev`, then open http://localhost:5199/#admin (or `#chat`, `#agent`, `#records`, `#home`, `#household`, `#settings`, `#auth`, `#catalog`).
|
|
59
|
+
- `?mode=dark` or `?mode=light` pins the theme without touching localStorage; `?shot` hides the screen switcher. Example: http://localhost:5199/?mode=dark&shot#agent
|
|
60
|
+
- A headless capture: `pnpm shots admin` writes `.github/assets/screens/admin-*.webp`.
|
|
61
|
+
|
|
62
|
+
## README imagery
|
|
63
|
+
|
|
64
|
+
- `.github/assets/logo.svg` is the mark. It is hand-written SVG; edit it directly. `logo.png` is exported from it.
|
|
65
|
+
- Screens are real playground captures. Art (`hero`, `light-dark`, `agents`) is HTML in `.github/assets/art/` that composes those captures, rendered by `pnpm shots`. `social.png` is the hero at 1x for GitHub's social preview (upload it by hand in repo Settings).
|
|
66
|
+
- After a visible change to a screen or the theme, run `pnpm shots` and commit the images with the change. Check the results by eye before committing.
|
|
67
|
+
- If `npx harebear-ui init` output changes, update the terminal text in `art/agents.html` to match.
|
|
48
68
|
|
|
49
69
|
## Changing shadcn primitives
|
|
50
70
|
|
package/README.md
CHANGED
|
@@ -1,28 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/assets/logo.svg" width="72" height="72" alt="harebear-ui logo" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">harebear-ui</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Pre-themed React components for admin tools, data apps and AI chat.<br />
|
|
9
|
+
shadcn/ui restyled to one design system, plus the composites you build every time. Agent-ready out of the box.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/harebear-ui"><img src="https://img.shields.io/npm/v/harebear-ui?style=flat-square&color=5b84ff&labelColor=101012" alt="npm version" /></a>
|
|
14
|
+
<a href="https://github.com/HMilbradt/harebear-ui/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/HMilbradt/harebear-ui/ci.yml?branch=main&style=flat-square&labelColor=101012&label=CI" alt="CI status" /></a>
|
|
15
|
+
<img src="https://img.shields.io/badge/React-19-5b84ff?style=flat-square&labelColor=101012" alt="React 19" />
|
|
16
|
+
<img src="https://img.shields.io/badge/Tailwind_CSS-v4-a98ae0?style=flat-square&labelColor=101012" alt="Tailwind CSS v4" />
|
|
17
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/harebear-ui?style=flat-square&color=3cb8a8&labelColor=101012" alt="MIT license" /></a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src=".github/assets/hero.webp" alt="harebear-ui: an admin dashboard, an agent chat with approvals and a records table, all in dark mode" />
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<a href="docs/guides/getting-started.md"><b>Get started</b></a> ·
|
|
26
|
+
<a href="docs/COMPONENTS.md"><b>Components</b></a> ·
|
|
27
|
+
<a href="docs/guides/theming.md"><b>Theming</b></a> ·
|
|
28
|
+
<a href="docs/guides/agents.md"><b>AI agents</b></a> ·
|
|
29
|
+
<a href="playground/screens"><b>Reference screens</b></a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
## Why
|
|
33
|
+
|
|
34
|
+
Every internal tool, dashboard and chat app ends up needing the same pieces: an app shell with a collapsible sidebar, KPI cards, a sortable table with a detail panel, trend charts, a chat thread with a composer and tool calls. shadcn/ui gives you primitives to build them. harebear-ui gives you them built, in one consistent look, so you (or your agent) spend the time on the product.
|
|
35
|
+
|
|
36
|
+
- **Every shadcn/ui primitive**, same names and props, restyled to one set of tokens.
|
|
37
|
+
- **Composites on top:** `AppShell`, `StatCard`, `DataTable`, `TrendChart`, `ChatShell`, `PromptInput`, `ToolStep`, `ApprovalCard`, `LoginForm` and more.
|
|
38
|
+
- **Light and dark from one accent.** Change the brand color, radius and font with props or CSS variables.
|
|
39
|
+
- **Docs agents can use.** `npx harebear-ui init` installs project rules and a Claude Code skill; `npx harebear-ui docs DataTable` prints one component's API.
|
|
40
|
+
|
|
41
|
+
## Screens
|
|
42
|
+
|
|
43
|
+
Built only from public exports. Each one is a file in [`playground/screens/`](playground/screens) you can copy from.
|
|
44
|
+
|
|
45
|
+
<table>
|
|
46
|
+
<tr>
|
|
47
|
+
<td width="50%"><a href="playground/screens/admin.tsx"><img src=".github/assets/screens/admin-dark.webp" alt="Admin dashboard with KPI cards, a visitors trend chart and traffic sources" /></a><br /><b>Admin dashboard</b>: <code>AppShell</code>, <code>StatCard</code>, <code>TrendChart</code>, <code>BarList</code></td>
|
|
48
|
+
<td width="50%"><a href="playground/screens/agent.tsx"><img src=".github/assets/screens/agent-dark.webp" alt="Agent chat waiting on a command approval, with a details panel" /></a><br /><b>Agent with approvals</b>: <code>ChatShell</code>, <code>ToolGroup</code>, <code>ApprovalCard</code></td>
|
|
49
|
+
</tr>
|
|
50
|
+
<tr>
|
|
51
|
+
<td width="50%"><a href="playground/screens/chat.tsx"><img src=".github/assets/screens/chat-dark.webp" alt="AI chat with a conversation list, highlighted code and a prompt composer" /></a><br /><b>AI chat</b>: <code>ChatPanel</code>, <code>ChatMessage</code>, <code>CodeSnippet</code>, <code>PromptInput</code></td>
|
|
52
|
+
<td width="50%"><a href="playground/screens/records.tsx"><img src=".github/assets/screens/records-dark.webp" alt="Customer table with status badges and a detail panel for the selected row" /></a><br /><b>Records</b>: <code>DataTable</code>, <code>KeyValueGrid</code>, <code>Timeline</code></td>
|
|
53
|
+
</tr>
|
|
54
|
+
<tr>
|
|
55
|
+
<td width="50%"><a href="playground/screens/home.tsx"><img src=".github/assets/screens/home-dark.webp" alt="Status home page with service tiles and uptime bars" /></a><br /><b>Status home</b>: <code>LinkTile</code>, <code>StatusBars</code>, <code>Sparkline</code></td>
|
|
56
|
+
<td width="50%"><a href="playground/screens/auth.tsx"><img src=".github/assets/screens/auth-dark.webp" alt="Sign-in page" /></a><br /><b>Auth</b>: <code>AuthLayout</code>, <code>LoginForm</code>, <code>SignupForm</code></td>
|
|
57
|
+
</tr>
|
|
58
|
+
</table>
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
Needs React 19 and Tailwind CSS v4.
|
|
8
63
|
|
|
9
64
|
```bash
|
|
10
65
|
npm i harebear-ui
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
66
|
npx harebear-ui init
|
|
15
67
|
```
|
|
16
68
|
|
|
17
|
-
`init` adds the stylesheet import
|
|
18
|
-
|
|
19
|
-
Manual setup, if you prefer:
|
|
20
|
-
|
|
21
|
-
```css
|
|
22
|
-
/* app.css */
|
|
23
|
-
@import "tailwindcss";
|
|
24
|
-
@import "harebear-ui/styles.css";
|
|
25
|
-
```
|
|
69
|
+
`init` adds the stylesheet import after `@import "tailwindcss"`, writes a harebear-ui section into `AGENTS.md` (or `CLAUDE.md`), and installs a Claude Code skill. Then wrap your root:
|
|
26
70
|
|
|
27
71
|
```tsx
|
|
28
72
|
import { ThemeProvider, Toaster, TooltipProvider } from "harebear-ui"
|
|
@@ -39,24 +83,26 @@ export function Root() {
|
|
|
39
83
|
}
|
|
40
84
|
```
|
|
41
85
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## Use
|
|
86
|
+
And build:
|
|
45
87
|
|
|
46
88
|
```tsx
|
|
47
|
-
import { AppShell,
|
|
89
|
+
import { AppShell, Button, DataTable, PageHeader, StatCard } from "harebear-ui"
|
|
48
90
|
```
|
|
49
91
|
|
|
50
|
-
Everything imports from the package root.
|
|
92
|
+
Everything imports from the package root. Geist and Geist Mono are bundled.
|
|
51
93
|
|
|
52
|
-
- **
|
|
53
|
-
- **Reference screens:** [playground/screens](playground/screens) has an admin dashboard, an AI chat, a data table with detail panel, and a status home page built only from public exports.
|
|
94
|
+
Step-by-step setup for **Vite**, **Next.js** and **React Router**, plus troubleshooting: [Getting started](docs/guides/getting-started.md).
|
|
54
95
|
|
|
55
96
|
## Theming
|
|
56
97
|
|
|
57
|
-
|
|
98
|
+
<img src=".github/assets/light-dark.webp" alt="The same admin dashboard split diagonally between light and dark mode" />
|
|
99
|
+
|
|
100
|
+
Light, dark and system modes, accent, radius and font are `ThemeProvider` props, changeable live with `useTheme()`. Every component reads shadcn-named tokens, so you can override any of them in CSS:
|
|
58
101
|
|
|
59
102
|
```css
|
|
103
|
+
@import "tailwindcss";
|
|
104
|
+
@import "harebear-ui/styles.css";
|
|
105
|
+
|
|
60
106
|
:root {
|
|
61
107
|
--brand: #0f8a7e;
|
|
62
108
|
--radius: 0.625rem;
|
|
@@ -66,19 +112,43 @@ Light and dark mode, accent, radius, and font are runtime props on `ThemeProvide
|
|
|
66
112
|
}
|
|
67
113
|
```
|
|
68
114
|
|
|
69
|
-
|
|
115
|
+
SSR apps render `<ThemeScript />` in `<head>` to avoid a flash of the wrong mode. More in [Theming](docs/guides/theming.md).
|
|
70
116
|
|
|
71
|
-
##
|
|
117
|
+
## Built for AI agents
|
|
72
118
|
|
|
73
|
-
|
|
119
|
+
<img src=".github/assets/agents.webp" alt="Terminal running harebear-ui init and docs, with the four agent files it sets up" />
|
|
74
120
|
|
|
75
|
-
|
|
76
|
-
| ----------------------------- | ------------------------------------------------------ |
|
|
77
|
-
| `llms.txt` | Short index: setup, rules, component list. |
|
|
78
|
-
| `docs/COMPONENTS.md` | Every export with props and examples. |
|
|
79
|
-
| `skills/harebear-ui/SKILL.md` | Claude Code skill; `npx harebear-ui init` installs it. |
|
|
121
|
+
The package carries its own agent docs, so Claude Code, Codex, Cursor and friends know what exists and how to use it:
|
|
80
122
|
|
|
81
|
-
|
|
123
|
+
| File | For |
|
|
124
|
+
| ----------------------------- | --------------------------------------------------------------------- |
|
|
125
|
+
| `llms.txt` | Short index: setup, rules, every export by group. |
|
|
126
|
+
| `docs/COMPONENTS.md` | Every export with props and examples. Test-enforced to stay complete. |
|
|
127
|
+
| `docs/guides/` | Setup per framework, theming, working with agents. |
|
|
128
|
+
| `skills/harebear-ui/SKILL.md` | Claude Code skill; `npx harebear-ui init` installs it. |
|
|
129
|
+
|
|
130
|
+
Agents look up one component at a time instead of reading the whole catalog:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx harebear-ui docs DataTable StatCard # exports
|
|
134
|
+
npx harebear-ui docs Prompt # prefix: PromptInput, PromptInputChip, PromptInputSelect
|
|
135
|
+
npx harebear-ui docs Tokens # a section
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Prompts that work, and what to check before you accept an agent's UI: [Working with AI agents](docs/guides/agents.md).
|
|
139
|
+
|
|
140
|
+
## What's inside
|
|
141
|
+
|
|
142
|
+
| Group | Exports |
|
|
143
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
144
|
+
| Layout, admin | `SidebarLayout`, `AppShell`, `AppNav`, `FocusLayout`, `SettingsSection`, `PageHeader`, `StatCard`, `StatusBadge`, `UserMenu`, `ConfirmDialog` |
|
|
145
|
+
| Data | `SectionedList`, `TaskRow`, `DataTable`, `TrendChart`, `BarList`, `Sparkline`, `StatusBars` |
|
|
146
|
+
| Assistant | `AssistantProvider`, `AssistantDock`, `AskBox`, `DictationButton`, `ActionReceipt`, `toastUndo` |
|
|
147
|
+
| AI and chat | `ChatShell`, `ChatPanel`, `ChatMessage`, `PromptInput`, `ModelPicker`, `ToolGroup`, `ApprovalCard`, `CodeSnippet`, `Prose` |
|
|
148
|
+
| Auth | `AuthLayout`, `LoginForm`, `SignupForm` |
|
|
149
|
+
| Primitives | The full shadcn/ui registry: `Button`, `Dialog`, `Select`, `Sidebar`, `Tabs`, `Command`, `Chart`, and the rest |
|
|
150
|
+
|
|
151
|
+
The full list with props: [docs/COMPONENTS.md](docs/COMPONENTS.md).
|
|
82
152
|
|
|
83
153
|
## Develop
|
|
84
154
|
|
|
@@ -86,8 +156,11 @@ Working on this repo with an agent: see [AGENTS.md](AGENTS.md).
|
|
|
86
156
|
pnpm install
|
|
87
157
|
pnpm dev # playground on :5199
|
|
88
158
|
pnpm check # lint, typecheck, test, build, package lint
|
|
159
|
+
pnpm shots # regenerate the README images (needs pnpm dev running, Chrome and cwebp)
|
|
89
160
|
```
|
|
90
161
|
|
|
162
|
+
Working on this repo with an agent: see [AGENTS.md](AGENTS.md).
|
|
163
|
+
|
|
91
164
|
## License
|
|
92
165
|
|
|
93
166
|
MIT
|
package/bin/harebear-ui.mjs
CHANGED
|
@@ -24,6 +24,7 @@ const AGENT_BLOCK = `${START}
|
|
|
24
24
|
All UI in this project uses the \`harebear-ui\` component library (pre-themed shadcn/ui on Tailwind v4, plus admin, data table, chart, and AI chat components).
|
|
25
25
|
|
|
26
26
|
- Read \`node_modules/harebear-ui/docs/COMPONENTS.md\` before building UI. It lists every component with props and examples.
|
|
27
|
+
- Look up single entries with \`npx harebear-ui docs DataTable PromptInput\` (export names, a prefix, or a section such as \`Tokens\`).
|
|
27
28
|
- Import from the package root: \`import { Button, Card, DataTable } from "harebear-ui"\`.
|
|
28
29
|
- Style only with theme tokens (\`bg-card\`, \`bg-muted\`, \`text-muted-foreground\`, \`bg-primary-soft\`, \`text-success\`, \`shadow-card\`, \`rounded-xl\`). No hex or Tailwind palette colors.
|
|
29
30
|
- Check new screens in light and dark mode and at phone width.
|
|
@@ -86,6 +87,50 @@ function installSkill() {
|
|
|
86
87
|
console.log(`✓ Installed Claude Code skill to ${relative(cwd, destDir)}/SKILL.md`)
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
// Backticked identifiers in a heading or table cell: "`ChatMessage`" -> "chatmessage".
|
|
91
|
+
const namesIn = (text) => [...text.matchAll(/`([A-Za-z]\w*)/g)].map((m) => m[1].toLowerCase())
|
|
92
|
+
|
|
93
|
+
// The parts of the catalog about one export or section: "### `Name`" sections, primitive table rows
|
|
94
|
+
// (with their table header), or a whole "## Section" by title. Exact names first, then prefix matches.
|
|
95
|
+
function lookup(catalog, query) {
|
|
96
|
+
const q = query.toLowerCase()
|
|
97
|
+
const blocks = catalog.split(/^(?=#{2,3} )/m)
|
|
98
|
+
const section = blocks.find((b) => b.startsWith("## ") && b.split("\n", 1)[0].slice(3).toLowerCase() === q)
|
|
99
|
+
if (section) {
|
|
100
|
+
// A "## Section" block stops at its first "###"; take the subsections that follow it too.
|
|
101
|
+
const start = blocks.indexOf(section)
|
|
102
|
+
const end = blocks.findIndex((b, i) => i > start && b.startsWith("## "))
|
|
103
|
+
return blocks
|
|
104
|
+
.slice(start, end < 0 ? undefined : end)
|
|
105
|
+
.join("")
|
|
106
|
+
.trimEnd()
|
|
107
|
+
}
|
|
108
|
+
for (const match of [(n) => n === q, (n) => n.startsWith(q)]) {
|
|
109
|
+
const out = blocks.filter((b) => b.startsWith("### ") && namesIn(b.split("\n", 1)[0]).some(match))
|
|
110
|
+
let header = []
|
|
111
|
+
let headerShown = null
|
|
112
|
+
const rows = []
|
|
113
|
+
for (const line of catalog.split("\n")) {
|
|
114
|
+
if (!line.startsWith("|")) {
|
|
115
|
+
header = []
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
if (header.length < 2) {
|
|
119
|
+
header.push(line)
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
const [, first = "", second = ""] = line.split("|")
|
|
123
|
+
if (!namesIn(first + second).some(match)) continue
|
|
124
|
+
if (headerShown !== header) rows.push(...header)
|
|
125
|
+
headerShown = header
|
|
126
|
+
rows.push(line)
|
|
127
|
+
}
|
|
128
|
+
if (rows.length) out.push(rows.join("\n"))
|
|
129
|
+
if (out.length) return out.map((b) => b.trimEnd()).join("\n\n")
|
|
130
|
+
}
|
|
131
|
+
return undefined
|
|
132
|
+
}
|
|
133
|
+
|
|
89
134
|
switch (cmd) {
|
|
90
135
|
case "init": {
|
|
91
136
|
if (!existsSync(join(cwd, "package.json"))) {
|
|
@@ -109,9 +154,25 @@ Next: wrap your app root.
|
|
|
109
154
|
`)
|
|
110
155
|
break
|
|
111
156
|
}
|
|
112
|
-
case "docs":
|
|
113
|
-
|
|
157
|
+
case "docs": {
|
|
158
|
+
const catalog = readFileSync(join(pkgRoot, "docs/COMPONENTS.md"), "utf8")
|
|
159
|
+
const queries = args.filter((a) => !a.startsWith("--"))
|
|
160
|
+
if (queries.length === 0) {
|
|
161
|
+
process.stdout.write(catalog)
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
let missed = false
|
|
165
|
+
for (const q of queries) {
|
|
166
|
+
const found = lookup(catalog, q)
|
|
167
|
+
if (found) process.stdout.write(`${found}\n\n`)
|
|
168
|
+
else {
|
|
169
|
+
missed = true
|
|
170
|
+
console.error(`! Nothing in the catalog for "${q}". Run \`npx harebear-ui docs\` for the full catalog.`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (missed) process.exitCode = 1
|
|
114
174
|
break
|
|
175
|
+
}
|
|
115
176
|
default:
|
|
116
177
|
console.log(`harebear-ui
|
|
117
178
|
|
|
@@ -119,7 +180,9 @@ Next: wrap your app root.
|
|
|
119
180
|
Add the stylesheet import after @import "tailwindcss", add a harebear-ui
|
|
120
181
|
section to AGENTS.md (or CLAUDE.md), and install the Claude Code skill.
|
|
121
182
|
|
|
122
|
-
npx harebear-ui docs
|
|
183
|
+
npx harebear-ui docs [name...]
|
|
123
184
|
Print the component catalog (props and examples for every export).
|
|
185
|
+
With names, print only those entries: an export (DataTable, Button),
|
|
186
|
+
a name prefix (Prompt), or a section title (Tokens, Auth).
|
|
124
187
|
`)
|
|
125
188
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { AppNavItem } from "./app-shell.js";
|
|
2
|
+
import { SidebarLayoutProps } from "./sidebar-layout.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
export interface AppNavPlace extends Omit<AppNavItem, "badge"> {
|
|
5
|
+
/** Count at the right in the sidebar and on the icon in the tab bar, e.g. open to-dos. Hidden when 0. */
|
|
6
|
+
count?: number;
|
|
7
|
+
/** Show in the phone tab bar. Default true; the first 5 that are not false are shown. */
|
|
8
|
+
tab?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface AppNavGroup {
|
|
11
|
+
/** Group heading, e.g. "Lists". */
|
|
12
|
+
label: string;
|
|
13
|
+
items: AppNavPlace[];
|
|
14
|
+
/** Expanded at first. Default true. */
|
|
15
|
+
defaultOpen?: boolean;
|
|
16
|
+
/** Shows an add button next to the heading. */
|
|
17
|
+
onAdd?: () => void;
|
|
18
|
+
/** Accessible label of the add button, e.g. "New list". Default "Add". */
|
|
19
|
+
addLabel?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AppNavAction {
|
|
22
|
+
label: string;
|
|
23
|
+
icon: React.ReactNode;
|
|
24
|
+
onSelect: () => void;
|
|
25
|
+
}
|
|
26
|
+
export interface AppNavProps {
|
|
27
|
+
/** Product mark at the top of the sidebar, usually <AppBrand />. */
|
|
28
|
+
brand?: React.ReactNode;
|
|
29
|
+
/** Top-level places (Today, Calendar, Docs). Sidebar items on desktop, tabs on phones. */
|
|
30
|
+
places: AppNavPlace[];
|
|
31
|
+
/** One collapsible group under the places, e.g. Lists with an add button. Desktop sidebar only. */
|
|
32
|
+
group?: AppNavGroup;
|
|
33
|
+
/** Sidebar bottom, usually <UserMenu />. Desktop sidebar only. */
|
|
34
|
+
footer?: React.ReactNode;
|
|
35
|
+
/** Primary action: a button under the brand on desktop, a round floating button above the tab bar on phones. */
|
|
36
|
+
action?: AppNavAction;
|
|
37
|
+
/** Accessible name of the navigation. Default "Main". */
|
|
38
|
+
label?: string;
|
|
39
|
+
/** Component used for links. Default "a". Pass Next.js Link or React Router Link. */
|
|
40
|
+
linkComponent?: React.ElementType;
|
|
41
|
+
collapsible?: SidebarLayoutProps["collapsible"];
|
|
42
|
+
storageKey?: string;
|
|
43
|
+
defaultOpen?: boolean;
|
|
44
|
+
/** Sidebar width. Default "15.75rem". */
|
|
45
|
+
width?: string;
|
|
46
|
+
/** Classes for the main column. */
|
|
47
|
+
className?: string;
|
|
48
|
+
children?: React.ReactNode;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* App navigation from one config. Desktop (768px and up): SidebarLayout with the brand, action, places, a
|
|
52
|
+
* collapsible group and the footer; the active place is a raised card. Phones: a bottom tab bar with up to 5 places
|
|
53
|
+
* and an optional round action button. The main column gets bottom padding and sets `--hb-bottom-inset` (and
|
|
54
|
+
* `--hb-fab-inset`) so an AssistantDock rendered inside sits above the tab bar and the action button.
|
|
55
|
+
*/
|
|
56
|
+
declare function AppNav({ brand, places, group, footer, action, label, linkComponent, collapsible, storageKey, defaultOpen, width, className, children }: AppNavProps): React.JSX.Element;
|
|
57
|
+
export { AppNav };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { SidebarLayout } from "./sidebar-layout.js";
|
|
3
|
+
import { Badge } from "../ui/badge.js";
|
|
4
|
+
import { Button } from "../ui/button.js";
|
|
5
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
6
|
+
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar.js";
|
|
7
|
+
import { cn } from "../../lib/utils.js";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
import { ChevronDownIcon, PlusIcon } from "lucide-react";
|
|
11
|
+
// src/components/admin/app-nav.tsx
|
|
12
|
+
const TAB_LIMIT = 5;
|
|
13
|
+
/**
|
|
14
|
+
* App navigation from one config. Desktop (768px and up): SidebarLayout with the brand, action, places, a
|
|
15
|
+
* collapsible group and the footer; the active place is a raised card. Phones: a bottom tab bar with up to 5 places
|
|
16
|
+
* and an optional round action button. The main column gets bottom padding and sets `--hb-bottom-inset` (and
|
|
17
|
+
* `--hb-fab-inset`) so an AssistantDock rendered inside sits above the tab bar and the action button.
|
|
18
|
+
*/
|
|
19
|
+
function AppNav({ brand, places, group, footer, action, label = "Main", linkComponent = "a", collapsible, storageKey, defaultOpen, width = "15.75rem", className, children }) {
|
|
20
|
+
const tabs = places.filter((p) => p.tab !== false).slice(0, TAB_LIMIT);
|
|
21
|
+
return /* @__PURE__ */ jsxs(SidebarLayout, {
|
|
22
|
+
brand,
|
|
23
|
+
collapsible,
|
|
24
|
+
storageKey,
|
|
25
|
+
defaultOpen,
|
|
26
|
+
width,
|
|
27
|
+
sidebarFooter: footer,
|
|
28
|
+
sidebarActions: action && /* @__PURE__ */ jsxs(Button, {
|
|
29
|
+
type: "button",
|
|
30
|
+
"aria-label": action.label,
|
|
31
|
+
onClick: action.onSelect,
|
|
32
|
+
className: "w-full justify-start group-data-[collapsible=icon]:size-10 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0",
|
|
33
|
+
children: [action.icon, /* @__PURE__ */ jsx("span", {
|
|
34
|
+
className: "group-data-[collapsible=icon]:hidden",
|
|
35
|
+
children: action.label
|
|
36
|
+
})]
|
|
37
|
+
}),
|
|
38
|
+
sidebar: /* @__PURE__ */ jsxs("nav", {
|
|
39
|
+
"aria-label": label,
|
|
40
|
+
"data-slot": "app-nav",
|
|
41
|
+
className: "flex flex-col gap-5",
|
|
42
|
+
children: [/* @__PURE__ */ jsx(SidebarMenu, {
|
|
43
|
+
className: "gap-1 group-data-[collapsible=icon]:items-center",
|
|
44
|
+
children: places.map((place) => /* @__PURE__ */ jsx(PlaceItem, {
|
|
45
|
+
place,
|
|
46
|
+
Link: linkComponent
|
|
47
|
+
}, place.label))
|
|
48
|
+
}), group && /* @__PURE__ */ jsx(NavGroup, {
|
|
49
|
+
group,
|
|
50
|
+
Link: linkComponent
|
|
51
|
+
})]
|
|
52
|
+
}),
|
|
53
|
+
className: cn("max-md:pb-[calc(4.5rem+env(safe-area-inset-bottom))] max-md:[--hb-bottom-inset:calc(4.5rem+env(safe-area-inset-bottom))]", action && "max-md:[--hb-fab-inset:4.5rem]", className),
|
|
54
|
+
children: [
|
|
55
|
+
children,
|
|
56
|
+
tabs.length > 0 && /* @__PURE__ */ jsx(TabBar, {
|
|
57
|
+
tabs,
|
|
58
|
+
label,
|
|
59
|
+
Link: linkComponent
|
|
60
|
+
}),
|
|
61
|
+
action && /* @__PURE__ */ jsx(Button, {
|
|
62
|
+
type: "button",
|
|
63
|
+
"data-slot": "app-nav-fab",
|
|
64
|
+
size: "icon-lg",
|
|
65
|
+
shape: "pill",
|
|
66
|
+
"aria-label": action.label,
|
|
67
|
+
onClick: action.onSelect,
|
|
68
|
+
className: "fixed right-4 bottom-[calc(4.5rem+env(safe-area-inset-bottom)+1rem)] z-30 size-14 shadow-card md:hidden [&_svg:not([class*='size-'])]:size-6",
|
|
69
|
+
children: action.icon
|
|
70
|
+
})
|
|
71
|
+
]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function PlaceItem({ place, Link }) {
|
|
75
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
76
|
+
place.icon,
|
|
77
|
+
/* @__PURE__ */ jsx("span", {
|
|
78
|
+
className: "flex-1",
|
|
79
|
+
children: place.label
|
|
80
|
+
}),
|
|
81
|
+
!!place.count && /* @__PURE__ */ jsx(Badge, {
|
|
82
|
+
variant: "soft",
|
|
83
|
+
className: "h-5 px-2 group-data-[collapsible=icon]:hidden",
|
|
84
|
+
children: place.count
|
|
85
|
+
})
|
|
86
|
+
] });
|
|
87
|
+
return /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsx(SidebarMenuButton, {
|
|
88
|
+
asChild: !!place.href,
|
|
89
|
+
isActive: place.active,
|
|
90
|
+
"aria-current": place.active ? "page" : void 0,
|
|
91
|
+
onClick: place.onSelect,
|
|
92
|
+
tooltip: place.label,
|
|
93
|
+
children: place.href ? /* @__PURE__ */ jsx(Link, {
|
|
94
|
+
href: place.href,
|
|
95
|
+
children: content
|
|
96
|
+
}) : content
|
|
97
|
+
}) });
|
|
98
|
+
}
|
|
99
|
+
function NavGroup({ group, Link }) {
|
|
100
|
+
const [open, setOpen] = React.useState(group.defaultOpen ?? true);
|
|
101
|
+
return /* @__PURE__ */ jsxs(Collapsible, {
|
|
102
|
+
open,
|
|
103
|
+
onOpenChange: setOpen,
|
|
104
|
+
"data-slot": "app-nav-group",
|
|
105
|
+
className: "flex flex-col gap-1 group-data-[collapsible=icon]:hidden",
|
|
106
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
107
|
+
className: "flex items-center gap-1 pr-1",
|
|
108
|
+
children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
|
|
109
|
+
className: "flex h-8 min-w-0 flex-1 items-center gap-1.5 rounded-md px-3 text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/40",
|
|
110
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
111
|
+
className: "truncate",
|
|
112
|
+
children: group.label
|
|
113
|
+
}), /* @__PURE__ */ jsx(ChevronDownIcon, {
|
|
114
|
+
"aria-hidden": "true",
|
|
115
|
+
className: cn("size-3.5 transition-transform motion-reduce:transition-none", !open && "-rotate-90")
|
|
116
|
+
})]
|
|
117
|
+
}), group.onAdd && /* @__PURE__ */ jsx(Button, {
|
|
118
|
+
type: "button",
|
|
119
|
+
variant: "ghost",
|
|
120
|
+
size: "icon-sm",
|
|
121
|
+
"aria-label": group.addLabel ?? "Add",
|
|
122
|
+
onClick: group.onAdd,
|
|
123
|
+
children: /* @__PURE__ */ jsx(PlusIcon, {})
|
|
124
|
+
})]
|
|
125
|
+
}), /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx(SidebarMenu, {
|
|
126
|
+
className: "gap-1",
|
|
127
|
+
children: group.items.map((place) => /* @__PURE__ */ jsx(PlaceItem, {
|
|
128
|
+
place,
|
|
129
|
+
Link
|
|
130
|
+
}, place.label))
|
|
131
|
+
}) })]
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function TabBar({ tabs, label, Link }) {
|
|
135
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
136
|
+
"aria-label": label,
|
|
137
|
+
"data-slot": "app-nav-tabs",
|
|
138
|
+
className: "fixed inset-x-0 bottom-0 z-30 border-t border-border bg-background/95 px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))] backdrop-blur md:hidden",
|
|
139
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
140
|
+
className: "mx-auto flex max-w-lg",
|
|
141
|
+
children: tabs.map((tab) => {
|
|
142
|
+
const Comp = tab.href ? Link : "button";
|
|
143
|
+
return /* @__PURE__ */ jsx("li", {
|
|
144
|
+
className: "flex min-w-0 flex-1",
|
|
145
|
+
children: /* @__PURE__ */ jsxs(Comp, {
|
|
146
|
+
...tab.href ? { href: tab.href } : {
|
|
147
|
+
type: "button",
|
|
148
|
+
onClick: tab.onSelect
|
|
149
|
+
},
|
|
150
|
+
"aria-current": tab.active ? "page" : void 0,
|
|
151
|
+
className: "flex min-h-14 min-w-0 flex-1 flex-col items-center justify-center gap-1 rounded-xl px-1 text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-4 focus-visible:ring-ring/20 aria-[current=page]:text-primary",
|
|
152
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
153
|
+
className: "relative [&_svg]:size-6",
|
|
154
|
+
children: [tab.icon, !!tab.count && /* @__PURE__ */ jsx("span", {
|
|
155
|
+
"aria-hidden": "true",
|
|
156
|
+
className: "absolute -top-1.5 left-[calc(100%-0.5rem)] grid h-4.5 min-w-4.5 place-items-center rounded-full bg-primary px-1 text-xs leading-none font-semibold text-primary-foreground",
|
|
157
|
+
children: tab.count > 99 ? "99+" : tab.count
|
|
158
|
+
})]
|
|
159
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
160
|
+
className: "max-w-full truncate",
|
|
161
|
+
children: [tab.label, !!tab.count && /* @__PURE__ */ jsxs("span", {
|
|
162
|
+
className: "sr-only",
|
|
163
|
+
children: [", ", tab.count]
|
|
164
|
+
})]
|
|
165
|
+
})]
|
|
166
|
+
})
|
|
167
|
+
}, tab.label);
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
export { AppNav };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface FocusLayoutProps extends Omit<React.ComponentProps<"div">, "title"> {
|
|
3
|
+
/** Page title in the top bar. */
|
|
4
|
+
title?: React.ReactNode;
|
|
5
|
+
/** Leading control, usually a close or back link: <Button asChild variant="ghost" size="icon"><a aria-label="Close">…</a></Button>. */
|
|
6
|
+
back?: React.ReactNode;
|
|
7
|
+
/** Trailing top bar controls, e.g. a Save button. */
|
|
8
|
+
actions?: React.ReactNode;
|
|
9
|
+
/** Column width. Default "md" (42rem). */
|
|
10
|
+
width?: "sm" | "md" | "lg";
|
|
11
|
+
}
|
|
12
|
+
/** A page outside the app frame (settings, onboarding, setup): slim top bar with back link and title, one centred column. */
|
|
13
|
+
declare function FocusLayout({ title, back, actions, width, className, children, ...props }: FocusLayoutProps): React.JSX.Element;
|
|
14
|
+
export { FocusLayout };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
// src/components/admin/focus-layout.tsx
|
|
5
|
+
const widthClass = {
|
|
6
|
+
sm: "max-w-xl",
|
|
7
|
+
md: "max-w-2xl",
|
|
8
|
+
lg: "max-w-4xl"
|
|
9
|
+
};
|
|
10
|
+
/** A page outside the app frame (settings, onboarding, setup): slim top bar with back link and title, one centred column. */
|
|
11
|
+
function FocusLayout({ title, back, actions, width = "md", className, children, ...props }) {
|
|
12
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
13
|
+
"data-slot": "focus-layout",
|
|
14
|
+
className: cn("flex min-h-dvh flex-col bg-background", className),
|
|
15
|
+
...props,
|
|
16
|
+
children: [/* @__PURE__ */ jsxs("header", {
|
|
17
|
+
className: "sticky top-0 z-20 flex min-h-14 items-center gap-2 bg-background/90 px-3 pt-[env(safe-area-inset-top)] backdrop-blur sm:px-4",
|
|
18
|
+
children: [
|
|
19
|
+
back,
|
|
20
|
+
title && /* @__PURE__ */ jsx("h1", {
|
|
21
|
+
className: "min-w-0 flex-1 truncate text-[15px] font-semibold",
|
|
22
|
+
children: title
|
|
23
|
+
}),
|
|
24
|
+
!title && /* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
25
|
+
actions && /* @__PURE__ */ jsx("div", {
|
|
26
|
+
className: "flex items-center gap-2",
|
|
27
|
+
children: actions
|
|
28
|
+
})
|
|
29
|
+
]
|
|
30
|
+
}), /* @__PURE__ */ jsx("main", {
|
|
31
|
+
className: cn("mx-auto flex w-full flex-col gap-6 px-4 pt-4 pb-[max(3rem,env(safe-area-inset-bottom))]", widthClass[width]),
|
|
32
|
+
children
|
|
33
|
+
})]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export { FocusLayout };
|