Skip to content

feat(theme): per-theme visual identity + desktop-OS chrome (#28) - #31

Merged
Xuepoo merged 13 commits into
mainfrom
carryctx/web-0034-visual-identity
Aug 25, 2026
Merged

Xuepoo merged 13 commits into
mainfrom
carryctx/web-0034-visual-identity

Conversation

@Xuepoo

@Xuepoo Xuepoo commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Closes #28.

Implements the approved theme-identity & desktop-OS-feel spec (webos-docs/specs/2026-08-24-theme-identity-and-os-feel.md), P0+P1.

What ships

  • P0: all 7 presets get distinct era token matrices (aero→Win11 Fluent, cloud→ChromeOS/Material, breeze→KDE Plasma, aqua→Mac OS X 10.3, y2k→Win98/2000 bevels+gradient, vaporwave→neon dark, dreamcore→pastel liminal) + identity contract test (pairwise 21-token matrix distinctness) + era wallpapers (R2, sha256-verified, inline-SVG fallback).
  • P1 app-side chrome: WebOSTaskbar (pinned/running indicators, tray cluster, two-line clock), WebOSStartMenu (search/grid/footer, y2k cascade), desktop context menu, boot splash (?nosplash escape hatch), era icon treatments, window shadow stacks + y2k bevels.
  • Supersedes fix(desktop): pin taskbar clock and clip entries against runtime reflow (#27) #29's structural guard: engine bar no longer mounted; clock repositioned from live bar width every tick/geometry change; entries clipped via tray-bounded host + clipChildren; dist contract ported to test/desktop/webos-taskbar.dist.test.ts.
  • Engine asks filed upstream (vectojs-docs 2fc127a): shadow primitive, inactive chrome pair, caption anatomy, lifecycle hooks, composition API, text glow, backdrop material.

Validation

137 pass / 0 fail · tsc clean · oxlint clean · oxfmt clean · build 417.10 kB (+10% vs main, wallpapers+chrome) · contrast floors hold (16 floor-forced deviations documented) · review round + 6-finding fix round complete (hex falloff, icon filter binding, splash hatch, distinctness test, seam typing).

Xuepoo added 13 commits August 25, 2026 02:58
… tokens

Apply the approved spec's §3.2 matrix to all 7 presets (aero/cloud/breeze/
aqua/y2k/vaporwave/dreamcore) and extend AppThemeTokens with the §3.3
era chrome tokens (menu-*, tray-bg, button-shape, chrome-font,
taskbar-height, window-shadow, inactive pair, bevel, pinstripe, glow).

- Category fix: aero Classic->Modern, aqua Modern->Classic (spec §5-P0)
- 11 values deviate from the spec matrix where the WEB-0023 contrast
  contract floors fail (hairline borders, muted rose, pressed-gray
  active); deviations recorded inline + DEC-0017/0018
- Contrast WORST baselines remeasured (2026-08-25, base dfebbe7)
- New per-theme identity contract test pins signatures, categories,
  era tokens and wallpaper art markers

Engine asks filed first in vectojs-docs 2fc127a (shadow, inactive
chrome, caption anatomy, lifecycle hooks, composition, text glow,
backdrop).

Refs #28
…odels

- chrome/shadow.ts: parseShadowToken for the composite
  desktop-window-shadow token + drawShadow stacking translucent rings
  (renderer blur is an upstream ask; finding filed first)
- chrome/bevels.ts: Win98 4-tone raised/sunken bevels + Aqua pinstripes
- model/clock-format.ts: per-era taskbar clock formats (modern two-line,
  aqua 'Mon 24 10:48', y2k '10:48 AM')
- model/start-menu-model.ts: app filter, recent-cap and y2k program groups

Refs #28
Replace the engine chrome through its public seams (spec §4 gaps #2-#4,

- WebOSTaskbar replaces shell.taskbar: Start tile, pinned launchers,
  running entries with era indicator (Fluent underline / aqua dock dot /
  y2k pressed bevel), tray cluster + sunken tray well, two-line clock
  repositioned on every tick (supersedes the WEB-0032 structural guard)
- WebOSStartMenu (search field, 6-col pinned grid, recent list,
  user+power footer) driven by an overridden toggleStartMenu; Escape and
  outside-click dismissal reimplemented; opener focus restoration
  preserved (PX-0077 contract); y2k era gets cascading program groups
  via ui ContextMenu instead (era-correct)
- Desktop right-click menu (Refresh/wallpaper/Display/About) themed by
  desktop-menu-* tokens
- Boot splash: 900ms era mark, non-projected, viewport-tracked so the
  audit overflow gate stays clean
- Icon treatments per §3.4 keyed by preset id; desktop icons re-skin
  live via setSVGSource on theme switch
- config boots with the era taskbar height so engine placement math
  agrees from the first mount

Smoke: park-focus helper finds role=button mirrors (chrome is now
entity-drawn). Engine-drawn titlebar hover/inactive/lifecycle gaps stay
deferred (DEC-0017).

Refs #28
…kbar

Rebase resolution for origin/main 95d7b63 (#27/#29): per carryctx DEC-0019
the WEB-0034 WebOS-owned taskbar supersedes the WEB-0032 structural clock
guard. The replacement bar owns both guarantees directly instead of pinning
engine internals:

- Clock placement: ClockView is repositioned from live bar width on every
  setGeometry/layout AND every tick (taskbar.ts updateClock), so the
  same-minute-resize stale-clock trap cannot recur.
- Entries clipping: entriesHost.clipChildren with a width bounded by the
  tray cluster edge; rebuild() stops placing entries past it.

Guarantees survive; the mirrored engine-private formulas and their dist
test are deleted. The dist-contract pattern is ported to the new seam:
webos-taskbar.dist.test.ts pins every member the engine touches on the
public shell.taskbar field (setGeometry(width,y), destroy+remount on
setTheme, dispose teardown, outside-click startButtonRight hit test)
against the installed @vectojs/desktop dist.

Changelog: merged the two Unreleased sections; #27 fix entry annotated as
now provided by the WebOS bar; guard retirement recorded under Removed.
…rs (#28)

Review F1: scaleAlpha only matched rgba() and the vaporwave glow path
built its falloff with a `.replace(')', ...)`, which never matches a hex
token - so #FF71CE glow overdraws and any hex shadow layer rendered at
full opacity instead of fading.

src/chrome/color.ts becomes the single source for token color math
(parseColor, scaleAlpha, glowStackColors, scaleHex): shadow.ts scales
ring alpha through it, the start-menu wordmark builds its 0.25/i halo
via glowStackColors, and the taskbar clock imports scaleHex instead of
keeping a private copy. Regression tests pin hex-shadow falloff and the
shipped vaporwave glow stack; two dead `void` statements in start-menu
go with it (F6 nit).
…28)

Review F2: the Material-era feDropShadow filter was referenced from a
zero-size invisible carrier rect, so it rendered nothing and Material
icons shipped untreated - while the era-distinctiveness test counted
the inert defs substring as evidence of treatment.

The treatment now wraps the base glyph in a visible filter-bearing
group (new `wrap` slot in IconTreatment), and the identity test asserts
the filter attribute sits on a group containing the painted path with
no zero-size carrier left behind.
#28)

Review F6: the shell.taskbar assignment was cast through
`{ taskbar: unknown }`, bypassing assignability on the public seam. A
local minimal TaskbarLike interface (setGeometry, startButtonRight,
bounds) now types the cast, so WebOSTaskbar must satisfy the surface
the engine actually drives; webos-taskbar.dist.test.ts keeps pinning
the runtime contract.

Also removes the dead `void` statements flagged alongside: the unused
spec param on PinnedTile and era field on ClockView (params dropped at
both call sites) and the unused theme lookup in context-menu items.
…Id (#28)

Review F4: every load paid 900ms mark + 220ms fade with no way to skip
it for tests or perf benches. Following the established ?debug
query-param convention, ?nosplash now skips the splash entirely; the
boot smoke boots under it and asserts no Splash entity mounts.

The per-era art gap is made explicit rather than implied: Splash took a
presetId and voided it (all eras share one four-pane mark, palette-only
variation), so the param is gone until the deferred artwork lands
(carryctx DEC-0022; webos-docs TODO Deferred).
…#28)

Review F5: era-distinctiveness was asserted via description strings
only, so two presets shipping identical 21-token matrices would pass.
The identity contract now also requires the full engine-token matrix to
differ pairwise across all 7 presets - a real distinctness guarantee
that fails loudly if a preset ever collapses onto another era.
User-visible outcomes of the fix round: ?nosplash escape hatch and the
actually-rendering Material icon drop shadow (Added), hex-token alpha
falloff for glow/elevation layers (new Fixed entry).
…switch

applyTheme updated config.desktop.taskbarHeight (drives bar remount and
placement) but never DisplayLayout, whose setTaskbar() is the only knob
behind workArea()/clampRect(). After switching to an era with a different
bar height the painted bar and the engine's usable area disagreed: windows
clamped against the boot-time floor overlapped a taller bar or stranded
above a shorter one (review PX-0163).

applyTheme now calls shell.layout.setTaskbar() alongside the config write
and re-clamps windows to the new work area; dist-contract test pins the
setter against upstream renames. (#28)
@Xuepoo
Xuepoo merged commit 8689e79 into main Aug 25, 2026
4 checks passed
Xuepoo added a commit that referenced this pull request Aug 25, 2026
…rk-area change (#30) (#34)

* fix(desktop): shrink-clamp escaping windows and refit maximized on work-area change (#30)

Viewport/DPR changes stranded windows outside the reachable work area
(audit WEB-0038). Three compensations for engine gaps filed upstream in
vectojs-docs forge/findings (commit 794340f):

- clampRect in model/window-geometry mirrors the engine's
  DisplayLayout.clampRect shrink-then-pin semantics (with optional min
  floors): a position-only clamp left windows taller/wider than a
  shrunken work area stranded past the bottom/right forever (4/5 windows
  outside at 568x315).
- fit() re-applies maximize after shell.resize so maximized windows track
  the new work area instead of keeping their stale pre-shrink box (E1,
  measured 1280x760 in a 568x355 viewport).
- every window-manager event re-runs the clamp so restore()'s stale
  pre-maximize box is pulled back inside immediately (E2).

* docs(changelog): note window work-area stranding fixes (#30)

* fix(desktop): gate stream re-clamp to state events; test zero-height area (#30)

Review round PX-0159..0163 on WEB-0038:

- Restrict the window-manager stream clamp to "state" events
  (clampWindowsOnEvent): engine drag deliberately parks windows mostly
  off-screen (titlebar + 48px visible, Window.clampMovePosition), so an
  unrelated open/focus/close event must not yank them back on-screen.
  Restore/un-minimize still emit "state" and keep the E2 stale-box
  correction.
- Cover the zero-height work-area edge: clampRect floors to a 1px box
  pinned at area.y, the documented DEC-0021 app-side deviation from the
  engine's applyGeom re-flooring.
- Dist contract: pin the drag-parking permission so an engine change
  that forbids parking surfaces here before the event gate is relaxed.

Rebased onto origin/main 8689e79 (#31): guardTaskbar dropped (the
WebOS-owned taskbar self-subscribes to the wm stream), refitMaximized
kept in fit(), and the per-era setTaskbar sync composes with the
work-area clamp unchanged (applyTheme re-clamps after setTaskbar).
Restores oxfmt-canonical single-quote formatting in main.ts and
window-geometry.test.ts after an external editor session re-saved them
with Biome defaults.
Xuepoo added a commit that referenced this pull request Aug 25, 2026
…nned (#32) (#38)

* fix(notes): open persisted documents with their saved content

The open path never read the VFS: create() seeded the editor with the
welcome copy and only the click-driven reloadFromVfs() ever called
vfs.read. After a full page reload StorageVfs had already replayed the
snapshot (Files listed the document), yet a freshly opened Notes window
rendered seed text until the user clicked Reload — confirmed E2E
cross-engine on the previous deploy (PX-0099/PX-0108).

Opening now restores persisted content immediately when the
deterministic target document (/notes/note-N.txt) already exists. The
restore stands down if the user typed while stat/read were in flight,
so async loading can never clobber live edits; read errors keep the
seed text and stay on the Reload path's status reporting.

Regression test reproduces the defect through the real boot: write the
next note's document into the restored VFS, shell.open('notes'), assert
the editor value (red-checked against the unfixed tree). (#32)

* test(desktop): pin start-menu dismissal focus contract, drop dead keys shim

Re-derived WEB-0035 defect B against the post-rewrite architecture: the
opener-focus restore survived PR #31 reimplemented inline in main.ts
(closeStartMenu captures the opener at open and hands focus back when
dismissal leaves it stranded on body). Escape, toggle and click-outside
closes all run through that path.

Add the missing outside-click close-path regression to the smoke suite,
following the established post-prune focus pattern (overlay mirrors are
not reliably projected under happy-dom).

Remove src/desktop/start-menu-keys.ts: the pre-rewrite keyboard shim
wrapped shell.toggleStartMenu for the engine StartMenu entity and owned
the old restore logic; PR #31 stopped installing it (was main.ts:324)
leaving zero references while its header still claimed ownership of
focus restoration — exactly the drift this module list warns about.
roving-focus.ts stays: pure tested util, not tied to the dead wiring. (#32)

* chore(changelog): record WEB-0035 defect fixes under Unreleased

* fix(desktop): refocus Start when a theme switch strands the menu opener

applyTheme rebuilds the WebOSTaskbar before closeStartMenu runs, so an
opener captured while the menu was open is already disconnected at
restore time and focus() on the detached node silently no-ops — Escape,
outside click and launch dismissal all strand DOM focus on body after a
theme switch (review F3, PR #31 leftover).

Re-validate the captured opener at restore time: keep the immediate
focus() while it is still connected, otherwise defer focus to the live
bar's Start tile mirror one sync pass out — rebuilt mirrors are
projected by the next a11y sync pass, not on scene.add. The deferred
restore yields if anything else took focus meanwhile, matching the
immediate path's contract.

WebOSTaskbar gains a startButton accessor for the fallback target. The
regression test drives applyTheme with a captured opener and pins focus
on the rebuilt bar's Start mirror; verified red without the fix.

webos#36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Visual identity & desktop-OS feel: per-theme token matrices + chrome anatomy (WEB-0034)

1 participant