@fragments-sdk/core 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/index.js ADDED
@@ -0,0 +1,1431 @@
1
+ // src/constants.ts
2
+ var BRAND = {
3
+ /** Display name (e.g., "Fragments") */
4
+ name: "Fragments",
5
+ /** Lowercase name for file paths and CLI (e.g., "fragments") */
6
+ nameLower: "fragments",
7
+ /** File extension for fragment definition files (e.g., ".fragment.tsx") */
8
+ fileExtension: ".fragment.tsx",
9
+ /** Legacy file extension for segments (still supported for migration) */
10
+ legacyFileExtension: ".segment.tsx",
11
+ /** JSON file extension for compiled output */
12
+ jsonExtension: ".fragment.json",
13
+ /** Default output file name (e.g., "fragments.json") */
14
+ outFile: "fragments.json",
15
+ /** Config file name (e.g., "fragments.config.ts") */
16
+ configFile: "fragments.config.ts",
17
+ /** Legacy config file name (still supported for migration) */
18
+ legacyConfigFile: "segments.config.ts",
19
+ /** CLI command name (e.g., "fragments") */
20
+ cliCommand: "fragments",
21
+ /** Package scope (e.g., "@fragments") */
22
+ packageScope: "@fragments",
23
+ /** Directory for storing fragments, registry, and cache */
24
+ dataDir: ".fragments",
25
+ /** Components subdirectory within .fragments/ */
26
+ componentsDir: "components",
27
+ /** Registry file name */
28
+ registryFile: "registry.json",
29
+ /** Context file name (AI-ready markdown) */
30
+ contextFile: "context.md",
31
+ /** Screenshots subdirectory */
32
+ screenshotsDir: "screenshots",
33
+ /** Cache subdirectory (gitignored) */
34
+ cacheDir: "cache",
35
+ /** Diff output subdirectory (gitignored) */
36
+ diffDir: "diff",
37
+ /** Manifest filename */
38
+ manifestFile: "manifest.json",
39
+ /** Prefix for localStorage keys (e.g., "fragments-") */
40
+ storagePrefix: "fragments-",
41
+ /** Static viewer HTML file name */
42
+ viewerHtmlFile: "fragments-viewer.html",
43
+ /** MCP tool name prefix (e.g., "fragments_") */
44
+ mcpToolPrefix: "fragments_",
45
+ /** File extension for block definition files */
46
+ blockFileExtension: ".block.ts",
47
+ /** @deprecated Use blockFileExtension instead */
48
+ recipeFileExtension: ".recipe.ts",
49
+ /** Vite plugin namespace */
50
+ vitePluginNamespace: "fragments-core-shim"
51
+ };
52
+ var DEFAULTS = {
53
+ /** Default viewport dimensions */
54
+ viewport: {
55
+ width: 1280,
56
+ height: 800
57
+ },
58
+ /** Default diff threshold (percentage) */
59
+ diffThreshold: 5,
60
+ /** Browser pool size */
61
+ poolSize: 3,
62
+ /** Idle timeout before browser shutdown (ms) - 5 minutes */
63
+ idleTimeoutMs: 5 * 60 * 1e3,
64
+ /** Delay after render before capture (ms) */
65
+ captureDelayMs: 100,
66
+ /** Font loading timeout (ms) */
67
+ fontTimeoutMs: 3e3,
68
+ /** Default theme */
69
+ theme: "light",
70
+ /** Dev server port */
71
+ port: 6006
72
+ };
73
+
74
+ // src/performance-presets.ts
75
+ var PRESETS = {
76
+ strict: { bundleSize: 8 * 1024 },
77
+ // 8KB gzipped
78
+ standard: { bundleSize: 15 * 1024 },
79
+ // 15KB gzipped
80
+ relaxed: { bundleSize: 30 * 1024 }
81
+ // 30KB gzipped
82
+ };
83
+ var PRESET_NAMES = Object.keys(PRESETS);
84
+ function resolvePerformanceConfig(input) {
85
+ if (!input) {
86
+ return { preset: "standard", budgets: PRESETS.standard };
87
+ }
88
+ if (typeof input === "string") {
89
+ const budgets = PRESETS[input];
90
+ if (!budgets) {
91
+ throw new Error(
92
+ `Unknown performance preset "${input}". Available: ${PRESET_NAMES.join(", ")}`
93
+ );
94
+ }
95
+ return { preset: input, budgets };
96
+ }
97
+ const presetName = input.preset ?? "standard";
98
+ const baseBudgets = PRESETS[presetName];
99
+ if (!baseBudgets) {
100
+ throw new Error(
101
+ `Unknown performance preset "${presetName}". Available: ${PRESET_NAMES.join(", ")}`
102
+ );
103
+ }
104
+ return {
105
+ preset: presetName,
106
+ budgets: {
107
+ bundleSize: input.budgets?.bundleSize ?? baseBudgets.bundleSize
108
+ }
109
+ };
110
+ }
111
+ function classifyComplexity(gzipBytes) {
112
+ if (gzipBytes < 5 * 1024) return "lightweight";
113
+ if (gzipBytes < 15 * 1024) return "moderate";
114
+ return "heavy";
115
+ }
116
+ function formatBytes(bytes) {
117
+ if (bytes < 1024) return `${bytes}B`;
118
+ const kb = bytes / 1024;
119
+ return kb < 10 ? `${kb.toFixed(1)}KB` : `${Math.round(kb)}KB`;
120
+ }
121
+ function budgetBar(percent, width = 20) {
122
+ const filled = Math.min(Math.round(percent / 100 * width), width);
123
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
124
+ return percent > 100 ? `\x1B[31m${bar}\x1B[0m` : `\x1B[32m${bar}\x1B[0m`;
125
+ }
126
+
127
+ // src/schema.ts
128
+ import { z } from "zod";
129
+ var figmaStringMappingSchema = z.object({
130
+ __type: z.literal("figma-string"),
131
+ figmaProperty: z.string().min(1)
132
+ });
133
+ var figmaBooleanMappingSchema = z.object({
134
+ __type: z.literal("figma-boolean"),
135
+ figmaProperty: z.string().min(1),
136
+ valueMapping: z.object({ true: z.unknown(), false: z.unknown() }).optional()
137
+ });
138
+ var figmaEnumMappingSchema = z.object({
139
+ __type: z.literal("figma-enum"),
140
+ figmaProperty: z.string().min(1),
141
+ valueMapping: z.record(z.unknown())
142
+ });
143
+ var figmaInstanceMappingSchema = z.object({
144
+ __type: z.literal("figma-instance"),
145
+ figmaProperty: z.string().min(1)
146
+ });
147
+ var figmaChildrenMappingSchema = z.object({
148
+ __type: z.literal("figma-children"),
149
+ layers: z.array(z.string().min(1))
150
+ });
151
+ var figmaTextContentMappingSchema = z.object({
152
+ __type: z.literal("figma-text-content"),
153
+ layer: z.string().min(1)
154
+ });
155
+ var figmaPropMappingSchema = z.discriminatedUnion("__type", [
156
+ figmaStringMappingSchema,
157
+ figmaBooleanMappingSchema,
158
+ figmaEnumMappingSchema,
159
+ figmaInstanceMappingSchema,
160
+ figmaChildrenMappingSchema,
161
+ figmaTextContentMappingSchema
162
+ ]);
163
+ var fragmentMetaSchema = z.object({
164
+ name: z.string().min(1),
165
+ description: z.string().min(1),
166
+ category: z.string().min(1),
167
+ tags: z.array(z.string()).optional(),
168
+ status: z.enum(["stable", "beta", "deprecated", "experimental"]).optional(),
169
+ since: z.string().optional(),
170
+ dependencies: z.array(z.object({
171
+ name: z.string().min(1),
172
+ version: z.string().min(1),
173
+ reason: z.string().optional()
174
+ })).optional(),
175
+ figma: z.string().url().optional(),
176
+ figmaProps: z.record(figmaPropMappingSchema).optional()
177
+ });
178
+ var fragmentUsageSchema = z.object({
179
+ when: z.array(z.string()),
180
+ whenNot: z.array(z.string()),
181
+ guidelines: z.array(z.string()).optional(),
182
+ accessibility: z.array(z.string()).optional()
183
+ });
184
+ var propTypeSchema = z.enum([
185
+ "string",
186
+ "number",
187
+ "boolean",
188
+ "enum",
189
+ "function",
190
+ "node",
191
+ "element",
192
+ "object",
193
+ "array",
194
+ "union",
195
+ "custom"
196
+ ]);
197
+ var propDefinitionSchema = z.object({
198
+ type: propTypeSchema,
199
+ values: z.array(z.string()).readonly().optional(),
200
+ default: z.unknown().optional(),
201
+ description: z.string().optional(),
202
+ required: z.boolean().optional(),
203
+ constraints: z.array(z.string()).optional(),
204
+ typeDetails: z.record(z.unknown()).optional()
205
+ });
206
+ var relationshipTypeSchema = z.enum([
207
+ "alternative",
208
+ "sibling",
209
+ "parent",
210
+ "child",
211
+ "composition",
212
+ "complementary",
213
+ "used-by"
214
+ ]);
215
+ var componentRelationSchema = z.object({
216
+ component: z.string().min(1),
217
+ relationship: relationshipTypeSchema,
218
+ note: z.string().min(1)
219
+ });
220
+ var fragmentVariantSchema = z.object({
221
+ name: z.string().min(1),
222
+ description: z.string().min(1),
223
+ render: z.function().returns(z.unknown()),
224
+ code: z.string().optional(),
225
+ figma: z.string().url().optional()
226
+ });
227
+ var fragmentBanSchema = z.object({
228
+ pattern: z.string().min(1),
229
+ message: z.string().min(1)
230
+ });
231
+ var fragmentContractSchema = z.object({
232
+ propsSummary: z.array(z.string()).optional(),
233
+ a11yRules: z.array(z.string()).optional(),
234
+ bans: z.array(fragmentBanSchema).optional(),
235
+ scenarioTags: z.array(z.string()).optional(),
236
+ performanceBudget: z.number().positive().optional()
237
+ });
238
+ var fragmentGeneratedSchema = z.object({
239
+ source: z.enum(["storybook", "manual", "ai"]),
240
+ sourceFile: z.string().optional(),
241
+ confidence: z.number().min(0).max(1).optional(),
242
+ timestamp: z.string().datetime().optional()
243
+ });
244
+ var aiMetadataSchema = z.object({
245
+ compositionPattern: z.enum(["compound", "simple", "controlled", "wrapper"]).optional(),
246
+ subComponents: z.array(z.string()).optional(),
247
+ requiredChildren: z.array(z.string()).optional(),
248
+ commonPatterns: z.array(z.string()).optional()
249
+ });
250
+ var blockDefinitionSchema = z.object({
251
+ name: z.string().min(1),
252
+ description: z.string().min(1),
253
+ category: z.string().min(1),
254
+ components: z.array(z.string().min(1)).min(1),
255
+ code: z.string().min(1),
256
+ tags: z.array(z.string()).optional()
257
+ });
258
+ var fragmentDefinitionSchema = z.object({
259
+ component: z.any(),
260
+ // Allow any component type (function, class, forwardRef, etc.)
261
+ meta: fragmentMetaSchema,
262
+ usage: fragmentUsageSchema,
263
+ props: z.record(propDefinitionSchema),
264
+ relations: z.array(componentRelationSchema).optional(),
265
+ variants: z.array(fragmentVariantSchema),
266
+ // Allow empty variants array
267
+ contract: fragmentContractSchema.optional(),
268
+ ai: aiMetadataSchema.optional(),
269
+ _generated: fragmentGeneratedSchema.optional()
270
+ });
271
+ var fragmentsConfigSchema = z.object({
272
+ include: z.array(z.string()).min(1),
273
+ exclude: z.array(z.string()).optional(),
274
+ components: z.array(z.string()).optional(),
275
+ outFile: z.string().optional(),
276
+ framework: z.enum(["react", "vue", "svelte"]).optional(),
277
+ figmaFile: z.string().url().optional(),
278
+ figmaToken: z.string().optional(),
279
+ screenshots: z.object({}).passthrough().optional(),
280
+ service: z.object({}).passthrough().optional(),
281
+ registry: z.object({}).passthrough().optional(),
282
+ tokens: z.object({
283
+ include: z.array(z.string()).min(1)
284
+ }).passthrough().optional(),
285
+ snippets: z.object({
286
+ mode: z.enum(["warn", "error"]).optional(),
287
+ scope: z.enum(["snippet", "snippet+render"]).optional(),
288
+ requireFullSnippet: z.boolean().optional(),
289
+ allowedExternalModules: z.array(z.string().min(1)).optional()
290
+ }).optional(),
291
+ performance: z.union([
292
+ z.enum(["strict", "standard", "relaxed"]),
293
+ z.object({
294
+ preset: z.enum(["strict", "standard", "relaxed"]).optional(),
295
+ budgets: z.object({
296
+ bundleSize: z.number().positive().optional()
297
+ }).optional()
298
+ })
299
+ ]).optional(),
300
+ storybook: z.object({
301
+ exclude: z.array(z.string()).optional(),
302
+ include: z.array(z.string()).optional(),
303
+ excludeDeprecated: z.boolean().optional(),
304
+ excludeTests: z.boolean().optional(),
305
+ excludeSvgIcons: z.boolean().optional(),
306
+ excludeSubComponents: z.boolean().optional()
307
+ }).optional()
308
+ });
309
+ var recipeDefinitionSchema = blockDefinitionSchema;
310
+
311
+ // src/defineFragment.ts
312
+ function defineFragment(definition) {
313
+ if (process.env.NODE_ENV !== "production") {
314
+ const result = fragmentDefinitionSchema.safeParse(definition);
315
+ if (!result.success) {
316
+ const errors = result.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
317
+ throw new Error(
318
+ `Invalid fragment definition for "${definition.meta?.name || "unknown"}":
319
+ ${errors}`
320
+ );
321
+ }
322
+ }
323
+ return definition;
324
+ }
325
+ function compileFragment(definition, filePath) {
326
+ return {
327
+ filePath,
328
+ meta: definition.meta,
329
+ usage: definition.usage,
330
+ props: definition.props,
331
+ relations: definition.relations,
332
+ variants: definition.variants.map((v) => ({
333
+ name: v.name,
334
+ description: v.description,
335
+ code: v.code,
336
+ figma: v.figma
337
+ })),
338
+ contract: definition.contract,
339
+ _generated: definition._generated
340
+ };
341
+ }
342
+ function defineBlock(definition) {
343
+ if (process.env.NODE_ENV !== "production") {
344
+ const result = blockDefinitionSchema.safeParse(definition);
345
+ if (!result.success) {
346
+ const errors = result.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
347
+ throw new Error(
348
+ `Invalid block definition for "${definition.name || "unknown"}":
349
+ ${errors}`
350
+ );
351
+ }
352
+ }
353
+ return definition;
354
+ }
355
+ var defineRecipe = defineBlock;
356
+ function compileBlock(definition, filePath) {
357
+ return {
358
+ filePath,
359
+ name: definition.name,
360
+ description: definition.description,
361
+ category: definition.category,
362
+ components: definition.components,
363
+ code: definition.code,
364
+ tags: definition.tags
365
+ };
366
+ }
367
+ var compileRecipe = compileBlock;
368
+
369
+ // src/storyAdapter.ts
370
+ import { createElement } from "react";
371
+
372
+ // src/storybook-csf.ts
373
+ import {
374
+ toId as storybookToId,
375
+ storyNameFromExport as storybookStoryNameFromExport,
376
+ isExportStory as storybookIsExportStory
377
+ } from "@storybook/csf";
378
+ var toId = (...args) => storybookToId(...args);
379
+ var storyNameFromExport = (...args) => storybookStoryNameFromExport(...args);
380
+ var isExportStory = (...args) => storybookIsExportStory(...args);
381
+
382
+ // src/storyAdapter.ts
383
+ var globalPreviewConfig = {};
384
+ function setPreviewConfig(config) {
385
+ globalPreviewConfig = config;
386
+ }
387
+ function getPreviewConfig() {
388
+ return globalPreviewConfig;
389
+ }
390
+ function storyModuleToFragment(storyModule, filePath) {
391
+ const meta = storyModule.default;
392
+ const component = meta.component;
393
+ if (!component) {
394
+ return null;
395
+ }
396
+ const componentName = extractComponentName(meta, filePath);
397
+ const category = extractCategory(meta.title);
398
+ const props = convertArgTypes(meta.argTypes ?? {}, globalPreviewConfig.argTypes);
399
+ const variants = extractVariants(storyModule, component, meta);
400
+ const figmaUrl = extractFigmaUrl(meta.parameters);
401
+ const fragmentMeta = {
402
+ name: componentName,
403
+ description: meta.parameters?.docs?.description?.component ?? `${componentName} component`,
404
+ category,
405
+ tags: meta.tags?.filter((t) => t !== "autodocs"),
406
+ status: "stable",
407
+ figma: figmaUrl
408
+ };
409
+ const usage = {
410
+ when: [`Use ${componentName} for its intended purpose`],
411
+ whenNot: ["When a more specific component is available"]
412
+ };
413
+ return {
414
+ component,
415
+ meta: fragmentMeta,
416
+ usage,
417
+ props,
418
+ variants
419
+ };
420
+ }
421
+ function extractComponentName(meta, filePath) {
422
+ if (meta.title) {
423
+ const parts = meta.title.split("/");
424
+ return parts[parts.length - 1];
425
+ }
426
+ if (meta.component?.displayName) {
427
+ return meta.component.displayName;
428
+ }
429
+ if (meta.component?.name && meta.component.name !== "Component") {
430
+ return meta.component.name;
431
+ }
432
+ const match = filePath.match(/([^/\\]+)\.stories\.(tsx?|jsx?)$/);
433
+ return match?.[1] ?? "Unknown";
434
+ }
435
+ function extractCategory(title) {
436
+ if (!title) return "general";
437
+ const parts = title.split("/");
438
+ if (parts.length >= 3) {
439
+ return parts[parts.length - 2].toLowerCase();
440
+ }
441
+ return "general";
442
+ }
443
+ function extractFigmaUrl(parameters) {
444
+ if (!parameters) return void 0;
445
+ const design = parameters.design;
446
+ if (design?.url && typeof design.url === "string") {
447
+ return design.url;
448
+ }
449
+ if (typeof parameters.figma === "string") {
450
+ return parameters.figma;
451
+ }
452
+ return void 0;
453
+ }
454
+ function convertArgTypes(argTypes, globalArgTypes) {
455
+ const props = {};
456
+ const mergedArgTypes = { ...globalArgTypes, ...argTypes };
457
+ for (const [name, argType] of Object.entries(mergedArgTypes)) {
458
+ if (argType.table?.disable) continue;
459
+ if (argType.control === false && argType.action) continue;
460
+ const { controlType, controlOptions } = extractControlInfo(argType);
461
+ props[name] = {
462
+ type: inferPropType(argType),
463
+ description: argType.description ?? `${name} prop`,
464
+ ...argType.options && { values: argType.options },
465
+ ...argType.table?.defaultValue && {
466
+ default: argType.table.defaultValue.summary
467
+ },
468
+ ...argType.defaultValue !== void 0 && {
469
+ default: argType.defaultValue
470
+ },
471
+ ...argType.type?.required && { required: true },
472
+ ...controlType && { controlType },
473
+ ...controlOptions && Object.keys(controlOptions).length > 0 && { controlOptions }
474
+ };
475
+ }
476
+ return props;
477
+ }
478
+ function extractControlInfo(argType) {
479
+ if (argType.control === void 0 || argType.control === false) {
480
+ return {};
481
+ }
482
+ const control = typeof argType.control === "string" ? { type: argType.control } : argType.control;
483
+ const validControlTypes = [
484
+ "text",
485
+ "number",
486
+ "range",
487
+ "boolean",
488
+ "select",
489
+ "multi-select",
490
+ "radio",
491
+ "inline-radio",
492
+ "check",
493
+ "inline-check",
494
+ "object",
495
+ "file",
496
+ "color",
497
+ "date"
498
+ ];
499
+ const controlType = validControlTypes.includes(control.type) ? control.type : void 0;
500
+ const controlOptions = {};
501
+ if (control.min !== void 0) controlOptions.min = control.min;
502
+ if (control.max !== void 0) controlOptions.max = control.max;
503
+ if (control.step !== void 0) controlOptions.step = control.step;
504
+ if (control.presetColors) controlOptions.presetColors = control.presetColors;
505
+ return {
506
+ controlType,
507
+ controlOptions: Object.keys(controlOptions).length > 0 ? controlOptions : void 0
508
+ };
509
+ }
510
+ function inferPropType(argType) {
511
+ if (argType.action) return "function";
512
+ if (argType.options?.length) return "enum";
513
+ if (argType.type?.name) {
514
+ const typeMap = {
515
+ string: "string",
516
+ number: "number",
517
+ boolean: "boolean",
518
+ object: "object",
519
+ array: "array",
520
+ function: "function"
521
+ };
522
+ const mapped = typeMap[argType.type.name];
523
+ if (mapped) return mapped;
524
+ }
525
+ const control = typeof argType.control === "string" ? argType.control : argType.control ? argType.control.type : void 0;
526
+ if (control) {
527
+ const controlMap = {
528
+ // Text controls
529
+ text: "string",
530
+ // Number controls
531
+ number: "number",
532
+ range: "number",
533
+ // Boolean controls
534
+ boolean: "boolean",
535
+ check: "boolean",
536
+ "inline-check": "boolean",
537
+ // Enum/selection controls
538
+ select: "enum",
539
+ "multi-select": "enum",
540
+ radio: "enum",
541
+ "inline-radio": "enum",
542
+ // Object controls
543
+ object: "object",
544
+ file: "object",
545
+ // Special string controls
546
+ color: "string",
547
+ date: "string"
548
+ };
549
+ const mapped = controlMap[control];
550
+ if (mapped) return mapped;
551
+ }
552
+ return "string";
553
+ }
554
+ function isStory(value) {
555
+ if (typeof value === "object" && value !== null) {
556
+ const obj = value;
557
+ if ("args" in obj || "render" in obj || "play" in obj) return true;
558
+ }
559
+ if (typeof value === "function") {
560
+ const fn = value;
561
+ if ("args" in fn) return true;
562
+ }
563
+ return false;
564
+ }
565
+ function extractVariants(storyModule, component, meta) {
566
+ const variants = [];
567
+ for (const [exportName, exportValue] of Object.entries(storyModule)) {
568
+ if (exportName === "default") continue;
569
+ if (!isExportStory(exportName, meta)) continue;
570
+ if (!isStory(exportValue)) continue;
571
+ const story = exportValue;
572
+ const storyName = typeof story === "object" && story.name || typeof story === "object" && story.storyName || typeof story === "function" && story.storyName || storyNameFromExport(exportName);
573
+ const storyId = toId(meta.title || "Unknown", exportName);
574
+ let description = `${storyName} variant`;
575
+ if (typeof story === "object" && story.parameters?.docs?.description?.story) {
576
+ description = story.parameters.docs.description.story;
577
+ }
578
+ const storyPlayFn = typeof story === "object" ? story.play : story.play;
579
+ const hasPlayFunction = !!storyPlayFn;
580
+ const wrappedPlay = storyPlayFn ? async (context) => {
581
+ const args = {
582
+ ...globalPreviewConfig.args,
583
+ ...meta.args,
584
+ ...typeof story === "function" ? story.args : story.args
585
+ };
586
+ const fullContext = buildStoryContext(meta, story, args, storyId, storyName);
587
+ const playContext = {
588
+ ...fullContext,
589
+ canvasElement: context.canvasElement,
590
+ args: context.args,
591
+ step: context.step
592
+ };
593
+ await storyPlayFn(playContext);
594
+ } : void 0;
595
+ const storyTags = typeof story === "object" ? story.tags : void 0;
596
+ const loaders = collectLoaders(meta, story);
597
+ const variantArgs = {
598
+ ...globalPreviewConfig.args,
599
+ ...meta.args,
600
+ ...typeof story === "function" ? story.args : story.args
601
+ };
602
+ const hasArgs = Object.keys(variantArgs).length > 0;
603
+ variants.push({
604
+ name: storyName,
605
+ description,
606
+ render: createRenderFunction(story, component, meta, storyId, storyName),
607
+ // Store Storybook-specific metadata
608
+ ...hasPlayFunction && { hasPlayFunction: true },
609
+ ...wrappedPlay && { play: wrappedPlay },
610
+ ...storyId && { storyId },
611
+ ...storyTags && { tags: storyTags },
612
+ ...loaders.length > 0 && { loaders },
613
+ ...hasArgs && { args: variantArgs }
614
+ });
615
+ }
616
+ return variants;
617
+ }
618
+ function collectLoaders(meta, story) {
619
+ const allLoaders = [
620
+ ...globalPreviewConfig.loaders ?? [],
621
+ ...meta.loaders ?? [],
622
+ ...typeof story === "function" ? story.loaders ?? [] : story.loaders ?? []
623
+ ];
624
+ if (allLoaders.length === 0) {
625
+ return [];
626
+ }
627
+ return allLoaders.map((loader) => {
628
+ return async () => {
629
+ const minimalContext = {
630
+ args: {},
631
+ argTypes: {},
632
+ globals: {},
633
+ parameters: {},
634
+ id: "",
635
+ kind: meta.title || "Unknown",
636
+ name: "",
637
+ story: "",
638
+ viewMode: "story",
639
+ loaded: {},
640
+ abortSignal: new AbortController().signal,
641
+ componentId: "",
642
+ title: meta.title || "Unknown"
643
+ };
644
+ return loader(minimalContext);
645
+ };
646
+ });
647
+ }
648
+ function buildStoryContext(meta, story, args, storyId, storyName, loadedData) {
649
+ const mergedArgs = {
650
+ ...globalPreviewConfig.args,
651
+ ...meta.args,
652
+ ...typeof story === "object" ? story.args : story.args,
653
+ ...args
654
+ };
655
+ const mergedArgTypes = {
656
+ ...globalPreviewConfig.argTypes,
657
+ ...meta.argTypes,
658
+ ...typeof story === "object" ? story.argTypes : story.argTypes
659
+ };
660
+ const mergedParameters = {
661
+ ...globalPreviewConfig.parameters,
662
+ ...meta.parameters,
663
+ ...typeof story === "object" ? story.parameters : story.parameters
664
+ };
665
+ return {
666
+ args: mergedArgs,
667
+ argTypes: mergedArgTypes ?? {},
668
+ globals: {},
669
+ parameters: mergedParameters ?? {},
670
+ id: storyId,
671
+ kind: meta.title || "Unknown",
672
+ name: storyName,
673
+ story: storyName,
674
+ viewMode: "story",
675
+ loaded: loadedData ?? {},
676
+ abortSignal: new AbortController().signal,
677
+ componentId: toId(meta.title || "Unknown", ""),
678
+ title: meta.title || "Unknown"
679
+ };
680
+ }
681
+ function createRenderFunction(story, component, meta, storyId, storyName) {
682
+ return (options) => {
683
+ const args = {
684
+ ...globalPreviewConfig.args,
685
+ ...meta.args,
686
+ ...typeof story === "function" ? story.args : story.args,
687
+ ...options?.args
688
+ // Runtime overrides from viewer props panel
689
+ };
690
+ const loadedData = options?.loadedData;
691
+ const context = buildStoryContext(meta, story, args, storyId, storyName, loadedData);
692
+ let renderFn;
693
+ if (typeof story === "function") {
694
+ renderFn = () => story(args);
695
+ } else if (story.render) {
696
+ renderFn = () => story.render.length >= 2 ? story.render(args, context) : story.render(args);
697
+ } else if (meta.render) {
698
+ renderFn = () => meta.render.length >= 2 ? meta.render(args, context) : meta.render(args);
699
+ } else {
700
+ renderFn = () => createElement(component, args);
701
+ }
702
+ const allDecorators = [
703
+ ...globalPreviewConfig.decorators ?? [],
704
+ ...meta.decorators ?? [],
705
+ ...typeof story === "function" ? story.decorators ?? [] : story.decorators ?? []
706
+ ].reverse();
707
+ if (allDecorators.length > 0) {
708
+ return applyDecorators(renderFn, allDecorators, context);
709
+ }
710
+ return renderFn();
711
+ };
712
+ }
713
+ function applyDecorators(renderFn, decorators, context) {
714
+ let storyFn = renderFn;
715
+ for (const decorator of decorators) {
716
+ const wrappedFn = storyFn;
717
+ storyFn = () => decorator(wrappedFn, context);
718
+ }
719
+ return storyFn();
720
+ }
721
+
722
+ // src/storyFilters.ts
723
+ var EXCLUDED_TAGS = /* @__PURE__ */ new Set(["hidden", "internal", "no-fragment"]);
724
+ var SVG_ICON_RE = /^Svg[A-Z]/;
725
+ var TEST_TITLE_RE = /\/tests?$/i;
726
+ var TEST_FILE_RE = /\.test\.stories\./;
727
+ var DEPRECATED_TITLE_RE = /\bDeprecated\b/i;
728
+ function checkStoryExclusion(opts) {
729
+ const { config } = opts;
730
+ if (isForceIncluded(opts.componentName, config)) {
731
+ return { excluded: false };
732
+ }
733
+ if (isConfigExcluded(opts.componentName, config)) {
734
+ return {
735
+ excluded: true,
736
+ reason: "config-excluded",
737
+ detail: `'${opts.componentName}' matches storybook.exclude pattern`
738
+ };
739
+ }
740
+ if (config.excludeDeprecated !== false && opts.storybookTitle && DEPRECATED_TITLE_RE.test(opts.storybookTitle)) {
741
+ return {
742
+ excluded: true,
743
+ reason: "deprecated",
744
+ detail: `Title "${opts.storybookTitle}" contains "Deprecated"`
745
+ };
746
+ }
747
+ if (config.excludeTests !== false) {
748
+ if (opts.storybookTitle && TEST_TITLE_RE.test(opts.storybookTitle)) {
749
+ return {
750
+ excluded: true,
751
+ reason: "test-story",
752
+ detail: `Title "${opts.storybookTitle}" ends with /test(s)`
753
+ };
754
+ }
755
+ if (TEST_FILE_RE.test(opts.filePath)) {
756
+ return {
757
+ excluded: true,
758
+ reason: "test-story",
759
+ detail: `File path matches *.test.stories.*`
760
+ };
761
+ }
762
+ }
763
+ if (config.excludeSvgIcons !== false) {
764
+ const names = [opts.componentName, opts.componentDisplayName, opts.componentFunctionName].filter(Boolean);
765
+ for (const name of names) {
766
+ if (SVG_ICON_RE.test(name)) {
767
+ return {
768
+ excluded: true,
769
+ reason: "svg-icon",
770
+ detail: `Component name "${name}" matches Svg[A-Z] pattern`
771
+ };
772
+ }
773
+ }
774
+ }
775
+ if (opts.tags?.length) {
776
+ const hit = opts.tags.find((t) => EXCLUDED_TAGS.has(t));
777
+ if (hit) {
778
+ return {
779
+ excluded: true,
780
+ reason: "tag-excluded",
781
+ detail: `Tag "${hit}" is in the exclusion set`
782
+ };
783
+ }
784
+ }
785
+ if (opts.variantCount === 0) {
786
+ return {
787
+ excluded: true,
788
+ reason: "empty-variants",
789
+ detail: "Zero renderable story exports"
790
+ };
791
+ }
792
+ return { excluded: false };
793
+ }
794
+ function detectSubComponentPaths(storyFiles) {
795
+ const byDir = /* @__PURE__ */ new Map();
796
+ for (const file of storyFiles) {
797
+ const parts = file.relativePath.split("/");
798
+ if (parts.length < 2) continue;
799
+ const fileName = parts[parts.length - 1];
800
+ const baseMatch = fileName.match(/^([^.]+)\.stories\./);
801
+ if (!baseMatch) continue;
802
+ const dir = parts.slice(0, -1).join("/");
803
+ const baseName = baseMatch[1];
804
+ if (!byDir.has(dir)) byDir.set(dir, []);
805
+ byDir.get(dir).push({ relativePath: file.relativePath, baseName });
806
+ }
807
+ const subComponentMap = /* @__PURE__ */ new Map();
808
+ for (const [dir, files] of byDir) {
809
+ if (files.length <= 1) continue;
810
+ const dirName = dir.split("/").pop();
811
+ const primary = files.find((f) => f.baseName === dirName);
812
+ if (!primary) continue;
813
+ for (const file of files) {
814
+ if (file.relativePath === primary.relativePath) continue;
815
+ subComponentMap.set(file.relativePath, primary.baseName);
816
+ }
817
+ }
818
+ return subComponentMap;
819
+ }
820
+ function isForceIncluded(name, config) {
821
+ if (!config.include?.length) return false;
822
+ return config.include.some((pattern) => matchesPattern(name, pattern));
823
+ }
824
+ function isConfigExcluded(name, config) {
825
+ if (!config.exclude?.length) return false;
826
+ return config.exclude.some((pattern) => matchesPattern(name, pattern));
827
+ }
828
+ function matchesPattern(name, pattern) {
829
+ if (!pattern.includes("*")) {
830
+ return name === pattern;
831
+ }
832
+ const parts = pattern.split("*");
833
+ if (parts.length === 2) {
834
+ const [prefix, suffix] = parts;
835
+ if (prefix && suffix) return name.startsWith(prefix) && name.endsWith(suffix);
836
+ if (prefix) return name.startsWith(prefix);
837
+ if (suffix) return name.endsWith(suffix);
838
+ return true;
839
+ }
840
+ const escaped = parts.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
841
+ return new RegExp(`^${escaped}$`).test(name);
842
+ }
843
+
844
+ // src/context.ts
845
+ import { generateContext, filterPlaceholders, PLACEHOLDER_PATTERNS } from "@fragments-sdk/context/generate";
846
+
847
+ // src/figma.ts
848
+ function string(figmaProperty) {
849
+ return {
850
+ __type: "figma-string",
851
+ figmaProperty
852
+ };
853
+ }
854
+ function boolean(figmaProperty, valueMapping) {
855
+ return {
856
+ __type: "figma-boolean",
857
+ figmaProperty,
858
+ valueMapping
859
+ };
860
+ }
861
+ function enumValue(figmaProperty, valueMapping) {
862
+ return {
863
+ __type: "figma-enum",
864
+ figmaProperty,
865
+ valueMapping
866
+ };
867
+ }
868
+ function instance(figmaProperty) {
869
+ return {
870
+ __type: "figma-instance",
871
+ figmaProperty
872
+ };
873
+ }
874
+ function children(layers) {
875
+ return {
876
+ __type: "figma-children",
877
+ layers
878
+ };
879
+ }
880
+ function textContent(layer) {
881
+ return {
882
+ __type: "figma-text-content",
883
+ layer
884
+ };
885
+ }
886
+ var figma = {
887
+ string,
888
+ boolean,
889
+ enum: enumValue,
890
+ instance,
891
+ children,
892
+ textContent
893
+ };
894
+ function isFigmaPropMapping(value) {
895
+ if (typeof value !== "object" || value === null || !("__type" in value)) {
896
+ return false;
897
+ }
898
+ const typeValue = value.__type;
899
+ return typeof typeValue === "string" && typeValue.startsWith("figma-");
900
+ }
901
+ function resolveFigmaMapping(mapping, figmaValues) {
902
+ switch (mapping.__type) {
903
+ case "figma-string":
904
+ return figmaValues[mapping.figmaProperty] ?? "";
905
+ case "figma-boolean": {
906
+ const boolValue = figmaValues[mapping.figmaProperty];
907
+ if (mapping.valueMapping) {
908
+ return boolValue ? mapping.valueMapping.true : mapping.valueMapping.false;
909
+ }
910
+ return boolValue;
911
+ }
912
+ case "figma-enum": {
913
+ const enumKey = figmaValues[mapping.figmaProperty];
914
+ return mapping.valueMapping[enumKey] ?? enumKey;
915
+ }
916
+ case "figma-instance":
917
+ return figmaValues[mapping.figmaProperty];
918
+ case "figma-children":
919
+ return mapping.layers.map((layer) => figmaValues[layer]);
920
+ case "figma-text-content":
921
+ return figmaValues[mapping.layer] ?? "";
922
+ default:
923
+ return void 0;
924
+ }
925
+ }
926
+
927
+ // src/token-parser.ts
928
+ var NAMING_RULES = [
929
+ { pattern: /--\w+-font-/, category: "typography" },
930
+ { pattern: /--\w+-line-height-/, category: "typography" },
931
+ { pattern: /--\w+-space-/, category: "spacing" },
932
+ { pattern: /--\w+-padding-/, category: "spacing" },
933
+ { pattern: /--\w+-radius-/, category: "radius" },
934
+ { pattern: /--\w+-color-/, category: "colors" },
935
+ { pattern: /--\w+-bg-/, category: "surfaces" },
936
+ { pattern: /--\w+-text-/, category: "text" },
937
+ { pattern: /--\w+-border/, category: "borders" },
938
+ { pattern: /--\w+-shadow-/, category: "shadows" },
939
+ { pattern: /--\w+-focus-/, category: "focus" },
940
+ { pattern: /--\w+-transition-/, category: "transitions" },
941
+ { pattern: /--\w+-scrollbar-/, category: "scrollbar" },
942
+ { pattern: /--\w+-z-index/, category: "z-index" },
943
+ { pattern: /--\w+-(button|input|touch)-/, category: "component-sizing" },
944
+ { pattern: /--\w+-appshell-/, category: "layout" },
945
+ { pattern: /--\w+-header-/, category: "layout" },
946
+ { pattern: /--\w+-code-/, category: "code" },
947
+ { pattern: /--\w+-tooltip-/, category: "tooltip" },
948
+ { pattern: /--\w+-hero-/, category: "marketing" }
949
+ ];
950
+ function inferCategory(name) {
951
+ for (const rule of NAMING_RULES) {
952
+ if (rule.pattern.test(name)) {
953
+ return rule.category;
954
+ }
955
+ }
956
+ return "other";
957
+ }
958
+ function detectPrefix(names) {
959
+ if (names.length === 0) return "--";
960
+ const stripped = names.map((n) => n.slice(2));
961
+ let prefix = "";
962
+ const first = stripped[0];
963
+ for (let i = 0; i < first.length; i++) {
964
+ const ch = first[i];
965
+ if (stripped.every((s) => s[i] === ch)) {
966
+ prefix += ch;
967
+ } else {
968
+ break;
969
+ }
970
+ }
971
+ const lastHyphen = prefix.lastIndexOf("-");
972
+ if (lastHyphen > 0) {
973
+ prefix = prefix.slice(0, lastHyphen + 1);
974
+ }
975
+ return `--${prefix}`;
976
+ }
977
+ function normalizeCategory(comment) {
978
+ const text = comment.trim().replace(/^\/\/\s*/, "").replace(/^\/\*+\s*/, "").replace(/\s*\*+\/$/, "").trim().toLowerCase();
979
+ const mappings = {
980
+ "base configuration": "base",
981
+ "typography": "typography",
982
+ "spacing (micro)": "spacing",
983
+ "spacing": "spacing",
984
+ "density padding": "spacing",
985
+ "border radius": "radius",
986
+ "transitions": "transitions",
987
+ "colors": "colors",
988
+ "surfaces": "surfaces",
989
+ "text": "text",
990
+ "borders": "borders",
991
+ "shadows": "shadows",
992
+ "focus": "focus",
993
+ "scrollbar": "scrollbar",
994
+ "component heights": "component-sizing",
995
+ "appshell layout": "layout",
996
+ "codeblock": "code",
997
+ "tooltip": "tooltip",
998
+ "hero/marketing gradient": "marketing"
999
+ };
1000
+ return mappings[text] ?? text.replace(/\s+/g, "-");
1001
+ }
1002
+ function extractScssVariables(content) {
1003
+ const vars = /* @__PURE__ */ new Map();
1004
+ const scssVarRegex = /^\s*(\$[\w-]+)\s*:\s*(.+?)\s*(?:!default\s*)?;/gm;
1005
+ let match;
1006
+ while ((match = scssVarRegex.exec(content)) !== null) {
1007
+ const name = match[1];
1008
+ const value = match[2].replace(/\s*\/\/.*$/, "").trim();
1009
+ if (!vars.has(name)) {
1010
+ vars.set(name, value);
1011
+ }
1012
+ }
1013
+ return vars;
1014
+ }
1015
+ function resolveTokenValue(rawValue, scssVars, cssVarValues, depth = 0) {
1016
+ if (depth > 5) return rawValue;
1017
+ let resolved = rawValue;
1018
+ resolved = resolved.replace(/#\{(\$[\w-]+)\}/g, (_, varName) => {
1019
+ const val = scssVars.get(varName);
1020
+ return val !== void 0 ? resolveTokenValue(val, scssVars, cssVarValues, depth + 1) : `#{${varName}}`;
1021
+ });
1022
+ resolved = resolved.replace(/(?<![#\{])(\$[\w-]+)/g, (_, varName) => {
1023
+ const val = scssVars.get(varName);
1024
+ return val !== void 0 ? resolveTokenValue(val, scssVars, cssVarValues, depth + 1) : varName;
1025
+ });
1026
+ resolved = resolved.replace(
1027
+ /var\((--[\w-]+)(?:\s*,\s*(.+?))?\)/g,
1028
+ (original, tokenName, fallback) => {
1029
+ const tokenVal = cssVarValues.get(tokenName);
1030
+ if (tokenVal !== void 0) {
1031
+ return resolveTokenValue(tokenVal, scssVars, cssVarValues, depth + 1);
1032
+ }
1033
+ if (fallback) {
1034
+ return resolveTokenValue(fallback.trim(), scssVars, cssVarValues, depth + 1);
1035
+ }
1036
+ return original;
1037
+ }
1038
+ );
1039
+ return resolved;
1040
+ }
1041
+ function parseTokenFile(content, filePath) {
1042
+ const lines = content.split("\n");
1043
+ const tokens = [];
1044
+ const seenNames = /* @__PURE__ */ new Set();
1045
+ let currentCategory = "other";
1046
+ let hasCommentCategories = false;
1047
+ const scssVars = extractScssVariables(content);
1048
+ const varDeclRegex = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;/;
1049
+ const sectionCommentRegex = /^\s*\/\/\s+([A-Z].+)$/;
1050
+ for (const line of lines) {
1051
+ const commentMatch = line.match(sectionCommentRegex);
1052
+ if (commentMatch) {
1053
+ const normalized = normalizeCategory(commentMatch[0]);
1054
+ if (normalized) {
1055
+ currentCategory = normalized;
1056
+ hasCommentCategories = true;
1057
+ }
1058
+ continue;
1059
+ }
1060
+ const varMatch = line.match(varDeclRegex);
1061
+ if (varMatch) {
1062
+ const name = varMatch[1];
1063
+ const rawValue = varMatch[2];
1064
+ if (seenNames.has(name)) continue;
1065
+ seenNames.add(name);
1066
+ const inlineComment = line.match(/\/\/\s*(.+)$/);
1067
+ const description = inlineComment ? inlineComment[1].trim() : void 0;
1068
+ const cleanValue = rawValue.replace(/\s*\/\/.*$/, "").trim();
1069
+ tokens.push({
1070
+ name,
1071
+ value: cleanValue || void 0,
1072
+ category: hasCommentCategories ? currentCategory : inferCategory(name),
1073
+ description
1074
+ });
1075
+ }
1076
+ }
1077
+ const cssVarValues = /* @__PURE__ */ new Map();
1078
+ for (const token of tokens) {
1079
+ if (token.value) {
1080
+ cssVarValues.set(token.name, token.value);
1081
+ }
1082
+ }
1083
+ for (const token of tokens) {
1084
+ if (token.value) {
1085
+ const resolved = resolveTokenValue(token.value, scssVars, cssVarValues);
1086
+ if (resolved !== token.value && !resolved.includes("#{") && !resolved.includes("$")) {
1087
+ token.resolvedValue = resolved;
1088
+ }
1089
+ }
1090
+ }
1091
+ const categories = {};
1092
+ for (const token of tokens) {
1093
+ if (!categories[token.category]) {
1094
+ categories[token.category] = [];
1095
+ }
1096
+ categories[token.category].push(token);
1097
+ }
1098
+ const prefix = detectPrefix(tokens.map((t) => t.name));
1099
+ return {
1100
+ prefix,
1101
+ categories,
1102
+ total: tokens.length
1103
+ };
1104
+ }
1105
+
1106
+ // src/composition.ts
1107
+ import { ComponentGraphEngine } from "@fragments-sdk/context/graph";
1108
+ var CATEGORY_AFFINITIES = {
1109
+ forms: ["feedback"],
1110
+ actions: ["feedback"]
1111
+ };
1112
+ function analyzeComposition(fragments, componentNames, _context, options) {
1113
+ const allNames = new Set(Object.keys(fragments));
1114
+ const components = [];
1115
+ const unknown = [];
1116
+ for (const name of componentNames) {
1117
+ if (allNames.has(name)) {
1118
+ components.push(name);
1119
+ } else {
1120
+ unknown.push(name);
1121
+ }
1122
+ }
1123
+ const selectedSet = new Set(components);
1124
+ const warnings = [];
1125
+ const suggestions = [];
1126
+ const guidelines = [];
1127
+ const suggestedSet = /* @__PURE__ */ new Set();
1128
+ for (const name of components) {
1129
+ const fragment = fragments[name];
1130
+ if (fragment.relations) {
1131
+ for (const rel of fragment.relations) {
1132
+ switch (rel.relationship) {
1133
+ case "parent":
1134
+ if (!selectedSet.has(rel.component)) {
1135
+ warnings.push({
1136
+ type: "missing_parent",
1137
+ component: name,
1138
+ message: `"${name}" expects to be wrapped by "${rel.component}"${rel.note ? `: ${rel.note}` : ""}`,
1139
+ relatedComponent: rel.component
1140
+ });
1141
+ }
1142
+ break;
1143
+ case "child":
1144
+ if (!selectedSet.has(rel.component) && !suggestedSet.has(rel.component)) {
1145
+ suggestions.push({
1146
+ component: rel.component,
1147
+ reason: `"${name}" typically contains "${rel.component}"${rel.note ? `: ${rel.note}` : ""}`,
1148
+ relationship: "child",
1149
+ sourceComponent: name
1150
+ });
1151
+ suggestedSet.add(rel.component);
1152
+ }
1153
+ break;
1154
+ case "composition":
1155
+ if (!selectedSet.has(rel.component)) {
1156
+ warnings.push({
1157
+ type: "missing_composition",
1158
+ component: name,
1159
+ message: `"${name}" is typically used together with "${rel.component}"${rel.note ? `: ${rel.note}` : ""}`,
1160
+ relatedComponent: rel.component
1161
+ });
1162
+ }
1163
+ break;
1164
+ case "sibling":
1165
+ if (!selectedSet.has(rel.component) && !suggestedSet.has(rel.component)) {
1166
+ suggestions.push({
1167
+ component: rel.component,
1168
+ reason: `"${rel.component}" is a sibling of "${name}"${rel.note ? `: ${rel.note}` : ""}`,
1169
+ relationship: "sibling",
1170
+ sourceComponent: name
1171
+ });
1172
+ suggestedSet.add(rel.component);
1173
+ }
1174
+ break;
1175
+ case "alternative":
1176
+ if (selectedSet.has(rel.component)) {
1177
+ warnings.push({
1178
+ type: "redundant_alternative",
1179
+ component: name,
1180
+ message: `"${name}" and "${rel.component}" are alternatives \u2014 using both may be redundant${rel.note ? `: ${rel.note}` : ""}`,
1181
+ relatedComponent: rel.component
1182
+ });
1183
+ }
1184
+ break;
1185
+ }
1186
+ }
1187
+ }
1188
+ if (fragment.usage?.whenNot) {
1189
+ for (const whenNotEntry of fragment.usage.whenNot) {
1190
+ const lower = whenNotEntry.toLowerCase();
1191
+ for (const other of components) {
1192
+ if (other !== name && lower.includes(other.toLowerCase())) {
1193
+ guidelines.push({
1194
+ component: name,
1195
+ guideline: `Potential conflict with "${other}": ${whenNotEntry}`
1196
+ });
1197
+ }
1198
+ }
1199
+ }
1200
+ }
1201
+ if (fragment.meta.status === "deprecated") {
1202
+ warnings.push({
1203
+ type: "deprecated",
1204
+ component: name,
1205
+ message: fragment.meta.description ? `"${name}" is deprecated: ${fragment.meta.description}` : `"${name}" is deprecated`
1206
+ });
1207
+ } else if (fragment.meta.status === "experimental") {
1208
+ warnings.push({
1209
+ type: "experimental",
1210
+ component: name,
1211
+ message: `"${name}" is experimental and may change without notice`
1212
+ });
1213
+ }
1214
+ }
1215
+ const selectedCategories = new Set(
1216
+ components.map((name) => fragments[name].meta.category)
1217
+ );
1218
+ for (const [category, affinities] of Object.entries(CATEGORY_AFFINITIES)) {
1219
+ if (!selectedCategories.has(category)) continue;
1220
+ for (const neededCategory of affinities) {
1221
+ if (selectedCategories.has(neededCategory)) continue;
1222
+ const candidate = findBestCategoryCandidate(
1223
+ fragments,
1224
+ neededCategory,
1225
+ selectedSet,
1226
+ suggestedSet
1227
+ );
1228
+ if (candidate) {
1229
+ suggestions.push({
1230
+ component: candidate,
1231
+ reason: `Compositions using "${category}" components often benefit from a "${neededCategory}" component`,
1232
+ relationship: "category_gap",
1233
+ sourceComponent: components.find(
1234
+ (n) => fragments[n].meta.category === category
1235
+ )
1236
+ });
1237
+ suggestedSet.add(candidate);
1238
+ }
1239
+ }
1240
+ }
1241
+ if (options?.graph) {
1242
+ const engine = new ComponentGraphEngine(options.graph);
1243
+ for (const name of components) {
1244
+ const deps = engine.dependencies(name, ["imports", "hook-depends"]);
1245
+ for (const dep of deps) {
1246
+ if (!selectedSet.has(dep.target) && !suggestedSet.has(dep.target) && allNames.has(dep.target)) {
1247
+ suggestions.push({
1248
+ component: dep.target,
1249
+ reason: `"${name}" ${dep.type === "hook-depends" ? "uses a hook from" : "imports"} "${dep.target}"`,
1250
+ relationship: "composition",
1251
+ sourceComponent: name
1252
+ });
1253
+ suggestedSet.add(dep.target);
1254
+ }
1255
+ }
1256
+ }
1257
+ for (const name of components) {
1258
+ const blocks = engine.blocksUsing(name);
1259
+ for (const blockName of blocks) {
1260
+ const blockComps = options.graph.edges.filter(
1261
+ (e) => e.type === "composes" && e.provenance === `block:${blockName}` && (e.source === name || e.target === name)
1262
+ ).map((e) => e.source === name ? e.target : e.source);
1263
+ for (const comp of blockComps) {
1264
+ if (!selectedSet.has(comp) && !suggestedSet.has(comp) && allNames.has(comp)) {
1265
+ suggestions.push({
1266
+ component: comp,
1267
+ reason: `"${name}" and "${comp}" are used together in the "${blockName}" block`,
1268
+ relationship: "composition",
1269
+ sourceComponent: name
1270
+ });
1271
+ suggestedSet.add(comp);
1272
+ }
1273
+ }
1274
+ }
1275
+ }
1276
+ }
1277
+ return { components, unknown, warnings, suggestions, guidelines };
1278
+ }
1279
+ function findBestCategoryCandidate(fragments, category, selectedSet, suggestedSet) {
1280
+ let best = null;
1281
+ let bestScore = -1;
1282
+ for (const [name, fragment] of Object.entries(fragments)) {
1283
+ if (fragment.meta.category !== category) continue;
1284
+ if (selectedSet.has(name) || suggestedSet.has(name)) continue;
1285
+ const status = fragment.meta.status ?? "stable";
1286
+ let score = 0;
1287
+ if (status === "stable") score = 3;
1288
+ else if (status === "beta") score = 2;
1289
+ else if (status === "experimental") score = 1;
1290
+ if (score > bestScore) {
1291
+ bestScore = score;
1292
+ best = name;
1293
+ }
1294
+ }
1295
+ return best;
1296
+ }
1297
+
1298
+ // src/preview-runtime.tsx
1299
+ import { useEffect, useState } from "react";
1300
+ import { Fragment, jsx } from "react/jsx-runtime";
1301
+ var EMPTY_STATE = {
1302
+ content: null,
1303
+ isLoading: false,
1304
+ error: null,
1305
+ loadedData: void 0
1306
+ };
1307
+ function toError(error) {
1308
+ return error instanceof Error ? error : new Error(String(error));
1309
+ }
1310
+ async function executeVariantLoaders(loaders, loadedData) {
1311
+ const hasLoaders = !!loaders && loaders.length > 0;
1312
+ if (!hasLoaders) {
1313
+ return loadedData;
1314
+ }
1315
+ const results = await Promise.all(loaders.map((loader) => loader()));
1316
+ const mergedFromLoaders = results.reduce(
1317
+ (acc, result) => ({ ...acc, ...result }),
1318
+ {}
1319
+ );
1320
+ return loadedData ? { ...mergedFromLoaders, ...loadedData } : mergedFromLoaders;
1321
+ }
1322
+ async function resolvePreviewRuntimeState(options) {
1323
+ const { variant, loadedData } = options;
1324
+ if (!variant) {
1325
+ return EMPTY_STATE;
1326
+ }
1327
+ try {
1328
+ const mergedLoadedData = await executeVariantLoaders(variant.loaders, loadedData);
1329
+ const content = variant.render({ loadedData: mergedLoadedData });
1330
+ return {
1331
+ content,
1332
+ isLoading: false,
1333
+ error: null,
1334
+ loadedData: mergedLoadedData
1335
+ };
1336
+ } catch (error) {
1337
+ return {
1338
+ content: null,
1339
+ isLoading: false,
1340
+ error: toError(error),
1341
+ loadedData: void 0
1342
+ };
1343
+ }
1344
+ }
1345
+ function usePreviewVariantRuntime(options) {
1346
+ const { variant, loadedData } = options;
1347
+ const [state, setState] = useState(EMPTY_STATE);
1348
+ useEffect(() => {
1349
+ let cancelled = false;
1350
+ if (!variant) {
1351
+ setState(EMPTY_STATE);
1352
+ return () => {
1353
+ cancelled = true;
1354
+ };
1355
+ }
1356
+ const hasLoaders = !!variant.loaders && variant.loaders.length > 0;
1357
+ setState({
1358
+ content: null,
1359
+ isLoading: hasLoaders,
1360
+ error: null,
1361
+ loadedData: void 0
1362
+ });
1363
+ resolvePreviewRuntimeState({ variant, loadedData }).then((nextState) => {
1364
+ if (!cancelled) {
1365
+ setState(nextState);
1366
+ }
1367
+ });
1368
+ return () => {
1369
+ cancelled = true;
1370
+ };
1371
+ }, [variant, loadedData]);
1372
+ return state;
1373
+ }
1374
+ function PreviewVariantRuntime({
1375
+ variant,
1376
+ loadedData,
1377
+ children: children2
1378
+ }) {
1379
+ const state = usePreviewVariantRuntime({ variant, loadedData });
1380
+ return /* @__PURE__ */ jsx(Fragment, { children: children2(state) });
1381
+ }
1382
+ export {
1383
+ BRAND,
1384
+ DEFAULTS,
1385
+ PRESET_NAMES,
1386
+ PreviewVariantRuntime,
1387
+ aiMetadataSchema,
1388
+ analyzeComposition,
1389
+ blockDefinitionSchema,
1390
+ budgetBar,
1391
+ checkStoryExclusion,
1392
+ classifyComplexity,
1393
+ compileBlock,
1394
+ compileFragment,
1395
+ compileRecipe,
1396
+ componentRelationSchema,
1397
+ defineBlock,
1398
+ defineFragment,
1399
+ defineRecipe,
1400
+ detectSubComponentPaths,
1401
+ executeVariantLoaders,
1402
+ figma,
1403
+ figmaPropMappingSchema,
1404
+ formatBytes,
1405
+ fragmentBanSchema,
1406
+ fragmentContractSchema,
1407
+ fragmentDefinitionSchema,
1408
+ fragmentGeneratedSchema,
1409
+ fragmentMetaSchema,
1410
+ fragmentUsageSchema,
1411
+ fragmentVariantSchema,
1412
+ fragmentsConfigSchema,
1413
+ generateContext,
1414
+ getPreviewConfig,
1415
+ isConfigExcluded,
1416
+ isExportStory,
1417
+ isFigmaPropMapping,
1418
+ isForceIncluded,
1419
+ parseTokenFile,
1420
+ propDefinitionSchema,
1421
+ recipeDefinitionSchema,
1422
+ resolveFigmaMapping,
1423
+ resolvePerformanceConfig,
1424
+ resolvePreviewRuntimeState,
1425
+ setPreviewConfig,
1426
+ storyModuleToFragment,
1427
+ storyNameFromExport,
1428
+ toId,
1429
+ usePreviewVariantRuntime
1430
+ };
1431
+ //# sourceMappingURL=index.js.map