diff --git a/webui2/src/components/bugs/IssueRow.stories.tsx b/webui2/src/components/bugs/IssueRow.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..01a3a066262ba3d4d29731525cda06b490520759 --- /dev/null +++ b/webui2/src/components/bugs/IssueRow.stories.tsx @@ -0,0 +1,123 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { formatDistanceToNow } from "date-fns"; + +import { Status } from "@/__generated__/graphql"; +import { withRouter } from "@/../.storybook/decorators"; + +import * as IssueRow from "./IssueRow"; +import { LabelBadge } from "./LabelBadge"; + +const meta = { + component: IssueRow.Root, + decorators: [withRouter], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const ago = formatDistanceToNow(new Date(Date.now() - 3600 * 1000), { addSuffix: true }); + +export const OpenIssue: Story = { + args: { children: null }, + render: () => ( + + +
+ + + Fix login page crash on empty email + + + + + + #a1b2c3 opened {ago} by Jane Doe + +
+ +
+ ), +}; + +export const ClosedIssue: Story = { + args: { children: null }, + render: () => ( + + +
+ + + Add dark mode support + + + + #d4e5f6 opened {ago} +
+ +
+ ), +}; + +export const NoLabelsNoComments: Story = { + args: { children: null }, + render: () => ( + + +
+ + + Simple issue with no labels + + + #abc123 opened {ago} by Bob +
+ +
+ ), +}; + +export const List: Story = { + args: { children: null }, + render: () => ( +
+ + +
+ + + Fix login page crash on empty email + + + + #a1b2c3 opened {ago} by Jane Doe +
+ +
+ + +
+ + + Add dark mode support + + + + #d4e5f6 opened {ago} by Bob +
+ +
+ + +
+ + + Update dependencies + + + #g7h8i9 opened {ago} by Alice +
+ +
+
+ ), +}; diff --git a/webui2/src/components/bugs/IssueRow.tsx b/webui2/src/components/bugs/IssueRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2f45891968aba5044700dd714bbeb0c850eeeefd --- /dev/null +++ b/webui2/src/components/bugs/IssueRow.tsx @@ -0,0 +1,71 @@ +import { CircleDot, CircleCheck, MessageSquare } from "lucide-react"; + +import { Status } from "@/__generated__/graphql"; +import { cn } from "@/lib/utils"; + +interface RootProps { + className?: string; + children: React.ReactNode; +} + +export function Root({ className, children }: RootProps) { + return ( +
+ {children} +
+ ); +} + +interface StatusIconProps { + status: Status; +} + +export function StatusIcon({ status }: StatusIconProps) { + const isOpen = status === Status.Open; + const Icon = isOpen ? CircleDot : CircleCheck; + return ( + + ); +} + +interface TitleAreaProps { + children: React.ReactNode; +} + +export function TitleArea({ children }: TitleAreaProps) { + return
{children}
; +} + +interface MetaProps { + children: React.ReactNode; +} + +export function Meta({ children }: MetaProps) { + return

{children}

; +} + +interface CommentCountProps { + count: number; +} + +export function CommentCount({ count }: CommentCountProps) { + if (count <= 0) return null; + return ( +
+ + {count} +
+ ); +} diff --git a/webui2/src/components/bugs/LabelBadge.tsx b/webui2/src/components/bugs/LabelBadge.tsx index fa2c98e3e20d3e0a44b2e539ed616a0b7d7c2755..8a5e0c47d4f0549ca8b9d8d1647e857844128879 100644 --- a/webui2/src/components/bugs/LabelBadge.tsx +++ b/webui2/src/components/bugs/LabelBadge.tsx @@ -1,7 +1,10 @@ +import { createLink, type LinkComponent } from "@tanstack/react-router"; +import * as React from "react"; + interface LabelBadgeProps { name: string; color: { R: number; G: number; B: number }; - onClick?: (name: string) => void; + className?: string; } function contrastColor(r: number, g: number, b: number): string { @@ -10,35 +13,50 @@ function contrastColor(r: number, g: number, b: number): string { return luminance > 0.55 ? "rgba(0,0,0,0.75)" : "rgba(255,255,255,0.9)"; } -// Coloured label pill. Renders as a + ); - } - - return ( - - {name} - - ); -} + }, +); +LabelBadge.displayName = "LabelBadge"; + +// LabelBadge as a TanStack Router link — renders as with label styling. +const CreatedLabelBadgeLink = createLink( + React.forwardRef, "color">>( + ({ name, color, className, ...props }, ref) => { + const bg = `rgb(${color.R},${color.G},${color.B})`; + const text = contrastColor(color.R, color.G, color.B); + + return ( + + {name} + + ); + }, + ), +); + +const LabelBadgeLink: LinkComponent = (props) => { + return ; +}; + +export { LabelBadge, LabelBadgeLink }; diff --git a/webui2/src/components/ui/status-tabs.stories.tsx b/webui2/src/components/ui/status-tabs.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6b1f6badaadeff8115f94a7b40aad80f52f2e44f --- /dev/null +++ b/webui2/src/components/ui/status-tabs.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import * as StatusTabs from "./status-tabs"; + +const meta = { + component: StatusTabs.Root, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { children: null }, + render: () => ( + + + + + ), +}; diff --git a/webui2/src/components/ui/status-tabs.tsx b/webui2/src/components/ui/status-tabs.tsx new file mode 100644 index 0000000000000000000000000000000000000000..39eb9fb5c75705163b54b460fde425006bcb6763 --- /dev/null +++ b/webui2/src/components/ui/status-tabs.tsx @@ -0,0 +1,78 @@ +import { createLink, type LinkComponent } from "@tanstack/react-router"; +import { CircleDot, CircleCheck } from "lucide-react"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +interface RootProps { + children: React.ReactNode; + className?: string; +} + +export function Root({ children, className }: RootProps) { + return
{children}
; +} + +// Tab is a createLink-wrapped component — it IS the link. +const TabComponent = React.forwardRef< + HTMLAnchorElement, + { className?: string; children?: React.ReactNode } & React.AnchorHTMLAttributes +>(({ className, children, ...props }, ref) => { + return ( + + {children} + + ); +}); +TabComponent.displayName = "TabComponent"; + +const CreatedTab = createLink(TabComponent); + +export const Tab: LinkComponent = (props) => { + return ( + + ); +}; + +interface IndicatorProps { + className?: string; +} + +export function OpenIndicator({ className }: IndicatorProps) { + return ( + + ); +} + +export function ClosedIndicator({ className }: IndicatorProps) { + return ( + + ); +} + +interface CountProps { + children: React.ReactNode; +} + +export function Count({ children }: CountProps) { + return ( + + {children} + + ); +} diff --git a/webui2/src/components/ui/write-preview.stories.tsx b/webui2/src/components/ui/write-preview.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c6d7cd12d7401c1b94ae7bbff947ce85f30157ee --- /dev/null +++ b/webui2/src/components/ui/write-preview.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useState } from "react"; + +import { Markdown } from "@/components/content/Markdown"; +import { Textarea } from "@/components/ui/textarea"; +import { withRouter } from "@/../.storybook/decorators"; + +import * as WritePreview from "./write-preview"; + +const meta = { + component: WritePreview.Root, + decorators: [withRouter], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Uncontrolled: Story = { + args: { children: null }, + render: () => { + const [message, setMessage] = useState("Hello **world**!"); + return ( + + + +