ai-design-system 0.1.112 → 0.1.114
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 +14 -1
- package/components/blocks/SectionLayout/interfaces.ts +6 -0
- package/components/composites/AdjustableLayout/AdjustableLayout.stories.tsx +10 -0
- package/components/composites/AdjustableLayout/AdjustableLayout.tsx +281 -81
- package/components/composites/DocumentTabBar/DocumentTabBar.tsx +29 -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/SessionHeader/SessionHeader.tsx +21 -1
- package/components/composites/WorkspaceEmptyState/WorkspaceEmptyState.tsx +17 -1
- package/components/composites/index.ts +4 -0
- package/components/features/ChatPanel/ChatPanel.tsx +13 -0
- package/components/features/TextEditor/TextEditor.stories.tsx +8 -0
- package/components/features/TextEditor/TextEditor.tsx +112 -13
- package/dist/index.cjs +1568 -1199
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +45 -50
- package/dist/index.d.ts +72 -1
- package/dist/index.js +1568 -1199
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -22,12 +22,21 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
|
|
|
22
22
|
dragHandleColor = "border",
|
|
23
23
|
className,
|
|
24
24
|
padded = true,
|
|
25
|
+
mobileBehavior = "tabs",
|
|
26
|
+
hideMobileTabs = false,
|
|
27
|
+
activeSectionId,
|
|
28
|
+
onActiveSectionChange,
|
|
25
29
|
...props
|
|
26
30
|
}) => {
|
|
27
|
-
// Transform sections to include headers
|
|
31
|
+
// Transform sections to include headers and resolve labels
|
|
28
32
|
const transformedSections = sections.map(section => ({
|
|
29
33
|
...section,
|
|
30
34
|
resizable,
|
|
35
|
+
label:
|
|
36
|
+
section.label ||
|
|
37
|
+
section.title ||
|
|
38
|
+
(typeof section.header?.title === "string" ? section.header.title : undefined) ||
|
|
39
|
+
section.header?.tabs?.[0]?.label,
|
|
31
40
|
content: (
|
|
32
41
|
<div className="h-full min-h-0 flex flex-col overflow-hidden">
|
|
33
42
|
{section.header && (
|
|
@@ -49,6 +58,10 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
|
|
|
49
58
|
dragHandleColor={dragHandleColor}
|
|
50
59
|
className={className}
|
|
51
60
|
padded={padded}
|
|
61
|
+
mobileBehavior={mobileBehavior}
|
|
62
|
+
hideMobileTabs={hideMobileTabs}
|
|
63
|
+
activeSectionId={activeSectionId}
|
|
64
|
+
onActiveSectionChange={onActiveSectionChange}
|
|
52
65
|
{...props}
|
|
53
66
|
/>
|
|
54
67
|
)
|
|
@@ -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,8 @@ 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
|
+
hideMobileTabs?: boolean;
|
|
29
|
+
activeSectionId?: string;
|
|
30
|
+
onActiveSectionChange?: (sectionId: string) => void;
|
|
25
31
|
}
|
|
@@ -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,37 @@ 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
|
+
* Whether to hide the mobile tab bar when mobileBehavior is "tabs".
|
|
36
|
+
* Used when switching between panels is driven by internal controls (e.g. Overview / Show Chat).
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
hideMobileTabs?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* Active section ID in mobile tabs mode (for controlled usage).
|
|
42
|
+
*/
|
|
43
|
+
activeSectionId?: string
|
|
44
|
+
/**
|
|
45
|
+
* Callback when active section changes in mobile tabs mode.
|
|
46
|
+
*/
|
|
47
|
+
onActiveSectionChange?: (sectionId: string) => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatSectionTitle(id: string): string {
|
|
51
|
+
if (!id) return "Panel"
|
|
52
|
+
return id
|
|
53
|
+
.split(/[-_]/)
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
56
|
+
.join(" ")
|
|
23
57
|
}
|
|
24
58
|
|
|
25
59
|
/**
|
|
@@ -38,6 +72,10 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
38
72
|
dragHandleColor = "border",
|
|
39
73
|
className,
|
|
40
74
|
padded = false,
|
|
75
|
+
mobileBehavior = "tabs",
|
|
76
|
+
hideMobileTabs = false,
|
|
77
|
+
activeSectionId,
|
|
78
|
+
onActiveSectionChange,
|
|
41
79
|
...props
|
|
42
80
|
}) => {
|
|
43
81
|
// Color mapping for drag handles
|
|
@@ -46,21 +84,57 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
46
84
|
secondary: "bg-secondary hover:bg-secondary/80",
|
|
47
85
|
accent: "bg-accent hover:bg-accent/80",
|
|
48
86
|
border: "bg-border hover:bg-border/80",
|
|
49
|
-
muted: "bg-muted hover:bg-muted/80"
|
|
87
|
+
muted: "bg-muted hover:bg-muted/80",
|
|
50
88
|
}
|
|
51
89
|
|
|
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
90
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
60
91
|
|
|
92
|
+
// Responsive mobile detection
|
|
93
|
+
const [isMobile, setIsMobile] = React.useState(false)
|
|
94
|
+
|
|
95
|
+
React.useEffect(() => {
|
|
96
|
+
const checkMobile = () => {
|
|
97
|
+
const winMobile = typeof window !== "undefined" && window.innerWidth < 768
|
|
98
|
+
const containerWidth = containerRef.current?.getBoundingClientRect().width ?? 0
|
|
99
|
+
const containerMobile = containerWidth > 0 && containerWidth < 768
|
|
100
|
+
setIsMobile(winMobile || containerMobile)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
checkMobile()
|
|
104
|
+
window.addEventListener("resize", checkMobile)
|
|
105
|
+
return () => window.removeEventListener("resize", checkMobile)
|
|
106
|
+
}, [])
|
|
107
|
+
|
|
108
|
+
// Active section state for mobile tabs mode
|
|
109
|
+
const [internalActiveId, setInternalActiveId] = React.useState<string>(
|
|
110
|
+
sections[0]?.id ?? ""
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
if (sections.length > 0 && !sections.some((s) => s.id === internalActiveId)) {
|
|
115
|
+
setInternalActiveId(sections[0].id)
|
|
116
|
+
}
|
|
117
|
+
}, [sections, internalActiveId])
|
|
118
|
+
|
|
119
|
+
const currentActiveId =
|
|
120
|
+
(activeSectionId && sections.some((s) => s.id === activeSectionId)
|
|
121
|
+
? activeSectionId
|
|
122
|
+
: undefined) ??
|
|
123
|
+
(sections.some((s) => s.id === internalActiveId)
|
|
124
|
+
? internalActiveId
|
|
125
|
+
: (sections[0]?.id ?? ""))
|
|
126
|
+
|
|
127
|
+
const handleTabChange = React.useCallback(
|
|
128
|
+
(newId: string) => {
|
|
129
|
+
setInternalActiveId(newId)
|
|
130
|
+
onActiveSectionChange?.(newId)
|
|
131
|
+
},
|
|
132
|
+
[onActiveSectionChange]
|
|
133
|
+
)
|
|
134
|
+
|
|
61
135
|
// Compute default sizes (server-safe — no localStorage access)
|
|
62
136
|
const defaultSizes = React.useMemo(() => {
|
|
63
|
-
const raw = sections.map((section) => section.defaultSize ??
|
|
137
|
+
const raw = sections.map((section) => section.defaultSize ?? 100 / sections.length)
|
|
64
138
|
const total = raw.reduce((sum, size) => sum + size, 0)
|
|
65
139
|
return raw.map((size) => (size / total) * 100)
|
|
66
140
|
}, [sections])
|
|
@@ -107,99 +181,136 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
107
181
|
}
|
|
108
182
|
|
|
109
183
|
updateContainerSize()
|
|
110
|
-
window.addEventListener(
|
|
111
|
-
return () => window.removeEventListener(
|
|
184
|
+
window.addEventListener("resize", updateContainerSize)
|
|
185
|
+
return () => window.removeEventListener("resize", updateContainerSize)
|
|
112
186
|
}, [orientation])
|
|
113
187
|
|
|
114
188
|
// Save to localStorage when sizes change (only in browser)
|
|
115
189
|
React.useEffect(() => {
|
|
116
|
-
if (storageKey && typeof window !==
|
|
190
|
+
if (storageKey && typeof window !== "undefined") {
|
|
117
191
|
localStorage.setItem(storageKey, JSON.stringify(sizes))
|
|
118
192
|
}
|
|
119
193
|
}, [sizes, storageKey])
|
|
120
194
|
|
|
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
|
-
}
|
|
195
|
+
const handleMouseDown = React.useCallback(
|
|
196
|
+
(index: number, e: React.MouseEvent) => {
|
|
197
|
+
e.preventDefault()
|
|
198
|
+
setDraggingIndex(index)
|
|
199
|
+
setStartX(orientation === "horizontal" ? e.clientX : e.clientY)
|
|
200
|
+
setStartSizes([...sizes])
|
|
201
|
+
},
|
|
202
|
+
[sizes, orientation]
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const handleTouchStart = React.useCallback(
|
|
206
|
+
(index: number, e: React.TouchEvent) => {
|
|
207
|
+
if (e.touches.length !== 1) return
|
|
208
|
+
setDraggingIndex(index)
|
|
209
|
+
setStartX(orientation === "horizontal" ? e.touches[0].clientX : e.touches[0].clientY)
|
|
210
|
+
setStartSizes([...sizes])
|
|
211
|
+
},
|
|
212
|
+
[sizes, orientation]
|
|
213
|
+
)
|
|
167
214
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
215
|
+
const applyDelta = React.useCallback(
|
|
216
|
+
(deltaPixels: number) => {
|
|
217
|
+
if (draggingIndex === null || containerSize === 0) return
|
|
218
|
+
|
|
219
|
+
const deltaPercent = (deltaPixels / containerSize) * 100
|
|
220
|
+
|
|
221
|
+
const p1 = sections[draggingIndex]
|
|
222
|
+
const p2 = sections[draggingIndex + 1]
|
|
223
|
+
|
|
224
|
+
const p1Start = startSizes[draggingIndex]
|
|
225
|
+
const p2Start = startSizes[draggingIndex + 1]
|
|
226
|
+
|
|
227
|
+
const p1Min = p1.minSize ?? 10
|
|
228
|
+
const p1Max = p1.maxSize ?? 80
|
|
229
|
+
const p2Min = p2.minSize ?? 10
|
|
230
|
+
const p2Max = p2.maxSize ?? 80
|
|
231
|
+
|
|
232
|
+
// Calculate how much we can actually change panel 1
|
|
233
|
+
const maxPositiveDelta = Math.max(
|
|
234
|
+
0,
|
|
235
|
+
Math.min(
|
|
236
|
+
p1Max - p1Start, // Space p1 has to grow
|
|
237
|
+
p2Start - p2Min // Space p2 has to shrink
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const maxNegativeDelta = Math.min(
|
|
242
|
+
0,
|
|
243
|
+
Math.max(
|
|
244
|
+
p1Min - p1Start, // Space p1 has to shrink (negative)
|
|
245
|
+
p2Start - p2Max // Space p2 has to grow (negative)
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
// Clamp the delta
|
|
250
|
+
let clampedDelta = deltaPercent
|
|
251
|
+
if (clampedDelta > 0) {
|
|
252
|
+
clampedDelta = Math.min(clampedDelta, maxPositiveDelta)
|
|
253
|
+
} else {
|
|
254
|
+
clampedDelta = Math.max(clampedDelta, maxNegativeDelta)
|
|
255
|
+
}
|
|
171
256
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
257
|
+
const newSizes = [...startSizes]
|
|
258
|
+
newSizes[draggingIndex] = p1Start + clampedDelta
|
|
259
|
+
newSizes[draggingIndex + 1] = p2Start - clampedDelta
|
|
175
260
|
|
|
176
|
-
|
|
261
|
+
// Normalize to 100% just in case of floating point drift
|
|
262
|
+
const total = newSizes.reduce((sum, size) => sum + size, 0)
|
|
263
|
+
const normalizedSizes = newSizes.map((size) => (size / total) * 100)
|
|
177
264
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
onSectionResize
|
|
182
|
-
|
|
183
|
-
|
|
265
|
+
setSizes(normalizedSizes)
|
|
266
|
+
|
|
267
|
+
// Notify parent
|
|
268
|
+
if (onSectionResize) {
|
|
269
|
+
onSectionResize(sections[draggingIndex].id, normalizedSizes[draggingIndex])
|
|
270
|
+
onSectionResize(sections[draggingIndex + 1].id, normalizedSizes[draggingIndex + 1])
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
[draggingIndex, containerSize, sections, startSizes, onSectionResize]
|
|
274
|
+
)
|
|
184
275
|
|
|
185
|
-
const
|
|
276
|
+
const handleMouseMove = React.useCallback(
|
|
277
|
+
(e: MouseEvent) => {
|
|
278
|
+
const currentPos = orientation === "horizontal" ? e.clientX : e.clientY
|
|
279
|
+
applyDelta(currentPos - startX)
|
|
280
|
+
},
|
|
281
|
+
[orientation, startX, applyDelta]
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const handleTouchMove = React.useCallback(
|
|
285
|
+
(e: TouchEvent) => {
|
|
286
|
+
if (e.touches.length !== 1) return
|
|
287
|
+
const currentPos = orientation === "horizontal" ? e.touches[0].clientX : e.touches[0].clientY
|
|
288
|
+
applyDelta(currentPos - startX)
|
|
289
|
+
},
|
|
290
|
+
[orientation, startX, applyDelta]
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
const handleEnd = React.useCallback(() => {
|
|
186
294
|
setDraggingIndex(null)
|
|
187
295
|
}, [])
|
|
188
296
|
|
|
189
297
|
React.useEffect(() => {
|
|
190
298
|
if (draggingIndex !== null) {
|
|
191
299
|
document.addEventListener("mousemove", handleMouseMove)
|
|
192
|
-
document.addEventListener("mouseup",
|
|
300
|
+
document.addEventListener("mouseup", handleEnd)
|
|
301
|
+
document.addEventListener("touchmove", handleTouchMove, { passive: false })
|
|
302
|
+
document.addEventListener("touchend", handleEnd)
|
|
193
303
|
return () => {
|
|
194
304
|
document.removeEventListener("mousemove", handleMouseMove)
|
|
195
|
-
document.removeEventListener("mouseup",
|
|
305
|
+
document.removeEventListener("mouseup", handleEnd)
|
|
306
|
+
document.removeEventListener("touchmove", handleTouchMove)
|
|
307
|
+
document.removeEventListener("touchend", handleEnd)
|
|
196
308
|
}
|
|
197
309
|
}
|
|
198
|
-
}, [draggingIndex, handleMouseMove,
|
|
310
|
+
}, [draggingIndex, handleMouseMove, handleTouchMove, handleEnd])
|
|
199
311
|
|
|
200
312
|
const renderPanel = (section: AdjustableLayoutSection, size: number, index: number) => {
|
|
201
313
|
const nextSection = sections[index + 1]
|
|
202
|
-
// Drag handles should be between panels, so only show for panels that aren't the last one
|
|
203
314
|
const isResizable =
|
|
204
315
|
section.resizable !== false &&
|
|
205
316
|
sections.length > 1 &&
|
|
@@ -230,25 +341,23 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
230
341
|
{section.content}
|
|
231
342
|
</div>
|
|
232
343
|
|
|
233
|
-
{/* Show drag handle or spacer after this panel if it's not the last one */}
|
|
234
344
|
{index < sections.length - 1 && (
|
|
235
345
|
isResizable ? (
|
|
236
346
|
<div
|
|
237
347
|
className={cn(
|
|
238
|
-
"flex-shrink-0 flex items-center justify-center relative group",
|
|
348
|
+
"flex-shrink-0 flex items-center justify-center relative group touch-none select-none",
|
|
239
349
|
orientation === "vertical"
|
|
240
350
|
? "cursor-row-resize h-2 w-full"
|
|
241
351
|
: "cursor-col-resize w-2 h-full"
|
|
242
352
|
)}
|
|
243
353
|
onMouseDown={(e) => handleMouseDown(index, e)}
|
|
354
|
+
onTouchStart={(e) => handleTouchStart(index, e)}
|
|
244
355
|
>
|
|
245
356
|
{/* Visible pill */}
|
|
246
357
|
<div
|
|
247
358
|
className={cn(
|
|
248
359
|
`${colorMap[dragHandleColor]} transition-colors duration-200 rounded-full`,
|
|
249
|
-
orientation === "vertical"
|
|
250
|
-
? "h-1 w-8"
|
|
251
|
-
: "w-1 h-8"
|
|
360
|
+
orientation === "vertical" ? "h-1 w-8" : "w-1 h-8"
|
|
252
361
|
)}
|
|
253
362
|
/>
|
|
254
363
|
{/* Invisible large hit area */}
|
|
@@ -274,6 +383,97 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
274
383
|
)
|
|
275
384
|
}
|
|
276
385
|
|
|
386
|
+
const isMobileMode = isMobile && mobileBehavior !== "none"
|
|
387
|
+
|
|
388
|
+
if (isMobileMode && mobileBehavior === "tabs") {
|
|
389
|
+
return (
|
|
390
|
+
<div
|
|
391
|
+
ref={containerRef}
|
|
392
|
+
className={cn(
|
|
393
|
+
"flex flex-col overflow-hidden h-full min-w-0 w-full",
|
|
394
|
+
padded && (!hideMobileTabs ? "p-2 sm:p-4 gap-2" : "p-2 sm:p-4"),
|
|
395
|
+
className
|
|
396
|
+
)}
|
|
397
|
+
{...props}
|
|
398
|
+
>
|
|
399
|
+
{sections.length > 1 && !hideMobileTabs && (
|
|
400
|
+
<div className="flex-none pb-1">
|
|
401
|
+
<Tabs
|
|
402
|
+
value={currentActiveId}
|
|
403
|
+
onValueChange={handleTabChange}
|
|
404
|
+
className="w-full"
|
|
405
|
+
>
|
|
406
|
+
<TabsList
|
|
407
|
+
className="grid w-full h-9 bg-muted/60 p-1 rounded-lg"
|
|
408
|
+
style={{
|
|
409
|
+
gridTemplateColumns: `repeat(${sections.length}, minmax(0, 1fr))`,
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
{sections.map((section) => (
|
|
413
|
+
<TabsTrigger
|
|
414
|
+
key={section.id}
|
|
415
|
+
value={section.id}
|
|
416
|
+
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"
|
|
417
|
+
>
|
|
418
|
+
{section.label || section.title || formatSectionTitle(section.id)}
|
|
419
|
+
</TabsTrigger>
|
|
420
|
+
))}
|
|
421
|
+
</TabsList>
|
|
422
|
+
</Tabs>
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
|
|
426
|
+
<div className="flex-1 min-h-0 relative overflow-hidden">
|
|
427
|
+
{sections.map((section) => {
|
|
428
|
+
const isActive = section.id === currentActiveId
|
|
429
|
+
return (
|
|
430
|
+
<div
|
|
431
|
+
key={section.id}
|
|
432
|
+
className={cn(
|
|
433
|
+
"h-full w-full min-h-0 overflow-hidden",
|
|
434
|
+
!isActive && "hidden",
|
|
435
|
+
(section.variant ?? "default") === "default" &&
|
|
436
|
+
"bg-card border border-border rounded-xl",
|
|
437
|
+
section.className
|
|
438
|
+
)}
|
|
439
|
+
>
|
|
440
|
+
{section.content}
|
|
441
|
+
</div>
|
|
442
|
+
)
|
|
443
|
+
})}
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (isMobileMode && mobileBehavior === "stack") {
|
|
450
|
+
return (
|
|
451
|
+
<div
|
|
452
|
+
ref={containerRef}
|
|
453
|
+
className={cn(
|
|
454
|
+
"flex flex-col overflow-y-auto h-full gap-2 min-w-0 w-full",
|
|
455
|
+
padded && "p-2 sm:p-4",
|
|
456
|
+
className
|
|
457
|
+
)}
|
|
458
|
+
{...props}
|
|
459
|
+
>
|
|
460
|
+
{sections.map((section) => (
|
|
461
|
+
<div
|
|
462
|
+
key={section.id}
|
|
463
|
+
className={cn(
|
|
464
|
+
"min-h-[200px] overflow-hidden w-full flex-shrink-0",
|
|
465
|
+
(section.variant ?? "default") === "default" &&
|
|
466
|
+
"bg-card border border-border rounded-xl",
|
|
467
|
+
section.className
|
|
468
|
+
)}
|
|
469
|
+
>
|
|
470
|
+
{section.content}
|
|
471
|
+
</div>
|
|
472
|
+
))}
|
|
473
|
+
</div>
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
277
477
|
return (
|
|
278
478
|
<div
|
|
279
479
|
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,24 @@ 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"
|
|
84
91
|
>
|
|
92
|
+
<Icon name="folder" size="xs" />
|
|
93
|
+
</Button>
|
|
94
|
+
)}
|
|
95
|
+
{tabs.length > 0 && (
|
|
96
|
+
<ScrollArea className="flex-1">
|
|
97
|
+
<Tabs
|
|
98
|
+
value={activeTabId || tabs[0]?.id}
|
|
99
|
+
onValueChange={onTabSelect}
|
|
100
|
+
className="h-auto flex-1"
|
|
101
|
+
>
|
|
85
102
|
<TabsList className="h-10 rounded-none border-none bg-transparent p-0 w-full justify-start gap-0">
|
|
86
103
|
{tabs.map((tab) => (
|
|
87
104
|
<div
|
|
@@ -93,13 +110,13 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
|
|
|
93
110
|
className={cn(
|
|
94
111
|
'data-[state=inactive]:bg-muted/20 data-[state=inactive]:text-muted-foreground',
|
|
95
112
|
'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',
|
|
113
|
+
'px-2.5 sm:px-3 py-2 text-xs sm:text-sm font-medium whitespace-nowrap',
|
|
114
|
+
'flex items-center gap-1.5 sm:gap-2 h-10',
|
|
98
115
|
'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0',
|
|
99
116
|
'transition-colors'
|
|
100
117
|
)}
|
|
101
118
|
>
|
|
102
|
-
<div className="flex items-center gap-2">
|
|
119
|
+
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
103
120
|
{/* Dirty indicator */}
|
|
104
121
|
{tab.isDirty && (
|
|
105
122
|
<Icon
|
|
@@ -109,7 +126,7 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
|
|
|
109
126
|
/>
|
|
110
127
|
)}
|
|
111
128
|
{/* Tab name */}
|
|
112
|
-
<span className="truncate max-w-[200px]">{tab.name}</span>
|
|
129
|
+
<span className="truncate max-w-[120px] sm:max-w-[200px]">{tab.name}</span>
|
|
113
130
|
</div>
|
|
114
131
|
</TabsTrigger>
|
|
115
132
|
|
|
@@ -136,6 +153,7 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
|
|
|
136
153
|
</TabsList>
|
|
137
154
|
</Tabs>
|
|
138
155
|
</ScrollArea>
|
|
156
|
+
)}
|
|
139
157
|
</div>
|
|
140
158
|
)
|
|
141
159
|
}
|