@tpitre/story-ui 4.9.2 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -639,6 +639,92 @@
639
639
  flex-shrink: 0;
640
640
  }
641
641
 
642
+ /* ============================================
643
+ MCP Toggle Switch Component
644
+ ============================================ */
645
+ .sui-mcp-toggle {
646
+ display: flex;
647
+ align-items: center;
648
+ margin-left: var(--space-3);
649
+ padding-left: var(--space-3);
650
+ border-left: 1px solid hsl(var(--border));
651
+ }
652
+
653
+ .sui-toggle-label {
654
+ display: flex;
655
+ align-items: center;
656
+ gap: var(--space-2);
657
+ cursor: pointer;
658
+ user-select: none;
659
+ }
660
+
661
+ .sui-toggle-text {
662
+ font-size: 0.75rem;
663
+ font-weight: 500;
664
+ color: hsl(var(--muted-foreground));
665
+ white-space: nowrap;
666
+ }
667
+
668
+ .sui-toggle-switch {
669
+ position: relative;
670
+ width: 36px;
671
+ height: 20px;
672
+ flex-shrink: 0;
673
+ }
674
+
675
+ .sui-toggle-switch input {
676
+ opacity: 0;
677
+ width: 0;
678
+ height: 0;
679
+ }
680
+
681
+ .sui-toggle-slider {
682
+ position: absolute;
683
+ cursor: pointer;
684
+ top: 0;
685
+ left: 0;
686
+ right: 0;
687
+ bottom: 0;
688
+ background-color: hsl(var(--muted));
689
+ border: 1px solid hsl(var(--border));
690
+ border-radius: var(--radius-full);
691
+ transition: all var(--transition-fast);
692
+ }
693
+
694
+ .sui-toggle-slider::before {
695
+ position: absolute;
696
+ content: "";
697
+ height: 14px;
698
+ width: 14px;
699
+ left: 2px;
700
+ bottom: 2px;
701
+ background-color: hsl(var(--background));
702
+ border-radius: var(--radius-full);
703
+ box-shadow: var(--shadow-sm);
704
+ transition: all var(--transition-fast);
705
+ }
706
+
707
+ .sui-toggle-switch input:checked + .sui-toggle-slider {
708
+ background-color: hsl(var(--primary));
709
+ border-color: hsl(var(--primary));
710
+ }
711
+
712
+ .sui-toggle-switch input:checked + .sui-toggle-slider::before {
713
+ transform: translateX(16px);
714
+ }
715
+
716
+ .sui-toggle-switch input:focus + .sui-toggle-slider {
717
+ box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2);
718
+ }
719
+
720
+ .sui-toggle-label:hover .sui-toggle-slider {
721
+ background-color: hsl(var(--accent));
722
+ }
723
+
724
+ .sui-toggle-label:hover .sui-toggle-switch input:checked + .sui-toggle-slider {
725
+ background-color: hsl(var(--primary) / 0.9);
726
+ }
727
+
642
728
  /* ============================================
643
729
  ShadCN-Style Badge Component
644
730
  ============================================ */
@@ -1047,7 +1133,7 @@
1047
1133
  ============================================ */
1048
1134
  .sui-input-form {
1049
1135
  display: flex;
1050
- align-items: center;
1136
+ align-items: flex-end; /* Align buttons to bottom when textarea expands */
1051
1137
  gap: var(--space-2);
1052
1138
  background: hsl(var(--card));
1053
1139
  border: 1px solid hsl(var(--border));
@@ -1093,12 +1179,16 @@
1093
1179
  background: transparent;
1094
1180
  color: hsl(var(--foreground));
1095
1181
  font-size: 0.9375rem;
1096
- padding: 0 var(--space-3);
1097
- height: 40px;
1182
+ font-family: inherit;
1183
+ padding: 10px var(--space-3);
1184
+ min-height: 40px;
1185
+ max-height: 200px;
1098
1186
  min-width: 0;
1099
1187
  resize: none;
1100
1188
  outline: none;
1101
- /* Input type="text" auto-centers text vertically */
1189
+ line-height: 1.5;
1190
+ overflow-y: auto;
1191
+ /* Auto-expands with content via JS, scrolls when max-height reached */
1102
1192
  }
1103
1193
 
