Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,17 +176,18 @@ media, and the Toaster's typed toasts (`toast.success`, `toast.error`…).

### The Badge's status variants

`success`, `warning`, `info` and `destructive` share one recipe: the status's surface role, its ink
role, a focus ring at matching opacity, and a `/15` step of the ink for the anchor hover. `quiet`
is `muted` — a state that exists without asking for attention. Geometry is untouched by every
variant.
`success`, `warning` and `info` share one recipe: the status's surface role, its ink role, a
focus ring at matching opacity, and a `/15` step of the ink for the anchor hover. `destructive`
has no surface role: it wears the Button's destructive recipe — the role over `/10` of itself
(`/20` in dark), with a `/20` hover (`/30` in dark) — as does every other destructive variant or
state. `quiet` is `muted` — a state that exists without asking for attention. Geometry is
untouched by every variant.

## Action hierarchy

Six Button variants, one intent each: `default` (the one dominant action in a view), `outline`,
`secondary`, `ghost`, `destructive` (the action that ends something), `link`. Sizes are `default`,
`xs`, `sm`, `lg`, `icon`, `icon-xs`, `icon-sm`, `icon-lg` and `inline` — the last one `h-auto p-0`
for a link button inside a sentence.
`xs`, `sm`, `lg`, `icon`, `icon-xs`, `icon-sm` and `icon-lg`.

**A Button does not submit a form unless asked.** A native `<button>` renders `type="button"` by
default; `<Button type="submit">` is the explicit opt-in. With `asChild` the child keeps its own
Expand All @@ -198,8 +199,8 @@ reports itself with a toast or a Badge while its button stays `default`.
## Status and feedback

