diff --git a/DESIGN.md b/DESIGN.md index e7d17a97..f554a144 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -136,16 +136,22 @@ Preserve established icon and data colors during surface or theme refactors. Use ### Fonts and hierarchy -The global body size is currently 14px, and `src/assets/index.css` does not define a custom brand font. Keep system sans-serif fonts with platform Chinese fallbacks. Do not introduce decorative fonts or remote font dependencies for the client. Keep Monaco's monospace editor font; SQL, types, object identifiers, and code snippets may use the existing `font-mono` utility. +The global body size is `var(--font-size-form)` (14px), and `src/assets/index.css` does not define a custom brand font. Keep system sans-serif fonts with platform Chinese fallbacks. Do not introduce decorative fonts or remote font dependencies for the client. Keep Monaco's monospace editor font; SQL, types, object identifiers, and code snippets may use the existing `font-mono` utility. + +The type scale is token-driven. `index.css` defines `--font-size-caption / dense / body / form / title / page-title` (11 / 12 / 13 / 14 / 16 / 20px); `uno.config.ts` maps the two sizes missing from the default scale (`text-caption` = 11px, `text-body` = 13px), and the rest reuse default utilities (`text-xs` = 12, `text-sm` = 14, `text-base` = 16, `text-xl` = 20). Business components must use these utilities or the CSS variables — never a bare px/rem font size. Monaco's editor font size remains a user setting. + +Custom font-size utilities must also be registered in the shared `cn()` helper so component overrides merge sizes independently of semantic text colors. The following hierarchy constrains new UI; it does not require rewriting all existing page titles. -| Role | Size / weight | Usage | +| Role | Token / utility | Usage | | --- | --- | --- | -| Page title | 20–24px / 600 | Standalone pages such as Connections and Settings; do not add large headings to editor workspaces | -| Dialog / section title | 14–18px / 500–600 | Dialog titles and major groups | -| Body / form | 14px / 400 | Descriptions, inputs, buttons; matches the current `text-sm` | -| Dense data / supporting labels | 12px / 400–500 | Result grids, object trees, toolbars, status bars | +| Page title | `text-xl` (20px / 600) | Standalone pages such as Connections and Settings; do not add large headings to editor workspaces | +| Dialog / panel / card title | `text-base` (16px / 600) | Dialog titles, side-panel headers, card and section titles | +| Body / form | `text-sm` (14px / 400) | Descriptions, inputs, buttons | +| Sidebar lists, trees, and menus | `text-body` (13px / 400–500) | Connection selector rows, object trees, saved-query names, query tabs, dropdown items | +| Dense data / supporting labels | `text-xs` (12px / 400–500) | Result grids, toolbars, status bars, empty-state hints | +| Captions and badges | `text-caption` (11px / 500) | Attribute badges, timestamps, task counters; the readable lower bound — 9–10px text is forbidden | - Use roughly 1.4–1.6 line height for ordinary text. Code follows editor settings; fixed-height table rows must not clip text. - Do not use 9–10px text for states, prices, field meanings, or instructions users must read. Adjust the layout first when space is tight. @@ -163,11 +169,12 @@ Retain the existing app shell: top window bar → left feature navigation → cu | --- | --- | --- | | Initial window | 1600 × 1000 | `src-tauri/tauri.conf.json`; not a minimum window size | | Window bar | 40px | `h-10` in `AppHeader.vue` | -| Feature navigation | 64px wide, 40 × 40px entries | `AppSidebar.vue` | +| Feature navigation | 56px wide, 36 × 36px entries, 20px icons | `AppSidebar.vue` | | Query sidebar | Initially 250px, resize range 200–400px | `QueriesPage.vue` | -| Query tabs | 35px high, maximum 200px per tab | `QueryTabs.vue` | -| Result toolbar | 36px high | `QueryResultPanel.vue` | -| Standard buttons / inputs | 32px high | `ui/button`, `ui/input` | +| Object tree rows | 28px row height (20px line + 8px padding), 13px text, 14px icons | `SchemaTree.vue` | +| Query tabs | 35px high, 13px labels, maximum 200px per tab | `QueryTabs.vue` | +| Result toolbar | 36px high (result panel and grid toolbars share this) | `QueryResultPanel.vue`, `DataGrid.vue` | +| Standard buttons / inputs | 32px high; 24 / 28 / 36px via `xs` / `sm` / `lg` | `ui/button`, `ui/input` | | Virtual result rows | 32px estimated height | `grid/DataGrid.vue`; not a shared height for every table | ### Spacing and containers @@ -204,7 +211,9 @@ Keep `--radius: 0.5rem`: `lg` is 8px, `md` is 6px, and `sm` is 4px. Use `rounded - Use corner radius to convey component hierarchy rather than inventing per-page values. Keep continuous straight edges for data grids, split panes, and editors. - Limit `rounded-full` to suitable elements such as avatars, small status dots, and existing badges. Do not make every button pill-shaped. -- Toolbar icons are usually 14–16px and main navigation icons 20px; the visible icon is smaller than its hit target. Keep standard action targets at 32px. Compact grid actions should generally be at least 24px with sufficient spacing and keyboard access. +- Icon scale: 12px for inline glyphs (chevrons, close buttons, badge icons), 14px for tree, tab, and compact toolbar icons, 16px for standard buttons, menu items, and input adornments, 20px for main navigation. Dropdown-menu icons inherit the shared `size-4` from `DropdownMenuItem`; do not override per menu. The visible icon is smaller than its hit target; keep standard action targets at 32px and compact grid actions at least 24px with sufficient spacing and keyboard access. +- Database vendor logos use three sizes: 14px inside lists and tabs, 16px in dropdown selected states and forms, 20px on cards and landing states (`DbTypeIcon` defaults to 16). +- Status dots use 8px (`h-2 w-2`) inline within rows and 10px (`h-2.5 w-2.5`) as a standalone indicator beside a control. - Reuse the component's existing Carbon / Lucide style. Do not add icon libraries for color or decoration, or mix emoji into an action group. ## Components @@ -217,7 +226,7 @@ Interactive components must cover default, hover, focus-visible, active / select ### Buttons and toolbars -- `default`: the single primary action, retaining `bg-primary text-primary-foreground`. Current default height is 32px; large is 36px. +- `default`: the single primary action, retaining `bg-primary text-primary-foreground`. Heights: `xs` 24px, `sm` 28px, default 32px, `lg` 36px; icon buttons mirror this via `icon-xs` / `icon-sm` / `icon`. Use `sm` for sidebar and toolbar rows instead of overriding heights per component. - `outline / secondary`: secondary actions; `ghost`: toolbar and infrequent actions; `link`: explicit text links. - `destructive`: genuinely destructive actions such as deletion. Ordinary disconnect and stop-query actions do not automatically become solid red buttons because they stop something. - Loading states prevent duplicate submission and keep readable labels. Provide cancellation for cancellable long tasks rather than disabling the entire panel. diff --git a/src/assets/index.css b/src/assets/index.css index d6d1ebbc..782fee20 100644 --- a/src/assets/index.css +++ b/src/assets/index.css @@ -92,6 +92,20 @@ --syntax-comment: 112.50 7.55% 58.43%; } +:root { + /* Type scale (see DESIGN.md Typography). text-caption / text-body map to + --font-size-caption / --font-size-body via uno.config.ts; the other sizes + reuse the default utilities text-xs (12) / text-sm (14) / text-base (16) / + text-xl (20). Business components must use these variables or utilities + instead of bare px/rem font sizes. */ + --font-size-caption: 11px; + --font-size-dense: 12px; + --font-size-body: 13px; + --font-size-form: 14px; + --font-size-title: 16px; + --font-size-page-title: 20px; +} + * { border-color: hsl(var(--border)); } @@ -107,7 +121,7 @@ body, body { background-color: hsl(var(--background)); color: hsl(var(--foreground)); - font-size: 14px; + font-size: var(--font-size-form); } [data-tauri-drag-region] { diff --git a/src/components/DeviceReplaceDialog.vue b/src/components/DeviceReplaceDialog.vue index f8c5ba76..aef8cf6e 100644 --- a/src/components/DeviceReplaceDialog.vue +++ b/src/components/DeviceReplaceDialog.vue @@ -152,7 +152,7 @@ function handleOpenChange(open: boolean) { } .slot-usage { - font-size: 13px; + font-size: var(--font-size-body); color: hsl(var(--muted-foreground)); margin-bottom: 12px; } @@ -204,26 +204,26 @@ function handleOpenChange(open: boolean) { .device-name { font-weight: 500; - font-size: 14px; + font-size: var(--font-size-form); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .current-tag { - font-size: 12px; + font-size: var(--font-size-dense); color: hsl(var(--muted-foreground)); font-weight: 400; } .device-info { - font-size: 12px; + font-size: var(--font-size-dense); color: hsl(var(--muted-foreground)); margin-top: 2px; } .replace-error { - font-size: 13px; + font-size: var(--font-size-body); color: hsl(var(--destructive-text)); margin-top: 8px; } diff --git a/src/components/SQLEditor.vue b/src/components/SQLEditor.vue index c6f8debb..f0ad617a 100644 --- a/src/components/SQLEditor.vue +++ b/src/components/SQLEditor.vue @@ -313,11 +313,11 @@ defineExpose({ getValue, setValue, handleFormat }) gap: 16px; padding: 6px 12px; cursor: pointer; - font-size: 13px; + font-size: var(--font-size-body); } .sql-context-menu li .shortcut { - font-size: 11px; + font-size: var(--font-size-dense); opacity: 0.5; white-space: nowrap; } diff --git a/src/components/UpdateNotification.vue b/src/components/UpdateNotification.vue index 68cc17c0..e84e7fce 100644 --- a/src/components/UpdateNotification.vue +++ b/src/components/UpdateNotification.vue @@ -78,7 +78,7 @@ const buttonsDisabled = computed(() =>
-
+
{{ t('updater.updateAvailable') }}
diff --git a/src/components/agent-message-bubble.vue b/src/components/agent-message-bubble.vue index f85ef321..28f05a11 100644 --- a/src/components/agent-message-bubble.vue +++ b/src/components/agent-message-bubble.vue @@ -534,7 +534,7 @@ function toolVerb(toolName: string, tc: AgentToolCall): string { max-width: 85%; padding: 10px 14px; border-radius: 12px; - font-size: 14px; + font-size: var(--font-size-form); line-height: 1.5; } @@ -595,7 +595,7 @@ function toolVerb(toolName: string, tc: AgentToolCall): string { } .iteration-label { - font-size: 10px; + font-size: var(--font-size-caption); font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; @@ -631,7 +631,7 @@ function toolVerb(toolName: string, tc: AgentToolCall): string { } .activity-label { - font-size: 13px; + font-size: var(--font-size-body); color: hsl(var(--muted-foreground)); white-space: nowrap; overflow: hidden; @@ -656,7 +656,7 @@ function toolVerb(toolName: string, tc: AgentToolCall): string { } .duration-badge { - font-size: 10.5px; + font-size: var(--font-size-caption); font-weight: 500; color: hsl(var(--muted-foreground)); background: hsl(var(--muted)); @@ -673,7 +673,7 @@ function toolVerb(toolName: string, tc: AgentToolCall): string { } .tool-name-badge { - font-size: 11.5px; + font-size: var(--font-size-dense); font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, monospace; color: hsl(var(--foreground) / 0.75); background: hsl(var(--muted)); @@ -688,7 +688,7 @@ function toolVerb(toolName: string, tc: AgentToolCall): string { } .tool-verb-label { - font-size: 12px; + font-size: var(--font-size-dense); color: hsl(var(--muted-foreground)); } @@ -719,7 +719,7 @@ function toolVerb(toolName: string, tc: AgentToolCall): string { } .result-preview { - font-size: 11.5px; + font-size: var(--font-size-dense); color: hsl(var(--muted-foreground)); white-space: nowrap; overflow: hidden; @@ -748,7 +748,7 @@ function toolVerb(toolName: string, tc: AgentToolCall): string { .tool-result-pre { margin: 0; font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, monospace; - font-size: 11px; + font-size: var(--font-size-dense); line-height: 1.5; white-space: pre-wrap; word-break: break-all; @@ -777,7 +777,7 @@ function toolVerb(toolName: string, tc: AgentToolCall): string { border-radius: 6px; background: hsl(var(--destructive-muted)); color: hsl(var(--destructive-text)); - font-size: 13px; + font-size: var(--font-size-body); overflow-wrap: anywhere; } @@ -802,7 +802,7 @@ details[open] .activity-chevron { .activity-body { margin: 0 0 4px 42px; padding: 6px 10px; - font-size: 11.5px; + font-size: var(--font-size-dense); line-height: 1.6; color: hsl(var(--muted-foreground)); background: hsl(var(--muted) / 0.5); @@ -859,7 +859,7 @@ details[open] .activity-chevron { .activity-inline-content { padding: 4px 0 4px 20px; - font-size: 13.5px; + font-size: var(--font-size-body); line-height: 1.55; color: hsl(var(--foreground)); } @@ -881,7 +881,7 @@ details[open] .activity-chevron { } .markdown-body :deep(pre code) { - font-size: 12px; + font-size: var(--font-size-dense); background: none; padding: 0; border: none; @@ -892,7 +892,7 @@ details[open] .activity-chevron { border-collapse: collapse; width: 100%; margin: 8px 0; - font-size: 12px; + font-size: var(--font-size-dense); } .markdown-body :deep(th), @@ -926,7 +926,7 @@ details[open] .activity-chevron { } .markdown-body :deep(h1) { - font-size: 22px; + font-size: var(--font-size-page-title); margin-top: 24px; margin-bottom: 12px; border-bottom: 1px solid hsl(var(--border)); @@ -934,7 +934,7 @@ details[open] .activity-chevron { } .markdown-body :deep(h2) { - font-size: 18px; + font-size: var(--font-size-title); margin-top: 20px; margin-bottom: 10px; border-bottom: 1px solid hsl(var(--border)); @@ -942,22 +942,22 @@ details[open] .activity-chevron { } .markdown-body :deep(h3) { - font-size: 16px; + font-size: var(--font-size-form); margin-top: 18px; } .markdown-body :deep(h4) { - font-size: 15px; + font-size: var(--font-size-body); margin-top: 16px; } .markdown-body :deep(h5) { - font-size: 14px; + font-size: var(--font-size-dense); margin-top: 14px; } .markdown-body :deep(h6) { - font-size: 13px; + font-size: var(--font-size-caption); margin-top: 14px; color: hsl(var(--muted-foreground)); } @@ -1054,7 +1054,7 @@ details[open] .activity-chevron { border: 1px solid hsl(var(--border) / 0.5); border-radius: 8px; max-width: fit-content; - font-size: 11.5px; + font-size: var(--font-size-dense); color: hsl(var(--muted-foreground)); } @@ -1075,7 +1075,7 @@ details[open] .activity-chevron { align-items: center; gap: 4px; font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, monospace; - font-size: 11px; + font-size: var(--font-size-dense); color: hsl(var(--foreground) / 0.75); padding: 1px 8px; background: hsl(var(--background)); @@ -1104,7 +1104,7 @@ details[open] .activity-chevron { background: none; border: none; cursor: pointer; - font-size: 11px; + font-size: var(--font-size-dense); color: hsl(var(--muted-foreground)); padding: 0; margin-left: 4px; @@ -1125,7 +1125,7 @@ details[open] .activity-chevron { padding: 8px 12px; background: hsl(var(--muted) / 0.4); border-radius: 8px; - font-size: 12px; + font-size: var(--font-size-dense); line-height: 1.55; color: hsl(var(--foreground) / 0.85); max-height: 240px; diff --git a/src/components/chat-panel.vue b/src/components/chat-panel.vue index 1dea1adb..5cbdcba6 100644 --- a/src/components/chat-panel.vue +++ b/src/components/chat-panel.vue @@ -315,7 +315,7 @@ onBeforeUnmount(() => {
{{ stopMessage }} - {{ t('dataStudio.agent.continueHint') }} + {{ t('dataStudio.agent.continueHint') }}
@@ -455,7 +455,7 @@ onBeforeUnmount(() => { outline: none; background: transparent; color: hsl(var(--foreground)); - font-size: 13px; + font-size: var(--font-size-body); resize: none; line-height: 1.5; } @@ -550,13 +550,13 @@ onBeforeUnmount(() => { } .loop-stopped-banner__message { - font-size: 13px; + font-size: var(--font-size-body); font-weight: 500; line-height: 1.4; } .loop-stopped-banner__hint { - font-size: 12px; + font-size: var(--font-size-dense); color: hsl(var(--muted-foreground)); line-height: 1.4; } @@ -570,7 +570,7 @@ onBeforeUnmount(() => { .loop-stopped-banner__action { padding: 4px 10px; - font-size: 12px; + font-size: var(--font-size-dense); font-weight: 600; border-radius: 6px; cursor: pointer; diff --git a/src/components/connections/ServerCard.vue b/src/components/connections/ServerCard.vue index f2adf7bb..6a9984fc 100644 --- a/src/components/connections/ServerCard.vue +++ b/src/components/connections/ServerCard.vue @@ -109,7 +109,7 @@ const connectTooltip = computed(() =>
@@ -123,7 +123,7 @@ const connectTooltip = computed(() =>
-

+

{{ connection.name }}

-

+

{{ connectionUrl }}

diff --git a/src/components/connections/SshTunnelConfigPanel.vue b/src/components/connections/SshTunnelConfigPanel.vue index cb9cc807..54d7a0e9 100644 --- a/src/components/connections/SshTunnelConfigPanel.vue +++ b/src/components/connections/SshTunnelConfigPanel.vue @@ -378,12 +378,12 @@ onMounted(detectProxy) v-if="detectedProxy && useSystemProxy" class="px-2.5 py-1.5 border border-primary/40 rounded-md bg-primary/5 flex gap-2 items-center" > - 0 + 0
{{ t('components.serverForm.ssh.systemProxy') }}
-
+
{{ detectedProxy }}
@@ -405,12 +405,12 @@ onMounted(detectProxy) v-else-if="!detectedProxy && useSystemProxy" class="px-2.5 py-1.5 border border-amber-500/50 rounded-md bg-amber-50 flex gap-2 items-center dark:bg-amber-900/20" > - 0 + 0
{{ t('components.serverForm.ssh.systemProxy') }}
-
+
{{ t('components.serverForm.ssh.systemProxyNotDetected') }}
@@ -453,12 +453,12 @@ onMounted(detectProxy) class="px-2.5 py-1.5 border rounded-md bg-background flex gap-2 items-center" > - {{ index + 1 }} + {{ index + 1 }}
{{ profile.name }}
-
+
{{ profile.username }}@{{ profile.host }}:{{ profile.port }}
@@ -489,7 +489,7 @@ onMounted(detectProxy)
- 1 + 1
{{ props.modelValue?.host }} @@ -520,10 +520,10 @@ onMounted(detectProxy)
-

+

{{ chainSegments.join(' → ') }}

-

+

{{ t('components.serverForm.ssh.hopOrderHint') }}

diff --git a/src/components/context-indicator.vue b/src/components/context-indicator.vue index 6862d19d..04e1337a 100644 --- a/src/components/context-indicator.vue +++ b/src/components/context-indicator.vue @@ -266,7 +266,7 @@ defineExpose({ refresh }) border-radius: 999px; background: hsl(var(--background)); color: hsl(var(--muted-foreground)); - font-size: 11px; + font-size: var(--font-size-dense); font-variant-numeric: tabular-nums; line-height: 1; cursor: pointer; @@ -297,7 +297,7 @@ defineExpose({ refresh }) border-radius: 4px; background: hsl(38 92% 48% / 0.18); color: hsl(38 92% 38%); - font-size: 9px; + font-size: var(--font-size-caption); font-weight: 700; letter-spacing: 0.06em; line-height: 1.4; @@ -313,7 +313,7 @@ defineExpose({ refresh }) border-radius: 6px; background: hsl(38 92% 48% / 0.08); color: hsl(38 92% 30%); - font-size: 11px; + font-size: var(--font-size-dense); line-height: 1.4; } @@ -368,7 +368,7 @@ defineExpose({ refresh }) background: hsl(var(--popover)); color: hsl(var(--popover-foreground)); box-shadow: 0 8px 24px hsl(0 0% 0% / 0.12); - font-size: 12px; + font-size: var(--font-size-dense); } .context-indicator__popover-header { @@ -384,7 +384,7 @@ defineExpose({ refresh }) } .context-indicator__popover-model { - font-size: 10px; + font-size: var(--font-size-caption); color: hsl(var(--muted-foreground)); max-width: 140px; overflow: hidden; @@ -406,7 +406,7 @@ defineExpose({ refresh }) } .context-indicator__grid dt { - font-size: 10px; + font-size: var(--font-size-caption); color: hsl(var(--muted-foreground)); text-transform: uppercase; letter-spacing: 0.04em; @@ -432,7 +432,7 @@ defineExpose({ refresh }) border-radius: 6px; background: hsl(var(--background)); color: hsl(var(--foreground)); - font-size: 11px; + font-size: var(--font-size-dense); cursor: pointer; transition: background-color 120ms ease, diff --git a/src/components/database-browser/DataTableView.vue b/src/components/database-browser/DataTableView.vue index e48c5315..a4a4fbe7 100644 --- a/src/components/database-browser/DataTableView.vue +++ b/src/components/database-browser/DataTableView.vue @@ -1363,7 +1363,7 @@ watch(