diff --git a/desktop/renderer/css/onboarding.css b/desktop/renderer/css/onboarding.css index f6d3a77a..0cecd3ae 100644 --- a/desktop/renderer/css/onboarding.css +++ b/desktop/renderer/css/onboarding.css @@ -1,484 +1,397 @@ /* ============================================================ - First-run wizard — title card, route choice, local picker, Hugging Face, download, cloud wizard, custom endpoint, second-backend offer, import. Screens ON-01…23. + First-run wizard in Soft Tactile — screens ON-01…23. Loaded after styles.css (tokens + kit). Colours come from tokens only. - ============================================================ */ - -/* ---------------- setup wizard ---------------- */ -#onboarding { - position: absolute; inset: 0; z-index: 40; - background: var(--bg-content); - display: flex; align-items: center; justify-content: center; - padding: var(--s8); - -webkit-app-region: drag; -} -#onboarding > * { -webkit-app-region: no-drag; } -.ob { width: min(560px, 100%); display: flex; flex-direction: column; gap: var(--s6); } -.ob-head { display: flex; flex-direction: column; gap: var(--s3); align-items: center; text-align: center; } -.ob-mark { opacity: .9; } -.ob-step { - font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; - color: var(--text-tertiary); -} -.ob-title { font-size: 22px; line-height: 28px; font-weight: 600; letter-spacing: -.02em; } -.ob-sub { font-size: 13px; color: var(--text-secondary); max-width: 44ch; } -.ob-list { display: flex; flex-direction: column; gap: var(--s3); } -.ob-opt { - display: grid; grid-template-columns: 20px 1fr auto; gap: var(--s4); align-items: center; - padding: var(--s4) var(--s5); width: 100%; text-align: left; - border: 1px solid var(--line); border-radius:0; background: var(--bg-raised); -} -.ob-opt:hover { border-color: var(--line-strong); } -.ob-opt.on { border-color: var(--accent); background: var(--accent-wash); } -.ob-opt .t { font-size: 15px; font-weight: 500; } -.ob-opt .d { font-size: 13px; color: var(--text-secondary); } -.ob-opt .tag { - font-size: 11px; font-weight: 500; color: var(--accent-text); - border: 1px solid var(--accent-line); border-radius:0; padding: 1px 6px; -} -.ob-opt[disabled] { opacity: .5; cursor: default; } -.ob-models { max-height: 46vh; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; } -.ob-foot { display: flex; align-items: center; gap: var(--s4); } -.ob-foot .grow { flex: 1; } -.ob-note { font-size: 12px; color: var(--text-tertiary); } -.ob-prog { - font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary); - background: var(--bg-sunken); border-radius:0; padding: var(--s3); - max-height: 120px; overflow-y: auto; white-space: pre-wrap; -} - -/* The wizard shell. The intro draws no header and fills the layer edge to - edge (the sky is the screen); every other step keeps the centred column. */ -#onboarding.ob-intro-layer{padding:0;background:var(--bg-sunken)} -.ob-hints{ - position:absolute;left:0;right:0;bottom:0;height:34px;display:flex;align-items:center; - justify-content:center;gap:6px;font-size:11px;color:var(--text-tertiary); - border-top:1px solid var(--line-soft);background:var(--bg-content); - -webkit-app-region:no-drag; -} -#onboarding.ob-intro-layer .ob-hints{background:transparent;border-top-color:transparent} -.ob-hints .kc{margin-right:2px} -.ob-hints .hint{display:inline-flex;align-items:center;gap:4px;margin:0 6px} -.ob{max-height:calc(100% - 44px);overflow-y:auto;padding-bottom:var(--s4)} - -/* Header lockup: the mark, `atomic`, and where in the flow we are — the - TUI's onboarding-header.tsx, laid out as a row rather than a column. */ -.ob-lock{display:flex;flex-direction:row;align-items:center;gap:10px;justify-content:center} -.ob-lock .ob-wm{font-size:15px;font-weight:600;color:var(--accent-text);line-height:1.1} -.ob-lock .ob-sub2{font-size:12px;color:var(--text-tertiary);line-height:1.2} - -/* Intro: the canvas sky, the mark hanging in it, the wordmark and tagline. */ -/* The title card. Centred on the window, sized to it — the block used to be - a fixed 320px column, which at 1470x923 was the 2% of the screen the tester - could not read. Nothing is animated and nothing is decorative: the mark, the - name, one rule, the build, and the line that tells you how to leave. */ -#ob-intro{position:absolute;inset:0;display:flex;align-items:center;justify-content:center; - background:var(--paper);padding:var(--s8)} -.ob-introc{position:relative;display:flex;flex-direction:column;align-items:center; - gap:var(--s4);width:min(720px,100%);text-align:center} -.ob-introc .ob-markbig{width:64px;height:64px;display:block} -.ob-introc .ob-markbig svg{width:64px;height:64px;display:block} -.ob-introc .ob-word{margin:0;font-size:var(--fs-display);font-weight:600; - letter-spacing:var(--tr-display);line-height:1.15;color:var(--ink)} -.ob-introc .ob-rule{width:100%;max-width:420px;border:0;border-top:var(--rule) solid var(--accent);margin:0} -.ob-introc .ob-build{font-family:var(--font-mono);font-size:var(--fs-cap);color:var(--ink-2)} -.ob-introc .ob-any{font-size:var(--fs-label);letter-spacing:var(--tr-label); - text-transform:uppercase;color:var(--ink-3);margin-top:var(--s5)} - -/* Rows. The TUI marks the selected row with `› `; here the marker is a real - gutter so mouse and keyboard land on the same box. */ -.ob-row{display:grid;grid-template-columns:18px 1fr;gap:var(--s3);align-items:start; - width:100%;text-align:left;padding:var(--s3) var(--s4);border:1px solid transparent; - border-radius:0;background:transparent;color:var(--text-primary)} -.ob-row:hover{background:var(--bg-hover)} -.ob-row.on{border-color:var(--accent-line);background:var(--accent-wash)} -.ob-row .mk{color:var(--accent-text);font-family:var(--font-mono)} -.ob-row .t{font-size:14px;font-weight:500} -.ob-row.on .t{color:var(--accent-text)} -.ob-row .d{font-size:12px;color:var(--text-secondary);display:block;line-height:1.5} -.ob-row .box{font-family:var(--font-mono);color:var(--text-secondary);margin-right:6px} -.ob-explain{font-size:13px;color:var(--text-secondary);line-height:1.6;white-space:pre-line} -.ob-h{font-size:14px;font-weight:600} -.ob-ok{color:var(--ok)} -.ob-warn{color:var(--warn)} -.ob-err{color:var(--danger);white-space:pre-wrap;max-width:72ch;font-size:12px} -.ob-mono{font-family:var(--font-mono);font-size:12px;white-space:pre} -.ob-inp{width:100%;height:30px;padding:0 8px;border:1px solid var(--line);border-radius:0; - background:var(--bg-sunken);color:var(--text-primary);font-family:var(--font-mono);font-size:12px} -.ob-inp:disabled{color:var(--text-tertiary)} - -/* Download screen: the two phase rows, and the two offer blocks the TUI - draws with a `┃ ` terminal rule — a real left border here. */ -.ob-phase{display:flex;align-items:center;gap:10px;font-family:var(--font-mono);font-size:11px} -.ob-phase .pl{color:var(--text-secondary);min-width:19ch} -.ob-phase .pb b{color:var(--accent);font-weight:400} -.ob-phase .pb i{color:var(--line-strong);font-style:normal} -/* The phase states dlResetPhases/dlOnPull actually emit are waiting, - active and done — `pending` was never written, so the waiting phase's - empty bar drew at full weight instead of the TUI's dimmed border - (onboarding-download-progress.tsx PhaseLine). */ -.ob-phase.waiting .pb i{color:var(--line)} -.ob-phase .pt{color:var(--text-secondary);font-variant-numeric:tabular-nums} -.ob-rate{font-size:12px;color:var(--text-tertiary);margin-top:var(--s3)} -/* r8: the two phase rows and the rate line are wrapped in one container so - a progress sample can repaint THEM and nothing else (refreshDlProgress) — - rebuilding the whole layer four times a second was destroying the row or - the offer card under the pointer, which is what "lighting up and down" - was. `display:contents` means the wrapper draws no box at all: the rows - stay direct flex items of `.ob`'s column and keep its gap, so the screen - is pixel-for-pixel what it was before the container existed. */ -.ob-dlprog{display:contents} -.ob-offer{border-left:2px solid var(--line);padding:2px 0 2px 12px;margin-top:var(--s5); - font-size:13px;color:var(--text-secondary);line-height:1.6;text-align:left;width:100%;background:none} -.ob-offer.cloud{border-left-color:var(--accent-line)} -.ob-offer b{display:block;font-weight:600;color:var(--text-primary);margin-top:2px} -.ob-offer:hover{background:var(--bg-hover)} - -/* The cloud step hosts the add-provider wizard inline: same rows, same - verification, but on the wizard layer rather than in the anchored popover - the model chip opens (there is no chip on screen during first run). */ -.ob-wiz{display:flex;flex-direction:column;gap:var(--s3);width:100%} -.ob-wiz .modelrow{width:100%;text-align:left;border:1px solid var(--line);border-radius:0; - background:var(--bg-raised);padding:var(--s3) var(--s4);display:flex} -.ob-wiz .modelrow:hover{border-color:var(--line-strong)} -.ob-wizlist{max-height:38vh;overflow-y:auto;display:flex;flex-direction:column;gap:6px} - -/* -------------------------------------------------------------------------- - r5 item 7 review fix — the cloud step's list cursor and its `/` search box. - The step's footer is the TUI's `↑/↓ move / search enter select esc - back`; the keys behind those hints are now real, so the row under the - cursor has to be visible without a mouse. - -------------------------------------------------------------------------- */ -.ob-wiz .modelrow.on{border-color:var(--accent);background:var(--accent-wash)} -.ob-wiz .modelrow.on .nm{color:var(--accent-text)} - -/* ========================================================================== - r6 UX — the setup wizard as a desktop surface. - - The style is the one the flow already had; what changes is the - interaction. A control now looks like a control and behaves like one: - a target big enough to hit, a hover that answers the pointer, a press - state, a focus ring for the keyboard, a disabled state that looks - disabled, and the primary action always in the same place — bottom - right, with the way back bottom left. - ========================================================================== */ - -/* The action bar every step now shares. Secondary left, primary right, - pinned under the body with a rule so the eye finds it in one place on - every screen instead of hunting for the key hint that meant it. */ -.ob-foot.ob-actions{ - padding-top:var(--s4); - border-top:1px solid var(--line-soft); - display:flex;align-items:center;gap:var(--s3); -} -.ob-foot.ob-actions .grow{flex:1} -.ob-foot .btn{height:32px;padding:0 14px;border-radius:0} -.ob-foot .btn-p{min-width:112px;justify-content:center} -.ob-foot .btn-g{border:1px solid transparent} -.ob-foot .btn-g:hover{background:var(--bg-hover);border-color:var(--line)} -.ob-foot .btn-g:active,.ob-foot .btn-p:active,.ob-wiz .ob-foot .btn:active{transform:translateY(1px)} -/* A disabled control must read as unavailable before it is clicked, not - after nothing happens. `.btn[disabled]` already greys the fill; the - pointer has to say so too. */ -.ob-foot .btn[disabled]{opacity:.55;pointer-events:none;border-color:transparent} - -/* Rows are the primary choice on six of the screens, so they get the - press state and the ring a button gets. One click activates them — - the pointer says so. */ -.ob-row{cursor:pointer;transition:background .1s ease,border-color .1s ease} -.ob-row:active{background:var(--accent-wash-soft,var(--bg-active))} -.ob-row:focus-visible,.ob-wiz .modelrow:focus-visible,.ob-offer:focus-visible{ - outline:2px solid var(--accent);outline-offset:2px; -} -.ob-wiz .modelrow{cursor:pointer;transition:background .1s ease,border-color .1s ease} -.ob-wiz .modelrow:active{background:var(--bg-active)} -/* An already-configured provider is dimmed; dimmed must not read as - dead, because the row still works — it re-opens the same key screen. */ -.ob-wiz .modelrow.dim:hover{opacity:.8} - -/* The two download offers are this screen's only controls. They were - drawn as the terminal's `┃ ` rule around a paragraph, which is a label, - not a button. Same copy, same chord, now a card. */ -.ob-offer{ - border:1px solid var(--line);border-left-width:2px;border-radius:0; - padding:var(--s3) var(--s4);cursor:pointer; - transition:background .1s ease,border-color .1s ease; -} -.ob-offer:hover{background:var(--bg-hover);border-color:var(--line-strong)} -.ob-offer:active{background:var(--bg-active)} -.ob-offer.cloud{border-left-color:var(--accent)} -.ob-offer.cloud:hover{background:var(--accent-wash)} -/* `press c` is the chord, so it is drawn as a key, not as bold prose. */ -.ob-offer b{ - display:inline-flex;align-items:center;height:20px;padding:0 7px;margin-top:6px; - border:1px solid var(--line);border-radius:0;background:var(--bg-sunken); - font-family:var(--font-mono);font-size:11px;font-weight:400;color:var(--text-secondary); -} -.ob-offer.cloud b{border-color:var(--accent-line);color:var(--accent-text)} -/* The Hugging Face `[ clear ]` sits beside a field, not on the download - screen, so it keeps its quiet inline shape rather than the card. */ -.ob-offer.ob-offer-inline{ - border:0;border-radius:0;padding:2px 8px;margin-top:var(--s2); - width:auto;display:inline-flex;font-size:12px; -} -.ob-offer.ob-offer-inline:hover{background:var(--bg-hover)} - -/* Fields: a 30px box is a terminal's idea of a text input. 34 is a - comfortable target, and the focus border says which one is live. */ -.ob-inp{height:34px;transition:border-color .1s ease} -.ob-inp:hover{border-color:var(--line-strong)} -.ob-inp:focus{border-color:var(--accent);outline:none} -.ob-inp:disabled{background:var(--bg-active);cursor:not-allowed} - -/* An error belongs under the control that caused it, at the size of a - hint rather than a headline. */ -.ob-err-field{margin-top:6px;display:flex;gap:6px;align-items:flex-start} -.ob-err-field::before{content:"✗";color:var(--danger)} - -/* `.ob` is already a flex column with a gap, so the offers' own - `margin-top` was doubling the space above them and pushing the second - card off a short window. The gap owns the rhythm; the cards sit in it. */ -#onboarding .ob-offer{margin-top:0} -/* A list that is longer than its box scrolls, the way a desktop list - does. The TUI painted six rows and a `↓ 7 more` line because a - terminal cannot scroll a region — which left the seventh model - unreachable by any mouse gesture at all. Same height, same rhythm; - the rows below it are now simply there. */ -#onboarding .ob-scroll{max-height:46vh;overflow-y:auto;overscroll-behavior:contain} + The title card is one indigo block. Every later step is two columns: + an indigo rail (lockup, the phase statement, the two phase steps, the + build) and the step itself on the window ground — title, lede, body, + the action bar and the keycap hint strip. + ============================================================ */ -/* The import step's tick boxes. `[x]` in a monospace span is a terminal - drawing a checkbox with the characters it has; this window has real - ones. Same two states, same row, same accent. */ -#onboarding .ob-row .box{ - display:inline-flex;align-items:center;justify-content:center; - width:15px;height:15px;margin-right:8px;vertical-align:-2px; - border:1px solid var(--line-strong);border-radius:0; - background:var(--bg-sunken);font-family:var(--font-ui);font-size:10px;line-height:1; - color:transparent;transition:background .1s ease,border-color .1s ease; +/* ---------------- shared wizard parts ---------------- + These are drawn OUTSIDE the first-run layer too: the composer's route + popover renders wizModelStepHTML(false) and its own key screen with + `.ob-help` / `.ob-err`, and `.ob-help` carries the composer's wait reason + and the modes popover's version note. Their base look stays unscoped; + the first-run layout below is scoped to #onboarding. */ +.ob-wiz{display:flex;flex-direction:column;gap:10px;width:100%;min-width:0} +.ob-kicker{font:600 12.5px/1.4 var(--font-ui);color:var(--ink-2);letter-spacing:0;text-transform:none} +.ob-wiz .ob-h{display:flex;align-items:center;gap:10px;min-width:0;font-size:15px;font-weight:600;letter-spacing:-.015em;color:var(--ink)} +.ob-help{font-size:12.5px;line-height:1.45;color:var(--ink-2);max-width:62ch} +.ob-err{color:var(--critical);white-space:pre-wrap;max-width:62ch;font-size:12.5px;line-height:1.45} +.ob-inp{width:100%;min-width:0;height:38px;padding:0 15px;border-radius:var(--r-pill);border:1px solid transparent; + background:var(--well);color:var(--ink);font:400 14px/1 var(--font-ui);outline:none; + transition:background var(--t-state) var(--ease),border-color var(--t-state) var(--ease),box-shadow var(--t-state) var(--ease)} +.ob-inp::placeholder{color:var(--ink-3)} +.ob-inp:hover:not(:disabled){border-color:var(--bd)} +.ob-inp:focus{background:var(--lift);border-color:var(--brand);box-shadow:var(--focus-ring)} +.ob-inp:disabled{color:var(--ink-3);cursor:not-allowed} +.ob-wizlist{display:flex;flex-direction:column;min-height:0;overflow-y:auto;overscroll-behavior:contain} +.prows{display:flex;flex-direction:column;min-width:0} +.prow{display:flex;align-items:center;gap:12px;width:100%;min-width:0;min-height:46px;padding:7px 12px;border:0; + border-radius:var(--r-row);background:transparent;color:var(--ink);text-align:left;cursor:pointer; + transition:background var(--t-state) var(--ease)} +.prow + .prow{box-shadow:0 -1px 0 var(--divider)} +.prow:hover{background:var(--hover)} +.prow:active{background:var(--press)} +.prow.on{background:var(--brand-wash);box-shadow:none} +.prow.on + .prow{box-shadow:none} +.prow:focus-visible{outline:2px solid var(--brand);outline-offset:-2px} +.prow .col{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1} +.prow .nm{font-size:14px;font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.prow .ep{font:400 12px/1.4 var(--font-mono);color:var(--ink-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.prow .ann{flex:none} +.prow-more{font:600 12.5px/1.4 var(--font-ui);color:var(--ink-2);letter-spacing:0;text-transform:none;margin:16px 0 8px 10px} + +/* ---------------- the layer ---------------- */ +#onboarding{ + position:absolute;inset:0;z-index:40; + display:grid;grid-template-columns:400px minmax(0,1fr); + background:var(--well);color:var(--ink); + -webkit-app-region:drag; } -#onboarding .ob-row .box.on{background:var(--accent);border-color:var(--accent);color:var(--on-fill)} -#onboarding .ob-row.ob-check:hover .box{border-color:var(--accent-line)} - -/* The offer cards read as one sentence with a keycap wedged into the - middle of it. A card is a control: the sentence on the left, the chord - that fires it on the right, both on one baseline. */ -#onboarding .ob-offer:not(.ob-offer-inline){ - display:flex;align-items:center;justify-content:space-between;gap:var(--s5); - width:100%;text-align:left; +#onboarding > *{-webkit-app-region:no-drag} + +/* ---------------- the title card (ON-01) ---------------- + `.ob-introc` keeps exactly its five children (mark, name, rule, build, + dismiss line) — the smoke counts them — so the glow is a sibling and the + dismiss line is placed against the card, not the column. The rule is + kept as a 3px transparent spacer: Soft Tactile draws no 3px rules, and + the smoke still reads its border width. Nothing here animates. */ +#onboarding.ob-intro-layer{display:block;background:var(--indigo)} +#ob-intro{position:absolute;inset:0;display:grid;place-items:center;overflow:hidden; + padding:var(--s8);background:var(--indigo);color:var(--on-strong)} +#ob-intro .ob-glow{position:absolute;left:50%;top:50%;width:760px;height:760px;margin:-380px 0 0 -380px; + border-radius:50%;pointer-events:none; + background:radial-gradient(circle,color-mix(in srgb,var(--brand) 90%,transparent) 0%,transparent 62%)} +.ob-introc{position:static;display:flex;flex-direction:column;align-items:center;gap:0;width:min(720px,100%);text-align:center} +.ob-introc .ob-markbig{position:relative;display:block;width:96px;height:96px;border-radius:22px; + box-shadow:0 30px 60px -20px var(--shade-50)} +.ob-introc .ob-markbig svg{display:block;width:96px;height:96px} +.ob-introc .ob-word{position:relative;margin:24px 0 0;font:700 64px/1.05 var(--font-ui);letter-spacing:-.055em;color:var(--on-strong)} +.ob-introc .ob-rule{position:relative;width:0;height:0;margin:0;border:0;border-top:3px solid transparent} +.ob-introc .ob-build{position:relative;margin-top:17px;font:400 12.5px/1.4 var(--font-mono);letter-spacing:0;color:var(--on-indigo)} +.ob-introc .ob-any{position:absolute;left:0;right:0;bottom:34px;font:400 13px/1.4 var(--font-ui); + letter-spacing:0;text-transform:none;color:var(--indigo-muted)} + +/* ---------------- the rail ---------------- + Room at the top for the real macOS traffic lights (hiddenInset at 20,20); + the rail carries no controls, so it is also the window's drag handle. */ +#onboarding .ob-rail{ + position:relative;overflow:hidden;min-height:0; + margin:10px 0 10px 10px;padding:60px 28px 26px;border-radius:var(--r-pop); + display:flex;flex-direction:column;gap:22px; + background:var(--indigo);color:var(--on-indigo); + -webkit-app-region:drag;user-select:none; } -#onboarding .ob-offer:not(.ob-offer-inline) b{margin-top:0;flex:0 0 auto} - -/* A list that scrolls must say so. macOS hides its overlay scrollbar - until something moves, so the wizard's long lists — 18 providers, a - whole model catalogue — ended in a row sliced in half with no hint - that there was more under it. A thin permanent bar, in the app's own - line colour. */ -/* Deliberately the -webkit- pseudo-elements and NOT `scrollbar-width` / - `scrollbar-color`: setting either of the standard properties makes - Chromium ignore these rules and fall back to the platform's OVERLAY - scrollbar, which on macOS is invisible until something moves — the - very thing this is here to fix. */ +#onboarding .ob-rail > *{position:relative} +/* The orbs are radial gradients that already fade to nothing, so they carry + no blur filter: it drew nothing visible and only added a paint surface on + a 440px layer inside the rail. Drawn soft by the gradient alone. */ +#onboarding .ob-rail > .ob-orb{position:absolute;border-radius:50%;pointer-events:none} +#onboarding .ob-orb-a{width:440px;height:440px;right:-180px;bottom:-160px; + background:radial-gradient(circle,color-mix(in srgb,var(--brand) 75%,transparent) 0%,transparent 68%)} +#onboarding .ob-orb-b{width:240px;height:240px;left:-90px;top:230px; + background:radial-gradient(circle,color-mix(in srgb,var(--indigo-muted) 25%,transparent) 0%,transparent 70%)} +#onboarding .ob-lock{display:flex;align-items:center;gap:12px;min-width:0} +#onboarding .ob-lock .ob-mark{display:flex;flex:none} +#onboarding .ob-lock .ob-mark svg{width:30px;height:30px} +#onboarding .ob-lock .ob-wm{font:600 17px/1.2 var(--font-ui);letter-spacing:-.02em;color:var(--on-strong);white-space:nowrap} +#onboarding .ob-statement{margin:auto 0 0;max-width:13ch;font:600 30px/1.13 var(--font-ui);letter-spacing:-.04em;color:var(--on-strong)} +#onboarding .ob-stepmarks{display:grid;gap:4px} +#onboarding .ob-stepmark{display:flex;align-items:center;gap:12px;height:44px;padding:0 12px;border-radius:var(--r-pill); + font:500 14px/1 var(--font-ui);letter-spacing:0;text-transform:none;color:var(--indigo-muted)} +#onboarding .ob-stepmark .n{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;flex:none; + font:500 11.5px/1 var(--font-mono);box-shadow:inset 0 0 0 1.5px var(--glass)} +#onboarding .ob-stepmark .n svg{width:13px;height:13px;stroke-width:2.2} +#onboarding .ob-stepmark.on{background:var(--glass-12);color:var(--on-strong)} +#onboarding .ob-stepmark.on .n{background:var(--on-strong);color:var(--indigo);box-shadow:none} +#onboarding .ob-stepmark.done{color:var(--on-strong)} +#onboarding .ob-stepmark.done .n{background:var(--glass-20);box-shadow:none} +#onboarding .ob-railbuild{font:400 11px/1.4 var(--font-mono);letter-spacing:0;color:var(--indigo-muted)} + +/* ---------------- the step column ---------------- + Title, then a body that owns the flexible height (its lists scroll + inside it), then the action bar and the hint strip, always in the same + place. The body's side padding is room for card shadows and the 2px + selection ring, which a scrolling box would otherwise clip. */ +#onboarding .ob{position:relative;display:flex;flex-direction:column;gap:0;width:auto;max-height:none; + min-width:0;min-height:0;overflow:hidden;padding:48px 64px 12px} +#onboarding .ob-head{flex:none;display:block} +#onboarding .ob-title{margin:0;max-width:760px;font:600 34px/1.15 var(--font-ui);letter-spacing:-.04em;color:var(--ink)} +#onboarding .ob-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:14px; + margin:10px -16px 0;padding:4px 16px 14px;overflow-y:auto;overscroll-behavior:contain} +#onboarding .ob-body > *{max-width:720px;flex-shrink:0} +#onboarding .ob-body > .ob-scroll,#onboarding .ob-body > .ob-wiz{flex-shrink:1} +/* The wizard carries its own action bar, which spans the column like every + other step's; its content is held to 720px below. */ +#onboarding .ob-body > .ob-wiz{max-width:none} +#onboarding .ob-explain{font-size:15.5px;line-height:1.5;color:var(--ink-2);max-width:60ch;white-space:pre-line} +#onboarding .ob-body > .ob-explain:first-child{margin:-4px 0 8px} +#onboarding .ob-h{font:600 15px/1.35 var(--font-ui);letter-spacing:-.01em;color:var(--ink)} +#onboarding .ob-h.ob-sec{font-size:12.5px;letter-spacing:0;color:var(--ink-2);margin:2px 0 -6px 2px} +#onboarding .ob-h.ob-ready{display:flex;align-items:center;gap:10px;font-size:17px;letter-spacing:-.015em} +#onboarding .ob-ok{color:var(--success)} +#onboarding .ob-mono{font-family:var(--font-mono);letter-spacing:0} + +/* ---------------- rows ---------------- + A list is a lifted card of ≥46px rows with inset dividers and a brand + tint for the row under the cursor. The TUI's `›` marker stays in the + markup and is not drawn — the tint is the cursor. */ +#onboarding .ob-list{display:flex;flex-direction:column;min-width:0} +/* Rows in a scrolling column must not shrink: an explicit min-height + replaces `min-height:auto`, and the rows then squeezed to 46px and drew + over each other instead of scrolling. */ +#onboarding .ob-list > *,#onboarding .ob-wizlist > *,#onboarding .prows > *{flex-shrink:0} +#onboarding .ob-list:not(.ob-routes){padding:6px;border-radius:var(--r-card);background:var(--lift);box-shadow:var(--sh-card)} +#onboarding .ob-row{position:relative;display:flex;align-items:center;gap:12px;width:100%;min-width:0;min-height:46px; + padding:8px 12px;border:0;border-radius:var(--r-row);background:transparent;color:var(--ink);text-align:left;cursor:pointer; + transition:background var(--t-state) var(--ease),box-shadow var(--t-state) var(--ease)} +#onboarding .ob-row .mk{display:none} +#onboarding .ob-list:not(.ob-routes) .ob-row + .ob-row{box-shadow:0 -1px 0 var(--divider)} +#onboarding .ob-row:hover{background:var(--hover)} +#onboarding .ob-row:active{background:var(--press)} +#onboarding .ob-row.on,#onboarding .ob-list:not(.ob-routes) .ob-row.on{background:var(--brand-wash);box-shadow:none} +#onboarding .ob-list:not(.ob-routes) .ob-row.on + .ob-row{box-shadow:none} +#onboarding .ob-row:focus-visible{outline:2px solid var(--brand);outline-offset:-2px} +#onboarding .ob-rbody{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} +#onboarding .ob-row .t{display:block;font-size:14px;font-weight:600;line-height:1.35;color:var(--ink)} +#onboarding .ob-row .d{display:block;font-size:12.5px;line-height:1.45;color:var(--ink-2)} +#onboarding .ob-row > .kc{margin-left:auto} +#onboarding .ob-row > .tk-ico,#onboarding .ob-row > .logo{flex:none} +/* A model with no brand mark wears the CPU badge at the logo's size, so + every row's text starts on the same line. */ +#onboarding .ob-row > .tk-ico--sm{width:30px;height:30px} + +/* Route cards (ON-02): three lifted cards, the one under the cursor ringed. */ +#onboarding .ob-routes{gap:12px;max-width:660px} +#onboarding .ob-routes .ob-row{gap:16px;padding:14px 18px;border-radius:var(--r-block);background:var(--lift);box-shadow:var(--sh-card)} +#onboarding .ob-routes .ob-row:hover{background:color-mix(in srgb,var(--ink) 3%,var(--lift))} +#onboarding .ob-routes .ob-row:active{background:color-mix(in srgb,var(--ink) 6%,var(--lift))} +#onboarding .ob-routes .ob-row.on{background:var(--lift);box-shadow:0 0 0 2px var(--brand),var(--sh-card)} +#onboarding .ob-routes .ob-row:focus-visible{outline-offset:3px} +#onboarding .ob-routes .ob-row .t{font-size:16px;letter-spacing:-.02em} +#onboarding .ob-routes .ob-row .d{font-size:13px} + +/* ---------------- the local model list (ON-03, ON-04) ---------------- */ +#onboarding .ob-models{flex:0 1 auto;min-height:140px;max-height:none;overflow-y:auto;overscroll-behavior:contain} +#onboarding .ob-scroll{min-height:120px;overflow-y:auto;overscroll-behavior:contain} +#onboarding .ob-models .ob-row{align-items:flex-start} +#onboarding .ob-models .ob-row > .logo,#onboarding .ob-models .ob-row > .tk-ico{margin-top:2px} +#onboarding .ob-row .t .ob-mono{font-size:13.5px;font-weight:500} +#onboarding .ob-badge{display:inline-flex;align-items:center;height:22px;padding:0 8px;margin-left:8px;vertical-align:1px; + border-radius:var(--r-pill);font:500 11.5px/1 var(--font-ui);letter-spacing:0;white-space:nowrap; + background:var(--brand-wash);color:var(--blue)} +#onboarding .ob-badge-best{background:var(--indigo);color:var(--on-indigo)} +#onboarding .ob-badge-warn{background:var(--amber-wash);color:var(--amber-text)} +#onboarding .ob-row .d > span{display:block} +#onboarding .ob-row .d > .ob-desc{display:inline} +#onboarding .ob-row .d > .ob-desc + span{display:inline} +#onboarding .ob-row .d > .ob-desc::after{content:" · "} +#onboarding .ob-row .d .ob-fit{margin-top:1px} +#onboarding .ob-fit-comfortable{color:var(--success)} +#onboarding .ob-fit-tight{color:var(--amber-text)} +#onboarding .ob-fit-over{color:var(--ink-2)} +#onboarding .ob-row .d .ob-caution{margin-top:2px;color:var(--ink-2)} +#onboarding .ob-hfrow{min-height:40px;padding:6px 12px} +#onboarding .ob-hfrow .ob-rbody{flex-direction:row;align-items:baseline;flex-wrap:wrap;gap:2px 8px} +#onboarding .ob-hfrow .t{font-size:13.5px} +/* Out of reach: shown, explained, not a control — a quiet well inside the + list card, after the rows it could not be one of. */ +#onboarding .ob-out-h{margin:12px 8px 6px;font-size:13.5px;font-weight:600;color:var(--ink)} +#onboarding .ob-out-list{display:flex;flex-direction:column;gap:2px;padding:6px;border-radius:var(--r-row);background:var(--well)} +#onboarding .ob-out{display:grid;grid-template-columns:18px minmax(0,1fr);column-gap:10px;align-items:center; + padding:6px 8px;border:0;color:var(--ink-2);cursor:default} +#onboarding .ob-out > .logo,#onboarding .ob-out > .tk-ico{grid-row:1 / span 3;align-self:start;margin-top:1px} +#onboarding .ob-out .t{font:500 13px/1.35 var(--font-mono);color:var(--ink-2)} +#onboarding .ob-out .ob-desc,#onboarding .ob-out .ob-fit{display:block;font-size:12.5px;line-height:1.4;color:var(--ink-2)} + +/* ---------------- fields ---------------- */ +#onboarding .ob-lbl{display:block;margin:4px 0 -6px 2px;font:600 13px/1.4 var(--font-ui);color:var(--ink)} +#onboarding .ob-lbl .ob-explain{display:inline;font-size:inherit;line-height:inherit;font-weight:500;color:var(--ink-2);white-space:normal} +#onboarding .ob-inp{height:52px;max-width:640px;padding:0 18px;border:0;background:var(--lift); + box-shadow:inset 0 0 0 1px var(--bd-2);font-size:14.5px} +#onboarding .ob-inp:hover:not(:disabled){box-shadow:inset 0 0 0 1px var(--ink-3)} +#onboarding .ob-inp:focus{background:var(--lift);box-shadow:inset 0 0 0 1.5px var(--brand),var(--focus-ring)} +#onboarding .ob-field{display:flex;align-items:center;gap:10px;width:100%;max-width:640px;height:52px;padding:0 8px 0 18px; + border-radius:var(--r-pill);background:var(--lift);box-shadow:inset 0 0 0 1px var(--bd-2);color:var(--ink-3); + transition:box-shadow var(--t-state) var(--ease)} +#onboarding .ob-field:hover{box-shadow:inset 0 0 0 1px var(--ink-3)} +#onboarding .ob-field:focus-within{box-shadow:inset 0 0 0 1.5px var(--brand),var(--focus-ring)} +#onboarding .ob-field.is-error{box-shadow:inset 0 0 0 1.5px var(--critical),0 0 0 4px var(--critical-wash)} +#onboarding .ob-field.is-warn{box-shadow:inset 0 0 0 1.5px var(--amber)} +#onboarding .ob-field > svg{width:16px;height:16px;flex:none} +#onboarding .ob-field > .logo{margin-left:-6px} +#onboarding .ob-field > .tk-spin{margin-right:10px} +#onboarding .ob-field > .tk-ico{margin-right:4px} +#onboarding .ob-field > .kc{margin-right:10px} +#onboarding .ob-field .ob-inp,#onboarding .ob-field .ob-inp:hover:not(:disabled),#onboarding .ob-field .ob-inp:focus{ + flex:1;height:100%;max-width:none;padding:0;border:0;border-radius:0;background:transparent;box-shadow:none} +#onboarding .ob-field .ob-inp{font:400 14.5px/1 var(--font-mono);color:var(--ink)} +#onboarding .ob-field.ob-search .ob-inp{font-family:var(--font-ui)} +#onboarding .ob-field .ob-inp:focus-visible{outline:none} +#onboarding .ob-offer.ob-offer-inline{display:inline-flex;align-items:center;flex:none;width:auto;height:32px;margin:0;padding:0 14px; + border:0;border-radius:var(--r-pill);background:var(--hover);color:var(--ink-2);font:600 12.5px/1 var(--font-ui);cursor:pointer; + transition:background var(--t-state) var(--ease),color var(--t-state) var(--ease)} +#onboarding .ob-offer.ob-offer-inline:hover{background:var(--press);color:var(--ink)} +#onboarding .ob-help{padding-left:4px} +#onboarding .ob-explain.ob-examples{font-size:13px;line-height:1.5} + +/* Errors: a red notice under the control that caused it; a surface error + sits above the action bar in the same shape. */ +#onboarding .ob-err{display:flex;align-items:flex-start;gap:10px;max-width:640px;padding:10px 14px;border-radius:18px; + background:color-mix(in srgb,var(--critical) 8%,var(--lift));color:var(--ink);font-size:13px;line-height:1.5;white-space:pre-wrap} +#onboarding .ob-err > svg{width:16px;height:16px;flex:none;margin-top:2px;color:var(--critical)} +#onboarding .ob-err > span{min-width:0} +#onboarding .ob-note{display:flex;align-items:center;gap:10px;max-width:720px;padding:10px 14px;border-radius:18px; + font-size:13px;line-height:1.45;color:var(--ink);white-space:normal} +#onboarding .ob-note > svg{width:16px;height:16px;flex:none} +#onboarding .ob-note.is-blue{background:color-mix(in srgb,var(--brand) 9%,var(--lift))} +#onboarding .ob-note.is-blue > svg{color:var(--blue)} +#onboarding .ob-note.is-amber{background:color-mix(in srgb,var(--amber) 13%,var(--lift))} +#onboarding .ob-note.is-amber > svg{color:var(--amber-text)} + +/* ---------------- the cloud wizard (ON-09…15) ---------------- */ +#onboarding .ob-wiz{flex:1 1 auto;min-height:0;gap:12px} +#onboarding .ob-wiz > *{max-width:720px;flex-shrink:0} +#onboarding .ob-wiz > .ob-wizlist{flex:0 1 auto;min-height:120px;margin:0 -16px;padding:4px 16px 14px;max-width:752px} +#onboarding .ob-wiz > .ob-foot{max-width:none;margin-top:auto} +#onboarding .ob-wizlist .prows{padding:6px;border-radius:var(--r-card);background:var(--lift);box-shadow:var(--sh-card)} +#onboarding .prow .nm{font-size:14.5px} +#onboarding .prow .ann.lit{height:22px} +#onboarding [data-wizmodel] .ann.lit{background:var(--brand-wash);color:var(--blue)} +#onboarding .prow-more{margin:14px 0 8px 10px} +#onboarding .ob-wiz .ob-kicker{margin-bottom:-10px} +#onboarding .ob-wiz .ob-h{gap:14px;font:600 34px/1.15 var(--font-ui);letter-spacing:-.04em;color:var(--ink)} +#onboarding .ob-wiz .ob-h .logo,#onboarding .ob-wiz .ob-h .tk-ico{width:44px;height:44px} +#onboarding .ob-wiz .ob-h .tk-ico svg{width:20px;height:20px} +#onboarding .ob-wiz .ob-flabel{margin:6px 0 -4px 2px;font:600 13px/1.4 var(--font-ui);color:var(--ink);letter-spacing:0;text-transform:none} +#onboarding .ob-wiz .ob-field{margin-top:10px} +#onboarding .ob-wiz .ob-err{padding:0 0 0 4px;background:transparent;color:var(--critical);font-size:12.5px} +#onboarding .ob-wiz .ob-err > svg{width:14px;height:14px;margin-top:2px} +#onboarding .ob-wiz .ob-err.is-warn,#onboarding .ob-wiz .ob-err.is-warn > svg{color:var(--amber-text)} + +/* ---------------- the action bar and the hint strip ---------------- */ +#onboarding .ob-foot{flex:none;display:flex;align-items:center;gap:10px;padding-top:16px;border:0} +#onboarding .ob-foot .grow{flex:1} +#onboarding .ob-foot .btn{height:44px;padding:0 22px;font-size:14.5px} +#onboarding .ob-foot .btn svg{width:16px;height:16px} +#onboarding .ob-foot .btn-p{min-width:112px} +#onboarding .ob-foot .btn .tk-spin{width:14px;height:14px} +/* The quiet action keeps a hairline edge at rest: a Back that reads as a + label until the pointer is on it was a tester finding, and + onboarding-mouse.mjs still measures it. */ +#onboarding .ob-foot .btn-g{padding:0 18px 0 14px;border:1px solid var(--bd-2)} +#onboarding .ob-foot .btn-g:hover{background:var(--hover);color:var(--ink)} +#onboarding .ob-hints{position:static;flex:none;display:flex;align-items:center;justify-content:center;flex-wrap:wrap; + gap:2px 12px;min-height:40px;height:auto;padding:10px 0 0;border:0;background:transparent;font-size:12px;color:var(--ink-2)} +#onboarding .ob-hints .hint{display:inline-flex;align-items:center;gap:6px;height:24px;margin:0;padding:0 6px;border-radius:8px; + font-size:12px;letter-spacing:0} +#onboarding .ob-hints .hint-live{font:inherit;color:inherit;background:transparent;border:0;cursor:pointer; + transition:background var(--t-state) var(--ease),color var(--t-state) var(--ease)} +#onboarding .ob-hints .hint-live:hover{background:var(--hover);color:var(--ink)} +#onboarding .ob-hints .kc{margin:0} +#onboarding .ob-hints .kc + .kc{margin-left:-2px} + +/* ---------------- the download (ON-07, ON-08, ON-18) ---------------- + `.ob-dlprog` is the one box a progress sample repaints (refreshDlProgress); + its rows are fixed-width and never wrap, so a sample cannot move the + offer card a pointer rests on. */ +#onboarding .ob-dlprog{display:grid;gap:14px;max-width:640px;padding:18px 20px 16px;border-radius:var(--r-card); + background:var(--lift);box-shadow:var(--sh-card)} +#onboarding .ob-phase{display:grid;grid-template-columns:24px max-content minmax(48px,1fr) 23ch;align-items:center;gap:12px;white-space:nowrap} +#onboarding .ob-phase .tk-ico{width:24px;height:24px} +#onboarding .ob-phase .tk-ico svg{width:13px;height:13px} +#onboarding .ob-phase .pl{min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:14px;font-weight:600;color:var(--ink)} +#onboarding .ob-phase .pb{height:8px;min-width:0} +#onboarding .ob-phase .pb i{font-style:normal;color:var(--brand);background:currentColor} +#onboarding .ob-phase.waiting .pb i{color:var(--ink-3)} +#onboarding .ob-phase.done .pb{visibility:hidden} +#onboarding .ob-phase .pt{min-width:0;text-align:right;white-space:pre;font:400 12px/1 var(--font-mono); + font-variant-numeric:tabular-nums;color:var(--ink-2)} +#onboarding .ob-rate{margin:0;font:400 12px/1.4 var(--font-mono);color:var(--ink-2)} +#onboarding .ob-dlprog > .ob-err{padding:0;background:transparent;color:var(--critical)} +#onboarding .ob-offer:not(.ob-offer-inline){display:flex;align-items:center;gap:14px;width:100%;max-width:640px;min-height:64px;margin:0; + padding:12px 16px;border:0;border-radius:var(--r-block);background:var(--lift);box-shadow:var(--sh-card); + color:var(--ink);text-align:left;cursor:pointer; + transition:background var(--t-state) var(--ease),box-shadow var(--t-state) var(--ease)} +#onboarding .ob-offer:not(.ob-offer-inline):hover{background:color-mix(in srgb,var(--ink) 3%,var(--lift))} +#onboarding .ob-offer:not(.ob-offer-inline):active{background:color-mix(in srgb,var(--ink) 6%,var(--lift))} +#onboarding .ob-offer:focus-visible{outline:2px solid var(--brand);outline-offset:2px} +#onboarding .ob-offer .ob-rbody{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} +#onboarding .ob-offer .t{font-size:14.5px;font-weight:600;color:var(--ink)} +#onboarding .ob-offer .d{font-size:13px;line-height:1.45;color:var(--ink-2)} +#onboarding .ob-offer > .kc{margin-left:auto} + +/* ---------------- the import (ON-20…23) ---------------- */ +#onboarding .ob-row .box{width:18px;height:18px;margin:0;border:0;border-radius:6px;display:inline-grid;place-items:center;flex:none; + background:transparent;box-shadow:inset 0 0 0 1.5px var(--ink-3);color:var(--on-strong); + transition:background var(--t-state) var(--ease),box-shadow var(--t-state) var(--ease)} +#onboarding .ob-row .box svg{width:12px;height:12px;stroke-width:2.6} +#onboarding .ob-row .box.on{background:var(--brand);box-shadow:none} +#onboarding .ob-row.ob-check:hover .box:not(.on){box-shadow:inset 0 0 0 1.5px var(--brand)} +#onboarding .ob-check .d{font:400 12px/1.45 var(--font-mono)} +#onboarding .ob-models > .ob-explain{padding:10px 12px;font-size:13.5px} +#onboarding .ob-explain.ob-help{font-size:12.5px;line-height:1.45;max-width:62ch;padding-left:4px} +#onboarding .prow > .tk-ico{width:30px;height:30px} +#onboarding .ob-h.ob-lede{font:400 15.5px/1.5 var(--font-ui);letter-spacing:0;color:var(--ink-2)} +#onboarding .ob-out > :not(.logo):not(.tk-ico){grid-column:2} +#onboarding .ob-applogo svg{width:100%;height:100%} +#onboarding .ob-list.is-busy{opacity:.7} +#onboarding .ob-scanstrip{display:flex;align-items:center;gap:10px;max-width:680px;padding:10px 14px;border-radius:18px; + background:color-mix(in srgb,var(--amber) 13%,var(--lift));font-size:13px;color:var(--ink)} +#onboarding .ob-scanstrip .tk-spin{border-color:var(--amber-wash);border-top-color:var(--amber)} +#onboarding .ob-scanstrip .ob-scan-k{font-weight:700} +#onboarding .ob-scanstrip .readout{font:400 12px/1 var(--font-mono);color:var(--ink);min-width:0} +#onboarding .ob-scanstrip .ob-help{padding:0;font-size:13px;color:var(--ink-2)} +#onboarding .ob-summary{display:grid;gap:8px;max-width:700px;padding:16px 18px;border-radius:var(--r-card); + background:var(--lift);box-shadow:var(--sh-card);white-space:normal;font-size:14px;color:var(--ink-2)} +#onboarding .ob-sumrow{display:grid;grid-template-columns:130px minmax(0,1fr);gap:12px} +#onboarding .ob-sumrow .k{font-weight:600;color:var(--ink)} +#onboarding .ob-sumrow .v.is-warn{color:var(--amber-text)} +#onboarding .ob-failures{display:flex;flex-wrap:wrap;gap:8px} +#onboarding .ob-failtbl{max-width:700px;padding:4px 6px 8px;border-radius:var(--r-card);background:var(--lift);box-shadow:var(--sh-card)} +#onboarding .ob-failtbl td:first-child{font:500 12px/1 var(--font-mono)} + +/* Hugging Face file pick (ON-06). */ +#onboarding .ob-repo{display:flex;align-items:center;gap:10px;font:500 14px/1.3 var(--font-mono);letter-spacing:0;color:var(--ink)} +#onboarding .ob-file .t{font:500 13px/1.35 var(--font-mono)} +#onboarding .ob-file .ob-size{margin-left:auto;font:400 12px/1 var(--font-mono);color:var(--ink-2);white-space:nowrap} + +/* A list that scrolls must say so. macOS hides its overlay scrollbar until + something moves, so the long lists ended in a row sliced in half with no + hint of more. Deliberately the -webkit- pseudo-elements and NOT + `scrollbar-width` / `scrollbar-color`: either standard property makes + Chromium fall back to the invisible overlay bar. */ #onboarding .ob-scroll::-webkit-scrollbar,#onboarding .ob-wizlist::-webkit-scrollbar{width:10px} #onboarding .ob-scroll::-webkit-scrollbar-thumb,#onboarding .ob-wizlist::-webkit-scrollbar-thumb{ - background:var(--line-strong);border:3px solid transparent;background-clip:content-box;border-radius:0; -} + background:var(--bd-2);border:3px solid transparent;background-clip:content-box;border-radius:var(--r-pill)} #onboarding .ob-scroll::-webkit-scrollbar-thumb:hover,#onboarding .ob-wizlist::-webkit-scrollbar-thumb:hover{ - background:var(--text-disabled);background-clip:content-box; + background:var(--ink-3);background-clip:content-box} + +/* ---------------- smaller windows ---------------- + The rail narrows, then gives up its statement on a short window; every + control stays on screen down to the 940×620 minimum. */ +@media (max-width:1240px){ + #onboarding{grid-template-columns:340px minmax(0,1fr)} + #onboarding .ob{padding:44px 44px 12px} + #onboarding .ob-statement{font-size:27px} } - -/* ========================================================================== - r6 UX — the secondary control has to look like a control - ========================================================================== - - Driving the wizard screen by screen turned up the brief's own rule - ("no control that looks like a label") broken in the two places a - pointer needs most. Both drew their border as `transparent` at rest - and revealed it only on hover — which is a discoverability trick that - only works for a pointer that is ALREADY on the control. Until then - `Back`, `Skip setup for now` and `Clear` are grey text on the surface, - sitting next to real prose, with nothing to say they can be pressed. - - The app already owns the answer: `.btn-s` — its standard secondary - button — rests at `1px solid var(--line)` and strengthens on hover. - Using that here is the app's existing idiom, not a new look: the same - token, the same weight, the same hover. Nothing else moves. */ -.ob-foot .btn-g{border-color:var(--line)} -.ob-foot .btn-g:hover{border-color:var(--line-strong)} - -/* `Clear` was 46x23px. The wizard's own floor is 32px for a row and 34 - for a field, and 23px is under every desktop platform's minimum for a - pointer target — on the one screen where a mouse user has to undo a - typo. It keeps its quiet inline shape and its 12px type; it just - becomes big enough to hit and outlined enough to find. */ -.ob-offer.ob-offer-inline{ - border:1px solid var(--line);min-height:26px;padding:3px 10px;align-items:center; +@media (max-width:1060px){ + #onboarding{grid-template-columns:272px minmax(0,1fr)} + #onboarding .ob-rail{padding:56px 20px 20px;gap:18px} + #onboarding .ob-statement{font-size:24px} + #onboarding .ob{padding:36px 32px 10px} + #onboarding .ob-title,#onboarding .ob-wiz .ob-h{font-size:28px} + #onboarding .ob-phase{gap:10px} } -.ob-offer.ob-offer-inline:hover{border-color:var(--line-strong)} - -/* ============================================================ - r6 cloud item 1 — the onboarding hint strip's `esc` chord is a real - control now, not decoration. It must keep the strip's exact metrics - (a '; + + (lead || '') + + '' + label + '' + + (detail ? '' + detail + '' : '') + '' + + (trail || '') + ''; } /** describeDownloadingModel (local-model-picks.ts:139-145). */ @@ -6914,10 +6951,18 @@ function obOutOfReach() { } function obChooseHTML() { - const rows = OB_CHOICES.map((choice, i) => - obRow(i, OB.cursor === i, esc(choice.label), esc(choice.detail[0]) + '
' + esc(choice.detail[1]))).join(''); - return '
' + esc(OB_COPY.chooseExplainer.join('\n')) + '
' - + '
' + rows + '
'; + /* Route cards: where it runs, the route, one line, and the digit that + jumps to it. */ + const look = {local:['laptop', ' tk-ico--indigo'], cloud:['cloud', ' tk-ico--blue'], custom:['server', '']}; + const rows = OB_CHOICES.map((choice, i) => { + const l = look[choice.id] || ['server', '']; + return obRow(i, OB.cursor === i, esc(choice.label), + esc(choice.detail[0]) + (choice.detail[1] ? '
' + esc(choice.detail[1]) : ''), '', '', + '' + ic(l[0]) + '', + '' + (i + 1) + ''); + }).join(''); + return '
' + esc(OB_COPY.chooseExplainer.join(' ')) + '
' + + '
' + rows + '
'; } function obLocalPickHTML() { @@ -6925,35 +6970,29 @@ function obLocalPickHTML() { const models = rows.filter((r) => r.kind === 'model'); const onHf = OB.cursor >= models.length; /* r6 UX: windowLocalPicks paints six rows and says `↓ 7 more`, because - a terminal cannot scroll a region. This one can — and while the - window was a mouse DEAD END (there is no gesture that reaches the - seventh model; only an arrow key moves the window), a scroller is - the desktop's own list. Every row is drawn, the box scrolls, and the - cursor is kept in view for the keyboard — which behaves exactly as - it did, six rows at a time or not. */ + a terminal cannot scroll a region. This one can: every row is drawn, + the box scrolls, and the cursor is kept in view for the keyboard. */ const best = bestModelFor(OB.models, OB.ram); const body = models.length ? models.map((row, at) => { const model = row.model; return obRow(at, !onHf && at === OB.cursor, obModelRowLabel(model, best && model.id === best.id), - obModelRowDetail(model)); + obModelRowDetail(model), '', '', modelMark(model.id)); }).join('') : '
' + (OB.busy ? 'reading the catalogue…' : obNothingFitsLine()) + '
'; const hf = obRow(models.length, onHf, esc(HF_ROW_LABEL), - esc('paste an owner/repo id or a huggingface.co URL')); + esc('paste an owner/repo id or a huggingface.co URL'), 'ob-hfrow', '', logoHTML('huggingface', 'sm')); return '
' + esc('One download, then it runs offline. This machine reports ' + OB.ram + ' GB of RAM, ' + 'and the list below is ordered for it — the best fit first.') + '
' - + '
' + esc(OB_COPY.localHeading) + '
' + + '
' + esc(OB_COPY.localHeading) + '
' /* The out-of-reach block lives INSIDE the scroller, not beside it. - Beside it, it collapsed the list to nothing: `.ob-models` carries - `overflow-y:auto`, which sets its `min-height` to 0, so as a flex - item next to a sibling that cannot shrink it absorbed every pixel of - shrinkage and rendered 0px tall — three model rows present in the - DOM, readable by script, and INVISIBLE on screen. Caught in a - screenshot of the 8 GB pass; the 68 GB pass has no such sibling and - looked perfect. `min-height` below is the second belt. */ + Beside it, it collapsed the list to nothing: an `overflow-y:auto` flex + item next to a sibling that cannot shrink absorbs every pixel of + shrinkage and renders 0px tall — rows present in the DOM, readable by + script, and invisible on screen. `min-height` on `.ob-models` is the + second belt. */ + '
' + body + obOutOfReachHTML() + '
' + hf; } @@ -7011,7 +7050,7 @@ function obOutOfReachHTML() { if (!out.length) return ''; return '
' + esc('Needs a bigger machine than this one') + '
' + '
' + out.map((model) => - '
' + '
' + modelMark(model.id, 'xs') + '' + esc(model.id) + '' + (model.description ? '' + esc(model.description) + '' : '') + '' + esc(fitFor(model, OB.ram).label) + '
').join('') @@ -7024,24 +7063,26 @@ function obErrorIsInline() { } /** The step error, rendered where the step wants it. */ function obInlineErrHTML() { - return OB.error ? '
' + esc(OB.error) + '
' : ''; + return OB.error ? '
' + ic('alert') + '' + esc(OB.error) + '
' : ''; } /** hf-reference-editor.tsx, as its own step rather than a jump into Settings. */ function obHfRefHTML() { /* r6 UX: a `
' + obInlineErrHTML() - + (OB.busy ? '
' + esc(OB_COPY.urlProbing) + '
' : ''); + + (chat ? '' : '
' + esc(OB_COPY.urlEmbeddingNote) + '
') + + (OB.busy ? '
' + esc(OB_COPY.urlProbing) + '
' : ''); } /** onboarding-download-step.tsx:18-38 / :87-124. */ @@ -7089,24 +7136,45 @@ function obDownloadHTML() { // offerCloudMeanwhile: "hidden once a cloud provider is configured — // nothing left to offer" (:110-111). const offerCloud = !OB.cloudReady; + /* r6 UX: these two ARE this screen's buttons — the only way off it short + of waiting — so they are cards a mouse can see, not the terminal's `┃` + rule around a paragraph. The copy and the `c` / `s` chords are the + TUI's, unchanged. */ const cloud = offerCloud - ? '' + ? obOfferHTML(' cloud', 'key:c', '' + ic('cloud') + '', + failed ? [OB_COPY.cloudOfferFailed] : OB_COPY.cloudOffer, OB_COPY.cloudOfferKey) : ''; - /* r6 UX: these two ARE this screen's buttons — the only way off it - short of waiting — so they are drawn as cards a mouse can see, not as - the terminal's `┃` rule around a paragraph. The copy and the `press - c` / `press s` chords are the TUI's, unchanged. */ - const skip = ''; + const skip = obOfferHTML('', 'key:s', '' + ic('arrowR') + '', + failed ? [OB_COPY.skipOfferFailed] : OB_COPY.skipOffer, OB_COPY.skipOfferKey); return '
' + esc(failed ? 'The ' + label + ' download failed.' : 'Downloading ' + label + '. You can leave this running.') + '
' + obProgressBlockHTML() + cloud + skip; } +/** + * One download offer as a card: an icon, the offer as a title and a detail + * line, and the chord's keycap. The copy is the TUI's two wrapped lines, + * rejoined and split where it reads: a first line ending in a dash is the + * title, otherwise the first sentence is. + */ +function obOfferHTML(cls, spec, icon, lines, chord) { + const text = lines.join(' '); + let title = text; + let detail = ''; + if (lines.length > 1 && /—\s*$/.test(lines[0])) { + title = lines[0].replace(/\s*—\s*$/, ''); + detail = lines.slice(1).join(' '); + } else if (text.indexOf('. ') > 0) { + title = text.slice(0, text.indexOf('. ')); + detail = text.slice(text.indexOf('. ') + 2); + } + return ''; +} + /** onboarding-wait-or-jump-step.tsx:38-55, :139-150. */ function obWaitOrJumpHTML() { const status = dlStatus(); @@ -7118,28 +7186,45 @@ function obWaitOrJumpHTML() { {label:'Add another cloud provider', detail:'one more key or endpoint, then straight back to this screen'}, ]; if (status === 'failed') rows.push({label:'Retry the download', detail:'starts the same download again'}); + const icons = [ + '' + ic('arrowR') + '', + '' + ic('plus') + '', + '' + ic('retry') + '', + ]; const cursor = OB.cursor % rows.length; - return '
' + esc(OB_COPY.cloudReadyLabel) + '
' + return obReadyHTML(OB_COPY.cloudReadyLabel) + (status === 'running' ? '
' + esc('Still downloading ' + label + ' — it keeps running whichever row you pick.') + '
' : '') - + (status === 'ready' - ? '
' + esc(label + ' downloaded — the local model is ready too') + '
' : '') + + (status === 'ready' ? obReadyHTML(label + ' downloaded — the local model is ready too') : '') + (status === 'failed' ? '
' + esc('The ' + label + ' download failed — the cloud model still works.') + '
' : '') + (status === 'ready' ? '' : obProgressBlockHTML()) - + '
' + rows.map((row, i) => obRow(i, cursor === i, esc(row.label), esc(row.detail))).join('') + '
'; + + '
' + rows.map((row, i) => + obRow(i, cursor === i, esc(row.label), esc(row.detail), '', '', icons[i])).join('') + '
'; +} + +/** Something that is done, said once: a green tick and the sentence. */ +function obReadyHTML(text) { + return '
' + ic('check') + '' + + esc(text) + '
'; } /** onboarding-propose-step.tsx:11-33. */ function obProposeHTML() { - const accept = OB.offer === 'local' + const local = OB.offer === 'local'; + const accept = local ? {label:'Set up local models too', detail:'one download, then it runs offline and costs nothing per token'} : {label:'Set up a cloud model too', detail:'an API key and a model — about a minute, for the heavy turns'}; const rows = [accept, OB_COPY.proposeSkip]; - return '
' + esc(obConfiguredLabel()) + '
' - + '
' + esc(OB_COPY.proposeExplainer.join('\n')) + '
' + const icons = [ + local ? '' + ic('laptop') + '' + : '' + ic('cloud') + '', + '' + ic('arrowR') + '', + ]; + return obReadyHTML(obConfiguredLabel()) + + '
' + esc(OB_COPY.proposeExplainer.join(' ')) + '
' + '
' - + rows.map((row, i) => obRow(i, (OB.cursor % 2) === i, esc(row.label), esc(row.detail))).join('') + '
'; + + rows.map((row, i) => obRow(i, (OB.cursor % 2) === i, esc(row.label), esc(row.detail), '', '', icons[i])).join('') + '
'; } /** buildImportPickRows (import-step.ts:101-113): agents, then the import @@ -7170,8 +7255,9 @@ function obScanStripHTML() { const all = OB.importAgents || []; const done = OB.scanDone || 0; const at = OB.scanAt || ''; - return '
' - + 'Scanning' + return '
' + + '' + + 'Scanning' + '' + esc(all.length ? (Math.min(done + 1, all.length) + ' of ' + all.length) : '…') + '' + (at ? '' + esc(at) + '' : '') + '
'; @@ -7180,26 +7266,27 @@ function obScanStripHTML() { function obImportPickHTML() { const rows = obImportRows(); const cursor = OB.cursor % rows.length; - return '
' + esc(OB_COPY.importExplainer.join('\n')) + '
' - + '
' + rows.filter((r) => !r.offList).map((row, i) => { + /* B.5 — the scan is a readout (which source, how far through), so a scan + can be told from a hang. It sits above the list it is reading, and the + list dims while it runs. */ + return '
' + esc(OB_COPY.importExplainer.join(' ')) + '
' + + (OB.busy ? obScanStripHTML() : '') + + '
' + rows.filter((r) => !r.offList).map((row, i) => { if (row.kind === 'agent') { - /* r6 UX: `[x]` and `[ ]` are a terminal's checkbox. This row IS - a checkbox — it toggles, it does not navigate — so it is - drawn as one and announced as one. The tick is the same - state, in the shape a desktop reader already knows. */ - return obRow(i, cursor === i, - '' + esc(row.agent.label), - esc(row.agent.dir), 'ob-check', - ' role="checkbox" aria-checked="' + (row.agent.enabled ? 'true' : 'false') + '"'); + /* r6 UX: this row IS a checkbox — it toggles, it does not + navigate — so it is drawn as one and announced as one. The + source wears its own mark; the terminal app wears ours. */ + const agent = row.agent; + const mark = agent.id === OB_TUI_AGENT_ID + ? '' + : providerMark(providerLogoKey(agent.id) ? agent.id : agent.label); + return obRow(i, cursor === i, esc(agent.label), esc(agent.dir), 'ob-check', + ' role="checkbox" aria-checked="' + (agent.enabled ? 'true' : 'false') + '"', + '' + mark); } return ''; - }).join('') + '
' - /* B.5 — "scanning the sources…" was a grey sentence that said nothing - about progress, so there was no way to tell a scan from a hang ("не было - полосы импорта. Я не понимаю, работает или нет"). It is a readout now: - which source, and how far through. */ - + (OB.busy ? obScanStripHTML() : ''); + }).join('') + '
'; } /** summarizeImportReport (import-step.ts:167-189) + reportHeadline (:200-212). */ @@ -7247,16 +7334,33 @@ function obImportReportHTML(executed) { const report = OB.importReport; const bad = obImportFailures(); const open = OB.importFailuresOpen; - return '
' + esc(obImportHeadline(report, executed)) + '
' - + (report ? '
' + esc(obImportSummary(report).join('\n')) + '
' : '') + /* The headline keeps its words; a leading ✓ or ⚠ becomes the matching + icon. The per-kind summary is a two-column card: kind, then counts. */ + const headline = obImportHeadline(report, executed); + const glyph = /^([✓⚠])\s+/.exec(headline); + const icon = !glyph ? '' + : glyph[1] === '✓' ? '' + ic('check') + '' + : '' + ic('alert') + ''; + const lines = report ? obImportSummary(report) : []; + return '
' + icon + + esc(glyph ? headline.slice(glyph[0].length) : headline) + '
' + + (lines.length + ? '
' + lines.map((line) => { + const at = line.indexOf(': '); + const kind = at > 0 ? line.slice(0, at) : ''; + const counts = at > 0 ? line.slice(at + 2) : line; + return '
' + esc(kind) + '' + + '' + esc(counts) + '
'; + }).join('') + '
' + : '') + (bad.length && !OB.busy ? '
' + '' - + (executed ? '' : '') + + esc((open ? 'Hide' : 'What failed') + ' (' + bad.length + ')') + ic(open ? 'chevU' : 'chevD') + '' + + (executed ? '' : '') + '
' + (open - ? '
' + ? '
' + '' + bad.slice(0, 200).map((i) => '' @@ -7291,21 +7395,28 @@ function wizModelStepHTML(withFoot) { const all = WIZ.models || []; const rows = q ? all.filter((m) => (m.id + ' ' + (m.name || '')).toLowerCase().includes(q)) : all; const shown = rows.slice(0, 200); + /* Soft Tactile: every row wears its model's mark. On the first-run layer + (withFoot) the provider's mark titles the step and search is a pill + with its magnifier; the popover's copy of this step keeps its plain + field and title. */ + const search = ''; + const providerId = WIZ.savedId || (WIZ.row && WIZ.row.id) || ''; return '
' + '
Model
' - + '
' + esc(WIZ.savedLabel || 'Choose a model') + '
' + + '
' + (withFoot ? providerMark(providerId) : '') + esc(WIZ.savedLabel || 'Choose a model') + '
' + (WIZ.unverifiedNote - ? '
' + esc(WIZ.unverifiedNote) + '
' + ? '
' + esc(WIZ.unverifiedNote) + '
' : '') + '
' + esc(WIZ.defaultModel ? 'Our default for this provider is ' + WIZ.defaultModel + '.' : 'Pick the model this provider should answer with.') + '
' + (all.length > 8 - ? '' + ? (withFoot ? '' : search) : '') + '
' + shown.map((m) => '
' + + '
' + '
'; } const k = WIZ.row; const verifying = WIZ.phase === 'verifying'; /* COPY, disclosed rather than silently dropped: providers-wizard.tsx also has an `Embedding backend` (:445) and a `Chat model id` (:450) - screen. The desktop's add-provider wizard has neither phase — it - takes the kind's default chat model and never offers a cloud - embedding backend — so those two titles have nowhere honest to go - and are NOT rendered. `LLM provider — add provider`, `API key — - ${service}` and the .env sentence are all present, verbatim. */ - /* r6 UX: the key box had no label at all — a bare dark rectangle under - a sentence about .env, which is a footnote and not a name. It is - labelled, it says what goes in it, and the .env sentence has moved - BELOW the field, where help text belongs. The sentence itself is the - copy contract's, verbatim. */ + screen. The desktop's add-provider wizard has neither phase, so those + two titles have nowhere honest to go and are NOT rendered. */ if (WIZ.phase === 'pick_model') return wizModelStepHTML(true); - /* B.4 — one label, not three. This screen used to say "API key — AI/ML API" - as a heading, "API key" again as the field's label, and then put the .env - sentence between them; the tester read it as the same words twice and she - was right. The screen is titled in the 11px label style, the provider is - the subhead, and naming the field is the placeholder's job. + /* B.4 — one label, not three: "API key" once as the kicker, the provider + (with its mark) as the title, and naming the field is the placeholder's + job. The .env sentence is the copy contract's, verbatim, under the field. F1 — when the key could not be CHECKED (as opposed to rejected), the two buttons below are the whole decision: try again, or save it knowing it is - unchecked. There is no path from here to a completion screen. */ + unchecked. Unreachable reads amber, rejected reads red. */ const service = k.label.split(' (')[0]; const unchecked = WIZ.uncheckedFor; + const tone = WIZ.error ? (unchecked ? ' is-warn' : ' is-error') : ''; return '
' + '
API key
' - + '
' + esc(service) + '
' + + '
' + providerMark(k.custom ? '' : k.id) + esc(service) + '
' + (k.custom ? '' + '' : '') - + '' - + (WIZ.error ? '
' + esc(WIZ.error) + '
' : '') + + '
' + ic('key') + + '' + + (verifying ? '' : '') + + '
' + + (WIZ.error + ? '
' + ic(unchecked ? 'info' : 'alert') + + '' + esc(WIZ.error) + '
' + : '') + (k.env ? '
' + esc('Saved to .env as ' + k.env + ' (mode 0600).') + '
' : '') + (verifying ? '
Asking ' + esc(service) + ' to answer once with this key…
' : '') + (unchecked ? '
' - + '' + + '' + '' + '' - + '' + + '' + '
' - : '
' + : '
' + '' + '
') + + (verifying ? 'Verifying…' : 'Next' + ic('arrowR')) + '
') + '
'; } @@ -7449,9 +7553,9 @@ function obWizardHTML() { — the same router the keyboard uses. A control cannot drift from a chord because there is nothing for it to drift to. -------------------------------------------------------------------------- */ -function obBtn(spec, label, cls, disabled) { +function obBtn(spec, label, cls, disabled, icon, trail) { return ''; + + (disabled ? ' disabled' : '') + '>' + (icon ? ic(icon) : '') + esc(label) + (trail ? ic(trail) : '') + ''; } /** The verbs of one step, or '' for a step whose rows already carry them. */ @@ -7462,27 +7566,30 @@ function obFootHTML() { switch (OB.step) { case 'choose': left = obBtn('nav:back', 'Skip setup for now', 'btn-g'); + /* Soft Tactile: Enter's verb as a button too. It presses Enter through + obPress, so it opens the route under the cursor, exactly as the key. */ + right = obBtn('nav:go', 'Continue', 'btn-p', false, '', 'arrowR'); break; case 'local_pick': - left = obBtn('nav:back', 'Back', 'btn-g'); + left = obBtn('nav:back', 'Back', 'btn-g', false, 'chevL'); break; case 'local_hf_ref': - left = obBtn('nav:back', busy ? 'Cancel' : 'Back', 'btn-g'); + left = obBtn('nav:back', busy ? 'Cancel' : 'Back', 'btn-g', false, busy ? '' : 'chevL'); right = obBtn('nav:go', busy ? 'Looking it up…' : 'Look it up', 'btn-p', busy || OB.hfReference.length === 0); break; case 'local_hf_pick': - left = obBtn('nav:back', 'Back', 'btn-g'); - right = obBtn('nav:go', 'Download this file', 'btn-p', false); + left = obBtn('nav:back', 'Back', 'btn-g', false, 'chevL'); + right = obBtn('nav:go', 'Download this file', 'btn-p', false, 'download'); break; case 'custom_chat_url': - left = obBtn('nav:back', 'Back', 'btn-g', busy); + left = obBtn('nav:back', 'Back', 'btn-g', busy, 'chevL'); right = obBtn('nav:go', busy ? 'Testing…' : 'Test and continue', 'btn-p', busy); break; case 'custom_embedding_url': - left = obBtn('nav:back', 'Back', 'btn-g', busy) - + obBtn('url:skip', 'Continue without embeddings', 'btn-g', busy); - right = obBtn('nav:go', busy ? 'Testing…' : 'Test and save', 'btn-p', busy); + left = obBtn('nav:back', 'Back', 'btn-g', busy, 'chevL'); + right = obBtn('url:skip', 'Continue without embeddings', 'btn-s', busy) + + obBtn('nav:go', busy ? 'Testing…' : 'Test and save', 'btn-p', busy); break; /* B.5 — the import step's two verbs, on the action bar with every other step's verbs rather than as underlined rows inside the list. Import is @@ -7491,10 +7598,9 @@ function obFootHTML() { case 'import_pick': { const picked = (OB.importAgents || []).filter((a) => a.enabled).length; left = obBtn('import:skip', OB_COPY.importSkipLabel, 'btn-g', busy); - /* Nothing ticked, no import verb. A greyed "Import from 0 agents" is a - control that exists only to be unusable — the row it replaced was - simply absent until something was ticked, and that was right. */ - right = picked === 0 ? '' : obBtn('import:go', busy ? 'Scanning…' : obImportActionLabel(picked), 'btn-p', busy); + /* Nothing ticked, no import verb: a greyed "Import from 0 agents" is a + control that exists only to be unusable. */ + right = picked === 0 ? '' : obBtn('import:go', busy ? 'Scanning…' : obImportActionLabel(picked), 'btn-p', busy, busy ? '' : 'import'); break; } case 'import_preview': { @@ -7505,7 +7611,8 @@ function obFootHTML() { break; } case 'import_done': - right = obBtn('nav:go', 'Start using the agent', 'btn-p', false); + // The go moment of the whole flow: brand blue, not ink. + right = obBtn('nav:go', 'Start using the agent', 'btn-p btn-blue', false, '', 'arrowR'); break; default: return ''; @@ -7537,14 +7644,17 @@ function obHTML() { /* r6 UX: an error belongs beside the control that produced it. The two URL steps and the Hugging Face reference draw their own, directly under the field that was rejected; for every other step the surface - error is the only place it can go, and it sits above the action bar - rather than under it. */ - const err = OB.error && !obErrorIsInline() ? '
' + esc(OB.error) + '
' : ''; + error sits at the end of the body, above the action bar. */ + const err = OB.error && !obErrorIsInline() + ? '
' + ic('alert') + '' + esc(OB.error) + '
' : ''; /* r6 UX: it IS a modal — the app's chrome is behind it and cannot be - operated — so it says so, and Tab is trapped inside it to match. */ + operated — so it says so, and Tab is trapped inside it to match. + Soft Tactile: the indigo rail on the left; on the right the title, a + body that owns the flexible height, the action bar and the hint strip. */ return ''; + + obRailHTML() + + '
' + obHeadHTML() + '
' + body + err + '
' + + obFootHTML() + obHintsHTML() + '
'; } /* ============================================================
ItemSourceWhy
' + esc(i.name || i.id || i.kind || 'item') + '