# @orochibraru/ui Svelte 5 components and the theme shared by OpenCount, LocalAssistant, Homerun, Sergio's, Nuvio Web and Penombre: the shadcn-svelte primitives (vega style) as these apps customised them, a handful of app-level blocks, and the oklch theme they all draw from. One package instead of six drifting copies. Built for SvelteKit 3, Svelte 5 runes and Tailwind CSS v4. ## Install ```bash bun add -d @orochibraru/ui ``` It goes in `devDependencies`: the app bundles it, so nothing from it is needed at runtime next to the compiled server. ## Set up the stylesheet Import the theme right after Tailwind in the app's root stylesheet: ```css @import "tailwindcss"; @import "@orochibraru/ui/theme.css"; ``` The theme brings the design tokens, the `data-*` state variants the primitives style with, `tw-animate-css`, and an `@source` that makes Tailwind scan the package, so no other configuration is needed. ## Use Every primitive is a subpath, with the same exports shadcn-svelte generates: ```svelte ``` Moving an app over is a rewrite of its imports: `#lib/components/ui/button/index.js` becomes `@orochibraru/ui/button`. Primitives: accordion, alert, alert-dialog, avatar, badge, breadcrumb, button, button-group, calendar, card, checkbox, command, context-menu, dialog, drawer, dropdown-menu, empty, field, input, input-group, input-otp, label, menubar, pagination, popover, progress, radio-group, range-calendar, resizable, scroll-area, select, separator, sheet, sidebar, skeleton, slider, sonner, spinner, switch, table, tabs, textarea, toggle, toggle-group, tooltip. `input` turns password managers off on fields that don't set `autocomplete`, so a search box never gets a login suggestion. ## Blocks Under `@orochibraru/ui/blocks/`: | Block | What it does | | --------------------- | ----------------------------------------------------------------------------- | | `accent-toggle` | The accent presets: swatch strip on desktop, labelled rows on phones. | | `auth-shell` | Centered sign-in layout: heading, optional `brand` and `footer` snippets. | | `bottom-nav` | Phone tab bar of top-level destinations, the current one lit. | | `copy-field` | Monospace value with a copy button and a promise toast. | | `document-title` | `` from the page title, suffixed with `appName`. | | `empty-state` | Icon, title, subtitle and an action slot for an empty list. | | `error-page` | Status, message and error id from `page`, with a link to `homeHref`. | | `fab` | Floating action button above the phone tab bar, with a haptic tap. | | `icon-button` | Round icon button with a tooltip that doubles as its accessible name. | | `install-prompt` | One-time PWA install sheet: native prompt, iOS steps or browser menu steps. | | `music-player` | Audio bar with a queue: play, skip, seek, volume; positioning is the app's. | | `nav-cards` | Phone-only list of a section's tabs as cards: icon, label, description. | | `new-password-fields` | New password and its confirmation: reveal, strength meter, rules. | | `option-group` | Radio cards for a settings choice, with an icon or a `swatch` snippet each. | | `otp-field` | One-time code: digits only, SMS autofill, posted as one `code` field. | | `password-input` | Password field with a button to reveal it. | | `responsive-sheet` | A dialog on desktop, a bottom drawer under 768px. | | `setting-switch` | Labelled switch row for settings forms, posting `name=true` when on. | | `stat-card` | Glass card with an icon, a value and a hint, optionally a link. | | `tab-nav` | Pill tab bar of links, the current page's tab highlighted. | | `tab-section` | Layout for a tabbed section: tabs on desktop, nav cards on phones. | | `theme-picker` | System, light and dark as radio cards, for an appearance page. | | `theme-toggle` | System, light and dark: icon strip on desktop, three rows on phones. | | `top-loading-bar` | Progress bar during SvelteKit navigations, safe-area aware. | | `video-player` | Video with scrubbing, chapters, subtitles, speed, fullscreen, gestures, keys. | Blocks with built-in copy (`accent-toggle`, `copy-field`, `error-page`, `install-prompt`, `new-password-fields`, `password-input`, `theme-picker`, `theme-toggle`) take a `messages` prop. Its keys override the English defaults one by one, which is how a French or translated app uses them. Blocks never set the page title, except `tab-section`, which owns its section's title: the app calls `setPageTitle` itself. ### Tabbed sections A tabbed page is one real route per tab, each tab with a description, under a `tab-section` layout. On phones the index route renders `nav-cards` and each tab is a page titled by its label, its back button returning to the cards. On desktop the index replaces itself with the first tab and `tab-nav` shows them all. The tabs live in one list (`SectionTab[]`, exported by `nav-cards`) that the layout and the index page share. ### Theme `theme-picker` and `theme-toggle` call `mode-watcher`'s `setMode`, so the app mounts `<ModeWatcher />` in its root layout. Both take the saved theme as `value`, so the server-rendered page checks the right one, and call `onchange` with the new theme for the app to persist. `theme-picker` also posts as a `theme` radio field (`name` changes it) inside a form. `icon-button`, and `copy-field` through it, need a `Tooltip.Provider` above them, which apps put in their root layout. ## Modules - `@orochibraru/ui/utils`: `cn` and the shadcn-svelte prop helper types. - `@orochibraru/ui/toast`: `enhanceToast` and `saveToast` narrate a form action through one promise toast, `toastError` turns anything thrown into a message. Form updates keep `reset: false` by default so `bind:value` fields survive a submit. - `@orochibraru/ui/page-title`: `providePageTitle` once in the root layout, `setPageTitle` in pages, `usePageTitle` in app bars. - `@orochibraru/ui/appearance`: `applyAppearance` and `appearanceBootScript` apply a saved accent, theme, dark variant, font and corner style to `<html>`, the boot script before first paint. - `@orochibraru/ui/password`: `passwordRules` and `passwordScore`, the rules a new password must pass and a 0 to 4 strength, the same on the form and the server. `new-password-fields` renders them; `ruleLabels` translates them. - `@orochibraru/ui/haptics`: `haptic("tap" | "success" | "error")`, a no-op where vibration isn't supported. ## Theming Colours derive from two variables, so an app usually overrides only these after importing the theme: ```css :root { --hue: 8; --primary-chroma: 0.15; } ``` On `<html>`: - `class="dark"` switches to the dark palette, `data-dark-variant="amoled"` makes it pure black. - `data-accent` picks a preset: `bordeaux`, `green`, `blue`, `violet`, `rose`, `orange`, `amber`, `cyan` or `neutral`. - `data-font` (`system`, `serif`, `mono`) and `data-corners="square"` change the type and the radius. `class="aurora"` on `<body>` adds the drifting gradient background. It is opt-in so apps with a flat look don't get it. The theme also defines the `glass`, `glow`, `no-scrollbar` and `tabular` utilities, the `standalone` variant for installed PWAs, `safe-*` spacing for notches, the toast styling and the page transitions on phones. Fonts are left to each app: set `--app-font` and `--display-font`. ## Develop ```bash bun install bun run dev ``` The dev server is the docs site: a page per block, primitive and module, with the component live and the code that renders it. That code is the example file itself, its `#lib` imports rewritten to the package subpaths, so the two can't drift apart. A file named `<slug>.<name>` next to an example is a companion, shown after it: the layout, action or page an app needs for what the preview can't hold. An example without a script is a note for the preview only. Every component page can be copied as Markdown, viewed as Markdown, exported as a PDF (through the browser's print dialog, in the light theme), or opened in Claude or ChatGPT. The list pages filter by category. The `Dockerfile` deploys the docs site: `bun run build:docs` compiles it to one Bun binary that serves on port 3000 and reads `x-forwarded-proto` and `x-forwarded-host`, so llms.txt links point at the public URL behind a proxy. `bun run check`, `bun run test` and `bun run build` are the gates; `build` packages `dist/` and runs publint. New upstream components come from the shadcn-svelte CLI (`components.json` is set up), followed by `node_modules/.bin/tailwint --fix` on the new files. The CLI overwrites any customised primitive a new one depends on (drawer, input and sonner carry the apps' changes): copy those aside before `add --overwrite` and put them back after. Dependencies use caret ranges, unlike the apps' exact pins, so a consuming app dedupes `bits-ui` and friends with its own copy instead of bundling two. ## MCP server The package ships an MCP server with the docs and every example's code bundled in, so an editor's assistant can list, search and read the components without the docs site. It runs over stdio from the package's `bin`: ```bash claude mcp add orochibraru-ui -- npx -y @orochibraru/ui ``` Cursor, VS Code and other clients take the same command: ```json { "mcpServers": { "orochibraru-ui": { "command": "npx", "args": ["-y", "@orochibraru/ui"] } } } ``` Its tools are `get_setup` (this README), `list_components` (by group or category), `search_components` and `get_component` (the description, import path and usage code). `bun run build` bundles it with its content, so it needs no dependency at runtime. The docs site serves the same server over streamable HTTP at `/mcp`, plus `/llms.txt`, `/llms-full.txt` and a Markdown page per component at `/<group>/<slug>.md`. --- # Accent toggle > The accent presets: a swatch strip on desktop, labelled rows on phones. Category: Appearance. Import from `@orochibraru/ui/blocks/accent-toggle`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { toast } from "svelte-sonner"; import AccentToggle from "@orochibraru/ui/blocks/accent-toggle"; </script> <AccentToggle onchange={(accent) => toast(`Accent: ${accent}.`)} /> ``` --- # Auth shell > Centered sign-in layout: heading, optional brand and footer. Category: Layout. Import from `@orochibraru/ui/blocks/auth-shell`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import AuthShell from "@orochibraru/ui/blocks/auth-shell"; import { Button } from "@orochibraru/ui/button"; import * as Field from "@orochibraru/ui/field"; import { Input } from "@orochibraru/ui/input"; </script> <AuthShell heading="Sign in" subheading="Welcome back."> {#snippet brand()} <div class="bg-primary glow size-12 rounded-2xl"></div> {/snippet} <form class="grid gap-4"> <Field.Field> <Field.Label for="auth-email">Email</Field.Label> <Input id="auth-email" type="email" autocomplete="email" /> </Field.Field> <Button type="submit" class="h-11">Continue</Button> </form> {#snippet footer()} No account? Ask your admin. {/snippet} </AuthShell> ``` --- # Bottom nav > The phone tab bar: top-level destinations, the current one lit. Category: Navigation. Import from `@orochibraru/ui/blocks/bottom-nav`. ## Usage ### +layout.svelte ```svelte <script lang="ts"> import HouseIcon from "@lucide/svelte/icons/house"; import UserRoundIcon from "@lucide/svelte/icons/user-round"; import UsersIcon from "@lucide/svelte/icons/users"; import BottomNav from "@orochibraru/ui/blocks/bottom-nav"; const { children } = $props(); const items = [ { href: "/", label: "Home", icon: HouseIcon }, { href: "/groups", label: "Groups", icon: UsersIcon }, { href: "/account", label: "Account", icon: UserRoundIcon }, ]; </script> <main class="min-h-dvh pb-32 md:pb-10"> {@render children()} </main> <BottomNav {items} /> ``` --- # Copy field > Monospace value with a copy button and a promise toast. Category: Forms. Import from `@orochibraru/ui/blocks/copy-field`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import CopyField from "@orochibraru/ui/blocks/copy-field"; </script> <CopyField label="Copy the token" value="oc_4f9a2c81d7e3b605" /> ``` --- # Document title > The tab's <title> from the page title, suffixed with the app. Category: Layout. Import from `@orochibraru/ui/blocks/document-title`. ## Usage ### +layout.svelte ```svelte <script lang="ts"> import DocumentTitle from "@orochibraru/ui/blocks/document-title"; import { providePageTitle } from "@orochibraru/ui/page-title"; const { children } = $props(); providePageTitle(); </script> <DocumentTitle appName="Acme" /> {@render children()} ``` --- # Empty state > Icon, title, subtitle and an action for an empty list. Category: Feedback. Import from `@orochibraru/ui/blocks/empty-state`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import InboxIcon from "@lucide/svelte/icons/inbox"; import PlusIcon from "@lucide/svelte/icons/plus"; import EmptyState from "@orochibraru/ui/blocks/empty-state"; import { Button } from "@orochibraru/ui/button"; </script> <EmptyState icon={InboxIcon} title="No messages yet" subtitle="Start a conversation and it shows up here." > <Button><PlusIcon /> New message</Button> </EmptyState> ``` --- # Error page > Status, message and error id, with a link back home. Category: Layout. Import from `@orochibraru/ui/blocks/error-page`. ## Usage ### +error.svelte ```svelte <script lang="ts"> import ErrorPage from "@orochibraru/ui/blocks/error-page"; </script> <ErrorPage homeHref="/" messages={{ notFoundTitle: "Rien ici", home: "Retour à l'accueil" }} /> ``` --- # Fab > Floating action button above the phone tab bar, with a haptic tap. Category: Actions. Import from `@orochibraru/ui/blocks/fab`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import PlusIcon from "@lucide/svelte/icons/plus"; import { toast } from "svelte-sonner"; import Fab from "@orochibraru/ui/blocks/fab"; </script> <p class="text-muted-foreground text-sm"> Phones only: narrow the window and it floats bottom right, above the tab bar. On desktop the same action is a primary button in the page header. </p> <Fab label="New task" icon={PlusIcon} onclick={() => toast("Fab tapped.")} /> ``` --- # Icon button > Round icon button whose tooltip is its accessible name. Category: Actions. Import from `@orochibraru/ui/blocks/icon-button`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import PencilIcon from "@lucide/svelte/icons/pencil"; import SettingsIcon from "@lucide/svelte/icons/settings"; import Trash2Icon from "@lucide/svelte/icons/trash-2"; import IconButton from "@orochibraru/ui/blocks/icon-button"; </script> <div class="flex flex-wrap items-center gap-2"> <IconButton label="Edit" icon={PencilIcon} /> <IconButton label="Settings" icon={SettingsIcon} href="#" variant="outline" /> <IconButton label="Delete" icon={Trash2Icon} variant="destructive" /> </div> ``` --- # Install prompt > One-time PWA install sheet: native prompt or the steps. Category: Overlays. Import from `@orochibraru/ui/blocks/install-prompt`. ## Usage ### +layout.svelte ```svelte <script lang="ts"> import InstallPrompt from "@orochibraru/ui/blocks/install-prompt"; const { children } = $props(); </script> {@render children()} <InstallPrompt appName="Acme" storageKey="acme-install-prompt"> {#snippet brand()} <img src="/icon-192.png" alt="" class="size-14 rounded-2xl" /> {/snippet} </InstallPrompt> ``` --- # Music player > An audio player bar with a queue: play, skip, seek and volume. Category: Media. Import from `@orochibraru/ui/blocks/music-player`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import MusicPlayer from "@orochibraru/ui/blocks/music-player"; const tracks = [ { src: "https://upload.wikimedia.org/wikipedia/commons/0/03/Music_to_Delight_%28ISRC_USUAN1200023%29.mp3", title: "Music to Delight", artist: "Kevin MacLeod", }, { src: "https://upload.wikimedia.org/wikipedia/commons/9/9c/Music_Box_Theme_%28ISRC_USUAN1100417%29.mp3", title: "Music Box Theme", artist: "Kevin MacLeod", }, { src: "https://upload.wikimedia.org/wikipedia/commons/c/c4/Industrial_Music_Box_%28ISRC_USUAN1100812%29.mp3", title: "Industrial Music Box", artist: "Kevin MacLeod", }, ]; let index = $state(0); let paused = $state(true); </script> <p class="text-muted-foreground text-sm"> It renders in flow, as wide as its container: wrap it in a fixed bar to dock it at the bottom of the screen. Tracks by Kevin MacLeod, CC BY 3.0, from Wikimedia Commons. </p> <MusicPlayer {tracks} bind:index bind:paused /> ``` --- # Nav cards > Phone-only list of a section's tabs as cards. Category: Navigation. Import from `@orochibraru/ui/blocks/nav-cards`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import BellIcon from "@lucide/svelte/icons/bell"; import FingerprintIcon from "@lucide/svelte/icons/fingerprint"; import UserRoundIcon from "@lucide/svelte/icons/user-round"; import NavCards, { type SectionTab, } from "@orochibraru/ui/blocks/nav-cards"; const tabs: SectionTab[] = [ { href: "#profile", label: "Profile", description: "Your name and the email you sign in with.", icon: UserRoundIcon, }, { href: "#security", label: "Security", description: "Password, passkeys and backup codes.", icon: FingerprintIcon, }, { href: "#notifications", label: "Notifications", description: "Get told on this device when something happens.", icon: BellIcon, }, ]; </script> <p class="text-muted-foreground hidden text-sm md:block"> Phones only: narrow the window to see the cards. On desktop the section shows its tabs instead. </p> <NavCards {tabs} /> ``` --- # New password fields > New password with a reveal button, a strength meter and the rules it must pass. Category: Forms. Import from `@orochibraru/ui/blocks/new-password-fields`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import NewPasswordFields from "@orochibraru/ui/blocks/new-password-fields"; import * as Field from "@orochibraru/ui/field"; </script> <Field.Group> <NewPasswordFields email="alex@example.com" /> </Field.Group> ``` ### +page.server.ts ```ts import { type Actions, fail } from "@sveltejs/kit"; import { passwordRules } from "@orochibraru/ui/password"; export const actions = { default: async ({ request }) => { const form = await request.formData(); const password = String(form.get("password") ?? ""); const failed = passwordRules( password, String(form.get("email") ?? ""), ).filter((rule) => !rule.passed); if (failed.length > 0) { return fail(400, { errors: { password: failed.map((rule) => rule.label) }, }); } if (form.get("confirmPassword") !== password) { return fail(400, { errors: { confirmPassword: ["The passwords don't match."] }, }); } return { saved: true }; }, } satisfies Actions; ``` --- # Option group > Radio cards for a settings choice, each with an icon or swatch. Category: Forms. Import from `@orochibraru/ui/blocks/option-group`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import BookOpenIcon from "@lucide/svelte/icons/book-open"; import CodeIcon from "@lucide/svelte/icons/code"; import TypeIcon from "@lucide/svelte/icons/type"; import OptionGroup from "@orochibraru/ui/blocks/option-group"; const fonts = [ { value: "default", label: "Default", description: "The app's own typefaces.", icon: TypeIcon, }, { value: "serif", label: "Serif", description: "A classic book face for a calmer read.", icon: BookOpenIcon, }, { value: "mono", label: "Mono", description: "Every character the same width.", icon: CodeIcon, }, ]; const accents = [ { value: "blue", label: "Blue", color: "oklch(0.6 0.19 255)" }, { value: "green", label: "Green", color: "oklch(0.6 0.14 165)" }, { value: "rose", label: "Rose", color: "oklch(0.6 0.2 12)" }, ]; let font = $state("default"); let accent = $state("blue"); </script> <div class="grid gap-6"> <OptionGroup name="font" legend="Font" options={fonts} bind:value={font} onchange={() => {}} /> <OptionGroup name="accent" legend="Accent" options={accents} bind:value={accent} onchange={() => {}} > {#snippet swatch(option)} <span class="size-5 rounded-full" style:background={accents.find((item) => item.value === option.value) ?.color} ></span> {/snippet} </OptionGroup> </div> ``` --- # Otp field > A one-time code: digits only, autofilled from SMS, posted as one field. Category: Forms. Import from `@orochibraru/ui/blocks/otp-field`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { toast } from "svelte-sonner"; import OtpField from "@orochibraru/ui/blocks/otp-field"; import * as Field from "@orochibraru/ui/field"; </script> <Field.Field> <Field.Label for="otp-code">Code from your authenticator app</Field.Label> <OtpField id="otp-code" oncomplete={(code) => toast(`Verifying ${code}.`)} /> </Field.Field> ``` --- # Password input > A password field with a button to reveal what was typed. Category: Forms. Import from `@orochibraru/ui/blocks/password-input`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import PasswordInput from "@orochibraru/ui/blocks/password-input"; import * as Field from "@orochibraru/ui/field"; </script> <Field.Field> <Field.Label for="sign-in-password">Password</Field.Label> <PasswordInput id="sign-in-password" required /> </Field.Field> ``` --- # Responsive sheet > A dialog on desktop, a bottom drawer on phones. Category: Overlays. Import from `@orochibraru/ui/blocks/responsive-sheet`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import ResponsiveSheet from "@orochibraru/ui/blocks/responsive-sheet"; import { Button } from "@orochibraru/ui/button"; let open = $state(false); </script> <div> <Button variant="outline" onclick={() => (open = true)}>Edit profile</Button> </div> <ResponsiveSheet bind:open title="Edit profile" description="A dialog on desktop, a drawer on phones." > <p class="pb-6 text-sm">Resize the window to see it switch.</p> </ResponsiveSheet> ``` --- # Setting switch > Labelled switch row for settings forms. Category: Forms. Import from `@orochibraru/ui/blocks/setting-switch`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import SettingSwitch from "@orochibraru/ui/blocks/setting-switch"; </script> <form method="POST" action="?/save"> <SettingSwitch name="push" label="Push notifications" description="On this device only." checked /> <SettingSwitch name="digest" label="Weekly digest" description="Every Monday morning." checked={false} /> </form> ``` --- # Stat card > Glass card with an icon, a value and a hint, optionally a link. Category: Display. Import from `@orochibraru/ui/blocks/stat-card`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import FolderIcon from "@lucide/svelte/icons/folder"; import HardDriveIcon from "@lucide/svelte/icons/hard-drive"; import ListTodoIcon from "@lucide/svelte/icons/list-todo"; import UsersIcon from "@lucide/svelte/icons/users"; import StatCard from "@orochibraru/ui/blocks/stat-card"; </script> <div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4"> <StatCard label="Projects" value={12} hint="This month" icon={FolderIcon} /> <StatCard label="Active members" value={48} icon={UsersIcon} tone="positive" /> <StatCard label="Storage used" value="86 %" icon={HardDriveIcon} tone="warning" /> <StatCard label="Open tasks" value={7} href="#" icon={ListTodoIcon} /> </div> ``` --- # Tab nav > Pill tab bar of links, the current page's tab highlighted. Category: Navigation. Import from `@orochibraru/ui/blocks/tab-nav`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import FingerprintIcon from "@lucide/svelte/icons/fingerprint"; import PaletteIcon from "@lucide/svelte/icons/palette"; import UserRoundIcon from "@lucide/svelte/icons/user-round"; import TabNav from "@orochibraru/ui/blocks/tab-nav"; const tabs = [ { href: "/blocks/tab-nav", label: "Profile", icon: UserRoundIcon }, { href: "/account/appearance", label: "Appearance", icon: PaletteIcon }, { href: "/account/security", label: "Security", icon: FingerprintIcon }, ]; </script> <TabNav {tabs} /> ``` --- # Tab section > Tabbed section layout: tabs on desktop, nav cards on phones. Category: Navigation. Import from `@orochibraru/ui/blocks/tab-section`. ## Usage ### section/+layout.svelte ```svelte <script lang="ts"> import { resolve } from "$app/paths"; import TabSection from "@orochibraru/ui/blocks/tab-section"; import { tabs } from "./tabs.js"; const { children } = $props(); </script> <TabSection title="Section" indexHref={resolve("/section")} backHref={resolve("/[group]/[slug]", { group: "blocks", slug: "tab-section", })} {tabs} > {@render children()} </TabSection> ``` ### section/+page.svelte ```svelte <script lang="ts"> import NavCards from "@orochibraru/ui/blocks/nav-cards"; import { tabs } from "./tabs.js"; </script> <NavCards {tabs} /> ``` ### section/appearance/+page.svelte ```svelte <script lang="ts"> import ThemePicker from "@orochibraru/ui/blocks/theme-picker"; </script> <ThemePicker /> ``` ### section/profile/+page.svelte ```svelte <script lang="ts"> import * as Field from "@orochibraru/ui/field"; import { Input } from "@orochibraru/ui/input"; </script> <Field.Group> <Field.Field> <Field.Label for="name">Name</Field.Label> <Input id="name" value="Alex" /> </Field.Field> <Field.Field> <Field.Label for="profile-email">Email</Field.Label> <Input id="profile-email" type="email" value="alex@example.com" /> </Field.Field> </Field.Group> ``` ### section/tabs.ts ```ts import { resolve } from "$app/paths"; import PaletteIcon from "@lucide/svelte/icons/palette"; import UserRoundIcon from "@lucide/svelte/icons/user-round"; import type { SectionTab } from "@orochibraru/ui/blocks/nav-cards"; export const tabs: SectionTab[] = [ { href: resolve("/section/profile"), label: "Profile", description: "Your name and the email you sign in with.", icon: UserRoundIcon, }, { href: resolve("/section/appearance"), label: "Appearance", description: "Light, dark, or whatever the device says.", icon: PaletteIcon, }, ]; ``` --- # Theme picker > System, light and dark as radio cards. Category: Appearance. Import from `@orochibraru/ui/blocks/theme-picker`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { toast } from "svelte-sonner"; import ThemePicker from "@orochibraru/ui/blocks/theme-picker"; </script> <ThemePicker onchange={(theme) => toast(`Theme: ${theme}.`)} /> ``` --- # Theme toggle > System, light and dark: an icon strip on desktop, three rows on phones. Category: Appearance. Import from `@orochibraru/ui/blocks/theme-toggle`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import ThemeToggle from "@orochibraru/ui/blocks/theme-toggle"; </script> <div class="flex flex-wrap items-center gap-6"> <ThemeToggle /> <ThemeToggle messages={{ system: "Système", light: "Clair", dark: "Sombre" }} /> </div> ``` --- # Top loading bar > Progress bar during SvelteKit navigations. Category: Feedback. Import from `@orochibraru/ui/blocks/top-loading-bar`. ## Usage ### +layout.svelte ```svelte <script lang="ts"> import TopLoadingBar from "@orochibraru/ui/blocks/top-loading-bar"; const { children } = $props(); </script> <TopLoadingBar /> {@render children()} ``` --- # Video player > A video player: scrubbing, chapters, subtitles, speed, fullscreen, gestures and shortcuts. Category: Media. Import from `@orochibraru/ui/blocks/video-player`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import VideoPlayer from "@orochibraru/ui/blocks/video-player"; let currentTime = $state(0); </script> <div class="grid gap-3"> <VideoPlayer src="https://upload.wikimedia.org/wikipedia/commons/transcoded/c/c0/Big_Buck_Bunny_4K.webm/Big_Buck_Bunny_4K.webm.720p.vp9.webm" poster="https://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Big_Buck_Bunny_4K.webm/1280px--Big_Buck_Bunny_4K.webm.jpg" title="Big Buck Bunny" chapters={[ { start: 0, title: "Opening" }, { start: 240, title: "Middle" }, { start: 490, title: "Credits" }, ]} bind:currentTime /> <p class="text-muted-foreground text-sm"> Space or K plays and pauses, the arrows seek and change the volume, F goes fullscreen, M mutes. On a phone, tap to show the controls and double-tap a side to seek. At {Math.floor(currentTime)} s. Big Buck Bunny © Blender Foundation, CC BY 3.0. </p> </div> ``` --- # Accordion > Stacked sections that expand one or several at a time. Category: Display. Import from `@orochibraru/ui/accordion`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Accordion from "@orochibraru/ui/accordion"; </script> <Accordion.Root type="single" value="invite"> <Accordion.Item value="invite"> <Accordion.Trigger>How do I invite team members?</Accordion.Trigger> <Accordion.Content> Open the project settings and send an invite by email. They join with the role you pick. </Accordion.Content> </Accordion.Item> <Accordion.Item value="export"> <Accordion.Trigger>Can I export my documents?</Accordion.Trigger> <Accordion.Content> Yes. Every document exports to PDF or Markdown from its menu. </Accordion.Content> </Accordion.Item> <Accordion.Item value="delete"> <Accordion.Trigger>What happens when I delete a project?</Accordion.Trigger> <Accordion.Content> It stays in the trash for 30 days, then it is gone for good with all its files. </Accordion.Content> </Accordion.Item> </Accordion.Root> ``` --- # Alert > A callout for something the reader should notice. Category: Feedback. Import from `@orochibraru/ui/alert`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import CircleAlertIcon from "@lucide/svelte/icons/circle-alert"; import InfoIcon from "@lucide/svelte/icons/info"; import * as Alert from "@orochibraru/ui/alert"; import { Button } from "@orochibraru/ui/button"; </script> <div class="grid gap-3"> <Alert.Root> <InfoIcon /> <Alert.Title>Sync runs every hour</Alert.Title> <Alert.Description> Changes made on your other devices show up within the hour. </Alert.Description> </Alert.Root> <Alert.Root variant="destructive"> <CircleAlertIcon /> <Alert.Title>Upload failed</Alert.Title> <Alert.Description> report.pdf is larger than the 25 MB limit. </Alert.Description> <Alert.Action> <Button size="sm" variant="outline">Retry</Button> </Alert.Action> </Alert.Root> </div> ``` --- # Alert dialog > A modal that asks to confirm before something drastic. Category: Overlays. Import from `@orochibraru/ui/alert-dialog`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import Trash2Icon from "@lucide/svelte/icons/trash-2"; import { toast } from "svelte-sonner"; import * as AlertDialog from "@orochibraru/ui/alert-dialog"; import { Button } from "@orochibraru/ui/button"; </script> <AlertDialog.Root> <AlertDialog.Trigger> {#snippet child({ props })} <Button {...props} variant="destructive">Delete project</Button> {/snippet} </AlertDialog.Trigger> <AlertDialog.Content> <AlertDialog.Header> <AlertDialog.Media> <Trash2Icon /> </AlertDialog.Media> <AlertDialog.Title>Delete "Website redesign"?</AlertDialog.Title> <AlertDialog.Description> The project and its 48 files are deleted for every member. This can't be undone. </AlertDialog.Description> </AlertDialog.Header> <AlertDialog.Footer> <AlertDialog.Cancel>Cancel</AlertDialog.Cancel> <AlertDialog.Action variant="destructive" onclick={() => toast.success("Project deleted.")} > Delete </AlertDialog.Action> </AlertDialog.Footer> </AlertDialog.Content> </AlertDialog.Root> ``` --- # Avatar > A user's picture, falling back to initials. Category: Display. Import from `@orochibraru/ui/avatar`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Avatar from "@orochibraru/ui/avatar"; </script> <div class="flex flex-wrap items-center gap-6"> <Avatar.Root size="sm"> <Avatar.Fallback>AM</Avatar.Fallback> </Avatar.Root> <Avatar.Root> <Avatar.Image src="https://github.com/shadcn.png" alt="shadcn" /> <Avatar.Fallback>SC</Avatar.Fallback> </Avatar.Root> <Avatar.Root size="lg"> <Avatar.Fallback>NB</Avatar.Fallback> <Avatar.Badge /> </Avatar.Root> <Avatar.Group> <Avatar.Root> <Avatar.Fallback>AL</Avatar.Fallback> </Avatar.Root> <Avatar.Root> <Avatar.Fallback>SM</Avatar.Fallback> </Avatar.Root> <Avatar.Root> <Avatar.Fallback>JD</Avatar.Fallback> </Avatar.Root> <Avatar.GroupCount>+4</Avatar.GroupCount> </Avatar.Group> </div> ``` --- # Badge > A small label for a status or a count. Category: Display. Import from `@orochibraru/ui/badge`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import CheckIcon from "@lucide/svelte/icons/check"; import { Badge } from "@orochibraru/ui/badge"; </script> <div class="flex flex-wrap items-center gap-2"> <Badge>Active</Badge> <Badge variant="secondary">Pending</Badge> <Badge variant="outline">Archived</Badge> <Badge variant="destructive">Overdue</Badge> <Badge variant="secondary" ><CheckIcon data-icon="inline-start" /> Completed</Badge > <Badge href="#">3 new messages</Badge> </div> ``` --- # Breadcrumb > The path from the root to the current page. Category: Navigation. Import from `@orochibraru/ui/breadcrumb`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Breadcrumb from "@orochibraru/ui/breadcrumb"; </script> <Breadcrumb.Root> <Breadcrumb.List> <Breadcrumb.Item> <Breadcrumb.Link href="#">Groups</Breadcrumb.Link> </Breadcrumb.Item> <Breadcrumb.Separator /> <Breadcrumb.Item> <Breadcrumb.Ellipsis /> </Breadcrumb.Item> <Breadcrumb.Separator /> <Breadcrumb.Item> <Breadcrumb.Link href="#">Lisbon trip</Breadcrumb.Link> </Breadcrumb.Item> <Breadcrumb.Separator /> <Breadcrumb.Item> <Breadcrumb.Page>Dinner at Ramiro</Breadcrumb.Page> </Breadcrumb.Item> </Breadcrumb.List> </Breadcrumb.Root> ``` --- # Button > Every button variant and size. Category: Actions. Import from `@orochibraru/ui/button`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import PlusIcon from "@lucide/svelte/icons/plus"; import { Button } from "@orochibraru/ui/button"; import { Spinner } from "@orochibraru/ui/spinner"; </script> <div class="flex flex-wrap items-center gap-2"> <Button>Primary</Button> <Button variant="secondary">Secondary</Button> <Button variant="outline">Outline</Button> <Button variant="ghost">Ghost</Button> <Button variant="destructive">Delete</Button> <Button variant="link">Link</Button> <Button disabled><Spinner /> Saving</Button> <Button size="icon" aria-label="Add"><PlusIcon /></Button> <Button href="/">As a link</Button> </div> ``` --- # Button group > Buttons joined into one control. Category: Actions. Import from `@orochibraru/ui/button-group`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left"; import ChevronRightIcon from "@lucide/svelte/icons/chevron-right"; import CopyIcon from "@lucide/svelte/icons/copy"; import * as ButtonGroup from "@orochibraru/ui/button-group"; import { Button } from "@orochibraru/ui/button"; import { Input } from "@orochibraru/ui/input"; </script> <div class="flex flex-wrap items-center gap-4"> <ButtonGroup.Root> <Button variant="outline">Day</Button> <Button variant="outline">Week</Button> <Button variant="outline">Month</Button> </ButtonGroup.Root> <ButtonGroup.Root> <Button variant="outline" size="icon" aria-label="Previous month"> <ChevronLeftIcon /> </Button> <ButtonGroup.Text>October 2026</ButtonGroup.Text> <Button variant="outline" size="icon" aria-label="Next month"> <ChevronRightIcon /> </Button> </ButtonGroup.Root> <ButtonGroup.Root> <ButtonGroup.Text>https://</ButtonGroup.Text> <Input value="acme.com" aria-label="Domain" /> <Button variant="outline" size="icon" aria-label="Copy"> <CopyIcon /> </Button> </ButtonGroup.Root> </div> ``` --- # Calendar > A month grid to pick a date. Category: Forms. Import from `@orochibraru/ui/calendar`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { type DateValue, getLocalTimeZone, today, } from "@internationalized/date"; import { Calendar } from "@orochibraru/ui/calendar"; let dueDate = $state<DateValue | undefined>(today(getLocalTimeZone())); </script> <div class="flex flex-wrap items-start gap-6"> <Calendar type="single" bind:value={dueDate} class="rounded-lg border" /> <Calendar type="single" captionLayout="dropdown" minValue={today(getLocalTimeZone())} class="rounded-lg border" /> <p class="text-muted-foreground text-sm"> Due on {dueDate?.toDate(getLocalTimeZone()).toLocaleDateString() ?? "—"} </p> </div> ``` --- # Card > A surface with a header, content and footer. Category: Display. Import from `@orochibraru/ui/card`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import EllipsisIcon from "@lucide/svelte/icons/ellipsis"; import { Button } from "@orochibraru/ui/button"; import * as Card from "@orochibraru/ui/card"; </script> <div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"> <Card.Root> <Card.Header> <Card.Title>Website redesign</Card.Title> <Card.Description>4 members · 48 files</Card.Description> <Card.Action> <Button variant="ghost" size="icon-sm" aria-label="More"> <EllipsisIcon /> </Button> </Card.Action> </Card.Header> <Card.Content> <p class="text-2xl font-semibold tabular-nums">12 of 20 tasks</p> <p class="text-muted-foreground">Due in 5 days</p> </Card.Content> <Card.Footer class="gap-2"> <Button variant="outline">Share</Button> <Button>New task</Button> </Card.Footer> </Card.Root> <Card.Root size="sm"> <Card.Header> <Card.Title>Quarterly report</Card.Title> <Card.Description>Edited 12 minutes ago</Card.Description> </Card.Header> <Card.Content class="text-muted-foreground"> Shared with 3 team members. </Card.Content> </Card.Root> </div> ``` --- # Checkbox > A checkbox, with a label through Field. Category: Forms. Import from `@orochibraru/ui/checkbox`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Checkbox } from "@orochibraru/ui/checkbox"; import * as Field from "@orochibraru/ui/field"; let notify = $state(true); </script> <Field.Group> <Field.Field orientation="horizontal"> <Checkbox id="notify" bind:checked={notify} /> <Field.Label for="notify">Email me when someone comments</Field.Label> </Field.Field> <Field.Field orientation="horizontal"> <Checkbox id="weekly" /> <Field.Content> <Field.Label for="weekly">Weekly digest</Field.Label> <Field.Description>A summary of activity every Monday.</Field.Description> </Field.Content> </Field.Field> <Field.Field orientation="horizontal"> <Checkbox id="share" indeterminate /> <Field.Label for="share">Share with everyone</Field.Label> </Field.Field> <Field.Field orientation="horizontal" data-disabled> <Checkbox id="archived" disabled /> <Field.Label for="archived">Include archived projects</Field.Label> </Field.Field> </Field.Group> ``` --- # Command > A searchable list of commands, also as a palette. Category: Overlays. Import from `@orochibraru/ui/command`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import CalendarIcon from "@lucide/svelte/icons/calendar"; import FileTextIcon from "@lucide/svelte/icons/file-text"; import FolderIcon from "@lucide/svelte/icons/folder"; import PlusIcon from "@lucide/svelte/icons/plus"; import SearchIcon from "@lucide/svelte/icons/search"; import SettingsIcon from "@lucide/svelte/icons/settings"; import { Button } from "@orochibraru/ui/button"; import * as Command from "@orochibraru/ui/command"; let open = $state(false); </script> <div class="grid gap-4"> <Command.Root class="rounded-lg border"> <Command.Input placeholder="Search projects and documents..." /> <Command.List> <Command.Empty>No results.</Command.Empty> <Command.Group heading="Projects"> <Command.Item><FolderIcon /> Website redesign</Command.Item> <Command.Item><FolderIcon /> Mobile app</Command.Item> </Command.Group> <Command.Separator /> <Command.Group heading="Documents"> <Command.Item><FileTextIcon /> Quarterly report</Command.Item> <Command.Item><FileTextIcon /> Meeting notes</Command.Item> </Command.Group> </Command.List> </Command.Root> <Button variant="outline" class="w-fit" onclick={() => (open = true)}> <SearchIcon /> Open command palette </Button> <Command.Dialog bind:open> <Command.Input placeholder="Type a command..." /> <Command.List> <Command.Empty>No results.</Command.Empty> <Command.Group heading="Actions"> <Command.Item onSelect={() => (open = false)}> <PlusIcon /> New document <Command.Shortcut>⌘N</Command.Shortcut> </Command.Item> <Command.Item onSelect={() => (open = false)}> <CalendarIcon /> Schedule a meeting </Command.Item> <Command.Item onSelect={() => (open = false)}> <SettingsIcon /> Settings <Command.Shortcut>⌘,</Command.Shortcut> </Command.Item> </Command.Group> </Command.List> </Command.Dialog> </div> ``` --- # Context menu > A menu on right click or long press. Category: Overlays. Import from `@orochibraru/ui/context-menu`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import CopyIcon from "@lucide/svelte/icons/copy"; import PencilIcon from "@lucide/svelte/icons/pencil"; import Trash2Icon from "@lucide/svelte/icons/trash-2"; import * as ContextMenu from "@orochibraru/ui/context-menu"; let showArchived = $state(false); let sort = $state("date"); </script> <ContextMenu.Root> <ContextMenu.Trigger class="text-muted-foreground flex h-36 w-full items-center justify-center rounded-lg border border-dashed text-sm" > Right-click (or long-press) the file list </ContextMenu.Trigger> <ContextMenu.Content class="w-56"> <ContextMenu.Item> <PencilIcon /> Edit <ContextMenu.Shortcut>⌘E</ContextMenu.Shortcut> </ContextMenu.Item> <ContextMenu.Item><CopyIcon /> Duplicate</ContextMenu.Item> <ContextMenu.Sub> <ContextMenu.SubTrigger>Move to project</ContextMenu.SubTrigger> <ContextMenu.SubContent> <ContextMenu.Item>Website redesign</ContextMenu.Item> <ContextMenu.Item>Mobile app</ContextMenu.Item> </ContextMenu.SubContent> </ContextMenu.Sub> <ContextMenu.Separator /> <ContextMenu.CheckboxItem bind:checked={showArchived}> Show archived </ContextMenu.CheckboxItem> <ContextMenu.Separator /> <ContextMenu.Group> <ContextMenu.GroupHeading>Sort by</ContextMenu.GroupHeading> <ContextMenu.RadioGroup bind:value={sort}> <ContextMenu.RadioItem value="date">Date</ContextMenu.RadioItem> <ContextMenu.RadioItem value="name">Name</ContextMenu.RadioItem> </ContextMenu.RadioGroup> </ContextMenu.Group> <ContextMenu.Separator /> <ContextMenu.Item variant="destructive" ><Trash2Icon /> Delete</ContextMenu.Item > </ContextMenu.Content> </ContextMenu.Root> ``` --- # Dialog > A modal window over the page. Category: Overlays. Import from `@orochibraru/ui/dialog`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { toast } from "svelte-sonner"; import { Button } from "@orochibraru/ui/button"; import * as Dialog from "@orochibraru/ui/dialog"; import * as Field from "@orochibraru/ui/field"; import { Input } from "@orochibraru/ui/input"; let open = $state(false); function save(event: SubmitEvent) { event.preventDefault(); open = false; toast.success("Project renamed."); } </script> <Dialog.Root bind:open> <Dialog.Trigger> {#snippet child({ props })} <Button {...props} variant="outline">Rename project</Button> {/snippet} </Dialog.Trigger> <Dialog.Content> <form class="grid gap-6" onsubmit={save}> <Dialog.Header> <Dialog.Title>Rename project</Dialog.Title> <Dialog.Description> Every member sees the new name right away. </Dialog.Description> </Dialog.Header> <Field.Field> <Field.Label for="project-name">Name</Field.Label> <Input id="project-name" value="Website redesign" /> </Field.Field> <Dialog.Footer> <Dialog.Close> {#snippet child({ props })} <Button {...props} variant="outline">Cancel</Button> {/snippet} </Dialog.Close> <Button type="submit">Save</Button> </Dialog.Footer> </form> </Dialog.Content> </Dialog.Root> ``` --- # Drawer > A panel sliding up from the bottom, made for phones. Category: Overlays. Import from `@orochibraru/ui/drawer`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { toast } from "svelte-sonner"; import { Button } from "@orochibraru/ui/button"; import * as Drawer from "@orochibraru/ui/drawer"; const members = [ { name: "Alex", role: "Owner" }, { name: "Sam", role: "Editor" }, { name: "Jordan", role: "Viewer" }, ]; let open = $state(false); function save() { open = false; toast.success("Access updated."); } </script> <Drawer.Root bind:open> <Drawer.Trigger> {#snippet child({ props })} <Button {...props} variant="outline">Manage access</Button> {/snippet} </Drawer.Trigger> <Drawer.Content> <Drawer.Header> <Drawer.Title>Manage access</Drawer.Title> <Drawer.Description>Website redesign · 3 members</Drawer.Description> </Drawer.Header> <ul class="divide-y px-4"> {#each members as member (member.name)} <li class="flex justify-between py-3"> <span>{member.name}</span> <span class="text-muted-foreground">{member.role}</span> </li> {/each} </ul> <Drawer.Footer> <Button onclick={save}>Save</Button> <Drawer.Close> {#snippet child({ props })} <Button {...props} variant="outline">Cancel</Button> {/snippet} </Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Root> ``` --- # Dropdown menu > A menu opened from a button. Category: Overlays. Import from `@orochibraru/ui/dropdown-menu`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import EllipsisIcon from "@lucide/svelte/icons/ellipsis"; import PencilIcon from "@lucide/svelte/icons/pencil"; import Share2Icon from "@lucide/svelte/icons/share-2"; import Trash2Icon from "@lucide/svelte/icons/trash-2"; import { Button } from "@orochibraru/ui/button"; import * as DropdownMenu from "@orochibraru/ui/dropdown-menu"; let notify = $state(true); let visibility = $state("private"); </script> <DropdownMenu.Root> <DropdownMenu.Trigger> {#snippet child({ props })} <Button {...props} variant="outline"> Website redesign <EllipsisIcon /> </Button> {/snippet} </DropdownMenu.Trigger> <DropdownMenu.Content class="w-56" align="start"> <DropdownMenu.Group> <DropdownMenu.Label>Project</DropdownMenu.Label> <DropdownMenu.Item> <PencilIcon /> Edit <DropdownMenu.Shortcut>⌘E</DropdownMenu.Shortcut> </DropdownMenu.Item> <DropdownMenu.Item><Share2Icon /> Share</DropdownMenu.Item> </DropdownMenu.Group> <DropdownMenu.Separator /> <DropdownMenu.CheckboxItem bind:checked={notify}> Notifications </DropdownMenu.CheckboxItem> <DropdownMenu.Sub> <DropdownMenu.SubTrigger>Visibility</DropdownMenu.SubTrigger> <DropdownMenu.SubContent> <DropdownMenu.RadioGroup bind:value={visibility}> <DropdownMenu.RadioItem value="private" >Private</DropdownMenu.RadioItem > <DropdownMenu.RadioItem value="team">Team</DropdownMenu.RadioItem> </DropdownMenu.RadioGroup> </DropdownMenu.SubContent> </DropdownMenu.Sub> <DropdownMenu.Separator /> <DropdownMenu.Item variant="destructive"> <Trash2Icon /> Delete </DropdownMenu.Item> </DropdownMenu.Content> </DropdownMenu.Root> ``` --- # Empty > The parts of an empty state, to compose your own. Category: Feedback. Import from `@orochibraru/ui/empty`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import FileTextIcon from "@lucide/svelte/icons/file-text"; import PlusIcon from "@lucide/svelte/icons/plus"; import { Button } from "@orochibraru/ui/button"; import * as Empty from "@orochibraru/ui/empty"; </script> <Empty.Root class="border"> <Empty.Header> <Empty.Media variant="icon"> <FileTextIcon /> </Empty.Media> <Empty.Title>No documents yet</Empty.Title> <Empty.Description> Create the first document of this project to get started. </Empty.Description> </Empty.Header> <Empty.Content> <Button><PlusIcon /> New document</Button> </Empty.Content> </Empty.Root> ``` --- # Field > Label, control, description and error, laid out together. Category: Forms. Import from `@orochibraru/ui/field`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Field from "@orochibraru/ui/field"; import { Input } from "@orochibraru/ui/input"; import { Switch } from "@orochibraru/ui/switch"; let estimate = $state(""); const invalid = $derived(estimate !== "" && Number(estimate) <= 0); </script> <Field.Set> <Field.Legend>New task</Field.Legend> <Field.Description>Visible to everyone in the project.</Field.Description> <Field.Group class="grid gap-6 md:grid-cols-2"> <Field.Field> <Field.Label for="task-title">Title</Field.Label> <Input id="task-title" placeholder="Write the release notes" /> </Field.Field> <Field.Field data-invalid={invalid}> <Field.Label for="task-estimate">Estimate</Field.Label> <Input id="task-estimate" type="number" placeholder="0" aria-invalid={invalid} bind:value={estimate} /> {#if invalid} <Field.Error>The estimate must be greater than zero.</Field.Error> {:else} <Field.Description>In hours.</Field.Description> {/if} </Field.Field> <Field.Field orientation="horizontal"> <Switch id="task-recurring" /> <Field.Label for="task-recurring">Repeat every week</Field.Label> </Field.Field> </Field.Group> </Field.Set> ``` --- # Input > A text input; password managers stay off unless asked. Category: Forms. Import from `@orochibraru/ui/input`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Input } from "@orochibraru/ui/input"; import { Label } from "@orochibraru/ui/label"; </script> <div class="grid gap-4 md:grid-cols-2"> <div class="flex flex-col gap-2"> <Label for="account-email">Email</Label> <Input id="account-email" type="email" autocomplete="email" placeholder="you@example.com" /> </div> <div class="flex flex-col gap-2"> <Label for="service-search">Search services</Label> <Input id="service-search" type="search" placeholder="postgres, redis…" /> </div> </div> ``` --- # Input group > An input with addons: icons, text or buttons. Category: Forms. Import from `@orochibraru/ui/input-group`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import CopyIcon from "@lucide/svelte/icons/copy"; import SearchIcon from "@lucide/svelte/icons/search"; import * as InputGroup from "@orochibraru/ui/input-group"; </script> <div class="grid gap-4 md:grid-cols-3"> <InputGroup.Root> <InputGroup.Addon> <SearchIcon /> </InputGroup.Addon> <InputGroup.Input placeholder="Search files" /> </InputGroup.Root> <InputGroup.Root> <InputGroup.Input type="number" placeholder="25" /> <InputGroup.Addon align="inline-end"> <InputGroup.Text>MB</InputGroup.Text> </InputGroup.Addon> </InputGroup.Root> <InputGroup.Root> <InputGroup.Input readonly value="https://acme.com/s/report" /> <InputGroup.Addon align="inline-end"> <InputGroup.Button size="icon-xs" aria-label="Copy URL"> <CopyIcon /> </InputGroup.Button> </InputGroup.Addon> </InputGroup.Root> </div> ``` --- # Input otp > One box per digit for a one-time code. Category: Forms. Import from `@orochibraru/ui/input-otp`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as InputOTP from "@orochibraru/ui/input-otp"; let code = $state(""); </script> <div class="flex flex-col gap-3"> <InputOTP.Root maxlength={6} bind:value={code}> {#snippet children({ cells })} <InputOTP.Group> {#each cells.slice(0, 3) as cell (cell)} <InputOTP.Slot {cell} /> {/each} </InputOTP.Group> <InputOTP.Separator /> <InputOTP.Group> {#each cells.slice(3, 6) as cell (cell)} <InputOTP.Slot {cell} /> {/each} </InputOTP.Group> {/snippet} </InputOTP.Root> <p class="text-muted-foreground text-sm"> {code.length === 6 ? `Verifying ${code}…` : "Enter the code we emailed you."} </p> </div> ``` --- # Label > A label for a form control. Category: Forms. Import from `@orochibraru/ui/label`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Checkbox } from "@orochibraru/ui/checkbox"; import { Input } from "@orochibraru/ui/input"; import { Label } from "@orochibraru/ui/label"; </script> <div class="flex flex-wrap items-end gap-6"> <div class="flex flex-col gap-2"> <Label for="project-name">Project name</Label> <Input id="project-name" placeholder="Marketing site" /> </div> <div class="flex h-9 items-center gap-2"> <Checkbox id="project-notify" /> <Label for="project-notify">Notify team members</Label> </div> </div> ``` --- # Menubar > A desktop-style bar of menus. Category: Navigation. Import from `@orochibraru/ui/menubar`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Menubar from "@orochibraru/ui/menubar"; let showArchived = $state(false); let sort = $state("name"); </script> <Menubar.Root> <Menubar.Menu> <Menubar.Trigger>File</Menubar.Trigger> <Menubar.Content> <Menubar.Item >New file <Menubar.Shortcut>⌘N</Menubar.Shortcut></Menubar.Item > <Menubar.Item >Duplicate <Menubar.Shortcut>⌘D</Menubar.Shortcut></Menubar.Item > <Menubar.Sub> <Menubar.SubTrigger>Export</Menubar.SubTrigger> <Menubar.SubContent> <Menubar.Item>PDF</Menubar.Item> <Menubar.Item>Markdown</Menubar.Item> </Menubar.SubContent> </Menubar.Sub> <Menubar.Separator /> <Menubar.Item variant="destructive">Delete</Menubar.Item> </Menubar.Content> </Menubar.Menu> <Menubar.Menu> <Menubar.Trigger>View</Menubar.Trigger> <Menubar.Content> <Menubar.CheckboxItem bind:checked={showArchived}> Show hidden files </Menubar.CheckboxItem> <Menubar.Separator /> <Menubar.Group> <Menubar.GroupHeading>Sort by</Menubar.GroupHeading> <Menubar.RadioGroup bind:value={sort}> <Menubar.RadioItem value="name">Name</Menubar.RadioItem> <Menubar.RadioItem value="modified">Date modified</Menubar.RadioItem> <Menubar.RadioItem value="size">Size</Menubar.RadioItem> </Menubar.RadioGroup> </Menubar.Group> </Menubar.Content> </Menubar.Menu> </Menubar.Root> ``` --- # Pagination > Links to the pages of a long list. Category: Navigation. Import from `@orochibraru/ui/pagination`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Pagination from "@orochibraru/ui/pagination"; let page = $state(1); </script> <Pagination.Root count={240} perPage={20} bind:page> {#snippet children({ pages, currentPage })} <Pagination.Content> <Pagination.Item> <Pagination.Previous /> </Pagination.Item> {#each pages as p (p.key)} <Pagination.Item> {#if p.type === "ellipsis"} <Pagination.Ellipsis /> {:else} <Pagination.Link page={p} isActive={currentPage === p.value} /> {/if} </Pagination.Item> {/each} <Pagination.Item> <Pagination.Next /> </Pagination.Item> </Pagination.Content> {/snippet} </Pagination.Root> ``` --- # Popover > Floating content anchored to a trigger. Category: Overlays. Import from `@orochibraru/ui/popover`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { buttonVariants } from "@orochibraru/ui/button"; import { Input } from "@orochibraru/ui/input"; import { Label } from "@orochibraru/ui/label"; import * as Popover from "@orochibraru/ui/popover"; </script> <Popover.Root> <Popover.Trigger class={buttonVariants({ variant: "outline" })}> Set storage limit </Popover.Trigger> <Popover.Content> <Popover.Header> <Popover.Title>Storage limit</Popover.Title> <Popover.Description> We'll warn the team when the workspace gets close. </Popover.Description> </Popover.Header> <div class="flex flex-col gap-2"> <Label for="storage-limit">Limit (GB)</Label> <Input id="storage-limit" type="number" value="100" /> </div> </Popover.Content> </Popover.Root> ``` --- # Progress > How far along something is. Category: Feedback. Import from `@orochibraru/ui/progress`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Progress } from "@orochibraru/ui/progress"; const used = 62; const total = 100; </script> <div class="flex flex-col gap-2"> <div class="flex justify-between text-sm"> <span class="font-medium">Storage</span> <span class="text-muted-foreground">{used} GB of {total} GB</span> </div> <Progress value={used} max={total} /> </div> ``` --- # Radio group > Pick one of a few options. Category: Forms. Import from `@orochibraru/ui/radio-group`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Label } from "@orochibraru/ui/label"; import * as RadioGroup from "@orochibraru/ui/radio-group"; let notify = $state("all"); </script> <div class="flex flex-col gap-4"> <RadioGroup.Root bind:value={notify}> <div class="flex items-center gap-2"> <RadioGroup.Item value="all" id="notify-all" /> <Label for="notify-all">All new messages</Label> </div> <div class="flex items-center gap-2"> <RadioGroup.Item value="mentions" id="notify-mentions" /> <Label for="notify-mentions">Mentions only</Label> </div> <div class="flex items-center gap-2"> <RadioGroup.Item value="none" id="notify-none" /> <Label for="notify-none">Nothing</Label> </div> </RadioGroup.Root> <p class="text-muted-foreground text-sm">Selected: {notify}</p> </div> ``` --- # Range calendar > A month grid to pick a start and an end date. Category: Forms. Import from `@orochibraru/ui/range-calendar`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { type DateValue, getLocalTimeZone, today, } from "@internationalized/date"; import { RangeCalendar } from "@orochibraru/ui/range-calendar"; const start = today(getLocalTimeZone()); let value = $state<{ start: DateValue | undefined; end: DateValue | undefined; }>({ start, end: start.add({ days: 6 }), }); </script> <div class="flex flex-wrap items-start gap-6"> <RangeCalendar bind:value numberOfMonths={2} class="rounded-md border" /> <p class="text-muted-foreground text-sm"> Trip: {value.start?.toString() ?? "…"} → {value.end?.toString() ?? "…"} </p> </div> ``` --- # Resizable > Panes split by a draggable handle. Category: Layout. Import from `@orochibraru/ui/resizable`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Resizable from "@orochibraru/ui/resizable"; </script> <div class="h-72 rounded-md border"> <Resizable.PaneGroup direction="horizontal"> <Resizable.Pane defaultSize={25} minSize={15}> <div class="flex h-full items-center justify-center p-4"> <span class="font-medium">Services</span> </div> </Resizable.Pane> <Resizable.Handle withHandle /> <Resizable.Pane defaultSize={75}> <Resizable.PaneGroup direction="vertical"> <Resizable.Pane defaultSize={60}> <div class="flex h-full items-center justify-center p-4"> <span class="font-medium">Details</span> </div> </Resizable.Pane> <Resizable.Handle withHandle /> <Resizable.Pane defaultSize={40}> <div class="flex h-full items-center justify-center p-4"> <span class="font-medium">Logs</span> </div> </Resizable.Pane> </Resizable.PaneGroup> </Resizable.Pane> </Resizable.PaneGroup> </div> ``` --- # Scroll area > A scrolling box with a styled scrollbar. Category: Layout. Import from `@orochibraru/ui/scroll-area`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { ScrollArea } from "@orochibraru/ui/scroll-area"; import { Separator } from "@orochibraru/ui/separator"; const files = Array.from({ length: 30 }, (_, i) => ({ id: i + 1, name: ["Notes", "Roadmap", "Report", "Brief", "Draft"][i % 5], size: ((i * 37) % 120) + 8, })); </script> <ScrollArea class="h-72 rounded-md border"> <div class="p-4"> <h4 class="mb-4 text-sm font-medium">Files</h4> {#each files as file (file.id)} <div class="flex justify-between text-sm"> <span>{file.name}</span> <span class="text-muted-foreground">{file.size} KB</span> </div> <Separator class="my-2" /> {/each} </div> </ScrollArea> ``` --- # Select > Pick one option from a list. Category: Forms. Import from `@orochibraru/ui/select`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Select from "@orochibraru/ui/select"; const roles = [ { value: "viewer", label: "Viewer" }, { value: "editor", label: "Editor" }, { value: "admin", label: "Admin" }, { value: "owner", label: "Owner" }, ]; let role = $state(""); const label = $derived( roles.find((r) => r.value === role)?.label ?? "Pick a role", ); </script> <Select.Root type="single" bind:value={role}> <Select.Trigger class="w-56">{label}</Select.Trigger> <Select.Content> <Select.Group> <Select.Label>Roles</Select.Label> {#each roles as r (r.value)} <Select.Item value={r.value} label={r.label}>{r.label}</Select.Item> {/each} </Select.Group> </Select.Content> </Select.Root> ``` --- # Separator > A line between groups of content. Category: Layout. Import from `@orochibraru/ui/separator`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Separator } from "@orochibraru/ui/separator"; </script> <div class="grid gap-4"> <div class="grid gap-1"> <h4 class="text-sm font-medium">Marketing site</h4> <p class="text-sm text-muted-foreground">4 members, 23 tasks</p> </div> <Separator /> <div class="flex h-5 items-center gap-4 text-sm"> <a href="#overview">Overview</a> <Separator orientation="vertical" /> <a href="#tasks">Tasks</a> <Separator orientation="vertical" /> <a href="#settings">Settings</a> </div> </div> ``` --- # Sheet > A panel sliding in from a side of the screen. Category: Overlays. Import from `@orochibraru/ui/sheet`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Button } from "@orochibraru/ui/button"; import * as Field from "@orochibraru/ui/field"; import { Input } from "@orochibraru/ui/input"; import * as Sheet from "@orochibraru/ui/sheet"; let open = $state(false); let name = $state("Alex Martin"); </script> <Sheet.Root bind:open> <Sheet.Trigger> {#snippet child({ props })} <Button {...props} variant="outline">Edit profile</Button> {/snippet} </Sheet.Trigger> <Sheet.Content> <Sheet.Header> <Sheet.Title>Edit profile</Sheet.Title> <Sheet.Description>Changes are visible to your team.</Sheet.Description> </Sheet.Header> <Field.Group class="px-4"> <Field.Field> <Field.Label for="profile-name">Name</Field.Label> <Input id="profile-name" bind:value={name} /> </Field.Field> <Field.Field> <Field.Label for="profile-email">Email</Field.Label> <Input id="profile-email" value="alex@example.com" /> </Field.Field> </Field.Group> <Sheet.Footer> <Button onclick={() => (open = false)}>Save</Button> <Sheet.Close> {#snippet child({ props })} <Button {...props} variant="outline">Cancel</Button> {/snippet} </Sheet.Close> </Sheet.Footer> </Sheet.Content> </Sheet.Root> ``` --- # Sidebar > The app sidebar: groups, menus, collapsing to icons. Category: Navigation. Import from `@orochibraru/ui/sidebar`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import BoxIcon from "@lucide/svelte/icons/box"; import FolderIcon from "@lucide/svelte/icons/folder"; import HouseIcon from "@lucide/svelte/icons/house"; import SettingsIcon from "@lucide/svelte/icons/settings"; import UsersIcon from "@lucide/svelte/icons/users"; import * as Sidebar from "@orochibraru/ui/sidebar"; const items = [ { title: "Overview", href: "#overview", icon: HouseIcon }, { title: "Projects", href: "#projects", icon: FolderIcon, badge: 12 }, { title: "Members", href: "#members", icon: UsersIcon }, { title: "Settings", href: "#settings", icon: SettingsIcon }, ]; let active = $state("Projects"); </script> <Sidebar.Provider class="h-96 min-h-0 overflow-hidden rounded-lg border"> <Sidebar.Root collapsible="none" class="border-e"> <Sidebar.Header> <div class="flex items-center gap-2 p-2 font-semibold"> <BoxIcon class="size-4" /> Acme </div> </Sidebar.Header> <Sidebar.Content> <Sidebar.Group> <Sidebar.GroupLabel>Workspace</Sidebar.GroupLabel> <Sidebar.GroupContent> <Sidebar.Menu> {#each items as item (item.title)} <Sidebar.MenuItem> <Sidebar.MenuButton isActive={active === item.title}> {#snippet child({ props })} <a href={item.href} {...props} onclick={() => (active = item.title)} > <item.icon /> <span>{item.title}</span> </a> {/snippet} </Sidebar.MenuButton> {#if item.badge} <Sidebar.MenuBadge>{item.badge}</Sidebar.MenuBadge> {/if} </Sidebar.MenuItem> {/each} </Sidebar.Menu> </Sidebar.GroupContent> </Sidebar.Group> </Sidebar.Content> <Sidebar.Footer> <div class="p-2 text-sm text-muted-foreground">alex@example.com</div> </Sidebar.Footer> </Sidebar.Root> <div class="flex-1 p-6"> <h3 class="font-semibold">{active}</h3> <p class="text-sm text-muted-foreground">Page content goes here.</p> </div> </Sidebar.Provider> ``` --- # Skeleton > Placeholder shapes while content loads. Category: Feedback. Import from `@orochibraru/ui/skeleton`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Skeleton } from "@orochibraru/ui/skeleton"; </script> <div class="grid gap-4"> {#each [1, 2, 3] as row (row)} <div class="flex items-center gap-3"> <Skeleton class="size-10 shrink-0 rounded-full" /> <div class="grid flex-1 gap-2"> <Skeleton class="h-4 w-2/3" /> <Skeleton class="h-4 w-1/3" /> </div> <Skeleton class="h-4 w-16" /> </div> {/each} </div> ``` --- # Slider > Pick a value or a range by dragging. Category: Forms. Import from `@orochibraru/ui/slider`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Slider } from "@orochibraru/ui/slider"; let limit = $state(450); let range = $state([20, 80]); </script> <div class="grid gap-8"> <div class="grid gap-3"> <div class="flex justify-between text-sm"> <span>Storage limit</span> <span class="tabular">{limit} GB</span> </div> <Slider type="single" bind:value={limit} max={1000} step={10} /> </div> <div class="grid gap-3"> <div class="flex justify-between text-sm"> <span>File size between</span> <span class="tabular">{range[0]} MB and {range[1]} MB</span> </div> <Slider type="multiple" bind:value={range} max={200} step={5} /> </div> <Slider type="single" value={30} disabled /> </div> ``` --- # Sonner > Toasts, styled by the theme. Category: Feedback. Import from `@orochibraru/ui/sonner`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { toast } from "svelte-sonner"; import { Button } from "@orochibraru/ui/button"; function syncFiles() { return new Promise((resolve) => setTimeout(resolve, 1500)); } </script> <div class="flex flex-wrap items-center gap-2"> <Button variant="outline" onclick={() => toast("File uploaded.")}> Default </Button> <Button variant="outline" onclick={() => toast.success("Invite sent.", { description: "Sam will get an email shortly.", })} > Success </Button> <Button variant="outline" onclick={() => toast.error("Couldn't reach the server.")} > Error </Button> <Button variant="outline" onclick={() => toast.promise(syncFiles(), { loading: "Syncing files...", success: "Files up to date.", error: "Sync failed.", })} > Promise </Button> </div> ``` --- # Spinner > An indeterminate loading indicator. Category: Feedback. Import from `@orochibraru/ui/spinner`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Button } from "@orochibraru/ui/button"; import { Spinner } from "@orochibraru/ui/spinner"; </script> <div class="flex flex-wrap items-center gap-6"> <Spinner /> <Spinner class="size-6" /> <Spinner class="size-8 text-primary" /> <div class="flex items-center gap-2 text-sm text-muted-foreground"> <Spinner /> Syncing accounts </div> <Button disabled><Spinner /> Importing</Button> </div> ``` --- # Switch > An on or off toggle. Category: Forms. Import from `@orochibraru/ui/switch`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Label } from "@orochibraru/ui/label"; import { Switch } from "@orochibraru/ui/switch"; let emails = $state(true); let digest = $state(false); </script> <div class="grid gap-4"> <div class="flex items-center gap-3"> <Switch id="emails" bind:checked={emails} /> <Label for="emails">Email notifications</Label> </div> <div class="flex items-center gap-3"> <Switch id="digest" size="sm" bind:checked={digest} /> <Label for="digest">Weekly digest {digest ? "on" : "off"}</Label> </div> <div class="flex items-center gap-3"> <Switch id="sso" disabled /> <Label for="sso">Single sign-on (Pro plan)</Label> </div> </div> ``` --- # Table > Rows and columns of data. Category: Display. Import from `@orochibraru/ui/table`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Table from "@orochibraru/ui/table"; const files = [ { id: 1, name: "Roadmap.pdf", owner: "Alex", date: "Mar 12", size: 2.4 }, { id: 2, name: "Brand assets.zip", owner: "Sam", date: "Mar 14", size: 118, }, { id: 3, name: "Notes.md", owner: "Lou", date: "Mar 15", size: 0.2 }, ]; const total = files.reduce((sum, file) => sum + file.size, 0); </script> <Table.Root> <Table.Caption>Shared files, updated in March.</Table.Caption> <Table.Header> <Table.Row> <Table.Head>File</Table.Head> <Table.Head>Owner</Table.Head> <Table.Head>Modified</Table.Head> <Table.Head class="text-right">Size</Table.Head> </Table.Row> </Table.Header> <Table.Body> {#each files as file (file.id)} <Table.Row> <Table.Cell class="font-medium">{file.name}</Table.Cell> <Table.Cell>{file.owner}</Table.Cell> <Table.Cell>{file.date}</Table.Cell> <Table.Cell class="text-right tabular"> {file.size.toFixed(1)} MB </Table.Cell> </Table.Row> {/each} </Table.Body> <Table.Footer> <Table.Row> <Table.Cell colspan={3}>Total</Table.Cell> <Table.Cell class="text-right tabular">{total.toFixed(1)} MB</Table.Cell> </Table.Row> </Table.Footer> </Table.Root> ``` --- # Tabs > In-page tabs that switch panels without navigating. Category: Navigation. Import from `@orochibraru/ui/tabs`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Tabs from "@orochibraru/ui/tabs"; let tab = $state("projects"); </script> <div class="grid gap-8"> <Tabs.Root bind:value={tab}> <Tabs.List> <Tabs.Trigger value="projects">Projects</Tabs.Trigger> <Tabs.Trigger value="activity">Activity</Tabs.Trigger> <Tabs.Trigger value="members">Members</Tabs.Trigger> </Tabs.List> <Tabs.Content value="projects" class="text-sm"> 12 projects, 4 archived. </Tabs.Content> <Tabs.Content value="activity" class="text-sm"> Sam commented on the roadmap. </Tabs.Content> <Tabs.Content value="members" class="text-sm"> Alex, Sam, Lou and you. </Tabs.Content> </Tabs.Root> <Tabs.Root value="week"> <Tabs.List variant="line"> <Tabs.Trigger value="week">This week</Tabs.Trigger> <Tabs.Trigger value="month">This month</Tabs.Trigger> <Tabs.Trigger value="year" disabled>This year</Tabs.Trigger> </Tabs.List> </Tabs.Root> </div> ``` --- # Textarea > A multi-line text input. Category: Forms. Import from `@orochibraru/ui/textarea`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import * as Field from "@orochibraru/ui/field"; import { Textarea } from "@orochibraru/ui/textarea"; let message = $state(""); </script> <Field.Group> <Field.Field> <Field.Label for="message">Message</Field.Label> <Textarea id="message" bind:value={message} maxlength={200} placeholder="Write a message..." /> <Field.Description>{message.length} / 200 characters</Field.Description> </Field.Field> <Field.Field> <Field.Label for="archived-message">Archived message</Field.Label> <Textarea id="archived-message" value="This thread is closed." disabled /> </Field.Field> </Field.Group> ``` --- # Toggle > A button that stays pressed. Category: Actions. Import from `@orochibraru/ui/toggle`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import BellIcon from "@lucide/svelte/icons/bell"; import StarIcon from "@lucide/svelte/icons/star"; import { Toggle } from "@orochibraru/ui/toggle"; let starred = $state(false); </script> <div class="flex flex-wrap items-center gap-2"> <Toggle bind:pressed={starred} aria-label="Star this group"> <StarIcon class={starred ? "fill-current" : ""} /> </Toggle> <Toggle variant="outline" aria-label="Mute notifications"> <BellIcon /> Mute </Toggle> <Toggle size="sm" variant="outline">Recurring only</Toggle> <Toggle size="lg" pressed>Unpaid</Toggle> <Toggle disabled>Archived</Toggle> </div> ``` --- # Toggle group > Toggles where one or several stay pressed. Category: Actions. Import from `@orochibraru/ui/toggle-group`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import CalendarIcon from "@lucide/svelte/icons/calendar"; import LayoutGridIcon from "@lucide/svelte/icons/layout-grid"; import ListIcon from "@lucide/svelte/icons/list"; import * as ToggleGroup from "@orochibraru/ui/toggle-group"; let view = $state("list"); let statuses = $state(["pending"]); </script> <div class="flex flex-wrap items-center gap-4"> <ToggleGroup.Root type="single" bind:value={view} variant="outline"> <ToggleGroup.Item value="list" aria-label="List view"> <ListIcon /> </ToggleGroup.Item> <ToggleGroup.Item value="grid" aria-label="Grid view"> <LayoutGridIcon /> </ToggleGroup.Item> <ToggleGroup.Item value="calendar" aria-label="Calendar view"> <CalendarIcon /> </ToggleGroup.Item> </ToggleGroup.Root> <ToggleGroup.Root type="multiple" bind:value={statuses} spacing={1}> <ToggleGroup.Item value="paid">Paid</ToggleGroup.Item> <ToggleGroup.Item value="pending">Pending</ToggleGroup.Item> <ToggleGroup.Item value="overdue">Overdue</ToggleGroup.Item> </ToggleGroup.Root> </div> ``` --- # Tooltip > A hint on hover or focus. Category: Overlays. Import from `@orochibraru/ui/tooltip`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import DownloadIcon from "@lucide/svelte/icons/download"; import PencilIcon from "@lucide/svelte/icons/pencil"; import Trash2Icon from "@lucide/svelte/icons/trash-2"; import { Button } from "@orochibraru/ui/button"; import * as Tooltip from "@orochibraru/ui/tooltip"; </script> <div class="flex flex-wrap items-center gap-2"> <Tooltip.Root> <Tooltip.Trigger> {#snippet child({ props })} <Button {...props} variant="outline" size="icon" aria-label="Edit"> <PencilIcon /> </Button> {/snippet} </Tooltip.Trigger> <Tooltip.Content>Edit file</Tooltip.Content> </Tooltip.Root> <Tooltip.Root> <Tooltip.Trigger> {#snippet child({ props })} <Button {...props} variant="outline" size="icon" aria-label="Export"> <DownloadIcon /> </Button> {/snippet} </Tooltip.Trigger> <Tooltip.Content side="bottom">Export as CSV</Tooltip.Content> </Tooltip.Root> <Tooltip.Root> <Tooltip.Trigger> {#snippet child({ props })} <Button {...props} variant="outline" size="icon" aria-label="Delete"> <Trash2Icon /> </Button> {/snippet} </Tooltip.Trigger> <Tooltip.Content side="right">Delete for everyone</Tooltip.Content> </Tooltip.Root> </div> ``` --- # Appearance > Apply a saved accent, theme, font and corners to <html>. Category: Appearance. Import from `@orochibraru/ui/appearance`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { applyAppearance } from "@orochibraru/ui/appearance"; import { Button } from "@orochibraru/ui/button"; let corners = $state("rounded"); function toggleCorners() { corners = corners === "rounded" ? "square" : "rounded"; applyAppearance( document.documentElement, { accent: document.documentElement.dataset.accent ?? "blue", theme: "system", darkVariant: "default", font: "default", corners, }, matchMedia("(prefers-color-scheme: dark)").matches, ); } </script> <Button variant="outline" onclick={toggleCorners}> Corners: {corners} </Button> ``` ### +layout.svelte ```svelte <script lang="ts"> import { ModeWatcher } from "mode-watcher"; import { appearanceBootScript } from "@orochibraru/ui/appearance"; const { children, data } = $props(); </script> <svelte:head> {@html appearanceBootScript(data.appearance)} </svelte:head> <ModeWatcher defaultMode={data.appearance.theme} /> {@render children()} ``` --- # Haptics > A short vibration on tap, success or error. Category: Feedback. Import from `@orochibraru/ui/haptics`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Button } from "@orochibraru/ui/button"; import { haptic } from "@orochibraru/ui/haptics"; </script> <div class="flex flex-wrap gap-2"> <Button variant="outline" onclick={() => haptic("tap")}>Tap</Button> <Button variant="outline" onclick={() => haptic("success")}>Success</Button> <Button variant="outline" onclick={() => haptic("error")}>Error</Button> </div> ``` --- # Page title > One page title for the app bar, the header and the tab. Category: Layout. Import from `@orochibraru/ui/page-title`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { usePageTitle } from "@orochibraru/ui/page-title"; const title = usePageTitle(); </script> <p class="text-muted-foreground text-sm"> This page set its title to <strong>{title.current}</strong> and its back button to <code>{title.backHref}</code>. The phone app bar, the desktop header and the browser tab all read it. </p> ``` ### +layout.svelte ```svelte <script lang="ts"> import { providePageTitle } from "@orochibraru/ui/page-title"; const { children } = $props(); providePageTitle(); </script> {@render children()} ``` ### +page.svelte ```svelte <script lang="ts"> import PencilIcon from "@lucide/svelte/icons/pencil"; import PlusIcon from "@lucide/svelte/icons/plus"; import { setPageTitle } from "@orochibraru/ui/page-title"; const { data } = $props(); setPageTitle(() => data.project.name, "/projects", [ { label: "New task", icon: PlusIcon, href: "/tasks/new", primary: true, }, { label: "Edit project", icon: PencilIcon, href: "/projects/edit" }, ]); </script> ``` ### app-bar.svelte ```svelte <script lang="ts"> import { usePageTitle } from "@orochibraru/ui/page-title"; const title = usePageTitle(); </script> <header class="flex h-14 items-center gap-2 px-4"> {#if title.backHref} <a href={title.backHref}>Back</a> {/if} <h1 class="truncate font-semibold">{title.current}</h1> </header> ``` --- # Password > The password rules and strength score, for the form and the server alike. Category: Forms. Import from `@orochibraru/ui/password`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { Input } from "@orochibraru/ui/input"; import { passwordRules, passwordScore } from "@orochibraru/ui/password"; let password = $state(""); const rules = $derived(passwordRules(password, "alex@example.com")); const score = $derived(passwordScore(password, "alex@example.com")); </script> <div class="grid gap-3"> <Input type="text" placeholder="Type a password" aria-label="Password" bind:value={password} /> <p class="text-sm">Score: {score} / 4</p> <ul class="text-sm"> {#each rules as rule (rule.id)} <li class={rule.passed ? "text-positive" : "text-muted-foreground"}> {rule.passed ? "✓" : "✗"} {rule.label} </li> {/each} </ul> </div> ``` --- # Toast > Narrate a form action through one promise toast. Category: Feedback. Import from `@orochibraru/ui/toast`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { enhance } from "$app/forms"; import { Button } from "@orochibraru/ui/button"; import * as Field from "@orochibraru/ui/field"; import { Input } from "@orochibraru/ui/input"; import { saveToast } from "@orochibraru/ui/toast"; </script> <form method="POST" action="?/save" use:enhance={saveToast("Profile")} class="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end" > <Field.Field> <Field.Label for="toast-name">Name</Field.Label> <Input id="toast-name" name="name" value="Alex" /> <Field.Description>Clear it to see the action's error.</Field.Description> </Field.Field> <Button type="submit">Save</Button> </form> ``` ### +page.server.ts ```ts import { type Actions, fail } from "@sveltejs/kit"; export const actions = { save: async ({ request }) => { const form = await request.formData(); if (!String(form.get("name") ?? "").trim()) { return fail(400, { error: "Name is required." }); } return { saved: true }; }, } satisfies Actions; ``` --- # Utils > cn to merge Tailwind classes, and the prop helper types. Category: Utilities. Import from `@orochibraru/ui/utils`. ## Usage ### Example.svelte ```svelte <script lang="ts"> import { cn } from "@orochibraru/ui/utils"; let urgent = $state(false); </script> <button type="button" onclick={() => (urgent = !urgent)} class={cn( "bg-muted rounded-xl px-4 py-2 text-sm font-medium", urgent && "bg-destructive text-white", )} > {urgent ? "Urgent" : "Normal"}: click to toggle </button> ```