feat(ui): polish core component surfaces
This commit is contained in:
@@ -9,13 +9,24 @@ import {
|
||||
EmptyStateTitle
|
||||
} from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import type { ReactNode } from "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 className="grid gap-3">
|
||||
<div className="grid grid-cols-[1.1rem_3.6rem] items-center gap-2">
|
||||
<span className="size-4 rounded-full bg-[color-mix(in_oklch,var(--color-primary)_22%,var(--color-card))] shadow-[0_0_0_4px_color-mix(in_oklch,var(--color-primary)_10%,transparent)]" />
|
||||
<span className="h-4 rounded-[var(--radius-sm)] bg-[color-mix(in_oklch,var(--color-surface-strong)_86%,white_14%)]" />
|
||||
</div>
|
||||
<div className="grid gap-2 rounded-[1rem] border border-[color-mix(in_oklch,var(--color-border)_70%,transparent)] bg-[color-mix(in_oklch,var(--color-surface-container-low)_72%,white_28%)] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]">
|
||||
<div className="grid grid-cols-[1.2rem_3.4rem] gap-2">
|
||||
<span className="h-4 rounded-[var(--radius-sm)] bg-[color-mix(in_oklch,var(--color-tertiary-container)_60%,white_40%)]" />
|
||||
<span className="h-4 rounded-[var(--radius-sm)] bg-[color-mix(in_oklch,var(--color-surface-strong)_80%,white_20%)]" />
|
||||
</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>
|
||||
<div className="grid grid-cols-[4.8rem] gap-2">
|
||||
<span className="h-2 rounded-[var(--radius-full)] bg-[var(--color-border-strong)]" />
|
||||
@@ -25,11 +36,43 @@ function EmptyStateGlyph() {
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceSetupGlyph() {
|
||||
return (
|
||||
<div className="grid w-full gap-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-[1rem] border border-[color-mix(in_oklch,var(--color-border)_70%,transparent)] bg-[color-mix(in_oklch,var(--color-surface-container-low)_72%,white_28%)] px-3 py-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]">
|
||||
<div className="grid gap-1.5">
|
||||
<span className="h-2.5 w-20 rounded-full bg-[color-mix(in_oklch,var(--color-primary)_20%,var(--color-card))]" />
|
||||
<span className="h-2.5 w-12 rounded-full bg-[var(--color-border)]" />
|
||||
</div>
|
||||
<span className="grid size-10 place-items-center rounded-[1rem] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-primary-container)_76%,white_24%),color-mix(in_oklch,var(--color-tertiary-container)_72%,white_28%))] shadow-[0_10px_22px_color-mix(in_oklch,var(--color-primary)_14%,transparent)]">
|
||||
<span className="size-4 rounded-full border-2 border-[color-mix(in_oklch,var(--color-primary)_58%,white_42%)]" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-2 rounded-[1.25rem] border border-[color-mix(in_oklch,var(--color-border)_68%,transparent)] bg-[color-mix(in_oklch,var(--color-surface-bright)_76%,white_24%)] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.52)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2.5 rounded-full bg-[color-mix(in_oklch,var(--color-success)_72%,white_28%)]" />
|
||||
<span className="h-2.5 w-18 rounded-full bg-[var(--color-border-strong)]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2.5 rounded-full bg-[color-mix(in_oklch,var(--color-primary)_56%,white_44%)]" />
|
||||
<span className="h-2.5 w-24 rounded-full bg-[var(--color-border)]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2.5 rounded-full bg-[color-mix(in_oklch,var(--color-tertiary)_58%,white_42%)]" />
|
||||
<span className="h-2.5 w-16 rounded-full bg-[var(--color-border)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReleaseEmptyState({
|
||||
actionsLayout = "inline",
|
||||
align = "center",
|
||||
className,
|
||||
description = "Adjust the current filters or create a new release to start routing work.",
|
||||
eyebrow = "No results",
|
||||
glyph,
|
||||
layout = "default",
|
||||
mediaSize = "default",
|
||||
tone = "default",
|
||||
@@ -37,8 +80,10 @@ function ReleaseEmptyState({
|
||||
}: {
|
||||
actionsLayout?: "inline" | "stack";
|
||||
align?: "center" | "start";
|
||||
className?: string;
|
||||
description?: string;
|
||||
eyebrow?: string;
|
||||
glyph?: ReactNode;
|
||||
layout?: "compact" | "default" | "split";
|
||||
mediaSize?: "compact" | "default" | "hero";
|
||||
title?: string;
|
||||
@@ -47,12 +92,14 @@ function ReleaseEmptyState({
|
||||
return (
|
||||
<EmptyState
|
||||
align={align}
|
||||
className="w-[min(100%,42rem)]"
|
||||
className={[layout === "split" ? "w-full max-w-none" : "w-[min(100%,42rem)]", className]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
layout={layout}
|
||||
tone={tone}
|
||||
>
|
||||
<EmptyStateMedia size={mediaSize}>
|
||||
<EmptyStateGlyph />
|
||||
{glyph ?? <EmptyStateGlyph />}
|
||||
</EmptyStateMedia>
|
||||
<EmptyStateHeader align={align}>
|
||||
<EmptyStateEyebrow>{eyebrow}</EmptyStateEyebrow>
|
||||
@@ -140,8 +187,10 @@ export const SplitWorkspace: Story = {
|
||||
<div className="w-[940px] rounded-[var(--radius-xl)] border border-[var(--color-border)] bg-[var(--color-background)] p-6">
|
||||
<ReleaseEmptyState
|
||||
align="start"
|
||||
className="shadow-[0_26px_60px_color-mix(in_oklch,var(--color-primary)_10%,transparent)]"
|
||||
description="Invite operators, connect the first runtime, and define the initial routing lane to turn this empty workspace into an active control surface."
|
||||
eyebrow="Workspace setup"
|
||||
glyph={<WorkspaceSetupGlyph />}
|
||||
layout="split"
|
||||
mediaSize="hero"
|
||||
title="This command center needs its first operator"
|
||||
@@ -151,6 +200,42 @@ export const SplitWorkspace: Story = {
|
||||
)
|
||||
};
|
||||
|
||||
export const Motion: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"EmptyState keeps motion local to the decorative media and the call-to-action group. The slab itself stays anchored while the media drifts gently and the actions enter in a short sequence. In static motion mode those effects collapse back to near-instant feedback."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => (
|
||||
<div className="grid w-[960px] gap-4 lg:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)]">
|
||||
<ReleaseEmptyState
|
||||
actionsLayout="stack"
|
||||
align="start"
|
||||
description="Keep the empty queue readable without turning it into a hero banner. The media should feel alive, not restless."
|
||||
eyebrow="Queue calm"
|
||||
layout="compact"
|
||||
mediaSize="default"
|
||||
title="No routed work is waiting right now"
|
||||
tone="subtle"
|
||||
/>
|
||||
<ReleaseEmptyState
|
||||
align="start"
|
||||
className="shadow-[0_26px_60px_color-mix(in_oklch,var(--color-primary)_10%,transparent)]"
|
||||
description="The split workspace uses the larger hero media, but the slab stays planted. Motion belongs in the supporting ornament and the CTA entrance, not in the whole panel."
|
||||
eyebrow="Workspace setup"
|
||||
glyph={<WorkspaceSetupGlyph />}
|
||||
layout="split"
|
||||
mediaSize="hero"
|
||||
title="Invite the first operator to activate this command center"
|
||||
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)]">
|
||||
|
||||
Reference in New Issue
Block a user