Skip to content

Commit b336d9c

Browse files
committed
feat(react-ui): polish Manage page with kebab menus and gallery rows
Bring the System / Manage page up to the visual standard of the Install gallery so installed models and backends stop reading like a debug dump. - Unified ResourceRow anatomy (icon, name+description, badges, status, expandable detail) shared across both tabs. - Gallery enrichment cross-references installed names against the gallery list endpoints to surface icons, descriptions, license, tags, and links with a graceful "no description" fallback for custom imports. - Header summary with four StatCards (Models / Backends / Running / Updates) — clickable to switch tab + pre-set filter. - Backends meta + development entries hidden by default; "Show meta & development" paired toggle in the FilterBar with hidden-count hint. - Kebab (three-dot) ActionMenu replaces the inline button cluster on every row; restrained until hover, keyboard-navigable, danger items separated by a divider. - Backend "Version" cell now falls back to short digest, OCI tag, or ocifile basename when no semver is set, instead of showing "—" for every OCI install. Detail panel exposes full Source URI + Digest. - Drop redundant column headers ("Actions", "On") — kebabs and toggles carry their own affordance; screen readers still get a label. - Inline System / User / Meta / Dev badges next to the backend name so the dedicated Type column doesn't reserve space for "USER" repeated. - Tightened the spacing between the System Resources card and the StatCards so they no longer crowd the RAM bar. Extracted StatCard and GalleryLoader from Nodes.jsx and Models.jsx into shared components so the visual language is one source of truth. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-4-7 [Read] [Edit] [Write] [Bash]
1 parent f384c64 commit b336d9c

12 files changed

Lines changed: 1412 additions & 371 deletions

File tree

core/http/react-ui/src/App.css

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6195,6 +6195,304 @@ select.input {
61956195
margin-left: 4px;
61966196
}
61976197

