Combobox

Data Input

키워드를 입력해 결과를 빠르게 찾는 검색 입력.

Usage

선택지가 많아 검색으로 후보를 좁히거나 직접 입력과 선택을 함께 제공할 때 사용한다.

import

import

import { Combobox } from '@mildang/design-system/Combobox';

Combobox 하나만 가져오면 Combobox.Option · Combobox.OptionIcon · Combobox.OptionLabel · Combobox.OptionDescription · Combobox.Root · Combobox.Control · Combobox.Trigger · Combobox.Positioner · Combobox.Content · Combobox.ContentScrollWrapper · Combobox.ItemGroup · Combobox.ItemGroupLabel · Combobox.Separator · Combobox.Item · Combobox.ItemIndicator · Combobox.Empty 를 그 아래에서 쓸 수 있다.

예제를 복사해 쓸 때 필요한 준비

@mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.

외부 패키지를 따로 설치한다. DS 의 전이 의존성에 기대지 않는다.

Anatomy

tsx

import { Combobox } from '@mildang/design-system/Combobox';

export default function Example() {
  return (
    <Combobox>
      <Combobox.Control>
        <Combobox.Trigger />
      </Combobox.Control>
      <Combobox.Content>
        <Combobox.ItemGroup>
          <Combobox.Item />
        </Combobox.ItemGroup>
        <Combobox.Empty />
      </Combobox.Content>
    </Combobox>
  );
}

부품

필수 여부

반복

위치

Combobox.Root

필수

Combobox.Control

필수

Root 안에 둔다.

Combobox.Trigger

선택

Control 안에 둔다.

Combobox.Content

필수

Root 안에 둔다.

Combobox.ItemGroup

선택

여러 개 가능

Content 안에 둔다.

Combobox.Item

필수

여러 개 가능

Content 또는 ItemGroup 안에 둔다.

Combobox.Empty

선택

Content 안에 둔다.

  • Root 안에 Input과 Content를 구성하고, Content 안에 선택 항목을 반복 배치한다.

API Reference

Combobox Props

Prop

Type

Default

className

string

지정 안 함

disabled

boolean & import("@mildang/styled-system/types").ConditionalValue<boolean>

false

error

boolean

지정 안 함

fullWidth

boolean

지정 안 함

ghost

boolean

지정 안 함

size

"sm" | "md" | "lg" | "xl"

md

success

boolean

지정 안 함

warning

boolean

지정 안 함

Combobox.Control

입력과 트리거를 감싸는 컨트롤 영역이다.

공개 Props 없음

Combobox.Trigger

옵션 목록을 여닫는 트리거다.

공개 Props 없음

Combobox.Content

검색 결과 목록을 표시하는 콘텐츠다.

공개 Props 없음

Combobox.ItemGroup

검색 결과를 그룹으로 묶는다.

공개 Props 없음

Combobox.Item

선택 가능한 검색 결과 항목이다.

공개 Props 없음

Combobox.Empty

검색 결과가 없을 때 표시한다.

공개 Props 없음

같은 패밀리

@mildang/design-system/Combobox 에서 같이 내보내는 컴포넌트다.

옵션 콘텐츠 구성과 상태

Combobox.Option(ComboboxOptionComponent)은 Menu·Select 와 같은 content_type 을 지원한다. Menu 는 정적, Combobox 는 검색+필터가 붙는 차이만 있고 아이템 모양은 통일된다.

Figma content_type사용 prop
text_onlychildren
icon_leftstartAdornment (아이콘)
with_avatarstartAdornment (Avatar 컴포넌트)
with_avatar_captionstartAdornment + caption
bottom_descriptiondescription (기본, 라벨 아래)
right_descriptiondescription + descriptionPosition: 'right'
chipchildren 슬롯에 Chip 컴포넌트 삽입

caption 이 배열이면 항목 사이에 divider(2px 원형)가 자동 삽입된다.

옵션 상태는 두 갈래다.

  • selected — Ark Combobox 컨텍스트가 현재 선택값 기준으로 자동 반영한다.
  • active — prop 으로 직접 켠다. 현재 컨텍스트(예: 로그인한 사용자, 열린 채팅방)를 강조하는 지속 상태이고 CSS :active 와는 무관하다.

Select 와 고르는 기준은 검색 가능 여부가 아니다 — Select 문서의 select-vs-combobox 섹션을 참고한다.

콘텐츠 타입

아이콘·아바타+caption·description(아래/오른쪽)·active 를 한 목록에 섞습니다 — Figma content_type 이 옵션 prop 으로 대응되는 모습입니다.

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';
import { renderChipTag } from '@mildang/design-system/Combobox';
import { Chip } from '@mildang/design-system/Chip';
import { type ChipVariantProps } from '@mildang/styled-system/recipes';

// ============================================================================
// Chip 옵션 스토리
// ============================================================================

type ChipItem = {
  label: string;
  value: string;
  color: NonNullable<ChipVariantProps['color']>;
};

const chipStatusItems: ChipItem[] = [
  { label: '완료', value: 'done', color: 'green' },
  { label: '진행 중', value: 'in_progress', color: 'blue' },
  { label: '대기', value: 'pending', color: 'orange' },
  { label: '오류', value: 'error', color: 'red' },
];

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '상태를 선택하세요',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxChipOptionsWithHelperExampleRender = (args: ComponentProps<typeof Combobox>) => {
    const { contains } = useFilter({ sensitivity: 'base' });
    const { collection, filter } = useListCollection({
      initialItems: chipStatusItems,
      itemToString: (item: ChipItem) => item.label,
      itemToValue: (item: ChipItem) => item.value,
      filter: (_itemText: string, filterText: string, item: ChipItem) => contains(item.label, filterText),
    });

    return (
      <Combobox
        {...args}
        multiple
        collection={collection}
        filter={filter}
        // items가 { label, color } 규약을 따르면 renderChipTag helper로 한 줄 처리
        renderTag={renderChipTag}
      >
        {(collection.items || []).map((item: ChipItem) => (
          <Combobox.Option key={item.value} item={item.value}>
            <Chip label={item.label} color={item.color} type="text" size="sm" />
          </Combobox.Option>
        ))}
      </Combobox>
    );
  };

export default function ComboboxChipOptionsWithHelperExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxChipOptionsWithHelperExampleRender(mergedProps);
}

선택 값을 Chip 으로 표시

multiple 모드에서 선택된 값을 입력창(트리거)에 chip 으로 보여주려면 renderTagrenderChipTag 를 넘긴다. item 이 { label, color } 규약을 따르면 자동으로 Chip 으로 렌더한다.

  • renderTag — 트리거의 선택된 값 렌더. menu 안 아이템은 Combobox.Option children 으로 Chip 을 직접 넣는다(Figma content_type=chip). 두 곳의 label/color 가 자동으로 일치한다.
  • item 규약이 다르거나 커스텀 렌더가 필요하면 renderTag 에 직접 함수를 넘기면 된다.

단일 모드(multiple=false)는 트리거가 Input 이라 chip 을 렌더할 자리가 없다. Select 처럼 트리거에 chip 하나만 노출하려면 singleChip 을 쓴다 — 내부적으로 multiple + max=1 + _closeOnSelect 로 위임되어 TagsInput 이 chip 1개만 허용하는 형태로 렌더된다.

Select 의 renderChipValues 와 대칭 API다 — 어느 쪽이든 같은 { label, color } 규약만 지키면 동일하게 쓴다. colorbrand/neutral/green/orange/blue/red 만 유효하다.

값을 Chip 으로 표시

multiple + renderTag={renderChipTag} 로 트리거와 옵션 모두 chip 으로, singleChip 으로 단일 선택도 chip 하나로 표시합니다.

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';
import { Chip } from '@mildang/design-system/Chip';
import { type ChipVariantProps } from '@mildang/styled-system/recipes';

