Studio: size Agents and System to their pane, restore palette focus
- The GPU row's breakpoint could never fire. The settings pane is about 664px at any window size, and the name, three figures, meter and percentage need more than that, so the one-row layout was dead code. Each device is now two compact lines: name with the percentage, then the figures with a fixed-width meter. The name stays readable, the meter no longer runs the width of the pane, and there is no breakpoint that cannot be reached. - The agent, model and quantization grid switched on the viewport, so between 768px and 1024px it went to three columns inside a 440px pane and crushed every control. It now keys on the pane and stacks until the columns genuinely fit. - Dark mode dropped the palette card's resting border, but that border is the only focus indicator since the card removes its outline, and an unlayered rule beats Tailwind's layered focus-visible utility. Keyboard focus paints the ring colour again.
This commit is contained in:
parent
ecb80670e5
commit
29131cc85e
3 changed files with 261 additions and 249 deletions
|
|
@ -1113,233 +1113,240 @@ export function AgentsTab() {
|
|||
aria-label={t("settings.agents.commandBuilder")}
|
||||
className="flex w-full flex-col gap-6"
|
||||
>
|
||||
<div className="grid grid-cols-[minmax(0,0.8fr)_minmax(0,1fr)_minmax(9rem,0.5fr)] items-start gap-3 max-md:grid-cols-1">
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
{/* Keyed to this pane, not the viewport. The dialog leaves the tab
|
||||
about 440px at a 768px window, where three columns crush the agent
|
||||
and model controls; 34rem is the point all three stay usable. */}
|
||||
<div className="@container">
|
||||
<div className="grid grid-cols-1 items-start gap-3 @[34rem]:grid-cols-[minmax(0,0.8fr)_minmax(0,1fr)_minmax(9rem,0.5fr)]">
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span
|
||||
data-settings-label={t("settings.agents.agent")}
|
||||
className="text-xs font-medium text-foreground"
|
||||
>
|
||||
{t("settings.agents.agent")}
|
||||
</span>
|
||||
<a
|
||||
href={selectedAgentDetails.docsUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={t("settings.agents.agentDocs", {
|
||||
agent: selectedAgentDetails.name,
|
||||
})}
|
||||
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-ui-11 font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
{t("settings.agents.docs")}
|
||||
<HugeiconsIcon icon={ArrowUpRight01Icon} className="size-3" />
|
||||
</a>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedAgent}
|
||||
onValueChange={(agent) => {
|
||||
agentSelectionChanged.current = true;
|
||||
setSelectedAgent(agent);
|
||||
resetCopied();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t("settings.agents.agent")}
|
||||
className="w-full rounded-lg"
|
||||
>
|
||||
<SelectValue>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<AgentIcon
|
||||
logo={selectedAgentDetails.logo}
|
||||
icon={selectedAgentDetails.icon}
|
||||
darkIcon={selectedAgentDetails.darkIcon}
|
||||
invertIconInDark={selectedAgentDetails.invertIconInDark}
|
||||
color={selectedAgentDetails.color}
|
||||
mark={selectedAgentDetails.mark}
|
||||
/>
|
||||
<span className="truncate">
|
||||
{selectedAgentDetails.name}
|
||||
</span>
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
{agents.map((agentId) => {
|
||||
const agent = detailsFor(agentId);
|
||||
return (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<AgentIcon
|
||||
logo={agent.logo}
|
||||
icon={agent.icon}
|
||||
darkIcon={agent.darkIcon}
|
||||
invertIconInDark={agent.invertIconInDark}
|
||||
color={agent.color}
|
||||
mark={agent.mark}
|
||||
/>
|
||||
<span className="truncate">{agent.name}</span>
|
||||
{localDetection &&
|
||||
loaded &&
|
||||
detectedAgents.has(agent.id) ? (
|
||||
<span className="shrink-0 rounded-full bg-control-accent/10 px-2 py-1 text-ui-10 leading-none font-semibold text-control-accent">
|
||||
{t("settings.agents.quickstart.installed")}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<span
|
||||
data-settings-label={t("settings.agents.agent")}
|
||||
data-settings-label={t("settings.agents.model")}
|
||||
className="text-xs font-medium text-foreground"
|
||||
>
|
||||
{t("settings.agents.agent")}
|
||||
{t("settings.agents.model")}
|
||||
</span>
|
||||
<a
|
||||
href={selectedAgentDetails.docsUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={t("settings.agents.agentDocs", {
|
||||
agent: selectedAgentDetails.name,
|
||||
})}
|
||||
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-ui-11 font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
<Popover
|
||||
open={modelPickerOpen}
|
||||
onOpenChange={(open) => {
|
||||
setModelPickerOpen(open);
|
||||
if (!open) {
|
||||
setModelSearch("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("settings.agents.docs")}
|
||||
<HugeiconsIcon icon={ArrowUpRight01Icon} className="size-3" />
|
||||
</a>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedAgent}
|
||||
onValueChange={(agent) => {
|
||||
agentSelectionChanged.current = true;
|
||||
setSelectedAgent(agent);
|
||||
resetCopied();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t("settings.agents.agent")}
|
||||
className="w-full rounded-lg"
|
||||
>
|
||||
<SelectValue>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<AgentIcon
|
||||
logo={selectedAgentDetails.logo}
|
||||
icon={selectedAgentDetails.icon}
|
||||
darkIcon={selectedAgentDetails.darkIcon}
|
||||
invertIconInDark={selectedAgentDetails.invertIconInDark}
|
||||
color={selectedAgentDetails.color}
|
||||
mark={selectedAgentDetails.mark}
|
||||
/>
|
||||
<span className="truncate">
|
||||
{selectedAgentDetails.name}
|
||||
<PopoverTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("settings.agents.model")}
|
||||
aria-expanded={modelPickerOpen}
|
||||
title={selectedModel}
|
||||
className="flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-border bg-background px-3 text-left transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring dark:border-transparent dark:bg-white/[0.06] dark:hover:bg-white/10"
|
||||
>
|
||||
<span className="min-w-0 truncate font-mono text-xs">
|
||||
{labelFor(selectedModel)}
|
||||
</span>
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
{agents.map((agentId) => {
|
||||
const agent = detailsFor(agentId);
|
||||
return (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<AgentIcon
|
||||
logo={agent.logo}
|
||||
icon={agent.icon}
|
||||
darkIcon={agent.darkIcon}
|
||||
invertIconInDark={agent.invertIconInDark}
|
||||
color={agent.color}
|
||||
mark={agent.mark}
|
||||
/>
|
||||
<span className="truncate">{agent.name}</span>
|
||||
{localDetection &&
|
||||
loaded &&
|
||||
detectedAgents.has(agent.id) ? (
|
||||
<span className="shrink-0 rounded-full bg-control-accent/10 px-2 py-1 text-ui-10 leading-none font-semibold text-control-accent">
|
||||
{t("settings.agents.quickstart.installed")}
|
||||
<HugeiconsIcon
|
||||
icon={ChevronDownStandardIcon}
|
||||
strokeWidth={2}
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
className="w-[var(--radix-popover-trigger-width)] max-w-[calc(100vw-2rem)] gap-0 rounded-lg p-1"
|
||||
>
|
||||
<Command
|
||||
shouldFilter={false}
|
||||
className="rounded-none bg-transparent p-0"
|
||||
>
|
||||
<CommandInput
|
||||
value={modelSearch}
|
||||
onValueChange={setModelSearch}
|
||||
aria-label={t("settings.agents.searchModels")}
|
||||
placeholder={t("settings.agents.searchModels")}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t("settings.agents.noModels")}
|
||||
</CommandEmpty>
|
||||
{visibleModels.map((model) => (
|
||||
<CommandItem
|
||||
key={model}
|
||||
value={model}
|
||||
data-checked={model === selectedModel}
|
||||
onSelect={() => {
|
||||
modelSelectionChanged.current = true;
|
||||
setSelectedModel(model);
|
||||
setSelectedVariant(knownVariants[model] ?? null);
|
||||
setVariants([]);
|
||||
setVariantsFailed(false);
|
||||
setVariantsLoading(isHuggingFaceRepo(model));
|
||||
setModelSearch("");
|
||||
setModelPickerOpen(false);
|
||||
resetCopied();
|
||||
}}
|
||||
className="cursor-pointer font-mono text-xs"
|
||||
>
|
||||
<span className="min-w-0 truncate" title={model}>
|
||||
{labelFor(model)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
{matchingModels.length > visibleModels.length ? (
|
||||
<p className="border-t border-border/60 px-3 py-2 text-ui-11 text-muted-foreground">
|
||||
{t("settings.agents.showingModels", {
|
||||
shown: visibleModels.length,
|
||||
total: matchingModels.length,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<span
|
||||
data-settings-label={t("settings.agents.quantization")}
|
||||
className="text-xs font-medium text-foreground"
|
||||
>
|
||||
{t("settings.agents.quantization")}
|
||||
</span>
|
||||
<Select
|
||||
value={selectedVariant ?? undefined}
|
||||
onValueChange={(variant) => {
|
||||
chosenVariant.current = { model: selectedModel, variant };
|
||||
setSelectedVariant(variant);
|
||||
resetCopied();
|
||||
}}
|
||||
disabled={variantsLoading || variants.length === 0}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t("settings.agents.quantization")}
|
||||
className="w-full rounded-lg font-mono text-xs"
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
variantsLoading
|
||||
? t("settings.agents.loadingQuantizations")
|
||||
: t("settings.agents.noQuantizations")
|
||||
}
|
||||
>
|
||||
{selectedVariant}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start" className="min-w-[16rem]">
|
||||
{variants.map((variant) => {
|
||||
// Size only: the recommended/downloaded tags wrapped every
|
||||
// row onto two lines and made the list hard to scan.
|
||||
const size = formatBytes(
|
||||
variant.download_size_bytes ?? variant.size_bytes,
|
||||
);
|
||||
return (
|
||||
<SelectItem
|
||||
key={variant.quant}
|
||||
value={variant.quant}
|
||||
// Stretch the item text so the size can sit flush right,
|
||||
// giving the list a clean two-column read.
|
||||
className="[&>span:last-child]:w-full [&>span:last-child]:justify-between"
|
||||
>
|
||||
<span className="font-mono text-xs whitespace-nowrap">
|
||||
{variant.quant}
|
||||
</span>
|
||||
{size ? (
|
||||
<span className="text-ui-10 whitespace-nowrap text-muted-foreground">
|
||||
{size}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<span
|
||||
data-settings-label={t("settings.agents.model")}
|
||||
className="text-xs font-medium text-foreground"
|
||||
>
|
||||
{t("settings.agents.model")}
|
||||
</span>
|
||||
<Popover
|
||||
open={modelPickerOpen}
|
||||
onOpenChange={(open) => {
|
||||
setModelPickerOpen(open);
|
||||
if (!open) {
|
||||
setModelSearch("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("settings.agents.model")}
|
||||
aria-expanded={modelPickerOpen}
|
||||
title={selectedModel}
|
||||
className="flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-border bg-background px-3 text-left transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring dark:border-transparent dark:bg-white/[0.06] dark:hover:bg-white/10"
|
||||
>
|
||||
<span className="min-w-0 truncate font-mono text-xs">
|
||||
{labelFor(selectedModel)}
|
||||
</span>
|
||||
<HugeiconsIcon
|
||||
icon={ChevronDownStandardIcon}
|
||||
strokeWidth={2}
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
className="w-[var(--radix-popover-trigger-width)] max-w-[calc(100vw-2rem)] gap-0 rounded-lg p-1"
|
||||
>
|
||||
<Command
|
||||
shouldFilter={false}
|
||||
className="rounded-none bg-transparent p-0"
|
||||
>
|
||||
<CommandInput
|
||||
value={modelSearch}
|
||||
onValueChange={setModelSearch}
|
||||
aria-label={t("settings.agents.searchModels")}
|
||||
placeholder={t("settings.agents.searchModels")}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("settings.agents.noModels")}</CommandEmpty>
|
||||
{visibleModels.map((model) => (
|
||||
<CommandItem
|
||||
key={model}
|
||||
value={model}
|
||||
data-checked={model === selectedModel}
|
||||
onSelect={() => {
|
||||
modelSelectionChanged.current = true;
|
||||
setSelectedModel(model);
|
||||
setSelectedVariant(knownVariants[model] ?? null);
|
||||
setVariants([]);
|
||||
setVariantsFailed(false);
|
||||
setVariantsLoading(isHuggingFaceRepo(model));
|
||||
setModelSearch("");
|
||||
setModelPickerOpen(false);
|
||||
resetCopied();
|
||||
}}
|
||||
className="cursor-pointer font-mono text-xs"
|
||||
>
|
||||
<span className="min-w-0 truncate" title={model}>
|
||||
{labelFor(model)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
{matchingModels.length > visibleModels.length ? (
|
||||
<p className="border-t border-border/60 px-3 py-2 text-ui-11 text-muted-foreground">
|
||||
{t("settings.agents.showingModels", {
|
||||
shown: visibleModels.length,
|
||||
total: matchingModels.length,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<span
|
||||
data-settings-label={t("settings.agents.quantization")}
|
||||
className="text-xs font-medium text-foreground"
|
||||
>
|
||||
{t("settings.agents.quantization")}
|
||||
</span>
|
||||
<Select
|
||||
value={selectedVariant ?? undefined}
|
||||
onValueChange={(variant) => {
|
||||
chosenVariant.current = { model: selectedModel, variant };
|
||||
setSelectedVariant(variant);
|
||||
resetCopied();
|
||||
}}
|
||||
disabled={variantsLoading || variants.length === 0}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t("settings.agents.quantization")}
|
||||
className="w-full rounded-lg font-mono text-xs"
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
variantsLoading
|
||||
? t("settings.agents.loadingQuantizations")
|
||||
: t("settings.agents.noQuantizations")
|
||||
}
|
||||
>
|
||||
{selectedVariant}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start" className="min-w-[16rem]">
|
||||
{variants.map((variant) => {
|
||||
// Size only: the recommended/downloaded tags wrapped every
|
||||
// row onto two lines and made the list hard to scan.
|
||||
const size = formatBytes(
|
||||
variant.download_size_bytes ?? variant.size_bytes,
|
||||
);
|
||||
return (
|
||||
<SelectItem
|
||||
key={variant.quant}
|
||||
value={variant.quant}
|
||||
// Stretch the item text so the size can sit flush right,
|
||||
// giving the list a clean two-column read.
|
||||
className="[&>span:last-child]:w-full [&>span:last-child]:justify-between"
|
||||
>
|
||||
<span className="font-mono text-xs whitespace-nowrap">
|
||||
{variant.quant}
|
||||
</span>
|
||||
{size ? (
|
||||
<span className="text-ui-10 whitespace-nowrap text-muted-foreground">
|
||||
{size}
|
||||
</span>
|
||||
) : null}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -456,9 +456,7 @@ export function ResourcesTab() {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Sized against this pane, not the viewport: the Settings dialog caps
|
||||
the tab at roughly 650px however wide the window is. */}
|
||||
<div className="@container">
|
||||
<div>
|
||||
{hasGpu ? (
|
||||
metrics.devices.map((device, index) => {
|
||||
const ordinal = deviceOrdinal(device);
|
||||
|
|
@ -490,29 +488,37 @@ export function ResourcesTab() {
|
|||
? formatPercent(safePercent)
|
||||
: unknownLabel;
|
||||
return (
|
||||
// One row per device: a full-width bar under a single GPU read
|
||||
// as a page-wide rule, so the bar is sized to the row instead.
|
||||
// Two compact lines per device. The settings pane is about
|
||||
// 664px at any window size, which cannot hold the name, three
|
||||
// figures, meter and percentage on one row, so no breakpoint
|
||||
// would ever fire. Splitting them keeps the name readable and
|
||||
// the meter narrow instead of running the width of the pane.
|
||||
<div
|
||||
key={`${device.index ?? index}-${device.name ?? "gpu"}`}
|
||||
className="flex min-w-0 flex-col items-stretch gap-2 py-3 @[50rem]:flex-row @[50rem]:items-center @[50rem]:gap-4"
|
||||
className="flex min-w-0 flex-col gap-2 py-3"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">
|
||||
{device.name ?? t("settings.resources.gpu.unknownDevice")}
|
||||
<div className="flex min-w-0 items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium text-foreground">
|
||||
{device.name ??
|
||||
t("settings.resources.gpu.unknownDevice")}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{ordinal === undefined
|
||||
? backendLabel
|
||||
: `${t("settings.resources.gpu.deviceWithIndex", {
|
||||
index: ordinal,
|
||||
})}, ${backendLabel}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{ordinal === undefined
|
||||
? backendLabel
|
||||
: `${t("settings.resources.gpu.deviceWithIndex", {
|
||||
index: ordinal,
|
||||
})}, ${backendLabel}`}
|
||||
<div className="shrink-0 font-mono text-xs tabular-nums text-muted-foreground">
|
||||
{percentText}{" "}
|
||||
{t("settings.resources.gpu.vramUtilization")}
|
||||
</div>
|
||||
</div>
|
||||
{/* Stacked until the pane is genuinely wide enough: the
|
||||
figures, meter and percentage need about 50rem before the
|
||||
name has room left to read. */}
|
||||
<div className="flex flex-col items-stretch gap-2 @[50rem]:flex-row @[50rem]:shrink-0 @[50rem]:items-center @[50rem]:gap-4">
|
||||
<div className="flex justify-between gap-3 font-mono text-xs tabular-nums text-muted-foreground @[50rem]:shrink-0 @[50rem]:justify-start">
|
||||
{/* Wraps rather than overflows once the pane gets narrow. */}
|
||||
<div className="flex min-w-0 flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
||||
<div className="flex min-w-0 gap-3 font-mono text-xs tabular-nums text-muted-foreground">
|
||||
<span className="truncate">
|
||||
{t("settings.resources.gpu.used", { value: usedText })}
|
||||
</span>
|
||||
|
|
@ -525,20 +531,12 @@ export function ResourcesTab() {
|
|||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-4">
|
||||
{/* Fills the space left by the percentage rather than
|
||||
claiming the full row, which overflowed once stacked. */}
|
||||
<Progress
|
||||
value={safePercent}
|
||||
aria-label={device.name ?? "GPU"}
|
||||
className="h-1.5 min-w-0 flex-1 rounded-full bg-muted @[50rem]:w-40 @[50rem]:flex-none dark:bg-black/40"
|
||||
indicatorClassName={usageIndicatorClass(safePercent)}
|
||||
/>
|
||||
<div className="w-[5.5rem] shrink-0 text-right font-mono text-xs tabular-nums text-muted-foreground">
|
||||
{percentText}{" "}
|
||||
{t("settings.resources.gpu.vramUtilization")}
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={safePercent}
|
||||
aria-label={device.name ?? "GPU"}
|
||||
className="h-1.5 w-40 max-w-full shrink-0 rounded-full bg-muted dark:bg-black/40"
|
||||
indicatorClassName={usageIndicatorClass(safePercent)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -671,6 +671,13 @@ html[data-chat-font] .aui-root {
|
|||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* The card removes its outline, so the border is the only focus indicator.
|
||||
This rule is unlayered like the one above, which would otherwise beat
|
||||
Tailwind's layered focus-visible:border-ring and leave nothing visible. */
|
||||
.dark .palette-card:focus-visible {
|
||||
border-color: var(--ring);
|
||||
}
|
||||
|
||||
.palette-card:hover {
|
||||
border-color: var(--ring-soft);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue