Previously the disclosure was an absolute child of the picker row,
clipped by the sidebar's overflow_hidden, with no way for an outside
click to close it. Real popovers (Arc, Linear, Raycast) close the
moment you click anywhere else, and they spill past their host
container so a long workspace list isn't cut at the sidebar edge.
Lift the disclosure to the render_browser tree:
* `render_workspace_disclosure_backdrop` paints a fullscreen
transparent layer that closes the picker on mouse-down — no
on_click so the close fires immediately, before any synthetic click.
* `render_workspace_disclosure` paints next, pinned to fixed
window-relative anchor coords (top 98, left 66, width 180) that
match the picker pill's row in the default sidebar layout.
The picker row inside the sidebar header drops the inline disclosure
child entirely.
cargo test --workspace: 440 passed, 0 failed.
User reports the sidebar can't be resized. Until now sidebar width
was only mutable through the COLLAPSED/DEFAULT toggle and through
typed settings. Real browsers (Arc, Dia, Zen) all let you drag the
right edge of the sidebar to size it live.
Wire it through the existing space.sidebar_width_px:
* `ElyShell.sidebar_resize_origin: Option<(f32, u16)>` records the
cursor-x and width-px at the moment the handle is grabbed.
* `begin_sidebar_resize` / `end_sidebar_resize` set and clear it.
* Window-level `on_mouse_move` consults the origin first; while held,
it forwards the delta to `set_active_sidebar_width` clamped to
220–480 px so you can't accidentally annihilate either pane.
* Window-level `on_mouse_up` releases the drag.
* The handle itself is a 6 px transparent strip pinned to the right
edge of the expanded sidebar, `cursor_col_resize`, with a soft white
hover. mouse-down captures the origin; the rest is window events.
cargo test --workspace: 440 passed, 0 failed.
The design's `--ely-font-sans` is Geist; we were registering only
Newsreader. Body text everywhere fell through to GPUI's
`.SystemUIFont` (SF Pro on macOS) — too humanist for the geometric,
calm tech aesthetic the design wants.
Bundle Geist-Regular (SIL OFL 1.1, 126 KB), register it alongside
Newsreader, and set the root window's `.font_family(SANS_FAMILY)`.
SERIF_FAMILY callsites continue to override per element so headlines
keep using Newsreader.
Only Regular is bundled — GPUI synthesizes weights from the metrics,
so 500/600 still read correctly. Bold/italic file additions are a
later polish.
Picking a workspace previously expanded the sidebar header inline:
the disclosure list was a normal flex child, so opening it shoved
the home anchor row, every launcher, and the tab list down by ~200
px. User correctly flagged this as a popover, not an accordion.
Move the disclosure under the picker row as an absolute overlay
(`.relative()` on the picker row + `.absolute().top_full()` on the
disclosure) with the existing fade_in animation. Picker row height
stays constant whether the disclosure is open or closed; the list
floats over whatever lives beneath it inside the sidebar's clip.
Picking a space still calls `close_workspace_picker`, and toggling
the pill still hides it. Outside-click dismiss is the next polish
pass.
Round 12 left the sidebar header at `pt(36)` so the title row landed
two rows below the macOS traffic lights — the screenshot shows them
stacked vertically instead of sharing a row. The design renders the
dots and the workspace title side-by-side.
Tighten the header's top inset to 8 px and reserve the first 68 px of
the title row for the traffic lights via `TRAFFIC_LIGHT_RESERVE`. The
title now sits inline with the dots and the workspace picker row
follows underneath at the design's gap.
Round 12 painted the inner highlight ring through an absolutely
positioned overlay that covered every glass panel. User reports the
right pane was unclickable, the search bar wouldn't take input, and
sidebar tab close buttons never appeared on hover. Even though the
overlay div had no listeners, in this layout it was racing the
parent's hit-test for the same pixels — the close glyph in
`render_launcher_row` is `opacity(0)` until `group_hover` fires, and
the overlay was preventing that hover from registering.
Move the highlight onto each panel's own `.border_1()` so the ring is
part of the panel paint, not a separate overlay. Painted, never
hit-tested. The four wired callers (expanded sidebar, compact
sidebar, main pane, command overlay panel) now each carry their
inner border directly. `chrome::glass` deletes; nothing else used it.
The 1 px brighter top-edge specular sliver from the design is gone —
GPUI 0.2.2 has no asymmetric border colors and live clicks beat that
single-pixel polish.
cargo test --workspace: 440 passed, 0 failed.
The design's TopBar.url placeholder reads `Search ELY or type a
command…`. We were shipping `Search or enter address`, the generic
URL-bar string from when the omnibar didn't yet host commands.
Match the design copy so the empty-state on `ely://new-tab` matches
home.jsx and command.jsx exactly.
home.jsx renders the Open Notion pill with `<Brand.Notion s={12}/>` —
the actual N-in-a-rounded-square mark. We were drawing a generic
BookOpen icon there, so the pill read as just another quick action
instead of a branded shortcut.
Split `render_pill` so it accepts an `AnyElement` leading slot; the
two icon-only pills go through `render_pill_icon`, the Notion pill
hands in `render_glyph_for(Some("notion.so"), …, 14.0)` directly.
cargo test --workspace: 440 passed, 0 failed.
Match plugins.jsx, where every card cover gets a different pastel→bold
gradient (Pink+Blue, Mint+Violet, Cream+Coral, etc). Previously every
card painted the same Pink→Blue ramp, making the marketplace grid feel
uniform.
`plugin_cover_gradient(name)` hashes the plugin name into the design's
8-stop palette so the same plugin always lands on the same ramp without
needing any new manifest fields.
cargo test --workspace: 440 passed, 0 failed.
Match home.jsx's `View all history <I.ArrowRight size={11}/>` rather
than the unicode arrow we shipped earlier — same gap-4 lockup, real
icon. Trivial visual swap; no behavior change.
The design's home hero pairs the greeting with a Sunrise glyph in the
warm horizon orange (#e89a6e). Previously we showed `IconName::Sun`
in `ACCENT_LIGHT` regardless of phase, so an evening user saw a noon
sun next to "Good evening, Alex".
Promote the day phase to a `DayPhase` enum so hero.rs can pick the
glyph + tint per phase: warm orange Sun in the morning, amber Sun in
the afternoon, cool violet Moon in the evening. Tests now compare
against the enum instead of the former `&'static str` wrapper.
Bonus: third quick-launch pill swaps from "Open History" (Undo2) to
"Open Notion" (BookOpen → notion.so) to match the design's third pill.
cargo test --workspace: 440 passed, 0 failed.
Match the design's `0 1px 0 rgba(0,0,0,0.05), 0 12px 30px -16px
rgba(0,0,0,0.18)` shadow stack on every split pane card and tighten
inter-pane gap from 12 px (`gap_3`) to the design's 10 px. Without the
ambient shadow the panes read as flat seams against the wallpaper;
with it they sit on the canvas the way the design renders them.
Grid axis now also gets a 10 px row gap so the second row doesn't
butt up against the first.
cargo test --workspace: 440 passed, 0 failed.
The design's split.jsx Pane header leads with an 8 px brand-accent dot,
the lock indicator, host name in INK, then the tab title in INK_4.
The previous header rendered a 24 px brand glyph plus the URL path —
visually heavier and less informative once both panes share most of
their URL prefix.
* brand_glyph: `brand_accent_color(brand)` and `accent_color_for_host`
collapse each Brand to one design-matching dot color (Figma 7c6cf7,
Linear 5e6ad2, etc.).
* split_pane: 32 → 30 px header, glyph → 8 px dot, URL path → tab
title in INK_4. `pane_path_label` deleted; nothing else used it.
cargo test --workspace: 440 passed, 0 failed.
Closes the three "platform-impossible" gaps from the design audit
without resorting to hacks or shaders.
* chrome::glass — `render_inner_highlight(radius)` paints the design's
`box-shadow: inset 0 0 0 1px` ring as an absolute overlay div with a
1 px top-edge highlight sliver. GPUI 0.2.2's BoxShadow has no inset
flag, so the composite is the only way to keep the panel content
unclipped while the ring rides on top.
* chrome::animations — `blink` and `fade_in` wrap GPUI's first-class
`with_animation` driver. Square-wave caret in the command header,
180 ms opacity ramp on the command backdrop and workspace
disclosure list.
* chrome::sidebar — panel_bg now tints toward the active wallpaper
theme so the wallpaper bleeds through every glass surface, the
closest honest substitute for backdrop-filter without a shader pass.
* command_overlay split: row helpers move to chrome::command_rows so
command_overlay drops from 504 → 232 lines, well under the 500-line
ceiling. No behavior change.
cargo test --workspace: 440 passed, 0 failed.
The design uses asymmetric spacing — 48 px between the hero and the
favorites grid, and only 16 px between favorites and the Continue +
Activity recap row. The previous uniform gap(40) flattened both gaps
to the same value. Wrap each section in a div with the correct margin
so the lower row hugs the favorites strip the way the design does.
The reading-list cover badge previously read "Reading List · 1" when a
featured entry existed and "Reading List · 0" otherwise — a binary
flag stamped onto a count format. The user's actual reading list can
hold many entries, and the design clearly intends the badge to show
the total ("Reading List · 5" in the mock).
Pass snapshot.reading_list.len() through render_reading_list_cover so
the badge mirrors the real count.
Design overflow scoping uses maxHeight:440 on the inner results region,
not the whole panel. The previous max_h(540) on the panel capped the
overall card height (header + results + footer) and let the results
expand to fill, which clipped the footer when many sections were
visible.
Now the panel grows to fit content while the results section caps at
440 px and overflows internally — matches the design's
panel-with-scrollable-middle layout exactly.
- Nav-back / nav-forward now render lucide ArrowLeft / ArrowRight to
match the design's TopBar exactly. Chevron arrows read as accordion
toggles in this design language; arrow heads read as page navigation.
- Omnibar uses uniform px(14) horizontal padding instead of asymmetric
pl(14) pr(8), so the inner chips inherit the design's symmetric breathing
room. The flex/gap layout already keeps the chips at the right edge.
The footer chips already advertised ↑↓ to navigate and ↵ to open;
they now match reality.
- chrome::command_match exposes COMMAND_ACTIONS / matching_actions
alongside the tab/history/bookmark match helpers and a
CommandSelection enum + visible_command_rows that returns the flat
ordered list of activatable rows. command_overlay drops its private
copies of the action const and matcher and consumes them from the
shared module so render and key handling share one source of truth.
- ElyShell tracks command_selected_index with command_select_next /
command_select_prev (cyclic, no notify when index doesn't change)
and activate_selected_command which dispatches the right shell call
for the currently selected CommandSelection variant and dismisses
the overlay. Dismissing command mode resets the index to 0.
- The shell root captures key_down via on_command_overlay_key_down.
When the live snapshot's command_query starts with '>', up / down
/ enter run the matching helper and the event stops propagating so
the omnibar input doesn't move its caret.
- Each rendered row receives a `selected` flag. The selected row gets
the design's tinted bg + accent-bar on the left edge so the active
result is unambiguous at any keyboard step.
Cursor-reach reveal now fulfils the "Slide in on cursor reach"
description on the design's Hidden-on-hover layout card.
- BrowserCore::active_space_sidebar_width() returns the current
sidebar tier without cloning a full BrowserSnapshot, so the
mouse_move hot path stays cheap.
- on_window_mouse_move on the shell root hits-tests the cursor x
against REVEAL_THRESHOLD_PX (24 px from the left edge). Outside
the reveal/collapse zones it returns immediately, so 99 % of
mouse moves never even read the snapshot.
- When in HIDDEN mode and the cursor crosses the reveal threshold,
expand_hidden_sidebar fires (its early-return on already-expanded
state prevents notify spam). When the cursor passes
COLLAPSE_THRESHOLD_PX (shell inset + default sidebar width + 24 px
buffer), collapse_hidden_sidebar fires.
- Click-to-expand on the rail and click-on-backdrop-to-collapse
remain as predictable fallbacks.
Domain ships HIDDEN_SIDEBAR_WIDTH_PX = 8 alongside the existing
collapsed/default tiers. ElyShell tracks sidebar_hover_expanded with
expand_hidden_sidebar / collapse_hidden_sidebar helpers; switching
back to a non-hidden width via the layout cards or core API resets
the flag automatically so the sidebar can never be both hidden and
expanded after a mode change.
Renderer:
- render_sidebar takes a sidebar_hidden flag and routes to a thin
8 px clickable rail (hover bg + click expands) when the active
space's width is at HIDDEN.
- While the rail is expanded, render_browser overlays a transparent
backdrop + the full default-width sidebar absolutely positioned in
the shell inset, so the main pane content never reflows.
- collapsed_sidebar_active still drives the COLLAPSED-tier compact
sidebar; the hidden tier is opted out of that path.
Appearance form:
- Layout cards section gains the design's third "Hidden on hover"
card with a 6 px sliver preview that mutates the active space to
HIDDEN_SIDEBAR_WIDTH_PX. LayoutMode now derives id/width/preview
from a small enum so adding a fourth mode would be one match arm.
The hover-expanded state never persists into the domain; once the
user switches modes or clicks the backdrop, it collapses cleanly.
Clicking the picker pill now opens an in-flow space-list disclosure
beneath the workspace row instead of cycling to the next space. The
list shows every space's emoji glyph + name, highlights the active one
with the accent check, and on row click switches to that space and
closes the disclosure.
Sidebar disclosure rules:
- The chevron flips ChevronDown → ChevronUp when open so the affordance
reads at a glance.
- A "Manage spaces" footer routes to ely://settings/spaces and closes
the picker.
- ElyShell carries a workspace_picker_open: bool with toggle / close /
select_space_from_picker helpers; render_sidebar_header receives a
&ElyShell so it can read the open flag without leaking shell internals
to free helpers.
The existing SelectNextSpace shortcut still routes through
cycle_to_next_space, so the keyboard cycle behaviour is unchanged.
ElyShell now hosts a translucency_slider: Entity<SliderState> bound to
0..=100 step 1, defaulting to DEFAULT_TRANSLUCENCY_PCT. A subscription
on SliderEvent::Change writes the rounded value into the core via
set_translucency_pct, so dragging the thumb mutates the persisted
appearance setting in real time.
The appearance form swaps the static track-and-thumb visual for the
gpui_component Slider (160 wide) plus a live percentage readout. The
three preset chips move below the row as fast-set buttons that go
through a new set_translucency_pct_from_preset helper which writes
both core and the SliderState so the thumb tracks the chip choice.
reset_appearance now resets the slider to DEFAULT_TRANSLUCENCY_PCT
alongside resetting the core, keeping every UI source of truth in
lock-step.
Domain:
- AppearanceSettings gains translucency_pct (u8, 0..=100, default 40)
with a clamping setter and serde round-trip coverage.
- DEFAULT_TRANSLUCENCY_PCT and MAX_TRANSLUCENCY_PCT exported for the
shell.
Core:
- BrowserCore::set_translucency_pct delegates to the appearance struct;
the existing reset_appearance covers the reset path.
- Integration test covers persistence into snapshot.appearance.
Render:
- chrome::sidebar::panel_bg(snapshot) replaces the static PANEL_BG
constant, mapping the user's translucency_pct linearly into the alpha
byte 0xff..0xb3. Sidebar (expanded + compact) and main pane consume
the helper so changing the setting at runtime updates every glass
surface in lock-step.
Form:
- Translucency row in chrome::appearance_form mirrors the design's
static track + thumb visual driven by the persisted percentage, plus
three preset chips (Solid 0 / Default 40 / Glassy 75) that mutate the
setting through shell.set_translucency_pct.
Strict UX rule preserved: alpha never drops below 0xb3 so panels stay
readable without backdrop blur (which GPUI 0.2.2 doesn't expose).
- Continue card: switch from grid_cols(2) (50/50) to flex with a
flex-1 history column and a fixed 240 px reading-list cover, matching
the design's grid-template-columns: 1fr 240px.
- Cmd+K (Ctrl+K on Windows/Linux) now aliases FocusAddressBar so the
⌘K affordance shown in Arc/Dia/Linear-style designs lands on the
omnibar without disturbing the existing Cmd+L mapping.
Replaces the static "Install plugin · N active" hero button with the
design's pair: a glass search input + a primary Install action. The
input is bound to a new InputState on ElyShell (plugin_search_input);
on every render the catalog reads its current value and filters the
plugin grid by case-insensitive contains on name, author, and
description. An empty-needle render shows every installed plugin so
the page still works at rest.
When the search yields no matches, the grid swaps to an empty-state
copy that quotes the user's needle. Install action moves into a
trailing dark chip that calls choose_plugin_package, preserving the
existing file-picker flow.
Design's command switcher exposes Open tabs / History / Bookmarks /
Actions. The implementation now includes a Bookmarks section sourced
from snapshot.bookmarks: filtered by lowercase needle on title and
URL, capped at RESULT_LIMIT, rendered with brand glyphs, opening the
URL on click.
Match helpers (matching_tabs / matching_history / matching_bookmarks)
move into chrome::command_match so command_overlay.rs stays under the
500-line ceiling. Ask ELY remains skipped — no AI surface in domain
yet, no fabrication.
The design's Slack/Linear/Gmail rows show numeric badges (12, 3) that
real apps publish in their tab title prefix — "(12) Slack | …",
"(3) Inbox — Linear", "(99+) Gmail". Add a pure parser
ely_domain::parse_title_unread_count that recognises the leading
"(N)" pattern and surfaces it through BrowserTab::unread_count(); the
sidebar launcher row renders a glass pill badge whenever the count
is non-zero, capped at "99+" for very high counts.
No domain field, no fabrication: the badge appears only when a real
website publishes its own unread count via the title. Tests cover
canonical formats, non-prefixed titles, leading whitespace, and
non-numeric / overflow inputs.
- AppearanceSettings derives Default instead of carrying a manual impl
that's identical to the derived one.
- topbar::render_lock_or_search collapses the duplicated Search arms
into a single fallback so clippy stops flagging identical blocks.
- command_overlay row helpers bundle id/title/hint/keys into a small
CommandRowContent struct so render_row, render_row_with_glyph, and
render_row_inner stay under the 7-arg threshold without losing any
call-site clarity.
Each sidebar launcher row now ships with a 16 px close glyph that fades
in via group_hover. Clicking the glyph selects the row's tab then
closes the active tab — the same flow the split pane close uses.
Matches the design's .ely-nav-item .close opacity-0 → 1 transition
without keeping the X visible at rest.
Below the appearance rows the Appearance form now exposes the design's
"Sidebar / Layout" section: serif "Layout" sub-headline plus a 2-up
preview-card grid wired to the active space's sidebar_width — Single
column lands on DEFAULT_SIDEBAR_WIDTH_PX, Compact lands on
COLLAPSED_SIDEBAR_WIDTH_PX. Each card draws a miniature sidebar +
canvas preview so the choice reads at a glance.
The shell exposes a new set_active_sidebar_width helper alongside the
existing toggle so the cards mutate state without re-implementing the
toggle logic. The "Hidden on hover" preset from the design is omitted
until that mode actually ships — no placeholder option.
The cards live in chrome::appearance_layout_cards so appearance_form
stays under 500 lines.
The Continue card's reading-list cover was a flat 2-stop gradient. The
design layers a cream base with a pink upper-right glow and a blue
lower-left glow. Add two diagonal overlay layers that fade to
transparent so the cover feels more atmospheric without changing the
clickable surface. Adds purposeful hover/active feedback so the cover
acts visibly clickable.
Plugin detail now opens with the design's marketplace card: a
4/3 brand-gradient cover plus install/secondary buttons plus a
permissions list with risk badges on the left, and a category
overline plus serif Newsreader title plus description plus a
4-up real-data stat grid (permissions, high-risk, contributes,
signature) plus a "What it adds to ELY" contributions list on
the right. Every value comes from PluginManifest — no fabricated
ratings, install counts, or feature copy.
Internal: chrome::plugin_detail_view holds the layout, and
chrome::plugin_labels owns the permission scope labels and
contribution copy so plugin_detail_view stays under 500 lines.
The internal_pages/plugin_details.rs shim only handles route
parsing and the missing-plugin fallback.
Embeds Newsreader (variable opsz/wght, OFL-licensed) into the
ely_app binary via include_bytes!, registers it through GPUI's
text_system at startup (chrome::typography::register_serif_fonts),
and exposes the SERIF_FAMILY constant for downstream surfaces.
Apply the family to every page hero where the design uses
ely-serif: home headline, settings/appearance headline, plugin
marketplace headline, sync headline, plugin detail name, and the
home reading-list cover headline. The OFL license file ships
alongside the .ttf assets to satisfy the font's redistribution
clause.
Per the design, ely://settings opens directly on the appearance form
(serif "Appearance" headline + wallpaper grid + theme/accent/motion
rows), not a route summary. The settings layout's right column now
delegates to render_appearance_form so /settings and /settings/appearance
share the same canonical form rendering.
The metric cards / "Jump in" list helpers and the METRIC_BG constant
are deleted — they were only referenced by the previous summary right
column. Left nav still highlights Appearance as active when the user
lands on /settings.
The appearance route now opens with the design layout: serif "GENERAL"
overline + "Appearance" headline + intro paragraph, a four-tile
wallpaper picker (Dawn/Violet/Mint/Slate with active outline + check
glyph), a theme-mode segmented control, an accent swatch row, a
reduce-motion toggle, and a reset row. Every control mutates real state
through new shell methods (set_wallpaper_theme, set_theme_mode,
toggle_reduce_motion, reset_appearance) which delegate to the core.
The chrome lives in chrome::appearance_form so the page module is a
six-line shim. Light/Dark theme buttons currently switch the persisted
mode; rendering swaps will land when light/dark token sheets ship —
keeping the persistence so the eventual flip is one place.
The shell now reads snapshot.appearance.wallpaper and feeds the domain
WallpaperTheme directly into render_wallpaper. The chrome enum is
deleted; the four design themes (Dawn / Violet / Mint / Slate) all map
to base + upper-blob + lower-blob HSLA palettes that approximate the
design's radial gradients with GPUI's 2-stop linear gradient pair.
Switching themes via core.set_wallpaper_theme now propagates through
snapshots into the next render.
command_overlay.rs grew past 500 lines after wiring brand glyphs.
Move the footer renderer, the keyboard hint chunks, and the kbd chip
helper into their own module so the overlay file stays under budget
without changing any rendered output.
Brings hover/active opacity feedback to a consistent 0.92/0.82 baseline
across the omnibar content, the home search pill, and the workspace
picker tile + add button. The home search pill and the omnibar styled
URL display previously had no hover state at all, so the click target
felt invisible. The motion design framework wants every interactive
element to acknowledge hover and click; this change closes the gap
without introducing new keyframe animations (GPUI 0.2.2 has no
transition layer to drive them).
The design omnibar shows the host in ink-1 weight 500 and the path in
ink-3, switching to a plain editable address only while the user types.
The shell now compares the input value to the active tab URL: when they
match (and the URL isn't ely://new-tab) the omnibar renders the styled
host/path display; clicking it routes through focus_address_bar which
re-mounts the Input via the same listener that already drove typing.
Internal: render_lock_or_search picks Search vs Globe based on the URL
scheme so insecure pages get a different leading glyph in display mode.
Replace the colored accent dot in each split pane header with the
host's brand glyph, and add the design's reload control next to the
close glyph. The header height bumps from 30 to 32 px so the 16 px
glyph sits cleanly between the 11 px lock and host text.
Open tabs and History results in the command switcher now lead with the
brand glyph for their host (Notion, GitHub, Figma, Linear, etc.) instead
of a generic Globe / Undo2. The action rows still use IconName glyphs
because they map to internal navigation, not external sites.
Internal: split row rendering into render_row_inner so both the icon
and glyph variants share the same hover/click body.
Replace the rounded-square initial in the Continue history rows and the
generic Globe icon in the Recent activity rows with brand glyphs derived
from each entry's URL host. Unknown hosts still use the gradient initial
fallback so unbranded sites stay legible. Adds a hover opacity feedback
to both rows so the click affordance matches the rest of the home tiles.
The previous sidebar listed FAVORITES / PINNED / SPACES / TABS / ARCHIVE
sections side-by-side. The design opens with a single Home anchor row,
then promotes favorites and pinned tabs as Arc-style launcher rows
(brand glyph + label, no section label between Home and the launchers),
then a TABS · {count} section with a + New Tab row at the end.
Spaces are no longer listed as nav rows because the workspace picker
already cycles through them; archived tabs move out of the sidebar (the
ely://archive route still exposes them). The footer is split into a
Settings row + a profile row that opens ely://settings/profiles. Every
launcher tab now resolves its icon through brand_glyph::glyph_for_host,
so figma.com/github.com/etc surfaces show the design's stylized glyphs.
The design distinguishes apps by stylized brand glyphs (Notion N,
YouTube ▶, Dribbble dot, X 𝕏, Vercel ▲, Behance Bē, Figma F,
Slack #, GitHub ◯, Linear L, plus reading and news clusters). Add
chrome::brand_glyph with a host → Brand dispatcher so any place that
shows an app icon can route through one helper instead of inventing
fallback initials.
Wire it into the home favorites tile first — tabs whose host matches a
known brand now show the proper glyph; unknown hosts still get the
gradient initial fallback. Tests cover canonical hosts, www/subdomain
normalization, the news/reading clusters, and unknown rejection.
plugin_catalog.rs was 516 lines after the marketplace redesign. Move
the editor's pick card, the featured plugin selector, and the shared
action_button helper into plugin_editors_pick.rs so the catalog file
stays under the 500-line per-module budget.
splits.rs had grown to 551 lines after adding the design's pane header.
Move the header renderer, the close glyph, the URL helpers, and the
compact-canvas placeholder into chrome::split_pane so splits.rs is back
to a 422-line action + render orchestrator and the new helpers stay
under their own 150-line file.
Behaviour preserved: the pane click still selects the tab, the close
glyph still selects+closes, and pane_host/path/secure still derive from
the same UrlText fields.
The marketplace landing now matches the design's two-row hero (serif
"Quiet tools. Everyday magic." headline + native-to-ELY tagline on the
left, an Editor's Pick card on the right that promotes the first
enabled plugin or, when none are installed, an empty-state CTA), a row
of category chips with sandbox audit count, and a 4-column card grid.
Each plugin card uses real manifest data (name, author, description,
permission counts, sandbox flag) and routes to ely://plugin/{id} on
click. Empty state surfaces an honest "drop a signed .rplug" message
plus the install action. Nothing on the page is fabricated catalog data.
Sync now opens with the design's two-column structure: a serif headline
and Cloudflare Sync status pill on the left with a local-queue card
showing pending/failed counts and a reset action, and a "What syncs"
card on the right that lists every SyncObjectKind with a state dot,
local count, and a working policy toggle.
The connection label is honest: until Better Auth integration ships, it
reads "Local-only · sign-in coming soon" rather than displaying a fake
sign-in form. All toggles dispatch real shell.set_sync_object_policy
calls so the state changes are persisted through the existing core API.