// ============================================================================
// Chip 옵션 스토리
// ============================================================================

type ChipItem = {
  label: string;
  value: string;
  color: NonNullable<ChipVariantProps['color']>;
};

const chipStatusItems: ChipItem[] = [
  { label: '완료', value: 'done', color: 'green' },
  { label: '진행 중', value: 'in_progress', color: 'blue' },
  { label: '대기', value: 'pending', color: 'orange' },
  { label: '오류', value: 'error', color: 'red' },
];

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '상태를 선택하세요',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxChipOptionsExampleRender = (args: ComponentProps<typeof Combobox>) => {
    const { contains } = useFilter({ sensitivity: 'base' });
    const { collection, filter } = useListCollection({
      initialItems: chipStatusItems,
      itemToString: (item: ChipItem) => item.label,
      itemToValue: (item: ChipItem) => item.value,
      filter: (_itemText: string, filterText: string, item: ChipItem) => contains(item.label, filterText),
    });

    return (
      <Combobox
        {...args}
        multiple
        collection={collection}
        filter={filter}
        // 트리거 chip 렌더: item.label / item.color 그대로 사용
        renderTag={({ item, onDelete }) =>
          item ? (
            <Chip label={item.label} color={item.color} onDelete={onDelete} type="text" size="sm" />
          ) : null
        }
      >
        {(collection.items || []).map((item: ChipItem) => (
          <Combobox.Option key={item.value} item={item.value}>
            <Chip label={item.label} color={item.color} type="text" size="sm" />
          </Combobox.Option>
        ))}
      </Combobox>
    );
  };

export default function ComboboxChipOptionsExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxChipOptionsExampleRender(mergedProps);
}

예제

기본 사용

useListCollection 과 useFilter 로 검색되는 목록을 만든 최소 형태 — 옵션이 많아 타이핑으로 좁혀야 할 때 씁니다.

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const sampleItems: StoryItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte' },
  { label: 'Next.js', value: 'nextjs', optionlabel: 'Next.js' },
  { label: 'Nuxt.js', value: 'nuxtjs', optionlabel: 'Nuxt.js' },
  { label: 'SvelteKit', value: 'sveltekit', optionlabel: 'SvelteKit' },
  { label: 'Remix', value: 'remix', optionlabel: 'Remix' },
  { label: 'Astro', value: 'astro', optionlabel: 'Astro' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid' },
];

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '프레임워크를 검색하세요',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxDefaultExampleRender = (args: ComponentProps<typeof Combobox>) => {
    const { contains } = useFilter({ sensitivity: 'base' });
    const { collection, filter } = useListCollection({
      initialItems: sampleItems,
      itemToString: (item: StoryItem) => item.label,
      itemToValue: (item: StoryItem) => item.value,
      filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
    });

    return (
      <Combobox {...args} collection={collection} filter={filter}>
        {(collection.items || []).map((item: StoryItem) => {
          return (
            <Combobox.Option key={item.value} item={item.value}>
              <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
              {item.description && (
                <Combobox.OptionDescription>{item.description}</Combobox.OptionDescription>
              )}
            </Combobox.Option>
          );
        })}
      </Combobox>
    );
  };

export default function ComboboxDefaultExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxDefaultExampleRender(mergedProps);
}

열린 상태

입력과 그 아래 열린 옵션 목록을 함께 고정해 보여줍니다 — 닫힌 입력만으로는 검색해서 고르는 물건임이 드러나지 않습니다.

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useListCollection } from '@ark-ui/react';

// EmptyItems 스토리를 위한 컴포넌트 (hook 사용을 위해)
const EmptyItemsCombobox = (args: Partial<ComboboxProps<string>>) => {
  const { collection } = useListCollection<string>({
    initialItems: [],
    itemToString: () => '',
    itemToValue: () => '',
  });

  return <Combobox {...args} collection={collection} placeholder="프레임워크를 검색하세요" />;
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '프레임워크를 검색하세요',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxEmptyItemsExampleRender = (args: ComponentProps<typeof Combobox>) => <EmptyItemsCombobox {...args} />;

export default function ComboboxEmptyItemsExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxEmptyItemsExampleRender(mergedProps);
}

크기

sm·md·lg·xl 의 입력 높이를 비교합니다. 같은 폼 안의 다른 입력과 같은 size 로 맞춥니다.

코드

import { useFilter, useListCollection } from '@ark-ui/react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { renderChipTag } from '@mildang/design-system/Combobox';
import { Chip } from '@mildang/design-system/Chip';
import React from 'react';
import { ChipVariantProps } from '@mildang/styled-system/recipes';

type SizesChipItem = {
  label: string;
  value: string;
  optionlabel: string;
  color: NonNullable<ChipVariantProps['color']>;
};

// Size 매트릭스 스토리 전용 — Basic 은 label 텍스트, Chip 은 <Chip color> 로 렌더.
const sizesChipItems: SizesChipItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React', color: 'blue' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue', color: 'green' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte', color: 'orange' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular', color: 'red' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid', color: 'brand' },
];

const SIZES_ORDER = ['sm', 'md', 'lg', 'xl'] as const;

type SizesMatrixVariant = {
  label: string;
  render: (size: (typeof SIZES_ORDER)[number]) => React.ReactNode;
};

const SizesMatrixSection = ({ title, variants }: { title: string; variants: SizesMatrixVariant[] }) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
    <div style={{ fontSize: '14px', fontWeight: 700, color: '#1f2933' }}>{title}</div>
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: `80px repeat(${variants.length}, 320px)`,
        columnGap: '16px',
        rowGap: '12px',
        alignItems: 'center',
      }}
    >
      <div />
      {variants.map((variant) => (
        <div key={variant.label} style={{ fontSize: '12px', fontWeight: 500, color: '#4d5358' }}>
          {variant.label}
        </div>
      ))}
      {SIZES_ORDER.map((size) => (
        <React.Fragment key={size}>
          <div style={{ fontSize: '12px', color: '#878d96' }}>size={size}</div>
          {variants.map((variant) => (
            <div key={`${size}-${variant.label}`}>{variant.render(size)}</div>
          ))}
        </React.Fragment>
      ))}
    </div>
  </div>
);

const SizesMatrixCombobox = () => {
  const { contains } = useFilter({ sensitivity: 'base' });
  // Basic 과 Chip 섹션이 동일한 드롭다운 옵션을 사용하도록 sizesChipItems 를 공유.
  // Basic 은 label 텍스트만 렌더하고, Chip 은 <Chip label color /> 을 렌더.
  const shared = useListCollection({
    initialItems: sizesChipItems,
    itemToString: (item: SizesChipItem) => item.label,
    itemToValue: (item: SizesChipItem) => item.value,
    filter: (_itemText: string, filterText: string, item: SizesChipItem) => contains(item.label, filterText),
  });

  const basicVariants: SizesMatrixVariant[] = [
    {
      label: 'Single',
      render: (size) => (
        <Combobox
          collection={shared.collection}
          filter={shared.filter}
          size={size}
          placeholder={size}
          fullWidth
        >
          {(shared.collection.items || []).map((item: SizesChipItem) => (
            <Combobox.Option key={item.value} item={item.value}>
              <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
            </Combobox.Option>
          ))}
        </Combobox>
      ),
    },
    {
      label: 'Multiple',
      render: (size) => (
        <Combobox
          collection={shared.collection}
          filter={shared.filter}
          size={size}
          multiple
          showChip
          placeholder={size}
          fullWidth
        >
          {(shared.collection.items || []).map((item: SizesChipItem) => (
            <Combobox.Option key={item.value} item={item.value}>
              <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
            </Combobox.Option>
          ))}
        </Combobox>
      ),
    },
  ];

  // Chip 노출은 `renderTag` 로 통일. single/multiple 은 `singleChip` / `multiple` prop 으로 구분.
  // closeOnSelect 는 Combobox 가 모드에 맞는 기본값(single→true, multiple→false)을 자동 적용.
  const renderChipCombobox = (size: (typeof SIZES_ORDER)[number], mode: 'single' | 'multiple') => (
    <Combobox
      collection={shared.collection}
      filter={shared.filter}
      size={size}
      singleChip={mode === 'single'}
      multiple={mode === 'multiple'}
      placeholder={size}
      fullWidth
      renderTag={renderChipTag}
    >
      {(shared.collection.items || []).map((item: SizesChipItem) => (
        <Combobox.Option key={item.value} item={item.value}>
          <Chip label={item.label} color={item.color} type="text" size="sm" />
        </Combobox.Option>
      ))}
    </Combobox>
  );

  const chipVariants: SizesMatrixVariant[] = [
    { label: 'Chip · Single', render: (size) => renderChipCombobox(size, 'single') },
    { label: 'Chip · Multiple', render: (size) => renderChipCombobox(size, 'multiple') },
  ];

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
      <SizesMatrixSection title="Basic" variants={basicVariants} />
      <SizesMatrixSection title="Chip" variants={chipVariants} />
    </div>
  );
};

