@fragments-sdk/cli 0.15.3 → 0.15.5
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.js +2 -2
- package/dist/bin.js.map +1 -1
- package/dist/{create-JVAU3YKN.js → create-AC2PMGBF.js} +394 -145
- package/dist/create-AC2PMGBF.js.map +1 -0
- package/package.json +18 -18
- package/src/bin.ts +1 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
- package/src/commands/__tests__/create.test.ts +10 -3
- package/src/commands/create.ts +378 -83
- package/src/theme/seed-presets.ts +104 -0
- package/dist/create-JVAU3YKN.js.map +0 -1
package/src/commands/create.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { BRAND } from '../core/index.js';
|
|
|
13
13
|
import { decompressTheme } from '../theme/serializer.js';
|
|
14
14
|
import { generateCssTokens, generateScssTokens } from '../theme/generator.js';
|
|
15
15
|
import type { ThemeConfig } from '../theme/types.js';
|
|
16
|
+
import { SEED_PRESETS, generateSeedScss } from '../theme/seed-presets.js';
|
|
17
|
+
import type { SeedConfig } from '../theme/seed-presets.js';
|
|
16
18
|
|
|
17
19
|
// ============================================
|
|
18
20
|
// Font Helpers
|
|
@@ -47,6 +49,7 @@ export interface CreateOptions {
|
|
|
47
49
|
theme?: string;
|
|
48
50
|
preset?: string;
|
|
49
51
|
brand?: string;
|
|
52
|
+
seeds?: SeedConfig;
|
|
50
53
|
scss?: boolean;
|
|
51
54
|
mcp?: boolean;
|
|
52
55
|
yes?: boolean;
|
|
@@ -150,7 +153,7 @@ async function resolveTheme(options: CreateOptions): Promise<ThemeConfig | null>
|
|
|
150
153
|
// Templates
|
|
151
154
|
// ============================================
|
|
152
155
|
|
|
153
|
-
export function generateNextjsLayout(themePath: string, theme?: ThemeConfig): string {
|
|
156
|
+
export function generateNextjsLayout(themePath: string, theme?: ThemeConfig, useSeedPath = false): string {
|
|
154
157
|
const fontName = theme?.typography?.fontSans ? extractFontFamily(theme.typography.fontSans) : null;
|
|
155
158
|
const fontUrl = fontName ? googleFontsUrl(fontName) : null;
|
|
156
159
|
|
|
@@ -165,9 +168,14 @@ export function generateNextjsLayout(themePath: string, theme?: ThemeConfig): st
|
|
|
165
168
|
: ` <html lang="en" suppressHydrationWarning>
|
|
166
169
|
<body>`;
|
|
167
170
|
|
|
171
|
+
// When using seed path, theme.scss already @use's the styles entry point.
|
|
172
|
+
// A bare import would compile with default seeds, defeating customization.
|
|
173
|
+
const stylesImport = useSeedPath
|
|
174
|
+
? `import '${themePath}';`
|
|
175
|
+
: `import '@fragments-sdk/ui/styles';\nimport '${themePath}';`;
|
|
176
|
+
|
|
168
177
|
return `import type { Metadata } from 'next';
|
|
169
|
-
|
|
170
|
-
import '${themePath}';
|
|
178
|
+
${stylesImport}
|
|
171
179
|
import { Providers } from './providers';
|
|
172
180
|
|
|
173
181
|
export const metadata: Metadata = {
|
|
@@ -211,38 +219,255 @@ export function Providers({ children }: { children: ReactNode }) {
|
|
|
211
219
|
export function generateNextjsPage(): string {
|
|
212
220
|
return `'use client';
|
|
213
221
|
|
|
214
|
-
import {
|
|
222
|
+
import { useState } from 'react';
|
|
223
|
+
import {
|
|
224
|
+
Avatar,
|
|
225
|
+
Badge,
|
|
226
|
+
Button,
|
|
227
|
+
Card,
|
|
228
|
+
Checkbox,
|
|
229
|
+
Input,
|
|
230
|
+
Progress,
|
|
231
|
+
Separator,
|
|
232
|
+
Stack,
|
|
233
|
+
Switch,
|
|
234
|
+
Tabs,
|
|
235
|
+
Text,
|
|
236
|
+
Tooltip,
|
|
237
|
+
} from '@fragments-sdk/ui';
|
|
238
|
+
import styles from './page.module.css';
|
|
239
|
+
|
|
240
|
+
function StatCard({ label, value, change }: { label: string; value: string; change: string }) {
|
|
241
|
+
const isPositive = change.startsWith('+');
|
|
242
|
+
return (
|
|
243
|
+
<Card>
|
|
244
|
+
<Card.Body>
|
|
245
|
+
<Stack gap="xs">
|
|
246
|
+
<Text size="sm" color="secondary">{label}</Text>
|
|
247
|
+
<Text size="2xl" weight="semibold">{value}</Text>
|
|
248
|
+
<Badge variant={isPositive ? 'success' : 'error'} size="sm">{change}</Badge>
|
|
249
|
+
</Stack>
|
|
250
|
+
</Card.Body>
|
|
251
|
+
</Card>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function TaskItem({ title, done }: { title: string; done?: boolean }) {
|
|
256
|
+
const [checked, setChecked] = useState(done ?? false);
|
|
257
|
+
return (
|
|
258
|
+
<Stack direction="row" align="center" gap="sm">
|
|
259
|
+
<Checkbox checked={checked} onChange={() => setChecked(!checked)} />
|
|
260
|
+
<Text size="sm" className={checked ? styles.done : undefined}>{title}</Text>
|
|
261
|
+
</Stack>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
215
264
|
|
|
216
265
|
export default function Home() {
|
|
266
|
+
const [notifications, setNotifications] = useState(true);
|
|
267
|
+
|
|
217
268
|
return (
|
|
218
|
-
<main
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
269
|
+
<main className={styles.page}>
|
|
270
|
+
{/* Hero */}
|
|
271
|
+
<Stack align="center" gap="md" className={styles.hero}>
|
|
272
|
+
<Badge variant="info">Powered by Fragments UI</Badge>
|
|
273
|
+
<Text as="h1" size="2xl" weight="bold">Your app is ready</Text>
|
|
274
|
+
<Text size="lg" color="secondary" className={styles.heroDescription}>
|
|
275
|
+
This page showcases your custom theme and components.
|
|
276
|
+
Edit <code>src/app/page.tsx</code> to start building.
|
|
277
|
+
</Text>
|
|
278
|
+
<Stack direction="row" gap="sm">
|
|
279
|
+
<Button size="lg">Start building</Button>
|
|
280
|
+
<Button size="lg" variant="secondary">Read the docs</Button>
|
|
281
|
+
</Stack>
|
|
282
|
+
</Stack>
|
|
283
|
+
|
|
284
|
+
<Separator />
|
|
285
|
+
|
|
286
|
+
{/* Stats */}
|
|
287
|
+
<div className={styles.statsGrid}>
|
|
288
|
+
<StatCard label="Components" value="66" change="+12 this month" />
|
|
289
|
+
<StatCard label="Design tokens" value="80" change="+5 new" />
|
|
290
|
+
<StatCard label="Accessibility" value="98%" change="+2.4%" />
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{/* Content */}
|
|
294
|
+
<div className={styles.contentGrid}>
|
|
295
|
+
<Card>
|
|
296
|
+
<Card.Header>
|
|
297
|
+
<Card.Title>Getting started</Card.Title>
|
|
298
|
+
<Card.Description>Pick up where you left off</Card.Description>
|
|
299
|
+
</Card.Header>
|
|
300
|
+
<Card.Body>
|
|
301
|
+
<Tabs defaultValue="tasks">
|
|
302
|
+
<Tabs.List>
|
|
303
|
+
<Tabs.Tab value="tasks">Tasks</Tabs.Tab>
|
|
304
|
+
<Tabs.Tab value="progress">Progress</Tabs.Tab>
|
|
305
|
+
</Tabs.List>
|
|
306
|
+
<Tabs.Panel value="tasks">
|
|
307
|
+
<Stack gap="sm" className={styles.tabContent}>
|
|
308
|
+
<TaskItem title="Install Fragments UI" done />
|
|
309
|
+
<TaskItem title="Configure your theme" done />
|
|
310
|
+
<TaskItem title="Build your first page" />
|
|
311
|
+
<TaskItem title="Set up MCP for AI tooling" />
|
|
312
|
+
<TaskItem title="Deploy to production" />
|
|
313
|
+
</Stack>
|
|
314
|
+
</Tabs.Panel>
|
|
315
|
+
<Tabs.Panel value="progress">
|
|
316
|
+
<Stack gap="md" className={styles.tabContent}>
|
|
317
|
+
<Stack gap="xs">
|
|
318
|
+
<Stack direction="row" justify="between">
|
|
319
|
+
<Text size="sm">Setup</Text>
|
|
320
|
+
<Text size="sm" color="secondary">2 of 5</Text>
|
|
321
|
+
</Stack>
|
|
322
|
+
<Progress value={40} />
|
|
323
|
+
</Stack>
|
|
324
|
+
<Text size="sm" color="secondary">
|
|
325
|
+
Complete the remaining tasks to finish setting up your project.
|
|
326
|
+
</Text>
|
|
327
|
+
</Stack>
|
|
328
|
+
</Tabs.Panel>
|
|
329
|
+
</Tabs>
|
|
330
|
+
</Card.Body>
|
|
331
|
+
</Card>
|
|
332
|
+
|
|
333
|
+
<Card>
|
|
334
|
+
<Card.Header>
|
|
335
|
+
<Card.Title>Settings</Card.Title>
|
|
336
|
+
<Card.Description>Manage your preferences</Card.Description>
|
|
337
|
+
</Card.Header>
|
|
338
|
+
<Card.Body>
|
|
339
|
+
<Stack gap="md">
|
|
340
|
+
<Input placeholder="Search settings..." />
|
|
341
|
+
<Stack direction="row" align="center" justify="between">
|
|
342
|
+
<Stack gap="xs">
|
|
343
|
+
<Text size="sm" weight="medium">Notifications</Text>
|
|
344
|
+
<Text size="xs" color="secondary">Receive alerts for updates</Text>
|
|
345
|
+
</Stack>
|
|
346
|
+
<Switch checked={notifications} onChange={() => setNotifications(!notifications)} />
|
|
347
|
+
</Stack>
|
|
348
|
+
<Separator />
|
|
349
|
+
<Stack direction="row" align="center" justify="between">
|
|
350
|
+
<Stack gap="xs">
|
|
351
|
+
<Text size="sm" weight="medium">Theme</Text>
|
|
352
|
+
<Text size="xs" color="secondary">System preference detected</Text>
|
|
353
|
+
</Stack>
|
|
354
|
+
<Badge variant="default" size="sm">Auto</Badge>
|
|
355
|
+
</Stack>
|
|
356
|
+
<Separator />
|
|
357
|
+
<Stack direction="row" gap="sm" align="center">
|
|
358
|
+
<Avatar name="You" size="sm" />
|
|
359
|
+
<Stack gap="xs" className={styles.flex1}>
|
|
360
|
+
<Text size="sm" weight="medium">Your account</Text>
|
|
361
|
+
<Text size="xs" color="secondary">Manage profile and billing</Text>
|
|
362
|
+
</Stack>
|
|
363
|
+
<Tooltip content="Go to account settings">
|
|
364
|
+
<Button variant="ghost" size="sm">Manage</Button>
|
|
365
|
+
</Tooltip>
|
|
366
|
+
</Stack>
|
|
230
367
|
</Stack>
|
|
231
|
-
</
|
|
232
|
-
</Card
|
|
233
|
-
</
|
|
368
|
+
</Card.Body>
|
|
369
|
+
</Card>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
{/* Footer */}
|
|
373
|
+
<Text size="sm" color="secondary" className={styles.footer}>
|
|
374
|
+
Built with{' '}
|
|
375
|
+
<a href="https://usefragments.com" className={styles.link}>Fragments UI</a>
|
|
376
|
+
{' '}— 66 components, 80 design tokens, accessible by default.
|
|
377
|
+
</Text>
|
|
234
378
|
</main>
|
|
235
379
|
);
|
|
236
380
|
}
|
|
237
381
|
`;
|
|
238
382
|
}
|
|
239
383
|
|
|
240
|
-
function
|
|
384
|
+
export function generatePageCssModule(): string {
|
|
385
|
+
return `.page {
|
|
386
|
+
min-height: 100vh;
|
|
387
|
+
display: flex;
|
|
388
|
+
flex-direction: column;
|
|
389
|
+
align-items: center;
|
|
390
|
+
padding: var(--fui-space-6, 3rem) var(--fui-space-3, 1.5rem);
|
|
391
|
+
gap: var(--fui-space-4, 2rem);
|
|
392
|
+
max-width: 960px;
|
|
393
|
+
margin: 0 auto;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.hero {
|
|
397
|
+
text-align: center;
|
|
398
|
+
padding-top: var(--fui-space-4, 2rem);
|
|
399
|
+
padding-bottom: var(--fui-space-2, 1rem);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.heroDescription {
|
|
403
|
+
max-width: 480px;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.heroDescription code {
|
|
407
|
+
font-size: 0.875em;
|
|
408
|
+
font-family: var(--fui-font-mono, monospace);
|
|
409
|
+
background: var(--fui-bg-secondary);
|
|
410
|
+
padding: 0.125em 0.375em;
|
|
411
|
+
border-radius: var(--fui-radius-sm, 0.25rem);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.statsGrid {
|
|
415
|
+
display: grid;
|
|
416
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
417
|
+
gap: var(--fui-space-2, 1rem);
|
|
418
|
+
width: 100%;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.contentGrid {
|
|
422
|
+
display: grid;
|
|
423
|
+
grid-template-columns: 1fr 1fr;
|
|
424
|
+
gap: var(--fui-space-3, 1.5rem);
|
|
425
|
+
width: 100%;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@media (max-width: 768px) {
|
|
429
|
+
.contentGrid {
|
|
430
|
+
grid-template-columns: 1fr;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.tabContent {
|
|
435
|
+
padding-top: var(--fui-space-1, 0.75rem);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.done {
|
|
439
|
+
text-decoration: line-through;
|
|
440
|
+
opacity: 0.5;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.flex1 {
|
|
444
|
+
flex: 1;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.footer {
|
|
448
|
+
padding-top: var(--fui-space-2, 1rem);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.link {
|
|
452
|
+
color: inherit;
|
|
453
|
+
text-decoration: underline;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.link:hover {
|
|
457
|
+
color: var(--fui-color-accent);
|
|
458
|
+
}
|
|
459
|
+
`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function generateViteMain(themePath: string, useSeedPath = false): string {
|
|
463
|
+
const stylesImport = useSeedPath
|
|
464
|
+
? `import '${themePath}';`
|
|
465
|
+
: `import '@fragments-sdk/ui/styles';\nimport '${themePath}';`;
|
|
466
|
+
|
|
241
467
|
return `import { StrictMode } from 'react';
|
|
242
468
|
import { createRoot } from 'react-dom/client';
|
|
243
469
|
import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';
|
|
244
|
-
|
|
245
|
-
import '${themePath}';
|
|
470
|
+
${stylesImport}
|
|
246
471
|
import App from './App';
|
|
247
472
|
|
|
248
473
|
createRoot(document.getElementById('root')!).render(
|
|
@@ -260,32 +485,13 @@ createRoot(document.getElementById('root')!).render(
|
|
|
260
485
|
}
|
|
261
486
|
|
|
262
487
|
function generateViteApp(): string {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
<Card.Title>Welcome to Fragments</Card.Title>
|
|
271
|
-
<Card.Description>Your app is ready. Start building something great.</Card.Description>
|
|
272
|
-
</Card.Header>
|
|
273
|
-
<Card.Body>
|
|
274
|
-
<Stack gap={3}>
|
|
275
|
-
<Input placeholder="Enter something..." />
|
|
276
|
-
<Stack direction="row" gap={2}>
|
|
277
|
-
<Button>Get Started</Button>
|
|
278
|
-
<Button variant="secondary">Learn More</Button>
|
|
279
|
-
</Stack>
|
|
280
|
-
</Stack>
|
|
281
|
-
</Card.Body>
|
|
282
|
-
</Card>
|
|
283
|
-
</main>
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export default App;
|
|
288
|
-
`;
|
|
488
|
+
// Same content as Next.js page but with App.module.css import
|
|
489
|
+
return generateNextjsPage()
|
|
490
|
+
.replace("'use client';\n\n", '')
|
|
491
|
+
.replace("import styles from './page.module.css';", "import styles from './App.module.css';")
|
|
492
|
+
.replace('src/app/page.tsx', 'src/App.tsx')
|
|
493
|
+
.replace('export default function Home()', 'function App()')
|
|
494
|
+
+ '\nexport default App;\n';
|
|
289
495
|
}
|
|
290
496
|
|
|
291
497
|
function injectFontIntoViteHtml(projectDir: string, theme: ThemeConfig): void {
|
|
@@ -348,6 +554,59 @@ async function promptIfMissing(options: CreateOptions): Promise<CreateOptions> {
|
|
|
348
554
|
resolved.template = resolved.template || 'nextjs';
|
|
349
555
|
resolved.packageManager = resolved.packageManager || detectPackageManager();
|
|
350
556
|
|
|
557
|
+
// Theme selection — only when no explicit theme flags are passed
|
|
558
|
+
if (!resolved.preset && !resolved.theme && !resolved.brand && !resolved.yes) {
|
|
559
|
+
try {
|
|
560
|
+
const { select, input } = await import('@inquirer/prompts');
|
|
561
|
+
|
|
562
|
+
const choices = SEED_PRESETS.map((p) => ({
|
|
563
|
+
name: `${p.name} (${p.description})`,
|
|
564
|
+
value: p.name.toLowerCase(),
|
|
565
|
+
}));
|
|
566
|
+
choices.push({ name: 'Custom (pick color + palette)', value: 'custom' });
|
|
567
|
+
|
|
568
|
+
const selected = await select({
|
|
569
|
+
message: 'Theme:',
|
|
570
|
+
choices,
|
|
571
|
+
default: 'stone',
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
if (selected === 'custom') {
|
|
575
|
+
const brandColor = await input({
|
|
576
|
+
message: 'Brand color (hex):',
|
|
577
|
+
default: '#6366f1',
|
|
578
|
+
validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) || 'Enter a valid hex color (e.g., #6366f1)',
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const neutral = await select({
|
|
582
|
+
message: 'Neutral palette:',
|
|
583
|
+
choices: [
|
|
584
|
+
{ name: 'Stone (warm gray)', value: 'stone' as const },
|
|
585
|
+
{ name: 'Sand (warm beige)', value: 'sand' as const },
|
|
586
|
+
{ name: 'Ice (cool blue)', value: 'ice' as const },
|
|
587
|
+
{ name: 'Earth (olive green)', value: 'earth' as const },
|
|
588
|
+
{ name: 'Fire (warm orange)', value: 'fire' as const },
|
|
589
|
+
],
|
|
590
|
+
default: 'stone',
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
resolved.seeds = {
|
|
594
|
+
brand: brandColor,
|
|
595
|
+
neutral,
|
|
596
|
+
density: 'default',
|
|
597
|
+
radiusStyle: 'default',
|
|
598
|
+
};
|
|
599
|
+
} else {
|
|
600
|
+
const preset = SEED_PRESETS.find((p) => p.name.toLowerCase() === selected);
|
|
601
|
+
if (preset) {
|
|
602
|
+
resolved.seeds = { ...preset.seeds };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
// User cancelled or prompt failed — fall through to default theme
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
351
610
|
return resolved;
|
|
352
611
|
}
|
|
353
612
|
|
|
@@ -367,33 +626,48 @@ function scaffoldFramework(name: string, template: string, pm: string): void {
|
|
|
367
626
|
}
|
|
368
627
|
}
|
|
369
628
|
|
|
370
|
-
function installDeps(projectDir: string, pm: string
|
|
629
|
+
function installDeps(projectDir: string, pm: string): void {
|
|
371
630
|
console.log(pc.cyan('\nInstalling Fragments UI...\n'));
|
|
372
631
|
|
|
373
632
|
const install = getInstallCommand(pm);
|
|
633
|
+
const devInstall = getDevInstallCommand(pm);
|
|
634
|
+
// sass is required — without it, @fragments-sdk/ui/styles resolves to the
|
|
635
|
+
// compiled CSS which is missing the :root token definitions and body resets.
|
|
636
|
+
// The SCSS entry (used when sass is present) includes the full token layer.
|
|
374
637
|
execSync(`${install} @fragments-sdk/ui`, { cwd: projectDir, stdio: 'inherit' });
|
|
375
|
-
|
|
376
|
-
if (scss) {
|
|
377
|
-
const devInstall = getDevInstallCommand(pm);
|
|
378
|
-
execSync(`${devInstall} sass`, { cwd: projectDir, stdio: 'inherit' });
|
|
379
|
-
}
|
|
638
|
+
execSync(`${devInstall} sass`, { cwd: projectDir, stdio: 'inherit' });
|
|
380
639
|
}
|
|
381
640
|
|
|
382
|
-
function writeThemeFile(
|
|
641
|
+
function writeThemeFile(
|
|
642
|
+
projectDir: string,
|
|
643
|
+
template: string,
|
|
644
|
+
source: { kind: 'seeds'; seeds: SeedConfig } | { kind: 'css'; theme: ThemeConfig },
|
|
645
|
+
): string {
|
|
383
646
|
const stylesDir = join(projectDir, 'src', 'styles');
|
|
384
647
|
mkdirSync(stylesDir, { recursive: true });
|
|
385
648
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
649
|
+
let content: string;
|
|
650
|
+
|
|
651
|
+
if (source.kind === 'seeds') {
|
|
652
|
+
// Seed path: clean @use ... with() — the SCSS derivation system does the rest
|
|
653
|
+
content = generateSeedScss(source.seeds);
|
|
654
|
+
} else {
|
|
655
|
+
// CSS custom property path: for themes from the website / API / --brand flag
|
|
656
|
+
const cssContent = generateCssTokens(source.theme);
|
|
657
|
+
content = `// Fragments UI — tokens, resets, dark mode
|
|
658
|
+
@use '@fragments-sdk/ui/styles';
|
|
659
|
+
|
|
660
|
+
// Theme overrides (generated from your preset)
|
|
661
|
+
${cssContent}`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const filePath = join(stylesDir, 'theme.scss');
|
|
389
665
|
writeFileSync(filePath, content, 'utf-8');
|
|
390
666
|
|
|
391
|
-
// Next.js layout is at src/app/layout.tsx → relative path is ../styles/theme.ext
|
|
392
|
-
// Vite main is at src/main.tsx → relative path is ./styles/theme.ext
|
|
393
667
|
if (template === 'nextjs') {
|
|
394
|
-
return
|
|
668
|
+
return '../styles/theme.scss';
|
|
395
669
|
}
|
|
396
|
-
return
|
|
670
|
+
return './styles/theme.scss';
|
|
397
671
|
}
|
|
398
672
|
|
|
399
673
|
export function addNextTranspilePackages(projectDir: string): void {
|
|
@@ -443,24 +717,27 @@ export function addNextTranspilePackages(projectDir: string): void {
|
|
|
443
717
|
}
|
|
444
718
|
}
|
|
445
719
|
|
|
446
|
-
function rewriteAppFiles(
|
|
720
|
+
function rewriteAppFiles(
|
|
721
|
+
projectDir: string,
|
|
722
|
+
template: string,
|
|
723
|
+
themePath: string,
|
|
724
|
+
theme: ThemeConfig | null,
|
|
725
|
+
useSeedPath: boolean,
|
|
726
|
+
): void {
|
|
447
727
|
if (template === 'nextjs') {
|
|
448
728
|
// Rewrite layout.tsx
|
|
449
729
|
const layoutPath = join(projectDir, 'src', 'app', 'layout.tsx');
|
|
450
|
-
writeFileSync(layoutPath, generateNextjsLayout(themePath, theme), 'utf-8');
|
|
730
|
+
writeFileSync(layoutPath, generateNextjsLayout(themePath, theme ?? undefined, useSeedPath), 'utf-8');
|
|
451
731
|
|
|
452
732
|
const providersPath = join(projectDir, 'src', 'app', 'providers.tsx');
|
|
453
733
|
writeFileSync(providersPath, generateNextjsProviders(), 'utf-8');
|
|
454
734
|
|
|
455
|
-
// Rewrite page.tsx
|
|
735
|
+
// Rewrite page.tsx + CSS module
|
|
456
736
|
const pagePath = join(projectDir, 'src', 'app', 'page.tsx');
|
|
457
737
|
writeFileSync(pagePath, generateNextjsPage(), 'utf-8');
|
|
458
738
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
unlinkSync(moduleCssPath);
|
|
463
|
-
} catch { /* doesn't exist, that's fine */ }
|
|
739
|
+
const pageCssPath = join(projectDir, 'src', 'app', 'page.module.css');
|
|
740
|
+
writeFileSync(pageCssPath, generatePageCssModule(), 'utf-8');
|
|
464
741
|
|
|
465
742
|
// Remove globals.css if it exists (we use our own theme)
|
|
466
743
|
const globalsCssPath = join(projectDir, 'src', 'app', 'globals.css');
|
|
@@ -472,11 +749,14 @@ function rewriteAppFiles(projectDir: string, template: string, themePath: string
|
|
|
472
749
|
} else {
|
|
473
750
|
// Vite: rewrite main.tsx and App.tsx
|
|
474
751
|
const mainPath = join(projectDir, 'src', 'main.tsx');
|
|
475
|
-
writeFileSync(mainPath, generateViteMain(themePath), 'utf-8');
|
|
752
|
+
writeFileSync(mainPath, generateViteMain(themePath, useSeedPath), 'utf-8');
|
|
476
753
|
|
|
477
754
|
const appPath = join(projectDir, 'src', 'App.tsx');
|
|
478
755
|
writeFileSync(appPath, generateViteApp(), 'utf-8');
|
|
479
756
|
|
|
757
|
+
const appCssModulePath = join(projectDir, 'src', 'App.module.css');
|
|
758
|
+
writeFileSync(appCssModulePath, generatePageCssModule(), 'utf-8');
|
|
759
|
+
|
|
480
760
|
// Clean up default Vite files
|
|
481
761
|
for (const file of ['src/App.css', 'src/index.css']) {
|
|
482
762
|
try {
|
|
@@ -484,7 +764,10 @@ function rewriteAppFiles(projectDir: string, template: string, themePath: string
|
|
|
484
764
|
} catch { /* doesn't exist */ }
|
|
485
765
|
}
|
|
486
766
|
|
|
487
|
-
|
|
767
|
+
// Font injection only applies to CSS path (seed path has no typography config)
|
|
768
|
+
if (theme) {
|
|
769
|
+
injectFontIntoViteHtml(projectDir, theme);
|
|
770
|
+
}
|
|
488
771
|
}
|
|
489
772
|
}
|
|
490
773
|
|
|
@@ -528,6 +811,7 @@ export async function create(options: CreateOptions): Promise<CreateResult> {
|
|
|
528
811
|
const name = resolved.name!;
|
|
529
812
|
const template = resolved.template!;
|
|
530
813
|
const pm = resolved.packageManager!;
|
|
814
|
+
const hasSeedPath = !!resolved.seeds;
|
|
531
815
|
|
|
532
816
|
// 2. Validate
|
|
533
817
|
if (!isValidProjectName(name)) {
|
|
@@ -539,10 +823,13 @@ export async function create(options: CreateOptions): Promise<CreateResult> {
|
|
|
539
823
|
return { success: false, error: `Directory "${name}" already exists.` };
|
|
540
824
|
}
|
|
541
825
|
|
|
542
|
-
// 3. Resolve theme
|
|
543
|
-
|
|
544
|
-
if (!
|
|
545
|
-
|
|
826
|
+
// 3. Resolve theme (skip when using seed path — SCSS handles derivation)
|
|
827
|
+
let theme: ThemeConfig | null = null;
|
|
828
|
+
if (!hasSeedPath) {
|
|
829
|
+
theme = await resolveTheme(resolved);
|
|
830
|
+
if (!theme) {
|
|
831
|
+
return { success: false, error: 'Invalid theme configuration.' };
|
|
832
|
+
}
|
|
546
833
|
}
|
|
547
834
|
|
|
548
835
|
// 4. Scaffold framework
|
|
@@ -553,16 +840,18 @@ export async function create(options: CreateOptions): Promise<CreateResult> {
|
|
|
553
840
|
}
|
|
554
841
|
|
|
555
842
|
// 5. Install deps
|
|
556
|
-
installDeps(projectDir, pm
|
|
843
|
+
installDeps(projectDir, pm);
|
|
557
844
|
|
|
558
845
|
// 6. Write theme tokens
|
|
559
|
-
const themePath =
|
|
846
|
+
const themePath = hasSeedPath
|
|
847
|
+
? writeThemeFile(projectDir, template, { kind: 'seeds', seeds: resolved.seeds! })
|
|
848
|
+
: writeThemeFile(projectDir, template, { kind: 'css', theme: theme! });
|
|
560
849
|
|
|
561
850
|
// 7. Rewrite app files with providers + theme import
|
|
562
|
-
rewriteAppFiles(projectDir, template, themePath, theme);
|
|
851
|
+
rewriteAppFiles(projectDir, template, themePath, theme, hasSeedPath);
|
|
563
852
|
|
|
564
|
-
// 8. MCP config
|
|
565
|
-
if (resolved.mcp) {
|
|
853
|
+
// 8. MCP config (default-on; pass --no-mcp to skip)
|
|
854
|
+
if (resolved.mcp !== false) {
|
|
566
855
|
configureMcp(projectDir);
|
|
567
856
|
console.log(pc.dim(' Configured MCP server for AI tooling'));
|
|
568
857
|
}
|
|
@@ -581,9 +870,15 @@ export async function create(options: CreateOptions): Promise<CreateResult> {
|
|
|
581
870
|
console.log(` ${pc.cyan(run)} dev`);
|
|
582
871
|
console.log('');
|
|
583
872
|
|
|
584
|
-
if (
|
|
873
|
+
if (hasSeedPath) {
|
|
874
|
+
const seeds = resolved.seeds!;
|
|
875
|
+
const presetName = seeds.neutral.charAt(0).toUpperCase() + seeds.neutral.slice(1);
|
|
876
|
+
console.log(pc.dim(` Theme: ${presetName} palette with ${seeds.brand} brand`));
|
|
877
|
+
console.log(pc.dim(` Edit src/styles/theme.scss to customize seed values.`));
|
|
878
|
+
console.log(pc.dim(` Fine-tune: ${pc.cyan('fragments init --configure')}\n`));
|
|
879
|
+
} else if (theme && theme.name !== 'default') {
|
|
585
880
|
console.log(pc.dim(` Theme "${theme.name}" applied with full token set.`));
|
|
586
|
-
console.log(pc.dim(` Edit src/styles/theme
|
|
881
|
+
console.log(pc.dim(` Edit src/styles/theme.scss to customize.\n`));
|
|
587
882
|
}
|
|
588
883
|
|
|
589
884
|
return { success: true, projectDir };
|