@autono/create-open-pages 0.6.0 → 0.8.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/dist/cli.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import * as p from "@clack/prompts";
3
+ import { S_ERROR } from "@clack/prompts";
2
4
  import chalk from "chalk";
3
5
  import { cp, mkdir, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
4
- import { basename, dirname, join, resolve, sep } from "node:path";
6
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
7
  import { fileURLToPath } from "node:url";
6
8
  import { Command } from "commander";
7
- import prompts from "prompts";
8
9
  import { spawn } from "node:child_process";
9
10
  import { createHash } from "node:crypto";
10
11
  import { existsSync } from "node:fs";
11
-
12
12
  //#region src/git.ts
13
13
  const IS_WINDOWS$1 = process.platform === "win32";
14
14
  async function run$1(cmd, args, cwd) {
15
- return new Promise((resolve$1, reject) => {
15
+ return new Promise((resolve, reject) => {
16
16
  const child = spawn(cmd, args, {
17
17
  cwd,
18
18
  stdio: [
@@ -31,7 +31,7 @@ async function run$1(cmd, args, cwd) {
31
31
  stderr += d.toString();
32
32
  });
33
33
  child.on("error", reject);
34
- child.on("close", (code) => resolve$1({
34
+ child.on("close", (code) => resolve({
35
35
  code,
36
36
  stdout,
37
37
  stderr
@@ -40,8 +40,7 @@ async function run$1(cmd, args, cwd) {
40
40
  }
41
41
  async function isGitAvailable() {
42
42
  try {
43
- const res = await run$1("git", ["--version"], process.cwd());
44
- return res.code === 0;
43
+ return (await run$1("git", ["--version"], process.cwd())).code === 0;
45
44
  } catch {
46
45
  return false;
47
46
  }
@@ -63,10 +62,10 @@ async function gitInitAndCommit(target) {
63
62
  status: "skipped-nested",
64
63
  message: "target is already inside a git work tree; leaving parent repo alone"
65
64
  };
66
- const init$1 = await run$1("git", ["init"], target);
67
- if (init$1.code !== 0) return {
65
+ const init = await run$1("git", ["init"], target);
66
+ if (init.code !== 0) return {
68
67
  status: "failed",
69
- message: `git init failed: ${init$1.stderr.trim() || init$1.stdout.trim()}`
68
+ message: `git init failed: ${init.stderr.trim() || init.stdout.trim()}`
70
69
  };
71
70
  const add = await run$1("git", ["add", "-A"], target);
72
71
  if (add.code !== 0) return {
@@ -84,7 +83,6 @@ async function gitInitAndCommit(target) {
84
83
  };
85
84
  return { status: "committed" };
86
85
  }
87
-
88
86
  //#endregion
89
87
  //#region src/init.ts
90
88
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -99,11 +97,10 @@ function sanitizeDirName(value) {
99
97
  }
100
98
  async function isDirNonEmpty(target) {
101
99
  if (!existsSync(target)) return false;
102
- const entries = await readdir(target);
103
- return entries.some((e) => !e.startsWith("."));
100
+ return (await readdir(target)).some((e) => !e.startsWith("."));
104
101
  }
105
102
  function coreVersionRange() {
106
- return `^0.6.0`;
103
+ return `^0.8.0`;
107
104
  }
108
105
  async function linkOrCopy(relSrc, dst) {
109
106
  await rm(dst, {
@@ -155,21 +152,9 @@ async function collectFiles(dir, prefix = "") {
155
152
  }
156
153
  return out;
157
154
  }
158
- async function runInstall(pm, cwd) {
159
- await new Promise((res, rej) => {
160
- const child = spawn(pm, ["install"], {
161
- cwd,
162
- stdio: "inherit",
163
- shell: IS_WINDOWS
164
- });
165
- child.on("error", rej);
166
- child.on("close", (code) => code === 0 ? res() : rej(new Error(`${pm} install exited with code ${code}`)));
167
- });
168
- }
169
- async function init(opts) {
170
- const { dir, force, name, packageManager, install, git } = opts;
155
+ async function scaffold(opts) {
156
+ const { target, force, name } = opts;
171
157
  if (!existsSync(TEMPLATE_DIR)) throw new Error(`Template missing at ${TEMPLATE_DIR}. If you are running from source, run \`pnpm --filter @autono/create-open-pages build\` first.`);
172
- const target = resolve(process.cwd(), dir);
173
158
  await mkdir(target, { recursive: true });
174
159
  if (await isDirNonEmpty(target) && !force) throw new Error(`Target ${target} is not empty. Pass --force to scaffold into it anyway.`);
175
160
  await cp(TEMPLATE_DIR, target, { recursive: true });
@@ -185,34 +170,38 @@ async function init(opts) {
185
170
  await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
186
171
  }
187
172
  await writeFile(join(target, ".gitignore"), "node_modules\ndist\nexport\n.DS_Store\n");
188
- const cdTarget = dir === "." ? basename(target) : dir;
189
- process.stdout.write(`\n${chalk.green.bold("✔ Created open-pages workspace")} ${chalk.dim(`in ${target}`)}\n`);
190
- let installed = false;
191
- if (install) {
192
- process.stdout.write(`\n${chalk.bold(`Installing dependencies with ${packageManager}…`)}\n\n`);
193
- try {
194
- await runInstall(packageManager, target);
195
- installed = true;
196
- } catch (err) {
197
- const msg = err instanceof Error ? err.message : String(err);
198
- process.stdout.write(`\n${chalk.yellow("! Dependency install failed:")} ${chalk.dim(msg)}\n` + chalk.dim(` You can retry manually with \`${packageManager} install\`.\n`));
199
- }
200
- }
201
- if (git) {
202
- const result = await gitInitAndCommit(target);
203
- if (result.status === "committed") process.stdout.write(`${chalk.green("✔")} Initialized git repository with first commit.\n`);
204
- else if (result.status === "skipped-nested") process.stdout.write(`${chalk.yellow("!")} Skipped ${chalk.bold("git init")}: ${chalk.dim(result.message ?? "")}\n`);
205
- else if (result.status === "skipped-no-git") process.stdout.write(`${chalk.yellow("!")} Skipped git setup: ${chalk.dim(result.message ?? "")}\n`);
206
- else process.stdout.write(`${chalk.yellow("!")} Git setup failed: ${chalk.dim(result.message ?? "")}\n` + chalk.dim(" You can initialize the repo manually.\n"));
207
- }
208
- process.stdout.write(`\n${chalk.bold("Next steps:")}\n`);
209
- process.stdout.write(` ${chalk.cyan(`cd ${cdTarget}`)}\n`);
210
- if (!installed && install) process.stdout.write(` ${chalk.cyan(`${packageManager} install`)}\n`);
211
- else if (!install) process.stdout.write(` ${chalk.cyan(`${packageManager} install`)} ${chalk.dim("# install was skipped")}\n`);
212
- const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
213
- process.stdout.write(` ${chalk.cyan(devCommand)}\n`);
214
173
  }
215
-
174
+ function installDependencies(pm, cwd) {
175
+ return new Promise((res) => {
176
+ let output = "";
177
+ const child = spawn(pm, ["install"], {
178
+ cwd,
179
+ stdio: [
180
+ "ignore",
181
+ "pipe",
182
+ "pipe"
183
+ ],
184
+ shell: IS_WINDOWS
185
+ });
186
+ child.stdout?.on("data", (chunk) => {
187
+ output += chunk.toString();
188
+ });
189
+ child.stderr?.on("data", (chunk) => {
190
+ output += chunk.toString();
191
+ });
192
+ child.on("error", (err) => res({
193
+ ok: false,
194
+ output: err.message
195
+ }));
196
+ child.on("close", (code) => {
197
+ if (code === 0) res({ ok: true });
198
+ else res({
199
+ ok: false,
200
+ output: output.trim() || `${pm} install exited with code ${code}`
201
+ });
202
+ });
203
+ });
204
+ }
216
205
  //#endregion
217
206
  //#region src/package-manager.ts
218
207
  const PACKAGE_MANAGERS = [
@@ -228,17 +217,18 @@ function detectPackageManager() {
228
217
  if (ua.startsWith("bun")) return "bun";
229
218
  return "npm";
230
219
  }
231
-
232
220
  //#endregion
233
221
  //#region src/index.ts
234
222
  async function readVersion() {
235
223
  const here = dirname(fileURLToPath(import.meta.url));
236
- const pkg = JSON.parse(await readFile(join(here, "..", "package.json"), "utf8"));
237
- return pkg.version;
224
+ return JSON.parse(await readFile(join(here, "..", "package.json"), "utf8")).version;
238
225
  }
239
- function onCancel() {
240
- process.stdout.write(chalk.dim("\nCancelled.\n"));
241
- process.exit(130);
226
+ function unwrap(value) {
227
+ if (p.isCancel(value)) {
228
+ p.cancel("Cancelled.");
229
+ process.exit(130);
230
+ }
231
+ return value;
242
232
  }
243
233
  function packageManagerFromFlags(flags) {
244
234
  const picks = [];
@@ -246,94 +236,134 @@ function packageManagerFromFlags(flags) {
246
236
  if (flags.usePnpm) picks.push("pnpm");
247
237
  if (flags.useYarn) picks.push("yarn");
248
238
  if (flags.useBun) picks.push("bun");
249
- if (picks.length > 1) throw new Error(`Only one of --use-npm / --use-pnpm / --use-yarn / --use-bun may be specified (got ${picks.map((p) => `--use-${p}`).join(", ")}).`);
239
+ if (picks.length > 1) throw new Error(`Only one of --use-npm / --use-pnpm / --use-yarn / --use-bun may be specified (got ${picks.map((pm) => `--use-${pm}`).join(", ")}).`);
250
240
  return picks[0];
251
241
  }
242
+ function displayPath(target) {
243
+ const rel = relative(process.cwd(), target);
244
+ if (rel === "" || rel.startsWith("..")) return target;
245
+ return rel;
246
+ }
247
+ function step(label, isTTY) {
248
+ const spinner = isTTY ? p.spinner() : void 0;
249
+ if (spinner) spinner.start(label);
250
+ else p.log.step(label);
251
+ return {
252
+ done(message) {
253
+ if (spinner) spinner.stop(message);
254
+ else p.log.step(message);
255
+ },
256
+ fail(message) {
257
+ if (spinner) spinner.error(message);
258
+ else p.log.error(message);
259
+ }
260
+ };
261
+ }
262
+ function tail(output, lines = 12) {
263
+ return output.split("\n").slice(-lines).join("\n");
264
+ }
252
265
  async function runInit(dirArg, flags) {
253
266
  const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
267
+ let packageManager = packageManagerFromFlags(flags);
254
268
  let dir = dirArg;
255
- const name = flags.name;
256
269
  let force = flags.force ?? false;
257
- let packageManager = packageManagerFromFlags(flags);
258
- if (isTTY && dir === void 0) {
259
- const answers = await prompts({
260
- type: "text",
261
- name: "dir",
262
- message: "Target directory",
263
- initial: "."
264
- }, { onCancel });
265
- dir = answers.dir;
266
- }
270
+ const install = flags.install !== false;
271
+ const git = flags.git !== false;
272
+ if (isTTY && dir === void 0) dir = unwrap(await p.text({
273
+ message: "Where should we create your workspace?",
274
+ placeholder: ".",
275
+ defaultValue: "."
276
+ }));
267
277
  if (dir !== void 0) {
268
278
  const safe = sanitizeDirName(dir);
269
279
  if (safe !== dir) {
270
280
  if (!isTTY) throw new Error(`Target directory "${dir}" contains characters that break shell commands (spaces, quotes, etc.). Try "${safe}" instead.`);
271
- process.stdout.write(`${chalk.yellow("!")} ${chalk.bold(`"${dir}"`)} has characters that confuse shells.\n Suggested: ${chalk.cyan(`"${safe}"`)}\n`);
272
- const answers = await prompts({
273
- type: "text",
274
- name: "dir",
281
+ p.log.warn(`${chalk.bold(`"${dir}"`)} has characters that confuse shells.`);
282
+ dir = sanitizeDirName(unwrap(await p.text({
275
283
  message: "Directory name",
276
- initial: safe
277
- }, { onCancel });
278
- dir = sanitizeDirName(answers.dir ?? safe);
284
+ initialValue: safe,
285
+ validate: (value) => value?.trim() ? void 0 : "Enter a directory name."
286
+ })));
279
287
  }
280
288
  }
281
- if (isTTY && packageManager === void 0 && flags.install !== false) {
289
+ if (isTTY && packageManager === void 0 && install) {
282
290
  const detected = detectPackageManager();
283
- const answers = await prompts({
284
- type: "select",
285
- name: "packageManager",
291
+ packageManager = unwrap(await p.select({
286
292
  message: "Package manager",
287
- choices: PACKAGE_MANAGERS.map((pm) => ({
288
- title: pm,
289
- value: pm
293
+ options: PACKAGE_MANAGERS.map((pm) => ({
294
+ value: pm,
295
+ label: pm,
296
+ hint: pm === detected ? "detected" : void 0
290
297
  })),
291
- initial: PACKAGE_MANAGERS.indexOf(detected)
292
- }, { onCancel });
293
- packageManager = answers.packageManager;
298
+ initialValue: detected
299
+ }));
294
300
  }
295
301
  const resolvedDir = dir ?? ".";
296
302
  const target = resolve(process.cwd(), resolvedDir);
297
303
  if (!force && await isDirNonEmpty(target)) {
298
304
  if (!isTTY) throw new Error(`Target ${target} is not empty. Pass --force to scaffold into it anyway.`);
299
- const { overwrite } = await prompts({
300
- type: "confirm",
301
- name: "overwrite",
302
- message: `${chalk.yellow(target)} is not empty. Scaffold into it anyway?`,
303
- initial: false
304
- }, { onCancel });
305
- if (!overwrite) {
306
- process.stdout.write(chalk.dim("Aborted.\n"));
305
+ if (!unwrap(await p.confirm({
306
+ message: `${chalk.yellow(displayPath(target))} is not empty. Scaffold into it anyway?`,
307
+ initialValue: false
308
+ }))) {
309
+ p.outro(chalk.dim("Nothing written."));
307
310
  return;
308
311
  }
309
312
  force = true;
310
313
  }
311
- const opts = {
312
- dir: resolvedDir,
314
+ const pm = packageManager ?? detectPackageManager();
315
+ await scaffold({
316
+ target,
313
317
  force,
314
- name,
315
- packageManager: packageManager ?? detectPackageManager(),
316
- install: flags.install !== false,
317
- git: flags.git !== false
318
- };
319
- await init(opts);
318
+ name: flags.name
319
+ });
320
+ p.log.step(`Created workspace ${chalk.dim(`in ${displayPath(target)}`)}`);
321
+ let installed = false;
322
+ if (install) {
323
+ const task = step(`Installing dependencies with ${pm}`, isTTY);
324
+ const result = await installDependencies(pm, target);
325
+ if (result.ok) {
326
+ installed = true;
327
+ task.done(`Installed dependencies with ${pm}`);
328
+ } else {
329
+ task.fail("Dependency install failed");
330
+ p.log.message(chalk.dim(tail(result.output)));
331
+ }
332
+ }
333
+ if (git) {
334
+ const result = await gitInitAndCommit(target);
335
+ if (result.status === "committed") p.log.step("Initialized git repository");
336
+ else if (result.status === "failed") p.log.warn(`Git setup failed ${chalk.dim(`· ${result.message ?? ""}`)}`);
337
+ else p.log.info(`Skipped git init ${chalk.dim(`· ${result.message ?? ""}`)}`);
338
+ }
339
+ const next = [];
340
+ if (target !== process.cwd()) next.push(`cd ${resolvedDir}`);
341
+ if (!installed) next.push(`${pm} install`);
342
+ next.push(pm === "npm" ? "npm run dev" : `${pm} dev`);
343
+ p.note(next.map((line) => chalk.cyan(line)).join("\n"), "Next steps");
344
+ p.outro(`All set! ${chalk.dim("Docs: https://docs.openpages.sh")}`);
320
345
  }
321
346
  async function run(argv) {
322
347
  const version = await readVersion();
323
348
  const program = new Command();
324
349
  program.name("create-open-pages").description("Scaffold and manage open-pages workspaces.").version(version, "-v, --version", "print version").helpOption("-h, --help", "show help").showHelpAfterError(chalk.dim("(run `create-open-pages --help` for usage)"));
325
350
  program.command("init", { isDefault: true }).description("Create a new open-pages workspace").argument("[dir]", "target directory", void 0).option("-f, --force", "overwrite non-empty target directory", false).option("-n, --name <name>", "override package name (defaults to folder name)").option("--use-npm", "use npm to install dependencies").option("--use-pnpm", "use pnpm to install dependencies").option("--use-yarn", "use yarn to install dependencies").option("--use-bun", "use bun to install dependencies").option("--no-install", "skip dependency installation").option("--no-git", "skip git init and initial commit").action(async (dir, flags) => {
326
- await runInit(dir, flags);
351
+ p.intro(`${chalk.inverse.bold(" open-pages ")} ${chalk.dim(`v${version}`)}`);
352
+ try {
353
+ await runInit(dir, flags);
354
+ } catch (err) {
355
+ p.cancel(err instanceof Error ? err.message : String(err));
356
+ process.exit(1);
357
+ }
327
358
  });
328
359
  await program.parseAsync(argv, { from: "user" });
329
360
  }
330
-
331
361
  //#endregion
332
362
  //#region src/cli.ts
333
363
  run(process.argv.slice(2)).catch((err) => {
334
364
  const message = err instanceof Error ? err.message : String(err);
335
- process.stderr.write(`${chalk.red("error:")} ${message}\n`);
365
+ process.stderr.write(`\n ${chalk.red(S_ERROR)} ${message}\n`);
336
366
  process.exit(1);
337
367
  });
338
-
339
- //#endregion
368
+ //#endregion
369
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autono/create-open-pages",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Scaffold an open-pages workspace — agent skills preconfigured, live real-PDF preview out of the box.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "prepack": "pnpm build"
19
19
  },
20
20
  "engines": {
21
- "node": ">=18"
21
+ "node": "^20.19.0 || >=22.12.0"
22
22
  },
23
23
  "keywords": [
24
24
  "pdf",
@@ -45,14 +45,13 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
+ "@clack/prompts": "^1.7.0",
48
49
  "chalk": "^6.0.0",
49
- "commander": "^15.0.0",
50
- "prompts": "^2.4.2"
50
+ "commander": "^15.0.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^22.19.17",
54
- "@types/prompts": "^2.4.9",
55
- "tsdown": "^0.9.9",
56
- "typescript": "^5.9.3"
53
+ "@types/node": "^26.6.2",
54
+ "tsdown": "^0.23.0",
55
+ "typescript": "^7.0.2"
57
56
  }
58
57
  }
@@ -56,6 +56,7 @@ Your job: read those markers, perform the described edits, and delete the marker
56
56
  - After all edits, re-read the file and confirm the only remaining markers are ones you reported as skipped.
57
57
  - Confirm the edited JSX is well-formed (balanced tags, no dangling attributes) and that changed `className` strings are literal Tailwind utilities. If the project's `package.json` has typecheck/lint scripts, run them with the project's package manager; scaffolded projects ship neither TypeScript nor a linter — there, rely on the running dev server (or the `build` script) to surface compile errors. Fix any errors you introduced.
58
58
  - For layout changes, mentally check the Mobile viewport (390px): did the edit introduce a fixed width or a grid with no stacking fallback?
59
+ - When a comment changed interactive elements, forms, motion, or layout, run the `web-design-guidelines` skill on the page and fix any regression it reports before you report.
59
60
 
60
61
  7. **Report.**
61
62
  - Summarise: `N applied, M skipped` plus a one-line description of each change (including the page id).
@@ -94,7 +94,11 @@ Read the **`page-authoring`** skill before writing — file contract, `@/ui/*` c
94
94
 
95
95
  ## Step 7 — Self-review
96
96
 
97
- Run the checklist in `page-authoring` ("Self-review before finishing"): every button/input/card/dialog/tab is a `@/ui` component, colors are tokens, nothing under `ui/` changed. Check all three viewports.
97
+ Run the checklist in `page-authoring` ("Self-review before finishing"): every button/input/card/dialog/tab is a `@/ui` component, colors are tokens, nothing under `ui/` changed. Check all three viewports. Pay particular attention to the CTA and motion items: one label per intent, no wrapped button text at desktop, every animation answers "what does this communicate?".
98
+
99
+ ## Step 7b — Guidelines review
100
+
101
+ Run the `web-design-guidelines` skill on `pages/<id>/` (it fetches Vercel's current Web Interface Guidelines and reports `file:line` findings). Fix what it finds in the page files, re-run until clean or until only findings you can justify remain, and mention any you left in the hand-off.
98
102
 
99
103
  ## Step 8 — Hand off to the user
100
104
 
@@ -25,6 +25,7 @@ A theme can be derived from any combination of:
25
25
  - **Image references / brand guidelines** — paths or URLs to screenshots, mood boards, logo files, a brand PDF. You will translate them into OKLCH tokens.
26
26
  - **Free-text description** — prose describing the desired palette, weight, feel.
27
27
  - **An existing page** — `pages/<id>/index.tsx` whose look should become reusable.
28
+ - **A `DESIGN.md`** — a design-system document in the Google-spec shape (YAML frontmatter, then Overview, Colors, Typography, Layout, Elevation, Shapes, Components, Do's and Don'ts). [designmd.supply](https://designmd.supply) generates one from any public domain, so "match our brand" is: run the site through it, drop the file at `themes/<id>.design.md` (or paste the path), and ask for a theme.
28
29
 
29
30
  If the user's original message already specifies the inputs unambiguously, skip the question and proceed. Otherwise call `AskUserQuestion` (multi-select) so they can pick one or more sources, and ask follow-ups (paths, preset code, page id, prose) only as needed.
30
31
 
@@ -34,6 +35,7 @@ If the user's original message already specifies the inputs unambiguously, skip
34
35
  - **Images**: read each path with the `Read` tool (it accepts images). Note dominant colors (write them as hex, then convert to OKLCH), type family feel, corner radius, surface treatment (flat vs. cards vs. borders), light or dark default, and chrome (nav style, footer).
35
36
  - **Text**: extract explicit values (hex codes, font names, "rounded", "sharp", "dense") and resolve vague language into concrete decisions before writing.
36
37
  - **Existing page**: read `pages/<id>/index.tsx` (and `components/`) and pull any raw palette classes or hex values into token roles (the page's `bg-[#0b0b10]` root → `--background`; its CTA fill → `--primary`; its card border → `--border`), plus fonts and radius.
38
+ - **`DESIGN.md`**: read it and map sections onto tokens — **Colors** gives `--background`/`--foreground`, `--primary`, `--secondary`, `--accent`, `--muted`, `--destructive` (primary = the brand's main action color, not its logo color, when they differ); **Typography** gives `--font-sans` / `--font-heading` and the type-scale feel; **Shapes** gives `--radius`; **Elevation** decides flat vs. card-with-shadow treatment in the component notes; **Layout** informs container widths and density in the demo; **Components** and **Do's and Don'ts** become the "Components in this theme" guidance. Colors may arrive as hex, `rgb()`, `hsl()`, `oklch()`, or CSS named colors — parse each by its own syntax, then convert to OKLCH. A single light palette is normal; derive the `.dark` block from it (invert lightness, keep hue and chroma) and say so in the `.md`.
37
39
 
38
40
  Every color ends up as `oklch(L C H)` — the same format as `styles/globals.css`. When inputs disagree (images use blue but the description says green), ask the user which to honor.
39
41
 
@@ -187,7 +187,10 @@ A theme is `themes/<id>.md` (direction and component notes) + `themes/<id>.css`
187
187
  - [ ] One coherent type scale across the page; contrast holds on dark sections.
188
188
  - [ ] Designed repeats are explicit component instances; data lists are a `.map` over a typed const.
189
189
  - [ ] No `window`/`document` access at module top level; effects clean up.
190
+ - [ ] One label per CTA intent across the page ("Get started" in the nav, hero, and footer — not "Get started" / "Sign up free" / "Try it"), and no primary button label wraps at desktop width.
191
+ - [ ] Every animation is motivated: it shows hierarchy, sequence, feedback, or a state change. If you cannot say which in one sentence, remove it. `motion-safe:` / `motion-reduce:` variants respect the user's preference.
190
192
  - [ ] Nothing outside `pages/<id>/` was edited.
193
+ - [ ] Ran the `web-design-guidelines` skill on the page and resolved or justified its findings.
191
194
 
192
195
  ## Anti-patterns
193
196
 
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: web-design-guidelines
3
+ description: Review page code against Vercel's Web Interface Guidelines — accessibility, focus and keyboard handling, forms, motion, layout, typography, and performance. Use when asked to "review my page", "check accessibility", "audit the design", "review UX", or as the review pass at the end of `create-page` and `apply-comments`.
4
+ metadata:
5
+ author: vercel
6
+ version: "1.0.0"
7
+ argument-hint: <page-id or file-or-pattern>
8
+ ---
9
+
10
+ # Web Interface Guidelines
11
+
12
+ Review page files for compliance with Vercel's [Web Interface Guidelines](https://github.com/vercel-labs/web-interface-guidelines) (MIT). Vendored from the [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) `web-design-guidelines` skill and scoped to an open-pages workspace.
13
+
14
+ ## How it works
15
+
16
+ 1. Fetch the latest guidelines from the source URL below.
17
+ 2. Read the files to review.
18
+ 3. Check them against every rule in the fetched guidelines.
19
+ 4. Report findings in the terse `file:line` format the guidelines specify.
20
+
21
+ ## Guidelines source
22
+
23
+ Fetch fresh guidelines before each review:
24
+
25
+ ```
26
+ https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
27
+ ```
28
+
29
+ Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and the output format. If the fetch fails (offline, blocked), say so and fall back to the self-review checklist in the `page-authoring` skill instead of guessing at rules.
30
+
31
+ ## What to review
32
+
33
+ - Given a page id, review the page's entry (`pages/<id>/index.tsx` or `pages/<id>/index.html`), everything under `pages/<id>/components/`, and any `styles.css`, `style.css`, or `main.js` beside the entry.
34
+ - Given a file or glob, review those files.
35
+ - Given nothing, resolve the current page with the `current-page` skill; if that yields nothing, ask which page to review.
36
+
37
+ Never review or report on files under `ui/`, `lib/`, `hooks/`, or `styles/`. Those are the shared shadcn set and are not edited for one page; a finding there is a `create-theme` or upstream matter, not a page fix.
38
+
39
+ ## Applying findings
40
+
41
+ Report first. When the user asks you to fix, or the review runs inside `create-page` or `apply-comments`, apply the fixes to the page files only, following the `page-authoring` skill: keep `@/ui/*` components, keep semantic tokens, keep the type scale. Do not add dependencies to satisfy a rule.
@@ -18,6 +18,7 @@ You are authoring **web pages** in this repo. Every page is a React component re
18
18
  - **Applying inspector comments** (`@page-comment` markers in a page) — use the `apply-comments` skill.
19
19
  - **Creating or extracting a theme** — use the `create-theme` skill. A theme is `themes/<id>.md` plus `themes/<id>.css` (shadcn token overrides) and a `<id>.demo.tsx` preview; `create-page` reads it before authoring and a page opts in with `meta.theme`.
20
20
  - **shadcn CLI, registries, presets, component docs** — the bundled `shadcn` skill (the official one) covers `npx shadcn@latest search / view / docs / add / apply`.
21
+ - **Reviewing a page for accessibility and interaction quality** — the bundled `web-design-guidelines` skill (Vercel's, vendored) checks a page against the current Web Interface Guidelines. `create-page` runs it before hand-off; run it on its own when asked to "review my page" or "check accessibility".
21
22
  - **Resolving "this page" / "this element"** — when the user references the current page or selection without naming it, consult the `current-page` skill. It reads the dev server's `node_modules/.open-pages/current.json` to find which page and inspector-picked element they mean.
22
23
  - **Any other page edit** — read the `page-authoring` skill before writing. It is the technical reference for everything inside `pages/<id>/`: file contract, styling with Tailwind, layout and responsiveness, interactivity, assets and fonts, self-review checklist. `create-page` and `apply-comments` both defer to it for the *how*.
23
24
 
@@ -1,3 +1,7 @@
1
+ [build]
2
+ command = "npm run build"
3
+ publish = "dist"
4
+
1
5
  [[redirects]]
2
6
  from = "/*"
3
7
  to = "/index.html"
@@ -17,7 +17,7 @@
17
17
  "react-dom": "^19.2.0",
18
18
  "@base-ui/react": "^1.7.0",
19
19
  "@fontsource-variable/geist": "^5.3.0",
20
- "@shadcn/react": "^0.3.0",
20
+ "@shadcn/react": "^0.3.1",
21
21
  "class-variance-authority": "^0.7.1",
22
22
  "clsx": "^2.1.1",
23
23
  "cmdk": "^1.1.1",
@@ -39,7 +39,6 @@
39
39
  "devDependencies": {
40
40
  "@types/react": "^19.2.0",
41
41
  "@types/react-dom": "^19.2.0",
42
- "vite": "^6.4.3",
43
42
  "tailwindcss": "^4.3.3",
44
43
  "typescript": "^5.9.3"
45
44
  }
@@ -12,7 +12,6 @@
12
12
  "strict": true,
13
13
  "skipLibCheck": true,
14
14
  "types": ["@autono/open-pages/env"],
15
- "baseUrl": ".",
16
15
  "paths": {
17
16
  "@/*": ["./*"]
18
17
  }
@@ -1,3 +1,5 @@
1
1
  {
2
+ "buildCommand": "npm run build",
3
+ "outputDirectory": "dist",
2
4
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
3
5
  }