const ComboboxSizesExample = () => <SizesMatrixCombobox />;

export default ComboboxSizesExample;

LongLabel

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';

const STORY_DEFAULT_ARGS = { ...({}), ...({}) } as ComponentProps<typeof Combobox>;

const ComboboxLongLabelExampleRender = (args: ComponentProps<typeof Combobox>) => {
    const { contains } = useFilter({ sensitivity: 'base' });
    const { collection, filter } = useListCollection({
      initialItems: [
        { label: '짧은 옵션', value: 'short' },
        { label: '아주 길고 긴 옵션 라벨 텍스트 예시입니다 1234567890 abcdefg', value: 'long' },
        {
          label:
            '아주 아주 아주 아주 아주 아주 길고 긴 옵션 라벨 텍스트 예시입니다 1234567890 abcdefg 1234567890 abcdefg 1234567890 abcdefg',
          value: 'very_long',
        },
        { label: '중간 길이 옵션 라벨', value: 'medium' },
      ],
      itemToString: (item: { label: string; value: string }) => item.label,
      itemToValue: (item: { label: string; value: string }) => item.value,
      filter: (_t: string, f: string, item: { label: string; value: string }) => contains(item.label, f),
    });
    // 기본 너비(380px)를 그대로 보여준다. 긴 라벨은 2줄 clamp + ellipsis (드롭다운은 늘어나지 않음).
    return (
      <Combobox {...args} collection={collection} filter={filter} placeholder="검색">
        {(collection.items || []).map((item: { label: string; value: string }) => (
          <Combobox.Option key={item.value} item={item.value}>
            <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
          </Combobox.Option>
        ))}
      </Combobox>
    );
  };

export default function ComboboxLongLabelExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxLongLabelExampleRender(mergedProps);
}

State

코드

import { useFilter, useListCollection } from '@ark-ui/react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const sampleItems: StoryItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte' },
  { label: 'Next.js', value: 'nextjs', optionlabel: 'Next.js' },
  { label: 'Nuxt.js', value: 'nuxtjs', optionlabel: 'Nuxt.js' },
  { label: 'SvelteKit', value: 'sveltekit', optionlabel: 'SvelteKit' },
  { label: 'Remix', value: 'remix', optionlabel: 'Remix' },
  { label: 'Astro', value: 'astro', optionlabel: 'Astro' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid' },
];

const StateCombobox = ({
  state,
  multiple,
}: {
  state: 'error' | 'warning' | 'success' | 'ghost';
  multiple?: boolean;
}) => {
  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter } = useListCollection({
    initialItems: sampleItems,
    itemToString: (item: StoryItem) => item.label,
    itemToValue: (item: StoryItem) => item.value,
    filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
  });
  return (
    <Combobox
      collection={collection}
      filter={filter}
      multiple={multiple}
      showChip
      placeholder={state}
      fullWidth
      error={state === 'error'}
      warning={state === 'warning'}
      success={state === 'success'}
      ghost={state === 'ghost'}
    >
      {(collection.items || []).map((item: StoryItem) => (
        <Combobox.Option key={item.value} item={item.value}>
          <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
        </Combobox.Option>
      ))}
    </Combobox>
  );
};

const ComboboxStateExample = () => (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '320px' }}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
        <div style={{ fontSize: '14px', fontWeight: 700, color: '#1f2933' }}>Single</div>
        <StateCombobox state="error" />
        <StateCombobox state="warning" />
        <StateCombobox state="success" />
        <StateCombobox state="ghost" />
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
        <div style={{ fontSize: '14px', fontWeight: 700, color: '#1f2933' }}>Multiple</div>
        <StateCombobox state="error" multiple />
        <StateCombobox state="warning" multiple />
        <StateCombobox state="success" multiple />
        <StateCombobox state="ghost" multiple />
      </div>
    </div>
  );

export default ComboboxStateExample;

LongList

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const sampleItems: StoryItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte' },
  { label: 'Next.js', value: 'nextjs', optionlabel: 'Next.js' },
  { label: 'Nuxt.js', value: 'nuxtjs', optionlabel: 'Nuxt.js' },
  { label: 'SvelteKit', value: 'sveltekit', optionlabel: 'SvelteKit' },
  { label: 'Remix', value: 'remix', optionlabel: 'Remix' },
  { label: 'Astro', value: 'astro', optionlabel: 'Astro' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid' },
];

const longListItems: StoryItem[] = [
  ...sampleItems,
  { label: 'Qwik', value: 'qwik', optionlabel: 'Qwik' },
  { label: 'Lit', value: 'lit', optionlabel: 'Lit' },
  { label: 'Stencil', value: 'stencil', optionlabel: 'Stencil' },
  { label: 'Mithril', value: 'mithril', optionlabel: 'Mithril' },
  { label: 'Preact', value: 'preact', optionlabel: 'Preact' },
  { label: 'Inferno', value: 'inferno', optionlabel: 'Inferno' },
  { label: 'Hyperapp', value: 'hyperapp', optionlabel: 'Hyperapp' },
  { label: 'Alpine.js', value: 'alpinejs', optionlabel: 'Alpine.js' },
  { label: 'Stimulus', value: 'stimulus', optionlabel: 'Stimulus' },
  { label: 'HTMX', value: 'htmx', optionlabel: 'HTMX' },
];

// LongList 스토리를 위한 컴포넌트 (hook 사용을 위해)
const LongListCombobox = (args: Partial<ComboboxProps<StoryItem>>) => {
  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter } = useListCollection({
    initialItems: longListItems,
    itemToString: (item: StoryItem) => item.label,
    itemToValue: (item: StoryItem) => item.value,
    filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
  });

  return (
    <Combobox {...args} collection={collection} filter={filter} placeholder="프레임워크를 검색하세요">
      <Combobox.ContentScrollWrapper maxHeight="300px">
        {(collection.items || []).map((item: StoryItem) => {
          return (
            <Combobox.Option key={item.value} item={item.value}>
              <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
              {item.description && (
                <Combobox.OptionDescription>{item.description}</Combobox.OptionDescription>
              )}
            </Combobox.Option>
          );
        })}
      </Combobox.ContentScrollWrapper>
    </Combobox>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '프레임워크를 검색하세요',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxLongListExampleRender = (args: ComponentProps<typeof Combobox>) => <LongListCombobox {...args} />;

export default function ComboboxLongListExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxLongListExampleRender(mergedProps);
}

