From c0197ec60940b356416237bf01d77709e7b6e4a8 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 17 Oct 2026 10:36:58 -0400 Subject: [PATCH 1/9] Theme step 0: guardrails in report mode Add scripts/check-theme-tokens.mjs (yarn theme:check, wired into CI after stylelint). It checks that the dark and light theme blocks define the same names in the same order, that every var(--x) resolves, that every defined custom property is read, and that [data-theme only appears in the tokens file. Today's offenders are listed in KNOWN_OFFENDERS and print as warnings until steps 1, 2 and 5 remove them. Add useTheme unit tests and a parity test that evaluates the inline FOUC script from index.html against the same storage and media cases. Extend the visual theme sweep with nine seeded routes on CS101-2024 from seed_e2e_data: student course detail and three problem-set solve pages (MCQ, probeable, refute), instructor overview, submissions and problem sets, admin students and problem sets. The instructor students page is left out: it lists every accumulated test student and the full-page screenshot times out. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 + .secrets.baseline | 8 +- e2e/visual-theme-audit.spec.ts | 27 +- purplex/client/package.json | 3 +- purplex/client/scripts/check-theme-tokens.mjs | 280 ++++++++++++++++++ .../composables/__tests__/useTheme.test.ts | 223 ++++++++++++++ 6 files changed, 528 insertions(+), 17 deletions(-) create mode 100644 purplex/client/scripts/check-theme-tokens.mjs create mode 100644 purplex/client/src/composables/__tests__/useTheme.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc56d328..63b34d96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,10 @@ jobs: yarn lint:check yarn stylelint:check + - name: Check theme tokens + working-directory: purplex/client + run: yarn theme:check + - name: Run TypeScript type checking working-directory: purplex/client continue-on-error: true diff --git a/.secrets.baseline b/.secrets.baseline index 438c1848..ea6d43a3 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -158,21 +158,21 @@ "filename": ".github/workflows/ci.yml", "hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd", "is_verified": false, - "line_number": 107 + "line_number": 111 }, { "type": "Basic Auth Credentials", "filename": ".github/workflows/ci.yml", "hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd", "is_verified": false, - "line_number": 151 + "line_number": 155 }, { "type": "Secret Keyword", "filename": ".github/workflows/ci.yml", "hashed_secret": "dab4fb93ec0b6fab34ac303369fa5d25cd186a49", "is_verified": false, - "line_number": 153 + "line_number": 157 } ], ".github/workflows/postgres-load-tests.yml": [ @@ -360,5 +360,5 @@ } ] }, - "generated_at": "2026-10-16T22:08:50Z" + "generated_at": "2026-10-17T14:36:50Z" } diff --git a/e2e/visual-theme-audit.spec.ts b/e2e/visual-theme-audit.spec.ts index 72e48308..f37dc055 100644 --- a/e2e/visual-theme-audit.spec.ts +++ b/e2e/visual-theme-audit.spec.ts @@ -15,6 +15,10 @@ interface RouteEntry { auth: AuthRole; } +// Seeded routes read the fixtures from `python manage.py seed_e2e_data` +// (course CS101-2024 and the e2e-* problem sets), which the E2E stack loads. +const SEEDED_COURSE = 'CS101-2024'; + const ROUTES: Record = { // Public (no auth required) 'login': { path: '/', auth: null }, @@ -41,19 +45,18 @@ const ROUTES: Record = { 'admin-submissions': { path: '/admin/submissions', auth: 'admin' }, 'admin-courses': { path: '/admin/courses', auth: 'admin' }, 'admin-course-new': { path: '/admin/courses/new', auth: 'admin' }, -}; -// Data-dependent routes — uncomment after seeding the dev database. -// These require the backend to be running with test data loaded. -// -// 'course-detail': { path: '/courses/ENGGEN131', auth: 'student' }, -// 'course-problem-set': { path: '/courses/ENGGEN131/problem-set/demo-problem-set', auth: 'student' }, -// 'instructor-course-overview': { path: '/instructor/courses/ENGGEN131', auth: 'instructor' }, -// 'instructor-course-students': { path: '/instructor/courses/ENGGEN131/students', auth: 'instructor' }, -// 'instructor-course-subs': { path: '/instructor/courses/ENGGEN131/submissions', auth: 'instructor' }, -// 'instructor-course-ps': { path: '/instructor/courses/ENGGEN131/problem-sets', auth: 'instructor' }, -// 'admin-course-students': { path: '/admin/courses/ENGGEN131/students', auth: 'admin' }, -// 'admin-course-ps': { path: '/admin/courses/ENGGEN131/problem-sets', auth: 'admin' }, + // Seeded (student, instructor and admin views of CS101-2024) + 'course-detail': { path: `/courses/${SEEDED_COURSE}`, auth: 'student' }, + 'course-ps-mcq': { path: `/courses/${SEEDED_COURSE}/problem-set/e2e-basics?p=0`, auth: 'student' }, + 'course-ps-probeable': { path: `/courses/${SEEDED_COURSE}/problem-set/e2e-code?p=1`, auth: 'student' }, + 'course-ps-refute': { path: `/courses/${SEEDED_COURSE}/problem-set/e2e-mixed?p=6`, auth: 'student' }, + 'instructor-course-overview': { path: `/instructor/courses/${SEEDED_COURSE}`, auth: 'instructor' }, + 'instructor-course-subs': { path: `/instructor/courses/${SEEDED_COURSE}/submissions`, auth: 'instructor' }, + 'instructor-course-ps': { path: `/instructor/courses/${SEEDED_COURSE}/problem-sets`, auth: 'instructor' }, + 'admin-course-students': { path: `/admin/courses/${SEEDED_COURSE}/students`, auth: 'admin' }, + 'admin-course-ps': { path: `/admin/courses/${SEEDED_COURSE}/problem-sets`, auth: 'admin' }, +}; const THEMES = ['dark', 'light'] as const; const SCREENSHOT_ROOT = path.resolve(__dirname, '..', 'visual-tests', 'screenshots'); diff --git a/purplex/client/package.json b/purplex/client/package.json index c1d85300..80a9e30d 100644 --- a/purplex/client/package.json +++ b/purplex/client/package.json @@ -32,7 +32,8 @@ "i18n:check": "node scripts/check-i18n.mjs", "i18n:badges": "node scripts/generate-i18n-badges.mjs", "stylelint": "stylelint \"src/**/*.{vue,css}\" --fix", - "stylelint:check": "stylelint \"src/**/*.{vue,css}\"" + "stylelint:check": "stylelint \"src/**/*.{vue,css}\"", + "theme:check": "node scripts/check-theme-tokens.mjs" }, "dependencies": { "ace-builds": "^1.32.5", diff --git a/purplex/client/scripts/check-theme-tokens.mjs b/purplex/client/scripts/check-theme-tokens.mjs new file mode 100644 index 00000000..12568892 --- /dev/null +++ b/purplex/client/scripts/check-theme-tokens.mjs @@ -0,0 +1,280 @@ +#!/usr/bin/env node +/** + * Theme token check + * + * Keeps the light and dark themes in step and catches token typos that + * would otherwise render silently as `unset`. + * + * 1. Parity: the dark and light theme blocks in TOKENS_FILE define the same + * names in the same order, hold no theme-independent token (spacing, + * radius, type, transitions, layout), and light comes after dark. + * 2. Resolution: every `var(--x` in src/ and index.html (fallbacks included, + * because a fallback hides a typo) has a definition somewhere in src/. + * 3. Usage: every custom property defined in src/ is read somewhere. + * 4. Containment: `[data-theme` appears only in TOKENS_FILE. + * + * Usage: node scripts/check-theme-tokens.mjs (run from purplex/client/) + * Exit: 1 on any problem not listed in KNOWN_OFFENDERS, or on a + * KNOWN_OFFENDERS entry that no longer occurs (delete it). + */ + +import { readdirSync, readFileSync } from 'fs'; +import { join, relative, resolve } from 'path'; + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +/** The single file allowed to define theme colours, relative to the client root. */ +const TOKENS_FILE = 'src/App.vue'; + +const DARK_SELECTOR = ':root, [data-theme="dark"]'; +const LIGHT_SELECTOR = '[data-theme="light"]'; + +/** Theme-independent tokens; they belong in the plain `:root` block. */ +const NON_COLOUR_PREFIXES = ['--spacing-', '--radius-', '--font-', '--transition-', '--max-width-']; + +/** Per-instance variables set from inline `:style` bindings, not from CSS. */ +const INLINE_VARIABLES = [/^--modal-/, /^--param-count$/, /^--segment-bg-color$/]; + +/** Prefixes read with a computed name, e.g. getPropertyValue(`--color-segment-${n}`). */ +const DYNAMIC_PREFIXES = ['--color-segment-']; + +/** + * Problems that exist today and are scheduled to be fixed. Each entry is + * `:`. Listed problems print as warnings; anything else fails. + */ +const KNOWN_OFFENDERS = [ + // Step 1 moves the tokens to src/styles/tokens.css with a separate base block. + 'parity:theme-independent tokens in the dark block', + 'unused:--hint-spacing', + 'unused:--hint-border-radius', + 'unused:--hint-transition', + 'unused:--seg-spacing-xs', + 'unused:--seg-transition-base', + 'unused:--seg-progress-height', + 'unused:--seg-z-modal', + // Step 2 maps or deletes these. + 'undefined:--shadow-xl', + 'undefined:--color-bg-card', + 'undefined:--color-danger', + 'undefined:--font-mono', + 'undefined:--font-sans', + 'undefined:--font-family-mono', + 'undefined:--color-bg-primary', + 'undefined:--color-bg-muted', + 'undefined:--color-bg-page', + 'undefined:--color-bg-base', + 'undefined:--color-border', + 'undefined:--color-surface', + 'undefined:--color-bg-code', + 'undefined:--color-background', + 'undefined:--color-text-code', + 'unused:--color-bg-table', + 'unused:--color-info-text', + 'unused:--transition-slow', + 'unused:--max-width-panel', + 'data-theme:src/style.css', + // Step 5 replaces these component overrides with nav-sub tokens. + 'data-theme:src/components/AdminNavBar.vue', + 'data-theme:src/components/NavBar.vue', + 'data-theme:src/components/instructor/InstructorCourseOverview.vue', + 'data-theme:src/components/instructor/InstructorNavBar.vue', +]; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const clientRoot = resolve(import.meta.dirname, '..'); + +function walk(dir, out = []) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + walk(path, out); + } else if (/\.(vue|css|ts)$/.test(entry.name)) { + out.push(path); + } + } + return out; +} + +const stripComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, ''); + +/** Top-level rules of a stylesheet as { selector, body, index }. */ +function topLevelRules(css) { + const rules = []; + let depth = 0; + let start = 0; + let selector = ''; + for (let i = 0; i < css.length; i++) { + if (css[i] === '{') { + if (depth === 0) { + selector = css.slice(start, i).trim().replace(/\s+/g, ' '); + start = i + 1; + } + depth++; + } else if (css[i] === '}') { + depth--; + if (depth === 0) { + rules.push({ selector, body: css.slice(start, i), index: rules.length }); + start = i + 1; + } + } + } + return rules; +} + +const declaredNames = (css) => + [...css.matchAll(/(? m[1]); + +function tokensCss() { + const text = readFileSync(join(clientRoot, TOKENS_FILE), 'utf-8'); + if (!TOKENS_FILE.endsWith('.vue')) { + return stripComments(text); + } + const blocks = [...text.matchAll(/]*>([\s\S]*?)<\/style>/g)].map((m) => m[1]); + return stripComments(blocks.join('\n')); +} + +// --------------------------------------------------------------------------- +// Collect +// --------------------------------------------------------------------------- + +const problems = []; +const files = [...walk(join(clientRoot, 'src')), join(clientRoot, 'index.html')]; + +const definitions = new Map(); // name -> first file +const uses = new Map(); // name -> [file:line] +const dataThemeFiles = new Set(); + +for (const file of files) { + const rel = relative(clientRoot, file); + const text = stripComments(readFileSync(file, 'utf-8')); + + for (const name of declaredNames(text)) { + if (!definitions.has(name)) {definitions.set(name, rel);} + } + + const lines = text.split('\n'); + lines.forEach((line, i) => { + for (const m of line.matchAll(/var\(\s*(--[\w-]+)(\$)?/g)) { + const name = m[2] ? `${m[1]}*` : m[1]; + if (!uses.has(name)) {uses.set(name, []);} + uses.get(name).push(`${rel}:${i + 1}`); + } + for (const m of line.matchAll(/getPropertyValue\(\s*['"](--[\w-]+)['"]/g)) { + if (!uses.has(m[1])) {uses.set(m[1], []);} + uses.get(m[1]).push(`${rel}:${i + 1}`); + } + if (line.includes('[data-theme') && rel !== TOKENS_FILE) { + dataThemeFiles.add(rel); + } + }); +} + +// --------------------------------------------------------------------------- +// 1. Parity +// --------------------------------------------------------------------------- + +const rules = topLevelRules(tokensCss()); +// Before the tokens move, App.vue's plain :root doubles as the dark block. +const dark = + rules.find((r) => r.selector === DARK_SELECTOR) ?? rules.find((r) => r.selector === ':root'); +const light = rules.find((r) => r.selector === LIGHT_SELECTOR); + +if (!dark || !light) { + problems.push(`parity:missing ${!dark ? 'dark' : 'light'} block in ${TOKENS_FILE}`); +} else { + if (light.index < dark.index) { + problems.push('parity:light block must come after the dark block'); + } + const darkNames = declaredNames(dark.body); + const lightNames = declaredNames(light.body); + for (const [label, names] of [['dark', darkNames], ['light', lightNames]]) { + const misplaced = names.filter((n) => NON_COLOUR_PREFIXES.some((p) => n.startsWith(p))); + if (misplaced.length) { + problems.push( + `parity:theme-independent tokens in the ${label} block (${misplaced.length}x: ${misplaced.join(', ')})`, + ); + } + } + const darkColour = darkNames.filter((n) => !NON_COLOUR_PREFIXES.some((p) => n.startsWith(p))); + for (const name of darkColour) { + if (!lightNames.includes(name)) {problems.push(`parity:missing in light: ${name}`);} + } + for (const name of lightNames) { + if (!darkColour.includes(name)) {problems.push(`parity:missing in dark: ${name}`);} + } + const shared = darkColour.filter((n) => lightNames.includes(n)); + const lightShared = lightNames.filter((n) => darkColour.includes(n)); + const firstMismatch = shared.findIndex((n, i) => n !== lightShared[i]); + if (firstMismatch !== -1) { + problems.push( + `parity:order differs at ${shared[firstMismatch]} (light has ${lightShared[firstMismatch]})`, + ); + } +} + +// --------------------------------------------------------------------------- +// 2. Resolution and 3. Usage +// --------------------------------------------------------------------------- + +const isInline = (name) => INLINE_VARIABLES.some((re) => re.test(name)); + +for (const [name, sites] of uses) { + if (name.endsWith('*')) { + const prefix = name.slice(0, -1); + if (!DYNAMIC_PREFIXES.includes(prefix)) { + problems.push(`undefined:${name} (computed name; add to DYNAMIC_PREFIXES) at ${sites[0]}`); + } + continue; + } + if (!definitions.has(name) && !isInline(name)) { + problems.push(`undefined:${name} (${sites.length}x, first at ${sites[0]})`); + } +} + +for (const [name, file] of definitions) { + if (uses.has(name) || isInline(name)) {continue;} + if (DYNAMIC_PREFIXES.some((p) => name.startsWith(p))) {continue;} + problems.push(`unused:${name} (defined in ${file})`); +} + +// --------------------------------------------------------------------------- +// 4. Containment +// --------------------------------------------------------------------------- + +for (const file of [...dataThemeFiles].sort()) { + problems.push(`data-theme:${file}`); +} + +// --------------------------------------------------------------------------- +// Report +// --------------------------------------------------------------------------- + +const key = (p) => p.replace(/ \(.*$/, ''); +const known = new Set(KNOWN_OFFENDERS); +const errors = problems.filter((p) => !known.has(key(p))); +const warnings = problems.filter((p) => known.has(key(p))); +const found = new Set(problems.map(key)); +const stale = KNOWN_OFFENDERS.filter((k) => !found.has(k)); + +console.log(`Theme tokens: ${TOKENS_FILE}, ${definitions.size} custom properties defined in src/`); +if (warnings.length) { + console.log(`\nKnown offenders (${warnings.length}), scheduled for removal:`); + warnings.forEach((p) => console.log(` warn ${p}`)); +} +if (stale.length) { + console.log(`\nFixed but still listed in KNOWN_OFFENDERS (${stale.length}); delete them:`); + stale.forEach((p) => console.log(` stale ${p}`)); +} +if (errors.length) { + console.log(`\nProblems (${errors.length}):`); + errors.forEach((p) => console.log(` error ${p}`)); +} +if (errors.length || stale.length) { + process.exit(1); +} +console.log('\nOK'); diff --git a/purplex/client/src/composables/__tests__/useTheme.test.ts b/purplex/client/src/composables/__tests__/useTheme.test.ts new file mode 100644 index 00000000..7226c490 --- /dev/null +++ b/purplex/client/src/composables/__tests__/useTheme.test.ts @@ -0,0 +1,223 @@ +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const STORAGE_KEY = 'purplex_theme' + +// Node 25+ ships its own localStorage global that shadows happy-dom's, and +// happy-dom's matchMedia never matches, so both are stubbed explicitly. +let store: Map +let storageThrows: boolean +let systemLight: boolean +let mediaListeners: Array<() => void> + +const localStorageStub = { + getItem: vi.fn((key: string) => { + if (storageThrows) {throw new Error('storage disabled')} + return store.get(key) ?? null + }), + setItem: vi.fn((key: string, value: string) => { + if (storageThrows) {throw new Error('storage disabled')} + store.set(key, value) + }), +} + +function matchMediaStub(query: string) { + const wantsLight = /prefers-color-scheme:\s*light/.test(query) + return { + matches: wantsLight ? systemLight : !systemLight, + media: query, + addEventListener: (_event: string, handler: () => void) => { + mediaListeners.push(handler) + }, + } +} + +async function loadUseTheme() { + vi.resetModules() + const mod = await import('../useTheme') + return mod.useTheme +} + +const dataTheme = () => document.documentElement.getAttribute('data-theme') + +beforeEach(() => { + store = new Map() + storageThrows = false + systemLight = false + mediaListeners = [] + vi.stubGlobal('localStorage', localStorageStub) + vi.stubGlobal('matchMedia', matchMediaStub) + window.matchMedia = matchMediaStub as unknown as typeof window.matchMedia + localStorageStub.setItem.mockClear() + document.documentElement.removeAttribute('data-theme') + document.body.style.colorScheme = '' +}) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('useTheme', () => { + describe('initial preference', () => { + it.each([ + ['light', false, 'light'], + ['dark', true, 'dark'], + ['auto', true, 'light'], + ['auto', false, 'dark'], + ['sepia', true, 'light'], + ])('stored %s with system light=%s resolves to %s', async (stored, light, expected) => { + store.set(STORAGE_KEY, stored) + systemLight = light + const { theme, effectiveTheme } = (await loadUseTheme())() + + expect(theme.value).toBe(['light', 'dark', 'auto'].includes(stored) ? stored : 'auto') + expect(effectiveTheme.value).toBe(expected) + expect(dataTheme()).toBe(expected) + }) + + it('falls back to auto when nothing is stored', async () => { + systemLight = true + const { theme, effectiveTheme } = (await loadUseTheme())() + + expect(theme.value).toBe('auto') + expect(effectiveTheme.value).toBe('light') + }) + + it('falls back to auto when localStorage throws', async () => { + storageThrows = true + const { theme, effectiveTheme } = (await loadUseTheme())() + + expect(theme.value).toBe('auto') + expect(effectiveTheme.value).toBe('dark') + }) + }) + + describe('setTheme', () => { + it('persists the preference and applies data-theme', async () => { + const { setTheme, theme, effectiveTheme } = (await loadUseTheme())() + + setTheme('light') + + expect(localStorageStub.setItem).toHaveBeenCalledWith(STORAGE_KEY, 'light') + expect(theme.value).toBe('light') + expect(effectiveTheme.value).toBe('light') + expect(dataTheme()).toBe('light') + expect(document.body.style.colorScheme).toBe('light') + }) + + it('resolves auto against the system preference', async () => { + store.set(STORAGE_KEY, 'dark') + systemLight = true + const { setTheme, effectiveTheme } = (await loadUseTheme())() + + setTheme('auto') + + expect(store.get(STORAGE_KEY)).toBe('auto') + expect(effectiveTheme.value).toBe('light') + expect(dataTheme()).toBe('light') + }) + }) + + describe('system theme listener', () => { + it('follows the system under auto', async () => { + const { effectiveTheme } = (await loadUseTheme())() + expect(effectiveTheme.value).toBe('dark') + + systemLight = true + mediaListeners.forEach((handler) => handler()) + + expect(effectiveTheme.value).toBe('light') + expect(dataTheme()).toBe('light') + }) + + it('ignores the system under an explicit preference', async () => { + store.set(STORAGE_KEY, 'dark') + const { effectiveTheme } = (await loadUseTheme())() + + systemLight = true + mediaListeners.forEach((handler) => handler()) + + expect(effectiveTheme.value).toBe('dark') + expect(dataTheme()).toBe('dark') + }) + + it('registers one listener however often the composable is called', async () => { + const useTheme = await loadUseTheme() + useTheme() + useTheme() + + expect(mediaListeners).toHaveLength(1) + }) + }) + + describe('editorTheme', () => { + it.each([ + ['light', 'chrome'], + ['dark', 'tomorrow_night'], + ] as const)('maps %s to the %s Ace theme', async (effective, aceTheme) => { + const { setTheme, editorTheme } = (await loadUseTheme())() + + setTheme(effective) + + expect(editorTheme.value).toBe(aceTheme) + }) + }) +}) + +// The inline script in index.html sets data-theme before first paint. It +// must agree with useTheme, or the page flashes the wrong theme on load. +describe('index.html FOUC script', () => { + const html = readFileSync(resolve(__dirname, '../../../index.html'), 'utf-8') + const inline = [...html.matchAll(/ diff --git a/purplex/client/src/components/content/ContentEditorLayout.vue b/purplex/client/src/components/content/ContentEditorLayout.vue index aab32b70..c3fe44b2 100644 --- a/purplex/client/src/components/content/ContentEditorLayout.vue +++ b/purplex/client/src/components/content/ContentEditorLayout.vue @@ -143,7 +143,7 @@ defineExpose({ ctx }); } .breadcrumb-link:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } diff --git a/purplex/client/src/components/content/CourseEditorShell.vue b/purplex/client/src/components/content/CourseEditorShell.vue index a37677b6..2c71c8c2 100644 --- a/purplex/client/src/components/content/CourseEditorShell.vue +++ b/purplex/client/src/components/content/CourseEditorShell.vue @@ -366,7 +366,7 @@ onMounted(loadInstructors); width: 40px; height: 40px; border: 3px solid var(--color-bg-hover); - border-top: 3px solid var(--color-primary-gradient-start); + border-top: 3px solid var(--color-accent); border-radius: var(--radius-circle); animation: spin 1s linear infinite; } @@ -443,8 +443,8 @@ onMounted(loadInstructors); .form-group textarea:focus, .form-group select:focus { outline: none; - border-color: var(--color-primary-gradient-start); - box-shadow: 0 0 0 3px var(--color-primary-overlay); + border-color: var(--color-focus-ring); + box-shadow: 0 0 0 3px var(--color-accent-overlay); } .form-group input:disabled { @@ -507,14 +507,14 @@ onMounted(loadInstructors); } .btn-primary { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); color: var(--color-text-on-filled); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } .btn-primary:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: 0 6px 20px var(--color-primary-glow); + box-shadow: 0 6px 20px var(--color-accent-glow); } .btn-secondary { diff --git a/purplex/client/src/components/content/CourseList.vue b/purplex/client/src/components/content/CourseList.vue index 847a2d01..9094650e 100644 --- a/purplex/client/src/components/content/CourseList.vue +++ b/purplex/client/src/components/content/CourseList.vue @@ -255,13 +255,13 @@ onMounted(fetchTable); .search-input:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); } .course-id { font-family: monospace; font-weight: 500; - color: var(--color-primary-gradient-start); + color: var(--color-accent); } .actions-cell { @@ -286,14 +286,14 @@ onMounted(fetchTable); } .add-button { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); - box-shadow: var(--shadow-colored); + background: var(--gradient-primary); + box-shadow: var(--shadow-accent); color: var(--color-text-on-filled); } .add-button:hover { transform: translateY(-2px); - box-shadow: 0 6px 20px var(--color-primary-shadow); + box-shadow: 0 6px 20px var(--color-accent-shadow); } .add-button::before { @@ -321,7 +321,7 @@ onMounted(fetchTable); .icon-button:hover { transform: translateY(-2px); box-shadow: var(--shadow-sm); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); background: var(--color-bg-input); } diff --git a/purplex/client/src/components/content/CourseProblemSetsPage.vue b/purplex/client/src/components/content/CourseProblemSetsPage.vue index 3ab6dbbd..cdd843e6 100644 --- a/purplex/client/src/components/content/CourseProblemSetsPage.vue +++ b/purplex/client/src/components/content/CourseProblemSetsPage.vue @@ -576,7 +576,7 @@ watch( width: 40px; height: 40px; border: 3px solid var(--color-bg-hover); - border-top: 3px solid var(--color-primary-gradient-start); + border-top: 3px solid var(--color-accent); border-radius: var(--radius-circle); animation: spin 1s linear infinite; } @@ -674,7 +674,7 @@ watch( } .order-btn:hover:not(:disabled) { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); color: var(--color-text-primary); } @@ -702,7 +702,7 @@ watch( } .due-date-input:focus { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); outline: none; } @@ -739,7 +739,7 @@ watch( } .deadline-select:focus { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); outline: none; } @@ -775,7 +775,7 @@ watch( } .available-item:hover { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); } .item-info { @@ -830,7 +830,7 @@ watch( .secondary-button:hover { background: var(--color-bg-input); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); color: var(--color-text-primary); } diff --git a/purplex/client/src/components/content/CourseStudentsPage.vue b/purplex/client/src/components/content/CourseStudentsPage.vue index 63cc1dbe..ec407822 100644 --- a/purplex/client/src/components/content/CourseStudentsPage.vue +++ b/purplex/client/src/components/content/CourseStudentsPage.vue @@ -281,7 +281,7 @@ watch( .search-input:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); } .email { @@ -339,7 +339,7 @@ watch( .secondary-button:hover { background: var(--color-bg-input); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); color: var(--color-text-primary); } diff --git a/purplex/client/src/components/content/ProblemEditorShell.vue b/purplex/client/src/components/content/ProblemEditorShell.vue index 651bcb34..00608928 100644 --- a/purplex/client/src/components/content/ProblemEditorShell.vue +++ b/purplex/client/src/components/content/ProblemEditorShell.vue @@ -325,20 +325,20 @@ async function confirmDelete() { } .btn:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } .btn-primary { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); color: var(--color-text-on-filled); - border-color: var(--color-primary-gradient-start); - box-shadow: var(--shadow-colored); + border-color: var(--color-accent); + box-shadow: var(--shadow-accent); } .btn-primary:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: 0 8px 25px var(--color-primary-glow); + box-shadow: 0 8px 25px var(--color-accent-glow); } .btn-secondary { @@ -421,7 +421,7 @@ async function confirmDelete() { .form-group select:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); background: var(--color-bg-panel); } @@ -458,7 +458,7 @@ async function confirmDelete() { width: 40px; height: 40px; border: 3px solid var(--color-bg-border); - border-top-color: var(--color-primary-gradient-start); + border-top-color: var(--color-accent); border-radius: 50%; animation: spin 1s linear infinite; } diff --git a/purplex/client/src/components/content/ProblemList.vue b/purplex/client/src/components/content/ProblemList.vue index 45895b37..4d8ac320 100644 --- a/purplex/client/src/components/content/ProblemList.vue +++ b/purplex/client/src/components/content/ProblemList.vue @@ -257,7 +257,7 @@ onMounted(fetchTable); .search-input:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); } .actions-cell { @@ -280,14 +280,14 @@ onMounted(fetchTable); } .add-button { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); - box-shadow: var(--shadow-colored); + background: var(--gradient-primary); + box-shadow: var(--shadow-accent); color: var(--color-text-on-filled); } .add-button:hover { transform: translateY(-2px); - box-shadow: 0 6px 20px var(--color-primary-shadow); + box-shadow: 0 6px 20px var(--color-accent-shadow); } .add-button::before { @@ -304,7 +304,7 @@ onMounted(fetchTable); .edit-button:hover { background: var(--color-bg-input); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); color: var(--color-text-primary); } diff --git a/purplex/client/src/components/content/ProblemSetEditorShell.vue b/purplex/client/src/components/content/ProblemSetEditorShell.vue index c0422d61..397b289c 100644 --- a/purplex/client/src/components/content/ProblemSetEditorShell.vue +++ b/purplex/client/src/components/content/ProblemSetEditorShell.vue @@ -497,7 +497,7 @@ onMounted(async () => { width: 40px; height: 40px; border: 3px solid var(--color-bg-border); - border-top-color: var(--color-primary-gradient-start); + border-top-color: var(--color-accent); border-radius: 50%; animation: spin 1s linear infinite; } @@ -506,7 +506,7 @@ onMounted(async () => { width: 20px; height: 20px; border: 2px solid var(--color-bg-border); - border-top-color: var(--color-primary-gradient-start); + border-top-color: var(--color-accent); border-radius: 50%; animation: spin 1s linear infinite; } @@ -588,7 +588,7 @@ onMounted(async () => { .form-group input[type="text"]:focus, .form-group textarea:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); background: var(--color-bg-panel); } @@ -605,7 +605,7 @@ onMounted(async () => { .checkbox-label input[type="checkbox"] { width: 18px; height: 18px; - accent-color: var(--color-primary-gradient-start); + accent-color: var(--color-accent); } /* Problems Section */ @@ -665,7 +665,7 @@ onMounted(async () => { .problem-search:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); } .problem-list { @@ -707,7 +707,7 @@ onMounted(async () => { .problem-order { width: 28px; height: 28px; - background: var(--color-primary-gradient-start); + background: var(--color-accent); color: var(--color-text-on-filled); border-radius: 50%; display: flex; @@ -811,20 +811,20 @@ onMounted(async () => { } .btn:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } .btn-primary { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); color: var(--color-text-on-filled); - border-color: var(--color-primary-gradient-start); - box-shadow: var(--shadow-colored); + border-color: var(--color-accent); + box-shadow: var(--shadow-accent); } .btn-primary:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: 0 8px 25px var(--color-primary-glow); + box-shadow: 0 8px 25px var(--color-accent-glow); } .btn-secondary { diff --git a/purplex/client/src/components/content/ProblemSetManager.vue b/purplex/client/src/components/content/ProblemSetManager.vue index 6aea461f..fa8e584e 100644 --- a/purplex/client/src/components/content/ProblemSetManager.vue +++ b/purplex/client/src/components/content/ProblemSetManager.vue @@ -216,7 +216,7 @@ onMounted(fetchTable); .search-input:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); } .actions-cell { @@ -240,14 +240,14 @@ onMounted(fetchTable); } .add-button { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); - box-shadow: var(--shadow-colored); + background: var(--gradient-primary); + box-shadow: var(--shadow-accent); color: var(--color-text-on-filled); } .add-button:hover { transform: translateY(-2px); - box-shadow: 0 6px 20px var(--color-primary-shadow); + box-shadow: 0 6px 20px var(--color-accent-shadow); } .add-button::before { @@ -264,7 +264,7 @@ onMounted(fetchTable); .edit-button:hover { background: var(--color-bg-input); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); color: var(--color-text-primary); } diff --git a/purplex/client/src/components/content/SubmissionDetailPage.vue b/purplex/client/src/components/content/SubmissionDetailPage.vue index d73a5439..651b79a1 100644 --- a/purplex/client/src/components/content/SubmissionDetailPage.vue +++ b/purplex/client/src/components/content/SubmissionDetailPage.vue @@ -302,7 +302,7 @@ onMounted(fetchSubmission); .retry-btn { margin-top: 16px; padding: 8px 20px; - background: var(--color-primary-gradient-start); + background: var(--color-accent); color: var(--color-text-on-filled); border: none; border-radius: 4px; diff --git a/purplex/client/src/components/content/SubmissionsPage.vue b/purplex/client/src/components/content/SubmissionsPage.vue index 591c0349..a1ce4588 100644 --- a/purplex/client/src/components/content/SubmissionsPage.vue +++ b/purplex/client/src/components/content/SubmissionsPage.vue @@ -510,12 +510,12 @@ onMounted(fetchSubmissions); .filter-group input:focus, .filter-group select:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); } .filter-group input:focus-visible, .filter-group select:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -542,8 +542,8 @@ onMounted(fetchSubmissions); .clear-filters-btn:hover { background: var(--color-bg-hover); - border-color: var(--color-primary-gradient-start); - color: var(--color-primary-gradient-start); + border-color: var(--color-accent); + color: var(--color-accent); } /* Active Filter Chips */ @@ -572,17 +572,17 @@ onMounted(fetchSubmissions); align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); - background: var(--color-primary-overlay); - border: 1px solid var(--color-primary-gradient-start); + background: var(--color-accent-overlay); + border: 1px solid var(--color-accent); border-radius: var(--radius-xl); font-size: var(--font-size-xs); - color: var(--color-primary-gradient-start); + color: var(--color-accent); cursor: pointer; transition: var(--transition-base); } .filter-chip:hover { - background: var(--color-primary-overlay); + background: var(--color-accent-overlay); } .chip-label { @@ -604,7 +604,7 @@ onMounted(fetchSubmissions); height: 16px; margin-left: var(--spacing-xs); border-radius: 50%; - background: var(--color-primary-gradient-start); + background: var(--color-accent); color: var(--color-text-on-filled); font-size: 10px; font-weight: bold; @@ -653,7 +653,7 @@ onMounted(fetchSubmissions); width: 28px; height: 28px; border-radius: var(--radius-circle); - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); display: flex; align-items: center; justify-content: center; @@ -825,9 +825,9 @@ onMounted(fetchSubmissions); } .export-button { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); color: var(--color-text-on-filled); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); padding: var(--spacing-sm) var(--spacing-lg); font-size: var(--font-size-sm); } @@ -840,7 +840,7 @@ onMounted(fetchSubmissions); .export-button:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: 0 6px 20px var(--color-primary-glow); + box-shadow: 0 6px 20px var(--color-accent-glow); } .view-button { diff --git a/purplex/client/src/components/hints/SuggestedTraceOverlay.vue b/purplex/client/src/components/hints/SuggestedTraceOverlay.vue index fe3c5c67..d061c84e 100644 --- a/purplex/client/src/components/hints/SuggestedTraceOverlay.vue +++ b/purplex/client/src/components/hints/SuggestedTraceOverlay.vue @@ -57,7 +57,7 @@ function openInPyTutor(): void { } .suggested-trace:hover { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); box-shadow: var(--shadow-sm); } @@ -93,7 +93,7 @@ function openInPyTutor(): void { align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-md); - background: var(--color-primary-gradient-start); + background: var(--color-accent); color: var(--color-text-on-filled); border: none; border-radius: var(--radius-base); @@ -113,7 +113,7 @@ function openInPyTutor(): void { } .trace-btn:hover:not(:disabled) { - background: var(--color-primary-gradient-end); + background: var(--color-accent-strong); transform: translateY(-1px); box-shadow: var(--shadow-sm); } diff --git a/purplex/client/src/components/instructor/CourseTeamManager.vue b/purplex/client/src/components/instructor/CourseTeamManager.vue index c39c8224..eaac9ce5 100644 --- a/purplex/client/src/components/instructor/CourseTeamManager.vue +++ b/purplex/client/src/components/instructor/CourseTeamManager.vue @@ -305,8 +305,8 @@ onMounted(fetchTeam); } .role-primary { - background: var(--color-primary-overlay); - color: var(--color-primary-gradient-start); + background: var(--color-accent-overlay); + color: var(--color-accent); } .role-ta { @@ -380,7 +380,7 @@ onMounted(fetchTeam); .btn-add { padding: var(--spacing-sm) var(--spacing-md); - background: linear-gradient(135deg, var(--color-primary-gradient-start), var(--color-primary-gradient-end)); + background: var(--gradient-primary); color: var(--color-text-on-filled); border: none; border-radius: var(--radius-xs); diff --git a/purplex/client/src/components/instructor/InstructorCourseOverview.vue b/purplex/client/src/components/instructor/InstructorCourseOverview.vue index ecebf6d8..63488bbb 100644 --- a/purplex/client/src/components/instructor/InstructorCourseOverview.vue +++ b/purplex/client/src/components/instructor/InstructorCourseOverview.vue @@ -808,11 +808,11 @@ onMounted(() => { height: 48px; border: 4px solid var(--color-bg-border); border-top-color: var(--color-admin); - border-right-color: var(--color-primary-gradient-end); + border-right-color: var(--color-accent-strong); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: var(--spacing-lg); - box-shadow: 0 0 15px var(--color-primary-overlay); + box-shadow: 0 0 15px var(--color-accent-overlay); } @keyframes spin { @@ -843,18 +843,18 @@ onMounted(() => { .back-btn { display: inline-block; padding: var(--spacing-md) var(--spacing-xl); - background: linear-gradient(135deg, var(--color-admin), var(--color-primary-gradient-end)); + background: var(--gradient-admin); color: var(--color-text-on-filled); text-decoration: none; border-radius: var(--radius-base); font-weight: 600; transition: var(--transition-base); - box-shadow: 0 2px 8px var(--color-primary-glow); + box-shadow: 0 2px 8px var(--color-accent-glow); } .back-btn:hover { transform: translateY(-2px); - box-shadow: 0 4px 12px var(--color-primary-glow); + box-shadow: 0 4px 12px var(--color-accent-glow); } /* Unified Course Card */ @@ -959,18 +959,18 @@ onMounted(() => { /* At-rest card appearance - clearly clickable */ background: var(--color-overlay-subtle); - border: 1px solid var(--color-primary-overlay); + border: 1px solid var(--color-accent-overlay); box-shadow: var(--shadow-xs); } .journey-item:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); } .journey-item.selected { - background: var(--color-primary-overlay); + background: var(--color-accent-overlay); border-color: var(--color-admin); box-shadow: var(--shadow-lg); } @@ -1002,12 +1002,12 @@ onMounted(() => { justify-content: center; width: 32px; height: 32px; - background: linear-gradient(135deg, var(--color-admin), var(--color-primary-gradient-end)); + background: var(--gradient-admin); color: var(--color-text-on-filled); font-weight: 600; font-size: var(--font-size-sm); border-radius: 50%; - box-shadow: 0 3px 8px var(--color-primary-glow); + box-shadow: 0 3px 8px var(--color-accent-glow); } .marker-line { @@ -1133,8 +1133,8 @@ onMounted(() => { justify-content: center; width: 32px; height: 32px; - background: var(--color-primary-overlay); - border: 1px solid var(--color-primary-overlay); + background: var(--color-accent-overlay); + border: 1px solid var(--color-accent-overlay); border-radius: var(--radius-base); color: var(--color-admin); cursor: pointer; @@ -1143,7 +1143,7 @@ onMounted(() => { } .download-btn:hover { - background: var(--color-primary-overlay); + background: var(--color-accent-overlay); border-color: var(--color-admin); opacity: 1; transform: translateY(-1px); @@ -1196,10 +1196,10 @@ onMounted(() => { transform: translate(-50%, -50%); width: 16px; height: 16px; - background: linear-gradient(135deg, var(--color-admin), var(--color-primary-gradient-end)); + background: var(--gradient-admin); border: 3px solid var(--color-bg-panel); border-radius: 50%; - box-shadow: 0 2px 6px var(--color-primary-glow); + box-shadow: 0 2px 6px var(--color-accent-glow); } .journey-stats { @@ -1219,8 +1219,8 @@ onMounted(() => { align-items: center; justify-content: space-between; padding: var(--spacing-md) var(--spacing-lg); - background: linear-gradient(135deg, var(--color-primary-overlay), var(--color-primary-overlay)); - border: 1px solid var(--color-primary-glow); + background: linear-gradient(135deg, var(--color-accent-overlay), var(--color-accent-overlay)); + border: 1px solid var(--color-accent-glow); border-radius: var(--radius-base); margin-bottom: var(--spacing-lg); } @@ -1333,7 +1333,7 @@ onMounted(() => { } .chart-line { - filter: drop-shadow(0 1px 2px var(--color-primary-glow)); + filter: drop-shadow(0 1px 2px var(--color-accent-glow)); } .chart-bars { @@ -1359,8 +1359,8 @@ onMounted(() => { max-width: 20px; background: linear-gradient( to top, - var(--color-primary-glow), - var(--color-primary-overlay) + var(--color-accent-glow), + var(--color-accent-overlay) ); border-radius: var(--radius-sm) var(--radius-sm) 0 0; min-height: 4px; @@ -1374,9 +1374,9 @@ onMounted(() => { background: linear-gradient( to top, var(--color-admin), - var(--color-primary-gradient-end) + var(--color-gradient-end) ); - box-shadow: 0 2px 8px var(--color-primary-glow); + box-shadow: 0 2px 8px var(--color-accent-glow); } /* Data point dots at bar tops */ @@ -1397,7 +1397,7 @@ onMounted(() => { .chart-bar-container:hover .chart-point { transform: translate(-50%, 50%) scale(1.3); background: var(--color-admin); - box-shadow: 0 0 8px var(--color-primary-glow); + box-shadow: 0 0 8px var(--color-accent-glow); } .chart-label { @@ -1431,7 +1431,7 @@ onMounted(() => { .snapshot-value { font-size: var(--font-size-xxl); font-weight: 700; - background: linear-gradient(135deg, var(--color-admin), var(--color-primary-gradient-end)); + background: var(--gradient-admin); /* stylelint-disable-next-line property-no-vendor-prefix -- needed for Safari */ -webkit-background-clip: text; background-clip: text; diff --git a/purplex/client/src/components/instructor/InstructorDashboard.vue b/purplex/client/src/components/instructor/InstructorDashboard.vue index d0c47692..59e2e53f 100644 --- a/purplex/client/src/components/instructor/InstructorDashboard.vue +++ b/purplex/client/src/components/instructor/InstructorDashboard.vue @@ -506,7 +506,7 @@ onMounted(() => { } .retry-btn:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -534,12 +534,12 @@ onMounted(() => { .course-card:hover, .course-card:focus-visible { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); box-shadow: var(--shadow-lg); } .course-card:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -553,9 +553,9 @@ onMounted(() => { align-items: center; justify-content: center; width: 0; - background: var(--color-primary-glow); + background: var(--color-accent-glow); backdrop-filter: blur(8px); - color: var(--color-primary-gradient-start); + color: var(--color-accent); overflow: hidden; transition: width 0.25s ease; } @@ -655,8 +655,8 @@ onMounted(() => { } .role-primary { - background: var(--color-primary-overlay); - color: var(--color-primary-gradient-start); + background: var(--color-accent-overlay); + color: var(--color-accent); } .role-ta { @@ -673,9 +673,9 @@ onMounted(() => { .add-course-card:hover, .add-course-card:focus-visible { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); border-style: solid; - background: var(--color-primary-overlay); + background: var(--color-accent-overlay); } .add-card-content { @@ -694,7 +694,7 @@ onMounted(() => { .add-course-card:hover .add-icon, .add-course-card:focus-visible .add-icon { - color: var(--color-primary-gradient-start); + color: var(--color-accent); } .add-text { @@ -706,13 +706,13 @@ onMounted(() => { .add-course-card:hover .add-text, .add-course-card:focus-visible .add-text { - color: var(--color-primary-gradient-start); + color: var(--color-accent); } /* Expandable Form Card */ .add-course-card.form-expanded { border-style: solid; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); background: var(--color-bg-panel); min-height: auto; cursor: default; @@ -820,8 +820,8 @@ onMounted(() => { .form-field input:focus { outline: none; - border-color: var(--color-primary-gradient-start); - box-shadow: 0 0 0 3px var(--color-primary-overlay); + border-color: var(--color-focus-ring); + box-shadow: 0 0 0 3px var(--color-accent-overlay); } .form-field input:disabled { @@ -901,14 +901,14 @@ onMounted(() => { .btn-create { padding: var(--spacing-sm) var(--spacing-xl); - background: linear-gradient(135deg, var(--color-admin) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-admin); color: var(--color-text-on-filled); border: none; border-radius: var(--radius-base); font-weight: 600; cursor: pointer; transition: var(--transition-base); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } .btn-create:disabled { @@ -920,7 +920,7 @@ onMounted(() => { .btn-create:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: 0 6px 20px var(--color-primary-glow); + box-shadow: 0 6px 20px var(--color-accent-glow); } /* Mobile responsive */ diff --git a/purplex/client/src/components/instructor/InstructorNavBar.vue b/purplex/client/src/components/instructor/InstructorNavBar.vue index 8ea15279..c2ccbfe1 100644 --- a/purplex/client/src/components/instructor/InstructorNavBar.vue +++ b/purplex/client/src/components/instructor/InstructorNavBar.vue @@ -184,7 +184,7 @@ const isActiveRoute = (path: string): boolean => { position: fixed; top: -100%; left: 0; - background: var(--color-primary-gradient-start); + background: var(--color-accent); color: var(--color-text-on-filled); padding: var(--spacing-md) var(--spacing-lg); text-decoration: none; diff --git a/purplex/client/src/components/instructor/InstructorStudents.vue b/purplex/client/src/components/instructor/InstructorStudents.vue index 531510e4..62664474 100644 --- a/purplex/client/src/components/instructor/InstructorStudents.vue +++ b/purplex/client/src/components/instructor/InstructorStudents.vue @@ -324,7 +324,7 @@ onMounted(() => { width: 48px; height: 48px; border: 4px solid var(--color-bg-input); - border-top-color: var(--color-primary-gradient-start); + border-top-color: var(--color-accent); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: var(--spacing-lg); @@ -375,7 +375,7 @@ onMounted(() => { } .retry-btn:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -405,8 +405,8 @@ onMounted(() => { .search-input:focus { outline: none; - border-color: var(--color-primary-gradient-start); - box-shadow: 0 0 0 3px var(--color-primary-overlay); + border-color: var(--color-focus-ring); + box-shadow: 0 0 0 3px var(--color-accent-overlay); } .search-input::placeholder { diff --git a/purplex/client/src/components/privacy/AccountDeletion.vue b/purplex/client/src/components/privacy/AccountDeletion.vue index d76d81c0..79f92552 100644 --- a/purplex/client/src/components/privacy/AccountDeletion.vue +++ b/purplex/client/src/components/privacy/AccountDeletion.vue @@ -203,7 +203,7 @@ function formatDate(isoDate: string): string { } .account-deletion__btn:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -218,13 +218,13 @@ function formatDate(isoDate: string): string { } .account-deletion__btn--cancel { - background: linear-gradient(135deg, var(--color-primary-gradient-start), var(--color-primary-gradient-end)); + background: var(--gradient-primary); color: var(--color-text-on-filled); } .account-deletion__btn--cancel:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } .account-deletion__pending { diff --git a/purplex/client/src/components/privacy/AgeGate.vue b/purplex/client/src/components/privacy/AgeGate.vue index cd93d00e..b75840d3 100644 --- a/purplex/client/src/components/privacy/AgeGate.vue +++ b/purplex/client/src/components/privacy/AgeGate.vue @@ -156,7 +156,7 @@ function submitAge() { .age-gate__input:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); } .age-gate__message { @@ -180,7 +180,7 @@ function submitAge() { .age-gate__submit { padding: var(--spacing-md); - background: linear-gradient(135deg, var(--color-primary-gradient-start), var(--color-primary-gradient-end)); + background: var(--gradient-primary); color: var(--color-text-on-filled); border: none; border-radius: var(--radius-sm); @@ -191,7 +191,7 @@ function submitAge() { } .age-gate__submit:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -202,6 +202,6 @@ function submitAge() { .age-gate__submit:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } diff --git a/purplex/client/src/components/privacy/ConsentForm.vue b/purplex/client/src/components/privacy/ConsentForm.vue index 495c22ed..aef8491c 100644 --- a/purplex/client/src/components/privacy/ConsentForm.vue +++ b/purplex/client/src/components/privacy/ConsentForm.vue @@ -188,7 +188,7 @@ function submitConsent() { .consent-item input[type="checkbox"] { margin-top: 3px; flex-shrink: 0; - accent-color: var(--color-primary-gradient-start); + accent-color: var(--color-accent); } .consent-item__text { @@ -198,11 +198,11 @@ function submitConsent() { } .consent-item__text a { - color: var(--color-primary-gradient-start); + color: var(--color-accent); } .consent-item__text a:hover { - color: var(--color-primary-gradient-end); + color: var(--color-accent-strong); } .consent-item__detail { @@ -219,7 +219,7 @@ function submitConsent() { .consent-form__submit { width: 100%; padding: var(--spacing-md); - background: linear-gradient(135deg, var(--color-primary-gradient-start), var(--color-primary-gradient-end)); + background: var(--gradient-primary); color: var(--color-text-on-filled); border: none; border-radius: var(--radius-sm); @@ -230,7 +230,7 @@ function submitConsent() { } .consent-form__submit:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -241,6 +241,6 @@ function submitConsent() { .consent-form__submit:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } diff --git a/purplex/client/src/components/privacy/CookieConsent.vue b/purplex/client/src/components/privacy/CookieConsent.vue index c96f8420..4728ddc0 100644 --- a/purplex/client/src/components/privacy/CookieConsent.vue +++ b/purplex/client/src/components/privacy/CookieConsent.vue @@ -99,11 +99,11 @@ function acceptAll() { } .cookie-consent__link { - color: var(--color-primary-gradient-start); + color: var(--color-accent); } .cookie-consent__link:hover { - color: var(--color-primary-gradient-end); + color: var(--color-accent-strong); } .cookie-consent__actions { @@ -123,7 +123,7 @@ function acceptAll() { } .cookie-consent__btn:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -134,18 +134,18 @@ function acceptAll() { .cookie-consent__btn--essential:hover { background: var(--color-bg-hover); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); } .cookie-consent__btn--accept { - background: linear-gradient(135deg, var(--color-primary-gradient-start), var(--color-primary-gradient-end)); + background: var(--gradient-primary); color: var(--color-text-on-filled); border-color: transparent; } .cookie-consent__btn--accept:hover { transform: translateY(-2px); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } @media (width <= 768px) { diff --git a/purplex/client/src/components/privacy/DataExport.vue b/purplex/client/src/components/privacy/DataExport.vue index 3ad2c0ef..1f58aaa4 100644 --- a/purplex/client/src/components/privacy/DataExport.vue +++ b/purplex/client/src/components/privacy/DataExport.vue @@ -84,7 +84,7 @@ async function exportData() { .data-export__btn { padding: var(--spacing-sm) var(--spacing-lg); - background: linear-gradient(135deg, var(--color-primary-gradient-start), var(--color-primary-gradient-end)); + background: var(--gradient-primary); color: var(--color-text-on-filled); border: none; border-radius: var(--radius-sm); @@ -94,7 +94,7 @@ async function exportData() { } .data-export__btn:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -105,7 +105,7 @@ async function exportData() { .data-export__btn:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } .data-export__error { diff --git a/purplex/client/src/components/privacy/PrivacyPolicy.vue b/purplex/client/src/components/privacy/PrivacyPolicy.vue index 5321769b..7be49e47 100644 --- a/purplex/client/src/components/privacy/PrivacyPolicy.vue +++ b/purplex/client/src/components/privacy/PrivacyPolicy.vue @@ -156,10 +156,10 @@ const effectiveDate = new Date().toLocaleDateString(undefined, { } .policy-page a { - color: var(--color-primary-gradient-start); + color: var(--color-accent); } .policy-page a:hover { - color: var(--color-primary-gradient-end); + color: var(--color-accent-strong); } diff --git a/purplex/client/src/components/privacy/PrivacySettings.vue b/purplex/client/src/components/privacy/PrivacySettings.vue index e124d09f..363c41e8 100644 --- a/purplex/client/src/components/privacy/PrivacySettings.vue +++ b/purplex/client/src/components/privacy/PrivacySettings.vue @@ -226,8 +226,8 @@ async function updateDirectoryInfo() { .consent-toggle__required { font-size: var(--font-size-xs); padding: 2px var(--spacing-xs); - background: var(--color-primary-overlay); - color: var(--color-primary-gradient-start); + background: var(--color-accent-overlay); + color: var(--color-accent); border-radius: var(--radius-xs); font-weight: 600; } @@ -267,7 +267,7 @@ async function updateDirectoryInfo() { } .consent-toggle__switch input:checked + .consent-toggle__slider { - background: var(--color-primary-gradient-start); + background: var(--color-accent); } .consent-toggle__switch input:checked + .consent-toggle__slider::before { @@ -280,7 +280,7 @@ async function updateDirectoryInfo() { } .consent-toggle__switch input:focus-visible + .consent-toggle__slider { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } diff --git a/purplex/client/src/components/privacy/TermsOfService.vue b/purplex/client/src/components/privacy/TermsOfService.vue index e8b1d8df..4a80f272 100644 --- a/purplex/client/src/components/privacy/TermsOfService.vue +++ b/purplex/client/src/components/privacy/TermsOfService.vue @@ -134,10 +134,10 @@ const effectiveDate = new Date().toLocaleDateString(undefined, { } .policy-page a { - color: var(--color-primary-gradient-start); + color: var(--color-accent); } .policy-page a:hover { - color: var(--color-primary-gradient-end); + color: var(--color-accent-strong); } diff --git a/purplex/client/src/components/segmentation/SegmentMapping.vue b/purplex/client/src/components/segmentation/SegmentMapping.vue index d8b1a064..d32bcbd8 100644 --- a/purplex/client/src/components/segmentation/SegmentMapping.vue +++ b/purplex/client/src/components/segmentation/SegmentMapping.vue @@ -528,15 +528,15 @@ export default defineComponent({ } .code-line.highlighted { - background: var(--color-primary-overlay); + background: var(--color-accent-overlay); } .code-line.highlighted .line-number { - background: var(--color-primary-overlay); + background: var(--color-accent-overlay); } .code-line.highlighted .line-content { - background: var(--color-primary-overlay); + background: var(--color-accent-overlay); } .code-line.dimmed { diff --git a/purplex/client/src/components/ui/AsyncError.vue b/purplex/client/src/components/ui/AsyncError.vue index 4d316a84..fa2bc207 100644 --- a/purplex/client/src/components/ui/AsyncError.vue +++ b/purplex/client/src/components/ui/AsyncError.vue @@ -64,12 +64,12 @@ defineProps<{ .retry-button:hover { background: var(--color-bg-input); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); color: var(--color-text-primary); } .retry-button:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } diff --git a/purplex/client/src/components/ui/AsyncLoader.vue b/purplex/client/src/components/ui/AsyncLoader.vue index acba8612..81b6feca 100644 --- a/purplex/client/src/components/ui/AsyncLoader.vue +++ b/purplex/client/src/components/ui/AsyncLoader.vue @@ -30,7 +30,7 @@ width: 32px; height: 32px; border: 3px solid var(--color-bg-input); - border-top-color: var(--color-primary-gradient-start); + border-top-color: var(--color-accent); border-radius: var(--radius-circle); animation: spin 0.8s linear infinite; } diff --git a/purplex/client/src/components/ui/DataTable.vue b/purplex/client/src/components/ui/DataTable.vue index 0447bdbd..a1db7724 100644 --- a/purplex/client/src/components/ui/DataTable.vue +++ b/purplex/client/src/components/ui/DataTable.vue @@ -302,7 +302,7 @@ function getRowKey(item: T, index: number): string | number { .retry-button { padding: var(--spacing-sm) var(--spacing-lg); - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); border: none; border-radius: var(--radius-base); color: var(--color-text-on-filled); @@ -313,7 +313,7 @@ function getRowKey(item: T, index: number): string | number { .retry-button:hover { transform: translateY(-2px); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } .table-wrapper { diff --git a/purplex/client/src/components/ui/DataTablePagination.vue b/purplex/client/src/components/ui/DataTablePagination.vue index b77cd479..59feb40d 100644 --- a/purplex/client/src/components/ui/DataTablePagination.vue +++ b/purplex/client/src/components/ui/DataTablePagination.vue @@ -182,17 +182,17 @@ function handlePageSizeChange(event: Event) { } .pagination-btn:hover:not(:disabled) { - border-color: var(--color-primary-gradient-start); - background: var(--color-primary-gradient-start); + border-color: var(--color-accent); + background: var(--color-accent); color: var(--color-text-on-filled); } .pagination-btn.active { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); - border-color: var(--color-primary-gradient-start); + background: var(--gradient-primary); + border-color: var(--color-accent); color: var(--color-text-on-filled); font-weight: 600; - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } /* Responsive */ diff --git a/purplex/client/src/components/ui/FunctionCallTable.vue b/purplex/client/src/components/ui/FunctionCallTable.vue index 08da2133..8667e032 100644 --- a/purplex/client/src/components/ui/FunctionCallTable.vue +++ b/purplex/client/src/components/ui/FunctionCallTable.vue @@ -120,7 +120,7 @@ function parseParamNames(signature: string): string[] { } .return-cell code { - color: var(--color-primary-gradient-start); + color: var(--color-accent); font-weight: 600; } diff --git a/purplex/client/src/components/ui/StatusBadge.vue b/purplex/client/src/components/ui/StatusBadge.vue index 3839b84d..ed11dde9 100644 --- a/purplex/client/src/components/ui/StatusBadge.vue +++ b/purplex/client/src/components/ui/StatusBadge.vue @@ -80,9 +80,9 @@ const displayLabel = computed(() => props.label || props.value); /* Admin - gradient purple (matches AdminUsers badge) */ .status-admin { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); color: var(--color-text-on-filled); - box-shadow: 0 2px 8px var(--color-primary-glow); + box-shadow: 0 2px 8px var(--color-accent-glow); border: none; } diff --git a/purplex/client/src/components/ui/TerminalDisplay.vue b/purplex/client/src/components/ui/TerminalDisplay.vue index 05fad05a..81d0ee09 100644 --- a/purplex/client/src/components/ui/TerminalDisplay.vue +++ b/purplex/client/src/components/ui/TerminalDisplay.vue @@ -106,11 +106,11 @@ defineProps() } .terminal-input { - color: var(--color-primary-gradient-start); + color: var(--color-accent); } .input-marker { - color: var(--color-primary-gradient-start); + color: var(--color-accent); font-weight: 700; user-select: none; flex-shrink: 0; diff --git a/purplex/client/src/features/auth/Login.vue b/purplex/client/src/features/auth/Login.vue index f44ba219..c395eff2 100644 --- a/purplex/client/src/features/auth/Login.vue +++ b/purplex/client/src/features/auth/Login.vue @@ -388,7 +388,7 @@ export default { font-family: 'Exo 2', sans-serif; font-size: var(--font-size-title); margin: 0; - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 50%, var(--color-admin-hover) 100%); + background: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 50%, var(--color-admin-hover) 100%); -webkit-text-fill-color: transparent; /* stylelint-disable-next-line property-no-vendor-prefix -- Safari requires -webkit- prefix for gradient text */ -webkit-background-clip: text; @@ -467,8 +467,8 @@ input { input:focus { outline: none; - border-color: var(--color-primary-gradient-start); - box-shadow: 0 0 0 3px var(--color-primary-overlay); + border-color: var(--color-focus-ring); + box-shadow: 0 0 0 3px var(--color-accent-overlay); } input::placeholder { @@ -497,9 +497,9 @@ input::placeholder { } .login-btns button:first-child { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); color: var(--color-text-on-filled); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } .login-btns button:nth-child(2) { @@ -532,17 +532,17 @@ input::placeholder { .login-btns button:first-child:hover { transform: translateY(-2px); - box-shadow: 0 6px 20px var(--color-primary-glow); + box-shadow: 0 6px 20px var(--color-accent-glow); } .login-btns button:nth-child(2):hover { background: var(--color-bg-input); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); color: var(--color-text-primary); } .login-btns button:nth-child(3):hover { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); transform: translateY(-1px); } @@ -564,7 +564,7 @@ input::placeholder { .redirect-mode-btn:hover { background: var(--color-bg-input) !important; - border-color: var(--color-primary-gradient-start) !important; + border-color: var(--color-accent) !important; color: var(--color-text-primary) !important; } @@ -580,13 +580,13 @@ input::placeholder { } .info-message { - background: var(--color-primary-overlay); + background: var(--color-accent-overlay); color: var(--color-text-secondary); padding: var(--spacing-md); border-radius: var(--radius-base); text-align: center; font-size: var(--font-size-sm); - border: 1px solid var(--color-primary-glow); + border: 1px solid var(--color-accent-glow); line-height: 1.5; } diff --git a/purplex/client/src/features/editor/Editor.vue b/purplex/client/src/features/editor/Editor.vue index 72149e92..0d003d4c 100644 --- a/purplex/client/src/features/editor/Editor.vue +++ b/purplex/client/src/features/editor/Editor.vue @@ -355,7 +355,7 @@ defineExpose({ } :deep(.ace_editor:hover) { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); box-shadow: var(--shadow-lg); } @@ -395,7 +395,7 @@ defineExpose({ } :deep(.ace_scrollbar::-webkit-scrollbar-thumb:hover) { - background: var(--color-primary-gradient-start); + background: var(--color-accent); } /* Selection styling */ @@ -406,26 +406,26 @@ defineExpose({ /* Active line highlighting */ :deep(.ace_active-line) { - background: var(--color-primary-overlay); + background: var(--color-accent-overlay); } /* Cursor styling */ :deep(.ace_cursor) { - color: var(--color-primary-gradient-start); - border-left: 2px solid var(--color-primary-gradient-start); + color: var(--color-accent); + border-left: 2px solid var(--color-accent); } /* Bracket matching */ :deep(.ace_bracket) { margin: -1px -1px 0; - border: 1px solid var(--color-primary-gradient-start); - background: var(--color-primary-overlay); + border: 1px solid var(--color-accent); + background: var(--color-accent-overlay); } /* Search highlights */ :deep(.ace_selected-word) { - border: 1px solid var(--color-primary-gradient-start); - background: var(--color-primary-overlay); + border: 1px solid var(--color-accent); + background: var(--color-accent-overlay); } @@ -441,7 +441,7 @@ defineExpose({ } :deep(.ace_fold-widget:hover) { - color: var(--color-primary-gradient-start); + color: var(--color-accent); } /* Indent guides */ diff --git a/purplex/client/src/features/problems/ProblemSet.vue b/purplex/client/src/features/problems/ProblemSet.vue index 18315b94..5e61121f 100644 --- a/purplex/client/src/features/problems/ProblemSet.vue +++ b/purplex/client/src/features/problems/ProblemSet.vue @@ -2078,11 +2078,11 @@ export default { .progress-bar-fill { height: 100%; background: linear-gradient(90deg, - var(--color-primary-gradient-start) 0%, - var(--color-primary-gradient-end) 100%); + var(--color-gradient-start) 0%, + var(--color-gradient-end) 100%); animation: progressSlide 1.5s ease-in-out infinite; transform-origin: left; - box-shadow: 0 0 10px var(--color-primary-glow); + box-shadow: 0 0 10px var(--color-accent-glow); } @keyframes progressSlide { @@ -2224,12 +2224,12 @@ export default { } .nav-button:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } .nav-button:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -2242,9 +2242,9 @@ export default { } .nav-button:hover:not(:disabled) { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); color: var(--color-text-on-filled); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); transform: translateY(-1px); } @@ -2327,7 +2327,7 @@ export default { } .progress-bar.submitting { - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); position: relative; overflow: hidden; } @@ -2387,7 +2387,7 @@ export default { /* Active state */ .progress-bar.active { - box-shadow: 0 0 0 2px var(--color-bg-panel), 0 0 0 4px var(--color-primary-gradient-start); + box-shadow: 0 0 0 2px var(--color-bg-panel), 0 0 0 4px var(--color-accent); } .progress-bar.active.not_started { @@ -2406,7 +2406,7 @@ export default { } .progress-bar.active.submitting { - box-shadow: 0 0 0 2px var(--color-bg-panel), 0 0 0 4px var(--color-primary-gradient-start); + box-shadow: 0 0 0 2px var(--color-bg-panel), 0 0 0 4px var(--color-accent); } /* Hover effects */ @@ -2657,12 +2657,12 @@ export default { } .zoom-btn:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } .zoom-btn:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -2673,7 +2673,7 @@ export default { .zoom-btn:hover:not(:disabled) { background: var(--color-bg-input); color: var(--color-text-primary); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); } .zoom-icon { @@ -2708,7 +2708,7 @@ export default { .toolbar-btn:hover { background: var(--color-bg-input); color: var(--color-text-primary); - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); } .toolbar-options { @@ -2855,12 +2855,12 @@ export default { /* Focus styles for keyboard navigation */ .progress-bar:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } .progress-bar:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -2869,12 +2869,12 @@ export default { } .toolbar-btn:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } .toolbar-btn:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } diff --git a/purplex/client/src/modals/AccountModal.vue b/purplex/client/src/modals/AccountModal.vue index 6c5551be..749041f4 100644 --- a/purplex/client/src/modals/AccountModal.vue +++ b/purplex/client/src/modals/AccountModal.vue @@ -262,7 +262,7 @@ function getMemberSince(): string { .avatar { width: 64px; height: 64px; - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); border-radius: var(--radius-circle); display: flex; align-items: center; @@ -270,7 +270,7 @@ function getMemberSince(): string { font-size: var(--font-size-xl); font-weight: 600; color: var(--color-text-on-filled); - box-shadow: 0 4px 12px var(--color-primary-glow); + box-shadow: 0 4px 12px var(--color-accent-glow); } .user-info { diff --git a/purplex/client/src/modals/CorrectnessModal.vue b/purplex/client/src/modals/CorrectnessModal.vue index 28caf54b..2adc752d 100644 --- a/purplex/client/src/modals/CorrectnessModal.vue +++ b/purplex/client/src/modals/CorrectnessModal.vue @@ -762,12 +762,12 @@ onMounted(() => { } .version-pill:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } .version-pill.active { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); background: var(--color-bg-hover); } @@ -864,7 +864,7 @@ h4.section-title { } .code-accessible:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: -2px; } @@ -906,7 +906,7 @@ ul.tests-list { } .test-item:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: -2px; background: var(--color-bg-hover); } diff --git a/purplex/client/src/modals/CourseEnrollmentModal.vue b/purplex/client/src/modals/CourseEnrollmentModal.vue index de70f508..46207fca 100644 --- a/purplex/client/src/modals/CourseEnrollmentModal.vue +++ b/purplex/client/src/modals/CourseEnrollmentModal.vue @@ -246,8 +246,8 @@ async function enrollInCourse(): Promise { .course-input:focus { outline: none; - border-color: var(--color-primary-gradient-start); - box-shadow: 0 0 0 3px var(--color-primary-overlay); + border-color: var(--color-focus-ring); + box-shadow: 0 0 0 3px var(--color-accent-overlay); } .course-input:disabled { @@ -258,7 +258,7 @@ async function enrollInCourse(): Promise { .lookup-btn { padding: var(--spacing-md) var(--spacing-lg); - background: linear-gradient(135deg, var(--color-primary-gradient-start) 0%, var(--color-primary-gradient-end) 100%); + background: var(--gradient-primary); color: var(--color-text-on-filled); border: none; border-radius: var(--radius-base); @@ -266,7 +266,7 @@ async function enrollInCourse(): Promise { cursor: pointer; transition: var(--transition-base); white-space: nowrap; - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } .lookup-btn:disabled { @@ -279,7 +279,7 @@ async function enrollInCourse(): Promise { .lookup-btn:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: 0 6px 20px var(--color-primary-glow); + box-shadow: 0 6px 20px var(--color-accent-glow); } .course-preview { @@ -347,7 +347,7 @@ async function enrollInCourse(): Promise { font-weight: 600; cursor: pointer; transition: var(--transition-base); - box-shadow: var(--shadow-colored); + box-shadow: var(--shadow-accent); } .enroll-btn:disabled { diff --git a/purplex/client/src/modals/ViewSubmissionModal.vue b/purplex/client/src/modals/ViewSubmissionModal.vue index 3547b0d6..2f5ce3f9 100644 --- a/purplex/client/src/modals/ViewSubmissionModal.vue +++ b/purplex/client/src/modals/ViewSubmissionModal.vue @@ -960,8 +960,8 @@ watch(() => props.submission, () => { .expand-btn:hover { background: var(--color-bg-input); - border-color: var(--color-primary-gradient-start); - color: var(--color-primary-gradient-start); + border-color: var(--color-accent); + color: var(--color-accent); } .type-badge { @@ -978,7 +978,7 @@ watch(() => props.submission, () => { .download-btn { padding: 6px 12px; - background: var(--color-primary-gradient-start); + background: var(--color-accent); color: var(--color-text-on-filled); border: none; border-radius: 4px; @@ -994,7 +994,7 @@ watch(() => props.submission, () => { } .download-btn:hover:not(:disabled) { - background: var(--color-primary-gradient-end); + background: var(--color-accent-strong); } /* Metrics Bar */ @@ -1429,7 +1429,7 @@ details[open] .group-icon { min-width: 28px; height: 28px; padding: 0 8px; - background: linear-gradient(135deg, var(--color-segment-1) 0%, var(--color-primary-gradient-start) 100%); + background: linear-gradient(135deg, var(--color-segment-1) 0%, var(--color-gradient-start) 100%); color: var(--color-text-on-filled); font-size: 16px; font-weight: 700; @@ -1475,8 +1475,8 @@ details[open] .group-icon { } .hint-item:hover { - border-color: var(--color-primary-gradient-start); - box-shadow: 0 2px 8px var(--color-primary-overlay); + border-color: var(--color-accent); + box-shadow: 0 2px 8px var(--color-accent-overlay); } .hint-icon { @@ -1546,7 +1546,7 @@ details[open] .group-icon { .resize-handle:hover::before, .resize-handle:active::before { opacity: 1; - background: var(--color-primary-gradient-start); + background: var(--color-accent); width: 3px; } diff --git a/purplex/client/src/style.css b/purplex/client/src/style.css index 6ca24a51..8203ced0 100644 --- a/purplex/client/src/style.css +++ b/purplex/client/src/style.css @@ -23,13 +23,13 @@ body { a { font-weight: 500; - color: var(--color-primary-gradient-start); + color: var(--color-accent); text-decoration: inherit; transition: var(--transition-fast); } a:hover { - color: var(--color-primary-gradient-end); + color: var(--color-accent-strong); } h1 { @@ -62,13 +62,13 @@ button { } button:hover { - border-color: var(--color-primary-gradient-start); + border-color: var(--color-accent); background-color: var(--color-bg-hover); } button:focus, button:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } @@ -95,7 +95,7 @@ input:focus, textarea:focus, select:focus { outline: none; - border-color: var(--color-primary-gradient-start); + border-color: var(--color-focus-ring); } /* Ensure placeholder text is visible */ diff --git a/purplex/client/src/styles/hints.css b/purplex/client/src/styles/hints.css index 37571f01..608ceb88 100644 --- a/purplex/client/src/styles/hints.css +++ b/purplex/client/src/styles/hints.css @@ -84,7 +84,7 @@ var(--color-info-overlay) 95%, transparent 100% ); - border-left: 3px solid var(--color-primary-gradient-start); + border-left: 3px solid var(--color-accent); margin: 0 -var(--spacing-md); padding: 0 var(--spacing-md); } @@ -99,7 +99,7 @@ } .variable-meaningful { - color: var(--color-primary-gradient-start); + color: var(--color-accent); font-weight: 600; background: var(--color-info-overlay); padding: 0 var(--spacing-xs); @@ -150,7 +150,7 @@ } 50% { - background-color: var(--color-primary-glow); + background-color: var(--color-accent-glow); } } @@ -177,7 +177,7 @@ } .subgoal-dot.active { - background: var(--color-primary-gradient-start); + background: var(--color-accent); width: 24px; border-radius: 4px; } @@ -197,8 +197,8 @@ height: 100%; background: linear-gradient( to bottom, - var(--color-primary-gradient-start), - var(--color-primary-gradient-end) + var(--color-gradient-start), + var(--color-gradient-end) ); opacity: 0; transition: opacity 0.3s ease; @@ -213,14 +213,14 @@ display: inline-flex; align-items: center; gap: var(--spacing-xs); - color: var(--color-primary-gradient-start); + color: var(--color-accent); text-decoration: none; font-weight: 500; transition: var(--transition-base); } .python-tutor-link:hover { - color: var(--color-primary-gradient-end); + color: var(--color-accent-strong); text-decoration: underline; } @@ -240,7 +240,7 @@ width: 40px; height: 40px; border: 3px solid var(--color-bg-border); - border-top-color: var(--color-primary-gradient-start); + border-top-color: var(--color-accent); border-radius: 50%; animation: spin 0.8s linear infinite; } @@ -305,7 +305,7 @@ /* Accessibility improvements */ .hint-button:focus-visible, .hint-navigation button:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } diff --git a/purplex/client/src/styles/segmentation.css b/purplex/client/src/styles/segmentation.css index c3ae249c..6b0d7070 100644 --- a/purplex/client/src/styles/segmentation.css +++ b/purplex/client/src/styles/segmentation.css @@ -291,22 +291,22 @@ } .seg-connection.active { - stroke: var(--color-primary-gradient-start); + stroke: var(--color-accent); stroke-width: 3; opacity: 1; - filter: drop-shadow(0 0 4px var(--color-primary-gradient-start)); + filter: drop-shadow(0 0 4px var(--color-accent)); } .seg-connection.highlighted { - stroke: var(--color-primary-gradient-start); + stroke: var(--color-accent); stroke-width: 4; opacity: 1; } /* Segmentation Code Highlighting */ .seg-code-highlight { - background: var(--color-primary-overlay); - border-left: 3px solid var(--color-primary-gradient-start); + background: var(--color-accent-overlay); + border-left: 3px solid var(--color-accent); padding-left: calc(var(--spacing-sm) - 3px); transition: var(--seg-transition-fast); } @@ -343,7 +343,7 @@ } .seg-interactive.selected { - box-shadow: 0 0 0 2px var(--color-primary-gradient-start); + box-shadow: 0 0 0 2px var(--color-accent); } /* Segmentation Tooltips */ @@ -453,12 +453,12 @@ /* Accessibility Improvements */ .seg-progress-block:focus { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } .seg-interactive:focus-visible { - outline: 2px solid var(--color-primary-gradient-start); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } diff --git a/purplex/client/src/styles/tokens.css b/purplex/client/src/styles/tokens.css index 37ced93e..4187c142 100644 --- a/purplex/client/src/styles/tokens.css +++ b/purplex/client/src/styles/tokens.css @@ -59,7 +59,7 @@ --font-mono: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace; /* Focus Ring (resolves per theme) */ - --color-focus-ring: var(--color-primary-gradient-start); + --color-focus-ring: var(--color-accent); } :root, @@ -69,11 +69,15 @@ /* Primary Colors */ --color-primary: #800080; --color-primary-hover: #9b009b; - --color-primary-gradient-start: #667eea; - --color-primary-gradient-end: #764ba2; - --color-logo-start: #a78bfa; - --color-logo-mid: #c4b5fd; - --color-logo-end: #8b7ec8; + --color-accent: #667eea; + --color-accent-strong: #764ba2; + + /* Gradients (stops only inside gradients; prefer the whole-gradient tokens) */ + --color-gradient-start: #667eea; + --color-gradient-end: #764ba2; + --gradient-primary: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 100%); + --gradient-admin: linear-gradient(135deg, var(--color-admin) 0%, var(--color-gradient-end) 100%); + --gradient-logo: linear-gradient(135deg, #a78bfa, #c4b5fd, #8b7ec8); /* Background Colors */ --color-bg-main: #242424; @@ -134,9 +138,9 @@ --color-error-overlay: rgb(220 53 69 / 15%); --color-info-overlay: rgb(33 150 243 / 15%); - /* Primary Overlays */ - --color-primary-overlay: rgb(102 126 234 / 10%); - --color-primary-glow: rgb(102 126 234 / 30%); + /* Accent Overlays */ + --color-accent-overlay: rgb(102 126 234 / 10%); + --color-accent-glow: rgb(102 126 234 / 30%); /* Status Gradient Endpoints */ --color-success-accent: #34d399; @@ -162,7 +166,7 @@ --color-backdrop-heavy: rgb(0 0 0 / 75%); /* Colored Glows (box-shadows) */ - --color-primary-shadow: rgb(102 126 234 / 40%); + --color-accent-shadow: rgb(102 126 234 / 40%); --color-admin-shadow: rgb(103 58 183 / 30%); --color-info-shadow: rgb(59 130 246 / 30%); --color-segment-shadow: rgb(159 122 234 / 30%); @@ -187,7 +191,7 @@ --shadow-base: 0 4px 12px rgb(0 0 0 / 30%); --shadow-md: 0 4px 10px rgb(0 0 0 / 30%); --shadow-lg: 0 8px 25px rgb(0 0 0 / 40%); - --shadow-colored: 0 4px 15px rgb(102 126 234 / 30%); + --shadow-accent: 0 4px 15px rgb(102 126 234 / 30%); /* One-off shadows */ --shadow-nav: 0 2px 10px rgb(0 0 0 / 30%); @@ -209,11 +213,15 @@ /* Primary Colors */ --color-primary: #6b006b; --color-primary-hover: #570057; - --color-primary-gradient-start: #4f46e5; - --color-primary-gradient-end: #6d28d9; - --color-logo-start: #7c3aed; - --color-logo-mid: #8b5cf6; - --color-logo-end: #6d28d9; + --color-accent: #4f46e5; + --color-accent-strong: #6d28d9; + + /* Gradients (stops only inside gradients; prefer the whole-gradient tokens) */ + --color-gradient-start: #4f46e5; + --color-gradient-end: #6d28d9; + --gradient-primary: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 100%); + --gradient-admin: linear-gradient(135deg, var(--color-admin) 0%, var(--color-gradient-end) 100%); + --gradient-logo: linear-gradient(135deg, #7c3aed, #8b5cf6, #6d28d9); /* Background Colors */ --color-bg-main: #f8f9fa; @@ -274,9 +282,9 @@ --color-error-overlay: rgb(220 38 38 / 8%); --color-info-overlay: rgb(29 78 216 / 8%); - /* Primary Overlays */ - --color-primary-overlay: rgb(79 70 229 / 7%); - --color-primary-glow: rgb(79 70 229 / 15%); + /* Accent Overlays */ + --color-accent-overlay: rgb(79 70 229 / 7%); + --color-accent-glow: rgb(79 70 229 / 15%); /* Status Gradient Endpoints */ --color-success-accent: #059669; @@ -302,7 +310,7 @@ --color-backdrop-heavy: rgb(0 0 0 / 60%); /* Colored Glows (~50% opacity reduction) */ - --color-primary-shadow: rgb(79 70 229 / 20%); + --color-accent-shadow: rgb(79 70 229 / 20%); --color-admin-shadow: rgb(91 33 182 / 15%); --color-info-shadow: rgb(29 78 216 / 15%); --color-segment-shadow: rgb(124 58 237 / 15%); @@ -327,7 +335,7 @@ --shadow-base: 0 4px 12px rgb(0 0 0 / 8%); --shadow-md: 0 4px 10px rgb(0 0 0 / 8%); --shadow-lg: 0 8px 25px rgb(0 0 0 / 10%); - --shadow-colored: 0 4px 15px rgb(79 70 229 / 15%); + --shadow-accent: 0 4px 15px rgb(79 70 229 / 15%); /* One-off shadows */ --shadow-nav: 0 2px 10px rgb(0 0 0 / 6%); From 836c0f403477a6572aa9bc719804504fbe3cc897 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 17 Oct 2026 11:12:12 -0400 Subject: [PATCH 5/9] Theme step 4: rename surface tokens and the categorical ramp Rename the tokens whose names say the wrong thing: --color-bg-border is the line colour, now --color-border (256 uses); --color-bg-dark is the sunken well, now --color-bg-sunken; --color-segment-1..6 become --color-category-1..6, the one categorical ramp. --color-bg-panel-light had one consumer (segmentation.css) and merges into --color-bg-panel, which moves that panel from #1f1f1f to #1e1e1e in dark (light unchanged). The two getComputedStyle reads of the ramp now fall back to the token itself instead of the dark-only #9f7aea, and the unused connectionColor data field with its dark-only #667eea is gone. The unused .bg-dark utility class is deleted. The theme check's dynamic prefix follows the ramp to --color-category-. The poisoned var(--color-border, ...) and var(--color-surface, ...) fallbacks were already rewritten in step 2. Co-Authored-By: Claude Opus 5.5 --- purplex/client/scripts/check-theme-tokens.mjs | 4 +- purplex/client/src/App.vue | 4 +- purplex/client/src/components/AdminUsers.vue | 2 +- .../client/src/components/CourseDetail.vue | 4 +- purplex/client/src/components/Feedback.vue | 4 +- purplex/client/src/components/HintButton.vue | 12 ++--- purplex/client/src/components/Home.vue | 2 +- .../src/components/LanguageSwitcher.vue | 4 +- purplex/client/src/components/NavBar.vue | 4 +- .../client/src/components/ThemeSwitcher.vue | 2 +- .../activities/feedback/AbstractionCard.vue | 4 +- .../feedback/CodeSubmissionFeedback.vue | 6 +-- .../activities/feedback/McqFeedback.vue | 6 +-- .../activities/feedback/RefuteFeedback.vue | 2 +- .../activities/inputs/DebugFixInput.vue | 4 +- .../activities/inputs/DescriptionInput.vue | 2 +- .../components/activities/inputs/McqInput.vue | 4 +- .../activities/inputs/ProbeableCodeInput.vue | 2 +- .../activities/inputs/ProbeableSpecInput.vue | 2 +- .../activities/inputs/RefuteInput.vue | 4 +- .../activities/inputs/shared/ProbePanel.vue | 10 ++-- .../admin/editors/DebugFixProblemEditor.vue | 28 +++++----- .../admin/editors/EiplProblemEditor.vue | 52 +++++++++---------- .../admin/editors/McqProblemEditor.vue | 14 ++--- .../editors/ProbeableCodeProblemEditor.vue | 8 +-- .../editors/ProbeableSpecProblemEditor.vue | 18 +++---- .../admin/editors/PromptProblemEditor.vue | 24 ++++----- .../admin/editors/RefuteProblemEditor.vue | 16 +++--- .../admin/editors/shared/BasicInfoSection.vue | 10 ++-- .../admin/editors/shared/EditorToolbar.vue | 4 +- .../editors/shared/ProbeSettingsSection.vue | 10 ++-- .../shared/SegmentationConfigSection.vue | 16 +++--- .../admin/editors/shared/TestCasesSection.vue | 18 +++---- .../content/ContentEditorLayout.vue | 4 +- .../components/content/CourseEditorShell.vue | 6 +-- .../src/components/content/CourseList.vue | 2 +- .../content/CourseProblemSetsPage.vue | 12 ++--- .../components/content/CourseStudentsPage.vue | 4 +- .../components/content/ProblemEditorShell.vue | 12 ++--- .../src/components/content/ProblemList.vue | 2 +- .../content/ProblemSetEditorShell.vue | 24 ++++----- .../components/content/ProblemSetManager.vue | 2 +- .../hints/SuggestedTraceOverlay.vue | 6 +-- .../instructor/CourseTeamManager.vue | 10 ++-- .../instructor/InstructorCourseOverview.vue | 14 ++--- .../instructor/InstructorDashboard.vue | 16 +++--- .../src/components/privacy/AIConsentModal.vue | 2 +- .../components/privacy/AccountDeletion.vue | 2 +- .../client/src/components/privacy/AgeGate.vue | 4 +- .../src/components/privacy/ConsentForm.vue | 4 +- .../src/components/privacy/CookieConsent.vue | 4 +- .../src/components/privacy/DataExport.vue | 2 +- .../components/privacy/PrivacySettings.vue | 4 +- .../segmentation/SegmentAnalysisModal.vue | 6 +-- .../segmentation/SegmentMapping.vue | 15 +++--- .../client/src/components/ui/AsyncError.vue | 2 +- .../src/components/ui/ConfirmDialog.vue | 2 +- .../src/components/ui/FunctionCallTable.vue | 6 +-- .../client/src/components/ui/StatusBadge.vue | 2 +- .../src/components/ui/TerminalDisplay.vue | 4 +- purplex/client/src/features/auth/Login.vue | 8 +-- purplex/client/src/features/editor/Editor.vue | 6 +-- .../src/features/problems/ProblemSet.vue | 10 ++-- purplex/client/src/modals/AccountModal.vue | 4 +- .../client/src/modals/CorrectnessModal.vue | 12 ++--- .../src/modals/CourseEnrollmentModal.vue | 2 +- purplex/client/src/modals/PyTutorModal.vue | 10 ++-- .../client/src/modals/ViewSubmissionModal.vue | 8 +-- purplex/client/src/style.css | 12 ++--- purplex/client/src/styles/hints.css | 8 +-- purplex/client/src/styles/segmentation.css | 8 +-- purplex/client/src/styles/tokens.css | 38 +++++++------- 72 files changed, 299 insertions(+), 306 deletions(-) diff --git a/purplex/client/scripts/check-theme-tokens.mjs b/purplex/client/scripts/check-theme-tokens.mjs index 2d8c9f81..c90a08fa 100644 --- a/purplex/client/scripts/check-theme-tokens.mjs +++ b/purplex/client/scripts/check-theme-tokens.mjs @@ -37,8 +37,8 @@ const NON_COLOUR_PREFIXES = ['--spacing-', '--radius-', '--font-', '--transition /** Per-instance variables set from inline `:style` bindings, not from CSS. */ const INLINE_VARIABLES = [/^--modal-/, /^--param-count$/, /^--segment-bg-color$/]; -/** Prefixes read with a computed name, e.g. getPropertyValue(`--color-segment-${n}`). */ -const DYNAMIC_PREFIXES = ['--color-segment-']; +/** Prefixes read with a computed name, e.g. getPropertyValue(`--color-category-${n}`). */ +const DYNAMIC_PREFIXES = ['--color-category-']; /** * Problems that exist today and are scheduled to be fixed. Each entry is diff --git a/purplex/client/src/App.vue b/purplex/client/src/App.vue index 60d1ba92..1cf6463b 100644 --- a/purplex/client/src/App.vue +++ b/purplex/client/src/App.vue @@ -291,7 +291,7 @@ export default defineComponent({ } .divider { - border-top: 3px solid var(--color-bg-dark); + border-top: 3px solid var(--color-bg-sunken); } /* Common utility classes */ @@ -365,7 +365,7 @@ export default defineComponent({ right: 0; width: 100%; background-color: var(--color-bg-header); - border-top: 1px solid var(--color-bg-border); + border-top: 1px solid var(--color-border); padding: var(--spacing-md) var(--spacing-xl); z-index: 1000; box-sizing: border-box; diff --git a/purplex/client/src/components/AdminUsers.vue b/purplex/client/src/components/AdminUsers.vue index 3e925248..254ea6d4 100644 --- a/purplex/client/src/components/AdminUsers.vue +++ b/purplex/client/src/components/AdminUsers.vue @@ -294,7 +294,7 @@ onMounted(() => { width: 100%; padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-panel); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-sm); diff --git a/purplex/client/src/components/CourseDetail.vue b/purplex/client/src/components/CourseDetail.vue index dd031872..2731626b 100644 --- a/purplex/client/src/components/CourseDetail.vue +++ b/purplex/client/src/components/CourseDetail.vue @@ -351,7 +351,7 @@ onMounted(async () => { .problem-set-card { background: var(--color-bg-panel); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-lg); padding: 0; cursor: pointer; @@ -474,7 +474,7 @@ onMounted(async () => { background-color: var(--color-bg-panel); border-radius: var(--radius-lg); color: var(--color-text-muted); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Error State */ diff --git a/purplex/client/src/components/Feedback.vue b/purplex/client/src/components/Feedback.vue index 2c278617..e95fe771 100644 --- a/purplex/client/src/components/Feedback.vue +++ b/purplex/client/src/components/Feedback.vue @@ -1019,7 +1019,7 @@ export default defineComponent({ .attempt-indicator { width: 2px; height: 12px; - background: var(--color-bg-border); + background: var(--color-border); border-radius: 1px; position: absolute; left: 2px; @@ -1128,7 +1128,7 @@ export default defineComponent({ /* Progress Bar */ .progress-bar-container { height: 8px; - background: var(--color-bg-dark); + background: var(--color-bg-sunken); border-radius: var(--radius-xs); overflow: hidden; margin-bottom: var(--spacing-md); diff --git a/purplex/client/src/components/HintButton.vue b/purplex/client/src/components/HintButton.vue index 559242ff..8938ebcb 100644 --- a/purplex/client/src/components/HintButton.vue +++ b/purplex/client/src/components/HintButton.vue @@ -694,7 +694,7 @@ export default { justify-content: space-between; align-items: center; padding: 16px; - border-bottom: 1px solid var(--color-bg-border); + border-bottom: 1px solid var(--color-border); } .hint-menu-header h4 { @@ -817,7 +817,7 @@ export default { .hint-content { background: var(--color-bg-section); - border-top: 1px solid var(--color-bg-border); + border-top: 1px solid var(--color-border); } .hint-content-body { @@ -845,7 +845,7 @@ export default { .hint-example pre { margin: 0; - background: var(--color-bg-dark); + background: var(--color-bg-sunken); color: var(--color-text-primary); padding: 12px; border-radius: 4px; @@ -856,7 +856,7 @@ export default { .hint-footer { padding: 12px 16px; background: var(--color-bg-section); - border-top: 1px solid var(--color-bg-border); + border-top: 1px solid var(--color-border); display: flex; flex-direction: column; gap: 12px; @@ -890,7 +890,7 @@ export default { } .action-btn.secondary:hover:not(:disabled) { - background: var(--color-bg-border); + background: var(--color-border); } .action-btn.danger { @@ -945,7 +945,7 @@ export default { position: absolute; cursor: pointer; inset: 0; - background-color: var(--color-bg-border); + background-color: var(--color-border); transition: 0.3s; border-radius: 24px; } diff --git a/purplex/client/src/components/Home.vue b/purplex/client/src/components/Home.vue index fa822e4e..fa547226 100644 --- a/purplex/client/src/components/Home.vue +++ b/purplex/client/src/components/Home.vue @@ -320,7 +320,7 @@ main { .problem-set-card { background: var(--color-bg-panel); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-lg); padding: 0; cursor: pointer; diff --git a/purplex/client/src/components/LanguageSwitcher.vue b/purplex/client/src/components/LanguageSwitcher.vue index 7e7b5830..343de931 100644 --- a/purplex/client/src/components/LanguageSwitcher.vue +++ b/purplex/client/src/components/LanguageSwitcher.vue @@ -123,7 +123,7 @@ async function handleLanguageChange() { font-family: inherit; color: var(--color-text-primary); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-base); cursor: pointer; transition: var(--transition-fast); @@ -150,7 +150,7 @@ async function handleLanguageChange() { } .language-select option { - background: var(--color-bg-dark); + background: var(--color-bg-sunken); color: var(--color-text-primary); padding: var(--spacing-sm); } diff --git a/purplex/client/src/components/NavBar.vue b/purplex/client/src/components/NavBar.vue index 12fcd559..9431c8af 100644 --- a/purplex/client/src/components/NavBar.vue +++ b/purplex/client/src/components/NavBar.vue @@ -452,8 +452,8 @@ onUnmounted(() => { /* Account button styling */ .account-item { - background: var(--color-bg-dark); - border-color: var(--color-bg-border); + background: var(--color-bg-sunken); + border-color: var(--color-border); } .account-item:hover { diff --git a/purplex/client/src/components/ThemeSwitcher.vue b/purplex/client/src/components/ThemeSwitcher.vue index 7ad093e0..6811128d 100644 --- a/purplex/client/src/components/ThemeSwitcher.vue +++ b/purplex/client/src/components/ThemeSwitcher.vue @@ -95,7 +95,7 @@ const options = computed<{ value: ThemePreference; label: string }[]>(() => [ .theme-option.active { background: var(--color-bg-panel); color: var(--color-text-primary); - box-shadow: var(--shadow-sm), 0 0 0 1px var(--color-bg-border); + box-shadow: var(--shadow-sm), 0 0 0 1px var(--color-border); } @media (width <= 480px) { diff --git a/purplex/client/src/components/activities/feedback/AbstractionCard.vue b/purplex/client/src/components/activities/feedback/AbstractionCard.vue index f6acf983..ded802e1 100644 --- a/purplex/client/src/components/activities/feedback/AbstractionCard.vue +++ b/purplex/client/src/components/activities/feedback/AbstractionCard.vue @@ -253,7 +253,7 @@ defineExpose({ open }) .progress-bar-container { height: 8px; - background: var(--color-bg-dark); + background: var(--color-bg-sunken); border-radius: var(--radius-xs); overflow: hidden; margin-bottom: var(--spacing-md); @@ -274,7 +274,7 @@ defineExpose({ open }) } .progress-bar-fill.bar-locked { - background: var(--color-bg-border); + background: var(--color-border); } .card-status { diff --git a/purplex/client/src/components/activities/feedback/CodeSubmissionFeedback.vue b/purplex/client/src/components/activities/feedback/CodeSubmissionFeedback.vue index 86e88e6a..b3c70012 100644 --- a/purplex/client/src/components/activities/feedback/CodeSubmissionFeedback.vue +++ b/purplex/client/src/components/activities/feedback/CodeSubmissionFeedback.vue @@ -638,7 +638,7 @@ onBeforeUnmount(() => { background-size: 200% 100%; border-radius: var(--radius-base); animation: shimmer 2.5s ease-in-out infinite; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .skeleton-test-item { @@ -652,7 +652,7 @@ onBeforeUnmount(() => { background-size: 200% 100%; border-radius: var(--radius-xs); animation: shimmer 2.5s ease-in-out infinite; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } @keyframes shimmer { @@ -855,7 +855,7 @@ onBeforeUnmount(() => { .attempt-indicator { width: 2px; height: 12px; - background: var(--color-bg-border); + background: var(--color-border); border-radius: 1px; position: absolute; left: 2px; diff --git a/purplex/client/src/components/activities/feedback/McqFeedback.vue b/purplex/client/src/components/activities/feedback/McqFeedback.vue index d9b97d86..f1104715 100644 --- a/purplex/client/src/components/activities/feedback/McqFeedback.vue +++ b/purplex/client/src/components/activities/feedback/McqFeedback.vue @@ -205,7 +205,7 @@ const bannerClass = computed(() => { .loading-spinner { width: 32px; height: 32px; - border: 3px solid var(--color-bg-border); + border: 3px solid var(--color-border); border-top-color: var(--color-accent); border-radius: var(--radius-circle); animation: spin 1s linear infinite; @@ -317,7 +317,7 @@ const bannerClass = computed(() => { border-radius: var(--radius-base); font-size: var(--font-size-base); color: var(--color-text-primary); - border-left: 4px solid var(--color-bg-border); + border-left: 4px solid var(--color-border); } .answer-value--correct { @@ -336,7 +336,7 @@ const bannerClass = computed(() => { .explanation-section { margin-top: var(--spacing-md); padding-top: var(--spacing-md); - border-top: 1px solid var(--color-bg-border); + border-top: 1px solid var(--color-border); } .explanation-text { diff --git a/purplex/client/src/components/activities/feedback/RefuteFeedback.vue b/purplex/client/src/components/activities/feedback/RefuteFeedback.vue index c013ea8e..e208847c 100644 --- a/purplex/client/src/components/activities/feedback/RefuteFeedback.vue +++ b/purplex/client/src/components/activities/feedback/RefuteFeedback.vue @@ -236,7 +236,7 @@ const resultInterpretation = computed(() => { .loading-spinner { width: 32px; height: 32px; - border: 3px solid var(--color-bg-border); + border: 3px solid var(--color-border); border-top-color: var(--color-accent); border-radius: var(--radius-circle); animation: spin 1s linear infinite; diff --git a/purplex/client/src/components/activities/inputs/DebugFixInput.vue b/purplex/client/src/components/activities/inputs/DebugFixInput.vue index 6bbe1ef5..5c2a0a01 100644 --- a/purplex/client/src/components/activities/inputs/DebugFixInput.vue +++ b/purplex/client/src/components/activities/inputs/DebugFixInput.vue @@ -202,7 +202,7 @@ function handleReset() { .code-editor-wrapper { padding: 0; background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); margin: var(--spacing-md) var(--spacing-xl); margin-bottom: 0; border-radius: var(--radius-base); @@ -226,7 +226,7 @@ function handleReset() { padding: var(--spacing-md) var(--spacing-lg); background: var(--color-bg-hover); color: var(--color-text-secondary); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-base); font-size: var(--font-size-sm); font-weight: 500; diff --git a/purplex/client/src/components/activities/inputs/DescriptionInput.vue b/purplex/client/src/components/activities/inputs/DescriptionInput.vue index ea688c37..61c9fcb3 100644 --- a/purplex/client/src/components/activities/inputs/DescriptionInput.vue +++ b/purplex/client/src/components/activities/inputs/DescriptionInput.vue @@ -175,7 +175,7 @@ function handleSubmit() { .prompt-editor-wrapper { padding: 0; background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); margin: var(--spacing-md) var(--spacing-xl); margin-bottom: 0; border-radius: var(--radius-base); diff --git a/purplex/client/src/components/activities/inputs/McqInput.vue b/purplex/client/src/components/activities/inputs/McqInput.vue index 99bacd41..cf7f8e2c 100644 --- a/purplex/client/src/components/activities/inputs/McqInput.vue +++ b/purplex/client/src/components/activities/inputs/McqInput.vue @@ -206,7 +206,7 @@ function getButtonAriaLabel(): string { font-size: var(--font-size-lg); color: var(--color-text-primary); line-height: 1.6; - border-bottom: 1px solid var(--color-bg-border); + border-bottom: 1px solid var(--color-border); } /* Section Header */ @@ -240,7 +240,7 @@ function getButtonAriaLabel(): string { align-items: center; padding: var(--spacing-md) var(--spacing-lg); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); cursor: pointer; transition: var(--transition-base); diff --git a/purplex/client/src/components/activities/inputs/ProbeableCodeInput.vue b/purplex/client/src/components/activities/inputs/ProbeableCodeInput.vue index c008cc3e..31dad451 100644 --- a/purplex/client/src/components/activities/inputs/ProbeableCodeInput.vue +++ b/purplex/client/src/components/activities/inputs/ProbeableCodeInput.vue @@ -256,7 +256,7 @@ function handleSubmit() { .code-editor-wrapper { padding: 0; background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); margin: var(--spacing-md) var(--spacing-xl); margin-bottom: 0; border-radius: var(--radius-base); diff --git a/purplex/client/src/components/activities/inputs/ProbeableSpecInput.vue b/purplex/client/src/components/activities/inputs/ProbeableSpecInput.vue index 3b63a346..65380c14 100644 --- a/purplex/client/src/components/activities/inputs/ProbeableSpecInput.vue +++ b/purplex/client/src/components/activities/inputs/ProbeableSpecInput.vue @@ -257,7 +257,7 @@ function handleSubmit() { .prompt-editor-wrapper { padding: 0; background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); margin: var(--spacing-md) var(--spacing-xl); margin-bottom: 0; border-radius: var(--radius-base); diff --git a/purplex/client/src/components/activities/inputs/RefuteInput.vue b/purplex/client/src/components/activities/inputs/RefuteInput.vue index 21882c70..ec3fc5eb 100644 --- a/purplex/client/src/components/activities/inputs/RefuteInput.vue +++ b/purplex/client/src/components/activities/inputs/RefuteInput.vue @@ -430,7 +430,7 @@ function formatOutput(value: unknown): string { /* Test Wrapper */ .test-wrapper { background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); margin: var(--spacing-md) var(--spacing-lg); padding: var(--spacing-sm) var(--spacing-md); @@ -476,7 +476,7 @@ function formatOutput(value: unknown): string { height: 28px; padding: 0 var(--spacing-sm); background: var(--color-bg-panel); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-sm); color: var(--color-text-primary); font-family: var(--font-mono); diff --git a/purplex/client/src/components/activities/inputs/shared/ProbePanel.vue b/purplex/client/src/components/activities/inputs/shared/ProbePanel.vue index c4494da6..abf4dc95 100644 --- a/purplex/client/src/components/activities/inputs/shared/ProbePanel.vue +++ b/purplex/client/src/components/activities/inputs/shared/ProbePanel.vue @@ -449,7 +449,7 @@ watch(() => props.probeHistory.length, () => { =========================================== */ .probe-input-wrapper { background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); margin: var(--spacing-sm) var(--spacing-lg); padding: var(--spacing-sm) var(--spacing-md); @@ -491,7 +491,7 @@ watch(() => props.probeHistory.length, () => { flex-shrink: 0; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-hover); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-sm); color: var(--color-text-muted); font-size: var(--font-size-xs); @@ -528,7 +528,7 @@ watch(() => props.probeHistory.length, () => { min-height: 32px; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-sm); font-family: var(--font-mono); font-size: var(--font-size-sm); @@ -766,7 +766,7 @@ watch(() => props.probeHistory.length, () => { .modal-content { background: var(--color-bg-panel); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-lg); width: 90%; max-width: 480px; @@ -781,7 +781,7 @@ watch(() => props.probeHistory.length, () => { justify-content: space-between; align-items: center; padding: var(--spacing-md) var(--spacing-lg); - border-bottom: 1px solid var(--color-bg-border); + border-bottom: 1px solid var(--color-border); } .modal-header h4 { diff --git a/purplex/client/src/components/admin/editors/DebugFixProblemEditor.vue b/purplex/client/src/components/admin/editors/DebugFixProblemEditor.vue index 4ee1f1b5..ac419788 100644 --- a/purplex/client/src/components/admin/editors/DebugFixProblemEditor.vue +++ b/purplex/client/src/components/admin/editors/DebugFixProblemEditor.vue @@ -442,7 +442,7 @@ onMounted(() => { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Main Container */ @@ -465,7 +465,7 @@ onMounted(() => { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .section-description { @@ -498,7 +498,7 @@ onMounted(() => { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -529,7 +529,7 @@ onMounted(() => { /* Code Editor */ .code-editor { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-top: none; border-radius: 0 0 var(--radius-base) var(--radius-base); overflow: hidden; @@ -590,7 +590,7 @@ onMounted(() => { align-items: center; padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--radius-base) var(--radius-base) 0 0; - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-bottom: none; } @@ -696,7 +696,7 @@ onMounted(() => { width: 28px; height: 28px; background: var(--color-bg-hover); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-secondary); font-size: var(--font-size-lg); @@ -715,7 +715,7 @@ onMounted(() => { .section-content { padding-top: var(--spacing-lg); - border-top: 2px solid var(--color-bg-border); + border-top: 2px solid var(--color-border); margin-top: var(--spacing-lg); } @@ -731,7 +731,7 @@ onMounted(() => { padding: var(--spacing-md); background: var(--color-bg-hover); border-radius: var(--radius-base); - border-left: 4px solid var(--color-bg-border); + border-left: 4px solid var(--color-border); } .hint-item.level-1 { @@ -767,7 +767,7 @@ onMounted(() => { .level-select { padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -777,7 +777,7 @@ onMounted(() => { width: 100%; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -804,7 +804,7 @@ onMounted(() => { padding: var(--spacing-lg); background: var(--color-bg-hover); border-radius: var(--radius-base); - border: 2px dashed var(--color-bg-border); + border: 2px dashed var(--color-border); } .add-hint-form h4 { @@ -838,7 +838,7 @@ onMounted(() => { .level-guide { margin-top: var(--spacing-md); padding-top: var(--spacing-md); - border-top: 1px solid var(--color-bg-border); + border-top: 1px solid var(--color-border); font-size: var(--font-size-xs); color: var(--color-text-muted); line-height: 1.6; @@ -886,7 +886,7 @@ onMounted(() => { padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-panel); color: var(--color-text-secondary); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); cursor: pointer; font-weight: 500; @@ -913,7 +913,7 @@ onMounted(() => { width: 24px; height: 24px; background: var(--color-bg-panel); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-muted); cursor: pointer; diff --git a/purplex/client/src/components/admin/editors/EiplProblemEditor.vue b/purplex/client/src/components/admin/editors/EiplProblemEditor.vue index 7b960f86..dd55edd0 100644 --- a/purplex/client/src/components/admin/editors/EiplProblemEditor.vue +++ b/purplex/client/src/components/admin/editors/EiplProblemEditor.vue @@ -479,7 +479,7 @@ defineExpose({ } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Form Sections */ @@ -496,7 +496,7 @@ defineExpose({ font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } /* Form Groups */ @@ -519,7 +519,7 @@ defineExpose({ width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -541,7 +541,7 @@ defineExpose({ align-items: center; padding: var(--spacing-sm); background: var(--color-bg-hover); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-bottom: none; border-radius: var(--radius-base) var(--radius-base) 0 0; } @@ -562,7 +562,7 @@ defineExpose({ width: 28px; height: 28px; border-radius: 50%; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); background: var(--color-bg-panel); color: var(--color-text-secondary); cursor: pointer; @@ -593,14 +593,14 @@ defineExpose({ .theme-dropdown { padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-panel); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); } .code-editor { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-top: none; border-radius: 0 0 var(--radius-base) var(--radius-base); overflow: hidden; @@ -623,7 +623,7 @@ defineExpose({ padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-panel); color: var(--color-text-secondary); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); cursor: pointer; font-weight: 500; @@ -671,7 +671,7 @@ defineExpose({ .test-case { background: var(--color-bg-hover); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); padding: var(--spacing-md); } @@ -732,7 +732,7 @@ defineExpose({ padding: var(--spacing-sm); padding-right: 50px; background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -840,7 +840,7 @@ defineExpose({ width: 28px; height: 28px; border-radius: 50%; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); background: var(--color-bg-panel); color: var(--color-text-muted); cursor: pointer; @@ -901,7 +901,7 @@ defineExpose({ .spinner { width: 40px; height: 40px; - border: 3px solid var(--color-bg-border); + border: 3px solid var(--color-border); border-top-color: var(--color-accent); border-radius: 50%; animation: spin 1s linear infinite; @@ -924,7 +924,7 @@ defineExpose({ display: flex; gap: var(--spacing-xs); margin-bottom: var(--spacing-lg); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); padding-bottom: var(--spacing-sm); } @@ -995,7 +995,7 @@ defineExpose({ width: 60px; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -1032,7 +1032,7 @@ defineExpose({ flex: 1; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -1048,13 +1048,13 @@ defineExpose({ align-items: center; gap: var(--spacing-sm); padding-top: var(--spacing-md); - border-top: 1px solid var(--color-bg-border); + border-top: 1px solid var(--color-border); } .subgoal-item { padding: var(--spacing-md); background: var(--color-bg-panel); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-base); margin-bottom: var(--spacing-sm); } @@ -1070,7 +1070,7 @@ defineExpose({ flex: 1; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -1092,7 +1092,7 @@ defineExpose({ width: 60px; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -1103,7 +1103,7 @@ defineExpose({ width: 100%; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -1112,7 +1112,7 @@ defineExpose({ .add-subgoal { padding-top: var(--spacing-md); - border-top: 1px solid var(--color-bg-border); + border-top: 1px solid var(--color-border); } .add-subgoal h5 { @@ -1135,7 +1135,7 @@ defineExpose({ width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-family: monospace; @@ -1244,7 +1244,7 @@ defineExpose({ width: 60px; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -1319,7 +1319,7 @@ defineExpose({ flex: 1; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -1340,7 +1340,7 @@ defineExpose({ width: 100px; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -1350,7 +1350,7 @@ defineExpose({ width: 24px; height: 24px; border-radius: 50%; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); background: var(--color-bg-panel); color: var(--color-text-muted); cursor: pointer; diff --git a/purplex/client/src/components/admin/editors/McqProblemEditor.vue b/purplex/client/src/components/admin/editors/McqProblemEditor.vue index dbe9aff1..0825d1e0 100644 --- a/purplex/client/src/components/admin/editors/McqProblemEditor.vue +++ b/purplex/client/src/components/admin/editors/McqProblemEditor.vue @@ -136,7 +136,7 @@ defineExpose({ } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Form Sections */ @@ -153,7 +153,7 @@ defineExpose({ font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .section-description { @@ -181,7 +181,7 @@ defineExpose({ width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -211,7 +211,7 @@ defineExpose({ .mcq-option-item { background: var(--color-bg-hover); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); padding: var(--spacing-md); transition: all 0.2s ease; @@ -262,7 +262,7 @@ defineExpose({ width: 28px; height: 28px; border-radius: 50%; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); background: var(--color-bg-panel); color: var(--color-text-muted); cursor: pointer; @@ -295,7 +295,7 @@ defineExpose({ width: 100%; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -321,7 +321,7 @@ defineExpose({ padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-panel); color: var(--color-text-secondary); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); cursor: pointer; font-weight: 500; diff --git a/purplex/client/src/components/admin/editors/ProbeableCodeProblemEditor.vue b/purplex/client/src/components/admin/editors/ProbeableCodeProblemEditor.vue index a688a3c2..014de848 100644 --- a/purplex/client/src/components/admin/editors/ProbeableCodeProblemEditor.vue +++ b/purplex/client/src/components/admin/editors/ProbeableCodeProblemEditor.vue @@ -161,7 +161,7 @@ onMounted(() => { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Main Container */ @@ -184,7 +184,7 @@ onMounted(() => { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .form-section h4 { @@ -231,7 +231,7 @@ onMounted(() => { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -266,7 +266,7 @@ onMounted(() => { /* Code Editor */ .code-editor { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-top: none; border-radius: 0 0 var(--radius-base) var(--radius-base); overflow: hidden; diff --git a/purplex/client/src/components/admin/editors/ProbeableSpecProblemEditor.vue b/purplex/client/src/components/admin/editors/ProbeableSpecProblemEditor.vue index fef837bf..261e0bb6 100644 --- a/purplex/client/src/components/admin/editors/ProbeableSpecProblemEditor.vue +++ b/purplex/client/src/components/admin/editors/ProbeableSpecProblemEditor.vue @@ -170,7 +170,7 @@ onMounted(() => { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Main Container */ @@ -193,7 +193,7 @@ onMounted(() => { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .form-section h4 { @@ -240,7 +240,7 @@ onMounted(() => { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -275,7 +275,7 @@ onMounted(() => { /* Code Editor */ .code-editor { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-top: none; border-radius: 0 0 var(--radius-base) var(--radius-base); overflow: hidden; @@ -331,7 +331,7 @@ onMounted(() => { width: 60px; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -409,7 +409,7 @@ onMounted(() => { flex: 1; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -430,7 +430,7 @@ onMounted(() => { width: 100px; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -440,7 +440,7 @@ onMounted(() => { width: 24px; height: 24px; border-radius: 50%; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); background: var(--color-bg-panel); color: var(--color-text-muted); cursor: pointer; @@ -461,7 +461,7 @@ onMounted(() => { padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-panel); color: var(--color-text-secondary); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); cursor: pointer; font-weight: 500; diff --git a/purplex/client/src/components/admin/editors/PromptProblemEditor.vue b/purplex/client/src/components/admin/editors/PromptProblemEditor.vue index 426a1de0..4f19ff92 100644 --- a/purplex/client/src/components/admin/editors/PromptProblemEditor.vue +++ b/purplex/client/src/components/admin/editors/PromptProblemEditor.vue @@ -471,7 +471,7 @@ onMounted(() => { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Main Container */ @@ -494,7 +494,7 @@ onMounted(() => { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .section-description { @@ -528,7 +528,7 @@ onMounted(() => { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -581,7 +581,7 @@ onMounted(() => { .preview-container { position: relative; background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); overflow: hidden; max-height: 400px; @@ -610,7 +610,7 @@ onMounted(() => { /* Image Placeholder */ .image-placeholder { background: var(--color-bg-input); - border: 2px dashed var(--color-bg-border); + border: 2px dashed var(--color-border); border-radius: var(--radius-base); padding: var(--spacing-xxl); } @@ -635,7 +635,7 @@ onMounted(() => { /* Code Editor */ .code-editor { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); overflow: hidden; } @@ -647,7 +647,7 @@ onMounted(() => { .terminal-run-editor { background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); padding: var(--spacing-md); margin-bottom: var(--spacing-md); @@ -678,7 +678,7 @@ onMounted(() => { flex-shrink: 0; padding: var(--spacing-sm); background: var(--color-bg-panel); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -688,7 +688,7 @@ onMounted(() => { flex: 1; padding: var(--spacing-sm); background: var(--color-bg-panel); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -730,7 +730,7 @@ onMounted(() => { width: 100%; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -749,7 +749,7 @@ onMounted(() => { /* Shared buttons */ .btn-add { background: none; - border: 2px dashed var(--color-bg-border); + border: 2px dashed var(--color-border); border-radius: var(--radius-base); color: var(--color-text-muted); padding: var(--spacing-sm) var(--spacing-md); @@ -762,7 +762,7 @@ onMounted(() => { .btn-add-small { background: none; - border: 2px dashed var(--color-bg-border); + border: 2px dashed var(--color-border); border-radius: var(--radius-base); color: var(--color-text-muted); cursor: pointer; diff --git a/purplex/client/src/components/admin/editors/RefuteProblemEditor.vue b/purplex/client/src/components/admin/editors/RefuteProblemEditor.vue index 59785c5d..85d19b85 100644 --- a/purplex/client/src/components/admin/editors/RefuteProblemEditor.vue +++ b/purplex/client/src/components/admin/editors/RefuteProblemEditor.vue @@ -223,7 +223,7 @@ onMounted(() => { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Main Container */ @@ -246,7 +246,7 @@ onMounted(() => { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .section-description { @@ -279,7 +279,7 @@ onMounted(() => { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -394,7 +394,7 @@ onMounted(() => { .examples-table td { text-align: left; padding: var(--spacing-sm) var(--spacing-md); - border-bottom: 1px solid var(--color-bg-border); + border-bottom: 1px solid var(--color-border); } .examples-table th { @@ -422,7 +422,7 @@ onMounted(() => { align-items: center; padding: var(--spacing-sm); background: var(--color-bg-hover); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-bottom: none; border-radius: var(--radius-base) var(--radius-base) 0 0; } @@ -443,7 +443,7 @@ onMounted(() => { width: 28px; height: 28px; border-radius: 50%; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); background: var(--color-bg-panel); color: var(--color-text-secondary); cursor: pointer; @@ -474,14 +474,14 @@ onMounted(() => { .theme-dropdown { padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-panel); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); } .code-editor { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-top: none; border-radius: 0 0 var(--radius-base) var(--radius-base); overflow: hidden; diff --git a/purplex/client/src/components/admin/editors/shared/BasicInfoSection.vue b/purplex/client/src/components/admin/editors/shared/BasicInfoSection.vue index 25df219f..02ed9b74 100644 --- a/purplex/client/src/components/admin/editors/shared/BasicInfoSection.vue +++ b/purplex/client/src/components/admin/editors/shared/BasicInfoSection.vue @@ -105,7 +105,7 @@ function removeTag(index: number) { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } .form-section { @@ -120,7 +120,7 @@ function removeTag(index: number) { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .form-group { @@ -144,7 +144,7 @@ function removeTag(index: number) { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -187,7 +187,7 @@ function removeTag(index: number) { gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-hover); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-base); font-size: var(--font-size-sm); color: var(--color-text-secondary); @@ -212,7 +212,7 @@ function removeTag(index: number) { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); diff --git a/purplex/client/src/components/admin/editors/shared/EditorToolbar.vue b/purplex/client/src/components/admin/editors/shared/EditorToolbar.vue index 8dc20976..2931b419 100644 --- a/purplex/client/src/components/admin/editors/shared/EditorToolbar.vue +++ b/purplex/client/src/components/admin/editors/shared/EditorToolbar.vue @@ -46,7 +46,7 @@ const editor = computed(() => props.editor) align-items: center; padding: var(--spacing-sm); background: var(--color-bg-hover); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-bottom: none; border-radius: var(--radius-base) var(--radius-base) 0 0; } @@ -67,7 +67,7 @@ const editor = computed(() => props.editor) width: 28px; height: 28px; border-radius: 50%; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); background: var(--color-bg-panel); color: var(--color-text-secondary); cursor: pointer; diff --git a/purplex/client/src/components/admin/editors/shared/ProbeSettingsSection.vue b/purplex/client/src/components/admin/editors/shared/ProbeSettingsSection.vue index 14bf5f02..1fea1d6a 100644 --- a/purplex/client/src/components/admin/editors/shared/ProbeSettingsSection.vue +++ b/purplex/client/src/components/admin/editors/shared/ProbeSettingsSection.vue @@ -150,7 +150,7 @@ const selectedModeDescription = computed(() => { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Form Section */ @@ -166,7 +166,7 @@ const selectedModeDescription = computed(() => { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .section-description { @@ -204,7 +204,7 @@ const selectedModeDescription = computed(() => { color: var(--color-text-muted); font-size: var(--font-size-xs); padding-left: var(--spacing-sm); - border-left: 1px solid var(--color-bg-border); + border-left: 1px solid var(--color-border); } /* Probe Mode Section */ @@ -226,7 +226,7 @@ const selectedModeDescription = computed(() => { .segmented-control { display: flex; background: var(--color-bg-hover); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); padding: 2px; gap: 2px; @@ -300,7 +300,7 @@ const selectedModeDescription = computed(() => { width: 80px; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-sm); diff --git a/purplex/client/src/components/admin/editors/shared/SegmentationConfigSection.vue b/purplex/client/src/components/admin/editors/shared/SegmentationConfigSection.vue index 16a29dd4..315cb34e 100644 --- a/purplex/client/src/components/admin/editors/shared/SegmentationConfigSection.vue +++ b/purplex/client/src/components/admin/editors/shared/SegmentationConfigSection.vue @@ -204,7 +204,7 @@ withDefaults(defineProps(), { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Form Section */ @@ -220,7 +220,7 @@ withDefaults(defineProps(), { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .form-section h4 { @@ -257,7 +257,7 @@ withDefaults(defineProps(), { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -341,7 +341,7 @@ withDefaults(defineProps(), { width: 60px; padding: var(--spacing-sm); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -419,7 +419,7 @@ withDefaults(defineProps(), { flex: 1; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -440,7 +440,7 @@ withDefaults(defineProps(), { width: 100px; padding: var(--spacing-xs) var(--spacing-sm); background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -450,7 +450,7 @@ withDefaults(defineProps(), { width: 24px; height: 24px; border-radius: 50%; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); background: var(--color-bg-panel); color: var(--color-text-muted); cursor: pointer; @@ -471,7 +471,7 @@ withDefaults(defineProps(), { padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-panel); color: var(--color-text-secondary); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); cursor: pointer; font-weight: 500; diff --git a/purplex/client/src/components/admin/editors/shared/TestCasesSection.vue b/purplex/client/src/components/admin/editors/shared/TestCasesSection.vue index 66a36778..33c5ccaa 100644 --- a/purplex/client/src/components/admin/editors/shared/TestCasesSection.vue +++ b/purplex/client/src/components/admin/editors/shared/TestCasesSection.vue @@ -278,7 +278,7 @@ function removeTestCase(index: number) { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } .hover-primary:hover { @@ -299,7 +299,7 @@ function removeTestCase(index: number) { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } /* Test Actions */ @@ -324,7 +324,7 @@ function removeTestCase(index: number) { /* Test Case Cards */ .test-case { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); background: var(--color-bg-panel); transition: var(--transition-fast); @@ -390,7 +390,7 @@ function removeTestCase(index: number) { .param-input { flex: 1; padding: var(--spacing-md); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); background: var(--color-bg-input); color: var(--color-text-primary); @@ -479,13 +479,13 @@ function removeTestCase(index: number) { .type-none { background: var(--color-bg-section); color: var(--color-text-muted); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .type-any { background: var(--color-bg-hover); color: var(--color-text-tertiary); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .type-invalid { @@ -568,7 +568,7 @@ function removeTestCase(index: number) { /* Remove Button */ .remove-btn { background: var(--color-bg-hover); - border: 1.5px solid var(--color-bg-border); + border: 1.5px solid var(--color-border); color: var(--color-text-muted); cursor: pointer; border-radius: var(--radius-circle); @@ -639,7 +639,7 @@ function removeTestCase(index: number) { .spinner { width: 40px; height: 40px; - border: 3px solid var(--color-bg-border); + border: 3px solid var(--color-border); border-top-color: var(--color-accent); border-radius: 50%; animation: spin 1s linear infinite; @@ -681,7 +681,7 @@ function removeTestCase(index: number) { .btn-secondary { background: var(--color-bg-panel); color: var(--color-text-secondary); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); padding: var(--spacing-md) var(--spacing-lg); font-weight: 600; font-size: var(--font-size-base); diff --git a/purplex/client/src/components/content/ContentEditorLayout.vue b/purplex/client/src/components/content/ContentEditorLayout.vue index c3fe44b2..a0af73d9 100644 --- a/purplex/client/src/components/content/ContentEditorLayout.vue +++ b/purplex/client/src/components/content/ContentEditorLayout.vue @@ -123,7 +123,7 @@ defineExpose({ ctx }); .breadcrumb { margin-bottom: var(--spacing-xl); padding: var(--spacing-md) 0; - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .breadcrumb-link { @@ -171,7 +171,7 @@ defineExpose({ ctx }); /* Utility classes */ .border-default { - border-color: var(--color-bg-border); + border-color: var(--color-border); } .transition-fast { diff --git a/purplex/client/src/components/content/CourseEditorShell.vue b/purplex/client/src/components/content/CourseEditorShell.vue index 2c71c8c2..3695aea8 100644 --- a/purplex/client/src/components/content/CourseEditorShell.vue +++ b/purplex/client/src/components/content/CourseEditorShell.vue @@ -407,7 +407,7 @@ onMounted(loadInstructors); margin: 0 0 var(--spacing-lg) 0; color: var(--color-text-primary); font-size: var(--font-size-lg); - border-bottom: 1px solid var(--color-bg-border); + border-bottom: 1px solid var(--color-border); padding-bottom: var(--spacing-md); } @@ -433,7 +433,7 @@ onMounted(loadInstructors); padding: var(--spacing-md); background: var(--color-bg-input); color: var(--color-text-primary); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); font-size: var(--font-size-base); transition: var(--transition-fast); @@ -520,7 +520,7 @@ onMounted(loadInstructors); .btn-secondary { background: var(--color-bg-hover); color: var(--color-text-secondary); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .btn-secondary:hover { diff --git a/purplex/client/src/components/content/CourseList.vue b/purplex/client/src/components/content/CourseList.vue index 9094650e..ba78adea 100644 --- a/purplex/client/src/components/content/CourseList.vue +++ b/purplex/client/src/components/content/CourseList.vue @@ -310,7 +310,7 @@ onMounted(fetchTable); height: 36px; padding: 0; background: var(--color-bg-hover); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-base); cursor: pointer; transition: var(--transition-base); diff --git a/purplex/client/src/components/content/CourseProblemSetsPage.vue b/purplex/client/src/components/content/CourseProblemSetsPage.vue index cdd843e6..b87f74f0 100644 --- a/purplex/client/src/components/content/CourseProblemSetsPage.vue +++ b/purplex/client/src/components/content/CourseProblemSetsPage.vue @@ -660,7 +660,7 @@ watch( .order-btn { padding: var(--spacing-xs) var(--spacing-sm); background-color: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-xs); color: var(--color-text-secondary); cursor: pointer; @@ -693,7 +693,7 @@ watch( .due-date-input { background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: var(--spacing-xs) var(--spacing-sm); color: var(--color-text-primary); @@ -729,7 +729,7 @@ watch( .deadline-select { background: var(--color-bg-input); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: var(--spacing-xs) var(--spacing-sm); color: var(--color-text-primary); @@ -825,7 +825,7 @@ watch( .secondary-button { background: var(--color-bg-hover); color: var(--color-text-secondary); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .secondary-button:hover { @@ -850,7 +850,7 @@ watch( .remove-button { background-color: transparent; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); color: var(--color-text-muted); padding: var(--spacing-xs) var(--spacing-md); } @@ -911,7 +911,7 @@ watch( .btn-secondary { background: var(--color-bg-hover); color: var(--color-text-secondary); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .btn-danger { diff --git a/purplex/client/src/components/content/CourseStudentsPage.vue b/purplex/client/src/components/content/CourseStudentsPage.vue index ec407822..674d55bb 100644 --- a/purplex/client/src/components/content/CourseStudentsPage.vue +++ b/purplex/client/src/components/content/CourseStudentsPage.vue @@ -334,7 +334,7 @@ watch( .secondary-button { background: var(--color-bg-hover); color: var(--color-text-secondary); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .secondary-button:hover { @@ -345,7 +345,7 @@ watch( .remove-button { background-color: transparent; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); color: var(--color-text-muted); padding: var(--spacing-xs) var(--spacing-md); } diff --git a/purplex/client/src/components/content/ProblemEditorShell.vue b/purplex/client/src/components/content/ProblemEditorShell.vue index 00608928..16b4dd88 100644 --- a/purplex/client/src/components/content/ProblemEditorShell.vue +++ b/purplex/client/src/components/content/ProblemEditorShell.vue @@ -282,7 +282,7 @@ async function confirmDelete() { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Header Section */ @@ -344,7 +344,7 @@ async function confirmDelete() { .btn-secondary { background: var(--color-bg-hover); color: var(--color-text-secondary); - border-color: var(--color-bg-border); + border-color: var(--color-border); } .btn-secondary:hover:not(:disabled) { @@ -388,7 +388,7 @@ async function confirmDelete() { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } /* Form Groups */ @@ -412,7 +412,7 @@ async function confirmDelete() { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -446,7 +446,7 @@ async function confirmDelete() { padding: var(--spacing-xxl); background: var(--color-bg-panel); border-radius: var(--radius-lg); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } .loading-editor p { @@ -457,7 +457,7 @@ async function confirmDelete() { .spinner { width: 40px; height: 40px; - border: 3px solid var(--color-bg-border); + border: 3px solid var(--color-border); border-top-color: var(--color-accent); border-radius: 50%; animation: spin 1s linear infinite; diff --git a/purplex/client/src/components/content/ProblemList.vue b/purplex/client/src/components/content/ProblemList.vue index 4d8ac320..2ff0fcbb 100644 --- a/purplex/client/src/components/content/ProblemList.vue +++ b/purplex/client/src/components/content/ProblemList.vue @@ -299,7 +299,7 @@ onMounted(fetchTable); .edit-button { background: var(--color-bg-hover); color: var(--color-text-tertiary); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .edit-button:hover { diff --git a/purplex/client/src/components/content/ProblemSetEditorShell.vue b/purplex/client/src/components/content/ProblemSetEditorShell.vue index 397b289c..7ad788bf 100644 --- a/purplex/client/src/components/content/ProblemSetEditorShell.vue +++ b/purplex/client/src/components/content/ProblemSetEditorShell.vue @@ -450,7 +450,7 @@ onMounted(async () => { } .border-default { - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } /* Header */ @@ -485,7 +485,7 @@ onMounted(async () => { padding: var(--spacing-xxl); background: var(--color-bg-panel); border-radius: var(--radius-lg); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); } .loading-container p { @@ -496,7 +496,7 @@ onMounted(async () => { .spinner { width: 40px; height: 40px; - border: 3px solid var(--color-bg-border); + border: 3px solid var(--color-border); border-top-color: var(--color-accent); border-radius: 50%; animation: spin 1s linear infinite; @@ -505,7 +505,7 @@ onMounted(async () => { .spinner-small { width: 20px; height: 20px; - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-top-color: var(--color-accent); border-radius: 50%; animation: spin 1s linear infinite; @@ -544,7 +544,7 @@ onMounted(async () => { font-size: var(--font-size-lg); font-weight: 600; padding-bottom: var(--spacing-base); - border-bottom: 2px solid var(--color-bg-border); + border-bottom: 2px solid var(--color-border); } .form-row { @@ -578,7 +578,7 @@ onMounted(async () => { width: 100%; padding: var(--spacing-md); background: var(--color-bg-input); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); color: var(--color-text-primary); font-size: var(--font-size-base); @@ -644,18 +644,18 @@ onMounted(async () => { min-height: 100px; max-height: 300px; overflow-y: auto; - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .available-problems-section { - border-top: 1px solid var(--color-bg-border); + border-top: 1px solid var(--color-border); padding-top: var(--spacing-lg); } .problem-search { width: 100%; padding: var(--spacing-md); - border: 2px solid var(--color-bg-border); + border: 2px solid var(--color-border); border-radius: var(--radius-base); background: var(--color-bg-input); color: var(--color-text-primary); @@ -674,7 +674,7 @@ onMounted(async () => { background: var(--color-bg-main); border-radius: var(--radius-base); padding: var(--spacing-md); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .problem-item { @@ -692,7 +692,7 @@ onMounted(async () => { .problem-item.selected { background: var(--color-bg-panel); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .problem-item.available { @@ -830,7 +830,7 @@ onMounted(async () => { .btn-secondary { background: var(--color-bg-hover); color: var(--color-text-secondary); - border-color: var(--color-bg-border); + border-color: var(--color-border); } .btn-secondary:hover:not(:disabled) { diff --git a/purplex/client/src/components/content/ProblemSetManager.vue b/purplex/client/src/components/content/ProblemSetManager.vue index fa8e584e..96a1638e 100644 --- a/purplex/client/src/components/content/ProblemSetManager.vue +++ b/purplex/client/src/components/content/ProblemSetManager.vue @@ -259,7 +259,7 @@ onMounted(fetchTable); .edit-button { background: var(--color-bg-hover); color: var(--color-text-tertiary); - border: 1px solid var(--color-bg-border); + border: 1px solid var(--color-border); } .edit-button:hover { diff --git a/purplex/client/src/components/hints/SuggestedTraceOverlay.vue b/purplex/client/src/components/hints/SuggestedTraceOverlay.vue index d061c84e..d9c5e9e3 100644 --- a/purplex/client/src/components/hints/SuggestedTraceOverlay.vue +++ b/purplex/client/src/components/hints/SuggestedTraceOverlay.vue @@ -49,7 +49,7 @@ function openInPyTutor(): void { - - - diff --git a/purplex/client/src/components/NavBar.vue b/purplex/client/src/components/NavBar.vue index 9431c8af..ed6255ff 100644 --- a/purplex/client/src/components/NavBar.vue +++ b/purplex/client/src/components/NavBar.vue @@ -553,12 +553,3 @@ onUnmounted(() => { } } - - - diff --git a/purplex/client/src/components/instructor/InstructorCourseOverview.vue b/purplex/client/src/components/instructor/InstructorCourseOverview.vue index 2e0dfad6..50765502 100644 --- a/purplex/client/src/components/instructor/InstructorCourseOverview.vue +++ b/purplex/client/src/components/instructor/InstructorCourseOverview.vue @@ -853,6 +853,7 @@ onMounted(() => { } .back-btn:hover { + background: var(--gradient-admin-hover); transform: translateY(-2px); box-shadow: 0 4px 12px var(--color-accent-glow); } @@ -1013,7 +1014,7 @@ onMounted(() => { .marker-line { flex: 1; width: 2px; - background: linear-gradient(to bottom, var(--color-admin), var(--color-border)); + background: var(--marker-line-bg); margin-top: var(--spacing-sm); min-height: 40px; } @@ -1584,34 +1585,3 @@ onMounted(() => { } } - - - diff --git a/purplex/client/src/components/instructor/InstructorNavBar.vue b/purplex/client/src/components/instructor/InstructorNavBar.vue index c2ccbfe1..94bb75c7 100644 --- a/purplex/client/src/components/instructor/InstructorNavBar.vue +++ b/purplex/client/src/components/instructor/InstructorNavBar.vue @@ -202,8 +202,8 @@ const isActiveRoute = (path: string): boolean => { } .instructor-nav { - background: linear-gradient(135deg, var(--color-admin) 0%, var(--color-admin-hover) 100%); - color: var(--color-text-on-filled); + background: var(--nav-sub-bg); + color: var(--color-nav-sub-text); width: 100%; padding: var(--spacing-lg) 0; margin-bottom: var(--spacing-xl); @@ -266,7 +266,7 @@ const isActiveRoute = (path: string): boolean => { .instructor-link { display: flex; align-items: center; - color: var(--color-text-primary); + color: var(--color-nav-sub-text); text-decoration: none; padding: var(--spacing-md) var(--spacing-xl); border-radius: var(--radius-base); @@ -295,17 +295,18 @@ const isActiveRoute = (path: string): boolean => { .instructor-link:hover { transform: translateY(-2px); - border-color: var(--color-overlay-strong); + border-color: var(--color-nav-sub-hover-border); } .instructor-link:focus-visible { - outline: 2px solid var(--color-focus-ring); + outline: 2px solid var(--color-nav-sub-focus); outline-offset: 2px; } .instructor-link.router-link-active { - background: var(--color-overlay-strong); - border-color: var(--color-overlay-strong); + background: var(--color-nav-sub-active-bg); + border-color: var(--color-nav-sub-active-bg); + color: var(--color-nav-sub-active-text); box-shadow: var(--shadow-base), 0 0 20px var(--color-overlay-strong); @@ -318,7 +319,7 @@ const isActiveRoute = (path: string): boolean => { left: 20%; right: 20%; height: 4px; - background: var(--color-text-primary); + background: var(--color-nav-sub-active-text); border-radius: var(--radius-xs); box-shadow: 0 0 8px var(--color-overlay-strong); } @@ -372,34 +373,3 @@ const isActiveRoute = (path: string): boolean => { } } - - - diff --git a/purplex/client/src/styles/tokens.css b/purplex/client/src/styles/tokens.css index 4d90392d..efd808cb 100644 --- a/purplex/client/src/styles/tokens.css +++ b/purplex/client/src/styles/tokens.css @@ -77,6 +77,7 @@ --color-gradient-end: #764ba2; --gradient-primary: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 100%); --gradient-admin: linear-gradient(135deg, var(--color-admin) 0%, var(--color-gradient-end) 100%); + --gradient-admin-hover: var(--gradient-admin); --gradient-logo: linear-gradient(135deg, #a78bfa, #c4b5fd, #8b7ec8); /* Background Colors */ @@ -126,6 +127,19 @@ --color-admin-hover: #5e35b1; --color-admin-bg: #673ab7; + /* Sub-navigation bars (AdminNavBar, InstructorNavBar); --nav-sub-bg may be a gradient */ + --nav-sub-bg: linear-gradient(135deg, var(--color-admin) 0%, var(--color-admin-hover) 100%); + --color-nav-sub-text: var(--color-text-on-filled); + --color-nav-sub-active-bg: var(--color-overlay-strong); + --color-nav-sub-active-text: var(--color-text-on-filled); + --color-nav-sub-hover-border: var(--color-overlay-strong); + --color-nav-sub-focus: var(--color-focus-ring); + + /* One-off decorations */ + --marker-line-bg: linear-gradient(to bottom, var(--color-admin), var(--color-border)); + --logo-filter: brightness(0) invert(0.7); + --logo-filter-hover: brightness(0) invert(0.9); + /* Overlay Colors */ --color-overlay-subtle: rgb(255 255 255 / 5%); --color-overlay-medium: rgb(255 255 255 / 10%); @@ -219,7 +233,8 @@ --color-gradient-start: #4f46e5; --color-gradient-end: #6d28d9; --gradient-primary: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 100%); - --gradient-admin: linear-gradient(135deg, var(--color-admin) 0%, var(--color-gradient-end) 100%); + --gradient-admin: var(--color-admin); + --gradient-admin-hover: var(--color-admin-hover); --gradient-logo: linear-gradient(135deg, #7c3aed, #8b5cf6, #6d28d9); /* Background Colors */ @@ -269,6 +284,19 @@ --color-admin-hover: #6d28d9; --color-admin-bg: #ede9fe; + /* Sub-navigation bars (inverted: soft admin tint, admin text, solid active tab) */ + --nav-sub-bg: var(--color-admin-bg); + --color-nav-sub-text: var(--color-admin); + --color-nav-sub-active-bg: var(--color-admin); + --color-nav-sub-active-text: var(--color-text-on-filled); + --color-nav-sub-hover-border: var(--color-admin-shadow); + --color-nav-sub-focus: var(--color-admin); + + /* One-off decorations */ + --marker-line-bg: rgb(124 58 237 / 30%); + --logo-filter: brightness(0) invert(0.3); + --logo-filter-hover: brightness(0) invert(0.1); + /* Overlay Colors (white-alpha → black-alpha) */ --color-overlay-subtle: rgb(0 0 0 / 3%); --color-overlay-medium: rgb(0 0 0 / 5%); From bcbf0cde3b2e53b2bdb68fa5719ab4dfff1310fa Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 17 Oct 2026 11:33:31 -0400 Subject: [PATCH 7/9] Theme step 6: derive transparency variants with color-mix Every alpha token whose RGB equals a base token is now color-mix(in srgb, var(--base) P%, transparent) instead of a copied literal, so a base colour change carries its overlays, borders and glows with it: the four status -overlay and -border tokens, the accent overlay/glow/shadow, --color-ace-selection (primary), --color-segment-shadow (category-1), --color-warning-pulse (dark: warning-accent, light: warning), --color-admin-shadow (dark), --color-info-shadow (light) and --marker-line-bg (light). --color-warning-pulse-end is plain transparent. Literals whose RGB drifted from their base stay, marked inline: dark --color-info-shadow and --shadow-info-*, light --color-admin-shadow and --shadow-admin-*. The tokens.css header states the browser floor (Chrome 111, Safari 16.2, Firefox 113). Stylelint disallows color-mix() and light-dark() outside tokens.css. Computed colours are equal to the old literals; pixels differ by at most 1/255 where Chrome kept 8-bit alpha for rgb() and keeps float alpha for color-mix(). Co-Authored-By: Claude Opus 5.5 --- purplex/client/.stylelintrc.json | 2 +- purplex/client/src/styles/tokens.css | 84 +++++++++++++++------------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/purplex/client/.stylelintrc.json b/purplex/client/.stylelintrc.json index c0182955..b8c6dd4f 100644 --- a/purplex/client/.stylelintrc.json +++ b/purplex/client/.stylelintrc.json @@ -9,7 +9,7 @@ "rules": { "color-no-hex": true, "color-named": "never", - "function-disallowed-list": ["/^rgb/", "/^hsl/"], + "function-disallowed-list": ["/^rgb/", "/^hsl/", "color-mix", "light-dark"], "selector-class-pattern": null, "keyframes-name-pattern": null, "selector-disallowed-list": ["/\\[data-theme/"], diff --git a/purplex/client/src/styles/tokens.css b/purplex/client/src/styles/tokens.css index efd808cb..079e7881 100644 --- a/purplex/client/src/styles/tokens.css +++ b/purplex/client/src/styles/tokens.css @@ -17,6 +17,12 @@ * - No [data-theme] selectors anywhere else. If a component needs a * different treatment per theme, express it as a token here. * - No colour literals outside this file; stylelint enforces it. + * - Transparency variants derive from their base token with + * color-mix(in srgb, var(--base) P%, transparent). A literal alpha stays + * only where its RGB has drifted from the base (marked inline). + * color-mix() and light-dark() are allowed in this file only. + * + * Browser floor (color-mix): Chrome 111, Safari 16.2, Firefox 113. */ :root { @@ -146,14 +152,14 @@ --color-overlay-strong: rgb(255 255 255 / 15%); /* Status Overlays (low-alpha backgrounds) */ - --color-success-overlay: rgb(76 175 80 / 15%); - --color-warning-overlay: rgb(255 193 7 / 15%); - --color-error-overlay: rgb(220 53 69 / 15%); - --color-info-overlay: rgb(33 150 243 / 15%); + --color-success-overlay: color-mix(in srgb, var(--color-success) 15%, transparent); + --color-warning-overlay: color-mix(in srgb, var(--color-warning) 15%, transparent); + --color-error-overlay: color-mix(in srgb, var(--color-error) 15%, transparent); + --color-info-overlay: color-mix(in srgb, var(--color-info) 15%, transparent); /* Accent Overlays */ - --color-accent-overlay: rgb(102 126 234 / 10%); - --color-accent-glow: rgb(102 126 234 / 30%); + --color-accent-overlay: color-mix(in srgb, var(--color-accent) 10%, transparent); + --color-accent-glow: color-mix(in srgb, var(--color-accent) 30%, transparent); /* Status Gradient Endpoints */ --color-success-accent: #34d399; @@ -169,22 +175,22 @@ --color-overlay-shimmer: rgb(255 255 255 / 35%); /* Status Borders (colored border accents) */ - --color-success-border: rgb(76 175 80 / 40%); - --color-warning-border: rgb(255 193 7 / 40%); - --color-error-border: rgb(220 53 69 / 40%); - --color-info-border: rgb(33 150 243 / 40%); + --color-success-border: color-mix(in srgb, var(--color-success) 40%, transparent); + --color-warning-border: color-mix(in srgb, var(--color-warning) 40%, transparent); + --color-error-border: color-mix(in srgb, var(--color-error) 40%, transparent); + --color-info-border: color-mix(in srgb, var(--color-info) 40%, transparent); /* Modal Backdrops */ --color-backdrop: rgb(0 0 0 / 50%); --color-backdrop-heavy: rgb(0 0 0 / 75%); /* Colored Glows (box-shadows) */ - --color-accent-shadow: rgb(102 126 234 / 40%); - --color-admin-shadow: rgb(103 58 183 / 30%); - --color-info-shadow: rgb(59 130 246 / 30%); - --color-segment-shadow: rgb(159 122 234 / 30%); - --color-warning-pulse: rgb(251 191 36 / 70%); - --color-warning-pulse-end: rgb(251 191 36 / 0%); + --color-accent-shadow: color-mix(in srgb, var(--color-accent) 40%, transparent); + --color-admin-shadow: color-mix(in srgb, var(--color-admin) 30%, transparent); + --color-info-shadow: rgb(59 130 246 / 30%); /* drifted from base; retuned in step 7 */ + --color-segment-shadow: color-mix(in srgb, var(--color-category-1) 30%, transparent); + --color-warning-pulse: color-mix(in srgb, var(--color-warning-accent) 70%, transparent); + --color-warning-pulse-end: transparent; /* Categorical ramp (segments; read by SegmentMapping and ViewSubmissionModal) */ --color-category-1: #9f7aea; @@ -195,7 +201,7 @@ --color-category-6: #fc8181; /* Ace Editor */ - --color-ace-selection: rgb(128 0 128 / 30%); + --color-ace-selection: color-mix(in srgb, var(--color-primary) 30%, transparent); /* Shadows */ --shadow-xs: 0 1px 3px rgb(0 0 0 / 10%); @@ -213,8 +219,8 @@ --shadow-inset: inset 0 1px 3px rgb(0 0 0 / 20%); --shadow-float: 0 24px 64px rgb(0 0 0 / 50%); --shadow-up: 0 -2px 10px rgb(0 0 0 / 30%); - --shadow-info-hover: 0 4px 12px rgb(59 130 246 / 50%); - --shadow-info-active: 0 4px 16px rgb(59 130 246 / 60%); + --shadow-info-hover: 0 4px 12px rgb(59 130 246 / 50%); /* drifted from base; retuned in step 7 */ + --shadow-info-active: 0 4px 16px rgb(59 130 246 / 60%); /* drifted from base; retuned in step 7 */ --shadow-admin-hover: 0 4px 12px rgb(103 58 183 / 50%); --shadow-admin-active: 0 4px 16px rgb(103 58 183 / 60%); --shadow-segment-hover: 0 4px 16px rgb(159 122 234 / 50%); @@ -293,7 +299,7 @@ --color-nav-sub-focus: var(--color-admin); /* One-off decorations */ - --marker-line-bg: rgb(124 58 237 / 30%); + --marker-line-bg: color-mix(in srgb, var(--color-admin) 30%, transparent); --logo-filter: brightness(0) invert(0.3); --logo-filter-hover: brightness(0) invert(0.1); @@ -303,14 +309,14 @@ --color-overlay-strong: rgb(0 0 0 / 8%); /* Status Overlays */ - --color-success-overlay: rgb(21 128 61 / 8%); - --color-warning-overlay: rgb(180 83 9 / 8%); - --color-error-overlay: rgb(220 38 38 / 8%); - --color-info-overlay: rgb(29 78 216 / 8%); + --color-success-overlay: color-mix(in srgb, var(--color-success) 8%, transparent); + --color-warning-overlay: color-mix(in srgb, var(--color-warning) 8%, transparent); + --color-error-overlay: color-mix(in srgb, var(--color-error) 8%, transparent); + --color-info-overlay: color-mix(in srgb, var(--color-info) 8%, transparent); /* Accent Overlays */ - --color-accent-overlay: rgb(79 70 229 / 7%); - --color-accent-glow: rgb(79 70 229 / 15%); + --color-accent-overlay: color-mix(in srgb, var(--color-accent) 7%, transparent); + --color-accent-glow: color-mix(in srgb, var(--color-accent) 15%, transparent); /* Status Gradient Endpoints */ --color-success-accent: #059669; @@ -326,22 +332,22 @@ --color-overlay-shimmer: rgb(0 0 0 / 6%); /* Status Borders */ - --color-success-border: rgb(21 128 61 / 30%); - --color-warning-border: rgb(180 83 9 / 30%); - --color-error-border: rgb(220 38 38 / 30%); - --color-info-border: rgb(29 78 216 / 30%); + --color-success-border: color-mix(in srgb, var(--color-success) 30%, transparent); + --color-warning-border: color-mix(in srgb, var(--color-warning) 30%, transparent); + --color-error-border: color-mix(in srgb, var(--color-error) 30%, transparent); + --color-info-border: color-mix(in srgb, var(--color-info) 30%, transparent); /* Modal Backdrops (reduced — less darkening over light content) */ --color-backdrop: rgb(0 0 0 / 30%); --color-backdrop-heavy: rgb(0 0 0 / 60%); /* Colored Glows (~50% opacity reduction) */ - --color-accent-shadow: rgb(79 70 229 / 20%); - --color-admin-shadow: rgb(91 33 182 / 15%); - --color-info-shadow: rgb(29 78 216 / 15%); - --color-segment-shadow: rgb(124 58 237 / 15%); - --color-warning-pulse: rgb(180 83 9 / 50%); - --color-warning-pulse-end: rgb(180 83 9 / 0%); + --color-accent-shadow: color-mix(in srgb, var(--color-accent) 20%, transparent); + --color-admin-shadow: rgb(91 33 182 / 15%); /* drifted from base; retuned in step 7 */ + --color-info-shadow: color-mix(in srgb, var(--color-info) 15%, transparent); + --color-segment-shadow: color-mix(in srgb, var(--color-category-1) 15%, transparent); + --color-warning-pulse: color-mix(in srgb, var(--color-warning) 50%, transparent); + --color-warning-pulse-end: transparent; /* Categorical ramp (deeper for contrast on light backgrounds) */ --color-category-1: #7c3aed; @@ -352,7 +358,7 @@ --color-category-6: #dc2626; /* Ace Editor */ - --color-ace-selection: rgb(107 0 107 / 20%); + --color-ace-selection: color-mix(in srgb, var(--color-primary) 20%, transparent); /* Shadows (same geometry, ~60-70% opacity reduction) */ --shadow-xs: 0 1px 3px rgb(0 0 0 / 4%); @@ -372,7 +378,7 @@ --shadow-up: 0 -2px 10px rgb(0 0 0 / 6%); --shadow-info-hover: 0 4px 12px rgb(29 78 216 / 20%); --shadow-info-active: 0 4px 16px rgb(29 78 216 / 25%); - --shadow-admin-hover: 0 4px 12px rgb(91 33 182 / 20%); - --shadow-admin-active: 0 4px 16px rgb(91 33 182 / 25%); + --shadow-admin-hover: 0 4px 12px rgb(91 33 182 / 20%); /* drifted from base; retuned in step 7 */ + --shadow-admin-active: 0 4px 16px rgb(91 33 182 / 25%); /* drifted from base; retuned in step 7 */ --shadow-segment-hover: 0 4px 16px rgb(124 58 237 / 20%); } From dcb760d94768eedd3848891829eafae68c97f517 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 17 Oct 2026 11:40:03 -0400 Subject: [PATCH 8/9] Theme step 7a: Studio light and Dusk dark palettes Applies the two chosen palettes as token values in styles/tokens.css. Light (Studio): one violet accent (#6d28d9) that primary, info, admin and the gradient stops alias; --gradient-primary and --gradient-admin are flat; cool grey surfaces with --color-bg-header equal to the panel, so header bands merge with their card and only the hairline stays; quieter shadows and no nav shadow; sub-navigation bars on the panel with an admin tint for the active tab. Dark (Dusk): warm violet-grey surfaces, lavender accent (#c3a6ff) for text, borders and the focus ring, #7c3aed for filled buttons; the CTA keeps a soft violet gradient, admin fills and gradient text go solid; filled header bands stay. Status colours move to the Tailwind 300/400 steps. The drifted alpha literals (dark --color-info-shadow and --shadow-info-*, light --color-admin-shadow and --shadow-admin-*) are now color-mix of their bases. --color-nav-sub-hover-border points at --color-border in both themes. Ace: tomorrow in light and tomorrow_night_eighties in dark; the light syntax tokens follow tomorrow (number darkened to #b45309 for AA) and the dark ones follow tomorrow_night_eighties. Co-Authored-By: Claude Opus 5.5 --- .../composables/__tests__/useTheme.test.ts | 4 +- purplex/client/src/composables/useTheme.ts | 2 +- purplex/client/src/features/editor/Editor.vue | 4 +- .../editor/__tests__/Editor.keyboard.test.ts | 4 +- .../features/editor/__tests__/Editor.test.ts | 12 +- purplex/client/src/styles/tokens.css | 312 +++++++++--------- 6 files changed, 171 insertions(+), 167 deletions(-) diff --git a/purplex/client/src/composables/__tests__/useTheme.test.ts b/purplex/client/src/composables/__tests__/useTheme.test.ts index a09c7450..cbe03a78 100644 --- a/purplex/client/src/composables/__tests__/useTheme.test.ts +++ b/purplex/client/src/composables/__tests__/useTheme.test.ts @@ -160,8 +160,8 @@ describe('useTheme', () => { describe('editorTheme', () => { it.each([ - ['light', 'chrome'], - ['dark', 'tomorrow_night'], + ['light', 'tomorrow'], + ['dark', 'tomorrow_night_eighties'], ] as const)('maps %s to the %s Ace theme', async (effective, aceTheme) => { const { setTheme, editorTheme } = (await loadUseTheme())() diff --git a/purplex/client/src/composables/useTheme.ts b/purplex/client/src/composables/useTheme.ts index ed199155..6a604985 100644 --- a/purplex/client/src/composables/useTheme.ts +++ b/purplex/client/src/composables/useTheme.ts @@ -77,7 +77,7 @@ export function useTheme() { initialize() const editorTheme = computed(() => - effectiveTheme.value === 'light' ? 'chrome' : 'tomorrow_night' + effectiveTheme.value === 'light' ? 'tomorrow' : 'tomorrow_night_eighties' ) return { diff --git a/purplex/client/src/features/editor/Editor.vue b/purplex/client/src/features/editor/Editor.vue index ef39cbd9..ab3eda0f 100644 --- a/purplex/client/src/features/editor/Editor.vue +++ b/purplex/client/src/features/editor/Editor.vue @@ -33,8 +33,8 @@ import { VAceEditor } from 'vue3-ace-editor' import { useTheme } from '@/composables/useTheme' import ace from 'ace-builds' import 'ace-builds/src-noconflict/mode-python' -import 'ace-builds/src-noconflict/theme-chrome' -import 'ace-builds/src-noconflict/theme-tomorrow_night' +import 'ace-builds/src-noconflict/theme-tomorrow' +import 'ace-builds/src-noconflict/theme-tomorrow_night_eighties' // Marker definition for line highlighting interface EditorMarker { diff --git a/purplex/client/src/features/editor/__tests__/Editor.keyboard.test.ts b/purplex/client/src/features/editor/__tests__/Editor.keyboard.test.ts index deed1740..c5a144cd 100644 --- a/purplex/client/src/features/editor/__tests__/Editor.keyboard.test.ts +++ b/purplex/client/src/features/editor/__tests__/Editor.keyboard.test.ts @@ -28,8 +28,8 @@ vi.mock('vue3-ace-editor', () => ({ })) vi.mock('ace-builds/src-noconflict/mode-python', () => ({})) -vi.mock('ace-builds/src-noconflict/theme-chrome', () => ({})) -vi.mock('ace-builds/src-noconflict/theme-tomorrow_night', () => ({})) +vi.mock('ace-builds/src-noconflict/theme-tomorrow', () => ({})) +vi.mock('ace-builds/src-noconflict/theme-tomorrow_night_eighties', () => ({})) describe('Editor keyboard behaviour (real Ace)', () => { type RealAceEditor = import('ace-builds').Ace.Editor diff --git a/purplex/client/src/features/editor/__tests__/Editor.test.ts b/purplex/client/src/features/editor/__tests__/Editor.test.ts index 7930df13..3a4ad045 100644 --- a/purplex/client/src/features/editor/__tests__/Editor.test.ts +++ b/purplex/client/src/features/editor/__tests__/Editor.test.ts @@ -47,8 +47,8 @@ vi.mock('vue3-ace-editor', () => ({ })) vi.mock('ace-builds/src-noconflict/mode-python', () => ({})) -vi.mock('ace-builds/src-noconflict/theme-chrome', () => ({})) -vi.mock('ace-builds/src-noconflict/theme-tomorrow_night', () => ({})) +vi.mock('ace-builds/src-noconflict/theme-tomorrow', () => ({})) +vi.mock('ace-builds/src-noconflict/theme-tomorrow_night_eighties', () => ({})) type Fn = ReturnType @@ -198,8 +198,8 @@ describe('Editor Component', () => { expect(aceEditor.props()).toMatchObject({ lang: 'python', // Comes from useTheme(): happy-dom's matchMedia reports - // prefers-color-scheme: light, so 'auto' resolves to light -> 'chrome' - theme: 'chrome', + // prefers-color-scheme: light, so 'auto' resolves to light -> 'tomorrow' + theme: 'tomorrow', mode: 'python', value: 'print("Hello")', style: { height: '300px', width: '500px' }, @@ -602,11 +602,11 @@ describe('Editor Component', () => { // Dark first: in happy-dom the 'auto' default already resolves to light useTheme().setTheme('dark') await nextTick() - expect(aceEditor.props().theme).toBe('tomorrow_night') + expect(aceEditor.props().theme).toBe('tomorrow_night_eighties') useTheme().setTheme('light') await nextTick() - expect(aceEditor.props().theme).toBe('chrome') + expect(aceEditor.props().theme).toBe('tomorrow') }) }) }) diff --git a/purplex/client/src/styles/tokens.css b/purplex/client/src/styles/tokens.css index 079e7881..156d6a14 100644 --- a/purplex/client/src/styles/tokens.css +++ b/purplex/client/src/styles/tokens.css @@ -68,81 +68,83 @@ --color-focus-ring: var(--color-accent); } +/* Dark theme: Dusk (warm violet-grey surfaces, lavender accent) */ :root, [data-theme="dark"] { color-scheme: dark; - /* Primary Colors */ - --color-primary: #800080; - --color-primary-hover: #9b009b; - --color-accent: #667eea; - --color-accent-strong: #764ba2; + /* Primary Colors (primary = filled buttons; accent = text, borders, focus) */ + --color-primary: #7c3aed; + --color-primary-hover: #8855f0; + --color-accent: #c3a6ff; + --color-accent-strong: #d8c6ff; /* Gradients (stops only inside gradients; prefer the whole-gradient tokens) */ - --color-gradient-start: #667eea; - --color-gradient-end: #764ba2; + --color-gradient-start: #8b5cf6; + --color-gradient-end: #7c3aed; --gradient-primary: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 100%); - --gradient-admin: linear-gradient(135deg, var(--color-admin) 0%, var(--color-gradient-end) 100%); - --gradient-admin-hover: var(--gradient-admin); - --gradient-logo: linear-gradient(135deg, #a78bfa, #c4b5fd, #8b7ec8); + --gradient-admin: var(--color-admin); + --gradient-admin-hover: var(--color-admin-hover); + --gradient-logo: linear-gradient(135deg, #c3a6ff, #e0d3ff, #9d7cf6); /* Background Colors */ - --color-bg-main: #242424; - --color-bg-sunken: #1a1a1a; - --color-bg-panel: #1e1e1e; - --color-bg-header: #191919; - --color-bg-hover: #2a2a2a; - --color-bg-section: #2a2a2a; - --color-bg-input: #333; - --color-border: #444; - --color-bg-disabled: #555; + --color-bg-main: #2a2528; + --color-bg-sunken: #221f21; + --color-bg-panel: #342e32; + --color-bg-header: #3e373c; + --color-bg-hover: #484045; + --color-bg-section: #3e373c; + --color-bg-input: #423b40; + --color-border: #564e54; + --color-bg-disabled: #877d84; /* Text Colors */ - --color-text-primary: #fff; - --color-text-secondary: #e0e0e0; - --color-text-tertiary: #ddd; - --color-text-muted: #999; - --color-text-default: rgb(255 255 255 / 87%); + --color-text-primary: #f5f0f3; + --color-text-secondary: #d6cdd3; + --color-text-tertiary: #b0a6ad; + --color-text-muted: #b0a6ad; + --color-text-default: rgb(245 240 243 / 92%); --color-text-on-filled: #fff; - /* Syntax highlighting (token colors — tomorrow_night palette) */ - --color-syntax-keyword: #B294BB; - --color-syntax-string: #B5BD68; - --color-syntax-comment: #969896; - --color-syntax-number: #DE935F; - --color-syntax-builtin: #81A2BE; - --color-syntax-variable: #C66; - --color-syntax-operator: #8ABEB7; - --color-syntax-punctuation: #C5C8C6; + /* Syntax highlighting (token colors — tomorrow_night_eighties palette) */ + --color-syntax-keyword: #c9c; + --color-syntax-string: #9c9; + --color-syntax-comment: #999; + --color-syntax-number: #f99157; + --color-syntax-builtin: #69c; + --color-syntax-variable: #f2777a; + --color-syntax-operator: #6cc; + --color-syntax-punctuation: #ccc; /* Status Colors */ - --color-success: #4CAF50; - --color-success-bg: #2d4d3a; - --color-success-text: #a3e9c1; - --color-warning: #FFC107; - --color-warning-bg: #4d4c2d; - --color-warning-text: #e9e4a3; - --color-error: #dc3545; - --color-error-bg: #4d2d2d; - --color-error-text: #e9a3a3; - --color-info: #2196F3; - --color-info-bg: #2d3a4d; - - /* Admin Colors */ - --color-admin: #673ab7; - --color-admin-hover: #5e35b1; - --color-admin-bg: #673ab7; + --color-success: #4ade80; + --color-success-bg: #1f3a28; + --color-success-text: #86efac; + --color-warning: #fbbf24; + --color-warning-bg: #3b2d16; + --color-warning-text: #fcd34d; + --color-error: #fb7185; + --color-error-bg: #41262e; + --color-error-text: #fda4af; + --color-info: var(--color-accent); + --color-info-bg: #3a2f52; + + /* Role Colors */ + --color-admin: var(--color-primary); + --color-admin-hover: var(--color-primary-hover); + --color-admin-bg: #3a2f52; + /* Sub-navigation bars (AdminNavBar, InstructorNavBar); --nav-sub-bg may be a gradient */ - --nav-sub-bg: linear-gradient(135deg, var(--color-admin) 0%, var(--color-admin-hover) 100%); - --color-nav-sub-text: var(--color-text-on-filled); - --color-nav-sub-active-bg: var(--color-overlay-strong); - --color-nav-sub-active-text: var(--color-text-on-filled); - --color-nav-sub-hover-border: var(--color-overlay-strong); + --nav-sub-bg: var(--color-bg-panel); + --color-nav-sub-text: var(--color-text-secondary); + --color-nav-sub-active-bg: var(--color-admin-bg); + --color-nav-sub-active-text: var(--color-accent-strong); + --color-nav-sub-hover-border: var(--color-border); --color-nav-sub-focus: var(--color-focus-ring); /* One-off decorations */ - --marker-line-bg: linear-gradient(to bottom, var(--color-admin), var(--color-border)); + --marker-line-bg: color-mix(in srgb, var(--color-accent) 30%, transparent); --logo-filter: brightness(0) invert(0.7); --logo-filter-hover: brightness(0) invert(0.9); @@ -162,13 +164,13 @@ --color-accent-glow: color-mix(in srgb, var(--color-accent) 30%, transparent); /* Status Gradient Endpoints */ - --color-success-accent: #34d399; - --color-warning-accent: #fbbf24; - --color-error-accent: #f87171; - --color-error-dark: #c82333; - --color-warning-dark: #e0a800; - --color-success-dark: #218838; - --color-info-dark: #1976d2; + --color-success-accent: #86efac; + --color-warning-accent: #fcd34d; + --color-error-accent: #fda4af; + --color-error-dark: #e11d48; + --color-warning-dark: #b45309; + --color-success-dark: #15803d; + --color-info-dark: #7c3aed; /* Extended Overlays (white on dark, black on light) */ --color-overlay-border: rgb(255 255 255 / 25%); @@ -187,7 +189,7 @@ /* Colored Glows (box-shadows) */ --color-accent-shadow: color-mix(in srgb, var(--color-accent) 40%, transparent); --color-admin-shadow: color-mix(in srgb, var(--color-admin) 30%, transparent); - --color-info-shadow: rgb(59 130 246 / 30%); /* drifted from base; retuned in step 7 */ + --color-info-shadow: color-mix(in srgb, var(--color-primary) 30%, transparent); --color-segment-shadow: color-mix(in srgb, var(--color-category-1) 30%, transparent); --color-warning-pulse: color-mix(in srgb, var(--color-warning-accent) 70%, transparent); --color-warning-pulse-end: transparent; @@ -204,102 +206,104 @@ --color-ace-selection: color-mix(in srgb, var(--color-primary) 30%, transparent); /* Shadows */ - --shadow-xs: 0 1px 3px rgb(0 0 0 / 10%); - --shadow-subtle: 0 2px 4px rgb(0 0 0 / 10%); - --shadow-sm: 0 2px 8px rgb(0 0 0 / 20%); - --shadow-base: 0 4px 12px rgb(0 0 0 / 30%); - --shadow-md: 0 4px 10px rgb(0 0 0 / 30%); - --shadow-lg: 0 8px 25px rgb(0 0 0 / 40%); - --shadow-accent: 0 4px 15px rgb(102 126 234 / 30%); + --shadow-xs: 0 1px 3px rgb(0 0 0 / 30%); + --shadow-subtle: 0 1px 3px rgb(0 0 0 / 30%); + --shadow-sm: 0 2px 8px rgb(0 0 0 / 30%); + --shadow-base: 0 4px 12px rgb(0 0 0 / 35%); + --shadow-md: 0 4px 12px rgb(0 0 0 / 35%); + --shadow-lg: 0 8px 24px rgb(0 0 0 / 40%); + --shadow-accent: 0 4px 14px color-mix(in srgb, var(--color-primary) 30%, transparent); /* One-off shadows */ - --shadow-nav: 0 2px 10px rgb(0 0 0 / 30%); - --shadow-dropdown: 0 8px 32px rgb(0 0 0 / 40%); - --shadow-modal: 0 4px 20px rgb(0 0 0 / 15%); - --shadow-inset: inset 0 1px 3px rgb(0 0 0 / 20%); + --shadow-nav: none; + --shadow-dropdown: 0 8px 32px rgb(0 0 0 / 45%); + --shadow-modal: 0 4px 20px rgb(0 0 0 / 40%); + --shadow-inset: inset 0 1px 3px rgb(0 0 0 / 30%); --shadow-float: 0 24px 64px rgb(0 0 0 / 50%); --shadow-up: 0 -2px 10px rgb(0 0 0 / 30%); - --shadow-info-hover: 0 4px 12px rgb(59 130 246 / 50%); /* drifted from base; retuned in step 7 */ - --shadow-info-active: 0 4px 16px rgb(59 130 246 / 60%); /* drifted from base; retuned in step 7 */ - --shadow-admin-hover: 0 4px 12px rgb(103 58 183 / 50%); - --shadow-admin-active: 0 4px 16px rgb(103 58 183 / 60%); - --shadow-segment-hover: 0 4px 16px rgb(159 122 234 / 50%); + --shadow-info-hover: 0 4px 12px color-mix(in srgb, var(--color-primary) 30%, transparent); + --shadow-info-active: 0 4px 12px color-mix(in srgb, var(--color-primary) 40%, transparent); + --shadow-admin-hover: 0 4px 12px color-mix(in srgb, var(--color-admin) 30%, transparent); + --shadow-admin-active: 0 4px 12px color-mix(in srgb, var(--color-admin) 40%, transparent); + --shadow-segment-hover: 0 4px 16px color-mix(in srgb, var(--color-accent) 30%, transparent); } +/* Light theme: Studio (cool greys, one violet accent, flat fills) */ [data-theme="light"] { color-scheme: light; - /* Primary Colors */ - --color-primary: #6b006b; - --color-primary-hover: #570057; - --color-accent: #4f46e5; - --color-accent-strong: #6d28d9; + /* Primary Colors (primary = filled buttons; accent = text, borders, focus) */ + --color-primary: #6d28d9; + --color-primary-hover: #5b21b6; + --color-accent: #6d28d9; + --color-accent-strong: #5b21b6; - /* Gradients (stops only inside gradients; prefer the whole-gradient tokens) */ - --color-gradient-start: #4f46e5; - --color-gradient-end: #6d28d9; - --gradient-primary: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 100%); + /* Gradients (flat in Studio: a solid colour is valid in `background`) */ + --color-gradient-start: #6d28d9; + --color-gradient-end: #5b21b6; + --gradient-primary: var(--color-accent); --gradient-admin: var(--color-admin); --gradient-admin-hover: var(--color-admin-hover); --gradient-logo: linear-gradient(135deg, #7c3aed, #8b5cf6, #6d28d9); - /* Background Colors */ - --color-bg-main: #f8f9fa; - --color-bg-sunken: #e9ecef; + /* Background Colors (header bands merge with the panel in Studio) */ + --color-bg-main: #f3f4f8; + --color-bg-sunken: #e6e8ef; --color-bg-panel: #fff; - --color-bg-header: #f1f3f5; - --color-bg-hover: #dde1e6; - --color-bg-section: #f5f5f7; - --color-bg-input: #e9ecef; - --color-border: #d1d5db; - --color-bg-disabled: #9ca3af; + --color-bg-header: #fff; + --color-bg-hover: #e8eaf1; + --color-bg-section: #f7f8fb; + --color-bg-input: #eef0f5; + --color-border: #e1e4ec; + --color-bg-disabled: #a3a8b8; /* Text Colors */ - --color-text-primary: #1a1a2e; - --color-text-secondary: #1f2937; - --color-text-tertiary: #374151; - --color-text-muted: #4b5563; - --color-text-default: rgb(26 26 46 / 87%); + --color-text-primary: #141624; + --color-text-secondary: #2b2f41; + --color-text-tertiary: #4a5069; + --color-text-muted: #636983; + --color-text-default: rgb(20 22 36 / 88%); --color-text-on-filled: #fff; - /* Syntax highlighting (token colors — chrome palette) */ - --color-syntax-keyword: #930f80; - --color-syntax-string: #1A1AA6; - --color-syntax-comment: #236e24; - --color-syntax-number: #0000CD; - --color-syntax-builtin: #3c4c72; - --color-syntax-variable: #318495; - --color-syntax-operator: #687687; - --color-syntax-punctuation: #333; + /* Syntax highlighting (token colors — tomorrow palette, number darkened for AA) */ + --color-syntax-keyword: #8959a8; + --color-syntax-string: #718c00; + --color-syntax-comment: #8e908c; + --color-syntax-number: #b45309; + --color-syntax-builtin: #4271ae; + --color-syntax-variable: #c82829; + --color-syntax-operator: #3e999f; + --color-syntax-punctuation: #4d4d4c; /* Status Colors */ --color-success: #15803d; - --color-success-bg: #dcfce7; + --color-success-bg: #e8f7ed; --color-success-text: #14532d; --color-warning: #b45309; - --color-warning-bg: #fef3c7; + --color-warning-bg: #fdf3df; --color-warning-text: #78350f; - --color-error: #dc2626; - --color-error-bg: #fee2e2; + --color-error: #b91c1c; + --color-error-bg: #fdecec; --color-error-text: #7f1d1d; - --color-info: #1d4ed8; - --color-info-bg: #dbeafe; - - /* Admin Colors */ - --color-admin: #7c3aed; - --color-admin-hover: #6d28d9; - --color-admin-bg: #ede9fe; - - /* Sub-navigation bars (inverted: soft admin tint, admin text, solid active tab) */ - --nav-sub-bg: var(--color-admin-bg); - --color-nav-sub-text: var(--color-admin); - --color-nav-sub-active-bg: var(--color-admin); - --color-nav-sub-active-text: var(--color-text-on-filled); - --color-nav-sub-hover-border: var(--color-admin-shadow); - --color-nav-sub-focus: var(--color-admin); + --color-info: var(--color-accent); + --color-info-bg: #efeafc; + + /* Role Colors */ + --color-admin: var(--color-accent); + --color-admin-hover: var(--color-primary-hover); + --color-admin-bg: #efeafc; + + + /* Sub-navigation bars (AdminNavBar, InstructorNavBar); --nav-sub-bg may be a gradient */ + --nav-sub-bg: var(--color-bg-panel); + --color-nav-sub-text: var(--color-text-secondary); + --color-nav-sub-active-bg: var(--color-admin-bg); + --color-nav-sub-active-text: var(--color-admin); + --color-nav-sub-hover-border: var(--color-border); + --color-nav-sub-focus: var(--color-focus-ring); /* One-off decorations */ - --marker-line-bg: color-mix(in srgb, var(--color-admin) 30%, transparent); + --marker-line-bg: color-mix(in srgb, var(--color-accent) 30%, transparent); --logo-filter: brightness(0) invert(0.3); --logo-filter-hover: brightness(0) invert(0.1); @@ -321,11 +325,11 @@ /* Status Gradient Endpoints */ --color-success-accent: #059669; --color-warning-accent: #d97706; - --color-error-accent: #ef4444; - --color-error-dark: #b91c1c; + --color-error-accent: #dc2626; + --color-error-dark: #991b1b; --color-warning-dark: #92400e; --color-success-dark: #065f46; - --color-info-dark: #1e40af; + --color-info-dark: #5b21b6; /* Extended Overlays */ --color-overlay-border: rgb(0 0 0 / 12%); @@ -343,7 +347,7 @@ /* Colored Glows (~50% opacity reduction) */ --color-accent-shadow: color-mix(in srgb, var(--color-accent) 20%, transparent); - --color-admin-shadow: rgb(91 33 182 / 15%); /* drifted from base; retuned in step 7 */ + --color-admin-shadow: color-mix(in srgb, var(--color-admin) 15%, transparent); --color-info-shadow: color-mix(in srgb, var(--color-info) 15%, transparent); --color-segment-shadow: color-mix(in srgb, var(--color-category-1) 15%, transparent); --color-warning-pulse: color-mix(in srgb, var(--color-warning) 50%, transparent); @@ -360,25 +364,25 @@ /* Ace Editor */ --color-ace-selection: color-mix(in srgb, var(--color-primary) 20%, transparent); - /* Shadows (same geometry, ~60-70% opacity reduction) */ - --shadow-xs: 0 1px 3px rgb(0 0 0 / 4%); - --shadow-subtle: 0 2px 4px rgb(0 0 0 / 4%); - --shadow-sm: 0 2px 8px rgb(0 0 0 / 6%); - --shadow-base: 0 4px 12px rgb(0 0 0 / 8%); - --shadow-md: 0 4px 10px rgb(0 0 0 / 8%); - --shadow-lg: 0 8px 25px rgb(0 0 0 / 10%); - --shadow-accent: 0 4px 15px rgb(79 70 229 / 15%); + /* Shadows (quiet: hairline borders carry the separation) */ + --shadow-xs: 0 1px 2px rgb(20 22 36 / 5%); + --shadow-subtle: 0 1px 2px rgb(20 22 36 / 5%); + --shadow-sm: 0 1px 2px rgb(20 22 36 / 5%); + --shadow-base: 0 2px 6px rgb(20 22 36 / 6%); + --shadow-md: 0 2px 6px rgb(20 22 36 / 6%); + --shadow-lg: 0 8px 24px rgb(20 22 36 / 8%); + --shadow-accent: 0 2px 8px color-mix(in srgb, var(--color-accent) 15%, transparent); /* One-off shadows */ - --shadow-nav: 0 2px 10px rgb(0 0 0 / 6%); - --shadow-dropdown: 0 8px 32px rgb(0 0 0 / 10%); - --shadow-modal: 0 4px 20px rgb(0 0 0 / 8%); - --shadow-inset: inset 0 1px 3px rgb(0 0 0 / 6%); - --shadow-float: 0 24px 64px rgb(0 0 0 / 12%); - --shadow-up: 0 -2px 10px rgb(0 0 0 / 6%); - --shadow-info-hover: 0 4px 12px rgb(29 78 216 / 20%); - --shadow-info-active: 0 4px 16px rgb(29 78 216 / 25%); - --shadow-admin-hover: 0 4px 12px rgb(91 33 182 / 20%); /* drifted from base; retuned in step 7 */ - --shadow-admin-active: 0 4px 16px rgb(91 33 182 / 25%); /* drifted from base; retuned in step 7 */ - --shadow-segment-hover: 0 4px 16px rgb(124 58 237 / 20%); + --shadow-nav: none; + --shadow-dropdown: 0 8px 24px rgb(20 22 36 / 10%); + --shadow-modal: 0 4px 20px rgb(20 22 36 / 8%); + --shadow-inset: inset 0 1px 2px rgb(20 22 36 / 6%); + --shadow-float: 0 16px 48px rgb(20 22 36 / 12%); + --shadow-up: 0 -1px 4px rgb(20 22 36 / 6%); + --shadow-info-hover: 0 2px 8px color-mix(in srgb, var(--color-info) 15%, transparent); + --shadow-info-active: 0 2px 8px color-mix(in srgb, var(--color-info) 20%, transparent); + --shadow-admin-hover: 0 2px 8px color-mix(in srgb, var(--color-admin) 15%, transparent); + --shadow-admin-active: 0 2px 8px color-mix(in srgb, var(--color-admin) 20%, transparent); + --shadow-segment-hover: 0 4px 12px color-mix(in srgb, var(--color-category-1) 15%, transparent); } From 2c1c677aac5d8f532741bb390750351dc4100322 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 17 Oct 2026 11:52:03 -0400 Subject: [PATCH 9/9] Theme step 7b: components, semantic colours and docs Component changes that go with the Studio and Dusk palettes, all through tokens (no [data-theme] outside tokens.css). - NavBar: Instructor and Admin share one role style on the new --color-nav-item-bg/-text/-hover-bg/-border tokens (tinted in Dusk, plain text in Studio); the per-role fills and glows are gone. Account sits on the panel with a hairline. Active route is --color-nav-item-hover-bg plus accent text and a solid accent underline; the rule follows hover at equal specificity, so it holds while hovered. - Probe button and TestLadder Debug button: tinted (--color-info-bg with the restored --color-info-text). Debug shows a visible "Debug"/"Depurar" label (feedback.tests.debug). Passing pips get a success border; the failing pip is solid --color-error-dark without the double ring. - ProblemSet progress strip: in-progress moves from warning to info. - Semantic sites: --color-instructor (new) for StatusBadge and the AccountModal role badge (outline, since lavender cannot carry white text in Dusk); admin badges are solid --color-admin. StatusBadge gains category-1..6 outline variants used for ProblemList problem types (border carries the ramp colour, text stays neutral because the ramp clears 3:1 but not 4.5:1). Data-type badges move to one global styles/data-types.css on the ramp; the dead copy in EiplProblemEditor is removed. SegmentAnalysisModal goal badge uses the accent; ProbePanel .fn-name uses --color-syntax-builtin. - Dead after the restyle and deleted: --color-info-shadow, --color-admin-shadow, --shadow-info-hover/-active, --shadow-admin-hover/-active. - Docs: DEVELOPMENT.md "Dark Theme Only" replaced by a short Theming section pointing at tokens.css and yarn theme:check; TECH_DEBT.md item 25 removed; stale dark-only comment in style.css removed. Co-Authored-By: Claude Opus 5.5 --- docs/development/DEVELOPMENT.md | 58 +++----------- docs/reference/TECH_DEBT.md | 14 +--- purplex/client/src/components/NavBar.vue | 75 ++++++------------- .../activities/feedback/TestLadder.vue | 23 +++--- .../activities/inputs/shared/ProbePanel.vue | 17 ++--- .../admin/editors/EiplProblemEditor.vue | 45 ----------- .../admin/editors/shared/TestCasesSection.vue | 54 ------------- .../src/components/content/ProblemList.vue | 19 +++-- .../segmentation/SegmentAnalysisModal.vue | 6 +- .../client/src/components/ui/StatusBadge.vue | 36 ++++++--- .../src/features/problems/ProblemSet.vue | 12 +-- .../client/src/i18n/locales/en/feedback.json | 1 + .../client/src/i18n/locales/es/feedback.json | 1 + purplex/client/src/modals/AccountModal.vue | 10 ++- purplex/client/src/style.css | 5 +- purplex/client/src/styles/data-types.css | 37 +++++++++ purplex/client/src/styles/tokens.css | 26 ++++--- purplex/client/src/types/datatable.ts | 15 +++- 18 files changed, 177 insertions(+), 277 deletions(-) create mode 100644 purplex/client/src/styles/data-types.css diff --git a/docs/development/DEVELOPMENT.md b/docs/development/DEVELOPMENT.md index 4c263785..2aefe0a4 100644 --- a/docs/development/DEVELOPMENT.md +++ b/docs/development/DEVELOPMENT.md @@ -664,54 +664,14 @@ Always include relevant context with logs and distinguish between what developer This section documents the established design system, patterns, and conventions used throughout the Purplex frontend. -### CSS Architecture - -The project uses a **CSS Variables-based design system** defined in `App.vue` that provides consistent theming across all components. - -#### Color System -```css -/* Primary Brand Colors */ ---color-primary: #800080; ---color-primary-hover: #9b009b; ---color-primary-gradient-start: #667eea; ---color-primary-gradient-end: #764ba2; - -/* Background Hierarchy */ ---color-bg-main: #242424; /* Main app background */ ---color-bg-dark: #1a1a1a; /* Darker sections */ ---color-bg-panel: #1e1e1e; /* Card/panel backgrounds */ ---color-bg-header: #191919; /* Header backgrounds */ ---color-bg-hover: #2a2a2a; /* Hover states */ - -/* Text Hierarchy */ ---color-text-primary: #ffffff; /* Main text */ ---color-text-secondary: #e0e0e0; /* Secondary text */ ---color-text-muted: #999; /* Muted/disabled text */ - -/* Status Colors */ ---color-success: #4CAF50; ---color-warning: #FFC107; ---color-error: #dc3545; ---color-info: #2196F3; -``` - -#### Spacing System -```css ---spacing-xs: 4px; ---spacing-sm: 8px; ---spacing-md: 12px; ---spacing-base: 15px; ---spacing-lg: 20px; ---spacing-xl: 30px; ---spacing-xxl: 50px; -``` - -### Component Styling Patterns - -1. **Scoped Styles**: All Vue components use ` diff --git a/purplex/client/src/features/problems/ProblemSet.vue b/purplex/client/src/features/problems/ProblemSet.vue index 52144176..7973b8ac 100644 --- a/purplex/client/src/features/problems/ProblemSet.vue +++ b/purplex/client/src/features/problems/ProblemSet.vue @@ -2280,9 +2280,9 @@ export default { } .progress-stat.in_progress { - color: var(--color-warning); - background: var(--color-warning-bg); - border-color: var(--color-warning); + color: var(--color-info); + background: var(--color-info-bg); + border-color: var(--color-info); } .progress-stat.remaining { @@ -2319,7 +2319,7 @@ export default { } .progress-bar.in_progress { - background: var(--color-warning); /* Yellow for incomplete/partial */ + background: var(--color-info); /* Violet for attempted, not yet complete */ } .progress-bar.completed { @@ -2396,8 +2396,8 @@ export default { } .progress-bar.active.in_progress { - background: var(--color-warning); /* Yellow for incomplete/partial */ - box-shadow: 0 0 0 2px var(--color-bg-panel), 0 0 0 4px var(--color-warning); + background: var(--color-info); /* Violet for attempted, not yet complete */ + box-shadow: 0 0 0 2px var(--color-bg-panel), 0 0 0 4px var(--color-info); } .progress-bar.active.completed { diff --git a/purplex/client/src/i18n/locales/en/feedback.json b/purplex/client/src/i18n/locales/en/feedback.json index 2dfe1756..75280611 100644 --- a/purplex/client/src/i18n/locales/en/feedback.json +++ b/purplex/client/src/i18n/locales/en/feedback.json @@ -64,6 +64,7 @@ "abstractionOpened": "Abstraction Details dialog opened" }, "tests": { + "debug": "Debug", "passing": "{count} Passing", "failing": "{count} Failing", "failingGroup": "Failing Tests ({count})", diff --git a/purplex/client/src/i18n/locales/es/feedback.json b/purplex/client/src/i18n/locales/es/feedback.json index 845080be..f9e79ac9 100644 --- a/purplex/client/src/i18n/locales/es/feedback.json +++ b/purplex/client/src/i18n/locales/es/feedback.json @@ -64,6 +64,7 @@ "abstractionOpened": "Diálogo de Detalles de Abstracción abierto" }, "tests": { + "debug": "Depurar", "passing": "{count} Aprobadas", "failing": "{count} Fallidas", "failingGroup": "Pruebas Fallidas ({count})", diff --git a/purplex/client/src/modals/AccountModal.vue b/purplex/client/src/modals/AccountModal.vue index 6319c928..b74cc878 100644 --- a/purplex/client/src/modals/AccountModal.vue +++ b/purplex/client/src/modals/AccountModal.vue @@ -379,9 +379,15 @@ function getMemberSince(): string { } .role-badge.admin { - background: linear-gradient(135deg, var(--color-admin) 0%, var(--color-admin-hover) 100%); + background: var(--color-admin); color: var(--color-text-on-filled); - box-shadow: 0 2px 8px var(--color-admin-shadow); + border: 1px solid var(--color-admin); +} + +.role-badge.instructor { + background: var(--color-bg-panel); + color: var(--color-instructor); + border: 1px solid var(--color-instructor); } .role-badge.user { diff --git a/purplex/client/src/style.css b/purplex/client/src/style.css index 8f59d1f0..a8c85058 100644 --- a/purplex/client/src/style.css +++ b/purplex/client/src/style.css @@ -1,5 +1,6 @@ @import url('./styles/tokens.css'); @import url('./styles/segmentation.css'); +@import url('./styles/data-types.css'); * { box-sizing: border-box; @@ -185,7 +186,3 @@ textarea::selection { .bg-panel { background-color: var(--color-bg-panel); } - -/* Remove light mode for consistency */ - -/* All components now use dark theme CSS variables */ diff --git a/purplex/client/src/styles/data-types.css b/purplex/client/src/styles/data-types.css new file mode 100644 index 00000000..a73502e3 --- /dev/null +++ b/purplex/client/src/styles/data-types.css @@ -0,0 +1,37 @@ +/* + * Data-type badges on test-case inputs and outputs (TestCasesSection, used by + * every problem editor). Classes come from useTestCases().getTypeClass(). + * + * Types are labels, not status: each takes a categorical ramp slot as an + * outline. The ramp clears 3:1 but not 4.5:1, so the slot colours the border + * and the text stays neutral. Validation failures keep the error colour. + */ +.param-type-badge.type-number, +.param-type-badge.type-string, +.param-type-badge.type-boolean, +.param-type-badge.type-collection, +.param-type-badge.type-optional, +.param-type-badge.type-none, +.param-type-badge.type-any { + background: var(--color-bg-panel); + color: var(--color-text-secondary); + border: 1px solid var(--color-border); +} + +.param-type-badge.type-number { border-color: var(--color-category-2); } +.param-type-badge.type-string { border-color: var(--color-category-4); } +.param-type-badge.type-boolean { border-color: var(--color-category-1); } +.param-type-badge.type-collection { border-color: var(--color-category-5); } +.param-type-badge.type-optional { border-color: var(--color-category-3); } + +.param-type-badge.type-none, +.param-type-badge.type-any { + color: var(--color-text-muted); +} + +.param-type-badge.type-invalid, +.param-type-badge.type-error { + background: var(--color-error-bg); + color: var(--color-error-text); + border: 1px solid var(--color-error); +} diff --git a/purplex/client/src/styles/tokens.css b/purplex/client/src/styles/tokens.css index 156d6a14..4ae7e5c5 100644 --- a/purplex/client/src/styles/tokens.css +++ b/purplex/client/src/styles/tokens.css @@ -128,12 +128,19 @@ --color-error-text: #fda4af; --color-info: var(--color-accent); --color-info-bg: #3a2f52; + --color-info-text: #d8c6ff; /* Role Colors */ --color-admin: var(--color-primary); --color-admin-hover: var(--color-primary-hover); --color-admin-bg: #3a2f52; + --color-instructor: var(--color-accent); + /* Top navigation role items (Instructor, Admin) */ + --color-nav-item-bg: var(--color-info-bg); + --color-nav-item-text: var(--color-accent-strong); + --color-nav-item-hover-bg: #463a60; + --color-nav-item-border: transparent; /* Sub-navigation bars (AdminNavBar, InstructorNavBar); --nav-sub-bg may be a gradient */ --nav-sub-bg: var(--color-bg-panel); @@ -188,8 +195,6 @@ /* Colored Glows (box-shadows) */ --color-accent-shadow: color-mix(in srgb, var(--color-accent) 40%, transparent); - --color-admin-shadow: color-mix(in srgb, var(--color-admin) 30%, transparent); - --color-info-shadow: color-mix(in srgb, var(--color-primary) 30%, transparent); --color-segment-shadow: color-mix(in srgb, var(--color-category-1) 30%, transparent); --color-warning-pulse: color-mix(in srgb, var(--color-warning-accent) 70%, transparent); --color-warning-pulse-end: transparent; @@ -221,10 +226,6 @@ --shadow-inset: inset 0 1px 3px rgb(0 0 0 / 30%); --shadow-float: 0 24px 64px rgb(0 0 0 / 50%); --shadow-up: 0 -2px 10px rgb(0 0 0 / 30%); - --shadow-info-hover: 0 4px 12px color-mix(in srgb, var(--color-primary) 30%, transparent); - --shadow-info-active: 0 4px 12px color-mix(in srgb, var(--color-primary) 40%, transparent); - --shadow-admin-hover: 0 4px 12px color-mix(in srgb, var(--color-admin) 30%, transparent); - --shadow-admin-active: 0 4px 12px color-mix(in srgb, var(--color-admin) 40%, transparent); --shadow-segment-hover: 0 4px 16px color-mix(in srgb, var(--color-accent) 30%, transparent); } @@ -287,12 +288,19 @@ --color-error-text: #7f1d1d; --color-info: var(--color-accent); --color-info-bg: #efeafc; + --color-info-text: #3b1a86; /* Role Colors */ --color-admin: var(--color-accent); --color-admin-hover: var(--color-primary-hover); --color-admin-bg: #efeafc; + --color-instructor: var(--color-accent); + /* Top navigation role items (Instructor, Admin) */ + --color-nav-item-bg: transparent; + --color-nav-item-text: var(--color-text-secondary); + --color-nav-item-hover-bg: var(--color-bg-input); + --color-nav-item-border: transparent; /* Sub-navigation bars (AdminNavBar, InstructorNavBar); --nav-sub-bg may be a gradient */ --nav-sub-bg: var(--color-bg-panel); @@ -347,8 +355,6 @@ /* Colored Glows (~50% opacity reduction) */ --color-accent-shadow: color-mix(in srgb, var(--color-accent) 20%, transparent); - --color-admin-shadow: color-mix(in srgb, var(--color-admin) 15%, transparent); - --color-info-shadow: color-mix(in srgb, var(--color-info) 15%, transparent); --color-segment-shadow: color-mix(in srgb, var(--color-category-1) 15%, transparent); --color-warning-pulse: color-mix(in srgb, var(--color-warning) 50%, transparent); --color-warning-pulse-end: transparent; @@ -380,9 +386,5 @@ --shadow-inset: inset 0 1px 2px rgb(20 22 36 / 6%); --shadow-float: 0 16px 48px rgb(20 22 36 / 12%); --shadow-up: 0 -1px 4px rgb(20 22 36 / 6%); - --shadow-info-hover: 0 2px 8px color-mix(in srgb, var(--color-info) 15%, transparent); - --shadow-info-active: 0 2px 8px color-mix(in srgb, var(--color-info) 20%, transparent); - --shadow-admin-hover: 0 2px 8px color-mix(in srgb, var(--color-admin) 15%, transparent); - --shadow-admin-active: 0 2px 8px color-mix(in srgb, var(--color-admin) 20%, transparent); --shadow-segment-hover: 0 4px 12px color-mix(in srgb, var(--color-category-1) 15%, transparent); } diff --git a/purplex/client/src/types/datatable.ts b/purplex/client/src/types/datatable.ts index f09c8b91..cb1beee9 100644 --- a/purplex/client/src/types/datatable.ts +++ b/purplex/client/src/types/datatable.ts @@ -111,7 +111,20 @@ export interface UseDataTableOptions> { // ===== BADGE TYPES ===== /** Badge variant for StatusBadge component */ -export type BadgeVariant = 'success' | 'warning' | 'error' | 'info' | 'default' | 'admin' | 'instructor'; +export type BadgeVariant = + | 'success' + | 'warning' + | 'error' + | 'info' + | 'default' + | 'admin' + | 'instructor' + | 'category-1' + | 'category-2' + | 'category-3' + | 'category-4' + | 'category-5' + | 'category-6'; /** * Badge configuration for mapping values to variants.