From f66dc50eed24dcf1c06d8cd7bc29f2491ada937f Mon Sep 17 00:00:00 2001 From: Pavel Date: Sat, 26 Sep 2026 12:18:05 +0400 Subject: [PATCH] Phase 2 --- AGENTS.md | 4 + frontend/DESIGN.md | 694 +++++++-- frontend/eslint.config.js | 76 +- frontend/package-lock.json | 6 - frontend/package.json | 6 - frontend/prettier.config.cjs | 1 + frontend/src/App.tsx | 23 +- frontend/src/Hero.tsx | 28 +- frontend/src/Navigation.tsx | 289 ++-- frontend/src/PageNotFound.tsx | 6 +- frontend/src/admin/Activity.tsx | 165 +-- frontend/src/admin/AdminUI.tsx | 65 +- frontend/src/admin/Admins.tsx | 27 +- frontend/src/admin/LoadError.test.tsx | 40 + frontend/src/admin/Overview.tsx | 72 +- frontend/src/admin/QuotaEditor.tsx | 11 +- frontend/src/admin/Quotas.tsx | 163 ++- frontend/src/admin/Usage.tsx | 121 +- frontend/src/admin/UsageChart.test.ts | 35 + frontend/src/admin/UsageChart.tsx | 98 +- frontend/src/admin/UserQuotaModal.tsx | 18 +- frontend/src/admin/UserUsageModal.tsx | 81 +- frontend/src/admin/Users.tsx | 116 +- frontend/src/admin/index.tsx | 48 +- frontend/src/admin/usageChartData.test.ts | 28 + frontend/src/admin/usageChartData.ts | 41 +- frontend/src/agents/AgentCard.tsx | 31 +- frontend/src/agents/AgentLogs.tsx | 53 +- frontend/src/agents/AgentPreview.tsx | 2 +- frontend/src/agents/AgentsList.tsx | 115 +- frontend/src/agents/FolderCard.tsx | 24 +- frontend/src/agents/NewAgent.test.tsx | 37 + frontend/src/agents/NewAgent.tsx | 263 ++-- frontend/src/agents/SharedAgent.tsx | 31 +- frontend/src/agents/SharedAgentCard.tsx | 42 +- .../agents/components/AgentTypeModal.test.tsx | 4 +- .../src/agents/components/AgentTypeModal.tsx | 14 +- .../components/GuardrailEvents.test.tsx | 100 +- .../src/agents/components/GuardrailEvents.tsx | 238 ++- .../components/GuardrailsSection.test.tsx | 114 +- .../agents/components/GuardrailsSection.tsx | 127 +- .../agents/schedules/RunDetailDrawer.test.tsx | 136 ++ .../src/agents/schedules/RunDetailDrawer.tsx | 171 ++- frontend/src/agents/schedules/RunLog.tsx | 74 +- .../agents/schedules/ScheduleFormModal.tsx | 147 +- .../schedules/SchedulerToolCallCard.test.ts | 96 +- .../schedules/SchedulerToolCallCard.tsx | 149 +- .../src/agents/schedules/SchedulesView.tsx | 431 +++--- .../src/agents/schedules/StatusBadge.test.tsx | 16 +- frontend/src/agents/schedules/StatusBadge.tsx | 7 +- .../src/agents/schedules/TimezoneCombobox.tsx | 7 +- .../src/agents/schedules/cronBuilder.test.ts | 62 + frontend/src/agents/schedules/cronBuilder.ts | 70 +- .../src/agents/workflow/WorkflowBuilder.tsx | 1293 +++++++++++------ .../agents/workflow/WorkflowMiniMap.test.tsx | 9 +- .../src/agents/workflow/WorkflowPreview.tsx | 117 +- .../workflow/WorkflowRunArtifacts.test.tsx | 95 ++ .../agents/workflow/WorkflowRunArtifacts.tsx | 99 +- .../workflow/components/MobileBlocker.tsx | 15 +- .../components/NodeDocumentsControl.tsx | 66 +- .../components/PromptTextArea.test.tsx | 77 + .../workflow/components/PromptTextArea.tsx | 342 ++--- .../src/agents/workflow/nodes/BaseNode.tsx | 18 +- .../src/agents/workflow/nodes/CodeNode.tsx | 25 +- .../agents/workflow/nodes/ConditionNode.tsx | 30 +- .../agents/workflow/nodes/SetStateNode.tsx | 29 +- frontend/src/agents/workflow/nodes/index.tsx | 46 +- .../agents/workflow/workflowPreviewSlice.ts | 4 +- frontend/src/components/ActionButtons.tsx | 32 +- frontend/src/components/AdminRoute.test.tsx | 5 +- frontend/src/components/AdminRoute.tsx | 8 +- .../src/components/ArtifactSidebar.test.tsx | 108 ++ frontend/src/components/ArtifactSidebar.tsx | 156 +- frontend/src/components/Chunks.test.tsx | 160 ++ frontend/src/components/Chunks.tsx | 476 +++--- frontend/src/components/ConfigFields.tsx | 13 +- frontend/src/components/ConnectorAuth.tsx | 2 +- frontend/src/components/ConnectorTree.tsx | 23 +- frontend/src/components/CopyButton.test.tsx | 32 + frontend/src/components/CopyButton.tsx | 83 +- .../src/components/DocumentArtifactView.tsx | 82 +- .../src/components/DocumentPagination.tsx | 132 -- frontend/src/components/FilePicker.test.tsx | 6 +- frontend/src/components/FilePicker.tsx | 107 +- frontend/src/components/FileTree.tsx | 11 +- frontend/src/components/FileUpload.tsx | 4 +- frontend/src/components/GoogleDrivePicker.tsx | 45 +- frontend/src/components/GraphView.tsx | 118 +- frontend/src/components/Help.tsx | 126 +- frontend/src/components/MarkdownPreview.tsx | 27 +- .../src/components/MermaidRenderer.test.tsx | 15 +- frontend/src/components/MermaidRenderer.tsx | 69 +- frontend/src/components/MessageInput.tsx | 95 +- .../components/MultiSelectPopover.test.tsx | 2 +- .../src/components/MultiSelectPopover.tsx | 45 +- frontend/src/components/Notification.tsx | 14 +- frontend/src/components/PageToolbar.test.tsx | 46 + frontend/src/components/PageToolbar.tsx | 46 + frontend/src/components/ProfileButton.tsx | 39 +- frontend/src/components/SearchInput.test.tsx | 24 + frontend/src/components/SearchInput.tsx | 35 + frontend/src/components/SkeletonLoader.tsx | 254 ++-- frontend/src/components/StatCard.test.tsx | 96 ++ frontend/src/components/StatCard.tsx | 78 + .../src/components/TextToSpeechButton.tsx | 30 +- frontend/src/components/ToolIcon.tsx | 4 +- frontend/src/components/UploadToast.tsx | 6 +- frontend/src/components/WikiViewer.tsx | 63 +- .../src/components/graphViewUtils.test.ts | 34 +- frontend/src/components/graphViewUtils.ts | 35 + .../message-input/AttachFileButton.tsx | 2 +- .../message-input/AttachmentChipList.tsx | 49 +- .../components/message-input/MicButton.tsx | 31 +- .../message-input/SourcesTrigger.tsx | 2 +- .../components/message-input/ToolsTrigger.tsx | 15 +- .../src/components/tree/PathHeader.test.tsx | 51 + frontend/src/components/tree/PathHeader.tsx | 88 ++ .../src/components/tree/TreeBrowser.test.tsx | 111 ++ frontend/src/components/tree/TreeBrowser.tsx | 204 ++- frontend/src/components/ui/accordion.tsx | 6 +- frontend/src/components/ui/alert.test.tsx | 64 + frontend/src/components/ui/alert.tsx | 21 +- frontend/src/components/ui/avatar.test.tsx | 17 +- frontend/src/components/ui/avatar.tsx | 31 +- frontend/src/components/ui/badge.test.tsx | 15 +- frontend/src/components/ui/badge.tsx | 15 +- frontend/src/components/ui/button.test.tsx | 53 +- frontend/src/components/ui/button.tsx | 44 +- frontend/src/components/ui/calendar.tsx | 20 +- frontend/src/components/ui/card.test.tsx | 44 +- frontend/src/components/ui/card.tsx | 42 +- frontend/src/components/ui/checkbox.test.tsx | 9 + frontend/src/components/ui/checkbox.tsx | 8 +- frontend/src/components/ui/command.test.tsx | 25 + frontend/src/components/ui/command.tsx | 8 +- .../components/ui/description-list.test.tsx | 48 + .../src/components/ui/description-list.tsx | 95 ++ frontend/src/components/ui/dialog.test.tsx | 64 +- frontend/src/components/ui/dialog.tsx | 14 +- frontend/src/components/ui/dropdown-menu.tsx | 48 +- frontend/src/components/ui/dropzone.test.tsx | 8 + frontend/src/components/ui/dropzone.tsx | 46 +- .../src/components/ui/empty-state.test.tsx | 55 + frontend/src/components/ui/empty-state.tsx | 97 ++ .../src/components/ui/form-field.test.tsx | 101 +- frontend/src/components/ui/form-field.tsx | 113 +- .../src/components/ui/icon-button.test.tsx | 55 + frontend/src/components/ui/icon-button.tsx | 57 + frontend/src/components/ui/input.test.tsx | 14 +- frontend/src/components/ui/input.tsx | 29 +- frontend/src/components/ui/label.tsx | 2 +- frontend/src/components/ui/list-row.test.tsx | 37 + frontend/src/components/ui/list-row.tsx | 93 ++ .../src/components/ui/loading-state.test.tsx | 36 + frontend/src/components/ui/loading-state.tsx | 57 + .../src/components/ui/message-scroller.tsx | 2 +- frontend/src/components/ui/modal.test.tsx | 35 +- frontend/src/components/ui/modal.tsx | 51 +- .../src/components/ui/multi-select.test.tsx | 21 +- frontend/src/components/ui/multi-select.tsx | 45 +- frontend/src/components/ui/option-card.tsx | 2 +- .../pagination-size.test.tsx} | 24 +- .../src/components/ui/pagination.test.tsx | 99 ++ frontend/src/components/ui/pagination.tsx | 160 ++ frontend/src/components/ui/popover.tsx | 2 +- .../src/components/ui/section-header.test.tsx | 57 + frontend/src/components/ui/section-header.tsx | 80 + frontend/src/components/ui/select.test.tsx | 28 +- frontend/src/components/ui/select.tsx | 17 +- frontend/src/components/ui/separator.test.tsx | 31 + frontend/src/components/ui/separator.tsx | 29 + frontend/src/components/ui/sheet.test.tsx | 53 +- frontend/src/components/ui/sheet.tsx | 45 +- frontend/src/components/ui/skeleton.test.tsx | 10 + frontend/src/components/ui/skeleton.tsx | 26 +- frontend/src/components/ui/spinner.test.tsx | 6 + frontend/src/components/ui/spinner.tsx | 4 +- frontend/src/components/ui/switch.tsx | 6 +- frontend/src/components/ui/table.test.tsx | 37 + frontend/src/components/ui/table.tsx | 8 +- frontend/src/components/ui/tabs.tsx | 6 +- frontend/src/components/ui/textarea.test.tsx | 3 +- frontend/src/components/ui/textarea.tsx | 19 +- frontend/src/components/ui/toast.tsx | 2 +- .../src/components/ui/toggle-group.test.tsx | 44 + frontend/src/components/ui/toggle-group.tsx | 54 + frontend/src/components/ui/tooltip.test.tsx | 22 + frontend/src/components/ui/tooltip.tsx | 47 +- .../src/components/wikiViewerUtils.test.ts | 26 +- frontend/src/components/wikiViewerUtils.ts | 25 - frontend/src/conversation/AnswerFlow.test.tsx | 4 +- frontend/src/conversation/AnswerFlow.tsx | 53 +- frontend/src/conversation/Conversation.tsx | 22 +- .../conversation/ConversationBubble.test.tsx | 186 ++- .../src/conversation/ConversationBubble.tsx | 312 ++-- .../src/conversation/ConversationMessages.tsx | 25 +- .../conversation/ConversationTile.test.tsx | 79 + .../src/conversation/ConversationTile.tsx | 84 +- frontend/src/conversation/MarkdownAnswer.tsx | 26 +- .../src/conversation/ResearchProgress.tsx | 74 +- .../src/conversation/SharedConversation.tsx | 24 +- .../src/conversation/StreamingStatusLine.tsx | 4 +- .../answerFlowIntegration.test.tsx | 2 +- frontend/src/conversation/quotaError.test.ts | 3 +- frontend/src/conversation/quotaError.ts | 7 +- frontend/src/design/DesignSystem.tsx | 1069 ++++++++++++-- frontend/src/hooks/index.ts | 20 +- frontend/src/hooks/useMediaQuery.test.tsx | 56 + frontend/src/index.css | 698 +-------- frontend/src/lib/markdown.test.tsx | 24 + frontend/src/lib/markdown.tsx | 17 + frontend/src/lib/utils.ts | 11 +- frontend/src/locale/accessTokens.test.ts | 9 - frontend/src/locale/de.json | 471 +++++- frontend/src/locale/en.json | 509 ++++++- frontend/src/locale/es.json | 471 +++++- frontend/src/locale/jp.json | 469 +++++- frontend/src/locale/ru.json | 489 ++++++- frontend/src/locale/zh-TW.json | 469 +++++- frontend/src/locale/zh.json | 469 +++++- frontend/src/main.tsx | 13 +- .../src/modals/AccessTokenCreatedModal.tsx | 57 +- frontend/src/modals/AddActionModal.tsx | 23 +- frontend/src/modals/AddToolModal.tsx | 75 +- frontend/src/modals/AgentDetailsModal.tsx | 71 +- frontend/src/modals/ConfigToolModal.tsx | 11 +- frontend/src/modals/ConfirmationModal.tsx | 4 +- .../src/modals/CreateAccessTokenModal.tsx | 10 +- frontend/src/modals/CustomModelModal.tsx | 34 +- frontend/src/modals/ImportAgentModal.test.tsx | 51 + frontend/src/modals/ImportAgentModal.tsx | 135 +- frontend/src/modals/ImportSpecModal.tsx | 90 +- frontend/src/modals/JWTModal.tsx | 33 +- frontend/src/modals/MCPServerModal.tsx | 77 +- .../src/modals/MoveToFolderModal.test.tsx | 5 +- frontend/src/modals/MoveToFolderModal.tsx | 199 ++- .../src/modals/RegenerateAccessTokenModal.tsx | 21 +- .../modals/SearchConversationsModal.test.tsx | 2 +- .../src/modals/ShareConversationModal.tsx | 71 +- frontend/src/navigation/SectionIndexPage.tsx | 56 +- frontend/src/navigation/SectionNav.tsx | 59 +- frontend/src/navigation/SectionPageHeader.tsx | 4 +- frontend/src/navigation/SectionPills.tsx | 21 +- frontend/src/navigation/SectionRail.tsx | 16 +- frontend/src/navigation/SectionShell.tsx | 47 + frontend/src/notifications/ActionToast.tsx | 2 +- .../notifications/TeamNotificationToast.tsx | 2 +- .../src/notifications/ToolApprovalToast.tsx | 2 +- .../src/preferences/PromptsModal.test.tsx | 53 +- frontend/src/preferences/PromptsModal.tsx | 181 +-- frontend/src/settings/Analytics.tsx | 224 ++- frontend/src/settings/ConvertToWikiModal.tsx | 15 +- frontend/src/settings/CustomModels.tsx | 173 ++- frontend/src/settings/EnableGraphRAGModal.tsx | 36 +- frontend/src/settings/General.tsx | 30 +- frontend/src/settings/Logs.tsx | 97 +- frontend/src/settings/PairDeviceModal.tsx | 32 +- .../src/settings/PersonalAccessTokens.tsx | 205 ++- frontend/src/settings/Prompts.test.tsx | 86 +- frontend/src/settings/Prompts.tsx | 335 ++--- .../src/settings/RemoteDeviceConfig.test.tsx | 19 + frontend/src/settings/RemoteDeviceConfig.tsx | 216 +-- frontend/src/settings/SourceConfigModal.tsx | 6 +- frontend/src/settings/Sources.tsx | 216 +-- frontend/src/settings/Teams.tsx | 502 +++---- .../TestRetrievalModal.notices.test.tsx | 6 +- frontend/src/settings/TestRetrievalModal.tsx | 17 +- frontend/src/settings/ToolConfig.test.tsx | 141 +- frontend/src/settings/ToolConfig.tsx | 884 +++++------ frontend/src/settings/Tools.tsx | 284 ++-- .../src/settings/accessTokenUtils.test.ts | 25 - frontend/src/settings/accessTokenUtils.ts | 23 - .../settings/components/RetrievalOptions.tsx | 53 +- .../src/settings/components/UsageQuota.tsx | 22 +- frontend/src/settings/index.tsx | 74 +- frontend/src/settings/traces/TraceChips.tsx | 13 +- .../src/settings/traces/TraceSheet.test.tsx | 74 + frontend/src/settings/traces/TraceSheet.tsx | 58 +- .../src/settings/traces/TraceSpanDetails.tsx | 60 +- .../src/settings/traces/TraceWaterfall.tsx | 45 +- frontend/src/teams/ShareToTeamModal.tsx | 188 ++- frontend/src/teams/TeamSwitcher.tsx | 16 +- frontend/src/upload/Upload.tsx | 93 +- frontend/src/upload/types/ingestor.ts | 42 +- frontend/src/upload/uploadSlice.ts | 3 +- frontend/src/utils/chartUtils.test.ts | 78 + frontend/src/utils/chartUtils.ts | 118 ++ frontend/src/utils/dateTimeUtils.test.ts | 40 + frontend/src/utils/dateTimeUtils.ts | 55 + 289 files changed, 17148 insertions(+), 8081 deletions(-) create mode 100644 frontend/src/admin/LoadError.test.tsx create mode 100644 frontend/src/admin/UsageChart.test.ts create mode 100644 frontend/src/agents/schedules/RunDetailDrawer.test.tsx create mode 100644 frontend/src/agents/workflow/WorkflowRunArtifacts.test.tsx create mode 100644 frontend/src/agents/workflow/components/PromptTextArea.test.tsx create mode 100644 frontend/src/components/ArtifactSidebar.test.tsx create mode 100644 frontend/src/components/Chunks.test.tsx delete mode 100644 frontend/src/components/DocumentPagination.tsx create mode 100644 frontend/src/components/PageToolbar.test.tsx create mode 100644 frontend/src/components/PageToolbar.tsx create mode 100644 frontend/src/components/SearchInput.test.tsx create mode 100644 frontend/src/components/SearchInput.tsx create mode 100644 frontend/src/components/StatCard.test.tsx create mode 100644 frontend/src/components/StatCard.tsx create mode 100644 frontend/src/components/tree/PathHeader.test.tsx create mode 100644 frontend/src/components/tree/PathHeader.tsx create mode 100644 frontend/src/components/tree/TreeBrowser.test.tsx create mode 100644 frontend/src/components/ui/description-list.test.tsx create mode 100644 frontend/src/components/ui/description-list.tsx create mode 100644 frontend/src/components/ui/empty-state.test.tsx create mode 100644 frontend/src/components/ui/empty-state.tsx create mode 100644 frontend/src/components/ui/icon-button.test.tsx create mode 100644 frontend/src/components/ui/icon-button.tsx create mode 100644 frontend/src/components/ui/list-row.test.tsx create mode 100644 frontend/src/components/ui/list-row.tsx create mode 100644 frontend/src/components/ui/loading-state.test.tsx create mode 100644 frontend/src/components/ui/loading-state.tsx rename frontend/src/components/{DocumentPagination.test.tsx => ui/pagination-size.test.tsx} (81%) create mode 100644 frontend/src/components/ui/pagination.test.tsx create mode 100644 frontend/src/components/ui/pagination.tsx create mode 100644 frontend/src/components/ui/section-header.test.tsx create mode 100644 frontend/src/components/ui/section-header.tsx create mode 100644 frontend/src/components/ui/separator.test.tsx create mode 100644 frontend/src/components/ui/separator.tsx create mode 100644 frontend/src/components/ui/toggle-group.test.tsx create mode 100644 frontend/src/components/ui/toggle-group.tsx create mode 100644 frontend/src/conversation/ConversationTile.test.tsx create mode 100644 frontend/src/hooks/useMediaQuery.test.tsx create mode 100644 frontend/src/lib/markdown.test.tsx create mode 100644 frontend/src/lib/markdown.tsx create mode 100644 frontend/src/navigation/SectionShell.tsx create mode 100644 frontend/src/settings/traces/TraceSheet.test.tsx create mode 100644 frontend/src/utils/chartUtils.test.ts diff --git a/AGENTS.md b/AGENTS.md index 6aea7be9..143cda22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -207,6 +207,10 @@ vale . - If shared state must be added, use Redux rather than introducing a new global state library. - Avoid broad UI refactors unless the task explicitly asks for them. - Do not re-create components if we already have some in the app. +- Follow `frontend/DESIGN.md`: compose `components/ui/` parts and pick their look with props, use theme tokens, and + keep typography, spacing, radius and motion on its roles. +- Every user-visible string, attributes included (`aria-label`, `label`, `placeholder`, `title`, `alt`), is a `t()` + key in all seven locales under `frontend/src/locale/` (`de en es jp ru zh zh-TW`). Admin pages stay English. #### Icons diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 005438b1..13f02e74 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -14,7 +14,13 @@ 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. +disable comment. Conditional classes go through `cn(...)`, never a template +literal (prettier sorts the classes inside `cn`, and `cn` merges conflicts). +Every user-visible string, attributes included (`aria-label`, IconButton +`label`, `placeholder`, `title`, `alt`, `hint`), is a `t()` key in all seven +locales (`de en es jp ru zh zh-TW`); admin pages are English by design. +Interpolated user text (a name, a timezone, a date) passes `interpolation: { +escapeValue: false }`, or `/` renders as `/`. ## Colour tokens @@ -30,7 +36,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, field borders, focus rings | `border-gray-200/300`, `dark:border-gray-600/700` | +| `border`, `input`, `ring` | dividers and field borders (fields use `border-border`), 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` | @@ -48,6 +54,12 @@ 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. +The dark `primary` (#8855f1) is set so white text on it reaches 4.55:1 on +every default Button. As text on the dark surfaces it is below 4.5:1 (3.45:1 +on background, 3.06:1 on card), so links and `text-primary` in dark pass only +as large or UI text; a fix for that needs its own link token. `ring`, +`secondary` and `sidebar-primary` keep the lighter #976af3. + 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. @@ -60,6 +72,27 @@ 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. +The status set is `success | warning | destructive | info`. `default` is the +component's own base tone (brand on a Badge or Button, quiet on an Alert or +Toast). `neutral` is the grey pill or box. + +One tint scale, by purpose: + +- Wash `/5`: a whole surface that is selected or receiving a drop (Card + `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`. +- Status soft fill: `bg-/10` in both themes (Badge, Alert, ToastHeader, + a danger-zone panel); no `dark:` twin. +- Status border: `border-/50`. +- Tinted hover on a row that is already accent: `/15` light, `/20` dark + (`ghost-on-accent`, `ghost-destructive-on-accent`, a destructive menu item). +- Neutral hover: solid `bg-accent` in both themes (ghost buttons, combobox, + SelectTrigger, Card `interactive`, Dropzone, every list-row highlight). +- Quiet panel inside a page or card: `bg-muted`, not `bg-muted/40` or `/60`. +- Dividers: `border-border`, not `border-border/60`. + Patterns: - Status pill: ``. Status box: ``. @@ -76,7 +109,17 @@ Patterns: 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'}`. + `variant={active ? 'secondary' : 'ghost-muted'}`. That is for one thing + switched on or off. Picking one value of several (a 7d / 30d / 90d range, + a schedule's frequency, a filter row) is a `ToggleGroup`, whose on item has + the `outline` look with no hue. +- A brand chip, a small action that opens something an answer produced (an + artifact chip under an answer, a citation pill in its text), is also + `variant="secondary" shape="pill"`: the default size with a lucide icon + first for artifacts, `size="xs"` for citations. It is the same tint as a + pressed toggle; the difference is that a chip never toggles and always + opens or scrolls to something. The citation pill keeps `h-5 min-w-5` + (20px) so it sits in a line of text. - 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. @@ -84,6 +127,10 @@ Patterns: in `currentColor`. - `white`, `black`, `transparent`, `current` and `inherit` are allowed; use them only for overlays and imagery, not for text or surfaces. +- Swapping a state's colour for its same-hue token (amber to `warning`, red + to `destructive`) is fine without review; changing the hue of a state the + user sees (an error, a recording state, a selected tab) is a design + decision. Non-state UI follows the token even when its hue shifts. ## Components and their variants @@ -109,13 +156,39 @@ Patterns: `foreground/15` (`destructive/15`) tint, which shows on accent where ghost's accent square would not. - Icons are `lucide-react` at the default stroke (2). Don't pass - `strokeWidth`, and don't use an `` for a glyph lucide has. + `strokeWidth`, and don't use an `` 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 ``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 + (`TriangleAlert`, `CircleAlert`, `CircleCheck`, `CircleX`, `Trash2`, + `CodeXml`, `Search`), no `as` aliases. Inside a Button the size comes from + the Button: 16px, 14px at `xs`, 20px at `size="icon"`; write a `size-N` + only to differ from it, since any `size-` class switches the default off. - 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. +- A link inside running text (an artifact link in an answer, a link in + markdown, a hint's "Learn more") is `variant="link" size="inline"`, with + `asChild` around the ``: no height or padding, underlined on hover, the + shared focus ring. Standalone links ("Learn more" with `ExternalLink`, "Go to + Tools" with `ArrowRight`) are the same, the icon a child. Toggles and crumbs + keep a normal size. The base is `text-sm font-medium`, so a link in a 12px + 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 `` as a + link. +- Icon-only buttons are `IconButton` (below), never a `Button` with a + `title`. +- Roles for dangerous and dismissive actions: a delete on a page (a "Danger + zone" card's Delete agent or Revoke, Delete all) is `destructive-outline`; + the submit of a confirm dialog is `destructive` (`ModalActions destructive`, + `ConfirmationModal variant="destructive"`); Cancel is `ghost` at the size and + shape of the button beside it, in a modal footer, a form header or an inline + 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` with no margin; the size's `gap-1.5` spaces them, and the label span keeps @@ -125,16 +198,24 @@ Patterns: 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. - `Input` and `SelectTrigger` take `size="field"` too (the same 42px as - Input `default` and SelectTrigger `lg`), so a form column has one name for - one height; prefer `field` in new form rows. With `shape="pill"` its text starts 21px in, like - the Input and Select pills beside it. The agent form's pickers are +- A button or picker that sits in a row of fields is `size="field"`: 38px. + `Input` and `SelectTrigger` take `size="field"` too (the same 38px as + Input `default`), so a form column has one name for one height. Page + actions beside a page's search field (Add Source, Add Tool, Test + 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`. - 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. + with `asChild` around a `` for navigation rows, the label in a + `truncate` span. A row that also holds buttons (an agent's pin, a + conversation's menu, rename's Save / Cancel) puts the link and the buttons + side by side in a `group relative` wrapper, never buttons inside the link; + the link keeps its fill while the pointer is on a sibling with + `group-hover:bg-sidebar-accent`. The section sidebar's "Back to app" row is + the same variant. - 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 @@ -165,7 +246,10 @@ justify-start` and `aria-expanded`: a lucide `ChevronRight` first - Drop `text-white` on default and destructive buttons; the foreground token already provides it. Drop `disabled:cursor-not-allowed` on buttons; the base disables pointer events. Fields are the other way round (see Focus, - elevation and stacking: "Disabled"). + elevation and stacking: "Disabled"). A disabled primary button just fades + (`disabled:opacity-50`); never hand-roll a grey disabled state. +- A text button that removes something (Remove on a row) is + `ghost-destructive size="xs"`: grey at rest, red on hover. - A button that is busy (saving, creating, testing) takes `loading`: it disables itself, sets `aria-busy`, and draws a 16px spinner over the label, which stays in the layout (invisible) so the width doesn't jump. Keep the @@ -173,25 +257,88 @@ justify-start` and `aria-expanded`: a lucide `ChevronRight` first "Saving…", or pin a fixed width to stop the jump. The one exception is a button whose busy state says something the user needs: a progress figure (a connector's Sync shows "42%") or a mode (the composer's Voice button shows - "Transcribing"). It keeps its busy label with a spinning mark where its - icon was: Sync uses `Spinner size="sm"` at icon size (`size-4`); Voice still - spins lucide's `LoaderCircle`. + "Transcribing"). It keeps its busy label with a `Spinner size="xs"` (the + 16px icon step, with a `label` so it doesn't announce "Loading") where its + icon was. ### 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) | +| `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, chart panels and stat tiles) | | `interactive` | whole card is the target: hover, focus ring and `selected` highlight; pair with `asChild` around a `
`
+in a Card picked by the surface underneath: `filled padding="sm"` (a muted
+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
+`
` 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`.
+
+Tile text: the name is `CardTitle` (14px semibold from Card's `text-sm`; pass
+`as="h2"` on a page that goes from its title straight to a tile grid, but
+never a heading inside a clickable tile, whose children a button flattens), the
+description `CardDescription size="xs"` (12px muted, `leading-relaxed`), and
+meta lines (a date, a token count, a model id or host) go in `CardFooter` at
+regular weight. `font-medium` is for list rows (`ListRow`), not tiles.
+
+### SectionHeader (`ui/section-header.tsx`)
+
+``. `size`: `default`
+(18px `text-lg font-semibold`, a section title on a page or panel), `sm` (the
+eyebrow, `text-muted-foreground text-xs font-semibold uppercase
+tracking-wider`, a label set in caps above a group or a table head), `xs`
+(14px `text-sm font-semibold`, a sub-heading inside a panel, drawer or modal).
+`tone="destructive"` for a danger zone only. `as` is the level in the outline
+(`h2` default). The spacing below belongs to the parent's `gap-*`, never an
+`mb-*` on the heading. Not for dialog and sheet titles (their own title) or a
+page byline (a muted `text-sm` paragraph).
+
+A title with controls beside it (Add, a filter Select, Import spec) passes
+them as `actions`, never a hand-rolled `flex justify-between` row around the
+header: the row centres the title on the buttons and wraps them under it on
+a phone (`flex-wrap items-center gap-x-4 gap-y-2`). Panel and chart titles
+are `size="xs"` with `as="h3"`; a title row that also holds a legend or a
+"Resets" line keeps its wrapper and swaps only the heading. Disclosure
+headers (`Button variant="section-toggle"`) and the Agents breadcrumb are not
+SectionHeaders.
 
 ### OptionCard (`ui/option-card.tsx`)
 
