docs(plans): reconcile plan statuses; archive 10 done plans
Move ten completed plans from plans/ to plans/done/ and update the index:
- 2026-07-18 session-review-three-sessions, 2026-07-20 desktop-mascot,
2026-07-20 session-review-ten-sessions, 2026-07-21 chat-full-polish,
2026-07-29 health-check-reality-and-knowledge-graph,
2026-07-30 session-review-plan-drift, and the four 2026-08-03 chat plans
(changes-review, reliability-and-ux-audit, cyberspace-style-adoption,
working-visibility).
- Refresh two stale statuses: cyberspace-style-adoption ("Draft" -> shipped as
full replacement in v0.16.0/757ef2f) and health-check-reality ("ready for
implementation" -> shipped across the v0.14.x-0.16.x check commits).
- .gitignore: ignore local tooling artifacts (.playwright-mcp/, config-screen.png).
No code change. index.md Active/Done tables now match the filesystem (no orphans).
VERSION: 0.17.0 -> 0.17.1
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,3 +24,7 @@ cmd/desktop/build/
|
||||
cmd/desktop/Oikos
|
||||
desktop
|
||||
/eval
|
||||
|
||||
# Local tooling artifacts (Playwright MCP session logs, stray screenshots)
|
||||
.playwright-mcp/
|
||||
config-screen.png
|
||||
|
||||
503
plans/done/2026-08-03-cyberspace-style-adoption.md
Normal file
503
plans/done/2026-08-03-cyberspace-style-adoption.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# 2026-08-03 — Adopt cyberspace.online terminal aesthetic + dithered images
|
||||
|
||||
**Status:** Implemented in v0.16.0 (`757ef2f`). Shipped as a **full theme
|
||||
replacement** (Terracotta/Carbon → cyberspace BBS/terminal style), not the
|
||||
opt-in addition originally drafted below — the operator chose full replacement
|
||||
during execution (see decision `theme.replace_with_cyberspace`). The `<RasterImage>`
|
||||
Atkinson-dithering component and the warm-cream/JetBrains-Mono look landed as
|
||||
drafted; only the "opt-in vs replace" scope changed.
|
||||
|
||||
Adopt the look of https://cyberspace.online/ (a BBS / "social media
|
||||
de-imagined" terminal aesthetic) as a **new, opt-in theme family** in oikos,
|
||||
with **both light and dark variants**, plus a reusable **`<RasterImage>`**
|
||||
component that renders images to a `<canvas>` with Atkinson dithering (the
|
||||
"kinda dithered" image style). The existing Terracotta/Carbon themes stay the
|
||||
default; this adds, it does not replace.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
1. Add a third theme family — **"Cyberspace Dark"** and **"Cyberspace Light"** —
|
||||
wired through the same `--background` / `--foreground` / … token layer every
|
||||
component already uses, so nothing in the UI tree changes; only the tokens
|
||||
get new values. Square corners (`--radius: 0`), warm cream-on-black, mono
|
||||
everything.
|
||||
2. Extend `web/src/lib/stores/theme.svelte.ts` from a 2-state `'light'|'dark'`
|
||||
toggle to a named-theme model, keeping `.dark` class behavior for
|
||||
compatibility.
|
||||
3. Self-host JetBrains Mono (body) + a pixel/terminal face (VT323 or Departure
|
||||
Mono) for the logo/headings accents, replacing the Google Fonts `<link>`.
|
||||
4. Build `web/src/lib/components/RasterImage.svelte`: draws any image to a
|
||||
`<canvas>` reduced to a 2-color (theme `fg`/`bg`) palette via **Atkinson
|
||||
dithering**, with an `<img>` fallback and a skeleton placeholder — exactly
|
||||
the cyberspace pattern. Re-renders when the theme changes (palette flips).
|
||||
5. Optional cosmetic idioms (terminal-box focus ring, braille spinner, `<s>`
|
||||
strike lists) as small additive utilities, not a redesign.
|
||||
|
||||
The whole thing is **non-breaking and incremental**: each step ships behind the
|
||||
existing theme picker, so Terracotta/Carbon users see nothing until they opt in.
|
||||
|
||||
---
|
||||
|
||||
## 1. Extracted style spec (source of truth from cyberspace.online)
|
||||
|
||||
Captured from the live site's SSR HTML + inline boot script. This is the
|
||||
reference the tokens below are derived from.
|
||||
|
||||
### 1.1 Color model
|
||||
|
||||
Cyberspace defines **exactly three colors per theme** — `fg`, `bg`, `fgDim` —
|
||||
applied to CSS custom properties. Everything else (borders, primary, cards) is
|
||||
*derived* from those three. There are 11 named themes total; the two we care
|
||||
about:
|
||||
|
||||
| Theme | `fg` (text) | `bg` (canvas) | `fgDim` (muted) |
|
||||
|---------|--------------|---------------|-----------------|
|
||||
| Dark | `#efe5c0` | `#000000` | `#a89984` |
|
||||
| Light | `#000000` | `#efe5c0` | `#3a3a3a` |
|
||||
|
||||
Note the elegance: **light and dark are exact inverses** — they share the same
|
||||
warm cream (`#efe5c0`, a Gruvbox-ish paper tone) and just swap which side of it
|
||||
is ink vs. paper. The muted tone `#a89984` is straight out of the Gruvbox
|
||||
palette. This is why both themes read as "the same site" despite opposite
|
||||
polarity.
|
||||
|
||||
Boot-time fallback (the site's original/GRiD theme) is amber `#FF9810` on
|
||||
`#120900` — useful as a *third* optional accent if we ever want a true-phosphor
|
||||
variant.
|
||||
|
||||
### 1.2 Type
|
||||
|
||||
- **Body / mono:** JetBrains Mono (self-hosted `.woff2`, Regular).
|
||||
- **Boot + logo accents:** Departure Mono (self-hosted `.woff2`). A quirky
|
||||
monospace; VT323 (Google, free) is a close, easy substitute.
|
||||
- **Stylized wordmark** (`ᑕ¥βєяรקค¢є`, class `.font-vt`): a terminal/pixel face.
|
||||
Rule lives in their external `entry.*.css` (not in the SSR dump); VT323 is the
|
||||
safe assumption.
|
||||
|
||||
cyberspace sets `font-mono` on the root wrapper — the **entire UI is
|
||||
monospace**. There is no proportional body face. Headings use the same mono
|
||||
family at larger size / normal weight.
|
||||
|
||||
### 1.3 Layout & component idioms
|
||||
|
||||
- **Left rail nav:** fixed, icon-only when minimized (~80px), expands on click.
|
||||
Square buttons, Phosphor icons, uppercase `text-xs` labels.
|
||||
- **`.terminal-box`:** the universal card. Bordered (`border border-border`),
|
||||
**square corners** (`rounded-none` everywhere — `--radius` is effectively 0),
|
||||
and on focus/emphasis gets `ring-2 ring-fg` (a 2px ring in the foreground
|
||||
color).
|
||||
- **Emphasis by inversion:** active/primary state is `bg-fg text-bg` — fill with
|
||||
foreground ink, text becomes the canvas color. No separate "accent" hue; the
|
||||
accent *is* fg.
|
||||
- **Strikethrough as a feature list:** `<s>Ads</s> <s>Videos</s> …` — crossed-out
|
||||
`<s>` elements spell out what the product removes. Cheap, on-brand.
|
||||
- **Braille spinner:** `BrailleSpinner` component animates braille block chars
|
||||
(`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`) for loading states instead of a circle.
|
||||
- **Max content width** `max-w-4xl`, centered; generous vertical rhythm; thin
|
||||
2px scrollbars colored `--color-border`.
|
||||
- Borders are `1px solid` in a border color derived from `fg`/`fgDim` at low
|
||||
alpha (their `--color-border` is not literally in the dump, but every
|
||||
bordered surface uses it, and it tracks `fg`).
|
||||
|
||||
### 1.4 The dithered image (`RasterImage`) — what we actually know
|
||||
|
||||
From the SSR HTML the component is unambiguous about its *shape*, silent on its
|
||||
*algorithm* (the dither JS is in an external `/_nuxt/*.js` bundle not present in
|
||||
the page dump):
|
||||
|
||||
- Renders a **`<canvas>`** as primary output, with an **`<img>` fallback** as a
|
||||
sibling. Parent selectors `[&>canvas]:max-w-full [&>canvas]:h-auto` and
|
||||
`[&>img]:…` size both responsively.
|
||||
- Emits a **`.raster-image-skeleton`** placeholder (empty div, `background:
|
||||
var(--color-bg)`) during SSR/before hydration — no flash of the raw photo.
|
||||
- Scoped styles (`data-v-4d61df89`): `.raster-image { display:block }`,
|
||||
`.raster-image-skeleton { display:block; background:var(--color-bg) }`.
|
||||
|
||||
**Inferred technique** (standard for this look): Canvas 2D → `drawImage` →
|
||||
`getImageData` → per-pixel luminance reduction to a 2-color palette (`fg`/`bg`)
|
||||
with an **error-diffusion** pass (Atkinson or Floyd–Steinberg) → `putImageData`.
|
||||
This produces the characteristic speckled 1-bit halftone. Target is almost
|
||||
certainly the theme's own fg/bg, which is *why* the dithered art recolors
|
||||
correctly when you flip themes.
|
||||
|
||||
We will implement Atkinson (see §4) — it's the classic Mac/BBS dither, slightly
|
||||
softer than Floyd–Steinberg, and matches "kinda dithered" precisely.
|
||||
|
||||
---
|
||||
|
||||
## 2. Recommended approach: opt-in theme family (not a rebrand)
|
||||
|
||||
oikos today = Art-Nouveau / terracotta / rounded / serif-heading (Inknut
|
||||
Antiqua), floating-window desktop shell. cyberspace = BBS / mono / square /
|
||||
cream-on-black. These are **opposite poles**; a flat rebrand would discard the
|
||||
existing art direction and rework every component's rounding/spacing.
|
||||
|
||||
**Decision: add cyberspace as a new theme family, selectable in the existing
|
||||
theme picker.** This is low-risk, reversible, and lets the dithered images +
|
||||
terminal idioms land incrementally. The full-rebrand alternative is documented
|
||||
in §7 for if you later decide to make it the default.
|
||||
|
||||
Because every oikos component consumes colors through the Tailwind v4 token
|
||||
layer (`--background`, `--foreground`, `--card`, `--border`, `--primary`, …)
|
||||
defined in `web/src/app.css` `@theme inline`, a new theme is **just a new set
|
||||
of values for those same custom properties** — zero component edits required
|
||||
for the recolor. That indirection is the whole reason this is cheap.
|
||||
|
||||
---
|
||||
|
||||
## 3. Theme token additions (`web/src/app.css`)
|
||||
|
||||
Add two new blocks alongside the existing `:root` (Terracotta) and `.dark`
|
||||
(Carbon). They set the *same* token names to cyberspace's values, plus pin
|
||||
`--radius: 0` for square corners and remap fonts (see §5).
|
||||
|
||||
Driven by a `data-theme` attribute on `<html>` (set by the store, §6), so all
|
||||
four states — Terracotta, Carbon, Cyberspace Dark, Cyberspace Light — coexist:
|
||||
|
||||
```css
|
||||
/* ── Cyberspace Dark (cream on black) ── */
|
||||
:root[data-theme='cyber-dark'] {
|
||||
--radius: 0px;
|
||||
--background: #000000;
|
||||
--foreground: #efe5c0;
|
||||
--card: #000000; /* cyberspace has no card tint; cards are just bordered bg */
|
||||
--card-foreground: #efe5c0;
|
||||
--popover: #000000;
|
||||
--popover-foreground: #efe5c0;
|
||||
--primary: #efe5c0; /* emphasis = fg ink */
|
||||
--primary-foreground: #000000; /* inverted */
|
||||
--secondary: #1a1a1a;
|
||||
--secondary-foreground: #efe5c0;
|
||||
--muted: #141414;
|
||||
--muted-foreground: #a89984; /* fgDim */
|
||||
--accent: #efe5c0;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #cc241d; /* Gruvbox red, sits in the same palette */
|
||||
--destructive-foreground: #efe5c0;
|
||||
--border: color-mix(in oklab, #efe5c0 22%, transparent); /* fg-derived hairline */
|
||||
--input: color-mix(in oklab, #efe5c0 28%, transparent);
|
||||
--ring: #efe5c0; /* the ring-2 ring-fg look */
|
||||
--sidebar: #000000;
|
||||
--sidebar-foreground: #efe5c0;
|
||||
--sidebar-primary: #efe5c0;
|
||||
--sidebar-primary-foreground: #000000;
|
||||
--sidebar-accent: #1a1a1a;
|
||||
--sidebar-accent-foreground: #efe5c0;
|
||||
--sidebar-border: color-mix(in oklab, #efe5c0 22%, transparent);
|
||||
--sidebar-ring: #efe5c0;
|
||||
--chart-1: #efe5c0; --chart-2: #a89984; --chart-3: #fabd2f;
|
||||
--chart-4: #b8bb26; --chart-5: #83a598; /* Gruvbox for charts */
|
||||
--success: #b8bb26; --warning: #fabd2f;
|
||||
|
||||
/* oikos semantic aliases (app.css :root block) */
|
||||
--bg: var(--background); --bg-surface: var(--card); --bg-deeper: #050505;
|
||||
--bg-hover: var(--secondary); --bg-active: var(--accent);
|
||||
--text: var(--foreground); --text-muted: var(--muted-foreground);
|
||||
--accent-blue: #83a598; --accent-green: var(--success);
|
||||
--accent-red: var(--destructive); --accent-orange: var(--warning);
|
||||
|
||||
/* terminal face for this theme only (see §5) */
|
||||
--font-sans: 'JetBrains Mono', ui-monospace, Menlo, monospace;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
|
||||
--font-heading: 'VT323', 'JetBrains Mono', monospace; /* pixel wordmark feel */
|
||||
}
|
||||
|
||||
/* ── Cyberspace Light (black on cream paper) — exact inverse ── */
|
||||
:root[data-theme='cyber-light'] {
|
||||
--radius: 0px;
|
||||
--background: #efe5c0;
|
||||
--foreground: #000000;
|
||||
--card: #efe5c0;
|
||||
--card-foreground: #000000;
|
||||
--popover: #efe5c0;
|
||||
--popover-foreground: #000000;
|
||||
--primary: #000000;
|
||||
--primary-foreground: #efe5c0;
|
||||
--secondary: #e0d6b0;
|
||||
--secondary-foreground: #000000;
|
||||
--muted: #e6dcc0;
|
||||
--muted-foreground: #3a3a3a; /* fgDim */
|
||||
--accent: #000000;
|
||||
--accent-foreground: #efe5c0;
|
||||
--destructive: #9d0006;
|
||||
--destructive-foreground: #efe5c0;
|
||||
--border: color-mix(in oklab, #000000 22%, transparent);
|
||||
--input: color-mix(in oklab, #000000 28%, transparent);
|
||||
--ring: #000000;
|
||||
--sidebar: #efe5c0;
|
||||
--sidebar-foreground: #000000;
|
||||
--sidebar-primary: #000000;
|
||||
--sidebar-primary-foreground: #efe5c0;
|
||||
--sidebar-accent: #e0d6b0;
|
||||
--sidebar-accent-foreground: #000000;
|
||||
--sidebar-border: color-mix(in oklab, #000000 22%, transparent);
|
||||
--sidebar-ring: #000000;
|
||||
--chart-1: #000000; --chart-2: #3a3a3a; --chart-3: #b57614;
|
||||
--chart-4: #79740e; --chart-5: #076678;
|
||||
--success: #79740e; --warning: #b57614;
|
||||
|
||||
--bg: var(--background); --bg-surface: var(--card); --bg-deeper: #e6dcc0;
|
||||
--bg-hover: var(--secondary); --bg-active: var(--accent);
|
||||
--text: var(--foreground); --text-muted: var(--muted-foreground);
|
||||
--accent-blue: #076678; --accent-green: var(--success);
|
||||
--accent-red: var(--destructive); --accent-orange: var(--warning);
|
||||
|
||||
--font-sans: 'JetBrains Mono', ui-monospace, Menlo, monospace;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
|
||||
--font-heading: 'VT323', 'JetBrains Mono', monospace;
|
||||
}
|
||||
```
|
||||
|
||||
Two notes:
|
||||
|
||||
- **`.dark` vs `data-theme`.** The current store flips `.dark` on `<html>`. To
|
||||
keep Carbon working unchanged, leave `.dark` logic alone and layer
|
||||
`data-theme` on top: when a cyberspace theme is active the store sets
|
||||
`data-theme` and **removes** `.dark` (cyberspace themes are self-contained —
|
||||
they set both polarities explicitly). See §6.
|
||||
- **Borders from `fg`.** cyberspace's hairline tracks the foreground, not a
|
||||
fixed gray. `color-mix(in oklab, <fg> 22%, transparent)` reproduces that and
|
||||
auto-flips between the two themes. Tune the % after visual review.
|
||||
|
||||
---
|
||||
|
||||
## 4. The dithered image component (`RasterImage.svelte`)
|
||||
|
||||
**File:** `web/src/lib/components/RasterImage.svelte` (sibling of the existing
|
||||
`Spinner.svelte`).
|
||||
|
||||
### 4.1 API
|
||||
|
||||
```svelte
|
||||
<RasterImage src={entity.iconUrl} alt="host icon" width={320} />
|
||||
<!-- optional: scale (downsample factor), threshold bias, mono palette override -->
|
||||
```
|
||||
|
||||
- `src`, `alt` — as `<img>`.
|
||||
- `width` — render width in CSS px; canvas is sized to this × natural aspect.
|
||||
Downscaling before dithering is what sells the "lo-fi" look (defaults ~256–
|
||||
320). Expose `scale` (0–1) to control.
|
||||
- Reads the active theme's `--foreground` / `--background` via
|
||||
`getComputedStyle(document.documentElement)` so the dither palette **follows
|
||||
the theme** (cream/black in cyber-dark, black/cream in cyber-light, and
|
||||
perfectly sensible in Terracotta/Carbon too).
|
||||
|
||||
### 4.2 Behavior
|
||||
|
||||
1. Show `.raster-image-skeleton` (empty, `background: var(--background)`) until
|
||||
the source image loads — matches cyberspace's no-flash placeholder.
|
||||
2. On load: create an offscreen canvas at `width × (h/w*width)`, `drawImage`
|
||||
(with `imageSmoothingEnabled = true` for the downscale), pull
|
||||
`getImageData`.
|
||||
3. Run **Atkinson dithering** to 2 colors:
|
||||
- For each pixel: luminance `Y = 0.299R + 0.587G + 0.114B`.
|
||||
- Threshold at 128 (+ optional `bias`), snap to either `fg` or `bg`.
|
||||
- Push **1/8 of the quantization error** to each of 6 neighbors (Atkinson's
|
||||
kernel): right, below-left, below, below-right, and two pixels down on the
|
||||
next-next row. (Atkinson diffuses less than Floyd–Steinberg → softer, more
|
||||
"screen-printed" — exactly the cyberspace feel.)
|
||||
- Write `fg`/`bg` (read from CSS vars at render time) into the buffer.
|
||||
4. `putImageData`. Canvas is the visible output; the loaded `<img>` is kept as
|
||||
`aria-hidden` fallback for no-JS / copy-image / accessibility.
|
||||
5. **Re-dither on theme change**: subscribe to the theme store; when it flips,
|
||||
re-read `--foreground`/`--background` and re-run steps 3–4 (cheap — the
|
||||
decoded `ImageBitmap` is cached, only the palette pass reruns). This is the
|
||||
detail that makes the art flip polarity with the theme toggle.
|
||||
6. **Respect `prefers-reduced-data` / reduced motion?** Dithering is not motion,
|
||||
but offer a `plain` prop to skip the canvas and render the raw `<img>` for
|
||||
users who want crisp photos (e.g. entity detail screens where legibility
|
||||
beats aesthetic).
|
||||
|
||||
### 4.3 Reference dither kernel (Atkinson)
|
||||
|
||||
```
|
||||
* → 1/8 1/8
|
||||
1/8 1/8 1/8 (current pixel = *)
|
||||
1/8 1/8 (* is at top-left of this 4×? — see standard Atkinson spread)
|
||||
```
|
||||
|
||||
Spread pattern (error e from pixel at (x,y) distributed):
|
||||
|
||||
```
|
||||
px x+1 (1/8) x+2 (1/8)
|
||||
x-1 (1/8) x (1/8) x+1 (1/8)
|
||||
x+1 (1/8) x+2 (1/8) [next row offsets]
|
||||
```
|
||||
|
||||
Concretely, 6 neighbors each get `e/8`: `(x+1,y)`, `(x+2,y)`, `(x-1,y+1)`,
|
||||
`(x,y+1)`, `(x+1,y+1)`, `(x,y+2)`. (Clamp at edges — drop, don't wrap.)
|
||||
|
||||
### 4.4 Where to use it
|
||||
|
||||
- Entity icons / host thumbnails in the KB and entity desktop (the obvious win).
|
||||
- Mascot or login/Config background art (`ConfigBackground.svelte` already
|
||||
exists — a dithered backdrop there would be striking).
|
||||
- Any user-uploaded image in chat/knowledge where we want the "de-imagined"
|
||||
tone. Keep it **opt-in per call site** via the `plain` prop — don't dither
|
||||
diagrams/screenshots that need to stay readable.
|
||||
|
||||
### 4.5 Cross-origin caveat
|
||||
|
||||
`getImageData` throws on tainted canvases. If `src` is cross-origin and the
|
||||
server doesn't send CORS headers, fall back to the plain `<img>` (log once).
|
||||
For self-hosted assets (the common case here) it's a non-issue.
|
||||
|
||||
---
|
||||
|
||||
## 5. Fonts: self-host JetBrains Mono + VT323
|
||||
|
||||
cyberspace self-hosts both faces as `.woff2`. oikos currently pulls DM Sans /
|
||||
DM Mono / Inknut Antiqua from Google Fonts via a `<link>` in
|
||||
`web/index.html:10`.
|
||||
|
||||
- Drop `JetBrainsMono-Regular.woff2` and `VT323-Regular.woff2` under
|
||||
`web/static/fonts/` (or `web/public/fonts/` — match where static assets are
|
||||
served from; check `vite.config`).
|
||||
- Add `@font-face` blocks at the top of `app.css` with `font-display: swap`.
|
||||
- For the cyberspace themes only, the `--font-sans`/`--font-mono`/`--font-heading`
|
||||
overrides in §3 remap the families — Terracotta/Carbon keep DM Sans/Inknut
|
||||
untouched. This is the key trick: **font choice is part of the theme**, not a
|
||||
global swap, so the two art directions don't fight.
|
||||
- Leave the Google Fonts `<link>` in place for now (Terracotta/Carbon still need
|
||||
it); add a follow-up to self-host those too if we want to kill the external
|
||||
request entirely. Out of scope for this plan.
|
||||
|
||||
VT323 vs Departure Mono: VT323 is free on Google Fonts and trivial to self-host;
|
||||
Departure Mono is the authentic cyberspace face but needs a license check.
|
||||
**Recommend VT323** to start; swap to Departure Mono later if you want exact
|
||||
fidelity.
|
||||
|
||||
---
|
||||
|
||||
## 6. Theme store changes (`web/src/lib/stores/theme.svelte.ts`)
|
||||
|
||||
Current: `Theme = 'light' | 'dark'`, flips `.dark` class. Extend to a named set
|
||||
while preserving the existing API (callers of `toggleTheme`/`getTheme` keep
|
||||
working):
|
||||
|
||||
```ts
|
||||
export type ThemeName = 'terracotta' | 'carbon' | 'cyber-dark' | 'cyber-light'
|
||||
// Back-compat aliases used by existing callers:
|
||||
// 'light' -> 'terracotta', 'dark' -> 'carbon'
|
||||
```
|
||||
|
||||
- Store key stays `oikos-theme`; migrate old `'light'`/`'dark'` values on read.
|
||||
- `applyClass` becomes `applyTheme`: sets `data-theme` on `<html>` and toggles
|
||||
`.dark` **only** for `carbon` (so Terracotta and both cyberspace themes run
|
||||
with no `.dark`). This is important: the `.dark` block in `app.css` must not
|
||||
layer on top of the cyberspace token blocks — cyberspace sets its own
|
||||
polarities.
|
||||
- Update `THEME_LABELS` to the four names; update whatever UI surfaces the
|
||||
picker (search for `THEME_LABELS` / `toggleTheme` usages — likely
|
||||
`Settings.svelte` or the desktop shell's chrome) to a 4-option control instead
|
||||
of a binary toggle.
|
||||
|
||||
**Watch out:** any code that assumes `document.documentElement.classList.contains('dark')`
|
||||
≡ "dark colors" will be wrong for `cyber-dark`. Audit `grep -rn "classList.*dark\|\.dark" web/src` and prefer reading `getTheme()`/`data-theme` instead.
|
||||
|
||||
---
|
||||
|
||||
## 7. Optional cosmetic idioms (additive utilities)
|
||||
|
||||
Small, theme-aware utilities in `app.css` — usable in any theme but idiomatic
|
||||
for cyberspace:
|
||||
|
||||
- `.terminal-box` — `{ border:1px solid var(--border); border-radius:0 }` plus a
|
||||
`.terminal-box:focus-within { box-shadow: 0 0 0 2px var(--ring) }` to mirror
|
||||
the `ring-2 ring-fg` focus. Lets cards opt into the terminal look without a
|
||||
component rewrite.
|
||||
- `.braille-spinner` — keyframe cycling `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` as `::after` content, colored
|
||||
`var(--muted-foreground)`. Alternative to `Spinner.svelte` for loading states
|
||||
under cyberspace themes.
|
||||
- `.font-vt` — `{ font-family: var(--font-heading) }` so the wordmark class
|
||||
cyberspace uses maps to our heading var (VT323 under cyber themes, Inknut
|
||||
under Terracotta). Drop-in for any stylized title.
|
||||
- `.strike-list` — `li > s { color: var(--muted-foreground) }` convenience for
|
||||
the crossed-out feature-list pattern in marketing/empty states.
|
||||
|
||||
None of these are required for the theme to work; they're palette for the
|
||||
"de-imagined" voice where we want it.
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation order (incremental, each step shippable)
|
||||
|
||||
1. **Fonts** (§5) — self-host JetBrains Mono + VT323, `@font-face` in app.css.
|
||||
No visual change yet (only cyberspace themes reference them).
|
||||
2. **Tokens** (§3) — add the two `:root[data-theme='cyber-*']` blocks.
|
||||
3. **Store** (§6) — extend `theme.svelte.ts` to named themes + `data-theme`;
|
||||
update the picker UI. **At this point both cyberspace themes are live and
|
||||
fully recolor the whole app** — the cheapest milestone, biggest visible win.
|
||||
4. **`RasterImage.svelte`** (§4) — build + wire into entity icons and
|
||||
`ConfigBackground`. This is the "dithered image" deliverable.
|
||||
5. **Idioms** (§7) — terminal-box, braille spinner, etc., applied opportunistically.
|
||||
|
||||
Each step is independently mergeable. Step 3 alone satisfies "light + dark
|
||||
cyberspace themes"; step 4 satisfies "dithered images."
|
||||
|
||||
---
|
||||
|
||||
## 9. Verification
|
||||
|
||||
- `cd web && npm run build` (or the repo's build command — confirm in
|
||||
`web/package.json`) — Tailwind v4 must accept the new `data-theme` selectors
|
||||
and `color-mix()` (both standard; no config change expected).
|
||||
- `npm run check` / `svelte-check` for the store + component TS.
|
||||
- Manual: cycle all four themes in the picker; confirm no `.dark` bleed on
|
||||
`cyber-light`; confirm `RasterImage` re-dithers on theme flip; confirm
|
||||
`prefers-reduced-data`/`plain` prop shows crisp image; confirm cross-origin
|
||||
`src` degrades to `<img>` without console errors.
|
||||
- Lighthouse / a11y: 1-bit dithered images still need a real `alt` (kept on the
|
||||
fallback `<img>`); contrast on `#a89984`-on-black passes WCAG AA for body text
|
||||
(ratio ≈ 7.4:1) — fine.
|
||||
|
||||
---
|
||||
|
||||
## 10. Alternatives considered
|
||||
|
||||
- **Full rebrand (replace Terracotta/Carbon).** Highest visual payoff, highest
|
||||
cost: every component's rounding/serif/spacing was authored for the Art
|
||||
ouveau
|
||||
direction; square + mono would need a component-level sweep, not just tokens.
|
||||
Defer unless you want cyberspace as *the* oikos look — then do it as a
|
||||
follow-up that deletes Terracotta/Carbon and makes `cyber-dark` the sole
|
||||
default.
|
||||
- **CSS-only image dither (filters / SVG turbulence).** Cheaper, but can't do
|
||||
true 1-bit error diffusion or recolor to theme fg/bg. Rejected — the canvas
|
||||
pass is the whole point and is ~60 lines.
|
||||
- **Ordered (Bayer) dither instead of Atkinson.** More regular/grid-like
|
||||
("newspaper halftone"). Atkinson is softer and more terminal-like; keep
|
||||
Bayer as a `algorithm='bayer'` prop option later if wanted.
|
||||
- **Server-side dithering.** Could pre-dither icons at ingest. Rejected for
|
||||
now — client canvas keeps one source of truth (the original image) and lets
|
||||
the palette follow the live theme, which a baked asset can't.
|
||||
|
||||
---
|
||||
|
||||
## 11. Non-goals / out of scope
|
||||
|
||||
- Replicating cyberspace's sidebar-rail *layout* (oikos uses a floating-window
|
||||
desktop shell; the rail is a different app model). We take the *visual*
|
||||
language, not the IA.
|
||||
- Porting the 9 other novelty themes (C64, Matrix, VT320, …). Two (light/dark)
|
||||
satisfy the request; the token model makes adding more trivial later.
|
||||
- Removing the Google Fonts dependency for Terracotta/Carbon (follow-up).
|
||||
- Licensing/redistributing Departure Mono (use VT323 unless cleared).
|
||||
|
||||
---
|
||||
|
||||
## 12. Risks
|
||||
|
||||
- **`.dark` coupling.** Existing code may equate `.dark` with "dark UI".
|
||||
Mitigation: audit in step 3; the grep is small.
|
||||
- **Dither perf on large images.** Atkinson is O(n) and runs on a downscaled
|
||||
canvas (≤~320px wide), so per-image cost is negligible; but batch-rendering
|
||||
many entity icons on first paint could jank. Mitigation: dither lazily (on
|
||||
intersection) and cache the result on the element.
|
||||
- **Tainted canvas** on cross-origin images → silent fallback to `<img>`
|
||||
(already handled in the design).
|
||||
- **Token drift.** If a component hardcodes a color instead of using a token,
|
||||
it won't recolor under cyberspace. This is the same risk Carbon already has;
|
||||
no new exposure, just more visible under a stronger theme.
|
||||
@@ -17,13 +17,8 @@ went sideways, open an investigation.
|
||||
| 2026-07-14 | [Activity gaps](2026-07-14-activity-gaps.md) | In Progress |
|
||||
| 2026-07-14 | [Activity timeline](2026-07-14-activity-timeline.md) | In Progress |
|
||||
| 2026-07-17 | [Codebase review, lint audit, and documentation maintenance](2026-07-17-codebase-review-and-cleanup.md) | Report delivered — doc/tooling fixes applied; code refactors pending |
|
||||
| 2026-07-18 | [Session review: three recent sessions](2026-07-18-session-review-three-sessions.md) | Implemented in v0.7.12 — P0.1/P0.2/P1.3/P1.4/P1.5/P1.6/P1.8/P2.10; P1.7 and P2.9 deferred (retry cap covers) |
|
||||
| 2026-07-20 | [Desktop mascot ("Cluck")](2026-07-20-desktop-mascot.md) | Implemented in v0.8.0 — see deviation note; physics/window-interaction follow-ups tracked separately |
|
||||
| 2026-07-20 | [Session review: past 10 sessions](2026-07-20-session-review-ten-sessions.md) | Implemented in v0.7.13 — all P0/P1/P2 items landed |
|
||||
| 2026-07-20 | [Mascot physics/window-interaction audit](2026-07-20-mascot-physics-audit.md) | P0–P2 implemented; P3 ("cool stuff") ideas open |
|
||||
| 2026-07-21 | [Frontend as OS + Apps — architecture audit & refactor](2026-07-21-frontend-os-apps-architecture.md) | Planned — Phase 1 ready |
|
||||
| 2026-08-03 | [Nomos chat: reliability & predictability audit](2026-08-03-nomos-chat-reliability-and-ux-audit.md) | In Progress — F1–F7 shipped in v0.15.0; F8 + follow-ups open |
|
||||
| 2026-08-03 | [Nomos chat: working-visibility, message queue, generation-aware timeline](2026-08-03-nomos-chat-working-visibility.md) | Implemented in v0.17.0 — F1–F4 shipped; model-efficiency + F8 follow-ups open |
|
||||
|
||||
## Done
|
||||
|
||||
@@ -61,6 +56,16 @@ See [`done/`](done/) for executed plans:
|
||||
| 2026-07-14 | [Post-fix session remainders: empty responses & plan drift](done/2026-07-14-post-fix-session-remainders.md) |
|
||||
| 2026-07-15 | [Plan-first and iteration](done/2026-07-15-plan-first-and-iteration.md) |
|
||||
| 2026-07-15 | [WhatsApp session audit](done/2026-07-15-whatsapp-session-audit.md) |
|
||||
| 2026-07-18 | [Session review: three recent sessions](done/2026-07-18-session-review-three-sessions.md) |
|
||||
| 2026-07-20 | [Desktop mascot ("Cluck")](done/2026-07-20-desktop-mascot.md) |
|
||||
| 2026-07-20 | [Session review: past 10 sessions](done/2026-07-20-session-review-ten-sessions.md) |
|
||||
| 2026-07-21 | [Chat window full polish](done/2026-07-21-chat-full-polish.md) |
|
||||
| 2026-07-29 | [Make health reflect reality + complete the knowledge graph](done/2026-07-29-health-check-reality-and-knowledge-graph.md) |
|
||||
| 2026-07-30 | [Session review: plan drift & dead activity panel](done/2026-07-30-session-review-plan-drift-and-dead-activity-panel.md) |
|
||||
| 2026-08-03 | [Nomos chat changes review (P0/P1/P2)](done/2026-08-03-nomos-chat-changes-review.md) |
|
||||
| 2026-08-03 | [Nomos chat: reliability & predictability audit](done/2026-08-03-nomos-chat-reliability-and-ux-audit.md) |
|
||||
| 2026-08-03 | [Adopt cyberspace.online terminal aesthetic + dithered images](done/2026-08-03-cyberspace-style-adoption.md) |
|
||||
| 2026-08-03 | [Nomos chat: working-visibility, message queue, generation-aware timeline](done/2026-08-03-nomos-chat-working-visibility.md) |
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user