htmlboard 0.2.0 → 0.4.0
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/README.md +83 -69
- package/bin/htmlboard.js +83 -46
- package/package.json +1 -1
- package/skills/htmlboard/SKILL.md +39 -34
- package/src/client/explorer.css +69 -34
- package/src/client/explorer.js +148 -25
- package/src/client/index.html +2 -0
- package/src/designs.js +398 -0
- package/src/index.js +7 -2
- package/src/migrate.js +193 -0
- package/src/server.js +1 -198
- package/src/templates/{artboards/desktop.html → 01-screens/01-desktop.html} +51 -167
- package/src/templates/{artboards/mobile.html → 01-screens/02-mobile.html} +48 -143
- package/src/templates/02-components/01-ui-kit.html +77 -0
- package/src/templates/styles.css +133 -0
- package/src/templates/artboards/components.html +0 -199
- package/src/templates/designs.json +0 -50
package/src/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import net from 'node:net';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { loadDesignsManifest } from './designs.js';
|
|
6
7
|
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = path.dirname(__filename);
|
|
@@ -31,210 +32,12 @@ const MIME_TYPES = {
|
|
|
31
32
|
'.mp4': 'video/mp4'
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
const IGNORED_DIRS = new Set([
|
|
35
|
-
'node_modules',
|
|
36
|
-
'.git',
|
|
37
|
-
'.github',
|
|
38
|
-
'dist',
|
|
39
|
-
'build',
|
|
40
|
-
'.vscode',
|
|
41
|
-
'.idea',
|
|
42
|
-
'.gemini',
|
|
43
|
-
'.agents'
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Extract <title> content from HTML text if available
|
|
48
|
-
*/
|
|
49
|
-
function extractTitleFromHtml(content, fallbackName) {
|
|
50
|
-
const match = content.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
51
|
-
if (match && match[1]?.trim()) {
|
|
52
|
-
return match[1].trim();
|
|
53
|
-
}
|
|
54
|
-
return fallbackName
|
|
55
|
-
.replace(/\.html$/i, '')
|
|
56
|
-
.replace(/[-_]/g, ' ')
|
|
57
|
-
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Default frame size for each category */
|
|
61
|
-
const CATEGORY_SIZES = {
|
|
62
|
-
mobile: [390, 844],
|
|
63
|
-
tablet: [768, 1024],
|
|
64
|
-
components: [720, 600],
|
|
65
|
-
desktop: [1280, 800],
|
|
66
|
-
responsive: [1200, 800]
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Guess an artboard's category from words in its path, then from its width
|
|
71
|
-
*/
|
|
72
|
-
export function inferCategory(file, width) {
|
|
73
|
-
const lower = file.toLowerCase();
|
|
74
|
-
if (/mobile|phone|iphone/.test(lower)) return 'mobile';
|
|
75
|
-
if (/tablet|ipad/.test(lower)) return 'tablet';
|
|
76
|
-
if (/component|\bui\b|atomic/.test(lower)) return 'components';
|
|
77
|
-
if (/desktop|laptop|screen/.test(lower)) return 'desktop';
|
|
78
|
-
if (width) {
|
|
79
|
-
if (width <= 480) return 'mobile';
|
|
80
|
-
if (width <= 1024) return 'tablet';
|
|
81
|
-
return 'desktop';
|
|
82
|
-
}
|
|
83
|
-
return 'responsive';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function slugify(text) {
|
|
87
|
-
return String(text).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
|
88
|
-
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
35
|
/** True when target is base itself or inside it */
|
|
92
36
|
function isInside(base, target) {
|
|
93
37
|
const rel = path.relative(base, target);
|
|
94
38
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
95
39
|
}
|
|
96
40
|
|
|
97
|
-
/**
|
|
98
|
-
* Scan target directory recursively for HTML files to auto-discover
|
|
99
|
-
*/
|
|
100
|
-
export function discoverDesigns(baseDir) {
|
|
101
|
-
const artboards = [];
|
|
102
|
-
|
|
103
|
-
function scan(currentDir, relativePath = '') {
|
|
104
|
-
let entries;
|
|
105
|
-
try {
|
|
106
|
-
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
107
|
-
} catch {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
for (const entry of entries) {
|
|
112
|
-
if (entry.name.startsWith('.') && entry.name !== '.') continue;
|
|
113
|
-
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
114
|
-
|
|
115
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
116
|
-
const relPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
117
|
-
|
|
118
|
-
if (entry.isDirectory()) {
|
|
119
|
-
scan(fullPath, relPath);
|
|
120
|
-
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
|
|
121
|
-
let title = '';
|
|
122
|
-
let fileContent = '';
|
|
123
|
-
try {
|
|
124
|
-
fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
125
|
-
title = extractTitleFromHtml(fileContent, entry.name);
|
|
126
|
-
} catch {
|
|
127
|
-
title = entry.name;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const category = inferCategory(relPath);
|
|
131
|
-
const [width, height] = CATEGORY_SIZES[category];
|
|
132
|
-
|
|
133
|
-
const sectionName = relativePath ? relativePath.replace(/[\\/]/g, ' / ') : 'Root Designs';
|
|
134
|
-
|
|
135
|
-
artboards.push({
|
|
136
|
-
id: slugify(relPath.replace(/\.html$/i, '')),
|
|
137
|
-
title,
|
|
138
|
-
file: relPath.replace(/\\/g, '/'),
|
|
139
|
-
category,
|
|
140
|
-
width,
|
|
141
|
-
height,
|
|
142
|
-
sectionName,
|
|
143
|
-
tags: [category]
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
scan(baseDir);
|
|
150
|
-
|
|
151
|
-
// Group into sections
|
|
152
|
-
const sectionsMap = new Map();
|
|
153
|
-
for (const board of artboards) {
|
|
154
|
-
const sName = board.sectionName;
|
|
155
|
-
if (!sectionsMap.has(sName)) {
|
|
156
|
-
sectionsMap.set(sName, {
|
|
157
|
-
id: sName.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
158
|
-
name: sName,
|
|
159
|
-
description: `HTML artboards discovered in ${sName}`,
|
|
160
|
-
artboards: []
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
const { sectionName, ...rest } = board;
|
|
164
|
-
sectionsMap.get(sName).artboards.push(rest);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const projectName = path.basename(path.resolve(baseDir)) || 'Design Explorer';
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
title: projectName,
|
|
171
|
-
description: 'Auto-discovered HTML artboards in workspace',
|
|
172
|
-
autoDiscovered: true,
|
|
173
|
-
sections: Array.from(sectionsMap.values())
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Fill in what a manifest may leave out, so the explorer always gets
|
|
179
|
-
* `sections[].{id, name, artboards[].{id, title, file, category}}`.
|
|
180
|
-
* Accepts the short form too: `sections[].title` and `sections[].designs[]`
|
|
181
|
-
* with only `{ file, title, width, height }`.
|
|
182
|
-
* `prefix` is prepended to every file (the designs folder relative to the served root).
|
|
183
|
-
*/
|
|
184
|
-
export function normalizeManifest(manifest, prefix = '') {
|
|
185
|
-
const usedIds = new Set();
|
|
186
|
-
const uniqueId = (wanted) => {
|
|
187
|
-
let id = wanted || 'artboard';
|
|
188
|
-
for (let n = 2; usedIds.has(id); n++) id = `${wanted}-${n}`;
|
|
189
|
-
usedIds.add(id);
|
|
190
|
-
return id;
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const sections = (Array.isArray(manifest.sections) ? manifest.sections : []).map((section, i) => {
|
|
194
|
-
const name = section.name ?? section.title ?? `Section ${i + 1}`;
|
|
195
|
-
const boards = section.artboards ?? section.designs ?? [];
|
|
196
|
-
return {
|
|
197
|
-
...section,
|
|
198
|
-
id: section.id ?? (slugify(name) || `section-${i + 1}`),
|
|
199
|
-
name,
|
|
200
|
-
artboards: boards.filter((board) => board && board.file).map((board) => {
|
|
201
|
-
const file = prefix ? path.posix.join(prefix, board.file) : board.file;
|
|
202
|
-
return {
|
|
203
|
-
...board,
|
|
204
|
-
id: uniqueId(board.id ?? slugify(board.file.replace(/\.html?(?=$|\?)/i, ''))),
|
|
205
|
-
title: board.title ?? board.file,
|
|
206
|
-
file,
|
|
207
|
-
category: board.category ?? inferCategory(board.file.split('?')[0], board.width)
|
|
208
|
-
};
|
|
209
|
-
})
|
|
210
|
-
};
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
return { ...manifest, sections };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Load designs configuration from designs.json or auto-discover.
|
|
218
|
-
* With `rootDir` (a folder containing baseDir), artboard paths are given relative to it.
|
|
219
|
-
*/
|
|
220
|
-
export function loadDesignsManifest(baseDir, rootDir = baseDir) {
|
|
221
|
-
const prefix = path.relative(rootDir, baseDir).split(path.sep).join('/');
|
|
222
|
-
const configPath = path.join(baseDir, 'designs.json');
|
|
223
|
-
if (fs.existsSync(configPath)) {
|
|
224
|
-
try {
|
|
225
|
-
const raw = fs.readFileSync(configPath, 'utf8');
|
|
226
|
-
const data = JSON.parse(raw);
|
|
227
|
-
return {
|
|
228
|
-
...normalizeManifest(data, prefix),
|
|
229
|
-
autoDiscovered: false
|
|
230
|
-
};
|
|
231
|
-
} catch (err) {
|
|
232
|
-
console.warn(`[htmlboard] Failed parsing designs.json: ${err.message}. Falling back to auto-discovery.`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return normalizeManifest(discoverDesigns(baseDir), prefix);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
41
|
/** Folders the watcher leaves alone: hidden ones (.git, .vscode, …) and dependencies */
|
|
239
42
|
const skipWatching = (name) => name.startsWith('.') || name === 'node_modules';
|
|
240
43
|
|
|
@@ -4,30 +4,11 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Desktop Dashboard</title>
|
|
7
|
+
<meta name="description" content="Main analytics dashboard with sidebar, metric cards, and charts">
|
|
8
|
+
<meta name="htmlboard" content="width=1280, height=820">
|
|
9
|
+
<link rel="stylesheet" href="../styles.css">
|
|
7
10
|
<style>
|
|
8
|
-
* {
|
|
9
|
-
box-sizing: border-box;
|
|
10
|
-
margin: 0;
|
|
11
|
-
padding: 0;
|
|
12
|
-
-webkit-font-smoothing: antialiased;
|
|
13
|
-
}
|
|
14
|
-
:root {
|
|
15
|
-
--bg: #0b0d13;
|
|
16
|
-
--sidebar-bg: #10131d;
|
|
17
|
-
--panel-bg: #151824;
|
|
18
|
-
--border: #23283c;
|
|
19
|
-
--accent: #6366f1;
|
|
20
|
-
--accent-light: #818cf8;
|
|
21
|
-
--text: #f1f5f9;
|
|
22
|
-
--text-muted: #94a3b8;
|
|
23
|
-
--emerald: #10b981;
|
|
24
|
-
--amber: #f59e0b;
|
|
25
|
-
--purple: #a855f7;
|
|
26
|
-
}
|
|
27
11
|
body {
|
|
28
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
29
|
-
background: var(--bg);
|
|
30
|
-
color: var(--text);
|
|
31
12
|
display: flex;
|
|
32
13
|
height: 100vh;
|
|
33
14
|
overflow: hidden;
|
|
@@ -35,7 +16,7 @@
|
|
|
35
16
|
/* Sidebar */
|
|
36
17
|
.sidebar {
|
|
37
18
|
width: 240px;
|
|
38
|
-
background:
|
|
19
|
+
background: #10131d;
|
|
39
20
|
border-right: 1px solid var(--border);
|
|
40
21
|
display: flex;
|
|
41
22
|
flex-direction: column;
|
|
@@ -43,10 +24,8 @@
|
|
|
43
24
|
flex-shrink: 0;
|
|
44
25
|
}
|
|
45
26
|
.logo-container {
|
|
46
|
-
display: flex;
|
|
47
|
-
align-items: center;
|
|
48
|
-
gap: 10px;
|
|
49
27
|
padding: 4px 8px 24px;
|
|
28
|
+
gap: 10px;
|
|
50
29
|
font-weight: 700;
|
|
51
30
|
font-size: 17px;
|
|
52
31
|
letter-spacing: -0.02em;
|
|
@@ -54,8 +33,8 @@
|
|
|
54
33
|
.logo-badge {
|
|
55
34
|
width: 32px;
|
|
56
35
|
height: 32px;
|
|
57
|
-
background: linear-gradient(135deg,
|
|
58
|
-
border-radius:
|
|
36
|
+
background: linear-gradient(135deg, var(--accent), #3b82f6);
|
|
37
|
+
border-radius: var(--radius);
|
|
59
38
|
display: flex;
|
|
60
39
|
align-items: center;
|
|
61
40
|
justify-content: center;
|
|
@@ -68,22 +47,14 @@
|
|
|
68
47
|
gap: 4px;
|
|
69
48
|
flex: 1;
|
|
70
49
|
}
|
|
71
|
-
.nav-label {
|
|
72
|
-
font-size: 11px;
|
|
73
|
-
text-transform: uppercase;
|
|
74
|
-
letter-spacing: 0.08em;
|
|
75
|
-
color: #64748b;
|
|
76
|
-
padding: 8px 12px 4px;
|
|
77
|
-
font-weight: 600;
|
|
78
|
-
}
|
|
50
|
+
.nav-group .label { padding: 8px 12px 4px; font-size: 11px; }
|
|
79
51
|
.nav-link {
|
|
80
52
|
display: flex;
|
|
81
53
|
align-items: center;
|
|
82
54
|
gap: 12px;
|
|
83
55
|
padding: 10px 12px;
|
|
84
|
-
border-radius:
|
|
56
|
+
border-radius: var(--radius);
|
|
85
57
|
color: var(--text-muted);
|
|
86
|
-
text-decoration: none;
|
|
87
58
|
font-size: 14px;
|
|
88
59
|
font-weight: 500;
|
|
89
60
|
transition: all 0.15s ease;
|
|
@@ -98,25 +69,12 @@
|
|
|
98
69
|
font-weight: 600;
|
|
99
70
|
}
|
|
100
71
|
.user-footer {
|
|
101
|
-
display: flex;
|
|
102
|
-
align-items: center;
|
|
103
|
-
gap: 12px;
|
|
104
72
|
padding: 12px;
|
|
105
73
|
border-radius: 10px;
|
|
106
74
|
background: rgba(255, 255, 255, 0.03);
|
|
107
75
|
border: 1px solid var(--border);
|
|
108
76
|
}
|
|
109
|
-
.user-avatar {
|
|
110
|
-
width: 36px;
|
|
111
|
-
height: 36px;
|
|
112
|
-
border-radius: 50%;
|
|
113
|
-
background: linear-gradient(135deg, #ec4899, #8b5cf6);
|
|
114
|
-
display: flex;
|
|
115
|
-
align-items: center;
|
|
116
|
-
justify-content: center;
|
|
117
|
-
font-weight: 600;
|
|
118
|
-
font-size: 14px;
|
|
119
|
-
}
|
|
77
|
+
.user-footer .avatar { background: linear-gradient(135deg, #ec4899, #8b5cf6); font-weight: 600; }
|
|
120
78
|
/* Main Content */
|
|
121
79
|
.main {
|
|
122
80
|
flex: 1;
|
|
@@ -124,7 +82,6 @@
|
|
|
124
82
|
flex-direction: column;
|
|
125
83
|
overflow-y: auto;
|
|
126
84
|
}
|
|
127
|
-
/* Top Header */
|
|
128
85
|
.topbar {
|
|
129
86
|
height: 64px;
|
|
130
87
|
border-bottom: 1px solid var(--border);
|
|
@@ -135,114 +92,51 @@
|
|
|
135
92
|
background: rgba(11, 13, 19, 0.8);
|
|
136
93
|
backdrop-filter: blur(12px);
|
|
137
94
|
}
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
border: 1px solid var(--border);
|
|
141
|
-
padding: 8px 16px 8px 36px;
|
|
142
|
-
border-radius: 8px;
|
|
143
|
-
color: var(--text);
|
|
144
|
-
font-size: 13px;
|
|
145
|
-
width: 280px;
|
|
146
|
-
position: relative;
|
|
147
|
-
}
|
|
148
|
-
.header-actions {
|
|
149
|
-
display: flex;
|
|
150
|
-
align-items: center;
|
|
151
|
-
gap: 16px;
|
|
152
|
-
}
|
|
153
|
-
.primary-btn {
|
|
154
|
-
background: var(--accent);
|
|
155
|
-
color: #fff;
|
|
156
|
-
border: none;
|
|
157
|
-
padding: 8px 18px;
|
|
158
|
-
border-radius: 8px;
|
|
159
|
-
font-size: 13px;
|
|
160
|
-
font-weight: 600;
|
|
161
|
-
cursor: pointer;
|
|
162
|
-
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
|
163
|
-
}
|
|
164
|
-
/* Dashboard Area */
|
|
95
|
+
.topbar .input { width: 280px; padding: 8px 16px; }
|
|
96
|
+
.topbar .row { gap: 16px; font-size: 13px; }
|
|
165
97
|
.content {
|
|
166
98
|
padding: 28px 32px;
|
|
167
99
|
display: flex;
|
|
168
100
|
flex-direction: column;
|
|
169
101
|
gap: 24px;
|
|
170
102
|
}
|
|
171
|
-
.page-header {
|
|
172
|
-
display: flex;
|
|
173
|
-
justify-content: space-between;
|
|
174
|
-
align-items: flex-end;
|
|
175
|
-
}
|
|
176
103
|
.page-title {
|
|
177
104
|
font-size: 24px;
|
|
178
105
|
font-weight: 700;
|
|
179
106
|
letter-spacing: -0.02em;
|
|
180
107
|
}
|
|
181
|
-
.page-sub {
|
|
182
|
-
color: var(--text-muted);
|
|
183
|
-
font-size: 14px;
|
|
184
|
-
margin-top: 4px;
|
|
185
|
-
}
|
|
108
|
+
.page-sub { font-size: 14px; margin-top: 4px; }
|
|
186
109
|
/* Stat Cards */
|
|
187
110
|
.stats-row {
|
|
188
111
|
display: grid;
|
|
189
112
|
grid-template-columns: repeat(4, 1fr);
|
|
190
113
|
gap: 20px;
|
|
191
114
|
}
|
|
192
|
-
.stat-
|
|
193
|
-
background: var(--panel-bg);
|
|
194
|
-
border: 1px solid var(--border);
|
|
195
|
-
border-radius: 14px;
|
|
196
|
-
padding: 20px;
|
|
197
|
-
}
|
|
198
|
-
.stat-title {
|
|
199
|
-
font-size: 13px;
|
|
200
|
-
color: var(--text-muted);
|
|
201
|
-
margin-bottom: 8px;
|
|
202
|
-
}
|
|
115
|
+
.stat-title { font-size: 13px; margin-bottom: 8px; }
|
|
203
116
|
.stat-val {
|
|
204
117
|
font-size: 26px;
|
|
205
118
|
font-weight: 700;
|
|
206
119
|
margin-bottom: 8px;
|
|
207
120
|
}
|
|
208
|
-
.stat-badge {
|
|
209
|
-
font-size: 12px;
|
|
210
|
-
font-weight: 600;
|
|
211
|
-
display: inline-flex;
|
|
212
|
-
align-items: center;
|
|
213
|
-
gap: 4px;
|
|
214
|
-
}
|
|
121
|
+
.stat-badge { font-size: 12px; font-weight: 600; }
|
|
215
122
|
.stat-badge.pos { color: var(--emerald); }
|
|
216
123
|
.stat-badge.warn { color: var(--amber); }
|
|
217
|
-
|
|
218
|
-
/* Visual Grid */
|
|
124
|
+
/* Panels */
|
|
219
125
|
.grid-2 {
|
|
220
126
|
display: grid;
|
|
221
127
|
grid-template-columns: 2fr 1fr;
|
|
222
128
|
gap: 20px;
|
|
223
129
|
}
|
|
224
|
-
.
|
|
225
|
-
background: var(--panel-bg);
|
|
226
|
-
border: 1px solid var(--border);
|
|
227
|
-
border-radius: 14px;
|
|
228
|
-
padding: 22px;
|
|
229
|
-
}
|
|
130
|
+
.grid-2 .card { padding: 22px; }
|
|
230
131
|
.panel-header {
|
|
231
132
|
display: flex;
|
|
232
133
|
justify-content: space-between;
|
|
233
134
|
align-items: center;
|
|
234
135
|
margin-bottom: 20px;
|
|
136
|
+
font-size: 12px;
|
|
235
137
|
}
|
|
236
|
-
.panel-title {
|
|
237
|
-
|
|
238
|
-
font-weight: 600;
|
|
239
|
-
}
|
|
240
|
-
/* Chart SVG */
|
|
241
|
-
.chart-container {
|
|
242
|
-
width: 100%;
|
|
243
|
-
height: 180px;
|
|
244
|
-
}
|
|
245
|
-
/* Table */
|
|
138
|
+
.panel-title { font-size: 16px; font-weight: 600; }
|
|
139
|
+
.chart-container { width: 100%; height: 180px; }
|
|
246
140
|
.data-table {
|
|
247
141
|
width: 100%;
|
|
248
142
|
border-collapse: collapse;
|
|
@@ -250,7 +144,7 @@
|
|
|
250
144
|
}
|
|
251
145
|
.data-table th {
|
|
252
146
|
text-align: left;
|
|
253
|
-
color:
|
|
147
|
+
color: var(--text-subtle);
|
|
254
148
|
font-weight: 500;
|
|
255
149
|
padding-bottom: 12px;
|
|
256
150
|
border-bottom: 1px solid var(--border);
|
|
@@ -259,90 +153,80 @@
|
|
|
259
153
|
padding: 12px 0;
|
|
260
154
|
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
261
155
|
}
|
|
262
|
-
.
|
|
263
|
-
display: inline-block;
|
|
264
|
-
padding: 3px 8px;
|
|
265
|
-
border-radius: 12px;
|
|
266
|
-
font-size: 11px;
|
|
267
|
-
font-weight: 600;
|
|
268
|
-
}
|
|
269
|
-
.status-pill.success { background: rgba(16, 185, 129, 0.15); color: var(--emerald); }
|
|
270
|
-
.status-pill.pending { background: rgba(245, 158, 11, 0.15); color: var(--amber); }
|
|
156
|
+
.data-table .badge { padding: 3px 8px; font-size: 11px; }
|
|
271
157
|
</style>
|
|
272
158
|
</head>
|
|
273
159
|
<body>
|
|
274
160
|
<aside class="sidebar">
|
|
275
|
-
<div class="logo-container">
|
|
161
|
+
<div class="logo-container row">
|
|
276
162
|
<div class="logo-badge">⚡</div>
|
|
277
163
|
<span>Nexus Platform</span>
|
|
278
164
|
</div>
|
|
279
165
|
|
|
280
166
|
<nav class="nav-group">
|
|
281
|
-
<div class="
|
|
167
|
+
<div class="label">Core</div>
|
|
282
168
|
<a href="#" class="nav-link active">📊 Dashboard</a>
|
|
283
169
|
<a href="#" class="nav-link">👥 Customers</a>
|
|
284
170
|
<a href="#" class="nav-link">💳 Invoices</a>
|
|
285
171
|
<a href="#" class="nav-link">📈 Analytics</a>
|
|
286
172
|
|
|
287
|
-
<div class="
|
|
173
|
+
<div class="label" style="margin-top: 16px">System</div>
|
|
288
174
|
<a href="#" class="nav-link">⚙️ Settings</a>
|
|
289
175
|
<a href="#" class="nav-link">🛡️ Security</a>
|
|
290
176
|
</nav>
|
|
291
177
|
|
|
292
|
-
<div class="user-footer">
|
|
293
|
-
<div class="
|
|
178
|
+
<div class="user-footer row">
|
|
179
|
+
<div class="avatar">AD</div>
|
|
294
180
|
<div>
|
|
295
|
-
<div style="font-size: 13px; font-weight: 600
|
|
296
|
-
<div style="font-size: 11px
|
|
181
|
+
<div style="font-size: 13px; font-weight: 600">Alex Morgan</div>
|
|
182
|
+
<div class="muted" style="font-size: 11px">alex@nexus.io</div>
|
|
297
183
|
</div>
|
|
298
184
|
</div>
|
|
299
185
|
</aside>
|
|
300
186
|
|
|
301
187
|
<main class="main">
|
|
302
188
|
<header class="topbar">
|
|
303
|
-
<input type="text" class="
|
|
304
|
-
<div class="
|
|
305
|
-
<span
|
|
306
|
-
<button class="primary
|
|
189
|
+
<input type="text" class="input" placeholder="Search resources, clients, reports...">
|
|
190
|
+
<div class="row">
|
|
191
|
+
<span class="muted">Last sync: Just now</span>
|
|
192
|
+
<button class="btn btn-primary">+ New Deployment</button>
|
|
307
193
|
</div>
|
|
308
194
|
</header>
|
|
309
195
|
|
|
310
196
|
<div class="content">
|
|
311
|
-
<div
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
<p class="page-sub">Performance metrics and active client deployments</p>
|
|
315
|
-
</div>
|
|
197
|
+
<div>
|
|
198
|
+
<h1 class="page-title">Executive Overview</h1>
|
|
199
|
+
<p class="page-sub muted">Performance metrics and active client deployments</p>
|
|
316
200
|
</div>
|
|
317
201
|
|
|
318
202
|
<div class="stats-row">
|
|
319
|
-
<div class="
|
|
320
|
-
<div class="stat-title">Monthly Recurring Revenue</div>
|
|
203
|
+
<div class="card">
|
|
204
|
+
<div class="stat-title muted">Monthly Recurring Revenue</div>
|
|
321
205
|
<div class="stat-val">$184,290</div>
|
|
322
206
|
<div class="stat-badge pos">▲ +12.4% vs last mo</div>
|
|
323
207
|
</div>
|
|
324
|
-
<div class="
|
|
325
|
-
<div class="stat-title">Active Organizations</div>
|
|
208
|
+
<div class="card">
|
|
209
|
+
<div class="stat-title muted">Active Organizations</div>
|
|
326
210
|
<div class="stat-val">1,428</div>
|
|
327
211
|
<div class="stat-badge pos">▲ +48 new</div>
|
|
328
212
|
</div>
|
|
329
|
-
<div class="
|
|
330
|
-
<div class="stat-title">API Requests (24h)</div>
|
|
213
|
+
<div class="card">
|
|
214
|
+
<div class="stat-title muted">API Requests (24h)</div>
|
|
331
215
|
<div class="stat-val">42.8M</div>
|
|
332
216
|
<div class="stat-badge pos">▲ 99.98% uptime</div>
|
|
333
217
|
</div>
|
|
334
|
-
<div class="
|
|
335
|
-
<div class="stat-title">Average Latency</div>
|
|
218
|
+
<div class="card">
|
|
219
|
+
<div class="stat-title muted">Average Latency</div>
|
|
336
220
|
<div class="stat-val">34ms</div>
|
|
337
221
|
<div class="stat-badge warn">▼ -2ms target</div>
|
|
338
222
|
</div>
|
|
339
223
|
</div>
|
|
340
224
|
|
|
341
225
|
<div class="grid-2">
|
|
342
|
-
<div class="
|
|
226
|
+
<div class="card">
|
|
343
227
|
<div class="panel-header">
|
|
344
228
|
<h2 class="panel-title">Throughput & Workload Trends</h2>
|
|
345
|
-
<span
|
|
229
|
+
<span class="muted">Trailing 7 Days</span>
|
|
346
230
|
</div>
|
|
347
231
|
<div class="chart-container">
|
|
348
232
|
<svg viewBox="0 0 500 150" width="100%" height="100%" preserveAspectRatio="none">
|
|
@@ -359,7 +243,7 @@
|
|
|
359
243
|
</div>
|
|
360
244
|
</div>
|
|
361
245
|
|
|
362
|
-
<div class="
|
|
246
|
+
<div class="card">
|
|
363
247
|
<div class="panel-header">
|
|
364
248
|
<h2 class="panel-title">Recent Audits</h2>
|
|
365
249
|
</div>
|
|
@@ -373,19 +257,19 @@
|
|
|
373
257
|
<tbody>
|
|
374
258
|
<tr>
|
|
375
259
|
<td>us-east-cluster-01</td>
|
|
376
|
-
<td><span class="
|
|
260
|
+
<td><span class="badge badge-emerald">Optimal</span></td>
|
|
377
261
|
</tr>
|
|
378
262
|
<tr>
|
|
379
263
|
<td>auth-gateway-proxy</td>
|
|
380
|
-
<td><span class="
|
|
264
|
+
<td><span class="badge badge-emerald">Optimal</span></td>
|
|
381
265
|
</tr>
|
|
382
266
|
<tr>
|
|
383
267
|
<td>edge-cache-frankfurt</td>
|
|
384
|
-
<td><span class="
|
|
268
|
+
<td><span class="badge badge-amber">Rebalancing</span></td>
|
|
385
269
|
</tr>
|
|
386
270
|
<tr>
|
|
387
271
|
<td>db-replica-primary</td>
|
|
388
|
-
<td><span class="
|
|
272
|
+
<td><span class="badge badge-emerald">Optimal</span></td>
|
|
389
273
|
</tr>
|
|
390
274
|
</tbody>
|
|
391
275
|
</table>
|