@@ -209,30 +356,36 @@ 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--100 px-2 py-0.5 text-xs text--700 dark:...`
-pill, including schedule and run status pills. HTTP method pills take their variant from
+pill, including schedule and run status pills, 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
+stat chips `tabular-nums`, as approved exceptions. HTTP method pills take their variant from
 `getMethodBadgeVariant` (`utils/httpMethodColors.ts`): GET `success`, POST
 `info`, PUT `warning`, DELETE `destructive`, PATCH `default`, anything else
 `neutral`. `MultiSelect` (`ui/multi-select.tsx`) shows its first two picks as
 `default` Badges with a remove X, then "+N more", on a `Button
-variant="combobox" size="field"` trigger that grows past 42px when the chips
-wrap.
+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"`.
 
 ### Input (`ui/input.tsx`)
 
-| Prop      | Values                                                                                                |
-| --------- | ----------------------------------------------------------------------------------------------------- |
-| `size`    | `sm` (h-8), `default` (h-10.5, the historical 42px), `lg` (h-12), `field` (h-10.5, the form-row name) |
-| `shape`   | `default`, `pill`                                                                                     |
-| `variant` | `default`, `bare` (no border, padding, radius, shadow or ring), `filled` (card fill)                  |
+| Prop      | Values                                                                               |
+| --------- | ------------------------------------------------------------------------------------ |
+| `size`    | `sm` (h-8), `default` (h-9.5, 38px), `lg` (h-12), `field` (h-9.5, the form-row name) |
+| `shape`   | `default`, `pill`                                                                    |
+| `variant` | `default`, `bare` (no border, padding, radius, shadow or ring), `filled` (card fill) |
 
 Text alignment classes (`text-right`) and `font-mono` (code fields) are
