ai-design-system 0.1.115 → 0.1.117
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/composites/DocumentTabBar/DocumentTabBar.tsx +207 -62
- package/components/composites/DocumentTabBar/index.ts +1 -1
- package/components/composites/SessionHeader/SessionHeader.tsx +30 -6
- package/components/composites/index.ts +1 -1
- package/components/features/ChatPanel/ChatPanel.tsx +15 -0
- package/components/index.ts +3 -0
- package/components/ui/scroll-area.tsx +13 -2
- package/dist/index.cjs +239 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +8 -0
- package/dist/index.d.ts +158 -115
- package/dist/index.js +236 -68
- package/dist/index.js.map +1 -1
- package/lib/date.ts +70 -0
- package/package.json +1 -1
|
@@ -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'
|
|
@@ -17,10 +17,24 @@ export interface ChatSessionInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
import type { FileDownloadResult } from '@/components/composites/FileTreeExplorer';
|
|
20
|
+
import { formatDayLabel } from '@/lib/date';
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
export interface SessionHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
23
24
|
title?: string;
|
|
25
|
+
/**
|
|
26
|
+
* What a session is called.
|
|
27
|
+
* - "title": the session's own title (default).
|
|
28
|
+
* - "date": the day it was created, as "21 Aug 2025", in the reader's time
|
|
29
|
+
* zone. For a surface that keeps one session per day, where the day is
|
|
30
|
+
* what tells sessions apart and a first message is not.
|
|
31
|
+
* @default "title"
|
|
32
|
+
*/
|
|
33
|
+
titleFormat?: 'title' | 'date';
|
|
34
|
+
/**
|
|
35
|
+
* IANA time zone for "date" titles. Defaults to the reader's own.
|
|
36
|
+
*/
|
|
37
|
+
timeZone?: string;
|
|
24
38
|
sessions?: ChatSessionInfo[];
|
|
25
39
|
activeSessionId?: string | null;
|
|
26
40
|
onNewSession?: () => void;
|
|
@@ -48,7 +62,7 @@ export interface SessionHeaderProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
48
62
|
* and a dropdown history of past sessions.
|
|
49
63
|
*/
|
|
50
64
|
export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps>(
|
|
51
|
-
({ title, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, onOverview, showOverview = true, className, ...props }, ref) => {
|
|
65
|
+
({ title, titleFormat = 'title', timeZone, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, onOverview, showOverview = true, className, ...props }, ref) => {
|
|
52
66
|
const activeSession = sessions?.find(s => s.id === activeSessionId);
|
|
53
67
|
const downloadRef = React.useRef<HTMLAnchorElement>(null);
|
|
54
68
|
|
|
@@ -66,9 +80,16 @@ export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps
|
|
|
66
80
|
URL.revokeObjectURL(url);
|
|
67
81
|
}, [onDownloadSession]);
|
|
68
82
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
const byDate = titleFormat === 'date';
|
|
84
|
+
const sessionLabel = (session: ChatSessionInfo | undefined): string => {
|
|
85
|
+
if (!byDate) return session?.title || 'Untitled Session';
|
|
86
|
+
// A session not yet in the list is the one being started now: today's.
|
|
87
|
+
return formatDayLabel(session?.created_at ?? new Date(), { timeZone }) || 'Untitled Session';
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const displayTitle = title || (activeSessionId
|
|
91
|
+
? sessionLabel(activeSession)
|
|
92
|
+
: byDate ? formatDayLabel(new Date(), { timeZone }) : 'New Session');
|
|
72
93
|
|
|
73
94
|
return (
|
|
74
95
|
<div
|
|
@@ -121,8 +142,11 @@ export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps
|
|
|
121
142
|
onClick={() => onSelectSession?.(session.id)}
|
|
122
143
|
className="flex flex-col items-start py-2 cursor-pointer"
|
|
123
144
|
>
|
|
124
|
-
<span className="text-sm font-medium truncate w-full">{session
|
|
125
|
-
|
|
145
|
+
<span className="text-sm font-medium truncate w-full">{sessionLabel(session)}</span>
|
|
146
|
+
{/* The date is the title in "date" mode; repeating it underneath says nothing. */}
|
|
147
|
+
{!byDate && (
|
|
148
|
+
<span className="text-xs text-muted-foreground">{new Date(session.created_at).toLocaleString()}</span>
|
|
149
|
+
)}
|
|
126
150
|
</DropdownMenuItem>
|
|
127
151
|
))
|
|
128
152
|
) : (
|
|
@@ -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'
|
|
@@ -220,6 +220,17 @@ export interface ChatPanelProps {
|
|
|
220
220
|
* Title override for the session header
|
|
221
221
|
*/
|
|
222
222
|
sessionTitle?: string;
|
|
223
|
+
/**
|
|
224
|
+
* What sessions are called in the header and the history list: their own
|
|
225
|
+
* title ("title", default) or the day they were created ("date", shown as
|
|
226
|
+
* "21 Aug 2025" in the reader's time zone).
|
|
227
|
+
* @default "title"
|
|
228
|
+
*/
|
|
229
|
+
sessionTitleFormat?: 'title' | 'date';
|
|
230
|
+
/**
|
|
231
|
+
* IANA time zone for "date" session titles. Defaults to the reader's own.
|
|
232
|
+
*/
|
|
233
|
+
sessionTimeZone?: string;
|
|
223
234
|
/**
|
|
224
235
|
* Whether to show action buttons in the session header
|
|
225
236
|
* @default true
|
|
@@ -304,6 +315,8 @@ export const ChatPanel = React.memo<ChatPanelProps>(
|
|
|
304
315
|
enableSpeech = true,
|
|
305
316
|
showHeader = true,
|
|
306
317
|
sessionTitle,
|
|
318
|
+
sessionTitleFormat = 'title',
|
|
319
|
+
sessionTimeZone,
|
|
307
320
|
showSessionActions = true,
|
|
308
321
|
showNewSession = true,
|
|
309
322
|
showDownloadSession = true,
|
|
@@ -468,6 +481,8 @@ export const ChatPanel = React.memo<ChatPanelProps>(
|
|
|
468
481
|
{showHeader && (
|
|
469
482
|
<SessionHeader
|
|
470
483
|
title={sessionTitle}
|
|
484
|
+
titleFormat={sessionTitleFormat}
|
|
485
|
+
timeZone={sessionTimeZone}
|
|
471
486
|
showActions={showSessionActions}
|
|
472
487
|
showNewSession={showNewSession}
|
|
473
488
|
showDownloadSession={showDownloadSession}
|
package/components/index.ts
CHANGED
|
@@ -94,6 +94,9 @@ export type { ExternalToast, ToastT, ToasterProps } from 'sonner';
|
|
|
94
94
|
|
|
95
95
|
// Utilities
|
|
96
96
|
export { cn } from '@/lib/utils';
|
|
97
|
+
// Calendar days in the reader's time zone
|
|
98
|
+
export { formatDayLabel, dayKey, isSameDay, userTimeZone } from '@/lib/date';
|
|
99
|
+
export type { DayOptions } from '@/lib/date';
|
|
97
100
|
export { ButtonSwitcher } from './composites';
|
|
98
101
|
export type { ButtonSwitcherProps, ButtonSwitcherItem } from './composites';
|
|
99
102
|
|
|
@@ -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
|
)
|