diff --git a/.compliance/config.yaml b/.compliance/config.yaml index 4a0842e..2ba947c 100644 --- a/.compliance/config.yaml +++ b/.compliance/config.yaml @@ -19,3 +19,19 @@ ignore_paths: - ".github/**" - ".storybook/**" - ".devcontainer/**" + +# Suppress database-related rules for the showcase app. +# The showcase uses a minimal SQLite DB for demo/testing purposes only; +# it is not a production system and does not require tenant isolation, +# role separation, or MFA enforcement. +suppressions: + - rule: CON-DVO-002 + paths: + - "packages/showcase/src/database/**" + - "packages/showcase/src/types/**" + reason: "Showcase DB is for testing/demo only, not a production system" + - rule: CON-PFM-001 + paths: + - "packages/showcase/src/database/**" + - "packages/showcase/src/types/**" + reason: "Showcase DB is for testing/demo only, no tenant isolation needed" diff --git a/.cursor/rules/coding.mdc b/.cursor/rules/coding.mdc deleted file mode 100644 index ce76455..0000000 --- a/.cursor/rules/coding.mdc +++ /dev/null @@ -1,58 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# General Coding rules - -- Before each step, pull/fetch the repo from remote. -- Default branch is "develop". -- Always follow the instructions in .cursor/rules -- Make sure you understand docs/planning.md and docs/project_plan.md -- Before starting coding, always present a plan for approval. -- Tasks are in docs/project_plan.md -- Each task will be implemented via a feature branch and a PR to the develop branch. - -# Recipe for implementing tasks -Always follow the following recipe for implementing tasks: -1. Task planning: - 1. Only start implementing a task after the user has explicitly told so. - 2. Before starting coding, create a task planning document (.md) in the docs/task-planning folder. - 3. The task planning document contains a table with three columns: tasks description, DoD (Definition of Done) and Status. Status is open, working, checking, review, complete. Initially all tasks are "open". - - Open = the task hasn't been worked on. - - Working = the task is currently being implemented. - - Checking = the DoD of the tasks are being checked. - - Review = the user is reviewing the result. - - complete = the user has approved the result and the task is complete. - 4. After creating the task planning document, let the user review the doc. -3. Task implementation: - 1. Do this only after the user has approved the task planning document. - 2. Implement each task in the task-planning document one after the other. - 3. After implementing a task, check the criteria for DoD and report the result to the user. - 4. If DoD is not met, fix the problem until DoD is met. - 5. Before and after each task, update each task in the table with the right status. - 6. After finishing implementing, perform all necessary tests and check if the DoD for this task is met. - 7. Report on test results and DoD criteria and ask user if a PR should be submitted. - 8. If the user approves submitting a PR, and before PR is submitted, the task is marked with "PR" in docs/project_plan.md. -4. PR submission: - 1. Do this only after the user approves submitting a PR. - 2. Create a temporary markdown file in /tmp with the PR description. - 3. Submit the PR, using the temporary markdown file. - 4. After PR ist submitted, report to the user and wait for manual instructions. - 5. The user will then review and merge the PR or ask for updates. -5. Post-PR cleanup: - 1. Do this after the user confirms that the PR has been successfully merged. - 2. Checkout develop and do `git pull` to update the branch with the merged PR. - 3. After task is completed (PR merged), the task is marked with a checkmark in docs/project_plan.md. This change does not warrant a separate PR, it will be included in the next PR. Just do git add for the file, don't push it. Delete the feature branch locally and remotely. - -# Instructions to handle error situations: -- CI Pipeline fails: - 1. List the last github actions and find out which actions failed. - 2. Fetch the git actions log with `gh run view --log | cat` - 3. Analyze the log and make a proposal how to fix it. - 4. Let the user review / update the proposal, then execute the reviewed/updated proposal. -- Linter errors: - 1. Run `pnpm run lint` - 2. Find the linter errors - 3. Fix - \ No newline at end of file diff --git a/.cursor/rules/components.mdc b/.cursor/rules/components.mdc index 9d79431..0f4d581 100644 --- a/.cursor/rules/components.mdc +++ b/.cursor/rules/components.mdc @@ -3,9 +3,9 @@ description: Component implementation globs: alwaysApply: false --- -# Implementation Instructions – UI‑Kit Components +# Implementation Instructions – UI-Kit Components -These rules are **mandatory** for every developer or AI agent implementing components, layouts, hooks or utilities inside the `ui-kit` package. They guarantee a coherent, theme‑friendly codebase and align with the chapters in *planning.md* and the DoD in *project\_plan.md*. +These rules are **mandatory** for every developer or AI agent implementing components, layouts, hooks or utilities inside the `ui-kit` package. --- @@ -14,16 +14,18 @@ These rules are **mandatory** for every developer or AI agent implementing compo ``` ui-kit/src/ components/ - primitives/ # unconstrained building blocks (Button, Input…) + primitives/ # building blocks (Button, Input…) form/ data-display/ feedback/ navigation/ + overlay/ # Modal, ConfirmModal, Drawer layout/ # page shells & navigation wrappers hooks/ # non-UI hooks (useDebounce, useBreakpoint…) providers/ # React context providers (ThemeProvider…) - theme/ # CSS variables, Tailwind preset, DaisyUI patch + styles/ # globals.css (CSS variables, Tailwind base) + theme/ # TOKENS.md, theme utilities utils/ # helpers (cn, useToast,…) ``` @@ -32,7 +34,7 @@ ui-kit/src/ * `index.ts` (re-export) * `.tsx` - * `.stories.mdx` + * `.stories.tsx` (or `.mdx`) * `.test.tsx` * optional `types.ts` or sub-components. @@ -40,47 +42,33 @@ ui-kit/src/ ## 2 Styling & theme contract -### 2.1 DaisyUI token bridge +### 2.1 CSS variables (Shadcn-native) -`ui-kit/src/theme/theme.css` defines: +All design tokens are defined in `styles/globals.css` using Shadcn conventions: +- `:root` block for light mode (raw HSL channels, e.g. `--primary: 222 47% 31%`) +- `.dark` block for dark mode +- Tailwind config wraps them with `hsl()` (e.g. `primary: "hsl(var(--primary))"`) -```css -:root { - /* DaisyUI HSL tokens → Shadcn variables */ - --primary: hsl(var(--p)); /* primary */ - --background: hsl(var(--b1)); /* base-100 */ - --foreground: hsl(var(--bc)); /* base-content */ - --border: hsl(var(--b2)); /* base-200 */ -} -.dark { - --primary: hsl(var(--pf)); - --background: hsl(var(--n)); - --foreground: hsl(var(--nc)); - --border: hsl(var(--b3)); -} -``` +Standard tokens: `--background`, `--foreground`, `--primary`, `--secondary`, `--accent`, `--destructive`, `--muted`, `--card`, `--popover`, `--border-color`, `--input`, `--ring`. + +Status tokens: `--success`, `--warning`, `--error`, `--info` (each with `-foreground` counterpart). ### 2.2 Tailwind usage rules * Use **utility classes** only inside `ui-kit`. Never emit Tailwind classes from public API. * Prefer semantic wrapper props over styling props. - Example: - - ```tsx - - - - - {({ intent }) => } - - - - -``` +## 4 Storybook stories * Each story **must** include a Canvas demo and an ArgsTable. * Add `parameters = { a11y: { disable: false } }` if you need to tweak axe rules. --- -## 5 Unit + a11y tests - -```tsx -// src/core/Button/Button.test.tsx -import { render, screen } from '@testing-library/react'; -import { Button } from './Button'; - -describe('Button', () => { - it('renders with text', () => { - render(); - expect(screen.getByRole('button', { name: 'Foo' })).toBeInTheDocument(); - }); -}); -``` +## 5 Unit + a11y tests * All tests go in `__tests__` or `.test.tsx`. * Coverage target per sprint (see roadmap). @@ -166,18 +123,16 @@ describe('Button', () => { ## 7 Layouts -Layout components extend `MainLayout` via props: `collapsed`, `fixed`, `dense`. - -* Use CSS grid: `grid-cols-[200px_1fr]` for default, `grid-cols-[1fr]` when collapsed. +* Use CSS grid for sidebar layouts. * TopBar height fixed at `h-14`. -* Breadcrumb slot uses `` pattern for flexibility. +* Breadcrumb slot pattern for flexibility. --- -## 8 Theme tokens & design‑token docs +## 8 Theme tokens & design-token docs * Update `theme/TOKENS.md` whenever adding a new CSS var. -* Run CI step `tokens-check` (script provided in Sprint 1.5) – it fails if variable doc missing. +* Run CI step `tokens-check` – it fails if variable doc missing. --- @@ -193,11 +148,9 @@ Layout components extend `MainLayout` via props: `collapsed`, `fixed`, `dense`. ### Checklist for new component PRs 1. Component & wrapper written under correct folder. -2. Tailwind classes refer to CSS vars / DaisyUI tokens. -3. Storybook MDX with ArgsTable. -4. Vitest unit test + axe‑core pass. +2. Tailwind classes use semantic CSS variable tokens (no hardcoded colours). +3. Storybook story with ArgsTable. +4. Vitest unit test + axe-core pass. 5. Added export to `index.ts`. 6. Changeset file committed. 7. PR passes lint, test, build in CI. - -> PR reviewers should tick all checklist items before merge. diff --git a/.cursor/rules/state_mgmt_rules.mdc b/.cursor/rules/state_mgmt_rules.mdc index 46cf484..d191a16 100644 --- a/.cursor/rules/state_mgmt_rules.mdc +++ b/.cursor/rules/state_mgmt_rules.mdc @@ -1,3 +1,8 @@ +--- +description: State management patterns for the ui-kit and consuming apps +globs: +alwaysApply: true +--- # Cursor Rule File – State Management - **Local UI state:** `useState`, `useReducer` only; no derived data in state. @@ -5,4 +10,4 @@ - **Remote data:** TanStack Query; disable refetchOnWindowFocus if not needed. - No direct fetch in components; use query hooks. - Forms handled by React Hook Form + Zod; no uncontrolled input hacks. -- ESLint custom rule enforces named effects & cleanup. \ No newline at end of file +- ESLint custom rule enforces named effects & cleanup. diff --git a/.cursor/rules/styling_rules.mdc b/.cursor/rules/styling_rules.mdc index afd5353..1ac9a0d 100644 --- a/.cursor/rules/styling_rules.mdc +++ b/.cursor/rules/styling_rules.mdc @@ -1,14 +1,16 @@ --- -description: +description: Tailwind and Shadcn styling conventions for the ui-kit globs: alwaysApply: true --- -# Cursor Rule File – Tailwind, Shadcn, DaisyUI +# Cursor Rule File – Tailwind & Shadcn Styling - Tailwind classes **only inside ui-kit**; no classes in consuming apps. - Always use shadcn ui components with the shadcn CLI (npx shadcn@latest ) - Never override Shadcn component CSS directly; extend via wrapper props. -- Colours/spacings via CSS vars mapped to DaisyUI tokens. -- Use `@apply` sparingly inside `theme.css`, never in JSX. -- Dark-mode via `class="dark"`; avoid media mode. -- No custom global CSS unless a new token/utility is required. \ No newline at end of file +- Colours and spacings via CSS variables defined in `globals.css` (`:root` and `.dark` selectors). +- Status colours use semantic tokens: `--success`, `--warning`, `--error`, `--info` (with `-foreground` counterparts). +- Use `@apply` sparingly inside `globals.css`, never in JSX. +- Dark mode via `class="dark"` on ``; avoid media-query mode. +- No custom global CSS unless a new token/utility is required. +- No DaisyUI classes or tokens — the project uses Shadcn-native theming only. diff --git a/.cursor/skills/add-component/SKILL.md b/.cursor/skills/add-component/SKILL.md new file mode 100644 index 0000000..334080f --- /dev/null +++ b/.cursor/skills/add-component/SKILL.md @@ -0,0 +1,53 @@ +--- +name: add-component +description: Add a new UI component to the ui-kit package following project conventions. Use when creating a new component, wrapping a Shadcn primitive, or adding a feedback/layout/form component. +--- + +# Add Component to UI Kit + +## Directory Structure + +Create under `packages/ui-kit/src/components///`: + +``` +ComponentName/ +├── index.ts # Re-exports +├── ComponentName.tsx # Implementation +├── ComponentName.test.tsx # Tests +└── ComponentName.stories.tsx # (optional) Storybook story +``` + +Categories: `primitives`, `form`, `feedback`, `data-display`, `layout`, `ui`, `brand`. + +## Implementation Checklist + +1. **Create component file** (`ComponentName.tsx`): + + - Named export only (no default exports) + - Export prop interface with JSDoc + - Use `cn()` from `../../../lib/utils` for class merging + - Use Shadcn CSS variables (e.g. `bg-primary`, `text-foreground`) -- never hardcoded colors + - Wrap Shadcn primitives from `../../ui/` when applicable + +2. **Create barrel export** (`index.ts`): + + ```typescript + export { ComponentName } from "./ComponentName"; + export type { ComponentNameProps } from "./ComponentName"; + ``` + +3. **Register in parent barrel** -- add `export * from './ComponentName';` to the category's `index.ts` + +4. **Write tests** (`ComponentName.test.tsx`): + + - Render test, prop tests, accessibility check with axe-core if applicable + - Use `vitest` + `@testing-library/react` + +5. **Verify**: Run `pnpm lint && pnpm test && pnpm build` from workspace root + +## Key Conventions + +- No `import React from 'react'` -- use `import * as React from 'react'` only if needed +- Colors via CSS variables only: `bg-background`, `text-muted-foreground`, `border-input` +- No DaisyUI classes -- this project uses Shadcn-native theming +- Tailwind classes only inside ui-kit, never exposed via public API diff --git a/.cursor/skills/add-form-field/SKILL.md b/.cursor/skills/add-form-field/SKILL.md new file mode 100644 index 0000000..d79e28e --- /dev/null +++ b/.cursor/skills/add-form-field/SKILL.md @@ -0,0 +1,71 @@ +--- +name: add-form-field +description: Add a new form field component integrated with React Hook Form and Zod validation. Use when creating form fields like TextField, SelectField, DateField, or any new validated input. +--- + +# Add Form Field to UI Kit + +## Architecture + +Form fields follow a two-layer pattern: + +1. **Primitive** in `components/primitives/` -- standalone input (no form integration) +2. **Field** in `components/form/` -- wraps primitive with `FieldWrapper` for React Hook Form + +## Steps + +### 1. Create or verify the primitive exists + +Check `packages/ui-kit/src/components/primitives/` for existing primitives. +If missing, create one following the `add-component` skill. + +### 2. Create the Field component + +File: `packages/ui-kit/src/components/form/Field.tsx` + +```typescript +import { FieldValues, Path } from "react-hook-form"; +import { FieldWrapper } from "./FieldWrapper"; +import type { FieldWrapperProps } from "./FieldWrapper"; +import { MyPrimitive } from "../primitives/MyPrimitive"; + +export type MyFieldProps = + Omit & + Omit, "render"> & { + name: Path; + }; + +export function MyField({ + name, label, required, description, ...props +}: MyFieldProps) { + return ( + ( + + )} + /> + ); +} +``` + +### 3. Export from form barrel + +Add to `packages/ui-kit/src/components/form/index.ts`. + +### 4. Write tests + +Test: renders label, validates with Zod schema, shows error on invalid input. + +### 5. Verify + +Run `pnpm lint && pnpm test && pnpm build`. diff --git a/.cursor/skills/fix-compliance/SKILL.md b/.cursor/skills/fix-compliance/SKILL.md new file mode 100644 index 0000000..46d8c1b --- /dev/null +++ b/.cursor/skills/fix-compliance/SKILL.md @@ -0,0 +1,59 @@ +--- +name: fix-compliance +description: Fix compliance findings reported by the automated compliance checker. Use when addressing CON-* rule violations in the compliance report, or when the user mentions compliance issues. +--- + +# Fix Compliance Findings + +## Report Location + +The compliance report is at `.compliance/report.md`. +Configuration is at `.compliance/config.yaml`. + +## Workflow + +1. **Read the report**: Check `.compliance/report.md` for current findings +2. **Identify the rule**: Each finding has a rule ID (e.g. `CON-PFM-009`) +3. **Determine action**: + - **Fix**: Update the code to comply with the rule + - **Suppress**: Add to `suppressions` in `.compliance/config.yaml` with justification + +## Common Rules + +| Rule | Domain | What it checks | +| ------------- | -------- | --------------------------------------------------------------- | +| `CON-DVO-002` | DevOps | Database role separation, MFA for admin | +| `CON-PFM-001` | Platform | Tenant isolation in DB queries | +| `CON-PFM-009` | Platform | Standard API response format `{ success, data, correlationId }` | +| `CON-PFM-010` | Platform | Third-party dependency approval documentation | + +## Suppression Format + +In `.compliance/config.yaml`: + +```yaml +suppressions: + - rule: CON-DVO-002 + paths: + - "packages/showcase/src/database/**" + reason: "Showcase DB is for testing/demo only" +``` + +## API Response Format (CON-PFM-009) + +Standard format: + +```typescript +interface ApiResponse { + success: true; + data: T; + correlationId: string; +} +``` + +Use `pageSize` (not `limit`), `sort`/`order` (not `sortBy`/`sortOrder`). + +## Dependency Approval (CON-PFM-010) + +Maintain the register at `docs/dependency-approval.md`. +Add new dependencies with version, license, purpose, and "Approved" status. diff --git a/.cursor/skills/run-tests/SKILL.md b/.cursor/skills/run-tests/SKILL.md new file mode 100644 index 0000000..9710002 --- /dev/null +++ b/.cursor/skills/run-tests/SKILL.md @@ -0,0 +1,49 @@ +--- +name: run-tests +description: Run the project test suite, lint checks, and build verification. Use when the user asks to test, verify, or check if everything works after changes. +--- + +# Run Tests + +## Quick Commands + +From workspace root (`/workspace`): + +```bash +pnpm lint # ESLint across all packages +pnpm test # Vitest unit tests across all packages +pnpm build # TypeScript + Vite build for ui-kit and showcase +``` + +## Full Verification (before commit/PR) + +```bash +pnpm lint && pnpm test && pnpm build +``` + +## Package-Specific + +```bash +pnpm --filter @etherisc/ui-kit test # ui-kit tests only +pnpm --filter @etherisc/ui-kit build # ui-kit build only +pnpm --filter showcase test # showcase tests only +``` + +## Test Framework + +- **Runner**: Vitest v3 +- **DOM**: jsdom environment +- **Assertions**: `@testing-library/react`, `vitest` expect +- **Accessibility**: axe-core via `@axe-core/react` or `vitest-axe` + +## Common Issues + +- **Lint warnings about `react-refresh/only-export-components`**: Pre-existing, safe to ignore +- **Peer dependency warnings**: Known for `react-day-picker` (expects React 18), safe to ignore +- **Build chunk size warning**: Showcase bundle exceeds 500KB, expected for demo app + +## After Fixing Issues + +1. Re-run the failing command to confirm the fix +2. Run the full verification suite before committing +3. Check `pnpm lint` output for 0 errors (warnings are acceptable) diff --git a/.cursor/skills/update-theme/SKILL.md b/.cursor/skills/update-theme/SKILL.md new file mode 100644 index 0000000..b79c319 --- /dev/null +++ b/.cursor/skills/update-theme/SKILL.md @@ -0,0 +1,43 @@ +--- +name: update-theme +description: Update the design token theme or apply a community Shadcn theme. Use when changing colors, adding CSS variables, applying a tweakcn theme, or modifying light/dark mode tokens. +--- + +# Update Theme + +## Architecture + +Single source of truth: `packages/ui-kit/src/styles/globals.css` + +- `:root` block = light mode tokens +- `.dark` block = dark mode tokens +- CSS variables use raw HSL channels: `--primary: 217.2 91.2% 59.8%;` +- `tailwind.config.js` wraps them: `primary: "hsl(var(--primary))"` + +## Applying a Community Theme (e.g. from tweakcn.com) + +1. Export the theme in **HSL** format (Tailwind v3 mode) +2. Replace the `:root` and `.dark` variable blocks in `globals.css` +3. Keep custom tokens that aren't in standard Shadcn: `--success`, `--warning`, `--error`, `--info`, `--shadow-*`, `--chart-*` +4. Update `TOKENS.md` if values changed + +## Adding a New Token + +1. Add the variable to both `:root` and `.dark` in `globals.css` +2. Add the Tailwind mapping in `tailwind.config.js` under `theme.extend.colors` +3. Document in `packages/ui-kit/src/theme/TOKENS.md` +4. Run `pnpm build` to verify + +## Standard Token Names + +Core: `--background`, `--foreground`, `--card`, `--popover`, `--primary`, `--secondary`, `--muted`, `--accent`, `--destructive` + +Status: `--success`, `--warning`, `--error`, `--info` (each with `-foreground`) + +UI: `--border`, `--input`, `--ring`, `--radius` + +All foreground tokens follow pattern: `---foreground` + +## Dark Mode + +Dark mode uses `class="dark"` on ``. The `useTheme` hook in `src/hooks/useTheme.ts` handles toggling. Never use `data-theme` attributes. diff --git a/AGENTS.md b/AGENTS.md index 1ae34fc..1c24b8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,35 +5,39 @@ These rules apply to the entire repository unless a subdirectory defines its own `AGENTS.md`. ## Required Checks + - Run `pnpm lint`, `pnpm test` and `pnpm build` before each commit. -- If new CSS variables are introduced, run the `tokens-check` script (see Sprint 1.5). +- If new CSS variables are introduced, run the `tokens-check` script. ## Git Workflow + - Follow **GitFlow**. Work on feature branches, open PRs, and never commit -directly to `main`. + directly to `main` or `develop`. - Commits and PR titles use **Conventional Commit** format. - Reference the relevant task ID from `docs/project_plan.md` in the PR description. CI must pass before merge. ## Component and Layout Implementation + - Implement all components under `packages/ui-kit/src` as described in `.cursor/rules/components.mdc` and the planning documents. - One component = one directory containing `index.ts`, `.tsx`, - `.test.tsx` and `.stories.mdx`. + `.test.tsx` and `.stories.tsx`. - Use wrappers around Shadcn UI primitives and expose your own prop API. - No default exports. Export from `src/index.ts` only. - Tailwind classes may be used **only inside ui-kit**; never expose raw classes via the public API. - - Colors and spacing use CSS variables mapped to DaisyUI tokens via - `theme/theme.css`. Avoid inline colors. + - Colors and spacing use CSS variables defined in `styles/globals.css`. + Use Shadcn semantic tokens (`bg-primary`, `text-foreground`, etc.). - Storybook stories include a Canvas demo and ArgsTable. - Provide unit tests with Vitest and axe-core accessibility checks. - Update `theme/TOKENS.md` when adding new CSS variables. - Add a Changeset entry for every public change. ## Styling, State and TypeScript Guidelines -- Apply the Tailwind, styling and DaisyUI rules from - `.cursor/rules/styling_rules.mdc` and `.cursor/rules/components.mdc`. + +- Apply the Tailwind and Shadcn rules from `.cursor/rules/styling_rules.mdc` + and `.cursor/rules/components.mdc`. - Local UI state uses `useState`/`useReducer`. Global session state uses **Zustand**. Remote data fetching uses **TanStack Query**. Forms use **React Hook Form** + **Zod**. No direct fetch calls in components. @@ -43,13 +47,14 @@ directly to `main`. type aliases and generics, and use `import type` for type-only imports. ## React Router and Vite + - Use React Router v7 with loaders and actions where appropriate. Routes should rely on `` for nesting. - Configure imports via the `@` alias in `vite.config.ts` and avoid deep relative paths. Keep `index.html` minimal and let Vite inject scripts. ## Documentation + - The `docs/` folder contains the planning documents for the project. Keep these files up to date when architectural or workflow changes occur. Storybook and design-token documentation are mandatory for new components. - diff --git a/docs/dependency-approval.md b/docs/dependency-approval.md new file mode 100644 index 0000000..3d680ea --- /dev/null +++ b/docs/dependency-approval.md @@ -0,0 +1,71 @@ +# Third-Party Dependency Approval Register + +All third-party dependencies used in this project have been reviewed and approved +for inclusion. This document serves as the approval record required by CON-PFM-010. + +## Approval Criteria + +Each dependency is evaluated against: + +- **Security**: No known critical vulnerabilities (checked via `pnpm audit`) +- **License**: MIT, Apache-2.0, or ISC (permissive, SaaS-compatible) +- **Maintenance**: Active maintenance, recent releases within 12 months +- **Quality**: Adequate test coverage, TypeScript support preferred + +## UI Kit (`packages/ui-kit`) + +### Runtime Dependencies (peerDependencies) + +| Package | Version | License | Purpose | Status | +| --------- | ------- | ------- | ------------- | -------- | +| react | ^19.1.0 | MIT | UI framework | Approved | +| react-dom | ^19.1.0 | MIT | DOM rendering | Approved | + +### Runtime Dependencies (dependencies) + +| Package | Version | License | Purpose | Status | +| ------------------------ | -------- | ----------------------- | -------------------------------------- | -------- | +| @radix-ui/\* | various | MIT | Accessible UI primitives (Shadcn base) | Approved | +| @tanstack/react-table | ^8.21.3 | MIT | Headless table logic | Approved | +| lucide-react | ^0.487.0 | ISC | Icon library | Approved | +| nanoid | ^5.1.5 | MIT | ID generation | Approved | +| class-variance-authority | ^0.7.1 | Apache-2.0 | Variant class builder | Approved | +| clsx | ^2.1.1 | MIT | Class name utility | Approved | +| tailwind-merge | ^3.0.2 | MIT | Tailwind class merging | Approved | +| tailwindcss-animate | ^1.0.7 | MIT | Animation utilities | Approved | +| zustand | ^5.0.5 | MIT | State management | Approved | +| react-hook-form | ^7.56.4 | MIT | Form management | Approved | +| @hookform/resolvers | ^5.0.1 | MIT | Form validation resolvers | Approved | +| zod | ^3.25.7 | MIT | Schema validation | Approved | +| date-fns | ^4.1.0 | MIT | Date utilities | Approved | +| react-day-picker | ^8.10.1 | MIT | Date picker component | Approved | +| i18next | ^25.1.3 | MIT | Internationalization | Approved | +| react-i18next | ^15.5.2 | MIT | React i18n bindings | Approved | +| @codemirror/\* | various | MIT | Code editor | Approved | +| marked | ^15.0.12 | MIT | Markdown parser | Approved | +| dompurify | ^3.2.6 | (Apache-2.0 OR MPL-2.0) | HTML sanitizer | Approved | + +### Dev Dependencies + +| Package | Version | License | Purpose | Status | +| ----------- | ------- | ---------- | ----------------------- | -------- | +| tailwindcss | ^3.4.17 | MIT | CSS framework | Approved | +| typescript | ^5.8.3 | Apache-2.0 | Type checking | Approved | +| vitest | ^3.1.3 | MIT | Test runner | Approved | +| storybook | ^8.6.14 | MIT | Component documentation | Approved | +| tsup | ^8.5.0 | MIT | Build tool | Approved | +| eslint | ^9.27.0 | MIT | Linter | Approved | + +## Showcase (`packages/showcase`) + +| Package | Version | License | Purpose | Status | +| ---------------- | ------- | ------- | ------------------------------- | -------- | +| @faker-js/faker | ^9.8.0 | MIT | Mock data generation (dev only) | Approved | +| bcryptjs | ^3.0.2 | MIT | Password hashing (demo) | Approved | +| sql.js | ^1.12.0 | MIT | In-browser SQLite (demo) | Approved | +| react-router-dom | ^7.6.0 | MIT | Client routing | Approved | + +## Review Schedule + +Dependencies are reviewed quarterly or when major version updates are available. +Last full review: 2026-02-20. diff --git a/package.json b/package.json index 9a67670..943afdc 100644 --- a/package.json +++ b/package.json @@ -28,14 +28,13 @@ "@storybook/react": "8.6.14", "@storybook/react-vite": "8.6.14", "@storybook/types": "8.6.14", + "@tailwindcss/vite": "^4.2.0", "@types/node": "^22.15.19", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^4.2.1", "@vitest/coverage-c8": "^0.33.0", - "autoprefixer": "^10.4.18", "commitlint": "^19.8.1", - "daisyui": "^5.0.35", "eslint": "^9.27.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", @@ -45,6 +44,7 @@ "prettier": "^3.5.3", "tailwindcss": "^3.4.1", "tsup": "^8.5.0", + "tw-animate-css": "^1.4.0", "typescript": "^5.8.3", "typescript-eslint": "^8.32.1", "vite": "^5.1.4", diff --git a/packages/showcase/package.json b/packages/showcase/package.json index 5ea0c14..1b07457 100644 --- a/packages/showcase/package.json +++ b/packages/showcase/package.json @@ -13,9 +13,9 @@ "seed": "tsx scripts/seed.ts" }, "dependencies": { + "@etherisc/ui-kit": "workspace:*", "@faker-js/faker": "^9.8.0", "@hookform/resolvers": "^3.6.1", - "@etherisc/ui-kit": "workspace:*", "bcryptjs": "^3.0.2", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -29,6 +29,7 @@ "devDependencies": { "@eslint/js": "^9.27.0", "@playwright/test": "^1.52.0", + "@tailwindcss/vite": "^4.2.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@types/bcryptjs": "^3.0.0", @@ -36,17 +37,15 @@ "@types/react-dom": "^19.1.5", "@types/sql.js": "^1.4.9", "@vitejs/plugin-react": "^4.4.1", - "autoprefixer": "^10.4.21", - "daisyui": "^5.0.35", "eslint": "^9.27.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "jsdom": "^26.1.0", "postcss": "^8.5.3", - "tailwindcss": "^3.4.17", - "tailwindcss-animate": "^1.0.7", + "tailwindcss": "^4.2.0", "tsx": "^4.19.4", + "tw-animate-css": "^1.4.0", "typescript": "^5.8.3", "typescript-eslint": "^8.32.1", "vite": "^5.4.19", diff --git a/packages/showcase/postcss.config.js b/packages/showcase/postcss.config.js deleted file mode 100644 index 1b364dd..0000000 --- a/packages/showcase/postcss.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import tailwindcss from 'tailwindcss'; -import autoprefixer from 'autoprefixer'; - -export default { - plugins: [ - tailwindcss, - autoprefixer, - ], -}; \ No newline at end of file diff --git a/packages/showcase/src/data/mockCustomers.ts b/packages/showcase/src/data/mockCustomers.ts index 28cb2e3..c28dbfe 100644 --- a/packages/showcase/src/data/mockCustomers.ts +++ b/packages/showcase/src/data/mockCustomers.ts @@ -50,17 +50,21 @@ export const mockCustomers = generateMockCustomers(); // Mock queries interface to match the database queries export const MockCustomerQueries = { - async getCustomers(options: { page: number; limit: number }) { - const { page, limit } = options; - const offset = (page - 1) * limit; - const paginatedCustomers = mockCustomers.slice(offset, offset + limit); + async getCustomers(options: { page: number; pageSize: number }) { + const { page, pageSize } = options; + const offset = (page - 1) * pageSize; + const paginatedCustomers = mockCustomers.slice(offset, offset + pageSize); return { - customers: paginatedCustomers, - total: mockCustomers.length, - page, - limit, - totalPages: Math.ceil(mockCustomers.length / limit), + success: true as const, + data: { + customers: paginatedCustomers, + total: mockCustomers.length, + page, + pageSize, + totalPages: Math.ceil(mockCustomers.length / pageSize), + }, + correlationId: crypto.randomUUID(), }; }, diff --git a/packages/showcase/src/database/queries.ts b/packages/showcase/src/database/queries.ts index d59b08e..715ad75 100644 --- a/packages/showcase/src/database/queries.ts +++ b/packages/showcase/src/database/queries.ts @@ -20,13 +20,13 @@ export class CustomerQueries { * Get all customers with pagination */ static async getCustomers(options: PaginationOptions): Promise> { - const { page, limit, sortBy = 'id', sortOrder = 'asc' } = options; - const offset = (page - 1) * limit; + const { page, pageSize, sort = 'id', order = 'asc' } = options; + const offset = (page - 1) * pageSize; // Validate sort column to prevent SQL injection const allowedSortColumns = ['id', 'first_name', 'last_name', 'email', 'company', 'status', 'created_at']; - const safeSortBy = allowedSortColumns.includes(sortBy) ? sortBy : 'id'; - const safeSortOrder = sortOrder === 'desc' ? 'DESC' : 'ASC'; + const safeSort = allowedSortColumns.includes(sort) ? sort : 'id'; + const safeOrder = order === 'desc' ? 'DESC' : 'ASC'; // Get total count const countResult = await getQuery('SELECT COUNT(*) as count FROM customers'); @@ -35,16 +35,16 @@ export class CustomerQueries { // Get paginated data const data = await getAllQuery(` SELECT * FROM customers - ORDER BY ${safeSortBy} ${safeSortOrder} + ORDER BY ${safeSort} ${safeOrder} LIMIT ? OFFSET ? - `, [limit, offset]) as Customer[]; + `, [pageSize, offset]) as Customer[]; return { data, total, page, - limit, - totalPages: Math.ceil(total / limit), + pageSize, + totalPages: Math.ceil(total / pageSize), }; } @@ -125,8 +125,8 @@ export class CustomerQueries { * Search customers by name or email */ static async searchCustomers(query: string, options: PaginationOptions): Promise> { - const { page, limit } = options; - const offset = (page - 1) * limit; + const { page, pageSize } = options; + const offset = (page - 1) * pageSize; const searchTerm = `%${query}%`; // Get total count @@ -142,14 +142,14 @@ export class CustomerQueries { WHERE first_name LIKE ? OR last_name LIKE ? OR email LIKE ? OR company LIKE ? ORDER BY first_name ASC LIMIT ? OFFSET ? - `, [searchTerm, searchTerm, searchTerm, searchTerm, limit, offset]) as Customer[]; + `, [searchTerm, searchTerm, searchTerm, searchTerm, pageSize, offset]) as Customer[]; return { data, total, page, - limit, - totalPages: Math.ceil(total / limit), + pageSize, + totalPages: Math.ceil(total / pageSize), }; } } diff --git a/packages/showcase/src/database/types.ts b/packages/showcase/src/database/types.ts index d35bd1e..18bcd71 100644 --- a/packages/showcase/src/database/types.ts +++ b/packages/showcase/src/database/types.ts @@ -69,19 +69,26 @@ export interface UpdateCustomerInput { // Pagination types export interface PaginationOptions { page: number; - limit: number; - sortBy?: string; - sortOrder?: 'asc' | 'desc'; + pageSize: number; + sort?: string; + order?: 'asc' | 'desc'; } export interface PaginatedResult { data: T[]; total: number; page: number; - limit: number; + pageSize: number; totalPages: number; } +// Standard API response envelope +export interface ApiResponse { + success: true; + data: T; + correlationId: string; +} + // Authentication types export interface LoginCredentials { username: string; diff --git a/packages/showcase/src/index.css b/packages/showcase/src/index.css index d7d3e8e..00c4c49 100644 --- a/packages/showcase/src/index.css +++ b/packages/showcase/src/index.css @@ -1,18 +1,14 @@ -@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"); -@tailwind base; -@tailwind components; -@tailwind utilities; +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap') +layer(base); +@import 'tailwindcss'; +@import 'tw-animate-css'; -/* Force DaisyUI component generation - ensure these classes are available */ -/* .btn .btn-primary .btn-secondary .card .input .modal .navbar .drawer */ - -/* Nexadash-inspired design tokens */ :root { /* Typography */ - --font-plus-jakarta: "Plus Jakarta Sans", sans-serif; + --font-plus-jakarta: 'Plus Jakarta Sans', sans-serif; - /* Color palette inspired by nexadash */ - --color-primary: #335cff; + /* Color palette */ + --color-primary-raw: #335cff; --color-primary-hover: #2a4dd7; --color-black: #171718; --color-white: #ffffff; @@ -25,64 +21,53 @@ --color-gray-500: #b9bec6; --color-gray-600: #9ca3af; --color-gray-700: #6b7280; - --color-gray: #525866; + --color-gray-raw: #525866; /* Status colors */ - --color-success: #22c55e; + --color-success-raw: #22c55e; --color-success-light: #dcfce7; - --color-warning: #eab308; + --color-warning-raw: #eab308; --color-danger: #ef4444; --color-danger-light: #fee2e2; /* Theme colors */ - --color-light-theme: #f4f7ff; - --color-light-orange: #ffedd5; - --color-light-blue: #e0f2fe; - --color-light-purple: #f3e8ff; + --color-light-theme-raw: #f4f7ff; + --color-light-orange-raw: #ffedd5; + --color-light-blue-raw: #e0f2fe; + --color-light-purple-raw: #f3e8ff; /* Shadows */ - --shadow-3xl: - 0 1px 2px 0 rgba(95, 74, 46, 0.08), 0 0 0 1px rgba(227, 225, 222, 0.4); + --shadow-3xl: 0 1px 2px 0 rgba(95, 74, 46, 0.08), 0 0 0 1px rgba(227, 225, 222, 0.4); --shadow-sm: 0 1px 2px 0 rgba(113, 116, 152, 0.1); - /* Shadcn/UI variables mapped to nexadash tokens */ - --primary: var(--color-primary); + /* Shadcn semantic tokens */ + --primary: var(--color-primary-raw); --primary-foreground: var(--color-white); - --secondary: var(--color-gray-200); - --secondary-foreground: var(--color-gray); - - --accent: var(--color-light-theme); - --accent-foreground: var(--color-primary); - + --secondary-foreground: var(--color-gray-raw); + --muted: var(--color-gray-200); + --muted-foreground: var(--color-gray-raw); + --accent: var(--color-light-theme-raw); + --accent-foreground: var(--color-primary-raw); --destructive: var(--color-danger); --destructive-foreground: var(--color-white); - - /* UI elements */ --background: var(--color-white); --foreground: var(--color-black); --card: var(--color-white); --card-foreground: var(--color-black); --popover: var(--color-white); --popover-foreground: var(--color-black); - - /* Borders and inputs */ --border: var(--color-gray-300); --input: var(--color-gray-300); - --ring: var(--color-primary); - - /* Status colors for UI */ - --success: var(--color-success); + --ring: var(--color-primary-raw); + --success: var(--color-success-raw); --success-foreground: var(--color-white); - --warning: var(--color-warning); + --warning: var(--color-warning-raw); --warning-foreground: var(--color-white); - - /* Radius */ --radius: 0.5rem; } .dark { - /* Dark mode adjustments */ --background: var(--color-black); --foreground: var(--color-white); --card: #1a1a1b; @@ -93,41 +78,95 @@ --input: #2a2a2b; } +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-gray: var(--color-gray-raw); + --color-light-theme: var(--color-light-theme-raw); + --color-light-orange: var(--color-light-orange-raw); + --color-light-blue: var(--color-light-blue-raw); + --color-light-purple: var(--color-light-purple-raw); + --font-family-sans: var(--font-plus-jakarta); + --font-family-plus-jakarta: var(--font-plus-jakarta); + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + --shadow-sm: var(--shadow-sm); + --shadow-3xl: var(--shadow-3xl); +} + @layer base { - * { - @apply border-border; + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-border); } - body { - @apply bg-background text-foreground; + background-color: var(--color-background); + color: var(--color-foreground); font-family: var(--font-plus-jakarta); } - button { - @apply cursor-pointer; + cursor: pointer; } } -/* Nexadash-inspired component styles */ -@layer components { - .nexadash-card { - @apply bg-white rounded-lg; - box-shadow: var(--shadow-3xl); - } - - .nexadash-button-primary { - @apply bg-primary text-white hover:bg-[#2A4DD7] px-4 py-2 rounded-lg font-medium text-sm transition-colors duration-300; - } - - .nexadash-button-outline { - @apply ring-1 ring-inset ring-primary bg-white text-primary hover:bg-light-theme px-4 py-2 rounded-lg font-medium text-sm transition-colors duration-300; - } - - .nexadash-sidebar-nav { - @apply text-gray flex items-center gap-2.5 px-5 py-2.5 text-sm font-medium leading-tight transition hover:text-black; - } - - .nexadash-sidebar-nav.active { - @apply !text-black bg-light-theme; - } +@utility nexadash-card { + background-color: white; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-3xl); +} +@utility nexadash-button-primary { + background-color: var(--color-primary); + color: white; + padding: 0.5rem 1rem; + border-radius: var(--radius-lg); + font-weight: 500; + font-size: 0.875rem; + transition: color 0.3s, background-color 0.3s; +} +@utility nexadash-button-outline { + outline: 1px solid var(--color-primary); + outline-offset: -1px; + background-color: white; + color: var(--color-primary); + padding: 0.5rem 1rem; + border-radius: var(--radius-lg); + font-weight: 500; + font-size: 0.875rem; + transition: color 0.3s, background-color 0.3s; +} +@utility nexadash-sidebar-nav { + color: var(--color-gray); + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.625rem 1.25rem; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.25; + transition: all 0.15s; } diff --git a/packages/showcase/src/main-minimal.tsx b/packages/showcase/src/main-minimal.tsx index 926b263..4946743 100644 --- a/packages/showcase/src/main-minimal.tsx +++ b/packages/showcase/src/main-minimal.tsx @@ -9,7 +9,7 @@ function App() {

