diff --git a/.github/workflows/pull_request_checks.yml b/.github/workflows/pull_request_checks.yml new file mode 100644 index 0000000..ad7b5e4 --- /dev/null +++ b/.github/workflows/pull_request_checks.yml @@ -0,0 +1,34 @@ +name: Pull Request Checks + +on: + pull_request: + branches: + - main + +env: + BUN_VERSION: 1.3.6 + +jobs: + ci: + name: Lint, Typecheck & Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: ${{ env.BUN_VERSION }} + - name: Configure bun to install from GitHub Packages + run: rm -rf .npmrc + - name: Install dependencies + run: bun install + env: + SCHEMAVAULTS_GITHUB_PACKAGE_REGISTRY_USER: ${{ github.actor }} + SCHEMAVAULTS_GITHUB_PACKAGE_REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Lint + run: bun run lint + - name: Typecheck + run: bun run typecheck + - name: Build + run: bun run build diff --git a/CLAUDE.md b/CLAUDE.md index ecdae1b..25154a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,12 @@ This is `@schemavaults/ui`, a React component library package for SchemaVaults f ## Commands +**Important:** Always run `bun install` first before running any of the commands below. Dependencies may not be installed in a fresh environment, and typecheck/lint/build will fail without them. + ```bash +# Install dependencies (run this first) +bun install + # Build the package (compiles TypeScript and resolves path aliases) bun run build diff --git a/package.json b/package.json index f641932..bb8d446 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@schemavaults/ui", - "version": "0.13.13", + "version": "0.13.14", "private": false, "license": "UNLICENSED", "description": "React.js UI components for SchemaVaults frontend applications", diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index b96652e..2da7daa 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -128,3 +128,6 @@ export type * from "./slider"; export * from "./switch"; export type * from "./switch"; + +export * from "./progress-bar"; +export type * from "./progress-bar"; diff --git a/src/components/ui/progress-bar/ProgressBar.stories.tsx b/src/components/ui/progress-bar/ProgressBar.stories.tsx new file mode 100644 index 0000000..8a53336 --- /dev/null +++ b/src/components/ui/progress-bar/ProgressBar.stories.tsx @@ -0,0 +1,105 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { ReactElement } from "react"; +import { LazyFramerMotionProvider } from "@/providers/lazy_framer"; +import { ProgressBar, progressBarSizeIds } from "./progress-bar"; + +const meta = { + title: "Components/ProgressBar", + component: ProgressBar, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + value: { + control: { + type: "range", + min: 0, + max: 100, + step: 1, + }, + }, + size: { + options: progressBarSizeIds, + control: { + type: "radio", + }, + }, + min: { + control: { + type: "number", + }, + }, + max: { + control: { + type: "number", + }, + }, + }, + args: { + value: 50, + label: "Progress", + min: 0, + max: 100, + }, + decorators: [ + (Story): ReactElement => { + return ( + +
+ +
+
+ ); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 50, + label: "Upload progress", + }, +}; + +export const Empty: Story = { + args: { + value: 0, + label: "Upload progress", + }, +}; + +export const Full: Story = { + args: { + value: 100, + label: "Upload progress", + }, +}; + +export const Small: Story = { + args: { + value: 65, + label: "Upload progress", + size: "sm", + }, +}; + +export const Large: Story = { + args: { + value: 75, + label: "Upload progress", + size: "lg", + }, +}; + +export const CustomRange: Story = { + args: { + value: 7, + label: "Steps completed", + min: 0, + max: 10, + }, +}; diff --git a/src/components/ui/progress-bar/index.ts b/src/components/ui/progress-bar/index.ts new file mode 100644 index 0000000..15979cd --- /dev/null +++ b/src/components/ui/progress-bar/index.ts @@ -0,0 +1,3 @@ +export { ProgressBar, progressBarVariants, progressBarSizeIds } from "./progress-bar"; +export type * from "./progress-bar"; +export { ProgressBar as default } from "./progress-bar"; diff --git a/src/components/ui/progress-bar/progress-bar.tsx b/src/components/ui/progress-bar/progress-bar.tsx new file mode 100644 index 0000000..9718199 --- /dev/null +++ b/src/components/ui/progress-bar/progress-bar.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { cva, type VariantProps } from "class-variance-authority"; +import { m } from "@/framer-motion"; +import { cn } from "@/lib/utils"; +import type { ReactElement, HTMLAttributes } from "react"; + +export const progressBarVariants = cva( + "relative w-full overflow-hidden rounded-full bg-secondary", + { + variants: { + size: { + sm: "h-2", + default: "h-3", + lg: "h-5", + }, + }, + defaultVariants: { + size: "default", + }, + }, +); + +export const progressBarSizeIds = ["sm", "default", "lg"] as const; + +export type ProgressBarSizeId = (typeof progressBarSizeIds)[number]; + +export interface ProgressBarProps + extends Omit, "role">, + VariantProps { + /** Current progress value (0-100) */ + value: number; + /** Accessible label describing what the progress bar represents */ + label: string; + /** Minimum value (defaults to 0) */ + min?: number; + /** Maximum value (defaults to 100) */ + max?: number; + /** Additional classes for the filled indicator */ + indicatorClassName?: string; +} + +export function ProgressBar({ + value, + label, + min = 0, + max = 100, + size, + className, + indicatorClassName, + ...props +}: ProgressBarProps): ReactElement { + const clampedValue: number = Math.min(max, Math.max(min, value)); + const percentage: number = ((clampedValue - min) / (max - min)) * 100; + + return ( +
+ +
+ ); +} + +ProgressBar.displayName = "ProgressBar";