Collapsible
Free, copy-and-go Collapsible components built on the SevenUI Collapsible primitive.Read the primitive docs.
"use client";
import { ChevronDownIcon } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
const changes = [
"Workspace invites now expire after 7 days instead of 30.",
"CSV exports include the timezone of every timestamp.",
"Fixed a crash when renaming a project with an emoji in its name.",
];
export default function Collapsible01() {
return (
<Collapsible className="w-full max-w-md rounded-lg border bg-background">
<CollapsibleTrigger className="group flex w-full items-center justify-between gap-4 rounded-lg px-4 py-3 text-left text-sm font-medium outline-none hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50">
<span className="flex flex-col gap-0.5">
<span>Version 4.12 release notes</span>
<span className="text-xs font-normal text-muted-foreground">
Published September 18
</span>
</span>
<ChevronDownIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-panel-open:rotate-180"
/>
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="flex list-disc flex-col gap-1.5 border-t py-3 pr-4 pl-8 text-sm text-muted-foreground">
{changes.map((change) => (
<li key={change}>{change}</li>
))}
</ul>
</CollapsibleContent>
</Collapsible>
);
}
npx shadcn@latest add @sevenui/component/collapsible-01pnpm dlx shadcn@latest add @sevenui/component/collapsible-01yarn dlx shadcn@latest add @sevenui/component/collapsible-01bunx --bun shadcn@latest add @sevenui/component/collapsible-01About this workspace
Northwind Studio is a twelve-person product team shipping the mobile booking app for independent fitness studios. We plan in two-week cycles and review designs every Thursday.
"use client";
import * as React from "react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
export default function Collapsible02() {
const [open, setOpen] = React.useState(false);
return (
<Collapsible
open={open}
onOpenChange={setOpen}
className="w-full max-w-md text-sm leading-relaxed"
>
<h3 className="mb-2 text-base font-semibold">About this workspace</h3>
<p className="text-muted-foreground">
Northwind Studio is a twelve-person product team shipping the mobile
booking app for independent fitness studios. We plan in two-week cycles
and review designs every Thursday.
</p>
<CollapsibleContent>
<div className="flex flex-col gap-3 pt-3 text-muted-foreground">
<p>
New members get read access to every project by default. Editors
can publish to staging, while production releases need approval
from one of the three workspace admins.
</p>
<p>
Questions about access or billing go to the #ops channel, which is
staffed from 9am to 6pm Central European Time on weekdays.
</p>
</div>
</CollapsibleContent>
<CollapsibleTrigger className="mt-2 rounded-sm font-medium text-foreground underline underline-offset-4 decoration-border outline-none hover:decoration-foreground focus-visible:ring-3 focus-visible:ring-ring/50">
{open ? "Show less" : "Read more"}
</CollapsibleTrigger>
</Collapsible>
);
}
npx shadcn@latest add @sevenui/component/collapsible-02pnpm dlx shadcn@latest add @sevenui/component/collapsible-02yarn dlx shadcn@latest add @sevenui/component/collapsible-02bunx --bun shadcn@latest add @sevenui/component/collapsible-02"use client";
import * as React from "react";
import { ChevronDownIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Label } from "@/components/ui/label";
type Facet = {
id: string;
name: string;
options: { value: string; count: number }[];
};
const facets: Facet[] = [
{
id: "category",
name: "Category",
options: [
{ value: "Trail runners", count: 42 },
{ value: "Road runners", count: 67 },
{ value: "Hiking boots", count: 28 },
],
},
{
id: "size",
name: "Size (US)",
options: [
{ value: "8", count: 51 },
{ value: "9", count: 58 },
{ value: "10", count: 61 },
{ value: "11", count: 39 },
],
},
{
id: "width",
name: "Width",
options: [
{ value: "Regular", count: 104 },
{ value: "Wide", count: 33 },
],
},
];
const catalogSize = 137;
const slug = (value: string) =>
value.toLowerCase().replace(/[^a-z0-9]+/g, "-");
export default function Collapsible03() {
const [selected, setSelected] = React.useState<Record<string, string[]>>({
category: ["Trail runners"],
size: ["9", "10"],
});
const total = Object.values(selected).reduce(
(sum, values) => sum + values.length,
0,
);
// Rough result estimate: each facet with a selection narrows the catalog
// by the share of products its selected options cover.
const results = Math.round(
facets.reduce((estimate, facet) => {
const values = selected[facet.id] ?? [];
if (values.length === 0) return estimate;
const all = facet.options.reduce((sum, option) => sum + option.count, 0);
const picked = facet.options
.filter((option) => values.includes(option.value))
.reduce((sum, option) => sum + option.count, 0);
return (estimate * picked) / all;
}, catalogSize),
);
const toggle = (facetId: string, value: string, checked: boolean) => {
setSelected((current) => {
const values = current[facetId] ?? [];
return {
...current,
[facetId]: checked
? [...values, value]
: values.filter((item) => item !== value),
};
});
};
return (
<aside
aria-labelledby="collapsible-03-title"
className="flex w-full max-w-xs flex-col rounded-xl border bg-card text-card-foreground"
>
<header className="flex items-center justify-between gap-2 border-b px-4 py-3">
<h3 id="collapsible-03-title" className="text-sm font-semibold">
Filters
</h3>
<Button
variant="ghost"
size="xs"
disabled={total === 0}
onClick={() => setSelected({})}
>
Clear all
</Button>
</header>
<div className="flex flex-col divide-y">
{facets.map((facet) => {
const values = selected[facet.id] ?? [];
return (
<Collapsible key={facet.id} defaultOpen={facet.id !== "width"}>
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-medium outline-none hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset">
<span className="flex-1">{facet.name}</span>
{values.length > 0 && (
<span className="rounded-full bg-primary px-1.5 text-xs text-primary-foreground tabular-nums">
{values.length}
<span className="sr-only"> selected</span>
</span>
)}
<ChevronDownIcon
aria-hidden="true"
className="size-4 text-muted-foreground transition-transform duration-200 group-data-panel-open:rotate-180"
/>
</CollapsibleTrigger>
<CollapsibleContent>
<fieldset className="flex flex-col gap-2.5 px-4 pb-4">
<legend className="sr-only">{facet.name}</legend>
{facet.options.map((option) => {
const id = `collapsible-03-${facet.id}-${slug(
option.value,
)}`;
return (
<div
key={option.value}
className="flex items-center gap-2.5"
>
<Checkbox
id={id}
checked={values.includes(option.value)}
onCheckedChange={(checked) =>
toggle(facet.id, option.value, checked)
}
/>
<Label htmlFor={id} className="flex-1 font-normal">
{option.value}
</Label>
<span className="text-xs text-muted-foreground tabular-nums">
{option.count}
</span>
</div>
);
})}
</fieldset>
</CollapsibleContent>
</Collapsible>
);
})}
</div>
<footer className="border-t p-3">
<Button className="w-full" disabled={results === 0}>
{results === 0
? "No matching products"
: `Show ${results} ${results === 1 ? "result" : "results"}`}
</Button>
</footer>
</aside>
);
}
npx shadcn@latest add @sevenui/component/collapsible-03pnpm dlx shadcn@latest add @sevenui/component/collapsible-03yarn dlx shadcn@latest add @sevenui/component/collapsible-03bunx --bun shadcn@latest add @sevenui/component/collapsible-03- Production serversk_live_••••8f2a2 minutes ago
- Staging workersk_live_••••c41eYesterday
"use client";
import {
ChevronRightIcon,
KeyRoundIcon,
LockIcon,
ReceiptIcon,
ScrollTextIcon,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
const apiKeys = [
{ name: "Production server", suffix: "8f2a", lastUsed: "2 minutes ago" },
{ name: "Staging worker", suffix: "c41e", lastUsed: "Yesterday" },
];
const triggerClassName =
"group flex w-full items-center gap-3 px-4 py-3 text-left text-sm font-medium outline-none hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent";
const chevron = (
<ChevronRightIcon
aria-hidden="true"
className="size-4 text-muted-foreground transition-transform duration-200 group-data-disabled:opacity-40 group-data-panel-open:rotate-90"
/>
);
export default function Collapsible04() {
return (
<div className="w-full max-w-md divide-y overflow-hidden rounded-lg border">
<Collapsible defaultOpen>
<CollapsibleTrigger className={triggerClassName}>
<KeyRoundIcon aria-hidden="true" className="size-4" />
<span className="flex-1">API keys</span>
<Badge variant="outline">Read-only</Badge>
{chevron}
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="flex flex-col gap-2 px-4 pb-4">
{apiKeys.map((key) => (
<li
key={key.suffix}
className="flex items-center justify-between gap-3 rounded-md bg-muted/50 px-3 py-2 text-sm"
>
<span className="flex min-w-0 flex-col">
<span className="truncate font-medium">{key.name}</span>
<span className="font-mono text-xs text-muted-foreground">
sk_live_••••{key.suffix}
</span>
</span>
<span className="shrink-0 text-xs text-muted-foreground">
{key.lastUsed}
</span>
</li>
))}
</ul>
</CollapsibleContent>
</Collapsible>
<Collapsible>
<CollapsibleTrigger className={triggerClassName}>
<ReceiptIcon aria-hidden="true" className="size-4" />
<span className="flex-1">Invoices</span>
<span className="text-xs font-normal text-muted-foreground">
Empty
</span>
{chevron}
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mx-4 mb-4 flex flex-col items-center gap-1 rounded-md border border-dashed px-4 py-6 text-center">
<p className="text-sm font-medium">No invoices yet</p>
<p className="text-xs text-muted-foreground">
Your first invoice arrives when the trial ends on October 9.
</p>
</div>
</CollapsibleContent>
</Collapsible>
<Collapsible disabled>
<CollapsibleTrigger className={triggerClassName}>
<ScrollTextIcon
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
<span className="flex-1 text-muted-foreground">Audit log</span>
<Badge variant="secondary">
<LockIcon aria-hidden="true" />
Enterprise
</Badge>
{chevron}
</CollapsibleTrigger>
<CollapsibleContent />
</Collapsible>
<div className="flex items-center justify-between gap-3 bg-muted/30 px-4 py-3 text-xs text-muted-foreground">
<span>Audit log requires the Enterprise plan.</span>
<Button size="xs" variant="outline">
Compare plans
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/collapsible-04pnpm dlx shadcn@latest add @sevenui/component/collapsible-04yarn dlx shadcn@latest add @sevenui/component/collapsible-04bunx --bun shadcn@latest add @sevenui/component/collapsible-04Environment variables
1 of 3 expanded
- DATABASE_URL
- postgres://prod-db.internal:5432/app
- STRIPE_MODE
- live
- LOG_LEVEL
- warn
"use client";
import * as React from "react";
import {
ChevronDownIcon,
ChevronsDownUpIcon,
ChevronsUpDownIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
const environments = [
{
id: "production",
name: "Production",
vars: [
{ key: "DATABASE_URL", value: "postgres://prod-db.internal:5432/app" },
{ key: "STRIPE_MODE", value: "live" },
{ key: "LOG_LEVEL", value: "warn" },
],
},
{
id: "preview",
name: "Preview",
vars: [
{ key: "DATABASE_URL", value: "postgres://preview-db.internal:5432/app" },
{ key: "STRIPE_MODE", value: "test" },
],
},
{
id: "development",
name: "Development",
vars: [
{ key: "DATABASE_URL", value: "postgres://localhost:5432/app" },
{ key: "LOG_LEVEL", value: "debug" },
],
},
];
export default function Collapsible05() {
const [openIds, setOpenIds] = React.useState<string[]>(["production"]);
const allOpen = openIds.length === environments.length;
const setOpen = (id: string, open: boolean) => {
setOpenIds((current) =>
open ? [...current, id] : current.filter((value) => value !== id),
);
};
return (
<div className="flex w-full max-w-md flex-col gap-3">
<div className="flex items-center justify-between gap-3">
<div>
<h3 className="text-sm font-semibold">Environment variables</h3>
<p className="text-xs text-muted-foreground">
{openIds.length} of {environments.length} expanded
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() =>
setOpenIds(allOpen ? [] : environments.map((env) => env.id))
}
>
{allOpen ? (
<ChevronsDownUpIcon aria-hidden="true" />
) : (
<ChevronsUpDownIcon aria-hidden="true" />
)}
{allOpen ? "Collapse all" : "Expand all"}
</Button>
</div>
<div className="flex flex-col gap-2">
{environments.map((env) => (
<Collapsible
key={env.id}
open={openIds.includes(env.id)}
onOpenChange={(open) => setOpen(env.id, open)}
className="rounded-lg border bg-card"
>
<CollapsibleTrigger className="group flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm font-medium outline-none focus-visible:ring-3 focus-visible:ring-ring/50">
<ChevronDownIcon
aria-hidden="true"
className="size-4 -rotate-90 text-muted-foreground transition-transform duration-200 group-data-panel-open:rotate-0"
/>
<span className="flex-1">{env.name}</span>
<span className="text-xs font-normal text-muted-foreground tabular-nums">
{env.vars.length} variables
</span>
</CollapsibleTrigger>
<CollapsibleContent>
<dl className="flex flex-col border-t px-3 py-2 font-mono text-xs">
{env.vars.map((variable) => (
<div
key={variable.key}
className="flex flex-col gap-0.5 py-1.5 sm:flex-row sm:gap-3"
>
<dt className="shrink-0 font-medium sm:w-32">
{variable.key}
</dt>
<dd className="min-w-0 truncate text-muted-foreground">
{variable.value}
</dd>
</div>
))}
</dl>
</CollapsibleContent>
</Collapsible>
))}
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/collapsible-05pnpm dlx shadcn@latest add @sevenui/component/collapsible-05yarn dlx shadcn@latest add @sevenui/component/collapsible-05bunx --bun shadcn@latest add @sevenui/component/collapsible-05- MCMaya ChenVP of Engineering
"use client";
import { ChevronRightIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/components/ui/avatar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
type Member = { name: string; role: string };
type Team = { name: string; members: Member[]; teams?: Team[] };
const org: Team = {
name: "Engineering",
members: [{ name: "Maya Chen", role: "VP of Engineering" }],
teams: [
{
name: "Platform",
members: [
{ name: "Daniel Okafor", role: "Engineering Manager" },
{ name: "Priya Raman", role: "Staff Engineer" },
],
teams: [
{
name: "Infrastructure",
members: [
{ name: "Lucas Moreau", role: "Site Reliability Engineer" },
{ name: "Hana Sato", role: "Backend Engineer" },
],
},
],
},
{
name: "Product",
members: [
{ name: "Sofia Lindqvist", role: "Engineering Manager" },
{ name: "Omar Haddad", role: "Frontend Engineer" },
{ name: "Grace Kim", role: "Mobile Engineer" },
],
},
],
};
const initials = (name: string) =>
name
.split(" ")
.map((part) => part[0])
.join("");
const countMembers = (team: Team): number =>
team.members.length +
(team.teams ?? []).reduce((sum, child) => sum + countMembers(child), 0);
function TeamNode({
team,
defaultOpen,
}: {
team: Team;
defaultOpen?: boolean;
}) {
return (
<Collapsible defaultOpen={defaultOpen}>
<CollapsibleTrigger className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50">
<ChevronRightIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-panel-open:rotate-90"
/>
<span className="flex-1 truncate font-medium">{team.name}</span>
<AvatarGroup
aria-hidden="true"
className="-space-x-1.5 transition-opacity duration-200 group-data-panel-open:opacity-0"
>
{team.members.slice(0, 3).map((member) => (
<Avatar key={member.name} size="sm">
<AvatarFallback className="group-data-[size=sm]/avatar:text-[0.625rem]">
{initials(member.name)}
</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
<span className="w-6 text-right text-xs text-muted-foreground tabular-nums">
{countMembers(team)}
<span className="sr-only"> members</span>
</span>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="ml-4 flex flex-col gap-0.5 border-l pl-2">
<ul className="flex flex-col">
{team.members.map((member) => (
<li
key={member.name}
className="flex items-center gap-2.5 rounded-md px-2 py-1.5"
>
<Avatar size="sm">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback className="group-data-[size=sm]/avatar:text-[0.625rem]">
{initials(member.name)}
</AvatarFallback>
</Avatar>
<span className="flex min-w-0 flex-col">
<span className="truncate text-sm">{member.name}</span>
<span className="truncate text-xs text-muted-foreground">
{member.role}
</span>
</span>
</li>
))}
</ul>
{team.teams?.map((child) => (
<TeamNode key={child.name} team={child} />
))}
</div>
</CollapsibleContent>
</Collapsible>
);
}
export default function Collapsible06() {
return (
<section
aria-label="Engineering org chart"
className="w-full max-w-sm rounded-lg border bg-card p-2"
>
<TeamNode team={org} defaultOpen />
</section>
);
}
npx shadcn@latest add @sevenui/component/collapsible-06pnpm dlx shadcn@latest add @sevenui/component/collapsible-06yarn dlx shadcn@latest add @sevenui/component/collapsible-06bunx --bun shadcn@latest add @sevenui/component/collapsible-06"use client";
import * as React from "react";
import {
CircleCheckIcon,
ChevronRightIcon,
SlidersHorizontalIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
import { Switch } from "@/components/ui/switch";
const retryPolicies = [
{ value: "exponential", label: "Exponential backoff (5 attempts)" },
{ value: "linear", label: "Linear, every 60 seconds (3 attempts)" },
{ value: "none", label: "Do not retry" },
];
export default function Collapsible07() {
const [advancedOpen, setAdvancedOpen] = React.useState(false);
const [addedUrl, setAddedUrl] = React.useState<string | null>(null);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = event.currentTarget;
const url = new FormData(form).get("url");
// reset() fires onReset synchronously, so set the status afterwards.
form.reset();
setAddedUrl(typeof url === "string" ? url : null);
}
function handleReset() {
setAddedUrl(null);
setAdvancedOpen(false);
}
return (
<form
className="flex w-full max-w-md flex-col gap-5 rounded-xl border bg-card p-5 text-card-foreground"
onSubmit={handleSubmit}
onReset={handleReset}
>
<div className="flex flex-col gap-1">
<h3 className="font-semibold">Add webhook endpoint</h3>
<p className="text-sm text-muted-foreground">
We send a POST request to this URL whenever an order changes state.
</p>
</div>
<Field>
<FieldLabel htmlFor="collapsible-07-url">Endpoint URL</FieldLabel>
<Input
id="collapsible-07-url"
name="url"
type="url"
placeholder="https://api.example.com/hooks/orders"
required
/>
</Field>
<Collapsible
open={advancedOpen}
onOpenChange={setAdvancedOpen}
className="rounded-lg border"
>
<CollapsibleTrigger className="group flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm font-medium outline-none hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50">
<SlidersHorizontalIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<span className="flex-1">Advanced options</span>
{!advancedOpen && (
<span className="hidden text-xs font-normal text-muted-foreground sm:inline">
Signing, retries, timeout
</span>
)}
<ChevronRightIcon
aria-hidden="true"
className="size-4 text-muted-foreground transition-transform group-data-panel-open:rotate-90"
/>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-4 border-t px-3 pt-4 pb-3">
<Field>
<FieldLabel htmlFor="collapsible-07-secret">
Signing secret
</FieldLabel>
<Input
id="collapsible-07-secret"
defaultValue="whsec_7Kq2mZr81xPa"
className="font-mono"
/>
<FieldDescription>
Verify the X-Signature header against this value.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="collapsible-07-retry">
Retry policy
</FieldLabel>
<NativeSelect
id="collapsible-07-retry"
defaultValue="exponential"
className="w-full"
>
{retryPolicies.map((policy) => (
<NativeSelectOption key={policy.value} value={policy.value}>
{policy.label}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
<Field>
<FieldLabel htmlFor="collapsible-07-timeout">
Timeout (seconds)
</FieldLabel>
<Input
id="collapsible-07-timeout"
type="number"
min={1}
max={30}
defaultValue={10}
className="w-24"
/>
</Field>
<Field orientation="horizontal" className="justify-between">
<FieldLabel htmlFor="collapsible-07-batch">
Batch events every 5 seconds
</FieldLabel>
<Switch id="collapsible-07-batch" />
</Field>
</div>
</CollapsibleContent>
</Collapsible>
<div className="flex flex-wrap items-center justify-end gap-2">
{addedUrl && (
<p
role="status"
className="mr-auto flex min-w-0 items-center gap-1.5 text-sm text-muted-foreground"
>
<CircleCheckIcon
aria-hidden="true"
className="size-4 shrink-0 text-success"
/>
<span className="truncate">Added {addedUrl}</span>
</p>
)}
<Button type="reset" variant="ghost">
Cancel
</Button>
<Button type="submit">Add endpoint</Button>
</div>
</form>
);
}
npx shadcn@latest add @sevenui/component/collapsible-07pnpm dlx shadcn@latest add @sevenui/component/collapsible-07yarn dlx shadcn@latest add @sevenui/component/collapsible-07bunx --bun shadcn@latest add @sevenui/component/collapsible-07"use client";
import { ChevronDownIcon, ShoppingBagIcon } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Separator } from "@/components/ui/separator";
const lineItems = [
{
name: "Merino crew sweater",
variant: "Oat · Size M",
quantity: 1,
price: 128,
},
{
name: "Organic cotton tee",
variant: "Charcoal · Size M",
quantity: 2,
price: 38,
},
{
name: "Wool blend socks",
variant: "3-pack · One size",
quantity: 1,
price: 24,
},
];
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
const subtotal = lineItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
const discount = 22.8;
const shipping = 0;
const tax = 17.1;
const total = subtotal - discount + shipping + tax;
export default function Collapsible08() {
return (
<Collapsible className="w-full max-w-sm overflow-hidden rounded-xl border bg-card text-card-foreground">
<CollapsibleTrigger className="group flex w-full items-center gap-3 bg-muted/50 px-4 py-3.5 text-left outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset">
<ShoppingBagIcon aria-hidden="true" className="size-4 shrink-0" />
<span className="flex-1 text-sm font-medium">
<span className="group-data-panel-open:hidden">
Show order summary
</span>
<span className="hidden group-data-panel-open:inline">
Hide order summary
</span>
</span>
<ChevronDownIcon
aria-hidden="true"
className="size-4 text-muted-foreground transition-transform group-data-panel-open:rotate-180"
/>
<span className="font-semibold tabular-nums">
{currency.format(total)}
</span>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-4 border-t p-4">
<ul className="flex flex-col gap-3">
{lineItems.map((item) => (
<li key={item.name} className="flex items-center gap-3">
<div className="relative shrink-0">
<img
src="/placeholder.svg"
alt=""
className="size-12 rounded-md border bg-muted object-cover"
/>
<span className="absolute -top-1.5 -right-1.5 flex size-5 items-center justify-center rounded-full bg-primary text-[0.7rem] font-medium text-primary-foreground tabular-nums">
<span className="sr-only">Quantity </span>
{item.quantity}
</span>
</div>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">
{item.name}
</span>
<span className="text-xs text-muted-foreground">
{item.variant}
</span>
</div>
<span className="text-sm tabular-nums">
{currency.format(item.price * item.quantity)}
</span>
</li>
))}
</ul>
<Separator />
<dl className="grid grid-cols-[1fr_auto] gap-y-1.5 text-sm">
<dt className="text-muted-foreground">Subtotal</dt>
<dd className="text-right tabular-nums">
{currency.format(subtotal)}
</dd>
<dt className="text-muted-foreground">
Discount{" "}
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
AUTUMN10
</span>
</dt>
<dd className="text-right text-success tabular-nums">
−{currency.format(discount)}
</dd>
<dt className="text-muted-foreground">Shipping</dt>
<dd className="text-right">Free</dd>
<dt className="text-muted-foreground">Estimated tax</dt>
<dd className="text-right tabular-nums">{currency.format(tax)}</dd>
</dl>
<Separator />
<div className="flex items-baseline justify-between">
<span className="font-medium">Total</span>
<span className="text-lg font-semibold tabular-nums">
<span className="mr-1 text-xs font-normal text-muted-foreground">
USD
</span>
{currency.format(total)}
</span>
</div>
</div>
</CollapsibleContent>
</Collapsible>
);
}
npx shadcn@latest add @sevenui/component/collapsible-08pnpm dlx shadcn@latest add @sevenui/component/collapsible-08yarn dlx shadcn@latest add @sevenui/component/collapsible-08bunx --bun shadcn@latest add @sevenui/component/collapsible-08Workspace members
3 of 10 seats used
- MTMaya Thompsonmaya@northwind.ioOwner
- DBDaniel Brooksdaniel@northwind.ioAdmin
- PSPriya Shahpriya@northwind.ioMember
"use client";
import * as React from "react";
import { ChevronRightIcon, MailIcon, RotateCwIcon, XIcon } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
const members = [
{ name: "Maya Thompson", email: "maya@northwind.io", role: "Owner" },
{ name: "Daniel Brooks", email: "daniel@northwind.io", role: "Admin" },
{ name: "Priya Shah", email: "priya@northwind.io", role: "Member" },
];
const initialInvites = [
{ email: "leo.martin@northwind.io", role: "Member", sent: "2 days ago" },
{ email: "hannah@studio-fold.com", role: "Guest", sent: "5 days ago" },
{ email: "sam.okafor@northwind.io", role: "Admin", sent: "Expired" },
];
function initials(name: string) {
return name
.split(" ")
.map((part) => part[0])
.join("");
}
export default function Collapsible09() {
const [invites, setInvites] = React.useState(initialInvites);
const [resent, setResent] = React.useState<string[]>([]);
return (
<section
aria-labelledby="collapsible-09-title"
className="w-full max-w-md rounded-xl border bg-card text-card-foreground"
>
<header className="flex items-center justify-between gap-3 p-4">
<div className="flex flex-col gap-0.5">
<h3 id="collapsible-09-title" className="font-semibold">
Workspace members
</h3>
<p className="text-sm text-muted-foreground">
{members.length} of 10 seats used
</p>
</div>
<Button size="sm">Invite</Button>
</header>
<ul className="flex flex-col border-t">
{members.map((member) => (
<li
key={member.email}
className="flex items-center gap-3 border-b px-4 py-3"
>
<Avatar>
<AvatarFallback>{initials(member.name)}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">
{member.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{member.email}
</span>
</div>
<span className="text-xs text-muted-foreground">{member.role}</span>
</li>
))}
</ul>
<Collapsible>
<CollapsibleTrigger className="group flex w-full items-center gap-2 rounded-b-xl px-4 py-3 text-left text-sm font-medium outline-none hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 data-panel-open:rounded-none">
<ChevronRightIcon
aria-hidden="true"
className="size-4 text-muted-foreground transition-transform group-data-panel-open:rotate-90"
/>
<span className="flex-1">Pending invitations</span>
<Badge variant="secondary" className="tabular-nums">
{invites.length}
</Badge>
</CollapsibleTrigger>
<CollapsibleContent>
{invites.length === 0 && (
<p className="px-4 pb-4 text-sm text-muted-foreground">
No pending invitations. Everyone you invited has joined.
</p>
)}
<ul className="flex flex-col gap-1 px-2 pb-2 empty:hidden">
{invites.map((invite) => {
const expired = invite.sent === "Expired";
const wasResent = resent.includes(invite.email);
return (
<li
key={invite.email}
className="flex items-center gap-3 rounded-lg bg-muted/40 px-2 py-2"
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-full border border-dashed text-muted-foreground">
<MailIcon aria-hidden="true" className="size-3.5" />
</span>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm">{invite.email}</span>
<span
className={
expired && !wasResent
? "text-xs text-destructive"
: "text-xs text-muted-foreground"
}
>
{invite.role} ·{" "}
{wasResent
? "Sent just now"
: expired
? "Invitation expired"
: `Sent ${invite.sent}`}
</span>
</div>
<Button
variant="ghost"
size="icon-sm"
disabled={wasResent}
aria-label={`Resend invitation to ${invite.email}`}
onClick={() =>
setResent((current) => [...current, invite.email])
}
>
<RotateCwIcon aria-hidden="true" />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label={`Revoke invitation to ${invite.email}`}
onClick={() =>
setInvites((current) =>
current.filter((item) => item.email !== invite.email),
)
}
>
<XIcon aria-hidden="true" />
</Button>
</li>
);
})}
</ul>
</CollapsibleContent>
</Collapsible>
</section>
);
}
npx shadcn@latest add @sevenui/component/collapsible-09pnpm dlx shadcn@latest add @sevenui/component/collapsible-09yarn dlx shadcn@latest add @sevenui/component/collapsible-09bunx --bun shadcn@latest add @sevenui/component/collapsible-09Estimated bill · Sep 1 – Sep 30
$184.20
"use client";
import { ChevronDownIcon, ReceiptTextIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const charges = [
{
product: "Pro plan",
detail: "5 seats × $20",
amount: 100,
color: "bg-chart-1",
},
{
product: "Build minutes",
detail: "420 min over the 3,000 included",
amount: 33.6,
color: "bg-chart-2",
},
{
product: "Bandwidth",
detail: "1.24 TB at $0.04 / GB over 1 TB",
amount: 9.8,
color: "bg-chart-3",
},
{
product: "Image optimization",
detail: "41,200 source images",
amount: 40.8,
color: "bg-chart-4",
},
];
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
const total = charges.reduce((sum, charge) => sum + charge.amount, 0);
export default function Collapsible10() {
return (
<section
aria-labelledby="collapsible-10-title"
className="flex w-full max-w-md flex-col gap-5 rounded-xl border bg-card p-5 text-card-foreground"
>
<div className="flex items-start justify-between gap-4">
<div className="flex flex-col gap-1">
<h3
id="collapsible-10-title"
className="text-sm text-muted-foreground"
>
Estimated bill ·{" "}
<span className="whitespace-nowrap">Sep 1 – Sep 30</span>
</h3>
<p className="text-3xl font-semibold tracking-tight tabular-nums">
{currency.format(total)}
</p>
</div>
<Button variant="outline" size="sm">
<ReceiptTextIcon aria-hidden="true" data-icon="inline-start" />
Invoices
</Button>
</div>
<Meter value={3420} max={4000} aria-valuetext="3,420 of 4,000 minutes">
<div className="flex items-baseline justify-between gap-2">
<MeterLabel>Build minutes</MeterLabel>
<MeterValue className="tabular-nums">
{() => "3,420 / 4,000 min"}
</MeterValue>
</div>
</Meter>
<div
role="img"
aria-label="Share of this bill by product"
className="flex h-2 w-full gap-0.5 overflow-hidden rounded-full"
>
{charges.map((charge) => (
<span
key={charge.product}
className={charge.color}
style={{ width: `${(charge.amount / total) * 100}%` }}
/>
))}
</div>
<Collapsible className="-mx-2">
<CollapsibleTrigger className="group flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-sm font-medium outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50">
Usage breakdown
<ChevronDownIcon
aria-hidden="true"
className="size-4 text-muted-foreground transition-transform group-data-panel-open:rotate-180"
/>
</CollapsibleTrigger>
<CollapsibleContent>
<table className="mt-2 w-full text-sm">
<caption className="sr-only">Charges this billing period</caption>
<thead className="sr-only">
<tr>
<th scope="col">Product</th>
<th scope="col">Amount</th>
</tr>
</thead>
<tbody>
{charges.map((charge) => (
<tr key={charge.product} className="border-b last:border-b-0">
<th
scope="row"
className="px-2 py-2.5 text-left align-top font-normal"
>
<span className="flex items-center gap-2 font-medium">
<span
aria-hidden="true"
className={`size-2 shrink-0 rounded-full ${charge.color}`}
/>
{charge.product}
</span>
<span className="block pl-4 text-xs text-muted-foreground">
{charge.detail}
</span>
</th>
<td className="px-2 py-2.5 text-right align-top tabular-nums">
{currency.format(charge.amount)}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="border-t">
<th scope="row" className="px-2 pt-2.5 text-left font-medium">
Total before tax
</th>
<td className="px-2 pt-2.5 text-right font-semibold tabular-nums">
{currency.format(total)}
</td>
</tr>
</tfoot>
</table>
</CollapsibleContent>
</Collapsible>
</section>
);
}
npx shadcn@latest add @sevenui/component/collapsible-10pnpm dlx shadcn@latest add @sevenui/component/collapsible-10yarn dlx shadcn@latest add @sevenui/component/collapsible-10bunx --bun shadcn@latest add @sevenui/component/collapsible-10Incorrect seat count on March invoice
Ticket 4821 · Open- OBOlivia BennettMon 9:12 AM
Our March invoice charged us for 12 seats, but we removed four people on March 3. Can you correct the amount?
- OBOlivia BennettTue 8:47 AM
Appreciate it. Will the credit show up on this invoice or the next one?
- MSMarcus from SupportStaffToday 2:15 PM
Billing approved a prorated credit of $213.33. It is applied to your April invoice, and you will get an updated receipt by email.
"use client";
import * as React from "react";
import { SendIcon } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Textarea } from "@/components/ui/textarea";
type Message = {
id: string;
author: string;
initials: string;
staff?: boolean;
time: string;
body: string;
};
const thread: Message[] = [
{
id: "m1",
author: "Olivia Bennett",
initials: "OB",
time: "Mon 9:12 AM",
body: "Our March invoice charged us for 12 seats, but we removed four people on March 3. Can you correct the amount?",
},
{
id: "m2",
author: "Marcus from Support",
initials: "MS",
staff: true,
time: "Mon 10:40 AM",
body: "Thanks Olivia. I can see the seat change on March 3. Could you confirm the workspace ID so I can pull the billing log?",
},
{
id: "m3",
author: "Olivia Bennett",
initials: "OB",
time: "Mon 11:02 AM",
body: "Sure, it is ws_48213. The four removed users were all on the design team.",
},
{
id: "m4",
author: "Marcus from Support",
initials: "MS",
staff: true,
time: "Tue 8:30 AM",
body: "Found it. The seats were removed after the invoice was generated, so they were billed for the full month. I have escalated this to billing.",
},
{
id: "m5",
author: "Olivia Bennett",
initials: "OB",
time: "Tue 8:47 AM",
body: "Appreciate it. Will the credit show up on this invoice or the next one?",
},
{
id: "m6",
author: "Marcus from Support",
initials: "MS",
staff: true,
time: "Today 2:15 PM",
body: "Billing approved a prorated credit of $213.33. It is applied to your April invoice, and you will get an updated receipt by email.",
},
];
function MessageRow({ message }: { message: Message }) {
return (
<li className="flex gap-3 px-4 py-3">
<Avatar size="sm">
<AvatarFallback>{message.initials}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
<span className="text-sm font-medium">{message.author}</span>
{message.staff && <Badge variant="secondary">Staff</Badge>}
<span className="text-xs text-muted-foreground">{message.time}</span>
</div>
<p className="text-sm text-pretty text-foreground/90">{message.body}</p>
</div>
</li>
);
}
export default function Collapsible11() {
const [open, setOpen] = React.useState(false);
const [replies, setReplies] = React.useState<Message[]>([]);
const [draft, setDraft] = React.useState("");
const first = thread[0];
const hidden = thread.slice(1, -2);
const recent = [...thread.slice(-2), ...replies];
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const body = draft.trim();
if (!body) return;
setReplies((current) => [
...current,
{
id: `reply-${current.length + 1}`,
author: "Olivia Bennett",
initials: "OB",
time: "Just now",
body,
},
]);
setDraft("");
}
return (
<section
aria-labelledby="collapsible-11-title"
className="w-full max-w-md rounded-xl border bg-card text-card-foreground"
>
<header className="flex flex-wrap items-center justify-between gap-2 border-b px-4 py-3">
<h3 id="collapsible-11-title" className="text-sm font-semibold">
Incorrect seat count on March invoice
</h3>
<Badge variant="outline">Ticket 4821 · Open</Badge>
</header>
<Collapsible open={open} onOpenChange={setOpen}>
<ul className="flex flex-col">
<MessageRow message={first} />
</ul>
<div className="relative px-4 py-1">
<div
aria-hidden="true"
className="absolute inset-x-0 top-1/2 border-t border-dashed"
/>
<CollapsibleTrigger className="relative mx-auto flex items-center gap-1.5 rounded-full border bg-background px-3 py-1 text-xs font-medium text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50">
<span className="flex -space-x-1" aria-hidden="true">
{["OB", "MS"].map((initials) => (
<span
key={initials}
className="flex size-4 items-center justify-center rounded-full bg-muted text-[0.55rem] ring-2 ring-background"
>
{initials[0]}
</span>
))}
</span>
{open
? "Hide earlier messages"
: `Show ${hidden.length} earlier messages`}
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<ul className="flex flex-col">
{hidden.map((message) => (
<MessageRow key={message.id} message={message} />
))}
</ul>
</CollapsibleContent>
<ul className="flex flex-col">
{recent.map((message) => (
<MessageRow key={message.id} message={message} />
))}
</ul>
</Collapsible>
<form
className="flex flex-col gap-2 border-t p-3"
onSubmit={handleSubmit}
>
<label htmlFor="collapsible-11-reply" className="sr-only">
Reply to Marcus
</label>
<Textarea
id="collapsible-11-reply"
placeholder="Write a reply…"
className="min-h-16"
value={draft}
onChange={(event) => setDraft(event.target.value)}
/>
<div className="flex justify-end">
<Button type="submit" size="sm" disabled={!draft.trim()}>
<SendIcon aria-hidden="true" data-icon="inline-start" />
Send reply
</Button>
</div>
</form>
</section>
);
}
npx shadcn@latest add @sevenui/component/collapsible-11pnpm dlx shadcn@latest add @sevenui/component/collapsible-11yarn dlx shadcn@latest add @sevenui/component/collapsible-11bunx --bun shadcn@latest add @sevenui/component/collapsible-11Notifications7
Deployments
storefront deployed to productionmain · 4f2c9a1 · 48s buildPull requests
Ava requested your review on PR 1284Add saved carts to account pageTeam
Noah Kim joined the Payments teamInvited by Maya Thompson
"use client";
import * as React from "react";
import {
ChevronDownIcon,
GitPullRequestIcon,
RocketIcon,
UserPlusIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
type Notice = { id: string; title: string; meta: string; time: string };
type Group = {
id: string;
source: string;
icon: typeof RocketIcon;
notices: Notice[];
};
const initialGroups: Group[] = [
{
id: "deployments",
source: "Deployments",
icon: RocketIcon,
notices: [
{
id: "d1",
title: "storefront deployed to production",
meta: "main · 4f2c9a1 · 48s build",
time: "2m",
},
{
id: "d2",
title: "Preview ready for checkout-redesign",
meta: "feat/checkout · 91be0d3",
time: "18m",
},
{
id: "d3",
title: "admin-panel build failed",
meta: "Type error in orders/table.tsx",
time: "1h",
},
{
id: "d4",
title: "docs deployed to production",
meta: "main · 0c7d2e8 · 31s build",
time: "3h",
},
],
},
{
id: "reviews",
source: "Pull requests",
icon: GitPullRequestIcon,
notices: [
{
id: "r1",
title: "Ava requested your review on PR 1284",
meta: "Add saved carts to account page",
time: "25m",
},
{
id: "r2",
title: "Your PR 1279 was approved",
meta: "Fix currency rounding in refunds",
time: "2h",
},
],
},
{
id: "team",
source: "Team",
icon: UserPlusIcon,
notices: [
{
id: "t1",
title: "Noah Kim joined the Payments team",
meta: "Invited by Maya Thompson",
time: "5h",
},
],
},
];
function NotificationGroup({
group,
onClear,
}: {
group: Group;
onClear: () => void;
}) {
const [latest, ...older] = group.notices;
const Icon = group.icon;
return (
<li className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2 px-1">
<h4 className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<Icon aria-hidden="true" className="size-3.5" />
{group.source}
</h4>
<Button
variant="ghost"
size="xs"
onClick={onClear}
aria-label={`Clear ${group.source} notifications`}
>
Clear
</Button>
</div>
<Collapsible className="group/stack relative">
<div className="relative z-10 flex items-start gap-3 rounded-lg border bg-card p-3 shadow-xs">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-sm font-medium">{latest.title}</span>
<span className="truncate text-xs text-muted-foreground">
{latest.meta}
</span>
</div>
<time className="text-xs text-muted-foreground tabular-nums">
{latest.time}
</time>
</div>
{older.length > 0 && (
<>
<div
aria-hidden="true"
className="mx-2 -mt-1.5 h-3 rounded-b-lg border border-t-0 bg-card group-data-open/stack:hidden"
/>
<CollapsibleContent>
<ul className="flex flex-col gap-1.5 pt-1.5">
{older.map((notice) => (
<li
key={notice.id}
className="flex items-start gap-3 rounded-lg border bg-card p-3"
>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-sm">{notice.title}</span>
<span className="truncate text-xs text-muted-foreground">
{notice.meta}
</span>
</div>
<time className="text-xs text-muted-foreground tabular-nums">
{notice.time}
</time>
</li>
))}
</ul>
</CollapsibleContent>
<CollapsibleTrigger className="group mt-1.5 flex w-full items-center justify-center gap-1 rounded-md py-1 text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50">
<span className="group-data-panel-open:hidden">
{older.length} more from {group.source}
</span>
<span className="hidden group-data-panel-open:inline">
Show less
</span>
<ChevronDownIcon
aria-hidden="true"
className="size-3.5 transition-transform group-data-panel-open:rotate-180"
/>
</CollapsibleTrigger>
</>
)}
</Collapsible>
</li>
);
}
export default function Collapsible12() {
const [groups, setGroups] = React.useState(initialGroups);
const count = groups.reduce((sum, group) => sum + group.notices.length, 0);
return (
<section
aria-labelledby="collapsible-12-title"
className="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-muted/40 p-4"
>
<header className="flex items-center justify-between gap-2">
<h3 id="collapsible-12-title" className="font-semibold">
Notifications
<span className="ml-2 text-sm font-normal text-muted-foreground tabular-nums">
{count}
</span>
</h3>
<Button
variant="link"
size="sm"
className="px-0"
disabled={groups.length === 0}
onClick={() => setGroups([])}
>
Clear all
</Button>
</header>
{groups.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
You are all caught up.
</p>
) : (
<ul className="flex flex-col gap-4">
{groups.map((group) => (
<NotificationGroup
key={group.id}
group={group}
onClear={() =>
setGroups((current) =>
current.filter((item) => item.id !== group.id),
)
}
/>
))}
</ul>
)}
</section>
);
}
npx shadcn@latest add @sevenui/component/collapsible-12pnpm dlx shadcn@latest add @sevenui/component/collapsible-12yarn dlx shadcn@latest add @sevenui/component/collapsible-12bunx --bun shadcn@latest add @sevenui/component/collapsible-12test / unitFailed
PR 1284 · Add saved carts · triggered by ava-lin · 1m 52s
- vitest run --reporter=dot✓ 412 passedFAIL src/cart/totals.test.ts > applies percentage discountAssertionError: expected 91.8 to be 91.79at src/cart/totals.test.ts:48:32Test Files 1 failed | 63 passed (64)Error: Process completed with exit code 1.
"use client";
import * as React from "react";
import {
ChevronRightIcon,
CircleCheckIcon,
CircleDashedIcon,
CircleXIcon,
RotateCcwIcon,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Spinner } from "@/components/ui/spinner";
type StepStatus = "passed" | "failed" | "skipped";
type Step = {
id: string;
name: string;
status: StepStatus;
duration: string;
log: { text: string; tone?: "error" | "muted" }[];
};
const steps: Step[] = [
{
id: "checkout",
name: "Check out repository",
status: "passed",
duration: "2s",
log: [
{ text: "Syncing repository: acme/storefront" },
{ text: "Checking out ref refs/pull/1284/merge" },
{ text: "HEAD is now at 91be0d3", tone: "muted" },
],
},
{
id: "install",
name: "Install dependencies",
status: "passed",
duration: "38s",
log: [
{ text: "pnpm install --frozen-lockfile" },
{ text: "Packages: +1,284" },
{ text: "Done in 37.6s", tone: "muted" },
],
},
{
id: "test",
name: "Run unit tests",
status: "failed",
duration: "1m 12s",
log: [
{ text: "vitest run --reporter=dot" },
{ text: "✓ 412 passed", tone: "muted" },
{
text: "FAIL src/cart/totals.test.ts > applies percentage discount",
tone: "error",
},
{ text: "AssertionError: expected 91.8 to be 91.79", tone: "error" },
{ text: " at src/cart/totals.test.ts:48:32", tone: "muted" },
{ text: "Test Files 1 failed | 63 passed (64)" },
{ text: "Error: Process completed with exit code 1.", tone: "error" },
],
},
{
id: "build",
name: "Build production bundle",
status: "skipped",
duration: "—",
log: [{ text: "Skipped because a previous step failed.", tone: "muted" }],
},
];
const statusIcon: Record<StepStatus, React.ReactNode> = {
passed: (
<CircleCheckIcon aria-hidden="true" className="size-4 text-success" />
),
failed: (
<CircleXIcon aria-hidden="true" className="size-4 text-destructive" />
),
skipped: (
<CircleDashedIcon
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
),
};
const statusLabel: Record<StepStatus, string> = {
passed: "Passed",
failed: "Failed",
skipped: "Skipped",
};
export default function Collapsible13() {
const [rerunning, setRerunning] = React.useState(false);
// No real runner here: release the queued state after a moment so the
// demo can be re-run.
React.useEffect(() => {
if (!rerunning) return;
const timeout = window.setTimeout(() => setRerunning(false), 3000);
return () => window.clearTimeout(timeout);
}, [rerunning]);
return (
<section
aria-labelledby="collapsible-13-title"
className="w-full max-w-lg overflow-hidden rounded-xl border bg-card text-card-foreground"
>
<header className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
<div className="flex min-w-0 flex-col gap-0.5">
<h3
id="collapsible-13-title"
className="flex items-center gap-2 font-semibold"
>
test / unit
<Badge variant={rerunning ? "secondary" : "destructive"}>
{rerunning ? "Queued" : "Failed"}
</Badge>
</h3>
<p className="truncate text-xs text-muted-foreground">
PR 1284 · Add saved carts · triggered by ava-lin · 1m 52s
</p>
</div>
<Button
variant="outline"
size="sm"
disabled={rerunning}
onClick={() => setRerunning(true)}
>
{rerunning ? (
<Spinner data-icon="inline-start" />
) : (
<RotateCcwIcon aria-hidden="true" data-icon="inline-start" />
)}
{rerunning ? "Waiting for runner" : "Re-run failed"}
</Button>
</header>
<ol className="flex flex-col divide-y">
{steps.map((step, index) => (
<li key={step.id}>
<Collapsible defaultOpen={step.status === "failed"}>
<CollapsibleTrigger className="group flex w-full items-center gap-3 px-4 py-2.5 text-left text-sm outline-none hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset">
<ChevronRightIcon
aria-hidden="true"
className="size-3.5 text-muted-foreground transition-transform group-data-panel-open:rotate-90"
/>
{statusIcon[step.status]}
<span className="sr-only">{statusLabel[step.status]}:</span>
<span
className={
step.status === "skipped"
? "flex-1 truncate text-muted-foreground"
: "flex-1 truncate"
}
>
{step.name}
</span>
<span className="font-mono text-xs text-muted-foreground tabular-nums">
{step.duration}
</span>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="overflow-x-auto bg-muted/60 py-2 font-mono text-xs leading-5">
{step.log.map((line, lineIndex) => (
<div
// biome-ignore lint/suspicious/noArrayIndexKey: log lines are static and ordered
key={lineIndex}
className={
line.tone === "error"
? "flex bg-destructive/10 text-destructive"
: line.tone === "muted"
? "flex text-muted-foreground"
: "flex"
}
>
<span
aria-hidden="true"
className="w-10 shrink-0 pr-3 text-right text-muted-foreground/70 select-none"
>
{index + 1}.{lineIndex + 1}
</span>
<span className="pr-4 whitespace-pre">{line.text}</span>
</div>
))}
</div>
</CollapsibleContent>
</Collapsible>
</li>
))}
</ol>
</section>
);
}
npx shadcn@latest add @sevenui/component/collapsible-13pnpm dlx shadcn@latest add @sevenui/component/collapsible-13yarn dlx shadcn@latest add @sevenui/component/collapsible-13bunx --bun shadcn@latest add @sevenui/component/collapsible-13Get your store ready to sell
2 of 5Link a bank account so sales are paid out every Friday. Verification usually takes one business day.
"use client";
import * as React from "react";
import { CheckIcon, ChevronDownIcon, UndoIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Progress } from "@/components/ui/progress";
type Task = {
id: string;
title: string;
description: string;
action: string;
estimate: string;
};
const tasks: Task[] = [
{
id: "profile",
title: "Create your store profile",
description: "Add a store name, logo, and support email.",
action: "Edit profile",
estimate: "2 min",
},
{
id: "product",
title: "Add your first product",
description:
"Upload photos, set a price, and choose how much stock you have on hand.",
action: "Add product",
estimate: "5 min",
},
{
id: "payments",
title: "Connect a payout account",
description:
"Link a bank account so sales are paid out every Friday. Verification usually takes one business day.",
action: "Connect bank",
estimate: "4 min",
},
{
id: "shipping",
title: "Set shipping rates",
description:
"Offer flat-rate, free-over-threshold, or live carrier rates for domestic and international orders.",
action: "Configure shipping",
estimate: "3 min",
},
{
id: "domain",
title: "Connect a custom domain",
description:
"Point shop.yourbrand.com at your store. We handle the SSL certificate for you.",
action: "Add domain",
estimate: "6 min",
},
];
export default function Collapsible14() {
const [done, setDone] = React.useState<string[]>(["profile", "product"]);
const [openId, setOpenId] = React.useState<string | null>("payments");
const completed = tasks.filter((task) => done.includes(task.id));
const remaining = tasks.filter((task) => !done.includes(task.id));
const percent = Math.round((completed.length / tasks.length) * 100);
function complete(id: string) {
const next = remaining.find((task) => task.id !== id);
setDone((current) => [...current, id]);
setOpenId(next ? next.id : null);
}
function reopen(id: string) {
setDone((current) => current.filter((item) => item !== id));
setOpenId(id);
}
return (
<section
aria-labelledby="collapsible-14-title"
className="flex w-full max-w-md flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground"
>
<header className="flex flex-col gap-3">
<div className="flex items-baseline justify-between gap-3">
<h3 id="collapsible-14-title" className="font-semibold">
Get your store ready to sell
</h3>
<span className="shrink-0 text-sm whitespace-nowrap text-muted-foreground tabular-nums">
{completed.length} of {tasks.length}
</span>
</div>
<Progress value={percent} aria-label="Setup progress" />
</header>
{remaining.length === 0 ? (
<div className="flex flex-col items-center gap-2 rounded-lg bg-muted/50 px-4 py-6 text-center">
<span className="flex size-9 items-center justify-center rounded-full bg-primary text-primary-foreground">
<CheckIcon aria-hidden="true" className="size-4" />
</span>
<p className="font-medium">Your store is ready to launch</p>
<p className="text-sm text-muted-foreground">
Publish it whenever you are ready. You can change any of these later
in Settings.
</p>
<Button size="sm" className="mt-2">
Launch store
</Button>
</div>
) : (
<ol className="flex flex-col gap-2">
{remaining.map((task) => (
<li key={task.id}>
<Collapsible
open={openId === task.id}
onOpenChange={(open) => setOpenId(open ? task.id : null)}
className="rounded-lg border data-open:border-ring/40 data-open:bg-muted/30"
>
<CollapsibleTrigger className="group flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50">
<span
aria-hidden="true"
className="size-4 shrink-0 rounded-full border-2 border-muted-foreground/40 group-data-panel-open:border-primary"
/>
<span className="flex-1 text-sm font-medium">
{task.title}
</span>
<span className="text-xs text-muted-foreground">
{task.estimate}
</span>
<ChevronDownIcon
aria-hidden="true"
className="size-4 text-muted-foreground transition-transform group-data-panel-open:rotate-180"
/>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-3 px-3 pb-3 pl-10">
<p className="text-sm text-muted-foreground">
{task.description}
</p>
<div className="flex flex-wrap gap-2">
<Button size="sm">{task.action}</Button>
<Button
size="sm"
variant="ghost"
onClick={() => complete(task.id)}
>
Mark as done
</Button>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</li>
))}
</ol>
)}
{completed.length > 0 && (
<Collapsible className="border-t pt-3">
<CollapsibleTrigger className="group flex w-full items-center gap-2 rounded-md py-1 text-left text-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50">
<ChevronDownIcon
aria-hidden="true"
className="size-4 -rotate-90 transition-transform group-data-panel-open:rotate-0"
/>
{completed.length} completed
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="flex flex-col gap-1 pt-2">
{completed.map((task) => (
<li
key={task.id}
className="flex items-center gap-3 rounded-md px-1 py-1"
>
<span className="flex size-4 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
<CheckIcon aria-hidden="true" className="size-3" />
</span>
<span className="flex-1 text-sm text-muted-foreground line-through decoration-muted-foreground/50">
{task.title}
</span>
<Button
variant="ghost"
size="icon-xs"
aria-label={`Mark "${task.title}" as not done`}
onClick={() => reopen(task.id)}
>
<UndoIcon aria-hidden="true" />
</Button>
</li>
))}
</ul>
</CollapsibleContent>
</Collapsible>
)}
</section>
);
}
npx shadcn@latest add @sevenui/component/collapsible-14pnpm dlx shadcn@latest add @sevenui/component/collapsible-14yarn dlx shadcn@latest add @sevenui/component/collapsible-14bunx --bun shadcn@latest add @sevenui/component/collapsible-14