chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from "./page-templates.tsx";
|
||||
@@ -0,0 +1,268 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { PageHeader } from "../components/page-header.tsx";
|
||||
import { Button } from "../components/ui/button.ts";
|
||||
import { useLocale } from "../i18n/index.ts";
|
||||
|
||||
export type PageHeading = Readonly<{
|
||||
title: string;
|
||||
description?: string;
|
||||
eyebrow?: string;
|
||||
}>;
|
||||
|
||||
export type PageActionDefinition =
|
||||
| Readonly<{
|
||||
kind: "button";
|
||||
label: string;
|
||||
onAction(): void;
|
||||
disabled?: boolean;
|
||||
variant?: "primary" | "secondary" | "danger" | "ghost";
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "link";
|
||||
label: string;
|
||||
href: string;
|
||||
variant?: "primary" | "secondary" | "danger" | "ghost";
|
||||
}>;
|
||||
|
||||
export type PageTemplateSlots = Readonly<{
|
||||
heading: PageHeading;
|
||||
breadcrumb?: ReactNode;
|
||||
status?: ReactNode;
|
||||
actions?: readonly PageActionDefinition[];
|
||||
notices?: ReactNode;
|
||||
children?: ReactNode;
|
||||
aside?: ReactNode;
|
||||
feedback?: ReactNode;
|
||||
}>;
|
||||
|
||||
export function StandardPage(props: PageTemplateSlots) {
|
||||
const { message } = useLocale();
|
||||
return (
|
||||
<article className="ui-page page-template page-template--standard">
|
||||
{props.breadcrumb ? (
|
||||
<nav
|
||||
className="page-template__breadcrumb"
|
||||
aria-label={message("template.breadcrumb")}
|
||||
>
|
||||
{props.breadcrumb}
|
||||
</nav>
|
||||
) : null}
|
||||
<div className="page-template__heading">
|
||||
<PageHeader {...props.heading} />
|
||||
{props.status ? (
|
||||
<div className="page-template__status">{props.status}</div>
|
||||
) : null}
|
||||
{props.actions?.length ? (
|
||||
<PageActionBar actions={props.actions} />
|
||||
) : null}
|
||||
</div>
|
||||
{props.notices ? (
|
||||
<div className="page-template__notices">{props.notices}</div>
|
||||
) : null}
|
||||
{props.feedback ? (
|
||||
<div className="page-template__feedback">{props.feedback}</div>
|
||||
) : null}
|
||||
<div
|
||||
className="page-template__layout"
|
||||
data-has-aside={props.aside ? "true" : "false"}
|
||||
>
|
||||
<div className="page-template__content">{props.children}</div>
|
||||
{props.aside ? (
|
||||
<aside
|
||||
className="page-template__aside"
|
||||
aria-label={message("template.relatedInformation")}
|
||||
>
|
||||
{props.aside}
|
||||
</aside>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function CollectionPage(
|
||||
props: PageTemplateSlots &
|
||||
Readonly<{
|
||||
toolbar?: ReactNode;
|
||||
activeFilters?: ReactNode;
|
||||
resultCount?: ReactNode;
|
||||
bulkActions?: ReactNode;
|
||||
pagination?: ReactNode;
|
||||
}>,
|
||||
) {
|
||||
const { message } = useLocale();
|
||||
return (
|
||||
<StandardPage
|
||||
{...props}
|
||||
notices={
|
||||
<>
|
||||
{props.notices}
|
||||
{props.toolbar ? (
|
||||
<section
|
||||
className="collection-page__toolbar"
|
||||
aria-label={message("template.searchAndFilter")}
|
||||
>
|
||||
{props.toolbar}
|
||||
</section>
|
||||
) : null}
|
||||
{props.activeFilters ? (
|
||||
<div className="collection-page__active-filters">
|
||||
{props.activeFilters}
|
||||
</div>
|
||||
) : null}
|
||||
{props.resultCount ? (
|
||||
<div className="collection-page__result-count" role="status">
|
||||
{props.resultCount}
|
||||
</div>
|
||||
) : null}
|
||||
{props.bulkActions ? (
|
||||
<div className="collection-page__bulk-actions">
|
||||
{props.bulkActions}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="collection-page__results">{props.children}</div>
|
||||
{props.pagination ? (
|
||||
<nav
|
||||
className="collection-page__pagination"
|
||||
aria-label={message("template.pagination")}
|
||||
>
|
||||
{props.pagination}
|
||||
</nav>
|
||||
) : null}
|
||||
</StandardPage>
|
||||
);
|
||||
}
|
||||
|
||||
export function DetailPage(
|
||||
props: PageTemplateSlots &
|
||||
Readonly<{
|
||||
metadata?: ReactNode;
|
||||
destructiveAction?: ReactNode;
|
||||
}>,
|
||||
) {
|
||||
const { message } = useLocale();
|
||||
return (
|
||||
<StandardPage {...props}>
|
||||
{props.metadata ? (
|
||||
<section
|
||||
className="detail-page__metadata"
|
||||
aria-label={message("template.summary")}
|
||||
>
|
||||
{props.metadata}
|
||||
</section>
|
||||
) : null}
|
||||
<div className="detail-page__sections">{props.children}</div>
|
||||
{props.destructiveAction ? (
|
||||
<section
|
||||
className="detail-page__danger"
|
||||
aria-label={message("template.dangerActions")}
|
||||
>
|
||||
{props.destructiveAction}
|
||||
</section>
|
||||
) : null}
|
||||
</StandardPage>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormPage(
|
||||
props: PageTemplateSlots &
|
||||
Readonly<{
|
||||
errorSummary?: ReactNode;
|
||||
fields?: ReactNode;
|
||||
formActions?: ReactNode;
|
||||
guard?: ReactNode;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<StandardPage {...props}>
|
||||
{props.errorSummary ? (
|
||||
<div className="form-page__error-summary">{props.errorSummary}</div>
|
||||
) : null}
|
||||
<div className="form-page__fields">{props.fields ?? props.children}</div>
|
||||
{props.formActions ? (
|
||||
<div className="form-page__actions">{props.formActions}</div>
|
||||
) : null}
|
||||
{props.guard}
|
||||
</StandardPage>
|
||||
);
|
||||
}
|
||||
|
||||
export type StatusPageVariant =
|
||||
| "unauthenticated"
|
||||
| "forbidden"
|
||||
| "not-found"
|
||||
| "unavailable"
|
||||
| "offline"
|
||||
| "maintenance"
|
||||
| "unexpected";
|
||||
|
||||
export function StatusPage(
|
||||
props: Readonly<{
|
||||
variant: StatusPageVariant;
|
||||
heading: PageHeading;
|
||||
primaryAction?: PageActionDefinition;
|
||||
secondaryAction?: PageActionDefinition;
|
||||
supportReference?: string;
|
||||
}>,
|
||||
) {
|
||||
const { message } = useLocale();
|
||||
return (
|
||||
<section
|
||||
className={`ui-page page-template status-page status-page--${props.variant}`}
|
||||
data-status-variant={props.variant}
|
||||
>
|
||||
<PageHeader {...props.heading} />
|
||||
{props.primaryAction || props.secondaryAction ? (
|
||||
<PageActionBar
|
||||
className="status-page__actions"
|
||||
actions={
|
||||
[props.primaryAction, props.secondaryAction].filter(
|
||||
Boolean,
|
||||
) as PageActionDefinition[]
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{props.supportReference ? (
|
||||
<p className="status-page__support">
|
||||
{message("template.supportReference", {
|
||||
reference: props.supportReference,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function PageActionBar(props: Readonly<{
|
||||
actions: readonly PageActionDefinition[];
|
||||
className?: string;
|
||||
}>) {
|
||||
return (
|
||||
<div className={props.className ?? "page-template__actions"}>
|
||||
{props.actions.map((action) =>
|
||||
action.kind === "link" ? (
|
||||
<a
|
||||
className={`ui-button ui-button--${action.variant ?? "primary"}`}
|
||||
href={action.href}
|
||||
key={`${action.kind}:${action.label}`}
|
||||
>
|
||||
{action.label}
|
||||
</a>
|
||||
) : (
|
||||
<Button
|
||||
disabled={action.disabled}
|
||||
key={`${action.kind}:${action.label}`}
|
||||
onClick={action.onAction}
|
||||
variant={action.variant}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user