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.
- package/README.md +307 -0
- package/components/ai-elements/actions.tsx +65 -0
- package/components/ai-elements/artifact.tsx +147 -0
- package/components/ai-elements/branch.tsx +212 -0
- package/components/ai-elements/canvas.tsx +24 -0
- package/components/ai-elements/chain-of-thought.tsx +228 -0
- package/components/ai-elements/code-block.tsx +179 -0
- package/components/ai-elements/confirmation.tsx +169 -0
- package/components/ai-elements/connection.tsx +28 -0
- package/components/ai-elements/context.tsx +408 -0
- package/components/ai-elements/controls.tsx +18 -0
- package/components/ai-elements/conversation.tsx +97 -0
- package/components/ai-elements/edge.tsx +140 -0
- package/components/ai-elements/image.tsx +24 -0
- package/components/ai-elements/inline-citation.tsx +287 -0
- package/components/ai-elements/loader.tsx +96 -0
- package/components/ai-elements/message.tsx +80 -0
- package/components/ai-elements/node.tsx +71 -0
- package/components/ai-elements/open-in-chat.tsx +363 -0
- package/components/ai-elements/panel.tsx +15 -0
- package/components/ai-elements/plan.tsx +142 -0
- package/components/ai-elements/prompt-input.tsx +1352 -0
- package/components/ai-elements/queue.tsx +274 -0
- package/components/ai-elements/reasoning.tsx +178 -0
- package/components/ai-elements/response.tsx +22 -0
- package/components/ai-elements/shimmer.tsx +64 -0
- package/components/ai-elements/sources.tsx +77 -0
- package/components/ai-elements/suggestion.tsx +56 -0
- package/components/ai-elements/task.tsx +87 -0
- package/components/ai-elements/tool.tsx +179 -0
- package/components/ai-elements/toolbar.tsx +16 -0
- package/components/ai-elements/web-preview.tsx +263 -0
- package/components/blocks/AIConversation/AIConversation.stories.tsx +164 -0
- package/components/blocks/AIConversation/AIConversation.tsx +186 -0
- package/components/blocks/AIConversation/index.ts +8 -0
- package/components/blocks/AppSidebar/AppSidebar.stories.tsx +63 -0
- package/components/blocks/AppSidebar/AppSidebar.tsx +87 -0
- package/components/blocks/AppSidebar/index.ts +2 -0
- package/components/blocks/DocumentEditorWithComments/DocumentEditorWithComments.stories.tsx +341 -0
- package/components/blocks/DocumentEditorWithComments/DocumentEditorWithComments.tsx +255 -0
- package/components/blocks/DocumentEditorWithComments/index.ts +9 -0
- package/components/blocks/FileChangeQueue/FileChangeQueue.stories.tsx +207 -0
- package/components/blocks/FileChangeQueue/FileChangeQueue.tsx +143 -0
- package/components/blocks/FileChangeQueue/index.ts +7 -0
- package/components/blocks/LayoutProvider/LayoutProvider.tsx +34 -0
- package/components/blocks/LayoutProvider/index.ts +1 -0
- package/components/blocks/index.ts +2 -0
- package/components/composites/AgentIndicator/AgentIndicator.stories.tsx +154 -0
- package/components/composites/AgentIndicator/AgentIndicator.tsx +102 -0
- package/components/composites/AgentIndicator/index.ts +8 -0
- package/components/composites/AppHeader/AppHeader.stories.tsx +46 -0
- package/components/composites/AppHeader/AppHeader.tsx +24 -0
- package/components/composites/AppHeader/index.ts +2 -0
- package/components/composites/CommentBox/CommentBox.stories.tsx +192 -0
- package/components/composites/CommentBox/CommentBox.tsx +364 -0
- package/components/composites/CommentBox/index.ts +8 -0
- package/components/composites/Confirmation/Confirmation.stories.tsx +151 -0
- package/components/composites/Confirmation/Confirmation.tsx +93 -0
- package/components/composites/Confirmation/index.ts +7 -0
- package/components/composites/DataTable/DataTable.stories.tsx +35 -0
- package/components/composites/DataTable/DataTable.tsx +95 -0
- package/components/composites/DataTable/index.ts +2 -0
- package/components/composites/DocumentEditor/DocumentEditor.css +106 -0
- package/components/composites/DocumentEditor/DocumentEditor.stories.tsx +927 -0
- package/components/composites/DocumentEditor/DocumentEditor.tsx +279 -0
- package/components/composites/DocumentEditor/index.ts +8 -0
- package/components/composites/FileQueue/FileQueue.stories.tsx +175 -0
- package/components/composites/FileQueue/FileQueue.tsx +161 -0
- package/components/composites/FileQueue/FileStatusBadge.tsx +74 -0
- package/components/composites/FileQueue/index.ts +24 -0
- package/components/composites/InteractiveChart/InteractiveChart.stories.tsx +49 -0
- package/components/composites/InteractiveChart/InteractiveChart.tsx +69 -0
- package/components/composites/InteractiveChart/index.ts +2 -0
- package/components/composites/ModeToggle/ModeToggle.stories.tsx +212 -0
- package/components/composites/ModeToggle/ModeToggle.tsx +100 -0
- package/components/composites/ModeToggle/index.ts +7 -0
- package/components/composites/NavUser/NavUser.stories.tsx +50 -0
- package/components/composites/NavUser/NavUser.tsx +60 -0
- package/components/composites/NavUser/index.ts +2 -0
- package/components/composites/NavigationList/NavigationList.stories.tsx +46 -0
- package/components/composites/NavigationList/NavigationList.tsx +46 -0
- package/components/composites/NavigationList/index.ts +2 -0
- package/components/composites/OrchestratorMessage/OrchestratorMessage.stories.tsx +188 -0
- package/components/composites/OrchestratorMessage/OrchestratorMessage.tsx +72 -0
- package/components/composites/OrchestratorMessage/index.ts +8 -0
- package/components/composites/PageContainer/PageContainer.stories.tsx +41 -0
- package/components/composites/PageContainer/PageContainer.tsx +24 -0
- package/components/composites/PageContainer/index.ts +2 -0
- package/components/composites/PromptInput/PromptInput.stories.tsx +200 -0
- package/components/composites/PromptInput/PromptInput.tsx +129 -0
- package/components/composites/PromptInput/index.ts +8 -0
- package/components/composites/SpecialistMessage/SpecialistMessage.stories.tsx +286 -0
- package/components/composites/SpecialistMessage/SpecialistMessage.tsx +107 -0
- package/components/composites/SpecialistMessage/index.ts +8 -0
- package/components/composites/StatsCard/StatsCard.stories.tsx +76 -0
- package/components/composites/StatsCard/StatsCard.tsx +81 -0
- package/components/composites/StatsCard/index.ts +2 -0
- package/components/composites/TablePagination/TablePagination.stories.tsx +38 -0
- package/components/composites/TablePagination/TablePagination.tsx +119 -0
- package/components/composites/TablePagination/index.ts +2 -0
- package/components/composites/TableToolbar/TableToolbar.stories.tsx +60 -0
- package/components/composites/TableToolbar/TableToolbar.tsx +66 -0
- package/components/composites/TableToolbar/index.ts +2 -0
- package/components/composites/ThemeSelector/ThemeSelector.stories.tsx +48 -0
- package/components/composites/ThemeSelector/ThemeSelector.tsx +79 -0
- package/components/composites/ThemeSelector/index.ts +2 -0
- package/components/composites/ToolCallDisplay/ToolCallDisplay.stories.tsx +49 -0
- package/components/composites/ToolCallDisplay/ToolCallDisplay.tsx +108 -0
- package/components/composites/ToolCallDisplay/index.ts +8 -0
- package/components/composites/UserMessage/UserMessage.stories.tsx +59 -0
- package/components/composites/UserMessage/UserMessage.tsx +52 -0
- package/components/composites/UserMessage/index.ts +8 -0
- package/components/composites/index.ts +90 -0
- package/components/features/AIDocEditor/AIDocEditor.behaviors.stories.tsx +451 -0
- package/components/features/AIDocEditor/AIDocEditor.mocks.ts +96 -0
- package/components/features/AIDocEditor/AIDocEditor.stories.tsx +140 -0
- package/components/features/AIDocEditor/AIDocEditor.tsx +130 -0
- package/components/features/AIDocEditor/index.ts +8 -0
- package/components/features/AIDocEditor/useAIDocEditor.d.ts +97 -0
- package/components/features/AIDocEditor/useAIDocEditor.mock.ts +83 -0
- package/components/features/PageLayout/PageLayout.behaviors.stories.tsx +119 -0
- package/components/features/PageLayout/PageLayout.mocks.ts +27 -0
- package/components/features/PageLayout/PageLayout.stories.tsx +142 -0
- package/components/features/PageLayout/PageLayout.tsx +94 -0
- package/components/features/PageLayout/index.ts +4 -0
- package/components/features/PageLayout/usePageLayout.d.ts +24 -0
- package/components/features/PageLayout/usePageLayout.mock.ts +19 -0
- package/components/features/RefinementPanel/README.md +189 -0
- package/components/features/RefinementPanel/RefinementPanel.behaviors.stories.tsx +475 -0
- package/components/features/RefinementPanel/RefinementPanel.mocks.ts +131 -0
- package/components/features/RefinementPanel/RefinementPanel.stories.tsx +141 -0
- package/components/features/RefinementPanel/RefinementPanel.tsx +160 -0
- package/components/features/RefinementPanel/index.ts +25 -0
- package/components/features/RefinementPanel/useRefinementPanel.d.ts +74 -0
- package/components/features/RefinementPanel/useRefinementPanel.mock.ts +121 -0
- package/components/features/SpecNavigator/SpecNavigator.behaviors.stories.tsx +379 -0
- package/components/features/SpecNavigator/SpecNavigator.mocks.ts +131 -0
- package/components/features/SpecNavigator/SpecNavigator.stories.tsx +122 -0
- package/components/features/SpecNavigator/SpecNavigator.tsx +43 -0
- package/components/features/SpecNavigator/index.ts +2 -0
- package/components/features/SpecNavigator/useSpecNavigator.d.ts +122 -0
- package/components/features/SpecNavigator/useSpecNavigator.mock.ts +93 -0
- package/components/features/index.ts +18 -0
- package/components/index.ts +14 -0
- package/components/primitives/Accordion/Accordion.stories.tsx +87 -0
- package/components/primitives/Accordion/Accordion.tsx +66 -0
- package/components/primitives/Accordion/index.ts +13 -0
- package/components/primitives/Alert/Alert.stories.tsx +422 -0
- package/components/primitives/Alert/Alert.tsx +61 -0
- package/components/primitives/Alert/index.ts +8 -0
- package/components/primitives/AlertDialog/AlertDialog.stories.tsx +367 -0
- package/components/primitives/AlertDialog/AlertDialog.tsx +182 -0
- package/components/primitives/AlertDialog/index.ts +25 -0
- package/components/primitives/Avatar/Avatar.stories.tsx +321 -0
- package/components/primitives/Avatar/Avatar.tsx +63 -0
- package/components/primitives/Avatar/index.ts +8 -0
- package/components/primitives/Badge/Badge.stories.tsx +74 -0
- package/components/primitives/Badge/Badge.tsx +49 -0
- package/components/primitives/Badge/index.ts +2 -0
- package/components/primitives/Button/Button.stories.tsx +445 -0
- package/components/primitives/Button/Button.tsx +89 -0
- package/components/primitives/Button/index.ts +7 -0
- package/components/primitives/Card/Card.stories.tsx +831 -0
- package/components/primitives/Card/Card.tsx +242 -0
- package/components/primitives/Card/index.ts +30 -0
- package/components/primitives/Carousel/Carousel.stories.tsx +32 -0
- package/components/primitives/Carousel/Carousel.tsx +63 -0
- package/components/primitives/Carousel/index.ts +13 -0
- package/components/primitives/Chart/Chart.stories.tsx +346 -0
- package/components/primitives/Chart/Chart.tsx +117 -0
- package/components/primitives/Chart/index.ts +20 -0
- package/components/primitives/Checkbox/Checkbox.stories.tsx +87 -0
- package/components/primitives/Checkbox/Checkbox.tsx +38 -0
- package/components/primitives/Checkbox/index.ts +2 -0
- package/components/primitives/Collapsible/Collapsible.stories.tsx +38 -0
- package/components/primitives/Collapsible/Collapsible.tsx +39 -0
- package/components/primitives/Collapsible/index.ts +8 -0
- package/components/primitives/Command/Command.stories.tsx +150 -0
- package/components/primitives/Command/Command.tsx +147 -0
- package/components/primitives/Command/index.ts +20 -0
- package/components/primitives/Dialog/Dialog.stories.tsx +390 -0
- package/components/primitives/Dialog/Dialog.tsx +140 -0
- package/components/primitives/Dialog/index.ts +22 -0
- package/components/primitives/Drawer/Drawer.stories.tsx +327 -0
- package/components/primitives/Drawer/Drawer.tsx +208 -0
- package/components/primitives/Drawer/index.ts +27 -0
- package/components/primitives/DropdownMenu/DropdownMenu.stories.tsx +150 -0
- package/components/primitives/DropdownMenu/DropdownMenu.tsx +73 -0
- package/components/primitives/DropdownMenu/index.ts +1 -0
- package/components/primitives/HoverCard/HoverCard.stories.tsx +26 -0
- package/components/primitives/HoverCard/HoverCard.tsx +39 -0
- package/components/primitives/HoverCard/index.ts +8 -0
- package/components/primitives/Icon/Icon.stories.tsx +281 -0
- package/components/primitives/Icon/Icon.tsx +87 -0
- package/components/primitives/Icon/index.ts +8 -0
- package/components/primitives/Input/Input.stories.tsx +370 -0
- package/components/primitives/Input/Input.tsx +88 -0
- package/components/primitives/Input/index.ts +7 -0
- package/components/primitives/InputGroup/InputGroup.stories.tsx +40 -0
- package/components/primitives/InputGroup/InputGroup.tsx +72 -0
- package/components/primitives/InputGroup/index.ts +14 -0
- package/components/primitives/Label/Label.stories.tsx +227 -0
- package/components/primitives/Label/Label.tsx +53 -0
- package/components/primitives/Label/index.ts +7 -0
- package/components/primitives/Popover/Popover.stories.tsx +42 -0
- package/components/primitives/Popover/Popover.tsx +107 -0
- package/components/primitives/Popover/index.ts +2 -0
- package/components/primitives/Progress/Progress.stories.tsx +340 -0
- package/components/primitives/Progress/Progress.tsx +31 -0
- package/components/primitives/Progress/index.ts +1 -0
- package/components/primitives/ScrollArea/ScrollArea.stories.tsx +26 -0
- package/components/primitives/ScrollArea/ScrollArea.tsx +28 -0
- package/components/primitives/ScrollArea/index.ts +6 -0
- package/components/primitives/Select/Select.stories.tsx +288 -0
- package/components/primitives/Select/Select.tsx +162 -0
- package/components/primitives/Select/index.ts +22 -0
- package/components/primitives/Separator/Separator.stories.tsx +264 -0
- package/components/primitives/Separator/Separator.tsx +48 -0
- package/components/primitives/Separator/index.ts +7 -0
- package/components/primitives/Sidebar/Sidebar.stories.tsx +358 -0
- package/components/primitives/Sidebar/Sidebar.tsx +317 -0
- package/components/primitives/Sidebar/index.ts +41 -0
- package/components/primitives/Table/Table.stories.tsx +389 -0
- package/components/primitives/Table/Table.tsx +191 -0
- package/components/primitives/Table/index.ts +26 -0
- package/components/primitives/Tabs/Tabs.stories.tsx +129 -0
- package/components/primitives/Tabs/Tabs.tsx +70 -0
- package/components/primitives/Tabs/index.ts +13 -0
- package/components/primitives/Textarea/Textarea.stories.tsx +358 -0
- package/components/primitives/Textarea/Textarea.tsx +91 -0
- package/components/primitives/Textarea/index.ts +7 -0
- package/components/primitives/ToggleGroup/ToggleGroup.stories.tsx +87 -0
- package/components/primitives/ToggleGroup/ToggleGroup.tsx +52 -0
- package/components/primitives/ToggleGroup/index.ts +6 -0
- package/components/primitives/Tooltip/Tooltip.stories.tsx +336 -0
- package/components/primitives/Tooltip/Tooltip.tsx +78 -0
- package/components/primitives/Tooltip/index.ts +10 -0
- package/components/primitives/index.ts +34 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +157 -0
- package/components/ui/alert.tsx +66 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/card.tsx +117 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +334 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +184 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/drawer.tsx +118 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/input-group.tsx +170 -0
- package/components/ui/input.tsx +48 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/popover.tsx +33 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/select.tsx +187 -0
- package/components/ui/separator.tsx +31 -0
- package/components/ui/sidebar.tsx +577 -0
- package/components/ui/table.tsx +120 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +46 -0
- package/components/ui/toggle-group.tsx +83 -0
- package/components/ui/toggle.tsx +47 -0
- package/components/ui/tooltip.tsx +61 -0
- package/dist/index.cjs +7389 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +75 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +7160 -0
- package/dist/index.js.map +1 -0
- package/hooks/useAIDocReviewer.d.ts +0 -0
- package/lib/utils.ts +6 -0
- package/package.json +140 -0
- package/tokens/color/base.json +14 -0
- package/tokens/color/dark.json +40 -0
- package/tokens/color/green.json +21 -0
- package/tokens/color/light.json +52 -0
- package/tokens/color/neutral.json +20 -0
- package/tokens/color/violet.json +21 -0
- package/tokens/spacing.json +22 -0
- package/utils/ai-editor/format-date.ts +41 -0
- package/utils/ai-editor/index.ts +22 -0
- package/utils/ai-editor/type-guards.ts +72 -0
- package/utils/ai-editor/validation.ts +130 -0
- 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,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";
|