@hanzo/svelte 1.0.0 — the Svelte backend, on its own

Extracted from @hanzo/ui pkg/ui/src/backends/svelte with its history. It lived
inside a React component library, which meant every consumer of @hanzo/ui
resolved svelte whether they rendered a single Svelte component or not, and the
package declared peer svelte>=5 against a dev svelte ^4.2.20 — two answers to
one question.

Three runtime dependencies, all load-bearing: class-variance-authority for the
variants the components already used, and clsx + tailwind-merge behind cn. The
cn helper is vendored (17 lines) rather than imported back from @hanzo/ui,
because depending on the package this was extracted from would undo the split.
No design-system runtime, no icon pack, no CSS-in-JS.

svelte-check: 50 files, 0 errors.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-28 12:28:39 -07:00
co-authored by Hanzo Dev
parent 8194bf32af
commit 1008926ef7
39 changed files with 140 additions and 35 deletions
+1
View File
@@ -0,0 +1 @@
node_modules
+1 -1
View File
@@ -21,7 +21,7 @@
</script>
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
export let variant: BadgeVariant = 'default'
+1 -1
View File
@@ -47,7 +47,7 @@
</script>
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- Card container — standard-token surface; mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- CardAction — mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- CardContent — mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- CardDescription — mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- CardFooter — mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- CardHeader — mirrors the shadcn backend's grid header. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- CardTitle — mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -3,7 +3,7 @@
the shadcn backend's data-state token styling. `bind:checked` is supported.
-->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
import { createEventDispatcher } from 'svelte'
let className: string | undefined = undefined
+1 -1
View File
@@ -7,7 +7,7 @@
import { onDestroy } from 'svelte'
import { getDisclosure, DIALOG } from './internal/context'
import { portal, trapFocus, dismiss } from './internal/actions'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -1,7 +1,7 @@
<!-- DialogDescription — describes the dialog (aria-describedby target). -->
<script lang="ts">
import { getDisclosure, DIALOG } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
const { descId } = getDisclosure(DIALOG, 'DialogDescription')
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- DialogFooter — mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- DialogHeader — mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,7 +1,7 @@
<!-- DialogTitle — labels the dialog for assistive tech (aria-labelledby target). -->
<script lang="ts">
import { getDisclosure, DIALOG } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
const { labelId } = getDisclosure(DIALOG, 'DialogTitle')
+1 -1
View File
@@ -6,7 +6,7 @@
<script lang="ts">
import { getDisclosure, DROPDOWN } from './internal/context'
import { rovingFocus } from './internal/actions'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -1,7 +1,7 @@
<!-- DropdownMenuItem — a menu item (native button → free Enter/Space activation). -->
<script lang="ts">
import { getDisclosure, DROPDOWN } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- DropdownMenuLabel — mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
export let inset = false
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- DropdownMenuSeparator — mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- Input — standard-token text field; class string mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- Label — standard-token form label; mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -5,7 +5,7 @@
-->
<script lang="ts">
import { getDisclosure, POPOVER } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+45
View File
@@ -0,0 +1,45 @@
# @hanzo/svelte
Hanzo UI for Svelte 5.
These components used to live inside `@hanzo/ui` as `@hanzo/ui/svelte`. They are
here now because a component library should be one stack: `@hanzo/ui` is React on
`@hanzo/gui`, and shipping a second renderer inside it meant every consumer
resolved `svelte` whether or not they used it.
Nothing about the components changed. Same shapes, same `cva` variants, same
tokens.
## Install
```sh
npm i @hanzo/svelte svelte
```
Two dependencies, on purpose: `svelte` (peer, yours) and `class-variance-authority`
(the variant function). No design-system runtime, no icon pack, no CSS-in-JS.
## Use
```svelte
<script>
import { Button, Card, CardHeader, CardContent } from '@hanzo/svelte'
</script>
<Card>
<CardHeader>Ship it</CardHeader>
<CardContent><Button variant="default">Go</Button></CardContent>
</Card>
```
Import one component if you want one component:
```svelte
import Button from '@hanzo/svelte/Button.svelte'
```
## Composition
Every component takes `class` and spreads the rest onto its root element, so a
variant is a function of props and nothing is hidden behind a theme provider.
Compose by wrapping, not by configuring.
+1 -1
View File
@@ -6,7 +6,7 @@
<script lang="ts">
import { getSelect } from './internal/context'
import { rovingFocus } from './internal/actions'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -6,7 +6,7 @@
<script lang="ts">
import { onMount } from 'svelte'
import { getSelect } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- SelectLabel — a group heading. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- SelectSeparator — divides option groups. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
</script>
+1 -1
View File
@@ -5,7 +5,7 @@
-->
<script lang="ts">
import { getSelect } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- Separator — standard-token rule; mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -3,7 +3,7 @@
shadcn backend's data-state token styling. `bind:checked` is supported.
-->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
import { createEventDispatcher } from 'svelte'
let className: string | undefined = undefined
+1 -1
View File
@@ -5,7 +5,7 @@
-->
<script lang="ts">
import { createSelection, TABS } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -1,7 +1,7 @@
<!-- TabsContent — the panel shown for its `value` (role=tabpanel). -->
<script lang="ts">
import { getSelection, TABS } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -4,7 +4,7 @@
-->
<script lang="ts">
import { rovingFocus } from './internal/actions'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -4,7 +4,7 @@
-->
<script lang="ts">
import { getSelection, TABS } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- Textarea — standard-token multiline field; mirrors the shadcn backend. -->
<script lang="ts">
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+1 -1
View File
@@ -4,7 +4,7 @@
-->
<script lang="ts">
import { getDisclosure, TOOLTIP } from './internal/context'
import { cn } from '../../core/cn'
import { cn } from './core/cn'
let className: string | undefined = undefined
export { className as class }
+18
View File
@@ -0,0 +1,18 @@
/**
* `cn` — the one class-name composer.
*
* `clsx` resolves conditional/array/object class inputs; `tailwind-merge` then
* collapses conflicting Tailwind utilities so the LAST wins (a caller's
* `bg-background` overrides a component's default `bg-popover`). Every component
* here merges through this single function.
*
* Vendored rather than imported from @hanzo/ui: this package exists so a Svelte
* app does not pull in a React component library, and depending back on it for
* three lines would undo that.
*/
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs))
}
+41
View File
@@ -0,0 +1,41 @@
{
"name": "@hanzo/svelte",
"version": "1.0.0",
"description": "Hanzo UI, Svelte 5. The shadcn-shaped components extracted from @hanzo/ui so the core library stays one stack. Same shapes, same variants, same tokens.",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/hanzoai/svelte.git"
},
"type": "module",
"sideEffects": false,
"files": [
"*.svelte",
"index.ts",
"core",
"README.md"
],
"svelte": "./index.ts",
"types": "./index.ts",
"exports": {
".": {
"svelte": "./index.ts",
"types": "./index.ts",
"default": "./index.ts"
},
"./*.svelte": "./*.svelte"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"tailwind-merge": "^2.6.1"
},
"peerDependencies": {
"svelte": ">=5"
},
"devDependencies": {
"svelte": "^5.56.8",
"svelte-check": "^4.7.4",
"typescript": "^6.0.3"
}
}