ai-design-system 0.1.51 → 0.1.53
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/InboxPanel/InboxPanel.tsx +1 -1
- package/components/blocks/SectionLayout/SectionLayout.tsx +2 -0
- package/components/blocks/SectionLayout/interfaces.ts +1 -0
- package/components/composites/AdjustableLayout/AdjustableLayout.tsx +62 -40
- package/components/composites/FileTreeExplorer/FileTreeExplorer.stories.tsx +73 -0
- package/components/composites/FileTreeExplorer/FileTreeExplorer.tsx +95 -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/NavigationList/NavigationList.tsx +8 -1
- package/components/composites/NavigationList/interfaces.ts +1 -0
- package/components/composites/PromptInput/PromptInput.tsx +5 -1
- package/components/composites/index.ts +4 -0
- package/components/features/AIDocEditor/AIDocEditor.mocks.ts +10 -3
- package/components/features/AIDocEditor/AIDocEditor.stories.tsx +1 -0
- package/components/features/AIDocEditor/AIDocEditor.tsx +98 -22
- package/components/features/AIDocEditor/useAIDocEditor.d.ts +6 -0
- package/components/features/AIDocEditor/useAIDocEditor.mock.ts +56 -9
- package/components/features/RefinementPanel/RefinementPanel.tsx +7 -6
- package/components/features/WorkflowObservabilityFeature/WorkflowObservabilityFeature.tsx +14 -13
- package/components/index.ts +1 -1
- 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 +526 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +107 -75
- package/dist/index.d.ts +65 -19
- package/dist/index.js +527 -73
- 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
|
+
);
|
|
@@ -21,6 +21,7 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
|
|
|
21
21
|
resizable = true,
|
|
22
22
|
dragHandleColor = "border",
|
|
23
23
|
className,
|
|
24
|
+
padded = true,
|
|
24
25
|
...props
|
|
25
26
|
}) => {
|
|
26
27
|
// Transform sections to include headers
|
|
@@ -47,6 +48,7 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
|
|
|
47
48
|
onSectionResize={onSectionResize}
|
|
48
49
|
dragHandleColor={dragHandleColor}
|
|
49
50
|
className={className}
|
|
51
|
+
padded={padded}
|
|
50
52
|
{...props}
|
|
51
53
|
/>
|
|
52
54
|
)
|
|
@@ -20,4 +20,5 @@ export interface SectionLayoutProps extends React.ComponentPropsWithoutRef<"div"
|
|
|
20
20
|
onSectionResize?: (sectionId: string, newSize: number) => void;
|
|
21
21
|
resizable?: boolean;
|
|
22
22
|
dragHandleColor?: "primary" | "secondary" | "accent" | "border" | "muted";
|
|
23
|
+
padded?: boolean;
|
|
23
24
|
}
|
|
@@ -18,6 +18,7 @@ export interface AdjustableLayoutProps extends React.ComponentPropsWithoutRef<"d
|
|
|
18
18
|
storageKey?: string // localStorage key for persistence
|
|
19
19
|
onSectionResize?: (sectionId: string, newSize: number) => void
|
|
20
20
|
dragHandleColor?: "primary" | "secondary" | "accent" | "border" | "muted"
|
|
21
|
+
padded?: boolean
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -28,19 +29,20 @@ export interface AdjustableLayoutProps extends React.ComponentPropsWithoutRef<"d
|
|
|
28
29
|
* Supports localStorage persistence and responsive behavior.
|
|
29
30
|
*/
|
|
30
31
|
export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
31
|
-
({
|
|
32
|
-
sections,
|
|
33
|
-
orientation = "horizontal",
|
|
32
|
+
({
|
|
33
|
+
sections,
|
|
34
|
+
orientation = "horizontal",
|
|
34
35
|
storageKey,
|
|
35
36
|
onSectionResize,
|
|
36
37
|
dragHandleColor = "border",
|
|
37
38
|
className,
|
|
38
|
-
|
|
39
|
+
padded = false,
|
|
40
|
+
...props
|
|
39
41
|
}) => {
|
|
40
42
|
// Color mapping for drag handles
|
|
41
43
|
const colorMap = {
|
|
42
44
|
primary: "bg-primary hover:bg-primary/90",
|
|
43
|
-
secondary: "bg-secondary hover:bg-secondary/80",
|
|
45
|
+
secondary: "bg-secondary hover:bg-secondary/80",
|
|
44
46
|
accent: "bg-accent hover:bg-accent/80",
|
|
45
47
|
border: "bg-border hover:bg-border/80",
|
|
46
48
|
muted: "bg-muted hover:bg-muted/80"
|
|
@@ -49,7 +51,7 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
49
51
|
const hoverColorMap = {
|
|
50
52
|
primary: "group-hover:bg-primary/30",
|
|
51
53
|
secondary: "group-hover:bg-secondary/30",
|
|
52
|
-
accent: "group-hover:bg-accent/30",
|
|
54
|
+
accent: "group-hover:bg-accent/30",
|
|
53
55
|
border: "group-hover:bg-border/30",
|
|
54
56
|
muted: "group-hover:bg-muted/30"
|
|
55
57
|
}
|
|
@@ -77,7 +79,7 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
77
79
|
} catch {
|
|
78
80
|
// ignore malformed storage
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
83
|
}, [storageKey])
|
|
82
84
|
|
|
83
85
|
const [draggingIndex, setDraggingIndex] = React.useState<number | null>(null)
|
|
@@ -119,33 +121,33 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
119
121
|
|
|
120
122
|
const currentX = orientation === "horizontal" ? e.clientX : e.clientY
|
|
121
123
|
const deltaX = currentX - startX
|
|
122
|
-
|
|
124
|
+
|
|
123
125
|
const deltaPercent = (deltaX / containerSize) * 100
|
|
124
|
-
|
|
126
|
+
|
|
125
127
|
const p1 = sections[draggingIndex]
|
|
126
128
|
const p2 = sections[draggingIndex + 1]
|
|
127
|
-
|
|
129
|
+
|
|
128
130
|
const p1Start = startSizes[draggingIndex]
|
|
129
131
|
const p2Start = startSizes[draggingIndex + 1]
|
|
130
|
-
|
|
132
|
+
|
|
131
133
|
const p1Min = p1.minSize ?? 10
|
|
132
134
|
const p1Max = p1.maxSize ?? 80
|
|
133
135
|
const p2Min = p2.minSize ?? 10
|
|
134
136
|
const p2Max = p2.maxSize ?? 80
|
|
135
|
-
|
|
137
|
+
|
|
136
138
|
// Calculate how much we can actually change panel 1
|
|
137
139
|
// If deltaPercent > 0, we are growing p1 and shrinking p2
|
|
138
140
|
const maxPositiveDelta = Math.max(0, Math.min(
|
|
139
141
|
p1Max - p1Start, // Space p1 has to grow
|
|
140
142
|
p2Start - p2Min // Space p2 has to shrink
|
|
141
143
|
))
|
|
142
|
-
|
|
144
|
+
|
|
143
145
|
// If deltaPercent < 0, we are shrinking p1 and growing p2
|
|
144
146
|
const maxNegativeDelta = Math.min(0, Math.max(
|
|
145
147
|
p1Min - p1Start, // Space p1 has to shrink (negative)
|
|
146
148
|
p2Start - p2Max // Space p2 has to grow (negative)
|
|
147
149
|
))
|
|
148
|
-
|
|
150
|
+
|
|
149
151
|
// Clamp the delta
|
|
150
152
|
let clampedDelta = deltaPercent
|
|
151
153
|
if (clampedDelta > 0) {
|
|
@@ -153,17 +155,17 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
153
155
|
} else {
|
|
154
156
|
clampedDelta = Math.max(clampedDelta, maxNegativeDelta)
|
|
155
157
|
}
|
|
156
|
-
|
|
158
|
+
|
|
157
159
|
const newSizes = [...startSizes]
|
|
158
160
|
newSizes[draggingIndex] = p1Start + clampedDelta
|
|
159
161
|
newSizes[draggingIndex + 1] = p2Start - clampedDelta
|
|
160
|
-
|
|
162
|
+
|
|
161
163
|
// Normalize to 100% just in case of floating point drift
|
|
162
164
|
const total = newSizes.reduce((sum, size) => sum + size, 0)
|
|
163
165
|
const normalizedSizes = newSizes.map(size => (size / total) * 100)
|
|
164
|
-
|
|
166
|
+
|
|
165
167
|
setSizes(normalizedSizes)
|
|
166
|
-
|
|
168
|
+
|
|
167
169
|
// Notify parent
|
|
168
170
|
if (onSectionResize) {
|
|
169
171
|
onSectionResize(sections[draggingIndex].id, normalizedSizes[draggingIndex])
|
|
@@ -201,12 +203,12 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
201
203
|
? { flex: `0 0 ${section.fixedSize}`, width: section.fixedSize, minWidth: section.fixedSize }
|
|
202
204
|
: { flex: `0 0 ${section.fixedSize}`, height: section.fixedSize, minHeight: section.fixedSize }
|
|
203
205
|
: null
|
|
204
|
-
|
|
206
|
+
|
|
205
207
|
return (
|
|
206
208
|
<React.Fragment key={section.id}>
|
|
207
209
|
<div
|
|
208
210
|
className={cn(
|
|
209
|
-
"min-h-0 overflow-hidden bg-card border border-border rounded-
|
|
211
|
+
"min-h-0 overflow-hidden bg-card border border-border rounded-xl",
|
|
210
212
|
section.className
|
|
211
213
|
)}
|
|
212
214
|
style={{
|
|
@@ -217,27 +219,46 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
217
219
|
>
|
|
218
220
|
{section.content}
|
|
219
221
|
</div>
|
|
220
|
-
|
|
221
|
-
{/* Show drag handle after this panel if it's not the last one */}
|
|
222
|
-
{
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
222
|
+
|
|
223
|
+
{/* Show drag handle or spacer after this panel if it's not the last one */}
|
|
224
|
+
{index < sections.length - 1 && (
|
|
225
|
+
isResizable ? (
|
|
226
|
+
<div
|
|
227
|
+
className={cn(
|
|
228
|
+
"flex-shrink-0 flex items-center justify-center relative group",
|
|
229
|
+
orientation === "vertical"
|
|
230
|
+
? "cursor-row-resize h-2 w-full"
|
|
231
|
+
: "cursor-col-resize w-2 h-full"
|
|
232
|
+
)}
|
|
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
|
+
) : (
|
|
255
|
+
<div
|
|
233
256
|
className={cn(
|
|
234
|
-
|
|
235
|
-
orientation === "vertical"
|
|
236
|
-
|
|
237
|
-
: "w-3 -translate-x-1"
|
|
238
|
-
)}
|
|
257
|
+
"flex-shrink-0",
|
|
258
|
+
orientation === "vertical" ? "h-2 w-full" : "w-2 h-full"
|
|
259
|
+
)}
|
|
239
260
|
/>
|
|
240
|
-
|
|
261
|
+
)
|
|
241
262
|
)}
|
|
242
263
|
</React.Fragment>
|
|
243
264
|
)
|
|
@@ -248,12 +269,13 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
248
269
|
ref={containerRef}
|
|
249
270
|
className={cn(
|
|
250
271
|
"flex overflow-hidden h-full gap-1 min-w-0",
|
|
272
|
+
padded && "p-4",
|
|
251
273
|
orientation === "horizontal" ? "flex-row" : "flex-col",
|
|
252
274
|
className
|
|
253
275
|
)}
|
|
254
276
|
{...props}
|
|
255
277
|
>
|
|
256
|
-
{sections.map((section, index) =>
|
|
278
|
+
{sections.map((section, index) =>
|
|
257
279
|
renderPanel(section, sizes[index], index)
|
|
258
280
|
)}
|
|
259
281
|
</div>
|
|
@@ -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
|
+
}
|