"use client"; import type { ReactNode } from "react"; import { cx } from "../../utils"; export type FoxBulletLevel = 1 | 2 | 3; /** ①~⑳. 시안 3단계가 쓰는 모양이고, 넘어가면 `(21)`처럼 괄호로 적는다. */ const CIRCLED = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"; /** a·b·c … z 다음에는 aa·ab로 이어진다(스프레드시트 열 이름과 같은 방식). */ function alphaOrdinal(index: number): string { let rest = index; let text = ""; while (rest > 0) { const digit = (rest - 1) % 26; text = String.fromCharCode(97 + digit) + text; rest = Math.floor((rest - 1) / 26); } return text; } /** 2단계 기호는 시안에서 글자 `-`다. 1·3단계는 도형이라 스타일이 그린다. */ function unorderedMarker(level: FoxBulletLevel): string | null { return level === 2 ? "-" : null; } function orderedMarker(level: FoxBulletLevel, ordinal: number): string { if (level === 2) { return `${alphaOrdinal(ordinal)}.`; } if (level === 3) { return ordinal <= CIRCLED.length ? CIRCLED[ordinal - 1] : `(${ordinal})`; } return `${ordinal}.`; } export interface FoxBulletListItemProps { /** * 몇 단계인지. 목록은 이 값만 보고 중첩을 만든다 — 호출부는 평평하게 늘어놓고 * 단계만 적으면 된다. */ level?: FoxBulletLevel; children: ReactNode; /** 마커를 직접 정할 때. 번호 매김이 시안과 달라야 하는 경우에만 쓴다. */ marker?: ReactNode; /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ hidden?: boolean; /** 배치 조정용. */ className?: string; /** FoxBulletList가 채운다 — 번호 목록인지. 직접 넘기지 않는다. */ ordered?: boolean; /** FoxBulletList가 채운다 — 같은 묶음 안에서 몇 번째인지. 직접 넘기지 않는다. */ ordinal?: number; /** FoxBulletList가 채운다 — 이 항목 아래에 붙는 중첩 목록. 직접 넘기지 않는다. */ nested?: ReactNode; } /** * @fox 글머리 목록의 한 줄. 마커와 글이 나란히 서고, 글이 길어지면 마커 오른쪽에서만 * 접힌다. * * 마커 모양과 번호는 `level`·`ordered`가 정한다 — 채운 원 / `-` / 빈 원, 또는 * `1.` / `a.` / `①`. 번호는 같은 묶음 안에서 1부터 다시 센다. * * 혼자 쓰지 않고 `FoxBulletList` 안에 둔다. 목록이 단계를 보고 중첩과 번호를 채워 준다. * * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` * (또는 개별 파티셜)로 한 번 불러와야 한다. */ export function FoxBulletListItem({ level = 1, children, marker, hidden = false, className, ordered = false, ordinal = 1, nested, }: FoxBulletListItemProps) { if (hidden) { return null; } const kind = ordered ? "ordered" : "unordered"; return (