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.
- package/components/blocks/SectionLayout/SectionLayout.tsx +12 -1
- package/components/blocks/SectionLayout/interfaces.ts +5 -0
- package/components/composites/AdjustableLayout/AdjustableLayout.stories.tsx +10 -0
- package/components/composites/AdjustableLayout/AdjustableLayout.tsx +274 -81
- package/components/composites/DocumentTabBar/DocumentTabBar.tsx +30 -11
- package/components/composites/FileTreeExplorer/FileTreeDrawer.tsx +62 -0
- package/components/composites/FileTreeExplorer/index.ts +6 -0
- package/components/composites/ProjectSwitcher/ProjectSwitcher.tsx +0 -1
- package/components/composites/WorkspaceEmptyState/WorkspaceEmptyState.tsx +17 -1
- package/components/composites/index.ts +4 -0
- package/components/features/TextEditor/TextEditor.stories.tsx +8 -0
- package/components/features/TextEditor/TextEditor.tsx +112 -13
- package/dist/index.cjs +1539 -1199
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +45 -50
- package/dist/index.d.ts +72 -26
- package/dist/index.js +1539 -1199
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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"
|
|
@@ -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 {
|
|
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-
|
|
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-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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-
|
|
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={
|
|
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',
|