@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:
|
|
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
|
-
|
|
1097
|
-
|
|
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
|
-
|
|
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,
|
|
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("
|
|
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
|
@@ -1047,7 +1047,7 @@
|
|
|
1047
1047
|
============================================ */
|
|
1048
1048
|
.sui-input-form {
|
|
1049
1049
|
display: flex;
|
|
1050
|
-
align-items:
|
|
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
|
-
|
|
1097
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
<
|
|
1992
|
+
<textarea
|
|
1975
1993
|
ref={inputRef}
|
|
1976
|
-
|
|
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
|
/>
|