@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
@@ -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 {
@@ -7,7 +7,7 @@
7
7
  import { useMemo, useState, useCallback, useEffect } from 'react';
8
8
  import type { FragmentDefinition } from '../../core/index.js';
9
9
  import type { ImpactValue } from 'axe-core';
10
- import { Badge, Progress, Stack, Text, Card, EmptyState, Table } from '@fragments/ui';
10
+ import { Badge, Progress, Stack, Text, Card, EmptyState, Table } from '@fragments-sdk/ui';
11
11
  import {
12
12
  getAllA11yData,
13
13
  getA11ySummary,
@@ -1,4 +1,4 @@
1
- import { Badge, Stack, Text } from '@fragments/ui';
1
+ import { Badge, Stack, Text } from '@fragments-sdk/ui';
2
2
  import { HMR_STATUS } from '../constants/ui.js';
3
3
  import { useHmrStatus } from '../hooks/useHmrStatus.js';
4
4
  import { WifiIcon, WifiOffIcon } from './Icons.js';
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { useState, useCallback, useRef, useEffect } from "react";
13
- import { Button, Stack, Text, Badge, EmptyState, CodeBlock, Alert } from "@fragments/ui";
13
+ import { Button, Stack, Text, Badge, EmptyState, CodeBlock, Alert } from "@fragments-sdk/ui";
14
14
  import type { PlayFunction, PlayFunctionContext, FragmentVariant } from "../../core/index.js";
15
15
  import {
16
16
  PlayIcon,
@@ -1,7 +1,7 @@
1
1
  import { useMemo, useEffect, useState } from "react";
2
2
  import type { FragmentDefinition } from "../../core/index.js";
3
3
  import { VariantRenderer } from "./VariantRenderer.js";
4
- import { getBackgroundStyle, type BackgroundOption, type ZoomLevel } from "./PreviewToolbar.js";
4
+ import { getBackgroundStyle, type BackgroundOption, type ZoomLevel } from "../constants/ui.js";
5
5
 
6
6
  interface IsolatedRenderProps {
7
7
  fragments: Array<{ path: string; fragment: FragmentDefinition }>;
@@ -5,7 +5,7 @@
5
5
  * Uses Fragments UI Dialog compound component.
6
6
  */
7
7
 
8
- import { Dialog, Stack, Text, Badge } from '@fragments/ui';
8
+ import { Dialog, Stack, Text, Badge } from '@fragments-sdk/ui';
9
9
  import { SHORTCUTS } from "../hooks/useKeyboardShortcuts.js";
10
10
 
11
11
  interface KeyboardShortcutsHelpProps {
@@ -3,7 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
3
3
  import { z } from 'zod';
4
4
  import { useState, useRef, useEffect } from 'react';
5
5
  import { HexColorPicker } from 'react-colorful';
6
- import { Button, Input, Toggle, Stack, Text, Card, Field, Separator, Select } from '@fragments/ui';
6
+ import { Button, Input, Toggle, Stack, Text, Card, Field, Separator, Select } from '@fragments-sdk/ui';
7
7
  import { generateColorScheme, type ColorSchemeType } from '../utils/colorSchemes.js';
8
8
  import { ChevronDownIcon } from './Icons.js';
9
9
 
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import { AppShell } from '@fragments/ui';
2
+ import { AppShell } from '@fragments-sdk/ui';
3
3
 
4
4
  interface LayoutProps {
5
5
  leftSidebar: ReactNode;
@@ -1,6 +1,6 @@
1
1
  import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
2
2
  import type { FragmentDefinition } from '../../core/index.js';
3
- import { Sidebar, useSidebar, Text } from '@fragments/ui';
3
+ import { Sidebar, useSidebar, Text, Menu } from '@fragments-sdk/ui';
4
4
 
5
5
  // Fuzzy matching utility
6
6
  interface FuzzyMatch {
@@ -125,11 +125,40 @@ interface LeftSidebarProps {
125
125
  onHealthClick?: () => void;
126
126
  }
127
127
 
128
+ const FilterIcon = ({ active }: { active?: boolean }) => (
129
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
130
+ <path
131
+ d="M2 3h12l-4.5 5.5V13l-3-1.5V8.5L2 3z"
132
+ stroke={active ? 'var(--color-brand, #3b82f6)' : 'currentColor'}
133
+ strokeWidth="1.5"
134
+ strokeLinejoin="round"
135
+ fill={active ? 'var(--color-brand, #3b82f6)' : 'none'}
136
+ fillOpacity={active ? 0.15 : 0}
137
+ />
138
+ </svg>
139
+ );
140
+
141
+ const isInstalled = (path: string) => path.startsWith('@');
142
+
128
143
  export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect, showHealth, onHealthClick }: LeftSidebarProps) {
129
144
  const [focusedIndex, setFocusedIndex] = useState(-1);
145
+ const [showCustom, setShowCustom] = useState(true);
146
+ const [showLibrary, setShowLibrary] = useState(true);
130
147
  const { isMobile, setOpen } = useSidebar();
131
148
  const navRef = useRef<HTMLDivElement>(null);
132
149
 
150
+ // Determine if both sources exist (only show filter when useful)
151
+ const hasBothSources = useMemo(() => {
152
+ let hasCustom = false;
153
+ let hasLibrary = false;
154
+ for (const item of fragments) {
155
+ if (isInstalled(item.path)) hasLibrary = true;
156
+ else hasCustom = true;
157
+ if (hasCustom && hasLibrary) return true;
158
+ }
159
+ return false;
160
+ }, [fragments]);
161
+
133
162
  const debouncedSearch = useDebounce(searchQuery, 150);
134
163
 
135
164
  const searchResults = useMemo(() => {
@@ -155,19 +184,30 @@ export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect,
155
184
  return map;
156
185
  }, [searchResults]);
157
186
 
187
+ const isFilterActive = showCustom !== showLibrary;
188
+
189
+ // Apply source filter
190
+ const sourceFiltered = useMemo(() => {
191
+ if (!isFilterActive) return fragments;
192
+ return fragments.filter(item =>
193
+ showLibrary ? isInstalled(item.path) : !isInstalled(item.path)
194
+ );
195
+ }, [fragments, isFilterActive, showLibrary]);
196
+
158
197
  // Flat alphabetical list (search results sorted by relevance, otherwise alphabetical)
159
198
  const flatItems = useMemo(() => {
160
199
  if (searchResults) {
161
200
  return searchResults
162
201
  .map(r => r.item)
163
- .filter(item => item.fragment?.meta?.name);
202
+ .filter(item => item.fragment?.meta?.name)
203
+ .filter(item => !isFilterActive || (showLibrary ? isInstalled(item.path) : !isInstalled(item.path)));
164
204
  }
165
- return [...fragments]
205
+ return [...sourceFiltered]
166
206
  .filter(item => item.fragment?.meta?.name)
167
207
  .sort((a, b) =>
168
208
  a.fragment.meta.name.toLowerCase().localeCompare(b.fragment.meta.name.toLowerCase())
169
209
  );
170
- }, [fragments, searchResults]);
210
+ }, [sourceFiltered, searchResults, isFilterActive, showLibrary]);
171
211
 
172
212
  const keyboardItems = useMemo(() => {
173
213
  const componentItems = flatItems.map((item) => ({ type: 'component' as const, path: item.path }));
@@ -252,19 +292,64 @@ export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect,
252
292
  </Sidebar.Section>
253
293
  )}
254
294
 
255
- <Sidebar.Section label="Components">
256
- {flatItems.map((item) => (
257
- <Sidebar.Item
258
- key={item.path}
259
- active={activeFragment === item.path}
260
- onClick={() => handleSelect(item.path)}
261
- >
262
- <HighlightedText
263
- text={item.fragment.meta.name}
264
- indices={highlightMap.get(item.path) || []}
265
- />
266
- </Sidebar.Item>
267
- ))}
295
+ <Sidebar.Section
296
+ label="Components"
297
+ action={hasBothSources ? (
298
+ <Menu modal={false}>
299
+ <Menu.Trigger asChild>
300
+ <button
301
+ type="button"
302
+ aria-label="Filter components by source"
303
+ style={{
304
+ background: 'none',
305
+ border: 'none',
306
+ cursor: 'pointer',
307
+ padding: '2px',
308
+ display: 'flex',
309
+ alignItems: 'center',
310
+ color: 'var(--text-tertiary)',
311
+ borderRadius: '4px',
312
+ }}
313
+ >
314
+ <FilterIcon active={isFilterActive} />
315
+ </button>
316
+ </Menu.Trigger>
317
+ <Menu.Content>
318
+ <Menu.CheckboxItem checked={showCustom} onCheckedChange={setShowCustom}>
319
+ Custom
320
+ </Menu.CheckboxItem>
321
+ <Menu.CheckboxItem checked={showLibrary} onCheckedChange={setShowLibrary}>
322
+ Fragments UI
323
+ </Menu.CheckboxItem>
324
+ </Menu.Content>
325
+ </Menu>
326
+ ) : undefined}
327
+ >
328
+ {flatItems.map((item) => {
329
+ const hasError = !!(item.fragment as any)?._loadError;
330
+ return (
331
+ <Sidebar.Item
332
+ key={item.path}
333
+ active={activeFragment === item.path}
334
+ onClick={() => handleSelect(item.path)}
335
+ >
336
+ <span style={{ display: 'flex', alignItems: 'center', gap: '6px', width: '100%' }}>
337
+ <HighlightedText
338
+ text={item.fragment.meta.name}
339
+ indices={highlightMap.get(item.path) || []}
340
+ />
341
+ {hasError && (
342
+ <span
343
+ title="Missing dependencies"
344
+ style={{ color: 'var(--color-warning, #f59e0b)', fontSize: '14px', lineHeight: 1, flexShrink: 0 }}
345
+ >
346
+ &#9888;
347
+ </span>
348
+ )}
349
+ </span>
350
+ </Sidebar.Item>
351
+ );
352
+ })}
268
353
  </Sidebar.Section>
269
354
 
270
355
  {flatItems.length === 0 && (
@@ -278,7 +363,9 @@ export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect,
278
363
  {/* Footer */}
279
364
  <Sidebar.Footer>
280
365
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
281
- <Text size="xs" color="tertiary">{fragments.length} components</Text>
366
+ <Text size="xs" color="tertiary">
367
+ {isFilterActive ? `${flatItems.length} / ${fragments.length}` : fragments.length} components
368
+ </Text>
282
369
  <Sidebar.CollapseToggle />
283
370
  </div>
284
371
  </Sidebar.Footer>
@@ -10,7 +10,7 @@ import { useState, type ReactNode } from "react";
10
10
  import { ErrorBoundary } from "./ErrorBoundary.js";
11
11
  import { IsolatedPreviewFrame } from "./IsolatedPreviewFrame.js";
12
12
  import { ChevronDownIcon } from "./Icons.js";
13
- import { getBackgroundStyle, type BackgroundOption } from "./PreviewToolbar.js";
13
+ import { getBackgroundStyle, type BackgroundOption } from "../constants/ui.js";
14
14
 
15
15
  interface ViewportConfig {
16
16
  name: string;
@@ -13,9 +13,8 @@ import { FigmaEmbed } from './FigmaEmbed.js';
13
13
  import { VariantMatrix } from './VariantMatrix.js';
14
14
  import { MultiViewportPreview } from './MultiViewportPreview.js';
15
15
  import { IsolatedPreviewFrame } from './IsolatedPreviewFrame.js';
16
- import { getBackgroundStyle, type ZoomLevel, type BackgroundOption } from './PreviewToolbar.js';
16
+ import { getBackgroundStyle, getViewportWidth, type ZoomLevel, type BackgroundOption, type ViewportPreset, type ViewportSize } from '../constants/ui.js';
17
17
  import type { PreviewTheme } from '../hooks/useViewSettings.js';
18
- import { getViewportWidth, type ViewportPreset, type ViewportSize } from './ViewportSelector.js';
19
18
 
20
19
  interface PreviewAreaProps {
21
20
  // Component data
@@ -89,10 +89,6 @@ function resolvePreviewMode(fragment: FragmentDefinition): PreviewMode {
89
89
  const name = fragment.meta.name.toLowerCase();
90
90
  const category = (fragment.meta.category || '').toLowerCase();
91
91
 
92
- if (category === 'layout' || category === 'navigation') {
93
- return 'full-bleed';
94
- }
95
-
96
92
  if (name.includes('appshell') || name.includes('sidebar') || name.includes('header') || name.includes('layout')) {
97
93
  return 'full-bleed';
98
94
  }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * PreviewMenu — Storybook-style hamburger menu for the viewer.
3
+ * Provides navigation, view mode toggles, panel controls, and settings submenus.
4
+ * Placed left of the header beside the logo.
5
+ */
6
+
7
+ import { useEffect, useCallback } from 'react';
8
+ import { Button, Menu, Text, Stack } from '@fragments-sdk/ui';
9
+ import { List, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, GridFour, DeviceMobile, Rows, Eye } from '@phosphor-icons/react';
10
+ import {
11
+ ZOOM_LEVELS,
12
+ VIEWPORT_PRESETS,
13
+ type ZoomLevel,
14
+ type BackgroundOption,
15
+ type ViewportPreset,
16
+ } from '../constants/ui.js';
17
+
18
+ // Background options with display labels
19
+ const BACKGROUND_OPTIONS_UI: { value: BackgroundOption; label: string }[] = [
20
+ { value: 'checkerboard', label: 'Checkerboard' },
21
+ { value: 'black', label: 'Dark' },
22
+ { value: 'white', label: 'Light' },
23
+ { value: 'transparent', label: 'Transparent' },
24
+ ];
25
+
26
+ // Viewport presets for submenu display
27
+ const VIEWPORT_OPTIONS = Object.entries(VIEWPORT_PRESETS).map(([value, config]) => ({
28
+ value: value as Exclude<ViewportPreset, 'custom'>,
29
+ label: config.label,
30
+ }));
31
+
32
+ /** Shortcut label helper — renders a right-aligned keyboard hint */
33
+ function Shortcut({ keys }: { keys: string }) {
34
+ return (
35
+ <Text as="span" size="2xs" color="tertiary" style={{ marginLeft: 'auto', paddingLeft: '16px', fontFamily: 'inherit' }}>
36
+ {keys}
37
+ </Text>
38
+ );
39
+ }
40
+
41
+ interface PreviewMenuProps {
42
+ zoom: ZoomLevel;
43
+ background: BackgroundOption;
44
+ viewport: ViewportPreset;
45
+ showMatrixView: boolean;
46
+ showMultiViewport: boolean;
47
+ panelOpen: boolean;
48
+ onZoomChange: (zoom: ZoomLevel) => void;
49
+ onBackgroundChange: (bg: BackgroundOption) => void;
50
+ onViewportChange: (viewport: ViewportPreset) => void;
51
+ onToggleMatrix: () => void;
52
+ onToggleMultiViewport: () => void;
53
+ onTogglePanel: () => void;
54
+ onPrevComponent: () => void;
55
+ onNextComponent: () => void;
56
+ onPrevVariant: () => void;
57
+ onNextVariant: () => void;
58
+ }
59
+
60
+ export function PreviewMenu({
61
+ zoom,
62
+ background,
63
+ viewport,
64
+ showMatrixView,
65
+ showMultiViewport,
66
+ panelOpen,
67
+ onZoomChange,
68
+ onBackgroundChange,
69
+ onViewportChange,
70
+ onToggleMatrix,
71
+ onToggleMultiViewport,
72
+ onTogglePanel,
73
+ onPrevComponent,
74
+ onNextComponent,
75
+ onPrevVariant,
76
+ onNextVariant,
77
+ }: PreviewMenuProps) {
78
+ // Keyboard shortcuts for zoom
79
+ const handleKeyDown = useCallback((e: KeyboardEvent) => {
80
+ const target = e.target as HTMLElement;
81
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
82
+ return;
83
+ }
84
+
85
+ if (e.key === '=' || e.key === '+') {
86
+ e.preventDefault();
87
+ const currentIndex = ZOOM_LEVELS.indexOf(zoom);
88
+ if (currentIndex < ZOOM_LEVELS.length - 1) {
89
+ onZoomChange(ZOOM_LEVELS[currentIndex + 1]);
90
+ }
91
+ } else if (e.key === '-') {
92
+ e.preventDefault();
93
+ const currentIndex = ZOOM_LEVELS.indexOf(zoom);
94
+ if (currentIndex > 0) {
95
+ onZoomChange(ZOOM_LEVELS[currentIndex - 1]);
96
+ }
97
+ } else if (e.key === '0') {
98
+ e.preventDefault();
99
+ onZoomChange(100);
100
+ }
101
+ }, [zoom, onZoomChange]);
102
+
103
+ useEffect(() => {
104
+ document.addEventListener('keydown', handleKeyDown);
105
+ return () => document.removeEventListener('keydown', handleKeyDown);
106
+ }, [handleKeyDown]);
107
+
108
+ const iconSize = 16;
109
+ const iconWeight = 'regular' as const;
110
+
111
+ return (
112
+ <Menu modal={false}>
113
+ <Menu.Trigger asChild>
114
+ <Button variant="ghost" size="sm" aria-label="Options menu">
115
+ <Stack direction="row" gap="xs" align="center">
116
+ <List size={18} weight="bold" />
117
+ <Text size="sm" weight="medium">Options</Text>
118
+ </Stack>
119
+ </Button>
120
+ </Menu.Trigger>
121
+ <Menu.Content side="bottom" align="start" style={{ minWidth: '14rem' }}>
122
+ {/* View modes */}
123
+ <Menu.Item
124
+ checked={showMatrixView}
125
+ icon={<GridFour size={iconSize} weight={iconWeight} />}
126
+ onSelect={onToggleMatrix}
127
+ >
128
+ <Stack direction="row" align="center" style={{ flex: 1 }}>
129
+ Matrix view
130
+ <Shortcut keys="M" />
131
+ </Stack>
132
+ </Menu.Item>
133
+ <Menu.Item
134
+ checked={showMultiViewport}
135
+ icon={<DeviceMobile size={iconSize} weight={iconWeight} />}
136
+ onSelect={onToggleMultiViewport}
137
+ >
138
+ <Stack direction="row" align="center" style={{ flex: 1 }}>
139
+ Responsive view
140
+ <Shortcut keys="V" />
141
+ </Stack>
142
+ </Menu.Item>
143
+ <Menu.Item
144
+ checked={panelOpen}
145
+ icon={<Rows size={iconSize} weight={iconWeight} />}
146
+ onSelect={onTogglePanel}
147
+ >
148
+ <Stack direction="row" align="center" style={{ flex: 1 }}>
149
+ Show addons
150
+ <Shortcut keys="P" />
151
+ </Stack>
152
+ </Menu.Item>
153
+
154
+ <Menu.Separator />
155
+
156
+ {/* Navigation */}
157
+ <Menu.Group>
158
+ <Menu.GroupLabel>Navigate</Menu.GroupLabel>
159
+ <Menu.Item
160
+ icon={<ArrowUp size={iconSize} weight={iconWeight} />}
161
+ onSelect={onPrevComponent}
162
+ >
163
+ <Stack direction="row" align="center" style={{ flex: 1 }}>
164
+ Previous component
165
+ <Shortcut keys="⌘↑" />
166
+ </Stack>
167
+ </Menu.Item>
168
+ <Menu.Item
169
+ icon={<ArrowDown size={iconSize} weight={iconWeight} />}
170
+ onSelect={onNextComponent}
171
+ >
172
+ <Stack direction="row" align="center" style={{ flex: 1 }}>
173
+ Next component
174
+ <Shortcut keys="⌘↓" />
175
+ </Stack>
176
+ </Menu.Item>
177
+ <Menu.Item
178
+ icon={<ArrowLeft size={iconSize} weight={iconWeight} />}
179
+ onSelect={onPrevVariant}
180
+ >
181
+ <Stack direction="row" align="center" style={{ flex: 1 }}>
182
+ Previous variant
183
+ <Shortcut keys="⌘←" />
184
+ </Stack>
185
+ </Menu.Item>
186
+ <Menu.Item
187
+ icon={<ArrowRight size={iconSize} weight={iconWeight} />}
188
+ onSelect={onNextVariant}
189
+ >
190
+ <Stack direction="row" align="center" style={{ flex: 1 }}>
191
+ Next variant
192
+ <Shortcut keys="⌘→" />
193
+ </Stack>
194
+ </Menu.Item>
195
+ </Menu.Group>
196
+
197
+ <Menu.Separator />
198
+
199
+ {/* Settings submenus */}
200
+ <Menu.Submenu>
201
+ <Menu.SubmenuTrigger icon={<Eye size={iconSize} weight={iconWeight} />}>Viewport</Menu.SubmenuTrigger>
202
+ <Menu.Content side="right" align="start">
203
+ {VIEWPORT_OPTIONS.map((option) => (
204
+ <Menu.Item
205
+ key={option.value}
206
+ checked={viewport === option.value}
207
+ onSelect={() => onViewportChange(option.value)}
208
+ >
209
+ {option.label}
210
+ </Menu.Item>
211
+ ))}
212
+ </Menu.Content>
213
+ </Menu.Submenu>
214
+
215
+ <Menu.Submenu>
216
+ <Menu.SubmenuTrigger>Background</Menu.SubmenuTrigger>
217
+ <Menu.Content side="right" align="start">
218
+ {BACKGROUND_OPTIONS_UI.map((option) => (
219
+ <Menu.Item
220
+ key={option.value}
221
+ checked={background === option.value}
222
+ onSelect={() => onBackgroundChange(option.value)}
223
+ >
224
+ {option.label}
225
+ </Menu.Item>
226
+ ))}
227
+ </Menu.Content>
228
+ </Menu.Submenu>
229
+
230
+ <Menu.Submenu>
231
+ <Menu.SubmenuTrigger>Zoom ({zoom}%)</Menu.SubmenuTrigger>
232
+ <Menu.Content side="right" align="start">
233
+ {ZOOM_LEVELS.map((level) => (
234
+ <Menu.Item
235
+ key={level}
236
+ checked={zoom === level}
237
+ onSelect={() => onZoomChange(level)}
238
+ >
239
+ {level}%
240
+ </Menu.Item>
241
+ ))}
242
+ </Menu.Content>
243
+ </Menu.Submenu>
244
+ </Menu.Content>
245
+ </Menu>
246
+ );
247
+ }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useCallback } from 'react';
2
- import { Button, Menu, Stack, Separator } from '@fragments/ui';
2
+ import { Button, Menu, Stack, Separator } from '@fragments-sdk/ui';
3
3
  import {
4
4
  ZOOM_LEVELS,
5
5
  type ZoomLevel,
@@ -1,6 +1,6 @@
1
1
  import { useState } from "react";
2
2
  import type { PropDefinition } from "../../core/index.js";
3
- import { Input, Select, Toggle } from "@fragments/ui";
3
+ import { Input, Select, Toggle } from "@fragments-sdk/ui";
4
4
  import { ControlsIcon, ChevronDownIcon, RefreshIcon } from "./Icons.js";
5
5
 
6
6
  interface PropsEditorProps {
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { PropDefinition } from '../../core/index.js';
3
- import { Table, createColumns, Badge, Text, Stack } from '@fragments/ui';
3
+ import { Table, createColumns, Badge, Text, Stack } from '@fragments-sdk/ui';
4
4
  import { WarningIcon } from './Icons.js';
5
5
 
6
6
  interface PropsTableProps {
@@ -1,6 +1,6 @@
1
1
  import type { FragmentDefinition } from '../../core/index.js';
2
2
  import { useScrollSpy } from '../hooks/useScrollSpy.js';
3
- import { Sidebar } from '@fragments/ui';
3
+ import { Sidebar } from '@fragments-sdk/ui';
4
4
 
5
5
  interface RightSidebarProps {
6
6
  fragment: FragmentDefinition;
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useState, memo } from 'react';
7
7
  import html2canvas from 'html2canvas';
8
- import { Button, Tooltip } from '@fragments/ui';
8
+ import { Button, Tooltip } from '@fragments-sdk/ui';
9
9
  import { CameraIcon } from './Icons.js';
10
10
 
11
11
  interface ScreenshotButtonProps {
@@ -4,7 +4,7 @@
4
4
  * Shows animated placeholders while the app is loading.
5
5
  */
6
6
 
7
- import { Skeleton, Loading } from '@fragments/ui';
7
+ import { Skeleton, Loading } from '@fragments-sdk/ui';
8
8
 
9
9
  /**
10
10
  * Full app skeleton shown during initial load
@@ -1,3 +1,3 @@
1
1
  // Re-export Fragments UI Toast for use in the viewer
2
- export { ToastProvider, useToast } from '@fragments/ui';
3
- export type { ToastData as ToastMessage } from '@fragments/ui';
2
+ export { ToastProvider, useToast } from '@fragments-sdk/ui';
3
+ export type { ToastData as ToastMessage } from '@fragments-sdk/ui';
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { useState, useEffect, useCallback } from "react";
12
- import { Badge } from "@fragments/ui";
12
+ import { Badge } from "@fragments-sdk/ui";
13
13
  import type { DesignToken, EnhancedStyleDiffItem, TokenUsageSummary } from "../../core/index.js";
14
14
  import { CheckIcon, XIcon, LoadingIcon, FigmaIcon, WandIcon } from "./Icons.js";
15
15
 
@@ -17,7 +17,7 @@ import { ErrorBoundary } from "./ErrorBoundary.js";
17
17
  import { StoryRenderer, LoaderIndicator } from "./StoryRenderer.js";
18
18
  import { IsolatedPreviewFrame } from "./IsolatedPreviewFrame.js";
19
19
  import { ChevronDownIcon } from "./Icons.js";
20
- import { getBackgroundStyle, type BackgroundOption } from "./PreviewToolbar.js";
20
+ import { getBackgroundStyle, type BackgroundOption } from "../constants/ui.js";
21
21
 
22
22
  interface VariantMatrixProps {
23
23
  /** All variants to display */
@@ -1,4 +1,4 @@
1
- import { Tabs } from '@fragments/ui';
1
+ import { Tabs } from '@fragments-sdk/ui';
2
2
  import type { FragmentVariant } from '../../core/index.js';
3
3
  import { PlayIcon } from './Icons.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { Button, Menu, Stack, Input, Text, Box } from '@fragments/ui';
1
+ import { Button, Menu, Stack, Input, Text, Box } from '@fragments-sdk/ui';
2
2
  import { VIEWPORT_PRESETS, type ViewportPreset } from '../constants/ui.js';
3
3
  import {
4
4
  ViewportIcon,