LongTextList

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const longTextListItems: StoryItem[] = [
  {
    label: 'React - A JavaScript library for building user interfaces',
    value: 'react',
    optionlabel: 'React - A JavaScript library for building user interfaces',
  },
  {
    label: 'Vue.js - The Progressive JavaScript Framework',
    value: 'vue',
    optionlabel: 'Vue.js - The Progressive JavaScript Framework',
  },
  {
    label: "Angular - The modern web developer's platform",
    value: 'angular',
    optionlabel: "Angular - The modern web developer's platform",
  },
  {
    label: 'Svelte - Cybernetically enhanced web apps',
    value: 'svelte',
    optionlabel: 'Svelte - Cybernetically enhanced web apps',
  },
  {
    label: 'Next.js - The React Framework for Production',
    value: 'nextjs',
    optionlabel: 'Next.js - The React Framework for Production',
  },
  {
    label: 'Nuxt.js - The Intuitive Vue Framework',
    value: 'nuxtjs',
    optionlabel: 'Nuxt.js - The Intuitive Vue Framework',
  },
  {
    label: 'SvelteKit - The fastest way to build Svelte apps',
    value: 'sveltekit',
    optionlabel: 'SvelteKit - The fastest way to build Svelte apps',
  },
  {
    label: 'Remix - Build better websites with React',
    value: 'remix',
    optionlabel: 'Remix - Build better websites with React',
  },
  {
    label: 'Astro - Build faster websites with less client-side JavaScript',
    value: 'astro',
    optionlabel: 'Astro - Build faster websites with less client-side JavaScript',
  },
  {
    label: 'Solid - A declarative JavaScript library for creating user interfaces',
    value: 'solid',
    optionlabel: 'Solid - A declarative JavaScript library for creating user interfaces',
  },
];

// LongTextList 스토리를 위한 컴포넌트 (hook 사용을 위해)
const LongTextListCombobox = (args: Partial<ComboboxProps<StoryItem>>) => {
  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter } = useListCollection({
    initialItems: longTextListItems,
    itemToString: (item: StoryItem) => item.label,
    itemToValue: (item: StoryItem) => item.value,
    filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
  });

  return (
    <Combobox {...args} collection={collection} filter={filter} width="300px">
      {(collection.items || []).map((item: StoryItem) => {
        return (
          <Combobox.Option key={item.value} item={item.value}>
            <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
            {item.description && <Combobox.OptionDescription>{item.description}</Combobox.OptionDescription>}
          </Combobox.Option>
        );
      })}
    </Combobox>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '프레임워크를 검색하세요',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxLongTextListExampleRender = (args: ComponentProps<typeof Combobox>) => <LongTextListCombobox {...args} />;

export default function ComboboxLongTextListExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxLongTextListExampleRender(mergedProps);
}

WithGroupBy

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

interface FrameworkOption extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  category: string;
  stars: number;
  optionlabel: string;
}

const frameworkOptions: FrameworkOption[] = [
  {
    label: 'React',
    value: 'react',
    description: 'Frontend',
    category: 'Frontend',
    stars: 220000,
    optionlabel: 'React',
  },
  {
    label: 'Vue',
    value: 'vue',
    description: 'Frontend',
    category: 'Frontend',
    stars: 210000,
    optionlabel: 'Vue',
  },
  {
    label: 'Angular',
    value: 'angular',
    description: 'Frontend',
    category: 'Frontend',
    stars: 90000,
    optionlabel: 'Angular',
  },
  {
    label: 'Next.js',
    value: 'nextjs',
    description: 'Fullstack',
    category: 'Fullstack',
    stars: 120000,
    optionlabel: 'Next.js',
  },
  {
    label: 'Nuxt.js',
    value: 'nuxtjs',
    description: 'Fullstack',
    category: 'Fullstack',
    stars: 50000,
    optionlabel: 'Nuxt.js',
  },
  {
    label: 'Express',
    value: 'express',
    description: 'Backend',
    category: 'Backend',
    stars: 65000,
    optionlabel: 'Express',
  },
  {
    label: 'NestJS',
    value: 'nestjs',
    description: 'Backend',
    category: 'Backend',
    stars: 68000,
    optionlabel: 'NestJS',
  },
  {
    label: 'Svelte',
    value: 'svelte',
    description: 'Frontend',
    category: 'Frontend',
    stars: 75000,
    optionlabel: 'Svelte',
  },
  {
    label: 'Remix',
    value: 'remix',
    description: 'Fullstack',
    category: 'Fullstack',
    stars: 28000,
    optionlabel: 'Remix',
  },
];

// ============================================================================
// GroupBy 스토리
// ============================================================================

// GroupBy를 사용하는 스토리
const GroupByCombobox = (args: Partial<ComboboxProps<FrameworkOption>>) => {
  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter } = useListCollection({
    initialItems: frameworkOptions,
    itemToString: (item: FrameworkOption) => item.label,
    itemToValue: (item: FrameworkOption) => item.value,
    filter: (_itemText: string, filterText: string, item: FrameworkOption) =>
      contains(item.label, filterText),
    groupBy: (item: FrameworkOption) => item.category,
  });

  // 카테고리별로 그룹화 (필터링된 collection.items 사용)
  const groupedByCategory = (collection.items || []).reduce(
    (acc: Record<string, FrameworkOption[]>, item: FrameworkOption) => {
      const category = item.category;
      if (!acc[category]) {
        acc[category] = [];
      }
      acc[category].push(item);
      return acc;
    },
    {} as Record<string, FrameworkOption[]>,
  );

  return (
    <Combobox collection={collection} filter={filter} {...args}>
      {Object.entries(groupedByCategory).map(([category, items], index, entries) => (
        <Combobox.ItemGroup key={category}>
          <Combobox.ItemGroupLabel>{category}</Combobox.ItemGroupLabel>
          {items.map((item: FrameworkOption) => (
            <Combobox.Option key={item.value} item={item.value}>
              <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
              {item.description && (
                <Combobox.OptionDescription>{item.description}</Combobox.OptionDescription>
              )}
            </Combobox.Option>
          ))}
          {index < entries.length - 1 && <Combobox.Separator />}
        </Combobox.ItemGroup>
      ))}
    </Combobox>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '카테고리별로 그룹화된 프레임워크',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxWithGroupByExampleRender = (args: ComponentProps<typeof Combobox>) => <GroupByCombobox {...args} />;

export default function ComboboxWithGroupByExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxWithGroupByExampleRender(mergedProps);
}

WithoutSearchIcon

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const sampleItems: StoryItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte' },
  { label: 'Next.js', value: 'nextjs', optionlabel: 'Next.js' },
  { label: 'Nuxt.js', value: 'nuxtjs', optionlabel: 'Nuxt.js' },
  { label: 'SvelteKit', value: 'sveltekit', optionlabel: 'SvelteKit' },
  { label: 'Remix', value: 'remix', optionlabel: 'Remix' },
  { label: 'Astro', value: 'astro', optionlabel: 'Astro' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid' },
];

// ============================================================================
// 검색 아이콘 없는 Combobox 스토리
// ============================================================================

