Remove the Windows 7 (7.css) skin
The win7 skin is dropped. Everything else the branch built stays: Linux support and the HTML context menus, the workspace-tab menu, the continuous interface scale (rem everywhere, stepper, Ctrl/Cmd +/-/0) and the core/ service layer. The modern tree already carried all of them, and nothing outside skins/7like/ imported the win7 tree except main.ts and styles.css. Removed: the skins/7like/ tree (5.1k lines, fonts), the 7.css dependency, the skin setting and its options row + strings in all 7 locales, the frameless-window plumbing (window-control RPCs, windowState push, trackMaximizeState, the synthesized resize grip and its postinstall watchdog), the session-skin pinning in buildPreload/chromeOptions, the skin/skinfix cascade layers, and the win7-only probes and screenshot harness (the harness cannot render the modern tree: plain Firefox has no RPC bridge). chromeOptions() stays as the one shared frame decision. Fixed on the way out: - index.html's per-theme pre-paint background is restored. The branch had replaced it with a win7-only transparent rule, so modern dark windows had silently lost one of the three flash guards. - editor-windows.ts wrote its window-key separator as a literal NUL byte, which made git and grep treat the file as binary; it is `\0` now (same string at runtime). No settings migration: a stale "skin" key is carried harmlessly by the store's spread-merge and dropped on the next save. UI bundle: 616.7 kB -> 405.4 kB, back under the 500 kB budget. Docs: architecture, gotchas and platform-notes lose the win7 material but keep what stays true (construction-time preloads/frames, drag regions ignoring preventDefault, electrobun's missing resize/maximize primitives); flash-prevention, dev-URL and Bun-panic notes move back under Electrobun; stale context-menu/tab-bar links fixed; README documents the scale keys. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+30
-234
@@ -44,17 +44,15 @@ no-op functions, reassigned in `src/bun/index.ts` after everything exists:
|
||||
## Window kinds
|
||||
|
||||
`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`).
|
||||
`__APP_PLATFORM`, `__APP_MODE` (`main|editor|manager`), `__APP_CTX` before any
|
||||
page script, plus the persisted theme attributes and interface scale so the
|
||||
first paint is already right. `ui/src/main.ts` picks the root component from
|
||||
`APP_MODE`; all three roots use selector `app-root` (index.html hosts only that
|
||||
tag). `chromeOptions()` is the ONLY place the native frame is decided, spread by
|
||||
the main window and `openSecondaryWindow`: 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.
|
||||
|
||||
`WindowRegistry<Rec>` (same file): keyed window map with dedupe/activate,
|
||||
cascade placement off an anchor frame, focus tracking. electrobun's emitter
|
||||
@@ -62,7 +60,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/skins/*/editor-window/`)
|
||||
### Editor windows (`src/bun/editor-windows.ts` + `ui/src/app/skins/shadlike/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).
|
||||
@@ -171,39 +169,23 @@ would float over editor windows).
|
||||
rpc; manager/editors constructed after win; quit guard; focus tracking for
|
||||
notification suppression across all windows).
|
||||
|
||||
## UI layout: one core, two skin trees
|
||||
## UI layout
|
||||
|
||||
```
|
||||
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/
|
||||
core/ app.config.ts, icons.ts, platform.ts, format.ts, file-icons.ts,
|
||||
metrics.ts and every *.service.ts — the non-visual layer
|
||||
skins/shadlike/ the component tree: three window roots (SHAD_ROOTS barrel,
|
||||
picked by main.ts), dialogs, context menu
|
||||
```
|
||||
|
||||
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.
|
||||
Components reach `core/` through the **`@core/*`** tsconfig alias (alongside
|
||||
`@shared/*` → `src/shared`), which is deliberate: components sit at varying
|
||||
depths and an alias keeps their imports identical wherever a file moves. Do not
|
||||
replace it with relative paths.
|
||||
|
||||
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`.
|
||||
**Where a new rule goes**: behaviour → `core/`; markup and appearance → the
|
||||
component; tokens → `ui/src/styles.css`.
|
||||
|
||||
## UI services (`ui/src/app/core/`)
|
||||
|
||||
@@ -235,8 +217,8 @@ one look → that skin's folder; tokens both skins consume → `ui/src/styles.cs
|
||||
enqueue and runs the collision flow (same-path auto-skip, type-mismatch
|
||||
skip, overwrite dialog with all/none batch modes).
|
||||
- `settings.service.ts` — one signal per setting; **imperative apply pattern**
|
||||
(`applyTheme/DarkTone/Accent/TextSize` set `data-*` attributes on
|
||||
`<html>`); `applyAll` also serves the `settingsChanged` broadcast so editor
|
||||
(`applyTheme/DarkTone/Accent` set `data-*` attributes on `<html>`;
|
||||
`applyUiScale` sets its inline `font-size`); `applyAll` also serves the `settingsChanged` broadcast so editor
|
||||
and manager windows restyle live (they render no settings UI, so no
|
||||
persist echo). New setting = five touches here + types.ts (backend merges
|
||||
automatically).
|
||||
@@ -256,7 +238,7 @@ one look → that skin's folder; tokens both skins consume → `ui/src/styles.cs
|
||||
boot and on the `settingsChanged` broadcast (so editor/manager windows follow).
|
||||
- `file-actions.service.ts` — toolbar/keyboard/context-menu file ops, always
|
||||
against an explicit panel; `menuTarget` remembers the right-clicked row.
|
||||
- `context-menu.service.ts` + `context-menu.ts` — the panel and tab right-click
|
||||
- `context-menu.service.ts` + `skins/shadlike/context-menu.ts` — the panel and tab right-click
|
||||
menus, in HTML. Electrobun's native `showContextMenu` is a no-op on Linux, so it was
|
||||
retired on ALL platforms rather than kept as a second code path (see
|
||||
platform-notes.md). The service builds the item list (row / empty-space menu,
|
||||
@@ -274,11 +256,10 @@ one look → that skin's folder; tokens both skins consume → `ui/src/styles.cs
|
||||
`disconnectWorkspace`. **The target id travels inside the action string**: the
|
||||
right-clicked tab is not necessarily the active one, so nothing on that path
|
||||
may fall back to `activeWorkspaceId`. Illegal entries are `disabled`, not
|
||||
omitted — Win7 greys out, and a menu whose items move between right-clicks is
|
||||
worse than one with a dead row. 7.css hangs that state on
|
||||
`li[role=menuitem][aria-disabled]` (opacity + `pointer-events: none`), which
|
||||
the keyboard path never consults — so `pick()` and the arrow-key walker check
|
||||
the flag themselves, in both skins.
|
||||
omitted — a menu whose items move between right-clicks is worse than one with
|
||||
a dead row. The disabled styling (`pointer-events: none`) only stops the
|
||||
pointer; the keyboard path never consults it — so `pick()` and the arrow-key
|
||||
walker check the flag themselves.
|
||||
- `file-icons.ts` — extension → category map driving glyphs, colors
|
||||
(fixed `--icon-*` tokens, never accent-tinted) and the Kind column.
|
||||
- `platform.ts` — preload-global readers (`PLATFORM`, `APP_MODE`,
|
||||
@@ -319,194 +300,9 @@ toolbars deliberately stay on `--background`. Accent tones: one
|
||||
pixel-identical to the old neutral hover. **Interface scale** (`settings.uiScale`,
|
||||
0.5–4 in 0.1 steps): one root font-size lever — `applyUiScale` sets
|
||||
`html { font-size: 16px * scale }`, and since EVERY size in the app is rem
|
||||
(panel tokens like `--row-h`, all Tailwind utilities, ng-icon sizes, `--w7-fs`
|
||||
and the 7.css bridge in 7like.css), both skins scale whole, chrome included.
|
||||
(panel tokens like `--row-h`, all Tailwind utilities, ng-icon sizes), the
|
||||
whole UI scales, chrome included.
|
||||
Driven by the tab-bar stepper, Ctrl/Cmd +/−/0, and preload-baked for a
|
||||
flash-free first paint. It replaced the 3-tier `data-text` axis; the store
|
||||
migrates old `textSize` values. TS that measures rem-sized CSS converts through
|
||||
`@core/metrics` `remPx()` — never an assumed 16.
|
||||
|
||||
### Skins (`settings.skin`: `modern | win7`)
|
||||
|
||||
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.
|
||||
`buildPreload` is the **only** writer of that class, ever — there is
|
||||
deliberately no `SettingsService.applySkin`.
|
||||
|
||||
**Unlike the other four axes, this one is launch-time only.** It selects a
|
||||
whole component tree (see "UI layout") as well as CSS, and neither half can
|
||||
change on a live window: the tree is bootstrapped once from the preload-baked
|
||||
`__APP_SKIN`, and `titleBarStyle`/`transparent` are `BrowserWindow` constructor
|
||||
options with no setters. Clicking a skin therefore only persists it; the
|
||||
options row shows the saved value as pending (`skinPending()` compares
|
||||
`settings.skin()` against `APP_SKIN`) and the running UI is untouched until the
|
||||
next app start.
|
||||
|
||||
The bun side pins this: `initSessionSkin` freezes the skin at startup, before
|
||||
any window exists, and both `buildPreload` and `chromeOptions` read that pinned
|
||||
value rather than the live settings snapshot. Otherwise an editor or manager
|
||||
opened after a pending change would boot the *new* tree and frame while the
|
||||
main window still ran the old one.
|
||||
|
||||
**Stylesheet split.** `ui/src/styles.css` holds only what BOTH skins consume —
|
||||
the `@layer` statement, the Tailwind import, OKLCH tokens, `@theme inline`,
|
||||
panel-chrome tokens, 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`/
|
||||
`select`/`textarea`/`table` silently loses under the skin; (2) neutralizers
|
||||
that want the Tailwind value back must sit **inside `@layer skin`**, not
|
||||
`skinfix` — `revert-layer` rolls back one layer, so from `skin` it lands on
|
||||
`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** (`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:
|
||||
`--background` (content panes) is Explorer white and `--chrome` is the darker
|
||||
`#f0f0f0` window surface. `--radius: 5px` so `rounded-md`
|
||||
(`calc(--radius - 2px)`, 74 uses) lands exactly on 7.css's 3px `--w7-el-bdr`.
|
||||
`--primary`/`--ring` are deliberately left alone so the accent axis stays live;
|
||||
`:root.win7[data-accent]` feeds it to `--w7-w-bg`, which is authentic (Win7
|
||||
shipped "Window Color"). Dark Win7 is hand-authored (Microsoft never shipped
|
||||
one) as ~20 `--w7-*` overrides, duplicated forced + system like every other
|
||||
dark block. `--w7-font` is restated as a rem `font` shorthand so the interface
|
||||
scale reaches it and 7.css's `pt` shorthand stops resetting weight/line-height;
|
||||
the rest of 7.css's px metrics are re-expressed in rem by the scale bridge in
|
||||
7like.css's `skinfix`.
|
||||
|
||||
**Markup hooks**: `role="progressbar"` (+aria) on the transfer bars,
|
||||
`has-scrollbar` on scroll containers (7.css only styles `::-webkit-scrollbar`
|
||||
under that class), `w7-toolbar` on every command row — the three window header
|
||||
rows plus each file panel's session strip and path bar (their buttons go flat
|
||||
with a hover glow — Explorer's command bar was flat, unlike dialog push
|
||||
buttons; it only targets `button`, so the path `<input>` keeps its 7.css
|
||||
textbox treatment), `w7-flat`
|
||||
as the per-button escape hatch for the same treatment outside a toolbar (the
|
||||
status-bar transfers summary is a text toggle, not a push button), `w7-btn`
|
||||
on the 10 dialog action buttons. The flat treatment is split across two
|
||||
layers on purpose: `background: revert-layer` must sit in `skin` to reach
|
||||
`utilities`, while `border-color`/`box-shadow` sit in `skinfix` because 7.css
|
||||
restates `border-color` at (0,3,1) and no sane `.w7-flat` selector outranks
|
||||
that.
|
||||
|
||||
**Tabs**: the workspace strip is 7.css's real tab component —
|
||||
`menu[role=tablist]` + one `button[role=tab][aria-selected]` per workspace, so
|
||||
the framework draws the joined inactive tabs and the raised active one. The
|
||||
selector is a DESCENDANT (`menu[role=tablist] button`), so the `+` button, the
|
||||
update pill and the right-hand tools deliberately sit outside the `<menu>`.
|
||||
Tabs are exempted from the `w7-toolbar` flattening via `:not([role='tab'])` —
|
||||
they want the full Aero slab their neighbours are stripped of. There is no
|
||||
per-tab ✕ (Win7 tab controls had none, and a nested button is invalid HTML):
|
||||
closing is `Ctrl/Cmd+W` or middle-click. `role="tabpanel"` on the panel area
|
||||
was considered and rejected — its `padding: 14px`, `margin-bottom: 9px` and
|
||||
border fight the split-pane layout for a join that cannot read anyway with the
|
||||
panel toolbar row in between.
|
||||
|
||||
**Frame transparency**: the rounded frame only reads as rounded because
|
||||
nothing paints an opaque full-bleed ground behind it. That is not free —
|
||||
`html, body` paint `var(--background)` **unlayered**, which beats every
|
||||
`@layer`, so `.win7` resets them to transparent in an equally unlayered rule
|
||||
next to the `font-family` one. A maximized window drops the radius (7.css
|
||||
declares it three times: `.window`, `::before`, `::after` — all three must be
|
||||
zeroed); the frame reads the state off its own child with
|
||||
`:has(> .title-bar.is-maximized)`, which `title-bar.ts` publishes from its
|
||||
existing `maximized` signal rather than subscribing to `windowState` twice.
|
||||
|
||||
**Selection band**: the file list's selected row is a FLAT Win7-Explorer blue
|
||||
(`--w7-sel-bg`, `#cbe8f6`) with **no border** — not 7.css's `--w7-el-grad-h`,
|
||||
which is its Aero element-hover ramp and stays reserved for buttons and tabs
|
||||
that still want the gloss, and not its `#aaddfa` row outline, which stacked a
|
||||
second edge inside the fill and, with the cursor row's dotted rect, drew three
|
||||
concentric outlines on one row. Every row still reserves `border: 1px solid
|
||||
transparent` for geometry; the fill paints under it (`background-clip`
|
||||
defaults to `border-box`). The unfocused pane gets `--w7-sel-bg-idle`,
|
||||
Explorer's grey inactive selection — the band never disappears with focus,
|
||||
only weakens (the commander convention above). Both are dark-overridden in
|
||||
BOTH dark blocks. The context menu's item highlight uses the same
|
||||
`--w7-sel-bg`, so selection reads identically wherever it appears.
|
||||
|
||||
**Context menu**: pure 7.css via its real menu contract —
|
||||
`ul[role=menu] > li[role=menuitem] > button` for actions,
|
||||
`input[type=checkbox] + label` for the checkable column toggles, dividers as
|
||||
`.has-divider` on the PRECEDING item (an `::after` bevel, not a node). The
|
||||
contract is exact: an earlier iteration rendered `div > button`, matched
|
||||
nothing, and wrongly concluded "7.css has no menu component" — it has a full
|
||||
one (`dist/gui/menu.css`), including the gutter stripe, disabled state and
|
||||
submenu arrows. Keyboard moves REAL focus between items (7.css styles
|
||||
`:focus-visible` like `:hover`); Enter/Space activate buttons natively. Only
|
||||
menu.css's hardcoded light literals are restated for dark, in `skinfix` — plus
|
||||
three overrides keyed on the app's own `w7-ctx` marker: `position: fixed`
|
||||
(7.css's `position: relative` on `ul[role=menu]` had been silently winning),
|
||||
the flat `--w7-sel-bg` highlight in place of its gradient wash, and
|
||||
`content: none` on the item buttons' pseudo-elements, which kills both the
|
||||
1s Aero afterglow and the infinite `pulse-anim` on a focused item (see
|
||||
gotchas.md). `scripts/menu-probe.ts` regression-tests placement AND highlight
|
||||
flatness, each with a control that proves the check can fail.
|
||||
|
||||
**Font**: Segoe UI is absent off Windows and not redistributable. Noto Sans —
|
||||
the substitute 7.css itself names — is vendored (OFL-1.1, from
|
||||
`@fontsource/noto-sans`) into `ui/src/fonts/`; under `src/` and not `public/`
|
||||
so esbuild emits it hashed into `media/` and rewrites the `url()`.
|
||||
|
||||
**Window chrome**: `chromeOptions(skin)` in `src/bun/windows.ts` is the single
|
||||
place either skin's frame is decided, spread by both the main window and
|
||||
`openSecondaryWindow`. `win7` uses `titleBarStyle: "hidden"` on **every**
|
||||
platform — that clears `Titled` from the Cocoa style mask, which is also the
|
||||
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/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 of that skin's window roots. Because `titleBarStyle` is
|
||||
fixed at construction, **a skin change only reaches the frame on the next app
|
||||
start** — and nothing about the skin flips live, deliberately. `IS_WIN7_CHROME` in
|
||||
`platform.ts` reads the preload-baked `__APP_SKIN` (settings resolve too late
|
||||
for layout) and forces `IS_MAC_CHROME` false, since there are no traffic
|
||||
lights to reserve `pl-24` for.
|
||||
|
||||
**Window resize (STOPGAP)**: undecorating the window also removes the WM's
|
||||
resize borders, and electrobun 1.18.1 has no native resize gesture — so
|
||||
`skins/7like/window-resize/` synthesizes one: a bottom-right grip drags the
|
||||
frame through `windowGetFrame`/`windowSetFrame` → `BrowserWindow.setFrame`.
|
||||
Mounted by all three win7 roots, hidden while maximized by a CSS `:has()` rule
|
||||
(no second `windowState` subscription), and the grip carries
|
||||
`electrobun-webkit-app-region-no-drag` or the drag region would move the window
|
||||
instead. **One corner, not eight handles**: the handles are invisible and paint
|
||||
above the frame, so a north strip or top corner overlays the caption buttons and
|
||||
steals their `:hover`, which reads as the Aero glow flickering on those buttons.
|
||||
The bottom-right corner is the only one that overlaps nothing (it sits in the
|
||||
6px gutter `.window-body`'s margin leaves). The frame math is a dependency-free module so
|
||||
`scripts/resize-math-probe.ts` can assert all eight edges without a window.
|
||||
**This is meant to be deleted**: the component header carries the removal
|
||||
checklist and `scripts/check-electrobun-resize.ts` (wired into `postinstall`)
|
||||
prints it when upstream ships the primitive. See platform-notes.md.
|
||||
|
||||
**Window controls**: new `windowMinimize` / `windowToggleMaximize` /
|
||||
`windowClose` / `windowIsMaximized` requests, built by
|
||||
`windowControlHandlers(getWindow, onClose?)` in `rpc-handlers.ts` and served by
|
||||
all three window kinds (each passing a late-bound getter for its own window —
|
||||
the window takes the rpc in its constructor, same reason `events` is
|
||||
late-bound). `onClose` is overridden per window kind so the X reuses the
|
||||
existing close path: editors run their dirty prompt, the manager runs
|
||||
`managerClose`, main falls through to the cancellable `before-quit` guard.
|
||||
electrobun emits **no maximize event**, so `trackMaximizeState` derives it from
|
||||
`resize` + the synchronous `isMaximized()` and pushes `windowState` — which
|
||||
also covers Aero Snap and Win+Up. Double-click-to-maximize is likewise absent
|
||||
from electrobun's drag regions and is implemented in the title-bar component.
|
||||
|
||||
@@ -119,7 +119,7 @@ Wired on top of the download host above. See the "Auto-updater" section in
|
||||
`applyUpdate` (handlers in [rpc-handlers.ts](../src/bun/rpc-handlers.ts) call the electrobun `Updater`);
|
||||
push message `updateStatus`.
|
||||
- **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))
|
||||
(`state`/`version`/`progress`); the tab-bar "update pill" ([tab-bar.ts](../ui/src/app/skins/shadlike/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
|
||||
|
||||
+60
-228
@@ -28,17 +28,6 @@ touching the adjacent code.
|
||||
- Tailwind v4 canonical class names: `wrap-break-word` not `break-words`,
|
||||
`accent-primary` not `accent-[var(--primary)]`, `z-60` not `z-[60]`.
|
||||
Every color used in a template must resolve through `@theme inline`.
|
||||
- **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 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
|
||||
`@import "7.css"` resolves to nothing. Use `7.css/dist/7.scoped.css`.
|
||||
- Stylesheet `url()` must resolve at BUILD time: assets referenced from
|
||||
`styles.css` live under `ui/src/` (esbuild emits them hashed into `media/`
|
||||
and rewrites the url), NOT `ui/public/` — a public path fails the build with
|
||||
@@ -53,6 +42,18 @@ touching the adjacent code.
|
||||
- **A component `template:` is a TS template literal, so no backticks inside
|
||||
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.
|
||||
- **Reach `core/` through the `@core/*` alias, never a relative path.**
|
||||
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.
|
||||
- **The build DOES type-check components nothing imports.** ngtsc compiles the
|
||||
whole program, so an unreferenced component still fails `bun run build:ui` on
|
||||
a type or template error (verified by injecting an error into an
|
||||
unreferenced file).
|
||||
- **`overflow-x: auto` also clips vertically.** CSS refuses to pair `visible`
|
||||
with a non-visible axis, so the computed `overflow-y` becomes `auto` too:
|
||||
anything poking out of a horizontally scrolling strip (a raised tab, an
|
||||
underline at `-bottom-px`) is silently clipped. Reserve padding instead.
|
||||
|
||||
## The interface scale (root font-size lever)
|
||||
|
||||
@@ -66,137 +67,16 @@ touching the adjacent code.
|
||||
JSDoc's "(WebKit only)" means macOS WKWebView. This is why the scale is CSS.
|
||||
- **Everything the UI sizes with must be rem.** The scale works by setting the
|
||||
root font-size, so a px value freezes while everything around it grows. The
|
||||
three px reservoirs were: the panel-chrome tokens (`--row-h` etc.), 93
|
||||
`text-[11px]` arbitraries, and 7.css itself — 370 px values, zero rem, with
|
||||
`--w7-font` reaching only 15 rules' typography. 7like.css's `skinfix` carries
|
||||
a scale bridge re-expressing 7.css's vars and hardcoded box metrics in rem.
|
||||
1px hairlines and focus rects stay px deliberately (crisp at any scale; the
|
||||
`tr` border exists to stop selection shifting rows).
|
||||
- **Restating a 7.css rule in `skinfix` also outranks 7.css's VARIANTS of that
|
||||
rule — by layer, regardless of specificity.** The title-bar-controls
|
||||
restatement silently shrank the close button from 48px to 29px (7.css widens
|
||||
it via `button[aria-label=Close]`), and the tablist padding flattened the
|
||||
active tab's 4px raise. Caught only by the harness pixel diff. When bridging
|
||||
a rule, grep 7.scoped.css for every variant that overrides the same
|
||||
properties and restate those too.
|
||||
px reservoirs were the panel-chrome tokens (`--row-h` etc.) and 93
|
||||
`text-[11px]` arbitraries. 1px hairlines and focus rects stay px deliberately
|
||||
(crisp at any scale).
|
||||
- **px constants in TS that mirror rem-sized CSS are latent scale bugs.** The
|
||||
shadlike context menu's `MENU_W = 224` was a hardcoded mirror of `w-56`
|
||||
context menu's `MENU_W = 224` was a hardcoded mirror of `w-56`
|
||||
(14rem): box grew, math didn't, flip fired at the wrong edge. Convert through
|
||||
`@core/metrics` `remPx()` at use time — it reads the LIVE root font-size,
|
||||
and per call, since the scale changes mid-session. The editor's canvas-based
|
||||
`charWidth` is cached keyed on the computed font string for the same reason.
|
||||
|
||||
## Hover states and the frame edge
|
||||
|
||||
- **A child's box-shadow is NOT clipped by its parent's border or radius.** The
|
||||
close button's hover glow (7.css: `0 0 7px 3px #e68e75` on a `::before`)
|
||||
painted across the window's top border and onto the desktop beyond it — the
|
||||
caption buttons sit flush against the top edge (`border-top: 0`), and the
|
||||
frame's rounded transparent corners clip nothing. Fixed with
|
||||
`clip-path: inset(0 0 -1rem 0)` on `.w7-frame > .title-bar`: glow stops at
|
||||
the frame like the real DWM, bottom bleed (authentic) stays open. Note
|
||||
clip-path also clips hit-testing — safe here because the drag region is the
|
||||
title-bar's own box, entirely inside the clip.
|
||||
- **Hover states CAN be tested headlessly**: `scripts/hover-probe.ts` copies
|
||||
the built stylesheet with `:hover` rewritten to `.simhover`, renders the
|
||||
frame markup with the state forced, and pixel-scans artifact regions,
|
||||
exiting non-zero on hover-introduced red outside the frame. Firefox
|
||||
`--screenshot` has no pointer and `import -window` captures the real
|
||||
pointer's state, so the class rewrite is the only programmatic hover on this
|
||||
machine. Re-run it after touching title-bar or hover styling.
|
||||
|
||||
## 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.**
|
||||
`html, body { background: … }` in styles.css is unlayered, so no `skinfix`
|
||||
rule can override it — the win7 transparent-corner reset has to be unlayered
|
||||
too. The frame's rounded corners only read as rounded because nothing paints
|
||||
an opaque full-bleed ground behind them; anything that does fills the corners
|
||||
and the window becomes a rectangle with a curve drawn on it.
|
||||
- **7.css restates `border-color` at (0,3,1)** via
|
||||
`.win7 button:not(:disabled):not(:hover)` and its `:hover` twin. A two-class
|
||||
selector like `.win7 .w7-flat` (0,2,0) loses to it, so paint suppression for
|
||||
flat buttons lives in `skinfix` where layer order settles it. The
|
||||
`.w7-toolbar` arm only ever won by tying on specificity and landing later in
|
||||
the same layer — an accident `w7-flat` did not share, which is why it
|
||||
rendered a bordered slab while its neighbours went flat.
|
||||
- **`overflow-x: auto` also clips vertically.** CSS refuses to pair `visible`
|
||||
with a non-visible axis, so the computed `overflow-y` becomes `auto` too.
|
||||
7.css raises the active tab with `margin-top: -2px`; inside the horizontally
|
||||
scrolling tab strip that put its top border outside the scroll box and
|
||||
deleted it silently. The strip reserves `padding-top: 2px` to compensate —
|
||||
mirroring the `padding-left: 3px` 7.css already reserves for the same tab's
|
||||
`-3px` left margin.
|
||||
- **7.css hardcodes light-mode literals outside its own var system** —
|
||||
`color: #222` on tablist buttons, black/white bevels in the ListView and
|
||||
menu. Each one has to be routed through a `--w7-*` token declared in
|
||||
`:root.win7` and overridden in **both** dark blocks (the `[data-theme]`
|
||||
attribute one *and* the `prefers-color-scheme` one; system dark sets no
|
||||
attribute).
|
||||
- **The skin layer can also override `position`, and that breaks LAYOUT, not
|
||||
just paint.** `ul[role=menu]` carries `position: relative` in 7.css (for the
|
||||
gutter stripe's `::before` and submenu anchoring), which outranked Tailwind's
|
||||
`.fixed` on the app's own context menu. `[style.left/top.px]` then became
|
||||
*flow* offsets from the menu's static position — the last child of the
|
||||
`h-screen` frame — so every right-click menu rendered below the viewport,
|
||||
unreachable behind `overflow: hidden`. The backdrop is a plain `<div>`, which
|
||||
7.css does not match, so it stayed genuinely fixed and ate the next click:
|
||||
the symptom was "right-click does nothing, then the app ignores me once".
|
||||
Fixed with a `position: fixed` restatement in `skinfix`, scoped to a `w7-ctx`
|
||||
marker class so 7.css's own menus keep theirs. Regressed in `0ae2a62`, when
|
||||
the menu root became a real `ul[role=menu]` — i.e. adopting the framework's
|
||||
markup is what exposed it. **A resting pixel diff cannot catch this class of
|
||||
bug**: nothing about the idle window changes. `scripts/menu-probe.ts` renders
|
||||
the menu at declared coordinates and scans whether it landed there, with a
|
||||
marker-less control menu as its own discriminator.
|
||||
- **7.css's button effects reach anything that IS a button — including menu
|
||||
items — and one of them fades out over a FULL SECOND.** `button::before` is
|
||||
an Aero glow overlay with `opacity: 0`, raised to 1 on hover, and
|
||||
`button:not(:hover):before` carries `transition: opacity 1s linear`.
|
||||
menu.css suppresses it with `content: none` on the **`:hover` arm only**, so
|
||||
while an item is hovered nothing paints, but the moment the pointer leaves,
|
||||
the pseudo returns with opacity animating 1 → 0: a blue ghost trailing the
|
||||
cursor down the menu. Killing `content` in *every* state is the fix —
|
||||
suppressing the transition alone leaves a glow that simply snaps. Two more
|
||||
from the same family: `button:focus` runs `pulse-anim` *infinitely*
|
||||
(`1s ease infinite alternate`), which matters because the context menu moves
|
||||
REAL focus between items for keyboard nav, and the hover paint is 7.css's
|
||||
translucent gradient wash — the same one the ListView selection dropped.
|
||||
All three are overridden together, scoped to `w7-ctx`.
|
||||
- **Its selectors are exact, and a near-miss looks exactly like "7.css doesn't
|
||||
support this".** `menu[role=tablist] button` is a DESCENDANT selector, so
|
||||
every button inside the strip becomes a tab — the `+` and the right-hand
|
||||
tools must live outside it. `.window.glass > .title-bar` is direct-child, so
|
||||
an inner wrapper breaks the glass coupling. A wrong guess here already
|
||||
produced one hand-written lookalike that was deleted.
|
||||
|
||||
## Electrobun
|
||||
|
||||
- **Window `close` is not cancellable** (response type `{}`; only the
|
||||
@@ -221,9 +101,48 @@ 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/skins/*/context-menu.ts`) on
|
||||
nothing happens. The menu is plain HTML now (`ui/src/app/skins/shadlike/context-menu.ts`) on
|
||||
every platform; don't reintroduce the native one for "nativeness" on mac/win
|
||||
without keeping a Linux path.
|
||||
- **Window-open flash prevention is three-layer** — remove any one and the
|
||||
white/inverted blink returns: (1) windows are created `hidden: true` and
|
||||
revealed by `showWhenReady` on the webview's first `dom-ready` (800 ms
|
||||
fallback), (2) the bun preload bakes the persisted `data-theme`/tone/accent
|
||||
attributes and the interface scale before first paint (`buildPreload`),
|
||||
(3) `ui/src/index.html` carries an inline `<style>` with the html background
|
||||
per theme — its oklch values MUST stay in sync with the `--background` tokens
|
||||
in styles.css. (The win7 branch once replaced (3) with a transparent rule and
|
||||
the modern skin silently lost it — the other two layers do not cover it.)
|
||||
- Dev vs prod URL: `getMainViewUrl()` resolves the ng-serve dev server only
|
||||
when `DEV_SERVER_URL` is set AND the updater channel is `dev` AND the
|
||||
server answers a HEAD request; secondary windows must reuse the resolved
|
||||
URL, never hardcode `views://`.
|
||||
- Upstream Bun panic (`FFI_Callback_threadsafe_call` → `web_worker.spin`):
|
||||
rare segfault under long sessions; upstream electrobun×Bun issue, restart
|
||||
recovers, nothing app-side to fix.
|
||||
- **Preloads and frames are construction-time only.** The preload is a
|
||||
persistent `WebKitUserScript` at DOCUMENT_START, frozen at webview
|
||||
construction — `webview.loadURL()` re-runs the OLD one
|
||||
(`updatePreloadScriptToWebView` is only reachable from electrobun's internal
|
||||
`<electrobun-webview>` handler). `titleBarStyle`/`transparent` have no
|
||||
setters. Anything `buildPreload` bakes is a first-paint snapshot the UI must
|
||||
correct live over RPC. Recreating the main window is not an escape hatch:
|
||||
`exitOnLastWindowClosed`, push closures and the tray capture it, and there
|
||||
is no `off()`.
|
||||
- **Drag regions ignore `preventDefault`.** The preload starts a window move
|
||||
from a bubble-phase `mousedown` on `document` and never reads
|
||||
`defaultPrevented`; a `pointerdown` handler doesn't stop it either (the
|
||||
compat `mousedown` still fires). `isAppRegionDrag` checks
|
||||
`.electrobun-webkit-app-region-no-drag` first — that class on the element is
|
||||
the only reliable suppressor (`dist/api/bun/preload/dragRegions.ts`).
|
||||
- **No resize, maximize-event or double-click-maximize primitives (1.18.1).**
|
||||
`titleBarStyle: "hidden"` drops the whole frame (Cocoa `Titled` — the only
|
||||
way to remove the traffic lights; `gtk_window_set_decorated(false)` on
|
||||
Linux) and with it the WM resize borders; nothing restores them (no
|
||||
`startWindowResize`, no `gtk_window_begin_resize_drag`, no
|
||||
`setResizable`/min-size options — only `setFrame`/`getFrame`). No maximize
|
||||
event exists (derive it from `resize` + `isMaximized()`), and drag regions
|
||||
don't double-click-maximize. A custom-chrome window has to build all three.
|
||||
|
||||
## Linux
|
||||
|
||||
@@ -266,100 +185,13 @@ touching the adjacent code.
|
||||
pattern appears in the killing command's OWN argv. Break the literal with a
|
||||
character class — `pkill -f 'dev-linux-x[6]4'` — the way `ps | grep [x]yz`
|
||||
has always worked.
|
||||
- **A frameless window cannot be resized, and `preventDefault` will not save
|
||||
you from the drag region.** Undecorating the GTK window (what
|
||||
`titleBarStyle: "hidden"` does) takes the WM's resize borders with it, and
|
||||
electrobun exposes no resize gesture — hence the synthesized grip in
|
||||
`ui/src/app/skins/7like/window-resize/` (a STOPGAP; that file carries the
|
||||
removal checklist, and `scripts/check-electrobun-resize.ts` fires from
|
||||
`postinstall` when upstream makes it deletable).
|
||||
**An invisible resize handle over a control steals that control's hover.** A
|
||||
full eight-handle set shipped briefly and was reverted: the north strip ran
|
||||
the whole width across the caption buttons and the north-east square sat on
|
||||
the close button's corner, so crossing one dropped the button's `:hover`
|
||||
mid-glow and looked like a rendering artifact in 7.css — nothing in the CSS
|
||||
was wrong. Only the bottom-right corner overlaps nothing (it lands in the 6px
|
||||
gutter `.window-body { margin: 0 6px 6px }` leaves). Any new handle must be
|
||||
checked against what sits under it; south/east/west are innocent, north and
|
||||
both top corners are not.
|
||||
Three more traps live in that code, all verified against
|
||||
`dist/api/bun/preload/dragRegions.ts`:
|
||||
- It starts a window **move** from a `mousedown` listener on `document`, in
|
||||
the **bubble** phase. It never reads `defaultPrevented`, so
|
||||
`event.preventDefault()` does **nothing** to stop it.
|
||||
- A `pointerdown` handler does not stop it either: the compatibility
|
||||
`mousedown` still fires afterwards, and stopping propagation on a *pointer*
|
||||
event has no effect on a *mouse* event. This is the trap that looks fixed
|
||||
in every test until the window jumps.
|
||||
- `isAppRegionDrag` checks `.electrobun-webkit-app-region-no-drag` **first**
|
||||
and short-circuits, so that class — on the handle itself — is the only
|
||||
reliable suppressor. Without it, every resize drag also drags the window.
|
||||
Also: deltas must come from `screenX/screenY`. North/west resizes move the
|
||||
window's origin, which moves the webview's client origin under a stationary
|
||||
pointer, so `clientX` feeds that movement back into the next delta and the
|
||||
window runs away. And the minimum-size clamp must re-anchor the opposite
|
||||
edge — clamping the width alone lets `x` keep tracking the pointer, walking
|
||||
the window off-screen while it looks frozen (`scripts/resize-math-probe.ts`
|
||||
asserts exactly that, and was verified to catch it).
|
||||
- **Prefer the headless probes over launching the app.** `bun run start` opens a
|
||||
real window that takes focus, which interrupts whatever the user is doing;
|
||||
there is no Xvfb/xvfb-run on this machine and Wayland offers no
|
||||
launch-without-focus. `scripts/shot.ts` (+ `--dark`), `hover-probe.ts` and
|
||||
`menu-probe.ts` all render through headless Firefox and open nothing. Launch
|
||||
the real app only for what Gecko genuinely cannot answer — WebKitGTK
|
||||
behaviour, the native frame, transparency, live RPC — ask first, and batch
|
||||
every live check into a single launch.
|
||||
- **X11 capture cannot answer "is this pixel transparent?"** for the frameless
|
||||
window. `import -window` grabs the window's own pixels with no compositing,
|
||||
so a transparent corner and an opaque black one both come out black;
|
||||
`png32:` just fills alpha with 1 and proves nothing. XWayland's root window
|
||||
is not the composited desktop, so `import -window root` fails too. Frame
|
||||
transparency and the maximized corners need a human looking at the screen —
|
||||
say so rather than reporting a screenshot as confirmation.
|
||||
- **Launching the app steals focus — ask first.** `bun run start` opens a real
|
||||
window that takes focus, which interrupts whatever the user is doing; there
|
||||
is no Xvfb/xvfb-run on this machine and Wayland offers no
|
||||
launch-without-focus. Batch every live check into a single launch.
|
||||
- Benign startup noise on Linux — non-fatal, the app runs normally:
|
||||
`libayatana-appindicator is deprecated. Please use libayatana-appindicator-glib`
|
||||
(upstream electrobun's choice, not ours).
|
||||
- **Window-open flash prevention is three-layer** — remove any one and the
|
||||
white/inverted blink returns: (1) windows are created `hidden: true` and
|
||||
revealed by `showWhenReady` on the webview's first `dom-ready` (800 ms
|
||||
fallback), (2) the bun preload bakes the persisted `data-theme`/tone/accent
|
||||
attributes before first paint (`buildPreload`), (3) `ui/src/index.html`
|
||||
carries an inline `<style>` with the html background per theme — its oklch
|
||||
values MUST stay in sync with the `--background` tokens in styles.css.
|
||||
The win7 skin adds a **fourth value to keep in sync**: `buildPreload` also
|
||||
bakes the `.win7` class, and index.html carries a matching `html.win7` arm
|
||||
for all four theme states. Miss it and every win7 window flashes white.
|
||||
- **The skin is a LAUNCH-TIME choice. Never apply one to a running window.**
|
||||
This shipped broken once: `SettingsService.applySkin` toggled the `.win7`
|
||||
class the moment the setting changed, which was harmless when there was one
|
||||
component tree and became a broken window as soon as there were two — 7.css
|
||||
painted onto shadlike markup that has no `.window.glass`, no `.title-bar`, no
|
||||
`menu[role=tablist]` and no `<table>` ListView. Each skin's CSS is written
|
||||
against its OWN tree's markup; the class must only ever be written by
|
||||
`buildPreload`, for the tree that window actually booted.
|
||||
- **A reload does not rescue it.** The preload is a persistent
|
||||
`WebKitUserScript` injected at DOCUMENT_START, so its string is frozen at
|
||||
webview construction — `webview.loadURL()` re-runs it with the OLD
|
||||
`__APP_SKIN`. (`updatePreloadScriptToWebView` exists in the FFI but is
|
||||
reachable only from electrobun's internal `<electrobun-webview>` handler.)
|
||||
- **`titleBarStyle`/`transparent` are construction-time only** — no setters
|
||||
exist, so a live window can never be re-framed. win7 → modern is the
|
||||
dangerous direction: the modern tree draws no title bar, so it would land
|
||||
in a frameless transparent window with no way to move, minimize or close.
|
||||
- Don't "fix" this by recreating the main window either: `exitOnLastWindowClosed`
|
||||
is true, the main `rpc` is captured by six push closures, the tray captures
|
||||
`win`, listeners cannot be removed (no `off()`) — and it would drop every
|
||||
session and tab.
|
||||
- Bun pins it: `initSessionSkin` freezes the skin before any window exists,
|
||||
and `buildPreload`/`chromeOptions` read that, never the live settings. Skip
|
||||
the pin and a window opened after a pending change boots the other skin.
|
||||
- Dev vs prod URL: `getMainViewUrl()` resolves the ng-serve dev server only
|
||||
when `DEV_SERVER_URL` is set AND the updater channel is `dev` AND the
|
||||
server answers a HEAD request; secondary windows must reuse the resolved
|
||||
URL, never hardcode `views://`.
|
||||
- Upstream Bun panic (`FFI_Callback_threadsafe_call` → `web_worker.spin`):
|
||||
rare segfault under long sessions; upstream electrobun×Bun issue, restart
|
||||
recovers, nothing app-side to fix.
|
||||
|
||||
## Domain logic
|
||||
|
||||
|
||||
+2
-27
@@ -8,9 +8,7 @@ macOS is the primary target; Windows 11+ x64 and Linux x64 are **beta** targets
|
||||
|
||||
| Concern | File | mac | win | linux |
|
||||
|---|---|---|---|---|
|
||||
| Window chrome (modern skin) | `src/bun/windows.ts`, `index.ts` | hiddenInset + traffic-light offset, UI drag header | native titlebar, compact header | same as win |
|
||||
| Window chrome (win7 skin) | `src/bun/windows.ts chromeOptions` | `hidden` (clears Titled → no traffic lights) | frameless | `gtk_window_set_decorated(false)` |
|
||||
| Window **resize** | `ui/.../7like/window-resize/` | native frame resizes; win7 frameless uses the synthesized handles | as mac | as mac |
|
||||
| Window chrome | `src/bun/windows.ts chromeOptions` | hiddenInset + traffic-light offset, UI drag header | native titlebar, compact header | same as win |
|
||||
| External editor launch | `src/bun/edits.ts launchEditor` | `open [-a app]` | `cmd /c start "" [app] file` | app directly / `xdg-open` |
|
||||
| Editor app picker | `rpc-handlers.ts pickEditorApp` | /Applications, `.app` | %ProgramFiles%, `.exe` | home, any |
|
||||
| Owner/group names | `fs/local.ts resolveId` | `id -nu` + `dscacheutil` | undefined → "—" | `id -nu` + `getent` |
|
||||
@@ -25,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/skins/*/context-menu.ts` +
|
||||
The **context menu is HTML on every platform** (`ui/src/app/skins/shadlike/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.
|
||||
@@ -33,29 +31,6 @@ the default app, copy path) cross to bun, via the `shellItemAction` RPC.
|
||||
Remote (SFTP) paths are ALWAYS POSIX regardless of host or server OS
|
||||
(`path.posix` throughout `fs/sftp.ts`).
|
||||
|
||||
### Frameless windows lose the WM's resize borders (win7 skin, every platform)
|
||||
|
||||
`titleBarStyle: "hidden"` is what lets 7.css draw the Aero caption — on Linux the
|
||||
GTK wrapper honours it with `gtk_window_set_decorated(false)`. Undecorating also
|
||||
removes the window manager's resize borders and corner grips, and electrobun
|
||||
1.18.1 offers **no** native resize gesture to put back: the shipped
|
||||
`libNativeWrapper` exports `startWindowMove`/`stopWindowMove` with no resize
|
||||
counterpart, does not link `gtk_window_begin_resize_drag`, and `BrowserWindow`
|
||||
has neither `setResizable` nor min-size constructor options. The window is still
|
||||
*programmatically* resizable (`setFrame`/`getFrame` work, and
|
||||
`gtk_window_set_resizable(false)` is never called), so the app **synthesizes the
|
||||
input affordance**: a bottom-right grip in the webview drags the native frame
|
||||
over RPC (`ui/src/app/skins/7like/window-resize/`). Only that one corner — an
|
||||
invisible handle anywhere else overlays a control and steals its hover, which is
|
||||
what a full edge set did to the caption buttons.
|
||||
|
||||
That is a **stopgap** — each drag tick is an RPC hop plus an FFI call, where a
|
||||
real WM grab is compositor-side. `scripts/check-electrobun-resize.ts` watches the
|
||||
installed electrobun for the missing primitives and prints the removal checklist
|
||||
when they appear; it runs from `postinstall`, so an electrobun bump surfaces it
|
||||
without anyone remembering to look. The modern skin keeps the native frame and
|
||||
needs none of this.
|
||||
|
||||
## Memory footprint (expected, not a bug)
|
||||
|
||||
Investigated 2026-07 — packaged build. Roughly **~200 MB macOS / ~300 MB
|
||||
|
||||
Reference in New Issue
Block a user