Files
cadence-ui/apps/docs/src/components/empty-state.stories.tsx
T

157 lines
5.9 KiB
TypeScript

import {
Button,
EmptyState,
EmptyStateActions,
EmptyStateDescription,
EmptyStateEyebrow,
EmptyStateHeader,
EmptyStateMedia,
EmptyStateTitle
} from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
function EmptyStateGlyph() {
return (
<div className="grid gap-2">
<div className="grid grid-cols-[1.3rem_3.4rem] gap-2">
<span className="h-5 rounded-[var(--radius-sm)] bg-[color-mix(in_oklch,var(--color-primary)_18%,var(--color-card))]" />
<span className="h-5 rounded-[var(--radius-sm)] bg-[var(--color-surface-strong)]" />
</div>
<div className="grid grid-cols-[4.8rem] gap-2">
<span className="h-2 rounded-[var(--radius-full)] bg-[var(--color-border-strong)]" />
<span className="h-2 rounded-[var(--radius-full)] bg-[var(--color-border)]" />
</div>
</div>
);
}
function ReleaseEmptyState({
description = "Adjust the current filters or create a new release to start routing work.",
eyebrow = "No results",
tone = "default",
title = "No matching releases"
}: {
description?: string;
eyebrow?: string;
title?: string;
tone?: "default" | "subtle" | "accent";
}) {
return (
<EmptyState className="w-[min(100%,42rem)]" tone={tone}>
<EmptyStateMedia>
<EmptyStateGlyph />
</EmptyStateMedia>
<EmptyStateHeader>
<EmptyStateEyebrow>{eyebrow}</EmptyStateEyebrow>
<EmptyStateTitle>{title}</EmptyStateTitle>
<EmptyStateDescription>{description}</EmptyStateDescription>
</EmptyStateHeader>
<EmptyStateActions>
<Button>Create release</Button>
<Button variant="ghost">Reset filters</Button>
</EmptyStateActions>
</EmptyState>
);
}
const meta = {
title: "Components/EmptyState",
component: ReleaseEmptyState,
parameters: {
docs: {
description: {
component:
"EmptyState is the system surface for no-results, first-run, and no-content moments that should still feel intentional. It gives teams one stable composition for media, framing copy, and next-step actions instead of improvising ad hoc placeholder cards."
}
},
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof ReleaseEmptyState>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const Scenarios: Story = {
render: () => (
<div className="grid w-[860px] gap-4 lg:grid-cols-2">
<ReleaseEmptyState />
<ReleaseEmptyState
description="Invite teammates and define the first approval lane to turn this empty project into a reusable workflow."
eyebrow="First run"
title="This workspace is ready for its first rollout"
tone="accent"
/>
</div>
)
};
export const Anatomy: Story = {
render: () => (
<div className="w-[760px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
<div className="space-y-4">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Empty state anatomy
</p>
<ReleaseEmptyState />
<div className="grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>
<code className="text-[var(--color-foreground)]">data-slot=\"media\"</code> holds the
decorative or explanatory visual.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-slot=\"header\"</code>,{" "}
<code className="text-[var(--color-foreground)]">data-slot=\"eyebrow\"</code>,{" "}
<code className="text-[var(--color-foreground)]">data-slot=\"label\"</code>, and{" "}
<code className="text-[var(--color-foreground)]">data-slot=\"description\"</code>{" "}
structure the framing copy.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-slot=\"actions\"</code> groups
the primary next step and any secondary recovery action.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-tone</code> exposes whether the
surface stays neutral, subtle, or accent-led.
</p>
</div>
</div>
</div>
)
};
export const Accessibility: Story = {
parameters: {
docs: {
description: {
story:
"Use empty states to explain what happened and what the user can do next. Keep the title concrete, the description actionable, and ensure the primary action is a real next step rather than decorative reassurance."
}
}
},
render: () => (
<div className="grid w-[840px] gap-4 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)]">
Accessibility notes
</h3>
<div className="mt-4 grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>Describe the absence clearly. &quot;No matching releases&quot; is better than &quot;Nothing here&quot;.</p>
<p>Use actions that recover the user, such as clearing filters or creating the first item.</p>
<p>Do not hide critical instructions inside decorative media. The copy should stand on its own.</p>
</div>
</article>
<div className="flex items-center justify-center rounded-[var(--radius-lg)] border border-dashed border-[var(--color-border-strong)] bg-[var(--color-background)] p-6">
<ReleaseEmptyState
description="Clear the current filters or create a release with a broader audience to repopulate this view."
title="No results for this routing lane"
tone="subtle"
/>
</div>
</div>
)
};