@fragments-sdk/cli 0.7.8 → 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 (99) 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/assets/fragments-logo.ts +4 -0
  43. package/src/viewer/components/AccessibilityPanel.tsx +1 -1
  44. package/src/viewer/components/ActionsPanel.tsx +1 -1
  45. package/src/viewer/components/App.tsx +138 -97
  46. package/src/viewer/components/BottomPanel.tsx +1 -1
  47. package/src/viewer/components/CodePanel.naming.test.tsx +1 -1
  48. package/src/viewer/components/CodePanel.tsx +1 -1
  49. package/src/viewer/components/CommandPalette.tsx +1 -1
  50. package/src/viewer/components/ComponentGraph.tsx +1 -1
  51. package/src/viewer/components/ComponentHeader.tsx +1 -1
  52. package/src/viewer/components/ContractPanel.tsx +1 -1
  53. package/src/viewer/components/ErrorBoundary.tsx +1 -1
  54. package/src/viewer/components/HealthDashboard.tsx +1 -1
  55. package/src/viewer/components/HmrStatusIndicator.tsx +1 -1
  56. package/src/viewer/components/InteractionsPanel.tsx +1 -1
  57. package/src/viewer/components/IsolatedRender.tsx +1 -1
  58. package/src/viewer/components/KeyboardShortcutsHelp.tsx +1 -1
  59. package/src/viewer/components/LandingPage.tsx +1 -1
  60. package/src/viewer/components/Layout.tsx +1 -1
  61. package/src/viewer/components/LeftSidebar.tsx +105 -18
  62. package/src/viewer/components/MultiViewportPreview.tsx +1 -1
  63. package/src/viewer/components/PreviewArea.tsx +1 -2
  64. package/src/viewer/components/PreviewFrameHost.tsx +0 -4
  65. package/src/viewer/components/PreviewMenu.tsx +247 -0
  66. package/src/viewer/components/PreviewToolbar.tsx +1 -1
  67. package/src/viewer/components/PropsEditor.tsx +1 -1
  68. package/src/viewer/components/PropsTable.tsx +1 -1
  69. package/src/viewer/components/RightSidebar.tsx +1 -1
  70. package/src/viewer/components/ScreenshotButton.tsx +1 -1
  71. package/src/viewer/components/SkeletonLoader.tsx +1 -1
  72. package/src/viewer/components/Toast.tsx +2 -2
  73. package/src/viewer/components/TokenStylePanel.tsx +1 -1
  74. package/src/viewer/components/VariantMatrix.tsx +1 -1
  75. package/src/viewer/components/VariantTabs.tsx +1 -1
  76. package/src/viewer/components/ViewportSelector.tsx +1 -1
  77. package/src/viewer/constants/ui.ts +14 -0
  78. package/src/viewer/entry.tsx +3 -4
  79. package/src/viewer/hooks/useKeyboardShortcuts.ts +65 -17
  80. package/src/viewer/hooks/useViewSettings.ts +1 -2
  81. package/src/viewer/index.ts +1 -1
  82. package/src/viewer/preview-frame.html +6 -9
  83. package/src/viewer/server.ts +80 -7
  84. package/src/viewer/styles/globals.css +12 -51
  85. package/src/viewer/vite-plugin.ts +70 -9
  86. package/dist/chunk-2EFVPE5Q.js.map +0 -1
  87. package/dist/chunk-3JPJTU25.js.map +0 -1
  88. package/dist/chunk-AA6CAHCZ.js.map +0 -1
  89. package/dist/chunk-CWKQQR6C.js.map +0 -1
  90. package/dist/init-4VXL3Q6N.js.map +0 -1
  91. package/dist/viewer-ZWQQ74FV.js.map +0 -1
  92. /package/dist/{chunk-LHIIBI6F.js.map → chunk-M42XIHPV.js.map} +0 -0
  93. /package/dist/{core-YAPWXDZW.js.map → core/index.js.map} +0 -0
  94. /package/dist/{generate-LEBVZCCH.js.map → generate-ZPERYZLF.js.map} +0 -0
  95. /package/dist/{scan-3NYSRF6G.js.map → scan-BSMLGBX4.js.map} +0 -0
  96. /package/dist/{service-HL6TMP3B.js.map → service-QACVPR37.js.map} +0 -0
  97. /package/dist/{static-viewer-KLD24I4R.js.map → static-viewer-2RQD5QLR.js.map} +0 -0
  98. /package/dist/{test-Y7YZOJLE.js.map → test-36UELXTE.js.map} +0 -0
  99. /package/dist/{tokens-M4FCJKBK.js.map → tokens-A3BZIQPB.js.map} +0 -0
