Two new extra-detailed themes: Cybperpunk and Blueprint

This commit is contained in:
Brian Fertig 2026-08-28 11:33:57 -06:00
parent 5e23a984e1
commit be6926c027
12 changed files with 634 additions and 30 deletions

View File

@ -5,9 +5,9 @@
@import "@uiw/react-markdown-preview/markdown.css"; @import "@uiw/react-markdown-preview/markdown.css";
/* "dark:" must be true for every dark-surfaced theme, not just .dark -- /* "dark:" must be true for every dark-surfaced theme, not just .dark --
the dark looks (Dark, Ocean, Pine, Plum, Midnight, Ember, Rosewood) all the dark looks (Dark, Ocean, Pine, Plum, Midnight, Ember, Rosewood,
turn on dark utilities. */ Cyberpunk) all turn on dark utilities. */
@custom-variant dark (&:is(.dark *, .ocean *, .theme-pine *, .theme-plum *, .theme-midnight *, .theme-ember *, .theme-rosewood *)); @custom-variant dark (&:is(.dark *, .ocean *, .theme-pine *, .theme-plum *, .theme-midnight *, .theme-ember *, .theme-rosewood *, .theme-cyberpunk *));
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
@ -59,7 +59,7 @@
/* --------------------------------------------------------------------------- /* ---------------------------------------------------------------------------
Themes Themes
The app ships fourteen looks, chosen via the theme picker: The app ships sixteen looks, chosen via the theme picker:
Light: Light:
default -- calm, cool neutral light theme (the safe, focused one) default -- calm, cool neutral light theme (the safe, focused one)
sunset -- warm paper + persimmon accent, a cozier light theme sunset -- warm paper + persimmon accent, a cozier light theme
@ -68,6 +68,9 @@
rose -- soft pink paper with a raspberry accent ("Ros\xe9") rose -- soft pink paper with a raspberry accent ("Ros\xe9")
lavender -- lilac paper with a violet accent lavender -- lilac paper with a violet accent
slate -- cool grey-blue paper with a steel-blue accent slate -- cool grey-blue paper with a steel-blue accent
blueprint -- EXTRA: deep cobalt drafting-paper background with
white paper cards + cobalt-ink lettering, grid +
registration marks, sharp corners, hand-drawn headings
Dark: Dark:
dark -- deep charcoal with an indigo accent dark -- deep charcoal with an indigo accent
ocean -- deep blue-green night theme with a seafoam accent ocean -- deep blue-green night theme with a seafoam accent
@ -76,8 +79,12 @@
midnight -- near-black navy with an ice-periwinkle accent midnight -- near-black navy with an ice-periwinkle accent
ember -- warm hearth charcoal with a glowing amber accent ember -- warm hearth charcoal with a glowing amber accent
rosewood -- deep wine with a rose accent rosewood -- deep wine with a rose accent
cyberpunk -- EXTRA: near-black violet, neon magenta + cyan,
scanlines, glitch, glow, VT323 pixel face
Each is a complete token set; components only ever use tokens, so adding Each is a complete token set; components only ever use tokens, so adding
another theme later is just another block like these. another theme later is just another block like these. The two EXTRA
themes additionally carry their own fonts, backgrounds, and animations,
all scoped in the "extras" section below.
--------------------------------------------------------------------------- */ --------------------------------------------------------------------------- */
:root, :root,
@ -169,12 +176,12 @@
--popover-foreground: oklch(0.27 0.032 52); --popover-foreground: oklch(0.27 0.032 52);
/* Persimmon -- warm enough to feel like part of the paper, saturated /* Persimmon -- warm enough to feel like part of the paper, saturated
enough to stay unmistakably "the accent". */ enough to stay unmistakably "the accent". */
--primary: oklch(0.58 0.19 42); --primary: oklch(0.565 0.19 42);
--primary-foreground: oklch(0.98 0.01 60); --primary-foreground: oklch(0.98 0.01 60);
--secondary: oklch(0.945 0.02 72); --secondary: oklch(0.945 0.02 72);
--secondary-foreground: oklch(0.32 0.05 50); --secondary-foreground: oklch(0.32 0.05 50);
--muted: oklch(0.938 0.02 72); --muted: oklch(0.938 0.02 72);
--muted-foreground: oklch(0.54 0.045 58); --muted-foreground: oklch(0.52 0.045 58);
--accent: oklch(0.928 0.035 62); --accent: oklch(0.928 0.035 62);
--accent-foreground: oklch(0.36 0.11 44); --accent-foreground: oklch(0.36 0.11 44);
--destructive: oklch(0.57 0.21 25); --destructive: oklch(0.57 0.21 25);
@ -191,7 +198,7 @@
--radius: 0.9rem; --radius: 0.9rem;
--sidebar: oklch(0.958 0.013 75); --sidebar: oklch(0.958 0.013 75);
--sidebar-foreground: oklch(0.27 0.032 52); --sidebar-foreground: oklch(0.27 0.032 52);
--sidebar-primary: oklch(0.58 0.19 42); --sidebar-primary: oklch(0.565 0.19 42);
--sidebar-primary-foreground: oklch(0.98 0.01 60); --sidebar-primary-foreground: oklch(0.98 0.01 60);
--sidebar-accent: oklch(0.928 0.035 62); --sidebar-accent: oklch(0.928 0.035 62);
--sidebar-accent-foreground: oklch(0.36 0.11 44); --sidebar-accent-foreground: oklch(0.36 0.11 44);
@ -250,12 +257,12 @@
--popover: oklch(0.992 0.005 145); --popover: oklch(0.992 0.005 145);
--popover-foreground: oklch(0.26 0.03 150); --popover-foreground: oklch(0.26 0.03 150);
/* Emerald -- leafy enough to feel grown, saturated enough to lead. */ /* Emerald -- leafy enough to feel grown, saturated enough to lead. */
--primary: oklch(0.55 0.16 155); --primary: oklch(0.52 0.16 155);
--primary-foreground: oklch(0.98 0.01 145); --primary-foreground: oklch(0.98 0.01 145);
--secondary: oklch(0.945 0.018 145); --secondary: oklch(0.945 0.018 145);
--secondary-foreground: oklch(0.31 0.05 150); --secondary-foreground: oklch(0.31 0.05 150);
--muted: oklch(0.938 0.016 145); --muted: oklch(0.938 0.016 145);
--muted-foreground: oklch(0.53 0.04 150); --muted-foreground: oklch(0.515 0.04 150);
--accent: oklch(0.925 0.03 150); --accent: oklch(0.925 0.03 150);
--accent-foreground: oklch(0.35 0.1 155); --accent-foreground: oklch(0.35 0.1 155);
--destructive: oklch(0.57 0.21 25); --destructive: oklch(0.57 0.21 25);
@ -271,7 +278,7 @@
--radius: 0.8rem; --radius: 0.8rem;
--sidebar: oklch(0.965 0.01 145); --sidebar: oklch(0.965 0.01 145);
--sidebar-foreground: oklch(0.26 0.03 150); --sidebar-foreground: oklch(0.26 0.03 150);
--sidebar-primary: oklch(0.55 0.16 155); --sidebar-primary: oklch(0.52 0.16 155);
--sidebar-primary-foreground: oklch(0.98 0.01 145); --sidebar-primary-foreground: oklch(0.98 0.01 145);
--sidebar-accent: oklch(0.925 0.03 150); --sidebar-accent: oklch(0.925 0.03 150);
--sidebar-accent-foreground: oklch(0.35 0.1 155); --sidebar-accent-foreground: oklch(0.35 0.1 155);
@ -290,12 +297,12 @@
--popover: oklch(0.99 0.014 95); --popover: oklch(0.99 0.014 95);
--popover-foreground: oklch(0.28 0.04 80); --popover-foreground: oklch(0.28 0.04 80);
/* Honey-amber -- glowing but gentle, like light through glass. */ /* Honey-amber -- glowing but gentle, like light through glass. */
--primary: oklch(0.6 0.16 70); --primary: oklch(0.555 0.16 70);
--primary-foreground: oklch(0.98 0.015 90); --primary-foreground: oklch(0.98 0.015 90);
--secondary: oklch(0.948 0.03 92); --secondary: oklch(0.948 0.03 92);
--secondary-foreground: oklch(0.33 0.06 78); --secondary-foreground: oklch(0.33 0.06 78);
--muted: oklch(0.94 0.028 92); --muted: oklch(0.94 0.028 92);
--muted-foreground: oklch(0.55 0.05 82); --muted-foreground: oklch(0.52 0.05 82);
--accent: oklch(0.93 0.04 88); --accent: oklch(0.93 0.04 88);
--accent-foreground: oklch(0.37 0.11 75); --accent-foreground: oklch(0.37 0.11 75);
--destructive: oklch(0.57 0.21 25); --destructive: oklch(0.57 0.21 25);
@ -312,7 +319,7 @@
--radius: 0.85rem; --radius: 0.85rem;
--sidebar: oklch(0.962 0.02 95); --sidebar: oklch(0.962 0.02 95);
--sidebar-foreground: oklch(0.28 0.04 80); --sidebar-foreground: oklch(0.28 0.04 80);
--sidebar-primary: oklch(0.6 0.16 70); --sidebar-primary: oklch(0.555 0.16 70);
--sidebar-primary-foreground: oklch(0.98 0.015 90); --sidebar-primary-foreground: oklch(0.98 0.015 90);
--sidebar-accent: oklch(0.93 0.04 88); --sidebar-accent: oklch(0.93 0.04 88);
--sidebar-accent-foreground: oklch(0.37 0.11 75); --sidebar-accent-foreground: oklch(0.37 0.11 75);
@ -337,7 +344,7 @@
--secondary: oklch(0.948 0.02 15); --secondary: oklch(0.948 0.02 15);
--secondary-foreground: oklch(0.32 0.06 12); --secondary-foreground: oklch(0.32 0.06 12);
--muted: oklch(0.94 0.018 15); --muted: oklch(0.94 0.018 15);
--muted-foreground: oklch(0.54 0.045 12); --muted-foreground: oklch(0.525 0.045 12);
--accent: oklch(0.93 0.032 14); --accent: oklch(0.93 0.032 14);
--accent-foreground: oklch(0.36 0.11 12); --accent-foreground: oklch(0.36 0.11 12);
/* A deeper red than the rest of the pink, so errors still read as errors. */ /* A deeper red than the rest of the pink, so errors still read as errors. */
@ -379,7 +386,7 @@
--secondary: oklch(0.948 0.018 300); --secondary: oklch(0.948 0.018 300);
--secondary-foreground: oklch(0.32 0.06 305); --secondary-foreground: oklch(0.32 0.06 305);
--muted: oklch(0.94 0.016 300); --muted: oklch(0.94 0.016 300);
--muted-foreground: oklch(0.54 0.045 305); --muted-foreground: oklch(0.525 0.045 305);
--accent: oklch(0.93 0.03 302); --accent: oklch(0.93 0.03 302);
--accent-foreground: oklch(0.36 0.11 305); --accent-foreground: oklch(0.36 0.11 305);
--destructive: oklch(0.57 0.21 25); --destructive: oklch(0.57 0.21 25);
@ -645,6 +652,561 @@
--sidebar-ring: oklch(0.72 0.11 20); --sidebar-ring: oklch(0.72 0.11 20);
} }
/* Blueprint -- a deep cobalt drafting board with white drafting paper on
top: the sheet is the white card, the ink is cobalt blue. It's a *light*
surface (the working paper is white with dark blue ink), but the page
backdrop stays deep cobalt like a blueprint under glass. The board chrome
(titles, lane labels, buttons) that sits directly on the cobalt uses the
near-white `--foreground`; the paper carries the cobalt `--card-foreground`
ink (see the extras section, which also re-inks the group-card title and
the board chrome's muted text). Sharp corners, a bright azure highlighter
for focus, and a hand-drawn architectural face. */
.theme-blueprint {
color-scheme: light;
/* The board: deep drafting cobalt. */
--background: oklch(0.42 0.13 255);
/* Board chrome ink -- sits on the cobalt, so it's near-white. */
--foreground: oklch(0.97 0.01 225);
/* The paper: warm near-white drafting sheet. */
--card: oklch(0.976 0.006 250);
/* Drafting ink: the cobalt pen that writes on the paper. */
--card-foreground: oklch(0.43 0.13 255);
--popover: oklch(0.976 0.006 250);
--popover-foreground: oklch(0.43 0.13 255);
/* The action color: a strong cobalt pen stroke. */
--primary: oklch(0.47 0.16 255);
--primary-foreground: oklch(0.99 0 0);
--secondary: oklch(0.915 0.03 250);
--secondary-foreground: oklch(0.4 0.13 255);
--muted: oklch(0.93 0.022 250);
/* Drafting ink at a muted weight (secondary text on the paper). */
--muted-foreground: oklch(0.47 0.12 255);
--accent: oklch(0.895 0.045 250);
--accent-foreground: oklch(0.38 0.13 255);
--destructive: oklch(0.58 0.2 25);
--destructive-foreground: oklch(0.99 0 0);
--border: oklch(0.74 0.06 250);
--input: oklch(0.74 0.06 250);
/* Bright azure highlighter for focus + registration marks. */
--ring: oklch(0.72 0.16 215);
--lane: oklch(1 0 0 / 8%);
--chart-1: oklch(0.58 0.14 250);
--chart-2: oklch(0.66 0.14 200);
--chart-3: oklch(0.6 0.13 180);
--chart-4: oklch(0.58 0.13 120);
--chart-5: oklch(0.6 0.14 60);
/* Sharp, technical corners. */
--radius: 0.15rem;
--sidebar: oklch(0.45 0.125 255);
--sidebar-foreground: oklch(0.96 0.01 225);
--sidebar-primary: oklch(0.97 0.01 225);
--sidebar-primary-foreground: oklch(0.4 0.13 255);
--sidebar-accent: oklch(0.5 0.11 255);
--sidebar-accent-foreground: oklch(0.97 0.01 225);
--sidebar-border: oklch(1 0 0 / 30%);
--sidebar-ring: oklch(0.72 0.16 215);
}
/* Cyberpunk -- near-black violet night + neon magenta with an electric-cyan
secondary: the loud one. Dark-surfaced; see the extras section for the
scanlines, glitch, glow, and VT323 pixel face. */
.theme-cyberpunk {
color-scheme: dark;
--background: oklch(0.16 0.03 290);
--foreground: oklch(0.9 0.03 210);
--card: oklch(0.2 0.035 290);
--card-foreground: oklch(0.9 0.03 210);
--popover: oklch(0.21 0.04 290);
--popover-foreground: oklch(0.9 0.03 210);
/* Neon magenta -- the city at night, at full saturation. */
--primary: oklch(0.72 0.24 350);
--primary-foreground: oklch(0.16 0.03 300);
--secondary: oklch(0.24 0.045 290);
--secondary-foreground: oklch(0.88 0.06 200);
--muted: oklch(0.23 0.04 290);
--muted-foreground: oklch(0.7 0.03 220);
--accent: oklch(0.28 0.06 295);
--accent-foreground: oklch(0.92 0.06 350);
--destructive: oklch(0.68 0.23 30);
--border: oklch(0.9 0.06 210 / 22%);
--input: oklch(0.9 0.06 210 / 16%);
/* Electric-cyan focus. */
--ring: oklch(0.85 0.14 195);
--lane: oklch(1 0 0 / 6%);
--chart-1: oklch(0.85 0.15 195);
--chart-2: oklch(0.75 0.18 280);
--chart-3: oklch(0.68 0.22 350);
--chart-4: oklch(0.78 0.14 230);
--chart-5: oklch(0.9 0.03 200);
/* Angular chrome. */
--radius: 0.2rem;
--sidebar: oklch(0.185 0.035 290);
--sidebar-foreground: oklch(0.9 0.03 210);
--sidebar-primary: oklch(0.72 0.24 350);
--sidebar-primary-foreground: oklch(0.16 0.03 300);
--sidebar-accent: oklch(0.28 0.06 295);
--sidebar-accent-foreground: oklch(0.92 0.06 350);
--sidebar-border: oklch(0.9 0.06 210 / 22%);
--sidebar-ring: oklch(0.85 0.14 195);
}
/* ---------------------------------------------------------------------------
EXTRA THEMES -- beyond tokens, Blueprint and Cyberpunk carry their own
fonts, backgrounds, and animations. Everything here is scoped under the
theme's own class on <html>, so no other theme is touched. The reduced-
motion guard in @layer base still kills the animations for users who
ask for it (the static styling remains).
--------------------------------------------------------------------------- */
/* ------------------------------ Blueprint -------------------------------- */
/* Engineering grid on the drafting paper: fine 16px squares with a bolder
96px major line, fixed to the viewport like a drawing under glass. */
html.theme-blueprint body {
background-image:
linear-gradient(rgb(255 255 255 / 0.14) 1px, transparent 1px),
linear-gradient(90deg, rgb(255 255 255 / 0.14) 1px, transparent 1px),
linear-gradient(rgb(255 255 255 / 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgb(255 255 255 / 0.06) 1px, transparent 1px);
background-size:
96px 96px,
96px 96px,
16px 16px,
16px 16px;
background-attachment: fixed;
}
/* The drafting face carries the whole sheet: a hand-drawn architectural face
(Architects Daughter) for headings AND for the body/to-do text -- everything
reads as lettered by hand, not just the title blocks. We retarget the
theme's sans variable so every `font-sans` surface (to-dos, buttons, nav,
note editor) picks it up. */
html.theme-blueprint {
--font-sans: var(--font-blueprint), "Segoe Print", "Bradley Hand", cursive;
--font-heading: var(--font-blueprint), "Segoe Print", "Bradley Hand", cursive;
}
/* The base body sets a tight negative letter-spacing tuned for Inter -- a
handwriting face crowds under it, so relax it back to natural spacing. */
html.theme-blueprint body,
html.theme-blueprint h1,
html.theme-blueprint h2,
html.theme-blueprint h3,
html.theme-blueprint h4 {
letter-spacing: 0;
}
/* Headings: keep the natural hand-drawn weight -- faux-bolding a single-weight
handwriting face blurs its strokes, so hold the title blocks at 400. */
html.theme-blueprint .font-heading {
font-weight: 400;
}
/* Sharp, technical corners across the UI (rounded-full stays round --
progress rings, check marks, dots keep their circles). */
html.theme-blueprint .rounded-2xl {
border-radius: 4px;
}
html.theme-blueprint .rounded-xl {
border-radius: 3px;
}
html.theme-blueprint .rounded-lg {
border-radius: 2px;
}
/* Group cards: dashed stroke, like a layer outline on a drawing, plus a deep
cobalt offset shadow for a sheet-of-paper lift off the board. */
html.theme-blueprint .border-\[3px\] {
border-style: dashed;
box-shadow: 5px 5px 0 0 rgb(14 24 64 / 0.32);
}
/* Standard cards and dialogs get a double drafting frame: a thin cobalt inner
line plus a deep offset, like a mounted sheet. */
html.theme-blueprint [data-slot="card"],
html.theme-blueprint [data-slot="dialog-content"] {
box-shadow:
0 0 0 1px rgb(40 60 160 / 0.28),
5px 5px 0 0 rgb(14 24 64 / 0.3);
}
/* You're at a drafting table -- crosshair everywhere (inputs keep the
text cursor via UA defaults). */
html.theme-blueprint {
cursor: crosshair;
}
/* Registration marks: corner brackets snap onto buttons on hover/focus,
like aligning a plotter. */
html.theme-blueprint [data-slot="button"] {
position: relative;
}
html.theme-blueprint [data-slot="button"]::before,
html.theme-blueprint [data-slot="button"]::after {
content: "";
position: absolute;
width: 10px;
height: 10px;
border: 1px solid var(--ring);
opacity: 0;
pointer-events: none;
transition:
opacity 150ms ease,
transform 150ms ease;
}
html.theme-blueprint [data-slot="button"]::before {
top: -4px;
left: -4px;
border-right: 0;
border-bottom: 0;
transform: translate(3px, 3px);
}
html.theme-blueprint [data-slot="button"]::after {
bottom: -4px;
right: -4px;
border-left: 0;
border-top: 0;
transform: translate(-3px, -3px);
}
html.theme-blueprint [data-slot="button"]:hover::before,
html.theme-blueprint [data-slot="button"]:hover::after,
html.theme-blueprint [data-slot="button"]:focus-visible::before,
html.theme-blueprint [data-slot="button"]:focus-visible::after {
opacity: 1;
transform: translate(0, 0);
}
/* Completed to-dos: a slightly heavier, pen-weight strikethrough across the
words. Native line-through spans only the glyphs (not the whole flex row),
so it reads as pen crossing out the text rather than a line across the card. */
html.theme-blueprint .line-through {
text-decoration: line-through;
text-decoration-thickness: 1.5px;
}
/* To-do checkbox: a small technical box -- squarer corner, crisp stroke, and
registration ticks that snap in on hover/focus like a plotter aligning to a
point. (The box is the base-ui checkbox button; its stroke/fill already come
from the group's own accent color.) */
html.theme-blueprint [data-todo-checkbox] {
border-radius: 2px;
}
html.theme-blueprint [data-todo-checkbox]:hover {
box-shadow: 0 0 0 3px rgb(40 60 160 / 0.18);
}
html.theme-blueprint [data-todo-checkbox]::before,
html.theme-blueprint [data-todo-checkbox]::after {
content: "";
position: absolute;
width: 6px;
height: 6px;
border: 1px solid var(--ring);
opacity: 0;
pointer-events: none;
transition: opacity 150ms ease;
}
html.theme-blueprint [data-todo-checkbox]::before {
top: -5px;
left: -5px;
border-right: 0;
border-bottom: 0;
}
html.theme-blueprint [data-todo-checkbox]::after {
bottom: -5px;
right: -5px;
border-left: 0;
border-top: 0;
}
html.theme-blueprint [data-todo-checkbox]:hover::before,
html.theme-blueprint [data-todo-checkbox]:hover::after,
html.theme-blueprint [data-todo-checkbox]:focus-visible::before,
html.theme-blueprint [data-todo-checkbox]:focus-visible::after {
opacity: 1;
}
/* The group card's title inherits the board's near-white foreground, which
would wash out on the white paper -- re-ink the whole card with the cobalt
drafting ink (the open to-do rows already carry their own accent color, and
the muted bits below read correctly on the paper). */
html.theme-blueprint [data-group-card] {
color: var(--card-foreground);
}
/* Board chrome that sits on the cobalt (the lane label, the open-to-do counts,
the add-lane button, the empty state, and the two sidebar columns) uses the
muted ink -- re-ink it light so it reads against the deep board, not the
paper. (Group cards and dialogs/popovers are the white paper and keep the
cobalt ink.) */
html.theme-blueprint [data-board-chrome] .text-muted-foreground,
html.theme-blueprint [data-board-chrome].text-muted-foreground,
html.theme-blueprint .bg-sidebar .text-muted-foreground,
html.theme-blueprint .bg-sidebar.text-muted-foreground {
color: var(--sidebar-foreground);
}
/* ------------------------------ Cyberpunk -------------------------------- */
/* The pixel face runs the whole deck: VT323 for headings AND for the
body/to-do text, so everything reads like a CRT terminal, not just the
headers. We retarget the theme's sans variable so every `font-sans`
surface (to-dos, buttons, nav, note editor) picks it up. */
html.theme-cyberpunk {
--font-sans: var(--font-cyberpunk), ui-monospace, monospace;
--font-heading: var(--font-cyberpunk), ui-monospace, monospace;
}
/* Headings keep the pixel face + a touch of tracking for the terminal feel. */
html.theme-cyberpunk .font-heading {
font-family: var(--font-cyberpunk), monospace;
letter-spacing: 0.04em;
}
/* Angular chrome (rounded-full stays round). */
html.theme-cyberpunk .rounded-2xl {
border-radius: 4px;
}
html.theme-cyberpunk .rounded-xl {
border-radius: 3px;
}
html.theme-cyberpunk .rounded-lg {
border-radius: 2px;
}
/* Faint tron grid under everything. */
html.theme-cyberpunk body {
background-image:
linear-gradient(rgb(34 255 238 / 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgb(34 255 238 / 0.04) 1px, transparent 1px);
background-size: 28px 28px;
}
/* CRT scanlines + vignette over the whole app -- fixed and click-through,
so it sits over dialogs and toasts too, the way a monitor sits over
everything. */
html.theme-cyberpunk body::before {
content: "";
position: fixed;
inset: 0;
z-index: 999;
pointer-events: none;
background:
radial-gradient(120% 90% at 50% 45%, transparent 55%, rgb(0 0 0 / 0.35) 100%),
repeating-linear-gradient(
0deg,
rgb(0 0 0 / 0.12) 0px,
rgb(0 0 0 / 0.12) 1px,
transparent 1px,
transparent 3px
);
}
/* A slow scan band drifting down the screen, like a CRT refresh. */
html.theme-cyberpunk body::after {
content: "";
position: fixed;
left: 0;
right: 0;
top: 0;
height: 15%;
z-index: 999;
pointer-events: none;
background: linear-gradient(
180deg,
transparent,
rgb(34 255 238 / 0.05) 45%,
rgb(255 43 214 / 0.04) 55%,
transparent
);
animation: cp-scan 9s linear infinite;
}
@keyframes cp-scan {
from {
transform: translateY(-120%);
}
to {
transform: translateY(720%);
}
}
/* Neon focus glow on top of the standard ring. */
html.theme-cyberpunk :focus-visible {
box-shadow:
0 0 0 3px color-mix(in oklch, var(--ring) 45%, transparent),
0 0 16px rgb(34 255 238 / 0.4);
}
/* Energized surfaces: anything filled with the primary color blooms. */
html.theme-cyberpunk .bg-primary {
box-shadow: 0 0 14px rgb(255 43 214 / 0.3);
}
/* Glitch: a brief boot flicker whenever the theme (re)applies, and an
RGB-split jitter when a heading is hovered. */
html.theme-cyberpunk .font-heading {
animation: cp-boot 700ms steps(12, jump-none) 1;
}
@keyframes cp-boot {
0% {
opacity: 0;
}
8% {
opacity: 1;
}
14% {
opacity: 0.3;
}
22% {
opacity: 1;
}
30% {
opacity: 0.55;
}
42%,
100% {
opacity: 1;
}
}
html.theme-cyberpunk .font-heading:hover {
text-shadow:
2px 0 rgb(255 43 214 / 0.85),
-2px 0 rgb(34 255 238 / 0.85);
animation: cp-glitch 260ms steps(3, jump-none) 1;
}
@keyframes cp-glitch {
0%,
100% {
transform: translate(0, 0) skewX(0deg);
}
25% {
transform: translate(-2px, 1px) skewX(-6deg);
}
50% {
transform: translate(3px, -1px) skewX(3deg);
}
75% {
transform: translate(-1px, 0) skewX(-2deg);
}
}
/* Corruption: completed to-dos glitch out before they fade/drop away. */
html.theme-cyberpunk .line-through {
animation: cp-corrupt 450ms steps(6, jump-none) 1;
}
@keyframes cp-corrupt {
0% {
transform: translate(0, 0);
text-shadow: none;
}
15% {
transform: translate(-3px, 1px) skewX(-8deg);
text-shadow:
3px 0 rgb(255 43 214 / 0.9),
-3px 0 rgb(34 255 238 / 0.9);
}
35% {
transform: translate(4px, -1px) skewX(5deg);
text-shadow:
-4px 0 rgb(34 255 238 / 0.9),
4px 0 rgb(255 43 214 / 0.9);
}
55% {
transform: translate(-2px, 1px);
text-shadow:
2px 0 rgb(255 43 214 / 0.8),
-2px 0 rgb(34 255 238 / 0.8);
opacity: 0.7;
}
75% {
transform: translate(2px, -1px) skewX(-3deg);
text-shadow:
-2px 0 rgb(255 43 214 / 0.7),
2px 0 rgb(34 255 238 / 0.7);
}
100% {
transform: translate(0, 0);
text-shadow: none;
opacity: 1;
}
}
/* To-do checkbox: a neon terminal box -- faint scanline texture on the empty
box, a glow that builds on hover, and a full bloom the moment it's checked.
(The check's own stroke/fill already come from the group's accent color.) */
html.theme-cyberpunk [data-todo-checkbox] {
border-radius: 2px;
}
html.theme-cyberpunk [data-todo-checkbox]:not([aria-checked="true"]) {
background-image: repeating-linear-gradient(
0deg,
rgb(34 255 238 / 0.07) 0px,
rgb(34 255 238 / 0.07) 1px,
transparent 1px,
transparent 3px
);
}
html.theme-cyberpunk [data-todo-checkbox]:hover {
box-shadow: 0 0 10px rgb(34 255 238 / 0.5);
}
html.theme-cyberpunk [data-todo-checkbox][aria-checked="true"] {
box-shadow: 0 0 12px rgb(34 255 238 / 0.6);
}
html.theme-cyberpunk [data-todo-checkbox] svg {
filter: drop-shadow(0 0 3px rgb(34 255 238 / 0.85));
}
/* The whole card glitches every so often -- a brief RGB-split, a skew, and a
sliced clip, then it settles back. Most of the cycle is calm; the burst is a
handful of held frames (steps, so it jitters rather than eases). Cards are
phase-offset so a boardful of them don't all tear on the same beat. */
html.theme-cyberpunk [data-group-card] {
animation: cp-card-glitch 7s steps(1, end) infinite;
will-change: transform, filter;
}
html.theme-cyberpunk [data-group-card]:nth-child(2n) {
animation-delay: -3.5s;
}
html.theme-cyberpunk [data-group-card]:nth-child(3n) {
animation-delay: -1.7s;
}
@keyframes cp-card-glitch {
0%,
86%,
93%,
100% {
/* rest: no properties set -> the card keeps its natural transform,
filter, clip and shadow (incl. hover lift) between bursts */
}
87% {
transform: translate(-2px, 1px) skewX(-3deg);
filter: saturate(1.6) hue-rotate(-16deg);
box-shadow:
3px 0 rgb(255 43 214 / 0.55),
-3px 0 rgb(34 255 238 / 0.55);
}
88% {
transform: translate(3px, -1px);
clip-path: inset(0 0 14% 0);
filter: saturate(1.8);
}
89% {
transform: translate(-1px, 1px) skewX(2deg);
clip-path: inset(12% 0 0 0);
}
90% {
transform: translate(2px, -1px);
clip-path: inset(6% 0 18% 0);
box-shadow:
-4px 0 rgb(255 43 214 / 0.5),
4px 0 rgb(34 255 238 / 0.5);
}
91% {
transform: translate(-2px, 0) skewX(-2deg);
filter: hue-rotate(14deg);
}
92% {
transform: translate(1px, 1px);
filter: saturate(1.4);
}
}
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;

View File

@ -1,5 +1,5 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Figtree, Inter, JetBrains_Mono } from "next/font/google"; import { Architects_Daughter, Figtree, Inter, JetBrains_Mono, VT323 } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "@/components/theme/theme-provider"; import { ThemeProvider } from "@/components/theme/theme-provider";
@ -27,6 +27,21 @@ const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"], subsets: ["latin"],
}); });
/** Blueprint theme's drafting face (see app/globals.css) -- a hand-drawn
* architectural face, lettered like a drawing's title block. */
const architectsDaughter = Architects_Daughter({
variable: "--font-blueprint",
subsets: ["latin"],
weight: "400",
});
/** Cyberpunk theme's pixel-terminal face (see app/globals.css). */
const vt323 = VT323({
variable: "--font-cyberpunk",
subsets: ["latin"],
weight: "400",
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Organize", title: "Organize",
description: "An extended to-do list and notes organizer.", description: "An extended to-do list and notes organizer.",
@ -53,19 +68,21 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
return ( return (
<html <html
lang="en" lang="en"
className={`${inter.variable} ${figtree.variable} ${jetbrainsMono.variable} h-full antialiased`} className={`${inter.variable} ${figtree.variable} ${jetbrainsMono.variable} ${architectsDaughter.variable} ${vt323.variable} h-full antialiased`}
suppressHydrationWarning suppressHydrationWarning
> >
<body className="min-h-full flex flex-col"> <body className="min-h-full flex flex-col">
{/* Fourteen looks: seven light (Default, Sunset, Meadow, Honey, {/* Sixteen looks: eight light (Default, Sunset, Meadow, Honey, Rosé,
Rosé, Lavender, Slate) and seven dark (Dark, Ocean, Pine, Plum, Lavender, Slate, Blueprint) and eight dark (Dark, Ocean, Pine,
Midnight, Ember, Rosewood) -- complete token sets in Plum, Midnight, Ember, Rosewood, Cyberpunk) -- complete token
app/globals.css. "system" resolves to Default/Dark by OS sets in app/globals.css; Blueprint and Cyberpunk additionally
preference. No-FOUC theme restore: runs before first paint, carry their own fonts, backgrounds, and animations.
applies the stored preference to <html> (class + color-scheme). */} "system" resolves to Default/Dark by OS preference.
No-FOUC theme restore: runs before first paint, applies the
stored preference to <html> (class + color-scheme). */}
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `!(function(){try{var r=document.documentElement,c=['theme-default','theme-sunset','theme-meadow','theme-honey','theme-rose','theme-lavender','theme-slate','dark','ocean','theme-pine','theme-plum','theme-midnight','theme-ember','theme-rosewood','light'];for(var i=0;i<c.length;i++){r.classList.remove(c[i]);}var v=null;try{v=localStorage.getItem('theme');}catch(e){}var m={default:'theme-default',sunset:'theme-sunset',meadow:'theme-meadow',honey:'theme-honey',rose:'theme-rose',lavender:'theme-lavender',slate:'theme-slate',dark:'dark',ocean:'ocean',pine:'theme-pine',plum:'theme-plum',midnight:'theme-midnight',ember:'theme-ember',rosewood:'theme-rosewood'};var d=['dark','ocean','pine','plum','midnight','ember','rosewood'];if(v==='system'||!v){v=window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'default';}if(m[v]){r.classList.add(m[v]);r.style.colorScheme=d.indexOf(v)>=0?'dark':'light';}}catch(e){}})();`, __html: `!(function(){try{var r=document.documentElement,c=['theme-default','theme-sunset','theme-meadow','theme-honey','theme-rose','theme-lavender','theme-slate','theme-blueprint','dark','ocean','theme-pine','theme-plum','theme-midnight','theme-ember','theme-rosewood','theme-cyberpunk','light'];for(var i=0;i<c.length;i++){r.classList.remove(c[i]);}var v=null;try{v=localStorage.getItem('theme');}catch(e){}var m={default:'theme-default',sunset:'theme-sunset',meadow:'theme-meadow',honey:'theme-honey',rose:'theme-rose',lavender:'theme-lavender',slate:'theme-slate',blueprint:'theme-blueprint',dark:'dark',ocean:'ocean',pine:'theme-pine',plum:'theme-plum',midnight:'theme-midnight',ember:'theme-ember',rosewood:'theme-rosewood',cyberpunk:'theme-cyberpunk'};var d=['dark','ocean','pine','plum','midnight','ember','rosewood','cyberpunk'];if(v==='system'||!v){v=window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'default';}if(m[v]){r.classList.add(m[v]);r.style.colorScheme=d.indexOf(v)>=0?'dark':'light';}}catch(e){}})();`,
}} }}
/> />
<ThemeProvider defaultTheme="system"> <ThemeProvider defaultTheme="system">

