diff --git a/client/src/components/Admin/AdminPluginsPanel.tsx b/client/src/components/Admin/AdminPluginsPanel.tsx
index ac0810d60..c66841ef2 100644
--- a/client/src/components/Admin/AdminPluginsPanel.tsx
+++ b/client/src/components/Admin/AdminPluginsPanel.tsx
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import {
Blocks, AlertTriangle, PackageOpen, RefreshCw, Trash2, Download, Bug, X, ShieldCheck,
ArrowUpCircle, Github, ExternalLink, ChevronDown, Check, Lock, Search,
- SlidersHorizontal, MoreHorizontal, RotateCw, ArrowRight, Database, Users, LayoutDashboard,
+ SlidersHorizontal, ArrowUpDown, CircleDot, MoreHorizontal, RotateCw, ArrowRight, Database, Users, LayoutDashboard,
Radio, Luggage, Plane, Globe, Image, CalendarDays, Map, Bell, Cloud, Camera, Compass,
BookOpen, Wallet, Puzzle,
} from 'lucide-react'
@@ -267,7 +267,7 @@ export default function AdminPluginsPanel() {
{/* Click-away layer for any open dropdown (filters or a row's ⋯ menu). */}
{menu &&
setMenu(null)} />}
{/* Header */}
-
+
{t('admin.plugins.title')}
@@ -283,7 +283,7 @@ export default function AdminPluginsPanel() {
{/* Runtime-disabled notice */}
{!runtimeOn && !loading && !error && (
-
+
{t('admin.plugins.disabledTitle')}
@@ -292,15 +292,23 @@ export default function AdminPluginsPanel() {
)}
- {/* Toolbar */}
+ {/* Toolbar — on mobile: tabs+rescan, then full-width search, then a
+ right-aligned filter row. On sm+ the wrappers collapse (sm:contents)
+ so everything sits in one wrapping row. */}
{runtimeOn && !loading && !error && (
-
-
-
setView('installed')} label={t('admin.plugins.installed')} count={plugins.length} />
-
+
+
+
+ setView('installed')} label={t('admin.plugins.installed')} count={plugins.length} />
+
+
+
act('__rescan', adminApi.pluginRescan, t('admin.plugins.rescanned'))} title={t('admin.plugins.rescan')}
+ className="sm:hidden h-[38px] w-[38px] grid place-items-center rounded-xl border border-edge bg-surface-card text-content-muted hover:text-content hover:border-content-faint transition-colors shrink-0">
+
+
-
+
setQ(e.target.value)} type="search"
@@ -309,33 +317,35 @@ export default function AdminPluginsPanel() {
/>
-
}
- options={[
- ['all', t('admin.plugins.allTypes')], ['widget', t('admin.plugins.type.widget')],
- ['integration', t('admin.plugins.type.integration')], ['page', t('admin.plugins.type.page')],
- ]}
- valueLabel={typeFilter === 'all' ? t('admin.plugins.allTypes') : t(`admin.plugins.type.${typeFilter}` as never)}
- onPick={v => setTypeFilter(v as TypeFilter)} />
-
- {view === 'installed' && (
-
+ }
options={[
- ['all', t('admin.plugins.allStatuses')], ['on', t('admin.plugins.status.active')], ['off', t('admin.plugins.stateOff')],
- ['update', t('admin.plugins.filterUpdate')], ['err', t('admin.plugins.status.error')],
+ ['all', t('admin.plugins.allTypes')], ['widget', t('admin.plugins.type.widget')],
+ ['integration', t('admin.plugins.type.integration')], ['page', t('admin.plugins.type.page')],
]}
- valueLabel={statusLabel(statusFilter, t)}
- onPick={v => setStatusFilter(v as StatusFilter)} />
- )}
+ valueLabel={typeFilter === 'all' ? t('admin.plugins.allTypes') : t(`admin.plugins.type.${typeFilter}` as never)}
+ onPick={v => setTypeFilter(v as TypeFilter)} />
- setSort(v as SortKey)} />
+ {view === 'installed' && (
+ }
+ options={[
+ ['all', t('admin.plugins.allStatuses')], ['on', t('admin.plugins.status.active')], ['off', t('admin.plugins.stateOff')],
+ ['update', t('admin.plugins.filterUpdate')], ['err', t('admin.plugins.status.error')],
+ ]}
+ valueLabel={statusLabel(statusFilter, t)}
+ onPick={v => setStatusFilter(v as StatusFilter)} />
+ )}
- act('__rescan', adminApi.pluginRescan, t('admin.plugins.rescanned'))} title={t('admin.plugins.rescan')}
- className="h-[38px] w-[38px] grid place-items-center rounded-xl border border-edge bg-surface-card text-content-muted hover:text-content hover:border-content-faint transition-colors">
-
-
+ }
+ options={[['name', t('admin.plugins.sortName')], ['recent', t('admin.plugins.sortRecent')], ['updates', t('admin.plugins.sortUpdates')]]}
+ valueLabel={sortLabel(sort, t)}
+ onPick={v => setSort(v as SortKey)} />
+
+ act('__rescan', adminApi.pluginRescan, t('admin.plugins.rescanned'))} title={t('admin.plugins.rescan')}
+ className="hidden sm:grid h-[38px] w-[38px] place-items-center rounded-xl border border-edge bg-surface-card text-content-muted hover:text-content hover:border-content-faint transition-colors">
+
+
+
)}
@@ -351,9 +361,9 @@ export default function AdminPluginsPanel() {
) : plugins.length === 0 ? (
) : (
-
+
{updatable.length > 0 && statusFilter !== 'err' && (
-
+
{t('admin.plugins.updatesAvailable', { count: updatable.length })}
updatable.forEach(runUpdate)}
@@ -465,12 +475,12 @@ function FilterMenu({ id, label, valueLabel, options, onPick, value, menu, setMe
return (
setMenu(open ? null : id)}
- className="h-[38px] px-3 inline-flex items-center gap-1.5 rounded-xl border border-edge bg-surface-card text-[13px] text-content-secondary hover:border-content-faint transition-colors whitespace-nowrap">
- {icon}{label}: {valueLabel}
+ className="h-[38px] px-2.5 sm:px-3 inline-flex items-center gap-1.5 rounded-xl border border-edge bg-surface-card text-[13px] text-content-secondary hover:border-content-faint transition-colors whitespace-nowrap">
+ {icon}{label}: {valueLabel}
{open && (
-
+
{options.map(([v, lbl]) => (
{ onPick(v); setMenu(null) }}
className={`w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[13px] transition-colors hover:bg-surface-tertiary ${
@@ -506,7 +516,7 @@ function InstalledRow({ p, t, busy, menu, setMenu, hasUpdate, latestVer, onToggl
const caps = deriveCaps(parseJson(p.permissions, []), parseJson<{ widget?: { slot?: string } }>(p.capabilities, {}), t)
const menuOpen = menu === `row:${p.id}`
return (
-
+
@@ -538,11 +548,11 @@ function InstalledRow({ p, t, busy, menu, setMenu, hasUpdate, latestVer, onToggl
)}
-
+
{hasUpdate && (
-
- {t('admin.plugins.updateTo', { version: latestVer })}
+
+ {t('admin.plugins.updateTo', { version: latestVer })}
)}
@@ -618,7 +628,7 @@ function RegistryGrid({ items, onInstall, onOpenDetail, busy, t, installedIds, f
)
return (
-
+
{items.map(item => {
const installed = installedIds.has(item.id)
return (
@@ -694,7 +704,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
-
+
@@ -706,12 +716,12 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
{item.author}{item.latest ? ` · v${item.latest}` : ''}
onInstall(item.id)} disabled={busy === item.id || installed}
- className="self-end text-[13px] font-semibold px-4 py-2 rounded-lg bg-accent text-accent-text hover:bg-accent-hover disabled:opacity-50 disabled:bg-surface-tertiary disabled:text-content-faint transition-colors shrink-0">
+ className="self-end text-[13px] font-semibold px-3 sm:px-4 py-2 rounded-lg bg-accent text-accent-text hover:bg-accent-hover disabled:opacity-50 disabled:bg-surface-tertiary disabled:text-content-faint transition-colors shrink-0">
{installed ? t('admin.plugins.installed') : t('admin.plugins.install')}
-
+
{item.description}
{failed &&
{t('admin.plugins.detailError')}
}
@@ -774,7 +784,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
-