@gunjo/ui 0.0.1-alpha.1 → 0.0.1-alpha.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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +90 -0
  3. package/README.md +52 -91
  4. package/package.json +47 -6
  5. package/src/components/display/Accordion.tsx +185 -0
  6. package/src/components/display/AccordionGroup.tsx +155 -0
  7. package/src/components/display/ActionDataTable.tsx +413 -0
  8. package/src/components/display/ActivityTimelineCard.tsx +483 -0
  9. package/src/components/display/AnalyticsCard.tsx +167 -0
  10. package/src/components/display/AssetCard.tsx +242 -0
  11. package/src/components/display/AssetGrid.tsx +164 -0
  12. package/src/components/display/Avatar.tsx +127 -0
  13. package/src/components/display/AvatarGroup.tsx +131 -0
  14. package/src/components/{atoms → display}/Badge.tsx +3 -3
  15. package/src/components/display/BarChart.tsx +247 -0
  16. package/src/components/{molecules → display}/Card.tsx +1 -1
  17. package/src/components/display/Carousel.tsx +593 -0
  18. package/src/components/display/ChartLegend.tsx +124 -0
  19. package/src/components/display/ChatMessage.tsx +382 -0
  20. package/src/components/display/ChoroplethMap.tsx +613 -0
  21. package/src/components/display/Code.tsx +42 -0
  22. package/src/components/display/CodeBlock.tsx +338 -0
  23. package/src/components/display/ColorSwatch.tsx +71 -0
  24. package/src/components/display/ConcentricProgressCard.tsx +545 -0
  25. package/src/components/display/DataTable.tsx +522 -0
  26. package/src/components/display/DistributionBar.tsx +102 -0
  27. package/src/components/display/DocNote.tsx +36 -0
  28. package/src/components/display/DonutChart.tsx +257 -0
  29. package/src/components/display/EmptyState.tsx +44 -0
  30. package/src/components/display/FileTree.tsx +180 -0
  31. package/src/components/display/GaugeChart.tsx +219 -0
  32. package/src/components/display/HeatmapChart.tsx +266 -0
  33. package/src/components/display/Icon.tsx +66 -0
  34. package/src/components/display/ImagePreview.tsx +140 -0
  35. package/src/components/{atoms → display}/Img.tsx +46 -12
  36. package/src/components/display/LabeledDonutCard.tsx +475 -0
  37. package/src/components/display/LineChart.tsx +464 -0
  38. package/src/components/{molecules → display}/List.tsx +20 -13
  39. package/src/components/display/MarkdownRenderer.tsx +157 -0
  40. package/src/components/display/MetadataList.tsx +81 -0
  41. package/src/components/display/MiniDistributionBarCard.tsx +314 -0
  42. package/src/components/display/PieChart.tsx +234 -0
  43. package/src/components/display/QuadrantMatrix.tsx +330 -0
  44. package/src/components/display/RadarChart.tsx +335 -0
  45. package/src/components/display/RadialBarChart.tsx +264 -0
  46. package/src/components/display/RetentionCohortCard.tsx +350 -0
  47. package/src/components/display/RibbonChart.tsx +618 -0
  48. package/src/components/display/SearchableAccordion.tsx +270 -0
  49. package/src/components/display/SegmentTimelineCard.tsx +452 -0
  50. package/src/components/display/SegmentedGaugeCard.tsx +607 -0
  51. package/src/components/display/Spacer.tsx +51 -0
  52. package/src/components/display/SparklineChart.tsx +394 -0
  53. package/src/components/display/StackedBarChart.tsx +393 -0
  54. package/src/components/display/Statistic.tsx +70 -0
  55. package/src/components/{molecules → display}/Table.tsx +22 -7
  56. package/src/components/display/Tag.tsx +80 -0
  57. package/src/components/display/TagEditor.tsx +141 -0
  58. package/src/components/display/Timeline.tsx +121 -0
  59. package/src/components/{atoms → display}/ToolPill.tsx +42 -18
  60. package/src/components/display/TreeView.tsx +226 -0
  61. package/src/components/display/chart-tooltip.tsx +423 -0
  62. package/src/components/display/chart-utils.ts +71 -0
  63. package/src/components/display/circular-chart-utils.ts +147 -0
  64. package/src/components/display/generated/default-variant-keys.ts +90 -0
  65. package/src/components/display/generated/variant-keys.ts +169 -0
  66. package/src/components/{atoms → feedback}/Alert.tsx +12 -5
  67. package/src/components/feedback/Banner.tsx +90 -0
  68. package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
  69. package/src/components/feedback/ProgressWidget.tsx +44 -0
  70. package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
  71. package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
  72. package/src/components/feedback/StatusScreen.tsx +148 -0
  73. package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
  74. package/src/components/feedback/Toast.tsx +108 -0
  75. package/src/components/feedback/ToastProvider.tsx +78 -0
  76. package/src/components/feedback/generated/default-variant-keys.ts +16 -0
  77. package/src/components/feedback/generated/variant-keys.ts +21 -0
  78. package/src/components/generated/component-manifest.ts +1568 -454
  79. package/src/components/generated/component-style-hints.ts +1958 -718
  80. package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
  81. package/src/components/inputs/Calendar.tsx +212 -0
  82. package/src/components/inputs/ChatComposer.tsx +75 -0
  83. package/src/components/inputs/ChatInput.tsx +528 -0
  84. package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
  85. package/src/components/inputs/Combobox.tsx +175 -0
  86. package/src/components/inputs/CopyButton.tsx +187 -0
  87. package/src/components/inputs/DatePicker.tsx +519 -0
  88. package/src/components/inputs/DateRangePicker.tsx +878 -0
  89. package/src/components/inputs/EditableField.tsx +182 -0
  90. package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
  91. package/src/components/inputs/FilterButton.tsx +163 -0
  92. package/src/components/{molecules → inputs}/Form.tsx +20 -3
  93. package/src/components/{atoms → inputs}/Input.tsx +2 -0
  94. package/src/components/inputs/InputOTP.tsx +75 -0
  95. package/src/components/inputs/Mention.tsx +279 -0
  96. package/src/components/inputs/NumberInput.tsx +109 -0
  97. package/src/components/inputs/PasswordGroup.tsx +138 -0
  98. package/src/components/inputs/PasswordInput.tsx +74 -0
  99. package/src/components/inputs/PasswordRequirementList.tsx +96 -0
  100. package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
  101. package/src/components/inputs/PhoneInput.tsx +99 -0
  102. package/src/components/inputs/PostalCodeInput.tsx +98 -0
  103. package/src/components/inputs/RangeSlider.tsx +129 -0
  104. package/src/components/inputs/SearchInput.tsx +76 -0
  105. package/src/components/inputs/Select.tsx +39 -0
  106. package/src/components/{atoms → inputs}/Slider.tsx +18 -5
  107. package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
  108. package/src/components/{atoms → inputs}/Switch.tsx +15 -4
  109. package/src/components/inputs/TagInput.tsx +114 -0
  110. package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
  111. package/src/components/inputs/TimePicker.tsx +150 -0
  112. package/src/components/inputs/Toggle.tsx +48 -0
  113. package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
  114. package/src/components/inputs/TooltipButton.tsx +148 -0
  115. package/src/components/inputs/VoiceInputButton.tsx +317 -0
  116. package/src/components/inputs/calendar-holidays.ts +56 -0
  117. package/src/components/inputs/generated/default-variant-keys.ts +32 -0
  118. package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
  119. package/src/components/layout/AspectRatio.tsx +12 -0
  120. package/src/components/layout/AssetInspectorPanel.tsx +416 -0
  121. package/src/components/layout/Cluster.tsx +56 -0
  122. package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
  123. package/src/components/layout/Container.tsx +43 -0
  124. package/src/components/layout/DeviceFrame.tsx +227 -0
  125. package/src/components/layout/Grid.tsx +65 -0
  126. package/src/components/layout/HStack.tsx +73 -0
  127. package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
  128. package/src/components/layout/MarqueeFrame.tsx +158 -0
  129. package/src/components/layout/Resizable.tsx +94 -0
  130. package/src/components/layout/ScrollArea.tsx +71 -0
  131. package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
  132. package/src/components/layout/VStack.tsx +69 -0
  133. package/src/components/layout/generated/default-variant-keys.ts +16 -0
  134. package/src/components/layout/generated/variant-keys.ts +21 -0
  135. package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
  136. package/src/components/navigation/Command.tsx +266 -0
  137. package/src/components/navigation/CommandPalette.tsx +83 -0
  138. package/src/components/navigation/DocumentPager.tsx +171 -0
  139. package/src/components/navigation/Footer.tsx +88 -0
  140. package/src/components/navigation/Header.tsx +80 -0
  141. package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
  142. package/src/components/navigation/NavigationMenu.tsx +128 -0
  143. package/src/components/navigation/PageAside.tsx +84 -0
  144. package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
  145. package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
  146. package/src/components/navigation/Sidebar.tsx +223 -0
  147. package/src/components/navigation/SidebarItem.tsx +160 -0
  148. package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
  149. package/src/components/navigation/TextLink.tsx +71 -0
  150. package/src/components/navigation/generated/default-variant-keys.ts +12 -0
  151. package/src/components/navigation/generated/variant-keys.ts +13 -0
  152. package/src/components/overlay/AIChatInput.tsx +5 -0
  153. package/src/components/overlay/AIChatMessage.tsx +6 -0
  154. package/src/components/overlay/AlertDialog.tsx +145 -0
  155. package/src/components/overlay/ChatPanel.tsx +180 -0
  156. package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
  157. package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
  158. package/src/components/overlay/Drawer.tsx +131 -0
  159. package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
  160. package/src/components/overlay/FloatingPanel.tsx +90 -0
  161. package/src/components/overlay/HoverCard.tsx +36 -0
  162. package/src/components/overlay/MediaLightbox.tsx +403 -0
  163. package/src/components/overlay/MediaPickerDialog.tsx +198 -0
  164. package/src/components/overlay/Modal.tsx +103 -0
  165. package/src/components/overlay/OnboardingFlow.tsx +172 -0
  166. package/src/components/overlay/Popover.tsx +36 -0
  167. package/src/components/overlay/ShareModal.tsx +324 -0
  168. package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
  169. package/src/components/overlay/Tooltip.tsx +130 -0
  170. package/src/components/overlay/generated/default-variant-keys.ts +14 -0
  171. package/src/components/overlay/generated/variant-keys.ts +17 -0
  172. package/src/components/patterns/BlogTemplate.tsx +46 -0
  173. package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
  174. package/src/components/patterns/DocsTemplate.tsx +41 -0
  175. package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
  176. package/src/components/patterns/OnboardingTemplate.tsx +32 -0
  177. package/src/components/patterns/PricingTemplate.tsx +106 -0
  178. package/src/globals.css +173 -22
  179. package/src/index.ts +177 -76
  180. package/tailwind-theme-extend.cjs +48 -3
  181. package/design/atoms-metadata.json +0 -82
  182. package/design/molecules-metadata.json +0 -130
  183. package/design/organisms-metadata.json +0 -38
  184. package/design/templates-metadata.json +0 -38
  185. package/src/components/atoms/Avatar.tsx +0 -57
  186. package/src/components/atoms/Select.tsx +0 -28
  187. package/src/components/atoms/generated/default-variant-keys.ts +0 -36
  188. package/src/components/molecules/AIChatInput.tsx +0 -140
  189. package/src/components/molecules/AIChatMessage.tsx +0 -109
  190. package/src/components/molecules/Accordion.tsx +0 -99
  191. package/src/components/molecules/Calendar.tsx +0 -60
  192. package/src/components/molecules/Carousel.tsx +0 -261
  193. package/src/components/molecules/Command.tsx +0 -152
  194. package/src/components/molecules/FilterButton.tsx +0 -133
  195. package/src/components/molecules/HoverCard.tsx +0 -29
  196. package/src/components/molecules/Modal.tsx +0 -66
  197. package/src/components/molecules/Popover.tsx +0 -31
  198. package/src/components/molecules/ProgressWidget.tsx +0 -40
  199. package/src/components/molecules/Resizable.tsx +0 -47
  200. package/src/components/molecules/ScrollArea.tsx +0 -48
  201. package/src/components/molecules/SidebarItem.tsx +0 -134
  202. package/src/components/molecules/Toast.tsx +0 -57
  203. package/src/components/molecules/Tooltip.tsx +0 -30
  204. package/src/components/molecules/generated/default-variant-keys.ts +0 -22
  205. package/src/components/molecules/generated/variant-keys.ts +0 -33
  206. package/src/components/organisms/CommandPalette.tsx +0 -58
  207. package/src/components/organisms/FloatingPanel.tsx +0 -46
  208. package/src/components/organisms/ShareModal.tsx +0 -182
  209. package/src/components/organisms/ToastProvider.tsx +0 -49
  210. /package/src/components/{atoms → display}/Kbd.tsx +0 -0
  211. /package/src/components/{atoms → display}/Separator.tsx +0 -0
  212. /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
  213. /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
  214. /package/src/components/{atoms → inputs}/Button.tsx +0 -0
  215. /package/src/components/{atoms → inputs}/Label.tsx +0 -0
  216. /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
  217. /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
  218. /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
  219. /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
  220. /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
  221. /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
  222. /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
  223. /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
  224. /package/src/components/{templates → patterns}/SettingsTemplate.tsx +0 -0
