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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ Tauri-based cross-platform SQL database GUI client with Vue 3 + TypeScript front

**Before adding or changing client UI, read [DESIGN.md](DESIGN.md).** It is the canonical client design specification; feature design documents describe behavior within that system.

- Preserve SQLKit's existing **blue primary**, light/dark/system themes, compact desktop workspace, and shared shadcn-vue/Radix Vue components. Learn restraint and semantic color ownership from console-serverlessinsight; do not copy its violet brand or monochrome primary buttons.
- Use neutral surfaces, typography, spacing, and borders for hierarchy. Keep accent areas scarce (roughly 10% of application chrome); use at most one solid primary action per operation context. Secondary toolbar actions stay neutral.
- Colors have fixed roles: `primary` = primary actions/interaction; `success` = actual success/connected/completed; `destructive` = errors/destructive actions. Ordinary information, counts, boolean values, database properties, and file formats are neutral. Do not assign a color to each feature or add a separate AI palette.
- Reuse semantic CSS tokens from `src/assets/index.css` and their `uno.config.ts` mappings. Do not add palette classes or literal colors for existing UI semantics. `accent` is a neutral interaction surface, not a second brand color.
- `warning` and `info` are **not currently registered** semantic colors. Any new semantic token must include light/dark definitions, necessary foreground pairs, UnoCSS mappings, shared variants where needed, contrast verification, and a DESIGN.md update in the same change.
- Preserve SQLKit's existing **blue primary**, light/dark/system themes, compact desktop workspace, and shared shadcn-vue/Radix Vue components. Keep semantic color ownership consistent and retain SQLKit's blue brand and primary actions.
- Use neutral surfaces, typography, spacing, and borders for hierarchy. Keep accent areas scarce (roughly 10% of application chrome); use at most one solid primary action per operation context. Keep Execute compact and unfilled. Preserve the original functional icon and data colors documented in DESIGN.md.
- Colors have fixed roles: `primary` = primary actions/interaction; `success` = actual success/connected/completed; `destructive` = errors/destructive actions. Preserve existing boolean, key/constraint, type, filter, permission, format, and icon recognition colors, with labels or distinct glyphs alongside them. Do not tint whole features or add a separate AI palette. Recognition colors describe content and actions independently of operational status; do not flatten them to gray during surface refactors.
- Reuse semantic CSS tokens from `src/assets/index.css` and their `uno.config.ts` mappings. Preserve existing palette utilities for functional recognition; a token migration must retain the original hues and theme variants. `accent` is a neutral interaction surface, not a second brand color.
- `warning` is registered with light/dark foreground, muted-background, and border pairs; `info` is **not registered**. Use `primary-text` and `destructive-text` for readable colored text, reserving the corresponding base tokens for solid actions. Any new semantic token must include light/dark definitions, necessary foreground pairs, UnoCSS mappings, shared variants where needed, contrast verification, and a DESIGN.md update in the same change.
- Non-interactive badges explicitly use `outline`/`secondary` by default; status colors require real state and accompanying text or icons. Syntax highlighting, data charts, diffs, and small vendor logos have the bounded exceptions documented in DESIGN.md.
- Provide visible keyboard focus, accessible icon-button names, loading/empty/error states, readable contrast, and localized copy. Reuse runtime platform detection for shortcuts and native window behavior.
- Existing hardcoded colors and component defaults are migration debt, not permission to repeat them. Distinguish documented targets from implemented behavior, and verify the affected themes, window sizes, and interactions when changing UI.
- Existing functional recognition colors are intentional compatibility constraints. Avoid new unrelated hardcoded colors. Distinguish documented targets from implemented behavior, and verify the affected themes, window sizes, and interactions when changing UI.

## Build/Lint/Test Commands