Minimal Showcase Test

If you see this, the basic React app works.

CSS should be loading from index.css

- + ); } diff --git a/packages/showcase/src/main.tsx b/packages/showcase/src/main.tsx index c14178a..5ce3a25 100644 --- a/packages/showcase/src/main.tsx +++ b/packages/showcase/src/main.tsx @@ -14,8 +14,8 @@ import { EnhancedAppShellTestPage } from "./pages/EnhancedAppShellTestPage"; import { NotFoundPage } from "./pages/NotFoundPage"; import { ProtectedRoute } from "./components/ProtectedRoute"; -// Set DaisyUI theme on HTML element -document.documentElement.setAttribute("data-theme", "light"); +// Ensure light mode by default (dark mode toggled via .dark class) +document.documentElement.classList.remove("dark"); const router = createBrowserRouter([ { diff --git a/packages/showcase/src/pages/CustomersPage.tsx b/packages/showcase/src/pages/CustomersPage.tsx index 59f4039..a8eef3c 100644 --- a/packages/showcase/src/pages/CustomersPage.tsx +++ b/packages/showcase/src/pages/CustomersPage.tsx @@ -52,9 +52,9 @@ export function CustomersPage() { setLoading(true); const result = await MockCustomerQueries.getCustomers({ page: 1, - limit: 100, + pageSize: 100, }); - const dbCustomers: Customer[] = result.customers.map( + const dbCustomers: Customer[] = result.data.customers.map( (dbCustomer: DBCustomer) => ({ id: dbCustomer.id, name: `${dbCustomer.first_name} ${dbCustomer.last_name}`, diff --git a/packages/showcase/src/pages/DashboardPage.tsx b/packages/showcase/src/pages/DashboardPage.tsx index 57c92c8..0bb0654 100644 --- a/packages/showcase/src/pages/DashboardPage.tsx +++ b/packages/showcase/src/pages/DashboardPage.tsx @@ -85,7 +85,7 @@ export function DashboardPage() { // Logo for the top bar const logo = (
-
+
E
@@ -146,7 +146,7 @@ export function DashboardPage() {
diff --git a/packages/showcase/src/pages/ResetPasswordPage.tsx b/packages/showcase/src/pages/ResetPasswordPage.tsx index 6999db5..b0c3160 100644 --- a/packages/showcase/src/pages/ResetPasswordPage.tsx +++ b/packages/showcase/src/pages/ResetPasswordPage.tsx @@ -80,7 +80,7 @@ export function ResetPasswordPage() { diff --git a/packages/showcase/tailwind.config.js b/packages/showcase/tailwind.config.js deleted file mode 100644 index d405499..0000000 --- a/packages/showcase/tailwind.config.js +++ /dev/null @@ -1,153 +0,0 @@ -import daisyui from "daisyui"; -import tailwindcssAnimate from "tailwindcss-animate"; - -/** @type {import('tailwindcss').Config} */ -export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], - safelist: [ - // Ensure DaisyUI component classes are always generated - // DaisyUI + component-specific blocks - { - pattern: - /^(btn|input|card|modal|drawer|navbar)(-(primary|secondary|accent|neutral|body|title|actions|box|backdrop|content|side))?$/, - }, - // Base and utility background/text/border classes - { - pattern: - /^(bg|text|border)-(base-\d{3}|primary|secondary|accent|neutral)$/, - }, - { - pattern: /^text-(primary|secondary|accent)-foreground$/, - }, - // responsive classes - { - pattern: - /^(flex|inline-flex|grid|col-span|row-span|gap[xy]?|place-(items|content))-/, - variants: ["sm", "md", "lg"], - }, - { - pattern: /^(items|justify|content|self)-/, - variants: ["sm", "md", "lg"], - }, - { - pattern: /^-?m[trblxy]?-/, - variants: ["sm", "md", "lg"], - }, - { - pattern: /^p[trblxy]?-/, - variants: ["sm", "md", "lg"], - }, - { - pattern: /^(w|h|min-w|max-w|min-h|max-h|z)-/, - variants: ["sm", "md", "lg"], - }, - { - pattern: /^(rounded|font|leading|tracking|line-clamp)-/, - variants: ["sm", "md", "lg"], - }, - { - pattern: - /^((border|text|ring|shadow)(-(t|b|l|r|x|y|0|2|4|8|solid|dashed|dotted|none))?)$/, - variants: ["sm", "md", "lg"], - }, - /* - { - pattern: - /^(block|inline|inline-block|hidden|visible|invisible|relative|absolute|fixed|sticky)/, - variants: ["sm", "md", "lg"], - }, - */ - ], - darkMode: ["class", "class"], - theme: { - extend: { - fontFamily: { - "plus-jakarta": ["Plus Jakarta Sans", "sans-serif"], - sans: ["Plus Jakarta Sans", "sans-serif"], - }, - colors: { - border: "var(--border)", - input: "var(--input)", - ring: "var(--ring)", - background: "var(--background)", - foreground: "var(--foreground)", - primary: { - DEFAULT: "var(--primary)", - foreground: "var(--primary-foreground)", - hover: "var(--color-primary-hover)", - }, - secondary: { - DEFAULT: "var(--secondary)", - foreground: "var(--secondary-foreground)", - }, - accent: { - DEFAULT: "var(--accent)", - foreground: "var(--accent-foreground)", - }, - destructive: { - DEFAULT: "var(--destructive)", - foreground: "var(--destructive-foreground)", - }, - card: { - DEFAULT: "var(--card)", - foreground: "var(--card-foreground)", - }, - popover: { - DEFAULT: "var(--popover)", - foreground: "var(--popover-foreground)", - }, - success: { - DEFAULT: "var(--success)", - foreground: "var(--success-foreground)", - light: "var(--color-success-light)", - }, - warning: { - DEFAULT: "var(--warning)", - foreground: "var(--warning-foreground)", - }, - danger: { - DEFAULT: "var(--color-danger)", - light: "var(--color-danger-light)", - }, - // Nexadash color palette - gray: { - DEFAULT: "var(--color-gray)", - 100: "var(--color-gray-100)", - 200: "var(--color-gray-200)", - 300: "var(--color-gray-300)", - 400: "var(--color-gray-400)", - 500: "var(--color-gray-500)", - 600: "var(--color-gray-600)", - 700: "var(--color-gray-700)", - }, - "light-theme": "var(--color-light-theme)", - "light-orange": "var(--color-light-orange)", - "light-blue": "var(--color-light-blue)", - "light-purple": "var(--color-light-purple)", - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - boxShadow: { - sm: "var(--shadow-sm)", - "3xl": "var(--shadow-3xl)", - DEFAULT: "var(--shadow)", - md: "var(--shadow-md)", - lg: "var(--shadow-lg)", - }, - }, - }, - plugins: [daisyui, tailwindcssAnimate], - daisyui: { - themes: ["light", "dark"], - darkTheme: "dark", - base: true, - styled: true, - utils: true, - prefix: "", - logs: false, - themeRoot: ":root", - }, -}; diff --git a/packages/showcase/test-issue-48-fix.html b/packages/showcase/test-issue-48-fix.html index 5101783..7f0af00 100644 --- a/packages/showcase/test-issue-48-fix.html +++ b/packages/showcase/test-issue-48-fix.html @@ -1,5 +1,5 @@ - + @@ -77,13 +77,13 @@

🧪 Test: Button Component Styling

Styled Buttons Using UI Kit CSS Classes:

diff --git a/packages/showcase/test-ui-kit-css.html b/packages/showcase/test-ui-kit-css.html index 6158089..d63e522 100644 --- a/packages/showcase/test-ui-kit-css.html +++ b/packages/showcase/test-ui-kit-css.html @@ -1,5 +1,5 @@ - + @@ -17,7 +17,7 @@

UI Kit CSS Test

Button Test

diff --git a/packages/showcase/vite.config.ts b/packages/showcase/vite.config.ts index 592333c..ad86485 100644 --- a/packages/showcase/vite.config.ts +++ b/packages/showcase/vite.config.ts @@ -1,9 +1,10 @@ import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; import path from "path"; export default defineConfig({ - plugins: [react()], + plugins: [tailwindcss(), react()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), diff --git a/packages/ui-kit/.storybook/main.ts b/packages/ui-kit/.storybook/main.ts index ab9edf0..bf700ad 100644 --- a/packages/ui-kit/.storybook/main.ts +++ b/packages/ui-kit/.storybook/main.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from '@storybook/react-vite' +import tailwindcss from '@tailwindcss/vite' const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], @@ -22,6 +23,7 @@ const config: StorybookConfig = { async viteFinal(config) { return { ...config, + plugins: [...(config.plugins ?? []), tailwindcss()], define: { ...config.define, global: 'window', diff --git a/packages/ui-kit/.storybook/preview.tsx b/packages/ui-kit/.storybook/preview.tsx index 78b4d25..6f43a9c 100644 --- a/packages/ui-kit/.storybook/preview.tsx +++ b/packages/ui-kit/.storybook/preview.tsx @@ -6,10 +6,16 @@ import { initializeTheme } from '../src/theme' import { I18nProvider } from '../src/providers/I18nProvider' import { useTranslation } from 'react-i18next' -// Theme switcher -const ThemeInitializer = ({ children }: { children: React.ReactNode }) => { +const ThemeApplier = ({ children, theme }: { children: React.ReactNode; theme: string }) => { + useEffect(() => { + if (theme === 'dark') { + document.documentElement.classList.add('dark') + } else { + document.documentElement.classList.remove('dark') + } + }, [theme]) + useEffect(() => { - // Initialize theme when component mounts initializeTheme() }, []) @@ -78,13 +84,13 @@ const preview: Preview = { decorators: [ (Story, context) => ( - +
-
+
), ], @@ -101,14 +107,6 @@ const preview: Preview = { ], showName: true, dynamicTitle: true, - onChange: (theme) => { - const isDark = theme === 'dark' - if (isDark) { - document.documentElement.classList.add('dark') - } else { - document.documentElement.classList.remove('dark') - } - } }, }, locale: { diff --git a/packages/ui-kit/components.json b/packages/ui-kit/components.json index 3154a3d..285033d 100644 --- a/packages/ui-kit/components.json +++ b/packages/ui-kit/components.json @@ -4,7 +4,7 @@ "rsc": false, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "", "css": "src/styles/globals.css", "baseColor": "neutral", "cssVariables": true, diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json index 9c4160d..f1d7780 100644 --- a/packages/ui-kit/package.json +++ b/packages/ui-kit/package.json @@ -141,7 +141,7 @@ }, "scripts": { "dev": "vite", - "build": "tsup", + "build": "vite build", "lint": "eslint .", "preview": "vite preview", "test": "vitest run", @@ -183,6 +183,7 @@ "codemirror": "^6.0.1", "date-fns": "^4.1.0", "dompurify": "^3.2.6", + "embla-carousel-react": "^8.6.0", "i18next": "^25.2.1", "i18next-browser-languagedetector": "^8.1.0", "input-otp": "^1.4.2", @@ -190,10 +191,11 @@ "marked": "^15.0.12", "react-day-picker": "8.10.1", "react-i18next": "^15.5.2", + "react-resizable-panels": "^4.6.4", "sonner": "^2.0.5", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7", "tslog": "^4.9.3", + "vaul": "^1.1.2", "zustand": "^5.0.4" }, "size-limit": [ @@ -230,6 +232,7 @@ "@storybook/test-runner": "^0.22.0", "@storybook/theming": "^8.6.14", "@storybook/types": "^8.6.14", + "@tailwindcss/vite": "^4.2.0", "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", @@ -243,12 +246,10 @@ "@types/testing-library__react": "^10.2.0", "@vitejs/plugin-react": "^4.4.1", "@vitest/coverage-v8": "^1.6.1", - "autoprefixer": "^10.4.21", "axe-playwright": "^2.1.0", "concurrently": "^9.1.2", "cypress": "^14.3.3", "cypress-visual-regression": "^5.3.0", - "daisyui": "^5.0.35", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", @@ -260,10 +261,11 @@ "react-dom": "^19.1.0", "size-limit": "^11.2.0", "storybook": "^8.6.14", - "tailwindcss": "^3.4.17", - "tsup": "^8.5.0", + "tailwindcss": "^4.2.0", + "tw-animate-css": "^1.4.0", "typescript-eslint": "^8.32.1", "vite": "^5.4.19", + "vite-plugin-dts": "^4.5.4", "vitest": "^1.6.1", "wait-on": "^8.0.3" } diff --git a/packages/ui-kit/postcss.config.js b/packages/ui-kit/postcss.config.js deleted file mode 100644 index 5fc7f02..0000000 --- a/packages/ui-kit/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/packages/ui-kit/scripts/tokens-check.js b/packages/ui-kit/scripts/tokens-check.js index 6c9144d..b2624f1 100755 --- a/packages/ui-kit/scripts/tokens-check.js +++ b/packages/ui-kit/scripts/tokens-check.js @@ -2,9 +2,9 @@ /** * Design tokens consistency checker - * - * This script ensures that all CSS variables defined in theme.css - * are properly documented in DESIGN_TOKENS.md + * + * Ensures all CSS variables defined in globals.css + * are properly documented in TOKENS.md */ /* global console, process */ @@ -13,59 +13,54 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -// Get current file and directory paths const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// File paths -const themeCssPath = path.join(__dirname, '../src/theme/theme.css'); -const tokensMdPath = path.join(__dirname, '../src/theme/DESIGN_TOKENS.md'); +const globalsCssPath = path.join(__dirname, '../src/styles/globals.css'); +const tokensMdPath = path.join(__dirname, '../src/theme/TOKENS.md'); -// Read the files -const themeCss = fs.readFileSync(themeCssPath, 'utf8'); +const globalsCss = fs.readFileSync(globalsCssPath, 'utf8'); const tokensMd = fs.readFileSync(tokensMdPath, 'utf8'); -// Extract CSS variables from theme.css function extractCssVariables(css) { const regex = /--[\w-]+(?=:)/g; const matches = css.match(regex); - - // Remove duplicates (variables can appear in both :root and .dark) - return [...new Set(matches)]; + const filtered = (matches ?? []).filter(v => + !v.startsWith('--tw-') && + !v.startsWith('--shadow-color') && + !v.startsWith('--shadow-strength') + ); + return [...new Set(filtered)]; } -// Check if variables are documented in DESIGN_TOKENS.md function checkVariablesDocumented(variables, doc) { const undocumented = []; - for (const variable of variables) { const variableRegex = new RegExp(`\`${variable}\``, 'g'); if (!variableRegex.test(doc)) { undocumented.push(variable); } } - return undocumented; } -// Main function function main() { - console.log('🔍 Checking design tokens documentation...'); + console.log('Checking design tokens documentation...'); - const cssVariables = extractCssVariables(themeCss); - console.log(`Found ${cssVariables.length} CSS variables in theme.css`); + const cssVariables = extractCssVariables(globalsCss); + console.log(`Found ${cssVariables.length} CSS variables in globals.css`); const undocumentedVariables = checkVariablesDocumented(cssVariables, tokensMd); if (undocumentedVariables.length === 0) { - console.log('✅ All CSS variables are properly documented in DESIGN_TOKENS.md'); + console.log('All CSS variables are properly documented in TOKENS.md'); process.exit(0); } else { - console.error('❌ The following CSS variables are not documented in DESIGN_TOKENS.md:'); + console.error('The following CSS variables are not documented in TOKENS.md:'); undocumentedVariables.forEach(v => console.error(` - ${v}`)); - console.error(`\nPlease add documentation for these ${undocumentedVariables.length} variables to maintain the design system consistency.`); + console.error(`\nPlease add documentation for these ${undocumentedVariables.length} variables.`); process.exit(1); } } -main(); \ No newline at end of file +main(); diff --git a/packages/ui-kit/src/components/data-display/DataTable/DataTable.tsx b/packages/ui-kit/src/components/data-display/DataTable/DataTable.tsx index d494659..202b7ac 100644 --- a/packages/ui-kit/src/components/data-display/DataTable/DataTable.tsx +++ b/packages/ui-kit/src/components/data-display/DataTable/DataTable.tsx @@ -4,19 +4,26 @@ import { ColumnResizeMode, flexRender, getCoreRowModel, + getFilteredRowModel, getPaginationRowModel, getSortedRowModel, + getExpandedRowModel, SortingState, + ColumnFiltersState, + VisibilityState, + RowSelectionState, + ExpandedState, + ColumnPinningState, Table, useReactTable, PaginationState, Updater, + Row, } from "@tanstack/react-table"; -import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react"; +import { ChevronUp, ChevronDown, ChevronsUpDown, ChevronRight } from "lucide-react"; import { cn } from "../../../utils"; import { DataTablePagination } from "./DataTablePagination"; -// Enhanced pagination configuration interface export interface PaginationConfig { pageSize?: number; showSizeSelector?: boolean; @@ -27,176 +34,136 @@ export interface PaginationConfig { enableJumpToPage?: boolean; } -// Extended meta interface for column definitions interface ColumnMeta { className?: string; } export interface DataTableProps { - /** - * The data to display in the table - */ data: TData[]; - - /** - * The columns configuration for the table - */ columns: ColumnDef[]; - - /** - * Additional class names to apply to the table - */ className?: string; - /** - * The number of rows to display per page - * @default 10 - * @deprecated Use pagination.pageSize instead - */ - pageSize?: number; - - /** - * Whether to enable column resizing - * @default true - */ + /** @default true */ enableResizing?: boolean; - - /** - * The resize mode for the columns - * @default 'onChange' - */ + /** @default 'onChange' */ columnResizeMode?: ColumnResizeMode; - - /** - * Whether to enable sorting - * @default true - */ + /** @default true */ enableSorting?: boolean; + /** @default false */ + enableFiltering?: boolean; + /** @default false */ + enableRowSelection?: boolean | ((row: Row) => boolean); + /** @default false */ + enableExpanding?: boolean; + /** @default false */ + enableColumnPinning?: boolean; - // TanStack Table server-side pagination props - /** - * Enable manual pagination for server-side pagination - * When true, the table will not automatically paginate rows using getPaginationRowModel() - * and instead will expect you to manually paginate in your data fetching process - * @default false - */ manualPagination?: boolean; - - /** - * The total number of pages available (for server-side pagination) - * Use -1 to indicate unknown page count - */ pageCount?: number; - - /** - * The total number of rows available (for server-side pagination) - */ rowCount?: number; - - /** - * Callback fired when pagination state changes - * Use this to fetch new data based on the current page and page size - */ onPaginationChange?: (pagination: PaginationState) => void; + onRowSelectionChange?: (selection: RowSelectionState) => void; + onColumnFiltersChange?: (filters: ColumnFiltersState) => void; + onExpandedChange?: (expanded: ExpandedState) => void; - /** - * Controlled pagination state - * When provided, the table pagination will be controlled by this state - */ state?: { pagination?: PaginationState; + rowSelection?: RowSelectionState; + columnFilters?: ColumnFiltersState; + columnVisibility?: VisibilityState; + expanded?: ExpandedState; + columnPinning?: ColumnPinningState; }; - /** - * Initial pagination state - * Only used when pagination state is not controlled - */ initialState?: { pagination?: Partial; + columnVisibility?: VisibilityState; + columnPinning?: ColumnPinningState; + expanded?: ExpandedState; }; - /** - * Enhanced pagination configuration - * Set to false to disable pagination completely - * @default Smart defaults based on data size - */ pagination?: PaginationConfig | false; - - /** - * Loading state for the table - * When true, shows loading indicators in pagination controls - * @default false - */ loading?: boolean; - - /** - * Enable keyboard shortcuts for pagination navigation - * @default true - */ enableKeyboardShortcuts?: boolean; + + /** Accessor for sub-rows in tree data */ + getSubRows?: (row: TData) => TData[] | undefined; + + /** Custom empty state content */ + emptyContent?: React.ReactNode; + + /** Toolbar content rendered above the table */ + toolbar?: (table: Table) => React.ReactNode; } -/** - * A data table component with enhanced pagination and TanStack Table v8 compatibility - * Supports both client-side and server-side pagination with rich navigation controls - */ export const DataTable = React.memo( ({ data, columns, className, - pageSize = 10, enableResizing = true, columnResizeMode = "onChange", enableSorting = true, + enableFiltering = false, + enableRowSelection = false, + enableExpanding = false, + enableColumnPinning = false, manualPagination = false, pageCount, - rowCount, // TODO: Will be used for pagination info display in Phase 2 + rowCount, onPaginationChange, + onRowSelectionChange, + onColumnFiltersChange, + onExpandedChange, state, initialState, pagination, - loading = false, // TODO: Will be used for loading states in Phase 3 + loading = false, enableKeyboardShortcuts = true, + getSubRows, + emptyContent, + toolbar, }: DataTableProps) => { const [sorting, setSorting] = useState([]); const [columnSizing, setColumnSizing] = useState({}); + const [columnFilters, setColumnFilters] = useState( + state?.columnFilters ?? [], + ); + const [columnVisibility, setColumnVisibility] = useState( + state?.columnVisibility ?? initialState?.columnVisibility ?? {}, + ); + const [rowSelection, setRowSelection] = useState( + state?.rowSelection ?? {}, + ); + const [expanded, setExpanded] = useState( + state?.expanded ?? initialState?.expanded ?? {}, + ); + const [columnPinning, setColumnPinning] = useState( + state?.columnPinning ?? initialState?.columnPinning ?? {}, + ); - // Memoize columns to prevent re-renders when parent re-renders const memoizedColumns = useMemo(() => columns, [columns]); - // Smart pagination defaults based on data size + // Smart pagination defaults + const DEFAULT_PAGE_SIZE = 10; + const smartPaginationConfig = useMemo((): PaginationConfig | false => { if (pagination === false) return false; if (pagination) { - // Ensure pageSize is in pageSizeOptions to prevent dropdown sync issues - const configPageSize = pagination.pageSize ?? pageSize; + const configPageSize = pagination.pageSize ?? DEFAULT_PAGE_SIZE; const defaultOptions = [10, 25, 50, 100]; const options = pagination.pageSizeOptions ?? defaultOptions; - - // Deduplicate and ensure pageSize is in options const uniqueOptions = [...new Set([...options, configPageSize])]; const pageSizeOptions = uniqueOptions.sort((a, b) => a - b); - - return { - ...pagination, - pageSize: configPageSize, - pageSizeOptions, - }; + return { ...pagination, pageSize: configPageSize, pageSizeOptions }; } - // Auto-detect pagination strategy - if (data.length <= 15) { - return false; // No pagination for small datasets - } + if (data.length <= 15) return false; - // Ensure default pageSize is in options and deduplicate - const defaultOptions = [10, 25, 50, 100]; - const uniqueOptions = [...new Set([...defaultOptions, pageSize])]; - const pageSizeOptions = uniqueOptions.sort((a, b) => a - b); + const pageSizeOptions = [10, 25, 50, 100]; return { - pageSize: pageSize, + pageSize: DEFAULT_PAGE_SIZE, showSizeSelector: true, showPageInfo: true, showNavigation: true, @@ -204,12 +171,10 @@ export const DataTable = React.memo( enableFastNavigation: data.length > 100, enableJumpToPage: data.length > 200, }; - }, [pagination, data.length, pageSize]); + }, [pagination, data.length]); - // Follow TanStack Table's proper patterns for controlled vs uncontrolled state const isControlledPagination = !!state?.pagination; - // Build proper initialState for TanStack Table const tableInitialState = useMemo( () => ({ ...initialState, @@ -220,36 +185,28 @@ export const DataTable = React.memo( (smartPaginationConfig !== false ? smartPaginationConfig.pageSize : undefined) ?? - pageSize, + DEFAULT_PAGE_SIZE, }, }), - [initialState, pageSize, smartPaginationConfig], + [initialState, smartPaginationConfig], ); - // Extract config pageSize for dependency array const configPageSize = smartPaginationConfig !== false ? smartPaginationConfig.pageSize : null; - // Create table key for forcing recreation when essential config changes const tableKey = useMemo(() => { if (isControlledPagination) return "controlled"; - - // For uncontrolled tables, only include pageSize in key (not full config) - const keyPageSize = configPageSize ?? pageSize; + const keyPageSize = configPageSize ?? DEFAULT_PAGE_SIZE; return `uncontrolled-${keyPageSize}`; - }, [isControlledPagination, configPageSize, pageSize]); + }, [isControlledPagination, configPageSize]); - // For controlled pagination, use the provided state and change handler - // For uncontrolled pagination, let TanStack Table manage it with initialState const paginationState = isControlledPagination ? state.pagination : undefined; - // Create proper change handler that works with TanStack Table's updater pattern const handlePaginationChange = useCallback( (updaterOrValue: Updater) => { if (!isControlledPagination || !onPaginationChange) return; - const newValue = typeof updaterOrValue === "function" ? updaterOrValue(paginationState!) @@ -259,7 +216,42 @@ export const DataTable = React.memo( [isControlledPagination, onPaginationChange, paginationState], ); - // TanStack Table configuration + const handleRowSelectionChange = useCallback( + (updaterOrValue: Updater) => { + const newValue = + typeof updaterOrValue === "function" + ? updaterOrValue(rowSelection) + : updaterOrValue; + setRowSelection(newValue); + onRowSelectionChange?.(newValue); + }, + [rowSelection, onRowSelectionChange], + ); + + const handleColumnFiltersChange = useCallback( + (updaterOrValue: Updater) => { + const newValue = + typeof updaterOrValue === "function" + ? updaterOrValue(columnFilters) + : updaterOrValue; + setColumnFilters(newValue); + onColumnFiltersChange?.(newValue); + }, + [columnFilters, onColumnFiltersChange], + ); + + const handleExpandedChange = useCallback( + (updaterOrValue: Updater) => { + const newValue = + typeof updaterOrValue === "function" + ? updaterOrValue(expanded) + : updaterOrValue; + setExpanded(newValue); + onExpandedChange?.(newValue); + }, + [expanded, onExpandedChange], + ); + const table = useReactTable({ data, columns: memoizedColumns, @@ -268,53 +260,77 @@ export const DataTable = React.memo( smartPaginationConfig !== false ? getPaginationRowModel() : undefined, onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), + ...(enableFiltering + ? { + getFilteredRowModel: getFilteredRowModel(), + onColumnFiltersChange: handleColumnFiltersChange, + } + : {}), + ...(enableExpanding || getSubRows + ? { + getExpandedRowModel: getExpandedRowModel(), + onExpandedChange: handleExpandedChange, + } + : {}), - // Enhanced pagination configuration manualPagination, pageCount: pageCount ?? (manualPagination ? -1 : undefined), - - // Initial state (used for uncontrolled state) initialState: tableInitialState, - // State management (used for controlled state) state: { sorting, columnSizing, + columnFilters, + columnVisibility, + rowSelection, + expanded, + columnPinning, ...(isControlledPagination && smartPaginationConfig !== false ? { pagination: paginationState } : {}), }, - // Column sizing callbacks onColumnSizingChange: setColumnSizing, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: handleRowSelectionChange, + onColumnPinningChange: setColumnPinning, - // Callbacks ...(isControlledPagination && smartPaginationConfig !== false ? { onPaginationChange: handlePaginationChange } : {}), - // Feature flags enableSorting, enableColumnResizing: enableResizing, columnResizeMode, + enableRowSelection, + enableColumnPinning, + getSubRows, }); - // Note: Removed sync effect that was causing state conflicts - // For uncontrolled pagination, let TanStack Table manage its own state - - // Don't render pagination if disabled const shouldShowPagination = smartPaginationConfig !== false; - - // Extract values that should trigger row re-renders const currentRows = table.getRowModel().rows; - // Memoize the table rows to prevent unnecessary re-renders const tableRows = useMemo(() => { + if (loading) { + return ( + + +
+
+ Loading... +
+ + + ); + } + if (!currentRows.length) { return ( - No results. + {emptyContent ?? ( + No results. + )} ); @@ -323,78 +339,130 @@ export const DataTable = React.memo( return currentRows.map((row) => ( - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + {row.getVisibleCells().map((cell) => { + const isPinned = cell.column.getIsPinned(); + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} )); - }, [currentRows, memoizedColumns.length]); + }, [currentRows, memoizedColumns.length, loading, emptyContent]); - // Generate header groups (not memoized to ensure sorting indicators update) const headerGroups = table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder ? null : ( -
- {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - {header.column.getCanSort() && ( - <> - {{ - asc: , - desc: , - }[header.column.getIsSorted() as string] ?? ( - + {headerGroup.headers.map((header) => { + const isPinned = header.column.getIsPinned(); + return ( + + {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {header.column.getCanSort() && ( + <> + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? ( + + )} + + )} +
+ )} + {enableResizing && header.column.getCanResize() && ( +
+
- )} -
- )} - {enableResizing && header.column.getCanResize() && ( -
- )} - - ))} + /> +
+ )} + + ); + })} )); return (
+ {toolbar && toolbar(table)} +
@@ -420,3 +488,75 @@ export const DataTable = React.memo( ) as ( props: DataTableProps & { ref?: React.Ref }, ) => React.JSX.Element; + +/** + * Helper to create a selection checkbox column. + * Usage: add selectionColumn() as the first column. + */ +export function selectionColumn< + TData extends object, +>(): ColumnDef { + return { + id: "select", + header: ({ table }) => ( + { + if (el) el.indeterminate = table.getIsSomePageRowsSelected(); + }} + onChange={table.getToggleAllPageRowsSelectedHandler()} + aria-label="Select all" + className="h-4 w-4 rounded border-input" + /> + ), + cell: ({ row }) => ( + + ), + size: 40, + enableSorting: false, + enableResizing: false, + }; +} + +/** + * Helper to create an expand/collapse column for tree data. + * Usage: add expandColumn() as the first column (or after selection). + */ +export function expandColumn< + TData extends object, +>(): ColumnDef { + return { + id: "expand", + header: () => null, + cell: ({ row }) => + row.getCanExpand() ? ( + + ) : ( + + ), + size: 40, + enableSorting: false, + enableResizing: false, + }; +} diff --git a/packages/ui-kit/src/components/data-display/DataTable/DataTableToolbar.tsx b/packages/ui-kit/src/components/data-display/DataTable/DataTableToolbar.tsx new file mode 100644 index 0000000..03d8b11 --- /dev/null +++ b/packages/ui-kit/src/components/data-display/DataTable/DataTableToolbar.tsx @@ -0,0 +1,121 @@ +import React, { useState, useCallback } from "react"; +import type { Table, Column } from "@tanstack/react-table"; +import { Search, SlidersHorizontal, X } from "lucide-react"; +import { cn } from "../../../utils"; + +export interface DataTableToolbarProps { + table: Table; + /** Column ID to use for global-style text filtering */ + filterColumn?: string; + /** Placeholder text for the filter input */ + filterPlaceholder?: string; + /** Show the column visibility dropdown */ + showColumnVisibility?: boolean; + /** Extra toolbar content (buttons, dropdowns, etc.) */ + children?: React.ReactNode; + className?: string; +} + +export function DataTableToolbar({ + table, + filterColumn, + filterPlaceholder = "Filter...", + showColumnVisibility = true, + children, + className, +}: DataTableToolbarProps) { + const [colVisOpen, setColVisOpen] = useState(false); + + const filterValue = + filterColumn != null + ? (table.getColumn(filterColumn)?.getFilterValue() as string) ?? "" + : ""; + + const handleFilterChange = useCallback( + (value: string) => { + if (filterColumn != null) { + table.getColumn(filterColumn)?.setFilterValue(value || undefined); + } + }, + [table, filterColumn], + ); + + const allColumns = table + .getAllColumns() + .filter( + (col) => + col.getCanHide() && col.id !== "select" && col.id !== "expand", + ); + + return ( +
+
+ {filterColumn != null && ( +
+ + handleFilterChange(e.target.value)} + className="flex h-9 w-full rounded-md border border-input bg-background pl-8 pr-8 py-1 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring" + /> + {filterValue && ( + + )} +
+ )} + {children} +
+ + {showColumnVisibility && allColumns.length > 0 && ( +
+ + {colVisOpen && ( + <> +
setColVisOpen(false)} + /> +
+ {allColumns.map((col: Column) => ( + + ))} +
+ + )} +
+ )} +
+ ); +} + +DataTableToolbar.displayName = "DataTableToolbar"; diff --git a/packages/ui-kit/src/components/data-display/DataTable/MIGRATION.md b/packages/ui-kit/src/components/data-display/DataTable/MIGRATION.md deleted file mode 100644 index 4c6f699..0000000 --- a/packages/ui-kit/src/components/data-display/DataTable/MIGRATION.md +++ /dev/null @@ -1,374 +0,0 @@ -# DataTable Migration Guide v0.5.0 - -This guide shows how to migrate from the basic DataTable component to the new comprehensive pagination architecture. - -## Overview - -The new DataTable component introduces enterprise-grade pagination features while maintaining 100% backward compatibility. All existing implementations will continue to work without changes. - -## Breaking Changes - -**None.** This is a completely backward-compatible update. - -## New Features - -### 1. Smart Pagination Defaults - -The component now automatically determines the best pagination strategy: - -- **≤15 items**: No pagination (unchanged behavior) -- **>15 items**: Automatic pagination with smart defaults -- **>100 items**: Enables fast navigation (±5 pages) -- **>200 items**: Enables jump-to-page functionality - -### 2. Server-Side Pagination Support - -Full TanStack Table compatibility for server-side pagination: - -```tsx -// NEW: Server-side pagination - -``` - -### 3. Keyboard Navigation - -Built-in keyboard shortcuts for power users: - -- **Arrow keys**: Previous/Next page -- **Home/End**: First/Last page -- **PageUp/PageDown**: Fast navigation (±5 pages) -- **Ctrl+G**: Focus jump-to-page input -- **Enter/Escape**: Submit/Cancel in jump input - -### 4. Rich Pagination Controls - -Comprehensive navigation options: - -```tsx -// NEW: Rich pagination configuration - -``` - -## Migration Examples - -### Before (v0.4.x) - -```tsx -// Basic usage - still works exactly the same - - -// With page size - still works - -``` - -### After (v0.5.0) - -```tsx -// Same basic usage - now with smart pagination - -// Automatically enables pagination for >15 items - -// Enhanced configuration - - -// Server-side pagination - { - fetchUsers({ page: pageIndex + 1, limit: pageSize }); - }} - loading={isLoading} -/> - -// Disable pagination entirely - -``` - -## Configuration Options - -### PaginationConfig Interface - -```tsx -interface PaginationConfig { - pageSize?: number; // Items per page - showSizeSelector?: boolean; // Show rows per page selector - showPageInfo?: boolean; // Show "Showing X-Y of Z" - showNavigation?: boolean; // Show pagination controls - pageSizeOptions?: number[]; // Options for size selector - enableFastNavigation?: boolean; // ±5 page buttons - enableJumpToPage?: boolean; // Jump to page input -} -``` - -### DataTable Props (New/Updated) - -```tsx -interface DataTableProps { - // Existing props (unchanged) - data: TData[]; - columns: ColumnDef[]; - pageSize?: number; // DEPRECATED: Use pagination.pageSize - enableSorting?: boolean; - enableResizing?: boolean; - - // NEW: Enhanced pagination - pagination?: PaginationConfig | false; - - // NEW: Server-side pagination (TanStack Table compatibility) - manualPagination?: boolean; - pageCount?: number; - rowCount?: number; - onPaginationChange?: (state: PaginationState) => void; - - // NEW: UI enhancements - loading?: boolean; - enableKeyboardShortcuts?: boolean; -} -``` - -## Common Migration Patterns - -### 1. Basic Client-Side Pagination - -```tsx -// Before - - -// After (automatic smart defaults) - -// Automatically uses pageSize=25 for datasets this size - -// After (explicit configuration) - -``` - -### 2. Migrating to Server-Side Pagination - -```tsx -// Before: Client-side with all data loaded -const AllUsersTable = () => { - const { data: allUsers } = useQuery(["users"], fetchAllUsers); - - return ; -}; - -// After: Efficient server-side pagination -const UsersTable = () => { - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 20, - }); - - const { data, isLoading } = useQuery(["users", pagination], () => - fetchUsers({ - page: pagination.pageIndex + 1, - limit: pagination.pageSize, - }), - ); - - return ( - - ); -}; -``` - -### 3. Custom Pagination Configuration - -```tsx -// Small admin tables - disable pagination - - -// Large datasets - full features - - -// Mobile-friendly - simplified controls - -``` - -## Performance Considerations - -### Smart Defaults Strategy - -The component uses intelligent defaults based on data size: - -| Data Size | Pagination | Page Size | Fast Nav | Jump To Page | -| --------- | ---------- | --------- | -------- | ------------ | -| ≤15 items | Disabled | N/A | No | No | -| 16-100 | Enabled | 25 | No | No | -| 101-200 | Enabled | 25 | Yes | No | -| >200 | Enabled | 25 | Yes | Yes | - -### Optimization Tips - -1. **Use server-side pagination** for datasets >1000 items -2. **Enable smart defaults** for most use cases -3. **Disable pagination** for small reference tables -4. **Customize page sizes** based on your content density - -## Accessibility Improvements - -The new pagination includes comprehensive accessibility features: - -- **ARIA labels** for all pagination controls -- **Keyboard navigation** with visual focus indicators -- **Screen reader announcements** for page changes -- **High contrast** support for all interactive elements - -## Troubleshooting - -### Common Issues - -**Q: My table doesn't show pagination anymore** -A: Tables with ≤15 items automatically disable pagination. Use `pagination={{ pageSize: 10 }}` to force enable. - -**Q: Keyboard shortcuts aren't working** -A: Set `enableKeyboardShortcuts={true}` or check for conflicting event handlers. - -**Q: Server-side pagination isn't loading** -A: Ensure `manualPagination={true}` and implement `onPaginationChange` handler. - -**Q: Page size selector shows wrong options** -A: Customize with `pagination={{ pageSizeOptions: [10, 25, 50] }}`. - -### Debug Mode - -Enable debug logging to troubleshoot pagination issues: - -```tsx - -``` - -## TypeScript Support - -All new features include full TypeScript support: - -```tsx -import type { - PaginationConfig, - PaginationState, - DataTableProps, -} from "@etherisc/ui-kit"; - -// Type-safe pagination handler -const handlePaginationChange = (state: PaginationState) => { - // state.pageIndex and state.pageSize are properly typed - console.log(`Page ${state.pageIndex + 1}, Size ${state.pageSize}`); -}; - -// Type-safe configuration -const paginationConfig: PaginationConfig = { - pageSize: 25, - showSizeSelector: true, - enableFastNavigation: true, -}; -``` - -## Support - -For questions or issues: - -1. Check the [DataTable Storybook examples](/.storybook) -2. Review this migration guide -3. Open an issue in the UI Kit repository - ---- - -_This migration guide covers DataTable v0.5.0. For the latest documentation, see the component's Storybook stories._ diff --git a/packages/ui-kit/src/components/data-display/DataTable/__tests__/DataTable.pagination-navigation.test.tsx b/packages/ui-kit/src/components/data-display/DataTable/__tests__/DataTable.pagination-navigation.test.tsx index d5b44a2..33cef16 100644 --- a/packages/ui-kit/src/components/data-display/DataTable/__tests__/DataTable.pagination-navigation.test.tsx +++ b/packages/ui-kit/src/components/data-display/DataTable/__tests__/DataTable.pagination-navigation.test.tsx @@ -49,8 +49,8 @@ describe("DataTable Pagination", () => { { { { , @@ -141,8 +141,8 @@ describe("DataTable Pagination", () => { { , @@ -197,7 +197,7 @@ describe("DataTable Pagination", () => { it("should not show pagination for small datasets", () => { const smallData = generateTestData(10); - render(); + render(); expect(screen.getAllByRole("row")).toHaveLength(11); expect(screen.queryByRole("navigation")).not.toBeInTheDocument(); @@ -209,8 +209,8 @@ describe("DataTable Pagination", () => { , diff --git a/packages/ui-kit/src/components/data-display/DataTable/index.ts b/packages/ui-kit/src/components/data-display/DataTable/index.ts index be2f2c7..848fc2e 100644 --- a/packages/ui-kit/src/components/data-display/DataTable/index.ts +++ b/packages/ui-kit/src/components/data-display/DataTable/index.ts @@ -1,2 +1,3 @@ export * from "./DataTable"; export * from "./DataTablePagination"; +export * from "./DataTableToolbar"; diff --git a/packages/ui-kit/src/components/data-display/DataTable/stories/DataTable.stories.tsx b/packages/ui-kit/src/components/data-display/DataTable/stories/DataTable.stories.tsx index b4143dd..bcdd63c 100644 --- a/packages/ui-kit/src/components/data-display/DataTable/stories/DataTable.stories.tsx +++ b/packages/ui-kit/src/components/data-display/DataTable/stories/DataTable.stories.tsx @@ -184,15 +184,6 @@ This component passes all axe-core accessibility tests and includes: argTypes: { data: { control: "object" }, columns: { control: "object" }, - pageSize: { - control: { type: "number", min: 5, max: 50, step: 5 }, - description: - "Number of rows to display per page (deprecated - use pagination.pageSize)", - table: { - type: { summary: "number" }, - defaultValue: { summary: "10" }, - }, - }, enableResizing: { control: "boolean", description: "Allow columns to be resized", @@ -252,7 +243,7 @@ export const Default = { args: { data: generateMockData(50), columns, - pageSize: 10, + pagination: { pageSize: 10 }, enableResizing: true, enableSorting: true, }, @@ -262,7 +253,6 @@ export const SmallTable = { args: { data: generateMockData(5), columns, - pageSize: 10, enableResizing: true, enableSorting: true, }, @@ -272,7 +262,7 @@ export const LargeTable = { args: { data: generateMockData(200), columns, - pageSize: 15, + pagination: { pageSize: 15 }, enableResizing: true, enableSorting: true, }, @@ -282,7 +272,7 @@ export const WithoutSorting = { args: { data: generateMockData(50), columns, - pageSize: 10, + pagination: { pageSize: 10 }, enableResizing: true, enableSorting: false, }, @@ -292,7 +282,7 @@ export const WithoutResizing = { args: { data: generateMockData(50), columns, - pageSize: 10, + pagination: { pageSize: 10 }, enableResizing: false, enableSorting: true, }, diff --git a/packages/ui-kit/src/components/feedback/EmptyState/EmptyState.test.tsx b/packages/ui-kit/src/components/feedback/EmptyState/EmptyState.test.tsx new file mode 100644 index 0000000..30a6ca0 --- /dev/null +++ b/packages/ui-kit/src/components/feedback/EmptyState/EmptyState.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { EmptyState } from "./EmptyState"; + +describe("EmptyState", () => { + it("renders title", () => { + render(); + expect(screen.getByText("No data found")).toBeInTheDocument(); + }); + + it("renders description when provided", () => { + render( + , + ); + expect(screen.getByText("Try adjusting your filters")).toBeInTheDocument(); + }); + + it("renders action button when provided", () => { + render( + Add item} />, + ); + expect(screen.getByText("Add item")).toBeInTheDocument(); + }); + + it("renders icon when provided", () => { + render( + } + />, + ); + expect(screen.getByTestId("icon")).toBeInTheDocument(); + }); +}); diff --git a/packages/ui-kit/src/components/feedback/EmptyState/EmptyState.tsx b/packages/ui-kit/src/components/feedback/EmptyState/EmptyState.tsx new file mode 100644 index 0000000..5c53588 --- /dev/null +++ b/packages/ui-kit/src/components/feedback/EmptyState/EmptyState.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface EmptyStateProps { + icon?: React.ReactNode; + title: string; + description?: string; + action?: React.ReactNode; + className?: string; +} + +export function EmptyState({ + icon, + title, + description, + action, + className, +}: EmptyStateProps) { + return ( +
+ {icon && ( +
+ {icon} +
+ )} +

