Brings in 6c05e39 (re-sign electrobun's CLI binary so the app launches).
Two conflicts, both resolved to keep both sides:
- package.json: start keeps scripts/start.ts (injects the Linux NVIDIA env)
behind main's fix-electrobun-cli re-sign; build and postinstall take
main's re-sign step (the resize watchdog is already gone).
- docs/build-and-release.md: the release usage keeps the linux-x64 target
and gains main's fix-electrobun-cli step.
scripts/release.ts and docs/gotchas.md auto-merged with both sides intact:
the Linux target + tarball wrapper, and the arm64/x64 CLI re-sign.
Co-Authored-By: Claude <noreply@anthropic.com>
Ctrl/Cmd +/-/0 in editor and connection-manager windows called
SettingsService.persist() -> saveSettings, but only the main window's RPC
served that request: it went unanswered, the new scale applied locally, and
the next broadcast from main snapped it back.
All three window RPCs now serve saveSettings through one shared
saveSettingsHandler (persist, then hand off). index.ts's settingsSaved() is
the single place settings side effects run — transfer concurrency, locale +
app menu, the preload snapshot — whichever window saved, and it rebroadcasts
to every window EXCEPT the sender: an echo arriving behind a newer local
change (a held Cmd+) would drag it backwards. Editors are skipped by their
registry key, main and the manager by name.
Known ceiling, noted at the handler: whole-object writes, so two windows
saving in the same instant means the later one wins.
Co-Authored-By: Claude <noreply@anthropic.com>
The deleted win7 tree held a few behaviours the modern tree never got:
- Context menu: the first ArrowUp in a fresh menu skipped the last item
(Delete in the row menu, Disconnect in the tab menu) — stepping from -1
landed one short. The index math moves to core/menu-nav.ts stepIndex(),
checked by menu-nav.spec.ts (bun test; verified to fail on the old logic).
Items get ids and the menu exposes aria-activedescendant, so screen
readers follow the arrow keys while focus stays on the menu.
- Middle-click closes a tab, as in browsers. auxclick also fires for the
right button, so only button 1 counts; closeWorkspace already refuses the
last tab. A middle press is swallowed so WebView2 doesn't start autoscroll
over the scrollable strip. The hover ✕ stays.
- ARIA: the tab strip is a tablist of role=tab buttons with aria-selected
(the in-tab ✕ is taken out of the a11y tree and tab order — Ctrl/Cmd+W and
the tab menu remain the keyboard paths); transfer bars are progressbars
with value and a name.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
The handles are invisible and paint above the frame, so the north strip ran the
full window width across the caption buttons and the north-east square sat on
the close button's own top-right corner. Crossing one dropped that button's
:hover mid-glow, which reads as the Aero glow flickering or sticking — an
artifact with nothing wrong in the CSS at all.
Only the bottom-right corner overlaps nothing: it lands in the 6px gutter that
.window-body's margin leaves below and right of the content. That is also where
Win7 put its own resize gripper, so one grip is the more faithful answer anyway.
Bumped to 1rem from 0.75rem since it is now the only way to resize.
resizeFrame stays general and the probe still exercises all eight edges: the
origin-moving cases are the subtle ones and are worth keeping proven, so
restoring a side later is one entry in the component against tested math. Both
the component and gotchas.md now spell out which edges are safe to restore
(south, east, west) and which are not (north and both top corners).
Harness diffs 0; math, hover and menu probes clean.
Co-Authored-By: Claude <noreply@anthropic.com>
The win7 skin is frameless so 7.css can draw the Aero caption, and on GTK that
means undecorated, which takes the window manager's resize borders and corner
grips with it. All three win7 windows were completely unresizable.
electrobun 1.18.1 offers nothing to re-enable, verified against the shipped
binary rather than assumed: libNativeWrapper exports startWindowMove/
stopWindowMove with no resize counterpart, imports gtk_window_begin_move_drag
but not gtk_window_begin_resize_drag, dragRegions.ts implements moving only,
and BrowserWindow has neither setResizable nor min-size options. But the
window is still programmatically resizable, so only the input affordance is
missing: eight handles overlay the frame and drag it through a new
windowGetFrame/windowSetFrame pair on the shared windowControlHandlers factory,
which gives all three window kinds resize with no per-window wiring.
Three things that would have made this quietly wrong:
- dragRegions.ts starts a window MOVE from a mousedown listener on document,
never checks defaultPrevented, and is not suppressed by a pointerdown
handler either (the compatibility mousedown still fires). Every handle
carries app-region-no-drag, which short-circuits its check first.
- deltas come from screenX, not clientX: a north/west resize moves the window
origin under a stationary pointer, so client coords feed that back into the
next delta and the window runs away.
- the min-size clamp re-anchors the opposite edge; clamping width alone lets x
keep tracking the pointer, walking the window off-screen while it looks
frozen.
This is a stopgap and the point is that it is removable on sight. The frame
math is a dependency-free module so scripts/resize-math-probe.ts asserts all
eight edges without a window (verified it catches the unanchored-clamp bug:
x jumps to 5100 instead of parking at 460). scripts/check-electrobun-resize.ts
scans the installed electrobun for the primitives this is waiting on and prints
the removal checklist when they appear; it runs from postinstall so a version
bump surfaces it, never fails an install, and is silent otherwise. Both trigger
paths were simulated. The component header carries the full checklist; docs
cover the platform consequence and the dragRegions traps.
Harness diffs exactly 0 in both themes; hover and menu probes still clean.
Not yet exercised with a real pointer — that needs a live window.
Co-Authored-By: Claude <noreply@anthropic.com>
Three things looked wrong, three different causes.
The selected row carried a border. 7.css outlines tr.highlighted (#aaddfa) on
top of its fill, and we had been restating that outline in the new blue — so
the band had an edge, and on the cursor row the dotted focus rect made three
concentric outlines around a single row. The reserved 1px border stays (it is
what stops selection from shifting rows) but is transparent in every state;
the fill paints under it, since background-clip is border-box. --w7-sel-bd and
--w7-sel-bd-idle are gone with it.
The context menu highlighted items with 7.css's translucent gradient wash —
the same gradient the ListView selection just dropped. It now uses
--w7-sel-bg, so selection reads identically wherever it appears.
The hover-off animation was button.css bleeding into menu items, which ARE
buttons: button::before is a glow overlay that fades out over a full second,
and menu.css suppresses it with content:none on the :hover arm ONLY — so
leaving an item brought the pseudo back with opacity animating 1 -> 0, a ghost
trailing the pointer. Killing content in every state removes it. Same family,
also fixed: button:focus runs pulse-anim infinitely, and this menu moves real
focus between items for keyboard nav.
menu-probe.ts now checks the highlight too, using hover-probe's :hover ->
.simhover rewrite: it measures colour spread down a text-free strip of a
force-hovered item, so "is it flat?" is a measurement. Verified the check can
fail by injecting the old wash into the built stylesheet — spread 9, exit 2;
the flat fill reads spread 0 at exactly #cbe8f6. The harness lost its
selection outlines and nothing else, in both themes.
Co-Authored-By: Claude <noreply@anthropic.com>
Right-click did nothing under the win7 skin, and then swallowed the next
click. 7.css declares `position: relative` on ul[role=menu] and the `skin`
layer outranks `utilities`, so Tailwind's `.fixed` lost: [style.left/top.px]
became flow offsets from the menu's static position — the last child of the
h-screen frame — and every menu rendered below the viewport, unreachable
behind overflow:hidden. Its backdrop is a plain <div>, unmatched by 7.css, so
that stayed genuinely fixed and ate the following click. Regressed in 0ae2a62,
when the menu adopted 7.css's real markup.
Restated `position: fixed` in skinfix, scoped to a new `w7-ctx` marker so
7.css's own menus keep theirs. scripts/menu-probe.ts is the regression test:
it renders the menu at declared coordinates, pixel-scans whether it landed
there, and carries a marker-less control menu (the bug verbatim) as its own
discriminator. A resting pixel diff cannot see this class of bug.
Selection band goes flat: --w7-sel-bg/-bd (#cbe8f6/#26a0da) is what Explorer
actually painted, where 7.css reused --w7-el-grad-h, its Aero element-hover
ramp. That var is left alone — buttons and tabs still want the gloss. The
unfocused pane gets the grey idle band; the band still never disappears with
focus, it only weakens. Harness diff confirms the change touches selection
rows and nothing else, in both themes.
New workspace-tab right-click menu in both skins: New Tab, Close Tab, Close
Other Tabs, Close Tabs to the Right, Disconnect. The target id travels inside
the action string because the right-clicked tab is not necessarily the active
one. Illegal entries are disabled rather than omitted, the way Win7 greyed
out — and since 7.css's [aria-disabled] handling is CSS-only, the keyboard
paths check the flag themselves.
Co-Authored-By: Claude <noreply@anthropic.com>
Hovering the win7 X painted a red mark on the window border's far edge.
Diagnosed programmatically, per request, with a new scripts/hover-probe.ts:
Firefox headless has no pointer, so the probe copies the built stylesheet
with every `:hover` rewritten to a `.simhover` class, renders the app's exact
frame markup (w7-frame + direct-child title-bar + aria-label'd controls) with
the X force-hovered, and pixel-scans four regions for red-dominant pixels,
printing a verdict table and exiting non-zero on artifacts.
Verdict from the first run: the right border and the region beside it were
CLEAN — the reported mark is the glow escaping over the TOP edge. 7.css's
hover ::before carries box-shadow 0 0 7px 3px #e68e75, a child's box-shadow
is not clipped by the parent's border or radius, and the caption buttons sit
flush against the window's top edge (border-top: 0) — so 100 red pixels
landed above the frame: a tint across the top border and a halo floating on
the desktop past it.
Fix: clip-path inset(0 0 -1rem 0) on `.w7-frame > .title-bar` in skinfix. The
glow now dies at the frame border — where the real Win7 DWM stopped it — and
the bottom stays open so the authentic bleed toward the caption survives.
clip-path clips hit-testing too, but only outside the region; the drag area
is the title-bar's own box, entirely inside. Scoped to w7-frame so harness
and demo windows keep stock 7.css behaviour.
Verified: probe verdict now clean (escape regions 0, the intended in-frame
glow still measures 30 px — the effect survived, only the escape is gone);
the resting-state harness diffs exactly zero, so nothing but hover pixels
changed; tsc + build clean. The probe stays in scripts/ as the regression
test; gotchas.md records both the clipping rule and the :hover -> class
technique.
Co-Authored-By: Claude <noreply@anthropic.com>
Groundwork for the vim-style bindings planned next: the app previously had NO
keyboard abstraction — `event.metaKey || event.ctrlKey` copy-pasted inline in
six places, no typing-target guard anywhere, every window root matching keys
by hand.
New @core/shortcuts.service.ts owns:
- isMod() (also accepts MouseEvent — Mod+click toggle-select uses the same
rule) and isTypingTarget();
- ONE declarative keymap: resolve(event, scope) -> ShortcutId | null, scoped
main / editor / manager. Layout rules are encoded once: digits, +/- and
Alt-letter chords match on event.code (AZERTY digit rows, layouts where '+'
needs Shift, macOS Option-Z producing Ω); plain letter chords match on
event.key so they follow the user's layout.
Both app.ts, both editor-window.ts and both connection-manager.ts now resolve
through the service and switch on ids — behaviour-preserving for every
existing shortcut, including the deliberate quirks (Ctrl+Tab never meta
because Cmd-Tab is the macOS app switcher; Cmd+9 = last tab; editor Mod+W is
the Windows/Linux arm, macOS routes via the app menu). Escape and Tab stay
component-local on purpose: their meaning is per-window state (find bar open?
close prompt up? caret in the textarea?), not a global binding. The file
panels keep their contextual key state machine but delegate modifier tests.
On top of the table: Ctrl/Cmd +/-/0 steps/resets the interface scale in every
window kind, all three previously unclaimed. isTypingTarget is applied
per-shortcut, not globally in resolve(): the scale keys must not fire while
typing ('-' in a rename prompt must type '-'), but the editor WANTS Mod+S
mid-typing and the tab shortcuts keep their existing reach.
Docs: architecture.md's density prose now describes the uiScale lever;
gotchas.md gains the scale section — the disassembly evidence that native
page zoom is an empty stub on Linux (so nobody tries it again), the
everything-must-be-rem rule, the skinfix-outranks-variants trap the harness
caught (48px close button silently 29px), and the px-mirrors-in-TS class of
bug with the remPx() remedy.
Verified: tsc + build clean; live boot with the user's real legacy settings
file migrates textSize:large -> uiScale 1.2 and renders scaled. Not verified
here (needs interaction): the stepper clicks and the shortcut keys themselves.
Co-Authored-By: Claude <noreply@anthropic.com>
AppSettings.textSize (compact|default|large) becomes uiScale: number, 0.5-4
in 0.1 steps, default 1. One shared clampUiScale (next to the
effectiveConcurrency precedent) defines "valid" for the store, the service
and the stepper: clamp to range, snap to the step grid, strip float noise,
and map NaN/Infinity to 1.
- SettingsStore.load migrates legacy files (compact/default/large ->
0.9/1/1.2, never over an explicit uiScale) and clamps ALWAYS, not just on
migration — buildPreload writes this value into every window's root
font-size, so a hand-edited settings.json (uiScale: 100) must not be able
to render the UI unusable. Verified against 100, 0.001, -3, NaN, Infinity
and float noise; migration verified against a real legacy file.
- SettingsService: applyUiScale sets the root font-size (16 * scale, removed
entirely at 1 per the delete-when-default rule). Rides the existing
settingsChanged broadcast, so open editor/manager windows follow live.
- buildPreload bakes the same font-size so the first paint is already scaled
— without it every window opens at 100% and snaps.
- Both tab-bars: the 3-button Interface row becomes the stepper. Clicking the
percentage resets to 100% (tooltip says so). i18n: compact/default/large
dropped, smaller/larger/reset added, all 7 locales.
Verified live, win7 + modern: scale 1 is pixel-identical to the pre-work
baseline (opaque interior diff exactly 0); 0.5x is a real dense mode (~2x
rows); 2x scales title bar, tabs, rows, icons and controls together with text
contained in its boxes — which is what stage 1c's 7.css bridge bought; 4x is
enormous but coherent, and the win7 title-bar controls stay on-screen at the
default window size. The known caveat stands: no minimum window size exists,
so 4x in a small window is legal but cramped.
Co-Authored-By: Claude <noreply@anthropic.com>
Groundwork for the continuous interface scale (a root font-size lever): every
size the UI derives from must be root-relative, or it freezes while the rest
grows. Gate for this commit: at the default 16px root the app renders
pixel-identical to before — every conversion is exact (11px = 0.6875rem …).
- The 8 panel-chrome tokens in styles.css: px -> rem, and the data-text
compact/large tiers are DELETED — a continuous scale replaces discrete
tiers. Same for --w7-fs (12px -> 0.75rem) and the win7 --radius (5px ->
0.3125rem, still landing rounded-md exactly on --w7-el-bdr's 3px).
--radius-sm/md subtrahends: px -> rem, or corners would sharpen as the UI
grew.
- All 93 text-[11px]/text-[10px] arbitraries -> rem equivalents, plus the two
inline padding:24px empty-state cells in the 7like file panel.
- 7like.css gains a scale bridge in skinfix: 7.css is 370 px values with zero
rem, and --w7-font reaches only 15 rules' TYPOGRAPHY — under a root lever
win7 text would grow inside boxes frozen at 23px/14px/15px. The bridge
re-expresses 7.css's size vars and its hardcoded box metrics (buttons via
.w7-btn only, inputs, table rows/headers, title-bar controls, menu items,
tabs, the 10pt check glyph) in rem at identical scale-1 pixels. 1px
hairlines and focus rects stay px deliberately — crisp at any scale, and
the tr border exists precisely to stop selection shifting rows.
- Two skinfix traps found while writing it, one by reasoning and one by
pixels: (a) NO blanket `.win7 button` restatement — skinfix outranks the
skin-layer neutralizer that strips 7.css's 75x23 metrics off the ~84 icon
buttons, so a blanket rule would re-inflate them all; (b) restating a base
rule in skinfix also outranks 7.css's own VARIANTS of it by layer — the
title-bar rule silently shrank the close button from 48px to 29px until the
harness pixel diff caught it, and the active tab's padding-bottom needed
the same treatment. Both variants are restated alongside their base rules.
- TS px constants that mirror rem CSS are the latent-bug half of this work:
both context menus' flip/clamp math converts through a new @core/metrics
remPx() helper reading the LIVE root font-size (the shadlike menu's
MENU_W=224 diverged from its w-56 box the moment scale != 1; the 7like
menu's [style.width.px] pinned the box at 200px while text grew — it now
binds width in rem, so math and box cannot disagree). The editors'
cachedCharWidth is now keyed on the computed font string, self-invalidating
under any font change, and both || px fallbacks derive from the element's
font size instead of assuming a 16px root.
Verified: tsc + build clean; modern and win7 live captures and the harness
all diff to zero against pre-change baselines in their opaque regions (the
harness to exactly zero end to end; modern's only delta is the native GTK
titlebar focus state; win7's is the translucent Aero corner over the
desktop).
Co-Authored-By: Claude <noreply@anthropic.com>
Switching Modern -> Win7 at runtime broke the UI. SettingsService.applySkin
toggled the `.win7` class the moment the setting changed — harmless when there
was one component tree, and a broken window as soon as there were two. The tree
is bootstrapped once from the preload-baked __APP_SKIN, so the live flip painted
7.css onto shadlike markup that has no .window.glass, no .title-bar, no
menu[role=tablist] and no <table> ListView. The CSS landed on a tree that cannot
receive it.
A reload would not have helped, and neither would anything short of a restart:
- The preload is a persistent WebKitUserScript injected at DOCUMENT_START, so
its string is frozen at webview construction — 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 have no setters; a live window cannot 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 it.
- Recreating the main window is also out: exitOnLastWindowClosed is true, the
main rpc is captured by six push closures, the tray captures `win`, and
listeners cannot be removed.
So: applySkin is deleted outright, from setSkin AND from applyAll — the latter
matters independently, since applyAll runs on the settingsChanged push and was
therefore breaking every open editor and manager window too. Clicking a skin now
only persists it.
Also fixed a second leak found while tracing this: manager-window and
editor-windows build from `currentSettings()`, the LIVE snapshot, so a window
opened after a pending change would boot the new tree and frame while main still
ran the old one. `initSessionSkin` now freezes the skin at startup before any
window exists, and buildPreload/chromeOptions read that pinned value —
chromeOptions loses its parameter so no call site can get it wrong.
The options row shows the saved value as pending (skinPending() compares
settings.skin() against APP_SKIN) with the hint retexted in all 7 locales; it
said "Reopen the window to change its frame", which described the deleted
design.
Verified: tsc + build:ui clean; both skins boot correctly from a persisted
setting (win7 frameless, modern with the native frame). The live flip is now
impossible by construction, which is the check that matters here — the UI bundle
contains no classList write for win7 at all, the only writer is buildPreload,
and sessionSkin has exactly one assignment site.
Not verified: the click itself and the pending hint appearing, which need an
interaction pass — no window automation is available here. Both are present in
the built bundle.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
The reunion is complete: one build contains both UIs and picks between them at
bootstrap.
- src/shared/types.ts regains `skin: "modern" | "win7"` on AppSettings.
- SettingsService regains the skin signal, setSkin() and applySkin(), which
toggles the `.win7` class on <html>.
- buildPreload emits __APP_SKIN and adds `.win7` conditionally again, instead
of unconditionally as it had since fe028a9.
- chromeOptions() regains its parameter: win7 gets frameless + transparent,
modern gets the macOS hiddenInset fusion or the plain native frame.
- core/platform.ts regains APP_SKIN / IS_WIN7_CHROME (added in stage 4).
- main.ts now selects on both axes — skin picks the tree, APP_MODE picks the
root within it.
- Both tab-bars get a skin row, with the reopen caveat as its tooltip.
DEFAULT_SETTINGS.skin is "modern", matching the original design and `main`.
Worth flagging: this branch has booted win7 unconditionally since fe028a9, so
after this commit it starts in the modern UI unless the setting says otherwise.
One word in types.ts flips that if the win7 look should be the default.
i18n: en.ts is the source of truth and everything else falls back to it, so
only en was strictly required — but es/eu/fr/de/zh/ja got real translations
too, each matching the terminology that locale already uses for the Theme row.
Verified both skins on live single-instance captures. Modern boots and renders
fully styled, which also proves Tailwind scans skins/shadlike/** — a purge
would have produced an unstyled page rather than this one. Win7, selected by
setting skin in the persisted settings, is still pixel-identical to the
pre-refactor baseline across all five stages. The persisted settings file was
backed up and restored, so it still carries no skin key and resolves to the
default through the backend's merge.
Not verified: clicking the new skin row itself, which needs an interaction pass
— no window automation is available here. Its bindings compile and the built
bundle contains options.skin, setSkin and the translated labels.
Co-Authored-By: Claude <noreply@anthropic.com>
Both UIs now live side by side. The 18 modern component files come straight
from `git show main:<path>` — recovered, not rewritten, which is possible
because main's tip IS the merge base of this branch. There is no title-bar
among them: that component is win7-only, since the modern skin defers to the
native window frame.
Their imports were rewritten to @core the same way stage 1 did, leaving only
skin-internal relative imports behind. Added skins/shadlike/index.ts exporting
SHAD_ROOTS with the same shape as SEVEN_ROOTS, so main.ts can select a tree
without knowing either skin's internals.
Restored the skin axis in core/platform.ts, deleted in fe028a9 and needed
again by the modern tree: __APP_SKIN on the Window global, APP_SKIN,
IS_WIN7_CHROME, and IS_MAC_CHROME = IS_MAC && !IS_WIN7_CHROME. That last one
is what reserves pl-24 for the macOS traffic lights in the fused hiddenInset
header — and correctly stops reserving it under win7, where titleBarStyle
'hidden' removes the traffic lights entirely.
The tree is not reachable yet; the build is the gate. I checked that the gate
is real rather than assuming it: injecting a type error into an unreferenced
shadlike file does fail `bun run build:ui`, so ngtsc type-checks the whole
program including components nothing imports. Without that check, "the build
passes" would have proven nothing about 18 files no one references.
win7 remains pixel-identical to the pre-refactor baseline.
Co-Authored-By: Claude <noreply@anthropic.com>
The 919-line stylesheet had a clean seam: `main`'s version is 278 lines and
this branch only ever APPENDED to it. Verified that directly — a diff of
main's styles.css against our first 298 lines differs only by the @layer
statement and the 7.css import.
- ui/src/styles.css (296) keeps the shared half: the @layer statement, the
Tailwind import, the OKLCH tokens, @theme inline, the data-text interface
sizes, the animations and the accent axis. The @layer statement must stay
first here — it is one of only two rule types allowed above @import, and it
is what sorts `skin` above `utilities`.
- app/skins/7like/7like.css (636) takes the whole win7 section plus the
scoped-7.css import that feeds layer(skin).
- app/skins/shadlike/shadlike.css is near-empty by nature, not by omission:
that skin IS the shared token system plus inline Tailwind, so there is no
framework to bridge and no cascade to invert. The file documents why, and
what does/does not belong in it.
Both are @imported from styles.css, so one bundle still ships both, scoped by
`.win7` exactly as before.
One real trap caught here: the @font-face url() is resolved relative to the
stylesheet, so moving the win7 section three directories deeper would have
silently 404'd Noto Sans — the build stays green and the app just falls back
to a different font. Fixed by moving the fonts INTO the skin (they exist only
for the Win7 look, where Noto Sans substitutes for Segoe UI), which keeps
`./fonts/...` correct wherever the folder goes. Confirmed in the built output:
both woff2 emitted as hashed assets with the url rewritten to ./media/.
Verified: build clean, built CSS keeps `@layer theme,base,components,
utilities,skin,skinfix` and all 7.css selectors, the live window's opaque
interior is pixel-identical to the pre-refactor baseline, and both harness
renders (light and dark) diff to exactly zero. A layer-assignment mistake here
would not be subtle — 7.css would lose to ~1900 Tailwind utilities and the app
would look nothing like this.
Co-Authored-By: Claude <noreply@anthropic.com>
All 14 components (including title-bar/, which has no modern counterpart)
move under app/skins/7like/. git records every one as a rename — no content
changed except main.ts.
The @core alias from stage 1 paid off exactly as intended: not one of the 67
service imports needed touching despite every file changing depth. The only
relative imports left inside the tree are skin-internal (app -> its dialogs,
connection-manager -> title-bar), and those survived because the move
preserved their relative structure.
Added skins/7like/index.ts exporting SEVEN_ROOTS { main, manager, editor }.
Every skin will export that same shape, so main.ts can pick a tree by skin
and then a root by APP_MODE without knowing either skin's internals. main.ts
selects from that barrel now, with the second axis still hardcoded until the
shadlike tree lands.
Verified: tsc clean, build:ui clean, opaque interior pixel-identical to the
pre-refactor baseline. That last check also proves Tailwind still scans the
templates from their new skins/** paths — a silent utility purge would have
collapsed the layout rather than reproducing it exactly.
The three structural 7.css contracts that a careless move breaks silently are
intact by construction, since no markup changed: app-title-bar is still a
direct child of .window.w7-frame, nothing new nests inside menu[role=tablist],
and the context menu still emits ul[role=menu] > li[role=menuitem] > button.
Co-Authored-By: Claude <noreply@anthropic.com>
First step of splitting the UI into two skin trees. All 12 services plus
platform.ts, format.ts, file-icons.ts, icons.ts and app.config.ts move to
ui/src/app/core/ — the layer both skins will share.
This is a pure move: git records all 17 as renames, and no file content
changed beyond import paths. The premise holds because `git diff
main...feat/7css-skin` touches none of these files — the service layer was
already skin-agnostic, so there is nothing to untangle, only to relocate.
Components now reach it through a new `@core/*` tsconfig alias rather than a
relative path. That is deliberate and load-bearing for the next stage: skin
components are about to move under app/skins/<skin>/ at varying depths, and
an alias keeps every one of these 67 imports correct wherever its file ends
up. `@shared/*` (the bun-side src/shared) already set the precedent.
Intra-core imports needed no edits at all — every ./sibling reference among
the moved files points at another moved file.
Verified: tsc clean, `bun run build:ui` clean (AOT resolves every import and
template symbol), and the running window's opaque interior is pixel-identical
to a pre-refactor capture.
Note on that comparison: the Aero frame and title bar do NOT compare equal
between launches, and that is not a regression — .window.glass::before runs
at opacity .6, so the chrome composites over whatever desktop is behind the
window and shifts by a uniform ~8% brightness. The check therefore diffs the
opaque interior (inset 30px sides, 80px top), where anything this refactor
could break would show.
Co-Authored-By: Claude <noreply@anthropic.com>
The file panels' action verbs (Send/Edit/Rename/new folder/new file/
permissions/Delete) and the panel picker were wearing the full Aero slab,
which is the dialog push-button treatment — wrong for what is really an
Explorer command bar. Marked the session strip w7-toolbar so they get the
flat-with-hover-glow treatment the Connections button already had from the
tab-bar row.
The path bar gets it too. Its up/refresh controls sit directly under the
action row, so leaving them as slabs beside newly-flat neighbours would have
read as arbitrary rather than deliberate. w7-toolbar only targets button, so
the path <input> keeps its 7.css textbox border.
Verified on a live single-instance capture: both rows flat, the path input
unchanged. architecture.md's "three header rows" was now wrong and says
"every command row" instead.
Co-Authored-By: Claude <noreply@anthropic.com>
The status bar's transfers summary is a text toggle, but 7.css painted it as
a full Aero push button — a stray bordered slab sitting in the status line.
Marked it w7-flat, the per-button escape hatch that existed for exactly this
and had never been used.
It did not work, for a reason worth keeping: 7.css restates border-color at
`.win7 button:not(:disabled):not(:hover)` = (0,3,1), which outranks
`.win7 .w7-flat` = (0,2,0). The .w7-toolbar arm of the same rule only ever
won that fight by tying on specificity and landing later in the same layer —
an accident w7-flat did not share. Split the rule: `background:
revert-layer` stays in `skin` (it must land on `utilities`), and the
paint-suppression literals move to `skinfix`, where layer order settles it
instead of a specificity arms race.
Docs updated in the same change per the working agreement — architecture.md
gains the tabs contract, the w7-flat/w7-toolbar split and the frame
transparency invariant; gotchas.md gains a win7-skin section (unlayered beats
every layer, the (0,3,1) border-color restatement, overflow-x clipping the
y-axis, 7.css's hardcoded light literals, exact selectors) plus the two
Angular traps this round hit: a duplicate `class` attribute is silently
dropped, and a backtick inside a component template's HTML comment terminates
the TS template literal.
Also recorded the verification trap that cost three wrong readings today:
`bun run start` leaves the old instance running and `import -window` grabs
the OLDEST match, so screenshots silently come from a stale build — this is
what made the w7-flat fix above look like a no-op when it was already
correct. Kill and count instances before capturing. And `pkill -f
'electrobun dev'` matches the shell running it and kills your own command.
Co-Authored-By: Claude <noreply@anthropic.com>
The rounded frame only works if nothing paints an opaque full-bleed surface
behind it — the area outside a border-radius is not painted by the element,
so whatever sits underneath fills the corners. The frame comment asserted
that invariant ("nothing but .window paints an opaque surface") but nothing
enforced it: `html, body { background: var(--background) }` painted an
opaque rectangle across the whole window, so the corners had a ground to
show and the window read as a rectangle with a curve drawn on it.
Reset html/body to transparent under .win7. The override has to be
UNLAYERED, which is the non-obvious part: the html/body rule it overrides is
itself unlayered, and unlayered declarations beat every @layer regardless of
specificity, so the skinfix layer could not have reached it. Corrected the
frame comment, which stated the invariant as if it were free.
Maximized windows now drop the radius: a rounded maximized window leaves
four notches of desktop in the screen corners. title-bar.ts publishes its
existing `maximized` signal as `is-maximized` on its host, and the frame
reads it off its own direct child with :has(), avoiding a second
windowState subscription. The radius is declared three times in 7.css — on
.window and on both the ::before tint and the ::after glass layer — so all
three are zeroed; leaving one rounded would cut the corner off the square
frame.
NOT visually verified, and I could not verify it here: `import -window`
captures the window's own pixels without compositing, so a genuinely
transparent corner and an opaquely black one flatten to the same black in
the capture, and forcing png32 just fills alpha with 1. XWayland's root
window is not the composited desktop, and no compositor-level capture tool
(grim/scrot/maim) is installed. Both rules are confirmed present in the
built stylesheet; whether the corner now shows the desktop, and how the
maximized state looks, need eyes on the actual screen.
Co-Authored-By: Claude <noreply@anthropic.com>
The tab strip was a hand-rolled lookalike: bg-accent for the active tab and
an absolutely-positioned bg-primary underline, competing with a tab
component 7.css already ships. Replaced with its actual contract —
menu[role=tablist] plus one button[role=tab][aria-selected] per workspace —
so 7.css draws the joined inactive tabs and the raised white active one.
Per the agreed design the per-tab X is gone: real Win7 tab controls had
none, and a button nested in a button is invalid HTML. Closing keeps
Ctrl/Cmd+W and gains middle-click (auxclick, guarded on button 1 so the
right button never closes a tab). Drag-reorder moved onto the tab button
itself and still works.
Three things 7.css does that had to be accommodated, each found by looking
rather than by reasoning:
- Its selector is the DESCENDANT `menu[role=tablist] button`, so the +,
update pill and right-hand tools deliberately stay outside the menu.
- The strip is a .w7-toolbar, whose neutralizer strips background, border
and both pseudo-elements to make toolbar buttons flat. That flattened the
tabs to nothing, so the rule now exempts :not([role=tab]) — tabs want the
full Aero slab, their neighbours want flat.
- overflow-x:auto on the scroller forces computed overflow-y from visible
to auto, so the strip clips vertically. 7.css raises the active tab with
margin-top:-2px, which put its top border outside the scroll box and
silently deleted it. The strip now reserves 2px of padding-top, mirroring
the padding-left:3px 7.css already reserves for the same tab's -3px left
margin. Caught by pixel-scanning the tab's top edge against 7.css's own
reference markup in the harness: reference had #8e8f8f, ours jumped
straight from toolbar grey to tab fill.
Also: tabs.css hardcodes color:#222 on every tablist button, outside 7.css's
var system, leaving labels near-invisible in dark. Routed through a new
--w7-tab-c token declared light in :root.win7 and overridden in BOTH dark
blocks (attribute and prefers-color-scheme), matching the ListView and menu
literals. And its `display: block` outranks Tailwind's .flex, which jammed
the icon against the label — flex/gap restored in skinfix.
role="tabpanel" on the panel area was considered and skipped: 7.css gives it
padding:14px, margin-bottom:9px and its own border, which would fight the
split-pane layout for a join that cannot read anyway with the panel toolbar
row in between.
The harness grew a section 5 mirroring this markup, because the live app
starts with exactly one workspace and there is no way to click "+" from a
headless capture — multi-tab state is only checkable there. Verified in
both themes headless and on a live window capture.
Co-Authored-By: Claude <noreply@anthropic.com>
Three layouts collapsed in fe028a9, where the isMacChrome branches were
removed by a scripted edit that was wrong in two ways. The *bindings* were
dead by construction; the layout classes inside them were not, and nothing
replaced them.
- tab-bar: the edit appended a second `class` attribute to an element that
already had one. A duplicate attribute is silently dropped, so the
workspace strip lost h-9/pt-1/pl-1.5 entirely and collapsed under the
title bar. Merged into one attribute, with a comment saying why there is
exactly one. A template sweep found no other element carrying two.
- connection-manager + editor-window <header>: the edit deleted all five
[class.h-*]/[class.pl-*] bindings and put nothing back, leaving both
headers with no height and no left padding. Restored h-9 pl-3 (the
non-mac arm) and dropped the now-false "on macOS this row IS the
titlebar" comments — every window is frameless now.
Separately, the ListView never filled its panel: 7.css sets table-layout:
fixed but declares no width, so the table shrank to content and left a dead
grey gutter down the right of each pane. w-full is safe as a utility
precisely because 7.css declares no width for `skin` to outrank it with;
the unsized first <col> then absorbs the slack, which is what makes Name
elastic.
Verified on a live window capture, not from a build log: the tab strip has
its height back and both tables reach the pane edge. The connection-manager
window needs a click to reach and is unverified visually — its fix is a
verbatim copy of the editor-window one.
Co-Authored-By: Claude <noreply@anthropic.com>
Two requests, plus a correctness bug the first one exposed.
Drop shadow removed: the frame keeps only 7.css's inset 1px glass highlight.
That highlight shares the `box-shadow` declaration with the drop shadow, so it
has to be restated rather than the shorthand cleared. With no shadow there is
nothing to reserve a gutter for, so the window is full-bleed 100vh again
instead of inset by 9/13px.
Dark theme retoned after the Ares reference: the glass goes near-black
(--w7-w-bg #101013, #060607 on the darker tone) with a white caption, and
7.css's own alpha streaks/highlights become the gloss. Blue stays in the
accents — selection, sorted column, active tab — not in the glass. Two literals
7.css hardcodes had to be themed for this: the caption text (black + an 8-fold
white halo, which is invisible on black) and the glass streak gradient (its two
#fff5 corner shines washed a dark caption silver).
The bug: those dark restatements were keyed on [data-theme='dark'], which
matches ONLY the forced-dark path. SYSTEM dark sets no attribute — it runs
through the prefers-color-scheme media query — so every literal I had restated
for the ListView and menu was silently reverting to its light value for anyone
on system dark. Rather than duplicate each rule, the restated literals are now
tokens (--w7-lv-*, --w7-mn-*, --w7-cap-*, --w7-glass-title) declared in
:root.win7 with 7.css's own light values and overridden in BOTH existing dark
blocks, which is the pattern this file already uses. The rules themselves are
written once and theme through the tokens.
Also: the accent -> --w7-w-bg mapping is now light-only, or picking an accent
repaints the Ares glass with it.
Verified zoomed in both themes: light renders identically to 7.css's reference
beside it; dark shows black glass, white caption, subdued streaks.
Co-Authored-By: Claude <noreply@anthropic.com>
The menu now renders 7.css's real contract — ul[role=menu] > li[role=menuitem]
> button for actions, input[type=checkbox] + label for the checkable column
toggles, and dividers as .has-divider on the PRECEDING item (7.css draws them
as an ::after bevel, not as a node; the service's separator entries collapse
onto their predecessor at render time). With the markup right, the framework
supplies everything the hand-written version imitated or lacked: the left
gutter stripe, bevelled separators, the gradient selection band, the checked
box, and disabled/submenu styling we previously had none of.
Deleted: the w7-menu lookalike (both the button-slab stripper in `skin` and
the whole hand-authored Aero set in `skinfix`) and its w7-menu/w7-menu-sep
hooks. The "7.css has no menu component" claims in styles.css and
architecture.md were FALSE — dist/gui/menu.css is a full component; the old
markup (div > button) simply matched none of it. Both corrected to state the
real contract and the trap.
Keyboard now follows the platform instead of a highlight class: arrows move
REAL focus between items — 7.css styles :focus-visible identically to :hover,
and keyboard-initiated programmatic focus counts — and Enter/Space activate
buttons natively; only labels need a synthetic click. This also makes the menu
honest to screen readers.
Dark mode: menu.css hardcodes light literals (color:initial, white bevel
halves, the #b3d3f9 checked-box ring); restated in skinfix's dark arm like the
ListView's.
Also fixed, caught BY the harness: the Stage 3 frame sizing rule was keyed on
bare `.window`, forcing 100vh height onto every 7.css window (the harness's
demo windows blew up to full page). It is now keyed on `w7-frame`, a hook only
the three real window roots carry.
Verified zoomed in light and dark against 7.css's reference markup: gutter,
dividers, checkmark box, grey right-aligned accelerator all match; bundle
490.92 kB.
Co-Authored-By: Claude <noreply@anthropic.com>
Three defects reported from the real window, each with a distinct cause:
1. "Top and sides do not connect" — 7.css couples the glass treatment to the
title bar with a DIRECT-child selector (.window.glass > .title-bar), and our
.title-bar lived inside an inner div of <app-title-bar>, so the rule never
matched: the title bar fell back to the opaque base style with its own
border and radius, a blue slab sitting on the frame. The .title-bar class
(and the drag region + dblclick-maximize) moved onto the component HOST, so
the selector now matches and the title bar is the translucent streaked glass
fused with the frame.
2. "Black background near the window buttons" — .window.glass::after is
backdrop-filter: blur(4px). CSS cannot sample the DESKTOP behind a
transparent native window (there is no backdrop to read) and WebKitGTK
paints the unsampleable layer BLACK, so every see-through part of the glass
— the ring, the area around the controls — composited over black.
Neutralized in skinfix; the glass tint now shows the real desktop through
the transparent window, which is closer to Aero than a blur we cannot have.
(True blur-behind needs compositor support; GNOME exposes none to
X11/XWayland clients.)
3. "Shadow way too strong" — the 6px gutter hard-clipped 7.css's
2px 2px 10px 1px #000000b3 shadow into a solid dark band. The gutter now
matches the shadow's extents exactly: blur+spread-offset = 9px up/left,
offset+blur+spread = 13px down/right, so it falls off to nothing.
Verified on the REAL window this time: captured the running app with
ImageMagick import (the app runs on XWayland, so X11 capture works) before and
after. Before: black block around min/max/close, murky dark ring, detached blue
title strip. After: streaks span the top, controls hang from the frame edge as
on real Win7, ring is glass-blue, no black anywhere.
Known remaining (pre-existing, next stages): the tab-bar/toolbar row overlaps
itself top-left (tabs conversion, Stage 4); maximized windows should drop
radius/shadow/gutter.
Co-Authored-By: Claude <noreply@anthropic.com>
.window's 6px radius and its outer drop shadow are painted by the webview
OUTSIDE the window's own rectangle, so on an opaque surface they were simply
clipped — square corners, no shadow. chromeOptions now passes
`transparent: true`, the page paints no background of its own, and .window
takes a 6px margin as the gutter the shadow falls into (h-screen shrinks by the
same amount so the gutter does not produce a scrollbar).
Confirmed on Linux/WebKitGTK in a real run, which is the half the Firefox
harness cannot model:
GTK: Created window without decorations (custom titlebar)
GTK: Created transparent window
GTK WebKit: Applied transparent background to webview
Also caught by the run rather than shipped: `transparent` is set in
chromeOptions, which openSecondaryWindow spreads too — so the editor and
connection-manager windows became transparent while still rendering bare
content, i.e. floating widgets on nothing. Both roots now take the same
.window.glass frame and their own windowFocused signal. The editor's blur
binding had to merge with the existing flushDraft() call; a duplicated host key
is a TS1117 build error rather than a merge, which is how it surfaced.
The old anti-flash <style> in index.html is gone with the opaque backgrounds it
guarded — windows are created hidden and revealed on dom-ready anyway.
Not handled yet: a maximized Win7 window has no radius, shadow or gutter. The
6px margin currently applies in every state.
Co-Authored-By: Claude <noreply@anthropic.com>
The app rendered a bare .title-bar with no .window around it, which is why the
Aero frame, glass and drop shadow were missing and the title-bar controls sat
permanently in their inactive colours: EVERY control-glyph rule in 7.css is
keyed on `.title-bar.active` / `.window.active .title-bar`, and nothing ever
set .active.
- app.html's root is now `.window.glass`, so it picks up the 6px radius, the
1px frame, the `inset 0 0 0 1px #fff9` highlight, the drop shadow and the
backdrop-filter glass layer.
- The content below the title bar moved into `.window-body`, 7.css's recessed
inner panel (6px inset + its own border) — the thing that gives a Win7 window
its depth.
- `.active` binds to a new `windowFocused` signal, seeded from
document.hasFocus() and updated from window focus/blur. The webview receives
those when the NATIVE window's focus changes, so no RPC is needed.
Harness renders the shell focused and unfocused: active shows the red close
button and crisp glyphs, inactive desaturates, matching the reference.
Native window transparency (for the drop shadow and rounded corners to fall
outside the window rather than being clipped by an opaque surface) is
deliberately NOT in this commit — it is a native flag with a real chance of
misbehaving on Linux, so it gets its own change and a real-app check.
Co-Authored-By: Claude <noreply@anthropic.com>
7.css gives ONLY tr.highlighted a border (1px solid #aaddfa), and its table is
border-collapse: collapse — so a selected row genuinely grew 1px at top and
bottom and pushed every row beneath it down by 2px. Moving the cursor made the
whole list jitter.
Every tbody row now reserves that 1px as a transparent border, so selecting a
row changes colour and nothing about the geometry.
The rule has to live in `skinfix`, which outranks `skin` by LAYER regardless of
specificity — so a blanket transparent border there also blanks 7.css's own
highlight border. Both selected states therefore restate border-color
explicitly: the inactive pane via --w7-li-bd-hl (the same #aaddfa 7.css uses,
and already restated by the dark palette), the focused pane via --w7-el-bd-h.
Added a "selection geometry" pair to the harness — identical tables, one with a
middle row selected — so the baselines either line up or they visibly do not.
They did not before this change; they do now, verified zoomed.
Co-Authored-By: Claude <noreply@anthropic.com>
The previous model made the dotted rectangle the de-facto focus signal: the
cursor row got ONLY dots (and only in the active pane), so an unfocused pane's
cursor row disappeared entirely, and selection strength was the only band
difference. Wrong way around for a commander UI.
Now (matching Win7 Explorer / the commander convention):
- tr.highlighted = "selected via Space" OR "cursor row" — the template ORs
them, so the cursor row always carries the selection band in BOTH panes.
- Pane focus is what changes the band: strong Aero band (--w7-el-grad-h) in
the focused pane, 7.css's own faint inactive wash in the unfocused pane —
that wash IS Win7's inactive-selection colour, faint by design.
- The dotted rect is demoted to what it was in Win7: a keyboard-focus marker
layered ON TOP of the band, focused pane only. It is no longer the thing
that tells you which pane is active.
Harness now renders a focused and an unfocused pane stacked, with selected
rows and a cursor row in each; verified zoomed in light and dark.
Co-Authored-By: Claude <noreply@anthropic.com>
You were right that selecting a line looked off. Three separate problems, two
of them introduced by the Stage 2 dark-mode block:
1. The selection band was ERASED in dark mode. Stage 2 restated the dark
surface on `table > tbody > tr > *`, i.e. on the CELLS — but 7.css paints
tr.highlighted on the ROW, and a cell background paints over a row
background. The selected row kept only its 1px border and no fill. The base
surface now goes on `table` alone; cells stay transparent so the row band
shows through. Same trap re-applies to the cell separators, which 7.css
deliberately drops inside a selected row and the dark rule was re-adding.
2. The cursor rectangle was drawn PER CELL. `tr.is-cursor > *` puts a dotted
outline around every cell, so the focused row rendered as a row of dotted
boxes. Win7 drew one rectangle around the row — the outline belongs on the
`tr`.
3. Selection was barely visible even in light, and that one is 7.css's own
semantics rather than a bug: listview's `.highlighted` is Win7's *inactive*
selection, a near-white wash. Win7 drew the FOCUSED pane's selection as the
stronger Aero band. The table now carries `is-active` from the panel's own
isActive(), and the focused pane's selection uses --w7-el-grad-h/-bd-h,
which the dark palette already restates, so it recolours for free.
Verified by cropping and zooming the harness screenshots in both themes rather
than eyeballing a full page: full-row band, separators dropped inside it, and a
single dotted rectangle on the cursor row.
Co-Authored-By: Claude <noreply@anthropic.com>
The file list was a CSS grid of <div>s, so 7.css's listview component never
matched a single selector and Tailwind styled the whole thing. It is now the
plain <table> 7.css documents, which is what makes the Aero treatment appear.
- The column header row moved OUT of its own div and INTO <thead>: 7.css makes
thead position:sticky, so it stays put while the body scrolls, and it has to
be inside the same table to do that.
- Column widths moved from a computed grid-template-columns to <colgroup>,
because 7.css sets table-layout: fixed. `gridTemplate()` is deleted.
- Selection is now `tr.highlighted` (7.css's gradient band + #aaddfa border)
instead of bg-primary/15.
- The sort arrow was the text glyph ↑/↓ from `sortIndicator()`, which is also
deleted; the sorted column is now `th.highlighted.indicator[.up]` and 7.css
draws the real clip-path triangle in the header.
- Row height, padding, borders and fonts now come from 7.css, so --row-h and
the per-cell text/color/font utilities are gone.
Two things 7.css does not cover, added in `skinfix`:
- Dark mode. listview.css hardcodes light literals (#fff, #c0c1cd, #eee and a
white header gradient) rather than --w7-* vars, so it does NOT follow the
hand-authored dark palette — the header labels rendered invisible, dark on
dark. The dark arms restate those rules; light keeps 7.css's own values.
- The keyboard cursor. 7.css models selection only; Win7 drew the focused row
with a dotted focus rectangle over it, which is what tells "cursor" apart
from "selected" when they differ. That is `tr.is-cursor`.
Verified visually this time, not just by build log: harness screenshots in both
light and dark show the header gradient, sort triangle, cell separators,
selection band and cursor rectangle matching 7.css's reference markup. Backend
`tsc --noEmit` clean, `bun run build:ui` clean, bundle 489.83 kB (from 490.50).
Co-Authored-By: Claude <noreply@anthropic.com>
The dual-skin arrangement existed to let one build toggle between the modern
shadcn look and 7.css. Keeping both correct is what forced the cascade tricks
that fight 7.css, so the branch drops the toggle: this build IS the Windows 7
build.
- `skin` is gone from AppSettings/DEFAULT_SETTINGS, the settings store, the
SettingsService (signal, applySkin, persist payload), the options popover
control, and the i18n keys.
- buildPreload adds `.win7` unconditionally; `chromeOptions()` no longer takes a
skin and always returns frameless `titleBarStyle: "hidden"`.
- `IS_WIN7_CHROME` and `IS_MAC_CHROME` are deleted along with every
`isMacChrome` branch in the tab bar, editor window and connection manager.
Those reserved `pl-24` for macOS traffic lights, which frameless chrome
removes on every platform, so the branches were dead by construction.
- The title bar is no longer conditional — it is the window's only chrome.
- index.html keeps just the win7 anti-flash arm (four theme states).
Not done here, deliberately: stripping Tailwind's element styling. The
neutralizers in styles.css (notably `.win7 button { min-width: 0 }`) are still
load-bearing until each component's markup is converted to 7.css's real
contract, so they come out per component rather than in one sweep.
Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean; bundle
490.50 kB, down from 492.52 kB.
Co-Authored-By: Claude <noreply@anthropic.com>
Everything about this branch's look has so far been verified by reading build
logs, which is how a fully black window got reported as a clean boot. This adds
a way to actually LOOK at the skin.
- scripts/shot.ts — renders a page with Firefox headless (already installed; no
Chromium/puppeteer download) and writes a PNG. Firefox needs a dedicated
profile, since a running Firefox locks the default one.
- ui/dev/harness.html — static gallery pairing 7.css's own documented markup
("reference") against what the app currently renders ("ours"), for the window
frame, ListView, context menu, buttons, textbox, progress bars and tabs. Lives
outside ui/src so the Angular build never sees it.
Two traps found while building it, both documented in the files:
- The composed page MUST be written into ui/dist beside the stylesheet.
Firefox's security.fileuri.strict_origin_policy blocks a file:// document
from loading CSS in another directory, and the result renders completely
unstyled — indistinguishable from "the skin is broken".
- replaceAll, not replace, for the {{STYLES}} token: the harness documents the
token in a header comment that precedes the <link>, so a first-match replace
substitutes the comment and leaves the stylesheet unlinked. This produced one
false "everything is unstyled" result before it was spotted.
What the first screenshots already confirm, visually:
- The window has no Aero frame, radius, drop shadow or glass, and its title-bar
controls sit in their inactive colours — we render a bare .title-bar with no
.window.glass.active wrapper.
- The file list gets none of the ListView treatment (header gradient, sort
indicator, cell separators, selection band) because it is a div grid.
- The context menu gets none of the menu treatment (gutter, dividers, disabled
state, real checkmarks).
- NEW, not previously predicted: `.win7 button { min-width:0; padding:
revert-layer }` — added to stop Tailwind icon buttons ballooning — also
crushes legitimate dialog push buttons well below Win7's 75x23 metrics.
- Dark mode: ListView header labels and tab labels are invisible (dark on
dark), since listview.css hardcodes light literals the dark block never
restates.
Caveat recorded in the script: Firefox is Gecko, not the WebKitGTK engine the
app ships on. Good for skin fidelity, useless for native window transparency.
Co-Authored-By: Claude <noreply@anthropic.com>
main retired electrobun's native context menu (a documented no-op on Linux) in
favour of an HTML one on every platform. Merging that here needs the menu to
look right under the Win7 skin as well.
Conflicts, all resolved by keeping both sides:
- src/bun/index.ts — createAppRPC keeps the branch's mainWindow getter, drops
main's removed contextMenu dep.
- ui/src/app/app.ts — imports both TitleBar and ContextMenu.
- ui/src/app/platform.ts — kept the branch's version (its IS_MAC_CHROME accounts
for the frameless win7 chrome, and MOD_LABEL correctly keys on the real
platform rather than the chrome), plus exporting IS_MAC and adding IS_LINUX
for the menu's per-OS shell wording.
Aero menu (branch-only): 7.css has no menu component, so it is hand-authored
across both layers — the row <button>s shed their push-button slab in `skin`
(revert-layer only reaches `utilities` from there), and the square-cornered
slab, hairline border and gradient selection band are stated in `skinfix`.
Built strictly from existing --w7-* vars, never literal hex, so the
hand-written dark Win7 palette recolours it for free. Hover and the keyboard
cursor share one band (the component marks the cursor with `bg-accent`, which
is overridden alongside :hover) as a real Win7 menu does.
Also answers a question the merge raised: frameless windows DO work on Linux.
The GTK wrapper honours titleBarStyle "hidden" — it calls
gtk_window_set_decorated(false) and logs "GTK: Created window without
decorations (custom titlebar)". Verified on GNOME/Wayland and documented in
architecture.md.
Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (492.52 kB
initial, still under the 500 kB budget), app launches with the skin active,
frameless, no console errors and no GBM failures.
Co-Authored-By: Claude <noreply@anthropic.com>
On the NVIDIA proprietary driver the app booted, logged "miniSCP started",
reported no error — and painted a completely black window, with stderr showing
"Failed to create GBM buffer of size WxH: Invalid argument" and an X11
GLXBadWindow. WebKitGTK's DMABUF renderer cannot allocate buffers there;
WEBKIT_DISABLE_DMABUF_RENDERER=1 is the upstream workaround. Pre-existing on
Linux, unrelated to the context-menu change — it just had never been looked at
because nothing on Linux had been run before.
The app CANNOT apply this to itself. WebKit reads the variable when it forks
its web process, and Bun's `process.env` writes do not propagate to child
processes (verified: a spawned child sees it unset), so setting it in
src/bun/index.ts is inert even from a module imported before `electrobun/bun`.
It has to be in the environment before the app process starts, which means
every launch path must inject it:
- scripts/linux-gpu.ts — shared `launchEnv()` helper, gated on
/proc/driver/nvidia so Intel/AMD keep hardware compositing, and never
overriding an explicit value from the user's environment.
- scripts/dev.ts — uses it for `bun run dev`.
- scripts/start.ts (new) — `bun run start` previously called `bunx electrobun
dev` straight from package.json, where no env could be set; it now goes
through a script like dev does.
- scripts/release.ts — writes a `miniSCP.sh` wrapper beside bin/launcher in the
Linux tarball, since a packaged app goes through none of the above.
Documented in gotchas.md (including that a "does it boot?" log check does NOT
catch this — the log looks healthy) and platform-notes.md, with the smoke test
updated to require that the UI actually renders.
Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean, and
`bun run start` now boots with zero GBM/GLX errors.
Co-Authored-By: Claude <noreply@anthropic.com>
`bun run start` died on Linux with ERR_DLOPEN_FAILED (missing
libayatana-appindicator3.so.1). Fixing the environment exposed the real gap:
electrobun's ContextMenu.showContextMenu is a documented no-op on Linux, and
since the UI suppresses the webview's own right-click menu, EVERY panel context
menu was silently dead there — Send to Other Panel, Open, Rename, Permissions,
Delete, Copy Path, Reveal and the column picker all unreachable. Electrobun
itself logs "Application menus are not supported on Linux. Implement menu UI in
your webview HTML instead." at boot.
- Context menus are now HTML on EVERY platform (ui/src/app/context-menu.ts +
context-menu.service.ts), not just as a Linux fallback: one implementation
cannot drift from a second one. The item builders are ported verbatim from
the old bun menu, so ordering, per-platform labels and the Windows-local
chmod hide are unchanged, and picks are emitted as the same `ctx-*` / `col:*`
strings through RpcService.emitMenuAction — App.handleMenuAction is untouched
and still the single dispatcher for both the macOS menubar and right-clicks.
Adds what the native menu gave for free: arrow/Enter/Escape keys, viewport
edge-flip, close on blur/resize/scroll. The Edit item now shows the real F4
hint instead of the native menu's "e" mnemonic.
- New shellItemAction RPC replaces showRowMenu/showColumnsMenu — the only part
the webview can't do itself (reveal, open-with-default, copy path). Deletes
src/bun/context-menu.ts entirely.
- release.ts gains a linux-x64 target. Electrobun emits only the .tar.zst
updater blob on Linux, so the artifact is a .tar.gz of the assembled app
folder, mirroring the Windows portable zip.
- Docs: platform-notes gets a Linux runtime-dependency table (AppIndicator is
linked HARD — a missing lib is a boot failure, not a degraded tray), the
ldd diagnostic, GNOME tray caveat, known issues and a first-boot smoke test;
gotchas gets the ERR_DLOPEN_FAILED trail and the benign GLX/GBM startup
noise; architecture/build-and-release/README updated accordingly.
Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (399 kB
initial), app boots on Linux with no errors beyond the documented benign
warnings. NOT verified on macOS/Windows — the native menu is retired on those
platforms too, so the menus need eyeballing on a Mac before the next release.
Co-Authored-By: Claude <noreply@anthropic.com>
Evaluation branch: a full Aero skin, including frameless windows with our own
title bar, switchable against the existing modern look.
The app already had a four-axis runtime skinning mechanism and zero component
stylesheets, so the skin is one new setting plus one stylesheet section rather
than a parallel UI. The key move is cascade order: 7.css is imported into a
`skin` layer ABOVE Tailwind's `utilities`, so its bare element rules outrank
the ~1900 inline utility classes already in the templates. Template changes are
limited to additive, skin-inert hooks (ARIA roles, `has-scrollbar`,
`w7-toolbar`, `w7-btn`).
- styles.css: explicit layer order, `7.css/dist/7.scoped.css` via `layer(skin)`,
a token bridge re-pointing the existing semantic tokens at Win7 values (no new
tokens, so `@theme inline` is untouched), a hand-authored dark Win7 palette,
and neutralizers that `revert-layer` 7.css's dialog-button metrics back to the
utility layer. Neutralizers must live inside `skin`, not `skinfix` —
`revert-layer` only reaches `utilities` from one layer up.
- Frameless chrome: `chromeOptions(skin)` is now the single place either skin's
frame is decided. `titleBarStyle: "hidden"` on every platform, which is also
the only way to drop the macOS traffic lights. New `title-bar` component
mounted by all three window roots.
- Window controls over RPC (minimize/toggle-maximize/close/is-maximized), served
per window kind so the X reuses each one's existing close path — editors still
get their dirty prompt. electrobun has no maximize event, so the state is
derived from `resize`; double-click-to-maximize is implemented here too.
- Font: Noto Sans vendored (OFL-1.1) under ui/src/fonts, since Segoe UI is
absent off Windows and not redistributable.
Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (486 kB initial,
under the 500 kB budget), app launches with the skin active and no console
errors. The rendered result has not been eyeballed — screen capture and
accessibility are both denied to the shell on this machine.
Co-Authored-By: Claude <noreply@anthropic.com>
The darker tone sat at #090908 for --background while --chrome was already
pure black, which both inverted the elevation ramp (frame strips darker than
the content beside them) and missed the point of an AMOLED tone — only a
true #000000 pixel is actually off on OLED.
--background now collapses into --chrome at pure black, so the main window,
connection manager and editor all render black end to end: panes, bars,
sidebar, gutter and textarea. Elevation is kept where it earns its place —
dialogs, popovers and toasts stay #181817, hover and selection #272726 — and
--border/--input go from 12%/15% to 16%/18% white, since on pure black those
lines are the only thing left defining panes, bars and rows.
No plumbing needed: buildPreload already bakes data-dark-tone onto every
window pre-paint, and tone changes broadcast live to open windows.
Co-Authored-By: Claude <noreply@anthropic.com>
The dark palette was shadcn's cool zinc (#202023, hue ~286). Anchor it on
#1a1a19 instead — cds gray-830, a warm near-neutral (hue 107) — and retone
the whole dark neutral family to match, since a warm background under cool
panels clashes. Lightness ramp is preserved; chromatic tokens (destructive,
success, icon hues, accent formulas) and the light palette are untouched.
Also introduce --chrome so the app isn't one flat tone: frame strips (tab
bar, status bar, transfer bar, editor/manager header+footer, connection
sidebar) now sit one step below the content panes, with popovers and hover
above them — the standard dark-UI elevation order.
Dark ramp: #151514 chrome, #1a1a19 content, #20201f card, #2e2e2d hover;
the "darker" tone drops chrome to true black. Light gains only a zinc-50
chrome step.
Co-Authored-By: Claude <noreply@anthropic.com>
The main feature (copy selection to the other panel, F5) is now labeled
"Send" in the toolbar button, tooltips, and context menu across all 7
locales. Internal identifiers (copyToOtherPanel, ctx-copy, i18n keys)
and secondary strings (toasts, status bar, errors) are unchanged.
The context-menu entry moves ahead of Open/Edit as the first row item.
README, marketing site, and the Windows QA checklist updated to match.
Co-Authored-By: Claude <noreply@anthropic.com>
~200 MB macOS / ~300 MB Windows is normal for a Bun + system-WebView app;
record the process breakdown, why Windows (WebView2/Chromium) is higher,
that it's not a bug, and the only real growth levers, so it isn't
re-investigated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Quit item was role:"quit" with no accelerator, so ⌘Q was never
bound. Make it a custom action + accelerator "q" dispatched to
Utils.quit(), which emits before-quit so the existing guard (active
transfers / unsaved editors, native confirm dialog) runs instead of the
OS terminating directly. Handled before the manager-open check so ⌘Q
works with the connection manager open too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wires electrobun's Updater to the /dl download host. The packaged app checks
https://miniscp.com/dl on launch + every 6h; when a newer build exists, an
accent-colored "update tool" appears next to Connections in the tab bar. It
announces the update, becomes its own inline progress bar while downloading,
then flips to "Restart to apply" (two-step: download, then user-triggered
relaunch).
- electrobun.config.ts: release.baseUrl = https://miniscp.com/dl (baked into
version.json at build; miniscp.com fronted by an external proxy for TLS).
- rpc.ts: checkForUpdate/downloadUpdate/applyUpdate requests + updateStatus push.
- rpc-handlers.ts + index.ts: call Updater; poll on launch+6h; stream
onStatusChange progress. checkForUpdate self-guards the dev channel.
- rpc.service.ts + new updater.service.ts: signal-based state
(available -> downloading -> ready, ready derived from downloadUpdate ok).
- tab-bar.ts: single bg-primary pill; progress fills the pill itself.
- icons.ts: register mynaDownload. i18n: tabBar + menu keys across all locales.
- menu.ts + app.ts: "Check for Updates…" app-menu item -> UpdaterService.check().
- docs: build-and-release + architecture updated.
Unsigned builds still update (applyUpdate strips the macOS quarantine xattr).
Verified: tsc --noEmit (exit 0) + build:ui AOT (exit 0). Live pill + full
download/apply flow require a two-build test on the host.
Co-Authored-By: Claude <noreply@anthropic.com>
Self-contained, no-JS landing page (tinygrad-minimal) that doubles as the
artifact download host, served by Caddy on an internal :80 box.
- site/index.html: static page, "Same tool, only a little different", feature
highlights (remote->remote, symmetric panels, ssh_config awareness, edit-in-
place, self-copy guard, ...), fixed stable download links, FAQ (unsigned notes).
- site/Caddyfile: one flat /dl/ folder with browse + range requests and per-type
Cache-Control (patches immutable, update.json no-cache, installers short).
- site/deploy.sh: drop a build's artifacts/stable-* into /dl/ (renames the win
portable zip to a stable name).
- Flat folder matches electrobun's artifacts/ layout, so it's also the future
updater baseUrl; per-build folders/symlink dropped in favor of it.
- docs/build-and-release.md: document the download host + deploy flow.
Verified: caddy validate + live smoke test (links, cache headers, 206 range,
404 fallback, deploy.sh copy/rename) against a temp root.
Co-Authored-By: Claude <noreply@anthropic.com>
Custom signal-based runtime (no i18n dependency): shared src/shared/i18n
dictionaries, ui I18nService.t() reactive by construction so language
switches live, and a backend i18n.ts for native menus + thrown errors.
Every user-facing string keyed across templates, toasts, dialogs, file
kinds, columns, macOS menu/context menus, and errors. Language picker in
Options (auto-detects OS locale, falls back to English); switching
relabels the webview and native chrome instantly. English/Spanish/French/
German fully drafted; Chinese/Japanese/Basque drafted, flagged for native
review. Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codify three standing behaviors so they persist across sessions: ask
clarifying questions in plan mode until unambiguous; verify then
auto-commit + push after an approved plan; keep docs/ up to date in the
same change.
Co-Authored-By: Claude <noreply@anthropic.com>
Local panels are fs.watched (LocalDirWatcher + setWatchedDirs -> pathRefresh);
remote panels poll every 3s. New refresh({silent}) skips the loading/busy
flags and touches no DOM, snapshotting sessionId+cwd to discard results if the
user navigated mid-flight. Only the active tab is live; switching tabs
refreshes the new pair. A future on/off setting is left as commented
breadcrumbs. Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Folders become Stacks (connections.json v2, no v1 migration): collapsible
stack-first list, open-stack-in-tabs (first reuses a pristine tab), drag
connections between stacks. More... reveals a per-connection Local
directory applied to the other panel when local and untouched. Import
reads a WinSCP.ini export incl. deobfuscated passwords.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Connecting blanks the list to a fading spinner with the target name and
a Cancel button (token discards late results and orphan sessions).
Listings >300ms dim the list under a spinner. All panel input is
blocked while busy; the source picker stays usable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tone row appears under Theme when the effective theme is dark.
Grey palette uses macOS-like #26262a background.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Action buttons, server picker, and its dropdown now follow the size
setting via new --action-h/--action-font/--action-icon variables.
Default tier matches the old fixed sizes; tab bar stays fixed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read-only group below saved connections; hosts resolve HostName/User/
Port/IdentityFile (Include, wildcards, %-tokens) and connect on click.
New ssh-agent auth kind covers hosts without a usable identity file;
ProxyJump/ProxyCommand hosts are shown disabled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>