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:
2026-07-21 09:27:10 -07:00
parent c9f51db1da
commit 1ee64263ff
130 changed files with 2926 additions and 49 deletions
+75 -22
View File
@@ -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
View File
@@ -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",
+72
View File
@@ -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`)
+42
View File
@@ -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.
+19
View File
@@ -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 }
+50
View File
@@ -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 }
+54
View File
@@ -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 }
+104
View File
@@ -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 }
+77
View File
@@ -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 }
+37
View File
@@ -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 }
+157
View File
@@ -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,
}
+117
View File
@@ -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,
}
+94
View File
@@ -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'
+89
View File
@@ -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 }
+24
View File
@@ -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 }
+32
View File
@@ -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 }
+27
View File
@@ -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 }
+157
View File
@@ -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,
}
+26
View File
@@ -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 }
+35
View File
@@ -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 }
+31
View File
@@ -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 }
+66
View File
@@ -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 }
+23
View File
@@ -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 }
+42
View File
@@ -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 }
+33
View File
@@ -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 }
+8
View File
@@ -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'
+6
View File
@@ -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'
+15
View File
@@ -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))
}
+27
View File
@@ -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
+9
View File
@@ -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'
+43
View File
@@ -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
View File
@@ -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.
+152
View File
@@ -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
+197
View File
@@ -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')
}
+19
View File
@@ -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'
+43
View File
@@ -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[]
}
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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'
+4
View File
@@ -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