feat(ui): @hanzo/ui — canonical, backend-flexible, gui/tokens-based component library
Rework pkg/ui (@hanzo/ui@8) into THE canonical Hanzo UI package: one design core
(@hanzo/tokens, Hanzo dark-first identity, Geist Sans/Mono) with per-backend
implementations. The root barrel now exposes the shadcn-compatible component API
apps import, so consumers drop the @hanzo/ui-shadcn alias and point @hanzo/ui here
with zero import churn.
Organization (src/):
- core/ backend-agnostic: cn (clsx+tailwind-merge), tokens (re-export of
@hanzo/tokens), fonts (Geist vars)
- theme.css self-contained standard-token CSS vars + Geist — the identity
- backends/ shadcn/ (Radix + Tailwind, the web API) and gui/ (@hanzo/gui product
layer); README documents adding a backend (svelte/solid/…)
- models/ the unified ModelSelector + catalog helpers
- primitives/ GENERATED per-member entrypoints (scripts/gen-primitives.mjs) so a
host that modularizes @hanzo/ui imports resolves unchanged
Ported the full component surface hanzo.app imports (Badge, Button, Card*,
Checkbox, Dialog*, DropdownMenu*, Input, Toaster, Avatar*, Tabs*, Select*,
Tooltip*, Popover*, Command*, Collapsible*, ScrollArea, Slider, Switch, Progress,
Separator, Label, Textarea, AspectRatio) + models. Behaviour-heavy components keep
Radix for a11y/portal/keyboard, styled with STANDARD tokens only.
Token-bug fix: every component uses standard design tokens (bg-popover,
border-border, bg-primary, text-muted-foreground, …); the app-private tokens that
rendered transparent (bg-bg-dark, bg-bg-secondary, text-text-secondary,
bg-divider, bg-brand, bg-level-2, hard-coded bg-gray-*) are gone. No hard-coded
font: UI inherits Geist Sans, code Geist Mono, portaled surfaces bind font-sans.
Root barrel is pure web (no @hanzo/gui/react-native-web pull); the gui product
layer + gui theme tokens live on the explicit /product and /gui subpaths.
Also: fix @hanzo/tokens dts build under TS 6 (ignoreDeprecations). Verified:
tsconfig.check.json typecheck green; runtime export test — all 66 required
components + ModelSelector export from @hanzo/ui root; per-member entrypoints
resolve.
This commit is contained in:
@@ -6,31 +6,84 @@ React component library (shadcn/ui fork). 161 components, 24+ blocks, two themes
|
||||
|
||||
**Docs**: https://ui.hanzo.ai | **Dev port**: 3003
|
||||
|
||||
## v8 — the one import surface (`@hanzo/ui@8`, `pkg/ui`)
|
||||
## v8 — the canonical, backend-flexible `@hanzo/ui` (`pkg/ui`)
|
||||
|
||||
`@hanzo/ui@8` (`pkg/ui`) is the cross-platform product/record layer on `@hanzo/gui`.
|
||||
Every recent component kit is reachable from this single package as a thin subpath
|
||||
that re-exports its home package (code lives once; each home is an OPTIONAL peer,
|
||||
pulled only when the subpath is used):
|
||||
`@hanzo/ui@8` (`pkg/ui`) is THE Hanzo component library: one design core, many
|
||||
rendering backends. It is `@hanzo/gui` + `@hanzo/tokens` based, and its ROOT
|
||||
barrel exposes the **shadcn-compatible component API** apps import (so consumers
|
||||
drop the old `@hanzo/ui-shadcn` alias and point `@hanzo/ui` here with zero import
|
||||
churn). `@hanzo/ui-shadcn` (`pkgs/ui`, v5.x) is the legacy standalone shadcn
|
||||
package — superseded by this.
|
||||
|
||||
| Subpath | Home | Components |
|
||||
|---|---|---|
|
||||
| `@hanzo/ui` · `/product` | (this) | charts, metrics, PageHeader, StatusTag, EmptyState, ComboBox, SlideOver, Toast, Reorder, Field |
|
||||
| `@hanzo/ui/data` | `@hanzo/data` | RecordsView, DataTable, typed field editors |
|
||||
| `@hanzo/ui/canvas` | `@hanzo/canvas` | ProjectCanvas, ServiceNode, DeployTimeline, EnvSwitcher, ServiceDetailDrawer, ServiceStatusBadge |
|
||||
| `@hanzo/ui/wallet` | `@hanzo/ui-shadcn/wallet` | WalletMenu, injectedEvmAdapter (EIP-1193), walletAvailable, ensureEvmNetwork |
|
||||
| `@hanzo/ui/network` | `@hanzo/ui-shadcn/network` | NetworkSwitcher, useNetwork, configureNetworks, HANZO_NETWORKS |
|
||||
| `@hanzo/ui/billing` | `@hanzo/ui-shadcn/billing` | CreditModal |
|
||||
| `@hanzo/ui/dashboard` | `@hanzo/dashboard` | landing + deploy-pipeline + overview kit |
|
||||
| `@hanzo/ui/usage` | `@hanzo/usage` | UsageMeter, UsageProviderCard, UsageDashboard |
|
||||
| `@hanzo/ui/gitops` | `@hanzo/gitops` | GitopsAppList, tree, diff, sync/rollback, HealthBadge |
|
||||
### Layout (backend-flexible)
|
||||
|
||||
The **8 newest kits** = canvas, wallet, network, billing, dashboard, usage, gitops,
|
||||
data. Add one by mirroring `src/gitops.ts` (a one-line `export *`) + a `./name`
|
||||
export + an optional peer/devDep. NOTE: `pkg/ui` (singular) sits OUTSIDE the
|
||||
`pkgs/*` pnpm workspace, so it publishes via the maintainer flow, not `publish.yml`
|
||||
(which auto-publishes `pkgs/*` on a version bump). Lux surfaces use `@luxfi/web3`
|
||||
for wallet/login; `@hanzo/ui/wallet`+`/network` are the Hanzo-branded equivalents.
|
||||
```
|
||||
pkg/ui/src/
|
||||
core/ design core (backend-agnostic): cn.ts (clsx+tailwind-merge),
|
||||
tokens.ts (re-export of @hanzo/tokens), fonts.ts (Geist vars)
|
||||
theme.css SELF-CONTAINED standard-token CSS vars (Hanzo dark-first,
|
||||
sourced from @hanzo/tokens) + Geist Sans/Mono — the identity
|
||||
backends/
|
||||
shadcn/ Radix + Tailwind — the shadcn-compatible web API (root default)
|
||||
gui/ @hanzo/gui (Tamagui) product layer — web + native + desktop
|
||||
README.md backend contract + how to add one (svelte/solid/…)
|
||||
models/ the unified ModelSelector + catalog helpers
|
||||
primitives/ GENERATED per-member entrypoints (see scripts/gen-primitives.mjs)
|
||||
index.ts root barrel = shadcn API + product layer + tokens
|
||||
```
|
||||
|
||||
Add a backend under `src/backends/<name>/` (same tokens, different substrate) +
|
||||
a `./<name>` export. The two that exist are `shadcn` and `gui`.
|
||||
|
||||
### Token-bug fix (the point of the rework)
|
||||
|
||||
The old shadcn components reached for app-private token names — `bg-bg-dark`,
|
||||
`bg-bg-secondary`, `text-text-secondary`, `bg-divider`, `bg-brand`, `bg-level-2`,
|
||||
hard-coded `bg-gray-*` — that consumers never defined, so surfaces rendered
|
||||
transparent. Every component here uses ONLY standard design tokens
|
||||
(`bg-popover`, `border-border`, `bg-primary`, `text-muted-foreground`, …) and no
|
||||
hard-coded font (UI inherits Geist Sans, code Geist Mono; portaled surfaces bind
|
||||
`font-sans`). `theme.css` defines every token, so the package is self-contained.
|
||||
|
||||
### Subpaths
|
||||
|
||||
| Subpath | What |
|
||||
|---|---|
|
||||
| `@hanzo/ui` | the component API (shadcn backend, the default): Button, Badge, Card*, Checkbox, Dialog*, DropdownMenu*, Input, Toaster, Avatar*, Tabs*, Select*, Tooltip*, Popover*, Command*, Collapsible*, ScrollArea, Slider, Switch, Progress, Separator, Label, Textarea, AspectRatio — + `cn` + tokens (the gui product layer is kept off root, at `/product`, so web consumers never pull the native runtime) |
|
||||
| `@hanzo/ui/shadcn` | the shadcn backend barrel (explicit) |
|
||||
| `@hanzo/ui/gui` · `/product` | the @hanzo/gui product layer: charts, metrics, PageHeader, StatusTag, EmptyState, ComboBox, SlideOver, Toast, Reorder, Field |
|
||||
| `@hanzo/ui/models` | ModelSelector + fetchModelCatalog + catalog helpers |
|
||||
| `@hanzo/ui/core` · `/tokens` | cn, Geist font vars, the @hanzo/tokens color/theme/radii/spacing scale |
|
||||
| `@hanzo/ui/theme.css` | the self-contained Hanzo identity stylesheet |
|
||||
| `@hanzo/ui/primitives/<Member>` | per-member entrypoints (for hosts that modularize `@hanzo/ui` imports) |
|
||||
| `@hanzo/ui/data` | `@hanzo/data`: RecordsView, DataTable, typed field editors |
|
||||
| `@hanzo/ui/{canvas,wallet,network,billing,dashboard,usage,gitops}` | the optional-peer kits (unchanged; each re-exports its home package) |
|
||||
|
||||
### modularizeImports support
|
||||
|
||||
`scripts/gen-primitives.mjs` reads the shadcn barrel and emits one
|
||||
`src/primitives/<Member>.tsx` per exported value (re-export from the backend).
|
||||
This makes `@hanzo/ui/primitives/Button` etc. resolve, so a host whose
|
||||
`next.config` rewrites `@hanzo/ui` → `@hanzo/ui/primitives/{{member}}` works
|
||||
unchanged. Re-run `pnpm gen:primitives` after changing the surface.
|
||||
|
||||
NOTE: `pkg/ui` (singular) sits OUTSIDE the `pkgs/*` pnpm workspace, installs
|
||||
standalone, and publishes via the maintainer flow, not `publish.yml`. Because the
|
||||
optional-peer kits (canvas/dashboard/gitops/ui-shadcn/usage) are not on the public
|
||||
registry, a standalone install must skip auto-installing peers. `.npmrc` and
|
||||
`pnpm-lock.yaml` are gitignored here, so create the `.npmrc` once:
|
||||
|
||||
```bash
|
||||
cd pkg/ui
|
||||
printf 'auto-install-peers=false\nstrict-peer-dependencies=false\n' > .npmrc
|
||||
pnpm install --ignore-workspace # component-surface deps (radix, cmdk, sonner, …) + @hanzo/tokens
|
||||
pnpm gen:primitives # refresh the per-member entrypoints
|
||||
pnpm typecheck:ui # scoped typecheck of the component surface (green)
|
||||
```
|
||||
|
||||
`@hanzo/tokens` (`pkgs/tokens`) must be built first (`pnpm --filter @hanzo/tokens build`)
|
||||
so the `file:` link resolves. The scoped `typecheck:ui` excludes the optional-peer
|
||||
subpaths, whose homes aren't installed standalone.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
|
||||
+69
-9
@@ -2,12 +2,37 @@
|
||||
"name": "@hanzo/ui",
|
||||
"version": "8.0.3",
|
||||
"type": "module",
|
||||
"description": "Hanzo UI \u2014 the one cross-platform component library on @hanzo/gui. The product/app layer (charts, metrics, page headers, status tags, rich empty states, combobox, slide-over, toasts, drag-reorder, labeled field rows, provider/product marks) + the metadata-driven record layer (@hanzo/data: RecordsView, DataTable, board, typed field editors) + the calm dark-first tokens and motion. Presentational, host-agnostic (data/effects injected), clean-room. Web + native (iOS) + desktop.",
|
||||
"description": "Hanzo UI — the one canonical, backend-flexible component library on @hanzo/gui + @hanzo/tokens. A shared design core (Hanzo dark-first tokens, Geist Sans/Mono typography) with per-backend implementations: the shadcn-compatible web surface (Radix + Tailwind, the API apps import) and the @hanzo/gui product layer (cross-platform web + native + desktop). Self-contained (theme.css), presentational, host-agnostic, clean-room.",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./shadcn": {
|
||||
"types": "./src/backends/shadcn/index.ts",
|
||||
"default": "./src/backends/shadcn/index.ts"
|
||||
},
|
||||
"./gui": {
|
||||
"types": "./src/backends/gui/index.ts",
|
||||
"default": "./src/backends/gui/index.ts"
|
||||
},
|
||||
"./components": {
|
||||
"types": "./src/components.ts",
|
||||
"default": "./src/components.ts"
|
||||
},
|
||||
"./models": {
|
||||
"types": "./src/models/index.ts",
|
||||
"default": "./src/models/index.ts"
|
||||
},
|
||||
"./core": {
|
||||
"types": "./src/core/index.ts",
|
||||
"default": "./src/core/index.ts"
|
||||
},
|
||||
"./tokens": {
|
||||
"types": "./src/core/tokens.ts",
|
||||
"default": "./src/core/tokens.ts"
|
||||
},
|
||||
"./theme.css": "./src/theme.css",
|
||||
"./product": {
|
||||
"types": "./types/product/index.d.ts",
|
||||
"default": "./src/product/index.ts"
|
||||
@@ -16,10 +41,18 @@
|
||||
"types": "./types/data.d.ts",
|
||||
"default": "./src/data.ts"
|
||||
},
|
||||
"./primitives": {
|
||||
"types": "./src/primitives/index.ts",
|
||||
"default": "./src/primitives/index.ts"
|
||||
},
|
||||
"./primitives/bases/data": {
|
||||
"types": "./types/primitives/bases/data/index.d.ts",
|
||||
"default": "./src/primitives/bases/data/index.ts"
|
||||
},
|
||||
"./primitives/*": {
|
||||
"types": "./src/primitives/*.tsx",
|
||||
"default": "./src/primitives/*.tsx"
|
||||
},
|
||||
"./gitops": {
|
||||
"types": "./types/gitops.d.ts",
|
||||
"default": "./src/gitops.ts"
|
||||
@@ -53,7 +86,7 @@
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"module": "./src/index.ts",
|
||||
"types": "./types/index.d.ts",
|
||||
"types": "./src/index.ts",
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
@@ -64,12 +97,41 @@
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"clean": "rm -rf types",
|
||||
"gen:primitives": "node scripts/gen-primitives.mjs",
|
||||
"prepack": "pnpm run build",
|
||||
"tc": "tsc --noEmit",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck:ui": "tsc -p tsconfig.check.json",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzo/tokens": "file:../../pkgs/tokens",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"lucide-react": ">=0.456.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@hanzo/canvas": ">=0.2.0",
|
||||
"@hanzo/dashboard": ">=0.1.0",
|
||||
@@ -78,7 +140,9 @@
|
||||
"@hanzo/gui": ">=7.2.2",
|
||||
"@hanzo/ui-shadcn": ">=5.7.0",
|
||||
"@hanzo/usage": ">=0.1.0",
|
||||
"react": ">=19"
|
||||
"next-themes": ">=0.2.1",
|
||||
"react": ">=19",
|
||||
"react-dom": ">=19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@hanzo/canvas": {
|
||||
@@ -98,13 +162,8 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hanzo/canvas": "workspace:*",
|
||||
"@hanzo/dashboard": "workspace:*",
|
||||
"@hanzo/data": "1.2.1",
|
||||
"@hanzo/gitops": "workspace:*",
|
||||
"@hanzo/gui": "7.3.0",
|
||||
"@hanzo/ui-shadcn": "workspace:*",
|
||||
"@hanzo/usage": "^0.1.6",
|
||||
"@hanzogui/config": "7.3.0",
|
||||
"@hanzogui/core": "7.3.0",
|
||||
"@hanzogui/lucide-icons-2": "7.3.0",
|
||||
@@ -112,6 +171,7 @@
|
||||
"@hanzogui/web": "7.3.0",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Emit one re-export entrypoint per shadcn-surface member into `src/primitives/`.
|
||||
*
|
||||
* A host may modularize `@hanzo/ui` imports into per-member subpaths — e.g.
|
||||
* hanzo.app's `next.config.ts`:
|
||||
* modularizeImports: { '@hanzo/ui': { transform: '@hanzo/ui/primitives/{{member}}' } }
|
||||
* rewrites `import { Button } from '@hanzo/ui'` into
|
||||
* `import { Button } from '@hanzo/ui/primitives/Button'`. For that to resolve,
|
||||
* `@hanzo/ui/primitives/<Member>` must be a module exporting exactly `<Member>`.
|
||||
*
|
||||
* The shadcn backend barrel (`src/backends/shadcn/index.ts`) is the SINGLE source
|
||||
* of truth for the surface; this script derives the member list from it, so the
|
||||
* per-member files never drift. Re-run after changing the surface:
|
||||
* node scripts/gen-primitives.mjs
|
||||
*/
|
||||
import { readFileSync, writeFileSync, readdirSync, unlinkSync, mkdirSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const root = join(here, '..')
|
||||
const barrelPath = join(root, 'src/backends/shadcn/index.ts')
|
||||
const outDir = join(root, 'src/primitives')
|
||||
|
||||
const src = readFileSync(barrelPath, 'utf8')
|
||||
|
||||
// Collect VALUE member names from every `export { ... } from '...'` block.
|
||||
// Type-only members (`type Foo`) are skipped: they are not values, cannot be
|
||||
// re-exported without `export type` under isolatedModules, and a host only
|
||||
// modularizes value imports.
|
||||
const members = new Set()
|
||||
for (const m of src.matchAll(/export\s*\{([^}]*)\}\s*from/g)) {
|
||||
for (const raw of m[1].split(',')) {
|
||||
const name = raw.trim()
|
||||
if (!name || name.startsWith('type ')) continue
|
||||
// `X as Y` → the exported name is Y.
|
||||
const exported = name.includes(' as ') ? name.split(/\s+as\s+/)[1].trim() : name
|
||||
if (/^[A-Za-z_$][\w$]*$/.test(exported)) members.add(exported)
|
||||
}
|
||||
}
|
||||
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
|
||||
const BANNER =
|
||||
'// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint\n' +
|
||||
'// for hosts that modularize `@hanzo/ui` imports. Source of truth:\n' +
|
||||
'// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs\n'
|
||||
|
||||
// Clear previously generated member files (keep hand-authored index.ts + bases/).
|
||||
for (const f of readdirSync(outDir, { withFileTypes: true })) {
|
||||
if (f.isFile() && f.name.endsWith('.tsx') && f.name !== 'index.ts') {
|
||||
const p = join(outDir, f.name)
|
||||
if (readFileSync(p, 'utf8').startsWith('// GENERATED')) unlinkSync(p)
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...members].sort()
|
||||
for (const name of sorted) {
|
||||
writeFileSync(
|
||||
join(outDir, `${name}.tsx`),
|
||||
`${BANNER}export { ${name} } from '../backends/shadcn'\n`,
|
||||
)
|
||||
}
|
||||
|
||||
// A `./primitives` root and `./primitives/index` that mirror the full surface.
|
||||
writeFileSync(
|
||||
join(outDir, 'index.ts'),
|
||||
`${BANNER}export * from '../backends/shadcn'\n`,
|
||||
)
|
||||
|
||||
console.log(`gen-primitives: wrote ${sorted.length} member entrypoints + index.ts`)
|
||||
@@ -0,0 +1,42 @@
|
||||
# @hanzo/ui backends
|
||||
|
||||
`@hanzo/ui` is backend-flexible: one shared design core, many rendering
|
||||
substrates. A "backend" is a directory under `src/backends/<name>/` that
|
||||
implements components on one substrate. Every backend draws from the SAME core
|
||||
(`src/core` + `theme.css`): the Hanzo dark-first identity, the standard design
|
||||
tokens (sourced from `@hanzo/tokens`), and Geist Sans/Mono typography. Backends
|
||||
differ ONLY in substrate — never in design language.
|
||||
|
||||
```
|
||||
src/
|
||||
core/ one design core, backend-agnostic
|
||||
cn.ts class-name composer (clsx + tailwind-merge)
|
||||
tokens.ts re-export of @hanzo/tokens (the token source of truth)
|
||||
fonts.ts Geist Sans / Geist Mono family variables
|
||||
theme.css self-contained standard token vars + Geist (the identity)
|
||||
backends/
|
||||
shadcn/ Radix + Tailwind — the shadcn-compatible web API (default)
|
||||
gui/ @hanzo/gui (Tamagui) — cross-platform web + native + desktop
|
||||
<your-backend>/ e.g. svelte, solid, vue, react-native — add here
|
||||
```
|
||||
|
||||
## Contract a backend implements
|
||||
|
||||
- Components use ONLY the standard design tokens (`bg-popover`, `border-border`,
|
||||
`bg-primary`, `text-muted-foreground`, …) or the gui token config — never
|
||||
app-private token names, never a hard-coded font family (inherit, or bind to
|
||||
`font-sans` / `font-mono`, which resolve to Geist through the theme).
|
||||
- A backend exposes a barrel `index.ts` naming its component surface.
|
||||
- Behaviour-heavy components (dialog, dropdown, select, popover, tooltip) keep an
|
||||
accessible primitive (Radix on the web) rather than reimplementing focus
|
||||
management, portalling, and keyboard handling.
|
||||
|
||||
## Adding a backend (e.g. svelte)
|
||||
|
||||
1. `src/backends/svelte/` with the component implementations on that substrate,
|
||||
styled from the shared tokens.
|
||||
2. A barrel `src/backends/svelte/index.ts`.
|
||||
3. A package `exports` subpath (`./svelte`) pointing at the barrel.
|
||||
|
||||
The gui + shadcn backends are what exist today; the structure is what makes the
|
||||
next one drop-in.
|
||||
@@ -0,0 +1,19 @@
|
||||
// @hanzo/ui — gui backend.
|
||||
//
|
||||
// The cross-platform (web + native/iOS + desktop/Tauri) component layer built on
|
||||
// @hanzo/gui primitives (YStack/XStack/Text/styled + the gui token config). This
|
||||
// is the product/app layer — charts, metrics, page headers, status tags, empty
|
||||
// states, combobox, slide-over, toasts, drag-reorder, labeled field rows,
|
||||
// provider/product marks, theme toggle — presentational and host-agnostic (data
|
||||
// and effects are injected as props).
|
||||
//
|
||||
// It draws from the SAME design language as the shadcn backend (one token core,
|
||||
// one Hanzo dark-first identity, Geist typography); the two backends differ only
|
||||
// in their rendering substrate (gui/Tamagui here, Radix + Tailwind there).
|
||||
|
||||
export * from '../../product'
|
||||
|
||||
// The gui/product theme object + tag tones (one source of truth, from
|
||||
// @hanzo/data) — the theme the gui backend renders against. Design-token PRIMITIVES
|
||||
// (colors, dark/light, radii, spacing) are the runtime-free `@hanzo/ui/core`.
|
||||
export { tokens, TAG_TONES, tagTone, type TagTone } from '@hanzo/data'
|
||||
@@ -0,0 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
|
||||
|
||||
/** AspectRatio — thin passthrough of the Radix primitive (no styling needed). */
|
||||
function AspectRatio(props: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
|
||||
}
|
||||
|
||||
export { AspectRatio }
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ComponentRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="avatar"
|
||||
className={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ComponentRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
data-slot="avatar-image"
|
||||
className={cn('aspect-square size-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ComponentRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive [&>svg]:pointer-events-none overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90 border-transparent shadow-sm',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 border-transparent shadow-sm focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'text-foreground border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground border-transparent',
|
||||
link: 'text-primary underline-offset-4 hover:underline border-transparent',
|
||||
// Hanzo compatibility variants — retokenized to standard vars
|
||||
// (was `bg-gray-600 text-white`, an app-private color).
|
||||
inputAdornment: 'bg-muted px-2 font-medium text-foreground border-transparent',
|
||||
tags: 'text-muted-foreground border-border bg-muted rounded-lg px-2 py-1 font-normal capitalize',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'span'> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'span'
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
/**
|
||||
* Button — every class is a STANDARD design token (`bg-primary`,
|
||||
* `text-primary-foreground`, `bg-accent`, `bg-secondary`, `bg-destructive`,
|
||||
* `border-input`, `ring-ring`). No app-private token names, so it renders solid
|
||||
* against any host that defines the standard shadcn variables (which the Hanzo
|
||||
* theme, `theme.css`, and every consuming app do).
|
||||
*/
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
// Hanzo compatibility variants (kept so existing call sites don't churn).
|
||||
primary: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
linkFG: 'text-primary underline-offset-4 hover:underline',
|
||||
linkMuted:
|
||||
'text-muted-foreground underline-offset-4 hover:underline hover:text-foreground',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
asChild = false,
|
||||
isLoading = false,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
const iconSize = size === 'icon' || size === 'icon-sm' || size === 'icon-lg'
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{asChild ? (
|
||||
// A Radix `Slot` calls `React.Children.only`; it must receive exactly
|
||||
// one child, so an injected spinner alongside `children` would crash
|
||||
// the tree. Pass `children` through unchanged when slotted.
|
||||
children
|
||||
) : (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Loader2 className={cn('h-4 w-4 animate-spin', !iconSize && 'mr-2')} />
|
||||
) : null}
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</Comp>
|
||||
)
|
||||
},
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="card-content" className={cn('px-6', className)} {...props} />
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { CheckIcon } from '@radix-ui/react-icons'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
/**
|
||||
* Checkbox — standard-token control. Checked state uses `bg-primary` /
|
||||
* `text-primary-foreground` (was the app-private `bg-brand`), border uses
|
||||
* `border-input` (was `border-gray-400`), so it reads correctly in any host.
|
||||
*/
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ComponentRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
'peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground aria-invalid:border-destructive',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className={cn('flex items-center justify-center text-current')}
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
|
||||
|
||||
function Collapsible(props: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger(
|
||||
props: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>,
|
||||
) {
|
||||
return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleContent(
|
||||
props: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>,
|
||||
) {
|
||||
return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -0,0 +1,157 @@
|
||||
'use client'
|
||||
|
||||
import { Command as CommandPrimitive } from 'cmdk'
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './dialog'
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ComponentRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground font-sans flex h-full w-full flex-col overflow-hidden rounded-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
function CommandDialog({
|
||||
title = 'Command Palette',
|
||||
description = 'Search for a command to run...',
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & { title?: string; description?: string }) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ComponentRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div data-slot="command-input-wrapper" className="flex h-9 items-center gap-2 border-b px-3">
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ComponentRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
data-slot="command-list"
|
||||
className={cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ComponentRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ComponentRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ComponentRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
data-slot="command-separator"
|
||||
className={cn('bg-border -mx-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ComponentRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { XIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ComponentRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ComponentRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }
|
||||
>(({ className, children, showCloseButton = true, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
'bg-background text-foreground font-sans fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:max-w-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton ? (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
) : null}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ComponentRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
data-slot="dialog-title"
|
||||
className={cn('text-lg leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ComponentRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
data-slot="dialog-description"
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
'use client'
|
||||
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
/**
|
||||
* Dropdown menu — Radix for a11y/portal/keyboard, styled with STANDARD tokens
|
||||
* only. The prior version leaked app-private tokens (`bg-bg-dark`,
|
||||
* `bg-bg-secondary`, `text-text-secondary`, `bg-divider`, hard-coded
|
||||
* `bg-gray-300`) that were undefined in consumers, so the menu surface,
|
||||
* item-hover, label, and separators rendered transparent. Every surface here is
|
||||
* `bg-popover` / `text-popover-foreground`, every hover is `focus:bg-accent`,
|
||||
* every divider is `bg-border` — solid in any host.
|
||||
*/
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground font-sans z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: 'default' | 'destructive'
|
||||
}
|
||||
>(({ className, inset, variant = 'default', ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors select-none focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
checked={checked}
|
||||
className={cn(
|
||||
'relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
'relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
'flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground data-[inset]:pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground font-sans z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// @hanzo/ui — shadcn backend.
|
||||
//
|
||||
// The shadcn-compatible component surface: Radix primitives for behaviour
|
||||
// (a11y, portalling, keyboard) styled entirely with STANDARD design tokens
|
||||
// (`bg-popover`, `border-border`, `bg-primary`, `text-muted-foreground`, …) —
|
||||
// no app-private token names, so every component renders solid against the
|
||||
// Hanzo theme (`theme.css`) or any host that defines the standard shadcn
|
||||
// variables. Fonts are inherited or bound to `font-sans`/`font-mono` (Geist via
|
||||
// the theme); nothing here hard-codes a font family.
|
||||
//
|
||||
// This barrel is the canonical component API `@hanzo/ui` exposes at its root.
|
||||
// The explicit named blocks below also serve as the manifest that
|
||||
// `scripts/gen-primitives.mjs` reads to emit the per-member `./primitives/*`
|
||||
// entrypoints (for hosts that modularize `@hanzo/ui` imports).
|
||||
|
||||
export { AspectRatio } from './aspect-ratio'
|
||||
export { Avatar, AvatarImage, AvatarFallback } from './avatar'
|
||||
export { Badge, badgeVariants } from './badge'
|
||||
export { Button, buttonVariants, type ButtonProps } from './button'
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from './card'
|
||||
export { Checkbox } from './checkbox'
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent } from './collapsible'
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
} from './command'
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './dialog'
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
} from './dropdown-menu'
|
||||
export { Input, type InputProps } from './input'
|
||||
export { Label } from './label'
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } from './popover'
|
||||
export { Progress } from './progress'
|
||||
export { ScrollArea, ScrollBar } from './scroll-area'
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
} from './select'
|
||||
export { Separator } from './separator'
|
||||
export { Slider } from './slider'
|
||||
export { Switch } from './switch'
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs'
|
||||
export { Textarea } from './textarea'
|
||||
export { Toaster, toast } from './toaster'
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './tooltip'
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { EyeIcon, EyeOffIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
/** Optional leading affordance (icon/text) rendered inside the field. */
|
||||
startAdornment?: React.ReactNode
|
||||
/** Optional trailing affordance rendered inside the field. */
|
||||
endAdornment?: React.ReactNode
|
||||
/** Suppress the built-in show/hide control for `type="password"`. */
|
||||
hidePasswordToggle?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Input — canonical shadcn field on STANDARD tokens (`border-input`,
|
||||
* `bg-transparent`, `ring-ring`, `text-foreground`, `placeholder:text-muted-foreground`).
|
||||
*
|
||||
* The previous implementation hard-coded a floating-label look with app-private
|
||||
* tokens (`bg-bg-secondary`, `border-border-input`, `text-text-default`, `h-input`)
|
||||
* that were undefined in consumers, so plain `<Input placeholder="…">` rendered
|
||||
* transparent/broken. This renders solid everywhere. A bare input is emitted when
|
||||
* no adornment/password affordance is requested (the common path); adornments and
|
||||
* the password toggle add a relative wrapper only when actually used.
|
||||
*/
|
||||
const BASE =
|
||||
'flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-sm text-foreground shadow-xs transition-colors outline-none file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20'
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, startAdornment, endAdornment, hidePasswordToggle, ...props }, ref) => {
|
||||
const [showPassword, setShowPassword] = React.useState(false)
|
||||
const isPassword = type === 'password'
|
||||
const showToggle = isPassword && !hidePasswordToggle
|
||||
const inputType = isPassword && showPassword ? 'text' : type
|
||||
|
||||
const field = (
|
||||
<input
|
||||
ref={ref}
|
||||
data-slot="input"
|
||||
type={inputType}
|
||||
spellCheck={false}
|
||||
className={cn(
|
||||
BASE,
|
||||
startAdornment && 'pl-9',
|
||||
(endAdornment || showToggle) && 'pr-9',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!startAdornment && !endAdornment && !showToggle) return field
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
{startAdornment ? (
|
||||
<span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground [&_svg]:size-4">
|
||||
{startAdornment}
|
||||
</span>
|
||||
) : null}
|
||||
{field}
|
||||
{endAdornment && !showToggle ? (
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&_svg]:size-4">
|
||||
{endAdornment}
|
||||
</span>
|
||||
) : null}
|
||||
{showToggle ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon aria-hidden className="size-4" />
|
||||
) : (
|
||||
<EyeIcon aria-hidden className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ComponentRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client'
|
||||
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ComponentRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground font-sans z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ComponentRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="progress"
|
||||
className={cn('bg-muted relative h-2 w-full overflow-hidden rounded-full', className)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'flex touch-none p-px transition-colors select-none',
|
||||
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent',
|
||||
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="scroll-area"
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="size-full rounded-[inherit] outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-[color,box-shadow]"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -0,0 +1,157 @@
|
||||
'use client'
|
||||
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ComponentRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
data-slot="select-trigger"
|
||||
className={cn(
|
||||
"border-input bg-transparent text-foreground data-[placeholder]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive flex h-9 w-fit items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ComponentRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="select-content"
|
||||
position={position}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground font-sans relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ComponentRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
data-slot="select-label"
|
||||
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ComponentRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ComponentRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
data-slot="select-separator"
|
||||
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ComponentRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ComponentRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="slider"
|
||||
className={cn(
|
||||
'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className="bg-muted relative h-1.5 w-full grow overflow-hidden rounded-full"
|
||||
>
|
||||
<SliderPrimitive.Range data-slot="slider-range" className="bg-primary absolute h-full" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-none disabled:pointer-events-none"
|
||||
/>
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import * as SwitchPrimitive from '@radix-ui/react-switch'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ComponentRef<typeof SwitchPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
'peer inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-colors outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
'bg-background pointer-events-none block size-4 rounded-full ring-0 shadow-lg transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitive.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const Tabs = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="tabs"
|
||||
className={cn('flex flex-col gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Tabs.displayName = TabsPrimitive.Root.displayName
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-[3px] focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="tabs-content"
|
||||
className={cn('flex-1 outline-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
/** Textarea — standard-token multiline field. */
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<'textarea'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<textarea
|
||||
ref={ref}
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm text-foreground shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 field-sizing-content',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
Textarea.displayName = 'Textarea'
|
||||
|
||||
export { Textarea }
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from 'next-themes'
|
||||
import { Toaster as Sonner, toast, type ToasterProps } from 'sonner'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
/**
|
||||
* Toaster — Sonner surface bound to STANDARD tokens. The prior version used
|
||||
* app-private classes (`bg-level-2`, `text-primary-fg`, `border-muted-3`); here
|
||||
* the toast surface maps to `--popover`, borders to `--border`, and buttons to
|
||||
* the primary/muted tokens via Sonner's CSS-variable API, so it matches the host
|
||||
* theme (light/dark) with no undefined tokens.
|
||||
*/
|
||||
const Toaster = ({ className, ...props }: ToasterProps) => {
|
||||
const { theme = 'system' } = useTheme()
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps['theme']}
|
||||
className={cn('toaster group', className)}
|
||||
style={
|
||||
{
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
'group toast group-[.toaster]:bg-popover group-[.toaster]:text-popover-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
|
||||
description: 'group-[.toast]:text-muted-foreground',
|
||||
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
|
||||
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster, toast }
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './utils'
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ComponentRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, children, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground font-sans z-50 w-fit rounded-md border px-3 py-1.5 text-xs shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Local `cn` handle for the shadcn backend.
|
||||
*
|
||||
* Every component in this backend merges classes through the ONE composer in the
|
||||
* shared core; re-exporting it here keeps each component's import local
|
||||
* (`./utils`) and the merge logic single-sourced.
|
||||
*/
|
||||
export { cn } from '../../core/cn'
|
||||
@@ -0,0 +1,6 @@
|
||||
// @hanzo/ui/components — alias of the component API (the shadcn backend surface).
|
||||
//
|
||||
// Some hosts re-export the library through this subpath (e.g. a
|
||||
// `declare module '@hanzo/ui' { export * from '@hanzo/ui/components' }` shim).
|
||||
// It resolves to the same canonical surface as the root barrel.
|
||||
export * from './backends/shadcn'
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* `cn` — the one class-name composer.
|
||||
*
|
||||
* `clsx` resolves conditional/array/object class inputs; `tailwind-merge` then
|
||||
* collapses conflicting Tailwind utilities so the LAST wins (e.g. a caller's
|
||||
* `bg-background` overrides a component's default `bg-popover`). Every component
|
||||
* in every backend that emits Tailwind classes merges through this single
|
||||
* function — one way, everywhere.
|
||||
*/
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Typography identity — Geist (UI/body/display) + Geist Mono (code/data).
|
||||
*
|
||||
* The families are referenced through CSS variables so a host that loads Geist
|
||||
* however it likes (next/font, @fontsource, a CDN link) just has to bind these
|
||||
* two variables; the components and `theme.css` read the variables, never a
|
||||
* hard-coded font name. These constants are the single source of truth for the
|
||||
* variable names so a host and the stylesheet can never drift.
|
||||
*/
|
||||
|
||||
/** CSS custom-property that must resolve to the Geist Sans family. */
|
||||
export const FONT_SANS_VAR = '--font-geist-sans' as const
|
||||
/** CSS custom-property that must resolve to the Geist Mono family. */
|
||||
export const FONT_MONO_VAR = '--font-geist-mono' as const
|
||||
|
||||
/**
|
||||
* UI / body / display / heading stack — Geist Sans, then the CSS generic only.
|
||||
* No Inter, no named system fonts: the host always binds `--font-geist-sans`
|
||||
* (next/font, @fontsource, CDN), so the generic is a last-resort generic, never
|
||||
* a system-UI look.
|
||||
*/
|
||||
export const fontSans = `var(${FONT_SANS_VAR}), sans-serif` as const
|
||||
|
||||
/** Code / data / tabular stack — Geist Mono, then the CSS generic only. */
|
||||
export const fontMono = `var(${FONT_MONO_VAR}), monospace` as const
|
||||
|
||||
export const fonts = { sans: fontSans, mono: fontMono } as const
|
||||
@@ -0,0 +1,9 @@
|
||||
// @hanzo/ui — design core.
|
||||
//
|
||||
// Backend-agnostic foundation every backend draws from: the class-name composer,
|
||||
// the design tokens (from @hanzo/tokens), and the Geist typography variables.
|
||||
// The visual identity these describe is emitted as CSS by `theme.css`.
|
||||
|
||||
export { cn } from './cn'
|
||||
export { fonts, fontSans, fontMono, FONT_SANS_VAR, FONT_MONO_VAR } from './fonts'
|
||||
export * from './tokens'
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Design tokens — re-exported from `@hanzo/tokens`, the single source of truth.
|
||||
*
|
||||
* `@hanzo/ui` is BUILT ON `@hanzo/tokens`: the color scale, the dark/light theme
|
||||
* token sets (the exact values `theme.css` emits as CSS variables), the radii,
|
||||
* the spacing, and the typography scale all originate there. Surfacing them here
|
||||
* lets a consumer read the same tokens programmatically —
|
||||
* `import { themes, colors } from '@hanzo/ui/tokens'` — that the stylesheet and
|
||||
* the components render against, so runtime and CSS can never drift.
|
||||
*
|
||||
* NOTE: the root barrel's `tokens` export (the gui/product theme object from
|
||||
* `@hanzo/data`) is a DIFFERENT, complementary thing; these are named exports
|
||||
* (`colors`, `themes`, `dark`, `light`, `radii`, …) and never collide with it.
|
||||
*/
|
||||
export {
|
||||
colors,
|
||||
zinc,
|
||||
oklch,
|
||||
hsl,
|
||||
spacing,
|
||||
spacingPx,
|
||||
radii,
|
||||
radiiPx,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontSizePx,
|
||||
fontWeight,
|
||||
lineHeight,
|
||||
letterSpacing,
|
||||
dark,
|
||||
light,
|
||||
themes,
|
||||
} from '@hanzo/tokens'
|
||||
|
||||
export type {
|
||||
ColorToken,
|
||||
ZincStep,
|
||||
SpacingStep,
|
||||
RadiusToken,
|
||||
FontSizeToken,
|
||||
FontWeightToken,
|
||||
ThemeTokens,
|
||||
} from '@hanzo/tokens'
|
||||
+37
-18
@@ -1,22 +1,41 @@
|
||||
// @hanzo/ui — the one Hanzo component library, on @hanzo/gui.
|
||||
// @hanzo/ui — the one Hanzo component library, on @hanzo/gui + @hanzo/tokens.
|
||||
//
|
||||
// import { PageHeader, Sparkline, EmptyState, tokens } from '@hanzo/ui' // product + tokens
|
||||
// import { RecordsView, DataTable, registerField } from '@hanzo/ui/data' // metadata record layer
|
||||
// Backend-flexible: one design core (tokens, Hanzo dark-first identity, Geist
|
||||
// typography), many rendering substrates (see src/backends). The ROOT barrel is
|
||||
// the canonical component API apps import — the shadcn-compatible surface — plus
|
||||
// the gui product layer and the design tokens. Pick a backend explicitly via a
|
||||
// subpath when you want to:
|
||||
//
|
||||
// Two orthogonal concerns, ONE package, zero duplication:
|
||||
// • the product/app component layer (this barrel — charts, metrics, headers,
|
||||
// status tags, empty states, combobox, slide-over, toasts, reorder, fields), and
|
||||
// • the metadata-driven record layer (the `./data` subpath — RecordsView and the
|
||||
// typed field editors, whose source of truth is @hanzo/data).
|
||||
// Both cross-platform (web + native + desktop), presentational, host-agnostic
|
||||
// (data + effects injected), clean-room. The two layers each own a `DataTable`
|
||||
// (product = generic typed <T> list; data = field-driven record grid); keeping the
|
||||
// record layer on the `./data` subpath keeps each name unambiguous.
|
||||
// import { Button, Card, Dialog, DropdownMenu, Input, Toaster } from '@hanzo/ui' // component API (shadcn backend)
|
||||
// import { PageHeader, Sparkline, EmptyState } from '@hanzo/ui/product' // gui product layer
|
||||
// import { RecordsView, registerField } from '@hanzo/ui/data' // metadata record layer
|
||||
// import { ModelSelector } from '@hanzo/ui/models' // unified model picker
|
||||
// import { cn, themes, colors } from '@hanzo/ui/core' // design core + tokens
|
||||
// import '@hanzo/ui/theme.css' // the self-contained identity
|
||||
//
|
||||
// Every component renders solid against any host that defines the standard design
|
||||
// tokens (which `@hanzo/ui/theme.css` and every consuming app do); nothing
|
||||
// reaches for app-private token names, and nothing hard-codes a font family —
|
||||
// UI inherits Geist Sans, code inherits Geist Mono, both through the token theme.
|
||||
|
||||
// Product/app component layer.
|
||||
export * from './product'
|
||||
// The canonical component API — the shadcn-compatible surface (Radix behaviour,
|
||||
// standard-token styling), the DEFAULT (web) backend. This is what
|
||||
// `import { Button } from '@hanzo/ui'` resolves to.
|
||||
export * from './backends/shadcn'
|
||||
|
||||
// Design tokens — the calm, dark-first zinc-on-black identity (surface, text,
|
||||
// accent, tag tones). One source of truth (@hanzo/data's theme), surfaced here
|
||||
// for convenience so `import { tokens } from '@hanzo/ui'` works.
|
||||
export { tokens, TAG_TONES, tagTone, type TagTone } from '@hanzo/data'
|
||||
// The gui product/app layer (charts, metrics, page headers, status tags, empty
|
||||
// states, combobox, slide-over, provider marks, theme toggle) lives on its own
|
||||
// subpath — `@hanzo/ui/product` (alias `@hanzo/ui/gui`). It is a DIFFERENT
|
||||
// backend (@hanzo/gui / Tamagui) with a cross-platform runtime, so it is kept
|
||||
// off the root barrel: a web consumer of the component API never pays for the
|
||||
// native runtime, and each backend stays orthogonal.
|
||||
|
||||
// `cn` — the class-name composer, surfaced for convenience.
|
||||
export { cn } from './core/cn'
|
||||
|
||||
// The design-token scale (colors, dark/light themes, radii, spacing, typography)
|
||||
// lives on the `@hanzo/ui/core` / `@hanzo/ui/tokens` subpath — pure data, no
|
||||
// runtime. The gui/product theme object (`tokens`, `TAG_TONES`) is on the gui
|
||||
// backend (`@hanzo/ui/gui`), since it belongs to @hanzo/data's gui world. Keeping
|
||||
// both off this root barrel is what lets the component API stay a clean,
|
||||
// web-only import with no native (@hanzo/gui) runtime pulled in.
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
|
||||
import { cn } from '../core/cn'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '../backends/shadcn'
|
||||
import { familyOf, filterChatModels, groupModelsByFamily, type ModelCatalogEntry } from './catalog'
|
||||
|
||||
/** Show the search input once a catalog gets this large. */
|
||||
const SEARCH_THRESHOLD = 12
|
||||
|
||||
function fmtContext(ctx: number | undefined): string {
|
||||
if (!ctx) return ''
|
||||
if (ctx >= 1_000_000) return `${(ctx / 1_000_000).toFixed(ctx % 1_000_000 === 0 ? 0 : 1)}M`
|
||||
if (ctx >= 1000) return `${Math.round(ctx / 1000)}K`
|
||||
return `${ctx}`
|
||||
}
|
||||
|
||||
export interface ModelSelectorProps {
|
||||
models: ModelCatalogEntry[]
|
||||
value?: string
|
||||
onChange: (id: string) => void
|
||||
disabled?: boolean
|
||||
size?: 'sm' | 'md'
|
||||
placeholder?: string
|
||||
/** Default true: filter to chat-capable models (exclude embedding/image/etc.). */
|
||||
chatOnly?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ModelSelector — the one unified model picker for every Hanzo app.
|
||||
*
|
||||
* Family-grouped picker in a compact Radix Popover + cmdk Command combobox:
|
||||
* grouped sections with family headers, premium markers, context suffixes,
|
||||
* keyboard navigation, and type-to-filter search for large catalogs. Monochrome,
|
||||
* dark-first, data-agnostic. Every class is a standard token; the font is
|
||||
* inherited (Geist).
|
||||
*/
|
||||
export function ModelSelector({
|
||||
models,
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
size = 'md',
|
||||
placeholder = 'Select model',
|
||||
chatOnly = true,
|
||||
className,
|
||||
}: ModelSelectorProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const visible = useMemo(() => (chatOnly ? filterChatModels(models) : models), [models, chatOnly])
|
||||
const groups = useMemo(() => groupModelsByFamily(visible), [visible])
|
||||
const showSearch = visible.length > SEARCH_THRESHOLD
|
||||
|
||||
// Resolve the selected label from the FULL list so a current pick still shows
|
||||
// even when chatOnly would otherwise hide it.
|
||||
const selected = value ? models.find((m) => m.id === value) : undefined
|
||||
const selectedLabel = selected ? (selected.label ?? selected.id) : placeholder
|
||||
const selectedFamily = selected ? familyOf(selected) : undefined
|
||||
|
||||
const isSm = size === 'sm'
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-md border border-border bg-background text-left font-medium text-foreground transition hover:border-primary/40 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||
isSm ? 'h-7 px-2 text-xs' : 'h-9 px-3 text-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{selectedLabel}</span>
|
||||
{selectedFamily && <span className="truncate text-muted-foreground">{selectedFamily}</span>}
|
||||
<ChevronsUpDown
|
||||
className={cn('ml-auto shrink-0 opacity-50', isSm ? 'h-3 w-3' : 'h-3.5 w-3.5')}
|
||||
/>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-[var(--radix-popover-trigger-width)] min-w-[14rem] border-border bg-popover p-0 text-popover-foreground shadow-md"
|
||||
>
|
||||
<Command>
|
||||
{showSearch && <CommandInput placeholder="Search models…" className="h-9 text-sm" />}
|
||||
<CommandList className="max-h-72">
|
||||
<CommandEmpty>No models found.</CommandEmpty>
|
||||
{groups.map((group) => (
|
||||
<CommandGroup
|
||||
key={group.family}
|
||||
heading={group.family}
|
||||
className="[&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wide [&_[cmdk-group-heading]]:text-muted-foreground"
|
||||
>
|
||||
{group.models.map((m) => {
|
||||
const label = m.label ?? m.id
|
||||
const ctx = fmtContext(m.context_window)
|
||||
return (
|
||||
<CommandItem
|
||||
key={m.id}
|
||||
value={`${group.family} ${label} ${m.id}`}
|
||||
onSelect={() => {
|
||||
onChange(m.id)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="gap-2"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 shrink-0',
|
||||
m.id === value ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{label}</span>
|
||||
{m.premium && (
|
||||
<span className="text-muted-foreground" title="Premium" aria-label="Premium">
|
||||
✦
|
||||
</span>
|
||||
)}
|
||||
{ctx && (
|
||||
<span className="ml-auto shrink-0 pl-2 text-[10px] tabular-nums text-muted-foreground">
|
||||
{ctx}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelSelector
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @hanzo/ui-shadcn/models — Model catalog data layer
|
||||
*
|
||||
* Pure, SSR-safe helpers shared by the unified ModelSelector. No React,
|
||||
* no browser globals — usable on the server, in edge runtimes, or the client.
|
||||
*
|
||||
* ModelCatalogEntry is the gateway/OpenAI-shaped record ({ id, owned_by, ... })
|
||||
* returned by `${baseUrl}/models`. It is intentionally distinct from the
|
||||
* registry-shaped ZenModelLike in ./types (which drives the marketing cards).
|
||||
*/
|
||||
|
||||
export interface ModelCatalogEntry {
|
||||
/** Gateway id, e.g. "claude-opus-4.8" */
|
||||
id: string
|
||||
/** e.g. "anthropic" | "openai" | "hanzo" | "deepseek" */
|
||||
owned_by?: string
|
||||
/** Optional explicit family label override */
|
||||
family?: string
|
||||
/** Display label; defaults to id */
|
||||
label?: string
|
||||
premium?: boolean
|
||||
/** "chat" (default) | "embedding" | "image" | ... */
|
||||
modality?: string
|
||||
context_window?: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
/** owned_by → display family. */
|
||||
const OWNER_FAMILY: Record<string, string> = {
|
||||
hanzo: 'Zen',
|
||||
anthropic: 'Anthropic',
|
||||
openai: 'OpenAI',
|
||||
deepseek: 'DeepSeek',
|
||||
alibaba: 'Qwen',
|
||||
qwen: 'Qwen',
|
||||
meta: 'Meta Llama',
|
||||
mistralai: 'Mistral',
|
||||
mistral: 'Mistral',
|
||||
google: 'Google Gemma',
|
||||
nvidia: 'NVIDIA',
|
||||
moonshot: 'Kimi',
|
||||
}
|
||||
|
||||
/** House then marquee; everything else sorts alphabetically after these. */
|
||||
const FAMILY_ORDER = ['Enso', 'Zen', 'Anthropic', 'OpenAI']
|
||||
|
||||
function capitalize(s: string): string {
|
||||
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s
|
||||
}
|
||||
|
||||
/** Infer a family from an id prefix. Returns undefined when nothing matches. */
|
||||
function familyFromId(id: string): string | undefined {
|
||||
if (id.startsWith('enso')) return 'Enso'
|
||||
if (id.startsWith('zen')) return 'Zen'
|
||||
if (id.startsWith('claude')) return 'Anthropic'
|
||||
if (id.startsWith('gpt') || id === 'o1' || id.startsWith('o1-') || id.startsWith('o3')) return 'OpenAI'
|
||||
if (id.startsWith('qwen')) return 'Qwen'
|
||||
if (id.startsWith('llama')) return 'Meta Llama'
|
||||
if (id.startsWith('glm')) return 'GLM'
|
||||
if (id.startsWith('kimi')) return 'Kimi'
|
||||
if (id.startsWith('deepseek')) return 'DeepSeek'
|
||||
if (id.startsWith('mistral')) return 'Mistral'
|
||||
if (id.startsWith('gemma')) return 'Google Gemma'
|
||||
if (id.startsWith('nemotron')) return 'NVIDIA'
|
||||
if (id.startsWith('minimax')) return 'MiniMax'
|
||||
if (id.startsWith('mimo')) return 'MiMo'
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the display family of a model.
|
||||
* Precedence: explicit `family` → enso id (house split, beats owned_by) →
|
||||
* owned_by map → llama id under any owner → capitalized owned_by → id prefix → "Other".
|
||||
*/
|
||||
export function familyOf(m: ModelCatalogEntry): string {
|
||||
const explicit = m.family?.trim()
|
||||
if (explicit) return explicit
|
||||
|
||||
const id = (m.id ?? '').toLowerCase()
|
||||
// Enso ids form their own house family regardless of owned_by.
|
||||
if (id.startsWith('enso')) return 'Enso'
|
||||
|
||||
const owned = m.owned_by?.toLowerCase().trim()
|
||||
if (owned) {
|
||||
const byOwner = OWNER_FAMILY[owned]
|
||||
if (byOwner) return byOwner
|
||||
// llama* ids resolve to Meta Llama even under a different owner.
|
||||
const byId = familyFromId(id)
|
||||
if (byId) return byId
|
||||
return capitalize(owned)
|
||||
}
|
||||
|
||||
return familyFromId(id) ?? 'Other'
|
||||
}
|
||||
|
||||
function orderFamilies(a: string, b: string): number {
|
||||
const ia = FAMILY_ORDER.indexOf(a)
|
||||
const ib = FAMILY_ORDER.indexOf(b)
|
||||
if (ia !== -1 || ib !== -1) {
|
||||
if (ia === -1) return 1
|
||||
if (ib === -1) return -1
|
||||
return ia - ib
|
||||
}
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
|
||||
/**
|
||||
* Group models by family, ordered house-then-marquee-then-alphabetical.
|
||||
* Model order within each family is the input order (stable).
|
||||
*/
|
||||
export function groupModelsByFamily(
|
||||
models: ModelCatalogEntry[],
|
||||
): { family: string; models: ModelCatalogEntry[] }[] {
|
||||
const byFamily = new Map<string, ModelCatalogEntry[]>()
|
||||
for (const m of models) {
|
||||
const fam = familyOf(m)
|
||||
const arr = byFamily.get(fam)
|
||||
if (arr) arr.push(m)
|
||||
else byFamily.set(fam, [m])
|
||||
}
|
||||
return Array.from(byFamily.keys())
|
||||
.sort(orderFamilies)
|
||||
.map((family) => ({ family, models: byFamily.get(family)! }))
|
||||
}
|
||||
|
||||
/** Modality/id keywords that mark a model as NOT chat-capable. */
|
||||
const NON_CHAT = [
|
||||
'embedding',
|
||||
'embed',
|
||||
'image',
|
||||
'video',
|
||||
'music',
|
||||
'voice',
|
||||
'tts',
|
||||
'rerank',
|
||||
'guard',
|
||||
'foley',
|
||||
'moderation',
|
||||
'router',
|
||||
]
|
||||
|
||||
/**
|
||||
* True when a model can be used for chat. Excludes embedding/image/video/
|
||||
* music/voice/tts/rerank/guard/foley/moderation/router by explicit modality
|
||||
* or by an id-segment heuristic.
|
||||
*/
|
||||
export function isChatModel(m: ModelCatalogEntry): boolean {
|
||||
const modality = (m.modality ?? '').toLowerCase().trim()
|
||||
if (modality && NON_CHAT.some((k) => modality.includes(k))) return false
|
||||
const id = (m.id ?? '').toLowerCase()
|
||||
if (NON_CHAT.some((k) => id.includes(k))) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/** Keep only chat-capable models. */
|
||||
export function filterChatModels(models: ModelCatalogEntry[]): ModelCatalogEntry[] {
|
||||
return models.filter(isChatModel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a model catalog from an OpenAI-shaped `/models` endpoint.
|
||||
* SSR-safe: uses global fetch, no browser globals, no caching/state —
|
||||
* consumers own caching. Throws on a non-2xx response.
|
||||
*/
|
||||
export async function fetchModelCatalog(
|
||||
baseUrl?: string,
|
||||
token?: string,
|
||||
): Promise<ModelCatalogEntry[]> {
|
||||
const base = (baseUrl ?? 'https://api.hanzo.ai/v1').replace(/\/+$/, '')
|
||||
const headers: Record<string, string> = { Accept: 'application/json' }
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
|
||||
const res = await fetch(`${base}/models`, { headers })
|
||||
if (!res.ok) {
|
||||
throw new Error(`fetchModelCatalog: ${res.status} ${res.statusText}`)
|
||||
}
|
||||
|
||||
const json: unknown = await res.json()
|
||||
const rows: any[] = Array.isArray(json)
|
||||
? json
|
||||
: Array.isArray((json as { data?: unknown })?.data)
|
||||
? (json as { data: any[] }).data
|
||||
: []
|
||||
|
||||
return rows
|
||||
.map((d): ModelCatalogEntry => ({
|
||||
id: String(d.id),
|
||||
owned_by: d.owned_by ?? d.ownedBy ?? undefined,
|
||||
family: d.family ?? undefined,
|
||||
label: d.label ?? d.name ?? undefined,
|
||||
premium: d.premium ?? undefined,
|
||||
modality: d.modality ?? undefined,
|
||||
context_window: d.context_window ?? d.contextWindow ?? d.context ?? undefined,
|
||||
description: d.description ?? undefined,
|
||||
}))
|
||||
.filter((m) => m.id && m.id !== 'undefined')
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// @hanzo/ui/models — the unified, data-agnostic model picker + catalog helpers.
|
||||
//
|
||||
// import { ModelSelector, fetchModelCatalog } from '@hanzo/ui/models'
|
||||
// const models = await fetchModelCatalog()
|
||||
// <ModelSelector models={models} value={id} onChange={setId} />
|
||||
|
||||
export { ModelSelector } from './ModelSelector'
|
||||
export type { ModelSelectorProps } from './ModelSelector'
|
||||
|
||||
export {
|
||||
familyOf,
|
||||
groupModelsByFamily,
|
||||
isChatModel,
|
||||
filterChatModels,
|
||||
fetchModelCatalog,
|
||||
} from './catalog'
|
||||
export type { ModelCatalogEntry } from './catalog'
|
||||
|
||||
export type { ZenModelLike, ModelFamilyLike, ModelSpecLike, ModelPricingLike } from './types'
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @hanzo/ui — Model component types
|
||||
*
|
||||
* These types are structurally compatible with @hanzo/zen-models ZenModel / ModelFamily,
|
||||
* so ZenModel objects can be passed directly to these components without casting.
|
||||
*/
|
||||
|
||||
export interface ModelSpecLike {
|
||||
params: string
|
||||
activeParams?: string | null
|
||||
context?: number
|
||||
arch?: string
|
||||
}
|
||||
|
||||
export interface ModelPricingLike {
|
||||
input: number | null
|
||||
output: number | null
|
||||
cacheRead?: number | null
|
||||
cacheWrite?: number | null
|
||||
}
|
||||
|
||||
export interface ZenModelLike {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
spec: ModelSpecLike
|
||||
pricing: ModelPricingLike | null
|
||||
/** 'available' | 'preview' | 'coming-soon' | 'contact-sales' | 'cloud-only' */
|
||||
status: string
|
||||
huggingface?: string | null
|
||||
github?: string | null
|
||||
paper?: string | null
|
||||
}
|
||||
|
||||
export interface ModelFamilyLike {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
/** lucide-react icon name */
|
||||
icon?: string
|
||||
/** ordered list of model IDs */
|
||||
models: string[]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { AspectRatio } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Avatar } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { AvatarFallback } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { AvatarImage } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Badge } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Button } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Card } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CardAction } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CardContent } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CardDescription } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CardFooter } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CardHeader } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CardTitle } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Checkbox } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Collapsible } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CollapsibleContent } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CollapsibleTrigger } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Command } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CommandDialog } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CommandEmpty } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CommandGroup } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CommandInput } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CommandItem } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CommandList } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CommandSeparator } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { CommandShortcut } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Dialog } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DialogClose } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DialogContent } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DialogDescription } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DialogFooter } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DialogHeader } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DialogOverlay } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DialogPortal } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DialogTitle } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DialogTrigger } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenu } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuCheckboxItem } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuContent } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuGroup } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuItem } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuLabel } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuPortal } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuRadioGroup } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuRadioItem } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuSeparator } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuShortcut } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuSub } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuSubContent } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuSubTrigger } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { DropdownMenuTrigger } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Input } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Label } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Popover } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { PopoverAnchor } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { PopoverContent } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { PopoverTrigger } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { Progress } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { ScrollArea } from '../backends/shadcn'
|
||||
@@ -0,0 +1,4 @@
|
||||
// GENERATED by scripts/gen-primitives.mjs — do not edit. Per-member entrypoint
|
||||
// for hosts that modularize `@hanzo/ui` imports. Source of truth:
|
||||
// src/backends/shadcn/index.ts. Re-run: node scripts/gen-primitives.mjs
|
||||
export { ScrollBar } from '../backends/shadcn'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user