Appearance
Theming System Design
Date: 2026-03-17 Branch: test/modern-styling-tweaks (dark theme experiment) → new branch for implementation Scope: Replace the dual color system with a unified CSS custom property architecture supporting light and dark mode, consolidate ~58 style files into ~14, and modernize visual polish.
1. Business Case
Why This Matters
The current styling architecture has accumulated significant technical debt that directly impacts development velocity, visual consistency, and our ability to ship a polished product.
The problem in numbers:
- 2 competing color systems —
_colors.scss(38 semantic variables like$background-panel) and_newcolors.scss(grey scale + 5 color families like$grey-20). Neither is used consistently. - ~95 Vue components contain hardcoded hex color values or references to old color tokens that compile to static values.
- 47 global SCSS files in
src/styles/+ 11 Reports-specific SCSS files insrc/pages/Reports/styles/with overlapping responsibilities (e.g., 6 files just for the data table). - 40+ files contain
!importantoverrides to fight Vuetify's built-intheme--lightdefaults. - No runtime theme switching — SCSS compiles to static CSS. Dark mode required manually changing variable values and hoping nothing was hardcoded.
What we're fighting against:
Vuetify 2.3's theme system is minimal — it provides a light/dark toggle and a few color slots (primary, secondary, error). Our app doesn't use it. Instead, every component that uses a Vuetify element (v-card, v-sheet, v-list, v-data-table, v-menu, v-dialog) must individually override Vuetify's white defaults. This leads to code like:
scss
// This pattern is repeated in 40+ files
.v-card { background: $grey-20 !important; }
.v-sheet { background: $grey-20 !important; }
.v-list { background: $grey-20 !important; }Every new component or page requires the developer to remember which Vuetify defaults need overriding, which color system to use, and where to put the styles. This is error-prone and slow.
What this unlocks:
- Light + dark mode for end users, switchable at runtime
- One place to change colors — adjust a primitive token, the entire app updates
- Faster feature development — new components inherit correct colors automatically
- Future extensibility — the architecture supports custom color themes without restructuring
- Vuetify independence — the theme system works regardless of UI framework, easing future migration
2. Architecture
2.1 Three-Layer Token System
┌─────────────────────────────────────────────┐
│ Layer 3: Component Tokens (optional) │
│ --table-row-alt, --input-bg │
│ Only when a component needs an exception │
├─────────────────────────────────────────────┤
│ Layer 2: Semantic Tokens │
│ --surface-raised, --text-primary │
│ What the color MEANS — changes per theme │
├─────────────────────────────────────────────┤
│ Layer 1: Primitive Tokens │
│ --primitive-grey-100, --primitive-blue-500 │
│ The actual OKLCH color values │
└─────────────────────────────────────────────┘Rule: Components only use semantic tokens (Layer 2) or component tokens (Layer 3). No component ever references a primitive token or a raw color value.
2.2 File Structure for Tokens
Critical architecture decision: CSS custom properties (:root { --var: value }) are runtime CSS, not SCSS compile-time variables. They must NOT be injected via Vite's additionalData — that would duplicate the entire :root block into every component's compiled CSS (~389 times).
Solution — two files:
_theme-helpers.scss— SCSS-only file with helper mixins/functions for working with tokens. Injected viaadditionalDataso all components have access. Contains no:rootblocks, only SCSS constructs.theme.css(no underscore, plain CSS) — The:rootand[data-theme]blocks defining all CSS custom properties. Imported once inmain.ts. This is the single source of truth for all color values.
2.3 Primitive Tokens (OKLCH)
OKLCH (Oklab Lightness-Chroma-Hue) provides perceptually uniform color scaling. A lightness of 50% looks like 50% regardless of hue. This makes palette generation predictable and consistent.
Every OKLCH value includes a hex fallback for maximum browser compatibility:
css
:root {
/* Grey scale — neutral with subtle blue undertone */
/* Each value has a hex fallback above the OKLCH definition */
--primitive-grey-50: #1f2024;
--primitive-grey-50: oklch(15% 0.01 260);
--primitive-grey-100: #333338;
--primitive-grey-100: oklch(22% 0.01 260);
--primitive-grey-150: #3e3e44;
--primitive-grey-150: oklch(26% 0.01 260);
--primitive-grey-200: #494950;
--primitive-grey-200: oklch(30% 0.01 260);
--primitive-grey-250: #54545c;
--primitive-grey-250: oklch(34% 0.01 260);
--primitive-grey-300: #606068;
--primitive-grey-300: oklch(38% 0.01 260);
--primitive-grey-400: #73737c;
--primitive-grey-400: oklch(46% 0.01 260);
--primitive-grey-500: #878790;
--primitive-grey-500: oklch(54% 0.01 260);
--primitive-grey-600: #9c9ca6;
--primitive-grey-600: oklch(62% 0.01 260);
--primitive-grey-700: #b5b5bf;
--primitive-grey-700: oklch(72% 0.01 260);
--primitive-grey-800: #cfcfd8;
--primitive-grey-800: oklch(82% 0.01 260);
--primitive-grey-850: #dddde6;
--primitive-grey-850: oklch(87% 0.01 260);
--primitive-grey-900: #ebebf2;
--primitive-grey-900: oklch(92% 0.01 260);
--primitive-grey-950: #f5f5fa;
--primitive-grey-950: oklch(96% 0.01 260);
--primitive-white: #ffffff;
--primitive-white: oklch(100% 0 0);
/* Brand colors */
--primitive-blue-500: oklch(58% 0.18 240); /* primary */
--primitive-red-500: oklch(58% 0.20 18); /* danger */
--primitive-green-500: oklch(62% 0.17 155); /* success */
--primitive-yellow-500: oklch(82% 0.14 85); /* warning */
/* Status colors (table indicators, categorical) */
--primitive-status-blue: #0693c7;
--primitive-status-green: #09d270;
--primitive-status-purple: #7b76f5;
--primitive-status-yellow: #fcc11f;
--primitive-status-red: #f50023;
--primitive-status-pink: #ea5391;
}The hex fallback pattern (property: hex; property: oklch(...)) is standard progressive enhancement — browsers that don't support OKLCH use the hex value, modern browsers override with OKLCH.
The exact values will be tuned visually during implementation. The important thing is the structure — one place, easy to adjust.
2.4 Semantic Tokens
These map meaning to primitives. Switching theme = changing which primitives the semantic tokens point to.
css
/* Dark theme (default) */
[data-theme="dark"] {
/* Surfaces */
--surface-base: var(--primitive-grey-100); /* page background */
--surface-raised: var(--primitive-grey-150); /* cards, panels */
--surface-overlay: var(--primitive-grey-50); /* topbar, dropdowns, modals */
--surface-sunken: var(--primitive-grey-50); /* inputs, textareas */
--surface-hover: var(--primitive-grey-250); /* hover states */
--surface-active: var(--primitive-grey-300); /* active/pressed */
/* Text */
--text-primary: var(--primitive-grey-850); /* headings, body */
--text-secondary: var(--primitive-grey-600); /* labels, meta */
--text-muted: var(--primitive-grey-400); /* placeholders, disabled */
/* Borders */
--border-default: var(--primitive-grey-250); /* card edges, dividers */
--border-strong: var(--primitive-grey-400); /* input borders */
--border-subtle: var(--primitive-grey-200); /* row separators */
/* Accents */
--accent-primary: var(--primitive-blue-500);
--accent-danger: var(--primitive-red-500);
--accent-success: var(--primitive-green-500);
--accent-warning: var(--primitive-yellow-500);
/* Status (categorical — same in both themes) */
--status-blue: var(--primitive-status-blue);
--status-green: var(--primitive-status-green);
--status-purple: var(--primitive-status-purple);
--status-yellow: var(--primitive-status-yellow);
--status-red: var(--primitive-status-red);
--status-pink: var(--primitive-status-pink);
/* Focus */
--focus-ring: var(--primitive-blue-500);
/* Shadows — more visible on dark backgrounds */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 4px 24px rgba(0, 0, 0, 0.35);
}
/* Light theme */
[data-theme="light"] {
--surface-base: var(--primitive-grey-950);
--surface-raised: var(--primitive-white);
--surface-overlay: var(--primitive-white);
--surface-sunken: var(--primitive-grey-900);
--surface-hover: var(--primitive-grey-900);
--surface-active: var(--primitive-grey-850);
--text-primary: var(--primitive-grey-100);
--text-secondary: var(--primitive-grey-500);
--text-muted: var(--primitive-grey-600);
--border-default: var(--primitive-grey-850);
--border-strong: var(--primitive-grey-700);
--border-subtle: var(--primitive-grey-900);
--accent-primary: var(--primitive-blue-500);
--accent-danger: var(--primitive-red-500);
--accent-success: var(--primitive-green-500);
--accent-warning: var(--primitive-yellow-500);
--status-blue: var(--primitive-status-blue);
--status-green: var(--primitive-status-green);
--status-purple: var(--primitive-status-purple);
--status-yellow: var(--primitive-status-yellow);
--status-red: var(--primitive-status-red);
--status-pink: var(--primitive-status-pink);
--focus-ring: var(--primitive-blue-500);
/* Shadows — subtler on light backgrounds */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.1);
}Note: Shadow tokens use rgba() for the shadow color component, not OKLCH. This is intentional — shadows are always black with opacity, and rgba() is universally supported. The OKLCH choice applies to visible color tokens only.
2.5 Theme Switching
typescript
// Runtime theme switch — one line
document.documentElement.setAttribute('data-theme', 'dark'); // or 'light'
// Persist preference
localStorage.setItem('cavai-theme', 'dark');
// Respect system preference as default
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;2.6 Vuetify Reset
Important: Do NOT change Vuetify's theme.dark property in main.ts. Leave Vuetify permanently in "light" mode. The _vuetify-reset.scss file neutralizes its defaults, and our data-theme attribute handles actual theme switching independently of Vuetify. Toggling Vuetify's own dark theme would re-introduce a second set of color defaults to fight against.
A single file that neutralizes all Vuetify theme--light defaults so our tokens take over:
scss
// _vuetify-reset.scss (~50 lines)
.theme--light.v-application,
.theme--light.v-sheet,
.theme--light.v-card,
.theme--light.v-list,
.theme--light.v-data-table,
.theme--light.v-menu__content,
.theme--light.v-dialog,
.theme--light.v-expansion-panels,
.theme--light.v-expansion-panel,
.theme--light.v-autocomplete__content {
background-color: var(--surface-raised);
color: var(--text-primary);
}
.theme--light.v-application {
background-color: var(--surface-base);
}
// Input overrides
.theme--light .v-input input,
.theme--light .v-input textarea,
.theme--light .v-select__selection {
color: var(--text-primary);
}
.theme--light .v-label {
color: var(--text-secondary);
}This one file eliminates the need for per-component Vuetify overrides scattered across 40+ files. After this reset, Vuetify components inherit our tokens automatically.
3. File Consolidation
Current: ~58 files
src/styles/ (47 files):
_animations.scss, _colors.scss, _fonts.scss, _newcolors.scss, _shadows.scss,
_variables.scss, ActionMenu.scss, Breadcrumbs.scss, BuilderPanels.scss,
BuilderTop.scss, Buttons.scss, CTooltip.scss, Chip.scss, ContentTop.scss,
DatePicker.scss, Dialog.scss, DropdownsAndSelects.scss, EditProfileForm.scss,
FormShell.scss, GlobalTypography.scss, ImageUploader.scss, Input.scss,
List.scss, LoadingCircle.scss, MainPage.scss, MainPageTiles.scss,
MainTable.scss, MainTableFooter.scss, MainTableHeader.scss,
MainTableNumeric.scss, MainTableSearch.scss, MainTableStatus.scss,
MiddlewareOp.scss, OperatorStyles.scss, OptionRowShared.scss, Preview.scss,
ReportsManagementView.scss, ReportsTable.scss, ScrollBar.scss, SignIn.scss,
Stepper.scss, Toast.scss, TopBar.scss, TreeView.scss, Wizards.scss,
user_profile.scss, vertical_tabbed_layout_variables.scss, vuetify.min.custom.csssrc/pages/Reports/styles/ (11 files):
reports.scss, reports-charts.scss, reports-charts-header.scss,
reports-data-sources.scss, reports-dialogs.scss, reports-heading.scss,
reports-table-container.scss, reports-table-wrapper.scss, reports-tabs.scss,
reports-templates.scss, reports-vuetify-overwrite.scsssrc/pages/Chatbots/components/CavaiFlow/flowactors/invert.scss:
Contains 130+ *-invert and *-invert-alt variables from _newcolors.scss, used for a localized light-mode flow editor. This becomes obsolete — the .invert class is replaced by applying [data-theme="light"] to the flow editor container.
Target: ~14 files
src/styles/
├── theme.css # CSS custom properties — primitives, semantic tokens,
│ # shadows, motion tokens. Imported ONCE in main.ts.
├── _theme-helpers.scss # SCSS helpers/mixins for token access. In additionalData.
├── _variables.scss # Sizing, spacing, border-radius (kept, absorbs
│ # vertical_tabbed_layout_variables sizing vars)
├── _fonts.scss # Font faces (kept)
├── _animations.scss # Keyframes + animation definitions (modernized)
├── _vuetify-reset.scss # Neutralize Vuetify theme--light defaults
├── _components-navigation.scss # TopBar, Breadcrumbs, ContentTop
├── _components-tables.scss # MainTable, header, footer, numeric, search, status
├── _components-forms.scss # Input, DropdownsAndSelects, DatePicker, ImageUploader
├── _components-dialogs.scss # Dialog, FormShell, Wizards
├── _components-buttons.scss # Buttons
├── _components-builder.scss # BuilderPanels, BuilderTop, OptionRowShared,
│ # OperatorStyles, Preview, MiddlewareOp
├── _components-misc.scss # Toast, Chip, CTooltip, LoadingCircle, ScrollBar,
│ # ActionMenu, List, Stepper, TreeView
├── _pages.scss # MainPage, MainPageTiles, SignIn, EditProfileForm,
│ # user_profile
├── _reports.scss # All 11 Reports SCSS files consolidated (215 color
│ # declarations warrant a dedicated file)
└── vuetify.min.custom.css # Vuetify base CSS (kept, untouched)Deleted entirely:
_colors.scss— replaced bytheme.css_newcolors.scss— replaced bytheme.css(including all*-invert/*-invert-altvariables)_shadows.scss— shadow tokens move intotheme.cssGlobalTypography.scss— base text styles handled by Vuetify reset + tokensvertical_tabbed_layout_variables.scss— sizing into_variables.scss, colors into tokensinvert.scss— replaced by[data-theme="light"]scoping on flow editor- All 34 individual component SCSS files in
src/styles/(replaced by 8 consolidated files) - All 11 individual Reports SCSS files (replaced by
_reports.scss)
Vite additionalData updated to inject:
_theme-helpers.scss(SCSS helpers only — NO:rootblocks)_variables.scss_fonts.scss_animations.scss
Imported once in main.ts:
theme.css(CSS custom properties)_vuetify-reset.scss- All
_components-*.scssand_pages.scssand_reports.scss ScrollBar.scssstyles (previously inadditionalData, moved to entry-point import)
4. Known Issues Catalog
Complete inventory of visual issues discovered during the dark theme experiment. All are resolved by the token migration.
Surfaces (white backgrounds)
| Component | Current Issue | Token Fix |
|---|---|---|
| Vuetify defaults (v-card, v-sheet, v-list, v-data-table) | White from theme--light | _vuetify-reset.scss → --surface-raised |
| Modals (Create Brand, Create Campaign, Bulk Tag Export) | White modal background | --surface-raised on .v-dialog .v-card |
| Dropdown menus | Hardcoded #EFEFEF hover | --surface-hover |
| Color picker popup | White v-card wrapper | --surface-overlay |
| Builder preview panel | White iframe border/frame | --border-default |
| Dashboard cards (MainPageTiles) | Were $global-white | --surface-raised |
| Table container | No dark background set | --surface-raised |
| Data table rows | White default | --surface-raised + zebra via --surface-hover |
| Number input spinners | White native WebKit controls | Global CSS reset (not token-related) |
| Outlined buttons | White background | --surface-raised + --border-default |
| Export tag badges | White/light background | --surface-active + --border-default |
| "+12 more" / "show less" | White text/background | --text-secondary |
| Format tags in table | Blend with background | --surface-active + --text-secondary |
Text (invisible on dark background)
| Component | Current Issue | Token Fix |
|---|---|---|
| Breadcrumbs | $typography-minus4 (#2C3038) — invisible | --text-secondary |
| Page titles (ContentHeader) | Inherited black | --text-primary |
| Table numeric data | No color set — inherited black | --text-secondary |
| Table link/name text | $typography-minus6 (#14171E) | --text-primary |
| Search input text | Hardcoded #252525 | --text-primary |
| Dialog title | $typography-default (medium gray) | --text-primary |
| Dialog subtitle | $typography-minus1 (dark gray) | --text-secondary |
| Select dropdown text | $grey-25 (too dark) | --text-primary |
Borders & Dividers
| Component | Current Issue | Token Fix |
|---|---|---|
| Dashboard card divider | $grey-85 (too light/white) | --border-default |
| Table header border | $typography-plus6 (light gray) | --border-default |
| Table row separators | Light mix() formula | --border-subtle |
| Slider section dividers | $grey-95 (nearly white) | --border-subtle |
| Card section dividers | $grey-95 | --border-subtle |
| Dropdown separator | $grey-90 | --border-subtle |
| Form field borders | Various light values | --border-strong |
Interactive States
| Component | Current Issue | Token Fix |
|---|---|---|
| Dropdown hover | $grey-95 / #EFEFEF | --surface-hover |
| Table row hover | Light mix() formula | --surface-hover |
| Button active | Various light values | --surface-active |
| Close button (dialogs) | White background | transparent + --text-secondary icon |
| Focus outline (builder blocks) | White-ish outline | --focus-ring (accent-primary) |
| Tooltip backgrounds | White | --surface-overlay |
| Dropdown caret | $global-white background | --surface-overlay |
| Sort arrow (table header) | $global-white | --text-secondary |
Components Not Yet Verified
These were not encountered during the dark theme experiment but likely have similar issues:
- Reports dashboard charts and tables (11 SCSS files, 215 color declarations)
- Stepper component
- Sign-in page
- User profile/edit profile
- Template management views
- Image uploader states
- Date picker popup
- App.vue style block (references
$background-body,$primary-font-color)
5. Animation & Polish Modernization
As part of the consolidation, update transitions and animations to feel more modern and consistent.
Current State
_animations.scssdefines some keyframes_variables.scsshas transition tokens:$transition-quick(0.15s),$transition-long(0.4s),$transition-medium(0.25s)- Many components define their own ad-hoc transitions (e.g.,
transition: background-color 500ms cubic-bezier(0, 0.5, 0, 1))
New Approach
Standardize on a small set of motion tokens using modern easing. These are defined in theme.css alongside color tokens (single import, no duplication):
css
:root {
/* Duration */
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 350ms;
/* Easing — snappy, modern feel */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* quick deceleration */
--ease-in-out: cubic-bezier(0.45, 0, 0.55, 1); /* balanced */
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* slight overshoot */
/* Composed transitions */
--transition-colors: var(--duration-fast) var(--ease-out);
--transition-transform: var(--duration-normal) var(--ease-spring);
--transition-layout: var(--duration-slow) var(--ease-in-out);
}Usage pattern:
css
.card {
transition: background-color var(--transition-colors),
box-shadow var(--transition-colors);
}Polish Targets
- Hover transitions — consistent
--duration-fasteverywhere instead of scattered 70ms–1000ms values - Card hover — subtle
translateY(-1px)+ shadow lift instead of the current size-change approach - Border radius — already standardized to
$border-radius-milli(6px),$border-radius-base(10px),$border-radius-kilo(14px). Keep as-is in_variables.scss. - Shadows — theme-aware via
--shadow-sm/md/lg, stronger on dark to compensate for low-contrast backgrounds - Table rows — smooth color transitions on hover
- Buttons — consistent press feedback
6. Migration Strategy
Approach: Single PR
Build the entire new system and migrate all files in one branch. No intermediate state to maintain.
Execution Order
Phase 1: Foundation
- Create
theme.csswith all primitive and semantic tokens (including hex fallbacks) - Create
_theme-helpers.scsswith any SCSS helper mixins - Create
_vuetify-reset.scss - Update
vite.config.ts— changeadditionalDatato inject_theme-helpers.scssinstead of_colors.scssand_newcolors.scss - Update
main.ts— importtheme.cssand_vuetify-reset.scss - Update
index.htmlbody background to use system preference - Update
App.vuestyle block — replace$background-bodyand.theme--lightoverrides with tokens
Phase 2: Global Style Migration 8. Migrate all 47 src/styles/*.scss files:
- Replace all
$grey-XX,$background-*,$border-*,$typography-*,$illustration-*,$global-white,$white-color,$black-colorwith semantic tokens - Remove all
!importantoverrides that exist solely to fight Vuetify - Consolidate into the target files
- Migrate all 11
src/pages/Reports/styles/*.scssfiles into_reports.scss - Delete
_colors.scss,_newcolors.scss,_shadows.scss,invert.scss
Phase 3: Component Migration 11. Migrate all Vue components with <style> blocks referencing old SCSS variables or hardcoded hex values 12. Special attention to: - App.vue — root styles - Builder components (largest surface area) - Reports components (charts, tables, 215 color declarations) - Dialog/modal components - Flow editor components (replace .invert class with [data-theme="light"] scoping)
Phase 4: Theme Switching 13. Add theme toggle mechanism (localStorage + system preference detection) 14. Add theme toggle UI (location TBD — likely user menu in TopBar)
Phase 5: Verification & Polish 15. Visual QA of every page/view in both themes 16. Animation/transition modernization pass 17. Final cleanup of any remaining hardcoded values
Graduated Color Scales
The current _colors.scss defines graduated scales like $primary-plus1 through $primary-plus7 and $primary-minus1 through $primary-minus7 (used in 28 files, 61 occurrences). Decision for migration:
- Accent colors that only appear at full strength (buttons, links, status indicators) → use
--accent-primarydirectly - Lighter/darker accent variants (hover states, subtle backgrounds) → use
opacitymodifiers or OKLCH lightness adjustments in component tokens where needed, rather than pre-defining 14 variants per color - Status indicator colors → preserved as-is via
--status-*tokens (they're categorical, not scaled)
Risk Mitigation
- Large PR risk: Mitigated by systematic, file-by-file approach. Each file migration is mechanical (replace old token → new token). The creative decisions are in the token definitions, not in the migration.
- Merge conflicts: Minimize by communicating with team about the branch, and merging promptly once complete.
- Visual regressions: Verify each page in both light and dark mode before marking complete.
- OKLCH browser support: Supported in all modern browsers (Chrome 111+, Firefox 113+, Safari 15.4+). Hex fallbacks included for every primitive token using the standard progressive enhancement pattern (
property: hex; property: oklch(...)). Opera Mini and older iOS Safari get hex values.
7. Variable Migration Reference
Quick-reference mapping for the mechanical migration. Context determines which semantic token to use.
Backgrounds → Surface tokens
| Old Variable(s) | New Token | Usage |
|---|---|---|
$background-body, $grey-15 | var(--surface-base) | Page background |
$background-content, $background-panel, $grey-20 | var(--surface-raised) | Cards, panels, tables |
$background-header, $grey-10 | var(--surface-overlay) | TopBar, dropdowns, modals |
$background-light, $grey-25 (as input bg) | var(--surface-sunken) | Inputs, textareas |
$global-white, $white-color, #FFFFFF | Context-dependent | --surface-raised or --surface-overlay |
Text → Text tokens
| Old Variable(s) | New Token | Usage |
|---|---|---|
$typography-minus6, $typography-minus7, $grey-80 (as text) | var(--text-primary) | Headings, body text |
$typography-default, $grey-60 (as text) | var(--text-secondary) | Labels, captions |
$grey-50 (as placeholder), $global-disabled | var(--text-muted) | Placeholders, disabled |
Borders → Border tokens
| Old Variable(s) | New Token | Usage |
|---|---|---|
$border-default, $grey-30, $grey-35 | var(--border-default) | Card edges, dividers |
$text-input-bottom-border, $grey-40, $grey-45 | var(--border-strong) | Input borders |
$grey-85, $grey-90, $grey-95 (as separator) | var(--border-subtle) | Row separators, sections |
Interactive → Surface/accent tokens
| Old Variable(s) | New Token | Usage |
|---|---|---|
$grey-25 (as hover bg) | var(--surface-hover) | Row/item hover |
$grey-30 (as active bg) | var(--surface-active) | Active/pressed |
$primary-default | var(--accent-primary) | Primary buttons, links |
$error-color, $secondary-default | var(--accent-danger) | Error states |
$tertiary-default | var(--accent-success) | Success states |
$warning-color | var(--accent-warning) | Warnings |
8. Files Changed
New Files
src/styles/theme.css— CSS custom properties (primitives + semantic + shadows + motion)src/styles/_theme-helpers.scss— SCSS helpers for token accesssrc/styles/_vuetify-reset.scss— Vuetify neutralization
Consolidated Files (new, replacing many)
src/styles/_components-navigation.scsssrc/styles/_components-tables.scsssrc/styles/_components-forms.scsssrc/styles/_components-dialogs.scsssrc/styles/_components-buttons.scsssrc/styles/_components-builder.scsssrc/styles/_components-misc.scsssrc/styles/_pages.scsssrc/styles/_reports.scss
Modified Files
src/styles/_variables.scss— absorb sizing vars from vertical_tabbed_layout_variablessrc/styles/_animations.scss— modernize, remove ad-hoc transition definitionsvite.config.ts— updateadditionalDataimportssrc/main.ts— add theme.css import, theme initializationsrc/App.vue— migrate style block to tokensindex.html— system preference background color- ~95 Vue component files — replace hardcoded colors with tokens
Deleted Files
src/styles/_colors.scsssrc/styles/_newcolors.scsssrc/styles/_shadows.scsssrc/styles/GlobalTypography.scsssrc/styles/vertical_tabbed_layout_variables.scsssrc/pages/Chatbots/components/CavaiFlow/flowactors/invert.scss- All 34 individual component SCSS files in
src/styles/(replaced by 9 consolidated files) - All 11 Reports SCSS files in
src/pages/Reports/styles/(replaced by_reports.scss)
9. Success Criteria
- Zero hardcoded color values in any SCSS or Vue component
<style>block - Zero
!importantoverrides for color/background properties (except where Vuetify specificity genuinely requires it) - Light and dark mode both visually correct across all pages
- Runtime theme switching works via
data-themeattribute - ~14 style files instead of ~58
- One color system instead of two
- Build passes with no SCSS compilation errors
- All existing functionality preserved — no behavioral changes
- Hex fallbacks for all OKLCH values
10. Out of Scope
- Custom user themes / brand colors — architecture supports it, but no UI for it in this PR
- Vuetify migration (to v3 or removal) — separate project
- Vuetify
theme.darktoggle — Vuetify stays in light mode permanently; our system handles theming - Component library extraction — separate project
- Responsive design changes — not part of this work
- New features — this is purely a styling architecture change