- **Badge** — a state belonging to a row, a card or a heading, that stays on screen.
- **Alert** — a condition about the surface the reader is looking at, in place. `default` and
`destructive`; no informational or settled Alert.
- **Alert** — a condition about the surface the reader is looking at, in place. A plain
`default` plus four tonal variants (`info`, `success`, `warning`, `destructive`).
- **Toaster** and `toast` — the outcome of something the reader just did.
- **Progress** — how much of a known quantity is done. No polarity, no variant; it forwards its
`value` to the Radix root so `aria-valuenow` is announced with nothing asked of the caller.
Expand Down
6 changes: 3 additions & 3 deletions docs/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ generated API table, in the docs site under `examples/catalog/` (`pnpm docs:dev`
| Component | Anatomy exported | Behaviour | Notes |
| --- | --- | --- | --- |
| Accordion | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` | Radix Accordion | `type`: `single` (with `collapsible`) or `multiple`. Each trigger is a button in an `h3`; the arrows, Home and End move between them. The height animation plays both ways: a region in the flow has no dismissable layer. |
| Alert | `Alert`, `AlertTitle`, `AlertDescription`, `AlertAction` | none (`role="alert"`) | `default`, `destructive`. `destructive` wears `bg-destructive-surface text-destructive` with a transparent border. |
| Alert | `Alert`, `AlertTitle`, `AlertDescription`, `AlertAction` | none (`role="alert"`) | `default`, `destructive`. `destructive` wears the Button's destructive recipe (`bg-destructive/10 text-destructive`, `/20` in dark) with a transparent border. |
| Attachment | `Attachment`, `AttachmentGroup`, `AttachmentMedia`, `AttachmentContent`, `AttachmentTitle`, `AttachmentDescription`, `AttachmentActions`, `AttachmentAction`, `AttachmentTrigger` | Radix Slot (`asChild` on the trigger) | An uploaded file as a card. `state`: `idle`, `uploading`, `processing`, `error`, `done` — every part styles off it. `size`, `orientation`. `AttachmentTrigger` covers the card and renders `type="button"`. |
| Avatar | `Avatar`, `AvatarImage`, `AvatarFallback`, `AvatarBadge`, `AvatarGroup`, `AvatarGroupCount` | Radix Avatar | `size`: `sm`, `default`, `lg`, carried as `data-size` so a group sizes its overflow count to match. A fallback is not optional. |
| Badge | `Badge`, `badgeVariants` | Radix Slot (`asChild`) | `default`, `secondary`, `destructive`, `success`, `warning`, `info`, `quiet`, `outline`, `ghost`, `link`. The four statuses are `bg-<status>-surface text-<status>`: a Tailwind 50/950 surface under a 700/300 ink. |
| Breadcrumb | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbEllipsis` | Radix Slot (`asChild` on the link) | `nav[aria-label="breadcrumb"]`. The last crumb is a `BreadcrumbPage` (`aria-current="page"`), not a link; separators are `aria-hidden`. |
| Button | `Button`, `buttonVariants` | Radix Slot (`asChild`) | `type="button"` by default. Six variants; sizes incl. `icon-*` and `inline`. |
| Button | `Button`, `buttonVariants` | Radix Slot (`asChild`) | `type="button"` by default. Six variants; sizes incl. `icon-*`. |
| ButtonGroup | `ButtonGroup`, `ButtonGroupText`, `ButtonGroupSeparator`, `buttonGroupVariants` | Radix Slot (`asChild` on the text segment) | `role="group"`; `orientation`. Rounds the first and last child that carry a `data-slot`, so every segment has one. |
| Card | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter` | none | `size="sm"` tightens `--card-spacing`. |
| Checkbox | `Checkbox` | Radix Checkbox | `aria-checked`, incl. `mixed` for `checked="indeterminate"`, drawn filled with a dash rather than a tick. `type="button"`; inside a form a hidden native input posts `name`. `aria-invalid` styles the invalid state. |
Expand Down Expand Up @@ -49,7 +49,7 @@ generated API table, in the docs site under `examples/catalog/` (`pnpm docs:dev`
| Sonner | `Toaster`, `toast` | sonner | `Toaster` follows the `dark` class on `<html>`; icons from lucide. `richColors` is on: typed toasts wear the status surface and ink roles (`richColors={false}` for neutral ones). |
| Spinner | `Spinner` | none (`role="status"`) | A lucide glyph that announces itself as `Loading`. Compose it into a Button with `data-icon` and `disabled`; there is no `isPending` prop. |
| Switch | `Switch` | Radix Switch | `role="switch"` with `aria-checked`, for a setting that applies at once; not a Toggle (`aria-pressed`). `size`: `sm`, `default`. |
| Table | `Table`, `TableHeader`, `TableBody`, `TableFooter`, `TableRow`, `TableHead`, `TableCell`, `TableCaption` | native | A `rounded-xl border` frame that is also the `overflow-x-auto` scroller. Header on `bg-muted/50` in muted ink; 52px rows; a vertical rule per column (none on the last); the caption closes the frame as a bordered band. |
| Table | `Table`, `TableHeader`, `TableBody`, `TableFooter`, `TableRow`, `TableHead`, `TableCell`, `TableCaption`, `TablePagination` | native | A `rounded-xl border` frame that is also the `overflow-x-auto` scroller. Header on `bg-muted/50` in muted ink; 52px rows; a vertical rule per column (none on the last); the caption closes the frame as a bordered band. |
| Tabs | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `tabsListVariants` | Radix Tabs | `TabsList` `variant`: `default`, `line`. Horizontal and vertical. |
| Textarea | `Textarea` | native | `field-sizing-content`. |
| Toggle | `Toggle`, `toggleVariants` | Radix Toggle | `aria-pressed` is the state. `variant`: `default`, `outline`; `size`: `sm`, `default`, `lg`. |
Expand Down
2 changes: 1 addition & 1 deletion examples/catalog/src/components/BuiltOn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function BuiltOn({ builtOn }: { builtOn: BuiltOnEntry[] }) {
))}
<span className="text-muted-foreground">This component uses {sentence}.</span>
{primary ? (
<Button asChild variant="link" size="inline" className="ml-auto">
<Button asChild variant="link" className="ml-auto h-auto p-0">
{/* One way out of the page: the upstream API, or its docs where there is no
separate API page (cmdk, a native element). */}
<a href={primary.apiUrl ?? primary.docsUrl} target="_blank" rel="noreferrer">
Expand Down
31 changes: 31 additions & 0 deletions examples/catalog/src/components/CodeBlock.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
import CopyButton from "@/components/CopyButton";

/**
* A fenced code block in an MDX body, with a copy button. MDX hands every `pre`
* here already highlighted by Shiki, so the text to copy is recovered from the
* rendered markup: Shiki puts each line in its own span and escapes only the
* five HTML-significant characters, so dropping the tags and decoding those is
* exact.
*/
const { class: className, ...attributes } = Astro.props;
const html = await Astro.slots.render("default");
const code = html
.replace(/<[^>]*>/g, "")
.replace(/&(lt|gt|quot|apos|amp|#\d+|#x[\da-f]+);/gi, (entity, name: string) => {
const named: Record<string, string> = { lt: "<", gt: ">", quot: '"', apos: "'", amp: "&" };
if (name in named) return named[name]!;
const hex = name.toLowerCase().startsWith("#x");
const point = Number.parseInt(name.slice(hex ? 2 : 1), hex ? 16 : 10);
return Number.isNaN(point) ? entity : String.fromCodePoint(point);
})
.replace(/\n$/, "");
---

<div class="relative">
<!-- pr-12 keeps a long first line from running under the button. -->
<pre {...attributes} class:list={[className, "pr-12"]} set:html={html}></pre>
<div class="absolute top-2 right-2">
<CopyButton client:visible code={code} />
</div>
</div>
8 changes: 5 additions & 3 deletions examples/catalog/src/components/DocsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ export default function DocsSidebar({ nav, current }: { nav: Nav; current: strin
return (
<SidebarProvider className="min-h-0 w-auto">
<Sidebar collapsible="none" className="w-full bg-transparent">
<SidebarContent className="gap-6 py-6">
<Group label="Sections" links={nav.sections} current={current} />
<Group label="Components" links={nav.components} current={current} />
<SidebarContent className="gap-5 py-6">
<Group label="Getting started" links={nav.sections} current={current} />
{nav.groups.map((group) => (
<Group key={group.label} label={group.label} links={group.links} current={current} />
))}
</SidebarContent>
</Sidebar>
</SidebarProvider>
Expand Down
3 changes: 2 additions & 1 deletion examples/catalog/src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const links = [
---

<header class="sticky top-0 z-40 border-b bg-background/90 backdrop-blur-sm">
<div class="mx-auto flex h-14 w-full max-w-[90rem] items-center gap-2 px-4 md:px-6">
<!-- md:px-5 puts the wordmark on the sidebar's label edge (px-3 + the label's px-2). -->
<div class="mx-auto flex h-14 w-full max-w-[90rem] items-center gap-2 px-4 md:px-5">
<MobileNav client:load nav={nav} current={current} />
<a href="/" class="mr-2 font-heading text-sm font-medium">
{SITE.short}
Expand Down
2 changes: 1 addition & 1 deletion examples/catalog/src/components/Lede.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const { text } = Astro.props;
const parts = text.split("`");
---

<p class="text-lg text-muted-foreground">
<p class="text-lg/relaxed text-pretty text-muted-foreground">
{parts.map((part, index) =>
index % 2 === 1 ? (
<code class="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[0.85em]">{part}</code>
Expand Down
6 changes: 4 additions & 2 deletions examples/catalog/src/components/Preview.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ const alignment = {
---

<figure
class:list={["my-6 overflow-hidden rounded-xl bg-card ring-1 ring-foreground/10", className]}
class:list={["my-6 overflow-hidden rounded-xl ring-1 ring-foreground/10", className]}
data-demo={demo}
>
<div class:list={["flex min-h-44 flex-wrap gap-3 p-6 md:p-10", alignment]}>
<!-- The height follows the demo; the floor only keeps a lone button from
looking stranded. -->
<div class:list={["flex min-h-40 flex-wrap gap-3 px-6 py-10 md:px-10 md:py-12", alignment]}>
<slot />
</div>
<CodePanel code={code} />
Expand Down
6 changes: 5 additions & 1 deletion examples/catalog/src/components/Swatches.astro
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,11 @@ const STATUS = [
{ name: "success", solid: "bg-success", soft: "bg-success-surface text-success" },
{ name: "warning", solid: "bg-warning", soft: "bg-warning-surface text-warning" },
{ name: "info", solid: "bg-info", soft: "bg-info-surface text-info" },
{ name: "destructive", solid: "bg-destructive", soft: "bg-destructive-surface text-destructive" },
{
name: "destructive",
solid: "bg-destructive",
soft: "bg-destructive/10 text-destructive dark:bg-destructive/20",
},
] as const;

const STRUCTURE = [
Expand Down
11 changes: 8 additions & 3 deletions examples/catalog/src/components/Toc.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ const items = headings.filter((heading) => heading.depth === 2 || heading.depth
{items.length > 0 && (
<nav aria-label="On this page" class="text-sm" data-toc>
<p class="mb-3 font-medium">On This Page</p>
<ul class="flex flex-col gap-2">
<!-- A rail with the current heading marked on it: position is read from
the edge, not only from a colour change. -->
<ul class="flex flex-col border-l">
{items.map((heading) => (
<li class={heading.depth === 3 ? "pl-4" : undefined}>
<li>
<a
href={`#${heading.slug}`}
class="block text-muted-foreground transition-colors hover:text-foreground aria-[current=location]:text-foreground"
class:list={[
"-ml-px block border-l border-transparent py-1 pl-3 text-pretty text-muted-foreground transition-colors hover:text-foreground aria-[current=location]:border-foreground aria-[current=location]:text-foreground",
heading.depth === 3 && "pl-6",
]}
>
{heading.text}
</a>
Expand Down
13 changes: 7 additions & 6 deletions examples/catalog/src/content/components/alert.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Alert
description: An in-page notice with two variants, `default` and `destructive`, where the variant recolours the alert's ink and never its border.
description: An in-page notice with a plain `default` and four tonal variants, `info`, `success`, `warning` and `destructive`, where a variant paints the alert's surface and ink and never its border.
builtOn:
- name: native HTML
docsUrl: https://developer.mozilla.org/docs/Web/Accessibility/ARIA/Roles/alert_role
Expand All @@ -26,8 +26,9 @@ import { Alert, AlertTitle, AlertDescription } from "@robomous/ui-core";
</Alert>
```

`role="alert"` announces the mount itself: there is no live-region prop to set and nothing else to
wire up.
`default`, `warning` and `destructive` render `role="alert"`, which announces the mount itself;
`info` and `success` render `role="status"`, announced politely. There is no live-region prop to set
and nothing else to wire up.

## Composition

Expand All @@ -41,9 +42,9 @@ Alert

## Variants

Two variants. `default` is a paused-but-recoverable notice; `destructive` is a failure that needs
attention. Either way the variant recolours the alert's ink and its description, never the border
around it — status is never a stroke.
`default` is a plain notice on the card surface. `info`, `success`, `warning` and `destructive` are
tonal: the status's surface role under its ink role, with the icon and description in that same ink.
No variant recolours the border around it — status is never a stroke.

<Preview demo="alert/variants">
<Variants client:load />
Expand Down
11 changes: 1 addition & 10 deletions examples/catalog/src/content/components/button.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Button
description: The action element. Six variants with one intent each, sizes down to an inline link, and `type="button"` unless asked otherwise.
description: The action element. Six variants with one intent each, eight sizes from xs to icon-lg, and `type="button"` unless asked otherwise.
builtOn:
- name: Radix Slot
docsUrl: https://www.radix-ui.com/primitives/docs/utilities/slot
Expand All @@ -10,7 +10,6 @@ builtOn:
import AsChild from "@/demos/button/as-child";
import Disabled from "@/demos/button/disabled";
import Icon from "@/demos/button/icon";
import Inline from "@/demos/button/inline";
import Sizes from "@/demos/button/sizes";
import Variants from "@/demos/button/variants";
import WithIcon from "@/demos/button/with-icon";
Expand Down Expand Up @@ -76,14 +75,6 @@ which side to pad.
<WithIcon client:load />
</Preview>

## Inline

The `inline` size sits inside a sentence, with the line's own height.

<Preview demo="button/inline">
<Inline client:load />
</Preview>

## As a link

`asChild` hands the classes to the child element, so a link looks like a button and stays a link.
Expand Down
2 changes: 1 addition & 1 deletion examples/catalog/src/content/components/sheet.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Sheet
## Sides

`side` picks which edge the panel slides from. `top` and `bottom` span the full width; `left`
and `right` run the full height at `w-3/4` up to `sm:max-w-sm`, each with a border on its inner
and `right` run the full height at `w-3/4` up to `sm:max-w-[520px]`, each with a border on its inner
edge.

<Preview demo="sheet/sides">
Expand Down
12 changes: 12 additions & 0 deletions examples/catalog/src/content/components/table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Caption from "@/demos/table/caption";
import Dataset from "@/demos/table/dataset";
import EmptyState from "@/demos/table/empty-state";
import Footer from "@/demos/table/footer";
import Pagination from "@/demos/table/pagination";

<Preview demo="table/dataset">
<Dataset client:load />
Expand Down Expand Up @@ -83,6 +84,17 @@ above rather than continuing them.
<Footer client:load />
</Preview>

## Pagination

`TablePagination` is the footer of a table that pages: the range on the left, Rows per page, the
page position and first / previous / next / last on the right. It renders a `<tfoot>`, so give it
`colSpan` for the columns above. With one page it is a count only, and `loading` swaps the range
for a skeleton and disables the pager.

<Preview demo="table/pagination">
<Pagination client:load />
</Preview>

## Caption

`TableCaption` closes the frame as a muted band under a rule, for the one line of context that a
Expand Down
24 changes: 22 additions & 2 deletions examples/catalog/src/demos/alert/variants.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Alert, AlertTitle, AlertDescription } from "@robomous/ui-core";
import { TriangleAlertIcon, CircleAlertIcon } from "@robomous/ui-core/icons";
import {
TriangleAlertIcon,
CircleAlertIcon,
InfoIcon,
CircleCheckIcon,
} from "@robomous/ui-core/icons";

/** Two variants: default recolours nothing extra, destructive recolours its ink, never its border. */
/** Default on card, then four tonal roles: each paints its surface and its ink, never its border. */
export default function Variants() {
return (
<div className="flex w-full max-w-md flex-col gap-4">
Expand All @@ -10,6 +15,21 @@ export default function Variants() {
<AlertTitle>Ingest paused</AlertTitle>
<AlertDescription>The bucket is unreachable. Retrying in a minute.</AlertDescription>
</Alert>
<Alert variant="info">
<InfoIcon />
<AlertTitle>Inference is running locally</AlertTitle>
<AlertDescription>No frame leaves this machine.</AlertDescription>
</Alert>
<Alert variant="success">
<CircleCheckIcon />
<AlertTitle>Release v0.6 is verified</AlertTitle>
<AlertDescription>All assets match the manifest.</AlertDescription>
</Alert>
<Alert variant="warning">
<TriangleAlertIcon />
<AlertTitle>Local suggestions are off</AlertTitle>
<AlertDescription>Install the runtime, then restart the server.</AlertDescription>
</Alert>
<Alert variant="destructive">
<CircleAlertIcon />
<AlertTitle>Export failed</AlertTitle>
Expand Down
14 changes: 0 additions & 14 deletions examples/catalog/src/demos/button/inline.tsx

This file was deleted.

Loading
Loading