Theseus 0.3.96: home — left column back to grid cells, widened layout

The left-column icons return to a fixed 3-column grid, each icon in its own
cell. Drag reorders which cell holds which icon, with before/after indicators
matching the right-side cards. Drops out of x,y positioning: no more free-
floating icons sticking against the card grid; defaults flow left-to-right
across the three cells. The "+" add button sits in the next free cell at
the end of the slot grid.

Homepage max-width widens to 1680px (from 1400) so on wide screens the left
column doesn't crowd the cards. Legacy x,y values on stored icons are
ignored by the renderer (array order is what shows).
This commit is contained in:
Local Dev 2026-10-21 01:31:21 +03:00
parent 5d34fa18af
commit 9450f5ec3a
2 changed files with 51 additions and 150 deletions

199
home.html
View file

@ -28,30 +28,23 @@
.hero form button { padding: 13px 20px; border-radius: 999px; border: none; background: var(--blue); color: #fff; font-size: 15px; cursor: pointer; } .hero form button { padding: 13px 20px; border-radius: 999px; border: none; background: var(--blue); color: #fff; font-size: 15px; cursor: pointer; }
.hero .hint { color: var(--dim); font-size: 12.5px; margin-top: .8rem; } .hero .hint { color: var(--dim); font-size: 12.5px; margin-top: .8rem; }
/* Two-column desktop under the hero: wide LEFT area for a 3-column grid /* Two-column desktop under the hero: a 3-column grid of icon slots on the
of icon slots, wider RIGHT area with a 3-column grid of Silent Mode LEFT, a 3-column grid of Silent Mode cards on the RIGHT. Both expand
cards. Both expand together on wider screens. */ together on wider screens; the layout stretches to the viewport so the
.homepage { display: grid; grid-template-columns: minmax(300px, 340px) minmax(380px, 1fr); left column doesn't crowd against the cards. */
gap: 36px; max-width: 1400px; margin: 2.4rem auto 0; padding: 0 32px 48px; align-items: start; } .homepage { display: grid; grid-template-columns: minmax(300px, 360px) minmax(380px, 1fr);
gap: 36px; max-width: 1680px; margin: 2.4rem auto 0; padding: 0 40px 48px; align-items: start; }
@media (max-width: 900px) { .homepage { grid-template-columns: 1fr; gap: 20px; padding: 0 14px 32px; } } @media (max-width: 900px) { .homepage { grid-template-columns: 1fr; gap: 20px; padding: 0 14px 32px; } }
/* LEFT AREA — free-positioning canvas. Each icon stores its own x,y and /* LEFT AREA — fixed-cell grid. Each icon lives in its own cell; drag
snaps to a 20px grid. A faint grid shows underneath while dragging to reorders which icon is in which cell. Simpler than free x,y and keeps
make placement feel OS-desktop-like. */ the icons tidy against the left edge of the column. */
.left-column { display: flex; flex-direction: column; gap: 14px; } .left-column { display: flex; flex-direction: column; gap: 14px; }
.slots-list { position: relative; width: 100%; min-height: 480px; padding: 8px 4px; border-radius: 10px; } .slots-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; width: 100%; align-items: start; }
.slots-list::before { @media (max-width: 900px) { .slots-list { grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); } }
content: ""; position: absolute; inset: 0; opacity: 0; .cards-grid.drop-target { outline: 2px dashed var(--acid); outline-offset: 4px; border-radius: 14px; }
background-image: linear-gradient(to right, rgba(255,255,255,.04) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255,255,255,.04) 1px, transparent 1px);
background-size: 20px 20px; pointer-events: none; transition: opacity .15s;
border-radius: 10px;
}
body.dragging-any .slots-list::before { opacity: 1; }
.slots-list.drop-target, .cards-grid.drop-target { outline: 2px dashed var(--acid); outline-offset: 4px; border-radius: 14px; }
.slots-list .slot { position: absolute; }
.slot { display: flex; flex-direction: column; align-items: center; .slot { position: relative; display: flex; flex-direction: column; align-items: center;
cursor: pointer; user-select: none; text-decoration: none; color: inherit; cursor: pointer; user-select: none; text-decoration: none; color: inherit;
transition: transform .12s ease; } transition: transform .12s ease; }
.slot:hover { transform: translateY(-2px); } .slot:hover { transform: translateY(-2px); }
@ -70,16 +63,21 @@
.slot.size-medium .tile { width: 72px; height: 72px; font-size: 28px; border-radius: 14px; } .slot.size-medium .tile { width: 72px; height: 72px; font-size: 28px; border-radius: 14px; }
.slot.size-large .tile { width: 88px; height: 88px; font-size: 34px; border-radius: 18px; } .slot.size-large .tile { width: 88px; height: 88px; font-size: 34px; border-radius: 18px; }
.slot.size-small .label { font-size: 10.5px; max-width: 72px; } .slot.size-small .label { font-size: 10.5px; max-width: 72px; }
.slot.dragging { opacity: .85; cursor: grabbing; z-index: 2; } .slot.dragging { opacity: .6; }
.slot.drag-before::before, .slot.drag-after::after {
content: ""; position: absolute; top: 10%; bottom: 10%; width: 3px;
background: var(--acid); border-radius: 2px; box-shadow: 0 0 8px var(--acid);
}
.slot.drag-before::before { left: -8px; }
.slot.drag-after::after { right: -8px; }
/* Slot-add floats at the bottom-left of the free-positioning area so the /* Slot-add sits in the grid alongside the slot icons so it's always at
user always has a way to add without hunting for empty space. */ the end of the row, not pinned to a corner. */
.slot-add { position: absolute; left: 10px; bottom: 10px; .slot-add { display: flex; align-items: center; justify-content: center;
display: flex; align-items: center; justify-content: center; min-height: 72px; border-radius: 12px; border: 2px dashed var(--line2);
width: 60px; height: 60px; border-radius: 14px; border: 2px dashed var(--line2); background: transparent; color: var(--mut); font-size: 20px; cursor: pointer;
background: rgba(20,26,36,.6); color: var(--mut); font-size: 20px; cursor: pointer;
transition: color .12s, border-color .12s, background .12s; } 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 / .08); } .slot-add:hover { color: var(--acid); border-color: var(--acid); background: rgb(from var(--acid) r g b / .04); }
/* RIGHT COLUMN — just the 3-column grid of Silent Mode cards; search /* RIGHT COLUMN — just the 3-column grid of Silent Mode cards; search
lives in the hero above. */ lives in the hero above. */
@ -334,13 +332,14 @@
{ title: "Ariadne.X", url: "https://ariadne.x/", brand: "ariadne", sub: "BNS resolver — read .x and .bch names." }, { title: "Ariadne.X", url: "https://ariadne.x/", brand: "ariadne", sub: "BNS resolver — read .x and .bch names." },
{ title: "Aegis.X", url: "https://aegis.x/", brand: "aegis", sub: "Multi-chain wallet, derived from the vault." }, { title: "Aegis.X", url: "https://aegis.x/", brand: "aegis", sub: "Multi-chain wallet, derived from the vault." },
], ],
// Your-links icons are free-positioned (per-item x,y, 20px snap grid). // Your-links icons live in fixed grid cells; drag reorders which
// Defaults seed a tidy row across the top of the left area; drag anywhere. // icon is in which cell. Order in the array = position in the grid
// (row-first, 3 per row).
q: [ q: [
{ title: "hello.bch", url: "https://hello.bch/", brand: "hellobch", x: 10, y: 10 }, { title: "hello.bch", url: "https://hello.bch/", brand: "hellobch" },
{ title: "sia.coin", url: "https://sia.coin/", brand: "siacoin", x: 110, y: 10 }, { title: "sia.coin", url: "https://sia.coin/", brand: "siacoin" },
{ title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos", x: 210, y: 10 }, { title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos" },
{ title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot", x: 10, y: 120 }, { title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot" },
], ],
}, },
}; };
@ -397,8 +396,8 @@
function iconUsesBrand(icon) { return !!(icon.brand && BRAND[icon.brand]); } function iconUsesBrand(icon) { return !!(icon.brand && BRAND[icon.brand]); }
function render() { function render() {
// LEFT — free-positioned icons. Each stores its own x,y; legacy items // LEFT — fixed-cell grid. Icons flow left-to-right, top-to-bottom; the
// without x,y fall back to a tidy auto-layout at the top of the area. // "+" cell always sits at the end so there's a visible way to add.
const q = state.sections.q || []; const q = state.sections.q || [];
const list = $("slotsList"); const list = $("slotsList");
list.innerHTML = ""; list.innerHTML = "";
@ -406,9 +405,6 @@
const el = document.createElement("a"); const el = document.createElement("a");
el.className = `slot size-${state.iconSize}`; el.className = `slot size-${state.iconSize}`;
el.href = ic.url; el.dataset.sec = "q"; el.dataset.i = i; el.href = ic.url; el.dataset.sec = "q"; el.dataset.i = i;
const autoX = 10 + (i % 3) * 100, autoY = 10 + Math.floor(i / 3) * 110;
el.style.left = (ic.x != null ? ic.x : autoX) + "px";
el.style.top = (ic.y != null ? ic.y : autoY) + "px";
if (!iconUsesBrand(ic)) { if (!iconUsesBrand(ic)) {
el.style.setProperty("--iconBg", ic.color || "#d6ff3d"); el.style.setProperty("--iconBg", ic.color || "#d6ff3d");
el.style.setProperty("--iconFg", isLightColor(ic.color || "#d6ff3d") ? "#0b0e14" : "#ffffff"); el.style.setProperty("--iconFg", isLightColor(ic.color || "#d6ff3d") ? "#0b0e14" : "#ffffff");
@ -452,107 +448,28 @@
$("ctxIconSizeVal").textContent = state.iconSize; $("ctxIconSizeVal").textContent = state.iconSize;
} }
// LEFT slots use pointer events for free-drag: live movement with snap to // Both sections are 2D grids: slots are fixed cells on the left (3 wide),
// a 20px grid, final x,y committed on pointerup. A short move threshold // cards are fixed cells on the right (3 wide). Drag reorders inside a
// keeps plain clicks working for navigation. Dropping over the right // section with a before/after indicator on the hover target; drop onto
// card grid re-parents the item (removes x,y, appends to sm). // the other container's empty space re-parents (appends).
const SNAP = 20, DRAG_THRESHOLD = 4; function wireSlot(el, i) { wireGridItem(el, "q", i); }
let drag = null; function wireCard(el, i) { wireGridItem(el, "sm", i); }
function wireSlot(el, i) { function wireGridItem(el, sec, i) {
el.addEventListener("click", (e) => {
if (drag && drag.moved) { e.preventDefault(); return; }
e.preventDefault();
const url = state.sections.q[i].url;
if (window.home && window.home.navigate) window.home.navigate(url);
else window.location.href = url;
});
el.addEventListener("contextmenu", (e) => {
e.preventDefault(); e.stopPropagation();
openCtxItem(e.clientX, e.clientY, "q", i);
});
el.addEventListener("pointerdown", (e) => startSlotDrag(e, i, el));
}
function startSlotDrag(e, i, el) {
if (e.button !== 0) return;
if (e.target.closest(".ctx-menu")) return;
const rect = el.getBoundingClientRect();
drag = {
kind: "slot", i, el,
grabX: e.clientX - rect.left, grabY: e.clientY - rect.top,
startX: e.clientX, startY: e.clientY, moved: false,
origLeft: el.style.left, origTop: el.style.top,
};
el.setPointerCapture(e.pointerId);
hideAllCtx();
}
window.addEventListener("pointermove", (e) => {
if (!drag || drag.kind !== "slot") return;
if (!drag.moved) {
if (Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < DRAG_THRESHOLD) return;
drag.moved = true;
drag.el.classList.add("dragging");
document.body.classList.add("dragging-any");
}
const slotsList = $("slotsList");
const target = document.elementFromPoint(e.clientX, e.clientY);
const overSlots = target && target.closest("#slotsList");
const overCards = target && target.closest("#cardsGrid");
document.querySelectorAll(".drop-target").forEach((g) => g.classList.remove("drop-target"));
if (overCards) overCards.classList.add("drop-target");
if (overSlots || !overCards) {
// Live-move within the free area.
const r = slotsList.getBoundingClientRect();
const w = drag.el.offsetWidth, h = drag.el.offsetHeight;
const nx = Math.max(0, Math.min(r.width - w, e.clientX - r.left - drag.grabX));
const ny = Math.max(0, Math.min(r.height - h, e.clientY - r.top - drag.grabY));
drag.el.style.left = Math.round(nx / SNAP) * SNAP + "px";
drag.el.style.top = Math.round(ny / SNAP) * SNAP + "px";
}
});
window.addEventListener("pointerup", (e) => {
if (!drag || drag.kind !== "slot") return;
const el = drag.el;
el.classList.remove("dragging");
document.body.classList.remove("dragging-any");
document.querySelectorAll(".drop-target").forEach((g) => g.classList.remove("drop-target"));
if (drag.moved) {
const target = document.elementFromPoint(e.clientX, e.clientY);
const overCards = target && target.closest("#cardsGrid");
if (overCards) {
// Re-parent into the right-side grid (drop x,y — cards flow).
const moved = state.sections.q[drag.i];
delete moved.x; delete moved.y;
state.sections.q.splice(drag.i, 1);
state.sections.sm.push(moved);
persist(); render();
} else {
// Commit the final x,y to state and persist.
const moved = state.sections.q[drag.i];
moved.x = parseInt(el.style.left, 10);
moved.y = parseInt(el.style.top, 10);
persist();
}
}
drag = null;
});
// RIGHT cards keep HTML5 DnD for grid reorder with before/after indicators.
function wireCard(el, i) {
el.draggable = true; el.draggable = true;
el.addEventListener("click", (e) => { el.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
const url = state.sections.sm[i].url; const url = state.sections[sec][i].url;
if (window.home && window.home.navigate) window.home.navigate(url); if (window.home && window.home.navigate) window.home.navigate(url);
else window.location.href = url; else window.location.href = url;
}); });
el.addEventListener("contextmenu", (e) => { el.addEventListener("contextmenu", (e) => {
e.preventDefault(); e.stopPropagation(); e.preventDefault(); e.stopPropagation();
openCtxItem(e.clientX, e.clientY, "sm", i); openCtxItem(e.clientX, e.clientY, sec, i);
}); });
el.addEventListener("dragstart", (e) => { el.addEventListener("dragstart", (e) => {
e.dataTransfer.effectAllowed = "move"; e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("application/x-slot", JSON.stringify({ sec: "sm", i })); e.dataTransfer.setData("application/x-slot", JSON.stringify({ sec, i }));
el.classList.add("dragging"); el.classList.add("dragging");
hideAllCtx(); hideAllCtx();
}); });
@ -578,36 +495,20 @@
const r = el.getBoundingClientRect(); const r = el.getBoundingClientRect();
const side = e.clientX < r.left + r.width / 2 ? -1 : 1; const side = e.clientX < r.left + r.width / 2 ? -1 : 1;
let targetIdx = i + (side === 1 ? 1 : 0); let targetIdx = i + (side === 1 ? 1 : 0);
moveItem(payload.sec, payload.i, "sm", targetIdx); moveItem(payload.sec, payload.i, sec, targetIdx);
}); });
} }
// Container-level drops. Slot→card re-parenting is handled by the slot // Container-level drops handle cross-section drops onto empty space —
// pointer flow; this handles card→slot (seeds x,y) and card reorder onto // the item appends to the target section's grid.
// empty cards-grid space.
function wireContainer(container, sec) { function wireContainer(container, sec) {
container.addEventListener("dragover", (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }); container.addEventListener("dragover", (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; });
container.addEventListener("drop", (e) => { container.addEventListener("drop", (e) => {
if (e.target !== container) return;
e.preventDefault(); e.preventDefault();
const payload = tryParse(e.dataTransfer.getData("application/x-slot")); const payload = tryParse(e.dataTransfer.getData("application/x-slot"));
if (!payload) return; if (!payload) return;
if (sec === "q") { moveItem(payload.sec, payload.i, sec, state.sections[sec].length);
// Card → slot. Pull it out of sm, seed x,y under the drop point.
if (payload.sec !== "sm") return;
const r = container.getBoundingClientRect();
const moved = state.sections.sm[payload.i];
if (!moved) return;
state.sections.sm.splice(payload.i, 1);
const nx = Math.round(Math.max(0, e.clientX - r.left - 36) / SNAP) * SNAP;
const ny = Math.round(Math.max(0, e.clientY - r.top - 36) / SNAP) * SNAP;
moved.x = nx; moved.y = ny;
state.sections.q.push(moved);
persist(); render();
return;
}
// sec === "sm" — card reorder onto empty space (append).
if (e.target !== container) return;
moveItem(payload.sec, payload.i, "sm", state.sections.sm.length);
}); });
} }
function tryParse(s) { try { return JSON.parse(s); } catch { return null; } } function tryParse(s) { try { return JSON.parse(s); } catch { return null; } }

View file

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