Runs at http://localhost:7010 via bun run dev. The playground imports the library from src through a committed symlink and tsconfig path alias, so component edits hot-reload without a build step. It has its own install root and is excluded from the published package.
377 lines
15 KiB
TypeScript
377 lines
15 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
ArrowUpRight,
|
|
Bell,
|
|
Box,
|
|
CheckCircle2,
|
|
ChevronDown,
|
|
Inbox,
|
|
} from "lucide-react";
|
|
import Border from "@moduletrace/twui/elements/Border";
|
|
import Breadcrumbs from "@moduletrace/twui/elements/Breadcrumbs";
|
|
import Card from "@moduletrace/twui/elements/Card";
|
|
import CheckBulletPoints from "@moduletrace/twui/elements/CheckBulletPoints";
|
|
import CodeBlock from "@moduletrace/twui/elements/CodeBlock";
|
|
import Dropdown from "@moduletrace/twui/elements/Dropdown";
|
|
import EmptyContent from "@moduletrace/twui/elements/EmptyContent";
|
|
import Loading from "@moduletrace/twui/elements/Loading";
|
|
import Modal from "@moduletrace/twui/elements/Modal";
|
|
import Pagination from "@moduletrace/twui/elements/Pagination";
|
|
import Paper from "@moduletrace/twui/elements/Paper";
|
|
import Popover from "@moduletrace/twui/elements/Popover";
|
|
import StarRating from "@moduletrace/twui/elements/StarRating";
|
|
import Table from "@moduletrace/twui/elements/Table";
|
|
import Tabs from "@moduletrace/twui/elements/Tabs";
|
|
import Tag from "@moduletrace/twui/elements/Tag";
|
|
import Toast from "@moduletrace/twui/elements/Toast";
|
|
import Button from "@moduletrace/twui/layout/Button";
|
|
import Divider from "@moduletrace/twui/layout/Divider";
|
|
import H1 from "@moduletrace/twui/layout/H1";
|
|
import P from "@moduletrace/twui/layout/P";
|
|
import Row from "@moduletrace/twui/layout/Row";
|
|
import Span from "@moduletrace/twui/layout/Span";
|
|
import Stack from "@moduletrace/twui/layout/Stack";
|
|
import { TWUIButtonColors } from "@moduletrace/twui/layout/Button";
|
|
import DemoSection from "../(components)/DemoSection";
|
|
|
|
const TABLE_DATA = [
|
|
{ component: "Button", folder: "layout", status: "stable" },
|
|
{ component: "Input", folder: "form", status: "stable" },
|
|
{ component: "Tabs", folder: "elements", status: "stable" },
|
|
{ component: "AceEditor", folder: "editors", status: "optional peer" },
|
|
];
|
|
|
|
const TAG_COLORS = TWUIButtonColors.filter((color) =>
|
|
["primary", "secondary", "success", "error", "warning", "gray"].includes(
|
|
color,
|
|
),
|
|
);
|
|
|
|
export default function ElementsPage() {
|
|
const [page, setPage] = useState(1);
|
|
const [toastOpen, setToastOpen] = useState(false);
|
|
const [rating, setRating] = useState(3);
|
|
|
|
return (
|
|
<Stack className="w-full gap-10">
|
|
<Stack className="gap-2">
|
|
<H1 className="!mb-0">Elements</H1>
|
|
<P className="!pt-0 opacity-70">
|
|
Composite components. Overlays render into portals appended
|
|
to <code>document.body</code>, so they escape any stacking
|
|
context in the page.
|
|
</P>
|
|
</Stack>
|
|
|
|
<DemoSection
|
|
title="Cards, tags and surfaces"
|
|
description="Card is a link when href is passed. Tag reuses the button color palette."
|
|
>
|
|
<Stack className="w-full gap-4">
|
|
<Row className="items-stretch gap-4">
|
|
<Card className="w-full max-w-sm flex-col items-start gap-2">
|
|
<Span className="font-semibold">Plain card</Span>
|
|
<Span size="small" variant="faded">
|
|
A bordered surface with no link behaviour.
|
|
</Span>
|
|
</Card>
|
|
<Card
|
|
href="#"
|
|
className="w-full max-w-sm flex-col items-start gap-2"
|
|
>
|
|
<Span className="font-semibold">
|
|
Linked card
|
|
<ArrowUpRight
|
|
size={14}
|
|
className="ml-1 inline-block"
|
|
/>
|
|
</Span>
|
|
<Span size="small" variant="faded">
|
|
The whole card is clickable.
|
|
</Span>
|
|
</Card>
|
|
<Paper className="w-full max-w-sm">
|
|
<Span className="font-semibold">Paper</Span>
|
|
<Span size="small" variant="faded">
|
|
Used for grouped content and forms.
|
|
</Span>
|
|
</Paper>
|
|
</Row>
|
|
|
|
<Row className="gap-3">
|
|
{TAG_COLORS.map((color) => (
|
|
<Tag key={color} color={color}>
|
|
{color}
|
|
</Tag>
|
|
))}
|
|
{TAG_COLORS.map((color) => (
|
|
<Tag
|
|
key={`${color}-outlined`}
|
|
color={color}
|
|
variant="outlined"
|
|
>
|
|
{color}
|
|
</Tag>
|
|
))}
|
|
</Row>
|
|
|
|
<Border className="w-full">
|
|
<Row className="w-full justify-between">
|
|
<Span size="small">
|
|
Border with default spacing
|
|
</Span>
|
|
<Tag color="gray" variant="ghost">
|
|
variant matrix
|
|
</Tag>
|
|
</Row>
|
|
</Border>
|
|
</Stack>
|
|
</DemoSection>
|
|
|
|
<DemoSection
|
|
title="Tabs"
|
|
description="Tab content stays mounted; switching is client-side state."
|
|
>
|
|
<Tabs
|
|
className="w-full"
|
|
tabsContentArray={[
|
|
{
|
|
title: "Overview",
|
|
content: (
|
|
<P className="!pt-2">
|
|
The first tab is active by default. Tab
|
|
values are slugified from the title unless
|
|
you provide one.
|
|
</P>
|
|
),
|
|
},
|
|
{
|
|
title: "Details",
|
|
content: (
|
|
<CheckBulletPoints
|
|
className="pt-2"
|
|
bulletPoints={[
|
|
{
|
|
title: "Accepts an array of tabs",
|
|
},
|
|
{
|
|
title: "Supports a default value",
|
|
},
|
|
{
|
|
title: "Can sync to the URL hash",
|
|
},
|
|
]}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: "Disabled-looking",
|
|
content: (
|
|
<P className="!pt-2 opacity-60">
|
|
This is just a third tab; there is no
|
|
disabled state in the current API.
|
|
</P>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</DemoSection>
|
|
|
|
<DemoSection
|
|
title="Table and pagination"
|
|
description="Table derives columns from the keys of the first row."
|
|
>
|
|
<Stack className="w-full gap-4">
|
|
<Border className="w-full overflow-hidden !p-0">
|
|
<Table data={TABLE_DATA} />
|
|
</Border>
|
|
<Pagination
|
|
page={page}
|
|
setPage={setPage}
|
|
count={42}
|
|
limit={5}
|
|
className="justify-center"
|
|
/>
|
|
<Span size="small" variant="faded">
|
|
Page {page} of {Math.ceil(42 / 5)}
|
|
</Span>
|
|
</Stack>
|
|
</DemoSection>
|
|
|
|
<DemoSection
|
|
title="Overlays"
|
|
description="Modal and Popover share the same implementation; Popover renders in place while Modal is a full dialog."
|
|
>
|
|
<Row className="items-start gap-4">
|
|
<OverlayDemo />
|
|
</Row>
|
|
</DemoSection>
|
|
|
|
<DemoSection
|
|
title="Dropdown"
|
|
description="A lightweight positioned panel with click/hover targets."
|
|
>
|
|
<Dropdown
|
|
target={
|
|
<Button
|
|
title="Open dropdown"
|
|
color="gray"
|
|
variant="outlined"
|
|
afterIcon={<ChevronDown size={16} />}
|
|
>
|
|
Actions
|
|
</Button>
|
|
}
|
|
contentWrapperProps={{ className: "min-w-48" }}
|
|
>
|
|
<Stack className="gap-1">
|
|
<Span size="small">Edit item</Span>
|
|
<Span size="small">Duplicate item</Span>
|
|
<Divider />
|
|
<Span size="small" className="text-error">
|
|
Delete item
|
|
</Span>
|
|
</Stack>
|
|
</Dropdown>
|
|
</DemoSection>
|
|
|
|
<DemoSection
|
|
title="Toasts"
|
|
description="Toast portals into a body-level root and auto-closes after four seconds."
|
|
>
|
|
<Row className="gap-3">
|
|
<Button
|
|
title="Show success toast"
|
|
color="success"
|
|
onClick={() => setToastOpen(true)}
|
|
beforeIcon={<Bell size={16} />}
|
|
>
|
|
Show toast
|
|
</Button>
|
|
<Span size="small" variant="faded">
|
|
Hovering the toast pauses the timer.
|
|
</Span>
|
|
</Row>
|
|
<Toast
|
|
open={toastOpen}
|
|
setOpen={setToastOpen}
|
|
color="success"
|
|
>
|
|
Build finished successfully.
|
|
</Toast>
|
|
</DemoSection>
|
|
|
|
<DemoSection
|
|
title="Status and feedback"
|
|
description="Loading, empty states, code blocks and ratings."
|
|
>
|
|
<div className="grid w-full gap-6 md:grid-cols-2">
|
|
<Stack className="w-full gap-3">
|
|
<Span className="font-semibold">Loading</Span>
|
|
<Row className="gap-4">
|
|
<Loading size="smaller" />
|
|
<Loading size="small" />
|
|
<Loading size="normal" />
|
|
<Loading size="large" />
|
|
</Row>
|
|
<Span className="font-semibold">Star rating</Span>
|
|
<StarRating
|
|
value={rating}
|
|
allowRating
|
|
changeHandler={setRating}
|
|
/>
|
|
<Span size="small" variant="faded">
|
|
Current rating: {rating}
|
|
</Span>
|
|
</Stack>
|
|
|
|
<EmptyContent
|
|
title="No results — EmptyContent takes an optional link and icon."
|
|
icon={<Inbox size={24} />}
|
|
url="#"
|
|
/>
|
|
</div>
|
|
|
|
<Stack className="mt-6 w-full gap-2">
|
|
<Span className="font-semibold">Code block</Span>
|
|
<CodeBlock data-title="install">
|
|
{`bun add @moduletrace/twui`}
|
|
</CodeBlock>
|
|
</Stack>
|
|
</DemoSection>
|
|
|
|
<DemoSection
|
|
title="Breadcrumbs"
|
|
description="Infer crumbs from the current path, or pass pageUrl to render deterministically."
|
|
>
|
|
<Stack className="w-full gap-4">
|
|
<Breadcrumbs pageUrl="/elements/overlays/modal" />
|
|
<Row className="gap-2">
|
|
<CheckCircle2 size={16} className="text-success" />
|
|
<Span size="small" variant="faded">
|
|
The divider and back button are configurable.
|
|
</Span>
|
|
</Row>
|
|
</Stack>
|
|
</DemoSection>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
function OverlayDemo() {
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
|
|
return (
|
|
<>
|
|
<Button
|
|
title="Open modal"
|
|
onClick={() => setModalOpen(true)}
|
|
beforeIcon={<Box size={16} />}
|
|
>
|
|
Open modal
|
|
</Button>
|
|
<Modal
|
|
open={modalOpen}
|
|
setOpen={setModalOpen}
|
|
target={null}
|
|
no_cancel_button
|
|
>
|
|
<Stack className="w-full gap-3">
|
|
<Span className="text-lg font-semibold">Modal content</Span>
|
|
<P className="!pt-0">
|
|
The modal is controlled by the <code>open</code> and{" "}
|
|
<code>setOpen</code> props. Without a target it renders
|
|
nothing until you set it.
|
|
</P>
|
|
<Row className="justify-end">
|
|
<Button
|
|
title="Close modal"
|
|
color="gray"
|
|
variant="outlined"
|
|
onClick={() => setModalOpen(false)}
|
|
>
|
|
Close
|
|
</Button>
|
|
</Row>
|
|
</Stack>
|
|
</Modal>
|
|
|
|
<Popover
|
|
trigger="hover"
|
|
target={
|
|
<Tag color="secondary" variant="outlined">
|
|
Hover for a popover
|
|
</Tag>
|
|
}
|
|
>
|
|
<Stack className="w-56 gap-1">
|
|
<Span className="font-semibold">Popover</Span>
|
|
<Span size="small" variant="faded">
|
|
Same props as Modal, plus hover trigger support.
|
|
</Span>
|
|
</Stack>
|
|
</Popover>
|
|
</>
|
|
);
|
|
}
|