diff --git a/webui2/src/components/code/__snapshots__/FileViewer.test.tsx.snap b/webui2/src/components/code/__snapshots__/FileViewer.test.tsx.snap index 0e53868cd9037a08ed4ba537ab24e690495d81b2..72bab0c906d484c02570e30a10a24100e47a0f4a 100644 --- a/webui2/src/components/code/__snapshots__/FileViewer.test.tsx.snap +++ b/webui2/src/components/code/__snapshots__/FileViewer.test.tsx.snap @@ -261,83 +261,83 @@ exports[`FileViewer/TypeScriptFile matches snapshot 1`] = ` >
diff --git a/webui2/src/components/ui/__snapshots__/comment-card.test.tsx.snap b/webui2/src/components/ui/__snapshots__/comment-card.test.tsx.snap new file mode 100644 index 0000000000000000000000000000000000000000..6447a4a870737a21f0a03463d7cf4bff6e7bf4b0 --- /dev/null +++ b/webui2/src/components/ui/__snapshots__/comment-card.test.tsx.snap @@ -0,0 +1,146 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`CommentCard/Default matches snapshot 1`] = ` +
+
+ + + JA + + +
+
+ + Jane Doe + + + 2 hours ago + +
+
+

+ This is a comment body with some text content. +

+
+
+
+
+`; + +exports[`CommentCard/EmptyBody matches snapshot 1`] = ` +
+
+ + + AL + + +
+
+ + Alice Wu + + + just now + +
+
+

+ No description provided. +

+
+
+
+
+`; + +exports[`CommentCard/WithEditButton matches snapshot 1`] = ` +
+
+ + + BO + + +
+
+ + Bob Smith + + + 1 day ago + + + edited + + +
+
+

+ Updated the configuration to fix the build issue. +

+
+
+
+
+`; diff --git a/webui2/src/components/ui/__snapshots__/empty-state.test.tsx.snap b/webui2/src/components/ui/__snapshots__/empty-state.test.tsx.snap new file mode 100644 index 0000000000000000000000000000000000000000..c4a7103c13a3a19419e7bf5323d923717c989b51 --- /dev/null +++ b/webui2/src/components/ui/__snapshots__/empty-state.test.tsx.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`EmptyState/Default matches snapshot 1`] = ` +
+

+ No open issues found. +

+
+`; + +exports[`EmptyState/NotFound matches snapshot 1`] = ` +
+

+ Issue not found. +

