Add bunext playground for local component development
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.
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user