diff --git a/ui/src/assets/icons/chat-bubbles.png b/ui/src/assets/icons/chat-bubbles.png
new file mode 100644
index 0000000..118e4d9
Binary files /dev/null and b/ui/src/assets/icons/chat-bubbles.png differ
diff --git a/ui/src/assets/icons/dashboard.png b/ui/src/assets/icons/dashboard.png
new file mode 100644
index 0000000..ea2f7e0
Binary files /dev/null and b/ui/src/assets/icons/dashboard.png differ
diff --git a/ui/src/assets/icons/logout.png b/ui/src/assets/icons/logout.png
new file mode 100644
index 0000000..a0b94ff
Binary files /dev/null and b/ui/src/assets/icons/logout.png differ
diff --git a/ui/src/assets/icons/moon.png b/ui/src/assets/icons/moon.png
new file mode 100644
index 0000000..ed711e9
Binary files /dev/null and b/ui/src/assets/icons/moon.png differ
diff --git a/ui/src/assets/icons/save.png b/ui/src/assets/icons/save.png
new file mode 100644
index 0000000..059ad8b
Binary files /dev/null and b/ui/src/assets/icons/save.png differ
diff --git a/ui/src/assets/icons/setting.png b/ui/src/assets/icons/setting.png
new file mode 100644
index 0000000..7f21dcb
Binary files /dev/null and b/ui/src/assets/icons/setting.png differ
diff --git a/ui/src/assets/icons/sun.png b/ui/src/assets/icons/sun.png
new file mode 100644
index 0000000..64e9653
Binary files /dev/null and b/ui/src/assets/icons/sun.png differ
diff --git a/ui/src/assets/icons/upload.png b/ui/src/assets/icons/upload.png
new file mode 100644
index 0000000..ba0d959
Binary files /dev/null and b/ui/src/assets/icons/upload.png differ
diff --git a/ui/src/components/app/ChatInput.test.tsx b/ui/src/components/app/ChatInput.test.tsx
new file mode 100644
index 0000000..af0d399
--- /dev/null
+++ b/ui/src/components/app/ChatInput.test.tsx
@@ -0,0 +1,23 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { ChatInput } from "@/components/app/ChatInput";
+
+describe("ChatInput", () => {
+ it("does not render the removed sample question chips", () => {
+ render(
+
{message.content}
-{formatTimestamp(message.createdAt)}
+{formatTimestamp(message.createdAt)}
-{code}++); diff --git a/ui/src/components/app/StatusBadge.tsx b/ui/src/components/app/StatusBadge.tsx index 6bf0cf5..55dd2f1 100644 --- a/ui/src/components/app/StatusBadge.tsx +++ b/ui/src/components/app/StatusBadge.tsx @@ -8,9 +8,9 @@ interface StatusBadgeProps { const toneStyles = { neutral: "border-line bg-surface text-muted", accent: "border-accent/15 bg-accent-soft text-accent-strong", - success: "border-green-200 bg-green-50 text-success", - warning: "border-amber-200 bg-amber-50 text-warning", - danger: "border-red-200 bg-red-50 text-danger", + success: "border-success/20 bg-success-soft text-success", + warning: "border-warning/20 bg-warning-soft text-warning", + danger: "border-danger/20 bg-danger-soft text-danger", }; export function StatusBadge({ label, tone = "neutral" }: StatusBadgeProps) { diff --git a/ui/src/components/marketing/HeroSection.tsx b/ui/src/components/marketing/HeroSection.tsx index 97c7795..8da1b59 100644 --- a/ui/src/components/marketing/HeroSection.tsx +++ b/ui/src/components/marketing/HeroSection.tsx @@ -38,7 +38,7 @@ export function HeroSection() { -{code}+ @@ -50,7 +50,7 @@ export function HeroSection() {-+Why is enterprise conversion softening this month?@@ -90,7 +90,7 @@ export function HeroSection() {- +SELECT segment, AVG(conversion_rate)
FROM revenue_pipeline diff --git a/ui/src/components/marketing/Navbar.test.tsx b/ui/src/components/marketing/Navbar.test.tsx new file mode 100644 index 0000000..d77e3f4 --- /dev/null +++ b/ui/src/components/marketing/Navbar.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it } from "vitest"; +import { Navbar } from "@/components/marketing/Navbar"; +import { ThemeProvider } from "@/context/ThemeProvider"; + +describe("Navbar", () => { + it("renders the theme toggle in the marketing navigation", () => { + render( ++ , + ); + + expect(screen.getAllByRole("button", { name: "Switch to dark mode" })).toHaveLength(2); + }); +}); diff --git a/ui/src/components/marketing/Navbar.tsx b/ui/src/components/marketing/Navbar.tsx index 8cc5027..60b426f 100644 --- a/ui/src/components/marketing/Navbar.tsx +++ b/ui/src/components/marketing/Navbar.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import { Button } from "@/components/shared/Button"; import { PageContainer } from "@/components/shared/PageContainer"; +import { ThemeToggle } from "@/components/shared/ThemeToggle"; import { homeNavLinks } from "@/lib/constants"; import { classNames } from "@/lib/classNames"; @@ -26,7 +27,7 @@ export function Navbar() { )} > -+ ++ P+PPlanera+Sign In diff --git a/ui/src/components/marketing/PricingSection.tsx b/ui/src/components/marketing/PricingSection.tsx index 9333136..481ce56 100644 --- a/ui/src/components/marketing/PricingSection.tsx +++ b/ui/src/components/marketing/PricingSection.tsx @@ -69,7 +69,7 @@ export function PricingSection() { className="overflow-hidden rounded-[36px] border border-line bg-panel px-6 py-8 shadow-soft sm:px-8 sm:py-10 lg:px-10" style={{ backgroundImage: - "radial-gradient(circle at top right, rgba(35, 88, 82, 0.14), transparent 30%), linear-gradient(180deg, rgba(255, 253, 249, 0.96), rgba(247, 242, 234, 0.92))", + "radial-gradient(circle at top right, rgb(var(--color-accent) / 0.16), transparent 30%), linear-gradient(180deg, rgb(var(--color-panel) / 0.96), rgb(var(--color-elevated) / 0.92))", }} > @@ -82,7 +82,7 @@ export function PricingSection() {-+ All paid plans include
{[ @@ -106,7 +106,7 @@ export function PricingSection() { elevated className={classNames( "flex h-full flex-col rounded-[30px] p-7 sm:p-8", - plan.featured ? "border-ink bg-ink text-white" : "bg-panel/85 backdrop-blur", + plan.featured ? "border-contrast bg-contrast text-contrast-foreground" : "bg-panel/85 backdrop-blur", )} >-@@ -114,42 +114,44 @@ export function PricingSection() {{plan.featured ? ( - + Popular ) : null}{plan.label}
-{plan.name}
+{plan.name}
{plan.description}
+{plan.description}
- {plan.price} - {plan.cadence} + {plan.price} + {plan.cadence}-{plan.note}
+{plan.note}
--+
+What's included
@@ -158,10 +160,10 @@ export function PricingSection() { - {feature} + {feature} ))}
diff --git a/ui/src/components/shared/Button.tsx b/ui/src/components/shared/Button.tsx index 91ceec1..cc34a83 100644 --- a/ui/src/components/shared/Button.tsx +++ b/ui/src/components/shared/Button.tsx @@ -12,11 +12,11 @@ interface ButtonProps extends ButtonHTMLAttributes, PropsWith const variantStyles: Record = { primary: - "bg-ink text-white shadow-card hover:bg-black/90 focus-visible:ring-ink/20", + "bg-contrast text-contrast-foreground shadow-card hover:bg-contrast/90 focus-visible:ring-accent/30", secondary: - "border border-line bg-panel text-ink hover:border-ink/20 hover:bg-white", + "border border-line bg-panel text-ink hover:border-line/90 hover:bg-surface", ghost: - "bg-transparent text-muted hover:bg-black/[0.03] hover:text-ink", + "bg-transparent text-muted hover:bg-surface hover:text-ink", subtle: "bg-accent-soft text-accent-strong hover:bg-accent-soft/80", }; diff --git a/ui/src/components/shared/Drawer.tsx b/ui/src/components/shared/Drawer.tsx index 84419a6..320057b 100644 --- a/ui/src/components/shared/Drawer.tsx +++ b/ui/src/components/shared/Drawer.tsx @@ -7,7 +7,7 @@ interface DrawerProps { title?: string; subtitle?: string; side?: "left" | "right"; - maximized?: boolean; + size?: "default" | "wide"; actions?: ReactNode; children: ReactNode; } @@ -18,7 +18,7 @@ export function Drawer({ title, subtitle, side = "right", - maximized = false, + size = "default", actions, children, }: DrawerProps) { @@ -61,7 +61,7 @@ export function Drawer({ side === "right" ? "right-0 border-l" : "left-0 border-r", - maximized + size === "wide" ? "w-full lg:w-[min(88vw,980px)]" : "w-full sm:w-[min(92vw,560px)] lg:w-[520px]", open @@ -70,6 +70,9 @@ export function Drawer({ ? "translate-x-full" : "-translate-x-full", )} + role="dialog" + aria-modal="true" + aria-label={title} > diff --git a/ui/src/components/shared/ErrorState.tsx b/ui/src/components/shared/ErrorState.tsx index 07f99f8..57b280a 100644 --- a/ui/src/components/shared/ErrorState.tsx +++ b/ui/src/components/shared/ErrorState.tsx @@ -9,9 +9,9 @@ interface ErrorStateProps { export function ErrorState({ title, description, onRetry }: ErrorStateProps) { return ( -+ -+{error ? ( -diff --git a/ui/src/components/shared/Input.tsx b/ui/src/components/shared/Input.tsx index 9eb3ada..46f78a6 100644 --- a/ui/src/components/shared/Input.tsx +++ b/ui/src/components/shared/Input.tsx @@ -5,7 +5,7 @@ export function Input({ className, ...props }: InputHTMLAttributes; +} diff --git a/ui/src/components/shared/Tabs.tsx b/ui/src/components/shared/Tabs.tsx index 5b06cca..58e2050 100644 --- a/ui/src/components/shared/Tabs.tsx +++ b/ui/src/components/shared/Tabs.tsx @@ -22,8 +22,8 @@ export function Tabs ({ tabs, activeTab, onChange }: TabsProps< className={classNames( "rounded-full px-4 py-2 text-sm font-medium transition", activeTab === tab.id - ? "bg-ink text-white shadow-card" - : "text-muted hover:bg-white hover:text-ink", + ? "bg-contrast text-contrast-foreground shadow-card" + : "text-muted hover:bg-panel hover:text-ink", )} > {tab.label} diff --git a/ui/src/components/shared/Textarea.tsx b/ui/src/components/shared/Textarea.tsx index 5c307b1..7a2b56e 100644 --- a/ui/src/components/shared/Textarea.tsx +++ b/ui/src/components/shared/Textarea.tsx @@ -9,7 +9,7 @@ export const Textarea = forwardRef + + + + + {showLabel ? ( + + Theme + {isDark ? "Dark mode" : "Light mode"} + + ) : null} + + ); +} diff --git a/ui/src/context/ThemeProvider.test.tsx b/ui/src/context/ThemeProvider.test.tsx new file mode 100644 index 0000000..e1846bf --- /dev/null +++ b/ui/src/context/ThemeProvider.test.tsx @@ -0,0 +1,72 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { ThemeProvider } from "@/context/ThemeProvider"; +import { useTheme } from "@/hooks/useTheme"; +import { ThemeToggle } from "@/components/shared/ThemeToggle"; + +function ThemeProbe() { + const { theme } = useTheme(); + return {theme}
; +} + +describe("ThemeProvider", () => { + it("prefers the saved theme over the system preference", () => { + localStorage.setItem("planera.theme", "dark"); + vi.mocked(window.matchMedia).mockReturnValue({ + matches: false, + media: "(prefers-color-scheme: dark)", + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + }); + + render( ++ , + ); + + expect(screen.getByText(/^dark$/)).toBeInTheDocument(); + expect(document.documentElement.dataset.theme).toBe("dark"); + }); + + it("falls back to the system preference on first load", () => { + vi.mocked(window.matchMedia).mockReturnValue({ + matches: true, + media: "(prefers-color-scheme: dark)", + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + }); + + render( ++ + , + ); + + expect(screen.getByText(/^dark$/)).toBeInTheDocument(); + expect(document.documentElement.dataset.theme).toBe("dark"); + expect(localStorage.getItem("planera.theme")).toBe("dark"); + }); + + it("updates the document theme and storage when toggled", () => { + render( ++ + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Switch to dark mode" })); + + expect(document.documentElement.dataset.theme).toBe("dark"); + expect(localStorage.getItem("planera.theme")).toBe("dark"); + expect(screen.getByRole("button", { name: "Switch to light mode" })).toBeInTheDocument(); + }); +}); diff --git a/ui/src/context/ThemeProvider.tsx b/ui/src/context/ThemeProvider.tsx new file mode 100644 index 0000000..eca4081 --- /dev/null +++ b/ui/src/context/ThemeProvider.tsx @@ -0,0 +1,44 @@ +import type { PropsWithChildren } from "react"; +import { useCallback, useLayoutEffect, useMemo, useState } from "react"; +import { ThemeContext, type Theme, type ThemeContextValue } from "@/context/theme-context"; +import { uiStore } from "@/store/uiStore"; + +function getSystemTheme(): Theme { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") { + return "light"; + } + + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +function getInitialTheme(): Theme { + return uiStore.getTheme() ?? getSystemTheme(); +} + +export function ThemeProvider({ children }: PropsWithChildren) { + const [theme, setTheme] = useState+ (getInitialTheme); + + useLayoutEffect(() => { + document.documentElement.dataset.theme = theme; + uiStore.setTheme(theme); + }, [theme]); + + const updateTheme = useCallback((nextTheme: Theme) => { + setTheme(nextTheme); + }, []); + + const toggleTheme = useCallback(() => { + setTheme((currentTheme) => (currentTheme === "light" ? "dark" : "light")); + }, []); + + const value = useMemo ( + () => ({ + theme, + setTheme: updateTheme, + toggleTheme, + }), + [theme, updateTheme, toggleTheme], + ); + + return {children} ; +} diff --git a/ui/src/context/theme-context.ts b/ui/src/context/theme-context.ts new file mode 100644 index 0000000..17a58f7 --- /dev/null +++ b/ui/src/context/theme-context.ts @@ -0,0 +1,11 @@ +import { createContext } from "react"; + +export type Theme = "light" | "dark"; + +export interface ThemeContextValue { + theme: Theme; + setTheme: (theme: Theme) => void; + toggleTheme: () => void; +} + +export const ThemeContext = createContext(undefined); diff --git a/ui/src/hooks/useInspectionPanel.ts b/ui/src/hooks/useInspectionPanel.ts index 0e46d74..8a8fc45 100644 --- a/ui/src/hooks/useInspectionPanel.ts +++ b/ui/src/hooks/useInspectionPanel.ts @@ -6,7 +6,6 @@ import type { InspectionData, InspectionTabId } from "@/types/inspection"; export function useInspectionPanel() { const { token } = useAuth(); const [open, setOpen] = useState(false); - const [maximized, setMaximized] = useState(false); const [activeTab, setActiveTab] = useState ("sql"); const [inspection, setInspection] = useState (null); const [loading, setLoading] = useState(false); @@ -31,7 +30,6 @@ export function useInspectionPanel() { return { open, - maximized, activeTab, inspection, loading, @@ -39,6 +37,5 @@ export function useInspectionPanel() { setActiveTab, openInspection, closeInspection: () => setOpen(false), - toggleMaximized: () => setMaximized((value) => !value), }; } diff --git a/ui/src/hooks/useTheme.ts b/ui/src/hooks/useTheme.ts new file mode 100644 index 0000000..b26ccf7 --- /dev/null +++ b/ui/src/hooks/useTheme.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +import { ThemeContext } from "@/context/theme-context"; + +export function useTheme() { + const value = useContext(ThemeContext); + + if (!value) { + throw new Error("useTheme must be used within a ThemeProvider."); + } + + return value; +} diff --git a/ui/src/lib/constants.ts b/ui/src/lib/constants.ts index 3796ff6..80e3e25 100644 --- a/ui/src/lib/constants.ts +++ b/ui/src/lib/constants.ts @@ -6,13 +6,6 @@ export const homeNavLinks = [ { label: "Sign In", href: "/sign-in" }, ] as const; -export const promptSuggestions = [ - "Why is pipeline conversion dropping?", - "Show churn by segment", - "Summarize top anomalies", - "Generate SQL for this question", -]; - export const sidebarNavItems = [ { id: "chats", label: "Chats" }, { id: "uploads", label: "Uploads" }, diff --git a/ui/src/pages/AppPage.tsx b/ui/src/pages/AppPage.tsx index 8887d65..2590727 100644 --- a/ui/src/pages/AppPage.tsx +++ b/ui/src/pages/AppPage.tsx @@ -161,10 +161,6 @@ export function AppPage() { value={draft} onChange={setDraft} onSubmit={() => void handleSubmit()} - onPickPrompt={(prompt) => { - setDraft(prompt); - handleSectionChange("chats"); - }} onUpload={(file) => void handleChatUpload(file)} onRemoveAttachment={removeActiveUpload} attachments={activeUploads} @@ -291,9 +287,7 @@ export function AppPage() { error={inspection.error} inspection={inspection.inspection} activeTab={inspection.activeTab} - maximized={inspection.maximized} onClose={inspection.closeInspection} - onToggleMaximized={inspection.toggleMaximized} onTabChange={inspection.setActiveTab} /> } diff --git a/ui/src/pages/SignInPage.tsx b/ui/src/pages/SignInPage.tsx index 8990703..9d9f43c 100644 --- a/ui/src/pages/SignInPage.tsx +++ b/ui/src/pages/SignInPage.tsx @@ -166,7 +166,7 @@ export function SignInPage() { +{error}) : null} diff --git a/ui/src/router/RootLayout.tsx b/ui/src/router/RootLayout.tsx index 32be3ef..6526c59 100644 --- a/ui/src/router/RootLayout.tsx +++ b/ui/src/router/RootLayout.tsx @@ -1,10 +1,13 @@ import { Outlet } from "react-router-dom"; import { AuthProvider } from "@/context/AuthProvider"; +import { ThemeProvider } from "@/context/ThemeProvider"; export function RootLayout() { return ( -- +- + ); } diff --git a/ui/src/store/uiStore.ts b/ui/src/store/uiStore.ts index bcc4ce4..a158179 100644 --- a/ui/src/store/uiStore.ts +++ b/ui/src/store/uiStore.ts @@ -2,8 +2,11 @@ const keys = { sidebarCollapsed: "planera.sidebar.collapsed", activeSection: "planera.active.section", activeConversation: "planera.active.conversation", + theme: "planera.theme", }; +type ThemePreference = "light" | "dark"; + export const uiStore = { getSidebarCollapsed() { return window.localStorage.getItem(keys.sidebarCollapsed) === "true"; @@ -23,4 +26,11 @@ export const uiStore = { setActiveConversation(value: string) { window.localStorage.setItem(keys.activeConversation, value); }, + getTheme(): ThemePreference | null { + const value = window.localStorage.getItem(keys.theme); + return value === "light" || value === "dark" ? value : null; + }, + setTheme(value: ThemePreference) { + window.localStorage.setItem(keys.theme, value); + }, }; diff --git a/ui/src/styles/globals.css b/ui/src/styles/globals.css index a56ab53..067ba92 100644 --- a/ui/src/styles/globals.css +++ b/ui/src/styles/globals.css @@ -11,9 +11,13 @@ body { @apply bg-canvas font-sans text-ink antialiased; background-image: - radial-gradient(circle at top left, rgba(35, 88, 82, 0.06), transparent 30%), - radial-gradient(circle at top right, rgba(199, 179, 148, 0.1), transparent 26%); + radial-gradient(circle at top left, rgb(var(--color-accent) / 0.12), transparent 32%), + radial-gradient(circle at top right, rgb(var(--color-contrast) / 0.08), transparent 30%); min-height: 100vh; + transition: + background-color 220ms ease, + color 220ms ease, + background-image 220ms ease; } * { @@ -21,7 +25,7 @@ } ::selection { - background: rgba(35, 88, 82, 0.16); + background: rgb(var(--color-accent) / 0.16); } h1, @@ -49,12 +53,12 @@ } .surface-ring { - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65); + box-shadow: inset 0 1px 0 var(--surface-ring); } .scroll-fade { scrollbar-width: thin; - scrollbar-color: rgba(99, 108, 114, 0.5) transparent; + scrollbar-color: rgb(var(--color-muted) / 0.5) transparent; } .scroll-fade::-webkit-scrollbar { @@ -63,7 +67,7 @@ } .scroll-fade::-webkit-scrollbar-thumb { - background: rgba(99, 108, 114, 0.35); + background: rgb(var(--color-muted) / 0.35); border-radius: 999px; } } diff --git a/ui/src/styles/theme.css b/ui/src/styles/theme.css index 944bd04..70b9649 100644 --- a/ui/src/styles/theme.css +++ b/ui/src/styles/theme.css @@ -1,19 +1,58 @@ -:root { - --color-canvas: #f4efe8; - --color-surface: #faf7f2; - --color-panel: #fffdf9; - --color-elevated: #f7f2ea; - --color-line: #d9d0c4; - --color-ink: #161819; - --color-muted: #636c72; - --color-accent: #235852; - --color-accent-soft: #d9ebe7; - --color-accent-strong: #193f3b; - --color-success: #2f6d5a; - --color-warning: #8a6227; - --color-danger: #8c4141; +:root, +[data-theme="light"] { + color-scheme: light; + --color-canvas: 244 239 232; + --color-surface: 250 247 242; + --color-panel: 255 253 249; + --color-elevated: 247 242 234; + --color-line: 217 208 196; + --color-ink: 22 24 25; + --color-muted: 99 108 114; + --color-contrast: 22 24 25; + --color-contrast-foreground: 255 255 255; + --color-accent: 35 88 82; + --color-accent-soft: 217 235 231; + --color-accent-strong: 25 63 59; + --color-success: 47 109 90; + --color-success-soft: 227 241 234; + --color-warning: 138 98 39; + --color-warning-soft: 247 236 215; + --color-danger: 140 65 65; + --color-danger-soft: 250 232 232; + --color-code: 17 20 23; + --color-code-ink: 230 239 233; --shadow-soft: 0 16px 48px rgba(24, 32, 34, 0.07); --shadow-card: 0 12px 28px rgba(24, 32, 34, 0.06); + --shadow-field: inset 0 1px 0 rgba(255, 255, 255, 0.62); + --surface-ring: rgba(255, 255, 255, 0.65); --radius-panel: 28px; --radius-card: 22px; } + +[data-theme="dark"] { + color-scheme: dark; + --color-canvas: 10 14 18; + --color-surface: 17 23 29; + --color-panel: 24 30 37; + --color-elevated: 31 39 47; + --color-line: 66 78 89; + --color-ink: 231 237 240; + --color-muted: 159 170 178; + --color-contrast: 235 241 243; + --color-contrast-foreground: 12 16 20; + --color-accent: 55 131 120; + --color-accent-soft: 27 56 52; + --color-accent-strong: 195 231 224; + --color-success: 118 200 172; + --color-success-soft: 25 52 46; + --color-warning: 229 183 93; + --color-warning-soft: 71 50 21; + --color-danger: 243 156 156; + --color-danger-soft: 76 37 37; + --color-code: 12 17 22; + --color-code-ink: 226 236 232; + --shadow-soft: 0 22px 56px rgba(1, 5, 9, 0.42); + --shadow-card: 0 14px 34px rgba(1, 5, 9, 0.34); + --shadow-field: inset 0 1px 0 rgba(255, 255, 255, 0.05); + --surface-ring: rgba(255, 255, 255, 0.04); +} diff --git a/ui/src/test/setup.ts b/ui/src/test/setup.ts index f149f27..4bdc87b 100644 --- a/ui/src/test/setup.ts +++ b/ui/src/test/setup.ts @@ -1 +1,26 @@ import "@testing-library/jest-dom/vitest"; +import { cleanup } from "@testing-library/react"; +import { afterEach, beforeEach, vi } from "vitest"; + +beforeEach(() => { + Object.defineProperty(window, "matchMedia", { + writable: true, + configurable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); +}); + +afterEach(() => { + cleanup(); + localStorage.clear(); + delete document.documentElement.dataset.theme; +}); diff --git a/ui/tailwind.config.ts b/ui/tailwind.config.ts index 9bf3272..b79a649 100644 --- a/ui/tailwind.config.ts +++ b/ui/tailwind.config.ts @@ -5,22 +5,38 @@ const config: Config = { theme: { extend: { colors: { - canvas: "#f4efe8", - surface: "#faf7f2", - panel: "#fffdf9", - elevated: "#f7f2ea", - line: "#d9d0c4", - ink: "#161819", - muted: "#636c72", + canvas: "rgb(var(--color-canvas) /+ ++ )", + surface: "rgb(var(--color-surface) / )", + panel: "rgb(var(--color-panel) / )", + elevated: "rgb(var(--color-elevated) / )", + line: "rgb(var(--color-line) / )", + ink: "rgb(var(--color-ink) / )", + muted: "rgb(var(--color-muted) / )", + contrast: { + DEFAULT: "rgb(var(--color-contrast) / )", + foreground: "rgb(var(--color-contrast-foreground) / )", + }, accent: { - DEFAULT: "#235852", - soft: "#d9ebe7", - strong: "#193f3b", + DEFAULT: "rgb(var(--color-accent) / )", + soft: "rgb(var(--color-accent-soft) / )", + strong: "rgb(var(--color-accent-strong) / )", + }, + success: { + DEFAULT: "rgb(var(--color-success) / )", + soft: "rgb(var(--color-success-soft) / )", + }, + warning: { + DEFAULT: "rgb(var(--color-warning) / )", + soft: "rgb(var(--color-warning-soft) / )", + }, + danger: { + DEFAULT: "rgb(var(--color-danger) / )", + soft: "rgb(var(--color-danger-soft) / )", + }, + code: { + DEFAULT: "rgb(var(--color-code) / )", + ink: "rgb(var(--color-code-ink) / )", }, - sand: "#ece3d5", - success: "#2f6d5a", - warning: "#8a6227", - danger: "#8c4141", }, fontFamily: { sans: ['"Inter"', '"Avenir Next"', '"Segoe UI"', "system-ui", "sans-serif"], @@ -28,9 +44,10 @@ const config: Config = { mono: ['"SFMono-Regular"', '"SF Mono"', "ui-monospace", "monospace"], }, boxShadow: { - soft: "0 16px 48px rgba(24, 32, 34, 0.07)", - card: "0 12px 28px rgba(24, 32, 34, 0.06)", - focus: "0 0 0 3px rgba(35, 88, 82, 0.16)", + soft: "var(--shadow-soft)", + card: "var(--shadow-card)", + field: "var(--shadow-field)", + focus: "0 0 0 3px rgb(var(--color-accent) / 0.16)", }, borderRadius: { xl2: "1.4rem",