The new-tab page is now a two-column desktop. The right column holds the
curated Silent Mode X-family (Theseus.X, SilentMode.X, Sirius.X,
Hephaestus.X, Prometheus.X, Hermes.X, Helios.X, Ariadne.X, Aegis.X) with
real brand marks pulled from each product's own favicon or add-on manifest.
The left column is yours — hello.bch, sia.coin, Pithos.sia, Bergamot.X by
default, plus whatever you add. Icons pick up and move within and across
columns in edit mode.
Edit mode also carries per-column default-size pickers (S / M / L / XL),
six background presets + a custom colour + an image upload, and + slots
in each column. The Ariadne.X mark uses the real face/thread illustration
in ancient-Greek-pottery colours (terracotta rings on cream). The Aegis.X
tile matches the one the add-on itself ships.
The on-disk format moves from a bare array of cards (v1) to a v3 object:
{ version:3, iconSize, quickSize, background, sections:{ sm, q } }
On first read, a v1 array lands as sections.q and the Silent Mode column
stays at the code default. The remote home-cards.json URL is still
accepted as either shape; migrateCards() normalises before use.
603 lines
35 KiB
HTML
603 lines
35 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head><meta charset="utf-8"><title>Theseus</title>
|
|
<!-- Theseus compass mark as the tab icon of the home / new-tab page (inline, so it needs no network and no file path). Same mark as theseus.x/assets/favicon.svg. -->
|
|
<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; height: 100vh; overflow: hidden; 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));
|
|
}
|
|
|
|
/* Topbar: brand+search+edit. Three-column grid keeps the search centred
|
|
even when the brand and edit button expand to different widths. */
|
|
.topbar { position: relative; z-index: 10; flex: none; padding: 24px 28px 12px;
|
|
display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 20px; }
|
|
.search { display: flex; justify-content: center; }
|
|
.search form { display: flex; gap: 8px; width: 100%; max-width: 560px; }
|
|
.search input { flex: 1; padding: 12px 20px; 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); }
|
|
.search input:focus { border-color: var(--blue); }
|
|
.search button { padding: 12px 22px; border-radius: 999px; border: none; background: var(--blue); color: #fff; font-size: 15px; cursor: pointer; }
|
|
.brandbar { display: flex; align-items: center; gap: 12px; justify-content: flex-end; }
|
|
.brandbar .brand { display: inline-flex; align-items: center; gap: 10px; color: var(--ink); font-size: 15px; white-space: nowrap; }
|
|
.brandbar .brand .mark { width: 24px; height: 24px; display: inline-block; }
|
|
.brandbar .brand .g { color: var(--acid); font-weight: 600; }
|
|
.brandbar 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); }
|
|
.brandbar button:hover { background: var(--card-hi); color: var(--ink); }
|
|
.brandbar button.on { border-color: var(--acid); color: var(--acid); background: rgb(from var(--acid) r g b / .08); }
|
|
|
|
/* Edit toolbar — floating pill, shown only while editing. */
|
|
.editbar { position: absolute; top: 76px; left: 50%; transform: translateX(-50%); z-index: 9;
|
|
display: none; 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; 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: Your links (left) + Silent Mode family (right).
|
|
Column widths roughly match the icon counts (right carries the big
|
|
curated set). Collapses to one column on narrow windows. Dragging
|
|
an icon highlights the column under the cursor as a drop target. */
|
|
.desktop { position: relative; flex: 1; overflow: auto; padding: 24px 32px 32px;
|
|
display: grid; grid-template-columns: minmax(240px, 2fr) minmax(300px, 3fr);
|
|
gap: 36px; align-items: start; }
|
|
.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); }
|
|
.section-grid { display: flex; flex-wrap: wrap; gap: 20px 24px; padding: 10px 4px; align-content: flex-start;
|
|
min-height: 140px; border-radius: 14px; }
|
|
body.dragging-any .section-grid { outline: 1px dashed var(--line2); outline-offset: 4px; }
|
|
body.dragging-any .section-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; } }
|
|
|
|
/* Icon tile. Brand marks render as inline SVG or an <img src>; user-added
|
|
entries fall back to a colored tile with the first letter. */
|
|
.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: 80px; height: 80px; font-size: 30px; border-radius: 18px; }
|
|
.icon.size-large .tile { width: 104px; height: 104px; font-size: 40px; border-radius: 22px; }
|
|
.icon.size-xl .tile { width: 136px; height: 136px; font-size: 52px; border-radius: 28px; }
|
|
.icon .label { max-width: 120px; }
|
|
.icon.size-small .label { max-width: 80px; font-size: 11px; margin-top: 5px; }
|
|
.icon.size-large .label { max-width: 140px; font-size: 13px; }
|
|
.icon.size-xl .label { max-width: 160px; font-size: 14px; }
|
|
|
|
.icon .ctrls { position: absolute; top: -8px; right: -8px; display: none; gap: 2px; z-index: 2; }
|
|
.icon .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; }
|
|
.icon .ctrls button.rm:hover { background: var(--rose); border-color: var(--rose); }
|
|
.icon .ctrls button.ed:hover { background: var(--blue); border-color: var(--blue); }
|
|
body.editing .icon .ctrls { display: flex; }
|
|
body.editing .icon { cursor: grab; }
|
|
body.editing .icon.dragging { opacity: .85; cursor: grabbing; }
|
|
|
|
/* "+" slot inside each column — only in edit mode */
|
|
.add-slot { display: none; align-items: center; justify-content: center;
|
|
border: 2px dashed var(--line2); border-radius: 18px;
|
|
color: var(--mut); font-size: 13.5px; cursor: pointer; }
|
|
.column.left .add-slot { width: 80px; height: 80px; border-radius: 16px; font-size: 12px; }
|
|
.column.right .add-slot { width: 104px; height: 104px; }
|
|
.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; }
|
|
|
|
/* 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="topbar">
|
|
<div></div>
|
|
<div class="search">
|
|
<form id="searchForm">
|
|
<input id="q" placeholder="Search the web" autofocus spellcheck="false">
|
|
<button type="submit">Search</button>
|
|
</form>
|
|
</div>
|
|
<div class="brandbar">
|
|
<div class="brand">
|
|
<span 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>
|
|
</span>
|
|
Theseus <span class="g">Navigator</span>
|
|
</div>
|
|
<button id="editBtn" type="button">Edit desktop</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="editbar">
|
|
<div class="group">
|
|
<span class="label">Family size</span>
|
|
<div class="sz" data-which="iconSize">
|
|
<button data-size="small">S</button>
|
|
<button data-size="medium">M</button>
|
|
<button data-size="large" class="on">L</button>
|
|
<button data-size="xl">XL</button>
|
|
</div>
|
|
</div>
|
|
<div class="group">
|
|
<span class="label">Your-links size</span>
|
|
<div class="sz" data-which="quickSize">
|
|
<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 two-column layout">Reset</button>
|
|
</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="section-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/">
|
|
<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>Size</label>
|
|
<select id="mSize">
|
|
<option value="">(column default)</option>
|
|
<option value="small">Small</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="large">Large</option>
|
|
<option value="xl">Extra large</option>
|
|
</select>
|
|
</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, "<").replace(/"/g, """);
|
|
|
|
// ---- Brand marks ----
|
|
// Keyed by icon.brand slug; value is an inline SVG string or an
|
|
// { src: "<relative-url>" } descriptor pointing at a bundled file.
|
|
// Entries without a brand fall back to a colored tile with a letter.
|
|
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 — real brand mark in ancient-Greek-pottery palette (terracotta
|
|
// rings around the cream figure, derived from site-ariadne-x/assets/favicon.svg).
|
|
// Bundled under home-brand/ariadne-pottery.svg and referenced relatively.
|
|
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 — straight from bundled-addons/aegis/addon.json's data: URI,
|
|
// decoded as inline SVG so the home tile matches the sidebar icon.
|
|
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>`,
|
|
};
|
|
|
|
// ---- state ----
|
|
// v3 schema: { version:3, iconSize, quickSize, background:{kind,value|dataUrl},
|
|
// sections: { q:[icon,...], sm:[icon,...] } }
|
|
// Icons: { title, url, brand?, color?, letter?, size? }
|
|
// `brand` keys into BRAND for an inline SVG or {src} descriptor
|
|
// `size` overrides the column's default (small/medium/large/xl)
|
|
// `color` + `letter` are only used when brand is absent
|
|
// The whole object is stored under userData/home-cards.json via main's
|
|
// home-cards-get / home-cards-set IPC. If the stored file is a v1 bare
|
|
// array (older schema), main migrates to v3 on first read.
|
|
let state = null;
|
|
|
|
function defaultState() {
|
|
return {
|
|
version: 3,
|
|
iconSize: "large",
|
|
quickSize: "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) {
|
|
// Keep whatever the user has, but guarantee the keys the render loop
|
|
// reads — tolerate a half-migrated or partial object without crashing.
|
|
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";
|
|
s.version = 3;
|
|
return s;
|
|
}
|
|
function persist() { if (window.home && window.home.setCards) window.home.setCards(state); }
|
|
|
|
// ---- background ----
|
|
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;
|
|
}
|
|
|
|
// ---- icon render ----
|
|
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() {
|
|
for (const sec of ["sm", "q"]) {
|
|
const grid = $(sec === "sm" ? "smGrid" : "qGrid");
|
|
const defaultSize = sec === "sm" ? state.iconSize : state.quickSize;
|
|
const list = state.sections[sec] || [];
|
|
$(sec === "sm" ? "smCount" : "qCount").textContent = `· ${list.length}`;
|
|
grid.innerHTML = "";
|
|
list.forEach((ic, i) => {
|
|
const size = ic.size || defaultSize;
|
|
const el = document.createElement("a");
|
|
el.className = `icon size-${size}`;
|
|
el.href = ic.url;
|
|
el.dataset.sec = sec; 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="ctrls">
|
|
<button class="ed" title="Edit">✎</button>
|
|
<button class="rm" title="Remove">×</button>
|
|
</div>
|
|
<div class="tile">${iconArt(ic)}</div>
|
|
<div class="label">${esc(ic.title || ic.url)}</div>`;
|
|
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();
|
|
if (window.home && window.home.navigate) window.home.navigate(ic.url);
|
|
else window.location.href = ic.url;
|
|
});
|
|
el.addEventListener("pointerdown", (e) => startDrag(e, sec, i, el));
|
|
grid.appendChild(el);
|
|
});
|
|
const add = document.createElement("button");
|
|
add.className = "add-slot";
|
|
add.textContent = "+";
|
|
add.title = sec === "q" ? "Add to Your links" : "Add to Silent Mode";
|
|
add.onclick = () => openModal(sec, -1);
|
|
grid.appendChild(add);
|
|
}
|
|
}
|
|
|
|
// ---- drag + drop (within and across columns) ----
|
|
let drag = null;
|
|
function startDrag(e, sec, i, el) {
|
|
if (!document.body.classList.contains("editing")) return;
|
|
if (e.target.closest(".ctrls")) return;
|
|
e.preventDefault();
|
|
drag = { sec, i, el };
|
|
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");
|
|
document.querySelectorAll(".section-grid.drop-target").forEach((g) => { if (g !== over) g.classList.remove("drop-target"); });
|
|
if (over) over.classList.add("drop-target");
|
|
});
|
|
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");
|
|
const targetSec = targetGrid && targetGrid.dataset ? targetGrid.dataset.sec : null;
|
|
if (targetSec && (targetSec === "sm" || targetSec === "q")) {
|
|
const siblings = [...targetGrid.querySelectorAll(".icon")];
|
|
let insertAt = siblings.length;
|
|
for (let k = 0; k < siblings.length; k++) {
|
|
const r = siblings[k].getBoundingClientRect();
|
|
if (e.clientX < r.left + r.width / 2 && e.clientY < r.bottom) { insertAt = k; break; }
|
|
}
|
|
const [moved] = state.sections[drag.sec].splice(drag.i, 1);
|
|
if (targetSec === drag.sec && insertAt > drag.i) insertAt--;
|
|
state.sections[targetSec].splice(insertAt, 0, moved);
|
|
persist(); render();
|
|
}
|
|
document.querySelectorAll(".section-grid.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: "", color: "#d6ff3d", letter: "", size: "" }
|
|
: state.sections[sec][i];
|
|
$("mTitle").value = c.title || "";
|
|
$("mUrl").value = c.url || "";
|
|
$("mSection").value = sec;
|
|
$("mSize").value = c.size || "";
|
|
$("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, color: $("mColor").value, letter: $("mLetter").value.trim() || null, size: $("mSize").value || 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").forEach((group) => {
|
|
const which = group.dataset.which; // iconSize | quickSize
|
|
group.querySelectorAll("button").forEach((btn) => {
|
|
btn.onclick = () => {
|
|
state[which] = btn.dataset.size;
|
|
group.querySelectorAll("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 ----
|
|
// Submit goes through main (which applies the 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);
|
|
});
|
|
// Reflect the current default search engine in the placeholder, so the
|
|
// input reads "Search the web with <engine>" instead of being generic.
|
|
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").forEach((group) => {
|
|
const which = group.dataset.which;
|
|
group.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b.dataset.size === state[which]));
|
|
});
|
|
}
|
|
|
|
// ---- 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();
|
|
// Main may push a fresher state (remote-cache refresh succeeded) via onCards.
|
|
if (window.home && window.home.onCards) {
|
|
window.home.onCards((data) => {
|
|
const next = ensureShape(data);
|
|
if (next) { state = next; applyBg(); applySizeButtons(); render(); }
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|