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:
Palash Debnath
2026-07-01 03:23:40 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent ce74ac8777
commit ff7d8d08dc
4 changed files with 50 additions and 197 deletions
@@ -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>
+5 -166
View File
@@ -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;
}