@fragments-sdk/cli 0.7.9 → 0.7.10

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 (98) hide show
  1. package/dist/bin.js +13 -13
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-CWKQQR6C.js → chunk-57OW43NL.js} +3 -3
  4. package/dist/chunk-57OW43NL.js.map +1 -0
  5. package/dist/{chunk-AA6CAHCZ.js → chunk-7CRC46HV.js} +2 -2
  6. package/dist/chunk-7CRC46HV.js.map +1 -0
  7. package/dist/{chunk-3JPJTU25.js → chunk-CRTN6BIW.js} +5 -5
  8. package/dist/chunk-CRTN6BIW.js.map +1 -0
  9. package/dist/{chunk-LHIIBI6F.js → chunk-M42XIHPV.js} +2 -2
  10. package/dist/{chunk-2EFVPE5Q.js → chunk-TQOGBAOZ.js} +2 -2
  11. package/dist/chunk-TQOGBAOZ.js.map +1 -0
  12. package/dist/core/index.d.ts +1944 -0
  13. package/dist/{core-YAPWXDZW.js → core/index.js} +5 -5
  14. package/dist/defineFragment-C6PFzZyo.d.ts +656 -0
  15. package/dist/{generate-LEBVZCCH.js → generate-ZPERYZLF.js} +4 -4
  16. package/dist/index.d.ts +4 -159
  17. package/dist/index.js +9 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-4VXL3Q6N.js → init-GID2DXB3.js} +69 -7
  20. package/dist/init-GID2DXB3.js.map +1 -0
  21. package/dist/mcp-bin.js +3 -3
  22. package/dist/{scan-3NYSRF6G.js → scan-BSMLGBX4.js} +5 -5
  23. package/dist/{service-HL6TMP3B.js → service-QACVPR37.js} +3 -3
  24. package/dist/{static-viewer-KLD24I4R.js → static-viewer-2RQD5QLR.js} +3 -3
  25. package/dist/{test-Y7YZOJLE.js → test-36UELXTE.js} +3 -3
  26. package/dist/{tokens-M4FCJKBK.js → tokens-A3BZIQPB.js} +4 -4
  27. package/dist/{viewer-ZWQQ74FV.js → viewer-ZA7WK3EY.js} +137 -30
  28. package/dist/viewer-ZA7WK3EY.js.map +1 -0
  29. package/package.json +6 -1
  30. package/src/commands/add.ts +1 -1
  31. package/src/commands/init.ts +84 -4
  32. package/src/core/defineFragment.ts +1 -1
  33. package/src/core/figma.ts +1 -1
  34. package/src/core/index.ts +2 -2
  35. package/src/core/loader.ts +3 -3
  36. package/src/core/schema.ts +1 -1
  37. package/src/index.ts +6 -0
  38. package/src/migrate/converter.ts +1 -1
  39. package/src/service/snippet-validation.test.ts +5 -5
  40. package/src/service/snippet-validation.ts +0 -1
  41. package/src/viewer/__tests__/viewer-integration.test.ts +16 -23
  42. package/src/viewer/components/AccessibilityPanel.tsx +1 -1
  43. package/src/viewer/components/ActionsPanel.tsx +1 -1
  44. package/src/viewer/components/App.tsx +137 -96
  45. package/src/viewer/components/BottomPanel.tsx +1 -1
  46. package/src/viewer/components/CodePanel.naming.test.tsx +1 -1
  47. package/src/viewer/components/CodePanel.tsx +1 -1
  48. package/src/viewer/components/CommandPalette.tsx +1 -1
  49. package/src/viewer/components/ComponentGraph.tsx +1 -1
  50. package/src/viewer/components/ComponentHeader.tsx +1 -1
  51. package/src/viewer/components/ContractPanel.tsx +1 -1
  52. package/src/viewer/components/ErrorBoundary.tsx +1 -1
  53. package/src/viewer/components/HealthDashboard.tsx +1 -1
  54. package/src/viewer/components/HmrStatusIndicator.tsx +1 -1
  55. package/src/viewer/components/InteractionsPanel.tsx +1 -1
  56. package/src/viewer/components/IsolatedRender.tsx +1 -1
  57. package/src/viewer/components/KeyboardShortcutsHelp.tsx +1 -1
  58. package/src/viewer/components/LandingPage.tsx +1 -1
  59. package/src/viewer/components/Layout.tsx +1 -1
  60. package/src/viewer/components/LeftSidebar.tsx +105 -18
  61. package/src/viewer/components/MultiViewportPreview.tsx +1 -1
  62. package/src/viewer/components/PreviewArea.tsx +1 -2
  63. package/src/viewer/components/PreviewFrameHost.tsx +0 -4
  64. package/src/viewer/components/PreviewMenu.tsx +247 -0
  65. package/src/viewer/components/PreviewToolbar.tsx +1 -1
  66. package/src/viewer/components/PropsEditor.tsx +1 -1
  67. package/src/viewer/components/PropsTable.tsx +1 -1
  68. package/src/viewer/components/RightSidebar.tsx +1 -1
  69. package/src/viewer/components/ScreenshotButton.tsx +1 -1
  70. package/src/viewer/components/SkeletonLoader.tsx +1 -1
  71. package/src/viewer/components/Toast.tsx +2 -2
  72. package/src/viewer/components/TokenStylePanel.tsx +1 -1
  73. package/src/viewer/components/VariantMatrix.tsx +1 -1
  74. package/src/viewer/components/VariantTabs.tsx +1 -1
  75. package/src/viewer/components/ViewportSelector.tsx +1 -1
  76. package/src/viewer/constants/ui.ts +14 -0
  77. package/src/viewer/entry.tsx +3 -4
  78. package/src/viewer/hooks/useKeyboardShortcuts.ts +65 -17
  79. package/src/viewer/hooks/useViewSettings.ts +1 -2
  80. package/src/viewer/index.ts +1 -1
  81. package/src/viewer/preview-frame.html +6 -9
  82. package/src/viewer/server.ts +80 -7
  83. package/src/viewer/styles/globals.css +12 -51
  84. package/src/viewer/vite-plugin.ts +70 -9
  85. package/dist/chunk-2EFVPE5Q.js.map +0 -1
  86. package/dist/chunk-3JPJTU25.js.map +0 -1
  87. package/dist/chunk-AA6CAHCZ.js.map +0 -1
  88. package/dist/chunk-CWKQQR6C.js.map +0 -1
  89. package/dist/init-4VXL3Q6N.js.map +0 -1
  90. package/dist/viewer-ZWQQ74FV.js.map +0 -1
  91. /package/dist/{chunk-LHIIBI6F.js.map → chunk-M42XIHPV.js.map} +0 -0
  92. /package/dist/{core-YAPWXDZW.js.map → core/index.js.map} +0 -0
  93. /package/dist/{generate-LEBVZCCH.js.map → generate-ZPERYZLF.js.map} +0 -0
  94. /package/dist/{scan-3NYSRF6G.js.map → scan-BSMLGBX4.js.map} +0 -0
  95. /package/dist/{service-HL6TMP3B.js.map → service-QACVPR37.js.map} +0 -0
  96. /package/dist/{static-viewer-KLD24I4R.js.map → static-viewer-2RQD5QLR.js.map} +0 -0
  97. /package/dist/{test-Y7YZOJLE.js.map → test-36UELXTE.js.map} +0 -0
  98. /package/dist/{tokens-M4FCJKBK.js.map → tokens-A3BZIQPB.js.map} +0 -0