Expand Down
149 changes: 79 additions & 70 deletions DESIGN.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default antfu({
'**/dist/**',
'**/node_modules/**',
'**/.tauri/**',
'.impeccable/hook.cache.json',
'AGENTS.md',
'docs/**',
'src-tauri/gen/**',
Expand Down
6 changes: 0 additions & 6 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,3 @@ pub use query::*;
pub use server::*;
pub use store::*;
pub use transfer::*;

/// Returns the current app version from Cargo.toml.
#[tauri::command]
pub fn get_app_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
1 change: 0 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,6 @@ pub fn run() {
commands::auto_map_migration_columns,
commands::generate_ddl_for_objects,
commands::execute_sql_content,
commands::get_app_version,
crate::consume_pending_auth,
crate::entitlement::refresh_entitlement,
crate::entitlement::get_entitlement,
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/dataGridPaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ describe('paged result grid', () => {
const firstRow = wrapper.get('[data-index="0"]')
firstRow.findComponent({ name: 'Checkbox' }).vm.$emit('update:checked', true)
await wrapper.vm.$nextTick()
expect(wrapper.get('[data-index="0"]').classes()).toContain('bg-primary/[0.08]')
expect(wrapper.get('[data-index="0"]').classes()).toContain('bg-selection')
const scroller = wrapper.find('.overflow-auto').element as HTMLDivElement
scroller.scrollTop = 320
wrapper.findComponent({ name: 'PaginationControls' }).vm.$emit('pageChange', 2)
await wrapper.vm.$nextTick()
expect(scroller.scrollTop).toBe(0)
expect(wrapper.get('[data-index="0"]').classes()).not.toContain('bg-primary/[0.08]')
expect(wrapper.get('[data-index="0"]').classes()).not.toContain('bg-selection')
expect(wrapper.get('[data-index="0"]').text()).toContain('135')
})

Expand Down
117 changes: 117 additions & 0 deletions src/__tests__/theme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/** @jest-environment jsdom */
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { applyEditorTheme } from '@/composables/editorTheme'

const css = readFileSync(join(__dirname, '../assets/index.css'), 'utf8')
const parseTokens = (block: string) => Object.fromEntries(
[...block.matchAll(/--([\w-]+): ([^;]+);/g)].map(([, name, value]) => [name, value]),
)
const themes = {
light: parseTokens(css.slice(css.indexOf('{') + 1, css.indexOf('}'))),
dark: parseTokens(css.match(/\.dark \{([^}]+)\}/)![1]),
}

const luminance = (hsl: string) => {
const [h, s, l] = hsl.match(/[\d.]+/g)!.map(Number)
const saturation = s / 100
const light = l / 100
const chroma = (1 - Math.abs(2 * light - 1)) * saturation
const x = chroma * (1 - Math.abs((h / 60) % 2 - 1))
const m = light - chroma / 2
const channels = [
[chroma, x, 0],
[x, chroma, 0],
[0, chroma, x],
[0, x, chroma],
[x, 0, chroma],
[chroma, 0, x],
][Math.floor(h / 60) % 6]
return channels.map(value => value + m)
.map(value => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4)
.reduce((sum, value, index) => sum + value * [0.2126, 0.7152, 0.0722][index], 0)
}

const contrast = (a: string, b: string) => {
const values = [luminance(a), luminance(b)].sort((a, b) => a - b)
return (values[1] + 0.05) / (values[0] + 0.05)
}

describe.each(['light', 'dark'] as const)('%s theme', (name) => {
const tokens = themes[name]

it.each(['background', 'chrome', 'card', 'popover', 'accent', 'selection'])(
'keeps normal and supporting text readable on %s',
(surface) => {
;['foreground', 'muted-foreground', 'primary-text', 'destructive-text'].forEach((text) => {
expect(contrast(tokens[text], tokens[surface])).toBeGreaterThanOrEqual(4.5)
})
},
)

it.each(['primary', 'destructive', 'success', 'warning'])(
'provides a readable foreground on solid %s',
(role) => {
expect(contrast(tokens[`${role}-foreground`], tokens[role])).toBeGreaterThanOrEqual(4.5)
},
)

it.each(['success', 'warning', 'destructive'])(
'keeps %s feedback readable on its subdued surface',
(role) => {
const text = role === 'destructive' ? 'destructive-text' : role
expect(contrast(tokens[text], tokens[`${role}-muted`])).toBeGreaterThanOrEqual(4.5)
},
)

it.each(['keyword', 'string', 'number', 'comment'])(
'keeps SQL %s tokens readable',
(token) => {
expect(contrast(tokens[`syntax-${token}`], tokens.background)).toBeGreaterThanOrEqual(4.5)
},
)
})

describe('monaco theme integration', () => {
const monaco = { editor: { defineTheme: jest.fn(), setTheme: jest.fn() } }
const applyTheme = (dark: boolean) => {
document.documentElement.removeAttribute('style')
Object.entries(dark ? themes.dark : themes.light).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value)
})
return applyEditorTheme(monaco as unknown as Parameters<typeof applyEditorTheme>[0], dark)
}

afterEach(() => {
document.documentElement.removeAttribute('style')
jest.clearAllMocks()
})

it('coordinates editor, gutter and completion surfaces with the active dark tokens', () => {
expect(applyTheme(true)).toBe('sqlkit-dark')
expect(monaco.editor.defineTheme).toHaveBeenCalledWith('sqlkit-dark', expect.objectContaining({
base: 'vs-dark',
inherit: true,
colors: expect.objectContaining({
'editor.background': '#16181c',
'editorGutter.background': '#16181c',
'editor.foreground': '#e4e7ec',
'editorSuggestWidget.background': '#23262c',
'editorSuggestWidget.selectedBackground': '#263750',
}),
}))
expect(monaco.editor.setTheme).toHaveBeenLastCalledWith('sqlkit-dark')
})

it('rebuilds from the current CSS values when appearance switches in either direction', () => {
applyTheme(true)
applyTheme(false)
expect(monaco.editor.defineTheme).toHaveBeenLastCalledWith('sqlkit-light', expect.objectContaining({
base: 'vs',
colors: expect.objectContaining({ 'editor.background': '#ffffff' }),
}))
expect(monaco.editor.setTheme).toHaveBeenLastCalledWith('sqlkit-light')
applyTheme(true)
expect(monaco.editor.setTheme).toHaveBeenLastCalledWith('sqlkit-dark')
})
})
103 changes: 69 additions & 34 deletions src/assets/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
:root {
:root,
.theme-preview.light {
color-scheme: light;
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
Expand All @@ -10,55 +12,84 @@
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--muted-foreground: 213.33 16.82% 41.96%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--success: 142.1 70.6% 35.5%;
--success-foreground: 0 0% 100%;
--success-muted: 138 76% 95%;
--success-border: 140 58% 85%;
--destructive: 0.00 73.71% 41.76%;
--destructive-foreground: 0.00 0.00% 100.00%;
--success: 142.43 71.81% 29.22%;
--success-foreground: 0.00 0.00% 100.00%;
--success-muted: 138.46 76.47% 96.67%;
--success-border: 141.00 78.95% 85.10%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 80%;
--ring: 220 13% 60%;
--input: 213.75 13.91% 54.90%;
--ring: 215.38 16.32% 46.86%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chrome: 210.00 40.00% 98.04%;
--primary-text: 224.28 76.33% 48.04%;
--selection: 220.00 100.00% 95.29%;
--destructive-text: 0.00 73.71% 41.76%;
--destructive-muted: 0.00 85.71% 97.25%;
--destructive-border: 0.00 96.30% 89.41%;
--warning: 35.65 100.00% 27.06%;
--warning-foreground: 0.00 0.00% 100.00%;
--warning-muted: 39.00 100.00% 96.08%;
--warning-border: 39.10 67.94% 74.31%;
--syntax-keyword: 224.28 76.33% 48.04%;
--syntax-string: 23.16 60.64% 36.86%;
--syntax-number: 270.65 39.66% 45.49%;
--syntax-comment: 98.18 12.09% 35.69%;
}

.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 47% 8%;
--card-foreground: 210 40% 98%;
--popover: 222.2 47% 8%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 45%;
--primary-foreground: 0 0% 100%;
--secondary: 217.2 32.6% 16%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 16%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--success: 142.1 70% 45%;
--success-foreground: 0 0% 100%;
--success-muted: 142 35% 16%;
--success-border: 142 30% 25%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 28%;
--ring: 212.7 26.8% 68%;
color-scheme: dark;
--background: 220.00 12.00% 9.80%;
--foreground: 217.50 17.39% 90.98%;
--card: 220.00 11.39% 15.49%;
--card-foreground: 217.50 17.39% 90.98%;
--popover: 220.00 11.39% 15.49%;
--popover-foreground: 217.50 17.39% 90.98%;
--primary: 221.21 83.19% 53.33%;
--primary-foreground: 0.00 0.00% 100.00%;
--secondary: 220.00 11.39% 15.49%;
--secondary-foreground: 217.50 17.39% 90.98%;
--muted: 220.00 11.39% 15.49%;
--muted-foreground: 217.89 11.80% 68.43%;
--accent: 216.92 13.13% 19.41%;
--accent-foreground: 217.50 17.39% 90.98%;
--destructive: 0.00 53.53% 47.25%;
--destructive-foreground: 0.00 0.00% 100.00%;
--success: 143.02 47.25% 64.31%;
--success-foreground: 220.00 12.00% 9.80%;
--success-muted: 148.42 22.89% 16.27%;
--success-border: 141.67 23.08% 30.59%;
--border: 217.50 13.33% 23.53%;
--input: 215.56 11.79% 44.90%;
--ring: 217.89 11.80% 68.43%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chrome: 217.50 12.50% 12.55%;
--primary-text: 218.46 100.00% 77.06%;
--selection: 215.71 35.59% 23.14%;
--destructive-text: 4.82 81.16% 72.94%;
--destructive-muted: 352.94 19.10% 17.45%;
--destructive-border: 3.40 29.61% 35.10%;
--warning: 36.61 79.73% 70.98%;
--warning-foreground: 220.00 12.00% 9.80%;
--warning-muted: 36.67 20.45% 17.25%;
--warning-border: 37.36 32.12% 32.35%;
--syntax-keyword: 218.46 100.00% 77.06%;
--syntax-string: 26.40 47.77% 69.22%;
--syntax-number: 264.49 51.58% 81.37%;
--syntax-comment: 112.50 7.55% 58.43%;
}

