@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,124 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import type { ChartColor } from "./chart-utils"
7
+ import { chartLabelToString, getChartColor } from "./chart-utils"
8
+ import { ChartTooltip } from "./chart-tooltip"
9
+ import type { ChartLegendVariantKey } from "./generated/variant-keys"
10
+ import { chartLegendDefaultVariantKey } from "./generated/default-variant-keys"
11
+
12
+ export interface ChartLegendItem {
13
+ label: React.ReactNode
14
+ value?: React.ReactNode
15
+ color?: ChartColor
16
+ description?: React.ReactNode
17
+ }
18
+
19
+ export interface ChartLegendProps extends React.HTMLAttributes<HTMLDivElement> {
20
+ items: ChartLegendItem[]
21
+ variant?: ChartLegendVariantKey
22
+ activeIndex?: number
23
+ }
24
+
25
+ const chartLegendVariantClasses: Record<ChartLegendVariantKey, string> = {
26
+ horizontal: "inline-flex w-full flex-wrap items-center gap-2 p-0",
27
+ vertical: "flex w-full flex-col items-stretch gap-2 p-0",
28
+ }
29
+
30
+ function chartLegendNodeToString(value: React.ReactNode, fallback = ""): string {
31
+ if (value === undefined || value === null || typeof value === "boolean") {
32
+ return fallback
33
+ }
34
+
35
+ if (typeof value === "string" || typeof value === "number") {
36
+ return String(value)
37
+ }
38
+
39
+ if (Array.isArray(value)) {
40
+ const text = value
41
+ .map((item) => chartLegendNodeToString(item))
42
+ .join("")
43
+ .trim()
44
+ return text || fallback
45
+ }
46
+
47
+ return fallback
48
+ }
49
+
50
+ function getLegendItemLabel(item: ChartLegendItem, index: number) {
51
+ const label = chartLegendNodeToString(item.label, `Series ${index + 1}`)
52
+ const value = chartLegendNodeToString(item.value)
53
+ const description = chartLegendNodeToString(item.description)
54
+
55
+ return [label, value, description].filter(Boolean).join(": ")
56
+ }
57
+
58
+ const ChartLegend = React.forwardRef<HTMLDivElement, ChartLegendProps>(
59
+ (
60
+ {
61
+ className,
62
+ items,
63
+ variant = chartLegendDefaultVariantKey,
64
+ activeIndex,
65
+ role,
66
+ ...props
67
+ },
68
+ ref
69
+ ) => (
70
+ <div
71
+ ref={ref}
72
+ className={cn(chartLegendVariantClasses[variant], className)}
73
+ role={role ?? "list"}
74
+ {...props}
75
+ >
76
+ {items.map((item, index) => {
77
+ const itemLabel = getLegendItemLabel(item, index)
78
+ const isActive = activeIndex === index
79
+
80
+ return (
81
+ <ChartTooltip
82
+ key={`${chartLabelToString(item.label, "Series")}-${index}`}
83
+ label={item.label}
84
+ value={item.value}
85
+ description={item.description}
86
+ >
87
+ <div
88
+ className={cn(
89
+ "inline-flex min-w-0 items-center gap-3 rounded-sm border border-transparent px-2 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
90
+ variant === "vertical"
91
+ ? "w-full justify-between"
92
+ : "w-auto justify-start",
93
+ isActive && "border-border bg-muted/60 text-foreground shadow-sm"
94
+ )}
95
+ role="listitem"
96
+ tabIndex={0}
97
+ aria-current={isActive ? "true" : undefined}
98
+ aria-label={itemLabel}
99
+ >
100
+ <span className="inline-flex min-w-0 items-center gap-2 text-muted-foreground">
101
+ <span
102
+ className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
103
+ style={{
104
+ backgroundColor: getChartColor(item.color, index),
105
+ }}
106
+ aria-hidden="true"
107
+ />
108
+ <span className="truncate">{item.label}</span>
109
+ </span>
110
+ {item.value !== undefined ? (
111
+ <span className="flex-shrink-0 font-medium text-foreground">
112
+ {item.value}
113
+ </span>
114
+ ) : null}
115
+ </div>
116
+ </ChartTooltip>
117
+ )
118
+ })}
119
+ </div>
120
+ )
121
+ )
122
+ ChartLegend.displayName = "ChartLegend"
123
+
124
+ export { ChartLegend }
@@ -0,0 +1,382 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ IconCopy as Copy,
6
+ IconCode as Code,
7
+ IconGitBranch as GitBranch,
8
+ IconPencil as Pencil,
9
+ IconSparkles as Sparkles,
10
+ IconUser as User,
11
+ } from "@tabler/icons-react";
12
+ import { cn } from "../../lib/utils";
13
+ import { Avatar, AvatarFallback, AvatarImage } from "./Avatar";
14
+ import { TooltipButton } from "../inputs/TooltipButton";
15
+ import { chatMessageDefaultVariantKey } from "./generated/default-variant-keys";
16
+ import type { ChatMessageVariantKey } from "./generated/variant-keys";
17
+
18
+ export type ChatMessageRole = "assistant" | "user" | "system";
19
+ export type ChatMessageActionKey = "copy" | "branch" | "raw" | "edit" | (string & {});
20
+
21
+ const chatMessageVariantClassNames: Record<ChatMessageVariantKey, string> = {
22
+ assistant: "",
23
+ system: "justify-center",
24
+ typing: "",
25
+ user: "flex-row-reverse",
26
+ };
27
+
28
+ export interface ChatMessageAction {
29
+ key: ChatMessageActionKey;
30
+ label: React.ReactNode;
31
+ icon?: React.ReactNode;
32
+ disabled?: boolean;
33
+ onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
34
+ }
35
+
36
+ export interface ChatMessageLabels {
37
+ copy?: string;
38
+ branch?: string;
39
+ raw?: string;
40
+ edit?: string;
41
+ typing?: string;
42
+ positiveFeedback?: string;
43
+ negativeFeedback?: string;
44
+ }
45
+
46
+ export interface ChatMessageProps {
47
+ variant?: ChatMessageVariantKey;
48
+ role: ChatMessageRole;
49
+ content: React.ReactNode;
50
+ copyValue?: string;
51
+ typingMessages?: string[];
52
+ avatarSrc?: string;
53
+ userName?: string;
54
+ timestamp?: string;
55
+ isTyping?: boolean;
56
+ actions?: ChatMessageAction[];
57
+ showActions?: boolean;
58
+ onAction?: (actionKey: ChatMessageActionKey, event: React.MouseEvent<HTMLButtonElement>) => void;
59
+ labels?: ChatMessageLabels;
60
+ className?: string;
61
+ }
62
+
63
+ export interface ChatMessageActionsProps extends React.HTMLAttributes<HTMLDivElement> {
64
+ actions: ChatMessageAction[];
65
+ align?: "start" | "end";
66
+ onAction?: (actionKey: ChatMessageActionKey, event: React.MouseEvent<HTMLButtonElement>) => void;
67
+ }
68
+
69
+ export function ChatMessageActions({
70
+ actions,
71
+ align = "start",
72
+ onAction,
73
+ className,
74
+ ...props
75
+ }: ChatMessageActionsProps) {
76
+ if (actions.length === 0) return null;
77
+
78
+ return (
79
+ <div
80
+ className={cn(
81
+ "mt-2 flex flex-wrap items-center gap-1",
82
+ align === "end" ? "justify-end" : "justify-start",
83
+ className
84
+ )}
85
+ {...props}
86
+ >
87
+ {actions.map((action) => (
88
+ <TooltipButton
89
+ key={action.key}
90
+ type="button"
91
+ tooltip={action.label}
92
+ aria-label={typeof action.label === "string" ? action.label : undefined}
93
+ aria-disabled={action.disabled}
94
+ variant="ghost"
95
+ size="xs"
96
+ className={cn(
97
+ "h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground",
98
+ action.disabled && "opacity-50 hover:bg-transparent hover:text-muted-foreground"
99
+ )}
100
+ onClick={(event) => {
101
+ if (action.disabled) {
102
+ event.preventDefault();
103
+ return;
104
+ }
105
+ action.onClick?.(event);
106
+ if (!event.defaultPrevented) {
107
+ onAction?.(action.key, event);
108
+ }
109
+ }}
110
+ >
111
+ {action.icon ? <span className="flex h-3.5 w-3.5 items-center justify-center">{action.icon}</span> : null}
112
+ <span>{action.label}</span>
113
+ </TooltipButton>
114
+ ))}
115
+ </div>
116
+ );
117
+ }
118
+
119
+ function useTypingMessage(messages: string[], enabled: boolean) {
120
+ const messageKey = messages.join("\u0000");
121
+ const [text, setText] = React.useState(messages[0] ?? "");
122
+ const [caretVisible, setCaretVisible] = React.useState(true);
123
+
124
+ React.useEffect(() => {
125
+ const sequence = messageKey.split("\u0000").filter(Boolean);
126
+ const firstMessage = sequence[0] ?? "";
127
+
128
+ if (!enabled || sequence.length === 0) {
129
+ setText(firstMessage);
130
+ setCaretVisible(true);
131
+ return;
132
+ }
133
+
134
+ const reduceMotion = typeof window !== "undefined"
135
+ && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
136
+
137
+ if (reduceMotion || sequence.length === 1) {
138
+ setText(firstMessage);
139
+ setCaretVisible(true);
140
+ return;
141
+ }
142
+
143
+ let cancelled = false;
144
+ let timer: number | undefined;
145
+ let messageIndex = 0;
146
+ let charIndex = firstMessage.length;
147
+ let phase: "hold" | "delete" | "type" = "hold";
148
+ let holdToggleCount = 0;
149
+ const getHoldToggleLimit = () => {
150
+ const isLastMessage = messageIndex === sequence.length - 1;
151
+ return isLastMessage ? 8 : 2;
152
+ };
153
+
154
+ setText(firstMessage);
155
+ setCaretVisible(true);
156
+
157
+ const schedule = (delay: number) => {
158
+ timer = window.setTimeout(tick, delay);
159
+ };
160
+
161
+ const tick = () => {
162
+ if (cancelled) return;
163
+
164
+ const currentMessage = sequence[messageIndex] ?? "";
165
+
166
+ if (phase === "hold") {
167
+ if (holdToggleCount < getHoldToggleLimit()) {
168
+ holdToggleCount += 1;
169
+ setCaretVisible((current) => !current);
170
+ schedule(700);
171
+ return;
172
+ }
173
+
174
+ holdToggleCount = 0;
175
+ setCaretVisible(true);
176
+ phase = "delete";
177
+ schedule(240);
178
+ return;
179
+ }
180
+
181
+ if (phase === "delete") {
182
+ setCaretVisible(true);
183
+ if (charIndex > 0) {
184
+ charIndex -= 1;
185
+ setText(currentMessage.slice(0, charIndex));
186
+ schedule(56);
187
+ return;
188
+ }
189
+
190
+ messageIndex = (messageIndex + 1) % sequence.length;
191
+ phase = "type";
192
+ schedule(180);
193
+ return;
194
+ }
195
+
196
+ const nextMessage = sequence[messageIndex] ?? "";
197
+ if (charIndex < nextMessage.length) {
198
+ setCaretVisible(true);
199
+ charIndex += 1;
200
+ setText(nextMessage.slice(0, charIndex));
201
+ schedule(58);
202
+ return;
203
+ }
204
+
205
+ phase = "hold";
206
+ holdToggleCount = 0;
207
+ setCaretVisible(true);
208
+ schedule(700);
209
+ };
210
+
211
+ schedule(700);
212
+
213
+ return () => {
214
+ cancelled = true;
215
+ if (timer !== undefined) {
216
+ window.clearTimeout(timer);
217
+ }
218
+ };
219
+ }, [enabled, messageKey]);
220
+
221
+ return { text, caretVisible };
222
+ }
223
+
224
+ export function ChatMessage({
225
+ variant,
226
+ role,
227
+ content,
228
+ copyValue,
229
+ typingMessages,
230
+ avatarSrc,
231
+ userName = role === "user" ? "You" : "Assistant",
232
+ timestamp,
233
+ isTyping = false,
234
+ actions,
235
+ showActions = true,
236
+ onAction,
237
+ labels,
238
+ className,
239
+ }: ChatMessageProps) {
240
+ const typingFallback = labels?.typing ?? "内容を作成しています...";
241
+ const hasContent = content !== null && content !== undefined && content !== "";
242
+ const isEmptyTyping = isTyping && !hasContent;
243
+ const resolvedVariant = variant ?? (isTyping ? "typing" : role) ?? chatMessageDefaultVariantKey;
244
+ const { text: animatedTypingText, caretVisible } = useTypingMessage(
245
+ typingMessages && typingMessages.length > 0 ? typingMessages : [typingFallback],
246
+ isEmptyTyping
247
+ );
248
+
249
+ if (role === "system") {
250
+ return (
251
+ <div className={cn("my-4 inline-flex", chatMessageVariantClassNames[resolvedVariant], className)}>
252
+ <span className="rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground">
253
+ {content}
254
+ </span>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ const isUser = role === "user";
260
+ const actionLabels = {
261
+ copy: labels?.copy ?? "コピー",
262
+ branch: labels?.branch ?? "分岐",
263
+ raw: labels?.raw ?? "Raw",
264
+ edit: labels?.edit ?? "編集",
265
+ };
266
+ const resolvedContent = isEmptyTyping ? animatedTypingText : content;
267
+ const resolvedCopyValue = copyValue ?? (typeof resolvedContent === "string" ? resolvedContent : undefined);
268
+ const defaultActions: ChatMessageAction[] = isUser
269
+ ? [
270
+ {
271
+ key: "copy",
272
+ label: actionLabels.copy,
273
+ icon: <Copy className="h-3.5 w-3.5" />,
274
+ },
275
+ {
276
+ key: "edit",
277
+ label: actionLabels.edit,
278
+ icon: <Pencil className="h-3.5 w-3.5" />,
279
+ },
280
+ ]
281
+ : [
282
+ {
283
+ key: "copy",
284
+ label: actionLabels.copy,
285
+ icon: <Copy className="h-3.5 w-3.5" />,
286
+ },
287
+ {
288
+ key: "branch",
289
+ label: actionLabels.branch,
290
+ icon: <GitBranch className="h-3.5 w-3.5" />,
291
+ },
292
+ {
293
+ key: "raw",
294
+ label: actionLabels.raw,
295
+ icon: <Code className="h-3.5 w-3.5" />,
296
+ },
297
+ ];
298
+ const resolvedActions = actions ?? defaultActions;
299
+
300
+ const handleAction = React.useCallback(
301
+ (actionKey: ChatMessageActionKey, event: React.MouseEvent<HTMLButtonElement>) => {
302
+ if (actionKey === "copy" && resolvedCopyValue && navigator.clipboard) {
303
+ void navigator.clipboard.writeText(resolvedCopyValue).catch(() => undefined);
304
+ }
305
+ onAction?.(actionKey, event);
306
+ },
307
+ [onAction, resolvedCopyValue]
308
+ );
309
+
310
+ return (
311
+ <div
312
+ className={cn(
313
+ "inline-flex w-full max-w-full gap-4 p-4",
314
+ chatMessageVariantClassNames[resolvedVariant],
315
+ className
316
+ )}
317
+ >
318
+ <div className={cn("shrink-0", isUser ? "mt-1" : "mt-0.5")}>
319
+ <Avatar
320
+ className={cn(
321
+ "h-8 w-8",
322
+ isUser
323
+ ? "ring-2 ring-primary-border"
324
+ : "bg-primary text-primary-foreground"
325
+ )}
326
+ >
327
+ {avatarSrc ? (
328
+ <AvatarImage src={avatarSrc} alt={userName} />
329
+ ) : (
330
+ <AvatarFallback
331
+ className={cn(
332
+ isUser
333
+ ? "bg-background text-muted-foreground"
334
+ : "bg-transparent text-primary-foreground"
335
+ )}
336
+ >
337
+ {isUser ? <User className="h-4 w-4" /> : <Sparkles className="h-4 w-4" />}
338
+ </AvatarFallback>
339
+ )}
340
+ </Avatar>
341
+ </div>
342
+
343
+ <div className={cn("flex min-w-0 flex-col", isUser ? "max-w-[82%] items-end" : "max-w-[min(100%,42rem)] flex-1 items-start")}>
344
+ <div className="mb-1 flex max-w-full items-center gap-2">
345
+ <span className="truncate text-sm font-semibold text-foreground">{userName}</span>
346
+ {timestamp ? (
347
+ <span className="shrink-0 text-xs text-muted-foreground">{timestamp}</span>
348
+ ) : null}
349
+ </div>
350
+
351
+ <div
352
+ className={cn(
353
+ "min-w-0 text-sm leading-relaxed",
354
+ typeof resolvedContent === "string" && "whitespace-pre-wrap",
355
+ isUser
356
+ ? "rounded-2xl rounded-tr-sm border border-border/70 bg-accent/60 px-4 py-3 text-foreground shadow-sm"
357
+ : "w-full text-foreground"
358
+ )}
359
+ >
360
+ {resolvedContent}
361
+ {isTyping ? (
362
+ <span
363
+ aria-hidden="true"
364
+ className={cn(
365
+ "ml-1 inline-block h-4 w-1.5 bg-current align-middle",
366
+ isEmptyTyping && !caretVisible && "opacity-0"
367
+ )}
368
+ />
369
+ ) : null}
370
+ </div>
371
+
372
+ {showActions && !isTyping ? (
373
+ <ChatMessageActions
374
+ actions={resolvedActions}
375
+ align={isUser ? "end" : "start"}
376
+ onAction={handleAction}
377
+ />
378
+ ) : null}
379
+ </div>
380
+ </div>
381
+ );
382
+ }