Files
platform/src/workspaces/theme/css/themes/plain.css
T
pastilhasandClaude Opus 5 e9cb244818 the wallpaper, the header and the mark belong to the theme
Asked whether these could follow the theme too. They are the three most visible things
on screen and were the three least reachable: `Background.tsx` hardcoded
`url('/landscape1.webp')`, the header carried `rgba(255,255,255,0.25)` and `#1d2724`
in an inline style, and the logo was an `<img src>`.

Between them that is most of what "the app looks like Officer" actually meant. The
Plain theme turned every panel white and all three stayed exactly as they were, which
is how they were found — the leak detector doing its job on its first outing.

Verified across all three themes:

  duck-dark   wallpaper=landscape1.webp  header=rgba(255,255,255,0.25)
  duck-light  wallpaper=landscape1.webp  header=rgba(255,255,255,0.25)
  plain       wallpaper=none             header=rgb(255,255,255)

── Images could not be done the obvious way ──

`--logo-image: url('/officer-logo.svg')` fails the build. Bun's CSS bundler resolves
`url()` at BUILD time and these files are served from `public/` at RUNTIME, so it goes
looking for a module that does not exist. Colours apply themselves; images cannot.

So a theme publishes a quoted PATH — an ordinary string token the bundler ignores —
and `useThemeAsset` reads it and wraps it in `url()`. One hook rather than the read
inline in two components, because the quote-stripping is the kind of detail that gets
forgotten the second time.

The logo is a `<div role="img">` with a background image rather than an `<img>`, since
CSS cannot set a `src` and a white-labelled build has to replace the mark from its
theme rather than by editing the header.

The header's title colour goes through a `.header-ink` class: `EditableTitle` accepts
a className and not a style, and the token stays the source of truth either way.

Panel radius and the ~985 raw palette usages are still outstanding — see
THEMES-NOTES.md.

tsgo clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:52:43 +00:00

157 lines
7.1 KiB
CSS

/*
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;
/* ── Legacy variable names, for direct `var()` consumers ──────────────────────────────────────
The `duck-*` UTILITIES are remapped in base.css, but a stylesheet reading `var(--duck-teal)` gets
nothing from that — it wants the variable itself. `prose.css` does exactly this eleven times for chat
markdown (links, blockquotes, inline code, checkboxes), and the rename silently emptied all of them.
Caught by diffing computed variables between the old and new builds, not by reading.
Kept as aliases rather than as values: there is one source of truth above, and these follow it. They
retire when nothing reads them — the same migration as the utility names. */
--duck-teal: var(--brand-primary);
--duck-dark: var(--ink);
--duck-yellow: var(--brand-accent);
--duck-forest: var(--brand-deep);
--duck-beige: var(--surface-warm);
--duck-orange: var(--brand-warm);
/* ── 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;
/* ── The wallpaper, the header and the mark ───────────────────────────────────────────────────
No wallpaper, no blur, a hard border. If an image appears behind the panels under this theme,
something is still drawing it from outside the theme layer. */
/* Empty means no wallpaper — see the note in the Duck themes. */
--page-bg-src: '';
--header-bg: #ffffff;
--header-border: #000000;
--header-fg: #000000;
--header-blur: 0px;
--logo-src: '/officer-logo.svg';
/* ── 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);
}