ai-design-system 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/README.md +307 -0
  2. package/components/ai-elements/actions.tsx +65 -0
  3. package/components/ai-elements/artifact.tsx +147 -0
  4. package/components/ai-elements/branch.tsx +212 -0
  5. package/components/ai-elements/canvas.tsx +24 -0
  6. package/components/ai-elements/chain-of-thought.tsx +228 -0
  7. package/components/ai-elements/code-block.tsx +179 -0
  8. package/components/ai-elements/confirmation.tsx +169 -0
  9. package/components/ai-elements/connection.tsx +28 -0
  10. package/components/ai-elements/context.tsx +408 -0
  11. package/components/ai-elements/controls.tsx +18 -0
  12. package/components/ai-elements/conversation.tsx +97 -0
  13. package/components/ai-elements/edge.tsx +140 -0
  14. package/components/ai-elements/image.tsx +24 -0
  15. package/components/ai-elements/inline-citation.tsx +287 -0
  16. package/components/ai-elements/loader.tsx +96 -0
  17. package/components/ai-elements/message.tsx +80 -0
  18. package/components/ai-elements/node.tsx +71 -0
  19. package/components/ai-elements/open-in-chat.tsx +363 -0
  20. package/components/ai-elements/panel.tsx +15 -0
  21. package/components/ai-elements/plan.tsx +142 -0
  22. package/components/ai-elements/prompt-input.tsx +1352 -0
  23. package/components/ai-elements/queue.tsx +274 -0
  24. package/components/ai-elements/reasoning.tsx +178 -0
  25. package/components/ai-elements/response.tsx +22 -0
  26. package/components/ai-elements/shimmer.tsx +64 -0
  27. package/components/ai-elements/sources.tsx +77 -0
  28. package/components/ai-elements/suggestion.tsx +56 -0
  29. package/components/ai-elements/task.tsx +87 -0
  30. package/components/ai-elements/tool.tsx +179 -0
  31. package/components/ai-elements/toolbar.tsx +16 -0
  32. package/components/ai-elements/web-preview.tsx +263 -0
  33. package/components/blocks/AIConversation/AIConversation.stories.tsx +164 -0
  34. package/components/blocks/AIConversation/AIConversation.tsx +186 -0
  35. package/components/blocks/AIConversation/index.ts +8 -0
  36. package/components/blocks/AppSidebar/AppSidebar.stories.tsx +63 -0
  37. package/components/blocks/AppSidebar/AppSidebar.tsx +87 -0
  38. package/components/blocks/AppSidebar/index.ts +2 -0
  39. package/components/blocks/DocumentEditorWithComments/DocumentEditorWithComments.stories.tsx +341 -0
  40. package/components/blocks/DocumentEditorWithComments/DocumentEditorWithComments.tsx +255 -0
  41. package/components/blocks/DocumentEditorWithComments/index.ts +9 -0
  42. package/components/blocks/FileChangeQueue/FileChangeQueue.stories.tsx +207 -0
  43. package/components/blocks/FileChangeQueue/FileChangeQueue.tsx +143 -0
  44. package/components/blocks/FileChangeQueue/index.ts +7 -0
  45. package/components/blocks/LayoutProvider/LayoutProvider.tsx +34 -0
  46. package/components/blocks/LayoutProvider/index.ts +1 -0
  47. package/components/blocks/index.ts +2 -0
  48. package/components/composites/AgentIndicator/AgentIndicator.stories.tsx +154 -0
  49. package/components/composites/AgentIndicator/AgentIndicator.tsx +102 -0
  50. package/components/composites/AgentIndicator/index.ts +8 -0
  51. package/components/composites/AppHeader/AppHeader.stories.tsx +46 -0
  52. package/components/composites/AppHeader/AppHeader.tsx +24 -0
  53. package/components/composites/AppHeader/index.ts +2 -0
  54. package/components/composites/CommentBox/CommentBox.stories.tsx +192 -0
  55. package/components/composites/CommentBox/CommentBox.tsx +364 -0
  56. package/components/composites/CommentBox/index.ts +8 -0
  57. package/components/composites/Confirmation/Confirmation.stories.tsx +151 -0
  58. package/components/composites/Confirmation/Confirmation.tsx +93 -0
  59. package/components/composites/Confirmation/index.ts +7 -0
  60. package/components/composites/DataTable/DataTable.stories.tsx +35 -0
  61. package/components/composites/DataTable/DataTable.tsx +95 -0
  62. package/components/composites/DataTable/index.ts +2 -0
  63. package/components/composites/DocumentEditor/DocumentEditor.css +106 -0
  64. package/components/composites/DocumentEditor/DocumentEditor.stories.tsx +927 -0
  65. package/components/composites/DocumentEditor/DocumentEditor.tsx +279 -0
  66. package/components/composites/DocumentEditor/index.ts +8 -0
  67. package/components/composites/FileQueue/FileQueue.stories.tsx +175 -0
  68. package/components/composites/FileQueue/FileQueue.tsx +161 -0
  69. package/components/composites/FileQueue/FileStatusBadge.tsx +74 -0
  70. package/components/composites/FileQueue/index.ts +24 -0
  71. package/components/composites/InteractiveChart/InteractiveChart.stories.tsx +49 -0
  72. package/components/composites/InteractiveChart/InteractiveChart.tsx +69 -0
  73. package/components/composites/InteractiveChart/index.ts +2 -0
  74. package/components/composites/ModeToggle/ModeToggle.stories.tsx +212 -0
  75. package/components/composites/ModeToggle/ModeToggle.tsx +100 -0
  76. package/components/composites/ModeToggle/index.ts +7 -0
  77. package/components/composites/NavUser/NavUser.stories.tsx +50 -0
  78. package/components/composites/NavUser/NavUser.tsx +60 -0
  79. package/components/composites/NavUser/index.ts +2 -0
  80. package/components/composites/NavigationList/NavigationList.stories.tsx +46 -0
  81. package/components/composites/NavigationList/NavigationList.tsx +46 -0
  82. package/components/composites/NavigationList/index.ts +2 -0
  83. package/components/composites/OrchestratorMessage/OrchestratorMessage.stories.tsx +188 -0
  84. package/components/composites/OrchestratorMessage/OrchestratorMessage.tsx +72 -0
  85. package/components/composites/OrchestratorMessage/index.ts +8 -0
  86. package/components/composites/PageContainer/PageContainer.stories.tsx +41 -0
  87. package/components/composites/PageContainer/PageContainer.tsx +24 -0
  88. package/components/composites/PageContainer/index.ts +2 -0
  89. package/components/composites/PromptInput/PromptInput.stories.tsx +200 -0
  90. package/components/composites/PromptInput/PromptInput.tsx +129 -0
  91. package/components/composites/PromptInput/index.ts +8 -0
  92. package/components/composites/SpecialistMessage/SpecialistMessage.stories.tsx +286 -0
  93. package/components/composites/SpecialistMessage/SpecialistMessage.tsx +107 -0
  94. package/components/composites/SpecialistMessage/index.ts +8 -0
  95. package/components/composites/StatsCard/StatsCard.stories.tsx +76 -0
  96. package/components/composites/StatsCard/StatsCard.tsx +81 -0
  97. package/components/composites/StatsCard/index.ts +2 -0
  98. package/components/composites/TablePagination/TablePagination.stories.tsx +38 -0
  99. package/components/composites/TablePagination/TablePagination.tsx +119 -0
  100. package/components/composites/TablePagination/index.ts +2 -0
  101. package/components/composites/TableToolbar/TableToolbar.stories.tsx +60 -0
  102. package/components/composites/TableToolbar/TableToolbar.tsx +66 -0
  103. package/components/composites/TableToolbar/index.ts +2 -0
  104. package/components/composites/ThemeSelector/ThemeSelector.stories.tsx +48 -0
  105. package/components/composites/ThemeSelector/ThemeSelector.tsx +79 -0
  106. package/components/composites/ThemeSelector/index.ts +2 -0
  107. package/components/composites/ToolCallDisplay/ToolCallDisplay.stories.tsx +49 -0
  108. package/components/composites/ToolCallDisplay/ToolCallDisplay.tsx +108 -0
  109. package/components/composites/ToolCallDisplay/index.ts +8 -0
  110. package/components/composites/UserMessage/UserMessage.stories.tsx +59 -0
  111. package/components/composites/UserMessage/UserMessage.tsx +52 -0
  112. package/components/composites/UserMessage/index.ts +8 -0
  113. package/components/composites/index.ts +90 -0
  114. package/components/features/AIDocEditor/AIDocEditor.behaviors.stories.tsx +451 -0
  115. package/components/features/AIDocEditor/AIDocEditor.mocks.ts +96 -0
  116. package/components/features/AIDocEditor/AIDocEditor.stories.tsx +140 -0
  117. package/components/features/AIDocEditor/AIDocEditor.tsx +130 -0
  118. package/components/features/AIDocEditor/index.ts +8 -0
  119. package/components/features/AIDocEditor/useAIDocEditor.d.ts +97 -0
  120. package/components/features/AIDocEditor/useAIDocEditor.mock.ts +83 -0
  121. package/components/features/PageLayout/PageLayout.behaviors.stories.tsx +119 -0
  122. package/components/features/PageLayout/PageLayout.mocks.ts +27 -0
  123. package/components/features/PageLayout/PageLayout.stories.tsx +142 -0
  124. package/components/features/PageLayout/PageLayout.tsx +94 -0
  125. package/components/features/PageLayout/index.ts +4 -0
  126. package/components/features/PageLayout/usePageLayout.d.ts +24 -0
  127. package/components/features/PageLayout/usePageLayout.mock.ts +19 -0
  128. package/components/features/RefinementPanel/README.md +189 -0
  129. package/components/features/RefinementPanel/RefinementPanel.behaviors.stories.tsx +475 -0
  130. package/components/features/RefinementPanel/RefinementPanel.mocks.ts +131 -0
  131. package/components/features/RefinementPanel/RefinementPanel.stories.tsx +141 -0
  132. package/components/features/RefinementPanel/RefinementPanel.tsx +160 -0
  133. package/components/features/RefinementPanel/index.ts +25 -0
  134. package/components/features/RefinementPanel/useRefinementPanel.d.ts +74 -0
  135. package/components/features/RefinementPanel/useRefinementPanel.mock.ts +121 -0
  136. package/components/features/SpecNavigator/SpecNavigator.behaviors.stories.tsx +379 -0
  137. package/components/features/SpecNavigator/SpecNavigator.mocks.ts +131 -0
  138. package/components/features/SpecNavigator/SpecNavigator.stories.tsx +122 -0
  139. package/components/features/SpecNavigator/SpecNavigator.tsx +43 -0
  140. package/components/features/SpecNavigator/index.ts +2 -0
  141. package/components/features/SpecNavigator/useSpecNavigator.d.ts +122 -0
  142. package/components/features/SpecNavigator/useSpecNavigator.mock.ts +93 -0
  143. package/components/features/index.ts +18 -0
  144. package/components/index.ts +14 -0
  145. package/components/primitives/Accordion/Accordion.stories.tsx +87 -0
  146. package/components/primitives/Accordion/Accordion.tsx +66 -0
  147. package/components/primitives/Accordion/index.ts +13 -0
  148. package/components/primitives/Alert/Alert.stories.tsx +422 -0
  149. package/components/primitives/Alert/Alert.tsx +61 -0
  150. package/components/primitives/Alert/index.ts +8 -0
  151. package/components/primitives/AlertDialog/AlertDialog.stories.tsx +367 -0
  152. package/components/primitives/AlertDialog/AlertDialog.tsx +182 -0
  153. package/components/primitives/AlertDialog/index.ts +25 -0
  154. package/components/primitives/Avatar/Avatar.stories.tsx +321 -0
  155. package/components/primitives/Avatar/Avatar.tsx +63 -0
  156. package/components/primitives/Avatar/index.ts +8 -0
  157. package/components/primitives/Badge/Badge.stories.tsx +74 -0
  158. package/components/primitives/Badge/Badge.tsx +49 -0
  159. package/components/primitives/Badge/index.ts +2 -0
  160. package/components/primitives/Button/Button.stories.tsx +445 -0
  161. package/components/primitives/Button/Button.tsx +89 -0
  162. package/components/primitives/Button/index.ts +7 -0
  163. package/components/primitives/Card/Card.stories.tsx +831 -0
  164. package/components/primitives/Card/Card.tsx +242 -0
  165. package/components/primitives/Card/index.ts +30 -0
  166. package/components/primitives/Carousel/Carousel.stories.tsx +32 -0
  167. package/components/primitives/Carousel/Carousel.tsx +63 -0
  168. package/components/primitives/Carousel/index.ts +13 -0
  169. package/components/primitives/Chart/Chart.stories.tsx +346 -0
  170. package/components/primitives/Chart/Chart.tsx +117 -0
  171. package/components/primitives/Chart/index.ts +20 -0
  172. package/components/primitives/Checkbox/Checkbox.stories.tsx +87 -0
  173. package/components/primitives/Checkbox/Checkbox.tsx +38 -0
  174. package/components/primitives/Checkbox/index.ts +2 -0
  175. package/components/primitives/Collapsible/Collapsible.stories.tsx +38 -0
  176. package/components/primitives/Collapsible/Collapsible.tsx +39 -0
  177. package/components/primitives/Collapsible/index.ts +8 -0
  178. package/components/primitives/Command/Command.stories.tsx +150 -0
  179. package/components/primitives/Command/Command.tsx +147 -0
  180. package/components/primitives/Command/index.ts +20 -0
  181. package/components/primitives/Dialog/Dialog.stories.tsx +390 -0
  182. package/components/primitives/Dialog/Dialog.tsx +140 -0
  183. package/components/primitives/Dialog/index.ts +22 -0
  184. package/components/primitives/Drawer/Drawer.stories.tsx +327 -0
  185. package/components/primitives/Drawer/Drawer.tsx +208 -0
  186. package/components/primitives/Drawer/index.ts +27 -0
  187. package/components/primitives/DropdownMenu/DropdownMenu.stories.tsx +150 -0
  188. package/components/primitives/DropdownMenu/DropdownMenu.tsx +73 -0
  189. package/components/primitives/DropdownMenu/index.ts +1 -0
  190. package/components/primitives/HoverCard/HoverCard.stories.tsx +26 -0
  191. package/components/primitives/HoverCard/HoverCard.tsx +39 -0
  192. package/components/primitives/HoverCard/index.ts +8 -0
  193. package/components/primitives/Icon/Icon.stories.tsx +281 -0
  194. package/components/primitives/Icon/Icon.tsx +87 -0
  195. package/components/primitives/Icon/index.ts +8 -0
  196. package/components/primitives/Input/Input.stories.tsx +370 -0
  197. package/components/primitives/Input/Input.tsx +88 -0
  198. package/components/primitives/Input/index.ts +7 -0
  199. package/components/primitives/InputGroup/InputGroup.stories.tsx +40 -0
  200. package/components/primitives/InputGroup/InputGroup.tsx +72 -0
  201. package/components/primitives/InputGroup/index.ts +14 -0
  202. package/components/primitives/Label/Label.stories.tsx +227 -0
  203. package/components/primitives/Label/Label.tsx +53 -0
  204. package/components/primitives/Label/index.ts +7 -0
  205. package/components/primitives/Popover/Popover.stories.tsx +42 -0
  206. package/components/primitives/Popover/Popover.tsx +107 -0
  207. package/components/primitives/Popover/index.ts +2 -0
  208. package/components/primitives/Progress/Progress.stories.tsx +340 -0
  209. package/components/primitives/Progress/Progress.tsx +31 -0
  210. package/components/primitives/Progress/index.ts +1 -0
  211. package/components/primitives/ScrollArea/ScrollArea.stories.tsx +26 -0
  212. package/components/primitives/ScrollArea/ScrollArea.tsx +28 -0
  213. package/components/primitives/ScrollArea/index.ts +6 -0
  214. package/components/primitives/Select/Select.stories.tsx +288 -0
  215. package/components/primitives/Select/Select.tsx +162 -0
  216. package/components/primitives/Select/index.ts +22 -0
  217. package/components/primitives/Separator/Separator.stories.tsx +264 -0
  218. package/components/primitives/Separator/Separator.tsx +48 -0
  219. package/components/primitives/Separator/index.ts +7 -0
  220. package/components/primitives/Sidebar/Sidebar.stories.tsx +358 -0
  221. package/components/primitives/Sidebar/Sidebar.tsx +317 -0
  222. package/components/primitives/Sidebar/index.ts +41 -0
  223. package/components/primitives/Table/Table.stories.tsx +389 -0
  224. package/components/primitives/Table/Table.tsx +191 -0
  225. package/components/primitives/Table/index.ts +26 -0
  226. package/components/primitives/Tabs/Tabs.stories.tsx +129 -0
  227. package/components/primitives/Tabs/Tabs.tsx +70 -0
  228. package/components/primitives/Tabs/index.ts +13 -0
  229. package/components/primitives/Textarea/Textarea.stories.tsx +358 -0
  230. package/components/primitives/Textarea/Textarea.tsx +91 -0
  231. package/components/primitives/Textarea/index.ts +7 -0
  232. package/components/primitives/ToggleGroup/ToggleGroup.stories.tsx +87 -0
  233. package/components/primitives/ToggleGroup/ToggleGroup.tsx +52 -0
  234. package/components/primitives/ToggleGroup/index.ts +6 -0
  235. package/components/primitives/Tooltip/Tooltip.stories.tsx +336 -0
  236. package/components/primitives/Tooltip/Tooltip.tsx +78 -0
  237. package/components/primitives/Tooltip/index.ts +10 -0
  238. package/components/primitives/index.ts +34 -0
  239. package/components/ui/accordion.tsx +66 -0
  240. package/components/ui/alert-dialog.tsx +157 -0
  241. package/components/ui/alert.tsx +66 -0
  242. package/components/ui/avatar.tsx +53 -0
  243. package/components/ui/badge.tsx +46 -0
  244. package/components/ui/button.tsx +60 -0
  245. package/components/ui/card.tsx +117 -0
  246. package/components/ui/carousel.tsx +241 -0
  247. package/components/ui/chart.tsx +334 -0
  248. package/components/ui/checkbox.tsx +32 -0
  249. package/components/ui/collapsible.tsx +33 -0
  250. package/components/ui/command.tsx +184 -0
  251. package/components/ui/dialog.tsx +143 -0
  252. package/components/ui/drawer.tsx +118 -0
  253. package/components/ui/dropdown-menu.tsx +257 -0
  254. package/components/ui/hover-card.tsx +44 -0
  255. package/components/ui/input-group.tsx +170 -0
  256. package/components/ui/input.tsx +48 -0
  257. package/components/ui/label.tsx +26 -0
  258. package/components/ui/popover.tsx +33 -0
  259. package/components/ui/progress.tsx +31 -0
  260. package/components/ui/scroll-area.tsx +58 -0
  261. package/components/ui/select.tsx +187 -0
  262. package/components/ui/separator.tsx +31 -0
  263. package/components/ui/sidebar.tsx +577 -0
  264. package/components/ui/table.tsx +120 -0
  265. package/components/ui/tabs.tsx +66 -0
  266. package/components/ui/textarea.tsx +46 -0
  267. package/components/ui/toggle-group.tsx +83 -0
  268. package/components/ui/toggle.tsx +47 -0
  269. package/components/ui/tooltip.tsx +61 -0
  270. package/dist/index.cjs +7389 -0
  271. package/dist/index.cjs.map +1 -0
  272. package/dist/index.css +75 -0
  273. package/dist/index.css.map +1 -0
  274. package/dist/index.js +7160 -0
  275. package/dist/index.js.map +1 -0
  276. package/hooks/useAIDocReviewer.d.ts +0 -0
  277. package/lib/utils.ts +6 -0
  278. package/package.json +140 -0
  279. package/tokens/color/base.json +14 -0
  280. package/tokens/color/dark.json +40 -0
  281. package/tokens/color/green.json +21 -0
  282. package/tokens/color/light.json +52 -0
  283. package/tokens/color/neutral.json +20 -0
  284. package/tokens/color/violet.json +21 -0
  285. package/tokens/spacing.json +22 -0
  286. package/utils/ai-editor/format-date.ts +41 -0
  287. package/utils/ai-editor/index.ts +22 -0
  288. package/utils/ai-editor/type-guards.ts +72 -0
  289. package/utils/ai-editor/validation.ts +130 -0
  290. package/utils/editor-annotations.ts +122 -0
