@tpitre/story-ui 4.9.2 → 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.
@@ -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.2",
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
  />