refactor(ui): declutter Home - discoverable + dismissable API, vertical balance

Home felt overloaded and top-heavy. Three changes from review:
- The API endpoint catalog (12 endpoints) is collapsed by default behind a
  "Browse the API" disclosure; only the base URL + copy stay visible, so the
  catalog is discoverable without dominating the page.
- The whole connect card is dismissable (x): dismissing unmounts it so the
  vertical space is recovered, and the choice is remembered (localStorage).
- .home-page now fills its column and vertically centers its content when
  there is slack, so sparse states (no models / card dismissed) read as a
  balanced launcher instead of content jammed at the top. Overflow-safe:
  tall content flows from the top and scrolls.

Adds connect.browse / connect.hide / connect.dismiss i18n keys to all locales.

Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto
2026-06-18 14:31:54 +00:00
parent 8caf93fb3a
commit 7cca8352cb
9 changed files with 96 additions and 7 deletions

View File

@@ -84,6 +84,9 @@
"compatTitle": "Drop-in-Kompatibilität",
"apiReference": "Vollständige API-Referenz",
"copy": "Kopieren",
"copied": "Kopiert"
"copied": "Kopiert",
"browse": "Browse the API",
"hide": "Hide endpoints",
"dismiss": "Dismiss"
}
}

View File

@@ -84,6 +84,9 @@
"compatTitle": "Drop-in compatibility",
"apiReference": "Full API reference",
"copy": "Copy",
"copied": "Copied"
"copied": "Copied",
"browse": "Browse the API",
"hide": "Hide endpoints",
"dismiss": "Dismiss"
}
}

View File

@@ -84,6 +84,9 @@
"compatTitle": "Compatibilidad directa",
"apiReference": "Referencia completa de la API",
"copy": "Copiar",
"copied": "Copiado"
"copied": "Copiado",
"browse": "Browse the API",
"hide": "Hide endpoints",
"dismiss": "Dismiss"
}
}

View File

@@ -84,6 +84,9 @@
"compatTitle": "Kompatibilitas drop-in",
"apiReference": "Referensi API lengkap",
"copy": "Salin",
"copied": "Disalin"
"copied": "Disalin",
"browse": "Browse the API",
"hide": "Hide endpoints",
"dismiss": "Dismiss"
}
}

View File

@@ -84,6 +84,9 @@
"compatTitle": "Compatibilità drop-in",
"apiReference": "Riferimento API completo",
"copy": "Copia",
"copied": "Copiato"
"copied": "Copiato",
"browse": "Esplora le API",
"hide": "Nascondi gli endpoint",
"dismiss": "Ignora"
}
}

View File

@@ -84,6 +84,9 @@
"compatTitle": "드롭인 호환성",
"apiReference": "전체 API 레퍼런스",
"copy": "복사",
"copied": "복사됨"
"copied": "복사됨",
"browse": "Browse the API",
"hide": "Hide endpoints",
"dismiss": "Dismiss"
}
}

View File

@@ -84,6 +84,9 @@
"compatTitle": "即插即用兼容",
"apiReference": "完整 API 参考",
"copy": "复制",
"copied": "已复制"
"copied": "已复制",
"browse": "Browse the API",
"hide": "Hide endpoints",
"dismiss": "Dismiss"
}
}

View File

@@ -5618,6 +5618,8 @@ select.input {
.home-page {
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
gap: var(--space-section);
max-width: var(--page-max-medium);
margin: 0 auto;
@@ -6121,6 +6123,39 @@ select.input {
padding: 0;
}
.home-connect-url .btn { flex-shrink: 0; display: inline-flex; align-items: center; gap: 6px; }
.home-connect-dismiss {
margin-left: auto;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: none;
border: none;
border-radius: var(--radius-md);
color: var(--color-text-muted);
cursor: pointer;
transition: background var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default);
}
.home-connect-dismiss:hover { background: var(--color-surface-hover); color: var(--color-text-primary); }
.home-connect-toggle {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
margin-top: var(--spacing-md);
padding: var(--spacing-xs) 0;
background: none;
border: none;
color: var(--color-primary);
font: inherit;
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
cursor: pointer;
}
.home-connect-toggle:hover { color: var(--color-primary-hover); }
.home-connect-toggle i { font-size: 0.7em; }
.home-connect-endpoints { margin-top: var(--spacing-sm); }
.home-connect-block { margin-top: var(--spacing-md); }
.home-connect-block-head {
display: flex;

View File

@@ -27,6 +27,14 @@ const COMPAT = [
export default function HomeConnect() {
const { t } = useTranslation('home')
const [copied, setCopied] = useState(false)
// Endpoint catalog is collapsed by default so Home stays uncluttered; the
// base URL stays visible and the full list is one click away (discoverable).
const [showEndpoints, setShowEndpoints] = useState(false)
// Dismissable: hiding the card unmounts it entirely so the vertical space is
// recovered, and the choice is remembered across visits.
const [dismissed, setDismissed] = useState(() => {
try { return localStorage.getItem('localai_home_connect_dismissed') === '1' } catch { return false }
})
// Absolute base for this instance, honouring any sub-path mount.
const base = new URL(apiUrl('/'), window.location.origin).href.replace(/\/$/, '')
@@ -39,6 +47,13 @@ export default function HomeConnect() {
} catch (_) { /* clipboard blocked — the URL is selectable anyway */ }
}
const dismiss = () => {
try { localStorage.setItem('localai_home_connect_dismissed', '1') } catch { /* ignore */ }
setDismissed(true)
}
if (dismissed) return null
return (
<section className="home-connect card" aria-labelledby="home-connect-title">
<div className="home-connect-head">
@@ -47,6 +62,9 @@ export default function HomeConnect() {
<h2 id="home-connect-title" className="home-connect-title">{t('connect.title')}</h2>
<p className="home-connect-sub">{t('connect.subtitle')}</p>
</div>
<button type="button" className="home-connect-dismiss" onClick={dismiss} aria-label={t('connect.dismiss')} title={t('connect.dismiss')}>
<i className="fas fa-times" aria-hidden="true" />
</button>
</div>
<div className="home-connect-url">
@@ -57,6 +75,19 @@ export default function HomeConnect() {
</button>
</div>
<button
type="button"
className="home-connect-toggle"
aria-expanded={showEndpoints}
aria-controls="home-connect-endpoints"
onClick={() => setShowEndpoints(v => !v)}
>
<i className={`fas fa-chevron-${showEndpoints ? 'up' : 'down'}`} aria-hidden="true" />
<span>{showEndpoints ? t('connect.hide') : t('connect.browse')}</span>
</button>
{showEndpoints && (
<div id="home-connect-endpoints" className="home-connect-endpoints">
<div className="home-connect-block">
<div className="home-connect-block-head">
<span className="home-connect-block-title">{t('connect.nativeTitle')}</span>
@@ -87,6 +118,8 @@ export default function HomeConnect() {
))}
</ul>
</div>
</div>
)}
</section>
)
}