agnosticui-cli 2.0.0-alpha.2 → 2.0.0-alpha.21

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.
Files changed (52) hide show
  1. package/README.md +124 -1170
  2. package/dist/cli.js +70 -28
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/add.d.ts.map +1 -1
  5. package/dist/commands/add.js +25 -34
  6. package/dist/commands/add.js.map +1 -1
  7. package/dist/commands/context.d.ts +3 -0
  8. package/dist/commands/context.d.ts.map +1 -0
  9. package/dist/commands/context.js +243 -0
  10. package/dist/commands/context.js.map +1 -0
  11. package/dist/commands/init.d.ts.map +1 -1
  12. package/dist/commands/init.js +145 -110
  13. package/dist/commands/init.js.map +1 -1
  14. package/dist/commands/list.js +4 -4
  15. package/dist/commands/list.js.map +1 -1
  16. package/dist/commands/playbook.d.ts +3 -0
  17. package/dist/commands/playbook.d.ts.map +1 -0
  18. package/dist/commands/playbook.js +189 -0
  19. package/dist/commands/playbook.js.map +1 -0
  20. package/dist/commands/remove.js +2 -2
  21. package/dist/commands/remove.js.map +1 -1
  22. package/dist/commands/storybook.d.ts +3 -0
  23. package/dist/commands/storybook.d.ts.map +1 -0
  24. package/dist/commands/storybook.js +144 -0
  25. package/dist/commands/storybook.js.map +1 -0
  26. package/dist/commands/sync.d.ts +1 -4
  27. package/dist/commands/sync.d.ts.map +1 -1
  28. package/dist/commands/sync.js +41 -33
  29. package/dist/commands/sync.js.map +1 -1
  30. package/dist/commands/view.d.ts +3 -0
  31. package/dist/commands/view.d.ts.map +1 -0
  32. package/dist/commands/view.js +109 -0
  33. package/dist/commands/view.js.map +1 -0
  34. package/dist/types/index.d.ts +24 -2
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/utils/logger.d.ts +1 -0
  37. package/dist/utils/logger.d.ts.map +1 -1
  38. package/dist/utils/logger.js +3 -0
  39. package/dist/utils/logger.js.map +1 -1
  40. package/dist/utils/npm.d.ts +13 -0
  41. package/dist/utils/npm.d.ts.map +1 -0
  42. package/dist/utils/npm.js +62 -0
  43. package/dist/utils/npm.js.map +1 -0
  44. package/dist/utils/stories.d.ts +11 -0
  45. package/dist/utils/stories.d.ts.map +1 -0
  46. package/dist/utils/stories.js +661 -0
  47. package/dist/utils/stories.js.map +1 -0
  48. package/dist/utils/viewer.d.ts +4 -0
  49. package/dist/utils/viewer.d.ts.map +1 -0
  50. package/dist/utils/viewer.js +1067 -0
  51. package/dist/utils/viewer.js.map +1 -0
  52. package/package.json +7 -5
