File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
08-14
08-14
08-18
08-14
08-18
08-18
File name
Commit message
Commit date
"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 (
<li
className={cx(
"fox-bullet-list-item",
`fox-bullet-list-item--lv${level}`,
`fox-bullet-list-item--${kind}`,
className
)}
>
<span className="fox-bullet-list-item__row">
{/* 마커는 글을 읽는 데 필요 없다 — 몇 번째인지는 목록 구조가 이미 알린다. */}
<span className="fox-bullet-list-item__marker" aria-hidden="true">
{marker ?? (ordered ? orderedMarker(level, ordinal) : unorderedMarker(level))}
</span>
<span className="fox-bullet-list-item__text">{children}</span>
</span>
{nested}
</li>
);
}