extract the visual layer into its own workspace
The application no longer knows what it looks like. It imports one stylesheet and
mounts one provider; every colour, surface and radius lives in `src/workspaces/theme`.
Proven both directions, because either alone would be worthless:
Duck renders IDENTICALLY to before — checked against screenshots taken before
anything was touched, not asserted.
Plain renders as white with black ink, no component edits — panels turn white, the
teal avatar turns black, the grid fills vanish.
── What moved ──
`globals.css` held three unrelated things: Tailwind wiring, the token contract, and
the Duck brand's hex values. Split along those seams. `base.css` is structure and
names no colour; a theme file is values and nothing else, scoped to
`[data-theme='<id>']`. Adding a theme is a CSS file plus one registry line.
Light and dark are separate themes rather than a mode — the owner's call, and it
removes a concept from the app. Each is written out in full: a theme that only makes
sense as a diff against another cannot be authored by a stranger or sold to a
customer. `.dark` survives only as a per-theme flag, because 396 `dark:` utilities in
33 components resolve against it; retiring those retires the flag.
The `duck-*` utility names are kept for now and pointed at neutral variables, so a
non-Duck theme can define them sensibly. Renaming 1,470 occurrences in the same commit
as the extraction would make any visual difference impossible to attribute. Worth
recording that `duck-dark` was never a brand colour — `#14532d` in light, near-white
in dark, used by 946 components to mean "text". It is `--ink` now.
Surfaces are complete CSS values rather than HSL triplets: `--panel-bg` has to be able
to be a gradient or an image, or the glass the app already has would be unthemeable.
The old provider is a re-export, so its six call sites are untouched and the
extraction stays one variable.
── Bug found by the Plain theme ──
`useThemeSync` stamped the server's `appearance.colorMode` over the local theme on
every load, so choosing Plain and reloading silently reverted it. Diagnosed by asking
the page what it was wearing rather than reasoning: `stored` said `duck-dark` when the
script had written `plain`. The server value is now a starting point for a browser
that has never chosen, not a continuous instruction.
── Tools ──
Two, both of which lied to me before they worked. The snapshot script photographed
light twice and called it a clean run (one browser context, accumulated init scripts);
it now warns when a light/dark pair is identical. The preview server returned the
shell for missing assets, so the logo and wallpaper vanished and read as regressions
in the comparison — it falls through to upstream now.
Dead Tailwind v3 residue deleted: `components/ui/tailwind.{config.ts,css}`, referenced
by nothing, with content globs pointing at a directory that does not exist.
── Not done, deliberately ──
`THEMES-NOTES.md` has the full list. The headlines: the reference repo has not been
read, so the switching mechanism is deliberately the dumbest thing that works; the
wallpaper, panel radius and logo are still component-owned and Plain proves it; and
~985 raw Tailwind palette usages remain, worst in Soulseek.
tsgo clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
THE STRUCTURE OF THE LOOK — never its values.
|
||||
============================================================================================
|
||||
|
||||
This file says WHAT can be themed. A theme file says what it looks like. Nothing here names a colour,
|
||||
and nothing here belongs to any particular theme: swapping every theme in the repository must not
|
||||
require editing this file, and adding a variable here is a change to the contract that every theme has
|
||||
to answer.
|
||||
|
||||
The split exists because it is the whole feature. Before it, `globals.css` mixed three things — Tailwind
|
||||
wiring, the token contract, and the Duck brand's hex values — so "change how the app looks" and "change
|
||||
how theming works" were the same edit, and neither could be done by anyone who did not own the app.
|
||||
|
||||
── How a value reaches a component ──
|
||||
|
||||
component .bg-surface
|
||||
@theme (here) --color-surface: hsl(var(--surface))
|
||||
theme file --surface: 0 0% 100%
|
||||
|
||||
The indirection through `var()` is load-bearing: Tailwind resolves `@theme` at BUILD time, so a theme
|
||||
that set `--color-surface` directly could never be swapped without a rebuild. Resolving to a custom
|
||||
property means the browser reads it at paint, and changing `data-theme` on <html> re-paints the app.
|
||||
|
||||
── Two spellings, and why ──
|
||||
|
||||
Colours are HSL TRIPLETS without the function (`0 0% 100%`), so `hsl(var(--x) / 0.5)` gives opacity for
|
||||
free — that is what makes `bg-surface/50` work, and it is why every shadcn-derived token in this file
|
||||
looks like that.
|
||||
|
||||
Surfaces (panel, dock, card, page) are COMPLETE CSS VALUES instead, because a theme needs to set things
|
||||
a triplet cannot express: `rgba()`, a gradient, an image, `none`. A panel background that could only ever
|
||||
be a flat colour would rule out the glass look the app has today.
|
||||
*/
|
||||
|
||||
@import 'tailwindcss';
|
||||
@plugin 'tailwindcss-animate';
|
||||
|
||||
/*
|
||||
Dark is still a CLASS, even though light and dark are now separate themes.
|
||||
|
||||
Two themes was the simplification the owner chose, and it is the right one — but 396 `dark:` utilities
|
||||
are written into 33 components, and they all resolve against this variant. A theme that wants them
|
||||
active declares `applyDarkClass` in its manifest and the provider adds the class; a theme that does not
|
||||
simply never sees them. So the concept is gone from the app while the utilities keep working, and
|
||||
removing those 396 becomes a later cleanup rather than a prerequisite.
|
||||
*/
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
@media (width >= --theme(--breakpoint-sm)) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 1400px) {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* ── Semantic colour, the whole vocabulary a component is allowed to use ──────────────────────── */
|
||||
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
--color-brand: hsl(var(--brand));
|
||||
--color-brand-foreground: hsl(var(--brand-foreground));
|
||||
--color-brand-muted: hsl(var(--brand-muted));
|
||||
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
--color-success: hsl(var(--success));
|
||||
--color-success-foreground: hsl(var(--success-foreground));
|
||||
|
||||
--color-warning: hsl(var(--warning));
|
||||
--color-warning-foreground: hsl(var(--warning-foreground));
|
||||
|
||||
--color-info: hsl(var(--info));
|
||||
--color-info-foreground: hsl(var(--info-foreground));
|
||||
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
--color-cta: hsl(var(--cta));
|
||||
--color-cta-foreground: hsl(var(--cta-foreground));
|
||||
--color-ctahover: hsl(var(--ctahover));
|
||||
|
||||
--color-naturegreen: hsl(var(--naturegreen));
|
||||
--color-naturegreen-foreground: hsl(var(--naturegreen-foreground));
|
||||
|
||||
/*
|
||||
── The brand slots ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
These are the six utilities that were `duck-*`, kept under their old names for now so ~1,470 existing
|
||||
usages keep compiling, but pointed at variables a theme owns rather than at brand hex.
|
||||
|
||||
They are DEPRECATED as spellings. `duck-dark` is the clearest case of why: it is `#14532d` in light and
|
||||
near-white in dark, so it was never a brand green — it is ink, and 946 components are using it to mean
|
||||
"text colour". A theme called anything other than Duck cannot sensibly define `--duck-dark`.
|
||||
|
||||
The rename to semantic names is the largest mechanical piece of this refactor and is deliberately not
|
||||
done in the same change as the extraction: one of them can be verified by screenshot, and doing both at
|
||||
once would make a visual difference impossible to attribute.
|
||||
*/
|
||||
--color-duck-yellow: var(--brand-accent);
|
||||
--color-duck-orange: var(--brand-warm);
|
||||
--color-duck-teal: var(--brand-primary);
|
||||
--color-duck-forest: var(--brand-deep);
|
||||
--color-duck-dark: var(--ink);
|
||||
--color-duck-beige: var(--surface-warm);
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out forwards;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out forwards;
|
||||
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Tailwind v4 changed the default border colour to `currentcolor`. Kept so an element with a border and no
|
||||
explicit colour looks as it did under v3 — removing it means auditing every bordered element in the app.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
/*
|
||||
The page itself is a themed surface, not just a colour.
|
||||
|
||||
`--page-bg` is a complete `background` value so a theme can be a photograph, a gradient, or nothing at
|
||||
all. Duck is a wallpaper; Plain is white. Both are the same line here.
|
||||
*/
|
||||
body {
|
||||
@apply text-foreground;
|
||||
background: var(--page-bg);
|
||||
background-attachment: fixed;
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
App furniture: the rules that are neither Tailwind nor a component, expressed in theme variables.
|
||||
|
||||
These came out of `globals.css` with hardcoded greys baked in — `#444` scrollbars, `rgb(128 128 128 / .3)`
|
||||
thumbs. Small, but exactly the kind of thing that makes a light theme look broken in the code editor and
|
||||
that a theme author has no way to reach. Now every value here is a variable a theme owns.
|
||||
*/
|
||||
|
||||
/* Sonner renders at the document root, above everything the app draws. */
|
||||
[data-sonner-toaster] {
|
||||
z-index: 700 !important;
|
||||
}
|
||||
|
||||
/* Code editor — thin scrollbars. Dark by default under Duck, because the editor surface is dark in both
|
||||
Duck themes; a theme with a light editor sets these to suit. */
|
||||
.code-editor-scrollable {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) transparent;
|
||||
}
|
||||
|
||||
.code-editor-scrollable::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.code-editor-scrollable::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.code-editor-scrollable::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.code-editor-scrollable::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* Chat — the background-task strip. A default horizontal scrollbar is ~15px tall and is laid out INSIDE
|
||||
the row, so it stole most of the chips' breathing room and made the tray read as squeezed. */
|
||||
.task-strip {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thin-thumb) transparent;
|
||||
}
|
||||
|
||||
.task-strip::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.task-strip::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.task-strip::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thin-thumb);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.task-strip:hover::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thin-thumb-hover);
|
||||
}
|
||||
|
||||
/* File browser — while a context menu is open, the rest of the grid stops responding. Behaviour, not
|
||||
appearance, but it lives with the other furniture rather than in a component's stylesheet. */
|
||||
.file-grid:has([data-state='open']) [data-file-item]:not([data-state='open']) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-grid:has([data-state='open']) [data-file-item]:not([data-state='open']) .file-item-ellipsis {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.file-grid [data-file-item][data-state='open'] .file-item-ellipsis {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
The stylesheet an application imports. One line in the app; everything else is in here.
|
||||
|
||||
Order matters and is the only reason this file has content:
|
||||
|
||||
1. `base.css` — Tailwind, the `@theme` contract, element defaults. Structure, no values.
|
||||
2. themes — values, each scoped to its own `[data-theme='…']`, so they cannot collide.
|
||||
3. `chrome.css` — app furniture (scrollbars, overlays) expressed in theme variables.
|
||||
|
||||
A theme file added below is a theme the app can wear. Nothing else has to change — which is the point of
|
||||
the whole exercise, and the thing to protect when this grows to six or seven of them.
|
||||
*/
|
||||
|
||||
@import './base.css';
|
||||
|
||||
@import './themes/duck-light.css';
|
||||
@import './themes/duck-dark.css';
|
||||
@import './themes/plain.css';
|
||||
|
||||
@import './chrome.css';
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
DUCK — DARK
|
||||
============================================================================================
|
||||
|
||||
A sibling of Duck Light, not a variant of it. The owner's decision: light and dark are two themes, which
|
||||
removes "colour mode" from the application as a concept.
|
||||
|
||||
It is written out in full rather than as a diff against light. Overriding would be shorter and would
|
||||
reintroduce exactly the coupling this refactor removes — a theme that only makes sense in the presence of
|
||||
another is not something a third party can author or a customer can buy. A complete file is also the only
|
||||
way an author can see what they have to supply.
|
||||
|
||||
`applyDarkClass` is set for this theme in the registry, so the 396 `dark:` utilities still written into
|
||||
components keep resolving. That is a compatibility shim with a known end date, not part of the design.
|
||||
|
||||
Values are the `.dark` block of the original `globals.css`, unchanged. Where light defined something that
|
||||
the original dark block did not override (`--duck-yellow`, `--duck-orange`), the light value is repeated
|
||||
here — that is what the browser was computing before, made explicit.
|
||||
*/
|
||||
|
||||
[data-theme='duck-dark'] {
|
||||
/* ── Brand — lightened for contrast on a dark ground ──────────────────────────────────────────
|
||||
`--ink` flipping to near-white is the clearest evidence that `duck-dark` was never a brand colour. */
|
||||
--brand-primary: #22d3ee;
|
||||
--brand-accent: #f4c430;
|
||||
--brand-warm: #ea580c;
|
||||
--brand-deep: #4ade80;
|
||||
--ink: #f1f5f9;
|
||||
--surface-warm: #292524;
|
||||
|
||||
/* ── The page ─────────────────────────────────────────────────────────────────────────────────── */
|
||||
--page-bg: hsl(222.2 84% 4.9%);
|
||||
--pixel-grid-color: rgba(200, 230, 210, 0.08);
|
||||
|
||||
/* ── Surfaces ─────────────────────────────────────────────────────────────────────────────────── */
|
||||
--glass-bg: rgba(0, 0, 0, 0.2);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--panel-bg: rgba(0, 0, 0, 0.2);
|
||||
--panel-border: rgba(255, 255, 255, 0.1);
|
||||
--panel-radius: 0.75rem;
|
||||
--panel-header-bg: rgba(0, 0, 0, 0.15);
|
||||
--panel-header-fg: hsl(var(--foreground));
|
||||
--panel-accent: var(--brand-primary);
|
||||
--panel-grid-color: rgba(200, 230, 210, 0.06);
|
||||
|
||||
--dock-bg: rgba(0, 0, 0, 0.3);
|
||||
--dock-border: rgba(255, 255, 255, 0.08);
|
||||
--dock-tooltip-bg: rgba(0, 0, 0, 0.85);
|
||||
|
||||
--card-bg: rgba(10, 20, 15, 0.9);
|
||||
--card-grid-color: rgba(200, 230, 210, 0.06);
|
||||
|
||||
/* ── Semantic colour ──────────────────────────────────────────────────────────────────────────── */
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 211 82% 64%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--success: 142.1 76.2% 36.3%;
|
||||
--success-foreground: 355.7 100% 97.3%;
|
||||
|
||||
--brand: 145 60% 45%;
|
||||
--brand-foreground: 0 0% 100%;
|
||||
--brand-muted: 145 30% 25%;
|
||||
|
||||
--warning: 43 96% 56%;
|
||||
--warning-foreground: 0 0% 0%;
|
||||
|
||||
/* Lifted well above the light value — 50% lightness violet on a near-black background reads as muddy
|
||||
grey at 12px, which is exactly the size it is used at. */
|
||||
--info: 263 90% 70%;
|
||||
--info-foreground: 0 0% 100%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
|
||||
/* The original dark block never overrode these, so the light values were in force. Written out because
|
||||
a theme that silently inherits from another is the thing this file exists to stop. */
|
||||
--cta: 211 74.77% 45.58%;
|
||||
--cta-foreground: 0 0% 100%;
|
||||
--ctahover: 211 91.53% 34.82%;
|
||||
|
||||
--naturegreen: 145 79% 38%;
|
||||
--naturegreen-foreground: 0 0% 100%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* ── Data visualisation and status ────────────────────────────────────────────────────────────── */
|
||||
--chart-primary: 211 82% 64%;
|
||||
--chart-secondary: 211 82% 84%;
|
||||
--status-success: 142 76% 36%;
|
||||
--status-warning: 43 96% 56%;
|
||||
--status-in-progress: 211 82% 64%;
|
||||
|
||||
/* ── Chrome ───────────────────────────────────────────────────────────────────────────────────── */
|
||||
--scrollbar-thumb: #444;
|
||||
--scrollbar-thumb-hover: #555;
|
||||
--scrollbar-thin-thumb: rgb(128 128 128 / 0.3);
|
||||
--scrollbar-thin-thumb-hover: rgb(128 128 128 / 0.5);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
DUCK — LIGHT
|
||||
============================================================================================
|
||||
|
||||
The application's original appearance, moved here unchanged. Every value in this file was lifted from
|
||||
`apps/officer-web/styles/globals.css`; none of it was redesigned. That is deliberate — the extraction has
|
||||
to be provably invisible, and a "small improvement" smuggled in alongside it makes the screenshot
|
||||
comparison meaningless.
|
||||
|
||||
This file is the reference implementation of the theme contract. An author writing a new theme can copy
|
||||
it, replace values, and change nothing else.
|
||||
|
||||
Everything is scoped to `[data-theme='duck-light']`, so themes cannot leak into one another and the
|
||||
active one is legible in the DOM.
|
||||
*/
|
||||
|
||||
[data-theme='duck-light'] {
|
||||
/* ── Brand ────────────────────────────────────────────────────────────────────────────────────
|
||||
The six slots the `duck-*` utilities resolve through. A theme names its own colours here; the app
|
||||
never sees these directly.
|
||||
|
||||
`--ink` is the interesting one: components use it as their text colour ~950 times, which is why it
|
||||
must be a near-black here and a near-white in the dark theme. It was called `duck-dark`, which is
|
||||
what hid that fact for so long. */
|
||||
--brand-primary: #0891b2;
|
||||
--brand-accent: #f4c430;
|
||||
--brand-warm: #ea580c;
|
||||
--brand-deep: #166534;
|
||||
--ink: #14532d;
|
||||
--surface-warm: #e7d4b5;
|
||||
|
||||
/* ── The page ─────────────────────────────────────────────────────────────────────────────────
|
||||
A complete `background` value, so a theme can be an image or a gradient rather than a colour. */
|
||||
--page-bg: hsl(0 0% 100%);
|
||||
--pixel-grid-color: rgba(20, 83, 45, 0.1);
|
||||
|
||||
/* ── Surfaces ─────────────────────────────────────────────────────────────────────────────────
|
||||
Panels are the cornerstone of the layout, so they are themed as first-class surfaces rather than
|
||||
inheriting whatever `card` happens to be. These were `bg-black/10` and `duck-teal` literals written
|
||||
into the panel chrome, which is why panels could not be restyled without editing components. */
|
||||
--glass-bg: rgba(255, 255, 255, 0.1);
|
||||
--glass-border: rgba(255, 255, 255, 0.2);
|
||||
|
||||
--panel-bg: rgba(255, 255, 255, 0.1);
|
||||
--panel-border: rgba(255, 255, 255, 0.2);
|
||||
--panel-radius: 0.75rem;
|
||||
--panel-header-bg: rgba(0, 0, 0, 0.05);
|
||||
--panel-header-fg: hsl(var(--foreground));
|
||||
--panel-accent: var(--brand-primary);
|
||||
--panel-grid-color: rgba(20, 83, 45, 0.08);
|
||||
|
||||
--dock-bg: rgba(0, 0, 0, 0.15);
|
||||
--dock-border: rgba(255, 255, 255, 0.1);
|
||||
--dock-tooltip-bg: rgba(0, 0, 0, 0.75);
|
||||
|
||||
--card-bg: rgba(237, 231, 222, 0.9);
|
||||
--card-grid-color: rgba(20, 83, 45, 0.08);
|
||||
|
||||
/* ── Semantic colour (HSL triplets — see base.css for why they are unwrapped) ─────────────────── */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 211 74.77% 45.58%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--success: 142.1 76.2% 36.3%;
|
||||
--success-foreground: 355.7 100% 97.3%;
|
||||
|
||||
--brand: 145 79% 38%;
|
||||
--brand-foreground: 0 0% 100%;
|
||||
--brand-muted: 145 40% 90%;
|
||||
|
||||
--warning: 43 96% 56%;
|
||||
--warning-foreground: 0 0% 0%;
|
||||
|
||||
/* The fifth state tone. "In progress" and "merged" are neither good news nor bad, and painting them
|
||||
with --success makes a merged PR and an open one look like the same thing. Violet because it has to
|
||||
stay legible next to success green and destructive red at pill size. */
|
||||
--info: 263 70% 50%;
|
||||
--info-foreground: 0 0% 100%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--cta: 211 74.77% 45.58%;
|
||||
--cta-foreground: 0 0% 100%;
|
||||
--ctahover: 211 91.53% 34.82%;
|
||||
|
||||
--naturegreen: 145 79% 38%;
|
||||
--naturegreen-foreground: 0 0% 100%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* ── Data visualisation and status ────────────────────────────────────────────────────────────── */
|
||||
--chart-primary: 211 82% 64%;
|
||||
--chart-secondary: 211 82% 84%;
|
||||
--status-success: 142 76% 36%;
|
||||
--status-warning: 43 96% 56%;
|
||||
--status-in-progress: 211 82% 64%;
|
||||
|
||||
/* ── Chrome that used hardcoded greys ─────────────────────────────────────────────────────────
|
||||
Scrollbars were `#444`/`#555`/`rgb(128 128 128 / .3)` in the stylesheet, which meant the code editor
|
||||
kept dark scrollbars under a light theme. Now a theme's problem. */
|
||||
--scrollbar-thumb: #444;
|
||||
--scrollbar-thumb-hover: #555;
|
||||
--scrollbar-thin-thumb: rgb(128 128 128 / 0.3);
|
||||
--scrollbar-thin-thumb-hover: rgb(128 128 128 / 0.5);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
PLAIN — white ground, black ink, visible borders
|
||||
============================================================================================
|
||||
|
||||
Not a design. An instrument.
|
||||
|
||||
Every value here is white, black, or a grey, and every surface is flat and square. So when the app is
|
||||
rendered under this theme, ANY colour still on the screen is a component painting itself instead of
|
||||
asking the theme — a hardcoded `text-emerald-400`, a `bg-black/10`, a hex literal. The leaks light up.
|
||||
|
||||
That makes it the acceptance test for the whole decoupling effort, and a much better one than reading
|
||||
code: the survey found ~985 raw Tailwind palette usages and ~119 hex literals, and no amount of grepping
|
||||
tells you which of those are load-bearing. A screenshot does.
|
||||
|
||||
It is also the honest starting point for a third-party author — copy this, and every value you change is
|
||||
one you have decided about, with nothing inherited from someone else's brand.
|
||||
|
||||
Deliberately NOT beautiful. A pleasant neutral theme would hide small leaks in its own greys; stark black
|
||||
on stark white hides nothing.
|
||||
*/
|
||||
|
||||
[data-theme='plain'] {
|
||||
/* ── Brand slots ──────────────────────────────────────────────────────────────────────────────
|
||||
All black. The `duck-*` utilities resolve through these, so anything still using them renders as flat
|
||||
black rather than teal — visible as "this should have been a semantic token" without being colourful
|
||||
enough to be mistaken for intent. */
|
||||
--brand-primary: #000000;
|
||||
--brand-accent: #000000;
|
||||
--brand-warm: #000000;
|
||||
--brand-deep: #000000;
|
||||
--ink: #000000;
|
||||
--surface-warm: #ffffff;
|
||||
|
||||
/* ── The page ─────────────────────────────────────────────────────────────────────────────────
|
||||
Flat white and no grid. If a pixel grid or a wallpaper appears under this theme, something is drawing
|
||||
it from outside the theme layer. */
|
||||
--page-bg: #ffffff;
|
||||
--pixel-grid-color: transparent;
|
||||
|
||||
/* ── Surfaces ─────────────────────────────────────────────────────────────────────────────────
|
||||
Opaque white with a hard black border, and square corners. The app's panels are translucent with
|
||||
rounded corners and a grid fill, so any panel that still looks like that under Plain is styling itself
|
||||
rather than reading `--panel-*`. */
|
||||
--glass-bg: #ffffff;
|
||||
--glass-border: #000000;
|
||||
|
||||
--panel-bg: #ffffff;
|
||||
--panel-border: #000000;
|
||||
--panel-radius: 0;
|
||||
--panel-header-bg: #f2f2f2;
|
||||
--panel-header-fg: #000000;
|
||||
--panel-accent: #000000;
|
||||
--panel-grid-color: transparent;
|
||||
|
||||
--dock-bg: #ffffff;
|
||||
--dock-border: #000000;
|
||||
--dock-tooltip-bg: #000000;
|
||||
|
||||
--card-bg: #ffffff;
|
||||
--card-grid-color: transparent;
|
||||
|
||||
/* ── Semantic colour ──────────────────────────────────────────────────────────────────────────
|
||||
Greys only. The state tones (success/warning/destructive/info) are greys too, which is wrong for a
|
||||
real theme and right for this one: it proves the app is asking for a token rather than reaching for a
|
||||
green it likes. A theme that wanted real state colour would simply set them. */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 0%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 0%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 0%;
|
||||
|
||||
--primary: 0 0% 0%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 0 0% 95%;
|
||||
--secondary-foreground: 0 0% 0%;
|
||||
|
||||
--muted: 0 0% 95%;
|
||||
--muted-foreground: 0 0% 35%;
|
||||
|
||||
--accent: 0 0% 90%;
|
||||
--accent-foreground: 0 0% 0%;
|
||||
|
||||
--destructive: 0 0% 25%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--success: 0 0% 25%;
|
||||
--success-foreground: 0 0% 100%;
|
||||
|
||||
--brand: 0 0% 0%;
|
||||
--brand-foreground: 0 0% 100%;
|
||||
--brand-muted: 0 0% 90%;
|
||||
|
||||
--warning: 0 0% 45%;
|
||||
--warning-foreground: 0 0% 100%;
|
||||
|
||||
--info: 0 0% 35%;
|
||||
--info-foreground: 0 0% 100%;
|
||||
|
||||
--border: 0 0% 0%;
|
||||
--input: 0 0% 0%;
|
||||
--ring: 0 0% 0%;
|
||||
|
||||
--cta: 0 0% 0%;
|
||||
--cta-foreground: 0 0% 100%;
|
||||
--ctahover: 0 0% 20%;
|
||||
|
||||
--naturegreen: 0 0% 0%;
|
||||
--naturegreen-foreground: 0 0% 100%;
|
||||
|
||||
/* Square everything. Rounded corners surviving under Plain means a hardcoded `rounded-xl`, which is a
|
||||
shape leak rather than a colour one — worth seeing too. */
|
||||
--radius: 0;
|
||||
|
||||
/* ── Data visualisation and status ────────────────────────────────────────────────────────────── */
|
||||
--chart-primary: 0 0% 20%;
|
||||
--chart-secondary: 0 0% 60%;
|
||||
--status-success: 0 0% 25%;
|
||||
--status-warning: 0 0% 45%;
|
||||
--status-in-progress: 0 0% 35%;
|
||||
|
||||
/* ── Chrome ───────────────────────────────────────────────────────────────────────────────────── */
|
||||
--scrollbar-thumb: #000000;
|
||||
--scrollbar-thumb-hover: #333333;
|
||||
--scrollbar-thin-thumb: rgb(0 0 0 / 0.4);
|
||||
--scrollbar-thin-thumb-hover: rgb(0 0 0 / 0.7);
|
||||
}
|
||||
Reference in New Issue
Block a user