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 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 skill
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
- HtmlBoard ships an [Agent Skill](https://agentskills.io) that teaches your agent how to use it: where to put artboards, how to size, name and group them (files describe themselves; `designs.json` is optional) and how to start the canvas. Install it in your project:
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/htmlboard/` (Claude Code) and `.agents/skills/htmlboard/` (Codex and other agents that read `.agents/skills`). Commit them so everyone working on the project gets the skill. Then start a new agent session and just ask for a design.
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 it (accepts the same --global / --agent)
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 skill; a copy you edited is kept unless you pass `--force`.
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 skill, 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.
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 that query string, then its label. As many as you like; they follow the file. |
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
- │ └── htmlboard/ # Agent skill (SKILL.md) copied by `htmlboard skills install`
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 skill to this project's agents
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 skill into your agents' skill folders
158
- htmlboard skills uninstall [options] Remove it
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 skill, then ask it for a design.`);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlboard",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Infinite canvas and design explorer for HTML artboards — designed for humans and LLMs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, components, flyers or any visual proposal, or mentions htmlboard, artboards or designs.json.
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
- - If a server is already running for this folder, reuse it. `GET /api/health` answers `{"status":"ok","dir":…}`.
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
- - No build step: plain HTML, CSS and browser JavaScript (ES modules and CDN scripts work).
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 components you'll repeat.
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, so a design writes one tag instead of the block: `<tx-item icon="☕" name="Blue Bottle" amount="-$6.75"></tx-item>`.
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
- - 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.
96
- - `{{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.
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
- - Saving an artboard's HTML reloads just that frame; the user's view stays put. A new name or size in its `<head>` updates in place.
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 an artboard with `http://localhost:3000/#<id>`: the id is the file's path without prefixes and `.html`, other symbols turned into `-` (`03-checkout/02-steps.html` → `checkout-steps`; a variant adds its query: `checkout-steps-theme-dark`).
133
- - 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, as text, 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 of its box. Fix what it lists and run it again. A note that content goes on below a frame of fixed height is fine for a screen that scrolls, not for a flyer. The check runs in the browser: if it says the canvas isn't open, ask the user to open its URL.
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).
@@ -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
@@ -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.file === board.file);
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
- /** <meta name="htmlboard:variant" content="?tema=escuro · escuro">: the query, then a label */
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 [, first, rest] = content.match(/^(\S+)\s*(.*)$/s);
126
- const query = first.startsWith('?') ? first : `?${first}`;
127
- if (query === '?') return null;
128
- return { query, label: rest.replace(/^[·|:,\-–—]+\s*/, '').trim() };
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
- function variantDesign(base, { query, label }) {
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
- key: base.file + query,
234
+ ...size,
235
+ key: base.file + query + (sized ? `@${size.width}x${size.height ?? ''}` : ''),
203
236
  file: base.file + query,
204
- title: `${base.baseTitle} · ${label || query.slice(1)}`,
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(v.query + (v.label ? SEP + v.label : ''))}">`);
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') {