Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Three premium themes, each with light and dark variants:
| ------------------- | ------------------------------------- |
| **Iridescent Void** | Futuristic, expensive, slightly alien |
| **Carbon** | Stark, modern, performance-focused |
| **Cotton Candy** | Sweet, dreamy, pink and blue |
| **Purple Stuff** | Plush lilac, grape-soda, subtly noir |

All themes define the same set of CSS custom properties. Components must use semantic
tokens (`bg-accent`, `text-muted-foreground`) — never theme-specific values.
Expand Down
22 changes: 14 additions & 8 deletions apps/web/src/hooks/useTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback, useEffect, useSyncExternalStore } from "react";
import { initCustomTheme } from "../lib/customTheme";

type Theme = "light" | "dark" | "system";
type ColorTheme = "default" | "iridescent-void" | "carbon" | "cotton-candy" | "custom";
type ColorTheme = "default" | "iridescent-void" | "carbon" | "purple-stuff" | "custom";

type FontFamily = "dm-sans" | "inter" | "plus-jakarta-sans";

Expand All @@ -17,7 +17,7 @@ export const COLOR_THEMES: { id: ColorTheme; label: string }[] = [
{ id: "default", label: "Default" },
{ id: "iridescent-void", label: "Iridescent Void" },
{ id: "carbon", label: "Carbon" },
{ id: "cotton-candy", label: "Cotton Candy" },
{ id: "purple-stuff", label: "Purple Stuff" },
{ id: "custom", label: "Custom" },
];

Expand Down Expand Up @@ -59,14 +59,20 @@ function getStored(): Theme {

function getStoredColorTheme(): ColorTheme {
const raw = localStorage.getItem(COLOR_THEME_STORAGE_KEY);
const normalized = raw === "cotton-candy" ? "purple-stuff" : raw;

if (
raw === "default" ||
raw === "iridescent-void" ||
raw === "carbon" ||
raw === "cotton-candy" ||
raw === "custom"
normalized === "default" ||
normalized === "iridescent-void" ||
normalized === "carbon" ||
normalized === "purple-stuff" ||
normalized === "custom"
) {
return raw;
if (normalized !== raw) {
localStorage.setItem(COLOR_THEME_STORAGE_KEY, normalized);
}

return normalized;
}
return DEFAULT_COLOR_THEME;
}
Expand Down
192 changes: 139 additions & 53 deletions apps/web/src/themes.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,62 +123,148 @@
--warning-foreground: #f7b955;
}

/* ─── Cotton Candy ─── sweet, dreamy pink & blue ─── */
/* ─── Purple Stuff ─── plush lilac surfaces with grape-soda contrast ─── */

