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.
@@ -22,12 +22,20 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
22
22
  dragHandleColor = "border",
23
23
  className,
24
24
  padded = true,
25
+ mobileBehavior = "tabs",
26
+ activeSectionId,
27
+ onActiveSectionChange,
25
28
  ...props
26
29
  }) => {
27
- // Transform sections to include headers
30
+ // Transform sections to include headers and resolve labels
28
31
  const transformedSections = sections.map(section => ({
29
32
  ...section,
30
33
  resizable,
34
+ label:
35
+ section.label ||
36
+ section.title ||
37
+ (typeof section.header?.title === "string" ? section.header.title : undefined) ||
38
+ section.header?.tabs?.[0]?.label,
31
39
  content: (
32
40
  <div className="h-full min-h-0 flex flex-col overflow-hidden">
33
41
  {section.header && (
@@ -49,6 +57,9 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
49
57
  dragHandleColor={dragHandleColor}
50
58
  className={className}
51
59
  padded={padded}
60
+ mobileBehavior={mobileBehavior}
61
+ activeSectionId={activeSectionId}
62
+ onActiveSectionChange={onActiveSectionChange}
52
63
  {...props}
53
64
  />
54
65
  )
@@ -4,6 +4,8 @@ import type { AppHeaderProps } from "@/components/composites/AppHeader/interface
4
4
  export interface SectionLayoutSection {
5
5
  id: string;
6
6
  content: React.ReactNode;
7
+ label?: string;
8
+ title?: string;
7
9
  fixedSize?: string;
8
10
  defaultSize?: number;
9
11
  minSize?: number;
@@ -22,4 +24,7 @@ export interface SectionLayoutProps extends React.ComponentPropsWithoutRef<"div"
22
24
  resizable?: boolean;
23
25
  dragHandleColor?: "primary" | "secondary" | "accent" | "border" | "muted";
24
26
  padded?: boolean;
27
+ mobileBehavior?: "tabs" | "stack" | "none";
28
+ activeSectionId?: string;
29
+ onActiveSectionChange?: (sectionId: string) => void;
25
30
  }
@@ -95,10 +95,19 @@ export const Default: Story = {
95
95
  * Simple 2-panel layout with resizable divider.
96
96
  */
97
97
  export const TwoPanels: Story = {
98
+ parameters: {
99
+ docs: {
100
+ description: {
101
+ story:
102
+ 'Responsive 2-panel layout. On desktop viewports (>= 768px), panels sit side-by-side with a resizable divider. On mobile viewports (< 768px), panels adapt into an accessible tabbed segmented view giving full width and height to each panel.',
103
+ },
104
+ },
105
+ },
98
106
  render: () => {
99
107
  const sections: AdjustableLayoutSection[] = [
100
108
  {
101
109
  id: 'main',
110
+ label: 'Main Content',
102
111
  content: (
103
112
  <Card className="h-full">
104
113
  <CardHeader>
@@ -113,6 +122,7 @@ export const TwoPanels: Story = {
113
122
  },
114
123
  {
115
124
  id: 'sidebar',
125
+ label: 'Sidebar',
116
126
  content: (
117
127
  <Card className="h-full">
118
128
  <CardHeader>
@@ -1,9 +1,12 @@
1
1
  import * as React from "react"
2
2
  import { cn } from "@/lib/utils"
3
+ import { Tabs, TabsList, TabsTrigger } from "@/components/primitives/Tabs"
3
4
 
4
5
  export interface AdjustableLayoutSection {
5
6
  id: string
6
7
  content: React.ReactNode
8
+ label?: string
9
+ title?: string
7
10
  fixedSize?: string // CSS size value, e.g. "16rem"
8
11
  defaultSize?: number // percentage (0-100)
9
12
  minSize?: number // minimum percentage
@@ -20,6 +23,31 @@ export interface AdjustableLayoutProps extends React.ComponentPropsWithoutRef<"d
20
23
  onSectionResize?: (sectionId: string, newSize: number) => void
21
24
  dragHandleColor?: "primary" | "secondary" | "accent" | "border" | "muted"
22
25
  padded?: boolean
26
+ /**
27
+ * Layout behavior on mobile screens (< 768px).
28
+ * - 'tabs': Renders a mobile tab bar/segmented control to toggle between panels (default)
29
+ * - 'stack': Stacks panels vertically (flex-col)
30
+ * - 'none': Keeps standard orientation without mobile adaptation
31
+ * @default 'tabs'
32
+ */
33
+ mobileBehavior?: "tabs" | "stack" | "none"
34
+ /**
35
+ * Active section ID in mobile tabs mode (for controlled usage).
36
+ */
37
+ activeSectionId?: string
38
+ /**
39
+ * Callback when active section changes in mobile tabs mode.
40
+ */
41
+ onActiveSectionChange?: (sectionId: string) => void
42
+ }
43
+
44
+ function formatSectionTitle(id: string): string {
45
+ if (!id) return "Panel"
46
+ return id
47
+ .split(/[-_]/)
48
+ .filter(Boolean)
49
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
50
+ .join(" ")
23
51
  }
24
52
 
25
53
  /**
@@ -38,6 +66,9 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
38
66
  dragHandleColor = "border",
39
67
  className,
40
68
  padded = false,
69
+ mobileBehavior = "tabs",
70
+ activeSectionId,
71
+ onActiveSectionChange,
41
72
  ...props
42
73
  }) => {
43
74
  // Color mapping for drag handles
@@ -46,21 +77,57 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
46
77
  secondary: "bg-secondary hover:bg-secondary/80",
47
78
  accent: "bg-accent hover:bg-accent/80",
48
79
  border: "bg-border hover:bg-border/80",
49
- muted: "bg-muted hover:bg-muted/80"
80
+ muted: "bg-muted hover:bg-muted/80",
50
81
  }
51
82
 
52
- const hoverColorMap = {
53
- primary: "group-hover:bg-primary/30",
54
- secondary: "group-hover:bg-secondary/30",
55
- accent: "group-hover:bg-accent/30",
56
- border: "group-hover:bg-border/30",
57
- muted: "group-hover:bg-muted/30"
58
- }
59
83
  const containerRef = React.useRef<HTMLDivElement>(null)
60
84
 
85
+ // Responsive mobile detection
86
+ const [isMobile, setIsMobile] = React.useState(false)
87
+
88
+ React.useEffect(() => {
89
+ const checkMobile = () => {
90
+ const winMobile = typeof window !== "undefined" && window.innerWidth < 768
91
+ const containerWidth = containerRef.current?.getBoundingClientRect().width ?? 0
92
+ const containerMobile = containerWidth > 0 && containerWidth < 768
93
+ setIsMobile(winMobile || containerMobile)
94
+ }
95
+
96
+ checkMobile()
97
+ window.addEventListener("resize", checkMobile)
98
+ return () => window.removeEventListener("resize", checkMobile)
99
+ }, [])
100
+
101
+ // Active section state for mobile tabs mode
102
+ const [internalActiveId, setInternalActiveId] = React.useState<string>(
103
+ sections[0]?.id ?? ""
104
+ )
105
+
106
+ React.useEffect(() => {
107
+ if (sections.length > 0 && !sections.some((s) => s.id === internalActiveId)) {
108
+ setInternalActiveId(sections[0].id)
109
+ }
110
+ }, [sections, internalActiveId])
111
+
112
+ const currentActiveId =
113
+ (activeSectionId && sections.some((s) => s.id === activeSectionId)
114
+ ? activeSectionId
115
+ : undefined) ??
116
+ (sections.some((s) => s.id === internalActiveId)
117
+ ? internalActiveId
118
+ : (sections[0]?.id ?? ""))
119
+
120
+ const handleTabChange = React.useCallback(
121
+ (newId: string) => {
122
+ setInternalActiveId(newId)
123
+ onActiveSectionChange?.(newId)
124
+ },
125
+ [onActiveSectionChange]
126
+ )
127
+
61
128
  // Compute default sizes (server-safe — no localStorage access)
62
129
  const defaultSizes = React.useMemo(() => {
63
- const raw = sections.map((section) => section.defaultSize ?? (100 / sections.length))
130
+ const raw = sections.map((section) => section.defaultSize ?? 100 / sections.length)
64
131
  const total = raw.reduce((sum, size) => sum + size, 0)
65
132
  return raw.map((size) => (size / total) * 100)
66
133
  }, [sections])
@@ -107,99 +174,136 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
107
174
  }
108
175
 
109
176
  updateContainerSize()
110
- window.addEventListener('resize', updateContainerSize)
111
- return () => window.removeEventListener('resize', updateContainerSize)
177
+ window.addEventListener("resize", updateContainerSize)
178
+ return () => window.removeEventListener("resize", updateContainerSize)
112
179
  }, [orientation])
113
180
 
114
181
  // Save to localStorage when sizes change (only in browser)
115
182
  React.useEffect(() => {
116
- if (storageKey && typeof window !== 'undefined') {
183
+ if (storageKey && typeof window !== "undefined") {
117
184
  localStorage.setItem(storageKey, JSON.stringify(sizes))
118
185
  }
119
186
  }, [sizes, storageKey])
120
187
 
121
- const handleMouseDown = React.useCallback((index: number, e: React.MouseEvent) => {
122
- e.preventDefault()
123
- setDraggingIndex(index)
124
- setStartX(orientation === "horizontal" ? e.clientX : e.clientY)
125
- setStartSizes([...sizes])
126
- }, [sizes, orientation])
127
-
128
- const handleMouseMove = React.useCallback((e: MouseEvent) => {
129
- if (draggingIndex === null || containerSize === 0) return
130
-
131
- const currentX = orientation === "horizontal" ? e.clientX : e.clientY
132
- const deltaX = currentX - startX
133
-
134
- const deltaPercent = (deltaX / containerSize) * 100
135
-
136
- const p1 = sections[draggingIndex]
137
- const p2 = sections[draggingIndex + 1]
138
-
139
- const p1Start = startSizes[draggingIndex]
140
- const p2Start = startSizes[draggingIndex + 1]
141
-
142
- const p1Min = p1.minSize ?? 10
143
- const p1Max = p1.maxSize ?? 80
144
- const p2Min = p2.minSize ?? 10
145
- const p2Max = p2.maxSize ?? 80
146
-
147
- // Calculate how much we can actually change panel 1
148
- // If deltaPercent > 0, we are growing p1 and shrinking p2
149
- const maxPositiveDelta = Math.max(0, Math.min(
150
- p1Max - p1Start, // Space p1 has to grow
151
- p2Start - p2Min // Space p2 has to shrink
152
- ))
153
-
154
- // If deltaPercent < 0, we are shrinking p1 and growing p2
155
- const maxNegativeDelta = Math.min(0, Math.max(
156
- p1Min - p1Start, // Space p1 has to shrink (negative)
157
- p2Start - p2Max // Space p2 has to grow (negative)
158
- ))
159
-
160
- // Clamp the delta
161
- let clampedDelta = deltaPercent
162
- if (clampedDelta > 0) {
163
- clampedDelta = Math.min(clampedDelta, maxPositiveDelta)
164
- } else {
165
- clampedDelta = Math.max(clampedDelta, maxNegativeDelta)
166
- }
188
+ const handleMouseDown = React.useCallback(
189
+ (index: number, e: React.MouseEvent) => {
190
+ e.preventDefault()
191
+ setDraggingIndex(index)
192
+ setStartX(orientation === "horizontal" ? e.clientX : e.clientY)
193
+ setStartSizes([...sizes])
194
+ },
195
+ [sizes, orientation]
196
+ )
197
+
198
+ const handleTouchStart = React.useCallback(
199
+ (index: number, e: React.TouchEvent) => {
200
+ if (e.touches.length !== 1) return
201
+ setDraggingIndex(index)
202
+ setStartX(orientation === "horizontal" ? e.touches[0].clientX : e.touches[0].clientY)
203
+ setStartSizes([...sizes])
204
+ },
205
+ [sizes, orientation]
206
+ )
167
207
 
168
- const newSizes = [...startSizes]
169
- newSizes[draggingIndex] = p1Start + clampedDelta
170
- newSizes[draggingIndex + 1] = p2Start - clampedDelta
208
+ const applyDelta = React.useCallback(
209
+ (deltaPixels: number) => {
210
+ if (draggingIndex === null || containerSize === 0) return
211
+
212
+ const deltaPercent = (deltaPixels / containerSize) * 100
213
+
214
+ const p1 = sections[draggingIndex]
215
+ const p2 = sections[draggingIndex + 1]
216
+
217
+ const p1Start = startSizes[draggingIndex]
218
+ const p2Start = startSizes[draggingIndex + 1]
219
+
220
+ const p1Min = p1.minSize ?? 10
221
+ const p1Max = p1.maxSize ?? 80
222
+ const p2Min = p2.minSize ?? 10
223
+ const p2Max = p2.maxSize ?? 80
224
+
225
+ // Calculate how much we can actually change panel 1
226
+ const maxPositiveDelta = Math.max(
227
+ 0,
228
+ Math.min(
229
+ p1Max - p1Start, // Space p1 has to grow
230
+ p2Start - p2Min // Space p2 has to shrink
231
+ )
232
+ )
233
+
234
+ const maxNegativeDelta = Math.min(
235
+ 0,
236
+ Math.max(
237
+ p1Min - p1Start, // Space p1 has to shrink (negative)
238
+ p2Start - p2Max // Space p2 has to grow (negative)
239
+ )
240
+ )
241
+
242
+ // Clamp the delta
243
+ let clampedDelta = deltaPercent
244
+ if (clampedDelta > 0) {
245
+ clampedDelta = Math.min(clampedDelta, maxPositiveDelta)
246
+ } else {
247
+ clampedDelta = Math.max(clampedDelta, maxNegativeDelta)
248
+ }
171
249
 
172
- // Normalize to 100% just in case of floating point drift
173
- const total = newSizes.reduce((sum, size) => sum + size, 0)
174
- const normalizedSizes = newSizes.map(size => (size / total) * 100)
250
+ const newSizes = [...startSizes]
251
+ newSizes[draggingIndex] = p1Start + clampedDelta
252
+ newSizes[draggingIndex + 1] = p2Start - clampedDelta
175
253
 
176
- setSizes(normalizedSizes)
254
+ // Normalize to 100% just in case of floating point drift
255
+ const total = newSizes.reduce((sum, size) => sum + size, 0)
256
+ const normalizedSizes = newSizes.map((size) => (size / total) * 100)
177
257
 
178
- // Notify parent
179
- if (onSectionResize) {
180
- onSectionResize(sections[draggingIndex].id, normalizedSizes[draggingIndex])
181
- onSectionResize(sections[draggingIndex + 1].id, normalizedSizes[draggingIndex + 1])
182
- }
183
- }, [draggingIndex, startX, startSizes, sections, orientation, containerSize, onSectionResize])
258
+ setSizes(normalizedSizes)
259
+
260
+ // Notify parent
261
+ if (onSectionResize) {
262
+ onSectionResize(sections[draggingIndex].id, normalizedSizes[draggingIndex])
263
+ onSectionResize(sections[draggingIndex + 1].id, normalizedSizes[draggingIndex + 1])
264
+ }
265
+ },
266
+ [draggingIndex, containerSize, sections, startSizes, onSectionResize]
267
+ )
184
268
 
185
- const handleMouseUp = React.useCallback(() => {
269
+ const handleMouseMove = React.useCallback(
270
+ (e: MouseEvent) => {
271
+ const currentPos = orientation === "horizontal" ? e.clientX : e.clientY
272
+ applyDelta(currentPos - startX)
273
+ },
274
+ [orientation, startX, applyDelta]
275
+ )
276
+
277
+ const handleTouchMove = React.useCallback(
278
+ (e: TouchEvent) => {
279
+ if (e.touches.length !== 1) return
280
+ const currentPos = orientation === "horizontal" ? e.touches[0].clientX : e.touches[0].clientY
281
+ applyDelta(currentPos - startX)
282
+ },
283
+ [orientation, startX, applyDelta]
284
+ )
285
+
286
+ const handleEnd = React.useCallback(() => {
186
287
  setDraggingIndex(null)
187
288
  }, [])
188
289
 
189
290
  React.useEffect(() => {
190
291
  if (draggingIndex !== null) {
191
292
  document.addEventListener("mousemove", handleMouseMove)
192
- document.addEventListener("mouseup", handleMouseUp)
293
+ document.addEventListener("mouseup", handleEnd)
294
+ document.addEventListener("touchmove", handleTouchMove, { passive: false })
295
+ document.addEventListener("touchend", handleEnd)
193
296
  return () => {
194
297
  document.removeEventListener("mousemove", handleMouseMove)
195
- document.removeEventListener("mouseup", handleMouseUp)
298
+ document.removeEventListener("mouseup", handleEnd)
299
+ document.removeEventListener("touchmove", handleTouchMove)
300
+ document.removeEventListener("touchend", handleEnd)
196
301
  }
197
302
  }
198
- }, [draggingIndex, handleMouseMove, handleMouseUp])
303
+ }, [draggingIndex, handleMouseMove, handleTouchMove, handleEnd])
199
304
 
200
305
  const renderPanel = (section: AdjustableLayoutSection, size: number, index: number) => {
201
306
  const nextSection = sections[index + 1]
202
- // Drag handles should be between panels, so only show for panels that aren't the last one
203
307
  const isResizable =
204
308
  section.resizable !== false &&
205
309
  sections.length > 1 &&
@@ -230,25 +334,23 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
230
334
  {section.content}
231
335
  </div>
232
336
 
233
- {/* Show drag handle or spacer after this panel if it's not the last one */}
234
337
  {index < sections.length - 1 && (
235
338
  isResizable ? (
236
339
  <div
237
340
  className={cn(
238
- "flex-shrink-0 flex items-center justify-center relative group",
341
+ "flex-shrink-0 flex items-center justify-center relative group touch-none select-none",
239
342
  orientation === "vertical"
240
343
  ? "cursor-row-resize h-2 w-full"
241
344
  : "cursor-col-resize w-2 h-full"
242
345
  )}
243
346
  onMouseDown={(e) => handleMouseDown(index, e)}
347
+ onTouchStart={(e) => handleTouchStart(index, e)}
244
348
  >
245
349
  {/* Visible pill */}
246
350
  <div
247
351
  className={cn(
248
352
  `${colorMap[dragHandleColor]} transition-colors duration-200 rounded-full`,
249
- orientation === "vertical"
250
- ? "h-1 w-8"
251
- : "w-1 h-8"
353
+ orientation === "vertical" ? "h-1 w-8" : "w-1 h-8"
252
354
  )}
253
355
  />
254
356
  {/* Invisible large hit area */}
@@ -274,6 +376,97 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
274
376
  )
275
377
  }
276
378
 
379
+ const isMobileMode = isMobile && mobileBehavior !== "none"
380
+
381
+ if (isMobileMode && mobileBehavior === "tabs") {
382
+ return (
383
+ <div
384
+ ref={containerRef}
385
+ className={cn(
386
+ "flex flex-col overflow-hidden h-full min-w-0 w-full",
387
+ padded && "p-2 sm:p-4 gap-2",
388
+ className
389
+ )}
390
+ {...props}
391
+ >
392
+ {sections.length > 1 && (
393
+ <div className="flex-none pb-1">
394
+ <Tabs
395
+ value={currentActiveId}
396
+ onValueChange={handleTabChange}
397
+ className="w-full"
398
+ >
399
+ <TabsList
400
+ className="grid w-full h-9 bg-muted/60 p-1 rounded-lg"
401
+ style={{
402
+ gridTemplateColumns: `repeat(${sections.length}, minmax(0, 1fr))`,
403
+ }}
404
+ >
405
+ {sections.map((section) => (
406
+ <TabsTrigger
407
+ key={section.id}
408
+ value={section.id}
409
+ className="text-xs sm:text-sm font-medium truncate px-2 py-1 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md"
410
+ >
411
+ {section.label || section.title || formatSectionTitle(section.id)}
412
+ </TabsTrigger>
413
+ ))}
414
+ </TabsList>
415
+ </Tabs>
416
+ </div>
417
+ )}
418
+
419
+ <div className="flex-1 min-h-0 relative overflow-hidden">
420
+ {sections.map((section) => {
421
+ const isActive = section.id === currentActiveId
422
+ return (
423
+ <div
424
+ key={section.id}
425
+ className={cn(
426
+ "h-full w-full min-h-0 overflow-hidden",
427
+ !isActive && "hidden",
428
+ (section.variant ?? "default") === "default" &&
429
+ "bg-card border border-border rounded-xl",
430
+ section.className
431
+ )}
432
+ >
433
+ {section.content}
434
+ </div>
435
+ )
436
+ })}
437
+ </div>
438
+ </div>
439
+ )
440
+ }
441
+
442
+ if (isMobileMode && mobileBehavior === "stack") {
443
+ return (
444
+ <div
445
+ ref={containerRef}
446
+ className={cn(
447
+ "flex flex-col overflow-y-auto h-full gap-2 min-w-0 w-full",
448
+ padded && "p-2 sm:p-4",
449
+ className
450
+ )}
451
+ {...props}
452
+ >
453
+ {sections.map((section) => (
454
+ <div
455
+ key={section.id}
456
+ className={cn(
457
+ "min-h-[200px] overflow-hidden w-full flex-shrink-0",
458
+ (section.variant ?? "default") === "default" &&
459
+ "bg-card border border-border rounded-xl",
460
+ section.className
461
+ )}
462
+ >
463
+ {section.content}
464
+ </div>
465
+ ))}
466
+ </div>
467
+ )
468
+ }
469
+
277
470
  return (
278
471
  <div
279
472
  ref={containerRef}
@@ -45,6 +45,11 @@ export interface DocumentTabBarProps {
45
45
  * Additional CSS classes
46
46
  */
47
47
  className?: string
48
+
49
+ /**
50
+ * Callback to open/toggle the file explorer (e.g. mobile drawer)
51
+ */
52
+ onToggleExplorer?: () => void
48
53
  }
49
54
 
50
55
  /**
@@ -63,8 +68,8 @@ export interface DocumentTabBarProps {
63
68
  * - Dirty state indicator
64
69
  */
65
70
  export const DocumentTabBar = React.memo<DocumentTabBarProps>(
66
- ({ tabs, activeTabId, onTabSelect, onTabClose, className }) => {
67
- if (tabs.length === 0) {
71
+ ({ tabs, activeTabId, onTabSelect, onTabClose, onToggleExplorer, className }) => {
72
+ if (tabs.length === 0 && !onToggleExplorer) {
68
73
  return null
69
74
  }
70
75
 
@@ -76,12 +81,25 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
76
81
  )}
77
82
  data-slot="document-tab-bar"
78
83
  >
79
- <ScrollArea className="flex-1">
80
- <Tabs
81
- value={activeTabId || tabs[0]?.id}
82
- onValueChange={onTabSelect}
83
- className="h-auto flex-1"
84
+ {onToggleExplorer && (
85
+ <Button
86
+ variant="ghost"
87
+ size="icon-sm"
88
+ onClick={onToggleExplorer}
89
+ className="h-10 w-10 shrink-0 rounded-none border-r border-border/50 text-muted-foreground hover:text-foreground md:hidden"
90
+ aria-label="Toggle file explorer"
91
+ title="Toggle file explorer"
84
92
  >
93
+ <Icon name="folder" size="xs" />
94
+ </Button>
95
+ )}
96
+ {tabs.length > 0 && (
97
+ <ScrollArea className="flex-1">
98
+ <Tabs
99
+ value={activeTabId || tabs[0]?.id}
100
+ onValueChange={onTabSelect}
101
+ className="h-auto flex-1"
102
+ >
85
103
  <TabsList className="h-10 rounded-none border-none bg-transparent p-0 w-full justify-start gap-0">
86
104
  {tabs.map((tab) => (
87
105
  <div
@@ -93,13 +111,13 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
93
111
  className={cn(
94
112
  'data-[state=inactive]:bg-muted/20 data-[state=inactive]:text-muted-foreground',
95
113
  'rounded-none border-b-2 border-transparent data-[state=active]:border-primary',
96
- 'px-3 py-2 text-sm font-medium whitespace-nowrap',
97
- 'flex items-center gap-2 h-10',
114
+ 'px-2.5 sm:px-3 py-2 text-xs sm:text-sm font-medium whitespace-nowrap',
115
+ 'flex items-center gap-1.5 sm:gap-2 h-10',
98
116
  'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0',
99
117
  'transition-colors'
100
118
  )}
101
119
  >
102
- <div className="flex items-center gap-2">
120
+ <div className="flex items-center gap-1.5 sm:gap-2">
103
121
  {/* Dirty indicator */}
104
122
  {tab.isDirty && (
105
123
  <Icon
@@ -109,7 +127,7 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
109
127
  />
110
128
  )}
111
129
  {/* Tab name */}
112
- <span className="truncate max-w-[200px]">{tab.name}</span>
130
+ <span className="truncate max-w-[120px] sm:max-w-[200px]">{tab.name}</span>
113
131
  </div>
114
132
  </TabsTrigger>
115
133
 
@@ -136,6 +154,7 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
136
154
  </TabsList>
137
155
  </Tabs>
138
156
  </ScrollArea>
157
+ )}
139
158
  </div>
140
159
  )
141
160
  }
@@ -0,0 +1,62 @@
1
+ import * as React from "react"
2
+ import {
3
+ Drawer,
4
+ DrawerClose,
5
+ DrawerContent,
6
+ DrawerHeader,
7
+ DrawerTitle,
8
+ } from "@/components/primitives/Drawer"
9
+ import { Icon } from "@/components/primitives/Icon"
10
+ import { Button } from "@/components/primitives/Button"
11
+ import { FileTreeExplorer, type FileTreeExplorerProps } from "./FileTreeExplorer"
12
+ import { cn } from "@/lib/utils"
13
+
14
+ export interface FileTreeDrawerProps extends FileTreeExplorerProps {
15
+ open: boolean
16
+ onOpenChange: (open: boolean) => void
17
+ title?: string
18
+ }
19
+
20
+ export const FileTreeDrawer = React.memo<FileTreeDrawerProps>(
21
+ ({
22
+ open,
23
+ onOpenChange,
24
+ title = "Files",
25
+ tree,
26
+ selectedPath,
27
+ onSelect,
28
+ onDownload,
29
+ className,
30
+ ...props
31
+ }) => {
32
+ return (
33
+ <Drawer direction="left" open={open} onOpenChange={onOpenChange}>
34
+ <DrawerContent className={cn("w-[85vw] max-w-[320px] h-full flex flex-col p-0 focus:outline-none", className)}>
35
+ <DrawerHeader className="p-3 border-b flex items-center justify-between shrink-0">
36
+ <DrawerTitle className="text-sm font-semibold flex items-center gap-2">
37
+ <Icon name="folder" size="xs" />
38
+ {title}
39
+ </DrawerTitle>
40
+ <DrawerClose asChild>
41
+ <Button variant="ghost" size="icon-sm" className="h-7 w-7">
42
+ <Icon name="x" size="xs" />
43
+ </Button>
44
+ </DrawerClose>
45
+ </DrawerHeader>
46
+ <div className="flex-1 min-h-0 overflow-y-auto">
47
+ <FileTreeExplorer
48
+ className="h-full w-full rounded-none border-0"
49
+ tree={tree}
50
+ selectedPath={selectedPath}
51
+ onSelect={onSelect}
52
+ onDownload={onDownload}
53
+ {...props}
54
+ />
55
+ </div>
56
+ </DrawerContent>
57
+ </Drawer>
58
+ )
59
+ }
60
+ )
61
+
62
+ FileTreeDrawer.displayName = "FileTreeDrawer"