const WithoutSearchIconCombobox = (args: Partial<ComboboxProps<StoryItem>>) => {
  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter } = useListCollection({
    initialItems: sampleItems,
    itemToString: (item: StoryItem) => item.label,
    itemToValue: (item: StoryItem) => item.value,
    filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
  });

  return (
    <Combobox
      {...args}
      collection={collection}
      filter={filter}
      placeholder="검색 아이콘 없이 표시됩니다"
      InputProps={{ startAdornment: null }}
    >
      {(collection.items || []).map((item: StoryItem) => {
        return (
          <Combobox.Option key={item.value} item={item.value}>
            <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
            {item.description && <Combobox.OptionDescription>{item.description}</Combobox.OptionDescription>}
          </Combobox.Option>
        );
      })}
    </Combobox>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '검색 아이콘 없이 표시됩니다',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxWithoutSearchIconExampleRender = (args: ComponentProps<typeof Combobox>) => <WithoutSearchIconCombobox {...args} />;

export default function ComboboxWithoutSearchIconExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxWithoutSearchIconExampleRender(mergedProps);
}

HideClearButton

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const sampleItems: StoryItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte' },
  { label: 'Next.js', value: 'nextjs', optionlabel: 'Next.js' },
  { label: 'Nuxt.js', value: 'nuxtjs', optionlabel: 'Nuxt.js' },
  { label: 'SvelteKit', value: 'sveltekit', optionlabel: 'SvelteKit' },
  { label: 'Remix', value: 'remix', optionlabel: 'Remix' },
  { label: 'Astro', value: 'astro', optionlabel: 'Astro' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid' },
];

const HideClearButtonCombobox = (args: Partial<ComboboxProps<StoryItem>>) => {
  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter } = useListCollection({
    initialItems: sampleItems,
    itemToString: (item: StoryItem) => item.label,
    itemToValue: (item: StoryItem) => item.value,
    filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
  });

  return (
    <Combobox {...args} collection={collection} filter={filter} hideClearButton>
      {(collection.items || []).map((item: StoryItem) => {
        return (
          <Combobox.Option key={item.value} item={item.value}>
            <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
            {item.description && <Combobox.OptionDescription>{item.description}</Combobox.OptionDescription>}
          </Combobox.Option>
        );
      })}
    </Combobox>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '값을 선택해도 X 버튼이 표시되지 않습니다',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxHideClearButtonExampleRender = (args: ComponentProps<typeof Combobox>) => <HideClearButtonCombobox {...args} />;

export default function ComboboxHideClearButtonExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxHideClearButtonExampleRender(mergedProps);
}

NumberInput

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';
import React from 'react';

// ============================================================================
// Number Input 스토리
// ============================================================================

const TEXT_SIZES = ['40', '32', '28', '24', '20', '18', '16'];

const NumberInputCombobox = (args: Partial<ComboboxProps<string>>) => {
  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter } = useListCollection({
    initialItems: TEXT_SIZES,
    itemToString: (item: string) => item,
    itemToValue: (item: string) => item,
    filter: (_itemText: string, filterText: string, item: string) => contains(item, filterText),
  });

  const [inputValue, setInputValue] = React.useState('18');

  const handleInputChange = (details: { inputValue: string }) => {
    setInputValue(details.inputValue);
  };

  return (
    <Combobox
      {...args}
      collection={collection}
      filter={filter}
      defaultInputValue={inputValue}
      onInputChange={handleInputChange}
      allowCustomValue
      closeOnSelect={false}
      width="86px"
      InputProps={{
        type: 'number',
        startAdornment: null,
        endAdornment: null,
      }}
    >
      {(collection.items || []).map((item: string) => {
        return (
          <Combobox.Option key={item} item={item}>
            <Combobox.OptionLabel>{item}</Combobox.OptionLabel>
          </Combobox.Option>
        );
      })}
    </Combobox>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxNumberInputExampleRender = (args: ComponentProps<typeof Combobox>) => <NumberInputCombobox {...args} />;

export default function ComboboxNumberInputExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxNumberInputExampleRender(mergedProps);
}

SearchIconEnd

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const sampleItems: StoryItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte' },
  { label: 'Next.js', value: 'nextjs', optionlabel: 'Next.js' },
  { label: 'Nuxt.js', value: 'nuxtjs', optionlabel: 'Nuxt.js' },
  { label: 'SvelteKit', value: 'sveltekit', optionlabel: 'SvelteKit' },
  { label: 'Remix', value: 'remix', optionlabel: 'Remix' },
  { label: 'Astro', value: 'astro', optionlabel: 'Astro' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid' },
];

// ============================================================================
// SearchIconEnd 스토리
// ============================================================================

const SearchIconEndCombobox = (args: Partial<ComboboxProps<StoryItem>>) => {
  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter } = useListCollection({
    initialItems: sampleItems,
    itemToString: (item: StoryItem) => item.label,
    itemToValue: (item: StoryItem) => item.value,
    filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
  });

  return (
    <Combobox
      {...args}
      collection={collection}
      filter={filter}
      searchIconPosition="end"
      placeholder="검색어를 입력하세요"
    >
      {(collection.items || []).map((item: StoryItem) => (
        <Combobox.Option key={item.value} item={item.value}>
          <Combobox.OptionLabel>{item.label}</Combobox.OptionLabel>
        </Combobox.Option>
      ))}
    </Combobox>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '검색어를 입력하세요',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxSearchIconEndExampleRender = (args: ComponentProps<typeof Combobox>) => <SearchIconEndCombobox {...args} />;

export default function ComboboxSearchIconEndExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxSearchIconEndExampleRender(mergedProps);
}

[코드] Highlight

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const sampleItems: StoryItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte' },
  { label: 'Next.js', value: 'nextjs', optionlabel: 'Next.js' },
  { label: 'Nuxt.js', value: 'nuxtjs', optionlabel: 'Nuxt.js' },
  { label: 'SvelteKit', value: 'sveltekit', optionlabel: 'SvelteKit' },
  { label: 'Remix', value: 'remix', optionlabel: 'Remix' },
  { label: 'Astro', value: 'astro', optionlabel: 'Astro' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid' },
];

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '하이라이트 테스트',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxHighlightExampleRender = (args: ComponentProps<typeof Combobox>) => {
    const { contains } = useFilter({ sensitivity: 'base' });

    const { collection, filter } = useListCollection({
      initialItems: sampleItems,
      itemToString: (item: StoryItem) => item.label,
      itemToValue: (item: StoryItem) => item.value,
      filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
    });

    return (
      <Combobox {...args} collection={collection} filter={filter}>
        {(collection.items || []).map((item: StoryItem) => {
          return (
            <Combobox.Option key={item.value} item={item.value}>
              <Combobox.OptionLabel highlight>{item.label}</Combobox.OptionLabel>

              {item.description && (
                <Combobox.OptionDescription>{item.description}</Combobox.OptionDescription>
              )}
            </Combobox.Option>
          );
        })}
      </Combobox>
    );
  };

export default function ComboboxHighlightExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxHighlightExampleRender(mergedProps);
}

ChipOptionsSingle

코드

import type { ComponentProps } from 'react';
import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';
import { renderChipTag } from '@mildang/design-system/Combobox';
import { Chip } from '@mildang/design-system/Chip';
import { type ChipVariantProps } from '@mildang/styled-system/recipes';

// ============================================================================
// Chip 옵션 스토리
// ============================================================================

type ChipItem = {
  label: string;
  value: string;
  color: NonNullable<ChipVariantProps['color']>;
};

const chipStatusItems: ChipItem[] = [
  { label: '완료', value: 'done', color: 'green' },
  { label: '진행 중', value: 'in_progress', color: 'blue' },
  { label: '대기', value: 'pending', color: 'orange' },
  { label: '오류', value: 'error', color: 'red' },
];

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '상태를 선택하세요',
  }) } as ComponentProps<typeof Combobox>;

