mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
Compare commits
1
Commits
main
...
sdk-v1.0.20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f332aa5c3 |
No files matched your search
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 26.10.5
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Card grids in the Plugins tab and in plugins can show Auto (as many as fit) or a set number of cards per row
|
||||||
|
|
||||||
## 26.10.4
|
## 26.10.4
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "26.10.4",
|
"version": "26.10.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "26.10.4",
|
"version": "26.10.5",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "26.10.4",
|
"version": "26.10.5",
|
||||||
"description": "Self-hosted, plugin-based server management.",
|
"description": "Self-hosted, plugin-based server management.",
|
||||||
"author": "Karmaa",
|
"author": "Karmaa",
|
||||||
"main": "electron/main.cjs",
|
"main": "electron/main.cjs",
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.0.20
|
||||||
|
|
||||||
|
Plugin API 1.12.0.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `columns` and `onColumns` on `ViewToggle` show a cards per row picker: Auto fits as many as the pane has room for, or pick 1 to 6
|
||||||
|
- `size` and `minCardWidth` on `DataView`, so a card grid follows that pick, and the `GridSize` type
|
||||||
|
- `usePluginPanelView` also keeps `columns`, the cards per row pick, with `setColumns`
|
||||||
|
|
||||||
## 1.0.19
|
## 1.0.19
|
||||||
|
|
||||||
Plugin API 1.11.0.
|
Plugin API 1.11.0.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Termix plugin guide
|
# Termix plugin guide
|
||||||
|
|
||||||
SDK 1.0.19 | Plugin API 1.11.0
|
SDK 1.0.20 | Plugin API 1.12.0
|
||||||
|
|
||||||
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
|
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
|
||||||
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
|
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
|
||||||
@@ -643,6 +643,9 @@ exports with props is `ui.api.txt` in the Termix repo
|
|||||||
`PanePrompt` for small prompts.
|
`PanePrompt` for small prompts.
|
||||||
- Every tab or panel sits in `PanelShell`. Lists use `PanelList` and `ListRow`, add with `AddButton`,
|
- Every tab or panel sits in `PanelShell`. Lists use `PanelList` and `ListRow`, add with `AddButton`,
|
||||||
forms end with `FormFooter`, empty lists show `EmptyState`.
|
forms end with `FormFooter`, empty lists show `EmptyState`.
|
||||||
|
- Card grids use `DataView` with `ViewToggle`. Pass `columns` and `onColumns` to `ViewToggle` and the same
|
||||||
|
value as `size` to `DataView`, so users can pick Auto (as many as fit) or a fixed number of cards per
|
||||||
|
row. `usePluginPanelView` keeps the choice as `columns`. Older Termix versions ignore both props.
|
||||||
- Forms: `TextField`, `NumberField`, `SelectField`, `SwitchRow`, `SecretField`, `SettingRow` and friends.
|
- Forms: `TextField`, `NumberField`, `SelectField`, `SwitchRow`, `SecretField`, `SettingRow` and friends.
|
||||||
- Host sessions use `ConnectionGate` for the whole connect flow (host keys, TOTP, passphrases, errors).
|
- Host sessions use `ConnectionGate` for the whole connect flow (host keys, TOTP, passphrases, errors).
|
||||||
- Mobile: check `useIsMobile()` and give small screens a layout that works. The desktop app always uses the
|
- Mobile: check `useIsMobile()` and give small screens a layout that works. The desktop app always uses the
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@termix-ssh/plugin-sdk",
|
"name": "@termix-ssh/plugin-sdk",
|
||||||
"version": "1.0.19",
|
"version": "1.0.20",
|
||||||
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1711,28 +1711,49 @@ export function usePluginUiPreferences<
|
|||||||
return bridge.usePluginUiPreferences(bridge.usePluginId()) as never;
|
return bridge.usePluginUiPreferences(bridge.usePluginId()) as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PanelGridSize = "auto" | "1" | "2" | "3" | "4" | "5" | "6";
|
||||||
|
const PANEL_GRID_SIZES: readonly string[] = [
|
||||||
|
"auto",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grid or list and row density for one of this plugin's views, kept in its
|
* Grid or list, row density and cards per row for one of this plugin's
|
||||||
* Appearance area as `<prefix>viewMode` and `<prefix>density`. Declare those
|
* views, kept in its Appearance area as `<prefix>viewMode`, `<prefix>density`
|
||||||
* keys in contributes.uiPresets so each preset level can pick a default.
|
* and `<prefix>columns`. Declare those keys in contributes.uiPresets so each
|
||||||
|
* preset level can pick a default. Hand columns to ViewToggle and DataView.
|
||||||
*/
|
*/
|
||||||
export function usePluginPanelView(prefix = ""): {
|
export function usePluginPanelView(prefix = ""): {
|
||||||
view: "grid" | "list";
|
view: "grid" | "list";
|
||||||
density: "comfortable" | "compact";
|
density: "comfortable" | "compact";
|
||||||
compact: boolean;
|
compact: boolean;
|
||||||
|
columns: PanelGridSize;
|
||||||
setView: (view: "grid" | "list") => void;
|
setView: (view: "grid" | "list") => void;
|
||||||
setDensity: (density: "comfortable" | "compact") => void;
|
setDensity: (density: "comfortable" | "compact") => void;
|
||||||
|
setColumns: (columns: PanelGridSize) => void;
|
||||||
} {
|
} {
|
||||||
const { values, set } = usePluginUiPreferences();
|
const { values, set } = usePluginUiPreferences();
|
||||||
const view = values[`${prefix}viewMode`] === "list" ? "list" : "grid";
|
const view = values[`${prefix}viewMode`] === "list" ? "list" : "grid";
|
||||||
const density =
|
const density =
|
||||||
values[`${prefix}density`] === "compact" ? "compact" : "comfortable";
|
values[`${prefix}density`] === "compact" ? "compact" : "comfortable";
|
||||||
|
const saved = values[`${prefix}columns`];
|
||||||
|
const columns =
|
||||||
|
typeof saved === "string" && PANEL_GRID_SIZES.includes(saved)
|
||||||
|
? (saved as PanelGridSize)
|
||||||
|
: "auto";
|
||||||
return {
|
return {
|
||||||
view,
|
view,
|
||||||
density,
|
density,
|
||||||
compact: density === "compact",
|
compact: density === "compact",
|
||||||
|
columns,
|
||||||
setView: (next) => set(`${prefix}viewMode`, next),
|
setView: (next) => set(`${prefix}viewMode`, next),
|
||||||
setDensity: (next) => set(`${prefix}density`, next),
|
setDensity: (next) => set(`${prefix}density`, next),
|
||||||
|
setColumns: (next) => set(`${prefix}columns`, next),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export {
|
|||||||
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
||||||
* version by the server.
|
* version by the server.
|
||||||
*/
|
*/
|
||||||
export const PLUGIN_API_VERSION = "1.11.0";
|
export const PLUGIN_API_VERSION = "1.12.0";
|
||||||
|
|
||||||
/** The API major, the default engine.api for a new plugin. */
|
/** The API major, the default engine.api for a new plugin. */
|
||||||
export const SUPPORTED_PLUGIN_API_VERSION = String(
|
export const SUPPORTED_PLUGIN_API_VERSION = String(
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ DEFAULT_FOLDER_COLOR (value): "#f59145"
|
|||||||
DEFAULT_FOLDER_ICON (value): "folder"
|
DEFAULT_FOLDER_ICON (value): "folder"
|
||||||
DashboardCardId (type): DashboardCardId
|
DashboardCardId (type): DashboardCardId
|
||||||
DataColumn (type): DataColumn<T>
|
DataColumn (type): DataColumn<T>
|
||||||
DataView (value): <T>({ items, view, density, getKey, renderCard, columns, listColumns, onRowClick, isRowActive, empty, className, }: { items: T[]; view: PanelViewMode; density?: PanelDensity; getKey: (item: T) => string; renderCard: (item: T) => ReactNode; columns: GridColumns; listColumns: DataColumn<T>[]; onRowClick?: (item: T) => void; isRowActive?: (item: T) => boolean; empty: ReactNode; className?: string; }) => Element
|
DataView (value): <T>({ items, view, density, getKey, renderCard, columns, size, minCardWidth, listColumns, onRowClick, isRowActive, empty, className, }: { items: T[]; view: PanelViewMode; density?: PanelDensity; getKey: (item: T) => string; renderCard: (item: T) => ReactNode; columns: GridColumns; size?: GridSize; minCardWidth?: number; listColumns: DataColumn<T>[]; onRowClick?: (item: T) => void; isRowActive?: (item: T) => boolean; empty: ReactNode; className?: string; }) => Element
|
||||||
DefaultOverrides (type): DefaultOverrides
|
DefaultOverrides (type): DefaultOverrides
|
||||||
DefaultsOnly (value): ({ settingKey, children, }: { settingKey: string; children: ReactNode; }) => Element
|
DefaultsOnly (value): ({ settingKey, children, }: { settingKey: string; children: ReactNode; }) => Element
|
||||||
DesktopUpdateMode (type): DesktopUpdateMode
|
DesktopUpdateMode (type): DesktopUpdateMode
|
||||||
@@ -131,6 +131,7 @@ GridCardCatalogEntry (type): GridCardCatalogEntry
|
|||||||
GridColSpan (type): GridColSpan
|
GridColSpan (type): GridColSpan
|
||||||
GridColumns (type): GridColumns
|
GridColumns (type): GridColumns
|
||||||
GridLayout (type): GridLayout
|
GridLayout (type): GridLayout
|
||||||
|
GridSize (type): GridSize
|
||||||
GridSlot (type): GridSlot
|
GridSlot (type): GridSlot
|
||||||
GroupHeading (value): ({ title, count, action, className, }: { title: string; count?: number; action?: ReactNode; className?: string; }) => Element
|
GroupHeading (value): ({ title, count, action, className, }: { title: string; count?: number; action?: ReactNode; className?: string; }) => Element
|
||||||
HexColorPicker (value): ({ value, onChange, }: { value: string; onChange: (hex: string) => void; }) => Element
|
HexColorPicker (value): ({ value, onChange, }: { value: string; onChange: (hex: string) => void; }) => Element
|
||||||
@@ -321,7 +322,7 @@ UserRole (type): UserRole
|
|||||||
UserSecondFactorSummary (type): UserSecondFactorSummary
|
UserSecondFactorSummary (type): UserSecondFactorSummary
|
||||||
VersionBadge (value): ({ status, releaseUrl, className, }: VersionBadgeProps) => Element
|
VersionBadge (value): ({ status, releaseUrl, className, }: VersionBadgeProps) => Element
|
||||||
VersionInfo (type): VersionInfo
|
VersionInfo (type): VersionInfo
|
||||||
ViewToggle (value): ({ view, onView, density, onDensity, }: { view: PanelViewMode; onView: (next: PanelViewMode) => void; density?: PanelDensity; onDensity?: (next: PanelDensity) => void; }) => Element
|
ViewToggle (value): ({ view, onView, density, onDensity, columns, onColumns, }: { view: PanelViewMode; onView: (next: PanelViewMode) => void; density?: PanelDensity; onDensity?: (next: PanelDensity) => void; columns?: GridSize; onColumns?: (next: GridSize) => void; }) => Element
|
||||||
WebSocketConnectionTarget (type): WebSocketConnectionTarget
|
WebSocketConnectionTarget (type): WebSocketConnectionTarget
|
||||||
WidgetTitle (value): ({ title, icon, fallback }: WidgetTitleProps) => Element
|
WidgetTitle (value): ({ title, icon, fallback }: WidgetTitleProps) => Element
|
||||||
actionOrder (value): (action: HostActionDef) => number
|
actionOrder (value): (action: HostActionDef) => number
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ const ALLOWED = [
|
|||||||
"GridCardCatalogEntry",
|
"GridCardCatalogEntry",
|
||||||
"GridColSpan",
|
"GridColSpan",
|
||||||
"GridColumns",
|
"GridColumns",
|
||||||
|
"GridSize",
|
||||||
"GridLayout",
|
"GridLayout",
|
||||||
"GridSlot",
|
"GridSlot",
|
||||||
"GroupHeading",
|
"GroupHeading",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import type React from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
PANEL,
|
PANEL,
|
||||||
|
gridSizeStyle,
|
||||||
|
type GridSize,
|
||||||
type PanelDensity,
|
type PanelDensity,
|
||||||
type PanelViewMode,
|
type PanelViewMode,
|
||||||
} from "@/components/panel-layout";
|
} from "@/components/panel-layout";
|
||||||
@@ -67,6 +69,8 @@ export function DataView<T>({
|
|||||||
getKey,
|
getKey,
|
||||||
renderCard,
|
renderCard,
|
||||||
columns,
|
columns,
|
||||||
|
size,
|
||||||
|
minCardWidth,
|
||||||
listColumns,
|
listColumns,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
isRowActive,
|
isRowActive,
|
||||||
@@ -78,7 +82,12 @@ export function DataView<T>({
|
|||||||
density?: PanelDensity;
|
density?: PanelDensity;
|
||||||
getKey: (item: T) => string;
|
getKey: (item: T) => string;
|
||||||
renderCard: (item: T) => React.ReactNode;
|
renderCard: (item: T) => React.ReactNode;
|
||||||
|
/** Breakpoint columns, used when no size is given. */
|
||||||
columns: GridColumns;
|
columns: GridColumns;
|
||||||
|
/** Columns the user picked with ViewToggle. Wins over columns. */
|
||||||
|
size?: GridSize;
|
||||||
|
/** Narrowest card in auto mode, in px. */
|
||||||
|
minCardWidth?: number;
|
||||||
listColumns: DataColumn<T>[];
|
listColumns: DataColumn<T>[];
|
||||||
onRowClick?: (item: T) => void;
|
onRowClick?: (item: T) => void;
|
||||||
isRowActive?: (item: T) => boolean;
|
isRowActive?: (item: T) => boolean;
|
||||||
@@ -93,9 +102,19 @@ export function DataView<T>({
|
|||||||
|
|
||||||
if (view === "grid") {
|
if (view === "grid") {
|
||||||
return (
|
return (
|
||||||
<div className={cn("grid", PANEL.gap, gridClass(columns), className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid",
|
||||||
|
PANEL.gap,
|
||||||
|
!size && gridClass(columns),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={size ? gridSizeStyle(size, minCardWidth) : undefined}
|
||||||
|
>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div key={getKey(item)}>{renderCard(item)}</div>
|
<div key={getKey(item)} className="flex flex-col *:flex-1">
|
||||||
|
{renderCard(item)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
/* eslint-disable react-refresh/only-export-components */
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
import { Children, Fragment, isValidElement, useRef } from "react";
|
import { Children, Fragment, isValidElement, useRef } from "react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { ArrowLeft, Grid3X3, List, Rows3, Search, X } from "lucide-react";
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
Columns3,
|
||||||
|
Grid3X3,
|
||||||
|
List,
|
||||||
|
Rows3,
|
||||||
|
Search,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Separator } from "@/components/separator";
|
import { Separator } from "@/components/separator";
|
||||||
import { DocsLink } from "@/components/docs-link";
|
import { DocsLink } from "@/components/docs-link";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/select";
|
||||||
|
|
||||||
/** One spacing rhythm for every panel and tab. */
|
/** One spacing rhythm for every panel and tab. */
|
||||||
export const PANEL = {
|
export const PANEL = {
|
||||||
@@ -19,6 +34,37 @@ export const PANEL = {
|
|||||||
export type PanelViewMode = "grid" | "list";
|
export type PanelViewMode = "grid" | "list";
|
||||||
export type PanelDensity = "comfortable" | "compact";
|
export type PanelDensity = "comfortable" | "compact";
|
||||||
|
|
||||||
|
/** Cards per row. "auto" fits as many as the pane has room for. */
|
||||||
|
export type GridSize = "auto" | "1" | "2" | "3" | "4" | "5" | "6";
|
||||||
|
|
||||||
|
export const GRID_SIZES: readonly GridSize[] = [
|
||||||
|
"auto",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sized off the pane's own width, not the window. Cards keep a min width,
|
||||||
|
// so narrow panes drop columns on their own.
|
||||||
|
export function gridSizeStyle(
|
||||||
|
size: GridSize,
|
||||||
|
minCardWidth = 320,
|
||||||
|
gap = "0.5rem",
|
||||||
|
): React.CSSProperties {
|
||||||
|
if (size === "auto") {
|
||||||
|
return {
|
||||||
|
gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${minCardWidth}px), 1fr))`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const n = Number(size);
|
||||||
|
return {
|
||||||
|
gridTemplateColumns: `repeat(auto-fill, minmax(max(min(100%, 220px), calc((100% - ${n - 1} * ${gap}) / ${n} - 0.5px)), 1fr))`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** The header, toolbar band and body every tab and panel draws. */
|
/** The header, toolbar band and body every tab and panel draws. */
|
||||||
export function PanelShell({
|
export function PanelShell({
|
||||||
chrome = true,
|
chrome = true,
|
||||||
@@ -308,20 +354,25 @@ export function Segmented<T extends string>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Grid or list, plus the compact rows toggle. */
|
/** Grid or list, plus the compact rows toggle and the cards per row picker. */
|
||||||
export function ViewToggle({
|
export function ViewToggle({
|
||||||
view,
|
view,
|
||||||
onView,
|
onView,
|
||||||
density,
|
density,
|
||||||
onDensity,
|
onDensity,
|
||||||
|
columns,
|
||||||
|
onColumns,
|
||||||
}: {
|
}: {
|
||||||
view: PanelViewMode;
|
view: PanelViewMode;
|
||||||
onView: (next: PanelViewMode) => void;
|
onView: (next: PanelViewMode) => void;
|
||||||
density?: PanelDensity;
|
density?: PanelDensity;
|
||||||
onDensity?: (next: PanelDensity) => void;
|
onDensity?: (next: PanelDensity) => void;
|
||||||
|
/** Cards per row in grid view. Pass with onColumns to show the picker. */
|
||||||
|
columns?: GridSize;
|
||||||
|
onColumns?: (next: GridSize) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
const toggle = (
|
||||||
<div className="flex items-center border border-border">
|
<div className="flex items-center border border-border">
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
active={view === "grid"}
|
active={view === "grid"}
|
||||||
@@ -356,6 +407,34 @@ export function ViewToggle({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!columns || !onColumns || view !== "grid") return toggle;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select value={columns} onValueChange={(v) => onColumns(v as GridSize)}>
|
||||||
|
<SelectTrigger
|
||||||
|
size="sm"
|
||||||
|
className="w-auto gap-1.5 text-xs"
|
||||||
|
title={t("panel.columns.title")}
|
||||||
|
aria-label={t("panel.columns.title")}
|
||||||
|
>
|
||||||
|
<Columns3 className="size-3.5 text-muted-foreground" />
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{GRID_SIZES.map((size) => (
|
||||||
|
<SelectItem key={size} value={size} className="text-xs">
|
||||||
|
{size === "auto"
|
||||||
|
? t("panel.columns.auto")
|
||||||
|
: t("panel.columns.count", { count: Number(size) })}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{toggle}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleButton({
|
function ToggleButton({
|
||||||
|
|||||||
@@ -1325,11 +1325,6 @@
|
|||||||
"updated": "Recently updated",
|
"updated": "Recently updated",
|
||||||
"category": "Category"
|
"category": "Category"
|
||||||
},
|
},
|
||||||
"columns": {
|
|
||||||
"title": "Columns",
|
|
||||||
"auto": "Auto",
|
|
||||||
"count": "{{count}} wide"
|
|
||||||
},
|
|
||||||
"table": {
|
"table": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
@@ -1654,7 +1649,12 @@
|
|||||||
"gridView": "Grid",
|
"gridView": "Grid",
|
||||||
"listView": "List",
|
"listView": "List",
|
||||||
"compactRows": "Compact rows",
|
"compactRows": "Compact rows",
|
||||||
"comfortableRows": "Comfortable rows"
|
"comfortableRows": "Comfortable rows",
|
||||||
|
"columns": {
|
||||||
|
"title": "Columns",
|
||||||
|
"auto": "Auto",
|
||||||
|
"count": "{{count}} wide"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"secretSaved": "Saved. Type to replace it."
|
"secretSaved": "Saved. Type to replace it."
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ export {
|
|||||||
PanelShell,
|
PanelShell,
|
||||||
Segmented,
|
Segmented,
|
||||||
ViewToggle,
|
ViewToggle,
|
||||||
|
type GridSize,
|
||||||
type PanelDensity,
|
type PanelDensity,
|
||||||
type PanelViewMode,
|
type PanelViewMode,
|
||||||
} from "@/components/panel-layout";
|
} from "@/components/panel-layout";
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Columns3,
|
|
||||||
Download,
|
Download,
|
||||||
Globe,
|
Globe,
|
||||||
Library,
|
Library,
|
||||||
@@ -37,7 +36,9 @@ import {
|
|||||||
GroupHeading,
|
GroupHeading,
|
||||||
PANEL,
|
PANEL,
|
||||||
PanelShell,
|
PanelShell,
|
||||||
|
GRID_SIZES,
|
||||||
ViewToggle,
|
ViewToggle,
|
||||||
|
type GridSize,
|
||||||
type PanelViewMode,
|
type PanelViewMode,
|
||||||
} from "@/components/panel-layout";
|
} from "@/components/panel-layout";
|
||||||
import { DataView, type DataColumn } from "@/components/data-view";
|
import { DataView, type DataColumn } from "@/components/data-view";
|
||||||
@@ -72,9 +73,6 @@ import { DocsLink } from "@/components/docs-link";
|
|||||||
|
|
||||||
type Section = "installed" | "browse" | "updates" | "registries";
|
type Section = "installed" | "browse" | "updates" | "registries";
|
||||||
|
|
||||||
const GRID_SIZES = ["auto", "2", "3", "4", "5", "6"] as const;
|
|
||||||
type GridSize = (typeof GRID_SIZES)[number];
|
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
view: PanelViewMode;
|
view: PanelViewMode;
|
||||||
columns: GridSize;
|
columns: GridSize;
|
||||||
@@ -364,54 +362,16 @@ function LayoutControls({
|
|||||||
onView: (next: PanelViewMode) => void;
|
onView: (next: PanelViewMode) => void;
|
||||||
onColumns: (next: GridSize) => void;
|
onColumns: (next: GridSize) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ViewToggle
|
||||||
{layout.view === "grid" && (
|
view={layout.view}
|
||||||
<Select
|
onView={onView}
|
||||||
value={layout.columns}
|
columns={layout.columns}
|
||||||
onValueChange={(v) => onColumns(v as GridSize)}
|
onColumns={onColumns}
|
||||||
>
|
/>
|
||||||
<SelectTrigger
|
|
||||||
size="sm"
|
|
||||||
className="w-auto gap-1.5 text-xs"
|
|
||||||
title={t("plugins.manager.columns.title")}
|
|
||||||
aria-label={t("plugins.manager.columns.title")}
|
|
||||||
>
|
|
||||||
<Columns3 className="size-3.5 text-muted-foreground" />
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{GRID_SIZES.map((size) => (
|
|
||||||
<SelectItem key={size} value={size} className="text-xs">
|
|
||||||
{size === "auto"
|
|
||||||
? t("plugins.manager.columns.auto")
|
|
||||||
: t("plugins.manager.columns.count", { count: Number(size) })}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
<ViewToggle view={layout.view} onView={onView} />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sized off the tab's own width, not the window. Cards keep a min width,
|
|
||||||
// so narrow panes drop columns on their own.
|
|
||||||
function gridStyle(columns: GridSize): React.CSSProperties {
|
|
||||||
if (columns === "auto") {
|
|
||||||
return {
|
|
||||||
gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 400px), 1fr))",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const n = Number(columns);
|
|
||||||
const gaps = (n - 1) * 12;
|
|
||||||
return {
|
|
||||||
gridTemplateColumns: `repeat(auto-fill, minmax(max(220px, calc((100% - ${gaps}px) / ${n} - 0.5px)), 1fr))`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function PluginCollection({
|
function PluginCollection({
|
||||||
plugins,
|
plugins,
|
||||||
layout,
|
layout,
|
||||||
@@ -440,13 +400,17 @@ function PluginCollection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-3" style={gridStyle(layout.columns)}>
|
<DataView
|
||||||
{plugins.map((p) => (
|
items={plugins}
|
||||||
<div key={p.id} className="flex flex-col *:flex-1">
|
view="grid"
|
||||||
{renderCard(p)}
|
getKey={(p) => p.id}
|
||||||
</div>
|
renderCard={renderCard}
|
||||||
))}
|
columns={{}}
|
||||||
</div>
|
size={layout.columns}
|
||||||
|
minCardWidth={400}
|
||||||
|
listColumns={listColumns}
|
||||||
|
empty={null}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,24 @@ describe("DataView", () => {
|
|||||||
expect(screen.queryByText("Name")).toBeNull();
|
expect(screen.queryByText("Name")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the picked size over breakpoint columns", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<DataView
|
||||||
|
items={items}
|
||||||
|
view="grid"
|
||||||
|
getKey={(item) => item.id}
|
||||||
|
renderCard={(item) => <div>card {item.name}</div>}
|
||||||
|
columns={{ base: 1, md: 2 }}
|
||||||
|
size="3"
|
||||||
|
listColumns={[]}
|
||||||
|
empty={null}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const grid = container.firstElementChild as HTMLElement;
|
||||||
|
expect(grid.className).not.toContain("md:grid-cols-2");
|
||||||
|
expect(grid.style.gridTemplateColumns).toContain("/ 3");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders a header and clickable rows in list mode", () => {
|
it("renders a header and clickable rows in list mode", () => {
|
||||||
const onRowClick = vi.fn();
|
const onRowClick = vi.fn();
|
||||||
renderView("list", onRowClick);
|
renderView("list", onRowClick);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
PanelShell,
|
PanelShell,
|
||||||
Segmented,
|
Segmented,
|
||||||
ViewToggle,
|
ViewToggle,
|
||||||
|
gridSizeStyle,
|
||||||
} from "@/components/panel-layout";
|
} from "@/components/panel-layout";
|
||||||
|
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
@@ -72,6 +73,34 @@ describe("ViewToggle", () => {
|
|||||||
fireEvent.click(screen.getByLabelText("panel.compactRows"));
|
fireEvent.click(screen.getByLabelText("panel.compactRows"));
|
||||||
expect(onDensity).toHaveBeenCalledWith("compact");
|
expect(onDensity).toHaveBeenCalledWith("compact");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows the columns picker only in grid view", () => {
|
||||||
|
const props = { onView: vi.fn(), columns: "auto" as const };
|
||||||
|
const { rerender } = render(
|
||||||
|
<ViewToggle view="grid" {...props} onColumns={vi.fn()} />,
|
||||||
|
);
|
||||||
|
expect(screen.getByLabelText("panel.columns.title")).toBeTruthy();
|
||||||
|
|
||||||
|
rerender(<ViewToggle view="list" {...props} onColumns={vi.fn()} />);
|
||||||
|
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
|
||||||
|
|
||||||
|
rerender(<ViewToggle view="grid" {...props} />);
|
||||||
|
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("gridSizeStyle", () => {
|
||||||
|
it("fits auto cards to the pane", () => {
|
||||||
|
expect(gridSizeStyle("auto", 280).gridTemplateColumns).toBe(
|
||||||
|
"repeat(auto-fill, minmax(min(100%, 280px), 1fr))",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits the width for a fixed count", () => {
|
||||||
|
expect(gridSizeStyle("4").gridTemplateColumns).toContain(
|
||||||
|
"(100% - 3 * 0.5rem) / 4",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Facts", () => {
|
describe("Facts", () => {
|
||||||
|
|||||||
@@ -183,17 +183,13 @@ describe("PluginsTab", () => {
|
|||||||
it("switches to a table and remembers it", async () => {
|
it("switches to a table and remembers it", async () => {
|
||||||
await renderTab();
|
await renderTab();
|
||||||
expect(screen.queryByRole("table")).toBeNull();
|
expect(screen.queryByRole("table")).toBeNull();
|
||||||
expect(
|
expect(screen.getByLabelText(en.panel.columns.title)).toBeTruthy();
|
||||||
screen.getByLabelText(en.plugins.manager.columns.title),
|
|
||||||
).toBeTruthy();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText(en.panel.listView));
|
fireEvent.click(screen.getByLabelText(en.panel.listView));
|
||||||
|
|
||||||
expect(screen.getByRole("table")).toBeTruthy();
|
expect(screen.getByRole("table")).toBeTruthy();
|
||||||
expect(screen.getByText(en.plugins.manager.table.version)).toBeTruthy();
|
expect(screen.getByText(en.plugins.manager.table.version)).toBeTruthy();
|
||||||
expect(
|
expect(screen.queryByLabelText(en.panel.columns.title)).toBeNull();
|
||||||
screen.queryByLabelText(en.plugins.manager.columns.title),
|
|
||||||
).toBeNull();
|
|
||||||
expect(localStorage.getItem("pluginsTab.view")).toBe("list");
|
expect(localStorage.getItem("pluginsTab.view")).toBe("list");
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|||||||
Reference in new issue
Block a user