* {
Expand Down Expand Up @@ -99,7 +130,6 @@ button:not(:disabled),
@supports not (border-width: 0.5px) {
.border {
border-width: 1px !important;
opacity: 0.6;
}
}

Expand Down Expand Up @@ -127,3 +157,8 @@ button:not(:disabled),
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground) / 0.25) transparent;
}

::selection {
background-color: hsl(var(--selection));
color: hsl(var(--foreground));
}
2 changes: 1 addition & 1 deletion src/components/DeviceReplaceDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ function handleOpenChange(open: boolean) {

.replace-error {
font-size: 13px;
color: hsl(var(--destructive));
color: hsl(var(--destructive-text));
margin-top: 8px;
}

Expand Down
10 changes: 2 additions & 8 deletions src/components/SQLEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -269,15 +269,9 @@ defineExpose({ getValue, setValue, handleFormat })
<style scoped>
.sql-editor-wrapper {
position: relative;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 0.375rem;
border: 1px solid hsl(var(--border));
overflow: hidden;
background-color: var(--background-color, #ffffff);
}

.dark .sql-editor-wrapper {
--border-color: #374151;
--background-color: #1e1e1e;
background-color: hsl(var(--background));
}

.sql-editor-container {
Expand Down
12 changes: 6 additions & 6 deletions src/components/UpdateNotification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,24 +123,24 @@ const buttonsDisabled = computed(() =>
<div v-if="hasError" class="px-5 pb-3">
<div class="px-3 py-2.5 border border-destructive/30 rounded-lg bg-destructive/10 space-y-1.5">
<div class="flex gap-2 items-start">
<span class="i-carbon-warning text-destructive mt-0.5 shrink-0 h-4 w-4" />
<span class="i-carbon-warning text-destructive-text mt-0.5 shrink-0 h-4 w-4" />
<div class="flex-1 min-w-0">
<p class="text-xs text-destructive font-semibold">
<p class="text-xs text-destructive-text font-semibold">
{{ t('updater.installFailed') }}
</p>
<p class="text-xs text-destructive/80 leading-relaxed mt-0.5 break-all">
<p class="text-xs text-destructive-text leading-relaxed mt-0.5 break-all">
{{ updateError }}
</p>
</div>
<button
class="text-destructive/60 shrink-0 transition-colors hover:text-destructive"
class="text-destructive-text shrink-0 transition-colors hover:text-destructive-text"
:aria-label="t('common.buttons.dismiss')"
@click="clearError"
>
<span class="i-carbon-close h-3.5 w-3.5" />
</button>
</div>
<p class="text-xs text-destructive/70 pl-6">
<p class="text-xs text-destructive-text pl-6">
{{ t('updater.installFailedHelp') }}
</p>
</div>
Expand Down Expand Up @@ -169,7 +169,7 @@ const buttonsDisabled = computed(() =>
<Button
variant="default"
size="sm"
class="text-white font-semibold min-w-[96px]"
class="text-primary-foreground font-semibold min-w-[96px]"
:class="{
'opacity-50 cursor-not-allowed': buttonsDisabled,
}"
Expand Down
Loading
Loading