ai-design-system 0.1.112 → 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"
@@ -20,7 +20,6 @@ import {
20
20
  export interface ProjectSwitcherItem {
21
21
  id: string
22
22
  name: string
23
- [key: string]: unknown
24
23
  }
25
24
 
26
25
  // Backwards compatibility alias
@@ -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
  }
@@ -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',