htmlboard 0.5.0 → 0.6.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 +21 -8
- package/bin/htmlboard.js +24 -4
- package/package.json +1 -1
- package/skills/htmlboard/SKILL.md +21 -60
- package/skills/htmlboard-components/SKILL.md +36 -0
- package/skills/htmlboard-designs-json/SKILL.md +25 -0
- package/src/canvases.js +38 -0
- package/src/check.js +1 -31
- package/src/client/explorer.js +1 -1
- package/src/designs.js +44 -8
- package/src/migrate.js +2 -2
- package/src/server.js +1 -1
- package/src/skills.js +2 -1
package/README.md
CHANGED
|
@@ -93,6 +93,8 @@ Options:
|
|
|
93
93
|
-h, --help Display help message
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
Running `htmlboard` for a folder it already shows (same folder and root, same version) doesn't start a second canvas on the next port: it prints the running one's URL, and opens it with `--open`.
|
|
97
|
+
|
|
96
98
|
### Examples
|
|
97
99
|
|
|
98
100
|
```bash
|
|
@@ -131,31 +133,39 @@ htmlboard migrate design --write
|
|
|
131
133
|
|
|
132
134
|
HtmlBoard is built from the ground up to pair with AI coding agents (Claude, Gemini, Codex, ChatGPT, Antigravity, Cursor).
|
|
133
135
|
|
|
134
|
-
### Install the agent
|
|
136
|
+
### Install the agent skills
|
|
137
|
+
|
|
138
|
+
HtmlBoard ships [Agent Skills](https://agentskills.io) that teach your agent how to use it. An agent keeps only each skill's one-line description in mind and reads a skill when a task calls for it, so the ones it rarely needs cost almost nothing until then:
|
|
139
|
+
|
|
140
|
+
| Skill | What it teaches | Read |
|
|
141
|
+
| :--- | :--- | :--- |
|
|
142
|
+
| `htmlboard` | Start the canvas; write, size, name and group designs; shared styles; size and state variants; the catalog and the check | For any design work |
|
|
143
|
+
| `htmlboard-components` | The component file: attributes, slots, scoped CSS, a script, its first-line example | Before writing or changing a component |
|
|
144
|
+
| `htmlboard-designs-json` | Curating the canvas with `designs.json`, and `htmlboard migrate` | When the designs folder has a `designs.json` |
|
|
135
145
|
|
|
136
|
-
|
|
146
|
+
Install them in your project:
|
|
137
147
|
|
|
138
148
|
```bash
|
|
139
149
|
npx htmlboard skills install
|
|
140
150
|
```
|
|
141
151
|
|
|
142
|
-
This writes `.claude/skills
|
|
152
|
+
This writes them in `.claude/skills/` (Claude Code) and `.agents/skills/` (Codex and other agents that read `.agents/skills`). Commit them so everyone working on the project gets them. Then start a new agent session and just ask for a design.
|
|
143
153
|
|
|
144
154
|
```bash
|
|
145
155
|
npx htmlboard skills install --global # for your user (~/.claude/skills, ~/.agents/skills), all projects
|
|
146
156
|
npx htmlboard skills install --agent claude # only one agent: claude | agents | all (default)
|
|
147
157
|
npx htmlboard skills install --force # update a copy you edited locally
|
|
148
|
-
npx htmlboard skills uninstall # remove
|
|
158
|
+
npx htmlboard skills uninstall # remove them (accepts the same --global / --agent)
|
|
149
159
|
```
|
|
150
160
|
|
|
151
|
-
Running `install` again after upgrading HtmlBoard updates the
|
|
161
|
+
Running `install` again after upgrading HtmlBoard updates the skills; a copy you edited is kept unless you pass `--force`.
|
|
152
162
|
|
|
153
163
|
### The workflow
|
|
154
164
|
|
|
155
165
|
1. **Ask your LLM to generate artboards:**
|
|
156
166
|
> *"Create 3 design explorations for our checkout flow in `designs/03-checkout/`."*
|
|
157
167
|
|
|
158
|
-
With the
|
|
168
|
+
With the skills, the agent writes `01-one-page.html`, `02-steps.html`, `03-express.html`, each with its size in a `<meta>` tag. They show up as a new "checkout" row, in that order.
|
|
159
169
|
|
|
160
170
|
2. **Run HtmlBoard in the background:**
|
|
161
171
|
```bash
|
|
@@ -240,6 +250,7 @@ Every `.html` file under the folder is a design (hidden folders, `node_modules`,
|
|
|
240
250
|
<meta name="htmlboard" content="width=390, height=844"> <!-- its size (and category=…) -->
|
|
241
251
|
<meta name="htmlboard:label" content="light"> <!-- added to its name: "Checkout · light" -->
|
|
242
252
|
<meta name="htmlboard:variant" content="?theme=dark · dark"> <!-- one more frame: "Checkout · dark" -->
|
|
253
|
+
<meta name="htmlboard:variant" content="width=768, height=1024 · tablet"> <!-- and at a tablet's size -->
|
|
243
254
|
```
|
|
244
255
|
|
|
245
256
|
| Tag | What it says |
|
|
@@ -248,7 +259,7 @@ Every `.html` file under the folder is a design (hidden folders, `node_modules`,
|
|
|
248
259
|
| `<meta name="description">` | Its description, shown in the list and found by the search. |
|
|
249
260
|
| `<meta name="htmlboard" content="…">` | `width=…`, `height=…`, `category=…` (`mobile`, `tablet`, `desktop`, `components` or your own word), or `skip` to leave the file out (partials, templates). |
|
|
250
261
|
| `<meta name="htmlboard:label">` | Text added to the name of the file's own frame, when it has variants. |
|
|
251
|
-
| `<meta name="htmlboard:variant">` | One more frame of the same file with
|
|
262
|
+
| `<meta name="htmlboard:variant">` | One more frame of the same file: with a query string (`?theme=dark`), at another size (`width=390, height=844`, so a responsive page shows on a phone too), or both; then its label. As many as you like; they follow the file. |
|
|
252
263
|
|
|
253
264
|
**Sizes.** The width is the frame's viewport: the page's CSS needs it before it renders. Give the height for designs of a fixed size (a phone screen, a flyer: the frame is that tall and cuts the rest); leave it out and the frame grows with the page. Without a size, words in the path decide (`mobile` → 390 × 844, `tablet` → 768 × 1024, `desktop` → 1280 × 800, `component` → 720 × 600), else 1200 wide.
|
|
254
265
|
|
|
@@ -466,7 +477,9 @@ htmlboard/
|
|
|
466
477
|
│ ├── 01-screens/ # Dashboard, customers, mobile feed
|
|
467
478
|
│ └── 02-ui-kit/
|
|
468
479
|
├── skills/
|
|
469
|
-
│
|
|
480
|
+
│ ├── htmlboard/ # Agent skills (SKILL.md each) copied by `htmlboard skills install`
|
|
481
|
+
│ ├── htmlboard-components/
|
|
482
|
+
│ └── htmlboard-designs-json/
|
|
470
483
|
├── brand/ # The logo's files (from design/tools/logo.py)
|
|
471
484
|
├── design/ # htmlboard's own designs, on its own canvas: `npx htmlboard design --root .`
|
|
472
485
|
├── package.json
|
package/bin/htmlboard.js
CHANGED
|
@@ -10,6 +10,7 @@ import { planMigration, checkMigration, applyMigration } from '../src/migrate.js
|
|
|
10
10
|
import { installComponents, COMPONENTS_FOLDER, COMPONENTS_SCRIPT } from '../src/components.js';
|
|
11
11
|
import { buildCatalog, formatCatalog } from '../src/catalog.js';
|
|
12
12
|
import { runCheck, formatReport, hasProblems, CheckError } from '../src/check.js';
|
|
13
|
+
import { PORTS, findCanvases } from '../src/canvases.js';
|
|
13
14
|
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
16
|
const __dirname = path.dirname(__filename);
|
|
@@ -99,7 +100,7 @@ Examples:
|
|
|
99
100
|
htmlboard ./designs -p 8080 # Serve ./designs on port 8080
|
|
100
101
|
htmlboard design --root . # Artboards in ./design that link to ../css/…
|
|
101
102
|
htmlboard --init # Add sample designs to the current folder
|
|
102
|
-
htmlboard skills install # Add the HtmlBoard
|
|
103
|
+
htmlboard skills install # Add the HtmlBoard skills to this project's agents
|
|
103
104
|
htmlboard components designs # Let ./designs use components (components/tx-item.html is <tx-item>)
|
|
104
105
|
htmlboard catalog designs # What ./designs share, in a few lines (for your AI agent)
|
|
105
106
|
htmlboard check designs # Script errors, missing files, cut-off text in ./designs
|
|
@@ -154,8 +155,8 @@ function scaffoldProject(targetDir) {
|
|
|
154
155
|
function showSkillsHelp() {
|
|
155
156
|
console.log(`
|
|
156
157
|
Usage:
|
|
157
|
-
htmlboard skills install [options] Copy the HtmlBoard
|
|
158
|
-
htmlboard skills uninstall [options] Remove
|
|
158
|
+
htmlboard skills install [options] Copy the HtmlBoard skills into your agents' skill folders
|
|
159
|
+
htmlboard skills uninstall [options] Remove them
|
|
159
160
|
|
|
160
161
|
Options:
|
|
161
162
|
-g, --global Install for your user (~/.claude/skills, ~/.agents/skills) instead of this project
|
|
@@ -211,7 +212,7 @@ function runSkills(args) {
|
|
|
211
212
|
console.log(` ${labels[status]} ${path.relative(process.cwd(), dest) || dest}`);
|
|
212
213
|
}
|
|
213
214
|
if (action === 'install' && results.some((r) => r.status === 'installed' || r.status === 'updated')) {
|
|
214
|
-
console.log(`\n Restart your agent session to load the
|
|
215
|
+
console.log(`\n Restart your agent session to load the skills, then ask it for a design.`);
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
|
|
@@ -453,6 +454,25 @@ async function main() {
|
|
|
453
454
|
scaffoldProject(targetDir);
|
|
454
455
|
}
|
|
455
456
|
|
|
457
|
+
// A canvas already running for this folder (same root, same version): say where, rather than
|
|
458
|
+
// starting a second one on the next port
|
|
459
|
+
const root = options.root ? path.resolve(options.root) : targetDir;
|
|
460
|
+
const running = (await findCanvases([...PORTS, ...Array.from({ length: 21 }, (_, i) => options.port + i)]))
|
|
461
|
+
.filter((canvas) => canvas.dir === targetDir && canvas.root === root);
|
|
462
|
+
const same = running.find((canvas) => canvas.version === pkg.version);
|
|
463
|
+
if (same) {
|
|
464
|
+
console.log(`
|
|
465
|
+
\x1b[90mhtml\x1b[0m\x1b[1mboard\x1b[0m is already running for this folder:
|
|
466
|
+
|
|
467
|
+
\x1b[32m➜\x1b[0m \x1b[1mLocal:\x1b[0m \x1b[36m${same.url}\x1b[0m
|
|
468
|
+
`);
|
|
469
|
+
if (options.open) openBrowser(same.url);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
for (const canvas of running) {
|
|
473
|
+
console.log(`[htmlboard] v${canvas.version} is running for this folder at ${canvas.url}; starting v${pkg.version} too.`);
|
|
474
|
+
}
|
|
475
|
+
|
|
456
476
|
try {
|
|
457
477
|
const { url, rootDir } = await startServer({
|
|
458
478
|
dir: targetDir,
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: htmlboard
|
|
3
|
-
description: Design UI in plain HTML files and show them side by side on an infinite, live-reloading canvas with the htmlboard CLI. Use when the user asks for mockups, screens, design variants or explorations,
|
|
3
|
+
description: Design UI in plain HTML files and show them side by side on an infinite, live-reloading canvas with the htmlboard CLI. Use when the user asks for mockups, screens, design variants or explorations, flyers or any visual proposal, or mentions htmlboard or artboards.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# HtmlBoard
|
|
7
7
|
|
|
8
8
|
HtmlBoard serves a folder of HTML files as artboards on a zoomable canvas, like a Figma page. Every design is one ordinary HTML file that says its own name and size, so you design by writing HTML and CSS, and the user sees the result update live while you work.
|
|
9
9
|
|
|
10
|
+
Two more skills cover the rest when you need it: **htmlboard-components** to write or change a component, **htmlboard-designs-json** when the designs folder has a `designs.json`.
|
|
11
|
+
|
|
10
12
|
## Start the canvas
|
|
11
13
|
|
|
12
14
|
Run it in the background, pointed at the designs folder (it is created if missing):
|
|
@@ -19,7 +21,7 @@ npx htmlboard designs --open
|
|
|
19
21
|
- `-p <port>` picks the port. `--init` adds sample designs; skip it unless the user wants a starter.
|
|
20
22
|
- If the artboards link to files above the designs folder (`../css/theme.css`), serve from the project root: `npx htmlboard design --root .`
|
|
21
23
|
- An empty folder gets the samples automatically, so write your first artboard before starting the server.
|
|
22
|
-
-
|
|
24
|
+
- Running it again for a folder that already has a canvas doesn't start another: it prints that canvas's URL.
|
|
23
25
|
|
|
24
26
|
## Write an artboard
|
|
25
27
|
|
|
@@ -48,87 +50,46 @@ One design per file, with its facts in the `<head>`, linking the shared styleshe
|
|
|
48
50
|
- Always give the `width`: it is the frame's viewport, and the page's CSS needs it. Give the `height` for fixed sizes (a phone screen, a flyer: the frame cuts at it); leave it out for pages that should grow with their content.
|
|
49
51
|
- Design at the real size of the target: phone 390×844, tablet 768×1024, desktop 1280×800 (or 1440×900), and the exact size for print or social formats (e.g. 1080×1350).
|
|
50
52
|
- Use real content in the user's language, not lorem ipsum.
|
|
51
|
-
- Relative links to CSS, images, scripts and fonts work (`assets/logo.svg`; `../css/theme.css` with `--root`).
|
|
52
|
-
|
|
53
|
+
- Relative links to CSS, images, scripts and fonts work (`assets/logo.svg`; `../css/theme.css` with `--root`). No build step: plain HTML, CSS and browser JavaScript (ES modules and CDN scripts work).
|
|
54
|
+
|
|
55
|
+
More tags, all optional:
|
|
56
|
+
|
|
57
|
+
- `<meta name="htmlboard:variant" content="?theme=dark · dark">` shows the same file again with that query string, named "<title> · dark": for states the page builds from its URL (themes, empty or error states, data). A variant can also change the size: `content="width=390, height=844 · mobile"` shows a responsive page at a phone's size too, from the same file (`?theme=dark width=390 · dark mobile` does both). One tag per variant; they follow the file.
|
|
58
|
+
- `<meta name="htmlboard:label" content="light">` adds a label to the name of the file's own frame ("Checkout · light"), to tell it from its variants.
|
|
59
|
+
- `category=…` in the `htmlboard` tag sets the explorer's filter (`mobile`, `components`, your own word) when the path and size don't say it.
|
|
60
|
+
- `<meta name="htmlboard" content="skip">` leaves a file out (partials, templates, a page that isn't a design).
|
|
53
61
|
|
|
54
62
|
## Shared styles
|
|
55
63
|
|
|
56
64
|
Put what the designs share in one stylesheet instead of repeating it in every file: it keeps the designs consistent and spares writing the same CSS again.
|
|
57
65
|
|
|
58
66
|
- The designs folder has a `styles.css` at its top (sample projects start with one). Before writing a design, see what the designs already share: `npx htmlboard catalog <designs folder>` prints it in a few lines, read from the files (each stylesheet's variables and classes, the scripts, the components with their attributes and an example). Use what fits (`var(--accent)`, `.btn`, `<tx-item>`) and add what's missing; open a file only for what the list doesn't say.
|
|
59
|
-
- If there is none and the project already has a stylesheet or design tokens, link those. Otherwise create `styles.css` with your first design: colors, type and spacing as custom properties, the base styles, and the
|
|
67
|
+
- If there is none and the project already has a stylesheet or design tokens, link those. Otherwise create `styles.css` with your first design: colors, type and spacing as custom properties, the base styles, and the classes you'll repeat.
|
|
60
68
|
- Link it with a relative path: `styles.css` from the top of the folder, `../styles.css` from a section folder.
|
|
61
|
-
- A design's own `<style>` holds only what is specific to it. When a second design needs the same thing, move it to `styles.css`.
|
|
62
|
-
- Split it when it grows (`forms.css`, `charts.css`) and link the extra files where they are used.
|
|
69
|
+
- A design's own `<style>` holds only what is specific to it. When a second design needs the same thing, move it to `styles.css`. Split it when it grows (`forms.css`, `charts.css`) and link the extra files where they are used.
|
|
63
70
|
- Editing `styles.css` restyles every design that links it, which is the point for a product's screens. For one-off explorations that should keep their look (a different style, logo options), style them in their own file instead.
|
|
64
71
|
|
|
65
|
-
More tags, all optional:
|
|
66
|
-
|
|
67
|
-
- `category=…` in the `htmlboard` tag files the design under the explorer's filters: `mobile`, `tablet`, `desktop`, `components` or your own word. Usually not needed: it comes from words in the path, then from the size when it is a screen's.
|
|
68
|
-
- `<meta name="htmlboard:variant" content="?theme=dark · dark">` shows the same file again with that query string, named "<title> · dark". Use it for states the page builds from its URL (themes, empty or error states, data). Add one tag per variant; they follow the file.
|
|
69
|
-
- `<meta name="htmlboard:label" content="light">` adds a label to the name of the file's own frame ("Checkout · light"), to tell it from its variants.
|
|
70
|
-
- `<meta name="htmlboard" content="skip">` leaves a file out (partials, templates, a page that isn't a design).
|
|
71
|
-
|
|
72
72
|
## Components
|
|
73
73
|
|
|
74
|
-
Markup that designs repeat (a sidebar, a tab bar, a list row, a card with content) goes in a component,
|
|
75
|
-
|
|
76
|
-
- `components/tx-item.html`, at the top of the designs folder, is `<tx-item>`: the file name is the tag, with a hyphen and no number prefix. Designs load `components.js` once (above). If the folder has no `components.js`, run `npx htmlboard components <designs folder>`.
|
|
77
|
-
- The catalog (above) lists them, with their attributes, an example and the designs that use them. Use them; don't copy their markup into designs.
|
|
78
|
-
- Make one when a second design repeats a block (or a design repeats it three times). Not for what a `styles.css` class already does (`<button class="btn btn-primary">` is short), nor in one-off explorations.
|
|
79
|
-
|
|
80
|
-
A component file is a piece of HTML (no `<!DOCTYPE>`, `<html>`, `<head>` or `<body>`):
|
|
81
|
-
|
|
82
|
-
```html
|
|
83
|
-
<!-- <tx-item icon="💼" name="Invoice #1042" meta="Yesterday" amount="+$3,450.00" positive></tx-item> -->
|
|
84
|
-
<div class="icon">{{icon}}</div>
|
|
85
|
-
<div class="what"><b>{{name}}</b> <span class="muted">{{meta}}</span></div>
|
|
86
|
-
<div class="amount">{{amount}}</div>
|
|
87
|
-
|
|
88
|
-
<style>
|
|
89
|
-
& { display: flex; align-items: center; gap: 14px; } /* & is the element itself */
|
|
90
|
-
.icon { width: 40px; height: 40px; border-radius: 12px; background: var(--surface-2); }
|
|
91
|
-
&[positive] .amount { color: var(--emerald); }
|
|
92
|
-
</style>
|
|
93
|
-
```
|
|
74
|
+
Markup that designs repeat (a sidebar, a tab bar, a list row, a card with content) goes in a component, a file in `components/` that designs use as one tag instead of the block: `<tx-item icon="☕" name="Blue Bottle" amount="-$6.75"></tx-item>`.
|
|
94
75
|
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
- No logic in the markup: variants are attributes styled in CSS (`&[positive]`), and a list is several elements.
|
|
98
|
-
- Its `<style>` only reaches inside the element, which is a block box unless `&` says otherwise. `styles.css` applies inside too: build with its classes (`card`, `btn`, `muted`) and name the component's own parts with names it doesn't use. A design's own `<style>` comes after and can restyle one element, but its selectors reach inside components too, so keep them specific.
|
|
99
|
-
- `<script>` runs for each element once it is built, with `this` as the element. Use it sparingly, a few lines for what markup and CSS can't do, like marking the current link (`<app-sidebar active="Customers">`) or a toggle. No fetching, data or frameworks.
|
|
100
|
-
- Relative links in a component start from its file (`../01-screens/02-customers.html`).
|
|
101
|
-
- Close the tag: `<tx-item …></tx-item>`, never `<tx-item />`. A component can't be a table's row or cell itself (the browser moves it out of the `<table>`), but it works inside a cell: `<td><org-cell …></org-cell></td>`. Where it sits (fixed, margins, the grid around it) belongs to the design.
|
|
102
|
-
- Editing a component changes every design that uses it, like `styles.css`.
|
|
76
|
+
- The catalog lists the project's components with their attributes, an example and the designs that use them. Use them; don't copy their markup into designs.
|
|
77
|
+
- Make one when a second design repeats a block (or a design repeats it three times); not for what a `styles.css` class already does, nor in one-off explorations. To write or change one, use the htmlboard-components skill.
|
|
103
78
|
|
|
104
79
|
## Group and order
|
|
105
80
|
|
|
106
81
|
- **A folder is a section** (a row on the canvas), named after the folder. Put related designs in a folder: `designs/03-checkout/`.
|
|
107
82
|
- **Number prefixes set the order** of files and folders: `01-one-page.html`, `02-steps.html`. They don't show in names or links, so you can renumber freely. Files at the top of the designs folder form a first row without a title.
|
|
108
83
|
- New files and folders show up on the canvas by themselves; you don't register them anywhere.
|
|
109
|
-
|
|
110
|
-
**`designs.json`** is optional. When a project has one (in the designs folder), it curates: sections with titles (accents, capitals, an order across folders), and values that win over the files'. Designs it doesn't list still show after the ones it lists, in their folder's section, unless it says `"unlisted": "hide"`. If the project's designs.json groups designs by hand, add a new design to the right section there:
|
|
111
|
-
|
|
112
|
-
```json
|
|
113
|
-
{
|
|
114
|
-
"sections": [
|
|
115
|
-
{ "title": "Checkout · mobile", "designs": ["03-checkout/01-one-page.html", "03-checkout/02-steps.html"] },
|
|
116
|
-
{ "title": "Launch flyers", "folder": "04-flyers" }
|
|
117
|
-
]
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
An entry is a file name (its variants follow it), or `{ "file", "title", "width", "height", "category", "description" }` to override the file. `"folder"` brings a whole folder. Components aren't on the canvas unless designs.json says `"components": "show"` (or lists `components/<tag>.html`): each shows alone, from its first-line example, 480 wide unless its file has `<meta name="htmlboard" content="width=…">`. `npx htmlboard migrate <folder>` moves an old designs.json's sizes, names and variants into the files (`--write` to apply; without it, it only shows the plan).
|
|
84
|
+
- A `designs.json` in the designs folder, when there is one, curates the canvas (section titles, an order across folders, designs it hides): use the htmlboard-designs-json skill before adding, moving or renaming designs there.
|
|
122
85
|
|
|
123
86
|
## Live reload
|
|
124
87
|
|
|
125
|
-
|
|
126
|
-
- Saving a file artboards use (a stylesheet, a component, a script, an image, data) reloads the artboards that loaded it.
|
|
127
|
-
- Adding, removing or moving artboards, or saving `designs.json`, rebuilds the canvas.
|
|
88
|
+
Saving a design reloads its frame, saving a file designs use (a stylesheet, a component, an image) reloads the designs that loaded it, and new or removed designs show up by themselves. The user's view stays put: there is nothing to refresh.
|
|
128
89
|
|
|
129
90
|
## Working with the user
|
|
130
91
|
|
|
131
92
|
- Put variants in separate files next to each other (`01-hero-a.html`, `02-hero-b.html`) instead of overwriting the current design, so they can be compared on the canvas. Keep the current one as the reference until the user picks.
|
|
132
|
-
- Say which files you created or changed and what to look at; the user is watching the canvas, not the code. Link straight to
|
|
133
|
-
- Before reporting back, run `npx htmlboard check <designs folder>` (or a design's file)
|
|
93
|
+
- Say which files you created or changed and what to look at; the user is watching the canvas, not the code. Link straight to a design with its file after the `#`: `http://localhost:3000/#03-checkout/02-steps.html`.
|
|
94
|
+
- Before reporting back, run `npx htmlboard check <designs folder>` (or a design's file): the canvas open in the user's browser loads each design again and lists what went wrong (script errors, missing files and components, fonts that didn't load, content cut at the frame's edge, text cut off or spilling out). Fix it and run it again. A note about content below a fixed-height frame is fine for a screen that scrolls, not for a flyer. If it says the canvas isn't open, ask the user to open its URL.
|
|
134
95
|
- The check can't judge looks (colors, spacing, alignment). If you can take screenshots, also look at the design at its real size (`http://localhost:3000/<file>`).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: htmlboard-components
|
|
3
|
+
description: Write or change an htmlboard component, a file in components/ that designs use as a custom element (components/tx-item.html is <tx-item>). Use when a block repeats across htmlboard designs, or before creating or editing a file in components/.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HtmlBoard components
|
|
7
|
+
|
|
8
|
+
A component is markup that designs repeat (a sidebar, a tab bar, a list row, a card with content), written once and used as one tag: `<tx-item icon="☕" name="Blue Bottle" amount="-$6.75"></tx-item>`. The htmlboard skill covers the designs themselves.
|
|
9
|
+
|
|
10
|
+
- `components/tx-item.html`, at the top of the designs folder, is `<tx-item>`: the file name is the tag, with a hyphen and no number prefix.
|
|
11
|
+
- Designs load `components.js` once, after their stylesheet: `<script type="module" src="../components.js"></script>`. If the designs folder has no `components.js`, run `npx htmlboard components <designs folder>`.
|
|
12
|
+
- See what exists first: `npx htmlboard catalog <designs folder>` lists the components with their attributes, an example and the designs that use them.
|
|
13
|
+
|
|
14
|
+
A component file is a piece of HTML (no `<!DOCTYPE>`, `<html>`, `<head>` or `<body>`):
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<!-- <tx-item icon="💼" name="Invoice #1042" meta="Yesterday" amount="+$3,450.00" positive></tx-item> -->
|
|
18
|
+
<div class="icon">{{icon}}</div>
|
|
19
|
+
<div class="what"><b>{{name}}</b> <span class="muted">{{meta}}</span></div>
|
|
20
|
+
<div class="amount">{{amount}}</div>
|
|
21
|
+
|
|
22
|
+
<style>
|
|
23
|
+
& { display: flex; align-items: center; gap: 14px; } /* & is the element itself */
|
|
24
|
+
.icon { width: 40px; height: 40px; border-radius: 12px; background: var(--surface-2); }
|
|
25
|
+
&[positive] .amount { color: var(--emerald); }
|
|
26
|
+
</style>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- First line: a comment with an example of use, with real values, every attribute and its variants side by side. The catalog shows it, and it is the component's preview on the canvas.
|
|
30
|
+
- `{{name}}` is the element's `name="…"` attribute, as text. Content goes in slots: `<slot></slot>` takes the element's children, `<slot name="actions"></slot>` its children with `slot="actions"`, and what a slot holds shows when nothing fills it.
|
|
31
|
+
- No logic in the markup: variants are attributes styled in CSS (`&[positive]`), and a list is several elements.
|
|
32
|
+
- Its `<style>` only reaches inside the element, which is a block box unless `&` says otherwise. `styles.css` applies inside too: build with its classes (`card`, `btn`, `muted`) and name the component's own parts with names it doesn't use. A design's own `<style>` comes after and can restyle one element, but its selectors reach inside components too, so keep them specific.
|
|
33
|
+
- `<script>` runs for each element once it is built, with `this` as the element. Use it sparingly, a few lines for what markup and CSS can't do, like marking the current link (`<app-sidebar active="Customers">`) or a toggle. No fetching, data or frameworks.
|
|
34
|
+
- Relative links in a component start from its file (`../01-screens/02-customers.html`).
|
|
35
|
+
- Close the tag: `<tx-item …></tx-item>`, never `<tx-item />`. A component can't be a table's row or cell itself (the browser moves it out of the `<table>`), but it works inside a cell: `<td><org-cell …></org-cell></td>`. Where it sits (fixed, margins, the grid around it) belongs to the design.
|
|
36
|
+
- Editing a component changes every design that uses it, like `styles.css`. Afterwards, `npx htmlboard check <designs folder>` lists components that weren't built and errors in their scripts.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: htmlboard-designs-json
|
|
3
|
+
description: Curate an htmlboard canvas with designs.json (section titles and order, a design's name or size, hiding unlisted designs, showing components). Use when the designs folder has a designs.json, or the user wants sections named or ordered beyond what folders give.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HtmlBoard designs.json
|
|
7
|
+
|
|
8
|
+
Without one, folders are the canvas's sections and number prefixes set the order (the htmlboard skill covers the designs themselves). A `designs.json` in the designs folder curates on top of that: sections with titles (accents, capitals, an order across folders), and values that win over the files'. The files still say the rest: their name, size and variants in their `<head>`.
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"title": "Wallet app",
|
|
13
|
+
"sections": [
|
|
14
|
+
{ "title": "Checkout · mobile", "designs": ["03-checkout/01-one-page.html", "03-checkout/02-steps.html"] },
|
|
15
|
+
{ "title": "Launch flyers", "folder": "04-flyers" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- Sections come in their order, designs in theirs. An entry is a file name (its variants follow it; with a query string, it places one variant), or `{ "file", "title", "width", "height", "category", "description" }` to override the file's values.
|
|
21
|
+
- `"folder"` brings a whole folder, in prefix order.
|
|
22
|
+
- Designs it doesn't list still show after the ones it lists, in the section that holds their folder (or a section of their own), unless it says `"unlisted": "hide"`. When it groups designs by hand, add a new design to the right section.
|
|
23
|
+
- Components aren't on the canvas unless it says `"components": "show"` (or lists `components/<tag>.html`): each shows alone, from its first-line example, 480 wide unless its file has `<meta name="htmlboard" content="width=…">`.
|
|
24
|
+
- Saving it rebuilds the canvas.
|
|
25
|
+
- `npx htmlboard migrate <designs folder>` moves an old designs.json's sizes, names and variants into the files and keeps only what they can't say (`--write` to apply; without it, it only shows the plan).
|
package/src/canvases.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
|
|
3
|
+
// The htmlboard canvases running on this machine, found by asking ports for /api/health: for
|
|
4
|
+
// `htmlboard check`, and so that starting htmlboard again for a folder points at the running one.
|
|
5
|
+
|
|
6
|
+
/** Where htmlboard listens unless told otherwise: 3000, or the next free port */
|
|
7
|
+
export const PORTS = Array.from({ length: 21 }, (_, i) => 3000 + i);
|
|
8
|
+
|
|
9
|
+
/** GET a URL: its status and body */
|
|
10
|
+
export function get(url, { timeout = 0 } = {}) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const req = http.get(url, (res) => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
15
|
+
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
|
|
16
|
+
res.on('error', reject);
|
|
17
|
+
});
|
|
18
|
+
req.on('error', reject);
|
|
19
|
+
if (timeout) req.setTimeout(timeout, () => req.destroy(new Error('timeout')));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The htmlboard servers listening on these ports: each its health (dir, root, version) with its
|
|
25
|
+
* port and url.
|
|
26
|
+
*/
|
|
27
|
+
export async function findCanvases(ports = PORTS) {
|
|
28
|
+
const found = await Promise.all([...new Set(ports)].map(async (port) => {
|
|
29
|
+
try {
|
|
30
|
+
const { status, body } = await get(`http://127.0.0.1:${port}/api/health`, { timeout: 1500 });
|
|
31
|
+
const health = JSON.parse(body);
|
|
32
|
+
return status === 200 && health.status === 'ok' && health.dir ? { ...health, port, url: `http://localhost:${port}` } : null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
return found.filter(Boolean);
|
|
38
|
+
}
|
package/src/check.js
CHANGED
|
@@ -1,48 +1,18 @@
|
|
|
1
|
-
import http from 'node:http';
|
|
2
1
|
import path from 'node:path';
|
|
2
|
+
import { PORTS, get, findCanvases } from './canvases.js';
|
|
3
3
|
|
|
4
4
|
// `htmlboard check`: the canvas running for a folder, open in a browser, loads its designs and says
|
|
5
5
|
// what went wrong in each (see client/check.js); this finds that canvas, asks it, and puts the
|
|
6
6
|
// answer in a few lines of text.
|
|
7
7
|
|
|
8
|
-
/** Where htmlboard listens unless told otherwise: 3000, or the next free port */
|
|
9
|
-
const PORTS = Array.from({ length: 21 }, (_, i) => 3000 + i);
|
|
10
|
-
|
|
11
8
|
/** A problem the check couldn't get past (no canvas running, none open in a browser): exit code 2 */
|
|
12
9
|
export class CheckError extends Error {}
|
|
13
10
|
|
|
14
|
-
function get(url, { timeout = 0 } = {}) {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const req = http.get(url, (res) => {
|
|
17
|
-
const chunks = [];
|
|
18
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
19
|
-
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
|
|
20
|
-
res.on('error', reject);
|
|
21
|
-
});
|
|
22
|
-
req.on('error', reject);
|
|
23
|
-
if (timeout) req.setTimeout(timeout, () => req.destroy(new Error('timeout')));
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
11
|
const isInside = (base, target) => {
|
|
28
12
|
const rel = path.relative(base, target);
|
|
29
13
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
30
14
|
};
|
|
31
15
|
|
|
32
|
-
/** The htmlboard servers listening on these ports, each with its designs folder (dir) and root */
|
|
33
|
-
async function findCanvases(ports) {
|
|
34
|
-
const found = await Promise.all(ports.map(async (port) => {
|
|
35
|
-
try {
|
|
36
|
-
const { status, body } = await get(`http://127.0.0.1:${port}/api/health`, { timeout: 1500 });
|
|
37
|
-
const health = JSON.parse(body);
|
|
38
|
-
return status === 200 && health.status === 'ok' && health.dir ? { ...health, port, url: `http://localhost:${port}` } : null;
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}));
|
|
43
|
-
return found.filter(Boolean);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
16
|
/**
|
|
47
17
|
* Check designs with the canvas that shows them: a designs folder, a folder in it, or a design's
|
|
48
18
|
* file (default: all the designs of the canvas found). The canvas must be open in a browser, where
|
package/src/client/explorer.js
CHANGED
|
@@ -797,7 +797,7 @@ async function takeCheck(request) {
|
|
|
797
797
|
|
|
798
798
|
async function runCheck({ id, designs: boards }) {
|
|
799
799
|
const designs = boards.map((board) => {
|
|
800
|
-
const shown = state.designs.find((d) => d.
|
|
800
|
+
const shown = state.designs.find((d) => d.id === board.id); // a size variant has its file's URL
|
|
801
801
|
const height = Number(board.height) || 0;
|
|
802
802
|
return {
|
|
803
803
|
file: board.file,
|
package/src/designs.js
CHANGED
|
@@ -120,12 +120,36 @@ function parseSettings(content) {
|
|
|
120
120
|
return settings;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
// A variant's own setting, as the htmlboard tag says it: width=390, height=844, category=mobile
|
|
124
|
+
const VARIANT_SETTING = /^(width|height|category)=[^,;\s]+[,;]?$/i;
|
|
125
|
+
const LABEL_START = /^[·|:,\-–—]+\s*/;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* <meta name="htmlboard:variant" content="?tema=escuro · escuro">: the file once more, with a
|
|
129
|
+
* query string, a size (width=390, height=844) or both, then a label. A query may leave out its "?".
|
|
130
|
+
*/
|
|
124
131
|
function parseVariant(content) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
const tokens = content.trim().split(/\s+/);
|
|
133
|
+
let query = '';
|
|
134
|
+
const settings = [];
|
|
135
|
+
let i = 0;
|
|
136
|
+
for (; i < tokens.length; i++) {
|
|
137
|
+
const token = tokens[i];
|
|
138
|
+
if (VARIANT_SETTING.test(token)) settings.push(token.replace(/[,;]$/, ''));
|
|
139
|
+
else if (token.startsWith('?') && !query) query = token;
|
|
140
|
+
else if (i === 0 && !LABEL_START.test(token)) query = `?${token}`;
|
|
141
|
+
else break;
|
|
142
|
+
}
|
|
143
|
+
if (query === '?') query = '';
|
|
144
|
+
const { width, height, category } = parseSettings(settings.join(','));
|
|
145
|
+
if (!query && !width && !height) return null;
|
|
146
|
+
return { query, label: tokens.slice(i).join(' ').replace(LABEL_START, '').trim(), ...compact({ width, height, category }) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A variant as its tag's content says it: "?theme=dark width=390, height=844 · dark mobile" */
|
|
150
|
+
export function formatVariant({ query = '', label = '', width, height, category }) {
|
|
151
|
+
const size = [width && `width=${width}`, height && `height=${height}`, category && `category=${category}`].filter(Boolean).join(', ');
|
|
152
|
+
return [query, size].filter(Boolean).join(' ') + (label ? ` · ${label}` : '');
|
|
129
153
|
}
|
|
130
154
|
|
|
131
155
|
/** What an HTML file says about itself, from its <head> (see the top of this file) */
|
|
@@ -196,12 +220,24 @@ function fileDesign(file, folder, meta) {
|
|
|
196
220
|
});
|
|
197
221
|
}
|
|
198
222
|
|
|
199
|
-
|
|
223
|
+
// A variant of a file's design: another query string (a state the page builds from its URL), another
|
|
224
|
+
// size (the same page at a phone's width), or both. Without a height of its own, a variant that
|
|
225
|
+
// changes the width grows with its page. One that only changes the size loads the same URL as its
|
|
226
|
+
// file, so its id comes from its label.
|
|
227
|
+
function variantDesign(base, { query = '', label, width, height, category }) {
|
|
228
|
+
const sized = Boolean(width || height);
|
|
229
|
+
const size = sized ? { width: width ?? base.width, height: height ?? (width ? undefined : base.height) } : {};
|
|
230
|
+
const sizeName = sized ? `${size.width}${size.height ? ` × ${size.height}` : ''}` : '';
|
|
231
|
+
const name = label || [query.slice(1), sizeName].filter(Boolean).join(' · ');
|
|
200
232
|
return compact({
|
|
201
233
|
...base,
|
|
202
|
-
|
|
234
|
+
...size,
|
|
235
|
+
key: base.file + query + (sized ? `@${size.width}x${size.height ?? ''}` : ''),
|
|
203
236
|
file: base.file + query,
|
|
204
|
-
title: `${base.baseTitle} · ${
|
|
237
|
+
title: `${base.baseTitle} · ${name}`,
|
|
238
|
+
// A new size is a new device: its category comes from its size, unless it says one
|
|
239
|
+
category: sized ? category : category ?? base.category,
|
|
240
|
+
id: sized ? `${designId(base.file + query)}-${slugify(name)}` : undefined,
|
|
205
241
|
variantOf: base.file
|
|
206
242
|
});
|
|
207
243
|
}
|
package/src/migrate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { loadDesignsManifest, parseDesignMeta, stripPrefix } from './designs.js';
|
|
4
|
+
import { loadDesignsManifest, parseDesignMeta, stripPrefix, formatVariant } from './designs.js';
|
|
5
5
|
|
|
6
6
|
// `htmlboard migrate`: moves what a designs.json says about each design into the design's own file
|
|
7
7
|
// (its size, category, label, variants and description) and leaves designs.json with what only it
|
|
@@ -110,7 +110,7 @@ export function planMigration(designsDir) {
|
|
|
110
110
|
}
|
|
111
111
|
for (const key of ['description', 'tags', 'id']) if (item.entry[key] !== undefined) item.keep[key] = item.entry[key];
|
|
112
112
|
}
|
|
113
|
-
for (const v of declared) lines.push(`<meta name="htmlboard:variant" content="${escapeAttr(
|
|
113
|
+
for (const v of declared) lines.push(`<meta name="htmlboard:variant" content="${escapeAttr(formatVariant(v))}">`);
|
|
114
114
|
|
|
115
115
|
const next = rewriteHead(html, lines);
|
|
116
116
|
if (next !== html) files.push({ file: base, lines, html: next });
|
package/src/server.js
CHANGED
|
@@ -224,7 +224,7 @@ export function createServer(options = {}) {
|
|
|
224
224
|
const wanted = only.map((item) => item.replace(/\\/g, '/').replace(/^\.?\/+|\/+$/g, ''));
|
|
225
225
|
const designs = loadDesignsManifest(targetDir, rootDir).sections
|
|
226
226
|
.flatMap((section) => section.artboards)
|
|
227
|
-
.map(({ file, title, width, height, fit }) => ({ file, title, width, height, fit }))
|
|
227
|
+
.map(({ id, file, title, width, height, fit }) => ({ id, file, title, width, height, fit }))
|
|
228
228
|
.filter(({ file }) => {
|
|
229
229
|
const own = (designsPath ? file.slice(designsPath.length + 1) : file).split('?')[0];
|
|
230
230
|
return !wanted.length || wanted.some((item) => !item || own === item || own.startsWith(`${item}/`));
|
package/src/skills.js
CHANGED
|
@@ -21,7 +21,8 @@ export const SKILL_TARGETS = {
|
|
|
21
21
|
function bundledSkills() {
|
|
22
22
|
return fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
23
23
|
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(SKILLS_DIR, entry.name, 'SKILL.md')))
|
|
24
|
-
.map((entry) => entry.name)
|
|
24
|
+
.map((entry) => entry.name)
|
|
25
|
+
.sort();
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function resolveTargets(agent = 'all') {
|