Theseus 0.3.92: home — hero returns above a slot column + 3-column card grid
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.
This commit is contained in:
parent
8ac8b549b9
commit
092a779a6a
2 changed files with 422 additions and 369 deletions
789
home.html
789
home.html
|
|
@ -10,14 +10,11 @@
|
||||||
--acid: #d6ff3d; --blue: #4b7bec; --sia: #b39ddb; --srv: #4fd1a5; --warm: #ffc75f; --rose: #f6768a; }
|
--acid: #d6ff3d; --blue: #4b7bec; --sia: #b39ddb; --srv: #4fd1a5; --warm: #ffc75f; --rose: #f6768a; }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
html, body { margin: 0; padding: 0; min-height: 100vh; 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 {
|
body { background: radial-gradient(1200px 600px at 50% -10%, var(--bg1), var(--bg2)); }
|
||||||
display: flex; flex-direction: column;
|
|
||||||
background: radial-gradient(1200px 600px at 50% -10%, var(--bg1), var(--bg2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero (identity + search). Centered at the top of the page, same shape
|
/* Hero (identity + search). Centered at the top of the page — the user's
|
||||||
as the pre-desktop home — this is what the user sees most often. */
|
most-used thing on this screen. The slot/card desktop lives beneath. */
|
||||||
.hero { text-align: center; padding: 5vh 1rem 0; }
|
.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 { display: inline-block; width: 64px; height: 64px; line-height: 1; }
|
||||||
.hero .mark svg { width: 100%; height: 100%; display: block; }
|
.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 { font-size: 2.4rem; margin: .3rem 0 .2rem; letter-spacing: .5px; font-weight: 600; }
|
||||||
|
|
@ -25,96 +22,68 @@
|
||||||
.hero .tag { color: var(--mut); margin: 0 0 1.4rem; }
|
.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 { 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);
|
.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;
|
background: rgba(20,26,36,.72); color: var(--ink); font-size: 15px; outline: none;
|
||||||
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
|
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
|
||||||
.hero form input:focus { border-color: var(--blue); }
|
.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 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; }
|
.hero .hint { color: var(--dim); font-size: 12.5px; margin-top: .8rem; }
|
||||||
|
|
||||||
.corner { position: absolute; top: 18px; right: 18px; z-index: 10; }
|
/* Two-column desktop under the hero: narrow LEFT column of stackable icon
|
||||||
.corner button { background: rgba(20,26,36,.6); border: 1px solid var(--line2); color: var(--mut);
|
slots, wider RIGHT column with a 3-column grid of Silent Mode cards. */
|
||||||
border-radius: 8px; padding: 8px 14px; font-size: 12.5px; cursor: pointer; backdrop-filter: blur(6px); }
|
.homepage { display: grid; grid-template-columns: minmax(96px, 110px) minmax(380px, 1fr);
|
||||||
.corner button:hover { background: var(--card-hi); color: var(--ink); }
|
gap: 36px; max-width: 1180px; margin: 2.4rem auto 0; padding: 0 32px 48px; align-items: start; }
|
||||||
.corner button.on { border-color: var(--acid); color: var(--acid); background: rgb(from var(--acid) r g b / .08); }
|
@media (max-width: 760px) { .homepage { grid-template-columns: 1fr; gap: 20px; padding: 0 14px 32px; } }
|
||||||
|
|
||||||
/* Edit toolbar (shown only while editing) */
|
/* LEFT COLUMN — vertical stack of slots + an "add" button underneath */
|
||||||
.editbar { display: none; margin: 1.6rem auto .4rem; gap: 14px; align-items: center; padding: 10px 16px;
|
.left-column { display: flex; flex-direction: column; align-items: center; gap: 14px; }
|
||||||
background: rgba(20,26,36,.9); border: 1px solid var(--line2); border-radius: 14px;
|
.slots-list { display: flex; flex-direction: column; gap: 14px; align-items: center; width: 100%; }
|
||||||
backdrop-filter: blur(10px); box-shadow: 0 10px 30px #0008; width: fit-content; max-width: calc(100vw - 48px); flex-wrap: wrap; }
|
|
||||||
body.editing .editbar { display: flex; }
|
|
||||||
.editbar .group { display: flex; align-items: center; gap: 8px; }
|
|
||||||
.editbar .label { font-size: 11px; color: var(--mut); letter-spacing: .08em; text-transform: uppercase; }
|
|
||||||
.editbar .sz { display: inline-flex; border: 1px solid var(--line2); border-radius: 999px; overflow: hidden; }
|
|
||||||
.editbar .sz button { background: transparent; border: 0; color: var(--mut); padding: 6px 10px; font-size: 12px; cursor: pointer; }
|
|
||||||
.editbar .sz button.on { background: var(--acid); color: #0b0e14; font-weight: 600; }
|
|
||||||
.editbar .bg { display: inline-flex; gap: 6px; }
|
|
||||||
.editbar .bg button { width: 22px; height: 22px; border-radius: 6px; border: 1px solid var(--line2); cursor: pointer; padding: 0; }
|
|
||||||
.editbar .bg button.on { outline: 2px solid var(--acid); outline-offset: 1px; }
|
|
||||||
.editbar input[type=color] { width: 30px; height: 22px; border: 1px solid var(--line2); border-radius: 6px; background: transparent; padding: 0; cursor: pointer; }
|
|
||||||
.editbar .filebtn { font-size: 11.5px; color: var(--mut); border: 1px dashed var(--line2); border-radius: 6px; padding: 4px 10px; cursor: pointer; }
|
|
||||||
.editbar .filebtn:hover { color: var(--acid); border-color: var(--acid); }
|
|
||||||
.editbar .add { background: var(--acid); color: #0b0e14; font-weight: 600; border: none; border-radius: 8px; padding: 6px 12px; font-size: 12.5px; cursor: pointer; }
|
|
||||||
.editbar .reset { background: transparent; border: 1px solid var(--line2); color: var(--mut); border-radius: 8px; padding: 6px 12px; font-size: 12px; cursor: pointer; }
|
|
||||||
.editbar .reset:hover { color: var(--rose); border-color: var(--rose); }
|
|
||||||
|
|
||||||
/* Two-column canvas under the hero: narrow LEFT for icon tiles,
|
.slot { position: relative; display: flex; flex-direction: column; align-items: center;
|
||||||
wider RIGHT for Silent Mode cards. Not full width — centered with
|
|
||||||
breathing room on both sides. Collapses to one column under 820px. */
|
|
||||||
.desktop { position: relative; width: 100%; max-width: 1180px; margin: 2.2rem auto 3rem; padding: 0 2rem;
|
|
||||||
display: grid; grid-template-columns: minmax(180px, 220px) minmax(380px, 760px);
|
|
||||||
gap: 44px; align-items: start; justify-content: center; }
|
|
||||||
.column { min-width: 0; }
|
|
||||||
.section-hdr { display: flex; align-items: baseline; gap: 10px; margin: 4px 4px 14px;
|
|
||||||
font-size: 11px; color: var(--mut); letter-spacing: .14em; text-transform: uppercase; font-weight: 600; }
|
|
||||||
.section-hdr .acid { color: var(--acid); }
|
|
||||||
.section-hdr .count { color: var(--dim); font-weight: 400; letter-spacing: .08em; font-size: 10.5px; }
|
|
||||||
.section-hdr .rule { flex: 1; height: 1px; background: linear-gradient(to right, var(--line2), transparent); }
|
|
||||||
body.dragging-any .section-grid, body.dragging-any .cards-grid { outline: 1px dashed var(--line2); outline-offset: 4px; border-radius: 14px; }
|
|
||||||
body.dragging-any .section-grid.drop-target, body.dragging-any .cards-grid.drop-target { outline-color: var(--acid); background: rgb(from var(--acid) r g b / .04); }
|
|
||||||
@media (max-width: 820px) { .desktop { grid-template-columns: 1fr; gap: 20px; padding: 0 1rem; } }
|
|
||||||
|
|
||||||
/* LEFT column — OS-desktop style: icons live at absolute x,y positions
|
|
||||||
inside the column area so the user can drag them anywhere. The grid
|
|
||||||
is a faint 20px snap grid that appears while dragging. Minimum height
|
|
||||||
keeps the area tall enough that the first drag has somewhere to go. */
|
|
||||||
.section-grid { position: relative; padding: 8px 4px; min-height: 420px; border-radius: 10px; }
|
|
||||||
.section-grid::before {
|
|
||||||
content: ""; position: absolute; inset: 0; opacity: 0;
|
|
||||||
background-image: linear-gradient(to right, rgba(255,255,255,.04) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, rgba(255,255,255,.04) 1px, transparent 1px);
|
|
||||||
background-size: 20px 20px; pointer-events: none; transition: opacity .15s;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
body.dragging-any .section-grid::before { opacity: 1; }
|
|
||||||
.section-grid .icon { position: absolute; }
|
|
||||||
|
|
||||||
.icon { display: flex; flex-direction: column; align-items: center; position: relative;
|
|
||||||
cursor: pointer; user-select: none; text-decoration: none; color: inherit;
|
cursor: pointer; user-select: none; text-decoration: none; color: inherit;
|
||||||
transition: transform .12s ease; }
|
transition: transform .12s ease; }
|
||||||
.icon:hover { transform: translateY(-2px); }
|
.slot:hover { transform: translateY(-2px); }
|
||||||
.icon .tile { display: flex; align-items: center; justify-content: center;
|
.slot .tile { display: flex; align-items: center; justify-content: center;
|
||||||
border-radius: 18px; background: var(--iconBg, #0b0e14);
|
border-radius: 16px; background: var(--iconBg, #0b0e14);
|
||||||
color: var(--iconFg, #e7eaf1); font-weight: 700; letter-spacing: -.02em;
|
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);
|
box-shadow: 0 6px 18px #0007, inset 0 0 0 1px rgba(255,255,255,.08);
|
||||||
overflow: hidden; }
|
overflow: hidden; }
|
||||||
.icon .tile svg, .icon .tile img { width: 72%; height: 72%; display: block; }
|
.slot .tile svg, .slot .tile img { width: 72%; height: 72%; display: block; }
|
||||||
.icon .label { margin-top: 7px; font-size: 12.5px; line-height: 1.2; color: #fff;
|
.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: 100%;
|
text-shadow: 0 1px 3px #000c, 0 0 10px #000a; max-width: 100px;
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
padding: 2px 6px; border-radius: 4px; }
|
padding: 2px 6px; border-radius: 4px; }
|
||||||
body.light .icon .label { color: #0b0e14; text-shadow: 0 1px 2px #fffb, 0 0 10px #fffc; }
|
body.light .slot .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; }
|
.slot.size-small .tile { width: 56px; height: 56px; font-size: 22px; border-radius: 12px; }
|
||||||
.icon.size-medium .tile { width: 72px; height: 72px; font-size: 28px; border-radius: 16px; }
|
.slot.size-medium .tile { width: 72px; height: 72px; font-size: 28px; border-radius: 14px; }
|
||||||
.icon.size-large .tile { width: 96px; height: 96px; font-size: 36px; border-radius: 20px; }
|
.slot.size-large .tile { width: 88px; height: 88px; font-size: 34px; border-radius: 18px; }
|
||||||
.icon .label { max-width: 100px; }
|
.slot.size-small .label { font-size: 10.5px; max-width: 72px; }
|
||||||
.icon.size-small .label { max-width: 72px; font-size: 11px; margin-top: 5px; }
|
.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; }
|
||||||
|
|
||||||
/* Silent Mode CARDS — icon + title + sub + badge, one per row */
|
.slot-add { display: flex; align-items: center; justify-content: center;
|
||||||
.cards-grid { display: flex; flex-direction: column; gap: 10px; padding: 10px 0; min-height: 140px; }
|
width: 56px; height: 56px; border-radius: 12px; border: 2px dashed var(--line2);
|
||||||
.card { display: flex; align-items: center; gap: 14px; padding: 14px 18px;
|
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;
|
background: var(--card); border: 1px solid var(--line); border-radius: 14px;
|
||||||
text-decoration: none; color: inherit; cursor: pointer;
|
text-decoration: none; color: inherit; cursor: pointer; position: relative;
|
||||||
transition: transform .12s, background .12s, border-color .12s; 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: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;
|
.card .card-ico { flex: none; width: 44px; height: 44px; border-radius: 10px;
|
||||||
background: var(--iconBg, transparent);
|
background: var(--iconBg, transparent);
|
||||||
|
|
@ -122,39 +91,42 @@
|
||||||
color: var(--iconFg, #e7eaf1); font-weight: 700; font-size: 20px;
|
color: var(--iconFg, #e7eaf1); font-weight: 700; font-size: 20px;
|
||||||
overflow: hidden; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06); }
|
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-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: 14px; font-weight: 600; color: var(--ink); text-align: center;
|
||||||
.card .card-title { font-size: 15px; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
||||||
.card .card-title .tld { color: var(--acid); font-weight: 700; letter-spacing: .3px;
|
.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); }
|
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;
|
.card.dragging { opacity: .6; }
|
||||||
overflow: hidden; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; }
|
.card.drag-before::before, .card.drag-after::after {
|
||||||
.card:hover .card-sub { color: var(--ink); }
|
content: ""; position: absolute; top: 10%; bottom: 10%; width: 3px;
|
||||||
.card .card-badge { flex: none; font-size: 10.5px; padding: 2px 9px; border-radius: 999px;
|
background: var(--acid); border-radius: 2px; box-shadow: 0 0 8px var(--acid);
|
||||||
letter-spacing: .02em; background: rgb(from var(--acid) r g b / .12); color: 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); }
|
||||||
|
|
||||||
.ctrls { position: absolute; top: -8px; right: -8px; display: none; gap: 2px; z-index: 2; }
|
/* Context menus. One for the background (edit actions), one for images
|
||||||
.ctrls button { width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--line2);
|
(copy / edit / remove). Positioned at the pointer by JS. */
|
||||||
background: rgba(0,0,0,.65); color: #fff; font-size: 11px; cursor: pointer; padding: 0; line-height: 1; }
|
.ctx-menu { position: fixed; display: none; z-index: 1000; min-width: 200px;
|
||||||
.ctrls button.rm:hover { background: var(--rose); border-color: var(--rose); }
|
background: rgba(20,26,36,.96); border: 1px solid var(--line2); border-radius: 10px;
|
||||||
.ctrls button.ed:hover { background: var(--blue); border-color: var(--blue); }
|
padding: 6px; box-shadow: 0 16px 40px #000a; backdrop-filter: blur(10px); }
|
||||||
body.editing .icon .ctrls, body.editing .card .ctrls { display: flex; }
|
.ctx-menu.on { display: block; }
|
||||||
body.editing .icon, body.editing .card { cursor: grab; }
|
.ctx-menu button, .ctx-menu .submenu-trigger {
|
||||||
body.editing .icon.dragging, body.editing .card.dragging { opacity: .85; cursor: grabbing; }
|
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; }
|
||||||
|
|
||||||
.add-slot { display: none; align-items: center; justify-content: center;
|
/* Background chooser modal (opened from the context menu) */
|
||||||
border: 2px dashed var(--line2); border-radius: 14px;
|
.modal-bg { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 1100;
|
||||||
color: var(--mut); font-size: 13.5px; cursor: pointer; }
|
|
||||||
/* Left-column + slot is positioned at the bottom of the free-drag area */
|
|
||||||
.column.left .add-slot { position: absolute; left: 10px; bottom: 10px;
|
|
||||||
width: 60px; height: 60px; border-radius: 14px; font-size: 11px; }
|
|
||||||
.column.right .add-slot { width: 100%; padding: 12px; border-radius: 14px; }
|
|
||||||
.add-slot:hover { border-color: var(--acid); color: var(--acid); background: rgb(from var(--acid) r g b / .04); }
|
|
||||||
body.editing .add-slot { display: inline-flex; }
|
|
||||||
.column.right .add-slot { display: none; }
|
|
||||||
body.editing .column.right .add-slot { display: flex; }
|
|
||||||
|
|
||||||
/* Add/Edit modal */
|
|
||||||
.modal-bg { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 100;
|
|
||||||
align-items: center; justify-content: center; padding: 1rem; backdrop-filter: blur(4px); }
|
align-items: center; justify-content: center; padding: 1rem; backdrop-filter: blur(4px); }
|
||||||
.modal-bg.on { display: flex; }
|
.modal-bg.on { display: flex; }
|
||||||
.modal { background: var(--card); border: 1px solid var(--line2); border-radius: 14px;
|
.modal { background: var(--card); border: 1px solid var(--line2); border-radius: 14px;
|
||||||
|
|
@ -171,21 +143,31 @@
|
||||||
.modal button:hover { background: var(--card-hi); color: var(--ink); }
|
.modal button:hover { background: var(--card-hi); color: var(--ink); }
|
||||||
.modal button.primary { background: var(--blue); color: #fff; border-color: var(--blue); }
|
.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) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root { --bg1: #dce6f2; --bg2: #eef0f4;
|
:root { --bg1: #dce6f2; --bg2: #eef0f4;
|
||||||
--card: #ffffff; --card-hi: #f4f7fb; --line: rgba(0,0,0,.10); --line2: rgba(0,0,0,.14);
|
--card: #ffffff; --card-hi: #f4f7fb; --line: rgba(0,0,0,.10); --line2: rgba(0,0,0,.14);
|
||||||
--ink: #1a1f28; --mut: #4a5262; --dim: #8a93a2;
|
--ink: #1a1f28; --mut: #4a5262; --dim: #8a93a2;
|
||||||
--acid: #0AC18E; }
|
--acid: #0AC18E; }
|
||||||
.icon .label { color: #0b0e14; text-shadow: 0 1px 2px #fffb, 0 0 10px #fffc; }
|
.slot .label { color: #0b0e14; text-shadow: 0 1px 2px #fffb, 0 0 10px #fffc; }
|
||||||
.modal { background: #ffffff; }
|
.modal, .ctx-menu { background: #ffffff; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="corner">
|
|
||||||
<button id="editBtn" type="button">Edit desktop</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<div class="mark">
|
<div class="mark">
|
||||||
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|
@ -203,53 +185,39 @@
|
||||||
<button type="submit">Search</button>
|
<button type="submit">Search</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="hint">Tip: type a <b>.bch</b> name in the address bar above to open a decentralized site.</p>
|
<p class="hint">Tip: type a <b>.bch</b> name in the address bar above to open a decentralized site.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="editbar">
|
<div class="homepage">
|
||||||
<div class="group">
|
<div class="left-column">
|
||||||
<span class="label">Icon size</span>
|
<div class="slots-list" id="slotsList"></div>
|
||||||
<div class="sz">
|
<button class="slot-add" id="slotAdd" title="Add link">+</button>
|
||||||
<button data-size="small">S</button>
|
</div>
|
||||||
<button data-size="medium" class="on">M</button>
|
<div class="right-column">
|
||||||
<button data-size="large">L</button>
|
<div class="cards-grid" id="cardsGrid"></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>
|
|
||||||
<input id="bgColor" type="color" value="#0b0e14" title="Custom color">
|
|
||||||
<label class="filebtn">Upload image<input id="bgImage" type="file" accept="image/*" style="display:none"></label>
|
|
||||||
</div>
|
|
||||||
<div class="group">
|
|
||||||
<button class="add" id="addBtn">+ Add link</button>
|
|
||||||
<button class="reset" id="resetBtn" title="Restore the default layout">Reset</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="desktop" id="desktop">
|
<div class="ctx-menu" id="ctxBg">
|
||||||
<div class="column left">
|
<button data-action="change-bg">Change background…</button>
|
||||||
<div class="section-hdr">
|
<button class="submenu-trigger" data-action="icon-size">
|
||||||
<span>Your links</span>
|
<span>Icon size</span><span class="value" id="ctxIconSizeVal">medium</span>
|
||||||
<span class="count" id="qCount"></span>
|
</button>
|
||||||
<span class="rule"></span>
|
<hr>
|
||||||
</div>
|
<button data-action="add-link">Add link (left column)</button>
|
||||||
<div class="section-grid" id="qGrid" data-sec="q"></div>
|
<button data-action="add-card">Add card (Silent Mode)</button>
|
||||||
</div>
|
<hr>
|
||||||
<div class="column right">
|
<button class="danger" data-action="reset">Reset to defaults</button>
|
||||||
<div class="section-hdr">
|
</div>
|
||||||
<span class="acid">Silent Mode</span>
|
|
||||||
<span class="count" id="smCount"></span>
|
<div class="ctx-menu" id="ctxItem">
|
||||||
<span class="rule"></span>
|
<button data-action="open">Open</button>
|
||||||
</div>
|
<button data-action="open-new">Open in new tab</button>
|
||||||
<div class="cards-grid" id="smGrid" data-sec="sm"></div>
|
<hr>
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
<div class="modal-bg" id="modal">
|
<div class="modal-bg" id="modal">
|
||||||
|
|
@ -257,28 +225,48 @@
|
||||||
<h2 id="modalTitle">Add link</h2>
|
<h2 id="modalTitle">Add link</h2>
|
||||||
<label>Title</label><input id="mTitle" placeholder="e.g. hello.bch">
|
<label>Title</label><input id="mTitle" placeholder="e.g. hello.bch">
|
||||||
<label>URL</label><input id="mUrl" placeholder="https://example.com/">
|
<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 class="row">
|
||||||
<div>
|
<div>
|
||||||
<label>Column</label>
|
<label>Column</label>
|
||||||
<select id="mSection">
|
<select id="mSection">
|
||||||
<option value="q">Your links (left)</option>
|
<option value="q">Left column (slots)</option>
|
||||||
<option value="sm">Silent Mode (right)</option>
|
<option value="sm">Silent Mode (3-column grid)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Badge (for cards)</label>
|
<label>Icon color (fallback)</label>
|
||||||
<input id="mBadge" placeholder="e.g. Navigator">
|
<input id="mColor" type="color" value="#d6ff3d">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label>Icon color (used when no brand mark)</label>
|
<label>Letter / emoji (optional)</label>
|
||||||
<input id="mColor" type="color" value="#d6ff3d" style="width:60px;height:30px;padding:0;cursor:pointer">
|
<input id="mLetter" placeholder="first letter is used by default" maxlength="4">
|
||||||
<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 class="btns"><button id="mCancel" type="button">Cancel</button><button id="mSave" class="primary" type="button">Save</button></div>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const esc = (s) => String(s || "").replace(/</g, "<").replace(/"/g, """);
|
const esc = (s) => String(s || "").replace(/</g, "<").replace(/"/g, """);
|
||||||
|
|
@ -287,6 +275,10 @@
|
||||||
if (!m) return esc(t);
|
if (!m) return esc(t);
|
||||||
return esc(m[1]) + '<span class="tld">' + esc(m[2]) + '</span>';
|
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
|
// 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.
|
// under home-brand/. Fall back to a letter tile when the icon has no brand.
|
||||||
|
|
@ -306,37 +298,33 @@
|
||||||
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>`,
|
siacoin: `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="12" fill="#0b0e14"/><circle cx="32" cy="32" r="18" fill="#3ddc97"/><text x="32" y="40" font-family="system-ui" font-weight="800" font-size="22" text-anchor="middle" fill="#0b0e14">S</text></svg>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// v4 schema. Same shape as v3 (sections.sm + sections.q) with per-item
|
// v5 schema. Dropped the per-item x,y that v4 used for free-drag — left
|
||||||
// `sub` + `badge` fields on cards. Main.js's migrateCards preserves these
|
// column is a vertical stack now, order within the array is the order on
|
||||||
// fields across v3 and v4 (it treats items as opaque objects).
|
// screen. Legacy x,y values are ignored by render() but preserved in
|
||||||
|
// storage (migrateCards treats items as opaque).
|
||||||
let state = null;
|
let state = null;
|
||||||
|
|
||||||
function defaultState() {
|
function defaultState() {
|
||||||
return {
|
return {
|
||||||
version: 4,
|
version: 5,
|
||||||
iconSize: "large",
|
iconSize: "medium",
|
||||||
quickSize: "medium",
|
|
||||||
background: { kind: "preset", value: "dark" },
|
background: { kind: "preset", value: "dark" },
|
||||||
sections: {
|
sections: {
|
||||||
sm: [
|
sm: [
|
||||||
{ title: "Theseus.X", url: "https://theseus.x/", brand: "theseus", sub: "The Web Navigator — this browser's own address.", badge: "Navigator" },
|
{ title: "Theseus.X", url: "https://theseus.x/", brand: "theseus" },
|
||||||
{ title: "SilentMode.X", url: "https://silentmode.x/", brand: "silentmode", sub: "Infrastructure development for a decentralized web.", badge: "Infrastructure" },
|
{ title: "SilentMode.X", url: "https://silentmode.x/", brand: "silentmode" },
|
||||||
{ title: "Sirius.X", url: "https://sirius.x/", brand: "sirius", sub: "Register and manage BCDN names.", badge: "Registrar" },
|
{ title: "Sirius.X", url: "https://sirius.x/", brand: "sirius" },
|
||||||
{ title: "Hephaestus.X", url: "https://hephaestus.x/", brand: "hephaestus", sub: "The forge — Silent Mode's code host.", badge: "Code host" },
|
{ title: "Hephaestus.X", url: "https://hephaestus.x/", brand: "hephaestus" },
|
||||||
{ title: "Prometheus.X", url: "https://prometheus.x/", brand: "prometheus", sub: "Decentralized app marketplace.", badge: "App store" },
|
{ title: "Prometheus.X", url: "https://prometheus.x/", brand: "prometheus" },
|
||||||
{ title: "Hermes.X", url: "https://hermes.x/", brand: "hermes", sub: "Messaging — end-to-end encrypted over Nostr.", badge: "Messaging" },
|
{ title: "Hermes.X", url: "https://hermes.x/", brand: "hermes" },
|
||||||
{ title: "Helios.X", url: "https://helios.x/", brand: "helios", sub: "Search engine for the decentralized web (in design).", badge: "Search" },
|
{ title: "Helios.X", url: "https://helios.x/", brand: "helios" },
|
||||||
{ title: "Ariadne.X", url: "https://ariadne.x/", brand: "ariadne", sub: "BNS resolver — read decentralized names natively.", badge: "Resolver" },
|
{ title: "Ariadne.X", url: "https://ariadne.x/", brand: "ariadne" },
|
||||||
{ title: "Aegis.X", url: "https://aegis.x/", brand: "aegis", sub: "Multi-chain wallet, derived from your Theseus vault.", badge: "Wallet" },
|
{ title: "Aegis.X", url: "https://aegis.x/", brand: "aegis" },
|
||||||
],
|
],
|
||||||
// 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: [
|
q: [
|
||||||
{ title: "hello.bch", url: "https://hello.bch/", brand: "hellobch", x: 10, y: 10 },
|
{ title: "hello.bch", url: "https://hello.bch/", brand: "hellobch" },
|
||||||
{ title: "sia.coin", url: "https://sia.coin/", brand: "siacoin", x: 110, y: 10 },
|
{ title: "sia.coin", url: "https://sia.coin/", brand: "siacoin" },
|
||||||
{ title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos", x: 10, y: 110 },
|
{ title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos" },
|
||||||
{ title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot", x: 110, y: 110 },
|
{ title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -347,8 +335,7 @@
|
||||||
if (!Array.isArray(s.sections.sm)) s.sections.sm = [];
|
if (!Array.isArray(s.sections.sm)) s.sections.sm = [];
|
||||||
if (!Array.isArray(s.sections.q)) s.sections.q = [];
|
if (!Array.isArray(s.sections.q)) s.sections.q = [];
|
||||||
if (!s.background) s.background = { kind: "preset", value: "dark" };
|
if (!s.background) s.background = { kind: "preset", value: "dark" };
|
||||||
if (!s.iconSize) s.iconSize = "large";
|
if (!s.iconSize) s.iconSize = "medium";
|
||||||
if (!s.quickSize) s.quickSize = "medium";
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
function persist() { if (window.home && window.home.setCards) window.home.setCards(state); }
|
function persist() { if (window.home && window.home.setCards) window.home.setCards(state); }
|
||||||
|
|
@ -368,7 +355,7 @@
|
||||||
else if (b.kind === "image" && b.dataUrl) document.body.style.background = `#0b0e14 center/cover no-repeat url("${b.dataUrl}")`;
|
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));
|
const isLight = (b.kind === "preset" && b.value === "light") || (b.kind === "color" && isLightColor(b.value));
|
||||||
document.body.classList.toggle("light", isLight);
|
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("#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");
|
$("bgColor").value = (b.kind === "color" ? b.value : "#0b0e14");
|
||||||
}
|
}
|
||||||
function isLightColor(hex) {
|
function isLightColor(hex) {
|
||||||
|
|
@ -394,22 +381,13 @@
|
||||||
function iconUsesBrand(icon) { return !!(icon.brand && BRAND[icon.brand]); }
|
function iconUsesBrand(icon) { return !!(icon.brand && BRAND[icon.brand]); }
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
// LEFT column — freely positioned icon tiles. x/y are stored per icon
|
|
||||||
// and default to a tidy auto-layout if a legacy icon lacks them.
|
|
||||||
const q = state.sections.q || [];
|
const q = state.sections.q || [];
|
||||||
const qGrid = $("qGrid");
|
const list = $("slotsList");
|
||||||
qGrid.innerHTML = "";
|
list.innerHTML = "";
|
||||||
$("qCount").textContent = `· ${q.length}`;
|
|
||||||
q.forEach((ic, i) => {
|
q.forEach((ic, i) => {
|
||||||
const size = ic.size || state.quickSize;
|
|
||||||
const el = document.createElement("a");
|
const el = document.createElement("a");
|
||||||
el.className = `icon size-${size}`;
|
el.className = `slot size-${state.iconSize}`;
|
||||||
el.href = ic.url; el.dataset.sec = "q"; el.dataset.i = i;
|
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)) {
|
if (!iconUsesBrand(ic)) {
|
||||||
el.style.setProperty("--iconBg", ic.color || "#d6ff3d");
|
el.style.setProperty("--iconBg", ic.color || "#d6ff3d");
|
||||||
el.style.setProperty("--iconFg", isLightColor(ic.color || "#d6ff3d") ? "#0b0e14" : "#ffffff");
|
el.style.setProperty("--iconFg", isLightColor(ic.color || "#d6ff3d") ? "#0b0e14" : "#ffffff");
|
||||||
|
|
@ -417,27 +395,14 @@
|
||||||
el.style.setProperty("--iconBg", "transparent");
|
el.style.setProperty("--iconBg", "transparent");
|
||||||
el.style.setProperty("--iconFg", "#e7eaf1");
|
el.style.setProperty("--iconFg", "#e7eaf1");
|
||||||
}
|
}
|
||||||
el.innerHTML = `
|
el.innerHTML = `<div class="tile">${iconArt(ic)}</div><div class="label">${esc(ic.title || ic.url)}</div>`;
|
||||||
<div class="ctrls">
|
wireItem(el, "q", i);
|
||||||
<button class="ed" title="Edit">✎</button>
|
list.appendChild(el);
|
||||||
<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 sm = state.sections.sm || [];
|
||||||
const smGrid = $("smGrid");
|
const grid = $("cardsGrid");
|
||||||
smGrid.innerHTML = "";
|
grid.innerHTML = "";
|
||||||
$("smCount").textContent = `· ${sm.length}`;
|
|
||||||
sm.forEach((ic, i) => {
|
sm.forEach((ic, i) => {
|
||||||
const el = document.createElement("a");
|
const el = document.createElement("a");
|
||||||
el.className = "card";
|
el.className = "card";
|
||||||
|
|
@ -446,135 +411,244 @@
|
||||||
el.style.setProperty("--iconBg", ic.color || "#d6ff3d");
|
el.style.setProperty("--iconBg", ic.color || "#d6ff3d");
|
||||||
el.style.setProperty("--iconFg", isLightColor(ic.color || "#d6ff3d") ? "#0b0e14" : "#ffffff");
|
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="card-ico">${iconArt(ic)}</div><div class="card-title">${styledTitle(ic.title)}</div>`;
|
||||||
el.innerHTML = `
|
wireItem(el, "sm", i);
|
||||||
<div class="ctrls">
|
grid.appendChild(el);
|
||||||
<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");
|
const add = document.createElement("button");
|
||||||
smAdd.className = "add-slot"; smAdd.textContent = "+ Add card to Silent Mode";
|
add.className = "card-add"; add.textContent = "+ Add card";
|
||||||
smAdd.onclick = () => openModal("sm", -1);
|
add.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openModal("sm", -1); };
|
||||||
smGrid.appendChild(smAdd);
|
add.oncontextmenu = (e) => e.preventDefault();
|
||||||
|
grid.appendChild(add);
|
||||||
|
|
||||||
|
$("ctxIconSizeVal").textContent = state.iconSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireIconClicks(el, sec, i) {
|
// 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) => {
|
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();
|
e.preventDefault();
|
||||||
const url = state.sections[sec][i].url;
|
const url = state.sections[sec][i].url;
|
||||||
if (window.home && window.home.navigate) window.home.navigate(url);
|
if (window.home && window.home.navigate) window.home.navigate(url);
|
||||||
else window.location.href = url;
|
else window.location.href = url;
|
||||||
});
|
});
|
||||||
}
|
el.addEventListener("contextmenu", (e) => {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
// Drag model: LEFT column uses free (x,y) positioning like an OS desktop —
|
openCtxItem(e.clientX, e.clientY, sec, i);
|
||||||
// snap to 20px, clamp inside the column. RIGHT column (cards) stays
|
});
|
||||||
// grid-ordered — drag inserts at the nearest Y-midpoint. Drop into the
|
el.addEventListener("dragstart", (e) => {
|
||||||
// other column re-parents (becomes free or inserted into the grid as
|
e.dataTransfer.effectAllowed = "move";
|
||||||
// appropriate).
|
e.dataTransfer.setData("application/x-slot", JSON.stringify({ sec, i }));
|
||||||
const SNAP = 20;
|
el.classList.add("dragging");
|
||||||
let drag = null;
|
hideAllCtx();
|
||||||
function startDrag(e, sec, i, el) {
|
});
|
||||||
if (!document.body.classList.contains("editing")) return;
|
el.addEventListener("dragend", () => {
|
||||||
if (e.target.closest(".ctrls")) return;
|
el.classList.remove("dragging");
|
||||||
e.preventDefault();
|
document.querySelectorAll(".drag-above,.drag-below,.drag-before,.drag-after")
|
||||||
const rect = el.getBoundingClientRect();
|
.forEach((n) => n.classList.remove("drag-above", "drag-below", "drag-before", "drag-after"));
|
||||||
drag = { sec, i, el, grabX: e.clientX - rect.left, grabY: e.clientY - rect.top };
|
});
|
||||||
el.classList.add("dragging");
|
el.addEventListener("dragover", (e) => {
|
||||||
document.body.classList.add("dragging-any");
|
e.preventDefault(); e.dataTransfer.dropEffect = "move";
|
||||||
el.setPointerCapture(e.pointerId);
|
const r = el.getBoundingClientRect();
|
||||||
}
|
// Vertical mid-split for slots, horizontal mid-split for cards.
|
||||||
window.addEventListener("pointermove", (e) => {
|
if (sec === "q") {
|
||||||
if (!drag) return;
|
const above = e.clientY < r.top + r.height / 2;
|
||||||
const target = document.elementFromPoint(e.clientX, e.clientY);
|
el.classList.toggle("drag-above", above);
|
||||||
const over = target && (target.closest(".section-grid") || target.closest(".cards-grid"));
|
el.classList.toggle("drag-below", !above);
|
||||||
document.querySelectorAll(".drop-target").forEach((g) => { if (g !== over) g.classList.remove("drop-target"); });
|
|
||||||
if (over) over.classList.add("drop-target");
|
|
||||||
// Live move when dragging within the LEFT (free-positioning) column —
|
|
||||||
// gives OS-desktop feel. Cards stay still until drop.
|
|
||||||
if (drag.sec === "q" && over && over.id === "qGrid") {
|
|
||||||
const gridRect = over.getBoundingClientRect();
|
|
||||||
const w = drag.el.offsetWidth, h = drag.el.offsetHeight;
|
|
||||||
const nx = Math.max(0, Math.min(gridRect.width - w, e.clientX - gridRect.left - drag.grabX));
|
|
||||||
const ny = Math.max(0, Math.min(gridRect.height - h, e.clientY - gridRect.top - drag.grabY));
|
|
||||||
drag.el.style.left = Math.round(nx / SNAP) * SNAP + "px";
|
|
||||||
drag.el.style.top = Math.round(ny / SNAP) * SNAP + "px";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.addEventListener("pointerup", (e) => {
|
|
||||||
if (!drag) return;
|
|
||||||
const el = drag.el;
|
|
||||||
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
||||||
const targetGrid = target && (target.closest(".section-grid") || target.closest(".cards-grid"));
|
|
||||||
const targetSec = targetGrid && targetGrid.dataset ? targetGrid.dataset.sec : null;
|
|
||||||
if (targetSec && (targetSec === "sm" || targetSec === "q")) {
|
|
||||||
const moved = state.sections[drag.sec][drag.i];
|
|
||||||
if (targetSec === "q") {
|
|
||||||
// LEFT drop — set the free x,y from where the pointer landed,
|
|
||||||
// clamped inside the grid area. From the SAME column this just
|
|
||||||
// keeps the x,y the live-move computed; from the OTHER column
|
|
||||||
// (RIGHT → LEFT) we move the item over and seed its x,y.
|
|
||||||
const gridRect = targetGrid.getBoundingClientRect();
|
|
||||||
const w = el.offsetWidth, h = el.offsetHeight;
|
|
||||||
const nx = Math.max(0, Math.min(gridRect.width - w, e.clientX - gridRect.left - drag.grabX));
|
|
||||||
const ny = Math.max(0, Math.min(gridRect.height - h, e.clientY - gridRect.top - drag.grabY));
|
|
||||||
moved.x = Math.round(nx / SNAP) * SNAP;
|
|
||||||
moved.y = Math.round(ny / SNAP) * SNAP;
|
|
||||||
if (targetSec !== drag.sec) {
|
|
||||||
state.sections[drag.sec].splice(drag.i, 1);
|
|
||||||
state.sections.q.push(moved);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// RIGHT drop — grid-ordered insert by Y midpoint of siblings.
|
const before = e.clientX < r.left + r.width / 2;
|
||||||
const siblings = [...targetGrid.querySelectorAll(".card")];
|
el.classList.toggle("drag-before", before);
|
||||||
let insertAt = siblings.length;
|
el.classList.toggle("drag-after", !before);
|
||||||
for (let k = 0; k < siblings.length; k++) {
|
|
||||||
const r = siblings[k].getBoundingClientRect();
|
|
||||||
if (e.clientY < r.top + r.height / 2) { insertAt = k; break; }
|
|
||||||
}
|
|
||||||
const [m] = state.sections[drag.sec].splice(drag.i, 1);
|
|
||||||
if (targetSec === drag.sec && insertAt > drag.i) insertAt--;
|
|
||||||
// Moving a free-positioned icon into the right column drops its
|
|
||||||
// x,y — they'd just be noise in the card list.
|
|
||||||
delete m.x; delete m.y;
|
|
||||||
state.sections.sm.splice(insertAt, 0, m);
|
|
||||||
}
|
}
|
||||||
persist(); render();
|
});
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
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
|
$("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;
|
let editKey = null;
|
||||||
function openModal(sec, i) {
|
function openModal(sec, i) {
|
||||||
editKey = { sec, i };
|
editKey = { sec, i };
|
||||||
const isNew = i < 0;
|
const isNew = i < 0;
|
||||||
$("modalTitle").textContent = isNew ? "Add link" : "Edit link";
|
$("modalTitle").textContent = isNew ? "Add link" : "Edit link";
|
||||||
const c = isNew ? { title: "", url: "", sub: "", badge: "", color: "#d6ff3d", letter: "" }
|
const c = isNew ? { title: "", url: "", color: "#d6ff3d", letter: "" }
|
||||||
: state.sections[sec][i];
|
: state.sections[sec][i];
|
||||||
$("mTitle").value = c.title || "";
|
$("mTitle").value = c.title || "";
|
||||||
$("mUrl").value = c.url || "";
|
$("mUrl").value = c.url || "";
|
||||||
$("mSub").value = c.sub || "";
|
|
||||||
$("mBadge").value = c.badge || "";
|
|
||||||
$("mSection").value = sec;
|
$("mSection").value = sec;
|
||||||
$("mColor").value = c.color || "#d6ff3d";
|
$("mColor").value = c.color || "#d6ff3d";
|
||||||
$("mLetter").value = c.letter || "";
|
$("mLetter").value = c.letter || "";
|
||||||
|
|
@ -582,13 +656,8 @@
|
||||||
setTimeout(() => $("mTitle").focus(), 20);
|
setTimeout(() => $("mTitle").focus(), 20);
|
||||||
}
|
}
|
||||||
function closeModal() { $("modal").classList.remove("on"); }
|
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;
|
$("mCancel").onclick = closeModal;
|
||||||
$("modal").addEventListener("click", (e) => { if (e.target.id === "modal") 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 = () => {
|
$("mSave").onclick = () => {
|
||||||
const title = $("mTitle").value.trim();
|
const title = $("mTitle").value.trim();
|
||||||
let url = $("mUrl").value.trim();
|
let url = $("mUrl").value.trim();
|
||||||
|
|
@ -596,8 +665,6 @@
|
||||||
if (!/^[a-z]+:\/\//i.test(url) && !/^[a-z]+:/.test(url)) url = "https://" + url;
|
if (!/^[a-z]+:\/\//i.test(url) && !/^[a-z]+:/.test(url)) url = "https://" + url;
|
||||||
const next = {
|
const next = {
|
||||||
title, url,
|
title, url,
|
||||||
sub: $("mSub").value.trim() || null,
|
|
||||||
badge: $("mBadge").value.trim() || null,
|
|
||||||
color: $("mColor").value,
|
color: $("mColor").value,
|
||||||
letter: $("mLetter").value.trim() || null,
|
letter: $("mLetter").value.trim() || null,
|
||||||
};
|
};
|
||||||
|
|
@ -612,28 +679,11 @@
|
||||||
persist(); render(); closeModal();
|
persist(); render(); closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
// edit toggle + toolbar
|
// Background chooser modal
|
||||||
$("editBtn").onclick = () => {
|
function closeBgModal() { $("bgModal").classList.remove("on"); }
|
||||||
const editing = document.body.classList.toggle("editing");
|
$("bgClose").onclick = closeBgModal;
|
||||||
$("editBtn").classList.toggle("on", editing);
|
$("bgModal").addEventListener("click", (e) => { if (e.target.id === "bgModal") closeBgModal(); });
|
||||||
$("editBtn").textContent = editing ? "Done" : "Edit desktop";
|
document.querySelectorAll("#bgModal .bg-preset-row button").forEach((btn) => {
|
||||||
};
|
|
||||||
$("addBtn").onclick = () => openModal("q", -1);
|
|
||||||
$("resetBtn").onclick = async () => {
|
|
||||||
if (!confirm("Restore the default desktop? Your added links will be removed.")) return;
|
|
||||||
if (window.home && window.home.resetCards) await window.home.resetCards();
|
|
||||||
state = defaultState();
|
|
||||||
applyBg(); applySizeButtons(); render();
|
|
||||||
persist();
|
|
||||||
};
|
|
||||||
document.querySelectorAll(".editbar .sz button").forEach((btn) => {
|
|
||||||
btn.onclick = () => {
|
|
||||||
state.quickSize = btn.dataset.size;
|
|
||||||
document.querySelectorAll(".editbar .sz button").forEach((b) => b.classList.toggle("on", b === btn));
|
|
||||||
persist(); render();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
document.querySelectorAll(".editbar .bg button[data-bg]").forEach((btn) => {
|
|
||||||
btn.onclick = () => { state.background = { kind: "preset", value: btn.dataset.bg }; applyBg(); persist(); };
|
btn.onclick = () => { state.background = { kind: "preset", value: btn.dataset.bg }; applyBg(); persist(); };
|
||||||
});
|
});
|
||||||
$("bgColor").oninput = () => { state.background = { kind: "color", value: $("bgColor").value }; applyBg(); persist(); };
|
$("bgColor").oninput = () => { state.background = { kind: "color", value: $("bgColor").value }; applyBg(); persist(); };
|
||||||
|
|
@ -645,7 +695,9 @@
|
||||||
r.readAsDataURL(f);
|
r.readAsDataURL(f);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Search form — routes through main.js (user's default engine + locale).
|
// 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) => {
|
$("searchForm").addEventListener("submit", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const q = $("q").value.trim(); if (!q) return;
|
const q = $("q").value.trim(); if (!q) return;
|
||||||
|
|
@ -659,20 +711,21 @@
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySizeButtons() {
|
// Container-level drop handlers accept cross-column drops onto empty space.
|
||||||
document.querySelectorAll(".editbar .sz button").forEach((b) => b.classList.toggle("on", b.dataset.size === state.quickSize));
|
wireContainer($("slotsList"), "q");
|
||||||
}
|
wireContainer($("cardsGrid"), "sm");
|
||||||
|
|
||||||
// Initial load
|
// 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 () => {
|
(async () => {
|
||||||
let loaded = null;
|
let loaded = null;
|
||||||
try { if (window.home && window.home.getCards) loaded = await window.home.getCards(); } catch {}
|
try { if (window.home && window.home.getCards) loaded = await window.home.getCards(); } catch {}
|
||||||
state = ensureShape(loaded) || defaultState();
|
state = ensureShape(loaded) || defaultState();
|
||||||
applyBg(); applySizeButtons(); render();
|
applyBg(); render();
|
||||||
if (window.home && window.home.onCards) {
|
if (window.home && window.home.onCards) {
|
||||||
window.home.onCards((data) => {
|
window.home.onCards((data) => {
|
||||||
const next = ensureShape(data);
|
const next = ensureShape(data);
|
||||||
if (next) { state = next; applyBg(); applySizeButtons(); render(); }
|
if (next) { state = next; applyBg(); render(); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "theseus-navigator",
|
"name": "theseus-navigator",
|
||||||
"version": "0.3.91",
|
"version": "0.3.92",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue