fix: plugin page clipping and badges, match select and textarea sizes

This commit is contained in:
LukeGus committed 2026-11-04 20:03:15 -06:00
1 parent 2ee31ac9cc
commit a2308c9d71
12 files changed
+70 -40

No files matched your search

+3 -3
View File
@@ -118,6 +118,9 @@ export function PanelShell({
const bands = (
<>
{tabs && (
<div className="shrink-0 border-b border-border px-1">{tabs}</div>
)}
{toolbar && (
<div
className={`flex shrink-0 items-center gap-2 border-b border-border ${PANEL.band}`}
@@ -125,9 +128,6 @@ export function PanelShell({
{toolbar}
</div>
)}
{tabs && (
<div className="shrink-0 border-b border-border px-1">{tabs}</div>
)}
</>
);
+2 -2
View File
@@ -35,7 +35,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-none border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-none border bg-transparent px-2.5 py-1 text-xs whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-8 data-[size=sm]:h-7 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
@@ -105,7 +105,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-none py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-none py-1.5 pr-8 pl-2 text-xs outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
+4 -4
View File
@@ -154,7 +154,7 @@ function Select2({
aria-expanded={open}
aria-required={required}
className={cn(
"border-input bg-transparent dark:bg-input/30 dark:hover:bg-input/50 text-foreground flex h-9 w-full items-center justify-between gap-2 rounded-none border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"border-input bg-transparent dark:bg-input/30 dark:hover:bg-input/50 text-foreground flex h-8 w-full items-center justify-between gap-2 rounded-none border px-2.5 py-1 text-xs shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
onKeyDown={(event) => {
@@ -189,7 +189,7 @@ function Select2({
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={t("common.search")}
className="h-9 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
className="h-8 w-full bg-transparent text-xs outline-none placeholder:text-muted-foreground"
autoFocus
/>
</div>
@@ -198,7 +198,7 @@ function Select2({
className="max-h-64 overflow-y-auto p-1 thin-scrollbar"
>
{filteredOptions.length === 0 ? (
<div className="px-2 py-6 text-center text-sm text-muted-foreground">
<div className="px-2 py-6 text-center text-xs text-muted-foreground">
{t("common.noResults")}
</div>
) : (
@@ -222,7 +222,7 @@ function Select2({
aria-selected={isSelected}
disabled={option.disabled}
className={cn(
"flex w-full items-center gap-2 rounded-none px-2 py-1.5 text-left text-sm outline-none transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50",
"flex w-full items-center gap-2 rounded-none px-2 py-1.5 text-left text-xs outline-none transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50",
isSelected && "bg-muted text-accent-foreground",
)}
onClick={() => selectValue(option.value)}
+1 -1
View File
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-none border border-input bg-transparent dark:bg-input/30 px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] duration-200 outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none",
"flex min-h-[80px] w-full rounded-none border border-input bg-transparent dark:bg-input/30 px-2.5 py-2 text-xs shadow-xs transition-[color,box-shadow] duration-200 outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none",
className,
)}
ref={ref}
+1 -1
View File
@@ -48,7 +48,7 @@ export function LookSettings({ isAdmin }: { isAdmin: boolean }) {
</p>
{isAdmin && response && (
<div className="flex flex-col gap-1.5 border border-border p-2">
<div className="mt-1 flex flex-col gap-1.5 border-t border-border pt-3">
<span className="text-[11px] font-medium text-foreground">
{t("looks.defaultsLabel")}
</span>
+16 -10
View File
@@ -42,6 +42,7 @@ import {
betaToTry,
describeContributions,
formatBytes,
hasSourceBadge,
orderByRisk,
reportIssueUrl,
requestFeatureUrl,
@@ -70,13 +71,16 @@ export function PluginDetail({
const running = plugin.status === "running";
const failed = plugin.status === "failed" || plugin.status === "blocked";
const compatible = plugin.versions.filter((v) => v.compatible);
const [picked, setPicked] = useState<string>(
plugin.version ?? plugin.latestVersion ?? "",
);
// A local build can be a version the registry never listed.
const startVersion =
plugin.version && compatible.some((v) => v.version === plugin.version)
? plugin.version
: (plugin.latestVersion ?? compatible[0]?.version ?? "");
const [picked, setPicked] = useState<string>(startVersion);
useEffect(() => {
setPicked(plugin.version ?? plugin.latestVersion ?? "");
}, [plugin.version, plugin.latestVersion]);
setPicked(startVersion);
}, [startVersion]);
const contributions = describeContributions(plugin.contributes);
// Only people who can manage plugins get here, and they see admin settings
@@ -108,10 +112,12 @@ export function PluginDetail({
</span>
<Facts className="text-[11px] text-muted-foreground/70">
{plugin.unverified && <UnverifiedBadge />}
<SourceBadge
source={plugin.source}
registryName={plugin.registryName}
/>
{hasSourceBadge(plugin.source) && (
<SourceBadge
source={plugin.source}
registryName={plugin.registryName}
/>
)}
{plugin.isBeta && <BetaBadge />}
{plugin.author && <span>{plugin.author}</span>}
{plugin.version ? (
@@ -671,7 +677,7 @@ function DataCard({
))}
</div>
)}
<div className="flex flex-wrap gap-x-4 gap-y-1 border-t border-border py-2 text-[11px] text-muted-foreground">
<div className="flex flex-wrap gap-x-4 gap-y-1 border-t border-border px-3 py-2 text-[11px] text-muted-foreground">
<span>
{t("plugins.manager.data.migrations", {
count: data.migrations.length,
+3 -3
View File
@@ -62,7 +62,7 @@ function Release({
.filter((group) => group.items.length > 0);
return (
<div className="flex flex-col gap-2 py-3">
<div className="flex flex-col gap-2 px-3 py-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-semibold">{row.version}</span>
{row.version === installedVersion && (
@@ -180,7 +180,7 @@ export function PluginReleaseNotes({ plugin }: { plugin: PluginEntry }) {
) : undefined
}
>
<div className="-mx-3 -my-1 md:-mx-4">
<div className="-mx-3 -my-1">
<Release row={latest} installedVersion={plugin.version} />
{showOlder && older.length > 0 && (
<div className="max-h-96 divide-y divide-border overflow-y-auto border-t border-border">
@@ -211,7 +211,7 @@ export function PluginVideo({
if (!src) return null;
return (
<div className="aspect-video w-full border border-border bg-black">
<div className="aspect-video w-full bg-black">
<iframe
src={src}
title={t("plugins.manager.videoTitle", { name })}
+14 -9
View File
@@ -59,6 +59,7 @@ import { PluginDetail } from "./PluginDetail";
import {
categoriesOf,
describeContributions,
hasSourceBadge,
isSevere,
kindsOf,
matchesQuery,
@@ -1078,10 +1079,12 @@ function InstalledCard({
facts={
<>
{plugin.unverified && <UnverifiedBadge />}
<SourceBadge
source={plugin.source}
registryName={plugin.registryName}
/>
{hasSourceBadge(plugin.source) && (
<SourceBadge
source={plugin.source}
registryName={plugin.registryName}
/>
)}
{plugin.isBeta && <BetaBadge />}
{plugin.author && <span>{plugin.author}</span>}
<span>v{plugin.version}</span>
@@ -1503,7 +1506,7 @@ function BrowseSection({
}}
/>
<p className="text-[11px] text-muted-foreground">
<p className="px-3 py-2 text-[11px] text-muted-foreground">
{t("plugins.manager.registries.browseHint")}{" "}
<DocsLink core="communityRegistry" className="text-[11px]">
{t("plugins.manager.communitySubmit")}
@@ -1535,10 +1538,12 @@ function BrowseCard({
onOpen={onOpen}
facts={
<>
<SourceBadge
source={plugin.source}
registryName={plugin.registryName}
/>
{hasSourceBadge(plugin.source) && (
<SourceBadge
source={plugin.source}
registryName={plugin.registryName}
/>
)}
{plugin.author && <span>{plugin.author}</span>}
{plugin.latestVersion && <span>v{plugin.latestVersion}</span>}
{plugin.category && <span>{plugin.category}</span>}
+10 -5
View File
@@ -1,7 +1,12 @@
import { useTranslation } from "react-i18next";
import { PluginIcon } from "@/lib/plugin-icon";
import { cn } from "@/lib/utils";
import { capabilityRisk, formatCount, isSevere } from "./plugin-model";
import {
capabilityRisk,
formatCount,
hasSourceBadge,
isSevere,
} from "./plugin-model";
export function PluginIconBox({
name,
@@ -80,7 +85,7 @@ export function UnverifiedBadge() {
const { t } = useTranslation();
return (
<span
className="shrink-0 border border-warning/40 bg-warning/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-warning"
className="inline-block shrink-0 border border-warning/40 bg-warning/10 px-1.5 py-0.5 leading-none text-[10px] font-bold uppercase tracking-wider text-warning"
title={t("plugins.manager.unverifiedHint")}
>
{t("plugins.manager.unverified")}
@@ -97,14 +102,14 @@ export function SourceBadge({
registryName?: string | null;
}) {
const { t } = useTranslation();
if (source !== "community" && source !== "custom") return null;
if (!hasSourceBadge(source)) return null;
const label =
source === "community"
? t("plugins.manager.registries.communityBadge")
: (registryName ?? t("plugins.manager.registries.customBadge"));
return (
<span
className="max-w-32 shrink-0 truncate border border-border bg-muted/40 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-muted-foreground"
className="inline-block max-w-32 shrink-0 truncate border border-border bg-muted/40 px-1.5 py-0.5 leading-none align-middle text-[10px] font-bold uppercase tracking-wider text-muted-foreground"
title={t(`plugins.manager.registries.${source}BadgeHint`, {
name: label,
})}
@@ -118,7 +123,7 @@ export function BetaBadge() {
const { t } = useTranslation();
return (
<span
className="shrink-0 border border-accent-brand/40 bg-accent-brand/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-accent-brand"
className="inline-block shrink-0 border border-accent-brand/40 bg-accent-brand/10 px-1.5 py-0.5 leading-none text-[10px] font-bold uppercase tracking-wider text-accent-brand"
title={t("plugins.manager.beta.badgeHint")}
>
{t("plugins.manager.beta.badge")}
+5
View File
@@ -591,3 +591,8 @@ export function youtubeEmbedUrl(videoId: string | undefined): string | null {
? `https://www.youtube-nocookie.com/embed/${videoId}?rel=0`
: null;
}
/** True when SourceBadge draws something, so lists can skip the empty slot. */
export function hasSourceBadge(source: string): boolean {
return source === "community" || source === "custom";
}
+2 -2
View File
@@ -19,13 +19,13 @@ export function AuthEnrollmentSections({
);
if (sections.length === 0) return null;
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col divide-y divide-border">
{sections.map((entry) => {
const Enrollment = entry.enrollment!;
return (
<div
key={`${entry.pluginId}:${entry.id}`}
className="flex flex-col gap-2"
className="flex flex-col gap-2 py-3 first:pt-0"
>
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{t(entry.titleKey)}