Pulls the Theseus Navigator hero (compass + title + search + .bch tip) back to the top so identity + search stay the first thing on the page. Underneath, the two-column desktop keeps the shape from 0.3.91 but drops the second search bar: a narrow left column of stackable slot tiles (hello.bch, sia.coin, Pithos.sia, Bergamot.X, + add) and a 3-column grid of nine Silent Mode cards on the right. Right-click opens context menus — background edits (change bg, icon size, add link/card, reset) on empty space, and copy image / copy link / open / edit / remove on any icon or card. Copy image renders the brand mark to a 256px PNG and writes it with navigator.clipboard.write. Slot order is HTML5 drag-and-drop between either column; schema is v5 and passes through migrateCards unchanged.
734 lines
40 KiB
HTML
734 lines
40 KiB
HTML
<!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 { background: radial-gradient(1200px 600px at 50% -10%, var(--bg1), var(--bg2)); }
|
|
|
|
/* Hero (identity + search). Centered at the top of the page — the user's
|
|
most-used thing on this screen. The slot/card desktop lives beneath. */
|
|
.hero { text-align: center; padding: 5vh 1rem 0; max-width: 760px; margin: 0 auto; }
|
|
.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; }
|
|
|
|
/* Two-column desktop under the hero: narrow LEFT column of stackable icon
|
|
slots, wider RIGHT column with a 3-column grid of Silent Mode cards. */
|
|
.homepage { display: grid; grid-template-columns: minmax(96px, 110px) minmax(380px, 1fr);
|
|
gap: 36px; max-width: 1180px; margin: 2.4rem auto 0; padding: 0 32px 48px; align-items: start; }
|
|
@media (max-width: 760px) { .homepage { grid-template-columns: 1fr; gap: 20px; padding: 0 14px 32px; } }
|
|
|
|
/* LEFT COLUMN — vertical stack of slots + an "add" button underneath */
|
|
.left-column { display: flex; flex-direction: column; align-items: center; gap: 14px; }
|
|
.slots-list { display: flex; flex-direction: column; gap: 14px; align-items: center; width: 100%; }
|
|
|
|
.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); }
|
|
.slot .tile { display: flex; align-items: center; justify-content: center;
|
|
border-radius: 16px; 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; }
|
|
.slot .tile svg, .slot .tile img { width: 72%; height: 72%; display: block; }
|
|
.slot .label { margin-top: 6px; font-size: 11.5px; line-height: 1.2; color: #fff;
|
|
text-shadow: 0 1px 3px #000c, 0 0 10px #000a; max-width: 100px;
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
padding: 2px 6px; border-radius: 4px; }
|
|
body.light .slot .label { color: #0b0e14; text-shadow: 0 1px 2px #fffb, 0 0 10px #fffc; }
|
|
.slot.size-small .tile { width: 56px; height: 56px; font-size: 22px; border-radius: 12px; }
|
|
.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-above::before, .slot.drag-below::after {
|
|
content: ""; position: absolute; left: 50%; transform: translateX(-50%);
|
|
width: 60%; height: 2px; background: var(--acid); border-radius: 2px;
|
|
box-shadow: 0 0 8px var(--acid);
|
|
}
|
|
.slot.drag-above::before { top: -8px; }
|
|
.slot.drag-below::after { bottom: -8px; }
|
|
|
|
.slot-add { display: flex; align-items: center; justify-content: center;
|
|
width: 56px; height: 56px; 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; }
|
|
.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. */
|
|
.right-column { display: flex; flex-direction: column; gap: 22px; min-width: 0; }
|
|
.cards-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; align-content: start; }
|
|
@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;
|
|
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; }
|
|
.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-title { font-size: 14px; font-weight: 600; color: var(--ink); text-align: center;
|
|
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.dragging { opacity: .6; }
|
|
.card.drag-before::before, .card.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);
|
|
}
|
|
.card.drag-before::before { left: -8px; }
|
|
.card.drag-after::after { right: -8px; }
|
|
.card-add { display: flex; align-items: center; justify-content: center; min-height: 128px;
|
|
border: 2px dashed var(--line2); border-radius: 14px; background: transparent;
|
|
color: var(--mut); font-size: 13.5px; cursor: pointer;
|
|
transition: color .12s, border-color .12s, background .12s; }
|
|
.card-add:hover { color: var(--acid); border-color: var(--acid); background: rgb(from var(--acid) r g b / .04); }
|
|
|
|
/* Context menus. One for the background (edit actions), one for images
|
|
(copy / edit / remove). Positioned at the pointer by JS. */
|
|
.ctx-menu { position: fixed; display: none; z-index: 1000; min-width: 200px;
|
|
background: rgba(20,26,36,.96); border: 1px solid var(--line2); border-radius: 10px;
|
|
padding: 6px; box-shadow: 0 16px 40px #000a; backdrop-filter: blur(10px); }
|
|
.ctx-menu.on { display: block; }
|
|
.ctx-menu button, .ctx-menu .submenu-trigger {
|
|
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
|
width: 100%; text-align: left; background: transparent; border: 0;
|
|
color: var(--ink); padding: 8px 12px; border-radius: 6px; cursor: pointer;
|
|
font: inherit; font-size: 13px; }
|
|
.ctx-menu button:hover, .ctx-menu .submenu-trigger:hover { background: var(--card-hi); }
|
|
.ctx-menu button.danger { color: var(--rose); }
|
|
.ctx-menu hr { border: 0; border-top: 1px solid var(--line); margin: 4px 2px; }
|
|
.ctx-menu .caret { color: var(--dim); font-size: 11px; }
|
|
.ctx-menu .value { color: var(--mut); font-size: 12px; }
|
|
|
|
/* Background chooser modal (opened from the context menu) */
|
|
.modal-bg { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 1100;
|
|
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); }
|
|
|
|
.bg-preset-row { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; margin-top: 6px; }
|
|
.bg-preset-row button { height: 44px; border-radius: 8px; border: 1px solid var(--line2); cursor: pointer; padding: 0; }
|
|
.bg-preset-row button.on { outline: 2px solid var(--acid); outline-offset: 1px; }
|
|
.bg-color-row { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
|
|
.bg-color-row input[type=color] { width: 44px; height: 32px; border: 1px solid var(--line2); border-radius: 6px; background: transparent; padding: 0; cursor: pointer; }
|
|
.bg-file { font-size: 12.5px; color: var(--mut); border: 1px dashed var(--line2); border-radius: 6px; padding: 6px 12px; cursor: pointer; }
|
|
.bg-file:hover { color: var(--acid); border-color: var(--acid); }
|
|
|
|
.toast { position: fixed; left: 50%; bottom: 32px; transform: translateX(-50%) translateY(20px);
|
|
background: rgba(20,26,36,.96); color: var(--ink); padding: 10px 18px; border-radius: 10px;
|
|
border: 1px solid var(--line2); font-size: 13px; opacity: 0; pointer-events: none;
|
|
transition: opacity .18s, transform .18s; z-index: 1200; }
|
|
.toast.on { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
|
|
@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; }
|
|
.slot .label { color: #0b0e14; text-shadow: 0 1px 2px #fffb, 0 0 10px #fffc; }
|
|
.modal, .ctx-menu { background: #ffffff; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<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>
|
|
|
|
<div class="homepage">
|
|
<div class="left-column">
|
|
<div class="slots-list" id="slotsList"></div>
|
|
<button class="slot-add" id="slotAdd" title="Add link">+</button>
|
|
</div>
|
|
<div class="right-column">
|
|
<div class="cards-grid" id="cardsGrid"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ctx-menu" id="ctxBg">
|
|
<button data-action="change-bg">Change background…</button>
|
|
<button class="submenu-trigger" data-action="icon-size">
|
|
<span>Icon size</span><span class="value" id="ctxIconSizeVal">medium</span>
|
|
</button>
|
|
<hr>
|
|
<button data-action="add-link">Add link (left column)</button>
|
|
<button data-action="add-card">Add card (Silent Mode)</button>
|
|
<hr>
|
|
<button class="danger" data-action="reset">Reset to defaults</button>
|
|
</div>
|
|
|
|
<div class="ctx-menu" id="ctxItem">
|
|
<button data-action="open">Open</button>
|
|
<button data-action="open-new">Open in new tab</button>
|
|
<hr>
|
|
<button data-action="copy-image">Copy image</button>
|
|
<button data-action="copy-url">Copy link address</button>
|
|
<hr>
|
|
<button data-action="edit">Edit…</button>
|
|
<button class="danger" data-action="remove">Remove</button>
|
|
</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/">
|
|
<div class="row">
|
|
<div>
|
|
<label>Column</label>
|
|
<select id="mSection">
|
|
<option value="q">Left column (slots)</option>
|
|
<option value="sm">Silent Mode (3-column grid)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>Icon color (fallback)</label>
|
|
<input id="mColor" type="color" value="#d6ff3d">
|
|
</div>
|
|
</div>
|
|
<label>Letter / emoji (optional)</label>
|
|
<input id="mLetter" placeholder="first letter is used by default" maxlength="4">
|
|
<div class="btns"><button id="mCancel" type="button">Cancel</button><button id="mSave" class="primary" type="button">Save</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-bg" id="bgModal">
|
|
<div class="modal">
|
|
<h2>Background</h2>
|
|
<label>Preset</label>
|
|
<div class="bg-preset-row">
|
|
<button data-bg="dark" 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>
|
|
<label>Custom color</label>
|
|
<div class="bg-color-row">
|
|
<input id="bgColor" type="color" value="#0b0e14">
|
|
<label class="bg-file">Upload image<input id="bgImage" type="file" accept="image/*" style="display:none"></label>
|
|
</div>
|
|
<div class="btns"><button id="bgClose" class="primary" type="button">Done</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
const $ = (id) => document.getElementById(id);
|
|
const esc = (s) => String(s || "").replace(/</g, "<").replace(/"/g, """);
|
|
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>';
|
|
}
|
|
function toast(msg) {
|
|
const t = $("toast"); t.textContent = msg; t.classList.add("on");
|
|
clearTimeout(toast._h); toast._h = setTimeout(() => t.classList.remove("on"), 1800);
|
|
}
|
|
|
|
// 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>`,
|
|
};
|
|
|
|
// v5 schema. Dropped the per-item x,y that v4 used for free-drag — left
|
|
// column is a vertical stack now, order within the array is the order on
|
|
// screen. Legacy x,y values are ignored by render() but preserved in
|
|
// storage (migrateCards treats items as opaque).
|
|
let state = null;
|
|
function defaultState() {
|
|
return {
|
|
version: 5,
|
|
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" },
|
|
],
|
|
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" },
|
|
],
|
|
},
|
|
};
|
|
}
|
|
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 = "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("#bgModal .bg-preset-row 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() {
|
|
const q = state.sections.q || [];
|
|
const list = $("slotsList");
|
|
list.innerHTML = "";
|
|
q.forEach((ic, i) => {
|
|
const el = document.createElement("a");
|
|
el.className = `slot size-${state.iconSize}`;
|
|
el.href = ic.url; el.dataset.sec = "q"; el.dataset.i = i;
|
|
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="tile">${iconArt(ic)}</div><div class="label">${esc(ic.title || ic.url)}</div>`;
|
|
wireItem(el, "q", i);
|
|
list.appendChild(el);
|
|
});
|
|
|
|
const sm = state.sections.sm || [];
|
|
const grid = $("cardsGrid");
|
|
grid.innerHTML = "";
|
|
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");
|
|
}
|
|
el.innerHTML = `<div class="card-ico">${iconArt(ic)}</div><div class="card-title">${styledTitle(ic.title)}</div>`;
|
|
wireItem(el, "sm", i);
|
|
grid.appendChild(el);
|
|
});
|
|
const add = document.createElement("button");
|
|
add.className = "card-add"; add.textContent = "+ Add card";
|
|
add.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openModal("sm", -1); };
|
|
add.oncontextmenu = (e) => e.preventDefault();
|
|
grid.appendChild(add);
|
|
|
|
$("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;
|
|
el.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
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, sec, i);
|
|
});
|
|
el.addEventListener("dragstart", (e) => {
|
|
e.dataTransfer.effectAllowed = "move";
|
|
e.dataTransfer.setData("application/x-slot", JSON.stringify({ sec, i }));
|
|
el.classList.add("dragging");
|
|
hideAllCtx();
|
|
});
|
|
el.addEventListener("dragend", () => {
|
|
el.classList.remove("dragging");
|
|
document.querySelectorAll(".drag-above,.drag-below,.drag-before,.drag-after")
|
|
.forEach((n) => n.classList.remove("drag-above", "drag-below", "drag-before", "drag-after"));
|
|
});
|
|
el.addEventListener("dragover", (e) => {
|
|
e.preventDefault(); e.dataTransfer.dropEffect = "move";
|
|
const r = el.getBoundingClientRect();
|
|
// Vertical mid-split for slots, horizontal mid-split for cards.
|
|
if (sec === "q") {
|
|
const above = e.clientY < r.top + r.height / 2;
|
|
el.classList.toggle("drag-above", above);
|
|
el.classList.toggle("drag-below", !above);
|
|
} else {
|
|
const before = e.clientX < r.left + r.width / 2;
|
|
el.classList.toggle("drag-before", before);
|
|
el.classList.toggle("drag-after", !before);
|
|
}
|
|
});
|
|
el.addEventListener("dragleave", () => {
|
|
el.classList.remove("drag-above", "drag-below", "drag-before", "drag-after");
|
|
});
|
|
el.addEventListener("drop", (e) => {
|
|
e.preventDefault();
|
|
const payload = tryParse(e.dataTransfer.getData("application/x-slot"));
|
|
if (!payload) return;
|
|
const r = el.getBoundingClientRect();
|
|
const side = (sec === "q")
|
|
? (e.clientY < r.top + r.height / 2 ? -1 : 1)
|
|
: (e.clientX < r.left + r.width / 2 ? -1 : 1);
|
|
let targetIdx = i + (side === 1 ? 1 : 0);
|
|
moveItem(payload.sec, payload.i, sec, 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.
|
|
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);
|
|
});
|
|
}
|
|
function tryParse(s) { try { return JSON.parse(s); } catch { return null; } }
|
|
function moveItem(fromSec, fromI, toSec, toIdx) {
|
|
const [m] = state.sections[fromSec].splice(fromI, 1);
|
|
if (fromSec === toSec && toIdx > fromI) toIdx--;
|
|
toIdx = Math.max(0, Math.min(state.sections[toSec].length, toIdx));
|
|
state.sections[toSec].splice(toIdx, 0, m);
|
|
persist(); render();
|
|
}
|
|
|
|
// Context menus. Right-click on an empty area shows the background menu;
|
|
// right-click on an icon/card shows the item menu (copy image, edit, …).
|
|
let ctxTarget = null;
|
|
function hideAllCtx() { $("ctxBg").classList.remove("on"); $("ctxItem").classList.remove("on"); ctxTarget = null; }
|
|
function positionMenu(menu, x, y) {
|
|
menu.classList.add("on");
|
|
const r = menu.getBoundingClientRect();
|
|
const nx = Math.min(x, window.innerWidth - r.width - 8);
|
|
const ny = Math.min(y, window.innerHeight - r.height - 8);
|
|
menu.style.left = nx + "px"; menu.style.top = ny + "px";
|
|
}
|
|
function openCtxBg(x, y) {
|
|
hideAllCtx();
|
|
$("ctxIconSizeVal").textContent = state.iconSize;
|
|
positionMenu($("ctxBg"), x, y);
|
|
}
|
|
function openCtxItem(x, y, sec, i) {
|
|
hideAllCtx();
|
|
ctxTarget = { sec, i };
|
|
positionMenu($("ctxItem"), x, y);
|
|
}
|
|
document.addEventListener("contextmenu", (e) => {
|
|
// Default: background menu. Items override this with their own handler.
|
|
e.preventDefault();
|
|
openCtxBg(e.clientX, e.clientY);
|
|
});
|
|
document.addEventListener("click", (e) => {
|
|
if (!e.target.closest(".ctx-menu")) hideAllCtx();
|
|
});
|
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") { hideAllCtx(); closeModal(); closeBgModal(); } });
|
|
window.addEventListener("blur", hideAllCtx);
|
|
window.addEventListener("scroll", hideAllCtx, true);
|
|
|
|
$("ctxBg").addEventListener("click", async (e) => {
|
|
const btn = e.target.closest("button[data-action]"); if (!btn) return;
|
|
const a = btn.dataset.action;
|
|
hideAllCtx();
|
|
if (a === "change-bg") $("bgModal").classList.add("on");
|
|
else if (a === "icon-size") {
|
|
const next = state.iconSize === "small" ? "medium" : state.iconSize === "medium" ? "large" : "small";
|
|
state.iconSize = next; persist(); render();
|
|
toast("Icon size: " + next);
|
|
}
|
|
else if (a === "add-link") openModal("q", -1);
|
|
else if (a === "add-card") openModal("sm", -1);
|
|
else if (a === "reset") {
|
|
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(); render(); persist();
|
|
}
|
|
});
|
|
|
|
$("ctxItem").addEventListener("click", async (e) => {
|
|
const btn = e.target.closest("button[data-action]"); if (!btn) return;
|
|
if (!ctxTarget) { hideAllCtx(); return; }
|
|
const { sec, i } = ctxTarget;
|
|
const item = state.sections[sec][i]; if (!item) { hideAllCtx(); return; }
|
|
const a = btn.dataset.action;
|
|
hideAllCtx();
|
|
if (a === "open") {
|
|
if (window.home && window.home.navigate) window.home.navigate(item.url);
|
|
else window.location.href = item.url;
|
|
} else if (a === "open-new") {
|
|
if (window.home && window.home.openInNewTab) window.home.openInNewTab(item.url);
|
|
else window.open(item.url, "_blank");
|
|
} else if (a === "copy-image") {
|
|
await copyImage(item);
|
|
} else if (a === "copy-url") {
|
|
try { await navigator.clipboard.writeText(item.url); toast("Link copied"); }
|
|
catch { toast("Copy failed"); }
|
|
} else if (a === "edit") {
|
|
openModal(sec, i);
|
|
} else if (a === "remove") {
|
|
if (!confirm(`Remove "${item.title}"?`)) return;
|
|
state.sections[sec].splice(i, 1); persist(); render();
|
|
}
|
|
});
|
|
|
|
// Render the brand mark to a PNG blob and write it to the clipboard as an
|
|
// image. SVG strings get base64-encoded for the <img> source. {src: …}
|
|
// brands are loaded straight from the bundled file.
|
|
async function copyImage(item) {
|
|
try {
|
|
const b = (item.brand && BRAND[item.brand]) || null;
|
|
let srcUrl;
|
|
if (typeof b === "string") {
|
|
srcUrl = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(b)));
|
|
} else if (b && b.src) {
|
|
srcUrl = b.src;
|
|
} else {
|
|
// Letter tile — render a 128px square with the icon color + letter.
|
|
srcUrl = letterTileToDataURL(item);
|
|
}
|
|
const png = await imageToPngBlob(srcUrl, 256);
|
|
await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
|
|
toast("Image copied");
|
|
} catch (err) {
|
|
console.error("copy image failed", err);
|
|
toast("Copy failed");
|
|
}
|
|
}
|
|
function imageToPngBlob(url, size) {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = size; canvas.height = size;
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.clearRect(0, 0, size, size);
|
|
ctx.drawImage(img, 0, 0, size, size);
|
|
canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("toBlob null")), "image/png");
|
|
};
|
|
img.onerror = (e) => reject(e);
|
|
img.src = url;
|
|
});
|
|
}
|
|
function letterTileToDataURL(item) {
|
|
const size = 256;
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = size; canvas.height = size;
|
|
const ctx = canvas.getContext("2d");
|
|
const bg = item.color || "#d6ff3d";
|
|
const fg = isLightColor(bg) ? "#0b0e14" : "#ffffff";
|
|
const r = 48;
|
|
ctx.fillStyle = bg;
|
|
ctx.beginPath();
|
|
ctx.moveTo(r, 0); ctx.lineTo(size - r, 0); ctx.quadraticCurveTo(size, 0, size, r);
|
|
ctx.lineTo(size, size - r); ctx.quadraticCurveTo(size, size, size - r, size);
|
|
ctx.lineTo(r, size); ctx.quadraticCurveTo(0, size, 0, size - r);
|
|
ctx.lineTo(0, r); ctx.quadraticCurveTo(0, 0, r, 0); ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = fg;
|
|
ctx.font = "bold 140px system-ui, sans-serif";
|
|
ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
ctx.fillText(iconLetter(item), size / 2, size / 2 + 10);
|
|
return canvas.toDataURL("image/png");
|
|
}
|
|
|
|
// Add/edit modal — the same shape works for both left slots and right cards.
|
|
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: "", color: "#d6ff3d", letter: "" }
|
|
: state.sections[sec][i];
|
|
$("mTitle").value = c.title || "";
|
|
$("mUrl").value = c.url || "";
|
|
$("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"); }
|
|
$("mCancel").onclick = closeModal;
|
|
$("modal").addEventListener("click", (e) => { if (e.target.id === "modal") 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,
|
|
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();
|
|
};
|
|
|
|
// Background chooser modal
|
|
function closeBgModal() { $("bgModal").classList.remove("on"); }
|
|
$("bgClose").onclick = closeBgModal;
|
|
$("bgModal").addEventListener("click", (e) => { if (e.target.id === "bgModal") closeBgModal(); });
|
|
document.querySelectorAll("#bgModal .bg-preset-row button").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);
|
|
};
|
|
|
|
// Slot-add button (bottom of the left column) and search form
|
|
$("slotAdd").onclick = (e) => { e.preventDefault(); openModal("q", -1); };
|
|
$("slotAdd").oncontextmenu = (e) => e.preventDefault();
|
|
$("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(() => {});
|
|
}
|
|
|
|
// Container-level drop handlers accept cross-column drops onto empty space.
|
|
wireContainer($("slotsList"), "q");
|
|
wireContainer($("cardsGrid"), "sm");
|
|
|
|
// Initial load. If the stored state has x,y on q items (v4 free-drag),
|
|
// we just ignore them — their order in the array is their order on screen.
|
|
(async () => {
|
|
let loaded = null;
|
|
try { if (window.home && window.home.getCards) loaded = await window.home.getCards(); } catch {}
|
|
state = ensureShape(loaded) || defaultState();
|
|
applyBg(); render();
|
|
if (window.home && window.home.onCards) {
|
|
window.home.onCards((data) => {
|
|
const next = ensureShape(data);
|
|
if (next) { state = next; applyBg(); render(); }
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|