Release v0.1.6

This commit is contained in:
2026-07-07 16:00:38 +02:00
parent a2053518d1
commit 150b720f12
149 changed files with 14311 additions and 8005 deletions

View File

@@ -0,0 +1,26 @@
import type { ReactNode } from "react";
import Button from "../Button";
type Props = {
label: string;
icon: ReactNode;
onClick?: () => void;
disabled?: boolean;
variant?: "primary" | "secondary" | "ghost" | "danger";
};
export default function AdminIconButton({ label, icon, onClick, disabled = false, variant = "secondary" }: Props) {
return (
<Button
type="button"
variant={variant}
className="admin-icon-button"
aria-label={label}
title={label}
disabled={disabled}
onClick={onClick}
>
{icon}
</Button>
);
}

View File

@@ -0,0 +1,45 @@
import type { ReactNode } from "react";
import DismissibleAlert from "../DismissibleAlert";
import LoadingFrame from "../LoadingFrame";
import PageTitle from "../PageTitle";
type Props = {
title: string;
description: string;
loading?: boolean;
loadingLabel?: string;
error?: string;
success?: string;
actions?: ReactNode;
children: ReactNode;
className?: string;
};
export default function AdminPageLayout({
title,
description,
loading = false,
loadingLabel = "Loading administration data...",
error = "",
success = "",
actions,
children,
className = ""
}: Props) {
return (
<div className={`admin-section-page ${className}`.trim()}>
<div className="page-heading split workspace-heading admin-page-heading">
<div>
<PageTitle loading={loading}>{title}</PageTitle>
<p>{description}</p>
</div>
{actions && <div className="button-row compact-actions admin-page-actions">{actions}</div>}
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
<LoadingFrame loading={loading} label={loadingLabel}>
{children}
</LoadingFrame>
</div>
);
}

View File

@@ -0,0 +1,44 @@
type Option = {
id: string;
label: string;
description?: string | null;
disabled?: boolean;
};
export default function AdminSelectionList({
options,
selected,
onChange,
emptyText = "No options available."
}: {
options: Option[];
selected: string[];
onChange: (next: string[]) => void;
emptyText?: string;
}) {
if (!options.length) return <p className="muted small-note">{emptyText}</p>;
const selectedSet = new Set(selected);
return (
<div className="admin-selection-list">
{options.map((option) => (
<label key={option.id} className={`admin-selection-item ${option.disabled ? "disabled" : ""}`}>
<input
type="checkbox"
checked={selectedSet.has(option.id)}
disabled={option.disabled}
onChange={(event) => {
const next = new Set(selectedSet);
if (event.target.checked) next.add(option.id);
else next.delete(option.id);
onChange(Array.from(next));
}}
/>
<span>
<strong>{option.label}</strong>
{option.description && <small>{option.description}</small>}
</span>
</label>
))}
</div>
);
}

View File

@@ -0,0 +1,27 @@
import { formatDateTime as formatPlatformDateTime } from "../../utils/datetime";
export function adminErrorMessage(error: unknown): string {
if (!(error instanceof Error)) return "The administrative operation failed.";
const message = error.message;
const jsonStart = message.indexOf("{");
if (jsonStart >= 0) {
try {
const parsed = JSON.parse(message.slice(jsonStart)) as { detail?: unknown };
if (typeof parsed.detail === "string") return parsed.detail;
if (Array.isArray(parsed.detail)) {
return parsed.detail.map((item) => typeof item === "object" && item && "msg" in item ? String((item as { msg: unknown }).msg) : String(item)).join("; ");
}
} catch {
// Fall through to the original message.
}
}
return message;
}
export function formatAdminDateTime(value?: string | null): string {
return formatPlatformDateTime(value, { fallback: "Never" });
}
export function joinLabels(values: Array<{ name: string }>): string {
return values.length ? values.map((value) => value.name).join(", ") : "—";
}