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:
parent
41ade01d98
commit
b4cea7b83f
6 changed files with 285 additions and 103 deletions
82
PENDING.md
82
PENDING.md
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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=",
|
||||
|
|
|
|||
30
chrome.html
30
chrome.html
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
268
home.html
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue