Repaint the userscript panel in Cinder

The panel and the web UI are the same product on the same phone, and the
old purple-on-charcoal panel read as a different application once the web
UI moved to Cinder. Same tokens, same rule that heat is typographic:
.item.hot mirrors the web UI's is-new, .item.dim mirrors is-dim, chips and
buttons become mono small-caps with hairline borders instead of pills, and
the loading spinner becomes the same burning hairline rather than a
rotating ring.

Tokens are declared on :host so the panel has one palette block to edit,
but the fonts stay system serif and mono — the web UI's webfonts cannot
follow, because an @import inside the shadow root is at the mercy of the
host site's CSP.

Structure, ids and classes are untouched: this is a restyle, and in
particular the edge tab keeps its geometry, touch-action and #hit sizing
(see docs/superpowers/specs/2026-07-28-edge-tab-hitbox-design.md).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 09:33:56 +07:00
parent e940b96eae
commit 686fcc19cc
+118 -60
View File
@@ -1173,7 +1173,10 @@
// Cover and title are the only continue links. The subtitle and the action // Cover and title are the only continue links. The subtitle and the action
// row live in the same text column but are not navigation targets — a thumb // row live in the same text column but are not navigation targets — a thumb
// that misses ★ must not land on one, and Remove must never be one. // that misses ★ must not land on one, and Remove must never be one.
return el("div", { class: "item" }, [ // hot = a chapter is out that has not been read (crimson title, ember rule);
// dim = archived (italic grey). Same two states the web UI paints.
const heat = (behind ? " hot" : "") + (statusOf(b) === "archived" ? " dim" : "");
return el("div", { class: "item" + heat }, [
el("a", { class: "go", href: cont }, [ el("a", { class: "go", href: cont }, [
b.cover b.cover
? el("img", { class: "cover", src: b.cover, loading: "lazy", alt: "" }) ? el("img", { class: "cover", src: b.cover, loading: "lazy", alt: "" })
@@ -1380,11 +1383,11 @@
<div id="backdrop"></div> <div id="backdrop"></div>
<aside id="panel" role="dialog" aria-label="Manga bookmarks"> <aside id="panel" role="dialog" aria-label="Manga bookmarks">
<header> <header>
<span>Manga Bookmarks</span> <span>manga<em>Bookmark</em></span>
<button id="closeBtn" aria-label="Close">✕</button> <button id="closeBtn" aria-label="Close">close</button>
</header> </header>
<div id="nav"> <div id="nav">
<a class="chip" href="${WEB_BASE}" target="_blank" rel="noopener">🌐 Web</a> <a class="chip" href="${WEB_BASE}" target="_blank" rel="noopener">Web</a>
<a class="chip" href="https://asurascans.com" target="_blank" rel="noopener">Asura</a> <a class="chip" href="https://asurascans.com" target="_blank" rel="noopener">Asura</a>
<a class="chip" href="https://demonicscans.org" target="_blank" rel="noopener">Demonic</a> <a class="chip" href="https://demonicscans.org" target="_blank" rel="noopener">Demonic</a>
<button id="pending" class="chip pending" hidden>⟳ 0 pending</button> <button id="pending" class="chip pending" hidden>⟳ 0 pending</button>
@@ -1392,7 +1395,7 @@
<section id="context"></section> <section id="context"></section>
<div id="tabs" role="tablist"> <div id="tabs" role="tablist">
<button id="tabAll" class="tab active" role="tab">All</button> <button id="tabAll" class="tab active" role="tab">All</button>
<button id="tabFav" class="tab" role="tab">★ Favourites</button> <button id="tabFav" class="tab" role="tab">Favourites</button>
<button id="tabArc" class="tab" role="tab">Archived</button> <button id="tabArc" class="tab" role="tab">Archived</button>
</div> </div>
<div id="list"></div> <div id="list"></div>
@@ -1401,22 +1404,35 @@
`; `;
const CSS = ` const CSS = `
:host { all: initial; } /* Cinder, ported to the panel: same tokens and the same rule that heat is
* { box-sizing: border-box; font-family: system-ui, -apple-system, sans-serif; } typographic. Fonts are system stacks, not the web UI's Google fonts — an
@import here would be at the mercy of the host page's CSP.
See docs/design-system.md. */
:host {
all: initial;
--ink: #100f0e; --ash: #161413; --dim: #0d0c0b;
--rule: #221f1d; --field-line: #2c2926; --hover: #1a1816;
--paper: #f2ece5; --paper-hot: #f0d3cb; --paper-dim: #ddd5cb;
--mute: #8d857c; --mute-2: #5a5450; --faint: #3a3733; --faint-2: #57504b;
--ember: #e0452c; --ember-wash: #1a1211; --ember-ink: #150907;
--ember-soft: #eda798; --brass: #b8912f; --trash: #6b5450;
--font-display: Georgia, "Times New Roman", serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
--font-body: system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; font-family: var(--font-body); }
#fab { #fab {
position: fixed; position: fixed;
width: 7px; height: 44px; border: none; padding: 0; width: 7px; height: 44px; border: none; padding: 0;
border-radius: 4px 0 0 4px; background: var(--ember); opacity: .5;
background: #6d28d9; opacity: .45; box-shadow: 0 2px 8px rgba(0,0,0,.5); z-index: 2147483646; cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,.4); z-index: 2147483646; cursor: pointer;
touch-action: none; user-select: none; -webkit-touch-callout: none; touch-action: none; user-select: none; -webkit-touch-callout: none;
} }
#fab.left { border-radius: 0 4px 4px 0; }
#fab:active, #fab.armed { opacity: 1; } #fab:active, #fab.armed { opacity: 1; }
#fab.armed { box-shadow: 0 0 0 2px #c4b5fd; } #fab.armed { box-shadow: 0 0 0 2px var(--ember-soft); }
#fill { #fill {
position: absolute; left: 0; right: 0; bottom: 0; height: 0; position: absolute; left: 0; right: 0; bottom: 0; height: 0;
background: #c4b5fd; pointer-events: none; border-radius: inherit; background: var(--paper); pointer-events: none;
} }
/* Invisible tap extension: the visible sliver is 7x44, this makes it 28x72. /* Invisible tap extension: the visible sliver is 7x44, this makes it 28x72.
It grows inward (toward the screen centre) because the tab is docked flush It grows inward (toward the screen centre) because the tab is docked flush
@@ -1427,90 +1443,132 @@
} }
#fab.left #hit { left: 0; right: -21px; } #fab.left #hit { left: 0; right: -21px; }
#backdrop { #backdrop {
position: fixed; inset: 0; background: rgba(0,0,0,.5); position: fixed; inset: 0; background: rgba(0,0,0,.6);
opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 2147483646; opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 2147483646;
} }
#backdrop.open { opacity: 1; pointer-events: auto; } #backdrop.open { opacity: 1; pointer-events: auto; }
#panel { #panel {
position: fixed; top: 0; right: 0; height: 100%; position: fixed; top: 0; right: 0; height: 100%;
width: min(400px, 92vw); background: #1b1b22; color: #eee; width: min(400px, 92vw); background: var(--ink); color: var(--paper-dim);
transform: translateX(105%); transition: transform .25s ease; transform: translateX(105%); transition: transform .25s ease;
z-index: 2147483647; display: flex; flex-direction: column; z-index: 2147483647; display: flex; flex-direction: column;
padding-bottom: env(safe-area-inset-bottom, 0px); padding-bottom: env(safe-area-inset-bottom, 0px);
box-shadow: -4px 0 20px rgba(0,0,0,.5); border-left: 1px solid var(--rule);
box-shadow: -4px 0 20px rgba(0,0,0,.6);
} }
#panel.open { transform: translateX(0); } #panel.open { transform: translateX(0); }
header { header {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: baseline; justify-content: space-between;
padding: 14px 16px; border-bottom: 1px solid #33333d; font-weight: 600; font-size: 16px; padding: 16px; border-bottom: 1px solid var(--rule);
font: 400 22px/1 var(--font-display); color: var(--paper);
} }
#closeBtn { background: none; border: none; color: #aaa; font-size: 18px; cursor: pointer; } header em { color: var(--ember); font-style: italic; }
#nav { display: flex; gap: 8px; padding: 10px 16px; border-bottom: 1px solid #33333d; } #closeBtn {
background: none; border: none; color: var(--mute); cursor: pointer;
font: 500 10px var(--font-mono); letter-spacing: .12em; text-transform: uppercase;
border-bottom: 1px solid var(--field-line); padding: 0 0 2px;
}
#closeBtn:active { color: var(--paper); }
#nav { display: flex; gap: 14px; padding: 11px 16px; border-bottom: 1px solid var(--rule); }
/* Chips are set as small caps rules, not pills — the sheet has no corners. */
.chip { .chip {
background: #2a2a33; color: #c4b5fd; text-decoration: none; background: none; border: none; padding: 0 0 2px;
padding: 6px 11px; border-radius: 999px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--field-line);
color: var(--mute); text-decoration: none; cursor: pointer;
font: 500 10px var(--font-mono); letter-spacing: .12em; text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
} }
.chip:active { opacity: .8; } .chip:active { color: var(--paper); border-bottom-color: var(--paper); }
.chip.pending { background: #4c1d95; border: none; cursor: pointer; } .chip.pending { color: var(--ember); border-bottom-color: var(--ember); }
.chip.pending[hidden] { display: none; } .chip.pending[hidden] { display: none; }
#context { #context {
padding: 12px 16px; border-bottom: 1px solid #33333d; padding: 14px 16px; border-bottom: 1px solid var(--rule);
display: flex; flex-direction: column; gap: 8px; display: flex; flex-direction: column; gap: 9px; align-items: flex-start;
} }
.ctx-title { font-weight: 600; font-size: 15px; } .ctx-title { font: 400 19px/1.2 var(--font-display); color: var(--paper); }
.ctx-sub { color: #9a9aa5; font-size: 13px; } .ctx-sub {
#tabs { display: flex; gap: 14px; padding: 10px 16px 0; border-bottom: 1px solid #33333d; } font: 500 10px/1.4 var(--font-mono); letter-spacing: .12em;
text-transform: uppercase; color: var(--mute);
}
#tabs { display: flex; gap: 16px; padding: 2px 16px 0; border-bottom: 1px solid var(--rule); }
.tab { .tab {
background: none; border: none; color: #9a9aa5; cursor: pointer; background: none; border: none; cursor: pointer; color: var(--mute);
font-size: 13px; font-weight: 600; padding: 4px 0 9px; font: 400 16px var(--font-display); padding: 8px 0 12px;
border-bottom: 2px solid transparent; margin-bottom: -1px; border-bottom: 2px solid transparent; margin-bottom: -1px;
} }
.tab.active { color: #eee; border-bottom-color: #6d28d9; } .tab.active { color: var(--paper); border-bottom-color: var(--paper); }
#list { overflow-y: auto; flex: 1; padding: 8px 0; } #list { overflow-y: auto; flex: 1; }
.empty { color: #9a9aa5; text-align: center; padding: 30px 16px; font-size: 14px; } .empty {
color: var(--mute); padding: 34px 16px;
font: 400 15px/1.6 var(--font-body);
}
/* Loading is the web UI's burning hairline, laid flat across the list. */
.spinner { .spinner {
width: 26px; height: 26px; margin: 0 auto; width: 100%; height: 1px; background: var(--rule); overflow: hidden; position: relative;
border: 3px solid #33333d; border-top-color: #6d28d9; border-radius: 50%;
animation: spin .8s linear infinite;
} }
@keyframes spin { to { transform: rotate(360deg); } } .spinner::after {
content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 30%;
background: var(--ember); animation: barSlide 1.15s linear infinite;
}
@keyframes barSlide { from { transform: translateX(-100%) } to { transform: translateX(320%) } }
.item { .item {
display: flex; flex-direction: row; gap: 10px; display: flex; flex-direction: row; gap: 12px;
padding: 10px 16px; border-bottom: 1px solid #2a2a33; padding: 14px 16px; border-bottom: 1px solid var(--rule);
} }
.item.dim { background: var(--dim); }
.go { display: block; flex: none; } .go { display: block; flex: none; }
.go:active, .go-t:active { opacity: .7; } .go:active, .go-t:active { opacity: .7; }
.go-t { display: block; text-decoration: none; color: inherit; } .go-t { display: block; text-decoration: none; color: inherit; }
.cover { width: 46px; height: 62px; object-fit: cover; border-radius: 4px; flex: none; background: #333; } .cover {
.cover.ph { display: block; } width: 52px; height: 70px; object-fit: cover; flex: none;
.meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 6px; } background: repeating-linear-gradient(135deg, #211d1b 0 5px, #191614 5px 10px);
.t { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.c { color: #9a9aa5; font-size: 12px; margin: 0; }
.c.behind { color: #c4b5fd; }
.actions { display: flex; gap: 6px; flex-wrap: wrap; }
.btn {
background: #33333d; color: #eee; border: none; border-radius: 6px;
padding: 8px 12px; font-size: 13px; cursor: pointer; text-decoration: none; display: inline-block;
} }
.btn:active { opacity: .8; } .cover.ph { display: block; }
.btn.primary { background: #6d28d9; color: #fff; } .item.dim .cover { filter: grayscale(1); opacity: .85; }
.btn.small { padding: 6px 10px; font-size: 12px; } .meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 7px; align-items: flex-start; }
.btn.danger { background: #7f1d1d; } .t {
.btn.star { line-height: 1; } font: 400 17px/1.2 var(--font-display); color: var(--paper-dim);
.btn.star.on { background: #6d28d9; color: #fff; } max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.item.hot .t { color: var(--paper-hot); border-bottom: 1px solid var(--ember); padding-bottom: 3px; }
.item.dim .t { font-style: italic; color: var(--mute); }
.c {
margin: 0; color: var(--mute);
font: 500 10px/1.4 var(--font-mono); letter-spacing: .12em; text-transform: uppercase;
}
.c.behind { color: var(--ember); }
.actions { display: flex; gap: 0; flex-wrap: wrap; margin-top: 2px; }
.btn {
background: none; color: var(--mute); border: 1px solid var(--rule);
margin: 0 -1px -1px 0; padding: 0 12px; height: 38px; line-height: 36px;
cursor: pointer; text-decoration: none; display: inline-block;
font: 500 10px var(--font-mono); letter-spacing: .12em; text-transform: uppercase;
}
.btn:active { color: var(--paper); background: var(--hover); }
.btn.primary {
border-color: var(--paper); background: var(--paper); color: var(--ink);
font: 400 16px var(--font-display); letter-spacing: 0; text-transform: none;
height: 44px; line-height: 42px; padding: 0 18px;
}
.btn.small { height: 34px; line-height: 32px; padding: 0 10px; }
.btn.danger { color: var(--trash); }
.btn.danger:active { color: var(--ember); background: var(--ember-wash); }
.btn.star { font-size: 13px; letter-spacing: 0; }
.btn.star.on { color: var(--brass); border-color: #332b14; }
#toasts { #toasts {
position: fixed; left: 50%; transform: translateX(-50%); position: fixed; left: 50%; transform: translateX(-50%);
bottom: calc(80px + env(safe-area-inset-bottom, 0px)); bottom: calc(80px + env(safe-area-inset-bottom, 0px));
z-index: 2147483647; display: flex; flex-direction: column; gap: 8px; align-items: center; z-index: 2147483647; display: flex; flex-direction: column; gap: 8px; align-items: center;
} }
.toast { .toast {
background: #2f2f3a; color: #fff; padding: 10px 16px; border-radius: 8px; background: var(--ash); color: var(--paper); padding: 12px 16px;
font-size: 13px; opacity: 0; transform: translateY(10px); transition: all .3s; border-left: 2px solid var(--paper);
box-shadow: 0 4px 14px rgba(0,0,0,.4); max-width: 80vw; font: 400 16px var(--font-display);
opacity: 0; transform: translateY(10px); transition: all .3s;
box-shadow: 0 4px 14px rgba(0,0,0,.5); max-width: 80vw;
} }
.toast.show { opacity: 1; transform: translateY(0); } .toast.show { opacity: 1; transform: translateY(0); }
.toast.err { background: #7f1d1d; } .toast.err { background: var(--ember-wash); border-left-color: var(--ember); color: var(--ember-soft); }
`; `;
// ============================================================ // ============================================================