-allowed on Input and Textarea. A floating
-`label` sits on the field's border, so its background must match the surface
-behind the field: `labelSurface` is `card` (default, forms in cards and
-modals), `background` (fields straight on a page) or `muted` (fields on a
-muted panel). Never pass a background class for it. The chat and hero
+allowed on Input and Textarea. `` 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
 fields (`rounded-3xl px-5 py-3`) are `size="lg" shape="pill"`. A
-default-size pill (42px, the form-row height) pads `px-5` like the large
+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
 filters (`h-auto px-2 py-1 text-sm`) are `size="sm"`. An inset icon (a
 search glass) is `leftIcon`, with or without a `label`; it pads the field
@@ -245,11 +398,16 @@ instead of showing the panel through; never pass `bg-card` for it.
 
 ### SelectTrigger (`ui/select.tsx`)
 
-`size`: `sm` (32px), `default` (36px), `lg` (42px), `field` (42px, the
-form-row name); `variant`: `default`, `ghost`; `shape`: `default`, `pill`.
-Pills pad `px-5` at every size but `sm` (`px-3`), so their text starts 21px
-in like the Input and Button field pills. The large rounded selects
-(`rounded-3xl px-5 py-3`) are `size="lg" shape="pill"`.
+`size`: `sm` (32px), `default` (36px), `field` (38px, the form-row
+height); `variant`: `default`, `ghost`; `shape`: `default`, `pill`. Pills
+pad `px-5` at every size but `sm` (`px-3`), so their text starts 21px in
+like the Input and Button field pills. A select in a form is
+`size="field"`, labelled by `FormField`. SelectTrigger is `w-fit`; pass
+`w-full` in a form column. Fields that take typing or sit beside one are 16px
+below `md` (iOS zooms on focus under 16px) and 14px from `md`: Input,
+Textarea, SelectTrigger `default | field`, Button `combobox` `default | field |
+lg` and CommandInput. The `sm` sizes stay 14px. A highlighted list row is
+`bg-accent` in Select, Command and DropdownMenu alike.
 
 ### Textarea (`ui/textarea.tsx`)
 
