@usefragments/ui 1.3.2 → 1.4.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 +3 -1
- package/dist/assets/ui.css +1261 -849
- package/dist/chart.cjs +60 -24
- package/dist/chart.js +38 -24
- package/dist/codeblock.cjs +14 -6
- package/dist/codeblock.js +14 -6
- package/dist/colorpicker.cjs +56 -9
- package/dist/colorpicker.js +34 -9
- package/dist/components/Badge/Badge.module.scss.cjs +23 -16
- package/dist/components/Badge/Badge.module.scss.js +23 -16
- package/dist/components/Badge/index.cjs +8 -1
- package/dist/components/Badge/index.d.ts +3 -0
- package/dist/components/Badge/index.d.ts.map +1 -1
- package/dist/components/Badge/index.js +8 -1
- package/dist/components/Button/Button.module.scss.cjs +16 -13
- package/dist/components/Button/Button.module.scss.js +16 -13
- package/dist/components/Button/index.d.ts +2 -1
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Chart/index.d.ts +3 -0
- package/dist/components/Chart/index.d.ts.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +21 -21
- package/dist/components/CodeBlock/CodeBlock.module.scss.js +21 -21
- package/dist/components/CodeBlock/index.d.ts.map +1 -1
- package/dist/components/ColorPicker/index.d.ts +4 -0
- package/dist/components/ColorPicker/index.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.module.scss.cjs +23 -23
- package/dist/components/Combobox/Combobox.module.scss.js +23 -23
- package/dist/components/Command/Command.module.scss.cjs +11 -11
- package/dist/components/Command/Command.module.scss.js +11 -11
- package/dist/components/ConversationList/ConversationList.module.scss.cjs +13 -10
- package/dist/components/ConversationList/ConversationList.module.scss.js +14 -11
- package/dist/components/ConversationList/index.cjs +26 -22
- package/dist/components/ConversationList/index.d.ts +5 -1
- package/dist/components/ConversationList/index.d.ts.map +1 -1
- package/dist/components/ConversationList/index.js +27 -23
- package/dist/components/DataTable/DataTable.module.scss.cjs +35 -35
- package/dist/components/DataTable/DataTable.module.scss.js +35 -35
- package/dist/components/DataTable/index.d.ts +4 -1
- package/dist/components/DataTable/index.d.ts.map +1 -1
- package/dist/components/DatePicker/index.d.ts +3 -0
- package/dist/components/DatePicker/index.d.ts.map +1 -1
- package/dist/components/Editor/index.d.ts +4 -1
- package/dist/components/Editor/index.d.ts.map +1 -1
- package/dist/components/Listbox/Listbox.module.scss.cjs +8 -8
- package/dist/components/Listbox/Listbox.module.scss.js +8 -8
- package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
- package/dist/components/Markdown/Markdown.module.scss.js +1 -1
- package/dist/components/Markdown/index.d.ts +16 -0
- package/dist/components/Markdown/index.d.ts.map +1 -1
- package/dist/components/Menu/Menu.module.scss.cjs +14 -14
- package/dist/components/Menu/Menu.module.scss.js +14 -14
- package/dist/components/Message/Message.module.scss.cjs +23 -20
- package/dist/components/Message/Message.module.scss.js +24 -21
- package/dist/components/Message/index.cjs +5 -1
- package/dist/components/Message/index.d.ts.map +1 -1
- package/dist/components/Message/index.js +5 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.scss.cjs +28 -28
- package/dist/components/NavigationMenu/NavigationMenu.module.scss.js +28 -28
- package/dist/components/Pagination/Pagination.module.scss.cjs +7 -7
- package/dist/components/Pagination/Pagination.module.scss.js +7 -7
- package/dist/components/Prompt/Prompt.module.scss.cjs +52 -22
- package/dist/components/Prompt/Prompt.module.scss.js +52 -22
- package/dist/components/Prompt/index.cjs +197 -2
- package/dist/components/Prompt/index.d.ts +98 -3
- package/dist/components/Prompt/index.d.ts.map +1 -1
- package/dist/components/Prompt/index.js +198 -3
- package/dist/components/Select/Select.module.scss.cjs +29 -17
- package/dist/components/Select/Select.module.scss.js +30 -18
- package/dist/components/Select/index.cjs +45 -8
- package/dist/components/Select/index.d.ts +17 -2
- package/dist/components/Select/index.d.ts.map +1 -1
- package/dist/components/Select/index.js +45 -8
- package/dist/components/Sidebar/Sidebar.module.scss.cjs +45 -42
- package/dist/components/Sidebar/Sidebar.module.scss.js +45 -42
- package/dist/components/Sidebar/index.cjs +2 -0
- package/dist/components/Sidebar/index.d.ts +6 -1
- package/dist/components/Sidebar/index.d.ts.map +1 -1
- package/dist/components/Sidebar/index.js +2 -0
- package/dist/components/Text/Text.module.scss.cjs +63 -51
- package/dist/components/Text/Text.module.scss.js +63 -51
- package/dist/components/Text/index.d.ts +4 -2
- package/dist/components/Text/index.d.ts.map +1 -1
- package/dist/data-table.cjs +210 -28
- package/dist/data-table.js +189 -29
- package/dist/datepicker.cjs +63 -14
- package/dist/datepicker.js +41 -14
- package/dist/editor.cjs +67 -16
- package/dist/editor.js +45 -16
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/markdown.cjs +63 -19
- package/dist/markdown.js +41 -19
- package/fragments.json +1 -1
- package/package.json +2 -2
- package/src/components/Badge/Badge.contract.json +28 -5
- package/src/components/Badge/Badge.module.scss +21 -0
- package/src/components/Badge/index.tsx +10 -1
- package/src/components/Button/Button.contract.json +9 -3
- package/src/components/Button/Button.module.scss +34 -0
- package/src/components/Button/index.tsx +11 -1
- package/src/components/Chart/Chart.contract.json +7 -0
- package/src/components/Chart/index.tsx +44 -27
- package/src/components/CodeBlock/CodeBlock.contract.json +7 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +7 -2
- package/src/components/CodeBlock/index.tsx +38 -6
- package/src/components/ColorPicker/ColorPicker.contract.json +9 -1
- package/src/components/ColorPicker/index.tsx +42 -12
- package/src/components/Combobox/Combobox.module.scss +4 -13
- package/src/components/ConversationList/ConversationList.contract.json +16 -1
- package/src/components/ConversationList/ConversationList.module.scss +12 -0
- package/src/components/ConversationList/index.tsx +40 -26
- package/src/components/DataTable/DataTable.contract.json +8 -1
- package/src/components/DataTable/DataTable.module.scss +5 -0
- package/src/components/DataTable/index.tsx +282 -51
- package/src/components/DatePicker/DatePicker.contract.json +8 -1
- package/src/components/DatePicker/index.tsx +51 -22
- package/src/components/Editor/Editor.contract.json +19 -1
- package/src/components/Editor/index.tsx +67 -23
- package/src/components/Listbox/Listbox.module.scss +3 -11
- package/src/components/Markdown/Markdown.contract.json +15 -1
- package/src/components/Markdown/Markdown.module.scss +18 -1
- package/src/components/Markdown/index.tsx +58 -24
- package/src/components/Menu/Menu.module.scss +12 -2
- package/src/components/Message/Message.contract.json +3 -0
- package/src/components/Message/Message.module.scss +33 -4
- package/src/components/Message/index.tsx +7 -1
- package/src/components/Pagination/Pagination.contract.json +1 -0
- package/src/components/Pagination/Pagination.module.scss +5 -4
- package/src/components/Prompt/Prompt.contract.json +40 -1
- package/src/components/Prompt/Prompt.module.scss +351 -4
- package/src/components/Prompt/index.tsx +324 -0
- package/src/components/Select/Select.contract.json +11 -1
- package/src/components/Select/Select.module.scss +80 -13
- package/src/components/Select/index.tsx +73 -7
- package/src/components/Sidebar/Sidebar.contract.json +1 -0
- package/src/components/Sidebar/Sidebar.module.scss +10 -0
- package/src/components/Sidebar/index.tsx +7 -0
- package/src/components/Text/Text.contract.json +7 -4
- package/src/components/Text/Text.module.scss +18 -0
- package/src/components/Text/index.tsx +4 -2
- package/src/index.ts +1 -0
- package/src/tokens/_mixins.scss +39 -4
- package/src/tokens/_variables.scss +30 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import styles from './Prompt.module.scss';
|
|
5
5
|
import { Loading } from '../Loading';
|
|
6
|
+
import { Select, type SelectOption, type SelectValue } from '../Select';
|
|
6
7
|
|
|
7
8
|
// ============================================
|
|
8
9
|
// Types
|
|
@@ -10,6 +11,8 @@ import { Loading } from '../Loading';
|
|
|
10
11
|
|
|
11
12
|
export type PromptVariant = 'default' | 'fixed' | 'sticky';
|
|
12
13
|
|
|
14
|
+
export type PromptAppearance = 'panel' | 'seamless';
|
|
15
|
+
|
|
13
16
|
export interface PromptProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'onSubmit' | 'defaultValue'> {
|
|
14
17
|
children: React.ReactNode;
|
|
15
18
|
/** Controlled input value */
|
|
@@ -34,8 +37,19 @@ export interface PromptProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
|
|
|
34
37
|
autoResize?: boolean;
|
|
35
38
|
/** Submit on Enter key (Shift+Enter for newline) */
|
|
36
39
|
submitOnEnter?: boolean;
|
|
40
|
+
/** Files added to the prompt, by any of the three routes people actually
|
|
41
|
+
* use: the attach button, a paste, or a drop anywhere on the card. Providing
|
|
42
|
+
* it is what turns all three on. */
|
|
43
|
+
onFiles?: (files: File[]) => void;
|
|
44
|
+
/** `accept` for the attach button's picker, e.g. `"image/*"`. */
|
|
45
|
+
accept?: string;
|
|
37
46
|
/** Visual variant - "fixed" for bottom-fixed elevated prompt */
|
|
38
47
|
variant?: PromptVariant;
|
|
48
|
+
/** How the card is divided up. `panel` keeps the toolbar as a filled footer
|
|
49
|
+
* under a rule; `seamless` makes the whole card one writing surface with the
|
|
50
|
+
* controls floating on it — the shape most agent composers use.
|
|
51
|
+
* @default "panel" */
|
|
52
|
+
appearance?: PromptAppearance;
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
export interface PromptTextareaProps extends Omit<
|
|
@@ -48,6 +62,8 @@ export interface PromptTextareaProps extends Omit<
|
|
|
48
62
|
onChange?: React.ChangeEventHandler<HTMLTextAreaElement>;
|
|
49
63
|
/** Composed with internal submit-on-enter logic */
|
|
50
64
|
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>;
|
|
65
|
+
/** Composed with the prompt's paste-to-attach behaviour */
|
|
66
|
+
onPaste?: React.ClipboardEventHandler<HTMLTextAreaElement>;
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
export interface PromptToolbarProps {
|
|
@@ -101,6 +117,59 @@ export interface PromptModeButtonProps {
|
|
|
101
117
|
className?: string;
|
|
102
118
|
}
|
|
103
119
|
|
|
120
|
+
/** One thing attached to the prompt, as the strip needs to render it. */
|
|
121
|
+
export interface PromptAttachment {
|
|
122
|
+
id: string;
|
|
123
|
+
name: string;
|
|
124
|
+
/** Size in bytes. Shown beside the name when known. */
|
|
125
|
+
size?: number;
|
|
126
|
+
/** Object URL or data URI. An image gets a thumbnail instead of an icon. */
|
|
127
|
+
previewUrl?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface PromptAttachmentsProps {
|
|
131
|
+
items: PromptAttachment[];
|
|
132
|
+
/** Omit to render the strip read-only. */
|
|
133
|
+
onRemove?: (id: string) => void;
|
|
134
|
+
className?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface PromptAttachProps {
|
|
138
|
+
/** Accessible name.
|
|
139
|
+
* @default "Attach files" */
|
|
140
|
+
'aria-label'?: string;
|
|
141
|
+
/** Custom glyph. Defaults to a plus. */
|
|
142
|
+
children?: React.ReactNode;
|
|
143
|
+
/** Overrides the prompt's own `accept` for this control. */
|
|
144
|
+
accept?: string;
|
|
145
|
+
/** @default true */
|
|
146
|
+
multiple?: boolean;
|
|
147
|
+
disabled?: boolean;
|
|
148
|
+
className?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface PromptSelectProps {
|
|
152
|
+
/** Leading glyph — says what the choice is about, so the visible text can be
|
|
153
|
+
* the choice itself. */
|
|
154
|
+
icon?: React.ReactNode;
|
|
155
|
+
/** Accessible name. The trigger shows the current choice, not this. */
|
|
156
|
+
'aria-label': string;
|
|
157
|
+
/** Controlled value */
|
|
158
|
+
value?: SelectValue | null;
|
|
159
|
+
/** Default value for uncontrolled usage */
|
|
160
|
+
defaultValue?: SelectValue;
|
|
161
|
+
/** Called when the choice changes */
|
|
162
|
+
onValueChange?: (value: SelectValue | null) => void;
|
|
163
|
+
/** The choices. Omit and pass `Select.Item` children for richer options. */
|
|
164
|
+
options?: SelectOption[];
|
|
165
|
+
children?: React.ReactNode;
|
|
166
|
+
/** Shown before anything is chosen */
|
|
167
|
+
placeholder?: string;
|
|
168
|
+
/** Disabled independently of the prompt's own disabled state */
|
|
169
|
+
disabled?: boolean;
|
|
170
|
+
className?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
104
173
|
export interface PromptUsageProps {
|
|
105
174
|
children: React.ReactNode;
|
|
106
175
|
className?: string;
|
|
@@ -118,6 +187,36 @@ export interface PromptSubmitProps {
|
|
|
118
187
|
// Icons
|
|
119
188
|
// ============================================
|
|
120
189
|
|
|
190
|
+
function PlusIcon() {
|
|
191
|
+
return (
|
|
192
|
+
<svg
|
|
193
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
194
|
+
width="16"
|
|
195
|
+
height="16"
|
|
196
|
+
viewBox="0 0 256 256"
|
|
197
|
+
fill="currentColor"
|
|
198
|
+
aria-hidden="true"
|
|
199
|
+
>
|
|
200
|
+
<path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z" />
|
|
201
|
+
</svg>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function CloseIcon() {
|
|
206
|
+
return (
|
|
207
|
+
<svg
|
|
208
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
209
|
+
width="16"
|
|
210
|
+
height="16"
|
|
211
|
+
viewBox="0 0 256 256"
|
|
212
|
+
fill="currentColor"
|
|
213
|
+
aria-hidden="true"
|
|
214
|
+
>
|
|
215
|
+
<path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z" />
|
|
216
|
+
</svg>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
121
220
|
function ArrowUpIcon() {
|
|
122
221
|
return (
|
|
123
222
|
<svg
|
|
@@ -149,6 +248,10 @@ interface PromptContextValue {
|
|
|
149
248
|
submitOnEnter: boolean;
|
|
150
249
|
handleSubmit: () => void;
|
|
151
250
|
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
251
|
+
accept?: string;
|
|
252
|
+
/** Undefined when the consumer has not opted into files at all, which is how
|
|
253
|
+
* Attach knows to disable itself and the root knows not to accept drops. */
|
|
254
|
+
onFiles?: (files: File[]) => void;
|
|
152
255
|
}
|
|
153
256
|
|
|
154
257
|
const PromptContext = React.createContext<PromptContextValue | null>(null);
|
|
@@ -205,10 +308,19 @@ function PromptRoot({
|
|
|
205
308
|
autoResize = true,
|
|
206
309
|
submitOnEnter = true,
|
|
207
310
|
variant = 'default',
|
|
311
|
+
appearance = 'panel',
|
|
312
|
+
onFiles,
|
|
313
|
+
accept,
|
|
208
314
|
className,
|
|
209
315
|
...htmlProps
|
|
210
316
|
}: PromptProps) {
|
|
211
317
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
318
|
+
const [dragging, setDragging] = React.useState(false);
|
|
319
|
+
// Drag events fire for every element the pointer crosses inside the card, so
|
|
320
|
+
// a naive enter/leave pair flickers as the cursor moves between children.
|
|
321
|
+
// Counting them means the highlight only drops when the pointer has actually
|
|
322
|
+
// left the composer.
|
|
323
|
+
const dragDepth = React.useRef(0);
|
|
212
324
|
|
|
213
325
|
const [value, setValue] = useControllableState(
|
|
214
326
|
controlledValue,
|
|
@@ -221,6 +333,14 @@ function PromptRoot({
|
|
|
221
333
|
onSubmit?.(value);
|
|
222
334
|
}, [disabled, loading, value, onSubmit]);
|
|
223
335
|
|
|
336
|
+
const acceptFiles = React.useCallback(
|
|
337
|
+
(files: File[]) => {
|
|
338
|
+
if (!onFiles || disabled || loading || files.length === 0) return;
|
|
339
|
+
onFiles(files);
|
|
340
|
+
},
|
|
341
|
+
[onFiles, disabled, loading]
|
|
342
|
+
);
|
|
343
|
+
|
|
224
344
|
const contextValue: PromptContextValue = {
|
|
225
345
|
value,
|
|
226
346
|
setValue,
|
|
@@ -233,12 +353,43 @@ function PromptRoot({
|
|
|
233
353
|
submitOnEnter,
|
|
234
354
|
handleSubmit,
|
|
235
355
|
textareaRef,
|
|
356
|
+
accept,
|
|
357
|
+
onFiles: onFiles ? acceptFiles : undefined,
|
|
236
358
|
};
|
|
237
359
|
|
|
360
|
+
const dropHandlers = onFiles
|
|
361
|
+
? {
|
|
362
|
+
onDragEnter: (event: React.DragEvent<HTMLDivElement>) => {
|
|
363
|
+
if (!event.dataTransfer.types.includes('Files')) return;
|
|
364
|
+
dragDepth.current += 1;
|
|
365
|
+
setDragging(true);
|
|
366
|
+
},
|
|
367
|
+
onDragOver: (event: React.DragEvent<HTMLDivElement>) => {
|
|
368
|
+
if (!event.dataTransfer.types.includes('Files')) return;
|
|
369
|
+
// Without this the browser navigates to the dropped file.
|
|
370
|
+
event.preventDefault();
|
|
371
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
372
|
+
},
|
|
373
|
+
onDragLeave: () => {
|
|
374
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
375
|
+
if (dragDepth.current === 0) setDragging(false);
|
|
376
|
+
},
|
|
377
|
+
onDrop: (event: React.DragEvent<HTMLDivElement>) => {
|
|
378
|
+
if (!event.dataTransfer.types.includes('Files')) return;
|
|
379
|
+
event.preventDefault();
|
|
380
|
+
dragDepth.current = 0;
|
|
381
|
+
setDragging(false);
|
|
382
|
+
acceptFiles(Array.from(event.dataTransfer.files));
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
: null;
|
|
386
|
+
|
|
238
387
|
const classes = [
|
|
239
388
|
styles.prompt,
|
|
240
389
|
variant === 'fixed' && styles.fixed,
|
|
241
390
|
variant === 'sticky' && styles.sticky,
|
|
391
|
+
appearance === 'seamless' && styles.seamless,
|
|
392
|
+
dragging && styles.dragging,
|
|
242
393
|
disabled && styles.disabled,
|
|
243
394
|
loading && styles.loading,
|
|
244
395
|
className,
|
|
@@ -248,10 +399,13 @@ function PromptRoot({
|
|
|
248
399
|
<PromptContext.Provider value={contextValue}>
|
|
249
400
|
<div
|
|
250
401
|
{...htmlProps}
|
|
402
|
+
{...dropHandlers}
|
|
251
403
|
className={classes}
|
|
252
404
|
data-disabled={disabled || undefined}
|
|
253
405
|
data-loading={loading || undefined}
|
|
254
406
|
data-variant={variant}
|
|
407
|
+
data-appearance={appearance}
|
|
408
|
+
data-dragging={dragging || undefined}
|
|
255
409
|
>
|
|
256
410
|
{children}
|
|
257
411
|
</div>
|
|
@@ -264,6 +418,7 @@ function PromptTextarea({
|
|
|
264
418
|
className,
|
|
265
419
|
onChange,
|
|
266
420
|
onKeyDown,
|
|
421
|
+
onPaste,
|
|
267
422
|
'aria-label': ariaLabel,
|
|
268
423
|
...htmlProps
|
|
269
424
|
}: PromptTextareaProps) {
|
|
@@ -279,6 +434,7 @@ function PromptTextarea({
|
|
|
279
434
|
submitOnEnter,
|
|
280
435
|
handleSubmit,
|
|
281
436
|
textareaRef,
|
|
437
|
+
onFiles,
|
|
282
438
|
} = usePromptContext();
|
|
283
439
|
|
|
284
440
|
const lineHeight = 1.5;
|
|
@@ -321,6 +477,26 @@ function PromptTextarea({
|
|
|
321
477
|
}
|
|
322
478
|
};
|
|
323
479
|
|
|
480
|
+
// Pasting a screenshot is how people actually attach one. The clipboard
|
|
481
|
+
// carries it as a file with no name, so it gets one here — a bare "image.png"
|
|
482
|
+
// in the strip is worse than useless when there are two of them.
|
|
483
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
|
484
|
+
onPaste?.(e);
|
|
485
|
+
if (e.defaultPrevented || !onFiles) return;
|
|
486
|
+
const files = Array.from(e.clipboardData.files);
|
|
487
|
+
if (files.length === 0) return;
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
onFiles(
|
|
490
|
+
files.map((file, index) =>
|
|
491
|
+
file.name && file.name !== 'image.png'
|
|
492
|
+
? file
|
|
493
|
+
: new File([file], `pasted-${index + 1}.${file.type.split('/')[1] || 'png'}`, {
|
|
494
|
+
type: file.type,
|
|
495
|
+
})
|
|
496
|
+
)
|
|
497
|
+
);
|
|
498
|
+
};
|
|
499
|
+
|
|
324
500
|
const classes = [styles.textarea, className].filter(Boolean).join(' ');
|
|
325
501
|
|
|
326
502
|
return (
|
|
@@ -331,6 +507,7 @@ function PromptTextarea({
|
|
|
331
507
|
value={value}
|
|
332
508
|
onChange={handleChange}
|
|
333
509
|
onKeyDown={handleKeyDown}
|
|
510
|
+
onPaste={handlePaste}
|
|
334
511
|
placeholder={overridePlaceholder ?? placeholder}
|
|
335
512
|
disabled={disabled || loading}
|
|
336
513
|
rows={minRows}
|
|
@@ -431,6 +608,147 @@ function PromptModeButton({
|
|
|
431
608
|
);
|
|
432
609
|
}
|
|
433
610
|
|
|
611
|
+
/** Bytes as something a person reads at a glance, not as a precise count. */
|
|
612
|
+
function formatSize(bytes: number): string {
|
|
613
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
614
|
+
const kb = bytes / 1024;
|
|
615
|
+
if (kb < 1024) return `${Math.round(kb)} KB`;
|
|
616
|
+
return `${(kb / 1024).toFixed(kb / 1024 < 10 ? 1 : 0)} MB`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* What is going with the message. Renders above the textarea rather than under
|
|
621
|
+
* the toolbar, because these are part of what you are sending and not a
|
|
622
|
+
* setting on the sending.
|
|
623
|
+
*/
|
|
624
|
+
function PromptAttachments({ items, onRemove, className }: PromptAttachmentsProps) {
|
|
625
|
+
if (items.length === 0) return null;
|
|
626
|
+
const classes = [styles.attachments, className].filter(Boolean).join(' ');
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<ul className={classes} aria-label="Attachments">
|
|
630
|
+
{items.map((item) => (
|
|
631
|
+
<li key={item.id} className={styles.attachment}>
|
|
632
|
+
{item.previewUrl ? (
|
|
633
|
+
// Decorative: the filename beside it is the accessible name, and a
|
|
634
|
+
// thumbnail of a screenshot has no description worth inventing.
|
|
635
|
+
<img src={item.previewUrl} alt="" className={styles.attachmentThumb} />
|
|
636
|
+
) : null}
|
|
637
|
+
<span className={styles.attachmentName} title={item.name}>
|
|
638
|
+
{item.name}
|
|
639
|
+
</span>
|
|
640
|
+
{item.size != null && (
|
|
641
|
+
<span className={styles.attachmentSize}>{formatSize(item.size)}</span>
|
|
642
|
+
)}
|
|
643
|
+
{onRemove && (
|
|
644
|
+
<button
|
|
645
|
+
type="button"
|
|
646
|
+
className={styles.attachmentRemove}
|
|
647
|
+
onClick={() => onRemove(item.id)}
|
|
648
|
+
aria-label={`Remove ${item.name}`}
|
|
649
|
+
>
|
|
650
|
+
<CloseIcon />
|
|
651
|
+
</button>
|
|
652
|
+
)}
|
|
653
|
+
</li>
|
|
654
|
+
))}
|
|
655
|
+
</ul>
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* The attach control. Does nothing on its own — it opens a picker and hands
|
|
661
|
+
* the result to the prompt's `onFiles`, the same callback that paste and drop
|
|
662
|
+
* go through, so a consumer writes one handler for all three.
|
|
663
|
+
*/
|
|
664
|
+
function PromptAttach({
|
|
665
|
+
'aria-label': ariaLabel = 'Attach files',
|
|
666
|
+
children,
|
|
667
|
+
accept: acceptOverride,
|
|
668
|
+
multiple = true,
|
|
669
|
+
disabled: buttonDisabled,
|
|
670
|
+
className,
|
|
671
|
+
}: PromptAttachProps) {
|
|
672
|
+
const { disabled, loading, accept, onFiles } = usePromptContext();
|
|
673
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
674
|
+
const isDisabled = disabled || loading || buttonDisabled || !onFiles;
|
|
675
|
+
|
|
676
|
+
const classes = [styles.attach, className].filter(Boolean).join(' ');
|
|
677
|
+
|
|
678
|
+
return (
|
|
679
|
+
<>
|
|
680
|
+
<button
|
|
681
|
+
type="button"
|
|
682
|
+
className={classes}
|
|
683
|
+
onClick={() => inputRef.current?.click()}
|
|
684
|
+
disabled={isDisabled}
|
|
685
|
+
aria-label={ariaLabel}
|
|
686
|
+
>
|
|
687
|
+
{children ?? <PlusIcon />}
|
|
688
|
+
</button>
|
|
689
|
+
{/* `hidden` rather than a class: the button above is the control, and
|
|
690
|
+
this must be out of the accessibility tree entirely or it reads as a
|
|
691
|
+
second, unlabelled one. A hidden input still opens its picker when
|
|
692
|
+
clicked programmatically. */}
|
|
693
|
+
<input
|
|
694
|
+
ref={inputRef}
|
|
695
|
+
type="file"
|
|
696
|
+
hidden
|
|
697
|
+
accept={acceptOverride ?? accept}
|
|
698
|
+
multiple={multiple}
|
|
699
|
+
onChange={(event) => {
|
|
700
|
+
const files = Array.from(event.target.files ?? []);
|
|
701
|
+
// Reset first, or picking the same file twice in a row is a no-op.
|
|
702
|
+
event.target.value = '';
|
|
703
|
+
onFiles?.(files);
|
|
704
|
+
}}
|
|
705
|
+
/>
|
|
706
|
+
</>
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* A choice that scopes the submission — model, agent, tone, which project this
|
|
712
|
+
* runs against. `ModeButton` covers a setting you toggle; this covers one you
|
|
713
|
+
* pick from a list, which every non-trivial composer ends up needing.
|
|
714
|
+
*
|
|
715
|
+
* It is a `Select` throughout, in its ghost variant, so the popup, keyboard
|
|
716
|
+
* handling and selection state are the same ones the rest of the library uses.
|
|
717
|
+
* The trigger shows the current choice rather than a field label, because in a
|
|
718
|
+
* toolbar the choice is the only part worth the width — what kind of choice it
|
|
719
|
+
* is comes from the icon and the accessible name.
|
|
720
|
+
*/
|
|
721
|
+
function PromptSelect({
|
|
722
|
+
icon,
|
|
723
|
+
'aria-label': ariaLabel,
|
|
724
|
+
options,
|
|
725
|
+
children,
|
|
726
|
+
placeholder,
|
|
727
|
+
disabled: selectDisabled,
|
|
728
|
+
className,
|
|
729
|
+
...selectProps
|
|
730
|
+
}: PromptSelectProps) {
|
|
731
|
+
const { disabled, loading } = usePromptContext();
|
|
732
|
+
const classes = [styles.select, className].filter(Boolean).join(' ');
|
|
733
|
+
|
|
734
|
+
return (
|
|
735
|
+
<Select
|
|
736
|
+
{...selectProps}
|
|
737
|
+
size="sm"
|
|
738
|
+
variant="ghost"
|
|
739
|
+
options={options}
|
|
740
|
+
placeholder={placeholder}
|
|
741
|
+
disabled={disabled || loading || selectDisabled}
|
|
742
|
+
>
|
|
743
|
+
<Select.Trigger className={classes} icon={icon} aria-label={ariaLabel} />
|
|
744
|
+
{/* No children: Content renders what `options` describes, which is how a
|
|
745
|
+
hint or anything else the root knows about an option survives being
|
|
746
|
+
composed into a custom trigger. */}
|
|
747
|
+
<Select.Content>{children}</Select.Content>
|
|
748
|
+
</Select>
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
434
752
|
function PromptUsage({ children, className }: PromptUsageProps) {
|
|
435
753
|
const classes = [styles.usage, className].filter(Boolean).join(' ');
|
|
436
754
|
return <span className={classes}>{children}</span>;
|
|
@@ -486,6 +804,9 @@ export const Prompt = Object.assign(PromptRoot, {
|
|
|
486
804
|
Info: PromptInfo,
|
|
487
805
|
ActionButton: PromptActionButton,
|
|
488
806
|
ModeButton: PromptModeButton,
|
|
807
|
+
Select: PromptSelect,
|
|
808
|
+
Attach: PromptAttach,
|
|
809
|
+
Attachments: PromptAttachments,
|
|
489
810
|
Usage: PromptUsage,
|
|
490
811
|
Submit: PromptSubmit,
|
|
491
812
|
});
|
|
@@ -500,6 +821,9 @@ export {
|
|
|
500
821
|
PromptInfo,
|
|
501
822
|
PromptActionButton,
|
|
502
823
|
PromptModeButton,
|
|
824
|
+
PromptSelect,
|
|
825
|
+
PromptAttach,
|
|
826
|
+
PromptAttachments,
|
|
503
827
|
PromptUsage,
|
|
504
828
|
PromptSubmit,
|
|
505
829
|
};
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"placeholder: string",
|
|
22
22
|
"options: array",
|
|
23
23
|
"size: sm|md|lg (default: md)",
|
|
24
|
+
"variant: field|ghost (default: field)",
|
|
24
25
|
"className: string",
|
|
25
26
|
"label: string",
|
|
26
27
|
"helperText: string",
|
|
@@ -96,6 +97,13 @@
|
|
|
96
97
|
"required": false,
|
|
97
98
|
"values": ["sm", "md", "lg"]
|
|
98
99
|
},
|
|
100
|
+
"variant": {
|
|
101
|
+
"type": "enum",
|
|
102
|
+
"description": "Visual treatment. \"ghost\" drops the field shell for a compact borderless control, for toolbars and dense rows. Pair with size=\"sm\".",
|
|
103
|
+
"default": "field",
|
|
104
|
+
"required": false,
|
|
105
|
+
"values": ["field", "ghost"]
|
|
106
|
+
},
|
|
99
107
|
"className": {
|
|
100
108
|
"type": "string",
|
|
101
109
|
"description": "Wrapper class name",
|
|
@@ -225,8 +233,10 @@
|
|
|
225
233
|
"placeholder: string - placeholder text",
|
|
226
234
|
"disabled: boolean - disable select",
|
|
227
235
|
"size: sm|md|lg (default: md)",
|
|
236
|
+
"variant: field|ghost (default: field) - ghost is borderless, for toolbars",
|
|
228
237
|
"options: SelectOption[] - convenience API for simple option lists",
|
|
229
|
-
"maxVisibleItems: number - max visible options before scrolling (default 4)"
|
|
238
|
+
"maxVisibleItems: number - max visible options before scrolling (default 4)",
|
|
239
|
+
"Select.Trigger accepts an icon prop for a leading adornment"
|
|
230
240
|
],
|
|
231
241
|
"a11yRules": ["A11Y_SELECT_KEYBOARD", "A11Y_SELECT_LABEL"]
|
|
232
242
|
},
|
|
@@ -34,6 +34,74 @@
|
|
|
34
34
|
@include control-size("lg", "input");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Ghost trigger — the same control with the field shell taken off, for
|
|
38
|
+
// toolbars and dense rows where a bordered field would read as a form rather
|
|
39
|
+
// than as a setting. Deliberately identical in padding, radius, type scale and
|
|
40
|
+
// hover to the other compact controls that share those rows, so a select sits
|
|
41
|
+
// among them as a sibling instead of a fourth vocabulary.
|
|
42
|
+
//
|
|
43
|
+
// Must be declared after `.trigger` and the size variants: it unsets what they
|
|
44
|
+
// set, and they are all single-class selectors.
|
|
45
|
+
.triggerGhost {
|
|
46
|
+
width: auto;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
// No block padding: the control is exactly the target size tall and centres
|
|
49
|
+
// its own contents, the way a button does. Padding here instead would add to
|
|
50
|
+
// the line box and leave a ghost taller than every other control in the row
|
|
51
|
+
// — and taller again wherever the surface has scaled the target down.
|
|
52
|
+
height: auto;
|
|
53
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
54
|
+
padding: 0 var(--fui-space-2, $fui-space-2);
|
|
55
|
+
border: none;
|
|
56
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
57
|
+
background-color: transparent;
|
|
58
|
+
box-shadow: none;
|
|
59
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
60
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
61
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
62
|
+
|
|
63
|
+
&:hover:not(:disabled):not([data-disabled]) {
|
|
64
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
65
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&:active:not(:disabled):not([data-disabled]) {
|
|
69
|
+
background-color: var(--fui-bg-active, $fui-bg-active);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Open is a held state, not a hover: the control stays lit while the popup
|
|
73
|
+
// it owns is on screen.
|
|
74
|
+
&[data-popup-open] {
|
|
75
|
+
border-color: transparent;
|
|
76
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
77
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// No shell to fill, so disabled reads as recession rather than as a
|
|
81
|
+
// greyed-out box.
|
|
82
|
+
&:disabled,
|
|
83
|
+
&[data-disabled] {
|
|
84
|
+
background-color: transparent;
|
|
85
|
+
opacity: 0.5;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Ghost sizes to its content and may shrink; the wrapper has to allow both or
|
|
90
|
+
// the trigger measures against its label and escapes a tight row.
|
|
91
|
+
.wrapperGhost {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
min-width: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Leading adornment on the trigger
|
|
97
|
+
.triggerIcon {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-shrink: 0;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
103
|
+
}
|
|
104
|
+
|
|
37
105
|
// Value display
|
|
38
106
|
.value {
|
|
39
107
|
flex: 1;
|
|
@@ -54,6 +122,14 @@
|
|
|
54
122
|
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
55
123
|
}
|
|
56
124
|
|
|
125
|
+
// The qualifier on an option — recessed, because it tells you which one this is
|
|
126
|
+
// rather than being the thing you are choosing.
|
|
127
|
+
.hint {
|
|
128
|
+
margin-inline-start: var(--fui-space-1, $fui-space-1);
|
|
129
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
130
|
+
font-weight: var(--fui-font-weight-normal, $fui-font-weight-normal);
|
|
131
|
+
}
|
|
132
|
+
|
|
57
133
|
// Chevron icon
|
|
58
134
|
.icon {
|
|
59
135
|
display: flex;
|
|
@@ -83,22 +159,13 @@
|
|
|
83
159
|
@include surface-elevated;
|
|
84
160
|
@include popup-container;
|
|
85
161
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Item height derived from text-base font + vertical padding
|
|
89
|
-
--_select-item-h: calc(
|
|
90
|
-
var(--fui-font-size-sm, #{$fui-font-size-sm}) *
|
|
91
|
-
var(--fui-line-height-normal, #{$fui-line-height-normal}) +
|
|
92
|
-
var(--fui-space-2, #{$fui-space-2}) * 2
|
|
93
|
-
);
|
|
162
|
+
@include popup-scroll-viewport;
|
|
94
163
|
|
|
164
|
+
border-color: var(--fui-form-group-border, $fui-border);
|
|
95
165
|
min-width: var(--anchor-width);
|
|
96
|
-
// Show N items + half-peek scroll hint (default 4.5 items)
|
|
166
|
+
// Show N items + half-peek scroll hint (default 4.5 items).
|
|
97
167
|
// !important needed to override Base UI's inline max-height: 100%
|
|
98
|
-
max-height:
|
|
99
|
-
var(--_select-item-h) * var(--fui-select-max-items, 4.5) + var(--fui-space-1, #{$fui-space-1}) *
|
|
100
|
-
2
|
|
101
|
-
) !important;
|
|
168
|
+
max-height: var(--_popup-scroll-max-h) !important;
|
|
102
169
|
overflow-y: auto !important;
|
|
103
170
|
|
|
104
171
|
// Animation
|