mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-19 04:15:32 +00:00
Agent menu in new style
This commit is contained in:
1 parent
20bcd92942
commit
d4c97a49ce
102 files changed
+4476
-1580
No files matched your search
+308
-189
@@ -1,8 +1,9 @@
|
||||
# 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
|
||||
`src/components/ui/`. `@shadcn/lint` and the `no-restricted-syntax`
|
||||
selectors in `eslint/` enforce the rules below through `npm run lint`; their
|
||||
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.
|
||||
|
||||
@@ -36,7 +37,7 @@ through `@theme inline`, so `bg-`, `text-`, `border-`, `ring-`, `fill-` and
|
||||
| `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 and field borders (fields use `border-border`), focus rings | `border-gray-200/300`, `dark:border-gray-600/700` |
|
||||
| `border`, `input`, `ring` | dividers, field borders (fields use `border-input`, `fieldFrame`), 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` |
|
||||
@@ -45,7 +46,7 @@ through `@theme inline`, so `bg-`, `text-`, `border-`, `ring-`, `fill-` and
|
||||
| `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 | |
|
||||
| `answer-bubble` | the source cards under an answer and the answer-side panels (AnswerFlow) | |
|
||||
|
||||
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
|
||||
@@ -82,7 +83,8 @@ One tint scale, by purpose:
|
||||
`selected`, Dropzone drag-active and drag-reject). The border carries the
|
||||
state; the wash only warms the surface. Never on a chip or a text fill.
|
||||
- Brand soft fill: `bg-secondary text-secondary-foreground` (Badge `default`,
|
||||
Avatar `primary`, the OptionCard icon square), never `bg-primary/10`.
|
||||
Avatar `primary`, the OptionCard icon square), never `bg-primary/10`
|
||||
(enforced).
|
||||
- Status soft fill: `bg-<role>/10` in both themes (Badge, Alert, ToastHeader,
|
||||
a danger-zone panel); no `dark:` twin.
|
||||
- Status border: `border-<role>/50`.
|
||||
@@ -102,12 +104,11 @@ Patterns:
|
||||
- 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
|
||||
- `secondary` is a brand tint, not a grey: primary at 10% (light) or the
|
||||
lighter #976af3 at 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
|
||||
@@ -142,7 +143,7 @@ Patterns:
|
||||
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `variant` | `default`, `secondary`, `outline`, `outline-primary`, `ghost`, `ghost-muted`, `ghost-destructive`, `ghost-on-accent`, `ghost-destructive-on-accent`, `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) |
|
||||
| `shape` | `default` (rounded-md), `pill` (rounded-full, wider padding at `default`, `lg` and `field`) |
|
||||
|
||||
- Round brand buttons (`rounded-3xl px-5`, `rounded-full px-6`): `shape="pill"`,
|
||||
plus `size="lg"` if they were `px-6`.
|
||||
@@ -159,9 +160,10 @@ Patterns:
|
||||
ghost's accent square would not.
|
||||
- Icons are `lucide-react` at the default stroke (2). Don't pass
|
||||
`strokeWidth`, and don't use an `<img>` for a glyph lucide has. The one
|
||||
exception is a tiny check mark inside a status circle (the custom-model
|
||||
test result, the artifact and research step ticks), drawn at 2.5 to 3 so
|
||||
it stays legible at 10 to 12px; raw progress-ring `<svg>`s are not icons.
|
||||
exception is a tiny glyph inside a status circle or chip (the custom-model
|
||||
test result and capability chips, the artifact and research step ticks,
|
||||
`ToastStatus`'s four status icons), drawn at 2.5 to 3 so it stays legible
|
||||
at 10 to 12px; raw progress-ring `<svg>`s are not icons.
|
||||
- Icon size is one `size-N` class (never `h-N w-N`, never lucide's `size`
|
||||
prop); colour goes on the icon (`text-muted-foreground`), spacing on the
|
||||
parent (`gap-*`, not `mr-2` on the icon). Use current lucide names
|
||||
@@ -181,9 +183,9 @@ Patterns:
|
||||
hint passes `text-xs font-normal` (an approved exception), and a link that
|
||||
must keep the colour of what it sits in (a status Alert, a dark overlay, a
|
||||
source card's URL row) passes `text-current`. Never style a raw `<a>` as a
|
||||
link.
|
||||
- Icon-only buttons are `IconButton` (below), never a `Button` with a
|
||||
`title`.
|
||||
link (enforced).
|
||||
- Icon-only buttons: see IconButton below. A `title` on a `Button` is
|
||||
rejected (enforced).
|
||||
- An action whose label doesn't fit beside a name on a phone (the shared
|
||||
agent card's Edit, in a long locale) is two elements: an `IconButton`
|
||||
with `sm:hidden` and the labelled `Button` with `hidden sm:inline-flex`.
|
||||
@@ -196,7 +198,7 @@ Patterns:
|
||||
editor. A Cancel inside a line of text (the composer's queued send) is
|
||||
`link inline`.
|
||||
- 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`
|
||||
Sources) are `outline sm 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"`,
|
||||
@@ -211,7 +213,28 @@ Patterns:
|
||||
retrieval, Sync) are `size="field" shape="pill"` too, with no min-width
|
||||
or hand 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`.
|
||||
`combobox field pill`, each labelled by a `FormField` (the Prompt picker
|
||||
through `Prompts titleAs="field"`), its Add button `outline-primary field
|
||||
pill`, the only `outline-primary` on the agent pages themselves (the Access
|
||||
details modal they open has its own).
|
||||
- One variant and size per role on the agent pages (Overview, Logs,
|
||||
Schedules, the workflow builder's toolbar): the page's primary action is
|
||||
`default field pill` (Publish, Save, New schedule), a secondary action
|
||||
`outline field pill` (Save draft, Preview with `Play` first), Cancel `ghost
|
||||
field pill`, and page-level actions go in the `ActionMenu` (`size="toolbar"`)
|
||||
at the end of the title row, through `SectionShell titleAction`: Access
|
||||
details and Share with team on Overview, and Preview until the agent is
|
||||
published (before that the preview only says "Publish to preview"). So the
|
||||
row never holds more than three buttons and fits a phone, where the main
|
||||
one stretches across it (`flex-1 sm:flex-none`; `PageToolbar`'s action slot
|
||||
spans the stacked row below `sm`). The workflow builder's toolbar has no
|
||||
title row and keeps its ⋯ at the row's end. The
|
||||
agent's Delete is in Overview's danger zone; only the workflow builder's
|
||||
toolbar has it in the ⋯ menu, as a `destructive` item. The row itself is
|
||||
`agents/components/AgentPageToolbar` (see Page chrome). A row's common
|
||||
action is `outline sm pill` (a schedule's Run now or Resume) with its other
|
||||
actions in the row's `ActionMenu`; a delete there is a `destructive` item
|
||||
that opens the confirm modal.
|
||||
- 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
|
||||
@@ -226,24 +249,30 @@ rounded-3xl`) are `variant="sidebar-item"`: left-aligned, full-radius, normal
|
||||
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.
|
||||
Use them for a row of route links (the agent sub-nav: a `<nav>` whose
|
||||
current link carries `aria-current="page"`). Tabs that switch a panel in
|
||||
place are `ui/tabs` with `variant="underline"` on `TabsList` and each
|
||||
`TabsTrigger`: the same pixels, plus `role="tablist"`/`"tab"`,
|
||||
`aria-selected` and arrow-key focus; wrap the panel in `TabsContent`
|
||||
(FilePicker's My Files / Shared with Me). The `default` variant is a pill
|
||||
tab, unused in the app.
|
||||
- Underline route tabs 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. Its one use is `agents/AgentPageHeader.tsx`, the workflow
|
||||
builder's fixed toolbar: a breadcrumb and the Overview / Logs / Schedules
|
||||
tabs in a `<nav>`, the others `<Link>`s and the current one a `<span
|
||||
aria-current="page">` with `data-active`. The
|
||||
agent section pages switch with the section sidebar (the phone menu below
|
||||
`lg`) and carry no pill row; the agent tile's ⋯ also opens Logs. Tabs that switch a panel in place are
|
||||
`ui/tabs` with `variant="underline"` on `TabsList` and each `TabsTrigger`:
|
||||
the same pixels, plus `role="tablist"`/`"tab"`, `aria-selected` and
|
||||
arrow-key focus; wrap the panel in `TabsContent` (FilePicker's My Files /
|
||||
Shared with Me, Schedules' Recurring / One-time in
|
||||
`agents/schedules/SchedulesView.tsx`). The `default` variant is a pill tab,
|
||||
unused in the app.
|
||||
- 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,
|
||||
primary underline on hover. Both call sites wrap the Button in an `<h2>`
|
||||
(a button flattens heading children, so the heading goes outside) and put
|
||||
the title in a `text-lg font-semibold` span. The button draws no focus ring; the panel does,
|
||||
so keyboard focus outlines the whole panel. The panel is a `Card` (a place,
|
||||
`subtle lg`), and every Card carries that ring
|
||||
(`has-[[data-variant=section-toggle]:focus-visible]:ring-3 … ring-inset`,
|
||||
@@ -276,6 +305,7 @@ justify-start` and `aria-expanded`: a lucide `ChevronRight` first
|
||||
| `tone` | `default`, `destructive` (the status soft fill and border, `border-destructive/50 bg-destructive/10`, over any variant) |
|
||||
| `padding` | `none`, `sm` (p-3, row boxes and code blocks), `default` (p-4), `lg` (p-6, tiles, panels and stat tiles) |
|
||||
| `interactive` | whole card is the target: hover, focus ring and `selected` highlight; pair with `asChild` around a `<button>` or `<Link>` |
|
||||
| `selected` | an `interactive` card that is the current choice: `border-primary` and the `/5` wash |
|
||||
|
||||
Parts: `CardHeader` (title left, `CardAction` top-right), `CardTitle`,
|
||||
`CardDescription`, `CardContent`, `CardFooter` (meta row, sticks to the
|
||||
@@ -291,19 +321,19 @@ question: is it a **thing** you open, move, share or delete as a whole, or a
|
||||
**place** where you read or edit content? The fill marks objects; the border
|
||||
marks structure.
|
||||
|
||||
| Role | Variant | Examples |
|
||||
| -------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| Thing (a tile) | `filled`, on a page or in a modal | agents, folders, sources, tools, custom models, teams, chunks, the shared agent card |
|
||||
| Place (a panel) | `subtle` on the page or a right drawer, `outline` on a card (modal, bottom sheet) | the agent form's sections, chart panels, stat tiles, log tables, schedule rows, the agent preview |
|
||||
| Choice (picker tile) | `outline interactive`, or `OptionCard` | the Add tool tiles; selection is drawn by the border, so the border stays |
|
||||
| Well | `filled padding="sm"` | code, a token to copy, run output, inside a panel (see Code blocks below) |
|
||||
| Role | Variant | Examples |
|
||||
| -------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| Thing (a tile) | `filled`, on a page or in a modal | agents, folders, sources, tools, custom models, teams, chunks, the shared agent card |
|
||||
| Place (a panel) | `subtle` on the page or a right drawer, `outline` on a card (modal, bottom sheet) | the agent form's sections, chart panels, stat tiles, log tables, schedule rows |
|
||||
| Choice (picker tile) | `outline interactive`, or `OptionCard` | the Add tool tiles; selection is drawn by the border, so the border stays |
|
||||
| Well | `filled padding="sm"` | code, a token to copy, run output, inside a panel (see Code blocks below) |
|
||||
|
||||
- Layers go page → panel (border) → well (fill), or page → tile (fill).
|
||||
`subtle` and `outline` are one role on two surfaces; in light mode they
|
||||
look the same.
|
||||
- An item that you read inside, rather than open elsewhere, is a place: a
|
||||
schedule is an item, but its row expands into a run table, so it is a
|
||||
`subtle` panel. The tile look is for a summary you open.
|
||||
schedule is an item, but its row (`agents/schedules/ScheduleRow`) expands
|
||||
into a run table, so it is a `subtle` panel. The tile look is for a summary you open.
|
||||
- **No fill on a fill.** Nothing on a `filled` tile repeats its muted fill,
|
||||
or it disappears: no `bg-muted` strip or box, no filled Card inside it, and
|
||||
Skeleton bars take `surface="muted"`. Put a meta line such as a token count
|
||||
@@ -314,9 +344,8 @@ marks structure.
|
||||
(`eslint/card-surfaces.js`).
|
||||
- Never a page-sized `bg-muted` panel around a page's cards: panels sit on
|
||||
the page background, as on Logs and Analytics.
|
||||
- A field in a `subtle` panel sits on the page background, so its floating
|
||||
label passes `labelSurface="background"`; in an `outline` panel it keeps
|
||||
the default `card`.
|
||||
- A field in a `subtle` panel passes `labelSurface="background"` (see
|
||||
FormField).
|
||||
|
||||
Recipes by role:
|
||||
|
||||
@@ -325,13 +354,20 @@ Recipes by role:
|
||||
choices: `outline interactive lg` with `asChild` around a `<button>`.
|
||||
- Chart panels are `subtle lg` with fixed heights passed as layout; a chart
|
||||
panel inside a modal is `outline` (the modal is already `bg-card`).
|
||||
- Form sections (the agent form's Meta, Source, Prompt, Advanced,
|
||||
Guardrails) are `subtle lg`; a disclosure section's header is a
|
||||
`section-toggle`, and Card draws its focus ring.
|
||||
- Row boxes inside a form or panel (a guardrail check, a schedule row, the
|
||||
schedule's timezone box, the discovered MCP tools) are `padding="sm"`, in
|
||||
the panel variant of the surface they sit on (`subtle` in a page panel,
|
||||
`outline` in a modal).
|
||||
- Form sections (the agent form's Basics, Knowledge and behaviour, Model,
|
||||
Advanced, Guardrails) are `subtle lg` with `gap-5`; Basics, Knowledge and
|
||||
behaviour and Model are titled by a `SectionHeader`, Advanced and
|
||||
Guardrails by a `section-toggle` (see Button). A disclosure panel's body
|
||||
under its toggle is one `flex flex-col gap-5` wrapper, its rows carrying
|
||||
no `mt-*`, so every section has 20px under its title. The destructive
|
||||
danger-zone Card follows them.
|
||||
- Row boxes inside a form or panel (a guardrail check, the schedule's
|
||||
timezone box, the discovered MCP tools) are `padding="sm"`, in the panel
|
||||
variant of the surface they sit on (`subtle` in a page panel, `outline` in
|
||||
a modal).
|
||||
- A schedule is `agents/schedules/ScheduleRow`: `subtle`, `padding="none"`,
|
||||
with its run log flush under it; a chevron `IconButton ghost-muted
|
||||
icon-xs` toggles the runs.
|
||||
- A danger zone (Delete agent, Revoke a device) and a failing stat are
|
||||
`tone="destructive"`; the title beside it is `SectionHeader
|
||||
tone="destructive"`. Muted text fails AA on the red fill, so the tone turns
|
||||
@@ -358,8 +394,10 @@ well) on a card or background surface, `subtle padding="sm"` on a muted one
|
||||
(the tool-approval card, an expanded log row). A copy row is the same Card
|
||||
with `className="flex-row items-start gap-2"` and the `CopyButton` inside.
|
||||
Inside an Alert or a trace's tool panel, and in a full-pane viewer, the
|
||||
`<pre>` takes the recipe with no Card, so boxes don't nest. Put scroll caps
|
||||
(`max-h-* overflow-y-auto`) on the Card; never `break-all`.
|
||||
`<pre>` takes the recipe with no Card, so boxes don't nest. A scroll cap
|
||||
goes on an inner `<div className="scrollbar-overlay max-h-* overflow-y-auto">`
|
||||
around the `<pre>`, inside the Card's padding, never on the Card: on the Card
|
||||
the scrollbar runs into its rounded corners. Never `break-all` (enforced).
|
||||
This recipe is for app output. A fenced code block in a markdown answer is
|
||||
source code: it keeps its lines (`white-space: pre`, indentation intact) and
|
||||
scrolls sideways inside its own bordered frame (see Chat answer column).
|
||||
@@ -408,7 +446,8 @@ modal) leave it out and stay plain buttons. Only layout classes on it.
|
||||
`variant`: `default` (brand), `neutral`, `success`, `warning`, `destructive`,
|
||||
`info`, `outline`. Replaces every hand-rolled
|
||||
`rounded-full bg-<colour>-100 px-2 py-0.5 text-xs text-<colour>-700 dark:...`
|
||||
pill, including schedule and run status pills, trace chips and statuses,
|
||||
pill, including schedule and run status pills (`agents/schedules/StatusBadge.tsx`
|
||||
maps schedule and run statuses to Badge variants), trace chips and statuses,
|
||||
token scope chips, "Disabled" and tool chips. A grey chip is `neutral`, never
|
||||
a `bg-muted` pill. The admin role is `default` wherever it shows (Teams, Admin
|
||||
→ Users); every other role is `neutral`. Chips that show code (token scopes) pass `font-mono`, and
|
||||
@@ -419,7 +458,22 @@ stat chips `tabular-nums`, as approved exceptions. HTTP method pills take their
|
||||
`default` Badges with a remove X, then "+N more", on a `Button
|
||||
variant="combobox" size="field"` trigger that grows past 38px when the chips
|
||||
wrap, with SelectTrigger's turning chevron; each row in its list shows a
|
||||
`Checkbox size="sm"`.
|
||||
`Checkbox size="sm"`. Inside a Modal pass `modal`, or its popover can't
|
||||
scroll and doesn't close on an outside click.
|
||||
|
||||
In a picker list, mark the item that is currently chosen with
|
||||
`CommandItem checked` (a `secondary` brand tint through `data-checked`), not
|
||||
with `bg-accent`: cmdk's own `data-selected` highlight is `bg-accent` and
|
||||
follows the pointer and arrow keys, so an accent fill would look like hover.
|
||||
While a checked item is also highlighted it keeps its tint and text and gains
|
||||
a 1px inset `primary` ring, so the chosen row never turns plain grey.
|
||||
|
||||
A picker's footer (`MultiSelectPopover footer`) links to the page that manages
|
||||
the list, as a `link inline` Button with a 12px `ArrowRight` (Go to Sources, Go
|
||||
to Tools). A shortcut action (Upload new, an `outline-primary pill`) sits at the
|
||||
right end of the same row: `flex flex-wrap items-center justify-between gap-3`,
|
||||
so on a narrow sheet or in a long locale it wraps under the link rather than
|
||||
taking a row of its own everywhere (`SourcesPopoverFooter`).
|
||||
|
||||
### Input (`ui/input.tsx`)
|
||||
|
||||
@@ -431,11 +485,8 @@ wrap, with SelectTrigger's turning chevron; each row in its list shows a
|
||||
|
||||
Text alignment classes (`text-right`) and `font-mono` (code fields) are
|
||||
allowed on Input and Textarea. `<Input label>` is the shorthand for a
|
||||
one-field `FormField`: the same floating label on the border, the same
|
||||
`labelSurface` (`card` default, forms in cards and modals; `background`,
|
||||
fields straight on a page; `muted`, fields on a muted panel). Never pass a
|
||||
background class for it. A placeholder under a floating label is an example:
|
||||
it stays hidden while the label rests inside and shows on focus. The chat and hero
|
||||
one-field `FormField`: the same floating label on the border and the same
|
||||
`labelSurface` (see FormField). The chat and hero
|
||||
fields (`rounded-3xl px-5 py-3`) are `size="lg" shape="pill"`. A
|
||||
default-size pill (38px, the form-row height) pads `px-5` like the large
|
||||
one, so its text lines up with the Select pills. Compact table
|
||||
@@ -475,9 +526,11 @@ variable highlighter and the chunk editor behind its line-number gutter.
|
||||
|
||||
Every boxed form control is labelled by a floating label:
|
||||
`<FormField label required hint error disabled labelSurface>` with the field
|
||||
as its only child. The label sits on the field's border (12px muted,
|
||||
`labelSurface` `card` | `background` | `muted` to match the surface behind
|
||||
the field, so the notch hides the line). It rests inside an empty, unfocused
|
||||
as its only child. The label sits on the field's border (12px muted), and
|
||||
`labelSurface` matches the surface behind the field so the notch hides the
|
||||
line: `card` (the default: forms in cards, modals and `outline` panels),
|
||||
`background` (fields straight on a page or in a `subtle` panel), `muted`
|
||||
(fields on a muted panel). Never pass a background class for it. It rests inside an empty, unfocused
|
||||
`Input` or `Textarea` and moves up on focus; on a `SelectTrigger`, combobox,
|
||||
`MultiSelect`, number field or `Dropzone` it stays on the border. It turns
|
||||
red (`text-destructive`) with an `error`, and dims while `disabled`. Under
|
||||
@@ -526,13 +579,30 @@ toggle) are not SettingRows.
|
||||
|
||||
### Checkbox (`ui/checkbox.tsx`)
|
||||
|
||||
`<Checkbox checked onCheckedChange>` (Radix), never `<input type="checkbox">`:
|
||||
native boxes take the OS accent and ignore dark mode. `size`: `default`
|
||||
`<Checkbox checked onCheckedChange>` (Radix), never `<input type="checkbox">`
|
||||
(enforced): native boxes take the OS accent and ignore dark mode. `size`: `default`
|
||||
(16px, option rows), `sm` (14px, table cells). Unchecked it is an
|
||||
`border-input` box; checked, `primary` with a `primary-foreground` check.
|
||||
Name it with a `Label htmlFor` or `aria-label`. For an on/off setting with a
|
||||
description use a Switch in a SettingRow instead.
|
||||
|
||||
### Switch, TimePicker and Calendar (`ui/switch.tsx`, `ui/time-picker.tsx`, `ui/calendar.tsx`)
|
||||
|
||||
`Switch` (Radix) is an on/off setting, placed inside a `SettingRow` that names
|
||||
it. The track is `primary` when on and `bg-input` when off, with a white
|
||||
thumb in both themes (see Elevation).
|
||||
|
||||
`TimePicker` picks a time of day as two `SelectTrigger`s, hours and minutes,
|
||||
with a `value`/`onChange` pair in 24-hour `"HH:MM"`. `minuteStep` sets the
|
||||
minute list's step (default 1), `ariaLabel` names the group and each select,
|
||||
and `showIcon={false}` drops the leading clock. Never a native
|
||||
`<input type="time">`, which ignores dark mode.
|
||||
|
||||
`Calendar` is react-day-picker styled with the theme tokens, its days `ghost`
|
||||
Buttons; open it in a `Popover` from a field (ScheduleFormModal's date). Its
|
||||
day button carries the one approved lint exception in the file (see Approved
|
||||
exceptions).
|
||||
|
||||
### Tooltip (`ui/tooltip.tsx`) and IconButton (`ui/icon-button.tsx`)
|
||||
|
||||
Every icon-only button is an `IconButton`: `<IconButton label icon hint?
|
||||
@@ -579,24 +649,32 @@ Replaces every `<hr>` and every `border-b` or `h-px` div that is only a line.
|
||||
`currentColor`, so colour it with a `text-*` token on it or its parent. `xs`
|
||||
is the icon-sized step: a busy Button, a step's status in a Preview or
|
||||
research row. Never shrink a larger size with `className="size-*"`, and don't
|
||||
use lucide `LoaderCircle` (formerly `Loader2`) or a hand-drawn SVG as a loader. `Skeleton` is a pulsing
|
||||
use lucide `LoaderCircle`/`Loader2` or a hand-drawn SVG as a loader (the
|
||||
`Loader`, `Loader2` and `LoaderCircle` imports are enforced). `Skeleton` is a pulsing
|
||||
muted block sized with layout classes; its default radius (`rounded-sm`, 6px)
|
||||
is the bar radius, so pass none (`rounded-full` for an avatar or switch
|
||||
stand-in). Bars inside a muted surface (a `Card variant="filled"` tile) take
|
||||
`surface="muted"` (`bg-muted-foreground/20`), since `bg-muted` would vanish
|
||||
there. A tile's loading mirror is the same `Card` as the tile (variant,
|
||||
padding, height) with Skeleton bars laid out like its content; the Card
|
||||
itself never pulses, only the bars do. Never put a Skeleton inside an
|
||||
element that pulses itself; the two animations multiply.
|
||||
itself never pulses, only the bars do. A loader inside a panel that is
|
||||
already on screen (a chart panel whose title shows while its data loads) is
|
||||
bars only, shaped like the content; the panel is the Card. A loader for an
|
||||
`Alert` or row box is an `outline sm` Card in a modal with bars inside. Never
|
||||
put a Skeleton inside an element that pulses itself; the two animations
|
||||
multiply.
|
||||
|
||||
### LoadingState and EmptyState (`ui/loading-state.tsx`, `ui/empty-state.tsx`)
|
||||
|
||||
A page, panel or dialog that is still loading shows `LoadingState`, never a
|
||||
hand-centred `Spinner`. `fill`: `parent` (`h-full`, needs a parent with a
|
||||
height: the artifact panel, a drawer body), `screen` (`h-screen`, the app and
|
||||
height: the artifact panel, a drawer body), `screen` (`h-dvh`, the app and
|
||||
admin guards) or `block` (`py-10`, a page section or dialog body with no
|
||||
height of its own). `label` puts a muted caption under the ring (a long job:
|
||||
Convert to wiki, Enable GraphRAG); it is also the ring's name. Spinners
|
||||
Convert to wiki, Enable GraphRAG); it is also the ring's name. `size` takes
|
||||
the Spinner sizes (`default` unless set): `sm` inside a picker (the
|
||||
MultiSelect popover's list), `lg` for a full panel (a remote device's
|
||||
config). Spinners
|
||||
inside a control (a busy Button, a picker's `sm` ring) stay `Spinner`.
|
||||
|
||||
Nothing to show is `EmptyState`: `size` `default | sm | xs` (128 / 96 / 64px
|
||||
@@ -618,20 +696,33 @@ width-percent divs in quota, indexing and guardrail views.
|
||||
`size`: `none` (image decides, the default), `xs` (28px), `sm` (32px),
|
||||
`default` (36px), `lg` (40px): Button's names at Button's heights;
|
||||
`shape`: `none`, `circle`, `square`; `variant`: `default`, `primary` (brand
|
||||
initials on `secondary`), `muted` (grey initials). Initials boxes pass the letters as
|
||||
initials on `secondary`), `muted` (a grey `bg-muted-foreground/15` box with
|
||||
`foreground` initials). Initials boxes pass the letters as
|
||||
children.
|
||||
|
||||
### Dropzone (`ui/dropzone.tsx`)
|
||||
|
||||
`size`: `default` (tall target), `compact` (one row, for forms and modals).
|
||||
`size`: `default` (tall target, for a step whose whole job is the file: Upload
|
||||
from device, Import agent, Import API specification), `compact` (one row, a
|
||||
file field among other fields),
|
||||
`tile` (a square beside a form's fields, the agent's avatar: 64px and
|
||||
icon-only on a phone beside the Name field, 88px with a one-word title from
|
||||
`sm` beside Name and Description; no description). `FileUpload size="tile"` fills the tile
|
||||
with the picked image, or with `currentImage` (the saved one) until then.
|
||||
Props: `onDrop`, `accept`, `multiple`, `maxFiles`, `maxSize`, `disabled`,
|
||||
`title`, `description`, `icon`, `error`. Border and fill follow the drag
|
||||
`title`, `description`, `icon`, `error`, `validator` (react-dropzone's
|
||||
per-file check) and `children`, which replaces the icon, title and
|
||||
description block. Border and fill follow the drag
|
||||
state through `data-drag-active` and `data-drag-reject` (the `/5` wash);
|
||||
it hovers to solid `accent`. Every drop
|
||||
target renders it, including `components/FileUpload.tsx` (which keeps its
|
||||
preview and validation logic) and the Import agent / Import API
|
||||
specification dialogs; don't hand-roll a `border-2 border-dashed` target
|
||||
around `useDropzone`.
|
||||
around `useDropzone`. Picked files show under it as `ListRows` (name and
|
||||
size) in an `outline` Card, and rejected files (too large, wrong type) as the
|
||||
Dropzone's `error`, never dropped silently. The chat composer is the one other
|
||||
`useDropzone` host: the whole composer is the drop area, with its own
|
||||
overlay, whose prompt is a Title (see Typography roles).
|
||||
|
||||
### Toast (`ui/toast.tsx`)
|
||||
|
||||
@@ -644,8 +735,8 @@ overlay. The app has one
|
||||
(`role="status"`, `aria-live="polite"`) and the fixed stack, so `Toast`
|
||||
cards carry no role and no toast renders its own rail or positioning. Top
|
||||
to bottom it holds `TeamNotificationToast`, `ToolApprovalToast`,
|
||||
`UploadToast` and `ActionToast`, and it moves to the bottom-left while the
|
||||
workflow Preview drawer is open. A new toast component returns only its
|
||||
`UploadToast` and `ActionToast`, and it moves to the bottom-left while any
|
||||
agent preview drawer is open (workflow or classic). A new toast component returns only its
|
||||
`Toast` cards and is added to that viewport. A page that reports the result
|
||||
of an action (the admin Users actions) dispatches
|
||||
`showActionToast({ variant: 'success' | 'destructive', message })` from
|
||||
@@ -770,15 +861,42 @@ Previous / Next buttons. Don't hand-roll a Previous / Next row.
|
||||
|
||||
Every section page (settings, admin, agents, teams) is wrapped in
|
||||
`navigation/SectionShell` (`width` `default` 6xl, `wide` 7xl for admin,
|
||||
`narrow` 5xl; `pills` for the phone destination pills); it draws the title and
|
||||
`narrow` 5xl; `title` replaces the section title; `titleAction` puts a page-level
|
||||
control, such as a toolbar `ActionMenu`, at the title row's end;
|
||||
`header={false}` drops the title for the phone section index, which draws
|
||||
its own);
|
||||
it draws the title and
|
||||
starts the content 32px below it, so pages carry no root `mt-*`. The block
|
||||
under the title is `components/PageToolbar`: `intro`, then the page `search`
|
||||
(left, `max-w-md`) and the page `action` (right, `Button size="field"
|
||||
shape="pill"`), then `divider` (a `Separator`). A page search is
|
||||
`components/SearchInput`: the 38px pill with a search icon and a floating
|
||||
`label`. The chunk viewer's and file tree's searches stay `CommandInput` in
|
||||
shape="pill"`), then `children` (notices that belong to the action, such as
|
||||
a limit warning), then `divider` (a `Separator`). With no `search` the intro
|
||||
moves into the row's left slot (`max-w-2xl`) beside the action. A page search
|
||||
is `components/SearchInput`: the 38px pill (`size="sm"` for 32px) with a
|
||||
search icon and a floating `label` on `labelSurface="background"` (its
|
||||
default); a placeholder-only search is named by its placeholder. The chunk viewer's and file tree's searches stay `CommandInput` in
|
||||
their frame, because their results are `CommandItem`s.
|
||||
|
||||
An agent's tabs (Overview, Logs, Schedules) share one header,
|
||||
`agents/components/AgentPageToolbar` on top of `PageToolbar`, under
|
||||
`SectionShell`'s title (the tab's name, or "New agent" through SectionShell's
|
||||
`title` prop). Its props: `name` (the agent, first in a muted byline),
|
||||
`status` (a Badge beside the name, Overview's Published / Draft), `meta` (the
|
||||
tab's context after a middle dot, "Last used" on Logs and Schedules),
|
||||
`actions` (the tab's buttons on the right, at most three; the page's ⋯ goes
|
||||
in the title row, not here), `intro` (a line that replaces
|
||||
the byline: the new-agent form) and `children` (notices between the row and
|
||||
the rule, such as the failed-save `Alert`). With a status badge the meta
|
||||
takes its own line on a phone. The row keeps the 38px field height when a
|
||||
tab has no actions, so the rule lands in the same place on every tab.
|
||||
`AgentPageHeader` remains only in the workflow builder's fixed toolbar. All
|
||||
three tabs are `SectionShell` pages at the default width; the agent preview
|
||||
is a drawer (see Modal, not Dialog). Overview groups its fields into three
|
||||
`subtle lg` panels (Basics, Knowledge and behaviour, Model) with the two-up
|
||||
field grid, then Advanced, Guardrails and the danger zone. Schedules shows a
|
||||
`StatCard` row, Recurring / One-time as `ui/tabs variant="underline"`, and
|
||||
each schedule as a `ScheduleRow` (see Card surfaces).
|
||||
|
||||
### App chrome: the phone top bar and New Chat
|
||||
|
||||
Below `lg` the top of every page is `navigation/MobileTopBar` (56px, `h-14`):
|
||||
@@ -829,12 +947,15 @@ wrapper).
|
||||
|
||||
### Grids
|
||||
|
||||
Two recipes, no component. Tiles (sources, tools, custom models, agents):
|
||||
Three recipes, no component. Tiles (sources, tools, custom models, agents):
|
||||
`grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`; tiles
|
||||
take the column width, never a fixed `w-[300px]`. Team cards stop at three
|
||||
columns (`lg:grid-cols-3`): their header row holds an initial, the name, a
|
||||
role badge and a chevron. Stat rows: `grid grid-cols-2 gap-4 md:grid-cols-4`
|
||||
(five tiles: `md:grid-cols-3 lg:grid-cols-5`; a dialog: `grid-cols-3`). A
|
||||
(five tiles: `md:grid-cols-3 lg:grid-cols-5`; a dialog: `grid-cols-3`). Text
|
||||
tiles whose body is the content (chunks) follow their container, since the
|
||||
same list renders beside a file-search column: `grid grid-cols-1 gap-4
|
||||
sm:grid-cols-[repeat(auto-fit,minmax(min(400px,100%),1fr))]`. A
|
||||
grid inside a Modal keeps its own column counts, because breakpoints follow
|
||||
the window, not the dialog.
|
||||
Skeleton mirrors follow the grid they stand in for.
|
||||
@@ -853,12 +974,12 @@ rejects imports of it elsewhere. App code uses `Modal` (sizes `sm` to
|
||||
`full`, `mobileVariant="sheet"`) or `CommandDialog`.
|
||||
|
||||
The width comes from `size` only: `sm` 384, `md` 512 (the default), `lg` 672
|
||||
(a form or a one-column list: Move to folder, Upload), `xl` 896 (a grid of
|
||||
(a form or a one-column list: Move to folder), `xl` 896 (a grid of
|
||||
tiles or a wide editor: Add tool, Test retrieval, the prompt editor), `full`.
|
||||
Never add a width or height class. The dialog caps itself at `85dvh` and
|
||||
its body is the one scroller, so the header and footer stay put; don't cap
|
||||
the body with `contentClassName`. `contentClassName="overflow-visible"`
|
||||
(`!overflow-visible`) is only for a body whose popover must escape it (the
|
||||
(`cn` drops the body's `overflow-y-auto` for it) is only for a body whose popover must escape it (the
|
||||
prompt editor, Share conversation, Move to folder). `className` is placement
|
||||
only.
|
||||
|
||||
@@ -894,23 +1015,31 @@ the same (`modals/SearchConversationsModal.tsx`). `CommandDialog` sits on
|
||||
`DialogContent`, which has Modal's surface (`bg-card rounded-2xl
|
||||
shadow-modal`, no border, `p-8`, a 20px title, a `gap-3` footer, a left-aligned
|
||||
header, and the `flex-col max-h-[85dvh]` column whose body the caller makes the
|
||||
scroller) and the same blurred overlay. Modal, DialogContent and every Sheet share one scrim,
|
||||
scroller) and the same blurred overlay. Modal, DialogContent, every Sheet and
|
||||
the phone sidebar's backdrop (`Navigation.tsx`, at `z-20` so an open sidebar
|
||||
dims the top bar, with SheetOverlay's `animate-in fade-in-0`) share one scrim,
|
||||
`overlayScrim` in `lib/utils.ts` (`bg-black/25 backdrop-blur-xs
|
||||
dark:bg-black/50`).
|
||||
|
||||
A right drawer (`SheetContent side="right"`) is on `bg-background`, like the
|
||||
page: its content paints no surface of its own, and panels in it are
|
||||
`subtle` (see Card surfaces). An agent is previewed in one shared drawer,
|
||||
`agents/components/AgentPreviewSheet`, for workflow and classic agents alike:
|
||||
`w-full sm:max-w-[600px] md:max-w-[700px] lg:max-w-[800px]`, a `SheetTitle`
|
||||
`subtle` (see Card surfaces). Its width is `size`, by role, never a `w-*` or
|
||||
`max-w-*` class (enforced): `default` (384px, 75% on a phone) for a companion
|
||||
list read beside the chat (an answer's sources, the phone artifact sheet);
|
||||
`detail` (576px, full width on a phone) for one record's fields (a schedule
|
||||
run); `wide` (600 / 700 / 800px at `sm` / `md` / `lg`, full width on a phone)
|
||||
for a working surface (a trace, an agent preview). An agent is previewed in
|
||||
one shared drawer, `agents/components/AgentPreviewSheet`, for workflow and
|
||||
classic agents alike: `size="wide"`, a `SheetTitle`
|
||||
and a one-line `SheetDescription` (the agent's name) in a header padded
|
||||
`pr-12` to clear the close X, an `info` Running badge while it answers, a
|
||||
`Separator`, then the preview. The workflow preview's Execution details and
|
||||
Artifacts rows are the answer's step-row recipe (see Alert), each step a
|
||||
`subtle sm` panel with its output in a `filled sm` well.
|
||||
|
||||
Every phone bottom sheet has one shape: `bg-card`, 16px top corners, no top
|
||||
border, `max-h-sheet` (the visible viewport less the top safe-area inset and a
|
||||
Every phone bottom sheet has one shape: `bg-card`, 18px top corners (`rounded-t-2xl`), no top
|
||||
border, `shadow-lg` (both `SheetContent side="bottom"` and Modal's phone
|
||||
sheet), `max-h-sheet` (the visible viewport less the top safe-area inset and a
|
||||
3rem strip, so the scrim above it can always be tapped to close), and bottom
|
||||
padding that clears the iPhone home indicator. Never cap a sheet with `vh` (see
|
||||
Viewport heights): a `90vh` sheet slides under Safari's URL bar and covers its
|
||||
@@ -918,8 +1047,9 @@ scrim. `SheetContent side="bottom"` gives the
|
||||
shape with `pb-safe-0` (the bare inset); pass `handle` for the grab bar, which
|
||||
also hides the X (the overlay dismisses it; the handle is only a cue and doesn't
|
||||
drag; pass `showCloseButton` to keep an X).
|
||||
`Modal mobileVariant="sheet"` shares the shape and the `SheetHandle`, with
|
||||
`pb-safe` (the inset, at least 1rem) under its footer. `pb-safe`, `pb-safe-0`
|
||||
`Modal mobileVariant="sheet"` shares the shape and the `SheetHandle` (and,
|
||||
like a handled sheet, has no X), with `pb-safe` (the inset, at least 1rem)
|
||||
under its footer. `pb-safe`, `pb-safe-0`
|
||||
and `max-h-sheet` are the `index.css` utilities for the safe-area insets; never
|
||||
spell `env()` in a class.
|
||||
|
||||
@@ -933,35 +1063,14 @@ unmounts, and Safari samples the page colour again. `useDarkTheme` calls
|
||||
`resetBottomTint()` and `resetTopTint()` (a 16px strip on the top edge) when the
|
||||
theme changes. Anything else fixed to the bottom edge that can go away (a
|
||||
drawer, a bottom banner) calls `resetBottomTint()` when it is removed. The sizes
|
||||
and the one-frame life were measured on an iPhone: 4px at the bottom, 6px at
|
||||
the top and a 16ms timer all failed, and a strip behind the content,
|
||||
transparent or at `opacity-0` isn't sampled, so don't shrink or hide it.
|
||||
and the one-frame life were measured on an iPhone (6px at the bottom, 16px at
|
||||
the top), and a strip behind the content, transparent or at `opacity-0` isn't
|
||||
sampled, so don't shrink or hide the strip.
|
||||
|
||||
**Open question: side panel or right sheet for chat content.** Chat has two
|
||||
ways to show something beside an answer. Notes, todos and files open in
|
||||
`components/ArtifactSidebar`, a panel that takes a column and leaves the chat
|
||||
usable. An answer's full source list opens in a right `Sheet`, which blurs
|
||||
and blocks the chat, so the answer being checked is hidden while its sources
|
||||
are read. No rule picks between them yet. The leading proposal is one
|
||||
surface: content read alongside the chat (artifacts, sources, a cited
|
||||
source) opens in the side panel, with citation chips opening it at that
|
||||
source; overlays stay for tasks that interrupt (forms, confirmations,
|
||||
pickers); phones keep the bottom sheet. Until that is decided, don't add a
|
||||
third pattern: new "read beside the chat" content uses the side panel.
|
||||
|
||||
In a picker list, mark the item that is currently chosen with
|
||||
`CommandItem checked` (a `secondary` brand tint through `data-checked`), not
|
||||
with `bg-accent`: cmdk's own `data-selected` highlight is `bg-accent` and
|
||||
follows the pointer and arrow keys, so an accent fill would look like hover.
|
||||
While a checked item is also highlighted it keeps its tint and text and gains
|
||||
a 1px inset `primary` ring, so the chosen row never turns plain grey.
|
||||
|
||||
A picker's footer (`MultiSelectPopover footer`) links to the page that manages
|
||||
the list, as a `link inline` Button with a 12px `ArrowRight` (Go to Sources, Go
|
||||
to Tools). A shortcut action (Upload new, an `outline-primary pill`) sits at the
|
||||
right end of the same row: `flex flex-wrap items-center justify-between gap-3`,
|
||||
so on a narrow sheet or in a long locale it wraps under the link rather than
|
||||
taking a row of its own everywhere (`SourcesPopoverFooter`).
|
||||
Content read alongside the chat (notes, todos, files) opens in
|
||||
`components/ArtifactSidebar`: a split column beside the chat on desktop, a
|
||||
right `Sheet` on phones. An answer's full source list is a right `Sheet`.
|
||||
Don't add a third pattern.
|
||||
|
||||
### ActionMenu (`ui/dropdown-menu.tsx`)
|
||||
|
||||
@@ -970,23 +1079,30 @@ The three-dots menu on a card, tile or row. Pass `options: MenuOption[]`
|
||||
`disabled`) and a `triggerLabel`. It renders a `ghost-on-accent` `icon-xs`
|
||||
trigger with `EllipsisVertical`, and stops clicks and keys on the trigger and
|
||||
the menu from reaching the host, so it can sit inside a clickable card.
|
||||
`className` takes layout only (position, margin) and lands on the trigger;
|
||||
In a page header or toolbar on a plain surface (the agent Overview's title
|
||||
row, through `SectionShell titleAction`; the workflow builder's toolbar), pass
|
||||
`size="toolbar"`: the trigger is a 36px `ghost-muted` icon button like the
|
||||
others there, hovering to `accent`, with its tooltip below. The default
|
||||
`row` trigger's darker hover is only for hosts that turn `accent` themselves.
|
||||
`align` (default `end`) places the menu, `disabled` greys the trigger, and
|
||||
`triggerTestId` puts a `data-testid` on it. `className` takes layout only (position, margin) and lands on the trigger;
|
||||
the menu is at least 144px wide and grows with its labels. `open`/`onOpenChange` make it controlled. Don't hand-build
|
||||
this menu from `DropdownMenu`, and don't declare a local option type.
|
||||
|
||||
### Table, Label, dialog text
|
||||
|
||||
Every table is `ui/table`; never a raw `<table>` or a table utility class (the
|
||||
chat's markdown tables are the one exception). The header row is dense by
|
||||
default (`px-2 py-1 text-sm font-normal text-foreground`, 28px, on
|
||||
Every table is `ui/table`; never a raw `<table>` (enforced) or a table
|
||||
utility class (the chat's markdown tables are the one exception, see Approved
|
||||
exceptions). The header row is dense by
|
||||
default (`px-2 py-1 lg:px-3 text-sm font-normal text-foreground`, 28px, on
|
||||
`TableHead`'s sticky `bg-muted` strip). A `TableRow` hovers (`bg-accent`,
|
||||
pointer) only when it has an `onClick`; a read-only row does not. Use
|
||||
`TableContainer` for the bordered, scrolling frame; a table that already sits
|
||||
in a frame renders `Table` alone. `Table` keeps a 600px minimum so wide
|
||||
tables scroll sideways; a narrow one inside a panel passes
|
||||
`minWidth="min-w-0"`. A fixed icon column is `width="50px" align="center"`.
|
||||
`minWidth="min-w-0"`. A fixed icon column is `width="40px" align="center"`.
|
||||
`TableCell` and `TableHeader` accept typography and alignment classes and
|
||||
`text-muted-foreground` (RunLog's headers pass the eyebrow). `Label`, `DialogTitle`, `DialogDescription`,
|
||||
`text-muted-foreground`. `Label`, `DialogTitle`, `DialogDescription`,
|
||||
`SheetTitle` and `SheetDescription` accept typography plus
|
||||
`text-foreground` and `text-muted-foreground`. `DialogContent`,
|
||||
`PopoverContent`, `SheetContent`, `SheetHeader`, `SheetFooter`,
|
||||
@@ -1019,8 +1135,8 @@ runs under the tab bar, and the app shell can be dragged and bounce. ESLint
|
||||
(`no-restricted-syntax`) rejects both. Pick by what the height does:
|
||||
|
||||
- **`dvh`** (the visible viewport, follows the toolbars): the app shell
|
||||
(`h-dvh`), full-screen states (`min-h-dvh`: 404, MobileBlocker, the `screen`
|
||||
`LoadingState`), and caps on things that pop up over the page (a search
|
||||
and the `screen` `LoadingState` (`h-dvh`), full-screen states (`min-h-dvh`:
|
||||
404, MobileBlocker), and caps on things that pop up over the page (a search
|
||||
dropdown's `max-h-[calc(100dvh-200px)]`, `Modal`'s `85dvh`).
|
||||
- **`svh`** (the smallest viewport, steady): a fixed-size panel inside a page
|
||||
that scrolls (the Logs panel's `h-[55svh]`, a list's `max-h-[45svh]`, the
|
||||
@@ -1033,12 +1149,16 @@ runs under the tab bar, and the app shell can be dragged and bounce. ESLint
|
||||
|
||||
### Typography roles
|
||||
|
||||
- **Page title**: `SectionPageHeader` (24px bold); the only other text at
|
||||
that size is a stat figure (`StatCard`).
|
||||
- **Page title**: `SectionPageHeader` (24px bold; the phone section index,
|
||||
`SectionIndexPage`, draws the same `h1`); the only other text at that size
|
||||
is a stat figure (`StatCard`).
|
||||
- **Title** of a dialog, sheet, drawer header or detail page: `text-xl
|
||||
leading-tight font-semibold`. `DialogTitle` and `SheetTitle` default to it;
|
||||
`font-bold` is never a title weight. A picker popover's header is a
|
||||
sub-heading, not a title.
|
||||
sub-heading, not a title. The composer's drag-and-drop prompt is a Title
|
||||
in `foreground`. The artifact panel's title (phone sheet and desktop split
|
||||
column alike) is a sub-heading, `text-sm font-semibold wrap-anywhere`: the
|
||||
panel sits beside the chat and its title is often a long generated name.
|
||||
- **Section title**: `SectionHeader` (18px). **Sub-heading** inside a panel,
|
||||
drawer or modal: `SectionHeader size="xs"` (14px semibold). **Eyebrow**
|
||||
(anything set in caps): `SectionHeader size="sm"`; never `uppercase` on a
|
||||
@@ -1064,7 +1184,7 @@ leading-tight font-semibold`. `DialogTitle` and `SheetTitle` default to it;
|
||||
is `wrap-anywhere`, which also lets a table cell or flex item shrink below
|
||||
the token instead of widening the table. Both break at spaces first and
|
||||
split a token only when it would overflow. Never `break-all`: it splits
|
||||
ordinary words mid-word ("notif / ications").
|
||||
ordinary words mid-word ("notif / ications"); the lint rejects it.
|
||||
|
||||
### Rhythm
|
||||
|
||||
@@ -1078,11 +1198,14 @@ gap, so drop it).
|
||||
### Radius by role
|
||||
|
||||
`rounded-xs` 2px, `rounded-sm` 6px, `rounded-md` 8px, `rounded-lg` 10px,
|
||||
`rounded-xl` 14px, `rounded-2xl` 18px (from `--radius`); bare `rounded` is a
|
||||
fixed 4px, so don't use it. Skeleton bars: the default. Tinted icon squares:
|
||||
`rounded-xl` 14px, `rounded-2xl` 18px, `rounded-3xl` 22px, `rounded-4xl`
|
||||
26px (from `--radius`); bare `rounded` is a fixed 4px, so don't use it
|
||||
(enforced). `3xl` is the chat's own shapes (the composer, the question
|
||||
bubble) and `4xl` the answer's source cards and the landing page's model
|
||||
picker; app chrome stops at `2xl`. Skeleton bars: the default. Tinted icon squares:
|
||||
`rounded-md` at `size-7|8`, `rounded-xl` at `size-12|14`, `rounded-2xl` at
|
||||
`size-20`. `rounded-full` only for avatars, status dots and the workflow
|
||||
palette pills.
|
||||
`size-20`. `rounded-full` only for pills (`shape="pill"` controls and
|
||||
fields), avatars, status dots and the workflow palette pills.
|
||||
|
||||
## Motion
|
||||
|
||||
@@ -1093,8 +1216,8 @@ collapsibles, `duration-300 ease-in-out` on the named property for the shell
|
||||
(sidebar, main column, top buttons); a shadow or ring change is
|
||||
`transition-shadow`, several at once a bare `transition`. No `transition-all`
|
||||
(the floating labels in `form-field.tsx` and `input.tsx`, which move and
|
||||
resize, are the exception) and no `hover:scale`: hover is a fill, border or
|
||||
text-colour change.
|
||||
resize, are the exception) and no `hover:scale-*`, both enforced: hover is a
|
||||
fill, border or text-colour change.
|
||||
|
||||
New entrances are `animate-in fade-in duration-200 motion-reduce:animate-none`
|
||||
(`SectionRail`). Two chat timings are decided exceptions: the disclosure fade
|
||||
@@ -1106,16 +1229,16 @@ fade-in slide-in-from-bottom-1.5 duration-260 ease-out`, both with
|
||||
|
||||
Phone / desktop is `lg` (1024px) in classes and `isDesktop` in JS
|
||||
(`useMediaQuery`); `sm` and `md` only reflow content. Wide two-column layouts
|
||||
that need more room (Analytics' chart rows, the agent form beside its preview)
|
||||
go two-up at `xl`. No custom breakpoints (`min-[…]`, `max-[…]`,
|
||||
`[@media(…)]`). The workflow builder needs `lg`; below it `MobileBlocker`
|
||||
that need more room (Analytics' chart rows) go two-up at `xl`. No custom
|
||||
breakpoints (`min-[…]`, `max-[…]`, `[@media(…)]`; enforced). The workflow builder needs `lg`; below it `MobileBlocker`
|
||||
shows.
|
||||
|
||||
## Focus, elevation and stacking
|
||||
|
||||
- **Focus ring**: `focus-visible:ring-3 focus-visible:ring-ring/50` plus
|
||||
`focus-visible:border-ring` on fields. Never `focus:` (mouse users see it)
|
||||
and never `ring-2` or `ring-[3px]`; the `ui/` components already carry it,
|
||||
and never `ring-2` or `ring-[3px]` (a `focus:` or `focus-within:` ring
|
||||
with `ring-2` is enforced); the `ui/` components already carry it,
|
||||
so plain elements should become components rather than copy the classes.
|
||||
Inside `ui/`, spell it with the `focusRing` constant from `lib/utils.ts`
|
||||
(with `invalidState` and `fieldFrame` for fields) rather than retyping it.
|
||||
@@ -1130,8 +1253,10 @@ shows.
|
||||
A dialog panel is a `tabIndex=-1` focus target that Radix can focus, so it
|
||||
carries `outline-none`: Safari draws its own blue `outline: auto` there and
|
||||
ignores our `outline-color`. Any new focusable container needs the same.
|
||||
- **Close buttons** on Modal, Sheet and DialogContent are a `ghost-muted`
|
||||
`size="icon-sm"` Button (32px, accent square on hover) at `top-2 right-2`.
|
||||
- **Close buttons**: every Modal, Sheet and DialogContent uses its built-in
|
||||
close (a phone sheet with a handle has none), a `ghost-muted` `size="icon-sm"` Button (32px, accent square on
|
||||
hover) at `top-2 right-2`, the artifact sheet included; never hand-place
|
||||
another X.
|
||||
- **Disabled**: buttons (Button, Accordion, Tabs) use
|
||||
`disabled:pointer-events-none disabled:opacity-50`, so the pointer passes
|
||||
through. Fields (Input, SelectTrigger, Switch, Textarea, CommandInput,
|
||||
@@ -1150,15 +1275,18 @@ shadow-lg` in both themes: the knob is white on any track, and a white knob
|
||||
publish-error panel, which float over the canvas at popover elevation
|
||||
(`shadow-md`, `z-20`); the chunk viewer's and file tree's search result
|
||||
lists (`shadow-md`, in-page `z-20`, their results are `CommandItem`s inside
|
||||
the search frame); and the Hero model picker's menu (an Approved
|
||||
exception). Don't add new ones: a floating panel is a `Popover`,
|
||||
the search frame); the sliding panel in `navigation/SidebarLevel.tsx`,
|
||||
which casts a horizontal shadow while it slides; and the Hero model
|
||||
picker's menu (both Approved exceptions). Don't add new ones: a floating panel is a `Popover`,
|
||||
`DropdownMenu` or `Modal`, which carry their own shadow and stacking; never
|
||||
an `absolute` div with a shadow and a hand-picked `z-*`.
|
||||
- **Stacking**: `z-10` sticky headers and table heads inside a page;
|
||||
`z-20` in-page floating chrome (banners, scroll-to-bottom, drag
|
||||
handles); `z-50` overlays, modals, sheets, toasts and the one-frame
|
||||
bar-tint strips; `z-200` every portalled floating list (popover, menu,
|
||||
select, tooltip), so it opens above a Modal without an override. Do not invent values in between. The app
|
||||
select, tooltip), so it opens above a Modal without an override. Do not
|
||||
invent values in between: a `z-*` outside 0, 10, 20, 50, 200 and `auto` is
|
||||
rejected (enforced). The app
|
||||
shell uses the low layers too: the phone top bar is `z-10`, and the
|
||||
sidebar and its phone backdrop are `z-20`, so an open sidebar dims the bar.
|
||||
|
||||
@@ -1189,7 +1317,7 @@ list stays reviewable.
|
||||
| `agents/workflow/WorkflowPreview.tsx` | `shadcn/no-restyle` | The Preview minimap's node rows are status tiles: the fill, border and ring follow the step (success, primary running + pulse, destructive, muted pending; a ring on the active row) and stay pinned on hover, pending and running rows stay unfaded while disabled, and clickable rows dim to 80% on hover. No Button variant is status-tinted. The rule reports each string inside `cn(...)`, so it's a `/* eslint-disable */` … `/* eslint-enable */` pair around the `className` attribute. |
|
||||
| `agents/schedules/ScheduleFormModal.tsx` | `shadcn/no-restyle` | The schedule's name is the dialog's editable title: a `bare` Input with title type (`text-xl font-semibold`), so the dialog passes `hideTitle`. One disable. |
|
||||
| `components/MermaidRenderer.tsx` | `shadcn/no-restyle` | The zoom − / + buttons sit on the diagram's `bg-black/70` overlay, where ghost's accent hover paints a light square with dark text; they hover to `white/20` with white text instead, in both themes. Two disables. |
|
||||
| `Hero.tsx` | `shadcn/no-restyle` | The landing page's demo cards are `Button outline lg pill`, but each is a two-line pill (a title over a clamped 12px query), so it undoes lg's height, the base's one-row layout, weight and nowrap: `h-auto w-full flex-col items-start gap-0 py-3.5 text-left text-xs font-normal whitespace-normal`. One disable (phase 7, 36a). |
|
||||
| `Hero.tsx` | `shadcn/no-restyle` | The landing page's demo cards are `Button outline lg pill`, but each is a two-line pill (a title over a clamped 12px query), so it undoes lg's height, the base's one-row layout, weight and nowrap: `h-auto w-full flex-col items-start gap-0 py-3.5 text-left text-xs font-normal whitespace-normal`. One disable. |
|
||||
| `Navigation.tsx`, `conversation/ConversationTile.tsx` | `shadcn/no-restyle` | A sidebar row whose link has sibling buttons (an agent's pin, a conversation's menu and rename Save / Cancel) keeps its fill while the pointer is on a sibling or the menu is open (`group-hover:bg-sidebar-accent`, `bg-sidebar-accent`), and `pr-10` keeps the label clear of the buttons. ConversationTile's `cn(...)` needs a `/* eslint-disable */` … `/* eslint-enable */` pair. |
|
||||
| `admin/Usage.tsx` | `shadcn/no-restyle` | The Top users id is a `link inline` Button inside a mono table cell; it keeps the cell's type and wraps (`font-mono text-xs font-normal whitespace-normal text-left`). One disable. |
|
||||
| `agents/workflow/WorkflowBuilder.tsx` | `shadcn/no-restyle` | The "Learn more" links in the Set state and Condition nodes' 12px hints keep the sentence's size and weight (`text-xs font-normal` on `link inline`). Two disables. |
|
||||
@@ -1197,35 +1325,23 @@ list stays reviewable.
|
||||
| `settings/PersonalAccessTokens.tsx` | `shadcn/no-restyle` | Token scope chips are identifiers, so the `neutral` Badge is set in mono (`font-mono`). One disable. |
|
||||
| `agents/workflow/WorkflowPreview.tsx` | `shadcn/no-restyle` | A step's state changes (keys and values the app serialised) are `neutral` Badges set in mono (`font-mono`), like token scopes. One disable. |
|
||||
| `settings/traces/TraceChips.tsx` | `shadcn/no-restyle` | Trace stat chips (durations, counts) use tabular figures so they don't jitter between rows (`tabular-nums` on Badge). One disable. |
|
||||
| `modals/MCPServerModal.tsx` | `shadcn/no-restyle` | The authorization link inside the test-result Alert keeps the Alert's status colour (`text-current` on `link inline`). One disable (phase 11, 63g). |
|
||||
| `components/MermaidRenderer.tsx` | `shadcn/no-restyle` | The zoom readout between − and + is a `link inline` Button on the `bg-black/70` overlay; it keeps the overlay's white 12px regular text (`text-xs font-normal text-current`). One disable, beside the two zoom-button ones above (phase 11, 63g). |
|
||||
| `conversation/SharedConversation.tsx` | `shadcn/no-restyle` | The "DocsGPT" link sits in the `/share/:id` page's regular-weight byline (`font-normal`). One disable (phase 11, 63g). |
|
||||
| `conversation/ConversationBubble.tsx` | `shadcn/no-restyle` | A source card's URL row is a `link inline` around an `<a>`: foreground at rest, primary on hover, regular weight, truncating (`text-current font-normal hover:text-primary underline-offset-2 max-w-full justify-start`). One disable (phase 11, 63g). |
|
||||
| `admin/Overview.tsx` | `shadcn/no-restyle` | "View in Audit" under the denied-sign-ins tile keeps the tile's destructive tone at hint size (`text-destructive text-xs font-normal`). One disable (phase 11, 63g). |
|
||||
| `settings/PairDeviceModal.tsx` | `shadcn/no-restyle` | The install link in Pair a remote machine sits in a 12px hint (`text-xs font-normal`, `self-start` in its column). One disable (phase 11, 63g). |
|
||||
| `modals/MCPServerModal.tsx` | `shadcn/no-restyle` | The authorization link inside the test-result Alert keeps the Alert's status colour (`text-current` on `link inline`). One disable. |
|
||||
| `components/MermaidRenderer.tsx` | `shadcn/no-restyle` | The zoom readout between − and + is a `link inline` Button on the `bg-black/70` overlay; it keeps the overlay's white 12px regular text (`text-xs font-normal text-current`). One disable, beside the two zoom-button ones above. |
|
||||
| `conversation/SharedConversation.tsx` | `shadcn/no-restyle` | The "DocsGPT" link sits in the `/share/:id` page's regular-weight byline (`font-normal`). One disable. |
|
||||
| `conversation/ConversationBubble.tsx` | `shadcn/no-restyle` | A source card's URL row is a `link inline` around an `<a>`: foreground at rest, primary on hover, regular weight, truncating (`text-current font-normal hover:text-primary underline-offset-2 max-w-full justify-start`). One disable. |
|
||||
| `admin/Overview.tsx` | `shadcn/no-restyle` | "View in Audit" under the denied-sign-ins tile keeps the tile's destructive tone at hint size (`text-destructive text-xs font-normal`). One disable. |
|
||||
| `settings/PairDeviceModal.tsx` | `shadcn/no-restyle` | The install link in Pair a remote machine sits in a 12px hint (`text-xs font-normal`, `self-start` in its column). One disable. |
|
||||
| `agents/workflow/WorkflowBuilder.tsx` | `shadcn/no-restyle` | The publish-error Alert floats over the canvas with its close button in the top-right corner, so it pads `pr-10` to keep a long title clear of the button. One disable. |
|
||||
| `conversation/MarkdownAnswer.tsx` | `no-restricted-syntax` | Markdown answer tables render the model's own table markup with its own styling, so they stay a raw `<table>`. |
|
||||
|
||||
Note that a multi-line reason has to be a `/* ... */` block comment;
|
||||
consecutive `//` lines only disable the next comment line, not the code.
|
||||
|
||||
## Removed classes
|
||||
|
||||
The lint reports classes Tailwind cannot generate; `no-unknown-classes` is at
|
||||
`error`, so none remain. For the record, they were bugs, not missing
|
||||
configuration:
|
||||
|
||||
- `xs:` has never been a breakpoint here (not in the old Tailwind v3 config
|
||||
either), so `xs:px-3`, `xs:text-xs` never applied. Remove them rather than
|
||||
declaring the breakpoint, which would change layouts that were never seen.
|
||||
- `text-s`, `dark:text-gray`, `w-inherit` are typos for `text-sm`,
|
||||
`dark:text-gray-*` and `w-[inherit]`.
|
||||
- `agents-container`, `conversations-container`, `loader` and `mermaid` are
|
||||
unused hooks; nothing selects them. Before deleting a class like these,
|
||||
grep for it in `querySelector` and `classList` calls too. `star` was
|
||||
defined at runtime by a `<style>` element in `components/Notification.tsx`;
|
||||
it is now the `notification-star` utility in `src/index.css`. `msc-spacer`
|
||||
was a lookup hook for `ConversationMessages`' `querySelector`; the lookup
|
||||
now uses the primitive's `[data-message-scroller-spacer]` attribute.
|
||||
`no-unknown-classes` is at `error`. Before deleting a class the lint calls
|
||||
unknown, grep for it in `querySelector`, `classList` and `data-` lookups; a
|
||||
class can be a hook for code rather than a style.
|
||||
|
||||
## Live style guide
|
||||
|
||||
@@ -1237,20 +1353,21 @@ variant or token, add it there too.
|
||||
|
||||
## Cleanup playbook
|
||||
|
||||
Every `shadcn/*` rule is at `error`, and `lint-staged` runs the lint on
|
||||
staged files, so a violation blocks the commit. Work one file at a time and
|
||||
Every `shadcn/*` rule is at `error`, and on commit `lint-staged` runs
|
||||
`npm run lint-fix` and `npm run format` (both over the whole `src`), so a
|
||||
violation blocks the commit. Work one file at a time and
|
||||
keep every step green.
|
||||
|
||||
1. **See the state.** `npm run lint:design` prints warnings by rule and the
|
||||
worst files. `npm run lint:design -- --file src/settings/Teams.tsx` lists
|
||||
every warning in one file with the fix the message proposes;
|
||||
1. **See the state.** `npm run lint:design` prints violations (`shadcn/*`
|
||||
and the design `no-restricted-syntax` selectors) by rule and the worst
|
||||
files. `npm run lint:design -- --file src/settings/Teams.tsx`
|
||||
lists every violation in one file with the fix the message proposes;
|
||||
`-- --rule no-raw-colors` lists files for one rule.
|
||||
2. **Pick a file, read it whole**, then fix all of its design warnings in
|
||||
2. **Pick a file, read it whole**, then fix all of its design violations in
|
||||
one pass. Map raw colours to tokens (table above), replace hand-rolled
|
||||
boxes, pills, tiles, spinners and dropzones with the `ui/` components,
|
||||
and move Button/Input/Select overrides onto `variant`, `size`, `shape`.
|
||||
Delete the `dark:` twin of every class you tokenise. Remove classes the
|
||||
lint calls unknown (`xs:*`, `text-s`, `dark:text-gray`, unused hooks).
|
||||
Delete the `dark:` twin of every class you tokenise.
|
||||
3. **Do not restyle `ui/` components from a page.** If a treatment is used
|
||||
in more than one place and no variant fits, add the variant in the
|
||||
component file, add it to `src/design/DesignSystem.tsx`, and document it
|
||||
@@ -1259,26 +1376,23 @@ keep every step green.
|
||||
translations (`t(...)` keys stay). Only classes and component choice
|
||||
change. Prettier with the Tailwind plugin reorders classes; run
|
||||
`npm run lint-fix` rather than fighting it.
|
||||
5. **Verify per file**: `npx eslint <file>` (zero `shadcn/*` warnings),
|
||||
5. **Verify per file**: `npx eslint <file>` (zero errors),
|
||||
`npx tsc --noEmit -p tsconfig.json`, `npm test`, and look at the screen
|
||||
in the running app (`npm run dev`, then the page that renders the file)
|
||||
in both themes.
|
||||
|
||||
Known traps:
|
||||
|
||||
- `npm run lint` also reports 6 pre-existing Prettier errors (union-type
|
||||
line breaks in `cronBuilder.ts`, `types/schedule.ts`, `types/workflow.ts`,
|
||||
`api/client.ts`, `models/misc.ts`, `models/types.ts`) and about 330
|
||||
TypeScript warnings unrelated to design; `npm run lint-fix` clears the
|
||||
Prettier ones.
|
||||
- `npm run lint` reports 0 errors and about 325 `@typescript-eslint`
|
||||
warnings (mostly `no-explicit-any`) unrelated to design; `tsc` is clean.
|
||||
- Port 5173 may be held by another checkout; check the Vite banner shows
|
||||
this repo's path before trusting what you see.
|
||||
- `components/SkeletonLoader.tsx` renders `Skeleton` in its table, logs,
|
||||
default and files layouts and its four tile mirrors (Card + bars). Its
|
||||
analysis, dropdown, chunk-card and connected-state layouts still pulse their
|
||||
own surface; move them onto Card + `Skeleton` the same way when you touch
|
||||
them. (`agents/schedules/StatusBadge.tsx` is a status→variant map over
|
||||
`Badge`, not a wrapper; keep it.)
|
||||
- `components/SkeletonLoader.tsx`: the chunk, source, tool, add-tool and
|
||||
agent layouts are Card mirrors with `Skeleton` bars, and the `chatbot`,
|
||||
table, logs, default, analysis, connected-state and files layouts use
|
||||
`Skeleton`; none pulses its own surface. `renderFilesSection` (and the
|
||||
GoogleDrivePicker box it mirrors) is still a raw `border rounded-lg` box;
|
||||
move both onto `Card` when you touch them.
|
||||
|
||||
## Working with the lint
|
||||
|
||||
@@ -1286,6 +1400,11 @@ Known traps:
|
||||
cd frontend && npm run lint
|
||||
```
|
||||
|
||||
All `shadcn/*` rules are `error`. Add a variant or token only when a treatment
|
||||
All `shadcn/*` rules are `error`, and so are the DESIGN.md selectors under
|
||||
`no-restricted-syntax`: `eslint/design-rules.js` (class and markup rules,
|
||||
tested in `src/design/designRules.lint.test.ts`), `eslint/card-surfaces.js`
|
||||
(tested in `src/design/cardSurfaces.lint.test.ts`) and the viewport-height
|
||||
and focus-return entries in `eslint.config.js`. Add a new checkable rule as
|
||||
a selector there, with a test and a line here. Add a variant or token only when a treatment
|
||||
is used in more than one place and none of the existing ones fits; a single
|
||||
special case gets a disable comment with a reason.
|
||||
Reference in new issue
Block a user