ai-design-system 0.1.114 → 0.1.116
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/WorkflowCanvas/WorkflowCanvas.tsx +40 -13
- package/components/composites/DocumentTabBar/DocumentTabBar.tsx +207 -62
- package/components/composites/DocumentTabBar/index.ts +1 -1
- package/components/composites/index.ts +1 -1
- package/components/ui/scroll-area.tsx +13 -2
- package/dist/index.cjs +220 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +8 -0
- package/dist/index.d.ts +90 -84
- package/dist/index.js +220 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type Node,
|
|
8
8
|
useReactFlow,
|
|
9
9
|
} from "@xyflow/react";
|
|
10
|
-
import { useCallback, useEffect } from "react";
|
|
10
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
11
11
|
import { Canvas } from "@/components/ai-elements/canvas";
|
|
12
12
|
import { Connection as ConnectionLine } from "@/components/ai-elements/connection";
|
|
13
13
|
import { Controls } from "@/components/ai-elements/controls";
|
|
@@ -58,6 +58,36 @@ function WorkflowCanvasInner({
|
|
|
58
58
|
className,
|
|
59
59
|
}: WorkflowCanvasProps) {
|
|
60
60
|
const { fitView } = useReactFlow();
|
|
61
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
62
|
+
const wasZeroSizeRef = useRef<boolean>(true);
|
|
63
|
+
|
|
64
|
+
// Automatically fit view when the canvas container transitions from hidden (0x0)
|
|
65
|
+
// to visible (e.g. mobile Overview icon clicked or tab activated)
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const el = containerRef.current;
|
|
68
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
69
|
+
|
|
70
|
+
const observer = new ResizeObserver((entries) => {
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const { width, height } = entry.contentRect;
|
|
73
|
+
if (width > 0 && height > 0) {
|
|
74
|
+
if (wasZeroSizeRef.current) {
|
|
75
|
+
wasZeroSizeRef.current = false;
|
|
76
|
+
// Delay slightly so xyflow's internal node measurements can settle
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
fitView({ padding: 0.2, duration: 300 });
|
|
79
|
+
}, 60);
|
|
80
|
+
return () => clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
wasZeroSizeRef.current = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
observer.observe(el);
|
|
89
|
+
return () => observer.disconnect();
|
|
90
|
+
}, [fitView]);
|
|
61
91
|
|
|
62
92
|
// Opt-in re-fit, e.g. after the device-preview toolbar's Route select
|
|
63
93
|
// switches to "All" and the node count/extent changes underneath the
|
|
@@ -67,20 +97,16 @@ function WorkflowCanvasInner({
|
|
|
67
97
|
// every such change would override the user's own pan/zoom constantly.
|
|
68
98
|
useEffect(() => {
|
|
69
99
|
if (fitViewSignal === undefined) return;
|
|
70
|
-
|
|
71
|
-
// own internal node measurement — verified live in both cases: the
|
|
72
|
-
// resulting scale was far smaller than the two device-preview nodes'
|
|
73
|
-
// true combined width needed (computed against their real DOM rects:
|
|
74
|
-
// ~1.2 would tightly fit them, but fitView produced ~0.57 and one node
|
|
75
|
-
// still landed partly outside the pane). xyflow measures each node via
|
|
76
|
-
// ResizeObserver, which fires as a separate, unsynchronized callback —
|
|
77
|
-
// not guaranteed to have run even two animation frames after the nodes
|
|
78
|
-
// prop change lands. A short real-time delay is the standard, if
|
|
79
|
-
// inelegant, workaround for this exact class of race.
|
|
80
|
-
const timer = setTimeout(() => {
|
|
100
|
+
const timer1 = setTimeout(() => {
|
|
81
101
|
fitView({ padding: 0.2, duration: 300 });
|
|
82
102
|
}, 50);
|
|
83
|
-
|
|
103
|
+
const timer2 = setTimeout(() => {
|
|
104
|
+
fitView({ padding: 0.2, duration: 300 });
|
|
105
|
+
}, 200);
|
|
106
|
+
return () => {
|
|
107
|
+
clearTimeout(timer1);
|
|
108
|
+
clearTimeout(timer2);
|
|
109
|
+
};
|
|
84
110
|
}, [fitViewSignal, fitView]);
|
|
85
111
|
|
|
86
112
|
useEffect(() => {
|
|
@@ -105,6 +131,7 @@ function WorkflowCanvasInner({
|
|
|
105
131
|
|
|
106
132
|
return (
|
|
107
133
|
<div
|
|
134
|
+
ref={containerRef}
|
|
108
135
|
className={className}
|
|
109
136
|
data-testid="workflow-canvas"
|
|
110
137
|
style={{
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Displays open documents as closeable tabs with dirty indicators.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React from 'react'
|
|
8
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
9
9
|
import { Tabs, TabsList, TabsTrigger, Button, ScrollArea } from '@/components/primitives'
|
|
10
10
|
import { Icon } from '@/components/primitives/Icon'
|
|
11
11
|
import { cn } from '@/lib/utils'
|
|
12
|
+
|
|
12
13
|
export interface DocumentFile {
|
|
13
14
|
id: string
|
|
14
15
|
name: string
|
|
@@ -50,6 +51,12 @@ export interface DocumentTabBarProps {
|
|
|
50
51
|
* Callback to open/toggle the file explorer (e.g. mobile drawer)
|
|
51
52
|
*/
|
|
52
53
|
onToggleExplorer?: () => void
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether to show navigation scroll arrows when tabs overflow
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
showScrollArrows?: boolean
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
/**
|
|
@@ -62,13 +69,103 @@ export interface DocumentTabBarProps {
|
|
|
62
69
|
* - Close button (X)
|
|
63
70
|
*
|
|
64
71
|
* Features:
|
|
65
|
-
* - Scrollable when tabs overflow horizontally
|
|
72
|
+
* - Scrollable when tabs overflow horizontally with multi-device support
|
|
73
|
+
* - Mouse wheel vertical-to-horizontal scroll translation
|
|
74
|
+
* - Auto-scroll active tab into view
|
|
75
|
+
* - Overflow indicators (edge gradients & navigation chevrons)
|
|
66
76
|
* - Accessible tab navigation via Radix UI Tabs
|
|
67
77
|
* - Close button to remove tabs
|
|
68
78
|
* - Dirty state indicator
|
|
69
79
|
*/
|
|
70
80
|
export const DocumentTabBar = React.memo<DocumentTabBarProps>(
|
|
71
|
-
({
|
|
81
|
+
({
|
|
82
|
+
tabs,
|
|
83
|
+
activeTabId,
|
|
84
|
+
onTabSelect,
|
|
85
|
+
onTabClose,
|
|
86
|
+
onToggleExplorer,
|
|
87
|
+
className,
|
|
88
|
+
showScrollArrows = true,
|
|
89
|
+
}) => {
|
|
90
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
91
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false)
|
|
92
|
+
const [canScrollRight, setCanScrollRight] = useState(false)
|
|
93
|
+
|
|
94
|
+
const getViewport = useCallback((): HTMLElement | null => {
|
|
95
|
+
return (
|
|
96
|
+
scrollContainerRef.current?.querySelector<HTMLElement>(
|
|
97
|
+
'[data-slot="scroll-area-viewport"]'
|
|
98
|
+
) ?? null
|
|
99
|
+
)
|
|
100
|
+
}, [])
|
|
101
|
+
|
|
102
|
+
const updateScrollState = useCallback(() => {
|
|
103
|
+
const viewport = getViewport()
|
|
104
|
+
if (!viewport) return
|
|
105
|
+
const { scrollLeft, scrollWidth, clientWidth } = viewport
|
|
106
|
+
setCanScrollLeft(scrollLeft > 2)
|
|
107
|
+
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 2)
|
|
108
|
+
}, [getViewport])
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const viewport = getViewport()
|
|
112
|
+
if (!viewport) return
|
|
113
|
+
|
|
114
|
+
updateScrollState()
|
|
115
|
+
viewport.addEventListener('scroll', updateScrollState, { passive: true })
|
|
116
|
+
|
|
117
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
118
|
+
updateScrollState()
|
|
119
|
+
})
|
|
120
|
+
resizeObserver.observe(viewport)
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
viewport.removeEventListener('scroll', updateScrollState)
|
|
124
|
+
resizeObserver.disconnect()
|
|
125
|
+
}
|
|
126
|
+
}, [getViewport, updateScrollState, tabs])
|
|
127
|
+
|
|
128
|
+
// Convert mouse wheel vertical scroll to horizontal scroll
|
|
129
|
+
const handleWheel = useCallback(
|
|
130
|
+
(e: React.WheelEvent) => {
|
|
131
|
+
if (e.deltaY === 0) return
|
|
132
|
+
const viewport = getViewport()
|
|
133
|
+
if (viewport) {
|
|
134
|
+
viewport.scrollLeft += e.deltaY
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
[getViewport]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// Auto-scroll active tab into view
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!activeTabId || !scrollContainerRef.current) return
|
|
143
|
+
const activeEl = scrollContainerRef.current.querySelector<HTMLElement>(
|
|
144
|
+
'[data-state="active"]'
|
|
145
|
+
)
|
|
146
|
+
if (activeEl) {
|
|
147
|
+
activeEl.scrollIntoView({
|
|
148
|
+
behavior: 'smooth',
|
|
149
|
+
block: 'nearest',
|
|
150
|
+
inline: 'nearest',
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
}, [activeTabId, tabs])
|
|
154
|
+
|
|
155
|
+
const handleScrollLeft = useCallback(() => {
|
|
156
|
+
const viewport = getViewport()
|
|
157
|
+
if (viewport) {
|
|
158
|
+
viewport.scrollBy({ left: -200, behavior: 'smooth' })
|
|
159
|
+
}
|
|
160
|
+
}, [getViewport])
|
|
161
|
+
|
|
162
|
+
const handleScrollRight = useCallback(() => {
|
|
163
|
+
const viewport = getViewport()
|
|
164
|
+
if (viewport) {
|
|
165
|
+
viewport.scrollBy({ left: 200, behavior: 'smooth' })
|
|
166
|
+
}
|
|
167
|
+
}, [getViewport])
|
|
168
|
+
|
|
72
169
|
if (tabs.length === 0 && !onToggleExplorer) {
|
|
73
170
|
return null
|
|
74
171
|
}
|
|
@@ -76,7 +173,7 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
|
|
|
76
173
|
return (
|
|
77
174
|
<div
|
|
78
175
|
className={cn(
|
|
79
|
-
'flex items-center border-b border-border bg-background',
|
|
176
|
+
'flex items-center border-b border-border bg-background relative',
|
|
80
177
|
className
|
|
81
178
|
)}
|
|
82
179
|
data-slot="document-tab-bar"
|
|
@@ -92,67 +189,114 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
|
|
|
92
189
|
<Icon name="folder" size="xs" />
|
|
93
190
|
</Button>
|
|
94
191
|
)}
|
|
192
|
+
|
|
95
193
|
{tabs.length > 0 && (
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
194
|
+
<div
|
|
195
|
+
ref={scrollContainerRef}
|
|
196
|
+
onWheel={handleWheel}
|
|
197
|
+
className="flex-1 min-w-0 flex items-center relative"
|
|
198
|
+
>
|
|
199
|
+
{showScrollArrows && canScrollLeft && (
|
|
200
|
+
<Button
|
|
201
|
+
variant="ghost"
|
|
202
|
+
size="icon-sm"
|
|
203
|
+
onClick={handleScrollLeft}
|
|
204
|
+
className="h-10 w-7 shrink-0 rounded-none border-r border-border/50 text-muted-foreground hover:text-foreground transition-colors z-10"
|
|
205
|
+
aria-label="Scroll tabs left"
|
|
206
|
+
>
|
|
207
|
+
<Icon name="chevron-left" size="xs" />
|
|
208
|
+
</Button>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
<div className="flex-1 min-w-0 relative h-10">
|
|
212
|
+
{canScrollLeft && (
|
|
104
213
|
<div
|
|
105
|
-
|
|
106
|
-
|
|
214
|
+
className="pointer-events-none absolute left-0 top-0 bottom-0 w-5 bg-gradient-to-r from-background to-transparent z-10"
|
|
215
|
+
aria-hidden="true"
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<ScrollArea orientation="horizontal" className="w-full h-10">
|
|
220
|
+
<Tabs
|
|
221
|
+
value={activeTabId || tabs[0]?.id}
|
|
222
|
+
onValueChange={onTabSelect}
|
|
223
|
+
className="h-auto flex-1"
|
|
107
224
|
>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
225
|
+
<TabsList className="h-10 rounded-none border-none bg-transparent p-0 w-full justify-start gap-0">
|
|
226
|
+
{tabs.map((tab) => (
|
|
227
|
+
<div
|
|
228
|
+
key={tab.id}
|
|
229
|
+
className="flex items-center shrink-0 border-r border-border/50 last:border-r-0"
|
|
230
|
+
>
|
|
231
|
+
<TabsTrigger
|
|
232
|
+
value={tab.id}
|
|
233
|
+
className={cn(
|
|
234
|
+
'data-[state=inactive]:bg-muted/20 data-[state=inactive]:text-muted-foreground',
|
|
235
|
+
'rounded-none border-b-2 border-transparent data-[state=active]:border-primary',
|
|
236
|
+
'px-2.5 sm:px-3 py-2 text-xs sm:text-sm font-medium whitespace-nowrap',
|
|
237
|
+
'flex items-center gap-1.5 sm:gap-2 h-10',
|
|
238
|
+
'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0',
|
|
239
|
+
'transition-colors'
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
243
|
+
{/* Dirty indicator */}
|
|
244
|
+
{tab.isDirty && (
|
|
245
|
+
<Icon
|
|
246
|
+
name="circle"
|
|
247
|
+
className="size-2 fill-primary text-primary"
|
|
248
|
+
aria-label="unsaved changes"
|
|
249
|
+
/>
|
|
250
|
+
)}
|
|
251
|
+
{/* Tab name */}
|
|
252
|
+
<span className="truncate max-w-[120px] sm:max-w-[200px]">{tab.name}</span>
|
|
253
|
+
</div>
|
|
254
|
+
</TabsTrigger>
|
|
255
|
+
|
|
256
|
+
{/* Close button */}
|
|
257
|
+
<Button
|
|
258
|
+
variant="ghost"
|
|
259
|
+
size="icon-sm"
|
|
260
|
+
onClick={(e) => {
|
|
261
|
+
e.stopPropagation()
|
|
262
|
+
onTabClose?.(tab.id)
|
|
263
|
+
}}
|
|
264
|
+
className={cn(
|
|
265
|
+
'h-8 w-8 mr-0.5',
|
|
266
|
+
'hover:bg-destructive/10 hover:text-destructive',
|
|
267
|
+
'focus-visible:ring-2 focus-visible:ring-ring',
|
|
268
|
+
'transition-colors'
|
|
269
|
+
)}
|
|
270
|
+
aria-label={`Close ${tab.name}`}
|
|
271
|
+
>
|
|
272
|
+
<Icon name="x" size="xs" />
|
|
273
|
+
</Button>
|
|
274
|
+
</div>
|
|
275
|
+
))}
|
|
276
|
+
</TabsList>
|
|
277
|
+
</Tabs>
|
|
278
|
+
</ScrollArea>
|
|
279
|
+
|
|
280
|
+
{canScrollRight && (
|
|
281
|
+
<div
|
|
282
|
+
className="pointer-events-none absolute right-0 top-0 bottom-0 w-5 bg-gradient-to-l from-background to-transparent z-10"
|
|
283
|
+
aria-hidden="true"
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{showScrollArrows && canScrollRight && (
|
|
289
|
+
<Button
|
|
290
|
+
variant="ghost"
|
|
291
|
+
size="icon-sm"
|
|
292
|
+
onClick={handleScrollRight}
|
|
293
|
+
className="h-10 w-7 shrink-0 rounded-none border-l border-border/50 text-muted-foreground hover:text-foreground transition-colors z-10"
|
|
294
|
+
aria-label="Scroll tabs right"
|
|
295
|
+
>
|
|
296
|
+
<Icon name="chevron-right" size="xs" />
|
|
297
|
+
</Button>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
156
300
|
)}
|
|
157
301
|
</div>
|
|
158
302
|
)
|
|
@@ -160,3 +304,4 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
|
|
|
160
304
|
)
|
|
161
305
|
|
|
162
306
|
DocumentTabBar.displayName = 'DocumentTabBar'
|
|
307
|
+
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { DocumentTabBar } from './DocumentTabBar'
|
|
2
|
-
export type { DocumentTabBarProps } from './DocumentTabBar'
|
|
2
|
+
export type { DocumentTabBarProps, DocumentFile } from './DocumentTabBar'
|
|
@@ -75,7 +75,7 @@ export type { ModeToggleProps } from './ModeToggle'
|
|
|
75
75
|
|
|
76
76
|
// DocumentTabBar Composite
|
|
77
77
|
export { DocumentTabBar } from './DocumentTabBar'
|
|
78
|
-
export type { DocumentTabBarProps } from './DocumentTabBar'
|
|
78
|
+
export type { DocumentTabBarProps, DocumentFile } from './DocumentTabBar'
|
|
79
79
|
|
|
80
80
|
// DefaultSwitcher Composite
|
|
81
81
|
export { DefaultSwitcher } from './DefaultSwitcher'
|
|
@@ -5,11 +5,17 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
|
|
5
5
|
|
|
6
6
|
import { cn } from "@/lib/utils"
|
|
7
7
|
|
|
8
|
+
export interface ScrollAreaProps
|
|
9
|
+
extends React.ComponentProps<typeof ScrollAreaPrimitive.Root> {
|
|
10
|
+
orientation?: "vertical" | "horizontal" | "both"
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
function ScrollArea({
|
|
9
14
|
className,
|
|
10
15
|
children,
|
|
16
|
+
orientation = "vertical",
|
|
11
17
|
...props
|
|
12
|
-
}:
|
|
18
|
+
}: ScrollAreaProps) {
|
|
13
19
|
return (
|
|
14
20
|
<ScrollAreaPrimitive.Root
|
|
15
21
|
data-slot="scroll-area"
|
|
@@ -22,7 +28,12 @@ function ScrollArea({
|
|
|
22
28
|
>
|
|
23
29
|
{children}
|
|
24
30
|
</ScrollAreaPrimitive.Viewport>
|
|
25
|
-
|
|
31
|
+
{(orientation === "vertical" || orientation === "both") && (
|
|
32
|
+
<ScrollBar orientation="vertical" />
|
|
33
|
+
)}
|
|
34
|
+
{(orientation === "horizontal" || orientation === "both") && (
|
|
35
|
+
<ScrollBar orientation="horizontal" className="h-1.5 border-t border-t-transparent" />
|
|
36
|
+
)}
|
|
26
37
|
<ScrollAreaPrimitive.Corner />
|
|
27
38
|
</ScrollAreaPrimitive.Root>
|
|
28
39
|
)
|