@@ -0,0 +1,83 @@
|
||||
# ChicoryTV SPA Design Tokens Themes Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement issue #79 by making the SPA consume the ChicoryTV design-system token layer and switch live among the three shipped themes with persistence.
|
||||
|
||||
**Architecture:** Keep `design-system/styles.css` as the single token entry imported by `web/src/designSystem.ts`. Add a small typed theme contract in `web/src/designSystem.ts` that applies `data-theme` to `<html>`, maps warm to the design-system default, and persists the selection. Render a compact theme switcher from `App.tsx`, and align `shell.css` to real design-system custom properties.
|
||||
|
||||
**Tech Stack:** Vite, React 19, TypeScript, Vitest, Testing Library, CSS custom properties.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Theme Contract Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/App.test.tsx`
|
||||
|
||||
- [x] **Step 1: Write failing tests**
|
||||
|
||||
Add tests that assert:
|
||||
- `applyDesignSystemTheme("warm")` removes `data-theme` from `<html>` and stores `warm`.
|
||||
- `applyDesignSystemTheme("cool")` sets `data-theme="cool"` and stores `cool`.
|
||||
- Invalid persisted values fall back to `warm`.
|
||||
- The app renders three theme buttons and clicking `dual` sets `data-theme="dual"`.
|
||||
|
||||
- [x] **Step 2: Run tests to verify failure**
|
||||
|
||||
Run: `cd web && npm test -- --run src/App.test.tsx`
|
||||
Expected: FAIL because the theme functions and switcher do not exist yet.
|
||||
|
||||
### Task 2: Minimal Theme Implementation
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/designSystem.ts`
|
||||
- Modify: `web/src/App.tsx`
|
||||
|
||||
- [x] **Step 1: Implement theme functions**
|
||||
|
||||
Export `designSystemThemes`, `defaultDesignSystemTheme`, `getStoredDesignSystemTheme`, and `applyDesignSystemTheme`.
|
||||
|
||||
- [x] **Step 2: Render switcher**
|
||||
|
||||
Use React state initialized from `getStoredDesignSystemTheme()`, apply the theme on mount and click, and expose accessible buttons named for Warm chicory, Cool periwinkle, and Dual accent.
|
||||
|
||||
- [x] **Step 3: Run focused tests**
|
||||
|
||||
Run: `cd web && npm test -- --run src/App.test.tsx`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Token-Aligned Shell CSS
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/shell.css`
|
||||
- Modify: `web/index.html`
|
||||
|
||||
- [x] **Step 1: Replace scaffold token names**
|
||||
|
||||
Replace non-existent custom properties with actual design-system aliases: `--surface-app`, `--ctv-bg-sunken`, `--surface-card`, `--surface-raised`, `--border-hairline`, `--text-on-accent`, `--ctv-ok-soft`, spacing, radius, type, and motion tokens.
|
||||
|
||||
- [x] **Step 2: Use warm default correctly**
|
||||
|
||||
Remove `data-theme="warm"` from `web/index.html` so the design-system default `:root` warm theme is used until JavaScript applies persisted state.
|
||||
|
||||
- [x] **Step 3: Respect reduced motion**
|
||||
|
||||
Add `@media (prefers-reduced-motion: reduce)` rules that disable shell transitions.
|
||||
|
||||
### Task 4: Verification
|
||||
|
||||
**Files:**
|
||||
- No further production changes expected.
|
||||
|
||||
- [x] **Step 1: Run verification**
|
||||
|
||||
Run:
|
||||
- `cd web && npm test -- --run`
|
||||
- `cd web && npm run lint`
|
||||
- `cd web && npm run typecheck`
|
||||
- `cd web && npm run build`
|
||||
|
||||
- [x] **Step 2: Use done skill**
|
||||
|
||||
Run the `done` skill workflow for issue #79 before final handoff.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="warm">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
+64
-3
@@ -1,9 +1,35 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { App } from './App';
|
||||
import { designSystemStylesheet } from './designSystem';
|
||||
import {
|
||||
applyDesignSystemTheme,
|
||||
designSystemStylesheet,
|
||||
getStoredDesignSystemTheme
|
||||
} from './designSystem';
|
||||
|
||||
describe('ChicoryTV SPA scaffold', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!window.localStorage) {
|
||||
const storage = new Map<string, string>();
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
clear: () => storage.clear(),
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
removeItem: (key: string) => storage.delete(key),
|
||||
setItem: (key: string, value: string) => storage.set(key, value)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.localStorage.clear();
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
});
|
||||
|
||||
it('renders the placeholder shell and loads the design system stylesheet', () => {
|
||||
render(<App />);
|
||||
|
||||
@@ -12,4 +38,39 @@ describe('ChicoryTV SPA scaffold', () => {
|
||||
expect(screen.getByText('SPA foundation ready')).toBeInTheDocument();
|
||||
expect(designSystemStylesheet).toBe('../../design-system/styles.css');
|
||||
});
|
||||
|
||||
it('applies the warm design-system default by removing the theme attribute', () => {
|
||||
document.documentElement.dataset.theme = 'cool';
|
||||
|
||||
applyDesignSystemTheme('warm');
|
||||
|
||||
expect(document.documentElement).not.toHaveAttribute('data-theme');
|
||||
expect(window.localStorage.getItem('ctv-theme')).toBe('warm');
|
||||
});
|
||||
|
||||
it('applies and persists alternate design-system themes', () => {
|
||||
applyDesignSystemTheme('cool');
|
||||
|
||||
expect(document.documentElement).toHaveAttribute('data-theme', 'cool');
|
||||
expect(window.localStorage.getItem('ctv-theme')).toBe('cool');
|
||||
});
|
||||
|
||||
it('ignores invalid persisted themes', () => {
|
||||
window.localStorage.setItem('ctv-theme', 'unknown');
|
||||
|
||||
expect(getStoredDesignSystemTheme()).toBe('warm');
|
||||
});
|
||||
|
||||
it('switches themes from the shell controls', () => {
|
||||
render(<App />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dual accent theme' }));
|
||||
|
||||
expect(document.documentElement).toHaveAttribute('data-theme', 'dual');
|
||||
expect(window.localStorage.getItem('ctv-theme')).toBe('dual');
|
||||
expect(screen.getByRole('button', { name: 'Dual accent theme' })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
CalendarClock,
|
||||
LayoutDashboard,
|
||||
@@ -9,6 +10,12 @@ import {
|
||||
Tv
|
||||
} from 'lucide-react';
|
||||
import chicoryIconUrl from '../../design-system/assets/chicorytv-icon.svg';
|
||||
import {
|
||||
applyDesignSystemTheme,
|
||||
designSystemThemes,
|
||||
getStoredDesignSystemTheme,
|
||||
type DesignSystemThemeId
|
||||
} from './designSystem';
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Dashboard', icon: LayoutDashboard, active: true },
|
||||
@@ -26,6 +33,12 @@ const stats = [
|
||||
];
|
||||
|
||||
export function App() {
|
||||
const [theme, setTheme] = useState<DesignSystemThemeId>(() => getStoredDesignSystemTheme());
|
||||
|
||||
useEffect(() => {
|
||||
applyDesignSystemTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="ctv-app-shell">
|
||||
<aside className="ctv-sidebar">
|
||||
@@ -59,6 +72,23 @@ export function App() {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section className="ctv-theme-switcher" aria-label="Theme">
|
||||
<span>Theme</span>
|
||||
<div>
|
||||
{designSystemThemes.map(({ id, description, label }) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={`ctv-theme-swatch ctv-theme-swatch-${id}`}
|
||||
aria-label={description}
|
||||
aria-pressed={theme === id}
|
||||
title={label}
|
||||
onClick={() => setTheme(id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="ctv-panel" aria-labelledby="foundation-title">
|
||||
<div className="ctv-panel-header">
|
||||
<div>
|
||||
|
||||
@@ -1,3 +1,64 @@
|
||||
import '../../design-system/styles.css';
|
||||
|
||||
export const designSystemStylesheet = '../../design-system/styles.css';
|
||||
|
||||
export type DesignSystemThemeId = 'warm' | 'cool' | 'dual';
|
||||
|
||||
export type DesignSystemTheme = {
|
||||
id: DesignSystemThemeId;
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const defaultDesignSystemTheme: DesignSystemThemeId = 'warm';
|
||||
export const designSystemThemeStorageKey = 'ctv-theme';
|
||||
|
||||
export const designSystemThemes: DesignSystemTheme[] = [
|
||||
{
|
||||
id: 'warm',
|
||||
label: 'Warm chicory',
|
||||
description: 'Warm chicory theme'
|
||||
},
|
||||
{
|
||||
id: 'cool',
|
||||
label: 'Cool periwinkle',
|
||||
description: 'Cool periwinkle theme'
|
||||
},
|
||||
{
|
||||
id: 'dual',
|
||||
label: 'Dual accent',
|
||||
description: 'Dual accent theme'
|
||||
}
|
||||
];
|
||||
|
||||
function isDesignSystemTheme(value: string | null): value is DesignSystemThemeId {
|
||||
return value === 'warm' || value === 'cool' || value === 'dual';
|
||||
}
|
||||
|
||||
function getStorage(): Storage | undefined {
|
||||
if (typeof window === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return window.localStorage;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getStoredDesignSystemTheme(): DesignSystemThemeId {
|
||||
const storedTheme = getStorage()?.getItem(designSystemThemeStorageKey) ?? null;
|
||||
|
||||
return isDesignSystemTheme(storedTheme) ? storedTheme : defaultDesignSystemTheme;
|
||||
}
|
||||
|
||||
export function applyDesignSystemTheme(theme: DesignSystemThemeId): void {
|
||||
if (theme === defaultDesignSystemTheme) {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
}
|
||||
|
||||
getStorage()?.setItem(designSystemThemeStorageKey, theme);
|
||||
}
|
||||
|
||||
+154
-57
@@ -1,23 +1,25 @@
|
||||
.ctv-app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 232px minmax(0, 1fr);
|
||||
background: var(--ctv-bg, #0f1115);
|
||||
color: var(--text-primary, #e6e9ef);
|
||||
grid-template-columns: var(--sidebar-w, 232px) minmax(0, 1fr);
|
||||
background: var(--surface-app, #12100e);
|
||||
color: var(--text-primary, #f0ebe4);
|
||||
font-family: var(--font-sans, system-ui, sans-serif);
|
||||
font-size: var(--text-sm, 13px);
|
||||
line-height: var(--leading-normal, 1.5);
|
||||
}
|
||||
|
||||
.ctv-sidebar {
|
||||
border-right: 1px solid var(--border-default, #262b36);
|
||||
background: var(--surface-sunken, #0b0d11);
|
||||
padding: 16px 10px;
|
||||
border-right: 1px solid var(--border-hairline, #2e2823);
|
||||
background: var(--ctv-bg-sunken, #0c0b09);
|
||||
padding: var(--space-7, 16px) var(--space-5, 10px);
|
||||
}
|
||||
|
||||
.ctv-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 10px 18px;
|
||||
gap: var(--space-5, 10px);
|
||||
padding: 0 var(--space-5, 10px) var(--space-8, 20px);
|
||||
}
|
||||
|
||||
.ctv-brand img {
|
||||
@@ -29,12 +31,12 @@
|
||||
.ctv-topbar h2,
|
||||
.ctv-panel h3 {
|
||||
margin: 0;
|
||||
letter-spacing: 0;
|
||||
letter-spacing: var(--tracking-normal, 0);
|
||||
}
|
||||
|
||||
.ctv-brand h1 {
|
||||
font-size: 16px;
|
||||
line-height: 1.1;
|
||||
font-size: var(--text-lg, 16px);
|
||||
line-height: var(--leading-tight, 1.2);
|
||||
}
|
||||
|
||||
.ctv-brand span,
|
||||
@@ -47,113 +49,185 @@
|
||||
.ctv-brand span,
|
||||
.ctv-panel-header p,
|
||||
.ctv-topbar p {
|
||||
font-size: 12px;
|
||||
font-size: var(--text-xs, 12px);
|
||||
}
|
||||
|
||||
.ctv-nav {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
gap: var(--space-2, 4px);
|
||||
}
|
||||
|
||||
.ctv-nav a {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border-radius: 5px;
|
||||
color: var(--text-secondary, #9aa4b2);
|
||||
gap: var(--space-4, 8px);
|
||||
height: var(--control-h, 34px);
|
||||
padding: 0 var(--space-5, 10px);
|
||||
border-radius: var(--radius-sm, 5px);
|
||||
color: var(--text-secondary, #a89e92);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-size: var(--text-sm, 13px);
|
||||
transition:
|
||||
background var(--dur-fast, 90ms) var(--ease-standard, ease),
|
||||
color var(--dur-fast, 90ms) var(--ease-standard, ease);
|
||||
}
|
||||
|
||||
.ctv-nav a:hover,
|
||||
.ctv-nav a.is-active {
|
||||
background: var(--surface-selected, #222732);
|
||||
color: var(--text-primary, #e6e9ef);
|
||||
background: var(--surface-selected, #2c2620);
|
||||
color: var(--text-primary, #f0ebe4);
|
||||
}
|
||||
|
||||
.ctv-nav a.is-active::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
height: var(--space-8, 20px);
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
background: var(--action-primary, #e08a3c);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.ctv-main {
|
||||
min-width: 0;
|
||||
padding: 24px 32px;
|
||||
padding: var(--space-9, 24px) var(--pad-page, 32px);
|
||||
}
|
||||
|
||||
.ctv-topbar {
|
||||
min-height: 52px;
|
||||
min-height: var(--topbar-h, 52px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid var(--border-default, #262b36);
|
||||
padding-bottom: 18px;
|
||||
gap: var(--space-7, 16px);
|
||||
border-bottom: 1px solid var(--border-hairline, #2e2823);
|
||||
padding-bottom: var(--space-8, 20px);
|
||||
}
|
||||
|
||||
.ctv-topbar h2 {
|
||||
font-size: 20px;
|
||||
font-size: var(--text-xl, 20px);
|
||||
line-height: var(--leading-tight, 1.2);
|
||||
}
|
||||
|
||||
.ctv-topbar p,
|
||||
.ctv-panel-header p {
|
||||
margin: 0 0 4px;
|
||||
margin: 0 0 var(--space-2, 4px);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-weight: var(--weight-semibold, 600);
|
||||
letter-spacing: var(--tracking-caps, 0.06em);
|
||||
}
|
||||
|
||||
.ctv-primary-action {
|
||||
height: 34px;
|
||||
height: var(--control-h, 34px);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--gap-inline, 8px);
|
||||
border: 1px solid var(--action-primary, #e08a3c);
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius-sm, 5px);
|
||||
background: var(--action-primary, #e08a3c);
|
||||
color: var(--action-primary-text, #111318);
|
||||
padding: 0 12px;
|
||||
color: var(--text-on-accent, #1f1204);
|
||||
padding: 0 var(--space-6, 12px);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-weight: var(--weight-semibold, 600);
|
||||
transition:
|
||||
background var(--dur-fast, 90ms) var(--ease-standard, ease),
|
||||
border-color var(--dur-fast, 90ms) var(--ease-standard, ease);
|
||||
}
|
||||
|
||||
.ctv-primary-action:hover {
|
||||
border-color: var(--action-primary-hover, #ec9a52);
|
||||
background: var(--action-primary-hover, #ec9a52);
|
||||
}
|
||||
|
||||
.ctv-theme-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--gap-inline, 8px);
|
||||
margin-top: var(--space-7, 16px);
|
||||
color: var(--text-secondary, #a89e92);
|
||||
font-size: var(--text-xs, 12px);
|
||||
}
|
||||
|
||||
.ctv-theme-switcher span {
|
||||
font-weight: var(--weight-semibold, 600);
|
||||
letter-spacing: var(--tracking-caps, 0.06em);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ctv-theme-switcher div {
|
||||
display: flex;
|
||||
gap: var(--space-3, 6px);
|
||||
}
|
||||
|
||||
.ctv-theme-swatch {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition:
|
||||
border-color var(--dur-fast, 90ms) var(--ease-standard, ease),
|
||||
transform var(--dur-fast, 90ms) var(--ease-standard, ease);
|
||||
}
|
||||
|
||||
.ctv-theme-swatch:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ctv-theme-swatch:focus-visible {
|
||||
outline: 0;
|
||||
box-shadow: var(--ring-focus, 0 0 0 3px var(--focus-ring));
|
||||
}
|
||||
|
||||
.ctv-theme-swatch[aria-pressed="true"] {
|
||||
border-color: var(--text-primary, #f0ebe4);
|
||||
}
|
||||
|
||||
.ctv-theme-swatch-warm {
|
||||
background: #e08a3c;
|
||||
}
|
||||
|
||||
.ctv-theme-swatch-cool {
|
||||
background: #5b7cfa;
|
||||
}
|
||||
|
||||
.ctv-theme-swatch-dual {
|
||||
background: linear-gradient(135deg, #5b7cfa 0 50%, #f0663f 50% 100%);
|
||||
}
|
||||
|
||||
.ctv-panel {
|
||||
margin-top: 24px;
|
||||
border: 1px solid var(--border-default, #262b36);
|
||||
border-radius: 7px;
|
||||
background: var(--surface-default, #171a21);
|
||||
margin-top: var(--space-9, 24px);
|
||||
border: 1px solid var(--border-hairline, #2e2823);
|
||||
border-radius: var(--radius-md, 7px);
|
||||
background: var(--surface-card, #1c1917);
|
||||
}
|
||||
|
||||
.ctv-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--border-default, #262b36);
|
||||
gap: var(--space-7, 16px);
|
||||
padding: var(--space-7, 16px);
|
||||
border-bottom: 1px solid var(--border-hairline, #2e2823);
|
||||
}
|
||||
|
||||
.ctv-panel h3 {
|
||||
font-size: 16px;
|
||||
font-size: var(--text-lg, 16px);
|
||||
line-height: var(--leading-snug, 1.35);
|
||||
}
|
||||
|
||||
.ctv-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--status-ok-bg, rgba(63, 185, 132, 0.14));
|
||||
gap: var(--space-3, 6px);
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
background: var(--ctv-ok-soft, rgba(63, 185, 132, 0.14));
|
||||
color: var(--status-ok, #3fb984);
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: var(--space-2, 4px) var(--space-4, 8px);
|
||||
font-size: var(--text-xs, 12px);
|
||||
font-weight: var(--weight-semibold, 600);
|
||||
}
|
||||
|
||||
.ctv-stats {
|
||||
@@ -163,9 +237,9 @@
|
||||
|
||||
.ctv-stats div {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 16px;
|
||||
border-right: 1px solid var(--border-default, #262b36);
|
||||
gap: var(--space-3, 6px);
|
||||
padding: var(--space-7, 16px);
|
||||
border-right: 1px solid var(--border-hairline, #2e2823);
|
||||
}
|
||||
|
||||
.ctv-stats div:last-child {
|
||||
@@ -174,7 +248,9 @@
|
||||
|
||||
.ctv-stats strong {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 26px;
|
||||
font-size: var(--text-2xl, 26px);
|
||||
font-feature-settings: var(--numeric-feature);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -185,7 +261,7 @@
|
||||
|
||||
.ctv-sidebar {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--border-default, #262b36);
|
||||
border-bottom: 1px solid var(--border-hairline, #2e2823);
|
||||
}
|
||||
|
||||
.ctv-nav,
|
||||
@@ -194,6 +270,27 @@
|
||||
}
|
||||
|
||||
.ctv-main {
|
||||
padding: 20px 16px;
|
||||
padding: var(--space-8, 20px) var(--space-7, 16px);
|
||||
}
|
||||
|
||||
.ctv-topbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ctv-theme-switcher {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ctv-nav a,
|
||||
.ctv-primary-action,
|
||||
.ctv-theme-swatch {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.ctv-theme-swatch:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
environmentOptions: {
|
||||
jsdom: {
|
||||
url: 'http://localhost/app/'
|
||||
}
|
||||
},
|
||||
setupFiles: './src/setupTests.ts'
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user