diff --git a/desktop/renderer/css/popovers.css b/desktop/renderer/css/popovers.css index 218b7f1e..85bd4036 100644 --- a/desktop/renderer/css/popovers.css +++ b/desktop/renderer/css/popovers.css @@ -3,63 +3,206 @@ Loaded after styles.css (tokens + kit). Colours come from tokens only. ============================================================ */ -.popover{position:absolute;background:var(--bg-overlay);border-radius:0;box-shadow:var(--shadow-overlay);width:340px;z-index:25;overflow:hidden} -.poprow{display:flex;align-items:flex-start;gap:var(--s3);padding:8px var(--s4);width:100%} -.poprow:hover{background:var(--bg-hover)} -.poprow.on{background:var(--accent-wash)} +/* ---- The popover shell ---- + Every composer popover is a floating --lift card: radius 18, the pop shadow, + 6px inside (the kit's .tk-pop rides on the same element). anchorStyle() pins + it above the composer card of the chip that opened it, gives it that chip's + width and caps its height to what the content column leaves; the numbers + here are only the fallback for a popover with no chip on screen. */ +.popover{position:absolute;z-index:25;width:340px;max-width:calc(100vw - 32px);overflow:hidden; + background:var(--lift);border-radius:var(--r-pop);box-shadow:var(--sh-pop);padding:6px; + display:flex;flex-direction:column;gap:1px;color:var(--ink)} +.popfoot{flex:none;display:flex;align-items:center;justify-content:flex-end;gap:6px; + margin-top:4px;padding:6px 0 0 2px;box-shadow:inset 0 1px 0 var(--divider)} +.popfoot .grow{flex:1;min-width:0} +/* Footer verbs are the small pill size everywhere, including the ones another + function hands the shell (the model step's Back · Use default · Use this model). */ +.popover .popfoot .btn:not(.icon){height:26px;padding:0 10px;font-size:12px} +.popover .btn svg{width:14px;height:14px} -.ctxbar{display:flex;height:6px;border-radius:0;overflow:hidden;margin-top:10px;gap:1px} -.ctxbar i{display:block} -.swatch{width:8px;height:8px;border-radius:0;flex:none} -.popfoot{display:flex;align-items:center;justify-content:flex-end;gap:var(--s3);padding:var(--s4);box-shadow:inset 0 1px 0 var(--line-soft)} +/* ---------------- model selector popup ---------------- */ +.selpop{width:460px;max-height:60vh} +.selhead{flex:none;display:flex;align-items:center;gap:8px;min-height:32px;padding:4px 8px; + font:600 13px/1.3 var(--font-ui);letter-spacing:-.003em;color:var(--ink)} +.selhead .selttl{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.selhead .logo,.selhead .tk-ico{flex:none} +.selhead .selbusy{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:6px;font-weight:500;color:var(--ink-2)} +.selhead .selbusy .tk-spin{width:11px;height:11px} -.modellist{display:flex;flex-direction:column} -.modelrow{display:flex;align-items:center;gap:var(--s4);padding:8px var(--s4);width:100%;border-left:2px solid transparent;cursor:pointer} -.modelrow + .modelrow{box-shadow:inset 0 1px 0 var(--line-soft)} -.modelrow:hover{background:var(--bg-hover)} -.modelrow.on{background:var(--accent-wash);border-left-color:var(--accent)} -.modelrow .col{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} -.modelrow .nm{color:var(--text-primary);font-size:13px} +.selsearch{flex:none;padding:0 0 6px} +.selpop .tk-inpwrap{height:36px;padding:0 12px} +.selpop .tk-inpwrap > svg{width:15px;height:15px;flex:none} +.selpop .tk-inpwrap input{font-size:13.5px} +.selpop .tk-inpwrap.is-error{background:var(--lift);border-color:var(--critical);box-shadow:0 0 0 4px var(--critical-wash)} +.selpop .tk-inpwrap.is-warn{border-color:var(--amber)} +.selpop .tk-inpwrap .tk-spin{flex:none;margin-right:4px} -/* ---------------- model selector popup ---------------- */ -.selpop { width: 460px; max-width: calc(100vw - 32px); display: flex; flex-direction: column; max-height: 60vh; } -.selhead { - flex: none; height: 36px; display: flex; align-items: center; gap: var(--s3); - padding: 0 var(--s5); font-size: 13px; font-weight: 500; - box-shadow: inset 0 -1px 0 var(--line-soft); -} -.selsearch { flex: none; padding: var(--s3) var(--s5); } -.selbody { flex: 1; min-height: 0; overflow-y: auto; } +.selbody{flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column} /* The list scrolls inside the popup — it must never grow past the window. */ -.sellist { flex: 1; min-height: 0; max-height: 320px; overflow-y: auto; display: flex; flex-direction: column; } -.selpop .popfoot { flex: none; } +.sellist{flex:1 1 auto;min-height:0;max-height:340px;overflow-y:auto;display:flex;flex-direction:column;gap:1px} +.selbody > .sellist{flex:none} +.selpop .popfoot{flex:none} + +/* One row: badge · id over its one-line facts · right slot (radio, Unverified, download). */ +.selpop .modelrow{display:flex;align-items:center;gap:10px;width:100%;min-height:42px;padding:6px 10px;flex:none; + border:0;border-radius:var(--r-well);background:transparent;color:var(--ink);text-align:left;cursor:pointer; + transition:background var(--t-state) var(--ease)} +.selpop .modelrow:hover{background:var(--hover)} +.selpop .modelrow.on{background:var(--brand-wash)} +.selpop .modelrow:focus-visible{outline:2px solid var(--brand);outline-offset:-2px} +.selpop .modelrow .col{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} +.selpop .modelrow .nm{font:600 13.5px/1.3 var(--font-ui);letter-spacing:-.003em;color:var(--ink); + overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.selpop .modelrow .nm.mono{font:500 13px/18px var(--font-mono);letter-spacing:0} +.selpop .modelrow .col > .cap{font-size:12px;line-height:1.4;color:var(--ink-2);overflow:hidden; + display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2} +.selpop .modelrow .cap.mono,.selpop .modelrow .cap .mono{font-family:var(--font-mono);font-size:11.5px;letter-spacing:0} +.selpop .modelrow.dim .nm{color:var(--ink-3)} +.selpop .modelrow.dim .logo,.selpop .modelrow.dim .tk-ico{opacity:.55} +.selpop .selr{margin-left:auto;flex:none;display:flex;align-items:center;gap:6px;padding-left:4px} +.selpop .seldl svg{width:13px;height:13px} +.selpop .selsep{margin:4px 8px} +.selspin{width:22px;height:22px;flex:none;display:grid;place-items:center} +/* MP-11: the typed query inside a result. A wrapper around the matched letters + only — the row's .nm keeps exactly the text it had. */ +.selhit{background:var(--brand-wash);color:var(--blue);border-radius:3px} + +.selnote{flex:none;display:flex;align-items:center;gap:8px;margin:0;padding:12px 10px;font-size:12.5px;line-height:1.45;color:var(--ink-2)} +/* A switch error. The inline colour on it is what drive-selector reads — keep it. */ +.selerr{flex:none;display:flex;align-items:flex-start;gap:8px;margin:6px 0 0;padding:8px 10px;border-radius:var(--r-well); + background:var(--critical-wash);font-size:12.5px;line-height:1.45;overflow-wrap:anywhere;max-height:140px;overflow-y:auto} +.selerr > svg{flex:none;width:14px;height:14px;margin-top:2px} + +/* MP-16: the popover's own download — the raw CLI line in a mono well. */ +.selpull{gap:8px;padding:2px 2px 6px} +.selpull .selpullline{margin:0;padding:10px 12px;border-radius:var(--r-well);background:var(--well);color:var(--ink); + font:400 12px/1.5 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere} +.selpull > .cap:not(.selpullline){margin:0 4px} + +/* MP-04: the preset list's key field. */ +.selkey{flex:none;padding:8px 0 2px} + +/* ---- The add-provider wizard inside the popover (MP-06…10) ---- */ +.selwiz{gap:6px;padding:0 2px 6px} +.selwiz .tk-lbl{padding:4px 4px 0} +.selwiz #wiz-key,.selwiz #wiz-url{font-family:var(--font-mono);font-size:12.5px} +.selpop .ob-help{margin:0;padding:0 4px;max-width:none;font-size:12.5px;line-height:1.45;color:var(--ink-2)} +.selpop .ob-err{margin:0;padding:0 4px;max-width:none;font-size:12.5px;line-height:1.45;white-space:pre-wrap;color:var(--critical)} +.selpop .ob-err.tk-help--warn{color:var(--amber-text)} + +/* Inside the popover the shell's own body is the scroller, so the list must + not add a second one — two nested scrollers is how the footer ends up + somewhere nobody can reach. */ +.selpop .ob-wizlist{max-height:none;overflow:visible} +.selpop .ob-wiz{display:flex;flex-direction:column;gap:6px;width:100%;padding:0 0 4px} +/* The popover's header already names the provider; the step's own kicker and heading would say it twice. */ +.selpop .ob-wiz > .ob-kicker,.selpop .ob-wiz > .ob-h{display:none} +.selpop #wiz-model-q{width:100%;height:34px;flex:none;padding:0 14px;border:1px solid transparent;border-radius:var(--r-pill); + background:var(--well);color:var(--ink);font:400 13.5px/1 var(--font-ui);outline:none} +.selpop #wiz-model-q::placeholder{color:var(--ink-3)} +.selpop #wiz-model-q:focus{background:var(--lift);border-color:var(--brand);box-shadow:var(--focus-ring)} +.selpop .prows{display:flex;flex-direction:column;gap:1px;border:0;box-shadow:none;background:transparent} +.selpop .prows [data-wizmodel]{display:flex;align-items:center;gap:10px;width:100%;min-height:42px;padding:6px 10px; + border:0;border-radius:var(--r-well);background:transparent;box-shadow:none;color:var(--ink);text-align:left;cursor:pointer} +.selpop .prows [data-wizmodel]:hover{background:var(--hover)} +.selpop .prows [data-wizmodel].on{background:var(--brand-wash)} +.selpop .prows [data-wizmodel] .col{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} +.selpop .prows [data-wizmodel] .nm{font:500 13.5px/1.3 var(--font-ui);color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.selpop .prows [data-wizmodel] .ep{font:400 11.5px/1.4 var(--font-mono);color:var(--ink-2)} +.selpop .prows [data-wizmodel] .ann{margin-left:auto;flex:none;background:var(--brand-wash);color:var(--blue)} +/* ---- Context breakdown (CM-15…17) ---- */ +.ctxpop{width:380px} +.ctxin{flex:none;display:flex;flex-direction:column;gap:10px;padding:8px 8px 10px} +.ctxpop .ctxttl{margin:0;font:500 12.5px/1.4 var(--font-mono);letter-spacing:0;color:var(--ink)} +.ctxmeter{display:flex;flex:none;height:8px} +.ctxmeter > i,.ctxmeter > b{display:block;height:100%;flex:none;border-radius:0} +.ctxmeter > b{background:var(--bd-2)} +.ctxmeter.proj > i{background:repeating-linear-gradient(90deg,var(--brand) 0 3px,transparent 3px 5px)} +.ctxpop .kvgrid{grid-template-columns:minmax(0,1fr) max-content;gap:5px 12px;font-size:13px} +.ctxpop .kvgrid dt{color:var(--ink)} +.ctxpop .kvgrid dd{text-align:right;font-size:12px;line-height:18px;color:var(--ink)} +.ctxpop .kvgrid .ter{color:var(--ink-2)} +.ctxpop .ctxin > .cap{margin:0;line-height:1.5} +/* ---- Item 7C: the context panel's binding line ---- */ +.ctxbound{display:flex;align-items:flex-start;gap:6px;color:var(--amber-text)} +.ctxbound > svg{flex:none;width:13px;height:13px;margin-top:2px} /* context panel dials */ -.ctxdials { box-shadow: inset 0 1px 0 var(--line-soft); padding: var(--s3) 0; } -.ctxdial { display: flex; align-items: center; gap: var(--s4); padding: var(--s2) var(--s5); } -.ctxdial .col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; font-size: 13px; } -.ctxdial .btn { min-width: 28px; justify-content: center; } +.ctxdials{flex:none;padding:0 8px 2px} +.ctxdial{display:flex;align-items:center;gap:6px;padding:10px 0 4px;box-shadow:inset 0 1px 0 var(--divider)} +.ctxdial .col{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} +.ctxdial .ctxdt{font:600 13px/1.3 var(--font-ui);color:var(--ink)} +.ctxdial .ctxstep{display:flex;align-items:center;gap:4px} +.ctxdial .ctxval{min-width:32px;text-align:center;font-size:13px} -.modelrow.dim { opacity: .5; } +/* ---- Modes (CM-12…14) ---- */ +.modepop{width:360px} +.modehead{flex:none;display:flex;align-items:baseline;gap:8px;min-height:30px;padding:6px 8px 4px; + font:600 13px/1.3 var(--font-ui);color:var(--ink)} +.modehead .cap{margin-left:auto;min-width:0;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.poprow{display:flex;align-items:center;gap:10px;width:100%;min-height:44px;padding:6px 10px;flex:none; + border:0;border-radius:var(--r-well);background:transparent;color:var(--ink);text-align:left;cursor:pointer; + transition:background var(--t-state) var(--ease)} +.poprow:hover{background:var(--hover)} +.poprow:focus-visible{outline:2px solid var(--brand);outline-offset:-2px} +.poprow .col{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} +.poprow .ml{font:600 13.5px/1.3 var(--font-ui);color:var(--ink)} +.poprow .col > .cap{font-size:12px;line-height:1.4} +.poprow .modecur{margin-left:auto;flex:none} +/* The row's word takes the chip's colour: default ink, plan blue, auto amber, bypass red. */ +.poprow.modetone-plan .ml{color:var(--blue)} +.poprow.modetone-auto .ml{color:var(--amber-text)} +.poprow.modetone-bypass .ml{color:var(--critical)} +.modenote{flex:none;display:flex;flex-direction:column;gap:4px;padding:6px 10px 2px} +.modenote p{margin:0;line-height:1.5} +.modenote .modewarn{color:var(--amber-text)} +.modenote .ob-help{max-width:none;font-size:12px;color:var(--ink-2)} /* ============================================================ Coding-mode popover — the unavailable state (item 6) ============================================================ `.dim` is applied to the mode rows when the agent has no - /api/coding-mode route, but the only `.dim` rules that existed were - `.palrow`, `.modelrow` and `.tuirow`, so the unavailable state was - invisible. A disabled row also has to stop looking clickable. */ -.poprow.dim{opacity:.5} + /api/coding-mode route. A disabled row has to look unavailable + and stop looking clickable. (The smoke looks for this rule.) */ +.poprow.dim{cursor:default} +.poprow.dim .ml,.poprow.dim .col > .cap{color:var(--ink-3)} .poprow[disabled]{cursor:default} .poprow[disabled]:hover{background:transparent} -/* ---- Item 7C: the context panel's binding line ---- */ -.ctxbound{color:var(--warn)} - -.modelrow .ann{margin-left:auto} - -/* Inside the popover the shell's own body is the scroller, so the list must - not add a second one — two nested scrollers is how the footer ends up - somewhere nobody can reach. */ -.selpop .ob-wizlist{max-height:none;overflow:visible} -.selpop .ob-wiz{padding:var(--s4) var(--s5)} +/* ---- Voice language menu (CM-08) ---- + voiceMenuHTML draws it inside the voice strip; it floats above the strip as a + popover, off .composerwrap (the strip's positioned ancestor). The .tk-vmenu + compound keeps these declarations ahead of the strip's own .vsm* rules. */ +.voicestrip .vsmenu.tk-vmenu{position:absolute;z-index:16;right:var(--s6);bottom:calc(100% + 6px); + width:420px;max-width:calc(100% - 16px);max-height:none;margin:0;padding:6px;border:0;border-radius:var(--r-pop); + background:var(--lift);box-shadow:var(--sh-pop);display:flex;flex-direction:column;gap:1px;overflow:hidden} +.voicestrip .tk-vmenu .vsmtitle{flex:none;padding:6px 8px 2px;font:600 13px/1.3 var(--font-ui);color:var(--ink)} +/* The list gives up height first on a short window, so the menu never climbs under the toolbar (940×620). */ +.voicestrip .tk-vmenu .vsmscroll{min-height:0;max-height:min(300px,max(160px,calc(100vh - 460px)));overflow-y:auto;display:flex;flex-direction:column;gap:1px} +.voicestrip .tk-vmenu .vsmhead{padding:8px 8px 3px;font:600 12px/1.3 var(--font-ui);letter-spacing:0;text-transform:none;color:var(--ink-2)} +.voicestrip .tk-vmenu .vsmrow{display:flex;align-items:center;gap:6px;padding:0 8px 0 0;border-radius:var(--r-well);background:transparent} +.voicestrip .tk-vmenu .vsmrow:hover{background:var(--hover)} +.voicestrip .tk-vmenu .vsmpick{flex:1;min-width:0;display:flex;align-items:center;gap:10px;height:auto;min-height:42px;padding:5px 10px; + border:0;border-radius:var(--r-well);background:transparent;color:var(--ink);text-align:left} +.voicestrip .tk-vmenu .vsmbody{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} +.voicestrip .tk-vmenu .vsmline{display:flex;align-items:center;gap:6px;min-width:0} +.voicestrip .tk-vmenu .vsmname{flex:none;font:500 13.5px/1.3 var(--font-ui);color:var(--ink)} +.voicestrip .tk-vmenu .vsmtag{flex:none;font:400 11px/1 var(--font-mono);color:var(--ink-2)} +.voicestrip .tk-vmenu .vsmnote{flex:none;text-align:left;font-size:12px;line-height:1.35;color:var(--ink-2); + white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.voicestrip .tk-vmenu .vsmbadge{flex:none;height:18px;display:inline-flex;align-items:center;padding:0 7px;border-radius:var(--r-pill); + background:var(--brand-wash);color:var(--blue);font:600 11px/1 var(--font-ui)} +.voicestrip .tk-vmenu .vsmadd{width:26px;height:26px;flex:none;display:grid;place-items:center;border:0;border-radius:50%; + background:transparent;color:var(--ink-2)} +.voicestrip .tk-vmenu .vsmadd:hover{background:var(--press);color:var(--ink)} +.voicestrip .tk-vmenu .vsmadd.on{background:var(--brand-wash);color:var(--blue)} +.voicestrip .tk-vmenu .vsmadd svg{width:14px;height:14px} +.voicestrip .tk-vmenu .vsmprog{flex:none;width:60px;min-width:0} +.voicestrip .tk-vmenu .vsmerr{padding:6px 10px;font-size:12.5px;line-height:1.45;color:var(--critical)} +.voicestrip .tk-vmenu .vsmfoot{flex:none;margin-top:4px;padding:8px 10px 4px;box-shadow:inset 0 1px 0 var(--divider); + font-size:12px;line-height:1.5;color:var(--ink-2)} +.voicestrip .tk-vmenu .vsmempty{padding:10px;font-size:12.5px;color:var(--ink-2)} +/* On a short window the long note scrolls in its own few lines instead of pushing the languages out. */ +@media (max-height:700px){ + .voicestrip .tk-vmenu .vsmfoot{max-height:78px;overflow-y:auto} +} diff --git a/desktop/renderer/renderer.js b/desktop/renderer/renderer.js index 58bf93a9..dd493279 100644 --- a/desktop/renderer/renderer.js +++ b/desktop/renderer/renderer.js @@ -2253,15 +2253,19 @@ function voiceChipHTML() { function voiceMenuHTML() { if (!VOICE.supported.length) { - return '
'; + return ''; } const installed = VOICE.supported.filter((id) => VOICE.installed.indexOf(id) >= 0); const rest = VOICE.supported.filter((id) => VOICE.installed.indexOf(id) < 0); + /* Soft Tactile (CM-08): a radio for the first language, the name with its + mono tag and `first` badge over the note, a bar while a model downloads, + and the + / ✓ that adds a second language on the right. */ const row = (id) => { const on = VOICE.locales.indexOf(id) >= 0; const primary = VOICE.locales[0] === id; const here = VOICE.installed.indexOf(id) >= 0; const eng = voiceEngineOf(id); + const pct = VOICE.installing === id ? Math.round(VOICE.installFraction * 100) : -1; const note = here ? (eng === 'dictation' ? 'on this Mac · plain text, no punctuation' : 'on this Mac') : (VOICE.installing === id @@ -2269,20 +2273,26 @@ function voiceMenuHTML() { : (eng === 'dictation' ? 'downloads a model · plain text, no punctuation' : 'downloads a model')); return '