1104
1194
  .sui-input-form-field::placeholder {
@@ -1 +1 @@
1
- {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAwwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CAkrCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAy2B5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CAovCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -30,6 +30,8 @@ const initialState = {
30
30
  error: null,
31
31
  considerations: '',
32
32
  isDarkMode: false,
33
+ storybookMcpAvailable: false,
34
+ useStorybookMcp: true, // Default to enabled when available
33
35
  };
34
36
  function panelReducer(state, action) {
35
37
  switch (action.type) {
@@ -98,6 +100,10 @@ function panelReducer(state, action) {
98
100
  return { ...state, considerations: action.payload };
99
101
  case 'SET_DARK_MODE':
100
102
  return { ...state, isDarkMode: action.payload };
103
+ case 'SET_STORYBOOK_MCP_AVAILABLE':
104
+ return { ...state, storybookMcpAvailable: action.payload };
105
+ case 'SET_USE_STORYBOOK_MCP':
106
+ return { ...state, useStorybookMcp: action.payload };
101
107
  case 'NEW_CHAT':
102
108
  return { ...state, conversation: [], activeChatId: null, activeTitle: '' };
103
109
  default:
@@ -216,6 +222,80 @@ function saveProviderPrefs(provider, model) {
216
222
  console.error('Failed to save provider preferences:', e);
217
223
  }
218
224
  }
225
+ // Storage key for Storybook MCP preference
226
+ const STORYBOOK_MCP_PREF_KEY = 'story-ui-use-storybook-mcp';
227
+ /**
228
+ * Detect if Storybook MCP addon is available.
229
+ * Checks for the MCP endpoint that @storybook/addon-mcp exposes.
230
+ * The addon returns SSE (Server-Sent Events) responses, not JSON.
231
+ */
232
+ async function detectStorybookMcp() {
233
+ try {
234
+ // Try to detect Storybook MCP on the same origin (works when running in Storybook)
235
+ const storybookOrigin = typeof window !== 'undefined' ? window.location.origin : '';
236
+ const mcpEndpoint = `${storybookOrigin}/mcp`;
237
+ const response = await fetch(mcpEndpoint, {
238
+ method: 'POST',
239
+ headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify({
241
+ jsonrpc: '2.0',
242
+ id: 1,
243
+ method: 'tools/list',
244
+ params: {}
245
+ })
246
+ });
247
+ if (!response.ok)
248
+ return false;
249
+ // Storybook MCP addon returns SSE (Server-Sent Events) responses
250
+ // Check content-type or read a small portion to verify it's SSE format
251
+ const contentType = response.headers.get('content-type') || '';
252
+ if (contentType.includes('text/event-stream')) {
253
+ console.log('[StoryUI] Storybook MCP addon detected (SSE endpoint)');
254
+ return true;
255
+ }
256
+ // Also check by reading a portion of the response
257
+ const text = await response.text();
258
+ if (text.startsWith('event:') || text.startsWith('data:')) {
259
+ console.log('[StoryUI] Storybook MCP addon detected (SSE response)');
260
+ return true;
261
+ }
262
+ // Try parsing as JSON as fallback (some implementations may return JSON)
263
+ try {
264
+ const data = JSON.parse(text);
265
+ if (data && data.result && Array.isArray(data.result.tools)) {
266
+ console.log('[StoryUI] Storybook MCP addon detected (JSON response)');
267
+ return true;
268
+ }
269
+ }
270
+ catch {
271
+ // Not JSON, but might still be valid SSE that we missed
272
+ }
273
+ return false;
274
+ }
275
+ catch (e) {
276
+ // Not available - this is normal if addon-mcp isn't installed
277
+ return false;
278
+ }
279
+ }
280
+ function loadStorybookMcpPref() {
281
+ try {
282
+ const stored = localStorage.getItem(STORYBOOK_MCP_PREF_KEY);
283
+ if (stored !== null)
284
+ return JSON.parse(stored);
285
+ }
286
+ catch (e) {
287
+ console.error('Failed to load Storybook MCP preference:', e);
288
+ }
289
+ return true; // Default to enabled
290
+ }
291
+ function saveStorybookMcpPref(enabled) {
292
+ try {
293
+ localStorage.setItem(STORYBOOK_MCP_PREF_KEY, JSON.stringify(enabled));
294
+ }
295
+ catch (e) {
296
+ console.error('Failed to save Storybook MCP preference:', e);
297
+ }
298
+ }
219
299
  async function testMCPConnection() {
220
300
  try {
221
301
  const response = await fetch(PROVIDERS_API, { method: 'GET' });
@@ -288,12 +368,21 @@ function formatTime(timestamp) {
288
368
  }
289
369
  function getModelDisplayName(model) {
290
370
  const displayNames = {
371
+ // Claude models
291
372
  'claude-opus-4-5-20251101': 'Claude Opus 4.5',
292
373
  'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
293
374
  'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
375
+ 'claude-sonnet-4-20250514': 'Claude Sonnet 4',
376
+ // OpenAI models
377
+ 'gpt-5.2': 'GPT-5.2',
378
+ 'gpt-5.1': 'GPT-5.1',
294
379
  'gpt-4o': 'GPT-4o',
295
380
  'gpt-4o-mini': 'GPT-4o Mini',
296
381
  'o1': 'o1',
382
+ // Gemini models
383
+ 'gemini-3-pro-preview': 'Gemini 3 Pro Preview',
384
+ 'gemini-2.5-pro': 'Gemini 2.5 Pro',
385
+ 'gemini-2.5-flash': 'Gemini 2.5 Flash',
297
386
  'gemini-2.0-flash': 'Gemini 2.0 Flash',
298
387
  'gemini-1.5-pro': 'Gemini 1.5 Pro',
299
388
  };
@@ -449,6 +538,22 @@ function StoryUIPanel({ mcpPort }) {
449
538
  const [isDeletingOrphans, setIsDeletingOrphans] = useState(false);
450
539
  const chatEndRef = useRef(null);
451
540
  const inputRef = useRef(null);
541
+ // Auto-resize textarea based on content
542
+ const adjustTextareaHeight = useCallback(() => {
543
+ const textarea = inputRef.current;
544
+ if (textarea) {
545
+ // Reset height to auto to get the correct scrollHeight
546
+ textarea.style.height = 'auto';
547
+ // Set height to scrollHeight, capped at max-height (200px)
548
+ const maxHeight = 200;
549
+ const newHeight = Math.min(textarea.scrollHeight, maxHeight);
550
+ textarea.style.height = `${newHeight}px`;
551
+ }
552
+ }, []);
553
+ // Adjust height when input changes
554
+ useEffect(() => {
555
+ adjustTextareaHeight();
556
+ }, [state.input, adjustTextareaHeight]);
452
557
  const fileInputRef = useRef(null);
453
558
  const abortControllerRef = useRef(null);
454
559
  const hasShownRefreshHint = useRef(false);
@@ -525,6 +630,19 @@ function StoryUIPanel({ mcpPort }) {
525
630
  pollForExternalStories();
526
631
  return () => clearInterval(intervalId);
527
632
  }, []);
633
+ // Detect Storybook MCP addon availability
634
+ useEffect(() => {
635
+ const checkStorybookMcp = async () => {
636
+ const available = await detectStorybookMcp();
637
+ dispatch({ type: 'SET_STORYBOOK_MCP_AVAILABLE', payload: available });
638
+ // Load saved preference if MCP is available
639
+ if (available) {
640
+ const savedPref = loadStorybookMcpPref();
641
+ dispatch({ type: 'SET_USE_STORYBOOK_MCP', payload: savedPref });
642
+ }
643
+ };
644
+ checkStorybookMcp();
645
+ }, []);
528
646
  // Detect Storybook MANAGER theme (not preview background)
529
647
  // This ensures Story UI follows Storybook's overall theme, not the story preview background toggle
530
648
  useEffect(() => {
@@ -1054,6 +1172,7 @@ function StoryUIPanel({ mcpPort }) {
1054
1172
  provider: state.selectedProvider || undefined,
1055
1173
  model: state.selectedModel || undefined,
1056
1174
  considerations: state.considerations || undefined,
1175
+ useStorybookMcp: state.storybookMcpAvailable && state.useStorybookMcp,
1057
1176
  };
1058
1177
  console.log('[StoryUI DEBUG] Request body being sent:', {
1059
1178
  fileName: requestBody.fileName,
@@ -1144,6 +1263,7 @@ function StoryUIPanel({ mcpPort }) {
1144
1263
  provider: state.selectedProvider || undefined,
1145
1264
  model: state.selectedModel || undefined,
1146
1265
  considerations: state.considerations || undefined,
1266
+ useStorybookMcp: state.storybookMcpAvailable && state.useStorybookMcp,
1147
1267
  }),
1148
1268
  });
1149
1269
  const data = await res.json();
@@ -1368,13 +1488,25 @@ function StoryUIPanel({ mcpPort }) {
1368
1488
  handleConfirmRename(chat.id);
1369
1489
  if (e.key === 'Escape')
1370
1490
  handleCancelRename();
1371
- }, onClick: e => e.stopPropagation(), autoFocus: true }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleConfirmRename(chat.id); }, "aria-label": "Save", children: Icons.check }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleCancelRename(); }, "aria-label": "Cancel", children: Icons.x })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "sui-chat-item-title", children: chat.title }), _jsxs("div", { className: "sui-chat-item-actions", children: [_jsx("button", { className: "sui-chat-item-menu sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); setContextMenuId(contextMenuId === chat.id ? null : chat.id); }, "aria-label": "More options", children: Icons.moreVertical }), contextMenuId === chat.id && (_jsxs("div", { className: "sui-context-menu", children: [_jsxs("button", { className: "sui-context-menu-item", onClick: e => handleStartRename(chat.id, chat.title, e), children: [Icons.pencil, _jsx("span", { children: "Rename" })] }), _jsxs("button", { className: "sui-context-menu-item sui-context-menu-item-danger", onClick: e => handleDeleteChat(chat.id, e), children: [Icons.trash, _jsx("span", { children: "Delete" })] })] }))] })] })) }, chat.id))) }), orphanCount > 0 && (_jsx("div", { className: "sui-orphan-footer", children: _jsx("button", { className: "sui-orphan-delete-btn", onClick: handleDeleteOrphans, disabled: isDeletingOrphans, title: `${orphanCount} story ${orphanCount === 1 ? 'file has' : 'files have'} no associated chat`, children: isDeletingOrphans ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sui-orphan-spinner" }), _jsx("span", { children: "Deleting..." })] })) : (_jsxs(_Fragment, { children: [Icons.trash, _jsxs("span", { children: [orphanCount, " orphan ", orphanCount === 1 ? 'story' : 'stories'] })] })) }) }))] })), !state.sidebarOpen && (_jsx("div", { style: { padding: '12px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { className: "sui-button sui-button-ghost sui-button-icon", onClick: () => dispatch({ type: 'SET_SIDEBAR', payload: true }), "aria-label": "Show sidebar", children: Icons.panelLeft }) }))] }), _jsxs("main", { className: "sui-main", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [state.isDragging && (_jsx("div", { className: "sui-drop-overlay", children: _jsxs("div", { className: "sui-drop-overlay-text", children: [Icons.image, _jsx("span", { children: "Drop images here" })] }) })), _jsxs("header", { className: "sui-header", children: [_jsxs("div", { className: "sui-header-left", children: [_jsx("span", { className: "sui-header-title", children: "Story UI" }), _jsxs(Badge, { variant: state.connectionStatus.connected ? 'success' : 'destructive', children: [_jsx("span", { className: "sui-badge-dot" }), state.connectionStatus.connected ? getConnectionDisplayText() : 'Disconnected'] })] }), _jsx("div", { className: "sui-header-right", children: state.connectionStatus.connected && state.availableProviders.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: state.availableProviders.find(p => p.type === state.selectedProvider)?.name || 'Provider' }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedProvider, onChange: e => {
1372
- const newProvider = e.target.value;
1373
- dispatch({ type: 'SET_SELECTED_PROVIDER', payload: newProvider });
1374
- const provider = state.availableProviders.find(p => p.type === newProvider);
1375
- if (provider?.models.length)
1376
- dispatch({ type: 'SET_SELECTED_MODEL', payload: provider.models[0] });
1377
- }, "aria-label": "Select provider", children: state.availableProviders.map(p => _jsx("option", { value: p.type, children: p.name }, p.type)) })] }), _jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: getModelDisplayName(state.selectedModel) }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedModel, onChange: e => dispatch({ type: 'SET_SELECTED_MODEL', payload: e.target.value }), "aria-label": "Select model", children: state.availableProviders.find(p => p.type === state.selectedProvider)?.models.map(model => (_jsx("option", { value: model, children: getModelDisplayName(model) }, model))) })] })] })) })] }), _jsxs("section", { className: "sui-chat-area", role: "log", "aria-live": "polite", children: [state.error && _jsx("div", { className: "sui-error", role: "alert", style: { margin: '24px' }, children: state.error }), state.conversation.length === 0 && !state.loading ? (_jsxs("div", { className: "sui-welcome", children: [_jsx("h2", { className: "sui-welcome-greeting", children: "What would you like to create?" }), _jsx("p", { className: "sui-welcome-subtitle", children: "Describe any UI component and I'll generate a Storybook story" }), _jsxs("div", { className: "sui-welcome-chips", children: [_jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a responsive card with image, title, and description' }), children: "Card" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a navigation bar with logo and menu links' }), children: "Navbar" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a form with input fields and validation' }), children: "Form" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a hero section with headline and call-to-action' }), children: "Hero" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a button group with primary and secondary actions' }), children: "Buttons" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a modal dialog with header, content, and footer' }), children: "Modal" })] })] })) : (_jsxs("div", { className: "sui-chat-messages", children: [state.conversation.map((msg, i) => (_jsx("article", { className: `sui-message ${msg.role === 'user' ? 'sui-message-user' : 'sui-message-ai'}`, children: _jsxs("div", { className: "sui-message-bubble", children: [msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content, msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (_jsx("div", { className: "sui-message-images", children: msg.attachedImages.map(img => (_jsx("img", { src: img.base64 ? `data:${img.mediaType};base64,${img.base64}` : img.preview, alt: "attached", className: "sui-message-image" }, img.id))) }))] }) }, i))), state.loading && (_jsx("div", { className: "sui-message sui-message-ai", children: state.streamingState ? _jsx(ProgressIndicator, { streamingState: state.streamingState }) : (_jsx("div", { className: "sui-progress", children: _jsxs("span", { className: "sui-progress-label", children: ["Please give us a moment while we generate your story", _jsx("span", { className: "sui-loading" })] }) })) })), _jsx("div", { ref: chatEndRef })] }))] }), _jsx("div", { className: "sui-input-area", children: _jsxs("div", { className: "sui-input-container", children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: 'none' }, onChange: handleFileSelect }), state.attachedImages.length > 0 && (_jsxs("div", { className: "sui-image-previews", children: [_jsxs("span", { className: "sui-image-preview-label", children: [Icons.image, " ", state.attachedImages.length, " image", state.attachedImages.length > 1 ? 's' : ''] }), state.attachedImages.map(img => (_jsxs("div", { className: "sui-image-preview-item", children: [_jsx("img", { src: img.preview, alt: "preview", className: "sui-image-preview-thumb" }), _jsx("button", { className: "sui-image-preview-remove", onClick: () => removeAttachedImage(img.id), "aria-label": "Remove", children: Icons.x })] }, img.id)))] })), _jsxs("form", { onSubmit: handleSend, className: "sui-input-form", style: state.attachedImages.length > 0 ? { borderTopLeftRadius: 0, borderTopRightRadius: 0 } : undefined, children: [_jsx("button", { type: "button", className: "sui-input-form-upload", onClick: () => fileInputRef.current?.click(), disabled: state.loading || state.attachedImages.length >= MAX_IMAGES, "aria-label": "Attach images", children: Icons.image }), _jsx("input", { ref: inputRef, type: "text", className: "sui-input-form-field", value: state.input, onChange: e => dispatch({ type: 'SET_INPUT', payload: e.target.value }), onPaste: handlePaste, placeholder: state.attachedImages.length > 0 ? 'Describe what to create from these images...' : 'Describe a UI component...' }), _jsx("button", { type: "submit", className: "sui-input-form-send", disabled: state.loading || (!state.input.trim() && state.attachedImages.length === 0), "aria-label": "Send", children: Icons.send })] })] }) })] })] }));
1491
+ }, onClick: e => e.stopPropagation(), autoFocus: true }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleConfirmRename(chat.id); }, "aria-label": "Save", children: Icons.check }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleCancelRename(); }, "aria-label": "Cancel", children: Icons.x })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "sui-chat-item-title", children: chat.title }), _jsxs("div", { className: "sui-chat-item-actions", children: [_jsx("button", { className: "sui-chat-item-menu sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); setContextMenuId(contextMenuId === chat.id ? null : chat.id); }, "aria-label": "More options", children: Icons.moreVertical }), contextMenuId === chat.id && (_jsxs("div", { className: "sui-context-menu", children: [_jsxs("button", { className: "sui-context-menu-item", onClick: e => handleStartRename(chat.id, chat.title, e), children: [Icons.pencil, _jsx("span", { children: "Rename" })] }), _jsxs("button", { className: "sui-context-menu-item sui-context-menu-item-danger", onClick: e => handleDeleteChat(chat.id, e), children: [Icons.trash, _jsx("span", { children: "Delete" })] })] }))] })] })) }, chat.id))) }), orphanCount > 0 && (_jsx("div", { className: "sui-orphan-footer", children: _jsx("button", { className: "sui-orphan-delete-btn", onClick: handleDeleteOrphans, disabled: isDeletingOrphans, title: `${orphanCount} story ${orphanCount === 1 ? 'file has' : 'files have'} no associated chat`, children: isDeletingOrphans ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sui-orphan-spinner" }), _jsx("span", { children: "Deleting..." })] })) : (_jsxs(_Fragment, { children: [Icons.trash, _jsxs("span", { children: [orphanCount, " orphan ", orphanCount === 1 ? 'story' : 'stories'] })] })) }) }))] })), !state.sidebarOpen && (_jsx("div", { style: { padding: '12px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { className: "sui-button sui-button-ghost sui-button-icon", onClick: () => dispatch({ type: 'SET_SIDEBAR', payload: true }), "aria-label": "Show sidebar", children: Icons.panelLeft }) }))] }), _jsxs("main", { className: "sui-main", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [state.isDragging && (_jsx("div", { className: "sui-drop-overlay", children: _jsxs("div", { className: "sui-drop-overlay-text", children: [Icons.image, _jsx("span", { children: "Drop images here" })] }) })), _jsxs("header", { className: "sui-header", children: [_jsxs("div", { className: "sui-header-left", children: [_jsx("span", { className: "sui-header-title", children: "Story UI" }), _jsxs(Badge, { variant: state.connectionStatus.connected ? 'success' : 'destructive', children: [_jsx("span", { className: "sui-badge-dot" }), state.connectionStatus.connected ? getConnectionDisplayText() : 'Disconnected'] })] }), _jsxs("div", { className: "sui-header-right", children: [state.connectionStatus.connected && state.availableProviders.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: state.availableProviders.find(p => p.type === state.selectedProvider)?.name || 'Provider' }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedProvider, onChange: e => {
1492
+ const newProvider = e.target.value;
1493
+ dispatch({ type: 'SET_SELECTED_PROVIDER', payload: newProvider });
1494
+ const provider = state.availableProviders.find(p => p.type === newProvider);
1495
+ if (provider?.models.length)
1496
+ dispatch({ type: 'SET_SELECTED_MODEL', payload: provider.models[0] });
1497
+ }, "aria-label": "Select provider", children: state.availableProviders.map(p => _jsx("option", { value: p.type, children: p.name }, p.type)) })] }), _jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: getModelDisplayName(state.selectedModel) }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedModel, onChange: e => dispatch({ type: 'SET_SELECTED_MODEL', payload: e.target.value }), "aria-label": "Select model", children: state.availableProviders.find(p => p.type === state.selectedProvider)?.models.map(model => (_jsx("option", { value: model, children: getModelDisplayName(model) }, model))) })] })] })), state.storybookMcpAvailable && (_jsx("div", { className: "sui-mcp-toggle", title: "Use Storybook MCP context for enhanced component generation", children: _jsxs("label", { className: "sui-toggle-label", children: [_jsx("span", { className: "sui-toggle-text", children: "MCP Context" }), _jsxs("div", { className: "sui-toggle-switch", children: [_jsx("input", { type: "checkbox", checked: state.useStorybookMcp, onChange: e => {
1498
+ const enabled = e.target.checked;
1499
+ dispatch({ type: 'SET_USE_STORYBOOK_MCP', payload: enabled });
1500
+ saveStorybookMcpPref(enabled);
1501
+ }, "aria-label": "Use Storybook MCP context" }), _jsx("span", { className: "sui-toggle-slider" })] })] }) }))] })] }), _jsxs("section", { className: "sui-chat-area", role: "log", "aria-live": "polite", children: [state.error && _jsx("div", { className: "sui-error", role: "alert", style: { margin: '24px' }, children: state.error }), state.conversation.length === 0 && !state.loading ? (_jsxs("div", { className: "sui-welcome", children: [_jsx("h2", { className: "sui-welcome-greeting", children: "What would you like to create?" }), _jsx("p", { className: "sui-welcome-subtitle", children: "Describe any UI component and I'll generate a Storybook story" }), _jsxs("div", { className: "sui-welcome-chips", children: [_jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a responsive card with image, title, and description' }), children: "Card" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a navigation bar with logo and menu links' }), children: "Navbar" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a form with input fields and validation' }), children: "Form" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a hero section with headline and call-to-action' }), children: "Hero" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a button group with primary and secondary actions' }), children: "Buttons" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a modal dialog with header, content, and footer' }), children: "Modal" })] })] })) : (_jsxs("div", { className: "sui-chat-messages", children: [state.conversation.map((msg, i) => (_jsx("article", { className: `sui-message ${msg.role === 'user' ? 'sui-message-user' : 'sui-message-ai'}`, children: _jsxs("div", { className: "sui-message-bubble", children: [msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content, msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (_jsx("div", { className: "sui-message-images", children: msg.attachedImages.map(img => (_jsx("img", { src: img.base64 ? `data:${img.mediaType};base64,${img.base64}` : img.preview, alt: "attached", className: "sui-message-image" }, img.id))) }))] }) }, i))), state.loading && (_jsx("div", { className: "sui-message sui-message-ai", children: state.streamingState ? _jsx(ProgressIndicator, { streamingState: state.streamingState }) : (_jsx("div", { className: "sui-progress", children: _jsxs("span", { className: "sui-progress-label", children: ["Please give us a moment while we generate your story", _jsx("span", { className: "sui-loading" })] }) })) })), _jsx("div", { ref: chatEndRef })] }))] }), _jsx("div", { className: "sui-input-area", children: _jsxs("div", { className: "sui-input-container", children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: 'none' }, onChange: handleFileSelect }), state.attachedImages.length > 0 && (_jsxs("div", { className: "sui-image-previews", children: [_jsxs("span", { className: "sui-image-preview-label", children: [Icons.image, " ", state.attachedImages.length, " image", state.attachedImages.length > 1 ? 's' : ''] }), state.attachedImages.map(img => (_jsxs("div", { className: "sui-image-preview-item", children: [_jsx("img", { src: img.preview, alt: "preview", className: "sui-image-preview-thumb" }), _jsx("button", { className: "sui-image-preview-remove", onClick: () => removeAttachedImage(img.id), "aria-label": "Remove", children: Icons.x })] }, img.id)))] })), _jsxs("form", { onSubmit: handleSend, className: "sui-input-form", style: state.attachedImages.length > 0 ? { borderTopLeftRadius: 0, borderTopRightRadius: 0 } : undefined, children: [_jsx("button", { type: "button", className: "sui-input-form-upload", onClick: () => fileInputRef.current?.click(), disabled: state.loading || state.attachedImages.length >= MAX_IMAGES, "aria-label": "Attach images", children: Icons.image }), _jsx("textarea", { ref: inputRef, rows: 1, className: "sui-input-form-field", value: state.input, onChange: e => dispatch({ type: 'SET_INPUT', payload: e.target.value }), onKeyDown: e => {
1502
+ // Submit on Enter, newline on Shift+Enter
1503
+ if (e.key === 'Enter' && !e.shiftKey) {
1504
+ e.preventDefault();
1505
+ if (!state.loading && (state.input.trim() || state.attachedImages.length > 0)) {
1506
+ handleSend(e);
1507
+ }
1508
+ }
1509
+ }, onPaste: handlePaste, placeholder: state.attachedImages.length > 0 ? 'Describe what to create from these images...' : 'Describe a UI component...' }), _jsx("button", { type: "submit", className: "sui-input-form-send", disabled: state.loading || (!state.input.trim() && state.attachedImages.length === 0), "aria-label": "Send", children: Icons.send })] })] }) })] })] }));
1378
1510
  }
