Theseus 0.3.130: startpage sidebar fills the panel, searchbar stops auto-growing, Store Claude Desktop detected

Startpage sidebar is now a real left column that starts at the Theseus
title's top edge and runs the full page height. The slot grid is 3
columns stretched to the full sidebar width, with enough rows to cover
the sidebar vertically (count derived from the main column's height on
each render, and re-derived on viewport resize). Empty cells sit as
semi-transparent dashed outlines in the exact position a filled tile
would occupy, so the grid reads as one uniform surface; any empty cell
doubles as the "add link" surface and clicking it opens the Add-link
modal with that cell pre-pinned, so new links land exactly where the
click did instead of being pushed to the end. The dedicated "+" button
is gone — the pin-location is the click target. A "Theseus v0.3.X —
What's new" link appears under the search tip, pointing at
silentmode.st/releases; version comes from a new getVersion exposed on
window.home over the existing app-version IPC.

Chrome toolbar: 0.3.128's "give the searchbox more room while typing"
rule is removed — focusing the search field was pulling the drag handle
with it, which made the URL / search ratio impossible to set by hand.
The URL bar now caps at 75% of the pair's shared budget via a hard CSS
max-width (so a wide urlBarWidthPx stored under the old 80% ceiling
clamps on load without the user having to re-drag), and the drag floor
matches at 25 % so the search field always keeps at least a quarter of
the pair.

