mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-05 18:15:24 +00:00
fix: plugin page clipping and badges, match select and textarea sizes
This commit is contained in:
1 parent
2ee31ac9cc
commit
a2308c9d71
12 files changed
+70
-40
No files matched your search
@@ -20,6 +20,8 @@
|
||||
- Host status, import and export, and credential tools now start on for new installs
|
||||
- Sync now starts on in a new desktop app, so it can link to a server
|
||||
- The first run plugin picker leaves plugins you did not choose off instead of removing them
|
||||
- Dropdowns and text areas are the same size as the text fields next to them
|
||||
- A page's own tabs sit above its search bar
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -30,6 +32,13 @@
|
||||
- An installed plugin update older than the copy in a newer image is removed at startup instead of logging an error every boot
|
||||
- Canceling a download to the desktop app right as it starts no longer leaves a partial file behind
|
||||
- Changing two interface settings at once, like resetting the dashboard, now saves both instead of only the last one
|
||||
- Release notes on a plugin's page are no longer cut off on the left
|
||||
- The Community badge in Browse is no longer missing its top border
|
||||
- Official plugin pages no longer show a stray divider before the author
|
||||
- The version picker on a plugin's page is no longer empty for a plugin installed from a local build
|
||||
- The plugin video no longer has a second border around it
|
||||
- The hint at the bottom of Browse and the stored data footer on plugin pages have the same padding as the rest of the page
|
||||
- Sign in methods on the Security page have a line between them
|
||||
|
||||
## 26.11.0
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })}
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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)}
|
||||
|
||||
Reference in new issue
Block a user