1379
1511
  export default StoryUIPanel;
1380
1512
  export { StoryUIPanel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "4.9.2",
3
+ "version": "4.11.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -639,6 +639,92 @@
639
639
  flex-shrink: 0;
640
640
  }
641
641
 
642
+ /* ============================================
643
+ MCP Toggle Switch Component
644
+ ============================================ */
645
+ .sui-mcp-toggle {
646
+ display: flex;
647
+ align-items: center;
648
+ margin-left: var(--space-3);
649
+ padding-left: var(--space-3);
650
+ border-left: 1px solid hsl(var(--border));
651
+ }
652
+
653
+ .sui-toggle-label {
654
+ display: flex;
655
+ align-items: center;
656
+ gap: var(--space-2);
657
+ cursor: pointer;
658
+ user-select: none;
659
+ }
660
+
661
+ .sui-toggle-text {
662
+ font-size: 0.75rem;
663
+ font-weight: 500;
664
+ color: hsl(var(--muted-foreground));
665
+ white-space: nowrap;
666
+ }
667
+
668
+ .sui-toggle-switch {
669
+ position: relative;
670
+ width: 36px;
671
+ height: 20px;
672
+ flex-shrink: 0;
673
+ }
674
+
675
+ .sui-toggle-switch input {
676
+ opacity: 0;
677
+ width: 0;
678
+ height: 0;
679
+ }
680
+
681
+ .sui-toggle-slider {
682
+ position: absolute;
683
+ cursor: pointer;
684
+ top: 0;
685
+ left: 0;
686
+ right: 0;
687
+ bottom: 0;
688
+ background-color: hsl(var(--muted));
689
+ border: 1px solid hsl(var(--border));
690
+ border-radius: var(--radius-full);
691
+ transition: all var(--transition-fast);
692
+ }
693
+
694
+ .sui-toggle-slider::before {
695
+ position: absolute;
696
+ content: "";
697
+ height: 14px;
698
+ width: 14px;
699
+ left: 2px;
700
+ bottom: 2px;
701
+ background-color: hsl(var(--background));
702
+ border-radius: var(--radius-full);
703
+ box-shadow: var(--shadow-sm);
704
+ transition: all var(--transition-fast);
705
+ }
706
+
707
+ .sui-toggle-switch input:checked + .sui-toggle-slider {
708
+ background-color: hsl(var(--primary));
709
+ border-color: hsl(var(--primary));
710
+ }
711
+
712
+ .sui-toggle-switch input:checked + .sui-toggle-slider::before {
713
+ transform: translateX(16px);
714
+ }
715
+
716
+ .sui-toggle-switch input:focus + .sui-toggle-slider {
717
+ box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2);
718
+ }
719
+
720
+ .sui-toggle-label:hover .sui-toggle-slider {
721
+ background-color: hsl(var(--accent));
722
+ }
723
+
724
+ .sui-toggle-label:hover .sui-toggle-switch input:checked + .sui-toggle-slider {
725
+ background-color: hsl(var(--primary) / 0.9);
726
+ }
727
+
642
728
  /* ============================================
643
729
  ShadCN-Style Badge Component
644
730
  ============================================ */