Claude connector: bumped bundled-addons/claude-connector from 0.2.0 to
0.2.1. The clients.cjs Store-install detection and the panel-access
work shipped in 0.3.129's bundled 0.2.0, but seedBundledAddons only
overwrites the installed copy when the bundle is strictly newer, so
every user with an existing 0.2.0 kept the stale pre-fix copy from
%APPDATA%/Theseus/extensions/claude-connector/. The version bump
surfaces the Store-install detection ("Claude Desktop not installed on
this computer" was false on every Microsoft Store install) and the
add-on-panel approvals split in the connector panel, both of which
were already in-tree but gated behind the version check.
This commit is contained in:
Theseus Dev 2026-10-29 14:46:29 +02:00 • committed by Hermes Dev
parent 41ade01d98
commit b4cea7b83f
6 changed files with 285 additions and 103 deletions

View file

@ -8,7 +8,7 @@ what belongs to whom and coordinate. Naming convention:
`YYYY-MM-DD:short-slug` for date-bounded work, or `parallel:<theme>` for
work owned by a session that isn't the current one.
Last updated: 2026-10-28.
Last updated: 2026-10-29.
---
@ -16,7 +16,7 @@ Releases are no longer recorded here: the live version and hashes are in
`../site/releases-manifest.json` and `https://dl.silentmode.st/releases-manifest.json`,
and each release commit is `Theseus 0.3.x: ...` in git history.
## Staged for the next release (post-0.3.128)
## Staged for the next release (post-0.3.129)
Queue of asks for the next cut. Items marked LANDED are edited in the
working tree waiting for a bump + build. Items marked TODO still need the
@ -35,6 +35,84 @@ code change.
is the only way through to installed users). `game.x` isn't resolving
yet (no s3 pointer on-chain, no manifest); wait until the service is up
before baking its description/favicon.
- **LANDED: Searchbox no longer auto-grows on focus; URL/search ratio
capped at 75/25.** Reverts the 0.3.128 "give the searchbox more room
while typing" rule — focusing the searchbox was pulling the drag handle
with it and making the ratio hard to set by hand. Changes in
[chrome.html](chrome.html): dropped the
`.urlsearch:has(.searchbox:focus-within) .searchbox { flex: 2 1 480px }`
rule and its `.14s` transition on `.searchbox`; added
`max-width: 75%` to `.urlwrap` so a stored `urlBarWidthPx` from the old
80 % ceiling clamps on load without the user having to redrag; bumped
the drag `MIN_SHARE` 0.20 → 0.25 so new drags can't push the search
bar below 25 % of the shared budget. 0.3.128's typing-wider effect
stays off; the user owns the ratio again.
- **LANDED: Startpage sidebar runs the full page height alongside the
hero; slots fill the whole sidebar panel; empty cells double as the
"add link" surface.** [home.html](home.html):
- **Page layout restructured.** Hero + homepage are now wrapped in a
single `.page` grid (`grid-template-columns: minmax(240, 280) 1fr`)
with the sidebar `<aside>` as a true left column and `.main-col` on
the right holding the hero on top of the SM cards. The sidebar's
top edge now aligns with the Theseus logo + search box instead of
starting below them. Hero top padding dropped (page handles it);
hero stays centered inside the main column.
- **Sidebar fills the panel vertically.** `.sidebar { align-self:
stretch }` makes the aside match the main column's height.
`.slots-list { grid-template-columns: repeat(3, 1fr); width: 100% }`
stretches cells to the sidebar's full width (icons stay 48 px,
centered inside the wider cell; labels gain `max-width: 100%` so
they center over the cell). A new `minSlotsForFill()` computes row
count from `.main-col` height on each render; a ResizeObserver
re-renders on viewport resize so new rows appear (or drop off) as
the window changes.
- **Empty tiles top-aligned.** `.slot-empty` switched to
`flex-direction: column; justify-content: flex-start` so the dashed
tile sits at the top of its 72×68 cell — same Y as a filled slot's
`.tile` (which sits above its label). Before this the dashed box
was centered in the cell while filled tiles were top-aligned, so
the two read as misaligned.
- **Chromeless sidebar.** `.left-column` renamed to `.sidebar`,
`.right-column` renamed to `.cards-wrap`; the new `.section.chromeless`
modifier removes background, inset border, and padding box on the
sidebar so it blends with the page body. The SM cards area keeps
its soft panel (the cards area is "the rest of the page", not the
sidebar).
- **Dedicated `.slot-add` cell removed; dashed outline semi-transparent.**
`.slot-empty::before` has `content: ""` (no "+" glyph) and renders at
.18 opacity on container hover, .35 on this-cell hover, strong acid
only on drop-hot — so empty cells never compete visually with the
real icons. Clicking any empty cell opens the Add-link modal with
that cell's index pre-pinned (`openModal(sec, i, pinSlot)` carries
the pin through `editKey` to the save handler), so the new link
lands exactly where the user clicked.
- `layoutSlots()` default `minSlots` still 18 (fallback for the first
render before layout is measured); render() passes the dynamic
value from `minSlotsForFill()` which uses it as a floor.
- **LANDED: Startpage shows running version + link to release notes.**
New line under the "Tip: hello.bch" hint on [home.html](home.html):
"Theseus v0.3.X — What's new ↗", linking to
`https://silentmode.st/releases`. Version comes from
`window.home.getVersion()` (added to [home-preload.js](home-preload.js);
wired to the existing `app-version` IPC — no new backend). Visible on
every startpage load so a user who just updated has a one-click path to
the changelog without hunting for a chrome chip that only shows when a
NEWER version is out.
- **LANDED: Claude connector detects Microsoft Store install of Claude
Desktop.** [bundled-addons/claude-connector/addon.json](bundled-addons/claude-connector/addon.json)
bumped from 0.2.0 to 0.2.1 so Theseus's `seedBundledAddons` reseeds the
stale copy sitting in `%APPDATA%\Theseus\extensions\claude-connector\`
on first boot of the next Theseus. The actual detection code (Store
package path under `%LOCALAPPDATA%\Packages\Claude_<pub>\LocalCache\
Roaming\Claude\` + fallback to `AnthropicClaude` / `Programs\Claude` /
`Program Files\Claude`) is already in the tree in
[bundled-addons/claude-connector/lib/clients.cjs](bundled-addons/claude-connector/lib/clients.cjs);
it just wasn't reaching installed users because the addon version
hadn't been bumped — `seedBundledAddons` skips the overwrite when
user version >= bundle version. User-visible symptom: the connector
panel showed Claude Desktop as "Not installed on this computer" on
every box where Claude Desktop is the Store install (which is the
default modern install).
- **LANDED: Claude connector reaches add-on sidebar panels** (Aegis, Hermes,
Pithos, and any other sidebar add-on). The tab-control hook now exposes
each open panel as a tab with a negative id, `kind: "panel"`, `addon`

View file

@ -1,7 +1,7 @@
{
"id": "claude-connector",
"name": "Claude connector",
"version": "0.2.0",
"version": "0.2.1",
"description": "Lets Claude Code or Claude Desktop on this computer work in your Theseus tabs through a local MCP connection: read pages, click, type, paste and upload. Off until you turn it on; asks before each new site; never types into password fields; logs every action.",
"author": "Silent Mode",
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PHJlY3QgeD0iNCIgeT0iNCIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iMTQiIGZpbGw9IiNkOTc3NTciLz48cGF0aCBkPSJNMzIgMTN2MzhNMTMgMzJoMzhNMTguNiAxOC42bDI2LjggMjYuOE00NS40IDE4LjYgMTguNiA0NS40IiBzdHJva2U9IiNmZmY0ZWUiIHN0cm9rZS13aWR0aD0iNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+DQo=",

View file

@ -230,18 +230,21 @@
and the trailing dock (Downloads, extensions, ⛓ Theseus) so they
never look like they collide. */
.urlsearch { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 6px; margin-right: 10px; }
.urlwrap { position: relative; flex: 0 1 var(--urlbar-w, 60%); min-width: 220px; display: flex; align-items: center; gap: 4px; background: var(--surface); border: 1px solid var(--line);
/* max-width: 75% is the hard cap on the URL bar's share of the .urlsearch
flex budget. Combined with the 25 % drag floor in the JS, the ratio can
run from 25/75 to 75/25 (URL bar / search bar), so the search field
always keeps at least a quarter of the pair — enough for the "Search"
placeholder plus a typical query — no matter what px value the user has
stored in urlBarWidthPx (which may have been saved under the old
80 % ceiling, or scaled wider than the current window). */
.urlwrap { position: relative; flex: 0 1 var(--urlbar-w, 60%); min-width: 220px; max-width: 75%; display: flex; align-items: center; gap: 4px; background: var(--surface); border: 1px solid var(--line);
border-radius: 999px; padding: 0 6px 0 4px; }
/* Search-box floor bumped so the "Search" placeholder always fits inside
the input — dropping below ~180px in normal font sizes was where the
placeholder text started getting clipped. */
.searchbox { flex: 1 1 0; min-width: 180px; transition: flex-basis .14s ease; }
/* While the user is typing a search, give the searchbox more room by
reducing the URL bar's basis (urlBarWidthPx can be set wide via drag,
which otherwise squeezes the search field below what fits a longer
query). Both shrink proportionally if the parent is tight, so this
never overflows. */
.urlsearch:has(.searchbox:focus-within) .searchbox { flex: 2 1 480px; }
placeholder text started getting clipped. No transition: the search bar
only changes size when the user drags the handle, and a drag should feel
direct, not animated. */
.searchbox { flex: 1 1 0; min-width: 180px; }
/* Single drag handle sitting between the two — a thin 6px column with a
col-resize cursor. Zero-sum semantics: dragging right grows the URL
bar and shrinks the search bar; dragging left does the inverse. */
@ -2095,11 +2098,12 @@
const urlsearchEl = $("urlsearch");
// Absolute minimums track the CSS min-widths so the drag can't push either
// bar below the size where its content stops fitting. On top of that we
// enforce a 20 % share floor for both bars: the ratio can run from 80/20
// to 20/80 (URL bar / search bar). Combined with the CSS min-widths, the
// tighter of the two rules wins at any given container width.
// enforce a 25 % share floor for both bars: the ratio can run from 75/25
// to 25/75 (URL bar / search bar). Combined with the CSS min-widths and
// the .urlwrap max-width: 75 % cap, the tighter of the two rules wins at
// any given container width.
const MIN_URL_PX = 220, MIN_SEARCH_PX = 180, HANDLE_W = 6, GAP = 6;
const MIN_SHARE = 0.20;
const MIN_SHARE = 0.25;
let dragging = false, startX = 0, startW = 0, lastW = 0;
handleEl.addEventListener("pointerdown", (e) => {
if (e.button !== 0) return;

View file

@ -24,6 +24,10 @@ contextBridge.exposeInMainWorld("home", {
// Main pushes an updated list here after a successful remote refresh
// (only when the user has not customised locally). home.html re-renders.
onCards: (cb) => ipcRenderer.on("home-cards", (_e, list) => cb(list)),
// Current Theseus version — exposed so the startpage can show a tiny
// "What's new in Theseus 0.3.X" link next to the search box. The IPC is
// the same one Settings uses; it reads app.getVersion() with no network.
getVersion: () => ipcRenderer.invoke("app-version"),
});
// Same preload also serves the branded error page (error.html). Handlers in
// main.js sender-check for error.html so a random page seeing the API shape

268
home.html
View file

@ -12,11 +12,12 @@
html, body { margin: 0; padding: 0; min-height: 100vh; font-family: system-ui, sans-serif; color: var(--ink); }
body { background: radial-gradient(1200px 600px at 50% -10%, var(--bg1), var(--bg2)); }
/* Hero (identity + search). Centered at the top of the page — the user's
most-used thing on this screen. The slot/card desktop lives beneath.
The mark + wordmark share a translucent panel so they read on any
wallpaper; the tag, search box and hint sit outside it, centered. */
.hero { text-align: center; padding: 5vh 1rem 0; max-width: 820px; margin: 0 auto; }
/* Hero (identity + search). Lives inside the right-hand .main-col,
centered within it. The outer .page grid handles the top padding so
the hero's own top padding is 0 — this keeps the sidebar's top edge
flush with the hero's top edge; the slots start at the same height
as the Theseus logo + search box. */
.hero { text-align: center; padding: 0 1rem; max-width: 820px; margin: 0 auto; }
.hero .hero-id { display: inline-flex; align-items: center; gap: 14px;
padding: 12px 24px 12px 14px; border-radius: 22px;
background: rgba(11,14,20,.55); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
@ -38,36 +39,58 @@
.hero .hint { color: var(--mut); font-size: 12.5px; margin-top: .8rem;
display: inline-block; padding: 4px 12px; border-radius: 999px;
background: rgba(11,14,20,.42); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
/* "Theseus v0.3.X — What's new ↗" line under the hint. Muted so it does
not pull attention from the search box, but visible enough that a user
who just updated has a one-click path to the release notes. Dynamic:
version comes from the running app via home-preload. */
.hero .ver-line { margin: .6rem 0 0; font-size: 11.5px; }
.hero .ver-line a { color: var(--mut); text-decoration: none;
padding: 2px 10px; border-radius: 999px;
background: rgba(11,14,20,.3); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
.hero .ver-line a:hover { color: var(--acid); }
/* Two distinct sections under the hero: a self-contained "Your links"
panel on the LEFT (quick-launch tiles) and a Silent Mode cards panel
on the RIGHT. Each gets its own panel background so they read as two
separate boxes instead of one shared body. The two are side-by-side
on desktop and stack vertically on narrow viewports. */
.homepage { display: grid; grid-template-columns: minmax(360px, 440px) minmax(380px, 1fr);
gap: 24px; max-width: 1680px; margin: 2.4rem auto 0; padding: 0 32px 48px; align-items: start; }
@media (max-width: 900px) { .homepage { grid-template-columns: 1fr; gap: 20px; padding: 0 14px 32px; } }
/* Page-level 2-column layout: the sidebar is a left column that starts
at the top of the page (same top edge as the hero's title) and spans
the full height of the content, so the slot grid sits beside the
Theseus logo + search box instead of below them. The main column
stacks the hero (title + search + version link) on top of the Silent
Mode cards. Narrow viewports collapse to one column: sidebar first,
then hero, then cards. */
.page { display: grid; grid-template-columns: minmax(240px, 280px) minmax(380px, 1fr);
gap: 28px; max-width: 1680px; margin: 0 auto;
padding: 32px 32px 48px; align-items: start; }
.main-col { min-width: 0; display: flex; flex-direction: column; gap: 2rem; }
@media (max-width: 900px) { .page { grid-template-columns: 1fr; gap: 20px; padding: 14px; }
.main-col { gap: 1.4rem; } }
/* Section container styling — each column is its own padded panel. The
panel itself is subtle (soft tinted surface, no hard border) so the
separation reads structurally without the sections feeling boxed-in. */
/* The RIGHT column keeps its subtle panel surface — the Silent Mode cards
read as a grouped collection and benefit from the soft tint. The LEFT
column opts out via the chromeless class below, which overrides this. */
.section { background: rgba(255,255,255,.025); border-radius: 16px;
padding: 16px 16px 20px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.03);
width: 100%; min-width: 0; }
body.light .section { background: rgba(0,0,0,.03); box-shadow: inset 0 0 0 1px rgba(0,0,0,.03); }
.section.chromeless { background: transparent; box-shadow: none; padding: 4px 0 0; }
body.light .section.chromeless { background: transparent; box-shadow: none; }
.section-title { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .9px;
color: var(--mut); margin: 0 2px 14px; }
/* LEFT AREA — fixed-cell grid. Each icon lives in its own cell; drag
reorders which icon is in which cell. Icons sized to roughly match a
Windows desktop shortcut (~48 px tile + label) so they stop looming
next to the Silent Mode cards. */
.left-column { align-self: start; }
/* Fixed-width cells (not 1fr) so icons sit in a tight desktop-style grid
instead of drifting inside stretched 3-equal-column cells. The whole
grid left-aligns to the column edge via justify-content: start. */
.slots-list { display: grid; grid-template-columns: repeat(auto-fill, 72px); gap: 10px 8px;
width: 100%; align-items: start; justify-content: start; }
/* LEFT AREA — a fixed 3-column grid of slots. Each slot holds one link
tile; empty slots are invisible by default and reveal subtly on
container hover so the user can see where a dragged tile will land.
3 × 72 px tiles + 2 × 8 px gaps = 232 px — the sidebar column is sized
to that so the grid sits flush with the hero and doesn't float in
half-empty whitespace. */
/* The sidebar grid cell stretches to match the main column's height, so
the sidebar panel is as tall as the hero + Silent Mode cards combined.
The slots-list fills that full area: 3 columns at 1fr each (stretch to
sidebar width — the slot's inner tile stays 48 px and centers inside
the wider cell), and enough rows to cover the sidebar's vertical span
(row count computed in render() from the main-col height, so a taller
page gets more rows automatically). */
.sidebar { align-self: stretch; }
.slots-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 8px;
width: 100%; align-items: start; justify-content: stretch; }
.cards-grid.drop-target { outline: 2px dashed var(--acid); outline-offset: 4px; border-radius: 14px; }
.slot { position: relative; display: flex; flex-direction: column; align-items: center;
@ -81,7 +104,7 @@
overflow: hidden; }
.slot .tile svg, .slot .tile img { width: 76%; height: 76%; display: block; }
.slot .label { margin-top: 5px; font-size: 11px; line-height: 1.2; color: var(--ink);
max-width: 72px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
padding: 1px 4px; border-radius: 4px; text-align: center; }
.slot.size-small .tile { width: 40px; height: 40px; font-size: 17px; border-radius: 10px; }
.slot.size-medium .tile { width: 48px; height: 48px; font-size: 20px; border-radius: 11px; }
@ -95,26 +118,38 @@
.slot.drag-before::before { left: -8px; }
.slot.drag-after::after { right: -8px; }
/* Slot-add sits in the grid alongside the slot icons so it's always at
the end of the row, not pinned to a corner. */
.slot-add { display: flex; align-items: center; justify-content: center;
width: 48px; height: 48px; margin: 0 auto;
border-radius: 10px; border: 2px dashed var(--line2);
background: transparent; color: var(--mut); font-size: 18px; cursor: pointer;
transition: color .12s, border-color .12s, background .12s; }
.slot-add:hover { color: var(--acid); border-color: var(--acid); background: rgb(from var(--acid) r g b / .04); }
/* Empty slots in the grid: drop targets for pinning any link to a
specific slot. Order matches the grid's cell index. Subtly visible
so users see where they can drop; brighter on hover of the strip. */
.slot-empty { display: block; width: 48px; height: 48px; margin: 0 auto;
border-radius: 10px; border: 2px dashed transparent;
background: transparent; transition: border-color .12s, background .12s; }
.slots-list:hover .slot-empty { border-color: rgb(from var(--line2) r g b / .55); }
.slot-empty.drop-hot { border-color: var(--acid); background: rgb(from var(--acid) r g b / .06); }
/* Empty slots occupy a full 72×68 cell — same shape and position as a
filled slot — so the grid reads as one uniform surface and the user
can click any empty cell to add a link pinned to it. The 48 px tile
region matches where a filled slot's tile renders, but it carries no
"+" glyph and the dashed outline stays semi-transparent even when
revealed, so the empty cells never compete with the real icons for
attention. Fully invisible by default; container-hover shows the
faintest hint of where cells live; this-cell hover brightens enough
to confirm the click target; drop-hot (a dragged tile over it) is
the only time the outline reads strongly, because that is action
feedback the user needs to see. */
/* flex-direction: column + justify-content: flex-start pins the dashed
tile to the TOP of the 72×68 cell — same vertical position as a
filled slot's .tile (which sits above its .label). Without this the
dashed box was centered in the cell while the filled tile sat up at
the top, so the two columns looked misaligned vertically. */
.slot-empty { position: relative; display: flex; flex-direction: column;
align-items: center; justify-content: flex-start;
width: 100%; height: 68px; cursor: pointer; }
.slot-empty::before { content: "";
display: block; width: 48px; height: 48px; border-radius: 11px;
border: 2px dashed transparent; background: transparent;
transition: border-color .14s, background .14s; }
.slots-list:hover .slot-empty::before { border-color: rgb(from var(--line2) r g b / .18); }
.slot-empty:hover::before { border-color: rgb(from var(--acid) r g b / .35); background: rgb(from var(--acid) r g b / .03); }
.slot-empty.drop-hot::before { border-color: var(--acid); background: rgb(from var(--acid) r g b / .14); }
/* RIGHT COLUMN — just the 3-column grid of Silent Mode cards; search
lives in the hero above. */
.right-column { display: flex; flex-direction: column; gap: 22px; min-width: 0; }
/* Silent Mode cards area — lives under the hero inside .main-col. The
.cards-wrap keeps the subtle panel surface (background, inset border,
rounded corners) so the cards read as a grouped collection; the
sidebar is chromeless so only this side has panel chrome. */
.cards-wrap { align-self: stretch; }
.cards-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; align-content: start; }
@media (max-width: 980px) { .cards-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .cards-grid { grid-template-columns: 1fr; } }
@ -218,33 +253,35 @@
</style>
</head>
<body>
<div class="hero">
<div class="hero-id">
<div class="mark">
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M32 6 L38 32 L26 32 Z" fill="#e04e4e"/>
<path d="M38 32 L32 58 L26 32 Z" fill="#d6ff3d"/>
<path d="M6 32 L32 27 L58 32 L32 37 Z" fill="#d6ff3d" opacity="0.55"/>
<circle cx="32" cy="32" r="3" fill="#0b0e14"/>
</svg>
</div>
<h1>Theseus <span class="g">Navigator</span></h1>
</div>
<p class="tag">A browser that follows the thread. Names that live on decentralized blockchain.</p>
<form id="searchForm">
<input id="q" placeholder="Search the web" autofocus spellcheck="false">
<button type="submit">Search</button>
</form>
<p class="hint">Tip: type <b>hello.bch</b> in the address bar above to open a decentralized site.</p>
</div>
<div class="homepage">
<div class="section left-column">
<div class="page">
<aside class="section chromeless sidebar">
<div class="slots-list" id="slotsList"></div>
</div>
<div class="section right-column">
<div class="cards-grid" id="cardsGrid"></div>
</aside>
<div class="main-col">
<div class="hero">
<div class="hero-id">
<div class="mark">
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M32 6 L38 32 L26 32 Z" fill="#e04e4e"/>
<path d="M38 32 L32 58 L26 32 Z" fill="#d6ff3d"/>
<path d="M6 32 L32 27 L58 32 L32 37 Z" fill="#d6ff3d" opacity="0.55"/>
<circle cx="32" cy="32" r="3" fill="#0b0e14"/>
</svg>
</div>
<h1>Theseus <span class="g">Navigator</span></h1>
</div>
<p class="tag">A browser that follows the thread. Names that live on decentralized blockchain.</p>
<form id="searchForm">
<input id="q" placeholder="Search the web" autofocus spellcheck="false">
<button type="submit">Search</button>
</form>
<p class="hint">Tip: type <b>hello.bch</b> in the address bar above to open a decentralized site.</p>
<p class="ver-line" id="verLine" hidden><a id="verLink" href="https://silentmode.st/releases"></a></p>
</div>
<div class="section cards-wrap">
<div class="cards-grid" id="cardsGrid"></div>
</div>
</div>
</div>
@ -444,7 +481,7 @@
// hover of the grid. The "+" cell sits at the end so adding is always
// one click away.
const q = state.sections.q || [];
const slotCells = layoutSlots(q);
const slotCells = layoutSlots(q, minSlotsForFill());
const list = $("slotsList");
list.innerHTML = "";
slotCells.forEach((entry, cellIdx) => {
@ -466,15 +503,11 @@
} else {
const empty = document.createElement("div");
empty.className = "slot-empty"; empty.dataset.cell = cellIdx;
empty.title = "Add link here";
wireEmptySlot(empty, cellIdx);
list.appendChild(empty);
}
});
const slotAdd = document.createElement("button");
slotAdd.className = "slot-add"; slotAdd.textContent = "+"; slotAdd.title = "Add link";
slotAdd.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openModal("q", -1); };
slotAdd.oncontextmenu = (e) => e.preventDefault();
list.appendChild(slotAdd);
// RIGHT — Silent Mode cards with sub-description under the title.
const sm = state.sections.sm || [];
@ -502,11 +535,25 @@
$("ctxIconSizeVal").textContent = state.iconSize;
}
// Compute how many slot rows fit the vertical space the sidebar covers.
// Sidebar height = main-col height (align-self: stretch on the aside),
// so basing the row count on main-col keeps the slot grid filling the
// whole sidebar panel instead of ending halfway down. Falls back to 24
// (8 rows) when the layout hasn't been measured yet (first render).
function minSlotsForFill() {
const main = document.querySelector(".main-col");
const h = main ? main.getBoundingClientRect().height : 0;
const rowH = 78; // .slot-empty height (68) + .slots-list row gap (10)
const rows = h > 0 ? Math.max(6, Math.floor(h / rowH)) : 8;
return rows * 3;
}
// Lay out the q[] items into a fixed-slot grid: pinned items (with a
// `slot: number`) go into that cell; unpinned items flow into the first
// free cell, top-to-bottom. The grid always extends to at least one
// trailing empty row for drop space.
function layoutSlots(items, minSlots = 9) {
// trailing empty row for drop space. minSlots defaults to 18 (3 × 6)
// when nothing better is passed; render() passes minSlotsForFill() so
// the grid actually covers the whole sidebar height.
function layoutSlots(items, minSlots = 18) {
const pinned = []; const unpinned = [];
items.forEach((ic, i) => {
const s = Number.isInteger(ic.slot) && ic.slot >= 0 ? ic.slot : null;
@ -618,6 +665,14 @@
if (!payload) return;
pinItemToSlot(payload.sec, payload.i, cellIdx);
});
// Any empty cell doubles as the "add link" surface — click opens the
// link modal with this cell pre-pinned, so the user controls WHERE a
// new link lands (no dedicated + button needed).
el.addEventListener("click", (e) => {
e.preventDefault();
openModal("q", -1, cellIdx);
});
el.addEventListener("contextmenu", (e) => e.preventDefault());
}
// Place the dragged item at `cellIdx` in the q grid, pinning it.
@ -839,8 +894,8 @@
// Add/edit modal — the same shape works for both left slots and right cards.
let editKey = null;
function openModal(sec, i) {
editKey = { sec, i };
function openModal(sec, i, pinSlot) {
editKey = { sec, i, pinSlot: Number.isInteger(pinSlot) ? pinSlot : null };
const isNew = i < 0;
$("modalTitle").textContent = isNew ? "Add link" : "Edit link";
const c = isNew ? { title: "", url: "", sub: "", color: "#d6ff3d", letter: "" }
@ -869,8 +924,13 @@
letter: $("mLetter").value.trim() || null,
};
const toSec = $("mSection").value;
if (!editKey || editKey.i < 0) state.sections[toSec].push(next);
else {
if (!editKey || editKey.i < 0) {
// Preserve the pinned cell index when the modal was opened from a
// specific empty slot — the new link lands exactly where the user
// clicked, not at the end.
if (editKey && editKey.pinSlot != null && toSec === "q") next.slot = editKey.pinSlot;
state.sections[toSec].push(next);
} else {
const prev = state.sections[editKey.sec][editKey.i];
const merged = { ...prev, ...next, brand: prev.brand || null };
if (toSec === editKey.sec) state.sections[toSec][editKey.i] = merged;
@ -895,7 +955,7 @@
r.readAsDataURL(f);
};
// Search form (slot-add button is rendered inside slotsList per render())
// Search form (empty cells themselves act as "add link" surfaces now;
// Select-all on first click of the search field so a click lands with any
// existing query ready to replace. Second click (already-focused) falls
// through to native cursor placement.
@ -917,6 +977,22 @@
if (name) $("q").placeholder = "Search the web with " + name;
}).catch(() => {});
}
// Running-version footer: "Theseus v0.3.X — What's new ↗". The link uses
// home.navigate so it opens release notes in this tab, same as any other
// link on the startpage. Hidden until the version resolves — kept off the
// page if the IPC isn't available (shouldn't happen in-app, but safe).
if (window.home && window.home.getVersion) {
window.home.getVersion().then((v) => {
if (!v) return;
const line = $("verLine"), a = $("verLink");
a.textContent = "Theseus v" + v + " — What’s new ↗";
a.addEventListener("click", (e) => {
e.preventDefault();
window.home.navigate("https://silentmode.st/releases");
});
line.hidden = false;
}).catch(() => {});
}
// Container-level drop handlers accept cross-column drops onto empty space.
wireContainer($("slotsList"), "q");
@ -928,13 +1004,33 @@
let loaded = null;
try { if (window.home && window.home.getCards) loaded = await window.home.getCards(); } catch {}
state = ensureShape(loaded) || defaultState();
applyBg(); render();
applyBg();
render();
// The first render runs before the main column has laid out, so the
// row count comes from the fallback (24). Re-render once the layout
// settles so the slot grid fills the sidebar vertically.
requestAnimationFrame(() => render());
if (window.home && window.home.onCards) {
window.home.onCards((data) => {
const next = ensureShape(data);
if (next) { state = next; applyBg(); render(); }
});
}
// Resize the slot grid when the viewport changes so new rows appear
// (or drop off) to match the sidebar's new height. ResizeObserver on
// the main column catches both window resizes and content-driven
// height changes (e.g. new cards being added).
const main = document.querySelector(".main-col");
if (main && window.ResizeObserver) {
let lastRows = -1, t = null;
const ro = new ResizeObserver(() => {
const rows = Math.floor(minSlotsForFill() / 3);
if (rows === lastRows) return;
lastRows = rows;
clearTimeout(t); t = setTimeout(() => render(), 80);
});
ro.observe(main);
}
})();
</script>
</body>

View file

@ -1,6 +1,6 @@
{
"name": "theseus-navigator",
"version": "0.3.129",
"version": "0.3.130",
"license": "MPL-2.0",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode",