@tpitre/story-ui 4.9.1 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -481,6 +481,21 @@ claude mcp add --transport http story-ui https://your-app-name.up.railway.app/mc
481
481
 
482
482
  Or add it to Claude Desktop via **Settings** → **Connectors** → **Add custom connector**.
483
483
 
484
+ **Important for Storybook Live Mode Deployments:**
485
+
486
+ If deploying Storybook with Story UI integrated (where users can generate stories in the deployed app), ensure the StoryUI panel files are committed to git:
487
+
488
+ ```bash
489
+ # Check if StoryUI is incorrectly gitignored
490
+ grep "StoryUI" .gitignore
491
+
492
+ # If found, remove from .gitignore and commit the panel
493
+ git add src/stories/StoryUI/
494
+ git commit -m "Add StoryUI panel for production"
495
+ ```
496
+
497
+ > **Note**: Story UI versions prior to 4.10.0 incorrectly added `src/stories/StoryUI/` to `.gitignore`. See [DEPLOYMENT.md](DEPLOYMENT.md#storybook-live-mode-deployment) for full instructions.
498
+
484
499
  See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment instructions and troubleshooting.
485
500
 
486
501
  ---
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAmDA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AAiWD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,iBA45B5D"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAmDA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AAiWD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,iBA85B5D"}
package/dist/cli/setup.js CHANGED
@@ -1153,10 +1153,12 @@ VITE_STORY_UI_PORT=${answers.mcpPort || '4001'}
1153
1153
  const gitignorePath = path.join(process.cwd(), '.gitignore');
1154
1154
  if (fs.existsSync(gitignorePath)) {
1155
1155
  const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
1156
+ // NOTE: Do NOT add StoryUI/ to gitignore - it must be committed for production deployments
1157
+ // The StoryUI panel component needs to be deployed to Railway/production environments
1156
1158
  const patterns = [
1157
1159
  '.env',
1158
1160
  path.relative(process.cwd(), config.generatedStoriesPath),
1159
- `${path.relative(process.cwd(), storiesDir)}/StoryUI/`
1161
+ '.story-ui-history/'
1160
1162
  ];
1161
1163
  let gitignoreUpdated = false;
1162
1164
  for (const pattern of patterns) {
@@ -1047,7 +1047,7 @@
1047
1047
  ============================================ */
1048
1048
  .sui-input-form {
1049
1049
  display: flex;
1050
- align-items: center;
1050
+ align-items: flex-end; /* Align buttons to bottom when textarea expands */
1051
1051
  gap: var(--space-2);
1052
1052
  background: hsl(var(--card));
1053
1053
  border: 1px solid hsl(var(--border));
@@ -1093,12 +1093,16 @@
1093
1093
  background: transparent;
1094
1094
  color: hsl(var(--foreground));
1095
1095
  font-size: 0.9375rem;
1096
- padding: 0 var(--space-3);
1097
- height: 40px;
1096
+ font-family: inherit;
1097
+ padding: 10px var(--space-3);
1098
+ min-height: 40px;
1099
+ max-height: 200px;
1098
1100
  min-width: 0;
1099
1101
  resize: none;
1100
1102
  outline: none;
1101
- /* Input type="text" auto-centers text vertically */
1103
+ line-height: 1.5;
1104
+ overflow-y: auto;
1105
+ /* Auto-expands with content via JS, scrolls when max-height reached */
1102
1106
  }
1103
1107
 
1104
1108
  .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;AAwwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CA6sCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -449,6 +449,22 @@ function StoryUIPanel({ mcpPort }) {
449
449
  const [isDeletingOrphans, setIsDeletingOrphans] = useState(false);
450
450
  const chatEndRef = useRef(null);
451
451
  const inputRef = useRef(null);
452
+ // Auto-resize textarea based on content
453
+ const adjustTextareaHeight = useCallback(() => {
454
+ const textarea = inputRef.current;
455
+ if (textarea) {
456
+ // Reset height to auto to get the correct scrollHeight
457
+ textarea.style.height = 'auto';
458
+ // Set height to scrollHeight, capped at max-height (200px)
459
+ const maxHeight = 200;
460
+ const newHeight = Math.min(textarea.scrollHeight, maxHeight);
461
+ textarea.style.height = `${newHeight}px`;
462
+ }
463
+ }, []);
464
+ // Adjust height when input changes
465
+ useEffect(() => {
466
+ adjustTextareaHeight();
467
+ }, [state.input, adjustTextareaHeight]);
452
468
  const fileInputRef = useRef(null);
453
469
  const abortControllerRef = useRef(null);
454
470
  const hasShownRefreshHint = useRef(false);
@@ -1374,7 +1390,15 @@ function StoryUIPanel({ mcpPort }) {
1374
1390
  const provider = state.availableProviders.find(p => p.type === newProvider);
1375
1391
  if (provider?.models.length)
1376
1392
  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 })] })] }) })] })] }));