:root.theme-cotton-candy {
:root.theme-purple-stuff {
color-scheme: light;
--background: #fdf6f9;
--foreground: #2a1f2e;
--card: #fceef5;
--card-foreground: #2a1f2e;
--popover: #fceef5;
--popover-foreground: #2a1f2e;
--primary: oklch(0.72 0.14 350);
--primary-foreground: #ffffff;
--secondary: rgba(248, 185, 215, 0.12);
--secondary-foreground: #2a1f2e;
--muted: rgba(160, 210, 248, 0.1);
--muted-foreground: #8a7e94;
--accent: rgba(150, 212, 252, 0.14);
--accent-foreground: #2a1f2e;
--destructive: #e85475;
--destructive-foreground: #d03058;
--border: rgba(248, 180, 215, 0.2);
--input: rgba(248, 180, 215, 0.22);
--ring: oklch(0.72 0.14 350);
--info: #7ec8ee;
--info-foreground: #5aaedb;
--success: #6cc4a0;
--success-foreground: #4aaa82;
--warning: #f5baca;
--warning-foreground: #d8929e;
--background: oklch(0.9201 0.0109 256.6974);
--foreground: oklch(0.3192 0.0102 216.7919);
--card: oklch(0.9201 0.0109 256.6974);
--card-foreground: oklch(0.3192 0.0102 216.7919);
--popover: oklch(0.9201 0.0109 256.6974);
--popover-foreground: oklch(0.3192 0.0102 216.7919);
--primary: oklch(0.5676 0.2021 283.0838);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.8831 0.0199 260.1689);
--secondary-foreground: oklch(0.3192 0.0102 216.7919);
--muted: oklch(0.8765 0.0112 225.9985);
--muted-foreground: oklch(0.5303 0.0148 221.5854);
--accent: oklch(0.5676 0.2021 283.0838);
--accent-foreground: oklch(1 0 0);
--destructive: oklch(0.7281 0.1678 22.5445);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.8831 0.0199 260.1689);
--input: oklch(0.9201 0.0109 256.6974);
--ring: oklch(0.5676 0.2021 283.0838);
--info: oklch(0.6052 0.1712 250.5691);
--info-foreground: oklch(0.3192 0.0102 216.7919);
--success: oklch(0.6972 0.1351 172.0843);
--success-foreground: oklch(0.3192 0.0102 216.7919);
--warning: oklch(0.7359 0.1411 285.6043);
--warning-foreground: oklch(0.3192 0.0102 216.7919);
--chart-1: oklch(0.5676 0.2021 283.0838);
--chart-2: oklch(0.7359 0.1411 285.6043);
--chart-3: oklch(0.8793 0.0993 195.4973);
--chart-4: oklch(0.7682 0.123 250.0275);
--chart-5: oklch(0.8598 0.1435 170.8155);
--sidebar: oklch(0.9201 0.0109 256.6974);
--sidebar-foreground: oklch(0.3192 0.0102 216.7919);
--sidebar-primary: oklch(0.5676 0.2021 283.0838);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.8831 0.0199 260.1689);
--sidebar-accent-foreground: oklch(0.5676 0.2021 283.0838);
--sidebar-border: oklch(0.8831 0.0199 260.1689);
--sidebar-ring: oklch(0.5676 0.2021 283.0838);
--font-sans: "Inter", sans-serif;
--font-serif: Georgia, serif;
--font-mono: "Fira Code", monospace;
--radius: 1.25rem;
--shadow-x: 9px;
--shadow-y: 9px;
--shadow-blur: 20px;
--shadow-spread: 0px;
--shadow-opacity: 0.6;
--shadow-color: #a3b1c6;
--shadow-2xs: 9px 9px 20px 0px hsl(216 23.4899% 70.7843% / 0.3);
--shadow-xs: 9px 9px 20px 0px hsl(216 23.4899% 70.7843% / 0.3);
--shadow-sm:
9px 9px 20px 0px hsl(216 23.4899% 70.7843% / 0.6),
9px 1px 2px -1px hsl(216 23.4899% 70.7843% / 0.6);
--shadow:
9px 9px 20px 0px hsl(216 23.4899% 70.7843% / 0.6),
9px 1px 2px -1px hsl(216 23.4899% 70.7843% / 0.6);
--shadow-md:
9px 9px 20px 0px hsl(216 23.4899% 70.7843% / 0.6),
9px 2px 4px -1px hsl(216 23.4899% 70.7843% / 0.6);
--shadow-lg:
9px 9px 20px 0px hsl(216 23.4899% 70.7843% / 0.6),
9px 4px 6px -1px hsl(216 23.4899% 70.7843% / 0.6);
--shadow-xl:
9px 9px 20px 0px hsl(216 23.4899% 70.7843% / 0.6),
9px 8px 10px -1px hsl(216 23.4899% 70.7843% / 0.6);
--shadow-2xl: 9px 9px 20px 0px hsl(216 23.4899% 70.7843% / 1.5);
--tracking-normal: 0.025em;
--spacing: 0.25rem;
}

