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",