theseus/home.html

682 lines
41 KiB
HTML
Raw Normal View History

<!doctype html>
<html>
<head><meta charset="utf-8"><title>Theseus</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%230b0e14'/%3E%3Cpath d='M32 6 L38 32 L26 32 Z' fill='%23e04e4e'/%3E%3Cpath d='M38 32 L32 58 L26 32 Z' fill='%23d6ff3d'/%3E%3Cpath d='M6 32 L32 27 L58 32 L32 37 Z' fill='%23d6ff3d' opacity='0.55'/%3E%3Ccircle cx='32' cy='32' r='3' fill='%230b0e14'/%3E%3C/svg%3E">
<style>
:root { color-scheme: light dark;
--bg1: #16202e; --bg2: #0b0e14;
--card: #141a24; --card-hi: #18202c; --line: #ffffff12; --line2: #ffffff22;
--ink: #e7eaf1; --mut: #8b98a9; --dim: #5e6678;
--acid: #d6ff3d; --blue: #4b7bec; --sia: #b39ddb; --srv: #4fd1a5; --warm: #ffc75f; --rose: #f6768a; }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; min-height: 100vh; font-family: system-ui, sans-serif; color: var(--ink); }
body {
display: flex; flex-direction: column;
background: radial-gradient(1200px 600px at 50% -10%, var(--bg1), var(--bg2));
}
/* Hero (identity + search). Centered at the top of the page, same shape
as the pre-desktop home — this is what the user sees most often. */
.hero { text-align: center; padding: 5vh 1rem 0; }
.hero .mark { display: inline-block; width: 64px; height: 64px; line-height: 1; }
.hero .mark svg { width: 100%; height: 100%; display: block; }
.hero h1 { font-size: 2.4rem; margin: .3rem 0 .2rem; letter-spacing: .5px; font-weight: 600; }
.hero h1 .g { color: var(--acid); }
.hero .tag { color: var(--mut); margin: 0 0 1.4rem; }
.hero form { display: flex; gap: 8px; justify-content: center; max-width: 620px; margin: 0 auto; padding: 0 1rem; }
.hero form input { flex: 1; padding: 13px 18px; border-radius: 999px; border: 1px solid var(--line2);
background: rgba(20,26,36,.72); color: var(--ink); font-size: 15px; outline: none;
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
.hero form input:focus { border-color: var(--blue); }
.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; }
.corner { position: absolute; top: 18px; right: 18px; z-index: 10; }
.corner button { background: rgba(20,26,36,.6); border: 1px solid var(--line2); color: var(--mut);
border-radius: 8px; padding: 8px 14px; font-size: 12.5px; cursor: pointer; backdrop-filter: blur(6px); }
.corner button:hover { background: var(--card-hi); color: var(--ink); }
.corner button.on { border-color: var(--acid); color: var(--acid); background: rgb(from var(--acid) r g b / .08); }
/* Edit toolbar (shown only while editing) */
.editbar { display: none; margin: 1.6rem auto .4rem; gap: 14px; align-items: center; padding: 10px 16px;
background: rgba(20,26,36,.9); border: 1px solid var(--line2); border-radius: 14px;
backdrop-filter: blur(10px); box-shadow: 0 10px 30px #0008; width: fit-content; max-width: calc(100vw - 48px); flex-wrap: wrap; }
body.editing .editbar { display: flex; }
.editbar .group { display: flex; align-items: center; gap: 8px; }
.editbar .label { font-size: 11px; color: var(--mut); letter-spacing: .08em; text-transform: uppercase; }
.editbar .sz { display: inline-flex; border: 1px solid var(--line2); border-radius: 999px; overflow: hidden; }
.editbar .sz button { background: transparent; border: 0; color: var(--mut); padding: 6px 10px; font-size: 12px; cursor: pointer; }
.editbar .sz button.on { background: var(--acid); color: #0b0e14; font-weight: 600; }
.editbar .bg { display: inline-flex; gap: 6px; }
.editbar .bg button { width: 22px; height: 22px; border-radius: 6px; border: 1px solid var(--line2); cursor: pointer; padding: 0; }
.editbar .bg button.on { outline: 2px solid var(--acid); outline-offset: 1px; }
.editbar input[type=color] { width: 30px; height: 22px; border: 1px solid var(--line2); border-radius: 6px; background: transparent; padding: 0; cursor: pointer; }
.editbar .filebtn { font-size: 11.5px; color: var(--mut); border: 1px dashed var(--line2); border-radius: 6px; padding: 4px 10px; cursor: pointer; }
.editbar .filebtn:hover { color: var(--acid); border-color: var(--acid); }
.editbar .add { background: var(--acid); color: #0b0e14; font-weight: 600; border: none; border-radius: 8px; padding: 6px 12px; font-size: 12.5px; cursor: pointer; }
.editbar .reset { background: transparent; border: 1px solid var(--line2); color: var(--mut); border-radius: 8px; padding: 6px 12px; font-size: 12px; cursor: pointer; }
.editbar .reset:hover { color: var(--rose); border-color: var(--rose); }
/* Two-column canvas under the hero: narrow LEFT for icon tiles,
wider RIGHT for Silent Mode cards. Not full width — centered with
breathing room on both sides. Collapses to one column under 820px. */
.desktop { position: relative; width: 100%; max-width: 1180px; margin: 2.2rem auto 3rem; padding: 0 2rem;
display: grid; grid-template-columns: minmax(180px, 220px) minmax(380px, 760px);
gap: 44px; align-items: start; justify-content: center; }
.column { min-width: 0; }
.section-hdr { display: flex; align-items: baseline; gap: 10px; margin: 4px 4px 14px;
font-size: 11px; color: var(--mut); letter-spacing: .14em; text-transform: uppercase; font-weight: 600; }
.section-hdr .acid { color: var(--acid); }
.section-hdr .count { color: var(--dim); font-weight: 400; letter-spacing: .08em; font-size: 10.5px; }
.section-hdr .rule { flex: 1; height: 1px; background: linear-gradient(to right, var(--line2), transparent); }
body.dragging-any .section-grid, body.dragging-any .cards-grid { outline: 1px dashed var(--line2); outline-offset: 4px; border-radius: 14px; }
body.dragging-any .section-grid.drop-target, body.dragging-any .cards-grid.drop-target { outline-color: var(--acid); background: rgb(from var(--acid) r g b / .04); }
@media (max-width: 820px) { .desktop { grid-template-columns: 1fr; gap: 20px; padding: 0 1rem; } }
/* LEFT column — OS-desktop style: icons live at absolute x,y positions
inside the column area so the user can drag them anywhere. The grid
is a faint 20px snap grid that appears while dragging. Minimum height
keeps the area tall enough that the first drag has somewhere to go. */
.section-grid { position: relative; padding: 8px 4px; min-height: 420px; border-radius: 10px; }
.section-grid::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 .section-grid::before { opacity: 1; }
.section-grid .icon { position: absolute; }
.icon { display: flex; flex-direction: column; align-items: center; position: relative;
cursor: pointer; user-select: none; text-decoration: none; color: inherit;
transition: transform .12s ease; }
.icon:hover { transform: translateY(-2px); }
.icon .tile { display: flex; align-items: center; justify-content: center;
border-radius: 18px; background: var(--iconBg, #0b0e14);
color: var(--iconFg, #e7eaf1); font-weight: 700; letter-spacing: -.02em;
box-shadow: 0 6px 18px #0007, inset 0 0 0 1px rgba(255,255,255,.08);
overflow: hidden; }
.icon .tile svg, .icon .tile img { width: 72%; height: 72%; display: block; }
.icon .label { margin-top: 7px; font-size: 12.5px; line-height: 1.2; color: #fff;
text-shadow: 0 1px 3px #000c, 0 0 10px #000a; max-width: 100%;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
padding: 2px 6px; border-radius: 4px; }
body.light .icon .label { color: #0b0e14; text-shadow: 0 1px 2px #fffb, 0 0 10px #fffc; }
.icon.size-small .tile { width: 56px; height: 56px; font-size: 22px; border-radius: 14px; }
.icon.size-medium .tile { width: 72px; height: 72px; font-size: 28px; border-radius: 16px; }
.icon.size-large .tile { width: 96px; height: 96px; font-size: 36px; border-radius: 20px; }
.icon .label { max-width: 100px; }
.icon.size-small .label { max-width: 72px; font-size: 11px; margin-top: 5px; }
/* Silent Mode CARDS — icon + title + sub + badge, one per row */
.cards-grid { display: flex; flex-direction: column; gap: 10px; padding: 10px 0; min-height: 140px; }
.card { display: flex; align-items: center; gap: 14px; padding: 14px 18px;
background: var(--card); border: 1px solid var(--line); border-radius: 14px;
text-decoration: none; color: inherit; cursor: pointer;
transition: transform .12s, background .12s, border-color .12s; position: relative; }
.card:hover { background: var(--card-hi); border-color: rgba(75,123,236,.4); transform: translateY(-1px); }
.card .card-ico { flex: none; width: 44px; height: 44px; border-radius: 10px;
background: var(--iconBg, transparent);
display: flex; align-items: center; justify-content: center;
color: var(--iconFg, #e7eaf1); font-weight: 700; font-size: 20px;
overflow: hidden; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06); }
.card .card-ico svg, .card .card-ico img { width: 100%; height: 100%; display: block; }
.card .card-body { flex: 1; min-width: 0; }
.card .card-title { font-size: 15px; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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 { color: var(--mut); font-size: 13px; margin-top: 3px; line-height: 1.4;
overflow: hidden; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; }
.card:hover .card-sub { color: var(--ink); }
.card .card-badge { flex: none; font-size: 10.5px; padding: 2px 9px; border-radius: 999px;
letter-spacing: .02em; background: rgb(from var(--acid) r g b / .12); color: var(--acid); }
.ctrls { position: absolute; top: -8px; right: -8px; display: none; gap: 2px; z-index: 2; }
.ctrls button { width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--line2);
background: rgba(0,0,0,.65); color: #fff; font-size: 11px; cursor: pointer; padding: 0; line-height: 1; }
.ctrls button.rm:hover { background: var(--rose); border-color: var(--rose); }
.ctrls button.ed:hover { background: var(--blue); border-color: var(--blue); }
body.editing .icon .ctrls, body.editing .card .ctrls { display: flex; }
body.editing .icon, body.editing .card { cursor: grab; }
body.editing .icon.dragging, body.editing .card.dragging { opacity: .85; cursor: grabbing; }
.add-slot { display: none; align-items: center; justify-content: center;
border: 2px dashed var(--line2); border-radius: 14px;
color: var(--mut); font-size: 13.5px; cursor: pointer; }
/* Left-column + slot is positioned at the bottom of the free-drag area */
.column.left .add-slot { position: absolute; left: 10px; bottom: 10px;
width: 60px; height: 60px; border-radius: 14px; font-size: 11px; }
.column.right .add-slot { width: 100%; padding: 12px; border-radius: 14px; }
.add-slot:hover { border-color: var(--acid); color: var(--acid); background: rgb(from var(--acid) r g b / .04); }
body.editing .add-slot { display: inline-flex; }
.column.right .add-slot { display: none; }
body.editing .column.right .add-slot { display: flex; }
/* Add/Edit modal */
.modal-bg { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 100;
align-items: center; justify-content: center; padding: 1rem; backdrop-filter: blur(4px); }
.modal-bg.on { display: flex; }
.modal { background: var(--card); border: 1px solid var(--line2); border-radius: 14px;
padding: 22px; width: 100%; max-width: 460px; box-shadow: 0 20px 60px #000c; }
.modal h2 { margin: 0 0 14px; font-size: 17px; color: var(--ink); }
.modal label { display: block; font-size: 12.5px; color: var(--mut); margin: 10px 0 4px; letter-spacing: .04em; text-transform: uppercase; }
.modal input, .modal select { width: 100%; padding: 9px 12px; border-radius: 8px; border: 1px solid var(--line2);
background: var(--bg2); color: var(--ink); font-size: 13.5px; outline: none; font-family: inherit; }
.modal input:focus, .modal select:focus { border-color: var(--blue); }
.modal .row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.modal .btns { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
.modal button { border-radius: 8px; padding: 8px 16px; font-size: 13.5px; cursor: pointer; border: 1px solid var(--line2);
background: transparent; color: var(--mut); }
.modal button:hover { background: var(--card-hi); color: var(--ink); }
.modal button.primary { background: var(--blue); color: #fff; border-color: var(--blue); }
@media (prefers-color-scheme: light) {
:root { --bg1: #dce6f2; --bg2: #eef0f4;
--card: #ffffff; --card-hi: #f4f7fb; --line: rgba(0,0,0,.10); --line2: rgba(0,0,0,.14);
--ink: #1a1f28; --mut: #4a5262; --dim: #8a93a2;
--acid: #0AC18E; }
.icon .label { color: #0b0e14; text-shadow: 0 1px 2px #fffb, 0 0 10px #fffc; }
.modal { background: #ffffff; }
}
</style>
</head>
<body>
<div class="corner">
<button id="editBtn" type="button">Edit desktop</button>
</div>
<div class="hero">
<div class="mark">
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M32 6 L38 32 L26 32 Z" fill="#e04e4e"/>
<path d="M38 32 L32 58 L26 32 Z" fill="#d6ff3d"/>
<path d="M6 32 L32 27 L58 32 L32 37 Z" fill="#d6ff3d" opacity="0.55"/>
<circle cx="32" cy="32" r="3" fill="#0b0e14"/>
</svg>
</div>
<h1>Theseus <span class="g">Navigator</span></h1>
<p class="tag">A browser that follows the thread. Names that live on the Bitcoin Cash chain.</p>
<form id="searchForm">
<input id="q" placeholder="Search the web" autofocus spellcheck="false">
<button type="submit">Search</button>
</form>
<p class="hint">Tip: type a <b>.bch</b> name in the address bar above to open a decentralized site.</p>
<div class="editbar">
<div class="group">
<span class="label">Icon size</span>
<div class="sz">
<button data-size="small">S</button>
<button data-size="medium" class="on">M</button>
<button data-size="large">L</button>
</div>
</div>
<div class="group">
<span class="label">Background</span>
<div class="bg">
<button data-bg="dark" class="on" style="background:#0b0e14" title="Dark"></button>
<button data-bg="indigo" style="background:linear-gradient(135deg,#1e2a4a,#0b0e14)" title="Indigo"></button>
<button data-bg="forest" style="background:linear-gradient(135deg,#132821,#0b140e)" title="Forest"></button>
<button data-bg="rose" style="background:linear-gradient(135deg,#3a1830,#140b14)" title="Rose"></button>
<button data-bg="warm" style="background:linear-gradient(135deg,#3a2a18,#140e0b)" title="Warm"></button>
<button data-bg="light" style="background:linear-gradient(135deg,#dce6f2,#eef0f4)" title="Light"></button>
</div>
<input id="bgColor" type="color" value="#0b0e14" title="Custom color">
<label class="filebtn">Upload image<input id="bgImage" type="file" accept="image/*" style="display:none"></label>
</div>
<div class="group">
<button class="add" id="addBtn">+ Add link</button>
<button class="reset" id="resetBtn" title="Restore the default layout">Reset</button>
</div>
</div>
</div>
<div class="desktop" id="desktop">
<div class="column left">
<div class="section-hdr">
<span>Your links</span>
<span class="count" id="qCount"></span>
<span class="rule"></span>
</div>
<div class="section-grid" id="qGrid" data-sec="q"></div>
</div>
<div class="column right">
<div class="section-hdr">
<span class="acid">Silent Mode</span>
<span class="count" id="smCount"></span>
<span class="rule"></span>
</div>
<div class="cards-grid" id="smGrid" data-sec="sm"></div>
</div>
</div>
<div class="modal-bg" id="modal">
<div class="modal">
<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 in cards)</label><input id="mSub" placeholder="one-line description (optional)">
<div class="row">
<div>
<label>Column</label>
<select id="mSection">
<option value="q">Your links (left)</option>
<option value="sm">Silent Mode (right)</option>
</select>
</div>
<div>
<label>Badge (for cards)</label>
<input id="mBadge" placeholder="e.g. Navigator">
</div>
</div>
<label>Icon color (used when no brand mark)</label>
<input id="mColor" type="color" value="#d6ff3d" style="width:60px;height:30px;padding:0;cursor:pointer">
<label>Letter / emoji (optional — default = first letter)</label>
<input id="mLetter" placeholder="e.g. H, 🌿, ⛓️" maxlength="4">
<div class="btns"><button id="mCancel" type="button">Cancel</button><button id="mSave" class="primary" type="button">Save</button></div>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s || "").replace(/</g, "&lt;").replace(/"/g, "&quot;");
function styledTitle(t) {
const m = String(t || "").match(/^(.+?)(\.[A-Za-z]{1,10})$/);
if (!m) return esc(t);
return esc(m[1]) + '<span class="tld">' + esc(m[2]) + '</span>';
}
// Brand marks — inline SVG strings or { src } references to files bundled
// under home-brand/. Fall back to a letter tile when the icon has no brand.
const BRAND = {
theseus: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M32 6 L38 32 L26 32 Z" fill="#e04e4e"/><path d="M38 32 L32 58 L26 32 Z" fill="#d6ff3d"/><path d="M6 32 L32 27 L58 32 L32 37 Z" fill="#d6ff3d" opacity=".55"/><circle cx="32" cy="32" r="3" fill="#0b0e14"/></svg>`,
silentmode: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><rect x="10" y="22" width="26" height="20" rx="10" fill="none" stroke="#8b98a9" stroke-width="4"/><rect x="28" y="22" width="26" height="20" rx="10" fill="none" stroke="#d6ff3d" stroke-width="4"/></svg>`,
sirius: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M32 4 L36.4 27.6 L60 32 L36.4 36.4 L32 60 L27.6 36.4 L4 32 L27.6 27.6 Z" fill="#d6ff3d"/></svg>`,
hephaestus: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M14 36 h36 l-4 6 h-28 Z" fill="#ffc75f"/><rect x="26" y="42" width="12" height="8" fill="#ffc75f"/><rect x="22" y="50" width="20" height="4" rx="1" fill="#ffc75f"/><rect x="34" y="12" width="4" height="18" rx="1" fill="#8b98a9" transform="rotate(25 36 21)"/><rect x="30" y="12" width="14" height="6" rx="1" fill="#e7eaf1" transform="rotate(25 37 15)"/></svg>`,
prometheus: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M32 8 C 26 18, 20 22, 20 32 C 20 42, 26 50, 32 50 C 38 50, 44 42, 44 32 C 44 24, 40 22, 36 18 C 36 24, 34 26, 32 28 C 30 24, 34 16, 32 8 Z" fill="#ffc75f"/><path d="M32 20 C 28 26, 26 30, 26 36 C 26 42, 29 46, 32 46 C 35 46, 38 42, 38 36 C 38 32, 36 30, 34 26 C 34 30, 33 32, 32 32 C 30 28, 33 24, 32 20 Z" fill="#f6768a"/></svg>`,
hermes: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M4 28 L32 20 L60 28 L58 32 L32 26 L6 32 Z" fill="#e7eaf1" opacity=".65"/><rect x="14" y="26" width="36" height="22" rx="3" fill="none" stroke="#f6768a" stroke-width="3"/><path d="M14 30 L32 42 L50 30" fill="none" stroke="#f6768a" stroke-width="3"/></svg>`,
helios: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><circle cx="32" cy="32" r="12" fill="#ffc75f"/><g stroke="#ffc75f" stroke-width="3" stroke-linecap="round"><line x1="32" y1="8" x2="32" y2="14"/><line x1="32" y1="50" x2="32" y2="56"/><line x1="8" y1="32" x2="14" y2="32"/><line x1="50" y1="32" x2="56" y2="32"/><line x1="15" y1="15" x2="19" y2="19"/><line x1="45" y1="45" x2="49" y2="49"/><line x1="15" y1="49" x2="19" y2="45"/><line x1="45" y1="19" x2="49" y2="15"/></g></svg>`,
ariadne: { src: "home-brand/ariadne-pottery.svg" },
pithos: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M22 18h20v4c0 2-2 3-2 5 8 3 14 10 14 20 0 10-9 17-22 17S10 57 10 47c0-10 6-17 14-20 0-2-2-3-2-5z" fill="#3ddc97"/><path d="M14 36h36M14 44h36" stroke="#0b1f18" stroke-width="2.5" stroke-linecap="round" opacity=".55"/></svg>`,
aegis: `<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><polygon points="16,2 28,9 28,23 16,30 4,23 4,9" fill="#0a0a0d" stroke="#D6FF3D" stroke-width="1.6" stroke-linejoin="round"/><circle cx="16" cy="16" r="4.5" fill="none" stroke="#D6FF3D" stroke-width="1.4"/><circle cx="16" cy="16" r="1.6" fill="#D6FF3D"/></svg>`,
bergamot: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M14 50 C 14 28, 30 14, 54 14 C 54 36, 40 50, 18 50 C 20 42, 30 32, 44 28" fill="#9fd98f" stroke="#4fa66b" stroke-width="2" stroke-linejoin="round"/></svg>`,
hellobch: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><text x="32" y="38" font-family="system-ui" font-weight="800" font-size="18" text-anchor="middle" fill="#8b98a9">hello</text><text x="32" y="54" font-family="system-ui" font-weight="800" font-size="13" text-anchor="middle" fill="#d6ff3d">.bch</text></svg>`,
siacoin: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><circle cx="32" cy="32" r="18" fill="#3ddc97"/><text x="32" y="40" font-family="system-ui" font-weight="800" font-size="22" text-anchor="middle" fill="#0b0e14">S</text></svg>`,
};
// v4 schema. Same shape as v3 (sections.sm + sections.q) with per-item
// `sub` + `badge` fields on cards. Main.js's migrateCards preserves these
// fields across v3 and v4 (it treats items as opaque objects).
let state = null;
function defaultState() {
return {
version: 4,
iconSize: "large",
quickSize: "medium",
background: { kind: "preset", value: "dark" },
sections: {
sm: [
{ title: "Theseus.X", url: "https://theseus.x/", brand: "theseus", sub: "The Web Navigator — this browser's own address.", badge: "Navigator" },
{ title: "SilentMode.X", url: "https://silentmode.x/", brand: "silentmode", sub: "Infrastructure development for a decentralized web.", badge: "Infrastructure" },
{ title: "Sirius.X", url: "https://sirius.x/", brand: "sirius", sub: "Register and manage BCDN names.", badge: "Registrar" },
{ title: "Hephaestus.X", url: "https://hephaestus.x/", brand: "hephaestus", sub: "The forge — Silent Mode's code host.", badge: "Code host" },
{ title: "Prometheus.X", url: "https://prometheus.x/", brand: "prometheus", sub: "Decentralized app marketplace.", badge: "App store" },
{ title: "Hermes.X", url: "https://hermes.x/", brand: "hermes", sub: "Messaging — end-to-end encrypted over Nostr.", badge: "Messaging" },
{ title: "Helios.X", url: "https://helios.x/", brand: "helios", sub: "Search engine for the decentralized web (in design).", badge: "Search" },
{ title: "Ariadne.X", url: "https://ariadne.x/", brand: "ariadne", sub: "BNS resolver — read decentralized names natively.", badge: "Resolver" },
{ title: "Aegis.X", url: "https://aegis.x/", brand: "aegis", sub: "Multi-chain wallet, derived from your Theseus vault.", badge: "Wallet" },
],
// Your-links icons are freely positioned (x,y per icon). The defaults
// put them in a 2-column grid near the top-left; drag anywhere to
// rearrange, like an OS desktop.
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: 10, y: 110 },
{ title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot", x: 110, y: 110 },
],
},
};
}
function ensureShape(s) {
if (!s || typeof s !== "object") return defaultState();
if (!s.sections || typeof s.sections !== "object") return defaultState();
if (!Array.isArray(s.sections.sm)) s.sections.sm = [];
if (!Array.isArray(s.sections.q)) s.sections.q = [];
if (!s.background) s.background = { kind: "preset", value: "dark" };
if (!s.iconSize) s.iconSize = "large";
if (!s.quickSize) s.quickSize = "medium";
return s;
}
function persist() { if (window.home && window.home.setCards) window.home.setCards(state); }
const BG_PRESETS = {
dark: "radial-gradient(1200px 600px at 50% -10%, #16202e, #0b0e14)",
indigo: "linear-gradient(135deg,#1e2a4a 0%, #0b0e14 100%)",
forest: "linear-gradient(135deg,#132821 0%, #0b140e 100%)",
rose: "linear-gradient(135deg,#3a1830 0%, #140b14 100%)",
warm: "linear-gradient(135deg,#3a2a18 0%, #140e0b 100%)",
light: "linear-gradient(135deg,#dce6f2 0%, #eef0f4 100%)",
};
function applyBg() {
const b = state.background || { kind: "preset", value: "dark" };
if (b.kind === "preset") document.body.style.background = BG_PRESETS[b.value] || BG_PRESETS.dark;
else if (b.kind === "color") document.body.style.background = b.value;
else if (b.kind === "image" && b.dataUrl) document.body.style.background = `#0b0e14 center/cover no-repeat url("${b.dataUrl}")`;
const isLight = (b.kind === "preset" && b.value === "light") || (b.kind === "color" && isLightColor(b.value));
document.body.classList.toggle("light", isLight);
document.querySelectorAll(".editbar .bg button").forEach((btn) => btn.classList.toggle("on", b.kind === "preset" && btn.dataset.bg === b.value));
$("bgColor").value = (b.kind === "color" ? b.value : "#0b0e14");
}
function isLightColor(hex) {
const h = String(hex || "").replace(/^#/, "");
if (h.length !== 6) return false;
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
return (r * 0.299 + g * 0.587 + b * 0.114) > 170;
}
function iconLetter(icon) {
if (icon.letter) return icon.letter;
const t = (icon.title || "").trim();
return (t.match(/^[A-Za-z0-9]/) || ["?"])[0].toUpperCase();
}
function iconArt(icon) {
if (icon.brand && BRAND[icon.brand]) {
const b = BRAND[icon.brand];
if (typeof b === "string") return b;
if (b && b.src) return `<img src="${esc(b.src)}" alt="" draggable="false" style="width:100%;height:100%;object-fit:contain">`;
}
return esc(iconLetter(icon));
}
function iconUsesBrand(icon) { return !!(icon.brand && BRAND[icon.brand]); }
function render() {
// LEFT column — freely positioned icon tiles. x/y are stored per icon
// and default to a tidy auto-layout if a legacy icon lacks them.
const q = state.sections.q || [];
const qGrid = $("qGrid");
qGrid.innerHTML = "";
$("qCount").textContent = `· ${q.length}`;
q.forEach((ic, i) => {
const size = ic.size || state.quickSize;
const el = document.createElement("a");
el.className = `icon size-${size}`;
el.href = ic.url; el.dataset.sec = "q"; el.dataset.i = i;
// Icons without x/y (legacy data or just-added) get placed on an
// implicit grid near the top so they don't pile on each other.
const autoX = 10 + (i % 2) * 100, autoY = 10 + Math.floor(i / 2) * 100;
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");
} else {
el.style.setProperty("--iconBg", "transparent");
el.style.setProperty("--iconFg", "#e7eaf1");
}
el.innerHTML = `
<div class="ctrls">
<button class="ed" title="Edit">&#9998;</button>
<button class="rm" title="Remove">&times;</button>
</div>
<div class="tile">${iconArt(ic)}</div>
<div class="label">${esc(ic.title || ic.url)}</div>`;
wireIconClicks(el, "q", i);
el.addEventListener("pointerdown", (e) => startDrag(e, "q", i, el));
qGrid.appendChild(el);
});
const qAdd = document.createElement("button");
qAdd.className = "add-slot"; qAdd.textContent = "+"; qAdd.title = "Add to Your links";
qAdd.onclick = () => openModal("q", -1);
qGrid.appendChild(qAdd);
// RIGHT column — Silent Mode cards
const sm = state.sections.sm || [];
const smGrid = $("smGrid");
smGrid.innerHTML = "";
$("smCount").textContent = `· ${sm.length}`;
sm.forEach((ic, i) => {
const el = document.createElement("a");
el.className = "card";
el.href = ic.url; el.dataset.sec = "sm"; el.dataset.i = i;
if (!iconUsesBrand(ic)) {
el.style.setProperty("--iconBg", ic.color || "#d6ff3d");
el.style.setProperty("--iconFg", isLightColor(ic.color || "#d6ff3d") ? "#0b0e14" : "#ffffff");
}
const badge = ic.badge ? `<span class="card-badge">${esc(ic.badge)}</span>` : "";
el.innerHTML = `
<div class="ctrls">
<button class="ed" title="Edit">&#9998;</button>
<button class="rm" title="Remove">&times;</button>
</div>
<div class="card-ico">${iconArt(ic)}</div>
<div class="card-body">
<div class="card-title">${styledTitle(ic.title)}</div>
<div class="card-sub">${esc(ic.sub || ic.url)}</div>
</div>
${badge}`;
wireIconClicks(el, "sm", i);
el.addEventListener("pointerdown", (e) => startDrag(e, "sm", i, el));
smGrid.appendChild(el);
});
const smAdd = document.createElement("button");
smAdd.className = "add-slot"; smAdd.textContent = "+ Add card to Silent Mode";
smAdd.onclick = () => openModal("sm", -1);
smGrid.appendChild(smAdd);
}
function wireIconClicks(el, sec, i) {
el.addEventListener("click", (e) => {
if (document.body.classList.contains("editing")) {
e.preventDefault();
if (e.target.classList.contains("rm")) return removeIcon(sec, i);
if (e.target.classList.contains("ed")) return openModal(sec, i);
return;
}
e.preventDefault();
const url = state.sections[sec][i].url;
if (window.home && window.home.navigate) window.home.navigate(url);
else window.location.href = url;
});
}
// Drag model: LEFT column uses free (x,y) positioning like an OS desktop —
// snap to 20px, clamp inside the column. RIGHT column (cards) stays
// grid-ordered — drag inserts at the nearest Y-midpoint. Drop into the
// other column re-parents (becomes free or inserted into the grid as
// appropriate).
const SNAP = 20;
let drag = null;
function startDrag(e, sec, i, el) {
if (!document.body.classList.contains("editing")) return;
if (e.target.closest(".ctrls")) return;
e.preventDefault();
const rect = el.getBoundingClientRect();
drag = { sec, i, el, grabX: e.clientX - rect.left, grabY: e.clientY - rect.top };
el.classList.add("dragging");
document.body.classList.add("dragging-any");
el.setPointerCapture(e.pointerId);
}
window.addEventListener("pointermove", (e) => {
if (!drag) return;
const target = document.elementFromPoint(e.clientX, e.clientY);
const over = target && (target.closest(".section-grid") || target.closest(".cards-grid"));
document.querySelectorAll(".drop-target").forEach((g) => { if (g !== over) g.classList.remove("drop-target"); });
if (over) over.classList.add("drop-target");
// Live move when dragging within the LEFT (free-positioning) column —
// gives OS-desktop feel. Cards stay still until drop.
if (drag.sec === "q" && over && over.id === "qGrid") {
const gridRect = over.getBoundingClientRect();
const w = drag.el.offsetWidth, h = drag.el.offsetHeight;
const nx = Math.max(0, Math.min(gridRect.width - w, e.clientX - gridRect.left - drag.grabX));
const ny = Math.max(0, Math.min(gridRect.height - h, e.clientY - gridRect.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) return;
const el = drag.el;
const target = document.elementFromPoint(e.clientX, e.clientY);
const targetGrid = target && (target.closest(".section-grid") || target.closest(".cards-grid"));
const targetSec = targetGrid && targetGrid.dataset ? targetGrid.dataset.sec : null;
if (targetSec && (targetSec === "sm" || targetSec === "q")) {
const moved = state.sections[drag.sec][drag.i];
if (targetSec === "q") {
// LEFT drop — set the free x,y from where the pointer landed,
// clamped inside the grid area. From the SAME column this just
// keeps the x,y the live-move computed; from the OTHER column
// (RIGHT → LEFT) we move the item over and seed its x,y.
const gridRect = targetGrid.getBoundingClientRect();
const w = el.offsetWidth, h = el.offsetHeight;
const nx = Math.max(0, Math.min(gridRect.width - w, e.clientX - gridRect.left - drag.grabX));
const ny = Math.max(0, Math.min(gridRect.height - h, e.clientY - gridRect.top - drag.grabY));
moved.x = Math.round(nx / SNAP) * SNAP;
moved.y = Math.round(ny / SNAP) * SNAP;
if (targetSec !== drag.sec) {
state.sections[drag.sec].splice(drag.i, 1);
state.sections.q.push(moved);
}
} else {
// RIGHT drop — grid-ordered insert by Y midpoint of siblings.
const siblings = [...targetGrid.querySelectorAll(".card")];
let insertAt = siblings.length;
for (let k = 0; k < siblings.length; k++) {
const r = siblings[k].getBoundingClientRect();
if (e.clientY < r.top + r.height / 2) { insertAt = k; break; }
}
const [m] = state.sections[drag.sec].splice(drag.i, 1);
if (targetSec === drag.sec && insertAt > drag.i) insertAt--;
// Moving a free-positioned icon into the right column drops its
// x,y — they'd just be noise in the card list.
delete m.x; delete m.y;
state.sections.sm.splice(insertAt, 0, m);
}
persist(); render();
}
document.querySelectorAll(".drop-target").forEach((g) => g.classList.remove("drop-target"));
document.body.classList.remove("dragging-any");
el.classList.remove("dragging");
drag = null;
});
// add/edit/delete
let editKey = null;
function openModal(sec, i) {
editKey = { sec, i };
const isNew = i < 0;
$("modalTitle").textContent = isNew ? "Add link" : "Edit link";
const c = isNew ? { title: "", url: "", sub: "", badge: "", color: "#d6ff3d", letter: "" }
: state.sections[sec][i];
$("mTitle").value = c.title || "";
$("mUrl").value = c.url || "";
$("mSub").value = c.sub || "";
$("mBadge").value = c.badge || "";
$("mSection").value = sec;
$("mColor").value = c.color || "#d6ff3d";
$("mLetter").value = c.letter || "";
$("modal").classList.add("on");
setTimeout(() => $("mTitle").focus(), 20);
}
function closeModal() { $("modal").classList.remove("on"); }
function removeIcon(sec, i) {
if (!confirm(`Remove "${state.sections[sec][i].title}" from the desktop?`)) return;
state.sections[sec].splice(i, 1); persist(); render();
}
$("mCancel").onclick = closeModal;
$("modal").addEventListener("click", (e) => { if (e.target.id === "modal") closeModal(); });
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && $("modal").classList.contains("on")) closeModal(); });
$("mSave").onclick = () => {
const title = $("mTitle").value.trim();
let url = $("mUrl").value.trim();
if (!title || !url) { alert("Title and URL are required."); return; }
if (!/^[a-z]+:\/\//i.test(url) && !/^[a-z]+:/.test(url)) url = "https://" + url;
const next = {
title, url,
sub: $("mSub").value.trim() || null,
badge: $("mBadge").value.trim() || null,
color: $("mColor").value,
letter: $("mLetter").value.trim() || null,
};
const toSec = $("mSection").value;
if (!editKey || editKey.i < 0) state.sections[toSec].push(next);
else {
const prev = state.sections[editKey.sec][editKey.i];
const merged = { ...prev, ...next, brand: prev.brand || null };
if (toSec === editKey.sec) state.sections[toSec][editKey.i] = merged;
else { state.sections[editKey.sec].splice(editKey.i, 1); state.sections[toSec].push(merged); }
}
persist(); render(); closeModal();
};
// edit toggle + toolbar
$("editBtn").onclick = () => {
const editing = document.body.classList.toggle("editing");
$("editBtn").classList.toggle("on", editing);
$("editBtn").textContent = editing ? "Done" : "Edit desktop";
};
$("addBtn").onclick = () => openModal("q", -1);
$("resetBtn").onclick = async () => {
if (!confirm("Restore the default desktop? Your added links will be removed.")) return;
if (window.home && window.home.resetCards) await window.home.resetCards();
state = defaultState();
applyBg(); applySizeButtons(); render();
persist();
};
document.querySelectorAll(".editbar .sz button").forEach((btn) => {
btn.onclick = () => {
state.quickSize = btn.dataset.size;
document.querySelectorAll(".editbar .sz button").forEach((b) => b.classList.toggle("on", b === btn));
persist(); render();
};
});
document.querySelectorAll(".editbar .bg button[data-bg]").forEach((btn) => {
btn.onclick = () => { state.background = { kind: "preset", value: btn.dataset.bg }; applyBg(); persist(); };
});
$("bgColor").oninput = () => { state.background = { kind: "color", value: $("bgColor").value }; applyBg(); persist(); };
$("bgImage").onchange = (e) => {
const f = e.target.files && e.target.files[0]; if (!f) return;
if (f.size > 2_000_000) { alert("Please pick an image under 2 MB."); return; }
const r = new FileReader();
r.onload = () => { state.background = { kind: "image", dataUrl: r.result }; applyBg(); persist(); };
r.readAsDataURL(f);
};
// Search form — routes through main.js (user's default engine + locale).
$("searchForm").addEventListener("submit", (e) => {
e.preventDefault();
const q = $("q").value.trim(); if (!q) return;
if (window.home && window.home.navigate) window.home.navigate(q);
else window.location.href = "https://www.startpage.com/do/search?q=" + encodeURIComponent(q);
});
if (window.home && window.home.getEngines) {
window.home.getEngines().then((r) => {
const name = r && r.engines && r.engines.find((x) => x.id === r.current)?.name;
if (name) $("q").placeholder = "Search the web with " + name;
}).catch(() => {});
}
function applySizeButtons() {
document.querySelectorAll(".editbar .sz button").forEach((b) => b.classList.toggle("on", b.dataset.size === state.quickSize));
}
// Initial load
(async () => {
let loaded = null;
try { if (window.home && window.home.getCards) loaded = await window.home.getCards(); } catch {}
state = ensureShape(loaded) || defaultState();
applyBg(); applySizeButtons(); render();
if (window.home && window.home.onCards) {
window.home.onCards((data) => {
const next = ensureShape(data);
if (next) { state = next; applyBg(); applySizeButtons(); render(); }
});
}
})();
</script>
</body>
</html>