@@ -0,0 +1,1067 @@
1
+ /**
2
+ * Viewer app template generation utilities for `ag view`
3
+ *
4
+ * Generates a lightweight Vite-powered component viewer app in
5
+ * .agnosticui-viewer/ that renders all ejected components using
6
+ * the project's own framework (React, Vue, or Lit/vanilla).
7
+ */
8
+ import path from 'node:path';
9
+ import { existsSync } from 'node:fs';
10
+ import { writeFile } from 'node:fs/promises';
11
+ import { ensureDir } from './files.js';
12
+ // ---------------------------------------------------------------------------
13
+ // Component render defaults
14
+ // ---------------------------------------------------------------------------
15
+ const TEXT_CHILD_COMPONENTS = new Set([
16
+ 'Alert', 'Badge', 'BadgeFx', 'Button', 'ButtonFx', 'Card',
17
+ 'Kbd', 'Link', 'Mark', 'MessageBubble', 'Tag',
18
+ ]);
19
+ // Components whose main React export name differs from React${name}.
20
+ // Maps component name to the actual exported identifier to import and render.
21
+ const REACT_EXPORT_OVERRIDES = {
22
+ Flex: 'ReactFlexRow',
23
+ };
24
+ // React-specific minimal renders for form/display components
25
+ const REACT_SPECIFIC = {
26
+ Avatar: '<ReactAvatar text="AG" />',
27
+ Checkbox: '<ReactCheckbox id="viewer-cb" label="Checkbox" />',
28
+ Collapsible: '<ReactCollapsible><span slot="summary">Toggle details</span><div>Content goes here.</div></ReactCollapsible>',
29
+ CopyButton: '<ReactCopyButton text="ag add button" />',
30
+ Divider: '<ReactDivider />',
31
+ EmptyState: '<ReactEmptyState />',
32
+ Flex: '<ReactFlexRow><span>Item 1</span><span>Item 2</span><span>Item 3</span></ReactFlexRow>',
33
+ IconButton: '<ReactIconButton aria-label="settings"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg></ReactIconButton>',
34
+ IconButtonFx: '<ReactIconButtonFx aria-label="settings"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/></svg></ReactIconButtonFx>',
35
+ Input: '<ReactInput id="viewer-input" label="Label" type="text" />',
36
+ Loader: '<ReactLoader />',
37
+ Progress: '<ReactProgress value={50} max={100} />',
38
+ ProgressRing: '<ReactProgressRing value={50} />',
39
+ Radio: '<ReactRadio id="viewer-radio" label="Radio" name="viewer-group" />',
40
+ Rating: '<ReactRating value={3} />',
41
+ ScrollProgress: '<ReactScrollProgress />',
42
+ ScrollToButton: '<ReactScrollToButton />',
43
+ Select: '<ReactSelect id="viewer-select" label="Select" />',
44
+ SkeletonLoader: '<ReactSkeletonLoader />',
45
+ Slider: '<ReactSlider id="viewer-slider" label="Slider" min={0} max={100} value={50} />',
46
+ Spinner: '<ReactSpinner />',
47
+ Toggle: '<ReactToggle id="viewer-toggle" label="Toggle" />',
48
+ VisuallyHidden: '<ReactVisuallyHidden>Screen reader only text</ReactVisuallyHidden>',
49
+ };
50
+ // Components whose main Vue export name differs from Vue${name}.vue.
51
+ // Maps component name to the actual .vue file basename to import and render.
52
+ const VUE_EXPORT_OVERRIDES = {
53
+ Flex: 'VueFlexRow',
54
+ };
55
+ // Vue-specific minimal renders
56
+ const VUE_SPECIFIC = {
57
+ Avatar: { props: 'text="AG"', slot: '' },
58
+ Checkbox: { props: 'id="viewer-cb" label="Checkbox"', slot: '' },
59
+ CopyButton: { props: 'text="ag add button"', slot: '' },
60
+ Divider: { props: '', slot: '' },
61
+ EmptyState: { props: '', slot: '' },
62
+ Flex: { props: '', slot: '<span>Item 1</span><span>Item 2</span><span>Item 3</span>' },
63
+ IconButton: { props: 'aria-label="settings"', slot: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/></svg>' },
64
+ IconButtonFx: { props: 'aria-label="settings"', slot: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/></svg>' },
65
+ Input: { props: 'id="viewer-input" label="Label" type="text"', slot: '' },
66
+ Loader: { props: '', slot: '' },
67
+ Progress: { props: ':value="50" :max="100"', slot: '' },
68
+ ProgressRing: { props: ':value="50"', slot: '' },
69
+ Radio: { props: 'id="viewer-radio" label="Radio" name="viewer-group"', slot: '' },
70
+ Rating: { props: ':value="3"', slot: '' },
71
+ ScrollProgress: { props: '', slot: '' },
72
+ ScrollToButton: { props: '', slot: '' },
73
+ Select: { props: 'id="viewer-select" label="Select"', slot: '' },
74
+ SkeletonLoader: { props: '', slot: '' },
75
+ Slider: { props: 'id="viewer-slider" label="Slider" :min="0" :max="100" :value="50"', slot: '' },
76
+ Spinner: { props: '', slot: '' },
77
+ Toggle: { props: 'id="viewer-toggle" label="Toggle"', slot: '' },
78
+ VisuallyHidden: { props: '', slot: 'Screen reader only text' },
79
+ };
80
+ // Lit/vanilla minimal HTML renders (uses ag-* tag names)
81
+ const LIT_SPECIFIC = {
82
+ Avatar: (tag) => `<${tag} text="AG"></${tag}>`,
83
+ Checkbox: (tag) => `<${tag} id="viewer-cb" label="Checkbox"></${tag}>`,
84
+ CopyButton: (tag) => `<${tag} text="ag add button"></${tag}>`,
85
+ Divider: (tag) => `<${tag}></${tag}>`,
86
+ EmptyState: (tag) => `<${tag}></${tag}>`,
87
+ Input: (tag) => `<${tag} id="viewer-input" label="Label" type="text"></${tag}>`,
88
+ Loader: (tag) => `<${tag}></${tag}>`,
89
+ Progress: (tag) => `<${tag} value="50" max="100"></${tag}>`,
90
+ ProgressRing: (tag) => `<${tag} value="50"></${tag}>`,
91
+ Radio: (tag) => `<${tag} id="viewer-radio" label="Radio" name="viewer-group"></${tag}>`,
92
+ Rating: (tag) => `<${tag} value="3"></${tag}>`,
93
+ ScrollProgress: (tag) => `<${tag}></${tag}>`,
94
+ ScrollToButton: (tag) => `<${tag}></${tag}>`,
95
+ Select: (tag) => `<${tag} id="viewer-select" label="Select"></${tag}>`,
96
+ SkeletonLoader: (tag) => `<${tag}></${tag}>`,
97
+ Slider: (tag) => `<${tag} id="viewer-slider" label="Slider" min="0" max="100" value="50"></${tag}>`,
98
+ Spinner: (tag) => `<${tag}></${tag}>`,
99
+ Toggle: (tag) => `<${tag} id="viewer-toggle" label="Toggle"></${tag}>`,
100
+ VisuallyHidden: (tag) => `<${tag}>Screen reader only text</${tag}>`,
101
+ };
102
+ // Components also used as viewer chrome — import aliases needed to avoid duplicate identifiers
103
+ const VIEWER_CHROME_COMPONENTS = new Set(['CopyButton', 'Header', 'Tabs']);
104
+ function toKebabCase(name) {
105
+ return name.replace(/([A-Z])/g, (match, char, offset) => (offset > 0 ? '-' : '') + char.toLowerCase());
106
+ }
107
+ function getAgTagName(name) {
108
+ return `ag-${toKebabCase(name)}`;
109
+ }
110
+ function getReactRender(name, localName) {
111
+ const wrapper = localName ?? `React${name}`;
112
+ if (TEXT_CHILD_COMPONENTS.has(name))
113
+ return `<${wrapper}>${name}</${wrapper}>`;
114
+ if (REACT_SPECIFIC[name]) {
115
+ const s = REACT_SPECIFIC[name];
116
+ // Only replace the default tag name if localName is provided AND the string
117
+ // doesn't already contain localName (e.g. REACT_SPECIFIC for Flex already
118
+ // uses 'ReactFlexRow' directly, so replacing 'ReactFlex' → 'ReactFlexRow'
119
+ // would produce 'ReactFlexRowRow').
120
+ if (localName && !s.includes(localName)) {
121
+ return s.replace(new RegExp(`React${name}`, 'g'), localName);
122
+ }
123
+ return s;
124
+ }
125
+ return `<${wrapper} />`;
126
+ }
127
+ function getVueRenderBlock(name, localName) {
128
+ const wrapper = localName ?? `Vue${name}`;
129
+ if (TEXT_CHILD_COMPONENTS.has(name)) {
130
+ return `<${wrapper}>${name}</${wrapper}>`;
131
+ }
132
+ const specific = VUE_SPECIFIC[name];
133
+ if (specific) {
134
+ if (specific.slot) {
135
+ return `<${wrapper}${specific.props ? ' ' + specific.props : ''}>${specific.slot}</${wrapper}>`;
136
+ }
137
+ return `<${wrapper}${specific.props ? ' ' + specific.props : ''} />`;
138
+ }
139
+ return `<${wrapper} />`;
140
+ }
141
+ function getLitRenderHtml(name) {
142
+ const tag = getAgTagName(name);
143
+ if (TEXT_CHILD_COMPONENTS.has(name))
144
+ return `<${tag}>${name}</${tag}>`;
145
+ const specificFn = LIT_SPECIFIC[name];
146
+ if (specificFn)
147
+ return specificFn(tag);
148
+ return `<${tag}></${tag}>`;
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // Public: detect optional theme file
152
+ // ---------------------------------------------------------------------------
153
+ export function detectThemeFile(componentsAbsPath) {
154
+ return existsSync(path.join(componentsAbsPath, 'styles', 'ag-theme.css'));
155
+ }
156
+ // ---------------------------------------------------------------------------
157
+ // Template generators
158
+ // ---------------------------------------------------------------------------
159
+ function generatePackageJson(framework) {
160
+ const deps = {
161
+ 'lit': '^3.0.0',
162
+ 'focus-trap': '^7.0.0',
163
+ '@floating-ui/dom': '^1.0.0',
164
+ };
165
+ const devDeps = {
166
+ 'vite': '^5.0.0',
167
+ 'typescript': '^5.0.0',
168
+ };
169
+ if (framework === 'react') {
170
+ deps['react'] = '^18.0.0';
171
+ deps['react-dom'] = '^18.0.0';
172
+ deps['@lit/react'] = '^1.0.0';
173
+ devDeps['@vitejs/plugin-react'] = '^4.0.0';
174
+ devDeps['@types/react'] = '^18.0.0';
175
+ devDeps['@types/react-dom'] = '^18.0.0';
176
+ }
177
+ else if (framework === 'vue') {
178
+ deps['vue'] = '^3.0.0';
179
+ devDeps['@vitejs/plugin-vue'] = '^5.0.0';
180
+ deps['@lit/react'] = '^1.0.0'; // needed by some ag-ref core files
181
+ }
182
+ else {
183
+ // lit / svelte / other
184
+ deps['@lit/react'] = '^1.0.0'; // pulled in by reference lib React wrappers if resolved
185
+ }
186
+ return JSON.stringify({
187
+ name: 'agnosticui-viewer',
188
+ version: '1.0.0',
189
+ private: true,
190
+ type: 'module',
191
+ scripts: { dev: 'vite' },
192
+ dependencies: deps,
193
+ devDependencies: devDeps,
194
+ }, null, 2) + '\n';
195
+ }
196
+ function generateTsConfig(framework) {
197
+ return JSON.stringify({
198
+ compilerOptions: {
199
+ target: 'ES2020',
200
+ lib: ['ES2020', 'DOM', 'DOM.Iterable'],
201
+ module: 'ESNext',
202
+ skipLibCheck: true,
203
+ moduleResolution: 'bundler',
204
+ allowImportingTsExtensions: true,
205
+ resolveJsonModule: true,
206
+ isolatedModules: true,
207
+ noEmit: true,
208
+ ...(framework === 'react' ? { jsx: 'react-jsx' } : {}),
209
+ strict: false,
210
+ experimentalDecorators: true,
211
+ useDefineForClassFields: false,
212
+ },
213
+ include: ['src'],
214
+ }, null, 2) + '\n';
215
+ }
216
+ function generateViteConfig(framework, componentsAbsPath, refLibAbsPath) {
217
+ const refComponentsPath = path.join(refLibAbsPath, 'src', 'components');
218
+ const agRefAlias = JSON.stringify(refComponentsPath);
219
+ const agComponentsAlias = JSON.stringify(componentsAbsPath);
220
+ const optimizeIncludes = framework === 'react'
221
+ ? `['react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', 'lit', 'lit/decorators.js', '@lit/react', '@floating-ui/dom', 'focus-trap']`
222
+ : framework === 'vue'
223
+ ? `['vue', 'lit', 'lit/decorators.js', '@floating-ui/dom', 'focus-trap']`
224
+ : `['lit', 'lit/decorators.js', '@floating-ui/dom', 'focus-trap']`;
225
+ const pluginImport = framework === 'react'
226
+ ? `import react from '@vitejs/plugin-react'`
227
+ : framework === 'vue'
228
+ ? `import vue from '@vitejs/plugin-vue'`
229
+ : '';
230
+ const pluginUsage = framework === 'react'
231
+ ? `react()`
232
+ : framework === 'vue'
233
+ ? `vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('ag-') } } })`
234
+ : '';
235
+ const pluginsLine = pluginUsage
236
+ ? ` plugins: [${pluginUsage}],\n`
237
+ : '';
238
+ return `import { defineConfig } from 'vite'
239
+ ${pluginImport ? pluginImport + '\n' : ''}
240
+ export default defineConfig({
241
+ ${pluginsLine} resolve: {
242
+ alias: {
243
+ '@ag-ref': ${agRefAlias},
244
+ '@ag-components': ${agComponentsAlias},
245
+ },
246
+ dedupe: ['lit', 'focus-trap', '@floating-ui/dom'],
247
+ },
248
+ server: {
249
+ fs: {
250
+ allow: ['..'],
251
+ },
252
+ },
253
+ optimizeDeps: {
254
+ noDiscovery: true,
255
+ holdUntilCrawlEnd: false,
256
+ include: ${optimizeIncludes},
257
+ },
258
+ })
259
+ `;
260
+ }
261
+ function generateIndexHtml(framework) {
262
+ const scriptSrc = framework === 'react' ? '/src/main.tsx' : '/src/main.ts';
263
+ return `<!DOCTYPE html>
264
+ <html lang="en">
265
+ <head>
266
+ <meta charset="UTF-8" />
267
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
268
+ <title>AgnosticUI Component Viewer</title>
269
+ </head>
270
+ <body>
271
+ ${framework === 'react' || framework === 'vue' ? '<div id="root"></div>' : ''}
272
+ <script type="module" src="${scriptSrc}"></script>
273
+ </body>
274
+ </html>
275
+ `;
276
+ }
277
+ function generateReactMain() {
278
+ return `import React from 'react'
279
+ import ReactDOM from 'react-dom/client'
280
+ import App from './App'
281
+
282
+ ReactDOM.createRoot(document.getElementById('root')!).render(
283
+ <React.StrictMode>
284
+ <App />
285
+ </React.StrictMode>
286
+ )
287
+ `;
288
+ }
289
+ function generateVueMain() {
290
+ return `import { createApp } from 'vue'
291
+ import App from './App.vue'
292
+
293
+ createApp(App).mount('#root')
294
+ `;
295
+ }
296
+ function generateLitMain() {
297
+ return `import './App'
298
+ `;
299
+ }
300
+ // ---------------------------------------------------------------------------
301
+ // React App (App.tsx)
302
+ // ---------------------------------------------------------------------------
303
+ function generateReactApp(installedComponents, componentsPath, hasTheme, componentsAbsPath) {
304
+ const names = Object.keys(installedComponents).sort();
305
+ // Determine import path: if React${n}.tsx/ts exists use that file, otherwise fall back to the
306
+ // directory index (some components like Flex export multiple sub-components from index.ts).
307
+ function getReactImportPath(n) {
308
+ const base = path.join(componentsAbsPath, n, 'react');
309
+ if (existsSync(path.join(base, `React${n}.tsx`)) || existsSync(path.join(base, `React${n}.ts`))) {
310
+ return `@ag-components/${n}/react/React${n}`;
311
+ }
312
+ return `@ag-components/${n}/react`;
313
+ }
314
+ const componentImports = names
315
+ .map(n => {
316
+ const importPath = getReactImportPath(n);
317
+ const exportName = REACT_EXPORT_OVERRIDES[n] ?? `React${n}`;
318
+ if (VIEWER_CHROME_COMPONENTS.has(n)) {
319
+ // Alias to avoid conflict with the viewer chrome import of the same name
320
+ return `import { ${exportName} as UserReact${n} } from '${importPath}'`;
321
+ }
322
+ if (REACT_EXPORT_OVERRIDES[n]) {
323
+ // Import under the real export name (e.g. ReactFlexContainer)
324
+ return `import { ${exportName} } from '${importPath}'`;
325
+ }
326
+ return `import { React${n} } from '${importPath}'`;
327
+ })
328
+ .join('\n');
329
+ const themeImport = hasTheme
330
+ ? `import '@ag-components/styles/ag-theme.css'`
331
+ : '';
332
+ const componentEntries = names.map(n => {
333
+ const meta = installedComponents[n];
334
+ const snippet = `import { React${n} } from '${componentsPath}/${n}/react/React${n}'`;
335
+ const localName = VIEWER_CHROME_COMPONENTS.has(n)
336
+ ? `UserReact${n}`
337
+ : REACT_EXPORT_OVERRIDES[n] ?? undefined;
338
+ const render = getReactRender(n, localName);
339
+ return ` {
340
+ name: ${JSON.stringify(n)},
341
+ version: ${JSON.stringify(meta.version)},
342
+ addedDate: ${JSON.stringify(meta.added)},
343
+ files: ${JSON.stringify(meta.files)},
344
+ importSnippet: ${JSON.stringify(snippet)},
345
+ render: () => (${render}),
346
+ }`;
347
+ }).join(',\n');
348
+ return `// Auto-generated by \`ag view\`. Do not edit manually.
349
+ import { useState } from 'react'
350
+ import { ReactHeader } from '@ag-ref/Header/react/ReactHeader'
351
+ import { ReactTabs, ReactTab, ReactTabPanel } from '@ag-ref/Tabs/react/ReactTabs'
352
+ import { ReactCopyButton } from '@ag-ref/CopyButton/react/ReactCopyButton'
353
+ import '@ag-components/styles/ag-tokens.css'
354
+ import '@ag-components/styles/ag-tokens-dark.css'
355
+ ${themeImport}
356
+ import './viewer.css'
357
+
358
+ ${componentImports}
359
+
360
+ type ComponentInfo = {
361
+ name: string
362
+ version: string
363
+ addedDate: string
364
+ files: string[]
365
+ importSnippet: string
366
+ render: () => any
367
+ }
368
+
369
+ const COMPONENTS: ComponentInfo[] = [
370
+ ${componentEntries}
371
+ ]
372
+
373
+ export default function App() {
374
+ const [selected, setSelected] = useState(COMPONENTS[0]?.name ?? '')
375
+ const [activeTab, setActiveTab] = useState(0)
376
+ const current = COMPONENTS.find(c => c.name === selected) ?? COMPONENTS[0]
377
+
378
+ return (
379
+ <div className="av-layout">
380
+ <ReactHeader>
381
+ <span className="av-brand">AgnosticUI Component Viewer</span>
382
+ </ReactHeader>
383
+ <div className="av-body">
384
+ <aside className="av-sidebar">
385
+ <div className="av-overview">
386
+ <p>Browse your ejected components. Click a name to preview.</p>
387
+ <p>
388
+ Add more with <code>ag add &lt;name&gt;</code>.{' '}
389
+ <a
390
+ href="https://www.agnosticui.com/installation.html#agnosticui-cli-recommended"
391
+ target="_blank"
392
+ rel="noopener noreferrer"
393
+ >
394
+ CLI docs →
395
+ </a>
396
+ </p>
397
+ </div>
398
+ <nav className="av-nav">
399
+ {COMPONENTS.map(c => (
400
+ <button
401
+ key={c.name}
402
+ onClick={() => { setSelected(c.name); setActiveTab(0) }}
403
+ className={\`av-nav-item\${c.name === selected ? ' av-nav-item--active' : ''}\`}
404
+ >
405
+ {c.name}
406
+ </button>
407
+ ))}
408
+ </nav>
409
+ </aside>
410
+ <main className="av-main">
411
+ {current && (
412
+ <>
413
+ <h2 className="av-component-title">{current.name}</h2>
414
+ <ReactTabs
415
+ activeTab={activeTab}
416
+ ariaLabel={\`\${current.name} tabs\`}
417
+ onTabChange={(e: any) => setActiveTab(e.detail.activeTab)}
418
+ >
419
+ <ReactTab slot="tab" panel="preview">Preview</ReactTab>
420
+ <ReactTab slot="tab" panel="html">HTML</ReactTab>
421
+ <ReactTab slot="tab" panel="info">Info</ReactTab>
422
+ <ReactTabPanel slot="panel" id="preview">
423
+ <div className="av-preview">{current.render()}</div>
424
+ </ReactTabPanel>
425
+ <ReactTabPanel slot="panel" id="html">
426
+ <div className="av-code-block">
427
+ <pre><code>{current.importSnippet}</code></pre>
428
+ <ReactCopyButton text={current.importSnippet} size="sm" />
429
+ </div>
430
+ </ReactTabPanel>
431
+ <ReactTabPanel slot="panel" id="info">
432
+ <div className="av-info">
433
+ <dl>
434
+ <dt>Version</dt>
435
+ <dd>{current.version}</dd>
436
+ <dt>Added</dt>
437
+ <dd>{new Date(current.addedDate).toLocaleDateString()}</dd>
438
+ </dl>
439
+ </div>
440
+ </ReactTabPanel>
441
+ </ReactTabs>
442
+ </>
443
+ )}
444
+ </main>
445
+ </div>
446
+ </div>
447
+ )
448
+ }
449
+ `;
450
+ }
451
+ // ---------------------------------------------------------------------------
452
+ // Vue App (App.vue)
453
+ // ---------------------------------------------------------------------------
454
+ function generateVueApp(installedComponents, componentsPath, hasTheme, componentsAbsPath) {
455
+ const names = Object.keys(installedComponents).sort();
456
+ // For Vue, Flex and similar components use a different .vue filename than Vue${name}.vue
457
+ function getVueImportFile(n) {
458
+ const override = VUE_EXPORT_OVERRIDES[n];
459
+ if (override)
460
+ return `${override}.vue`;
461
+ return `Vue${n}.vue`;
462
+ }
463
+ const componentImports = names
464
+ .map(n => {
465
+ const file = getVueImportFile(n);
466
+ const localName = VIEWER_CHROME_COMPONENTS.has(n) ? `UserVue${n}` : (VUE_EXPORT_OVERRIDES[n] ?? `Vue${n}`);
467
+ return `import ${localName} from '@ag-components/${n}/vue/${file}'`;
468
+ })
469
+ .join('\n');
470
+ const themeImport = hasTheme
471
+ ? `import '@ag-components/styles/ag-theme.css'` : '';
472
+ const componentMapEntries = names
473
+ .map(n => {
474
+ const localName = VIEWER_CHROME_COMPONENTS.has(n)
475
+ ? `UserVue${n}`
476
+ : (VUE_EXPORT_OVERRIDES[n] ?? `Vue${n}`);
477
+ return ` ${JSON.stringify(n)}: ${localName}`;
478
+ })
479
+ .join(',\n');
480
+ const componentDataEntries = names.map(n => {
481
+ const meta = installedComponents[n];
482
+ const snippet = `import Vue${n} from '${componentsPath}/${n}/vue/Vue${n}.vue'`;
483
+ return ` {
484
+ name: ${JSON.stringify(n)},
485
+ version: ${JSON.stringify(meta.version)},
486
+ addedDate: ${JSON.stringify(meta.added)},
487
+ files: ${JSON.stringify(meta.files)},
488
+ importSnippet: ${JSON.stringify(snippet)},
489
+ }`;
490
+ }).join(',\n');
491
+ // Build per-component v-if render blocks
492
+ const previewBlocks = names.map((n, i) => {
493
+ const localName = VIEWER_CHROME_COMPONENTS.has(n)
494
+ ? `UserVue${n}`
495
+ : VUE_EXPORT_OVERRIDES[n] ?? undefined;
496
+ const block = getVueRenderBlock(n, localName);
497
+ const condition = i === 0 ? `v-if="current?.name === '${n}'"` : `v-else-if="current?.name === '${n}'"`;
498
+ return ` <template ${condition}>${block}</template>`;
499
+ }).join('\n');
500
+ return `<!-- Auto-generated by \`ag view\`. Do not edit manually. -->
501
+ <script setup lang="ts">
502
+ import { ref, computed } from 'vue'
503
+ import { VueHeader } from '@ag-ref/Header/vue'
504
+ import { VueTabs, VueTab, VueTabPanel } from '@ag-ref/Tabs/vue'
505
+ import { VueCopyButton } from '@ag-ref/CopyButton/vue'
506
+ import '@ag-components/styles/ag-tokens.css'
507
+ import '@ag-components/styles/ag-tokens-dark.css'
508
+ ${themeImport}
509
+ import './viewer.css'
510
+
511
+ ${componentImports}
512
+
513
+ const componentMap: Record<string, any> = {
514
+ ${componentMapEntries}
515
+ }
516
+
517
+ type ComponentInfo = {
518
+ name: string
519
+ version: string
520
+ addedDate: string
521
+ files: string[]
522
+ importSnippet: string
523
+ }
524
+
525
+ const COMPONENTS: ComponentInfo[] = [
526
+ ${componentDataEntries}
527
+ ]
528
+
529
+ const selected = ref(COMPONENTS[0]?.name ?? '')
530
+ const activeTab = ref(0)
531
+ const current = computed(() => COMPONENTS.find(c => c.name === selected.value) ?? COMPONENTS[0])
532
+
533
+ function selectComponent(name: string) {
534
+ selected.value = name
535
+ activeTab.value = 0
536
+ }
537
+ </script>
538
+
539
+ <template>
540
+ <div class="av-layout">
541
+ <VueHeader>
542
+ <span class="av-brand">AgnosticUI Component Viewer</span>
543
+ </VueHeader>
544
+ <div class="av-body">
545
+ <aside class="av-sidebar">
546
+ <div class="av-overview">
547
+ <p>Browse your ejected components. Click a name to preview.</p>
548
+ <p>
549
+ Add more with <code>ag add &lt;name&gt;</code>.
550
+ <a
551
+ href="https://www.agnosticui.com/installation.html#agnosticui-cli-recommended"
552
+ target="_blank"
553
+ rel="noopener noreferrer"
554
+ >CLI docs →</a>
555
+ </p>
556
+ </div>
557
+ <nav class="av-nav">
558
+ <button
559
+ v-for="c in COMPONENTS"
560
+ :key="c.name"
561
+ @click="selectComponent(c.name)"
562
+ :class="['av-nav-item', { 'av-nav-item--active': c.name === selected }]"
563
+ >{{ c.name }}</button>
564
+ </nav>
565
+ </aside>
566
+ <main class="av-main">
567
+ <template v-if="current">
568
+ <h2 class="av-component-title">{{ current.name }}</h2>
569
+ <VueTabs
570
+ :active-tab="activeTab"
571
+ :aria-label="\`\${current.name} tabs\`"
572
+ @tab-change="activeTab = $event.activeTab"
573
+ >
574
+ <VueTab panel="preview">Preview</VueTab>
575
+ <VueTab panel="html">HTML</VueTab>
576
+ <VueTab panel="info">Info</VueTab>
577
+ <VueTabPanel panel="preview" id="preview">
578
+ <div class="av-preview">
579
+ ${previewBlocks}
580
+ <template v-else>
581
+ <component
582
+ v-if="componentMap[current.name]"
583
+ :is="componentMap[current.name]"
584
+ />
585
+ <div v-else class="av-complex-note">
586
+ <p>{{ current.name }} requires additional props or interaction to preview.</p>
587
+ </div>
588
+ </template>
589
+ </div>
590
+ </VueTabPanel>
591
+ <VueTabPanel panel="html" id="html">
592
+ <div class="av-code-block">
593
+ <pre><code>{{ current.importSnippet }}</code></pre>
594
+ <VueCopyButton :text="current.importSnippet" size="sm" />
595
+ </div>
596
+ </VueTabPanel>
597
+ <VueTabPanel panel="info" id="info">
598
+ <div class="av-info">
599
+ <dl>
600
+ <dt>Version</dt>
601
+ <dd>{{ current.version }}</dd>
602
+ <dt>Added</dt>
603
+ <dd>{{ new Date(current.addedDate).toLocaleDateString() }}</dd>
604
+ </dl>
605
+ </div>
606
+ </VueTabPanel>
607
+ </VueTabs>
608
+ </template>
609
+ </main>
610
+ </div>
611
+ </div>
612
+ </template>
613
+ `;
614
+ }
615
+ // ---------------------------------------------------------------------------
616
+ // Lit / vanilla App (App.ts) — vanilla DOM approach
617
+ // ---------------------------------------------------------------------------
618
+ function generateLitApp(installedComponents, componentsPath, hasTheme) {
619
+ const names = Object.keys(installedComponents).sort();
620
+ const coreImports = names
621
+ .map(n => `import '@ag-components/${n}/core/${n}'`)
622
+ .join('\n');
623
+ const themeImport = hasTheme
624
+ ? `import '@ag-components/styles/ag-theme.css'` : '';
625
+ const componentEntries = names.map(n => {
626
+ const meta = installedComponents[n];
627
+ const snippet = `import '${componentsPath}/${n}/core/${n}'`;
628
+ const renderHtml = getLitRenderHtml(n);
629
+ return ` {
630
+ name: ${JSON.stringify(n)},
631
+ tagName: ${JSON.stringify(getAgTagName(n))},
632
+ version: ${JSON.stringify(meta.version)},
633
+ addedDate: ${JSON.stringify(meta.added)},
634
+ files: ${JSON.stringify(meta.files)},
635
+ importSnippet: ${JSON.stringify(snippet)},
636
+ renderHTML: ${JSON.stringify(renderHtml)},
637
+ }`;
638
+ }).join(',\n');
639
+ // The Lit viewer uses plain HTML for chrome (header, tabs) to avoid shadow DOM
640
+ // slot issues that arise when using ag-* web components via innerHTML.
641
+ // ag-* elements are only used inside the preview area where they belong.
642
+ return `// Auto-generated by \`ag view\`. Do not edit manually.
643
+ import '@ag-components/styles/ag-tokens.css'
644
+ import '@ag-components/styles/ag-tokens-dark.css'
645
+ ${themeImport}
646
+ import './viewer.css'
647
+
648
+ ${coreImports}
649
+
650
+ type ComponentInfo = {
651
+ name: string
652
+ tagName: string
653
+ version: string
654
+ addedDate: string
655
+ files: string[]
656
+ importSnippet: string
657
+ renderHTML: string
658
+ }
659
+
660
+ const COMPONENTS: ComponentInfo[] = [
661
+ ${componentEntries}
662
+ ]
663
+
664
+ let selectedName = COMPONENTS[0]?.name ?? ''
665
+ let activeTab = 0
666
+
667
+ function escHtml(str: string): string {
668
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
669
+ }
670
+
671
+ function copyToClipboard(text: string): void {
672
+ navigator.clipboard?.writeText(text).catch(() => {})
673
+ }
674
+
675
+ function render(): void {
676
+ const current = COMPONENTS.find(c => c.name === selectedName) ?? COMPONENTS[0]
677
+ if (!current) return
678
+
679
+ const navItems = COMPONENTS.map(c =>
680
+ \`<button class="av-nav-item\${c.name === selectedName ? ' av-nav-item--active' : ''}" data-component="\${c.name}">\${c.name}</button>\`
681
+ ).join('\\n')
682
+
683
+ const tabs = ['Preview', 'HTML', 'Info']
684
+ const tabButtons = tabs.map((label, i) =>
685
+ \`<button class="av-tab\${i === activeTab ? ' av-tab--active' : ''}" data-tab="\${i}">\${label}</button>\`
686
+ ).join('\\n')
687
+
688
+ const panels = [
689
+ \`<div class="av-preview">\${current.renderHTML}</div>\`,
690
+ \`<div class="av-code-block">
691
+ <pre><code>\${escHtml(current.importSnippet)}</code></pre>
692
+ <button class="av-copy-btn" data-copy="\${current.importSnippet.replace(/"/g, '&quot;')}">Copy</button>
693
+ </div>\`,
694
+ \`<div class="av-info">
695
+ <dl>
696
+ <dt>Version</dt><dd>\${current.version}</dd>
697
+ <dt>Added</dt><dd>\${new Date(current.addedDate).toLocaleDateString()}</dd>
698
+ </dl>
699
+ </div>\`,
700
+ ]
701
+
702
+ document.body.innerHTML = \`
703
+ <div class="av-layout">
704
+ <header class="av-header">
705
+ <span class="av-brand">AgnosticUI Component Viewer</span>
706
+ </header>
707
+ <div class="av-body">
708
+ <aside class="av-sidebar">
709
+ <div class="av-overview">
710
+ <p>Browse your ejected components. Click a name to preview.</p>
711
+ <p>Add more with <code>ag add &lt;name&gt;</code>. <a href="https://www.agnosticui.com/installation.html#agnosticui-cli-recommended" target="_blank" rel="noopener">CLI docs &rarr;</a></p>
712
+ </div>
713
+ <nav class="av-nav">
714
+ \${navItems}
715
+ </nav>
716
+ </aside>
717
+ <main class="av-main">
718
+ <h2 class="av-component-title">\${current.name}</h2>
719
+ <div class="av-tabs">
720
+ <div class="av-tab-list" role="tablist">
721
+ \${tabButtons}
722
+ </div>
723
+ <div class="av-tab-panel">
724
+ \${panels[activeTab]}
725
+ </div>
726
+ </div>
727
+ </main>
728
+ </div>
729
+ </div>
730
+ \`
731
+
732
+ // Re-attach nav listeners
733
+ document.querySelectorAll<HTMLElement>('.av-nav-item').forEach(btn => {
734
+ btn.addEventListener('click', () => {
735
+ selectedName = btn.dataset.component ?? ''
736
+ activeTab = 0
737
+ render()
738
+ })
739
+ })
740
+
741
+ // Re-attach tab listeners
742
+ document.querySelectorAll<HTMLElement>('.av-tab').forEach(btn => {
743
+ btn.addEventListener('click', () => {
744
+ activeTab = parseInt(btn.dataset.tab ?? '0', 10)
745
+ render()
746
+ })
747
+ })
748
+
749
+ // Re-attach copy listener
750
+ document.querySelectorAll<HTMLElement>('.av-copy-btn').forEach(btn => {
751
+ btn.addEventListener('click', () => copyToClipboard(btn.dataset.copy ?? ''))
752
+ })
753
+ }
754
+
755
+ render()
756
+ `;
757
+ }
758
+ // ---------------------------------------------------------------------------
759
+ // Viewer CSS (same for all frameworks)
760
+ // ---------------------------------------------------------------------------
761
+ function generateViewerCss() {
762
+ return `/* AgnosticUI Component Viewer — Chrome Styles */
763
+ *, *::before, *::after { box-sizing: border-box; }
764
+
765
+ /* Hard reset — override Vite template body styles (flex, place-items, text-align, etc.) */
766
+ html, body {
767
+ margin: 0;
768
+ padding: 0;
769
+ display: block;
770
+ place-items: unset;
771
+ text-align: unset;
772
+ min-width: unset;
773
+ max-width: unset;
774
+ font-family: var(--ag-font-family-base, system-ui, -apple-system, sans-serif);
775
+ background: var(--ag-background-primary, #ffffff);
776
+ color: var(--ag-text-primary, #111827);
777
+ font-size: 16px;
778
+ line-height: 1.5;
779
+ }
780
+
781
+ /* Override framework root container styles (#root, #app) */
782
+ #root, #app {
783
+ max-width: unset;
784
+ margin: unset;
785
+ padding: unset;
786
+ text-align: unset;
787
+ }
788
+
789
+ .av-layout {
790
+ display: flex;
791
+ flex-direction: column;
792
+ width: 100%;
793
+ min-height: 100vh;
794
+ }
795
+
796
+ .av-body {
797
+ display: flex;
798
+ flex: 1;
799
+ min-height: 0;
800
+ overflow: hidden;
801
+ }
802
+
803
+ /* Sidebar */
804
+ .av-sidebar {
805
+ width: 220px;
806
+ flex-shrink: 0;
807
+ border-right: 1px solid var(--ag-border-color, #e5e7eb);
808
+ overflow-y: auto;
809
+ padding: 1.25rem 1rem;
810
+ background: var(--ag-background-secondary, #f9fafb);
811
+ }
812
+
813
+ .av-overview {
814
+ font-size: 0.8125rem;
815
+ color: var(--ag-text-secondary, #6b7280);
816
+ margin-bottom: 1.25rem;
817
+ padding-bottom: 1rem;
818
+ border-bottom: 1px solid var(--ag-border-color, #e5e7eb);
819
+ line-height: 1.55;
820
+ }
821
+
822
+ .av-overview p { margin: 0 0 0.5rem; }
823
+ .av-overview p:last-child { margin-bottom: 0; }
824
+
825
+ .av-overview a {
826
+ color: var(--ag-color-primary, #2563eb);
827
+ text-decoration: none;
828
+ }
829
+ .av-overview a:hover { text-decoration: underline; }
830
+
831
+ .av-overview code {
832
+ font-family: var(--ag-font-family-mono, monospace);
833
+ font-size: 0.75rem;
834
+ background: var(--ag-background-muted, #f3f4f6);
835
+ padding: 0.125rem 0.25rem;
836
+ border-radius: 3px;
837
+ }
838
+
839
+ /* Nav */
840
+ .av-nav {
841
+ display: flex;
842
+ flex-direction: column;
843
+ gap: 2px;
844
+ }
845
+
846
+ .av-nav-item {
847
+ display: block;
848
+ width: 100%;
849
+ text-align: left;
850
+ padding: 0.375rem 0.625rem;
851
+ border: none;
852
+ border-radius: 0.25rem;
853
+ background: transparent;
854
+ color: var(--ag-text-secondary, #374151);
855
+ cursor: pointer;
856
+ font-size: 0.875rem;
857
+ font-family: inherit;
858
+ transition: background 0.1s, color 0.1s;
859
+ }
860
+
861
+ .av-nav-item:hover {
862
+ background: var(--ag-background-muted, #f3f4f6);
863
+ color: var(--ag-text-primary, #111827);
864
+ }
865
+
866
+ .av-nav-item--active {
867
+ background: var(--ag-color-primary-50, #eff6ff);
868
+ color: var(--ag-color-primary-700, #1d4ed8);
869
+ font-weight: 600;
870
+ }
871
+
872
+ /* Main content */
873
+ .av-main {
874
+ flex: 1;
875
+ padding: 2rem;
876
+ overflow-y: auto;
877
+ }
878
+
879
+ .av-component-title {
880
+ margin: 0 0 1.25rem;
881
+ font-size: 1.5rem;
882
+ font-weight: 700;
883
+ color: var(--ag-text-primary, #111827);
884
+ }
885
+
886
+ /* Plain header (used by Lit viewer; React/Vue use ag-header instead) */
887
+ .av-header {
888
+ display: flex;
889
+ align-items: center;
890
+ padding: 0 1.5rem;
891
+ height: 56px;
892
+ background: var(--ag-background-primary, #ffffff);
893
+ border-bottom: 1px solid var(--ag-border-color, #e5e7eb);
894
+ flex-shrink: 0;
895
+ }
896
+
897
+ /* Header brand */
898
+ .av-brand {
899
+ font-weight: 600;
900
+ font-size: 1rem;
901
+ }
902
+
903
+ /* Plain tabs (used by Lit viewer; React/Vue use ag-tabs instead) */
904
+ .av-tabs {
905
+ display: flex;
906
+ flex-direction: column;
907
+ }
908
+
909
+ .av-tab-list {
910
+ display: flex;
911
+ gap: 0;
912
+ border-bottom: 1px solid var(--ag-border-color, #e5e7eb);
913
+ margin-bottom: 1.25rem;
914
+ }
915
+
916
+ .av-tab {
917
+ padding: 0.5rem 1rem;
918
+ border: none;
919
+ border-bottom: 2px solid transparent;
920
+ background: transparent;
921
+ color: var(--ag-text-secondary, #6b7280);
922
+ cursor: pointer;
923
+ font-size: 0.875rem;
924
+ font-family: inherit;
925
+ font-weight: 500;
926
+ transition: color 0.1s, border-color 0.1s;
927
+ margin-bottom: -1px;
928
+ }
929
+
930
+ .av-tab:hover {
931
+ color: var(--ag-text-primary, #111827);
932
+ }
933
+
934
+ .av-tab--active {
935
+ color: var(--ag-color-primary-700, #1d4ed8);
936
+ border-bottom-color: var(--ag-color-primary-700, #1d4ed8);
937
+ }
938
+
939
+ /* Copy button (plain, used by Lit viewer) */
940
+ .av-copy-btn {
941
+ margin-top: 0.75rem;
942
+ padding: 0.375rem 0.75rem;
943
+ border: 1px solid var(--ag-border-color, #e5e7eb);
944
+ border-radius: 0.25rem;
945
+ background: var(--ag-background-primary, #ffffff);
946
+ color: var(--ag-text-secondary, #374151);
947
+ cursor: pointer;
948
+ font-size: 0.8125rem;
949
+ font-family: inherit;
950
+ }
951
+
952
+ .av-copy-btn:hover {
953
+ background: var(--ag-background-muted, #f3f4f6);
954
+ }
955
+
956
+ /* Preview area */
957
+ .av-preview {
958
+ padding: 2rem;
959
+ border: 1px solid var(--ag-border-color, #e5e7eb);
960
+ border-radius: 0.5rem;
961
+ background: var(--ag-background-primary, #ffffff);
962
+ min-height: 120px;
963
+ display: flex;
964
+ flex-wrap: wrap;
965
+ align-items: flex-start;
966
+ gap: 1rem;
967
+ }
968
+
969
+ /* Code block */
970
+ .av-code-block {
971
+ background: var(--ag-background-secondary, #f9fafb);
972
+ border: 1px solid var(--ag-border-color, #e5e7eb);
973
+ border-radius: 0.5rem;
974
+ padding: 1.25rem;
975
+ }
976
+
977
+ .av-code-block pre {
978
+ margin: 0 0 1rem;
979
+ overflow-x: auto;
980
+ }
981
+
982
+ .av-code-block code {
983
+ font-family: var(--ag-font-family-mono, 'Fira Code', 'Cascadia Code', monospace);
984
+ font-size: 0.875rem;
985
+ color: var(--ag-text-primary, #111827);
986
+ }
987
+
988
+ /* Info panel */
989
+ .av-info { font-size: 0.875rem; }
990
+
991
+ .av-info dl {
992
+ display: grid;
993
+ grid-template-columns: auto 1fr;
994
+ gap: 0.5rem 1.5rem;
995
+ margin: 0;
996
+ }
997
+
998
+ .av-info dt {
999
+ font-weight: 600;
1000
+ color: var(--ag-text-secondary, #6b7280);
1001
+ }
1002
+
1003
+ .av-info dd {
1004
+ margin: 0;
1005
+ color: var(--ag-text-primary, #374151);
1006
+ word-break: break-all;
1007
+ }
1008
+
1009
+ /* Complex component placeholder */
1010
+ .av-complex-note {
1011
+ color: var(--ag-text-secondary, #6b7280);
1012
+ font-size: 0.875rem;
1013
+ font-style: italic;
1014
+ }
1015
+ `;
1016
+ }
1017
+ // ---------------------------------------------------------------------------
1018
+ // Public: orchestrator
1019
+ // ---------------------------------------------------------------------------
1020
+ export async function generateViewerApp(config, viewerPath, cwd) {
1021
+ const componentsAbsPath = path.resolve(cwd, config.paths.components);
1022
+ const refLibAbsPath = path.resolve(cwd, config.paths.reference);
1023
+ const hasTheme = detectThemeFile(componentsAbsPath);
1024
+ // Extract component metadata
1025
+ const installedComponents = {};
1026
+ for (const [name, entry] of Object.entries(config.components)) {
1027
+ installedComponents[name] = {
1028
+ version: entry.version,
1029
+ added: entry.added,
1030
+ files: entry.files,
1031
+ };
1032
+ }
1033
+ const { framework } = config;
1034
+ const srcPath = path.join(viewerPath, 'src');
1035
+ await ensureDir(srcPath);
1036
+ // Root files
1037
+ await writeFile(path.join(viewerPath, 'package.json'), generatePackageJson(framework));
1038
+ await writeFile(path.join(viewerPath, 'tsconfig.json'), generateTsConfig(framework));
1039
+ await writeFile(path.join(viewerPath, 'vite.config.ts'), generateViteConfig(framework, componentsAbsPath, refLibAbsPath));
1040
+ await writeFile(path.join(viewerPath, 'index.html'), generateIndexHtml(framework));
1041
+ // src/ files
1042
+ let mainContent;
1043
+ let appContent;
1044
+ let appFileName;
1045
+ if (framework === 'react') {
1046
+ mainContent = generateReactMain();
1047
+ appContent = generateReactApp(installedComponents, config.paths.components, hasTheme, componentsAbsPath);
1048
+ appFileName = 'App.tsx';
1049
+ await writeFile(path.join(srcPath, 'main.tsx'), mainContent);
1050
+ }
1051
+ else if (framework === 'vue') {
1052
+ mainContent = generateVueMain();
1053
+ appContent = generateVueApp(installedComponents, config.paths.components, hasTheme, componentsAbsPath);
1054
+ appFileName = 'App.vue';
1055
+ await writeFile(path.join(srcPath, 'main.ts'), mainContent);
1056
+ }
1057
+ else {
1058
+ // lit / svelte / other
1059
+ mainContent = generateLitMain();
1060
+ appContent = generateLitApp(installedComponents, config.paths.components, hasTheme);
1061
+ appFileName = 'App.ts';
1062
+ await writeFile(path.join(srcPath, 'main.ts'), mainContent);
1063
+ }
1064
+ await writeFile(path.join(srcPath, appFileName), appContent);
1065
+ await writeFile(path.join(srcPath, 'viewer.css'), generateViewerCss());
1066
+ }
1067
+ //# sourceMappingURL=viewer.js.map