Form

여러 개의 입력 단위를 하나로 묶어 사용자로부터 정보를 수집하는 폼 영역

Form preview

Type: Login

import './login-form.css';

import { Button, Checkbox, Field, Form, HStack, TextInput, VStack } from '@vapor-ui/core';

export default function LoginForm() {
    return (
        <VStack
            gap="$250"
            width="400px"
            padding="$300"
            borderRadius="$300"
            border="1px solid var(--vapor-color-border-normal)"
            className="login"
            render={<Form onSubmit={(event) => event.preventDefault()} />}
        >
            <VStack gap="$200">
                <Field.Root render={<VStack gap="$100" />}>
                    <Field.Label className="input-label">이메일</Field.Label>
                    <TextInput id="login-email" size="lg" required type="email" />
                    <Field.Error match="valueMissing">이메일을 입력해주세요.</Field.Error>
                    <Field.Error match="typeMismatch">유효한 이메일 형식이 아닙니다.</Field.Error>
                </Field.Root>

                <Field.Root render={<VStack gap="$100" />}>
                    <Field.Label className="input-label">비밀번호</Field.Label>
                    <TextInput
                        id="login-password"
                        size="lg"
                        type="password"
                        required
                        pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_]).{8,16}"
                    />
                    <Field.Description>8~16자, 대소문자 영문, 특수문자 포함</Field.Description>
                    <Field.Error match="valueMissing">비밀번호를 입력해주세요.</Field.Error>
                    <Field.Error match="patternMismatch">
                        유효한 비밀번호 형식이 아닙니다.
                    </Field.Error>
                </Field.Root>
            </VStack>
            <VStack gap="$100">
                <HStack justifyContent="space-between">
                    <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                        <Checkbox.Root id="login-auto-login" />
                        <Field.Label className="checkbox-label">자동 로그인</Field.Label>
                    </Field.Root>

                    <Button type="button" variant="ghost" color="secondary">
                        ID/비밀번호 찾기
                    </Button>
                </HStack>

                <Button size="lg">로그인</Button>
                <Button size="lg" color="secondary" variant="outline">
                    회원가입
                </Button>
            </VStack>
        </VStack>
    );
}
.input-label {
    color: var(--vapor-color-foreground-normal-100, #525463);

    font-size: var(--vapor-typography-fontSize-050, 0.75rem);
    font-weight: var(--vapor-typography-fontWeight-500);
    line-height: var(--vapor-typography-lineHeight-050, 1.125rem); /* 150% */
    letter-spacing: var(--vapor-typography-letterSpacing-000, 0);
}

.checkbox-label {
    color: var(--vapor-color-foreground-normal-100, #2b2d36);

    font-size: var(--vapor-typography-fontSize-075, 0.875rem);
    font-weight: var(--vapor-typography-fontWeight-400);
    line-height: var(--vapor-typography-lineHeight-075, 1.375rem); /* 157.143% */
    letter-spacing: var(--vapor-typography-letterSpacing-100, -0.00625rem);
}

.helper-text {
    color: var(--vapor-color-foreground-hint, #6c6e7e);

    font-size: var(--vapor-typography-fontSize-075, 0.875rem);
    font-weight: var(--vapor-typography-fontWeight-400);
    line-height: var(--vapor-typography-lineHeight-075, 1.375rem); /* 157.143% */
    letter-spacing: var(--vapor-typography-letterSpacing-100, -0.00625rem);
}

Type: SignUp

import './signup-form.css';

import { useState } from 'react';

import {
    Button,
    Checkbox,
    Field,
    Form,
    HStack,
    IconButton,
    Select,
    Text,
    TextInput,
    VStack,
} from '@vapor-ui/core';
import { ChevronRightOutlineIcon } from '@vapor-ui/icons';

const jobs = [
    { label: '개발자', value: 'developer' },
    { label: '디자이너', value: 'designer' },
    { label: '프로덕트 매니저', value: 'product-manager' },
    { label: '기타', value: 'etc' },
];

export default function SignupForm() {
    const [passwordCheck, setPasswordCheck] = useState('');
    // const passwordCheck = useRef<string>('');

    return (
        <VStack
            gap="$250"
            width="400px"
            padding="$300"
            borderRadius="$300"
            border="1px solid var(--vapor-color-border-normal)"
            className="login"
            render={<Form onSubmit={(event) => event.preventDefault()} />}
        >
            <VStack gap="$400">
                <VStack gap="$200">
                    <Field.Root render={<VStack gap="$100" />}>
                        <Field.Label className="input-label">이메일</Field.Label>
                        <TextInput id="signup-email" size="lg" required type="email" />
                        <Field.Error match="valueMissing">이메일을 입력해주세요.</Field.Error>
                        <Field.Error match="typeMismatch">
                            유효한 이메일 형식이 아닙니다.
                        </Field.Error>
                    </Field.Root>

                    <Field.Root render={<VStack gap="$100" />}>
                        <Field.Label className="input-label">비밀번호</Field.Label>
                        <TextInput
                            id="signup-password"
                            size="lg"
                            type="password"
                            onValueChange={(value) => {
                                setPasswordCheck(value);
                            }}
                            required
                            pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_]).{8,16}"
                        />
                        <Field.Description>
                            8~16자, 대소문자 영문, 숫자, 특수문자 포함
                        </Field.Description>
                        <Field.Error match="valueMissing">비밀번호를 입력해주세요.</Field.Error>
                        <Field.Error match="patternMismatch">
                            유효한 비밀번호 형식이 아닙니다.
                        </Field.Error>
                    </Field.Root>

                    <Field.Root render={<VStack gap="$100" />}>
                        <Field.Label className="input-label">비밀번호 확인</Field.Label>
                        <TextInput
                            id="signup-password-check"
                            size="lg"
                            type="password"
                            required
                            pattern={passwordCheck}
                        />
                        <Field.Description>8~16자, 대소문자 영문, 특수문자 포함</Field.Description>
                        <Field.Error match="valueMissing">비밀번호를 입력해주세요.</Field.Error>
                        <Field.Error match="patternMismatch">
                            비밀번호를 다시 확인해주세요.
                        </Field.Error>
                    </Field.Root>

                    <Field.Root render={<VStack gap="$100" />}>
                        <Field.Label className="input-label">이름</Field.Label>
                        <TextInput id="signup-name" size="lg" required />
                        <Field.Error match="valueMissing">이름을 입력해주세요.</Field.Error>
                    </Field.Root>

                    <Field.Root render={<VStack gap="$100" />}>
                        <Field.Label className="input-label">직업</Field.Label>
                        <Select.Root items={jobs} placeholder="직업을 선택해주세요." size="lg">
                            <Select.Trigger id="signup-jobs">
                                <Select.Value />
                                <Select.TriggerIcon />
                            </Select.Trigger>
                            <Select.Content>
                                {jobs.map((job) => (
                                    <Select.Item key={job.value} value={job.value}>
                                        {job.label}
                                        <Select.ItemIndicator />
                                    </Select.Item>
                                ))}
                            </Select.Content>
                        </Select.Root>
                    </Field.Root>
                </VStack>

                <VStack gap="$300">
                    <VStack justifyContent="space-between" gap="$050">
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root id="signup-agree-all" />
                            <Field.Label className="checkbox-label">
                                필수 약관에 모두 동의
                            </Field.Label>
                        </Field.Root>
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root id="signup-terms-of-service" />
                            <HStack width="100%" justifyContent="space-between" alignItems="center">
                                <Field.Label className="checkbox-label">이용 약관 동의</Field.Label>
                                <IconButton
                                    size="sm"
                                    color="secondary"
                                    variant="ghost"
                                    aria-label="약관 보기"
                                >
                                    <ChevronRightOutlineIcon />
                                </IconButton>
                            </HStack>
                        </Field.Root>
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root id="signup-personal-info-collection" />
                            <HStack width="100%" justifyContent="space-between" alignItems="center">
                                <Field.Label className="checkbox-label">
                                    개인 정보 수집 이용 동의
                                </Field.Label>
                                <IconButton
                                    size="sm"
                                    color="secondary"
                                    variant="ghost"
                                    aria-label="개인 정보 수집 이용 보기"
                                >
                                    <ChevronRightOutlineIcon />
                                </IconButton>
                            </HStack>
                        </Field.Root>
                    </VStack>

                    <Button size="lg">회원가입</Button>
                </VStack>
            </VStack>

            <HStack justifyContent={'center'}>
                <Text typography="body2">이미 계정이 있으세요?</Text>
                <Button type="button" size="sm" variant="ghost">
                    로그인
                </Button>
            </HStack>
        </VStack>
    );
}
.input-label {
    color: var(--vapor-color-foreground-normal-100, #525463);

    font-size: var(--vapor-typography-fontSize-050, 0.75rem);
    font-weight: var(--vapor-typography-fontWeight-500);
    line-height: var(--vapor-typography-lineHeight-050, 1.125rem); /* 150% */
    letter-spacing: var(--vapor-typography-letterSpacing-000, 0);
}

.checkbox-label {
    color: var(--vapor-color-foreground-normal-100, #2b2d36);

    font-size: var(--vapor-typography-fontSize-075, 0.875rem);
    font-weight: var(--vapor-typography-fontWeight-400);
    line-height: var(--vapor-typography-lineHeight-075, 1.375rem); /* 157.143% */
    letter-spacing: var(--vapor-typography-letterSpacing-100, -0.00625rem);
}

.helper-text {
    color: var(--vapor-color-foreground-hint, #6c6e7e);

    font-size: var(--vapor-typography-fontSize-075, 0.875rem);
    font-weight: var(--vapor-typography-fontWeight-400);
    line-height: var(--vapor-typography-lineHeight-075, 1.375rem); /* 157.143% */
    letter-spacing: var(--vapor-typography-letterSpacing-100, -0.00625rem);
}

Type: Authentication

import { Children, cloneElement, isValidElement, useState } from 'react';

import './authentication-form.css';

import { Button, Field, Form, Select, TextInput, VStack } from '@vapor-ui/core';

const codes = {
    '+82': '🇰🇷 +82',
    '+1': '🇺🇸 +1',
    '+34': '🇪🇸 +34',
    '+33': '🇫🇷 +33',
    '+39': '🇮🇹 +39',
    '+44': '🇬🇧 +44',
    '+81': '🇯🇵 +81',
    '+86': '🇨🇳 +86',
    '+7': '🇷🇺 +7',
};

export default function AuthenticationForm() {
    const [phoneNumber, setPhoneNumber] = useState('');
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setPhoneNumber(e.target.value);
    };

    const regex = /^[0-9\s-()]{6,20}$/;

    return (
        <VStack
            gap="$400"
            width="400px"
            padding="$300"
            borderRadius="$300"
            border="1px solid var(--vapor-color-border-normal)"
            render={<Form onSubmit={(e) => e.preventDefault()} />}
        >
            <VStack gap="$200">
                <Field.Root render={<VStack gap="$100" />}>
                    <Field.Label className="input-label">핸드폰 번호</Field.Label>

                    <Select.Root defaultValue={codes['+82']} size="lg">
                        <Group attached>
                            <Select.Trigger>
                                <Select.Value />
                                <Select.TriggerIcon />
                            </Select.Trigger>

                            <Select.Content>
                                {Object.entries(codes).map(([value, label]) => (
                                    <Select.Item key={value} value={value}>
                                        {label}
                                        <Select.ItemIndicator />
                                    </Select.Item>
                                ))}
                            </Select.Content>

                            <TextInput
                                style={{ flex: 1, width: '100%' }}
                                id="auth-phone"
                                type="tel"
                                value={phoneNumber}
                                onChange={handleChange}
                                required
                                pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
                                size="lg"
                            />

                            <Button type="button" size="lg" disabled={!regex.test(phoneNumber)}>
                                인증번호 받기
                            </Button>
                        </Group>
                    </Select.Root>

                    <Field.Error match="valueMissing">핸드폰 번호를 입력해주세요.</Field.Error>
                    <Field.Error match="patternMismatch">
                        올바른 핸드폰 번호를 입력해주세요.
                    </Field.Error>
                </Field.Root>

                <Field.Root render={<VStack gap="$100" />}>
                    <Field.Label className="input-label">인증번호</Field.Label>
                    <TextInput id="auth-verification-code" size="lg" required />
                    <Field.Error match="valueMissing">인증번호를 입력해주세요.</Field.Error>
                </Field.Root>
            </VStack>

            <Button size="lg">인증 완료</Button>
        </VStack>
    );
}

interface GroupProps {
    attached?: boolean;
    children?: React.ReactNode;
}

const Group = ({ attached = false, children: childrenProp }: GroupProps) => {
    const children = Children.map(childrenProp, (child, index) => {
        if (!isValidElement(child)) return;

        return cloneElement(child as React.ReactElement, {
            style: { '--group-index': index, ...child.props.style },
            ...(index === 0 ? { 'data-first-item': '' } : {}),
            ...(index === Children.count(childrenProp) - 1 ? { 'data-last-item': '' } : {}),
        });
    });

    return (
        <div data-part="group" className={`group` + (attached ? ' attached' : '')}>
            {children}
        </div>
    );
};
.input-label {
    color: var(--vapor-color-foreground-normal-100, #525463);

    font-size: var(--vapor-typography-fontSize-050, 0.75rem);
    font-weight: var(--vapor-typography-fontWeight-500);
    line-height: var(--vapor-typography-lineHeight-050, 1.125rem); /* 150% */
    letter-spacing: var(--vapor-typography-letterSpacing-000, 0);
}

.group {
    display: flex;
    align-items: center;
    gap: 0;

    &.attached {
        & > [data-first-item] {
            border-right: none;
            border-top-right-radius: 0;
            border-bottom-right-radius: 0;
        }

        & > [data-last-item] {
            border: 1px solid var(--vapor-color-border-normal, #d1d2d8);
            border-top-left-radius: 0;
            border-bottom-left-radius: 0;
        }

        & > *:not([data-first-item]):not([data-last-item]) {
            border: 1px solid var(--vapor-color-border-normal, #d1d2d8);
            border-right: none;
            border-radius: 0;
        }
    }
}

Type: Research

import './research-form.css';

import {
    Button,
    Checkbox,
    Field,
    Form,
    HStack,
    MultiSelect,
    Radio,
    RadioGroup,
    Select,
    Switch,
    Text,
    TextInput,
    VStack,
} from '@vapor-ui/core';

const jobs = [
    { label: '프론트엔드 개발자', value: 'frontend-engineer' },
    { label: '백엔드 개발자', value: 'backend-engineer' },
    { label: '풀스택 개발자', value: 'fullstack-engineer' },
    { label: '모바일 앱 개발자', value: 'app-engineer' },
    { label: 'DevOps/클라우드 엔지니어', value: 'devops-engineer' },
];

const stacks = [
    { label: 'HTML/CSS', value: 'html-css' },
    { label: 'JavaScript', value: 'javascript' },
    { label: 'React', value: 'react' },
    { label: 'Vue.js', value: 'vue' },
    { label: 'Next.js', value: 'nextjs' },
];

export default function ResearchForm() {
    return (
        <VStack
            gap="$500"
            width="400px"
            padding="$300"
            borderRadius="$300"
            border="1px solid var(--vapor-color-border-normal)"
            render={<Form onSubmit={(event) => event.preventDefault()} />}
        >
            <VStack gap="$200">
                <Text typography="heading5">기본 정보를 입력해주세요.</Text>

                <Field.Root render={<VStack gap="$100" />}>
                    <Field.Label className="input-label">이름 </Field.Label>
                    <TextInput id="research-name" required size="lg" />
                    <Field.Error match="valueMissing">이름을 입력해주세요.</Field.Error>
                </Field.Root>

                <Field.Root render={<VStack gap="$100" />}>
                    <Field.Label className="input-label">직업</Field.Label>
                    <Select.Root items={jobs} placeholder="직업을 선택해주세요." size="lg">
                        <Select.Trigger id="research-jobs">
                            <Select.Value />
                            <Select.TriggerIcon />
                        </Select.Trigger>
                        <Select.Content>
                            {jobs.map((job) => (
                                <Select.Item key={job.value} value={job.value}>
                                    {job.label}
                                    <Select.ItemIndicator />
                                </Select.Item>
                            ))}
                        </Select.Content>
                    </Select.Root>
                </Field.Root>

                <Field.Root render={<VStack gap="$100" />}>
                    <Field.Label className="input-label">스택</Field.Label>
                    <MultiSelect.Root
                        items={stacks}
                        placeholder="자주 사용하는 스택을 선택해주세요."
                        size="lg"
                    >
                        <MultiSelect.Trigger id="research-stack">
                            <MultiSelect.Value />
                            <MultiSelect.TriggerIcon />
                        </MultiSelect.Trigger>
                        <MultiSelect.Content>
                            {stacks.map((stack) => (
                                <MultiSelect.Item key={stack.value} value={stack.value}>
                                    {stack.label}
                                    <MultiSelect.ItemIndicator />
                                </MultiSelect.Item>
                            ))}
                        </MultiSelect.Content>
                    </MultiSelect.Root>
                </Field.Root>
            </VStack>

            <Field.Root render={<VStack gap="$150" />}>
                <RadioGroup.Root>
                    <RadioGroup.Label>만족도를 선택해주세요.</RadioGroup.Label>
                    <HStack
                        render={<Field.Label />}
                        alignItems="center"
                        gap="$100"
                        className="radio-label"
                    >
                        <Radio.Root
                            id="research-fully-satisfied"
                            value="fully-satisfied"
                            size="lg"
                        />
                        매우 만족
                    </HStack>

                    <HStack
                        render={<Field.Label />}
                        alignItems="center"
                        gap="$100"
                        className="radio-label"
                    >
                        <Radio.Root id="research-neutral" value="neutral" size="lg" />
                        매우 만족
                    </HStack>

                    <HStack
                        render={<Field.Label />}
                        alignItems="center"
                        gap="$100"
                        className="radio-label"
                    >
                        <Radio.Root id="research-not-satisfied" value="not-satisfied" size="lg" />
                        불만족
                    </HStack>
                </RadioGroup.Root>
            </Field.Root>

            <VStack gap="$100">
                <VStack marginBottom="$050">
                    <Text typography="heading5">좋았던 강의는 무엇인가요?</Text>
                    <Text typography="body2" foreground="normal-100">
                        중복 선택 가능
                    </Text>
                </VStack>

                <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                    <Checkbox.Root id="research-mentoring" size="lg" />
                    <Field.Label className="checkbox-label">멘토님 강연 능력</Field.Label>
                </Field.Root>

                <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                    <Checkbox.Root id="research-topic" size="lg" />
                    <Field.Label className="checkbox-label">
                        주제(협업 및 커뮤니케이션 스킬)
                    </Field.Label>
                </Field.Root>

                <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                    <Checkbox.Root id="research-content" size="lg" />
                    <Field.Label className="checkbox-label">전반적인 강의 내용</Field.Label>
                </Field.Root>

                <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                    <Checkbox.Root id="research-seminar" size="lg" />
                    <Field.Label className="checkbox-label">세미나 자료</Field.Label>
                </Field.Root>

                <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                    <Checkbox.Root id="research-etc" size="lg" />
                    <Field.Label className="checkbox-label">기타</Field.Label>
                </Field.Root>
            </VStack>

            <VStack gap="$100">
                <Text typography="heading5">개인 정보 수신 동의</Text>

                <Field.Root render={<HStack justifyContent="space-between" alignItems="center" />}>
                    <Field.Label className="checkbox-label">서비스 메일 수신 동의</Field.Label>
                    <Switch.Root defaultChecked id="research-service" />
                </Field.Root>
                <Field.Root render={<HStack justifyContent="space-between" alignItems="center" />}>
                    <Field.Label htmlFor="research-advertising" className="checkbox-label">
                        이벤트성 광고 수신 동의
                    </Field.Label>
                    <Switch.Root defaultChecked id="research-advertising" />
                </Field.Root>
            </VStack>

            <Button size="lg">제출하기</Button>
        </VStack>
    );
}
.input-label {
    color: var(--vapor-color-foreground-normal-100, #525463);

    font-size: var(--vapor-typography-fontSize-050, 0.75rem);
    font-weight: var(--vapor-typography-fontWeight-500);
    line-height: var(--vapor-typography-lineHeight-050, 1.125rem); /* 150% */
    letter-spacing: var(--vapor-typography-letterSpacing-000, 0);
}

.checkbox-label {
    color: var(--vapor-color-foreground-normal-100, #2b2d36);

    font-size: var(--vapor-typography-fontSize-075, 0.875rem);
    font-weight: var(--vapor-typography-fontWeight-400);
    line-height: var(--vapor-typography-lineHeight-075, 1.375rem); /* 157.143% */
    letter-spacing: var(--vapor-typography-letterSpacing-100, -0.00625rem);
}

.radio-label {
    color: var(--vapor-color-foreground-normal-100, #2b2d36);

    font-size: var(--vapor-typography-fontSize-075, 0.875rem);
    font-weight: var(--vapor-typography-fontWeight-400);
    line-height: var(--vapor-typography-lineHeight-075, 1.375rem); /* 157.143% */
    letter-spacing: var(--vapor-typography-letterSpacing-100, -0.00625rem);
}

Type: Filter

import './filter-form.css';

import type { FormEvent } from 'react';
import { useCallback, useRef, useState } from 'react';

import {
    Box,
    Button,
    Checkbox,
    Collapsible,
    Field,
    Form,
    HStack,
    Radio,
    RadioGroup,
    Text,
    VStack,
} from '@vapor-ui/core';
import { ChevronDownOutlineIcon, RefreshOutlineIcon } from '@vapor-ui/icons';

// 초기값 정의
type FormData = typeof FORM_SCHEME;

const FORM_SCHEME = {
    view: 'recent',
    sort: {
        feedback: false,
        buttons: false,
        'data-display': false,
        overlay: false,
        inputs: false,
        navigation: false,
        utils: false,
    },
    packs: {
        'goorm-dev/vapor-core': false,
        'goorm-dev/vapor-component': false,
        'vapor-ui/core': false,
    },
    status: { active: false, inactive: false, draft: false },
    tag: { ui: false, 'open-source': false, performance: false },
};

export default function FilterForm() {
    const formRef = useRef<HTMLFormElement>(null);
    const [formData, setFormData] = useState<FormData>({
        view: FORM_SCHEME.view,
        sort: { ...FORM_SCHEME.sort },
        packs: { ...FORM_SCHEME.packs },
        status: { ...FORM_SCHEME.status },
        tag: { ...FORM_SCHEME.tag },
    });

    const getFieldValues = useCallback(
        <T extends keyof FormData>(fieldName: T): FormData[T] => formData[fieldName],
        [formData],
    );

    const updateFormData = useCallback(
        (fieldName: keyof FormData, key: string, checked: boolean) => {
            setFormData((prev) => {
                const field = prev[fieldName];

                if (typeof field !== 'object') return prev;

                return { ...prev, [fieldName]: { ...field, [key]: checked } };
            });
        },
        [],
    );

    // 라디오 버튼 변경 핸들러
    const handleRadioChange = useCallback((fieldName: keyof FormData, value: string) => {
        setFormData((prev) => ({ ...prev, [fieldName]: value }));
    }, []);

    // 체크박스 변경 핸들러
    const handleCheckboxChange = useCallback(
        (fieldName: keyof FormData, key: string) => (checked: boolean) => {
            updateFormData(fieldName, key, checked);
        },
        [updateFormData],
    );

    const selectedSortCount = Object.values(getFieldValues('sort')).filter(Boolean).length;

    const handleReset = (event: FormEvent<HTMLFormElement>) => {
        event.preventDefault();

        setFormData({
            view: FORM_SCHEME.view,
            sort: { ...FORM_SCHEME.sort },
            packs: { ...FORM_SCHEME.packs },
            status: { ...FORM_SCHEME.status },
            tag: { ...FORM_SCHEME.tag },
        });
    };

    return (
        <VStack
            width="17.625rem"
            className="filter"
            render={<Form ref={formRef} onReset={handleReset} />}
        >
            <HStack justifyContent="space-between">
                <Text typography="heading5">Filter</Text>
                <Button type="reset" size="sm" variant="ghost" color="secondary">
                    <RefreshOutlineIcon />
                    Refresh
                </Button>
            </HStack>

            <Box
                render={<hr />}
                border="none"
                marginY="$150"
                height="1px"
                width="100%"
                backgroundColor="$gray-300"
            />

            <VStack gap="$300">
                <Collapsible.Root>
                    <Collapsible.Trigger className="collapsible-trigger">
                        <Text typography="heading6">View</Text>

                        <ChevronDownOutlineIcon className="trigger-icon" />
                    </Collapsible.Trigger>

                    <Collapsible.Panel>
                        <Field.Root render={<Box marginTop="$150" />}>
                            <RadioGroup.Root
                                value={getFieldValues('view')}
                                onValueChange={(value: unknown) =>
                                    handleRadioChange('view', value as string)
                                }
                            >
                                <HStack
                                    render={<Field.Label />}
                                    gap="$100"
                                    alignItems="center"
                                    className="radio-label"
                                >
                                    <Radio.Root id="filter-recent" value="recent" />
                                    Recent
                                </HStack>
                                <HStack
                                    render={<Field.Label />}
                                    gap="$100"
                                    alignItems="center"
                                    className="radio-label"
                                >
                                    <Radio.Root id="filter-popular" value="popular" />
                                    Most Popular
                                </HStack>
                            </RadioGroup.Root>
                        </Field.Root>
                    </Collapsible.Panel>
                </Collapsible.Root>

                <Collapsible.Root>
                    <Collapsible.Trigger className="collapsible-trigger">
                        <Text typography="heading6">
                            Sort <Text foreground="primary-100">{selectedSortCount}</Text>
                        </Text>
                        <ChevronDownOutlineIcon className="trigger-icon" />
                    </Collapsible.Trigger>

                    <Collapsible.Panel>
                        <Field.Root
                            render={<HStack alignItems="center" gap="$100" marginTop="$150" />}
                        >
                            <Checkbox.Root
                                id="filter-feedback"
                                checked={getFieldValues('sort').feedback}
                                onCheckedChange={handleCheckboxChange('sort', 'feedback')}
                            />
                            <Field.Label className="checkbox-label">Feedback</Field.Label>
                        </Field.Root>
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-buttons"
                                checked={getFieldValues('sort').buttons}
                                onCheckedChange={handleCheckboxChange('sort', 'buttons')}
                            />
                            <Field.Label className="checkbox-label">Buttons</Field.Label>
                        </Field.Root>

                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-data-display"
                                checked={getFieldValues('sort')['data-display']}
                                onCheckedChange={handleCheckboxChange('sort', 'data-display')}
                            />
                            <Field.Label className="checkbox-label">Data Display</Field.Label>
                        </Field.Root>

                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-overlay"
                                checked={getFieldValues('sort').overlay}
                                onCheckedChange={handleCheckboxChange('sort', 'overlay')}
                            />
                            <Field.Label className="checkbox-label">Overlay</Field.Label>
                        </Field.Root>

                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-inputs"
                                checked={getFieldValues('sort').inputs}
                                onCheckedChange={handleCheckboxChange('sort', 'inputs')}
                            />
                            <Field.Label className="checkbox-label">Inputs</Field.Label>
                        </Field.Root>

                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-navigation"
                                checked={getFieldValues('sort').navigation}
                                onCheckedChange={handleCheckboxChange('sort', 'navigation')}
                            />
                            <Field.Label className="checkbox-label">Navigation</Field.Label>
                        </Field.Root>

                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-utils"
                                checked={getFieldValues('sort').utils}
                                onCheckedChange={handleCheckboxChange('sort', 'utils')}
                            />
                            <Field.Label className="checkbox-label">Utils</Field.Label>
                        </Field.Root>
                    </Collapsible.Panel>
                </Collapsible.Root>

                <Collapsible.Root>
                    <Collapsible.Trigger className="collapsible-trigger">
                        <Text typography="heading6">Package</Text>
                        <ChevronDownOutlineIcon className="trigger-icon" />
                    </Collapsible.Trigger>

                    <Collapsible.Panel>
                        <Field.Root
                            render={<HStack alignItems="center" gap="$100" marginTop="$150" />}
                        >
                            <Checkbox.Root
                                id="filter-goorm-dev/vapor-core"
                                checked={getFieldValues('packs')['goorm-dev/vapor-core']}
                                onCheckedChange={handleCheckboxChange(
                                    'packs',
                                    'goorm-dev/vapor-core',
                                )}
                            />
                            <Field.Label className="checkbox-label">
                                goorm-dev/vapor-core
                            </Field.Label>
                        </Field.Root>
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-goorm-dev/vapor-component"
                                checked={getFieldValues('packs')['goorm-dev/vapor-component']}
                                onCheckedChange={handleCheckboxChange(
                                    'packs',
                                    'goorm-dev/vapor-component',
                                )}
                            />
                            <Field.Label className="checkbox-label">
                                goorm-dev/vapor-component
                            </Field.Label>
                        </Field.Root>
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-vapor-ui/core"
                                checked={getFieldValues('packs')['vapor-ui/core']}
                                onCheckedChange={handleCheckboxChange('packs', 'vapor-ui/core')}
                            />
                            <Field.Label className="checkbox-label">vapor-ui/core</Field.Label>
                        </Field.Root>
                    </Collapsible.Panel>
                </Collapsible.Root>

                <Collapsible.Root>
                    <Collapsible.Trigger className="collapsible-trigger">
                        <Text typography="heading6">Status</Text>
                        <ChevronDownOutlineIcon className="trigger-icon" />
                    </Collapsible.Trigger>

                    <Collapsible.Panel>
                        <Field.Root
                            render={<HStack alignItems="center" gap="$100" marginTop="$150" />}
                        >
                            <Checkbox.Root
                                id="filter-active"
                                checked={getFieldValues('status').active}
                                onCheckedChange={handleCheckboxChange('status', 'active')}
                            />
                            <Field.Label className="checkbox-label">Active</Field.Label>
                        </Field.Root>
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-inactive"
                                checked={getFieldValues('status').inactive}
                                onCheckedChange={handleCheckboxChange('status', 'inactive')}
                            />
                            <Field.Label className="checkbox-label">Inactive</Field.Label>
                        </Field.Root>
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-draft"
                                checked={getFieldValues('status').draft}
                                onCheckedChange={handleCheckboxChange('status', 'draft')}
                            />
                            <Field.Label className="checkbox-label">Draft</Field.Label>
                        </Field.Root>
                    </Collapsible.Panel>
                </Collapsible.Root>

                <Collapsible.Root>
                    <Collapsible.Trigger className="collapsible-trigger">
                        <Text typography="heading6">Tag</Text>
                        <ChevronDownOutlineIcon className="trigger-icon" />
                    </Collapsible.Trigger>

                    <Collapsible.Panel>
                        <Field.Root
                            render={<HStack alignItems="center" gap="$100" marginTop="$150" />}
                        >
                            <Checkbox.Root
                                id="filter-ui"
                                checked={getFieldValues('tag').ui}
                                onCheckedChange={handleCheckboxChange('tag', 'ui')}
                            />
                            <Field.Label className="checkbox-label">UI</Field.Label>
                        </Field.Root>
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-open-source"
                                checked={getFieldValues('tag')['open-source']}
                                onCheckedChange={handleCheckboxChange('tag', 'open-source')}
                            />
                            <Field.Label className="checkbox-label">Open Source</Field.Label>
                        </Field.Root>
                        <Field.Root render={<HStack alignItems="center" gap="$100" />}>
                            <Checkbox.Root
                                id="filter-performance"
                                checked={getFieldValues('tag').performance}
                                onCheckedChange={handleCheckboxChange('tag', 'performance')}
                            />
                            <Field.Label className="checkbox-label">Performance</Field.Label>
                        </Field.Root>
                    </Collapsible.Panel>
                </Collapsible.Root>
            </VStack>
        </VStack>
    );
}
.checkbox-label {
    color: var(--vapor-color-foreground-normal-100, #2b2d36);

    font-size: var(--vapor-typography-fontSize-075, 0.875rem);
    font-weight: var(--vapor-typography-fontWeight-400);
    line-height: var(--vapor-typography-lineHeight-075, 1.375rem); /* 157.143% */
    letter-spacing: var(--vapor-typography-letterSpacing-100, -0.00625rem);
}

.radio-label {
    color: var(--vapor-color-foreground-normal-100, #2b2d36);

    font-size: var(--vapor-typography-fontSize-075, 0.875rem);
    font-weight: var(--vapor-typography-fontWeight-400);
    line-height: var(--vapor-typography-lineHeight-075, 1.375rem); /* 157.143% */
    letter-spacing: var(--vapor-typography-letterSpacing-100, -0.00625rem);
}

.collapsible-trigger {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    transition: transform 150ms ease;
}

.trigger-icon {
    transition: transform 150ms ease;

    &:where([data-open] &) {
        transform: rotate(180deg);
    }

    &:where([data-closed] &) {
        transform: rotate(0);
    }
}

Type: BottomSheetFilter

import './sheet-form.css';

import type { FormEvent } from 'react';
import { useCallback, useState } from 'react';

import { Button, Checkbox, Field, Form, HStack, Sheet, Tabs, VStack } from '@vapor-ui/core';
import { RefreshOutlineIcon } from '@vapor-ui/icons';

type FormData = typeof FORM_SCHEME;

const FORM_SCHEME = {
    sort: {
        feedback: false,
        buttons: true,
        'data-display': false,
        overlay: false,
        inputs: true,
        navigation: false,
        utils: false,
    },
    packs: {
        'goorm-dev/vapor-core': true,
        'goorm-dev/vapor-component': false,
        'vapor-ui/core': false,
    },
    status: {
        active: true,
        inactive: false,
        draft: false,
    },
    tag: {
        ui: true,
        'open-source': false,
        performance: false,
    },
};

export default function SheetForm() {
    const [formData, setFormData] = useState<FormData>(() => {
        return {
            sort: { ...FORM_SCHEME.sort },
            packs: { ...FORM_SCHEME.packs },
            status: { ...FORM_SCHEME.status },
            tag: { ...FORM_SCHEME.tag },
        };
    });

    const getFieldValues = useCallback(
        <T extends keyof FormData>(fieldName: T): FormData[T] => formData[fieldName],
        [formData],
    );

    const updateFormData = useCallback(
        (fieldName: keyof FormData, key: string, checked: boolean) => {
            setFormData((prev) => {
                const field = prev[fieldName];

                if (typeof field !== 'object') return prev;

                return { ...prev, [fieldName]: { ...field, [key]: checked } };
            });
        },
        [],
    );

    const handleCheckboxChange = useCallback(
        (fieldName: keyof FormData, key: string) => (checked: boolean) => {
            updateFormData(fieldName, key, checked);
        },
        [updateFormData],
    );

    const handleReset = useCallback((event: FormEvent<HTMLFormElement>) => {
        event.preventDefault();

        setFormData({
            sort: { ...FORM_SCHEME.sort },
            packs: { ...FORM_SCHEME.packs },
            status: { ...FORM_SCHEME.status },
            tag: { ...FORM_SCHEME.tag },
        });
    }, []);

    return (
        <VStack
            gap="$250"
            width="400px"
            padding="$300"
            borderRadius="$300"
            border="1px solid #eee"
            className="sheet-form"
            render={<Form id="sheet-form" onReset={handleReset} />}
        >
            <Sheet.Root>
                <Sheet.Trigger render={<Button />}>Open Filter</Sheet.Trigger>
                <Sheet.Portal>
                    <Sheet.Overlay />
                    <Sheet.Positioner side="bottom">
                        <Sheet.Popup className={'popup'}>
                            <Sheet.Header className="header">
                                <Sheet.Title>Filter</Sheet.Title>
                            </Sheet.Header>
                            <Sheet.Body className="body">
                                <Tabs.Root defaultValue={'sort'} className={'tabs'}>
                                    <Tabs.List className={'tabs-list'}>
                                        <Tabs.Trigger value="sort">Sort</Tabs.Trigger>
                                        <Tabs.Trigger value="package">Package</Tabs.Trigger>
                                        <Tabs.Trigger value="status">Status</Tabs.Trigger>
                                        <Tabs.Trigger value="tag">Tag</Tabs.Trigger>
                                        <Tabs.Indicator />
                                    </Tabs.List>
                                    <Tabs.Panel value="sort" className={'tabs-panel'}>
                                        <VStack gap="$100">
                                            {/* Sort */}
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-feedback"
                                                    size="lg"
                                                    checked={getFieldValues('sort').feedback}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'sort',
                                                        'feedback',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Feedback
                                                </Field.Label>
                                            </Field.Root>

                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-buttons"
                                                    size="lg"
                                                    checked={getFieldValues('sort').buttons}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'sort',
                                                        'buttons',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Buttons
                                                </Field.Label>
                                            </Field.Root>

                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-data-display"
                                                    size="lg"
                                                    checked={getFieldValues('sort')['data-display']}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'sort',
                                                        'data-display',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Data Display
                                                </Field.Label>
                                            </Field.Root>
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-overlay"
                                                    size="lg"
                                                    checked={getFieldValues('sort').overlay}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'sort',
                                                        'overlay',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Overlay
                                                </Field.Label>
                                            </Field.Root>
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-inputs"
                                                    size="lg"
                                                    checked={getFieldValues('sort').inputs}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'sort',
                                                        'inputs',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Inputs
                                                </Field.Label>
                                            </Field.Root>
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-navigation"
                                                    size="lg"
                                                    checked={getFieldValues('sort').navigation}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'sort',
                                                        'navigation',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Navigation
                                                </Field.Label>
                                            </Field.Root>
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-utils"
                                                    size="lg"
                                                    checked={getFieldValues('sort').utils}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'sort',
                                                        'utils',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Utils
                                                </Field.Label>
                                            </Field.Root>
                                        </VStack>
                                    </Tabs.Panel>
                                    {/* Package */}
                                    <Tabs.Panel value="package" className={'tabs-panel'}>
                                        <VStack gap="$100">
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-goorm-dev/vapor-core"
                                                    size="lg"
                                                    checked={
                                                        getFieldValues('packs')[
                                                            'goorm-dev/vapor-core'
                                                        ]
                                                    }
                                                    onCheckedChange={handleCheckboxChange(
                                                        'packs',
                                                        'goorm-dev/vapor-core',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    goorm-dev/vapor-core
                                                </Field.Label>
                                            </Field.Root>
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-goorm-dev/vapor-component"
                                                    size="lg"
                                                    checked={
                                                        getFieldValues('packs')[
                                                            'goorm-dev/vapor-component'
                                                        ]
                                                    }
                                                    onCheckedChange={handleCheckboxChange(
                                                        'packs',
                                                        'goorm-dev/vapor-component',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    goorm-dev/vapor-component
                                                </Field.Label>
                                            </Field.Root>
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-vapor-ui/core"
                                                    size="lg"
                                                    checked={
                                                        getFieldValues('packs')['vapor-ui/core']
                                                    }
                                                    onCheckedChange={handleCheckboxChange(
                                                        'packs',
                                                        'vapor-ui/core',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    vapor-ui/core
                                                </Field.Label>
                                            </Field.Root>
                                        </VStack>
                                    </Tabs.Panel>
                                    {/* Status */}
                                    <Tabs.Panel value="status" className={'tabs-panel'}>
                                        <VStack gap="$100">
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-active"
                                                    size="lg"
                                                    checked={getFieldValues('status').active}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'status',
                                                        'active',
                                                    )}
                                                />
                                                <Field.Label className={'checkbox-label'}>
                                                    Active
                                                </Field.Label>
                                            </Field.Root>
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-inactive"
                                                    size="lg"
                                                    checked={getFieldValues('status').inactive}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'status',
                                                        'inactive',
                                                    )}
                                                />
                                                <Field.Label className={'checkbox-label'}>
                                                    Inactive
                                                </Field.Label>
                                            </Field.Root>
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-draft"
                                                    size="lg"
                                                    checked={getFieldValues('status').draft}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'status',
                                                        'draft',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Draft
                                                </Field.Label>
                                            </Field.Root>
                                        </VStack>
                                    </Tabs.Panel>
                                    {/* Tag */}
                                    <Tabs.Panel value="tag" className={'tabs-panel'}>
                                        <VStack gap="$100">
                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-ui"
                                                    size="lg"
                                                    checked={getFieldValues('tag').ui}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'tag',
                                                        'ui',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    UI
                                                </Field.Label>
                                            </Field.Root>

                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-open-source"
                                                    size="lg"
                                                    checked={getFieldValues('tag')['open-source']}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'tag',
                                                        'open-source',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Open Source
                                                </Field.Label>
                                            </Field.Root>

                                            <Field.Root
                                                render={<HStack alignItems="center" gap="$100" />}
                                            >
                                                <Checkbox.Root
                                                    id="sheet-performance"
                                                    size="lg"
                                                    checked={getFieldValues('tag').performance}
                                                    onCheckedChange={handleCheckboxChange(
                                                        'tag',
                                                        'performance',
                                                    )}
                                                />
                                                <Field.Label className="checkbox-label">
                                                    Performance
                                                </Field.Label>
                                            </Field.Root>
                                        </VStack>
                                    </Tabs.Panel>
                                </Tabs.Root>
                            </Sheet.Body>
                            <Sheet.Footer className="footer">
                                <Button
                                    type="reset"
                                    size="lg"
                                    color="secondary"
                                    className="refresh-button"
                                    form="sheet-form"
                                >
                                    <RefreshOutlineIcon />
                                    Refresh
                                </Button>
                                <Sheet.Close render={<Button size="lg" className="apply-button" />}>
                                    Apply
                                </Sheet.Close>
                            </Sheet.Footer>
                        </Sheet.Popup>
                    </Sheet.Positioner>
                </Sheet.Portal>
            </Sheet.Root>
        </VStack>
    );
}
.checkbox-label {
    color: var(--vapor-color-foreground-normal-100, #2b2d36);

    font-size: var(--vapor-typography-fontSize-075, 0.875rem);
    font-weight: var(--vapor-typography-fontWeight-400);
    line-height: var(--vapor-typography-lineHeight-075, 1.375rem); /* 157.143% */
    letter-spacing: var(--vapor-typography-letterSpacing-100, -0.00625rem);
}

.popup {
    border-top-left-radius: var(--vapor-size-borderRadius-300);
    border-top-right-radius: var(--vapor-size-borderRadius-300);
}

.tabs {
    height: 100%;
}

.tabs-list {
    padding-inline: 0;
}

.tabs-panel {
    padding-block: var(--vapor-size-space-200);
}

.header {
    padding: var(--vapor-size-space-250) var(--vapor-size-space-200) 0;
}

.body {
    padding-inline: var(--vapor-size-space-200);
}

.footer {
    gap: var(--vapor-size-space-100);
    padding-top: var(--vapor-size-space-100);
}

.refresh-button {
    flex-shrink: 0;
}

.apply-button {
    flex-shrink: 0;
    flex-grow: 1;
}