ai-design-system 0.1.111 → 0.1.113

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.
@@ -1,9 +1,15 @@
1
1
  export {
2
2
  FileTreeExplorer,
3
3
  } from "./FileTreeExplorer"
4
+ export {
5
+ FileTreeDrawer,
6
+ } from "./FileTreeDrawer"
4
7
 
5
8
  export type {
6
9
  FileDownloadResult,
7
10
  FileTreeExplorerProps,
8
11
  FileTreeNode,
9
12
  } from "./FileTreeExplorer"
13
+ export type {
14
+ FileTreeDrawerProps,
15
+ } from "./FileTreeDrawer"
@@ -17,33 +17,81 @@ import {
17
17
  PopoverTrigger,
18
18
  } from "@/components/primitives/Popover"
19
19
 
20
- export interface Project {
20
+ export interface ProjectSwitcherItem {
21
21
  id: string
22
22
  name: string
23
23
  }
24
24
 
25
+ // Backwards compatibility alias
26
+ export type Project = ProjectSwitcherItem
27
+
25
28
  export interface ProjectSwitcherProps {
26
- projects: Project[]
29
+ /** List of projects / channels / workspaces to switch between */
30
+ projects?: ProjectSwitcherItem[]
31
+ /** Generic alias for projects */
32
+ items?: ProjectSwitcherItem[]
33
+ /** ID of the currently selected item */
27
34
  selectedProjectId?: string | null
28
- onSelectProject: (id: string) => void
29
- onCreateProject: () => void
35
+ /** Generic alias for selectedProjectId */
36
+ selectedId?: string | null
37
+ /** Callback fired when an item is selected */
38
+ onSelectProject?: (id: string) => void
39
+ /** Generic alias for onSelectProject */
40
+ onSelect?: (id: string) => void
41
+ /** Callback fired when creating a new item */
42
+ onCreateProject?: () => void
43
+ /** Generic alias for onCreateProject */
44
+ onCreate?: () => void
45
+ /** Custom CSS class names */
30
46
  className?: string
47
+ /**
48
+ * Domain entity name used to automatically derive labels (e.g. "Project", "Channel", "Workspace").
49
+ * @default "Project"
50
+ */
51
+ entityName?: string
52
+ /** Placeholder when nothing is selected. Defaults to "Select <entityName>..." */
53
+ placeholder?: string
54
+ /** Search input placeholder. Defaults to "Find <entityName>..." */
55
+ searchPlaceholder?: string
56
+ /** Empty state message. Defaults to "No <entityName.toLowerCase()>s, yet!" */
57
+ emptyMessage?: string
58
+ /** Create button label. Defaults to "Create <entityName>" */
59
+ createButtonText?: string
31
60
  }
32
61
 
33
62
  export const ProjectSwitcher = React.memo<ProjectSwitcherProps>(
34
63
  ({
35
- projects = [],
64
+ projects,
65
+ items,
36
66
  selectedProjectId,
37
- onSelectProject = () => {},
38
- onCreateProject = () => {},
67
+ selectedId,
68
+ onSelectProject,
69
+ onSelect,
70
+ onCreateProject,
71
+ onCreate,
39
72
  className,
73
+ entityName = "Project",
74
+ placeholder,
75
+ searchPlaceholder,
76
+ emptyMessage,
77
+ createButtonText,
40
78
  }) => {
41
79
  const [open, setOpen] = React.useState(false)
42
80
 
43
- const selectedProject = projects.find((p) => String(p.id) === String(selectedProjectId))
44
- const displayLabel = selectedProject ? selectedProject.name : "Select Project..."
81
+ const list = items ?? projects ?? []
82
+ const activeId = selectedId !== undefined ? selectedId : selectedProjectId
83
+ const handleSelect = onSelect ?? onSelectProject ?? (() => {})
84
+ const handleCreate = onCreate ?? onCreateProject ?? (() => {})
85
+
86
+ const resolvedPlaceholder = placeholder ?? `Select ${entityName}...`
87
+ const resolvedSearchPlaceholder = searchPlaceholder ?? `Find ${entityName}...`
88
+ const resolvedEmptyMessage = emptyMessage ?? `No ${entityName.toLowerCase()}s, yet!`
89
+ const resolvedCreateButtonText = createButtonText ?? `Create ${entityName}`
90
+
91
+ const selectedItem = list.find((p) => String(p.id) === String(activeId))
92
+ const displayLabel = selectedItem ? selectedItem.name : resolvedPlaceholder
45
93
 
46
- const defaultValue = selectedProject ? `${selectedProject.name}-${selectedProject.id}` : undefined
94
+ const defaultValue = selectedItem ? `${selectedItem.name}-${selectedItem.id}` : undefined
47
95
 
48
96
  return (
49
97
  <Popover open={open} onOpenChange={setOpen}>
@@ -64,7 +112,7 @@ export const ProjectSwitcher = React.memo<ProjectSwitcherProps>(
64
112
  <Command defaultValue={defaultValue}>
65
113
  <div className="flex items-center justify-between border-b border-neutral-600 px-3" cmdk-input-wrapper="">
66
114
  <CommandPrimitive.Input
67
- placeholder="Find Project..."
115
+ placeholder={resolvedSearchPlaceholder}
68
116
  className="flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
69
117
  />
70
118
  <kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border border-neutral-700 bg-neutral-800 px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">Esc</kbd>
@@ -72,27 +120,28 @@ export const ProjectSwitcher = React.memo<ProjectSwitcherProps>(
72
120
  <CommandList className="max-h-[300px]">
73
121
  <CommandEmpty>
74
122
  <div className="flex flex-col items-center justify-center p-6 text-center text-sm text-muted-foreground">
75
- No projects, yet!
123
+ {resolvedEmptyMessage}
76
124
  </div>
77
125
  </CommandEmpty>
78
- {projects.length > 0 && (
126
+ {list.length > 0 && (
79
127
  <CommandGroup>
80
- {projects.map((project) => (
128
+ {list.map((item) => (
81
129
  <CommandItem
82
- key={project.id}
83
- value={`${project.name}-${project.id}`}
130
+ key={item.id}
131
+ value={`${item.name}-${item.id}`}
84
132
  onSelect={() => {
85
- onSelectProject(project.id)
133
+ handleSelect(item.id)
86
134
  setOpen(false)
87
135
  }}
88
136
  className="mb-1 last:mb-0 cursor-pointer flex items-center justify-between"
89
137
  >
90
138
  <div className="flex items-center">
91
- {project.name}
139
+ {item.name}
92
140
  </div>
93
- <Icon name="check"
141
+ <Icon
142
+ name="check"
94
143
  className={`h-4 w-4 ${
95
- String(selectedProjectId) === String(project.id) ? "opacity-100" : "opacity-0"
144
+ String(activeId) === String(item.id) ? "opacity-100" : "opacity-0"
96
145
  }`}
97
146
  />
98
147
  </CommandItem>
@@ -107,11 +156,11 @@ export const ProjectSwitcher = React.memo<ProjectSwitcherProps>(
107
156
  className="w-full justify-start text-sm font-normal"
108
157
  onClick={() => {
109
158
  setOpen(false)
110
- onCreateProject()
159
+ handleCreate()
111
160
  }}
112
161
  >
113
162
  <Icon name="plus" size="sm" className="mr-2 h-4 w-4" />
114
- Create Project
163
+ {resolvedCreateButtonText}
115
164
  </Button>
116
165
  </div>
117
166
  </Command>
@@ -13,6 +13,12 @@ export interface WorkspaceEmptyStateProps {
13
13
  isWaking?: boolean
14
14
  /** Additional CSS classes applied to the root container. */
15
15
  className?: string
16
+ /** Label for secondary or general action button */
17
+ actionLabel?: string
18
+ /** Callback for action button */
19
+ onAction?: () => void
20
+ /** Additional CSS classes for action button */
21
+ actionClassName?: string
16
22
  }
17
23
 
18
24
  /**
@@ -34,7 +40,7 @@ export interface WorkspaceEmptyStateProps {
34
40
  * ```
35
41
  */
36
42
  export const WorkspaceEmptyState = React.memo<WorkspaceEmptyStateProps>(
37
- ({ label, onWake, isWaking, className }) => {
43
+ ({ label, onWake, isWaking, actionLabel, onAction, actionClassName, className }) => {
38
44
  return (
39
45
  <div
40
46
  className={`flex flex-col items-center justify-center gap-3 h-full w-full${className ? ` ${className}` : ''}`}
@@ -52,6 +58,16 @@ export const WorkspaceEmptyState = React.memo<WorkspaceEmptyStateProps>(
52
58
  {isWaking ? 'Starting…' : 'Start workspace'}
53
59
  </Button>
54
60
  )}
61
+ {onAction && (
62
+ <Button
63
+ variant="outline"
64
+ size="sm"
65
+ onClick={onAction}
66
+ className={actionClassName}
67
+ >
68
+ {actionLabel ?? 'Browse files'}
69
+ </Button>
70
+ )}
55
71
  </div>
56
72
  )
57
73
  }
@@ -196,7 +196,7 @@ export type { EmptyStateProps } from './EmptyState'
196
196
 
197
197
  // ProjectSwitcher Composite
198
198
  export { ProjectSwitcher } from './ProjectSwitcher'
199
- export type { ProjectSwitcherProps, Project } from './ProjectSwitcher'
199
+ export type { ProjectSwitcherProps, ProjectSwitcherItem, Project } from './ProjectSwitcher'
200
200
 
201
201
  // DashboardChart Composite
202
202
  export { DashboardChart } from './DashboardChart'
@@ -294,4 +294,8 @@ export type { StoryPlayerProps, ScenePlayerProps, StorySegment, SceneItem } from
294
294
  export { AccountDialog, AccountButton } from './AccountDialog'
295
295
  export type { AccountDialogProps, AccountButtonProps, AccountUser } from './AccountDialog'
296
296
 
297
+ // FileTreeExplorer Composite
298
+ export { FileTreeExplorer, FileTreeDrawer } from './FileTreeExplorer'
299
+ export type { FileTreeExplorerProps, FileTreeDrawerProps, FileTreeNode, FileDownloadResult } from './FileTreeExplorer'
300
+
297
301
 
@@ -69,6 +69,14 @@ export const Empty: Story = {
69
69
  * multiple documents. Uses mock hook for realistic interaction simulation.
70
70
  */
71
71
  export const WithStateManagement: Story = {
72
+ parameters: {
73
+ docs: {
74
+ description: {
75
+ story:
76
+ 'Multi-tab interactive mode with full state management. On desktop (>= 768px), displays a split view with the file explorer and document editor. On mobile (< 768px), the editor pane takes 100% width, and the file explorer is opened via a mobile Drawer triggered from the tab bar or files button.',
77
+ },
78
+ },
79
+ },
72
80
  render: () => {
73
81
  const mockState = useTextEditorMock({
74
82
  multiDoc: true,
@@ -36,12 +36,12 @@ import React, { useCallback, useMemo } from 'react'
36
36
  import { DocumentEditorWithComments } from '@/components/blocks/DocumentEditorWithComments'
37
37
  import { DocumentTabBar } from '@/components/composites/DocumentTabBar'
38
38
  import { AdjustableLayout } from '@/components/composites/AdjustableLayout'
39
- import { FileTreeExplorer } from '@/components/composites/FileTreeExplorer'
39
+ import { FileTreeExplorer, FileTreeDrawer } from '@/components/composites/FileTreeExplorer'
40
40
  import type { FileDownloadResult, FileTreeNode } from '@/components/composites/FileTreeExplorer'
41
41
  import { cn } from '@/lib/utils'
42
42
  import type { JSONContent } from '@tiptap/core'
43
43
  import type { Annotation, User } from '@/types/ai-editor/annotations'
44
- import type { DocumentFile, DocumentWithAnnotations } from '@/types/ai-editor/editor'
44
+ import type { DocumentWithAnnotations } from '@/types/ai-editor/editor'
45
45
  import { StreamingMarkdown } from '@/components/composites/StreamingMarkdown'
46
46
  import { MediaPreview, isMediaFile } from '@/components/composites/MediaPreview'
47
47
  import { WorkspaceEmptyState } from '@/components/composites/WorkspaceEmptyState'
@@ -128,6 +128,10 @@ export interface TextEditorMultiTabProps {
128
128
  * of the normal editor UI.
129
129
  */
130
130
  workspace?: TextEditorWorkspaceProps
131
+ /** Active section in mobile view ('explorer' or 'editor') */
132
+ mobileSection?: 'explorer' | 'editor'
133
+ /** Callback when mobile section changes */
134
+ onMobileSectionChange?: (section: 'explorer' | 'editor') => void
131
135
  }
132
136
 
133
137
  /**
@@ -174,6 +178,23 @@ function formatJson(text: string): string {
174
178
  }
175
179
  }
176
180
 
181
+ function useIsMobile() {
182
+ const [isMobile, setIsMobile] = React.useState(() => {
183
+ if (typeof window !== 'undefined') {
184
+ return window.innerWidth < 768
185
+ }
186
+ return false
187
+ })
188
+
189
+ React.useEffect(() => {
190
+ const onResize = () => setIsMobile(window.innerWidth < 768)
191
+ window.addEventListener('resize', onResize)
192
+ return () => window.removeEventListener('resize', onResize)
193
+ }, [])
194
+
195
+ return isMobile
196
+ }
197
+
177
198
  /**
178
199
  * TextEditor - Document editor with inline comment annotations
179
200
  *
@@ -281,6 +302,33 @@ export const TextEditor = React.memo<TextEditorProps>(
281
302
 
282
303
  const handleTabSelect = isMultiTab ? (props as TextEditorMultiTabProps).onTabSelect : undefined
283
304
 
305
+ const isMobile = useIsMobile()
306
+ const [isDrawerOpenState, setIsDrawerOpenState] = React.useState(false)
307
+
308
+ const isDrawerOpen =
309
+ (isMultiTab && (props as TextEditorMultiTabProps).mobileSection === 'explorer') ||
310
+ isDrawerOpenState
311
+
312
+ const handleOpenChange = useCallback(
313
+ (open: boolean) => {
314
+ setIsDrawerOpenState(open)
315
+ if (isMultiTab) {
316
+ (props as TextEditorMultiTabProps).onMobileSectionChange?.(open ? 'explorer' : 'editor')
317
+ }
318
+ },
319
+ [isMultiTab, props]
320
+ )
321
+
322
+ const handleFileSelect = useCallback(
323
+ (path: string) => {
324
+ if (handleTabSelect) {
325
+ handleTabSelect(path)
326
+ }
327
+ handleOpenChange(false)
328
+ },
329
+ [handleTabSelect, handleOpenChange]
330
+ )
331
+
284
332
  const LinkComponent = useCallback((linkProps: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
285
333
  const { href, ...rest } = linkProps
286
334
  const isExternal = href?.startsWith('http://') || href?.startsWith('https://') || href?.startsWith('mailto:')
@@ -338,6 +386,7 @@ export const TextEditor = React.memo<TextEditorProps>(
338
386
  if (handleTabSelect) {
339
387
  handleTabSelect(targetId)
340
388
  }
389
+ handleOpenChange(false)
341
390
  }
342
391
  }
343
392
 
@@ -345,7 +394,7 @@ export const TextEditor = React.memo<TextEditorProps>(
345
394
  return <a href={href} target="_blank" rel="noopener noreferrer" {...rest} />
346
395
  }
347
396
  return <a href={href} onClick={handleClick} className="text-primary hover:underline cursor-pointer" {...rest} />
348
- }, [handleTabSelect, isMultiTab, props.activeDocumentId, props.documents])
397
+ }, [handleOpenChange, handleTabSelect, isMultiTab, props.activeDocumentId, props.documents])
349
398
 
350
399
  const markdownComponents = useMemo(() => ({ a: LinkComponent }), [LinkComponent])
351
400
 
@@ -363,7 +412,7 @@ export const TextEditor = React.memo<TextEditorProps>(
363
412
  ? `\`\`\`${format}\n${format === 'json' ? formatJson(contentStr) : contentStr}\n\`\`\``
364
413
  : contentStr
365
414
  return (
366
- <div className={cn('text-editor p-6 flex flex-col h-screen w-full flex-1', className)}>
415
+ <div className={cn('text-editor p-3 sm:p-6 flex flex-col h-full min-h-0 w-full flex-1 overflow-auto', className)}>
367
416
  <StreamingMarkdown mode="static" isAnimating={false} components={markdownComponents}>
368
417
  {content}
369
418
  </StreamingMarkdown>
@@ -371,7 +420,7 @@ export const TextEditor = React.memo<TextEditorProps>(
371
420
  )
372
421
  }
373
422
  return (
374
- <div className={cn('text-editor flex flex-col h-screen w-full flex-1', className)}>
423
+ <div className={cn('text-editor flex flex-col h-full min-h-0 w-full flex-1', className)}>
375
424
  <DocumentEditorWithComments
376
425
  content={props.content}
377
426
  format={props.format}
@@ -382,7 +431,7 @@ export const TextEditor = React.memo<TextEditorProps>(
382
431
  readOnly={mode === 'readonly'}
383
432
  onAnnotationAdd={handleAnnotationAdd}
384
433
  onAnnotationUpdate={handleAnnotationUpdate}
385
- className={cn('text-editor p-6 h-full flex flex-col', className)}
434
+ className={cn('text-editor p-3 sm:p-6 h-full flex flex-col', className)}
386
435
  />
387
436
  </div>
388
437
  )
@@ -421,11 +470,22 @@ export const TextEditor = React.memo<TextEditorProps>(
421
470
  activeTabId={props.activeDocumentId}
422
471
  onTabSelect={props.onTabSelect}
423
472
  onTabClose={props.onTabClose}
473
+ onToggleExplorer={() => handleOpenChange(true)}
424
474
  />
425
475
  )}
426
- <div className="flex-1 flex items-center justify-center p-6 text-muted-foreground">
427
- No documents selected
428
- </div>
476
+ {hideTabBar && (
477
+ <DocumentTabBar
478
+ tabs={[]}
479
+ onToggleExplorer={() => handleOpenChange(true)}
480
+ className="md:hidden"
481
+ />
482
+ )}
483
+ <WorkspaceEmptyState
484
+ label="No documents selected"
485
+ actionLabel="Browse files"
486
+ onAction={() => handleOpenChange(true)}
487
+ actionClassName="md:hidden"
488
+ />
429
489
  </div>
430
490
  )
431
491
  } else if (isMediaFile(currentDocument.file)) {
@@ -438,6 +498,14 @@ export const TextEditor = React.memo<TextEditorProps>(
438
498
  activeTabId={props.activeDocumentId}
439
499
  onTabSelect={props.onTabSelect}
440
500
  onTabClose={props.onTabClose}
501
+ onToggleExplorer={() => handleOpenChange(true)}
502
+ />
503
+ )}
504
+ {hideTabBar && (
505
+ <DocumentTabBar
506
+ tabs={[]}
507
+ onToggleExplorer={() => handleOpenChange(true)}
508
+ className="md:hidden"
441
509
  />
442
510
  )}
443
511
  <div className="flex-1 overflow-auto">
@@ -468,6 +536,14 @@ export const TextEditor = React.memo<TextEditorProps>(
468
536
  activeTabId={props.activeDocumentId}
469
537
  onTabSelect={props.onTabSelect}
470
538
  onTabClose={props.onTabClose}
539
+ onToggleExplorer={() => handleOpenChange(true)}
540
+ />
541
+ )}
542
+ {hideTabBar && (
543
+ <DocumentTabBar
544
+ tabs={[]}
545
+ onToggleExplorer={() => handleOpenChange(true)}
546
+ className="md:hidden"
471
547
  />
472
548
  )}
473
549
  <div className="flex-1 overflow-auto">
@@ -476,7 +552,7 @@ export const TextEditor = React.memo<TextEditorProps>(
476
552
  Loading document content...
477
553
  </div>
478
554
  ) : renderAsStreamdown ? (
479
- <div className="p-6">
555
+ <div className="p-3 sm:p-6">
480
556
  <StreamingMarkdown
481
557
  mode="static"
482
558
  isAnimating={false}
@@ -499,7 +575,7 @@ export const TextEditor = React.memo<TextEditorProps>(
499
575
  readOnly={mode === 'readonly'}
500
576
  onAnnotationAdd={handleAnnotationAdd}
501
577
  onAnnotationUpdate={handleAnnotationUpdate}
502
- className="text-editor p-6 min-h-full"
578
+ className="text-editor p-3 sm:p-6 min-h-full"
503
579
  />
504
580
  )}
505
581
  </div>
@@ -507,13 +583,35 @@ export const TextEditor = React.memo<TextEditorProps>(
507
583
  )
508
584
  }
509
585
 
586
+ const fileTreeDrawer = (
587
+ <FileTreeDrawer
588
+ open={isDrawerOpen}
589
+ onOpenChange={handleOpenChange}
590
+ tree={fileTreeNodes}
591
+ selectedPath={props.activeDocumentId}
592
+ onSelect={handleFileSelect}
593
+ onDownload={onDownloadAllFiles}
594
+ />
595
+ )
596
+
597
+ if (isMobile) {
598
+ return (
599
+ <div className={cn("text-editor flex-1 h-full min-h-0 w-full bg-background flex flex-col relative", className)}>
600
+ {editorPane}
601
+ {fileTreeDrawer}
602
+ </div>
603
+ )
604
+ }
605
+
510
606
  return (
511
607
  <AdjustableLayout
512
- className={cn("flex-1 h-screen w-full bg-background", className)}
608
+ className={cn("flex-1 h-full min-h-0 w-full bg-background", className)}
513
609
  orientation="horizontal"
610
+ mobileBehavior="none"
514
611
  sections={[
515
612
  {
516
613
  id: 'explorer',
614
+ label: 'Files',
517
615
  defaultSize: 20,
518
616
  minSize: 15,
519
617
  maxSize: 30,
@@ -524,13 +622,14 @@ export const TextEditor = React.memo<TextEditorProps>(
524
622
  headerClassName="border-b"
525
623
  tree={fileTreeNodes}
526
624
  selectedPath={props.activeDocumentId}
527
- onSelect={props.onTabSelect}
625
+ onSelect={handleFileSelect}
528
626
  onDownload={onDownloadAllFiles}
529
627
  />
530
628
  ),
531
629
  },
532
630
  {
533
631
  id: 'editor',
632
+ label: 'Editor',
534
633
  defaultSize: 80,
535
634
  minSize: 50,
536
635
  className: 'rounded-none border-0',
@@ -54,6 +54,7 @@ export type {
54
54
  ActionRequest,
55
55
  ReviewConfig,
56
56
  ProjectSwitcherProps,
57
+ ProjectSwitcherItem,
57
58
  Project,
58
59
  FormReportsDrawerFormProps,
59
60
  SessionHeaderProps,