From 1702948a7ed477140fcee69f122a3019a7c648a1 Mon Sep 17 00:00:00 2001 From: Zack Nelson Date: Tue, 1 Sep 2026 18:19:23 +0100 Subject: [PATCH 01/10] feat: lift light surfaces and neutralize chrome hover and focus WIP theme pass: compress the light elevation ramp, quiet shadows, and rebind hover/focus/selection off brand so crimson stays on tabs, notebook identity, and primary actions. Co-authored-by: Cursor --- THEME-STEERING.md | 286 ++++++++++++++++++ .../AssistantModesCompact.tsx | 2 +- src/components/Button/index.tsx | 2 +- src/components/Drawer/content-wrapper.tsx | 2 +- src/components/Drawer/index.tsx | 4 +- src/components/Form/FormInput/index.tsx | 3 + src/components/Input/index.tsx | 8 +- src/components/ResultGrid/styles.ts | 16 +- src/components/SegmentedControl/index.tsx | 7 +- src/components/SelectMenu/index.tsx | 2 +- src/components/Sidebar/index.tsx | 4 +- src/components/TableSelector/index.tsx | 12 +- src/components/Tooltip/index.tsx | 4 +- .../TopBar/InstanceSettingsPopper.tsx | 2 +- src/components/TopBar/index.tsx | 2 +- src/modules/OAuth2/views/login.tsx | 7 +- src/scenes/Console/index.tsx | 3 +- .../AIChatWindow/AIChatErrorBoundary.tsx | 2 +- .../Editor/AIChatWindow/AIChatWindowLazy.tsx | 2 +- .../Editor/AIChatWindow/ChatHistoryItem.tsx | 12 +- .../Editor/AIChatWindow/ChatHistoryView.tsx | 2 +- src/scenes/Editor/AIChatWindow/ChatInput.tsx | 4 +- .../Editor/AIChatWindow/ChatMessages.tsx | 5 +- src/scenes/Editor/AIChatWindow/index.tsx | 4 +- src/scenes/Editor/Menu/index.tsx | 2 +- src/scenes/Editor/Monaco/tabs.tsx | 7 +- .../Editor/Notebook/NotebookToolbar.tsx | 5 +- .../Notebook/cells/CellRunDrawToggles.tsx | 5 +- .../Editor/Notebook/cells/CellToolbar.tsx | 17 +- .../Editor/Notebook/cells/CellViewToggle.tsx | 7 + .../Editor/Notebook/cells/CellWrapper.tsx | 7 +- src/scenes/Layout/warning.tsx | 2 +- src/scenes/Schema/Row/index.tsx | 10 +- src/scenes/Schema/VirtualTables/index.tsx | 4 +- src/scenes/Schema/index.tsx | 6 + src/scenes/Search/SearchPanel.tsx | 12 +- src/scenes/Search/SearchResults.tsx | 8 +- src/scenes/SideMenu/index.tsx | 2 +- src/styles/_editor.scss | 2 +- src/styles/_grid.scss | 2 +- src/styles/_quick-vis.scss | 2 +- src/styles/lib/_react-chrome-tabs.scss | 24 +- src/styles/main.scss | 2 +- src/theme/global-styles.ts | 6 +- src/theme/global-styles/docsearch.ts | 2 +- src/theme/index.ts | 52 ++-- 46 files changed, 479 insertions(+), 104 deletions(-) create mode 100644 THEME-STEERING.md diff --git a/THEME-STEERING.md b/THEME-STEERING.md new file mode 100644 index 000000000..865b7a601 --- /dev/null +++ b/THEME-STEERING.md @@ -0,0 +1,286 @@ +# Theme steering log + +Designer-owned trail of palette experiments. `THEMING.md` stays the contract +until Emre reviews an entry and folds it in. + +Two lifts only: + +| Lift | What it is | Where it lands | Emre’s job | +| --- | --- | --- | --- | +| **Palette** | Retune an existing role in one or both themes | `src/theme/index.ts` only | Check neighbors in both modes, then update `THEMING.md` values | +| **Binding** | This component is using the wrong role, or needs a new role | Component file + maybe a new token in `index.ts` | Confirm the meaning, rebind, add the token only if reuse would lie | + +Palette is the default. Binding is the exception — including “brand crimson is +doing a job it should not.” Do not invent a component-named color +(`notebookCellBackground`). Either retune `surfaceRaised`, rebind the component +to a different existing role, or add a role whose *meaning* is new. + +Status: `applied locally` → `ready for Emre` → `accepted` / `tweaked` / `reverted`. + +--- + +## Entry template + +Copy this block. One decision per entry. If a palette change and a binding +change shipped together, split them. + +``` +### YYYY-MM-DD — short name +- Lift: palette | binding | palette + binding +- Status: applied locally +- Modes: light / dark / both +- Tokens: `tokenName` +- Before → after: `#old` → `#new` (light), … (dark) +- Binding: `Component` used `oldRole`, should use `newRole` +- Neighbors: stage / raised / inset / … +- Walked: notebook, SQL editor, details drawer, grid +- Why: +- Out of scope: +- For Emre: +``` + +--- + +## Log + +### 2026-09-01 — Light surfaces: lift baseline, compress elevation ramp +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `surfaceCanvas`, `surfaceStage`, `surfaceBase`, `surfaceInset`, `surfaceRaised`, `surfaceInput`, `surfaceOverlay`, `surfaceValue`, `surfaceScrim`, `surfaceTabRail` +- Before → after (light): + - `surfaceStage` `#c9cdd4` → `#e2e5ea` + - `surfaceCanvas` `#d9dce2` → `#e8eaee` + - `surfaceBase` `#e3e5e9` → `#eef0f3` + - `surfaceRaised` `#e8eaee` → `#f4f5f7` + - `surfaceInset` `#eceef1` → `#f7f8f9` + - `surfaceInput` / `surfaceOverlay` `#f2f3f5` → `#fafbfc` + - `surfaceValue` `#fbfcfd` → `#ffffff` + - `surfaceScrim` ink `.38` → `.32` + - `surfaceTabRail` `(218,221,227,.94)` → `(232,234,238,.94)` +- Neighbors: stage still recedes from canvas; raised still sits above stage; inset stays the bright well. Stage→raised gap ~31 RGB points → ~18. +- Walked: notebook cells, SQL canvas, details drawer. Grids/charts were already working — `gridRow` / `gridHeader` / `chartSeries*` not touched. +- Why: Light mode felt washed. Baseline too gray; elevation ramp between field and cards too large, so SQL, notebook, and details went muddy while grids still read. +- Out of scope: dark mode; content/accent; interaction fills (`interactionNeutral` still `#d9dce2`). +- For Emre: Confirm the compressed light ladder still matches the spatial story in `THEMING.md` (darker stage, brighter embedded content). Update the Core palette table if accepted. + +### 2026-09-01 — Light SQL canvas follows the new baseline +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `editorCanvas`, `editorBorder` +- Before → after (light): `editorCanvas` `#d9dce2` → `#eef0f3`; `editorBorder` `#c9cdd4` → `#e2e5ea` +- Neighbors: `editorCanvas` now matches `surfaceBase`; sits as a quiet well inside `surfaceRaised` cells. +- Why: The SQL editor does not read `surface*`. Leaving `editorCanvas` at the old gray would have stranded the named problem area inside the new cards. +- Out of scope: syntax colors, selection, active line. +- For Emre: This is still a palette steer, not a rebinding. Flag if editor should instead reuse `surfaceInset` / `surfaceBase` and drop the dedicated tokens. + +### 2026-09-01 — Light shadows quieter +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `shadowSubtle`, `shadowSoft`, `shadowMedium`, `shadowStrong`, `shadowOverlay` +- Before → after (light): `.06/.11/.18/.27/.38` → `.035/.055/.08/.16/.22` +- Why: Card and overlay shadows were doing too much of the elevation job once surfaces got closer together. One primary cue (surface step), not surface + heavy shade. +- Out of scope: dark shadows; glass tokens; shadow *geometry* (see next entry). +- For Emre: Dialogs/menus use `shadowStrong` / `shadowOverlay` — check they still float after the opacity cut. + +### 2026-09-01 — Notebook cell and details drawer shadow geometry +- Lift: binding (geometry, same tokens) +- Status: applied locally +- Modes: both (geometry is shared; light opacity change is what you feel first) +- Binding: + - `CellWrapper` hover `0 16px 44px shadowSoft` → `0 1px 2px shadowSubtle, 0 3px 8px shadowSoft` + - `CellWrapper` focus extra `0 18px 50px shadowMedium` → `0 1px 2px shadowSubtle, 0 4px 10px shadowSoft` (accent ring unchanged) + - `Drawer` `-18px 0 52px shadowSoft` → `-4px 0 16px shadowSoft` +- Why: Token opacity alone cannot tuck a 50px blur. Notebook cells and the details drawer were the named surfaces. +- Out of scope: dropdown/modal stacks in `overlayStyles.ts` (`0 1.2rem 1.6rem`, `0 2.4rem 7.2rem`). Call out if those should follow. +- For Emre: Higher lift than the opacity cut. Resting cells still have no shadow (border only); hover/focus are the ones tucked. Dark mode inherits the tighter geometry — glance at a focused cell in dark. + +### 2026-09-01 — Light result grid follows the dense-data ladder +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `gridRow`, `gridHeader` +- Before → after (light): `gridRow` `#e9ebef` → `#f4f5f7`; `gridHeader` `#dce0e6` → `#e7eaee` +- Neighbors (light, per `THEMING.md`): field `surfaceInset` `#f7f8f9` lightest → rows `#f4f5f7` → headers `#e7eaee` strongest gray. Same deltas as the pre-lift ladder (`#eceef1` / `#e9ebef` / `#dce0e6`), shifted with the new inset. `gridRow` now matches `surfaceRaised`, so a notebook cell’s body and its grid rows are one surface; the header bar is the structure cue. `gridSelection` / `gridFocus` / `contentObject` unchanged. +- Walked: standalone result grid, notebook inline grids. +- Why: The surface lift moved `surfaceInset` (empty field / viewport) and left `gridRow` / `gridHeader` on the old gray. The THEMING order was intact but the steps were the old muddy slab inside the new baseline. +- Out of scope: dark grid; zebra (the renderer does not stripe; hover uses `surfaceInset`); column-name crimson (`contentObject` — parking lot / binding). +- For Emre: Update the Result grid table in `THEMING.md` if accepted. Confirm header still reads as the strongest gray on a full-width grid and inside a raised cell. Selection `#e0afbf` was not retuned — glance at a selected row on the new `gridRow`. + +### 2026-09-01 — Light segmented chips sit on the new baseline +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `controlTrack`, `interactionNeutral`, `interactionNeutralHover` +- Before → after (light): + - `controlTrack` / `interactionNeutral` `#d9dce2` → `#e8eaee` (same hex as new `surfaceCanvas`) + - `interactionNeutralHover` `#d2d6dd` → `#e2e5ea` (same hex as new `surfaceStage`) +- Neighbors: Table/Chart chips use `controlTrack` as the segmented substrate on `surfaceRaised` `#f4f5f7`. Old track was the pre-lift canvas, so the chip block jumped ~20 RGB points off the header. New track is a one-step inset. Selected segment is still the glass lens (`glassSurface`), not these fills. `borderStrong` on the track is unchanged. +- Walked: notebook cell Table/Chart chips. List/Grid uses the same `NotebookViewToggle` track. +- Why: Leftover `#d9dce2` after the surface lift. The chip group read as a dark elevated slab. +- Out of scope: `borderStrong` on the segmented control; glass lens opacity; dark mode. +- For Emre: `THEMING.md` already maps `controlTrack` to segmented substrate and switch tracks — update the light hexes. Switches and other segmented controls share this token; check a switch at rest. + +### 2026-09-01 — Chrome hairline stagger: rails vs panel/work +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `borderDefault`, `borderSubtle` (values unchanged) +- Binding: icon rails (`Sidebar` left/right, `TopBar`) used `borderSubtle`; now `borderDefault`. Panel-to-work edges stay `borderSubtle` (`Console` left `Wrapper`, `SidePanelRight`, `Drawer`, allotment sash). +- Neighbors: `borderSubtle` is internal only — inside a panel or inside the working surface. Chrome rails sit one step stronger so the stagger reads. +- Walked: left rail | schema | editor; editor | details/AI | right rail; top bar under the logo/menu. +- Why: All chrome hairlines were the same weight, so rails and content panels sat on one plane. +- Out of scope: footer `border-top`; allotment sash hover (`contentAccent`); token values themselves. +- For Emre: Keep the stagger in the chrome composition notes. Do not bump panel-to-work to `borderDefault` — that would collapse the two weights. + +### 2026-09-01 — Content sidebars: both elevated +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `surfaceRaised` (values unchanged) +- Binding: schema (`Schema` / `SearchPanel` wrappers + `PaneContent` + `Console` left `Wrapper`), details (`Drawer` shell + `ContentWrapper`, `SidePanelRight`), AI (`AIChatWindow` shells, lazy loader, error boundary, history), mobile schema (`SideMenu`) used `surfaceBase`; now `surfaceRaised`. Icon rails stay `surfaceBase`. +- Neighbors: light `surfaceBase` `#eef0f3` → panels `#f4f5f7` (`surfaceRaised`). Dark `surfaceBase` `#17181d` → panels `#1d1e24`. Work surface stays `surfaceStage` / editor canvas. `PaneWrapper` / `PaneContent` globally are still `surfaceBase` so Editor / Result / Import are untouched. +- Walked: tables tree vs notebook; details drawer vs notebook; AI chat vs notebook. Light and dark. First pass left the tree on `surfaceBase` because `PaneContent` paints that fill over the wrapper — override both, with `&&` so the shared primitives cannot win on equal specificity. +- Why: Dark treated details/AI as a lift and tables as mute. Light had almost no step from canvas to either sidebar. Same role on both sides, both modes. +- Out of scope: `Panel.Header` (already `surfaceRaised` — title bar now matches the panel body); filter input (`surfaceInput`); result/editor panes; palette retune of `surfaceRaised`; drawer drop shadow (still `shadowSoft` on the right only). +- For Emre: Left and right content panels share `surfaceRaised`. Rails stay `surfaceBase`. Schema/Search must override `PaneContent` as well as `PaneWrapper`. Drawer header no longer steps above the body — the step is panel vs work, not title vs body. + +### 2026-09-01 — Light chrome shadows: tighter geometry +- Lift: binding (geometry, same tokens) +- Status: applied locally +- Modes: light, except tab-rail hairline is both +- Tokens: `shadowSubtle`, `shadowSoft`, `shadowMedium`, `borderSubtle` (values unchanged) +- Binding: + - Table/Chart (and List/Grid) glass lens `0 3px 9px shadowSoft` → `0 1px 1px shadowSubtle, 0 1px 3px shadowSoft` in light. Dark kept. Hover-preview on Run/Draw matches. + - Notebook title bar `0 12px 24px shadowSoft` → `0 1px 2px shadowSubtle, 0 2px 6px shadowSoft` in light. Existing `borderSubtle` hairline kept. Dark kept. + - Tab rail under-shadow `0 8px 20px shadowSoft` → same tight pair as the notebook title in light. Dark kept. + - Tab rail hairline: `border-bottom: 0` → `borderSubtle` in both modes, sitting with the shadow. + - Tab overflow fades: 10px `shadowMedium` → transparent → 6px with the stop at 65%, light only. +- Neighbors: same two-stop recipe as cell hover (`shadowSubtle` contact + `shadowSoft` lift), just smaller because these sit on chrome not on the stage. +- Walked: Table/Chart chip, notebook heading, tab rail with enough tabs to overflow. +- Why: Light opacity was already cut; 20–24px blurs still read as haze. Cloudflare-style lift is a 1–2px offset and a short blur. +- Out of scope: dark geometry (except the new tab hairline); active-tab glass (`0 6px 18px` still); Metrics heading; cell hover (already tucked); drawer; token opacities. +- For Emre: Geometry only. Do not retune `shadowSoft`. Overflow uses `html[data-theme="light"]` because those fades live in SCSS. + +### 2026-09-01 — Chat history hover follows the raised panel +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `surfaceBase` (light), `surfaceInput` (dark) — values unchanged +- Binding: `ChatHistoryItem` hover used `surfaceRaised`, which is now the panel fill. Hover inverts against the panel: one step darker in light (`surfaceBase` `#eef0f3`), one step lighter in dark (`surfaceInput` `#21222c`). +- Neighbors: panel `surfaceRaised` (`#f4f5f7` / `#1d1e24`). `surfaceInput` lightens both modes, so it is wrong in light. `surfaceOverlay` equals `surfaceRaised` in dark. `interactionHover` would invert, but as a wash rather than a surface step. +- Walked: AI chat history, light and dark. +- Why: Elevating the AI panel to `surfaceRaised` left hover on the same token. First rebind to `surfaceInput` everywhere; Zack caught that light hover should recede, not lift. +- Out of scope: rest/current row fill (still transparent); date separators; palette retune of `surfaceOverlay` in dark. +- For Emre: Hover on a raised list is not `surfaceRaised`. Light uses `surfaceBase`, dark uses `surfaceInput`. A single inverting overlay (`interactionHover`) would avoid the mode split if that role is the intended meaning. + +### 2026-09-01 — Tooltip border matches floating menus +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `borderDefault` (values unchanged) +- Binding: `Tooltip` box and arrow stroke used `contentDisabled` (a type color, reads as heavy as `borderStrong` or heavier). Rebind to `borderDefault`, same as `floatingSurfaceStyles` / dropdown menus. +- Neighbors: `borderSubtle` → `borderDefault` → `borderStrong`. Menus sit on `borderDefault`. Tooltip should not sit above them. +- Walked: editor toolbar tooltips, notebook heading actions, both modes. +- Why: After the surface lift the `contentDisabled` stroke was a hard outline on a quiet panel. +- Out of scope: tooltip fill (`surfaceInset`); dropdown/popover borders (already `borderDefault`); `contentDisabled` as type. +- For Emre: Tooltip chrome is a floating surface. Its edge is `borderDefault`, not a content token. + +### 2026-09-01 — Inline AI action cards invert in light +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: `surfaceValue` (light), `authBackdrop` (dark) +- Binding: `UserRequestBox` (Explain / Fix / Schema / Ask cards) used `authBackdrop` in both modes. That token is the login field: dark `#1d070e` recedes, light `#c7cbd2` is a muddy slab darker than the raised chat panel. Light now uses `surfaceValue` `#ffffff`. Dark stays on `authBackdrop`. +- Neighbors: chat panel `surfaceRaised` `#f4f5f7`. Nested SQL uses `editorCanvas` `#eef0f3`, so the query block recedes slightly inside the white card. Login `authBackdrop` is untouched. +- Walked: Explain Query card in the AI panel, light mode. Dark glance to confirm the crimson well remains. +- Why: Darker-on-dark should invert to lighter-on-light. `authBackdrop` cannot do that job in both themes. +- Out of scope: plain user `MessageBubble` (still `authBackdrop`); login page; palette retune of `authBackdrop`; inner LiteEditor. +- For Emre: AI action cards are not login chrome. Light fill is `surfaceValue`. Consider whether `MessageBubble` should follow. + +### 2026-09-01 — Cell icon hover is one step darker in light +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: `surfaceBase`, `interactionNeutralHover` (values unchanged) +- Binding: Ghost `IconButton` hover is `surfaceRaised`, which is the cell fill, so Maximize / More vanish. Light hover on those is now `surfaceBase` (`#eef0f3` on `#f4f5f7`). Split / reset inside the Table/Chart track sit on `controlTrack`; light hover is `interactionNeutralHover` (`#e2e5ea` on `#e8eaee`). Dark still uses `surfaceRaised`. +- Neighbors: do not retune global ghost hover — on `surfaceBase` chrome (rails, notebook title) `surfaceRaised` is the correct lift. +- Walked: focused cell Maximize and More, Table/Chart split icon, light mode. +- Why: Same-token hover on a raised cell. Dark already reads; light needed one receding step. +- Out of scope: global ghost; Table/Chart unselected `interactionHover`; markdown Edit/Apply; token opacities. +- For Emre: Ghost hover cannot be one fill everywhere. On raised cells it must recede (`surfaceBase`); on base chrome it must lift (`surfaceRaised`). + +### 2026-09-01 — Neutralise brand on fields, lists, and grid hover +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `borderStrong`, `borderDefault`, `interactionHover`, `interactionNeutral` (values unchanged) +- Binding: + - Default `Input` / `TextArea` focus: `contentAccent` → `borderStrong`. Accent is opt-in via `$tone="accent"` (AI chat composer, chat history search, login). + - Assistant Settings provider tab underline: `contentAccent` → `borderStrong`. Fields inherit the new default. + - `TableSelector` trigger and items: `borderAccent` / `interactionAccent*` → `borderDefault` / `interactionHover` / `interactionNeutral`. + - Schema tree row hover/focus and context-menu lock: `interactionAccent*` / `borderAccent` → `interactionHover` / `interactionNeutral` / `borderDefault`. Details `i` → `contentSecondary`. + - Result grid row hover wash: `interactionAccentHover` → `interactionHover`. Frozen-handle hover bar: `contentAccent` → `borderStrong`. +- Neighbors: rail `Navigation` stays `contentAccent` (tool selection). Table/column glyphs still `contentAccent`. `SelectMenu` check and open border stay accent (AI model dropdown). Grid *selection* `gridSelection` and column-resize ghost stay branded. Copy-pulse on schema rows stays accent. +- Walked: Filter... in tables, table picker, tree hover, result-grid hover, Assistant Settings provider + API key, AI composer (still crimson), login (still crimson). +- Why: Accent was doing hover, focus, and selection jobs that are not high-level brand actions. +- Out of scope: `contentObject` column names; tree type icons; rail icons; `SelectMenu`; `TabButton` globally (Monitoring/Details still accent); `gridSelection`; checkboxes/switches. +- For Emre: Default field focus is `borderStrong`. Accent is `$tone="accent"`. Do not retune `contentAccent` to fix these. **Superseded in part:** provider-tab underline reverted the same day (tabs stay branded); grid selection/resize and column names handled in the following two entries. + +### 2026-09-01 — Grid selection and cell-focus ring go slate +- Lift: palette +- Status: applied locally +- Modes: both +- Tokens: `gridSelection`, `gridFocus` +- Before → after: + - light `gridSelection` `#e0afbf` → `#d8dce3`; `gridFocus` `#8a0f35` → `#828b99` (same as light `borderStrong`) + - dark `gridSelection` `#2b1d25` → `#252830`; `gridFocus` `#b81447` → `#6b7382` (stronger than dark `borderStrong` `#3d414d` so the 1px inset still reads on the selected cell) +- Neighbors: light `gridRow` `#f4f5f7`, `gridHeader` `#e7eaee`. Dark `gridRow` `#17181d`, `gridHeader` `#202126`. Selection must stay stronger than the `interactionHover` overlay. +- Walked: result-grid row selection, focused cell ring, copy-pulse (now slate). Query-picker first-visit pulse rebound off `gridFocus` so it stays branded. +- Why: Selection wash and cell ring were still pink after hover went neutral. +- Out of scope: `editorSelection`; notebook cell focus (`CellWrapper`); checkboxes/switches. +- For Emre: `gridFocus` is the grid's keyboard/copy ring, not brand. Light matches `borderStrong`. Dark is a step lighter than `borderStrong` on purpose. + +### 2026-09-01 — Search, keyboard focus, column names, resize +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `borderStrong`, `borderDefault`, `interactionHover`, `interactionNeutral`, `contentPrimary`, `contentSecondary` (values unchanged) +- Binding: + - Editor Search results: hover `interactionHover`; focused row `interactionNeutral` + `borderDefault` (same as schema tree). File icons `contentSecondary`. Case/word/regex toggles `$activeTone="neutral"` + `borderDefault`. + - Global keyboard focus outlines: `contentAccent` → `borderStrong` (`*:focus-visible`, `Button`, ghost buttons, chrome-tab outline, DocSearch button, warning link, instance-settings slider, chat-history item, assistant-modes compact, segmented-control inset ring). Tab *rename* field and other leftover field-focus borders follow (`_editor`, slim-select, quick-vis). Split sash hover fill → `borderStrong` (same job as column resize). + - Result-grid column names: `contentObject` → `contentPrimary` (`HeaderName` and legacy `.qg-header-name`). + - Column-resize ghost: `contentAccent` → `borderStrong` (matches frozen-handle hover bar). + - Revert: Assistant Settings provider tab underline back to `TabButton` `contentAccent`. +- Neighbors: `TabButton` underline stays `contentAccent`. Notebook cell focus (`CellWrapper`) and cell-name field stay branded. Notebook title glyph and chrome-tab notebook/metrics favicons stay `contentObject`. Switch/Checkbox outlines stay branded. +- Walked: Search sidebar, result grid headers/selection/resize, tab underlines (Monitoring/Details, AI setup), notebook cell ring, keyboard tab through chrome. +- Why: Second binding pass. Tabs and notebook identity stay brand; search, focus rings, grid type, and resize are chrome. +- Out of scope: checkboxes/switches; rail `Navigation`; schema tree glyphs; `SelectMenu`; AI sparkle/MCP/pairing. +- For Emre: Keyboard focus is `borderStrong`. List hover/focus matches the schema tree. Column names are type (`contentPrimary`), not object glyphs. + +### 2026-09-01 — Chat history fields and dropdown open state +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `borderStrong` (values unchanged) +- Binding: + - Chat history search: drop `$tone="accent"` so it inherits default field focus (`borderStrong`). Composer stays `$tone="accent"`. + - Chat history rename: `actionPrimary` → `borderStrong`. + - `SelectMenu` open trigger: `borderAccent` → `borderStrong` (chart X-axis, AI model picker, and every other SelectMenu). Checkmark stays `contentAccent`. +- Neighbors: same `borderStrong` as Input focus and keyboard outlines. Login fields still `$tone="accent"`. +- Walked: chat history search + rename, AI model dropdown, chart X-axis dropdown. +- Why: These were still opted into brand after default field focus went neutral. +- Out of scope: SelectMenu check; AI Settings label; sparkle glyphs; composer; login; checkboxes/switches. +- For Emre: Open dropdown chrome is field focus, not brand. Check remains a selected-item glyph. + +--- + +## Parking lot — not logged as decisions yet + +Use this for hunches until they become an entry. + +- Brand core (`contentAccent`, `actionPrimary`, `contentObject` in light) may be overused. 2026-09-01 neutralized fields, lists, search, keyboard focus, dropdown open state, grid hover/selection/resize, and column names. Still branded: rail icons, tree glyphs, `SelectMenu` check, tab underlines, notebook cell focus + title glyphs, AI composer/login `$tone="accent"`, checkboxes/switches. Treat leftovers as **binding**. +- `interactionNeutral` / `controlTrack` leftover `#d9dce2` — addressed 2026-09-01 (table/chart chips). +- New surface roles: only if two neighbors with *different jobs* are forced to share a token. Propose the meaning first (`surfaceSomething`), then values in both themes. diff --git a/src/components/AIStatusIndicator/AssistantModesCompact.tsx b/src/components/AIStatusIndicator/AssistantModesCompact.tsx index c534c4557..37c3134f6 100644 --- a/src/components/AIStatusIndicator/AssistantModesCompact.tsx +++ b/src/components/AIStatusIndicator/AssistantModesCompact.tsx @@ -121,7 +121,7 @@ const ModeHeaderTop = styled.div<{ } &:focus-visible { - outline: 1px solid ${color("contentAccent")}; + outline: 1px solid ${color("borderStrong")}; outline-offset: 2px; } diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 6ac332a95..0eefb05d1 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -185,7 +185,7 @@ export const ButtonBase = styled.button.attrs<{ filter 120ms ease; &&:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.contentAccent}; + outline: 1px solid ${({ theme }) => theme.color.borderStrong}; outline-offset: 2px; } diff --git a/src/components/Drawer/content-wrapper.tsx b/src/components/Drawer/content-wrapper.tsx index 25a87e525..8332e654c 100644 --- a/src/components/Drawer/content-wrapper.tsx +++ b/src/components/Drawer/content-wrapper.tsx @@ -14,7 +14,7 @@ export const ContentWrapper = styled(Box).attrs({ flex: 1; min-height: 0; overflow: auto; - background: ${({ theme }) => theme.color.surfaceBase}; + background: ${({ theme }) => theme.color.surfaceRaised}; form { width: 100%; diff --git a/src/components/Drawer/index.tsx b/src/components/Drawer/index.tsx index c443eec35..95929f81d 100644 --- a/src/components/Drawer/index.tsx +++ b/src/components/Drawer/index.tsx @@ -59,9 +59,9 @@ const DrawerContent = styled(RadixDialog.Content).attrs({ forceMount: true })<{ }>` display: flex; flex-direction: column; - background-color: ${({ theme }) => theme.color.surfaceBase}; + background-color: ${({ theme }) => theme.color.surfaceRaised}; border-left: 1px solid ${({ theme }) => theme.color.borderSubtle}; - box-shadow: -18px 0 52px ${({ theme }) => theme.color.shadowSoft}; + box-shadow: -4px 0 16px ${({ theme }) => theme.color.shadowSoft}; position: ${({ mode }) => (mode === "modal" ? "fixed" : "inherit")}; top: 0; right: 0; diff --git a/src/components/Form/FormInput/index.tsx b/src/components/Form/FormInput/index.tsx index 14cb2f272..0cc04e6f4 100644 --- a/src/components/Form/FormInput/index.tsx +++ b/src/components/Form/FormInput/index.tsx @@ -12,6 +12,7 @@ export type FormInputProps = React.InputHTMLAttributes & { showPassword?: boolean autoFocus?: boolean autoComplete?: string + tone?: "neutral" | "accent" } const Wrapper = styled.div<{ @@ -69,6 +70,7 @@ export const FormInput = ({ showPassword, autoFocus, autoComplete, + tone, ...rest }: FormInputProps) => { const { formState, getFieldState, register, setFocus } = useFormContext() @@ -99,6 +101,7 @@ export const FormInput = ({ disabled={disabled} showPassword={showPassword} autoComplete={autoComplete} + $tone={tone} {...rest} id={name} aria-invalid={hasError || undefined} diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index a312d0624..8c351d726 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -3,8 +3,11 @@ import React from "react" export type InputVariant = "transparent" | "error" +export type InputTone = "neutral" | "accent" + export type InputStyleProps = { variant?: InputVariant + $tone?: InputTone } type InputProps = React.InputHTMLAttributes & InputStyleProps @@ -42,7 +45,10 @@ export const inputStyles = css` &:focus-visible { outline: none; box-shadow: none; - border-color: ${({ theme }) => theme.color.contentAccent}; + border-color: ${({ theme, $tone }) => + $tone === "accent" + ? theme.color.contentAccent + : theme.color.borderStrong}; background: ${({ theme }) => theme.color.surfaceInput}; } diff --git a/src/components/ResultGrid/styles.ts b/src/components/ResultGrid/styles.ts index 68b4e89cf..b5d718d15 100644 --- a/src/components/ResultGrid/styles.ts +++ b/src/components/ResultGrid/styles.ts @@ -80,7 +80,7 @@ export const HeaderNameRow = styled.div<{ $align: string }>` ` export const HeaderName = styled.span` - color: ${color("contentObject")}; + color: ${color("contentPrimary")}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -138,7 +138,7 @@ export const ColResizer = styled.div` } &:hover::after { - background: ${color("contentAccent")}; + background: ${color("borderStrong")}; } ` @@ -157,7 +157,7 @@ export const ResizeGhost = styled.div` top: 0; bottom: 0; width: 2px; - background: ${color("contentAccent")}; + background: ${color("borderStrong")}; pointer-events: none; /* Above the resizer overlay (z-index 6) so the drag line isn't clipped. */ z-index: 7; @@ -180,16 +180,16 @@ export const Row = styled.div<{ $active: boolean }>` &:hover { background: linear-gradient( - ${theme.color.interactionAccentHover}, - ${theme.color.interactionAccentHover} + ${theme.color.interactionHover}, + ${theme.color.interactionHover} ), ${theme.color.surfaceInset}; [data-frozen="true"] { background: linear-gradient( - ${theme.color.interactionAccentHover}, - ${theme.color.interactionAccentHover} + ${theme.color.interactionHover}, + ${theme.color.interactionHover} ), ${theme.color.surfaceInset}; } @@ -350,7 +350,7 @@ export const FreezeHandle = styled.div<{ !$dragging && css` &:hover::after { - background: ${color("contentAccent")}; + background: ${color("borderStrong")}; } `} ` diff --git a/src/components/SegmentedControl/index.tsx b/src/components/SegmentedControl/index.tsx index 350d266d2..e7fc25ae3 100644 --- a/src/components/SegmentedControl/index.tsx +++ b/src/components/SegmentedControl/index.tsx @@ -56,7 +56,7 @@ const SegmentedControlRoot = styled.div` && > button[aria-pressed="true"]:focus-visible { outline: none; outline-offset: 0; - box-shadow: inset 0 0 0 2px ${({ theme }) => theme.color.contentAccent}; + box-shadow: inset 0 0 0 2px ${({ theme }) => theme.color.borderStrong}; } ` @@ -82,7 +82,10 @@ const GlassSelection = styled.div` border-bottom-width: 2px; border-bottom-color: ${({ theme }) => theme.color.glassEdge}; border-radius: 0.4rem; - box-shadow: 0 3px 9px ${({ theme }) => theme.color.shadowSoft}; + box-shadow: ${({ theme }) => + theme.mode === "light" + ? `0 1px 1px ${theme.color.shadowSubtle}, 0 1px 3px ${theme.color.shadowSoft}` + : `0 3px 9px ${theme.color.shadowSoft}`}; backdrop-filter: blur(6px) saturate(145%); -webkit-backdrop-filter: blur(5px) saturate(150%); transition: opacity 100ms ease; diff --git a/src/components/SelectMenu/index.tsx b/src/components/SelectMenu/index.tsx index a31d67003..72cc7d4eb 100644 --- a/src/components/SelectMenu/index.tsx +++ b/src/components/SelectMenu/index.tsx @@ -41,7 +41,7 @@ const TriggerRoot = styled(Button).attrs({ variant: "secondary" })<{ text-align: left; &[aria-expanded="true"] { - border-color: ${({ theme }) => theme.color.borderAccent}; + border-color: ${({ theme }) => theme.color.borderStrong}; } &[aria-expanded="true"] ${TriggerCaret} { diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 7eb14d05c..85d3b3bbe 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -13,7 +13,7 @@ export const Sidebar = styled(Box).attrs({ flexDirection: "column" })<{ width: ${SIDEBAR_WIDTH}; height: 100%; background: ${({ theme }) => theme.color.surfaceBase}; - border-right: 1px solid ${({ theme }) => theme.color.borderSubtle}; + border-right: 1px solid ${({ theme }) => theme.color.borderDefault}; gap: 0.8rem; flex-shrink: 0; justify-content: ${({ align }) => @@ -37,6 +37,6 @@ export const Sidebar = styled(Box).attrs({ flexDirection: "column" })<{ &:last-of-type { border-right: 0; - border-left: 1px solid ${({ theme }) => theme.color.borderSubtle}; + border-left: 1px solid ${({ theme }) => theme.color.borderDefault}; } ` diff --git a/src/components/TableSelector/index.tsx b/src/components/TableSelector/index.tsx index 21a4115f9..0e8bd1917 100644 --- a/src/components/TableSelector/index.tsx +++ b/src/components/TableSelector/index.tsx @@ -54,8 +54,8 @@ const TriggerContainer = styled.div` &:hover, &:focus-within { - border-color: ${({ theme }) => theme.color.borderAccent}; - background: ${({ theme }) => theme.color.interactionAccentHover}; + border-color: ${({ theme }) => theme.color.borderDefault}; + background: ${({ theme }) => theme.color.interactionHover}; } ` @@ -110,18 +110,18 @@ const Item = styled.div<{ $active: boolean; $disabled?: boolean }>` color: ${({ theme, $disabled }) => $disabled ? theme.color.contentDisabled : theme.color.contentPrimary}; background: ${({ $active, theme }) => - $active ? theme.color.interactionAccentActive : "transparent"}; + $active ? theme.color.interactionNeutral : "transparent"}; border: 1px solid ${({ $active, theme }) => - $active ? theme.color.borderAccent : "transparent"}; + $active ? theme.color.borderDefault : "transparent"}; &:hover { background: ${({ $disabled, $active, theme }) => $disabled ? "transparent" : $active - ? theme.color.interactionAccentActive - : theme.color.interactionAccentHover}; + ? theme.color.interactionNeutral + : theme.color.interactionHover}; } .highlight { diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx index b62543cd9..9eee415fe 100644 --- a/src/components/Tooltip/index.tsx +++ b/src/components/Tooltip/index.tsx @@ -110,7 +110,7 @@ const ArrowWithBorder = React.forwardRef((props, ref) => { @@ -130,7 +130,7 @@ const TooltipContent = styled(RadixTooltip.Content)<{ max-width: ${({ $maxWidth }) => $maxWidth ?? "460px"}; padding: 1rem; background: ${color("surfaceInset")}; - border: 1px solid ${color("contentDisabled")}; + border: 1px solid ${color("borderDefault")}; border-radius: 6px; z-index: ${TOOLTIP_Z_INDEX}; animation-duration: 200ms; diff --git a/src/components/TopBar/InstanceSettingsPopper.tsx b/src/components/TopBar/InstanceSettingsPopper.tsx index bcf2a5c1f..7ee637d9a 100644 --- a/src/components/TopBar/InstanceSettingsPopper.tsx +++ b/src/components/TopBar/InstanceSettingsPopper.tsx @@ -154,7 +154,7 @@ const ColorSlider = styled.input.attrs({ type: "range", min: 0, max: 255 })` } &:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.contentAccent}; + outline: 1px solid ${({ theme }) => theme.color.borderStrong}; outline-offset: 2px; } diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx index 264165480..1c6ee493d 100644 --- a/src/components/TopBar/index.tsx +++ b/src/components/TopBar/index.tsx @@ -15,7 +15,7 @@ const Root = styled(Box).attrs({ min-height: ${TOP_BAR_HEIGHT}; gap: 0; background: ${({ theme }) => theme.color.surfaceBase}; - border-bottom: 1px solid ${({ theme }) => theme.color.borderSubtle}; + border-bottom: 1px solid ${({ theme }) => theme.color.borderDefault}; box-shadow: 0 6px 18px ${({ theme }) => theme.color.shadowSubtle}; z-index: 30; ` diff --git a/src/modules/OAuth2/views/login.tsx b/src/modules/OAuth2/views/login.tsx index 8aa013cc2..49940ac3a 100644 --- a/src/modules/OAuth2/views/login.tsx +++ b/src/modules/OAuth2/views/login.tsx @@ -443,13 +443,18 @@ export const Login = ({ > - + {errorMessage && ( diff --git a/src/scenes/Console/index.tsx b/src/scenes/Console/index.tsx index 8977b8db7..f0c41f556 100644 --- a/src/scenes/Console/index.tsx +++ b/src/scenes/Console/index.tsx @@ -78,13 +78,14 @@ const SidebarSpacer = styled.div` ` const SidePanelRight = styled.div` - background: ${({ theme }) => theme.color.surfaceBase}; + background: ${({ theme }) => theme.color.surfaceRaised}; height: 100%; border-left: 1px solid ${({ theme }) => theme.color.borderSubtle}; ` const Wrapper = styled.div` height: 100%; + background: ${({ theme }) => theme.color.surfaceRaised}; border-right: 1px solid ${({ theme }) => theme.color.borderSubtle}; ` diff --git a/src/scenes/Editor/AIChatWindow/AIChatErrorBoundary.tsx b/src/scenes/Editor/AIChatWindow/AIChatErrorBoundary.tsx index 79f6dcab9..b2989bbcb 100644 --- a/src/scenes/Editor/AIChatWindow/AIChatErrorBoundary.tsx +++ b/src/scenes/Editor/AIChatWindow/AIChatErrorBoundary.tsx @@ -23,7 +23,7 @@ const Wrapper = styled.div` width: 100%; height: 100%; padding: 2rem; - background: ${({ theme }) => theme.color.surfaceBase}; + background: ${({ theme }) => theme.color.surfaceRaised}; border-left: 0.2rem ${color("surfaceInset")} solid; ` diff --git a/src/scenes/Editor/AIChatWindow/AIChatWindowLazy.tsx b/src/scenes/Editor/AIChatWindow/AIChatWindowLazy.tsx index db07d5a2e..bdaf54bf5 100644 --- a/src/scenes/Editor/AIChatWindow/AIChatWindowLazy.tsx +++ b/src/scenes/Editor/AIChatWindow/AIChatWindowLazy.tsx @@ -11,7 +11,7 @@ const AIChatWindow = lazy(() => import("./index")) const LoaderContainer = styled.div` display: flex; align-items: center; - background: ${({ theme }) => theme.color.surfaceBase}; + background: ${({ theme }) => theme.color.surfaceRaised}; justify-content: center; height: 100%; width: 100%; diff --git a/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx b/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx index 358c5cce9..510b50fc5 100644 --- a/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx +++ b/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx @@ -25,8 +25,12 @@ const Container = styled.div<{ $disabled?: boolean }>` text-align: left; &:hover { - background: ${({ $disabled }) => - $disabled ? "transparent" : color("surfaceRaised")}; + background: ${({ $disabled, theme }) => + $disabled + ? "transparent" + : theme.mode === "light" + ? theme.color.surfaceBase + : theme.color.surfaceInput}; .chat-title { color: ${({ $disabled }) => @@ -35,7 +39,7 @@ const Container = styled.div<{ $disabled?: boolean }>` } &:focus-visible { - outline: 1px solid ${color("contentAccent")}; + outline: 1px solid ${color("borderStrong")}; outline-offset: 2px; } ` @@ -69,7 +73,7 @@ const Title = styled.div.attrs({ className: "chat-title" })` const TitleInput = styled.input` color: ${color("contentPrimary")}; background: transparent; - border: 1px solid ${color("actionPrimary")}; + border: 1px solid ${color("borderStrong")}; border-radius: 6px; outline: none; padding: 0.2rem 0.4rem; diff --git a/src/scenes/Editor/AIChatWindow/ChatHistoryView.tsx b/src/scenes/Editor/AIChatWindow/ChatHistoryView.tsx index ae7c2a2a3..67bc604cf 100644 --- a/src/scenes/Editor/AIChatWindow/ChatHistoryView.tsx +++ b/src/scenes/Editor/AIChatWindow/ChatHistoryView.tsx @@ -29,7 +29,7 @@ const Container = styled.div` height: 100%; width: 100%; padding: 2rem 1rem 4rem 1rem; - background: ${({ theme }) => theme.color.surfaceBase}; + background: ${({ theme }) => theme.color.surfaceRaised}; overflow: hidden; ` diff --git a/src/scenes/Editor/AIChatWindow/ChatInput.tsx b/src/scenes/Editor/AIChatWindow/ChatInput.tsx index 06b1ac03f..7ff451a4f 100644 --- a/src/scenes/Editor/AIChatWindow/ChatInput.tsx +++ b/src/scenes/Editor/AIChatWindow/ChatInput.tsx @@ -43,7 +43,9 @@ const InputWrapper = styled(Box)` overflow: hidden; ` -const StyledTextArea = styled(TextArea)<{ $hasContext: boolean }>` +const StyledTextArea = styled(TextArea).attrs({ $tone: "accent" })<{ + $hasContext: boolean +}>` flex: 1; min-height: 8rem; max-height: 30rem; diff --git a/src/scenes/Editor/AIChatWindow/ChatMessages.tsx b/src/scenes/Editor/AIChatWindow/ChatMessages.tsx index 42e6a9d99..76090730f 100644 --- a/src/scenes/Editor/AIChatWindow/ChatMessages.tsx +++ b/src/scenes/Editor/AIChatWindow/ChatMessages.tsx @@ -96,7 +96,10 @@ const UserRequestBox = styled(Box)` padding: 0.8rem; width: 100%; align-self: flex-end; - background: ${color("authBackdrop")}; + background: ${({ theme }) => + theme.mode === "light" + ? theme.color.surfaceValue + : theme.color.authBackdrop}; border: 1px solid ${color("borderDefault")}; border-radius: 0.6rem; flex-shrink: 0; diff --git a/src/scenes/Editor/AIChatWindow/index.tsx b/src/scenes/Editor/AIChatWindow/index.tsx index 4999bf773..07c4cc776 100644 --- a/src/scenes/Editor/AIChatWindow/index.tsx +++ b/src/scenes/Editor/AIChatWindow/index.tsx @@ -107,7 +107,7 @@ const ChatWindowContent = styled.div` height: 100%; width: 100%; overflow: hidden; - background: ${({ theme }) => theme.color.surfaceBase}; + background: ${({ theme }) => theme.color.surfaceRaised}; ` const InitialQueryContainer = styled.div` @@ -182,7 +182,7 @@ const ChatPanel = styled(Box)` height: 100%; width: 100%; gap: 0; - background: ${({ theme }) => theme.color.surfaceBase}; + background: ${({ theme }) => theme.color.surfaceRaised}; ` const AIChatWindow: React.FC = () => { diff --git a/src/scenes/Editor/Menu/index.tsx b/src/scenes/Editor/Menu/index.tsx index fc2414fc2..dad3c61df 100644 --- a/src/scenes/Editor/Menu/index.tsx +++ b/src/scenes/Editor/Menu/index.tsx @@ -100,7 +100,7 @@ const QueryPickerButton = styled(Button)<{ $firstTimeVisitor: boolean }>` ${({ $firstTimeVisitor, theme }) => $firstTimeVisitor ? css` - animation: ${queryPickerPulse(theme.color.gridFocus)} 2s infinite; + animation: ${queryPickerPulse(theme.color.contentAccent)} 2s infinite; ` : css` animation: none; diff --git a/src/scenes/Editor/Monaco/tabs.tsx b/src/scenes/Editor/Monaco/tabs.tsx index ed52ea915..5179482d0 100644 --- a/src/scenes/Editor/Monaco/tabs.tsx +++ b/src/scenes/Editor/Monaco/tabs.tsx @@ -59,8 +59,11 @@ const Root = styled(Box).attrs({ z-index: 12; isolation: isolate; background: transparent; - border-bottom: 0; - box-shadow: 0 8px 20px ${({ theme }) => theme.color.shadowSoft}; + border-bottom: 1px solid ${({ theme }) => theme.color.borderSubtle}; + box-shadow: ${({ theme }) => + theme.mode === "light" + ? `0 1px 2px ${theme.color.shadowSubtle}, 0 2px 6px ${theme.color.shadowSoft}` + : `0 8px 20px ${theme.color.shadowSoft}`}; padding-right: 1rem; gap: 0.4rem; diff --git a/src/scenes/Editor/Notebook/NotebookToolbar.tsx b/src/scenes/Editor/Notebook/NotebookToolbar.tsx index cafff4ef7..1ca9cfb1b 100644 --- a/src/scenes/Editor/Notebook/NotebookToolbar.tsx +++ b/src/scenes/Editor/Notebook/NotebookToolbar.tsx @@ -44,7 +44,10 @@ const Toolbar = styled(Box).attrs({ padding: 1rem 2rem; background: ${color("surfaceBase")}; border-bottom: 1px solid ${({ theme }) => theme.color.borderSubtle}; - box-shadow: 0 12px 24px ${({ theme }) => theme.color.shadowSoft}; + box-shadow: ${({ theme }) => + theme.mode === "light" + ? `0 1px 2px ${theme.color.shadowSubtle}, 0 2px 6px ${theme.color.shadowSoft}` + : `0 12px 24px ${theme.color.shadowSoft}`}; overflow: hidden; flex-shrink: 0; position: relative; diff --git a/src/scenes/Editor/Notebook/cells/CellRunDrawToggles.tsx b/src/scenes/Editor/Notebook/cells/CellRunDrawToggles.tsx index 39274b575..a46841d69 100644 --- a/src/scenes/Editor/Notebook/cells/CellRunDrawToggles.tsx +++ b/src/scenes/Editor/Notebook/cells/CellRunDrawToggles.tsx @@ -18,7 +18,10 @@ const ToggleButton = styled(NotebookViewToggleSegment)` box-shadow: inset 0 0 0 1px ${({ theme }) => theme.color.glassBorder}, inset 0 -2px 0 ${({ theme }) => theme.color.glassEdge}, - 0 3px 9px ${({ theme }) => theme.color.shadowSoft}; + ${({ theme }) => + theme.mode === "light" + ? `0 1px 1px ${theme.color.shadowSubtle}, 0 1px 3px ${theme.color.shadowSoft}` + : `0 3px 9px ${theme.color.shadowSoft}`}; backdrop-filter: blur(2px) saturate(150%); -webkit-backdrop-filter: blur(2px) saturate(150%); } diff --git a/src/scenes/Editor/Notebook/cells/CellToolbar.tsx b/src/scenes/Editor/Notebook/cells/CellToolbar.tsx index 73041b2bf..b8e87aea5 100644 --- a/src/scenes/Editor/Notebook/cells/CellToolbar.tsx +++ b/src/scenes/Editor/Notebook/cells/CellToolbar.tsx @@ -65,6 +65,15 @@ const ToolbarWrapper = styled.div<{ `} ` +const CellIconButton = styled(IconButton)` + &&:hover:not(:disabled):not([aria-disabled="true"]) { + background: ${({ theme }) => + theme.mode === "light" + ? theme.color.surfaceBase + : theme.color.surfaceRaised}; + } +` + type Props = { cellId: string cell: NotebookCell @@ -277,7 +286,7 @@ export const CellToolbar: React.FC = ({ $forceVisible={menuOpen} > - = ({ ) : ( )} - + {!isMaximized && ( = ({ > - + - + diff --git a/src/scenes/Editor/Notebook/cells/CellViewToggle.tsx b/src/scenes/Editor/Notebook/cells/CellViewToggle.tsx index f1ca2eabe..00c7993ac 100644 --- a/src/scenes/Editor/Notebook/cells/CellViewToggle.tsx +++ b/src/scenes/Editor/Notebook/cells/CellViewToggle.tsx @@ -39,6 +39,13 @@ const ViewIconButton = styled(IconButton)` width: 1.8rem; height: 1.8rem; } + + &&:hover:not(:disabled):not([aria-disabled="true"]) { + background: ${({ theme }) => + theme.mode === "light" + ? theme.color.interactionNeutralHover + : theme.color.surfaceRaised}; + } ` const Divider = styled.div` diff --git a/src/scenes/Editor/Notebook/cells/CellWrapper.tsx b/src/scenes/Editor/Notebook/cells/CellWrapper.tsx index 874529a2b..7ad6dc81b 100644 --- a/src/scenes/Editor/Notebook/cells/CellWrapper.tsx +++ b/src/scenes/Editor/Notebook/cells/CellWrapper.tsx @@ -23,7 +23,8 @@ export const CellWrapper = styled.div.attrs({ border-color: ${color("contentAccent")}; box-shadow: 0 0 0 3px ${color("interactionAccentActive")}, - 0 18px 50px ${color("shadowMedium")}; + 0 1px 2px ${color("shadowSubtle")}, + 0 4px 10px ${color("shadowSoft")}; `} ${({ $focused }) => @@ -31,7 +32,9 @@ export const CellWrapper = styled.div.attrs({ css` &:hover { border-color: ${color("borderDefault")}; - box-shadow: 0 16px 44px ${color("shadowSoft")}; + box-shadow: + 0 1px 2px ${color("shadowSubtle")}, + 0 3px 8px ${color("shadowSoft")}; } `} diff --git a/src/scenes/Layout/warning.tsx b/src/scenes/Layout/warning.tsx index 69f53e90e..9cbde94a3 100644 --- a/src/scenes/Layout/warning.tsx +++ b/src/scenes/Layout/warning.tsx @@ -65,7 +65,7 @@ const WorkaroundLink = styled.a` } &:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.contentAccent}; + outline: 1px solid ${({ theme }) => theme.color.borderStrong}; outline-offset: 2px; border-radius: 0.2rem; } diff --git a/src/scenes/Schema/Row/index.tsx b/src/scenes/Schema/Row/index.tsx index d9d631a09..08a130e2d 100644 --- a/src/scenes/Schema/Row/index.tsx +++ b/src/scenes/Schema/Row/index.tsx @@ -138,7 +138,7 @@ const Wrapper = styled.div<{ `} &:hover { - background: ${({ theme }) => theme.color.interactionAccentHover}; + background: ${({ theme }) => theme.color.interactionHover}; .table-menu-button { opacity: 1; } @@ -148,14 +148,14 @@ const Wrapper = styled.div<{ $focused && ` outline: none; - background: ${theme.color.interactionAccentActive}; - box-shadow: inset 0 0 0 1px ${theme.color.borderAccent}; + background: ${theme.color.interactionNeutral}; + box-shadow: inset 0 0 0 1px ${theme.color.borderDefault}; .table-menu-button { opacity: 1; } &:hover { - background: ${theme.color.interactionAccentActive}; + background: ${theme.color.interactionNeutral}; } `} @@ -676,7 +676,7 @@ const Row = ({ onClick={onOpenDetailsDrawer} onDoubleClick={(e) => e.stopPropagation()} > - + )} diff --git a/src/scenes/Schema/VirtualTables/index.tsx b/src/scenes/Schema/VirtualTables/index.tsx index 1931d6672..22c416ca6 100644 --- a/src/scenes/Schema/VirtualTables/index.tsx +++ b/src/scenes/Schema/VirtualTables/index.tsx @@ -139,8 +139,8 @@ const TableRow = styled(Row)<{ $contextMenuOpen: boolean }>` ${({ $contextMenuOpen, theme }) => $contextMenuOpen && ` - background: ${theme.color.interactionAccentActive}; - box-shadow: inset 0 0 0 1px ${theme.color.borderAccent}; + background: ${theme.color.interactionNeutral}; + box-shadow: inset 0 0 0 1px ${theme.color.borderDefault}; `} ` diff --git a/src/scenes/Schema/index.tsx b/src/scenes/Schema/index.tsx index 391505b42..f300e66b6 100644 --- a/src/scenes/Schema/index.tsx +++ b/src/scenes/Schema/index.tsx @@ -93,6 +93,9 @@ const Wrapper = styled(PaneWrapper)<{ }>` overflow-x: auto; height: 100%; + && { + background: ${({ theme }) => theme.color.surfaceRaised}; + } ${({ open }) => !open && css` @@ -106,6 +109,9 @@ const Content = styled(PaneContent)<{ display: flex; flex-direction: column; overflow: auto; + && { + background: ${({ theme }) => theme.color.surfaceRaised}; + } ${({ _loading }) => _loading && loadingStyles}; ` diff --git a/src/scenes/Search/SearchPanel.tsx b/src/scenes/Search/SearchPanel.tsx index 7cc70386b..878046cd3 100644 --- a/src/scenes/Search/SearchPanel.tsx +++ b/src/scenes/Search/SearchPanel.tsx @@ -49,6 +49,9 @@ const Wrapper = styled(PaneWrapper)<{ }>` overflow-x: auto; height: 100%; + && { + background: ${({ theme }) => theme.color.surfaceRaised}; + } ${({ $open }) => !$open && css` @@ -60,6 +63,9 @@ const Content = styled(PaneContent)` display: flex; flex-direction: column; overflow: auto; + && { + background: ${({ theme }) => theme.color.surfaceRaised}; + } ` const SearchInputContainer = styled.div` @@ -96,14 +102,16 @@ const ToggleButtonsContainer = styled.div` gap: 0.2rem; ` -const ToggleButton = styled(SegmentedControlButton)` +const ToggleButton = styled(SegmentedControlButton).attrs({ + $activeTone: "neutral" as const, +})` && { width: 2rem; border: 1px solid transparent; } &&[aria-pressed="true"] { - border-color: ${({ theme }) => theme.color.borderAccentStrong}; + border-color: ${({ theme }) => theme.color.borderDefault}; } ` diff --git a/src/scenes/Search/SearchResults.tsx b/src/scenes/Search/SearchResults.tsx index 09b749c92..345f0316e 100644 --- a/src/scenes/Search/SearchResults.tsx +++ b/src/scenes/Search/SearchResults.tsx @@ -44,15 +44,15 @@ const ItemWrapper = styled.div<{ min-height: 3.2rem; &:hover { - background: ${({ theme }) => theme.color.interactionAccentHover}; + background: ${({ theme }) => theme.color.interactionHover}; } ${({ $focused, theme }) => $focused && ` outline: none; - background: ${theme.color.interactionAccentActive}; - border: 1px solid ${theme.color.borderAccent}; + background: ${theme.color.interactionNeutral}; + box-shadow: inset 0 0 0 1px ${theme.color.borderDefault}; `} ` @@ -80,7 +80,7 @@ const FileIcon = styled.div` svg { width: 1.4rem; height: 1.4rem; - color: ${({ theme }) => theme.color.contentAccent}; + color: ${({ theme }) => theme.color.contentSecondary}; } ` diff --git a/src/scenes/SideMenu/index.tsx b/src/scenes/SideMenu/index.tsx index d4f7472ce..bcf84c87c 100644 --- a/src/scenes/SideMenu/index.tsx +++ b/src/scenes/SideMenu/index.tsx @@ -58,7 +58,7 @@ const Wrapper = styled.div` width: ${RESPONSIVE_WIDTH}; min-height: 0; overflow: hidden; - background: ${({ theme }) => theme.color.surfaceBase}; + background: ${({ theme }) => theme.color.surfaceRaised}; border-left: 1px solid ${({ theme }) => theme.color.borderSubtle}; z-index: 25; diff --git a/src/styles/_editor.scss b/src/styles/_editor.scss index 89eee5722..42d973421 100644 --- a/src/styles/_editor.scss +++ b/src/styles/_editor.scss @@ -171,7 +171,7 @@ color: theme-color(contentPrimary); &:focus { - border-color: theme-color(contentAccent); + border-color: theme-color(borderStrong); box-shadow: none; } diff --git a/src/styles/_grid.scss b/src/styles/_grid.scss index 35714c0ec..1ff23d74b 100644 --- a/src/styles/_grid.scss +++ b/src/styles/_grid.scss @@ -221,7 +221,7 @@ $drag-handle-margin: 2px; } .qg-header-name { - color: theme-color(contentObject) !important; + color: theme-color(contentPrimary) !important; font-size: medium; } diff --git a/src/styles/_quick-vis.scss b/src/styles/_quick-vis.scss index c444b7696..66d82dde9 100644 --- a/src/styles/_quick-vis.scss +++ b/src/styles/_quick-vis.scss @@ -247,7 +247,7 @@ &:focus, &:focus-visible { background: theme-color(surfaceInput); - border-color: theme-color(contentAccent); + border-color: theme-color(borderStrong); box-shadow: none; outline: none; } diff --git a/src/styles/lib/_react-chrome-tabs.scss b/src/styles/lib/_react-chrome-tabs.scss index 362c5bd80..34e253a13 100644 --- a/src/styles/lib/_react-chrome-tabs.scss +++ b/src/styles/lib/_react-chrome-tabs.scss @@ -56,7 +56,7 @@ filter 150ms ease; &:focus-visible { - outline: 1px solid theme-color(contentAccent); + outline: 1px solid theme-color(borderStrong); outline-offset: 2px; } @@ -178,6 +178,26 @@ background: linear-gradient(to left, theme-color(shadowMedium), transparent); } +html[data-theme="light"] .chrome-tabs .chrome-tabs-overflow-shadow { + width: 6px; +} + +html[data-theme="light"] .chrome-tabs .chrome-tabs-overflow-shadow-left { + background: linear-gradient( + to right, + theme-color(shadowMedium) 0%, + theme-color(transparent) 65% + ); +} + +html[data-theme="light"] .chrome-tabs .chrome-tabs-overflow-shadow-right { + background: linear-gradient( + to left, + theme-color(shadowMedium) 0%, + theme-color(transparent) 65% + ); +} + .chrome-tabs[data-overflow-left="true"] .chrome-tabs-overflow-shadow-left { opacity: 1; } @@ -212,7 +232,7 @@ display: none; position: relative; z-index: 1; - border: 1px solid theme-color(contentAccent); + border: 1px solid theme-color(borderStrong); padding: 0.2rem 0.4rem; line-height: 20px; height: 20px; diff --git a/src/styles/main.scss b/src/styles/main.scss index 276533db8..3a6bc07bf 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -53,7 +53,7 @@ $spacing-s: 4px !default; } .ss-content .ss-search input:focus { - border-color: theme-color(contentAccent); + border-color: theme-color(borderStrong); box-shadow: none; } diff --git a/src/theme/global-styles.ts b/src/theme/global-styles.ts index d313dd502..59788d1e0 100644 --- a/src/theme/global-styles.ts +++ b/src/theme/global-styles.ts @@ -81,7 +81,7 @@ export const GlobalStyle = createGlobalStyle` } button[data-button-variant="ghost"]:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.contentAccent}; + outline: 1px solid ${({ theme }) => theme.color.borderStrong}; outline-offset: 2px; } @@ -94,7 +94,7 @@ export const GlobalStyle = createGlobalStyle` } *:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.contentAccent}; + outline: 1px solid ${({ theme }) => theme.color.borderStrong}; outline-offset: 2px; } @@ -183,6 +183,6 @@ export const GlobalStyle = createGlobalStyle` } .allotment-module_splitView__L-yRc > .allotment-module_sashContainer__fzwJF > .allotment-module_sash__QA-2t:hover::before { - background: ${({ theme }) => theme.color.contentAccent}; + background: ${({ theme }) => theme.color.borderStrong}; } ` diff --git a/src/theme/global-styles/docsearch.ts b/src/theme/global-styles/docsearch.ts index c3f05765f..9f656815f 100644 --- a/src/theme/global-styles/docsearch.ts +++ b/src/theme/global-styles/docsearch.ts @@ -108,7 +108,7 @@ export const DocSearchStyles = css` } .DocSearch.DocSearch-Button:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.contentAccent}; + outline: 1px solid ${({ theme }) => theme.color.borderStrong}; outline-offset: 2px; } diff --git a/src/theme/index.ts b/src/theme/index.ts index bb339be7d..9990ae52b 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -157,8 +157,8 @@ export const darkColors: ColorShape = { // Result grid gridRow: "#17181d", gridHeader: "#202126", - gridSelection: "#2b1d25", - gridFocus: "#b81447", + gridSelection: "#252830", + gridFocus: "#6b7382", // Editor editorCanvas: "#17181d", @@ -229,16 +229,16 @@ export const lightColors: ColorShape = { onboardingAccent: "#176f87", // Surfaces - surfaceCanvas: "#d9dce2", - surfaceStage: "#c9cdd4", - surfaceBase: "#e3e5e9", - surfaceInset: "#eceef1", - surfaceRaised: "#e8eaee", - surfaceInput: "#f2f3f5", - surfaceOverlay: "#f2f3f5", - surfaceValue: "#fbfcfd", - surfaceScrim: "rgba(27, 31, 39, 0.38)", - surfaceTabRail: "rgba(218, 221, 227, 0.94)", + surfaceCanvas: "#e8eaee", + surfaceStage: "#e2e5ea", + surfaceBase: "#eef0f3", + surfaceInset: "#f7f8f9", + surfaceRaised: "#f4f5f7", + surfaceInput: "#fafbfc", + surfaceOverlay: "#fafbfc", + surfaceValue: "#ffffff", + surfaceScrim: "rgba(27, 31, 39, 0.32)", + surfaceTabRail: "rgba(232, 234, 238, 0.94)", // Content contentPrimary: "#1c2029", @@ -258,8 +258,8 @@ export const lightColors: ColorShape = { borderAccentStrong: "rgba(184, 20, 71, 0.56)", // Interaction and controls - interactionNeutral: "#d9dce2", - interactionNeutralHover: "#d2d6dd", + interactionNeutral: "#e8eaee", + interactionNeutralHover: "#e2e5ea", interactionHover: "#1c202913", interactionAccentHover: "rgba(184, 20, 71, 0.07)", interactionAccentActive: "rgba(184, 20, 71, 0.13)", @@ -267,7 +267,7 @@ export const lightColors: ColorShape = { scrollbarThumb: "#62656b", controlSurface: "#f6f7f8", controlSurfaceHover: "#e7e9ed", - controlTrack: "#d9dce2", + controlTrack: "#e8eaee", controlKnob: "#505968", // Actions and status @@ -292,11 +292,11 @@ export const lightColors: ColorShape = { statusFeature: "#6553aa", // Shadows - shadowSubtle: "rgba(27, 31, 39, 0.06)", - shadowSoft: "rgba(27, 31, 39, 0.11)", - shadowMedium: "rgba(27, 31, 39, 0.18)", - shadowStrong: "rgba(27, 31, 39, 0.27)", - shadowOverlay: "rgba(27, 31, 39, 0.38)", + shadowSubtle: "rgba(27, 31, 39, 0.035)", + shadowSoft: "rgba(27, 31, 39, 0.055)", + shadowMedium: "rgba(27, 31, 39, 0.08)", + shadowStrong: "rgba(27, 31, 39, 0.16)", + shadowOverlay: "rgba(27, 31, 39, 0.22)", // Liquid glass glassSurface: "rgba(255, 255, 255, 0.58)", @@ -310,14 +310,14 @@ export const lightColors: ColorShape = { aiGradientEnd: "#76184c", // Result grid - gridRow: "#e9ebef", - gridHeader: "#dce0e6", - gridSelection: "#e0afbf", - gridFocus: "#8a0f35", + gridRow: "#f4f5f7", + gridHeader: "#e7eaee", + gridSelection: "#d8dce3", + gridFocus: "#828b99", // Editor - editorCanvas: "#d9dce2", - editorBorder: "#c9cdd4", + editorCanvas: "#eef0f3", + editorBorder: "#e2e5ea", editorSelection: "#aec8ec", editorSelectionAccent: "#7a99c4", editorActiveLine: "#1c202909", From 0c928ba1c356bda1f7284ad13e95af035cc9d0ff Mon Sep 17 00:00:00 2001 From: Zack Nelson Date: Tue, 1 Sep 2026 23:52:36 +0100 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20continue=20light=20chrome=20pass?= =?UTF-8?q?=20=E2=80=94=20chips,=20form=20select,=20quieter=20wells?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the remaining palette and binding decisions in THEME-STEERING.md and apply them: status chips, Enabled/Validated, form SelectMenu, and the leftover light chrome leftover from the first lift. Co-authored-by: Cursor --- THEME-STEERING.md | 302 +++++++++++++++++- e2e/tests/console/aiAssistant.spec.js | 7 +- src/components/Badge/index.tsx | 72 ++++- src/components/Button/variants.ts | 5 +- src/components/MultiStepModal/index.tsx | 1 + src/components/SelectMenu/index.tsx | 16 +- src/components/SelectableCardButton/index.tsx | 13 +- .../SetupAIAssistant/CustomProviderModal.tsx | 11 +- .../SetupAIAssistant/SettingsModal.tsx | 92 ++++-- src/components/Sidebar/index.tsx | 5 +- .../Editor/AIChatWindow/ChatHistoryItem.tsx | 2 +- .../Editor/AIChatWindow/ChatMessages.tsx | 5 +- src/scenes/Editor/ButtonBar/index.tsx | 25 +- src/scenes/Editor/Monaco/index.tsx | 2 +- src/scenes/Footer/ConnectionStatus/index.tsx | 47 ++- src/scenes/Footer/index.tsx | 5 +- src/scenes/News/index.tsx | 4 +- src/scenes/Notifications/index.tsx | 1 - src/scenes/Result/index.tsx | 6 +- .../Schema/TableDetailsDrawer/index.tsx | 5 +- src/scenes/Schema/index.tsx | 2 +- src/styles/lib/_react-chrome-tabs.scss | 4 + src/theme/global-styles.ts | 19 +- src/theme/index.ts | 6 +- 24 files changed, 558 insertions(+), 99 deletions(-) diff --git a/THEME-STEERING.md b/THEME-STEERING.md index 865b7a601..8a12165c1 100644 --- a/THEME-STEERING.md +++ b/THEME-STEERING.md @@ -197,8 +197,8 @@ change shipped together, split them. - Neighbors: chat panel `surfaceRaised` `#f4f5f7`. Nested SQL uses `editorCanvas` `#eef0f3`, so the query block recedes slightly inside the white card. Login `authBackdrop` is untouched. - Walked: Explain Query card in the AI panel, light mode. Dark glance to confirm the crimson well remains. - Why: Darker-on-dark should invert to lighter-on-light. `authBackdrop` cannot do that job in both themes. -- Out of scope: plain user `MessageBubble` (still `authBackdrop`); login page; palette retune of `authBackdrop`; inner LiteEditor. -- For Emre: AI action cards are not login chrome. Light fill is `surfaceValue`. Consider whether `MessageBubble` should follow. +- Out of scope: login page; palette retune of `authBackdrop`; inner LiteEditor. Plain `MessageBubble` follows later the same day. +- For Emre: AI action cards are not login chrome. Light fill is `surfaceValue`. ### 2026-09-01 — Cell icon hover is one step darker in light - Lift: binding @@ -275,6 +275,304 @@ change shipped together, split them. - Out of scope: SelectMenu check; AI Settings label; sparkle glyphs; composer; login; checkboxes/switches. - For Emre: Open dropdown chrome is field focus, not brand. Check remains a selected-item glyph. +### 2026-09-01 — Results action bar owns the pane-top hairline +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `borderSubtle` (values unchanged) +- Binding: Result `Actions` gains `border-top` `borderSubtle`. Log `Notifications` wrapper drops `border-bottom` so log + action bar do not stack two lines. Action bar `border-bottom` (to the grid) stays. +- Neighbors: same weight as other panel-to-work edges (schema wrapper, drawer, allotment sash). Rails stay `borderDefault`. +- Walked: SQL results with schema open (action bar meets sidebar), log expanded and collapsed, grid and chart. +- Why: Schema lives only in the top pane. The results bar spans full width under it; without a top edge the two `surfaceRaised` panels fused. +- Out of scope: notebook `ResultActionsBar` (inside a cell, not against the schema). +- For Emre: The results pane's top hairline belongs on the action bar, not the log, because the log does not span under the sidebar. + +### 2026-09-01 — Light scrollbar thumb follows the surface ladder +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `scrollbarThumb` +- Before → after (light): `#62656b` → `#c5cad3` +- Neighbors: sits between `borderDefault` (ink `.15`) and `borderStrong` `#828b99`. Dark thumb stays white `.13`. +- Walked: search list, result grid, AI chat, news drawer. +- Why: The old thumb was charcoal on the lifted light surfaces, so every overflow read as a dark rail. +- Out of scope: dark thumb (already white `.13`). Monaco slider rebound in a later entry. +- For Emre: Light scrollbars are chrome, not content. Do not reuse `contentDisabled`. + +### 2026-09-01 — Light rails, results pane, and webkit scrollbars +- Lift: binding +- Status: applied locally +- Modes: light (rails/footer); both (results, news, webkit) +- Tokens: `surfaceRaised`, `scrollbarThumb`, `borderSubtle` (values unchanged except the thumb palette above) +- Binding: + - Icon rails (`Sidebar`) and footer: light `surfaceBase` → `surfaceRaised`. Dark rails stay `surfaceBase`. + - Result pane `Root` + `PaneContent`: `surfaceBase` → `surfaceRaised` (same as log, action bar, schema, AI). + - News list fill: `surfaceBase` → `surfaceRaised`; row rules `surfaceRaised` → `borderSubtle` so they do not vanish. + - Global `::-webkit-scrollbar` now paints `scrollbarThumb` (Firefox already used `scrollbar-color`). +- Neighbors: light chrome frame is one fill (`surfaceRaised` `#f4f5f7`) against work (`surfaceValue` / grid). Stagger is the `borderDefault` rail hairline, not a darker grey. +- Walked: left nav vs results, right nav vs AI, search overflow, grid overflow, footer. +- Why: Rails and the results well were still on `surfaceBase` after content panels moved up, so the grey frame looked unthemed. Chrome/Safari never read `scrollbar-color`. +- Out of scope: TopBar (still `surfaceBase`); editor `PaneWrapper`; checkboxes; `surfaceBase` palette. +- For Emre: Light icon rails share the panel fill. Dark still recedes (`surfaceBase`). Webkit thumbs are a global chrome rule. + +### 2026-09-01 — Run-query menu divider stays a hairline on hover +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `borderSubtle`, `interactionHover`, `surfaceOverlay` (values unchanged) +- Binding: `ButtonBar` dropdown was a one-off (`surfaceInset` + secondary `Button` items). Secondary hover paints `borderStrong`, which recolored the `borderSubtle` item rule in light (`#828b99` on a pale menu). Menu now uses `floatingSurfaceStyles`. Item hover is `interactionHover` and the divider stays `borderSubtle`. +- Neighbors: same recipe as `DropdownMenu` / `SelectMenu`. +- Walked: Run query chevron in light and dark, hover each item. +- Why: The line was not a separate divider component — it was the button hover border winning. +- Out of scope: splitting the items off `Button`; checkboxes. +- For Emre: Menu items that are `Button`s cannot keep the default secondary hover border. + +### 2026-09-01 — Monaco scrollbar uses the shared thumb +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `scrollbarThumb` (values unchanged) +- Binding: Monaco overlay slider `interactionNeutral` → `scrollbarThumb`. +- Neighbors: dark `interactionNeutral` `#32343e` happened to match the quiet white thumb; light `#e8eaee` sat on `editorCanvas` `#eef0f3` and disappeared / mismatched webkit `#c5cad3`. +- Walked: SQL editor overflow, both modes. +- Why: Dark already looked themed. Light was the leftover. +- Out of scope: Monaco theme JSON (`dracula.ts` menu separators still `interactionNeutral`). +- For Emre: Editor chrome scrollbars are `scrollbarThumb`, not a list-selection fill. + +### 2026-09-01 — Light user chat bubbles lift off the panel +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: `surfaceValue`, `borderDefault` (values unchanged) +- Binding: plain `MessageBubble` fill `authBackdrop` → `surfaceValue` in light. Dark stays `authBackdrop`. Border was already `borderDefault`. +- Neighbors: chat panel `surfaceRaised` `#f4f5f7`. Bubble `#ffffff` + `borderDefault` (ink `.15`). Same as `UserRequestBox`. +- Walked: typed user message in the AI panel, light mode. Dark unchanged. +- Why: `authBackdrop` light `#c7cbd2` recedes on the raised panel. Lighter-on-light needs `surfaceValue`. +- Out of scope: assistant reply (not a bubble); composer; login `authBackdrop`. +- For Emre: User chat chrome in light is `surfaceValue` + `borderDefault`, not the login well. + +### 2026-09-01 — Light ghost hover recedes, not lifts +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: `interactionHover` (values unchanged) +- Binding: ghost `Button` hover used `surfaceRaised`. On tab-rail / chat-header chrome that is already `surfaceRaised` (or close), hover lifted toward white. Light hover is now `interactionHover` (ink wash, same as Add metrics / Select). Dark still `surfaceRaised`. +- Neighbors: `SegmentedControlButton` hover is already `interactionHover`. Cell maximize/more still override light hover to `surfaceBase` on raised cells. +- Walked: Add New, Tab History, Tab Settings (SQL tab rail + AI header). +- Why: Ghost on grey chrome must darken. `surfaceRaised` is an elevation step, not a hover wash. +- Out of scope: dark ghost; pressed History chip (`surfaceRaised` + `borderDefault`). +- For Emre: Light ghost hover is the inverting overlay, not a surface lift. + +### 2026-09-01 — Schema toolbar: 2px gap between action chips +- Lift: binding (layout) +- Status: applied locally +- Modes: both +- Binding: Schema header `Box` around Add metrics / Select / Auto refresh `gap="0"` → `gap="0.2rem"` (2px at the 10px rem root). +- Walked: tables panel with auto-refresh on, hover Select. +- Why: Hover fills were flush, so Select fused with the active refresh chip. +- Out of scope: other `gap="0"` toolbars. +- For Emre: Spacing only; tokens unchanged. + +### 2026-09-01 — Light `borderStrong` quieter for activated controls +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `borderStrong` +- Before → after (light): `#828b99` → `#b0b7c2`. Dark unchanged `#3d414d`. +- Neighbors: rest stroke is `borderDefault` (ink `.15` ≈ `#dddfe2` on white). Activated hover/open/focus still steps up, but not to charcoal. `gridFocus` stays `#828b99` — denser grid ring, not the chrome stroke. +- Walked: AI model SelectMenu open, Download as Parquet hover, cell Auto Refresh SelectMenu, Filter/Search fields on focus, ThemeModeSelector open. +- Why: Activated chrome stroke was jumping the scale. Same role (`borderStrong`) — the light value was too heavy. +- Out of scope: dark; brand accent fields (`$tone="accent"`); `gridFocus`. +- For Emre: Light `borderStrong` is now the quiet activated control stroke. Keyboard outlines, sash hover, checkbox rest, and segmented-control track also inherit. Flag if those jobs need to stay at the old `#828b99` (would then be a new role, not this one). + +### 2026-09-01 — Chat history hover matches dropdown items +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: `interactionHover` (values unchanged) +- Binding: `ChatHistoryItem` hover `surfaceBase` → `interactionHover` in light. Dark stays `surfaceInput`. +- Neighbors: dropdown / schema-tree / search-row hover is `interactionHover`, not a surface. `surfaceBase` `#eef0f3` on a `surfaceRaised` `#f4f5f7` panel was a solid step and read as a different family. +- Walked: AI chat history list, light mode. +- Why: Same job as a menu item hover. Not a surface, and not one step lighter — the ink wash. +- Out of scope: dark (still a solid recede on the raised panel); selected/"Current" treatment (text only). +- For Emre: List-row hover is `interactionHover` in light. Do not invent a lighter surface for this. + +### 2026-09-01 — Light scrollbar thumb one notch quieter +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `scrollbarThumb` +- Before → after (light): `#c5cad3` → `#d2d6de`. Dark unchanged (`white` `.13`). +- Neighbors: still darker than `surfaceRaised` `#f4f5f7` / `surfaceCanvas` `#e8eaee`, quieter than `borderStrong` `#b0b7c2`. Webkit, Firefox `scrollbar-color`, and Monaco overlay all read this token. +- Walked: schema tree, editor, AI panel overflows. +- Why: After the surface lift the thumb still sat a bit heavy. One step toward the chrome, not a new role. +- Out of scope: dark; track (stays transparent). +- For Emre: Same `scrollbarThumb` role. Light value only. + +### 2026-09-01 — Chrome-tabs plus hover is the ghost leftover +- Lift: binding +- Status: applied locally +- Modes: light +- Tokens: `interactionHover` (values unchanged) +- Binding: `.new-tab-button:hover` in `_react-chrome-tabs.scss` used `surfaceRaised`, more specific than the ghost `Button` rule. Light hover → `interactionHover`. Dark still `surfaceRaised`. +- Neighbors: same wash as tab-rail History / Settings ghosts and Add metrics. +- Walked: SQL tab bar plus in light. +- Why: The plus is not an `IconButton`; chrome-tabs owns its hover in SCSS. +- Out of scope: tab close / edit chip hovers (still `surfaceRaised`); pressed History chip. +- For Emre: Chrome-tabs plus is ghost chrome. Do not let SCSS reintroduce a surface lift. + +### 2026-09-01 — Light scrollbar thumb into stone, not a cooler neighbor +- Lift: palette +- Status: tweaked (too warm; see following entry) +- Modes: light +- Tokens: `scrollbarThumb` +- Before → after (light): `#d2d6de` → `#ddd8d1`. Dark unchanged. +- Neighbors: no warm chrome role to borrow. Surfaces, controls, and `borderStrong` are all cool blue-gray. +- Why: The previous notch was not visible. Hue shift was noticeable, but too warm. + +### 2026-09-01 — Light scrollbar thumb cooler, still a real step +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `scrollbarThumb` +- Before → after (light): `#ddd8d1` → `#d7dbe3`. Dark unchanged. +- Neighbors: same cool family as `surfaceCanvas` `#e8eaee` and `borderStrong` `#b0b7c2`. Lighter than the original `#c5cad3`, cooler than the stone miss. +- Walked: schema, editor, AI overflows. +- Why: Stone was too warm. Stay on the cool ladder; take a larger lightness step than `#d2d6de` so it still reads quieter. +- Out of scope: dark. +- For Emre: Light thumb is cool chrome, not stone. `#c5cad3` → `#d7dbe3`. + +### 2026-09-01 — Provider choice cards go neutral +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `interactionHover`, `interactionNeutral`, `borderDefault` (values unchanged) +- Binding: `SelectableCardButton` hover `borderAccent` / `interactionAccentHover` → `borderDefault` / `interactionHover`. Selected `borderAccent` / `interactionAccentActive` / inset accent ring → `interactionNeutral` + `borderDefault` (same as list selected). +- Neighbors: AI setup provider cards and Add Metric type cards share this component. +- Walked: Add a model provider (OpenAI / Anthropic / Custom), both modes. +- Why: Choice cards are chrome selection, not a brand action. +- Out of scope: `TabButton` underlines (still branded); composer `$tone="accent"`. +- For Emre: Large-target selected cards match list selected, not accent. + +### 2026-09-01 — Multi-step pill gets a visible hairline +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `borderDefault` (values unchanged) +- Binding: `StepIndicatorContainer` had only `shadowSubtle`. Add `borderDefault` so the capsule reads on the modal. +- Neighbors: modal itself is `borderDefault`. Inner step name chip stays `interactionNeutral`. +- Walked: Add a model provider step pill, light and dark. +- Why: Shadow alone disappeared, especially in dark. `borderSubtle` would still vanish; `borderDefault` is the visible-subtle step. +- Out of scope: pill fill. +- For Emre: Step chrome uses the same stroke as the modal edge. + +### 2026-09-01 — Status badges are chips; count badges stay pills +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `statusSuccessSurface`, `statusDangerSurface`, `statusWarningSurface`, `statusInfoSurface`, `interactionNeutral` (values unchanged here) +- Binding: `Badge` default `shape="chip"` — 4px radius, 5px padding, 400/11px, no border, status surface fills. `shape="pill"` keeps the old stadium + 10%/32% wash. `TabBadge` (Monitoring warning/error counts) is pinned to `pill`. +- Neighbors: AI Validated chip, import file-status, Type/Trend badges, “New”, version/EE badges inherit chip. Monitoring tab count must not. +- Walked: Assistant Settings Validated chip; table details Monitoring tab. +- Why: Figma status chips are rounded rects on a status well, not stadium pills. Count badges on tabs are a different job. +- Out of scope: sidebar Enabled/Inactive (`StatusChip`, not `Badge`). +- For Emre: Two shapes on one primitive. Pill is the compact count. Chip is the status label. + +### 2026-09-01 — Light success wells denser so chips read +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `statusSuccessSurface` +- Before → after (light): `rgba(8, 122, 80, 0.1)` → `rgba(8, 122, 80, 0.16)`. Dark unchanged. +- Neighbors: `statusSuccess` `#067047` / `statusSuccessBorder` `.28`. Sits on `surfaceOverlay` `#fafbfc` and selected `interactionNeutralHover` `#e2e5ea`. +- Walked: Assistant Settings Enabled chip and Validated chip, light mode. +- Why: `.10` washed out on the new light ladder. Typical light success chips need a denser well. +- Out of scope: dark success surface; danger/warning/info surfaces. +- For Emre: Light success fill is the chip/banner well. `.16` is still a wash, not a solid green-100. + +### 2026-09-01 — Light Enabled chip uses the success well +- Lift: binding +- Status: applied locally +- Modes: light (dark unchanged) +- Tokens: `statusSuccessSurface`, `statusSuccessBorder`, `surfaceValue`, `borderDefault` (values as above) +- Binding: sidebar `StatusChip` Enabled was `interactionNeutral` in both modes — same as the selected provider row (`interactionNeutralHover`) in light, so the chip vanished. Light Enabled → `statusSuccessSurface` + `statusSuccessBorder`. Light Inactive → `surfaceValue` + `borderDefault`. Dark stays `interactionNeutral`, no border (Figma). +- Neighbors: selected provider `interactionNeutralHover` `#e2e5ea`. Modal `surfaceOverlay` `#fafbfc`. +- Walked: Assistant Settings sidebar, light, selected Enabled row. +- Why: Light chips need a fill that is not the selected-row gray. Enabled is a success state. +- Out of scope: dark Enabled (already matches Figma); Monitoring `TabBadge`. +- For Emre: Light Enabled is a success chip. Dark Enabled is a neutral well with success type. + +### 2026-09-01 — AI settings selected provider is one step quieter +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `interactionNeutral` (values unchanged) +- Binding: `ProviderTab` selected `interactionNeutralHover` → `interactionNeutral`. +- Neighbors: unselected hover stays `controlSurfaceHover`. Modal is `surfaceOverlay`. Tab underline stays `contentAccent`. +- Walked: Assistant Settings sidebar, light. +- Why: Selected row was a notch too heavy on the lifted light ladder. Same interaction role, one step quieter. +- Out of scope: editor/drawer `TabButton` (still `surfaceRaised`). +- For Emre: Vertical provider tabs use the list-selected well, not the hover well. + +### 2026-09-01 — Models empty well is inset, not the dialog scrim +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `surfaceInset` (values unchanged) +- Binding: `ModelsPlaceholder` `surfaceScrim` → `surfaceInset`. +- Neighbors: modal `surfaceOverlay` `#fafbfc`; inputs `surfaceInput` `#fafbfc`. Scrim is 32% ink — a backdrop, not a well. +- Walked: Assistant Settings before API key validation, light. +- Why: `surfaceScrim` is occlusion. An empty models region is an embedded well. +- Out of scope: Overlay / docsearch / grid fades still use scrim. +- For Emre: Placeholder copy sits on inset, not the dimmer. + +### 2026-09-01 — Models empty well one step heavier +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `surfaceRaised` (values unchanged) +- Binding: `ModelsPlaceholder` `surfaceInset` → `surfaceRaised`. +- Neighbors: modal `surfaceOverlay` `#fafbfc`. Light inset `#f7f8f9` vanished; raised `#f4f5f7` is the next visible well. +- Walked: Assistant Settings Enable Models placeholder, light. +- Why: Inset was a notch too close to the modal. Same empty-well job, one step more present. +- Out of scope: scrim; other inset wells. +- For Emre: Empty models copy on raised, not inset. + +### 2026-09-01 — Form selects use the input well +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `surfaceInput` (values unchanged) +- Binding: `SelectMenuControl` trigger was secondary `controlSurface`. Form fields should match `Input` (`surfaceInput`). Toolbar `SelectMenu.Trigger` stays `controlSurface`. +- Neighbors: Provider Type next to Provider Name / Base URL / API Key inputs. +- Walked: Add Custom Provider, light. +- Why: Same row of fields, two fills. Form selects are fields, not toolbar buttons. +- Out of scope: toolbar dropdowns. +- For Emre: `SelectMenuControl` is the form select. Trigger fill is `surfaceInput`. + +### 2026-09-01 — Footer Connected mark matches Ingesting +- Lift: binding (geometry) +- Status: applied locally +- Modes: both +- Tokens: `statusSuccess` / `statusDanger` (values unchanged) +- Binding: footer `ConnectionStatus` mark was a circle with success/danger glow. It is now the same rounded-square SVG as Ingesting / Enabled chips (`0.9rem`, 15% radius, 32% stroke). +- Neighbors: MCP / Ingesting marks in the same footer row. +- Walked: footer Connected, light and dark. +- Why: Status marks in one bar should share geometry. Color still carries the state. +- Out of scope: MCP pairing popover; Ingesting internals. +- For Emre: Footer status marks are squares, not dots. + +### 2026-09-01 — Provider Type uses the console select +- Lift: binding (control) +- Status: applied locally +- Modes: both +- Binding: Add Custom Provider `Select` (native ``. + --- ## Parking lot — not logged as decisions yet diff --git a/e2e/tests/console/aiAssistant.spec.js b/e2e/tests/console/aiAssistant.spec.js index e362d6e54..1b4ec5998 100644 --- a/e2e/tests/console/aiAssistant.spec.js +++ b/e2e/tests/console/aiAssistant.spec.js @@ -3291,10 +3291,9 @@ describe("custom providers", () => { cy.getByDataHook("custom-provider-name-input") .should("be.visible") .type("Ollama") - cy.getByDataHook("custom-provider-type-select").should( - "have.value", - "openai-chat-completions", - ) + cy.getByDataHook("custom-provider-type-select") + .should("be.visible") + .should("contain", "OpenAI Chat Completions API") cy.getByDataHook("custom-provider-base-url-input").type( "http://localhost:11434/v1", ) diff --git a/src/components/Badge/index.tsx b/src/components/Badge/index.tsx index 60d13d17e..932f9e30c 100644 --- a/src/components/Badge/index.tsx +++ b/src/components/Badge/index.tsx @@ -22,10 +22,15 @@ export const badgeVariants = [ export type BadgeVariant = (typeof badgeVariants)[number] +export type BadgeSize = "sm" | "md" +export type BadgeShape = "chip" | "pill" + type Props = React.ComponentPropsWithoutRef<"span"> & { type?: BadgeType variant?: BadgeVariant - size?: "sm" | "md" + size?: BadgeSize + /** `chip` is the default status badge. `pill` is reserved for compact count badges. */ + shape?: BadgeShape icon?: React.ReactNode pulsate?: boolean children?: React.ReactNode @@ -68,26 +73,55 @@ const getTone = (variant: BadgeVariant, theme: DefaultTheme) => { return tones[variant] } +const chipBackground = (variant: BadgeVariant, theme: DefaultTheme) => { + switch (variant) { + case "success": + return theme.color.statusSuccessSurface + case "danger": + return theme.color.statusDangerSurface + case "warning": + return theme.color.statusWarningSurface + case "info": + return theme.color.statusInfoSurface + case "neutral": + return theme.color.interactionNeutral + default: + return withAlpha(getTone(variant, theme), 0.1) + } +} + const Root = styled.span<{ $variant: BadgeVariant - $size: "sm" | "md" + $size: BadgeSize + $shape: BadgeShape pulsate?: boolean }>` display: inline-flex; align-items: center; justify-content: center; position: relative; - gap: 0.5rem; - height: ${({ $size }) => ($size === "sm" ? "2.2rem" : "2.8rem")}; - padding: ${({ $size }) => ($size === "sm" ? "0 0.7rem" : "0 0.9rem")}; - border: 1px solid - ${({ $variant, theme }) => withAlpha(getTone($variant, theme), 0.32)}; - border-radius: 999px; + gap: ${({ $shape }) => ($shape === "pill" ? "0.5rem" : "0.4rem")}; + height: ${({ $shape, $size }) => + $shape === "pill" ? ($size === "sm" ? "2.2rem" : "2.8rem") : "auto"}; + padding: ${({ $shape, $size }) => + $shape === "pill" + ? $size === "sm" + ? "0 0.7rem" + : "0 0.9rem" + : "0.5rem"}; + border: ${({ $shape, $variant, theme }) => + $shape === "pill" + ? `1px solid ${withAlpha(getTone($variant, theme), 0.32)}` + : 0}; + border-radius: ${({ $shape }) => ($shape === "pill" ? "999px" : "0.4rem")}; color: ${({ $variant, theme }) => getTone($variant, theme)}; - background: ${({ $variant, theme }) => - withAlpha(getTone($variant, theme), 0.1)}; - font-size: ${({ theme }) => theme.fontSize.xs}; - font-weight: 600; + background: ${({ $shape, $variant, theme }) => + $shape === "pill" + ? withAlpha(getTone($variant, theme), 0.1) + : chipBackground($variant, theme)}; + font-size: ${({ $shape, theme }) => + $shape === "pill" ? theme.fontSize.xs : "1.1rem"}; + font-weight: ${({ $shape }) => ($shape === "pill" ? 600 : 400)}; line-height: 1; white-space: nowrap; @@ -105,7 +139,7 @@ const Root = styled.span<{ `}; ` -const Icon = styled.div<{ hasGap: boolean }>` +const Icon = styled.div<{ hasGap: boolean; $shape: BadgeShape }>` display: flex; justify-content: center; align-items: center; @@ -113,8 +147,8 @@ const Icon = styled.div<{ hasGap: boolean }>` ${({ hasGap }) => !hasGap && "margin: 0 -0.1rem;"} svg { - width: 1.4rem; - height: 1.4rem; + width: ${({ $shape }) => ($shape === "pill" ? "1.4rem" : "1.6rem")}; + height: ${({ $shape }) => ($shape === "pill" ? "1.4rem" : "1.6rem")}; } ` @@ -124,6 +158,7 @@ export const Badge = React.forwardRef( type, variant, size = "md", + shape = "chip", icon, pulsate, children, @@ -139,10 +174,15 @@ export const Badge = React.forwardRef( className={className} $variant={getVariant(variant, type)} $size={size} + $shape={shape} pulsate={pulsate} data-hook={dataHook} > - {icon && 0}>{icon}} + {icon && ( + 0}> + {icon} + + )} {children} ), diff --git a/src/components/Button/variants.ts b/src/components/Button/variants.ts index 81a704ea9..b870f0e85 100644 --- a/src/components/Button/variants.ts +++ b/src/components/Button/variants.ts @@ -197,7 +197,10 @@ export const makeButtonVariant = (variant: ButtonVariant) => { } &&:hover:not(:disabled):not([aria-disabled="true"]) { - background: ${getColor(theme.hover.background)}; + background: ${({ theme: t }) => + variant === "ghost" && t.mode === "light" + ? t.color.interactionHover + : t.color[theme.hover.background]}; color: ${getColor(theme.hover.color)}; border-color: ${getColor(theme.hover.border)}; } diff --git a/src/components/MultiStepModal/index.tsx b/src/components/MultiStepModal/index.tsx index c68d0bf23..1f40cbd11 100644 --- a/src/components/MultiStepModal/index.tsx +++ b/src/components/MultiStepModal/index.tsx @@ -91,6 +91,7 @@ const StepIndicatorContainer = styled(Box).attrs({ backdrop-filter: blur(0.6rem); padding: 0.4rem; border-radius: 10rem; + border: 1px solid ${({ theme }) => theme.color.borderDefault}; box-shadow: 0 0.1rem 0.2rem ${({ theme }) => theme.color.shadowSubtle}; width: fit-content; ` diff --git a/src/components/SelectMenu/index.tsx b/src/components/SelectMenu/index.tsx index 72cc7d4eb..25f964e32 100644 --- a/src/components/SelectMenu/index.tsx +++ b/src/components/SelectMenu/index.tsx @@ -302,6 +302,20 @@ type SelectMenuControlProps = { * indicator as the toolbar dropdowns. Prefer this over native selects when the * menu is part of an application workflow rather than a browser form. */ +const FieldTrigger = styled(Trigger)` + && { + background: ${({ theme }) => theme.color.surfaceInput}; + } + + &&:hover:not(:disabled):not([aria-disabled="true"]) { + background: ${({ theme }) => theme.color.surfaceInput}; + } + + &&[aria-expanded="true"] { + background: ${({ theme }) => theme.color.surfaceInput}; + } +` + export const SelectMenuControl = ({ name, id, @@ -331,7 +345,7 @@ export const SelectMenuControl = ({ return ( - theme.color.borderAccent}; - background: ${({ theme }) => theme.color.interactionAccentHover}; + border-color: ${({ theme }) => theme.color.borderDefault}; + background: ${({ theme }) => theme.color.interactionHover}; color: ${({ theme }) => theme.color.contentPrimary}; } ${({ $selected, theme }) => $selected && css` - && { - border-color: ${theme.color.borderAccent}; - background: ${theme.color.interactionAccentActive}; + &&, + &&:hover:not(:disabled) { + border-color: ${theme.color.borderDefault}; + background: ${theme.color.interactionNeutral}; color: ${theme.color.contentPrimary}; - box-shadow: inset 0 0 0 1px ${theme.color.borderAccent}; + box-shadow: none; } `} ` diff --git a/src/components/SetupAIAssistant/CustomProviderModal.tsx b/src/components/SetupAIAssistant/CustomProviderModal.tsx index dbabef026..1ce94afec 100644 --- a/src/components/SetupAIAssistant/CustomProviderModal.tsx +++ b/src/components/SetupAIAssistant/CustomProviderModal.tsx @@ -11,7 +11,7 @@ import type { ProviderType, CustomProviderDefinition, } from "../../utils/ai/settings" -import { Select } from "../Select" +import { SelectMenuControl } from "../SelectMenu" import { toast } from "../Toast" import { ModelSettings, @@ -150,12 +150,13 @@ const StepOneContent = ({ Provider Type - `. +### 2026-09-02 — Light MCP notebook promo is a solid card +- Lift: binding +- Status: applied locally +- Modes: light +- Tokens: `surfaceValue` (values unchanged) +- Binding: `NotebookMcpPromo` `Container` was unfilled (stage showing through the dashed `contentAccent` stroke). Light fill is now `surfaceValue`. Dark stays transparent. +- Neighbors: notebook stage `surfaceStage`; cells `surfaceRaised`; command chip inside the promo already `surfaceRaised`. +- Walked: notebook MCP promo, light, expanded. +- Why: A dashed outline on the stage read as a hole. The notice is a card, not a well. White sits above the stage and above cells without a new token. +- Out of scope: dark fill; dashed stroke; `New` badge. +- For Emre: Light promo fill is `surfaceValue`. Dark is still the dashed stroke only. + +### 2026-09-04 — Switch is larger and squared +- Lift: binding (geometry) +- Status: applied locally +- Modes: both +- Tokens: unchanged (`controlTrack`, `controlKnob`, `contentAccent` / `statusSuccessStrong` on, `contentInverse` thumb) +- Binding: `Switch` track was a 36×20 pill with a 14px circular thumb. Default (`md`) is now 44×20, 6px radius, 1.5px inset, 24px rounded-square thumb (5px radius) — Figma node `1902:32067`. `sm` scales the same language (36×18 / 20px thumb). +- Neighbors: AI Settings model list (the Figma frame); Editor Settings; import settings; schema designated timestamp. +- Walked: Editor Settings switch, light and dark, on and off. +- Why: The pill read as a tiny iOS switch. Squared + longer travel makes the control a row action, not chrome jewelry. +- Out of scope: color retune (still branded on / success tone); checkboxes; Figma’s green-on — we keep current `tone`. +- For Emre: Switch geometry only. Do not copy Figma’s `#19ac43` onto the default accent switch. + +### 2026-09-04 — Switch stroke is 0.5px +- Lift: binding (geometry) +- Status: applied locally +- Modes: both +- Binding: `Switch` track stroke is a 0.5px inset `box-shadow` (not `border: 0.5px` — Chromium rounds that used value to 1px even at 2x). Padding is 2px so the hairline plus 1.5px gap still reads as 2px outside-to-thumb. Thumb height/travel stay `md` 16px / 16px. +- Neighbors: same as the squared-switch entry. +- Walked: Editor Settings switch, light, off and on. +- Why: 1px hairline plus 1.5px padding made a 2.5px gutter. Design is 0.5 + 1.5 = 2px outside-to-thumb. +- Out of scope: color; focus outline still 1px. +- For Emre: Switch rest/on stroke is 0.5px. Do not thicken it to match other controls. + +### 2026-09-04 — Table and mat-view glyphs are muted +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `contentMuted` (values unchanged) +- Binding: `TableIcon` (and standalone `MaterializedViewIcon`) `contentAccent` → `contentMuted`. Schema row title no longer forces accent onto nested SVGs. +- Neighbors: schema tree, details `TableSelector`, metrics table picker, AI schema chips, “Create materialized view” menu glyph. Rail table-details latch stays `contentAccent` when selected. +- Walked: schema tree + details table select, light. +- Why: Object-type marks were wearing brand. They are labels, not selected chrome. +- Out of scope: rail latches; notebook title glyphs. Column type glyphs: see following entry. +- For Emre: Table / mat-view / view identity icons are `contentMuted`. + +### 2026-09-04 — Column metadata glyphs are muted +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: `contentMuted` (values unchanged) +- Binding: `ColumnIcon` `TypeIcon` and designated-timestamp `SortDownIcon` `contentAccent` → `contentMuted`. +- Neighbors: schema tree column rows; details Columns list (same `ColumnIcon`). +- Walked: expanded table columns in schema tree, light. +- Why: Same job as table/mat-view marks — type labels, not brand. +- Out of scope: rail latches; copy-pulse highlight still uses `contentAccentStrong`. +- For Emre: Column type icons (including designated timestamp) are `contentMuted`. + +### 2026-09-04 — Base-table badge hover in dark +- Lift: binding +- Status: applied locally +- Modes: dark (light already this token) +- Tokens: `interactionHover` (values unchanged) +- Binding: `BaseTableLinkButton` (mat-view Details → Base Table) inherits ghost hover, which is `surfaceRaised` in dark — the same fill as the details drawer, so the wash disappears. Hover is now `interactionHover` in both modes, matching light ghost. +- Neighbors: drawer is `surfaceRaised`. Global dark ghost still `surfaceRaised` (deferred). +- Walked: `bbo_1s` Details, light and dark, hover on `market_data`. +- Why: Light ink wash is visible on raised chrome. Dark elevation hover is not. +- Out of scope: retuning dark `interactionHover`; other ghost buttons. +- For Emre: This badge uses the overlay, not a surface step. Flag if dark ghost should follow globally. + +### 2026-09-05 — Light tab rail is raised at 94% +- Lift: palette +- Status: applied locally +- Modes: light +- Tokens: `surfaceTabRail` +- Before → after (light): `rgba(232, 234, 238, 0.94)` → `rgba(244, 245, 247, 0.94)` (`#F4F5F7` at 94%) +- Neighbors: same RGB as `surfaceRaised` `#f4f5f7`, still the frosted rail (blur + 94%). Dark `rgba(18, 19, 23, 0.82)` unchanged. +- Walked: editor tab strip, light. +- Why: Rail was still the old canvas grey at 94%. Zack wants it to sit with raised chrome. +- Out of scope: dark rail; individual tab pills (still transparent / `interactionHover`). +- For Emre: Light `surfaceTabRail` is now raised-at-94%, not canvas-at-94%. + +### 2026-09-05 — Switch on-fill is a unique success fill +- Lift: palette + binding +- Status: applied locally +- Modes: both +- Tokens: `statusSuccessFill` (new). Meaning: opaque positive fill that does not need text contrast. Not `statusSuccess` / `Strong` (ink) and not a component-named `switchGreen`. +- Before → after: dark `#3fa659`, light `#1f7a39` (Figma `1919:102397` / Assistant Settings `1924:106893`, `1913:79570`) +- Binding: `Switch` on-state `contentAccent` / `statusSuccessStrong` → `statusSuccessFill`. Thumb is `contentInverse` in both states. Rest track in light is Figma `#c9cdd4` (not retuning `controlTrack` — segmented chips still use it). Stroke stays 0.5px inset: dark white `.15`, light ink `.25`. +- Geometry: track radius `8px` (`sm` `7px`), thumb radius `6px` (`sm` `5px`). Size still `44×20` / `24` thumb, `2px` pad. +- Neighbors: Enabled / Validated stay `statusSuccess` `#66bb6a` / `#067047`. The fill is brighter and more saturated on purpose. +- Walked: Editor Settings + AI Enable Models switches, light and dark. +- Why: Text greens cannot also be the on-track. Zack wants a reviewable unique green in the same family. +- Out of scope: checkboxes; hover step; retuning `controlTrack`; other controls. +- For Emre: `statusSuccessFill` is the opaque on-fill. Flag if the name should be `controlOn` once more controls share it. + +### 2026-09-06 — statusInfo family: Subtle, Text, Control +- Lift: palette +- Status: applied locally +- Modes: both +- Tokens: `statusInfo` (unchanged), `statusInfoSubtle` (new), `statusInfoControl` (new), `statusInfoSurface` (retuned to Subtle wash) +- Before → after: + - `statusInfo` stays `#81d3f9` / `#176f87` — info type + focus ring at 75% + - `statusInfoSubtle` dark `#b2e7ff`, light `#159cc1` — latch icon + - `statusInfoControl` dark `#0c80f3`, light `#0a88db` — checkbox / persistent on-fill (Option 2, bluer) + - `statusInfoSurface` dark Text `@10%` → Subtle `@5%` `rgba(178, 231, 255, 0.05)`; light Text `@10%` → Subtle `@10%` `rgba(21, 156, 193, 0.10)` (Figma latch well) +- Neighbors: do not retune `statusInfo`. Subtle is the quiet on-glyph, not a banner fill. Control is the opaque fill; porcelain ticks use `contentInverse` (~3.8:1 on Option 2). One wash — Surface is Subtle, not a second Text wash. +- Walked: Figma `1943:113543` / `115332` (latch), `115217` / `117006` (focus), `117179` / `118238` (checkbox) +- Why: Brand crimson was doing chrome jobs (latch, focus, checkbox). Info ramp splits quiet / type / fill so those jobs leave brand. +- Out of scope: `contentAccent` / `actionPrimary`; rail icons; tab underlines; notebook cell focus. +- For Emre: Three roles, one family. Flag if Surface should stay a Text wash for badges — latch Figma is Subtle at 5/10. + +### 2026-09-06 — Bind latches, focus, checkboxes, object glyphs to statusInfo +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: values unchanged here +- Binding: + - Non-rail latched icon buttons (`PrimaryToggleButton` `activeTone="info"`): schema auto-refresh / select, result + notebook freeze. Icon `statusInfoSubtle`, well `statusInfoSurface`. Idle stays `contentSecondary`. + - Rail `Navigation` (and other rail `PrimaryToggleButton`s) stay `$activeTone="accent"` — `contentAccent` + `interactionAccentActive`. Navigation pins the branded well so a later default change cannot leak. + - Keyboard focus (`*:focus-visible`, Button, Switch, Checkbox, chrome-tabs, leftover `borderStrong` rings) → `statusInfo` at 75% (`statusInfoFocus`). Notebook cell focus (`CellWrapper`) stays branded. + - `Checkbox` checked fill `contentAccent` → `statusInfoControl`; tick stays `contentInverse`. Schema multi-select filled circle unchanged. + - Notebook title glyph + chrome-tab notebook/metrics favicons: `contentObject` → `statusInfo` (light leaves brand `#b81447`). +- Neighbors: `TabButton` underline, AI `$tone="accent"`, `SelectMenu` check, switches (`statusSuccessFill`) unchanged. +- Walked: schema auto-refresh, freeze column, Search “Include closed tabs”, button focus, notebook glyph, rail icons. Light and dark. +- Why: Zack: only non-rail latches take the new pattern. Rail is the branded exception. +- Out of scope: `SelectMenu` check; cell focus; tab underlines; field focus borders (`Input` still `borderStrong` / accent). +- For Emre: `activeTone="info"` is the latch. Do not fold it into `accent`. `contentObject` is now unused in components — collapse when ready. + +### 2026-09-06 — Product brand is QDB Pink (same hexes both modes) +- Lift: palette +- Status: applied locally +- Modes: both +- Tokens: `contentAccent`, `contentAccentStrong`, `actionPrimary`, `actionPrimaryHover`, `interactionAccentHover`, `interactionAccentActive`, `borderAccent`, `borderAccentStrong`, `brandGradientStart`, `brandGradientEnd`, `aiGradientStart`, `aiGradientEnd` +- Before → after (both modes unless noted): + - `contentAccent` `#c94f74` / `#b81447` → `#f0428b` (Pink 300) — rail, tabs, remaining chrome accent + - `contentAccentStrong` `#cf1750` / `#8a0f35` → `#ee2b7c` (Pink 400) + - `actionPrimary` `#b81447` / `#8a0f35` → `#bd0f58` (Pink 700) + - `actionPrimaryHover` `#cf1750` / `#b81447` → `#d41162` (Pink 600) + - Accent wells rebase on Pink 300; `interactionAccentActive` is **10%** (rail latch well), was 15%/13% +- Neighbors: Pink 700 is the opaque action; Pink 300 is on-chrome. Do not collapse them. Cell focus and `SelectMenu` check inherit Accent. +- Walked: Figma Core `2244:340`; console `1913:61852` / `73608` / `83402` / `93667` +- Why: Pinker brand so it no longer reads as the error red. +- Out of scope: `contentObject`; inventing `qdbPink*` tokens. +- For Emre: Same hexes in light. Flag if light actions need a darker step (old light primary was `#8a0f35`). + +### 2026-09-06 — Danger rotates to pure red +- Lift: palette + binding +- Status: applied locally +- Modes: both (hexes shared; light contrast is a known risk) +- Tokens: `statusDanger` `#ff3333`; `statusDangerStrong` `#db2424` (was `#dc2828`); `statusDangerContrast` `#ff4d4d` (new, type on 15% wash); `statusDangerSubtle` `#ff8080` (new, type on 40% wash); `statusDangerSurface` `#db2424` @ **15%**; `statusDangerSurfaceHover` `#db2424` @ **40%**; `statusDangerMuted` Strong @ 72%; `statusDangerBorder` Strong @ 28% +- Binding: + - `danger` Button is solid Strong + `contentInverse` (Delete conversation confirm, Cancel script). Hover fill is Text `#ff3333`. + - `dangerGhost` rest Surface + Contrast; hover SurfaceHover + Subtle (Reset Provider, Figma `1948:123286`). + - Chat history row delete `dangerGhost` → `danger`. + - Field error border Strong; wash Surface. MCP / login / details banners use Surface, not `statusDanger`+`1f`. +- Neighbors: brand pink and danger red must not share a hex. +- Why: Separate “this is QuestDB” from “this is broken / destructive.” +- Out of scope: light-only darker danger type (Zack said dark first). +- For Emre: Contrast/Subtle are type-on-wash, not fills. Strong is the opaque source. + +### 2026-09-06 — Light brand + danger split from dark +- Lift: palette +- Status: applied locally +- Modes: light only (dark hexes unchanged) +- Tokens: + - `contentAccent` `#bd0f58` — rail icons + tab underlines + - `contentAccentStrong` `#8e0b42` — sits with actions + - `actionPrimary` `#8e0b42`; `actionPrimaryHover` `#bd0f58` + - `interactionAccentActive` `#bd0f58` @ **13%** (rail well); hover rebase @ 8% + - `statusDanger` `#ce1717` — type + icons always + - `statusDangerStrong` `#ce1717` — solid Delete fill + - `statusDangerContrast` `#b81414` — type on `#bd2828` @ 15% + - `statusDangerSubtle` `#8a0f0f` — type on `#db2424` @ 40% hover + - `statusDangerSurface` `#bd2828` @ 15%; `statusDangerSurfaceHover` `#db2424` @ 40% +- Neighbors: `danger` / `dangerGhost` variants unchanged — they read these roles. Light solid delete is Text, not the dark Strong `#db2424`. +- Walked: Figma `1913:61813` / `79570` / `85185` / `93656`; hover `1948:123305` +- Why: One-for-one with the dark pass, with a darker action and a contrast-safe light red. +- Out of scope: retuning dark; leaving brand on rail/tabs/cell focus. +- For Emre: Light Strong equals Text so `danger` buttons stay `#ce1717`. Do not share Strong across modes. + +### 2026-09-06 — MCP pair error banner +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: no new roles. Light fill is banner-only `rgba(189, 40, 56, 0.08)` (Figma, not the 15% wash). Dark fill is transparent. Stroke is `statusDanger` (light) / `statusDangerMuted` (dark). Type + icon `statusDanger`. +- Binding: `PairPopover` danger `StatusRow` is inset in the form — 1px border, 3px left accent, 6px radius, 12px padding. Validation / WS error / major version-mismatch use it. `StatusDetail` inherits Text, not `contentSecondary`. +- Walked: Figma `1913:86918` (light), `1913:85135` (dark) +- Why: Validation is a status banner, not a full-bleed wash. +- Out of scope: consent modal; connecting / minor-mismatch rows. +- For Emre: Do not fold the 8% banner wash into `statusDangerSurface`. + +### 2026-09-06 — Reset Provider is a true ghost, not dangerGhost +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: none. `dangerGhost` stays the washed secondary (Surface + Contrast / SurfaceHover + Subtle). +- Binding: Assistant Settings Reset / Remove Provider uses `ghost` + local danger type. Rest is transparent + `statusDanger`. Hover is `#db2424` @ 30% + Subtle in dark; `#bd2828` @ 10% + Text in light. Icons follow type. +- Walked: Figma `1924:106893` / `1948:123175` (dark), `1919:102447` / `1948:123305` (light) +- Why: The filled wash is for secondary destructive chips. Reset is a quiet text action until hover. +- Out of scope: retuning `dangerGhost`; chat-history solid delete. +- For Emre: Do not fold this hover into `statusDangerSurfaceHover` (40%). Reset is the 10%/30% ghost, not the secondary wash. + +### 2026-09-06 — Notebook drag slot is a primary wash +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: none. `interactionNeutral` @ 25% was the old slot — light `#e8eaee` on stage `#e2e5ea` vanished. +- Binding: `react-grid-placeholder` is `withAlpha(contentPrimary, 0.08)` light / `0.10` dark. Ink on the light stage, porcelain on the dark stage. Opacity lives in the alpha, not a second `opacity`. `interactionHover` is the same idea at 7.5% / 5.5% — too quiet on the stage, and Neutral still matches the light ladder. +- Neighbors: stage `surfaceStage`. Cells `surfaceRaised`. Drag chrome stays `contentAccent`. +- Why: The snap rectangle has to read as a hole on the stage, not a control fill. Neutral is the pressed-chip role. +- Out of scope: the always-on stage dot grid (`interactionHover`); retuning Neutral. +- For Emre: One-off wash. Do not invent `surfaceDragSlot`. + +### 2026-09-06 — Freeze latch shares toolbar control height +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: none +- Binding: Result freeze (`PrimaryToggleButton`) uses `&&` + `TOOLBAR_CONTROL_HEIGHT` (Button `md` 3.4rem) and the same `0 1.2rem` padding as sibling ghost Buttons. Notebook freeze uses `&&` + ActionButton's 2.8rem / `0 0.6rem`. Latch color stays `statusInfo`. +- Neighbors: `PrimaryToggleButton` itself stays 3.5rem for rail / other chrome. Schema toolbar chips stay 3rem. +- Why: Freeze was a segmented-control latch (3.0–3.5rem, width 4rem) sitting next to default `Button` ghosts. No reason for a different chip. +- Out of scope: retuning `PrimaryToggleButton` globally; latch color. +- For Emre: Geometry only. `&&` is required to beat `SegmentedControlButton` `$size="md"` (3rem) and `PrimaryToggleButton` (3.5rem). + +### 2026-09-06 — Result grid header names are Semibold +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: none +- Binding: Column header names are Open Sans Semibold (`600`). `ResultGrid` `HeaderName`, notebook shimmer, and the legacy `.qg-header-name` grid. Width sampling uses the same weight so names don't clip. +- Neighbors: header type stays Regular / secondary. Cell values unchanged. +- Why: Figma `1913:84504` — names are `Open Sans SemiBold`, not Regular. +- Out of scope: type-row italic; cell type color. +- For Emre: `HEADER_NAME_FONT_WEIGHT` in `ResultGrid/dimensions.ts`. + +### 2026-09-06 — Add Markdown hover is the chrome wash +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: none +- Binding: secondary Add Markdown (`AddButton` `$variant="secondary"`) hover `interactionNeutral` → `interactionHover`. Type still `contentPrimary` on hover. Add Cell stays `interactionAccentActive`. +- Neighbors: sits on `surfaceStage`. Light Neutral `#e8eaee` on stage `#e2e5ea` is a lift. Dark Neutral `#32343e` is a pressed chip, several steps up. Ghost / menu / tree / search hover is already `interactionHover` (ink 7.5% / porcelain 5.5%). +- Why: One-step invert against the surface — darker in light, lighter in dark. Neutral is the selected-chip role. +- Out of scope: Add Cell; retuning `interactionHover`; global ghost. +- For Emre: Same wash as other chrome hovers. Do not use Neutral for hover on the stage. + +### 2026-09-06 — Result row count is Open Sans Semibold 15 +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: none +- Binding: Result toolbar count number is `theme.font` / `fontSize.lg` (15px) / `600` / `2.14rem` (21.4px). Dropped `fontMonospace`. "rows" stays Regular `fontSize.sm` (13px) / `1.56rem` / `contentSecondary`. +- Neighbors: header names are Semibold 14. Cell values stay mono. +- Why: Figma `1913:84477` — `350,528` is Open Sans SemiBold 15 / 21.4; `rows` is Regular 13 / 15.6. +- Out of scope: notebook QueryResult "X rows in Yms"; inventing a type token. +- For Emre: Chrome count, not a cell. Keep tabular-nums. + +### 2026-09-06 — Light tooltips share the dropdown overlay +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: none +- Binding: `Tooltip` box + arrow fill `surfaceInset` → `surfaceOverlay` in light (`#fafbfc`). Dark stays `surfaceInset` (`#121317`). Stroke stays `borderDefault`. +- Neighbors: dropdowns / popovers already `surfaceOverlay` via `floatingSurfaceStyles`. Light Inset `#f7f8f9` is the editor/grid well, a gray step below Overlay. Dark Inset sits under Overlay on the ladder, so it already matches. +- Why: Light tooltip was a well next to a near-white menu. Same floating layer as the dropdown. +- Out of scope: retuning `surfaceInset`; dark tooltip; adding the menu shadow to tooltips. +- For Emre: Do not fold Inset into Overlay. Inset stays the recessed well. + +### 2026-09-06 — Light instance hover card matches tooltips +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: none +- Binding: TopBar `CustomTooltipWrapper` fill `surfaceInset` → `surfaceOverlay` in light. Dark stays `surfaceInset`. Stroke stays `borderDefault`. +- Neighbors: shared `Tooltip` already Overlay in light. Same floating-chrome job as Export / EE tooltips. +- Why: Inset is the well; Overlay is the floating layer. Demo data has no instance type so this card is easy to miss. +- Out of scope: Monaco `editorWidget`; chart settings drawer; instance color-picker well. +- For Emre: Same split as Tooltip. Do not retune Inset. + +### 2026-09-06 — Dropdown loom blur 16 → 12 (trial, reverted) +- Lift: binding (geometry, same tokens) +- Status: reverted +- Modes: both — `floatingSurfaceStyles` is shared +- Tokens: none +- Binding: tried third layer blur `1.6rem` → `1.2rem`. Reverted to `0 1.2rem 1.6rem -0.4rem` `shadowMedium`. +- Why: Zack: 12px was a look, not a keep. Light-only quieting is the palette lever (`shadowMedium` 8% vs dark 28%), not shared geometry. +- Out of scope: retuning shadow opacities; dropping the third layer. +- For Emre: Stack unchanged. Trial only. + +### 2026-09-06 — Dropdown shadow C, loom split by mode +- Lift: binding (geometry, same tokens) +- Status: applied locally (trial) +- Modes: both; third layer splits +- Tokens: none +- Binding: `floatingSurfaceStyles` (and Monaco / Quick Vis copies): + - Shared heel: `0 1px 2px 0` `shadowSoft` + - Shared weight: `0 4px 6px -2px` `shadowMedium` + - Light loom: `0 8px 8px -4px` `shadowSubtle` (Figma `1957:130606`) + - Dark loom: `0 12px 16px -4px` `shadowMedium` (kept the old third layer; "126" read as 16) +- Neighbors: SelectMenu / DropdownMenu / Popover / MCP pair / theme + AI model. Modals unchanged. +- Why: Zack: C in both, loom quieter/tighter in light only. Dark still needs the 16px Medium halo. +- Out of scope: retuning shadow opacities; instance hover card; `modalSurfaceStyles`. +- For Emre: Geometry split, not a palette retune. Light loom is Subtle so it does not stack two Mediums. + +### 2026-09-06 — Dark ghost danger type is Text, not Subtle +- Lift: binding +- Status: applied locally +- Modes: dark (light kept) +- Tokens: none. `statusDangerSubtle` stays `#ff8080`. +- Binding: `dangerGhost` hover type `statusDangerSubtle` → `statusDanger` (`#ff3333`) in dark. Reset Provider hover type follows. Light hover stays Subtle on the 40% wash. +- Neighbors: rest `dangerGhost` still Contrast `#ff4d4d` + Surface. Solid `danger` unchanged. +- Why: Figma `1948:123286` — Reset Provider type is `#ff3333`, not the 40%-wash Subtle. +- Out of scope: retuning Subtle; light ghost danger; wash opacities. +- For Emre: Subtle remains type-on-40%. Dark ghost hover uses Text. + +### 2026-09-06 — In-track maximize hover matches Table/Chart +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: none +- Binding: `ViewIconButton` (maximize / reset-zoom inside `NotebookViewToggle`) light hover `interactionNeutralHover` → `interactionHover`. Same wash as `SegmentedControlButton`. Dark stays `surfaceRaised`. +- Neighbors: track `controlTrack` `#e8eaee`. NeutralHover `#e2e5ea` on that track vanished. Outside-cell Maximize / More stay `surfaceBase` on the raised cell. +- Why: Zack: in-track hover must match Table/Chart, not the ghost icons outside the control. +- Out of scope: CellIconButton; retuning NeutralHover. +- For Emre: In-track hover is the ink wash, not a surface step on the chip track. + --- ## Parking lot — not logged as decisions yet Use this for hunches until they become an entry. -- Brand core (`contentAccent`, `actionPrimary`, `contentObject` in light) may be overused. 2026-09-01 neutralized fields, lists, search, keyboard focus, dropdown open state, grid hover/selection/resize, and column names. Still branded: rail icons, tree glyphs, `SelectMenu` check, tab underlines, notebook cell focus + title glyphs, AI composer/login `$tone="accent"`, checkboxes/switches. Treat leftovers as **binding**. +- Brand core is QDB Pink, split by mode: dark 300/700 (`#f0428b` / `#bd0f58`), light 700/800 (`#bd0f58` / `#8e0b42`). Still branded: rail, tabs, cell focus, `SelectMenu` check, AI `$tone="accent"`. Danger is the pure-red family (dark bright ramp; light `#ce1717` + contrast steps). `contentObject` unused. Treat leftovers as **binding**. - `interactionNeutral` / `controlTrack` leftover `#d9dce2` — addressed 2026-09-01 (table/chart chips). - New surface roles: only if two neighbors with *different jobs* are forced to share a token. Propose the meaning first (`surfaceSomething`), then values in both themes. diff --git a/src/components/AIStatusIndicator/AssistantModesCompact.tsx b/src/components/AIStatusIndicator/AssistantModesCompact.tsx index 37c3134f6..a8c0b7474 100644 --- a/src/components/AIStatusIndicator/AssistantModesCompact.tsx +++ b/src/components/AIStatusIndicator/AssistantModesCompact.tsx @@ -11,6 +11,7 @@ import { type OperationHistory, } from "../../providers/AIStatusProvider" import { color } from "../../utils" +import { statusInfoFocus } from "../../theme" import { BrainIcon } from "../SetupAIAssistant/BrainIcon" import { buildOperationSections, @@ -121,7 +122,8 @@ const ModeHeaderTop = styled.div<{ } &:focus-visible { - outline: 1px solid ${color("borderStrong")}; + outline: 1px solid + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } diff --git a/src/components/Badge/index.tsx b/src/components/Badge/index.tsx index 932f9e30c..0b10c2e94 100644 --- a/src/components/Badge/index.tsx +++ b/src/components/Badge/index.tsx @@ -68,7 +68,7 @@ const getTone = (variant: BadgeVariant, theme: DefaultTheme) => { info: theme.color.statusInfo, success: theme.color.statusSuccess, warning: theme.color.statusWarning, - danger: theme.color.statusDanger, + danger: theme.color.statusDangerContrast, } return tones[variant] } diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 0eefb05d1..aa66e915d 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -8,6 +8,7 @@ import { makeButtonVariant } from "./variants" import { brandLinearGradientHorizontal, brandLinearGradientVertical, + statusInfoFocus, } from "../../theme" import { BUTTON_HEIGHTS } from "./tokens" @@ -185,7 +186,8 @@ export const ButtonBase = styled.button.attrs<{ filter 120ms ease; &&:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.borderStrong}; + outline: 1px solid + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } @@ -194,7 +196,7 @@ export const ButtonBase = styled.button.attrs<{ cursor: not-allowed; } - &&:active:not(:disabled):not([aria-disabled="true"]) { + &&:active:not(:disabled):not([aria-disabled="true"]):not([role="tab"]) { filter: brightness(0.9); } ` diff --git a/src/components/Button/variants.ts b/src/components/Button/variants.ts index b870f0e85..18ef2c978 100644 --- a/src/components/Button/variants.ts +++ b/src/components/Button/variants.ts @@ -118,14 +118,14 @@ const themes: { }, dangerGhost: { normal: { - background: "transparent", + background: "statusDangerSurface", border: "transparent", - color: "statusDanger", + color: "statusDangerContrast", }, hover: { - background: "statusDangerSurface", + background: "statusDangerSurfaceHover", border: "transparent", - color: "statusDanger", + color: "statusDangerSubtle", }, disabled: { background: "transparent", @@ -135,14 +135,14 @@ const themes: { }, danger: { normal: { - background: "statusDangerSurface", - border: "transparent", - color: "statusDangerStrong", + background: "statusDangerStrong", + border: "statusDangerStrong", + color: "contentInverse", }, hover: { - background: "statusDangerSurfaceHover", - border: "transparent", - color: "statusDangerStrong", + background: "statusDanger", + border: "statusDanger", + color: "contentInverse", }, disabled: { background: "surfaceRaised", @@ -201,7 +201,10 @@ export const makeButtonVariant = (variant: ButtonVariant) => { variant === "ghost" && t.mode === "light" ? t.color.interactionHover : t.color[theme.hover.background]}; - color: ${getColor(theme.hover.color)}; + color: ${({ theme: t }) => + variant === "dangerGhost" && t.mode === "dark" + ? t.color.statusDanger + : t.color[theme.hover.color]}; border-color: ${getColor(theme.hover.border)}; } diff --git a/src/components/Checkbox/index.tsx b/src/components/Checkbox/index.tsx index fdab2c0eb..00b444219 100644 --- a/src/components/Checkbox/index.tsx +++ b/src/components/Checkbox/index.tsx @@ -1,6 +1,7 @@ import React, { forwardRef } from "react" import { Check } from "@phosphor-icons/react" import styled from "styled-components" +import { statusInfoFocus } from "../../theme" type Props = React.InputHTMLAttributes @@ -42,13 +43,12 @@ const NativeCheckbox = styled.input` cursor: pointer; &:not(:checked):not(:disabled):hover + ${Indicator} { - border-color: ${({ theme }) => theme.color.borderAccentStrong}; background: ${({ theme }) => theme.color.controlSurfaceHover}; } &:checked + ${Indicator} { - border-color: ${({ theme }) => theme.color.contentAccent}; - background: ${({ theme }) => theme.color.contentAccent}; + border-color: ${({ theme }) => theme.color.statusInfoControl}; + background: ${({ theme }) => theme.color.statusInfoControl}; svg { opacity: 1; @@ -57,14 +57,13 @@ const NativeCheckbox = styled.input` } &:checked:not(:disabled):hover + ${Indicator} { - border-color: ${({ theme }) => theme.color.contentAccentStrong}; - background: ${({ theme }) => theme.color.contentAccentStrong}; + border-color: ${({ theme }) => theme.color.statusInfoControl}; + background: ${({ theme }) => theme.color.statusInfoControl}; } &:focus-visible + ${Indicator} { - border-color: ${({ theme }) => theme.color.actionPrimaryHover}; - box-shadow: 0 0 0 0.2rem - ${({ theme }) => theme.color.interactionAccentActive}; + outline: 1px solid ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; + outline-offset: 2px; } &:disabled { diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 8c351d726..744053a00 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -13,10 +13,10 @@ export type InputStyleProps = { type InputProps = React.InputHTMLAttributes & InputStyleProps const errorStyle = css` - border-color: ${({ theme }) => theme.color.statusDanger}; + border-color: ${({ theme }) => theme.color.statusDangerStrong}; background-color: ${({ theme }) => theme.color.statusDangerSurface}; &:focus { - border-color: ${({ theme }) => theme.color.statusDanger}; + border-color: ${({ theme }) => theme.color.statusDangerStrong}; background: ${({ theme }) => theme.color.statusDangerSurface}; } ` diff --git a/src/components/ResultGrid/dimensions.ts b/src/components/ResultGrid/dimensions.ts index d59bdbb25..e5daadaa9 100644 --- a/src/components/ResultGrid/dimensions.ts +++ b/src/components/ResultGrid/dimensions.ts @@ -12,6 +12,7 @@ export const MAX_COLUMN_WIDTH_BOUNDS = { } export const CELL_FONT_SIZE_PX = 13 export const HEADER_NAME_FONT_SIZE_PX = 14 +export const HEADER_NAME_FONT_WEIGHT = 600 export const HEADER_TYPE_FONT_SIZE_PX = 10 export const CELL_PADDING_PX = 12 diff --git a/src/components/ResultGrid/inlineGridUtils.ts b/src/components/ResultGrid/inlineGridUtils.ts index 53c7f7058..d312d5743 100644 --- a/src/components/ResultGrid/inlineGridUtils.ts +++ b/src/components/ResultGrid/inlineGridUtils.ts @@ -4,6 +4,7 @@ import { theme } from "../../theme" import { CELL_FONT_SIZE_PX, HEADER_NAME_FONT_SIZE_PX, + HEADER_NAME_FONT_WEIGHT, HEADER_TYPE_FONT_SIZE_PX, CELL_PADDING_PX, CELL_BORDER_PX, @@ -28,7 +29,7 @@ const HEADER_CHROME_PX = const CELL_CHROME_PX = CELL_PADDING_PX + CELL_BORDER_PX const CELL_FONT = `${CELL_FONT_SIZE_PX}px ${theme.fontMonospace}` -const HEADER_NAME_FONT = `${HEADER_NAME_FONT_SIZE_PX}px ${theme.font}` +const HEADER_NAME_FONT = `${HEADER_NAME_FONT_WEIGHT} ${HEADER_NAME_FONT_SIZE_PX}px ${theme.font}` const HEADER_TYPE_FONT = `${HEADER_TYPE_FONT_SIZE_PX}px ${theme.font}` const TEXT_MEASUREMENT_SAFETY_PX = 4 diff --git a/src/components/ResultGrid/styles.ts b/src/components/ResultGrid/styles.ts index b5d718d15..5e1ff5115 100644 --- a/src/components/ResultGrid/styles.ts +++ b/src/components/ResultGrid/styles.ts @@ -9,6 +9,7 @@ import { HEADER_GAP_PX, HEADER_HEIGHT, HEADER_NAME_FONT_SIZE_PX, + HEADER_NAME_FONT_WEIGHT, HEADER_PADDING_PX, HEADER_TYPE_FONT_SIZE_PX, ROW_HEIGHT, @@ -86,6 +87,7 @@ export const HeaderName = styled.span` text-overflow: ellipsis; min-width: 0; font-size: ${HEADER_NAME_FONT_SIZE_PX}px; + font-weight: ${HEADER_NAME_FONT_WEIGHT}; ` export const HeaderType = styled.span` diff --git a/src/components/SegmentedControl/index.tsx b/src/components/SegmentedControl/index.tsx index e7fc25ae3..a611312de 100644 --- a/src/components/SegmentedControl/index.tsx +++ b/src/components/SegmentedControl/index.tsx @@ -14,7 +14,7 @@ import { createLiquidLensMap } from "../LiquidGlass/createLiquidLensMap" export type SegmentedControlTone = "neutral" | "success" | "info" -export type SegmentedControlActiveTone = "accent" | "neutral" +export type SegmentedControlActiveTone = "accent" | "info" | "neutral" export type SegmentedControlSize = "xs" | "sm" | "md" @@ -389,6 +389,16 @@ export const SegmentedControlButton = styled( ` } + if ($activeTone === "info") { + return css` + &&, + &&:hover:not(:disabled):not([aria-disabled="true"]) { + background: ${theme.color.statusInfoSurface}; + color: ${theme.color.statusInfoSubtle}; + } + ` + } + return css` && { background: ${theme.color.interactionAccentActive}; diff --git a/src/components/SetupAIAssistant/SettingsModal.tsx b/src/components/SetupAIAssistant/SettingsModal.tsx index 0a63e7999..16cf25a2d 100644 --- a/src/components/SetupAIAssistant/SettingsModal.tsx +++ b/src/components/SetupAIAssistant/SettingsModal.tsx @@ -316,6 +316,28 @@ const ValidateRemoveButton = styled(Button).attrs({ variant: "secondary" })` gap: 0.8rem; ` +const ResetProviderButton = styled(Button).attrs({ variant: "ghost" })` + && { + color: ${({ theme }) => theme.color.statusDanger}; + } + + && svg { + color: ${({ theme }) => theme.color.statusDanger}; + } + + &&:hover:not(:disabled):not([aria-disabled="true"]) { + background: ${({ theme }) => + theme.mode === "light" + ? "rgba(189, 40, 40, 0.1)" + : "rgba(219, 36, 36, 0.3)"}; + color: ${({ theme }) => theme.color.statusDanger}; + } + + &&:hover:not(:disabled):not([aria-disabled="true"]) svg { + color: ${({ theme }) => theme.color.statusDanger}; + } +` + const ModelsPlaceholder = styled(Box).attrs({ flexDirection: "column", gap: "1rem", @@ -1281,15 +1303,14 @@ export const SettingsModal = ({ open, onOpenChange }: SettingsModalProps) => { /> - + diff --git a/src/components/Sidebar/navigation.tsx b/src/components/Sidebar/navigation.tsx index 0d640f4bc..45b554307 100644 --- a/src/components/Sidebar/navigation.tsx +++ b/src/components/Sidebar/navigation.tsx @@ -18,6 +18,11 @@ export const Navigation = styled(PrimaryToggleButton)` color: ${({ selected, theme }) => selected ? theme.color.contentAccent : theme.color.contentSecondary}; + &&[aria-pressed="true"] { + background: ${({ theme }) => theme.color.interactionAccentActive}; + color: ${({ theme }) => theme.color.contentAccent}; + } + &:hover:not([disabled]) { color: ${({ selected, theme }) => selected ? theme.color.contentAccent : theme.color.contentPrimary}; @@ -27,6 +32,11 @@ export const Navigation = styled(PrimaryToggleButton)` : theme.color.interactionHover}; } + &&[aria-pressed="true"]:hover:not([disabled]) { + background: ${({ theme }) => theme.color.interactionAccentActive}; + color: ${({ theme }) => theme.color.contentAccent}; + } + & > span { margin-left: 0 !important; } diff --git a/src/components/Switch/index.tsx b/src/components/Switch/index.tsx index 463d07d93..50a17a9d0 100644 --- a/src/components/Switch/index.tsx +++ b/src/components/Switch/index.tsx @@ -1,6 +1,7 @@ import React from "react" import styled from "styled-components" import * as SwitchPrimitive from "@radix-ui/react-switch" +import { statusInfoFocus } from "../../theme" type Props = { className?: string @@ -15,56 +16,65 @@ type Props = { "aria-label"?: string } +const track = { + sm: { width: "36px", height: "18px", radius: "7px", travel: "12px" }, + md: { width: "44px", height: "20px", radius: "8px", travel: "16px" }, +} as const + +const thumb = { + sm: { width: "20px", height: "14px", radius: "5px" }, + md: { width: "24px", height: "16px", radius: "6px" }, +} as const + +const restStroke = (mode: "light" | "dark") => + mode === "light" ? "rgba(28, 32, 41, 0.25)" : "rgba(255, 255, 255, 0.15)" + +const restTrack = (theme: { + mode: "light" | "dark" + color: { controlTrack: string } +}) => (theme.mode === "light" ? "#c9cdd4" : theme.color.controlTrack) + const Root = styled(SwitchPrimitive.Root)<{ - $tone: "accent" | "success" $size: "sm" | "md" }>` display: inline-flex; - align-items: center; + align-items: stretch; justify-content: flex-start; + flex-shrink: 0; padding: 2px; - width: ${({ $size }) => ($size === "sm" ? "32px" : "36px")}; - height: ${({ $size }) => ($size === "sm" ? "18px" : "20px")}; - border-radius: 20px; - border: 1px solid ${({ theme }) => theme.color.borderSubtle}; + width: ${({ $size }) => track[$size].width}; + height: ${({ $size }) => track[$size].height}; + border-radius: ${({ $size }) => track[$size].radius}; + border: none; + box-shadow: inset 0 0 0 0.5px ${({ theme }) => restStroke(theme.mode)}; appearance: none; position: relative; + overflow: hidden; transition: background-color 120ms ease, - border-color 120ms ease, + box-shadow 120ms ease, opacity 120ms ease; cursor: pointer; - background: ${({ theme }) => theme.color.controlTrack}; + background: ${({ theme }) => restTrack(theme)}; &:hover:not([data-disabled]) { - border-color: ${({ theme }) => theme.color.borderAccent}; + box-shadow: inset 0 0 0 0.5px ${({ theme }) => restStroke(theme.mode)}; } &:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.contentAccent}; + outline: 1px solid + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } &[data-state="checked"] { - background: ${({ $tone, theme }) => - $tone === "success" - ? theme.color.statusSuccessStrong - : theme.color.contentAccent}; - border-color: ${({ $tone, theme }) => - $tone === "success" - ? theme.color.statusSuccess - : theme.color.contentAccent}; + background: ${({ theme }) => theme.color.statusSuccessFill}; + box-shadow: inset 0 0 0 0.5px ${({ theme }) => restStroke(theme.mode)}; } &[data-state="checked"]:hover:not([data-disabled]) { - background: ${({ $tone, theme }) => - $tone === "success" - ? theme.color.statusSuccessStrong - : theme.color.contentAccentStrong}; - border-color: ${({ $tone, theme }) => - $tone === "success" - ? theme.color.statusSuccess - : theme.color.contentAccentStrong}; + background: ${({ theme }) => theme.color.statusSuccessFill}; + box-shadow: inset 0 0 0 0.5px ${({ theme }) => restStroke(theme.mode)}; } &[data-disabled], @@ -76,12 +86,10 @@ const Root = styled(SwitchPrimitive.Root)<{ const StyledThumb = styled(SwitchPrimitive.Thumb)<{ $size: "sm" | "md" }>` display: block; - width: ${({ $size }) => ($size === "sm" ? "12px" : "14px")}; - height: ${({ $size }) => ($size === "sm" ? "12px" : "14px")}; - border: 1px solid ${({ theme }) => theme.color.borderDefault}; - background-color: ${({ theme }) => theme.color.controlKnob}; - border-radius: 100%; - box-shadow: 0 1px 3px ${({ theme }) => theme.color.shadowSoft}; + width: ${({ $size }) => thumb[$size].width}; + height: ${({ $size }) => thumb[$size].height}; + background-color: ${({ theme }) => theme.color.contentInverse}; + border-radius: ${({ $size }) => thumb[$size].radius}; transition: transform 120ms ease, background-color 120ms ease; @@ -89,8 +97,7 @@ const StyledThumb = styled(SwitchPrimitive.Thumb)<{ $size: "sm" | "md" }>` will-change: transform; &[data-state="checked"] { - background-color: ${({ theme }) => theme.color.contentInverse}; - transform: translateX(16px); + transform: translateX(${({ $size }) => track[$size].travel}); } &[data-disabled] { @@ -106,7 +113,6 @@ export const Switch = ({ dataHook, id, ariaDescribedBy, - tone = "accent", size = "md", "aria-label": ariaLabel, }: Props) => ( @@ -119,7 +125,6 @@ export const Switch = ({ id={id} aria-describedby={ariaDescribedBy} aria-label={ariaLabel} - $tone={tone} $size={size} > diff --git a/src/components/TabButton/index.tsx b/src/components/TabButton/index.tsx index f8a512c19..830932005 100644 --- a/src/components/TabButton/index.tsx +++ b/src/components/TabButton/index.tsx @@ -23,6 +23,11 @@ export const TabButton = styled(ButtonBase).attrs( font-size: ${({ theme }) => theme.fontSize.sm}; font-weight: 500; line-height: 1.15; + filter: none; + transition: + background-color 120ms ease, + border-color 120ms ease, + color 120ms ease; } &&:hover:not(:disabled) { @@ -30,6 +35,10 @@ export const TabButton = styled(ButtonBase).attrs( color: ${({ theme }) => theme.color.contentPrimary}; } + &&:active:not(:disabled):not([aria-disabled="true"]) { + filter: none; + } + ${({ $active, theme }) => $active && css` @@ -40,8 +49,10 @@ export const TabButton = styled(ButtonBase).attrs( font-weight: 600; } - &&:hover:not(:disabled) { + &&:hover:not(:disabled), + &&:active:not(:disabled):not([aria-disabled="true"]) { background: ${theme.color.surfaceRaised}; + filter: none; } `} ` diff --git a/src/components/ToggleButton/index.tsx b/src/components/ToggleButton/index.tsx index b13710d05..61861f823 100644 --- a/src/components/ToggleButton/index.tsx +++ b/src/components/ToggleButton/index.tsx @@ -28,7 +28,10 @@ import styled from "styled-components" import type { FontSize } from "../../types" import { ButtonProps } from "../Button" -import { SegmentedControlButton } from "../SegmentedControl" +import { + SegmentedControlButton, + type SegmentedControlActiveTone, +} from "../SegmentedControl" type Direction = "top" | "right" | "bottom" | "left" @@ -54,6 +57,7 @@ type Props = Readonly<{ direction: Direction selected: boolean readOnly?: boolean + activeTone?: SegmentedControlActiveTone }> & ButtonProps @@ -86,7 +90,7 @@ const PrimaryToggleButtonStyled = styled(SegmentedControlButton)` ` const PrimaryToggleButtonWithRef = ( - props: RenderRefProps, + { activeTone, ...props }: RenderRefProps, ref: Ref, ) => ( ) diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx index 9eee415fe..df215e8cb 100644 --- a/src/components/Tooltip/index.tsx +++ b/src/components/Tooltip/index.tsx @@ -106,7 +106,14 @@ const ArrowWithBorder = React.forwardRef((props, ref) => { viewBox="0 0 14 7" preserveAspectRatio="none" > - + $maxWidth ?? "460px"}; padding: 1rem; - background: ${color("surfaceInset")}; + background: ${({ theme }) => + theme.mode === "light" + ? theme.color.surfaceOverlay + : theme.color.surfaceInset}; border: 1px solid ${color("borderDefault")}; border-radius: 6px; z-index: ${TOOLTIP_Z_INDEX}; diff --git a/src/components/TopBar/InstanceSettingsPopper.tsx b/src/components/TopBar/InstanceSettingsPopper.tsx index 7ee637d9a..ff9fc4044 100644 --- a/src/components/TopBar/InstanceSettingsPopper.tsx +++ b/src/components/TopBar/InstanceSettingsPopper.tsx @@ -12,6 +12,7 @@ import { import { Preferences, InstanceType } from "../../utils/questdb/types" import { menuContainerStyles } from "../menuStyles" import { InstanceTypeIcon } from "./InstanceTypeIcon" +import { statusInfoFocus } from "../../theme" const Wrapper = styled.div` ${menuContainerStyles} @@ -154,7 +155,8 @@ const ColorSlider = styled.input.attrs({ type: "range", min: 0, max: 255 })` } &:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.borderStrong}; + outline: 1px solid + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } diff --git a/src/components/TopBar/toolbar.tsx b/src/components/TopBar/toolbar.tsx index 530ce7189..66995aaea 100644 --- a/src/components/TopBar/toolbar.tsx +++ b/src/components/TopBar/toolbar.tsx @@ -53,7 +53,10 @@ const CustomTooltipWrapper = styled.div` min-width: 26rem; max-width: min(36rem, calc(100vw - 2rem)); overflow: hidden; - background: ${({ theme }) => theme.color.surfaceInset}; + background: ${({ theme }) => + theme.mode === "light" + ? theme.color.surfaceOverlay + : theme.color.surfaceInset}; border: 1px solid ${({ theme }) => theme.color.borderDefault}; border-radius: 0.8rem; box-shadow: diff --git a/src/components/overlayStyles.ts b/src/components/overlayStyles.ts index df6cc447e..39c685636 100644 --- a/src/components/overlayStyles.ts +++ b/src/components/overlayStyles.ts @@ -2,17 +2,20 @@ import { css } from "styled-components" /** * Compact surfaces anchored to a trigger: dropdowns, popovers and pickers. - * Geometry and elevation must stay identical so changing controls does not - * change the perceived layer of the UI. + * Heel + weight are shared. The loom is quieter and tighter in light. */ export const floatingSurfaceStyles = css` background-color: ${({ theme }) => theme.color.surfaceOverlay}; border: 1px solid ${({ theme }) => theme.color.borderDefault}; border-radius: 0.6rem; - box-shadow: - 0 0.2rem 0.2rem -0.1rem ${({ theme }) => theme.color.shadowSubtle}, - 0 0.4rem 0.6rem -0.2rem ${({ theme }) => theme.color.shadowSubtle}, - 0 1.2rem 1.6rem -0.4rem ${({ theme }) => theme.color.shadowMedium}; + box-shadow: ${({ theme }) => + theme.mode === "light" + ? `0 0.1rem 0.2rem 0 ${theme.color.shadowSoft}, + 0 0.4rem 0.6rem -0.2rem ${theme.color.shadowMedium}, + 0 0.8rem 0.8rem -0.4rem ${theme.color.shadowSubtle}` + : `0 0.1rem 0.2rem 0 ${theme.color.shadowSoft}, + 0 0.4rem 0.6rem -0.2rem ${theme.color.shadowMedium}, + 0 1.2rem 1.6rem -0.4rem ${theme.color.shadowMedium}`}; color: ${({ theme }) => theme.color.contentPrimary}; outline: none; ` diff --git a/src/modules/OAuth2/views/login.tsx b/src/modules/OAuth2/views/login.tsx index 49940ac3a..47233fc05 100644 --- a/src/modules/OAuth2/views/login.tsx +++ b/src/modules/OAuth2/views/login.tsx @@ -160,6 +160,7 @@ const ErrorContainer = styled.div` border-radius: 0.8rem; border: 1.5px solid ${({ theme }) => theme.color.statusDangerMuted}; border-left: 6px solid ${({ theme }) => theme.color.statusDangerMuted}; + background: ${({ theme }) => theme.color.statusDangerSurface}; ` const RedirectErrorContainer = styled(ErrorContainer)` diff --git a/src/providers/MCPBridgeProvider/PairingConsentModal.tsx b/src/providers/MCPBridgeProvider/PairingConsentModal.tsx index 95fa501fe..3e5b9dbba 100644 --- a/src/providers/MCPBridgeProvider/PairingConsentModal.tsx +++ b/src/providers/MCPBridgeProvider/PairingConsentModal.tsx @@ -141,14 +141,16 @@ const StatusRow = styled.div<{ $tone: "info" | "danger" | "warning" }>` width: 100%; background: ${({ theme, $tone }) => $tone === "danger" - ? `${theme.color.statusDanger}1f` + ? theme.color.statusDangerSurface : $tone === "warning" ? theme.color.statusWarningSurface : theme.color.surfaceInset}; color: ${({ theme, $tone }) => - $tone === "info" - ? theme.color.contentSecondary - : theme.color.contentPrimary}; + $tone === "danger" + ? theme.color.statusDangerContrast + : $tone === "info" + ? theme.color.contentSecondary + : theme.color.contentPrimary}; padding: 0.8rem 2.4rem; @@ -164,7 +166,10 @@ const StatusRow = styled.div<{ $tone: "info" | "danger" | "warning" }>` } strong { - color: ${({ theme }) => theme.color.contentPrimary}; + color: ${({ theme, $tone }) => + $tone === "danger" + ? theme.color.statusDangerContrast + : theme.color.contentPrimary}; font-weight: 600; } ` diff --git a/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx b/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx index eb974d5c0..8804ebcde 100644 --- a/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx +++ b/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx @@ -6,6 +6,7 @@ import { TrashSimpleIcon, } from "@phosphor-icons/react" import { color } from "../../../utils" +import { statusInfoFocus } from "../../../theme" import type { ConversationMeta } from "../../../store/db" import { trackEvent } from "../../../modules/ConsoleEventTracker" import { ConsoleEvent } from "../../../modules/ConsoleEventTracker/events" @@ -39,7 +40,8 @@ const Container = styled.div<{ $disabled?: boolean }>` } &:focus-visible { - outline: 1px solid ${color("borderStrong")}; + outline: 1px solid + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } ` @@ -248,7 +250,7 @@ export const ChatHistoryItem: React.FC = ({ + theme.mode === "light" ? theme.color.surfaceValue : "transparent"}; border: 1.5px dashed ${color("contentAccent")}; border-radius: 0.4rem; ` diff --git a/src/scenes/Editor/Notebook/NotebookToolbar.tsx b/src/scenes/Editor/Notebook/NotebookToolbar.tsx index 1ca9cfb1b..e593382c5 100644 --- a/src/scenes/Editor/Notebook/NotebookToolbar.tsx +++ b/src/scenes/Editor/Notebook/NotebookToolbar.tsx @@ -66,7 +66,7 @@ const NotebookGlyph = styled(NotebookIcon).attrs({ })` display: block; flex-shrink: 0; - color: ${color("contentObject")}; + color: ${color("statusInfo")}; ` const NotebookIdentity = styled(Box).attrs({ align: "center", gap: "1rem" })` diff --git a/src/scenes/Editor/Notebook/cellVirtualization/GridShimmer.tsx b/src/scenes/Editor/Notebook/cellVirtualization/GridShimmer.tsx index e02d5e8dd..64112f6f8 100644 --- a/src/scenes/Editor/Notebook/cellVirtualization/GridShimmer.tsx +++ b/src/scenes/Editor/Notebook/cellVirtualization/GridShimmer.tsx @@ -10,6 +10,7 @@ import { HEADER_GAP_PX, HEADER_HEIGHT, HEADER_NAME_FONT_SIZE_PX, + HEADER_NAME_FONT_WEIGHT, HEADER_PADDING_PX, HEADER_TYPE_FONT_SIZE_PX, ROW_HEIGHT, @@ -141,6 +142,7 @@ const HeaderCopyButtonSpacer = styled.div` const HeaderName = styled.span` color: ${color("statusInfo")}; font-size: ${HEADER_NAME_FONT_SIZE_PX}px; + font-weight: ${HEADER_NAME_FONT_WEIGHT}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/scenes/Editor/Notebook/cells/AddCellButton.tsx b/src/scenes/Editor/Notebook/cells/AddCellButton.tsx index c8a636c6a..c6b8bf04a 100644 --- a/src/scenes/Editor/Notebook/cells/AddCellButton.tsx +++ b/src/scenes/Editor/Notebook/cells/AddCellButton.tsx @@ -76,7 +76,7 @@ const AddButton = styled(ButtonBase)<{ ` : css` color: ${color("contentPrimary")}; - background: ${color("interactionNeutral")}; + background: ${color("interactionHover")}; `} } diff --git a/src/scenes/Editor/Notebook/cells/CellViewToggle.tsx b/src/scenes/Editor/Notebook/cells/CellViewToggle.tsx index 00c7993ac..7f0147b3e 100644 --- a/src/scenes/Editor/Notebook/cells/CellViewToggle.tsx +++ b/src/scenes/Editor/Notebook/cells/CellViewToggle.tsx @@ -43,7 +43,7 @@ const ViewIconButton = styled(IconButton)` &&:hover:not(:disabled):not([aria-disabled="true"]) { background: ${({ theme }) => theme.mode === "light" - ? theme.color.interactionNeutralHover + ? theme.color.interactionHover : theme.color.surfaceRaised}; } ` diff --git a/src/scenes/Editor/Notebook/index.tsx b/src/scenes/Editor/Notebook/index.tsx index e68221c2c..0034cb79a 100644 --- a/src/scenes/Editor/Notebook/index.tsx +++ b/src/scenes/Editor/Notebook/index.tsx @@ -20,6 +20,7 @@ import { } from "../../../utils/notebooks/notebookController" import { NotebookToolError } from "../../../utils/notebooks/notebookToolError" import { color } from "../../../utils" +import { withAlpha } from "../../../theme" import { NotebookProvider, useNotebookActions, @@ -142,8 +143,12 @@ const GridScrollContainer = styled.div<{ $suppressTransitions?: boolean }>` ${editorStageSurfaceStyles} .react-grid-item.react-grid-placeholder { - background: ${color("interactionNeutral")}; - opacity: 0.25; + background: ${({ theme }) => + withAlpha( + theme.color.contentPrimary, + theme.mode === "light" ? 0.08 : 0.1, + )}; + opacity: 1; } .react-grid-item { diff --git a/src/scenes/Editor/Notebook/result-table/styles.ts b/src/scenes/Editor/Notebook/result-table/styles.ts index 6593125f3..915612bda 100644 --- a/src/scenes/Editor/Notebook/result-table/styles.ts +++ b/src/scenes/Editor/Notebook/result-table/styles.ts @@ -139,9 +139,15 @@ export const ActionButton = styled(Button)` gap: 0.3rem; ` -export const FreezeToggle = styled(PrimaryToggleButton)` - flex-shrink: 0; - height: 2.8rem; - width: 3.2rem; - padding: 0; +export const FreezeToggle = styled(PrimaryToggleButton).attrs({ + activeTone: "info" as const, +})` + && { + flex-shrink: 0; + height: 2.8rem; + min-height: 2.8rem; + width: auto; + min-width: 2.8rem; + padding: 0 0.6rem; + } ` diff --git a/src/scenes/Footer/MCPBridgeStatus/PairPopover.tsx b/src/scenes/Footer/MCPBridgeStatus/PairPopover.tsx index c83536a17..ced2762ba 100644 --- a/src/scenes/Footer/MCPBridgeStatus/PairPopover.tsx +++ b/src/scenes/Footer/MCPBridgeStatus/PairPopover.tsx @@ -1,5 +1,5 @@ import React, { forwardRef, useEffect, useState } from "react" -import styled from "styled-components" +import styled, { css } from "styled-components" import { InfoIcon, ArrowRightIcon, @@ -109,6 +109,12 @@ const FullWidthInput = styled(Input)` font-family: ${({ theme }) => theme.fontMonospace}; ` +const StatusDetail = styled.span` + font-size: 1.2rem; + color: ${({ theme }) => theme.color.contentSecondary}; + word-break: break-word; +` + const StatusRow = styled.div<{ $tone: "info" | "danger" | "warning" }>` display: flex; align-items: flex-start; @@ -116,11 +122,9 @@ const StatusRow = styled.div<{ $tone: "info" | "danger" | "warning" }>` width: 100%; padding: 1.6rem; background: ${({ theme, $tone }) => - $tone === "danger" - ? `${theme.color.statusDanger}1f` - : $tone === "warning" - ? theme.color.statusWarningSurface - : theme.color.surfaceInset}; + $tone === "warning" + ? theme.color.statusWarningSurface + : theme.color.surfaceInset}; color: ${({ theme, $tone }) => $tone === "info" ? theme.color.contentSecondary @@ -129,17 +133,45 @@ const StatusRow = styled.div<{ $tone: "info" | "danger" | "warning" }>` & > svg { flex-shrink: 0; color: ${({ theme, $tone }) => - $tone === "danger" - ? theme.color.statusDanger - : $tone === "warning" - ? theme.color.statusWarning - : theme.color.contentAccentStrong}; + $tone === "warning" + ? theme.color.statusWarning + : theme.color.contentAccentStrong}; } strong { color: ${({ theme }) => theme.color.contentPrimary}; font-weight: 600; } + + ${({ $tone, theme }) => + $tone === "danger" && + css` + align-items: center; + padding: 1.2rem; + border-radius: ${theme.borderRadius}; + background: ${theme.mode === "light" + ? "rgba(189, 40, 56, 0.08)" + : "transparent"}; + border: 1px solid + ${theme.mode === "light" + ? theme.color.statusDanger + : theme.color.statusDangerMuted}; + border-left-width: 0.3rem; + color: ${theme.color.statusDanger}; + + & > svg { + color: ${theme.color.statusDanger}; + } + + strong { + color: ${theme.color.statusDanger}; + } + + ${StatusDetail} { + color: ${theme.color.statusDanger}; + font-weight: 600; + } + `} ` const AgentChangesRow = styled(StatusRow)` @@ -179,12 +211,6 @@ const StatusText = styled.div` line-height: 1.4; ` -const StatusDetail = styled.span` - font-size: 1.2rem; - color: ${({ theme }) => theme.color.contentSecondary}; - word-break: break-word; -` - const Footer = styled.div` display: flex; justify-content: space-between; @@ -440,6 +466,42 @@ export const MCPBridgePairPopover = forwardRef( }} disabled={isConnecting} /> + + {validationError && ( + + + + {validationError} + + + )} + + {showWsError && ( + + + + Could not connect to MCP server + + {lastError ?? + `MCP server stopped responding after ${MAX_RECONNECT_ATTEMPTS} attempts. Try again, or ask your coding agent for a fresh deep link.`} + + + + )} + + {showVersionMismatch && versionMismatch === "major" && ( + + + + )} )} @@ -464,35 +526,9 @@ export const MCPBridgePairPopover = forwardRef( )} - {!succeeded && validationError && ( - - - - {validationError} - - - )} - - {showWsError && ( - - - - Could not connect to MCP server - - {lastError ?? - `MCP server stopped responding after ${MAX_RECONNECT_ATTEMPTS} attempts. Try again, or ask your coding agent for a fresh deep link.`} - - - - )} - - {showVersionMismatch && versionMismatch && ( + {showVersionMismatch && versionMismatch === "minor" && ( diff --git a/src/scenes/Footer/MCPBridgeStatus/PermissionsSection.tsx b/src/scenes/Footer/MCPBridgeStatus/PermissionsSection.tsx index 3c4bb728a..9b7b6bcb0 100644 --- a/src/scenes/Footer/MCPBridgeStatus/PermissionsSection.tsx +++ b/src/scenes/Footer/MCPBridgeStatus/PermissionsSection.tsx @@ -63,7 +63,7 @@ const FieldLabel = styled.span` ` const RichTitle = styled.span` - font-size: 1.8rem; + font-size: 1.6rem; font-weight: 600; color: ${({ theme }) => theme.color.contentPrimary}; ` diff --git a/src/scenes/Footer/MCPBridgeStatus/index.tsx b/src/scenes/Footer/MCPBridgeStatus/index.tsx index 53ec1d92e..c25164067 100644 --- a/src/scenes/Footer/MCPBridgeStatus/index.tsx +++ b/src/scenes/Footer/MCPBridgeStatus/index.tsx @@ -10,6 +10,7 @@ import { useAgentChanges } from "./useAgentChanges" import { Tone, accentColor, deriveTone } from "./tone" import { trackEvent } from "../../../modules/ConsoleEventTracker" import { ConsoleEvent } from "../../../modules/ConsoleEventTracker/events" +import { statusInfoFocus } from "../../../theme" const pulse = keyframes` 0%, 100% { opacity: 1; } @@ -59,10 +60,7 @@ const Wrapper = styled(ButtonBase)` &:focus-visible { outline: 1px solid - ${(props) => - props.$tone === "idle" - ? props.theme.color.contentAccent - : accent(props)}; + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } diff --git a/src/scenes/Layout/warning.tsx b/src/scenes/Layout/warning.tsx index 9cbde94a3..72ac3ae53 100644 --- a/src/scenes/Layout/warning.tsx +++ b/src/scenes/Layout/warning.tsx @@ -5,6 +5,7 @@ import { Close, ErrorWarning, ExternalLink } from "../../components/icons" import { errorWorkarounds } from "../../utils/errorWorkarounds" import { IconButton } from "../../components" import { ErrorTag } from "utils" +import { statusInfoFocus } from "../../theme" const WarningsRoot = styled.div` display: flex; @@ -65,7 +66,8 @@ const WorkaroundLink = styled.a` } &:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.borderStrong}; + outline: 1px solid + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; border-radius: 0.2rem; } diff --git a/src/scenes/Result/index.tsx b/src/scenes/Result/index.tsx index 9ade21baf..d7cfeb296 100644 --- a/src/scenes/Result/index.tsx +++ b/src/scenes/Result/index.tsx @@ -50,6 +50,7 @@ import { PrimaryToggleButton, Text, Tooltip, + TOOLBAR_CONTROL_HEIGHT, } from "../../components" import { actions, selectors } from "../../store" import { color, ErrorResult } from "../../utils" @@ -71,7 +72,6 @@ import { ConsoleEvent } from "../../modules/ConsoleEventTracker/events" import { useLocalStorage } from "../../providers/LocalStorageProvider" import { ResultGridAdapter } from "./ResultGridAdapter" import { type PaginationFn } from "./usePagedDataSource" -import { CELL_FONT_SIZE_PX } from "../../components/ResultGrid" import { ResultChart } from "./ResultChart" const Root = styled.div` @@ -112,27 +112,36 @@ const ResultCountBadge = styled.span` align-items: baseline; gap: 0.4rem; color: ${({ theme }) => theme.color.contentSecondary}; - font-size: ${CELL_FONT_SIZE_PX}px; - line-height: 1.2; + font-family: ${({ theme }) => theme.font}; + font-size: ${({ theme }) => theme.fontSize.sm}; + font-weight: 400; + line-height: 1.56rem; white-space: nowrap; ` const ResultCount = styled.span` color: ${({ theme }) => theme.color.contentPrimary}; - font-family: ${({ theme }) => theme.fontMonospace}; - font-size: inherit; - font-variant-numeric: tabular-nums; + font-family: ${({ theme }) => theme.font}; + font-size: ${({ theme }) => theme.fontSize.lg}; font-weight: 600; + line-height: 2.14rem; + font-variant-numeric: tabular-nums; ` const TableFreezeColumnIcon = styled(TableFreezeColumn)` transform: scaleX(-1); ` -const StyledPrimaryToggleButton = styled(PrimaryToggleButton)` - padding: 0 1rem; - height: 3rem; - width: 4rem; +const StyledPrimaryToggleButton = styled(PrimaryToggleButton).attrs({ + activeTone: "info" as const, +})` + && { + height: ${TOOLBAR_CONTROL_HEIGHT}; + min-height: ${TOOLBAR_CONTROL_HEIGHT}; + width: auto; + min-width: ${TOOLBAR_CONTROL_HEIGHT}; + padding: 0 1.2rem; + } ` const DownloadButton = styled(Button)` diff --git a/src/scenes/Schema/Row/index.tsx b/src/scenes/Schema/Row/index.tsx index 08a130e2d..3add4c060 100644 --- a/src/scenes/Schema/Row/index.tsx +++ b/src/scenes/Schema/Row/index.tsx @@ -198,10 +198,6 @@ const StyledTitle = styled(Title)` background-color: ${({ theme }) => theme.color.contentAccentStrong}; color: ${({ theme }) => theme.color.contentInverse}; } - - svg { - color: ${color("contentAccent")}; - } ` const TableActions = styled.span` @@ -227,7 +223,7 @@ const Spacer = styled.span` ` const SortDownIcon = styled(SortDown)` - color: ${color("contentAccent")}; + color: ${color("contentMuted")}; margin-right: 0.8rem; flex-shrink: 0; ` @@ -280,7 +276,7 @@ const TypeIcon = styled.div` margin-right: 0.8rem; display: flex; align-items: center; - color: ${color("contentAccent")}; + color: ${color("contentMuted")}; ` const TYPE_ICONS = { diff --git a/src/scenes/Schema/TableDetailsDrawer/DetailsTab.tsx b/src/scenes/Schema/TableDetailsDrawer/DetailsTab.tsx index 712365d94..b1cffc2c5 100644 --- a/src/scenes/Schema/TableDetailsDrawer/DetailsTab.tsx +++ b/src/scenes/Schema/TableDetailsDrawer/DetailsTab.tsx @@ -85,6 +85,10 @@ const BaseTableLinkButton = styled(TextButton)` display: flex; align-items: center; gap: 0.4rem; + + &&:hover:not(:disabled):not([aria-disabled="true"]) { + background: ${({ theme }) => theme.color.interactionHover}; + } ` const MetricsGrid = styled.div<{ $columns: number }>` diff --git a/src/scenes/Schema/TableDetailsDrawer/ErrorBanner.tsx b/src/scenes/Schema/TableDetailsDrawer/ErrorBanner.tsx index 665759d1e..e46fa6e96 100644 --- a/src/scenes/Schema/TableDetailsDrawer/ErrorBanner.tsx +++ b/src/scenes/Schema/TableDetailsDrawer/ErrorBanner.tsx @@ -41,12 +41,12 @@ const TitleRow = styled.div` ` const RedText = styled(Text)` - color: ${({ theme }) => theme.color.statusDangerStrong}; + color: ${({ theme }) => theme.color.statusDanger}; ` const IconWrapper = styled.div` flex-shrink: 0; - color: ${({ theme }) => theme.color.statusDangerMuted}; + color: ${({ theme }) => theme.color.statusDangerStrong}; display: flex; align-items: center; justify-content: center; diff --git a/src/scenes/Schema/TableDetailsDrawer/HealthStatusLabel.tsx b/src/scenes/Schema/TableDetailsDrawer/HealthStatusLabel.tsx index 2c9abb616..6ed3cf2c7 100644 --- a/src/scenes/Schema/TableDetailsDrawer/HealthStatusLabel.tsx +++ b/src/scenes/Schema/TableDetailsDrawer/HealthStatusLabel.tsx @@ -18,7 +18,7 @@ const LabelContainer = styled.div<{ $severity: HealthSeverity }>` background: ${({ theme, $severity }) => { switch ($severity) { case "critical": - return `${theme.color.statusDanger}1F` + return theme.color.statusDangerSurface case "warning": return `${theme.color.statusWarning}1F` case "recovering": diff --git a/src/scenes/Schema/index.tsx b/src/scenes/Schema/index.tsx index 52b538bdf..285f166eb 100644 --- a/src/scenes/Schema/index.tsx +++ b/src/scenes/Schema/index.tsx @@ -115,7 +115,9 @@ const Content = styled(PaneContent)<{ ${({ _loading }) => _loading && loadingStyles}; ` -const ToolbarToggleButton = styled(PrimaryToggleButton)` +const ToolbarToggleButton = styled(PrimaryToggleButton).attrs({ + activeTone: "info" as const, +})` &&:not(:disabled) { width: auto; padding: 0 1rem; diff --git a/src/scenes/Schema/table-icon.tsx b/src/scenes/Schema/table-icon.tsx index 1c3e03d2a..7e91c02d6 100644 --- a/src/scenes/Schema/table-icon.tsx +++ b/src/scenes/Schema/table-icon.tsx @@ -22,8 +22,10 @@ const Root = styled.div<{ $size: string }>` height: ${({ $size }) => $size}; position: relative; flex-shrink: 0; + color: ${color("contentMuted")}; + svg { - color: ${color("contentAccent")}; + color: inherit; } ` @@ -56,12 +58,17 @@ const NonPartitionedTableIcon = ({ ) +const MutedGlyph = styled.svg` + color: ${color("contentMuted")}; + flex-shrink: 0; +` + export const MaterializedViewIcon = ({ size = DEFAULT_SIZE, }: { size?: string }) => ( - - + ) export const ViewIcon = ({ diff --git a/src/styles/_editor.scss b/src/styles/_editor.scss index 42d973421..b211401c3 100644 --- a/src/styles/_editor.scss +++ b/src/styles/_editor.scss @@ -360,9 +360,16 @@ .monaco-menu-container .monaco-scrollable-element { border-radius: 0.6rem; box-shadow: - 0 0.2rem 0.2rem -0.1rem theme-color(shadowSubtle), - 0 0.4rem 0.6rem -0.2rem theme-color(shadowSubtle), - 0 1.2rem 1.6rem -0.4rem theme-color(shadowSubtle) !important; + 0 0.1rem 0.2rem 0 theme-color(shadowSoft), + 0 0.4rem 0.6rem -0.2rem theme-color(shadowMedium), + 0 1.2rem 1.6rem -0.4rem theme-color(shadowMedium) !important; + + html[data-theme="light"] & { + box-shadow: + 0 0.1rem 0.2rem 0 theme-color(shadowSoft), + 0 0.4rem 0.6rem -0.2rem theme-color(shadowMedium), + 0 0.8rem 0.8rem -0.4rem theme-color(shadowSubtle) !important; + } } .monaco-editor .find-widget.visible { diff --git a/src/styles/_grid.scss b/src/styles/_grid.scss index 1ff23d74b..d55f22447 100644 --- a/src/styles/_grid.scss +++ b/src/styles/_grid.scss @@ -223,6 +223,7 @@ $drag-handle-margin: 2px; .qg-header-name { color: theme-color(contentPrimary) !important; font-size: medium; + font-weight: 600; } .qg-header-type { diff --git a/src/styles/_quick-vis.scss b/src/styles/_quick-vis.scss index 66d82dde9..f89e4c720 100644 --- a/src/styles/_quick-vis.scss +++ b/src/styles/_quick-vis.scss @@ -212,9 +212,16 @@ border: 1px solid theme-color(borderDefault); border-radius: 0.6rem; box-shadow: - 0 0.2rem 0.2rem -0.1rem theme-color(shadowSubtle), - 0 0.4rem 0.6rem -0.2rem theme-color(shadowSubtle), - 0 1.2rem 1.6rem -0.4rem theme-color(shadowSubtle); + 0 0.1rem 0.2rem 0 theme-color(shadowSoft), + 0 0.4rem 0.6rem -0.2rem theme-color(shadowMedium), + 0 1.2rem 1.6rem -0.4rem theme-color(shadowMedium); + + html[data-theme="light"] & { + box-shadow: + 0 0.1rem 0.2rem 0 theme-color(shadowSoft), + 0 0.4rem 0.6rem -0.2rem theme-color(shadowMedium), + 0 0.8rem 0.8rem -0.4rem theme-color(shadowSubtle); + } color: theme-color(contentPrimary); font-family: theme-font(sans); font-size: 1.4rem; diff --git a/src/styles/lib/_react-chrome-tabs.scss b/src/styles/lib/_react-chrome-tabs.scss index 03abaef0c..0db8e7f4e 100644 --- a/src/styles/lib/_react-chrome-tabs.scss +++ b/src/styles/lib/_react-chrome-tabs.scss @@ -56,7 +56,8 @@ filter 150ms ease; &:focus-visible { - outline: 1px solid theme-color(borderStrong); + outline: 1px solid + color-mix(in srgb, theme-color(statusInfo) 75%, transparent); outline-offset: 2px; } @@ -358,13 +359,13 @@ html[data-theme="light"] .chrome-tabs .chrome-tabs-overflow-shadow-right { } .chrome-tabs .chrome-tab .chrome-tab-favicon--notebook { - background-color: theme-color(contentObject); + background-color: theme-color(statusInfo); -webkit-mask-image: url("/assets/icon-notebook.svg"); mask-image: url("/assets/icon-notebook.svg"); } .chrome-tabs .chrome-tab .chrome-tab-favicon--metrics { - background-color: theme-color(contentObject); + background-color: theme-color(statusInfo); -webkit-mask-image: url("/assets/icon-chart.svg"); mask-image: url("/assets/icon-chart.svg"); } diff --git a/src/theme/global-styles.ts b/src/theme/global-styles.ts index a6cb491d9..39b953cb6 100644 --- a/src/theme/global-styles.ts +++ b/src/theme/global-styles.ts @@ -1,6 +1,7 @@ import { createGlobalStyle } from "styled-components" import { DocSearchStyles } from "./global-styles/docsearch" +import { statusInfoFocus } from "./index" export const GlobalStyle = createGlobalStyle` ${DocSearchStyles} @@ -84,7 +85,8 @@ export const GlobalStyle = createGlobalStyle` } button[data-button-variant="ghost"]:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.borderStrong}; + outline: 1px solid + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } @@ -97,7 +99,8 @@ export const GlobalStyle = createGlobalStyle` } *:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.borderStrong}; + outline: 1px solid + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } diff --git a/src/theme/global-styles/docsearch.ts b/src/theme/global-styles/docsearch.ts index 9f656815f..5c67d9c0b 100644 --- a/src/theme/global-styles/docsearch.ts +++ b/src/theme/global-styles/docsearch.ts @@ -3,6 +3,7 @@ import { css, type DefaultTheme } from "styled-components" import { makeButtonVariant } from "../../components/Button/variants" import { TOOLBAR_CONTROL_HEIGHT } from "../../components/Button/tokens" import { shortcutKeycapStyles } from "../../components/Key/styles" +import { statusInfoFocus } from "../index" const docSearchVariablePattern = /var\(--docsearch-([a-z0-9-]+)(?:,\s*[^)]+)?\)/g @@ -108,7 +109,8 @@ export const DocSearchStyles = css` } .DocSearch.DocSearch-Button:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.borderStrong}; + outline: 1px solid + ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } diff --git a/src/theme/index.ts b/src/theme/index.ts index bfff12b3a..391413cc3 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -90,8 +90,8 @@ export const darkColors: ColorShape = { contentSecondary: "#9da1ad", contentMuted: "#858995", contentDisabled: "#747985", - contentAccent: "#c94f74", - contentAccentStrong: "#cf1750", + contentAccent: "#f0428b", + contentAccentStrong: "#ee2b7c", contentObject: "#81d3f9", contentSearchMatch: "rgb(163, 127, 96)", @@ -99,15 +99,15 @@ export const darkColors: ColorShape = { borderSubtle: "rgba(255, 255, 255, 0.04)", borderDefault: "rgba(255, 255, 255, 0.13)", borderStrong: "#3d414d", - borderAccent: "rgba(201, 79, 116, 0.42)", - borderAccentStrong: "rgba(201, 79, 116, 0.52)", + borderAccent: "rgba(240, 66, 139, 0.42)", + borderAccentStrong: "rgba(240, 66, 139, 0.52)", // Interaction and controls interactionNeutral: "#32343e", interactionNeutralHover: "#292b35", interactionHover: "#ffffff0e", - interactionAccentHover: "rgba(201, 79, 116, 0.08)", - interactionAccentActive: "rgba(201, 79, 116, 0.15)", + interactionAccentHover: "rgba(240, 66, 139, 0.08)", + interactionAccentActive: "rgba(240, 66, 139, 0.10)", interactionGuide: "#6272a4", scrollbarThumb: "rgba(255, 255, 255, 0.13)", controlSurface: "#262833", @@ -116,16 +116,19 @@ export const darkColors: ColorShape = { controlKnob: "#f8f8f2", // Actions and status - actionPrimary: "#b81447", - actionPrimaryHover: "#cf1750", - statusDanger: "#ff6b73", - statusDangerStrong: "#fa4d56", - statusDangerMuted: "rgba(220, 40, 40, 0.72)", - statusDangerSurface: "rgba(220, 40, 40, 0.3)", - statusDangerSurfaceHover: "rgba(220, 40, 40, 0.4)", - statusDangerBorder: "rgba(255, 107, 115, 0.24)", + actionPrimary: "#bd0f58", + actionPrimaryHover: "#d41162", + statusDanger: "#ff3333", + statusDangerStrong: "#db2424", + statusDangerContrast: "#ff4d4d", + statusDangerSubtle: "#ff8080", + statusDangerMuted: "rgba(219, 36, 36, 0.72)", + statusDangerSurface: "rgba(219, 36, 36, 0.15)", + statusDangerSurfaceHover: "rgba(219, 36, 36, 0.4)", + statusDangerBorder: "rgba(219, 36, 36, 0.28)", statusSuccess: "#66bb6a", statusSuccessStrong: "#188a5d", + statusSuccessFill: "#3fa659", statusSuccessSurface: "rgba(102, 187, 106, 0.12)", statusSuccessBorder: "rgba(102, 187, 106, 0.24)", statusWarning: "#ffd54f", @@ -133,7 +136,9 @@ export const darkColors: ColorShape = { statusWarningSurfaceHover: "rgba(255, 213, 79, 0.16)", statusAttention: "#e3ce78", statusInfo: "#81d3f9", - statusInfoSurface: "rgba(129, 211, 249, 0.1)", + statusInfoSubtle: "#b2e7ff", + statusInfoControl: "#0c80f3", + statusInfoSurface: "rgba(178, 231, 255, 0.05)", statusFeature: "#a99de8", // Shadows @@ -149,10 +154,10 @@ export const darkColors: ColorShape = { glassEdge: "rgba(255, 255, 255, 0.22)", // Brand - brandGradientStart: "#e51a59", - brandGradientEnd: "#8a0f35", - aiGradientStart: "#d14671", - aiGradientEnd: "#892c6c", + brandGradientStart: "#e21269", + brandGradientEnd: "#8e0b42", + aiGradientStart: "#ee2b7c", + aiGradientEnd: "#8e0b42", // Result grid gridRow: "#17181d", @@ -238,15 +243,15 @@ export const lightColors: ColorShape = { surfaceOverlay: "#fafbfc", surfaceValue: "#ffffff", surfaceScrim: "rgba(27, 31, 39, 0.32)", - surfaceTabRail: "rgba(232, 234, 238, 0.94)", + surfaceTabRail: "rgba(244, 245, 247, 0.94)", // Content contentPrimary: "#1c2029", contentSecondary: "#505968", contentMuted: "#565f6e", contentDisabled: "#596271", - contentAccent: "#b81447", - contentAccentStrong: "#8a0f35", + contentAccent: "#bd0f58", + contentAccentStrong: "#8e0b42", contentObject: "#b81447", contentSearchMatch: "rgba(202, 139, 44, 0.32)", @@ -254,15 +259,15 @@ export const lightColors: ColorShape = { borderSubtle: "rgba(28, 32, 41, 0.07)", borderDefault: "rgba(28, 32, 41, 0.15)", borderStrong: "#b0b7c2", - borderAccent: "rgba(184, 20, 71, 0.42)", - borderAccentStrong: "rgba(184, 20, 71, 0.56)", + borderAccent: "rgba(189, 15, 88, 0.42)", + borderAccentStrong: "rgba(189, 15, 88, 0.56)", // Interaction and controls interactionNeutral: "#e8eaee", interactionNeutralHover: "#e2e5ea", interactionHover: "#1c202913", - interactionAccentHover: "rgba(184, 20, 71, 0.07)", - interactionAccentActive: "rgba(184, 20, 71, 0.13)", + interactionAccentHover: "rgba(189, 15, 88, 0.08)", + interactionAccentActive: "rgba(189, 15, 88, 0.13)", interactionGuide: "#56657f", scrollbarThumb: "#d7dbe3", controlSurface: "#f6f7f8", @@ -271,16 +276,19 @@ export const lightColors: ColorShape = { controlKnob: "#505968", // Actions and status - actionPrimary: "#8a0f35", - actionPrimaryHover: "#b81447", - statusDanger: "#bd2838", - statusDangerStrong: "#a81f2e", - statusDangerMuted: "rgba(189, 40, 56, 0.72)", - statusDangerSurface: "rgba(189, 40, 56, 0.12)", - statusDangerSurfaceHover: "rgba(189, 40, 56, 0.18)", - statusDangerBorder: "rgba(189, 40, 56, 0.28)", + actionPrimary: "#8e0b42", + actionPrimaryHover: "#bd0f58", + statusDanger: "#ce1717", + statusDangerStrong: "#ce1717", + statusDangerContrast: "#b81414", + statusDangerSubtle: "#8a0f0f", + statusDangerMuted: "rgba(206, 23, 23, 0.72)", + statusDangerSurface: "rgba(189, 40, 40, 0.15)", + statusDangerSurfaceHover: "rgba(219, 36, 36, 0.4)", + statusDangerBorder: "rgba(189, 40, 40, 0.28)", statusSuccess: "#067047", statusSuccessStrong: "#05603e", + statusSuccessFill: "#1f7a39", statusSuccessSurface: "rgba(8, 122, 80, 0.16)", statusSuccessBorder: "rgba(8, 122, 80, 0.28)", statusWarning: "#8a570f", @@ -288,7 +296,9 @@ export const lightColors: ColorShape = { statusWarningSurfaceHover: "rgba(145, 82, 15, 0.2)", statusAttention: "#745c00", statusInfo: "#176f87", - statusInfoSurface: "rgba(23, 111, 135, 0.1)", + statusInfoSubtle: "#159cc1", + statusInfoControl: "#0a88db", + statusInfoSurface: "rgba(21, 156, 193, 0.10)", statusFeature: "#6553aa", // Shadows @@ -304,10 +314,10 @@ export const lightColors: ColorShape = { glassEdge: "rgba(28, 32, 41, 0.22)", // Brand - brandGradientStart: "#b81447", - brandGradientEnd: "#5c0a24", - aiGradientStart: "#a92352", - aiGradientEnd: "#76184c", + brandGradientStart: "#bd0f58", + brandGradientEnd: "#8e0b42", + aiGradientStart: "#bd0f58", + aiGradientEnd: "#8e0b42", // Result grid gridRow: "#f4f5f7", @@ -377,6 +387,14 @@ const fontSize: FontSizeShape = { hg: "3rem", } +/** + * Keyboard focus ring: `statusInfo` at 75% (Figma 1943:115217 / 1943:117006). + * Offset rings use this wash; a 1px stroke that dies on `surfaceValue` should + * use solid `statusInfo` instead of a fourth token. + */ +export const statusInfoFocus = (statusInfo: string): string => + withAlpha(statusInfo, 0.75) + /** * Derive translucent renderer colors from semantic opaque roles. Keeping the * alpha at the consumer avoids multiplying theme tokens for every opacity. diff --git a/src/types/styled.d.ts b/src/types/styled.d.ts index 95b841afb..fa1805a7c 100644 --- a/src/types/styled.d.ts +++ b/src/types/styled.d.ts @@ -67,12 +67,15 @@ export type ColorShape = { actionPrimaryHover: string statusDanger: string statusDangerStrong: string + statusDangerContrast: string + statusDangerSubtle: string statusDangerMuted: string statusDangerSurface: string statusDangerSurfaceHover: string statusDangerBorder: string statusSuccess: string statusSuccessStrong: string + statusSuccessFill: string statusSuccessSurface: string statusSuccessBorder: string statusWarning: string @@ -80,6 +83,8 @@ export type ColorShape = { statusWarningSurfaceHover: string statusAttention: string statusInfo: string + statusInfoSubtle: string + statusInfoControl: string statusInfoSurface: string statusFeature: string statusAssistant: string From 504d3f2f9095785d39212f44f43fc0ad3c9c3623 Mon Sep 17 00:00:00 2001 From: Zack Nelson Date: Sun, 6 Sep 2026 23:36:22 +0100 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20continue=20theme=20steering=20?= =?UTF-8?q?=E2=80=94=20provider=20tabs=20and=20grid=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Light provider-tab hover sits lighter than the selected well. Result-grid selection uses a double hover wash and info cell chrome without retuning the shared gridSelection token. Co-authored-by: Cursor --- THEME-STEERING.md | 33 +++++++++++++++++++ src/components/ResultGrid/styles.ts | 33 +++++++++++++++---- .../SetupAIAssistant/SettingsModal.tsx | 13 +++++--- src/styles/_grid.scss | 23 ++++++++++--- 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/THEME-STEERING.md b/THEME-STEERING.md index 78126ec7e..a439f95d7 100644 --- a/THEME-STEERING.md +++ b/THEME-STEERING.md @@ -901,6 +901,39 @@ change shipped together, split them. - Out of scope: CellIconButton; retuning NeutralHover. - For Emre: In-track hover is the ink wash, not a surface step on the chip track. +### 2026-09-06 — Provider tab hover is one step below selected +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: none +- Binding: unselected `ProviderTab` hover `controlSurfaceHover` → `interactionNeutralHover`. Selected stays `interactionNeutral` (including on hover). +- Neighbors: selected Neutral (`#e8eaee` / `#32343e`). NeutralHover is the next step down (`#e2e5ea` / `#292b35`). Dark `controlSurfaceHover` equals Neutral, so hover matched selected. +- Why: Zack: hover should sit one step below the active well, both modes. +- Out of scope: selected fill; tab underline; StatusChip. +- For Emre: Same family as selected. Do not use `controlSurfaceHover` here. + +### 2026-09-06 — Light provider-tab hover is one more notch +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: none +- Binding: unselected `ProviderTab` hover in light → `surfaceBase` (`#eef0f3`). Dark stays NeutralHover. Selected stays Neutral. Specificity `&&&&` so TabButton hover cannot win. +- Neighbors: Overlay `#fafbfc` < Base `#eef0f3` < Neutral `#e8eaee`. NeutralHover and the 22% ink wash both sit *below* Neutral (darker). Light hover is a smaller recede than selected, matching dark's "between sidebar and selected" step. +- Why: Zack: 22% had impact but the wrong direction — hover must be lighter than active, not darker. +- Out of scope: retuning NeutralHover; inventing a surface. +- For Emre: Light hover is Base, not an ink wash. Do not fold this into NeutralHover (Table/Chart track). + +### 2026-09-06 — Result-grid selection uses info, double hover +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: none. `gridSelection` stays `#252830` / `#d8dce3` (Monaco still reads it). +- Binding: ResultGrid selected row stacks white 11% / ink 15% (exactly 2× `interactionHover`) over `gridRow`. Focused cell stacks `statusInfoSurface` over that fill, ring `statusInfo`. Copy-pulse follows the ring. `gridFocus` unused here. Legacy `.qg-c-active` matches via `html[data-theme]`. +- Neighbors: hover stays 1× `interactionHover` over Inset. Monaco suggest/list hover unchanged. Figma `1943:114709` / `1943:116498`. +- Why: Zack: selection fill is double hover; cell chrome is the info family. Palette retune of `gridSelection` would have moved Monaco — do not. +- Out of scope: retuning `statusInfo` / `statusInfoSurface` / `gridSelection`; `editorSelection`; notebook cell focus. +- For Emre: Grid selection wash is local to ResultGrid, not a token. Cell ring is `statusInfo`, not `gridFocus`. + --- ## Parking lot — not logged as decisions yet diff --git a/src/components/ResultGrid/styles.ts b/src/components/ResultGrid/styles.ts index 5e1ff5115..b9e39ab95 100644 --- a/src/components/ResultGrid/styles.ts +++ b/src/components/ResultGrid/styles.ts @@ -1,4 +1,5 @@ import styled, { css, keyframes } from "styled-components" +import { withAlpha } from "../../theme" import { color } from "../../utils" import { CopyButton } from "../CopyButton" import { @@ -17,6 +18,12 @@ import { export { HEADER_HEIGHT, ROW_HEIGHT } +/** Exactly 2× `interactionHover`: white 11% dark, ink 15% light. */ +const selectionWash = (theme: { mode: string; color: { contentPrimary: string } }) => + theme.mode === "light" + ? withAlpha(theme.color.contentPrimary, 0.15) + : "rgba(255, 255, 255, 0.11)" + export const GridContainer = styled.div` flex: 1; overflow: hidden; @@ -173,7 +180,9 @@ export const Row = styled.div<{ $active: boolean }>` ${({ $active, theme }) => $active && css` - background: ${theme.color.gridSelection}; + background: + linear-gradient(${selectionWash(theme)}, ${selectionWash(theme)}), + ${theme.color.gridRow}; `} ${({ $active, theme }) => @@ -199,8 +208,8 @@ export const Row = styled.div<{ $active: boolean }>` `} ` -const pulseAnim = (pink: string, transparent: string) => keyframes` - 0% { box-shadow: ${pink} 0 0 0 1px; } +const pulseAnim = (ring: string, transparent: string) => keyframes` + 0% { box-shadow: ${ring} 0 0 0 1px; } 75% { box-shadow: ${transparent} 0 0 0 16px; } ` @@ -236,21 +245,31 @@ export const Cell = styled.div<{ ${({ $frozen, $rowActive, theme }) => $frozen && css` - background: ${$rowActive ? theme.color.gridSelection : color("gridRow")}; + background: ${$rowActive + ? `linear-gradient(${selectionWash(theme)}, ${selectionWash(theme)}), ${theme.color.gridRow}` + : color("gridRow")}; `} ${({ $isActive, theme }) => $isActive && css` - background: ${theme.color.gridSelection}; - box-shadow: inset 0 0 0 1px ${theme.color.gridFocus}; + background: + linear-gradient( + ${theme.color.statusInfoSurface}, + ${theme.color.statusInfoSurface} + ), + linear-gradient(${selectionWash(theme)}, ${selectionWash(theme)}), + ${theme.color.gridRow}; + border-right-color: transparent; + border-bottom-color: transparent; + box-shadow: inset 0 0 0 1px ${theme.color.statusInfo}; border-radius: 0; `} ${({ $isPulsing, theme }) => $isPulsing && css` - animation: ${pulseAnim(theme.color.gridFocus, theme.color.transparent)} 1s + animation: ${pulseAnim(theme.color.statusInfo, theme.color.transparent)} 1s ease-out; `} ` diff --git a/src/components/SetupAIAssistant/SettingsModal.tsx b/src/components/SetupAIAssistant/SettingsModal.tsx index 16cf25a2d..d40412a6d 100644 --- a/src/components/SetupAIAssistant/SettingsModal.tsx +++ b/src/components/SetupAIAssistant/SettingsModal.tsx @@ -156,11 +156,14 @@ const ProviderTab = styled(TabButton)` $active ? theme.color.interactionNeutral : theme.color.transparent}; } - &&:hover:not(:disabled) { - background: ${({ $active, theme }) => - $active - ? theme.color.interactionNeutral - : theme.color.controlSurfaceHover}; + &&&&:hover:not(:disabled) { + background: ${({ $active, theme }) => { + if ($active) return theme.color.interactionNeutral + if (theme.mode === "light") { + return theme.color.surfaceBase + } + return theme.color.interactionNeutralHover + }}; } ` diff --git a/src/styles/_grid.scss b/src/styles/_grid.scss index d55f22447..e5afc240b 100644 --- a/src/styles/_grid.scss +++ b/src/styles/_grid.scss @@ -75,7 +75,6 @@ $col-resize-ghost-z-index: ($panel-left-z-index + 1); $active-row-background: theme-color(interactionNeutral); $row-hover-background: theme-color(interactionNeutralHover); -$table-selection-background: theme-color(gridSelection); .qg-r-hover { background-color: $row-hover-background; @@ -263,11 +262,27 @@ $drag-handle-margin: 2px; } .qg-c-active { - box-shadow: inset 0 0 0 1px theme-color(gridFocus); - background: $table-selection-background; + box-shadow: inset 0 0 0 1px theme-color(statusInfo); + background: + linear-gradient( + theme-color(statusInfoSurface), + theme-color(statusInfoSurface) + ), + linear-gradient(rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.11)), + theme-color(gridRow); border-radius: 0.4rem; } +html[data-theme="light"] .qg-c-active { + background: + linear-gradient( + theme-color(statusInfoSurface), + theme-color(statusInfoSurface) + ), + linear-gradient(rgba(28, 32, 41, 0.15), rgba(28, 32, 41, 0.15)), + theme-color(gridRow); +} + .qg-c-active-pulse { animation: pulse 1000ms 0.1s; z-index: 4; @@ -275,7 +290,7 @@ $drag-handle-margin: 2px; @keyframes pulse { 0% { - box-shadow: theme-color(gridFocus) 0 0 0 1px; + box-shadow: theme-color(statusInfo) 0 0 0 1px; } 75% { box-shadow: theme-color(transparent) 0 0 0 16px; From f5a98d6f645e6771ff4a00b16868f6e0a1ff359f Mon Sep 17 00:00:00 2001 From: Zack Nelson Date: Mon, 7 Sep 2026 10:39:13 +0100 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20continue=20theme=20steering=20?= =?UTF-8?q?=E2=80=94=20calendar,=20warning=20pill,=20schema=20glyphs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Light metrics calendar flips enabled/disabled wells and keeps selected dates inverse. Monitoring count pills match Figma geometry with the quieter 32% stroke. Table and column glyphs settle on contentPrimary. Co-authored-by: Cursor --- THEME-STEERING.md | 55 +++++++++++++++++++ src/components/Badge/index.tsx | 15 ++--- src/scenes/Schema/Row/index.tsx | 4 +- .../Schema/TableDetailsDrawer/index.tsx | 2 +- src/scenes/Schema/table-icon.tsx | 4 +- src/styles/lib/_react-calendar.scss | 19 +++++++ 6 files changed, 87 insertions(+), 12 deletions(-) diff --git a/THEME-STEERING.md b/THEME-STEERING.md index a439f95d7..c3750c5ae 100644 --- a/THEME-STEERING.md +++ b/THEME-STEERING.md @@ -934,6 +934,61 @@ change shipped together, split them. - Out of scope: retuning `statusInfo` / `statusInfoSurface` / `gridSelection`; `editorSelection`; notebook cell focus. - For Emre: Grid selection wash is local to ResultGrid, not a token. Cell ring is `statusInfo`, not `gridFocus`. +### 2026-09-07 — Light metrics calendar: enabled light, disabled grey +- Lift: binding +- Status: applied locally +- Modes: light (dark kept) +- Tokens: none +- Binding: `.react-calendar` `editorCanvas` → Overlay. Disabled tiles Overlay + Muted → Base + `contentDisabled`. Enabled type Secondary → Primary. Selected range stays `actionPrimary`. +- Neighbors: Overlay `#fafbfc` is the popover. Base `#eef0f3` was the whole light calendar well, so enabled days looked greyed and future/`min`/`max` days looked lit. +- Why: Zack: flip enabled vs disabled in light. Enabled light; disabled greyed out. Follow-up: selected range type must stay `contentInverse` in light — the enabled-tile Primary rule was winning. +- Out of scope: dark; selected-range fill; neighboring-month type; popover stroke. +- For Emre: Do not invert Overlay/Canvas globally. This is calendar tiles only. Selected type is Inverse, not Primary. + +### 2026-09-07 — Table-details warning count matches Figma pill +- Lift: binding +- Status: applied locally +- Modes: both (geometry) +- Tokens: none +- Binding: `Badge` `shape="pill"` `sm` — 20px tall, 5px inline pad + 1px stroke (6px to icon), 4px gap, 12px icon/type Regular, 12% wash. Dropped `TabBadge` 18px height. Stroke stays tone @ 32% (not solid). +- Neighbors: error count pill on the same tab uses the same geometry. Chip-shaped badges unchanged. +- Why: Figma `1914:98448` is 36×20 with 6×4 pad, 4px gap, 12px glyph. The 18px override crushed it. Zack: keep the quieter 32% stroke, not the solid Figma border. +- Out of scope: chip shape; StatusChip; retuning `statusWarningSurface`. +- For Emre: Pill is the compact count. Do not restyle chips to this stadium. Stroke is 32% wash, not solid tone. + +### 2026-09-07 — Table and column icons step up to Primary +- Lift: binding +- Status: superseded the same day — glyphs went to Secondary +- Modes: both (dark was the read) +- Tokens: none +- Binding: `TableIcon` / `MaterializedViewIcon` / `TypeIcon` / designated-timestamp `SortDownIcon` `contentMuted` → `contentPrimary`. +- Neighbors: dark Muted `#858995` sits below Secondary `#9da1ad`. There is no content role between Secondary and Primary `#f8f8f2`. Table names were already Primary. +- Why: Zack: icons not visible enough in dark; wanted closer to Primary than Secondary. They were on Muted, quieter than Secondary. +- Out of scope: inventing a mid-content token; type *labels* stay Secondary; rail latches. +- For Emre: Superseded. Glyphs are Secondary. + +### 2026-09-07 — Table and column glyphs are Secondary +- Lift: binding +- Status: superseded the same day — back to Primary +- Modes: both +- Tokens: none +- Binding: table / mat-view / view / column-type / designated-timestamp glyphs `contentPrimary` → `contentSecondary`. +- Neighbors: names stay Primary. Parenthetical type labels were already Secondary — glyphs now match that weight, one step above Muted. +- Why: Zack: try Secondary after Primary. +- Out of scope: type labels; rail latches; inventing a mid-content token. +- For Emre: Superseded. Glyphs are Primary. + +### 2026-09-07 — Table and column glyphs settle on Primary +- Lift: binding +- Status: applied locally +- Modes: both +- Tokens: none +- Binding: table / mat-view / view / column-type / designated-timestamp glyphs `contentSecondary` → `contentPrimary`. +- Neighbors: names already Primary. Type labels stay Secondary. +- Why: Zack: go back to Primary after the Secondary trial. +- Out of scope: type labels; rail latches. +- For Emre: Object-type marks are Primary, same as the name. + --- ## Parking lot — not logged as decisions yet diff --git a/src/components/Badge/index.tsx b/src/components/Badge/index.tsx index 0b10c2e94..c4efb13d2 100644 --- a/src/components/Badge/index.tsx +++ b/src/components/Badge/index.tsx @@ -100,13 +100,13 @@ const Root = styled.span<{ align-items: center; justify-content: center; position: relative; - gap: ${({ $shape }) => ($shape === "pill" ? "0.5rem" : "0.4rem")}; + gap: 0.4rem; height: ${({ $shape, $size }) => - $shape === "pill" ? ($size === "sm" ? "2.2rem" : "2.8rem") : "auto"}; + $shape === "pill" ? ($size === "sm" ? "2rem" : "2.8rem") : "auto"}; padding: ${({ $shape, $size }) => $shape === "pill" ? $size === "sm" - ? "0 0.7rem" + ? "0 0.5rem" : "0 0.9rem" : "0.5rem"}; border: ${({ $shape, $variant, theme }) => @@ -117,11 +117,12 @@ const Root = styled.span<{ color: ${({ $variant, theme }) => getTone($variant, theme)}; background: ${({ $shape, $variant, theme }) => $shape === "pill" - ? withAlpha(getTone($variant, theme), 0.1) + ? withAlpha(getTone($variant, theme), 0.12) : chipBackground($variant, theme)}; font-size: ${({ $shape, theme }) => $shape === "pill" ? theme.fontSize.xs : "1.1rem"}; - font-weight: ${({ $shape }) => ($shape === "pill" ? 600 : 400)}; + font-weight: 400; + letter-spacing: ${({ $shape }) => ($shape === "pill" ? "0.1px" : "normal")}; line-height: 1; white-space: nowrap; @@ -147,8 +148,8 @@ const Icon = styled.div<{ hasGap: boolean; $shape: BadgeShape }>` ${({ hasGap }) => !hasGap && "margin: 0 -0.1rem;"} svg { - width: ${({ $shape }) => ($shape === "pill" ? "1.4rem" : "1.6rem")}; - height: ${({ $shape }) => ($shape === "pill" ? "1.4rem" : "1.6rem")}; + width: ${({ $shape }) => ($shape === "pill" ? "1.2rem" : "1.6rem")}; + height: ${({ $shape }) => ($shape === "pill" ? "1.2rem" : "1.6rem")}; } ` diff --git a/src/scenes/Schema/Row/index.tsx b/src/scenes/Schema/Row/index.tsx index 3add4c060..e6bc437a0 100644 --- a/src/scenes/Schema/Row/index.tsx +++ b/src/scenes/Schema/Row/index.tsx @@ -223,7 +223,7 @@ const Spacer = styled.span` ` const SortDownIcon = styled(SortDown)` - color: ${color("contentMuted")}; + color: ${color("contentPrimary")}; margin-right: 0.8rem; flex-shrink: 0; ` @@ -276,7 +276,7 @@ const TypeIcon = styled.div` margin-right: 0.8rem; display: flex; align-items: center; - color: ${color("contentMuted")}; + color: ${color("contentPrimary")}; ` const TYPE_ICONS = { diff --git a/src/scenes/Schema/TableDetailsDrawer/index.tsx b/src/scenes/Schema/TableDetailsDrawer/index.tsx index 8b84cf4d0..3ce1d8e61 100644 --- a/src/scenes/Schema/TableDetailsDrawer/index.tsx +++ b/src/scenes/Schema/TableDetailsDrawer/index.tsx @@ -145,7 +145,7 @@ const TabBadge = styled(Badge).attrs({ shape: "pill" as const, size: "sm" as const, })` - height: 1.8rem; + flex-shrink: 0; ` export const TableDetailsDrawer = () => { diff --git a/src/scenes/Schema/table-icon.tsx b/src/scenes/Schema/table-icon.tsx index 7e91c02d6..4e4b3338e 100644 --- a/src/scenes/Schema/table-icon.tsx +++ b/src/scenes/Schema/table-icon.tsx @@ -22,7 +22,7 @@ const Root = styled.div<{ $size: string }>` height: ${({ $size }) => $size}; position: relative; flex-shrink: 0; - color: ${color("contentMuted")}; + color: ${color("contentPrimary")}; svg { color: inherit; @@ -59,7 +59,7 @@ const NonPartitionedTableIcon = ({ ) const MutedGlyph = styled.svg` - color: ${color("contentMuted")}; + color: ${color("contentPrimary")}; flex-shrink: 0; ` diff --git a/src/styles/lib/_react-calendar.scss b/src/styles/lib/_react-calendar.scss index 3575eb47f..a69bcbf84 100644 --- a/src/styles/lib/_react-calendar.scss +++ b/src/styles/lib/_react-calendar.scss @@ -91,6 +91,25 @@ color: theme-color(contentMuted); } +html[data-theme="light"] .react-calendar { + background: theme-color(surfaceOverlay); +} + +html[data-theme="light"] .react-calendar__tile { + color: theme-color(contentPrimary); +} + +html[data-theme="light"] .react-calendar__tile:disabled { + background-color: theme-color(surfaceBase); + color: theme-color(contentDisabled); +} + +html[data-theme="light"] .react-calendar__tile--active, +html[data-theme="light"] .react-calendar__tile--range, +html[data-theme="light"] .react-calendar__tile--hasActive { + color: theme-color(contentInverse); +} + .react-calendar__tile:enabled:hover, .react-calendar__tile:enabled:focus { background-color: theme-color(borderDefault); From 5b7ad3934686ba6e6115aac4763c161189c4ab65 Mon Sep 17 00:00:00 2001 From: emrberk Date: Mon, 7 Sep 2026 13:47:54 +0300 Subject: [PATCH 06/10] mcp popover revisions, remove orphan color tokens, fix style lint issues --- e2e/questdb | 2 +- src/components/Badge/index.tsx | 6 +- src/components/Button/index.tsx | 3 +- src/components/ResultGrid/styles.ts | 23 ++- src/components/Select/index.tsx | 82 --------- .../SetupAIAssistant/SettingsModal.tsx | 12 +- src/components/Switch/index.tsx | 25 +-- .../TopBar/InstanceSettingsPopper.tsx | 3 +- src/components/index.ts | 1 - .../Editor/AIChatWindow/ChatHistoryItem.tsx | 3 +- .../Editor/Notebook/cells/CellNameLabel.tsx | 8 +- .../Footer/MCPBridgeStatus/PairPopover.tsx | 155 ++++++++---------- src/scenes/Footer/MCPBridgeStatus/index.tsx | 40 ++++- src/scenes/Layout/warning.tsx | 3 +- src/scenes/Schema/Row/index.tsx | 1 - src/styles/_grid.scss | 13 +- src/theme/global-styles/docsearch.ts | 3 +- src/theme/index.ts | 18 +- src/types/styled.d.ts | 9 +- 19 files changed, 153 insertions(+), 257 deletions(-) delete mode 100644 src/components/Select/index.tsx diff --git a/e2e/questdb b/e2e/questdb index 9b59a9211..00de5bbb3 160000 --- a/e2e/questdb +++ b/e2e/questdb @@ -1 +1 @@ -Subproject commit 9b59a921165af573cedd22bf8b12613de19cb8bd +Subproject commit 00de5bbb3aed7e23794e6d7ff0cd4089a8e1423b diff --git a/src/components/Badge/index.tsx b/src/components/Badge/index.tsx index c4efb13d2..754985388 100644 --- a/src/components/Badge/index.tsx +++ b/src/components/Badge/index.tsx @@ -104,11 +104,7 @@ const Root = styled.span<{ height: ${({ $shape, $size }) => $shape === "pill" ? ($size === "sm" ? "2rem" : "2.8rem") : "auto"}; padding: ${({ $shape, $size }) => - $shape === "pill" - ? $size === "sm" - ? "0 0.5rem" - : "0 0.9rem" - : "0.5rem"}; + $shape === "pill" ? ($size === "sm" ? "0 0.5rem" : "0 0.9rem") : "0.5rem"}; border: ${({ $shape, $variant, theme }) => $shape === "pill" ? `1px solid ${withAlpha(getTone($variant, theme), 0.32)}` diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index aa66e915d..c483a7d86 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -186,8 +186,7 @@ export const ButtonBase = styled.button.attrs<{ filter 120ms ease; &&:focus-visible { - outline: 1px solid - ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; + outline: 1px solid ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } diff --git a/src/components/ResultGrid/styles.ts b/src/components/ResultGrid/styles.ts index b9e39ab95..4aac5c9cb 100644 --- a/src/components/ResultGrid/styles.ts +++ b/src/components/ResultGrid/styles.ts @@ -1,5 +1,4 @@ import styled, { css, keyframes } from "styled-components" -import { withAlpha } from "../../theme" import { color } from "../../utils" import { CopyButton } from "../CopyButton" import { @@ -18,12 +17,6 @@ import { export { HEADER_HEIGHT, ROW_HEIGHT } -/** Exactly 2× `interactionHover`: white 11% dark, ink 15% light. */ -const selectionWash = (theme: { mode: string; color: { contentPrimary: string } }) => - theme.mode === "light" - ? withAlpha(theme.color.contentPrimary, 0.15) - : "rgba(255, 255, 255, 0.11)" - export const GridContainer = styled.div` flex: 1; overflow: hidden; @@ -181,7 +174,10 @@ export const Row = styled.div<{ $active: boolean }>` $active && css` background: - linear-gradient(${selectionWash(theme)}, ${selectionWash(theme)}), + linear-gradient( + ${theme.color.interactionSelected}, + ${theme.color.interactionSelected} + ), ${theme.color.gridRow}; `} @@ -246,7 +242,7 @@ export const Cell = styled.div<{ $frozen && css` background: ${$rowActive - ? `linear-gradient(${selectionWash(theme)}, ${selectionWash(theme)}), ${theme.color.gridRow}` + ? `linear-gradient(${theme.color.interactionSelected}, ${theme.color.interactionSelected}), ${theme.color.gridRow}` : color("gridRow")}; `} @@ -258,7 +254,10 @@ export const Cell = styled.div<{ ${theme.color.statusInfoSurface}, ${theme.color.statusInfoSurface} ), - linear-gradient(${selectionWash(theme)}, ${selectionWash(theme)}), + linear-gradient( + ${theme.color.interactionSelected}, + ${theme.color.interactionSelected} + ), ${theme.color.gridRow}; border-right-color: transparent; border-bottom-color: transparent; @@ -269,8 +268,8 @@ export const Cell = styled.div<{ ${({ $isPulsing, theme }) => $isPulsing && css` - animation: ${pulseAnim(theme.color.statusInfo, theme.color.transparent)} 1s - ease-out; + animation: ${pulseAnim(theme.color.statusInfo, theme.color.transparent)} + 1s ease-out; `} ` diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx deleted file mode 100644 index 6dac6015c..000000000 --- a/src/components/Select/index.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react" -import styled, { css } from "styled-components" -import { Input } from "../Input" -import { ArrowDropDown } from "../icons" - -export type SelectProps = { - name: string - options: { - label: string - value: string | number - }[] - prefixIcon?: React.ReactNode - disabled?: boolean -} & React.SelectHTMLAttributes - -const Root = styled.div<{ disabled?: boolean }>` - position: relative; - width: 100%; - - ${({ disabled }) => - disabled && - css` - .prefixIcon, - .arrowDownIcon { - opacity: 0.5; - color: ${({ theme }) => theme.color.interactionNeutral}; - } - `} - - .prefixIcon { - position: absolute; - left: 10px; - top: 50%; - transform: translate(0, -50%); - pointer-events: none; - } - - .arrowDownIcon { - position: absolute; - right: 0; - top: 50%; - transform: translate(0, -50%); - pointer-events: none; - fill: ${({ theme }) => theme.color.contentPrimary}; - } -` - -const StyledSelect = styled(Input).attrs({ as: "select" })<{ - withPrefixIcon: boolean -}>` - position: relative; - appearance: none; - cursor: pointer; - width: 100%; - padding-right: 2.4rem; - - ${({ withPrefixIcon }) => withPrefixIcon && `padding-left: 3rem;`} -` - -export const Select = React.forwardRef( - ({ options, prefixIcon, ...rest }: SelectProps, ref) => ( - - } - withPrefixIcon={typeof prefixIcon !== "undefined"} - {...rest} - > - {options.map(({ label, value }) => ( - - ))} - - - {/* dues to absolute positioning must be last to be rendered on top */} - {prefixIcon ? {prefixIcon} : null} - - - ), -) - -Select.displayName = "Select" diff --git a/src/components/SetupAIAssistant/SettingsModal.tsx b/src/components/SetupAIAssistant/SettingsModal.tsx index d40412a6d..4eafbb4d6 100644 --- a/src/components/SetupAIAssistant/SettingsModal.tsx +++ b/src/components/SetupAIAssistant/SettingsModal.tsx @@ -1,5 +1,6 @@ import React, { useState, useCallback, useMemo, useRef } from "react" import styled, { useTheme } from "styled-components" +import { withAlpha } from "../../theme" import * as RadixDialog from "@radix-ui/react-dialog" import { Dialog } from "../Dialog" import { Box } from "../Box" @@ -204,9 +205,7 @@ const StatusChip = styled.span<{ $enabled: boolean }>` border: ${({ $enabled, theme }) => theme.mode === "light" ? `1px solid ${ - $enabled - ? theme.color.statusSuccessBorder - : theme.color.borderDefault + $enabled ? theme.color.statusSuccessBorder : theme.color.borderDefault }` : 0}; ` @@ -330,9 +329,10 @@ const ResetProviderButton = styled(Button).attrs({ variant: "ghost" })` &&:hover:not(:disabled):not([aria-disabled="true"]) { background: ${({ theme }) => - theme.mode === "light" - ? "rgba(189, 40, 40, 0.1)" - : "rgba(219, 36, 36, 0.3)"}; + withAlpha( + theme.color.statusDangerSurface, + theme.mode === "light" ? 0.1 : 0.3, + )}; color: ${({ theme }) => theme.color.statusDanger}; } diff --git a/src/components/Switch/index.tsx b/src/components/Switch/index.tsx index 50a17a9d0..db0d6257a 100644 --- a/src/components/Switch/index.tsx +++ b/src/components/Switch/index.tsx @@ -11,7 +11,6 @@ type Props = { checked?: boolean id?: string ariaDescribedBy?: string - tone?: "accent" | "success" size?: "sm" | "md" "aria-label"?: string } @@ -26,14 +25,6 @@ const thumb = { md: { width: "24px", height: "16px", radius: "6px" }, } as const -const restStroke = (mode: "light" | "dark") => - mode === "light" ? "rgba(28, 32, 41, 0.25)" : "rgba(255, 255, 255, 0.15)" - -const restTrack = (theme: { - mode: "light" | "dark" - color: { controlTrack: string } -}) => (theme.mode === "light" ? "#c9cdd4" : theme.color.controlTrack) - const Root = styled(SwitchPrimitive.Root)<{ $size: "sm" | "md" }>` @@ -46,7 +37,7 @@ const Root = styled(SwitchPrimitive.Root)<{ height: ${({ $size }) => track[$size].height}; border-radius: ${({ $size }) => track[$size].radius}; border: none; - box-shadow: inset 0 0 0 0.5px ${({ theme }) => restStroke(theme.mode)}; + box-shadow: inset 0 0 0 0.5px ${({ theme }) => theme.color.controlTrackStroke}; appearance: none; position: relative; overflow: hidden; @@ -55,26 +46,28 @@ const Root = styled(SwitchPrimitive.Root)<{ box-shadow 120ms ease, opacity 120ms ease; cursor: pointer; - background: ${({ theme }) => restTrack(theme)}; + background: ${({ theme }) => theme.color.controlTrackRest}; &:hover:not([data-disabled]) { - box-shadow: inset 0 0 0 0.5px ${({ theme }) => restStroke(theme.mode)}; + box-shadow: inset 0 0 0 0.5px + ${({ theme }) => theme.color.controlTrackStroke}; } &:focus-visible { - outline: 1px solid - ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; + outline: 1px solid ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } &[data-state="checked"] { background: ${({ theme }) => theme.color.statusSuccessFill}; - box-shadow: inset 0 0 0 0.5px ${({ theme }) => restStroke(theme.mode)}; + box-shadow: inset 0 0 0 0.5px + ${({ theme }) => theme.color.controlTrackStroke}; } &[data-state="checked"]:hover:not([data-disabled]) { background: ${({ theme }) => theme.color.statusSuccessFill}; - box-shadow: inset 0 0 0 0.5px ${({ theme }) => restStroke(theme.mode)}; + box-shadow: inset 0 0 0 0.5px + ${({ theme }) => theme.color.controlTrackStroke}; } &[data-disabled], diff --git a/src/components/TopBar/InstanceSettingsPopper.tsx b/src/components/TopBar/InstanceSettingsPopper.tsx index ff9fc4044..fec889ec0 100644 --- a/src/components/TopBar/InstanceSettingsPopper.tsx +++ b/src/components/TopBar/InstanceSettingsPopper.tsx @@ -155,8 +155,7 @@ const ColorSlider = styled.input.attrs({ type: "range", min: 0, max: 255 })` } &:focus-visible { - outline: 1px solid - ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; + outline: 1px solid ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } diff --git a/src/components/index.ts b/src/components/index.ts index e65aed99a..0975923f6 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -59,7 +59,6 @@ export * from "./PaneWrapper" export * from "./PopperHover" export * from "./PopperToggle" export * from "./Popover" -export * from "./Select" export * from "./SelectMenu" export * from "./SegmentedControl" export * from "./SelectableCardButton" diff --git a/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx b/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx index 8804ebcde..7cfbe62c5 100644 --- a/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx +++ b/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx @@ -40,8 +40,7 @@ const Container = styled.div<{ $disabled?: boolean }>` } &:focus-visible { - outline: 1px solid - ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; + outline: 1px solid ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } ` diff --git a/src/scenes/Editor/Notebook/cells/CellNameLabel.tsx b/src/scenes/Editor/Notebook/cells/CellNameLabel.tsx index 3723cd3b3..b831be842 100644 --- a/src/scenes/Editor/Notebook/cells/CellNameLabel.tsx +++ b/src/scenes/Editor/Notebook/cells/CellNameLabel.tsx @@ -26,7 +26,7 @@ const Label = styled(ButtonBase)<{ $placeholder: boolean }>` &:hover, &:focus-visible { - border-color: ${({ theme }) => theme.color.contentAccent}; + border-color: ${({ theme }) => theme.color.borderStrong}; } ${({ $placeholder, theme }) => @@ -41,12 +41,6 @@ const Input = styled(NotebookRenameInput)` min-width: 8rem; height: 2.4rem; flex: 1; - border-color: ${({ theme }) => theme.color.contentAccent}; - - &:focus, - &:focus-visible { - border-color: ${({ theme }) => theme.color.contentAccent}; - } ` type Props = { diff --git a/src/scenes/Footer/MCPBridgeStatus/PairPopover.tsx b/src/scenes/Footer/MCPBridgeStatus/PairPopover.tsx index ced2762ba..2bc5dc48e 100644 --- a/src/scenes/Footer/MCPBridgeStatus/PairPopover.tsx +++ b/src/scenes/Footer/MCPBridgeStatus/PairPopover.tsx @@ -1,5 +1,6 @@ import React, { forwardRef, useEffect, useState } from "react" -import styled, { css } from "styled-components" +import styled from "styled-components" +import type { DefaultTheme } from "styled-components" import { InfoIcon, ArrowRightIcon, @@ -35,6 +36,9 @@ const SUCCESS_AUTOCLOSE_MS = 3_000 const DEFAULT_WS_URL_PREFIX = "ws://127.0.0.1:" +// Centres a 16px notice icon on the title's line box instead of the row top. +const ICON_TITLE_OFFSET = "0.2rem" + const Root = styled.div` ${floatingSurfaceStyles} width: min(38rem, calc(100vw - 2rem)); @@ -115,67 +119,49 @@ const StatusDetail = styled.span` word-break: break-word; ` -const StatusRow = styled.div<{ $tone: "info" | "danger" | "warning" }>` +type NoticeTone = "info" | "danger" | "warning" + +const rowSurface = (theme: DefaultTheme, tone: NoticeTone): string => { + if (tone === "warning") return theme.color.statusWarningSurface + if (tone === "danger") return theme.color.statusDangerSurface + return theme.color.surfaceInset +} + +const rowAccent = (theme: DefaultTheme, tone: NoticeTone): string => { + if (tone === "warning") return theme.color.statusWarning + if (tone === "danger") return theme.color.statusDanger + return theme.color.contentAccentStrong +} + +const rowText = (theme: DefaultTheme, tone: NoticeTone): string => + tone === "info" ? theme.color.contentSecondary : theme.color.contentPrimary + +// Full-bleed notice band, shared by every status message in the popover. It +// spans the popover edge to edge, so it lives outside `Body` and its padding. +// The icon holds the title's line box. +const StatusRow = styled.div<{ $tone: NoticeTone }>` display: flex; align-items: flex-start; gap: 0.8rem; width: 100%; padding: 1.6rem; - background: ${({ theme, $tone }) => - $tone === "warning" - ? theme.color.statusWarningSurface - : theme.color.surfaceInset}; - color: ${({ theme, $tone }) => - $tone === "info" - ? theme.color.contentSecondary - : theme.color.contentPrimary}; + background: ${({ theme, $tone }) => rowSurface(theme, $tone)}; + color: ${({ theme, $tone }) => rowText(theme, $tone)}; & > svg { flex-shrink: 0; - color: ${({ theme, $tone }) => - $tone === "warning" - ? theme.color.statusWarning - : theme.color.contentAccentStrong}; + margin-top: ${ICON_TITLE_OFFSET}; + color: ${({ theme, $tone }) => rowAccent(theme, $tone)}; } strong { color: ${({ theme }) => theme.color.contentPrimary}; font-weight: 600; } - - ${({ $tone, theme }) => - $tone === "danger" && - css` - align-items: center; - padding: 1.2rem; - border-radius: ${theme.borderRadius}; - background: ${theme.mode === "light" - ? "rgba(189, 40, 56, 0.08)" - : "transparent"}; - border: 1px solid - ${theme.mode === "light" - ? theme.color.statusDanger - : theme.color.statusDangerMuted}; - border-left-width: 0.3rem; - color: ${theme.color.statusDanger}; - - & > svg { - color: ${theme.color.statusDanger}; - } - - strong { - color: ${theme.color.statusDanger}; - } - - ${StatusDetail} { - color: ${theme.color.statusDanger}; - font-weight: 600; - } - `} ` const AgentChangesRow = styled(StatusRow)` - background: ${({ theme }) => theme.color.statusInfoSurface}; + background: ${({ theme }) => theme.color.statusInfoSurfaceStrong}; color: ${({ theme }) => theme.color.contentPrimary}; strong { @@ -309,9 +295,10 @@ export const MCPBridgePairPopover = forwardRef( const isConnecting = status === "connecting" || status === "reconnecting" const showConnecting = !succeeded && isConnecting - const showVersionMismatch = - versionMismatch === "major" || - (versionMismatch === "minor" && status === "connected") + const showValidationError = !succeeded && validationError !== null + const showMajorMismatch = !succeeded && versionMismatch === "major" + const showMinorMismatch = + versionMismatch === "minor" && status === "connected" const showWsError = !succeeded && !isConnecting && @@ -466,43 +453,43 @@ export const MCPBridgePairPopover = forwardRef( }} disabled={isConnecting} /> + + )} - {validationError && ( - - - - {validationError} - - - )} + {showValidationError && ( + + + + {validationError} + + + )} - {showWsError && ( - - - - Could not connect to MCP server - - {lastError ?? - `MCP server stopped responding after ${MAX_RECONNECT_ATTEMPTS} attempts. Try again, or ask your coding agent for a fresh deep link.`} - - - - )} + {showWsError && ( + + + + Could not connect to MCP server + + {lastError ?? + `MCP server stopped responding after ${MAX_RECONNECT_ATTEMPTS} attempts. Try again, or ask your coding agent for a fresh deep link.`} + + + + )} - {showVersionMismatch && versionMismatch === "major" && ( - - - - )} - + {showMajorMismatch && ( + + + )} {showConnecting && ( @@ -526,13 +513,13 @@ export const MCPBridgePairPopover = forwardRef( )} - {showVersionMismatch && versionMismatch === "minor" && ( + {showMinorMismatch && ( - + )} diff --git a/src/scenes/Footer/MCPBridgeStatus/index.tsx b/src/scenes/Footer/MCPBridgeStatus/index.tsx index c25164067..fbf6abaa5 100644 --- a/src/scenes/Footer/MCPBridgeStatus/index.tsx +++ b/src/scenes/Footer/MCPBridgeStatus/index.tsx @@ -23,14 +23,39 @@ type PillStyleProps = { $newChanges: boolean } -// New agent changes turn the pill magenta regardless of the connection tone — -// they are the one state the user can act on straight from the footer. +// New agent changes turn the pill cyan regardless of the connection tone — +// they are the one state the user can act on straight from the footer, and +// cyan is what the popover band already uses for the same message. Only the +// outline and icon carry it; the surface stays whatever the tone gives. const accent = ({ theme, $tone, $newChanges, }: PillStyleProps & { theme: DefaultTheme }) => - $newChanges ? theme.color.contentAccent : theme.color[accentColor($tone)] + $newChanges ? theme.color.statusInfo : theme.color[accentColor($tone)] + +// Idle is neutral chrome, not a status: it outlines in the shared border the +// neighbouring version badge uses, while its icon stays readable. Every other +// tone — and any tone with agent changes — outlines in its own accent. +const outline = (props: PillStyleProps & { theme: DefaultTheme }) => + props.$tone === "idle" && !props.$newChanges + ? props.theme.color.borderStrong + : accent(props) + +// The pill carries no surface of its own where it would only restate the +// footer: always on dark, and in the idle tone on light. It keeps its +// outline, and gains a surface on hover. +const isFlush = ({ theme, $tone }: PillStyleProps & { theme: DefaultTheme }) => + theme.mode === "dark" || $tone === "idle" + +const flushStyles = css` + background: transparent; + box-shadow: none; + + &:hover { + box-shadow: none; + } +` const Wrapper = styled(ButtonBase)` display: inline-flex; @@ -38,7 +63,7 @@ const Wrapper = styled(ButtonBase)` gap: 0.6rem; height: 3.2rem; padding: 0 1.2rem; - border: 1px solid ${accent}; + border: 1px solid ${outline}; border-radius: 0.6rem; background: ${({ theme }) => theme.color.controlSurface}; box-shadow: 0 0.1rem 0.2rem ${({ theme }) => theme.color.shadowSubtle}; @@ -54,16 +79,17 @@ const Wrapper = styled(ButtonBase)` &:hover { background: ${({ theme }) => theme.color.controlSurfaceHover}; - border-color: ${accent}; + border-color: ${outline}; box-shadow: 0 0.2rem 0.5rem ${({ theme }) => theme.color.shadowSoft}; } &:focus-visible { - outline: 1px solid - ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; + outline: 1px solid ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } + ${(props) => isFlush(props) && flushStyles} + svg { color: ${accent}; flex-shrink: 0; diff --git a/src/scenes/Layout/warning.tsx b/src/scenes/Layout/warning.tsx index 72ac3ae53..8a616d0e1 100644 --- a/src/scenes/Layout/warning.tsx +++ b/src/scenes/Layout/warning.tsx @@ -66,8 +66,7 @@ const WorkaroundLink = styled.a` } &:focus-visible { - outline: 1px solid - ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; + outline: 1px solid ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; border-radius: 0.2rem; } diff --git a/src/scenes/Schema/Row/index.tsx b/src/scenes/Schema/Row/index.tsx index e6bc437a0..b7783863e 100644 --- a/src/scenes/Schema/Row/index.tsx +++ b/src/scenes/Schema/Row/index.tsx @@ -246,7 +246,6 @@ const ExpandButton = styled(IconButton)<{ $expanded?: boolean }>` ` const DotIcon = styled(CheckboxBlankCircle)` - color: ${color("contentSecondary")}; margin-right: 1rem; ` diff --git a/src/styles/_grid.scss b/src/styles/_grid.scss index e5afc240b..e7095fcb8 100644 --- a/src/styles/_grid.scss +++ b/src/styles/_grid.scss @@ -268,19 +268,12 @@ $drag-handle-margin: 2px; theme-color(statusInfoSurface), theme-color(statusInfoSurface) ), - linear-gradient(rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.11)), - theme-color(gridRow); - border-radius: 0.4rem; -} - -html[data-theme="light"] .qg-c-active { - background: linear-gradient( - theme-color(statusInfoSurface), - theme-color(statusInfoSurface) + theme-color(interactionSelected), + theme-color(interactionSelected) ), - linear-gradient(rgba(28, 32, 41, 0.15), rgba(28, 32, 41, 0.15)), theme-color(gridRow); + border-radius: 0.4rem; } .qg-c-active-pulse { diff --git a/src/theme/global-styles/docsearch.ts b/src/theme/global-styles/docsearch.ts index 5c67d9c0b..72bedd82a 100644 --- a/src/theme/global-styles/docsearch.ts +++ b/src/theme/global-styles/docsearch.ts @@ -109,8 +109,7 @@ export const DocSearchStyles = css` } .DocSearch.DocSearch-Button:focus-visible { - outline: 1px solid - ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; + outline: 1px solid ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } diff --git a/src/theme/index.ts b/src/theme/index.ts index 391413cc3..50e571873 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -92,7 +92,6 @@ export const darkColors: ColorShape = { contentDisabled: "#747985", contentAccent: "#f0428b", contentAccentStrong: "#ee2b7c", - contentObject: "#81d3f9", contentSearchMatch: "rgb(163, 127, 96)", // Borders @@ -106,14 +105,15 @@ export const darkColors: ColorShape = { interactionNeutral: "#32343e", interactionNeutralHover: "#292b35", interactionHover: "#ffffff0e", - interactionAccentHover: "rgba(240, 66, 139, 0.08)", interactionAccentActive: "rgba(240, 66, 139, 0.10)", + interactionSelected: "#ffffff1c", interactionGuide: "#6272a4", scrollbarThumb: "rgba(255, 255, 255, 0.13)", controlSurface: "#262833", controlSurfaceHover: "#32343e", controlTrack: "#262833", - controlKnob: "#f8f8f2", + controlTrackRest: "#262833", + controlTrackStroke: "rgba(255, 255, 255, 0.15)", // Actions and status actionPrimary: "#bd0f58", @@ -133,12 +133,12 @@ export const darkColors: ColorShape = { statusSuccessBorder: "rgba(102, 187, 106, 0.24)", statusWarning: "#ffd54f", statusWarningSurface: "rgba(255, 213, 79, 0.10)", - statusWarningSurfaceHover: "rgba(255, 213, 79, 0.16)", statusAttention: "#e3ce78", statusInfo: "#81d3f9", statusInfoSubtle: "#b2e7ff", statusInfoControl: "#0c80f3", statusInfoSurface: "rgba(178, 231, 255, 0.05)", + statusInfoSurfaceStrong: "rgba(21, 156, 193, 0.30)", statusFeature: "#a99de8", // Shadows @@ -163,7 +163,6 @@ export const darkColors: ColorShape = { gridRow: "#17181d", gridHeader: "#202126", gridSelection: "#252830", - gridFocus: "#6b7382", // Editor editorCanvas: "#17181d", @@ -252,7 +251,6 @@ export const lightColors: ColorShape = { contentDisabled: "#596271", contentAccent: "#bd0f58", contentAccentStrong: "#8e0b42", - contentObject: "#b81447", contentSearchMatch: "rgba(202, 139, 44, 0.32)", // Borders @@ -266,14 +264,15 @@ export const lightColors: ColorShape = { interactionNeutral: "#e8eaee", interactionNeutralHover: "#e2e5ea", interactionHover: "#1c202913", - interactionAccentHover: "rgba(189, 15, 88, 0.08)", interactionAccentActive: "rgba(189, 15, 88, 0.13)", + interactionSelected: "#1c202926", interactionGuide: "#56657f", scrollbarThumb: "#d7dbe3", controlSurface: "#f6f7f8", controlSurfaceHover: "#e7e9ed", controlTrack: "#e8eaee", - controlKnob: "#505968", + controlTrackRest: "#c9cdd4", + controlTrackStroke: "rgba(28, 32, 41, 0.25)", // Actions and status actionPrimary: "#8e0b42", @@ -293,12 +292,12 @@ export const lightColors: ColorShape = { statusSuccessBorder: "rgba(8, 122, 80, 0.28)", statusWarning: "#8a570f", statusWarningSurface: "rgba(145, 82, 15, 0.14)", - statusWarningSurfaceHover: "rgba(145, 82, 15, 0.2)", statusAttention: "#745c00", statusInfo: "#176f87", statusInfoSubtle: "#159cc1", statusInfoControl: "#0a88db", statusInfoSurface: "rgba(21, 156, 193, 0.10)", + statusInfoSurfaceStrong: "rgba(21, 156, 193, 0.10)", statusFeature: "#6553aa", // Shadows @@ -323,7 +322,6 @@ export const lightColors: ColorShape = { gridRow: "#f4f5f7", gridHeader: "#e7eaee", gridSelection: "#d8dce3", - gridFocus: "#828b99", // Editor editorCanvas: "#eef0f3", diff --git a/src/types/styled.d.ts b/src/types/styled.d.ts index fa1805a7c..72774d6b1 100644 --- a/src/types/styled.d.ts +++ b/src/types/styled.d.ts @@ -45,7 +45,6 @@ export type ColorShape = { neutralInk: string contentAccent: string contentAccentStrong: string - contentObject: string contentSearchMatch: string borderSubtle: string borderDefault: string @@ -55,14 +54,15 @@ export type ColorShape = { interactionNeutral: string interactionNeutralHover: string interactionHover: string - interactionAccentHover: string interactionAccentActive: string + interactionSelected: string interactionGuide: string scrollbarThumb: string controlSurface: string controlSurfaceHover: string controlTrack: string - controlKnob: string + controlTrackRest: string + controlTrackStroke: string actionPrimary: string actionPrimaryHover: string statusDanger: string @@ -80,12 +80,12 @@ export type ColorShape = { statusSuccessBorder: string statusWarning: string statusWarningSurface: string - statusWarningSurfaceHover: string statusAttention: string statusInfo: string statusInfoSubtle: string statusInfoControl: string statusInfoSurface: string + statusInfoSurfaceStrong: string statusFeature: string statusAssistant: string statusAssistantStrong: string @@ -104,7 +104,6 @@ export type ColorShape = { gridRow: string gridHeader: string gridSelection: string - gridFocus: string editorCanvas: string editorBorder: string editorSelection: string From f1c1537d0bc12cc9f382aba19e147a9ed2368a66 Mon Sep 17 00:00:00 2001 From: emrberk Date: Mon, 7 Sep 2026 15:16:21 +0300 Subject: [PATCH 07/10] convert chat history delete icon to dangerGhost --- src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx b/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx index 7cfbe62c5..44ed0082f 100644 --- a/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx +++ b/src/scenes/Editor/AIChatWindow/ChatHistoryItem.tsx @@ -249,7 +249,7 @@ export const ChatHistoryItem: React.FC = ({ Date: Mon, 7 Sep 2026 16:01:11 +0300 Subject: [PATCH 08/10] update schema pulse color --- src/scenes/Schema/Row/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scenes/Schema/Row/index.tsx b/src/scenes/Schema/Row/index.tsx index b7783863e..542f628cc 100644 --- a/src/scenes/Schema/Row/index.tsx +++ b/src/scenes/Schema/Row/index.tsx @@ -91,9 +91,9 @@ type Props = Readonly<{ value?: string | React.ReactNode }> -const copyPulse = (pink: string) => keyframes` +const copyPulse = (color: string) => keyframes` 0% { - box-shadow: ${pink} 0 0 0 1px; + box-shadow: ${color} 0 0 0 1px; } 75% { box-shadow: transparent 0 0 0 16px; @@ -168,7 +168,7 @@ const Wrapper = styled.div<{ ${({ $isPulsing, theme }) => $isPulsing && css` - animation: ${copyPulse(theme.color.contentAccent)} 1000ms 0.1s; + animation: ${copyPulse(theme.color.borderDefault)} 1000ms 0.1s; `} ` From ecf50913e703c28071e211b7b222031961c36980 Mon Sep 17 00:00:00 2001 From: emrberk Date: Tue, 8 Sep 2026 16:18:02 +0300 Subject: [PATCH 09/10] submodule --- e2e/questdb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/questdb b/e2e/questdb index 00de5bbb3..f2e1a8de1 160000 --- a/e2e/questdb +++ b/e2e/questdb @@ -1 +1 @@ -Subproject commit 00de5bbb3aed7e23794e6d7ff0cd4089a8e1423b +Subproject commit f2e1a8de1ab1201f57ca144b71c36ea0f1358736 From 726162c23613440fdd77692de1092a54f183fac1 Mon Sep 17 00:00:00 2001 From: emrberk Date: Wed, 9 Sep 2026 12:37:26 +0300 Subject: [PATCH 10/10] reviews --- THEME-STEERING.md | 1000 ----------------- THEMING.md | 408 ------- src/components/Key/index.tsx | 25 +- .../TopBar/InstanceSettingsPopper.tsx | 22 +- src/components/TopBar/toolbar.tsx | 2 + src/components/UnreadItemsIcon/index.tsx | 6 +- src/modules/OAuth2/views/loginBackground.tsx | 2 +- src/scenes/Editor/ButtonBar/index.tsx | 76 +- src/theme/THEMING.md | 558 +++++++++ 9 files changed, 660 insertions(+), 1439 deletions(-) delete mode 100644 THEME-STEERING.md delete mode 100644 THEMING.md create mode 100644 src/theme/THEMING.md diff --git a/THEME-STEERING.md b/THEME-STEERING.md deleted file mode 100644 index c3750c5ae..000000000 --- a/THEME-STEERING.md +++ /dev/null @@ -1,1000 +0,0 @@ -# Theme steering log - -Designer-owned trail of palette experiments. `THEMING.md` stays the contract -until Emre reviews an entry and folds it in. - -Two lifts only: - -| Lift | What it is | Where it lands | Emre’s job | -| --- | --- | --- | --- | -| **Palette** | Retune an existing role in one or both themes | `src/theme/index.ts` only | Check neighbors in both modes, then update `THEMING.md` values | -| **Binding** | This component is using the wrong role, or needs a new role | Component file + maybe a new token in `index.ts` | Confirm the meaning, rebind, add the token only if reuse would lie | - -Palette is the default. Binding is the exception — including “brand crimson is -doing a job it should not.” Do not invent a component-named color -(`notebookCellBackground`). Either retune `surfaceRaised`, rebind the component -to a different existing role, or add a role whose *meaning* is new. - -Status: `applied locally` → `ready for Emre` → `accepted` / `tweaked` / `reverted`. - ---- - -## Entry template - -Copy this block. One decision per entry. If a palette change and a binding -change shipped together, split them. - -``` -### YYYY-MM-DD — short name -- Lift: palette | binding | palette + binding -- Status: applied locally -- Modes: light / dark / both -- Tokens: `tokenName` -- Before → after: `#old` → `#new` (light), … (dark) -- Binding: `Component` used `oldRole`, should use `newRole` -- Neighbors: stage / raised / inset / … -- Walked: notebook, SQL editor, details drawer, grid -- Why: -- Out of scope: -- For Emre: -``` - ---- - -## Log - -### 2026-09-01 — Light surfaces: lift baseline, compress elevation ramp -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `surfaceCanvas`, `surfaceStage`, `surfaceBase`, `surfaceInset`, `surfaceRaised`, `surfaceInput`, `surfaceOverlay`, `surfaceValue`, `surfaceScrim`, `surfaceTabRail` -- Before → after (light): - - `surfaceStage` `#c9cdd4` → `#e2e5ea` - - `surfaceCanvas` `#d9dce2` → `#e8eaee` - - `surfaceBase` `#e3e5e9` → `#eef0f3` - - `surfaceRaised` `#e8eaee` → `#f4f5f7` - - `surfaceInset` `#eceef1` → `#f7f8f9` - - `surfaceInput` / `surfaceOverlay` `#f2f3f5` → `#fafbfc` - - `surfaceValue` `#fbfcfd` → `#ffffff` - - `surfaceScrim` ink `.38` → `.32` - - `surfaceTabRail` `(218,221,227,.94)` → `(232,234,238,.94)` -- Neighbors: stage still recedes from canvas; raised still sits above stage; inset stays the bright well. Stage→raised gap ~31 RGB points → ~18. -- Walked: notebook cells, SQL canvas, details drawer. Grids/charts were already working — `gridRow` / `gridHeader` / `chartSeries*` not touched. -- Why: Light mode felt washed. Baseline too gray; elevation ramp between field and cards too large, so SQL, notebook, and details went muddy while grids still read. -- Out of scope: dark mode; content/accent; interaction fills (`interactionNeutral` still `#d9dce2`). -- For Emre: Confirm the compressed light ladder still matches the spatial story in `THEMING.md` (darker stage, brighter embedded content). Update the Core palette table if accepted. - -### 2026-09-01 — Light SQL canvas follows the new baseline -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `editorCanvas`, `editorBorder` -- Before → after (light): `editorCanvas` `#d9dce2` → `#eef0f3`; `editorBorder` `#c9cdd4` → `#e2e5ea` -- Neighbors: `editorCanvas` now matches `surfaceBase`; sits as a quiet well inside `surfaceRaised` cells. -- Why: The SQL editor does not read `surface*`. Leaving `editorCanvas` at the old gray would have stranded the named problem area inside the new cards. -- Out of scope: syntax colors, selection, active line. -- For Emre: This is still a palette steer, not a rebinding. Flag if editor should instead reuse `surfaceInset` / `surfaceBase` and drop the dedicated tokens. - -### 2026-09-01 — Light shadows quieter -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `shadowSubtle`, `shadowSoft`, `shadowMedium`, `shadowStrong`, `shadowOverlay` -- Before → after (light): `.06/.11/.18/.27/.38` → `.035/.055/.08/.16/.22` -- Why: Card and overlay shadows were doing too much of the elevation job once surfaces got closer together. One primary cue (surface step), not surface + heavy shade. -- Out of scope: dark shadows; glass tokens; shadow *geometry* (see next entry). -- For Emre: Dialogs/menus use `shadowStrong` / `shadowOverlay` — check they still float after the opacity cut. - -### 2026-09-01 — Notebook cell and details drawer shadow geometry -- Lift: binding (geometry, same tokens) -- Status: applied locally -- Modes: both (geometry is shared; light opacity change is what you feel first) -- Binding: - - `CellWrapper` hover `0 16px 44px shadowSoft` → `0 1px 2px shadowSubtle, 0 3px 8px shadowSoft` - - `CellWrapper` focus extra `0 18px 50px shadowMedium` → `0 1px 2px shadowSubtle, 0 4px 10px shadowSoft` (accent ring unchanged) - - `Drawer` `-18px 0 52px shadowSoft` → `-4px 0 16px shadowSoft` -- Why: Token opacity alone cannot tuck a 50px blur. Notebook cells and the details drawer were the named surfaces. -- Out of scope: dropdown/modal stacks in `overlayStyles.ts` (`0 1.2rem 1.6rem`, `0 2.4rem 7.2rem`). Call out if those should follow. -- For Emre: Higher lift than the opacity cut. Resting cells still have no shadow (border only); hover/focus are the ones tucked. Dark mode inherits the tighter geometry — glance at a focused cell in dark. - -### 2026-09-01 — Light result grid follows the dense-data ladder -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `gridRow`, `gridHeader` -- Before → after (light): `gridRow` `#e9ebef` → `#f4f5f7`; `gridHeader` `#dce0e6` → `#e7eaee` -- Neighbors (light, per `THEMING.md`): field `surfaceInset` `#f7f8f9` lightest → rows `#f4f5f7` → headers `#e7eaee` strongest gray. Same deltas as the pre-lift ladder (`#eceef1` / `#e9ebef` / `#dce0e6`), shifted with the new inset. `gridRow` now matches `surfaceRaised`, so a notebook cell’s body and its grid rows are one surface; the header bar is the structure cue. `gridSelection` / `gridFocus` / `contentObject` unchanged. -- Walked: standalone result grid, notebook inline grids. -- Why: The surface lift moved `surfaceInset` (empty field / viewport) and left `gridRow` / `gridHeader` on the old gray. The THEMING order was intact but the steps were the old muddy slab inside the new baseline. -- Out of scope: dark grid; zebra (the renderer does not stripe; hover uses `surfaceInset`); column-name crimson (`contentObject` — parking lot / binding). -- For Emre: Update the Result grid table in `THEMING.md` if accepted. Confirm header still reads as the strongest gray on a full-width grid and inside a raised cell. Selection `#e0afbf` was not retuned — glance at a selected row on the new `gridRow`. - -### 2026-09-01 — Light segmented chips sit on the new baseline -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `controlTrack`, `interactionNeutral`, `interactionNeutralHover` -- Before → after (light): - - `controlTrack` / `interactionNeutral` `#d9dce2` → `#e8eaee` (same hex as new `surfaceCanvas`) - - `interactionNeutralHover` `#d2d6dd` → `#e2e5ea` (same hex as new `surfaceStage`) -- Neighbors: Table/Chart chips use `controlTrack` as the segmented substrate on `surfaceRaised` `#f4f5f7`. Old track was the pre-lift canvas, so the chip block jumped ~20 RGB points off the header. New track is a one-step inset. Selected segment is still the glass lens (`glassSurface`), not these fills. `borderStrong` on the track is unchanged. -- Walked: notebook cell Table/Chart chips. List/Grid uses the same `NotebookViewToggle` track. -- Why: Leftover `#d9dce2` after the surface lift. The chip group read as a dark elevated slab. -- Out of scope: `borderStrong` on the segmented control; glass lens opacity; dark mode. -- For Emre: `THEMING.md` already maps `controlTrack` to segmented substrate and switch tracks — update the light hexes. Switches and other segmented controls share this token; check a switch at rest. - -### 2026-09-01 — Chrome hairline stagger: rails vs panel/work -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `borderDefault`, `borderSubtle` (values unchanged) -- Binding: icon rails (`Sidebar` left/right, `TopBar`) used `borderSubtle`; now `borderDefault`. Panel-to-work edges stay `borderSubtle` (`Console` left `Wrapper`, `SidePanelRight`, `Drawer`, allotment sash). -- Neighbors: `borderSubtle` is internal only — inside a panel or inside the working surface. Chrome rails sit one step stronger so the stagger reads. -- Walked: left rail | schema | editor; editor | details/AI | right rail; top bar under the logo/menu. -- Why: All chrome hairlines were the same weight, so rails and content panels sat on one plane. -- Out of scope: footer `border-top`; allotment sash hover (`contentAccent`); token values themselves. -- For Emre: Keep the stagger in the chrome composition notes. Do not bump panel-to-work to `borderDefault` — that would collapse the two weights. - -### 2026-09-01 — Content sidebars: both elevated -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `surfaceRaised` (values unchanged) -- Binding: schema (`Schema` / `SearchPanel` wrappers + `PaneContent` + `Console` left `Wrapper`), details (`Drawer` shell + `ContentWrapper`, `SidePanelRight`), AI (`AIChatWindow` shells, lazy loader, error boundary, history), mobile schema (`SideMenu`) used `surfaceBase`; now `surfaceRaised`. Icon rails stay `surfaceBase`. -- Neighbors: light `surfaceBase` `#eef0f3` → panels `#f4f5f7` (`surfaceRaised`). Dark `surfaceBase` `#17181d` → panels `#1d1e24`. Work surface stays `surfaceStage` / editor canvas. `PaneWrapper` / `PaneContent` globally are still `surfaceBase` so Editor / Result / Import are untouched. -- Walked: tables tree vs notebook; details drawer vs notebook; AI chat vs notebook. Light and dark. First pass left the tree on `surfaceBase` because `PaneContent` paints that fill over the wrapper — override both, with `&&` so the shared primitives cannot win on equal specificity. -- Why: Dark treated details/AI as a lift and tables as mute. Light had almost no step from canvas to either sidebar. Same role on both sides, both modes. -- Out of scope: `Panel.Header` (already `surfaceRaised` — title bar now matches the panel body); filter input (`surfaceInput`); result/editor panes; palette retune of `surfaceRaised`; drawer drop shadow (still `shadowSoft` on the right only). -- For Emre: Left and right content panels share `surfaceRaised`. Rails stay `surfaceBase`. Schema/Search must override `PaneContent` as well as `PaneWrapper`. Drawer header no longer steps above the body — the step is panel vs work, not title vs body. - -### 2026-09-01 — Light chrome shadows: tighter geometry -- Lift: binding (geometry, same tokens) -- Status: applied locally -- Modes: light, except tab-rail hairline is both -- Tokens: `shadowSubtle`, `shadowSoft`, `shadowMedium`, `borderSubtle` (values unchanged) -- Binding: - - Table/Chart (and List/Grid) glass lens `0 3px 9px shadowSoft` → `0 1px 1px shadowSubtle, 0 1px 3px shadowSoft` in light. Dark kept. Hover-preview on Run/Draw matches. - - Notebook title bar `0 12px 24px shadowSoft` → `0 1px 2px shadowSubtle, 0 2px 6px shadowSoft` in light. Existing `borderSubtle` hairline kept. Dark kept. - - Tab rail under-shadow `0 8px 20px shadowSoft` → same tight pair as the notebook title in light. Dark kept. - - Tab rail hairline: `border-bottom: 0` → `borderSubtle` in both modes, sitting with the shadow. - - Tab overflow fades: 10px `shadowMedium` → transparent → 6px with the stop at 65%, light only. -- Neighbors: same two-stop recipe as cell hover (`shadowSubtle` contact + `shadowSoft` lift), just smaller because these sit on chrome not on the stage. -- Walked: Table/Chart chip, notebook heading, tab rail with enough tabs to overflow. -- Why: Light opacity was already cut; 20–24px blurs still read as haze. Cloudflare-style lift is a 1–2px offset and a short blur. -- Out of scope: dark geometry (except the new tab hairline); active-tab glass (`0 6px 18px` still); Metrics heading; cell hover (already tucked); drawer; token opacities. -- For Emre: Geometry only. Do not retune `shadowSoft`. Overflow uses `html[data-theme="light"]` because those fades live in SCSS. - -### 2026-09-01 — Chat history hover follows the raised panel -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `surfaceBase` (light), `surfaceInput` (dark) — values unchanged -- Binding: `ChatHistoryItem` hover used `surfaceRaised`, which is now the panel fill. Hover inverts against the panel: one step darker in light (`surfaceBase` `#eef0f3`), one step lighter in dark (`surfaceInput` `#21222c`). -- Neighbors: panel `surfaceRaised` (`#f4f5f7` / `#1d1e24`). `surfaceInput` lightens both modes, so it is wrong in light. `surfaceOverlay` equals `surfaceRaised` in dark. `interactionHover` would invert, but as a wash rather than a surface step. -- Walked: AI chat history, light and dark. -- Why: Elevating the AI panel to `surfaceRaised` left hover on the same token. First rebind to `surfaceInput` everywhere; Zack caught that light hover should recede, not lift. -- Out of scope: rest/current row fill (still transparent); date separators; palette retune of `surfaceOverlay` in dark. -- For Emre: Hover on a raised list is not `surfaceRaised`. Light uses `surfaceBase`, dark uses `surfaceInput`. A single inverting overlay (`interactionHover`) would avoid the mode split if that role is the intended meaning. - -### 2026-09-01 — Tooltip border matches floating menus -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `borderDefault` (values unchanged) -- Binding: `Tooltip` box and arrow stroke used `contentDisabled` (a type color, reads as heavy as `borderStrong` or heavier). Rebind to `borderDefault`, same as `floatingSurfaceStyles` / dropdown menus. -- Neighbors: `borderSubtle` → `borderDefault` → `borderStrong`. Menus sit on `borderDefault`. Tooltip should not sit above them. -- Walked: editor toolbar tooltips, notebook heading actions, both modes. -- Why: After the surface lift the `contentDisabled` stroke was a hard outline on a quiet panel. -- Out of scope: tooltip fill (`surfaceInset`); dropdown/popover borders (already `borderDefault`); `contentDisabled` as type. -- For Emre: Tooltip chrome is a floating surface. Its edge is `borderDefault`, not a content token. - -### 2026-09-01 — Inline AI action cards invert in light -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: `surfaceValue` (light), `authBackdrop` (dark) -- Binding: `UserRequestBox` (Explain / Fix / Schema / Ask cards) used `authBackdrop` in both modes. That token is the login field: dark `#1d070e` recedes, light `#c7cbd2` is a muddy slab darker than the raised chat panel. Light now uses `surfaceValue` `#ffffff`. Dark stays on `authBackdrop`. -- Neighbors: chat panel `surfaceRaised` `#f4f5f7`. Nested SQL uses `editorCanvas` `#eef0f3`, so the query block recedes slightly inside the white card. Login `authBackdrop` is untouched. -- Walked: Explain Query card in the AI panel, light mode. Dark glance to confirm the crimson well remains. -- Why: Darker-on-dark should invert to lighter-on-light. `authBackdrop` cannot do that job in both themes. -- Out of scope: login page; palette retune of `authBackdrop`; inner LiteEditor. Plain `MessageBubble` follows later the same day. -- For Emre: AI action cards are not login chrome. Light fill is `surfaceValue`. - -### 2026-09-01 — Cell icon hover is one step darker in light -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: `surfaceBase`, `interactionNeutralHover` (values unchanged) -- Binding: Ghost `IconButton` hover is `surfaceRaised`, which is the cell fill, so Maximize / More vanish. Light hover on those is now `surfaceBase` (`#eef0f3` on `#f4f5f7`). Split / reset inside the Table/Chart track sit on `controlTrack`; light hover is `interactionNeutralHover` (`#e2e5ea` on `#e8eaee`). Dark still uses `surfaceRaised`. -- Neighbors: do not retune global ghost hover — on `surfaceBase` chrome (rails, notebook title) `surfaceRaised` is the correct lift. -- Walked: focused cell Maximize and More, Table/Chart split icon, light mode. -- Why: Same-token hover on a raised cell. Dark already reads; light needed one receding step. -- Out of scope: global ghost; Table/Chart unselected `interactionHover`; markdown Edit/Apply; token opacities. -- For Emre: Ghost hover cannot be one fill everywhere. On raised cells it must recede (`surfaceBase`); on base chrome it must lift (`surfaceRaised`). - -### 2026-09-01 — Neutralise brand on fields, lists, and grid hover -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `borderStrong`, `borderDefault`, `interactionHover`, `interactionNeutral` (values unchanged) -- Binding: - - Default `Input` / `TextArea` focus: `contentAccent` → `borderStrong`. Accent is opt-in via `$tone="accent"` (AI chat composer, chat history search, login). - - Assistant Settings provider tab underline: `contentAccent` → `borderStrong`. Fields inherit the new default. - - `TableSelector` trigger and items: `borderAccent` / `interactionAccent*` → `borderDefault` / `interactionHover` / `interactionNeutral`. - - Schema tree row hover/focus and context-menu lock: `interactionAccent*` / `borderAccent` → `interactionHover` / `interactionNeutral` / `borderDefault`. Details `i` → `contentSecondary`. - - Result grid row hover wash: `interactionAccentHover` → `interactionHover`. Frozen-handle hover bar: `contentAccent` → `borderStrong`. -- Neighbors: rail `Navigation` stays `contentAccent` (tool selection). Table/column glyphs still `contentAccent`. `SelectMenu` check and open border stay accent (AI model dropdown). Grid *selection* `gridSelection` and column-resize ghost stay branded. Copy-pulse on schema rows stays accent. -- Walked: Filter... in tables, table picker, tree hover, result-grid hover, Assistant Settings provider + API key, AI composer (still crimson), login (still crimson). -- Why: Accent was doing hover, focus, and selection jobs that are not high-level brand actions. -- Out of scope: `contentObject` column names; tree type icons; rail icons; `SelectMenu`; `TabButton` globally (Monitoring/Details still accent); `gridSelection`; checkboxes/switches. -- For Emre: Default field focus is `borderStrong`. Accent is `$tone="accent"`. Do not retune `contentAccent` to fix these. **Superseded in part:** provider-tab underline reverted the same day (tabs stay branded); grid selection/resize and column names handled in the following two entries. - -### 2026-09-01 — Grid selection and cell-focus ring go slate -- Lift: palette -- Status: applied locally -- Modes: both -- Tokens: `gridSelection`, `gridFocus` -- Before → after: - - light `gridSelection` `#e0afbf` → `#d8dce3`; `gridFocus` `#8a0f35` → `#828b99` (same as light `borderStrong`) - - dark `gridSelection` `#2b1d25` → `#252830`; `gridFocus` `#b81447` → `#6b7382` (stronger than dark `borderStrong` `#3d414d` so the 1px inset still reads on the selected cell) -- Neighbors: light `gridRow` `#f4f5f7`, `gridHeader` `#e7eaee`. Dark `gridRow` `#17181d`, `gridHeader` `#202126`. Selection must stay stronger than the `interactionHover` overlay. -- Walked: result-grid row selection, focused cell ring, copy-pulse (now slate). Query-picker first-visit pulse rebound off `gridFocus` so it stays branded. -- Why: Selection wash and cell ring were still pink after hover went neutral. -- Out of scope: `editorSelection`; notebook cell focus (`CellWrapper`); checkboxes/switches. -- For Emre: `gridFocus` is the grid's keyboard/copy ring, not brand. Light matches `borderStrong`. Dark is a step lighter than `borderStrong` on purpose. - -### 2026-09-01 — Search, keyboard focus, column names, resize -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `borderStrong`, `borderDefault`, `interactionHover`, `interactionNeutral`, `contentPrimary`, `contentSecondary` (values unchanged) -- Binding: - - Editor Search results: hover `interactionHover`; focused row `interactionNeutral` + `borderDefault` (same as schema tree). File icons `contentSecondary`. Case/word/regex toggles `$activeTone="neutral"` + `borderDefault`. - - Global keyboard focus outlines: `contentAccent` → `borderStrong` (`*:focus-visible`, `Button`, ghost buttons, chrome-tab outline, DocSearch button, warning link, instance-settings slider, chat-history item, assistant-modes compact, segmented-control inset ring). Tab *rename* field and other leftover field-focus borders follow (`_editor`, slim-select, quick-vis). Split sash hover fill → `borderStrong` (same job as column resize). - - Result-grid column names: `contentObject` → `contentPrimary` (`HeaderName` and legacy `.qg-header-name`). - - Column-resize ghost: `contentAccent` → `borderStrong` (matches frozen-handle hover bar). - - Revert: Assistant Settings provider tab underline back to `TabButton` `contentAccent`. -- Neighbors: `TabButton` underline stays `contentAccent`. Notebook cell focus (`CellWrapper`) and cell-name field stay branded. Notebook title glyph and chrome-tab notebook/metrics favicons stay `contentObject`. Switch/Checkbox outlines stay branded. -- Walked: Search sidebar, result grid headers/selection/resize, tab underlines (Monitoring/Details, AI setup), notebook cell ring, keyboard tab through chrome. -- Why: Second binding pass. Tabs and notebook identity stay brand; search, focus rings, grid type, and resize are chrome. -- Out of scope: checkboxes/switches; rail `Navigation`; schema tree glyphs; `SelectMenu`; AI sparkle/MCP/pairing. -- For Emre: Keyboard focus is `borderStrong`. List hover/focus matches the schema tree. Column names are type (`contentPrimary`), not object glyphs. - -### 2026-09-01 — Chat history fields and dropdown open state -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `borderStrong` (values unchanged) -- Binding: - - Chat history search: drop `$tone="accent"` so it inherits default field focus (`borderStrong`). Composer stays `$tone="accent"`. - - Chat history rename: `actionPrimary` → `borderStrong`. - - `SelectMenu` open trigger: `borderAccent` → `borderStrong` (chart X-axis, AI model picker, and every other SelectMenu). Checkmark stays `contentAccent`. -- Neighbors: same `borderStrong` as Input focus and keyboard outlines. Login fields still `$tone="accent"`. -- Walked: chat history search + rename, AI model dropdown, chart X-axis dropdown. -- Why: These were still opted into brand after default field focus went neutral. -- Out of scope: SelectMenu check; AI Settings label; sparkle glyphs; composer; login; checkboxes/switches. -- For Emre: Open dropdown chrome is field focus, not brand. Check remains a selected-item glyph. - -### 2026-09-01 — Results action bar owns the pane-top hairline -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `borderSubtle` (values unchanged) -- Binding: Result `Actions` gains `border-top` `borderSubtle`. Log `Notifications` wrapper drops `border-bottom` so log + action bar do not stack two lines. Action bar `border-bottom` (to the grid) stays. -- Neighbors: same weight as other panel-to-work edges (schema wrapper, drawer, allotment sash). Rails stay `borderDefault`. -- Walked: SQL results with schema open (action bar meets sidebar), log expanded and collapsed, grid and chart. -- Why: Schema lives only in the top pane. The results bar spans full width under it; without a top edge the two `surfaceRaised` panels fused. -- Out of scope: notebook `ResultActionsBar` (inside a cell, not against the schema). -- For Emre: The results pane's top hairline belongs on the action bar, not the log, because the log does not span under the sidebar. - -### 2026-09-01 — Light scrollbar thumb follows the surface ladder -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `scrollbarThumb` -- Before → after (light): `#62656b` → `#c5cad3` -- Neighbors: sits between `borderDefault` (ink `.15`) and `borderStrong` `#828b99`. Dark thumb stays white `.13`. -- Walked: search list, result grid, AI chat, news drawer. -- Why: The old thumb was charcoal on the lifted light surfaces, so every overflow read as a dark rail. -- Out of scope: dark thumb (already white `.13`). Monaco slider rebound in a later entry. -- For Emre: Light scrollbars are chrome, not content. Do not reuse `contentDisabled`. - -### 2026-09-01 — Light rails, results pane, and webkit scrollbars -- Lift: binding -- Status: applied locally -- Modes: light (rails/footer); both (results, news, webkit) -- Tokens: `surfaceRaised`, `scrollbarThumb`, `borderSubtle` (values unchanged except the thumb palette above) -- Binding: - - Icon rails (`Sidebar`) and footer: light `surfaceBase` → `surfaceRaised`. Dark rails stay `surfaceBase`. - - Result pane `Root` + `PaneContent`: `surfaceBase` → `surfaceRaised` (same as log, action bar, schema, AI). - - News list fill: `surfaceBase` → `surfaceRaised`; row rules `surfaceRaised` → `borderSubtle` so they do not vanish. - - Global `::-webkit-scrollbar` now paints `scrollbarThumb` (Firefox already used `scrollbar-color`). -- Neighbors: light chrome frame is one fill (`surfaceRaised` `#f4f5f7`) against work (`surfaceValue` / grid). Stagger is the `borderDefault` rail hairline, not a darker grey. -- Walked: left nav vs results, right nav vs AI, search overflow, grid overflow, footer. -- Why: Rails and the results well were still on `surfaceBase` after content panels moved up, so the grey frame looked unthemed. Chrome/Safari never read `scrollbar-color`. -- Out of scope: TopBar (still `surfaceBase`); editor `PaneWrapper`; checkboxes; `surfaceBase` palette. -- For Emre: Light icon rails share the panel fill. Dark still recedes (`surfaceBase`). Webkit thumbs are a global chrome rule. - -### 2026-09-01 — Run-query menu divider stays a hairline on hover -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `borderSubtle`, `interactionHover`, `surfaceOverlay` (values unchanged) -- Binding: `ButtonBar` dropdown was a one-off (`surfaceInset` + secondary `Button` items). Secondary hover paints `borderStrong`, which recolored the `borderSubtle` item rule in light (`#828b99` on a pale menu). Menu now uses `floatingSurfaceStyles`. Item hover is `interactionHover` and the divider stays `borderSubtle`. -- Neighbors: same recipe as `DropdownMenu` / `SelectMenu`. -- Walked: Run query chevron in light and dark, hover each item. -- Why: The line was not a separate divider component — it was the button hover border winning. -- Out of scope: splitting the items off `Button`; checkboxes. -- For Emre: Menu items that are `Button`s cannot keep the default secondary hover border. - -### 2026-09-01 — Monaco scrollbar uses the shared thumb -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `scrollbarThumb` (values unchanged) -- Binding: Monaco overlay slider `interactionNeutral` → `scrollbarThumb`. -- Neighbors: dark `interactionNeutral` `#32343e` happened to match the quiet white thumb; light `#e8eaee` sat on `editorCanvas` `#eef0f3` and disappeared / mismatched webkit `#c5cad3`. -- Walked: SQL editor overflow, both modes. -- Why: Dark already looked themed. Light was the leftover. -- Out of scope: Monaco theme JSON (`dracula.ts` menu separators still `interactionNeutral`). -- For Emre: Editor chrome scrollbars are `scrollbarThumb`, not a list-selection fill. - -### 2026-09-01 — Light user chat bubbles lift off the panel -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: `surfaceValue`, `borderDefault` (values unchanged) -- Binding: plain `MessageBubble` fill `authBackdrop` → `surfaceValue` in light. Dark stays `authBackdrop`. Border was already `borderDefault`. -- Neighbors: chat panel `surfaceRaised` `#f4f5f7`. Bubble `#ffffff` + `borderDefault` (ink `.15`). Same as `UserRequestBox`. -- Walked: typed user message in the AI panel, light mode. Dark unchanged. -- Why: `authBackdrop` light `#c7cbd2` recedes on the raised panel. Lighter-on-light needs `surfaceValue`. -- Out of scope: assistant reply (not a bubble); composer; login `authBackdrop`. -- For Emre: User chat chrome in light is `surfaceValue` + `borderDefault`, not the login well. - -### 2026-09-01 — Light ghost hover recedes, not lifts -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: `interactionHover` (values unchanged) -- Binding: ghost `Button` hover used `surfaceRaised`. On tab-rail / chat-header chrome that is already `surfaceRaised` (or close), hover lifted toward white. Light hover is now `interactionHover` (ink wash, same as Add metrics / Select). Dark still `surfaceRaised`. -- Neighbors: `SegmentedControlButton` hover is already `interactionHover`. Cell maximize/more still override light hover to `surfaceBase` on raised cells. -- Walked: Add New, Tab History, Tab Settings (SQL tab rail + AI header). -- Why: Ghost on grey chrome must darken. `surfaceRaised` is an elevation step, not a hover wash. -- Out of scope: dark ghost; pressed History chip (`surfaceRaised` + `borderDefault`). -- For Emre: Light ghost hover is the inverting overlay, not a surface lift. - -### 2026-09-01 — Schema toolbar: 2px gap between action chips -- Lift: binding (layout) -- Status: applied locally -- Modes: both -- Binding: Schema header `Box` around Add metrics / Select / Auto refresh `gap="0"` → `gap="0.2rem"` (2px at the 10px rem root). -- Walked: tables panel with auto-refresh on, hover Select. -- Why: Hover fills were flush, so Select fused with the active refresh chip. -- Out of scope: other `gap="0"` toolbars. -- For Emre: Spacing only; tokens unchanged. - -### 2026-09-01 — Light `borderStrong` quieter for activated controls -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `borderStrong` -- Before → after (light): `#828b99` → `#b0b7c2`. Dark unchanged `#3d414d`. -- Neighbors: rest stroke is `borderDefault` (ink `.15` ≈ `#dddfe2` on white). Activated hover/open/focus still steps up, but not to charcoal. `gridFocus` stays `#828b99` — denser grid ring, not the chrome stroke. -- Walked: AI model SelectMenu open, Download as Parquet hover, cell Auto Refresh SelectMenu, Filter/Search fields on focus, ThemeModeSelector open. -- Why: Activated chrome stroke was jumping the scale. Same role (`borderStrong`) — the light value was too heavy. -- Out of scope: dark; brand accent fields (`$tone="accent"`); `gridFocus`. -- For Emre: Light `borderStrong` is now the quiet activated control stroke. Keyboard outlines, sash hover, checkbox rest, and segmented-control track also inherit. Flag if those jobs need to stay at the old `#828b99` (would then be a new role, not this one). - -### 2026-09-01 — Chat history hover matches dropdown items -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: `interactionHover` (values unchanged) -- Binding: `ChatHistoryItem` hover `surfaceBase` → `interactionHover` in light. Dark stays `surfaceInput`. -- Neighbors: dropdown / schema-tree / search-row hover is `interactionHover`, not a surface. `surfaceBase` `#eef0f3` on a `surfaceRaised` `#f4f5f7` panel was a solid step and read as a different family. -- Walked: AI chat history list, light mode. -- Why: Same job as a menu item hover. Not a surface, and not one step lighter — the ink wash. -- Out of scope: dark (still a solid recede on the raised panel); selected/"Current" treatment (text only). -- For Emre: List-row hover is `interactionHover` in light. Do not invent a lighter surface for this. - -### 2026-09-01 — Light scrollbar thumb one notch quieter -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `scrollbarThumb` -- Before → after (light): `#c5cad3` → `#d2d6de`. Dark unchanged (`white` `.13`). -- Neighbors: still darker than `surfaceRaised` `#f4f5f7` / `surfaceCanvas` `#e8eaee`, quieter than `borderStrong` `#b0b7c2`. Webkit, Firefox `scrollbar-color`, and Monaco overlay all read this token. -- Walked: schema tree, editor, AI panel overflows. -- Why: After the surface lift the thumb still sat a bit heavy. One step toward the chrome, not a new role. -- Out of scope: dark; track (stays transparent). -- For Emre: Same `scrollbarThumb` role. Light value only. - -### 2026-09-01 — Chrome-tabs plus hover is the ghost leftover -- Lift: binding -- Status: applied locally -- Modes: light -- Tokens: `interactionHover` (values unchanged) -- Binding: `.new-tab-button:hover` in `_react-chrome-tabs.scss` used `surfaceRaised`, more specific than the ghost `Button` rule. Light hover → `interactionHover`. Dark still `surfaceRaised`. -- Neighbors: same wash as tab-rail History / Settings ghosts and Add metrics. -- Walked: SQL tab bar plus in light. -- Why: The plus is not an `IconButton`; chrome-tabs owns its hover in SCSS. -- Out of scope: tab close / edit chip hovers (still `surfaceRaised`); pressed History chip. -- For Emre: Chrome-tabs plus is ghost chrome. Do not let SCSS reintroduce a surface lift. - -### 2026-09-01 — Light scrollbar thumb into stone, not a cooler neighbor -- Lift: palette -- Status: tweaked (too warm; see following entry) -- Modes: light -- Tokens: `scrollbarThumb` -- Before → after (light): `#d2d6de` → `#ddd8d1`. Dark unchanged. -- Neighbors: no warm chrome role to borrow. Surfaces, controls, and `borderStrong` are all cool blue-gray. -- Why: The previous notch was not visible. Hue shift was noticeable, but too warm. - -### 2026-09-01 — Light scrollbar thumb cooler, still a real step -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `scrollbarThumb` -- Before → after (light): `#ddd8d1` → `#d7dbe3`. Dark unchanged. -- Neighbors: same cool family as `surfaceCanvas` `#e8eaee` and `borderStrong` `#b0b7c2`. Lighter than the original `#c5cad3`, cooler than the stone miss. -- Walked: schema, editor, AI overflows. -- Why: Stone was too warm. Stay on the cool ladder; take a larger lightness step than `#d2d6de` so it still reads quieter. -- Out of scope: dark. -- For Emre: Light thumb is cool chrome, not stone. `#c5cad3` → `#d7dbe3`. - -### 2026-09-01 — Provider choice cards go neutral -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `interactionHover`, `interactionNeutral`, `borderDefault` (values unchanged) -- Binding: `SelectableCardButton` hover `borderAccent` / `interactionAccentHover` → `borderDefault` / `interactionHover`. Selected `borderAccent` / `interactionAccentActive` / inset accent ring → `interactionNeutral` + `borderDefault` (same as list selected). -- Neighbors: AI setup provider cards and Add Metric type cards share this component. -- Walked: Add a model provider (OpenAI / Anthropic / Custom), both modes. -- Why: Choice cards are chrome selection, not a brand action. -- Out of scope: `TabButton` underlines (still branded); composer `$tone="accent"`. -- For Emre: Large-target selected cards match list selected, not accent. - -### 2026-09-01 — Multi-step pill gets a visible hairline -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `borderDefault` (values unchanged) -- Binding: `StepIndicatorContainer` had only `shadowSubtle`. Add `borderDefault` so the capsule reads on the modal. -- Neighbors: modal itself is `borderDefault`. Inner step name chip stays `interactionNeutral`. -- Walked: Add a model provider step pill, light and dark. -- Why: Shadow alone disappeared, especially in dark. `borderSubtle` would still vanish; `borderDefault` is the visible-subtle step. -- Out of scope: pill fill. -- For Emre: Step chrome uses the same stroke as the modal edge. - -### 2026-09-01 — Status badges are chips; count badges stay pills -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `statusSuccessSurface`, `statusDangerSurface`, `statusWarningSurface`, `statusInfoSurface`, `interactionNeutral` (values unchanged here) -- Binding: `Badge` default `shape="chip"` — 4px radius, 5px padding, 400/11px, no border, status surface fills. `shape="pill"` keeps the old stadium + 10%/32% wash. `TabBadge` (Monitoring warning/error counts) is pinned to `pill`. -- Neighbors: AI Validated chip, import file-status, Type/Trend badges, “New”, version/EE badges inherit chip. Monitoring tab count must not. -- Walked: Assistant Settings Validated chip; table details Monitoring tab. -- Why: Figma status chips are rounded rects on a status well, not stadium pills. Count badges on tabs are a different job. -- Out of scope: sidebar Enabled/Inactive (`StatusChip`, not `Badge`). -- For Emre: Two shapes on one primitive. Pill is the compact count. Chip is the status label. - -### 2026-09-01 — Light success wells denser so chips read -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `statusSuccessSurface` -- Before → after (light): `rgba(8, 122, 80, 0.1)` → `rgba(8, 122, 80, 0.16)`. Dark unchanged. -- Neighbors: `statusSuccess` `#067047` / `statusSuccessBorder` `.28`. Sits on `surfaceOverlay` `#fafbfc` and selected `interactionNeutralHover` `#e2e5ea`. -- Walked: Assistant Settings Enabled chip and Validated chip, light mode. -- Why: `.10` washed out on the new light ladder. Typical light success chips need a denser well. -- Out of scope: dark success surface; danger/warning/info surfaces. -- For Emre: Light success fill is the chip/banner well. `.16` is still a wash, not a solid green-100. - -### 2026-09-01 — Light Enabled chip uses the success well -- Lift: binding -- Status: applied locally -- Modes: light (dark unchanged) -- Tokens: `statusSuccessSurface`, `statusSuccessBorder`, `surfaceValue`, `borderDefault` (values as above) -- Binding: sidebar `StatusChip` Enabled was `interactionNeutral` in both modes — same as the selected provider row (`interactionNeutralHover`) in light, so the chip vanished. Light Enabled → `statusSuccessSurface` + `statusSuccessBorder`. Light Inactive → `surfaceValue` + `borderDefault`. Dark stays `interactionNeutral`, no border (Figma). -- Neighbors: selected provider `interactionNeutralHover` `#e2e5ea`. Modal `surfaceOverlay` `#fafbfc`. -- Walked: Assistant Settings sidebar, light, selected Enabled row. -- Why: Light chips need a fill that is not the selected-row gray. Enabled is a success state. -- Out of scope: dark Enabled (already matches Figma); Monitoring `TabBadge`. -- For Emre: Light Enabled is a success chip. Dark Enabled is a neutral well with success type. - -### 2026-09-01 — AI settings selected provider is one step quieter -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `interactionNeutral` (values unchanged) -- Binding: `ProviderTab` selected `interactionNeutralHover` → `interactionNeutral`. -- Neighbors: unselected hover stays `controlSurfaceHover`. Modal is `surfaceOverlay`. Tab underline stays `contentAccent`. -- Walked: Assistant Settings sidebar, light. -- Why: Selected row was a notch too heavy on the lifted light ladder. Same interaction role, one step quieter. -- Out of scope: editor/drawer `TabButton` (still `surfaceRaised`). -- For Emre: Vertical provider tabs use the list-selected well, not the hover well. - -### 2026-09-01 — Models empty well is inset, not the dialog scrim -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `surfaceInset` (values unchanged) -- Binding: `ModelsPlaceholder` `surfaceScrim` → `surfaceInset`. -- Neighbors: modal `surfaceOverlay` `#fafbfc`; inputs `surfaceInput` `#fafbfc`. Scrim is 32% ink — a backdrop, not a well. -- Walked: Assistant Settings before API key validation, light. -- Why: `surfaceScrim` is occlusion. An empty models region is an embedded well. -- Out of scope: Overlay / docsearch / grid fades still use scrim. -- For Emre: Placeholder copy sits on inset, not the dimmer. - -### 2026-09-01 — Models empty well one step heavier -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `surfaceRaised` (values unchanged) -- Binding: `ModelsPlaceholder` `surfaceInset` → `surfaceRaised`. -- Neighbors: modal `surfaceOverlay` `#fafbfc`. Light inset `#f7f8f9` vanished; raised `#f4f5f7` is the next visible well. -- Walked: Assistant Settings Enable Models placeholder, light. -- Why: Inset was a notch too close to the modal. Same empty-well job, one step more present. -- Out of scope: scrim; other inset wells. -- For Emre: Empty models copy on raised, not inset. - -### 2026-09-01 — Form selects use the input well -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `surfaceInput` (values unchanged) -- Binding: `SelectMenuControl` trigger was secondary `controlSurface`. Form fields should match `Input` (`surfaceInput`). Toolbar `SelectMenu.Trigger` stays `controlSurface`. -- Neighbors: Provider Type next to Provider Name / Base URL / API Key inputs. -- Walked: Add Custom Provider, light. -- Why: Same row of fields, two fills. Form selects are fields, not toolbar buttons. -- Out of scope: toolbar dropdowns. -- For Emre: `SelectMenuControl` is the form select. Trigger fill is `surfaceInput`. - -### 2026-09-01 — Footer Connected mark matches Ingesting -- Lift: binding (geometry) -- Status: applied locally -- Modes: both -- Tokens: `statusSuccess` / `statusDanger` (values unchanged) -- Binding: footer `ConnectionStatus` mark was a circle with success/danger glow. It is now the same rounded-square SVG as Ingesting / Enabled chips (`0.9rem`, 15% radius, 32% stroke). -- Neighbors: MCP / Ingesting marks in the same footer row. -- Walked: footer Connected, light and dark. -- Why: Status marks in one bar should share geometry. Color still carries the state. -- Out of scope: MCP pairing popover; Ingesting internals. -- For Emre: Footer status marks are squares, not dots. - -### 2026-09-01 — Provider Type uses the console select -- Lift: binding (control) -- Status: applied locally -- Modes: both -- Binding: Add Custom Provider `Select` (native ``. - -### 2026-09-02 — Light MCP notebook promo is a solid card -- Lift: binding -- Status: applied locally -- Modes: light -- Tokens: `surfaceValue` (values unchanged) -- Binding: `NotebookMcpPromo` `Container` was unfilled (stage showing through the dashed `contentAccent` stroke). Light fill is now `surfaceValue`. Dark stays transparent. -- Neighbors: notebook stage `surfaceStage`; cells `surfaceRaised`; command chip inside the promo already `surfaceRaised`. -- Walked: notebook MCP promo, light, expanded. -- Why: A dashed outline on the stage read as a hole. The notice is a card, not a well. White sits above the stage and above cells without a new token. -- Out of scope: dark fill; dashed stroke; `New` badge. -- For Emre: Light promo fill is `surfaceValue`. Dark is still the dashed stroke only. - -### 2026-09-04 — Switch is larger and squared -- Lift: binding (geometry) -- Status: applied locally -- Modes: both -- Tokens: unchanged (`controlTrack`, `controlKnob`, `contentAccent` / `statusSuccessStrong` on, `contentInverse` thumb) -- Binding: `Switch` track was a 36×20 pill with a 14px circular thumb. Default (`md`) is now 44×20, 6px radius, 1.5px inset, 24px rounded-square thumb (5px radius) — Figma node `1902:32067`. `sm` scales the same language (36×18 / 20px thumb). -- Neighbors: AI Settings model list (the Figma frame); Editor Settings; import settings; schema designated timestamp. -- Walked: Editor Settings switch, light and dark, on and off. -- Why: The pill read as a tiny iOS switch. Squared + longer travel makes the control a row action, not chrome jewelry. -- Out of scope: color retune (still branded on / success tone); checkboxes; Figma’s green-on — we keep current `tone`. -- For Emre: Switch geometry only. Do not copy Figma’s `#19ac43` onto the default accent switch. - -### 2026-09-04 — Switch stroke is 0.5px -- Lift: binding (geometry) -- Status: applied locally -- Modes: both -- Binding: `Switch` track stroke is a 0.5px inset `box-shadow` (not `border: 0.5px` — Chromium rounds that used value to 1px even at 2x). Padding is 2px so the hairline plus 1.5px gap still reads as 2px outside-to-thumb. Thumb height/travel stay `md` 16px / 16px. -- Neighbors: same as the squared-switch entry. -- Walked: Editor Settings switch, light, off and on. -- Why: 1px hairline plus 1.5px padding made a 2.5px gutter. Design is 0.5 + 1.5 = 2px outside-to-thumb. -- Out of scope: color; focus outline still 1px. -- For Emre: Switch rest/on stroke is 0.5px. Do not thicken it to match other controls. - -### 2026-09-04 — Table and mat-view glyphs are muted -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `contentMuted` (values unchanged) -- Binding: `TableIcon` (and standalone `MaterializedViewIcon`) `contentAccent` → `contentMuted`. Schema row title no longer forces accent onto nested SVGs. -- Neighbors: schema tree, details `TableSelector`, metrics table picker, AI schema chips, “Create materialized view” menu glyph. Rail table-details latch stays `contentAccent` when selected. -- Walked: schema tree + details table select, light. -- Why: Object-type marks were wearing brand. They are labels, not selected chrome. -- Out of scope: rail latches; notebook title glyphs. Column type glyphs: see following entry. -- For Emre: Table / mat-view / view identity icons are `contentMuted`. - -### 2026-09-04 — Column metadata glyphs are muted -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: `contentMuted` (values unchanged) -- Binding: `ColumnIcon` `TypeIcon` and designated-timestamp `SortDownIcon` `contentAccent` → `contentMuted`. -- Neighbors: schema tree column rows; details Columns list (same `ColumnIcon`). -- Walked: expanded table columns in schema tree, light. -- Why: Same job as table/mat-view marks — type labels, not brand. -- Out of scope: rail latches; copy-pulse highlight still uses `contentAccentStrong`. -- For Emre: Column type icons (including designated timestamp) are `contentMuted`. - -### 2026-09-04 — Base-table badge hover in dark -- Lift: binding -- Status: applied locally -- Modes: dark (light already this token) -- Tokens: `interactionHover` (values unchanged) -- Binding: `BaseTableLinkButton` (mat-view Details → Base Table) inherits ghost hover, which is `surfaceRaised` in dark — the same fill as the details drawer, so the wash disappears. Hover is now `interactionHover` in both modes, matching light ghost. -- Neighbors: drawer is `surfaceRaised`. Global dark ghost still `surfaceRaised` (deferred). -- Walked: `bbo_1s` Details, light and dark, hover on `market_data`. -- Why: Light ink wash is visible on raised chrome. Dark elevation hover is not. -- Out of scope: retuning dark `interactionHover`; other ghost buttons. -- For Emre: This badge uses the overlay, not a surface step. Flag if dark ghost should follow globally. - -### 2026-09-05 — Light tab rail is raised at 94% -- Lift: palette -- Status: applied locally -- Modes: light -- Tokens: `surfaceTabRail` -- Before → after (light): `rgba(232, 234, 238, 0.94)` → `rgba(244, 245, 247, 0.94)` (`#F4F5F7` at 94%) -- Neighbors: same RGB as `surfaceRaised` `#f4f5f7`, still the frosted rail (blur + 94%). Dark `rgba(18, 19, 23, 0.82)` unchanged. -- Walked: editor tab strip, light. -- Why: Rail was still the old canvas grey at 94%. Zack wants it to sit with raised chrome. -- Out of scope: dark rail; individual tab pills (still transparent / `interactionHover`). -- For Emre: Light `surfaceTabRail` is now raised-at-94%, not canvas-at-94%. - -### 2026-09-05 — Switch on-fill is a unique success fill -- Lift: palette + binding -- Status: applied locally -- Modes: both -- Tokens: `statusSuccessFill` (new). Meaning: opaque positive fill that does not need text contrast. Not `statusSuccess` / `Strong` (ink) and not a component-named `switchGreen`. -- Before → after: dark `#3fa659`, light `#1f7a39` (Figma `1919:102397` / Assistant Settings `1924:106893`, `1913:79570`) -- Binding: `Switch` on-state `contentAccent` / `statusSuccessStrong` → `statusSuccessFill`. Thumb is `contentInverse` in both states. Rest track in light is Figma `#c9cdd4` (not retuning `controlTrack` — segmented chips still use it). Stroke stays 0.5px inset: dark white `.15`, light ink `.25`. -- Geometry: track radius `8px` (`sm` `7px`), thumb radius `6px` (`sm` `5px`). Size still `44×20` / `24` thumb, `2px` pad. -- Neighbors: Enabled / Validated stay `statusSuccess` `#66bb6a` / `#067047`. The fill is brighter and more saturated on purpose. -- Walked: Editor Settings + AI Enable Models switches, light and dark. -- Why: Text greens cannot also be the on-track. Zack wants a reviewable unique green in the same family. -- Out of scope: checkboxes; hover step; retuning `controlTrack`; other controls. -- For Emre: `statusSuccessFill` is the opaque on-fill. Flag if the name should be `controlOn` once more controls share it. - -### 2026-09-06 — statusInfo family: Subtle, Text, Control -- Lift: palette -- Status: applied locally -- Modes: both -- Tokens: `statusInfo` (unchanged), `statusInfoSubtle` (new), `statusInfoControl` (new), `statusInfoSurface` (retuned to Subtle wash) -- Before → after: - - `statusInfo` stays `#81d3f9` / `#176f87` — info type + focus ring at 75% - - `statusInfoSubtle` dark `#b2e7ff`, light `#159cc1` — latch icon - - `statusInfoControl` dark `#0c80f3`, light `#0a88db` — checkbox / persistent on-fill (Option 2, bluer) - - `statusInfoSurface` dark Text `@10%` → Subtle `@5%` `rgba(178, 231, 255, 0.05)`; light Text `@10%` → Subtle `@10%` `rgba(21, 156, 193, 0.10)` (Figma latch well) -- Neighbors: do not retune `statusInfo`. Subtle is the quiet on-glyph, not a banner fill. Control is the opaque fill; porcelain ticks use `contentInverse` (~3.8:1 on Option 2). One wash — Surface is Subtle, not a second Text wash. -- Walked: Figma `1943:113543` / `115332` (latch), `115217` / `117006` (focus), `117179` / `118238` (checkbox) -- Why: Brand crimson was doing chrome jobs (latch, focus, checkbox). Info ramp splits quiet / type / fill so those jobs leave brand. -- Out of scope: `contentAccent` / `actionPrimary`; rail icons; tab underlines; notebook cell focus. -- For Emre: Three roles, one family. Flag if Surface should stay a Text wash for badges — latch Figma is Subtle at 5/10. - -### 2026-09-06 — Bind latches, focus, checkboxes, object glyphs to statusInfo -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: values unchanged here -- Binding: - - Non-rail latched icon buttons (`PrimaryToggleButton` `activeTone="info"`): schema auto-refresh / select, result + notebook freeze. Icon `statusInfoSubtle`, well `statusInfoSurface`. Idle stays `contentSecondary`. - - Rail `Navigation` (and other rail `PrimaryToggleButton`s) stay `$activeTone="accent"` — `contentAccent` + `interactionAccentActive`. Navigation pins the branded well so a later default change cannot leak. - - Keyboard focus (`*:focus-visible`, Button, Switch, Checkbox, chrome-tabs, leftover `borderStrong` rings) → `statusInfo` at 75% (`statusInfoFocus`). Notebook cell focus (`CellWrapper`) stays branded. - - `Checkbox` checked fill `contentAccent` → `statusInfoControl`; tick stays `contentInverse`. Schema multi-select filled circle unchanged. - - Notebook title glyph + chrome-tab notebook/metrics favicons: `contentObject` → `statusInfo` (light leaves brand `#b81447`). -- Neighbors: `TabButton` underline, AI `$tone="accent"`, `SelectMenu` check, switches (`statusSuccessFill`) unchanged. -- Walked: schema auto-refresh, freeze column, Search “Include closed tabs”, button focus, notebook glyph, rail icons. Light and dark. -- Why: Zack: only non-rail latches take the new pattern. Rail is the branded exception. -- Out of scope: `SelectMenu` check; cell focus; tab underlines; field focus borders (`Input` still `borderStrong` / accent). -- For Emre: `activeTone="info"` is the latch. Do not fold it into `accent`. `contentObject` is now unused in components — collapse when ready. - -### 2026-09-06 — Product brand is QDB Pink (same hexes both modes) -- Lift: palette -- Status: applied locally -- Modes: both -- Tokens: `contentAccent`, `contentAccentStrong`, `actionPrimary`, `actionPrimaryHover`, `interactionAccentHover`, `interactionAccentActive`, `borderAccent`, `borderAccentStrong`, `brandGradientStart`, `brandGradientEnd`, `aiGradientStart`, `aiGradientEnd` -- Before → after (both modes unless noted): - - `contentAccent` `#c94f74` / `#b81447` → `#f0428b` (Pink 300) — rail, tabs, remaining chrome accent - - `contentAccentStrong` `#cf1750` / `#8a0f35` → `#ee2b7c` (Pink 400) - - `actionPrimary` `#b81447` / `#8a0f35` → `#bd0f58` (Pink 700) - - `actionPrimaryHover` `#cf1750` / `#b81447` → `#d41162` (Pink 600) - - Accent wells rebase on Pink 300; `interactionAccentActive` is **10%** (rail latch well), was 15%/13% -- Neighbors: Pink 700 is the opaque action; Pink 300 is on-chrome. Do not collapse them. Cell focus and `SelectMenu` check inherit Accent. -- Walked: Figma Core `2244:340`; console `1913:61852` / `73608` / `83402` / `93667` -- Why: Pinker brand so it no longer reads as the error red. -- Out of scope: `contentObject`; inventing `qdbPink*` tokens. -- For Emre: Same hexes in light. Flag if light actions need a darker step (old light primary was `#8a0f35`). - -### 2026-09-06 — Danger rotates to pure red -- Lift: palette + binding -- Status: applied locally -- Modes: both (hexes shared; light contrast is a known risk) -- Tokens: `statusDanger` `#ff3333`; `statusDangerStrong` `#db2424` (was `#dc2828`); `statusDangerContrast` `#ff4d4d` (new, type on 15% wash); `statusDangerSubtle` `#ff8080` (new, type on 40% wash); `statusDangerSurface` `#db2424` @ **15%**; `statusDangerSurfaceHover` `#db2424` @ **40%**; `statusDangerMuted` Strong @ 72%; `statusDangerBorder` Strong @ 28% -- Binding: - - `danger` Button is solid Strong + `contentInverse` (Delete conversation confirm, Cancel script). Hover fill is Text `#ff3333`. - - `dangerGhost` rest Surface + Contrast; hover SurfaceHover + Subtle (Reset Provider, Figma `1948:123286`). - - Chat history row delete `dangerGhost` → `danger`. - - Field error border Strong; wash Surface. MCP / login / details banners use Surface, not `statusDanger`+`1f`. -- Neighbors: brand pink and danger red must not share a hex. -- Why: Separate “this is QuestDB” from “this is broken / destructive.” -- Out of scope: light-only darker danger type (Zack said dark first). -- For Emre: Contrast/Subtle are type-on-wash, not fills. Strong is the opaque source. - -### 2026-09-06 — Light brand + danger split from dark -- Lift: palette -- Status: applied locally -- Modes: light only (dark hexes unchanged) -- Tokens: - - `contentAccent` `#bd0f58` — rail icons + tab underlines - - `contentAccentStrong` `#8e0b42` — sits with actions - - `actionPrimary` `#8e0b42`; `actionPrimaryHover` `#bd0f58` - - `interactionAccentActive` `#bd0f58` @ **13%** (rail well); hover rebase @ 8% - - `statusDanger` `#ce1717` — type + icons always - - `statusDangerStrong` `#ce1717` — solid Delete fill - - `statusDangerContrast` `#b81414` — type on `#bd2828` @ 15% - - `statusDangerSubtle` `#8a0f0f` — type on `#db2424` @ 40% hover - - `statusDangerSurface` `#bd2828` @ 15%; `statusDangerSurfaceHover` `#db2424` @ 40% -- Neighbors: `danger` / `dangerGhost` variants unchanged — they read these roles. Light solid delete is Text, not the dark Strong `#db2424`. -- Walked: Figma `1913:61813` / `79570` / `85185` / `93656`; hover `1948:123305` -- Why: One-for-one with the dark pass, with a darker action and a contrast-safe light red. -- Out of scope: retuning dark; leaving brand on rail/tabs/cell focus. -- For Emre: Light Strong equals Text so `danger` buttons stay `#ce1717`. Do not share Strong across modes. - -### 2026-09-06 — MCP pair error banner -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: no new roles. Light fill is banner-only `rgba(189, 40, 56, 0.08)` (Figma, not the 15% wash). Dark fill is transparent. Stroke is `statusDanger` (light) / `statusDangerMuted` (dark). Type + icon `statusDanger`. -- Binding: `PairPopover` danger `StatusRow` is inset in the form — 1px border, 3px left accent, 6px radius, 12px padding. Validation / WS error / major version-mismatch use it. `StatusDetail` inherits Text, not `contentSecondary`. -- Walked: Figma `1913:86918` (light), `1913:85135` (dark) -- Why: Validation is a status banner, not a full-bleed wash. -- Out of scope: consent modal; connecting / minor-mismatch rows. -- For Emre: Do not fold the 8% banner wash into `statusDangerSurface`. - -### 2026-09-06 — Reset Provider is a true ghost, not dangerGhost -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: none. `dangerGhost` stays the washed secondary (Surface + Contrast / SurfaceHover + Subtle). -- Binding: Assistant Settings Reset / Remove Provider uses `ghost` + local danger type. Rest is transparent + `statusDanger`. Hover is `#db2424` @ 30% + Subtle in dark; `#bd2828` @ 10% + Text in light. Icons follow type. -- Walked: Figma `1924:106893` / `1948:123175` (dark), `1919:102447` / `1948:123305` (light) -- Why: The filled wash is for secondary destructive chips. Reset is a quiet text action until hover. -- Out of scope: retuning `dangerGhost`; chat-history solid delete. -- For Emre: Do not fold this hover into `statusDangerSurfaceHover` (40%). Reset is the 10%/30% ghost, not the secondary wash. - -### 2026-09-06 — Notebook drag slot is a primary wash -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: none. `interactionNeutral` @ 25% was the old slot — light `#e8eaee` on stage `#e2e5ea` vanished. -- Binding: `react-grid-placeholder` is `withAlpha(contentPrimary, 0.08)` light / `0.10` dark. Ink on the light stage, porcelain on the dark stage. Opacity lives in the alpha, not a second `opacity`. `interactionHover` is the same idea at 7.5% / 5.5% — too quiet on the stage, and Neutral still matches the light ladder. -- Neighbors: stage `surfaceStage`. Cells `surfaceRaised`. Drag chrome stays `contentAccent`. -- Why: The snap rectangle has to read as a hole on the stage, not a control fill. Neutral is the pressed-chip role. -- Out of scope: the always-on stage dot grid (`interactionHover`); retuning Neutral. -- For Emre: One-off wash. Do not invent `surfaceDragSlot`. - -### 2026-09-06 — Freeze latch shares toolbar control height -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: none -- Binding: Result freeze (`PrimaryToggleButton`) uses `&&` + `TOOLBAR_CONTROL_HEIGHT` (Button `md` 3.4rem) and the same `0 1.2rem` padding as sibling ghost Buttons. Notebook freeze uses `&&` + ActionButton's 2.8rem / `0 0.6rem`. Latch color stays `statusInfo`. -- Neighbors: `PrimaryToggleButton` itself stays 3.5rem for rail / other chrome. Schema toolbar chips stay 3rem. -- Why: Freeze was a segmented-control latch (3.0–3.5rem, width 4rem) sitting next to default `Button` ghosts. No reason for a different chip. -- Out of scope: retuning `PrimaryToggleButton` globally; latch color. -- For Emre: Geometry only. `&&` is required to beat `SegmentedControlButton` `$size="md"` (3rem) and `PrimaryToggleButton` (3.5rem). - -### 2026-09-06 — Result grid header names are Semibold -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: none -- Binding: Column header names are Open Sans Semibold (`600`). `ResultGrid` `HeaderName`, notebook shimmer, and the legacy `.qg-header-name` grid. Width sampling uses the same weight so names don't clip. -- Neighbors: header type stays Regular / secondary. Cell values unchanged. -- Why: Figma `1913:84504` — names are `Open Sans SemiBold`, not Regular. -- Out of scope: type-row italic; cell type color. -- For Emre: `HEADER_NAME_FONT_WEIGHT` in `ResultGrid/dimensions.ts`. - -### 2026-09-06 — Add Markdown hover is the chrome wash -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: none -- Binding: secondary Add Markdown (`AddButton` `$variant="secondary"`) hover `interactionNeutral` → `interactionHover`. Type still `contentPrimary` on hover. Add Cell stays `interactionAccentActive`. -- Neighbors: sits on `surfaceStage`. Light Neutral `#e8eaee` on stage `#e2e5ea` is a lift. Dark Neutral `#32343e` is a pressed chip, several steps up. Ghost / menu / tree / search hover is already `interactionHover` (ink 7.5% / porcelain 5.5%). -- Why: One-step invert against the surface — darker in light, lighter in dark. Neutral is the selected-chip role. -- Out of scope: Add Cell; retuning `interactionHover`; global ghost. -- For Emre: Same wash as other chrome hovers. Do not use Neutral for hover on the stage. - -### 2026-09-06 — Result row count is Open Sans Semibold 15 -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: none -- Binding: Result toolbar count number is `theme.font` / `fontSize.lg` (15px) / `600` / `2.14rem` (21.4px). Dropped `fontMonospace`. "rows" stays Regular `fontSize.sm` (13px) / `1.56rem` / `contentSecondary`. -- Neighbors: header names are Semibold 14. Cell values stay mono. -- Why: Figma `1913:84477` — `350,528` is Open Sans SemiBold 15 / 21.4; `rows` is Regular 13 / 15.6. -- Out of scope: notebook QueryResult "X rows in Yms"; inventing a type token. -- For Emre: Chrome count, not a cell. Keep tabular-nums. - -### 2026-09-06 — Light tooltips share the dropdown overlay -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: none -- Binding: `Tooltip` box + arrow fill `surfaceInset` → `surfaceOverlay` in light (`#fafbfc`). Dark stays `surfaceInset` (`#121317`). Stroke stays `borderDefault`. -- Neighbors: dropdowns / popovers already `surfaceOverlay` via `floatingSurfaceStyles`. Light Inset `#f7f8f9` is the editor/grid well, a gray step below Overlay. Dark Inset sits under Overlay on the ladder, so it already matches. -- Why: Light tooltip was a well next to a near-white menu. Same floating layer as the dropdown. -- Out of scope: retuning `surfaceInset`; dark tooltip; adding the menu shadow to tooltips. -- For Emre: Do not fold Inset into Overlay. Inset stays the recessed well. - -### 2026-09-06 — Light instance hover card matches tooltips -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: none -- Binding: TopBar `CustomTooltipWrapper` fill `surfaceInset` → `surfaceOverlay` in light. Dark stays `surfaceInset`. Stroke stays `borderDefault`. -- Neighbors: shared `Tooltip` already Overlay in light. Same floating-chrome job as Export / EE tooltips. -- Why: Inset is the well; Overlay is the floating layer. Demo data has no instance type so this card is easy to miss. -- Out of scope: Monaco `editorWidget`; chart settings drawer; instance color-picker well. -- For Emre: Same split as Tooltip. Do not retune Inset. - -### 2026-09-06 — Dropdown loom blur 16 → 12 (trial, reverted) -- Lift: binding (geometry, same tokens) -- Status: reverted -- Modes: both — `floatingSurfaceStyles` is shared -- Tokens: none -- Binding: tried third layer blur `1.6rem` → `1.2rem`. Reverted to `0 1.2rem 1.6rem -0.4rem` `shadowMedium`. -- Why: Zack: 12px was a look, not a keep. Light-only quieting is the palette lever (`shadowMedium` 8% vs dark 28%), not shared geometry. -- Out of scope: retuning shadow opacities; dropping the third layer. -- For Emre: Stack unchanged. Trial only. - -### 2026-09-06 — Dropdown shadow C, loom split by mode -- Lift: binding (geometry, same tokens) -- Status: applied locally (trial) -- Modes: both; third layer splits -- Tokens: none -- Binding: `floatingSurfaceStyles` (and Monaco / Quick Vis copies): - - Shared heel: `0 1px 2px 0` `shadowSoft` - - Shared weight: `0 4px 6px -2px` `shadowMedium` - - Light loom: `0 8px 8px -4px` `shadowSubtle` (Figma `1957:130606`) - - Dark loom: `0 12px 16px -4px` `shadowMedium` (kept the old third layer; "126" read as 16) -- Neighbors: SelectMenu / DropdownMenu / Popover / MCP pair / theme + AI model. Modals unchanged. -- Why: Zack: C in both, loom quieter/tighter in light only. Dark still needs the 16px Medium halo. -- Out of scope: retuning shadow opacities; instance hover card; `modalSurfaceStyles`. -- For Emre: Geometry split, not a palette retune. Light loom is Subtle so it does not stack two Mediums. - -### 2026-09-06 — Dark ghost danger type is Text, not Subtle -- Lift: binding -- Status: applied locally -- Modes: dark (light kept) -- Tokens: none. `statusDangerSubtle` stays `#ff8080`. -- Binding: `dangerGhost` hover type `statusDangerSubtle` → `statusDanger` (`#ff3333`) in dark. Reset Provider hover type follows. Light hover stays Subtle on the 40% wash. -- Neighbors: rest `dangerGhost` still Contrast `#ff4d4d` + Surface. Solid `danger` unchanged. -- Why: Figma `1948:123286` — Reset Provider type is `#ff3333`, not the 40%-wash Subtle. -- Out of scope: retuning Subtle; light ghost danger; wash opacities. -- For Emre: Subtle remains type-on-40%. Dark ghost hover uses Text. - -### 2026-09-06 — In-track maximize hover matches Table/Chart -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: none -- Binding: `ViewIconButton` (maximize / reset-zoom inside `NotebookViewToggle`) light hover `interactionNeutralHover` → `interactionHover`. Same wash as `SegmentedControlButton`. Dark stays `surfaceRaised`. -- Neighbors: track `controlTrack` `#e8eaee`. NeutralHover `#e2e5ea` on that track vanished. Outside-cell Maximize / More stay `surfaceBase` on the raised cell. -- Why: Zack: in-track hover must match Table/Chart, not the ghost icons outside the control. -- Out of scope: CellIconButton; retuning NeutralHover. -- For Emre: In-track hover is the ink wash, not a surface step on the chip track. - -### 2026-09-06 — Provider tab hover is one step below selected -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: none -- Binding: unselected `ProviderTab` hover `controlSurfaceHover` → `interactionNeutralHover`. Selected stays `interactionNeutral` (including on hover). -- Neighbors: selected Neutral (`#e8eaee` / `#32343e`). NeutralHover is the next step down (`#e2e5ea` / `#292b35`). Dark `controlSurfaceHover` equals Neutral, so hover matched selected. -- Why: Zack: hover should sit one step below the active well, both modes. -- Out of scope: selected fill; tab underline; StatusChip. -- For Emre: Same family as selected. Do not use `controlSurfaceHover` here. - -### 2026-09-06 — Light provider-tab hover is one more notch -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: none -- Binding: unselected `ProviderTab` hover in light → `surfaceBase` (`#eef0f3`). Dark stays NeutralHover. Selected stays Neutral. Specificity `&&&&` so TabButton hover cannot win. -- Neighbors: Overlay `#fafbfc` < Base `#eef0f3` < Neutral `#e8eaee`. NeutralHover and the 22% ink wash both sit *below* Neutral (darker). Light hover is a smaller recede than selected, matching dark's "between sidebar and selected" step. -- Why: Zack: 22% had impact but the wrong direction — hover must be lighter than active, not darker. -- Out of scope: retuning NeutralHover; inventing a surface. -- For Emre: Light hover is Base, not an ink wash. Do not fold this into NeutralHover (Table/Chart track). - -### 2026-09-06 — Result-grid selection uses info, double hover -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: none. `gridSelection` stays `#252830` / `#d8dce3` (Monaco still reads it). -- Binding: ResultGrid selected row stacks white 11% / ink 15% (exactly 2× `interactionHover`) over `gridRow`. Focused cell stacks `statusInfoSurface` over that fill, ring `statusInfo`. Copy-pulse follows the ring. `gridFocus` unused here. Legacy `.qg-c-active` matches via `html[data-theme]`. -- Neighbors: hover stays 1× `interactionHover` over Inset. Monaco suggest/list hover unchanged. Figma `1943:114709` / `1943:116498`. -- Why: Zack: selection fill is double hover; cell chrome is the info family. Palette retune of `gridSelection` would have moved Monaco — do not. -- Out of scope: retuning `statusInfo` / `statusInfoSurface` / `gridSelection`; `editorSelection`; notebook cell focus. -- For Emre: Grid selection wash is local to ResultGrid, not a token. Cell ring is `statusInfo`, not `gridFocus`. - -### 2026-09-07 — Light metrics calendar: enabled light, disabled grey -- Lift: binding -- Status: applied locally -- Modes: light (dark kept) -- Tokens: none -- Binding: `.react-calendar` `editorCanvas` → Overlay. Disabled tiles Overlay + Muted → Base + `contentDisabled`. Enabled type Secondary → Primary. Selected range stays `actionPrimary`. -- Neighbors: Overlay `#fafbfc` is the popover. Base `#eef0f3` was the whole light calendar well, so enabled days looked greyed and future/`min`/`max` days looked lit. -- Why: Zack: flip enabled vs disabled in light. Enabled light; disabled greyed out. Follow-up: selected range type must stay `contentInverse` in light — the enabled-tile Primary rule was winning. -- Out of scope: dark; selected-range fill; neighboring-month type; popover stroke. -- For Emre: Do not invert Overlay/Canvas globally. This is calendar tiles only. Selected type is Inverse, not Primary. - -### 2026-09-07 — Table-details warning count matches Figma pill -- Lift: binding -- Status: applied locally -- Modes: both (geometry) -- Tokens: none -- Binding: `Badge` `shape="pill"` `sm` — 20px tall, 5px inline pad + 1px stroke (6px to icon), 4px gap, 12px icon/type Regular, 12% wash. Dropped `TabBadge` 18px height. Stroke stays tone @ 32% (not solid). -- Neighbors: error count pill on the same tab uses the same geometry. Chip-shaped badges unchanged. -- Why: Figma `1914:98448` is 36×20 with 6×4 pad, 4px gap, 12px glyph. The 18px override crushed it. Zack: keep the quieter 32% stroke, not the solid Figma border. -- Out of scope: chip shape; StatusChip; retuning `statusWarningSurface`. -- For Emre: Pill is the compact count. Do not restyle chips to this stadium. Stroke is 32% wash, not solid tone. - -### 2026-09-07 — Table and column icons step up to Primary -- Lift: binding -- Status: superseded the same day — glyphs went to Secondary -- Modes: both (dark was the read) -- Tokens: none -- Binding: `TableIcon` / `MaterializedViewIcon` / `TypeIcon` / designated-timestamp `SortDownIcon` `contentMuted` → `contentPrimary`. -- Neighbors: dark Muted `#858995` sits below Secondary `#9da1ad`. There is no content role between Secondary and Primary `#f8f8f2`. Table names were already Primary. -- Why: Zack: icons not visible enough in dark; wanted closer to Primary than Secondary. They were on Muted, quieter than Secondary. -- Out of scope: inventing a mid-content token; type *labels* stay Secondary; rail latches. -- For Emre: Superseded. Glyphs are Secondary. - -### 2026-09-07 — Table and column glyphs are Secondary -- Lift: binding -- Status: superseded the same day — back to Primary -- Modes: both -- Tokens: none -- Binding: table / mat-view / view / column-type / designated-timestamp glyphs `contentPrimary` → `contentSecondary`. -- Neighbors: names stay Primary. Parenthetical type labels were already Secondary — glyphs now match that weight, one step above Muted. -- Why: Zack: try Secondary after Primary. -- Out of scope: type labels; rail latches; inventing a mid-content token. -- For Emre: Superseded. Glyphs are Primary. - -### 2026-09-07 — Table and column glyphs settle on Primary -- Lift: binding -- Status: applied locally -- Modes: both -- Tokens: none -- Binding: table / mat-view / view / column-type / designated-timestamp glyphs `contentSecondary` → `contentPrimary`. -- Neighbors: names already Primary. Type labels stay Secondary. -- Why: Zack: go back to Primary after the Secondary trial. -- Out of scope: type labels; rail latches. -- For Emre: Object-type marks are Primary, same as the name. - ---- - -## Parking lot — not logged as decisions yet - -Use this for hunches until they become an entry. - -- Brand core is QDB Pink, split by mode: dark 300/700 (`#f0428b` / `#bd0f58`), light 700/800 (`#bd0f58` / `#8e0b42`). Still branded: rail, tabs, cell focus, `SelectMenu` check, AI `$tone="accent"`. Danger is the pure-red family (dark bright ramp; light `#ce1717` + contrast steps). `contentObject` unused. Treat leftovers as **binding**. -- `interactionNeutral` / `controlTrack` leftover `#d9dce2` — addressed 2026-09-01 (table/chart chips). -- New surface roles: only if two neighbors with *different jobs* are forced to share a token. Propose the meaning first (`surfaceSomething`), then values in both themes. diff --git a/THEMING.md b/THEMING.md deleted file mode 100644 index 3c1b172dd..000000000 --- a/THEMING.md +++ /dev/null @@ -1,408 +0,0 @@ -# QuestDB Web Console theming - -This document is the contract for color in the Web Console. It explains the -palette, the surface hierarchy, the semantic meaning of every token, and how a -new component should choose colors. - -The implementation lives in [`src/theme/index.ts`](src/theme/index.ts). The -palette is intentionally flat: every color is `theme.color.tokenName`, grouped -by comments in the source. There are no nested color objects, and the only -first-party CSS custom properties are the `--qdb-*` properties generated from -this palette so legacy SCSS can read it. A small set of deliberately -theme-invariant roles is defined once and composed independently into both -palettes; light mode never inherits the dark palette. - -## What changed - -Before this redesign, `theme.color` exposed dark-only tokens. Many encoded a -hue, a component, or one specific opacity, while other UI colors still lived -outside the palette. That made global tuning difficult and encouraged new -near-duplicates. - -The palette now has two kinds of roles: - -- Shared UI roles for surfaces, content, borders, interactions, status, - shadows, glass, and brand treatments. -- Isolated roles required by renderers or deliberately self-contained - experiences: the result grid, Monaco, data visualization, authentication, - onboarding, browser window controls, instance presets, and the color picker. - -Most product UI should need only the shared roles. The specialized roles exist -because collapsing syntax colors, chart-series identity, or native window -controls into generic UI colors would make those systems less understandable, -not more maintainable. - -## Brand sources and semantic mapping - -The palette is informed by two QuestDB Figma sources: - -- [QuestDB Design System – Core v1.1](https://www.figma.com/design/yka873CDRnp51hp8HBb8Nv/QuestDB-Design-System-%E2%80%93-Core-v1.1?node-id=13-2916&p=f&m=dev) - supplies the dark neutral anchors and functional accents: midnight `#141725`, - charade `#21222c`, rock `#262833`, gray `#32343e`, porcelain `#f8f8f2`, sky - `#81d3f9`, green `#66bb6a`, and yellow `#ffd54f`. -- [QuestDB Agents Landing Page](https://www.figma.com/design/HPVrUIVW8TGyowgZZ5M13K/QuestDB-Agents-Landing-Page?node-id=1-4264&p=f&m=dev) - supplies the systematic QuestDB accent ramp used for actions and semantic - accent content. Its 341-degree ramp is the source of truth because its - rendered fills, hex labels, and HSL labels agree. A neighboring 345-degree - draft contains mismatches between the rendered fills and the visible hex - labels, so it is not used in product code. - -Source palette names never become application tokens. `QDB Pink 700`, for -example, describes a swatch, not why a component needs it. The same swatch may -therefore map to `contentAccent` in light mode and `actionPrimaryHover` in light -mode, while those roles receive different lightness values in dark mode. Code -always asks for a UI meaning such as `surfaceRaised`, `contentAccent`, or -`statusInfo`. - -The dark theme uses the source neutrals selectively rather than flooding every -large work surface with them. Midnight is reserved for the outer application -field; notebook stages, editors, grids, panels, and overlays use a lower-chroma -charcoal ladder derived around the source neutrals. The light theme keeps the -same semantic hierarchy but uses neutral gray surfaces designed for an -application rather than attempting to invert the website's dark-only -backgrounds. Brand hue families are preserved across modes; lightness and, when -needed, saturation change to provide contrast against each theme's surfaces. - -## The model: contrast between neighbors - -There is no universal rule that every higher surface must be lighter. Lightness -is read locally: a user understands a panel from the surfaces immediately next -to it. The console therefore uses a small set of spatial roles and assigns them -consistently in both themes. - -1. `surfaceCanvas` is the application field. -2. `surfaceStage` recedes behind the user's primary work. -3. `surfaceBase` unifies fixed chrome and ordinary panels. -4. `surfaceInset` separates content embedded inside another surface. -5. `surfaceRaised` groups cards, cells, and interactive rows. -6. `surfaceOverlay` identifies floating or overlay-like surfaces. It covers - menus, popovers, dialogs, Monaco widgets, the CTA banner, and legacy editor - chrome that needs the same elevation. - -Dark mode generally expresses inset depth by becoming darker and raised depth by -becoming lighter. Light mode intentionally uses a gray application field, a -darker work stage, and brighter embedded content. This avoids both an all-white -document look and the alternating “zebra” bands that appear when every container -invents its own gray. - -The practical rules are: - -- Adjacent regions with different responsibilities should not use the same - surface token. -- Siblings with the same responsibility should use the same surface token. -- Use one primary separation cue. Prefer a surface step; add a subtle border for - precise edges; add a shadow mainly when an element floats over content. -- Do not combine a large lightness jump, a strong border, and a heavy shadow - unless the element is a blocking overlay. -- Hover must differ from rest, and active must differ from hover. Never use - opacity on the whole control because it also weakens its text and icon. -- Focus is structural, not decorative. Use `borderAccentStrong` or the shared - focus treatment; do not remove it because a mouse hover already exists. - -As a review heuristic, a meaningful boundary should remain visible at normal -zoom without becoming the first thing the eye sees. Text follows the usual -contrast targets: 4.5:1 for ordinary text and 3:1 for large text and meaningful -UI graphics. Not every panel boundary needs 3:1; boundaries that are necessary -to understand or operate the UI do. - -## Core palette - -Values are shown as dark / light. “Same” means the dark value is deliberately -shared by light mode. - -### Surfaces - -| Token | Dark / light | Purpose and allowed use | -| ---------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `surfaceCanvas` | `#141725` / `#d9dce2` | Root application field and empty console canvas. This is the primary large-area use of Core midnight. | -| `surfaceStage` | `#0c0d11` / `#c9cdd4` | Neutral recessed notebook/metrics work stage behind cells and editors. | -| `surfaceBase` | `#17181d` / `#e3e5e9` | Top, side, and bottom chrome; panels and drawers that visually belong to that shell. | -| `surfaceInset` | `#121317` / `#eceef1` | Embedded wells: chart bodies, compact code regions, keycaps, and nested tool regions. | -| `surfaceRaised` | `#1d1e24` / `#e8eaee` | Cells, cards, rows, and controls raised from a parent surface. | -| `surfaceInput` | `#21222c` / `#f2f3f5` | Editable fields, textareas, and search inputs. | -| `surfaceOverlay` | `#1d1e24` / `#f2f3f5` | Dropdowns, context menus, dialogs, Monaco widgets, the CTA banner, and overlay-like legacy editor chrome. | -| `surfaceValue` | `#202126` / `#fbfcfd` | Read-only values in table details. It exists so label/value rows remain consistent without changing their layout. | -| `surfaceScrim` | `rgba(7, 7, 9, .72)` / `rgba(27, 31, 39, .38)` | Translucent occlusion for dialog backdrops, recessed placeholders, overlay arrows, and frozen-grid edge fades. | -| `surfaceTabRail` | `rgba(18, 19, 23, .82)` / `rgba(218, 221, 227, .94)` | Translucent substrate beneath the liquid-glass tab indicator. | -| `transparent` | `transparent` / same | Explicit absence of paint. This is a behavior token, not a palette color. | - -### Content - -| Token | Dark / light | Purpose and allowed use | -| --------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `contentPrimary` | `#f8f8f2` / `#1c2029` | Primary body text, titles, and default icons. Dark mode uses the brand porcelain. | -| `contentSecondary` | `#9da1ad` / `#505968` | Supporting text and neutral toolbar icons. This is the requested shared gray icon color in dark mode. | -| `contentMuted` | `#858995` / `#565f6e` | Metadata and low-emphasis labels that remain readable on the raised Core surface. | -| `contentDisabled` | `#747985` / `#596271` | Disabled text/icons only; never use it merely to make something “quiet.” | -| `contentInverse` | `#f8f8f2` / same | Porcelain content on saturated fills and other surfaces that need fixed light content. | -| `contentOnWarning` | `#000000` / same | Text/icons on bright yellow warning fills. | -| `neutralInk` | `#000000` / same | Opaque mask and picker anchor used on arbitrary fills rather than themed surfaces. | -| `contentAccent` | `#c94f74` / `#b81447` | Brand-accent icons, short labels, schema/table emphasis, focus, and selected accents. Do not treat it as default body text. | -| `contentAccentStrong` | `#cf1750` / `#8a0f35` | Saturated accent for selection fills, prominent identity, and cases that pair with porcelain content. | -| `contentObject` | `#81d3f9` / `#b81447` | Database object identity such as result-grid column names. Sky is retained in dark mode; brand crimson is used in light mode. | -| `contentSearchMatch` | `rgb(163, 127, 96)` / amber `.32` | Search-match emphasis inside text; its light value matches Monaco's search highlight for consistent recognition. | - -### Borders - -| Token | Dark / light | Purpose and allowed use | -| -------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------- | -| `borderSubtle` | `rgba(255,255,255,.04)` / `rgba(28,32,41,.07)` | Quiet dividers between already distinct surfaces. | -| `borderDefault` | `rgba(255,255,255,.13)` / `rgba(28,32,41,.15)` | Standard component, panel, input, and control boundary. | -| `borderStrong` | `#3d414d` / `#828b99` | Resizers, strongly bounded segmented controls, and edges that must remain findable. | -| `borderAccent` | brand accent at `.42` / brand accent at `.42` | Selected/accent boundary. | -| `borderAccentStrong` | brand accent at `.52` / brand accent at `.56` | Keyboard focus and high-priority accented boundary. | - -### Interaction and controls - -| Token | Dark / light | Purpose and allowed use | -| ------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------- | -| `interactionNeutral` | `#32343e` / `#d9dce2` | Neutral selected/pressed fill and legacy control fill. Dark mode uses the Core gray anchor. | -| `interactionNeutralHover` | `#292b35` / `#d2d6dd` | Hover for neutral rows and controls when a solid fill is needed. | -| `interactionHover` | `rgba(255,255,255,.055)` / `rgba(28,32,41,.075)` | Minimal hover overlay for tabs, icon buttons, and transparent actions. | -| `interactionAccentHover` | brand accent at `.08` / brand accent at `.07` | Quiet accent hover, especially schema/search rows. | -| `interactionAccentActive` | brand accent at `.15` / brand accent at `.13` | Selected accent row or active accent control. | -| `interactionGuide` | `#6272a4` / `#56657f` | Drag guides, resize handles, text selection helpers, and Monaco's auxiliary/inactive markers. | -| `scrollbarThumb` | white at `.13` / `#62656b` | Native vertical and horizontal scrollbar handles; neutral and distinct from editor selection. | -| `controlSurface` | `#262833` / `#f6f7f8` | Default solid secondary button, segmented-control item, and keycap face. | -| `controlSurfaceHover` | `#32343e` / `#e7e9ed` | Hover fill for `controlSurface`; dark mode rises to the Core gray anchor. | -| `controlTrack` | `#262833` / `#d9dce2` | Switch track and segmented-control substrate; matches secondary buttons in dark mode. | -| `controlKnob` | `#f8f8f2` / `#505968` | Resting switch knob on `controlTrack`; checked knobs use `contentInverse` on the accent fill. | - -### Actions and status - -| Token(s) | Dark / light | Purpose and allowed use | -| ----------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| `actionPrimary`, `actionPrimaryHover` | `#b81447`, `#cf1750` / `#8a0f35`, `#b81447` | Primary-button rest and hover fills. Both states retain porcelain-text contrast. | -| `statusDanger`, `statusDangerStrong` | `#ff6b73`, `#fa4d56` / `#bd2838`, `#a81f2e` | Error content and destructive emphasis; strong is for higher-priority glyphs/actions. | -| `statusDangerMuted` | `rgba(220,40,40,.72)` / `rgba(189,40,56,.72)` | Muted destructive decoration and legacy login error framing. | -| `statusDangerSurface`, `statusDangerSurfaceHover`, `statusDangerBorder` | red at `.30`, `.40`, `.24` / red at `.12`, `.18`, `.28` | Destructive button/alert fill, its hover, and its border. | -| `statusSuccess`, `statusSuccessStrong` | `#66bb6a`, `#188a5d` / `#067047`, `#05603e` | Success text/icons and stronger green actions. | -| `statusSuccessSurface`, `statusSuccessBorder` | green at `.12`, `.24` / green at `.10`, `.28` | Success banner/pill fill and boundary. | -| `statusWarning`, `statusWarningSurface`, `statusWarningSurfaceHover` | `#ffd54f`, yellow at `.10`, `.16` / `#8a570f`, amber at `.14`, `.20` | Warning content, low-emphasis warning fill, and its interactive hover state. | -| `statusAttention` | `#e3ce78` / `#745c00` | Pending or attention-needed state that is neither an error nor a warning action. | -| `statusInfo`, `statusInfoSurface` | `#81d3f9`, sky at `.10` / `#176f87`, teal at `.10` | Informational content and banners. | -| `statusFeature` | `#a99de8` / `#6553aa` | Feature/AI distinction when brand pink or status colors would imply the wrong meaning. | -| `statusAssistant`, `statusAssistantStrong` | `#d14671`, `#892c6c` in both themes | Fixed assistant progress gradient and completion checks across theme changes. | - -### Shadows and glass - -| Token(s) | Dark / light | Purpose and allowed use | -| --------------- | -------------------------- | --------------------------------------------------------------------------------- | -| `shadowSubtle` | black `.08` / ink `.06` | Small local lift, including quiet toolbar separation. | -| `shadowSoft` | black `.16` / ink `.11` | Cards and hover lift. | -| `shadowMedium` | black `.28` / ink `.18` | Dropdowns and small popovers. | -| `shadowStrong` | black `.42` / ink `.27` | Large popovers and dialogs. | -| `shadowOverlay` | black `.58` / ink `.38` | Strongest depth cue for dialogs, banners, legacy overlays, and pinned-grid edges. | -| `glassSurface` | white `.075` / white `.58` | Moving liquid-glass selection lens. | -| `glassBorder` | white `.14` / ink `.14` | Main glass lens edge. | -| `glassEdge` | white `.22` / ink `.22` | Refractive/highlight edge; use sparingly inside the glass implementation. | - -### Brand gradients - -The gradients are reserved for QuestDB identity and AI affordances. Ordinary -buttons and surfaces use solid semantic roles. The AI sparkle has an independent -semantic gradient so general QuestDB identity tuning cannot recolor it. The -QuestDB logo mark retains its supplied SVG gradients, while its wordmark inherits -the surrounding semantic content color so it remains readable in both themes. - -| Token(s) | Dark / light | Purpose | -| ---------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------- | -| `brandGradientStart`, `brandGradientEnd` | `#e51a59` → `#8a0f35` / `#b81447` → `#5c0a24` | QuestDB identity gradient for theme-aware product affordances. | -| `aiGradientStart`, `aiGradientEnd` | `#d14671` → `#892c6c` / `#a92352` → `#76184c` | Established AI sparkle gradient, isolated from logo palette changes. | - -## Specialized palettes - -These roles are still semantic, but they belong to a renderer or an isolated -experience. General UI components must not consume them. - -### Result grid - -The grid needs a dedicated dense-data ladder. In dark mode the empty grid field -is darkest, rows are darker, and headers are dark. In light mode the field is -lightest, rows are lighter, and headers are the strongest gray. That preserves -structure without card borders around every cell. - -| Token | Dark / light | Purpose | -| --------------- | --------------------- | ---------------------------------------------------------- | -| `surfaceInset` | `#121317` / `#eceef1` | Empty grid field and viewport. | -| `gridRow` | `#17181d` / `#e9ebef` | Standard data row. | -| `gridHeader` | `#202126` / `#dce0e6` | Column header background. | -| `contentObject` | `#81d3f9` / `#b81447` | Column-name identity; sky in dark, brand crimson in light. | -| `gridSelection` | `#2b1d25` / `#e0afbf` | Selected cell/row accent. | -| `gridFocus` | `#b81447` / `#8a0f35` | Focus boundary and outward copy pulse for the active cell. | - -### Monaco editor - -Monaco is themed through its API, not through application CSS. Its 18 -editor-specific roles remain isolated; ordinary surface, content, interaction, -and status roles are reused where they carry the same meaning. - -| Token(s) | Dark / light | Purpose | -| --------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| `editorCanvas`, `surfaceOverlay`, `editorBorder` | `#17181d`, `#1d1e24`, `#121317` / `#d9dce2`, `#f2f3f5`, `#c9cdd4` | Editor field, widgets/menus, and editor-specific edge. | -| `contentPrimary` | `#f8f8f2` / `#1c2029` | Default code text. | -| `interactionHover`, `interactionNeutral`, `controlSurfaceHover` | white `.055`, `#32343e`, `#32343e` / ink `.075`, `#d9dce2`, `#e7e9ed` | Menu selection, separators, and list-filter states. | -| `editorSelection`, `editorSelectionAccent` | `#44475a`, `#44475a` / `#aec8ec`, `#7a99c4` | Monaco and native form-control selection, plus its stronger selection boundary. | -| `editorActiveLine`, `editorActiveLineBorder` | white `.03`, transparent / ink `.035`, transparent | Active-line wash and border. The transparent border prevents layout/color artifacts. | -| `editorSuggestionMatchActive` | `#ff9abb` / `#8a0f35` | Active autocomplete match. | -| `editorRun`, `statusDanger` | `#ffffff`, `#ff6b73` / `#067047`, `#bd2838` | Monaco run glyph and error decoration. The run glyph intentionally changes contrast by theme. | -| `contentMuted` | `#858995` / `#565f6e` | Comments. | -| `editorSyntaxString` | `#f1fa8c` / `#695f00` | Strings. | -| `editorSyntaxNumber` | `#50fa7b` / `#056c45` | Numbers. | -| `editorSyntaxKeyword` | `#ff79c6` / `#b81447` | SQL keywords. | -| `editorSyntaxType` | `#8be9fd` / `#0a6a80` | Types and type-like identifiers. | -| `editorSyntaxConstant` | `#bd93f9` / `#6553aa` | Constants. | -| `editorSyntaxVariable` | `#ffb86c` / `#8e500a` | Variables. | -| `editorErrorHighlight`, `editorSuccessHighlight` | red/green `.15` / red/green `.13` | Full-line execution feedback. | -| `editorSearchHighlight`, `editorAiHighlight` | orange/yellow `.50` / amber `.32`/`.28` | Search and AI-generated-range highlights. | - -### Data visualization - -Series keep the same conceptual hue identity across themes; the light theme uses -darker lightness values to maintain contrast against a light plot while the dark -theme uses lighter values against a dark plot. The slot is the data -identity—components must not pick a series by color name. - -| Token | Dark / light | Purpose | -| ------------------------------ | ------------------------------------------- | ----------------------------------------------------------------------- | -| `dataGrid` | gray `.11` / ink `.16` | Chart axes and grid lines. | -| `dataArea` | white `.012` / ink `.018` | Quiet plot-area separation. | -| `dataSeries1` | `#ff6b6b` / `#d51515` | Metric widget series slot 1, red. `dataSeries*` are user-pickable metric colors; auto-assigned chart series use `chartSeries*`. | -| `dataSeries2` | `#4ecdc4` / `#0c7966` | Series slot 2, teal. | -| `dataSeries3` | `#ffd93d` / `#80690d` | Series slot 3, yellow/olive. | -| `dataSeries4` | `#95d86e` / `#357b0c` | Series slot 4, green. | -| `dataSeries5` | `#ff8f40` / `#ac5111` | Series slot 5, orange. | -| `dataSeries6` | `#bd93f9` / `#6415d5` | Series slot 6, violet. | -| `dataSeries7` | `#50fa7b` / `#0c7b37` | Series slot 7, bright green. | -| `dataSeries8` | `#ff79c6` / `#cc147e` | Series slot 8, magenta. | -| `dataSeries9` | `#8be9fd` / `#1072a5` | Series slot 9, cyan/blue. | -| `dataSeries10` | `#f1fa8c` / `#61740b` | Series slot 10, yellow/olive. | -| `chartSeries1..8` | `#8be9fd #d14671 #ffb86c #bd93f9 #f1fa8c #ff79c6 #50fa7b #ff5555` / `#1590aa #e42560 #c16f18 #7425e4 #728d11 #e425ac #139842 #e42525` | Auto-assigned series order for notebook/result charts and quick-vis. Calm brand hues lead; assignment is positional, not user-picked. | -| `dataPositive`, `dataNegative` | `#2ca875`, `#d94d58` / `#05603e`, `#a81f2e` | Directional financial/operational data, not generic form success/error. | - -### Authentication - -Authentication is a visually isolated first impression and keeps a deliberate -dark-crimson identity. These roles belong to login/version UI. `authBackdrop` is -also used for the dark tool and code blocks inside AI chat messages. - -| Token | Dark / light | Purpose | -| ------------------------------- | ---------------------------------------------- | --------------------------------------------------------- | -| `authBackdrop` | `#1d070e` / `#c7cbd2` | Login page field and AI chat tool/code-block background. | -| `authAccent`, `authAccentMuted` | `#9089fc`, violet `.64` / `#7d2948`, ink `.46` | Login illustration/action accent and its muted companion. | -| `surfaceCanvas`, `authBorder` | `#141725`, `#353946` / `#d9dce2`, `#a8b0bd` | Login form card and prominent edge. | -| `authVersionContent` | `#f5f3f0` / `#e7e3e5` | Version badge readable content. | - -### Onboarding - -The notebook and MCP onboarding experience follows the active luminance mode. -Dark mode retains the original terminal presentation; light mode reuses values -from the shared light surface, content, feature, and information roles. Modal -borders, subtle fills, and shadows use the general semantic tokens directly. - -| Token | Dark / light | Purpose | -| ------------------- | --------------------------------------- | -------------------------------------- | -| `onboardingSurface` | `#050505` / `surfaceBase` equivalent | Experience backdrop. | -| `onboardingDivider` | white `.04` / `borderSubtle` equivalent | Quiet internal rule. | -| `onboardingClose` | `#858585` / `contentMuted` equivalent | Close/secondary content. | -| `onboardingCommand` | `#111111` / `surfaceValue` equivalent | Command and transcript frame surfaces. | -| `onboardingInput` | `#202020` / `surfaceInset` equivalent | Simulated input. | -| `onboardingPrompt` | `#93adff` / `statusFeature` equivalent | Prompt identity. | -| `onboardingAccent` | `#29c6be` / `statusInfo` equivalent | Agent/progress accent. | - -### Fixed-purpose colors - -These values encode external or user-selected identity, so replacing them with -generic status roles would change meaning. - -| Token(s) | Value | Purpose | -| -------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| `instancePreset1`, `instancePreset2`, `instancePreset3` | `rgb(199,7,45)`, `rgb(0,170,59)`, `rgb(0,122,255)` | Stable user-selectable instance identities. | -| `windowControlClose`, `windowControlMinimize`, `windowControlExpand` | `#ff5f57`, `#febc2e`, `#28c840` | Familiar macOS-style window controls in illustrative UI. | -| `pickerHue0` … `pickerHue5` | red, yellow, green, cyan, blue, magenta endpoints | Mechanical hue stops for the color picker. Indexed names are intentional because the token is a position, not a UI meaning. | - -## Choosing a surface - -| Relationship | Recommended combination | -| ----------------------------------- | -------------------------------------------------------------------- | -| Application shell → work area | `surfaceCanvas` → `surfaceStage` | -| Chrome/sidebar → panel content | `surfaceBase` → `surfaceRaised` or `surfaceInset` | -| Stage → notebook/metric card | `surfaceStage` → `surfaceRaised` | -| Card header → embedded editor/chart | `surfaceRaised` → `surfaceInset` or the renderer-specific canvas | -| Page → input | parent surface → `surfaceInput` + `borderDefault` | -| Page → menu/dropdown | parent surface → `surfaceOverlay` + `borderDefault` + `shadowMedium` | -| Dialog → read-only value | dialog surface → `surfaceValue` | -| Result-grid viewport → row → header | `surfaceInset` → `gridRow` → `gridHeader` | - -Do not choose a token by comparing its hex value to a mockup. Choose the role -from this table, then tune the role in both themes if the relationship is wrong -everywhere it appears. - -## Runtime architecture - -- Styled components read `theme.color` directly. The `color()` helper resolves - the same typed object. -- Non-React renderers use `getThemeColor()` from `src/theme/runtime.ts`; the - active object is synchronized by `ThemeModeProvider`. -- Legacy SCSS uses `theme-color(name)`. Sass emits `var(--qdb-color-name)`, and - `ThemeCssVariables` declares every property from this same palette. This lets - the compiled `main.scss` bundle follow a runtime theme change through one - generated declaration block. -- SVG templates use semantic placeholders that are resolved before rendering. -- Monaco receives a complete theme through its public theming API. Any custom - properties inside Monaco itself are third-party implementation details, not - QuestDB palette inputs. -- `withAlpha()` derives one-off translucent renderer colors from a semantic - role. It handles hex and RGB inputs and avoids a separate token for every - renderer-specific opacity. - -### User preference and System mode - -The saved preference and the rendered theme are deliberately separate: - -- `ThemePreference` is `system`, `light`, or `dark` and is stored under - `appearance.themePreference`. -- `ThemeMode` is always the resolved `light` or `dark` value consumed by styled - components, Monaco, charts, legacy styles, and runtime renderers. -- The provider tracks the operating-system mode independently so switching back - to System always resolves to the current device preference. -- `system` resolves through `prefers-color-scheme`. A media-query change updates - the rendered theme immediately, so operating-system automatic schedules are - followed without reloading the console. -- Explicit `light` and `dark` preferences ignore operating-system changes. -- Preference changes synchronize across same-origin tabs through the browser - `storage` event. - -The root HTML entry point resolves the saved preference before the application -bundle executes and sets `data-theme` plus the native `color-scheme`. The React -provider uses the same resolution rules, so the first React render does not use -the opposite theme. - -## Contribution rules - -1. Never add a literal UI color outside `src/theme/index.ts`. Immutable supplied - brand artwork such as `QuestDBLogo` is the sole exception and must not be - recolored through theme roles. -2. Never add a first-party CSS custom property for theming. -3. Use a semantic role, not a hue name and not a component name. -4. Keep every runtime token directly under `theme.color`; organize source groups - with comments. A genuinely theme-invariant role belongs in `invariantColors`, - which is spread independently into both flat palettes. -5. Add a token only when an existing role would communicate the wrong meaning or - when a renderer needs stable independent control. -6. Define the dark and light value together and inspect their neighboring - surfaces in both modes. -7. Preserve state ordering: rest, hover, active/selected, focus, disabled. -8. Prefer `withAlpha()` for a renderer-specific wash. Promote it to a token only - when the same opacity has a shared semantic meaning across components. -9. Run `yarn lint:colors` and `yarn typecheck` after changes. - -Before approving a new color token, answer all four questions: - -- What meaning does it carry? -- Which existing token is closest, and why is reusing it incorrect? -- Which surfaces or components may consume it? -- What is its corresponding dark/light behavior? - -If those answers are not clear, the color is probably a local styling tweak, not -a design-system token. diff --git a/src/components/Key/index.tsx b/src/components/Key/index.tsx index 277ad7aa3..6c09aff9e 100644 --- a/src/components/Key/index.tsx +++ b/src/components/Key/index.tsx @@ -1,17 +1,27 @@ import React from "react" -import styled from "styled-components" +import styled, { css } from "styled-components" import { ArrowBendDownLeftIcon } from "@phosphor-icons/react" import { Box } from "../Box" import type { ThemeShape } from "../../types" import { shortcutKeycapStyles } from "./styles" type ColorFunction = (props?: { theme: ThemeShape }) => string | undefined +type KeySize = "sm" | "md" + +const ENTER_ICON_SIZES: Record = { sm: 11, md: 12 } + +const compactKeycapStyles = css` + height: 2rem; + min-width: 2.2rem; + padding: 0.15rem 0.5rem; +` const StyledKey = styled(Box).attrs({ alignItems: "center", justifyContent: "center", -})<{ $color?: string | ColorFunction }>` +})<{ $color?: string | ColorFunction; $size: KeySize }>` ${shortcutKeycapStyles} + ${({ $size }) => $size === "sm" && compactKeycapStyles} color: ${({ $color, theme }) => { if (typeof $color === "function") { @@ -26,15 +36,20 @@ const StyledKey = styled(Box).attrs({ type Props = { keyString: string color?: string | ColorFunction + size?: KeySize } -export const Key = ({ keyString, color: keyColor }: Props) => { +export const Key = ({ keyString, color: keyColor, size = "md" }: Props) => { const isEnter = keyString.toLowerCase() === "enter" return ( - + {isEnter ? ( - + ) : ( keyString )} diff --git a/src/components/TopBar/InstanceSettingsPopper.tsx b/src/components/TopBar/InstanceSettingsPopper.tsx index fec889ec0..b91df9b33 100644 --- a/src/components/TopBar/InstanceSettingsPopper.tsx +++ b/src/components/TopBar/InstanceSettingsPopper.tsx @@ -69,6 +69,25 @@ const ColorOption = styled(ButtonBase)<{ } ` +const DefaultColorOption = styled(ColorOption).attrs({ + $colorValue: "default", +})` + position: relative; + + &::before { + content: ""; + position: absolute; + top: 50%; + left: 0%; + width: 100%; + height: 1px; + border-radius: 1px; + background: ${({ theme }) => theme.color.borderAccentStrong}; + box-shadow: 0 0 0 0.5px ${({ theme }) => theme.color.contentPrimary}; + transform: rotate(-45deg); + } +` + const ColorWheelOption = styled(ButtonBase)<{ $selected: boolean }>` &&:disabled { opacity: 0.5; @@ -430,9 +449,8 @@ export const InstanceSettingsPopper = ({ Color - handleColorSelect("")} data-hook="topbar-instance-color-option-default" diff --git a/src/components/TopBar/toolbar.tsx b/src/components/TopBar/toolbar.tsx index 66995aaea..b8a9450ec 100644 --- a/src/components/TopBar/toolbar.tsx +++ b/src/components/TopBar/toolbar.tsx @@ -236,6 +236,8 @@ const EnterpriseBadge = styled(StatusBadge).attrs({ size: "sm", })` height: 1.8rem; + color: ${({ theme }) => theme.color.contentPrimary}; + font-weight: 600; &:not(:last-child) { margin-right: 0.25rem; diff --git a/src/components/UnreadItemsIcon/index.tsx b/src/components/UnreadItemsIcon/index.tsx index fe3fb592e..684c5003a 100644 --- a/src/components/UnreadItemsIcon/index.tsx +++ b/src/components/UnreadItemsIcon/index.tsx @@ -9,10 +9,10 @@ const Tick = styled.span` position: absolute; right: 0; top: 0; - width: 1.3rem; - height: 1.3rem; + width: 1rem; + height: 1rem; border-radius: 50%; - background-color: ${({ theme }) => theme.color.statusDanger}; + background-color: ${({ theme }) => theme.color.statusSuccess}; ` const Count = styled.span` diff --git a/src/modules/OAuth2/views/loginBackground.tsx b/src/modules/OAuth2/views/loginBackground.tsx index dfb62d297..b865f9a7d 100644 --- a/src/modules/OAuth2/views/loginBackground.tsx +++ b/src/modules/OAuth2/views/loginBackground.tsx @@ -19,7 +19,7 @@ const SEGMENTS: Segment[] = [ " \n01101 101011001010111001101110100011000010110110101110000001011000010000001100010 01101001 01100100 010100101000101 010011010101000010 0000011101 0000001 1101010000001 110100011010010110110101100101011100110111010001100 0101001010001010100110 001000101010000110101010000100000011101 00011010010110 1101011001 010111001101110100011000010110110101110000001011000010000001100010 01101001 01100100 010100101000101010011000100010101000011010101000010 ", { sql: "FROM trades" }, "\n0000011101 0001100010 01101001 01100100 01010010100010101001100010001010100001 10101010000100000011101 00011010010110110101100101011100110111010001100 0010110110101110000001011000010000001100010 01101001 01100100 000110100101101101011001 01011 ", - { sql: "WHERE symbol = 'MSFT' AND timestamp IN today()" }, + { sql: "WHERE symbol = 'MSFT' AND timestamp IN '$today'" }, " 0000 1011000010000001100010 01101001 011001000101001010001010100110001000101010000110101010000100000011101 01 SAMPLE BY 15m 0110 001000101010000110101010000100000011101 000110100101101101011001 010111001101110100011000010110110101110000001011000010000001100010 01101001 01100100 011011010111000000 1011000010000001100010 01101001 01100100 01010010100010101001100010001010100001 10101010000100000011101 00011010010110110101100101011100110111010001100 0101001010001010100110 volume,001000101010000110101010000100000011101 000110100101101101011001 010111001101110100011000010110110101110000001011000010000001100010 01101001 01100100 0101001010001010100110001000101010000110101010000100000011101 0010110110101110000001011000010000001100010 01101001 01100100 0101001010001010100110 01000101010000110101SELECT timestamp00000011101 000110100101101101011001 010111001101110100011000010110110101110000001011000010000001100010 01101001 01100100 0101001010001010100110001000101010000110101010000100000011101 0101001010001010100110 001000101010000110101010000100000011101 000110100101101101011001 15m0101001010001010100110 001000101010000110101010000100000011101 000110100101101101011001 010010000001100010 ", { sql: "SELECT timestamp time, symbol, price as priceAAPL," }, " 01101001 ", diff --git a/src/scenes/Editor/ButtonBar/index.tsx b/src/scenes/Editor/ButtonBar/index.tsx index c65241b34..f39566569 100644 --- a/src/scenes/Editor/ButtonBar/index.tsx +++ b/src/scenes/Editor/ButtonBar/index.tsx @@ -64,22 +64,36 @@ const ButtonGroup = styled.div` } ` -const SuccessButton = styled(Button)` +const RunButton = styled(Button)` margin-left: auto; - font-size: 1.5rem; + + &&, + &&:hover:not(:disabled):not([aria-disabled="true"]), + &&:active:not(:disabled):not([aria-disabled="true"]) { + background: ${({ theme }) => theme.color.statusSuccess}; + border-color: ${({ theme }) => theme.color.statusSuccess}; + color: ${({ theme }) => theme.color.editorCanvas}; + } + + &&:hover:not(:disabled):not([aria-disabled="true"]) { + filter: brightness(1.3); + } + + &&:active:not(:disabled):not([aria-disabled="true"]) { + filter: brightness(0.9); + } ` const StopButton = styled(Button)` margin-left: auto; - font-size: 1.5rem; ` -const MainRunButton = styled(SuccessButton)` +const MainRunButton = styled(RunButton)` border-right: 0; overflow: hidden; ` -const DropdownButton = styled(SuccessButton)<{ $open: boolean }>` +const DropdownButton = styled(RunButton)<{ $open: boolean }>` padding: 0 0.5rem; min-width: auto; svg { @@ -90,7 +104,6 @@ const DropdownButton = styled(SuccessButton)<{ $open: boolean }>` const CopyLinkMenuButton = styled(Button)` justify-content: space-between; border-radius: 0; - font-size: 1.5rem; ` const DropdownMenu = styled.div` @@ -106,11 +119,10 @@ const DropdownMenu = styled.div` && > button { justify-content: space-between; width: 100%; - min-height: 4rem; - padding: 0.7rem 1.2rem; + min-height: 3.6rem; + padding: 0.6rem 1.2rem; border: 0; border-radius: 0; - font-size: 1.5rem; background: transparent; } @@ -246,7 +258,7 @@ const ButtonBar = ({ variant="danger" data-hook="button-cancel-script" onClick={handleClickScriptButton} - prefixIcon={} + prefixIcon={} {...menuProps} > Cancel @@ -268,9 +280,13 @@ const ButtonBar = ({ > Run all queries - - - + + + ) @@ -284,7 +300,7 @@ const ButtonBar = ({ variant="danger" data-hook="button-cancel-query" onClick={handleClickQueryButton} - prefixIcon={} + prefixIcon={} > Cancel @@ -319,8 +335,16 @@ const ButtonBar = ({ > {getQueryButtonText()} - - + + - + } > @@ -354,9 +378,21 @@ const ButtonBar = ({ > Copy link to all queries - - - + + + diff --git a/src/theme/THEMING.md b/src/theme/THEMING.md new file mode 100644 index 000000000..c4c3e7db4 --- /dev/null +++ b/src/theme/THEMING.md @@ -0,0 +1,558 @@ +# QuestDB Web Console theming + +This document is the contract for color in the Web Console. It explains the +palette, the surface hierarchy, the semantic meaning of every token, how +components bind to those tokens, and how a new component should choose colors. + +The implementation lives in [`index.ts`](./index.ts). The palette is +intentionally flat: every color is `theme.color.tokenName`, grouped by comments +in the source. There are no nested color objects, and the only first-party CSS +custom properties are the `--qdb-*` properties generated from this palette so +legacy SCSS can read it. A small set of deliberately theme-invariant roles is +defined once and composed independently into both palettes; light mode never +inherits the dark palette. + +## Palette shape + +The palette has two kinds of roles: + +- Shared UI roles for surfaces, content, borders, interactions, status, shadows, + glass, and brand treatments. +- Isolated roles required by renderers or deliberately self-contained + experiences: the result grid, Monaco, data visualization, authentication, + onboarding, browser window controls, instance presets, and the color picker. + +Most product UI should need only the shared roles. The specialized roles exist +because collapsing syntax colors, chart-series identity, or native window +controls into generic UI colors would make those systems less understandable, +not more maintainable. + +## Brand sources and semantic mapping + +The palette is informed by two QuestDB Figma sources: + +- [QuestDB Design System – Core v1.1](https://www.figma.com/design/yka873CDRnp51hp8HBb8Nv/QuestDB-Design-System-%E2%80%93-Core-v1.1?node-id=13-2916&p=f&m=dev) + supplies the dark neutral anchors, the QDB Pink ramp, and the functional + accents: midnight `#141725`, charade `#21222c`, rock `#262833`, gray + `#32343e`, porcelain `#f8f8f2`, sky `#81d3f9`, green `#66bb6a`, and yellow + `#ffd54f`. +- [QuestDB Agents Landing Page](https://www.figma.com/design/HPVrUIVW8TGyowgZZ5M13K/QuestDB-Agents-Landing-Page?node-id=1-4264&p=f&m=dev) + supplies the systematic accent ramp. Its 341-degree ramp is the source of + truth because its rendered fills, hex labels, and HSL labels agree. A + neighboring 345-degree draft contains mismatches between the rendered fills + and the visible hex labels, so it is not used in product code. + +Source palette names never become application tokens. `QDB Pink 700`, for +example, describes a swatch, not why a component needs it. Code always asks for +a UI meaning such as `surfaceRaised`, `contentAccent`, or `statusInfo`. + +The product brand is QDB Pink, split by mode so it never reads as the error red: + +| Role | Dark | Light | +| --------------------- | ------------------ | ------------------ | +| On-chrome accent | Pink 300 `#f0428b` | Pink 700 `#bd0f58` | +| Strong accent | Pink 400 `#ee2b7c` | Pink 800 `#8e0b42` | +| Opaque primary action | Pink 700 `#bd0f58` | Pink 800 `#8e0b42` | +| Primary action hover | Pink 600 `#d41162` | Pink 700 `#bd0f58` | + +The on-chrome accent and the opaque action are different roles; do not collapse +them. Danger is a separate pure-red family. Brand pink and danger red must never +share a hex. + +The dark theme uses the source neutrals selectively rather than flooding every +large work surface with them. Midnight is reserved for the outer application +field; notebook stages, editors, grids, panels, and overlays use a lower-chroma +charcoal ladder derived around the source neutrals. The light theme keeps the +same semantic hierarchy on a compressed, lifted gray ladder designed for an +application rather than an inversion of the website's dark-only backgrounds. +Brand hue families are preserved across modes; lightness and, when needed, +saturation change to provide contrast against each theme's surfaces. + +## The model: contrast between neighbors + +There is no universal rule that every higher surface must be lighter. Lightness +is read locally: a user understands a panel from the surfaces immediately next +to it. The console therefore uses a small set of spatial roles and assigns them +consistently in both themes. + +1. `surfaceCanvas` is the application field. +2. `surfaceStage` recedes behind the user's primary work. +3. `surfaceBase` is the fixed chrome frame. +4. `surfaceInset` separates content embedded inside another surface. +5. `surfaceRaised` groups cards, cells, content panels, and interactive rows. +6. `surfaceOverlay` identifies floating surfaces: menus, popovers, dialogs, + tooltips, Monaco widgets, and the CTA banner. + +Dark mode generally expresses inset depth by becoming darker and raised depth by +becoming lighter. Light mode intentionally uses a gray application field, a +darker work stage, and brighter embedded content. This avoids both an all-white +document look and the alternating “zebra” bands that appear when every container +invents its own gray. The light ladder is deliberately compressed: the step from +stage to raised is about 18 RGB points, so surfaces carry structure without +heavy shadows. + +The practical rules are: + +- Adjacent regions with different responsibilities should not use the same + surface token. +- Siblings with the same responsibility should use the same surface token. +- Use one primary separation cue. Prefer a surface step; add a subtle border for + precise edges; add a shadow mainly when an element floats over content. +- Do not combine a large lightness jump, a strong border, and a heavy shadow + unless the element is a blocking overlay. +- Hover must differ from rest, and active must differ from hover. Never use + opacity on the whole control because it also weakens its text and icon. +- Hover inverts against its surface: one step darker in light, one step lighter + in dark. On chrome that is the `interactionHover` wash, not a surface step. +- `interactionNeutral` is the selected or pressed role. It is never a hover + fill. +- Focus is structural, not decorative. Keyboard focus is `statusInfo` at 75% + through `statusInfoFocus()`; do not remove it because a mouse hover exists. + +As a review heuristic, a meaningful boundary should remain visible at normal +zoom without becoming the first thing the eye sees. Text follows the usual +contrast targets: 4.5:1 for ordinary text and 3:1 for large text and meaningful +UI graphics. Not every panel boundary needs 3:1; boundaries that are necessary +to understand or operate the UI do. + +## Core palette + +Values are shown as dark / light. “Same” means the dark value is deliberately +shared by light mode. + +### Surfaces + +| Token | Dark / light | Purpose and allowed use | +| ---------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `surfaceCanvas` | `#141725` / `#e8eaee` | Root application field and empty console canvas. This is the primary large-area use of Core midnight. | +| `surfaceStage` | `#0c0d11` / `#e2e5ea` | Neutral recessed notebook/metrics work stage behind cells and editors. | +| `surfaceBase` | `#17181d` / `#eef0f3` | Top bar, dark-mode icon rails, and other fixed chrome. In light mode also the receding hover on raised cells. | +| `surfaceInset` | `#121317` / `#f7f8f9` | Embedded wells: chart bodies, compact code regions, keycaps, grid viewport, and dark-mode tooltips. | +| `surfaceRaised` | `#1d1e24` / `#f4f5f7` | Cells, cards, rows, content sidebars (schema, details, AI), results pane, and light-mode icon rails and footer. | +| `surfaceInput` | `#21222c` / `#fafbfc` | Editable fields, textareas, search inputs, and form selects. | +| `surfaceOverlay` | `#1d1e24` / `#fafbfc` | Dropdowns, context menus, dialogs, popovers, light-mode tooltips, Monaco widgets, and the CTA banner. | +| `surfaceValue` | `#202126` / `#ffffff` | Read-only values in table details, and light-mode cards that must lift above a raised panel (user chat bubbles). | +| `surfaceScrim` | `rgba(7, 7, 9, .72)` / `rgba(27, 31, 39, .32)` | Translucent occlusion for dialog backdrops, overlay arrows, and frozen-grid edge fades. Never an empty-state well. | +| `surfaceTabRail` | `rgba(18, 19, 23, .82)` / `rgba(244, 245, 247, .94)` | Translucent substrate beneath the liquid-glass tab indicator. Light is `surfaceRaised` at 94%. | +| `transparent` | `transparent` / same | Explicit absence of paint. This is a behavior token, not a palette color. | + +### Content + +| Token | Dark / light | Purpose and allowed use | +| --------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `contentPrimary` | `#f8f8f2` / `#1c2029` | Primary body text, titles, default icons, result-grid column names, and table/column type glyphs. | +| `contentSecondary` | `#9da1ad` / `#505968` | Supporting text, parenthetical type labels, and neutral toolbar icons. | +| `contentMuted` | `#858995` / `#565f6e` | Metadata and low-emphasis labels that remain readable on the raised surface. | +| `contentDisabled` | `#747985` / `#596271` | Disabled text/icons only; never use it merely to make something “quiet,” and never as a border. | +| `contentInverse` | `#f8f8f2` / same | Porcelain content on saturated fills: primary and danger buttons, switch thumbs, checkbox ticks. | +| `contentOnWarning` | `#000000` / same | Text/icons on bright yellow warning fills. | +| `neutralInk` | `#000000` / same | Opaque mask and picker anchor used on arbitrary fills rather than themed surfaces. | +| `contentAccent` | `#f0428b` / `#bd0f58` | Brand accent on chrome: rail navigation, tab underlines, `SelectMenu` check, drag chrome. Do not treat it as body text. | +| `contentAccentStrong` | `#ee2b7c` / `#8e0b42` | Saturated accent for copy-pulse highlights, prominent identity, and cases that pair with porcelain content. | +| `contentSearchMatch` | `rgb(163, 127, 96)` / amber `.32` | Search-match emphasis inside text; its light value matches Monaco's search highlight for consistent recognition. | + +### Borders + +| Token | Dark / light | Purpose and allowed use | +| -------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `borderSubtle` | `rgba(255,255,255,.04)` / `rgba(28,32,41,.07)` | Quiet dividers inside a panel or between a panel and the work surface. | +| `borderDefault` | `rgba(255,255,255,.13)` / `rgba(28,32,41,.15)` | Standard component, input, control, floating-surface, and chrome-rail boundary. | +| `borderStrong` | `#3d414d` / `#b0b7c2` | Activated control stroke: field focus, open dropdowns, resizers, sash hover, segmented tracks. | +| `borderAccent` | brand accent at `.42` / brand accent at `.42` | Selected/accent boundary. | +| `borderAccentStrong` | brand accent at `.52` / brand accent at `.56` | High-priority accented boundary such as the selected instance color swatch. | + +### Interaction and controls + +| Token | Dark / light | Purpose and allowed use | +| ------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `interactionNeutral` | `#32343e` / `#e8eaee` | Selected list row, selected choice card, selected vertical tab, and pressed chip fill. | +| `interactionNeutralHover` | `#292b35` / `#e2e5ea` | Hover one step below a selected neutral well, and hover inside a `controlTrack`. | +| `interactionHover` | white `.055` / ink `.075` | Inverting hover wash for menus, lists, tree rows, ghost buttons, tabs, and grid rows. | +| `interactionSelected` | white `.11` / ink `.15` | Result-grid selected row; exactly twice `interactionHover`. | +| `interactionAccentActive` | brand accent at `.10` / brand accent at `.13` | Branded latch well behind the rail navigation and Add Cell. | +| `interactionGuide` | `#6272a4` / `#56657f` | Drag guides, resize handles, text selection helpers, and Monaco's auxiliary/inactive markers. | +| `scrollbarThumb` | white `.13` / `#d7dbe3` | Native, webkit, and Monaco scrollbar handles; chrome, not content. | +| `controlSurface` | `#262833` / `#f6f7f8` | Default solid secondary button, toolbar select trigger, and keycap face. | +| `controlSurfaceHover` | `#32343e` / `#e7e9ed` | Hover fill for `controlSurface`; dark mode rises to the Core gray anchor. | +| `controlTrack` | `#262833` / `#e8eaee` | Segmented-control substrate (Table/Chart, List/Grid chips). | +| `controlTrackRest` | `#262833` / `#c9cdd4` | Switch track in the off state. | +| `controlTrackStroke` | white `.15` / ink `.25` | The 0.5px inset stroke on the switch track. | + +### Actions and status + +| Token(s) | Dark / light | Purpose and allowed use | +| ----------------------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| `actionPrimary`, `actionPrimaryHover` | `#bd0f58`, `#d41162` / `#8e0b42`, `#bd0f58` | Primary-button rest and hover fills, and the selected calendar range. Both keep porcelain contrast. | +| `statusDanger`, `statusDangerStrong` | `#ff3333`, `#db2424` / `#ce1717`, `#ce1717` | Danger type/icons, and the opaque destructive fill. Light Strong equals Text on purpose. | +| `statusDangerContrast`, `statusDangerSubtle` | `#ff4d4d`, `#ff8080` / `#b81414`, `#8a0f0f` | Danger type on the 15% wash and on the 40% hover wash. These are type colors, not fills. | +| `statusDangerMuted` | Strong at `.72` / Strong at `.72` | Muted destructive decoration and dark-mode banner strokes. | +| `statusDangerSurface`, `statusDangerSurfaceHover`, `statusDangerBorder` | red at `.15`, `.40`, `.28` / red at `.15`, `.40`, `.28` | Danger chip/badge/banner fill, its hover, and its border. | +| `statusSuccess`, `statusSuccessStrong` | `#66bb6a`, `#188a5d` / `#067047`, `#05603e` | Success type/icons, and the stronger green for accepted-change marks and glyph badges. | +| `statusSuccessFill` | `#3fa659` / `#1f7a39` | Opaque positive fill that carries no text: the switch on-track. | +| `statusSuccessSurface`, `statusSuccessBorder` | green at `.12`, `.24` / green at `.16`, `.28` | Success chip/banner fill and boundary. | +| `statusWarning`, `statusWarningSurface` | `#ffd54f`, yellow at `.10` / `#8a570f`, amber at `.14` | Warning type/icons and the low-emphasis warning fill. | +| `statusAttention` | `#e3ce78` / `#745c00` | Pending or attention-needed state that is neither an error nor a warning action. | +| `statusInfo` | `#81d3f9` / `#176f87` | Info type, notebook identity glyphs, grid cell-focus ring, and the keyboard focus ring at 75%. | +| `statusInfoSubtle` | `#b2e7ff` / `#159cc1` | Quiet on-glyph for non-rail latched toggles. | +| `statusInfoControl` | `#0c80f3` / `#0a88db` | Opaque persistent on-fill: checked checkboxes, with a porcelain tick. | +| `statusInfoSurface`, `statusInfoSurfaceStrong` | sky at `.05`, `.30` / teal at `.10`, `.10` | Latch well and grid focused-cell wash; Strong is the MCP pairing banner well. | +| `statusFeature` | `#a99de8` / `#6553aa` | Feature/AI distinction when brand pink or status colors would imply the wrong meaning. | +| `statusAssistant`, `statusAssistantStrong` | `#d14671`, `#892c6c` in both themes | Fixed assistant progress gradient and completion checks across theme changes. | + +### Shadows and glass + +Light-mode shadows are quiet because the compressed surface ladder does the +elevation work. Geometry matters as much as opacity: light lifts use a 1–2px +offset and a short blur, never a 20px haze. + +| Token(s) | Dark / light | Purpose and allowed use | +| --------------- | -------------------------- | --------------------------------------------------------------------------------- | +| `shadowSubtle` | black `.08` / ink `.035` | Contact shadow: the first layer of a card, chip, or title-bar lift. | +| `shadowSoft` | black `.16` / ink `.055` | Card and hover lift; the heel of the floating stack. | +| `shadowMedium` | black `.28` / ink `.08` | Weight layer of dropdowns and popovers; the dark loom. | +| `shadowStrong` | black `.42` / ink `.16` | Large popovers and dialogs. | +| `shadowOverlay` | black `.58` / ink `.22` | Strongest depth cue for dialogs, banners, legacy overlays, and pinned-grid edges. | +| `glassSurface` | white `.075` / white `.58` | Moving liquid-glass selection lens. | +| `glassBorder` | white `.14` / ink `.14` | Main glass lens edge. | +| `glassEdge` | white `.22` / ink `.22` | Refractive/highlight edge; use sparingly inside the glass implementation. | + +The shared floating stack in `floatingSurfaceStyles` is: heel `0 1px 2px` +`shadowSoft`, weight `0 4px 6px -2px` `shadowMedium`, then a loom that splits by +mode: `0 8px 8px -4px` `shadowSubtle` in light, `0 12px 16px -4px` +`shadowMedium` in dark. Retune opacities per mode; do not change shared geometry +to fix one mode. + +### Brand gradients + +The gradients are reserved for QuestDB identity and AI affordances. Ordinary +buttons and surfaces use solid semantic roles. The QuestDB logo mark retains its +supplied SVG gradients, while its wordmark inherits the surrounding semantic +content color so it remains readable in both themes. + +| Token(s) | Dark / light | Purpose | +| ---------------------------------------- | --------------------------------------------- | -------------------------------------------------------------- | +| `brandGradientStart`, `brandGradientEnd` | `#e21269` → `#8e0b42` / `#bd0f58` → `#8e0b42` | QuestDB identity gradient for theme-aware product affordances. | +| `aiGradientStart`, `aiGradientEnd` | `#ee2b7c` → `#8e0b42` / `#bd0f58` → `#8e0b42` | AI sparkle gradient, kept separate so it can diverge later. | + +## Specialized palettes + +These roles are still semantic, but they belong to a renderer or an isolated +experience. General UI components must not consume them. + +### Result grid + +The grid needs a dedicated dense-data ladder. In dark mode the empty grid field +is darkest, rows are darker, and headers are dark. In light mode the field is +lightest, rows are lighter, and headers are the strongest gray. That preserves +structure without card borders around every cell. In light mode `gridRow` equals +`surfaceRaised`, so a notebook cell and its rows are one surface and the header +bar is the structure cue. + +| Token | Dark / light | Purpose | +| --------------- | --------------------- | -------------------------------------------------------------------------- | +| `surfaceInset` | `#121317` / `#f7f8f9` | Empty grid field and viewport. | +| `gridRow` | `#17181d` / `#f4f5f7` | Standard data row. | +| `gridHeader` | `#202126` / `#e7eaee` | Column header background. Header names are `contentPrimary` at weight 600. | +| `gridSelection` | `#252830` / `#d8dce3` | Monaco selection substrate. The React grid does not read it. | + +Grid interaction is composed from shared roles: row hover is one +`interactionHover` over the field, the selected row is `interactionSelected`, +the focused cell stacks `statusInfoSurface` over that with a `statusInfo` ring, +and the copy pulse follows the ring. Column resize and frozen-handle hover use +`borderStrong`. + +### Monaco editor + +Monaco is themed through its API, not through application CSS. Its +editor-specific roles remain isolated; ordinary surface, content, interaction, +and status roles are reused where they carry the same meaning. + +| Token(s) | Dark / light | Purpose | +| --------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `editorCanvas`, `surfaceOverlay`, `editorBorder` | `#17181d`, `#1d1e24`, `#121317` / `#eef0f3`, `#fafbfc`, `#e2e5ea` | Editor field, widgets/menus, and editor-specific edge. Light canvas matches `surfaceBase`. | +| `contentPrimary` | `#f8f8f2` / `#1c2029` | Default code text. | +| `interactionHover`, `interactionNeutral`, `controlSurfaceHover` | white `.055`, `#32343e`, `#32343e` / ink `.075`, `#e8eaee`, `#e7e9ed` | Menu selection, separators, and list-filter states. | +| `scrollbarThumb` | white `.13` / `#d7dbe3` | Overlay scrollbar slider, shared with the rest of the console. | +| `editorSelection`, `editorSelectionAccent` | `#44475a`, `#44475a` / `#aec8ec`, `#7a99c4` | Monaco and native form-control selection, plus its stronger selection boundary. | +| `editorActiveLine`, `editorActiveLineBorder` | white `.03`, transparent / ink `.035`, transparent | Active-line wash and border. The transparent border prevents layout/color artifacts. | +| `editorSuggestionMatchActive` | `#ff9abb` / `#8a0f35` | Active autocomplete match. | +| `editorRun`, `statusDanger` | `#ffffff`, `#ff3333` / `#067047`, `#ce1717` | Query-dropdown run icon and error decoration. The run icon intentionally changes by theme. | +| `editorSyntaxNumber` | `#50fa7b` / `#056c45` | Numbers, and the gutter run glyph. | +| `contentMuted` | `#858995` / `#565f6e` | Comments. | +| `editorSyntaxString` | `#f1fa8c` / `#695f00` | Strings. | +| `editorSyntaxKeyword` | `#ff79c6` / `#b81447` | SQL keywords. | +| `editorSyntaxType` | `#8be9fd` / `#0a6a80` | Types and type-like identifiers. | +| `editorSyntaxConstant` | `#bd93f9` / `#6553aa` | Constants. | +| `editorSyntaxVariable` | `#ffb86c` / `#8e500a` | Variables. | +| `editorErrorHighlight`, `editorSuccessHighlight` | red/green `.15` / red/green `.13` | Full-line execution feedback. | +| `editorSearchHighlight`, `editorAiHighlight` | orange/yellow `.50` / amber `.32`/`.28` | Search and AI-generated-range highlights. | + +### Data visualization + +Series keep the same conceptual hue identity across themes; the light theme uses +darker lightness values to maintain contrast against a light plot while the dark +theme uses lighter values against a dark plot. The slot is the data +identity—components must not pick a series by color name. + +| Token | Dark / light | Purpose | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `dataGrid` | gray `.11` / ink `.16` | Chart axes and grid lines. | +| `dataArea` | white `.012` / ink `.018` | Quiet plot-area separation. | +| `dataSeries1` | `#ff6b6b` / `#d51515` | Metric widget series slot 1, red. `dataSeries*` are user-pickable metric colors; auto-assigned chart series use `chartSeries*`. | +| `dataSeries2` | `#4ecdc4` / `#0c7966` | Series slot 2, teal. | +| `dataSeries3` | `#ffd93d` / `#80690d` | Series slot 3, yellow/olive. | +| `dataSeries4` | `#95d86e` / `#357b0c` | Series slot 4, green. | +| `dataSeries5` | `#ff8f40` / `#ac5111` | Series slot 5, orange. | +| `dataSeries6` | `#bd93f9` / `#6415d5` | Series slot 6, violet. | +| `dataSeries7` | `#50fa7b` / `#0c7b37` | Series slot 7, bright green. | +| `dataSeries8` | `#ff79c6` / `#cc147e` | Series slot 8, magenta. | +| `dataSeries9` | `#8be9fd` / `#1072a5` | Series slot 9, cyan/blue. | +| `dataSeries10` | `#f1fa8c` / `#61740b` | Series slot 10, yellow/olive. | +| `chartSeries1..8` | `#8be9fd #d14671 #ffb86c #bd93f9 #f1fa8c #ff79c6 #50fa7b #ff5555` / `#1590aa #e42560 #c16f18 #7425e4 #728d11 #e425ac #139842 #e42525` | Auto-assigned series order for notebook/result charts and quick-vis. Calm brand hues lead; assignment is positional, not user-picked. | +| `dataPositive`, `dataNegative` | `#2ca875`, `#d94d58` / `#05603e`, `#a81f2e` | Directional financial/operational data, not generic form success/error. | + +### Authentication + +Authentication is a visually isolated first impression and keeps a deliberate +dark-crimson identity. These roles belong to login/version UI. In dark mode +`authBackdrop` is also the well behind AI chat tool/code blocks and user +bubbles; in light mode those use `surfaceValue` instead, because a dark login +well cannot lift off a raised panel. + +| Token | Dark / light | Purpose | +| ------------------------------- | ---------------------------------------------- | --------------------------------------------------------- | +| `authBackdrop` | `#1d070e` / `#c7cbd2` | Login page field and dark-mode AI chat wells. | +| `authAccent`, `authAccentMuted` | `#9089fc`, violet `.64` / `#7d2948`, ink `.46` | Login illustration/action accent and its muted companion. | +| `surfaceCanvas`, `authBorder` | `#141725`, `#353946` / `#e8eaee`, `#a8b0bd` | Login form card and prominent edge. | +| `authVersionContent` | `#f5f3f0` / `#e7e3e5` | Version badge readable content. | + +### Onboarding + +The notebook and MCP onboarding experience follows the active luminance mode. +Dark mode retains the original terminal presentation; light mode reuses values +from the light surface, content, feature, and information roles. Modal borders, +subtle fills, and shadows use the general semantic tokens directly. + +| Token | Dark / light | Purpose | +| ------------------- | ----------------------- | -------------------------------------- | +| `onboardingSurface` | `#050505` / `#e3e5e9` | Experience backdrop. | +| `onboardingDivider` | white `.04` / ink `.07` | Quiet internal rule. | +| `onboardingClose` | `#858585` / `#565f6e` | Close/secondary content. | +| `onboardingCommand` | `#111111` / `#fbfcfd` | Command and transcript frame surfaces. | +| `onboardingInput` | `#202020` / `#eceef1` | Simulated input. | +| `onboardingPrompt` | `#93adff` / `#6553aa` | Prompt identity. | +| `onboardingAccent` | `#29c6be` / `#176f87` | Agent/progress accent. | + +### Fixed-purpose colors + +These values encode external or user-selected identity, so replacing them with +generic status roles would change meaning. + +| Token(s) | Value | Purpose | +| -------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `instancePreset1`, `instancePreset2`, `instancePreset3` | `rgb(199,7,45)`, `rgb(0,170,59)`, `rgb(0,122,255)` | Stable user-selectable instance identities. | +| `windowControlClose`, `windowControlMinimize`, `windowControlExpand` | `#ff5f57`, `#febc2e`, `#28c840` | Familiar macOS-style window controls in illustrative UI. | +| `pickerHue0` … `pickerHue5` | red, yellow, green, cyan, blue, magenta endpoints | Mechanical hue stops for the color picker. Indexed names are intentional because the token is a position, not a UI meaning. | + +## Choosing a surface + +| Relationship | Recommended combination | +| ----------------------------------- | --------------------------------------------------------------------------------------- | +| Application shell → work area | `surfaceCanvas` → `surfaceStage` | +| Icon rail → content sidebar | `surfaceBase` (dark) or `surfaceRaised` (light) → `surfaceRaised`, `borderDefault` rail | +| Content sidebar → work surface | `surfaceRaised` → stage or editor canvas, `borderSubtle` edge | +| Stage → notebook/metric card | `surfaceStage` → `surfaceRaised` | +| Card header → embedded editor/chart | `surfaceRaised` → `surfaceInset` or the renderer-specific canvas | +| Page → input | parent surface → `surfaceInput` + `borderDefault` | +| Page → menu/dropdown/tooltip | parent surface → `surfaceOverlay` + `borderDefault` + floating shadow stack | +| Dialog → read-only value | dialog surface → `surfaceValue` | +| Dialog → empty-state well | dialog surface → `surfaceRaised` | +| Raised panel → card that must lift | `surfaceRaised` → `surfaceValue` (light); dark keeps its own well | +| Result-grid viewport → row → header | `surfaceInset` → `gridRow` → `gridHeader` | + +Do not choose a token by comparing its hex value to a mockup. Choose the role +from this table, then tune the role in both themes if the relationship is wrong +everywhere it appears. + +## Composition rules + +These bindings are the settled decisions behind the current UI. Follow them when +building or reviewing a component. + +### Chrome + +- Icon rails, top bar, and footer are `surfaceBase` in dark mode. In light mode + the rails and footer share `surfaceRaised` with the content panels; the + `borderDefault` rail hairline carries the stagger instead of a darker gray. +- Content sidebars (schema, details drawer, AI chat) are `surfaceRaised` on both + sides in both modes. Panel headers do not step above panel bodies; the step is + panel versus work. +- Rails use `borderDefault`; panel-to-work edges use `borderSubtle`. Do not + promote panel-to-work edges, or the two weights collapse. +- The results action bar owns the pane-top hairline, because it spans under the + sidebar and the log does not. +- Tooltips, popovers, dropdowns, and the instance hover card are floating + chrome: `surfaceOverlay` in light, `borderDefault` stroke, the shared shadow + stack. Dark tooltips keep `surfaceInset`. + +### Hover, selection, and focus + +- Menu items, tree rows, search rows, chat history rows, choice cards, and + light-mode ghost buttons hover with `interactionHover`. Dark ghost buttons on + base chrome may lift to `surfaceRaised`; on a raised cell they must recede. +- Selected rows, cards, and vertical tabs are `interactionNeutral` with + `borderDefault`. Their hover sits one step below the selected well: + `interactionNeutralHover` in dark, `surfaceBase` for light vertical tabs. +- Default field focus, open `SelectMenu` triggers, and resize affordances use + `borderStrong`. Accent focus is opt-in through `$tone="accent"` and is + reserved for the AI composer and login. +- Keyboard focus rings everywhere use `statusInfoFocus(theme.color.statusInfo)`. + Notebook cell focus stays branded. +- Non-rail latched toggles use `activeTone="info"`: glyph `statusInfoSubtle`, + well `statusInfoSurface`. Rail navigation stays `activeTone="accent"` with + `contentAccent` on `interactionAccentActive`. + +### Controls + +- Primary buttons are `actionPrimary` with `contentInverse`. +- Danger buttons are solid `statusDangerStrong` with `contentInverse`; hover is + `statusDanger`. The `dangerGhost` variant rests on `statusDangerSurface` with + `statusDangerContrast` type and hovers to `statusDangerSurfaceHover`; hover + type is `statusDangerSubtle` in light and `statusDanger` in dark. +- Switches: on-track `statusSuccessFill`, off-track `controlTrackRest`, a 0.5px + inset `controlTrackStroke`, thumb `contentInverse`. +- Checkboxes: checked fill `statusInfoControl`, tick `contentInverse`. +- Form selects are `SelectMenuControl` on `surfaceInput`, matching their sibling + inputs. Toolbar selects stay `controlSurface`. +- Menu items built from `Button` must lock their hover border, because the + secondary hover would repaint a `borderSubtle` divider as `borderStrong`. + +### Badges and status marks + +- `Badge` `shape="chip"` is the status label: 4px radius, no border, status + surface fill. `shape="pill"` is the compact count badge with a 12% wash and a + 32% tone stroke. Do not restyle one into the other. +- Light-mode Enabled chips use `statusSuccessSurface` + `statusSuccessBorder`; + dark keeps the neutral well with success type. +- Footer status marks share one rounded-square geometry; color carries state. + +### Object identity and type + +- Table, materialized-view, view, column-type, and designated-timestamp glyphs + are `contentPrimary`, the same as the object name. Parenthetical type labels + stay `contentSecondary`. +- Result-grid header names are `contentPrimary` at weight 600. The result row + count is `fontSize.lg` at weight 600 in the UI font, not monospace. +- Notebook title glyphs and chrome-tab notebook/metrics favicons are + `statusInfo`. + +### Where brand stays + +Rail navigation, tab underlines, notebook cell focus, the `SelectMenu` check, AI +`$tone="accent"` fields, and drag chrome are the branded surfaces. Everything +else that once used brand for hover, focus, or selection has moved to the +neutral or info families. Do not retune `contentAccent` to fix a hover or focus +problem; rebind the component instead. + +### Sanctioned one-off washes + +These derive from a role with `withAlpha()` and must not become tokens: + +- Notebook drag slot: `contentPrimary` at `.08` light / `.10` dark. +- MCP pairing error banner in light: `rgba(189, 40, 56, 0.08)`. +- Reset Provider ghost hover: `#db2424` at `.30` dark / `#bd2828` at `.10` + light. + +## Runtime architecture + +- Styled components read `theme.color` directly. The `color()` helper resolves + the same typed object. +- Non-React renderers use `getThemeColor()` from [`runtime.ts`](./runtime.ts); + the active object is synchronized by `ThemeModeProvider`. +- Legacy SCSS uses `theme-color(name)`. Sass emits `var(--qdb-color-name)`, and + [`css-variables.ts`](./css-variables.ts) declares every property from this + same palette. This lets the compiled `main.scss` bundle follow a runtime theme + change through one generated declaration block. Light-only SCSS overrides key + off `html[data-theme="light"]`. +- SVG templates use semantic placeholders that are resolved before rendering. +- Monaco receives a complete theme through its public theming API. Any custom + properties inside Monaco itself are third-party implementation details, not + QuestDB palette inputs. +- `withAlpha()` derives one-off translucent renderer colors from a semantic + role. It handles hex and RGB inputs and avoids a separate token for every + renderer-specific opacity. +- `statusInfoFocus()` is the shared keyboard focus ring: `statusInfo` at 75%. A + 1px stroke that dies on `surfaceValue` uses solid `statusInfo` instead. + +### User preference and System mode + +The saved preference and the rendered theme are deliberately separate: + +- `ThemePreference` is `system`, `light`, or `dark` and is stored under + `appearance.themePreference`. +- `ThemeMode` is always the resolved `light` or `dark` value consumed by styled + components, Monaco, charts, legacy styles, and runtime renderers. +- The provider tracks the operating-system mode independently so switching back + to System always resolves to the current device preference. +- `system` resolves through `prefers-color-scheme`. A media-query change updates + the rendered theme immediately, so operating-system automatic schedules are + followed without reloading the console. +- Explicit `light` and `dark` preferences ignore operating-system changes. +- Preference changes synchronize across same-origin tabs through the browser + `storage` event. + +The root HTML entry point resolves the saved preference before the application +bundle executes and sets `data-theme` plus the native `color-scheme`. The React +provider uses the same resolution rules, so the first React render does not use +the opposite theme. + +## Making a change + +There are two kinds of theme change. Decide which one you are making before you +touch a file. + +| Change | What it is | Where it lands | +| ----------- | ---------------------------------------------------- | ------------------------------------------------------------ | +| **Palette** | Retune an existing role in one or both themes | `index.ts` only, then the value tables in this document | +| **Binding** | A component uses the wrong role, or needs a new role | The component file, plus a new token only if reuse would lie | + +Palette is the default. Binding is the exception, including “brand is doing a +job it should not.” Never invent a component-named color such as +`notebookCellBackground`. Either retune the role, rebind the component to a +different existing role, or add a role whose meaning is new. + +For every change, walk the neighbors in both modes: the surface it sits on, the +surfaces beside it, and its own rest, hover, active, focus, and disabled states. + +## Contribution rules + +1. Never add a literal UI color outside `index.ts`. Immutable supplied brand + artwork such as `QuestDBLogo` is the sole exception and must not be recolored + through theme roles. +2. Never add a first-party CSS custom property for theming. +3. Use a semantic role, not a hue name and not a component name. +4. Keep every runtime token directly under `theme.color`; organize source groups + with comments. A genuinely theme-invariant role belongs in `invariantColors`, + which is spread independently into both flat palettes. +5. Add a token only when an existing role would communicate the wrong meaning or + when a renderer needs stable independent control. A new surface role is + justified only when two neighbors with different jobs are forced to share a + token. +6. Define the dark and light value together and inspect their neighboring + surfaces in both modes. +7. Preserve state ordering: rest, hover, active/selected, focus, disabled. +8. Prefer `withAlpha()` for a renderer-specific wash. Promote it to a token only + when the same opacity has a shared semantic meaning across components. +9. Fix a mode-specific problem with a mode-specific value, not with shared + geometry or a shared token change. +10. Run `yarn lint:colors` and `yarn typecheck` after changes. + +Before adding a new color token, answer all four questions: + +- What meaning does it carry? +- Which existing token is closest, and why is reusing it incorrect? +- Which surfaces or components may consume it? +- What is its corresponding dark/light behavior? + +If those answers are not clear, the color is probably a local styling tweak, not +a design-system token.