Theseus 0.3.91: home — hero back, Silent Mode cards, free-drag Your-links
Three changes to the desktop home that landed in 0.3.90: - Hero header is back. The compass mark, Theseus Navigator title, tagline and search sit centered at the top of the page, same shape as the home before the desktop redesign. The Edit button moves to the top-right corner so the hero reads as identity, not chrome. - Silent Mode on the right is CARDS again, not just tiles. Each card carries the brand icon on the left, title + one-line description stacked, and a muted acid-colored badge on the right (Navigator / Registrar / Code host / ...). Cards sit in a center-right column that stops before the right edge, giving the left column breathing room. - Your-links on the left is free-drag, OS-desktop style. Each icon stores x,y and snaps to a 20px grid; dragging an icon moves it live while the grid faintly shows underneath. The schema gains x,y on Your-links items — unchanged for Silent Mode cards. Legacy icons without x,y auto-layout in a tidy 2-column grid near the top. Compass mark replaces the chain emoji in the hero. The on-disk schema nudges to v4; v3 and v1 reads still migrate (missing x,y get an auto-layout fallback at render time).
This commit is contained in:
parent
94ec382f20
commit
8ac8b549b9
3 changed files with 339 additions and 260 deletions
584
home.html
584
home.html
|
|
@ -1,7 +1,6 @@
|
|||
<!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;
|
||||
|
|
@ -10,37 +9,38 @@
|
|||
--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); }
|
||||
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));
|
||||
}
|
||||
|
||||
/* 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); }
|
||||
/* 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; }
|
||||
|
||||
/* 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;
|
||||
.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; max-width: calc(100vw - 48px); flex-wrap: wrap; }
|
||||
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; }
|
||||
|
|
@ -57,27 +57,37 @@
|
|||
.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; }
|
||||
/* 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); }
|
||||
.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; } }
|
||||
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 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; }
|
||||
|
|
@ -94,31 +104,54 @@
|
|||
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.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; }
|
||||
|
||||
.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);
|
||||
/* 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; }
|
||||
.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; }
|
||||
.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; }
|
||||
|
||||
/* "+" 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;
|
||||
border: 2px dashed var(--line2); border-radius: 14px;
|
||||
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; }
|
||||
/* 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;
|
||||
|
|
@ -146,68 +179,57 @@
|
|||
.icon .label { color: #0b0e14; text-shadow: 0 1px 2px #fffb, 0 0 10px #fffc; }
|
||||
.modal { background: #ffffff; }
|
||||
}
|
||||
</style></head>
|
||||
|
||||
</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 class="corner">
|
||||
<button id="editBtn" type="button">Edit desktop</button>
|
||||
</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 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>
|
||||
<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>
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
|
||||
|
|
@ -226,7 +248,7 @@
|
|||
<span class="count" id="smCount"></span>
|
||||
<span class="rule"></span>
|
||||
</div>
|
||||
<div class="section-grid" id="smGrid" data-sec="sm"></div>
|
||||
<div class="cards-grid" id="smGrid" data-sec="sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -235,6 +257,7 @@
|
|||
<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>
|
||||
|
|
@ -244,14 +267,8 @@
|
|||
</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>
|
||||
<label>Badge (for cards)</label>
|
||||
<input id="mBadge" placeholder="e.g. Navigator">
|
||||
</div>
|
||||
</div>
|
||||
<label>Icon color (used when no brand mark)</label>
|
||||
|
|
@ -265,11 +282,14 @@
|
|||
<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>';
|
||||
}
|
||||
|
||||
// ---- 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.
|
||||
// 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>`,
|
||||
|
|
@ -278,74 +298,61 @@
|
|||
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.
|
||||
// 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: 3,
|
||||
version: 4,
|
||||
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" },
|
||||
{ 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" },
|
||||
{ title: "sia.coin", url: "https://sia.coin/", brand: "siacoin" },
|
||||
{ title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos" },
|
||||
{ title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot" },
|
||||
{ title: "hello.bch", url: "https://hello.bch/", brand: "hellobch", x: 10, y: 10 },
|
||||
{ title: "sia.coin", url: "https://sia.coin/", brand: "siacoin", x: 110, y: 10 },
|
||||
{ title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos", x: 10, y: 110 },
|
||||
{ title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot", x: 110, y: 110 },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
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;
|
||||
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); }
|
||||
|
||||
// ---- background ----
|
||||
const BG_PRESETS = {
|
||||
dark: "radial-gradient(1200px 600px at 50% -10%, #16202e, #0b0e14)",
|
||||
indigo: "linear-gradient(135deg,#1e2a4a 0%, #0b0e14 100%)",
|
||||
|
|
@ -361,9 +368,7 @@
|
|||
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);
|
||||
});
|
||||
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) {
|
||||
|
|
@ -373,7 +378,6 @@
|
|||
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();
|
||||
|
|
@ -390,62 +394,108 @@
|
|||
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);
|
||||
}
|
||||
// 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">✎</button>
|
||||
<button class="rm" title="Remove">×</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">✎</button>
|
||||
<button class="rm" title="Remove">×</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);
|
||||
}
|
||||
|
||||
// ---- drag + drop (within and across columns) ----
|
||||
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();
|
||||
drag = { sec, i, el };
|
||||
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);
|
||||
|
|
@ -453,46 +503,79 @@
|
|||
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"); });
|
||||
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");
|
||||
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 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][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);
|
||||
}
|
||||
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.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 ----
|
||||
// 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: "" }
|
||||
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;
|
||||
$("mSize").value = c.size || "";
|
||||
$("mColor").value = c.color || "#d6ff3d";
|
||||
$("mLetter").value = c.letter || "";
|
||||
$("modal").classList.add("on");
|
||||
|
|
@ -511,11 +594,16 @@
|
|||
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 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 {
|
||||
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;
|
||||
|
|
@ -524,7 +612,7 @@
|
|||
persist(); render(); closeModal();
|
||||
};
|
||||
|
||||
// ---- edit toggle + toolbar ----
|
||||
// edit toggle + toolbar
|
||||
$("editBtn").onclick = () => {
|
||||
const editing = document.body.classList.toggle("editing");
|
||||
$("editBtn").classList.toggle("on", editing);
|
||||
|
|
@ -538,15 +626,12 @@
|
|||
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 .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(); };
|
||||
|
|
@ -560,16 +645,13 @@
|
|||
r.readAsDataURL(f);
|
||||
};
|
||||
|
||||
// ---- search form ----
|
||||
// Submit goes through main (which applies the user's default engine + locale).
|
||||
// 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);
|
||||
});
|
||||
// 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;
|
||||
|
|
@ -578,19 +660,15 @@
|
|||
}
|
||||
|
||||
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]));
|
||||
});
|
||||
document.querySelectorAll(".editbar .sz button").forEach((b) => b.classList.toggle("on", b.dataset.size === state.quickSize));
|
||||
}
|
||||
|
||||
// ---- initial load ----
|
||||
// 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);
|
||||
|
|
|
|||
13
main.js
13
main.js
|
|
@ -862,13 +862,14 @@ const DEFAULT_HOME_STATE = {
|
|||
{ title: "Ariadne.X", url: "https://ariadne.x/", brand: "ariadne" },
|
||||
{ title: "Aegis.X", url: "https://aegis.x/", brand: "aegis" },
|
||||
],
|
||||
// Left column — user's own quick links. Seed includes the demos plus
|
||||
// Pithos.sia + Bergamot.X (tools-you-use, not apex names).
|
||||
// Left column — user's own quick links. Icons are freely positioned
|
||||
// (x,y per icon, snap-to-20 grid) so the user can rearrange them like
|
||||
// an OS desktop. Seeded with the demos plus Pithos.sia + Bergamot.X.
|
||||
q: [
|
||||
{ title: "hello.bch", url: "https://hello.bch/", brand: "hellobch" },
|
||||
{ title: "sia.coin", url: "https://sia.coin/", brand: "siacoin" },
|
||||
{ title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos" },
|
||||
{ title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot" },
|
||||
{ title: "hello.bch", url: "https://hello.bch/", brand: "hellobch", x: 10, y: 10 },
|
||||
{ title: "sia.coin", url: "https://sia.coin/", brand: "siacoin", x: 110, y: 10 },
|
||||
{ title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos", x: 10, y: 110 },
|
||||
{ title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot", x: 110, y: 110 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.90",
|
||||
"version": "0.3.91",
|
||||
"license": "MPL-2.0",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue