style(theseus): page dialogs as notification cards, toned by what they say
The sheet now follows the notification-card pattern: a tone icon in a tinted circle, the message in bold under a "who says" caption, a tinted primary action and a quiet Cancel, close in the corner. The tone is read from the message — delete/remove/error reads as destructive (red, and the confirm button says Yes), unsaved/required/leave as a warning, saved/completed as success, anything else as plain info — since a page hands over only a sentence. Also decide "who says" from the sender's current URL rather than the tab's prov, which lags a navigation: a tab that had just left the home page for a site was still labelled Theseus.
This commit is contained in:
parent
13a4dd692d
commit
3c516b20ce
2 changed files with 130 additions and 89 deletions
|
|
@ -5,45 +5,72 @@
|
|||
<title>Dialog</title>
|
||||
<style>
|
||||
/* Same surface tokens as approval.html so page dialogs and add-on approvals
|
||||
read as one family. */
|
||||
read as one family; tones follow the notification-card pattern (icon in a
|
||||
tinted circle, tinted primary action, quiet dismiss). */
|
||||
:root { color-scheme: light dark;
|
||||
--surface:#1c222c; --surface2:#0f1621; --line:rgba(255,255,255,.12);
|
||||
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d; }
|
||||
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
|
||||
--ok:#3ddc84; --warn:#f5a524; --danger:#f6768a; --info:#8b98a9; }
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root { --surface:#ffffff; --surface2:#f1f4fa; --line:rgba(0,0,0,.12);
|
||||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --acid:#0AC18E; }
|
||||
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --acid:#0AC18E;
|
||||
--ok:#16a34a; --warn:#d97706; --danger:#dc2626; --info:#5c6577; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; background: transparent; }
|
||||
body { font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: var(--ink); }
|
||||
.promptmask { position: fixed; inset: 0; background: rgba(0,0,0,.35);
|
||||
display: grid; place-items: start center; padding-top: 0; }
|
||||
/* A sheet hanging from the toolbar, the way Chrome anchors page dialogs:
|
||||
the page stays visible and dimmed behind it. */
|
||||
.promptbox { background: var(--surface); border: 1px solid var(--line); border-top: none;
|
||||
border-radius: 0 0 12px 12px; padding: 14px 18px 14px;
|
||||
width: min(480px, calc(100vw - 32px)); max-height: calc(100vh - 24px); overflow: auto;
|
||||
box-shadow: 0 20px 60px #000b; animation: drop .12s ease-out; }
|
||||
display: grid; place-items: start center; padding-top: 14px; }
|
||||
.card { --tone: var(--info);
|
||||
position: relative; background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
|
||||
padding: 16px 16px 14px 16px; width: min(440px, calc(100vw - 32px)); max-height: calc(100vh - 28px); overflow: auto;
|
||||
box-shadow: 0 18px 50px #000b; animation: drop .14s ease-out; }
|
||||
.card.ok { --tone: var(--ok); } .card.warn { --tone: var(--warn); } .card.danger { --tone: var(--danger); }
|
||||
@keyframes drop { from { transform: translateY(-8px); opacity: 0; } to { transform: none; opacity: 1; } }
|
||||
.who { display: flex; align-items: center; gap: 7px; color: var(--dim); font-size: 11.5px; margin-bottom: 8px; min-width: 0; }
|
||||
.who .src { color: var(--mut); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.who .src.site { font: 600 12px/1.3 ui-monospace, "Cascadia Code", Consolas, monospace; }
|
||||
.body { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); font-size: 13.5px; margin: 0 0 12px; max-height: 40vh; overflow: auto; }
|
||||
.body:empty { display: none; }
|
||||
input.pin { display: block; width: 100%; margin: 0 0 12px; padding: 8px 10px; border-radius: 7px;
|
||||
.row { display: flex; gap: 12px; align-items: flex-start; }
|
||||
.icon { flex: none; width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center;
|
||||
background: rgb(from var(--tone) r g b / .16); color: var(--tone); margin-top: 1px; }
|
||||
.icon svg { width: 17px; height: 17px; }
|
||||
.text { flex: 1; min-width: 0; padding-right: 22px; }
|
||||
.who { color: var(--dim); font-size: 11.5px; margin: 0 0 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.who .src { color: var(--mut); font-weight: 600; }
|
||||
.who .src.site { font: 600 11.5px/1.3 ui-monospace, "Cascadia Code", Consolas, monospace; }
|
||||
.body { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); font-size: 13.5px; font-weight: 500; max-height: 40vh; overflow: auto; }
|
||||
.close { position: absolute; top: 10px; right: 10px; width: 26px; height: 26px; border: none; border-radius: 7px;
|
||||
background: transparent; color: var(--dim); cursor: pointer; font-size: 15px; line-height: 1; }
|
||||
.close:hover { background: var(--surface2); color: var(--ink); }
|
||||
input.pin { display: block; width: 100%; margin: 10px 0 0; padding: 8px 10px; border-radius: 8px;
|
||||
background: var(--surface2); color: var(--ink); border: 1px solid var(--line); font: inherit; font-size: 13px; }
|
||||
input.pin:focus { outline: none; border-color: rgb(from var(--acid) r g b / .6); }
|
||||
.pact { display: flex; gap: 6px; justify-content: flex-end; }
|
||||
.pbtn { padding: 7px 16px; border-radius: 7px; border: 1px solid var(--line); background: var(--surface2);
|
||||
color: var(--ink); cursor: pointer; font: inherit; font-size: 12.5px; min-width: 76px; }
|
||||
.pbtn:hover { border-color: rgb(from var(--acid) r g b / .35); }
|
||||
.pbtn.primary { background: var(--acid); color: #0b0e14; border-color: transparent; font-weight: 650; }
|
||||
.pbtn:focus-visible { outline: 2px solid rgb(from var(--acid) r g b / .6); outline-offset: 1px; }
|
||||
input.pin:focus { outline: none; border-color: rgb(from var(--tone) r g b / .7); }
|
||||
.pact { display: flex; gap: 8px; align-items: center; margin: 12px 0 0 42px; }
|
||||
.pbtn { padding: 7px 16px; border-radius: 8px; border: 1px solid transparent; cursor: pointer; font: inherit; font-size: 12.5px; font-weight: 600; min-width: 84px; }
|
||||
.pbtn.primary { background: rgb(from var(--tone) r g b / .18); color: var(--tone); border-color: rgb(from var(--tone) r g b / .35); }
|
||||
.pbtn.primary:hover { background: rgb(from var(--tone) r g b / .28); }
|
||||
.card.info .pbtn.primary { background: var(--acid); color: #0b0e14; border-color: transparent; }
|
||||
.card.info .pbtn.primary:hover { filter: brightness(1.05); }
|
||||
.pbtn.quiet { background: transparent; color: var(--mut); }
|
||||
.pbtn.quiet:hover { background: var(--surface2); color: var(--ink); }
|
||||
.pbtn:focus-visible { outline: 2px solid rgb(from var(--tone) r g b / .6); outline-offset: 1px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]);
|
||||
// The page only gives us a sentence; read its tone from the words so a
|
||||
// "Delete…?" looks destructive and a "Saved." looks like good news.
|
||||
function toneFor(msg) {
|
||||
const m = String(msg || "").toLowerCase();
|
||||
if (/\b(error|fail|failed|failure|incorrect|invalid|wrong|cannot|can't|couldn't|unable|denied|delete|remove|discard|permanently|undone|lost|reject)\b/.test(m)) return "danger";
|
||||
if (/\b(warning|caution|required|please provide|must|expire|expiring|expired|unsaved|leave|overwrite|replace)\b/.test(m)) return "warn";
|
||||
if (/\b(success|successfully|saved|completed|complete|done|created|copied|sent|installed|updated|ready)\b/.test(m)) return "ok";
|
||||
return "info";
|
||||
}
|
||||
const ICONS = {
|
||||
ok: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5 L6.5 11.5 L12.5 5"/></svg>',
|
||||
warn: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.2 L14.3 13.3 H1.7 Z"/><path d="M8 6.2 V9.6"/><circle cx="8" cy="11.8" r=".6" fill="currentColor"/></svg>',
|
||||
danger: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="8" cy="8" r="6.3"/><path d="M8 4.6 V8.8"/><circle cx="8" cy="11.3" r=".6" fill="currentColor"/></svg>',
|
||||
info: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="8" cy="8" r="6.3"/><path d="M8 7.2 V11.4"/><circle cx="8" cy="4.9" r=".6" fill="currentColor"/></svg>',
|
||||
};
|
||||
let current = null;
|
||||
function finish(ok) {
|
||||
if (!current) return;
|
||||
|
|
@ -57,20 +84,28 @@
|
|||
window.jsdialog.onShow((req) => {
|
||||
current = req;
|
||||
const who = req.who || { label: "This page", kind: "site" };
|
||||
const icon = who.kind === "addon" ? "🧩" : who.kind === "app" ? "⛓" : "🌐";
|
||||
const verb = req.kind === "prompt" ? "asks" : "says";
|
||||
const tone = toneFor(req.message);
|
||||
const okLabel = req.kind === "alert" ? "OK" : tone === "danger" && /\?\s*$/.test(String(req.message || "")) ? "Yes" : "OK";
|
||||
document.body.innerHTML =
|
||||
`<div class="promptmask"><div class="promptbox" role="${req.kind === "alert" ? "alertdialog" : "dialog"}" aria-modal="true">
|
||||
<div class="who"><span>${icon}</span><span class="src ${esc(who.kind)}" title="${esc(who.label)}">${esc(who.label)}</span><span>${verb}</span></div>
|
||||
<div class="body">${esc(req.message)}</div>
|
||||
${req.kind === "prompt" ? `<input class="pin" id="pin" type="text" value="${esc(req.def ?? "")}" spellcheck="false">` : ""}
|
||||
`<div class="promptmask"><div class="card ${tone}" role="${req.kind === "alert" ? "alertdialog" : "dialog"}" aria-modal="true">
|
||||
<button class="close" id="close" type="button" title="${req.kind === "alert" ? "OK" : "Cancel"}" aria-label="Close">✕</button>
|
||||
<div class="row">
|
||||
<div class="icon">${ICONS[tone]}</div>
|
||||
<div class="text">
|
||||
<div class="who"><span class="src ${esc(who.kind)}" title="${esc(who.label)}">${esc(who.label)}</span> ${verb}</div>
|
||||
<div class="body">${esc(req.message)}</div>
|
||||
${req.kind === "prompt" ? `<input class="pin" id="pin" type="text" value="${esc(req.def ?? "")}" spellcheck="false">` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pact">
|
||||
${req.kind === "alert" ? "" : `<button class="pbtn" type="button" id="cancel">Cancel</button>`}
|
||||
<button class="pbtn primary" type="button" id="ok">OK</button>
|
||||
<button class="pbtn primary" type="button" id="ok">${okLabel}</button>
|
||||
${req.kind === "alert" ? "" : `<button class="pbtn quiet" type="button" id="cancel">Cancel</button>`}
|
||||
</div>
|
||||
</div></div>`;
|
||||
document.getElementById("ok").addEventListener("click", () => finish(true));
|
||||
const c = document.getElementById("cancel"); if (c) c.addEventListener("click", () => finish(false));
|
||||
document.getElementById("close").addEventListener("click", () => finish(req.kind === "alert"));
|
||||
const mask = document.querySelector(".promptmask");
|
||||
mask.addEventListener("mousedown", (e) => { if (e.target === mask) finish(req.kind === "alert"); });
|
||||
const input = document.getElementById("pin");
|
||||
|
|
@ -79,7 +114,7 @@
|
|||
document.addEventListener("keydown", (e) => {
|
||||
if (!current) return;
|
||||
if (e.key === "Escape") { e.preventDefault(); finish(current.kind === "alert"); }
|
||||
else if (e.key === "Enter" && !(e.target && e.target.id === "cancel")) { e.preventDefault(); finish(true); }
|
||||
else if (e.key === "Enter" && !(e.target && (e.target.id === "cancel" || e.target.id === "close"))) { e.preventDefault(); finish(true); }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
12
main.js
12
main.js
|
|
@ -4407,9 +4407,15 @@ function jsDialogWho(sender, tab) {
|
|||
}
|
||||
} catch {}
|
||||
}
|
||||
if (tab && (tab.settings || (tab.prov && tab.prov.kind === "home"))) return { label: "Theseus", kind: "app" };
|
||||
// file: outside a tab is one of Theseus's own views; inside a tab it is a local file the user opened.
|
||||
if (/^file:/i.test(u)) return tab ? { label: "This page", kind: "site" } : { label: "Theseus", kind: "app" };
|
||||
// Theseus's own pages: every non-tab view, and the tabs that show our own
|
||||
// files (home, settings). Decided from the URL — a tab's prov lags a
|
||||
// navigation, so a tab that just left the home page would still read as
|
||||
// "home". Any other file: in a tab is a local file the user opened.
|
||||
if (/^file:/i.test(u)) {
|
||||
if (!tab) return { label: "Theseus", kind: "app" };
|
||||
const own = (() => { try { return u.startsWith(url.pathToFileURL(__dirname).href); } catch { return false; } })();
|
||||
return own || tab.settings ? { label: "Theseus", kind: "app" } : { label: "This page", kind: "site" };
|
||||
}
|
||||
try { const p = new URL(u.replace(/^bns:\/\//i, "https://")); if (p.host) return { label: p.host, kind: "site" }; } catch {}
|
||||
return { label: "This page", kind: "site" };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue