ai-design-system 0.1.52 → 0.1.54
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/components/ai-elements/file-tree.tsx +304 -0
- package/components/blocks/AIConversation/AIConversation.tsx +6 -1
- package/components/composites/AdjustableLayout/AdjustableLayout.tsx +31 -22
- package/components/composites/FileTreeExplorer/FileTreeExplorer.stories.tsx +73 -0
- package/components/composites/FileTreeExplorer/FileTreeExplorer.tsx +145 -0
- package/components/composites/FileTreeExplorer/index.ts +8 -0
- package/components/composites/FormReports/FormReports.stories.tsx +33 -1
- package/components/composites/FormReports/FormReports.tsx +113 -0
- package/components/composites/FormReports/index.ts +8 -0
- package/components/composites/ToolCallDisplay/ToolCallDisplay.tsx +29 -1
- package/components/features/AIDocEditor/AIDocEditor.mocks.ts +10 -3
- package/components/features/AIDocEditor/AIDocEditor.stories.tsx +1 -0
- package/components/features/AIDocEditor/AIDocEditor.tsx +146 -58
- package/components/features/AIDocEditor/useAIDocEditor.d.ts +6 -0
- package/components/features/AIDocEditor/useAIDocEditor.mock.ts +62 -20
- package/components/features/RefinementPanel/RefinementPanel.tsx +9 -3
- package/components/features/RefinementPanel/useRefinementPanel.d.ts +4 -0
- package/components/features/RefinementPanel/useRefinementPanel.mock.ts +3 -0
- package/components/features/WorkflowObservabilityFeature/WorkflowObservabilityFeature.tsx +12 -14
- package/components/ui/collapsible.tsx +1 -1
- package/components/ui/command.tsx +2 -2
- package/components/ui/input-group.tsx +1 -1
- package/components/ui/popover.tsx +1 -1
- package/dist/index.cjs +584 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +29 -67
- package/dist/index.d.ts +58 -0
- package/dist/index.js +585 -80
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Collapsible,
|
|
5
|
+
CollapsibleContent,
|
|
6
|
+
CollapsibleTrigger,
|
|
7
|
+
} from "@/components/ui/collapsible";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import {
|
|
10
|
+
ChevronRightIcon,
|
|
11
|
+
FileIcon,
|
|
12
|
+
FolderIcon,
|
|
13
|
+
FolderOpenIcon,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
16
|
+
import {
|
|
17
|
+
createContext,
|
|
18
|
+
useCallback,
|
|
19
|
+
useContext,
|
|
20
|
+
useMemo,
|
|
21
|
+
useState,
|
|
22
|
+
} from "react";
|
|
23
|
+
|
|
24
|
+
interface FileTreeContextType {
|
|
25
|
+
expandedPaths: Set<string>;
|
|
26
|
+
togglePath: (path: string) => void;
|
|
27
|
+
selectedPath?: string;
|
|
28
|
+
onSelect?: (path: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Default noop for context default value
|
|
32
|
+
// oxlint-disable-next-line eslint(no-empty-function)
|
|
33
|
+
const noop = () => {};
|
|
34
|
+
|
|
35
|
+
const FileTreeContext = createContext<FileTreeContextType>({
|
|
36
|
+
// oxlint-disable-next-line eslint-plugin-unicorn(no-new-builtin)
|
|
37
|
+
expandedPaths: new Set(),
|
|
38
|
+
togglePath: noop,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export type FileTreeProps = Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> & {
|
|
42
|
+
expanded?: Set<string>;
|
|
43
|
+
defaultExpanded?: Set<string>;
|
|
44
|
+
selectedPath?: string;
|
|
45
|
+
onSelect?: (path: string) => void;
|
|
46
|
+
onExpandedChange?: (expanded: Set<string>) => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const FileTree = ({
|
|
50
|
+
expanded: controlledExpanded,
|
|
51
|
+
defaultExpanded = new Set(),
|
|
52
|
+
selectedPath,
|
|
53
|
+
onSelect,
|
|
54
|
+
onExpandedChange,
|
|
55
|
+
className,
|
|
56
|
+
children,
|
|
57
|
+
...props
|
|
58
|
+
}: FileTreeProps) => {
|
|
59
|
+
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
60
|
+
const expandedPaths = controlledExpanded ?? internalExpanded;
|
|
61
|
+
|
|
62
|
+
const togglePath = useCallback(
|
|
63
|
+
(path: string) => {
|
|
64
|
+
const newExpanded = new Set(expandedPaths);
|
|
65
|
+
if (newExpanded.has(path)) {
|
|
66
|
+
newExpanded.delete(path);
|
|
67
|
+
} else {
|
|
68
|
+
newExpanded.add(path);
|
|
69
|
+
}
|
|
70
|
+
setInternalExpanded(newExpanded);
|
|
71
|
+
onExpandedChange?.(newExpanded);
|
|
72
|
+
},
|
|
73
|
+
[expandedPaths, onExpandedChange]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const contextValue = useMemo(
|
|
77
|
+
() => ({ expandedPaths, onSelect, selectedPath, togglePath }),
|
|
78
|
+
[expandedPaths, onSelect, selectedPath, togglePath]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<FileTreeContext.Provider value={contextValue}>
|
|
83
|
+
<div
|
|
84
|
+
className={cn(
|
|
85
|
+
"rounded-lg border bg-background font-mono text-sm",
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
role="tree"
|
|
89
|
+
{...props}
|
|
90
|
+
>
|
|
91
|
+
<div className="p-2">{children}</div>
|
|
92
|
+
</div>
|
|
93
|
+
</FileTreeContext.Provider>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type FileTreeIconProps = HTMLAttributes<HTMLSpanElement>;
|
|
98
|
+
|
|
99
|
+
export const FileTreeIcon = ({
|
|
100
|
+
className,
|
|
101
|
+
children,
|
|
102
|
+
...props
|
|
103
|
+
}: FileTreeIconProps) => (
|
|
104
|
+
<span className={cn("shrink-0", className)} {...props}>
|
|
105
|
+
{children}
|
|
106
|
+
</span>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
export type FileTreeNameProps = HTMLAttributes<HTMLSpanElement>;
|
|
110
|
+
|
|
111
|
+
export const FileTreeName = ({
|
|
112
|
+
className,
|
|
113
|
+
children,
|
|
114
|
+
...props
|
|
115
|
+
}: FileTreeNameProps) => (
|
|
116
|
+
<span className={cn("truncate", className)} {...props}>
|
|
117
|
+
{children}
|
|
118
|
+
</span>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
interface FileTreeFolderContextType {
|
|
122
|
+
path: string;
|
|
123
|
+
name: string;
|
|
124
|
+
isExpanded: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const FileTreeFolderContext = createContext<FileTreeFolderContextType>({
|
|
128
|
+
isExpanded: false,
|
|
129
|
+
name: "",
|
|
130
|
+
path: "",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export type FileTreeFolderProps = HTMLAttributes<HTMLDivElement> & {
|
|
134
|
+
path: string;
|
|
135
|
+
name: string;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const FileTreeFolder = ({
|
|
139
|
+
path,
|
|
140
|
+
name,
|
|
141
|
+
className,
|
|
142
|
+
children,
|
|
143
|
+
...props
|
|
144
|
+
}: FileTreeFolderProps) => {
|
|
145
|
+
const { expandedPaths, togglePath, selectedPath, onSelect } =
|
|
146
|
+
useContext(FileTreeContext);
|
|
147
|
+
const isExpanded = expandedPaths.has(path);
|
|
148
|
+
const isSelected = selectedPath === path;
|
|
149
|
+
|
|
150
|
+
const handleOpenChange = useCallback(() => {
|
|
151
|
+
togglePath(path);
|
|
152
|
+
}, [togglePath, path]);
|
|
153
|
+
|
|
154
|
+
const handleSelect = useCallback(() => {
|
|
155
|
+
onSelect?.(path);
|
|
156
|
+
}, [onSelect, path]);
|
|
157
|
+
|
|
158
|
+
const folderContextValue = useMemo(
|
|
159
|
+
() => ({ isExpanded, name, path }),
|
|
160
|
+
[isExpanded, name, path]
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<FileTreeFolderContext.Provider value={folderContextValue}>
|
|
165
|
+
<Collapsible onOpenChange={handleOpenChange} open={isExpanded}>
|
|
166
|
+
<div
|
|
167
|
+
className={cn("", className)}
|
|
168
|
+
role="treeitem"
|
|
169
|
+
tabIndex={0}
|
|
170
|
+
{...props}
|
|
171
|
+
>
|
|
172
|
+
<div
|
|
173
|
+
className={cn(
|
|
174
|
+
"flex w-full items-center gap-1 rounded px-2 py-1 text-left transition-colors hover:bg-muted/50",
|
|
175
|
+
isSelected && "bg-muted"
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
<CollapsibleTrigger asChild>
|
|
179
|
+
<button
|
|
180
|
+
className="flex shrink-0 cursor-pointer items-center border-none bg-transparent p-0"
|
|
181
|
+
type="button"
|
|
182
|
+
>
|
|
183
|
+
<ChevronRightIcon
|
|
184
|
+
className={cn(
|
|
185
|
+
"size-4 shrink-0 text-muted-foreground transition-transform",
|
|
186
|
+
isExpanded && "rotate-90"
|
|
187
|
+
)}
|
|
188
|
+
/>
|
|
189
|
+
</button>
|
|
190
|
+
</CollapsibleTrigger>
|
|
191
|
+
<button
|
|
192
|
+
className="flex min-w-0 flex-1 cursor-pointer items-center gap-1 border-none bg-transparent p-0 text-left"
|
|
193
|
+
onClick={handleSelect}
|
|
194
|
+
type="button"
|
|
195
|
+
>
|
|
196
|
+
<FileTreeIcon>
|
|
197
|
+
{isExpanded ? (
|
|
198
|
+
<FolderOpenIcon className="size-4 text-blue-500" />
|
|
199
|
+
) : (
|
|
200
|
+
<FolderIcon className="size-4 text-blue-500" />
|
|
201
|
+
)}
|
|
202
|
+
</FileTreeIcon>
|
|
203
|
+
<FileTreeName>{name}</FileTreeName>
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
<CollapsibleContent>
|
|
207
|
+
<div className="ml-4 border-l pl-2">{children}</div>
|
|
208
|
+
</CollapsibleContent>
|
|
209
|
+
</div>
|
|
210
|
+
</Collapsible>
|
|
211
|
+
</FileTreeFolderContext.Provider>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
interface FileTreeFileContextType {
|
|
216
|
+
path: string;
|
|
217
|
+
name: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const FileTreeFileContext = createContext<FileTreeFileContextType>({
|
|
221
|
+
name: "",
|
|
222
|
+
path: "",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
export type FileTreeFileProps = HTMLAttributes<HTMLDivElement> & {
|
|
226
|
+
path: string;
|
|
227
|
+
name: string;
|
|
228
|
+
icon?: ReactNode;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export const FileTreeFile = ({
|
|
232
|
+
path,
|
|
233
|
+
name,
|
|
234
|
+
icon,
|
|
235
|
+
className,
|
|
236
|
+
children,
|
|
237
|
+
...props
|
|
238
|
+
}: FileTreeFileProps) => {
|
|
239
|
+
const { selectedPath, onSelect } = useContext(FileTreeContext);
|
|
240
|
+
const isSelected = selectedPath === path;
|
|
241
|
+
|
|
242
|
+
const handleClick = useCallback(() => {
|
|
243
|
+
onSelect?.(path);
|
|
244
|
+
}, [onSelect, path]);
|
|
245
|
+
|
|
246
|
+
const handleKeyDown = useCallback(
|
|
247
|
+
(e: React.KeyboardEvent) => {
|
|
248
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
249
|
+
onSelect?.(path);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
[onSelect, path]
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const fileContextValue = useMemo(() => ({ name, path }), [name, path]);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<FileTreeFileContext.Provider value={fileContextValue}>
|
|
259
|
+
<div
|
|
260
|
+
className={cn(
|
|
261
|
+
"flex cursor-pointer items-center gap-1 rounded px-2 py-1 transition-colors hover:bg-muted/50",
|
|
262
|
+
isSelected && "bg-muted",
|
|
263
|
+
className
|
|
264
|
+
)}
|
|
265
|
+
onClick={handleClick}
|
|
266
|
+
onKeyDown={handleKeyDown}
|
|
267
|
+
role="treeitem"
|
|
268
|
+
tabIndex={0}
|
|
269
|
+
{...props}
|
|
270
|
+
>
|
|
271
|
+
{children ?? (
|
|
272
|
+
<>
|
|
273
|
+
{/* Spacer for alignment */}
|
|
274
|
+
<span className="size-4 shrink-0" />
|
|
275
|
+
<FileTreeIcon>
|
|
276
|
+
{icon ?? <FileIcon className="size-4 text-muted-foreground" />}
|
|
277
|
+
</FileTreeIcon>
|
|
278
|
+
<FileTreeName>{name}</FileTreeName>
|
|
279
|
+
</>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</FileTreeFileContext.Provider>
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export type FileTreeActionsProps = HTMLAttributes<HTMLDivElement>;
|
|
287
|
+
|
|
288
|
+
const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
|
|
289
|
+
|
|
290
|
+
export const FileTreeActions = ({
|
|
291
|
+
className,
|
|
292
|
+
children,
|
|
293
|
+
...props
|
|
294
|
+
}: FileTreeActionsProps) => (
|
|
295
|
+
<div
|
|
296
|
+
className={cn("ml-auto flex items-center gap-1", className)}
|
|
297
|
+
onClick={stopPropagation}
|
|
298
|
+
onKeyDown={stopPropagation}
|
|
299
|
+
role="group"
|
|
300
|
+
{...props}
|
|
301
|
+
>
|
|
302
|
+
{children}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
@@ -61,6 +61,10 @@ export interface AIConversationProps
|
|
|
61
61
|
description?: string
|
|
62
62
|
icon?: React.ReactNode
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Callback fired when a tool action (like a link click) is triggered
|
|
66
|
+
*/
|
|
67
|
+
onToolAction?: (toolCall: ToolCall, action: string) => void
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
/**
|
|
@@ -73,6 +77,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
73
77
|
onSelectSubAgent,
|
|
74
78
|
selectedSubAgent,
|
|
75
79
|
emptyState,
|
|
80
|
+
onToolAction,
|
|
76
81
|
...conversationProps
|
|
77
82
|
}) => {
|
|
78
83
|
const isEmpty = React.useMemo(
|
|
@@ -146,7 +151,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
146
151
|
>
|
|
147
152
|
{/* Render direct tool calls */}
|
|
148
153
|
{directToolCalls.map((tc) => (
|
|
149
|
-
<ToolCallDisplay key={tc.id} toolCall={tc} />
|
|
154
|
+
<ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
|
|
150
155
|
))}
|
|
151
156
|
|
|
152
157
|
{/* Render specialist sub-agents */}
|
|
@@ -220,36 +220,45 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
220
220
|
{section.content}
|
|
221
221
|
</div>
|
|
222
222
|
|
|
223
|
-
{/* Show drag handle after this panel if it's not the last one */}
|
|
224
|
-
{
|
|
225
|
-
|
|
226
|
-
className={cn(
|
|
227
|
-
"flex-shrink-0 flex items-center justify-center relative group",
|
|
228
|
-
orientation === "vertical"
|
|
229
|
-
? "cursor-row-resize h-2 w-full"
|
|
230
|
-
: "cursor-col-resize w-2 h-full"
|
|
231
|
-
)}
|
|
232
|
-
onMouseDown={(e) => handleMouseDown(index, e)}
|
|
233
|
-
>
|
|
234
|
-
{/* Visible pill */}
|
|
223
|
+
{/* Show drag handle or spacer after this panel if it's not the last one */}
|
|
224
|
+
{index < sections.length - 1 && (
|
|
225
|
+
isResizable ? (
|
|
235
226
|
<div
|
|
236
227
|
className={cn(
|
|
237
|
-
|
|
228
|
+
"flex-shrink-0 flex items-center justify-center relative group",
|
|
238
229
|
orientation === "vertical"
|
|
239
|
-
? "h-
|
|
240
|
-
: "w-
|
|
230
|
+
? "cursor-row-resize h-2 w-full"
|
|
231
|
+
: "cursor-col-resize w-2 h-full"
|
|
241
232
|
)}
|
|
242
|
-
|
|
243
|
-
|
|
233
|
+
onMouseDown={(e) => handleMouseDown(index, e)}
|
|
234
|
+
>
|
|
235
|
+
{/* Visible pill */}
|
|
236
|
+
<div
|
|
237
|
+
className={cn(
|
|
238
|
+
`${colorMap[dragHandleColor]} transition-colors duration-200 rounded-full`,
|
|
239
|
+
orientation === "vertical"
|
|
240
|
+
? "h-1 w-8"
|
|
241
|
+
: "w-1 h-8"
|
|
242
|
+
)}
|
|
243
|
+
/>
|
|
244
|
+
{/* Invisible large hit area */}
|
|
245
|
+
<div
|
|
246
|
+
className={cn(
|
|
247
|
+
"absolute z-10",
|
|
248
|
+
orientation === "vertical"
|
|
249
|
+
? "inset-x-0 -top-2 -bottom-2"
|
|
250
|
+
: "inset-y-0 -left-2 -right-2"
|
|
251
|
+
)}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
) : (
|
|
244
255
|
<div
|
|
245
256
|
className={cn(
|
|
246
|
-
"
|
|
247
|
-
orientation === "vertical"
|
|
248
|
-
? "inset-x-0 -top-2 -bottom-2"
|
|
249
|
-
: "inset-y-0 -left-2 -right-2"
|
|
257
|
+
"flex-shrink-0",
|
|
258
|
+
orientation === "vertical" ? "h-2 w-full" : "w-2 h-full"
|
|
250
259
|
)}
|
|
251
260
|
/>
|
|
252
|
-
|
|
261
|
+
)
|
|
253
262
|
)}
|
|
254
263
|
</React.Fragment>
|
|
255
264
|
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { fn } from '@storybook/test'
|
|
3
|
+
import { FileTreeExplorer } from './FileTreeExplorer'
|
|
4
|
+
|
|
5
|
+
const sampleTree = [
|
|
6
|
+
{
|
|
7
|
+
name: 'src',
|
|
8
|
+
path: 'src',
|
|
9
|
+
type: 'folder' as const,
|
|
10
|
+
children: [
|
|
11
|
+
{
|
|
12
|
+
name: 'components',
|
|
13
|
+
path: 'src/components',
|
|
14
|
+
type: 'folder' as const,
|
|
15
|
+
children: [
|
|
16
|
+
{ name: 'Button.tsx', path: 'src/components/Button.tsx', type: 'file' as const },
|
|
17
|
+
{ name: 'Input.tsx', path: 'src/components/Input.tsx', type: 'file' as const },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'hooks',
|
|
22
|
+
path: 'src/hooks',
|
|
23
|
+
type: 'folder' as const,
|
|
24
|
+
children: [
|
|
25
|
+
{ name: 'use-toast.ts', path: 'src/hooks/use-toast.ts', type: 'file' as const },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{ name: 'index.ts', path: 'src/index.ts', type: 'file' as const },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{ name: 'package.json', path: 'package.json', type: 'file' as const },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const meta = {
|
|
35
|
+
title: 'Composites/FileTreeExplorer',
|
|
36
|
+
component: FileTreeExplorer,
|
|
37
|
+
tags: ['autodocs'],
|
|
38
|
+
parameters: {
|
|
39
|
+
layout: 'padded',
|
|
40
|
+
},
|
|
41
|
+
} satisfies Meta<typeof FileTreeExplorer>
|
|
42
|
+
|
|
43
|
+
export default meta
|
|
44
|
+
type Story = StoryObj<typeof meta>
|
|
45
|
+
|
|
46
|
+
export const Default: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
tree: sampleTree,
|
|
49
|
+
defaultExpanded: new Set(['src', 'src/components']),
|
|
50
|
+
selectedPath: 'src/components/Button.tsx',
|
|
51
|
+
onSelect: fn(),
|
|
52
|
+
createButtonLabel: 'New File',
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Empty: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
tree: [],
|
|
59
|
+
createButtonLabel: 'New File',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const DarkMode: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
tree: sampleTree,
|
|
66
|
+
defaultExpanded: new Set(['src']),
|
|
67
|
+
selectedPath: 'src/index.ts',
|
|
68
|
+
onSelect: fn(),
|
|
69
|
+
},
|
|
70
|
+
parameters: {
|
|
71
|
+
backgrounds: { disable: true },
|
|
72
|
+
},
|
|
73
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FileTree,
|
|
5
|
+
FileTreeFolder,
|
|
6
|
+
FileTreeFile,
|
|
7
|
+
} from "@/components/ai-elements/file-tree"
|
|
8
|
+
import { Icon } from "@/components/primitives/Icon"
|
|
9
|
+
import { Input } from "@/components/primitives/Input"
|
|
10
|
+
import { cn } from "@/lib/utils"
|
|
11
|
+
|
|
12
|
+
export type FileTreeNode = {
|
|
13
|
+
name: string
|
|
14
|
+
path: string
|
|
15
|
+
type: "file" | "folder"
|
|
16
|
+
children?: FileTreeNode[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FileTreeExplorerProps {
|
|
20
|
+
tree: FileTreeNode[]
|
|
21
|
+
defaultExpanded?: Set<string>
|
|
22
|
+
selectedPath?: string
|
|
23
|
+
onSelect?: (path: string) => void
|
|
24
|
+
searchPlaceholder?: string
|
|
25
|
+
onCreateClick?: () => void
|
|
26
|
+
createButtonLabel?: string
|
|
27
|
+
headerClassName?: string
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderTree(nodes: FileTreeNode[]): React.ReactNode {
|
|
32
|
+
return nodes.map((node) => {
|
|
33
|
+
if (node.type === "folder") {
|
|
34
|
+
return (
|
|
35
|
+
<FileTreeFolder key={node.path} name={node.name} path={node.path}>
|
|
36
|
+
{node.children ? renderTree(node.children) : null}
|
|
37
|
+
</FileTreeFolder>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
return <FileTreeFile key={node.path} name={node.name} path={node.path} />
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function filterTree(nodes: FileTreeNode[], query: string): FileTreeNode[] {
|
|
45
|
+
if (!query) return nodes;
|
|
46
|
+
const lowerQuery = query.toLowerCase();
|
|
47
|
+
|
|
48
|
+
return nodes.reduce<FileTreeNode[]>((acc, node) => {
|
|
49
|
+
if (node.type === 'file') {
|
|
50
|
+
if (node.name.toLowerCase().includes(lowerQuery)) {
|
|
51
|
+
acc.push(node);
|
|
52
|
+
}
|
|
53
|
+
} else if (node.type === 'folder' && node.children) {
|
|
54
|
+
const filteredChildren = filterTree(node.children, query);
|
|
55
|
+
// Keep the folder if it matches the query itself, or if any of its children match
|
|
56
|
+
if (node.name.toLowerCase().includes(lowerQuery) || filteredChildren.length > 0) {
|
|
57
|
+
acc.push({ ...node, children: filteredChildren });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return acc;
|
|
61
|
+
}, []);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getAllFolderPaths(nodes: FileTreeNode[]): Set<string> {
|
|
65
|
+
const paths = new Set<string>();
|
|
66
|
+
function traverse(n: FileTreeNode[]) {
|
|
67
|
+
for (const node of n) {
|
|
68
|
+
if (node.type === 'folder') {
|
|
69
|
+
paths.add(node.path);
|
|
70
|
+
if (node.children) traverse(node.children);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
traverse(nodes);
|
|
75
|
+
return paths;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
|
|
79
|
+
({
|
|
80
|
+
tree,
|
|
81
|
+
defaultExpanded,
|
|
82
|
+
selectedPath,
|
|
83
|
+
onSelect,
|
|
84
|
+
searchPlaceholder = "Filter files...",
|
|
85
|
+
onCreateClick,
|
|
86
|
+
createButtonLabel,
|
|
87
|
+
headerClassName,
|
|
88
|
+
className,
|
|
89
|
+
}) => {
|
|
90
|
+
const [searchQuery, setSearchQuery] = React.useState("")
|
|
91
|
+
|
|
92
|
+
const filteredTree = React.useMemo(() => {
|
|
93
|
+
return filterTree(tree, searchQuery)
|
|
94
|
+
}, [tree, searchQuery])
|
|
95
|
+
|
|
96
|
+
const expandedPaths = React.useMemo(() => {
|
|
97
|
+
if (searchQuery) {
|
|
98
|
+
return getAllFolderPaths(filteredTree)
|
|
99
|
+
}
|
|
100
|
+
return defaultExpanded
|
|
101
|
+
}, [searchQuery, filteredTree, defaultExpanded])
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className={cn("flex flex-col rounded-lg border bg-background", className)}>
|
|
105
|
+
<div className={cn("flex items-center gap-2 border-b px-3 py-2", headerClassName)}>
|
|
106
|
+
<div className="relative flex-1">
|
|
107
|
+
<Icon
|
|
108
|
+
name="search"
|
|
109
|
+
size="sm"
|
|
110
|
+
className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground"
|
|
111
|
+
/>
|
|
112
|
+
<Input
|
|
113
|
+
placeholder={searchPlaceholder}
|
|
114
|
+
value={searchQuery}
|
|
115
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
116
|
+
className="pl-8"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
{onCreateClick ? (
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={onCreateClick}
|
|
123
|
+
className="inline-flex items-center gap-1 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
|
124
|
+
>
|
|
125
|
+
<Icon name="plus" size="sm" />
|
|
126
|
+
{createButtonLabel ?? "New"}
|
|
127
|
+
</button>
|
|
128
|
+
) : null}
|
|
129
|
+
</div>
|
|
130
|
+
<div className="p-2">
|
|
131
|
+
<FileTree
|
|
132
|
+
className="border-none bg-transparent"
|
|
133
|
+
defaultExpanded={expandedPaths}
|
|
134
|
+
selectedPath={selectedPath}
|
|
135
|
+
onSelect={onSelect}
|
|
136
|
+
>
|
|
137
|
+
{renderTree(filteredTree)}
|
|
138
|
+
</FileTree>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
FileTreeExplorer.displayName = "FileTreeExplorer"
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
2
|
import { FormReports } from './FormReports'
|
|
3
|
+
import type { FormReportsFieldDefinition } from './FormReportsDrawerForm'
|
|
4
|
+
import type { FormReportsColumn, FormReportsEntity } from './FormReportsTable'
|
|
5
|
+
|
|
6
|
+
const sampleColumns: FormReportsColumn[] = [
|
|
7
|
+
{ key: "name", label: "Name" },
|
|
8
|
+
{ key: "email", label: "Email" },
|
|
9
|
+
{ key: "role", label: "Role" },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
const sampleItems: FormReportsEntity[] = [
|
|
13
|
+
{ id: 1, name: "Alice Smith", email: "alice@example.com", role: "Admin" },
|
|
14
|
+
{ id: 2, name: "Bob Jones", email: "bob@example.com", role: "Editor" },
|
|
15
|
+
{ id: 3, name: "Carol Lee", email: "carol@example.com", role: "Viewer" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const sampleFields: FormReportsFieldDefinition[] = [
|
|
19
|
+
{ name: "name", label: "Name", type: "text", required: true },
|
|
20
|
+
{ name: "email", label: "Email", type: "text", required: true },
|
|
21
|
+
{ name: "role", label: "Role", type: "select", options: [{ value: "Admin", label: "Admin" }, { value: "Editor", label: "Editor" }, { value: "Viewer", label: "Viewer" }] },
|
|
22
|
+
]
|
|
3
23
|
|
|
4
24
|
const meta = {
|
|
5
25
|
title: 'Composites/FormReports',
|
|
@@ -14,5 +34,17 @@ export default meta
|
|
|
14
34
|
type Story = StoryObj<typeof meta>
|
|
15
35
|
|
|
16
36
|
export const Default: Story = {
|
|
17
|
-
args: {
|
|
37
|
+
args: {
|
|
38
|
+
items: sampleItems,
|
|
39
|
+
columns: sampleColumns,
|
|
40
|
+
fields: sampleFields,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Empty: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
items: [],
|
|
47
|
+
columns: sampleColumns,
|
|
48
|
+
fields: sampleFields,
|
|
49
|
+
},
|
|
18
50
|
}
|