Document the core/skins split

architecture.md gains a "UI layout: one core, two skin trees" section — the
folder map, the barrel contract that lets main.ts stay ignorant of both trees,
why duplicate selectors across trees are safe, and where a new rule belongs
(core / a skin / shared tokens). Its "Window kinds" section now describes
two-axis root selection, and the "Skins" section is no longer stale prose about
a deleted design: it covers the stylesheet split and the two-phase nature of a
skin change (CSS live, tree and frame at next window open).

gotchas.md gains a section for the split, all of it earned during the work:
`.win7` cannot be renamed because 7.scoped.css is precompiled against that
literal; reach core through @core/* rather than relative paths, which is what
let 67 imports survive two relocations; @font-face url() rebases when a
stylesheet moves and fails silently; the @layer statement must stay first;
duplicate selectors across trees are safe; and ngtsc type-checks components
nothing imports, so a green build does say something about the inactive skin.

Also corrected the component paths that moved under skins/, and the
updater.service.ts link that moved under core/. Checked every ../ link in
docs/ resolves.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-16 05:04:11 +02:00
co-authored by Claude
parent eafe426c8e
commit 5bbfc2f475
4 changed files with 109 additions and 23 deletions
+74 -15
View File
@@ -43,14 +43,18 @@ no-op functions, reassigned in `src/bun/index.ts` after everything exists:
## Window kinds
`src/bun/windows.ts` — `buildPreload(mode, ctx?)` injects `__APP_PLATFORM`,
`__APP_MODE` (`main|editor|manager`), `__APP_CTX` before any page script.
`ui/src/main.ts` picks the root component from `APP_MODE`; all three roots use
selector `app-root` (index.html hosts only that tag). `openSecondaryWindow` is
the ONLY place that branches platform for secondary chrome: macOS
`hiddenInset` + `trafficLightOffset {x:12,y:13}` (the UI renders an
`h-10 pl-24` drag-region header via `IS_MAC_CHROME`); other platforms keep the
native titlebar and a compact `h-9` non-drag header.
`src/bun/windows.ts` — `buildPreload(mode, settings, ctx?)` injects
`__APP_PLATFORM`, `__APP_MODE` (`main|editor|manager`), `__APP_SKIN`
(`modern|win7`), `__APP_CTX` before any page script. `ui/src/main.ts` selects on
BOTH axes: the skin picks a component tree, `APP_MODE` picks a root within it
(see "UI layout" below). All roots use selector `app-root` (index.html hosts
only that tag). Both globals are preload-baked because both are needed before
first paint — settings arrive over an async RPC, far too late to choose a tree
without flashing the wrong UI. `chromeOptions(skin)` is the ONLY place the
native frame is decided: win7 is frameless + transparent everywhere, modern
keeps the native titlebar (macOS `hiddenInset` + `trafficLightOffset
{x:12,y:13}`, where the shadlike tree renders an `h-10 pl-24` drag-region header
via `IS_MAC_CHROME`).
`WindowRegistry<Rec>` (same file): keyed window map with dedupe/activate,
cascade placement off an anchor frame, focus tracking. electrobun's emitter
@@ -58,7 +62,7 @@ has **no `off()`** — `track()` closures capture only `(this, key)` so closed
records can be garbage collected. A future "window follows window" grouping
belongs here (`anchor.on('move')` + `setPosition`).
### Editor windows (`src/bun/editor-windows.ts` + `ui/src/app/editor-window/`)
### Editor windows (`src/bun/editor-windows.ts` + `ui/src/app/skins/*/editor-window/`)
One window per file, key `sessionId+path` (sessionId, not fsKey: saves route
through a concrete session, and `local` is one shared session anyway).
@@ -167,7 +171,41 @@ would float over editor windows).
rpc; manager/editors constructed after win; quit guard; focus tracking for
notification suppression across all windows).
## UI services (`ui/src/app/`)
## UI layout: one core, two skin trees
```
ui/src/app/
core/ app.config.ts, icons.ts, platform.ts, format.ts, file-icons.ts,
and all 12 *.service.ts — everything skin-agnostic
skins/
shadlike/ the modern (shadcn-style) tree + shadlike.css
7like/ the Windows 7 tree + 7like.css + fonts/ + title-bar/
```
Each skin owns a **complete** component tree, including its own three window
roots, and exports a barrel (`SHAD_ROOTS` / `SEVEN_ROOTS`) with the same
`{ main, manager, editor }` shape. `main.ts` therefore knows nothing about
either tree's internals: `APP_SKIN` picks the barrel, `APP_MODE` picks the root.
Both trees are imported statically into one bundle — the inactive skin costs
bundle size but no runtime work, which keeps bootstrap synchronous.
Duplicate selectors across trees (`app-file-panel` exists in both) are safe:
standalone components resolve through their importer's `imports:` array, never
a global registry, and the two trees are never mounted together.
Only `7like` has a `title-bar/`. The modern skin defers to the native frame,
which is exactly what `IS_MAC_CHROME` accounts for.
**Why this split was cheap**: the service layer was already skin-agnostic —
`git diff main...feat/7css-skin` touched none of it. Components reach it through
the **`@core/*`** tsconfig alias (alongside `@shared/*` → `src/shared`), which
is deliberate: skin components sit at varying depths and an alias keeps their
imports identical wherever a file moves. Do not replace it with relative paths.
**Where a new rule goes**: shared behaviour → `core/`; markup and appearance for
one look → that skin's folder; tokens both skins consume → `ui/src/styles.css`.
## UI services (`ui/src/app/core/`)
- `rpc.service.ts` — wraps `Electroview.defineRPC`. **Push messages fan out
through plain callback registries, never signals**: a signal consumed from
@@ -277,10 +315,31 @@ A fifth theming axis, and the only one that is a **class** (`.win7` on
`<html>`) rather than a `data-*` attribute — `7.css/dist/7.scoped.css` is
precompiled against a literal `.win7`, so the name is not ours to pick.
Default `modern` = class absent, matching the delete-when-default rule.
`SettingsService.applySkin` owns the class once the page is live;
`buildPreload` writes it for the first paint.
**Cascade** (`ui/src/styles.css`, first lines): an explicit
`@layer theme, base, components, utilities, skin, skinfix;` statement precedes
both imports. 7.css is imported with `layer(skin)`, i.e. **above `utilities`**,
Unlike the other four axes, this one is not only CSS: it selects a whole
component tree (see "UI layout"). So a skin change is **two-phase** —
`applySkin` flips the CSS immediately, but the tree and the native frame are
both fixed at window construction (`main.ts` reads the preload-baked
`__APP_SKIN`; `titleBarStyle`/`transparent` are `BrowserWindow` constructor
options). The options row carries the reopen hint as its tooltip.
**Stylesheet split.** `ui/src/styles.css` holds only what BOTH skins consume —
the `@layer` statement, the Tailwind import, OKLCH tokens, `@theme inline`,
`data-text` sizes, animations, the accent axis, and the `html, body` base. Each
skin's own rules live in its folder (`skins/7like/7like.css`,
`skins/shadlike/shadlike.css`) and are `@import`ed from there. `shadlike.css` is
near-empty by nature: that skin IS the shared token system plus inline Tailwind.
Two constraints: the `@layer` statement must stay the FIRST statement in
`styles.css` (it is one of only two rule types allowed above `@import`), and
`@font-face` `url()`s resolve relative to the file that declares them — which is
why 7like's Noto Sans lives at `skins/7like/fonts/`, inside the skin.
**Cascade** (`ui/src/styles.css` first lines + `skins/7like/7like.css`): the
explicit `@layer theme, base, components, utilities, skin, skinfix;` statement
precedes every import. 7.css is imported with `layer(skin)`, i.e. **above
`utilities`**,
so a bare `button` rule in 7.css outranks `rounded-md` on that same button —
that inversion is what reskins ~1900 inline utility classes with no template
edits. Consequences: (1) any utility later added to a `button`/`input`/
@@ -290,7 +349,7 @@ that want the Tailwind value back must sit **inside `@layer skin`**, not
`utilities`, but from `skinfix` it lands back on 7.css and does nothing.
`skinfix` is for rules that state an explicit value (`.w7-btn`).
**Token bridge** (bottom of styles.css): `:root.win7` re-points the existing
**Token bridge** (`skins/7like/7like.css`): `:root.win7` re-points the existing
semantic tokens at Win7 values, so utilities Tailwind still controls come out
right too. It adds NO new tokens on purpose — a new colour would need another
`@theme inline` line. The Win7 surface ramp **inverts** the modern one:
@@ -366,7 +425,7 @@ only way to remove the macOS traffic lights. **Linux honours it too**: the GTK
wrapper calls `gtk_window_set_decorated(false)` and logs "GTK: Created window
without decorations (custom titlebar)" (verified on GNOME/Wayland), so the
frameless Aero chrome is not a mac/win-only feature. The webview then draws the whole
frame via `ui/src/app/title-bar/title-bar.ts` (7.css's `.title-bar` >
frame via `ui/src/app/skins/7like/title-bar/title-bar.ts` (7.css's `.title-bar` >
`.title-bar-text` + `.title-bar-controls`, buttons keyed by `aria-label`),
mounted by all three window roots. Because `titleBarStyle` is fixed at
construction, **a skin change only reaches the frame on the next window open**;