@rashidee/co2 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- package/static/index.html +24 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
# Routing Patterns — React Router v7 + Feature-Based Architecture (Tailwind)
|
|
2
|
+
|
|
3
|
+
This reference describes the React Router v7 conventions, the feature-based directory
|
|
4
|
+
structure, and the Tailwind/Headless UI layout shell used in the spec. Include this content
|
|
5
|
+
in Sections 4, 7, 11, 12, and 13 of the generated specification.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Feature-Based Directory Structure
|
|
10
|
+
|
|
11
|
+
Each PRD module maps to a `src/features/<module>/` folder. This co-locates everything
|
|
12
|
+
related to a feature (types, API, hooks, components, pages) so the coding agent can
|
|
13
|
+
implement one feature folder at a time.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
src/
|
|
17
|
+
├── main.tsx # App entry — providers, ReactDOM.render
|
|
18
|
+
├── App.tsx # Root component — router, dark-mode sync, notifications
|
|
19
|
+
├── index.css # Tailwind directives + design-token CSS variables
|
|
20
|
+
├── router/
|
|
21
|
+
│ └── index.tsx # Route tree definition
|
|
22
|
+
├── lib/
|
|
23
|
+
│ ├── api/
|
|
24
|
+
│ │ ├── axiosInstance.ts # Axios instance with interceptors
|
|
25
|
+
│ │ └── authBridge.ts # [If Auth = Keycloak] Auth bridge for interceptors
|
|
26
|
+
│ ├── auth/
|
|
27
|
+
│ │ ├── oidcConfig.ts # [If Auth = Keycloak] oidc-client-ts config
|
|
28
|
+
│ │ ├── keycloakRoles.ts # [If Auth = Keycloak] Role extraction utilities
|
|
29
|
+
│ │ ├── useAuthUser.ts # [If Auth = Keycloak] Auth hook wrapper
|
|
30
|
+
│ │ └── roles.ts # Role constants
|
|
31
|
+
│ ├── utils/
|
|
32
|
+
│ │ ├── cn.ts # twMerge(clsx(...)) helper
|
|
33
|
+
│ │ └── zodErrors.ts # Zod error extraction utilities
|
|
34
|
+
│ └── queryClient.ts # TanStack Query QueryClient instance
|
|
35
|
+
├── store/
|
|
36
|
+
│ ├── auth.store.ts # [If Auth = Local] Zustand auth store
|
|
37
|
+
│ ├── notification.store.ts # Zustand toast queue
|
|
38
|
+
│ └── ui.store.ts # Zustand UI store (sidebar, theme mode)
|
|
39
|
+
├── shared/
|
|
40
|
+
│ ├── layouts/
|
|
41
|
+
│ │ ├── DashboardLayout.tsx # Authenticated layout (sidebar + topbar + outlet)
|
|
42
|
+
│ │ ├── PublicLayout.tsx # Public layout (header/footer + outlet)
|
|
43
|
+
│ │ ├── AuthLayout.tsx # Minimal centered layout for login/callback
|
|
44
|
+
│ │ └── Footer.tsx # Footer with version string
|
|
45
|
+
│ ├── components/
|
|
46
|
+
│ │ ├── ProtectedRoute.tsx # Auth + role guard wrapper
|
|
47
|
+
│ │ ├── Button.tsx # cva button
|
|
48
|
+
│ │ ├── Card.tsx # Surface container
|
|
49
|
+
│ │ ├── Spinner.tsx # Inline spinner
|
|
50
|
+
│ │ ├── AppTopBar.tsx # Sticky topbar (toggle, user menu, theme toggle)
|
|
51
|
+
│ │ ├── AppSidebar.tsx # Role-filtered navigation sidebar
|
|
52
|
+
│ │ ├── navigationConfig.ts # Navigation item definitions
|
|
53
|
+
│ │ ├── PageHeader.tsx # Page title + breadcrumbs + action buttons
|
|
54
|
+
│ │ ├── DataTable.tsx # Generic TanStack Table wrapper (sorting/selection)
|
|
55
|
+
│ │ ├── Pagination.tsx # Page navigation control
|
|
56
|
+
│ │ ├── ConfirmDialog.tsx # Headless UI Dialog confirm
|
|
57
|
+
│ │ ├── StatusBadge.tsx # cva status badge
|
|
58
|
+
│ │ ├── Modal.tsx # Headless UI Dialog wrapper for forms
|
|
59
|
+
│ │ ├── ImageUpload.tsx # Drag-and-drop image upload with preview
|
|
60
|
+
│ │ ├── LoadingOverlay.tsx # Full-page spinner overlay
|
|
61
|
+
│ │ ├── EmptyState.tsx # Empty list placeholder with icon + message
|
|
62
|
+
│ │ ├── ErrorBoundary.tsx # React ErrorBoundary with Tailwind fallback UI
|
|
63
|
+
│ │ ├── NotificationProvider.tsx # Headless UI Transition toast stack
|
|
64
|
+
│ │ └── SearchInput.tsx # Debounced search text field
|
|
65
|
+
│ ├── hooks/
|
|
66
|
+
│ │ ├── useDebounce.ts # Generic debounce hook
|
|
67
|
+
│ │ ├── useNotification.ts # Toast notification Zustand hook
|
|
68
|
+
│ │ ├── useColorMode.ts # Dark-mode toggle hook
|
|
69
|
+
│ │ └── useConfirm.ts # Confirm dialog imperative hook
|
|
70
|
+
│ └── form/
|
|
71
|
+
│ ├── TextFieldController.tsx # RHF Controller wrapping a Tailwind <input>
|
|
72
|
+
│ ├── TextAreaController.tsx # RHF Controller wrapping a Tailwind <textarea>
|
|
73
|
+
│ ├── SelectController.tsx # RHF Controller wrapping Headless UI Listbox
|
|
74
|
+
│ ├── DatePickerController.tsx # RHF Controller wrapping react-day-picker
|
|
75
|
+
│ ├── SwitchController.tsx # RHF Controller wrapping Headless UI Switch
|
|
76
|
+
│ ├── ComboboxController.tsx # RHF Controller wrapping Headless UI Combobox
|
|
77
|
+
│ └── RichTextController.tsx # [If RichText = yes] RHF Controller for Tiptap
|
|
78
|
+
├── features/
|
|
79
|
+
│ ├── auth/ # [If Auth != none] Auth feature
|
|
80
|
+
│ │ ├── pages/
|
|
81
|
+
│ │ │ ├── LoginPage.tsx # [If Auth = Local] Login form page
|
|
82
|
+
│ │ │ └── AuthCallbackPage.tsx # [If Auth = Keycloak] OIDC callback handler
|
|
83
|
+
│ │ └── index.ts
|
|
84
|
+
│ ├── {{module1}}/ # PRD module 1 (e.g., heroSection)
|
|
85
|
+
│ │ ├── api/
|
|
86
|
+
│ │ │ └── {{module1}}Api.ts # Axios API functions
|
|
87
|
+
│ │ ├── components/
|
|
88
|
+
│ │ │ ├── {{Module1}}Form.tsx # Create/edit form
|
|
89
|
+
│ │ │ └── {{Module1}}Card.tsx # Card component (if applicable)
|
|
90
|
+
│ │ ├── hooks/
|
|
91
|
+
│ │ │ ├── use{{Module1}}s.ts # useQuery for list
|
|
92
|
+
│ │ │ ├── use{{Module1}}.ts # useQuery for single item
|
|
93
|
+
│ │ │ └── use{{Module1}}Mutations.ts # useMutation for CUD operations
|
|
94
|
+
│ │ ├── pages/
|
|
95
|
+
│ │ │ ├── {{Module1}}ListPage.tsx # List page component (lazy-loaded)
|
|
96
|
+
│ │ │ └── {{Module1}}FormPage.tsx # Create/edit page (lazy-loaded)
|
|
97
|
+
│ │ ├── store/
|
|
98
|
+
│ │ │ └── {{module1}}.store.ts # [If needed] Feature-level Zustand store
|
|
99
|
+
│ │ ├── {{module1}}.types.ts # TypeScript interfaces
|
|
100
|
+
│ │ ├── {{module1}}.schema.ts # Zod validation schemas
|
|
101
|
+
│ │ └── index.ts # Public API re-exports
|
|
102
|
+
│ └── {{module2}}/
|
|
103
|
+
│ └── (same structure)
|
|
104
|
+
└── types/
|
|
105
|
+
└── global.d.ts # Global type augmentations (e.g., import.meta.env)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Route Tree Definition
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
// src/router/index.tsx
|
|
114
|
+
import { lazy, Suspense } from 'react';
|
|
115
|
+
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
|
116
|
+
import { DashboardLayout } from '../shared/layouts/DashboardLayout';
|
|
117
|
+
import { PublicLayout } from '../shared/layouts/PublicLayout';
|
|
118
|
+
import { AuthLayout } from '../shared/layouts/AuthLayout';
|
|
119
|
+
import { ProtectedRoute } from '../shared/components/ProtectedRoute';
|
|
120
|
+
import { LoadingOverlay } from '../shared/components/LoadingOverlay';
|
|
121
|
+
import { Roles } from '../lib/auth/roles';
|
|
122
|
+
|
|
123
|
+
// Lazy-load all page components for code splitting
|
|
124
|
+
// [If Auth = Keycloak]
|
|
125
|
+
const AuthCallbackPage = lazy(() =>
|
|
126
|
+
import('../features/auth/pages/AuthCallbackPage').then((m) => ({ default: m.AuthCallbackPage })),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// [If Auth = Local]
|
|
130
|
+
const LoginPage = lazy(() =>
|
|
131
|
+
import('../features/auth/pages/LoginPage').then((m) => ({ default: m.LoginPage })),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const DashboardPage = lazy(() =>
|
|
135
|
+
import('../features/dashboard/pages/DashboardPage').then((m) => ({ default: m.DashboardPage })),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Hero Section (example — replace with actual modules from PRD)
|
|
139
|
+
const HeroSectionListPage = lazy(() =>
|
|
140
|
+
import('../features/heroSection/pages/HeroSectionListPage').then((m) => ({
|
|
141
|
+
default: m.HeroSectionListPage,
|
|
142
|
+
})),
|
|
143
|
+
);
|
|
144
|
+
const HeroSectionFormPage = lazy(() =>
|
|
145
|
+
import('../features/heroSection/pages/HeroSectionFormPage').then((m) => ({
|
|
146
|
+
default: m.HeroSectionFormPage,
|
|
147
|
+
})),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
function SuspenseWrapper({ children }: { children: React.ReactNode }) {
|
|
151
|
+
return <Suspense fallback={<LoadingOverlay />}>{children}</Suspense>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const router = createBrowserRouter([
|
|
155
|
+
// ── Auth routes (public) ────────────────────────────────────────────
|
|
156
|
+
{
|
|
157
|
+
element: <AuthLayout />,
|
|
158
|
+
children: [
|
|
159
|
+
// [If Auth = Keycloak]
|
|
160
|
+
{
|
|
161
|
+
path: '/auth/callback',
|
|
162
|
+
element: (
|
|
163
|
+
<SuspenseWrapper>
|
|
164
|
+
<AuthCallbackPage />
|
|
165
|
+
</SuspenseWrapper>
|
|
166
|
+
),
|
|
167
|
+
},
|
|
168
|
+
// [If Auth = Local]
|
|
169
|
+
{
|
|
170
|
+
path: '/login',
|
|
171
|
+
element: (
|
|
172
|
+
<SuspenseWrapper>
|
|
173
|
+
<LoginPage />
|
|
174
|
+
</SuspenseWrapper>
|
|
175
|
+
),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// ── Protected / dashboard routes ────────────────────────────────────
|
|
181
|
+
{
|
|
182
|
+
element: (
|
|
183
|
+
<ProtectedRoute>
|
|
184
|
+
<DashboardLayout />
|
|
185
|
+
</ProtectedRoute>
|
|
186
|
+
),
|
|
187
|
+
children: [
|
|
188
|
+
{ index: true, element: <Navigate to="/dashboard" replace /> },
|
|
189
|
+
{
|
|
190
|
+
path: '/dashboard',
|
|
191
|
+
element: (
|
|
192
|
+
<SuspenseWrapper>
|
|
193
|
+
<DashboardPage />
|
|
194
|
+
</SuspenseWrapper>
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// Hero Section — accessible by ADMIN and EDITOR (module-based path, NOT role-prefixed)
|
|
199
|
+
{
|
|
200
|
+
path: '/hero-section',
|
|
201
|
+
element: (
|
|
202
|
+
<ProtectedRoute requiredRoles={[Roles.ADMIN, Roles.EDITOR]}>
|
|
203
|
+
<SuspenseWrapper>
|
|
204
|
+
<HeroSectionListPage />
|
|
205
|
+
</SuspenseWrapper>
|
|
206
|
+
</ProtectedRoute>
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
path: '/hero-section/create',
|
|
211
|
+
element: (
|
|
212
|
+
<ProtectedRoute requiredRoles={[Roles.ADMIN, Roles.EDITOR]}>
|
|
213
|
+
<SuspenseWrapper>
|
|
214
|
+
<HeroSectionFormPage />
|
|
215
|
+
</SuspenseWrapper>
|
|
216
|
+
</ProtectedRoute>
|
|
217
|
+
),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
path: '/hero-section/:id/edit',
|
|
221
|
+
element: (
|
|
222
|
+
<ProtectedRoute requiredRoles={[Roles.ADMIN, Roles.EDITOR]}>
|
|
223
|
+
<SuspenseWrapper>
|
|
224
|
+
<HeroSectionFormPage />
|
|
225
|
+
</SuspenseWrapper>
|
|
226
|
+
</ProtectedRoute>
|
|
227
|
+
),
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// Add additional module routes following the same pattern...
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// ── Error routes ────────────────────────────────────────────────────
|
|
235
|
+
{
|
|
236
|
+
path: '/unauthorized',
|
|
237
|
+
element: <div className="p-8 text-slate-700 dark:text-slate-200">You do not have permission to access this page.</div>,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
path: '*',
|
|
241
|
+
element: <div className="p-8 text-slate-700 dark:text-slate-200">404 — Page not found.</div>,
|
|
242
|
+
},
|
|
243
|
+
]);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## DashboardLayout
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
// src/shared/layouts/DashboardLayout.tsx
|
|
252
|
+
import { Outlet } from 'react-router-dom';
|
|
253
|
+
import { AppTopBar } from '../components/AppTopBar';
|
|
254
|
+
import { AppSidebar } from '../components/AppSidebar';
|
|
255
|
+
import { Footer } from './Footer';
|
|
256
|
+
import { useUiStore } from '../../store/ui.store';
|
|
257
|
+
import { cn } from '../../lib/utils/cn';
|
|
258
|
+
|
|
259
|
+
export function DashboardLayout() {
|
|
260
|
+
const sidebarOpen = useUiStore((s) => s.sidebarOpen);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div className="min-h-screen bg-canvas">
|
|
264
|
+
<AppTopBar />
|
|
265
|
+
<AppSidebar />
|
|
266
|
+
<div
|
|
267
|
+
className={cn(
|
|
268
|
+
'flex min-h-screen flex-col pt-16 transition-[padding] duration-200',
|
|
269
|
+
sidebarOpen ? 'lg:pl-60' : 'lg:pl-0',
|
|
270
|
+
)}
|
|
271
|
+
>
|
|
272
|
+
<main className="flex-1 p-4 sm:p-6">
|
|
273
|
+
<Outlet />
|
|
274
|
+
</main>
|
|
275
|
+
<Footer />
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
// src/shared/layouts/Footer.tsx
|
|
284
|
+
export function Footer() {
|
|
285
|
+
const version = import.meta.env.VITE_APP_VERSION;
|
|
286
|
+
return (
|
|
287
|
+
<footer className="border-t border-slate-200 px-6 py-3 text-center text-xs text-slate-500 dark:border-slate-700">
|
|
288
|
+
© {new Date().getFullYear()} {import.meta.env.VITE_APP_TITLE} — v{version}
|
|
289
|
+
</footer>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## AppTopBar (Headless UI Menu user dropdown + theme toggle)
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
// src/shared/components/AppTopBar.tsx
|
|
300
|
+
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
|
|
301
|
+
import { Bars3Icon, UserCircleIcon, ArrowRightStartOnRectangleIcon } from '@heroicons/react/24/outline';
|
|
302
|
+
import { useUiStore } from '../../store/ui.store';
|
|
303
|
+
import { useAuthUser } from '../../lib/auth/useAuthUser';
|
|
304
|
+
import { ThemeToggle } from './ThemeToggle';
|
|
305
|
+
import { cn } from '../../lib/utils/cn';
|
|
306
|
+
|
|
307
|
+
export function AppTopBar() {
|
|
308
|
+
const toggleSidebar = useUiStore((s) => s.toggleSidebar);
|
|
309
|
+
const { name, logout } = useAuthUser();
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<header className="fixed inset-x-0 top-0 z-30 flex h-16 items-center justify-between border-b border-slate-200 bg-surface px-4 dark:border-slate-700">
|
|
313
|
+
<div className="flex items-center gap-3">
|
|
314
|
+
<button
|
|
315
|
+
type="button"
|
|
316
|
+
onClick={toggleSidebar}
|
|
317
|
+
aria-label="Toggle navigation"
|
|
318
|
+
className="rounded p-2 text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700"
|
|
319
|
+
>
|
|
320
|
+
<Bars3Icon className="h-5 w-5" />
|
|
321
|
+
</button>
|
|
322
|
+
<span className="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
|
323
|
+
{import.meta.env.VITE_APP_TITLE}
|
|
324
|
+
</span>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div className="flex items-center gap-2">
|
|
328
|
+
<ThemeToggle />
|
|
329
|
+
<Menu>
|
|
330
|
+
<MenuButton className="flex items-center gap-2 rounded p-1.5 text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-700">
|
|
331
|
+
<UserCircleIcon className="h-6 w-6" />
|
|
332
|
+
<span className="hidden text-sm sm:inline">{name}</span>
|
|
333
|
+
</MenuButton>
|
|
334
|
+
<MenuItems
|
|
335
|
+
anchor="bottom end"
|
|
336
|
+
className="z-50 mt-1 w-48 rounded border border-slate-200 bg-surface py-1 text-sm shadow-lg focus:outline-none dark:border-slate-700"
|
|
337
|
+
>
|
|
338
|
+
<MenuItem>
|
|
339
|
+
<button
|
|
340
|
+
onClick={logout}
|
|
341
|
+
className={cn(
|
|
342
|
+
'flex w-full items-center gap-2 px-3 py-2 text-left text-slate-700 dark:text-slate-200',
|
|
343
|
+
'data-[focus]:bg-slate-100 dark:data-[focus]:bg-slate-700',
|
|
344
|
+
)}
|
|
345
|
+
>
|
|
346
|
+
<ArrowRightStartOnRectangleIcon className="h-4 w-4" />
|
|
347
|
+
Sign out
|
|
348
|
+
</button>
|
|
349
|
+
</MenuItem>
|
|
350
|
+
</MenuItems>
|
|
351
|
+
</Menu>
|
|
352
|
+
</div>
|
|
353
|
+
</header>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Sidebar Navigation
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// src/shared/components/navigationConfig.ts
|
|
364
|
+
import { HomeIcon, PhotoIcon, UsersIcon } from '@heroicons/react/24/outline';
|
|
365
|
+
import type { ComponentType, SVGProps } from 'react';
|
|
366
|
+
import { Roles } from '../../lib/auth/roles';
|
|
367
|
+
|
|
368
|
+
export interface NavItem {
|
|
369
|
+
label: string;
|
|
370
|
+
path: string;
|
|
371
|
+
icon: ComponentType<SVGProps<SVGSVGElement>>;
|
|
372
|
+
/** If provided, user must have at least one of these roles to see this item */
|
|
373
|
+
requiredRoles?: string[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* All sidebar navigation items.
|
|
378
|
+
* Items with requiredRoles are hidden for users who lack those roles.
|
|
379
|
+
* Replace with actual modules from the PRD.md (module-based paths only).
|
|
380
|
+
*/
|
|
381
|
+
export const navigationItems: NavItem[] = [
|
|
382
|
+
{ label: 'Dashboard', path: '/dashboard', icon: HomeIcon },
|
|
383
|
+
{ label: 'Hero Section', path: '/hero-section', icon: PhotoIcon, requiredRoles: [Roles.ADMIN, Roles.EDITOR] },
|
|
384
|
+
{ label: 'Users', path: '/users', icon: UsersIcon, requiredRoles: [Roles.ADMIN] },
|
|
385
|
+
// Add additional nav items matching the mockup sidebar files...
|
|
386
|
+
];
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
// src/shared/components/AppSidebar.tsx
|
|
391
|
+
import { NavLink } from 'react-router-dom';
|
|
392
|
+
import { useAuthUser } from '../../lib/auth/useAuthUser';
|
|
393
|
+
import { useUiStore } from '../../store/ui.store';
|
|
394
|
+
import { navigationItems } from './navigationConfig';
|
|
395
|
+
import { cn } from '../../lib/utils/cn';
|
|
396
|
+
|
|
397
|
+
export function AppSidebar() {
|
|
398
|
+
const { hasAnyRole } = useAuthUser();
|
|
399
|
+
const sidebarOpen = useUiStore((s) => s.sidebarOpen);
|
|
400
|
+
|
|
401
|
+
const visibleItems = navigationItems.filter(
|
|
402
|
+
(item) => !item.requiredRoles || hasAnyRole(item.requiredRoles),
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<aside
|
|
407
|
+
className={cn(
|
|
408
|
+
'fixed inset-y-0 left-0 z-20 w-60 border-r border-slate-200 bg-surface pt-16 transition-transform duration-200 dark:border-slate-700',
|
|
409
|
+
sidebarOpen ? 'translate-x-0' : '-translate-x-full',
|
|
410
|
+
)}
|
|
411
|
+
>
|
|
412
|
+
<nav className="flex flex-col gap-1 p-3">
|
|
413
|
+
{visibleItems.map((item) => {
|
|
414
|
+
const Icon = item.icon;
|
|
415
|
+
return (
|
|
416
|
+
<NavLink
|
|
417
|
+
key={item.path}
|
|
418
|
+
to={item.path}
|
|
419
|
+
className={({ isActive }) =>
|
|
420
|
+
cn(
|
|
421
|
+
'flex items-center gap-3 rounded px-3 py-2 text-sm font-medium transition-colors',
|
|
422
|
+
isActive
|
|
423
|
+
? 'bg-primary/10 text-primary'
|
|
424
|
+
: 'text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-700',
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
>
|
|
428
|
+
<Icon className="h-5 w-5 shrink-0" />
|
|
429
|
+
{item.label}
|
|
430
|
+
</NavLink>
|
|
431
|
+
);
|
|
432
|
+
})}
|
|
433
|
+
</nav>
|
|
434
|
+
</aside>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## PageHeader (breadcrumbs)
|
|
442
|
+
|
|
443
|
+
```tsx
|
|
444
|
+
// src/shared/components/PageHeader.tsx
|
|
445
|
+
import { Link } from 'react-router-dom';
|
|
446
|
+
import { ChevronRightIcon } from '@heroicons/react/20/solid';
|
|
447
|
+
|
|
448
|
+
export interface BreadcrumbItem {
|
|
449
|
+
label: string;
|
|
450
|
+
path?: string;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
interface PageHeaderProps {
|
|
454
|
+
title: string;
|
|
455
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
456
|
+
actions?: React.ReactNode;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function PageHeader({ title, breadcrumbs, actions }: PageHeaderProps) {
|
|
460
|
+
return (
|
|
461
|
+
<div className="mb-6">
|
|
462
|
+
{breadcrumbs && (
|
|
463
|
+
<nav className="mb-1 flex items-center gap-1 text-xs text-slate-500" aria-label="Breadcrumb">
|
|
464
|
+
{breadcrumbs.map((crumb, index) => (
|
|
465
|
+
<span key={index} className="flex items-center gap-1">
|
|
466
|
+
{index > 0 && <ChevronRightIcon className="h-3.5 w-3.5 text-slate-400" />}
|
|
467
|
+
{crumb.path ? (
|
|
468
|
+
<Link to={crumb.path} className="hover:text-primary hover:underline">
|
|
469
|
+
{crumb.label}
|
|
470
|
+
</Link>
|
|
471
|
+
) : (
|
|
472
|
+
<span className="text-slate-700 dark:text-slate-300">{crumb.label}</span>
|
|
473
|
+
)}
|
|
474
|
+
</span>
|
|
475
|
+
))}
|
|
476
|
+
</nav>
|
|
477
|
+
)}
|
|
478
|
+
<div className="flex items-center justify-between gap-3">
|
|
479
|
+
<h1 className="text-xl font-semibold text-slate-900 dark:text-slate-100">{title}</h1>
|
|
480
|
+
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Pagination
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
// src/shared/components/Pagination.tsx
|
|
493
|
+
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid';
|
|
494
|
+
import { Button } from './Button';
|
|
495
|
+
|
|
496
|
+
interface PaginationProps {
|
|
497
|
+
/** Zero-based current page index */
|
|
498
|
+
page: number;
|
|
499
|
+
size: number;
|
|
500
|
+
totalElements: number;
|
|
501
|
+
onPageChange: (page: number) => void;
|
|
502
|
+
onSizeChange?: (size: number) => void;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function Pagination({ page, size, totalElements, onPageChange, onSizeChange }: PaginationProps) {
|
|
506
|
+
const totalPages = Math.max(1, Math.ceil(totalElements / size));
|
|
507
|
+
const from = totalElements === 0 ? 0 : page * size + 1;
|
|
508
|
+
const to = Math.min((page + 1) * size, totalElements);
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 px-4 py-3 text-sm dark:border-slate-700">
|
|
512
|
+
<p className="text-slate-600 dark:text-slate-300">
|
|
513
|
+
Showing <span className="font-medium">{from}</span>–<span className="font-medium">{to}</span> of{' '}
|
|
514
|
+
<span className="font-medium">{totalElements}</span>
|
|
515
|
+
</p>
|
|
516
|
+
<div className="flex items-center gap-3">
|
|
517
|
+
{onSizeChange && (
|
|
518
|
+
<select
|
|
519
|
+
value={size}
|
|
520
|
+
onChange={(e) => onSizeChange(Number(e.target.value))}
|
|
521
|
+
className="rounded border border-slate-300 bg-surface px-2 py-1 text-sm focus:border-primary focus:ring-1 focus:ring-primary dark:border-slate-600"
|
|
522
|
+
aria-label="Rows per page"
|
|
523
|
+
>
|
|
524
|
+
{[10, 25, 50].map((n) => (
|
|
525
|
+
<option key={n} value={n}>
|
|
526
|
+
{n} / page
|
|
527
|
+
</option>
|
|
528
|
+
))}
|
|
529
|
+
</select>
|
|
530
|
+
)}
|
|
531
|
+
<div className="flex items-center gap-1">
|
|
532
|
+
<Button variant="outline" size="icon" disabled={page <= 0} onClick={() => onPageChange(page - 1)} aria-label="Previous page">
|
|
533
|
+
<ChevronLeftIcon className="h-4 w-4" />
|
|
534
|
+
</Button>
|
|
535
|
+
<span className="px-2 text-slate-600 dark:text-slate-300">
|
|
536
|
+
{page + 1} / {totalPages}
|
|
537
|
+
</span>
|
|
538
|
+
<Button variant="outline" size="icon" disabled={page + 1 >= totalPages} onClick={() => onPageChange(page + 1)} aria-label="Next page">
|
|
539
|
+
<ChevronRightIcon className="h-4 w-4" />
|
|
540
|
+
</Button>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## DataTable (TanStack Table wrapper) [Include when DataGrid = yes]
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
553
|
+
// src/shared/components/DataTable.tsx
|
|
554
|
+
import {
|
|
555
|
+
flexRender,
|
|
556
|
+
getCoreRowModel,
|
|
557
|
+
getSortedRowModel,
|
|
558
|
+
useReactTable,
|
|
559
|
+
type ColumnDef,
|
|
560
|
+
type SortingState,
|
|
561
|
+
} from '@tanstack/react-table';
|
|
562
|
+
import { useState } from 'react';
|
|
563
|
+
import { ChevronUpDownIcon } from '@heroicons/react/20/solid';
|
|
564
|
+
import { cn } from '../../lib/utils/cn';
|
|
565
|
+
|
|
566
|
+
interface DataTableProps<TData> {
|
|
567
|
+
columns: ColumnDef<TData, unknown>[];
|
|
568
|
+
data: TData[];
|
|
569
|
+
/** Optional empty-state message */
|
|
570
|
+
emptyMessage?: string;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function DataTable<TData>({ columns, data, emptyMessage = 'No records found.' }: DataTableProps<TData>) {
|
|
574
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
575
|
+
|
|
576
|
+
const table = useReactTable({
|
|
577
|
+
data,
|
|
578
|
+
columns,
|
|
579
|
+
state: { sorting },
|
|
580
|
+
onSortingChange: setSorting,
|
|
581
|
+
getCoreRowModel: getCoreRowModel(),
|
|
582
|
+
getSortedRowModel: getSortedRowModel(),
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
<div className="overflow-x-auto">
|
|
587
|
+
<table className="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
|
|
588
|
+
<thead className="bg-canvas">
|
|
589
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
590
|
+
<tr key={headerGroup.id}>
|
|
591
|
+
{headerGroup.headers.map((header) => {
|
|
592
|
+
const canSort = header.column.getCanSort();
|
|
593
|
+
return (
|
|
594
|
+
<th
|
|
595
|
+
key={header.id}
|
|
596
|
+
className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-600 dark:text-slate-300"
|
|
597
|
+
>
|
|
598
|
+
<button
|
|
599
|
+
type="button"
|
|
600
|
+
disabled={!canSort}
|
|
601
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
602
|
+
className={cn('flex items-center gap-1', canSort && 'cursor-pointer select-none')}
|
|
603
|
+
>
|
|
604
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
605
|
+
{canSort && <ChevronUpDownIcon className="h-3.5 w-3.5 text-slate-400" />}
|
|
606
|
+
</button>
|
|
607
|
+
</th>
|
|
608
|
+
);
|
|
609
|
+
})}
|
|
610
|
+
</tr>
|
|
611
|
+
))}
|
|
612
|
+
</thead>
|
|
613
|
+
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
|
614
|
+
{table.getRowModel().rows.length === 0 ? (
|
|
615
|
+
<tr>
|
|
616
|
+
<td colSpan={columns.length} className="px-4 py-8 text-center text-sm text-slate-500">
|
|
617
|
+
{emptyMessage}
|
|
618
|
+
</td>
|
|
619
|
+
</tr>
|
|
620
|
+
) : (
|
|
621
|
+
table.getRowModel().rows.map((row) => (
|
|
622
|
+
<tr key={row.id} className="hover:bg-canvas/60">
|
|
623
|
+
{row.getVisibleCells().map((cell) => (
|
|
624
|
+
<td key={cell.id} className="px-4 py-3 text-sm text-slate-700 dark:text-slate-200">
|
|
625
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
626
|
+
</td>
|
|
627
|
+
))}
|
|
628
|
+
</tr>
|
|
629
|
+
))
|
|
630
|
+
)}
|
|
631
|
+
</tbody>
|
|
632
|
+
</table>
|
|
633
|
+
</div>
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## LoadingOverlay & EmptyState
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
// src/shared/components/LoadingOverlay.tsx
|
|
644
|
+
import { Spinner } from './Spinner';
|
|
645
|
+
|
|
646
|
+
export function LoadingOverlay({ message }: { message?: string }) {
|
|
647
|
+
return (
|
|
648
|
+
<div className="flex min-h-[40vh] flex-col items-center justify-center gap-3 text-slate-500">
|
|
649
|
+
<Spinner className="h-8 w-8 text-primary" />
|
|
650
|
+
{message && <p className="text-sm">{message}</p>}
|
|
651
|
+
</div>
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
// src/shared/components/EmptyState.tsx
|
|
658
|
+
import { InboxIcon } from '@heroicons/react/24/outline';
|
|
659
|
+
|
|
660
|
+
export function EmptyState({ message, action }: { message: string; action?: React.ReactNode }) {
|
|
661
|
+
return (
|
|
662
|
+
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center text-slate-500">
|
|
663
|
+
<InboxIcon className="h-10 w-10 text-slate-300" />
|
|
664
|
+
<p className="text-sm">{message}</p>
|
|
665
|
+
{action}
|
|
666
|
+
</div>
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
```
|