Refactor renamed the component groups to something less obtuse:

Elements -> Groups -> Screens -> Views
This commit is contained in:
cliffhall 2026-03-28 12:58:47 -04:00
commit dd46acba87
97 changed files with 151 additions and 144 deletions

View file

@ -100,9 +100,9 @@ inspector/
// Component.tsx
const MessageContainer = Paper.withProps({ variant: 'message' });
```
- Theme files vs. Storybook atom components
- **Theme files** (`src/theme/<Component>.ts`) and **atom components** (`src/components/atoms/`) serve different purposes and both are needed.
- Theme files vs. Storybook element components
- **Theme files** (`src/theme/<Component>.ts`) and **element components** (`src/components/elements/`) serve different purposes and both are needed.
- Theme files customize every instance of a Mantine component app-wide — defaults (size, radius), custom variants, and global style overrides. They are applied automatically by `MantineProvider`.
- Atom components add domain-specific semantics on top of Mantine primitives. For example, `AnnotationBadge` maps domain concepts (audience, destructive, longRun) to Mantine's styling primitives (color, variant). Storybook documents these domain components for designers and developers.
- Atom components MUST import from `@mantine/core`, NOT from `src/theme/`. The theme layer is applied transparently by the provider — atoms do not need to know about `Theme<Name>` constants.
- NEVER push domain-specific variant logic (e.g., annotation types, transport types) into theme files. Domain variants belong in the atom component that owns those semantics. Theme files are for styling that applies to the Mantine primitive globally.
- Element components add domain-specific semantics on top of Mantine primitives. For example, `AnnotationBadge` maps domain concepts (audience, destructive, longRun) to Mantine's styling primitives (color, variant). Storybook documents these domain components for designers and developers.
- Element components MUST import from `@mantine/core`, NOT from `src/theme/`. The theme layer is applied transparently by the provider — elements do not need to know about `Theme<Name>` constants.
- NEVER push domain-specific variant logic (e.g., annotation types, transport types) into theme files. Domain variants belong in the element component that owns those semantics. Theme files are for styling that applies to the Mantine primitive globally.

View file

@ -0,0 +1,52 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { AnnotationBadge } from "./AnnotationBadge";
const meta: Meta<typeof AnnotationBadge> = {
title: "Elements/AnnotationBadge",
component: AnnotationBadge,
};
export default meta;
type Story = StoryObj<typeof AnnotationBadge>;
export const Audience: Story = {
args: {
label: "user",
variant: "audience",
},
};
export const ReadOnly: Story = {
args: {
label: "read-only",
variant: "readOnly",
},
};
export const Destructive: Story = {
args: {
label: "destructive",
variant: "destructive",
},
};
export const LongRun: Story = {
args: {
label: "long-run",
variant: "longRun",
},
};
export const PriorityHigh: Story = {
args: {
label: "priority: high",
variant: "priority",
},
};
export const Default: Story = {
args: {
label: "custom",
variant: "default",
},
};

View file

@ -0,0 +1,45 @@
import { Badge, useComputedColorScheme } from "@mantine/core";
export type AnnotationVariant =
| "audience"
| "readOnly"
| "destructive"
| "longRun"
| "priority"
| "default";
export interface AnnotationBadgeProps {
label: string;
variant?: AnnotationVariant;
}
const variantMap: Record<
AnnotationVariant,
{ color: string; variant: string }
> = {
audience: { color: "blue", variant: "outline" },
readOnly: { color: "green", variant: "filled" },
destructive: { color: "red", variant: "filled" },
longRun: { color: "yellow", variant: "filled" },
priority: { color: "orange", variant: "filled" },
default: { color: "gray", variant: "light" },
};
export function AnnotationBadge({
label,
variant = "default",
}: AnnotationBadgeProps) {
const colorScheme = useComputedColorScheme();
const { color, variant: badgeVariant } = variantMap[variant];
const textColor =
(variant === "priority" || variant === "destructive") &&
colorScheme === "dark"
? "black"
: undefined;
return (
<Badge color={color} variant={badgeVariant} c={textColor}>
{label}
</Badge>
);
}

View file

@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { CapabilityItem } from "./CapabilityItem";
const meta: Meta<typeof CapabilityItem> = {
title: "Elements/CapabilityItem",
component: CapabilityItem,
};
export default meta;
type Story = StoryObj<typeof CapabilityItem>;
export const Supported: Story = {
args: {
name: "Tools",
supported: true,
},
};
export const SupportedWithCount: Story = {
args: {
name: "Tools",
supported: true,
count: 4,
},
};
export const NotSupported: Story = {
args: {
name: "Completions",
supported: false,
},
};

View file

@ -0,0 +1,24 @@
import { Group, Text } from "@mantine/core";
export interface CapabilityItemProps {
name: string;
supported: boolean;
count?: number;
}
export function CapabilityItem({
name,
supported,
count,
}: CapabilityItemProps) {
const label = count != null ? `${name} (${count})` : name;
return (
<Group gap="xs">
<Text c={supported ? "green" : "red"}>
{supported ? "\u2713" : "\u2717"}
</Text>
<Text>{label}</Text>
</Group>
);
}

View file

@ -0,0 +1,46 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ConnectionToggle } from "./ConnectionToggle";
const meta: Meta<typeof ConnectionToggle> = {
title: "Elements/ConnectionToggle",
component: ConnectionToggle,
args: {
onChange: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ConnectionToggle>;
export const Connected: Story = {
args: {
checked: true,
loading: false,
disabled: false,
},
};
export const Disconnected: Story = {
args: {
checked: false,
loading: false,
disabled: false,
},
};
export const Loading: Story = {
args: {
checked: false,
loading: true,
disabled: false,
},
};
export const Disabled: Story = {
args: {
checked: false,
loading: false,
disabled: true,
},
};

View file

@ -0,0 +1,24 @@
import { Switch } from "@mantine/core";
export interface ConnectionToggleProps {
checked: boolean;
loading: boolean;
disabled: boolean;
onChange: (checked: boolean) => void;
}
export function ConnectionToggle({
checked,
loading,
disabled,
onChange,
}: ConnectionToggleProps) {
return (
<Switch
size="lg"
checked={checked}
disabled={disabled || loading}
onChange={(event) => onChange(event.currentTarget.checked)}
/>
);
}

View file

@ -0,0 +1,87 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ContentViewer } from "./ContentViewer";
const meta: Meta<typeof ContentViewer> = {
title: "Elements/ContentViewer",
component: ContentViewer,
};
export default meta;
type Story = StoryObj<typeof ContentViewer>;
export const PlainText: Story = {
args: {
type: "text",
content: "Hello, world!\nThis is plain text.",
},
};
export const JsonContent: Story = {
args: {
type: "json",
content:
'{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}',
},
};
export const ImagePreview: Story = {
args: {
type: "image",
content:
"iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAABWUlEQVR4nO3YsQnAMAwAQe+/dLKCRZpD+UA6cVh8p/Oc81z9t1/eJ+/oD/ybVxDMKwjmFQTzCoJ5BcG8gmBeQTCvIJg3mLQX2eIVBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8zqdYF5BMK8gmFcQzCsI5hUE8wqCeQXBvIJgXkEwbzBpL7LFKwjmFQTzCoJ5BcG8gmBeQTCvIJhXEMwrCOZ1OsG8gmBeQTCvIJhXEMwrCOYVBPMKgnkFwbyCYN5g0l5ki1cQzCsI5hUE8wqCeQXBvIJgXkEwryCYVxDM63SCeQXBvIJgXkEwryCYVxDMKwjmFQTzCoJ5BcG8waS9yBavIJhXEMwrCOYVBPMKgnkFwbyCYF5BMK8gmNfpBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8wqCeYNJe5EtXkEwryCYVxDMKwjmFQTzCoJ5BcG8gmBeQTDvBaFNwZ0T5gfxAAAAAElFTkSuQmCC",
mimeType: "image/png",
},
};
export const LongContent: Story = {
args: {
type: "json",
content: JSON.stringify({
name: "my-app",
version: "2.5.0",
description: "A comprehensive application configuration",
author: "Inspector Team",
license: "MIT",
repository: "https://github.com/example/my-app",
dependencies: {
react: "^18.0.0",
typescript: "^5.0.0",
mantine: "^7.0.0",
},
devDependencies: {
vitest: "^1.0.0",
storybook: "^8.0.0",
eslint: "^9.0.0",
},
scripts: {
dev: "vite",
build: "tsc && vite build",
test: "vitest",
lint: "eslint .",
},
settings: {
theme: "dark",
language: "en",
notifications: true,
maxRetries: 3,
},
}),
},
};
export const WithCopyButton: Story = {
args: {
type: "text",
content: "Hello, world!\nThis text can be copied.",
copyable: true,
},
};
export const JsonWithCopy: Story = {
args: {
type: "json",
content:
'{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}',
copyable: true,
},
};

View file

@ -0,0 +1,76 @@
import { Code, Flex, Image, Stack } from "@mantine/core";
import { CopyButton } from "../CopyButton/CopyButton";
export interface ContentViewerProps {
type: "text" | "json" | "image" | "audio";
content: string;
mimeType?: string;
copyable?: boolean;
}
function formatJson(content: string): string {
try {
return JSON.stringify(JSON.parse(content), null, 2);
} catch {
return content;
}
}
function buildDataUri(mimeType: string, content: string): string {
return `data:${mimeType};base64,${content}`;
}
function formatContent(type: "text" | "json", content: string): string {
return type === "json" ? formatJson(content) : content;
}
const ContentWrapper = Flex.withProps({
pos: "relative",
direction: "column",
});
const CopyOverlay = Flex.withProps({
pos: "absolute",
top: 4,
right: 4,
});
const PreviewImage = Image.withProps({
alt: "Content preview",
maw: 400,
radius: "md",
});
export function ContentViewer({
type,
content,
mimeType,
copyable = false,
}: ContentViewerProps) {
const showCopy = copyable && (type === "text" || type === "json");
return (
<Stack gap="xs">
{(type === "json" || type === "text") && (
<ContentWrapper>
<Code block p={36}>
{formatContent(type, content)}
</Code>
{showCopy && (
<CopyOverlay>
<CopyButton value={content} />
</CopyOverlay>
)}
</ContentWrapper>
)}
{type === "image" && (
<PreviewImage src={buildDataUri(mimeType || "image/png", content)} />
)}
{type === "audio" && (
<audio controls>
<source src={buildDataUri(mimeType || "audio/wav", content)} />
</audio>
)}
</Stack>
);
}

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { CopyButton } from "./CopyButton";
const meta: Meta<typeof CopyButton> = {
title: "Elements/CopyButton",
component: CopyButton,
};
export default meta;
type Story = StoryObj<typeof CopyButton>;
export const Default: Story = {
args: {
value: "Hello, world!",
},
};
export const LongValue: Story = {
args: {
value:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
};

View file

@ -0,0 +1,27 @@
import {
ActionIcon,
CopyButton as MantineCopyButton,
Tooltip,
} from "@mantine/core";
export interface CopyButtonProps {
value: string;
}
export function CopyButton({ value }: CopyButtonProps) {
return (
<MantineCopyButton value={value}>
{({ copied, copy }) => (
<Tooltip label={copied ? "Copied" : "Copy"}>
<ActionIcon
variant="subtle"
color={copied ? "green" : "gray"}
onClick={copy}
>
{copied ? "\u2713" : "\u2398"}
</ActionIcon>
</Tooltip>
)}
</MantineCopyButton>
);
}

View file

@ -0,0 +1,50 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { InlineError } from "./InlineError";
const meta: Meta<typeof InlineError> = {
title: "Elements/InlineError",
component: InlineError,
};
export default meta;
type Story = StoryObj<typeof InlineError>;
export const ShortError: Story = {
args: {
message: "Connection timeout after 20s",
},
};
export const WithRetryCount: Story = {
args: {
message: "Connection failed",
retryCount: 3,
maxRetries: 5,
},
};
export const WithDetails: Story = {
args: {
message: "Connection refused",
details:
"Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)",
},
};
export const WithDocLink: Story = {
args: {
message: "Connection failed",
docLink: "https://example.com/troubleshooting",
},
};
export const FullError: Story = {
args: {
message: "Connection refused",
details:
"Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)",
retryCount: 3,
maxRetries: 5,
docLink: "https://example.com/troubleshooting",
},
};

View file

@ -0,0 +1,91 @@
import { Alert, Button, Collapse, Group, Stack, Text } from "@mantine/core";
import { useState } from "react";
export interface InlineErrorProps {
message: string;
details?: string;
retryCount?: number;
maxRetries?: number;
docLink?: string;
}
const HeaderRow = Group.withProps({
justify: "space-between",
wrap: "nowrap",
});
const MessageGroup = Group.withProps({
gap: "sm",
wrap: "nowrap",
});
const ErrorMessage = Text.withProps({
size: "sm",
fw: 500,
c: "red",
});
const RetryText = Text.withProps({
size: "sm",
c: "dimmed",
});
const ExpandButton = Button.withProps({
variant: "transparent",
size: "compact-xs",
c: "var(--inspector-text-primary)",
});
const DocLinkButton = Button.withProps({
variant: "light",
size: "xs",
component: "a",
target: "_blank",
});
function formatRetryLabel(retryCount: number, maxRetries?: number): string {
return maxRetries !== undefined
? `Retry attempt ${retryCount} of ${maxRetries}`
: `Retry attempt ${retryCount}`;
}
export function InlineError({
message,
details,
retryCount,
maxRetries,
docLink,
}: InlineErrorProps) {
const [expanded, setExpanded] = useState(false);
const hasExpandable = details !== undefined || docLink !== undefined;
return (
<Alert color="red" variant="light">
<Stack gap="xs">
<HeaderRow>
<MessageGroup>
<ErrorMessage>{message}</ErrorMessage>
{retryCount !== undefined && (
<RetryText>{formatRetryLabel(retryCount, maxRetries)}</RetryText>
)}
</MessageGroup>
{hasExpandable && (
<ExpandButton onClick={() => setExpanded((v) => !v)}>
{expanded ? "Show less" : "Show more"}
</ExpandButton>
)}
</HeaderRow>
<Collapse in={expanded}>
<Stack gap="xs">
{details && <Text size="sm">{details}</Text>}
{docLink && (
<DocLinkButton href={docLink}>
View Troubleshooting Guide
</DocLinkButton>
)}
</Stack>
</Collapse>
</Stack>
</Alert>
);
}

View file

@ -0,0 +1,25 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ListChangedIndicator } from "./ListChangedIndicator";
const meta: Meta<typeof ListChangedIndicator> = {
title: "Elements/ListChangedIndicator",
component: ListChangedIndicator,
};
export default meta;
type Story = StoryObj<typeof ListChangedIndicator>;
export const Visible: Story = {
args: {
visible: true,
onRefresh: fn(),
},
};
export const Hidden: Story = {
args: {
visible: false,
onRefresh: fn(),
},
};

View file

@ -0,0 +1,38 @@
import { Button, Group, Paper, Text } from "@mantine/core";
export interface ListChangedIndicatorProps {
visible: boolean;
onRefresh: () => void;
}
const Dot = Paper.withProps({
w: 8,
h: 8,
radius: "xl",
bg: "var(--inspector-status-connecting)",
});
const UpdateLabel = Text.withProps({
size: "sm",
c: "dimmed",
});
const RefreshButton = Button.withProps({
size: "xs",
variant: "light",
});
export function ListChangedIndicator({
visible,
onRefresh,
}: ListChangedIndicatorProps) {
if (!visible) return null;
return (
<Group gap="xs">
<Dot />
<UpdateLabel>List updated</UpdateLabel>
<RefreshButton onClick={onRefresh}>↻ Refresh</RefreshButton>
</Group>
);
}

View file

@ -0,0 +1,83 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { LogEntry } from "./LogEntry";
const meta: Meta<typeof LogEntry> = {
title: "Elements/LogEntry",
component: LogEntry,
};
export default meta;
type Story = StoryObj<typeof LogEntry>;
export const Debug: Story = {
args: {
timestamp: "2026-03-17T10:00:00Z",
level: "debug",
message: "Initializing connection pool",
},
};
export const Info: Story = {
args: {
timestamp: "2026-03-17T10:00:01Z",
level: "info",
message: "Server started on port 3000",
},
};
export const Notice: Story = {
args: {
timestamp: "2026-03-17T10:00:02Z",
level: "notice",
message: "Configuration reloaded successfully",
},
};
export const Warning: Story = {
args: {
timestamp: "2026-03-17T10:00:03Z",
level: "warning",
message: "Memory usage above 80%",
},
};
export const Error: Story = {
args: {
timestamp: "2026-03-17T10:00:04Z",
level: "error",
message: "Failed to connect to database",
},
};
export const Critical: Story = {
args: {
timestamp: "2026-03-17T10:00:05Z",
level: "critical",
message: "Disk space critically low",
},
};
export const Alert: Story = {
args: {
timestamp: "2026-03-17T10:00:06Z",
level: "alert",
message: "Service degradation detected",
},
};
export const Emergency: Story = {
args: {
timestamp: "2026-03-17T10:00:07Z",
level: "emergency",
message: "System is unusable",
},
};
export const WithLogger: Story = {
args: {
timestamp: "2026-03-17T10:00:08Z",
level: "info",
message: "Tool execution completed",
logger: "mcp-server",
},
};

View file

@ -0,0 +1,93 @@
import { Badge, Group, Text } from "@mantine/core";
export type LogLevel =
| "debug"
| "info"
| "notice"
| "warning"
| "error"
| "critical"
| "alert"
| "emergency";
export interface LogEntryProps {
timestamp: string;
level: LogLevel;
message: string;
logger?: string;
}
const levelBadgeColor: Record<LogLevel, string> = {
debug: "gray",
info: "blue",
notice: "teal",
warning: "yellow",
error: "red",
critical: "red",
alert: "red",
emergency: "red",
};
const levelBadgeVariant: Record<LogLevel, string> = {
debug: "light",
info: "light",
notice: "light",
warning: "light",
error: "light",
critical: "filled",
alert: "filled",
emergency: "filled",
};
const levelMessageColor: Record<LogLevel, string | undefined> = {
debug: "dimmed",
info: "blue",
notice: undefined,
warning: "yellow",
error: "red",
critical: "red",
alert: "red",
emergency: "red",
};
function isBoldLevel(level: LogLevel): boolean {
return level === "alert" || level === "emergency";
}
function getBoldWeight(level: LogLevel): number | undefined {
return isBoldLevel(level) ? 700 : undefined;
}
function formatLogger(logger: string): string {
return `[${logger}]`;
}
const TimestampText = Text.withProps({
size: "sm",
ff: "monospace",
c: "dimmed",
});
const LoggerText = Text.withProps({
size: "xs",
c: "dimmed",
});
export function LogEntry({ timestamp, level, message, logger }: LogEntryProps) {
return (
<Group gap="sm" wrap="nowrap">
<TimestampText>{timestamp}</TimestampText>
<Badge
color={levelBadgeColor[level]}
variant={levelBadgeVariant[level]}
fw={getBoldWeight(level)}
>
{level}
</Badge>
{logger && <LoggerText>{formatLogger(logger)}</LoggerText>}
<Text size="sm" c={levelMessageColor[level]}>
{message}
</Text>
</Group>
);
}

View file

@ -0,0 +1,48 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MessageBubble } from "./MessageBubble";
const meta: Meta<typeof MessageBubble> = {
title: "Elements/MessageBubble",
component: MessageBubble,
};
export default meta;
type Story = StoryObj<typeof MessageBubble>;
export const UserText: Story = {
args: {
index: 0,
role: "user",
content: "Hello, my name is John and I like cats",
},
};
export const AssistantText: Story = {
args: {
index: 1,
role: "assistant",
content:
"Nice to meet you, John! It's wonderful that you enjoy cats. They're such fascinating and independent creatures. Do you have any cats of your own?",
},
};
export const UserWithImage: Story = {
args: {
index: 0,
role: "user",
content: "Check this out",
imageContent: {
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
mimeType: "image/png",
},
},
};
export const LongMessage: Story = {
args: {
index: 0,
role: "user",
content:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.",
},
};

View file

@ -0,0 +1,72 @@
import { Group, Image, Paper, Stack, Text } from "@mantine/core";
import { CopyButton } from "../CopyButton/CopyButton";
export interface MessageBubbleProps {
index: number;
role: "user" | "assistant";
content: string;
imageContent?: { data: string; mimeType: string };
audioContent?: { data: string; mimeType: string };
}
function buildDataUri(mimeType: string, data: string): string {
return `data:${mimeType};base64,${data}`;
}
function formatRoleLabel(index: number, role: string): string {
return `[${index}] role: ${role}`;
}
function formatQuotedContent(content: string): string {
return `"${content}"`;
}
const BubbleContainer = Paper.withProps({
p: "md",
radius: "md",
withBorder: true,
});
const RoleLabel = Text.withProps({
size: "xs",
c: "dimmed",
});
const PreviewImage = Image.withProps({
maw: 300,
radius: "sm",
mt: "xs",
});
export function MessageBubble({
index,
role,
content,
imageContent,
audioContent,
}: MessageBubbleProps) {
return (
<BubbleContainer>
<Stack gap="xs">
<Group justify="space-between">
<RoleLabel>{formatRoleLabel(index, role)}</RoleLabel>
<CopyButton value={content} />
</Group>
<Text size="sm">{formatQuotedContent(content)}</Text>
{imageContent && (
<PreviewImage
src={buildDataUri(imageContent.mimeType, imageContent.data)}
/>
)}
{audioContent && (
<audio controls>
<source
src={buildDataUri(audioContent.mimeType, audioContent.data)}
type={audioContent.mimeType}
/>
</audio>
)}
</Stack>
</BubbleContainer>
);
}

View file

@ -0,0 +1,39 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ProgressDisplay } from "./ProgressDisplay";
const meta: Meta<typeof ProgressDisplay> = {
title: "Elements/ProgressDisplay",
component: ProgressDisplay,
};
export default meta;
type Story = StoryObj<typeof ProgressDisplay>;
export const ZeroPercent: Story = {
args: {
progress: 0,
},
};
export const HalfComplete: Story = {
args: {
progress: 50,
description: "Processing...",
},
};
export const NearComplete: Story = {
args: {
progress: 95,
description: "Almost done",
elapsed: "1m 30s",
},
};
export const Complete: Story = {
args: {
progress: 100,
description: "Done",
elapsed: "2m 15s",
},
};

Some files were not shown because too many files have changed in this diff Show more