From 9450f5ec3a9554c513cf4edc8ae42578b563ba67 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 21 Oct 2026 01:31:21 +0300 Subject: [PATCH] =?UTF-8?q?Theseus=200.3.96:=20home=20=E2=80=94=20left=20c?= =?UTF-8?q?olumn=20back=20to=20grid=20cells,=20widened=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- home.html | 199 +++++++++++++-------------------------------------- package.json | 2 +- 2 files changed, 51 insertions(+), 150 deletions(-) diff --git a/home.html b/home.html index 72ebb1b0..cc89cd7c 100644 --- a/home.html +++ b/home.html @@ -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 .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 - of icon slots, wider RIGHT area with a 3-column grid of Silent Mode - cards. Both expand together on wider screens. */ - .homepage { display: grid; grid-template-columns: minmax(300px, 340px) minmax(380px, 1fr); - gap: 36px; max-width: 1400px; margin: 2.4rem auto 0; padding: 0 32px 48px; align-items: start; } + /* Two-column desktop under the hero: a 3-column grid of icon slots on the + LEFT, a 3-column grid of Silent Mode cards on the RIGHT. Both expand + together on wider screens; the layout stretches to the viewport so the + left column doesn't crowd against the cards. */ + .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; } } - /* LEFT AREA — free-positioning canvas. Each icon stores its own x,y and - snaps to a 20px grid. A faint grid shows underneath while dragging to - make placement feel OS-desktop-like. */ + /* LEFT AREA — fixed-cell grid. Each icon lives in its own cell; drag + reorders which icon is in which cell. Simpler than free x,y and keeps + the icons tidy against the left edge of the column. */ .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::before { - content: ""; position: absolute; inset: 0; opacity: 0; - 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; } + .slots-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; width: 100%; align-items: start; } + @media (max-width: 900px) { .slots-list { grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); } } + .cards-grid.drop-target { outline: 2px dashed var(--acid); outline-offset: 4px; border-radius: 14px; } - .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; transition: transform .12s ease; } .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-large .tile { width: 88px; height: 88px; font-size: 34px; border-radius: 18px; } .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 - user always has a way to add without hunting for empty space. */ - .slot-add { position: absolute; left: 10px; bottom: 10px; - display: flex; align-items: center; justify-content: center; - width: 60px; height: 60px; border-radius: 14px; border: 2px dashed var(--line2); - background: rgba(20,26,36,.6); color: var(--mut); font-size: 20px; cursor: pointer; + /* 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; + min-height: 72px; border-radius: 12px; border: 2px dashed var(--line2); + background: transparent; color: var(--mut); font-size: 20px; 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 / .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 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: "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). - // Defaults seed a tidy row across the top of the left area; drag anywhere. + // Your-links icons live in fixed grid cells; drag reorders which + // icon is in which cell. Order in the array = position in the grid + // (row-first, 3 per row). q: [ - { title: "hello.bch", url: "https://hello.bch/", brand: "hellobch", x: 10, y: 10 }, - { title: "sia.coin", url: "https://sia.coin/", brand: "siacoin", x: 110, y: 10 }, - { title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos", x: 210, y: 10 }, - { title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot", x: 10, y: 120 }, + { title: "hello.bch", url: "https://hello.bch/", brand: "hellobch" }, + { title: "sia.coin", url: "https://sia.coin/", brand: "siacoin" }, + { title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos" }, + { title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot" }, ], }, }; @@ -397,8 +396,8 @@ function iconUsesBrand(icon) { return !!(icon.brand && BRAND[icon.brand]); } function render() { - // LEFT — free-positioned icons. Each stores its own x,y; legacy items - // without x,y fall back to a tidy auto-layout at the top of the area. + // LEFT — fixed-cell grid. Icons flow left-to-right, top-to-bottom; the + // "+" cell always sits at the end so there's a visible way to add. const q = state.sections.q || []; const list = $("slotsList"); list.innerHTML = ""; @@ -406,9 +405,6 @@ const el = document.createElement("a"); el.className = `slot size-${state.iconSize}`; 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)) { el.style.setProperty("--iconBg", ic.color || "#d6ff3d"); el.style.setProperty("--iconFg", isLightColor(ic.color || "#d6ff3d") ? "#0b0e14" : "#ffffff"); @@ -452,107 +448,28 @@ $("ctxIconSizeVal").textContent = state.iconSize; } - // LEFT slots use pointer events for free-drag: live movement with snap to - // a 20px grid, final x,y committed on pointerup. A short move threshold - // keeps plain clicks working for navigation. Dropping over the right - // card grid re-parents the item (removes x,y, appends to sm). - const SNAP = 20, DRAG_THRESHOLD = 4; - let drag = null; + // Both sections are 2D grids: slots are fixed cells on the left (3 wide), + // cards are fixed cells on the right (3 wide). Drag reorders inside a + // section with a before/after indicator on the hover target; drop onto + // the other container's empty space re-parents (appends). + function wireSlot(el, i) { wireGridItem(el, "q", i); } + function wireCard(el, i) { wireGridItem(el, "sm", i); } - function wireSlot(el, 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) { + function wireGridItem(el, sec, i) { el.draggable = true; el.addEventListener("click", (e) => { 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); else window.location.href = url; }); el.addEventListener("contextmenu", (e) => { e.preventDefault(); e.stopPropagation(); - openCtxItem(e.clientX, e.clientY, "sm", i); + openCtxItem(e.clientX, e.clientY, sec, i); }); el.addEventListener("dragstart", (e) => { 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"); hideAllCtx(); }); @@ -578,36 +495,20 @@ const r = el.getBoundingClientRect(); const side = e.clientX < r.left + r.width / 2 ? -1 : 1; 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 - // pointer flow; this handles card→slot (seeds x,y) and card reorder onto - // empty cards-grid space. + // Container-level drops handle cross-section drops onto empty space — + // the item appends to the target section's grid. function wireContainer(container, sec) { container.addEventListener("dragover", (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }); container.addEventListener("drop", (e) => { + if (e.target !== container) return; e.preventDefault(); const payload = tryParse(e.dataTransfer.getData("application/x-slot")); if (!payload) return; - if (sec === "q") { - // 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); + moveItem(payload.sec, payload.i, sec, state.sections[sec].length); }); } function tryParse(s) { try { return JSON.parse(s); } catch { return null; } } diff --git a/package.json b/package.json index 4b97bb82..d242b6c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.95", + "version": "0.3.96", "license": "MPL-2.0", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode",