@adres/mgda-ui 0.0.0-stage → 0.0.2
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/brand-knowledge.json +451 -0
- package/knowledge/accordion.md +135 -0
- package/knowledge/activity-rings.md +123 -0
- package/knowledge/alert.md +184 -0
- package/knowledge/answer-actions.md +217 -0
- package/knowledge/aspect-ratio.md +82 -0
- package/knowledge/avatar.md +158 -0
- package/knowledge/badge.md +172 -0
- package/knowledge/bar-graph.md +167 -0
- package/knowledge/bento-grid.md +115 -0
- package/knowledge/blockquote.md +153 -0
- package/knowledge/box-list.md +177 -0
- package/knowledge/bullet-list.md +128 -0
- package/knowledge/button-group.md +179 -0
- package/knowledge/button.md +231 -0
- package/knowledge/calendar.md +168 -0
- package/knowledge/card.md +321 -0
- package/knowledge/cards-nav.md +170 -0
- package/knowledge/chain-of-thought.md +164 -0
- package/knowledge/chat-container.md +135 -0
- package/knowledge/chat-message.md +224 -0
- package/knowledge/checkbox-card.md +172 -0
- package/knowledge/checkbox.md +111 -0
- package/knowledge/citations.md +241 -0
- package/knowledge/combobox.md +265 -0
- package/knowledge/countdown-pill.md +153 -0
- package/knowledge/cta.md +118 -0
- package/knowledge/data-list.md +173 -0
- package/knowledge/data-table.md +220 -0
- package/knowledge/dialog.md +249 -0
- package/knowledge/drawer.md +236 -0
- package/knowledge/dropdown-menu.md +238 -0
- package/knowledge/dropzone.md +153 -0
- package/knowledge/email-chips-input.md +161 -0
- package/knowledge/empty-state.md +106 -0
- package/knowledge/entity-header.md +166 -0
- package/knowledge/feature-grid.md +120 -0
- package/knowledge/feedback-bar.md +127 -0
- package/knowledge/footer.md +116 -0
- package/knowledge/heading.md +189 -0
- package/knowledge/hero.md +361 -0
- package/knowledge/input-button.md +187 -0
- package/knowledge/input-group.md +185 -0
- package/knowledge/input.md +256 -0
- package/knowledge/inset.md +177 -0
- package/knowledge/kpi-card.md +206 -0
- package/knowledge/list-nav.md +181 -0
- package/knowledge/logo-cloud.md +102 -0
- package/knowledge/media-gallery.md +158 -0
- package/knowledge/message-actions.md +175 -0
- package/knowledge/nav-sidebar.md +225 -0
- package/knowledge/number-stepper.md +159 -0
- package/knowledge/page-header.md +109 -0
- package/knowledge/pagination.md +225 -0
- package/knowledge/popover.md +159 -0
- package/knowledge/pricing.md +132 -0
- package/knowledge/progress.md +156 -0
- package/knowledge/prompt-input.md +337 -0
- package/knowledge/prompt-suggestions.md +168 -0
- package/knowledge/quota-bar.md +160 -0
- package/knowledge/quote.md +116 -0
- package/knowledge/radio-card.md +172 -0
- package/knowledge/radio-group.md +135 -0
- package/knowledge/reasoning.md +142 -0
- package/knowledge/search.md +131 -0
- package/knowledge/section-nav.md +162 -0
- package/knowledge/segmented-control.md +181 -0
- package/knowledge/select-button.md +205 -0
- package/knowledge/select.md +210 -0
- package/knowledge/separator.md +75 -0
- package/knowledge/shimmer-text.md +140 -0
- package/knowledge/sidebar.md +227 -0
- package/knowledge/skeleton.md +142 -0
- package/knowledge/slider.md +133 -0
- package/knowledge/slideshow.md +159 -0
- package/knowledge/source.md +156 -0
- package/knowledge/spinner.md +120 -0
- package/knowledge/stat-strip.md +128 -0
- package/knowledge/stats.md +117 -0
- package/knowledge/step-indicator.md +157 -0
- package/knowledge/switch.md +88 -0
- package/knowledge/table.md +190 -0
- package/knowledge/tabs.md +169 -0
- package/knowledge/testimonials.md +115 -0
- package/knowledge/text-link.md +192 -0
- package/knowledge/text.md +194 -0
- package/knowledge/thinking-bar.md +115 -0
- package/knowledge/thinking-steps.md +140 -0
- package/knowledge/toaster.md +127 -0
- package/knowledge/toggle.md +113 -0
- package/knowledge/tool-call-card.md +241 -0
- package/knowledge/tooltip.md +120 -0
- package/knowledge/top-nav.md +138 -0
- package/knowledge.json +19854 -0
- package/package.json +94 -4
- package/src/components/accordion.stories.tsx +46 -0
- package/src/components/accordion.tsx +74 -0
- package/src/components/activity-rings.stories.tsx +73 -0
- package/src/components/activity-rings.tsx +121 -0
- package/src/components/alert.stories.tsx +43 -0
- package/src/components/alert.tsx +132 -0
- package/src/components/answer-actions.stories.tsx +54 -0
- package/src/components/answer-actions.test.tsx +37 -0
- package/src/components/answer-actions.tsx +313 -0
- package/src/components/aspect-ratio.stories.tsx +32 -0
- package/src/components/aspect-ratio.tsx +5 -0
- package/src/components/avatar.stories.tsx +40 -0
- package/src/components/avatar.tsx +154 -0
- package/src/components/badge.stories.tsx +27 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/bar-graph.stories.tsx +28 -0
- package/src/components/bar-graph.tsx +204 -0
- package/src/components/bento-grid.stories.tsx +23 -0
- package/src/components/bento-grid.tsx +67 -0
- package/src/components/blockquote.stories.tsx +28 -0
- package/src/components/blockquote.tsx +88 -0
- package/src/components/box-list.stories.tsx +25 -0
- package/src/components/box-list.tsx +108 -0
- package/src/components/bullet-list.stories.tsx +22 -0
- package/src/components/bullet-list.tsx +101 -0
- package/src/components/button-group.stories.tsx +31 -0
- package/src/components/button-group.tsx +156 -0
- package/src/components/button.stories.tsx +66 -0
- package/src/components/button.test.ts +17 -0
- package/src/components/button.tsx +139 -0
- package/src/components/calendar.stories.tsx +25 -0
- package/src/components/calendar.tsx +367 -0
- package/src/components/card.stories.tsx +44 -0
- package/src/components/card.tsx +392 -0
- package/src/components/cards-nav.stories.tsx +35 -0
- package/src/components/cards-nav.tsx +115 -0
- package/src/components/chain-of-thought.stories.tsx +67 -0
- package/src/components/chain-of-thought.test.tsx +50 -0
- package/src/components/chain-of-thought.tsx +170 -0
- package/src/components/chat-container.stories.tsx +47 -0
- package/src/components/chat-container.test.tsx +49 -0
- package/src/components/chat-container.tsx +222 -0
- package/src/components/chat-message.stories.tsx +52 -0
- package/src/components/chat-message.test.tsx +36 -0
- package/src/components/chat-message.tsx +163 -0
- package/src/components/checkbox-card.stories.tsx +27 -0
- package/src/components/checkbox-card.tsx +200 -0
- package/src/components/checkbox.stories.tsx +26 -0
- package/src/components/checkbox.tsx +61 -0
- package/src/components/citations.stories.tsx +64 -0
- package/src/components/citations.test.tsx +61 -0
- package/src/components/citations.tsx +220 -0
- package/src/components/combobox.stories.tsx +184 -0
- package/src/components/combobox.test.tsx +82 -0
- package/src/components/combobox.tsx +407 -0
- package/src/components/countdown-pill.stories.tsx +24 -0
- package/src/components/countdown-pill.tsx +129 -0
- package/src/components/cta.stories.tsx +29 -0
- package/src/components/cta.tsx +52 -0
- package/src/components/data-list.stories.tsx +50 -0
- package/src/components/data-list.test.tsx +35 -0
- package/src/components/data-list.tsx +181 -0
- package/src/components/data-table.stories.tsx +29 -0
- package/src/components/data-table.tsx +200 -0
- package/src/components/dialog.stories.tsx +42 -0
- package/src/components/dialog.tsx +200 -0
- package/src/components/drawer.stories.tsx +48 -0
- package/src/components/drawer.tsx +385 -0
- package/src/components/dropdown-menu.stories.tsx +41 -0
- package/src/components/dropdown-menu.tsx +252 -0
- package/src/components/dropzone.stories.tsx +19 -0
- package/src/components/dropzone.tsx +250 -0
- package/src/components/email-chips-input.stories.tsx +135 -0
- package/src/components/email-chips-input.tsx +210 -0
- package/src/components/empty-state.stories.tsx +22 -0
- package/src/components/empty-state.tsx +35 -0
- package/src/components/entity-header.stories.tsx +43 -0
- package/src/components/entity-header.test.tsx +35 -0
- package/src/components/entity-header.tsx +92 -0
- package/src/components/feature-grid.stories.tsx +25 -0
- package/src/components/feature-grid.tsx +87 -0
- package/src/components/feedback-bar.stories.tsx +25 -0
- package/src/components/feedback-bar.test.tsx +24 -0
- package/src/components/feedback-bar.tsx +62 -0
- package/src/components/footer.stories.tsx +23 -0
- package/src/components/footer.tsx +78 -0
- package/src/components/heading.stories.tsx +45 -0
- package/src/components/heading.test.tsx +22 -0
- package/src/components/heading.tsx +77 -0
- package/src/components/hero.stories.tsx +38 -0
- package/src/components/hero.tsx +498 -0
- package/src/components/input-button.stories.tsx +19 -0
- package/src/components/input-button.tsx +112 -0
- package/src/components/input-group.stories.tsx +38 -0
- package/src/components/input-group.tsx +157 -0
- package/src/components/input.stories.tsx +31 -0
- package/src/components/input.tsx +284 -0
- package/src/components/inset.stories.tsx +29 -0
- package/src/components/inset.tsx +70 -0
- package/src/components/kpi-card.stories.tsx +29 -0
- package/src/components/kpi-card.tsx +284 -0
- package/src/components/list-nav.stories.tsx +22 -0
- package/src/components/list-nav.tsx +145 -0
- package/src/components/logo-cloud.stories.tsx +23 -0
- package/src/components/logo-cloud.tsx +42 -0
- package/src/components/media-gallery.stories.tsx +25 -0
- package/src/components/media-gallery.tsx +347 -0
- package/src/components/message-actions.stories.tsx +45 -0
- package/src/components/message-actions.test.tsx +32 -0
- package/src/components/message-actions.tsx +119 -0
- package/src/components/nav-sidebar.stories.tsx +58 -0
- package/src/components/nav-sidebar.test.tsx +54 -0
- package/src/components/nav-sidebar.tsx +462 -0
- package/src/components/number-stepper.stories.tsx +19 -0
- package/src/components/number-stepper.tsx +158 -0
- package/src/components/page-header.stories.tsx +25 -0
- package/src/components/page-header.tsx +64 -0
- package/src/components/pagination.stories.tsx +110 -0
- package/src/components/pagination.test.tsx +46 -0
- package/src/components/pagination.tsx +144 -0
- package/src/components/popover.stories.tsx +26 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/pricing.stories.tsx +42 -0
- package/src/components/pricing.tsx +130 -0
- package/src/components/progress.stories.tsx +22 -0
- package/src/components/progress.tsx +189 -0
- package/src/components/prompt-input.stories.tsx +62 -0
- package/src/components/prompt-input.test.tsx +106 -0
- package/src/components/prompt-input.tsx +343 -0
- package/src/components/prompt-suggestions.stories.tsx +53 -0
- package/src/components/prompt-suggestions.test.tsx +37 -0
- package/src/components/prompt-suggestions.tsx +122 -0
- package/src/components/quota-bar.stories.tsx +19 -0
- package/src/components/quota-bar.tsx +191 -0
- package/src/components/quote.stories.tsx +18 -0
- package/src/components/quote.tsx +43 -0
- package/src/components/radio-card.stories.tsx +27 -0
- package/src/components/radio-card.tsx +225 -0
- package/src/components/radio-group.stories.tsx +32 -0
- package/src/components/radio-group.tsx +68 -0
- package/src/components/reasoning.stories.tsx +50 -0
- package/src/components/reasoning.test.tsx +48 -0
- package/src/components/reasoning.tsx +143 -0
- package/src/components/search.stories.tsx +18 -0
- package/src/components/search.tsx +133 -0
- package/src/components/section-nav.stories.tsx +59 -0
- package/src/components/section-nav.test.tsx +36 -0
- package/src/components/section-nav.tsx +146 -0
- package/src/components/segmented-control.stories.tsx +22 -0
- package/src/components/segmented-control.tsx +301 -0
- package/src/components/select-button.stories.tsx +27 -0
- package/src/components/select-button.tsx +174 -0
- package/src/components/select.stories.tsx +27 -0
- package/src/components/select.tsx +198 -0
- package/src/components/separator.stories.tsx +22 -0
- package/src/components/separator.tsx +23 -0
- package/src/components/shimmer-text.stories.tsx +23 -0
- package/src/components/shimmer-text.test.tsx +35 -0
- package/src/components/shimmer-text.tsx +43 -0
- package/src/components/sidebar.stories.tsx +25 -0
- package/src/components/sidebar.tsx +234 -0
- package/src/components/skeleton.stories.tsx +22 -0
- package/src/components/skeleton.tsx +96 -0
- package/src/components/slider.stories.tsx +20 -0
- package/src/components/slider.tsx +176 -0
- package/src/components/slideshow.stories.tsx +30 -0
- package/src/components/slideshow.tsx +291 -0
- package/src/components/source.stories.tsx +32 -0
- package/src/components/source.test.tsx +40 -0
- package/src/components/source.tsx +161 -0
- package/src/components/spinner.stories.tsx +19 -0
- package/src/components/spinner.tsx +76 -0
- package/src/components/stat-strip.stories.tsx +36 -0
- package/src/components/stat-strip.test.tsx +25 -0
- package/src/components/stat-strip.tsx +85 -0
- package/src/components/stats.stories.tsx +24 -0
- package/src/components/stats.tsx +83 -0
- package/src/components/step-indicator.stories.tsx +18 -0
- package/src/components/step-indicator.test.tsx +18 -0
- package/src/components/step-indicator.tsx +286 -0
- package/src/components/switch.stories.tsx +26 -0
- package/src/components/switch.tsx +32 -0
- package/src/components/table.stories.tsx +38 -0
- package/src/components/table.tsx +220 -0
- package/src/components/tabs.stories.tsx +25 -0
- package/src/components/tabs.tsx +276 -0
- package/src/components/testimonials.stories.tsx +22 -0
- package/src/components/testimonials.tsx +109 -0
- package/src/components/text-link.stories.tsx +47 -0
- package/src/components/text-link.test.tsx +18 -0
- package/src/components/text-link.tsx +88 -0
- package/src/components/text.stories.tsx +49 -0
- package/src/components/text.tsx +105 -0
- package/src/components/thinking-bar.stories.tsx +19 -0
- package/src/components/thinking-bar.test.tsx +27 -0
- package/src/components/thinking-bar.tsx +64 -0
- package/src/components/thinking-steps.stories.tsx +44 -0
- package/src/components/thinking-steps.test.tsx +42 -0
- package/src/components/thinking-steps.tsx +92 -0
- package/src/components/toaster.stories.tsx +80 -0
- package/src/components/toaster.tsx +36 -0
- package/src/components/toggle.stories.tsx +21 -0
- package/src/components/toggle.tsx +103 -0
- package/src/components/tool-call-card.stories.tsx +97 -0
- package/src/components/tool-call-card.test.tsx +88 -0
- package/src/components/tool-call-card.tsx +231 -0
- package/src/components/tooltip.stories.tsx +25 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/components/top-nav.stories.tsx +20 -0
- package/src/components/top-nav.tsx +76 -0
- package/src/docs/Colors.stories.tsx +192 -0
- package/src/docs/GettingStarted.mdx +72 -0
- package/src/docs/Mcp.mdx +52 -0
- package/src/docs/Typography.stories.tsx +114 -0
- package/src/hooks/use-in-view.ts +32 -0
- package/src/hooks/use-reduced-motion.ts +26 -0
- package/src/index.ts +343 -0
- package/src/lib/cn.test.ts +67 -0
- package/src/lib/cn.ts +81 -0
- package/src/lib/motion.ts +109 -0
- package/src/lib/prefix-utilities.test.ts +56 -0
- package/src/lib/prefix-utilities.ts +50 -0
- package/src/styles/base.css +489 -0
- package/src/styles/motion.css +69 -0
- package/src/styles/overrides.css +44 -0
- package/src/styles/typography.css +173 -0
- package/src/theme/theme.css +391 -0
- package/src/theme/tokens.ts +29 -0
- package/src/theme/types.ts +26 -0
- package/README.md +0 -3
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ArrowUp, FileText, Image as ImageIcon, Upload, X } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
import { Button, type ButtonProps } from "./button"
|
|
7
|
+
import { Textarea } from "./input"
|
|
8
|
+
import { Progress } from "./progress"
|
|
9
|
+
import { Text, type TextColor } from "./text"
|
|
10
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
|
|
11
|
+
|
|
12
|
+
export type PromptInputSize = "sm" | "md"
|
|
13
|
+
|
|
14
|
+
const SIZES = {
|
|
15
|
+
md: {
|
|
16
|
+
root: "ax:rounded-xl",
|
|
17
|
+
field: "ax:min-h-11 ax:px-md ax:pt-sm",
|
|
18
|
+
maxHeight: 8 * 24,
|
|
19
|
+
row: "ax:mt-lg ax:px-sm ax:pb-sm",
|
|
20
|
+
edge: "ax:px-sm ax:pt-xs",
|
|
21
|
+
hint: "ax:px-md ax:pb-sm",
|
|
22
|
+
button: "ax:size-9 ax:[&_svg]:size-4.5",
|
|
23
|
+
},
|
|
24
|
+
// For narrow chat columns and side panels.
|
|
25
|
+
sm: {
|
|
26
|
+
root: "ax:rounded-lg",
|
|
27
|
+
field: "ax:min-h-9 ax:px-sm ax:pt-xs",
|
|
28
|
+
maxHeight: 6 * 20,
|
|
29
|
+
row: "ax:mt-xs ax:px-xs ax:pb-xs",
|
|
30
|
+
edge: "ax:px-xs ax:pt-xs",
|
|
31
|
+
hint: "ax:px-sm ax:pb-xs",
|
|
32
|
+
button: "ax:size-8 ax:[&_svg]:size-4",
|
|
33
|
+
},
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
const PromptInputSizeContext = React.createContext<PromptInputSize>("md")
|
|
37
|
+
|
|
38
|
+
export type CharacterCountTone = Extract<TextColor, "muted" | "warning" | "destructive">
|
|
39
|
+
|
|
40
|
+
/** Hidden below 80% of the limit, then muted, warning from 90%, destructive once over. */
|
|
41
|
+
export function characterCountTone(length: number, max: number): CharacterCountTone | null {
|
|
42
|
+
if (length < max * 0.8) return null
|
|
43
|
+
if (length > max) return "destructive"
|
|
44
|
+
return length >= max * 0.9 ? "warning" : "muted"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatFileSize(bytes: number) {
|
|
48
|
+
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when a drag carries files, so text or link drags pass through untouched. */
|
|
52
|
+
export function isFileDrag(types: readonly string[]) {
|
|
53
|
+
return types.includes("Files")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PromptInputActionProps extends Omit<ButtonProps, "size" | "asChild"> {
|
|
57
|
+
/** Accessible name, and the tooltip unless `tooltip` is set. */
|
|
58
|
+
label: string
|
|
59
|
+
/** Longer tooltip text, e.g. accepted file types. */
|
|
60
|
+
tooltip?: React.ReactNode
|
|
61
|
+
/** Renders as a toggle: sets `aria-pressed`. */
|
|
62
|
+
pressed?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A round icon button with a tooltip for the composer's action row (attach, voice). Sized by its PromptInput. */
|
|
66
|
+
export const PromptInputAction = React.forwardRef<HTMLButtonElement, PromptInputActionProps>(
|
|
67
|
+
({ label, tooltip, pressed, variant = "outline", className, ...props }, ref) => {
|
|
68
|
+
const size = React.useContext(PromptInputSizeContext)
|
|
69
|
+
return (
|
|
70
|
+
<Tooltip>
|
|
71
|
+
<TooltipTrigger asChild>
|
|
72
|
+
<Button
|
|
73
|
+
ref={ref}
|
|
74
|
+
type="button"
|
|
75
|
+
variant={variant}
|
|
76
|
+
size="icon"
|
|
77
|
+
aria-label={label}
|
|
78
|
+
aria-pressed={pressed}
|
|
79
|
+
className={cn("ax:rounded-full", SIZES[size].button, className)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
</TooltipTrigger>
|
|
83
|
+
<TooltipContent>{tooltip ?? label}</TooltipContent>
|
|
84
|
+
</Tooltip>
|
|
85
|
+
)
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
PromptInputAction.displayName = "PromptInputAction"
|
|
89
|
+
|
|
90
|
+
export interface PromptInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSubmit" | "defaultValue"> {
|
|
91
|
+
value?: string
|
|
92
|
+
defaultValue?: string
|
|
93
|
+
onValueChange?: (value: string) => void
|
|
94
|
+
/** Called with the trimmed text on Enter or Send. The field then clears and keeps focus. */
|
|
95
|
+
onSubmit: (text: string) => void
|
|
96
|
+
/** True while the assistant answers; Send becomes Stop. */
|
|
97
|
+
busy?: boolean
|
|
98
|
+
onStop?: () => void
|
|
99
|
+
disabled?: boolean
|
|
100
|
+
/** Shown under the field when `disabled`, e.g. why the assistant is unavailable. */
|
|
101
|
+
disabledReason?: string
|
|
102
|
+
placeholder?: string
|
|
103
|
+
/** Accessible name of the field. */
|
|
104
|
+
label?: string
|
|
105
|
+
/** Characters allowed; the counter appears from 80% and sending is blocked when over. */
|
|
106
|
+
maxLength?: number
|
|
107
|
+
/** Lets an empty field send, e.g. when files are attached. */
|
|
108
|
+
hasAttachments?: boolean
|
|
109
|
+
/** Extra gate on sending, e.g. false while uploads are in progress. */
|
|
110
|
+
canSubmit?: boolean
|
|
111
|
+
/** Muted note under the field when nothing more urgent applies, e.g. "Waiting for uploads to finish." */
|
|
112
|
+
hint?: React.ReactNode
|
|
113
|
+
/** Above the field: usually a list of `AttachmentChip`s. */
|
|
114
|
+
attachments?: React.ReactNode
|
|
115
|
+
/** Start of the action row: attach, tools. Use `PromptInputAction` for icon buttons. */
|
|
116
|
+
toolbar?: React.ReactNode
|
|
117
|
+
/** End of the action row, before Send: voice input, status badges. */
|
|
118
|
+
endActions?: React.ReactNode
|
|
119
|
+
/** Receives files pasted into the field; without it, pasting files does nothing special. */
|
|
120
|
+
onFilesPaste?: (files: File[]) => void
|
|
121
|
+
/** Receives files dropped on the composer, which shows a drop overlay while dragging. Ignored while `disabled`. */
|
|
122
|
+
onFilesDrop?: (files: File[]) => void
|
|
123
|
+
/** Extra line in the drop overlay, e.g. accepted types and size limit. */
|
|
124
|
+
dropHint?: React.ReactNode
|
|
125
|
+
/** `sm` is a tighter composer for narrow chat columns and side panels. */
|
|
126
|
+
size?: PromptInputSize
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* PromptInput - the composer for an AI feature: an auto-growing field (up to
|
|
131
|
+
* about 8 lines), Enter to send and Shift+Enter for a new line, Send or Stop,
|
|
132
|
+
* a character limit, and slots for attachments, tools and voice.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* <PromptInput onSubmit={send} busy={replying} onStop={stop} toolbar={<AttachButton />} />
|
|
136
|
+
*/
|
|
137
|
+
export const PromptInput = React.forwardRef<HTMLDivElement, PromptInputProps>(
|
|
138
|
+
(
|
|
139
|
+
{
|
|
140
|
+
value: valueProp, defaultValue = "", onValueChange, onSubmit, busy = false, onStop, disabled = false,
|
|
141
|
+
disabledReason, placeholder = "Ask anything", label = "Prompt", maxLength = 2000, hasAttachments = false,
|
|
142
|
+
canSubmit = true, hint: extraHint, attachments, toolbar, endActions, onFilesPaste, onFilesDrop, dropHint, size = "md", className, ...props
|
|
143
|
+
},
|
|
144
|
+
ref,
|
|
145
|
+
) => {
|
|
146
|
+
const id = React.useId()
|
|
147
|
+
const [uncontrolled, setUncontrolled] = React.useState(defaultValue)
|
|
148
|
+
const text = valueProp ?? uncontrolled
|
|
149
|
+
const fieldRef = React.useRef<HTMLTextAreaElement>(null)
|
|
150
|
+
const [dragging, setDragging] = React.useState(false)
|
|
151
|
+
const sizes = SIZES[size]
|
|
152
|
+
const droppable = Boolean(onFilesDrop) && !disabled
|
|
153
|
+
|
|
154
|
+
function setText(next: string) {
|
|
155
|
+
if (valueProp === undefined) setUncontrolled(next)
|
|
156
|
+
onValueChange?.(next)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
React.useLayoutEffect(() => {
|
|
160
|
+
const field = fieldRef.current
|
|
161
|
+
if (!field) return
|
|
162
|
+
field.style.height = "auto"
|
|
163
|
+
field.style.height = `${Math.min(field.scrollHeight, sizes.maxHeight)}px`
|
|
164
|
+
}, [text, sizes.maxHeight])
|
|
165
|
+
|
|
166
|
+
const tooLong = text.length > maxLength
|
|
167
|
+
const hasContent = text.trim().length > 0 || hasAttachments
|
|
168
|
+
const canSend = hasContent && !tooLong && canSubmit && !disabled && !busy
|
|
169
|
+
const tone = characterCountTone(text.length, maxLength)
|
|
170
|
+
|
|
171
|
+
function submit() {
|
|
172
|
+
if (!canSend) return
|
|
173
|
+
onSubmit(text.trim())
|
|
174
|
+
setText("")
|
|
175
|
+
fieldRef.current?.focus()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const hint = disabled
|
|
179
|
+
? disabledReason
|
|
180
|
+
: tooLong
|
|
181
|
+
? `Shorten your message by ${(text.length - maxLength).toLocaleString("en-US")} characters to send it.`
|
|
182
|
+
: extraHint
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<PromptInputSizeContext.Provider value={size}>
|
|
186
|
+
<div
|
|
187
|
+
ref={ref}
|
|
188
|
+
data-slot="prompt-input"
|
|
189
|
+
data-size={size}
|
|
190
|
+
className={cn(
|
|
191
|
+
sizes.root,
|
|
192
|
+
"ax:relative ax:flex ax:cursor-text ax:flex-col ax:border ax:border-input ax:bg-popover ax:pt-1 ax:shadow-xs ax:transition-[border-color,box-shadow]",
|
|
193
|
+
"ax:focus-within:border-ring ax:focus-within:ring-2 ax:focus-within:ring-ring/20",
|
|
194
|
+
tooLong && "ax:border-destructive ax:ring-2 ax:ring-destructive/20 ax:focus-within:border-destructive ax:focus-within:ring-destructive/20",
|
|
195
|
+
disabled && "ax:cursor-not-allowed ax:opacity-60",
|
|
196
|
+
className,
|
|
197
|
+
)}
|
|
198
|
+
onClick={(event) => {
|
|
199
|
+
if (!disabled && event.target === event.currentTarget) fieldRef.current?.focus()
|
|
200
|
+
}}
|
|
201
|
+
onDragOver={droppable ? (event) => {
|
|
202
|
+
if (!isFileDrag(event.dataTransfer.types)) return
|
|
203
|
+
event.preventDefault()
|
|
204
|
+
setDragging(true)
|
|
205
|
+
} : undefined}
|
|
206
|
+
onDragLeave={droppable ? (event) => {
|
|
207
|
+
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setDragging(false)
|
|
208
|
+
} : undefined}
|
|
209
|
+
onDrop={droppable ? (event) => {
|
|
210
|
+
if (!isFileDrag(event.dataTransfer.types)) return
|
|
211
|
+
event.preventDefault()
|
|
212
|
+
setDragging(false)
|
|
213
|
+
onFilesDrop?.([...event.dataTransfer.files])
|
|
214
|
+
} : undefined}
|
|
215
|
+
{...props}
|
|
216
|
+
>
|
|
217
|
+
{attachments && <div className={sizes.edge}>{attachments}</div>}
|
|
218
|
+
|
|
219
|
+
<Textarea
|
|
220
|
+
ref={fieldRef}
|
|
221
|
+
value={text}
|
|
222
|
+
disabled={disabled}
|
|
223
|
+
onChange={(event) => setText(event.target.value)}
|
|
224
|
+
onPaste={(event) => {
|
|
225
|
+
const files = [...event.clipboardData.files]
|
|
226
|
+
if (files.length && onFilesPaste) {
|
|
227
|
+
event.preventDefault()
|
|
228
|
+
onFilesPaste(files)
|
|
229
|
+
}
|
|
230
|
+
}}
|
|
231
|
+
onKeyDown={(event) => {
|
|
232
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
233
|
+
event.preventDefault()
|
|
234
|
+
submit()
|
|
235
|
+
}
|
|
236
|
+
}}
|
|
237
|
+
rows={1}
|
|
238
|
+
resize="none"
|
|
239
|
+
placeholder={placeholder}
|
|
240
|
+
aria-label={label}
|
|
241
|
+
aria-invalid={tooLong || undefined}
|
|
242
|
+
aria-describedby={hint ? `${id}-hint` : undefined}
|
|
243
|
+
// The container owns the border, focus ring and invalid ring; the field stays bare inside it.
|
|
244
|
+
className={cn(sizes.field, "ax:border-0 ax:bg-transparent ax:text-sm ax:leading-snug ax:shadow-none ax:focus-visible:ring-0 ax:focus-visible:ring-offset-0 ax:aria-[invalid=true]:ring-0 ax:disabled:bg-transparent")}
|
|
245
|
+
/>
|
|
246
|
+
|
|
247
|
+
<div className={cn("ax:flex ax:items-center ax:justify-between ax:gap-xs", sizes.row)}>
|
|
248
|
+
<div className="ax:flex ax:items-center ax:gap-xs">{toolbar}</div>
|
|
249
|
+
<div className="ax:flex ax:items-center ax:gap-xs">
|
|
250
|
+
{tone && (
|
|
251
|
+
<Text size="1" color={tone} weight="medium" className="ax:tabular-nums" aria-live="polite">
|
|
252
|
+
{text.length.toLocaleString("en-US")} / {maxLength.toLocaleString("en-US")}
|
|
253
|
+
</Text>
|
|
254
|
+
)}
|
|
255
|
+
{endActions}
|
|
256
|
+
{busy ? (
|
|
257
|
+
<Button size="icon" aria-label="Stop generating" onClick={onStop} className={cn("ax:rounded-full", sizes.button)}>
|
|
258
|
+
<span className="ax:size-3 ax:rounded-xs ax:bg-current" aria-hidden />
|
|
259
|
+
</Button>
|
|
260
|
+
) : (
|
|
261
|
+
<Button size="icon" aria-label="Send message" disabled={!canSend} onClick={submit} className={cn("ax:rounded-full", sizes.button)}>
|
|
262
|
+
<ArrowUp />
|
|
263
|
+
</Button>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{hint && (
|
|
269
|
+
<Text as="p" id={`${id}-hint`} size="1" color={tooLong ? "destructive" : "muted"} className={sizes.hint}>
|
|
270
|
+
{hint}
|
|
271
|
+
</Text>
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
{droppable && dragging && (
|
|
275
|
+
<div className="ax:pointer-events-none ax:absolute ax:inset-0 ax:flex ax:flex-col ax:items-center ax:justify-center ax:gap-xs ax:rounded-[inherit] ax:border-2 ax:border-dashed ax:border-primary ax:bg-background/90">
|
|
276
|
+
<Upload className="ax:size-5 ax:text-primary-foreground" aria-hidden />
|
|
277
|
+
<Text size="2" weight="medium">Drop files to attach</Text>
|
|
278
|
+
{dropHint && <Text size="1" color="muted">{dropHint}</Text>}
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</PromptInputSizeContext.Provider>
|
|
283
|
+
)
|
|
284
|
+
},
|
|
285
|
+
)
|
|
286
|
+
PromptInput.displayName = "PromptInput"
|
|
287
|
+
|
|
288
|
+
export interface AttachmentChipProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
289
|
+
name: string
|
|
290
|
+
/** Bytes; shown once the upload finishes. */
|
|
291
|
+
size: number
|
|
292
|
+
kind?: "image" | "file"
|
|
293
|
+
/** Thumbnail for images. */
|
|
294
|
+
previewUrl?: string
|
|
295
|
+
/** 0-100 while uploading; omit or 100 when done. */
|
|
296
|
+
progress?: number
|
|
297
|
+
/** Replaces size and progress, and tints the chip. */
|
|
298
|
+
error?: string
|
|
299
|
+
onRemove: () => void
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** One file attached to a prompt: thumbnail or icon, name, then upload progress, size or the error. */
|
|
303
|
+
export const AttachmentChip = React.forwardRef<HTMLDivElement, AttachmentChipProps>(
|
|
304
|
+
({ name, size, kind = "file", previewUrl, progress = 100, error, onRemove, className, ...props }, ref) => {
|
|
305
|
+
const uploading = !error && progress < 100
|
|
306
|
+
return (
|
|
307
|
+
<div
|
|
308
|
+
ref={ref}
|
|
309
|
+
data-slot="attachment-chip"
|
|
310
|
+
className={cn(
|
|
311
|
+
"ax:flex ax:w-64 ax:items-center ax:gap-sm ax:rounded-md ax:border ax:border-border ax:bg-card ax:p-xs",
|
|
312
|
+
error && "ax:border-destructive-weaker ax:bg-destructive-weakest",
|
|
313
|
+
className,
|
|
314
|
+
)}
|
|
315
|
+
{...props}
|
|
316
|
+
>
|
|
317
|
+
<div className="ax:flex ax:size-10 ax:shrink-0 ax:items-center ax:justify-center ax:overflow-hidden ax:rounded ax:bg-muted ax:text-muted-foreground">
|
|
318
|
+
{previewUrl ? (
|
|
319
|
+
<img src={previewUrl} alt="" className="ax:size-full ax:object-cover" />
|
|
320
|
+
) : kind === "image" ? (
|
|
321
|
+
<ImageIcon className="ax:size-4" aria-hidden />
|
|
322
|
+
) : (
|
|
323
|
+
<FileText className="ax:size-4" aria-hidden />
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
<div className="ax:flex ax:min-w-0 ax:flex-1 ax:flex-col ax:gap-1">
|
|
327
|
+
<Text size="2" weight="medium" truncate>{name}</Text>
|
|
328
|
+
{error ? (
|
|
329
|
+
<Text size="1" color="destructive" truncate>{error}</Text>
|
|
330
|
+
) : uploading ? (
|
|
331
|
+
<Progress value={progress} size="sm" aria-label={`Uploading ${name}`} />
|
|
332
|
+
) : (
|
|
333
|
+
<Text size="1" color="muted">{formatFileSize(size)}</Text>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
<Button variant="ghost" size="icon-sm" aria-label={`Remove ${name}`} onClick={onRemove} className="ax:shrink-0 ax:rounded-full">
|
|
337
|
+
<X />
|
|
338
|
+
</Button>
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
},
|
|
342
|
+
)
|
|
343
|
+
AttachmentChip.displayName = "AttachmentChip"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Building2, Calculator, Home, MapPin } from "lucide-react"
|
|
4
|
+
import { StarterPrompts, SuggestionChips } from "./prompt-suggestions"
|
|
5
|
+
|
|
6
|
+
const FOLLOW_UPS = [
|
|
7
|
+
"Compare with Saadiyat Lagoons",
|
|
8
|
+
"Show only 4-bed townhouses",
|
|
9
|
+
"What are the service charges?",
|
|
10
|
+
"Book a viewing this weekend",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof SuggestionChips> = {
|
|
14
|
+
title: "Components/SuggestionChips",
|
|
15
|
+
component: SuggestionChips,
|
|
16
|
+
tags: ["autodocs"],
|
|
17
|
+
argTypes: { onPick: { control: false }, onRefresh: { control: false } },
|
|
18
|
+
args: { suggestions: FOLLOW_UPS, onPick: () => undefined },
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof SuggestionChips>
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {}
|
|
25
|
+
|
|
26
|
+
export const Wrapped: Story = { args: { wrap: true } }
|
|
27
|
+
|
|
28
|
+
export const Loading: Story = { args: { loading: true, onRefresh: () => undefined } }
|
|
29
|
+
|
|
30
|
+
export const InsertTemplates: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
insert: true,
|
|
33
|
+
wrap: true,
|
|
34
|
+
label: "Prompt templates",
|
|
35
|
+
suggestions: ["Find [bedrooms]-bed homes in [community]", "What's the price per sq ft in [community]?"],
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const Starters: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<div style={{ maxWidth: 640 }}>
|
|
42
|
+
<StarterPrompts
|
|
43
|
+
onPick={() => undefined}
|
|
44
|
+
starters={[
|
|
45
|
+
{ icon: Home, title: "Find a home", prompt: "Show me 3-bed villas in Yas Island under AED 6M" },
|
|
46
|
+
{ icon: Calculator, title: "Check affordability", prompt: "What can I afford with AED 40,000 a month?" },
|
|
47
|
+
{ icon: MapPin, title: "Compare communities", prompt: "Compare Saadiyat and Al Reem for families" },
|
|
48
|
+
{ icon: Building2, title: "Track a project", prompt: "When is Saadiyat Lagoons phase 2 handing over?" },
|
|
49
|
+
]}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { StarterPrompts, SuggestionChips } from "./prompt-suggestions"
|
|
5
|
+
|
|
6
|
+
const pick = () => undefined
|
|
7
|
+
|
|
8
|
+
describe("SuggestionChips", () => {
|
|
9
|
+
it("renders one button per prompt inside a labelled group", () => {
|
|
10
|
+
const html = renderToStaticMarkup(<SuggestionChips suggestions={["One", "Two"]} onPick={pick} label="Follow-ups" />)
|
|
11
|
+
expect(html).toMatch(/role="group" aria-label="Follow-ups" aria-busy="false"/)
|
|
12
|
+
expect(html.match(/<button/g)).toHaveLength(2)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("swaps chips for skeletons and marks the group busy while loading", () => {
|
|
16
|
+
const html = renderToStaticMarkup(<SuggestionChips suggestions={["One", "Two"]} onPick={pick} loading />)
|
|
17
|
+
expect(html).toContain('aria-busy="true"')
|
|
18
|
+
expect(html).not.toContain("<button")
|
|
19
|
+
expect(html).toContain("Loading suggestions")
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("labels insert-mode chips as edits", () => {
|
|
23
|
+
const html = renderToStaticMarkup(<SuggestionChips suggestions={["Find homes"]} onPick={pick} insert />)
|
|
24
|
+
expect(html).toContain('aria-label="Edit prompt: Find homes"')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe("StarterPrompts", () => {
|
|
29
|
+
it("shows each starter's title and full prompt", () => {
|
|
30
|
+
const html = renderToStaticMarkup(
|
|
31
|
+
<StarterPrompts starters={[{ title: "Find a home", prompt: "Show me villas" }]} onPick={pick} />,
|
|
32
|
+
)
|
|
33
|
+
expect(html).toContain('aria-label="Ways to start"')
|
|
34
|
+
expect(html).toContain("Find a home")
|
|
35
|
+
expect(html).toContain("Show me villas")
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { PenLine, RefreshCw } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
import { Button } from "./button"
|
|
7
|
+
import { Skeleton } from "./skeleton"
|
|
8
|
+
import { Text } from "./text"
|
|
9
|
+
|
|
10
|
+
export interface SuggestionChipsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
11
|
+
suggestions: string[]
|
|
12
|
+
onPick: (prompt: string) => void
|
|
13
|
+
/** Accessible name of the chip group. */
|
|
14
|
+
label?: string
|
|
15
|
+
/** Swaps each chip for a skeleton and marks the group busy. */
|
|
16
|
+
loading?: boolean
|
|
17
|
+
/** E.g. while the assistant replies. */
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
/** Chips put the prompt in the composer for editing instead of sending it. */
|
|
20
|
+
insert?: boolean
|
|
21
|
+
/** Adds an "Other suggestions" button under the row. */
|
|
22
|
+
onRefresh?: () => void
|
|
23
|
+
/** Wrap onto several lines instead of one horizontally scrolling row. */
|
|
24
|
+
wrap?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SuggestionChips - a row of prompt chips. Each chip is a button labelled with
|
|
29
|
+
* the full prompt it sends.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* <SuggestionChips suggestions={followUps} onPick={send} onRefresh={nextRound} />
|
|
33
|
+
*/
|
|
34
|
+
export const SuggestionChips = React.forwardRef<HTMLDivElement, SuggestionChipsProps>(
|
|
35
|
+
({ suggestions, onPick, label = "Suggested prompts", loading = false, disabled = false, insert = false, onRefresh, wrap = false, className, ...props }, ref) => (
|
|
36
|
+
<div ref={ref} data-slot="suggestion-chips" className={cn("ax:flex ax:flex-col ax:gap-xs", className)} {...props}>
|
|
37
|
+
<div
|
|
38
|
+
role="group"
|
|
39
|
+
aria-label={label}
|
|
40
|
+
aria-busy={loading}
|
|
41
|
+
className={cn("ax:flex ax:gap-xs", wrap ? "ax:flex-wrap" : "ax:overflow-x-auto ax:pb-1")}
|
|
42
|
+
>
|
|
43
|
+
{loading
|
|
44
|
+
? suggestions.map((prompt) => <Skeleton key={prompt} className="ax:h-3xl ax:w-44 ax:shrink-0 ax:rounded-full" />)
|
|
45
|
+
: suggestions.map((prompt) => (
|
|
46
|
+
<Button
|
|
47
|
+
key={prompt}
|
|
48
|
+
variant="outline"
|
|
49
|
+
size="sm"
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
onClick={() => onPick(prompt)}
|
|
52
|
+
aria-label={insert ? `Edit prompt: ${prompt}` : undefined}
|
|
53
|
+
className="ax:shrink-0 ax:rounded-full ax:font-normal"
|
|
54
|
+
>
|
|
55
|
+
{insert && <PenLine />}
|
|
56
|
+
{prompt}
|
|
57
|
+
</Button>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
{/* Outside the scrolling row, so it never ends up hidden past the last chip. */}
|
|
61
|
+
{onRefresh && (
|
|
62
|
+
<Button variant="ghost" size="sm" disabled={disabled || loading} onClick={onRefresh} className="ax:self-start ax:rounded-full">
|
|
63
|
+
<RefreshCw />
|
|
64
|
+
Other suggestions
|
|
65
|
+
</Button>
|
|
66
|
+
)}
|
|
67
|
+
<span className="ax:sr-only" aria-live="polite">{loading ? "Loading suggestions" : ""}</span>
|
|
68
|
+
</div>
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
SuggestionChips.displayName = "SuggestionChips"
|
|
72
|
+
|
|
73
|
+
export interface StarterPromptItem {
|
|
74
|
+
/** Decorative; the title names the starter. */
|
|
75
|
+
icon?: React.ElementType
|
|
76
|
+
/** Short, scannable name. */
|
|
77
|
+
title: string
|
|
78
|
+
/** The full prompt sent when picked. */
|
|
79
|
+
prompt: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface StarterPromptsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
83
|
+
starters: StarterPromptItem[]
|
|
84
|
+
onPick: (prompt: string) => void
|
|
85
|
+
/** Accessible name of the group. */
|
|
86
|
+
label?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* StarterPrompts - empty-state starters in a two-column grid: a short title
|
|
91
|
+
* for scanning, the full prompt underneath.
|
|
92
|
+
*/
|
|
93
|
+
export const StarterPrompts = React.forwardRef<HTMLDivElement, StarterPromptsProps>(
|
|
94
|
+
({ starters, onPick, label = "Ways to start", className, ...props }, ref) => (
|
|
95
|
+
<div
|
|
96
|
+
ref={ref}
|
|
97
|
+
data-slot="starter-prompts"
|
|
98
|
+
role="group"
|
|
99
|
+
aria-label={label}
|
|
100
|
+
className={cn("ax:grid ax:gap-sm ax:sm:grid-cols-2", className)}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
{starters.map(({ icon: Icon, title, prompt }) => (
|
|
104
|
+
<Button
|
|
105
|
+
key={title}
|
|
106
|
+
variant="outline"
|
|
107
|
+
onClick={() => onPick(prompt)}
|
|
108
|
+
// Icon in its own column so the prompt lines up under the title, not the icon.
|
|
109
|
+
className={cn(
|
|
110
|
+
"ax:grid ax:h-auto ax:items-center ax:justify-start ax:gap-x-xs ax:gap-y-1 ax:whitespace-normal ax:rounded-md ax:p-sm ax:text-start ax:shadow-none ax:hover:bg-muted",
|
|
111
|
+
Icon ? "ax:grid-cols-[auto_minmax(0,1fr)]" : "ax:grid-cols-1",
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
{Icon && <Icon className="ax:text-primary-foreground" aria-hidden />}
|
|
115
|
+
<Text size="2" weight="medium">{title}</Text>
|
|
116
|
+
<Text size="1" color="muted" wrap="pretty" className={cn("ax:font-normal", Icon && "ax:col-start-2")}>{prompt}</Text>
|
|
117
|
+
</Button>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
StarterPrompts.displayName = "StarterPrompts"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { QuotaBar } from "./quota-bar"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof QuotaBar> = {
|
|
6
|
+
title: "Components/QuotaBar",
|
|
7
|
+
component: QuotaBar,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
used: 72,
|
|
11
|
+
total: 100,
|
|
12
|
+
unit: "GB",
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default meta
|
|
17
|
+
type Story = StoryObj<typeof QuotaBar>
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {}
|