6198+
/* ResourceRow — unified expandable row anatomy used by the Manage page so
6199+
installed models and backends share the same visual grammar as the Install
6200+
gallery (icon, name, description, badges, expandable detail panel). */
6201+
.resource-row { transition: background var(--duration-fast) var(--ease-default); }
6202+
.resource-row.is-dimmed { opacity: 0.55; transition: opacity 0.2s; }
6203+
.resource-row.is-expanded { background: var(--color-bg-tertiary); }
6204+
6205+
.resource-row__chevron-cell { width: 30px; }
6206+
.resource-row__icon-cell { width: 64px; }
6207+
6208+
.resource-row__icon {
6209+
width: 48px;
6210+
height: 48px;
6211+
border-radius: var(--radius-md);
6212+
border: 1px solid var(--color-border-subtle);
6213+
background: var(--color-bg-primary);
6214+
display: flex;
6215+
align-items: center;
6216+
justify-content: center;
6217+
overflow: hidden;
6218+
flex-shrink: 0;
6219+
}
6220+
.resource-row__icon > img {
6221+
width: 100%;
6222+
height: 100%;
6223+
object-fit: cover;
6224+
}
6225+
.resource-row__icon > i {
6226+
font-size: 1.25rem;
6227+
color: var(--color-accent);
6228+
}
6229+
6230+
.resource-row__detail-row > .resource-row__detail-cell {
6231+
padding: 0;
6232+
background: var(--color-bg-primary);
6233+
border-top: 1px solid var(--color-border-subtle);
6234+
}
6235+
.resource-row__detail {
6236+
padding: var(--spacing-md) var(--spacing-lg);
6237+
}
6238+
.resource-row__detail h4 {
6239+
font-size: var(--text-xs);
6240+
font-weight: 600;
6241+
letter-spacing: 0.06em;
6242+
text-transform: uppercase;
6243+
color: var(--color-text-muted);
6244+
margin: 0 0 var(--spacing-sm) 0;
6245+
display: flex;
6246+
align-items: center;
6247+
gap: var(--spacing-xs);
6248+
}
6249+
.resource-row__detail h4 i { color: var(--color-accent); }
6250+
.resource-row__detail-grid {
6251+
display: grid;
6252+
grid-template-columns: max-content 1fr;
6253+
gap: 6px var(--spacing-md);
6254+
}
6255+
.resource-row__detail-grid dt {
6256+
color: var(--color-text-muted);
6257+
font-size: var(--text-xs);
6258+
text-transform: uppercase;
6259+
letter-spacing: 0.06em;
6260+
align-self: start;
6261+
padding-top: 2px;
6262+
}
6263+
.resource-row__detail-grid dd {
6264+
margin: 0;
6265+
font-size: var(--text-sm);
6266+
color: var(--color-text-primary);
6267+
min-width: 0;
6268+
word-break: break-word;
6269+
}
6270+
.resource-row__detail-md {
6271+
color: var(--color-text-secondary);
6272+
line-height: 1.6;
6273+
font-size: var(--text-sm);
6274+
}
6275+
.resource-row__detail-md p:first-child { margin-top: 0; }
6276+
.resource-row__detail-md p:last-child { margin-bottom: 0; }
6277+
6278+
/* Description line directly under the row name — Install gallery already
6279+
does this; Manage rows used to show only the bare name. */
6280+
.resource-row__desc {
6281+
display: block;
6282+
font-size: var(--text-xs);
6283+
color: var(--color-text-muted);
6284+
max-width: 42ch;
6285+
overflow: hidden;
6286+
text-overflow: ellipsis;
6287+
white-space: nowrap;
6288+
margin-top: 2px;
6289+
}
6290+
6291+
/* ResourceActions — split lifecycle vs destructive with a thin divider so
6292+
the trash icon doesn't sit at the same eye-weight as the routine buttons.
6293+
Used now only for the rare row whose actions can't collapse into the
6294+
kebab menu (e.g. a "Protected" badge on system backends). */
6295+
.resource-actions {
6296+
display: inline-flex;
6297+
align-items: center;
6298+
gap: var(--spacing-xs);
6299+
justify-content: flex-end;
6300+
}
6301+
.resource-actions__group {
6302+
display: inline-flex;
6303+
align-items: center;
6304+
gap: var(--spacing-xs);
6305+
}
6306+
.resource-actions__divider {
6307+
width: 1px;
6308+
height: 20px;
6309+
background: var(--color-border-subtle);
6310+
margin: 0 var(--spacing-xs);
6311+
flex-shrink: 0;
6312+
}
6313+
6314+
/* ActionMenu — kebab trigger + popover menu. Borrows claudemaster's
6315+
restrained pattern: button reads as a quiet ellipsis at rest, lights up
6316+
on row hover, and the menu items hold typography-first weight (icon +
6317+
label, no fills until hover). The trigger stays at low opacity until the
6318+
user reaches for the row, so dense tables don't read as control panels. */
6319+
.action-menu__trigger {
6320+
display: inline-flex;
6321+
align-items: center;
6322+
justify-content: center;
6323+
width: 28px;
6324+
height: 28px;
6325+
padding: 0;
6326+
border: 1px solid transparent;
6327+
border-radius: var(--radius-sm);
6328+
background: transparent;
6329+
color: var(--color-text-muted);
6330+
cursor: pointer;
6331+
font-size: var(--text-sm);
6332+
opacity: 0.45;
6333+
transition:
6334+
opacity var(--duration-fast) var(--ease-default),
6335+
color var(--duration-fast) var(--ease-default),
6336+
background var(--duration-fast) var(--ease-default),
6337+
border-color var(--duration-fast) var(--ease-default);
6338+
}
6339+
.resource-row:hover .action-menu__trigger,
6340+
.action-menu__trigger:focus-visible,
6341+
.action-menu__trigger.is-open {
6342+
opacity: 1;
6343+
}
6344+
.action-menu__trigger:hover,
6345+
.action-menu__trigger.is-open {
6346+
background: var(--color-bg-tertiary);
6347+
border-color: var(--color-border-subtle);
6348+
color: var(--color-text-primary);
6349+
}
6350+
.action-menu__trigger:focus-visible {
6351+
outline: 2px solid var(--color-border-focus);
6352+
outline-offset: 2px;
6353+
}
6354+
.action-menu__trigger--compact {
6355+
width: 24px;
6356+
height: 24px;
6357+
font-size: var(--text-xs);
6358+
}
6359+
6360+
.action-menu {
6361+
display: flex;
6362+
flex-direction: column;
6363+
min-width: 200px;
6364+
padding: 4px;
6365+
outline: none;
6366+
}
6367+
.action-menu__item {
6368+
display: flex;
6369+
align-items: center;
6370+
gap: var(--spacing-sm);
6371+
padding: 8px 10px;
6372+
border: 0;
6373+
border-radius: var(--radius-sm);
6374+
background: transparent;
6375+
color: var(--color-text-primary);
6376+
font-family: inherit;
6377+
font-size: var(--text-sm);
6378+
font-weight: 500;
6379+
text-align: left;
6380+
cursor: pointer;
6381+
width: 100%;
6382+
transition: background var(--duration-fast) var(--ease-default),
6383+
color var(--duration-fast) var(--ease-default);
6384+
}
6385+
.action-menu__item.is-active,
6386+
.action-menu__item:hover:not(:disabled) {
6387+
background: var(--color-bg-tertiary);
6388+
}
6389+
.action-menu__item:disabled {
6390+
cursor: not-allowed;
6391+
opacity: 0.45;
6392+
}
6393+
.action-menu__icon {
6394+
width: 14px;
6395+
text-align: center;
6396+
color: var(--color-text-muted);
6397+
font-size: var(--text-xs);
6398+
flex-shrink: 0;
6399+
}
6400+
.action-menu__item.is-active .action-menu__icon,
6401+
.action-menu__item:hover:not(:disabled) .action-menu__icon {
6402+
color: var(--color-text-primary);
6403+
}
6404+
.action-menu__label {
6405+
flex: 1;
6406+
min-width: 0;
6407+
white-space: nowrap;
6408+
overflow: hidden;
6409+
text-overflow: ellipsis;
6410+
}
6411+
.action-menu__shortcut {
6412+
font-family: var(--font-mono);
6413+
font-size: 10px;
6414+
color: var(--color-text-muted);
6415+
letter-spacing: 0.04em;
6416+
flex-shrink: 0;
6417+
}
6418+
.action-menu__item.is-danger { color: var(--color-error); }
6419+
.action-menu__item.is-danger .action-menu__icon { color: var(--color-error); }
6420+
.action-menu__item.is-danger:hover:not(:disabled),
6421+
.action-menu__item.is-danger.is-active {
6422+
background: var(--color-error-light);
6423+
color: var(--color-error);
6424+
}
6425+
.action-menu__item.is-danger:hover:not(:disabled) .action-menu__icon,
6426+
.action-menu__item.is-danger.is-active .action-menu__icon {
6427+
color: var(--color-error);
6428+
}
6429+
.action-menu__divider {
6430+
height: 1px;
6431+
background: var(--color-border-subtle);
6432+
margin: 4px 2px;
6433+
}
6434+
.action-menu__badge {
6435+
display: flex;
6436+
align-items: center;
6437+
gap: var(--spacing-sm);
6438+
padding: 8px 10px;
6439+
font-size: var(--text-xs);
6440+
font-weight: 500;
6441+
letter-spacing: 0.06em;
6442+
text-transform: uppercase;
6443+
color: var(--color-text-muted);
6444+
}
6445+
.action-menu__badge > i { color: var(--color-text-muted); font-size: var(--text-xs); width: 14px; text-align: center; }
6446+
6447+
/* Pulse modifier — subtle ring on the Pin button when a model is pinned, so
6448+
pinning is visible at a glance without a second icon next to the name. */
6449+
.btn--pulse {
6450+
animation: btnPulseWarning 1.6s ease-in-out infinite;
6451+
}
6452+
@keyframes btnPulseWarning {
6453+
0%, 100% { box-shadow: 0 0 0 0 rgba(235, 203, 139, 0); }
6454+
50% { box-shadow: 0 0 0 4px rgba(235, 203, 139, 0.18); }
6455+
}
6456+
6457+
/* StatCard clickable variant — the Manage summary cards double as shortcuts
6458+
to the relevant tab + filter, so the card needs a real button affordance. */
6459+
.stat-card[data-clickable="true"] {
6460+
cursor: pointer;
6461+
user-select: none;
6462+
}
6463+
.stat-card[data-clickable="true"]:hover {
6464+
border-color: var(--stat-accent, var(--color-border));
6465+
transform: translateY(-1px);
6466+
}
6467+
.stat-card[data-clickable="true"]:focus-visible {
6468+
outline: 2px solid var(--color-border-focus);
6469+
outline-offset: 2px;
6470+
}
6471+
6472+
/* Manage summary marker — same .stat-grid layout. Top margin separates the
6473+
cards from the System Resources card above (otherwise they sit too close
6474+
to the RAM bar) and bottom margin tightens the gap to the tabs below. */
6475+
.manage-summary {
6476+
margin-top: var(--spacing-xl);
6477+
margin-bottom: var(--spacing-lg);
6478+
}
6479+
6480+
/* Screen-reader-only label, used for table headers whose visual cell needs
6481+
no label (kebab-only Actions column, toggle-only Enabled column). The
6482+
header still announces correctly to assistive tech without making the
6483+
table feel like it's labelling sparse columns twice. */
6484+
.visually-hidden {
6485+
position: absolute;
6486+
width: 1px;
6487+
height: 1px;
6488+
padding: 0;
6489+
margin: -1px;
6490+
overflow: hidden;
6491+
clip: rect(0, 0, 0, 0);
6492+
white-space: nowrap;
6493+
border: 0;
6494+
}
6495+
61986496
/* Reduced motion accessibility */
61996497
@media (prefers-reduced-motion: reduce) {
62006498
*, *::before, *::after {

0 commit comments

Comments
 (0)