@@ -0,0 +1,279 @@
1
+ /**
2
+ * DocumentEditor Block
3
+ *
4
+ * Tiptap-based read-only editor with annotation overlays
5
+ * Block layer: uses primitives and Tiptap extensions only
6
+ */
7
+
8
+ import React, { useEffect } from 'react'
9
+ import './DocumentEditor.css'
10
+ import { useEditor, EditorContent } from '@tiptap/react'
11
+ import StarterKit from '@tiptap/starter-kit'
12
+ import Highlight from '@tiptap/extension-highlight'
13
+ import { Markdown } from '@tiptap/markdown'
14
+ import { cn } from '@/lib/utils'
15
+ import {
16
+ CommentMark,
17
+ PendingCommentMark,
18
+ SuggestionInsertMark,
19
+ SuggestionDeleteMark,
20
+ SuggestionModifyMark,
21
+ BlockAdditionNode,
22
+ } from '@/extensions/tiptap'
23
+ import { applyAnnotationsToEditor } from '@/utils/editor-annotations'
24
+ import type { DocumentEditorProps } from '@/types/ai-editor'
25
+
26
+ export const DocumentEditor = React.memo<DocumentEditorProps>(
27
+ ({
28
+ content,
29
+ format = 'json',
30
+ annotations,
31
+ selectedAnnotationId,
32
+ hoveredAnnotationId,
33
+ pendingCommentRange,
34
+ onTextSelect,
35
+ onAnnotationClick,
36
+ onAnnotationHover,
37
+ readOnly = true,
38
+ className,
39
+ }) => {
40
+ const editor = useEditor({
41
+ extensions: [
42
+ StarterKit,
43
+ Highlight,
44
+ Markdown,
45
+ CommentMark,
46
+ PendingCommentMark,
47
+ SuggestionInsertMark,
48
+ SuggestionDeleteMark,
49
+ SuggestionModifyMark,
50
+ BlockAdditionNode,
51
+ ],
52
+ content,
53
+ // Tell Tiptap what format the initial content is in
54
+ contentType: format === 'markdown' ? 'markdown' : 'json',
55
+ editable: !readOnly,
56
+ editorProps: {
57
+ attributes: {
58
+ class: cn(
59
+ 'prose max-w-none focus:outline-none',
60
+ 'min-h-[200px] p-4',
61
+ className
62
+ ),
63
+ },
64
+ },
65
+ })
66
+
67
+ // Update content when it changes
68
+ useEffect(() => {
69
+ if (editor && content) {
70
+ // Handle markdown format
71
+ if (format === 'markdown' && typeof content === 'string') {
72
+ // Use contentType option to tell Tiptap to parse as markdown
73
+ editor.commands.setContent(content, { contentType: 'markdown' })
74
+ }
75
+ // Handle JSON format
76
+ else {
77
+ const currentContent = editor.getJSON()
78
+ if (JSON.stringify(currentContent) !== JSON.stringify(content)) {
79
+ editor.commands.setContent(content)
80
+ }
81
+ }
82
+ }
83
+ }, [editor, content, format])
84
+
85
+ // Apply annotations when they change
86
+ useEffect(() => {
87
+ if (editor) {
88
+ applyAnnotationsToEditor(editor, annotations, selectedAnnotationId, hoveredAnnotationId)
89
+ }
90
+ }, [editor, annotations, selectedAnnotationId, hoveredAnnotationId])
91
+
92
+ // Apply pending comment highlight (yellow) when user selects text
93
+ useEffect(() => {
94
+ if (!editor) return
95
+
96
+ if (pendingCommentRange) {
97
+ const { from, to } = pendingCommentRange
98
+
99
+ // Apply pending comment mark to show yellow highlight
100
+ // Use transaction to avoid triggering selection events
101
+ const { state, view } = editor
102
+ const tr = state.tr
103
+ tr.addMark(from, to, state.schema.marks.pendingComment.create({ pending: true }))
104
+ view.dispatch(tr)
105
+ } else {
106
+ // Clear all pending comment marks
107
+ const { state, view } = editor
108
+ const tr = state.tr
109
+
110
+ state.doc.descendants((node, pos) => {
111
+ node.marks.forEach((mark) => {
112
+ if (mark.type.name === 'pendingComment') {
113
+ tr.removeMark(pos, pos + node.nodeSize, mark.type)
114
+ }
115
+ })
116
+ })
117
+
118
+ if (tr.docChanged) {
119
+ view.dispatch(tr)
120
+ }
121
+ }
122
+ }, [editor, pendingCommentRange])
123
+
124
+ // Handle text selection events - trigger on mouseup (selection complete)
125
+ useEffect(() => {
126
+ if (!editor || !onTextSelect) return
127
+
128
+ const handleMouseUp = (event: MouseEvent) => {
129
+ // Check if the click was on an existing annotation
130
+ // If so, skip text selection handling (annotation click handler will handle it)
131
+ const target = event.target as HTMLElement
132
+ const annotationEl = target.closest(
133
+ '[data-comment-id], [data-suggestion-id], [data-addition-id]'
134
+ )
135
+
136
+ if (annotationEl) {
137
+ // This is a click on an existing annotation, not a text selection
138
+ // The annotation click handler will handle this
139
+ return
140
+ }
141
+
142
+ // Small delay to ensure selection is finalized
143
+ setTimeout(() => {
144
+ let { from, to, empty } = editor.state.selection
145
+ if (!empty) {
146
+ let text = editor.state.doc.textBetween(from, to)
147
+
148
+ // Trim leading whitespace and adjust range
149
+ const leadingWhitespace = text.match(/^\s+/)
150
+ if (leadingWhitespace) {
151
+ from += leadingWhitespace[0].length
152
+ text = text.trimStart()
153
+ }
154
+
155
+ // Trim trailing whitespace and adjust range
156
+ const trailingWhitespace = text.match(/\s+$/)
157
+ if (trailingWhitespace) {
158
+ to -= trailingWhitespace[0].length
159
+ text = text.trimEnd()
160
+ }
161
+
162
+ // Skip if only whitespace was selected
163
+ if (!text.trim()) return
164
+
165
+ // Log selected text for API usage
166
+ console.log('Selected text:', text)
167
+ console.log('Selection range:', { from, to })
168
+
169
+ // Calculate position for CommentBox based on selection
170
+ const selection = window.getSelection()
171
+ if (selection && selection.rangeCount > 0) {
172
+ const range = selection.getRangeAt(0)
173
+ const rect = range.getBoundingClientRect()
174
+ const position = {
175
+ x: rect.left,
176
+ y: rect.bottom + 8, // 8px below selection
177
+ }
178
+
179
+ // Clear the browser selection
180
+ selection.removeAllRanges()
181
+
182
+ onTextSelect({ from, to }, text, position)
183
+ } else {
184
+ onTextSelect({ from, to }, text)
185
+ }
186
+ }
187
+ }, 10)
188
+ }
189
+
190
+ const editorElement = editor.view.dom
191
+ editorElement.addEventListener('mouseup', handleMouseUp)
192
+
193
+ return () => {
194
+ editorElement.removeEventListener('mouseup', handleMouseUp)
195
+ }
196
+ }, [editor, onTextSelect])
197
+
198
+ // Handle annotation click events
199
+ useEffect(() => {
200
+ if (!editor || !onAnnotationClick) return
201
+
202
+ const handleClick = (event: MouseEvent) => {
203
+ const target = event.target as HTMLElement
204
+ const annotationEl = target.closest(
205
+ '[data-comment-id], [data-suggestion-id], [data-addition-id]'
206
+ )
207
+
208
+ if (annotationEl) {
209
+ const id =
210
+ annotationEl.getAttribute('data-comment-id') ||
211
+ annotationEl.getAttribute('data-suggestion-id') ||
212
+ annotationEl.getAttribute('data-addition-id')
213
+
214
+ if (id) {
215
+ // Calculate position based on the clicked element
216
+ // Position below the annotation, similar to Google Docs
217
+ const rect = annotationEl.getBoundingClientRect()
218
+ const position = {
219
+ x: rect.left, // Align with left edge of annotation
220
+ y: rect.bottom + 8, // 8px below the annotation
221
+ }
222
+ onAnnotationClick(id, position)
223
+ }
224
+ }
225
+ }
226
+
227
+ const editorElement = editor.view.dom
228
+ editorElement.addEventListener('click', handleClick)
229
+
230
+ return () => {
231
+ editorElement.removeEventListener('click', handleClick)
232
+ }
233
+ }, [editor, onAnnotationClick])
234
+
235
+ // Handle annotation hover events
236
+ useEffect(() => {
237
+ if (!editor || !onAnnotationHover) return
238
+
239
+ const handleMouseOver = (event: MouseEvent) => {
240
+ const target = event.target as HTMLElement
241
+ const annotationEl = target.closest(
242
+ '[data-comment-id], [data-suggestion-id], [data-addition-id]'
243
+ )
244
+
245
+ if (annotationEl) {
246
+ const id =
247
+ annotationEl.getAttribute('data-comment-id') ||
248
+ annotationEl.getAttribute('data-suggestion-id') ||
249
+ annotationEl.getAttribute('data-addition-id')
250
+
251
+ if (id) {
252
+ onAnnotationHover(id)
253
+ }
254
+ } else {
255
+ onAnnotationHover(null)
256
+ }
257
+ }
258
+
259
+ const editorElement = editor.view.dom
260
+ editorElement.addEventListener('mouseover', handleMouseOver)
261
+
262
+ return () => {
263
+ editorElement.removeEventListener('mouseover', handleMouseOver)
264
+ }
265
+ }, [editor, onAnnotationHover])
266
+
267
+ if (!editor) {
268
+ return null
269
+ }
270
+
271
+ return (
272
+ <div className={cn('document-editor-wrapper', className)}>
273
+ <EditorContent editor={editor} />
274
+ </div>
275
+ )
276
+ }
277
+ )
278
+
279
+ DocumentEditor.displayName = 'DocumentEditor'
@@ -0,0 +1,8 @@
1
+ /**
2
+ * DocumentEditor Block
3
+ *
4
+ * Exports the DocumentEditor component
5
+ */
6
+
7
+ export { DocumentEditor } from './DocumentEditor'
8
+ export type { DocumentEditorProps } from '@/types/ai-editor'
@@ -0,0 +1,175 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { FileQueue, type FileGroup } from "./FileQueue";
4
+
5
+ const meta: Meta<typeof FileQueue> = {
6
+ title: "Blocks/FileQueue",
7
+ component: FileQueue,
8
+ parameters: {
9
+ layout: "padded",
10
+ },
11
+ argTypes: {
12
+ groups: {
13
+ description: "Array of file groups to display with customizable titles, icons, and files",
14
+ },
15
+ selectedFileId: {
16
+ description: "ID of currently selected file for visual highlighting",
17
+ },
18
+ onFileSelect: {
19
+ description: "Callback function invoked when a file is selected",
20
+ },
21
+ className: {
22
+ description: "Additional CSS classes for custom styling",
23
+ },
24
+ },
25
+ } satisfies Meta<typeof FileQueue>;
26
+
27
+ export default meta;
28
+ type Story = StoryObj<typeof meta>;
29
+
30
+ // Sample file groups for stories
31
+ const sampleGroups: FileGroup[] = [
32
+ {
33
+ id: "requirements",
34
+ title: "Requirements",
35
+ icon: "file-text",
36
+ iconColor: "text-blue-600 dark:text-blue-500",
37
+ files: [
38
+ { id: "req1", name: "requirements.md", path: ".kiro/specs/feature/" },
39
+ { id: "req2", name: "user-stories.md", path: ".kiro/specs/feature/" },
40
+ { id: "req3", name: "acceptance-criteria.md", path: ".kiro/specs/feature/" },
41
+ ],
42
+ defaultOpen: true,
43
+ },
44
+ {
45
+ id: "design",
46
+ title: "Design",
47
+ icon: "layout",
48
+ iconColor: "text-purple-600 dark:text-purple-500",
49
+ files: [
50
+ { id: "design1", name: "design.md", path: ".kiro/specs/feature/" },
51
+ { id: "design2", name: "architecture.md", path: ".kiro/specs/feature/" },
52
+ ],
53
+ defaultOpen: false,
54
+ },
55
+ {
56
+ id: "tasks",
57
+ title: "Tasks",
58
+ icon: "check-square",
59
+ iconColor: "text-green-600 dark:text-green-500",
60
+ files: [
61
+ { id: "task1", name: "tasks.md", path: ".kiro/specs/feature/" },
62
+ { id: "task2", name: "implementation-plan.md", path: ".kiro/specs/feature/" },
63
+ ],
64
+ defaultOpen: false,
65
+ },
66
+ ];
67
+
68
+ /**
69
+ * Default story demonstrating multiple file groups with different icons and colors.
70
+ * Shows the generic grouping capability with customizable group properties.
71
+ */
72
+ export const Default: Story = {
73
+ args: {
74
+ groups: sampleGroups,
75
+ },
76
+ };
77
+
78
+ /**
79
+ * Empty state with no file groups.
80
+ * Demonstrates how the component handles an empty groups array.
81
+ */
82
+ export const Empty: Story = {
83
+ args: {
84
+ groups: [],
85
+ },
86
+ };
87
+
88
+ /**
89
+ * Single group with multiple files.
90
+ * Shows the component with only one file group, useful for focused displays.
91
+ */
92
+ export const SingleGroup: Story = {
93
+ args: {
94
+ groups: [
95
+ {
96
+ id: "components",
97
+ title: "Components",
98
+ icon: "box",
99
+ iconColor: "text-orange-600 dark:text-orange-500",
100
+ files: [
101
+ { id: "comp1", name: "Button.tsx", path: "src/components/primitives/" },
102
+ { id: "comp2", name: "Input.tsx", path: "src/components/primitives/" },
103
+ { id: "comp3", name: "Card.tsx", path: "src/components/primitives/" },
104
+ ],
105
+ defaultOpen: true,
106
+ },
107
+ ],
108
+ },
109
+ };
110
+
111
+ /**
112
+ * Multiple groups demonstrating various use cases.
113
+ * Shows different group configurations including groups without icons.
114
+ */
115
+ export const MultipleGroups: Story = {
116
+ args: {
117
+ groups: [
118
+ {
119
+ id: "modified",
120
+ title: "Modified Files",
121
+ icon: "file-text",
122
+ iconColor: "text-blue-600 dark:text-blue-500",
123
+ files: [
124
+ { id: "mod1", name: "Button.tsx", path: "src/components/" },
125
+ { id: "mod2", name: "Input.tsx", path: "src/components/" },
126
+ ],
127
+ defaultOpen: true,
128
+ },
129
+ {
130
+ id: "created",
131
+ title: "Created Files",
132
+ icon: "plus",
133
+ iconColor: "text-green-600 dark:text-green-500",
134
+ files: [
135
+ { id: "new1", name: "Icon.tsx", path: "src/components/" },
136
+ { id: "new2", name: "Badge.tsx", path: "src/components/" },
137
+ ],
138
+ defaultOpen: false,
139
+ },
140
+ {
141
+ id: "documentation",
142
+ title: "Documentation",
143
+ files: [
144
+ { id: "doc1", name: "README.md" },
145
+ { id: "doc2", name: "CONTRIBUTING.md" },
146
+ ],
147
+ defaultOpen: false,
148
+ },
149
+ ],
150
+ },
151
+ };
152
+
153
+ /**
154
+ * File selection with interactive state management.
155
+ * Demonstrates the selection functionality with visual highlighting and click handling.
156
+ * Uses React state to manage the selected file ID.
157
+ */
158
+ export const WithSelection: Story = {
159
+ render: () => {
160
+ const [selectedFileId, setSelectedFileId] = useState<string>("req1");
161
+
162
+ return (
163
+ <div className="space-y-4">
164
+ <div className="text-sm text-muted-foreground">
165
+ Selected file: <span className="font-mono">{selectedFileId}</span>
166
+ </div>
167
+ <FileQueue
168
+ groups={sampleGroups}
169
+ selectedFileId={selectedFileId}
170
+ onFileSelect={setSelectedFileId}
171
+ />
172
+ </div>
173
+ );
174
+ },
175
+ };
@@ -0,0 +1,161 @@
1
+ import * as React from "react";
2
+ import {
3
+ Queue,
4
+ QueueSection,
5
+ QueueSectionTrigger,
6
+ QueueSectionLabel,
7
+ QueueSectionContent,
8
+ QueueList,
9
+ QueueItem,
10
+ QueueItemContent,
11
+ } from "@/components/ai-elements/queue";
12
+ import { Icon } from "@/components/primitives/Icon";
13
+ import { cn } from "@/lib/utils";
14
+
15
+ /**
16
+ * FileQueue Block
17
+ *
18
+ * Generic block component for displaying files organized into collapsible groups.
19
+ * Supports optional file selection with visual feedback and keyboard navigation.
20
+ * Uses Queue AI element for structure.
21
+ */
22
+
23
+ /**
24
+ * File item structure
25
+ */
26
+ export interface FileItem {
27
+ /** Unique identifier for the file */
28
+ id: string;
29
+ /** File name to display */
30
+ name: string;
31
+ /** Optional file path to display */
32
+ path?: string;
33
+ }
34
+
35
+ /**
36
+ * File group structure
37
+ */
38
+ export interface FileGroup {
39
+ /** Unique identifier for the group */
40
+ id: string;
41
+ /** Display title for the group */
42
+ title: string;
43
+ /** Optional icon name from icon registry */
44
+ icon?: string;
45
+ /** Optional icon color class */
46
+ iconColor?: string;
47
+ /** Array of files in this group */
48
+ files: FileItem[];
49
+ /** Whether group should be open by default */
50
+ defaultOpen?: boolean;
51
+ }
52
+
53
+ /**
54
+ * FileQueue component props
55
+ */
56
+ export interface FileQueueProps {
57
+ /** Array of file groups to display */
58
+ groups: FileGroup[];
59
+ /** ID of currently selected file */
60
+ selectedFileId?: string;
61
+ /** Callback when a file is selected */
62
+ onFileSelect?: (fileId: string) => void;
63
+ /** Additional CSS classes */
64
+ className?: string;
65
+ }
66
+
67
+ /**
68
+ * FileQueue component - displays files organized into collapsible groups
69
+ */
70
+ export const FileQueue = React.memo<FileQueueProps>(
71
+ ({ groups, selectedFileId, onFileSelect, className }) => {
72
+ const handleFileClick = React.useCallback(
73
+ (fileId: string) => {
74
+ if (onFileSelect) {
75
+ onFileSelect(fileId);
76
+ }
77
+ },
78
+ [onFileSelect]
79
+ );
80
+
81
+ const handleKeyDown = React.useCallback(
82
+ (e: React.KeyboardEvent, fileId: string) => {
83
+ if (onFileSelect && (e.key === "Enter" || e.key === " ")) {
84
+ e.preventDefault();
85
+ handleFileClick(fileId);
86
+ }
87
+ },
88
+ [onFileSelect, handleFileClick]
89
+ );
90
+
91
+ return (
92
+ <Queue className={cn("w-full", className)}>
93
+ {groups.map((group) => {
94
+ return (
95
+ <QueueSection key={group.id} defaultOpen={group.defaultOpen}>
96
+ <QueueSectionTrigger>
97
+ <QueueSectionLabel
98
+ count={group.files.length}
99
+ label={group.title}
100
+ icon={
101
+ group.icon ? (
102
+ <Icon
103
+ name={group.icon}
104
+ size="sm"
105
+ className={group.iconColor}
106
+ />
107
+ ) : undefined
108
+ }
109
+ />
110
+ </QueueSectionTrigger>
111
+ <QueueSectionContent>
112
+ <QueueList className="max-h-[300px]">
113
+ {group.files.length === 0 ? (
114
+ <div className="px-4 py-3 text-sm text-muted-foreground italic">
115
+ No files yet
116
+ </div>
117
+ ) : (
118
+ group.files.map((file) => {
119
+ const isSelected = selectedFileId === file.id;
120
+ const isClickable = !!onFileSelect;
121
+
122
+ return (
123
+ <QueueItem
124
+ key={file.id}
125
+ className={cn(
126
+ isClickable && "cursor-pointer",
127
+ isSelected && "bg-accent"
128
+ )}
129
+ onClick={() => isClickable && handleFileClick(file.id)}
130
+ role={isClickable ? "button" : undefined}
131
+ tabIndex={isClickable ? 0 : undefined}
132
+ onKeyDown={(e) => handleKeyDown(e, file.id)}
133
+ aria-selected={isClickable ? isSelected : undefined}
134
+ >
135
+ <div className="flex items-start gap-2">
136
+ <Icon name="file" size="sm" className="mt-0.5" />
137
+ <QueueItemContent>
138
+ {file.name}
139
+ {file.path && (
140
+ <span className="text-muted-foreground/70">
141
+ {" "}
142
+ • {file.path}
143
+ </span>
144
+ )}
145
+ </QueueItemContent>
146
+ </div>
147
+ </QueueItem>
148
+ );
149
+ })
150
+ )}
151
+ </QueueList>
152
+ </QueueSectionContent>
153
+ </QueueSection>
154
+ );
155
+ })}
156
+ </Queue>
157
+ );
158
+ }
159
+ );
160
+
161
+ FileQueue.displayName = "FileQueue";
@@ -0,0 +1,74 @@
1
+ import * as React from "react";
2
+ import { Badge } from "@/components/primitives/Badge";
3
+ import { Icon } from "@/components/primitives/Icon";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ /**
7
+ * FileStatusBadge
8
+ *
9
+ * Displays file change status with color-coded badge and icon.
10
+ * Adapted from Tool AI element's status badge pattern.
11
+ */
12
+
13
+ export type FileStatus = "pending" | "modified" | "created" | "deleted";
14
+
15
+ export interface FileStatusBadgeProps {
16
+ status: FileStatus;
17
+ className?: string;
18
+ }
19
+
20
+ const statusConfig: Record<
21
+ FileStatus,
22
+ {
23
+ label: string;
24
+ icon: string;
25
+ colorClass: string;
26
+ }
27
+ > = {
28
+ pending: {
29
+ label: "Pending",
30
+ icon: "loader-2",
31
+ colorClass: "text-yellow-600 dark:text-yellow-500",
32
+ },
33
+ modified: {
34
+ label: "Modified",
35
+ icon: "file-text",
36
+ colorClass: "text-blue-600 dark:text-blue-500",
37
+ },
38
+ created: {
39
+ label: "Created",
40
+ icon: "plus",
41
+ colorClass: "text-green-600 dark:text-green-500",
42
+ },
43
+ deleted: {
44
+ label: "Deleted",
45
+ icon: "x",
46
+ colorClass: "text-red-600 dark:text-red-500",
47
+ },
48
+ };
49
+
50
+ /**
51
+ * FileStatusBadge component - displays file change status
52
+ */
53
+ export const FileStatusBadge = React.memo<FileStatusBadgeProps>(
54
+ ({ status, className }) => {
55
+ const config = statusConfig[status];
56
+
57
+ return (
58
+ <Badge
59
+ variant="secondary"
60
+ className={cn("gap-1.5 rounded-full text-xs", className)}
61
+ >
62
+ <Icon
63
+ name={config.icon}
64
+ size="xs"
65
+ className={config.colorClass}
66
+ aria-hidden="true"
67
+ />
68
+ <span className={config.colorClass}>{config.label}</span>
69
+ </Badge>
70
+ );
71
+ }
72
+ );
73
+
74
+ FileStatusBadge.displayName = "FileStatusBadge";