Commit Graph
29 Commits
Author SHA1 Message Date
jgamboaandClaude 9f2a076e08 Scale stage 1: everything the UI sizes with is now rem
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>
2026-08-16 18:02:41 +02:00
jgamboaandClaude b35c30d5ab Stage 3: split styles.css into a shared base plus per-skin sheets
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>
2026-08-16 04:53:57 +02:00
jgamboaandClaude 06a14fb6a1 Make w7-flat actually flatten; document this round
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>
2026-08-16 04:10:36 +02:00
jgamboaandClaude 7921b2986b Let the frame corners be transparent; square them when maximized
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>
2026-08-16 04:04:04 +02:00
jgamboaandClaude aaa12c3e23 Workspace tabs are a real 7.css tablist
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>
2026-08-16 03:56:19 +02:00
jgamboaandClaude 7b15e0982e Ares-style dark glass; drop the window drop shadow
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>
2026-08-16 03:27:40 +02:00
jgamboaandClaude 0ae2a62229 Stage 4a: context menu is pure 7.css; retire the lookalike and the false claim
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>
2026-08-16 03:20:55 +02:00
jgamboaandClaude 1f773261a8 Fix glass frame: title-bar fused to frame, no black around buttons, sane shadow
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>
2026-08-16 02:51:24 +02:00
jgamboaandClaude fe75904872 Stage 3b: transparent native window so the Aero frame is a real frame
.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>
2026-08-16 02:35:44 +02:00
jgamboaandClaude 2db367318d Stop rows shifting 2px when selected
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>
2026-08-16 02:15:54 +02:00
jgamboaandClaude 66076cf1d7 Pane focus reads from band strength, not from the dots; cursor visible in both panes
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>
2026-08-16 02:10:36 +02:00
jgamboaandClaude 4102020f5c Fix ListView selection and cursor rendering (regressions from Stage 2)
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>
2026-08-16 02:00:30 +02:00
jgamboaandClaude 11c3d419c7 Stage 2: file panels become a real 7.css ListView
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>
2026-08-16 01:48:20 +02:00
jgamboaandClaude 6bb4c3cd7d Merge main into feat/7css-skin + give the HTML context menu an Aero skin
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>
2026-08-15 14:59:02 +02:00
jgamboaandClaude 07825c1bd1 Add a toggleable Windows 7 (7.css) skin behind a new skin setting
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>
2026-08-14 15:02:27 +02:00
jgamboaandClaude fc03fa6216 Make the darker tone true-black AMOLED
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>
2026-08-14 14:00:01 +02:00
jgamboaandClaude e4ba846719 Retone dark theme to warm gray #1a1a19 with layered surfaces
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>
2026-08-14 13:20:47 +02:00
jgamboa 9a4420fdc2 Toast fixes 2026-07-13 18:06:48 +02:00
jgamboaandClaude Fable 5 dda596c50c Panel busy states: Connecting placeholder with Cancel, slow-load overlay
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>
2026-07-13 16:36:44 +02:00
jgamboa 9f79bb0b17 Change dark coloring 2026-07-13 15:53:37 +02:00
jgamboaandClaude Fable 5 bdae49ba24 Dark tone setting: grey Dark (default) vs near-black Darker
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>
2026-07-13 15:43:01 +02:00
jgamboaandClaude Fable 5 628ccebd9a Rename Text size to Interface; scale panel header with it
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>
2026-07-13 14:26:25 +02:00
jgamboa 09792c93cf Fix light-dark scroll bars 2026-07-13 14:19:46 +02:00
JokinandClaude Fable 5 44f077380f File-type icon colors with Plain/Multi setting (multi default)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:31:10 +02:00
JokinandClaude Fable 5 2f4bc63ac8 Accent tone setting with macOS-style swatches (indigo default)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:26:35 +02:00
JokinandClaude Fable 5 70947da129 Text size setting also scales row height and path-bar controls
--row-h and --path-ctl-h CSS vars per size level; Compact brings the path
input/buttons to 24px, aligned with the panel action buttons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:12:36 +02:00
JokinandClaude Fable 5 ff62c07f15 Text size setting for path bar + file list (Compact/Default/Large)
Segmented row in the options popover; applied via data-text on <html> driving
CSS variables (--list-font / --list-meta-font / --path-font), persisted in
settings.json like theme and speed unit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 17:30:12 +02:00
JokinandClaude Fable 5 6048d5c814 v1.2: overwrite confirm, transfer speed/ETA, settings + themes, fused macOS titlebar
- Overwrite confirmation: name-collision detection before enqueue; dialog shows
  source vs destination (size, mtime, 'newer' badge) with per-item Skip/Replace
  and batch Skip all / Replace all; type mismatches auto-skip with a toast;
  cancel aborts the whole operation
- Transfer speed + ETA: UI computes smoothed bytes/sec from progress deltas
  (callback bus makes deltas meaningful); shown in the expanded queue rows and
  the status-bar summary; unit follows the speed-unit setting
- Settings: bun-side SettingsStore (settings.json, 0600, atomic) + get/save RPC;
  SettingsService; gear popover on the tab row (theme segmented control, speed
  unit); persisted across restarts
- Light theme: data-theme on <html> drives token overrides; absent = follow OS
- macOS fused titlebar: hiddenInset + trafficLightOffset, tab row becomes the
  draggable titlebar (app-region drag/no-drag classes); other platforms keep a
  separate titlebar (gated by a platform value injected via window preload)
- README: known-issues note for the upstream Bun/Electrobun FFI crash

Verified: tsc + ui build clean; SettingsStore roundtrip + 0600 headless;
app boots and the UI fetches settings over RPC at startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 17:14:50 +02:00
JokinandClaude Fable 5 8092cb0c08 P0: Electrobun + Angular 22 + Tailwind skeleton boots under Bun
- Electrobun 1.18 shell with branding in src/shared/branding.ts
- Angular 22 zoneless UI in ui/ (workspace), Tailwind v4 shadcn-style tokens
- ng-icons/mynaui icon registry
- Node-version shim so Angular CLI runs under Bun without Node installed
- ssh2 SFTP verified working under Bun against test box

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 13:23:15 +02:00