const ComboboxChipOptionsSingleExampleRender = (args: ComponentProps<typeof Combobox>) => {
    const { contains } = useFilter({ sensitivity: 'base' });
    const { collection, filter } = useListCollection({
      initialItems: chipStatusItems,
      itemToString: (item: ChipItem) => item.label,
      itemToValue: (item: ChipItem) => item.value,
      filter: (_itemText: string, filterText: string, item: ChipItem) => contains(item.label, filterText),
    });

    return (
      // Combobox 단일 모드는 트리거가 <Input> 이라 chip 을 렌더할 자리가 없다 (텍스트만).
      // 트리거에 chip 하나를 노출하려면 `singleChip` prop 을 켠다 — 내부적으로
      // multiple + max=1 + closeOnSelect 로 위임되어 TagsInput 이 chip 1 개만 허용.
      <Combobox {...args} collection={collection} filter={filter} singleChip renderTag={renderChipTag}>
        {(collection.items || []).map((item: ChipItem) => (
          <Combobox.Option key={item.value} item={item.value}>
            <Chip label={item.label} color={item.color} type="text" size="sm" />
          </Combobox.Option>
        ))}
      </Combobox>
    );
  };

export default function ComboboxChipOptionsSingleExample(props: Partial<ComponentProps<typeof Combobox>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Combobox>;
  return ComboboxChipOptionsSingleExampleRender(mergedProps);
}

AddOption

코드


import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';
import { useCallback, useMemo, useState } from 'react';
import { flushSync } from 'react-dom';
import { Combobox as ArkCombobox } from '@ark-ui/react/combobox';

export interface UseCreateOptionComboboxOptions<T> {
  /**
   * 초기 아이템 목록
   */
  initialItems: T[];
  /**
   * 아이템을 문자열로 변환하는 함수
   */
  itemToString: (item: T) => string;
  /**
   * 아이템을 값으로 변환하는 함수
   */
  itemToValue: (item: T) => string;
  /**
   * 필터 함수 (선택사항)
   * 제공하지 않으면 기본 필터링 로직 사용
   */
  filter?: (itemText: string, filterText: string, item: T) => boolean;
  /**
   * 새 아이템 생성 함수 (임시 옵션용, 선택사항)
   * 제공하지 않으면 기본 생성 로직 사용
   */
  createNewOption?: (value: string, newOptionValue: string) => T;
  /**
   * 새 아이템 데이터 생성 함수 (실제 추가용, 선택사항)
   * 제공하지 않으면 기본 생성 로직 사용
   */
  getNewOptionData?: (value: string) => T;
}

export interface UseCreateOptionComboboxReturn<T> {
  /**
   * Collection 객체
   */
  collection: ReturnType<typeof useListCollection<T>>['collection'];
  /**
   * 필터 함수
   */
  filter: ReturnType<typeof useListCollection<T>>['filter'];
  /**
   * 선택된 값들
   */
  selectedValues: string[];
  /**
   * 현재 입력값
   */
  inputValue: string;
  /**
   * Input 변경 핸들러
   */
  handleInputChange: (details: ArkCombobox.InputValueChangeDetails) => void;
  /**
   * 선택 변경 핸들러
   */
  handleValueChange: (details: { value: (string | number)[] }) => void;
  /**
   * 새 옵션 값인지 확인하는 함수
   */
  isNewOptionValue: (value: string) => boolean;
  /**
   * 새 옵션 값 상수
   */
  NEW_OPTION_VALUE: string;
  /**
   * 옵션 삭제 핸들러
   */
  handleDeleteOption: (item: T) => void;
  /**
   * 아이템 삭제 가능 여부 판단 함수
   */
  canDeleteItem: (item: T) => boolean;
}

/**
 * Combobox에서 "추가하기" 옵션 기능을 사용하기 위한 커스텀 훅
 * 입력값에 따라 "{keyword}" 추가하기 옵션을 동적으로 표시하고 추가할 수 있게 해줍니다.
 *
 * @example
 * ```tsx
 * const {
 *   collection,
 *   filter,
 *   selectedValues,
 *   inputValue,
 *   handleInputChange,
 *   handleValueChange,
 *   isNewOptionValue,
 *   NEW_OPTION_VALUE,
 * } = useCreateOptionCombobox({
 *   initialItems: items,
 *   itemToString: (item) => item.label,
 *   itemToValue: (item) => item.value,
 *   createNewOption: (value) => ({ label: value, value: NEW_OPTION_VALUE }),
 *   getNewOptionData: (value) => ({ label: value, value, __new__: true }),
 * });
 * ```
 */
export function useCreateOptionCombobox<T extends { value: string }>(
  options: UseCreateOptionComboboxOptions<T>,
): UseCreateOptionComboboxReturn<T> {
  const {
    initialItems,
    itemToString,
    itemToValue,
    filter: customFilter,
    createNewOption: customCreateNewOption,
    getNewOptionData: customGetNewOptionData,
  } = options;

  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter, upsert, remove, update } = useListCollection({
    initialItems,
    itemToString,
    itemToValue,
    filter:
      customFilter ||
      ((_itemText: string, filterText: string, item: T) => {
        // 검색어가 없으면 전체 목록 표시
        if (!filterText || filterText.trim() === '') {
          return true;
        }
        // 검색어가 있으면 필터링 적용
        return contains(itemToString(item), filterText);
      }),
  });

  // 초기 아이템들의 value를 Set으로 저장 (기본 옵션 추적 => 추가된 아이템만 삭제가능)
  const initialItemValues = useMemo(
    () => new Set(initialItems.map((item) => itemToValue(item))),
    [initialItems, itemToValue],
  );

  const [selectedValues, setSelectedValues] = useState<string[]>([]);
  const [inputValue, setInputValue] = useState('');

  console.log(inputValue, 'inputValue');

  const NEW_OPTION_VALUE = '[[new]]';

  const isNewOptionValue = useCallback((value: string) => value === NEW_OPTION_VALUE, []);

  const replaceNewOptionValue = useCallback(
    (values: string[], value: string) => values.map((v) => (v === NEW_OPTION_VALUE ? value : v)),
    [],
  );

  // 기본 새 옵션 생성 함수 (임시 옵션용)
  const createNewOption = useCallback(
    (value: string, newOptionValue: string): T => {
      if (customCreateNewOption) {
        return customCreateNewOption(value, newOptionValue);
      }
      // 기본 구현: value를 label로 사용하고, newOptionValue를 value로 설정
      // optionlabel도 label과 동일하게 설정 (일반적인 패턴)
      const label = value;
      return { value: newOptionValue, label, optionlabel: label } as unknown as T;
    },
    [customCreateNewOption],
  );

  // 기본 새 옵션 데이터 생성 함수 (실제 추가용)
  const getNewOptionData = useCallback(
    (value: string): T => {
      if (customGetNewOptionData) {
        return customGetNewOptionData(value);
      }
      // 기본 구현: value를 그대로 사용하고, label과 optionlabel도 value로 설정
      // __new__ 속성도 추가하여 새로 추가된 아이템임을 표시
      return { value, label: value, optionlabel: value, __new__: true } as unknown as T;
    },
    [customGetNewOptionData],
  );

  const isValidNewOption = useCallback(
    (inputValue: string) => {
      if (!inputValue.trim()) {
        return false;
      }
      const exactOptionMatch = (collection.items || []).some(
        (item: T) => itemToString(item).toLowerCase() === inputValue.toLowerCase(),
      );
      return !exactOptionMatch && inputValue.trim().length > 0;
    },
    [collection, itemToString],
  );

  const handleInputChange = useCallback(
    (details: ArkCombobox.InputValueChangeDetails) => {
      const { inputValue: newInputValue, reason } = details;

      if (reason === 'input-change' || reason === 'item-select') {
        flushSync(() => {
          if (isValidNewOption(newInputValue)) {
            upsert(NEW_OPTION_VALUE, createNewOption(newInputValue, NEW_OPTION_VALUE));
          } else if (newInputValue.trim().length === 0) {
            remove(NEW_OPTION_VALUE);
          }
        });
      }

      setInputValue(newInputValue);
    },
    [isValidNewOption, upsert, remove, createNewOption],
  );

  const handleValueChange = useCallback(
    (details: { value: (string | number)[] }) => {
      const newSelectedValues = replaceNewOptionValue(
        (details.value || []).map((v) => String(v)),
        inputValue,
      );
      setSelectedValues(newSelectedValues);

      if (details.value?.includes(NEW_OPTION_VALUE)) {
        update(NEW_OPTION_VALUE, getNewOptionData(inputValue));
      }
    },
    [inputValue, replaceNewOptionValue, update, getNewOptionData],
  );

  // 삭제
  const handleDeleteOption = useCallback(
    (item: T) => {
      remove(itemToValue(item));
      // 삭제 시 선택에서도 제거
      setSelectedValues((prev) => prev.filter((v) => v !== itemToValue(item)));
    },
    [remove, itemToValue],
  );

  // 새로 추가된 커스텀 옵션만 삭제 가능하도록 판단
  const canDeleteItem = useCallback(
    (item: T) => {
      // 초기 아이템이 아니면 삭제 가능 (새로 추가된 커스텀 옵션)
      return !initialItemValues.has(itemToValue(item));
    },
    [initialItemValues, itemToValue],
  );

  return {
    collection,
    filter,
    selectedValues,
    inputValue,
    handleInputChange,
    handleValueChange,
    isNewOptionValue,
    NEW_OPTION_VALUE,
    handleDeleteOption,
    canDeleteItem,
  };
}

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const sampleItems: StoryItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte' },
  { label: 'Next.js', value: 'nextjs', optionlabel: 'Next.js' },
  { label: 'Nuxt.js', value: 'nuxtjs', optionlabel: 'Nuxt.js' },
  { label: 'SvelteKit', value: 'sveltekit', optionlabel: 'SvelteKit' },
  { label: 'Remix', value: 'remix', optionlabel: 'Remix' },
  { label: 'Astro', value: 'astro', optionlabel: 'Astro' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid' },
];

