htmlboard 0.2.0 → 0.3.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 +80 -69
- package/bin/htmlboard.js +83 -46
- package/package.json +1 -1
- package/skills/htmlboard/SKILL.md +26 -32
- 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} +2 -0
- package/src/templates/{artboards/mobile.html → 01-screens/02-mobile.html} +2 -0
- package/src/templates/{artboards/components.html → 02-components/01-ui-kit.html} +2 -0
- 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,6 +4,8 @@
|
|
|
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">
|
|
7
9
|
<style>
|
|
8
10
|
* {
|
|
9
11
|
box-sizing: border-box;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Mobile Feed Screen</title>
|
|
7
|
+
<meta name="description" content="Smartphone screen with balance card, quick actions, and transaction feed">
|
|
8
|
+
<meta name="htmlboard" content="width=390, height=844">
|
|
7
9
|
<style>
|
|
8
10
|
* {
|
|
9
11
|
box-sizing: border-box;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Component Library Preview</title>
|
|
7
|
+
<meta name="description" content="Buttons, inputs, badges, cards, and interactive elements">
|
|
8
|
+
<meta name="htmlboard" content="width=780, height=720, category=components">
|
|
7
9
|
<style>
|
|
8
10
|
* {
|
|
9
11
|
box-sizing: border-box;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"title": "Design Exploration Canvas",
|
|
3
|
-
"description": "Infinite canvas and artboard explorer for HTML designs and components",
|
|
4
|
-
"sections": [
|
|
5
|
-
{
|
|
6
|
-
"id": "screens",
|
|
7
|
-
"name": "Screen Layouts",
|
|
8
|
-
"description": "Full page layouts across mobile and desktop viewports",
|
|
9
|
-
"artboards": [
|
|
10
|
-
{
|
|
11
|
-
"id": "desktop-dashboard",
|
|
12
|
-
"title": "Desktop Dashboard",
|
|
13
|
-
"description": "Main analytics dashboard with sidebar, metric cards, and charts",
|
|
14
|
-
"file": "artboards/desktop.html",
|
|
15
|
-
"width": 1280,
|
|
16
|
-
"height": 820,
|
|
17
|
-
"category": "desktop",
|
|
18
|
-
"tags": ["desktop", "dashboard", "analytics", "dark-mode"]
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "mobile-app",
|
|
22
|
-
"title": "Mobile App Feed",
|
|
23
|
-
"description": "Smartphone screen with balance card, quick actions, and transaction feed",
|
|
24
|
-
"file": "artboards/mobile.html",
|
|
25
|
-
"width": 390,
|
|
26
|
-
"height": 844,
|
|
27
|
-
"category": "mobile",
|
|
28
|
-
"tags": ["mobile", "ios", "feed", "finance"]
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"id": "components",
|
|
34
|
-
"name": "Component Library",
|
|
35
|
-
"description": "Reusable atomic UI blocks and design system primitives",
|
|
36
|
-
"artboards": [
|
|
37
|
-
{
|
|
38
|
-
"id": "ui-primitives",
|
|
39
|
-
"title": "UI Component Kit",
|
|
40
|
-
"description": "Buttons, inputs, badges, cards, and interactive elements",
|
|
41
|
-
"file": "artboards/components.html",
|
|
42
|
-
"width": 780,
|
|
43
|
-
"height": 720,
|
|
44
|
-
"category": "components",
|
|
45
|
-
"tags": ["design-system", "buttons", "badges", "inputs"]
|
|
46
|
-
}
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
]
|
|
50
|
-
}
|