feat: add storybook and new badge component with stories

This commit is contained in:
Scott Wilson
2025-10-23 09:30:30 -07:00
parent 1bf55017a1
commit 6d95985eec
19 changed files with 2437 additions and 168 deletions

3
frontend/.gitignore vendored
View File

@@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
*storybook.log
storybook-static

View File

@@ -0,0 +1,17 @@
import type { Decorator } from "@storybook/react-vite";
import { createRootRoute, createRouter, RouterProvider } from "@tanstack/react-router";
export const RouterDecorator: Decorator = (Story) => {
const rootRoute = createRootRoute({
component: Story
});
const routeTree = rootRoute;
const router = createRouter({
routeTree
});
// @ts-expect-error just to make Links happy :)
return <RouterProvider router={router} />;
};

View File

@@ -0,0 +1 @@
export * from "./RouterDecorator";

View File

@@ -0,0 +1,14 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: [
"../src/components/v3/**/*.mdx",
"../src/components/v3/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
addons: ["@storybook/addon-docs", "@storybook/addon-a11y"],
framework: {
name: "@storybook/react-vite",
options: {}
}
};
export default config;

View File

@@ -0,0 +1,45 @@
import type { Preview } from "@storybook/react-vite";
import { RouterDecorator } from "./decorators";
import "../src/index.css";
const preview: Preview = {
decorators: [
(Story) => {
const root = document.getElementsByTagName("html")[0];
root.setAttribute("class", "overflow-visible");
return <Story />;
},
RouterDecorator
],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
},
docs: {
backgroundColor: "var(--background)"
},
a11y: {
test: "todo"
},
backgrounds: {
default: "dark",
options: {
dark: { name: "Dark", value: "var(--background)" }
}
}
},
initialGlobals: {
backgrounds: {
value: "dark"
}
}
};
export default preview;

View File

@@ -1,3 +1,6 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from "eslint-plugin-storybook";
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
@@ -133,5 +136,6 @@ export default tseslint.config(
rules: Object.fromEntries(
Object.keys(stylisticPlugin.configs["all-flat"].rules ?? {}).map((key) => [key, "off"])
)
}
},
storybook.configs["flat/recommended"]
);

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,9 @@
"preview": "vite preview",
"lint": "eslint ./src",
"lint:fix": "eslint --fix ./src",
"type:check": "tsc --noEmit --project ./tsconfig.app.json"
"type:check": "tsc --noEmit --project ./tsconfig.app.json",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"overrides": {
"sha.js": "2.4.12"
@@ -47,7 +49,9 @@
"@radix-ui/react-popper": "^1.2.1",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.3",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.3",
@@ -64,6 +68,7 @@
"argon2-browser": "^1.18.0",
"axios": "^1.12.0",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"cva": "npm:class-variance-authority@^0.7.1",
"date-fns": "^4.1.0",
"dompurify": "^3.2.4",
@@ -76,6 +81,7 @@
"jsrp": "^0.2.4",
"jwt-decode": "^4.0.0",
"lexical": "^0.29.0",
"lucide-react": "^0.544.0",
"ms": "^2.1.3",
"nprogress": "^0.2.0",
"picomatch": "^4.0.2",
@@ -105,6 +111,9 @@
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.15.0",
"@kesills/eslint-config-airbnb-typescript": "^20.0.0",
"@storybook/addon-a11y": "^9.1.9",
"@storybook/addon-docs": "^9.1.9",
"@storybook/react-vite": "^9.1.9",
"@stylistic/eslint-plugin": "^2.12.1",
"@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/typography": "^0.5.15",
@@ -130,6 +139,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.14",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-storybook": "^9.1.9",
"globals": "^15.12.0",
"postcss": "^8.4.49",
"prettier": "3.4.2",

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,307 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Link } from "@tanstack/react-router";
import {
BanIcon,
BoxesIcon,
BoxIcon,
CheckIcon,
ChevronsUpDownIcon,
CircleXIcon,
ExternalLinkIcon,
GlobeIcon,
InfoIcon,
RadarIcon,
TriangleAlertIcon
} from "lucide-react";
import { Badge } from "./Badge";
/**
* Badges act as an indicator that can optionally be made interactable.
* You can place text and icons inside a badge.
* Badges are often used for the indication of a status, state or scope.
*/
const meta = {
title: "Generic/Badge",
component: Badge,
parameters: {
layout: "centered"
},
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["neutral", "success", "info", "warning", "danger", "project", "org", "sub-org"]
},
isTruncatable: {
table: {
disable: true
}
},
asChild: {
table: {
disable: true
}
},
children: {
table: {
disable: true
}
}
},
args: { children: "Badge", isTruncatable: false }
} satisfies Meta<typeof Badge>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Neutral: Story = {
name: "Variant: Neutral",
args: {
variant: "neutral",
children: (
<>
<BanIcon />
Disabled
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating neutral or disabled states."
}
}
}
};
export const Success: Story = {
name: "Variant: Success",
args: {
variant: "success",
children: (
<>
<CheckIcon />
Success
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating successful or healthy states."
}
}
}
};
export const Info: Story = {
name: "Variant: Info",
args: {
variant: "info",
children: (
<>
<InfoIcon />
Info
</>
)
},
parameters: {
docs: {
description: {
story:
"Use this variant when indicating informational states or linking to external references."
}
}
}
};
export const Warning: Story = {
name: "Variant: Warning",
args: {
variant: "warning",
children: (
<>
<TriangleAlertIcon />
Warning
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating activity or attention warranting states."
}
}
}
};
export const Danger: Story = {
name: "Variant: Danger",
args: {
variant: "danger",
children: (
<>
<CircleXIcon />
Danger
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating destructive or error states."
}
}
}
};
export const Organization: Story = {
name: "Variant: Organization",
args: {
variant: "org",
children: (
<>
<GlobeIcon />
Organization
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating organization scope or links."
}
}
}
};
export const SubOrganization: Story = {
name: "Variant: Sub-Organization",
args: {
variant: "sub-org",
children: (
<>
<BoxesIcon />
Sub-Organization
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating sub-organization scope or links."
}
}
}
};
export const Project: Story = {
name: "Variant: Project",
args: {
variant: "project",
children: (
<>
<BoxIcon />
Project
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating project scope or links."
}
}
}
};
export const AsExternalLink: Story = {
name: "Example: As External Link",
args: {
variant: "info",
asChild: true,
children: (
<a href="https://infisical.com/">
Link <ExternalLinkIcon />
</a>
)
},
parameters: {
docs: {
description: {
story: "Use the `asChild` prop with an `a` tag to use a badge as an external link."
}
}
}
};
export const AsRouterLink: Story = {
name: "Example: As Router Link",
args: {
variant: "project",
asChild: true,
children: (
<Link to=".">
<RadarIcon />
Secret Scanning
</Link>
)
},
parameters: {
docs: {
description: {
story: "Use the `asChild` prop with a `Link` component to use a badge as an internal link."
}
}
}
};
export const AsButton: Story = {
name: "Example: As Button",
args: {
variant: "org",
asChild: true,
children: (
<button type="button" onClick={() => alert("button clicked")}>
<GlobeIcon />
Organization
<ChevronsUpDownIcon />
</button>
)
},
parameters: {
docs: {
description: {
story:
"Use the `asChild` prop with a `button` tag to use a badge as a button. Do not use a styled `Button` component."
}
}
}
};
export const IsTruncatable: Story = {
name: "Example: isTruncatable",
args: {
isTruncatable: true,
children: (
<>
<GlobeIcon />
<span>Infisical Infrastructure</span>
</>
)
},
parameters: {
docs: {
description: {
story:
"Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation."
}
}
},
decorators: (Story) => (
<div className="flex w-32">
<Story />
</div>
)
};