// ============================================================================
// AddOption 스토리
// ============================================================================

const AddOptionCombobox = (args: Partial<ComboboxProps<StoryItem>>) => {
  const { contains } = useFilter({ sensitivity: 'base' });

  const {
    collection,
    filter,
    selectedValues,
    handleInputChange,
    handleValueChange,
    isNewOptionValue,
    handleDeleteOption,
    canDeleteItem,
  } = useCreateOptionCombobox({
    initialItems: sampleItems,
    itemToString: (item: StoryItem) => item.label,
    itemToValue: (item: StoryItem) => item.value,
    filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
  });

  return (
    <Combobox
      {...args}
      collection={collection}
      filter={filter}
      value={selectedValues}
      onInputChange={handleInputChange}
      onValueChange={handleValueChange}
      onDeleteOption={handleDeleteOption}
      canDeleteItem={canDeleteItem}
    >
      {(collection.items || []).map((item: StoryItem) => {
        return (
          <Combobox.Option key={item.value} item={item.value}>
            <Combobox.OptionLabel>
              {isNewOptionValue(item.value) ? `"${item.label}" 추가하기` : item.label}
            </Combobox.OptionLabel>
            {item.description && <Combobox.OptionDescription>{item.description}</Combobox.OptionDescription>}
          </Combobox.Option>
        );
      })}
    </Combobox>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '프레임워크를 검색하거나 추가하세요',
    multiple: true,
  }) } as ComboboxProps<StoryItem>;

const ComboboxAddOptionExampleRender = (args: ComboboxProps<StoryItem>) => <AddOptionCombobox {...args} />;

export default function ComboboxAddOptionExample(props: Partial<ComboboxProps<StoryItem>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComboboxProps<StoryItem>;
  return ComboboxAddOptionExampleRender(mergedProps);
}

AddOptionSingle

코드


import { Combobox as Combobox } from '@mildang/design-system/Combobox';
import { type ComboboxProps } from '@mildang/design-system/Combobox';
import { useFilter, useListCollection } from '@ark-ui/react';
import { useCallback, useMemo, useState } from 'react';
import { flushSync } from 'react-dom';
import { Combobox as ArkCombobox } from '@ark-ui/react/combobox';

export interface UseCreateOptionComboboxOptions<T> {
  /**
   * 초기 아이템 목록
   */
  initialItems: T[];
  /**
   * 아이템을 문자열로 변환하는 함수
   */
  itemToString: (item: T) => string;
  /**
   * 아이템을 값으로 변환하는 함수
   */
  itemToValue: (item: T) => string;
  /**
   * 필터 함수 (선택사항)
   * 제공하지 않으면 기본 필터링 로직 사용
   */
  filter?: (itemText: string, filterText: string, item: T) => boolean;
  /**
   * 새 아이템 생성 함수 (임시 옵션용, 선택사항)
   * 제공하지 않으면 기본 생성 로직 사용
   */
  createNewOption?: (value: string, newOptionValue: string) => T;
  /**
   * 새 아이템 데이터 생성 함수 (실제 추가용, 선택사항)
   * 제공하지 않으면 기본 생성 로직 사용
   */
  getNewOptionData?: (value: string) => T;
}

export interface UseCreateOptionComboboxReturn<T> {
  /**
   * Collection 객체
   */
  collection: ReturnType<typeof useListCollection<T>>['collection'];
  /**
   * 필터 함수
   */
  filter: ReturnType<typeof useListCollection<T>>['filter'];
  /**
   * 선택된 값들
   */
  selectedValues: string[];
  /**
   * 현재 입력값
   */
  inputValue: string;
  /**
   * Input 변경 핸들러
   */
  handleInputChange: (details: ArkCombobox.InputValueChangeDetails) => void;
  /**
   * 선택 변경 핸들러
   */
  handleValueChange: (details: { value: (string | number)[] }) => void;
  /**
   * 새 옵션 값인지 확인하는 함수
   */
  isNewOptionValue: (value: string) => boolean;
  /**
   * 새 옵션 값 상수
   */
  NEW_OPTION_VALUE: string;
  /**
   * 옵션 삭제 핸들러
   */
  handleDeleteOption: (item: T) => void;
  /**
   * 아이템 삭제 가능 여부 판단 함수
   */
  canDeleteItem: (item: T) => boolean;
}

/**
 * Combobox에서 "추가하기" 옵션 기능을 사용하기 위한 커스텀 훅
 * 입력값에 따라 "{keyword}" 추가하기 옵션을 동적으로 표시하고 추가할 수 있게 해줍니다.
 *
 * @example
 * ```tsx
 * const {
 *   collection,
 *   filter,
 *   selectedValues,
 *   inputValue,
 *   handleInputChange,
 *   handleValueChange,
 *   isNewOptionValue,
 *   NEW_OPTION_VALUE,
 * } = useCreateOptionCombobox({
 *   initialItems: items,
 *   itemToString: (item) => item.label,
 *   itemToValue: (item) => item.value,
 *   createNewOption: (value) => ({ label: value, value: NEW_OPTION_VALUE }),
 *   getNewOptionData: (value) => ({ label: value, value, __new__: true }),
 * });
 * ```
 */
