Theme-Stuff #4

Merged
brianfertig merged 2 commits from Theme-Stuff into main 2026-08-29 22:26:15 +00:00
3 changed files with 412 additions and 13 deletions

View File

@ -1023,6 +1023,48 @@ html.theme-blueprint .line-through {
text-decoration-thickness: 1.5px;
}
/* Then a red-ink "APPROVED" stamp thuds onto the row a beat after the strike
-- a hard scale-down with a small overshoot, settling tilted a few degrees
like a QC stamp pressed onto the drawing. Scoped to the board's to-do rows:
the title button is a flex row, so the stamp rides at its right end (the
scheduled panel's completed rows are truncate buttons, where a flex item
would get clipped by the ellipsis). */
html.theme-blueprint [data-group-card] .line-through::after {
content: "APPROVED";
flex: none;
margin-left: 4px;
padding: 1px 6px;
border: 1.5px solid oklch(0.58 0.2 25 / 0.75);
box-shadow: inset 0 0 0 1px oklch(0.58 0.2 25 / 0.28);
border-radius: 2px;
color: oklch(0.58 0.2 25 / 0.9);
font-size: 10px;
line-height: 1.6;
letter-spacing: 0.08em;
opacity: 0.9;
transform: rotate(-6deg);
animation: bp-stamp 380ms cubic-bezier(0.3, 0.9, 0.4, 1) 160ms backwards;
mix-blend-mode: multiply;
pointer-events: none;
}
@keyframes bp-stamp {
0% {
opacity: 0;
transform: rotate(-14deg) scale(2.1);
}
60% {
opacity: 0.9;
transform: rotate(-5deg) scale(0.92);
}
80% {
transform: rotate(-6.5deg) scale(1.06);
}
100% {
opacity: 0.9;
transform: rotate(-6deg) scale(1);
}
}
/* 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
@ -1512,21 +1554,25 @@ html.theme-vaporwave body {
oklch(0.955 0.055 190) 100%
);
}
/* Faint scanlines rolling slowly, like a monitor left on in the morning. */
/* Rolling scanlines: a fixed overlay over the whole app -- the "monitor"
sits over everything (cards, dialogs, toasts), the same move as cyberpunk's
CRT layer, so the texture is actually visible instead of hiding behind the
content. A 1px plum line every 4px, rolling downward at about 1px/s like a
monitor left on all morning. */
html.theme-vaporwave body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
z-index: 999;
pointer-events: none;
background-image: repeating-linear-gradient(
0deg,
oklch(0.42 0.16 320 / 0.035) 0px,
oklch(0.42 0.16 320 / 0.035) 1px,
oklch(0.35 0.16 330 / 0.06) 0px,
oklch(0.35 0.16 330 / 0.06) 1px,
transparent 1px,
transparent 4px
);
animation: vw-roll 16s linear infinite;
animation: vw-roll 4s linear infinite;
}
@keyframes vw-roll {
from {
@ -1537,6 +1583,260 @@ html.theme-vaporwave body::before {
}
}
/* The striped synth sun: the vaporwave icon, fixed in the sky behind the
content (it shows through the translucent lanes like light through glass).
Big and low, sitting on the outrun floor's horizon line. Solid across the
top, classic horizontal slats below, the slats drifting slowly downward
like the sun melting toward the horizon. Muted pastels close to the sky's
own lightness, so it reads as part of the sky rather than a sticker on it. */
html.theme-vaporwave body::after {
content: "";
position: fixed;
left: 50%;
bottom: 7vh;
width: 320px;
height: 320px;
z-index: -1;
pointer-events: none;
border-radius: 50%;
transform: translateX(-50%);
background: linear-gradient(
180deg,
oklch(0.93 0.055 90) 0%,
oklch(0.91 0.075 48) 38%,
oklch(0.89 0.09 30) 68%,
oklch(0.86 0.085 350) 100%
);
filter: drop-shadow(0 0 26px oklch(0.85 0.1 40 / 0.3));
-webkit-mask-image: linear-gradient(180deg, #000 0 55%, rgba(0, 0, 0, 0) 55%),
repeating-linear-gradient(180deg, #000 0 10px, rgba(0, 0, 0, 0) 10px 18px);
mask-image: linear-gradient(180deg, #000 0 55%, rgba(0, 0, 0, 0) 55%),
repeating-linear-gradient(180deg, #000 0 10px, rgba(0, 0, 0, 0) 10px 18px);
animation: vw-sun 5s linear infinite;
}
@keyframes vw-sun {
from {
-webkit-mask-position: 0 0, 0 0;
mask-position: 0 0, 0 0;
}
to {
-webkit-mask-position: 0 0, 0 18px;
mask-position: 0 0, 0 18px;
}
}
/* The outrun floor: a perspective grid across the bottom of the viewport,
behind the content (it shows through the translucent lanes). The horizontal
lines crawl toward the viewer while the verticals converge on the
vanishing point. Rendered by the .vw-floor element in app/layout.tsx,
visible only under this theme. */
.vw-floor {
display: none;
}
html.theme-vaporwave .vw-floor {
display: block;
position: fixed;
left: 50%;
bottom: 0;
width: 200vw;
height: 75vh;
z-index: -1;
pointer-events: none;
transform: translateX(-50%) perspective(300px) rotateX(60deg);
transform-origin: 50% 100%;
opacity: 0.8;
background-image:
repeating-linear-gradient(90deg, oklch(0.62 0.25 350 / 0.5) 0 2px, transparent 2px 90px),
repeating-linear-gradient(0deg, oklch(0.62 0.25 350 / 0.5) 0 2px, transparent 2px 44px);
-webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 35%);
mask-image: linear-gradient(180deg, transparent 0%, #000 35%);
animation: vw-floor 2.2s linear infinite;
}
@keyframes vw-floor {
from {
background-position: 0 0, 0 0;
}
to {
background-position: 0 0, 0 44px;
}
}
/* Palm silhouettes: a couple of minimal palms rooted at either side of the
horizon -- the classic vaporwave framing, kept faint so they read as
scenery, not stickers. Drawn as stroked arcs in the .vw-palm SVGs in
app/layout.tsx; they paint above the floor grid (DOM order) but stay
behind the content. Visible only under this theme. */
.vw-palm {
display: none;
}
.vw-palm-defs {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
html.theme-vaporwave .vw-palm {
display: block;
position: fixed;
bottom: 1vh;
z-index: -1;
pointer-events: none;
color: oklch(0.47 0.12 325);
}
html.theme-vaporwave .vw-palm--left-main {
left: 18vw;
width: 150px;
height: 187px;
opacity: 0.5;
}
html.theme-vaporwave .vw-palm--left-small {
left: 26vw;
width: 90px;
height: 112px;
opacity: 0.3;
}
html.theme-vaporwave .vw-palm--right-main {
right: 2vw;
width: 150px;
height: 187px;
opacity: 0.5;
transform: scaleX(-1);
}
html.theme-vaporwave .vw-palm--right-small {
right: 10vw;
width: 90px;
height: 112px;
opacity: 0.3;
transform: scaleX(-1);
}
/* VHS hiccup: every so often a card drops out of the signal for half a
second -- a 1-2px jitter, a red/cyan split on the title, and a tracking
band sweeping down the panel, all inside one short window (the first 5%
of each cycle). Cards run three alternating phases with different periods
and offsets, so the board flickers organically instead of in lockstep.
The :not(.opacity-50) guard matters: while dnd-kit drives a drag it sets
an inline `transform` on the card, and a running animation would override
that inline value (animations sit above inline styles in the cascade) --
the card stops following the pointer. opacity-50 is exactly the class the
card gets while dragging, so the hiccup stands down for the duration.
(overflow: hidden keeps the tracking band clipped to the card while it
sweeps; the hover lift uses the separate `translate` property, so the
jitter doesn't fight it.) */
html.theme-vaporwave [data-group-card]:not(.opacity-50) {
overflow: hidden;
animation: vw-card-glitch 10s linear infinite;
}
html.theme-vaporwave [data-group-card]:not(.opacity-50) > div:first-child h3 {
animation: vw-title-glitch 10s linear infinite;
}
html.theme-vaporwave [data-group-card]:not(.opacity-50)::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: -12%;
height: 12px;
opacity: 0;
pointer-events: none;
background:
repeating-linear-gradient(0deg, oklch(0.45 0.15 330 / 0.28) 0 1px, transparent 1px 3px),
linear-gradient(
180deg,
transparent 0%,
oklch(0.72 0.2 350 / 0.22) 20%,
oklch(0.99 0.01 220 / 0.4) 50%,
oklch(0.78 0.15 200 / 0.22) 80%,
transparent 100%
);
animation: vw-tracking 10s linear infinite;
}
/* Phase B: a longer period and an offset start, so neighbors don't hiccup
together. (Static styles above are the idle state -- before a card's
first delay elapses, and under reduced motion, everything just sits still.) */
html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n + 2) [data-group-card]:not(.opacity-50),
html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n + 2) [data-group-card]:not(.opacity-50) > div:first-child h3,
html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n + 2) [data-group-card]:not(.opacity-50)::after {
animation-duration: 13s;
animation-delay: 3s;
}
/* Phase C: the longest period, offset furthest. */
html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n) [data-group-card]:not(.opacity-50),
html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n) [data-group-card]:not(.opacity-50) > div:first-child h3,
html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n) [data-group-card]:not(.opacity-50)::after {
animation-duration: 17s;
animation-delay: 7s;
}
@keyframes vw-card-glitch {
0%,
5%,
100% {
transform: translateX(0);
}
0.8% {
transform: translateX(-1.5px);
}
1.6% {
transform: translateX(-1.5px);
}
2.4% {
transform: translateX(1px);
}
3.2% {
transform: translateX(1px);
}
4% {
transform: translateX(-0.75px);
}
4.6% {
transform: translateX(0.5px);
}
}
@keyframes vw-title-glitch {
0%,
5%,
100% {
text-shadow: none;
}
0.8% {
text-shadow:
1.5px 0 oklch(0.68 0.2 350 / 0.55),
-1.5px 0 oklch(0.85 0.15 200 / 0.55);
}
1.6% {
text-shadow:
-1.5px 0 oklch(0.68 0.2 350 / 0.55),
1.5px 0 oklch(0.85 0.15 200 / 0.55);
}
2.4% {
text-shadow:
1px 0 oklch(0.68 0.2 350 / 0.55),
-1px 0 oklch(0.85 0.15 200 / 0.55);
}
3.2% {
text-shadow:
-0.5px 0 oklch(0.68 0.2 350 / 0.55),
0.5px 0 oklch(0.85 0.15 200 / 0.55);
}
}
@keyframes vw-tracking {
0% {
top: -12%;
opacity: 0;
}
0.8% {
opacity: 0.85;
}
4.8% {
opacity: 0.85;
}
5%,
100% {
top: 112%;
opacity: 0;
}
}
/* Primary blooms with a soft magenta glow; focus glows electric cyan. */
html.theme-vaporwave .bg-primary {
box-shadow: 0 4px 18px oklch(0.62 0.25 350 / 0.35);
@ -1731,18 +2031,58 @@ html.theme-notebook body {
background-image: linear-gradient(180deg, oklch(0.88 0.024 82), oklch(0.84 0.028 80));
}
/* Ruled paper on the white card surfaces: blue rules + a red margin line on
the left, the classic notebook page. */
html.theme-notebook [data-slot="card"],
html.theme-notebook [data-slot="dialog-content"] {
/* The lane is the notebook page itself: white paper over the warm desk with
the classic ruling -- blue lines + a red margin rule running down the left
(inside the lane's own padding, so the post-its never cover it). Group
cards sit on top as post-its (below), so the ruling shows around them. */
html.theme-notebook [data-category-lane] {
background-color: oklch(0.978 0.008 90);
background-image:
linear-gradient(90deg, transparent 24px, oklch(0.6 0.19 25 / 0.4) 24px 26px, transparent 26px),
linear-gradient(90deg, transparent 6px, oklch(0.6 0.19 25 / 0.4) 6px 8px, transparent 8px),
repeating-linear-gradient(180deg, transparent 0 27px, oklch(0.68 0.09 250 / 0.4) 27px 28px);
box-shadow:
0 1px 2px rgb(96 74 40 / 0.14),
0 10px 24px -10px rgb(96 74 40 / 0.3);
}
/* Group cards keep their own pastel tint (they read as index cards) but carry
a faint red margin line so they stay in the stationery family. */
/* Group cards are post-its stuck onto the page: a slightly darker adhesive
band along the top, and a soft lift shadow that runs heavier to one side,
like a sheet curling off the paper. (:not(.ring-2) keeps the drag
drop-target ring -- a box-shadow utility -- from being overpainted.) */
html.theme-notebook [data-group-card] {
background-image: linear-gradient(90deg, transparent 20px, oklch(0.6 0.19 25 / 0.3) 20px 22px, transparent 22px);
border-width: 2px;
background-image: linear-gradient(180deg, rgb(96 74 40 / 0.07) 0 15%, transparent 15%);
}
html.theme-notebook [data-group-card]:not(.ring-2) {
box-shadow:
1px 1px 2px rgb(96 74 40 / 0.12),
6px 9px 16px -6px rgb(96 74 40 / 0.32);
}
html.theme-notebook [data-group-card]:not(.ring-2):hover {
box-shadow:
1px 2px 3px rgb(96 74 40 / 0.14),
8px 12px 20px -6px rgb(96 74 40 / 0.4);
}
/* A post-it is never stuck perfectly straight: alternate a hair of tilt down
the column so a boardful don't all lean the same way. (While a card is
being dragged, dnd-kit's inline transform overrides this -- the post-it
straightens out in your hand.) */
html.theme-notebook [data-category-lane] .overflow-y-auto > div:nth-child(2n) [data-group-card] {
transform: rotate(0.4deg);
}
html.theme-notebook [data-category-lane] .overflow-y-auto > div:nth-child(2n + 1) [data-group-card] {
transform: rotate(-0.4deg);
}
/* Hand-cut paper edges: the classic uneven border-radius so pages, cards, and
dialogs look drawn by hand instead of machine-rounded. */
html.theme-notebook [data-category-lane] {
border-radius: 170px 10px 150px 10px / 10px 150px 10px 170px;
}
html.theme-notebook [data-slot="card"],
html.theme-notebook [data-slot="dialog-content"],
html.theme-notebook [data-group-card] {
border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
}
/* Blue-ink focus. */
@ -1783,6 +2123,34 @@ html.theme-notebook .line-through {
background-size: 100% 60%;
}
}
/* Then a red pen draws a quick check at the end of the line -- grading the
homework. (Scoped to the board's to-do rows: the title button is a flex
row, so the check rides at its right end. It starts fully clipped and
reveals left-to-right, timed to land as the highlighter sweep finishes.) */
html.theme-notebook [data-group-card] .line-through::after {
content: "✓";
flex: none;
margin-left: 2px;
color: oklch(0.55 0.22 27 / 0.95);
font-size: 1.15em;
line-height: 1;
transform: translateY(-1px);
clip-path: inset(-25% 100% -25% 0);
animation: nb-check 300ms ease-out 300ms forwards;
pointer-events: none;
}
@keyframes nb-check {
to {
clip-path: inset(-25% -5% -25% 0);
}
}
/* The global reduced-motion guard kills the reveal animation -- fall back to
the check simply sitting there, already drawn. */
@media (prefers-reduced-motion: reduce) {
html.theme-notebook [data-group-card] .line-through::after {
clip-path: none;
}
}
@layer base {
* {

View File

@ -116,6 +116,36 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
__html: `!(function(){try{var r=document.documentElement,c=['theme-default','theme-sunset','theme-meadow','theme-honey','theme-rose','theme-lavender','theme-slate','theme-blueprint','theme-vaporwave','theme-notebook','dark','ocean','theme-pine','theme-plum','theme-midnight','theme-ember','theme-rosewood','theme-cyberpunk','theme-starfield','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',vaporwave:'theme-vaporwave',notebook:'theme-notebook',dark:'dark',ocean:'ocean',pine:'theme-pine',plum:'theme-plum',midnight:'theme-midnight',ember:'theme-ember',rosewood:'theme-rosewood',cyberpunk:'theme-cyberpunk',starfield:'theme-starfield'};var d=['dark','ocean','pine','plum','midnight','ember','rosewood','cyberpunk','starfield'];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){}})();`,
}}
/>
{/* Vaporwave scenery (see app/globals.css): the outrun floor grid,
plus palm silhouettes at either side of the horizon. All fixed
behind the content, hidden by default; the theme class on <html>
switches them on. */}
<div className="vw-floor" aria-hidden />
<svg className="vw-palm-defs" aria-hidden>
<symbol id="vw-palm" viewBox="0 0 120 150">
<g fill="none" stroke="currentColor" strokeLinecap="round">
<path d="M60 150C57 110 56 80 60 45" strokeWidth="8" />
<path d="M60 45Q38 30 10 48" strokeWidth="9" />
<path d="M60 45Q44 18 20 12" strokeWidth="9" />
<path d="M60 45Q56 14 42 4" strokeWidth="9" />
<path d="M60 45Q66 12 84 6" strokeWidth="9" />
<path d="M60 45Q78 16 100 12" strokeWidth="9" />
<path d="M60 45Q84 30 110 50" strokeWidth="9" />
</g>
</symbol>
</svg>
<svg className="vw-palm vw-palm--left-main" aria-hidden>
<use href="#vw-palm" />
</svg>
<svg className="vw-palm vw-palm--left-small" aria-hidden>
<use href="#vw-palm" />
</svg>
<svg className="vw-palm vw-palm--right-main" aria-hidden>
<use href="#vw-palm" />
</svg>
<svg className="vw-palm vw-palm--right-small" aria-hidden>
<use href="#vw-palm" />
</svg>
<ThemeProvider defaultTheme="system">
<TooltipProvider delay={200}>
{children}

View File

@ -97,6 +97,7 @@ export function CategoryLane({ category }: { category: CategoryDTO }) {
<>
<div
ref={setNodeRef}
data-category-lane=""
style={{ transform: CSS.Transform.toString(transform), transition }}
className={cn(
"flex h-full w-72 shrink-0 flex-col overflow-hidden rounded-2xl border bg-lane",