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