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
|
@@ -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 ??
|
|
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(
|
|
111
|
-
return () => window.removeEventListener(
|
|
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 !==
|
|
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(
|
|
122
|
-
e.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
250
|
+
const newSizes = [...startSizes]
|
|
251
|
+
newSizes[draggingIndex] = p1Start + clampedDelta
|
|
252
|
+
newSizes[draggingIndex + 1] = p2Start - clampedDelta
|
|
175
253
|
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
onSectionResize
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
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",
|
|
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",
|
|
298
|
+
document.removeEventListener("mouseup", handleEnd)
|
|
299
|
+
document.removeEventListener("touchmove", handleTouchMove)
|
|
300
|
+
document.removeEventListener("touchend", handleEnd)
|
|
196
301
|
}
|
|
197
302
|
}
|
|
198
|
-
}, [draggingIndex, handleMouseMove,
|
|
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
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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"
|