@rashidee/co2 1.3.6 → 1.3.8
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/.co2-dat/app.db +0 -0
- package/dist/.co2-dat/app.db-shm +0 -0
- package/dist/.co2-dat/app.db-wal +0 -0
- package/dist/index.js +167 -68
- package/package.json +41 -41
- package/plugin/skills/conductor-feature-develop/SKILL.md +1384 -1383
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +225 -224
- package/plugin/skills/mockgen-shadcn/SKILL.md +1073 -1067
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +5 -3
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +631 -498
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +2 -2
- package/plugin/skills/mockgen-tailwind/SKILL.md +913 -904
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +722 -720
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +631 -498
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -190
- package/static/assets/{abnfDiagram-VRR7QNED-CzKpxD9q.js → abnfDiagram-VRR7QNED-DP7zbvBZ.js} +1 -1
- package/static/assets/{arc-0jj0p8Qe.js → arc-Ckh0cqx_.js} +1 -1
- package/static/assets/{architectureDiagram-ZJ3FMSHR-DXUL6S5t.js → architectureDiagram-ZJ3FMSHR-EpVuCwzS.js} +1 -1
- package/static/assets/{blockDiagram-677ZJIJ3-BBjsQESd.js → blockDiagram-677ZJIJ3-DsgBkX_g.js} +1 -1
- package/static/assets/{c4Diagram-LMCZKHZV-CTsLC95S.js → c4Diagram-LMCZKHZV-CR04rrP-.js} +1 -1
- package/static/assets/channel-DfxyTZ0M.js +1 -0
- package/static/assets/{chunk-2Q5K7J3B-BpisBKYx.js → chunk-2Q5K7J3B-DeQC-sio.js} +1 -1
- package/static/assets/{chunk-32BRIVSS-DAfsr4ph.js → chunk-32BRIVSS-Qbam-XOV.js} +1 -1
- package/static/assets/{chunk-5VM5RSS4-Z3FtY-B6.js → chunk-5VM5RSS4-CH-IX0uh.js} +1 -1
- package/static/assets/{chunk-EX3LRPZG-FI9smQaN.js → chunk-EX3LRPZG-6gUCPZ6x.js} +1 -1
- package/static/assets/{chunk-JWPE2WC7-sOygQj_B.js → chunk-JWPE2WC7-DSNEXnbo.js} +1 -1
- package/static/assets/{chunk-MOJQB5TN-DCSpai7w.js → chunk-MOJQB5TN-BVmlLOXk.js} +1 -1
- package/static/assets/{chunk-RYQCIY6F-CchqtfnI.js → chunk-RYQCIY6F-2pIZN6PI.js} +1 -1
- package/static/assets/{chunk-V7JOEXUC-DaOzsfx7.js → chunk-V7JOEXUC-Dj8a39S7.js} +1 -1
- package/static/assets/{chunk-VR4S4FIN-DmxuoVTy.js → chunk-VR4S4FIN-D2kLoVlo.js} +1 -1
- package/static/assets/{chunk-XXDRQBXY-ChERxfri.js → chunk-XXDRQBXY-x3SQWIAg.js} +1 -1
- package/static/assets/classDiagram-OUVF2IWQ-DPLLyb_P.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DPLLyb_P.js +1 -0
- package/static/assets/{cose-bilkent-JH36ORCC-CO7_eF6b.js → cose-bilkent-JH36ORCC-ltgQwU4R.js} +1 -1
- package/static/assets/{cynefin-VYW2F7L2-CzQkWtdY.js → cynefin-VYW2F7L2-BwJnOy5O.js} +1 -1
- package/static/assets/{cynefinDiagram-TSTJHNR4-BN2o8rFa.js → cynefinDiagram-TSTJHNR4-Bi47Fvnv.js} +1 -1
- package/static/assets/{dagre-VKFMJZFB-GLjBE-Xs.js → dagre-VKFMJZFB-5_l7QdUh.js} +1 -1
- package/static/assets/{diagram-FQU43EPY-Cyv7saju.js → diagram-FQU43EPY-HKDyXOzo.js} +1 -1
- package/static/assets/{diagram-G47NLZAW-B9PmZJ3U.js → diagram-G47NLZAW-xHHInyx1.js} +1 -1
- package/static/assets/{diagram-NH7WQ7WH-ByKD8yJU.js → diagram-NH7WQ7WH-nftmRdjw.js} +1 -1
- package/static/assets/{diagram-OA4YK3LP-BvbW7fIS.js → diagram-OA4YK3LP-CX8gIXu7.js} +1 -1
- package/static/assets/{diagram-WEI45ONY-BgXZ9SJ8.js → diagram-WEI45ONY-BaT3ur6a.js} +1 -1
- package/static/assets/{ebnfDiagram-CCIWWBDH-CXEEJ26B.js → ebnfDiagram-CCIWWBDH-CM9hwJSh.js} +1 -1
- package/static/assets/{erDiagram-Q63AITRT-CcHs_S16.js → erDiagram-Q63AITRT-C9cK36a5.js} +1 -1
- package/static/assets/{flowDiagram-23GEKE2U-Cwg3I3GK.js → flowDiagram-23GEKE2U-BQw_JFaW.js} +1 -1
- package/static/assets/{ganttDiagram-NO4QXBWP-ky6w5Lvy.js → ganttDiagram-NO4QXBWP-Cgg9ZbM9.js} +1 -1
- package/static/assets/{gitGraphDiagram-IHSO6WYX-DN0eRQ7r.js → gitGraphDiagram-IHSO6WYX-SciNAnN3.js} +1 -1
- package/static/assets/{index-Cz5Fcq3G.css → index-CM4GfOLV.css} +1 -1
- package/static/assets/{index-CCC4-Ibh.js → index-HUEMwsDE.js} +167 -167
- package/static/assets/{infoDiagram-FWYZ7A6U-COojw6OC.js → infoDiagram-FWYZ7A6U-BPCKxKmJ.js} +1 -1
- package/static/assets/{ishikawaDiagram-FXEZZL3T-BR5w5ZqM.js → ishikawaDiagram-FXEZZL3T-C0eQKvpo.js} +1 -1
- package/static/assets/{journeyDiagram-5HDEW3XC-B5WtTEBM.js → journeyDiagram-5HDEW3XC-DgrJ90BU.js} +1 -1
- package/static/assets/{kanban-definition-HUTT4EX6-DnQ_zg4E.js → kanban-definition-HUTT4EX6-ISgFrxBl.js} +1 -1
- package/static/assets/{linear-C5kGzenz.js → linear-CaPqviiq.js} +1 -1
- package/static/assets/{mindmap-definition-LN4V7U3C-BMksxBso.js → mindmap-definition-LN4V7U3C-DTpJ3Okf.js} +1 -1
- package/static/assets/{pegDiagram-2B236MQR-BKrF97iU.js → pegDiagram-2B236MQR-D0EbYB8X.js} +1 -1
- package/static/assets/{pieDiagram-ENE6RG2P-DZAQkZwC.js → pieDiagram-ENE6RG2P-lAwviOjS.js} +1 -1
- package/static/assets/{quadrantDiagram-ABIIQ3AL-Dx0-5odM.js → quadrantDiagram-ABIIQ3AL-Dn8yVCWJ.js} +1 -1
- package/static/assets/{railroadDiagram-RFXS5EU6-BbY-CxL_.js → railroadDiagram-RFXS5EU6-BVLcNBsS.js} +1 -1
- package/static/assets/{requirementDiagram-TGXJPOKE-ItSYgvcN.js → requirementDiagram-TGXJPOKE-Bmjsh1uy.js} +1 -1
- package/static/assets/{sankeyDiagram-HTMAVEWB-CjIuRRYE.js → sankeyDiagram-HTMAVEWB-tdNqcgyd.js} +1 -1
- package/static/assets/{sequenceDiagram-DBY2YBRQ-B277PT2z.js → sequenceDiagram-DBY2YBRQ-ChtQkNW9.js} +1 -1
- package/static/assets/{sizeCapture-X5ZJPWSS-BYc46yWE.js → sizeCapture-X5ZJPWSS-BAnRJGV8.js} +1 -1
- package/static/assets/{stateDiagram-2N3HPSRC-Cq41gYoq.js → stateDiagram-2N3HPSRC-CI7wmYAe.js} +1 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-BJrR07NX.js +1 -0
- package/static/assets/{swimlanes-5IMT3BWC-DMxA2jQL.js → swimlanes-5IMT3BWC-k6NGsb3p.js} +2 -2
- package/static/assets/swimlanesDiagram-G3AALYLV-B0-5v6_-.js +8 -0
- package/static/assets/{timeline-definition-FHXFAJF6-CB8iORNI.js → timeline-definition-FHXFAJF6-CweEa8iU.js} +1 -1
- package/static/assets/{vennDiagram-L72KCM5P-CHwAX4cP.js → vennDiagram-L72KCM5P-DDPbV_sp.js} +1 -1
- package/static/assets/{wardleyDiagram-EHGQE667-DL19kD5s.js → wardleyDiagram-EHGQE667-IP4J8kqe.js} +1 -1
- package/static/assets/{xychartDiagram-FW5EYKEG-b271oXxd.js → xychartDiagram-FW5EYKEG-CMfWLoM8.js} +1 -1
- package/static/index.html +2 -2
- package/static/assets/channel-1MKDXK8w.js +0 -1
- package/static/assets/classDiagram-OUVF2IWQ-bADaUwoJ.js +0 -1
- package/static/assets/classDiagram-v2-EOCWNBFH-bADaUwoJ.js +0 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-B9V3_HpU.js +0 -1
- package/static/assets/swimlanesDiagram-G3AALYLV-qx8IX4cU.js +0 -8
|
@@ -1,720 +1,722 @@
|
|
|
1
|
-
# Admin Layout Architecture — Alpine.js + HTMX Assets (served by the Mockup Hub)
|
|
2
|
-
|
|
3
|
-
The application's mockup folder contains **assets only**. The shared **Mockup Hub**
|
|
4
|
-
(`<root>/mockup/server.js` — see [mockup-hub-template.md](mockup-hub-template.md))
|
|
5
|
-
assembles pages from partials server-side at `/{app_slug}/{role}/{page}`. **HTMX**
|
|
6
|
-
performs partial content swapping during navigation, and **Alpine.js** powers
|
|
7
|
-
interactive UI components (dropdowns, dark mode toggle).
|
|
8
|
-
|
|
9
|
-
`{{APP_SLUG}}` below is replaced **at generation time** with the application slug
|
|
10
|
-
(app folder name minus the leading `<number>_` prefix). It is constant for the whole
|
|
11
|
-
mockup folder.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## File Structure
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
mockup/
|
|
19
|
-
mockup-manifest.json # Hub discovery manifest (see mockup-hub-template.md)
|
|
20
|
-
MOCKUP.html # Per-app screen index (served by hub at /{app_slug})
|
|
21
|
-
partials/
|
|
22
|
-
shell.html # Page shell (assembled by server)
|
|
23
|
-
header.html # Top header (Alpine.js powered)
|
|
24
|
-
footer.html # Bottom footer
|
|
25
|
-
sidebar-{role_snake_case}.html # One sidebar per role
|
|
26
|
-
{role_snake_case}/
|
|
27
|
-
content/
|
|
28
|
-
home.html # Content fragment (no layout)
|
|
29
|
-
profile.html
|
|
30
|
-
account.html
|
|
31
|
-
notifications.html
|
|
32
|
-
{module_snake_case}.html
|
|
33
|
-
{module_snake_case}_detail.html
|
|
34
|
-
{module_snake_case}_create.html
|
|
35
|
-
{module_snake_case}_edit.html
|
|
36
|
-
...
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Placeholders Reference
|
|
42
|
-
|
|
43
|
-
| Placeholder | Description |
|
|
44
|
-
|-------------|-------------|
|
|
45
|
-
| `{{APP_NAME}}` | Application name (e.g., "Hub Middleware") |
|
|
46
|
-
| `{{APP_SLUG}}` | App slug used as URL prefix (e.g., "hub_middleware") — replaced at generation time |
|
|
47
|
-
| `{{PAGE_TITLE}}` | Screen title (e.g., "Location Information") |
|
|
48
|
-
| `{{ROLE}}` | Role in snake_case (e.g., "hub_administrator") — in `header.html` it is re-injected by the hub at request time |
|
|
49
|
-
| `{{SIDEBAR_MENU}}` | Navigation menu items HTML |
|
|
50
|
-
| `{{BREADCRUMB}}` | Breadcrumb trail HTML |
|
|
51
|
-
| `{{MAIN_CONTENT}}` | Screen-specific content |
|
|
52
|
-
| `{{COPYRIGHT_YEAR}}` | Current year |
|
|
53
|
-
| `{{VERSION}}` | Latest version from PRD.md |
|
|
54
|
-
| `{{DESIGN_PRIMARY}}` | Primary color hex from design system |
|
|
55
|
-
| `{{DESIGN_SECONDARY}}` | Secondary color hex from design system |
|
|
56
|
-
| `{{DESIGN_ACCENT}}` | Accent/CTA color hex |
|
|
57
|
-
| `{{DESIGN_BG}}` | Background color hex |
|
|
58
|
-
| `{{DESIGN_TEXT}}` | Text color hex |
|
|
59
|
-
| `{{HEADING_FONT}}` | Heading font |
|
|
60
|
-
| `{{BODY_FONT}}` | Body font |
|
|
61
|
-
| `{{GOOGLE_FONTS_URL}}` | Google Fonts import URL |
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Server
|
|
66
|
-
|
|
67
|
-
There is **no per-app server**. The shared Mockup Hub (`<root>/mockup/server.js`, created
|
|
68
|
-
per the Ensure-Hub rules in [mockup-hub-template.md](mockup-hub-template.md)) provides:
|
|
69
|
-
|
|
70
|
-
- `GET /{app_slug}` → this app's `MOCKUP.html`
|
|
71
|
-
- `GET /{app_slug}/{role}` → redirect to `/{app_slug}/{role}/home`
|
|
72
|
-
- `GET /{app_slug}/{role}/{page}` → shell + header + sidebar + content + footer assembly
|
|
73
|
-
- `GET /api/content/{app_slug}/{role}/{page}` → content fragment (HTMX swaps)
|
|
74
|
-
- `GET /{app_slug}/static/*`, `GET /{app_slug}/reports/*` → static assets
|
|
75
|
-
- The hub re-injects `{{ROLE}}` into `header.html` at request time
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## partials/shell.html Template
|
|
80
|
-
|
|
81
|
-
The shell is the outer HTML document. It is assembled server-side and NOT a partial
|
|
82
|
-
served to the browser — the hub substitutes `{{HEADER}}`, `{{SIDEBAR}}`,
|
|
83
|
-
`{{CONTENT}}`, and `{{FOOTER}}` before responding.
|
|
84
|
-
|
|
85
|
-
```html
|
|
86
|
-
<!DOCTYPE html>
|
|
87
|
-
<html lang="en" x-data="appShell()" :class="{ 'dark': darkMode }">
|
|
88
|
-
<head>
|
|
89
|
-
<meta charset="UTF-8">
|
|
90
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
91
|
-
<title>{{APP_NAME}}</title>
|
|
92
|
-
|
|
93
|
-
<!-- Tailwind CSS -->
|
|
94
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
95
|
-
|
|
96
|
-
<!-- HTMX -->
|
|
97
|
-
<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
|
|
98
|
-
|
|
99
|
-
<!-- Alpine.js -->
|
|
100
|
-
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
|
|
101
|
-
|
|
102
|
-
<!-- Google Fonts -->
|
|
103
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
104
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
105
|
-
<link href="{{GOOGLE_FONTS_URL}}" rel="stylesheet">
|
|
106
|
-
|
|
107
|
-
<script>
|
|
108
|
-
tailwind.config = {
|
|
109
|
-
darkMode: 'class',
|
|
110
|
-
theme: {
|
|
111
|
-
extend: {
|
|
112
|
-
colors: {
|
|
113
|
-
primary: '{{DESIGN_PRIMARY}}',
|
|
114
|
-
secondary: '{{DESIGN_SECONDARY}}',
|
|
115
|
-
accent: '{{DESIGN_ACCENT}}',
|
|
116
|
-
},
|
|
117
|
-
fontFamily: {
|
|
118
|
-
heading: ['{{HEADING_FONT}}', 'sans-serif'],
|
|
119
|
-
body: ['{{BODY_FONT}}', 'sans-serif'],
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function appShell() {
|
|
126
|
-
return {
|
|
127
|
-
darkMode: localStorage.getItem('darkMode') === 'true',
|
|
128
|
-
toggleDark() {
|
|
129
|
-
this.darkMode = !this.darkMode;
|
|
130
|
-
localStorage.setItem('darkMode', this.darkMode);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
</script>
|
|
135
|
-
|
|
136
|
-
<style>
|
|
137
|
-
body { font-family: '{{BODY_FONT}}', sans-serif; }
|
|
138
|
-
h1, h2, h3, h4, h5, h6 { font-family: '{{HEADING_FONT}}', sans-serif; }
|
|
139
|
-
::-webkit-scrollbar { width: 6px; }
|
|
140
|
-
::-webkit-scrollbar-track { background: #f1f5f9; }
|
|
141
|
-
::-webkit-scrollbar-thumb { background: #94a3b8; border-radius: 3px; }
|
|
142
|
-
::-webkit-scrollbar-thumb:hover { background: #64748b; }
|
|
143
|
-
/* HTMX loading indicator */
|
|
144
|
-
.htmx-request #content-spinner { display: flex; }
|
|
145
|
-
.htmx-request.htmx-request #content-area { opacity: 0.4; }
|
|
146
|
-
</style>
|
|
147
|
-
</head>
|
|
148
|
-
<body class="bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 min-h-screen flex flex-col">
|
|
149
|
-
|
|
150
|
-
<!-- ── Header (injected by server) ─────────────────────────────────── -->
|
|
151
|
-
{{HEADER}}
|
|
152
|
-
|
|
153
|
-
<!-- ── Body: Sidebar + Main ─────────────────────────────────────────── -->
|
|
154
|
-
<div class="flex pt-16 min-h-[calc(100vh-4rem)]">
|
|
155
|
-
|
|
156
|
-
<!-- ── Sidebar (injected by server) ──────────────────────────────── -->
|
|
157
|
-
{{SIDEBAR}}
|
|
158
|
-
|
|
159
|
-
<!-- ── Main ──────────────────────────────────────────────────────── -->
|
|
160
|
-
<main class="flex-1 ml-64 flex flex-col min-w-0">
|
|
161
|
-
|
|
162
|
-
<!-- Loading spinner (shown while HTMX is fetching) -->
|
|
163
|
-
<div id="content-spinner" class="hidden fixed inset-0 bg-white/50 z-50 items-center justify-center">
|
|
164
|
-
<div class="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
<!-- ── Content area (HTMX target) ─────────────────────────────── -->
|
|
168
|
-
<div id="content-area" class="flex-1 flex flex-col transition-opacity duration-200">
|
|
169
|
-
{{CONTENT}}
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
<!-- ── Footer (injected by server) ──────────────────────────────── -->
|
|
173
|
-
{{FOOTER}}
|
|
174
|
-
|
|
175
|
-
</main>
|
|
176
|
-
</div>
|
|
177
|
-
|
|
178
|
-
</body>
|
|
179
|
-
</html>
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
## partials/header.html Template
|
|
185
|
-
|
|
186
|
-
Alpine.js `x-data` scopes are per-component, declared on each dropdown wrapper.
|
|
187
|
-
The dark mode toggle dispatches to the parent `appShell()` context via `$dispatch`.
|
|
188
|
-
|
|
189
|
-
```html
|
|
190
|
-
|
|
191
|
-
<header class="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 h-16 flex items-center justify-between px-6 fixed top-0 left-0 right-0 z-30 shadow-sm">
|
|
192
|
-
|
|
193
|
-
<!-- Left: Logo + App Name -->
|
|
194
|
-
<div class="flex items-center gap-3">
|
|
195
|
-
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background-color: {{DESIGN_PRIMARY}}">
|
|
196
|
-
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
197
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
198
|
-
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
|
|
199
|
-
</svg>
|
|
200
|
-
</div>
|
|
201
|
-
<span class="font-semibold text-lg text-slate-900 dark:text-slate-100">{{APP_NAME}}</span>
|
|
202
|
-
</div>
|
|
203
|
-
|
|
204
|
-
<!-- Right: Controls -->
|
|
205
|
-
<div class="flex items-center gap-2">
|
|
206
|
-
|
|
207
|
-
<!-- Notification Bell -->
|
|
208
|
-
<div class="relative" x-data="{ open: false }">
|
|
209
|
-
<button @click="open = !open" @keydown.escape.window="open = false"
|
|
210
|
-
class="relative p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors duration-200">
|
|
211
|
-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
212
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
213
|
-
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
|
|
214
|
-
</svg>
|
|
215
|
-
<span class="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full"></span>
|
|
216
|
-
</button>
|
|
217
|
-
<!-- Notification Dropdown -->
|
|
218
|
-
<div x-show="open" @click.outside="open = false"
|
|
219
|
-
x-transition:enter="transition ease-out duration-150"
|
|
220
|
-
x-transition:enter-start="opacity-0 translate-y-1"
|
|
221
|
-
x-transition:enter-end="opacity-100 translate-y-0"
|
|
222
|
-
x-transition:leave="transition ease-in duration-100"
|
|
223
|
-
x-transition:leave-start="opacity-100 translate-y-0"
|
|
224
|
-
x-transition:leave-end="opacity-0 translate-y-1"
|
|
225
|
-
class="absolute right-0 top-full mt-2 w-80 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl z-50"
|
|
226
|
-
style="display: none;">
|
|
227
|
-
<div class="px-4 py-3 border-b border-slate-100 dark:border-slate-700 flex items-center justify-between">
|
|
228
|
-
<span class="font-medium text-sm text-slate-800 dark:text-slate-200">Notifications</span>
|
|
229
|
-
<button class="text-xs text-primary cursor-pointer hover:underline">Mark all read</button>
|
|
230
|
-
</div>
|
|
231
|
-
<div class="max-h-72 overflow-y-auto divide-y divide-slate-50 dark:divide-slate-700">
|
|
232
|
-
<div class="px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 flex gap-3">
|
|
233
|
-
<div class="w-2 h-2 rounded-full mt-2 shrink-0"
|
|
234
|
-
style="background-color: {{DESIGN_PRIMARY}}"></div>
|
|
235
|
-
<div>
|
|
236
|
-
<p class="text-sm text-slate-800 dark:text-slate-200 font-medium">New document uploaded</p>
|
|
237
|
-
<p class="text-xs text-slate-500 mt-0.5">A new employment contract has been uploaded for
|
|
238
|
-
review.</p>
|
|
239
|
-
<p class="text-xs text-slate-400 mt-1">2 minutes ago</p>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
<div class="px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 flex gap-3">
|
|
243
|
-
<div class="w-2 h-2 rounded-full mt-2 shrink-0"
|
|
244
|
-
style="background-color: {{DESIGN_PRIMARY}}"></div>
|
|
245
|
-
<div>
|
|
246
|
-
<p class="text-sm text-slate-800 dark:text-slate-200 font-medium">Corridor status
|
|
247
|
-
changed</p>
|
|
248
|
-
<p class="text-xs text-slate-500 mt-0.5">MYS-BGD corridor has been activated.</p>
|
|
249
|
-
<p class="text-xs text-slate-400 mt-1">15 minutes ago</p>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
<div class="px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 flex gap-3">
|
|
253
|
-
<div class="w-2 h-2 bg-transparent rounded-full mt-2 shrink-0 border border-slate-300"></div>
|
|
254
|
-
<div>
|
|
255
|
-
<p class="text-sm text-slate-600 dark:text-slate-400">Employer registration pending</p>
|
|
256
|
-
<p class="text-xs text-slate-500 mt-0.5">ABC Manufacturing Sdn Bhd requires approval.</p>
|
|
257
|
-
<p class="text-xs text-slate-400 mt-1">1 hour ago</p>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
<div class="px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 flex gap-3">
|
|
261
|
-
<div class="w-2 h-2 bg-transparent rounded-full mt-2 shrink-0 border border-slate-300"></div>
|
|
262
|
-
<div>
|
|
263
|
-
<p class="text-sm text-slate-600 dark:text-slate-400">System maintenance scheduled</p>
|
|
264
|
-
<p class="text-xs text-slate-500 mt-0.5">Scheduled downtime on Sunday 2:00 AM – 4:00 AM.</p>
|
|
265
|
-
<p class="text-xs text-slate-400 mt-1">3 hours ago</p>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/notifications"
|
|
270
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/notifications"
|
|
271
|
-
hx-target="#content-area"
|
|
272
|
-
hx-swap="innerHTML"
|
|
273
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/notifications"
|
|
274
|
-
@click="open = false"
|
|
275
|
-
class="block px-4 py-3 border-t border-slate-100 dark:border-slate-700 text-center text-sm font-medium hover:bg-slate-50 dark:hover:bg-slate-700 rounded-b-xl"
|
|
276
|
-
style="color: {{DESIGN_PRIMARY}}">
|
|
277
|
-
View all notifications
|
|
278
|
-
</a>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
|
|
282
|
-
<!-- Locale / Globe -->
|
|
283
|
-
<div class="relative" x-data="{ open: false }">
|
|
284
|
-
<button @click="open = !open" @keydown.escape.window="open = false"
|
|
285
|
-
class="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors duration-200">
|
|
286
|
-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
287
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
288
|
-
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
|
289
|
-
</svg>
|
|
290
|
-
</button>
|
|
291
|
-
<div x-show="open" @click.outside="open = false"
|
|
292
|
-
x-transition:enter="transition ease-out duration-150"
|
|
293
|
-
x-transition:enter-start="opacity-0 translate-y-1"
|
|
294
|
-
x-transition:enter-end="opacity-100 translate-y-0"
|
|
295
|
-
class="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl py-1 z-50"
|
|
296
|
-
style="display: none;">
|
|
297
|
-
<div class="px-4 py-2 border-b border-slate-100 dark:border-slate-700">
|
|
298
|
-
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Language</span>
|
|
299
|
-
</div>
|
|
300
|
-
<a href="javascript:void(0)" class="flex items-center gap-3 px-4 py-2 text-sm font-medium"
|
|
301
|
-
style="color: {{DESIGN_PRIMARY}}">
|
|
302
|
-
<span>🇬🇧</span> English (EN)
|
|
303
|
-
</a>
|
|
304
|
-
<a href="javascript:void(0)"
|
|
305
|
-
class="flex items-center gap-3 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
306
|
-
<span>🇲🇾</span> Bahasa Melayu (BM)
|
|
307
|
-
</a>
|
|
308
|
-
<a href="javascript:void(0)"
|
|
309
|
-
class="flex items-center gap-3 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
310
|
-
<span>🇧🇩</span> বাংলা (BN)
|
|
311
|
-
</a>
|
|
312
|
-
<a href="javascript:void(0)"
|
|
313
|
-
class="flex items-center gap-3 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
314
|
-
<span>🇮🇩</span> Bahasa Indonesia (ID)
|
|
315
|
-
</a>
|
|
316
|
-
</div>
|
|
317
|
-
</div>
|
|
318
|
-
|
|
319
|
-
<!-- Dark Mode Toggle -->
|
|
320
|
-
<button @click="toggleDark()"
|
|
321
|
-
class="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors duration-200"
|
|
322
|
-
:title="darkMode ? 'Switch to light mode' : 'Switch to dark mode'">
|
|
323
|
-
<!-- Moon icon (shown in light mode) -->
|
|
324
|
-
<svg x-show="!darkMode" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
325
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
326
|
-
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
|
|
327
|
-
</svg>
|
|
328
|
-
<!-- Sun icon (shown in dark mode) -->
|
|
329
|
-
<svg x-show="darkMode" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
330
|
-
style="display:none">
|
|
331
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
332
|
-
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
|
|
333
|
-
</svg>
|
|
334
|
-
</button>
|
|
335
|
-
|
|
336
|
-
<!-- Divider -->
|
|
337
|
-
<div class="w-px h-8 bg-slate-200 dark:bg-slate-600 mx-1"></div>
|
|
338
|
-
|
|
339
|
-
<!-- User Avatar + Dropdown -->
|
|
340
|
-
<div class="relative" x-data="{ open: false }">
|
|
341
|
-
<button @click="open = !open" @keydown.escape.window="open = false"
|
|
342
|
-
class="flex items-center gap-2 cursor-pointer p-1 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
|
|
343
|
-
<div class="w-8 h-8 rounded-full flex items-center justify-center font-semibold text-sm text-white"
|
|
344
|
-
style="background-color: {{DESIGN_PRIMARY}}">
|
|
345
|
-
U
|
|
346
|
-
</div>
|
|
347
|
-
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
348
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
349
|
-
</svg>
|
|
350
|
-
</button>
|
|
351
|
-
<div x-show="open" @click.outside="open = false"
|
|
352
|
-
x-transition:enter="transition ease-out duration-150"
|
|
353
|
-
x-transition:enter-start="opacity-0 translate-y-1"
|
|
354
|
-
x-transition:enter-end="opacity-100 translate-y-0"
|
|
355
|
-
class="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl py-1 z-50"
|
|
356
|
-
style="display: none;">
|
|
357
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/profile"
|
|
358
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/profile"
|
|
359
|
-
hx-target="#content-area"
|
|
360
|
-
hx-swap="innerHTML"
|
|
361
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/profile"
|
|
362
|
-
@click="open = false"
|
|
363
|
-
class="flex items-center gap-2 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
364
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
365
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
366
|
-
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
|
367
|
-
</svg>
|
|
368
|
-
Profile
|
|
369
|
-
</a>
|
|
370
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/account"
|
|
371
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/account"
|
|
372
|
-
hx-target="#content-area"
|
|
373
|
-
hx-swap="innerHTML"
|
|
374
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/account"
|
|
375
|
-
@click="open = false"
|
|
376
|
-
class="flex items-center gap-2 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
377
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
378
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
379
|
-
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
380
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
381
|
-
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
382
|
-
</svg>
|
|
383
|
-
Account
|
|
384
|
-
</a>
|
|
385
|
-
<div class="border-t border-slate-100 dark:border-slate-700 my-1"></div>
|
|
386
|
-
<a href="/"
|
|
387
|
-
class="flex items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20">
|
|
388
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
389
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
390
|
-
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
|
|
391
|
-
</svg>
|
|
392
|
-
Logout
|
|
393
|
-
</a>
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
396
|
-
</div>
|
|
397
|
-
</header>
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
> **Note:** In `header.html`, replace `{{APP_SLUG}}` at generation time but leave
|
|
401
|
-
> `{{ROLE}}` as-is — the Mockup Hub injects the actual role slug into the header partial
|
|
402
|
-
> at request time (`.replace(/\{\{ROLE\}\}/g, role)`), so one shared header works for
|
|
403
|
-
> every role.
|
|
404
|
-
|
|
405
|
-
---
|
|
406
|
-
|
|
407
|
-
## partials/footer.html Template
|
|
408
|
-
|
|
409
|
-
```html
|
|
410
|
-
<footer class="border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-6 py-3 flex items-center justify-between text-xs text-slate-400">
|
|
411
|
-
<span>© {{COPYRIGHT_YEAR}} {{APP_NAME}}. All rights reserved.</span>
|
|
412
|
-
<span>{{VERSION}}</span>
|
|
413
|
-
</footer>
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
---
|
|
417
|
-
|
|
418
|
-
## partials/sidebar-{role}.html Template
|
|
419
|
-
|
|
420
|
-
Replace `{{ROLE}}` with the role's snake_case name. The `hx-get` attribute fetches only
|
|
421
|
-
the content fragment; `hx-push-url` updates the browser URL so the page is bookmarkable.
|
|
422
|
-
|
|
423
|
-
```html
|
|
424
|
-
<aside class="w-64 bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 fixed top-16 bottom-0 left-0 overflow-y-auto z-20">
|
|
425
|
-
<nav class="py-4" x-data="{ active: window.location.pathname }">
|
|
426
|
-
|
|
427
|
-
<!-- Home -->
|
|
428
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/home"
|
|
429
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/home"
|
|
430
|
-
hx-target="#content-area"
|
|
431
|
-
hx-swap="innerHTML"
|
|
432
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/home"
|
|
433
|
-
@click="active = '/{{APP_SLUG}}/{{ROLE}}/home'"
|
|
434
|
-
:class="active.startsWith('/{{APP_SLUG}}/{{ROLE}}/home') ? 'bg-primary/5 text-primary border-r-2 border-primary font-medium' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-900'"
|
|
435
|
-
class="flex items-center gap-3 px-4 py-2.5 text-sm transition-colors duration-200 cursor-pointer">
|
|
436
|
-
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
437
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
438
|
-
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
|
439
|
-
</svg>
|
|
440
|
-
<span>Home</span>
|
|
441
|
-
</a>
|
|
442
|
-
|
|
443
|
-
<!-- Module separator -->
|
|
444
|
-
<div class="px-4 pt-4 pb-1">
|
|
445
|
-
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Menu</span>
|
|
446
|
-
</div>
|
|
447
|
-
|
|
448
|
-
<!-- Module menu items: repeat for each module assigned to this role -->
|
|
449
|
-
{{SIDEBAR_MENU}}
|
|
450
|
-
|
|
451
|
-
</nav>
|
|
452
|
-
</aside>
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
### Sidebar Active Menu Item Template
|
|
456
|
-
|
|
457
|
-
```html
|
|
458
|
-
<!-- Active state -->
|
|
459
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}"
|
|
460
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}"
|
|
461
|
-
hx-target="#content-area"
|
|
462
|
-
hx-swap="innerHTML"
|
|
463
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}"
|
|
464
|
-
@click="active = '/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}'"
|
|
465
|
-
:class="active.startsWith('/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}') ? 'bg-primary/5 text-primary border-r-2 border-primary font-medium' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-900'"
|
|
466
|
-
class="flex items-center gap-3 px-4 py-2.5 text-sm transition-colors duration-200 cursor-pointer">
|
|
467
|
-
<svg class="w-5 h-5 shrink-0"><!-- module-appropriate icon --></svg>
|
|
468
|
-
<span>{{MODULE_NAME}}</span>
|
|
469
|
-
</a>
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
### Suggested SVG Icons by Module Type
|
|
473
|
-
|
|
474
|
-
| Module Pattern | Icon |
|
|
475
|
-
|---------------|------|
|
|
476
|
-
| Document / File | paper with lines (`DocumentTextIcon`) |
|
|
477
|
-
| Location / Geography | map pin (`MapPinIcon`) |
|
|
478
|
-
| Corridor / Route | bidirectional arrows (`ArrowsRightLeftIcon`) |
|
|
479
|
-
| Recruitment / Steps | ordered list (`QueueListIcon`) |
|
|
480
|
-
| Employer / Company | office building (`BuildingOfficeIcon`) |
|
|
481
|
-
| Classification / Category | tag (`TagIcon`) |
|
|
482
|
-
| Quota / Allocation | pie chart (`ChartPieIcon`) |
|
|
483
|
-
| Job / Demand | briefcase (`BriefcaseIcon`) |
|
|
484
|
-
| Employee / Person | group of people (`UserGroupIcon`) |
|
|
485
|
-
| Settings / Config | gear (`CogIcon`) |
|
|
486
|
-
|
|
487
|
-
---
|
|
488
|
-
|
|
489
|
-
## Content Fragment Template
|
|
490
|
-
|
|
491
|
-
Content files contain **only the page content area** — no `<html>`, `<head>`, `<body>`,
|
|
492
|
-
layout, or Tailwind config. They are loaded via HTMX and injected into `#content-area`.
|
|
493
|
-
|
|
494
|
-
```html
|
|
495
|
-
<!-- {{PAGE_TITLE}} — {{ROLE}} -->
|
|
496
|
-
<!-- User story traceability: {{US_TAG}} [{{US_VERSION}}] -->
|
|
497
|
-
|
|
498
|
-
<!-- Breadcrumb -->
|
|
499
|
-
<div class="px-6 py-3 border-b border-slate-100 dark:border-slate-700 bg-white dark:bg-slate-800">
|
|
500
|
-
<nav class="flex items-center gap-2 text-sm text-slate-500">
|
|
501
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/home"
|
|
502
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/home"
|
|
503
|
-
hx-target="#content-area"
|
|
504
|
-
hx-swap="innerHTML"
|
|
505
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/home"
|
|
506
|
-
class="hover:text-slate-700 dark:hover:text-slate-300 cursor-pointer">Home</a>
|
|
507
|
-
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
508
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
509
|
-
</svg>
|
|
510
|
-
<span class="text-slate-800 dark:text-slate-200 font-medium">{{PAGE_TITLE}}</span>
|
|
511
|
-
</nav>
|
|
512
|
-
</div>
|
|
513
|
-
|
|
514
|
-
<!-- Page Content -->
|
|
515
|
-
<div class="flex-1 p-6">
|
|
516
|
-
{{MAIN_CONTENT}}
|
|
517
|
-
</div>
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
### Breadcrumb for Sub-screens (detail / edit / create)
|
|
521
|
-
|
|
522
|
-
```html
|
|
523
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/home" hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/home" hx-target="#content-area" hx-swap="innerHTML" hx-push-url="/{{APP_SLUG}}/{{ROLE}}/home" class="hover:text-slate-700 cursor-pointer">Home</a>
|
|
524
|
-
<svg ...><path .../></svg>
|
|
525
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}" hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}" hx-target="#content-area" hx-swap="innerHTML" hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}" class="hover:text-slate-700 cursor-pointer">{{MODULE_NAME}}</a>
|
|
526
|
-
<svg ...><path .../></svg>
|
|
527
|
-
<span class="text-slate-800 dark:text-slate-200 font-medium">{{PAGE_TITLE}}</span>
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
---
|
|
531
|
-
|
|
532
|
-
## Link Rules Inside Content Fragments
|
|
533
|
-
|
|
534
|
-
### Navigation links (within the app)
|
|
535
|
-
|
|
536
|
-
Use HTMX attributes for in-app navigation — never full page reloads for screen transitions:
|
|
537
|
-
|
|
538
|
-
```html
|
|
539
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{TARGET_PAGE}}"
|
|
540
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{TARGET_PAGE}}"
|
|
541
|
-
hx-target="#content-area"
|
|
542
|
-
hx-swap="innerHTML"
|
|
543
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{TARGET_PAGE}}"
|
|
544
|
-
class="...">
|
|
545
|
-
View Details
|
|
546
|
-
</a>
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
### Image links — MUST open in new tab
|
|
550
|
-
|
|
551
|
-
```html
|
|
552
|
-
<a href="/{{APP_SLUG}}/static/images/sample.jpg" target="_blank" rel="noopener noreferrer" class="...">
|
|
553
|
-
View Image
|
|
554
|
-
</a>
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
### PDF / document links — MUST open in new tab
|
|
558
|
-
|
|
559
|
-
```html
|
|
560
|
-
<a href="/{{APP_SLUG}}/static/docs/sample.pdf" target="_blank" rel="noopener noreferrer" class="...">
|
|
561
|
-
<svg ...><!-- pdf icon --></svg>
|
|
562
|
-
View PDF
|
|
563
|
-
</a>
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
### Action buttons in tables
|
|
567
|
-
|
|
568
|
-
```html
|
|
569
|
-
<!-- View / Details → HTMX navigation -->
|
|
570
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_detail"
|
|
571
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_detail"
|
|
572
|
-
hx-target="#content-area"
|
|
573
|
-
hx-swap="innerHTML"
|
|
574
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_detail"
|
|
575
|
-
class="text-xs px-2 py-1 rounded border border-primary text-primary hover:bg-primary/5">
|
|
576
|
-
View
|
|
577
|
-
</a>
|
|
578
|
-
|
|
579
|
-
<!-- Edit → HTMX navigation -->
|
|
580
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_edit"
|
|
581
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_edit"
|
|
582
|
-
hx-target="#content-area"
|
|
583
|
-
hx-swap="innerHTML"
|
|
584
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_edit"
|
|
585
|
-
class="text-xs px-2 py-1 rounded border border-slate-300 text-slate-600 hover:bg-slate-50">
|
|
586
|
-
Edit
|
|
587
|
-
</a>
|
|
588
|
-
|
|
589
|
-
<!-- Delete → Alpine.js confirm dialog (no href navigation) -->
|
|
590
|
-
<button x-data="{ confirm: false }"
|
|
591
|
-
@click="confirm = true"
|
|
592
|
-
x-show="!confirm"
|
|
593
|
-
class="text-xs px-2 py-1 rounded border border-red-300 text-red-600 hover:bg-red-50">
|
|
594
|
-
Delete
|
|
595
|
-
</button>
|
|
596
|
-
<div x-data="{ confirm: false }" x-show="confirm" class="flex items-center gap-2" style="display:none">
|
|
597
|
-
<span class="text-xs text-red-600">Sure?</span>
|
|
598
|
-
<button class="text-xs px-2 py-1 bg-red-600 text-white rounded">Yes, Delete</button>
|
|
599
|
-
<button @click="confirm = false" class="text-xs px-2 py-1 border border-slate-300 rounded">Cancel</button>
|
|
600
|
-
</div>
|
|
601
|
-
|
|
602
|
-
<!-- Add New → HTMX navigation -->
|
|
603
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_create"
|
|
604
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_create"
|
|
605
|
-
hx-target="#content-area"
|
|
606
|
-
hx-swap="innerHTML"
|
|
607
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_create"
|
|
608
|
-
class="px-4 py-2 rounded-lg text-sm text-white"
|
|
609
|
-
style="background-color: {{DESIGN_PRIMARY}}">
|
|
610
|
-
+ Add New
|
|
611
|
-
</a>
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
### Tab navigation in detail screens
|
|
615
|
-
|
|
616
|
-
Each tab links to its own content page via HTMX:
|
|
617
|
-
|
|
618
|
-
```html
|
|
619
|
-
<div class="flex border-b border-slate-200 mb-6">
|
|
620
|
-
<!-- Active tab -->
|
|
621
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_overview"
|
|
622
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_overview"
|
|
623
|
-
hx-target="#content-area"
|
|
624
|
-
hx-swap="innerHTML"
|
|
625
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_overview"
|
|
626
|
-
class="px-4 py-2 text-sm font-medium border-b-2 -mb-px"
|
|
627
|
-
style="color: {{DESIGN_PRIMARY}}; border-color: {{DESIGN_PRIMARY}}">
|
|
628
|
-
Overview
|
|
629
|
-
</a>
|
|
630
|
-
<!-- Inactive tab -->
|
|
631
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_documents"
|
|
632
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_documents"
|
|
633
|
-
hx-target="#content-area"
|
|
634
|
-
hx-swap="innerHTML"
|
|
635
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_documents"
|
|
636
|
-
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-slate-500 hover:text-slate-700 -mb-px">
|
|
637
|
-
Documents
|
|
638
|
-
</a>
|
|
639
|
-
</div>
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
### Back / Cancel buttons
|
|
643
|
-
|
|
644
|
-
```html
|
|
645
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{PARENT_MODULE}}"
|
|
646
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{PARENT_MODULE}}"
|
|
647
|
-
hx-target="#content-area"
|
|
648
|
-
hx-swap="innerHTML"
|
|
649
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{PARENT_MODULE}}"
|
|
650
|
-
class="px-4 py-2 border border-slate-300 rounded-lg text-sm text-slate-600 hover:bg-slate-50">
|
|
651
|
-
← Back to List
|
|
652
|
-
</a>
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
### Pagination links
|
|
656
|
-
|
|
657
|
-
```html
|
|
658
|
-
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}"
|
|
659
|
-
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}"
|
|
660
|
-
hx-target="#content-area"
|
|
661
|
-
hx-swap="innerHTML"
|
|
662
|
-
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}"
|
|
663
|
-
class="px-3 py-1.5 text-sm border border-slate-300 rounded hover:bg-slate-50">
|
|
664
|
-
2
|
|
665
|
-
</a>
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
---
|
|
669
|
-
|
|
670
|
-
## Common Screens Content
|
|
671
|
-
|
|
672
|
-
### home.html content fragment
|
|
673
|
-
|
|
674
|
-
```html
|
|
675
|
-
<!-- Dashboard Home -->
|
|
676
|
-
<div class="px-6 py-3 border-b border-slate-100 dark:border-slate-700 bg-white dark:bg-slate-800">
|
|
677
|
-
<nav class="flex items-center gap-2 text-sm text-slate-500">
|
|
678
|
-
<span class="text-slate-800 dark:text-slate-200 font-medium">Home</span>
|
|
679
|
-
</nav>
|
|
680
|
-
</div>
|
|
681
|
-
<div class="flex-1 p-6">
|
|
682
|
-
<h1 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">Welcome back</h1>
|
|
683
|
-
<p class="text-slate-500 mb-6">{{APP_NAME}} — {{ROLE_DISPLAY}}</p>
|
|
684
|
-
<!-- Summary stat cards -->
|
|
685
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
686
|
-
<!-- Repeat: one card per module -->
|
|
687
|
-
</div>
|
|
688
|
-
<!-- Recent activity table -->
|
|
689
|
-
</div>
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
### profile.html content fragment
|
|
693
|
-
|
|
694
|
-
User profile: avatar, full name, email, role badge, personal info form (read-only), Edit button.
|
|
695
|
-
|
|
696
|
-
### account.html content fragment
|
|
697
|
-
|
|
698
|
-
Account settings: Change Password form, Notification Preferences toggles (Alpine.js),
|
|
699
|
-
Language/Locale selector, Session Management table.
|
|
700
|
-
|
|
701
|
-
### notifications.html content fragment
|
|
702
|
-
|
|
703
|
-
Notifications list: Filter tabs (All / Unread / Read with Alpine.js tab state),
|
|
704
|
-
notification cards with icon / title / message / timestamp / unread dot,
|
|
705
|
-
Mark all as read button.
|
|
706
|
-
|
|
707
|
-
---
|
|
708
|
-
|
|
709
|
-
## Quick-Start Instructions (append to output summary)
|
|
710
|
-
|
|
711
|
-
```
|
|
712
|
-
Quick Start
|
|
713
|
-
===========
|
|
714
|
-
1. cd <root>/mockup
|
|
715
|
-
2. npm start (zero dependencies — no npm install required)
|
|
716
|
-
3. Open http://localhost:
|
|
717
|
-
|
|
718
|
-
-
|
|
719
|
-
|
|
720
|
-
|
|
1
|
+
# Admin Layout Architecture — Alpine.js + HTMX Assets (served by the Mockup Hub)
|
|
2
|
+
|
|
3
|
+
The application's mockup folder contains **assets only**. The shared **Mockup Hub**
|
|
4
|
+
(`<root>/mockup/server.js` — see [mockup-hub-template.md](mockup-hub-template.md))
|
|
5
|
+
assembles pages from partials server-side at `/{app_slug}/{role}/{page}`. **HTMX**
|
|
6
|
+
performs partial content swapping during navigation, and **Alpine.js** powers
|
|
7
|
+
interactive UI components (dropdowns, dark mode toggle).
|
|
8
|
+
|
|
9
|
+
`{{APP_SLUG}}` below is replaced **at generation time** with the application slug
|
|
10
|
+
(app folder name minus the leading `<number>_` prefix). It is constant for the whole
|
|
11
|
+
mockup folder.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
mockup/
|
|
19
|
+
mockup-manifest.json # Hub discovery manifest (see mockup-hub-template.md)
|
|
20
|
+
MOCKUP.html # Per-app screen index (served by hub at /{app_slug})
|
|
21
|
+
partials/
|
|
22
|
+
shell.html # Page shell (assembled by server)
|
|
23
|
+
header.html # Top header (Alpine.js powered)
|
|
24
|
+
footer.html # Bottom footer
|
|
25
|
+
sidebar-{role_snake_case}.html # One sidebar per role
|
|
26
|
+
{role_snake_case}/
|
|
27
|
+
content/
|
|
28
|
+
home.html # Content fragment (no layout)
|
|
29
|
+
profile.html
|
|
30
|
+
account.html
|
|
31
|
+
notifications.html
|
|
32
|
+
{module_snake_case}.html
|
|
33
|
+
{module_snake_case}_detail.html
|
|
34
|
+
{module_snake_case}_create.html
|
|
35
|
+
{module_snake_case}_edit.html
|
|
36
|
+
...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Placeholders Reference
|
|
42
|
+
|
|
43
|
+
| Placeholder | Description |
|
|
44
|
+
|-------------|-------------|
|
|
45
|
+
| `{{APP_NAME}}` | Application name (e.g., "Hub Middleware") |
|
|
46
|
+
| `{{APP_SLUG}}` | App slug used as URL prefix (e.g., "hub_middleware") — replaced at generation time |
|
|
47
|
+
| `{{PAGE_TITLE}}` | Screen title (e.g., "Location Information") |
|
|
48
|
+
| `{{ROLE}}` | Role in snake_case (e.g., "hub_administrator") — in `header.html` it is re-injected by the hub at request time |
|
|
49
|
+
| `{{SIDEBAR_MENU}}` | Navigation menu items HTML |
|
|
50
|
+
| `{{BREADCRUMB}}` | Breadcrumb trail HTML |
|
|
51
|
+
| `{{MAIN_CONTENT}}` | Screen-specific content |
|
|
52
|
+
| `{{COPYRIGHT_YEAR}}` | Current year |
|
|
53
|
+
| `{{VERSION}}` | Latest version from PRD.md |
|
|
54
|
+
| `{{DESIGN_PRIMARY}}` | Primary color hex from design system |
|
|
55
|
+
| `{{DESIGN_SECONDARY}}` | Secondary color hex from design system |
|
|
56
|
+
| `{{DESIGN_ACCENT}}` | Accent/CTA color hex |
|
|
57
|
+
| `{{DESIGN_BG}}` | Background color hex |
|
|
58
|
+
| `{{DESIGN_TEXT}}` | Text color hex |
|
|
59
|
+
| `{{HEADING_FONT}}` | Heading font |
|
|
60
|
+
| `{{BODY_FONT}}` | Body font |
|
|
61
|
+
| `{{GOOGLE_FONTS_URL}}` | Google Fonts import URL |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Server
|
|
66
|
+
|
|
67
|
+
There is **no per-app server**. The shared Mockup Hub (`<root>/mockup/server.js`, created
|
|
68
|
+
per the Ensure-Hub rules in [mockup-hub-template.md](mockup-hub-template.md)) provides:
|
|
69
|
+
|
|
70
|
+
- `GET /{app_slug}` → this app's `MOCKUP.html`
|
|
71
|
+
- `GET /{app_slug}/{role}` → redirect to `/{app_slug}/{role}/home`
|
|
72
|
+
- `GET /{app_slug}/{role}/{page}` → shell + header + sidebar + content + footer assembly
|
|
73
|
+
- `GET /api/content/{app_slug}/{role}/{page}` → content fragment (HTMX swaps)
|
|
74
|
+
- `GET /{app_slug}/static/*`, `GET /{app_slug}/reports/*` → static assets
|
|
75
|
+
- The hub re-injects `{{ROLE}}` into `header.html` at request time
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## partials/shell.html Template
|
|
80
|
+
|
|
81
|
+
The shell is the outer HTML document. It is assembled server-side and NOT a partial
|
|
82
|
+
served to the browser — the hub substitutes `{{HEADER}}`, `{{SIDEBAR}}`,
|
|
83
|
+
`{{CONTENT}}`, and `{{FOOTER}}` before responding.
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<!DOCTYPE html>
|
|
87
|
+
<html lang="en" x-data="appShell()" :class="{ 'dark': darkMode }">
|
|
88
|
+
<head>
|
|
89
|
+
<meta charset="UTF-8">
|
|
90
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
91
|
+
<title>{{APP_NAME}}</title>
|
|
92
|
+
|
|
93
|
+
<!-- Tailwind CSS -->
|
|
94
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
95
|
+
|
|
96
|
+
<!-- HTMX -->
|
|
97
|
+
<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
|
|
98
|
+
|
|
99
|
+
<!-- Alpine.js -->
|
|
100
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
|
|
101
|
+
|
|
102
|
+
<!-- Google Fonts -->
|
|
103
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
104
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
105
|
+
<link href="{{GOOGLE_FONTS_URL}}" rel="stylesheet">
|
|
106
|
+
|
|
107
|
+
<script>
|
|
108
|
+
tailwind.config = {
|
|
109
|
+
darkMode: 'class',
|
|
110
|
+
theme: {
|
|
111
|
+
extend: {
|
|
112
|
+
colors: {
|
|
113
|
+
primary: '{{DESIGN_PRIMARY}}',
|
|
114
|
+
secondary: '{{DESIGN_SECONDARY}}',
|
|
115
|
+
accent: '{{DESIGN_ACCENT}}',
|
|
116
|
+
},
|
|
117
|
+
fontFamily: {
|
|
118
|
+
heading: ['{{HEADING_FONT}}', 'sans-serif'],
|
|
119
|
+
body: ['{{BODY_FONT}}', 'sans-serif'],
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function appShell() {
|
|
126
|
+
return {
|
|
127
|
+
darkMode: localStorage.getItem('darkMode') === 'true',
|
|
128
|
+
toggleDark() {
|
|
129
|
+
this.darkMode = !this.darkMode;
|
|
130
|
+
localStorage.setItem('darkMode', this.darkMode);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<style>
|
|
137
|
+
body { font-family: '{{BODY_FONT}}', sans-serif; }
|
|
138
|
+
h1, h2, h3, h4, h5, h6 { font-family: '{{HEADING_FONT}}', sans-serif; }
|
|
139
|
+
::-webkit-scrollbar { width: 6px; }
|
|
140
|
+
::-webkit-scrollbar-track { background: #f1f5f9; }
|
|
141
|
+
::-webkit-scrollbar-thumb { background: #94a3b8; border-radius: 3px; }
|
|
142
|
+
::-webkit-scrollbar-thumb:hover { background: #64748b; }
|
|
143
|
+
/* HTMX loading indicator */
|
|
144
|
+
.htmx-request #content-spinner { display: flex; }
|
|
145
|
+
.htmx-request.htmx-request #content-area { opacity: 0.4; }
|
|
146
|
+
</style>
|
|
147
|
+
</head>
|
|
148
|
+
<body class="bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 min-h-screen flex flex-col">
|
|
149
|
+
|
|
150
|
+
<!-- ── Header (injected by server) ─────────────────────────────────── -->
|
|
151
|
+
{{HEADER}}
|
|
152
|
+
|
|
153
|
+
<!-- ── Body: Sidebar + Main ─────────────────────────────────────────── -->
|
|
154
|
+
<div class="flex pt-16 min-h-[calc(100vh-4rem)]">
|
|
155
|
+
|
|
156
|
+
<!-- ── Sidebar (injected by server) ──────────────────────────────── -->
|
|
157
|
+
{{SIDEBAR}}
|
|
158
|
+
|
|
159
|
+
<!-- ── Main ──────────────────────────────────────────────────────── -->
|
|
160
|
+
<main class="flex-1 ml-64 flex flex-col min-w-0">
|
|
161
|
+
|
|
162
|
+
<!-- Loading spinner (shown while HTMX is fetching) -->
|
|
163
|
+
<div id="content-spinner" class="hidden fixed inset-0 bg-white/50 z-50 items-center justify-center">
|
|
164
|
+
<div class="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- ── Content area (HTMX target) ─────────────────────────────── -->
|
|
168
|
+
<div id="content-area" class="flex-1 flex flex-col transition-opacity duration-200">
|
|
169
|
+
{{CONTENT}}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- ── Footer (injected by server) ──────────────────────────────── -->
|
|
173
|
+
{{FOOTER}}
|
|
174
|
+
|
|
175
|
+
</main>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
</body>
|
|
179
|
+
</html>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## partials/header.html Template
|
|
185
|
+
|
|
186
|
+
Alpine.js `x-data` scopes are per-component, declared on each dropdown wrapper.
|
|
187
|
+
The dark mode toggle dispatches to the parent `appShell()` context via `$dispatch`.
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
|
|
191
|
+
<header class="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 h-16 flex items-center justify-between px-6 fixed top-0 left-0 right-0 z-30 shadow-sm">
|
|
192
|
+
|
|
193
|
+
<!-- Left: Logo + App Name -->
|
|
194
|
+
<div class="flex items-center gap-3">
|
|
195
|
+
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background-color: {{DESIGN_PRIMARY}}">
|
|
196
|
+
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
197
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
198
|
+
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
|
|
199
|
+
</svg>
|
|
200
|
+
</div>
|
|
201
|
+
<span class="font-semibold text-lg text-slate-900 dark:text-slate-100">{{APP_NAME}}</span>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- Right: Controls -->
|
|
205
|
+
<div class="flex items-center gap-2">
|
|
206
|
+
|
|
207
|
+
<!-- Notification Bell -->
|
|
208
|
+
<div class="relative" x-data="{ open: false }">
|
|
209
|
+
<button @click="open = !open" @keydown.escape.window="open = false"
|
|
210
|
+
class="relative p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors duration-200">
|
|
211
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
212
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
213
|
+
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
|
|
214
|
+
</svg>
|
|
215
|
+
<span class="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full"></span>
|
|
216
|
+
</button>
|
|
217
|
+
<!-- Notification Dropdown -->
|
|
218
|
+
<div x-show="open" @click.outside="open = false"
|
|
219
|
+
x-transition:enter="transition ease-out duration-150"
|
|
220
|
+
x-transition:enter-start="opacity-0 translate-y-1"
|
|
221
|
+
x-transition:enter-end="opacity-100 translate-y-0"
|
|
222
|
+
x-transition:leave="transition ease-in duration-100"
|
|
223
|
+
x-transition:leave-start="opacity-100 translate-y-0"
|
|
224
|
+
x-transition:leave-end="opacity-0 translate-y-1"
|
|
225
|
+
class="absolute right-0 top-full mt-2 w-80 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl z-50"
|
|
226
|
+
style="display: none;">
|
|
227
|
+
<div class="px-4 py-3 border-b border-slate-100 dark:border-slate-700 flex items-center justify-between">
|
|
228
|
+
<span class="font-medium text-sm text-slate-800 dark:text-slate-200">Notifications</span>
|
|
229
|
+
<button class="text-xs text-primary cursor-pointer hover:underline">Mark all read</button>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="max-h-72 overflow-y-auto divide-y divide-slate-50 dark:divide-slate-700">
|
|
232
|
+
<div class="px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 flex gap-3">
|
|
233
|
+
<div class="w-2 h-2 rounded-full mt-2 shrink-0"
|
|
234
|
+
style="background-color: {{DESIGN_PRIMARY}}"></div>
|
|
235
|
+
<div>
|
|
236
|
+
<p class="text-sm text-slate-800 dark:text-slate-200 font-medium">New document uploaded</p>
|
|
237
|
+
<p class="text-xs text-slate-500 mt-0.5">A new employment contract has been uploaded for
|
|
238
|
+
review.</p>
|
|
239
|
+
<p class="text-xs text-slate-400 mt-1">2 minutes ago</p>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 flex gap-3">
|
|
243
|
+
<div class="w-2 h-2 rounded-full mt-2 shrink-0"
|
|
244
|
+
style="background-color: {{DESIGN_PRIMARY}}"></div>
|
|
245
|
+
<div>
|
|
246
|
+
<p class="text-sm text-slate-800 dark:text-slate-200 font-medium">Corridor status
|
|
247
|
+
changed</p>
|
|
248
|
+
<p class="text-xs text-slate-500 mt-0.5">MYS-BGD corridor has been activated.</p>
|
|
249
|
+
<p class="text-xs text-slate-400 mt-1">15 minutes ago</p>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 flex gap-3">
|
|
253
|
+
<div class="w-2 h-2 bg-transparent rounded-full mt-2 shrink-0 border border-slate-300"></div>
|
|
254
|
+
<div>
|
|
255
|
+
<p class="text-sm text-slate-600 dark:text-slate-400">Employer registration pending</p>
|
|
256
|
+
<p class="text-xs text-slate-500 mt-0.5">ABC Manufacturing Sdn Bhd requires approval.</p>
|
|
257
|
+
<p class="text-xs text-slate-400 mt-1">1 hour ago</p>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 flex gap-3">
|
|
261
|
+
<div class="w-2 h-2 bg-transparent rounded-full mt-2 shrink-0 border border-slate-300"></div>
|
|
262
|
+
<div>
|
|
263
|
+
<p class="text-sm text-slate-600 dark:text-slate-400">System maintenance scheduled</p>
|
|
264
|
+
<p class="text-xs text-slate-500 mt-0.5">Scheduled downtime on Sunday 2:00 AM – 4:00 AM.</p>
|
|
265
|
+
<p class="text-xs text-slate-400 mt-1">3 hours ago</p>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/notifications"
|
|
270
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/notifications"
|
|
271
|
+
hx-target="#content-area"
|
|
272
|
+
hx-swap="innerHTML"
|
|
273
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/notifications"
|
|
274
|
+
@click="open = false"
|
|
275
|
+
class="block px-4 py-3 border-t border-slate-100 dark:border-slate-700 text-center text-sm font-medium hover:bg-slate-50 dark:hover:bg-slate-700 rounded-b-xl"
|
|
276
|
+
style="color: {{DESIGN_PRIMARY}}">
|
|
277
|
+
View all notifications
|
|
278
|
+
</a>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<!-- Locale / Globe -->
|
|
283
|
+
<div class="relative" x-data="{ open: false }">
|
|
284
|
+
<button @click="open = !open" @keydown.escape.window="open = false"
|
|
285
|
+
class="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors duration-200">
|
|
286
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
287
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
288
|
+
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
|
289
|
+
</svg>
|
|
290
|
+
</button>
|
|
291
|
+
<div x-show="open" @click.outside="open = false"
|
|
292
|
+
x-transition:enter="transition ease-out duration-150"
|
|
293
|
+
x-transition:enter-start="opacity-0 translate-y-1"
|
|
294
|
+
x-transition:enter-end="opacity-100 translate-y-0"
|
|
295
|
+
class="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl py-1 z-50"
|
|
296
|
+
style="display: none;">
|
|
297
|
+
<div class="px-4 py-2 border-b border-slate-100 dark:border-slate-700">
|
|
298
|
+
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Language</span>
|
|
299
|
+
</div>
|
|
300
|
+
<a href="javascript:void(0)" class="flex items-center gap-3 px-4 py-2 text-sm font-medium"
|
|
301
|
+
style="color: {{DESIGN_PRIMARY}}">
|
|
302
|
+
<span>🇬🇧</span> English (EN)
|
|
303
|
+
</a>
|
|
304
|
+
<a href="javascript:void(0)"
|
|
305
|
+
class="flex items-center gap-3 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
306
|
+
<span>🇲🇾</span> Bahasa Melayu (BM)
|
|
307
|
+
</a>
|
|
308
|
+
<a href="javascript:void(0)"
|
|
309
|
+
class="flex items-center gap-3 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
310
|
+
<span>🇧🇩</span> বাংলা (BN)
|
|
311
|
+
</a>
|
|
312
|
+
<a href="javascript:void(0)"
|
|
313
|
+
class="flex items-center gap-3 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
314
|
+
<span>🇮🇩</span> Bahasa Indonesia (ID)
|
|
315
|
+
</a>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<!-- Dark Mode Toggle -->
|
|
320
|
+
<button @click="toggleDark()"
|
|
321
|
+
class="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors duration-200"
|
|
322
|
+
:title="darkMode ? 'Switch to light mode' : 'Switch to dark mode'">
|
|
323
|
+
<!-- Moon icon (shown in light mode) -->
|
|
324
|
+
<svg x-show="!darkMode" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
325
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
326
|
+
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
|
|
327
|
+
</svg>
|
|
328
|
+
<!-- Sun icon (shown in dark mode) -->
|
|
329
|
+
<svg x-show="darkMode" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
330
|
+
style="display:none">
|
|
331
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
332
|
+
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
|
|
333
|
+
</svg>
|
|
334
|
+
</button>
|
|
335
|
+
|
|
336
|
+
<!-- Divider -->
|
|
337
|
+
<div class="w-px h-8 bg-slate-200 dark:bg-slate-600 mx-1"></div>
|
|
338
|
+
|
|
339
|
+
<!-- User Avatar + Dropdown -->
|
|
340
|
+
<div class="relative" x-data="{ open: false }">
|
|
341
|
+
<button @click="open = !open" @keydown.escape.window="open = false"
|
|
342
|
+
class="flex items-center gap-2 cursor-pointer p-1 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
|
|
343
|
+
<div class="w-8 h-8 rounded-full flex items-center justify-center font-semibold text-sm text-white"
|
|
344
|
+
style="background-color: {{DESIGN_PRIMARY}}">
|
|
345
|
+
U
|
|
346
|
+
</div>
|
|
347
|
+
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
348
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
349
|
+
</svg>
|
|
350
|
+
</button>
|
|
351
|
+
<div x-show="open" @click.outside="open = false"
|
|
352
|
+
x-transition:enter="transition ease-out duration-150"
|
|
353
|
+
x-transition:enter-start="opacity-0 translate-y-1"
|
|
354
|
+
x-transition:enter-end="opacity-100 translate-y-0"
|
|
355
|
+
class="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl py-1 z-50"
|
|
356
|
+
style="display: none;">
|
|
357
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/profile"
|
|
358
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/profile"
|
|
359
|
+
hx-target="#content-area"
|
|
360
|
+
hx-swap="innerHTML"
|
|
361
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/profile"
|
|
362
|
+
@click="open = false"
|
|
363
|
+
class="flex items-center gap-2 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
364
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
365
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
366
|
+
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
|
367
|
+
</svg>
|
|
368
|
+
Profile
|
|
369
|
+
</a>
|
|
370
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/account"
|
|
371
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/account"
|
|
372
|
+
hx-target="#content-area"
|
|
373
|
+
hx-swap="innerHTML"
|
|
374
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/account"
|
|
375
|
+
@click="open = false"
|
|
376
|
+
class="flex items-center gap-2 px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700">
|
|
377
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
378
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
379
|
+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
380
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
381
|
+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
382
|
+
</svg>
|
|
383
|
+
Account
|
|
384
|
+
</a>
|
|
385
|
+
<div class="border-t border-slate-100 dark:border-slate-700 my-1"></div>
|
|
386
|
+
<a href="/"
|
|
387
|
+
class="flex items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20">
|
|
388
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
389
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
390
|
+
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
|
|
391
|
+
</svg>
|
|
392
|
+
Logout
|
|
393
|
+
</a>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</header>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
> **Note:** In `header.html`, replace `{{APP_SLUG}}` at generation time but leave
|
|
401
|
+
> `{{ROLE}}` as-is — the Mockup Hub injects the actual role slug into the header partial
|
|
402
|
+
> at request time (`.replace(/\{\{ROLE\}\}/g, role)`), so one shared header works for
|
|
403
|
+
> every role.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## partials/footer.html Template
|
|
408
|
+
|
|
409
|
+
```html
|
|
410
|
+
<footer class="border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-6 py-3 flex items-center justify-between text-xs text-slate-400">
|
|
411
|
+
<span>© {{COPYRIGHT_YEAR}} {{APP_NAME}}. All rights reserved.</span>
|
|
412
|
+
<span>{{VERSION}}</span>
|
|
413
|
+
</footer>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## partials/sidebar-{role}.html Template
|
|
419
|
+
|
|
420
|
+
Replace `{{ROLE}}` with the role's snake_case name. The `hx-get` attribute fetches only
|
|
421
|
+
the content fragment; `hx-push-url` updates the browser URL so the page is bookmarkable.
|
|
422
|
+
|
|
423
|
+
```html
|
|
424
|
+
<aside class="w-64 bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 fixed top-16 bottom-0 left-0 overflow-y-auto z-20">
|
|
425
|
+
<nav class="py-4" x-data="{ active: window.location.pathname }">
|
|
426
|
+
|
|
427
|
+
<!-- Home -->
|
|
428
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/home"
|
|
429
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/home"
|
|
430
|
+
hx-target="#content-area"
|
|
431
|
+
hx-swap="innerHTML"
|
|
432
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/home"
|
|
433
|
+
@click="active = '/{{APP_SLUG}}/{{ROLE}}/home'"
|
|
434
|
+
:class="active.startsWith('/{{APP_SLUG}}/{{ROLE}}/home') ? 'bg-primary/5 text-primary border-r-2 border-primary font-medium' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-900'"
|
|
435
|
+
class="flex items-center gap-3 px-4 py-2.5 text-sm transition-colors duration-200 cursor-pointer">
|
|
436
|
+
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
437
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
438
|
+
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
|
439
|
+
</svg>
|
|
440
|
+
<span>Home</span>
|
|
441
|
+
</a>
|
|
442
|
+
|
|
443
|
+
<!-- Module separator -->
|
|
444
|
+
<div class="px-4 pt-4 pb-1">
|
|
445
|
+
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Menu</span>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<!-- Module menu items: repeat for each module assigned to this role -->
|
|
449
|
+
{{SIDEBAR_MENU}}
|
|
450
|
+
|
|
451
|
+
</nav>
|
|
452
|
+
</aside>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Sidebar Active Menu Item Template
|
|
456
|
+
|
|
457
|
+
```html
|
|
458
|
+
<!-- Active state -->
|
|
459
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}"
|
|
460
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}"
|
|
461
|
+
hx-target="#content-area"
|
|
462
|
+
hx-swap="innerHTML"
|
|
463
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}"
|
|
464
|
+
@click="active = '/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}'"
|
|
465
|
+
:class="active.startsWith('/{{APP_SLUG}}/{{ROLE}}/{{SCREEN_FILE_NO_EXT}}') ? 'bg-primary/5 text-primary border-r-2 border-primary font-medium' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-900'"
|
|
466
|
+
class="flex items-center gap-3 px-4 py-2.5 text-sm transition-colors duration-200 cursor-pointer">
|
|
467
|
+
<svg class="w-5 h-5 shrink-0"><!-- module-appropriate icon --></svg>
|
|
468
|
+
<span>{{MODULE_NAME}}</span>
|
|
469
|
+
</a>
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Suggested SVG Icons by Module Type
|
|
473
|
+
|
|
474
|
+
| Module Pattern | Icon |
|
|
475
|
+
|---------------|------|
|
|
476
|
+
| Document / File | paper with lines (`DocumentTextIcon`) |
|
|
477
|
+
| Location / Geography | map pin (`MapPinIcon`) |
|
|
478
|
+
| Corridor / Route | bidirectional arrows (`ArrowsRightLeftIcon`) |
|
|
479
|
+
| Recruitment / Steps | ordered list (`QueueListIcon`) |
|
|
480
|
+
| Employer / Company | office building (`BuildingOfficeIcon`) |
|
|
481
|
+
| Classification / Category | tag (`TagIcon`) |
|
|
482
|
+
| Quota / Allocation | pie chart (`ChartPieIcon`) |
|
|
483
|
+
| Job / Demand | briefcase (`BriefcaseIcon`) |
|
|
484
|
+
| Employee / Person | group of people (`UserGroupIcon`) |
|
|
485
|
+
| Settings / Config | gear (`CogIcon`) |
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Content Fragment Template
|
|
490
|
+
|
|
491
|
+
Content files contain **only the page content area** — no `<html>`, `<head>`, `<body>`,
|
|
492
|
+
layout, or Tailwind config. They are loaded via HTMX and injected into `#content-area`.
|
|
493
|
+
|
|
494
|
+
```html
|
|
495
|
+
<!-- {{PAGE_TITLE}} — {{ROLE}} -->
|
|
496
|
+
<!-- User story traceability: {{US_TAG}} [{{US_VERSION}}] -->
|
|
497
|
+
|
|
498
|
+
<!-- Breadcrumb -->
|
|
499
|
+
<div class="px-6 py-3 border-b border-slate-100 dark:border-slate-700 bg-white dark:bg-slate-800">
|
|
500
|
+
<nav class="flex items-center gap-2 text-sm text-slate-500">
|
|
501
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/home"
|
|
502
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/home"
|
|
503
|
+
hx-target="#content-area"
|
|
504
|
+
hx-swap="innerHTML"
|
|
505
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/home"
|
|
506
|
+
class="hover:text-slate-700 dark:hover:text-slate-300 cursor-pointer">Home</a>
|
|
507
|
+
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
508
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
509
|
+
</svg>
|
|
510
|
+
<span class="text-slate-800 dark:text-slate-200 font-medium">{{PAGE_TITLE}}</span>
|
|
511
|
+
</nav>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<!-- Page Content -->
|
|
515
|
+
<div class="flex-1 p-6">
|
|
516
|
+
{{MAIN_CONTENT}}
|
|
517
|
+
</div>
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Breadcrumb for Sub-screens (detail / edit / create)
|
|
521
|
+
|
|
522
|
+
```html
|
|
523
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/home" hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/home" hx-target="#content-area" hx-swap="innerHTML" hx-push-url="/{{APP_SLUG}}/{{ROLE}}/home" class="hover:text-slate-700 cursor-pointer">Home</a>
|
|
524
|
+
<svg ...><path .../></svg>
|
|
525
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}" hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}" hx-target="#content-area" hx-swap="innerHTML" hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}" class="hover:text-slate-700 cursor-pointer">{{MODULE_NAME}}</a>
|
|
526
|
+
<svg ...><path .../></svg>
|
|
527
|
+
<span class="text-slate-800 dark:text-slate-200 font-medium">{{PAGE_TITLE}}</span>
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Link Rules Inside Content Fragments
|
|
533
|
+
|
|
534
|
+
### Navigation links (within the app)
|
|
535
|
+
|
|
536
|
+
Use HTMX attributes for in-app navigation — never full page reloads for screen transitions:
|
|
537
|
+
|
|
538
|
+
```html
|
|
539
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{TARGET_PAGE}}"
|
|
540
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{TARGET_PAGE}}"
|
|
541
|
+
hx-target="#content-area"
|
|
542
|
+
hx-swap="innerHTML"
|
|
543
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{TARGET_PAGE}}"
|
|
544
|
+
class="...">
|
|
545
|
+
View Details
|
|
546
|
+
</a>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Image links — MUST open in new tab
|
|
550
|
+
|
|
551
|
+
```html
|
|
552
|
+
<a href="/{{APP_SLUG}}/static/images/sample.jpg" target="_blank" rel="noopener noreferrer" class="...">
|
|
553
|
+
View Image
|
|
554
|
+
</a>
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### PDF / document links — MUST open in new tab
|
|
558
|
+
|
|
559
|
+
```html
|
|
560
|
+
<a href="/{{APP_SLUG}}/static/docs/sample.pdf" target="_blank" rel="noopener noreferrer" class="...">
|
|
561
|
+
<svg ...><!-- pdf icon --></svg>
|
|
562
|
+
View PDF
|
|
563
|
+
</a>
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Action buttons in tables
|
|
567
|
+
|
|
568
|
+
```html
|
|
569
|
+
<!-- View / Details → HTMX navigation -->
|
|
570
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_detail"
|
|
571
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_detail"
|
|
572
|
+
hx-target="#content-area"
|
|
573
|
+
hx-swap="innerHTML"
|
|
574
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_detail"
|
|
575
|
+
class="text-xs px-2 py-1 rounded border border-primary text-primary hover:bg-primary/5">
|
|
576
|
+
View
|
|
577
|
+
</a>
|
|
578
|
+
|
|
579
|
+
<!-- Edit → HTMX navigation -->
|
|
580
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_edit"
|
|
581
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_edit"
|
|
582
|
+
hx-target="#content-area"
|
|
583
|
+
hx-swap="innerHTML"
|
|
584
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_edit"
|
|
585
|
+
class="text-xs px-2 py-1 rounded border border-slate-300 text-slate-600 hover:bg-slate-50">
|
|
586
|
+
Edit
|
|
587
|
+
</a>
|
|
588
|
+
|
|
589
|
+
<!-- Delete → Alpine.js confirm dialog (no href navigation) -->
|
|
590
|
+
<button x-data="{ confirm: false }"
|
|
591
|
+
@click="confirm = true"
|
|
592
|
+
x-show="!confirm"
|
|
593
|
+
class="text-xs px-2 py-1 rounded border border-red-300 text-red-600 hover:bg-red-50">
|
|
594
|
+
Delete
|
|
595
|
+
</button>
|
|
596
|
+
<div x-data="{ confirm: false }" x-show="confirm" class="flex items-center gap-2" style="display:none">
|
|
597
|
+
<span class="text-xs text-red-600">Sure?</span>
|
|
598
|
+
<button class="text-xs px-2 py-1 bg-red-600 text-white rounded">Yes, Delete</button>
|
|
599
|
+
<button @click="confirm = false" class="text-xs px-2 py-1 border border-slate-300 rounded">Cancel</button>
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
<!-- Add New → HTMX navigation -->
|
|
603
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_create"
|
|
604
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_create"
|
|
605
|
+
hx-target="#content-area"
|
|
606
|
+
hx-swap="innerHTML"
|
|
607
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_create"
|
|
608
|
+
class="px-4 py-2 rounded-lg text-sm text-white"
|
|
609
|
+
style="background-color: {{DESIGN_PRIMARY}}">
|
|
610
|
+
+ Add New
|
|
611
|
+
</a>
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Tab navigation in detail screens
|
|
615
|
+
|
|
616
|
+
Each tab links to its own content page via HTMX:
|
|
617
|
+
|
|
618
|
+
```html
|
|
619
|
+
<div class="flex border-b border-slate-200 mb-6">
|
|
620
|
+
<!-- Active tab -->
|
|
621
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_overview"
|
|
622
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_overview"
|
|
623
|
+
hx-target="#content-area"
|
|
624
|
+
hx-swap="innerHTML"
|
|
625
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_overview"
|
|
626
|
+
class="px-4 py-2 text-sm font-medium border-b-2 -mb-px"
|
|
627
|
+
style="color: {{DESIGN_PRIMARY}}; border-color: {{DESIGN_PRIMARY}}">
|
|
628
|
+
Overview
|
|
629
|
+
</a>
|
|
630
|
+
<!-- Inactive tab -->
|
|
631
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_documents"
|
|
632
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_documents"
|
|
633
|
+
hx-target="#content-area"
|
|
634
|
+
hx-swap="innerHTML"
|
|
635
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}_tab_documents"
|
|
636
|
+
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-slate-500 hover:text-slate-700 -mb-px">
|
|
637
|
+
Documents
|
|
638
|
+
</a>
|
|
639
|
+
</div>
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Back / Cancel buttons
|
|
643
|
+
|
|
644
|
+
```html
|
|
645
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{PARENT_MODULE}}"
|
|
646
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{PARENT_MODULE}}"
|
|
647
|
+
hx-target="#content-area"
|
|
648
|
+
hx-swap="innerHTML"
|
|
649
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{PARENT_MODULE}}"
|
|
650
|
+
class="px-4 py-2 border border-slate-300 rounded-lg text-sm text-slate-600 hover:bg-slate-50">
|
|
651
|
+
← Back to List
|
|
652
|
+
</a>
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Pagination links
|
|
656
|
+
|
|
657
|
+
```html
|
|
658
|
+
<a href="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}"
|
|
659
|
+
hx-get="/api/content/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}"
|
|
660
|
+
hx-target="#content-area"
|
|
661
|
+
hx-swap="innerHTML"
|
|
662
|
+
hx-push-url="/{{APP_SLUG}}/{{ROLE}}/{{MODULE}}"
|
|
663
|
+
class="px-3 py-1.5 text-sm border border-slate-300 rounded hover:bg-slate-50">
|
|
664
|
+
2
|
|
665
|
+
</a>
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## Common Screens Content
|
|
671
|
+
|
|
672
|
+
### home.html content fragment
|
|
673
|
+
|
|
674
|
+
```html
|
|
675
|
+
<!-- Dashboard Home -->
|
|
676
|
+
<div class="px-6 py-3 border-b border-slate-100 dark:border-slate-700 bg-white dark:bg-slate-800">
|
|
677
|
+
<nav class="flex items-center gap-2 text-sm text-slate-500">
|
|
678
|
+
<span class="text-slate-800 dark:text-slate-200 font-medium">Home</span>
|
|
679
|
+
</nav>
|
|
680
|
+
</div>
|
|
681
|
+
<div class="flex-1 p-6">
|
|
682
|
+
<h1 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">Welcome back</h1>
|
|
683
|
+
<p class="text-slate-500 mb-6">{{APP_NAME}} — {{ROLE_DISPLAY}}</p>
|
|
684
|
+
<!-- Summary stat cards -->
|
|
685
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
686
|
+
<!-- Repeat: one card per module -->
|
|
687
|
+
</div>
|
|
688
|
+
<!-- Recent activity table -->
|
|
689
|
+
</div>
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### profile.html content fragment
|
|
693
|
+
|
|
694
|
+
User profile: avatar, full name, email, role badge, personal info form (read-only), Edit button.
|
|
695
|
+
|
|
696
|
+
### account.html content fragment
|
|
697
|
+
|
|
698
|
+
Account settings: Change Password form, Notification Preferences toggles (Alpine.js),
|
|
699
|
+
Language/Locale selector, Session Management table.
|
|
700
|
+
|
|
701
|
+
### notifications.html content fragment
|
|
702
|
+
|
|
703
|
+
Notifications list: Filter tabs (All / Unread / Read with Alpine.js tab state),
|
|
704
|
+
notification cards with icon / title / message / timestamp / unread dot,
|
|
705
|
+
Mark all as read button.
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## Quick-Start Instructions (append to output summary)
|
|
710
|
+
|
|
711
|
+
```
|
|
712
|
+
Quick Start
|
|
713
|
+
===========
|
|
714
|
+
1. cd <root>/mockup
|
|
715
|
+
2. npm start (zero dependencies — no npm install required)
|
|
716
|
+
3. Open http://localhost:{port} in your browser (first unused port from 4000 —
|
|
717
|
+
the exact URL is printed on start)
|
|
718
|
+
- Landing page lists ALL applications and roles
|
|
719
|
+
- This app's screen index: http://localhost:{port}/{app_slug}
|
|
720
|
+
4. Change port: the landing page's Port input, PORT=4100 npm start, or edit
|
|
721
|
+
<root>/mockup/mockup.config.json
|
|
722
|
+
```
|