View File

@ -38,6 +38,7 @@ export function AddCategoryLane() {
<DialogTrigger <DialogTrigger
render={ render={
<button <button
data-board-chrome=""
className="flex h-full min-h-32 w-72 shrink-0 flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-border/80 text-muted-foreground transition-colors hover:border-primary/60 hover:bg-primary/5 hover:text-primary" className="flex h-full min-h-32 w-72 shrink-0 flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-border/80 text-muted-foreground transition-colors hover:border-primary/60 hover:bg-primary/5 hover:text-primary"
aria-label="Add category" aria-label="Add category"
> >

View File

@ -103,7 +103,10 @@ export function CategoryLane({ category }: { category: CategoryDTO }) {
isDragging && "opacity-50" isDragging && "opacity-50"
)} )}
> >
<div className="flex items-center gap-1.5 px-3 pt-3 pb-2"> <div
data-board-chrome=""
className="flex items-center gap-1.5 px-3 pt-3 pb-2"
>
<button <button
ref={setActivatorNodeRef} ref={setActivatorNodeRef}
{...attributes} {...attributes}

View File

@ -2,7 +2,10 @@ import { LayoutDashboard } from "lucide-react";
export function EmptyState() { export function EmptyState() {
return ( return (
<div className="flex flex-1 flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-border/80 p-10 text-center"> <div
data-board-chrome=""
className="flex flex-1 flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-border/80 p-10 text-center"
>
<div className="flex size-14 items-center justify-center rounded-2xl bg-primary/10 text-primary"> <div className="flex size-14 items-center justify-center rounded-2xl bg-primary/10 text-primary">
<LayoutDashboard className="size-7" /> <LayoutDashboard className="size-7" />
</div> </div>

View File

@ -186,6 +186,7 @@ export function GroupCard({ group }: { group: GroupDTO }) {
<> <>
<div <div
ref={setNodeRef} ref={setNodeRef}
data-group-card=""
style={{ style={{
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,

View File

@ -128,7 +128,10 @@ function Board({ title }: { title: string }) {
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<div className="flex h-full flex-col gap-4 p-4"> <div className="flex h-full flex-col gap-4 p-4">
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2 px-1"> <div
data-board-chrome=""
className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2 px-1"
>
<div className="flex min-w-0 items-center gap-3"> <div className="flex min-w-0 items-center gap-3">
<span className="hidden size-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary sm:flex"> <span className="hidden size-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary sm:flex">
<LayoutDashboard className="size-5" /> <LayoutDashboard className="size-5" />

View File

@ -32,6 +32,7 @@ export function TodoCheckbox({
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
checked={checked} checked={checked}
onCheckedChange={onCheckedChange} onCheckedChange={onCheckedChange}
data-todo-checkbox=""
aria-label={ariaLabel} aria-label={ariaLabel}
style={{ style={{
borderColor: accentColor, borderColor: accentColor,

View File

@ -37,6 +37,7 @@ export const THEMES = [
"rose", "rose",
"lavender", "lavender",
"slate", "slate",
"blueprint",
"dark", "dark",
"ocean", "ocean",
"pine", "pine",
@ -44,6 +45,7 @@ export const THEMES = [
"midnight", "midnight",
"ember", "ember",
"rosewood", "rosewood",
"cyberpunk",
] as const; ] as const;
export type ThemeName = (typeof THEMES)[number]; export type ThemeName = (typeof THEMES)[number];
export type RawTheme = ThemeName | "system"; export type RawTheme = ThemeName | "system";
@ -57,6 +59,7 @@ const THEME_CLASSES: Record<ThemeName, string> = {
rose: "theme-rose", rose: "theme-rose",
lavender: "theme-lavender", lavender: "theme-lavender",
slate: "theme-slate", slate: "theme-slate",
blueprint: "theme-blueprint",
dark: "dark", dark: "dark",
ocean: "ocean", ocean: "ocean",
pine: "theme-pine", pine: "theme-pine",
@ -64,6 +67,7 @@ const THEME_CLASSES: Record<ThemeName, string> = {
midnight: "theme-midnight", midnight: "theme-midnight",
ember: "theme-ember", ember: "theme-ember",
rosewood: "theme-rosewood", rosewood: "theme-rosewood",
cyberpunk: "theme-cyberpunk",
}; };
const ALL_CLASSES = Object.values(THEME_CLASSES); const ALL_CLASSES = Object.values(THEME_CLASSES);
/* Themes whose *surfaces* are dark (see app/globals.css). The single source /* Themes whose *surfaces* are dark (see app/globals.css). The single source
@ -77,6 +81,7 @@ export const DARK_SURFACES: ThemeName[] = [
"midnight", "midnight",
"ember", "ember",
"rosewood", "rosewood",
"cyberpunk",
]; ];
interface ThemeContextValue { interface ThemeContextValue {

View File

@ -18,6 +18,8 @@ import {
Star, Star,
Flame, Flame,
Wine, Wine,
DraftingCompass,
CircuitBoard,
} from "lucide-react"; } from "lucide-react";
import { useTheme } from "@/components/theme/theme-provider"; import { useTheme } from "@/components/theme/theme-provider";
@ -49,6 +51,7 @@ const LIGHT_OPTIONS: ThemeOption[] = [
{ value: "rose", label: "Rosé", icon: Rose, swatch: "#e11d48" }, { value: "rose", label: "Rosé", icon: Rose, swatch: "#e11d48" },
{ value: "lavender", label: "Lavender", icon: Flower2, swatch: "#7c3aed" }, { value: "lavender", label: "Lavender", icon: Flower2, swatch: "#7c3aed" },
{ value: "slate", label: "Slate", icon: Mountain, swatch: "#4a6d9c" }, { value: "slate", label: "Slate", icon: Mountain, swatch: "#4a6d9c" },
{ value: "blueprint", label: "Blueprint", icon: DraftingCompass, swatch: "#2b4bc4" },
]; ];
const DARK_OPTIONS: ThemeOption[] = [ const DARK_OPTIONS: ThemeOption[] = [
{ value: "dark", label: "Dark", icon: Moon, swatch: "#7c83f2" }, { value: "dark", label: "Dark", icon: Moon, swatch: "#7c83f2" },
@ -58,6 +61,7 @@ const DARK_OPTIONS: ThemeOption[] = [
{ value: "midnight", label: "Midnight", icon: Star, swatch: "#93c5fd" }, { value: "midnight", label: "Midnight", icon: Star, swatch: "#93c5fd" },
{ value: "ember", label: "Ember", icon: Flame, swatch: "#fbbf24" }, { value: "ember", label: "Ember", icon: Flame, swatch: "#fbbf24" },
{ value: "rosewood", label: "Rosewood", icon: Wine, swatch: "#fb7185" }, { value: "rosewood", label: "Rosewood", icon: Wine, swatch: "#fb7185" },
{ value: "cyberpunk", label: "Cyberpunk", icon: CircuitBoard, swatch: "#ff2bd6" },
]; ];
const ALL_OPTIONS = [...LIGHT_OPTIONS, ...DARK_OPTIONS]; const ALL_OPTIONS = [...LIGHT_OPTIONS, ...DARK_OPTIONS];

View File

@ -20,7 +20,7 @@ function useMounted(): boolean {
/** /**
* True when the active theme has dark surfaces (Dark, Ocean, Pine, Plum, * True when the active theme has dark surfaces (Dark, Ocean, Pine, Plum,
* Midnight, Ember, Rosewood). * Midnight, Ember, Rosewood, Cyberpunk).
* *
* Reactive to UI theme switches: the value is derived from the provider's * Reactive to UI theme switches: the value is derived from the provider's
* theme state (the single source of truth), so the moment `setTheme` fires, * theme state (the single source of truth), so the moment `setTheme` fires,

View File

@ -164,7 +164,11 @@ export function getBrighterColor(hex: string, variant: "light" | "dark"): string
if (cached) return cached; if (cached) return cached;
const [hue, saturation, lightness] = rgbToHsl(...hexToRgb(hex)); const [hue, saturation, lightness] = rgbToHsl(...hexToRgb(hex));
const delta = variant === "dark" ? 8 : -8; // Light surfaces: the to-do ink runs DEEPER than the group's stroke (not
// lighter) so it clears WCAG AA (>=4.5:1) against the near-white pastel
// card fills -- a flat -8 only reached ~3.3:1 for the warm hues. Dark
// surfaces do the opposite: brighter ink over the dark tint.
const delta = variant === "dark" ? 8 : -16;
const result = hslToHex( const result = hslToHex(
hue, hue,
saturation, saturation,