:root.theme-cotton-candy.dark {
:root.theme-purple-stuff.dark {
color-scheme: dark;
--background: #170e1a;
--foreground: #f4e8f2;
--card: #1e1222;
--card-foreground: #f4e8f2;
--popover: #1e1222;
--popover-foreground: #f4e8f2;
--primary: oklch(0.75 0.14 350);
--primary-foreground: #170e1a;
--secondary: rgba(250, 170, 210, 0.08);
--secondary-foreground: #f4e8f2;
--muted: rgba(150, 210, 250, 0.08);
--muted-foreground: #a090aa;
--accent: rgba(150, 215, 255, 0.1);
--accent-foreground: #f4e8f2;
--destructive: #ff6b8a;
--destructive-foreground: #ff95aa;
--border: rgba(250, 170, 215, 0.1);
--input: rgba(250, 170, 215, 0.12);
--ring: oklch(0.75 0.14 350);
--info: #8ed4f5;
--info-foreground: #aee0f8;
--success: #7dd4b0;
--success-foreground: #90e0c0;
--warning: #f5bfcc;
--warning-foreground: #f8cdd8;
--background: oklch(0 0 0);
--foreground: oklch(0.9205 0.0086 225.0878);
--card: oklch(0.1291 0.0026 286.0284);
--card-foreground: oklch(0.9205 0.0086 225.0878);
--popover: oklch(0.3192 0.0102 216.7919);
--popover-foreground: oklch(0.9205 0.0086 225.0878);
--primary: oklch(0.7359 0.1411 285.6043);
--primary-foreground: oklch(0 0 0);
--secondary: oklch(0 0 0);
--secondary-foreground: oklch(0.9205 0.0086 225.0878);
--muted: oklch(0.2258 0.0025 247.9355);
--muted-foreground: oklch(0.7937 0.0151 224.5441);
--accent: oklch(0.7359 0.1411 285.6043);
--accent-foreground: oklch(0.9249 0 0);
--destructive: oklch(0.8251 0.0894 33.5775);
--destructive-foreground: oklch(0.3192 0.0102 216.7919);
--border: oklch(0.352 0.0241 265.6321);
--input: oklch(0.3192 0.0102 216.7919);
--ring: oklch(0.7359 0.1411 285.6043);
--info: oklch(0.7692 0.1322 191.6635);
--info-foreground: oklch(0.9205 0.0086 225.0878);
--success: oklch(0.6972 0.1351 172.0843);
--success-foreground: oklch(0.9205 0.0086 225.0878);
--warning: oklch(0.7359 0.1411 285.6043);
--warning-foreground: oklch(0.9249 0 0);
--chart-1: oklch(0.7359 0.1411 285.6043);
--chart-2: oklch(0.5676 0.2021 283.0838);
--chart-3: oklch(0.7692 0.1322 191.6635);
--chart-4: oklch(0.6052 0.1712 250.5691);
--chart-5: oklch(0.6972 0.1351 172.0843);
--sidebar: oklch(0.3192 0.0102 216.7919);
--sidebar-foreground: oklch(0.9205 0.0086 225.0878);
--sidebar-primary: oklch(0.7359 0.1411 285.6043);
--sidebar-primary-foreground: oklch(0.3192 0.0102 216.7919);
--sidebar-accent: oklch(0.5137 0.0082 268.4824);
--sidebar-accent-foreground: oklch(0.7359 0.1411 285.6043);
--sidebar-border: oklch(0.352 0.0241 265.6321);
--sidebar-ring: oklch(0.7359 0.1411 285.6043);
--font-sans: "Inter", sans-serif;
--font-serif: Georgia, serif;
--font-mono: "Fira Code", monospace;
--radius: 1.25rem;
--shadow-x: 6px;
--shadow-y: 6px;
--shadow-blur: 15px;
--shadow-spread: 0px;
--shadow-opacity: 0.8;
--shadow-color: #212529;
--shadow-2xs: 6px 6px 15px 0px hsl(210 10.8108% 14.5098% / 0.4);
--shadow-xs: 6px 6px 15px 0px hsl(210 10.8108% 14.5098% / 0.4);
--shadow-sm:
6px 6px 15px 0px hsl(210 10.8108% 14.5098% / 0.8),
6px 1px 2px -1px hsl(210 10.8108% 14.5098% / 0.8);
--shadow:
6px 6px 15px 0px hsl(210 10.8108% 14.5098% / 0.8),
6px 1px 2px -1px hsl(210 10.8108% 14.5098% / 0.8);
--shadow-md:
6px 6px 15px 0px hsl(210 10.8108% 14.5098% / 0.8),
6px 2px 4px -1px hsl(210 10.8108% 14.5098% / 0.8);
--shadow-lg:
6px 6px 15px 0px hsl(210 10.8108% 14.5098% / 0.8),
6px 4px 6px -1px hsl(210 10.8108% 14.5098% / 0.8);
--shadow-xl:
6px 6px 15px 0px hsl(210 10.8108% 14.5098% / 0.8),
6px 8px 10px -1px hsl(210 10.8108% 14.5098% / 0.8);
--shadow-2xl: 6px 6px 15px 0px hsl(210 10.8108% 14.5098% / 2);
--tracking-normal: 0.025em;
--spacing: 0.25rem;
}
18 changes: 9 additions & 9 deletions docs/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,16 @@ The goal is to avoid generic editor skins and instead build themes that feel lik

---

### 9. Cotton Candy
### 9. Purple Stuff

**Vibe:** sweet, dreamy, adorably feminine
**Palette:** pastel pink, baby blue, lavender, soft rose, cream
**Vibe:** plush, nocturnal, grape-soda futuristic
**Palette:** powdered lilac, black plum, electric violet, cool periwinkle, mint

- Soft pink and baby blue create a whimsical, cotton candy fairground feel
- Light mode is airy and bright with rosy card backgrounds
- Dark mode wraps everything in a cozy plum-kissed night
- Info/accent colors lean baby blue; primary leans hot pink
- Playful yet legible — sugar rush without the eye strain
- Light mode stays soft and airy without drifting into candy pink
- Dark mode goes all the way to black with vivid violet focus accents
- Primary surfaces feel syrupy purple instead of rosy or baby-blue
- Sidebars and panels read cleaner and more premium with frosted lilac neutrals
- Playful, but less twee and more intentional than the old Cotton Candy direction

---

Expand Down Expand Up @@ -153,7 +153,7 @@ If narrowing to a stronger first shortlist, these feel the most distinctive:
### Playful / expressive

- Candy Reactor
- Cotton Candy
- Purple Stuff
- Velvet Casino

### Atmospheric / weird
Expand Down
Loading