diff --git a/react/jest.config.js b/react/jest.config.js new file mode 100644 index 0000000..75a5a79 --- /dev/null +++ b/react/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + testMatch: ['**/*.test.ts', '**/*.test.tsx'], + setupFilesAfterEnv: ['/jest.setup.ts'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { tsconfig: { jsx: 'react-jsx', module: 'commonjs', esModuleInterop: true } }, + ], + }, +}; diff --git a/react/jest.setup.ts b/react/jest.setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/react/jest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/react/package.json b/react/package.json index eb6c0c7..f89666a 100644 --- a/react/package.json +++ b/react/package.json @@ -11,7 +11,8 @@ "scripts": { "build": "tsup src/index.ts --format cjs,esm --dts --clean", "dev": "tsup src/index.ts --format cjs,esm --watch --dts", - "lint": "eslint src/**/*.ts" + "lint": "eslint src/**/*.ts", + "test": "jest" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -21,10 +22,17 @@ "@bc-forge/sdk": "file:../sdk" }, "devDependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.4.0", + "@testing-library/react": "^16.0.0", + "@types/jest": "^29.5.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "react": "^18.0.0", "react-dom": "^18.0.0", + "ts-jest": "^29.1.0", "tsup": "^8.0.0", "typescript": "^5.0.0" } diff --git a/react/src/components/Alert.tsx b/react/src/components/Alert.tsx new file mode 100644 index 0000000..9aec061 --- /dev/null +++ b/react/src/components/Alert.tsx @@ -0,0 +1,71 @@ +import React, { forwardRef } from 'react'; + +export type AlertVariant = 'info' | 'success' | 'warning' | 'danger'; + +export interface AlertProps extends Omit, 'title'> { + /** Visual + semantic style of the alert. @default 'info' */ + variant?: AlertVariant; + /** Optional bold title rendered above the content. */ + title?: React.ReactNode; + /** When provided, renders a dismiss button that calls this handler. */ + onDismiss?: () => void; + /** Accessible label for the dismiss button. @default 'Dismiss alert' */ + dismissLabel?: string; +} + +const VARIANT_STYLES: Record = { + info: { backgroundColor: '#eff6ff', borderColor: '#bfdbfe', color: '#1e40af' }, + success: { backgroundColor: '#f0fdf4', borderColor: '#bbf7d0', color: '#166534' }, + warning: { backgroundColor: '#fffbeb', borderColor: '#fde68a', color: '#92400e' }, + danger: { backgroundColor: '#fef2f2', borderColor: '#fecaca', color: '#991b1b' }, +}; + +/** Alert banner; role is "alert" for danger/warning and "status" otherwise. */ +export const Alert = forwardRef(function Alert( + { variant = 'info', title, onDismiss, dismissLabel = 'Dismiss alert', style, children, ...rest }, + ref, +) { + const defaultRole = variant === 'danger' || variant === 'warning' ? 'alert' : 'status'; + + return ( +
+
+ {title ?
{title}
: null} +
{children}
+
+ {onDismiss ? ( + + ) : null} +
+ ); +}); diff --git a/react/src/components/Badge.tsx b/react/src/components/Badge.tsx new file mode 100644 index 0000000..e86ead7 --- /dev/null +++ b/react/src/components/Badge.tsx @@ -0,0 +1,64 @@ +import React, { forwardRef } from 'react'; + +export type BadgeVariant = + | 'default' + | 'primary' + | 'success' + | 'warning' + | 'danger' + | 'info'; + +export type BadgeSize = 'sm' | 'md' | 'lg'; + +export interface BadgeProps extends React.HTMLAttributes { + /** Visual style of the badge. @default 'default' */ + variant?: BadgeVariant; + /** Size of the badge. @default 'md' */ + size?: BadgeSize; +} + +const VARIANT_STYLES: Record = { + default: { backgroundColor: '#f3f4f6', color: '#374151' }, + primary: { backgroundColor: '#e0e7ff', color: '#3730a3' }, + success: { backgroundColor: '#dcfce7', color: '#166534' }, + warning: { backgroundColor: '#fef3c7', color: '#92400e' }, + danger: { backgroundColor: '#fee2e2', color: '#991b1b' }, + info: { backgroundColor: '#cffafe', color: '#155e75' }, +}; + +const SIZE_STYLES: Record = { + sm: { fontSize: 11, padding: '1px 6px' }, + md: { fontSize: 12, padding: '2px 8px' }, + lg: { fontSize: 14, padding: '4px 10px' }, +}; + +const BASE_STYLE: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + gap: 4, + borderRadius: 9999, + fontWeight: 600, + lineHeight: 1.4, + whiteSpace: 'nowrap', +}; + +/** Accessible status badge; forwards all standard span props and a ref. */ +export const Badge = forwardRef(function Badge( + { variant = 'default', size = 'md', style, children, ...rest }, + ref, +) { + return ( + + {children} + + ); +}); diff --git a/react/src/components/COMPONENTS.md b/react/src/components/COMPONENTS.md new file mode 100644 index 0000000..a1694cc --- /dev/null +++ b/react/src/components/COMPONENTS.md @@ -0,0 +1,113 @@ +# @bc-forge/react UI components + +Reusable, accessible, dependency-free React components. Each component ships +with inline styles (no CSS import or Tailwind setup required) and forwards +standard HTML attributes so it slots into any design system. + +```tsx +import { Badge, Alert, Pagination, Modal, Tooltip } from '@bc-forge/react'; +``` + +## Badge + +Status pill for labels, counts, and states. + +| Prop | Type | Default | Description | +| --------- | --------------------------------------------------------------------- | ----------- | ------------------------------------ | +| `variant` | `'default' \| 'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'` | `'default'` | Visual style. | +| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Badge size. | +| `...rest` | `React.HTMLAttributes` | — | Any span prop (`className`, `aria-*`). | + +Renders a `` and forwards a `ref`. Provide `aria-label` when the visible +text isn't descriptive on its own (e.g. a bare count). + +```tsx +Verified +3 +``` + +## Alert + +Inline notification banner. The ARIA role is derived from the variant — +`danger`/`warning` → `role="alert"` (assertive), `info`/`success` → +`role="status"` (polite). Pass `role` to override. + +| Prop | Type | Default | Description | +| -------------- | ----------------------------------------------- | ---------------- | ------------------------------------------ | +| `variant` | `'info' \| 'success' \| 'warning' \| 'danger'` | `'info'` | Visual + semantic style. | +| `title` | `React.ReactNode` | — | Optional bold heading. | +| `onDismiss` | `() => void` | — | When set, renders a keyboard-focusable dismiss button. | +| `dismissLabel` | `string` | `'Dismiss alert'`| Accessible label for the dismiss button. | +| `...rest` | `React.HTMLAttributes` | — | Any div prop. | + +```tsx +Your changes were stored. + setError(null)}>Mint failed. +``` + +## Pagination + +Page navigation rendered as a `