harebear-ui 0.6.0 → 0.7.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 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
 
@@ -31,8 +32,11 @@ src/
31
32
  hooks/, lib/utils.ts Shared hooks and cn().
32
33
  playground/screens/ Reference screens that use only public exports (#admin, #chat, #records, #home, #auth, #catalog). Visual spec and usage examples.
33
34
  docs/COMPONENTS.md Consumer-facing catalog. Shipped. Test-enforced to mention every export.
35
+ docs/guides/ Getting started (per framework), theming, working with agents. Shipped.
34
36
  llms.txt, skills/ Consumer-facing agent docs. Shipped.
35
- bin/harebear-ui.mjs `npx harebear-ui init|docs` for consuming apps.
37
+ bin/harebear-ui.mjs `npx harebear-ui init|docs [name...]` for consuming apps. Zero dependencies.
38
+ scripts/screenshots.mjs `pnpm shots`: captures playground screens and renders .github/assets/art/*.html to images.
39
+ .github/assets/ README imagery (not shipped): logo.svg, screens/*.webp, hero/light-dark/agents art, social.png.
36
40
  test/ Vitest: API snapshot, docs coverage, token integrity, behavior.
37
41
  ```
38
42
 
@@ -41,11 +45,24 @@ test/ Vitest: API snapshot, docs coverage, token integrity, be
41
45
  1. Pick the folder by purpose (`admin`, `data`, `ai`, `auth`; `ui` only for shadcn primitives).
42
46
  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
47
  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`.
48
+ 4. `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.
45
49
  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
50
  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
51
  7. `pnpm check`.
48
52
 
53
+ ## Looking at your work
54
+
55
+ - `pnpm dev`, then open http://localhost:5199/#admin (or `#chat`, `#agent`, `#records`, `#home`, `#auth`, `#catalog`).
56
+ - `?mode=dark` or `?mode=light` pins the theme without touching localStorage; `?shot` hides the screen switcher. Example: http://localhost:5199/?mode=dark&shot#agent
57
+ - A headless capture: `pnpm shots admin` writes `.github/assets/screens/admin-*.webp`.
58
+
59
+ ## README imagery
60
+
61
+ - `.github/assets/logo.svg` is the mark. It is hand-written SVG; edit it directly. `logo.png` is exported from it.
62
+ - 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).
63
+ - 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.
64
+ - If `npx harebear-ui init` output changes, update the terminal text in `art/agents.html` to match.
65
+
49
66
  ## Changing shadcn primitives
50
67
 
51
68
  - Keep upstream file structure so `pnpm shadcn add <name> -o` diffs stay readable. Change class strings, add variants; avoid restructuring.
package/README.md CHANGED
@@ -1,28 +1,72 @@
1
- # harebear-ui
2
-
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
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 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
- ```
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
- Requires Tailwind CSS v4 in the app. Geist and Geist Mono are bundled.
43
-
44
- ## Use
86
+ And build:
45
87
 
46
88
  ```tsx
47
- import { AppShell, PageHeader, StatCard, Button, DataTable } from "harebear-ui"
89
+ import { AppShell, Button, DataTable, PageHeader, StatCard } from "harebear-ui"
48
90
  ```
49
91
 
50
- Everything imports from the package root. Primitives keep shadcn/ui names and props.
92
+ Everything imports from the package root. Geist and Geist Mono are bundled.
51
93
 
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.
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
- 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:
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,42 @@ Light and dark mode, accent, radius, and font are runtime props on `ThemeProvide
66
112
  }
67
113
  ```
68
114
 
69
- For SSR apps, render `<ThemeScript />` in `<head>` to avoid a flash of the wrong mode.
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
- ## For AI agents
117
+ ## Built for AI agents
72
118
 
73
- The package ships its own agent docs:
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
- | 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. |
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
- Working on this repo with an agent: see [AGENTS.md](AGENTS.md).
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`, `PageHeader`, `StatCard`, `StatusBadge`, `UsageMeter`, `Timeline`, `UserMenu`, `ConfirmDialog` |
145
+ | Data | `DataTable`, `TrendChart`, `BarList`, `Sparkline`, `StatusBars` |
146
+ | AI and chat | `ChatShell`, `ChatPanel`, `ChatMessage`, `PromptInput`, `ModelPicker`, `ToolGroup`, `ApprovalCard`, `CodeSnippet`, `Prose` |
147
+ | Auth | `AuthLayout`, `LoginForm`, `SignupForm` |
148
+ | Primitives | The full shadcn/ui registry: `Button`, `Dialog`, `Select`, `Sidebar`, `Tabs`, `Command`, `Chart`, and the rest |
149
+
150
+ The full list with props: [docs/COMPONENTS.md](docs/COMPONENTS.md).
82
151
 
83
152
  ## Develop
84
153
 
@@ -86,8 +155,11 @@ Working on this repo with an agent: see [AGENTS.md](AGENTS.md).
86
155
  pnpm install
87
156
  pnpm dev # playground on :5199
88
157
  pnpm check # lint, typecheck, test, build, package lint
158
+ pnpm shots # regenerate the README images (needs pnpm dev running, Chrome and cwebp)
89
159
  ```
90
160
 
161
+ Working on this repo with an agent: see [AGENTS.md](AGENTS.md).
162
+
91
163
  ## License
92
164
 
93
165
  MIT
@@ -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
- process.stdout.write(readFileSync(join(pkgRoot, "docs/COMPONENTS.md"), "utf8"))
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,13 @@
1
+ import * as React from "react";
2
+ export interface MediaItemProps extends Omit<React.ComponentProps<"div">, "title"> {
3
+ title: React.ReactNode;
4
+ /** Opens in a new tab. */
5
+ href?: string;
6
+ image?: string;
7
+ imageShape?: "poster" | "square" | "wide";
8
+ meta?: React.ReactNode;
9
+ description?: React.ReactNode;
10
+ }
11
+ /** Thumbnail, title, a meta line and a two-line description. For media, articles, releases. */
12
+ declare function MediaItem({ title, href, image, imageShape, meta, description, className, ...props }: MediaItemProps): React.JSX.Element;
13
+ export { MediaItem };
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ // src/components/layout/media-item.tsx
5
+ const shapeClass = {
6
+ poster: "aspect-[2/3] w-14",
7
+ square: "aspect-square w-12",
8
+ wide: "aspect-video w-24"
9
+ };
10
+ /** Thumbnail, title, a meta line and a two-line description. For media, articles, releases. */
11
+ function MediaItem({ title, href, image, imageShape = "poster", meta, description, className, ...props }) {
12
+ const img = image ? /* @__PURE__ */ jsx("img", {
13
+ src: image,
14
+ alt: "",
15
+ loading: "lazy",
16
+ className: cn("shrink-0 rounded-md bg-muted object-cover", shapeClass[imageShape])
17
+ }) : null;
18
+ return /* @__PURE__ */ jsxs("div", {
19
+ "data-slot": "media-item",
20
+ className: cn("flex min-w-0 items-start gap-3", className),
21
+ ...props,
22
+ children: [img && href ? /* @__PURE__ */ jsx("a", {
23
+ href,
24
+ target: "_blank",
25
+ rel: "noreferrer",
26
+ tabIndex: -1,
27
+ className: "shrink-0",
28
+ children: img
29
+ }) : img, /* @__PURE__ */ jsxs("div", {
30
+ className: "flex min-w-0 flex-col gap-0.5",
31
+ children: [
32
+ /* @__PURE__ */ jsx("span", {
33
+ className: "line-clamp-2 font-medium",
34
+ children: href ? /* @__PURE__ */ jsx("a", {
35
+ href,
36
+ target: "_blank",
37
+ rel: "noreferrer",
38
+ className: "outline-none hover:underline focus-visible:underline",
39
+ children: title
40
+ }) : title
41
+ }),
42
+ meta && /* @__PURE__ */ jsx("span", {
43
+ className: "text-xs text-muted-foreground",
44
+ children: meta
45
+ }),
46
+ description && /* @__PURE__ */ jsx("span", {
47
+ className: "line-clamp-2 text-[13px] text-muted-foreground",
48
+ children: description
49
+ })
50
+ ]
51
+ })]
52
+ });
53
+ }
54
+ export { MediaItem };
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+ export interface MetricProps extends Omit<React.ComponentProps<"div">, "children"> {
3
+ label: React.ReactNode;
4
+ value: React.ReactNode;
5
+ unit?: React.ReactNode;
6
+ /** Percent change. Positive reads as good unless `lowerIsBetter`. */
7
+ delta?: number;
8
+ lowerIsBetter?: boolean;
9
+ align?: "start" | "center";
10
+ }
11
+ /** A number with a unit, a label under it, and an optional percent change over it. */
12
+ declare function Metric({ label, value, unit, delta, lowerIsBetter, align, className, ...props }: MetricProps): React.JSX.Element;
13
+ /** Metrics side by side, evenly spaced. */
14
+ declare function MetricGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
15
+ export { Metric, MetricGroup };
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { ArrowDownRightIcon, ArrowUpRightIcon } from "lucide-react";
5
+ // src/components/layout/metric.tsx
6
+ /** A number with a unit, a label under it, and an optional percent change over it. */
7
+ function Metric({ label, value, unit, delta, lowerIsBetter, align = "center", className, ...props }) {
8
+ const good = delta === void 0 || delta === 0 ? void 0 : lowerIsBetter ? delta < 0 : delta > 0;
9
+ return /* @__PURE__ */ jsxs("div", {
10
+ "data-slot": "metric",
11
+ className: cn("flex min-w-0 flex-col gap-0.5", align === "center" ? "items-center text-center" : "items-start", className),
12
+ ...props,
13
+ children: [
14
+ delta !== void 0 && Number.isFinite(delta) && /* @__PURE__ */ jsxs("span", {
15
+ className: cn("inline-flex items-center gap-0.5 text-xs font-medium tabular-nums", good === void 0 ? "text-muted-foreground" : good ? "text-success" : "text-destructive"),
16
+ children: [
17
+ delta > 0 ? "+" : "",
18
+ delta.toFixed(1),
19
+ "%",
20
+ delta > 0 ? /* @__PURE__ */ jsx(ArrowUpRightIcon, {
21
+ "aria-hidden": true,
22
+ className: "size-3.5"
23
+ }) : delta < 0 ? /* @__PURE__ */ jsx(ArrowDownRightIcon, {
24
+ "aria-hidden": true,
25
+ className: "size-3.5"
26
+ }) : null
27
+ ]
28
+ }),
29
+ /* @__PURE__ */ jsxs("span", {
30
+ className: "text-xl font-semibold tracking-tight whitespace-nowrap tabular-nums @sm:text-2xl",
31
+ children: [value, unit && /* @__PURE__ */ jsx("span", {
32
+ className: "ml-0.5 text-xs font-medium text-muted-foreground",
33
+ children: unit
34
+ })]
35
+ }),
36
+ /* @__PURE__ */ jsx("span", {
37
+ className: "text-xs font-medium tracking-wide text-muted-foreground uppercase",
38
+ children: label
39
+ })
40
+ ]
41
+ });
42
+ }
43
+ /** Metrics side by side, evenly spaced. */
44
+ function MetricGroup({ className, ...props }) {
45
+ return /* @__PURE__ */ jsx("div", {
46
+ "data-slot": "metric-group",
47
+ className: cn("grid auto-cols-fr grid-flow-col gap-2", className),
48
+ ...props
49
+ });
50
+ }
51
+ export { Metric, MetricGroup };
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ declare const gapClass: {
3
+ readonly none: "gap-0";
4
+ readonly xs: "gap-1";
5
+ readonly sm: "gap-2";
6
+ readonly md: "gap-3";
7
+ readonly lg: "gap-4";
8
+ readonly xl: "gap-6";
9
+ };
10
+ export interface ShowMoreProps extends React.ComponentProps<"div"> {
11
+ /** Children shown before the "Show N more" button. Default 5. */
12
+ initial?: number;
13
+ gap?: keyof typeof gapClass;
14
+ }
15
+ /** A list that collapses after `initial` children, with a button to show the rest. */
16
+ declare function ShowMore({ initial, gap, className, children, ...props }: ShowMoreProps): React.JSX.Element;
17
+ export { ShowMore };
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import * as React from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ // src/components/layout/show-more.tsx
6
+ const gapClass = {
7
+ none: "gap-0",
8
+ xs: "gap-1",
9
+ sm: "gap-2",
10
+ md: "gap-3",
11
+ lg: "gap-4",
12
+ xl: "gap-6"
13
+ };
14
+ /** A list that collapses after `initial` children, with a button to show the rest. */
15
+ function ShowMore({ initial = 5, gap = "md", className, children, ...props }) {
16
+ const [open, setOpen] = React.useState(false);
17
+ const all = React.Children.toArray(children);
18
+ return /* @__PURE__ */ jsxs("div", {
19
+ "data-slot": "show-more",
20
+ className: cn("flex min-w-0 flex-col", gapClass[gap], className),
21
+ ...props,
22
+ children: [open ? all : all.slice(0, initial), all.length > initial && /* @__PURE__ */ jsx("button", {
23
+ type: "button",
24
+ onClick: () => setOpen(!open),
25
+ className: "self-start rounded-md text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-4 focus-visible:ring-ring/20",
26
+ children: open ? "Show less" : `Show ${all.length - initial} more`
27
+ })]
28
+ });
29
+ }
30
+ export { ShowMore };
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ export type StackSpace = "none" | "xs" | "sm" | "md" | "lg" | "xl";
3
+ export type StackAlign = "start" | "center" | "end" | "stretch" | "baseline";
4
+ export type StackJustify = "start" | "center" | "end" | "between";
5
+ export interface StackProps extends React.ComponentProps<"div"> {
6
+ gap?: StackSpace;
7
+ align?: StackAlign;
8
+ }
9
+ /** Vertical stack with a named gap. */
10
+ declare function Stack({ gap, align, className, ...props }: StackProps): React.JSX.Element;
11
+ export interface RowProps extends React.ComponentProps<"div"> {
12
+ gap?: StackSpace;
13
+ align?: StackAlign;
14
+ justify?: StackJustify;
15
+ wrap?: boolean;
16
+ }
17
+ /** Horizontal row with a named gap. */
18
+ declare function Row({ gap, align, justify, wrap, className, ...props }: RowProps): React.JSX.Element;
19
+ export type GridCols = 1 | 2 | 3 | 4 | 5 | 6;
20
+ export type ResponsiveGridCols = GridCols | {
21
+ base?: GridCols;
22
+ sm?: GridCols;
23
+ md?: GridCols;
24
+ lg?: GridCols;
25
+ };
26
+ export interface GridProps extends React.ComponentProps<"div"> {
27
+ /** Equal columns: a number, or per breakpoint `{ base: 1, sm: 2, md: 3 }`. */
28
+ cols?: ResponsiveGridCols;
29
+ gap?: StackSpace;
30
+ /**
31
+ * What sm/md/lg measure. "viewport" (default): the screen, Tailwind's sm/md/lg.
32
+ * "container": the nearest `@container` ancestor (sm >= 28rem, md >= 42rem, lg >= 56rem),
33
+ * for blocks that sit in cards of any width.
34
+ */
35
+ query?: "viewport" | "container";
36
+ }
37
+ /** Grid of equal columns with named gaps and per-breakpoint column counts. */
38
+ declare function Grid({ cols: c, gap, query, className, ...props }: GridProps): React.JSX.Element;
39
+ export { Grid, Row, Stack };