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:
+74
-15
@@ -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**;
|
||||
|
||||
@@ -118,8 +118,8 @@ Wired on top of the download host above. See the "Auto-updater" section in
|
||||
- **RPC** ([src/shared/rpc.ts](../src/shared/rpc.ts)): requests `checkForUpdate` / `downloadUpdate` /
|
||||
`applyUpdate` (handlers in [rpc-handlers.ts](../src/bun/rpc-handlers.ts) call the electrobun `Updater`);
|
||||
push message `updateStatus`.
|
||||
- **UI**: `UpdaterService` ([ui/src/app/updater.service.ts](../ui/src/app/updater.service.ts)) holds signals
|
||||
(`state`/`version`/`progress`); the tab-bar "update pill" ([tab-bar.ts](../ui/src/app/tab-bar/tab-bar.ts))
|
||||
- **UI**: `UpdaterService` ([ui/src/app/core/updater.service.ts](../ui/src/app/core/updater.service.ts)) holds signals
|
||||
(`state`/`version`/`progress`); the tab-bar "update pill" ([tab-bar.ts](../ui/src/app/skins/7like/tab-bar/tab-bar.ts))
|
||||
is one accent (`bg-primary`) control that announces → fills as a progress bar while downloading →
|
||||
"Restart to apply". `state` is `available → downloading → ready` (ready derived from the
|
||||
`downloadUpdate` response). App-menu "Check for Updates…" ([menu.ts](../src/bun/menu.ts)) routes a
|
||||
|
||||
+32
-5
@@ -31,10 +31,10 @@ touching the adjacent code.
|
||||
- **The win7 skin layer outranks Tailwind utilities.** 7.css is imported
|
||||
`layer(skin)`, ordered above `utilities`, so any utility you add to a
|
||||
`button`/`input`/`select`/`textarea`/`table` is silently dead under that
|
||||
skin. If it matters, neutralize in the `@layer skin` block at the bottom of
|
||||
styles.css — and note `revert-layer` only reaches `utilities` from **inside
|
||||
`skin`**; used from `skinfix` it falls back onto 7.css and is a no-op. This
|
||||
bit once already.
|
||||
skin. If it matters, neutralize in the `@layer skin` block in
|
||||
`app/skins/7like/7like.css` — and note `revert-layer` only reaches
|
||||
`utilities` from **inside `skin`**; used from `skinfix` it falls back onto
|
||||
7.css and is a no-op. This bit once already.
|
||||
- `@import … layer(name)` works through `@tailwindcss/postcss` (it wraps the
|
||||
imported AST), but the **deep path is mandatory**: 7.css ships no `exports`
|
||||
map and Tailwind's resolver reads only `style`/`mainFields`, so a bare
|
||||
@@ -54,6 +54,33 @@ touching the adjacent code.
|
||||
it** — not even in an HTML comment. A backtick in a comment terminates the
|
||||
literal and the errors point at the prose, not at the quoting.
|
||||
|
||||
## The two skin trees (`ui/src/app/skins/`)
|
||||
|
||||
- **`.win7` is not a name we may change.** `7.scoped.css` is precompiled with
|
||||
every selector prefixed by that literal class, so the skin id in
|
||||
`AppSettings` stays `"win7"` even though its folder is `7like/`. The folder
|
||||
names are cosmetic; the class and the persisted setting value are not.
|
||||
- **Reach `core/` through the `@core/*` alias, never a relative path.** Skin
|
||||
components sit at varying depths and get moved; the alias is what let 67
|
||||
imports survive two relocations untouched. A `../../core/...` path is a
|
||||
future merge conflict.
|
||||
- **`@font-face` `url()` resolves relative to the file that declares it.** When
|
||||
the win7 rules moved three directories deeper, `./fonts/...` would silently
|
||||
have 404'd — build still green, app just renders a fallback font. The fonts
|
||||
therefore live inside the skin that uses them.
|
||||
- **The `@layer` statement must stay the first statement in `ui/src/styles.css`.**
|
||||
It is one of only two rule types allowed above `@import`, and it is what sorts
|
||||
`skin` above `utilities`. Every `@import` (Tailwind and both skin sheets) must
|
||||
also precede all other rules.
|
||||
- **Duplicate selectors across skins are safe.** `app-file-panel` exists in both
|
||||
trees; standalone components resolve through their importer's `imports:`
|
||||
array, never a global registry, and the trees are never mounted together.
|
||||
- **The build DOES type-check components nothing imports.** ngtsc compiles the
|
||||
whole program, so a skin that is currently unreachable still fails
|
||||
`bun run build:ui` on a type or template error. Verified deliberately by
|
||||
injecting an error into an unreferenced file — worth knowing before assuming
|
||||
a green build proves nothing about the inactive skin.
|
||||
|
||||
## The win7 skin (7.css)
|
||||
|
||||
- **Unlayered declarations beat every `@layer`, regardless of specificity.**
|
||||
@@ -113,7 +140,7 @@ touching the adjacent code.
|
||||
`dist/api/bun/proc/linux.md` says so; the app also prints "Application menus are
|
||||
not supported on Linux" at boot). Because the UI suppresses the webview's own
|
||||
right-click menu, that left EVERY panel context menu dead on Linux — no error,
|
||||
nothing happens. The menu is plain HTML now (`ui/src/app/context-menu.ts`) on
|
||||
nothing happens. The menu is plain HTML now (`ui/src/app/skins/*/context-menu.ts`) on
|
||||
every platform; don't reintroduce the native one for "nativeness" on mac/win
|
||||
without keeping a Linux path.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ macOS is the primary target; Windows 11+ x64 and Linux x64 are **beta** targets
|
||||
| ssh-agent | `fs/sftp.ts` | $SSH_AUTH_SOCK | `\\.\pipe\openssh-ssh-agent` fallback | $SSH_AUTH_SOCK |
|
||||
| Paths in UI | `ui format.ts joinPath/isRootPath` | "/" | separator detected from the dir string; drive-root regex | "/" |
|
||||
|
||||
The **context menu is HTML on every platform** (`ui/src/app/context-menu.ts` +
|
||||
The **context menu is HTML on every platform** (`ui/src/app/skins/*/context-menu.ts` +
|
||||
`context-menu.service.ts`), not a native electrobun menu — see the Linux section
|
||||
below for why. Only the three shell actions it can't do itself (reveal, open with
|
||||
the default app, copy path) cross to bun, via the `shellItemAction` RPC.
|
||||
|
||||
Reference in New Issue
Block a user