APPEARANCE: simplify to Theme & UI (scheme, theme families, background) + Text; custom gradients; Unsplash #248

Closed
opened 2026-09-27 17:08:12 +00:00 by kayg · 15 comments
Owner

Owner grill (2026-09-27): Settings → Appearance is "a straight no". Simplify. All decisions below are settled.

Target layout

Theme & UI

  • Colour scheme: System / Light / Dark (pill group).
  • Theme: ONE dropdown of theme families. Each family has a light and a dark variant; the colour scheme picks the variant (System follows the OS). Families with several variants of one scheme (Catppuccin Frappé/Macchiato/Mocha, Rosé Pine / Moon, Ayu Dark / Mirage) default to the flagship, with a small "Variant" choice beside it. Removes "Light theme", "Dark theme", "Same as light".
  • Background: Colour / Picture.
    • Colour: the theme's own gradient (default) or Custom: one editor for Solid / Linear / Radial, 2–3 stops, angle dial (linear) or draggable centre (radial), stop colours from the theme palette swatches plus a free picker, live preview behind the Settings window. Research the best gradient-editor designs first (see Phase 1).
    • Picture: pick from the user's calternal Photos, upload from the device, or search Unsplash (in-app, supported API). Automatic legibility: measure the picture's luminance and apply the matching blur/dim scrim; no slider.
    • Remembered per family and per scheme (e.g. Catppuccin-dark has its own background, separate from Catppuccin-light), for both Colour and Picture.
      Text (merges the fonts group from #234): Interface font, Display font, Mono font, Text size (the old "Reading size").
      Removed: Accent (Theme/Indigo/Teal/Plum): the theme owns its accent.
      Moved later (separate grill, not in this issue): Mode tray → a future Settings → Tabs section. Leave it where it is for now.

Families (curated but versatile)

Owner: "we want a curated list but we also want to be versatile". Every family must exist in BOTH schemes. Today: 29 themes (9 light, 18 dark, Mono both) in packages/ui/src/themes.ts / themes.css.

  • Pair the existing halves: Catppuccin (Latte ↔ Frappé/Macchiato/Mocha), Rosé Pine (Dawn ↔ Rosé Pine/Moon), Ayu (Light ↔ Dark/Mirage), GitHub (Light ↔ Dark), One (Light ↔ Dark), Mono.
  • Find the OFFICIAL light/dark counterparts for the single-scheme ones (owner: a deeper web search will find them): Tokyo Night (Day), Gruvbox (Light), Everforest (Light), Kanagawa (Lotus), Dracula (Alucard, official), Solarized (Dark), Nord (official light? if none, the best-known community light variant), and any others.
  • calternal's own themes paired into families: Paper ↔ Midnight, Meridian ↔ Forest, Bloom ↔ Ember (adjust pairing by hue if research suggests better).
  • Licences: only MIT/OFL/permissive palette sources; record source + licence per family in a THEMES.md.

Unsplash (owner approved)

  • Free API: demo 50 req/h, production 5,000 req/h after approval (guidelines screenshots). Terms: hotlink images.unsplash.com URLs, credit photographer + Unsplash with links, call the download endpoint when a photo is chosen, API key server-side only.
  • Server-side proxy route in calternal (key in instance config; Admin → Server configuration field "Unsplash access key"); UI hides Unsplash search when no key is set. Reuse the existing apps/web/src/routes/settings/appearance/UnsplashSheet.svelte (reuse gate). On calternal.cloud the owner will supply the key.

Phases (one job)

  1. Research (web search): (a) the official light/dark counterparts for every family with licences and exact palette sources; (b) the best gradient editor and background picker designs (e.g. Figma, Framer, Apple Freeform/Keynote, Raycast, Arc, CSS gradient tools, modern OS wallpaper pickers); post a short design brief with links and screenshots on this issue.
  2. Build on the existing components: shared SegmentedControl pills, the shared dropdown (the one #234 moves the font pickers to), OverlaySurface on desktop / sheet on phone (#244 rule), settings rows from SettingsGroup. Code reuse is paramount.
  3. Migrate stored per-user appearance settings: old light/dark theme ids → family + variant; old accent dropped; old backgrounds mapped to the new per-family/per-scheme store. Idempotent, tested.
  4. DESIGN (§ Appearance) updated, ASD-STE100.

Evidence

Production screenshots (desktop 1440 + phone 390, light + dark): the new Appearance page, the Theme family dropdown open, each family in both schemes (contact sheet), the gradient editor (solid / linear / radial), picture picker with Photos / upload / Unsplash tabs, a busy photo background with the automatic scrim. Attach, do not commit.

Depends on: #234 (fonts, the Text section's dropdowns) and #246 (menus portal/blur). Phase 1 research can start now.

Owner grill (2026-09-27): Settings → Appearance is "a straight no". Simplify. All decisions below are settled. ## Target layout **Theme & UI** - **Colour scheme:** System / Light / Dark (pill group). - **Theme:** ONE dropdown of **theme families**. Each family has a light and a dark variant; the colour scheme picks the variant (System follows the OS). Families with several variants of one scheme (Catppuccin Frappé/Macchiato/Mocha, Rosé Pine / Moon, Ayu Dark / Mirage) default to the flagship, with a small "Variant" choice beside it. Removes "Light theme", "Dark theme", "Same as light". - **Background:** Colour / Picture. - **Colour:** the theme's own gradient (default) or **Custom**: one editor for Solid / Linear / Radial, 2–3 stops, angle dial (linear) or draggable centre (radial), stop colours from the theme palette swatches plus a free picker, live preview behind the Settings window. Research the best gradient-editor designs first (see Phase 1). - **Picture:** pick from the user's calternal Photos, upload from the device, or search **Unsplash** (in-app, supported API). Automatic legibility: measure the picture's luminance and apply the matching blur/dim scrim; no slider. - Remembered **per family and per scheme** (e.g. Catppuccin-dark has its own background, separate from Catppuccin-light), for both Colour and Picture. **Text** (merges the fonts group from #234): Interface font, Display font, Mono font, **Text size** (the old "Reading size"). **Removed:** Accent (Theme/Indigo/Teal/Plum): the theme owns its accent. **Moved later (separate grill, not in this issue):** Mode tray → a future Settings → Tabs section. Leave it where it is for now. ## Families (curated but versatile) Owner: "we want a curated list but we also want to be versatile". Every family must exist in BOTH schemes. Today: 29 themes (9 light, 18 dark, Mono both) in `packages/ui/src/themes.ts` / `themes.css`. - Pair the existing halves: Catppuccin (Latte ↔ Frappé/Macchiato/Mocha), Rosé Pine (Dawn ↔ Rosé Pine/Moon), Ayu (Light ↔ Dark/Mirage), GitHub (Light ↔ Dark), One (Light ↔ Dark), Mono. - Find the OFFICIAL light/dark counterparts for the single-scheme ones (owner: a deeper web search will find them): Tokyo Night (Day), Gruvbox (Light), Everforest (Light), Kanagawa (Lotus), Dracula (Alucard, official), Solarized (Dark), Nord (official light? if none, the best-known community light variant), and any others. - calternal's own themes paired into families: Paper ↔ Midnight, Meridian ↔ Forest, Bloom ↔ Ember (adjust pairing by hue if research suggests better). - Licences: only MIT/OFL/permissive palette sources; record source + licence per family in a THEMES.md. ## Unsplash (owner approved) - Free API: demo 50 req/h, production 5,000 req/h after approval (guidelines screenshots). Terms: hotlink `images.unsplash.com` URLs, credit photographer + Unsplash with links, call the download endpoint when a photo is chosen, API key server-side only. - Server-side proxy route in calternal (key in instance config; Admin → Server configuration field "Unsplash access key"); UI hides Unsplash search when no key is set. Reuse the existing `apps/web/src/routes/settings/appearance/UnsplashSheet.svelte` (reuse gate). On calternal.cloud the owner will supply the key. ## Phases (one job) 1. **Research (web search):** (a) the official light/dark counterparts for every family with licences and exact palette sources; (b) the best gradient editor and background picker designs (e.g. Figma, Framer, Apple Freeform/Keynote, Raycast, Arc, CSS gradient tools, modern OS wallpaper pickers); post a short design brief with links and screenshots on this issue. 2. **Build** on the existing components: shared SegmentedControl pills, the shared dropdown (the one #234 moves the font pickers to), OverlaySurface on desktop / sheet on phone (#244 rule), settings rows from SettingsGroup. Code reuse is paramount. 3. **Migrate** stored per-user appearance settings: old light/dark theme ids → family + variant; old accent dropped; old backgrounds mapped to the new per-family/per-scheme store. Idempotent, tested. 4. DESIGN (§ Appearance) updated, ASD-STE100. ## Evidence Production screenshots (desktop 1440 + phone 390, light + dark): the new Appearance page, the Theme family dropdown open, each family in both schemes (contact sheet), the gradient editor (solid / linear / radial), picture picker with Photos / upload / Unsplash tabs, a busy photo background with the automatic scrim. Attach, do not commit. **Depends on:** #234 (fonts, the Text section's dropdowns) and #246 (menus portal/blur). Phase 1 research can start now.
Author
Owner

Starting Phase 1 on branch job/appearance, based on 200d171ea3. The repo has 29 theme IDs; I am checking the existing family groupings, official counterparts, upstream palette values and licences before writing docs/research/appearance-248.md.

Starting Phase 1 on branch job/appearance, based on 200d171ea35e1de53ad56e6c1d7fcef95881f0a3. The repo has 29 theme IDs; I am checking the existing family groupings, official counterparts, upstream palette values and licences before writing docs/research/appearance-248.md.
Author
Owner

Phase 1 report

Phase 1 research is complete. The brief is committed and pushed on job/appearance: docs/research/appearance-248.md. No product code changed.

The brief maps the 29 existing theme IDs to 16 light/dark families. It includes upstream source URLs, licenses, palette values for the appearance roles, and the proposed calternal pairings. The third-party sources are MIT (Gruvbox: MIT/X11); the existing calternal palettes are AGPL-3.0-only.

It also records the gradient editor proposal (solid, linear, radial; 2-3 stops; angle dial, radial centre, theme swatches, keyboard controls) and the Photos / Upload / Unsplash picker brief, including automatic contrast treatment, attribution, server-side API key handling, and Unsplash download tracking.

Decisions outside the current design text

  • Nord's maintainers say an official light Day theme is still planned. Keep Nord Light marked community-only; the brief recommends the IllegalStudio palette for its named UI roles and contrast rationale, with Huytd's port as a community reference.
  • Keep the current Catppuccin, Rose Pine, and Ayu variants. Do not add Tokyo Night Storm, Kanagawa Dragon, or other upstream variants to the selector.

Validation output

cargo fmt --check
(exit 0; no output)

cargo clippy --all-targets -- -D warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.52s

cargo test
CARGO_TEST_EXIT_CODE=0

bun run check
svelte-check found 0 errors and 0 warnings

bun run test
 Test Files  82 passed (82)
      Tests  593 passed (593)
   Start at  20:24:14
   Duration  140.27s (transform 66%, import 12%, environment 12%, tests 8%, setup 2%)

Setup note: clippy first ran before the web bundle existed and failed because apps/web/build/ was missing. After installing from the frozen lockfile and building the web app, clippy passed. The build emitted existing Rolldown notices about use client directives in vendored analytics files.

The branch includes the one-time dev merge. Document commit: 9b19ff6a; final head: dae2cee40f0d396d0d834ee987af3a6ddef4137a. git push origin job/appearance returned Everything up-to-date. Cargo and web build outputs were cleaned.

# Phase 1 report Phase 1 research is complete. The brief is committed and pushed on `job/appearance`: [docs/research/appearance-248.md](https://git.kayg.org/kayg/calternal/src/branch/job/appearance/docs/research/appearance-248.md). No product code changed. The brief maps the 29 existing theme IDs to 16 light/dark families. It includes upstream source URLs, licenses, palette values for the appearance roles, and the proposed calternal pairings. The third-party sources are MIT (Gruvbox: MIT/X11); the existing calternal palettes are AGPL-3.0-only. It also records the gradient editor proposal (solid, linear, radial; 2-3 stops; angle dial, radial centre, theme swatches, keyboard controls) and the Photos / Upload / Unsplash picker brief, including automatic contrast treatment, attribution, server-side API key handling, and Unsplash download tracking. ## Decisions outside the current design text - Nord's maintainers say an official light Day theme is still planned. Keep Nord Light marked community-only; the brief recommends the IllegalStudio palette for its named UI roles and contrast rationale, with Huytd's port as a community reference. - Keep the current Catppuccin, Rose Pine, and Ayu variants. Do not add Tokyo Night Storm, Kanagawa Dragon, or other upstream variants to the selector. ## Validation output ```text cargo fmt --check (exit 0; no output) cargo clippy --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.52s cargo test CARGO_TEST_EXIT_CODE=0 bun run check svelte-check found 0 errors and 0 warnings bun run test Test Files 82 passed (82) Tests 593 passed (593) Start at 20:24:14 Duration 140.27s (transform 66%, import 12%, environment 12%, tests 8%, setup 2%) ``` Setup note: clippy first ran before the web bundle existed and failed because `apps/web/build/` was missing. After installing from the frozen lockfile and building the web app, clippy passed. The build emitted existing Rolldown notices about `use client` directives in vendored analytics files. The branch includes the one-time `dev` merge. Document commit: `9b19ff6a`; final head: `dae2cee40f0d396d0d834ee987af3a6ddef4137a`. `git push origin job/appearance` returned `Everything up-to-date`. Cargo and web build outputs were cleaned.
Author
Owner

Starting Phase 2 on branch job/appearance, based on 088e17d4fe. I have read CLAUDE.md, CONTEXT.md, the relevant DESIGN sections, the issue, and both comments. I’m tracing the merged #234 fonts and #246 menu/overlay components plus the current appearance API and settings migration before implementation.

Starting Phase 2 on branch job/appearance, based on 088e17d4fe49d6a7b41f03fcc13f5f4959346be9. I have read CLAUDE.md, CONTEXT.md, the relevant DESIGN sections, the issue, and both comments. I’m tracing the merged #234 fonts and #246 menu/overlay components plus the current appearance API and settings migration before implementation.
Author
Owner

Finding: crates/calternal-server/src/appearance.rs already provides authenticated Unsplash search, selection/download tracking, and same-origin thumbnail proxy routes, and crates/calternal-server/src/wire.rs has the admin key endpoint. The current settings contract is still one {light, dark} background pair with theme/solid/image only; AppearanceSection.svelte renders separate light/dark theme rows and a manual image dim slider. I’m extending the existing contract and picker paths rather than adding a second proxy or duplicate selector.

Finding: crates/calternal-server/src/appearance.rs already provides authenticated Unsplash search, selection/download tracking, and same-origin thumbnail proxy routes, and crates/calternal-server/src/wire.rs has the admin key endpoint. The current settings contract is still one {light, dark} background pair with theme/solid/image only; AppearanceSection.svelte renders separate light/dark theme rows and a manual image dim slider. I’m extending the existing contract and picker paths rather than adding a second proxy or duplicate selector.
Author
Owner

Finding: the research names 16 theme families, but seven required light/dark palette counterparts were missing. Added those palettes and registered the 16 family pairs; focused web tests pass (81 tests).

Finding: the research names 16 theme families, but seven required light/dark palette counterparts were missing. Added those palettes and registered the 16 family pairs; focused web tests pass (81 tests).
Author
Owner

Finding: the old API stored only one light/dark pair, and the Unsplash route always replaced light. The server now stores backgrounds by family and scheme, validates bounded solid/linear/radial/image choices, migrates the legacy pair on first write, and accepts a family/scheme target for Unsplash. Focused server tests: 16 passed, 0 failed. OpenAPI/client types are regenerated.

Finding: the old API stored only one light/dark pair, and the Unsplash route always replaced light. The server now stores backgrounds by family and scheme, validates bounded solid/linear/radial/image choices, migrates the legacy pair on first write, and accepts a family/scheme target for Unsplash. Focused server tests: 16 passed, 0 failed. OpenAPI/client types are regenerated.
Author
Owner

Finding: the initial /api/v1/appearance read could return after a user changed a background. The late read replaced the optimistic edit and could also migrate stale legacy data over it. I reproduced this with a deferred GET and an intervening solid-colour assignment; the new regression test failed before the fix. BackgroundStore.load() now checks the save generation before applying the read or migrating legacy data. The regression test passes.

Finding: the initial `/api/v1/appearance` read could return after a user changed a background. The late read replaced the optimistic edit and could also migrate stale legacy data over it. I reproduced this with a deferred GET and an intervening solid-colour assignment; the new regression test failed before the fix. `BackgroundStore.load()` now checks the save generation before applying the read or migrating legacy data. The regression test passes.
Author
Owner

Finding: a normal background_assignment could return HTTP 200 while leaving the selected family's existing scheme value unchanged. The initial migration helper only added missing family entries, and normal assignments called that helper. I reproduced it by saving a second light background for the same family; the stored value stayed at the first colour. I split migration merge from authoritative assignment storage and extended the server regression test to verify that only the selected family/scheme changes.

The production screenshot run also showed that the family dropdown could list Paper while its closed trigger displayed the active palette name. The trigger now displays the family name, with a focused Svelte regression test. The rebuilt production review now captures 36 screenshots and checks icon cap-height alignment to within 1 px.

Finding: a normal `background_assignment` could return HTTP 200 while leaving the selected family's existing scheme value unchanged. The initial migration helper only added missing family entries, and normal assignments called that helper. I reproduced it by saving a second light background for the same family; the stored value stayed at the first colour. I split migration merge from authoritative assignment storage and extended the server regression test to verify that only the selected family/scheme changes. The production screenshot run also showed that the family dropdown could list `Paper` while its closed trigger displayed the active palette name. The trigger now displays the family name, with a focused Svelte regression test. The rebuilt production review now captures 36 screenshots and checks icon cap-height alignment to within 1 px.
Author
Owner

The post-merge appearance probe completed its #248 checks: hostile family, colour, gradient and radial inputs were refused; malformed and oversized payloads were refused; the disabled Unsplash routes were refused; and the 16-family concurrent writes retained every family/scheme pick. The write burst p95 was 0.566 s, reported as SLOW under shared-host load.

The run found that the three older Unsplash path probes omitted required family and scheme fields, so they stopped at JSON validation with 422. I corrected the probes to send a valid selection body; a targeted real-server request now reaches path validation and returns 400.

One oversized-body request returned the test proxy's 502 local adversarial server is unavailable, while a direct server request returned 413. I reproduced and fixed the proxy's early-rejection handling with Expect: 100-continue; the same 3 MiB request through the real test proxy now returns 413. The broad runner reached its 30-minute limit during the slow round-2 journal race checks, so that later suite is incomplete. The appearance checks had completed before the time limit.

The post-merge appearance probe completed its #248 checks: hostile family, colour, gradient and radial inputs were refused; malformed and oversized payloads were refused; the disabled Unsplash routes were refused; and the 16-family concurrent writes retained every family/scheme pick. The write burst p95 was 0.566 s, reported as SLOW under shared-host load. The run found that the three older Unsplash path probes omitted required `family` and `scheme` fields, so they stopped at JSON validation with 422. I corrected the probes to send a valid selection body; a targeted real-server request now reaches path validation and returns 400. One oversized-body request returned the test proxy's 502 `local adversarial server is unavailable`, while a direct server request returned 413. I reproduced and fixed the proxy's early-rejection handling with `Expect: 100-continue`; the same 3 MiB request through the real test proxy now returns 413. The broad runner reached its 30-minute limit during the slow round-2 journal race checks, so that later suite is incomplete. The appearance checks had completed before the time limit.
Author
Owner

Forgejo #248 final report

Implemented and pushed on job/appearance. Head: 74deabedfaeb82303bb29fb8812b8f0188192b18. Merged dev once before the final gates (bed1ced6).

Built:

  • Theme & UI controls for colour scheme, all 16 theme families, applicable variants, and family/scheme-specific solid, gradient, Photos, upload, and Unsplash backgrounds with a legibility scrim.
  • Text settings using the shared font menu and text-size control; removed Accent while keeping the Mode tray.
  • Idempotent appearance settings migration, server-side Unsplash proxy and admin key status/configuration, generated API updates, and DESIGN §35.
  • Production review captures: 36 screenshots attached to this issue. The capture run checks icon/text cap-height alignment within ±1 px.

Files: apps/web/src/routes/settings/appearance/, appearance/theme stores and styles in apps/web/src/lib/, apps/web/e2e/appearance-review.mjs and related harness scripts, shared theme UI in packages/ui/, crates/calternal-server/src/appearance.rs, contracts/openapi.json, packages/api-client/src/generated.ts, docs/DESIGN.md, and tests/adversarial/.

Gates (verbatim excerpts; all commands exited 0):

$ cargo fmt --check
(no output)

$ cargo clippy --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 12s

$ cargo test
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3m 36s
test result: ok. 486 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.49s

$ bun run --cwd apps/web check
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/appearance/apps/web
Getting Svelte diagnostics...
svelte-check found 0 errors and 0 warnings

$ bun run --cwd apps/web test
 Test Files  91 passed (91)
      Tests  641 passed (641)
   Start at  03:37:59
   Duration  79.66s (transform 64%, import 14%, environment 12%, tests 7%, setup 2%)

  Transform  |component| transforming modules took 110.88s · 54% of tracked time, re-done on every run
             persist transforms across runs with fsModuleCache: true
             learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns

Adversarial round: the single 30-minute run exercised the appearance probes, then timed out during round 2 (timeout status 124); round 2 is incomplete. Appearance writes across all family/scheme combinations passed. The mixed write storm had p95 0.566 s (SLOW under shared-host load). The oversized-body proxy response and missing fields in three Unsplash probe bodies were fixed and checked against a live local server. The editor image undo/redo duplicate was reproduced and reported on existing issue #265 (seed 25608414).

Decisions where DESIGN was silent: the theme picker displays the family name and offers a separate Variant control only for families that need it; uploaded background pictures are stored through the server in .calternal/backgrounds/, with bounded supported image types and size; photo choices are remembered by theme family and colour scheme.

Forgejo #248 final report Implemented and pushed on `job/appearance`. Head: `74deabedfaeb82303bb29fb8812b8f0188192b18`. Merged `dev` once before the final gates (`bed1ced6`). Built: - Theme & UI controls for colour scheme, all 16 theme families, applicable variants, and family/scheme-specific solid, gradient, Photos, upload, and Unsplash backgrounds with a legibility scrim. - Text settings using the shared font menu and text-size control; removed Accent while keeping the Mode tray. - Idempotent appearance settings migration, server-side Unsplash proxy and admin key status/configuration, generated API updates, and DESIGN §35. - Production review captures: 36 screenshots attached to this issue. The capture run checks icon/text cap-height alignment within ±1 px. Files: `apps/web/src/routes/settings/appearance/`, appearance/theme stores and styles in `apps/web/src/lib/`, `apps/web/e2e/appearance-review.mjs` and related harness scripts, shared theme UI in `packages/ui/`, `crates/calternal-server/src/appearance.rs`, `contracts/openapi.json`, `packages/api-client/src/generated.ts`, `docs/DESIGN.md`, and `tests/adversarial/`. Gates (verbatim excerpts; all commands exited 0): ```text $ cargo fmt --check (no output) $ cargo clippy --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 12s $ cargo test Finished `test` profile [unoptimized + debuginfo] target(s) in 3m 36s test result: ok. 486 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.49s $ bun run --cwd apps/web check Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/appearance/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings $ bun run --cwd apps/web test Test Files 91 passed (91) Tests 641 passed (641) Start at 03:37:59 Duration 79.66s (transform 64%, import 14%, environment 12%, tests 7%, setup 2%) Transform |component| transforming modules took 110.88s · 54% of tracked time, re-done on every run persist transforms across runs with fsModuleCache: true learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns ``` Adversarial round: the single 30-minute run exercised the appearance probes, then timed out during round 2 (`timeout` status 124); round 2 is incomplete. Appearance writes across all family/scheme combinations passed. The mixed write storm had p95 0.566 s (`SLOW` under shared-host load). The oversized-body proxy response and missing fields in three Unsplash probe bodies were fixed and checked against a live local server. The editor image undo/redo duplicate was reproduced and reported on existing issue #265 (seed `25608414`). Decisions where DESIGN was silent: the theme picker displays the family name and offers a separate Variant control only for families that need it; uploaded background pictures are stored through the server in `.calternal/backgrounds/`, with bounded supported image types and size; photo choices are remembered by theme family and colour scheme.
Author
Owner

Starting appearance polish on branch job/appearance.

Base SHA: 976b804f99188feb300e391c1942836599e2d429 (merge-base with dev)
Starting head: 74deabedfaeb82303bb29fb8812b8f0188192b18

I will address the gradient editor interactions, the phone sheet scroll issue, and capture updated production screenshots before the final gates.

Starting appearance polish on branch `job/appearance`. Base SHA: `976b804f99188feb300e391c1942836599e2d429` (merge-base with `dev`) Starting head: `74deabedfaeb82303bb29fb8812b8f0188192b18` I will address the gradient editor interactions, the phone sheet scroll issue, and capture updated production screenshots before the final gates.
Author
Owner

Finding from the current production review artifacts: appearance-light-390.png is 358×654 and ends during the Photos row. The review helper screenshots #opt-theme while it is inside the fixed-height sheet scrollport, so the rest of the group is clipped from the image. The mobile sheet's scroll item currently uses an intrinsic flex basis; I will make that scrollport shrink within its sheet and add a full-content capture path.

background-linear-gradient-dark-1440.png confirms the remaining editor mismatch: stop controls are rectangular, Add/Remove are outlined pills, and position and angle are exposed as number fields. No shared colour picker component exists in this repo or the calternal.js component tree, so I will add one reusable app component backed by the existing native browser colour input.

Finding from the current production review artifacts: `appearance-light-390.png` is 358×654 and ends during the Photos row. The review helper screenshots `#opt-theme` while it is inside the fixed-height sheet scrollport, so the rest of the group is clipped from the image. The mobile sheet's scroll item currently uses an intrinsic flex basis; I will make that scrollport shrink within its sheet and add a full-content capture path. `background-linear-gradient-dark-1440.png` confirms the remaining editor mismatch: stop controls are rectangular, Add/Remove are outlined pills, and position and angle are exposed as number fields. No shared colour picker component exists in this repo or the calternal.js component tree, so I will add one reusable app component backed by the existing native browser colour input.
Author
Owner

Completed on job/appearance; pushed head 41588c32e68a93d30af9a04aab4745e538825af4.

Built

  • Reworked the gradient editor: click the bar to add a stop; drag or press Delete/Backspace to remove; drag or use arrow keys to move; show stop position in a tooltip. The angle dial supports drag and arrow/Shift+arrow steps, and radial backgrounds support dragging the center on the preview.
  • Replaced the stop color field with palette swatches and one shared custom color picker. Removed the extra segmented-control focus ring.
  • Fixed phone settings sheet scrolling so the full Appearance page and picture source controls render.
  • Adjusted Kanagawa Lotus muted text from #62627a to #606078; the rendered glass contrast test measured 4.52:1 before rounding safety and now passes the 4.5:1 contract.

Files

  • apps/web/src/routes/settings/appearance/BackgroundGroup.svelte
  • apps/web/src/routes/settings/appearance/ColourPickerButton.svelte
  • apps/web/src/lib/backgroundGroup.svelte.test.ts
  • apps/web/src/lib/themes.test.ts
  • apps/web/e2e/appearance-review.mjs
  • packages/ui/src/components/SegmentedControl.svelte
  • packages/ui/src/components/OverlaySurface.svelte
  • packages/ui/src/themes.css

Review captures

Forty production-build captures were generated. The relevant full-length phone page, gradient editor, angle/radial editor, and picture picker captures are attached here:

The production review script passed its interaction checks for stop add/move/delete/off-bar drag, angle adjustment, radial center movement, and icon/text alignment within 1px.

Gates

cargo fmt --check
(exit 0; no output)

cargo clippy --all-targets -- -D warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 10s

cargo test
72 test result lines: all `test result: ok.`
GATE_EXIT=0

bun run check
$ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/appearance/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test
 Test Files  104 passed (104)
      Tests  690 passed (690)
   Start at  05:19:00
   Duration  59.25s (transform 62%, import 14%, environment 14%, tests 8%, setup 3%)

GATE_EXIT=0

The first post-merge web test run found the Lotus contrast issue above. After the palette correction, the focused theme test passed (68/68) and the full web suite passed (690/690). No test expectation changed.

Adversarial round

One time-boxed round ran for 25 minutes and timed out during dedup probes. The direct oversized Appearance request returned 413 and the server stayed up; the test proxy reported a false 502. Filed as #283. The attack2-only mode also reused users deleted by earlier probes and skipped restart coordination, invalidating its later dedup checks; filed as #285. Other completed probes showed no non-SLOW product failures; the adversarial round is incomplete because of the timeout.

Decision

The design doc did not prescribe the custom picker implementation. The shared custom color control uses the browser's native input[type=color] behind the styled swatch so keyboard focus and platform picker behavior remain available without adding a dependency.

Completed on `job/appearance`; pushed head `41588c32e68a93d30af9a04aab4745e538825af4`. ### Built - Reworked the gradient editor: click the bar to add a stop; drag or press Delete/Backspace to remove; drag or use arrow keys to move; show stop position in a tooltip. The angle dial supports drag and arrow/Shift+arrow steps, and radial backgrounds support dragging the center on the preview. - Replaced the stop color field with palette swatches and one shared custom color picker. Removed the extra segmented-control focus ring. - Fixed phone settings sheet scrolling so the full Appearance page and picture source controls render. - Adjusted Kanagawa Lotus muted text from `#62627a` to `#606078`; the rendered glass contrast test measured 4.52:1 before rounding safety and now passes the 4.5:1 contract. ### Files - `apps/web/src/routes/settings/appearance/BackgroundGroup.svelte` - `apps/web/src/routes/settings/appearance/ColourPickerButton.svelte` - `apps/web/src/lib/backgroundGroup.svelte.test.ts` - `apps/web/src/lib/themes.test.ts` - `apps/web/e2e/appearance-review.mjs` - `packages/ui/src/components/SegmentedControl.svelte` - `packages/ui/src/components/OverlaySurface.svelte` - `packages/ui/src/themes.css` ### Review captures Forty production-build captures were generated. The relevant full-length phone page, gradient editor, angle/radial editor, and picture picker captures are attached here: - Appearance page, light/dark: [light](https://git.kayg.org/attachments/e061ecf7-66c7-4fbd-9615-868e9382924e), [dark](https://git.kayg.org/attachments/8ea8cc68-ed7a-4d7a-999d-f4a383a9f6cd) - Linear editor, desktop: [light](https://git.kayg.org/attachments/51fed4ad-9297-46d9-abae-37ec6aa16e77), [dark](https://git.kayg.org/attachments/714704e7-906a-40dc-8ead-1429ea2b8544) - Linear editor, phone: [light](https://git.kayg.org/attachments/2a12a625-46be-48c5-9d8e-b4729adc6a58), [dark](https://git.kayg.org/attachments/bfd61de5-06db-47df-9e43-c5814f7013ac) - Angle tooltip, phone: [light](https://git.kayg.org/attachments/235f9711-f51f-4b57-a2d6-161717769b5d), [dark](https://git.kayg.org/attachments/8d410abb-cede-4139-b157-1af1cd6c731b) - Radial editor, phone: [light](https://git.kayg.org/attachments/fcfea6fc-2e25-4cdd-b96a-872066d31851), [dark](https://git.kayg.org/attachments/5bcf2eef-312c-42b4-b764-6109dceac45b) - Picture source, Photos: [light](https://git.kayg.org/attachments/1b078820-3219-4fb3-8750-e36afab087c4), [dark](https://git.kayg.org/attachments/79315a93-dd2e-4416-9934-d99c8601cf56) - Picture source, Upload: [light](https://git.kayg.org/attachments/8617fe92-0191-49d2-8b60-2d085d3b073a), [dark](https://git.kayg.org/attachments/6fd4ccb9-18a8-429c-9b35-b94682dce0a9) The production review script passed its interaction checks for stop add/move/delete/off-bar drag, angle adjustment, radial center movement, and icon/text alignment within 1px. ### Gates ```text cargo fmt --check (exit 0; no output) cargo clippy --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 10s cargo test 72 test result lines: all `test result: ok.` GATE_EXIT=0 bun run check $ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/appearance/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings bun run test Test Files 104 passed (104) Tests 690 passed (690) Start at 05:19:00 Duration 59.25s (transform 62%, import 14%, environment 14%, tests 8%, setup 3%) GATE_EXIT=0 ``` The first post-merge web test run found the Lotus contrast issue above. After the palette correction, the focused theme test passed (68/68) and the full web suite passed (690/690). No test expectation changed. ### Adversarial round One time-boxed round ran for 25 minutes and timed out during dedup probes. The direct oversized Appearance request returned `413` and the server stayed up; the test proxy reported a false `502`. Filed as [#283](https://git.kayg.org/kayg/calternal/issues/283). The attack2-only mode also reused users deleted by earlier probes and skipped restart coordination, invalidating its later dedup checks; filed as [#285](https://git.kayg.org/kayg/calternal/issues/285). Other completed probes showed no non-SLOW product failures; the adversarial round is incomplete because of the timeout. ### Decision The design doc did not prescribe the custom picker implementation. The shared custom color control uses the browser's native `input[type=color]` behind the styled swatch so keyboard focus and platform picker behavior remain available without adding a dependency.
Author
Owner

Correction to the report above: the adjusted #606078 muted color computes to 4.5897:1 on the quiet chrome wash; the previous #62627a value computed to 4.4529:1. The focused and full web test suites pass with the new value.

Correction to the report above: the adjusted `#606078` muted color computes to 4.5897:1 on the quiet chrome wash; the previous `#62627a` value computed to 4.4529:1. The focused and full web test suites pass with the new value.
kayg referenced this issue from a commit 2026-09-28 03:27:17 +00:00
Author
Owner

Merged in b8adafb9 (web 691/691, clippy clean, server 63/63; contract regenerated after the merge). Theme & UI (scheme, 16 families + variants, background colour/gradient editor/picture with Photos/upload/Unsplash + automatic scrim, per family+scheme), Text section, Accent removed. Unsplash search appears once an admin sets the key in Admin → Server configuration.

Merged in b8adafb9 (web 691/691, clippy clean, server 63/63; contract regenerated after the merge). Theme & UI (scheme, 16 families + variants, background colour/gradient editor/picture with Photos/upload/Unsplash + automatic scrim, per family+scheme), Text section, Accent removed. Unsplash search appears once an admin sets the key in Admin → Server configuration.
kayg closed this issue 2026-09-28 03:31:26 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#248
No description provided.