From 5bbfc2f475e3bc8525d2264075d46ad2bc619010 Mon Sep 17 00:00:00 2001 From: Jokin Date: Sun, 16 Aug 2026 05:04:11 +0200 Subject: [PATCH] Document the core/skins split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/architecture.md | 89 ++++++++++++++++++++++++++++++++------- docs/build-and-release.md | 4 +- docs/gotchas.md | 37 +++++++++++++--- docs/platform-notes.md | 2 +- 4 files changed, 109 insertions(+), 23 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 59999e7..ba685d4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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` (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 ``) 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**; diff --git a/docs/build-and-release.md b/docs/build-and-release.md index 25f8e0a..de5a6f9 100644 --- a/docs/build-and-release.md +++ b/docs/build-and-release.md @@ -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 diff --git a/docs/gotchas.md b/docs/gotchas.md index 1ebac3e..a7289f1 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -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. diff --git a/docs/platform-notes.md b/docs/platform-notes.md index 8c2e6cb..918ca9b 100644 --- a/docs/platform-notes.md +++ b/docs/platform-notes.md @@ -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.