# UISet: every docs page Generated from https://github.com/superzero11/uiset for coding agents. # UISet docs UISet is themes, components, kits and templates for Vercel's UI SDK. Every item installs with the shadcn CLI and is styled by the skin around it. ```bash npx shadcn@latest add superzero11/uiset/uiset ``` ## Guides - [Introduction](./introduction.md): What UISet is, how it relates to UI SDK and shadcn/ui, and what's in it. - [Installation](./installation.md): Add UISet with the shadcn CLI, load each skin's fonts, or use the Web Components build. - [Quick start](./quick-start.md): A working settings screen in a few minutes. - [Building with AI agents](./ai.md): The skills and llms.txt for coding agents, and UI SDK's agent bridge for agents that use your app. - [Web Components](./web-components.md): Every kit and template as custom elements, for plain HTML, Vue, Svelte and Solid. - [Templates](./templates.md): Whole pages built only from UISet items. - [Theming](./theming.md): Skins, the key color, dark mode, nesting, sound and haptics, and the theme builder. - [Coming from shadcn/ui](./migrate-from-shadcn.md): What maps to what, and how to move a shadcn/ui app over. - [Roadmap](./roadmap.md): What's shipped, what's next, and how UISet follows UI SDK. - [Blueprint skin](./blueprint.md): The Blueprint skin's design and tokens. - [Studio skin](./studio.md): The Studio skin's design and tokens. - [Phosphor skin](./phosphor.md): The Phosphor skin's design and tokens. - [Ink skin](./ink.md): The Ink skin's design and tokens. ## Kits and parts | Item | What it is | Docs | | --- | --- | --- | | `uiset` | The UISet root: skins, a key color, and sound and haptics for every UI SDK component inside it. | [Root](./theming.md) | | `app-shell` | A sidebar layout that collapses to icons with Mod+B, remembers it, and becomes a drawer on narrow screens; with sidebar navigation. | [App frame and data display](./app-shell.md) | | `breadcrumbs` | Where you are, with the current page marked. | [App frame and data display](./app-shell.md) | | `page-header` | A page title with description and actions. | [App frame and data display](./app-shell.md) | | `card` | A bordered block with header, title, description, content and footer. | [App frame and data display](./app-shell.md) | | `badge` | A short status label in five tones, readable in every skin. | [App frame and data display](./app-shell.md) | | `stat` | A number with its change, said in words as well as color, and an optional trend sparkline. | [App frame and data display](./app-shell.md) | | `empty-state` | What to do when there's nothing here yet. | [App frame and data display](./app-shell.md) | | `skeleton` | Loading placeholders, and a Loading region that announces once. | [App frame and data display](./app-shell.md) | | `description-list` | Terms and their details. | [App frame and data display](./app-shell.md) | | `item-list` | Rows with media, title, description, meta and actions. | [App frame and data display](./app-shell.md) | | `alert` | An inline message in four tones that names its tone, interrupts only when urgent, and can be dismissed. | [App frame and data display](./app-shell.md) | | `kbd` | Keys written the way this platform writes them. | [App frame and data display](./app-shell.md) | | `spinner` | A loading indicator that says what's loading. | [App frame and data display](./app-shell.md) | | `stepper` | Steps of a flow with the current one marked and later ones locked. | [App frame and data display](./app-shell.md) | | `search-field` | A search box with a clear button and a key that focuses it from anywhere. | [App frame and data display](./app-shell.md) | | `copy-button` | Copies a value and says so. | [App frame and data display](./app-shell.md) | | `rich-text` | A rich text editor on UI SDK with no editor library: headings, nested and numbered lists, to-dos, quotes, code, links, mentions, dividers and images; a toolbar, Markdown as you type, "/" for blocks and "@" for people (TriggerMenu); safe paste from documents and the web; undo; Markdown and clean HTML out; agents read and write Markdown. | [Rich text](./rich-text.md) | | `chat` | A conversation built on UI SDK: thread that follows new messages, messages with avatars, actions and versions, suggestions, plans and a thinking shimmer. | [Chat](./chat.md) | | `chat-response` | Streaming markdown that repairs itself mid-stream and never renders raw HTML, with code blocks highlighted by skin colors. | [Chat](./chat.md) | | `chat-parts` | Reasoning that opens while the model thinks, tool calls with status, input and output, sources, citations and images. | [Chat](./chat.md) | | `chat-composer` | The message box: Enter to send, history with ArrowUp, slash commands and mentions, attachments by drop, paste or picker, a model picker and a usage meter. | [Chat](./chat.md) | | `chat-sidebar` | A list of chats with rename and confirmed delete, and a floating assistant launcher. | [Chat](./chat.md) | | `data-grid` | A spreadsheet-grade grid on UI SDK: virtualized rows, sorting, filters, grouping, pinned, resizable and movable columns, row and cell selection, inline editing with validation, clipboard copy and paste, CSV export, a saved layout and agent actions. | [Data grid](./data-grid.md) | | `chart` | The frame every UISet chart shares: title, a legend that isolates series, a Chart/Table switch with the table twin, a keyboard listbox of data positions, tooltips, palettes validated for color blindness per skin, and agent actions. | [Charts](./charts.md) | | `line-chart` | Trends over time: one or more lines, a soft area for one series, or stacked areas, with a crosshair that lists every series and labels at line ends. | [Charts](./charts.md) | | `bar-chart` | Categories compared: columns or horizontal bars, grouped or stacked, ranked, with thin marks and values at the tips. | [Charts](./charts.md) | | `scatter-chart` | Two measures per item, up to three groups, with hover that finds the nearest point. | [Charts](./charts.md) | | `heatmap` | Magnitude on a grid in one hue, with a scale and arrows that move by cell and by row. | [Charts](./charts.md) | | `segmented-bar` | One whole split into up to six parts, with shares printed where they fit: the readable answer to a donut. | [Charts](./charts.md) | | `sparkline` | A trend inside text, a table cell or a stat, whose name says where the values went. Works in Server Components. | [Charts](./charts.md) | | `board` | A kanban board on UI SDK createTransfer: one tab stop, arrows across cards and columns, Space to carry a card, drag with the pointer, column limits, add in place, a details dialog to edit, move or delete, filters, and agent actions. | [Board](./board.md) | | `scheduler` | Calendars on UI SDK createSpans: a week, a day or a room timeline where people draw, move and resize events with the pointer or arrows, overlapping events sharing lanes, a month view, a toolbar, an event dialog, and agent actions. | [Scheduler](./scheduler.md) | | `canvas` | A node editor on UI SDK's canvas behaviors: pan and zoom, drag nodes or move them with Alt and arrows, spatial focus, Shift-drag box selection, wires drawn by pointer or with C and arrows, a minimap, zoom controls, a toolbar, an inspector, undo, and agent actions. | [Canvas](./canvas.md) | | `ide` | A code editor's parts on UI SDK: a file tree on Tree with version control marks and New, Rename, Move and Delete; editor tabs on Tabs; a code editor with syntax colors, indenting and problems; a command palette on Command; problems and a status bar. | [IDE](./ide.md) | | `mobile` | Phone app parts on UI SDK: rows that swipe aside to actions (createSwipeActions), wheel pickers and a time wheel (bindScrollPicker), bottom sheets and action sheets (Drawer), pull to refresh, an app bar and a tab bar. | [Mobile](./mobile.md) | | `terminal` | A terminal on UI SDK's input history and stick-to-bottom: ANSI colors kept readable in every skin, links, streaming commands with progress lines, Tab completion, Ctrl+C and Ctrl+L, built-in help, clear and history, and agent actions. | [Terminal](./terminal.md) | | `knob` | A rotary control built as a UI SDK primitive: drag, arrow keys, log scales, double-click reset, agent actions. | [Studio](./studio-kit.md) | | `patch-bay` | Jacks and cables built on UI SDK createConnect: patch by drag, keyboard or agent. | [Studio](./studio-kit.md) | | `fader` | A vertical UI SDK slider with its label and readout. | [Studio](./studio-kit.md) | | `level-meter` | A UI SDK meter drawn as an LED ladder. | [Studio](./studio-kit.md) | | `step-sequencer` | A row of steps built on UI SDK ToggleGroup, with downbeats and a playhead. | [Studio](./studio-kit.md) | | `agent-console` | The human side of the UI SDK agent bridge: approvals in your app, an activity feed with undo, highlights where the agent changed something, a thread and a prompt. | [Agent Console](./agent-console.md) | ## UI SDK components Each of UI SDK's 55 components as its own item, styled by the skin, with UISet's defaults. Their API, parts and keys are UI SDK's: the linked page applies as it is. | Item | What UISet adds | API and keys | | --- | --- | --- | | `button` | A button in solid, outline and ghost variants. | [UI SDK](https://ui-sdk.dev/docs/components/button) | | `switch` | An on and off switch, with its thumb in place. | [UI SDK](https://ui-sdk.dev/docs/components/switch) | | `slider` | A range slider whose track, fill and named thumbs render in one line. | [UI SDK](https://ui-sdk.dev/docs/components/slider) | | `segmented-control` | One choice among a few, in a row with a moving indicator. | [UI SDK](https://ui-sdk.dev/docs/components/segmented-control) | | `select` | Choose from a list in a popup; the trigger shows what's chosen. | [UI SDK](https://ui-sdk.dev/docs/components/select) | | `field` | A label, control, description and error, connected for screen readers. | [UI SDK](https://ui-sdk.dev/docs/components/field) | | `dialog` | A modal window for a focused task, closed by Escape or its close button. | [UI SDK](https://ui-sdk.dev/docs/components/dialog) | | `tabs` | Panels that share one space, switched by a row of tabs. | [UI SDK](https://ui-sdk.dev/docs/components/tabs) | | `accordion` | Sections that open one or many at a time. | [UI SDK](https://ui-sdk.dev/docs/components/accordion) | | `alert-dialog` | A confirmation that interrupts, with agent confirmation built in. | [UI SDK](https://ui-sdk.dev/docs/components/alert-dialog) | | `autocomplete` | Free text with suggestions as you type. | [UI SDK](https://ui-sdk.dev/docs/components/autocomplete) | | `avatar` | A person's image with initials as the fallback. | [UI SDK](https://ui-sdk.dev/docs/components/avatar) | | `calendar` | A month grid for picking dates, ready to use as is. | [UI SDK](https://ui-sdk.dev/docs/components/calendar) | | `carousel` | Slides with previous and next controls and keyboard paging. | [UI SDK](https://ui-sdk.dev/docs/components/carousel) | | `checkbox` | A checkbox with its check mark, and checkbox groups. | [UI SDK](https://ui-sdk.dev/docs/components/checkbox) | | `collapsible` | One section that shows and hides. | [UI SDK](https://ui-sdk.dev/docs/components/collapsible) | | `color-picker` | Area, hue and alpha sliders, swatch and hex input, ready to use. | [UI SDK](https://ui-sdk.dev/docs/components/color-picker) | | `combobox` | Search a list and choose one or several. | [UI SDK](https://ui-sdk.dev/docs/components/combobox) | | `command` | A searchable list of commands, for palettes and launchers. | [UI SDK](https://ui-sdk.dev/docs/components/command) | | `date-input` | A date typed segment by segment, in the reader's locale. | [UI SDK](https://ui-sdk.dev/docs/components/date-input) | | `date-picker` | A date field with a calendar popup. | [UI SDK](https://ui-sdk.dev/docs/components/date-picker) | | `drawer` | A panel that slides in from an edge. | [UI SDK](https://ui-sdk.dev/docs/components/drawer) | | `file-input` | Choose, drop or paste files, with validation. | [UI SDK](https://ui-sdk.dev/docs/components/file-input) | | `hover-card` | A preview that opens on hover or focus. | [UI SDK](https://ui-sdk.dev/docs/components/hover-card) | | `image-cropper` | Crop, zoom, rotate and flip an image. | [UI SDK](https://ui-sdk.dev/docs/components/image-cropper) | | `input` | A text field, with clear button and groups. | [UI SDK](https://ui-sdk.dev/docs/components/input) | | `label` | A label for any control. | [UI SDK](https://ui-sdk.dev/docs/components/label) | | `listbox` | An always-visible list to choose from. | [UI SDK](https://ui-sdk.dev/docs/components/listbox) | | `menu` | Actions from a button or a right click, with submenus. | [UI SDK](https://ui-sdk.dev/docs/components/menu) | | `menubar` | An application menu bar. | [UI SDK](https://ui-sdk.dev/docs/components/menubar) | | `meter` | A measurement within a range, ready to use. | [UI SDK](https://ui-sdk.dev/docs/components/meter) | | `navigation-menu` | Site navigation with panels. | [UI SDK](https://ui-sdk.dev/docs/components/navigation-menu) | | `number-input` | A number field with named step buttons, ready to use. | [UI SDK](https://ui-sdk.dev/docs/components/number-input) | | `otp-input` | A one-time code field with a slot per character. | [UI SDK](https://ui-sdk.dev/docs/components/otp-input) | | `pagination` | Page links with named previous and next, ready to use. | [UI SDK](https://ui-sdk.dev/docs/components/pagination) | | `password-input` | A password field with a show toggle. | [UI SDK](https://ui-sdk.dev/docs/components/password-input) | | `popover` | A panel beside its trigger. | [UI SDK](https://ui-sdk.dev/docs/components/popover) | | `preferences` | User preferences applied as data attributes and saved. | [UI SDK](https://ui-sdk.dev/docs/components/preferences) | | `progress` | Progress of a task, ready to use. | [UI SDK](https://ui-sdk.dev/docs/components/progress) | | `radio` | Radio groups with their dot. | [UI SDK](https://ui-sdk.dev/docs/components/radio) | | `rating` | A star rating, ready to use. | [UI SDK](https://ui-sdk.dev/docs/components/rating) | | `resizable` | Panels you resize by dragging or with the keyboard. | [UI SDK](https://ui-sdk.dev/docs/components/resizable) | | `scroll-area` | Styled scrolling, plus ScrollBox with the parts in place. | [UI SDK](https://ui-sdk.dev/docs/components/scroll-area) | | `separator` | A line between content. | [UI SDK](https://ui-sdk.dev/docs/components/separator) | | `textarea` | Multi-line text that can grow with its content. | [UI SDK](https://ui-sdk.dev/docs/components/textarea) | | `time-input` | A time typed segment by segment. | [UI SDK](https://ui-sdk.dev/docs/components/time-input) | | `timeline` | Events in order, with markers and a connecting line. | [UI SDK](https://ui-sdk.dev/docs/components/timeline) | | `toast` | Notifications, with a Toaster ready to render once. | [UI SDK](https://ui-sdk.dev/docs/components/toast) | | `toggle` | Pressed buttons and toggle groups. | [UI SDK](https://ui-sdk.dev/docs/components/toggle) | | `token-input` | Text mixed with tokens for files, people or tags. | [UI SDK](https://ui-sdk.dev/docs/components/token-input) | | `toolbar` | Buttons and toggles with one tab stop. | [UI SDK](https://ui-sdk.dev/docs/components/toolbar) | | `tooltip` | A short hint on hover or focus. | [UI SDK](https://ui-sdk.dev/docs/components/tooltip) | | `tree` | A hierarchy you expand and select. | [UI SDK](https://ui-sdk.dev/docs/components/tree) | | `trigger-menu` | Suggestions after @, / or # in a text field. | [UI SDK](https://ui-sdk.dev/docs/components/trigger-menu) | | `waterfall` | Spans on a time axis, like request timings or a trace. | [UI SDK](https://ui-sdk.dev/docs/components/waterfall) | ## Templates | Item | What it is | | --- | --- | | `auth` | Sign in, sign up, forgot and reset password, and verify a code: native validation, server messages, autocomplete hints and a password strength meter. | | `dashboard` | A date range that scopes everything: figures with trends, revenue, orders by channel, plan mix and recent orders. | | `settings` | Profile, security and sessions, notifications, billing with usage and invoices, team with invites and removal, and a danger zone. | | `admin` | A records list with search, filters, columns and export; create and edit in a drawer; inline edits; confirmed bulk delete; an empty state. | | `inbox` | Mail in three panes: folders, a message list you arrow through, the message with star, archive and delete, and compose. | | `chat-app` | The chat kit as a product: chats, a thread and a composer, with your model plugged in as an async stream of events. | | `ide-app` | A code editor in resizable panes: files with version control marks, tabs, an editor with syntax colors, a command palette (files, commands, lines), problems, an outline and a status bar. | | `project-board` | A project's issues as a board or a list: sprint progress, search and an assignee filter, column limits, card details, a new-issue form and Undo on moves. | | `calendar-app` | A calendar app: day, week and month views, a small month to jump to a day, calendars to show or hide, what's next, and events created, edited and deleted in a dialog. | | `pricing` | Plans with a monthly or yearly switch, a recommended plan, a comparison table and questions. | See [Templates](./templates.md) for each one's props and sample data. # Introduction UISet is the components, kits, page templates and themes for Vercel's UI SDK. It's open source, installs with the shadcn CLI, and works in React and, through Web Components, in Vue, Svelte, Solid, Angular and plain HTML. ```bash npx shadcn@latest add superzero11/uiset/uiset ``` ## Why it exists [UI SDK](https://ui-sdk.dev) moved the hard part of UI into reusable behaviors: arrow keys, focus, selection, dismissal, popups, drag and drop, and the ARIA that goes with them. Its 55 components are built from those behaviors, and its agent bridge lets AI agents read a page and work its controls without screenshots. Two things it leaves to you: - **How it looks.** UI SDK's components are unstyled. - **The large components apps are made of.** There's no data grid, chat, rich text editor, scheduler, board, node canvas, IDE or terminal. UISet fills both, without changing the behavior underneath. Everything is still UI SDK's: the same parts, the same names, the same keyboard and agent support. ## What's in it | What | How many | What it gives you | | ---- | -------- | ----------------- | | [Components](/components) | 55, plus 16 app parts | Every UI SDK component, styled; and the app frame and data display parts (sidebar layout, page header, cards, badges, stats, alerts, a stepper) | | [Kits](/kits) | 12 | Chat, rich text, data grid, charts, board, scheduler, canvas, IDE, mobile, terminal, Studio and the Agent Console | | [Templates](/templates) | 10 | Whole pages: auth, dashboard, settings, admin, inbox, chat app, project board, calendar, pricing, IDE | | [Themes](/themes) | 5 skins | Base, Blueprint, Studio, Phosphor and Ink, plus a [generator](/themes/generator) for your own | Each piece is its own registry item: install only what you use, and it brings only what it needs. ## How it works - **The root.** `` wraps your app (or any part of it). It holds the skin, the light or dark setting, and the interaction sounds. Every UISet and UI SDK component inside takes the skin. - **Skins are CSS.** A skin sets CSS variables in a cascade layer. Behavior never depends on them, so you can switch skins live or nest one inside another. - **Your code.** The shadcn CLI copies each item's source into `components/ui/`. Change it as you like; items import each other relatively, so they keep working after you move them. - **Named for agents.** Every control has an accessible name, and each kit gives agents actions that mean something (a grid sorts and filters; a scheduler adds events). ## Compared with shadcn/ui shadcn/ui styles Radix UI; UISet styles UI SDK. If you know shadcn/ui, you know the workflow: the same CLI, the same folder, and UISet's base skin picks up your shadcn theme variables. What's different is underneath (UI SDK's behaviors and agent bridge) and what's included (the kits and templates). See [Coming from shadcn/ui](./migrate-from-shadcn.md). ## Status UI SDK is in public beta (`0.1.0-beta.2`), and UISet pins that exact version. Every item is tested for accessibility in every skin and theme, for keyboard use, for agent use, and for a clean install into a new app. Expect API changes until UI SDK reaches 1.0; the [roadmap](./roadmap.md) says what's next. ## Next - [Installation](./installation.md): set up a project and add your first items. - [Quick start](./quick-start.md): a working screen in a few minutes. - [Building with AI agents](./ai.md): give your coding agent the skill and the docs. # Installation UISet installs with the shadcn CLI, one item at a time, into your project's components folder. You need React 19 and a project the shadcn CLI can write to. ## With the shadcn CLI ### 1. Set up the project If your project already uses shadcn/ui, skip this step. Otherwise run `init` once; it writes `components.json`, which tells the CLI where components go and what `@/` means. ```bash npx shadcn@latest init ``` UISet doesn't need Tailwind. If `init` asks, any choice works. ### 2. Add the root The root brings the tokens, the five skins, sound and haptics, and pins UI SDK's packages (`@ui-sdk/core` and `@ui-sdk/react`) at the exact version UISet is tested with. ```bash npx shadcn@latest add superzero11/uiset/uiset ``` ### 3. Wrap your app ```tsx import { UISet } from "@/components/ui/uiset" export function App() { return ( ) } ``` With a shadcn/ui theme on the page, the base skin uses its colors and radius. Without one, pass `keyColor={{ l: 0.5, c: 0.19, h: 268 }}` or make a theme in the [theme generator](/themes/generator). ### 4. Add what you need ```bash npx shadcn@latest add superzero11/uiset/button superzero11/uiset/field superzero11/uiset/data-grid ``` Each item lands in `components/ui/` and brings the items it depends on. Every docs page has the command for its item, for pnpm, npm, yarn and bun. ### Addresses `superzero11/uiset/` reads the registry from GitHub. Each item is also served from the site, if you prefer an address: ```bash npx shadcn@latest add https://uiset.dev/r/data-grid.json ``` ## Fonts The base skin uses your app's fonts. The themed skins name the faces they were drawn for and fall back to system fonts without them. Load the ones you use, from Google Fonts or your own files: | Skin | Faces | | ----------- | ------------------------------- | | `blueprint` | Barlow Semi Condensed | | `studio` | Saira Semi Condensed | | `phosphor` | Spline Sans Mono | | `ink` | Literata, Atkinson Hyperlegible | ## Web Components For Vue, Svelte, Solid, Angular or plain HTML, use the Web Components build: one module and one stylesheet, with UI SDK as a peer. ```bash npm install @uiset/web @ui-sdk/web ``` ```html ``` See [Web Components](./web-components.md) for every element. ## Check it works Render a button inside the root and tab to it: it should show the skin's focus ring. If it looks unstyled, the root isn't around it, or `uiset.css` (imported by `uiset.tsx`) didn't load. ## Updating Run the same `add` command again with `--overwrite` to take a newer version of an item. Your changes to that file are replaced, so commit first and compare. # Quick start Build a project settings screen: a form that saves with a toast, inside the app frame, in any skin. It takes a few minutes and shows the whole workflow. ## 1. Install what the screen uses ```bash npx shadcn@latest add superzero11/uiset/uiset superzero11/uiset/app-shell superzero11/uiset/page-header superzero11/uiset/field superzero11/uiset/select superzero11/uiset/switch superzero11/uiset/button superzero11/uiset/toast ``` ## 2. Write the screen ```tsx import { useState } from "react" import { AppShell, SidebarGroup, SidebarItem, SidebarNav } from "@/components/ui/app-shell" import { Button } from "@/components/ui/button" import { Field, FieldLabel, Input } from "@/components/ui/field" import { PageHeader } from "@/components/ui/page-header" import { Select, SelectContent, SelectItem, SelectLabel, SelectTrigger } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Toaster, toast } from "@/components/ui/toast" import { UISet } from "@/components/ui/uiset" export function Settings() { const [name, setName] = useState("Atlas") return ( Acme} sidebar={ Settings } >
{ event.preventDefault() toast("Settings saved", { description: `${name} is up to date.` }) }} > Project name
) } ``` Everything you see is UI SDK's behavior with UISet's look: the select opens with the keyboard and types ahead, the switch toggles with Space, the toast is announced, and focus always shows. ## 3. Try another skin Change `skin="base"` to `"blueprint"`, `"studio"`, `"phosphor"` or `"ink"`. Nothing else changes. (Load the skin's [fonts](./installation.md#fonts) for the intended look.) ## 4. Let an agent use it Every control above has a name, so UI SDK's agent bridge can see and use the form: ```ts import { createAgent } from "@ui-sdk/react/agent" const agent = createAgent() agent.snapshot() // Project name, Region, Preview deployments, Save settings… await agent.act({ steps: [{ ref: "Region", action: "setValue", args: ["North America"] }] }) ``` See [Building with AI agents](./ai.md) for approvals, undo and the Agent Console. ## 5. Add a kit ```bash npx shadcn@latest add superzero11/uiset/data-grid ``` Then follow the [data grid](/docs/kits/data-grid) page. Or skip ahead: the [settings template](/templates?template=settings) is a finished version of this screen. # Building with AI agents Two kinds of agent meet UISet: coding agents that build your interface, and agents that use it once it's running. UISet has something for each. ## Coding agents ### The skills Skills teach a coding agent (Claude Code, Cursor, Codex and others that read [agent skills](https://skills.sh)) how to use UISet: which item fits a task, how to install and wrap, the skins, and the rules that keep its output accessible and agent-ready. ```bash npx skills add superzero11/uiset ``` That adds `uiset`, the core skill, and one skill per kit (`uiset-chat`, `uiset-data-grid`, `uiset-rich-text` and the rest), each carrying that kit's docs. The core skill points the agent at UI SDK's own skill for building new behaviors. What the skills tell an agent, in short: - Use a UISet kit or component before building one; build anything new from UI SDK's behaviors, never with hand-written keyboard handling or ARIA. - Name every control; mark destructive actions with `data-agent-confirm`. - Style with the skin's CSS variables, never fixed colors, so it works in every skin. ### llms.txt [`/llms.txt`](/llms.txt) lists every docs page with a line on each, for agents that read docs from the web. [`/llms-full.txt`](/llms-full.txt) holds every page in one file. Every docs page has its Markdown a click away ("View Markdown"), and every item's API comes from UI SDK's own docs, which ship inside its packages at `node_modules/@ui-sdk/react/docs/`. ### A prompt to start from ```text This project uses UISet (components, kits, templates and themes for Vercel's UI SDK). Read https://uiset.dev/llms.txt for what's available, and the docs page for each item you use. Install items with: npx shadcn@latest add superzero11/uiset/ Wrap the app in from @/components/ui/uiset. Don't hand-write keyboard handling or ARIA that a UI SDK component covers, and give every control a name. ``` ## Agents that use your app UI SDK's agent bridge gives an agent the page as data: each component's name, value and actions, and a way to act on them. The agent never needs a screenshot. ```ts import { createAgent } from "@ui-sdk/react/agent" const agent = createAgent() agent.snapshot() // [{ ref: "c4", component: "data-grid", label: "Orders", actions: ["sort", "filter", …] }, …] await agent.act({ steps: [{ ref: "Orders", action: "sort", args: ["Amount", "descending"] }] }) ``` Register the same tools with a browser that supports WebMCP with `registerTools(agent)`, or hand `agentTools` to your own model. UI SDK's [agents guide](https://ui-sdk.dev/docs/agents) covers the bridge in full. ### What UISet adds - **Names.** Every control has an accessible name, which is how the bridge finds it. Specific names ("Search orders", not "Search") keep refs unambiguous. - **Actions that mean something.** Kits expose their work as actions: the data grid sorts, filters, groups and edits; the scheduler adds, moves and resizes events; the board moves cards; the rich text editor reads and writes Markdown; the patch bay plugs cables. - **Confirmation.** Destructive buttons carry `data-agent-confirm`, so the bridge's `allow` step asks a person before an agent deletes anything. - **The Agent Console.** The human side of the bridge: approvals in your app, a list of every change with undo, and highlights where the agent acted. See [Agent Console](/docs/kits/agent-console). ### The benchmark UI SDK measured agents doing a settings task with browser tools on another library and with the bridge on UI SDK. We ran the same comparison on UISet's kits; the numbers and the method are on the [Agents page](/agents#benchmark). # UISet for Web Components (and Vue, Svelte, Solid) UISet's skins, tokens and base styles dress **UI SDK's own custom elements**, the same 55 components the React registry wraps, because both carry the same `data-slot` attributes. So a page in plain HTML, Vue, Svelte or Solid gets UISet's look and themes from one stylesheet, and UISet's own primitives register as `` elements. ```bash pnpm web:build # makes web/dist/uiset.js and web/dist/uiset.css ``` The build keeps UI SDK as a peer: install `@ui-sdk/web` and `@ui-sdk/core` (0.1.0-beta.2) and import the UI SDK components you use. `uiset.css` already includes UI SDK's `web.css`. `uiset.js` is about 117 KB gzipped with every kit and `uiset.css` 39 KB; the chat's markdown parser (53 KB) loads only when a chat first draws an answer. The page templates aren't in the build: they're pages to copy, each with its own small script. Install `shiki` too for colored code in chats; without it, code blocks are plain. ## Plain HTML ```html Cutoff ``` - `` is the React ``: `skin` (`base`, `blueprint`, `studio`), `theme` (`light`, `dark`, or leave it out to follow the system), `sound` (present to turn the feedback sounds on) and `haptics="off"`. Change the attributes to switch live. - Variants go on the element that gets `data-slot`, which for a wrapping element like `` is the native button inside it: `
… ``` - A port whose value ends in `:out` patches into one ending in `:in`; an input takes one cable, and patching a connected pair again pulls the cable out. - `bay.value` reads and sets the cables (`[{ from, to }]`), and `valuechange` fires when a person or agent changes them. `bay.api.plug(from, to)`, `unplug` and `clear` work by port or by name. - Ports are named by their buttons' `aria-label`s, for agents and for each jack's "Patched to …" description, unless you set `bay.labels` yourself. - Mark the current step with `data-current` and the downbeats with `data-downbeat` on the buttons. ## Mobile ```html ``` ```ts list.items = messages.map((m) => ({ id: m.id, label: `${m.from}: ${m.subject}`, content: `${m.from} · ${m.subject}`, // or a Node start: [{ id: "read", label: "Read", icon: "mail" }], end: [ { id: "archive", label: "Archive", icon: "archive", tone: "neutral" }, { id: "delete", label: "Delete", icon: "trash", tone: "danger" }, ], })) list.addEventListener("action", (e) => run(e.detail.value, e.detail.action)) list.addEventListener("open", (e) => open(e.detail.value)) // with `openable` refresh.addEventListener("refresh", (e) => e.detail.waitUntil(fetchMail())) ``` - Set `items` again whenever your data changes; rows are kept by `id`, so the focused row, an open row and their elements stay. After an action, focus goes back to its row, or to the row now in its place. - Everything the React SwipeList does is here: a long swipe runs a side's first action, each row has an Actions menu for screen readers, `messages` translates the words, and agents `run(row, action)` and `openRow(row)` by name (a `danger` action needs the person). - `` scrolls its children and slides them down as it's pulled. A rejected `waitUntil` Promise is announced and fires `refresherror`; `refresh()` runs one from code, and a Refresh button shows on keyboard focus. `threshold` and `messages` tune it. - `` binds UI SDK's `bindScrollPicker` to the listbox inside it, and keeps focus on the value when it changes from elsewhere (an agent's `setValue`, say): ```html
1 …
``` - Sheets, action sheets, the app bar and the tab bar are UI SDK's drawer and buttons in UISet's classes: `mobile-sheet` (add `data-snaps` with several snap points), `mobile-sheet-handle`, `mobile-action` with `data-destructive`, `mobile-app-bar`, `mobile-tab-bar` and `mobile-tab` with `aria-current="page"`. An action sheet's choice comes back from `await drawer.api.open()` as `returnValue`, from ``. `web/mobile.html` has all of it working. - `iconElement(name)` (exported from `uiset.js`) draws UISet's icons without React. ## Board ```html ``` ```ts board.columns = [{ id: "todo", title: "To do" }, { id: "doing", title: "Doing", limit: 3 }, { id: "done", title: "Done" }] board.cards = { c1: { id: "c1", title: "Write the spec", assignee: "Grace Hopper", labels: ["Docs"] } } board.lanes = { todo: ["c1"], doing: [], done: [] } board.addEventListener("laneschange", (e) => save(e.detail.value)) // also e.detail.key, from, to, index board.addEventListener("open", (e) => showCard(e.detail.value)) // Enter or a click board.addEventListener("add", (e) => create(e.detail.value, e.detail.title)) ``` - `lanes` is live: read it any time (you get a copy), set it to change the board (an undo, a server update). Set `cards` again when a card changes: a card is kept by id and filled again in place, so it keeps focus, and an open "Add a card" form stays. Renamed columns, a new `renderCard` and a changed `today` attribute redraw too. - `addable` shows "Add a card" and `openable` makes Enter and clicks open cards. Without them the board does neither, and agents are told so. - Arrows move between cards and columns; Space picks a card up, arrows carry it (the board redraws as it goes), Space drops it and Escape puts it back. Column `limit`s hold for people, agents and `board.api.moveCard`. - `add` fires when someone submits "Add a card": add the card to `cards` and `lanes` yourself. A column that fills hands focus to its last card. Counts include every id in a column, as the limit check does. - `hiddenCards` (ids) hides cards in place for a search or filter: focus skips them (a focused card that gets hidden hands focus to its neighbor) and moves wait until it clears. Ids that aren't on the board don't hold moves back. (It isn't `hidden`, which hides the element itself.) - The board's help is added to your own `aria-describedby`, not put in its place. - `describeCard(card)` sets what screen readers hear after a title, `renderCard(card)` returns your own card face (a Node), and `messages` holds the words (`help`, `empty`, `full`, `filtered`). - `board.api` has `moveCard(card, column, position?)`, `addCard`, `openCard`, `focusCard`, `cardElement`, `neighbor` (where focus goes when a card leaves) and `hasRoom`, by id or by the names people see. Agents use the same names. - Card details are your page's: open a `ui-dialog` on `open`. On web components its `final-focus` is a selector, read as it opens: point it at `uiset-board-card[value="…"]`. `web/board.html` has a dialog that moves cards (a full column says so) and deletes them, returning focus to the card or its neighbor; each move and delete has its own Undo. ## Scheduler ```html ``` ```ts schedule.tracks = [{ id: "2026-11-16", label: "Monday, Nov 16", short: "16 Mon" }, …] // days, or rooms schedule.events = [{ id: "e1", track: "2026-11-16", start: 9 * 60, end: 10 * 60, title: "Standup", color: 1 }] schedule.addEventListener("eventschange", (e) => save(e.detail.value)) // a draw, move, resize or delete schedule.addEventListener("open", (e) => showEvent(e.detail.value)) // Enter or a click schedule.create = (span) => ({ ...span, title: "New event" }) // what a drag or click draws month.events = schedule.events month.addEventListener("daychoose", (e) => showDay(e.detail.value)) ``` - Times are minutes past midnight; a track is a day (an ISO date) or anything else, like a room. `axis="x"` lays tracks out as rows (a timeline). `start-hour`, `end-hour`, `step` (minutes, 15 by default), `size` (the default length) and `overlap="false"` (rooms) are attributes, and changing one redraws. - Each event is a button: drag it to move, drag its end edge to resize, drag an empty stretch to draw. Arrows move it by a step or a track, Shift with arrows changes its end, Delete removes it and hands focus to its neighbor, Enter opens it (with `openable`; without it the schedule doesn't open events, and agents are told). Events keep their elements as they move, so a drag holds its pointer and focus stays. - `events` is live: read it, or set it (an undo, a server update). `locked(event)` keeps events from changing, `timeLabel(minutes)` sets how times read (12-hour English by default), `renderEvent(event)` returns your own face, and `now` with `today` draws the current-time line. Your `aria-describedby` stays, with the schedule's help after it. - `schedule.api` has `addEvent`, `moveEvent`, `resizeEvent`, `removeEvent`, `openEvent`, `problem(event)` (why an event wouldn't fit: hours, steps, overlaps), `eventElement` and `neighbor`. Agents add, move, resize and open events by the names people see. - `` is UI SDK's calendar with each day's first events as chips (`limit`, 3 by default) and the events in each day's name. Choosing a day fires `daychoose`; the month buttons fire `monthchange`. - The toolbar and the event dialog are your page's: `web/schedule.html` has Day, Week, Month and Rooms, a dialog that checks times with `api.problem`, and focus that returns to the event or a deleted one's neighbor. ## Canvas ```html ``` ```ts canvas.kinds = [{ id: "prompt", label: "Prompt" }, { id: "model", label: "Model" }] canvas.nodes = [{ id: "a", kind: "prompt", title: "Answer template", x: 40, y: 40, outputs: [{ id: "out", label: "Prompt" }] }, …] canvas.edges = [{ id: "a->b", from: "a:out:out", to: "b:in:prompt" }] canvas.createNode = (kind, at, title) => ({ id: crypto.randomUUID(), kind, title: title ?? "New node", ...at }) canvas.addEventListener("nodeschange", (e) => save(e.detail.value)) // e.detail.type: move, add, remove, edit canvas.addEventListener("edgeschange", (e) => saveWires(e.detail.value)) inspector.renderFields = (node) => myFields(node) // your fields, under Rename ``` - The canvas owns its `nodes` and `edges` once you set them, like a form field: a drag, a removal, an add or a wire changes them and fires `nodeschange` or `edgeschange` first, and `preventDefault()` refuses the change. Setting them yourself replaces them. - Drag empty space to pan, scroll or pinch to zoom, Shift-drag to select a box. Tab reaches the nodes once; arrows go to the nearest node that way, Alt with arrows moves it, Space selects, C starts a wire (arrows choose an input, Enter connects), Delete removes (with its wires), Enter opens (with `openable`). Plus and minus zoom; 0 resets. - `history` keeps an undo stack: Control+Z and Control+Shift+Z on the canvas, `canvas.undo()`, `redo()`, `canUndo` and `canRedo`, and `historychange`. A removal takes its wires in the same step, and `canvas.updateNode(id, patch)` (the inspector's Rename) is a step too. - `fit-view` frames every node once the canvas has a size. `controls="false"` and `minimap="false"` leave out the zoom buttons and the map. `grid` sets the snap and the Alt+arrow step. `kinds`, `canConnect(from, to)`, `locked(node)` and `renderNode(node)` are properties, as in React. - `canvas.api` has `addNode`, `moveNode`, `connect`, `disconnect`, `select`, `openNode`, `fitView`, the zoom steps, `focusNode`, `nodeElement`, `selectedNodes` and `removeSelected`. Agents add, wire, move and select nodes by the names people see. - `` is Add node (the canvas's kinds), Remove, Undo and Redo (with `history`) and Fit view on UI SDK's toolbar. `` shows the selected node's title, a Rename form (`renamable`), your fields, and its wires, each with Disconnect, which hands focus to the next wire or the node. ## IDE ```html main ``` ```ts files.paths = Object.keys(project) // a path ending in "/" is an empty folder files.status = { "src/index.ts": "modified" } // version control marks files.addEventListener("open", (e) => openFile(e.detail.value)) files.addEventListener("rename", (e) => rename(e.detail.from, e.detail.value)) // or e.detail.refuse("why") tabs.files = [{ path: "src/index.ts", dirty: true }] tabs.renderPanel = (path) => Object.assign(document.createElement("uiset-code-editor"), { value: project[path] }) tabs.addEventListener("close", (e) => closeFile(e.detail.value)) palette.files = Object.keys(project) palette.addEventListener("openfile", (e) => openFile(e.detail.value)) ``` - Each part is its own element and keeps no project of its own: you hold the files, and each part shows what you give it and fires what people do. `web/ide.html` joins them into the React IDE template's editor, from the same sample project and linter (`templates/ide-sample.ts`). - `` is UI SDK's tree: folders first, marks from `status` (in words after each name), New file and New folder (`creatable`), Rename with F2 (`renamable`), Move (`movable`), Delete with the Delete key after a confirm (`deletable`). `create`, `rename` and `move` carry `refuse(message)`, which keeps the dialog open with your reason. A new or renamed item takes focus; a deleted one hands it to its neighbor. An agent's choice of a file opens it. - `` is UI SDK's tabs. `renderPanel(path)` fills a file's panel once and it stays while the tab is open, so an editor keeps its caret, scroll and undo. Delete on a tab, a middle click or its × fires `close`; once the file leaves `files`, focus goes to the tab that took over, or the empty pane. - `` is UI SDK's textarea (agents read and write it) with line numbers and colors behind the text. Tab and Shift+Tab indent, Escape then Tab leaves, Enter keeps the indent, F8 and Shift+F8 walk `problems`, and its own undo keeps a run of typing as one step and a change from outside (setting `value`) as another. `valuechange` and `cursorchange` report typing and the caret; `goTo(line, col)` moves it. Text inside the element is its first value. - `` opens files with ⌘P (Ctrl+P), commands after `>` with ⌘⇧P, and lines after `:`, on UI SDK's command in a dialog. Escape goes back where you were. - `` lists `problems` worst first and fires `choose`; `` is the labeled strip for your items. ## Agent Console ```html Rename the project ``` ```ts import { createAgent } from "@ui-sdk/web/agent" import { createHistory } from "@ui-sdk/web/history" import { createApprovals } from "@/components/ui/agent/approvals" const approvals = createApprovals() const agent = createAgent({ allow: approvals.allow }) approvalsEl.approvals = approvals activityEl.history = createHistory(agent, { within: () => settings }) snapshotEl.agent = agent ``` - The console is a labeled region with the agent's status (`idle`, `working`, `waiting`) in words, and it's hidden from agents, so they never act on their own console. - `` is one step of a plan: running, done, failed, or "You said no". - `` shows each pending approval as a card with Allow and Don't allow. Steps on controls marked `data-agent-confirm` always ask. The cards go to the top layer, above a modal the person has open, and stay usable while it is. - `` lists what the agent changed, newest first: Show outlines the control, and Undo agent changes puts back what it changed and leaves what people changed. - `` is a table of what the agent can see: each control, its kind, its state and its actions. `web/agent.html` runs a stand-in model (the gallery's planner) against project settings, so the whole loop runs without an API key. ## Terminal ```html ``` ```ts shell.commands = { echo: { description: "Prints its words", usage: "", run: (args) => args.join(" ") }, build: { description: "Builds, with a progress bar", async *run(_args, io) { for (let p = 0; p <= 100; p += 20) yield `\r${"█".repeat(p / 10).padEnd(10, "░")} ${p}%` yield "\n\x1b[32m✓\x1b[0m Built\n" }, }, } shell.history = ["ls"] // Up brings these back first shell.handle = (line, io) => socket.send(line) // lines no command matched shell.api.print("from the server\n") // output from outside, like a socket ``` - It runs on the React Terminal's own session (`terminal/session.ts`) and screen (`terminal/ansi.ts`): a command prints, returns text, streams an async iterable (a progress bar redraws its line with `\r`), or throws (shown in the danger tone). `help`, `clear` and `history` come built in. - Up and Down bring back earlier commands, Tab completes a command or (with a command's `complete`) its argument, and a second Tab lists several matches. Ctrl+C stops a running command (the prompt comes back at once), and with text selected it copies. Ctrl+L clears. - Output keeps ANSI colors, readable in every skin and theme, and links (OSC 8 and plain http addresses). The log reads out new lines only, and old lines go only while you're following the end, past `scrollback` lines. "New output below" appears when you've scrolled up. - `shell.api` has `run`, `clear`, `print` and `error`. Agents run commands and read the last lines; while a command runs, they're told to wait. ## Charts ```html ``` ```ts Object.assign(document.getElementById("revenue"), { data: months, x: (d) => d.month, series: [ { key: "revenue", label: "Revenue", value: (d) => d.revenue }, { key: "costs", label: "Costs", value: (d) => d.costs }, ], format: (n) => `$${n / 1000}k`, }) ``` - Each chart's geometry lives in `registry/uiset/ui/chart/plots/` as plain functions that return nodes; the React charts and these elements draw the same nodes, so they can't drift. - The title is `label` (a `title` attribute would give the whole chart the browser's tooltip). `description` and `height` are attributes too. The options React takes as props are properties here (`data`, `x`, `series`, `format`, …), or attributes where they're words: `layout` on line charts; `orientation`, `layout` and `sort` on bar charts; `x-label` and `y-label` on scatter charts; `measure` on heatmaps; `variant` on segmented bars; `locale` on all. Every attribute has a property that reflects it, so `chart.orientation = "horizontal"` works too. - A scatter chart's point names come from `pointLabel` (React's `label`), since `label` is the title on every chart. - Setting properties redraws once, on the next microtask, however many you set together. Properties set before the element is defined are picked up when it is. - Until its required options arrive, a chart shows its title and nothing else, with `aria-busy` on its figure. - Every chart is a listbox of its data positions: arrows move (by row in a heatmap), the tooltip and crosshair follow focus or the pointer (the nearest point, on a scatter chart), and Enter pins one. `selected` is live. `select` fires with `{ value, source, reason }`: `source` is `keyboard`, `pointer` or `agent`, and `preventDefault()` refuses the pin. When new data drops the pinned position, `select` fires once with `value: null`, `source: "data"`, `reason: "removed"`. - `` needs no script: `values` is comma-separated (an empty place is a gap), and its name says where the values went ("from 4 to 8, low 4, high 8"). `` says its change in words ("Up 12%") as well as color; `good="false"` marks a rise as bad. Both take `values`/`trend` and `format` as properties too. - The legend isolates a series; Chart and Table switch to the table twin. Agents `select` by reading ("Mar"), `isolate` and `showAll` series, and `showTable`, as in React. With the table showing, the switch is how an agent gets back to the chart. ## Data Grid ```html ``` ```ts grid.columns = [ { id: "name", header: "Name", value: (c) => c.name, pinned: true, editable: true, hideable: false }, { id: "plan", header: "Plan", value: (c) => c.plan, options: ["Free", "Pro", "Team"], editable: true }, { id: "seats", header: "Seats", type: "number", value: (c) => c.seats, editable: true }, { id: "renews", header: "Renews", type: "date", value: (c) => c.renews, text: (c) => day.format(c.renews) }, { id: "active", header: "Active", type: "boolean", value: (c) => c.active, editable: true }, ] grid.getRowId = (c) => c.id grid.rows = customers grid.addEventListener("celledit", (e) => { const { rowId, column, value } = e.detail // value is parsed: a number, a Date, a listed option if (!allowed(value)) return e.preventDefault() // refused: the editor stays open save(rowId, column, value) }) grid.addEventListener("rowaction", (e) => open(e.detail.row)) ``` - Columns are the React grid's (`type`, `options`, `width`, `minWidth`, `align`, `sortable`, `filterable`, `groupable`, `editable`, `pinned`, `hidden`, `hideable`), and `render(row)` returns a Node or text. Both share `data-grid/columns.ts` and `data-grid/view.ts`. - Attributes: `label`, `locale`, `selectable`, `editable` (with `celledit` listening), `height`, `empty`, `persist` (a localStorage key for the layout), `density`, `toolbar` and `filename` (for Export CSV). - State is live and reported: `selected` (selectedchange), `sort` (sortchange), `query` (querychange), `filters` (filterschange), `groupBy` (groupbychange), `columnState` (columnstatechange) and `density` (densitychange). Setting one fires no event; a person's or agent's change does, and `preventDefault()` cancels it: a canceled change isn't announced, doesn't move focus, and fails the agent step that asked for it. - `rows`, `columns`, `getRowId` and the state above can be set before the element is defined; they're picked up when it is. - Everything the keyboard does in React works here: one tab stop, arrows, Page Up and Down, Control+Home and End, Shift to select a range, copy and paste, Enter or typing to edit, Space to check, Shift+F10 for the column menu, Alt+arrows to resize, Alt+Shift+arrows to move. Only the rows near the view are drawn, in order. When rows or columns go (a search, a hidden column), the active cell moves to the nearest one left, so the grid keeps its tab stop. An edit left open with text that doesn't fit gives way when another cell starts one. - Header cells, their menus and the toolbar's tools are kept as the grid redraws: an open column menu stays open (Wider and Narrower keep it open), and closing it returns focus to its header. Double-clicking a resize edge resets the width. A redraw only touches the cells whose row, column or text changed, and a resize drag only changes widths. - `toolbar` adds search (or the selection, with Clear selection), column filters, grouping, the View menu (columns and density) and Export CSV. Each tool shows the grid's state however it changed, and follows `label` and header changes. `grid.exportCsv(name)` and `grid.focusGrid()` work from code. - Agents `sortBy`, `filter`, `filterColumn`, `selectRows`, `clearSelection`, `editCell`, `groupBy` and `showColumn` by the names people see, with the same checks. ## Chat ```html ``` ```ts // With AI SDK's useChat (Vue, Svelte or plain JavaScript), its state goes straight in: chat.messages = messages // [{ id, role, parts: [{ type: "text", text }, …] }] chat.status = status // "ready" | "submitted" | "streaming" | "error" composer.toggleAttribute("running", status === "submitted" || status === "streaming") composer.addEventListener("send", (e) => sendMessage({ text: e.detail.text, files: e.detail.files })) composer.addEventListener("stop", () => stop()) chat.addEventListener("retry", () => regenerate()) ``` - Parts drawn: `text` (markdown, with GitHub tables, task lists and code blocks highlighted by shiki), `reasoning` (open while it streams, then "Thought for 2s"), tool calls (`tool-*` and `dynamic-tool`, with their state, input, output or error), `source-url` (gathered into "Used 3 sources"), `file` (images shown, others linked) and UISet's own `tasks` (`{ type: "tasks", title, tasks: [{ id, title, status }] }`). - Raw HTML in markdown is dropped, never rendered: model output is untrusted. While a text part streams, half-written markdown is completed (remend) so it doesn't flicker. - The markdown parser is remark, as React's response uses, and loads only when a chat first draws an answer. - Under each answer: Copy, Retry (on the last answer, firing `retry`) and Good or Bad (`feedbackchange` with `{ value, id }`; `chat.feedback` is live). With `editable`, a user's message has Edit (`edit`). A tool call in `approval-requested` shows Allow and Deny and fires `approve` with `{ value, id, part }`; Allow carries `data-agent-confirm`. - `suggestions` show in the empty state, and a click fires `suggestion`. - The log follows new text unless you've scrolled up, then offers Jump to latest. - `` sends on Enter (Shift+Enter for a new line), shows Stop while `running`, brings back what you sent with Up and Down, takes files with `attachments` (its value is the accept list), and suggests `commands` after "/" and `mentions` after "@". Its event is `send`, not `submit`, which its own form's native event would share. - `` takes `threads` (`{ id, title, updatedAt? }`) and `value`. Choosing a chat sets `value` and fires `valuechange` (cancelable; not `select`, which a text field's native event shares). It also fires `new`, `rename` with `{ value, title }` and `delete` (after the person confirms in an alert dialog). - The composer's children go in its toolbar, beside Send. `` takes `models` (`{ value, label, description? }`) and fires `valuechange`; `` is a meter that reads "1.2K of 200K tokens" and turns amber past 70% and red past 90%. ```html ``` ## Rich text ```html ``` ```ts notes.mentions = [{ value: "ada", label: "Ada Lovelace" }] notes.uploadImage = async (file) => ({ src: await upload(file), alt: file.name }) notes.addEventListener("valuechange", (e) => { save(e.detail.value) // Markdown preview.doc = e.detail.doc // the document, drawn read-only }) ``` - The same editor as React's `RichTextEditor`: the toolbar is `` with ``s, a `` for the block type and a `` for links; "/" and "@" are ``. The keys, Markdown shortcuts, paste rules and HTML output are the ones in [Rich text](./rich-text.md). - `value` (attribute or property) is Markdown; `doc` is the document. `valuechange` fires with `{ value, doc }` for edits, not caret moves; `submit` fires on Mod+Enter outside a to-do. - `commands` replaces what "/" offers (`[]` turns it off), with the same shape as React's. `toolbar="false"` leaves the toolbar out; `readonly` hides it and refuses edits; `disabled` dims it too. - With `name`, a hidden field carries the HTML, or Markdown with `format="markdown"`. - Agents see ``, the editing surface, named by `label`, with `setValue`, `insert` and `clear` taking Markdown. - `` (or its `doc` property) draws saved text with the editor's styles, from the document, never from an HTML string. ## App Shell ```html Acme
…search, account…
…the page…
``` - `` lays out its children by `data-part`: `brand` at the top of the sidebar, `sidebar` under it, `header` in the header, and the rest in a `
` (or a `
` with `content="div"`, when the page has its own). - Mod+B (or the toggle) collapses the sidebar to icons and back, remembered in the same storage as the React AppShell (`uiset-sidebar`), and each link then gets a tooltip. Below `narrow-below` (48rem by default) the sidebar moves into a drawer opened from the header. - `` is the page's `h1` (or `h2`) with your actions as children. `` says what to do when there's nothing yet, with its actions as children. On both, `title` names the page or state; it isn't a tooltip. ## Page templates Every React page template is a web page too, in `web/templates/`, built only from UI SDK's elements and UISet's. Each one's sample data is a framework-free module the React template imports as well (`registry/uiset/ui/templates/*-sample.ts`), so the two show the same thing. | Page | What it shows | | ---- | ------------- | | `auth.html` | Sign in, sign up, forgot and reset password, verify a code: browser checks, server messages, a strength meter | | `pricing.html` | Plans with a monthly or yearly switch, a comparison table and questions | | `dashboard.html` | A date range that rescopes the figures, charts and recent orders, in the App Shell | | `admin.html` | Records with search, filters and export; create and edit in a drawer; confirmed bulk delete | | `settings.html` | Profile, security, notifications, billing with usage and invoices, team, and a danger zone | | `inbox.html` | Folders, a message list you arrow through, the message with star, archive, delete and Undo, and compose | | `chat-app.html` | Chats, the conversation and a composer with a model picker and usage, streaming from a stand-in model | | `project-board.html` | Sprint progress and the issues as a board or a list, with search, a person filter, card details, a new-issue form and Undo | | `calendar-app.html` | A day, a week (`uiset-schedule`) or a month (`uiset-schedule-month`), a small month that jumps to a day, calendars to show or hide, and what's next | | `../ide.html` | The IDE in resizable panes | The chat app streams from `sampleAssistant` (`templates/chat-app-sample.ts`), an async generator of events (text, reasoning, tool calls, sources). Swap in your model there; the page turns its events into the chat's message parts, the same shape AI SDK's `useChat` holds, so `useChat`'s `messages` and `status` can go into `` directly instead. ## Vue Tell Vue the tags are custom elements, then use them in templates: ```ts // vite.config.ts vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("ui-") || tag.startsWith("uiset-") } } }) ``` ```vue … ``` Or use UI SDK's Vue adapter (`@ui-sdk/vue`) for its components and load only `uiset.css`: the same `data-slot` attributes mean the same styles. ## Svelte and Solid Both render custom elements as they are. Listen with `onvaluechange` in Svelte 5, or `on:valuechange` in Solid. As with Vue, UI SDK's own adapters work with `uiset.css` too. ## React and the web Every kit is an element set, and every page template is a web page: Studio, Mobile, Board, Scheduler, Canvas, the IDE, the Agent Console, the Terminal, Charts with the sparkline and stat, the Data Grid, Chat and the App Shell, and the auth, pricing, dashboard, admin, settings, inbox, chat app and IDE pages. The rest of the registry wraps UI SDK's own components, which are already elements here (`ui-*`, dressed by `uiset.css`); UISet's pieces without behavior (badge, alert, card, item and description lists, breadcrumbs, skeleton, spinner, kbd) are classes in the same stylesheet. The three small parts with behavior of their own are elements too: ```html ``` - `` fires `valuechange` with `{ value }`; `shortcut` ("/" by default, `shortcut=""` for none) focuses it from anywhere, through UI SDK's shared shortcuts, and shows as a key hint until something is typed. Clear empties it and keeps focus. - `` copies `value`, announces "Copied" and shows it for a moment. - `` takes `steps` (`{ value, label, description? }`) and `value`; choosing a step sets `value` and fires `valuechange`. With `linear` (on unless `linear="false"`), later steps wait until the current one is reached. ## Tests `tests/e2e/web/` runs against plain HTML with no React on the page: axe in all three skins and both themes, the root's attributes, skins restyling UI SDK's elements, the knob and swipe rows by keyboard, the patch bay by keyboard, by script and by an agent, the Studio instruments, a UI SDK dialog's focus, and the built files themselves. `mobile.spec.ts`, `board.spec.ts`, `scheduler.spec.ts`, `canvas.spec.ts`, `ide.spec.ts`, `agent-console.spec.ts`, `terminal.spec.ts`, `charts.spec.ts`, `data-grid.spec.ts` and `chat.spec.ts` cover those kits' pages, and `templates.spec.ts` the page templates (`web/mobile.html`, `web/board.html`, `web/schedule.html`, `web/canvas.html`, `web/ide.html`, `web/agent.html`, `web/terminal.html`, `web/charts.html`, `web/data-grid.html`, `web/chat.html`) the same way: axe in all six skin and theme pairs, keyboard, pointer, events, script and agents. # Templates: design plan Step 5 of the owner's plan. A template is a whole page built only from UISet items, so a developer starts from a working screen instead of a blank one, and every piece of it stays the same component they'd use by hand. Install one, render it, replace the sample data with yours. ## The set (complete-set rule) What a product team ships before anything domain-specific: | Template | Screens | Built from | | ------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------- | | `auth` | Sign in, sign up, forgot password, reset password, verify code, and a layout | Field, Input, PasswordInput, OtpInput, Checkbox, Button, Card, Alert | | `dashboard` | KPI row with trends, filter row (date range, segment), charts, recent activity | AppShell, Stat, LineChart, BarChart, SegmentedBar, DataGrid, Select, SegmentedControl | | `settings` | Profile, account and security, notifications, billing with usage, team, danger zone | AppShell, Tabs, Field, Switch, Meter, DataGrid, Dialog, AlertDialog, Toast | | `admin` | A records list with create, edit in a drawer, bulk actions and confirmed delete | DataGrid, Drawer, Field, Select, AlertDialog, Toast, EmptyState | | `inbox` | Folders, a message list, a reading pane, compose | AppShell, Listbox, Badge, Dialog, Textarea, TokenInput, Toast | | `chat-app` | The chat kit as a full page: chats, thread, composer | Chat kit | | `pricing` | A landing section: plans with a monthly/yearly switch, comparison, FAQ | SegmentedControl, Card, Accordion, Badge | | `project-board` | A project's issues as a board or a list: sprint progress, filters, details, new issue | AppShell, PageHeader, Meter, Board kit, DataGrid, SegmentedControl, Dialog, Toast | | `calendar-app` | Day, week and month views, a small month to jump, calendars to show or hide, what's next | AppShell, Scheduler kit, Calendar, Checkbox, Tooltip, Toast | The project board and the calendar came with the Board and Scheduler kits (step 6). ## Rules for every template - **Only UISet items.** No new behavior lives in a template; if a page needs one, it becomes an item first. - **Sample data in one place** at the top of the file, typed, so replacing it is one edit. Handlers are props (`onSignIn`, `onSave`, `onDelete`) with sample defaults. - **Responsive** down to 360px, with the shell's drawer below 48rem. - **Agent-operable:** every control has a specific name (no two "Save" buttons on one page), destructive actions carry `data-agent-confirm`, secrets carry `data-agent-secret`. - **Forms** validate on submit with UI SDK's Field errors, keep what was typed, and focus the first problem. Auth fields carry the right `autocomplete` (`inputHints`). - **Every skin and theme** passes axe; each template has a keyboard-only e2e path. - Templates are client components whose data arrives as props, so a Server Component (or a loader) can fetch it and pass it in. ## Delivery Each template is a registry block whose files install under `components/ui/templates/`, importing UISet items relatively, like every other item. The site shows each one live at `/templates`, in the chosen skin, with its install command. ## Use ```bash npx shadcn@latest add superzero11/uiset/dashboard ``` Each template installs to `components/ui/templates/.tsx` with every item it uses. Render it, then replace its sample data: | Template | Render | Plug in | | ----------- | ------------------------------------------ | ---------------------------------------------------------------- | | `auth` | `` and the other four forms | Each handler returns nothing on success or a message to show | | `dashboard` | `` | Every figure, chart and row comes from `orders`; ranges end at `now` | | `settings` | ` …} onAction={…} />` | Each form's `FormData` by section; `onAction` for change plan, add seats, update card, change photo | | `admin` | `` | Reject to keep the drawer open with a message | | `inbox` | `` | Sending and folder moves go to your mail API | | `chat-app` | `` | An async generator of `text`, `reasoning`, `tool` and `sources` events | | `pricing` | `` | Plans, comparison rows and questions are props; "save n%" comes from the plans | | `project-board` | `` | Both views share the cards and lanes; the last column counts as done; `onCreate` and `onDelete` too | | `calendar-app` | `` | Each event names its calendar, and the calendar's chart slot is its color; times are minutes in `timeZone` | - Mount `` (from `toast`) once in your app; templates call `toast()`. - Inside a page that already has a `
`, pass `contentElement="div"`. - **Cascade layers:** UISet's styles live in layers, so any unlayered CSS in your app wins over them. A global `a { color: … }` would recolor a button rendered as a link; scope such rules to plain links, like `a:not([data-slot])`. ### Connecting a model to `chat-app` ```tsx async function* assistant({ text, history, signal }: AssistantInput) { const response = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ text, history }), signal, }) const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader() for (;;) { const { value, done } = await reader.read() if (done) return yield { type: "text", delta: value } as const } } ``` With the AI SDK's UI stream, map its parts the same way: text deltas to `text`, reasoning to `reasoning`, tool calls and results to `tool` (same `id` updates the card), and source parts to `sources`. ## Checked Every template passes axe in Base (light and dark), Blueprint and Studio, fits a 375px phone without sideways scrolling, and has a keyboard path in the e2e suite. Agents drive them by the names people see: the dashboard's date range, the customers grid, the inbox list, the issue board, the calendar. - **Project board:** the board stays mounted while the list shows, so the card dialog moves cards through it from either view and column limits hold. Each move offers Undo for that card alone. - **Calendar:** `EventDialog` has no calendar field, so a new event goes on the first calendar that's shown. While the sidebar shows, its New event is the page's one; the toolbar's takes over once the sidebar moves into the drawer, so no two buttons share a name. # Theming How UISet looks comes from a skin: CSS variables for color, type, edges, sound and motion. Behavior (keys, focus, screen readers, agents) never changes with it. ```bash npx shadcn@latest add superzero11/uiset/uiset ``` ## The root ```tsx import { UISet } from "@/components/ui/uiset" ``` | Prop | Default | What it does | | ---------- | ---------- | ----------------------------------------------------------------------------- | | `skin` | `"base"` | `"base"`, `"blueprint"`, `"studio"`, `"phosphor"` or `"ink"` | | `keyColor` | none | `{ l, c, h }` in OKLCH: the base skin's accent | | `theme` | `"system"` | `"light"`, `"dark"` or `"system"` (follows the device) | | `sound` | `false` | Short synthesized sounds on interaction, voiced per skin | | `haptics` | `true` | A tap on Android, and the system haptic on iOS 18 Safari, for switches and the like | Every UISet item and every UI SDK component inside takes the skin, popups included: UI SDK renders them in place, in the browser's top layer, so they inherit it. Web Components use `` the same way. ## Skins | Skin | Look | Scheme | Faces to load | | ----------- | ----------------------------------------------------------------------- | ------ | ------------------------------- | | `base` | Your shadcn theme, or one built from `keyColor` | both | none (your app's) | | `blueprint` | A technical drawing: cyanotype paper, linework, hatching | dark | Barlow Semi Condensed | | `studio` | A synth rack: gunmetal panels, rubber pads, amber lamps | dark | Saira Semi Condensed | | `phosphor` | A green-screen terminal: intensity, inverse video, faint scan lines | dark | Spline Sans Mono | | `ink` | E-paper: ink on grey, dotted greys, a reading serif, no motion at all | light | Literata, Atkinson Hyperlegible | Each skin falls back to system faces when its own aren't loaded; load them from Google Fonts or self-host them for the intended look. The themed skins have one scheme, so `theme` doesn't change them. The design notes for each are in `docs/studio.md`, `docs/phosphor.md` and `docs/ink.md`. Every skin is checked in every component and kit, in each scheme it has: text 4.5:1, control edges and focus 3:1 (axe in the test suite), chart colors told apart with color blindness (`tests/unit/chart-palette.test.ts`). ## The key color The base skin builds its accent from one OKLCH color: solid buttons, checked controls, the selected day, focus rings. Text on it picks black or white by lightness, and dark mode lifts the ring toward white so it stays visible. ### A whole theme from one color The [theme generator](https://uiset.dev/themes/generator) goes further than a key color. From one color, a neutral tint, a radius, a density and a text size, it builds every token for light and dark: surfaces, text, edges, focus, the four status tones and eight chart colors. It checks 32 pairs in each scheme (text 4.5:1, controls and focus 3:1, chart colors 3:1 and apart under color blindness), moves any derived color that fails to the nearest lightness that passes, and offers a one-click fix when the key itself fails. It exports the result three ways: a UISet skin file (use it with `skin="custom"`, or the name you give it), shadcn/ui variables for `:root` and `.dark`, and W3C design tokens. The address keeps every setting, so a link reopens the same theme. ### With a shadcn/ui theme With no `keyColor`, the base skin adopts the shadcn theme already on the page: `--background`, `--foreground`, `--muted`, `--muted-foreground`, `--border`, `--input`, `--primary` and `--radius`. UISet components then match the rest of your app, and your theme switcher keeps working. ## Dark mode `theme="system"` follows the device; `"light"` and `"dark"` pin it for the subtree. The base skin's colors use `light-dark()`, so a light section inside a dark page is just another ``. ## Nesting Skins only set variables, so the nearest root wins. A Blueprint panel inside a Base app: ```tsx ``` ## Your own parts Style your own markup with the variables, and it follows every skin: | Variable | What it is | | ------------------------------------------ | ----------------------------------------------- | | `--ui-background`, `--ui-foreground` | The surface and its text | | `--ui-muted`, `--ui-muted-foreground` | Quiet surfaces, and secondary text (4.5:1) | | `--ui-border`, `--ui-input` | Hairlines, and control edges (3:1) | | `--ui-primary`, `--ui-primary-foreground` | The accent and text on it | | `--ui-ring-border` | Focus rings | | `--ui-radius`, `--ui-font-sans`, `--ui-font-mono` | Corners and faces | | `--tone-info`, `--tone-success`, `--tone-warning`, `--tone-danger` | Status colors, readable as text | | `--chart-1` … `--chart-8`, `--chart-seq-1` … `--chart-seq-7` | Data colors, in a fixed order | ## Writing a skin A skin is a stylesheet in the `uiset.skin` layer that sets variables on its own selector: ```css @layer ui-sdk, uiset.tokens, uiset.base, uiset.skin; @layer uiset.skin { [data-uiset][data-skin="harbor"] { color-scheme: light; --ui-background: oklch(0.97 0.01 220); --ui-foreground: oklch(0.25 0.03 230); --ui-primary: oklch(0.45 0.12 230); --ui-primary-foreground: white; /* …and the rest of the table above */ } } ``` Then ``. Keep the contrast rules (text 4.5:1, edges and focus 3:1), and check it the way UISet's skins are checked: add it to the skin loops in `tests/e2e/components.spec.ts` and run axe. Decoration a variable can't express (a texture, a bevel) may use selectors under the same `[data-uiset][data-skin]`, so nested skins never inherit it. ## Sound and haptics `sound` turns on short sounds for presses, toggles, selection and errors, made with Web Audio (no files). Each skin picks a voice (`--uiset-sound`: `soft`, `mechanical`, `beep` or `paper`); `--uiset-sound-volume` sets the level. Sounds follow UI SDK's own events, so they play for agents' changes too. Haptics are on by default where the device has them. ## Motion and system settings UISet's motion stops under `prefers-reduced-motion`; Ink has none at all. In Windows' forced colors, state that relied on fills becomes borders, and Phosphor drops its bloom and scan lines. # Coming from shadcn/ui UISet installs with the same CLI into the same folder, and its base skin adopts your shadcn theme. What changes is underneath: components are UI SDK's, so keyboard, focus, screen reader and agent behavior come built in, and the API follows UI SDK's. ## Moving an app over 1. **Commit first.** UISet items have the same file names as shadcn's (`components/ui/button.tsx`), and the CLI asks before it overwrites one. 2. **Add the root** and wrap the app. Your theme variables are picked up as they are: ```bash npx shadcn@latest add superzero11/uiset/uiset ``` ```tsx ``` 3. **Replace components one at a time**, starting with leaves (button, badge, input) and then the composite ones (dialog, select, menu). Each item is independent, so the app keeps working between steps. 4. **Run your tests.** Selectors by role and name keep working; selectors on Radix's `data-state` need the UI SDK attributes below. ## What maps to what | shadcn/ui | UISet item | Notes | | -------------------- | ----------------- | --------------------------------------------------------------------- | | Button | `button` | `variant`: `solid` (default), `outline`, `ghost` | | Input, Label, Form | `field` | `Field`, `FieldLabel`, `Input`, `FieldDescription`, `FieldError`; native forms, no form library needed | | Select | `select` | `SelectTrigger` shows the chosen value by itself; `SelectLabel` names it | | DropdownMenu | `menu` | `Menu`, `MenuTrigger`, `MenuContent`, `MenuItem`; handle `onAction` on the menu | | Dialog | `dialog` | `Dialog`, `DialogTrigger`, `DialogContent`, `DialogTitle`, `DialogClose`; no Header or Footer parts, use your own markup | | AlertDialog | `alert-dialog` | Same parts with an `AlertDialog` prefix | | Sheet | `drawer` | `Drawer`, with snap points and a handle | | Popover, Tooltip, HoverCard | `popover`, `tooltip`, `hover-card` | Same idea, UI SDK's parts | | Tabs | `tabs` | `TabsTab` for TabsTrigger, `TabsPanel` for TabsContent | | Accordion | `accordion` | `AccordionItem`, `AccordionTrigger`, `AccordionPanel` for AccordionContent | | Command | `command` | `CommandInput`, `CommandList`, `CommandItem`, `CommandEmpty` | | Toast (Sonner) | `toast` | `toast("Saved", { description })`, with a `ToastRegion` in the app | | Checkbox, Switch, Slider, RadioGroup | `checkbox`, `switch`, `slider`, `radio` | Same use; Slider takes a number or an array | | Calendar, DatePicker | `calendar`, `date-picker` | Plus `date-input` and `time-input` for typed dates | | Table, DataTable | `data-grid` | Sorting, filters, grouping, pinning, editing and virtual rows built in | | Card, Badge, Alert, Skeleton, Breadcrumb | `card`, `badge`, `alert`, `skeleton`, `breadcrumbs` | See [App frame and data display](./app-shell.md) | | Sidebar | `app-shell` | A sidebar that collapses to icons and becomes a drawer on phones | | Chart (Recharts) | `line-chart`, `bar-chart`, … | No chart library: keyboard-explorable, with a table view | Things shadcn/ui doesn't have: the [chat kit](./chat.md), a [rich text editor](./rich-text.md), [board](./board.md), [scheduler](./scheduler.md), [canvas](./canvas.md), [IDE](./ide.md), [terminal](./terminal.md), the [Agent Console](./agent-console.md) and whole-page [templates](./templates.md). ## API differences - **`asChild` becomes `render`.** `` is ``. - **State attributes.** Radix's `data-state="open"` and `data-state="checked"` are `data-open` and `data-checked` (also `data-pressed`, `data-highlighted`, `data-disabled`). Every part has a `data-slot`, like `data-slot="dialog-content"`. - **Change handlers** are `onValueChange`, `onOpenChange`, `onCheckedChange` and `onPressedChange`, with a second `details` argument that says why it changed (a click, a key, an agent) and can cancel it. - **Styling.** UISet ships plain CSS in cascade layers, not Tailwind classes. Tailwind keeps working beside it: put a class on any part, and since your utilities aren't in UISet's layers, they win. - **Names for agents.** UI SDK's agent bridge finds controls by their accessible names. A control with a visible label is already named; an icon button needs `aria-label`. ## Theme Nothing to change: the base skin reads `--background`, `--foreground`, `--muted`, `--muted-foreground`, `--border`, `--input`, `--primary` and `--radius`. To set the accent without a shadcn theme, pass `keyColor`, or try one in the [theme generator](https://uiset.dev/themes/generator), which also exports shadcn/ui variables. See [Theming](./theming.md). # Roadmap UISet grows with UI SDK. This page says what's done, what's next, and what we won't do. ## Shipped - **The root and five skins**: Base (adopts your shadcn theme, or one key color), Blueprint, Studio, Phosphor and Ink, with sound and haptics. - **All 55 UI SDK components**, styled, each its own item; and 16 app frame and data display parts. - **Twelve kits**: chat, rich text, data grid, charts, board, scheduler, canvas, IDE, mobile, terminal, Studio and the Agent Console. - **Ten page templates**: auth, dashboard, settings, admin, inbox, chat app, project board, calendar, pricing and IDE. - **Web Components**: every kit and template as custom elements. - **For coding agents**: skills, llms.txt and Markdown docs. - **The theme generator**: one color in, a complete, checked theme out. ## Next - **More skins.** Cockpit (guarded switches, gauges), Paper (cards, tape, corkboard) and Print (brutalist type), each with its own sound. - **Framework guides.** Step-by-step setups for Vue, Svelte, Solid and Angular with the Web Components build. - **More templates.** Onboarding, an agent-wired settings console, and a docs site. - **Listing.** The shadcn registry directory, so items install as `@uiset/`. ## Following UI SDK UI SDK is in public beta. UISet pins its exact version and moves with each release after the full test suite passes in every skin. Problems we find in UI SDK go upstream as issues, with a test that shows them. ## What we won't do - Copy the look of a real product or brand. Every skin is an original design. - Put a second UI library underneath. Behavior always comes from UI SDK. - Use "Vercel" in a product name. UISet is a community project, not affiliated with Vercel. ## Asking for things Open an issue on [GitHub](https://github.com/superzero11/uiset/issues) with what you're building and what's missing. Kits and templates that many people need come first. # Blueprint A technical drawing: cyanotype paper, white linework, hatching for filled states, dimension lines for slider tracks, and lettering for labels. It was the first themed skin, built to prove that a skin can change everything about how UI SDK's components look while changing nothing about how they behave. ```tsx … ``` ## Tokens | Name | Value | Role | | ------ | ----------------------------- | ------------------------------------------------------ | | Paper | `oklch(0.36 0.11 254)` | The page: a deep cyanotype blue | | Line | `oklch(0.96 0.02 230)` | Text, borders, fills: drawn in white | | Faint | Line at 16% | Hover fills and the fine grid | | Grid | Line at 7% | The 8px and 64px drawing grid on the paper | | Ring | `oklch(0.9 0.13 200)` | Focus: a cyan line, distinct from the white drawing | **Type.** Barlow Semi Condensed; labels and buttons in spaced capitals, like drafting lettering. **Drawing language.** - Text boxes are a line to write on, not a box. - Checked and filled states are hatched, not filled solid: a switch's track, a slider's range. - Slider tracks are dimension lines with end ticks; the thumb is a diamond. - Popups are a sheet laid on the drawing, with an offset shadow line. - What isn't reached yet (an empty star, an upcoming step) is drawn faint. **Scheme.** Dark only: `theme` doesn't change it. **Contrast.** White linework on the paper clears 4.5:1 for text and 3:1 for every control edge; the chart colors are a set chosen for the blue paper and checked for color blindness (neighbors CVD ΔE ≥ 14.6). # Studio: design plan **Subject.** A rack of synthesizer modules on a studio desk: painted metal faceplates, rubber knob caps, silk-screened legends, amber indicator lamps and patch cables. **Audience.** Developers building music, audio and creative tools, and anyone who wants an interface that feels like hardware. **Job.** The demo that makes the skin system obvious: the same UI SDK components, turned into instruments you can hear. Not SPEC-01 (light molded pocket instruments, LCDs, orange-red keys). Studio is darker, rack-mounted, metal and rubber, with color reserved for cables and lamps. ## Tokens | Name | Value | Role | | ------ | ------------------------------- | --------------------------------------- | | Rack | `oklch(0.25 0.01 255)` | Rails and the gaps between modules | | Panel | `oklch(0.34 0.012 255)` | Module faceplates (gunmetal, not black) | | Legend | `oklch(0.94 0.012 85)` | Silk-screened text and scales | | Rubber | `oklch(0.19 0.006 255)` | Knob caps, pads | | Lamp | `oklch(0.82 0.15 72)` | The key color: lit state, value arcs | | Cables | red, blue, green, amber, violet | Which cable is which | **Type.** Saira Semi Condensed. Legends are small and uppercase because that is how faceplates are printed; readouts use tabular numerals. Body copy stays sentence case. **Layout.** Modules sit side by side on a rack, each with screws at the corners and its name at the top. Controls on a grid, jacks along the bottom, cables drape over all of it. **Principles.** 1. Spend boldness on the cables and the lamps. Panels stay quiet. 2. Every state a person sets lights something: a switch lamp, a step, a knob arc. 3. Depth is a 1px top highlight and a bottom shadow, never a blur haze. Forced colors replaces all of it with borders. 4. Sound is mechanical: thocks for pads, detent clicks for knobs, latches for switches. **Review against generic defaults.** Dark UI with one accent is a common default; here the panel is mid-dark gunmetal, the lamp color comes from real indicator lamps, and the five cable colors carry meaning. Uppercase is limited to printed legends. # Phosphor: design plan **Subject.** A monochrome video terminal on a lab bench: a curved glass tube with green phosphor glyphs, a faint bloom around bright text, scan lines you only notice up close, a block cursor, and boxes drawn with line characters. **Audience.** Developers building developer tools: CLIs in the browser, infrastructure consoles, logs, agents, and anyone who wants their app to feel like a terminal. **Job.** The skin that makes developers share a screenshot, and the natural partner of the Terminal kit. Named Phosphor, not Terminal, so the skin never gets confused with the Terminal kit. ## Tokens | Name | Value | Role | | ------- | ------------------------- | --------------------------------------------------------- | | Tube | `oklch(0.26 0.035 155)` | The glass: a dark green-grey, never black | | Raster | `oklch(0.3 0.04 155)` | Raised surfaces (popups, cards): a brighter scan area | | Dim | `oklch(0.75 0.11 150)` | Half-intensity text: secondary text, borders, placeholders | | Glow | `oklch(0.88 0.17 148)` | Full-intensity text and the key color | | Inverse | Glow fill, Tube text | Selection, the chosen item, solid buttons | | Signals | P3 amber `oklch(0.84 0.15 75)`, P22 red `oklch(0.75 0.16 28)`, P4 white `oklch(0.95 0.02 150)`, P7 blue `oklch(0.8 0.1 230)` | Data series and status, each a real phosphor type | **Type.** One monospace for everything, because a terminal has one character grid: "Spline Sans Mono", falling back to the platform's monospace. Body 14px with 1.5 line height; headings are the same face at a heavier weight, never uppercase. **Layout.** Everything aligns to the character grid: 0 radius, 1px Dim borders as if drawn with box characters, spacing in multiples of half a line. Popups are a brighter raster area with a Glow border, no shadow. **Principles.** 1. The interface is monochrome. Color is intensity: Dim, Glow, and Inverse video. Only data and status use the other phosphors, so they stand out. 2. A chosen thing is drawn in inverse video: the selected day, the active tab, the checked switch track, the highlighted menu item, solid buttons. 3. Bright text blooms a little (a tight text-shadow on Glow text only). Scan lines are a static, very faint pattern on the background, never on text, and never animated; no flicker, no blinking except the text caret. 4. Focus is a 2px Glow outline offset by 2px, unmissable on the dark tube. 5. Sound is the `beep` voice: short square-wave chirps. 6. Forced colors drops bloom and scan lines; reduced transparency drops scan lines. **Contrast** (computed). Glow on Tube 11.4:1, Dim on Tube 7.2:1, Tube on Glow (inverse) 11.4:1. Dim started at L 0.68 (5.6:1 on the tube); axe found it under 4.5:1 on tinted surfaces (badges, avatar chips, event blocks), so it went up to L 0.75, which clears 4.5:1 on all of them; red went from 0.72 to 0.75 for the danger badge. Borders in Dim clear 3:1. **Charts** (validated, `tests/unit/chart-palette.test.ts`): the tube's green, then blue, P3 amber, P22 red, P4 white, lavender, cyan and pink, each 3:1 or more on the tube; neighbors CVD ΔE ≥ 9.5 and normal ≥ 17, the first six pairwise CVD ≥ 9.4. **As shipped.** Inside inverse video, quiet text and chips reverse too (a menu item's hint, a tab's count). Code uses white for keywords, amber for strings and constants, blue for functions, Dim for comments; the Terminal kit's ANSI colors are the phosphors. **Review against generic defaults.** "Near-black with one acid-green accent" is a known default. Phosphor differs on purpose: the background is a green-grey glass, not near black; green is not an accent on neutral UI but the only ink, in intensity steps; the signature is inverse video for selection, which defaults don't do; and the second colors are named phosphors used only for data. Monospace here is the subject's own type, used for everything rather than for small labels. # Ink: design plan **Subject.** An electronic-paper reader: a matte, light grey screen, crisp black type, greys made of fine dots, and pages that change by redrawing rather than animating. **Audience.** Reading and writing apps, documentation, note-taking and focus tools, and products that want the calmest, highest-contrast interface with no motion at all. **Job.** The first light themed skin, and proof that the system works with no color. Named Ink, after the medium, rather than any e-reader brand. ## Tokens | Name | Value | Role | | ------ | ---------------------- | ----------------------------------------------------------- | | Screen | `oklch(0.95 0 0)` | The paper: a neutral light grey, not white and not cream | | Ink | `oklch(0.2 0 0)` | Text, borders, solid fills: the only "color" | | Grey | `oklch(0.42 0 0)` | Secondary text (7.3:1 on Screen) | | Tint | `oklch(0.88 0 0)` | Quiet fills: hover, muted surfaces, tracks | | Dots | a 2px dot screen of Ink on Screen | Mid-grey fills (progress, chart series, range middles) drawn as dither, the way the medium does it | | Hatch | 45° lines of Ink | A second fill texture, for danger and for a second data series | **Type.** Two faces with distinct jobs. "Literata", a typeface designed for reading on screens, for headings and long text; "Atkinson Hyperlegible", designed for legibility, for controls, labels and data. Body 16px; serif text gets 1.6 line height, controls 1.4. **Layout.** Calm and flat: 1.5px Ink borders on controls, 6px radius, generous padding, no shadows. Popups are Screen with a 2px Ink border, so they read as a sheet laid on top. **Principles.** 1. No color, so meaning never depends on it. Status pairs an icon and a word with a fill pattern: solid for success, dots for warning, hatch for danger. 2. No motion. Every transition and animation duration is zero, whatever the person's motion setting; a change is a redraw. 3. Chosen things are solid Ink with Screen text: the selected day, checked boxes, the active segment, solid buttons. Hover is the Tint fill. 4. Focus is a 3px Ink outline offset by 2px. 5. Sound is the `paper` voice, quiet; haptics stay on where the device has them. 6. Forced colors maps straight across, since the skin already avoids color. **Contrast** (computed). Ink on Screen 15.6:1, Grey on Screen 7.3:1 and on Tint 5.9:1, Screen on Ink (inverse) 15.6:1; Ink borders clear 3:1 easily. **Review against generic defaults.** A light skin with a serif risks the "cream paper, serif display, terracotta accent" default. Ink avoids each part: the screen is a neutral grey, not warm; there is no accent color at all; the serif is a reading face used for reading, paired with a legibility face for controls; and greys are dot screens rather than flat tints. It's flat but not broadsheet: radius stays 6px and there are no hairline column rules. **As shipped** (2026-11-15). - **Data is the one exception to "no color."** The plan drew chart series as dots and hatching, but the chart kit fills marks with a color per series and has no patterns, so a legend could not show which pattern is which. Charts and calendar events take a muted, print-like set instead, like a color e-paper panel: ink, blue, orange, sage, dark red, purple, teal, mustard, each 3:1 or more on the screen; neighbors CVD ΔE ≥ 9.8 and normal ≥ 16, the first six pairwise (`tests/unit/chart-palette.test.ts`). The interface itself stays ink only. - Body text is 15px (0.9375rem), not 16px: at 16px the denser kits (data grid, IDE, scheduler) cramped. Rich text and chat answers get the 1.6 line height. - Status: solid bars for information and success, dots for warnings, hatching for danger, each with its icon and word. A destructive button is framed in a double rule; an invalid field has a dashed edge. - An empty rating star is a screen of dots; a loading skeleton is dots, not a shimmer. - Code is set in ink and grey. Inside a solid (selected) row, quiet text reverses to a light grey. - No motion is set inside the skin, so it holds whatever the motion setting. # App frame and data display The frame an app's pages sit in, and the small parts most pages show. `AppShell` is a sidebar layout that collapses to icons with Mod+B, remembers that, and moves into a drawer on narrow screens. Around it are a page header, breadcrumbs, cards, badges, figures, lists, alerts, loading states, a stepper, a search field and a copy button. Most are plain HTML in UISet's styles; the ones with behavior use UI SDK: Drawer, Tooltip, Preferences and shortcuts for the shell, Input for search, and `announce` for copy. ```bash npx shadcn@latest add superzero11/uiset/app-shell superzero11/uiset/breadcrumbs \ superzero11/uiset/page-header superzero11/uiset/card superzero11/uiset/badge \ superzero11/uiset/stat superzero11/uiset/empty-state superzero11/uiset/skeleton \ superzero11/uiset/description-list superzero11/uiset/item-list superzero11/uiset/alert \ superzero11/uiset/kbd superzero11/uiset/spinner superzero11/uiset/stepper \ superzero11/uiset/search-field superzero11/uiset/copy-button ``` Each is its own item, so add only the ones you use. They share `shell/shell.css`, and the ones with icons bring `shell/icons.tsx`. ## Parts | Part | Item | What it is | | ----------------------------------------------------- | ------------------ | -------------------------------------------------------------- | | `AppShell` | `app-shell` | Sidebar, header and content; icons only with Mod+B; a drawer on narrow screens | | `SidebarNav`, `SidebarGroup`, `SidebarItem` | `app-shell` | The sidebar's links, in titled groups, with icons and badges | | `Breadcrumbs` | `breadcrumbs` | Where you are, the last item marked as the current page | | `PageHeader` | `page-header` | The page's heading, a description and actions | | `Card` and its header, title, description, content and footer | `card` | A bordered block | | `Badge` | `badge` | A short status label in five tones | | `Stat` | `stat` | A figure with its change in words and color, and an optional trend | | `EmptyState` | `empty-state` | What to do when there's nothing here yet | | `Skeleton`, `SkeletonText`, `Loading` | `skeleton` | Placeholder shapes, and a region that says "Loading…" once | | `DescriptionList` | `description-list` | Terms and their details | | `ItemList`, `Item` | `item-list` | Rows with media, a title, a description, meta and actions | | `Alert` | `alert` | An inline message in four tones that names its tone | | `Kbd` | `kbd` | Keys the way this platform writes them: ⌘K or Ctrl+K | | `Spinner` | `spinner` | A loading indicator that says what's loading | | `Stepper` | `stepper` | The steps of a flow, the current one marked, later ones locked | | `SearchField` | `search-field` | A search box with Clear and a key that focuses it from anywhere | | `CopyButton` | `copy-button` | Copies a value, then says Copied | ## Use ```tsx import { Icon } from "@/components/ui/shell/icons" Acme} sidebar={ } current>Overview }>Deployments } header={} > } title="Atlas" description="Production, Europe. Last deployed 4 minutes ago." actions={} /> {deploys.length === 0 && ( Connect a repository} /> )} ``` ### The shell - `sidebar` is required; `brand` sits above it and `header` across the top. `children` go in a `
`; pass `contentElement="div"` when your page already has one. - `label` ("Navigation") names the sidebar, and titles the drawer when there's no `brand`. - When the viewport is `narrowBelow` ("48rem") wide or less, the sidebar moves into a drawer opened by Open navigation in the header. On the server the shell renders the wide layout. - Expanded or collapsed is saved in `localStorage` under `uiset-sidebar` with UI SDK's preferences, and shown as `data-sidebar` on a wrapper. Every shell on the same origin shares it, the web element included. - `SidebarItem` takes `href`, `icon`, `current` (`aria-current="page"`), `badge` and its label as text. While the sidebar shows icons only, each link has a tooltip with its label. `SidebarGroup`'s `title` names its list; `SidebarNav`'s `label` defaults to "Sections". - `--app-shell-height` (100dvh) sets the shell's height and `--app-shell-pad` the content's padding, so a full-bleed page can set it to 0. - `Icon` takes a `name` from `shell/icon-paths.ts`: `home`, `users`, `orders`, `reports`, `settings`, `mail`, `inbox`, `folder`, `file`, `branch`, `star`, `bell` and more. ### Page parts - `Breadcrumbs` takes `items` (`{ label, href? }`) and `label` ("Breadcrumb"). The last item is the current page (`aria-current="page"`) and is never a link. - `PageHeader` takes `title`, `description`, `actions`, `above` (breadcrumbs, say) and `level`: 1 for an `h1` (the default) or 2. - `Card`, `CardHeader`, `CardDescription`, `CardContent` and `CardFooter` take a `div`'s props (a `p`'s for the description). `CardTitle` takes `level` (2, 3 or 4; `h3` by default), `className` and `children` only. - `EmptyState` takes `title`, `description`, `actions`, `icon` (a shell icon name, `inbox` by default, or `null`) and `level` (2, 3 or 4; `h2` by default). - `Stepper` takes `steps` (`{ value, label, description? }`), `value`, `onValueChange`, `label` ("Progress") and `linear` (on). Steps before the current one show a check and read "(done)"; the current one has `aria-current="step"`; with `linear`, later steps are disabled. A `value` that isn't in `steps` marks the first one. The stepper doesn't move by itself: your Back and Continue set `value`. ### Data display - `Badge` takes `tone` (`neutral` by default, `info`, `success`, `warning`, `danger`) and a `span`'s props. The tone is color only, so the words you pass carry the meaning. - `Stat` takes `label`, `value` and `description`. `change` is `{ value, direction, good?, period? }`: an arrow plus "Up" or "Down" for screen readers. A rise shows as good and a fall as bad; `good` overrides that (`good: false` for errors going up). `trend` (numbers oldest first, `null` for a gap) draws a sparkline named for where the values went ("Errors trend: from 0.2 to 0.4, low 0.2, high 0.4"), and `format` formats them. - `DescriptionList` takes `items` (`{ term, details }`) and `layout`: `"columns"` (the default) or `"stacked"`. - `ItemList` takes a `label`, which names the list. Each `Item` takes `title` (a link with `href`), `media`, `description`, `meta` and `actions`. - `Alert` takes `tone` (`info` by default, `success`, `warning`, `danger`), `title`, its text as children, `actions` and `onDismiss`, which adds Dismiss. Screen readers hear the tone before the title ("Warning: Card expires soon"); an alert with no `title` doesn't say it. - An `Alert` is static by default. `live` makes it a polite `role="status"`; `urgent` makes it `role="alert"`, for a message that appears in response to something and must interrupt. - `Kbd` takes `keys` in UI SDK's shortcut form (`"mod+k"`) and shows ⌘K on Apple devices and Ctrl+K elsewhere. - `Spinner` takes `label` ("Loading"), read by screen readers as a status, and `size` ("1rem"). - `Skeleton` takes `width`, `height` ("1rem") and `radius`; `SkeletonText` takes `lines` (3), the last one shorter. Both are hidden from screen readers. Wrap what's loading in `Loading` (`loading`, `label` "Loading…"): it's `aria-busy` while loading and says the label once. With reduced motion, skeletons and spinners stop moving. - `SearchField` takes UI SDK Input's props (`value`, `defaultValue`, `onValueChange`, `placeholder`, `name`) plus `label` ("Search") and `shortcut` ("/", or `null` for none). The key shows as a hint until there's text; then Clear empties the field and keeps focus there. - `CopyButton` takes `value`, `label` ("Copy") and `copiedLabel` ("Copied"). It writes `value` to the clipboard, announces `copiedLabel`, and shows it with a check for 1.6 seconds. If the clipboard refuses, nothing changes on screen. ## Keys | Keys | Where | Does | | ------------------------ | ------------ | ---------------------------------------------------------- | | Mod+B | anywhere | Collapse the sidebar to icons, or expand it (wide screens only) | | "/" (the `shortcut`) | anywhere | Focus the search field, except while typing in another field | | Escape | drawer | Close the navigation drawer | | Tab, Enter, Space | stepper | Each step is a button; locked steps are skipped | The sidebar toggle and the search field carry `aria-keyshortcuts` for their keys. ## For agents Most of these parts are plain HTML, so the bridge doesn't list them: Breadcrumbs, PageHeader, Card, Badge, Stat, EmptyState, Skeleton, Loading, DescriptionList, ItemList, Kbd and Spinner. The buttons, links and fields you put in them are listed, by their own names. Inside an `Item`, a control carries the row's text ("Ada Lovelace ada@acme.dev Owner"), so when two rows each have a Remove button, the bridge's error lists both with their rows and the agent picks one by ref. - **Shell:** the toggle is a button named "Collapse sidebar" or "Expand sidebar". On a narrow screen the drawer is "Open navigation", with `setOpen`. Each `SidebarItem` is a tooltip named by its link text, with `press` (follows the link) and `setOpen`. A `badge`'s text runs into that name with no space ("Deployments3"). - **SearchField:** one input named by `label`, with `setValue` and `clear`. Its Clear button is `data-agent-hidden`, so agents don't see a second control. - **Stepper:** each step is a button named by its text, "Account (done)" for a done step. A description runs into the label ("DetailsName and region"). Locked steps are disabled and have no actions. - **CopyButton:** a button named by its text, which reads `copiedLabel` for 1.6 seconds after a press. - **Alert:** Dismiss is a button named "Dismiss"; with two dismissible alerts on a page, a step needs the ref. None of these parts asks for confirmation. Put `data-agent-confirm` on destructive actions you pass in, as on a Delete in a page header. ## Web Components ``, `` and `` are in [App Shell](./web-components.md#app-shell). ``, `` and `` are in [React and the web](./web-components.md#react-and-the-web) (there, `shortcut=""` turns the key off), and `` is with [Charts](./web-components.md#charts). Badge, alert, card, item and description lists, breadcrumbs, skeleton, spinner and kbd are classes in `uiset.css`. # Rich text A rich text editor on UI SDK: headings, lists (nested, numbered, to-dos), quotes, code, links, mentions, dividers and images, with a toolbar, Markdown as you type, "/" for blocks and "@" for people. Its value is a small document you can save as Markdown or clean HTML, and agents read and write it as Markdown through UI SDK's bridge. ```bash npx shadcn@latest add superzero11/uiset/rich-text ``` It has no editor library underneath. The toolbar is UI SDK's Toolbar, Toggles, Select and Popover; "/" and "@" are UI SDK's TriggerMenu, connected through an editor adapter; undo is UI SDK's `createUndoStack`. The editing itself (`rich-text/view.ts`, `model.ts`) is shared by React and Web Components. ## Parts | Part | What it is | | ------------------ | -------------------------------------------------------------------------- | | `RichTextEditor` | The editor: toolbar, text, "/" and "@" menus, an optional count, a form value | | `RichTextContent` | Saved text, read-only, in the editor's styles (rendered as React, never as an HTML string) | | `toMarkdown`, `fromMarkdown`, `toHTML`, `fromHTML`, `toText` | The document to and from Markdown, HTML and plain text | | `richTextCommands` | What "/" offers by default, to extend or reorder | ## Use ```tsx const [doc, setDoc] = useState(() => fromMarkdown(saved)) // Later: save it. await save({ markdown: toMarkdown(doc), html: toHTML(doc) }) ``` - `defaultValue` takes Markdown or a document, for an editor you don't control. `value` takes a document; pass back what `onValueChange` gives you (it fires for edits, not for caret moves). - In a form, `name="notes"` adds a hidden field with the HTML (or Markdown, with `format="markdown"`). - `maxLength` shows a count. Going over is allowed and says so in words ("12 over the limit of 2000 characters"), so nothing typed is lost; the field is described by the count. - `commands` replaces what "/" offers: `{ value, label, description?, icon? | glyph?, run(state) }`, where `run` returns the next editor state (`setBlock`, `insertBlock` and the rest of `rich-text/model` are there for it). `commands={[]}` turns "/" off. - `uploadImage(file)` takes pasted, dropped and chosen images; return `{ src, alt? }` or `null`. Without it, the toolbar has no image button and image files aren't taken. - `readOnly` hides the toolbar and refuses edits from people and agents; `disabled` also dims it. - `messages` changes what's announced (marks and blocks turning on and off), for other languages. - `onSubmit` runs on Mod+Enter outside a to-do, for a comment box that sends. ## Safety Everything that comes in is checked the same way typing is: pasted HTML and Markdown, the editor's own clipboard format (any page can put it on the clipboard), a `value` or `defaultValue` document, and what `RichTextContent` shows. Unknown block types become paragraphs, heading levels and list depths are clamped, and links and images that aren't `http(s)`, `mailto:`, `tel:` or a path (not `//host`) are dropped, also when a link is added at a caret with nothing selected. `sanitizeDoc` from `rich-text/model` does it for your own code. ## The document A flat list of blocks, each `{ id, type, content }`: `paragraph`, `heading` (`level` 1–3), `quote`, `code` (`language`), `bullet`, `ordered`, `task` (`checked`) with `indent` 0–4, `divider` and `image` (`src`, `alt`). `content` is text runs with `marks` (`bold`, `italic`, `underline`, `strike`, `code`, `link`) and mentions (`{ type: "mention", id, label }`). It's plain JSON: store it as is, or store Markdown or HTML and read it back. - **Markdown out** is GitHub-flavored: `- [ ]` tasks, fenced code, `~~strike~~`, nested lists by two spaces. Underline has no Markdown; it's kept in HTML only. - **HTML out** is a small, fixed set of tags (`p`, `h1`–`h3`, `ul`/`ol`/`li`, `blockquote`, `pre`/`code`, `strong`, `em`, `u`, `s`, `a`, `hr`, `img`, `br`), with tasks as `
  • `. Text is escaped; links are `http(s)`, `mailto:`, `tel:` or a path. - **Mentions** are `@Name` in HTML and read back as mentions; Markdown has no mention syntax, so there they're plain `@Name` text. Store the document or HTML to keep them. - **HTML in** (paste, `fromHTML`) keeps only what the editor can show. A table's rows become lines with their cells apart by tabs. Images in pasted pages come in only when you pass `uploadImage` (no hotlinks or tracking pixels otherwise). Scripts, styles, event attributes, `javascript:` links and unsafe images are dropped. Bold, italics and strikethrough set by Google Docs' and Word's inline styles are read as marks. ## Keys | Keys | Does | | -------------------------- | ----------------------------------------------------- | | Mod+B, Mod+I, Mod+U | Bold, italic, underline (on the selection, or for what you type next) | | Mod+Shift+X, Mod+E | Strikethrough, inline code | | Mod+K | Add, edit or remove the link at the selection | | Mod+Alt+0, 1, 2, 3 | Text, Heading 1, 2, 3 | | Mod+Shift+7, 8, 9 | Numbered, bulleted, to-do list | | Tab, Shift+Tab | In a list: nest the item one level deeper, or less | | Enter | New block; on an empty item, out one level, then out of the list; twice at the end of a code block, out of the code | | Shift+Enter | A line break in the same block | | Mod+Enter | Check a to-do off, or `onSubmit` elsewhere | | Mod+Z, Mod+Shift+Z | Undo, redo (typing is grouped; an agent's edit is one step) | | "/" at the start of a line | The block menu: arrows, Enter or Tab, Escape | | "@" | Mention someone from `mentions` | Markdown as you type: `#`, `##`, `###`, `-`, `1.`, `[ ]`, `[x]`, `>` and a space at the start of a line; `---` or three backticks then Enter; `**bold**`, `*italic*`, `` `code` `` and `~~strike~~` as you close them. Pasting Markdown text turns it into blocks. A to-do's box is a checkbox named by its task; a click or a screen reader's activation checks it off. Each block takes its own text direction (`dir="auto"`), so Arabic or Hebrew reads right in a left-to-right app. A read-only editor stays in the Tab order so its text can be read and copied. Text from an input method takes the marks typing would there. The toolbar is one Tab stop; arrows move along it. Pressing a button keeps the selection and puts focus back in the text. Marks and blocks turning on and off are announced. ## For agents The editor is one component, named by its `label`, with its value as Markdown. `setValue(markdown)` replaces the text (unchanged Markdown changes nothing), `insert(markdown)` adds blocks at the end without re-reading what's there, so mentions and line breaks inside items survive, and `clear()` empties it. Each is one undo step for the person, and read-only or disabled editors refuse them with a reason. The toolbar is hidden from agents (it acts on a person's selection); agents format through Markdown instead. ## Web Components `` is the same editor, built from UI SDK's own elements. See [Web Components](./web-components.md#rich-text). # Chat A chat interface on UI SDK, in five items: the thread and its messages, Markdown answers that stream, the parts around an answer (reasoning, tool calls, sources), the message box, and the list of chats. Every part takes plain props, so your model's stream goes into your own state and the kit draws it. The `chat-app` template puts them together into a page. ```bash npx shadcn@latest add superzero11/uiset/chat superzero11/uiset/chat-response \ superzero11/uiset/chat-parts superzero11/uiset/chat-composer superzero11/uiset/chat-sidebar ``` Each of the last four brings `chat` with it. `chat-response` also adds `react-markdown`, `remark-gfm`, `remend` and `shiki`. The behavior is UI SDK's. The thread follows new text with `createStickToBottom`; the message box brings back what you sent with `createInputHistory`; "/" and "@" are `TriggerMenu`; attachments are `FileInput`; reasoning, tool calls and sources are `Collapsible`; the model picker is a `Select`; usage is a `Meter`. ## Parts | Part | Item | What it is | | ------------------------------------- | --------------- | -------------------------------------------------------------- | | `ChatThread` | `chat` | The scrolling log: follows new text unless you scroll up, then offers Jump to latest | | `ChatMessage` | `chat` | One message: who sent it, when, its content and an `actions` slot | | `ChatMessageActions` | `chat` | Copy, Retry, Edit and Good or Bad under a message, as one toolbar | | `ChatBranch` | `chat` | "2 of 3" with Previous and Next version, for regenerated answers | | `ChatEmpty`, `ChatSuggestions` | `chat` | The empty thread's heading, and prompts to start from | | `ChatTasks` | `chat` | A plan: each task's status in words, and "1 of 3 done" | | `ChatShimmer` | `chat` | "Thinking…" with a shimmer | | `ChatResponse` | `chat-response` | The model's Markdown, completed while it streams; raw HTML never renders | | `ChatCodeBlock` | `chat-response` | Highlighted code with Wrap and Copy (`ChatResponse` uses it for fenced code) | | `ChatReasoning` | `chat-parts` | The model's thinking: open while it streams, then "Thought for 4s" | | `ChatTool` | `chat-parts` | A tool call: its status in words, input, output or error | | `ChatSources`, `ChatCitation` | `chat-parts` | "Used 2 sources", and a numbered link with a hover card | | `ChatImage` | `chat-parts` | An image with its size reserved and an optional caption | | `ChatComposer` | `chat-composer` | The message box: Enter sends, commands, mentions, attachments, a toolbar | | `ChatModelPicker`, `ChatUsage` | `chat-composer` | A model `Select`, and a meter that reads "18.4K of 200K tokens" | | `ChatThreadList` | `chat-sidebar` | The chats: choose one, New chat, Rename, Delete (asks first) | | `ChatLauncher` | `chat-sidebar` | A floating button that opens the assistant in a panel | ## Use ```tsx {turns.length === 0 && ( )} {turns.map((turn) => turn.from === "user" ? ( {turn.text} ) : ( retry(turn.id)} feedback={turn.feedback} onFeedbackChange={(f) => rate(turn.id, f)} /> )} > {turn.reasoning && ( {turn.reasoning} )} {turn.tools.map((tool) => ( ))} {turn.text} ), )} send(text, files)} commands={[{ value: "summarize", label: "summarize", description: "Shorten the thread" }]} mentions={projectFiles.map((f) => ({ value: f.path, label: f.name }))} attachments={{ accept: "image/*,.pdf", maxFiles: 5, maxSize: 10 * 1024 * 1024 }} tools={ <> } /> ``` - `ChatThread`'s `label` ("Conversation") names its scroll region, which holds a `role="log"`. The region is capped at `--chat-thread-height` (26rem by default). - `ChatMessage` takes `from` (`"user"`, `"assistant"` or `"system"`), `name` (You, Assistant or System by default), `avatar` (`{ src?, fallback }`, named by `name`) and `time` (a `Date`, shown in the viewer's short time format). `streaming` marks it `aria-busy` and draws a caret after the last block. - `ChatMessageActions` shows Copy when `text` is given, Retry with `onRetry`, Edit with `onEdit`, and Good and Bad with `onFeedbackChange`. Pressing the pressed one again gives `null`. Copy announces "Copied". `label` names the toolbar ("Message actions"). - `ChatBranch` takes `index` (from 0), `count` and `onIndexChange`, and renders nothing for fewer than two versions. "2 of 3" is announced as it changes. - `ChatTasks` takes `tasks` (`{ id, title, status, detail? }`), `title` ("Plan") and `defaultOpen` (on). Statuses `pending`, `running`, `done` and `failed` read as To do, In progress, Done and Failed. ## Answers - `ChatResponse` takes the Markdown as its child, with GitHub tables, task lists and strikethrough. With `streaming`, half-written Markdown is completed (remend), so `**bold` shows as bold, not stars. - Raw HTML in the Markdown is dropped, never rendered: model output is untrusted. `http` and `https` links open in a new tab and say so to screen readers. Wide tables scroll in a focusable region. - Images in an answer load when they're same-origin, `data:` or `blob:`. Any other image shows as a link ("Image: chart"), because its address could carry what the model read to another server. `allowImage(url)` returns `true` for the ones to load. - Fenced code with a language is highlighted by shiki, loaded the first time it's needed, one language at a time, in the skin's colors. A language shiki doesn't know shows as plain text. Each block has a Wrap toggle and a Copy button named "Copy ts". Render `ChatCodeBlock` yourself with `code`, `language` and `filename` (which then names the block and its Copy button). - `ChatReasoning` opens while `streaming` is on and closes when it ends, unless a person or an agent opened or closed it; then it stays as they left it. It reads "Thinking…" while streaming, "Thought for 4s" with `seconds` (rounded, at least 1), and "Reasoning" without. - `ChatTool`'s `status` is `pending`, `running`, `approval`, `done`, `error` or `denied`, read as Queued, Running, Needs your approval, Done, Failed and Not allowed. It opens itself for `approval` and `error` unless you pass `defaultOpen`. `title` replaces `name` in the header; `input` and `output` show as JSON (strings as they are); `error` shows in place of the output. - `ChatTool` has no Allow or Deny buttons of its own. Put yours in its `children`, which render in the panel, and give Allow a `data-agent-confirm` (the web element does). - `ChatSources` takes `sources` (`{ href, title, description? }`) and renders nothing for an empty list. `ChatCitation` takes `index` and one `source`: a link named "Source 1: title" whose hover card shows the title, host and description. - `ChatImage` needs `src`, `alt`, `width` and `height`, so the space is held before it loads; `caption` is optional. ## The composer - `onSubmit({ text, files })` gets the trimmed text and the attached files, then the box clears. With no text and no files, nothing is sent. - While `running` is on, Enter and Send don't send. With `onStop` too, Send becomes Stop. - `commands` show after "/" at the start of the message and `mentions` after "@", each `{ value, label, description? }`. Choosing one inserts the trigger and the label ("/summarize "); `onSubmit` gets that text, not the item's `value`. - `attachments` is off by default. `{ accept?, maxFiles?, maxSize? }` (`maxSize` in bytes) adds a paperclip, takes files dropped on the box or pasted into it, and lists them as chips with Remove. UI SDK's `FileInput` announces files it rejects; they never join. - `tools` render in the composer's toolbar, beside Send. `label` ("Message") names the box; `placeholder` defaults to "Message…". - Up brings back messages sent from this box since it mounted. - `ChatModelPicker` takes `models` (`{ value, label, description? }`), `value` or `defaultValue` (the first model when neither is given), `onValueChange` and `label` ("Model", visually hidden). - `ChatUsage` takes `used` and `limit`, with `unit` ("tokens") and `label` ("Context used"). The meter carries `data-level="medium"` past 70% and `"high"` past 90%. ## Chats and the launcher - `ChatThreadList` takes `threads` (`{ id, title, updatedAt? }`), `value` (the open chat, marked `aria-current="page"`) and `onValueChange(id)`. `label` names the list ("Chats"). - `onNew` adds New chat. `onRename(id, title)` and `onDelete(id)` add a "More for title" menu to each chat. Rename edits in place: Enter or leaving the field saves, Escape cancels, and an empty or unchanged title isn't sent. Delete asks in an alert dialog ("Delete “Launch plan”?") before calling `onDelete`. - `updatedAt` shows as "4 min ago" and is the chat button's description. It's worked out when the list renders, not kept current. - `ChatLauncher` takes `label` ("Open assistant"), `title` ("Assistant") and the panel's `children`. The panel opens above its button and stays open while you click around the page; Escape, the button and Close shut it. ## The chat-app template ```bash npx shadcn@latest add superzero11/uiset/chat-app ``` `` is the kit as a page. `assistant` is an async generator given `{ text, files, model, history, signal }` that yields `text`, `reasoning`, `reasoning-end`, `tool` (the same `id` updates the card) and `sources` events; the sample one streams a placeholder. It also takes `models`, `commands`, `mentions`, `suggestions`, `userName` ("You"), `userAvatar` and `limit` (200000, for the usage meter). - It runs one answer at a time. Stop aborts `signal` and isn't shown as a failure; any other error adds "The answer stopped early. Try again." - It starts on the second entry in `models` (the sample's "Balanced"). - New chats are numbered ("Untitled chat 2") until the first message, cut to 40 characters, becomes the title. Deleting the last chat starts a new one. - Retry sends the question before that answer again, as a new turn. - Usage is an estimate per chat (`estimateTokens`: 900 plus 4 per character of each message sent), until your model reports its own. [Templates](./templates.md#connecting-a-model-to-chat-app) shows a model connected. ## Keys | Keys | Where | Does | | ----------------------- | -------------------------- | ------------------------------------------------------ | | Enter | message box | Send; with a "/" or "@" menu open, choose the item | | Shift+Enter | message box | A new line | | Up, Down | message box | What you sent before, when the caret is on the first or last line | | "/" at the start, "@" | message box | Commands, mentions: arrows move, Enter or Tab choose, Escape closes | | Tab | thread, code, tables | One stop each; arrows and Page keys scroll it | | Arrows | message actions, suggestions | Move along the toolbar (one Tab stop) | | Enter, Space | reasoning, tool, sources, plan | Open or close it | | Enter, Escape | rename field | Save, cancel (leaving the field saves too) | | Escape | launcher | Close it | Enter doesn't send while an input method is composing. Copying, version changes and attached or rejected files are announced. ## For agents The bridge sees UI SDK components, so messages themselves aren't in its snapshot: give your agent the conversation from your own state. What it does see, by name: - **Message box:** a trigger menu named by `label` ("Message") with `insert`, plus Send (or Stop) and "Attach files" (`setValue`, `addFiles`, `removeFile`, `clear`, `open`). `insert` writes only while the box has focus; without focus it reports success and writes nothing. There's no `setValue` on the box. - **Toolbar:** "Model" is a select an agent sets by a model's label or value; "Context used" is a meter it reads (`value`, `min`, `max`). - **Answers:** each reasoning, tool call, sources list and plan is a collapsible named by its header ("Thought for 4s", "Searched the docs, Done", "Used 2 sources"), with `setOpen`. "Message actions" is a toolbar to `press` Copy message, Retry message, Edit message, Good response or Bad response; "Feedback on this response" also takes `setValue`. Code blocks have "Wrap lines" and "Copy ts"; citations are hover cards ("Source 1: AbortController"). - **Chats:** New chat, each chat as a button named by its title, and "More for title" to `press` Rename or Delete. Rename shows a "Rename title" field; an agent's `setValue` fills it, but it's saved only on Enter or when focus leaves it. - **Delete chat** in the dialog carries `data-agent-confirm` ("Deletes the chat “Launch plan” and its messages"), so a step on it waits for the person, or fails with "Needs a person" when the bridge has no `allow`. Names repeat from one answer to the next ("Message actions", "Wrap lines", "Previous version"). With two answers on the page, a step that names one fails and lists the refs; retry with a ref, or give each `ChatMessageActions` its own `label`. Nothing in the kit is `data-agent-hidden`. ## Web Components ``, ``, ``, `` and `` are the kit as custom elements. `` draws a whole conversation from AI SDK's `messages` shape, with Allow and Deny on tool calls that wait for approval. The launcher has no element. See [Web Components](./web-components.md#chat). # Data Grid A grid for real data, built on UI SDK: thousands of rows, spreadsheet keys, editing in place, and actions an agent can call. One item, `data-grid`, gives you `DataGrid`, `DataGridToolbar` and the framework-free pieces under them. ```bash npx shadcn@latest add superzero11/uiset/data-grid ``` ## Why it's built this way UI SDK ships the hard behaviors a grid needs, and the grid is those behaviors wired together, not a rewrite of them: | Need | UI SDK piece | | -------------------------------------------- | ---------------------------------- | | One tab stop, arrows, Home/End, Page keys | `createGridFocus` | | Select a block, rows or columns; copy it | `createCellRange`, `textToCells` | | Edit in place: Enter, F2, typing, Escape | `createInlineEdit` | | Render 10,000 rows, reveal one off screen | `createVirtualizer` | | Column menu, View menu, filters, grouping | `Menu`, `Popover`, `Select` | | Drag a column edge | `bindDrag` | | Announce sorts, counts and edits | `announce` | | Agents sort, filter, select and edit | the primitive's `agent` key | Sorting, filtering, grouping and CSV export are plain functions in `data-grid/view.ts`, so they run on a server or in a worker too. ## Use ```tsx import { DataGrid, DataGridToolbar, type DataGridColumn } from "@/components/ui/data-grid" const columns: DataGridColumn[] = [ { id: "id", header: "Order", value: (o) => o.id, pinned: true, hideable: false }, { id: "customer", header: "Customer", value: (o) => o.customer, editable: true }, { id: "status", header: "Status", value: (o) => o.status, options: ["Paid", "Pending"], editable: true }, { id: "amount", header: "Amount", type: "number", value: (o) => o.amount, text: (o) => money.format(o.amount), editable: true, }, { id: "rush", header: "Rush", type: "boolean", value: (o) => o.rush, editable: true }, ] o.id} selectable onCellEdit={({ rowId, column, value }) => save(rowId, column, value)} persist="orders-grid" height="30rem" > Mark paid} /> ``` - `value` is what sorts and filters; `text` is what shows and copies; `render` replaces what shows (a badge, a link) without changing either. - `type` sets how an edit is read, with UI SDK's `parseNumber` and `parseDate` in `locale`: `"number"` follows the locale's grouping and accepts a currency sign (`$1,200.50`) but refuses words (`12 units`); `"date"` reads "Nov 20", "2026-11-20" or the locale's order and stores that calendar day; `"boolean"` flips with Space. `options` limits an edit to a list and suggests it. - `onCellEdit` gets the parsed value. Return `false` to keep the editor open. Without it, nothing is editable. - `sort`, `query`, `filters`, `groupBy`, `selected`, `columnState` and `density` can each be controlled (keep filters in the URL, say), with a `default…` and an `on…Change`. - Pass `locale` when rendering on a server, so counts format the same on both sides. - `persist` saves column order, widths, visibility, pinning and density in `localStorage`, read after the first render so server markup matches. ## Toolbar parts `DataGridToolbar` is one arrangement of parts you can use on their own, anywhere inside a `DataGrid`: `DataGridSearch`, `DataGridFilters`, `DataGridGroupBy`, `DataGridViewMenu`, `DataGridExport` and `DataGridSelection` (the count, your bulk actions, Clear selection). `useDataGrid()` gives a custom part the same state. ```tsx o.id} selectable>
    ``` ## Keys | Keys | Where | Does | | ------------------------------ | ------------- | -------------------------------------------- | | Arrows, Home, End, Page keys | anywhere | Move one cell, to the row's ends, by a page | | Control+Home, Control+End | anywhere | First cell, last cell (rows render on the way) | | Enter, Space | header | Sort ascending, descending, off | | Shift+Enter | header | Add the column to the sort | | Shift+F10 or the Menu key | header | Column menu: sort, group, pin, move, hide | | Alt+Left, Alt+Right | header | Narrower, wider (Option on a Mac) | | Alt+Shift+Left, Alt+Shift+Right | header | Move the column | | Enter, F2, or start typing | editable cell | Edit; Enter saves, Escape cancels, Tab saves | | Space | yes/no cell | Flip it | | Space | first column | Check the row | | Enter | group row | Open or close the group | | Shift+Arrows, Shift+click | anywhere | Select a block | | Shift+Space, Control+Space | anywhere | Select the rows, the columns | | Control+A, Escape | anywhere | Select all cells, clear the block | | Control+C, Control+V | anywhere | Copy the block; paste into editable cells | The grid's description lists these for screen reader users, and every sort, resize, move, paste and filter result is announced. Each header also has a column menu button for pointers, touch and touch screen readers (it never takes the tab stop), and the menu has **Wider** and **Narrower**, so resizing never needs a drag. ## For agents The grid offers `sortBy`, `filter`, `filterColumn`, `selectRows`, `clearSelection`, `editCell`, `groupBy` and `showColumn`. Columns are named by header and rows by their id or first column, the way a person reads them. The snapshot shows what the grid is doing (rows shown, sort, filter, grouping, selection), and `query` returns every row, including the ones not rendered. A step that can't run says why, so the agent can try again: ```txt No column "Total" in Orders. Its columns: Order, Customer, Region, Status, Amount, Placed, Rush. Amount needs a number Status must be one of Paid, Pending, Refunded, Failed ``` Edits go through the same parsing and `onCellEdit` as a person's, so your validation applies to both. ## Accessibility notes - `role="grid"` with `aria-rowcount` and `aria-rowindex`, so screen readers know the full size while only visible rows render. - The primary sort column carries `aria-sort`. Group cells carry `aria-expanded` (a grid row may not; only a treegrid's can). - Row checkboxes are real checkboxes that never take focus; the cell keeps it, so the arrows keep working after a click. Shift-click ranges follow the rows on screen. - The editor covers its cell, says what's wrong with `aria-invalid` and a message, and announces it. - Range outlines follow the reading direction, and the focused cell's ring is an outline, so both hold in right-to-left and in forced colors. ## Limits, for now - Rows have one height per density; wrapping text is cut with an ellipsis. - Columns aren't virtualized; hundreds of columns will be slow. - Find in page only finds rendered rows; use the search box. `createVirtualizer`'s `findable` can lift this later. # Charts: design plan Step 4 of the owner's plan. UI SDK ships no charts, and a dashboard is the first thing most apps build, so this is a gap a UI SDK developer hits on day one. ## The set (complete-set rule) What a developer needs to ship dashboards and reports end to end: | Item | What it is | | --------------- | --------------------------------------------------------------------------- | | `chart` | The frame every chart shares: title, description, legend, Chart/Table switch, tooltip, scales, palette tokens per skin | | `line-chart` | Trend over time; one or more series; area wash for one series; stacked areas | | `bar-chart` | Compare categories; vertical or horizontal; grouped or stacked; ranked | | `scatter-chart` | Two measures per item; up to three series; nearest-point hover | | `heatmap` | Magnitude on a grid (weekday × hour, day calendar); one hue, scale legend | | `segmented-bar` | One whole split into parts, tall (with values) or thin (beside figures) | | `sparkline` | A trend inside text, a table cell or a stat | | Stat with trend | The existing `stat` gains `delta` direction and a `trend` sparkline | No pie or donut. Part-to-whole is a segmented bar (at a glance) or ranked bars (to compare); the docs say why, so nobody thinks it was forgotten. ## Principles (from the dataviz method, applied to UISet) - **Form before color.** Every chart's docs start with the job it does. - **One axis.** No dual-axis option, ever. Two measures → two charts. - **Color follows the entity.** Series get slots by their place in `series`, so isolating or hiding one never repaints the others. There are eight slots; a ninth series and beyond draw in the "Other" gray, so fold your tail into one "Other" series. - **Thin marks.** Bars ≤ 24px with a 4px rounded data end, square at the baseline; 2px lines with round joins; markers ≥ 8px with a 2px surface ring; area wash ~10%; hairline solid grid and axes, never dashed; 2px surface gap between touching fills. - **Labels in ink, never series color.** Identity rides a swatch or line key beside it. Direct labels only at line ends (≤ 4 series) and bar tips where they fit. - **Legend for two or more series**, and it doubles as a filter: pressing a legend entry isolates or hides a series (toggle buttons, `aria-pressed`). - **Every chart has a table twin.** A Chart/Table switch shows the same data as a real ``. Tooltips enhance; they never gate. - **Hover and focus show the same thing.** Line/area: a crosshair snaps to the nearest x and the tooltip lists every series there. Bars, cells, dots: the mark is the target, at least 24px, and lifts on hover. ## Keyboard and screen readers The SVG is drawing only (`aria-hidden`). Over it sits an HTML layer of hit targets, one per data position (an x for lines, a bar, a cell, a point), which is what people and assistive tech use: - The layer is a `listbox` with one tab stop, from UI SDK's roving focus with a custom `move`: arrows follow the chart's direction (`aria-orientation` says which), and in a heatmap Up and Down move a row and stop at the edges. Home and End jump. Charts draw left to right in every language, so Right is always the next position. - Each target is an `option` whose name is the full reading: "March 3: Revenue $12,400, Costs $8,100". Selecting one pins it (and is a cross-filter hook: `onSelect`). - Moving announces nothing extra: the option's name is the announcement. - Targets are the hit areas for the pointer too, so hover and focus share one code path and the hit area is the whole band, never a 2px line. ## For agents Each chart is a UI SDK primitive with an `agent` key: `items` returns the data as rows, so `query` can answer "what was revenue in March?" without a screenshot, and actions `isolate`, `showAll`, `showTable` and `select` let an agent drive the view. `select` takes what people read ("March") before a key. The Chart/Table switch is hidden from agents while the chart shows, since `showTable` does its job; with the table showing, the chart's actions are out of reach, so the switch is open to agents as the way back. ## Color per skin (validated, not eyeballed) Tokens: `--chart-1` … `--chart-8` (categorical), `--chart-seq-1` … `-7` (sequential, one hue), `--chart-neg`/`--chart-mid`/`--chart-pos` (diverging), plus grid, axis and ink. Each skin sets them; base.css draws with them. | Skin, scheme | Surface | Palette | Result | | -------------------- | --------- | ---------------------------------------------------- | ------ | | Base, light | `#fdfdff` | reference light set | all gates pass; aqua, yellow, magenta < 3:1 → relief: direct labels + table view | | Base, dark | `#0e0f13` | reference dark set | all gates pass, all ≥ 3:1 | | Studio (dark only) | `#34383e` | reference dark set | all gates pass; magenta, green < 3:1 → same relief | | Blueprint (dark only) | `#053d75` | `#9da3ef #73edb1 #fd8537 #f55fad #fed917 #ffc4fe #b2e37e #73c3ff` | adjacent CVD 14.6, normal 18.7; first six all-pairs CVD 9.3, normal 17.3; all ≥ 3:1 | The gates are the validator's: adjacent pairs, which is what bars and lines need. Pairs that aren't neighbors can still be close in the reference sets (light pink and orange, dark green and pink under deutan vision), so charts never lean on color alone: a scatter chart's series differ in shape, a segmented bar labels its parts, and every chart has its table. Blueprint's first six clear every pair as well, since its mid-blue paper leaves less room between colors. `tests/unit/chart-palette.test.ts` measures them, so a re-picked color that brings back a lookalike pair fails. Blueprint's set sits above the validator's dark lightness band (0.48–0.67) on purpose: the band assumes a near-black surface, and on mid-blue paper (L 0.36) colors inside it fall under 3:1. Contrast is the check the band stands in for, and it passes. ## Look Charts take the skin. Base: quiet hairlines and the palette. Blueprint: the skin's white linework for axes, grid and crosshair, and its light colored-pencil set. Studio: the amber lamp as the crosshair and the sequential ramp. ## Use ```bash npx shadcn@latest add superzero11/uiset/line-chart superzero11/uiset/bar-chart \ superzero11/uiset/scatter-chart superzero11/uiset/heatmap \ superzero11/uiset/segmented-bar superzero11/uiset/sparkline ``` Every chart takes `title` (its name for people and agents), `data`, and accessors. `format` formats values in tooltips, labels and the table. ```tsx d.month} series={[ { key: "revenue", label: "Revenue", value: (d) => d.revenue }, { key: "costs", label: "Costs", value: (d) => d.costs }, ]} format={(n) => money.format(n)} /> d.region} series={[{ key: "revenue", label: "Revenue", value: (d) => d.revenue }]} orientation="horizontal" sort="descending" /> d.day} column={(d) => d.hour} value={(d) => d.sessions} measure="Sessions" /> d.name} x={(d) => d.days} y={(d) => d.size} xLabel="Days to close" yLabel="Deal size" /> ``` `Sparkline` and `Stat` hold no state, so they render in a Server Component. ```tsx ``` - `LineChart`: `layout` is `"line"`, `"area"` (a wash under each line) or `"stacked"` (areas to a total, whose readings and table include the total); `zero` keeps zero on the axis (default on). Stacks with negative layers stay inside the plot. - `BarChart`: `orientation` (`"vertical"` columns or `"horizontal"` bars), `layout` (`"grouped"` or `"stacked"`), `sort`, `labels`. - `ScatterChart`: `series` takes up to three groups, each with a `match`. - `SegmentedBar`: `variant="thin"` for an 8px bar beside its figures. - `selected` / `onSelect` pin a data position from outside, for cross-filtering a dashboard: select a month in one chart, filter the others. Keys are the datum's index in `data`, however a bar chart sorts it. `onSelect(key, { source, reason })` says who pinned it (`keyboard`, `pointer`, `agent`) and why; when new data drops the pinned position, it fires once with `null` and `{ source: "data", reason: "removed" }`. A controlled `selected` that the page doesn't change refuses the pin. - `locale` formats axis ticks, readings and shares; pass it when rendering on a server so the server and the browser print the same digits. - `ChartFrame` is exported for charts of your own: compute the drawing and the data positions, and it supplies the rest. ## Keys | Keys | Does | | ----------------------- | -------------------------------------------------- | | Tab | Into the chart (one stop) and out again | | Left, Right (Up, Down) | The next data position; bar charts on their side use Up and Down | | Up, Down in a heatmap | The cell above or below | | Home, End | The first or last position | | Enter, Space | Pin the position (and its tooltip); again to unpin | | Escape | Unpin; with nothing pinned, dismiss the tooltip | Each position's name is its full reading, so moving is announcing: "Aug: Revenue $70,000, Costs $52,000". # Board A kanban board on UI SDK's `createTransfer`: cards in columns that people move with the pointer, the keyboard or an agent, with the same rules and announcements for all three. ```bash npx shadcn@latest add superzero11/uiset/board ``` ## Use ```tsx const [board] = useState(createBoardController) setLanes(next)} onCardOpen={setOpen} onAddCard={(column, title) => add(column, title) /* return the new card's id */} filter={(card) => matches(card, query, person)} /> setOpen(null)} /> ``` - `lanes` maps each column to its card ids, in order. `cards` holds the cards by id. - A column's `limit` refuses cards past it, for people (the move skips to the next column with room), for drags, and for agents (with a reason). - `filter` hides cards that don't match without moving them; focus skips them, and the board's one tab stop never stays on a hidden card. While a filter hides cards, nothing moves: a carried card would step through cards nobody can see. Space says so. - `onAddCard` returns the new card's id. A controlled board's owner adds it to `lanes`; an uncontrolled board adds it itself. When it fills the column, focus goes to the new card. - `controller` (from `createBoardController()`) connects `BoardCardDialog`, or your code, to the board: `moveCard`, `addCard`, `hasRoom`, `focusCard`, `cardElement`, `neighbor`. The dialog moves cards through it, so a move keeps column limits, reaches `onLanesChange` (and your Undo toast), and focus goes back to the card, or to its neighbor after a delete or an edit the filter hides. - `BoardCardDialog` edits the assignee only when given `people`, and labels only when given `labels`; labels it doesn't list are kept. Full columns are listed but can't be chosen. - `renderCard` replaces the card face; `BoardCardFace` is the default. `describeCard` gives screen readers the details after the title (priority, due date, person and labels by default). - `locked` marks cards that can't move. `messages` replaces the help, empty, full and filtered copy; `moveMessage` replaces the move announcements (for other languages). - `today` marks overdue cards; pass the real date. ## Touch A finger scrolls the board from anywhere on a card, and drags it from the grip at its top corner, which shows on touch screens only. The keyboard and agents don't need it. ## Known limits Releasing a dragged card over a full column, or outside the board, drops it in the last column it passed that had room. That's UI SDK's `createTransfer` (reported upstream). ## Keys | Keys | Does | | ----------------------- | ------------------------------------------------------------ | | Tab | Into the board (one stop) and out | | Up, Down, Home, End | Cards in a column | | Left, Right | The card at the same height in the next column with cards | | Space | Pick a card up; arrows carry it; Space drops it | | Escape | Put a carried card back | | Enter | Open the card's details: edit, move to a column, delete | Every pickup, move and drop is announced ("Write the spec, moved to Doing, position 2 of 3"). Cards are options in a listbox per column, with `aria-roledescription` "card" and "column", the way UI SDK's sortable lists describe themselves; the carried card is the selected one. ## For agents `moveCard(card, column, position?)`, `addCard(column, title)` and `openCard(card)`, by the titles people see. The snapshot's items list every card with its column, assignee and due date. A board without `onCardOpen` or `onAddCard` tells agents it can't open or add cards. Deleting goes through the dialog's confirm, marked `data-agent-confirm`. # Scheduler Calendars on UI SDK's `createSpans`: events people draw, move and resize with the pointer or the keyboard, on a week, a day, or a timeline of rooms. ```bash npx shadcn@latest add superzero11/uiset/scheduler ``` ## Parts | Part | What it is | | ------------------ | -------------------------------------------------------------------------- | | `Schedule` | Tracks of events: days down a week (`axis="y"`), or rooms across a day (`axis="x"`) | | `MonthView` | A month on UI SDK's Calendar; each day lists its events; choosing a day opens it | | `SchedulerToolbar` | Today, previous and next, the range shown, the view, New event | | `EventDialog` | Create or edit: title, day, start, end, where; Delete asks first | | `createScheduleController` | Connects `EventDialog` (or your code) to a `Schedule`: its rules and its focus | ## Use ```tsx const [schedule] = useState(createScheduleController) setOpen(draft)} /> ({ id: d, label: longDate(d), short: shortDate(d) }))} events={events} onEventsChange={(next, { action, id }) => setEvents(next)} onEventOpen={setOpen} startHour={8} endHour={19} step={15} now={new Date()} timeZone="America/Los_Angeles" today={todayIso} /> setOpen(null)} /> ``` - An event is `{ id, track, start, end, title, color?, location? }`; `start` and `end` are minutes from midnight, `track` is a day (an ISO date) or a room id. - `overlap={false}` refuses bookings that collide, for every kind of input. With overlaps allowed, events share the day's width in lanes (`stackSpans`). - `onCreate(span)` makes the event a draw or click creates; return `null` to refuse. - `locked(event)` keeps an event from changing, for people and agents. - **Time zones.** `start` and `end` are wall-clock minutes in the zone the schedule shows, and `track` is that zone's date. `now` is read in `timeZone` (the viewer's by default), so the current-time line and `today` agree. Convert stored instants to the zone at the edge; a 9:00 event stays at 9:00 across a daylight-saving change. - `EventDialog` with the controller keeps the schedule's rules (hours, steps, overlaps) and says why a time won't do, on the field that's wrong. Focus goes back to the saved event, or to the next event once one is deleted. - `size` is how long a click on an empty stretch makes an event. `renderEvent` replaces an event's face; `timeLabel` changes how times read (24-hour, other languages); `MonthView` takes `locale` and `timeLabel` too. - On touch screens a finger scrolls the schedule; a tap makes an event, and events drag. ## Keys | Keys | Does | | -------------------------- | ----------------------------------------- | | Tab | Each event, in time order | | Up, Down (Left, Right on a timeline) | Earlier or later by the step | | Left, Right (Up, Down on a timeline) | The previous or next day or room | | Shift with those arrows | Change when it ends | | Delete, Backspace | Remove it (the demo offers Undo) | | Enter, Space | Open it (each event is a button) | New events by keyboard come from **New event**. Every move, resize and removal is announced. Delete hands focus to the next event, or to the schedule when none is left. Choosing a day in the month focuses that day's first event. In the month view each day is named with its events ("Wednesday, November 18, 2026, 2 events: Standup at 9:00 AM, Launch rehearsal at 3:00 PM"). ## For agents `addEvent(day, start, end, title)`, `moveEvent(event, day, start)`, `resizeEvent(event, end)` and `openEvent(event)`, with times as `HH:MM` and days by name ("Tuesday"). They keep the same rules a drag does, and a time that doesn't fit says why. Events are named by title or id; when two share a title, the error lists both with their ids. The snapshot lists each event as `{ value: id, label: "Standup, Monday, …" }`. Deleting goes through the dialog's confirm (`data-agent-confirm`), so a person approves it; `removeEvent` stays on the API for your own code. # Canvas A node editor composed from UI SDK's canvas behaviors: `createViewport` pans and zooms, `createMove` drags nodes, `createSelection` and `createLasso` choose them, `spatialMove` sends focus to the nearest node, and `createConnect` draws wires. People use a pointer or the keyboard; agents use the same actions by the names people see. ```bash npx shadcn@latest add superzero11/uiset/canvas ``` ## Parts | Part | What it is | | ------------------------ | -------------------------------------------------------------------------------- | | `Canvas` | The editor: dot grid, nodes with ports, wires, zoom controls and a minimap | | `CanvasToolbar` | On UI SDK's Toolbar: Add node (by kind), remove the selection, undo, redo, fit view | | `CanvasInspector` | The selected node: rename it, list its wires with Disconnect, and your own fields | | `useCanvasHistory` | Nodes and edges with undo and redo; one step per move, add, wire or removal | | `createCanvasController` | Connects the toolbar, inspector or your code to the canvas | ## Use ```tsx const [canvas] = useState(createCanvasController) const history = useCanvasHistory({ nodes, edges }) const [selected, setSelected] = useState([]) ({ id: crypto.randomUUID(), kind, title: title ?? "New step", ...at, ...portsFor(kind) })} renderNode={(node) => node.data?.summary} onUndo={history.undo} onRedo={history.redo} fitView /> {(node) => } ``` - A node is `{ id, title, x, y, inputs?, outputs?, kind?, color?, width?, data? }`; a port is `{ id, label }`. Ids may not contain `:`. - An edge is `{ id, from, to }`, with ports keyed `"node:out:port"` and `"node:in:port"`. Wires run from an output to an input on another node; each input takes one wire, and a new one replaces it. `canConnect(from, to)` adds your own rules, like matching types. - Nodes, edges and the selection are controlled or not (`defaultNodes`, `defaultEdges`, `defaultSelected`). Removing nodes removes their wires. - `renderNode` draws under a node's ports, as text or pictures: node faces aren't interactive, so the nodes can be a listbox. Its text is part of the node's description. Put fields in the inspector: its children start over for each node shown. - A node is named by its title, then its kind, as it reads on screen; its description adds the body and its wires. - `CanvasToolbar`'s `label` is its own name, "Canvas tools" by default. Keep the canvas's name out of it: agents find components by names that contain what they ask for. - `grid` is where dropped nodes snap and how far Alt with an arrow moves them (8). - `locked(node)` keeps a node in place. `fitView` frames every node on first show; `controls` and `minimap` turn the corner parts off. - Ports are drawn from fixed sizes (a 36px header and 28px rows, in canvas units), so wires never wait on measuring. Keep custom styles to those sizes. ## Keys | Keys | Does | | --------------------------- | ---------------------------------------------------------------------- | | Tab | Into the canvas (one stop, on the last node focused) and out | | Arrows | The nearest node in that direction | | Alt with arrows | Move the node (or the selection) by the grid; Shift for ten steps | | Space | Select or unselect the node | | Ctrl/⌘ A, Escape | Select every node; clear the selection | | C | Start a wire from the node's output (C again: its next output) | | Arrows, Enter, Escape | While wiring: choose an input, connect, stop | | Delete, Backspace | Remove the node, or the selection; focus moves to the nearest node | | Enter | Open the node (your `onNodeOpen`, like focusing the inspector) | | +, −, 0 | Zoom in, out, back to 100% | | Ctrl/⌘ Z, Ctrl/⌘ Shift Z | Undo, redo (with `onUndo` and `onRedo`) | Focus never falls to the page: removing a node (by Delete, the toolbar, an undo or an agent) moves it to the nearest node, the toolbar keeps disabled tools focusable, and Disconnect in the inspector moves to the next wire. A node added from the toolbar takes focus; one an agent adds doesn't take it from the person. Focus rings and the selection edge keep their width on screen at any zoom. With a pointer: drag empty space to pan, scroll or pinch to zoom, Shift-drag to select a box, drag a node (or the selection) to move it, drag from an output dot to an input to wire them, and press a wire's × to remove it. The minimap pans where you press. Every move, wire and removal is announced, and each node's description says what it's wired to ("Inputs: Prompt from Answer template Prompt."). ## For agents `addNode(kind, title?, x?, y?)`, `moveNode(node, x, y)`, `connect(from, to, fromPort?, toPort?)`, `disconnect(from, to)`, `select(...nodes)`, `openNode(node)` and `fitView()`, by titles (or ids, when two share a title). A wire that isn't allowed, an unknown kind or port, or a locked node says why. `query({ within: "Workflow" })` lists each node as `{ value: id, label: "Draft answer (at 600, 130; Inputs: …; Outputs: …)" }`. Removing goes through the toolbar's Remove button, marked `data-agent-confirm`, so a person approves it. ## Known limits Find in page doesn't reveal nodes off screen yet (UI SDK's `bindFindable` can add it). Very large graphs (thousands of nodes) render every node; virtualizing them is future work. # IDE The parts of a code editor, on UI SDK's Tree, Tabs, Command, Dialog and Textarea, plus an **IDE page** template that puts them together in resizable panes. ```bash npx shadcn@latest add superzero11/uiset/ide # the parts npx shadcn@latest add superzero11/uiset/ide-app # the whole page ``` ## Parts | Part | What it is | | ---------------- | -------------------------------------------------------------------------------------------------- | | `FileTree` | Files on UI SDK's Tree, folders first, version control marks; New file, New folder, Rename, Move, Delete | | `EditorTabs` | Open files on UI SDK's Tabs: unsaved dots, same-named files told apart by folder, closing | | `CodeEditor` | A textarea with line numbers, syntax colors (shiki, themed by every skin), indenting and problems | | `CommandPalette` | Go to a file (⌘P), run a command (⌘⇧P, or start with `>`), go to a line (start with `:`) | | `ProblemsList` | Problems worst first; choosing one opens its file at the line | | `StatusBar` | The bottom strip: `StatusBarItem`s as text or buttons | | `IdeTemplate` | All of it, with a sample project, a tiny linter, an outline, save and unsaved-close prompts | ## Use ```tsx ({ path, dirty: isDirty(path) }))} value={active} onValueChange={setActive} onClose={(path) => (isDirty(path) ? askFirst(path) : closeTab(path, nextTab(open, path)))}> {(path) => ( editors.set(path, handle)} label={path} path={path} value={files[path]} onValueChange={(text) => write(path, text)} onCursorChange={setCursor} problems={problems.filter((p) => p.path === path)} /> )} editor.current?.goTo(line)} /> ``` - `paths` lists files; a path ending in `/` is an empty folder. Folders come from the paths. - `onCreate`, `onRename` and `onMove` get the full new path (or the target folder) and may return a message to refuse; the dialog shows it. Names that exist or hold a slash are refused before your handler runs. Deleting asks first (`data-agent-confirm`). - `status` marks files `modified`, `added`, `deleted`, `untracked` or `conflict`: a letter and a color on screen, the word in the item's name. Folders say "has changes". - `EditorTabs` asks to close through `onClose`; the file closes when you drop it from `files` and pick the next `value` (`nextTab(paths, path)` gives the neighbor). So `onClose` can ask about unsaved changes first. Once it's gone, focus goes to the tab that took over, or to the empty pane. Every open file keeps its own panel, so each editor keeps its caret, scroll and undo while another tab shows. - `CodeEditor` takes `path` or `language` for colors, `readOnly`, `tabSize` and `problems`. Its handle has `focus()` and `goTo(line, col)`. It keeps its own undo (UI SDK's `createUndoStack`): a run of typing is one step until a pause or a caret move, and Tab, Enter and outside changes are steps of their own. In Chrome any text change on the page while typing ends the browser's own undo step, so a colored editor can't rely on it. - The text stays the textarea's own while you type and reaches `onValueChange` once a frame; text you change from outside is written back. Lines keep their colors while an edit is being colored. The colors come from `--shiki-*` variables in `code/code.css`, checked at 4.5:1 in every skin, and still 4.5:1 under the selection (`--code-selection`). - The editor is a plain textarea underneath, so screen readers, spell check (off by default), autofill and agents all treat it as text. ## Keys | Where | Keys | Does | | ----------- | ---------------------------- | -------------------------------------------------------- | | Anywhere | ⌘P / Ctrl+P | Go to a file | | Anywhere | ⌘⇧P / Ctrl+Shift+P | Run a command; `:` then a number goes to a line | | File tree | Arrows, Home, End, Enter | UI SDK's Tree; Enter opens a file | | File tree | F2, Delete | Rename, delete (asks first) | | Tabs | Arrows, Delete | UI SDK's Tabs; Delete closes the focused tab | | Editor | Tab, Shift+Tab | Indent, outdent (every selected line) | | Editor | Enter | New line, keeping the indent; one deeper after `{` | | Editor | Escape, then Tab | Leave the editor (Tab moves focus on like anywhere else) | | Editor | F8, Shift+F8 | Next and previous problem, read out | | Editor | Page Up, Page Down | The caret by a screenful (Shift selects) | | Editor | ⌘Z, ⌘⇧Z (Ctrl+Z, Ctrl+Y) | Undo and redo, a run of typing at a time | | IDE page | ⌘S / Ctrl+S | Save | ## For agents Every part is a UI SDK component, so agents use it by name: choosing a file in **Files** (`setValue`) opens it, as Enter does for a person; switch tabs, write code in the editor named by its path (`setValue`), press New file and fill the dialog, or choose a problem. Deleting goes through the confirm. UI SDK's Command doesn't yet let agents run a command (reported upstream), so palette commands are people-only for now. ## Known limits The editor draws every line, so it suits files up to about a thousand lines; it isn't a full code editor engine (no virtualized lines, multiple cursors, folding or completion). The template keeps files in memory: pass `onSave` to write them, and `lint` for your own linter or language server's problems. # Mobile The parts of a phone app, on UI SDK's gesture behaviors and components. Every gesture has a keyboard path and a screen reader path, and every part works with a mouse too. ```bash npx shadcn@latest add superzero11/uiset/mobile ``` ## Parts | Part | What it is | | -------------------- | -------------------------------------------------------------------------------------------- | | `SwipeList` | Rows that swipe aside to actions on either side; a long swipe runs the first (`createSwipeActions`) | | `PullToRefresh` | Pull down at the top to refresh, with resistance (`rubberBand`); a Refresh button for the keyboard | | `WheelPicker` | A wheel you scroll to choose from (`bindScrollPicker` on a Listbox) | | `TimeWheel` | Hour, minute and AM/PM wheels, as `"HH:MM"` | | `Sheet` | A bottom sheet, as tall as its content or resting at snap points (Drawer) | | `ActionSheet` | A sheet of choices with Cancel; `await sheet.choose()` returns the one chosen | | `AppBar`, `TabBar` | The top bar (back, title, actions) and the bottom bar (sections, the current one marked) | ## Use ```tsx function Inbox() { const [sheet] = useState(createActionSheet) const options = async () => { const choice = await sheet.choose() // "read-all", or undefined for Cancel if (choice === "read-all") markAllRead() } return ( <> void options()}>…} /> fetchMail()} onError={report}> ({ id: m.id, label: `${m.from}: ${m.subject}`, content: , start: [{ id: "read", label: "Read", icon: "mail" }], end: [ { id: "archive", label: "Archive", icon: "archive", tone: "neutral" }, { id: "delete", label: "Delete", icon: "trash", tone: "danger" }, ], }))} onOpen={open} onAction={(id, action) => run(id, action)} /> ) } Help}>… ``` - `start` actions show by swiping toward the end (left to right in English), `end` actions the other way; it flips in right-to-left pages. A side's first action sits at the row's edge. Past `fullSwipe` (0.6 of the row) that action fills the row, and letting go runs it and announces it. Hidden actions are inert. - After an action, focus goes back to its row, or, if the row went (archived, deleted), to the row now in its place. A tap on an open row closes it instead of opening the row. - Each row with actions also has an Actions menu. It's out of the tab order (a row is one tab stop) and out of sight, but screen readers reach it with their own keys and swipes, where neither swipes nor arrows reach the actions; it shows while it has focus. - A row without `label` is named by its content's text. - `PullToRefresh` waits for its `onRefresh` Promise. A rejected one is announced ("Couldn't refresh.") and handed to `onError`. Pulls only start at the top and only downward, so the list still scrolls; a short pull springs back. The pull moves by a CSS variable, so a pull doesn't re-render the list. - A `Sheet` with one snap point (the default, `"content"`) is as tall as its content and has a plain bar. With more it's 85% of the screen, UI SDK's limit for a drawer, and its handle is a button that steps through the points. Drawer's other props (`dismissible`, `defaultOpen`, `modalAbove`, …) pass through. - `WheelPicker` keeps focus on its value: when the value changes from outside, focus in the wheel moves with it, so nothing that reveals the focused option scrolls the wheel back. `TimeWheel` shows a value between its `minuteStep`s too (07 with a step of 5). - A `TabBar` item with `href` is a link, and a click with a modifier key is left to the browser (a new tab); without `href` it's a UI SDK Button. Badges are a number on screen and words in the name ("Inbox, 2 new"). - `AppBar` takes `headingLevel` (1 by default) to fit the page's outline. - Accent text uses `--mobile-accent`, the primary pulled toward the text color so it clears 4.5:1 in every skin. Focus on a filled swipe action is drawn in its own text color. ## Keys | Part | Keys | | ------------- | -------------------------------------------------------------------- | | SwipeList | Up and Down between rows (one tab stop); Left and Right show a row's actions and focus the first; Escape closes them; Enter opens the row | | PullToRefresh | Tab reaches a Refresh button at the top of the list | | WheelPicker | Arrows, Home, End, Page Up and Down, and typing, like any Listbox | | Sheet | Escape closes; the handle steps through snap points with Enter, or arrows, Home and End | ## Touch Rows swipe with a finger without stopping the list from scrolling up and down (`touch-action: pan-y`). At the top of the list, a finger pulling down is the refresh's, and scrolling down stays the browser's. Nothing gets selected while a row swipes or the list pulls. Wheels snap to an option (`scroll-snap-type`). ## For agents - **Messages** (the `SwipeList`'s label): `run(row, action)` runs a row's action by names people see, like `run("Ada Lovelace", "Archive")`; `openRow(row)` opens one. Agents read the rows with `query`, each with its actions. A `danger` action isn't run this way: the step fails with "Needs a person" and points at the row's Actions menu, whose item carries `data-agent-confirm`, so pressing it there asks the person. - `ActionSheet` choices marked `destructive` carry `data-agent-confirm` too. - Tab bar sections, app bar buttons, wheels (`setValue` on "Alarm time, hour") and sheets are UI SDK components agents use by name. ## Other languages Every word the parts say is a prop: `SwipeList messages` (`help`, `actionsFor`), `PullToRefresh messages` (`refresh`, `pull`, `release`, `refreshing`, `done`, `failed`), `TimeWheel labels` (`hour`, `minute`, `period`, `am`, `pm`), `Sheet resizeLabel`, `ActionSheet cancelLabel`, `TabBar label` and `badgeLabel`, `AppBar backLabel`. ## Known limits - With reduced motion, a press on a sheet's handle in the first moments after it opens closes it instead of resizing it (UI SDK's Drawer; reported upstream). - `TimeWheel` is a 12- or 24-hour clock in its own words; it doesn't read the page's locale. # Terminal A terminal for commands your app defines, like a web shell, a dev console or a CLI in the browser. Built on UI SDK's `createInputHistory` (Up and Down) and `createStickToBottom` (output follows its end unless you scroll up). ```bash npx shadcn@latest add superzero11/uiset/terminal ``` ## Use ```tsx ", run: (args) => args.join(" ") }, cat: { description: "Prints a file", usage: "", complete: () => Object.keys(files), run: (args, io) => io.print(read(args[0])), }, build: { description: "Builds, with a progress bar", async *run(_args, io) { for (let p = 0; p <= 100; p += 10) { await step(io.signal) yield `\r${p}%` } yield "\n\x1b[32m✓\x1b[0m done\n" }, }, }} onCommand={(line, io) => sendToServer(line, { signal: io.signal, onOutput: io.print })} /> // Output from elsewhere, like a socket: const [shell] = useState(createTerminalController) socket.onmessage = (event) => shell.print(event.data) ``` - A command's `run(args, io)` returns a string, a Promise of one, nothing (after calling `io.print` and `io.error`), or an async iterable that streams. A thrown error prints in the danger tone. `io.signal` aborts on Ctrl+C, and the prompt comes back at once even if the command doesn't listen. - Output goes through a small screen with a cursor, as a real terminal's does: `\r` returns to the line's start and what follows overwrites it (progress bars), `\b` steps back, and the erase-line, column and cursor-up codes spinners use (`\e[2K`, `\e[G`, `\e[A`) redraw in place. An escape split across two chunks waits for the rest; styles carry across lines. - ANSI styles: the 16 colors as 9 tokens (bright black is a muted grey), backgrounds as tints, bold, dim, italic, underline, strikethrough, and inverse as a solid fill (so test runners' PASS and FAIL badges stay readable). 256-color and true-color codes keep the skin's colors. URLs and OSC 8 links become links, http(s) only. Control and bidi-override characters are dropped. - The page catches up once a frame, however fast output comes, and only changed lines draw again. Old lines go only while you're following the end, so what you're reading stays put. - `help`, `clear` and `history` come built in. `onCommand` gets lines no command matched. - `messages` replaces the terminal's own words, for other languages. - `controller` (`createTerminalController()`) writes output (`print`, `error`), runs and clears from outside, like a socket's messages. - `scrollback` caps the lines kept (2000). - The colors are `--term-red` and friends, checked at 4.5:1 on the terminal in every skin and scheme. "Black" and "white" follow the scheme instead of vanishing into it. ## Keys | Keys | Does | | ----------------- | -------------------------------------------------------------------- | | Enter | Runs the line | | Up, Down | Earlier and later commands; Down past the newest brings back your draft | | Tab | Completes a command, or an argument with `complete` (quoting words with spaces); a second Tab lists several. With nothing to complete, Tab moves on | | Ctrl+C | Stops the running command, or drops the line; with text selected, copies | | Ctrl+L | Clears the screen | The output is a `log` region that takes focus for scrolling by keyboard; a click on it puts the caret in the prompt unless you were selecting text. ## For agents `run(command)` and `clear()`. `run` takes one line of plain text: control characters, which could fake what the person sees echoed, are refused. `query({ within: "Shell" })` returns the last 50 lines as plain text, keyed by line. Commands can do anything your app lets them, so gate `run` with the agent bridge's `allow` callback where that matters. ## Screen readers The output is a log that reads out new lines, not lines redrawn in place, so a progress bar is read once. Lines the terminal also announces (Ctrl+C, a completion list) are hidden from it. A very fast stream still reads every line; summarize long output in your command if that matters. # Studio Instruments built from UI SDK behaviors: a knob, a patch bay with cables, a fader, a level meter and a step sequencer. They make creative and audio tools, and they're the clearest demo of what a skin does: put them in the Studio skin and they look and sound like a rack. ```bash npx shadcn@latest add superzero11/uiset/knob superzero11/uiset/patch-bay superzero11/uiset/fader superzero11/uiset/level-meter superzero11/uiset/step-sequencer ``` ## Parts | Part | Item | What it is | | -------------------------- | ---------------- | ------------------------------------------------------------------ | | `Knob` | `knob` | A rotary control, built as a UI SDK primitive: drag, keys, log scales, double-click to reset | | `PatchBay`, `PatchBayJack` | `patch-bay` | Jacks and the cables between them, by drag or by keyboard | | `Fader` | `fader` | UI SDK's vertical Slider with its label and readout | | `LevelMeter` | `level-meter` | UI SDK's Meter drawn as a ladder of lamps | | `StepSequencer` | `step-sequencer` | A row of steps on UI SDK's ToggleGroup, with downbeats and a playhead | ## Use ```tsx import { useState } from "react" import { Fader } from "@/components/ui/fader" import { Knob } from "@/components/ui/knob" import { LevelMeter } from "@/components/ui/level-meter" import { PatchBay, PatchBayJack, type Cable } from "@/components/ui/patch-bay" import { StepSequencer } from "@/components/ui/step-sequencer" import { UISet } from "@/components/ui/uiset" export function Synth() { const [cutoff, setCutoff] = useState(900) const [cables, setCables] = useState([]) const [pattern, setPattern] = useState(["1", "5", "9", "13"]) return ( `${Math.round(hz)} Hz`} /> ) } ``` - **Knob** takes `value` or `defaultValue`, `min`, `max`, `step`, `largeStep`, `scale="linear" | "log"`, `format` (an `Intl.NumberFormat` options object or a function) and `locale`. `onValueChange` fires as it turns and `onValueCommit` when it settles. `hideLabel` and `hideValue` hide the visible label or readout (the label stays the knob's name). - **PatchBay** owns the cables: `value` or `defaultValue` (`{ from, to }[]`) and `onValueChange`. A jack's `port` ends in `:out` or `:in`, and cables run from an out to an in. `labels` maps ports to names for agents and descriptions; jacks' labels fill it in. - **Fader** is a vertical `Slider` with `label`; **LevelMeter** is a `Meter` with `label` and `value`. - **StepSequencer** is a multiple `ToggleGroup`: `value` is the steps that are on, `steps` (16 by default), `current` the playhead's index, `stepLabel(step)` each step's name. ## Keys | Keys | Does | | ----------------------- | ------------------------------------------------------- | | Arrow keys | Turn a knob, move a fader, move along the sequencer | | Page Up, Page Down | Turn a knob by its large step | | Home, End | A knob's minimum or maximum | | Double-click | Reset a knob | | Enter on an out jack | Start a cable; arrows choose an input; Enter plugs it | | Space | Turn a sequencer step on or off | Every change reads out to screen readers: a knob says its formatted value, a jack says what it's patched to. ## For agents Knobs offer `setValue`, `increment`, `decrement` and `reset`; the patch bay offers `plug`, `unplug` and `clear`, and lists its ports. Faders, meters and the sequencer are UI SDK's slider, meter and toggle group, with their own actions. The [Kits page](/kits#studio) has an agent playing the rack from prompts like "make it darker". ## Skin and sound These work in any skin; the Studio skin draws them as hardware (see [Studio's design notes](/docs/skins/studio)). With `sound` on the root, turning and plugging make short mechanical sounds. # Agent Console: design plan ## Who it's for Any team adding an AI assistant that works inside their app (a copilot that fills forms, changes settings, configures things) rather than one that only chats. That is most SaaS products in 2026. ## Why it isn't another chat kit Chat UI is well served: Vercel's AI Elements (~49 components), shadcn's AI Elements and chat primitives, assistant-ui (13k stars). They render conversations. None of them can show what an agent did _to the page_, because they don't know the page. UI SDK's agent bridge does. The Agent Console is the human side of that bridge: | Need | UI SDK piece | Console part | | ------------------------------------------- | ---------------------------------------------------------- | ---------------------------- | | Approve risky steps in the app, not a popup | `allow` returning a Promise | `AgentApprovals` | | See every change the agent made | `createHistory`, `describeEntry` | `AgentActivity` | | Undo the agent, keep my own edits | `history.undoSource("agent")` | Undo button | | See _where_ it changed something | `observe` + `elementOf` | `highlightAgent()` | | Talk to it | `createStickToBottom`, `createInputHistory`, `isComposing` | `AgentThread`, `AgentPrompt` | | See what it sees | `agent.snapshot()` | Page tab | | Plug in any model | `agentTools` (snapshot, query, act) | `runAgentTool()` | The console marks itself `data-agent-hidden`, so an agent can never press its own **Allow** button. ## Parts - `createApprovals()`: framework-free. Gives `allow` for `createAgent`, plus a queue the UI reads. Steps marked `data-agent-confirm` always wait; a `policy` can ask for more. An abort (timeout) clears the card and refuses the step. - `highlightAgent()`: framework-free. Marks the control an agent just changed with `data-agent-touched` for a moment, and scrolls it into view if it's off screen. - `runAgentTool(agent, name, input)`: routes a model's tool call to the bridge. - React: `AgentConsole` (header, status, Tabs: Chat · Activity · Page), `AgentThread`, `AgentMessage`, `AgentPrompt`, `AgentApprovals`, `AgentActivity`, `AgentSnapshot`. ## Look It inherits the skin around it, so it matches the app. Structure: a column with a header (name and a status lamp: Idle, Working…, Waiting for you), tabs, a scrolling body, approvals pinned above the prompt so a waiting question is never scrolled away. Approval cards are the one loud element: the agent's own sentence about what the step does, in quotes, and two buttons named for the outcome ("Allow", "Don't allow"). Activity rows are quiet, sentence-like ("Agent set Visibility to Public"), with a "Show" button that flashes the control on the page. ## Words Sentence case, plain verbs. Status: "Idle", "Working…", "Waiting for you". Empty activity: "Nothing yet. Changes the agent makes show up here, and you can undo them." Errors say what happened and what to do: "The agent couldn't change Region: it's disabled. Turn it on or change it yourself." ## Accessibility - The thread is `role="log"`; new assistant messages are announced politely. - An approval is announced (`role="status"`), never steals focus from what the person is typing, and its buttons are reachable with Tab. - Highlight uses an outline that survives forced colors, and no motion under reduced motion. ## Demo The gallery uses a small simulated model (pattern matching, no API key) so anyone can try it. It is labeled as simulated. The bridge, approvals, history, undo and highlight are all real. [Connect a model](#connect-a-model) shows how to drive the same console with a real model through `agentTools`. ## Set it up ```bash npx shadcn@latest add superzero11/uiset/agent-console ``` Create the bridge once, in the browser, before the app renders: ```ts // agent.ts import { createAgent } from "@ui-sdk/react/agent" import { createHistory } from "@ui-sdk/react/history" import { createApprovals } from "@/components/ui/agent/approvals" import { highlightAgent } from "@/components/ui/agent/highlight" export const approvals = createApprovals() export const agent = createAgent({ allow: approvals.allow }) export const history = createHistory(agent) highlightAgent() ``` Put the console anywhere, for example in a side panel: ```tsx {messages.map((m) => ( {m.text} ))} ``` Mark anything an agent must not do alone with `data-agent-confirm=""`. The step waits for **Allow** in the console, even when the agent opened a modal first. ## Connect a model The console doesn't care which model you use. Give the model UI SDK's three tools and run each tool call in the browser, where the page is: ```ts import { agentTools, runAgentTool } from "@/components/ui/agent/tools" // Send agentTools (names, descriptions and JSON Schemas for snapshot, query and act) // with your request. For each tool call the model makes: const result = await runAgentTool(agent, call.name, call.input) // ...and return `result` to the model as the tool result. ``` With the Vercel AI SDK, declare the three tools on your route without an `execute`, so they run on the client, and answer them in `useChat`'s `onToolCall` with `runAgentTool(agent, toolCall.toolName, toolCall.input)`. Check the exact hook and field names against your AI SDK version; this sketch isn't covered by our tests yet. The bridge's `allow`, `data-agent-confirm`, `data-agent-hidden` and change callbacks apply to every call, whichever model makes it.