@@ -126,6 +126,20 @@ export const VIEWPORT_PRESETS = {
126
126
 
127
127
  export type ViewportPreset = keyof typeof VIEWPORT_PRESETS | 'custom';
128
128
 
129
+ export interface ViewportSize {
130
+ width: number | null;
131
+ height: number | null;
132
+ }
133
+
134
+ /**
135
+ * Get viewport width from preset.
136
+ */
137
+ export function getViewportWidth(viewport: ViewportPreset, customSize: ViewportSize): number | null {
138
+ if (viewport === 'custom') return customSize.width;
139
+ if (viewport === 'responsive') return null;
140
+ return VIEWPORT_PRESETS[viewport]?.width ?? null;
141
+ }
142
+
129
143
  /**
130
144
  * Get CSS background style for preview pane.
131
145
  */
@@ -4,11 +4,10 @@ import { App } from "./components/App.js";
4
4
  import { ThemeProvider } from "./components/ThemeProvider.js";
5
5
  import { ToastProvider } from "./components/Toast.js";
6
6
  import { AppSkeleton } from "./components/SkeletonLoader.js";
7
- // Viewer shell styles - independent from UI library
8
- // UI library styles are only loaded in the isolated preview area
7
+ // Fragments UI globals - base resets (box-sizing, body, scrollbars) + --fui-* CSS variables
8
+ import "@fragments-sdk/ui";
9
+ // Viewer-specific token aliases and utility classes
9
10
  import "./styles/globals.css";
10
- // Fragments UI globals - adds --fui-* CSS variables for dogfooding UI components
11
- import "@fragments/ui";
12
11
 
13
12
  // App-level error boundary that catches any unhandled errors
14
13
  class AppErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean; error: Error | null; errorInfo: ErrorInfo | null }> {
@@ -2,11 +2,15 @@
2
2
  * Keyboard Shortcuts Hook
3
3
  *
4
4
  * Provides global keyboard navigation for the viewer:
5
- * - j/k or ↓/↑: Navigate components
6
- * - [/] or ←/→: Navigate variants
5
+ * - ⌘↓/⌘↑: Navigate components
6
+ * - ⌘←/⌘→: Navigate variants
7
+ * - j/k or ↓/↑: Navigate components (legacy)
8
+ * - [/] or ←/→: Navigate variants (legacy)
7
9
  * - 1-9: Jump to variant by number
8
10
  * - t: Toggle preview theme
9
11
  * - p: Toggle panel
12
+ * - m: Toggle matrix view
13
+ * - r: Toggle responsive view
10
14
  * - cmd+shift+c: Copy link
11
15
  * - ?: Show shortcuts help
12
16
  * - Escape: Close modals/clear search
@@ -29,6 +33,10 @@ export interface ShortcutActions {
29
33
  toggleTheme: () => void;
30
34
  /** Toggle panel open/closed */
31
35
  togglePanel: () => void;
36
+ /** Toggle matrix view */
37
+ toggleMatrix: () => void;
38
+ /** Toggle responsive/multi-viewport view */
39
+ toggleResponsive: () => void;
32
40
  /** Copy shareable link */
33
41
  copyLink: () => void;
34
42
  /** Show shortcuts help */
@@ -84,6 +92,7 @@ export function useKeyboardShortcuts(
84
92
 
85
93
  // Don't capture events from input elements (except for specific shortcuts)
86
94
  const isInput = isInputElement(event.target);
95
+ const isMeta = event.metaKey || event.ctrlKey;
87
96
 
88
97
  // Global shortcuts that work even in inputs
89
98
  if (event.key === "Escape") {
@@ -92,19 +101,41 @@ export function useKeyboardShortcuts(
92
101
  }
93
102
 
94
103
  // cmd+shift+c: Copy link (works everywhere)
95
- if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === "c") {
104
+ if (isMeta && event.shiftKey && event.key === "c") {
96
105
  event.preventDefault();
97
106
  actions.copyLink?.();
98
107
  return;
99
108
  }
100
109
 
101
110
  // cmd+k: Open search/command palette (works everywhere)
102
- if ((event.metaKey || event.ctrlKey) && event.key === "k") {
111
+ if (isMeta && event.key === "k") {
103
112
  event.preventDefault();
104
113
  actions.openSearch?.();
105
114
  return;
106
115
  }
107
116
 
117
+ // cmd+arrow: Navigation (works everywhere)
118
+ if (isMeta && event.key === "ArrowDown") {
119
+ event.preventDefault();
120
+ actions.nextComponent?.();
121
+ return;
122
+ }
123
+ if (isMeta && event.key === "ArrowUp") {
124
+ event.preventDefault();
125
+ actions.prevComponent?.();
126
+ return;
127
+ }
128
+ if (isMeta && event.key === "ArrowRight") {
129
+ event.preventDefault();
130
+ actions.nextVariant?.();
131
+ return;
132
+ }
133
+ if (isMeta && event.key === "ArrowLeft") {
134
+ event.preventDefault();
135
+ actions.prevVariant?.();
136
+ return;
137
+ }
138
+
108
139
  // Skip other shortcuts if in input
109
140
  if (isInput) return;
110
141
 
@@ -120,7 +151,7 @@ export function useKeyboardShortcuts(
120
151
  // Component navigation
121
152
  case "j":
122
153
  case "ArrowDown":
123
- if (!event.metaKey && !event.ctrlKey && !event.altKey) {
154
+ if (!isMeta && !event.altKey) {
124
155
  event.preventDefault();
125
156
  actions.nextComponent?.();
126
157
  }
@@ -128,7 +159,7 @@ export function useKeyboardShortcuts(
128
159
 
129
160
  case "k":
130
161
  case "ArrowUp":
131
- if (!event.metaKey && !event.ctrlKey && !event.altKey) {
162
+ if (!isMeta && !event.altKey) {
132
163
  event.preventDefault();
133
164
  actions.prevComponent?.();
134
165
  }
@@ -137,7 +168,7 @@ export function useKeyboardShortcuts(
137
168
  // Variant navigation
138
169
  case "[":
139
170
  case "ArrowLeft":
140
- if (!event.metaKey && !event.ctrlKey && !event.altKey) {
171
+ if (!isMeta && !event.altKey) {
141
172
  event.preventDefault();
142
173
  actions.prevVariant?.();
143
174
  }
@@ -145,7 +176,7 @@ export function useKeyboardShortcuts(
145
176
 
146
177
  case "]":
147
178
  case "ArrowRight":
148
- if (!event.metaKey && !event.ctrlKey && !event.altKey) {
179
+ if (!isMeta && !event.altKey) {
149
180
  event.preventDefault();
150
181
  actions.nextVariant?.();
151
182
  }
@@ -161,7 +192,7 @@ export function useKeyboardShortcuts(
161
192
  case "7":
162
193
  case "8":
163
194
  case "9":
164
- if (!event.metaKey && !event.ctrlKey && !event.altKey) {
195
+ if (!isMeta && !event.altKey) {
165
196
  const index = parseInt(event.key, 10) - 1;
166
197
  if (index < variantCount) {
167
198
  event.preventDefault();
@@ -172,7 +203,7 @@ export function useKeyboardShortcuts(
172
203
 
173
204
  // Theme toggle
174
205
  case "t":
175
- if (!event.metaKey && !event.ctrlKey && !event.altKey) {
206
+ if (!isMeta && !event.altKey) {
176
207
  event.preventDefault();
177
208
  actions.toggleTheme?.();
178
209
  }
@@ -180,12 +211,28 @@ export function useKeyboardShortcuts(
180
211
 
181
212
  // Panel toggle
182
213
  case "p":
183
- if (!event.metaKey && !event.ctrlKey && !event.altKey) {
214
+ if (!isMeta && !event.altKey) {
184
215
  event.preventDefault();
185
216
  actions.togglePanel?.();
186
217
  }
187
218
  break;
188
219
 
220
+ // Matrix view toggle
221
+ case "m":
222
+ if (!isMeta && !event.altKey) {
223
+ event.preventDefault();
224
+ actions.toggleMatrix?.();
225
+ }
226
+ break;
227
+
228
+ // Responsive view toggle
229
+ case "v":
230
+ if (!isMeta && !event.altKey) {
231
+ event.preventDefault();
232
+ actions.toggleResponsive?.();
233
+ }
234
+ break;
235
+
189
236
  // Help
190
237
  case "?":
191
238
  event.preventDefault();
@@ -206,14 +253,15 @@ export function useKeyboardShortcuts(
206
253
  * Keyboard shortcuts data for help display
207
254
  */
208
255
  export const SHORTCUTS = [
209
- { keys: ["j", "↓"], description: "Next component" },
210
- { keys: ["k", "↑"], description: "Previous component" },
211
- { keys: ["[", "←"], description: "Previous variant" },
212
- { keys: ["]", "→"], description: "Next variant" },
256
+ { keys: ["⌘↓"], description: "Next component" },
257
+ { keys: ["⌘↑"], description: "Previous component" },
258
+ { keys: ["⌘→"], description: "Next variant" },
259
+ { keys: ["⌘←"], description: "Previous variant" },
213
260
  { keys: ["1-9"], description: "Go to variant" },
214
261
  { keys: ["t"], description: "Toggle preview theme" },
215
- { keys: ["p"], description: "Toggle panel" },
216
- { keys: ["⌘⇧C"], description: "Copy link" },
262
+ { keys: ["p"], description: "Toggle addons panel" },
263
+ { keys: ["m"], description: "Matrix view" },
264
+ { keys: ["v"], description: "Responsive view" },
217
265
  { keys: ["/", "⌘K"], description: "Search" },
218
266
  { keys: ["?"], description: "Show shortcuts" },
219
267
  { keys: ["Esc"], description: "Close / Clear" },
@@ -4,11 +4,10 @@
4
4
  */
5
5
 
6
6
  import { useReducer, useCallback, useMemo } from 'react';
7
- import type { ZoomLevel, BackgroundOption } from '../components/PreviewToolbar.js';
7
+ import type { ZoomLevel, BackgroundOption, ViewportPreset, ViewportSize } from '../constants/ui.js';
8
8
 
9
9
  // Preview theme type (for canvas theming)
10
10
  export type PreviewTheme = 'light' | 'dark';
11
- import type { ViewportPreset, ViewportSize } from '../components/ViewportSelector.js';
12
11
 
13
12
  interface ViewSettings {
14
13
  zoom: ZoomLevel;
@@ -7,5 +7,5 @@ export { fragmentsPlugin } from "./vite-plugin.js";
7
7
  export type { FragmentsPluginOptions } from "./vite-plugin.js";
8
8
 
9
9
  // Note: Viewer components (App, Layout, etc.) are NOT exported here.
10
- // They use @fragments/ui which is a Vite-only alias and can't be resolved by Node.js.
10
+ // They use @fragments-sdk/ui which is resolved by the Vite dev server.
11
11
  // Vite serves these components directly from source during development.
@@ -30,19 +30,16 @@
30
30
  width: 100%;
31
31
  min-height: 100vh;
32
32
  display: flex;
33
- align-items: flex-start;
34
- justify-content: flex-start;
33
+ align-items: center;
34
+ justify-content: center;
35
35
  padding: 24px;
36
36
  box-sizing: border-box;
37
37
  overflow: auto;
38
38
  }
39
39
 
40
- body[data-preview-mode='centered'] #preview-root {
41
- align-items: center;
42
- justify-content: center;
43
- }
44
-
45
40
  body[data-preview-mode='full-bleed'] #preview-root {
41
+ align-items: flex-start;
42
+ justify-content: flex-start;
46
43
  padding: 0;
47
44
  min-height: 100vh;
48
45
  }
@@ -119,8 +116,8 @@
119
116
  }
120
117
  </style>
121
118
  </head>
122
- <body data-preview-mode="centered">
123
- <main>
119
+ <body>
120
+ <main style="height: 100%">
124
121
  <h1 class="sr-only">Component Preview</h1>
125
122
  <div id="preview-root">
126
123
  <div class="preview-loading">
@@ -37,6 +37,11 @@ const localUiLibRoot = resolve(packagesRoot, "../libs/ui/src");
37
37
  * In the monorepo: use the local libs/ui/src source.
38
38
  */
39
39
  function resolveUiLib(nodeModulesDir: string): string {
40
+ // Prefer local monorepo source (when CLI is linked for development)
41
+ const localIndex = join(localUiLibRoot, "index.ts");
42
+ if (existsSync(localIndex)) {
43
+ return localUiLibRoot;
44
+ }
40
45
  // Check for installed @fragments-sdk/ui in project's node_modules
41
46
  const installedUi = join(nodeModulesDir, "@fragments-sdk/ui/src/index.ts");
42
47
  if (existsSync(installedUi)) {
@@ -47,10 +52,77 @@ function resolveUiLib(nodeModulesDir: string): string {
47
52
  if (existsSync(installedUiDist)) {
48
53
  return installedUiDist;
49
54
  }
50
- // Fallback to local monorepo source
51
55
  return localUiLibRoot;
52
56
  }
53
57
 
58
+ /**
59
+ * Vite plugin to handle CJS-only packages imported from within node_modules.
60
+ * Vite's optimizeDeps only redirects imports from source files to pre-bundled ESM,
61
+ * not imports from other node_modules packages. This plugin provides virtual ESM
62
+ * wrappers for known CJS-only packages.
63
+ */
64
+ function cjsInteropPlugin(nodeModulesPath: string) {
65
+ // Virtual ESM wrappers for CJS-only packages
66
+ const virtualModules: Record<string, string> = {
67
+ // use-sync-external-store/shim is CJS-only, used by @base-ui/react.
68
+ // Since React 18+, useSyncExternalStore is built-in — redirect to React's native export.
69
+ 'use-sync-external-store/shim': `export { useSyncExternalStore } from 'react';`,
70
+ // use-sync-external-store/shim/with-selector is CJS-only, used by @base-ui/utils.
71
+ // Provides useSyncExternalStoreWithSelector — a selector wrapper around useSyncExternalStore.
72
+ 'use-sync-external-store/shim/with-selector': `
73
+ import { useSyncExternalStore, useRef, useEffect, useMemo, useDebugValue } from 'react';
74
+ export function useSyncExternalStoreWithSelector(subscribe, getSnapshot, getServerSnapshot, selector, isEqual) {
75
+ var instRef = useRef(null);
76
+ if (instRef.current === null) instRef.current = { hasValue: false, value: null };
77
+ var inst = instRef.current;
78
+ var _useMemo = useMemo(function () {
79
+ var hasMemo = false, memoizedSnapshot, memoizedSelection;
80
+ function memoizedSelector(nextSnapshot) {
81
+ if (!hasMemo) {
82
+ hasMemo = true;
83
+ memoizedSnapshot = nextSnapshot;
84
+ var nextSelection = selector(nextSnapshot);
85
+ if (isEqual !== undefined && inst.hasValue && isEqual(inst.value, nextSelection))
86
+ return memoizedSelection = inst.value;
87
+ return memoizedSelection = nextSelection;
88
+ }
89
+ if (Object.is(memoizedSnapshot, nextSnapshot)) return memoizedSelection;
90
+ var nextSelection = selector(nextSnapshot);
91
+ if (isEqual !== undefined && isEqual(memoizedSelection, nextSelection)) {
92
+ memoizedSnapshot = nextSnapshot;
93
+ return memoizedSelection;
94
+ }
95
+ memoizedSnapshot = nextSnapshot;
96
+ return memoizedSelection = nextSelection;
97
+ }
98
+ var maybeGetServerSnapshot = getServerSnapshot === undefined ? null : getServerSnapshot;
99
+ return [function () { return memoizedSelector(getSnapshot()); },
100
+ maybeGetServerSnapshot === null ? undefined : function () { return memoizedSelector(maybeGetServerSnapshot()); }];
101
+ }, [getSnapshot, getServerSnapshot, selector, isEqual]);
102
+ var value = useSyncExternalStore(subscribe, _useMemo[0], _useMemo[1]);
103
+ useEffect(function () { inst.hasValue = true; inst.value = value; }, [value]);
104
+ useDebugValue(value);
105
+ return value;
106
+ }`,
107
+ };
108
+
109
+ return {
110
+ name: 'fragments-cjs-interop',
111
+ enforce: 'pre' as const,
112
+ resolveId(source: string) {
113
+ if (source in virtualModules) {
114
+ return `\0cjs-interop:${source}`;
115
+ }
116
+ return undefined;
117
+ },
118
+ load(id: string) {
119
+ if (!id.startsWith('\0cjs-interop:')) return undefined;
120
+ const pkg = id.slice('\0cjs-interop:'.length);
121
+ return virtualModules[pkg];
122
+ },
123
+ };
124
+ }
125
+
54
126
  export interface DevServerOptions {
55
127
  /** Port to run the server on */
56
128
  port?: number;
@@ -151,6 +223,9 @@ export async function createDevServer(
151
223
  // React support (if not already in project config)
152
224
  ...(hasReactPlugin(projectViteConfig) ? [] : [react()]),
153
225
 
226
+ // CJS interop for packages imported from within node_modules
227
+ cjsInteropPlugin(nodeModulesPath),
228
+
154
229
  // Fragments plugins (array including SVGR)
155
230
  ...fragmentsPlugin({
156
231
  fragmentFiles: allFragmentFiles,
@@ -172,12 +247,10 @@ export async function createDevServer(
172
247
  // Dedupe ensures all imports of these packages resolve to the same copy
173
248
  dedupe: ["react", "react-dom"],
174
249
  alias: {
175
- // Allow importing from viewer package
176
- "@fragments/viewer": viewerRoot,
177
- // Resolve @fragments/ui to installed @fragments-sdk/ui or local source
178
- "@fragments/ui": resolveUiLib(nodeModulesPath),
179
- // Resolve @fragments/core to the consolidated core source
180
- "@fragments/core": resolve(cliPackageRoot, "src/core/index.ts"),
250
+ // Resolve @fragments-sdk/ui to local source or installed package
251
+ "@fragments-sdk/ui": resolveUiLib(nodeModulesPath),
252
+ // Resolve @fragments-sdk/cli/core to the CLI's own core source
253
+ "@fragments-sdk/cli/core": resolve(cliPackageRoot, "src/core/index.ts"),
181
254
  // Ensure ALL react imports resolve to project's node_modules
182
255
  // This is critical for viewer files loaded from outside project root
183
256
  "react": safeRealpath(join(nodeModulesPath, "react")),
@@ -1,10 +1,18 @@
1
1
  /* ============================================
2
- * Fragments Viewer Theme System
2
+ * Fragments Viewer Shell Styles
3
3
  * ============================================
4
- * The viewer shell (sidebar, toolbar, panels) mirrors @fragments/ui
5
- * tokens so previews and shell feel like the live docs experience.
4
+ * Viewer-specific token aliases and utility classes.
5
+ * Base resets (box-sizing, body, scrollbars) come from
6
+ * @fragments/ui globals imported via entry.tsx.
6
7
  * ============================================ */
7
8
 
9
+ /* ============================================
10
+ * Token Aliases
11
+ * ============================================
12
+ * Shorthand aliases mapping to --fui-* tokens so the viewer
13
+ * shell CSS can use shorter variable names.
14
+ */
15
+
8
16
  :root {
9
17
  --bg-primary: var(--fui-bg-primary, #ffffff);
10
18
  --bg-secondary: var(--fui-bg-secondary, #f7f7f8);
@@ -48,60 +56,13 @@
48
56
  }
49
57
 
50
58
  /* ============================================
51
- * Base Element Styles
59
+ * Viewer-specific overrides
52
60
  * ============================================ */
53
61
 
54
62
  * {
55
63
  border-color: var(--border);
56
64
  }
57
65
 
58
- *::selection {
59
- background-color: var(--color-accent-subtle);
60
- color: var(--text-primary);
61
- }
62
-
63
- html {
64
- scroll-behavior: smooth;
65
- }
66
-
67
- body {
68
- margin: 0;
69
- font-family: var(--fui-font-sans, Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
70
- background-color: var(--bg-primary);
71
- color: var(--text-primary);
72
- -webkit-font-smoothing: antialiased;
73
- -moz-osx-font-smoothing: grayscale;
74
- font-feature-settings: "rlig" 1, "calt" 1;
75
- }
76
-
77
- /* ============================================
78
- * Scrollbar Styles
79
- * ============================================ */
80
-
81
- ::-webkit-scrollbar {
82
- width: 6px;
83
- height: 6px;
84
- }
85
-
86
- ::-webkit-scrollbar-track {
87
- background-color: transparent;
88
- }
89
-
90
- ::-webkit-scrollbar-thumb {
91
- background-color: var(--text-muted);
92
- border-radius: 9999px;
93
- }
94
-
95
- ::-webkit-scrollbar-thumb:hover {
96
- background-color: var(--text-tertiary);
97
- }
98
-
99
- /* Firefox scrollbar */
100
- * {
101
- scrollbar-width: thin;
102
- scrollbar-color: var(--text-muted) transparent;
103
- }
104
-
105
66
  /* ============================================
106
67
  * Theme-aware Utility Classes
107
68
  * ============================================ */
@@ -1566,6 +1566,41 @@ function getBaseComponentPath(filePath: string): string {
1566
1566
  return filePath.replace(/\.(fragment|stories)\.(tsx?|jsx?)$/, "");
1567
1567
  }
1568
1568
 
1569
+ /**
1570
+ * Extract component name from a fragment/story file path.
1571
+ * e.g., "src/components/Chart/Chart.fragment.tsx" -> "Chart"
1572
+ * e.g., "@fragments-sdk/ui/src/components/Button/Button.fragment.tsx" -> "Button"
1573
+ */
1574
+ function extractComponentName(filePath: string): string {
1575
+ const match = filePath.match(/([^/\\]+)\.(fragment|stories)\.(tsx?|jsx?)$/);
1576
+ return match ? match[1] : filePath.split("/").pop() || filePath;
1577
+ }
1578
+
1579
+ /**
1580
+ * Extract dependency names from a fragment source file (best effort).
1581
+ * Reads the file and looks for `dependencies: [{ name: '...' }, ...]` patterns.
1582
+ * Returns an array of package names, or empty array if extraction fails.
1583
+ */
1584
+ async function extractDependenciesFromSource(absolutePath: string): Promise<string[]> {
1585
+ try {
1586
+ const source = await readFile(absolutePath, "utf-8");
1587
+ // Match the dependencies array in the defineFragment call
1588
+ const depsMatch = source.match(/dependencies:\s*\[([\s\S]*?)\]/);
1589
+ if (!depsMatch) return [];
1590
+
1591
+ // Extract name values from dependency objects
1592
+ const names: string[] = [];
1593
+ const nameRegex = /name:\s*['"]([^'"]+)['"]/g;
1594
+ let match;
1595
+ while ((match = nameRegex.exec(depsMatch[1])) !== null) {
1596
+ names.push(match[1]);
1597
+ }
1598
+ return names;
1599
+ } catch {
1600
+ return [];
1601
+ }
1602
+ }
1603
+
1569
1604
  /**
1570
1605
  * Generate the virtual fragments module.
1571
1606
  * Uses dynamic imports for lazy loading - fragments are loaded on demand.
@@ -1577,11 +1612,11 @@ function getBaseComponentPath(filePath: string): string {
1577
1612
  * - Merge METADATA from .fragment.tsx (Figma URLs, AI descriptions, usage guidelines)
1578
1613
  * - This gives us the best of both worlds: working renders + rich metadata
1579
1614
  */
1580
- function generateFragmentsModule(
1615
+ async function generateFragmentsModule(
1581
1616
  fragmentFiles: Array<{ absolutePath: string; relativePath: string }>,
1582
1617
  config: FragmentsConfig,
1583
1618
  previewConfigPath: string | null
1584
- ): string {
1619
+ ): Promise<string> {
1585
1620
  // Group files by base component path to identify pairs
1586
1621
  const filesByBasePath = new Map<string, {
1587
1622
  storyFile?: { absolutePath: string; relativePath: string };
@@ -1605,8 +1640,8 @@ function generateFragmentsModule(
1605
1640
 
1606
1641
  // Generate loaders with metadata merge support
1607
1642
  // Priority: stories for rendering, fragment for metadata (Figma URLs, etc.)
1608
- const loaders = Array.from(filesByBasePath.values())
1609
- .map((files) => {
1643
+ const loaderEntries = await Promise.all(
1644
+ Array.from(filesByBasePath.values()).map(async (files) => {
1610
1645
  // Determine which file to use for rendering
1611
1646
  const primaryFile = files.storyFile || files.fragmentFile;
1612
1647
  if (!primaryFile) return null;
@@ -1618,15 +1653,25 @@ function generateFragmentsModule(
1618
1653
  ? files.fragmentFile.absolutePath
1619
1654
  : null;
1620
1655
 
1656
+ // Extract component name from file path
1657
+ const componentName = extractComponentName(primaryFile.relativePath);
1658
+
1659
+ // Extract dependencies from source (best effort, for error stubs)
1660
+ const fragmentSource = files.fragmentFile || primaryFile;
1661
+ const dependencies = await extractDependenciesFromSource(fragmentSource.absolutePath);
1662
+
1621
1663
  return ` {
1622
1664
  path: "${primaryFile.relativePath}",
1623
1665
  isStory: ${isStory},
1666
+ componentName: ${JSON.stringify(componentName)},
1667
+ dependencies: ${JSON.stringify(dependencies)},
1624
1668
  loader: () => import("${primaryFile.absolutePath}"),
1625
1669
  metadataLoader: ${metadataPath ? `() => import("${metadataPath}")` : 'null'}
1626
1670
  }`;
1627
1671
  })
1628
- .filter(Boolean)
1629
- .join(",\n");
1672
+ );
1673
+
1674
+ const loaders = loaderEntries.filter(Boolean).join(",\n");
1630
1675
 
1631
1676
  // Generate preview config import if available
1632
1677
  const previewImport = previewConfigPath
@@ -1647,7 +1692,7 @@ setPreviewConfig({
1647
1692
  : "";
1648
1693
 
1649
1694
  return `
1650
- import { storyModuleToFragment, setPreviewConfig } from "@fragments/core";
1695
+ import { storyModuleToFragment, setPreviewConfig } from "@fragments-sdk/cli/core";
1651
1696
  ${previewImport}
1652
1697
  ${previewSetup}
1653
1698
  // Lazy fragment loaders (supports both .fragment.tsx and .stories.tsx)
@@ -1739,11 +1784,27 @@ export async function loadAllFragments() {
1739
1784
  return { path: loader.path, fragment };
1740
1785
  } catch (error) {
1741
1786
  console.warn("[Fragments] Failed to load " + loader.path + ":", error.message);
1742
- return null;
1787
+ // Create an error stub so the component still appears in the sidebar
1788
+ // with a helpful message about what went wrong
1789
+ return {
1790
+ path: loader.path,
1791
+ fragment: {
1792
+ meta: {
1793
+ name: loader.componentName || loader.path,
1794
+ description: 'Failed to load',
1795
+ category: '',
1796
+ },
1797
+ variants: [],
1798
+ _loadError: {
1799
+ message: error.message,
1800
+ dependencies: loader.dependencies || [],
1801
+ },
1802
+ },
1803
+ };
1743
1804
  }
1744
1805
  })
1745
1806
  );
1746
- // Filter out failed loads
1807
+ // Filter out nulls (fragments that had no component)
1747
1808
  return results.filter(r => r !== null);
1748
1809
  }
1749
1810