export function useCreateOptionCombobox<T extends { value: string }>(
  options: UseCreateOptionComboboxOptions<T>,
): UseCreateOptionComboboxReturn<T> {
  const {
    initialItems,
    itemToString,
    itemToValue,
    filter: customFilter,
    createNewOption: customCreateNewOption,
    getNewOptionData: customGetNewOptionData,
  } = options;

  const { contains } = useFilter({ sensitivity: 'base' });
  const { collection, filter, upsert, remove, update } = useListCollection({
    initialItems,
    itemToString,
    itemToValue,
    filter:
      customFilter ||
      ((_itemText: string, filterText: string, item: T) => {
        // 검색어가 없으면 전체 목록 표시
        if (!filterText || filterText.trim() === '') {
          return true;
        }
        // 검색어가 있으면 필터링 적용
        return contains(itemToString(item), filterText);
      }),
  });

  // 초기 아이템들의 value를 Set으로 저장 (기본 옵션 추적 => 추가된 아이템만 삭제가능)
  const initialItemValues = useMemo(
    () => new Set(initialItems.map((item) => itemToValue(item))),
    [initialItems, itemToValue],
  );

  const [selectedValues, setSelectedValues] = useState<string[]>([]);
  const [inputValue, setInputValue] = useState('');

  console.log(inputValue, 'inputValue');

  const NEW_OPTION_VALUE = '[[new]]';

  const isNewOptionValue = useCallback((value: string) => value === NEW_OPTION_VALUE, []);

  const replaceNewOptionValue = useCallback(
    (values: string[], value: string) => values.map((v) => (v === NEW_OPTION_VALUE ? value : v)),
    [],
  );

  // 기본 새 옵션 생성 함수 (임시 옵션용)
  const createNewOption = useCallback(
    (value: string, newOptionValue: string): T => {
      if (customCreateNewOption) {
        return customCreateNewOption(value, newOptionValue);
      }
      // 기본 구현: value를 label로 사용하고, newOptionValue를 value로 설정
      // optionlabel도 label과 동일하게 설정 (일반적인 패턴)
      const label = value;
      return { value: newOptionValue, label, optionlabel: label } as unknown as T;
    },
    [customCreateNewOption],
  );

  // 기본 새 옵션 데이터 생성 함수 (실제 추가용)
  const getNewOptionData = useCallback(
    (value: string): T => {
      if (customGetNewOptionData) {
        return customGetNewOptionData(value);
      }
      // 기본 구현: value를 그대로 사용하고, label과 optionlabel도 value로 설정
      // __new__ 속성도 추가하여 새로 추가된 아이템임을 표시
      return { value, label: value, optionlabel: value, __new__: true } as unknown as T;
    },
    [customGetNewOptionData],
  );

  const isValidNewOption = useCallback(
    (inputValue: string) => {
      if (!inputValue.trim()) {
        return false;
      }
      const exactOptionMatch = (collection.items || []).some(
        (item: T) => itemToString(item).toLowerCase() === inputValue.toLowerCase(),
      );
      return !exactOptionMatch && inputValue.trim().length > 0;
    },
    [collection, itemToString],
  );

  const handleInputChange = useCallback(
    (details: ArkCombobox.InputValueChangeDetails) => {
      const { inputValue: newInputValue, reason } = details;

      if (reason === 'input-change' || reason === 'item-select') {
        flushSync(() => {
          if (isValidNewOption(newInputValue)) {
            upsert(NEW_OPTION_VALUE, createNewOption(newInputValue, NEW_OPTION_VALUE));
          } else if (newInputValue.trim().length === 0) {
            remove(NEW_OPTION_VALUE);
          }
        });
      }

      setInputValue(newInputValue);
    },
    [isValidNewOption, upsert, remove, createNewOption],
  );

  const handleValueChange = useCallback(
    (details: { value: (string | number)[] }) => {
      const newSelectedValues = replaceNewOptionValue(
        (details.value || []).map((v) => String(v)),
        inputValue,
      );
      setSelectedValues(newSelectedValues);

      if (details.value?.includes(NEW_OPTION_VALUE)) {
        update(NEW_OPTION_VALUE, getNewOptionData(inputValue));
      }
    },
    [inputValue, replaceNewOptionValue, update, getNewOptionData],
  );

  // 삭제
  const handleDeleteOption = useCallback(
    (item: T) => {
      remove(itemToValue(item));
      // 삭제 시 선택에서도 제거
      setSelectedValues((prev) => prev.filter((v) => v !== itemToValue(item)));
    },
    [remove, itemToValue],
  );

  // 새로 추가된 커스텀 옵션만 삭제 가능하도록 판단
  const canDeleteItem = useCallback(
    (item: T) => {
      // 초기 아이템이 아니면 삭제 가능 (새로 추가된 커스텀 옵션)
      return !initialItemValues.has(itemToValue(item));
    },
    [initialItemValues, itemToValue],
  );

  return {
    collection,
    filter,
    selectedValues,
    inputValue,
    handleInputChange,
    handleValueChange,
    isNewOptionValue,
    NEW_OPTION_VALUE,
    handleDeleteOption,
    canDeleteItem,
  };
}

// ComboboxOption은 더 이상 사용하지 않으므로 로컬 타입으로 정의
interface ComboboxOption {
  optionlabel: string;
}

// ============================================================================
// 데이터 정의
// ============================================================================

interface StoryItem extends ComboboxOption {
  label: string;
  value: string;
  description?: string;
  optionlabel: string;
  __new__?: boolean;
}

const sampleItems: StoryItem[] = [
  { label: 'React', value: 'react', optionlabel: 'React' },
  { label: 'Vue', value: 'vue', optionlabel: 'Vue' },
  { label: 'Angular', value: 'angular', optionlabel: 'Angular' },
  { label: 'Svelte', value: 'svelte', optionlabel: 'Svelte' },
  { label: 'Next.js', value: 'nextjs', optionlabel: 'Next.js' },
  { label: 'Nuxt.js', value: 'nuxtjs', optionlabel: 'Nuxt.js' },
  { label: 'SvelteKit', value: 'sveltekit', optionlabel: 'SvelteKit' },
  { label: 'Remix', value: 'remix', optionlabel: 'Remix' },
  { label: 'Astro', value: 'astro', optionlabel: 'Astro' },
  { label: 'Solid', value: 'solid', optionlabel: 'Solid' },
];

// ============================================================================
// AddOption 스토리
// ============================================================================

const AddOptionCombobox = (args: Partial<ComboboxProps<StoryItem>>) => {
  const { contains } = useFilter({ sensitivity: 'base' });

  const {
    collection,
    filter,
    selectedValues,
    handleInputChange,
    handleValueChange,
    isNewOptionValue,
    handleDeleteOption,
    canDeleteItem,
  } = useCreateOptionCombobox({
    initialItems: sampleItems,
    itemToString: (item: StoryItem) => item.label,
    itemToValue: (item: StoryItem) => item.value,
    filter: (_itemText: string, filterText: string, item: StoryItem) => contains(item.label, filterText),
  });

  return (
    <Combobox
      {...args}
      collection={collection}
      filter={filter}
      value={selectedValues}
      onInputChange={handleInputChange}
      onValueChange={handleValueChange}
      onDeleteOption={handleDeleteOption}
      canDeleteItem={canDeleteItem}
    >
      {(collection.items || []).map((item: StoryItem) => {
        return (
          <Combobox.Option key={item.value} item={item.value}>
            <Combobox.OptionLabel>
              {isNewOptionValue(item.value) ? `"${item.label}" 추가하기` : item.label}
            </Combobox.OptionLabel>
            {item.description && <Combobox.OptionDescription>{item.description}</Combobox.OptionDescription>}
          </Combobox.Option>
        );
      })}
    </Combobox>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    placeholder: '프레임워크를 검색하거나 추가하세요',
  }) } as ComboboxProps<StoryItem>;

const ComboboxAddOptionSingleExampleRender = (args: ComboboxProps<StoryItem>) => <AddOptionCombobox {...args} />;

export default function ComboboxAddOptionSingleExample(props: Partial<ComboboxProps<StoryItem>>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComboboxProps<StoryItem>;
  return ComboboxAddOptionSingleExampleRender(mergedProps);
}