From b79d5d6bcd736c1428a3b75b93e3bb6d6e7de65b Mon Sep 17 00:00:00 2001 From: Valera Brizhatiuk <19537764+plombeer31@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:08:52 +0300 Subject: [PATCH] =?UTF-8?q?desktop:=20overlays=20in=20Soft=20Tactile=20?= =?UTF-8?q?=E2=80=94=20palette,=20sheets,=20alert,=20toasts,=20theme=20per?= =?UTF-8?q?sistence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Command palette (SH-10…13): 620px lifted panel, radius 24, blurred scrim, 56px input row with the Theme scope as a blue chip, sentence-case groups, rows of icon · bold title · context · keycaps or mono slash alias, brand-tint selection, Chats/Tasks/Skills headers and status dots under a query, the Ask-the-agent row when nothing matches, sun/moon/laptop rows with a check on the current theme. Sheets (SH-14/15): centred cards with a title, close button and footer. Switch session uses 46px list rows with the Tactile dot grammar and the state word; keyboard shortcuts is a two-column keycap grid without the prototype's Ctrl captions, and the approval row no longer advertises a grant key that does nothing. Delete alert (SH-03): 44px app mark, Cancel focused on arrival, Delete as the destructive fill. Toasts (SH-16): lifted cards with a round green check or red alert. The theme chosen in the palette or View > Appearance is saved to localStorage and re-applied before the first render. The palette field no longer gets its old text copied back after entering a scope. --- desktop/renderer/css/overlays.css | 184 +++++++++++++++++++++++------- desktop/renderer/renderer.js | 119 +++++++++++++------ 2 files changed, 221 insertions(+), 82 deletions(-) diff --git a/desktop/renderer/css/overlays.css b/desktop/renderer/css/overlays.css index 4301cf8f..9a0665e9 100644 --- a/desktop/renderer/css/overlays.css +++ b/desktop/renderer/css/overlays.css @@ -1,51 +1,147 @@ /* ============================================================ - Overlays — command palette, sheets (switch session, shortcuts), the delete alert, the page menubar menus, toasts. Screens SH-10…16. + Overlays — command palette, sheets (switch session, keyboard shortcuts), + the delete alert, toasts, and the page menubar menus. Screens SH-03, SH-10…16. Loaded after styles.css (tokens + kit). Colours come from tokens only. ============================================================ */ -/* overlays */ +/* ---- Scrims ---- + `.scrim` is shared: the context, modes and selector popovers draw it with an + inline transparent background as their click-outside catcher. So the base + rule stays a plain full-window flex layer, and the blur lives only on the + palette's own `.palscrim` and on `.sheetwrap` — a blur on bare `.scrim` + would smear the window behind every popover. */ .scrim{position:absolute;inset:0;background:var(--scrim);z-index:20;display:flex;justify-content:center} -.pal{width:640px;max-width:calc(100% - 32px);margin-top:12vh;height:max-content;max-height:70%;background:var(--bg-overlay);border-radius:0;box-shadow:var(--shadow-overlay);display:flex;flex-direction:column;overflow:hidden} -.palin{height:52px;flex:none;display:flex;align-items:center;gap:var(--s3);padding:0 var(--s5)} -.palin input{flex:1;background:none;border:none;outline:none;font-size:15px} -.palin input::placeholder{color:var(--text-tertiary)} -.palscope{height:22px;display:flex;align-items:center;gap:4px;padding:0 6px;border-radius:0;background:var(--accent-wash);color:var(--accent-text);font-size:11px;font-weight:500;flex:none} -.pallist{flex:1;min-height:0;overflow-y:auto;padding:0 6px 6px} -.palgroup{height:24px;display:flex;align-items:center;padding:0 10px;color:var(--text-secondary);position:sticky;top:0;background:var(--bg-overlay);z-index:1} -.palrow{height:40px;display:grid;grid-template-columns:20px 1fr auto 100px;gap:var(--s3);align-items:center;padding:0 10px;border-radius:0;width:100%} -.palrow .ic{color:var(--text-secondary);display:flex;justify-content:center} -.palrow .ti{font-weight:500;white-space:nowrap} -.palrow .cx{font-size:11px;color:var(--text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right} -.palrow .sc{display:flex;justify-content:flex-end;gap:2px} -.palrow.on{background:var(--accent-wash)} -.palrow.on .ti{color:var(--accent-text)} -.palrow.dim{opacity:.55} -.palfoot{height:36px;flex:none;display:flex;align-items:center;gap:var(--s3);padding:0 var(--s5);box-shadow:inset 0 1px 0 var(--line-soft);color:var(--text-secondary);font-size:11px} -.palempty{padding:var(--s5);display:flex;flex-direction:column;gap:var(--s2)} - -.menupanel{position:absolute;top:26px;background:var(--bg-overlay);border-radius:0;box-shadow:var(--shadow-overlay);padding:6px 0;min-width:260px;z-index:31} -.mrow{height:24px;display:flex;align-items:center;gap:var(--s4);padding:0 10px;width:100%;font-size:13px} -.mrow:hover:not(.dis){background:var(--accent);color:var(--on-fill)} -.mrow:hover:not(.dis) .msc,.mrow:hover:not(.dis) .mck{color:var(--on-fill);border-color:color-mix(in srgb, var(--on-fill) 40%, transparent);background:var(--glass-soft)} +.palscrim,.sheetwrap{-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)} + +/* ---- Command palette (SH-10…13) ---- */ +.pal{width:620px;max-width:calc(100% - 32px);margin-top:70px;height:max-content;max-height:min(640px, calc(100% - 100px)); + background:var(--lift);border-radius:var(--r-sheet);box-shadow:var(--sh-pop); + display:flex;flex-direction:column;overflow:hidden} +.palin{height:56px;flex:none;display:flex;align-items:center;gap:10px;padding:0 16px;border-bottom:1px solid var(--bd)} +.palin > svg{flex:none;color:var(--ink-3)} +.palin input{flex:1;min-width:0;height:100%;background:none;border:0;outline:none; + font:400 15px/1 var(--font-ui);letter-spacing:-.01em;color:var(--ink)} +.palin input::placeholder{color:var(--ink-3)} +/* Scope chip "Theme ×" — the × is the [data-popscope] hit target. */ +.palscope{flex:none;gap:3px;padding:0 3px 0 9px;font-weight:600} +.palscope-x{width:16px;height:16px;display:grid;place-items:center;border-radius:50%;cursor:pointer} +.palscope-x svg{width:11px;height:11px} +.palscope-x:hover{background:var(--brand-wash-2)} + +.pallist{flex:1;min-height:0;overflow-y:auto;padding:0 6px 6px;display:flex;flex-direction:column;gap:1px} +.pallist > .palrow:first-child,.pallist > .palempty:first-child{margin-top:6px} +.palgroup{flex:none;display:flex;align-items:center;padding:10px 10px 4px; + font:600 12px/16px var(--font-ui);color:var(--ink-2);position:sticky;top:0;background:var(--lift);z-index:1} + +/* Row: icon · title · context · shortcut (keycaps, a mono slash alias, a status dot or the current-theme check). */ +.palrow{flex:none;display:flex;align-items:center;gap:10px;min-height:36px;padding:5px 10px;width:100%; + border-radius:var(--r-well);text-align:left;color:var(--ink);font-size:13.5px;line-height:18px; + transition:background var(--t-state) var(--ease)} +.palrow:hover{background:var(--hover)} +.palrow .ic{flex:none;width:18px;display:flex;justify-content:center;color:var(--ink-2)} +.palrow .ti{flex:none;max-width:72%;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.palrow .ti b{font-weight:700} +.palrow .cx{flex:1;min-width:0;font-size:13px;font-weight:500;color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.palrow:not(.two) .cx:not(:empty){margin-left:-6px} +.palrow:not(.two) .cx:not(:empty)::before{content:"· "} +.palrow .sc{flex:none;margin-left:auto;display:flex;align-items:center;gap:3px;padding-left:8px} +.palrow .palslash{font-size:11.5px;line-height:1;color:var(--ink-2)} +.palrow .palcheck{display:flex;color:var(--blue)} +.palrow.on{background:var(--brand-wash)} +.palrow.on .ic,.palrow.on .ti,.palrow.on .palcheck{color:var(--blue)} +.palrow.dim{color:var(--ink-3)} +/* With a query the titles drop to 500, so the matched part is what reads bold. */ +.pallist.q .palrow .ti{font-weight:500} +.pallist.q .palrow .ti b{font-weight:700} +/* Two-line rows: chats, tasks and skills that match a query, and scope rows + (System · follow macOS). Context sits under the title. */ +.palrow.two{display:grid;grid-template-columns:18px minmax(0,1fr) auto;column-gap:10px;row-gap:0;min-height:40px;padding:6px 10px} +.palrow.two .ic{grid-row:1 / span 2} +.palrow.two .ti{grid-column:2;grid-row:1;max-width:none} +.palrow.two .cx{grid-column:2;grid-row:2;font-size:12px;line-height:16px;font-weight:400} +.palrow.two .cx:empty{display:none} +.palrow.two:has(> .cx:empty){min-height:36px} +.palrow.two .sc{grid-column:3;grid-row:1 / span 2} + +.palempty{display:flex;flex-direction:column;gap:6px} +.palempty-t{padding:12px 14px 0;font-size:14px;font-weight:600;color:var(--ink);overflow-wrap:anywhere} +.palempty .cap{padding:0 14px 6px} + +.palfoot{height:40px;flex:none;display:flex;align-items:center;gap:14px;padding:0 16px; + border-top:1px solid var(--bd);color:var(--ink-2);font-size:12px} +.palfoot > span{display:inline-flex;align-items:center;gap:5px;white-space:nowrap} +.palfoot .kc + .kc{margin-left:-2px} + +/* ---- Sheets (SH-14 switch session, SH-15 keyboard shortcuts) ---- */ +.sheetwrap{position:absolute;inset:0;background:var(--scrim);z-index:26;display:flex;align-items:flex-start;justify-content:center;padding:90px 16px 24px} +.sheet{width:520px;max-width:100%;max-height:100%;background:var(--lift);border-radius:var(--r-sheet);box-shadow:var(--sh-pop); + overflow:hidden;display:flex;flex-direction:column} +.sheet.sheet-kb{width:640px} +.sheethead{flex:none;display:flex;align-items:center;gap:8px;height:58px;padding:0 14px 0 22px} +.sheethead .ttl{flex:1;min-width:0;font-size:17px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ink)} +.sheetbody{flex:1;min-height:0;overflow-y:auto;padding:0 10px 10px} +.sheetfoot{flex:none;display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:12px 16px;border-top:1px solid var(--bd)} + +/* Switch session: filter pill, then rows with the sidebar's dot grammar and a state word. */ +.sesfilter{height:36px;margin:0 6px 6px;padding:0 14px 0 12px} +.sesfilter > svg{flex:none;color:var(--ink-3)} +.sesfilter input{font-size:13.5px} +.sesrows > .row{gap:12px;padding:7px 12px} +.sesrows > .row .main{gap:0} +.sesrows > .row .t{font-size:13.5px;font-weight:500;color:var(--ink)} +.sesrows > .row.on .t{color:var(--blue)} +.sesrows > .row .meta{font-size:12px;color:var(--ink-2)} +.sesempty{padding:14px 12px;font-size:13px;color:var(--ink-2)} + +/* Keyboard shortcuts: two columns of label · keycaps rows with a hairline under each. */ +.kbgrid{display:grid;grid-template-columns:1fr 1fr;column-gap:28px;padding:0 12px 4px} +.kbgrid > .kbrow{min-height:36px;padding:0;gap:12px;border-radius:0;box-shadow:inset 0 -1px 0 var(--divider)} +.kbgrid > .kbrow:hover{background:transparent} +.kbrow .main{gap:0} +.kbrow .t{font-size:13.5px;font-weight:500;color:var(--ink)} +.kbkeys{flex:none;display:flex;align-items:center;gap:4px} + +/* ---- Alert (SH-03, delete chat) ---- */ +.sheetwrap.alertwrap{align-items:center;padding:16px} +.alertbox{width:400px;max-width:100%;background:var(--lift);border-radius:var(--r-comp);box-shadow:var(--sh-pop); + padding:26px 24px 20px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center} +.alertmark{display:flex} +.alertmark svg{display:block} +.alertbox .ttl{font-size:18px;line-height:24px;color:var(--ink);overflow-wrap:anywhere} +.alertmsg{margin:0;font-size:13.5px;line-height:1.5;color:var(--ink-2)} +.alertacts{display:flex;gap:8px;width:100%;margin-top:6px} +.alertacts .btn{flex:1 1 0} +/* Cancel arrives focused (Enter or Space cancels); the ring says which button that is. */ +.alertacts .btn:focus{outline:2px solid var(--brand);outline-offset:2px} + +/* ---- Page menubar menus (hidden in Electron, which uses the native menu bar) ---- */ +.menupanel{position:absolute;top:26px;background:var(--lift);border-radius:var(--r-pop);box-shadow:var(--sh-pop);padding:6px;min-width:260px;z-index:31} +.mrow{min-height:30px;display:flex;align-items:center;gap:var(--s4);padding:0 10px;width:100%;font-size:13px;border-radius:8px;color:var(--ink)} +.mrow:hover:not(.dis){background:var(--brand);color:var(--on-strong)} +.mrow:hover:not(.dis) .msc,.mrow:hover:not(.dis) .mck{color:var(--on-strong);border-color:var(--glass-20);background:var(--glass-soft)} .mrow .mlb{flex:1;white-space:nowrap} -.mrow .msc{font-size:11px;color:var(--text-tertiary);font-variant-numeric:tabular-nums} -.mrow.dis{color:var(--text-disabled);cursor:default} -.mrow .why{font-size:11px;color:var(--text-disabled)} -.msep{height:1px;background:var(--line-soft);margin:5px 8px} -.mgroup{padding:8px 10px 2px;color:var(--text-secondary)} -.mtick{width:12px;font-size:11px;color:var(--text-secondary)} - -.sheetwrap{position:absolute;inset:0;background:var(--scrim);z-index:26;display:flex;align-items:flex-start;justify-content:center;padding-top:60px} -.sheet{width:520px;max-width:calc(100% - 32px);background:var(--bg-overlay);border-radius:0;box-shadow:var(--shadow-overlay);overflow:hidden;display:flex;flex-direction:column;max-height:78%} -.sheethead{padding:var(--s5) var(--s5) var(--s3)} -.sheetbody{padding:0 var(--s5) var(--s5);overflow-y:auto} -.sheetfoot{display:flex;justify-content:flex-end;gap:var(--s3);padding:var(--s4) var(--s5);box-shadow:inset 0 1px 0 var(--line-soft)} -.alertbox{width:380px;max-width:calc(100% - 32px);background:var(--bg-overlay);border-radius:0;box-shadow:var(--shadow-overlay);padding:var(--s6);display:flex;flex-direction:column;gap:var(--s4)} - -/* toasts */ -#toasts{position:absolute;right:var(--s5);bottom:var(--s5);display:flex;flex-direction:column;gap:var(--s3);z-index:29;align-items:flex-end} -.toast{width:320px;background:var(--bg-overlay);border-radius:0;box-shadow:var(--shadow-overlay);padding:var(--s4);border-left:3px solid var(--success);display:flex;gap:var(--s3);align-items:flex-start} - -@media (max-width:820px){ - .pal{margin-top:6vh} +.mrow .msc{font-size:11px;color:var(--ink-2);font-variant-numeric:tabular-nums} +.mrow.dis{color:var(--ink-3);cursor:default} +.mrow .why{font-size:11px;color:var(--ink-3)} +.msep{height:1px;background:var(--bd);margin:4px 8px} +.mgroup{padding:8px 10px 2px;font-size:12px;font-weight:600;color:var(--ink-2)} +.mtick{width:12px;font-size:11px;color:var(--ink-2)} + +/* ---- Toasts (SH-16) ---- + Bottom right, 6 s, Esc pops the newest. A green check for done, a red alert for `bad`. */ +#toasts{position:absolute;right:24px;bottom:24px;display:flex;flex-direction:column;gap:8px;z-index:29;align-items:flex-end} +.toast{width:380px;max-width:calc(100vw - 48px);display:flex;align-items:flex-start;gap:10px;padding:12px 16px 12px 12px; + background:var(--lift);color:var(--ink);border-radius:var(--r-pop);box-shadow:var(--sh-pop)} +.toast-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;padding-top:3px} +.toast-t{font-size:13.5px;line-height:18px;font-weight:600;color:var(--ink);overflow-wrap:anywhere} +.toast-s{font-size:12.5px;line-height:17px;color:var(--ink-2);overflow-wrap:anywhere; + display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} + +/* ---- Short windows ---- */ +@media (max-height:700px){ + .pal{margin-top:44px;max-height:calc(100% - 64px)} + .sheetwrap{padding-top:44px} +} +@media (max-width:700px){ + .kbgrid{grid-template-columns:1fr} } diff --git a/desktop/renderer/renderer.js b/desktop/renderer/renderer.js index 58bf93a9..5edc88d7 100644 --- a/desktop/renderer/renderer.js +++ b/desktop/renderer/renderer.js @@ -2513,7 +2513,7 @@ function renderConsole() { /* ---------------- palette ---------------- */ const SCOPES = { - theme: {label:'Theme', ph:'Choose a theme…', rows:[['gear','System','follow macOS','','theme:system'],['gear','Light','','','theme:light'],['gear','Dark','','','theme:dark']]}, + theme: {label:'Theme', ph:'Choose a theme…', rows:[['laptop','System','follow macOS','','theme:system'],['sun','Light','','','theme:light'],['moon','Dark','','','theme:dark']]}, // Item 7: the prototype's Task scope is gone — nothing targeted scope:task, and the Tasks tab is the one surface. }; @@ -2535,9 +2535,9 @@ function palRows() { const hits = all.map((r) => ({r, s:score(r)})).filter((x) => x.s < 9).sort((a, b) => a.s - b.s).map((x) => x.r); // cross-entity SESSIONS.filter((s) => s.t.toLowerCase().includes(q)).slice(0, 3) - .forEach((s) => hits.push({ic:'chat', t:s.t, cx:'Session · ' + chatStatusWord(s), sc:'', act:'ses:' + s.id, badge:'session'})); + .forEach((s) => hits.push({ic:'chat', t:s.t, cx:'Session · ' + chatStatusWord(s), sc:'', act:'ses:' + s.id, badge:'session', dot:chatDot(s)})); TASKS.filter((t) => t.t.toLowerCase().includes(q)).slice(0, 3) - .forEach((t) => hits.push({ic:'tasks', t:t.t, cx:'Task · ' + t.when, sc:'', act:'room:tasks', badge:'task'})); + .forEach((t) => hits.push({ic:'tasks', t:t.t, cx:'Task · ' + t.when, sc:'', act:'room:tasks', badge:'task', dot:taskDot(t)})); SKILLS.filter((s) => s.t.toLowerCase().includes(q)).slice(0, 3) .forEach((s) => hits.push({ic:'skills', t:s.t, cx:'Skill · ' + s.s, sc:'', act:'room:skills', badge:'skill'})); return hits; @@ -2550,11 +2550,18 @@ function bold(t, q) { return esc(t.slice(0, i)) + '' + esc(t.slice(i, i + q.length)) + '' + esc(t.slice(i + q.length)); } +/* Soft Tactile: the right slot carries, in order, the current theme's check (Theme + scope only), a mono slash alias, the status dot of a matching chat or task (only + when it needs you or is running — a read chat carries none), or keycaps. Chats, + tasks, skills and scope rows are two-line rows with the context under the title. */ function palRowHTML(r, i, q) { - const shortcut = r.sc && r.sc.startsWith('/') - ? '' + esc(r.sc) + '' + const current = S.scope === 'theme' && r.act === 'theme:' + S.theme; + const shortcut = current ? '' + ic('check') + '' + : r.sc && r.sc.startsWith('/') ? '' + esc(r.sc) + '' + : r.dot && r.dot[0] !== 'empty' ? '' : keycaps(r.sc); - return ''; + + ''; } else { - list = flat.map((r, i) => palRowHTML(r, i, q)).join(''); + // Ranked commands first, then a header before the chats, tasks and skills that + // match. Headers sit between rows only; data-palrow indexes stay the flat order. + const heads = {session:'Chats', task:'Tasks', skill:'Skills'}; + let last = ''; + flat.forEach((r, i) => { + if (r.badge && r.badge !== last) list += '
' + heads[r.badge] + '
'; + last = r.badge || ''; + list += palRowHTML(r, i, q); + }); } const sc = S.scope ? SCOPES[S.scope] : null; const dial = (sc && sc.dial) ? '
' @@ -2586,12 +2601,13 @@ function paletteHTML() { + '' + S.dialShare + '
' + '' + '
' + esc(shareBlurb(S.dialShare)) + '
' : ''; - return '