package/src/core/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Browser-safe exports for @fragments/core
3
- * For Node.js-only APIs (config, discovery, loader), use '@fragments/core/node'
2
+ * Browser-safe exports for @fragments-sdk/cli/core
3
+ * For Node.js-only APIs (config, discovery, loader), import from '@fragments-sdk/cli' directly.
4
4
  */
5
5
 
6
6
  // Brand and default constants
@@ -27,8 +27,8 @@ function createFragmentsCoreShimPlugin(): Plugin {
27
27
  return {
28
28
  name: BRAND.vitePluginNamespace,
29
29
  setup(build) {
30
- // Intercept @fragments/core imports
31
- build.onResolve({ filter: /^@fragments\/core$/ }, (args) => {
30
+ // Intercept @fragments-sdk/cli/core imports
31
+ build.onResolve({ filter: /^@fragments-sdk\/cli\/core$/ }, (args) => {
32
32
  return {
33
33
  path: args.path,
34
34
  namespace: BRAND.vitePluginNamespace,
@@ -71,7 +71,7 @@ export async function loadFragmentFile(
71
71
 
72
72
  try {
73
73
  // Use esbuild to bundle the fragment file
74
- // We inject a shim for @fragments/core so it doesn't need to be installed
74
+ // We inject a shim for @fragments-sdk/cli/core so it doesn't need to be installed
75
75
  // Using CommonJS format to avoid ESM/CJS interop issues with node_modules
76
76
  await build({
77
77
  entryPoints: [absolutePath],
@@ -149,7 +149,7 @@ export const fragmentGeneratedSchema = z.object({
149
149
  * Schema for AI-specific metadata for playground context generation
150
150
  */
151
151
  export const aiMetadataSchema = z.object({
152
- compositionPattern: z.enum(['compound', 'simple', 'controlled']).optional(),
152
+ compositionPattern: z.enum(['compound', 'simple', 'controlled', 'wrapper']).optional(),
153
153
  subComponents: z.array(z.string()).optional(),
154
154
  requiredChildren: z.array(z.string()).optional(),
155
155
  commonPatterns: z.array(z.string()).optional(),
package/src/index.ts CHANGED
@@ -43,6 +43,12 @@ export type { AnalyzeOptions, AnalyzeResult } from "./analyze.js";
43
43
  // Static Viewer
44
44
  export { generateStaticViewer, generateViewerFromJson } from "./static-viewer.js";
45
45
 
46
+ // Config type (used by generated fragments.config.ts)
47
+ export type { FragmentsConfig } from "./core/types.js";
48
+
49
+ // Fragment definition API (used by generated .fragment.tsx files)
50
+ export { defineFragment, defineBlock } from "./core/defineFragment.js";
51
+
46
52
  // CLI Command metadata (for docs)
47
53
  export { CLI_COMMANDS, CLI_COMMAND_CATEGORIES } from "./cli-commands.js";
48
54
  export type { CliCommandDef, CliOptionDef, CliCommandCategory, CliCategoryInfo } from "./cli-commands.js";
@@ -550,7 +550,7 @@ ${generated.skippedVariants.map(sv => ` { name: "${escapeString(sv.name)}",
550
550
  }
551
551
 
552
552
  // Import the actual component - this makes the fragment immediately usable
553
- return `import { defineFragment } from "@fragments/core";
553
+ return `import { defineFragment } from "@fragments-sdk/cli/core";
554
554
  import { ${componentName} } from "${componentImport}";
555
555
 
556
556
  export default defineFragment({
@@ -45,7 +45,7 @@ describe('validateSnippetPolicy', () => {
45
45
  projectDir,
46
46
  'Button',
47
47
  `import React from 'react';
48
- import { defineFragment } from '@fragments/core';
48
+ import { defineFragment } from '@fragments-sdk/cli/core';
49
49
  import { Button } from '.';
50
50
 
51
51
  export default defineFragment({
@@ -86,7 +86,7 @@ export default defineFragment({
86
86
  projectDir,
87
87
  'Icon',
88
88
  `import React from 'react';
89
- import { defineFragment } from '@fragments/core';
89
+ import { defineFragment } from '@fragments-sdk/cli/core';
90
90
  import { Icon } from '.';
91
91
 
92
92
  export default defineFragment({
@@ -124,7 +124,7 @@ import { House } from '@phosphor-icons/react';
124
124
  projectDir,
125
125
  'Header',
126
126
  `import React from 'react';
127
- import { defineFragment } from '@fragments/core';
127
+ import { defineFragment } from '@fragments-sdk/cli/core';
128
128
  import { Header } from '.';
129
129
 
130
130
  export default defineFragment({
@@ -166,7 +166,7 @@ export default defineFragment({
166
166
  projectDir,
167
167
  'Alpha',
168
168
  `import React from 'react';
169
- import { defineFragment } from '@fragments/core';
169
+ import { defineFragment } from '@fragments-sdk/cli/core';
170
170
  import { Alpha } from '.';
171
171
 
172
172
  export default defineFragment({
@@ -183,7 +183,7 @@ export default defineFragment({
183
183
  projectDir,
184
184
  'Beta',
185
185
  `import React from 'react';
186
- import { defineFragment } from '@fragments/core';
186
+ import { defineFragment } from '@fragments-sdk/cli/core';
187
187
  import { Beta } from '.';
188
188
 
189
189
  export default defineFragment({
@@ -118,7 +118,6 @@ function normalizePolicy(
118
118
  function isFragmentsModule(modulePath: string): boolean {
119
119
  return (
120
120
  modulePath === '@fragments-sdk/ui'
121
- || modulePath === '@fragments/ui'
122
121
  || modulePath === '.'
123
122
  || modulePath === '..'
124
123
  || modulePath.startsWith('@/components/')
@@ -12,7 +12,7 @@ import { discoverInstalledFragments } from "../../core/discovery.js";
12
12
  * After packages were merged into @fragments-sdk/cli, the viewer's
13
13
  * path resolution changed. These tests verify that:
14
14
  * - Viewer assets (HTML, TSX entry points) are found at the correct paths
15
- * - The @fragments/core alias resolves to the consolidated core source
15
+ * - The @fragments-sdk/cli/core alias resolves to the consolidated core source
16
16
  * - The virtual module generates valid import statements
17
17
  * - The Vite config references correct file system locations
18
18
  */
@@ -61,9 +61,9 @@ describe("viewer path resolution", () => {
61
61
  });
62
62
  });
63
63
 
64
- describe("@fragments/core alias resolution", () => {
64
+ describe("@fragments-sdk/cli/core alias resolution", () => {
65
65
  it("core/index.ts exists at the expected path", () => {
66
- // server.ts: "@fragments/core": resolve(cliPackageRoot, "src/core/index.ts")
66
+ // server.ts: "@fragments-sdk/cli/core": resolve(cliPackageRoot, "src/core/index.ts")
67
67
  const corePath = resolve(cliPackageRoot, "src/core/index.ts");
68
68
  expect(existsSync(corePath)).toBe(true);
69
69
  });
@@ -82,32 +82,25 @@ describe("@fragments/core alias resolution", () => {
82
82
  });
83
83
  });
84
84
 
85
- describe("virtual module @fragments/core import", () => {
86
- it("vite-plugin generates import from @fragments/core (resolved via alias)", async () => {
87
- // The virtual module template in vite-plugin.ts must import from @fragments/core
85
+ describe("virtual module @fragments-sdk/cli/core import", () => {
86
+ it("vite-plugin generates import from @fragments-sdk/cli/core (resolved via alias)", async () => {
87
+ // The virtual module template in vite-plugin.ts must import from @fragments-sdk/cli/core
88
88
  // which is resolved by the Vite alias to the consolidated core source
89
89
  const pluginPath = resolve(viewerDir, "vite-plugin.ts");
90
90
  const content = await readFile(pluginPath, "utf-8");
91
91
 
92
- // The generated virtual module string should reference @fragments/core
92
+ // The generated virtual module string should reference @fragments-sdk/cli/core
93
93
  expect(content).toContain(
94
- 'import { storyModuleToFragment, setPreviewConfig } from "@fragments/core"'
94
+ 'import { storyModuleToFragment, setPreviewConfig } from "@fragments-sdk/cli/core"'
95
95
  );
96
96
  });
97
97
 
98
- it("server.ts sets up @fragments/core alias to src/core/index.ts", async () => {
98
+ it("server.ts sets up @fragments-sdk/cli/core alias to src/core/index.ts", async () => {
99
99
  const serverPath = resolve(viewerDir, "server.ts");
100
100
  const content = await readFile(serverPath, "utf-8");
101
101
 
102
- // The alias must resolve @fragments/core to the consolidated core
103
- expect(content).toContain('"@fragments/core": resolve(cliPackageRoot, "src/core/index.ts")');
104
- });
105
-
106
- it("server.ts sets up @fragments/viewer alias", async () => {
107
- const serverPath = resolve(viewerDir, "server.ts");
108
- const content = await readFile(serverPath, "utf-8");
109
-
110
- expect(content).toContain('"@fragments/viewer": viewerRoot');
102
+ // The alias must resolve @fragments-sdk/cli/core to the CLI's core source
103
+ expect(content).toContain('"@fragments-sdk/cli/core": resolve(cliPackageRoot, "src/core/index.ts")');
111
104
  });
112
105
 
113
106
  it("vite-plugin merges authored variant code from metadata fragments", async () => {
@@ -228,20 +221,20 @@ describe("no stale @fragments/* package references", () => {
228
221
  const serverPath = resolve(viewerDir, "server.ts");
229
222
  const content = await readFile(serverPath, "utf-8");
230
223
 
231
- // Should NOT try to resolve @fragments/core from node_modules
224
+ // Should NOT try to resolve @fragments-sdk/cli/core from node_modules
232
225
  expect(content).not.toContain('resolveFragmentsPackage("core"');
233
226
  });
234
227
 
235
- it("no source files import from @fragments/core as a runtime dependency", async () => {
228
+ it("no source files import from @fragments-sdk/cli/core as a runtime dependency", async () => {
236
229
  // All source-level imports should use relative paths (../core/).
237
- // Only the generated virtual module string uses @fragments/core (resolved via alias).
230
+ // Only the generated virtual module string uses @fragments-sdk/cli/core (resolved via alias).
238
231
  const serverPath = resolve(viewerDir, "server.ts");
239
232
  const content = await readFile(serverPath, "utf-8");
240
233
 
241
- // server.ts should import from relative paths, not @fragments/core
234
+ // server.ts should import from relative paths, not @fragments-sdk/cli/core
242
235
  const lines = content.split("\n");
243
236
  const importLines = lines.filter(
244
- (l) => l.startsWith("import") && l.includes("@fragments/core")
237
+ (l) => l.startsWith("import") && l.includes("@fragments-sdk/cli/core")
245
238
  );
246
239
  expect(importLines).toHaveLength(0);
247
240
  });
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { useState, useEffect, useCallback, useMemo, useRef } from "react";
13
13
  import type { Result, NodeResult, ImpactValue, RunOptions } from "axe-core";
14
- import { Badge, Tabs, Dialog, Card, Alert, Text, Stack, Box, Button, Chip, EmptyState } from "@fragments/ui";
14
+ import { Badge, Tabs, Dialog, Card, Alert, Text, Stack, Box, Button, Chip, EmptyState } from "@fragments-sdk/ui";
15
15
  import { BRAND } from "../../core/index.js";
16
16
  import {
17
17
  updateComponentA11yResult,
@@ -8,7 +8,7 @@
8
8
  import { useState, useMemo } from "react";
9
9
  import type { ActionLog } from "../hooks/useActions.js";
10
10
  import { formatActionArg } from "../hooks/useActions.js";
11
- import { Button, Stack, Text, Badge, Input, Menu, Separator } from "@fragments/ui";
11
+ import { Button, Stack, Text, Badge, Input, Menu, Separator } from "@fragments-sdk/ui";
12
12
  import {
13
13
  TrashIcon,
14
14
  ChevronDownIcon,
@@ -15,8 +15,8 @@ import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp.js";
15
15
  import { useToast } from "./Toast.js";
16
16
 
17
17
  // Toolbar
18
- import { PreviewToolbar, getBackgroundStyle } from "./PreviewToolbar.js";
19
- import { ViewportSelector } from "./ViewportSelector.js";
18
+ import { PreviewMenu } from "./PreviewMenu.js";
19
+ import { getBackgroundStyle } from "../constants/ui.js";
20
20
 
21
21
  // Preview & Rendering
22
22
  import { PreviewArea } from "./PreviewArea.js";
@@ -28,10 +28,10 @@ import { useAllFigmaUrls } from "./FigmaEmbed.js";
28
28
  import { ActionCapture } from "./ActionCapture.js";
29
29
 
30
30
  // Fragments UI
31
- import { Header, Stack, Text, Separator, Tooltip, Button, EmptyState, Box, Alert, ScrollArea, Input, ThemeToggle } from "@fragments/ui";
31
+ import { Header, Stack, Text, Separator, Tooltip, Button, EmptyState, Box, Alert, ScrollArea, Input, ThemeToggle } from "@fragments-sdk/ui";
32
32
 
33
33
  // Icons
34
- import { EmptyIcon, ExternalLinkIcon, FigmaIcon, CompareIcon, CheckIcon, LinkIcon, GridIcon, DevicesIcon } from "./Icons.js";
34
+ import { EmptyIcon, FigmaIcon, CompareIcon } from "./Icons.js";
35
35
 
36
36
  // Logo
37
37
  import { fragmentsLogo } from "../assets/fragments-logo.js";
@@ -54,9 +54,6 @@ import { useUrlState, findFragmentByName, findVariantIndex } from "../hooks/useU
54
54
  import { usePanelDock } from "./ResizablePanel.js";
55
55
  import { useTheme } from "./ThemeProvider.js";
56
56
 
57
- // Utilities
58
- import { ScreenshotButton } from "./ScreenshotButton.js";
59
-
60
57
  interface AppProps {
61
58
  fragments: Array<{ path: string; fragment: FragmentDefinition }>;
62
59
  }
@@ -237,6 +234,8 @@ export function App({ fragments }: AppProps) {
237
234
  goToVariant: (index) => index < variantCount && handleSelectVariant(index),
238
235
  toggleTheme: viewSettings.toggleTheme,
239
236
  togglePanel: uiActions.togglePanel,
237
+ toggleMatrix: () => uiActions.setMatrixView(!uiState.showMatrixView),
238
+ toggleResponsive: () => uiActions.setMultiViewport(!uiState.showMultiViewport),
240
239
  copyLink: handleCopyLink,
241
240
  showHelp: uiActions.toggleShortcutsHelp,
242
241
  openSearch: focusSearchInput,
@@ -307,11 +306,19 @@ export function App({ fragments }: AppProps) {
307
306
  uiState={uiState}
308
307
  uiActions={uiActions}
309
308
  figmaUrl={figmaUrl}
310
- linkCopied={uiState.linkCopied}
311
- onCopyLink={handleCopyLink}
312
309
  searchQuery={searchQuery}
313
310
  onSearchChange={setSearchQuery}
314
311
  searchInputRef={searchInputRef}
312
+ onPrevComponent={() => {
313
+ const prevIndex = currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
314
+ handleSelectFragment(sortedFragmentPaths[prevIndex]);
315
+ }}
316
+ onNextComponent={() => {
317
+ const nextIndex = currentFragmentIndex < sortedFragmentPaths.length - 1 ? currentFragmentIndex + 1 : 0;
318
+ handleSelectFragment(sortedFragmentPaths[nextIndex]);
319
+ }}
320
+ onPrevVariant={() => handleSelectVariant(activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1)}
321
+ onNextVariant={() => handleSelectVariant(activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0)}
315
322
  />
316
323
  ) : (
317
324
  <ViewerHeader
@@ -362,9 +369,6 @@ export function App({ fragments }: AppProps) {
362
369
  activeIndex={activeVariantIndex}
363
370
  onSelect={handleSelectVariant}
364
371
  showMatrixView={uiState.showMatrixView}
365
- showMultiViewport={uiState.showMultiViewport}
366
- onToggleMatrix={() => uiActions.setMatrixView(!uiState.showMatrixView)}
367
- onToggleMultiViewport={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
368
372
  />
369
373
  )}
370
374
 
@@ -455,11 +459,13 @@ interface TopToolbarProps {
455
459
  uiState: ReturnType<typeof useAppState>['state'];
456
460
  uiActions: ReturnType<typeof useAppState>['actions'];
457
461
  figmaUrl?: string;
458
- linkCopied: boolean;
459
- onCopyLink: () => void;
460
462
  searchQuery: string;
461
463
  onSearchChange: (value: string) => void;
462
464
  searchInputRef: RefObject<HTMLInputElement>;
465
+ onPrevComponent: () => void;
466
+ onNextComponent: () => void;
467
+ onPrevVariant: () => void;
468
+ onNextVariant: () => void;
463
469
  }
464
470
 
465
471
  interface ViewerHeaderProps {
@@ -543,16 +549,36 @@ function TopToolbar({
543
549
  uiState,
544
550
  uiActions,
545
551
  figmaUrl,
546
- linkCopied,
547
- onCopyLink,
548
552
  searchQuery,
549
553
  onSearchChange,
550
554
  searchInputRef,
555
+ onPrevComponent,
556
+ onNextComponent,
557
+ onPrevVariant,
558
+ onNextVariant,
551
559
  }: TopToolbarProps) {
552
560
  const { setTheme, resolvedTheme } = useTheme();
553
561
  return (
554
562
  <Header aria-label="Component preview toolbar">
555
563
  <Header.Trigger />
564
+ <PreviewMenu
565
+ zoom={viewSettings.zoom}
566
+ background={viewSettings.background}
567
+ viewport={viewSettings.viewport}
568
+ showMatrixView={uiState.showMatrixView}
569
+ showMultiViewport={uiState.showMultiViewport}
570
+ panelOpen={uiState.panelOpen}
571
+ onZoomChange={viewSettings.setZoom}
572
+ onBackgroundChange={viewSettings.setBackground}
573
+ onViewportChange={viewSettings.setViewport}
574
+ onToggleMatrix={() => uiActions.setMatrixView(!uiState.showMatrixView)}
575
+ onToggleMultiViewport={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
576
+ onTogglePanel={uiActions.togglePanel}
577
+ onPrevComponent={onPrevComponent}
578
+ onNextComponent={onNextComponent}
579
+ onPrevVariant={onPrevVariant}
580
+ onNextVariant={onNextVariant}
581
+ />
556
582
  <Header.Brand>
557
583
  <Stack direction="row" align="center" gap="sm">
558
584
  <img src={fragmentsLogo} alt="" width={20} height={20} style={{ display: 'block' }} />
@@ -563,21 +589,6 @@ function TopToolbar({
563
589
  <HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
564
590
  <Header.Spacer />
565
591
  <Header.Actions>
566
- <PreviewToolbar
567
- zoom={viewSettings.zoom}
568
- background={viewSettings.background}
569
- onZoomChange={viewSettings.setZoom}
570
- onBackgroundChange={viewSettings.setBackground}
571
- />
572
- <Separator orientation="vertical" style={{ height: '16px' }} />
573
- <ViewportSelector
574
- viewport={viewSettings.viewport}
575
- customSize={viewSettings.customSize}
576
- onViewportChange={viewSettings.setViewport}
577
- onCustomSizeChange={viewSettings.setCustomSize}
578
- />
579
- <Separator orientation="vertical" style={{ height: '16px' }} />
580
-
581
592
  {figmaUrl && (
582
593
  <>
583
594
  <Tooltip content={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}>
@@ -602,41 +613,6 @@ function TopToolbar({
602
613
  <Separator orientation="vertical" style={{ height: '16px' }} />
603
614
  </>
604
615
  )}
605
-
606
- {variant && (
607
- <>
608
- <Tooltip content="Open in new window">
609
- <Button
610
- onClick={() => {
611
- const url = new URL(window.location.href);
612
- url.hash = '';
613
- url.searchParams.set('isolated', 'true');
614
- url.searchParams.set('component', fragment.fragment.meta.name);
615
- url.searchParams.set('variant', variant.name);
616
- if (viewSettings.zoom !== 100) url.searchParams.set('zoom', String(viewSettings.zoom));
617
- if (viewSettings.background !== 'transparent') url.searchParams.set('bg', viewSettings.background);
618
- window.open(url.toString(), '_blank', 'noopener,noreferrer');
619
- }}
620
- variant="ghost"
621
- size="sm"
622
- >
623
- <ExternalLinkIcon style={{ width: '16px', height: '16px' }} />
624
- </Button>
625
- </Tooltip>
626
- <ScreenshotButton componentName={fragment.fragment.meta.name} variantName={variant.name} />
627
- <Tooltip content="Copy link to share">
628
- <Button
629
- onClick={onCopyLink}
630
- variant="ghost"
631
- size="sm"
632
- style={linkCopied ? { color: '#16a34a', backgroundColor: 'rgba(22, 163, 74, 0.1)' } : {}}
633
- >
634
- {linkCopied ? <CheckIcon style={{ width: '16px', height: '16px' }} /> : <LinkIcon style={{ width: '16px', height: '16px' }} />}
635
- </Button>
636
- </Tooltip>
637
- </>
638
- )}
639
- <Separator orientation="vertical" style={{ height: '16px' }} />
640
616
  <ThemeToggle
641
617
  size="sm"
642
618
  value={resolvedTheme}
@@ -672,14 +648,11 @@ interface VariantTabsBarProps {
672
648
  activeIndex: number;
673
649
  onSelect: (index: number) => void;
674
650
  showMatrixView: boolean;
675
- showMultiViewport: boolean;
676
- onToggleMatrix: () => void;
677
- onToggleMultiViewport: () => void;
678
651
  }
679
652
 
680
- function VariantTabsBar({ variants, activeIndex, onSelect, showMatrixView, showMultiViewport, onToggleMatrix, onToggleMultiViewport }: VariantTabsBarProps) {
653
+ function VariantTabsBar({ variants, activeIndex, onSelect, showMatrixView }: VariantTabsBarProps) {
681
654
  return (
682
- <Stack direction="row" align="center" justify="between" style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
655
+ <div style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
683
656
  {!showMatrixView ? (
684
657
  <ScrollArea orientation="horizontal" showFades style={{ flex: 1, minWidth: 0 }}>
685
658
  <VariantTabs variants={variants} activeIndex={activeIndex} onSelect={onSelect} />
@@ -687,31 +660,7 @@ function VariantTabsBar({ variants, activeIndex, onSelect, showMatrixView, showM
687
660
  ) : (
688
661
  <Text size="sm" color="secondary">Showing all {variants.length} variants</Text>
689
662
  )}
690
- <Stack direction="row" align="center" gap="sm" style={{ marginLeft: '16px', flexShrink: 0 }}>
691
- {variants.length > 1 && (
692
- <Button
693
- onClick={onToggleMatrix}
694
- variant="ghost"
695
- size="sm"
696
- title={showMatrixView ? "Show single variant" : "Show all variants in grid"}
697
- style={showMatrixView ? { backgroundColor: 'rgba(59, 130, 246, 0.1)', color: 'var(--color-accent)' } : {}}
698
- >
699
- <GridIcon style={{ width: '16px', height: '16px' }} />
700
- {showMatrixView ? "Exit Matrix" : "Matrix"}
701
- </Button>
702
- )}
703
- <Button
704
- onClick={onToggleMultiViewport}
705
- variant="ghost"
706
- size="sm"
707
- title={showMultiViewport ? "Exit multi-viewport" : "Show at multiple screen sizes"}
708
- style={showMultiViewport ? { backgroundColor: 'rgba(34, 197, 94, 0.1)', color: '#16a34a' } : {}}
709
- >
710
- <DevicesIcon style={{ width: '16px', height: '16px' }} />
711
- {showMultiViewport ? "Exit Responsive" : "Responsive"}
712
- </Button>
713
- </Stack>
714
- </Stack>
663
+ </div>
715
664
  );
716
665
  }
717
666
 
@@ -721,6 +670,12 @@ interface NoVariantsMessageProps {
721
670
  }
722
671
 
723
672
  function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
673
+ // Check for load error (missing dependencies, schema errors, etc.)
674
+ const loadError = (fragment as any)?._loadError;
675
+ if (loadError) {
676
+ return <LoadErrorMessage error={loadError} componentName={fragment?.meta?.name} />;
677
+ }
678
+
724
679
  const skippedVariants = (fragment?._generated as any)?.skippedVariants;
725
680
 
726
681
  if (!skippedVariants || skippedVariants.length === 0) {
@@ -761,6 +716,92 @@ function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
761
716
  );
762
717
  }
763
718
 
719
+ // Load error message — shown when a fragment failed to import (missing deps, schema errors, etc.)
720
+ function LoadErrorMessage({ error, componentName }: { error: { message: string; dependencies: string[] }; componentName?: string }) {
721
+ const deps = error.dependencies || [];
722
+ const errorMessage = error.message || 'Unknown error';
723
+
724
+ // Determine if the error is a missing module/dependency issue
725
+ const isModuleError = /Failed to resolve import|Cannot find module|Module not found|does not provide an export/.test(errorMessage);
726
+
727
+ // Only suggest packages if the error is actually about missing modules
728
+ let suggestedPackages: string[] = [];
729
+ if (isModuleError) {
730
+ if (deps.length > 0) {
731
+ suggestedPackages = [...deps];
732
+ } else {
733
+ const match = errorMessage.match(/Failed to resolve import ["']([^"']+)["']/);
734
+ if (match) {
735
+ suggestedPackages = [match[1]];
736
+ }
737
+ }
738
+ }
739
+
740
+ const hasMissingDeps = suggestedPackages.length > 0;
741
+ const installCmd = `pnpm add ${suggestedPackages.join(' ')}`;
742
+
743
+ return (
744
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', padding: '24px' }}>
745
+ <Alert variant="warning">
746
+ <Alert.Body>
747
+ <Alert.Title>
748
+ {hasMissingDeps ? 'Missing Dependencies' : 'Failed to Load'}
749
+ </Alert.Title>
750
+ <Alert.Content>
751
+ <Stack direction="column" gap="sm">
752
+ {hasMissingDeps ? (
753
+ <>
754
+ <Text size="xs" color="secondary">
755
+ {componentName ? `${componentName} requires` : 'This component requires'} packages that are not installed in your project.
756
+ </Text>
757
+ <Text size="xs" weight="semibold" color="secondary">Install with:</Text>
758
+ <code style={{
759
+ display: 'block',
760
+ padding: '8px 12px',
761
+ borderRadius: '6px',
762
+ backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
763
+ fontFamily: 'monospace',
764
+ fontSize: '12px',
765
+ color: 'var(--text-primary, #111827)',
766
+ userSelect: 'all',
767
+ }}>
768
+ {installCmd}
769
+ </code>
770
+ <Text size="xs" color="tertiary">
771
+ After installing, restart the dev server.
772
+ </Text>
773
+ </>
774
+ ) : (
775
+ <>
776
+ <Text size="xs" color="secondary">
777
+ {componentName ? `${componentName} couldn't` : 'This component couldn\'t'} be loaded. This may be due to a schema validation error or missing imports.
778
+ </Text>
779
+ <Text size="xs" weight="semibold" color="secondary">Error:</Text>
780
+ <pre style={{
781
+ padding: '8px 12px',
782
+ borderRadius: '6px',
783
+ backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
784
+ fontFamily: 'monospace',
785
+ fontSize: '11px',
786
+ color: 'var(--text-secondary, #374151)',
787
+ whiteSpace: 'pre-wrap',
788
+ wordBreak: 'break-word',
789
+ margin: 0,
790
+ maxHeight: '200px',
791
+ overflow: 'auto',
792
+ }}>
793
+ {errorMessage}
794
+ </pre>
795
+ </>
796
+ )}
797
+ </Stack>
798
+ </Alert.Content>
799
+ </Alert.Body>
800
+ </Alert>
801
+ </div>
802
+ );
803
+ }
804
+
764
805
  // Empty variant message
765
806
  interface EmptyVariantMessageProps {
766
807
  reason: string;
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { memo, useCallback } from 'react';
7
7
  import type { FragmentDefinition, FragmentVariant } from '../../core/index.js';
8
- import { Tabs, Badge } from '@fragments/ui';
8
+ import { Tabs, Badge } from '@fragments-sdk/ui';
9
9
  import { ResizablePanel } from './ResizablePanel.js';
10
10
  import { CodePanel } from './CodePanel.js';
11
11
  import { TokenStylePanel } from './TokenStylePanel.js';
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import type { FragmentVariant } from '../../core/index.js';
3
3
 
4
- vi.mock('@fragments/ui', () => ({
4
+ vi.mock('@fragments-sdk/ui', () => ({
5
5
  CodeBlock: () => null,
6
6
  }));
7
7
 
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from 'react';
2
- import { CodeBlock } from '@fragments/ui';
2
+ import { CodeBlock } from '@fragments-sdk/ui';
3
3
  import type { FragmentVariant, PropDefinition } from '../../core/index.js';
4
4
 
5
5
  interface CodePanelProps {
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { useState, useEffect, useRef, useMemo, useCallback } from "react";
13
- import { Dialog, Stack, Text, Badge, Separator, Input } from '@fragments/ui';
13
+ import { Dialog, Stack, Text, Badge, Separator, Input } from '@fragments-sdk/ui';
14
14
  import type { FragmentDefinition } from "../../core/index.js";
15
15
  import { SearchIcon, ChevronRightIcon } from "./Icons.js";
16
16
 
@@ -12,7 +12,7 @@ import { useMemo, useState } from "react";
12
12
  import type { FragmentDefinition, ComponentRelation, RelationshipType } from "../../core/index.js";
13
13
  import { ChevronRightIcon, EmptyIcon, WandIcon } from "./Icons.js";
14
14
  import { detectAllRelationships, mergeRelationships } from "../utils/detectRelationships.js";
15
- import { Stack, Text, Badge, Button, EmptyState, CodeBlock, Grid, Separator } from "@fragments/ui";
15
+ import { Stack, Text, Badge, Button, EmptyState, CodeBlock, Grid, Separator } from "@fragments-sdk/ui";
16
16
 
17
17
  interface ComponentGraphProps {
18
18
  /** Current fragment definition */
@@ -1,5 +1,5 @@
1
1
  import type { FragmentMeta } from '../../core/index.js';
2
- import { Badge, Alert, Text, Stack, Separator } from '@fragments/ui';
2
+ import { Badge, Alert, Text, Stack, Separator } from '@fragments-sdk/ui';
3
3
  import { WarningIcon, BeakerIcon } from './Icons.js';
4
4
 
5
5
  interface ComponentHeaderProps {
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { memo } from 'react';
7
7
  import type { FragmentContract } from '../../core/index.js';
8
- import { Stack, Text, Card, Chip, Alert, EmptyState, CodeBlock, Badge } from '@fragments/ui';
8
+ import { Stack, Text, Card, Chip, Alert, EmptyState, CodeBlock, Badge } from '@fragments-sdk/ui';
9
9
 
10
10
  interface ContractPanelProps {
11
11
  contract?: FragmentContract;
@@ -1,5 +1,5 @@
1
1
  import { Component, type ReactNode, type ErrorInfo } from 'react';
2
- import { Button, Alert, CodeBlock, Collapsible, Stack, Text } from '@fragments/ui';
2
+ import { Button, Alert, CodeBlock, Collapsible, Stack, Text } from '@fragments-sdk/ui';
3
3
  import { ErrorIcon, RefreshIcon } from './Icons.js';
4
4
 
5
5
  interface ErrorBoundaryProps {