harebear-ui 0.0.0-stage → 0.2.2
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 +66 -0
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/bin/harebear-ui.mjs +125 -0
- package/dist/components/admin/activity-feed.d.ts +15 -0
- package/dist/components/admin/activity-feed.js +50 -0
- package/dist/components/admin/app-shell.d.ts +47 -0
- package/dist/components/admin/app-shell.js +108 -0
- package/dist/components/admin/callout.d.ts +7 -0
- package/dist/components/admin/callout.js +18 -0
- package/dist/components/admin/key-value-grid.d.ts +11 -0
- package/dist/components/admin/key-value-grid.js +23 -0
- package/dist/components/admin/link-tile.d.ts +17 -0
- package/dist/components/admin/link-tile.js +36 -0
- package/dist/components/admin/page-header.d.ts +14 -0
- package/dist/components/admin/page-header.js +41 -0
- package/dist/components/admin/search-input.d.ts +12 -0
- package/dist/components/admin/search-input.js +32 -0
- package/dist/components/admin/stat-card.d.ts +20 -0
- package/dist/components/admin/stat-card.js +53 -0
- package/dist/components/admin/status.d.ts +12 -0
- package/dist/components/admin/status.js +32 -0
- package/dist/components/admin/timeline.d.ts +13 -0
- package/dist/components/admin/timeline.js +36 -0
- package/dist/components/admin/usage-meter.d.ts +17 -0
- package/dist/components/admin/usage-meter.js +43 -0
- package/dist/components/admin/user-chip.d.ts +14 -0
- package/dist/components/admin/user-chip.js +38 -0
- package/dist/components/ai/chat.d.ts +44 -0
- package/dist/components/ai/chat.js +151 -0
- package/dist/components/ai/code-block.d.ts +27 -0
- package/dist/components/ai/code-block.js +63 -0
- package/dist/components/ai/conversation-list.d.ts +18 -0
- package/dist/components/ai/conversation-list.js +30 -0
- package/dist/components/ai/model-picker.d.ts +21 -0
- package/dist/components/ai/model-picker.js +55 -0
- package/dist/components/ai/prompt-input.d.ts +27 -0
- package/dist/components/ai/prompt-input.js +99 -0
- package/dist/components/auth/auth.d.ts +85 -0
- package/dist/components/auth/auth.js +325 -0
- package/dist/components/data/bar-list.d.ts +14 -0
- package/dist/components/data/bar-list.js +27 -0
- package/dist/components/data/data-table.d.ts +70 -0
- package/dist/components/data/data-table.js +208 -0
- package/dist/components/data/sparkline.d.ts +16 -0
- package/dist/components/data/sparkline.js +72 -0
- package/dist/components/data/status-bars.d.ts +17 -0
- package/dist/components/data/status-bars.js +37 -0
- package/dist/components/data/trend-chart.d.ts +31 -0
- package/dist/components/data/trend-chart.js +136 -0
- package/dist/components/theme/theme-provider.d.ts +42 -0
- package/dist/components/theme/theme-provider.js +110 -0
- package/dist/components/theme/theme-toggle.d.ts +5 -0
- package/dist/components/theme/theme-toggle.js +21 -0
- package/dist/components/ui/accordion.d.ts +7 -0
- package/dist/components/ui/accordion.js +53 -0
- package/dist/components/ui/alert-dialog.d.ts +18 -0
- package/dist/components/ui/alert-dialog.js +99 -0
- package/dist/components/ui/alert.d.ts +10 -0
- package/dist/components/ui/alert.js +42 -0
- package/dist/components/ui/aspect-ratio.d.ts +3 -0
- package/dist/components/ui/aspect-ratio.js +11 -0
- package/dist/components/ui/attachment.d.ts +24 -0
- package/dist/components/ui/attachment.js +100 -0
- package/dist/components/ui/avatar.d.ts +11 -0
- package/dist/components/ui/avatar.js +49 -0
- package/dist/components/ui/badge.d.ts +9 -0
- package/dist/components/ui/badge.js +30 -0
- package/dist/components/ui/breadcrumb.d.ts +11 -0
- package/dist/components/ui/breadcrumb.js +70 -0
- package/dist/components/ui/bubble.d.ts +17 -0
- package/dist/components/ui/bubble.js +71 -0
- package/dist/components/ui/button-group.d.ts +11 -0
- package/dist/components/ui/button-group.js +39 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/button.js +55 -0
- package/dist/components/ui/calendar.d.ts +10 -0
- package/dist/components/ui/calendar.js +108 -0
- package/dist/components/ui/card.d.ts +11 -0
- package/dist/components/ui/card.js +55 -0
- package/dist/components/ui/carousel.d.ts +28 -0
- package/dist/components/ui/carousel.js +131 -0
- package/dist/components/ui/chart.d.ts +44 -0
- package/dist/components/ui/chart.js +148 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/checkbox.js +19 -0
- package/dist/components/ui/code-block.d.ts +26 -0
- package/dist/components/ui/code-block.js +123 -0
- package/dist/components/ui/collapsible.d.ts +5 -0
- package/dist/components/ui/collapsible.js +23 -0
- package/dist/components/ui/combobox.d.ts +24 -0
- package/dist/components/ui/combobox.js +162 -0
- package/dist/components/ui/command.d.ts +18 -0
- package/dist/components/ui/command.js +86 -0
- package/dist/components/ui/context-menu.d.ts +31 -0
- package/dist/components/ui/context-menu.js +123 -0
- package/dist/components/ui/dialog.d.ts +17 -0
- package/dist/components/ui/dialog.js +94 -0
- package/dist/components/ui/direction.d.ts +7 -0
- package/dist/components/ui/direction.js +12 -0
- package/dist/components/ui/drawer.d.ts +13 -0
- package/dist/components/ui/drawer.js +76 -0
- package/dist/components/ui/dropdown-menu.d.ts +29 -0
- package/dist/components/ui/dropdown-menu.js +126 -0
- package/dist/components/ui/empty.d.ts +11 -0
- package/dist/components/ui/empty.js +59 -0
- package/dist/components/ui/field.d.ts +24 -0
- package/dist/components/ui/field.js +109 -0
- package/dist/components/ui/hover-card.d.ts +6 -0
- package/dist/components/ui/hover-card.js +30 -0
- package/dist/components/ui/input-group.d.ts +16 -0
- package/dist/components/ui/input-group.js +77 -0
- package/dist/components/ui/input-otp.d.ts +11 -0
- package/dist/components/ui/input-otp.js +46 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/input.js +13 -0
- package/dist/components/ui/item.d.ts +23 -0
- package/dist/components/ui/item.js +117 -0
- package/dist/components/ui/kbd.d.ts +3 -0
- package/dist/components/ui/kbd.js +19 -0
- package/dist/components/ui/label.d.ts +4 -0
- package/dist/components/ui/label.js +13 -0
- package/dist/components/ui/marker.d.ts +11 -0
- package/dist/components/ui/marker.js +39 -0
- package/dist/components/ui/menubar.d.ts +30 -0
- package/dist/components/ui/menubar.js +133 -0
- package/dist/components/ui/message-scroller.d.ts +10 -0
- package/dist/components/ui/message-scroller.js +59 -0
- package/dist/components/ui/message.d.ts +10 -0
- package/dist/components/ui/message.js +48 -0
- package/dist/components/ui/native-select.d.ts +8 -0
- package/dist/components/ui/native-select.js +37 -0
- package/dist/components/ui/navigation-menu.d.ts +14 -0
- package/dist/components/ui/navigation-menu.js +79 -0
- package/dist/components/ui/pagination.d.ts +17 -0
- package/dist/components/ui/pagination.js +79 -0
- package/dist/components/ui/popover.d.ts +10 -0
- package/dist/components/ui/popover.js +54 -0
- package/dist/components/ui/progress.d.ts +4 -0
- package/dist/components/ui/progress.js +18 -0
- package/dist/components/ui/questionnaire.d.ts +19 -0
- package/dist/components/ui/questionnaire.js +168 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/radio-group.js +25 -0
- package/dist/components/ui/resizable.d.ts +7 -0
- package/dist/components/ui/resizable.js +27 -0
- package/dist/components/ui/scroll-area.d.ts +5 -0
- package/dist/components/ui/scroll-area.js +35 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +98 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/separator.js +15 -0
- package/dist/components/ui/sheet.d.ts +14 -0
- package/dist/components/ui/sheet.js +88 -0
- package/dist/components/ui/sidebar.d.ts +69 -0
- package/dist/components/ui/sidebar.js +377 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/skeleton.js +12 -0
- package/dist/components/ui/slider.d.ts +4 -0
- package/dist/components/ui/slider.js +35 -0
- package/dist/components/ui/sonner.d.ts +3 -0
- package/dist/components/ui/sonner.js +29 -0
- package/dist/components/ui/spinner.d.ts +2 -0
- package/dist/components/ui/spinner.js +15 -0
- package/dist/components/ui/switch.d.ts +6 -0
- package/dist/components/ui/switch.js +18 -0
- package/dist/components/ui/table.d.ts +10 -0
- package/dist/components/ui/table.js +65 -0
- package/dist/components/ui/tabs.d.ts +11 -0
- package/dist/components/ui/tabs.js +45 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/textarea.js +12 -0
- package/dist/components/ui/toggle-group.d.ts +10 -0
- package/dist/components/ui/toggle-group.js +50 -0
- package/dist/components/ui/toggle.d.ts +9 -0
- package/dist/components/ui/toggle.js +35 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/components/ui/tooltip.js +34 -0
- package/dist/components/ui/trace.d.ts +18 -0
- package/dist/components/ui/trace.js +74 -0
- package/dist/hooks/use-copy-to-clipboard.d.ts +5 -0
- package/dist/hooks/use-copy-to-clipboard.js +24 -0
- package/dist/hooks/use-highlighted-code.d.ts +2 -0
- package/dist/hooks/use-highlighted-code.js +54 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +17 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +94 -0
- package/dist/lib/highlighter.d.ts +9 -0
- package/dist/lib/highlighter.js +77 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +8 -0
- package/dist/shadcn-base.css +638 -0
- package/dist/styles.css +188 -0
- package/docs/COMPONENTS.md +405 -0
- package/llms.txt +36 -0
- package/package.json +119 -4
- package/skills/harebear-ui/SKILL.md +34 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# harebear-ui: agent guide
|
|
2
|
+
|
|
3
|
+
This repo is the `harebear-ui` npm package: shadcn/ui primitives restyled to one design system, plus admin, data, and AI chat composites. React 19, Tailwind CSS v4, Radix (`radix-ui`), TanStack Table v9, Recharts 3, TypeScript 7, pnpm.
|
|
4
|
+
|
|
5
|
+
Consumers get compiled ESM + types from `dist/` and a Tailwind stylesheet (`harebear-ui/styles.css`). The source of truth for the look is `src/styles.css` (tokens) and the class strings in components.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
| Command | What it does |
|
|
10
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `pnpm dev` | Playground at http://localhost:5199 with the reference screens. |
|
|
12
|
+
| `pnpm check` | Everything CI runs: lint, typecheck, tests, build, publint, attw. Run before you say you're done. |
|
|
13
|
+
| `pnpm test` | Vitest. `pnpm test -u` updates the export snapshot after an intended API change. |
|
|
14
|
+
| `pnpm gen` | Regenerate `src/index.ts` from component export lists. Run after adding or renaming exports. |
|
|
15
|
+
| `pnpm format` | Biome format + safe fixes. |
|
|
16
|
+
| `pnpm shadcn add <name>` | Pull a new shadcn component into `src/components/ui/`. |
|
|
17
|
+
|
|
18
|
+
## Layout
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
src/
|
|
22
|
+
styles.css Theme: tokens on :root/.dark, @theme mapping, @source for consumers. Shipped as harebear-ui/styles.css.
|
|
23
|
+
shadcn-base.css Vendored copy of shadcn/tailwind.css (variants, keyframes). Re-copy on shadcn upgrades.
|
|
24
|
+
index.ts Generated barrel. Do not edit by hand.
|
|
25
|
+
components/ui/ shadcn primitives (radix-nova style), restyled. Keep upstream structure.
|
|
26
|
+
components/theme/ ThemeProvider, ThemeToggle, ThemeScript.
|
|
27
|
+
components/admin/ Layout and dashboard composites (AppShell, StatCard, PageHeader...).
|
|
28
|
+
components/data/ DataTable, TrendChart, Sparkline, BarList, StatusBars.
|
|
29
|
+
components/ai/ Chat composites (ChatThread, ChatMessage, PromptInput, ModelPicker, CodeSnippet...).
|
|
30
|
+
components/auth/ AuthLayout, LoginForm, SignupForm (parameterized shadcn login/signup blocks).
|
|
31
|
+
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
|
+
llms.txt, skills/ Consumer-facing agent docs. Shipped.
|
|
35
|
+
bin/harebear-ui.mjs `npx harebear-ui init|docs` for consuming apps.
|
|
36
|
+
test/ Vitest: API snapshot, docs coverage, token integrity, behavior.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Adding a component
|
|
40
|
+
|
|
41
|
+
1. Pick the folder by purpose (`admin`, `data`, `ai`, `auth`; `ui` only for shadcn primitives).
|
|
42
|
+
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. `pnpm gen`, then add the component to `docs/COMPONENTS.md` (the docs test fails otherwise) and to the index in `llms.txt`.
|
|
45
|
+
5. Use it in a playground screen if it is visual, and look at it in light and dark mode and at 375px width.
|
|
46
|
+
6. Add a behavior test in `test/components.test.tsx` if it has logic. `pnpm test -u` to accept the new export in the snapshot.
|
|
47
|
+
7. `pnpm check`.
|
|
48
|
+
|
|
49
|
+
## Changing shadcn primitives
|
|
50
|
+
|
|
51
|
+
- Keep upstream file structure so `pnpm shadcn add <name> -o` diffs stay readable. Change class strings, add variants; avoid restructuring.
|
|
52
|
+
- shadcn sometimes rewrites the utils import to `from "cn"`. It must be `from "@/lib/utils"`. Remove any `cn` dependency it adds. Same for `next-themes` (sonner uses our `useTheme`).
|
|
53
|
+
- New shadcn files need `"use client"` as the first line.
|
|
54
|
+
- Restyle new primitives to harebear's look: form controls are 40px `rounded-md bg-muted border-transparent` with `focus-visible:border-primary focus-visible:bg-card focus-visible:ring-4 focus-visible:ring-primary/15`; replace `border-input` / `dark:bg-input/30` patterns; selected states use `bg-primary-soft`.
|
|
55
|
+
- The package includes every `registry:ui` item. Check for gaps with `pnpm shadcn list @shadcn --json` and compare names against `src/components/ui/`.
|
|
56
|
+
|
|
57
|
+
## Gotchas
|
|
58
|
+
|
|
59
|
+
- `@/` is a source alias. tsdown rewrites it to relative paths in `dist`. Do not import from `harebear-ui` inside `src`.
|
|
60
|
+
- TanStack Table v9 needs features registered in `tableFeatures()` with row-model slots; see `dataTableFeatures` in `src/components/data/data-table.tsx` and the skills shipped in `node_modules/@tanstack/react-table/skills/`.
|
|
61
|
+
- `src/styles.css` has `@source "./"` so a consumer's Tailwind scans `dist/`. Classes must be complete literal strings (no string concatenation) or consumers will not get the CSS.
|
|
62
|
+
- Biome formats with no semicolons and double quotes. Rules for `src/components/ui/**` are relaxed in `biome.json` to keep upstream code as is.
|
|
63
|
+
|
|
64
|
+
## Releasing
|
|
65
|
+
|
|
66
|
+
Bump `version` in package.json, commit, tag `vX.Y.Z`, push the tag. The `release` workflow publishes to npm with provenance (trusted publishing, no token in the repo). Ask the maintainer before tagging.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Harrison Milbradt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# harebear-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pre-themed React components for admin tools, data apps, and AI chat. shadcn/ui primitives restyled to one opinionated design system, plus the composites you end up building every time: app shell, KPI cards, data table, trend charts, chat thread, prompt composer, model picker.
|
|
4
|
+
|
|
5
|
+
React 19 · Tailwind CSS v4 · Radix · TanStack Table v9 · Recharts 3 · TypeScript
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i harebear-ui
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx harebear-ui init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`init` adds the stylesheet import to your Tailwind entry CSS, writes a harebear-ui section into `AGENTS.md` (or `CLAUDE.md`), and installs a Claude Code skill in `.claude/skills/`. Flags: `--css <file>`, `--no-agents`, `--no-skill`.
|
|
18
|
+
|
|
19
|
+
Manual setup, if you prefer:
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
/* app.css */
|
|
23
|
+
@import "tailwindcss";
|
|
24
|
+
@import "harebear-ui/styles.css";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { ThemeProvider, Toaster, TooltipProvider } from "harebear-ui"
|
|
29
|
+
|
|
30
|
+
export function Root() {
|
|
31
|
+
return (
|
|
32
|
+
<ThemeProvider accent="#2F5BD3" radius={14}>
|
|
33
|
+
<TooltipProvider>
|
|
34
|
+
<App />
|
|
35
|
+
<Toaster />
|
|
36
|
+
</TooltipProvider>
|
|
37
|
+
</ThemeProvider>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Tailwind CSS v4 in the app. Geist and Geist Mono are bundled.
|
|
43
|
+
|
|
44
|
+
## Use
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { AppShell, PageHeader, StatCard, Button, DataTable } from "harebear-ui"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Everything imports from the package root. Primitives keep shadcn/ui names and props.
|
|
51
|
+
|
|
52
|
+
- **Component catalog:** [docs/COMPONENTS.md](docs/COMPONENTS.md), also at `node_modules/harebear-ui/docs/COMPONENTS.md` or `npx harebear-ui docs`.
|
|
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.
|
|
54
|
+
|
|
55
|
+
## Theming
|
|
56
|
+
|
|
57
|
+
Light and dark mode, accent, radius, and font are runtime props on `ThemeProvider` (`useTheme()` changes them live). Every component reads shadcn-named tokens, so you can override any of them in CSS:
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
:root {
|
|
61
|
+
--brand: #0f8a7e;
|
|
62
|
+
--radius: 0.625rem;
|
|
63
|
+
}
|
|
64
|
+
.dark {
|
|
65
|
+
--background: #0b0b0d;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For SSR apps, render `<ThemeScript />` in `<head>` to avoid a flash of the wrong mode.
|
|
70
|
+
|
|
71
|
+
## For AI agents
|
|
72
|
+
|
|
73
|
+
The package ships its own agent docs:
|
|
74
|
+
|
|
75
|
+
| File | For |
|
|
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. |
|
|
80
|
+
|
|
81
|
+
Working on this repo with an agent: see [AGENTS.md](AGENTS.md).
|
|
82
|
+
|
|
83
|
+
## Develop
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm install
|
|
87
|
+
pnpm dev # playground on :5199
|
|
88
|
+
pnpm check # lint, typecheck, test, build, package lint
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// harebear-ui CLI. Zero dependencies.
|
|
3
|
+
// npx harebear-ui init wire the stylesheet and install agent docs into this project
|
|
4
|
+
// npx harebear-ui docs print the component catalog
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
6
|
+
import { dirname, join, relative } from "node:path"
|
|
7
|
+
import { fileURLToPath } from "node:url"
|
|
8
|
+
|
|
9
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..")
|
|
10
|
+
const cwd = process.cwd()
|
|
11
|
+
const [cmd = "help", ...args] = process.argv.slice(2)
|
|
12
|
+
const flag = (name) => args.includes(`--${name}`)
|
|
13
|
+
const option = (name) => {
|
|
14
|
+
const i = args.indexOf(`--${name}`)
|
|
15
|
+
return i >= 0 ? args[i + 1] : undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const IMPORT_LINE = '@import "harebear-ui/styles.css";'
|
|
19
|
+
const START = "<!-- harebear-ui:start -->"
|
|
20
|
+
const END = "<!-- harebear-ui:end -->"
|
|
21
|
+
const AGENT_BLOCK = `${START}
|
|
22
|
+
## UI: harebear-ui
|
|
23
|
+
|
|
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
|
+
|
|
26
|
+
- Read \`node_modules/harebear-ui/docs/COMPONENTS.md\` before building UI. It lists every component with props and examples.
|
|
27
|
+
- Import from the package root: \`import { Button, Card, DataTable } from "harebear-ui"\`.
|
|
28
|
+
- 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
|
+
- Check new screens in light and dark mode and at phone width.
|
|
30
|
+
${END}`
|
|
31
|
+
|
|
32
|
+
function findCss(dir, depth = 0, out = []) {
|
|
33
|
+
if (depth > 4) return out
|
|
34
|
+
for (const name of readdirSync(dir)) {
|
|
35
|
+
if (name.startsWith(".") || ["node_modules", "dist", "build", ".next", "out", "coverage"].includes(name)) continue
|
|
36
|
+
const p = join(dir, name)
|
|
37
|
+
const st = statSync(p)
|
|
38
|
+
if (st.isDirectory()) findCss(p, depth + 1, out)
|
|
39
|
+
else if (/\.css$/.test(name) && /@import\s+["']tailwindcss["']/.test(readFileSync(p, "utf8"))) out.push(p)
|
|
40
|
+
}
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function wireCss() {
|
|
45
|
+
const explicit = option("css")
|
|
46
|
+
const files = explicit ? [join(cwd, explicit)] : findCss(cwd)
|
|
47
|
+
if (files.length === 0) {
|
|
48
|
+
console.log(
|
|
49
|
+
`! No CSS file with @import "tailwindcss" found. Add this after your Tailwind import:\n ${IMPORT_LINE}`,
|
|
50
|
+
)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (files.length > 1 && !explicit) {
|
|
54
|
+
console.log(
|
|
55
|
+
`! Several Tailwind entry files found; pass one with --css:\n${files.map((f) => ` ${relative(cwd, f)}`).join("\n")}`,
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
const file = files[0]
|
|
60
|
+
const css = readFileSync(file, "utf8")
|
|
61
|
+
if (css.includes("harebear-ui/styles.css")) {
|
|
62
|
+
console.log(`✓ ${relative(cwd, file)} already imports harebear-ui/styles.css`)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const next = css.replace(/(@import\s+["']tailwindcss["'][^;]*;)/, `$1\n${IMPORT_LINE}`)
|
|
66
|
+
writeFileSync(file, next)
|
|
67
|
+
console.log(`✓ Added ${IMPORT_LINE} to ${relative(cwd, file)}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeAgentInstructions() {
|
|
71
|
+
const target = ["AGENTS.md", "CLAUDE.md"].map((f) => join(cwd, f)).find(existsSync) ?? join(cwd, "AGENTS.md")
|
|
72
|
+
const current = existsSync(target) ? readFileSync(target, "utf8") : ""
|
|
73
|
+
const block = new RegExp(`${START}[\\s\\S]*?${END}`)
|
|
74
|
+
const next = block.test(current)
|
|
75
|
+
? current.replace(block, AGENT_BLOCK)
|
|
76
|
+
: `${current}${current && !current.endsWith("\n") ? "\n" : ""}${current ? "\n" : ""}${AGENT_BLOCK}\n`
|
|
77
|
+
writeFileSync(target, next)
|
|
78
|
+
console.log(`✓ Wrote harebear-ui section to ${relative(cwd, target)}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function installSkill() {
|
|
82
|
+
const src = join(pkgRoot, "skills/harebear-ui/SKILL.md")
|
|
83
|
+
const destDir = join(cwd, ".claude/skills/harebear-ui")
|
|
84
|
+
mkdirSync(destDir, { recursive: true })
|
|
85
|
+
writeFileSync(join(destDir, "SKILL.md"), readFileSync(src, "utf8"))
|
|
86
|
+
console.log(`✓ Installed Claude Code skill to ${relative(cwd, destDir)}/SKILL.md`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
switch (cmd) {
|
|
90
|
+
case "init": {
|
|
91
|
+
if (!existsSync(join(cwd, "package.json"))) {
|
|
92
|
+
console.error("Run this from your app's root (no package.json here).")
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
wireCss()
|
|
96
|
+
if (!flag("no-agents")) writeAgentInstructions()
|
|
97
|
+
if (!flag("no-skill")) installSkill()
|
|
98
|
+
console.log(`
|
|
99
|
+
Next: wrap your app root.
|
|
100
|
+
|
|
101
|
+
import { ThemeProvider, Toaster, TooltipProvider } from "harebear-ui"
|
|
102
|
+
|
|
103
|
+
<ThemeProvider>
|
|
104
|
+
<TooltipProvider>
|
|
105
|
+
<App />
|
|
106
|
+
<Toaster />
|
|
107
|
+
</TooltipProvider>
|
|
108
|
+
</ThemeProvider>
|
|
109
|
+
`)
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
case "docs":
|
|
113
|
+
process.stdout.write(readFileSync(join(pkgRoot, "docs/COMPONENTS.md"), "utf8"))
|
|
114
|
+
break
|
|
115
|
+
default:
|
|
116
|
+
console.log(`harebear-ui
|
|
117
|
+
|
|
118
|
+
npx harebear-ui init [--css <file>] [--no-agents] [--no-skill]
|
|
119
|
+
Add the stylesheet import after @import "tailwindcss", add a harebear-ui
|
|
120
|
+
section to AGENTS.md (or CLAUDE.md), and install the Claude Code skill.
|
|
121
|
+
|
|
122
|
+
npx harebear-ui docs
|
|
123
|
+
Print the component catalog (props and examples for every export).
|
|
124
|
+
`)
|
|
125
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ActivityItem {
|
|
3
|
+
id?: string;
|
|
4
|
+
actor: string;
|
|
5
|
+
avatarUrl?: string;
|
|
6
|
+
/** Verb phrase, e.g. "commented on". Rendered muted. */
|
|
7
|
+
action: React.ReactNode;
|
|
8
|
+
target?: React.ReactNode;
|
|
9
|
+
time: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/** "Maya published Pricing page v2 · 12 min ago" list. */
|
|
12
|
+
declare function ActivityFeed({ items, className, ...props }: Omit<React.ComponentProps<"ul">, "children"> & {
|
|
13
|
+
items: ActivityItem[];
|
|
14
|
+
}): React.JSX.Element;
|
|
15
|
+
export { ActivityFeed };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import { initials } from "./user-chip.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
// src/components/admin/activity-feed.tsx
|
|
7
|
+
/** "Maya published Pricing page v2 · 12 min ago" list. */
|
|
8
|
+
function ActivityFeed({ items, className, ...props }) {
|
|
9
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
10
|
+
"data-slot": "activity-feed",
|
|
11
|
+
className: cn("flex flex-col gap-4.5", className),
|
|
12
|
+
...props,
|
|
13
|
+
children: items.map((a, i) => /* @__PURE__ */ jsxs("li", {
|
|
14
|
+
className: "flex items-start gap-3",
|
|
15
|
+
children: [/* @__PURE__ */ jsxs(Avatar, {
|
|
16
|
+
size: "sm",
|
|
17
|
+
className: "size-8",
|
|
18
|
+
children: [a.avatarUrl && /* @__PURE__ */ jsx(AvatarImage, {
|
|
19
|
+
src: a.avatarUrl,
|
|
20
|
+
alt: ""
|
|
21
|
+
}), /* @__PURE__ */ jsx(AvatarFallback, {
|
|
22
|
+
className: "text-xs",
|
|
23
|
+
children: initials(a.actor)
|
|
24
|
+
})]
|
|
25
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
26
|
+
className: "flex min-w-0 flex-1 flex-col gap-0.5 text-[13px]",
|
|
27
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
28
|
+
/* @__PURE__ */ jsx("span", {
|
|
29
|
+
className: "font-semibold",
|
|
30
|
+
children: a.actor
|
|
31
|
+
}),
|
|
32
|
+
" ",
|
|
33
|
+
/* @__PURE__ */ jsx("span", {
|
|
34
|
+
className: "text-muted-foreground",
|
|
35
|
+
children: a.action
|
|
36
|
+
}),
|
|
37
|
+
" ",
|
|
38
|
+
a.target && /* @__PURE__ */ jsx("span", {
|
|
39
|
+
className: "font-medium",
|
|
40
|
+
children: a.target
|
|
41
|
+
})
|
|
42
|
+
] }), /* @__PURE__ */ jsx("span", {
|
|
43
|
+
className: "text-xs font-medium text-muted-foreground",
|
|
44
|
+
children: a.time
|
|
45
|
+
})]
|
|
46
|
+
})]
|
|
47
|
+
}, a.id ?? i))
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export { ActivityFeed };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface AppNavItem {
|
|
3
|
+
label: string;
|
|
4
|
+
/** Icon element, e.g. <LayoutDashboardIcon />. */
|
|
5
|
+
icon?: React.ReactNode;
|
|
6
|
+
href?: string;
|
|
7
|
+
onSelect?: () => void;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
/** Count or short label shown at the right. */
|
|
10
|
+
badge?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export interface AppNavSection {
|
|
13
|
+
label?: string;
|
|
14
|
+
items: AppNavItem[];
|
|
15
|
+
}
|
|
16
|
+
export interface AppBrandProps extends React.ComponentProps<"div"> {
|
|
17
|
+
name: React.ReactNode;
|
|
18
|
+
subtitle?: React.ReactNode;
|
|
19
|
+
/** Icon element shown in the primary-colored tile. */
|
|
20
|
+
icon?: React.ReactNode;
|
|
21
|
+
/** Optional trailing control, e.g. a workspace switcher button. */
|
|
22
|
+
action?: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/** Logo tile + product name. Used in the sidebar header or a top bar. */
|
|
25
|
+
declare function AppBrand({ name, subtitle, icon, action, className, ...props }: AppBrandProps): React.JSX.Element;
|
|
26
|
+
export interface AppShellProps {
|
|
27
|
+
/** Sidebar header content, usually <AppBrand />. */
|
|
28
|
+
brand?: React.ReactNode;
|
|
29
|
+
nav?: AppNavSection[];
|
|
30
|
+
/** Extra sidebar content under the nav (e.g. a usage card). */
|
|
31
|
+
sidebarExtra?: React.ReactNode;
|
|
32
|
+
/** Sidebar bottom content, usually <UserChip />. */
|
|
33
|
+
sidebarFooter?: React.ReactNode;
|
|
34
|
+
/** Top bar content inside the main area (search, actions). */
|
|
35
|
+
header?: React.ReactNode;
|
|
36
|
+
/** Component used for nav links. Default "a". Pass Next.js Link or React Router Link. */
|
|
37
|
+
linkComponent?: React.ElementType;
|
|
38
|
+
defaultOpen?: boolean;
|
|
39
|
+
className?: string;
|
|
40
|
+
children?: React.ReactNode;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Full admin layout: collapsible sidebar (sheet on mobile) + main column.
|
|
44
|
+
* Built on the Sidebar primitives; drop to those for anything custom.
|
|
45
|
+
*/
|
|
46
|
+
declare function AppShell({ brand, nav, sidebarExtra, sidebarFooter, header, linkComponent: Link, defaultOpen, className, children }: AppShellProps): React.JSX.Element;
|
|
47
|
+
export { AppBrand, AppShell };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Badge } from "../ui/badge.js";
|
|
3
|
+
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger } from "../ui/sidebar.js";
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
// src/components/admin/app-shell.tsx
|
|
7
|
+
/** Logo tile + product name. Used in the sidebar header or a top bar. */
|
|
8
|
+
function AppBrand({ name, subtitle, icon, action, className, ...props }) {
|
|
9
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
10
|
+
"data-slot": "app-brand",
|
|
11
|
+
className: cn("flex items-center gap-3", className),
|
|
12
|
+
...props,
|
|
13
|
+
children: [
|
|
14
|
+
icon && /* @__PURE__ */ jsx("div", {
|
|
15
|
+
className: "grid size-9 shrink-0 place-items-center rounded-md bg-primary text-primary-foreground [&_svg]:size-[18px]",
|
|
16
|
+
children: icon
|
|
17
|
+
}),
|
|
18
|
+
/* @__PURE__ */ jsxs("div", {
|
|
19
|
+
className: "min-w-0 flex-1",
|
|
20
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
21
|
+
className: "truncate text-[15px] font-semibold",
|
|
22
|
+
children: name
|
|
23
|
+
}), subtitle && /* @__PURE__ */ jsx("div", {
|
|
24
|
+
className: "truncate text-xs font-medium text-muted-foreground",
|
|
25
|
+
children: subtitle
|
|
26
|
+
})]
|
|
27
|
+
}),
|
|
28
|
+
action
|
|
29
|
+
]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Full admin layout: collapsible sidebar (sheet on mobile) + main column.
|
|
34
|
+
* Built on the Sidebar primitives; drop to those for anything custom.
|
|
35
|
+
*/
|
|
36
|
+
function AppShell({ brand, nav = [], sidebarExtra, sidebarFooter, header, linkComponent: Link = "a", defaultOpen = true, className, children }) {
|
|
37
|
+
return /* @__PURE__ */ jsxs(SidebarProvider, {
|
|
38
|
+
defaultOpen,
|
|
39
|
+
style: { "--sidebar-width": "15.75rem" },
|
|
40
|
+
children: [/* @__PURE__ */ jsxs(Sidebar, {
|
|
41
|
+
className: "border-r-0",
|
|
42
|
+
children: [
|
|
43
|
+
brand && /* @__PURE__ */ jsx(SidebarHeader, {
|
|
44
|
+
className: "px-4 pt-6 pb-2",
|
|
45
|
+
children: brand
|
|
46
|
+
}),
|
|
47
|
+
/* @__PURE__ */ jsxs(SidebarContent, {
|
|
48
|
+
className: "gap-5 px-2",
|
|
49
|
+
children: [nav.map((section, i) => /* @__PURE__ */ jsxs(SidebarGroup, { children: [section.label && /* @__PURE__ */ jsx(SidebarGroupLabel, {
|
|
50
|
+
className: "text-xs font-medium",
|
|
51
|
+
children: section.label
|
|
52
|
+
}), /* @__PURE__ */ jsx(SidebarMenu, {
|
|
53
|
+
className: "gap-1",
|
|
54
|
+
children: section.items.map((item) => /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsx(SidebarMenuButton, {
|
|
55
|
+
asChild: !!item.href,
|
|
56
|
+
isActive: item.active,
|
|
57
|
+
onClick: item.onSelect,
|
|
58
|
+
children: item.href ? /* @__PURE__ */ jsxs(Link, {
|
|
59
|
+
href: item.href,
|
|
60
|
+
children: [
|
|
61
|
+
item.icon,
|
|
62
|
+
/* @__PURE__ */ jsx("span", {
|
|
63
|
+
className: "flex-1",
|
|
64
|
+
children: item.label
|
|
65
|
+
}),
|
|
66
|
+
item.badge !== void 0 && /* @__PURE__ */ jsx(NavBadge, { children: item.badge })
|
|
67
|
+
]
|
|
68
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
69
|
+
item.icon,
|
|
70
|
+
/* @__PURE__ */ jsx("span", {
|
|
71
|
+
className: "flex-1",
|
|
72
|
+
children: item.label
|
|
73
|
+
}),
|
|
74
|
+
item.badge !== void 0 && /* @__PURE__ */ jsx(NavBadge, { children: item.badge })
|
|
75
|
+
] })
|
|
76
|
+
}) }, item.label))
|
|
77
|
+
})] }, section.label ?? i)), sidebarExtra && /* @__PURE__ */ jsx("div", {
|
|
78
|
+
className: "mt-auto px-2",
|
|
79
|
+
children: sidebarExtra
|
|
80
|
+
})]
|
|
81
|
+
}),
|
|
82
|
+
sidebarFooter && /* @__PURE__ */ jsx(SidebarFooter, {
|
|
83
|
+
className: "px-4 pb-6",
|
|
84
|
+
children: sidebarFooter
|
|
85
|
+
})
|
|
86
|
+
]
|
|
87
|
+
}), /* @__PURE__ */ jsxs(SidebarInset, {
|
|
88
|
+
className: cn("min-w-0", className),
|
|
89
|
+
children: [header !== void 0 && /* @__PURE__ */ jsxs("header", {
|
|
90
|
+
"data-slot": "app-header",
|
|
91
|
+
className: "flex flex-wrap items-center gap-3 px-4 pt-5 md:px-8",
|
|
92
|
+
children: [/* @__PURE__ */ jsx(SidebarTrigger, { className: "md:hidden" }), header]
|
|
93
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
94
|
+
"data-slot": "app-content",
|
|
95
|
+
className: "flex min-w-0 flex-col gap-7 px-4 pt-7 pb-12 md:px-8",
|
|
96
|
+
children
|
|
97
|
+
})]
|
|
98
|
+
})]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function NavBadge({ children }) {
|
|
102
|
+
return /* @__PURE__ */ jsx(Badge, {
|
|
103
|
+
variant: "soft",
|
|
104
|
+
className: "h-5 px-2",
|
|
105
|
+
children
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export { AppBrand, AppShell };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Quiet inline note with an icon, e.g. an insight or tip. Defaults to a sparkle icon. */
|
|
3
|
+
declare function Callout({ icon, tone, className, children, ...props }: React.ComponentProps<"div"> & {
|
|
4
|
+
icon?: React.ReactNode;
|
|
5
|
+
tone?: "muted" | "soft";
|
|
6
|
+
}): React.JSX.Element;
|
|
7
|
+
export { Callout };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { SparklesIcon } from "lucide-react";
|
|
5
|
+
// src/components/admin/callout.tsx
|
|
6
|
+
/** Quiet inline note with an icon, e.g. an insight or tip. Defaults to a sparkle icon. */
|
|
7
|
+
function Callout({ icon = /* @__PURE__ */ jsx(SparklesIcon, {}), tone = "muted", className, children, ...props }) {
|
|
8
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9
|
+
"data-slot": "callout",
|
|
10
|
+
className: cn("flex items-center gap-3 rounded-lg px-4 py-3.5 text-[13px] [&>svg]:size-[18px] [&>svg]:shrink-0 [&>svg]:text-primary", tone === "muted" ? "bg-muted" : "bg-primary-soft", className),
|
|
11
|
+
...props,
|
|
12
|
+
children: [icon, /* @__PURE__ */ jsx("div", {
|
|
13
|
+
className: "min-w-0 flex-1",
|
|
14
|
+
children
|
|
15
|
+
})]
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export { Callout };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface KeyValueItem {
|
|
3
|
+
label: React.ReactNode;
|
|
4
|
+
value: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
/** Label-over-value pairs in a responsive grid. Renders a <dl>. */
|
|
7
|
+
declare function KeyValueGrid({ items, columns, className, ...props }: Omit<React.ComponentProps<"dl">, "children"> & {
|
|
8
|
+
items: KeyValueItem[];
|
|
9
|
+
columns?: 1 | 2 | 3;
|
|
10
|
+
}): React.JSX.Element;
|
|
11
|
+
export { KeyValueGrid };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
// src/components/admin/key-value-grid.tsx
|
|
5
|
+
/** Label-over-value pairs in a responsive grid. Renders a <dl>. */
|
|
6
|
+
function KeyValueGrid({ items, columns = 2, className, ...props }) {
|
|
7
|
+
return /* @__PURE__ */ jsx("dl", {
|
|
8
|
+
"data-slot": "key-value-grid",
|
|
9
|
+
className: cn("grid gap-x-5 gap-y-4.5", columns === 1 && "grid-cols-1", columns === 2 && "grid-cols-2", columns === 3 && "grid-cols-2 sm:grid-cols-3", className),
|
|
10
|
+
...props,
|
|
11
|
+
children: items.map((item, i) => /* @__PURE__ */ jsxs("div", {
|
|
12
|
+
className: "flex min-w-0 flex-col gap-0.5",
|
|
13
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
14
|
+
className: "text-xs font-medium text-muted-foreground",
|
|
15
|
+
children: item.label
|
|
16
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
17
|
+
className: "truncate",
|
|
18
|
+
children: item.value
|
|
19
|
+
})]
|
|
20
|
+
}, i))
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export { KeyValueGrid };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StatusTone } from "./status.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export interface LinkTileProps extends Omit<React.ComponentProps<"a">, "title"> {
|
|
4
|
+
title: React.ReactNode;
|
|
5
|
+
/** Mono secondary line, e.g. a host:port. */
|
|
6
|
+
subtitle?: React.ReactNode;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
status?: {
|
|
9
|
+
tone: StatusTone;
|
|
10
|
+
label: React.ReactNode;
|
|
11
|
+
};
|
|
12
|
+
/** Pass your router's <Link href> as the only child; the tile content renders inside it. */
|
|
13
|
+
asChild?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Muted tile that lifts on hover. For service launchers and quick links. */
|
|
16
|
+
declare function LinkTile({ title, subtitle, icon, status, asChild, className, children, ...props }: LinkTileProps): React.JSX.Element;
|
|
17
|
+
export { LinkTile };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { StatusDot } from "./status.js";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Slot } from "radix-ui";
|
|
6
|
+
// src/components/admin/link-tile.tsx
|
|
7
|
+
/** Muted tile that lifts on hover. For service launchers and quick links. */
|
|
8
|
+
function LinkTile({ title, subtitle, icon, status, asChild, className, children, ...props }) {
|
|
9
|
+
const Comp = asChild ? Slot.Root : "a";
|
|
10
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
11
|
+
className: "flex items-start justify-between",
|
|
12
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
13
|
+
className: "grid size-9.5 place-items-center rounded-md bg-card text-primary shadow-card [&_svg]:size-[18px]",
|
|
14
|
+
children: icon
|
|
15
|
+
}), status && /* @__PURE__ */ jsxs("span", {
|
|
16
|
+
className: "inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground tabular-nums",
|
|
17
|
+
children: [/* @__PURE__ */ jsx(StatusDot, { tone: status.tone }), status.label]
|
|
18
|
+
})]
|
|
19
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
20
|
+
className: "flex min-w-0 flex-col gap-px",
|
|
21
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
22
|
+
className: "font-semibold",
|
|
23
|
+
children: title
|
|
24
|
+
}), subtitle && /* @__PURE__ */ jsx("span", {
|
|
25
|
+
className: "truncate font-mono text-xs text-muted-foreground",
|
|
26
|
+
children: subtitle
|
|
27
|
+
})]
|
|
28
|
+
})] });
|
|
29
|
+
return /* @__PURE__ */ jsxs(Comp, {
|
|
30
|
+
"data-slot": "link-tile",
|
|
31
|
+
className: cn("flex flex-col gap-3.5 rounded-lg bg-muted p-4 text-foreground no-underline transition-[background-color,transform,box-shadow] outline-none hover:-translate-y-px hover:bg-card hover:shadow-card focus-visible:ring-4 focus-visible:ring-ring/20", className),
|
|
32
|
+
...props,
|
|
33
|
+
children: [asChild && /* @__PURE__ */ jsx(Slot.Slottable, { children }), body]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export { LinkTile };
|