From 1702948a7ed477140fcee69f122a3019a7c648a1 Mon Sep 17 00:00:00 2001 From: Zack Nelson Date: Tue, 1 Sep 2026 18:19:23 +0100 Subject: [PATCH 1/6] 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 2/6] =?UTF-8?q?feat:=20continue=20light=20chrome=20pass=20?= =?UTF-8?q?=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 4/6] =?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 5/6] =?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 6/6] 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