1393
+ }, "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("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 => {
1394
+ // Submit on Enter, newline on Shift+Enter
1395
+ if (e.key === 'Enter' && !e.shiftKey) {
1396
+ e.preventDefault();
1397
+ if (!state.loading && (state.input.trim() || state.attachedImages.length > 0)) {
1398
+ handleSend(e);
1399
+ }
1400
+ }
1401
+ }, 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
1402
  }
1379
1403
  export default StoryUIPanel;
1380
1404
  export { StoryUIPanel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "4.9.1",
3
+ "version": "4.10.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1047,7 +1047,7 @@
1047
1047
  ============================================ */
1048
1048
  .sui-input-form {
1049
1049
  display: flex;
1050
- align-items: center;
1050
+ align-items: flex-end; /* Align buttons to bottom when textarea expands */
1051
1051
  gap: var(--space-2);
1052
1052
  background: hsl(var(--card));
1053
1053
  border: 1px solid hsl(var(--border));
@@ -1093,12 +1093,16 @@
1093
1093
  background: transparent;
1094
1094
  color: hsl(var(--foreground));
1095
1095
  font-size: 0.9375rem;
1096
- padding: 0 var(--space-3);
1097
- height: 40px;
1096
+ font-family: inherit;
1097
+ padding: 10px var(--space-3);
1098
+ min-height: 40px;
1099
+ max-height: 200px;
1098
1100
  min-width: 0;
1099
1101
  resize: none;
1100
1102
  outline: none;
1101
- /* Input type="text" auto-centers text vertically */
1103
+ line-height: 1.5;
1104
+ overflow-y: auto;
1105
+ /* Auto-expands with content via JS, scrolls when max-height reached */
1102
1106
  }
1103
1107
 
1104
1108
  .sui-input-form-field::placeholder {
@@ -795,7 +795,25 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
795
795
  const [orphanCount, setOrphanCount] = useState<number>(0);
796
796
  const [isDeletingOrphans, setIsDeletingOrphans] = useState<boolean>(false);
797
797
  const chatEndRef = useRef<HTMLDivElement>(null);
798
- const inputRef = useRef<HTMLInputElement>(null);
798
+ const inputRef = useRef<HTMLTextAreaElement>(null);
799
+
800
+ // Auto-resize textarea based on content
801
+ const adjustTextareaHeight = useCallback(() => {
802
+ const textarea = inputRef.current;
803
+ if (textarea) {
804
+ // Reset height to auto to get the correct scrollHeight
805
+ textarea.style.height = 'auto';
806
+ // Set height to scrollHeight, capped at max-height (200px)
807
+ const maxHeight = 200;
808
+ const newHeight = Math.min(textarea.scrollHeight, maxHeight);
809
+ textarea.style.height = `${newHeight}px`;
810
+ }
811
+ }, []);
812
+
813
+ // Adjust height when input changes
814
+ useEffect(() => {
815
+ adjustTextareaHeight();
816
+ }, [state.input, adjustTextareaHeight]);
799
817
  const fileInputRef = useRef<HTMLInputElement>(null);
800
818
  const abortControllerRef = useRef<AbortController | null>(null);
801
819
  const hasShownRefreshHint = useRef(false);
@@ -1971,12 +1989,21 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
1971
1989
  <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
1990
  {Icons.image}
1973
1991
  </button>
1974
- <input
1992
+ <textarea
1975
1993
  ref={inputRef}
1976
- type="text"
1994
+ rows={1}
1977
1995
  className="sui-input-form-field"
1978
1996
  value={state.input}
1979
1997
  onChange={e => dispatch({ type: 'SET_INPUT', payload: e.target.value })}
1998
+ onKeyDown={e => {
1999
+ // Submit on Enter, newline on Shift+Enter
2000
+ if (e.key === 'Enter' && !e.shiftKey) {
2001
+ e.preventDefault();
2002
+ if (!state.loading && (state.input.trim() || state.attachedImages.length > 0)) {
2003
+ handleSend(e as unknown as React.FormEvent);
2004
+ }
2005
+ }
2006
+ }}
1980
2007
  onPaste={handlePaste}
1981
2008
  placeholder={state.attachedImages.length > 0 ? 'Describe what to create from these images...' : 'Describe a UI component...'}
1982
2009
  />