@@ -0,0 +1,528 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ IconAdjustmentsHorizontal as Adjustments,
6
+ IconPlus as Plus,
7
+ IconSend as Send,
8
+ IconSquare as Square,
9
+ IconX as X,
10
+ } from "@tabler/icons-react";
11
+ import { cn } from "../../lib/utils";
12
+ import {
13
+ DropdownMenu,
14
+ DropdownMenuContent,
15
+ DropdownMenuRadioGroup,
16
+ DropdownMenuRadioItem,
17
+ DropdownMenuTrigger,
18
+ } from "../overlay/DropdownMenu";
19
+ import { Popover, PopoverContent, PopoverTrigger } from "../overlay/Popover";
20
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip";
21
+ import { Button } from "./Button";
22
+ import { Textarea } from "./Textarea";
23
+ import { TooltipButton } from "./TooltipButton";
24
+ import { VoiceInputButton } from "./VoiceInputButton";
25
+ import { buttonDefaultVariantKey, chatInputDefaultVariantKey } from "./generated/default-variant-keys";
26
+ import type { ChatInputVariantKey } from "./generated/variant-keys";
27
+
28
+ const chatInputVariantClassNames: Record<ChatInputVariantKey, string> = {
29
+ default: "",
30
+ processing: "border-primary-border bg-primary-subtle",
31
+ };
32
+
33
+ export interface ChatInputLabels {
34
+ attach?: string;
35
+ options?: string;
36
+ model?: string;
37
+ voice?: string;
38
+ send?: string;
39
+ stop?: string;
40
+ removeAttachment?: string;
41
+ emptyMessage?: string;
42
+ disabled?: string;
43
+ voiceActive?: string;
44
+ voiceRequesting?: string;
45
+ voiceUnsupported?: string;
46
+ voicePermissionDenied?: string;
47
+ voiceError?: string;
48
+ }
49
+
50
+ export interface ChatInputModelOption {
51
+ value: string;
52
+ label: React.ReactNode;
53
+ description?: React.ReactNode;
54
+ }
55
+
56
+ export interface ChatInputProps
57
+ extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "onSubmit"> {
58
+ variant?: ChatInputVariantKey;
59
+ onSend: (message: string, files?: File[]) => void;
60
+ onStop?: () => void;
61
+ isProcessing?: boolean;
62
+ enableAttachments?: boolean;
63
+ showOptionsButton?: boolean;
64
+ showModelSelector?: boolean;
65
+ showVoiceButton?: boolean;
66
+ toolbarAccessory?: React.ReactNode;
67
+ modelLabel?: React.ReactNode;
68
+ onOptionsClick?: () => void;
69
+ onModelClick?: () => void;
70
+ onVoiceClick?: () => void;
71
+ optionsContent?: React.ReactNode;
72
+ optionsOpen?: boolean;
73
+ defaultOptionsOpen?: boolean;
74
+ onOptionsOpenChange?: (open: boolean) => void;
75
+ modelOptions?: ChatInputModelOption[];
76
+ modelValue?: string;
77
+ defaultModelValue?: string;
78
+ onModelValueChange?: (value: string) => void;
79
+ voiceActive?: boolean;
80
+ defaultVoiceActive?: boolean;
81
+ onVoiceActiveChange?: (active: boolean) => void;
82
+ labels?: ChatInputLabels;
83
+ }
84
+
85
+ export function ChatInput({
86
+ variant,
87
+ onSend,
88
+ onStop,
89
+ isProcessing = false,
90
+ placeholder = "メッセージを入力...",
91
+ enableAttachments = true,
92
+ showOptionsButton = true,
93
+ showModelSelector = true,
94
+ showVoiceButton = true,
95
+ toolbarAccessory,
96
+ modelLabel,
97
+ onOptionsClick,
98
+ onModelClick,
99
+ onVoiceClick,
100
+ optionsContent,
101
+ optionsOpen,
102
+ defaultOptionsOpen = false,
103
+ onOptionsOpenChange,
104
+ modelOptions = [],
105
+ modelValue,
106
+ defaultModelValue,
107
+ onModelValueChange,
108
+ voiceActive,
109
+ defaultVoiceActive = false,
110
+ onVoiceActiveChange,
111
+ labels,
112
+ className,
113
+ disabled,
114
+ ...props
115
+ }: ChatInputProps) {
116
+ const [message, setMessage] = React.useState("");
117
+ const [files, setFiles] = React.useState<File[]>([]);
118
+ const [uncontrolledOptionsOpen, setUncontrolledOptionsOpen] = React.useState(defaultOptionsOpen);
119
+ const [uncontrolledModelValue, setUncontrolledModelValue] = React.useState(
120
+ defaultModelValue ?? modelOptions[0]?.value ?? ""
121
+ );
122
+ const [uncontrolledVoiceActive, setUncontrolledVoiceActive] = React.useState(defaultVoiceActive);
123
+ const [optionTooltipOpen, setOptionTooltipOpen] = React.useState(false);
124
+ const textareaRef = React.useRef<HTMLTextAreaElement>(null);
125
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
126
+ const isComposingRef = React.useRef(false);
127
+ const resolvedVariant = variant ?? (isProcessing ? "processing" : chatInputDefaultVariantKey);
128
+
129
+ const resolvedLabels = {
130
+ attach: labels?.attach ?? "ファイルを添付",
131
+ options: labels?.options ?? "入力オプション",
132
+ model: labels?.model ?? "モデルを選択",
133
+ voice: labels?.voice ?? "音声入力",
134
+ send: labels?.send ?? "送信",
135
+ stop: labels?.stop ?? "生成を停止",
136
+ removeAttachment: labels?.removeAttachment ?? "添付を削除",
137
+ emptyMessage: labels?.emptyMessage ?? "メッセージを入力すると送信できます。",
138
+ disabled: labels?.disabled ?? "現在は入力できません。",
139
+ voiceActive: labels?.voiceActive ?? "録音を停止",
140
+ voiceRequesting: labels?.voiceRequesting ?? "マイクのアクセス権を確認しています。",
141
+ voiceUnsupported: labels?.voiceUnsupported ?? "このブラウザは音声入力に対応していません。",
142
+ voicePermissionDenied: labels?.voicePermissionDenied ?? "マイクのアクセス権が許可されていません。",
143
+ voiceError: labels?.voiceError ?? "音声入力を開始できませんでした。",
144
+ };
145
+
146
+ const filePreviews = React.useMemo(
147
+ () =>
148
+ files.map((file) => ({
149
+ file,
150
+ url: file.type.startsWith("image/") ? URL.createObjectURL(file) : null,
151
+ })),
152
+ [files]
153
+ );
154
+
155
+ React.useEffect(() => {
156
+ return () => {
157
+ for (const preview of filePreviews) {
158
+ if (preview.url) URL.revokeObjectURL(preview.url);
159
+ }
160
+ };
161
+ }, [filePreviews]);
162
+
163
+ const adjustHeight = React.useCallback(() => {
164
+ const textarea = textareaRef.current;
165
+ if (!textarea) return;
166
+ textarea.style.height = "auto";
167
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
168
+ }, []);
169
+
170
+ const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
171
+ setMessage(event.target.value);
172
+ adjustHeight();
173
+ props.onChange?.(event);
174
+ };
175
+
176
+ const appendTranscript = React.useCallback(
177
+ (transcript: string) => {
178
+ const normalizedTranscript = transcript.trim();
179
+ if (!normalizedTranscript) return;
180
+ setMessage((currentMessage) => {
181
+ if (!currentMessage.trim()) return normalizedTranscript;
182
+ return `${currentMessage.trimEnd()} ${normalizedTranscript}`;
183
+ });
184
+ window.requestAnimationFrame(adjustHeight);
185
+ },
186
+ [adjustHeight]
187
+ );
188
+
189
+ const handleCompositionStart = (event: React.CompositionEvent<HTMLTextAreaElement>) => {
190
+ isComposingRef.current = true;
191
+ props.onCompositionStart?.(event);
192
+ };
193
+
194
+ const handleCompositionEnd = (event: React.CompositionEvent<HTMLTextAreaElement>) => {
195
+ isComposingRef.current = false;
196
+ props.onCompositionEnd?.(event);
197
+ };
198
+
199
+ const resetMessage = React.useCallback(() => {
200
+ setMessage("");
201
+ setFiles([]);
202
+ if (textareaRef.current) {
203
+ textareaRef.current.style.height = "auto";
204
+ }
205
+ }, []);
206
+
207
+ const handleSend = React.useCallback(() => {
208
+ const value = message.trim();
209
+ if ((!value && files.length === 0) || isProcessing || disabled) return;
210
+ onSend(value, files.length > 0 ? files : undefined);
211
+ resetMessage();
212
+ }, [disabled, files, isProcessing, message, onSend, resetMessage]);
213
+
214
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
215
+ props.onKeyDown?.(event);
216
+ if (event.defaultPrevented) return;
217
+ const nativeEvent = event.nativeEvent as KeyboardEvent & { isComposing?: boolean };
218
+ const isComposing = isComposingRef.current || nativeEvent.isComposing || nativeEvent.keyCode === 229;
219
+ if (event.key === "Enter" && !event.shiftKey && !isComposing) {
220
+ event.preventDefault();
221
+ handleSend();
222
+ }
223
+ };
224
+
225
+ const handleUploadClick = () => {
226
+ fileInputRef.current?.click();
227
+ };
228
+
229
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
230
+ const selectedFiles = event.target.files ? Array.from(event.target.files) : [];
231
+ if (selectedFiles.length === 0) return;
232
+ setFiles((current) => [...current, ...selectedFiles]);
233
+ event.target.value = "";
234
+ };
235
+
236
+ const isDisabled = Boolean(disabled);
237
+ const isOptionsOpenControlled = optionsOpen !== undefined;
238
+ const currentOptionsOpen = isOptionsOpenControlled ? optionsOpen : uncontrolledOptionsOpen;
239
+ const isModelControlled = modelValue !== undefined;
240
+ const currentModelValue = isModelControlled ? modelValue : uncontrolledModelValue;
241
+ const selectedModel = modelOptions.find((option) => option.value === currentModelValue);
242
+ const displayedModelLabel = modelLabel ?? selectedModel?.label ?? modelOptions[0]?.label ?? "自動";
243
+ const isVoiceControlled = voiceActive !== undefined;
244
+ const currentVoiceActive = isVoiceControlled ? voiceActive : uncontrolledVoiceActive;
245
+ const canSend = (message.trim().length > 0 || files.length > 0) && !isProcessing && !isDisabled;
246
+ const shouldShowOptionsButton = showOptionsButton && (Boolean(optionsContent) || Boolean(onOptionsClick));
247
+ const sendDisabledReason = isDisabled
248
+ ? resolvedLabels.disabled
249
+ : resolvedLabels.emptyMessage;
250
+
251
+ const handleOptionsOpenChange = (open: boolean) => {
252
+ if (!isOptionsOpenControlled) setUncontrolledOptionsOpen(open);
253
+ if (open) setOptionTooltipOpen(false);
254
+ onOptionsOpenChange?.(open);
255
+ };
256
+
257
+ const handleOptionsClick = () => {
258
+ onOptionsClick?.();
259
+ };
260
+
261
+ const handleModelValueChange = (value: string) => {
262
+ if (!isModelControlled) setUncontrolledModelValue(value);
263
+ onModelValueChange?.(value);
264
+ onModelClick?.();
265
+ };
266
+
267
+ const handleVoiceListeningChange = (active: boolean) => {
268
+ if (!isVoiceControlled) setUncontrolledVoiceActive(active);
269
+ onVoiceActiveChange?.(active);
270
+ };
271
+
272
+ const optionButton = (
273
+ <Button
274
+ type="button"
275
+ variant="ghost"
276
+ size="icon"
277
+ className={cn(
278
+ "h-9 w-9 shrink-0 text-muted-foreground hover:text-foreground",
279
+ currentOptionsOpen && "bg-accent text-foreground"
280
+ )}
281
+ onClick={handleOptionsClick}
282
+ disabled={isDisabled || isProcessing}
283
+ aria-label={resolvedLabels.options}
284
+ aria-pressed={optionsContent ? currentOptionsOpen : undefined}
285
+ >
286
+ <Adjustments className="h-5 w-5" />
287
+ </Button>
288
+ );
289
+
290
+ const modelButton = (
291
+ <Button
292
+ type="button"
293
+ variant="ghost"
294
+ size="sm"
295
+ className="h-9 shrink-0 px-2 text-sm font-semibold text-muted-foreground hover:text-foreground"
296
+ onClick={modelOptions.length > 0 ? undefined : onModelClick}
297
+ disabled={isDisabled || isProcessing}
298
+ aria-label={resolvedLabels.model}
299
+ >
300
+ {displayedModelLabel}
301
+ </Button>
302
+ );
303
+
304
+ return (
305
+ <div
306
+ className={cn(
307
+ "w-full max-w-full rounded-2xl border border-input bg-background p-3 shadow-sm transition-colors focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30",
308
+ chatInputVariantClassNames[resolvedVariant],
309
+ isDisabled && "bg-muted/40 opacity-70",
310
+ className
311
+ )}
312
+ onClick={() => textareaRef.current?.focus()}
313
+ >
314
+ {files.length > 0 ? (
315
+ <div className="mb-3 flex gap-2 overflow-x-auto pb-1" onClick={(event) => event.stopPropagation()}>
316
+ {filePreviews.map(({ file, url }, index) => (
317
+ <div
318
+ key={`${file.name}-${file.size}-${index}`}
319
+ className="group relative flex h-16 min-w-16 max-w-44 items-center gap-2 overflow-hidden rounded-lg border bg-muted/50 p-2"
320
+ >
321
+ {url ? (
322
+ // eslint-disable-next-line @next/next/no-img-element
323
+ <img
324
+ src={url}
325
+ alt={file.name}
326
+ className="h-12 w-12 shrink-0 rounded-md object-cover"
327
+ />
328
+ ) : (
329
+ <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-md bg-background text-xs font-medium text-muted-foreground">
330
+ {file.name.split(".").pop()?.slice(0, 4).toUpperCase() ?? "FILE"}
331
+ </div>
332
+ )}
333
+ <span className="min-w-0 truncate text-xs font-medium text-foreground">
334
+ {file.name}
335
+ </span>
336
+ <TooltipButton
337
+ type="button"
338
+ tooltip={resolvedLabels.removeAttachment}
339
+ aria-label={resolvedLabels.removeAttachment}
340
+ variant="ghost"
341
+ size="icon"
342
+ className="absolute right-1 top-1 h-6 w-6 bg-background/80 opacity-0 shadow-sm group-hover:opacity-100 group-focus-within:opacity-100"
343
+ onClick={() => setFiles((current) => current.filter((_, fileIndex) => fileIndex !== index))}
344
+ >
345
+ <X className="h-3.5 w-3.5" />
346
+ </TooltipButton>
347
+ </div>
348
+ ))}
349
+ </div>
350
+ ) : null}
351
+
352
+ <Textarea
353
+ {...props}
354
+ ref={textareaRef}
355
+ value={message}
356
+ onChange={handleInput}
357
+ onKeyDown={handleKeyDown}
358
+ onCompositionStart={handleCompositionStart}
359
+ onCompositionEnd={handleCompositionEnd}
360
+ placeholder={placeholder}
361
+ disabled={isDisabled || isProcessing}
362
+ className="max-h-[15rem] min-h-[3.5rem] w-full resize-none overflow-y-auto rounded-none border-0 bg-transparent px-1 py-2 text-base shadow-none focus-visible:ring-0 disabled:bg-transparent sm:text-sm"
363
+ rows={2}
364
+ onClick={(event) => event.stopPropagation()}
365
+ />
366
+
367
+ <div className="mt-2 flex items-center justify-between gap-3">
368
+ <div className="flex min-w-0 items-center gap-1" onClick={(event) => event.stopPropagation()}>
369
+ {enableAttachments ? (
370
+ <>
371
+ <input
372
+ ref={fileInputRef}
373
+ type="file"
374
+ multiple
375
+ className="hidden"
376
+ onChange={handleFileChange}
377
+ />
378
+ <TooltipButton
379
+ type="button"
380
+ tooltip={resolvedLabels.attach}
381
+ aria-label={resolvedLabels.attach}
382
+ variant="ghost"
383
+ size="icon"
384
+ className="h-9 w-9 shrink-0 text-muted-foreground hover:text-foreground"
385
+ onClick={handleUploadClick}
386
+ disabled={isDisabled || isProcessing}
387
+ >
388
+ <Plus className="h-5 w-5" />
389
+ </TooltipButton>
390
+ </>
391
+ ) : null}
392
+
393
+ {shouldShowOptionsButton ? (
394
+ optionsContent ? (
395
+ <Popover open={currentOptionsOpen} onOpenChange={handleOptionsOpenChange}>
396
+ <Tooltip
397
+ open={currentOptionsOpen ? false : optionTooltipOpen}
398
+ onOpenChange={setOptionTooltipOpen}
399
+ >
400
+ <TooltipTrigger asChild>
401
+ <PopoverTrigger asChild>{optionButton}</PopoverTrigger>
402
+ </TooltipTrigger>
403
+ <TooltipContent>{resolvedLabels.options}</TooltipContent>
404
+ </Tooltip>
405
+ <PopoverContent align="start" side="top" className="w-72">
406
+ {optionsContent}
407
+ </PopoverContent>
408
+ </Popover>
409
+ ) : (
410
+ <TooltipButton
411
+ type="button"
412
+ tooltip={resolvedLabels.options}
413
+ aria-label={resolvedLabels.options}
414
+ variant="ghost"
415
+ size="icon"
416
+ className="h-9 w-9 shrink-0 text-muted-foreground hover:text-foreground"
417
+ onClick={handleOptionsClick}
418
+ disabled={isDisabled || isProcessing}
419
+ >
420
+ <Adjustments className="h-5 w-5" />
421
+ </TooltipButton>
422
+ )
423
+ ) : null}
424
+ </div>
425
+
426
+ <div className="flex min-w-0 items-center gap-1" onClick={(event) => event.stopPropagation()}>
427
+ {showModelSelector ? (
428
+ modelOptions.length > 0 ? (
429
+ <DropdownMenu>
430
+ <DropdownMenuTrigger asChild>{modelButton}</DropdownMenuTrigger>
431
+ <DropdownMenuContent align="end" sideOffset={8} className="w-56">
432
+ <DropdownMenuRadioGroup value={currentModelValue} onValueChange={handleModelValueChange}>
433
+ {modelOptions.map((option) => (
434
+ <DropdownMenuRadioItem key={option.value} value={option.value} className="items-start gap-2">
435
+ <span className="min-w-0">
436
+ <span className="block truncate font-medium">{option.label}</span>
437
+ {option.description ? (
438
+ <span className="block truncate text-xs text-muted-foreground">
439
+ {option.description}
440
+ </span>
441
+ ) : null}
442
+ </span>
443
+ </DropdownMenuRadioItem>
444
+ ))}
445
+ </DropdownMenuRadioGroup>
446
+ </DropdownMenuContent>
447
+ </DropdownMenu>
448
+ ) : (
449
+ <TooltipButton
450
+ type="button"
451
+ tooltip={resolvedLabels.model}
452
+ aria-label={resolvedLabels.model}
453
+ variant="ghost"
454
+ size="sm"
455
+ className="h-9 shrink-0 px-2 text-sm font-semibold text-muted-foreground hover:text-foreground"
456
+ onClick={onModelClick}
457
+ disabled={isDisabled || isProcessing}
458
+ >
459
+ {displayedModelLabel}
460
+ </TooltipButton>
461
+ )
462
+ ) : null}
463
+
464
+ {toolbarAccessory ? (
465
+ <div className="flex shrink-0 items-center">
466
+ {toolbarAccessory}
467
+ </div>
468
+ ) : null}
469
+
470
+ {showVoiceButton ? (
471
+ <VoiceInputButton
472
+ onTranscript={appendTranscript}
473
+ listening={currentVoiceActive}
474
+ onListeningChange={handleVoiceListeningChange}
475
+ onToggle={onVoiceClick}
476
+ disabled={isDisabled || isProcessing}
477
+ labels={{
478
+ start: resolvedLabels.voice,
479
+ stop: resolvedLabels.voiceActive,
480
+ requesting: resolvedLabels.voiceRequesting,
481
+ unsupported: resolvedLabels.voiceUnsupported,
482
+ permissionDenied: resolvedLabels.voicePermissionDenied,
483
+ error: resolvedLabels.voiceError,
484
+ }}
485
+ />
486
+ ) : null}
487
+
488
+ {isProcessing ? (
489
+ <TooltipButton
490
+ type="button"
491
+ tooltip={resolvedLabels.stop}
492
+ aria-label={resolvedLabels.stop}
493
+ variant="destructive"
494
+ size="icon"
495
+ className="h-9 w-9 shrink-0 rounded-full"
496
+ onClick={onStop}
497
+ >
498
+ <Square className="h-4 w-4 fill-current" />
499
+ </TooltipButton>
500
+ ) : (
501
+ <Tooltip>
502
+ <TooltipTrigger asChild>
503
+ <span
504
+ className="inline-flex shrink-0 rounded-full"
505
+ tabIndex={canSend ? -1 : 0}
506
+ aria-label={canSend ? resolvedLabels.send : sendDisabledReason}
507
+ >
508
+ <Button
509
+ type="button"
510
+ variant={buttonDefaultVariantKey}
511
+ size="icon"
512
+ className="h-9 w-9 rounded-full bg-primary text-primary-foreground hover:bg-primary-strong"
513
+ onClick={handleSend}
514
+ disabled={!canSend}
515
+ aria-label={resolvedLabels.send}
516
+ >
517
+ <Send className="h-4 w-4" />
518
+ </Button>
519
+ </span>
520
+ </TooltipTrigger>
521
+ <TooltipContent>{canSend ? resolvedLabels.send : sendDisabledReason}</TooltipContent>
522
+ </Tooltip>
523
+ )}
524
+ </div>
525
+ </div>
526
+ </div>
527
+ );
528
+ }
@@ -1,13 +1,13 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
- import { Check } from "lucide-react"
4
+ import { IconCheck as Check } from "@tabler/icons-react";
5
5
  import { cn } from "../../lib/utils"
6
6
  import type { CheckboxVariantKey } from "./generated/variant-keys"
7
7
  import { checkboxDefaultVariantKey } from "./generated/default-variant-keys"
8
8
 
9
9
  const checkboxStateClasses: Record<CheckboxVariantKey, string> = {
10
- checked: "border-transparent bg-foreground text-primary-foreground text-xs font-semibold",
10
+ checked: "border-transparent bg-foreground text-background text-xs font-semibold",
11
11
  disabled: "bg-transparent disabled:bg-muted disabled:opacity-50",
12
12
  unchecked: "bg-transparent",
13
13
  }