@adieyal/catalogue-cli 0.1.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/bin.d.ts +5 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +53 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/build.d.ts +9 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/dev.d.ts +9 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/new.d.ts +11 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/preview.d.ts +9 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/test.d.ts +10 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/loader.d.ts +17 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/schema.d.ts +229 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/init-CI0WzrG1.js +2702 -0
- package/dist/init-CI0WzrG1.js.map +1 -0
- package/dist/registry/file-loader.d.ts +21 -0
- package/dist/registry/file-loader.d.ts.map +1 -0
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/vite/entry.d.ts +6 -0
- package/dist/vite/entry.d.ts.map +1 -0
- package/dist/vite/index.d.ts +4 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/plugin.d.ts +15 -0
- package/dist/vite/plugin.d.ts.map +1 -0
- package/dist/vite/server.d.ts +15 -0
- package/dist/vite/server.d.ts.map +1 -0
- package/package.json +59 -0
- package/skills/document-component.md +83 -0
- package/skills/migrate-library.md +193 -0
- package/skills/new-component.md +98 -0
- package/skills/new-scenario.md +56 -0
- package/skills/setup-tokens.md +148 -0
|
@@ -0,0 +1,2702 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { existsSync, readdirSync, statSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve, dirname, join, relative } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import "marked";
|
|
6
|
+
import { createServer, build as build$1, preview as preview$1 } from "vite";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
9
|
+
const CategoryDefinitionSchema = z.object({
|
|
10
|
+
/** Unique category identifier (kebab-case) */
|
|
11
|
+
id: z.string().min(1).regex(/^[a-z][a-z0-9-]*$/, {
|
|
12
|
+
message: "Category ID must be kebab-case starting with a letter"
|
|
13
|
+
}),
|
|
14
|
+
/** Human-readable label */
|
|
15
|
+
label: z.string().min(1),
|
|
16
|
+
/** Optional description */
|
|
17
|
+
description: z.string().optional()
|
|
18
|
+
});
|
|
19
|
+
const CategoriesConfigSchema = z.object({
|
|
20
|
+
/** Category definitions */
|
|
21
|
+
items: z.array(CategoryDefinitionSchema).default([]),
|
|
22
|
+
/** Label for uncategorised components */
|
|
23
|
+
uncategorisedLabel: z.string().optional().default("Other")
|
|
24
|
+
});
|
|
25
|
+
const CatalogueConfigSchema = z.object({
|
|
26
|
+
/** Title for the catalogue */
|
|
27
|
+
title: z.string().optional().default("Component Catalogue"),
|
|
28
|
+
/** Category configuration */
|
|
29
|
+
categories: CategoriesConfigSchema.optional().default({ items: [], uncategorisedLabel: "Other" }),
|
|
30
|
+
/** Path to the registry directory (relative to config) */
|
|
31
|
+
registryDir: z.string().optional().default("./registry"),
|
|
32
|
+
/** Output directory for builds (relative to config) */
|
|
33
|
+
outDir: z.string().optional().default("./dist/catalogue"),
|
|
34
|
+
/** Base path for deployment (e.g., '/docs/ui/') */
|
|
35
|
+
basePath: z.string().optional().default("/"),
|
|
36
|
+
/** Port for dev server */
|
|
37
|
+
port: z.number().optional().default(5173),
|
|
38
|
+
/** Component imports configuration */
|
|
39
|
+
components: z.object({
|
|
40
|
+
/** Path to component entry point (relative to config) */
|
|
41
|
+
entry: z.string().optional().default("./src/components/index.ts"),
|
|
42
|
+
/** Package name for imports */
|
|
43
|
+
package: z.string().optional()
|
|
44
|
+
}).optional().default({}),
|
|
45
|
+
/** Playwright test configuration */
|
|
46
|
+
playwright: z.object({
|
|
47
|
+
/** Breakpoints for visual regression tests */
|
|
48
|
+
breakpoints: z.array(z.object({
|
|
49
|
+
name: z.string(),
|
|
50
|
+
width: z.number(),
|
|
51
|
+
height: z.number()
|
|
52
|
+
})).optional(),
|
|
53
|
+
/** Themes to test */
|
|
54
|
+
themes: z.array(z.enum(["light", "dark"])).optional().default(["light", "dark"]),
|
|
55
|
+
/** Screenshot directory */
|
|
56
|
+
screenshotDir: z.string().optional().default("./screenshots")
|
|
57
|
+
}).optional().default({}),
|
|
58
|
+
/** Additional Vite configuration */
|
|
59
|
+
vite: z.record(z.unknown()).optional()
|
|
60
|
+
});
|
|
61
|
+
function validateConfig(data) {
|
|
62
|
+
const result = CatalogueConfigSchema.safeParse(data);
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
errors: result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { success: true, data: result.data };
|
|
70
|
+
}
|
|
71
|
+
const CONFIG_FILES = [
|
|
72
|
+
"catalogue.config.ts",
|
|
73
|
+
"catalogue.config.js",
|
|
74
|
+
"catalogue.config.mjs"
|
|
75
|
+
];
|
|
76
|
+
async function loadConfig(options = {}) {
|
|
77
|
+
const cwd = options.cwd || process.cwd();
|
|
78
|
+
let configPath;
|
|
79
|
+
if (options.configFile) {
|
|
80
|
+
configPath = resolve(cwd, options.configFile);
|
|
81
|
+
if (!existsSync(configPath)) {
|
|
82
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
for (const filename of CONFIG_FILES) {
|
|
86
|
+
const candidate = resolve(cwd, filename);
|
|
87
|
+
if (existsSync(candidate)) {
|
|
88
|
+
configPath = candidate;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!configPath) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`No catalogue config found in ${cwd}. Create one of: ${CONFIG_FILES.join(", ")}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
99
|
+
let rawConfig;
|
|
100
|
+
try {
|
|
101
|
+
const module = await import(configUrl);
|
|
102
|
+
rawConfig = module.default;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Failed to load config from ${configPath}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const result = validateConfig(rawConfig);
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Invalid config in ${configPath}:
|
|
112
|
+
${result.errors.map((e) => ` - ${e}`).join("\n")}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const config = result.data;
|
|
116
|
+
const configDir = dirname(configPath);
|
|
117
|
+
return {
|
|
118
|
+
...config,
|
|
119
|
+
configPath,
|
|
120
|
+
configDir,
|
|
121
|
+
registryPath: resolve(configDir, config.registryDir),
|
|
122
|
+
outPath: resolve(configDir, config.outDir)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function createDefaultConfig() {
|
|
126
|
+
return {
|
|
127
|
+
title: "Component Catalogue",
|
|
128
|
+
categories: {
|
|
129
|
+
items: [],
|
|
130
|
+
uncategorisedLabel: "Other"
|
|
131
|
+
},
|
|
132
|
+
registryDir: "./registry",
|
|
133
|
+
outDir: "./dist/catalogue",
|
|
134
|
+
basePath: "/",
|
|
135
|
+
port: 5173,
|
|
136
|
+
components: {
|
|
137
|
+
entry: "./src/components/index.ts"
|
|
138
|
+
},
|
|
139
|
+
playwright: {
|
|
140
|
+
themes: ["light", "dark"],
|
|
141
|
+
screenshotDir: "./screenshots"
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function loadRegistryFiles(options) {
|
|
146
|
+
const { registryPath, basePath = registryPath } = options;
|
|
147
|
+
const components = [];
|
|
148
|
+
const scenarios = [];
|
|
149
|
+
const examples = [];
|
|
150
|
+
const componentsDir = join(registryPath, "components");
|
|
151
|
+
if (existsSync(componentsDir)) {
|
|
152
|
+
const componentDirs = readdirSync(componentsDir).filter((name) => {
|
|
153
|
+
const path = join(componentsDir, name);
|
|
154
|
+
return statSync(path).isDirectory();
|
|
155
|
+
});
|
|
156
|
+
for (const componentId of componentDirs) {
|
|
157
|
+
const componentDir = join(componentsDir, componentId);
|
|
158
|
+
const componentJsonPath = join(componentDir, "component.json");
|
|
159
|
+
if (existsSync(componentJsonPath)) {
|
|
160
|
+
try {
|
|
161
|
+
const data = JSON.parse(readFileSync(componentJsonPath, "utf-8"));
|
|
162
|
+
const filePath = relative(basePath, componentJsonPath);
|
|
163
|
+
let docs;
|
|
164
|
+
const docsPath = join(componentDir, "docs.md");
|
|
165
|
+
if (existsSync(docsPath)) {
|
|
166
|
+
docs = readFileSync(docsPath, "utf-8");
|
|
167
|
+
}
|
|
168
|
+
components.push({ filePath, data, docs });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const err = error instanceof Error ? error.message : "Unknown error";
|
|
171
|
+
console.error(`Error loading ${componentJsonPath}: ${err}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const scenariosDir = join(componentDir, "scenarios");
|
|
175
|
+
if (existsSync(scenariosDir)) {
|
|
176
|
+
const scenarioFiles = readdirSync(scenariosDir).filter(
|
|
177
|
+
(name) => name.endsWith(".json")
|
|
178
|
+
);
|
|
179
|
+
for (const scenarioFile of scenarioFiles) {
|
|
180
|
+
const scenarioPath = join(scenariosDir, scenarioFile);
|
|
181
|
+
try {
|
|
182
|
+
const data = JSON.parse(readFileSync(scenarioPath, "utf-8"));
|
|
183
|
+
const filePath = relative(basePath, scenarioPath);
|
|
184
|
+
scenarios.push({ filePath, data });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const err = error instanceof Error ? error.message : "Unknown error";
|
|
187
|
+
console.error(`Error loading ${scenarioPath}: ${err}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const examplesDir = join(registryPath, "examples");
|
|
194
|
+
if (existsSync(examplesDir)) {
|
|
195
|
+
const exampleFiles = readdirSync(examplesDir).filter(
|
|
196
|
+
(name) => name.endsWith(".json")
|
|
197
|
+
);
|
|
198
|
+
for (const exampleFile of exampleFiles) {
|
|
199
|
+
const examplePath = join(examplesDir, exampleFile);
|
|
200
|
+
try {
|
|
201
|
+
const data = JSON.parse(readFileSync(examplePath, "utf-8"));
|
|
202
|
+
const filePath = relative(basePath, examplePath);
|
|
203
|
+
examples.push({ filePath, data });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const err = error instanceof Error ? error.message : "Unknown error";
|
|
206
|
+
console.error(`Error loading ${examplePath}: ${err}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { components, scenarios, examples };
|
|
211
|
+
}
|
|
212
|
+
function generateRegistryModule(registryPath) {
|
|
213
|
+
const data = loadRegistryFiles({ registryPath });
|
|
214
|
+
return `
|
|
215
|
+
// Auto-generated registry data
|
|
216
|
+
export const registryData = ${JSON.stringify(data, null, 2)};
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
function getRegistryWatchPaths(registryPath) {
|
|
220
|
+
const paths = [];
|
|
221
|
+
function walkDir(dir) {
|
|
222
|
+
if (!existsSync(dir)) return;
|
|
223
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
const fullPath = join(dir, entry.name);
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
walkDir(fullPath);
|
|
228
|
+
} else if (entry.name.endsWith(".json") || entry.name.endsWith(".md")) {
|
|
229
|
+
paths.push(fullPath);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
walkDir(registryPath);
|
|
234
|
+
return paths;
|
|
235
|
+
}
|
|
236
|
+
const VIRTUAL_REGISTRY_ID = "virtual:catalogue-registry";
|
|
237
|
+
const RESOLVED_VIRTUAL_REGISTRY_ID = "\0" + VIRTUAL_REGISTRY_ID;
|
|
238
|
+
function cataloguePlugin(options) {
|
|
239
|
+
const { config } = options;
|
|
240
|
+
let server;
|
|
241
|
+
return {
|
|
242
|
+
name: "catalogue",
|
|
243
|
+
configResolved(viteConfig) {
|
|
244
|
+
},
|
|
245
|
+
configureServer(_server) {
|
|
246
|
+
server = _server;
|
|
247
|
+
const watchPaths = getRegistryWatchPaths(config.registryPath);
|
|
248
|
+
for (const path of watchPaths) {
|
|
249
|
+
server.watcher.add(path);
|
|
250
|
+
}
|
|
251
|
+
server.watcher.on("change", (file) => {
|
|
252
|
+
if (file.startsWith(config.registryPath)) {
|
|
253
|
+
const module = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_REGISTRY_ID);
|
|
254
|
+
if (module) {
|
|
255
|
+
server.moduleGraph.invalidateModule(module);
|
|
256
|
+
server.ws.send({
|
|
257
|
+
type: "full-reload",
|
|
258
|
+
path: "*"
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
resolveId(id) {
|
|
265
|
+
if (id === VIRTUAL_REGISTRY_ID) {
|
|
266
|
+
return RESOLVED_VIRTUAL_REGISTRY_ID;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
load(id) {
|
|
270
|
+
if (id === RESOLVED_VIRTUAL_REGISTRY_ID) {
|
|
271
|
+
const data = loadRegistryFiles({ registryPath: config.registryPath });
|
|
272
|
+
const registryData = {
|
|
273
|
+
...data,
|
|
274
|
+
categories: config.categories
|
|
275
|
+
};
|
|
276
|
+
return `
|
|
277
|
+
export const registryData = ${JSON.stringify(registryData)};
|
|
278
|
+
export const config = ${JSON.stringify({
|
|
279
|
+
title: config.title,
|
|
280
|
+
basePath: config.basePath
|
|
281
|
+
})};
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function createHtmlTemplate(config) {
|
|
288
|
+
return `<!DOCTYPE html>
|
|
289
|
+
<html lang="en">
|
|
290
|
+
<head>
|
|
291
|
+
<meta charset="UTF-8">
|
|
292
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
293
|
+
<title>${config.title}</title>
|
|
294
|
+
<style>
|
|
295
|
+
* {
|
|
296
|
+
box-sizing: border-box;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
:root {
|
|
300
|
+
--catalogue-bg: #ffffff;
|
|
301
|
+
--catalogue-text: #1a1a1a;
|
|
302
|
+
--catalogue-border: #e5e5e5;
|
|
303
|
+
--catalogue-primary: #3b82f6;
|
|
304
|
+
--catalogue-code-bg: #f5f5f5;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
[data-theme="dark"] {
|
|
308
|
+
--catalogue-bg: #1a1a1a;
|
|
309
|
+
--catalogue-text: #f5f5f5;
|
|
310
|
+
--catalogue-border: #333333;
|
|
311
|
+
--catalogue-primary: #60a5fa;
|
|
312
|
+
--catalogue-code-bg: #2a2a2a;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
body {
|
|
316
|
+
margin: 0;
|
|
317
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
318
|
+
background: var(--catalogue-bg);
|
|
319
|
+
color: var(--catalogue-text);
|
|
320
|
+
line-height: 1.5;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.catalogue-harness-body {
|
|
324
|
+
padding: 0;
|
|
325
|
+
margin: 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#app {
|
|
329
|
+
min-height: 100vh;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Landing page styles */
|
|
333
|
+
.catalogue-landing {
|
|
334
|
+
max-width: 1200px;
|
|
335
|
+
margin: 0 auto;
|
|
336
|
+
padding: 2rem;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.catalogue-landing-header {
|
|
340
|
+
margin-bottom: 2rem;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.catalogue-landing-title {
|
|
344
|
+
margin: 0;
|
|
345
|
+
font-size: 2rem;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.catalogue-landing-controls {
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
gap: 1rem;
|
|
352
|
+
margin-bottom: 2rem;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.catalogue-search-input {
|
|
356
|
+
width: 100%;
|
|
357
|
+
padding: 0.75rem 1rem;
|
|
358
|
+
font-size: 1rem;
|
|
359
|
+
border: 1px solid var(--catalogue-border);
|
|
360
|
+
border-radius: 0.5rem;
|
|
361
|
+
background: var(--catalogue-bg);
|
|
362
|
+
color: var(--catalogue-text);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.catalogue-filters {
|
|
366
|
+
display: flex;
|
|
367
|
+
flex-wrap: wrap;
|
|
368
|
+
gap: 1rem;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.catalogue-filter-group {
|
|
372
|
+
display: flex;
|
|
373
|
+
align-items: center;
|
|
374
|
+
gap: 0.5rem;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.catalogue-filter-label {
|
|
378
|
+
font-weight: 500;
|
|
379
|
+
font-size: 0.875rem;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.catalogue-filter-option {
|
|
383
|
+
display: flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
gap: 0.25rem;
|
|
386
|
+
cursor: pointer;
|
|
387
|
+
font-size: 0.875rem;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.catalogue-component-list {
|
|
391
|
+
display: flex;
|
|
392
|
+
flex-direction: column;
|
|
393
|
+
gap: 1.5rem;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.catalogue-component-list--flat {
|
|
397
|
+
display: grid;
|
|
398
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.catalogue-expand-toggle {
|
|
402
|
+
padding: 0.5rem 1rem;
|
|
403
|
+
font-size: 0.875rem;
|
|
404
|
+
border: 1px solid var(--catalogue-border);
|
|
405
|
+
border-radius: 0.375rem;
|
|
406
|
+
background: var(--catalogue-bg);
|
|
407
|
+
color: var(--catalogue-text);
|
|
408
|
+
cursor: pointer;
|
|
409
|
+
margin-left: auto;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.catalogue-expand-toggle:hover {
|
|
413
|
+
background: var(--catalogue-code-bg);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.catalogue-category-section {
|
|
417
|
+
border: 1px solid var(--catalogue-border);
|
|
418
|
+
border-radius: 0.5rem;
|
|
419
|
+
overflow: hidden;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.catalogue-category-header {
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: center;
|
|
425
|
+
gap: 0.5rem;
|
|
426
|
+
padding: 1rem 1.25rem;
|
|
427
|
+
background: var(--catalogue-code-bg);
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
list-style: none;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.catalogue-category-header::-webkit-details-marker {
|
|
433
|
+
display: none;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.catalogue-category-header::before {
|
|
437
|
+
content: '▶';
|
|
438
|
+
font-size: 0.75rem;
|
|
439
|
+
transition: transform 0.2s;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.catalogue-category-section[open] .catalogue-category-header::before {
|
|
443
|
+
transform: rotate(90deg);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.catalogue-category-label {
|
|
447
|
+
font-weight: 600;
|
|
448
|
+
font-size: 1rem;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.catalogue-category-count {
|
|
452
|
+
font-size: 0.875rem;
|
|
453
|
+
color: var(--catalogue-text);
|
|
454
|
+
opacity: 0.6;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.catalogue-category-content {
|
|
458
|
+
display: grid;
|
|
459
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
460
|
+
gap: 1.5rem;
|
|
461
|
+
padding: 1.5rem;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.catalogue-component-card {
|
|
465
|
+
border: 1px solid var(--catalogue-border);
|
|
466
|
+
border-radius: 0.5rem;
|
|
467
|
+
padding: 1.25rem;
|
|
468
|
+
background: var(--catalogue-bg);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.catalogue-component-card-header {
|
|
472
|
+
display: flex;
|
|
473
|
+
justify-content: space-between;
|
|
474
|
+
align-items: flex-start;
|
|
475
|
+
margin-bottom: 0.5rem;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.catalogue-component-card-title {
|
|
479
|
+
margin: 0;
|
|
480
|
+
font-size: 1.125rem;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.catalogue-component-card-title a {
|
|
484
|
+
color: var(--catalogue-primary);
|
|
485
|
+
text-decoration: none;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.catalogue-component-card-title a:hover {
|
|
489
|
+
text-decoration: underline;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.catalogue-status-badge {
|
|
493
|
+
font-size: 0.75rem;
|
|
494
|
+
padding: 0.125rem 0.5rem;
|
|
495
|
+
border-radius: 9999px;
|
|
496
|
+
text-transform: uppercase;
|
|
497
|
+
font-weight: 500;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.catalogue-status-badge[data-status="stable"] {
|
|
501
|
+
background: #dcfce7;
|
|
502
|
+
color: #166534;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.catalogue-status-badge[data-status="beta"] {
|
|
506
|
+
background: #fef3c7;
|
|
507
|
+
color: #92400e;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.catalogue-status-badge[data-status="deprecated"] {
|
|
511
|
+
background: #fee2e2;
|
|
512
|
+
color: #991b1b;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
[data-theme="dark"] .catalogue-status-badge[data-status="stable"] {
|
|
516
|
+
background: #166534;
|
|
517
|
+
color: #dcfce7;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
[data-theme="dark"] .catalogue-status-badge[data-status="beta"] {
|
|
521
|
+
background: #92400e;
|
|
522
|
+
color: #fef3c7;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
[data-theme="dark"] .catalogue-status-badge[data-status="deprecated"] {
|
|
526
|
+
background: #991b1b;
|
|
527
|
+
color: #fee2e2;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.catalogue-component-card-description {
|
|
531
|
+
margin: 0.5rem 0;
|
|
532
|
+
color: var(--catalogue-text);
|
|
533
|
+
opacity: 0.8;
|
|
534
|
+
font-size: 0.875rem;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.catalogue-component-card-tags {
|
|
538
|
+
display: flex;
|
|
539
|
+
flex-wrap: wrap;
|
|
540
|
+
gap: 0.25rem;
|
|
541
|
+
margin: 0.75rem 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.catalogue-tag {
|
|
545
|
+
font-size: 0.75rem;
|
|
546
|
+
padding: 0.125rem 0.5rem;
|
|
547
|
+
background: var(--catalogue-code-bg);
|
|
548
|
+
border-radius: 0.25rem;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.catalogue-component-card-meta {
|
|
552
|
+
font-size: 0.75rem;
|
|
553
|
+
color: var(--catalogue-text);
|
|
554
|
+
opacity: 0.6;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/* Breadcrumb */
|
|
558
|
+
.catalogue-breadcrumb {
|
|
559
|
+
display: flex;
|
|
560
|
+
align-items: center;
|
|
561
|
+
gap: 0.5rem;
|
|
562
|
+
margin-bottom: 1rem;
|
|
563
|
+
font-size: 0.875rem;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.catalogue-breadcrumb a {
|
|
567
|
+
color: var(--catalogue-primary);
|
|
568
|
+
text-decoration: none;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.catalogue-breadcrumb a:hover {
|
|
572
|
+
text-decoration: underline;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.catalogue-breadcrumb-separator {
|
|
576
|
+
color: var(--catalogue-text);
|
|
577
|
+
opacity: 0.5;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* Component page */
|
|
581
|
+
.catalogue-component-page {
|
|
582
|
+
max-width: 1200px;
|
|
583
|
+
margin: 0 auto;
|
|
584
|
+
padding: 2rem;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.catalogue-component-header {
|
|
588
|
+
margin-bottom: 2rem;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.catalogue-component-title-row {
|
|
592
|
+
display: flex;
|
|
593
|
+
align-items: center;
|
|
594
|
+
gap: 1rem;
|
|
595
|
+
margin-bottom: 0.5rem;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.catalogue-component-title {
|
|
599
|
+
margin: 0;
|
|
600
|
+
font-size: 1.75rem;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.catalogue-component-description {
|
|
604
|
+
margin: 0.5rem 0;
|
|
605
|
+
font-size: 1rem;
|
|
606
|
+
opacity: 0.8;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.catalogue-component-tags {
|
|
610
|
+
display: flex;
|
|
611
|
+
flex-wrap: wrap;
|
|
612
|
+
gap: 0.5rem;
|
|
613
|
+
margin: 1rem 0;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.catalogue-component-actions {
|
|
617
|
+
margin-top: 1rem;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.catalogue-button {
|
|
621
|
+
display: inline-block;
|
|
622
|
+
padding: 0.5rem 1rem;
|
|
623
|
+
border-radius: 0.375rem;
|
|
624
|
+
text-decoration: none;
|
|
625
|
+
font-size: 0.875rem;
|
|
626
|
+
font-weight: 500;
|
|
627
|
+
cursor: pointer;
|
|
628
|
+
border: none;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.catalogue-button-primary {
|
|
632
|
+
background: var(--catalogue-primary);
|
|
633
|
+
color: white;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.catalogue-button-primary:hover {
|
|
637
|
+
opacity: 0.9;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/* Variants grid */
|
|
641
|
+
.catalogue-variants-section {
|
|
642
|
+
margin-top: 2rem;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.catalogue-variants-section h2 {
|
|
646
|
+
font-size: 1.25rem;
|
|
647
|
+
margin-bottom: 1rem;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.catalogue-variants-grid {
|
|
651
|
+
display: grid;
|
|
652
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
653
|
+
gap: 1.5rem;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.catalogue-scenario-card {
|
|
657
|
+
border: 1px solid var(--catalogue-border);
|
|
658
|
+
border-radius: 0.5rem;
|
|
659
|
+
overflow: hidden;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.catalogue-scenario-card-primary {
|
|
663
|
+
border-color: var(--catalogue-primary);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.catalogue-scenario-preview {
|
|
667
|
+
padding: 1.5rem;
|
|
668
|
+
background: var(--catalogue-bg);
|
|
669
|
+
min-height: 100px;
|
|
670
|
+
display: flex;
|
|
671
|
+
align-items: center;
|
|
672
|
+
justify-content: center;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.catalogue-scenario-info {
|
|
676
|
+
padding: 1rem;
|
|
677
|
+
border-top: 1px solid var(--catalogue-border);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.catalogue-scenario-title-row {
|
|
681
|
+
display: flex;
|
|
682
|
+
align-items: center;
|
|
683
|
+
gap: 0.5rem;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.catalogue-scenario-title {
|
|
687
|
+
margin: 0;
|
|
688
|
+
font-size: 0.875rem;
|
|
689
|
+
font-weight: 500;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.catalogue-primary-badge {
|
|
693
|
+
font-size: 0.625rem;
|
|
694
|
+
padding: 0.125rem 0.375rem;
|
|
695
|
+
background: var(--catalogue-primary);
|
|
696
|
+
color: white;
|
|
697
|
+
border-radius: 0.25rem;
|
|
698
|
+
text-transform: uppercase;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.catalogue-scenario-description {
|
|
702
|
+
margin: 0.5rem 0;
|
|
703
|
+
font-size: 0.75rem;
|
|
704
|
+
opacity: 0.7;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.catalogue-scenario-links {
|
|
708
|
+
margin-top: 0.5rem;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.catalogue-link {
|
|
712
|
+
font-size: 0.75rem;
|
|
713
|
+
color: var(--catalogue-primary);
|
|
714
|
+
text-decoration: none;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.catalogue-link:hover {
|
|
718
|
+
text-decoration: underline;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/* Docs section */
|
|
722
|
+
.catalogue-docs-section {
|
|
723
|
+
margin-top: 2rem;
|
|
724
|
+
padding-top: 2rem;
|
|
725
|
+
border-top: 1px solid var(--catalogue-border);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.catalogue-docs-section h2 {
|
|
729
|
+
font-size: 1.25rem;
|
|
730
|
+
margin-bottom: 1rem;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.catalogue-docs-content {
|
|
734
|
+
line-height: 1.7;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.catalogue-docs-content h1,
|
|
738
|
+
.catalogue-docs-content h2,
|
|
739
|
+
.catalogue-docs-content h3 {
|
|
740
|
+
margin-top: 1.5rem;
|
|
741
|
+
margin-bottom: 0.5rem;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.catalogue-docs-content pre {
|
|
745
|
+
background: var(--catalogue-code-bg);
|
|
746
|
+
padding: 1rem;
|
|
747
|
+
border-radius: 0.375rem;
|
|
748
|
+
overflow-x: auto;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.catalogue-docs-content code {
|
|
752
|
+
font-family: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
|
|
753
|
+
font-size: 0.875em;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/* Playground */
|
|
757
|
+
.catalogue-playground {
|
|
758
|
+
min-height: 100vh;
|
|
759
|
+
display: flex;
|
|
760
|
+
flex-direction: column;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.catalogue-playground-header {
|
|
764
|
+
padding: 1rem 2rem;
|
|
765
|
+
border-bottom: 1px solid var(--catalogue-border);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.catalogue-playground-title-row {
|
|
769
|
+
display: flex;
|
|
770
|
+
justify-content: space-between;
|
|
771
|
+
align-items: center;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.catalogue-playground-title-row h1 {
|
|
775
|
+
font-size: 1.25rem;
|
|
776
|
+
margin: 0;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.catalogue-playground-main {
|
|
780
|
+
display: flex;
|
|
781
|
+
flex: 1;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.catalogue-playground-preview-section {
|
|
785
|
+
flex: 1;
|
|
786
|
+
padding: 1rem;
|
|
787
|
+
display: flex;
|
|
788
|
+
flex-direction: column;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.catalogue-scenario-selector {
|
|
792
|
+
margin-bottom: 1rem;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.catalogue-scenario-select {
|
|
796
|
+
padding: 0.375rem 0.75rem;
|
|
797
|
+
border: 1px solid var(--catalogue-border);
|
|
798
|
+
border-radius: 0.25rem;
|
|
799
|
+
background: var(--catalogue-bg);
|
|
800
|
+
color: var(--catalogue-text);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.catalogue-playground-sidebar {
|
|
804
|
+
width: 350px;
|
|
805
|
+
border-left: 1px solid var(--catalogue-border);
|
|
806
|
+
display: flex;
|
|
807
|
+
flex-direction: column;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.catalogue-playground-controls-section {
|
|
811
|
+
padding: 1rem;
|
|
812
|
+
border-bottom: 1px solid var(--catalogue-border);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.catalogue-playground-controls-section h2 {
|
|
816
|
+
font-size: 0.875rem;
|
|
817
|
+
margin: 0 0 1rem 0;
|
|
818
|
+
text-transform: uppercase;
|
|
819
|
+
letter-spacing: 0.05em;
|
|
820
|
+
opacity: 0.7;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.catalogue-playground-code-section {
|
|
824
|
+
flex: 1;
|
|
825
|
+
overflow: auto;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/* Controls */
|
|
829
|
+
.catalogue-controls {
|
|
830
|
+
display: flex;
|
|
831
|
+
flex-direction: column;
|
|
832
|
+
gap: 1rem;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.catalogue-control {
|
|
836
|
+
display: flex;
|
|
837
|
+
flex-direction: column;
|
|
838
|
+
gap: 0.25rem;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.catalogue-control-label {
|
|
842
|
+
font-size: 0.75rem;
|
|
843
|
+
font-weight: 500;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.catalogue-control-input,
|
|
847
|
+
.catalogue-control-select {
|
|
848
|
+
padding: 0.5rem;
|
|
849
|
+
border: 1px solid var(--catalogue-border);
|
|
850
|
+
border-radius: 0.25rem;
|
|
851
|
+
background: var(--catalogue-bg);
|
|
852
|
+
color: var(--catalogue-text);
|
|
853
|
+
font-size: 0.875rem;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.catalogue-control-checkbox {
|
|
857
|
+
display: flex;
|
|
858
|
+
align-items: center;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.catalogue-control-radio-group {
|
|
862
|
+
display: flex;
|
|
863
|
+
flex-direction: column;
|
|
864
|
+
gap: 0.25rem;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.catalogue-control-radio {
|
|
868
|
+
display: flex;
|
|
869
|
+
align-items: center;
|
|
870
|
+
gap: 0.5rem;
|
|
871
|
+
font-size: 0.875rem;
|
|
872
|
+
cursor: pointer;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.catalogue-control-range-wrapper {
|
|
876
|
+
display: flex;
|
|
877
|
+
align-items: center;
|
|
878
|
+
gap: 0.5rem;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.catalogue-control-range {
|
|
882
|
+
flex: 1;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.catalogue-control-range-value {
|
|
886
|
+
font-size: 0.75rem;
|
|
887
|
+
min-width: 3ch;
|
|
888
|
+
text-align: right;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.catalogue-control-json {
|
|
892
|
+
width: 100%;
|
|
893
|
+
padding: 0.5rem;
|
|
894
|
+
border: 1px solid var(--catalogue-border);
|
|
895
|
+
border-radius: 0.25rem;
|
|
896
|
+
background: var(--catalogue-bg);
|
|
897
|
+
color: var(--catalogue-text);
|
|
898
|
+
font-family: ui-monospace, monospace;
|
|
899
|
+
font-size: 0.75rem;
|
|
900
|
+
resize: vertical;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.catalogue-control-json.invalid {
|
|
904
|
+
border-color: #ef4444;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/* Resizer */
|
|
908
|
+
.catalogue-resizer-wrapper {
|
|
909
|
+
flex: 1;
|
|
910
|
+
display: flex;
|
|
911
|
+
justify-content: center;
|
|
912
|
+
padding: 1rem;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
.catalogue-resizer {
|
|
916
|
+
position: relative;
|
|
917
|
+
border: 1px solid var(--catalogue-border);
|
|
918
|
+
border-radius: 0.25rem;
|
|
919
|
+
background: var(--catalogue-bg);
|
|
920
|
+
overflow: hidden;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.catalogue-resizer-content {
|
|
924
|
+
width: 100%;
|
|
925
|
+
height: 100%;
|
|
926
|
+
overflow: auto;
|
|
927
|
+
padding: 1rem;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.catalogue-resizer-handle {
|
|
931
|
+
position: absolute;
|
|
932
|
+
background: transparent;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.catalogue-resizer-handle-right {
|
|
936
|
+
right: 0;
|
|
937
|
+
top: 0;
|
|
938
|
+
bottom: 0;
|
|
939
|
+
width: 8px;
|
|
940
|
+
cursor: ew-resize;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.catalogue-resizer-handle-bottom {
|
|
944
|
+
bottom: 0;
|
|
945
|
+
left: 0;
|
|
946
|
+
right: 0;
|
|
947
|
+
height: 8px;
|
|
948
|
+
cursor: ns-resize;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.catalogue-resizer-handle-corner {
|
|
952
|
+
right: 0;
|
|
953
|
+
bottom: 0;
|
|
954
|
+
width: 16px;
|
|
955
|
+
height: 16px;
|
|
956
|
+
cursor: nwse-resize;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.catalogue-resizer-dimensions {
|
|
960
|
+
position: absolute;
|
|
961
|
+
bottom: 4px;
|
|
962
|
+
right: 20px;
|
|
963
|
+
font-size: 0.625rem;
|
|
964
|
+
color: var(--catalogue-text);
|
|
965
|
+
opacity: 0.5;
|
|
966
|
+
font-family: ui-monospace, monospace;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.catalogue-breakpoint-presets {
|
|
970
|
+
display: flex;
|
|
971
|
+
flex-wrap: wrap;
|
|
972
|
+
gap: 0.5rem;
|
|
973
|
+
margin-bottom: 1rem;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.catalogue-breakpoint-button {
|
|
977
|
+
padding: 0.25rem 0.5rem;
|
|
978
|
+
font-size: 0.75rem;
|
|
979
|
+
border: 1px solid var(--catalogue-border);
|
|
980
|
+
border-radius: 0.25rem;
|
|
981
|
+
background: var(--catalogue-bg);
|
|
982
|
+
color: var(--catalogue-text);
|
|
983
|
+
cursor: pointer;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.catalogue-breakpoint-button:hover {
|
|
987
|
+
background: var(--catalogue-code-bg);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/* Code panel */
|
|
991
|
+
.catalogue-code-panel {
|
|
992
|
+
border-top: 1px solid var(--catalogue-border);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
.catalogue-code-panel-header {
|
|
996
|
+
display: flex;
|
|
997
|
+
justify-content: space-between;
|
|
998
|
+
align-items: center;
|
|
999
|
+
padding: 0.75rem 1rem;
|
|
1000
|
+
border-bottom: 1px solid var(--catalogue-border);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.catalogue-code-panel-title {
|
|
1004
|
+
font-size: 0.75rem;
|
|
1005
|
+
font-weight: 500;
|
|
1006
|
+
text-transform: uppercase;
|
|
1007
|
+
letter-spacing: 0.05em;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.catalogue-code-panel-copy {
|
|
1011
|
+
padding: 0.25rem 0.5rem;
|
|
1012
|
+
font-size: 0.75rem;
|
|
1013
|
+
border: 1px solid var(--catalogue-border);
|
|
1014
|
+
border-radius: 0.25rem;
|
|
1015
|
+
background: var(--catalogue-bg);
|
|
1016
|
+
color: var(--catalogue-text);
|
|
1017
|
+
cursor: pointer;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.catalogue-code-panel-copy.copied {
|
|
1021
|
+
background: #22c55e;
|
|
1022
|
+
color: white;
|
|
1023
|
+
border-color: #22c55e;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.catalogue-code-panel-content {
|
|
1027
|
+
margin: 0;
|
|
1028
|
+
padding: 1rem;
|
|
1029
|
+
background: var(--catalogue-code-bg);
|
|
1030
|
+
overflow-x: auto;
|
|
1031
|
+
font-size: 0.75rem;
|
|
1032
|
+
line-height: 1.5;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
.catalogue-code-panel-content code {
|
|
1036
|
+
font-family: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
.catalogue-code-panel[data-collapsed] .catalogue-code-panel-content {
|
|
1040
|
+
display: none;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/* Theme toggle */
|
|
1044
|
+
.catalogue-theme-toggle {
|
|
1045
|
+
padding: 0.5rem;
|
|
1046
|
+
border: 1px solid var(--catalogue-border);
|
|
1047
|
+
border-radius: 0.375rem;
|
|
1048
|
+
background: var(--catalogue-bg);
|
|
1049
|
+
color: var(--catalogue-text);
|
|
1050
|
+
cursor: pointer;
|
|
1051
|
+
display: flex;
|
|
1052
|
+
align-items: center;
|
|
1053
|
+
justify-content: center;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
.catalogue-theme-toggle:hover {
|
|
1057
|
+
background: var(--catalogue-code-bg);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/* Harness */
|
|
1061
|
+
.catalogue-harness {
|
|
1062
|
+
min-height: 100vh;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.catalogue-harness-container {
|
|
1066
|
+
display: inline-block;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/* Global header */
|
|
1070
|
+
.catalogue-global-header {
|
|
1071
|
+
display: flex;
|
|
1072
|
+
justify-content: space-between;
|
|
1073
|
+
align-items: center;
|
|
1074
|
+
padding: 0.75rem 2rem;
|
|
1075
|
+
border-bottom: 1px solid var(--catalogue-border);
|
|
1076
|
+
background: var(--catalogue-bg);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.catalogue-home-link {
|
|
1080
|
+
font-weight: 600;
|
|
1081
|
+
color: var(--catalogue-text);
|
|
1082
|
+
text-decoration: none;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.catalogue-home-link:hover {
|
|
1086
|
+
color: var(--catalogue-primary);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/* Not found */
|
|
1090
|
+
.catalogue-not-found {
|
|
1091
|
+
max-width: 600px;
|
|
1092
|
+
margin: 4rem auto;
|
|
1093
|
+
text-align: center;
|
|
1094
|
+
padding: 2rem;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.catalogue-not-found h1 {
|
|
1098
|
+
font-size: 1.5rem;
|
|
1099
|
+
margin-bottom: 0.5rem;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.catalogue-not-found a {
|
|
1103
|
+
color: var(--catalogue-primary);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/* Empty state */
|
|
1107
|
+
.catalogue-empty-state {
|
|
1108
|
+
grid-column: 1 / -1;
|
|
1109
|
+
text-align: center;
|
|
1110
|
+
padding: 3rem;
|
|
1111
|
+
color: var(--catalogue-text);
|
|
1112
|
+
opacity: 0.6;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/* Subcomponents */
|
|
1116
|
+
.catalogue-subcomponents-section {
|
|
1117
|
+
margin-top: 2rem;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.catalogue-subcomponents-section h2 {
|
|
1121
|
+
font-size: 1.25rem;
|
|
1122
|
+
margin-bottom: 1rem;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
.catalogue-subcomponents-list {
|
|
1126
|
+
display: flex;
|
|
1127
|
+
flex-wrap: wrap;
|
|
1128
|
+
gap: 1rem;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.catalogue-subcomponent-card {
|
|
1132
|
+
padding: 1rem;
|
|
1133
|
+
border: 1px solid var(--catalogue-border);
|
|
1134
|
+
border-radius: 0.375rem;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.catalogue-subcomponent-card a {
|
|
1138
|
+
color: var(--catalogue-primary);
|
|
1139
|
+
text-decoration: none;
|
|
1140
|
+
font-weight: 500;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.catalogue-subcomponent-card p {
|
|
1144
|
+
margin: 0.5rem 0 0;
|
|
1145
|
+
font-size: 0.875rem;
|
|
1146
|
+
opacity: 0.7;
|
|
1147
|
+
}
|
|
1148
|
+
</style>
|
|
1149
|
+
</head>
|
|
1150
|
+
<body>
|
|
1151
|
+
<div id="app"></div>
|
|
1152
|
+
<script type="module" src="/@catalogue/entry.ts"><\/script>
|
|
1153
|
+
</body>
|
|
1154
|
+
</html>`;
|
|
1155
|
+
}
|
|
1156
|
+
const entryTemplate = `
|
|
1157
|
+
import { loadRegistry, mountCatalogueApp } from '@adieyal/catalogue-core';
|
|
1158
|
+
import { registryData, config } from 'virtual:catalogue-registry';
|
|
1159
|
+
|
|
1160
|
+
// Import consumer's components
|
|
1161
|
+
import '{{COMPONENTS_ENTRY}}';
|
|
1162
|
+
|
|
1163
|
+
// Load and validate the registry
|
|
1164
|
+
const { registry, errors } = loadRegistry(registryData);
|
|
1165
|
+
|
|
1166
|
+
if (errors.length > 0) {
|
|
1167
|
+
console.error('Registry validation errors:', errors);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Mount the app
|
|
1171
|
+
const app = mountCatalogueApp('#app', registry, {
|
|
1172
|
+
basePath: config.basePath,
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
// Hot module replacement
|
|
1176
|
+
if (import.meta.hot) {
|
|
1177
|
+
import.meta.hot.accept('virtual:catalogue-registry', (newModule) => {
|
|
1178
|
+
if (newModule) {
|
|
1179
|
+
window.location.reload();
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
`;
|
|
1184
|
+
const VIRTUAL_ENTRY_ID = "@catalogue/entry.ts";
|
|
1185
|
+
const RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID;
|
|
1186
|
+
const VIRTUAL_ENTRY_PATH = "/@catalogue/entry.ts";
|
|
1187
|
+
function createViteConfig(config, mode) {
|
|
1188
|
+
const componentsEntry = config.components?.entry ? resolve(config.configDir, config.components.entry) : resolve(config.configDir, "./src/components/index.ts");
|
|
1189
|
+
return {
|
|
1190
|
+
root: config.configDir,
|
|
1191
|
+
base: config.basePath,
|
|
1192
|
+
mode,
|
|
1193
|
+
define: {
|
|
1194
|
+
"process.env.NODE_ENV": JSON.stringify(mode)
|
|
1195
|
+
},
|
|
1196
|
+
plugins: [
|
|
1197
|
+
cataloguePlugin({ config }),
|
|
1198
|
+
{
|
|
1199
|
+
name: "catalogue-entry",
|
|
1200
|
+
enforce: "pre",
|
|
1201
|
+
resolveId(id) {
|
|
1202
|
+
if (id === VIRTUAL_ENTRY_ID || id === VIRTUAL_ENTRY_PATH || id.endsWith("@catalogue/entry.ts")) {
|
|
1203
|
+
return RESOLVED_VIRTUAL_ENTRY_ID;
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
load(id) {
|
|
1207
|
+
if (id === RESOLVED_VIRTUAL_ENTRY_ID) {
|
|
1208
|
+
return entryTemplate.replace("{{COMPONENTS_ENTRY}}", componentsEntry);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
name: "catalogue-html",
|
|
1214
|
+
configureServer(server) {
|
|
1215
|
+
server.middlewares.use((req, res, next) => {
|
|
1216
|
+
if (req.url === "/" || req.url?.startsWith("/?")) {
|
|
1217
|
+
res.setHeader("Content-Type", "text/html");
|
|
1218
|
+
server.transformIndexHtml("/", createHtmlTemplate(config)).then((html) => {
|
|
1219
|
+
res.end(html);
|
|
1220
|
+
});
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
next();
|
|
1224
|
+
});
|
|
1225
|
+
},
|
|
1226
|
+
// Only transform HTML in dev mode - production uses the written index.html
|
|
1227
|
+
transformIndexHtml: {
|
|
1228
|
+
order: "pre",
|
|
1229
|
+
handler(html, ctx) {
|
|
1230
|
+
if (ctx.server) {
|
|
1231
|
+
return createHtmlTemplate(config);
|
|
1232
|
+
}
|
|
1233
|
+
return html;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
],
|
|
1238
|
+
server: {
|
|
1239
|
+
port: config.port
|
|
1240
|
+
},
|
|
1241
|
+
build: {
|
|
1242
|
+
outDir: config.outPath,
|
|
1243
|
+
emptyOutDir: true
|
|
1244
|
+
},
|
|
1245
|
+
...config.vite || {}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
async function startDevServer(config) {
|
|
1249
|
+
const viteConfig = createViteConfig(config, "development");
|
|
1250
|
+
const server = await createServer(viteConfig);
|
|
1251
|
+
await server.listen();
|
|
1252
|
+
server.printUrls();
|
|
1253
|
+
}
|
|
1254
|
+
async function buildStatic(config) {
|
|
1255
|
+
const viteConfig = createViteConfig(config, "production");
|
|
1256
|
+
const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync2, existsSync: existsSync2 } = await import("node:fs");
|
|
1257
|
+
const { join: join2 } = await import("node:path");
|
|
1258
|
+
if (!existsSync2(config.configDir)) {
|
|
1259
|
+
mkdirSync2(config.configDir, { recursive: true });
|
|
1260
|
+
}
|
|
1261
|
+
const componentsEntry = config.components?.entry ? resolve(config.configDir, config.components.entry) : resolve(config.configDir, "./src/components/index.ts");
|
|
1262
|
+
const entryPath = join2(config.configDir, ".catalogue-entry.ts");
|
|
1263
|
+
writeFileSync2(entryPath, entryTemplate.replace("{{COMPONENTS_ENTRY}}", componentsEntry));
|
|
1264
|
+
const indexPath = join2(config.configDir, "index.html");
|
|
1265
|
+
writeFileSync2(indexPath, createHtmlTemplate(config).replace(
|
|
1266
|
+
"/@catalogue/entry.ts",
|
|
1267
|
+
"./.catalogue-entry.ts"
|
|
1268
|
+
));
|
|
1269
|
+
try {
|
|
1270
|
+
await build$1(viteConfig);
|
|
1271
|
+
} finally {
|
|
1272
|
+
const { unlinkSync } = await import("node:fs");
|
|
1273
|
+
try {
|
|
1274
|
+
unlinkSync(indexPath);
|
|
1275
|
+
unlinkSync(entryPath);
|
|
1276
|
+
} catch {
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async function startPreviewServer(config) {
|
|
1281
|
+
const viteConfig = createViteConfig(config, "production");
|
|
1282
|
+
const server = await preview$1({
|
|
1283
|
+
...viteConfig,
|
|
1284
|
+
preview: {
|
|
1285
|
+
port: config.port
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
server.printUrls();
|
|
1289
|
+
}
|
|
1290
|
+
async function dev(options = {}) {
|
|
1291
|
+
console.log(pc.cyan("Starting development server..."));
|
|
1292
|
+
try {
|
|
1293
|
+
const config = await loadConfig({
|
|
1294
|
+
configFile: options.config
|
|
1295
|
+
});
|
|
1296
|
+
if (options.port) {
|
|
1297
|
+
config.port = options.port;
|
|
1298
|
+
}
|
|
1299
|
+
await startDevServer(config);
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
console.error(pc.red("Error starting dev server:"));
|
|
1302
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async function build(options = {}) {
|
|
1307
|
+
console.log(pc.cyan("Building catalogue..."));
|
|
1308
|
+
try {
|
|
1309
|
+
const config = await loadConfig({
|
|
1310
|
+
configFile: options.config
|
|
1311
|
+
});
|
|
1312
|
+
if (options.outDir) {
|
|
1313
|
+
config.outPath = options.outDir;
|
|
1314
|
+
}
|
|
1315
|
+
await buildStatic(config);
|
|
1316
|
+
console.log(pc.green(`✓ Built to ${config.outPath}`));
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
console.error(pc.red("Error building catalogue:"));
|
|
1319
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1320
|
+
process.exit(1);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
async function preview(options = {}) {
|
|
1324
|
+
console.log(pc.cyan("Starting preview server..."));
|
|
1325
|
+
try {
|
|
1326
|
+
const config = await loadConfig({
|
|
1327
|
+
configFile: options.config
|
|
1328
|
+
});
|
|
1329
|
+
if (options.port) {
|
|
1330
|
+
config.port = options.port;
|
|
1331
|
+
}
|
|
1332
|
+
await startPreviewServer(config);
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
console.error(pc.red("Error starting preview server:"));
|
|
1335
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1336
|
+
process.exit(1);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const ue = z.object({
|
|
1340
|
+
/** Element tag name (e.g., 'div', 'my-component') */
|
|
1341
|
+
element: z.string().min(1),
|
|
1342
|
+
/** DOM attributes (set via setAttribute) */
|
|
1343
|
+
attrs: z.record(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
|
|
1344
|
+
/** DOM properties (set directly on element, for objects/arrays) */
|
|
1345
|
+
props: z.record(z.unknown()).optional(),
|
|
1346
|
+
/** Text content (mutually exclusive with children) */
|
|
1347
|
+
text: z.string().optional()
|
|
1348
|
+
}), R = ue.extend({
|
|
1349
|
+
/** Named slots - content to render with slot attribute */
|
|
1350
|
+
slots: z.record(z.union([
|
|
1351
|
+
z.string(),
|
|
1352
|
+
z.lazy(() => R),
|
|
1353
|
+
z.array(z.lazy(() => R))
|
|
1354
|
+
])).optional(),
|
|
1355
|
+
/** Child elements */
|
|
1356
|
+
children: z.array(z.lazy(() => R)).optional()
|
|
1357
|
+
});
|
|
1358
|
+
const pe = z.object({
|
|
1359
|
+
type: z.literal("text"),
|
|
1360
|
+
label: z.string().optional(),
|
|
1361
|
+
defaultValue: z.string().optional(),
|
|
1362
|
+
placeholder: z.string().optional()
|
|
1363
|
+
}), me = z.object({
|
|
1364
|
+
type: z.literal("number"),
|
|
1365
|
+
label: z.string().optional(),
|
|
1366
|
+
defaultValue: z.number().optional(),
|
|
1367
|
+
min: z.number().optional(),
|
|
1368
|
+
max: z.number().optional(),
|
|
1369
|
+
step: z.number().optional()
|
|
1370
|
+
}), he = z.object({
|
|
1371
|
+
type: z.literal("boolean"),
|
|
1372
|
+
label: z.string().optional(),
|
|
1373
|
+
defaultValue: z.boolean().optional()
|
|
1374
|
+
}), ge = z.object({
|
|
1375
|
+
type: z.literal("select"),
|
|
1376
|
+
label: z.string().optional(),
|
|
1377
|
+
defaultValue: z.string().optional(),
|
|
1378
|
+
options: z.array(z.union([
|
|
1379
|
+
z.string(),
|
|
1380
|
+
z.object({
|
|
1381
|
+
label: z.string(),
|
|
1382
|
+
value: z.string()
|
|
1383
|
+
})
|
|
1384
|
+
]))
|
|
1385
|
+
}), fe = z.object({
|
|
1386
|
+
type: z.literal("radio"),
|
|
1387
|
+
label: z.string().optional(),
|
|
1388
|
+
defaultValue: z.string().optional(),
|
|
1389
|
+
options: z.array(z.union([
|
|
1390
|
+
z.string(),
|
|
1391
|
+
z.object({
|
|
1392
|
+
label: z.string(),
|
|
1393
|
+
value: z.string()
|
|
1394
|
+
})
|
|
1395
|
+
]))
|
|
1396
|
+
}), Ce = z.object({
|
|
1397
|
+
type: z.literal("color"),
|
|
1398
|
+
label: z.string().optional(),
|
|
1399
|
+
defaultValue: z.string().optional()
|
|
1400
|
+
}), be = z.object({
|
|
1401
|
+
type: z.literal("range"),
|
|
1402
|
+
label: z.string().optional(),
|
|
1403
|
+
defaultValue: z.number().optional(),
|
|
1404
|
+
min: z.number(),
|
|
1405
|
+
max: z.number(),
|
|
1406
|
+
step: z.number().optional()
|
|
1407
|
+
}), ve = z.object({
|
|
1408
|
+
type: z.literal("json"),
|
|
1409
|
+
label: z.string().optional(),
|
|
1410
|
+
defaultValue: z.unknown().optional()
|
|
1411
|
+
}), Z = z.discriminatedUnion("type", [
|
|
1412
|
+
pe,
|
|
1413
|
+
me,
|
|
1414
|
+
he,
|
|
1415
|
+
ge,
|
|
1416
|
+
fe,
|
|
1417
|
+
Ce,
|
|
1418
|
+
be,
|
|
1419
|
+
ve
|
|
1420
|
+
]);
|
|
1421
|
+
const ye = z.enum(["stable", "beta", "deprecated"]), Ee = z.enum(["standalone", "subcomponent", "feature"]), xe = z.object({
|
|
1422
|
+
/** Custom element tag name (must be kebab-case with at least one hyphen) */
|
|
1423
|
+
customElement: z.string().regex(/^[a-z][a-z0-9]*-[a-z0-9-]+$/, {
|
|
1424
|
+
message: 'Custom element name must be kebab-case with at least one hyphen (e.g., "my-button")'
|
|
1425
|
+
}),
|
|
1426
|
+
/** npm package name (optional, defaults to consumer's package) */
|
|
1427
|
+
package: z.string().optional(),
|
|
1428
|
+
/** Entry point path (optional, defaults to main entry) */
|
|
1429
|
+
entry: z.string().optional()
|
|
1430
|
+
}), we = z.object({
|
|
1431
|
+
/** ID of the primary scenario to use as default in playground */
|
|
1432
|
+
primaryScenarioId: z.string().min(1),
|
|
1433
|
+
/** Control definitions keyed by property name */
|
|
1434
|
+
controls: z.record(Z)
|
|
1435
|
+
}), Ne = z.object({
|
|
1436
|
+
/** Unique identifier for the component (kebab-case) */
|
|
1437
|
+
id: z.string().min(1).regex(/^[a-z][a-z0-9-]*$/, {
|
|
1438
|
+
message: "Component ID must be kebab-case starting with a letter"
|
|
1439
|
+
}),
|
|
1440
|
+
/** Human-readable title */
|
|
1441
|
+
title: z.string().min(1),
|
|
1442
|
+
/** Component maturity status */
|
|
1443
|
+
status: ye,
|
|
1444
|
+
/** Component kind */
|
|
1445
|
+
kind: Ee,
|
|
1446
|
+
/** Optional description */
|
|
1447
|
+
description: z.string().optional(),
|
|
1448
|
+
/** Tags for search and filtering */
|
|
1449
|
+
tags: z.array(z.string()).optional(),
|
|
1450
|
+
/** Category ID (must match a category defined in config) */
|
|
1451
|
+
category: z.string().regex(/^[a-z][a-z0-9-]*$/, {
|
|
1452
|
+
message: "Category must be kebab-case starting with a letter"
|
|
1453
|
+
}).optional(),
|
|
1454
|
+
/** Export configuration */
|
|
1455
|
+
exports: xe,
|
|
1456
|
+
/** Playground configuration */
|
|
1457
|
+
playground: we,
|
|
1458
|
+
/** Parent component ID (required when kind is 'subcomponent') */
|
|
1459
|
+
parentId: z.string().optional(),
|
|
1460
|
+
/** List of subcomponent IDs */
|
|
1461
|
+
subcomponents: z.array(z.string()).optional()
|
|
1462
|
+
}).refine(
|
|
1463
|
+
(t) => !(t.kind === "subcomponent" && !t.parentId),
|
|
1464
|
+
{
|
|
1465
|
+
message: "Subcomponents must have a parentId",
|
|
1466
|
+
path: ["parentId"]
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
1469
|
+
function $e(t) {
|
|
1470
|
+
const e = Ne.safeParse(t);
|
|
1471
|
+
return e.success ? { success: true, data: e.data } : {
|
|
1472
|
+
success: false,
|
|
1473
|
+
errors: e.error.errors.map((a) => ({
|
|
1474
|
+
path: a.path.join(".") || "(root)",
|
|
1475
|
+
message: a.message
|
|
1476
|
+
}))
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
const ee = z.object({
|
|
1480
|
+
/** Unique identifier for the scenario (kebab-case) */
|
|
1481
|
+
id: z.string().min(1).regex(/^[a-z][a-z0-9-]*$/, {
|
|
1482
|
+
message: "Scenario ID must be kebab-case starting with a letter"
|
|
1483
|
+
}),
|
|
1484
|
+
/** Human-readable title */
|
|
1485
|
+
title: z.string().min(1),
|
|
1486
|
+
/** Optional description explaining the scenario */
|
|
1487
|
+
description: z.string().optional(),
|
|
1488
|
+
/** Component ID this scenario belongs to */
|
|
1489
|
+
componentId: z.string().min(1),
|
|
1490
|
+
/** Tags for search and filtering */
|
|
1491
|
+
tags: z.array(z.string()).optional(),
|
|
1492
|
+
/** The RenderNode tree that defines the DOM structure */
|
|
1493
|
+
render: R,
|
|
1494
|
+
/** Optional viewport configuration for screenshots */
|
|
1495
|
+
viewport: z.object({
|
|
1496
|
+
width: z.number().positive().optional(),
|
|
1497
|
+
height: z.number().positive().optional()
|
|
1498
|
+
}).optional(),
|
|
1499
|
+
/** Optional background color override */
|
|
1500
|
+
background: z.string().optional(),
|
|
1501
|
+
/** Whether this is the primary scenario (shown first) */
|
|
1502
|
+
primary: z.boolean().optional()
|
|
1503
|
+
});
|
|
1504
|
+
function te(t) {
|
|
1505
|
+
const e = ee.safeParse(t);
|
|
1506
|
+
return e.success ? { success: true, data: e.data } : {
|
|
1507
|
+
success: false,
|
|
1508
|
+
errors: e.error.errors.map((a) => ({
|
|
1509
|
+
path: a.path.join(".") || "(root)",
|
|
1510
|
+
message: a.message
|
|
1511
|
+
}))
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
function Se(t) {
|
|
1515
|
+
return te(t);
|
|
1516
|
+
}
|
|
1517
|
+
function ft(t) {
|
|
1518
|
+
const e = [], a = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
|
|
1519
|
+
for (const { filePath: s, data: i, docs: c } of t.components) {
|
|
1520
|
+
const d = $e(i);
|
|
1521
|
+
if (d.success) {
|
|
1522
|
+
if (d.data) {
|
|
1523
|
+
const u = d.data;
|
|
1524
|
+
a.set(u.id, {
|
|
1525
|
+
...u,
|
|
1526
|
+
filePath: s,
|
|
1527
|
+
docs: c,
|
|
1528
|
+
scenarios: []
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
} else for (const u of d.errors || [])
|
|
1532
|
+
e.push({
|
|
1533
|
+
file: s,
|
|
1534
|
+
path: u.path,
|
|
1535
|
+
message: u.message
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
for (const { filePath: s, data: i } of t.scenarios) {
|
|
1539
|
+
const c = te(i);
|
|
1540
|
+
if (c.success) {
|
|
1541
|
+
if (c.data) {
|
|
1542
|
+
const d = c.data, u = {
|
|
1543
|
+
...d,
|
|
1544
|
+
filePath: s
|
|
1545
|
+
};
|
|
1546
|
+
o.set(d.id, u);
|
|
1547
|
+
const h = a.get(d.componentId);
|
|
1548
|
+
h && h.scenarios.push(u);
|
|
1549
|
+
}
|
|
1550
|
+
} else for (const d of c.errors || [])
|
|
1551
|
+
e.push({
|
|
1552
|
+
file: s,
|
|
1553
|
+
path: d.path,
|
|
1554
|
+
message: d.message
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
for (const { filePath: s, data: i } of t.examples) {
|
|
1558
|
+
const c = Se(i);
|
|
1559
|
+
if (c.success) {
|
|
1560
|
+
if (c.data) {
|
|
1561
|
+
const d = c.data;
|
|
1562
|
+
n.set(d.id, {
|
|
1563
|
+
...d,
|
|
1564
|
+
filePath: s
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
} else for (const d of c.errors || [])
|
|
1568
|
+
e.push({
|
|
1569
|
+
file: s,
|
|
1570
|
+
path: d.path,
|
|
1571
|
+
message: d.message
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
const r = t.categories ?? {
|
|
1575
|
+
items: [],
|
|
1576
|
+
uncategorisedLabel: "Other"
|
|
1577
|
+
};
|
|
1578
|
+
return {
|
|
1579
|
+
registry: { components: a, scenarios: o, examples: n, categories: r },
|
|
1580
|
+
errors: e
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
function bt(t) {
|
|
1584
|
+
const e = [], a = [], o = /* @__PURE__ */ new Map();
|
|
1585
|
+
for (const [n, r] of t.components)
|
|
1586
|
+
o.has(n) || o.set(n, []), o.get(n).push(r.filePath);
|
|
1587
|
+
for (const [n, r] of t.scenarios)
|
|
1588
|
+
o.has(n) || o.set(n, []), o.get(n).push(r.filePath);
|
|
1589
|
+
for (const [n, r] of t.examples)
|
|
1590
|
+
o.has(n) || o.set(n, []), o.get(n).push(r.filePath);
|
|
1591
|
+
for (const [n, r] of o)
|
|
1592
|
+
r.length > 1 && e.push({
|
|
1593
|
+
file: r.join(", "),
|
|
1594
|
+
path: "id",
|
|
1595
|
+
message: `Duplicate ID "${n}" found in multiple files`
|
|
1596
|
+
});
|
|
1597
|
+
for (const [n, r] of t.components) {
|
|
1598
|
+
const s = r.playground.primaryScenarioId;
|
|
1599
|
+
if (r.scenarios.some((c) => c.id === s) || e.push({
|
|
1600
|
+
file: r.filePath,
|
|
1601
|
+
path: "playground.primaryScenarioId",
|
|
1602
|
+
message: `Primary scenario "${s}" not found for component "${n}"`
|
|
1603
|
+
}), r.parentId && (t.components.has(r.parentId) || e.push({
|
|
1604
|
+
file: r.filePath,
|
|
1605
|
+
path: "parentId",
|
|
1606
|
+
message: `Parent component "${r.parentId}" not found for component "${n}"`
|
|
1607
|
+
})), r.subcomponents)
|
|
1608
|
+
for (const c of r.subcomponents)
|
|
1609
|
+
if (!t.components.has(c))
|
|
1610
|
+
e.push({
|
|
1611
|
+
file: r.filePath,
|
|
1612
|
+
path: "subcomponents",
|
|
1613
|
+
message: `Subcomponent "${c}" not found for component "${n}"`
|
|
1614
|
+
});
|
|
1615
|
+
else {
|
|
1616
|
+
const d = t.components.get(c);
|
|
1617
|
+
d.parentId !== n && a.push({
|
|
1618
|
+
file: r.filePath,
|
|
1619
|
+
path: "subcomponents",
|
|
1620
|
+
message: `Subcomponent "${c}" has different parentId "${d.parentId}" than expected "${n}"`
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
r.scenarios.length === 0 && e.push({
|
|
1624
|
+
file: r.filePath,
|
|
1625
|
+
path: "scenarios",
|
|
1626
|
+
message: `Component "${n}" has no scenarios`
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
for (const [n, r] of t.scenarios)
|
|
1630
|
+
t.components.has(r.componentId) || e.push({
|
|
1631
|
+
file: r.filePath,
|
|
1632
|
+
path: "componentId",
|
|
1633
|
+
message: `Component "${r.componentId}" not found for scenario "${n}"`
|
|
1634
|
+
});
|
|
1635
|
+
for (const [n, r] of t.examples)
|
|
1636
|
+
t.components.has(r.componentId) || e.push({
|
|
1637
|
+
file: r.filePath,
|
|
1638
|
+
path: "componentId",
|
|
1639
|
+
message: `Component "${r.componentId}" not found for example "${n}"`
|
|
1640
|
+
});
|
|
1641
|
+
return {
|
|
1642
|
+
valid: e.length === 0,
|
|
1643
|
+
errors: e,
|
|
1644
|
+
warnings: a
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function vt(t) {
|
|
1648
|
+
const e = [];
|
|
1649
|
+
if (t.errors.length > 0) {
|
|
1650
|
+
e.push("Errors:");
|
|
1651
|
+
for (const a of t.errors)
|
|
1652
|
+
e.push(` ${a.file}: ${a.path}: ${a.message}`);
|
|
1653
|
+
}
|
|
1654
|
+
if (t.warnings.length > 0) {
|
|
1655
|
+
e.push("Warnings:");
|
|
1656
|
+
for (const a of t.warnings)
|
|
1657
|
+
e.push(` ${a.file}: ${a.path}: ${a.message}`);
|
|
1658
|
+
}
|
|
1659
|
+
return e.join(`
|
|
1660
|
+
`);
|
|
1661
|
+
}
|
|
1662
|
+
function _(t, e) {
|
|
1663
|
+
const a = new URLSearchParams();
|
|
1664
|
+
e != null && e.theme && a.set("theme", e.theme), (e == null ? void 0 : e.width) !== void 0 && a.set("w", String(e.width)), (e == null ? void 0 : e.height) !== void 0 && a.set("h", String(e.height)), e != null && e.background && a.set("bg", e.background);
|
|
1665
|
+
const o = a.toString();
|
|
1666
|
+
return `/harness/${t}${o ? `?${o}` : ""}`;
|
|
1667
|
+
}
|
|
1668
|
+
function St(t, e) {
|
|
1669
|
+
const a = [], o = ["light", "dark"], n = e || [
|
|
1670
|
+
{ width: 375, height: 667 },
|
|
1671
|
+
// Mobile
|
|
1672
|
+
{ width: 1024, height: 768 }
|
|
1673
|
+
// Desktop
|
|
1674
|
+
];
|
|
1675
|
+
for (const [r, s] of t.scenarios) {
|
|
1676
|
+
for (const i of o)
|
|
1677
|
+
a.push({
|
|
1678
|
+
scenarioId: r,
|
|
1679
|
+
componentId: s.componentId,
|
|
1680
|
+
url: _(r, { theme: i }),
|
|
1681
|
+
theme: i
|
|
1682
|
+
});
|
|
1683
|
+
for (const i of n)
|
|
1684
|
+
for (const c of o)
|
|
1685
|
+
a.push({
|
|
1686
|
+
scenarioId: r,
|
|
1687
|
+
componentId: s.componentId,
|
|
1688
|
+
url: _(r, {
|
|
1689
|
+
theme: c,
|
|
1690
|
+
width: i.width,
|
|
1691
|
+
height: i.height
|
|
1692
|
+
}),
|
|
1693
|
+
theme: c,
|
|
1694
|
+
width: i.width,
|
|
1695
|
+
height: i.height
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
return a;
|
|
1699
|
+
}
|
|
1700
|
+
async function validate(options = {}) {
|
|
1701
|
+
console.log(pc.cyan("Validating registry..."));
|
|
1702
|
+
try {
|
|
1703
|
+
const config = await loadConfig({
|
|
1704
|
+
configFile: options.config
|
|
1705
|
+
});
|
|
1706
|
+
const rawData = loadRegistryFiles({ registryPath: config.registryPath });
|
|
1707
|
+
const { registry, errors: parseErrors } = ft(rawData);
|
|
1708
|
+
if (parseErrors.length > 0) {
|
|
1709
|
+
console.log(pc.red("\nSchema validation errors:"));
|
|
1710
|
+
for (const error of parseErrors) {
|
|
1711
|
+
console.log(pc.red(` ${error.file}: ${error.path}: ${error.message}`));
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
const crossRefResult = bt(registry);
|
|
1715
|
+
if (!crossRefResult.valid) {
|
|
1716
|
+
console.log(pc.red("\nCross-reference validation errors:"));
|
|
1717
|
+
console.log(vt(crossRefResult));
|
|
1718
|
+
}
|
|
1719
|
+
if (crossRefResult.warnings.length > 0) {
|
|
1720
|
+
console.log(pc.yellow("\nWarnings:"));
|
|
1721
|
+
for (const warning of crossRefResult.warnings) {
|
|
1722
|
+
console.log(pc.yellow(` ${warning.file}: ${warning.path}: ${warning.message}`));
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const componentCount = registry.components.size;
|
|
1726
|
+
const scenarioCount = registry.scenarios.size;
|
|
1727
|
+
const exampleCount = registry.examples.size;
|
|
1728
|
+
console.log("");
|
|
1729
|
+
console.log(pc.dim(`Found ${componentCount} component(s), ${scenarioCount} scenario(s), ${exampleCount} example(s)`));
|
|
1730
|
+
if (parseErrors.length > 0 || !crossRefResult.valid) {
|
|
1731
|
+
console.log(pc.red("\n✗ Validation failed"));
|
|
1732
|
+
process.exit(1);
|
|
1733
|
+
}
|
|
1734
|
+
console.log(pc.green("\n✓ Registry is valid"));
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
console.error(pc.red("Error validating registry:"));
|
|
1737
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1738
|
+
process.exit(1);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
async function test(options = {}) {
|
|
1742
|
+
console.log(pc.cyan("Running visual tests..."));
|
|
1743
|
+
try {
|
|
1744
|
+
const config = await loadConfig({
|
|
1745
|
+
configFile: options.config
|
|
1746
|
+
});
|
|
1747
|
+
console.log(pc.dim("Validating registry..."));
|
|
1748
|
+
const rawData = loadRegistryFiles({ registryPath: config.registryPath });
|
|
1749
|
+
const { registry, errors: parseErrors } = ft(rawData);
|
|
1750
|
+
if (parseErrors.length > 0) {
|
|
1751
|
+
console.log(pc.red("Registry validation failed:"));
|
|
1752
|
+
for (const error of parseErrors) {
|
|
1753
|
+
console.log(pc.red(` ${error.file}: ${error.path}: ${error.message}`));
|
|
1754
|
+
}
|
|
1755
|
+
process.exit(1);
|
|
1756
|
+
}
|
|
1757
|
+
const crossRefResult = bt(registry);
|
|
1758
|
+
if (!crossRefResult.valid) {
|
|
1759
|
+
console.log(pc.red("Cross-reference validation failed"));
|
|
1760
|
+
process.exit(1);
|
|
1761
|
+
}
|
|
1762
|
+
console.log(pc.dim("Building catalogue..."));
|
|
1763
|
+
await buildStatic(config);
|
|
1764
|
+
const breakpoints = config.playwright?.breakpoints || [
|
|
1765
|
+
{ name: "mobile", width: 375, height: 667 },
|
|
1766
|
+
{ name: "desktop", width: 1024, height: 768 }
|
|
1767
|
+
];
|
|
1768
|
+
const harnessUrls = St(
|
|
1769
|
+
registry,
|
|
1770
|
+
breakpoints.map((bp) => ({ width: bp.width, height: bp.height }))
|
|
1771
|
+
);
|
|
1772
|
+
const testDir = join(config.configDir, ".catalogue-tests");
|
|
1773
|
+
if (!existsSync(testDir)) {
|
|
1774
|
+
mkdirSync(testDir, { recursive: true });
|
|
1775
|
+
}
|
|
1776
|
+
const testFile = generatePlaywrightTest(harnessUrls, config);
|
|
1777
|
+
const testFilePath = join(testDir, "visual.spec.ts");
|
|
1778
|
+
writeFileSync(testFilePath, testFile);
|
|
1779
|
+
const playwrightConfig = generatePlaywrightConfig(config, testDir);
|
|
1780
|
+
const configFilePath = join(testDir, "playwright.config.ts");
|
|
1781
|
+
writeFileSync(configFilePath, playwrightConfig);
|
|
1782
|
+
console.log(pc.dim("Running Playwright tests..."));
|
|
1783
|
+
const args = ["playwright", "test", "-c", configFilePath];
|
|
1784
|
+
if (options.update) {
|
|
1785
|
+
args.push("--update-snapshots");
|
|
1786
|
+
}
|
|
1787
|
+
if (options.headed) {
|
|
1788
|
+
args.push("--headed");
|
|
1789
|
+
}
|
|
1790
|
+
const playwrightProcess = spawn("npx", args, {
|
|
1791
|
+
cwd: config.configDir,
|
|
1792
|
+
stdio: "inherit",
|
|
1793
|
+
shell: true
|
|
1794
|
+
});
|
|
1795
|
+
playwrightProcess.on("close", (code) => {
|
|
1796
|
+
if (code === 0) {
|
|
1797
|
+
console.log(pc.green("\n✓ All visual tests passed"));
|
|
1798
|
+
} else {
|
|
1799
|
+
console.log(pc.red(`
|
|
1800
|
+
✗ Tests failed with code ${code}`));
|
|
1801
|
+
process.exit(code || 1);
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
console.error(pc.red("Error running tests:"));
|
|
1806
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1807
|
+
process.exit(1);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
function generatePlaywrightTest(harnessUrls, config) {
|
|
1811
|
+
const baseUrl = `http://localhost:${config.port}`;
|
|
1812
|
+
return `
|
|
1813
|
+
import { test, expect } from '@playwright/test';
|
|
1814
|
+
|
|
1815
|
+
const scenarios = ${JSON.stringify(harnessUrls, null, 2)};
|
|
1816
|
+
|
|
1817
|
+
for (const scenario of scenarios) {
|
|
1818
|
+
const testName = [
|
|
1819
|
+
scenario.componentId,
|
|
1820
|
+
scenario.scenarioId,
|
|
1821
|
+
scenario.theme,
|
|
1822
|
+
scenario.width ? \`\${scenario.width}x\${scenario.height}\` : 'default',
|
|
1823
|
+
].join('-');
|
|
1824
|
+
|
|
1825
|
+
test(testName, async ({ page }) => {
|
|
1826
|
+
const url = '${baseUrl}#' + scenario.url;
|
|
1827
|
+
await page.goto(url);
|
|
1828
|
+
|
|
1829
|
+
// Wait for component to render
|
|
1830
|
+
await page.waitForSelector('[data-catalogue-container]');
|
|
1831
|
+
|
|
1832
|
+
// Take screenshot
|
|
1833
|
+
const container = page.locator('[data-catalogue-container]');
|
|
1834
|
+
await expect(container).toHaveScreenshot(\`\${testName}.png\`);
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
`;
|
|
1838
|
+
}
|
|
1839
|
+
function generatePlaywrightConfig(config, testDir) {
|
|
1840
|
+
const screenshotDir = config.playwright?.screenshotDir ? resolve(config.configDir, config.playwright.screenshotDir) : join(config.configDir, "screenshots");
|
|
1841
|
+
return `
|
|
1842
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
1843
|
+
|
|
1844
|
+
export default defineConfig({
|
|
1845
|
+
testDir: '${testDir}',
|
|
1846
|
+
snapshotDir: '${screenshotDir}',
|
|
1847
|
+
snapshotPathTemplate: '{snapshotDir}/{testName}/{arg}{ext}',
|
|
1848
|
+
fullyParallel: true,
|
|
1849
|
+
forbidOnly: !!process.env.CI,
|
|
1850
|
+
retries: process.env.CI ? 2 : 0,
|
|
1851
|
+
workers: process.env.CI ? 1 : undefined,
|
|
1852
|
+
reporter: 'list',
|
|
1853
|
+
use: {
|
|
1854
|
+
baseURL: 'http://localhost:${config.port}',
|
|
1855
|
+
trace: 'on-first-retry',
|
|
1856
|
+
},
|
|
1857
|
+
projects: [
|
|
1858
|
+
{
|
|
1859
|
+
name: 'chromium',
|
|
1860
|
+
use: { ...devices['Desktop Chrome'] },
|
|
1861
|
+
},
|
|
1862
|
+
],
|
|
1863
|
+
webServer: {
|
|
1864
|
+
command: 'npx vite preview --port ${config.port}',
|
|
1865
|
+
port: ${config.port},
|
|
1866
|
+
reuseExistingServer: !process.env.CI,
|
|
1867
|
+
cwd: '${config.configDir}',
|
|
1868
|
+
},
|
|
1869
|
+
});
|
|
1870
|
+
`;
|
|
1871
|
+
}
|
|
1872
|
+
async function newComponent(componentId, options = {}) {
|
|
1873
|
+
if (!componentId) {
|
|
1874
|
+
console.error(pc.red("Error: Component ID is required"));
|
|
1875
|
+
console.log("Usage: catalogue new <component-id>");
|
|
1876
|
+
process.exit(1);
|
|
1877
|
+
}
|
|
1878
|
+
if (!/^[a-z][a-z0-9-]*$/.test(componentId)) {
|
|
1879
|
+
console.error(pc.red("Error: Component ID must be kebab-case starting with a letter"));
|
|
1880
|
+
console.log("Example: my-button, data-table, user-card");
|
|
1881
|
+
process.exit(1);
|
|
1882
|
+
}
|
|
1883
|
+
try {
|
|
1884
|
+
const config = await loadConfig({
|
|
1885
|
+
configFile: options.config
|
|
1886
|
+
});
|
|
1887
|
+
const componentDir = join(config.registryPath, "components", componentId);
|
|
1888
|
+
const scenariosDir = join(componentDir, "scenarios");
|
|
1889
|
+
if (existsSync(componentDir)) {
|
|
1890
|
+
console.error(pc.red(`Error: Component "${componentId}" already exists at ${componentDir}`));
|
|
1891
|
+
process.exit(1);
|
|
1892
|
+
}
|
|
1893
|
+
mkdirSync(scenariosDir, { recursive: true });
|
|
1894
|
+
const customElement = componentId.includes("-") ? componentId : `x-${componentId}`;
|
|
1895
|
+
const title = options.title || componentId.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1896
|
+
const status = options.status || "beta";
|
|
1897
|
+
const kind = options.kind || "standalone";
|
|
1898
|
+
const scenarioId = `${componentId}-default`;
|
|
1899
|
+
const componentJson = {
|
|
1900
|
+
id: componentId,
|
|
1901
|
+
title,
|
|
1902
|
+
status,
|
|
1903
|
+
kind,
|
|
1904
|
+
description: `TODO: Add description for ${title}`,
|
|
1905
|
+
tags: [],
|
|
1906
|
+
exports: {
|
|
1907
|
+
customElement
|
|
1908
|
+
},
|
|
1909
|
+
playground: {
|
|
1910
|
+
primaryScenarioId: scenarioId,
|
|
1911
|
+
controls: {
|
|
1912
|
+
// Example controls - customize as needed
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
writeFileSync(
|
|
1917
|
+
join(componentDir, "component.json"),
|
|
1918
|
+
JSON.stringify(componentJson, null, 2) + "\n"
|
|
1919
|
+
);
|
|
1920
|
+
const scenarioJson = {
|
|
1921
|
+
id: scenarioId,
|
|
1922
|
+
title: "Default",
|
|
1923
|
+
description: `Default state of ${title}`,
|
|
1924
|
+
componentId,
|
|
1925
|
+
primary: true,
|
|
1926
|
+
render: {
|
|
1927
|
+
element: customElement,
|
|
1928
|
+
attrs: {},
|
|
1929
|
+
slots: {
|
|
1930
|
+
default: `${title} content`
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
writeFileSync(
|
|
1935
|
+
join(scenariosDir, "default.json"),
|
|
1936
|
+
JSON.stringify(scenarioJson, null, 2) + "\n"
|
|
1937
|
+
);
|
|
1938
|
+
const docsMd = `# ${title}
|
|
1939
|
+
|
|
1940
|
+
TODO: Add description for ${title}.
|
|
1941
|
+
|
|
1942
|
+
## Usage
|
|
1943
|
+
|
|
1944
|
+
\`\`\`html
|
|
1945
|
+
<${customElement}>Content</${customElement}>
|
|
1946
|
+
\`\`\`
|
|
1947
|
+
|
|
1948
|
+
## Properties
|
|
1949
|
+
|
|
1950
|
+
| Property | Type | Default | Description |
|
|
1951
|
+
|----------|------|---------|-------------|
|
|
1952
|
+
| | | | |
|
|
1953
|
+
|
|
1954
|
+
## Slots
|
|
1955
|
+
|
|
1956
|
+
| Slot | Description |
|
|
1957
|
+
|------|-------------|
|
|
1958
|
+
| default | Main content |
|
|
1959
|
+
|
|
1960
|
+
## Examples
|
|
1961
|
+
|
|
1962
|
+
### Basic
|
|
1963
|
+
|
|
1964
|
+
\`\`\`html
|
|
1965
|
+
<${customElement}>
|
|
1966
|
+
Hello World
|
|
1967
|
+
</${customElement}>
|
|
1968
|
+
\`\`\`
|
|
1969
|
+
`;
|
|
1970
|
+
writeFileSync(join(componentDir, "docs.md"), docsMd);
|
|
1971
|
+
const componentTs = `/**
|
|
1972
|
+
* ${title} Web Component
|
|
1973
|
+
*/
|
|
1974
|
+
|
|
1975
|
+
export class ${toPascalCase(componentId)} extends HTMLElement {
|
|
1976
|
+
static get observedAttributes() {
|
|
1977
|
+
return [];
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
constructor() {
|
|
1981
|
+
super();
|
|
1982
|
+
this.attachShadow({ mode: 'open' });
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
connectedCallback() {
|
|
1986
|
+
this.render();
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
attributeChangedCallback() {
|
|
1990
|
+
this.render();
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
private render() {
|
|
1994
|
+
this.shadowRoot!.innerHTML = \`
|
|
1995
|
+
<style>
|
|
1996
|
+
:host {
|
|
1997
|
+
display: block;
|
|
1998
|
+
}
|
|
1999
|
+
</style>
|
|
2000
|
+
<div class="${componentId}">
|
|
2001
|
+
<slot></slot>
|
|
2002
|
+
</div>
|
|
2003
|
+
\`;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
customElements.define('${customElement}', ${toPascalCase(componentId)});
|
|
2008
|
+
`;
|
|
2009
|
+
const srcDir = join(config.configDir, "src", "components");
|
|
2010
|
+
if (existsSync(srcDir)) {
|
|
2011
|
+
const componentSrcPath = join(srcDir, `${componentId}.ts`);
|
|
2012
|
+
if (!existsSync(componentSrcPath)) {
|
|
2013
|
+
writeFileSync(componentSrcPath, componentTs);
|
|
2014
|
+
console.log(pc.green(`✓ Created ${pc.bold(`src/components/${componentId}.ts`)}`));
|
|
2015
|
+
console.log(pc.yellow(` → Add to src/components/index.ts: export * from './${componentId}.js';`));
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
console.log(pc.green(`
|
|
2019
|
+
✓ Created component "${componentId}":`));
|
|
2020
|
+
console.log(pc.dim(` ${componentDir}/`));
|
|
2021
|
+
console.log(` ├── component.json`);
|
|
2022
|
+
console.log(` ├── docs.md`);
|
|
2023
|
+
console.log(` └── scenarios/`);
|
|
2024
|
+
console.log(` └── default.json`);
|
|
2025
|
+
console.log(pc.cyan("\nNext steps:"));
|
|
2026
|
+
console.log(` 1. Edit ${pc.bold("component.json")} to add controls`);
|
|
2027
|
+
console.log(` 2. Edit ${pc.bold("docs.md")} to add documentation`);
|
|
2028
|
+
console.log(` 3. Add more scenarios in ${pc.bold("scenarios/")}`);
|
|
2029
|
+
console.log(` 4. Run ${pc.bold("catalogue validate")} to verify`);
|
|
2030
|
+
console.log(` 5. Run ${pc.bold("catalogue dev")} to preview`);
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
console.error(pc.red("Error creating component:"));
|
|
2033
|
+
console.error(error instanceof Error ? error.message : error);
|
|
2034
|
+
process.exit(1);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
function toPascalCase(str) {
|
|
2038
|
+
return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
2039
|
+
}
|
|
2040
|
+
async function init(options = {}) {
|
|
2041
|
+
const cwd = process.cwd();
|
|
2042
|
+
const projectName = options.name || "my-component-catalogue";
|
|
2043
|
+
console.log(pc.cyan(`
|
|
2044
|
+
Initializing component catalogue...
|
|
2045
|
+
`));
|
|
2046
|
+
const configFiles = ["catalogue.config.ts", "catalogue.config.js", "catalogue.config.mjs"];
|
|
2047
|
+
const existingConfig = configFiles.find((f) => existsSync(resolve(cwd, f)));
|
|
2048
|
+
if (existingConfig && !options.force) {
|
|
2049
|
+
console.log(pc.yellow(`Found existing ${existingConfig}. Use --force to overwrite.
|
|
2050
|
+
`));
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
const dirs = [
|
|
2054
|
+
"registry/components",
|
|
2055
|
+
"registry/examples",
|
|
2056
|
+
"src/components",
|
|
2057
|
+
"screenshots"
|
|
2058
|
+
];
|
|
2059
|
+
for (const dir of dirs) {
|
|
2060
|
+
const dirPath = resolve(cwd, dir);
|
|
2061
|
+
if (!existsSync(dirPath)) {
|
|
2062
|
+
mkdirSync(dirPath, { recursive: true });
|
|
2063
|
+
console.log(pc.green(` Created ${dir}/`));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
const configContent = `import type { CatalogueConfig } from '@adieyal/catalogue-cli';
|
|
2067
|
+
|
|
2068
|
+
export default {
|
|
2069
|
+
name: '${projectName}',
|
|
2070
|
+
registryDir: './registry',
|
|
2071
|
+
componentLoader: () => import('./src/components/index.js'),
|
|
2072
|
+
playwright: {
|
|
2073
|
+
breakpoints: [
|
|
2074
|
+
{ name: 'mobile', width: 375, height: 667 },
|
|
2075
|
+
{ name: 'tablet', width: 768, height: 1024 },
|
|
2076
|
+
{ name: 'desktop', width: 1280, height: 800 },
|
|
2077
|
+
],
|
|
2078
|
+
themes: ['light', 'dark'],
|
|
2079
|
+
screenshotDir: './screenshots',
|
|
2080
|
+
},
|
|
2081
|
+
} satisfies CatalogueConfig;
|
|
2082
|
+
`;
|
|
2083
|
+
writeFileSync(resolve(cwd, "catalogue.config.ts"), configContent);
|
|
2084
|
+
console.log(pc.green(` Created catalogue.config.ts`));
|
|
2085
|
+
const componentsIndexContent = `// Export your web components here
|
|
2086
|
+
// Example:
|
|
2087
|
+
// export * from './button.js';
|
|
2088
|
+
// export * from './card.js';
|
|
2089
|
+
`;
|
|
2090
|
+
writeFileSync(resolve(cwd, "src/components/index.ts"), componentsIndexContent);
|
|
2091
|
+
console.log(pc.green(` Created src/components/index.ts`));
|
|
2092
|
+
const sampleComponentDir = resolve(cwd, "registry/components/sample-button");
|
|
2093
|
+
mkdirSync(sampleComponentDir, { recursive: true });
|
|
2094
|
+
mkdirSync(resolve(sampleComponentDir, "scenarios"), { recursive: true });
|
|
2095
|
+
const componentJson = {
|
|
2096
|
+
id: "sample-button",
|
|
2097
|
+
title: "Sample Button",
|
|
2098
|
+
status: "beta",
|
|
2099
|
+
kind: "standalone",
|
|
2100
|
+
description: "A sample button component to get you started.",
|
|
2101
|
+
tags: ["form", "interactive"],
|
|
2102
|
+
playground: {
|
|
2103
|
+
primaryScenarioId: "sample-button-default"
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
writeFileSync(
|
|
2107
|
+
resolve(sampleComponentDir, "component.json"),
|
|
2108
|
+
JSON.stringify(componentJson, null, 2)
|
|
2109
|
+
);
|
|
2110
|
+
const docsContent = `# Sample Button
|
|
2111
|
+
|
|
2112
|
+
This is a sample button component to help you get started.
|
|
2113
|
+
|
|
2114
|
+
## Usage
|
|
2115
|
+
|
|
2116
|
+
\`\`\`html
|
|
2117
|
+
<sample-button variant="primary">Click me</sample-button>
|
|
2118
|
+
\`\`\`
|
|
2119
|
+
|
|
2120
|
+
## Features
|
|
2121
|
+
|
|
2122
|
+
- Multiple variants (primary, secondary, outline)
|
|
2123
|
+
- Disabled state
|
|
2124
|
+
- Click handling
|
|
2125
|
+
|
|
2126
|
+
Delete this component and create your own using \`catalogue new <component-id>\`.
|
|
2127
|
+
`;
|
|
2128
|
+
writeFileSync(resolve(sampleComponentDir, "docs.md"), docsContent);
|
|
2129
|
+
const scenarioJson = {
|
|
2130
|
+
id: "sample-button-default",
|
|
2131
|
+
componentId: "sample-button",
|
|
2132
|
+
title: "Default",
|
|
2133
|
+
description: "Default button appearance",
|
|
2134
|
+
render: {
|
|
2135
|
+
element: "sample-button",
|
|
2136
|
+
attributes: {
|
|
2137
|
+
variant: "primary"
|
|
2138
|
+
},
|
|
2139
|
+
slots: {
|
|
2140
|
+
default: "Click me"
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
writeFileSync(
|
|
2145
|
+
resolve(sampleComponentDir, "scenarios/default.json"),
|
|
2146
|
+
JSON.stringify(scenarioJson, null, 2)
|
|
2147
|
+
);
|
|
2148
|
+
console.log(pc.green(` Created sample-button component`));
|
|
2149
|
+
const sampleButtonTs = `export class SampleButton extends HTMLElement {
|
|
2150
|
+
static observedAttributes = ['variant', 'disabled'];
|
|
2151
|
+
|
|
2152
|
+
constructor() {
|
|
2153
|
+
super();
|
|
2154
|
+
this.attachShadow({ mode: 'open' });
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
connectedCallback() {
|
|
2158
|
+
this.render();
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
attributeChangedCallback() {
|
|
2162
|
+
this.render();
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
get variant() {
|
|
2166
|
+
return this.getAttribute('variant') || 'primary';
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
get disabled() {
|
|
2170
|
+
return this.hasAttribute('disabled');
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
render() {
|
|
2174
|
+
const styles = \`
|
|
2175
|
+
:host {
|
|
2176
|
+
display: inline-block;
|
|
2177
|
+
}
|
|
2178
|
+
button {
|
|
2179
|
+
padding: 8px 16px;
|
|
2180
|
+
border-radius: 4px;
|
|
2181
|
+
border: 2px solid transparent;
|
|
2182
|
+
font-size: 14px;
|
|
2183
|
+
font-weight: 500;
|
|
2184
|
+
cursor: pointer;
|
|
2185
|
+
transition: all 0.2s;
|
|
2186
|
+
}
|
|
2187
|
+
button:disabled {
|
|
2188
|
+
opacity: 0.5;
|
|
2189
|
+
cursor: not-allowed;
|
|
2190
|
+
}
|
|
2191
|
+
button.primary {
|
|
2192
|
+
background: #3b82f6;
|
|
2193
|
+
color: white;
|
|
2194
|
+
}
|
|
2195
|
+
button.primary:hover:not(:disabled) {
|
|
2196
|
+
background: #2563eb;
|
|
2197
|
+
}
|
|
2198
|
+
button.secondary {
|
|
2199
|
+
background: #6b7280;
|
|
2200
|
+
color: white;
|
|
2201
|
+
}
|
|
2202
|
+
button.secondary:hover:not(:disabled) {
|
|
2203
|
+
background: #4b5563;
|
|
2204
|
+
}
|
|
2205
|
+
button.outline {
|
|
2206
|
+
background: transparent;
|
|
2207
|
+
border-color: #3b82f6;
|
|
2208
|
+
color: #3b82f6;
|
|
2209
|
+
}
|
|
2210
|
+
button.outline:hover:not(:disabled) {
|
|
2211
|
+
background: #3b82f6;
|
|
2212
|
+
color: white;
|
|
2213
|
+
}
|
|
2214
|
+
\`;
|
|
2215
|
+
|
|
2216
|
+
this.shadowRoot!.innerHTML = \`
|
|
2217
|
+
<style>\${styles}</style>
|
|
2218
|
+
<button class="\${this.variant}" \${this.disabled ? 'disabled' : ''}>
|
|
2219
|
+
<slot></slot>
|
|
2220
|
+
</button>
|
|
2221
|
+
\`;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
customElements.define('sample-button', SampleButton);
|
|
2226
|
+
`;
|
|
2227
|
+
writeFileSync(resolve(cwd, "src/components/sample-button.ts"), sampleButtonTs);
|
|
2228
|
+
console.log(pc.green(` Created src/components/sample-button.ts`));
|
|
2229
|
+
writeFileSync(
|
|
2230
|
+
resolve(cwd, "src/components/index.ts"),
|
|
2231
|
+
`export * from './sample-button.js';
|
|
2232
|
+
`
|
|
2233
|
+
);
|
|
2234
|
+
const packageJsonPath = resolve(cwd, "package.json");
|
|
2235
|
+
let packageJson = {};
|
|
2236
|
+
if (existsSync(packageJsonPath)) {
|
|
2237
|
+
packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
2238
|
+
}
|
|
2239
|
+
packageJson.scripts = {
|
|
2240
|
+
...packageJson.scripts || {},
|
|
2241
|
+
"catalogue": "catalogue dev",
|
|
2242
|
+
"catalogue:build": "catalogue build",
|
|
2243
|
+
"catalogue:preview": "catalogue preview",
|
|
2244
|
+
"catalogue:validate": "catalogue validate",
|
|
2245
|
+
"catalogue:test": "catalogue test"
|
|
2246
|
+
};
|
|
2247
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
|
|
2248
|
+
console.log(pc.green(` Updated package.json scripts`));
|
|
2249
|
+
const tsconfigPath = resolve(cwd, "tsconfig.json");
|
|
2250
|
+
if (!existsSync(tsconfigPath)) {
|
|
2251
|
+
const tsconfig = {
|
|
2252
|
+
compilerOptions: {
|
|
2253
|
+
target: "ES2022",
|
|
2254
|
+
module: "ESNext",
|
|
2255
|
+
moduleResolution: "bundler",
|
|
2256
|
+
strict: true,
|
|
2257
|
+
esModuleInterop: true,
|
|
2258
|
+
skipLibCheck: true,
|
|
2259
|
+
declaration: true,
|
|
2260
|
+
outDir: "./dist",
|
|
2261
|
+
rootDir: "./src"
|
|
2262
|
+
},
|
|
2263
|
+
include: ["src/**/*", "catalogue.config.ts"],
|
|
2264
|
+
exclude: ["node_modules", "dist"]
|
|
2265
|
+
};
|
|
2266
|
+
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
2267
|
+
console.log(pc.green(` Created tsconfig.json`));
|
|
2268
|
+
}
|
|
2269
|
+
const gitignorePath = resolve(cwd, ".gitignore");
|
|
2270
|
+
const gitignoreAdditions = `
|
|
2271
|
+
# Catalogue
|
|
2272
|
+
.catalogue-tests/
|
|
2273
|
+
playwright-report/
|
|
2274
|
+
test-results/
|
|
2275
|
+
`;
|
|
2276
|
+
if (existsSync(gitignorePath)) {
|
|
2277
|
+
const existing = readFileSync(gitignorePath, "utf-8");
|
|
2278
|
+
if (!existing.includes(".catalogue-tests")) {
|
|
2279
|
+
writeFileSync(gitignorePath, existing + gitignoreAdditions);
|
|
2280
|
+
console.log(pc.green(` Updated .gitignore`));
|
|
2281
|
+
}
|
|
2282
|
+
} else {
|
|
2283
|
+
writeFileSync(gitignorePath, `node_modules/
|
|
2284
|
+
dist/
|
|
2285
|
+
${gitignoreAdditions}`);
|
|
2286
|
+
console.log(pc.green(` Created .gitignore`));
|
|
2287
|
+
}
|
|
2288
|
+
if (options.withClaude) {
|
|
2289
|
+
const claudeSkillsDir = resolve(cwd, ".claude/skills");
|
|
2290
|
+
mkdirSync(claudeSkillsDir, { recursive: true });
|
|
2291
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2292
|
+
const __dirname = dirname(__filename);
|
|
2293
|
+
const packageSkillsDir = resolve(__dirname, "../../skills");
|
|
2294
|
+
if (existsSync(packageSkillsDir)) {
|
|
2295
|
+
const skillFiles = readdirSync(packageSkillsDir).filter((f) => f.endsWith(".md"));
|
|
2296
|
+
for (const file of skillFiles) {
|
|
2297
|
+
const content = readFileSync(resolve(packageSkillsDir, file), "utf-8");
|
|
2298
|
+
writeFileSync(resolve(claudeSkillsDir, file), content);
|
|
2299
|
+
}
|
|
2300
|
+
console.log(pc.green(` Created .claude/skills/ (${skillFiles.length} skills)`));
|
|
2301
|
+
} else {
|
|
2302
|
+
installEmbeddedSkills(claudeSkillsDir);
|
|
2303
|
+
console.log(pc.green(` Created .claude/skills/`));
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
console.log(pc.cyan(`
|
|
2307
|
+
✓ Component catalogue initialized!
|
|
2308
|
+
`));
|
|
2309
|
+
console.log(`Next steps:
|
|
2310
|
+
`);
|
|
2311
|
+
console.log(` 1. Install dependencies:`);
|
|
2312
|
+
console.log(pc.gray(` npm install @adieyal/catalogue-cli @adieyal/catalogue-core
|
|
2313
|
+
`));
|
|
2314
|
+
console.log(` 2. Start the dev server:`);
|
|
2315
|
+
console.log(pc.gray(` npm run catalogue
|
|
2316
|
+
`));
|
|
2317
|
+
console.log(` 3. Create your first component:`);
|
|
2318
|
+
console.log(pc.gray(` npx catalogue new my-component
|
|
2319
|
+
`));
|
|
2320
|
+
if (options.withClaude) {
|
|
2321
|
+
console.log(` Claude Code skills installed. Available commands:`);
|
|
2322
|
+
console.log(pc.gray(` /new-component <id> - Create a new component`));
|
|
2323
|
+
console.log(pc.gray(` /new-scenario <id> <name> - Add a scenario`));
|
|
2324
|
+
console.log(pc.gray(` /document-component <id> - Generate docs`));
|
|
2325
|
+
console.log(pc.gray(` /migrate-library [path] - Migrate existing library`));
|
|
2326
|
+
console.log(pc.gray(` /setup-tokens [path] - Configure design tokens
|
|
2327
|
+
`));
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
function installEmbeddedSkills(skillsDir) {
|
|
2331
|
+
const skills = {
|
|
2332
|
+
"new-component.md": `# Create New Component
|
|
2333
|
+
|
|
2334
|
+
Create a new component entry in the catalogue registry.
|
|
2335
|
+
|
|
2336
|
+
## Arguments
|
|
2337
|
+
|
|
2338
|
+
- \`$ARGUMENTS\` - Component ID (kebab-case, e.g., \`user-avatar\`)
|
|
2339
|
+
|
|
2340
|
+
## Instructions
|
|
2341
|
+
|
|
2342
|
+
1. Parse the component ID from \`$ARGUMENTS\`. If empty, ask the user for a component ID (must be kebab-case).
|
|
2343
|
+
|
|
2344
|
+
2. Derive the component title by converting kebab-case to Title Case (e.g., \`user-avatar\` → \`User Avatar\`).
|
|
2345
|
+
|
|
2346
|
+
3. Ask the user:
|
|
2347
|
+
- What is the component's purpose/description?
|
|
2348
|
+
- What status? (stable, beta, deprecated) - default: beta
|
|
2349
|
+
- What kind? (standalone, subcomponent, feature) - default: standalone
|
|
2350
|
+
- What tags apply? (e.g., form, layout, navigation, feedback)
|
|
2351
|
+
|
|
2352
|
+
4. Create the directory structure:
|
|
2353
|
+
- \`registry/components/<id>/\`
|
|
2354
|
+
- \`registry/components/<id>/scenarios/\`
|
|
2355
|
+
|
|
2356
|
+
5. Create \`registry/components/<id>/component.json\`:
|
|
2357
|
+
|
|
2358
|
+
\`\`\`json
|
|
2359
|
+
{
|
|
2360
|
+
"id": "<component-id>",
|
|
2361
|
+
"title": "<Component Title>",
|
|
2362
|
+
"status": "<status>",
|
|
2363
|
+
"kind": "<kind>",
|
|
2364
|
+
"description": "<description>",
|
|
2365
|
+
"tags": [<tags>],
|
|
2366
|
+
"playground": {
|
|
2367
|
+
"primaryScenarioId": "<component-id>-default"
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
\`\`\`
|
|
2371
|
+
|
|
2372
|
+
6. Create \`registry/components/<id>/docs.md\`:
|
|
2373
|
+
|
|
2374
|
+
\`\`\`markdown
|
|
2375
|
+
# <Component Title>
|
|
2376
|
+
|
|
2377
|
+
<description>
|
|
2378
|
+
|
|
2379
|
+
## Usage
|
|
2380
|
+
|
|
2381
|
+
\\<\\<component-id>>\\</\\<component-id>>
|
|
2382
|
+
|
|
2383
|
+
## Properties
|
|
2384
|
+
|
|
2385
|
+
| Property | Type | Default | Description |
|
|
2386
|
+
|----------|------|---------|-------------|
|
|
2387
|
+
|
|
2388
|
+
## Slots
|
|
2389
|
+
|
|
2390
|
+
| Slot | Description |
|
|
2391
|
+
|------|-------------|
|
|
2392
|
+
| default | Main content |
|
|
2393
|
+
\`\`\`
|
|
2394
|
+
|
|
2395
|
+
7. Create \`registry/components/<id>/scenarios/default.json\`:
|
|
2396
|
+
|
|
2397
|
+
\`\`\`json
|
|
2398
|
+
{
|
|
2399
|
+
"id": "<component-id>-default",
|
|
2400
|
+
"componentId": "<component-id>",
|
|
2401
|
+
"title": "Default",
|
|
2402
|
+
"description": "Default appearance",
|
|
2403
|
+
"render": {
|
|
2404
|
+
"element": "<component-id>",
|
|
2405
|
+
"slots": {
|
|
2406
|
+
"default": "Content"
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
\`\`\`
|
|
2411
|
+
|
|
2412
|
+
8. Create \`src/components/<id>.ts\` with a Web Component class:
|
|
2413
|
+
- Class name: PascalCase version of the ID (e.g., \`user-avatar\` → \`UserAvatar\`)
|
|
2414
|
+
- Use Shadow DOM with \`mode: 'open'\`
|
|
2415
|
+
- Include basic styles and a slot
|
|
2416
|
+
|
|
2417
|
+
9. Update \`src/components/index.ts\` to add:
|
|
2418
|
+
\`\`\`typescript
|
|
2419
|
+
export * from './<component-id>.js';
|
|
2420
|
+
\`\`\`
|
|
2421
|
+
|
|
2422
|
+
10. Run \`npx catalogue validate\` to verify.
|
|
2423
|
+
|
|
2424
|
+
11. Tell the user what was created and suggest:
|
|
2425
|
+
- Adding properties to the component
|
|
2426
|
+
- Creating more scenarios
|
|
2427
|
+
- Updating the docs
|
|
2428
|
+
`,
|
|
2429
|
+
"new-scenario.md": `# Create New Scenario
|
|
2430
|
+
|
|
2431
|
+
Add a new scenario to an existing component.
|
|
2432
|
+
|
|
2433
|
+
## Arguments
|
|
2434
|
+
|
|
2435
|
+
- \`$ARGUMENTS\` - Format: \`<component-id> <scenario-name>\` (e.g., \`button disabled\`)
|
|
2436
|
+
|
|
2437
|
+
## Instructions
|
|
2438
|
+
|
|
2439
|
+
1. Parse arguments from \`$ARGUMENTS\`:
|
|
2440
|
+
- First word: component ID
|
|
2441
|
+
- Remaining words: scenario name
|
|
2442
|
+
- If missing, ask the user for component ID and scenario name
|
|
2443
|
+
|
|
2444
|
+
2. Verify the component exists at \`registry/components/<component-id>/component.json\`. If not, list available components and ask user to choose.
|
|
2445
|
+
|
|
2446
|
+
3. Generate scenario ID: \`<component-id>-<scenario-name-kebab>\` (e.g., \`button-disabled\`)
|
|
2447
|
+
|
|
2448
|
+
4. Ask the user:
|
|
2449
|
+
- Brief description of this scenario
|
|
2450
|
+
- What attributes/properties should be set?
|
|
2451
|
+
- What slot content should be shown?
|
|
2452
|
+
- Any custom viewport size? (optional)
|
|
2453
|
+
- Any custom background color? (optional)
|
|
2454
|
+
|
|
2455
|
+
5. Create \`registry/components/<component-id>/scenarios/<scenario-name-kebab>.json\`:
|
|
2456
|
+
|
|
2457
|
+
\`\`\`json
|
|
2458
|
+
{
|
|
2459
|
+
"id": "<scenario-id>",
|
|
2460
|
+
"componentId": "<component-id>",
|
|
2461
|
+
"title": "<Scenario Title>",
|
|
2462
|
+
"description": "<description>",
|
|
2463
|
+
"render": {
|
|
2464
|
+
"element": "<component-id>",
|
|
2465
|
+
"attributes": {
|
|
2466
|
+
// based on user input
|
|
2467
|
+
},
|
|
2468
|
+
"slots": {
|
|
2469
|
+
"default": "<slot content>"
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
\`\`\`
|
|
2474
|
+
|
|
2475
|
+
Include optional fields only if provided:
|
|
2476
|
+
- \`"viewport": { "width": <w>, "height": <h> }\`
|
|
2477
|
+
- \`"background": "<color>"\`
|
|
2478
|
+
|
|
2479
|
+
6. Run \`npx catalogue validate\` to verify.
|
|
2480
|
+
|
|
2481
|
+
7. Tell the user:
|
|
2482
|
+
- Scenario created at \`registry/components/<id>/scenarios/<name>.json\`
|
|
2483
|
+
- View it at \`http://localhost:5173/#/harness/<scenario-id>\`
|
|
2484
|
+
- Suggest running \`npm run catalogue\` to see it
|
|
2485
|
+
`,
|
|
2486
|
+
"document-component.md": `# Document Component
|
|
2487
|
+
|
|
2488
|
+
Generate or improve documentation for a component by analyzing its source code.
|
|
2489
|
+
|
|
2490
|
+
## Arguments
|
|
2491
|
+
|
|
2492
|
+
- \`$ARGUMENTS\` - Component ID (e.g., \`button\`)
|
|
2493
|
+
|
|
2494
|
+
## Instructions
|
|
2495
|
+
|
|
2496
|
+
1. Parse component ID from \`$ARGUMENTS\`. If empty, list components in \`registry/components/\` and ask user to choose.
|
|
2497
|
+
|
|
2498
|
+
2. Read the component source file at \`src/components/<id>.ts\` (or \`.js\`).
|
|
2499
|
+
|
|
2500
|
+
3. Analyze the source code to extract:
|
|
2501
|
+
- \`static observedAttributes\` → Properties
|
|
2502
|
+
- Getter/setter pairs → Properties with types
|
|
2503
|
+
- \`<slot>\` elements → Available slots
|
|
2504
|
+
- \`this.dispatchEvent()\` calls → Events
|
|
2505
|
+
- Any JSDoc comments
|
|
2506
|
+
|
|
2507
|
+
4. Read existing docs at \`registry/components/<id>/docs.md\`.
|
|
2508
|
+
|
|
2509
|
+
5. Read \`registry/components/<id>/component.json\` for title and description.
|
|
2510
|
+
|
|
2511
|
+
6. Generate improved documentation with sections for Usage, Properties, Slots, Events, Examples, and Accessibility.
|
|
2512
|
+
|
|
2513
|
+
7. Show the user the generated docs and ask if they want to:
|
|
2514
|
+
- Replace existing docs entirely
|
|
2515
|
+
- Merge with existing docs
|
|
2516
|
+
- Just see the output without saving
|
|
2517
|
+
|
|
2518
|
+
8. If saving, write to \`registry/components/<id>/docs.md\`.
|
|
2519
|
+
|
|
2520
|
+
9. Suggest creating scenarios for any documented states/variants not yet covered.
|
|
2521
|
+
`,
|
|
2522
|
+
"migrate-library.md": `# Migrate Existing Component Library
|
|
2523
|
+
|
|
2524
|
+
Guide migration of an existing component library to use the catalogue.
|
|
2525
|
+
|
|
2526
|
+
## Arguments
|
|
2527
|
+
|
|
2528
|
+
- \`$ARGUMENTS\` - Optional: path to components directory (e.g., \`src/components\`)
|
|
2529
|
+
|
|
2530
|
+
## Instructions
|
|
2531
|
+
|
|
2532
|
+
Follow this phased approach. Do NOT try to "port everything" at once.
|
|
2533
|
+
|
|
2534
|
+
### Phase 1: Assess the Library
|
|
2535
|
+
|
|
2536
|
+
1. If \`$ARGUMENTS\` provided, scan that directory. Otherwise, look for common patterns:
|
|
2537
|
+
- \`src/components/\`
|
|
2538
|
+
- \`lib/components/\`
|
|
2539
|
+
- \`packages/*/src/\`
|
|
2540
|
+
|
|
2541
|
+
2. List all components found (custom elements, web components, or framework components).
|
|
2542
|
+
|
|
2543
|
+
3. Check how components are currently registered:
|
|
2544
|
+
- Side-effect registration on import?
|
|
2545
|
+
- Manual \`customElements.define()\` calls?
|
|
2546
|
+
|
|
2547
|
+
4. Check for existing: design tokens, theme support, documentation, demo pages.
|
|
2548
|
+
|
|
2549
|
+
5. Report findings to user before proceeding.
|
|
2550
|
+
|
|
2551
|
+
### Phase 2: Create Registration Entrypoint
|
|
2552
|
+
|
|
2553
|
+
Create \`src/register-all.ts\` that imports all components:
|
|
2554
|
+
|
|
2555
|
+
\`\`\`typescript
|
|
2556
|
+
import './components/button/button';
|
|
2557
|
+
import './components/card/card';
|
|
2558
|
+
\`\`\`
|
|
2559
|
+
|
|
2560
|
+
Update \`catalogue.config.ts\` to point to this entrypoint.
|
|
2561
|
+
|
|
2562
|
+
### Phase 3: Generate Seed Registry
|
|
2563
|
+
|
|
2564
|
+
For EACH component, create minimal registry files:
|
|
2565
|
+
- \`registry/components/<id>/component.json\`
|
|
2566
|
+
- \`registry/components/<id>/scenarios/default.json\`
|
|
2567
|
+
- \`registry/components/<id>/docs.md\`
|
|
2568
|
+
|
|
2569
|
+
Rules: 1 scenario per component is enough initially. Use default props.
|
|
2570
|
+
|
|
2571
|
+
### Phase 4: Add Critical Variants (2-5 per component)
|
|
2572
|
+
|
|
2573
|
+
- Default/primary state
|
|
2574
|
+
- Disabled/loading (if applicable)
|
|
2575
|
+
- Dense case (long labels, overflow)
|
|
2576
|
+
- Responsive stress (narrow container)
|
|
2577
|
+
|
|
2578
|
+
Do NOT create scenarios for every prop combination.
|
|
2579
|
+
|
|
2580
|
+
### Phase 5: Establish Hierarchy
|
|
2581
|
+
|
|
2582
|
+
- \`standalone\` - User-facing components
|
|
2583
|
+
- \`subcomponent\` - Internal pieces (add \`parentId\`)
|
|
2584
|
+
- \`feature\` - Complex composites
|
|
2585
|
+
|
|
2586
|
+
### Phase 6: Validate
|
|
2587
|
+
|
|
2588
|
+
Run \`npx catalogue validate\` and \`npx catalogue dev\`.
|
|
2589
|
+
|
|
2590
|
+
### Migration Tips
|
|
2591
|
+
|
|
2592
|
+
**Components requiring app context:** Create wrapper scenarios that provide context.
|
|
2593
|
+
|
|
2594
|
+
**Components that fetch data:** Add prop to accept data directly. Scenarios must NOT make network calls.
|
|
2595
|
+
|
|
2596
|
+
**Container queries:** Recommend \`@container\` for component internals, \`@media\` for page layout. The catalogue resizer tests container width, not viewport.
|
|
2597
|
+
|
|
2598
|
+
### What "Done" Looks Like
|
|
2599
|
+
|
|
2600
|
+
- Every component has a primary scenario
|
|
2601
|
+
- Key components have 3-5 variants
|
|
2602
|
+
- Playground works for top 20% of components
|
|
2603
|
+
- Old demo pages can be deleted
|
|
2604
|
+
`,
|
|
2605
|
+
"setup-tokens.md": `# Setup Design Tokens
|
|
2606
|
+
|
|
2607
|
+
Configure design tokens for the catalogue so components render with correct styling.
|
|
2608
|
+
|
|
2609
|
+
## Arguments
|
|
2610
|
+
|
|
2611
|
+
- \`$ARGUMENTS\` - Optional: path to tokens file (e.g., \`src/tokens/tokens.css\`)
|
|
2612
|
+
|
|
2613
|
+
## Instructions
|
|
2614
|
+
|
|
2615
|
+
### Step 1: Identify Token Source
|
|
2616
|
+
|
|
2617
|
+
Search for: \`**/tokens.css\`, \`**/variables.css\`, \`**/theme.css\`, \`**/design-tokens.json\`
|
|
2618
|
+
|
|
2619
|
+
Ask user to confirm the correct path.
|
|
2620
|
+
|
|
2621
|
+
### Step 2: Identify Themes
|
|
2622
|
+
|
|
2623
|
+
Check for:
|
|
2624
|
+
- Separate files: \`tokens-light.css\`, \`tokens-dark.css\`
|
|
2625
|
+
- Selectors: \`[data-theme="dark"]\`, \`.dark-theme\`
|
|
2626
|
+
|
|
2627
|
+
### Step 3: Update Configuration
|
|
2628
|
+
|
|
2629
|
+
**Single theme:**
|
|
2630
|
+
\`\`\`typescript
|
|
2631
|
+
export default {
|
|
2632
|
+
themes: {
|
|
2633
|
+
tokenCss: './src/tokens/tokens.css',
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
\`\`\`
|
|
2637
|
+
|
|
2638
|
+
**Multiple themes:**
|
|
2639
|
+
\`\`\`typescript
|
|
2640
|
+
export default {
|
|
2641
|
+
themes: {
|
|
2642
|
+
default: 'light',
|
|
2643
|
+
available: ['light', 'dark'],
|
|
2644
|
+
tokenCss: {
|
|
2645
|
+
light: './src/tokens/tokens-light.css',
|
|
2646
|
+
dark: './src/tokens/tokens-dark.css',
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
\`\`\`
|
|
2651
|
+
|
|
2652
|
+
### Step 4: Scope Decision
|
|
2653
|
+
|
|
2654
|
+
**Model A - Global tokens (recommended):** Tokens apply to entire document.
|
|
2655
|
+
|
|
2656
|
+
**Model B - Scoped tokens:** Tokens apply only inside preview frame. Better if tokens use generic names (\`--text\`, \`--bg\`).
|
|
2657
|
+
|
|
2658
|
+
### Step 5: Container Query Setup
|
|
2659
|
+
|
|
2660
|
+
Ensure preview frame has:
|
|
2661
|
+
\`\`\`css
|
|
2662
|
+
.preview-frame {
|
|
2663
|
+
container-type: inline-size;
|
|
2664
|
+
}
|
|
2665
|
+
\`\`\`
|
|
2666
|
+
|
|
2667
|
+
Tell user: "Components should use \`@container\` queries for internal responsiveness, not \`@media\` queries."
|
|
2668
|
+
|
|
2669
|
+
### Step 6: Verify
|
|
2670
|
+
|
|
2671
|
+
1. Run \`npx catalogue dev\`
|
|
2672
|
+
2. Check components render with correct colors, typography, spacing
|
|
2673
|
+
3. Toggle themes and verify switching works
|
|
2674
|
+
4. Resize preview frame and verify container queries respond
|
|
2675
|
+
`
|
|
2676
|
+
};
|
|
2677
|
+
for (const [filename, content] of Object.entries(skills)) {
|
|
2678
|
+
writeFileSync(resolve(skillsDir, filename), content);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
export {
|
|
2682
|
+
CatalogueConfigSchema as C,
|
|
2683
|
+
getRegistryWatchPaths as a,
|
|
2684
|
+
createHtmlTemplate as b,
|
|
2685
|
+
cataloguePlugin as c,
|
|
2686
|
+
buildStatic as d,
|
|
2687
|
+
startPreviewServer as e,
|
|
2688
|
+
dev as f,
|
|
2689
|
+
generateRegistryModule as g,
|
|
2690
|
+
build as h,
|
|
2691
|
+
init as i,
|
|
2692
|
+
validateConfig as j,
|
|
2693
|
+
loadConfig as k,
|
|
2694
|
+
loadRegistryFiles as l,
|
|
2695
|
+
createDefaultConfig as m,
|
|
2696
|
+
newComponent as n,
|
|
2697
|
+
preview as p,
|
|
2698
|
+
startDevServer as s,
|
|
2699
|
+
test as t,
|
|
2700
|
+
validate as v
|
|
2701
|
+
};
|
|
2702
|
+
//# sourceMappingURL=init-CI0WzrG1.js.map
|