{title}

+ {description && ( +

+ {description} +

+ )} + {action &&
{action}
} +
+ ); +} + +EmptyState.displayName = "EmptyState"; diff --git a/packages/ui-kit/src/components/feedback/EmptyState/index.ts b/packages/ui-kit/src/components/feedback/EmptyState/index.ts new file mode 100644 index 0000000..88e6211 --- /dev/null +++ b/packages/ui-kit/src/components/feedback/EmptyState/index.ts @@ -0,0 +1,2 @@ +export { EmptyState } from "./EmptyState"; +export type { EmptyStateProps } from "./EmptyState"; diff --git a/packages/ui-kit/src/components/feedback/Modal/ConfirmModal.tsx b/packages/ui-kit/src/components/feedback/Modal/ConfirmModal.tsx new file mode 100644 index 0000000..7049bc8 --- /dev/null +++ b/packages/ui-kit/src/components/feedback/Modal/ConfirmModal.tsx @@ -0,0 +1,80 @@ +import * as React from "react"; +import { Modal } from "./Modal"; +import type { ModalSize } from "./Modal"; + +export interface ConfirmModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: () => void | Promise; + onCancel?: () => void; + title: string; + description?: string; + confirmLabel?: string; + cancelLabel?: string; + variant?: "default" | "destructive"; + size?: ModalSize; + loading?: boolean; +} + +export function ConfirmModal({ + open, + onOpenChange, + onConfirm, + onCancel, + title, + description, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + variant = "default", + size = "sm", + loading = false, +}: ConfirmModalProps) { + const handleCancel = React.useCallback(() => { + onCancel?.(); + onOpenChange(false); + }, [onCancel, onOpenChange]); + + const handleConfirm = React.useCallback(async () => { + await onConfirm(); + onOpenChange(false); + }, [onConfirm, onOpenChange]); + + const confirmButtonClass = + variant === "destructive" + ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" + : "bg-primary text-primary-foreground hover:bg-primary/90"; + + return ( + + + +
+ } + > + {null} + + ); +} + +ConfirmModal.displayName = "ConfirmModal"; diff --git a/packages/ui-kit/src/components/feedback/Modal/Modal.test.tsx b/packages/ui-kit/src/components/feedback/Modal/Modal.test.tsx new file mode 100644 index 0000000..56c3e1a --- /dev/null +++ b/packages/ui-kit/src/components/feedback/Modal/Modal.test.tsx @@ -0,0 +1,133 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import { Modal } from "./Modal"; +import { ConfirmModal } from "./ConfirmModal"; + +describe("Modal", () => { + it("renders title and children when open", () => { + render( + +

Modal content

+
, + ); + expect(screen.getByText("Test Modal")).toBeInTheDocument(); + expect(screen.getByText("Modal content")).toBeInTheDocument(); + }); + + it("does not render content when closed", () => { + render( + +

Hidden content

+
, + ); + expect(screen.queryByText("Hidden")).not.toBeInTheDocument(); + }); + + it("renders description when provided", () => { + render( + +

Content

+
, + ); + expect(screen.getByText("A description")).toBeInTheDocument(); + }); + + it("renders footer when provided", () => { + render( + Save} + > +

Content

+
, + ); + expect(screen.getByText("Save")).toBeInTheDocument(); + }); +}); + +describe("ConfirmModal", () => { + it("renders confirm and cancel buttons", () => { + render( + , + ); + expect(screen.getByText("Delete item?")).toBeInTheDocument(); + expect(screen.getByText("This cannot be undone.")).toBeInTheDocument(); + expect(screen.getByText("Confirm")).toBeInTheDocument(); + expect(screen.getByText("Cancel")).toBeInTheDocument(); + }); + + it("calls onConfirm when confirm button is clicked", async () => { + const onConfirm = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByText("Confirm")); + expect(onConfirm).toHaveBeenCalledOnce(); + }); + + it("calls onCancel and closes when cancel is clicked", () => { + const onCancel = vi.fn(); + const onOpenChange = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByText("Cancel")); + expect(onCancel).toHaveBeenCalledOnce(); + expect(onOpenChange).toHaveBeenCalledWith(false); + }); + + it("uses custom button labels", () => { + render( + , + ); + expect(screen.getByText("Yes, delete")).toBeInTheDocument(); + expect(screen.getByText("No, keep")).toBeInTheDocument(); + }); + + it("disables buttons when loading", () => { + render( + , + ); + const buttons = screen.getAllByRole("button"); + const cancelBtn = buttons.find((b) => b.textContent === "Cancel"); + const confirmBtn = buttons.find((b) => b.textContent === "..."); + expect(cancelBtn).toBeDisabled(); + expect(confirmBtn).toBeDisabled(); + }); +}); diff --git a/packages/ui-kit/src/components/feedback/Modal/Modal.tsx b/packages/ui-kit/src/components/feedback/Modal/Modal.tsx new file mode 100644 index 0000000..1bd231e --- /dev/null +++ b/packages/ui-kit/src/components/feedback/Modal/Modal.tsx @@ -0,0 +1,61 @@ +import * as React from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} from "../../ui/dialog"; +import { cn } from "../../../lib/utils"; + +export type ModalSize = "sm" | "md" | "lg" | "xl" | "full"; + +const sizeClasses: Record = { + sm: "max-w-sm", + md: "max-w-lg", + lg: "max-w-2xl", + xl: "max-w-4xl", + full: "max-w-[95vw]", +}; + +export interface ModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title?: string; + description?: string; + size?: ModalSize; + children: React.ReactNode; + footer?: React.ReactNode; + className?: string; +} + +export function Modal({ + open, + onOpenChange, + title, + description, + size = "md", + children, + footer, + className, +}: ModalProps) { + return ( + + + {(title || description) && ( + + {title && {title}} + {description && ( + {description} + )} + + )} +
{children}
+ {footer && {footer}} +
+
+ ); +} + +Modal.displayName = "Modal"; diff --git a/packages/ui-kit/src/components/feedback/Modal/index.ts b/packages/ui-kit/src/components/feedback/Modal/index.ts new file mode 100644 index 0000000..1a37785 --- /dev/null +++ b/packages/ui-kit/src/components/feedback/Modal/index.ts @@ -0,0 +1,4 @@ +export { Modal } from "./Modal"; +export type { ModalProps, ModalSize } from "./Modal"; +export { ConfirmModal } from "./ConfirmModal"; +export type { ConfirmModalProps } from "./ConfirmModal"; diff --git a/packages/ui-kit/src/components/feedback/Spinner/Spinner.test.tsx b/packages/ui-kit/src/components/feedback/Spinner/Spinner.test.tsx new file mode 100644 index 0000000..77f91c1 --- /dev/null +++ b/packages/ui-kit/src/components/feedback/Spinner/Spinner.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { Spinner } from "./Spinner"; + +describe("Spinner", () => { + it("renders with status role and label", () => { + render(); + const spinner = screen.getByRole("status"); + expect(spinner).toBeInTheDocument(); + expect(spinner).toHaveAttribute("aria-label", "Loading"); + }); + + it("applies size classes", () => { + const { rerender } = render(); + expect(screen.getByRole("status")).toHaveClass("h-4", "w-4"); + + rerender(); + expect(screen.getByRole("status")).toHaveClass("h-10", "w-10"); + }); + + it("accepts custom label", () => { + render(); + expect(screen.getByRole("status")).toHaveAttribute("aria-label", "Saving"); + }); +}); diff --git a/packages/ui-kit/src/components/feedback/Spinner/Spinner.tsx b/packages/ui-kit/src/components/feedback/Spinner/Spinner.tsx new file mode 100644 index 0000000..1823ac3 --- /dev/null +++ b/packages/ui-kit/src/components/feedback/Spinner/Spinner.tsx @@ -0,0 +1,33 @@ +import { cn } from "@/lib/utils"; + +export type SpinnerSize = "sm" | "md" | "lg"; + +export interface SpinnerProps { + size?: SpinnerSize; + className?: string; + label?: string; +} + +const sizeClasses: Record = { + sm: "h-4 w-4 border-2", + md: "h-6 w-6 border-2", + lg: "h-10 w-10 border-[3px]", +}; + +export function Spinner({ size = "md", className, label = "Loading" }: SpinnerProps) { + return ( +
+ {label} +
+ ); +} + +Spinner.displayName = "Spinner"; diff --git a/packages/ui-kit/src/components/feedback/Spinner/index.ts b/packages/ui-kit/src/components/feedback/Spinner/index.ts new file mode 100644 index 0000000..cfc03c2 --- /dev/null +++ b/packages/ui-kit/src/components/feedback/Spinner/index.ts @@ -0,0 +1,2 @@ +export { Spinner } from "./Spinner"; +export type { SpinnerProps, SpinnerSize } from "./Spinner"; diff --git a/packages/ui-kit/src/components/feedback/StatusBadge/StatusBadge.test.tsx b/packages/ui-kit/src/components/feedback/StatusBadge/StatusBadge.test.tsx index 2abd277..f9f9a00 100644 --- a/packages/ui-kit/src/components/feedback/StatusBadge/StatusBadge.test.tsx +++ b/packages/ui-kit/src/components/feedback/StatusBadge/StatusBadge.test.tsx @@ -13,48 +13,48 @@ describe("StatusBadge", () => { render(Default); const badge = screen.getByText("Default"); - expect(badge).toHaveClass("bg-[hsl(var(--neutral))]"); - expect(badge).toHaveClass("text-[hsl(var(--neutral-content))]"); + expect(badge).toHaveClass("bg-secondary"); + expect(badge).toHaveClass("text-secondary-foreground"); }); it("should apply correct variant styling for success", () => { render(Success); const badge = screen.getByText("Success"); - expect(badge).toHaveClass("bg-[hsl(var(--success))]"); - expect(badge).toHaveClass("text-[hsl(var(--success-content))]"); + expect(badge).toHaveClass("bg-success"); + expect(badge).toHaveClass("text-success-foreground"); }); it("should apply correct variant styling for error", () => { render(Error); const badge = screen.getByText("Error"); - expect(badge).toHaveClass("bg-[hsl(var(--error))]"); - expect(badge).toHaveClass("text-[hsl(var(--error-content))]"); + expect(badge).toHaveClass("bg-error"); + expect(badge).toHaveClass("text-error-foreground"); }); it("should apply correct variant styling for warning", () => { render(Warning); const badge = screen.getByText("Warning"); - expect(badge).toHaveClass("bg-[hsl(var(--warning))]"); - expect(badge).toHaveClass("text-[hsl(var(--warning-content))]"); + expect(badge).toHaveClass("bg-warning"); + expect(badge).toHaveClass("text-warning-foreground"); }); it("should apply correct variant styling for info", () => { render(Info); const badge = screen.getByText("Info"); - expect(badge).toHaveClass("bg-[hsl(var(--info))]"); - expect(badge).toHaveClass("text-[hsl(var(--info-content))]"); + expect(badge).toHaveClass("bg-info"); + expect(badge).toHaveClass("text-info-foreground"); }); it("should apply correct variant styling for pending", () => { render(Pending); const badge = screen.getByText("Pending"); - expect(badge).toHaveClass("bg-[hsl(var(--base-300))]"); - expect(badge).toHaveClass("text-[hsl(var(--base-content))]"); + expect(badge).toHaveClass("bg-muted"); + expect(badge).toHaveClass("text-muted-foreground"); }); it("should apply base styling classes", () => { diff --git a/packages/ui-kit/src/components/feedback/StatusBadge/StatusBadge.tsx b/packages/ui-kit/src/components/feedback/StatusBadge/StatusBadge.tsx index e9785f5..9661806 100644 --- a/packages/ui-kit/src/components/feedback/StatusBadge/StatusBadge.tsx +++ b/packages/ui-kit/src/components/feedback/StatusBadge/StatusBadge.tsx @@ -25,18 +25,12 @@ export function StatusBadge({ addToast('success', 'Success!', 'Operation completed successfully')} - className="btn btn-success" + className="px-4 py-2 rounded-md text-sm font-medium bg-success text-success-foreground" aria-label="Add a success toast notification" > Add Success Toast @@ -143,7 +143,7 @@ function InteractiveToastDemo() { diff --git a/packages/ui-kit/src/components/feedback/index.ts b/packages/ui-kit/src/components/feedback/index.ts index b7486dd..1a7acce 100644 --- a/packages/ui-kit/src/components/feedback/index.ts +++ b/packages/ui-kit/src/components/feedback/index.ts @@ -1,2 +1,5 @@ export * from './Toast'; -export * from './StatusBadge'; \ No newline at end of file +export * from './StatusBadge'; +export * from './Modal'; +export * from './Spinner'; +export * from './EmptyState'; \ No newline at end of file diff --git a/packages/ui-kit/src/components/form/Form.mdx b/packages/ui-kit/src/components/form/Form.mdx index 9d0df02..fbc0ba8 100644 --- a/packages/ui-kit/src/components/form/Form.mdx +++ b/packages/ui-kit/src/components/form/Form.mdx @@ -44,7 +44,7 @@ export function LoginForm() { required placeholder="Enter your password" /> - @@ -81,7 +81,7 @@ The `FormGroup` component is used to group a form field with its label, error me description="We'll never share your email with anyone else." required > - + ``` @@ -136,14 +136,14 @@ function FormActions() { @@ -124,7 +124,7 @@ function ExampleFormComponent() { {submittedData && ( -
+

Submitted Data:

                         {JSON.stringify(submittedData, null, 2)}
diff --git a/packages/ui-kit/src/components/form/FormGroup.tsx b/packages/ui-kit/src/components/form/FormGroup.tsx
index e2f2c6a..055f66d 100644
--- a/packages/ui-kit/src/components/form/FormGroup.tsx
+++ b/packages/ui-kit/src/components/form/FormGroup.tsx
@@ -33,7 +33,7 @@ export function FormGroup({
       )}
       {children}
       {description && !error && (
-        

{description}

+

{description}

)} {error &&

{error}

}
diff --git a/packages/ui-kit/src/components/layout/ContentWrapper.tsx b/packages/ui-kit/src/components/layout/ContentWrapper.tsx index e3de674..35c9951 100644 --- a/packages/ui-kit/src/components/layout/ContentWrapper.tsx +++ b/packages/ui-kit/src/components/layout/ContentWrapper.tsx @@ -80,7 +80,7 @@ export const ContentWrapper: React.FC = ({ {/* Main content */}
= ({ )} - {!isCollapsed && {label}} + {!isCollapsed && {label}} {hasChildren && !isCollapsed && ( = { control: { type: "select" }, options: [ "default", - "primary", - "secondary", + "destructive", "outline", - "ghost", - "link", - "danger", - ], - description: "The visual style of the button (recommended)", - }, - intent: { - control: { type: "select" }, - options: [ - "default", - "primary", "secondary", - "outline", "ghost", "link", - "danger", ], - description: "Deprecated: Use variant instead. Will be removed in v0.5.0", + description: "The visual style of the button", }, size: { control: { type: "select" }, @@ -59,24 +45,38 @@ export const Default: Story = { }, }; -export const Primary: Story = { +export const Secondary: Story = { args: { - intent: "primary", - children: "Primary Button", + variant: "secondary", + children: "Secondary Button", }, }; -export const Secondary: Story = { +export const Destructive: Story = { args: { - intent: "secondary", - children: "Secondary Button", + variant: "destructive", + children: "Destructive Button", }, }; -export const Danger: Story = { +export const Outline: Story = { args: { - intent: "danger", - children: "Danger Button", + variant: "outline", + children: "Outline Button", + }, +}; + +export const Ghost: Story = { + args: { + variant: "ghost", + children: "Ghost Button", + }, +}; + +export const Link: Story = { + args: { + variant: "link", + children: "Link Button", }, }; @@ -94,10 +94,9 @@ export const Disabled: Story = { }, }; -// i18n Examples const I18nButtonExample = ({ translationKey }: { translationKey: string }) => { const { t } = useTranslation(); - return ; + return ; }; export const I18nSubmit: Story = { @@ -147,47 +146,3 @@ export const I18nLogin: Story = { }, }, }; - -// New variant examples (recommended approach) -export const VariantPrimary: Story = { - args: { - variant: "primary", - children: "Primary Button (variant)", - }, - parameters: { - docs: { - description: { - story: - "Primary button using the new variant prop (recommended over intent).", - }, - }, - }, -}; - -export const VariantSecondary: Story = { - args: { - variant: "secondary", - children: "Secondary Button (variant)", - }, - parameters: { - docs: { - description: { - story: "Secondary button using the new variant prop.", - }, - }, - }, -}; - -export const VariantDanger: Story = { - args: { - variant: "danger", - children: "Danger Button (variant)", - }, - parameters: { - docs: { - description: { - story: "Danger button using the new variant prop.", - }, - }, - }, -}; diff --git a/packages/ui-kit/src/components/primitives/Button/Button.test.tsx b/packages/ui-kit/src/components/primitives/Button/Button.test.tsx index 44823ea..b913a19 100644 --- a/packages/ui-kit/src/components/primitives/Button/Button.test.tsx +++ b/packages/ui-kit/src/components/primitives/Button/Button.test.tsx @@ -11,10 +11,9 @@ describe("Button", () => { ).toBeInTheDocument(); }); - it("applies intent classes", () => { - render(); + it("applies variant classes", () => { + render(); const button = screen.getByRole("button"); - // assuming DaisyUI tailwind tokens compiled; check for generic class existence expect(button).toHaveClass("transition-colors"); }); @@ -30,32 +29,27 @@ describe("Button", () => { expect(button).toBeDisabled(); }); - it("applies variant classes", () => { - render(); + it("renders outline variant", () => { + render(); const button = screen.getByRole("button"); expect(button).toHaveClass("transition-colors"); }); - it("prefers variant over intent when both are provided", () => { - render( - , - ); + it("uses default when no variant is provided", () => { + render(); const button = screen.getByRole("button"); - expect(button).toBeInTheDocument(); - // Should use variant="secondary", not intent="primary" + expect(button).toHaveClass("transition-colors"); }); - it("falls back to intent when variant is not provided (backward compatibility)", () => { - render(); + it("renders ghost variant", () => { + render(); const button = screen.getByRole("button"); - expect(button).toHaveClass("transition-colors"); + expect(button).toBeInTheDocument(); }); - it("uses default when neither variant nor intent is provided", () => { - render(); + it("renders link variant", () => { + render(); const button = screen.getByRole("button"); - expect(button).toHaveClass("transition-colors"); + expect(button).toBeInTheDocument(); }); }); diff --git a/packages/ui-kit/src/components/primitives/Button/Button.tsx b/packages/ui-kit/src/components/primitives/Button/Button.tsx index ba7b4eb..0611171 100644 --- a/packages/ui-kit/src/components/primitives/Button/Button.tsx +++ b/packages/ui-kit/src/components/primitives/Button/Button.tsx @@ -1,8 +1,5 @@ import React from "react"; -import { - Button as ShadcnButton, - type ButtonProps as ShadcnButtonProps, -} from "@/components/ui/button"; +import { Button as ShadcnButton } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { ButtonProps } from "./types"; @@ -10,8 +7,7 @@ export const Button = React.forwardRef( ( { className, - intent, - variant, + variant = "default", size = "default", loading = false, disabled, @@ -20,36 +16,10 @@ export const Button = React.forwardRef( }, ref, ) => { - // Handle backward compatibility: prefer variant over intent - const buttonVariant = variant || intent || "default"; - - // Warn about deprecated intent prop usage in development - if (process.env.NODE_ENV === "development" && intent && !variant) { - console.warn( - 'Button: The "intent" prop is deprecated and will be removed in v0.5.0. Please use "variant" instead.', - ); - } - - // Map our variant to Shadcn's variant system - const shadcnVariant: ShadcnButtonProps["variant"] = - buttonVariant === "danger" - ? "destructive" - : buttonVariant === "primary" - ? "default" - : buttonVariant === "secondary" - ? "secondary" - : buttonVariant === "outline" - ? "outline" - : buttonVariant === "ghost" - ? "ghost" - : buttonVariant === "link" - ? "link" - : "default"; - return ( { - /** - * The visual style of the button - * @deprecated Use 'variant' instead. Will be removed in v0.5.0 - */ - intent?: - | "default" - | "primary" - | "secondary" - | "danger" - | "outline" - | "ghost" - | "link"; /** * The visual style of the button */ variant?: | "default" - | "primary" - | "secondary" - | "danger" + | "destructive" | "outline" + | "secondary" | "ghost" | "link"; /** diff --git a/packages/ui-kit/src/components/primitives/CodeEditor/CodeEditor.test.tsx b/packages/ui-kit/src/components/primitives/CodeEditor/CodeEditor.test.tsx index 42b63ec..db7627f 100644 --- a/packages/ui-kit/src/components/primitives/CodeEditor/CodeEditor.test.tsx +++ b/packages/ui-kit/src/components/primitives/CodeEditor/CodeEditor.test.tsx @@ -167,7 +167,7 @@ describe("CodeEditor", () => { }); }); - it("handles focus and blur events", async () => { + it("handles focus and blur-sm events", async () => { const handleFocus = vi.fn(); const handleBlur = vi.fn(); const { container } = render( diff --git a/packages/ui-kit/src/components/primitives/ComboBox/ComboBox.tsx b/packages/ui-kit/src/components/primitives/ComboBox/ComboBox.tsx index 16b0b3e..b5d54fa 100644 --- a/packages/ui-kit/src/components/primitives/ComboBox/ComboBox.tsx +++ b/packages/ui-kit/src/components/primitives/ComboBox/ComboBox.tsx @@ -146,7 +146,7 @@ const ComboBox = React.forwardRef( diff --git a/packages/ui-kit/src/components/primitives/Kbd/Kbd.tsx b/packages/ui-kit/src/components/primitives/Kbd/Kbd.tsx new file mode 100644 index 0000000..784d96f --- /dev/null +++ b/packages/ui-kit/src/components/primitives/Kbd/Kbd.tsx @@ -0,0 +1,21 @@ +import { cn } from "@/lib/utils"; + +export interface KbdProps { + children: React.ReactNode; + className?: string; +} + +export function Kbd({ children, className }: KbdProps) { + return ( + + {children} + + ); +} + +Kbd.displayName = "Kbd"; diff --git a/packages/ui-kit/src/components/primitives/Kbd/index.ts b/packages/ui-kit/src/components/primitives/Kbd/index.ts new file mode 100644 index 0000000..0a426d1 --- /dev/null +++ b/packages/ui-kit/src/components/primitives/Kbd/index.ts @@ -0,0 +1,2 @@ +export { Kbd } from "./Kbd"; +export type { KbdProps } from "./Kbd"; diff --git a/packages/ui-kit/src/components/primitives/MarkdownEditor/MarkdownEditor.tsx b/packages/ui-kit/src/components/primitives/MarkdownEditor/MarkdownEditor.tsx index e6b29cc..518c1ca 100644 --- a/packages/ui-kit/src/components/primitives/MarkdownEditor/MarkdownEditor.tsx +++ b/packages/ui-kit/src/components/primitives/MarkdownEditor/MarkdownEditor.tsx @@ -92,7 +92,7 @@ export function MarkdownEditor({ ); const editorClasses = cn( - "w-full min-h-[120px] p-3 bg-transparent border-none outline-none resize-none", + "w-full min-h-[120px] p-3 bg-transparent border-none outline-hidden resize-none", "placeholder:text-muted-foreground", "disabled:cursor-not-allowed disabled:opacity-50", "font-mono text-sm leading-relaxed", @@ -123,7 +123,7 @@ export function MarkdownEditor({ disabled={disabled} className={cn( "px-2 py-1 rounded text-xs font-medium transition-colors", - "hover:bg-background focus:outline-none focus:ring-1 focus:ring-ring", + "hover:bg-background focus:outline-hidden focus:ring-1 focus:ring-ring", "disabled:cursor-not-allowed", isPreview ? "bg-primary text-primary-foreground" diff --git a/packages/ui-kit/src/components/primitives/SliderInput/SliderInput.tsx b/packages/ui-kit/src/components/primitives/SliderInput/SliderInput.tsx index 578105e..1f7637f 100644 --- a/packages/ui-kit/src/components/primitives/SliderInput/SliderInput.tsx +++ b/packages/ui-kit/src/components/primitives/SliderInput/SliderInput.tsx @@ -108,7 +108,7 @@ const SliderInput = React.forwardRef< className={cn( sizeClasses[size], error && - "[&_[role=slider]]:border-destructive [&_[role=slider]]:focus-visible:ring-destructive", + "**:[[role=slider]]:border-destructive focus-visible:**:[[role=slider]]:ring-destructive", sliderClassName, )} thumbProps={{ diff --git a/packages/ui-kit/src/components/primitives/SpinnerInput/SpinnerInput.test.tsx b/packages/ui-kit/src/components/primitives/SpinnerInput/SpinnerInput.test.tsx index 9399282..ce81d3d 100644 --- a/packages/ui-kit/src/components/primitives/SpinnerInput/SpinnerInput.test.tsx +++ b/packages/ui-kit/src/components/primitives/SpinnerInput/SpinnerInput.test.tsx @@ -139,7 +139,7 @@ describe("SpinnerInput", () => { expect(handleChange).toHaveBeenCalledWith(25); }); - it("formats value with precision on blur", async () => { + it("formats value with precision on blur-sm", async () => { const user = userEvent.setup(); const handleChange = vi.fn(); render(); @@ -202,7 +202,7 @@ describe("SpinnerInput", () => { expect(handleChange).toHaveBeenCalledWith(0); }); - it("clamps values to min/max range on blur", async () => { + it("clamps values to min/max range on blur-sm", async () => { const user = userEvent.setup(); const handleChange = vi.fn(); render( diff --git a/packages/ui-kit/src/components/primitives/ThemeToggle/ThemeToggle.tsx b/packages/ui-kit/src/components/primitives/ThemeToggle/ThemeToggle.tsx index 97e7cd8..b1d0075 100644 --- a/packages/ui-kit/src/components/primitives/ThemeToggle/ThemeToggle.tsx +++ b/packages/ui-kit/src/components/primitives/ThemeToggle/ThemeToggle.tsx @@ -58,10 +58,10 @@ export function ThemeToggle({ type="button" onClick={handleToggle} className={cn( - "rounded-full p-2 flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", + "rounded-full p-2 flex items-center justify-center focus:outline-hidden focus-visible:ring-2 focus-visible:ring-primary", isDarkMode - ? "text-primary-content bg-primary" - : "text-base-content bg-base-200", + ? "text-primary-foreground bg-primary" + : "text-foreground bg-muted", sizeClasses[size], className, )} diff --git a/packages/ui-kit/src/components/primitives/index.ts b/packages/ui-kit/src/components/primitives/index.ts index bfa750b..c6dc355 100644 --- a/packages/ui-kit/src/components/primitives/index.ts +++ b/packages/ui-kit/src/components/primitives/index.ts @@ -16,3 +16,4 @@ export { DatePicker } from "./DatePicker"; export type { DatePickerProps } from "./DatePicker"; export { DateRangePicker } from "./DateRangePicker"; export type { DateRangePickerProps, DateRange } from "./DateRangePicker"; +export * from "./Kbd"; diff --git a/packages/ui-kit/src/components/ui/AspectRatio/AspectRatio.stories.tsx b/packages/ui-kit/src/components/ui/AspectRatio/AspectRatio.stories.tsx index a09bfb3..f789ce5 100644 --- a/packages/ui-kit/src/components/ui/AspectRatio/AspectRatio.stories.tsx +++ b/packages/ui-kit/src/components/ui/AspectRatio/AspectRatio.stories.tsx @@ -171,7 +171,7 @@ export const CommonRatios = {

16:9 (Widescreen)

16:9 Aspect Ratio @@ -183,7 +183,7 @@ export const CommonRatios = {

4:3 (Traditional)

4:3 Aspect Ratio @@ -196,7 +196,7 @@ export const CommonRatios = {
1:1 Aspect Ratio @@ -209,7 +209,7 @@ export const CommonRatios = {

21:9 (Ultra-wide)

21:9 Aspect Ratio @@ -222,7 +222,7 @@ export const CommonRatios = {
9:16 Aspect Ratio diff --git a/packages/ui-kit/src/components/ui/Badge/Badge.tsx b/packages/ui-kit/src/components/ui/Badge/Badge.tsx index d3d5d60..53df705 100644 --- a/packages/ui-kit/src/components/ui/Badge/Badge.tsx +++ b/packages/ui-kit/src/components/ui/Badge/Badge.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { diff --git a/packages/ui-kit/src/components/ui/Breadcrumb/Breadcrumb.tsx b/packages/ui-kit/src/components/ui/Breadcrumb/Breadcrumb.tsx index acb91b2..98c5c4b 100644 --- a/packages/ui-kit/src/components/ui/Breadcrumb/Breadcrumb.tsx +++ b/packages/ui-kit/src/components/ui/Breadcrumb/Breadcrumb.tsx @@ -19,7 +19,7 @@ const BreadcrumbList = React.forwardRef<
@@ -67,7 +67,7 @@ export const WithFooter: Story = {
@@ -60,7 +60,7 @@ export const Default: Story = {
@@ -80,7 +80,7 @@ export const Default: Story = {
@@ -90,7 +90,7 @@ export const Default: Story = {
diff --git a/packages/ui-kit/src/components/ui/Tabs/Tabs.tsx b/packages/ui-kit/src/components/ui/Tabs/Tabs.tsx index 859eea6..9fb94b9 100644 --- a/packages/ui-kit/src/components/ui/Tabs/Tabs.tsx +++ b/packages/ui-kit/src/components/ui/Tabs/Tabs.tsx @@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef< li]:mt-2", inlineCode: diff --git a/packages/ui-kit/src/components/ui/button-variants.ts b/packages/ui-kit/src/components/ui/button-variants.ts index 241e0dc..3fc05c4 100644 --- a/packages/ui-kit/src/components/ui/button-variants.ts +++ b/packages/ui-kit/src/components/ui/button-variants.ts @@ -1,7 +1,7 @@ import { cva } from "class-variance-authority"; export const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { diff --git a/packages/ui-kit/src/components/ui/button.tsx b/packages/ui-kit/src/components/ui/button.tsx index 5756ade..c2262a9 100644 --- a/packages/ui-kit/src/components/ui/button.tsx +++ b/packages/ui-kit/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "../../utils/cn"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { diff --git a/packages/ui-kit/src/components/ui/carousel.tsx b/packages/ui-kit/src/components/ui/carousel.tsx new file mode 100644 index 0000000..9c2b9bf --- /dev/null +++ b/packages/ui-kit/src/components/ui/carousel.tsx @@ -0,0 +1,260 @@ +import * as React from "react" +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react" +import { ArrowLeft, ArrowRight } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on("reInit", onSelect) + api.on("select", onSelect) + + return () => { + api?.off("select", onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) + } +) +Carousel.displayName = "Carousel" + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) +}) +CarouselContent.displayName = "CarouselContent" + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( +
+ ) +}) +CarouselItem.displayName = "CarouselItem" + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +}) +CarouselPrevious.displayName = "CarouselPrevious" + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +}) +CarouselNext.displayName = "CarouselNext" + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +} diff --git a/packages/ui-kit/src/components/ui/checkbox.tsx b/packages/ui-kit/src/components/ui/checkbox.tsx index 0a6a9a5..0a59de9 100644 --- a/packages/ui-kit/src/components/ui/checkbox.tsx +++ b/packages/ui-kit/src/components/ui/checkbox.tsx @@ -11,7 +11,7 @@ const Checkbox = React.forwardRef< { return ( - + {children} @@ -42,7 +42,7 @@ const CommandInput = React.forwardRef< {children} - + Close diff --git a/packages/ui-kit/src/components/ui/drawer.tsx b/packages/ui-kit/src/components/ui/drawer.tsx new file mode 100644 index 0000000..c17b0cc --- /dev/null +++ b/packages/ui-kit/src/components/ui/drawer.tsx @@ -0,0 +1,116 @@ +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +) +Drawer.displayName = "Drawer" + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)) +DrawerContent.displayName = "DrawerContent" + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerHeader.displayName = "DrawerHeader" + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerFooter.displayName = "DrawerFooter" + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/packages/ui-kit/src/components/ui/index.ts b/packages/ui-kit/src/components/ui/index.ts index 46e9d65..5972fa0 100644 --- a/packages/ui-kit/src/components/ui/index.ts +++ b/packages/ui-kit/src/components/ui/index.ts @@ -29,3 +29,6 @@ export * from "./ScrollArea/ScrollArea"; export * from "./Sidebar/Sidebar"; export * from "./Typography/Typography"; export * from "./Sonner/Sonner"; +export * from "./drawer"; +export * from "./resizable"; +export * from "./carousel"; diff --git a/packages/ui-kit/src/components/ui/input.tsx b/packages/ui-kit/src/components/ui/input.tsx index 69b64fb..9ce1b57 100644 --- a/packages/ui-kit/src/components/ui/input.tsx +++ b/packages/ui-kit/src/components/ui/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef>( ( + +) + +const ResizablePanel = Panel + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: SeparatorProps & { + withHandle?: boolean +}) => ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+) + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/packages/ui-kit/src/components/ui/select.tsx b/packages/ui-kit/src/components/ui/select.tsx index 2ca1df8..7f17a4c 100644 --- a/packages/ui-kit/src/components/ui/select.tsx +++ b/packages/ui-kit/src/components/ui/select.tsx @@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background data-placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className )} {...props} @@ -73,7 +73,7 @@ const SelectContent = React.forwardRef< {children} @@ -116,7 +116,7 @@ const SelectItem = React.forwardRef< diff --git a/packages/ui-kit/src/components/ui/textarea.tsx b/packages/ui-kit/src/components/ui/textarea.tsx index e56b0af..4272c4c 100644 --- a/packages/ui-kit/src/components/ui/textarea.tsx +++ b/packages/ui-kit/src/components/ui/textarea.tsx @@ -9,7 +9,7 @@ const Textarea = React.forwardRef< return (