Skip to content

Commit 4d49f6a

Browse files
committed
feat(TabbedForm): add TabbedForm component to organize sections into tabs
1 parent 707e96a commit 4d49f6a

File tree

2 files changed

+538
-0
lines changed

2 files changed

+538
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* FormSection Component
11+
*
12+
* A form section component that groups fields together with optional
13+
* collapsibility and multi-column layout. Aligns with @objectstack/spec FormSection.
14+
*/
15+
16+
import React, { useState } from 'react';
17+
import { ChevronDown, ChevronRight } from 'lucide-react';
18+
import { cn } from '@object-ui/components';
19+
20+
export interface FormSectionProps {
21+
/**
22+
* Section title/label
23+
*/
24+
label?: string;
25+
26+
/**
27+
* Section description
28+
*/
29+
description?: string;
30+
31+
/**
32+
* Whether the section can be collapsed
33+
* @default false
34+
*/
35+
collapsible?: boolean;
36+
37+
/**
38+
* Whether the section is initially collapsed
39+
* @default false
40+
*/
41+
collapsed?: boolean;
42+
43+
/**
44+
* Number of columns for field layout
45+
* @default 1
46+
*/
47+
columns?: 1 | 2 | 3 | 4;
48+
49+
/**
50+
* Section children (form fields)
51+
*/
52+
children: React.ReactNode;
53+
54+
/**
55+
* Additional CSS classes
56+
*/
57+
className?: string;
58+
}
59+
60+
/**
61+
* FormSection Component
62+
*
63+
* Groups form fields with optional header, collapsibility, and multi-column layout.
64+
*
65+
* @example
66+
* ```tsx
67+
* <FormSection label="Contact Details" columns={2} collapsible>
68+
* <FormField name="firstName" />
69+
* <FormField name="lastName" />
70+
* </FormSection>
71+
* ```
72+
*/
73+
export const FormSection: React.FC<FormSectionProps> = ({
74+
label,
75+
description,
76+
collapsible = false,
77+
collapsed: initialCollapsed = false,
78+
columns = 1,
79+
children,
80+
className,
81+
}) => {
82+
const [isCollapsed, setIsCollapsed] = useState(initialCollapsed);
83+
84+
const gridCols: Record<number, string> = {
85+
1: 'grid-cols-1',
86+
2: 'grid-cols-1 md:grid-cols-2',
87+
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
88+
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
89+
};
90+
91+
const handleToggle = () => {
92+
if (collapsible) {
93+
setIsCollapsed(!isCollapsed);
94+
}
95+
};
96+
97+
return (
98+
<div className={cn('form-section', className)}>
99+
{/* Section Header */}
100+
{(label || description) && (
101+
<div
102+
className={cn(
103+
'flex items-start gap-2 mb-4',
104+
collapsible && 'cursor-pointer select-none'
105+
)}
106+
onClick={handleToggle}
107+
role={collapsible ? 'button' : undefined}
108+
aria-expanded={collapsible ? !isCollapsed : undefined}
109+
>
110+
{collapsible && (
111+
<span className="mt-0.5 text-muted-foreground">
112+
{isCollapsed ? (
113+
<ChevronRight className="h-4 w-4" />
114+
) : (
115+
<ChevronDown className="h-4 w-4" />
116+
)}
117+
</span>
118+
)}
119+
<div className="flex-1">
120+
{label && (
121+
<h3 className="text-base font-semibold text-foreground">
122+
{label}
123+
</h3>
124+
)}
125+
{description && (
126+
<p className="text-sm text-muted-foreground mt-0.5">
127+
{description}
128+
</p>
129+
)}
130+
</div>
131+
</div>
132+
)}
133+
134+
{/* Section Content */}
135+
{!isCollapsed && (
136+
<div className={cn('grid gap-4', gridCols[columns])}>
137+
{children}
138+
</div>
139+
)}
140+
</div>
141+
);
142+
};
143+
144+
export default FormSection;

0 commit comments

Comments
 (0)