+
+`; diff --git a/webui2/src/components/ui/__snapshots__/pagination.test.tsx.snap b/webui2/src/components/ui/__snapshots__/pagination.test.tsx.snap new file mode 100644 index 0000000000000000000000000000000000000000..5a02f0b58a61f8a200728fea93a94cc1fb9c60ee --- /dev/null +++ b/webui2/src/components/ui/__snapshots__/pagination.test.tsx.snap @@ -0,0 +1,206 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Pagination/Default matches snapshot 1`] = ` +
+
+
+ Content above pagination +
+ +
+
+`; + +exports[`Pagination/LastPage matches snapshot 1`] = ` +
+ +
+`; + +exports[`Pagination/MiddlePage matches snapshot 1`] = ` +
+ +
+`; diff --git a/webui2/src/components/ui/__snapshots__/section-heading.test.tsx.snap b/webui2/src/components/ui/__snapshots__/section-heading.test.tsx.snap new file mode 100644 index 0000000000000000000000000000000000000000..e3f9b04b7017fb433c7147f71e59fb3b1ef17c89 --- /dev/null +++ b/webui2/src/components/ui/__snapshots__/section-heading.test.tsx.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`SectionHeading/Default matches snapshot 1`] = ` +
+

+ Participants +

+
+`; + +exports[`SectionHeading/Labels matches snapshot 1`] = ` +
+

+ Labels +

+
+`; diff --git a/webui2/src/components/ui/comment-card.stories.tsx b/webui2/src/components/ui/comment-card.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..03b0490d1362eeb8f666250b2b3d47b4619bf3ea --- /dev/null +++ b/webui2/src/components/ui/comment-card.stories.tsx @@ -0,0 +1,68 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import * as CommentCard from "./comment-card"; + +const meta = { + component: CommentCard.Root, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { children: null }, + render: () => ( + + + + + Jane Doe + 2 hours ago + + +

This is a comment body with some text content.

+
+
+
+ ), +}; + +export const WithEditButton: Story = { + args: { children: null }, + render: () => ( + + + + + Bob Smith + 1 day ago + edited + + + +

Updated the configuration to fix the build issue.

+
+
+
+ ), +}; + +export const EmptyBody: Story = { + args: { children: null }, + render: () => ( + + + + + Alice Wu + just now + + +

No description provided.

+
+
+
+ ), +}; diff --git a/webui2/src/components/ui/comment-card.test.tsx b/webui2/src/components/ui/comment-card.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3eb83ee37ff9ba6c6e12eb61f8144ca21b0d9713 --- /dev/null +++ b/webui2/src/components/ui/comment-card.test.tsx @@ -0,0 +1,13 @@ +import { composeStories } from "@storybook/react-vite"; +import { expect, test } from "vitest"; + +import * as stories from "./comment-card.stories"; + +const composed = composeStories(stories); + +for (const [name, Story] of Object.entries(composed)) { + test(`CommentCard/${name} matches snapshot`, async () => { + await Story.run(); + expect(document.body.firstChild).toMatchSnapshot(); + }); +} diff --git a/webui2/src/components/ui/comment-card.tsx b/webui2/src/components/ui/comment-card.tsx new file mode 100644 index 0000000000000000000000000000000000000000..61060b25fea262948309bc1e248b5c2b84cec394 --- /dev/null +++ b/webui2/src/components/ui/comment-card.tsx @@ -0,0 +1,69 @@ +import { cn } from "@/lib/utils"; + +import { Avatar, AvatarFallback, AvatarImage } from "./avatar"; + +interface RootProps { + children: React.ReactNode; + className?: string; +} + +export function Root({ children, className }: RootProps) { + return
{children}
; +} + +interface AuthorAvatarProps { + src?: string | null; + name: string; + className?: string; +} + +export function AuthorAvatar({ src, name, className }: AuthorAvatarProps) { + return ( + + + + {name.slice(0, 2).toUpperCase()} + + + ); +} + +interface CardProps { + children: React.ReactNode; + className?: string; +} + +export function Card({ children, className }: CardProps) { + return ( +
+ {children} +
+ ); +} + +interface CardHeaderProps { + children: React.ReactNode; + className?: string; +} + +export function CardHeader({ children, className }: CardHeaderProps) { + return ( +
+ {children} +
+ ); +} + +interface CardBodyProps { + children: React.ReactNode; + className?: string; +} + +export function CardBody({ children, className }: CardBodyProps) { + return
{children}
; +} diff --git a/webui2/src/components/ui/empty-state.stories.tsx b/webui2/src/components/ui/empty-state.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..411d598da83651a535c53ecb368775d3f41ce5cb --- /dev/null +++ b/webui2/src/components/ui/empty-state.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import { EmptyState } from "./empty-state"; + +const meta = { + component: EmptyState, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { children: "No open issues found." }, +}; + +export const NotFound: Story = { + args: { children: "Issue not found." }, +}; diff --git a/webui2/src/components/ui/empty-state.test.tsx b/webui2/src/components/ui/empty-state.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bf91e792dd2b322d3cf7234e4fe98f045435cbff --- /dev/null +++ b/webui2/src/components/ui/empty-state.test.tsx @@ -0,0 +1,13 @@ +import { composeStories } from "@storybook/react-vite"; +import { expect, test } from "vitest"; + +import * as stories from "./empty-state.stories"; + +const composed = composeStories(stories); + +for (const [name, Story] of Object.entries(composed)) { + test(`EmptyState/${name} matches snapshot`, async () => { + await Story.run(); + expect(document.body.firstChild).toMatchSnapshot(); + }); +} diff --git a/webui2/src/components/ui/empty-state.tsx b/webui2/src/components/ui/empty-state.tsx new file mode 100644 index 0000000000000000000000000000000000000000..808c8d06e110d880d17c764601c7953fd568c0cd --- /dev/null +++ b/webui2/src/components/ui/empty-state.tsx @@ -0,0 +1,14 @@ +import { cn } from "@/lib/utils"; + +interface EmptyStateProps { + children: React.ReactNode; + className?: string; +} + +export function EmptyState({ children, className }: EmptyStateProps) { + return ( +

+ {children} +

+ ); +} diff --git a/webui2/src/components/ui/pagination.stories.tsx b/webui2/src/components/ui/pagination.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..11520ee9f7020b7a4f135c3fc042df6eef282c1e --- /dev/null +++ b/webui2/src/components/ui/pagination.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import { withRouter } from "@/../.storybook/decorators"; + +import * as Pagination from "./pagination"; + +const meta = { + component: Pagination.Root, + decorators: [withRouter], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { children: null }, + render: () => ( +
+
Content above pagination
+ + + Page 1 of 5 + + +
+ ), +}; + +export const MiddlePage: Story = { + args: { children: null }, + render: () => ( + + + Page 3 of 5 + + + ), +}; + +export const LastPage: Story = { + args: { children: null }, + render: () => ( + + + Page 5 of 5 + + + ), +}; diff --git a/webui2/src/components/ui/pagination.test.tsx b/webui2/src/components/ui/pagination.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b0baa076c20eaab58e4049c9a69be76d5429f541 --- /dev/null +++ b/webui2/src/components/ui/pagination.test.tsx @@ -0,0 +1,13 @@ +import { composeStories } from "@storybook/react-vite"; +import { expect, test } from "vitest"; + +import * as stories from "./pagination.stories"; + +const composed = composeStories(stories); + +for (const [name, Story] of Object.entries(composed)) { + test(`Pagination/${name} matches snapshot`, async () => { + await Story.run(); + expect(document.body.firstChild).toMatchSnapshot(); + }); +} diff --git a/webui2/src/components/ui/pagination.tsx b/webui2/src/components/ui/pagination.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4644ef5e26efa349e09faab07421c964501fbf12 --- /dev/null +++ b/webui2/src/components/ui/pagination.tsx @@ -0,0 +1,82 @@ +import { createLink, type LinkComponent } from "@tanstack/react-router"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +import { buttonVariants } from "./button"; + +interface RootProps { + children: React.ReactNode; + className?: string; +} + +export function Root({ children, className }: RootProps) { + return ( +
+ {children} +
+ ); +} + +interface InfoProps { + children: React.ReactNode; +} + +export function Info({ children }: InfoProps) { + return {children}; +} + +// Previous/Next are createLink-wrapped so they work as router links. +const PreviousComponent = React.forwardRef< + HTMLAnchorElement, + { className?: string; children?: React.ReactNode; disabled?: boolean } & React.AnchorHTMLAttributes +>(({ className, children, ...props }, ref) => ( + + + {children ?? "Previous"} + +)); +PreviousComponent.displayName = "PreviousComponent"; + +const CreatedPrevious = createLink(PreviousComponent); +export const Previous: LinkComponent = (props) => ( + +); + +const NextComponent = React.forwardRef< + HTMLAnchorElement, + { className?: string; children?: React.ReactNode; disabled?: boolean } & React.AnchorHTMLAttributes +>(({ className, children, ...props }, ref) => ( + + {children ?? "Next"} + + +)); +NextComponent.displayName = "NextComponent"; + +const CreatedNext = createLink(NextComponent); +export const Next: LinkComponent = (props) => ( + +); diff --git a/webui2/src/components/ui/section-heading.stories.tsx b/webui2/src/components/ui/section-heading.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..53b54fa8c30cae2e90c26dae06a5de3c9222d4b2 --- /dev/null +++ b/webui2/src/components/ui/section-heading.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import { SectionHeading } from "./section-heading"; + +const meta = { + component: SectionHeading, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { children: "Participants" }, +}; + +export const Labels: Story = { + args: { children: "Labels" }, +}; diff --git a/webui2/src/components/ui/section-heading.test.tsx b/webui2/src/components/ui/section-heading.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b1a0e5b1b0d6d99f28b0ad5aae69cfe6228e24f3 --- /dev/null +++ b/webui2/src/components/ui/section-heading.test.tsx @@ -0,0 +1,13 @@ +import { composeStories } from "@storybook/react-vite"; +import { expect, test } from "vitest"; + +import * as stories from "./section-heading.stories"; + +const composed = composeStories(stories); + +for (const [name, Story] of Object.entries(composed)) { + test(`SectionHeading/${name} matches snapshot`, async () => { + await Story.run(); + expect(document.body.firstChild).toMatchSnapshot(); + }); +} diff --git a/webui2/src/components/ui/section-heading.tsx b/webui2/src/components/ui/section-heading.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fa3e6c7a0057c15c7dab57593bec3e9c4968ee5b --- /dev/null +++ b/webui2/src/components/ui/section-heading.tsx @@ -0,0 +1,14 @@ +import { cn } from "@/lib/utils"; + +interface SectionHeadingProps { + children: React.ReactNode; + className?: string; +} + +export function SectionHeading({ children, className }: SectionHeadingProps) { + return ( +

+ {children} +

+ ); +}