@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,1467 @@
|
|
|
1
|
+
# Admin Layout Architecture — Vite + React 19 + shadcn/ui
|
|
2
|
+
|
|
3
|
+
The mockup application uses **Vite** as the build tool (dev server for design iteration
|
|
4
|
+
only), **React 19** with **TypeScript** for UI components, **React Router v7** for
|
|
5
|
+
client-side navigation, **shadcn/ui** (Radix UI + Tailwind CSS) for the component library,
|
|
6
|
+
**Lucide React** for icons, and **next-themes** for dark mode support.
|
|
7
|
+
|
|
8
|
+
The app is generated with `base`/`basename` = `/{{APP_SLUG}}/` and **built**
|
|
9
|
+
(`npm run build`); the shared **Mockup Hub** (`<root>/mockup/server.js` — see
|
|
10
|
+
[mockup-hub-template.md](mockup-hub-template.md)) serves the `dist/` output at
|
|
11
|
+
`/{{APP_SLUG}}/...` with SPA fallback. `{{APP_SLUG}}` is replaced at generation time with
|
|
12
|
+
the app folder name minus its leading `<number>_` prefix.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## File Structure
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
mockup/
|
|
20
|
+
.gitignore
|
|
21
|
+
mockup-manifest.json # Hub discovery manifest (see mockup-hub-template.md)
|
|
22
|
+
dist/ # Built output served by the hub (npm run build; gitignored)
|
|
23
|
+
package.json
|
|
24
|
+
vite.config.ts
|
|
25
|
+
tsconfig.json
|
|
26
|
+
tsconfig.app.json
|
|
27
|
+
tsconfig.node.json
|
|
28
|
+
tailwind.config.js
|
|
29
|
+
postcss.config.js
|
|
30
|
+
components.json
|
|
31
|
+
index.html
|
|
32
|
+
MOCKUP.html
|
|
33
|
+
public/
|
|
34
|
+
reports/ # Report layouts (if applicable)
|
|
35
|
+
src/
|
|
36
|
+
main.tsx
|
|
37
|
+
App.tsx
|
|
38
|
+
index.css
|
|
39
|
+
lib/
|
|
40
|
+
utils.ts
|
|
41
|
+
components/
|
|
42
|
+
ui/
|
|
43
|
+
button.tsx
|
|
44
|
+
card.tsx
|
|
45
|
+
table.tsx
|
|
46
|
+
badge.tsx
|
|
47
|
+
input.tsx
|
|
48
|
+
label.tsx
|
|
49
|
+
select.tsx
|
|
50
|
+
dialog.tsx
|
|
51
|
+
dropdown-menu.tsx
|
|
52
|
+
avatar.tsx
|
|
53
|
+
separator.tsx
|
|
54
|
+
tabs.tsx
|
|
55
|
+
switch.tsx
|
|
56
|
+
tooltip.tsx
|
|
57
|
+
breadcrumb.tsx
|
|
58
|
+
pagination.tsx
|
|
59
|
+
sheet.tsx
|
|
60
|
+
layout/
|
|
61
|
+
app-layout.tsx
|
|
62
|
+
app-header.tsx
|
|
63
|
+
app-sidebar.tsx
|
|
64
|
+
app-footer.tsx
|
|
65
|
+
sidebar-config.ts
|
|
66
|
+
pages/
|
|
67
|
+
{role-kebab-case}/
|
|
68
|
+
home.tsx
|
|
69
|
+
profile.tsx
|
|
70
|
+
account.tsx
|
|
71
|
+
notifications.tsx
|
|
72
|
+
{module-kebab-case}.tsx
|
|
73
|
+
{module-kebab-case}-detail.tsx
|
|
74
|
+
{module-kebab-case}-create.tsx
|
|
75
|
+
{module-kebab-case}-edit.tsx
|
|
76
|
+
...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Placeholders Reference
|
|
82
|
+
|
|
83
|
+
| Placeholder | Description |
|
|
84
|
+
|-------------|-------------|
|
|
85
|
+
| `{{APP_NAME}}` | Application name (e.g., "Hub Middleware") |
|
|
86
|
+
| `{{APP_SLUG}}` | App slug used as URL base path (e.g., "hub_middleware") — replaced at generation time |
|
|
87
|
+
| `{{APP_INITIALS}}` | Application initials (e.g., "HM") |
|
|
88
|
+
| `{{PAGE_TITLE}}` | Screen title (e.g., "Location Information") |
|
|
89
|
+
| `{{COPYRIGHT_YEAR}}` | Current year |
|
|
90
|
+
| `{{VERSION}}` | Latest version from PRD.md |
|
|
91
|
+
| `{{DESIGN_PRIMARY}}` | Primary color hex from design system |
|
|
92
|
+
| `{{DESIGN_SECONDARY}}` | Secondary color hex from design system |
|
|
93
|
+
| `{{DESIGN_ACCENT}}` | Accent/CTA color hex |
|
|
94
|
+
| `{{HEADING_FONT}}` | Heading font family |
|
|
95
|
+
| `{{BODY_FONT}}` | Body font family |
|
|
96
|
+
| `{{GOOGLE_FONTS_URL}}` | Google Fonts import URL |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## package.json Template
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"name": "{{APP_SLUG}}-mockup",
|
|
105
|
+
"private": true,
|
|
106
|
+
"version": "1.0.0",
|
|
107
|
+
"type": "module",
|
|
108
|
+
"description": "UI Mockup (React + shadcn/ui) — generated by mockgen-shadcn; built dist/ is served by the Mockup Hub at <root>/mockup",
|
|
109
|
+
"scripts": {
|
|
110
|
+
"dev": "vite",
|
|
111
|
+
"build": "tsc -b && vite build",
|
|
112
|
+
"preview": "vite preview"
|
|
113
|
+
},
|
|
114
|
+
"dependencies": {
|
|
115
|
+
"react": "^19.0.0",
|
|
116
|
+
"react-dom": "^19.0.0",
|
|
117
|
+
"react-router-dom": "^7.1.0",
|
|
118
|
+
"lucide-react": "^0.468.0",
|
|
119
|
+
"next-themes": "^0.4.4",
|
|
120
|
+
"@radix-ui/react-avatar": "^1.1.2",
|
|
121
|
+
"@radix-ui/react-dialog": "^1.1.4",
|
|
122
|
+
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
|
123
|
+
"@radix-ui/react-label": "^2.1.1",
|
|
124
|
+
"@radix-ui/react-select": "^2.1.4",
|
|
125
|
+
"@radix-ui/react-separator": "^1.1.1",
|
|
126
|
+
"@radix-ui/react-switch": "^1.1.2",
|
|
127
|
+
"@radix-ui/react-tabs": "^1.1.2",
|
|
128
|
+
"@radix-ui/react-tooltip": "^1.1.6",
|
|
129
|
+
"@radix-ui/react-slot": "^1.1.1",
|
|
130
|
+
"class-variance-authority": "^0.7.1",
|
|
131
|
+
"clsx": "^2.1.1",
|
|
132
|
+
"tailwind-merge": "^2.6.0"
|
|
133
|
+
},
|
|
134
|
+
"devDependencies": {
|
|
135
|
+
"@types/react": "^19.0.0",
|
|
136
|
+
"@types/react-dom": "^19.0.0",
|
|
137
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
138
|
+
"autoprefixer": "^10.4.20",
|
|
139
|
+
"postcss": "^8.4.49",
|
|
140
|
+
"tailwindcss": "^3.4.17",
|
|
141
|
+
"typescript": "~5.7.0",
|
|
142
|
+
"vite": "^6.0.0"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## vite.config.ts Template
|
|
150
|
+
|
|
151
|
+
The `base` option is REQUIRED — it makes the built app servable by the Mockup Hub at
|
|
152
|
+
`/{{APP_SLUG}}/` (and the dev server serve at `http://localhost:5173/{{APP_SLUG}}/`).
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { defineConfig } from "vite";
|
|
156
|
+
import react from "@vitejs/plugin-react";
|
|
157
|
+
import path from "path";
|
|
158
|
+
|
|
159
|
+
export default defineConfig({
|
|
160
|
+
base: "/{{APP_SLUG}}/",
|
|
161
|
+
plugins: [react()],
|
|
162
|
+
resolve: {
|
|
163
|
+
alias: {
|
|
164
|
+
"@": path.resolve(__dirname, "./src"),
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## tsconfig.json Template
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"files": [],
|
|
177
|
+
"references": [
|
|
178
|
+
{ "path": "./tsconfig.app.json" },
|
|
179
|
+
{ "path": "./tsconfig.node.json" }
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## tsconfig.app.json Template
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"compilerOptions": {
|
|
189
|
+
"target": "ES2020",
|
|
190
|
+
"useDefineForClassFields": true,
|
|
191
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
192
|
+
"module": "ESNext",
|
|
193
|
+
"skipLibCheck": true,
|
|
194
|
+
"moduleResolution": "bundler",
|
|
195
|
+
"allowImportingTsExtensions": true,
|
|
196
|
+
"isolatedModules": true,
|
|
197
|
+
"moduleDetection": "force",
|
|
198
|
+
"noEmit": true,
|
|
199
|
+
"jsx": "react-jsx",
|
|
200
|
+
"strict": true,
|
|
201
|
+
"noUnusedLocals": false,
|
|
202
|
+
"noUnusedParameters": false,
|
|
203
|
+
"noFallthroughCasesInSwitch": true,
|
|
204
|
+
"noUncheckedSideEffectImports": true,
|
|
205
|
+
"baseUrl": ".",
|
|
206
|
+
"paths": {
|
|
207
|
+
"@/*": ["./src/*"]
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"include": ["src"]
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## tsconfig.node.json Template
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"compilerOptions": {
|
|
219
|
+
"target": "ES2022",
|
|
220
|
+
"lib": ["ES2023"],
|
|
221
|
+
"module": "ESNext",
|
|
222
|
+
"skipLibCheck": true,
|
|
223
|
+
"moduleResolution": "bundler",
|
|
224
|
+
"allowImportingTsExtensions": true,
|
|
225
|
+
"isolatedModules": true,
|
|
226
|
+
"moduleDetection": "force",
|
|
227
|
+
"noEmit": true,
|
|
228
|
+
"strict": true,
|
|
229
|
+
"noUnusedLocals": false,
|
|
230
|
+
"noUnusedParameters": false,
|
|
231
|
+
"noFallthroughCasesInSwitch": true,
|
|
232
|
+
"noUncheckedSideEffectImports": true
|
|
233
|
+
},
|
|
234
|
+
"include": ["vite.config.ts"]
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## tailwind.config.js Template
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
/** @type {import('tailwindcss').Config} */
|
|
244
|
+
export default {
|
|
245
|
+
darkMode: ["class"],
|
|
246
|
+
content: [
|
|
247
|
+
"./index.html",
|
|
248
|
+
"./src/**/*.{ts,tsx}",
|
|
249
|
+
],
|
|
250
|
+
theme: {
|
|
251
|
+
extend: {
|
|
252
|
+
colors: {
|
|
253
|
+
border: "hsl(var(--border))",
|
|
254
|
+
input: "hsl(var(--input))",
|
|
255
|
+
ring: "hsl(var(--ring))",
|
|
256
|
+
background: "hsl(var(--background))",
|
|
257
|
+
foreground: "hsl(var(--foreground))",
|
|
258
|
+
primary: {
|
|
259
|
+
DEFAULT: "hsl(var(--primary))",
|
|
260
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
261
|
+
},
|
|
262
|
+
secondary: {
|
|
263
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
264
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
265
|
+
},
|
|
266
|
+
destructive: {
|
|
267
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
268
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
269
|
+
},
|
|
270
|
+
muted: {
|
|
271
|
+
DEFAULT: "hsl(var(--muted))",
|
|
272
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
273
|
+
},
|
|
274
|
+
accent: {
|
|
275
|
+
DEFAULT: "hsl(var(--accent))",
|
|
276
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
277
|
+
},
|
|
278
|
+
popover: {
|
|
279
|
+
DEFAULT: "hsl(var(--popover))",
|
|
280
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
281
|
+
},
|
|
282
|
+
card: {
|
|
283
|
+
DEFAULT: "hsl(var(--card))",
|
|
284
|
+
foreground: "hsl(var(--card-foreground))",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
borderRadius: {
|
|
288
|
+
lg: "var(--radius)",
|
|
289
|
+
md: "calc(var(--radius) - 2px)",
|
|
290
|
+
sm: "calc(var(--radius) - 4px)",
|
|
291
|
+
},
|
|
292
|
+
fontFamily: {
|
|
293
|
+
heading: ["{{HEADING_FONT}}", "sans-serif"],
|
|
294
|
+
body: ["{{BODY_FONT}}", "sans-serif"],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
plugins: [],
|
|
299
|
+
};
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## postcss.config.js Template
|
|
305
|
+
|
|
306
|
+
```javascript
|
|
307
|
+
export default {
|
|
308
|
+
plugins: {
|
|
309
|
+
tailwindcss: {},
|
|
310
|
+
autoprefixer: {},
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## components.json Template (shadcn/ui config)
|
|
318
|
+
|
|
319
|
+
```json
|
|
320
|
+
{
|
|
321
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
322
|
+
"style": "new-york",
|
|
323
|
+
"rsc": false,
|
|
324
|
+
"tsx": true,
|
|
325
|
+
"tailwind": {
|
|
326
|
+
"config": "tailwind.config.js",
|
|
327
|
+
"css": "src/index.css",
|
|
328
|
+
"baseColor": "zinc",
|
|
329
|
+
"cssVariables": true
|
|
330
|
+
},
|
|
331
|
+
"aliases": {
|
|
332
|
+
"components": "@/components",
|
|
333
|
+
"utils": "@/lib/utils",
|
|
334
|
+
"ui": "@/components/ui"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## index.html Template (Vite entry)
|
|
342
|
+
|
|
343
|
+
```html
|
|
344
|
+
<!DOCTYPE html>
|
|
345
|
+
<html lang="en">
|
|
346
|
+
<head>
|
|
347
|
+
<meta charset="UTF-8" />
|
|
348
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
349
|
+
<title>{{APP_NAME}}</title>
|
|
350
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
351
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
352
|
+
<link href="{{GOOGLE_FONTS_URL}}" rel="stylesheet" />
|
|
353
|
+
</head>
|
|
354
|
+
<body>
|
|
355
|
+
<div id="root"></div>
|
|
356
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
357
|
+
</body>
|
|
358
|
+
</html>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## src/index.css Template
|
|
364
|
+
|
|
365
|
+
The CSS variables define the shadcn/ui color system. Replace HSL values to match the
|
|
366
|
+
design system from Step 2. The variable names are used by all shadcn/ui components.
|
|
367
|
+
|
|
368
|
+
```css
|
|
369
|
+
@tailwind base;
|
|
370
|
+
@tailwind components;
|
|
371
|
+
@tailwind utilities;
|
|
372
|
+
|
|
373
|
+
@layer base {
|
|
374
|
+
:root {
|
|
375
|
+
--background: 0 0% 100%;
|
|
376
|
+
--foreground: 240 10% 3.9%;
|
|
377
|
+
--card: 0 0% 100%;
|
|
378
|
+
--card-foreground: 240 10% 3.9%;
|
|
379
|
+
--popover: 0 0% 100%;
|
|
380
|
+
--popover-foreground: 240 10% 3.9%;
|
|
381
|
+
--primary: 240 5.9% 10%;
|
|
382
|
+
--primary-foreground: 0 0% 98%;
|
|
383
|
+
--secondary: 240 4.8% 95.9%;
|
|
384
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
385
|
+
--muted: 240 4.8% 95.9%;
|
|
386
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
387
|
+
--accent: 240 4.8% 95.9%;
|
|
388
|
+
--accent-foreground: 240 5.9% 10%;
|
|
389
|
+
--destructive: 0 84.2% 60.2%;
|
|
390
|
+
--destructive-foreground: 0 0% 98%;
|
|
391
|
+
--border: 240 5.9% 90%;
|
|
392
|
+
--input: 240 5.9% 90%;
|
|
393
|
+
--ring: 240 5.9% 10%;
|
|
394
|
+
--radius: 0.5rem;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.dark {
|
|
398
|
+
--background: 240 10% 3.9%;
|
|
399
|
+
--foreground: 0 0% 98%;
|
|
400
|
+
--card: 240 10% 3.9%;
|
|
401
|
+
--card-foreground: 0 0% 98%;
|
|
402
|
+
--popover: 240 10% 3.9%;
|
|
403
|
+
--popover-foreground: 0 0% 98%;
|
|
404
|
+
--primary: 0 0% 98%;
|
|
405
|
+
--primary-foreground: 240 5.9% 10%;
|
|
406
|
+
--secondary: 240 3.7% 15.9%;
|
|
407
|
+
--secondary-foreground: 0 0% 98%;
|
|
408
|
+
--muted: 240 3.7% 15.9%;
|
|
409
|
+
--muted-foreground: 240 5% 64.9%;
|
|
410
|
+
--accent: 240 3.7% 15.9%;
|
|
411
|
+
--accent-foreground: 0 0% 98%;
|
|
412
|
+
--destructive: 0 62.8% 30.6%;
|
|
413
|
+
--destructive-foreground: 0 0% 98%;
|
|
414
|
+
--border: 240 3.7% 15.9%;
|
|
415
|
+
--input: 240 3.7% 15.9%;
|
|
416
|
+
--ring: 240 4.9% 83.9%;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@layer base {
|
|
421
|
+
* {
|
|
422
|
+
@apply border-border;
|
|
423
|
+
}
|
|
424
|
+
body {
|
|
425
|
+
@apply bg-background text-foreground;
|
|
426
|
+
font-family: "{{BODY_FONT}}", sans-serif;
|
|
427
|
+
}
|
|
428
|
+
h1, h2, h3, h4, h5, h6 {
|
|
429
|
+
font-family: "{{HEADING_FONT}}", sans-serif;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
> **Design system integration**: When a design system provides custom colors (hex),
|
|
435
|
+
> convert them to HSL values for the CSS variables. For example, primary color `#1E40AF`
|
|
436
|
+
> becomes `--primary: 221.2 83.2% 53.3%`. Apply the design system's primary, secondary,
|
|
437
|
+
> and accent colors to the corresponding CSS variables.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## src/lib/utils.ts Template
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
import { type ClassValue, clsx } from "clsx";
|
|
445
|
+
import { twMerge } from "tailwind-merge";
|
|
446
|
+
|
|
447
|
+
export function cn(...inputs: ClassValue[]) {
|
|
448
|
+
return twMerge(clsx(inputs));
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## src/main.tsx Template
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
import { StrictMode } from "react";
|
|
458
|
+
import { createRoot } from "react-dom/client";
|
|
459
|
+
import { BrowserRouter } from "react-router-dom";
|
|
460
|
+
import { ThemeProvider } from "next-themes";
|
|
461
|
+
import App from "./App";
|
|
462
|
+
import "./index.css";
|
|
463
|
+
|
|
464
|
+
createRoot(document.getElementById("root")!).render(
|
|
465
|
+
<StrictMode>
|
|
466
|
+
<BrowserRouter basename="/{{APP_SLUG}}">
|
|
467
|
+
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
|
|
468
|
+
<App />
|
|
469
|
+
</ThemeProvider>
|
|
470
|
+
</BrowserRouter>
|
|
471
|
+
</StrictMode>
|
|
472
|
+
);
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
> The `basename` MUST match the Vite `base` — all internal `<Link to="/{role}/...">`
|
|
476
|
+
> paths stay unchanged; React Router resolves them under `/{{APP_SLUG}}` automatically.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## src/App.tsx Template
|
|
481
|
+
|
|
482
|
+
Defines all routes using React Router. The `AppLayout` component wraps all authenticated
|
|
483
|
+
role pages.
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import { Routes, Route, Navigate } from "react-router-dom";
|
|
487
|
+
import { AppLayout } from "@/components/layout/app-layout";
|
|
488
|
+
|
|
489
|
+
// Import all page components — grouped by role
|
|
490
|
+
// === Hub Administrator ===
|
|
491
|
+
import HubAdminHome from "@/pages/hub-administrator/home";
|
|
492
|
+
import HubAdminProfile from "@/pages/hub-administrator/profile";
|
|
493
|
+
import HubAdminAccount from "@/pages/hub-administrator/account";
|
|
494
|
+
import HubAdminNotifications from "@/pages/hub-administrator/notifications";
|
|
495
|
+
// ... module pages ...
|
|
496
|
+
|
|
497
|
+
// === Hub Operation Support ===
|
|
498
|
+
// ... repeat for each role ...
|
|
499
|
+
|
|
500
|
+
function App() {
|
|
501
|
+
return (
|
|
502
|
+
<Routes>
|
|
503
|
+
{/* Landing — redirect to first role's home */}
|
|
504
|
+
<Route path="/" element={<Navigate to="/hub-administrator/home" replace />} />
|
|
505
|
+
|
|
506
|
+
{/* Hub Administrator */}
|
|
507
|
+
<Route path="/hub-administrator" element={<AppLayout role="hub-administrator" />}>
|
|
508
|
+
<Route index element={<Navigate to="home" replace />} />
|
|
509
|
+
<Route path="home" element={<HubAdminHome />} />
|
|
510
|
+
<Route path="profile" element={<HubAdminProfile />} />
|
|
511
|
+
<Route path="account" element={<HubAdminAccount />} />
|
|
512
|
+
<Route path="notifications" element={<HubAdminNotifications />} />
|
|
513
|
+
{/* Module pages */}
|
|
514
|
+
{/* <Route path="location-information" element={<HubAdminLocationInformation />} /> */}
|
|
515
|
+
{/* <Route path="location-information-detail" element={<HubAdminLocationInformationDetail />} /> */}
|
|
516
|
+
{/* ... */}
|
|
517
|
+
</Route>
|
|
518
|
+
|
|
519
|
+
{/* Hub Operation Support */}
|
|
520
|
+
{/* <Route path="/hub-operation-support" element={<AppLayout role="hub-operation-support" />}> */}
|
|
521
|
+
{/* ... */}
|
|
522
|
+
{/* </Route> */}
|
|
523
|
+
|
|
524
|
+
{/* Catch-all */}
|
|
525
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
526
|
+
</Routes>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export default App;
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
> **Route generation**: For each role and page in the screen plan, generate a complete
|
|
534
|
+
> import statement and `<Route>` entry. The comments above are placeholders — replace
|
|
535
|
+
> with actual routes.
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## src/components/layout/app-layout.tsx Template
|
|
540
|
+
|
|
541
|
+
```tsx
|
|
542
|
+
import { useState } from "react";
|
|
543
|
+
import { Outlet } from "react-router-dom";
|
|
544
|
+
import { AppHeader } from "./app-header";
|
|
545
|
+
import { AppSidebar } from "./app-sidebar";
|
|
546
|
+
import { AppFooter } from "./app-footer";
|
|
547
|
+
|
|
548
|
+
interface AppLayoutProps {
|
|
549
|
+
role: string;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function AppLayout({ role }: AppLayoutProps) {
|
|
553
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<div className="min-h-screen flex flex-col bg-background">
|
|
557
|
+
{/* Header */}
|
|
558
|
+
<AppHeader role={role} />
|
|
559
|
+
|
|
560
|
+
{/* Body: Sidebar + Main */}
|
|
561
|
+
<div className="flex pt-16 min-h-[calc(100vh-4rem)]">
|
|
562
|
+
{/* Sidebar */}
|
|
563
|
+
<AppSidebar
|
|
564
|
+
role={role}
|
|
565
|
+
collapsed={sidebarCollapsed}
|
|
566
|
+
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
567
|
+
/>
|
|
568
|
+
|
|
569
|
+
{/* Main content */}
|
|
570
|
+
<main
|
|
571
|
+
className={cn(
|
|
572
|
+
"flex-1 flex flex-col min-w-0 transition-all duration-200",
|
|
573
|
+
sidebarCollapsed ? "ml-16" : "ml-64"
|
|
574
|
+
)}
|
|
575
|
+
>
|
|
576
|
+
{/* Page content (React Router Outlet) */}
|
|
577
|
+
<div className="flex-1 flex flex-col">
|
|
578
|
+
<Outlet />
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
{/* Footer */}
|
|
582
|
+
<AppFooter />
|
|
583
|
+
</main>
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
> **Note**: Import `cn` from `@/lib/utils` at the top of the file.
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## src/components/layout/app-header.tsx Template
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
import { Link } from "react-router-dom";
|
|
598
|
+
import { useTheme } from "next-themes";
|
|
599
|
+
import {
|
|
600
|
+
Bell,
|
|
601
|
+
Globe,
|
|
602
|
+
Moon,
|
|
603
|
+
Sun,
|
|
604
|
+
User,
|
|
605
|
+
Settings,
|
|
606
|
+
LogOut,
|
|
607
|
+
} from "lucide-react";
|
|
608
|
+
import { Button } from "@/components/ui/button";
|
|
609
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
610
|
+
import { Badge } from "@/components/ui/badge";
|
|
611
|
+
import { Separator } from "@/components/ui/separator";
|
|
612
|
+
import {
|
|
613
|
+
DropdownMenu,
|
|
614
|
+
DropdownMenuContent,
|
|
615
|
+
DropdownMenuItem,
|
|
616
|
+
DropdownMenuLabel,
|
|
617
|
+
DropdownMenuSeparator,
|
|
618
|
+
DropdownMenuTrigger,
|
|
619
|
+
} from "@/components/ui/dropdown-menu";
|
|
620
|
+
|
|
621
|
+
interface AppHeaderProps {
|
|
622
|
+
role: string;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function AppHeader({ role }: AppHeaderProps) {
|
|
626
|
+
const { theme, setTheme } = useTheme();
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<header className="bg-background border-b h-16 flex items-center justify-between px-6 fixed top-0 left-0 right-0 z-30">
|
|
630
|
+
{/* Left: Logo + App Name */}
|
|
631
|
+
<div className="flex items-center gap-3">
|
|
632
|
+
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
|
633
|
+
<span className="text-primary-foreground font-bold text-sm">{{APP_INITIALS}}</span>
|
|
634
|
+
</div>
|
|
635
|
+
<span className="font-semibold text-lg">{{APP_NAME}}</span>
|
|
636
|
+
</div>
|
|
637
|
+
|
|
638
|
+
{/* Right: Controls */}
|
|
639
|
+
<div className="flex items-center gap-1">
|
|
640
|
+
{/* Notification Bell */}
|
|
641
|
+
<DropdownMenu>
|
|
642
|
+
<DropdownMenuTrigger asChild>
|
|
643
|
+
<Button variant="ghost" size="icon" className="relative">
|
|
644
|
+
<Bell className="h-5 w-5" />
|
|
645
|
+
<Badge
|
|
646
|
+
variant="destructive"
|
|
647
|
+
className="absolute -top-1 -right-1 h-4 w-4 p-0 flex items-center justify-center text-[10px]"
|
|
648
|
+
>
|
|
649
|
+
3
|
|
650
|
+
</Badge>
|
|
651
|
+
</Button>
|
|
652
|
+
</DropdownMenuTrigger>
|
|
653
|
+
<DropdownMenuContent align="end" className="w-80">
|
|
654
|
+
<DropdownMenuLabel className="flex items-center justify-between">
|
|
655
|
+
<span>Notifications</span>
|
|
656
|
+
<Button variant="link" size="sm" className="h-auto p-0 text-xs">
|
|
657
|
+
Mark all read
|
|
658
|
+
</Button>
|
|
659
|
+
</DropdownMenuLabel>
|
|
660
|
+
<DropdownMenuSeparator />
|
|
661
|
+
<DropdownMenuItem className="flex flex-col items-start gap-1 py-3">
|
|
662
|
+
<div className="flex items-center gap-2">
|
|
663
|
+
<div className="w-2 h-2 rounded-full bg-primary shrink-0" />
|
|
664
|
+
<span className="font-medium text-sm">New document uploaded</span>
|
|
665
|
+
</div>
|
|
666
|
+
<span className="text-xs text-muted-foreground ml-4">
|
|
667
|
+
A new employment contract has been uploaded for review.
|
|
668
|
+
</span>
|
|
669
|
+
<span className="text-xs text-muted-foreground ml-4">2 minutes ago</span>
|
|
670
|
+
</DropdownMenuItem>
|
|
671
|
+
<DropdownMenuItem className="flex flex-col items-start gap-1 py-3">
|
|
672
|
+
<div className="flex items-center gap-2">
|
|
673
|
+
<div className="w-2 h-2 rounded-full bg-primary shrink-0" />
|
|
674
|
+
<span className="font-medium text-sm">Status changed</span>
|
|
675
|
+
</div>
|
|
676
|
+
<span className="text-xs text-muted-foreground ml-4">
|
|
677
|
+
Record status has been updated.
|
|
678
|
+
</span>
|
|
679
|
+
<span className="text-xs text-muted-foreground ml-4">15 minutes ago</span>
|
|
680
|
+
</DropdownMenuItem>
|
|
681
|
+
<DropdownMenuSeparator />
|
|
682
|
+
<DropdownMenuItem asChild>
|
|
683
|
+
<Link
|
|
684
|
+
to={`/${role}/notifications`}
|
|
685
|
+
className="w-full text-center text-sm font-medium text-primary"
|
|
686
|
+
>
|
|
687
|
+
View all notifications
|
|
688
|
+
</Link>
|
|
689
|
+
</DropdownMenuItem>
|
|
690
|
+
</DropdownMenuContent>
|
|
691
|
+
</DropdownMenu>
|
|
692
|
+
|
|
693
|
+
{/* Locale / Globe */}
|
|
694
|
+
<DropdownMenu>
|
|
695
|
+
<DropdownMenuTrigger asChild>
|
|
696
|
+
<Button variant="ghost" size="icon">
|
|
697
|
+
<Globe className="h-5 w-5" />
|
|
698
|
+
</Button>
|
|
699
|
+
</DropdownMenuTrigger>
|
|
700
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
701
|
+
<DropdownMenuLabel>Language</DropdownMenuLabel>
|
|
702
|
+
<DropdownMenuSeparator />
|
|
703
|
+
<DropdownMenuItem className="font-medium text-primary">
|
|
704
|
+
English (EN)
|
|
705
|
+
</DropdownMenuItem>
|
|
706
|
+
<DropdownMenuItem>Bahasa Melayu (BM)</DropdownMenuItem>
|
|
707
|
+
</DropdownMenuContent>
|
|
708
|
+
</DropdownMenu>
|
|
709
|
+
|
|
710
|
+
{/* Dark Mode Toggle */}
|
|
711
|
+
<Button
|
|
712
|
+
variant="ghost"
|
|
713
|
+
size="icon"
|
|
714
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
715
|
+
>
|
|
716
|
+
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
717
|
+
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
718
|
+
</Button>
|
|
719
|
+
|
|
720
|
+
<Separator orientation="vertical" className="h-8 mx-1" />
|
|
721
|
+
|
|
722
|
+
{/* User Avatar + Dropdown */}
|
|
723
|
+
<DropdownMenu>
|
|
724
|
+
<DropdownMenuTrigger asChild>
|
|
725
|
+
<Button variant="ghost" className="gap-2 px-2">
|
|
726
|
+
<Avatar className="h-8 w-8">
|
|
727
|
+
<AvatarFallback className="bg-primary text-primary-foreground text-sm">
|
|
728
|
+
U
|
|
729
|
+
</AvatarFallback>
|
|
730
|
+
</Avatar>
|
|
731
|
+
</Button>
|
|
732
|
+
</DropdownMenuTrigger>
|
|
733
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
734
|
+
<DropdownMenuItem asChild>
|
|
735
|
+
<Link to={`/${role}/profile`} className="flex items-center gap-2">
|
|
736
|
+
<User className="h-4 w-4" />
|
|
737
|
+
Profile
|
|
738
|
+
</Link>
|
|
739
|
+
</DropdownMenuItem>
|
|
740
|
+
<DropdownMenuItem asChild>
|
|
741
|
+
<Link to={`/${role}/account`} className="flex items-center gap-2">
|
|
742
|
+
<Settings className="h-4 w-4" />
|
|
743
|
+
Account
|
|
744
|
+
</Link>
|
|
745
|
+
</DropdownMenuItem>
|
|
746
|
+
<DropdownMenuSeparator />
|
|
747
|
+
<DropdownMenuItem asChild>
|
|
748
|
+
{/* Plain <a> — escapes the SPA (basename) to the hub landing page */}
|
|
749
|
+
<a href="/" className="flex items-center gap-2 text-destructive">
|
|
750
|
+
<LogOut className="h-4 w-4" />
|
|
751
|
+
Logout
|
|
752
|
+
</a>
|
|
753
|
+
</DropdownMenuItem>
|
|
754
|
+
</DropdownMenuContent>
|
|
755
|
+
</DropdownMenu>
|
|
756
|
+
</div>
|
|
757
|
+
</header>
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
## src/components/layout/app-footer.tsx Template
|
|
765
|
+
|
|
766
|
+
```tsx
|
|
767
|
+
import { Separator } from "@/components/ui/separator";
|
|
768
|
+
|
|
769
|
+
export function AppFooter() {
|
|
770
|
+
return (
|
|
771
|
+
<>
|
|
772
|
+
<Separator />
|
|
773
|
+
<footer className="bg-background px-6 py-3 flex items-center justify-between text-xs text-muted-foreground">
|
|
774
|
+
<span>© {{COPYRIGHT_YEAR}} {{APP_NAME}}. All rights reserved.</span>
|
|
775
|
+
<span>{{VERSION}}</span>
|
|
776
|
+
</footer>
|
|
777
|
+
</>
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
## src/components/layout/sidebar-config.ts Template
|
|
785
|
+
|
|
786
|
+
Export a configuration object defining sidebar navigation items per role. Each item has
|
|
787
|
+
a title, path, and Lucide icon name.
|
|
788
|
+
|
|
789
|
+
```typescript
|
|
790
|
+
export interface SidebarItem {
|
|
791
|
+
title: string;
|
|
792
|
+
path: string;
|
|
793
|
+
icon: string;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export interface SidebarGroup {
|
|
797
|
+
label?: string;
|
|
798
|
+
items: SidebarItem[];
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export interface RoleSidebarConfig {
|
|
802
|
+
label: string;
|
|
803
|
+
groups: SidebarGroup[];
|
|
804
|
+
reports?: { title: string; href: string }[];
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export const sidebarConfig: Record<string, RoleSidebarConfig> = {
|
|
808
|
+
"hub-administrator": {
|
|
809
|
+
label: "Hub Administrator",
|
|
810
|
+
groups: [
|
|
811
|
+
{
|
|
812
|
+
items: [
|
|
813
|
+
{ title: "Home", path: "home", icon: "Home" },
|
|
814
|
+
],
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
label: "Menu",
|
|
818
|
+
items: [
|
|
819
|
+
// { title: "Location Information", path: "location-information", icon: "MapPin" },
|
|
820
|
+
// { title: "Industrial Classification", path: "industrial-classification", icon: "Tag" },
|
|
821
|
+
// ... one entry per module for this role
|
|
822
|
+
],
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
},
|
|
826
|
+
// ... repeat for each role
|
|
827
|
+
};
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
> **Icon mapping**: Use Lucide React icon names matching the module purpose:
|
|
831
|
+
>
|
|
832
|
+
> | Module Pattern | Lucide Icon |
|
|
833
|
+
> |---------------|-------------|
|
|
834
|
+
> | Document / File | `FileText` |
|
|
835
|
+
> | Location / Geography | `MapPin` |
|
|
836
|
+
> | Corridor / Route | `ArrowLeftRight` |
|
|
837
|
+
> | Recruitment / Steps | `ListOrdered` |
|
|
838
|
+
> | Employer / Company | `Building2` |
|
|
839
|
+
> | Classification / Category | `Tag` |
|
|
840
|
+
> | Quota / Allocation | `PieChart` |
|
|
841
|
+
> | Job / Demand | `Briefcase` |
|
|
842
|
+
> | Employee / Person | `Users` |
|
|
843
|
+
> | Settings / Config | `Settings` |
|
|
844
|
+
> | Report | `FileBarChart` |
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
848
|
+
## src/components/layout/app-sidebar.tsx Template
|
|
849
|
+
|
|
850
|
+
```tsx
|
|
851
|
+
import { Link, useLocation } from "react-router-dom";
|
|
852
|
+
import {
|
|
853
|
+
Home,
|
|
854
|
+
MapPin,
|
|
855
|
+
Tag,
|
|
856
|
+
Building2,
|
|
857
|
+
Briefcase,
|
|
858
|
+
Users,
|
|
859
|
+
Settings,
|
|
860
|
+
FileText,
|
|
861
|
+
ListOrdered,
|
|
862
|
+
ArrowLeftRight,
|
|
863
|
+
PieChart,
|
|
864
|
+
FileBarChart,
|
|
865
|
+
ChevronsLeft,
|
|
866
|
+
ChevronsRight,
|
|
867
|
+
} from "lucide-react";
|
|
868
|
+
import { cn } from "@/lib/utils";
|
|
869
|
+
import { Button } from "@/components/ui/button";
|
|
870
|
+
import { Separator } from "@/components/ui/separator";
|
|
871
|
+
import {
|
|
872
|
+
Tooltip,
|
|
873
|
+
TooltipContent,
|
|
874
|
+
TooltipProvider,
|
|
875
|
+
TooltipTrigger,
|
|
876
|
+
} from "@/components/ui/tooltip";
|
|
877
|
+
import { sidebarConfig } from "./sidebar-config";
|
|
878
|
+
|
|
879
|
+
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
880
|
+
Home,
|
|
881
|
+
MapPin,
|
|
882
|
+
Tag,
|
|
883
|
+
Building2,
|
|
884
|
+
Briefcase,
|
|
885
|
+
Users,
|
|
886
|
+
Settings,
|
|
887
|
+
FileText,
|
|
888
|
+
ListOrdered,
|
|
889
|
+
ArrowLeftRight,
|
|
890
|
+
PieChart,
|
|
891
|
+
FileBarChart,
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
interface AppSidebarProps {
|
|
895
|
+
role: string;
|
|
896
|
+
collapsed: boolean;
|
|
897
|
+
onToggle: () => void;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
export function AppSidebar({ role, collapsed, onToggle }: AppSidebarProps) {
|
|
901
|
+
const location = useLocation();
|
|
902
|
+
const config = sidebarConfig[role];
|
|
903
|
+
|
|
904
|
+
if (!config) return null;
|
|
905
|
+
|
|
906
|
+
return (
|
|
907
|
+
<aside
|
|
908
|
+
className={cn(
|
|
909
|
+
"bg-background border-r fixed top-16 bottom-0 left-0 overflow-y-auto z-20 transition-all duration-200 flex flex-col",
|
|
910
|
+
collapsed ? "w-16" : "w-64"
|
|
911
|
+
)}
|
|
912
|
+
>
|
|
913
|
+
<nav className="flex-1 py-4">
|
|
914
|
+
<TooltipProvider delayDuration={0}>
|
|
915
|
+
{config.groups.map((group, gi) => (
|
|
916
|
+
<div key={gi}>
|
|
917
|
+
{group.label && !collapsed && (
|
|
918
|
+
<div className="px-4 pt-4 pb-1">
|
|
919
|
+
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
920
|
+
{group.label}
|
|
921
|
+
</span>
|
|
922
|
+
</div>
|
|
923
|
+
)}
|
|
924
|
+
{group.label && collapsed && (
|
|
925
|
+
<Separator className="my-2 mx-2" />
|
|
926
|
+
)}
|
|
927
|
+
{group.items.map((item) => {
|
|
928
|
+
const Icon = iconMap[item.icon] || FileText;
|
|
929
|
+
const fullPath = `/${role}/${item.path}`;
|
|
930
|
+
const isActive = location.pathname.startsWith(fullPath);
|
|
931
|
+
|
|
932
|
+
const linkContent = (
|
|
933
|
+
<Link
|
|
934
|
+
to={fullPath}
|
|
935
|
+
className={cn(
|
|
936
|
+
"flex items-center gap-3 px-4 py-2.5 text-sm transition-colors duration-200",
|
|
937
|
+
collapsed && "justify-center px-0",
|
|
938
|
+
isActive
|
|
939
|
+
? "bg-primary/5 text-primary border-r-2 border-primary font-medium"
|
|
940
|
+
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
941
|
+
)}
|
|
942
|
+
>
|
|
943
|
+
<Icon className="h-5 w-5 shrink-0" />
|
|
944
|
+
{!collapsed && <span>{item.title}</span>}
|
|
945
|
+
</Link>
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
if (collapsed) {
|
|
949
|
+
return (
|
|
950
|
+
<Tooltip key={item.path}>
|
|
951
|
+
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
|
|
952
|
+
<TooltipContent side="right">{item.title}</TooltipContent>
|
|
953
|
+
</Tooltip>
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
return <div key={item.path}>{linkContent}</div>;
|
|
957
|
+
})}
|
|
958
|
+
</div>
|
|
959
|
+
))}
|
|
960
|
+
|
|
961
|
+
{/* Reports section (if applicable) */}
|
|
962
|
+
{config.reports && config.reports.length > 0 && (
|
|
963
|
+
<div>
|
|
964
|
+
{!collapsed && (
|
|
965
|
+
<div className="px-4 pt-4 pb-1">
|
|
966
|
+
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
967
|
+
Reports
|
|
968
|
+
</span>
|
|
969
|
+
</div>
|
|
970
|
+
)}
|
|
971
|
+
{collapsed && <Separator className="my-2 mx-2" />}
|
|
972
|
+
{config.reports.map((report) => {
|
|
973
|
+
const linkContent = (
|
|
974
|
+
<a
|
|
975
|
+
href={report.href}
|
|
976
|
+
target="_blank"
|
|
977
|
+
rel="noopener noreferrer"
|
|
978
|
+
className={cn(
|
|
979
|
+
"flex items-center gap-3 px-4 py-2.5 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors duration-200",
|
|
980
|
+
collapsed && "justify-center px-0"
|
|
981
|
+
)}
|
|
982
|
+
>
|
|
983
|
+
<FileBarChart className="h-5 w-5 shrink-0" />
|
|
984
|
+
{!collapsed && <span>{report.title}</span>}
|
|
985
|
+
</a>
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
if (collapsed) {
|
|
989
|
+
return (
|
|
990
|
+
<Tooltip key={report.title}>
|
|
991
|
+
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
|
|
992
|
+
<TooltipContent side="right">{report.title}</TooltipContent>
|
|
993
|
+
</Tooltip>
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
return <div key={report.title}>{linkContent}</div>;
|
|
997
|
+
})}
|
|
998
|
+
</div>
|
|
999
|
+
)}
|
|
1000
|
+
</TooltipProvider>
|
|
1001
|
+
</nav>
|
|
1002
|
+
|
|
1003
|
+
{/* Collapse toggle */}
|
|
1004
|
+
<div className="p-2 border-t">
|
|
1005
|
+
<Button
|
|
1006
|
+
variant="ghost"
|
|
1007
|
+
size="sm"
|
|
1008
|
+
onClick={onToggle}
|
|
1009
|
+
className="w-full"
|
|
1010
|
+
>
|
|
1011
|
+
{collapsed ? (
|
|
1012
|
+
<ChevronsRight className="h-4 w-4" />
|
|
1013
|
+
) : (
|
|
1014
|
+
<>
|
|
1015
|
+
<ChevronsLeft className="h-4 w-4 mr-2" />
|
|
1016
|
+
<span className="text-xs">Collapse</span>
|
|
1017
|
+
</>
|
|
1018
|
+
)}
|
|
1019
|
+
</Button>
|
|
1020
|
+
</div>
|
|
1021
|
+
</aside>
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
---
|
|
1027
|
+
|
|
1028
|
+
## Page Component Template
|
|
1029
|
+
|
|
1030
|
+
Page components return only content — the layout wrapper (header, sidebar, footer)
|
|
1031
|
+
is handled by `AppLayout` via React Router's `<Outlet>`.
|
|
1032
|
+
|
|
1033
|
+
```tsx
|
|
1034
|
+
// {module-name}.tsx — {Role Display Name}
|
|
1035
|
+
// User story traceability: {US_TAG} [{US_VERSION}]
|
|
1036
|
+
|
|
1037
|
+
import { useState } from "react";
|
|
1038
|
+
import { Link } from "react-router-dom";
|
|
1039
|
+
import {
|
|
1040
|
+
Breadcrumb,
|
|
1041
|
+
BreadcrumbItem,
|
|
1042
|
+
BreadcrumbLink,
|
|
1043
|
+
BreadcrumbList,
|
|
1044
|
+
BreadcrumbPage,
|
|
1045
|
+
BreadcrumbSeparator,
|
|
1046
|
+
} from "@/components/ui/breadcrumb";
|
|
1047
|
+
|
|
1048
|
+
// ... other shadcn/ui component imports as needed
|
|
1049
|
+
|
|
1050
|
+
export default function PageComponent() {
|
|
1051
|
+
return (
|
|
1052
|
+
<>
|
|
1053
|
+
{/* Breadcrumb */}
|
|
1054
|
+
<div className="px-6 py-3 border-b bg-background">
|
|
1055
|
+
<Breadcrumb>
|
|
1056
|
+
<BreadcrumbList>
|
|
1057
|
+
<BreadcrumbItem>
|
|
1058
|
+
<BreadcrumbLink asChild>
|
|
1059
|
+
<Link to="/{{ROLE}}/home">Home</Link>
|
|
1060
|
+
</BreadcrumbLink>
|
|
1061
|
+
</BreadcrumbItem>
|
|
1062
|
+
<BreadcrumbSeparator />
|
|
1063
|
+
<BreadcrumbItem>
|
|
1064
|
+
<BreadcrumbPage>{{PAGE_TITLE}}</BreadcrumbPage>
|
|
1065
|
+
</BreadcrumbItem>
|
|
1066
|
+
</BreadcrumbList>
|
|
1067
|
+
</Breadcrumb>
|
|
1068
|
+
</div>
|
|
1069
|
+
|
|
1070
|
+
{/* Page Content */}
|
|
1071
|
+
<div className="flex-1 p-6">
|
|
1072
|
+
{/* ... page-specific content ... */}
|
|
1073
|
+
</div>
|
|
1074
|
+
</>
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### Breadcrumb for Sub-pages (detail / edit / create)
|
|
1080
|
+
|
|
1081
|
+
```tsx
|
|
1082
|
+
<Breadcrumb>
|
|
1083
|
+
<BreadcrumbList>
|
|
1084
|
+
<BreadcrumbItem>
|
|
1085
|
+
<BreadcrumbLink asChild>
|
|
1086
|
+
<Link to="/{{ROLE}}/home">Home</Link>
|
|
1087
|
+
</BreadcrumbLink>
|
|
1088
|
+
</BreadcrumbItem>
|
|
1089
|
+
<BreadcrumbSeparator />
|
|
1090
|
+
<BreadcrumbItem>
|
|
1091
|
+
<BreadcrumbLink asChild>
|
|
1092
|
+
<Link to="/{{ROLE}}/{{MODULE}}">{{MODULE_NAME}}</Link>
|
|
1093
|
+
</BreadcrumbLink>
|
|
1094
|
+
</BreadcrumbItem>
|
|
1095
|
+
<BreadcrumbSeparator />
|
|
1096
|
+
<BreadcrumbItem>
|
|
1097
|
+
<BreadcrumbPage>{{PAGE_TITLE}}</BreadcrumbPage>
|
|
1098
|
+
</BreadcrumbItem>
|
|
1099
|
+
</BreadcrumbList>
|
|
1100
|
+
</Breadcrumb>
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
---
|
|
1104
|
+
|
|
1105
|
+
## Link Rules Inside Page Components
|
|
1106
|
+
|
|
1107
|
+
### Navigation links (within the app)
|
|
1108
|
+
|
|
1109
|
+
Use React Router `<Link>` for in-app navigation:
|
|
1110
|
+
|
|
1111
|
+
```tsx
|
|
1112
|
+
import { Link } from "react-router-dom";
|
|
1113
|
+
|
|
1114
|
+
<Link
|
|
1115
|
+
to="/{{ROLE}}/{{TARGET_PAGE}}"
|
|
1116
|
+
className="..."
|
|
1117
|
+
>
|
|
1118
|
+
View Details
|
|
1119
|
+
</Link>
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
### Image links — MUST open in new tab
|
|
1123
|
+
|
|
1124
|
+
```tsx
|
|
1125
|
+
<a href="https://placehold.co/800x600" target="_blank" rel="noopener noreferrer">
|
|
1126
|
+
View Image
|
|
1127
|
+
</a>
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
### PDF / document links — MUST open in new tab
|
|
1131
|
+
|
|
1132
|
+
```tsx
|
|
1133
|
+
import { FileText } from "lucide-react";
|
|
1134
|
+
|
|
1135
|
+
<a href="/reports/sample.pdf" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
|
|
1136
|
+
<FileText className="h-4 w-4" />
|
|
1137
|
+
View PDF
|
|
1138
|
+
</a>
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
### Action buttons in tables
|
|
1142
|
+
|
|
1143
|
+
```tsx
|
|
1144
|
+
import { Link } from "react-router-dom";
|
|
1145
|
+
import { Button } from "@/components/ui/button";
|
|
1146
|
+
import {
|
|
1147
|
+
Dialog,
|
|
1148
|
+
DialogContent,
|
|
1149
|
+
DialogDescription,
|
|
1150
|
+
DialogFooter,
|
|
1151
|
+
DialogHeader,
|
|
1152
|
+
DialogTitle,
|
|
1153
|
+
DialogTrigger,
|
|
1154
|
+
} from "@/components/ui/dialog";
|
|
1155
|
+
|
|
1156
|
+
{/* View / Details → React Router Link */}
|
|
1157
|
+
<Button variant="outline" size="sm" asChild>
|
|
1158
|
+
<Link to="/{{ROLE}}/{{MODULE}}-detail">View</Link>
|
|
1159
|
+
</Button>
|
|
1160
|
+
|
|
1161
|
+
{/* Edit → React Router Link */}
|
|
1162
|
+
<Button variant="outline" size="sm" asChild>
|
|
1163
|
+
<Link to="/{{ROLE}}/{{MODULE}}-edit">Edit</Link>
|
|
1164
|
+
</Button>
|
|
1165
|
+
|
|
1166
|
+
{/* Delete → shadcn/ui Dialog */}
|
|
1167
|
+
<Dialog>
|
|
1168
|
+
<DialogTrigger asChild>
|
|
1169
|
+
<Button variant="outline" size="sm" className="text-destructive border-destructive/50">
|
|
1170
|
+
Delete
|
|
1171
|
+
</Button>
|
|
1172
|
+
</DialogTrigger>
|
|
1173
|
+
<DialogContent>
|
|
1174
|
+
<DialogHeader>
|
|
1175
|
+
<DialogTitle>Delete Record</DialogTitle>
|
|
1176
|
+
<DialogDescription>
|
|
1177
|
+
Are you sure you want to delete this record? This action cannot be undone.
|
|
1178
|
+
</DialogDescription>
|
|
1179
|
+
</DialogHeader>
|
|
1180
|
+
<DialogFooter>
|
|
1181
|
+
<Button variant="outline">Cancel</Button>
|
|
1182
|
+
<Button variant="destructive">Delete</Button>
|
|
1183
|
+
</DialogFooter>
|
|
1184
|
+
</DialogContent>
|
|
1185
|
+
</Dialog>
|
|
1186
|
+
|
|
1187
|
+
{/* Add New → React Router Link */}
|
|
1188
|
+
<Button asChild>
|
|
1189
|
+
<Link to="/{{ROLE}}/{{MODULE}}-create">
|
|
1190
|
+
+ Add New
|
|
1191
|
+
</Link>
|
|
1192
|
+
</Button>
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
### Tab navigation in detail pages
|
|
1196
|
+
|
|
1197
|
+
Use shadcn/ui `<Tabs>` for inline tabs:
|
|
1198
|
+
|
|
1199
|
+
```tsx
|
|
1200
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
1201
|
+
|
|
1202
|
+
<Tabs defaultValue="overview">
|
|
1203
|
+
<TabsList>
|
|
1204
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
1205
|
+
<TabsTrigger value="documents">Documents</TabsTrigger>
|
|
1206
|
+
<TabsTrigger value="history">History</TabsTrigger>
|
|
1207
|
+
</TabsList>
|
|
1208
|
+
<TabsContent value="overview">
|
|
1209
|
+
{/* Overview content */}
|
|
1210
|
+
</TabsContent>
|
|
1211
|
+
<TabsContent value="documents">
|
|
1212
|
+
{/* Documents content */}
|
|
1213
|
+
</TabsContent>
|
|
1214
|
+
<TabsContent value="history">
|
|
1215
|
+
{/* History content */}
|
|
1216
|
+
</TabsContent>
|
|
1217
|
+
</Tabs>
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
If tabs are separate page files, use `<Link>` instead:
|
|
1221
|
+
|
|
1222
|
+
```tsx
|
|
1223
|
+
import { Link, useLocation } from "react-router-dom";
|
|
1224
|
+
import { cn } from "@/lib/utils";
|
|
1225
|
+
|
|
1226
|
+
const tabs = [
|
|
1227
|
+
{ label: "Overview", path: "/{{ROLE}}/{{MODULE}}-tab-overview" },
|
|
1228
|
+
{ label: "Documents", path: "/{{ROLE}}/{{MODULE}}-tab-documents" },
|
|
1229
|
+
{ label: "History", path: "/{{ROLE}}/{{MODULE}}-tab-history" },
|
|
1230
|
+
];
|
|
1231
|
+
|
|
1232
|
+
const location = useLocation();
|
|
1233
|
+
|
|
1234
|
+
<div className="flex border-b mb-6">
|
|
1235
|
+
{tabs.map((tab) => (
|
|
1236
|
+
<Link
|
|
1237
|
+
key={tab.path}
|
|
1238
|
+
to={tab.path}
|
|
1239
|
+
className={cn(
|
|
1240
|
+
"px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
|
|
1241
|
+
location.pathname === tab.path
|
|
1242
|
+
? "border-primary text-primary"
|
|
1243
|
+
: "border-transparent text-muted-foreground hover:text-foreground"
|
|
1244
|
+
)}
|
|
1245
|
+
>
|
|
1246
|
+
{tab.label}
|
|
1247
|
+
</Link>
|
|
1248
|
+
))}
|
|
1249
|
+
</div>
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
### Back / Cancel buttons
|
|
1253
|
+
|
|
1254
|
+
```tsx
|
|
1255
|
+
import { Link } from "react-router-dom";
|
|
1256
|
+
import { Button } from "@/components/ui/button";
|
|
1257
|
+
import { ArrowLeft } from "lucide-react";
|
|
1258
|
+
|
|
1259
|
+
<Button variant="outline" asChild>
|
|
1260
|
+
<Link to="/{{ROLE}}/{{PARENT_MODULE}}">
|
|
1261
|
+
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
1262
|
+
Back to List
|
|
1263
|
+
</Link>
|
|
1264
|
+
</Button>
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
### Pagination
|
|
1268
|
+
|
|
1269
|
+
```tsx
|
|
1270
|
+
import { useState } from "react";
|
|
1271
|
+
import { Button } from "@/components/ui/button";
|
|
1272
|
+
import {
|
|
1273
|
+
Select,
|
|
1274
|
+
SelectContent,
|
|
1275
|
+
SelectItem,
|
|
1276
|
+
SelectTrigger,
|
|
1277
|
+
SelectValue,
|
|
1278
|
+
} from "@/components/ui/select";
|
|
1279
|
+
import {
|
|
1280
|
+
Pagination,
|
|
1281
|
+
PaginationContent,
|
|
1282
|
+
PaginationEllipsis,
|
|
1283
|
+
PaginationItem,
|
|
1284
|
+
PaginationLink,
|
|
1285
|
+
PaginationNext,
|
|
1286
|
+
PaginationPrevious,
|
|
1287
|
+
} from "@/components/ui/pagination";
|
|
1288
|
+
|
|
1289
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
1290
|
+
const [pageSize, setPageSize] = useState(10);
|
|
1291
|
+
const totalItems = 47;
|
|
1292
|
+
const totalPages = Math.ceil(totalItems / pageSize);
|
|
1293
|
+
|
|
1294
|
+
<div className="flex items-center justify-between mt-4">
|
|
1295
|
+
<span className="text-sm text-muted-foreground">
|
|
1296
|
+
Showing {(currentPage - 1) * pageSize + 1}–{Math.min(currentPage * pageSize, totalItems)} of {totalItems} results
|
|
1297
|
+
</span>
|
|
1298
|
+
|
|
1299
|
+
<Pagination>
|
|
1300
|
+
<PaginationContent>
|
|
1301
|
+
<PaginationItem>
|
|
1302
|
+
<PaginationPrevious
|
|
1303
|
+
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
1304
|
+
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
1305
|
+
/>
|
|
1306
|
+
</PaginationItem>
|
|
1307
|
+
{/* Page numbers — generate dynamically */}
|
|
1308
|
+
<PaginationItem>
|
|
1309
|
+
<PaginationLink
|
|
1310
|
+
isActive={currentPage === 1}
|
|
1311
|
+
onClick={() => setCurrentPage(1)}
|
|
1312
|
+
className="cursor-pointer"
|
|
1313
|
+
>
|
|
1314
|
+
1
|
|
1315
|
+
</PaginationLink>
|
|
1316
|
+
</PaginationItem>
|
|
1317
|
+
{currentPage > 3 && (
|
|
1318
|
+
<PaginationItem>
|
|
1319
|
+
<PaginationEllipsis />
|
|
1320
|
+
</PaginationItem>
|
|
1321
|
+
)}
|
|
1322
|
+
{/* ... middle pages ... */}
|
|
1323
|
+
<PaginationItem>
|
|
1324
|
+
<PaginationNext
|
|
1325
|
+
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
|
1326
|
+
className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
1327
|
+
/>
|
|
1328
|
+
</PaginationItem>
|
|
1329
|
+
</PaginationContent>
|
|
1330
|
+
</Pagination>
|
|
1331
|
+
|
|
1332
|
+
<Select value={String(pageSize)} onValueChange={(v) => { setPageSize(Number(v)); setCurrentPage(1); }}>
|
|
1333
|
+
<SelectTrigger className="w-20">
|
|
1334
|
+
<SelectValue />
|
|
1335
|
+
</SelectTrigger>
|
|
1336
|
+
<SelectContent>
|
|
1337
|
+
<SelectItem value="10">10</SelectItem>
|
|
1338
|
+
<SelectItem value="25">25</SelectItem>
|
|
1339
|
+
<SelectItem value="50">50</SelectItem>
|
|
1340
|
+
</SelectContent>
|
|
1341
|
+
</Select>
|
|
1342
|
+
</div>
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
---
|
|
1346
|
+
|
|
1347
|
+
## Common Pages Content
|
|
1348
|
+
|
|
1349
|
+
### home.tsx page component
|
|
1350
|
+
|
|
1351
|
+
```tsx
|
|
1352
|
+
import { Link } from "react-router-dom";
|
|
1353
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
1354
|
+
import {
|
|
1355
|
+
Table,
|
|
1356
|
+
TableBody,
|
|
1357
|
+
TableCell,
|
|
1358
|
+
TableHead,
|
|
1359
|
+
TableHeader,
|
|
1360
|
+
TableRow,
|
|
1361
|
+
} from "@/components/ui/table";
|
|
1362
|
+
import { Badge } from "@/components/ui/badge";
|
|
1363
|
+
import {
|
|
1364
|
+
Breadcrumb,
|
|
1365
|
+
BreadcrumbItem,
|
|
1366
|
+
BreadcrumbList,
|
|
1367
|
+
BreadcrumbPage,
|
|
1368
|
+
} from "@/components/ui/breadcrumb";
|
|
1369
|
+
|
|
1370
|
+
export default function Home() {
|
|
1371
|
+
return (
|
|
1372
|
+
<>
|
|
1373
|
+
<div className="px-6 py-3 border-b bg-background">
|
|
1374
|
+
<Breadcrumb>
|
|
1375
|
+
<BreadcrumbList>
|
|
1376
|
+
<BreadcrumbItem>
|
|
1377
|
+
<BreadcrumbPage>Home</BreadcrumbPage>
|
|
1378
|
+
</BreadcrumbItem>
|
|
1379
|
+
</BreadcrumbList>
|
|
1380
|
+
</Breadcrumb>
|
|
1381
|
+
</div>
|
|
1382
|
+
<div className="flex-1 p-6">
|
|
1383
|
+
<h1 className="text-2xl font-bold mb-2">Welcome back</h1>
|
|
1384
|
+
<p className="text-muted-foreground mb-6">{{APP_NAME}} — {{ROLE_DISPLAY}}</p>
|
|
1385
|
+
|
|
1386
|
+
{/* Summary stat cards — one per module */}
|
|
1387
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
1388
|
+
{/* Repeat: one Card per module */}
|
|
1389
|
+
<Card>
|
|
1390
|
+
<CardHeader className="pb-2">
|
|
1391
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
1392
|
+
Module Name
|
|
1393
|
+
</CardTitle>
|
|
1394
|
+
</CardHeader>
|
|
1395
|
+
<CardContent>
|
|
1396
|
+
<div className="text-2xl font-bold">142</div>
|
|
1397
|
+
<p className="text-xs text-muted-foreground">+12 this week</p>
|
|
1398
|
+
</CardContent>
|
|
1399
|
+
</Card>
|
|
1400
|
+
</div>
|
|
1401
|
+
|
|
1402
|
+
{/* Recent activity table */}
|
|
1403
|
+
<Card>
|
|
1404
|
+
<CardHeader>
|
|
1405
|
+
<CardTitle>Recent Activity</CardTitle>
|
|
1406
|
+
</CardHeader>
|
|
1407
|
+
<CardContent>
|
|
1408
|
+
<Table>
|
|
1409
|
+
<TableHeader>
|
|
1410
|
+
<TableRow>
|
|
1411
|
+
<TableHead>Action</TableHead>
|
|
1412
|
+
<TableHead>Module</TableHead>
|
|
1413
|
+
<TableHead>User</TableHead>
|
|
1414
|
+
<TableHead>Date</TableHead>
|
|
1415
|
+
</TableRow>
|
|
1416
|
+
</TableHeader>
|
|
1417
|
+
<TableBody>
|
|
1418
|
+
<TableRow>
|
|
1419
|
+
<TableCell><Badge>Created</Badge></TableCell>
|
|
1420
|
+
<TableCell>Module Name</TableCell>
|
|
1421
|
+
<TableCell>John Doe</TableCell>
|
|
1422
|
+
<TableCell>15 Aug 2025 10:30</TableCell>
|
|
1423
|
+
</TableRow>
|
|
1424
|
+
</TableBody>
|
|
1425
|
+
</Table>
|
|
1426
|
+
</CardContent>
|
|
1427
|
+
</Card>
|
|
1428
|
+
</div>
|
|
1429
|
+
</>
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
### profile.tsx page component
|
|
1435
|
+
|
|
1436
|
+
User profile: `<Avatar>`, full name, email, role `<Badge>`, personal info `<Card>` (read-only),
|
|
1437
|
+
Edit `<Button>`.
|
|
1438
|
+
|
|
1439
|
+
### account.tsx page component
|
|
1440
|
+
|
|
1441
|
+
Account settings: Change Password `<Card>` form, Notification Preferences `<Card>` with
|
|
1442
|
+
`<Switch>` toggles, Language/Locale `<Select>`, Session Management `<Card>` with `<Table>`.
|
|
1443
|
+
|
|
1444
|
+
### notifications.tsx page component
|
|
1445
|
+
|
|
1446
|
+
Notifications list: shadcn/ui `<Tabs>` for All/Unread/Read filter, notification `<Card>` items
|
|
1447
|
+
with Lucide icon / title / message / timestamp / read-unread dot, Mark all as read `<Button>`,
|
|
1448
|
+
`<Pagination>` (MANDATORY).
|
|
1449
|
+
|
|
1450
|
+
---
|
|
1451
|
+
|
|
1452
|
+
## Quick-Start Instructions (append to output summary)
|
|
1453
|
+
|
|
1454
|
+
```
|
|
1455
|
+
Quick Start
|
|
1456
|
+
===========
|
|
1457
|
+
1. cd {mockup folder} && npm install && npm run build (one-time build for the hub)
|
|
1458
|
+
2. cd <root>/mockup
|
|
1459
|
+
3. npm start (zero dependencies — no npm install required)
|
|
1460
|
+
4. Open http://localhost:3000 in your browser
|
|
1461
|
+
- Landing page lists ALL applications and roles
|
|
1462
|
+
- This app's screen index: http://localhost:3000/{app_slug}
|
|
1463
|
+
5. Change port: PORT=4000 npm start, or edit <root>/mockup/mockup.config.json
|
|
1464
|
+
|
|
1465
|
+
Design iteration (optional): cd {mockup folder} && npm run dev
|
|
1466
|
+
→ http://localhost:5173/{app_slug}/
|
|
1467
|
+
```
|