diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md new file mode 100644 index 00000000..81a41e0c --- /dev/null +++ b/frontend/DESIGN.md @@ -0,0 +1,502 @@ +# DocsGPT frontend design system + +The UI is Tailwind v4 plus the shadcn-style components in +`src/components/ui/`. `@shadcn/lint` enforces the rules below through +`npm run lint`; its messages point here. Everything in this file is either a +token in `src/index.css` or a variant in a `ui/` component, so an agent or a +contributor can always find the concrete thing to use. + +## Rules in one paragraph + +Pages compose `ui/` components and choose their look through props +(`variant`, `size`, `shape`). `className` on a component is for placement +only: margin, width, flex and grid, positioning. Colours come from the theme +tokens, never from the raw Tailwind palette. Spacing, type and radii come +from the Tailwind scale, never from arbitrary `[...]` values. Runtime values +go in CSS custom properties or, when unavoidable, an inline style with a +disable comment. + +## Colour tokens + +Defined in `src/index.css` (`:root` for light, `.dark` for dark) and exposed +through `@theme inline`, so `bg-`, `text-`, `border-`, `ring-`, `fill-` and +`stroke-` all accept them, including opacity modifiers such as +`bg-success/10`. One token class replaces a light + dark pair. + +| Token | Use for | Replaces | +| --------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------- | +| `background`, `foreground` | page surface and primary text | `bg-white`, `text-gray-700/800/900`, `dark:text-gray-200/300` | +| `card`, `card-foreground` | raised surfaces (cards, dialogs, dropdowns) | `bg-white dark:bg-[#2b2c31]` | +| `popover`, `popover-foreground` | floating menus and popovers | same as card | +| `muted`, `muted-foreground` | quiet fills and secondary text, icons, placeholders, timestamps | `bg-gray-100/200`, `text-gray-400/500/600`, `dark:text-gray-400` | +| `accent`, `accent-foreground` | hover and selected fills | `hover:bg-gray-100`, `dark:hover:bg-gray-700` | +| `border`, `input`, `ring` | dividers, field borders, focus rings | `border-gray-200/300`, `dark:border-gray-600/700` | +| `primary`, `primary-foreground` | brand purple: primary actions, links, selected states | `bg-purple-*`, `text-purple-*`, `#7D54D1`, `#A076F6` | +| `secondary`, `secondary-foreground` | pressed and active toggles (a brand tint), secondary buttons, the question bubble | `bg-accent` on an active icon button | +| `destructive`, `destructive-foreground` | errors, failed states, dangerous actions | `text-red-*`, `bg-red-50/100`, `#B42318`, `#E60000` | +| `success`, `success-foreground` | completed, active, healthy | `text-green-*`, `text-emerald-*`, `bg-green-50/100` | +| `warning`, `warning-foreground` | paused, pending review, degraded | `text-amber-*`, `text-yellow-*`, `text-orange-*`, `bg-amber-50/100` | +| `info`, `info-foreground` | running, informational | `text-blue-*`, `bg-blue-50/100` | +| `sidebar-*` | the navigation rail | | +| `chart-1` to `chart-5` | data series only, never UI chrome (see below) | | +| `answer-bubble` | assistant message background | | + +Status colours are tuned to stay vivid rather than turning brown or olive, +so their contrast is low. Against white in light mode, `destructive` and +`success` are about 3.8:1, `warning` is 2.9:1 and `info` is 5.2:1. On the +dark card, `destructive` is 2.9:1 and the others are 5.5:1 or more. Do not +use them for long body text; they are for badges, icons, short labels and +fills. + +Chart colours are not a separate palette. `chart-1` to `chart-5` alias +`primary`, `info`, `success`, `warning` and `destructive`, in that order, +so the charts use the same colours as the rest of the app in both themes. +A single series uses `chart-1`, the brand colour. When a series has a +meaning, use the token for that meaning, not the next one in order: +failed tool calls use `destructive`, succeeded ones `success`, pending +ones `warning`. Series with no meaning (models, agents, sources) take +`chart-1` to `chart-5` in order. When there are more than five, the four +largest keep `chart-1` to `chart-4` and the rest are summed into one +"Other" series in `chart-5`, so no colour repeats (`settings/foldSeries.ts`). +`secondary` is never a chart colour. + +Patterns: + +- Status pill: ``. Status box: ``. + Reach for `bg-success/10 text-success` directly only on dots and borders. +- Solid status fill: `bg-warning text-warning-foreground`. +- Guardrail outcomes: block `destructive`, flag `warning`, redact `info`, + not evaluated `neutral`. +- Status border: `border-destructive/50`. +- De-emphasised text: `text-muted-foreground`; go lighter with an opacity + modifier (`text-muted-foreground/70`) rather than a lighter grey. +- Text on the brand colour is `text-primary-foreground`, not `text-white`. +- `secondary` is a brand tint, not a grey: primary at 10% (light) or 15% + (dark) over whatever sits behind it, with `primary` text in light and a + lighter purple (#b89cf8) in dark. So a pressed toggle (CopyButton's copied + state, text-to-speech while speaking) shows on card, background and muted + alike, and never reads as the neutral ghost hover. Use it as + `variant={active ? 'secondary' : 'ghost-muted'}`. +- The user's question bubble is `bg-secondary text-foreground`: the brand + tint as the fill, with body text in `foreground` (15.7:1 light, 12.4:1 + dark) rather than `secondary-foreground`, which is only 4.5:1 in light. + Controls on it (the collapse chevron) are plain `ghost` with a lucide icon + in `currentColor`. +- `white`, `black`, `transparent`, `current` and `inherit` are allowed; use + them only for overlays and imagery, not for text or surfaces. + +## Components and their variants + +### Button (`ui/button.tsx`) + +| Prop | Values | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `variant` | `default`, `secondary`, `outline`, `outline-primary`, `ghost`, `ghost-muted`, `ghost-destructive`, `link`, `destructive`, `destructive-outline`, `combobox`, `sidebar-item`, `tab`, `section-toggle` | +| `size` | `xs`, `sm`, `default`, `lg`, `field`, `icon-xs`, `icon-sm`, `icon`, `icon-lg`, `inline` | +| `shape` | `default` (rounded-md), `pill` (rounded-full, wider padding) | + +- Round brand buttons (`rounded-3xl px-5`, `rounded-full px-6`): `shape="pill"`, + plus `size="lg"` if they were `px-6`. +- Muted icon buttons (`text-muted-foreground hover:text-foreground`): + `variant="ghost-muted"`. +- Icon actions that remove something (a trash can on a member or shared + resource row) are `variant="ghost-destructive"`: muted at rest, red on + hover, so the warning shows even where the click deletes without asking. +- Bordered purple buttons (`border-primary text-primary hover:bg-primary`): + `variant="outline-primary"`. +- Tiny inline actions (`h-auto px-2 py-1 text-xs`): `size="xs"`. +- A link inside running text (an artifact link in an answer) is + `variant="link" size="inline"`: no height or padding, so it wraps with the + sentence. Standalone links (toggles, crumbs) keep a normal size. +- The composer controls under the chat field (Attach, Voice, Tools, + Sources) are `size="sm" shape="pill"`. Their icons are `size-3.5 sm:size-4` + with no margin; the size's `gap-1.5` spaces them, and the label span keeps + `text-xs sm:text-sm` so the row still fits on a phone. +- Popover comboboxes (`role="combobox"` + `Command`) use `variant="combobox"`, + which matches `SelectTrigger`: card fill, normal weight, and muted text + while `data-placeholder` is set (`data-placeholder={value ? undefined : ''}`). + Pass only layout (`w-full justify-between`) and keep the chevron as the + last child. +- A button or picker that sits in a row of fields is `size="field"`: 42px, + the height of `Input` (default) and `SelectTrigger size="lg"`, so a form + column has one height. With `shape="pill"` its text starts 21px in, like + the Input and Select pills beside it. The agent form's pickers are + `combobox field pill`, its Add button `outline-primary field pill`. +- Rows in the navigation sidebar (`hover:bg-sidebar-accent … pl-3 gap-2.5 +rounded-3xl`) are `variant="sidebar-item"`: left-aligned, full-radius, normal + weight, `bg-sidebar-accent` on hover and while `aria-current="page"`. Use it + with `asChild` around a `` for navigation rows. +- Inline disclosure toggles ("Advanced settings", "Show advanced options") + are `variant="link" size="sm"` with only `-ml-3 w-fit justify-start`; + a chevron, when the toggle has one, is a lucide `ChevronRight` (so it + takes the link colour) as the first child. +- Underline tabs (FilePicker's My Files / Shared with Me, the agent page + sub-nav) are `variant="tab"`: muted text on a transparent 2px bottom border, + square corners, no hover fill. Mark the current tab with `data-active`, which + gives it `foreground` text and a `primary` underline. Padding-free tabs (a + sub-nav with `gap-6`) add `size="inline"`, which keeps 4px above the line; + the row draws the 1px baseline, and `-mb-px` lays the underline over it. + Tabs that switch a panel in place use `ui/tabs` instead. +- A section panel's disclosure header (NewAgent's Advanced and Guardrails + panels) is `variant="section-toggle" size="sm"` with `-ml-3 w-fit + justify-start` and `aria-expanded`: a lucide `ChevronRight` first + (`rotate-90` while open) in primary, then the foreground title, with a + primary underline on hover. The button draws no focus ring; the panel does, + so keyboard focus outlines the whole white block: + `has-[[data-variant=section-toggle]:focus-visible]:ring-3 …:ring-ring/50 + …:ring-inset` on the panel `
` (inset, because a scrolling column clips + an outset ring). Status badges go beside the button, not inside it, or the + hover underline runs under them. +- Drop `text-white` on default and destructive buttons; the foreground token + already provides it. Drop `disabled:cursor-not-allowed`; the base disables + pointer events. + +### Card (`ui/card.tsx`) + +| Prop | Values | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `variant` | `outline` (border + card surface, the default), `filled` (muted fill, no border, for tiles on a card-coloured page), `subtle` (border on the page background) | +| `padding` | `none`, `sm` (p-3, stat tiles and panels), `default` (p-4), `lg` (p-6) | +| `interactive` | whole card is the target: hover, focus ring and `selected` highlight; pair with `asChild` around a `