diff --git a/THEMING.md b/THEMING.md deleted file mode 100644 index 3c1b172dd..000000000 --- a/THEMING.md +++ /dev/null @@ -1,408 +0,0 @@ -# QuestDB Web Console theming - -This document is the contract for color in the Web Console. It explains the -palette, the surface hierarchy, the semantic meaning of every token, and how a -new component should choose colors. - -The implementation lives in [`src/theme/index.ts`](src/theme/index.ts). The -palette is intentionally flat: every color is `theme.color.tokenName`, grouped -by comments in the source. There are no nested color objects, and the only -first-party CSS custom properties are the `--qdb-*` properties generated from -this palette so legacy SCSS can read it. A small set of deliberately -theme-invariant roles is defined once and composed independently into both -palettes; light mode never inherits the dark palette. - -## What changed - -Before this redesign, `theme.color` exposed dark-only tokens. Many encoded a -hue, a component, or one specific opacity, while other UI colors still lived -outside the palette. That made global tuning difficult and encouraged new -near-duplicates. - -The palette now has two kinds of roles: - -- Shared UI roles for surfaces, content, borders, interactions, status, - shadows, glass, and brand treatments. -- Isolated roles required by renderers or deliberately self-contained - experiences: the result grid, Monaco, data visualization, authentication, - onboarding, browser window controls, instance presets, and the color picker. - -Most product UI should need only the shared roles. The specialized roles exist -because collapsing syntax colors, chart-series identity, or native window -controls into generic UI colors would make those systems less understandable, -not more maintainable. - -## Brand sources and semantic mapping - -The palette is informed by two QuestDB Figma sources: - -- [QuestDB Design System – Core v1.1](https://www.figma.com/design/yka873CDRnp51hp8HBb8Nv/QuestDB-Design-System-%E2%80%93-Core-v1.1?node-id=13-2916&p=f&m=dev) - supplies the dark neutral anchors and functional accents: midnight `#141725`, - charade `#21222c`, rock `#262833`, gray `#32343e`, porcelain `#f8f8f2`, sky - `#81d3f9`, green `#66bb6a`, and yellow `#ffd54f`. -- [QuestDB Agents Landing Page](https://www.figma.com/design/HPVrUIVW8TGyowgZZ5M13K/QuestDB-Agents-Landing-Page?node-id=1-4264&p=f&m=dev) - supplies the systematic QuestDB accent ramp used for actions and semantic - accent content. Its 341-degree ramp is the source of truth because its - rendered fills, hex labels, and HSL labels agree. A neighboring 345-degree - draft contains mismatches between the rendered fills and the visible hex - labels, so it is not used in product code. - -Source palette names never become application tokens. `QDB Pink 700`, for -example, describes a swatch, not why a component needs it. The same swatch may -therefore map to `contentAccent` in light mode and `actionPrimaryHover` in light -mode, while those roles receive different lightness values in dark mode. Code -always asks for a UI meaning such as `surfaceRaised`, `contentAccent`, or -`statusInfo`. - -The dark theme uses the source neutrals selectively rather than flooding every -large work surface with them. Midnight is reserved for the outer application -field; notebook stages, editors, grids, panels, and overlays use a lower-chroma -charcoal ladder derived around the source neutrals. The light theme keeps the -same semantic hierarchy but uses neutral gray surfaces designed for an -application rather than attempting to invert the website's dark-only -backgrounds. Brand hue families are preserved across modes; lightness and, when -needed, saturation change to provide contrast against each theme's surfaces. - -## The model: contrast between neighbors - -There is no universal rule that every higher surface must be lighter. Lightness -is read locally: a user understands a panel from the surfaces immediately next -to it. The console therefore uses a small set of spatial roles and assigns them -consistently in both themes. - -1. `surfaceCanvas` is the application field. -2. `surfaceStage` recedes behind the user's primary work. -3. `surfaceBase` unifies fixed chrome and ordinary panels. -4. `surfaceInset` separates content embedded inside another surface. -5. `surfaceRaised` groups cards, cells, and interactive rows. -6. `surfaceOverlay` identifies floating or overlay-like surfaces. It covers - menus, popovers, dialogs, Monaco widgets, the CTA banner, and legacy editor - chrome that needs the same elevation. - -Dark mode generally expresses inset depth by becoming darker and raised depth by -becoming lighter. Light mode intentionally uses a gray application field, a -darker work stage, and brighter embedded content. This avoids both an all-white -document look and the alternating “zebra” bands that appear when every container -invents its own gray. - -The practical rules are: - -- Adjacent regions with different responsibilities should not use the same - surface token. -- Siblings with the same responsibility should use the same surface token. -- Use one primary separation cue. Prefer a surface step; add a subtle border for - precise edges; add a shadow mainly when an element floats over content. -- Do not combine a large lightness jump, a strong border, and a heavy shadow - unless the element is a blocking overlay. -- Hover must differ from rest, and active must differ from hover. Never use - opacity on the whole control because it also weakens its text and icon. -- Focus is structural, not decorative. Use `borderAccentStrong` or the shared - focus treatment; do not remove it because a mouse hover already exists. - -As a review heuristic, a meaningful boundary should remain visible at normal -zoom without becoming the first thing the eye sees. Text follows the usual -contrast targets: 4.5:1 for ordinary text and 3:1 for large text and meaningful -UI graphics. Not every panel boundary needs 3:1; boundaries that are necessary -to understand or operate the UI do. - -## Core palette - -Values are shown as dark / light. “Same” means the dark value is deliberately -shared by light mode. - -### Surfaces - -| Token | Dark / light | Purpose and allowed use | -| ---------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `surfaceCanvas` | `#141725` / `#d9dce2` | Root application field and empty console canvas. This is the primary large-area use of Core midnight. | -| `surfaceStage` | `#0c0d11` / `#c9cdd4` | Neutral recessed notebook/metrics work stage behind cells and editors. | -| `surfaceBase` | `#17181d` / `#e3e5e9` | Top, side, and bottom chrome; panels and drawers that visually belong to that shell. | -| `surfaceInset` | `#121317` / `#eceef1` | Embedded wells: chart bodies, compact code regions, keycaps, and nested tool regions. | -| `surfaceRaised` | `#1d1e24` / `#e8eaee` | Cells, cards, rows, and controls raised from a parent surface. | -| `surfaceInput` | `#21222c` / `#f2f3f5` | Editable fields, textareas, and search inputs. | -| `surfaceOverlay` | `#1d1e24` / `#f2f3f5` | Dropdowns, context menus, dialogs, Monaco widgets, the CTA banner, and overlay-like legacy editor chrome. | -| `surfaceValue` | `#202126` / `#fbfcfd` | Read-only values in table details. It exists so label/value rows remain consistent without changing their layout. | -| `surfaceScrim` | `rgba(7, 7, 9, .72)` / `rgba(27, 31, 39, .38)` | Translucent occlusion for dialog backdrops, recessed placeholders, overlay arrows, and frozen-grid edge fades. | -| `surfaceTabRail` | `rgba(18, 19, 23, .82)` / `rgba(218, 221, 227, .94)` | Translucent substrate beneath the liquid-glass tab indicator. | -| `transparent` | `transparent` / same | Explicit absence of paint. This is a behavior token, not a palette color. | - -### Content - -| Token | Dark / light | Purpose and allowed use | -| --------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `contentPrimary` | `#f8f8f2` / `#1c2029` | Primary body text, titles, and default icons. Dark mode uses the brand porcelain. | -| `contentSecondary` | `#9da1ad` / `#505968` | Supporting text and neutral toolbar icons. This is the requested shared gray icon color in dark mode. | -| `contentMuted` | `#858995` / `#565f6e` | Metadata and low-emphasis labels that remain readable on the raised Core surface. | -| `contentDisabled` | `#747985` / `#596271` | Disabled text/icons only; never use it merely to make something “quiet.” | -| `contentInverse` | `#f8f8f2` / same | Porcelain content on saturated fills and other surfaces that need fixed light content. | -| `contentOnWarning` | `#000000` / same | Text/icons on bright yellow warning fills. | -| `neutralInk` | `#000000` / same | Opaque mask and picker anchor used on arbitrary fills rather than themed surfaces. | -| `contentAccent` | `#c94f74` / `#b81447` | Brand-accent icons, short labels, schema/table emphasis, focus, and selected accents. Do not treat it as default body text. | -| `contentAccentStrong` | `#cf1750` / `#8a0f35` | Saturated accent for selection fills, prominent identity, and cases that pair with porcelain content. | -| `contentObject` | `#81d3f9` / `#b81447` | Database object identity such as result-grid column names. Sky is retained in dark mode; brand crimson is used in light mode. | -| `contentSearchMatch` | `rgb(163, 127, 96)` / amber `.32` | Search-match emphasis inside text; its light value matches Monaco's search highlight for consistent recognition. | - -### Borders - -| Token | Dark / light | Purpose and allowed use | -| -------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------- | -| `borderSubtle` | `rgba(255,255,255,.04)` / `rgba(28,32,41,.07)` | Quiet dividers between already distinct surfaces. | -| `borderDefault` | `rgba(255,255,255,.13)` / `rgba(28,32,41,.15)` | Standard component, panel, input, and control boundary. | -| `borderStrong` | `#3d414d` / `#828b99` | Resizers, strongly bounded segmented controls, and edges that must remain findable. | -| `borderAccent` | brand accent at `.42` / brand accent at `.42` | Selected/accent boundary. | -| `borderAccentStrong` | brand accent at `.52` / brand accent at `.56` | Keyboard focus and high-priority accented boundary. | - -### Interaction and controls - -| Token | Dark / light | Purpose and allowed use | -| ------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------- | -| `interactionNeutral` | `#32343e` / `#d9dce2` | Neutral selected/pressed fill and legacy control fill. Dark mode uses the Core gray anchor. | -| `interactionNeutralHover` | `#292b35` / `#d2d6dd` | Hover for neutral rows and controls when a solid fill is needed. | -| `interactionHover` | `rgba(255,255,255,.055)` / `rgba(28,32,41,.075)` | Minimal hover overlay for tabs, icon buttons, and transparent actions. | -| `interactionAccentHover` | brand accent at `.08` / brand accent at `.07` | Quiet accent hover, especially schema/search rows. | -| `interactionAccentActive` | brand accent at `.15` / brand accent at `.13` | Selected accent row or active accent control. | -| `interactionGuide` | `#6272a4` / `#56657f` | Drag guides, resize handles, text selection helpers, and Monaco's auxiliary/inactive markers. | -| `scrollbarThumb` | white at `.13` / `#62656b` | Native vertical and horizontal scrollbar handles; neutral and distinct from editor selection. | -| `controlSurface` | `#262833` / `#f6f7f8` | Default solid secondary button, segmented-control item, and keycap face. | -| `controlSurfaceHover` | `#32343e` / `#e7e9ed` | Hover fill for `controlSurface`; dark mode rises to the Core gray anchor. | -| `controlTrack` | `#262833` / `#d9dce2` | Switch track and segmented-control substrate; matches secondary buttons in dark mode. | -| `controlKnob` | `#f8f8f2` / `#505968` | Resting switch knob on `controlTrack`; checked knobs use `contentInverse` on the accent fill. | - -### Actions and status - -| Token(s) | Dark / light | Purpose and allowed use | -| ----------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| `actionPrimary`, `actionPrimaryHover` | `#b81447`, `#cf1750` / `#8a0f35`, `#b81447` | Primary-button rest and hover fills. Both states retain porcelain-text contrast. | -| `statusDanger`, `statusDangerStrong` | `#ff6b73`, `#fa4d56` / `#bd2838`, `#a81f2e` | Error content and destructive emphasis; strong is for higher-priority glyphs/actions. | -| `statusDangerMuted` | `rgba(220,40,40,.72)` / `rgba(189,40,56,.72)` | Muted destructive decoration and legacy login error framing. | -| `statusDangerSurface`, `statusDangerSurfaceHover`, `statusDangerBorder` | red at `.30`, `.40`, `.24` / red at `.12`, `.18`, `.28` | Destructive button/alert fill, its hover, and its border. | -| `statusSuccess`, `statusSuccessStrong` | `#66bb6a`, `#188a5d` / `#067047`, `#05603e` | Success text/icons and stronger green actions. | -| `statusSuccessSurface`, `statusSuccessBorder` | green at `.12`, `.24` / green at `.10`, `.28` | Success banner/pill fill and boundary. | -| `statusWarning`, `statusWarningSurface`, `statusWarningSurfaceHover` | `#ffd54f`, yellow at `.10`, `.16` / `#8a570f`, amber at `.14`, `.20` | Warning content, low-emphasis warning fill, and its interactive hover state. | -| `statusAttention` | `#e3ce78` / `#745c00` | Pending or attention-needed state that is neither an error nor a warning action. | -| `statusInfo`, `statusInfoSurface` | `#81d3f9`, sky at `.10` / `#176f87`, teal at `.10` | Informational content and banners. | -| `statusFeature` | `#a99de8` / `#6553aa` | Feature/AI distinction when brand pink or status colors would imply the wrong meaning. | -| `statusAssistant`, `statusAssistantStrong` | `#d14671`, `#892c6c` in both themes | Fixed assistant progress gradient and completion checks across theme changes. | - -### Shadows and glass - -| Token(s) | Dark / light | Purpose and allowed use | -| --------------- | -------------------------- | --------------------------------------------------------------------------------- | -| `shadowSubtle` | black `.08` / ink `.06` | Small local lift, including quiet toolbar separation. | -| `shadowSoft` | black `.16` / ink `.11` | Cards and hover lift. | -| `shadowMedium` | black `.28` / ink `.18` | Dropdowns and small popovers. | -| `shadowStrong` | black `.42` / ink `.27` | Large popovers and dialogs. | -| `shadowOverlay` | black `.58` / ink `.38` | Strongest depth cue for dialogs, banners, legacy overlays, and pinned-grid edges. | -| `glassSurface` | white `.075` / white `.58` | Moving liquid-glass selection lens. | -| `glassBorder` | white `.14` / ink `.14` | Main glass lens edge. | -| `glassEdge` | white `.22` / ink `.22` | Refractive/highlight edge; use sparingly inside the glass implementation. | - -### Brand gradients - -The gradients are reserved for QuestDB identity and AI affordances. Ordinary -buttons and surfaces use solid semantic roles. The AI sparkle has an independent -semantic gradient so general QuestDB identity tuning cannot recolor it. The -QuestDB logo mark retains its supplied SVG gradients, while its wordmark inherits -the surrounding semantic content color so it remains readable in both themes. - -| Token(s) | Dark / light | Purpose | -| ---------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------- | -| `brandGradientStart`, `brandGradientEnd` | `#e51a59` → `#8a0f35` / `#b81447` → `#5c0a24` | QuestDB identity gradient for theme-aware product affordances. | -| `aiGradientStart`, `aiGradientEnd` | `#d14671` → `#892c6c` / `#a92352` → `#76184c` | Established AI sparkle gradient, isolated from logo palette changes. | - -## Specialized palettes - -These roles are still semantic, but they belong to a renderer or an isolated -experience. General UI components must not consume them. - -### Result grid - -The grid needs a dedicated dense-data ladder. In dark mode the empty grid field -is darkest, rows are darker, and headers are dark. In light mode the field is -lightest, rows are lighter, and headers are the strongest gray. That preserves -structure without card borders around every cell. - -| Token | Dark / light | Purpose | -| --------------- | --------------------- | ---------------------------------------------------------- | -| `surfaceInset` | `#121317` / `#eceef1` | Empty grid field and viewport. | -| `gridRow` | `#17181d` / `#e9ebef` | Standard data row. | -| `gridHeader` | `#202126` / `#dce0e6` | Column header background. | -| `contentObject` | `#81d3f9` / `#b81447` | Column-name identity; sky in dark, brand crimson in light. | -| `gridSelection` | `#2b1d25` / `#e0afbf` | Selected cell/row accent. | -| `gridFocus` | `#b81447` / `#8a0f35` | Focus boundary and outward copy pulse for the active cell. | - -### Monaco editor - -Monaco is themed through its API, not through application CSS. Its 18 -editor-specific roles remain isolated; ordinary surface, content, interaction, -and status roles are reused where they carry the same meaning. - -| Token(s) | Dark / light | Purpose | -| --------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| `editorCanvas`, `surfaceOverlay`, `editorBorder` | `#17181d`, `#1d1e24`, `#121317` / `#d9dce2`, `#f2f3f5`, `#c9cdd4` | Editor field, widgets/menus, and editor-specific edge. | -| `contentPrimary` | `#f8f8f2` / `#1c2029` | Default code text. | -| `interactionHover`, `interactionNeutral`, `controlSurfaceHover` | white `.055`, `#32343e`, `#32343e` / ink `.075`, `#d9dce2`, `#e7e9ed` | Menu selection, separators, and list-filter states. | -| `editorSelection`, `editorSelectionAccent` | `#44475a`, `#44475a` / `#aec8ec`, `#7a99c4` | Monaco and native form-control selection, plus its stronger selection boundary. | -| `editorActiveLine`, `editorActiveLineBorder` | white `.03`, transparent / ink `.035`, transparent | Active-line wash and border. The transparent border prevents layout/color artifacts. | -| `editorSuggestionMatchActive` | `#ff9abb` / `#8a0f35` | Active autocomplete match. | -| `editorRun`, `statusDanger` | `#ffffff`, `#ff6b73` / `#067047`, `#bd2838` | Monaco run glyph and error decoration. The run glyph intentionally changes contrast by theme. | -| `contentMuted` | `#858995` / `#565f6e` | Comments. | -| `editorSyntaxString` | `#f1fa8c` / `#695f00` | Strings. | -| `editorSyntaxNumber` | `#50fa7b` / `#056c45` | Numbers. | -| `editorSyntaxKeyword` | `#ff79c6` / `#b81447` | SQL keywords. | -| `editorSyntaxType` | `#8be9fd` / `#0a6a80` | Types and type-like identifiers. | -| `editorSyntaxConstant` | `#bd93f9` / `#6553aa` | Constants. | -| `editorSyntaxVariable` | `#ffb86c` / `#8e500a` | Variables. | -| `editorErrorHighlight`, `editorSuccessHighlight` | red/green `.15` / red/green `.13` | Full-line execution feedback. | -| `editorSearchHighlight`, `editorAiHighlight` | orange/yellow `.50` / amber `.32`/`.28` | Search and AI-generated-range highlights. | - -### Data visualization - -Series keep the same conceptual hue identity across themes; the light theme uses -darker lightness values to maintain contrast against a light plot while the dark -theme uses lighter values against a dark plot. The slot is the data -identity—components must not pick a series by color name. - -| Token | Dark / light | Purpose | -| ------------------------------ | ------------------------------------------- | ----------------------------------------------------------------------- | -| `dataGrid` | gray `.11` / ink `.16` | Chart axes and grid lines. | -| `dataArea` | white `.012` / ink `.018` | Quiet plot-area separation. | -| `dataSeries1` | `#ff6b6b` / `#d51515` | Metric widget series slot 1, red. `dataSeries*` are user-pickable metric colors; auto-assigned chart series use `chartSeries*`. | -| `dataSeries2` | `#4ecdc4` / `#0c7966` | Series slot 2, teal. | -| `dataSeries3` | `#ffd93d` / `#80690d` | Series slot 3, yellow/olive. | -| `dataSeries4` | `#95d86e` / `#357b0c` | Series slot 4, green. | -| `dataSeries5` | `#ff8f40` / `#ac5111` | Series slot 5, orange. | -| `dataSeries6` | `#bd93f9` / `#6415d5` | Series slot 6, violet. | -| `dataSeries7` | `#50fa7b` / `#0c7b37` | Series slot 7, bright green. | -| `dataSeries8` | `#ff79c6` / `#cc147e` | Series slot 8, magenta. | -| `dataSeries9` | `#8be9fd` / `#1072a5` | Series slot 9, cyan/blue. | -| `dataSeries10` | `#f1fa8c` / `#61740b` | Series slot 10, yellow/olive. | -| `chartSeries1..8` | `#8be9fd #d14671 #ffb86c #bd93f9 #f1fa8c #ff79c6 #50fa7b #ff5555` / `#1590aa #e42560 #c16f18 #7425e4 #728d11 #e425ac #139842 #e42525` | Auto-assigned series order for notebook/result charts and quick-vis. Calm brand hues lead; assignment is positional, not user-picked. | -| `dataPositive`, `dataNegative` | `#2ca875`, `#d94d58` / `#05603e`, `#a81f2e` | Directional financial/operational data, not generic form success/error. | - -### Authentication - -Authentication is a visually isolated first impression and keeps a deliberate -dark-crimson identity. These roles belong to login/version UI. `authBackdrop` is -also used for the dark tool and code blocks inside AI chat messages. - -| Token | Dark / light | Purpose | -| ------------------------------- | ---------------------------------------------- | --------------------------------------------------------- | -| `authBackdrop` | `#1d070e` / `#c7cbd2` | Login page field and AI chat tool/code-block background. | -| `authAccent`, `authAccentMuted` | `#9089fc`, violet `.64` / `#7d2948`, ink `.46` | Login illustration/action accent and its muted companion. | -| `surfaceCanvas`, `authBorder` | `#141725`, `#353946` / `#d9dce2`, `#a8b0bd` | Login form card and prominent edge. | -| `authVersionContent` | `#f5f3f0` / `#e7e3e5` | Version badge readable content. | - -### Onboarding - -The notebook and MCP onboarding experience follows the active luminance mode. -Dark mode retains the original terminal presentation; light mode reuses values -from the shared light surface, content, feature, and information roles. Modal -borders, subtle fills, and shadows use the general semantic tokens directly. - -| Token | Dark / light | Purpose | -| ------------------- | --------------------------------------- | -------------------------------------- | -| `onboardingSurface` | `#050505` / `surfaceBase` equivalent | Experience backdrop. | -| `onboardingDivider` | white `.04` / `borderSubtle` equivalent | Quiet internal rule. | -| `onboardingClose` | `#858585` / `contentMuted` equivalent | Close/secondary content. | -| `onboardingCommand` | `#111111` / `surfaceValue` equivalent | Command and transcript frame surfaces. | -| `onboardingInput` | `#202020` / `surfaceInset` equivalent | Simulated input. | -| `onboardingPrompt` | `#93adff` / `statusFeature` equivalent | Prompt identity. | -| `onboardingAccent` | `#29c6be` / `statusInfo` equivalent | Agent/progress accent. | - -### Fixed-purpose colors - -These values encode external or user-selected identity, so replacing them with -generic status roles would change meaning. - -| Token(s) | Value | Purpose | -| -------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| `instancePreset1`, `instancePreset2`, `instancePreset3` | `rgb(199,7,45)`, `rgb(0,170,59)`, `rgb(0,122,255)` | Stable user-selectable instance identities. | -| `windowControlClose`, `windowControlMinimize`, `windowControlExpand` | `#ff5f57`, `#febc2e`, `#28c840` | Familiar macOS-style window controls in illustrative UI. | -| `pickerHue0` … `pickerHue5` | red, yellow, green, cyan, blue, magenta endpoints | Mechanical hue stops for the color picker. Indexed names are intentional because the token is a position, not a UI meaning. | - -## Choosing a surface - -| Relationship | Recommended combination | -| ----------------------------------- | -------------------------------------------------------------------- | -| Application shell → work area | `surfaceCanvas` → `surfaceStage` | -| Chrome/sidebar → panel content | `surfaceBase` → `surfaceRaised` or `surfaceInset` | -| Stage → notebook/metric card | `surfaceStage` → `surfaceRaised` | -| Card header → embedded editor/chart | `surfaceRaised` → `surfaceInset` or the renderer-specific canvas | -| Page → input | parent surface → `surfaceInput` + `borderDefault` | -| Page → menu/dropdown | parent surface → `surfaceOverlay` + `borderDefault` + `shadowMedium` | -| Dialog → read-only value | dialog surface → `surfaceValue` | -| Result-grid viewport → row → header | `surfaceInset` → `gridRow` → `gridHeader` | - -Do not choose a token by comparing its hex value to a mockup. Choose the role -from this table, then tune the role in both themes if the relationship is wrong -everywhere it appears. - -## Runtime architecture - -- Styled components read `theme.color` directly. The `color()` helper resolves - the same typed object. -- Non-React renderers use `getThemeColor()` from `src/theme/runtime.ts`; the - active object is synchronized by `ThemeModeProvider`. -- Legacy SCSS uses `theme-color(name)`. Sass emits `var(--qdb-color-name)`, and - `ThemeCssVariables` declares every property from this same palette. This lets - the compiled `main.scss` bundle follow a runtime theme change through one - generated declaration block. -- SVG templates use semantic placeholders that are resolved before rendering. -- Monaco receives a complete theme through its public theming API. Any custom - properties inside Monaco itself are third-party implementation details, not - QuestDB palette inputs. -- `withAlpha()` derives one-off translucent renderer colors from a semantic - role. It handles hex and RGB inputs and avoids a separate token for every - renderer-specific opacity. - -### User preference and System mode - -The saved preference and the rendered theme are deliberately separate: - -- `ThemePreference` is `system`, `light`, or `dark` and is stored under - `appearance.themePreference`. -- `ThemeMode` is always the resolved `light` or `dark` value consumed by styled - components, Monaco, charts, legacy styles, and runtime renderers. -- The provider tracks the operating-system mode independently so switching back - to System always resolves to the current device preference. -- `system` resolves through `prefers-color-scheme`. A media-query change updates - the rendered theme immediately, so operating-system automatic schedules are - followed without reloading the console. -- Explicit `light` and `dark` preferences ignore operating-system changes. -- Preference changes synchronize across same-origin tabs through the browser - `storage` event. - -The root HTML entry point resolves the saved preference before the application -bundle executes and sets `data-theme` plus the native `color-scheme`. The React -provider uses the same resolution rules, so the first React render does not use -the opposite theme. - -## Contribution rules - -1. Never add a literal UI color outside `src/theme/index.ts`. Immutable supplied - brand artwork such as `QuestDBLogo` is the sole exception and must not be - recolored through theme roles. -2. Never add a first-party CSS custom property for theming. -3. Use a semantic role, not a hue name and not a component name. -4. Keep every runtime token directly under `theme.color`; organize source groups - with comments. A genuinely theme-invariant role belongs in `invariantColors`, - which is spread independently into both flat palettes. -5. Add a token only when an existing role would communicate the wrong meaning or - when a renderer needs stable independent control. -6. Define the dark and light value together and inspect their neighboring - surfaces in both modes. -7. Preserve state ordering: rest, hover, active/selected, focus, disabled. -8. Prefer `withAlpha()` for a renderer-specific wash. Promote it to a token only - when the same opacity has a shared semantic meaning across components. -9. Run `yarn lint:colors` and `yarn typecheck` after changes. - -Before approving a new color token, answer all four questions: - -- What meaning does it carry? -- Which existing token is closest, and why is reusing it incorrect? -- Which surfaces or components may consume it? -- What is its corresponding dark/light behavior? - -If those answers are not clear, the color is probably a local styling tweak, not -a design-system token. diff --git a/e2e/questdb b/e2e/questdb index 00de5bbb3..f2e1a8de1 160000 --- a/e2e/questdb +++ b/e2e/questdb @@ -1 +1 @@ -Subproject commit 00de5bbb3aed7e23794e6d7ff0cd4089a8e1423b +Subproject commit f2e1a8de1ab1201f57ca144b71c36ea0f1358736 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/AIStatusIndicator/AssistantModesCompact.tsx b/src/components/AIStatusIndicator/AssistantModesCompact.tsx index c534c4557..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("contentAccent")}; + 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 60d13d17e..754985388 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 @@ -63,31 +68,57 @@ 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] } +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: 0.4rem; + 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"}; + 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.12) + : chipBackground($variant, theme)}; + font-size: ${({ $shape, theme }) => + $shape === "pill" ? theme.fontSize.xs : "1.1rem"}; + font-weight: 400; + letter-spacing: ${({ $shape }) => ($shape === "pill" ? "0.1px" : "normal")}; line-height: 1; white-space: nowrap; @@ -105,7 +136,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 +144,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.2rem" : "1.6rem")}; + height: ${({ $shape }) => ($shape === "pill" ? "1.2rem" : "1.6rem")}; } ` @@ -124,6 +155,7 @@ export const Badge = React.forwardRef( type, variant, size = "md", + shape = "chip", icon, pulsate, children, @@ -139,10 +171,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/index.tsx b/src/components/Button/index.tsx index 6ac332a95..c483a7d86 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,7 @@ export const ButtonBase = styled.button.attrs<{ filter 120ms ease; &&:focus-visible { - outline: 1px solid ${({ theme }) => theme.color.contentAccent}; + outline: 1px solid ${({ theme }) => statusInfoFocus(theme.color.statusInfo)}; outline-offset: 2px; } @@ -194,7 +195,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 81a704ea9..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", @@ -197,8 +197,14 @@ export const makeButtonVariant = (variant: ButtonVariant) => { } &&:hover:not(:disabled):not([aria-disabled="true"]) { - background: ${getColor(theme.hover.background)}; - color: ${getColor(theme.hover.color)}; + background: ${({ theme: t }) => + variant === "ghost" && t.mode === "light" + ? t.color.interactionHover + : t.color[theme.hover.background]}; + 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/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..744053a00 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -3,17 +3,20 @@ 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 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}; } ` @@ -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/Key/index.tsx b/src/components/Key/index.tsx index 277ad7aa3..6c09aff9e 100644 --- a/src/components/Key/index.tsx +++ b/src/components/Key/index.tsx @@ -1,17 +1,27 @@ import React from "react" -import styled from "styled-components" +import styled, { css } from "styled-components" import { ArrowBendDownLeftIcon } from "@phosphor-icons/react" import { Box } from "../Box" import type { ThemeShape } from "../../types" import { shortcutKeycapStyles } from "./styles" type ColorFunction = (props?: { theme: ThemeShape }) => string | undefined +type KeySize = "sm" | "md" + +const ENTER_ICON_SIZES: Record = { sm: 11, md: 12 } + +const compactKeycapStyles = css` + height: 2rem; + min-width: 2.2rem; + padding: 0.15rem 0.5rem; +` const StyledKey = styled(Box).attrs({ alignItems: "center", justifyContent: "center", -})<{ $color?: string | ColorFunction }>` +})<{ $color?: string | ColorFunction; $size: KeySize }>` ${shortcutKeycapStyles} + ${({ $size }) => $size === "sm" && compactKeycapStyles} color: ${({ $color, theme }) => { if (typeof $color === "function") { @@ -26,15 +36,20 @@ const StyledKey = styled(Box).attrs({ type Props = { keyString: string color?: string | ColorFunction + size?: KeySize } -export const Key = ({ keyString, color: keyColor }: Props) => { +export const Key = ({ keyString, color: keyColor, size = "md" }: Props) => { const isEnter = keyString.toLowerCase() === "enter" return ( - + {isEnter ? ( - + ) : ( keyString )} diff --git a/src/components/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/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 68b4e89cf..4aac5c9cb 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, @@ -80,12 +81,13 @@ 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; min-width: 0; font-size: ${HEADER_NAME_FONT_SIZE_PX}px; + font-weight: ${HEADER_NAME_FONT_WEIGHT}; ` export const HeaderType = styled.span` @@ -138,7 +140,7 @@ export const ColResizer = styled.div` } &:hover::after { - background: ${color("contentAccent")}; + background: ${color("borderStrong")}; } ` @@ -157,7 +159,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; @@ -171,7 +173,12 @@ export const Row = styled.div<{ $active: boolean }>` ${({ $active, theme }) => $active && css` - background: ${theme.color.gridSelection}; + background: + linear-gradient( + ${theme.color.interactionSelected}, + ${theme.color.interactionSelected} + ), + ${theme.color.gridRow}; `} ${({ $active, theme }) => @@ -180,16 +187,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}; } @@ -197,8 +204,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; } ` @@ -234,22 +241,35 @@ export const Cell = styled.div<{ ${({ $frozen, $rowActive, theme }) => $frozen && css` - background: ${$rowActive ? theme.color.gridSelection : color("gridRow")}; + background: ${$rowActive + ? `linear-gradient(${theme.color.interactionSelected}, ${theme.color.interactionSelected}), ${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( + ${theme.color.interactionSelected}, + ${theme.color.interactionSelected} + ), + ${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 - ease-out; + animation: ${pulseAnim(theme.color.statusInfo, theme.color.transparent)} + 1s ease-out; `} ` @@ -350,7 +370,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..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" @@ -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; @@ -386,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/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/SelectMenu/index.tsx b/src/components/SelectMenu/index.tsx index a31d67003..25f964e32 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} { @@ -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 -