@@ -1047,7 +1133,7 @@
1047
1133
  ============================================ */
1048
1134
  .sui-input-form {
1049
1135
  display: flex;
1050
- align-items: center;
1136
+ align-items: flex-end; /* Align buttons to bottom when textarea expands */
1051
1137
  gap: var(--space-2);
1052
1138
  background: hsl(var(--card));
1053
1139
  border: 1px solid hsl(var(--border));
@@ -1093,12 +1179,16 @@
1093
1179
  background: transparent;
1094
1180
  color: hsl(var(--foreground));
1095
1181
  font-size: 0.9375rem;
1096
- padding: 0 var(--space-3);
1097
- height: 40px;
1182
+ font-family: inherit;
1183
+ padding: 10px var(--space-3);
1184
+ min-height: 40px;
1185
+ max-height: 200px;
1098
1186
  min-width: 0;
1099
1187
  resize: none;
1100
1188
  outline: none;
1101
- /* Input type="text" auto-centers text vertically */
1189
+ line-height: 1.5;
1190
+ overflow-y: auto;
1191
+ /* Auto-expands with content via JS, scrolls when max-height reached */
1102
1192
  }
1103
1193
 
1104
1194
  .sui-input-form-field::placeholder {
@@ -158,6 +158,8 @@ interface PanelState {
158
158
  error: string | null;
159
159
  considerations: string;
160
160
  isDarkMode: boolean;
161
+ storybookMcpAvailable: boolean;
162
+ useStorybookMcp: boolean;
161
163
  }
162
164
 
163
165
  type PanelAction =
@@ -188,6 +190,8 @@ type PanelAction =
188
190
  | { type: 'SET_ERROR'; payload: string | null }
189
191
  | { type: 'SET_CONSIDERATIONS'; payload: string }
190
192
  | { type: 'SET_DARK_MODE'; payload: boolean }
193
+ | { type: 'SET_STORYBOOK_MCP_AVAILABLE'; payload: boolean }
194
+ | { type: 'SET_USE_STORYBOOK_MCP'; payload: boolean }
191
195
  | { type: 'NEW_CHAT' };
192
196
 
193
197
  const initialState: PanelState = {
@@ -212,6 +216,8 @@ const initialState: PanelState = {
212
216
  error: null,
213
217
  considerations: '',
214
218
  isDarkMode: false,
219
+ storybookMcpAvailable: false,
220
+ useStorybookMcp: true, // Default to enabled when available
215
221
  };
216
222
 
217
223
  function panelReducer(state: PanelState, action: PanelAction): PanelState {
@@ -280,6 +286,10 @@ function panelReducer(state: PanelState, action: PanelAction): PanelState {
280
286
  return { ...state, considerations: action.payload };
281
287
  case 'SET_DARK_MODE':
282
288
  return { ...state, isDarkMode: action.payload };
289
+ case 'SET_STORYBOOK_MCP_AVAILABLE':
290
+ return { ...state, storybookMcpAvailable: action.payload };
291
+ case 'SET_USE_STORYBOOK_MCP':
292
+ return { ...state, useStorybookMcp: action.payload };
283
293
  case 'NEW_CHAT':
284
294
  return { ...state, conversation: [], activeChatId: null, activeTitle: '' };
285
295
  default:
@@ -403,6 +413,84 @@ function saveProviderPrefs(provider: string, model: string): void {
403
413
  }
404
414
  }
405
415
 
416
+ // Storage key for Storybook MCP preference
417
+ const STORYBOOK_MCP_PREF_KEY = 'story-ui-use-storybook-mcp';
418
+
419
+ /**
420
+ * Detect if Storybook MCP addon is available.
421
+ * Checks for the MCP endpoint that @storybook/addon-mcp exposes.
422
+ * The addon returns SSE (Server-Sent Events) responses, not JSON.
423
+ */
424
+ async function detectStorybookMcp(): Promise<boolean> {
425
+ try {
426
+ // Try to detect Storybook MCP on the same origin (works when running in Storybook)
427
+ const storybookOrigin = typeof window !== 'undefined' ? window.location.origin : '';
428
+ const mcpEndpoint = `${storybookOrigin}/mcp`;
429
+
430
+ const response = await fetch(mcpEndpoint, {
431
+ method: 'POST',
432
+ headers: { 'Content-Type': 'application/json' },
433
+ body: JSON.stringify({
434
+ jsonrpc: '2.0',
435
+ id: 1,
436
+ method: 'tools/list',
437
+ params: {}
438
+ })
439
+ });
440
+
441
+ if (!response.ok) return false;
442
+
443
+ // Storybook MCP addon returns SSE (Server-Sent Events) responses
444
+ // Check content-type or read a small portion to verify it's SSE format
445
+ const contentType = response.headers.get('content-type') || '';
446
+ if (contentType.includes('text/event-stream')) {
447
+ console.log('[StoryUI] Storybook MCP addon detected (SSE endpoint)');
448
+ return true;
449
+ }
450
+
451
+ // Also check by reading a portion of the response
452
+ const text = await response.text();
453
+ if (text.startsWith('event:') || text.startsWith('data:')) {
454
+ console.log('[StoryUI] Storybook MCP addon detected (SSE response)');
455
+ return true;
456
+ }
457
+
458
+ // Try parsing as JSON as fallback (some implementations may return JSON)
459
+ try {
460
+ const data = JSON.parse(text);
461
+ if (data && data.result && Array.isArray(data.result.tools)) {
462
+ console.log('[StoryUI] Storybook MCP addon detected (JSON response)');
463
+ return true;
464
+ }
465
+ } catch {
466
+ // Not JSON, but might still be valid SSE that we missed
467
+ }
468
+
469
+ return false;
470
+ } catch (e) {
471
+ // Not available - this is normal if addon-mcp isn't installed
472
+ return false;
473
+ }
474
+ }
475
+
476
+ function loadStorybookMcpPref(): boolean {
477
+ try {
478
+ const stored = localStorage.getItem(STORYBOOK_MCP_PREF_KEY);
479
+ if (stored !== null) return JSON.parse(stored);
480
+ } catch (e) {
481
+ console.error('Failed to load Storybook MCP preference:', e);
482
+ }
483
+ return true; // Default to enabled
484
+ }
485
+
486
+ function saveStorybookMcpPref(enabled: boolean): void {
487
+ try {
488
+ localStorage.setItem(STORYBOOK_MCP_PREF_KEY, JSON.stringify(enabled));
489
+ } catch (e) {
490
+ console.error('Failed to save Storybook MCP preference:', e);
491
+ }
492
+ }
493
+
406
494
  async function testMCPConnection(): Promise<{ connected: boolean; error?: string }> {
407
495
  try {
408
496
  const response = await fetch(PROVIDERS_API, { method: 'GET' });
@@ -471,12 +559,21 @@ function formatTime(timestamp: number): string {
471
559
 
472
560
  function getModelDisplayName(model: string): string {
473
561
  const displayNames: Record<string, string> = {
562
+ // Claude models
474
563
  'claude-opus-4-5-20251101': 'Claude Opus 4.5',
475
564
  'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
476
565
  'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
566
+ 'claude-sonnet-4-20250514': 'Claude Sonnet 4',
567
+ // OpenAI models
568
+ 'gpt-5.2': 'GPT-5.2',
569
+ 'gpt-5.1': 'GPT-5.1',
477
570
  'gpt-4o': 'GPT-4o',
478
571
  'gpt-4o-mini': 'GPT-4o Mini',
479
572
  'o1': 'o1',
573
+ // Gemini models
574
+ 'gemini-3-pro-preview': 'Gemini 3 Pro Preview',
575
+ 'gemini-2.5-pro': 'Gemini 2.5 Pro',
576
+ 'gemini-2.5-flash': 'Gemini 2.5 Flash',
480
577
  'gemini-2.0-flash': 'Gemini 2.0 Flash',
481
578
  'gemini-1.5-pro': 'Gemini 1.5 Pro',
482
579
  };
@@ -795,7 +892,25 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
795
892
  const [orphanCount, setOrphanCount] = useState<number>(0);
796
893
  const [isDeletingOrphans, setIsDeletingOrphans] = useState<boolean>(false);
797
894
  const chatEndRef = useRef<HTMLDivElement>(null);
798
- const inputRef = useRef<HTMLInputElement>(null);
895
+ const inputRef = useRef<HTMLTextAreaElement>(null);
896
+
897
+ // Auto-resize textarea based on content
898
+ const adjustTextareaHeight = useCallback(() => {
899
+ const textarea = inputRef.current;
900
+ if (textarea) {
901
+ // Reset height to auto to get the correct scrollHeight
902
+ textarea.style.height = 'auto';
903
+ // Set height to scrollHeight, capped at max-height (200px)
904
+ const maxHeight = 200;
905
+ const newHeight = Math.min(textarea.scrollHeight, maxHeight);
906
+ textarea.style.height = `${newHeight}px`;
907
+ }
908
+ }, []);
909
+
910
+ // Adjust height when input changes
911
+ useEffect(() => {
912
+ adjustTextareaHeight();
913
+ }, [state.input, adjustTextareaHeight]);
799
914
  const fileInputRef = useRef<HTMLInputElement>(null);
800
915
  const abortControllerRef = useRef<AbortController | null>(null);
801
916
  const hasShownRefreshHint = useRef(false);
@@ -881,6 +996,22 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
881
996
  return () => clearInterval(intervalId);
882
997
  }, []);
883
998
 
999
+ // Detect Storybook MCP addon availability
1000
+ useEffect(() => {
1001
+ const checkStorybookMcp = async () => {
1002
+ const available = await detectStorybookMcp();
1003
+ dispatch({ type: 'SET_STORYBOOK_MCP_AVAILABLE', payload: available });
1004
+
1005
+ // Load saved preference if MCP is available
1006
+ if (available) {
1007
+ const savedPref = loadStorybookMcpPref();
1008
+ dispatch({ type: 'SET_USE_STORYBOOK_MCP', payload: savedPref });
1009
+ }
1010
+ };
1011
+
1012
+ checkStorybookMcp();
1013
+ }, []);
1014
+
884
1015
  // Detect Storybook MANAGER theme (not preview background)
885
1016
  // This ensures Story UI follows Storybook's overall theme, not the story preview background toggle
886
1017
  useEffect(() => {
@@ -1418,6 +1549,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
1418
1549
  provider: state.selectedProvider || undefined,
1419
1550
  model: state.selectedModel || undefined,
1420
1551
  considerations: state.considerations || undefined,
1552
+ useStorybookMcp: state.storybookMcpAvailable && state.useStorybookMcp,
1421
1553
  };
1422
1554
  console.log('[StoryUI DEBUG] Request body being sent:', {
1423
1555
  fileName: requestBody.fileName,
@@ -1501,6 +1633,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
1501
1633
  provider: state.selectedProvider || undefined,
1502
1634
  model: state.selectedModel || undefined,
1503
1635
  considerations: state.considerations || undefined,
1636
+ useStorybookMcp: state.storybookMcpAvailable && state.useStorybookMcp,
1504
1637
  }),
1505
1638
  });
1506
1639
  const data = await res.json();
@@ -1890,6 +2023,27 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
1890
2023
  </div>
1891
2024
  </>
1892
2025
  )}
2026
+ {/* Storybook MCP Toggle - only shown when MCP addon is detected */}
2027
+ {state.storybookMcpAvailable && (
2028
+ <div className="sui-mcp-toggle" title="Use Storybook MCP context for enhanced component generation">
2029
+ <label className="sui-toggle-label">
2030
+ <span className="sui-toggle-text">MCP Context</span>
2031
+ <div className="sui-toggle-switch">
2032
+ <input
2033
+ type="checkbox"
2034
+ checked={state.useStorybookMcp}
2035
+ onChange={e => {
2036
+ const enabled = e.target.checked;
2037
+ dispatch({ type: 'SET_USE_STORYBOOK_MCP', payload: enabled });
2038
+ saveStorybookMcpPref(enabled);
2039
+ }}
2040
+ aria-label="Use Storybook MCP context"
2041
+ />
2042
+ <span className="sui-toggle-slider" />
2043
+ </div>
2044
+ </label>
2045
+ </div>
2046
+ )}
1893
2047
  </div>
1894
2048
  </header>
1895
2049
 
@@ -1971,12 +2125,21 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
1971
2125
  <button type="button" className="sui-input-form-upload" onClick={() => fileInputRef.current?.click()} disabled={state.loading || state.attachedImages.length >= MAX_IMAGES} aria-label="Attach images">
1972
2126
  {Icons.image}
1973
2127
  </button>
1974
- <input
2128
+ <textarea
1975
2129
  ref={inputRef}
1976
- type="text"
2130
+ rows={1}
1977
2131
  className="sui-input-form-field"
1978
2132
  value={state.input}
1979
2133
  onChange={e => dispatch({ type: 'SET_INPUT', payload: e.target.value })}
2134
+ onKeyDown={e => {
2135
+ // Submit on Enter, newline on Shift+Enter
2136
+ if (e.key === 'Enter' && !e.shiftKey) {
2137
+ e.preventDefault();
2138
+ if (!state.loading && (state.input.trim() || state.attachedImages.length > 0)) {
2139
+ handleSend(e as unknown as React.FormEvent);
2140
+ }
2141
+ }
2142
+ }}
1980
2143
  onPaste={handlePaste}
1981
2144
  placeholder={state.attachedImages.length > 0 ? 'Describe what to create from these images...' : 'Describe a UI component...'}
1982
2145
  />