ai-design-system 0.1.52 → 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.
@@ -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
+ );
@@ -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
- {isResizable && (
225
- <div
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
- `${colorMap[dragHandleColor]} transition-colors duration-200 rounded-full`,
228
+ "flex-shrink-0 flex items-center justify-center relative group",
238
229
  orientation === "vertical"
239
- ? "h-1 w-8"
240
- : "w-1 h-8"
230
+ ? "cursor-row-resize h-2 w-full"
231
+ : "cursor-col-resize w-2 h-full"
241
232
  )}
242
- />
243
- {/* Invisible large hit area */}
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
- "absolute z-10",
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
- </div>
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,95 @@
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
+ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
45
+ ({
46
+ tree,
47
+ defaultExpanded,
48
+ selectedPath,
49
+ onSelect,
50
+ searchPlaceholder = "Filter files...",
51
+ onCreateClick,
52
+ createButtonLabel,
53
+ headerClassName,
54
+ className,
55
+ }) => {
56
+ return (
57
+ <div className={cn("flex flex-col rounded-lg border bg-background", className)}>
58
+ <div className={cn("flex items-center gap-2 border-b px-3 py-2", headerClassName)}>
59
+ <div className="relative flex-1">
60
+ <Icon
61
+ name="search"
62
+ size="sm"
63
+ className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground"
64
+ />
65
+ <Input
66
+ placeholder={searchPlaceholder}
67
+ className="pl-8"
68
+ />
69
+ </div>
70
+ {onCreateClick ? (
71
+ <button
72
+ type="button"
73
+ onClick={onCreateClick}
74
+ 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"
75
+ >
76
+ <Icon name="plus" size="sm" />
77
+ {createButtonLabel ?? "New"}
78
+ </button>
79
+ ) : null}
80
+ </div>
81
+ <div className="p-2">
82
+ <FileTree
83
+ defaultExpanded={defaultExpanded}
84
+ selectedPath={selectedPath}
85
+ onSelect={onSelect}
86
+ >
87
+ {renderTree(tree)}
88
+ </FileTree>
89
+ </div>
90
+ </div>
91
+ )
92
+ }
93
+ )
94
+
95
+ FileTreeExplorer.displayName = "FileTreeExplorer"
@@ -0,0 +1,8 @@
1
+ export {
2
+ FileTreeExplorer,
3
+ } from "./FileTreeExplorer"
4
+
5
+ export type {
6
+ FileTreeExplorerProps,
7
+ FileTreeNode,
8
+ } from "./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: {} as any,
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
  }
@@ -0,0 +1,113 @@
1
+ import * as React from "react"
2
+
3
+ import type { FormReportsFieldDefinition, FormReportsValue, FormReportsValues } from "./FormReportsDrawerForm"
4
+ import { FormReportsDrawerForm } from "./FormReportsDrawerForm"
5
+ import type { DashboardPaginationState, FormReportsColumn, FormReportsEntity, FormReportsRowAction, FormReportsTableHandlers } from "./FormReportsTable"
6
+ import { FormReportsTable } from "./FormReportsTable"
7
+
8
+ export interface FormReportsProps {
9
+ items: FormReportsEntity[]
10
+ columns: FormReportsColumn[]
11
+ fields?: FormReportsFieldDefinition[]
12
+ rowActions?: FormReportsRowAction[]
13
+ pagination?: DashboardPaginationState
14
+ handlers?: FormReportsTableHandlers
15
+ leftActions?: React.ReactNode
16
+ rightActions?: React.ReactNode
17
+ drawerTitle?: string
18
+ drawerDescription?: string
19
+ submitLabel?: string
20
+ cancelLabel?: string
21
+ createButtonLabel?: string
22
+ onCreateSubmit?: (values: FormReportsValues) => void
23
+ }
24
+
25
+ export const FormReports = React.memo<FormReportsProps>(
26
+ ({
27
+ items,
28
+ columns,
29
+ fields,
30
+ rowActions,
31
+ pagination,
32
+ handlers,
33
+ leftActions,
34
+ rightActions,
35
+ drawerTitle = "New Item",
36
+ drawerDescription,
37
+ submitLabel = "Create",
38
+ cancelLabel = "Cancel",
39
+ createButtonLabel = "Create",
40
+ onCreateSubmit,
41
+ }) => {
42
+ const [drawerOpen, setDrawerOpen] = React.useState(false)
43
+ const [formValues, setFormValues] = React.useState<FormReportsValues>({})
44
+
45
+ const handleOpenChange = React.useCallback((open: boolean) => {
46
+ setDrawerOpen(open)
47
+ if (!open) {
48
+ setFormValues({})
49
+ }
50
+ }, [])
51
+
52
+ const handleFieldChange = React.useCallback(
53
+ (_name: string, _value: FormReportsValue, nextValues: FormReportsValues) => {
54
+ setFormValues(nextValues)
55
+ },
56
+ []
57
+ )
58
+
59
+ const handleCreateClick = React.useCallback(() => {
60
+ const defaults: FormReportsValues = {}
61
+ if (fields) {
62
+ for (const field of fields) {
63
+ if (field.defaultValue !== undefined) {
64
+ defaults[field.name] = field.defaultValue
65
+ }
66
+ }
67
+ }
68
+ setFormValues(defaults)
69
+ setDrawerOpen(true)
70
+ }, [fields])
71
+
72
+ const handleSubmit = React.useCallback(
73
+ (values: FormReportsValues) => {
74
+ onCreateSubmit?.(values)
75
+ setDrawerOpen(false)
76
+ setFormValues({})
77
+ },
78
+ [onCreateSubmit]
79
+ )
80
+
81
+ return (
82
+ <>
83
+ <FormReportsTable
84
+ items={items}
85
+ columns={columns}
86
+ rowActions={rowActions}
87
+ pagination={pagination}
88
+ handlers={handlers}
89
+ leftActions={leftActions}
90
+ rightActions={rightActions}
91
+ onCreateClick={handleCreateClick}
92
+ createButtonLabel={createButtonLabel}
93
+ />
94
+ {fields ? (
95
+ <FormReportsDrawerForm
96
+ open={drawerOpen}
97
+ onOpenChange={handleOpenChange}
98
+ title={drawerTitle}
99
+ description={drawerDescription}
100
+ fields={fields}
101
+ values={formValues}
102
+ submitLabel={submitLabel}
103
+ cancelLabel={cancelLabel}
104
+ onFieldChange={handleFieldChange}
105
+ onSubmit={handleSubmit}
106
+ />
107
+ ) : null}
108
+ </>
109
+ )
110
+ }
111
+ )
112
+
113
+ FormReports.displayName = "FormReports"