"use client"; import { Children, Fragment, isValidElement, type ReactElement, type ReactNode, } from "react"; import { cx } from "../../utils"; import type { FoxTableRowProps } from "../fox-table-row"; export type FoxTableType = "basic" | "list" | "form"; export interface FoxTableProps { /** `FoxTableRow`들. 머리행은 `head`를 주면 되고 순서는 신경 쓰지 않아도 된다. */ children?: ReactNode; /** * `basic`은 제목 열이 있는 상세표, `list`는 목록표, `form`은 레이블 + 입력표다. * 여백·글자·기본 정렬이 이 값에 따라 달라진다. */ type?: FoxTableType; /** 표가 무엇을 담는지. ``으로 나가며 화면에는 보이지 않는다. */ caption?: string; /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ hidden?: boolean; /** 배치 조정용. */ className?: string; } type RowElement = ReactElement; /** 프래그먼트 안까지 훑어 행을 모은다. */ function collectRows(children: ReactNode): RowElement[] { const rows: RowElement[] = []; Children.forEach(children, (child) => { if (!isValidElement(child)) { return; } if (child.type === Fragment) { const fragment = child as ReactElement<{ children?: ReactNode }>; rows.push(...collectRows(fragment.props.children)); return; } rows.push(child as RowElement); }); return rows; } /** * @fox 표. 진짜 ``이라 스크린리더가 몇 행 몇 열인지 읽어 주고 `colSpan`·`rowSpan`이 * 그대로 동작한다. * * 호출부는 행을 늘어놓기만 하면 된다 — 머리행(`head`)은 ``로, 나머지는 ``로 * 알아서 갈린다. * * 좁은 화면에서는 `list`가 한 줄씩 카드로 접히고 `form`은 레이블이 입력 위로 올라간다 * (시안의 device 축). 그 전환은 CSS가 화면 폭을 보고 하므로 프롭이 없다. * * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` * (또는 개별 파티셜)로 한 번 불러와야 한다. */ export function FoxTable({ children, type = "basic", caption, hidden = false, className, }: FoxTableProps) { if (hidden) { return null; } const rows = collectRows(children); const headRows = rows.filter((row) => row.props.head); const bodyRows = rows.filter((row) => !row.props.head); return (
{caption && } {headRows.length > 0 && {headRows}} {bodyRows.length > 0 && {bodyRows}}
{caption}
); }