opencode discovers only AGENTS.md, Claude Code only CLAUDE.md — so with the content living in CLAUDE.md, every doc here was invisible to opencode. Swaps all five pairs (root, src/apps, src/databases, themes, plugins): AGENTS.md now holds the full text verbatim, CLAUDE.md is a short pointer. docs-check's permission-count regex now reads root AGENTS.md instead of CLAUDE.md; the workspace-level officer-dev/CLAUDE.md check is untouched, out of scope.
119 lines
4.2 KiB
Markdown
119 lines
4.2 KiB
Markdown
# Themes
|
|
|
|
Drop a theme in here and it appears in the picker on the next page load. No install step, no rebuild, no
|
|
restart — a theme is a block of CSS custom properties, and none of that needs compiling.
|
|
|
|
**This directory is untracked**, exactly like `plugins/`. Only this file is in the repository. Your themes
|
|
are yours; the platform's own live in `src/workspaces/theme/css/themes/`.
|
|
|
|
---
|
|
|
|
## The shape
|
|
|
|
```
|
|
themes/
|
|
my-theme/
|
|
theme.css required — the values
|
|
theme.json required — the name a person reads
|
|
assets/ optional — a wallpaper, a logo
|
|
```
|
|
|
|
The directory name **is** the theme's id. It is not repeated in the manifest, because an id that can
|
|
disagree with where its files sit eventually will.
|
|
|
|
### `theme.json`
|
|
|
|
```json
|
|
{
|
|
"name": "My Theme",
|
|
"family": "My Theme",
|
|
"dark": true
|
|
}
|
|
```
|
|
|
|
| | |
|
|
|---|---|
|
|
| `name` | required — what the picker shows |
|
|
| `family` | optional — groups variants together, like `Duck Light` and `Duck Dark`. Defaults to `name` |
|
|
| `dark` | optional — adds the `.dark` class. Set it for a dark theme; several hundred `dark:` utilities still resolve against it |
|
|
|
|
### `theme.css`
|
|
|
|
Must target its own directory name, or nothing will happen and it will be baffling:
|
|
|
|
```css
|
|
[data-theme='my-theme'] {
|
|
--background: #1e1e2e;
|
|
--foreground: #cdd6f4;
|
|
/* …the rest */
|
|
}
|
|
```
|
|
|
|
The server checks this on discovery and reports it rather than letting you wonder.
|
|
|
|
---
|
|
|
|
## What to put in it
|
|
|
|
**Twenty tokens are required.** They are the standard shadcn set, which is what every published palette
|
|
already ships — Tokyo Night, Catppuccin, Gruvbox, Nord all come in this vocabulary, so porting one is
|
|
mostly copy-paste.
|
|
|
|
```
|
|
background foreground · card card-foreground · popover popover-foreground
|
|
primary primary-foreground · secondary secondary-foreground
|
|
muted muted-foreground · accent accent-foreground
|
|
destructive destructive-foreground · border input ring · radius
|
|
```
|
|
|
|
**Everything else is derived from those.** Panels, the header, the dock, glass, grid lines, scrollbars —
|
|
all computed from `card`, `border`, `background` and `foreground`. A palette theme never mentions them.
|
|
|
|
**Four more are worth setting**, because the standard set has no notion of them and the defaults are
|
|
generic: `--success`, `--warning`, `--info`, `--busy`, each with a `-foreground`. `busy` means *running*,
|
|
not *warning* — it is what a live job wears.
|
|
|
|
**Anything else you want to change, you can.** Declaring a variable in your theme beats the derived
|
|
default, because both land on `<html>` and yours is more specific. That is how a theme gets a *texture*
|
|
rather than just a palette:
|
|
|
|
```css
|
|
[data-theme='my-theme'] {
|
|
--panel-bg: rgba(255, 255, 255, 0.08); /* glass instead of a flat card */
|
|
--panel-blur: 20px;
|
|
--panel-radius: 0; /* square panels */
|
|
--page-bg-src: '/themes/my-theme/assets/wallpaper.webp';
|
|
--logo-src: '/themes/my-theme/assets/logo.svg';
|
|
}
|
|
```
|
|
|
|
The full list of what you can override is in `src/workspaces/theme/README.md`, and
|
|
`src/workspaces/theme/css/themes/duck-dark.css` is a worked example of a textured theme.
|
|
|
|
**Assets are quoted paths, not `url(...)`.** The bundler resolves `url()` at build time and your files are
|
|
served at runtime, so a `url()` token would break. The app wraps the path for you.
|
|
|
|
---
|
|
|
|
## Writing one
|
|
|
|
Start from `src/workspaces/theme/css/themes/plain.css` rather than from a Duck theme. Plain is white,
|
|
black and grey throughout, so every value you change is one you decided about, with nothing inherited from
|
|
someone else's brand.
|
|
|
|
Then: reload the page, open the picker or press `ctrl+shift+space`, and use `j`/`k` to browse. `l` applies
|
|
the focused theme to the app behind the list, so you can watch your own theme against real screens.
|
|
|
|
Editing `theme.css` and reloading is the whole loop. The stylesheet is cached for a minute.
|
|
|
|
---
|
|
|
|
## Checking your work
|
|
|
|
Render the app under **Plain** first if something looks wrong under yours. Anything still coloured under
|
|
Plain is a component painting itself rather than reading a token — that is a platform bug, not yours, and
|
|
worth reporting.
|
|
|
|
`bun run check:theme` counts hardcoded colours in the platform. It does not check themes, but it is why
|
|
your theme has a chance of working: the number can only go down.
|