Theseus 0.3.94: home — free-drag left area, card sub-descriptions back
The left column goes back to free positioning: each icon stores its own x,y and snaps to a 20px grid; a faint grid shows underneath while dragging so placement feels OS-desktop. Live-move via pointer events (clicks still navigate thanks to a 4px move threshold). Dropping a left icon over the right card grid re-parents it as a card; dropping a card onto the left area turns it into a free-positioned icon. Silent Mode cards get their one-line description back under the title. Add/edit modal gains a Description field. Defaults seed sub-text on every Silent Mode card and x,y on every Your-links icon.
This commit is contained in:
parent
a3b20a2446
commit
2821453278
2 changed files with 174 additions and 58 deletions
230
home.html
230
home.html
|
|
@ -35,14 +35,23 @@
|
|||
gap: 36px; max-width: 1400px; 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; } }
|
||||
|
||||
/* LEFT AREA — 3-column grid of slots, with the "add" button flowing in
|
||||
the grid alongside them. Icons fill left-to-right, top-to-bottom; the
|
||||
container is wide enough that a few icons still read as a tidy row. */
|
||||
/* 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-column { display: flex; flex-direction: column; gap: 14px; }
|
||||
.slots-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; width: 100%; }
|
||||
@media (max-width: 900px) { .slots-list { grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); } }
|
||||
.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; }
|
||||
|
||||
.slot { position: relative; display: flex; flex-direction: column; align-items: center;
|
||||
.slot { 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); }
|
||||
|
|
@ -61,23 +70,16 @@
|
|||
.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: .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.dragging { opacity: .85; cursor: grabbing; z-index: 2; }
|
||||
|
||||
/* Slot-add sits in the grid alongside the slot icons so it's always on
|
||||
the next free cell, not pinned under the whole column. */
|
||||
.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: 22px; cursor: pointer;
|
||||
transition: color .12s, border-color .12s, background .12s;
|
||||
margin-top: 15px; /* visually below the label of the icon above */ }
|
||||
.slots-list .slot-add { margin-top: 0; } /* in-grid, no extra top gap */
|
||||
.slot-add:hover { color: var(--acid); border-color: var(--acid); background: rgb(from var(--acid) r g b / .04); }
|
||||
/* 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;
|
||||
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); }
|
||||
|
||||
/* RIGHT COLUMN — just the 3-column grid of Silent Mode cards; search
|
||||
lives in the hero above. */
|
||||
|
|
@ -86,8 +88,8 @@
|
|||
@media (max-width: 980px) { .cards-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 560px) { .cards-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.card { display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 10px; padding: 20px 14px; min-height: 128px;
|
||||
.card { display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
|
||||
gap: 8px; padding: 18px 14px 16px; min-height: 148px;
|
||||
background: var(--card); border: 1px solid var(--line); border-radius: 14px;
|
||||
text-decoration: none; color: inherit; cursor: pointer; position: relative;
|
||||
transition: transform .12s, background .12s, border-color .12s; }
|
||||
|
|
@ -102,6 +104,10 @@
|
|||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
||||
.card .card-title .tld { color: var(--acid); font-weight: 700; letter-spacing: .3px;
|
||||
text-shadow: 0 0 8px rgb(from var(--acid) r g b / .35); }
|
||||
.card .card-sub { font-size: 12px; color: var(--mut); text-align: center; line-height: 1.35;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||||
overflow: hidden; max-width: 100%; }
|
||||
.card:hover .card-sub { color: var(--ink); }
|
||||
.card.dragging { opacity: .6; }
|
||||
.card.drag-before::before, .card.drag-after::after {
|
||||
content: ""; position: absolute; top: 10%; bottom: 10%; width: 3px;
|
||||
|
|
@ -231,6 +237,8 @@
|
|||
<h2 id="modalTitle">Add link</h2>
|
||||
<label>Title</label><input id="mTitle" placeholder="e.g. hello.bch">
|
||||
<label>URL</label><input id="mUrl" placeholder="https://example.com/">
|
||||
<label>Description (shown on cards)</label>
|
||||
<input id="mSub" placeholder="one short line — optional">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>Column</label>
|
||||
|
|
@ -311,26 +319,28 @@
|
|||
let state = null;
|
||||
function defaultState() {
|
||||
return {
|
||||
version: 5,
|
||||
version: 6,
|
||||
iconSize: "medium",
|
||||
background: { kind: "preset", value: "dark" },
|
||||
sections: {
|
||||
sm: [
|
||||
{ title: "Theseus.X", url: "https://theseus.x/", brand: "theseus" },
|
||||
{ title: "SilentMode.X", url: "https://silentmode.x/", brand: "silentmode" },
|
||||
{ title: "Sirius.X", url: "https://sirius.x/", brand: "sirius" },
|
||||
{ title: "Hephaestus.X", url: "https://hephaestus.x/", brand: "hephaestus" },
|
||||
{ title: "Prometheus.X", url: "https://prometheus.x/", brand: "prometheus" },
|
||||
{ title: "Hermes.X", url: "https://hermes.x/", brand: "hermes" },
|
||||
{ title: "Helios.X", url: "https://helios.x/", brand: "helios" },
|
||||
{ title: "Ariadne.X", url: "https://ariadne.x/", brand: "ariadne" },
|
||||
{ title: "Aegis.X", url: "https://aegis.x/", brand: "aegis" },
|
||||
{ title: "Theseus.X", url: "https://theseus.x/", brand: "theseus", sub: "The Web Navigator — this browser's home." },
|
||||
{ title: "SilentMode.X", url: "https://silentmode.x/", brand: "silentmode", sub: "Infrastructure for a decentralized web." },
|
||||
{ title: "Sirius.X", url: "https://sirius.x/", brand: "sirius", sub: "Register and manage BCDN names." },
|
||||
{ title: "Hephaestus.X", url: "https://hephaestus.x/", brand: "hephaestus", sub: "The forge — Silent Mode's code host." },
|
||||
{ title: "Prometheus.X", url: "https://prometheus.x/", brand: "prometheus", sub: "Decentralized app marketplace." },
|
||||
{ title: "Hermes.X", url: "https://hermes.x/", brand: "hermes", sub: "E2E-encrypted messaging over Nostr." },
|
||||
{ title: "Helios.X", url: "https://helios.x/", brand: "helios", sub: "Search engine for the decentralized web." },
|
||||
{ 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.
|
||||
q: [
|
||||
{ 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" },
|
||||
{ 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 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -387,6 +397,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.
|
||||
const q = state.sections.q || [];
|
||||
const list = $("slotsList");
|
||||
list.innerHTML = "";
|
||||
|
|
@ -394,6 +406,9 @@
|
|||
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");
|
||||
|
|
@ -402,7 +417,7 @@
|
|||
el.style.setProperty("--iconFg", "#e7eaf1");
|
||||
}
|
||||
el.innerHTML = `<div class="tile">${iconArt(ic)}</div><div class="label">${esc(ic.title || ic.url)}</div>`;
|
||||
wireItem(el, "q", i);
|
||||
wireSlot(el, i);
|
||||
list.appendChild(el);
|
||||
});
|
||||
const slotAdd = document.createElement("button");
|
||||
|
|
@ -411,6 +426,7 @@
|
|||
slotAdd.oncontextmenu = (e) => e.preventDefault();
|
||||
list.appendChild(slotAdd);
|
||||
|
||||
// RIGHT — Silent Mode cards with sub-description under the title.
|
||||
const sm = state.sections.sm || [];
|
||||
const grid = $("cardsGrid");
|
||||
grid.innerHTML = "";
|
||||
|
|
@ -422,8 +438,9 @@
|
|||
el.style.setProperty("--iconBg", ic.color || "#d6ff3d");
|
||||
el.style.setProperty("--iconFg", isLightColor(ic.color || "#d6ff3d") ? "#0b0e14" : "#ffffff");
|
||||
}
|
||||
el.innerHTML = `<div class="card-ico">${iconArt(ic)}</div><div class="card-title">${styledTitle(ic.title)}</div>`;
|
||||
wireItem(el, "sm", i);
|
||||
const sub = ic.sub ? `<div class="card-sub">${esc(ic.sub)}</div>` : "";
|
||||
el.innerHTML = `<div class="card-ico">${iconArt(ic)}</div><div class="card-title">${styledTitle(ic.title)}</div>${sub}`;
|
||||
wireCard(el, i);
|
||||
grid.appendChild(el);
|
||||
});
|
||||
const add = document.createElement("button");
|
||||
|
|
@ -435,25 +452,107 @@
|
|||
$("ctxIconSizeVal").textContent = state.iconSize;
|
||||
}
|
||||
|
||||
// Drag-reorder: left column is a vertical stack, right column is a 3-wide
|
||||
// grid. We use the HTML5 drag-and-drop API because it's simple and the
|
||||
// user explicitly asked for drag-and-drop. A tiny move-threshold would
|
||||
// require pointer events; keeping HTML5 DnD means plain clicks navigate.
|
||||
function wireItem(el, sec, i) {
|
||||
el.draggable = true;
|
||||
// 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;
|
||||
|
||||
function wireSlot(el, i) {
|
||||
el.addEventListener("click", (e) => {
|
||||
if (drag && drag.moved) { e.preventDefault(); return; }
|
||||
e.preventDefault();
|
||||
const url = state.sections[sec][i].url;
|
||||
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, sec, i);
|
||||
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.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const url = state.sections.sm[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);
|
||||
});
|
||||
el.addEventListener("dragstart", (e) => {
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setData("application/x-slot", JSON.stringify({ sec, i }));
|
||||
e.dataTransfer.setData("application/x-slot", JSON.stringify({ sec: "sm", i }));
|
||||
el.classList.add("dragging");
|
||||
hideAllCtx();
|
||||
});
|
||||
|
|
@ -465,7 +564,6 @@
|
|||
el.addEventListener("dragover", (e) => {
|
||||
e.preventDefault(); e.dataTransfer.dropEffect = "move";
|
||||
const r = el.getBoundingClientRect();
|
||||
// Both sections are 2D grids now — horizontal mid-split picks insert side.
|
||||
const before = e.clientX < r.left + r.width / 2;
|
||||
el.classList.toggle("drag-before", before);
|
||||
el.classList.toggle("drag-after", !before);
|
||||
|
|
@ -480,20 +578,36 @@
|
|||
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, sec, targetIdx);
|
||||
moveItem(payload.sec, payload.i, "sm", targetIdx);
|
||||
});
|
||||
}
|
||||
|
||||
// The slots-list and cards-grid themselves accept drops at the end — this
|
||||
// handles cross-column moves when the pointer lands on empty space.
|
||||
// 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.
|
||||
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;
|
||||
moveItem(payload.sec, payload.i, sec, state.sections[sec].length);
|
||||
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);
|
||||
});
|
||||
}
|
||||
function tryParse(s) { try { return JSON.parse(s); } catch { return null; } }
|
||||
|
|
@ -648,10 +762,11 @@
|
|||
editKey = { sec, i };
|
||||
const isNew = i < 0;
|
||||
$("modalTitle").textContent = isNew ? "Add link" : "Edit link";
|
||||
const c = isNew ? { title: "", url: "", color: "#d6ff3d", letter: "" }
|
||||
const c = isNew ? { title: "", url: "", sub: "", color: "#d6ff3d", letter: "" }
|
||||
: state.sections[sec][i];
|
||||
$("mTitle").value = c.title || "";
|
||||
$("mUrl").value = c.url || "";
|
||||
$("mSub").value = c.sub || "";
|
||||
$("mSection").value = sec;
|
||||
$("mColor").value = c.color || "#d6ff3d";
|
||||
$("mLetter").value = c.letter || "";
|
||||
|
|
@ -668,6 +783,7 @@
|
|||
if (!/^[a-z]+:\/\//i.test(url) && !/^[a-z]+:/.test(url)) url = "https://" + url;
|
||||
const next = {
|
||||
title, url,
|
||||
sub: $("mSub").value.trim() || null,
|
||||
color: $("mColor").value,
|
||||
letter: $("mLetter").value.trim() || null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.93",
|
||||
"version": "0.3.94",
|
||||
"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