feat(ui): migrate Settings models/reco/engines styling to shadcn, trim Settings.css (fast mode) (#830)
Migrate the last reducible CSS chunk in Settings.css — the recommendation banner, the models/engines toolbar chrome, and the role-tab/search controls — to Tailwind utilities (chrome tokens kept) at the JSX, following the established shadcn fast-mode convention. Behavior and palette unchanged. What moved to Tailwind: - RecoBanner (.reco-banner* → utilities on models/RecoBanner.jsx) - Models/Engines toolbar (.models-toolbar* → ModelStoreTab.jsx + EnginesTab.jsx), including the previously-unstyled HF-token inline chrome - Role tabs + search (.models-controls/.models-search/.models-roletabs) What was deleted as dead CSS (zero consumers, grep-verified): - the entire .engines-* block (EnginesTab already on shadcn; no consumer) - the .models-table__body > .models-row override (selector no longer matches the body > virtual > row DOM the table renders) What was KEPT as irreducible styling hooks (cannot be utilities): - .models-table* + .models-row* — the virtualized table geometry. Rows are absolutely positioned with an inline translateY from the virtualizer; the table body/virtual spacer and per-cell hooks must stay class-based. Settings.css: 386 → 225 lines (−161). Not deleted (virtualized hooks remain). Verified: vite build ✓, oxlint 0, oxfmt clean, vitest 641/641, visual 48/48, bun install --frozen-lockfile ✓. Live-eyeballed Settings → Models (store + 17-row virtualized table + reco banner) and Engines (matrix + toolbar) against the live backend; rows render correctly and chrome is coherent. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
mergetest
Claude Opus 4.8
parent
ce74ac8777
commit
ff7d8d08dc
@@ -31,8 +31,8 @@ export default function EnginesTab() {
|
||||
|
||||
return (
|
||||
<section className={SETTINGS_SECTION_SURFACE} data-slot="settings-section">
|
||||
<div className="models-toolbar">
|
||||
<div className="models-toolbar__stats">
|
||||
<div className="flex flex-wrap items-center justify-between gap-[var(--space-3)] px-[2px] pb-[6px] pt-[2px] font-[family-name:var(--chrome-font-mono)] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] max-[580px]:flex-col max-[580px]:items-start">
|
||||
<div className="inline-flex flex-wrap items-center gap-[var(--space-2)]">
|
||||
<Segmented
|
||||
size="xs"
|
||||
value={reviewMode}
|
||||
@@ -42,7 +42,7 @@ export default function EnginesTab() {
|
||||
{ value: 'off', label: t('engines.review_off') },
|
||||
]}
|
||||
/>
|
||||
<span className="models-toolbar__sep">·</span>
|
||||
<span className="text-[var(--chrome-fg-dim)]">·</span>
|
||||
<span>{reviewMode === 'on' ? t('engines.banners_on') : t('engines.banners_off')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -406,22 +406,26 @@ export default function ModelStoreTab({ info, modelBadge }) {
|
||||
|
||||
return (
|
||||
<section className={SETTINGS_SECTION_SURFACE} data-slot="settings-section">
|
||||
<div className="models-toolbar">
|
||||
<div className="models-toolbar__stats">
|
||||
<div className="flex flex-wrap items-center justify-between gap-[var(--space-3)] px-[2px] pb-[6px] pt-[2px] font-[family-name:var(--chrome-font-mono)] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] max-[580px]:flex-col max-[580px]:items-start">
|
||||
<div className="inline-flex flex-wrap items-center gap-[var(--space-2)]">
|
||||
<span>
|
||||
<strong>{fmtBytes(data.total_installed_bytes)}</strong>
|
||||
<strong className="font-semibold text-[var(--chrome-fg)]">
|
||||
{fmtBytes(data.total_installed_bytes)}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="models-toolbar__sep">·</span>
|
||||
<span className="models-toolbar__cache" title={data.hf_cache_dir}>
|
||||
<code>{data.hf_cache_dir?.replace(/^\/Users\/[^/]+/, '~')}</code>
|
||||
<span className="text-[var(--chrome-fg-dim)]">·</span>
|
||||
<span title={data.hf_cache_dir}>
|
||||
<code className="font-[family-name:var(--chrome-font-mono)] text-[length:var(--text-xs)] text-[var(--chrome-fg)]">
|
||||
{data.hf_cache_dir?.replace(/^\/Users\/[^/]+/, '~')}
|
||||
</code>
|
||||
</span>
|
||||
{info && <span className="models-toolbar__sep">·</span>}
|
||||
{info && <span className="text-[var(--chrome-fg-dim)]">·</span>}
|
||||
{info && <span>{modelBadge}</span>}
|
||||
{info?.fast_download?.xet_enabled && (
|
||||
<>
|
||||
<span className="models-toolbar__sep">·</span>
|
||||
<span className="text-[var(--chrome-fg-dim)]">·</span>
|
||||
<span
|
||||
className="models-toolbar__fast"
|
||||
className="text-[var(--chrome-accent)]"
|
||||
title={
|
||||
t('models.fast_download_title', {
|
||||
version: info.fast_download.xet_version || 'Xet',
|
||||
@@ -434,11 +438,11 @@ export default function ModelStoreTab({ info, modelBadge }) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="models-toolbar__actions">
|
||||
<div className="inline-flex items-center gap-[var(--space-2)]">
|
||||
{/* Compact HF token inline */}
|
||||
{!hfTokenSet && !hfExpanded && (
|
||||
<button
|
||||
className="models-toolbar__hf-btn"
|
||||
className="inline-flex cursor-pointer items-center gap-1 rounded-[var(--chrome-radius-pill)] [border:1px_solid_var(--chrome-border)] bg-transparent px-[var(--space-2)] py-[2px] text-[var(--chrome-fg-muted)] hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)]"
|
||||
onClick={() => setHfExpanded(true)}
|
||||
title={t('models.hf_set_title')}
|
||||
>
|
||||
@@ -446,10 +450,10 @@ export default function ModelStoreTab({ info, modelBadge }) {
|
||||
</button>
|
||||
)}
|
||||
{!hfTokenSet && hfExpanded && (
|
||||
<div className="models-toolbar__hf-row">
|
||||
<div className="inline-flex items-center gap-[var(--space-2)]">
|
||||
<input
|
||||
type="password"
|
||||
className="models-toolbar__hf-input"
|
||||
className="min-w-0 rounded-[var(--chrome-radius-pill)] [border:1px_solid_var(--chrome-border)] bg-[var(--chrome-input-bg)] px-[var(--space-2)] py-[2px] font-[family-name:var(--chrome-font-mono)] text-[length:var(--text-xs)] text-[var(--chrome-fg)] placeholder:text-[var(--chrome-fg-dim)] focus-visible:border-[var(--chrome-accent)] focus-visible:shadow-[var(--focus-ring)] focus-visible:outline-none"
|
||||
placeholder="hf_xxxxxxxxxxxx"
|
||||
value={hfToken}
|
||||
onChange={(e) => setHfToken(e.target.value)}
|
||||
@@ -470,7 +474,7 @@ export default function ModelStoreTab({ info, modelBadge }) {
|
||||
</Button>
|
||||
<a
|
||||
href="#"
|
||||
className="models-toolbar__hf-link"
|
||||
className="text-[var(--chrome-accent)] no-underline hover:underline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
openExternal('https://huggingface.co/settings/tokens');
|
||||
@@ -482,7 +486,7 @@ export default function ModelStoreTab({ info, modelBadge }) {
|
||||
</div>
|
||||
)}
|
||||
{hfTokenSet && (
|
||||
<span className="models-toolbar__hf-ok">
|
||||
<span className="inline-flex items-center gap-1 text-[var(--chrome-severity-ok)]">
|
||||
<KeyRound size={10} /> ✓
|
||||
</span>
|
||||
)}
|
||||
@@ -507,12 +511,12 @@ export default function ModelStoreTab({ info, modelBadge }) {
|
||||
onInstallRecommended={onInstallRecommended}
|
||||
/>
|
||||
|
||||
<div className="models-controls">
|
||||
<div className="my-[var(--space-2)] flex items-center gap-[var(--space-2)] max-[580px]:flex-col max-[580px]:items-stretch">
|
||||
<Segmented
|
||||
size="sm"
|
||||
value={currentRole}
|
||||
onChange={setActiveRole}
|
||||
className="models-roletabs"
|
||||
className="mb-[6px] mt-[4px]"
|
||||
items={[
|
||||
{
|
||||
value: 'all',
|
||||
@@ -529,7 +533,7 @@ export default function ModelStoreTab({ info, modelBadge }) {
|
||||
/>
|
||||
<SettingsInput
|
||||
type="search"
|
||||
className="models-search"
|
||||
className="max-w-none flex-1 text-[length:var(--text-xs)] min-w-[120px]"
|
||||
placeholder={t('models.search_placeholder')}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
||||
@@ -19,22 +19,24 @@ export default function RecoBanner({
|
||||
if (!reco) return null;
|
||||
if (reco.all_installed) {
|
||||
return (
|
||||
<div className="reco-banner reco-banner--ok">
|
||||
<div className="mb-[var(--space-2)] flex items-center gap-[var(--space-3)] rounded-[var(--chrome-radius-pill)] [border:1px_solid] [border-left-width:2px] [border-color:color-mix(in_srgb,#8ec07c_30%,transparent)] [border-left-color:#8ec07c] bg-[color-mix(in_srgb,#8ec07c_4%,transparent)] px-[var(--space-4)] py-[var(--space-2)] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)]">
|
||||
<CheckCircle size={12} color="#8ec07c" />
|
||||
<span className="flex-1">
|
||||
{t('models.reco_installed_for', { device: reco.device.label })}
|
||||
</span>
|
||||
<span className="reco-banner__gb">{reco.total_gb} GB</span>
|
||||
<span className="text-[length:var(--text-2xs)] text-[var(--chrome-fg-dim)]">
|
||||
{reco.total_gb} GB
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="reco-banner reco-banner--pending">
|
||||
<div className="reco-banner__top">
|
||||
<span className="reco-banner__title">
|
||||
<div className="mb-[var(--space-2)] flex flex-col items-stretch gap-[var(--space-2)] rounded-[var(--chrome-radius-pill)] [border:1px_solid] [border-left-width:2px] [border-color:color-mix(in_srgb,#f3a5b6_25%,transparent)] [border-left-color:#f3a5b6] bg-[linear-gradient(135deg,color-mix(in_srgb,#f3a5b6_4%,transparent),color-mix(in_srgb,#d3869b_2%,transparent))] px-[var(--space-4)] pb-[var(--space-4)] pt-[var(--space-3)] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] shadow-[0_0_12px_color-mix(in_srgb,#f3a5b6_6%,transparent)]">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[length:var(--text-md)] font-semibold text-[var(--chrome-fg)]">
|
||||
{t('models.reco_for', { device: reco.device.label })}
|
||||
</span>
|
||||
<div className="reco-banner__btns">
|
||||
<div className="flex flex-shrink-0 gap-1">
|
||||
{(() => {
|
||||
const requiredMissing = reco.models.filter((m) => m.required && !m.installed);
|
||||
const requiredGb = requiredMissing.reduce((s, m) => s + m.size_gb, 0);
|
||||
@@ -77,15 +79,23 @@ export default function RecoBanner({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="reco-banner__grid">
|
||||
<div className="grid grid-cols-2 gap-x-[var(--space-5)] gap-y-0 text-[length:var(--text-sm)] leading-[1.6]">
|
||||
{reco.models.map((m) => (
|
||||
<span
|
||||
key={m.repo_id}
|
||||
className={`reco-banner__model ${m.installed ? 'reco-banner__model--ok' : ''}`}
|
||||
className={`inline-flex items-center gap-1 overflow-hidden text-ellipsis whitespace-nowrap ${
|
||||
m.installed ? 'text-[var(--chrome-fg)]' : 'text-[var(--chrome-fg-muted)]'
|
||||
}`}
|
||||
>
|
||||
{m.installed ? '✓' : '○'} {m.label}
|
||||
<span className="reco-banner__model-size">{m.size_gb}</span>
|
||||
{m.required && <span className="reco-banner__req">{t('models.req_tag')}</span>}
|
||||
<span className="font-[family-name:var(--chrome-font-mono)] text-[length:var(--text-2xs)] text-[var(--chrome-fg-dim)]">
|
||||
{m.size_gb}
|
||||
</span>
|
||||
{m.required && (
|
||||
<span className="rounded-[999px] [border:1px_solid_color-mix(in_srgb,#d3869b_30%,transparent)] px-[3px] py-0 text-[length:var(--text-2xs)] uppercase leading-[1.5] tracking-[0.04em] text-[#d3869b]">
|
||||
{t('models.req_tag')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -64,51 +64,6 @@
|
||||
`@max-[600px]/settings:` row-stacking variant depends on) is Tailwind on
|
||||
Settings.jsx now — see the content `<div>` there. */
|
||||
|
||||
/* ── Engines tab ─────────────────────────────────────────────── */
|
||||
.engines-family { margin-bottom: var(--space-5); }
|
||||
.engines-family__active {
|
||||
margin-left: var(--space-4);
|
||||
font-weight: 400;
|
||||
color: var(--chrome-fg-dim);
|
||||
font-size: var(--text-xs);
|
||||
font-family: var(--chrome-font-mono);
|
||||
}
|
||||
.engines-family__active code { color: var(--chrome-fg); }
|
||||
|
||||
.engines-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.engines-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: transparent;
|
||||
border: 1px solid var(--chrome-border);
|
||||
border-left: 2px solid var(--chrome-border-strong);
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-md);
|
||||
color: var(--chrome-fg-muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.engines-list li.is-ok {
|
||||
border-left-color: var(--chrome-severity-ok);
|
||||
background: color-mix(in srgb, var(--chrome-severity-ok) 5%, transparent);
|
||||
}
|
||||
.engines-list li.is-off {
|
||||
border-left-color: var(--chrome-severity-warn);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.engines-list__name { flex: 1; min-width: 240px; color: var(--chrome-fg); }
|
||||
.engines-list__name code { color: var(--chrome-fg); font-family: var(--chrome-font-mono); font-size: var(--text-sm); }
|
||||
.engines-list__reason { flex-basis: 100%; font-size: var(--text-xs); color: var(--chrome-fg-dim); font-family: var(--font-sans); padding-left: var(--space-4); }
|
||||
|
||||
/* ── Models tab ──────────────────────────────────────────────── */
|
||||
.models-row__tag {
|
||||
margin-left: var(--space-2);
|
||||
@@ -123,111 +78,11 @@
|
||||
background: color-mix(in srgb, #d3869b 8%, transparent);
|
||||
}
|
||||
|
||||
/* ── Models tab: toolbar + role tabs + table ─────────────────── */
|
||||
.models-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: 2px 2px 6px;
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--chrome-fg-muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.models-toolbar__stats {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.models-toolbar__stats strong { color: var(--chrome-fg); font-weight: 600; }
|
||||
.models-toolbar__sep { color: var(--chrome-fg-dim); }
|
||||
.models-toolbar__cache code { color: var(--chrome-fg); font-family: var(--chrome-font-mono); font-size: var(--text-xs); }
|
||||
|
||||
.models-roletabs { margin: 4px 0 6px; }
|
||||
|
||||
/* ── Compact recommendation banner ───────────────────────────────── */
|
||||
.reco-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--chrome-fg-muted);
|
||||
border: 1px solid;
|
||||
border-left-width: 2px;
|
||||
}
|
||||
.reco-banner--ok {
|
||||
border-color: color-mix(in srgb, #8ec07c 30%, transparent);
|
||||
border-left-color: #8ec07c;
|
||||
background: color-mix(in srgb, #8ec07c 4%, transparent);
|
||||
}
|
||||
.reco-banner--pending {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-4) var(--space-4);
|
||||
border-color: color-mix(in srgb, #f3a5b6 25%, transparent);
|
||||
border-left-color: #f3a5b6;
|
||||
background: linear-gradient(135deg, color-mix(in srgb, #f3a5b6 4%, transparent), color-mix(in srgb, #d3869b 2%, transparent));
|
||||
box-shadow: 0 0 12px color-mix(in srgb, #f3a5b6 6%, transparent);
|
||||
}
|
||||
.reco-banner__gb {
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--chrome-fg-dim);
|
||||
}
|
||||
.reco-banner__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.reco-banner__title {
|
||||
font-weight: 600;
|
||||
font-size: var(--text-md);
|
||||
color: var(--chrome-fg);
|
||||
}
|
||||
.reco-banner__btns {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.reco-banner__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0 var(--space-5);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.reco-banner__model {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--chrome-fg-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.reco-banner__model--ok { color: var(--chrome-fg); }
|
||||
.reco-banner__model-size {
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--chrome-fg-dim);
|
||||
}
|
||||
.reco-banner__req {
|
||||
font-size: var(--text-2xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #d3869b;
|
||||
padding: 0 3px;
|
||||
border: 1px solid color-mix(in srgb, #d3869b 30%, transparent);
|
||||
border-radius: 999px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Models tab: table (virtualised) ─────────────────────────────
|
||||
The toolbar / role tabs / search chrome and the recommendation
|
||||
banner are Tailwind on their components now (ModelStoreTab.jsx,
|
||||
EnginesTab.jsx, models/RecoBanner.jsx). What remains here is the
|
||||
virtualised-table geometry the utilities can't carry. */
|
||||
/* Table — dense rows, fixed-width action column. */
|
||||
.models-table { border-radius: var(--chrome-radius-pill); }
|
||||
.models-table .ui-table-header { padding: 5px var(--space-3); }
|
||||
@@ -349,13 +204,6 @@
|
||||
color: var(--chrome-severity-err); font-size: var(--text-xs);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.models-controls {
|
||||
display: flex; align-items: center; gap: var(--space-2);
|
||||
margin: var(--space-2) 0;
|
||||
}
|
||||
/* Visuals come from SettingsInput; .models-search only adds the in-toolbar flex
|
||||
sizing + lifts the primitive's 360px cap so the search fills the toolbar. */
|
||||
.models-search { flex: 1; min-width: 120px; max-width: none; font-size: var(--text-xs); }
|
||||
.models-row__size { font-family: var(--chrome-font-mono); font-size: var(--text-xs); color: var(--chrome-fg-muted); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.models-row__actions {
|
||||
@@ -373,14 +221,5 @@
|
||||
.models-row__name { flex: 1 1 100%; }
|
||||
.models-row__size { width: auto !important; text-align: left !important; }
|
||||
.models-row__actions { margin-left: auto; }
|
||||
.models-toolbar { flex-direction: column; align-items: flex-start; }
|
||||
.models-controls { flex-direction: column; align-items: stretch; }
|
||||
}
|
||||
|
||||
/* ── Engines tab: rows are NOT virtualised, so override the absolute
|
||||
positioning that .models-row inherits from the Models tab. ───── */
|
||||
[data-slot='settings-section'] > .models-table > .models-table__body > .models-row {
|
||||
position: relative;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user