View File

@@ -0,0 +1,82 @@
import { forwardRef } from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "cva";
import { cn } from "@app/components/v3/utils";
const badgeVariants = cva(
[
"select-none items-center rounded-sm px-1.5 py-0.5 text-xs",
"gap-x-1 [a&,button&]:cursor-pointer inline-flex",
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&>svg]:size-3",
"transition duration-200 ease-in-out"
],
{
variants: {
isTruncatable: {
true: "[&>span,&>p]:truncate min-w-0",
false: "w-fit shrink-0 whitespace-nowrap overflow-hidden"
},
variant: {
neutral: [
"border-neutral/75 bg-neutral/30 text-neutral",
"[a&,button&]:hover:bg-neutral/40 [a&,button&]:hover:border-neutral"
],
success: [
"border-success/75 bg-success/30 text-success",
"[a&,button&]:hover:bg-success/40 [a&,button&]:hover:border-success"
],
info: [
"border-info/75 bg-info/30 text-info",
"[a&,button&]:hover:bg-info/40 [a&,button&]:hover:border-info"
],
warning: [
"border-warning/75 bg-warning/30 text-warning",
"[a&,button&]:hover:bg-warning/40 [a&,button&]:hover:border-warning"
],
danger: [
"border-danger/75 bg-danger/30 text-danger",
"[a&,button&]:hover:bg-danger/40 [a&,button&]:hover:border-danger"
],
project: [
"border-project/75 bg-project/30 text-project",
"[a&,button&]:hover:bg-project/40 [a&,button&]:hover:border-project"
],
org: [
"border-org/75 bg-org/30 text-org",
"[a&,button&]:hover:bg-org/40 [a&,button&]:hover:border-org"
],
"sub-org": [
"border-sub-org/75 bg-sub-org/30 text-sub-org",
"[a&,button&]:hover:bg-sub-org/40 [a&,button&]:hover:border-sub-org"
]
}
},
defaultVariants: {
variant: "success"
}
}
);
type TBadgeProps = VariantProps<typeof badgeVariants> &
React.ComponentProps<"span"> & {
asChild?: boolean;
};
const Badge = forwardRef<HTMLButtonElement | HTMLSpanElement | HTMLAnchorElement, TBadgeProps>(
({ className, variant, asChild = false, isTruncatable = false, ...props }, ref): JSX.Element => {
const Comp = asChild ? Slot : "span";
return (
<Comp
ref={ref}
data-slot="badge"
className={cn(badgeVariants({ variant, isTruncatable }), className)}
{...props}
/>
);
}
);
Badge.displayName = "Badge";
export { Badge, badgeVariants, type TBadgeProps };

View File

@@ -0,0 +1 @@
export * from "./Badge";

View File

@@ -0,0 +1 @@
export * from "./Badge";

View File

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -37,12 +37,26 @@
}
@theme {
/*legacy color schema */
/* Fonts */
--font-inter: "Inter", sans-serif;
--max-width-8xl: 88rem; /* 1408px */
/* Colors v2 */
--color-background: #19191c;
--color-foreground: white;
--color-success: #2ecc71;
--color-info: #34c2db;
--color-warning: #f1c40f;
--color-danger: #e74c3c;
--color-org: #30B3FF;
--color-sub-org: #96ff59;
--color-project: #e0ed34;
--color-neutral: #adaeb0;
/*legacy color schema */
--color-org-v1: #30B3FF;
--color-namespace-v1: #96ff59;
--max-width-8xl: 88rem; /* 1408px */
/* Primary */
--color-primary-50: #fffff5;
--color-primary-100: #fcfce8;

View File

@@ -31,5 +31,7 @@
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",
"./.storybook/**/*.ts",
"./.storybook/**/*.tsx",
],
}