The SQL Workbench (PR #47) did not look like part of Next UI, and dark mode looked bad. This PR rebuilds its controls from the app's standard styles and theme tokens, so light and dark mode both work by construction.
What was wrong
Editor: CodeMirror's built-in light highlight style showed dark purple and blue keywords on the dark background. The light active-line band, the white autocomplete popup and the light selection were also built in, and the border and focus colors pointed at CSS variables that do not exist (--border, --ring).
Borders: Tailwind v4 draws a bare border in currentColor, so every button, select, input and cell had a near-white outline in dark mode and a black one in light mode.
Hand-made controls: the cell actions were ▲ ▼ ✕ glyph buttons floating over the Write mode toggle. The library and result tabs were ad-hoc pill buttons. The export links were plain CSV / XLSX / JSON text. Notebook delete used window.confirm.
Charts: fixed hex colors, a white Recharts tooltip, and harsh grey grid lines in dark mode.
Contrast:text-amber-600 warnings measured about 3:1 on white. White text on the dark theme's red was 3.8:1. Hovered primary and danger buttons dropped below 4.5:1 because the /90 fill lightens on light surfaces.
Layout: a 16rem library column sat beside the notebook from lg up, so the notebook got about 500px at 1024. Markdown code blocks scrolled sideways inside the cell. Run statuses showed raw enums (SUCCEEDED), and dates were raw ISO strings.
What changed
src/components/ui/controls.ts: the standard control styles (the variants in Button.stories.tsx) as one design-system module: buttons (primary, secondary, destructive, danger, ghost; sm and md), icon buttons, input, select, checkbox, tabs, menu, badge, card, alert and warning. The script panel classes moved here and ScriptPanels.tsx re-exports them, so existing imports are unchanged.
Editor (editor-theme.ts): a CodeMirror theme built from the app tokens plus the shared --qqq-code-* palette. That palette already meets 4.5:1 in both modes. It themes the gutter, cursor, selection, active line, bracket match, placeholder and the autocomplete popup. Because it is all CSS variables, the editor switches live with the theme toggle without being reconfigured.
Result grid: uses the record grid's look (muted sticky header, standard-density cells, row hover, numbers right-aligned). It is a keyboard-focusable scroll region and the only inner scroll on the page. Export is now a menu (CSV, Excel (XLSX), JSON) like the record query's, and pagination and copy use the standard buttons.
Charts: use the theme chart palette (--color-chart-1..10, which the dark theme redefines), popover-surface tooltips and muted axes and grid.
Dialogs: the write confirmation and the new notebook-delete confirmation follow the app's alert-dialog pattern. Statement kinds have plain names, and the full statement is shown instead of truncated.
Layout: the workbench is one column until the widget itself is wide enough (a container query, so the app sidebar counts). Order on a phone is library, notebook, then recent runs. Cell move and delete actions moved into the cell header. Nothing scrolls sideways except the result grid.
Labels and dates: plain labels throughout: "New notebook", "Add SQL cell", "Add notes cell", "Results / Chart / Plan", "Succeeded · 37 ms · 12 rows", "1 row affected", "Raw text". Parameter types read Text, Whole number, Date and so on. Version, pinned-result and history dates use DateTimeText. SQL result values stay exactly as the database returned them.
Button hover and dark red: primary and danger buttons darken on hover with a token color-mix, and the danger fill is darkened in dark mode so white labels keep 4.5:1.
Screenshots
Before is main, after is this branch. The mock data and production build are the same for both.
Before
After
Light, 1440
Dark, 1440
Light, 375
Dark, 375
Editor autocomplete, dark
Chart, dark
Versions, plan, pinned results, dark
Write confirmation, dark
I also captured every state at 375, 768, 1024, 1280, 1440 and 1920 in both themes. None of them scroll sideways (scrollWidth - clientWidth = 0 everywhere).
Tests
Unit: the workbench tests are updated for the new tabs (role="tab" / aria-selected), the Export menu and the plain labels. The axe checks still pass.
E2E (tests/e2e/sql-workbench.spec.ts):
Theme: a new test switches to dark mode through Preferences without a reload. It asserts that the editor surface changes to the app's --color-background, and runs axe color-contrast scoped to the workbench in light mode, dark mode, the chart view and the write dialog. The suite-wide helper keeps that rule off for the rest of the app.
Phone: a new test asserts there is no sideways page scroll at 375 px.
Local results:
pnpm typecheck and pnpm lint (0 errors, existing warnings only)
unit tests: 204 of 207 files passed. WidgetRenderer passes when run alone (a load-related flake under coverage). The two standalone/acceptance path tests fail only on Windows (.pnpm\example path separators); they are unrelated to this change.
full mocked e2e: 122 passed
pnpm build and the static-export bundle budget pass (CSS 21.5 of 22 KB, sqlWorkbench chunk 131 of 135 KB)
Notes
Brand accent contrast: the contrast test uses the theme's own primary color. The mocked brand accent #6366f1 is under 4.5:1 behind white text everywhere in the app (see a11y-helpers.ts); that is the application's branding choice, not a workbench issue.
Overlap with open PRs:#61 also edits SqlWorkbenchWidget.tsx (the ?sql= deep link) and its test. Those hunks sit apart from this PR's, apart from the shared React import line, which this PR leaves alone.
## Summary
The SQL Workbench (PR #47) did not look like part of Next UI, and dark mode looked bad. This PR rebuilds its controls from the app's standard styles and theme tokens, so light and dark mode both work by construction.
### What was wrong
- **Editor:** CodeMirror's built-in light highlight style showed dark purple and blue keywords on the dark background. The light active-line band, the white autocomplete popup and the light selection were also built in, and the border and focus colors pointed at CSS variables that do not exist (`--border`, `--ring`).
- **Borders:** Tailwind v4 draws a bare `border` in `currentColor`, so every button, select, input and cell had a near-white outline in dark mode and a black one in light mode.
- **Hand-made controls:** the cell actions were ▲ ▼ ✕ glyph buttons floating over the Write mode toggle. The library and result tabs were ad-hoc pill buttons. The export links were plain `CSV / XLSX / JSON` text. Notebook delete used `window.confirm`.
- **Charts:** fixed hex colors, a white Recharts tooltip, and harsh grey grid lines in dark mode.
- **Contrast:** `text-amber-600` warnings measured about 3:1 on white. White text on the dark theme's red was 3.8:1. Hovered primary and danger buttons dropped below 4.5:1 because the `/90` fill lightens on light surfaces.
- **Layout:** a 16rem library column sat beside the notebook from `lg` up, so the notebook got about 500px at 1024. Markdown code blocks scrolled sideways inside the cell. Run statuses showed raw enums (`SUCCEEDED`), and dates were raw ISO strings.
### What changed
- **`src/components/ui/controls.ts`:** the standard control styles (the variants in `Button.stories.tsx`) as one design-system module: buttons (primary, secondary, destructive, danger, ghost; sm and md), icon buttons, input, select, checkbox, tabs, menu, badge, card, alert and warning. The script panel classes moved here and `ScriptPanels.tsx` re-exports them, so existing imports are unchanged.
- **Editor (`editor-theme.ts`):** a CodeMirror theme built from the app tokens plus the shared `--qqq-code-*` palette. That palette already meets 4.5:1 in both modes. It themes the gutter, cursor, selection, active line, bracket match, placeholder and the autocomplete popup. Because it is all CSS variables, the editor switches live with the theme toggle without being reconfigured.
- **Result grid:** uses the record grid's look (muted sticky header, standard-density cells, row hover, numbers right-aligned). It is a keyboard-focusable scroll region and the only inner scroll on the page. Export is now a menu (CSV, Excel (XLSX), JSON) like the record query's, and pagination and copy use the standard buttons.
- **Charts:** use the theme chart palette (`--color-chart-1..10`, which the dark theme redefines), popover-surface tooltips and muted axes and grid.
- **Dialogs:** the write confirmation and the new notebook-delete confirmation follow the app's alert-dialog pattern. Statement kinds have plain names, and the full statement is shown instead of truncated.
- **Layout:** the workbench is one column until the widget itself is wide enough (a container query, so the app sidebar counts). Order on a phone is library, notebook, then recent runs. Cell move and delete actions moved into the cell header. Nothing scrolls sideways except the result grid.
- **Labels and dates:** plain labels throughout: "New notebook", "Add SQL cell", "Add notes cell", "Results / Chart / Plan", "Succeeded · 37 ms · 12 rows", "1 row affected", "Raw text". Parameter types read Text, Whole number, Date and so on. Version, pinned-result and history dates use `DateTimeText`. SQL result values stay exactly as the database returned them.
- **Button hover and dark red:** primary and danger buttons darken on hover with a token `color-mix`, and the danger fill is darkened in dark mode so white labels keep 4.5:1.
### Screenshots
Before is `main`, after is this branch. The mock data and production build are the same for both.
| | Before | After |
|---|---|---|
| Light, 1440 |  |  |
| Dark, 1440 |  |  |
| Light, 375 |  |  |
| Dark, 375 |  |  |
| Editor autocomplete, dark |  |  |
| Chart, dark |  |  |
| Versions, plan, pinned results, dark |  |  |
| Write confirmation, dark |  |  |
I also captured every state at 375, 768, 1024, 1280, 1440 and 1920 in both themes. None of them scroll sideways (`scrollWidth - clientWidth = 0` everywhere).
### Tests
- **Unit:** the workbench tests are updated for the new tabs (`role="tab"` / `aria-selected`), the Export menu and the plain labels. The axe checks still pass.
- **E2E (`tests/e2e/sql-workbench.spec.ts`):**
- **Theme:** a new test switches to dark mode through Preferences without a reload. It asserts that the editor surface changes to the app's `--color-background`, and runs axe `color-contrast` scoped to the workbench in light mode, dark mode, the chart view and the write dialog. The suite-wide helper keeps that rule off for the rest of the app.
- **Phone:** a new test asserts there is no sideways page scroll at 375 px.
- **Local results:**
- `pnpm typecheck` and `pnpm lint` (0 errors, existing warnings only)
- unit tests: 204 of 207 files passed. `WidgetRenderer` passes when run alone (a load-related flake under coverage). The two standalone/acceptance path tests fail only on Windows (`.pnpm\example` path separators); they are unrelated to this change.
- full mocked e2e: 122 passed
- `pnpm build` and the static-export bundle budget pass (CSS 21.5 of 22 KB, `sqlWorkbench` chunk 131 of 135 KB)
### Notes
- **Brand accent contrast:** the contrast test uses the theme's own primary color. The mocked brand accent `#6366f1` is under 4.5:1 behind white text everywhere in the app (see `a11y-helpers.ts`); that is the application's branding choice, not a workbench issue.
- **Overlap with open PRs:** #61 also edits `SqlWorkbenchWidget.tsx` (the `?sql=` deep link) and its test. Those hunks sit apart from this PR's, apart from the shared `React` import line, which this PR leaves alone.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
The SQL Workbench (PR #47) did not look like part of Next UI, and dark mode looked bad. This PR rebuilds its controls from the app's standard styles and theme tokens, so light and dark mode both work by construction.
What was wrong
--border,--ring).borderincurrentColor, so every button, select, input and cell had a near-white outline in dark mode and a black one in light mode.CSV / XLSX / JSONtext. Notebook delete usedwindow.confirm.text-amber-600warnings measured about 3:1 on white. White text on the dark theme's red was 3.8:1. Hovered primary and danger buttons dropped below 4.5:1 because the/90fill lightens on light surfaces.lgup, so the notebook got about 500px at 1024. Markdown code blocks scrolled sideways inside the cell. Run statuses showed raw enums (SUCCEEDED), and dates were raw ISO strings.What changed
src/components/ui/controls.ts: the standard control styles (the variants inButton.stories.tsx) as one design-system module: buttons (primary, secondary, destructive, danger, ghost; sm and md), icon buttons, input, select, checkbox, tabs, menu, badge, card, alert and warning. The script panel classes moved here andScriptPanels.tsxre-exports them, so existing imports are unchanged.editor-theme.ts): a CodeMirror theme built from the app tokens plus the shared--qqq-code-*palette. That palette already meets 4.5:1 in both modes. It themes the gutter, cursor, selection, active line, bracket match, placeholder and the autocomplete popup. Because it is all CSS variables, the editor switches live with the theme toggle without being reconfigured.--color-chart-1..10, which the dark theme redefines), popover-surface tooltips and muted axes and grid.DateTimeText. SQL result values stay exactly as the database returned them.color-mix, and the danger fill is darkened in dark mode so white labels keep 4.5:1.Screenshots
Before is
main, after is this branch. The mock data and production build are the same for both.I also captured every state at 375, 768, 1024, 1280, 1440 and 1920 in both themes. None of them scroll sideways (
scrollWidth - clientWidth = 0everywhere).Tests
role="tab"/aria-selected), the Export menu and the plain labels. The axe checks still pass.tests/e2e/sql-workbench.spec.ts):--color-background, and runs axecolor-contrastscoped to the workbench in light mode, dark mode, the chart view and the write dialog. The suite-wide helper keeps that rule off for the rest of the app.pnpm typecheckandpnpm lint(0 errors, existing warnings only)WidgetRendererpasses when run alone (a load-related flake under coverage). The two standalone/acceptance path tests fail only on Windows (.pnpm\examplepath separators); they are unrelated to this change.pnpm buildand the static-export bundle budget pass (CSS 21.5 of 22 KB,sqlWorkbenchchunk 131 of 135 KB)Notes
#6366f1is under 4.5:1 behind white text everywhere in the app (seea11y-helpers.ts); that is the application's branding choice, not a workbench issue.SqlWorkbenchWidget.tsx(the?sql=deep link) and its test. Those hunks sit apart from this PR's, apart from the sharedReactimport line, which this PR leaves alone.🤖 Generated with Claude Code