@@ -263,23 +421,39 @@ variable highlighter and the chunk editor behind its line-number gutter.
 
 ### FormField (`ui/form-field.tsx`)
 
-A labelled field is `` with
-the field as its only child. It stacks `Label`, the field, a muted `text-xs`
-hint and a red `text-xs` error (`role="alert"`) 6px apart (`gap-1.5`), puts
-the required star 4px after the label (`aria-hidden`; the field gets
-`aria-required`, not native `required`), and dims the label while
-`disabled`. `Input`, `Textarea`, `SelectTrigger` (even nested in `Select`),
-`MultiSelect` and `Checkbox` read their `id`, `aria-invalid`, `aria-describedby`,
+Every boxed form control is labelled by a floating label:
+`` 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
+`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
+the field come a muted `text-xs` hint and a red `text-xs` error
+(`role="alert"`), 6px apart. The required star follows the label
+(`aria-hidden`; the field gets `aria-required`, not native `required`). A
+placeholder is an example: hidden while the label rests, shown on focus.
+Stack floating fields with `gap-5` so each label clears the field above.
+
+`Input`, `Textarea`, `SelectTrigger` (even nested in `Select`), `MultiSelect`,
+`Dropzone` and `Checkbox` read their `id`, `aria-invalid`, `aria-describedby`,
 `aria-required` and `disabled` from it, so don't set those by hand; an id
 the field already has wins. Any other control: pass `id` to FormField and
-the same id to the control. One FormField holds one control: a second
-field inside it (an "Add ref" box under a picker) needs its own `id`, or it
-takes the field's. Popovers reset the wiring (`FormFieldBoundary`), so a
-picker's search box is safe. There is one size: labels are `text-sm`
-everywhere, also in dense panels (Guardrails). `className` is layout only.
-Floating-label Inputs (``) label themselves and don't go in a
-FormField. Never hand-build a `