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
|
## Window kinds
|
||||||
|
|
||||||
`src/bun/windows.ts` — `buildPreload(mode, ctx?)` injects `__APP_PLATFORM`,
|
`src/bun/windows.ts` — `buildPreload(mode, settings, ctx?)` injects
|
||||||
`__APP_MODE` (`main|editor|manager`), `__APP_CTX` before any page script.
|
`__APP_PLATFORM`, `__APP_MODE` (`main|editor|manager`), `__APP_SKIN`
|
||||||
`ui/src/main.ts` picks the root component from `APP_MODE`; all three roots use
|
(`modern|win7`), `__APP_CTX` before any page script. `ui/src/main.ts` selects on
|
||||||
selector `app-root` (index.html hosts only that tag). `openSecondaryWindow` is
|
BOTH axes: the skin picks a component tree, `APP_MODE` picks a root within it
|
||||||
the ONLY place that branches platform for secondary chrome: macOS
|
(see "UI layout" below). All roots use selector `app-root` (index.html hosts
|
||||||
`hiddenInset` + `trafficLightOffset {x:12,y:13}` (the UI renders an
|
only that tag). Both globals are preload-baked because both are needed before
|
||||||
`h-10 pl-24` drag-region header via `IS_MAC_CHROME`); other platforms keep the
|
first paint — settings arrive over an async RPC, far too late to choose a tree
|
||||||
native titlebar and a compact `h-9` non-drag header.
|
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,
|
`WindowRegistry<Rec>` (same file): keyed window map with dedupe/activate,
|
||||||
cascade placement off an anchor frame, focus tracking. electrobun's emitter
|
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
|
records can be garbage collected. A future "window follows window" grouping
|
||||||
belongs here (`anchor.on('move')` + `setPosition`).
|
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
|
One window per file, key `sessionId+path` (sessionId, not fsKey: saves route
|
||||||
through a concrete session, and `local` is one shared session anyway).
|
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
|
rpc; manager/editors constructed after win; quit guard; focus tracking for
|
||||||
notification suppression across all windows).
|
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
|
- `rpc.service.ts` — wraps `Electroview.defineRPC`. **Push messages fan out
|
||||||
through plain callback registries, never signals**: a signal consumed from
|
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
|
`<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.
|
precompiled against a literal `.win7`, so the name is not ours to pick.
|
||||||
Default `modern` = class absent, matching the delete-when-default rule.
|
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
|
Unlike the other four axes, this one is not only CSS: it selects a whole
|
||||||
`@layer theme, base, components, utilities, skin, skinfix;` statement precedes
|
component tree (see "UI layout"). So a skin change is **two-phase** —
|
||||||
both imports. 7.css is imported with `layer(skin)`, i.e. **above `utilities`**,
|
`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 —
|
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
|
that inversion is what reskins ~1900 inline utility classes with no template
|
||||||
edits. Consequences: (1) any utility later added to a `button`/`input`/
|
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.
|
`utilities`, but from `skinfix` it lands back on 7.css and does nothing.
|
||||||
`skinfix` is for rules that state an explicit value (`.w7-btn`).
|
`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
|
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
|
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:
|
`@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
|
wrapper calls `gtk_window_set_decorated(false)` and logs "GTK: Created window
|
||||||
without decorations (custom titlebar)" (verified on GNOME/Wayland), so the
|
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
|
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`),
|
`.title-bar-text` + `.title-bar-controls`, buttons keyed by `aria-label`),
|
||||||
mounted by all three window roots. Because `titleBarStyle` is fixed at
|
mounted by all three window roots. Because `titleBarStyle` is fixed at
|
||||||
construction, **a skin change only reaches the frame on the next window open**;
|
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` /
|
- **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`);
|
`applyUpdate` (handlers in [rpc-handlers.ts](../src/bun/rpc-handlers.ts) call the electrobun `Updater`);
|
||||||
push message `updateStatus`.
|
push message `updateStatus`.
|
||||||
- **UI**: `UpdaterService` ([ui/src/app/updater.service.ts](../ui/src/app/updater.service.ts)) holds signals
|
- **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/tab-bar/tab-bar.ts))
|
(`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 →
|
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
|
"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
|
`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
|
- **The win7 skin layer outranks Tailwind utilities.** 7.css is imported
|
||||||
`layer(skin)`, ordered above `utilities`, so any utility you add to a
|
`layer(skin)`, ordered above `utilities`, so any utility you add to a
|
||||||
`button`/`input`/`select`/`textarea`/`table` is silently dead under that
|
`button`/`input`/`select`/`textarea`/`table` is silently dead under that
|
||||||
skin. If it matters, neutralize in the `@layer skin` block at the bottom of
|
skin. If it matters, neutralize in the `@layer skin` block in
|
||||||
styles.css — and note `revert-layer` only reaches `utilities` from **inside
|
`app/skins/7like/7like.css` — and note `revert-layer` only reaches
|
||||||
`skin`**; used from `skinfix` it falls back onto 7.css and is a no-op. This
|
`utilities` from **inside `skin`**; used from `skinfix` it falls back onto
|
||||||
bit once already.
|
7.css and is a no-op. This bit once already.
|
||||||
- `@import … layer(name)` works through `@tailwindcss/postcss` (it wraps the
|
- `@import … layer(name)` works through `@tailwindcss/postcss` (it wraps the
|
||||||
imported AST), but the **deep path is mandatory**: 7.css ships no `exports`
|
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
|
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
|
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.
|
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)
|
## The win7 skin (7.css)
|
||||||
|
|
||||||
- **Unlayered declarations beat every `@layer`, regardless of specificity.**
|
- **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
|
`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
|
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,
|
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
|
every platform; don't reintroduce the native one for "nativeness" on mac/win
|
||||||
without keeping a Linux path.
|
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 |
|
| 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 | "/" |
|
| 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
|
`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
|
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.
|
the default app, copy path) cross to bun, via the `shellItemAction` RPC.
|
||||||
|
|||||||
Reference in New Issue
Block a user