임동욱 임동욱 08-18
feat: Bullet 컴포넌트 2종 추가
Figma bullet(391:4049) · bullet-list-item(391:4081) · bullet-list(484:5722)를
FoxBulletList / FoxBulletListItem 둘로 옮겼다.

bullet을 따로 두지 않은 이유는 마커가 목록 밖에서 홀로 쓰이는 자리가 없고, 모양이
전부 단계·번호 여부로 갈리기 때문이다. 그 두 축이 항목의 수식어로 들어가 있다.

호출부는 단계를 평평하게 늘어놓고 level만 적는다. 어느 항목 안에 무엇을 넣을지는
목록이 정한다.

  
    레벨1
    레벨2
  

시안은 2·3단계를 형제 프레임으로 두고 왼쪽 여백만 20·40px 줬지만, 나가는 마크업은
    /
      안에
        /
          이 든 진짜 중첩이다 — 목록의 단계를 스크린리더에 알리려면 그래야 한다. 한 단계마다 20px씩 들어가 3단계가 40px이 되므로 서는 자리는 시안과 같다. 앞에 걸어 둘 항목이 없는 경우(첫 줄부터 2단계)는 빈
        1. 에 매달아 목록을 유효하게 둔다. 번호는 같은 묶음 안에서 1부터 다시 센다. 시안에 없는 범위는 z 다음 aa, ⑳ 다음 (21)로 잇고, 다르게 매겨야 하면 marker 프롭으로 덮는다. 승인된 비토큰 값: 마커 칸 2.2×2.6rem · 1.6×2.3rem · 2.4×2.3rem, 채운 원 0.6rem, 빈 원 0.4rem — 모두 시안 값이나 Figma가 변수로 export하지 않는다. 검증: 시안 대조 20항목 전부 일치(칸 크기·마커 모양·글자 크기·단계별 들여쓰기 0/20/40px). 목록의 자식이 li뿐이고 중첩 목록은 모두 li 안에 있다. sass 463규칙 · tsc --noEmit · eslint 통과. Co-Authored-By: Claude Opus 5
@b65f1b32ff3c6052ffdf24277413dab074232ba9
 
@fox/core/components/fox-bullet-list-item/fox-bullet-list-item.tsx (added)
+++ @fox/core/components/fox-bullet-list-item/fox-bullet-list-item.tsx
@@ -0,0 +1,107 @@
+"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>
+  );
+}
 
@fox/core/components/fox-bullet-list-item/index.ts (added)
+++ @fox/core/components/fox-bullet-list-item/index.ts
@@ -0,0 +1,5 @@
+export {
+  FoxBulletListItem,
+  type FoxBulletListItemProps,
+  type FoxBulletLevel,
+} from "./fox-bullet-list-item";
 
@fox/core/components/fox-bullet-list/fox-bullet-list.tsx (added)
+++ @fox/core/components/fox-bullet-list/fox-bullet-list.tsx
@@ -0,0 +1,134 @@
+"use client";
+
+import {
+  Children,
+  Fragment,
+  cloneElement,
+  isValidElement,
+  type ReactElement,
+  type ReactNode,
+} from "react";
+import { cx } from "../../utils";
+import type { FoxBulletLevel, FoxBulletListItemProps } from "../fox-bullet-list-item";
+
+export interface FoxBulletListProps {
+  /** `FoxBulletListItem`들. 단계는 평평하게 늘어놓고 각 항목의 `level`로 적는다. */
+  children: ReactNode;
+  /** 참이면 번호 목록(`1.` · `a.` · `①`)이 되고, 아니면 기호 목록이다. */
+  ordered?: boolean;
+  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
+  hidden?: boolean;
+  /** 배치 조정용. */
+  className?: string;
+}
+
+type ItemElement = ReactElement<FoxBulletListItemProps>;
+type Entry = { level: FoxBulletLevel; element: ItemElement };
+
+/** Fragment로 감싸 넘겨도 안쪽 항목까지 찾아낸다. */
+function collect(children: ReactNode): Entry[] {
+  const entries: Entry[] = [];
+  Children.forEach(children, (child) => {
+    if (!isValidElement(child)) {
+      return;
+    }
+    if (child.type === Fragment) {
+      const fragment = child as ReactElement<{ children?: ReactNode }>;
+      entries.push(...collect(fragment.props.children));
+      return;
+    }
+    const element = child as ItemElement;
+    entries.push({ level: element.props.level ?? 1, element });
+  });
+  return entries;
+}
+
+/**
+ * 평평한 단계 목록을 진짜 중첩으로 접는다.
+ *
+ * 더 깊은 항목들은 바로 앞 항목 **안쪽** 목록이 된다 — `<ul>`의 자식은 `<li>`뿐이라
+ * 중첩 목록을 형제로 둘 수 없다. 앞에 걸어 둘 항목이 없으면(첫 줄부터 2단계로 시작하는
+ * 경우) 빈 `<li>`에 매달아 유효한 목록을 지킨다.
+ */
+function build(entries: Entry[], depth: FoxBulletLevel, ordered: boolean): ReactNode[] {
+  const nodes: ReactNode[] = [];
+  let ordinal = 0;
+  let index = 0;
+
+  while (index < entries.length) {
+    let next = index;
+
+    if (entries[index].level > depth) {
+      while (next < entries.length && entries[next].level > depth) {
+        next += 1;
+      }
+      nodes.push(
+        <li className="fox-bullet-list__hanger" key={`hanger-${index}`}>
+          {sublist(entries.slice(index, next), depth, ordered)}
+        </li>
+      );
+      index = next;
+      continue;
+    }
+
+    next = index + 1;
+    while (next < entries.length && entries[next].level > depth) {
+      next += 1;
+    }
+    const deeper = entries.slice(index + 1, next);
+    ordinal += 1;
+
+    nodes.push(
+      cloneElement(entries[index].element, {
+        key: `item-${index}`,
+        level: depth,
+        ordered,
+        ordinal,
+        nested: deeper.length > 0 ? sublist(deeper, depth, ordered) : undefined,
+      })
+    );
+    index = next;
+  }
+
+  return nodes;
+}
+
+function sublist(entries: Entry[], depth: FoxBulletLevel, ordered: boolean): ReactNode {
+  const Tag = ordered ? "ol" : "ul";
+  const deeper = Math.min(depth + 1, 3) as FoxBulletLevel;
+  return (
+    <Tag className="fox-bullet-list fox-bullet-list--nested">{build(entries, deeper, ordered)}</Tag>
+  );
+}
+
+/**
+ * @fox 글머리 목록. 항목의 `level`만 보고 중첩과 번호를 스스로 만든다.
+ *
+ * 호출부는 단계를 평평하게 늘어놓기만 하면 된다 — 어느 항목 안에 무엇을 넣을지 따질
+ * 필요가 없다. 나가는 마크업은 `<ul>`/`<ol>` 안에 `<ul>`/`<ol>`이 든 진짜 중첩이라
+ * 스크린리더가 몇 단계인지 알린다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxBulletList({
+  children,
+  ordered = false,
+  hidden = false,
+  className,
+}: FoxBulletListProps) {
+  if (hidden) {
+    return null;
+  }
+
+  const entries = collect(children);
+  if (entries.length === 0) {
+    return null;
+  }
+
+  const Tag = ordered ? "ol" : "ul";
+
+  return (
+    <Tag className={cx("fox-bullet-list", className)}>{build(entries, 1, ordered)}</Tag>
+  );
+}
 
@fox/core/components/fox-bullet-list/index.ts (added)
+++ @fox/core/components/fox-bullet-list/index.ts
@@ -0,0 +1,1 @@
+export { FoxBulletList, type FoxBulletListProps } from "./fox-bullet-list";
@fox/core/components/index.ts
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
@@ -1,5 +1,7 @@
 export * from "./fox-address";
 export * from "./fox-breadcrumb";
+export * from "./fox-bullet-list";
+export * from "./fox-bullet-list-item";
 export * from "./fox-button";
 export * from "./fox-button-group";
 export * from "./fox-button-panel";
@fox/dev-test/component-registry.tsx
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
@@ -37,6 +37,8 @@
   FoxPlayIcon,
   FoxPlusIcon,
 } from "../core/icons";
+import { FoxBulletList } from "../core/components/fox-bullet-list";
+import { FoxBulletListItem } from "../core/components/fox-bullet-list-item";
 import styles from "./dev-test.module.scss";
 
 export interface ComponentExample {
@@ -2275,4 +2277,110 @@
       },
     ],
   },
+  {
+    id: "fox-bullet-list",
+    name: "FoxBulletList",
+    description:
+      "Figma 시안(bullet 391:4049 · bullet-list-item 391:4081 · bullet-list 484:5722)을 옮겼습니다. 호출부는 단계를 평평하게 늘어놓고 level만 적으면 되고, 목록이 중첩과 번호를 스스로 만듭니다. 나가는 마크업은 <ul>/<ol> 안에 <ul>/<ol>이 든 진짜 중첩입니다.",
+    variants: [
+      {
+        label: "기호 목록 — 시안 조합 그대로",
+        node: (
+          <div className={styles.demoField}>
+            <FoxBulletList>
+              <FoxBulletListItem>텍스트 목록 레벨1 텍스트 목록 레벨1텍스트 목록 레벨1</FoxBulletListItem>
+              <FoxBulletListItem>텍스트 목록 레벨1</FoxBulletListItem>
+              <FoxBulletListItem>텍스트 목록 레벨1</FoxBulletListItem>
+              <FoxBulletListItem level={2}>텍스트 목록 레벨2텍스트 목록 레벨2 텍스트 목록 레벨2</FoxBulletListItem>
+              <FoxBulletListItem level={2}>텍스트 목록 레벨2</FoxBulletListItem>
+              <FoxBulletListItem level={3}>텍스트 목록 레벨3 텍스트 목록 레벨3 텍스트 목록 레벨3</FoxBulletListItem>
+              <FoxBulletListItem level={3}>텍스트 목록 레벨3</FoxBulletListItem>
+              <FoxBulletListItem>텍스트 목록 레벨1</FoxBulletListItem>
+            </FoxBulletList>
+          </div>
+        ),
+      },
+      {
+        label: "번호 목록 — 같은 묶음 안에서 1부터 다시 셉니다",
+        node: (
+          <div className={styles.demoField}>
+            <FoxBulletList ordered>
+              <FoxBulletListItem>텍스트 목록 레벨1 텍스트 목록 레벨1텍스트 목록 레벨1</FoxBulletListItem>
+              <FoxBulletListItem>텍스트 목록 레벨1</FoxBulletListItem>
+              <FoxBulletListItem>텍스트 목록 레벨1</FoxBulletListItem>
+              <FoxBulletListItem level={2}>텍스트 목록 레벨2텍스트 목록 레벨2 텍스트 목록 레벨2</FoxBulletListItem>
+              <FoxBulletListItem level={2}>텍스트 목록 레벨2</FoxBulletListItem>
+              <FoxBulletListItem level={3}>텍스트 목록 레벨3 텍스트 목록 레벨3 텍스트 목록 레벨3</FoxBulletListItem>
+              <FoxBulletListItem level={3}>텍스트 목록 레벨3</FoxBulletListItem>
+              <FoxBulletListItem>텍스트 목록 레벨1</FoxBulletListItem>
+            </FoxBulletList>
+          </div>
+        ),
+      },
+      {
+        label: "마커 6종 — 단계별로 나란히",
+        node: (
+          <div className={styles.demoFields}>
+            <div className={styles.demoField}>
+              <FoxBulletList>
+                <FoxBulletListItem>1단계 — 채운 원</FoxBulletListItem>
+                <FoxBulletListItem level={2}>2단계 — 짧은 선</FoxBulletListItem>
+                <FoxBulletListItem level={3}>3단계 — 빈 원</FoxBulletListItem>
+              </FoxBulletList>
+            </div>
+            <div className={styles.demoField}>
+              <FoxBulletList ordered>
+                <FoxBulletListItem>1단계 — 1.</FoxBulletListItem>
+                <FoxBulletListItem level={2}>2단계 — a.</FoxBulletListItem>
+                <FoxBulletListItem level={3}>3단계 — ①</FoxBulletListItem>
+              </FoxBulletList>
+            </div>
+          </div>
+        ),
+      },
+      {
+        label: "번호가 이어질 때 — 같은 단계가 여러 줄",
+        node: (
+          <div className={styles.demoField}>
+            <FoxBulletList ordered>
+              <FoxBulletListItem>첫째</FoxBulletListItem>
+              <FoxBulletListItem level={2}>가</FoxBulletListItem>
+              <FoxBulletListItem level={2}>나</FoxBulletListItem>
+              <FoxBulletListItem level={3}>하나</FoxBulletListItem>
+              <FoxBulletListItem level={3}>둘</FoxBulletListItem>
+              <FoxBulletListItem level={3}>셋</FoxBulletListItem>
+              <FoxBulletListItem>둘째</FoxBulletListItem>
+              <FoxBulletListItem>셋째</FoxBulletListItem>
+            </FoxBulletList>
+          </div>
+        ),
+      },
+      {
+        label: "항목 안에 링크·강조를 넣어도 됩니다",
+        node: (
+          <div className={styles.demoField}>
+            <FoxBulletList>
+              <FoxBulletListItem>
+                자세한 내용은 <a href="#fox-bullet-list">이 문서</a>를 확인하세요.
+              </FoxBulletListItem>
+              <FoxBulletListItem level={2}>
+                <strong>굵은 글씨</strong>도 그대로 들어갑니다.
+              </FoxBulletListItem>
+            </FoxBulletList>
+          </div>
+        ),
+      },
+      {
+        label: "2단계로 시작할 때 — 빈 칸에 매달아 목록이 유효하게 유지됩니다",
+        node: (
+          <div className={styles.demoField}>
+            <FoxBulletList>
+              <FoxBulletListItem level={2}>앞에 1단계가 없습니다</FoxBulletListItem>
+              <FoxBulletListItem level={2}>그래도 들여쓰기는 유지됩니다</FoxBulletListItem>
+            </FoxBulletList>
+          </div>
+        ),
+      },
+    ],
+  },
 ];
 
@fox/styles/_fox-bullet-list-item.scss (added)
+++ @fox/styles/_fox-bullet-list-item.scss
@@ -0,0 +1,140 @@
+// FoxBulletListItem — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                           bullet-list-item (섹션 391:4081) · bullet (섹션 391:4049)
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <li class="fox-bullet-list-item fox-bullet-list-item--lv1 fox-bullet-list-item--unordered">
+//     <span class="fox-bullet-list-item__row">
+//       <span class="fox-bullet-list-item__marker" aria-hidden="true"></span>
+//       <span class="fox-bullet-list-item__text">글</span>
+//     </span>
+//     <ul class="fox-bullet-list fox-bullet-list--nested">…</ul>   <!-- 선택 -->
+//   </li>
+//
+// 시안의 bullet은 따로 두지 않았다 — 마커가 목록 밖에서 홀로 쓰이는 자리가 없고, 크기와
+// 모양이 전부 단계·번호 여부로 갈리기 때문이다. 그 두 축이 여기 수식어로 들어와 있다.
+//
+// 마커는 폭이 고정이고 글만 남는 폭을 먹는다. 그래서 글이 길어져도 마커 오른쪽에서만
+// 접히고 마커 아래로 흘러내리지 않는다.
+//
+// 1단계 채운 원과 3단계 빈 원은 시안에서 도형이라 가상 요소로 그린다. 2단계 `-`와 번호는
+// 시안에서 글자라 React가 글자로 넣는다.
+//
+// 항목 안에서 글줄과 중첩 목록 사이는 12px이다(시안에서 뿌리 목록의 줄 간격과 같다).
+//
+// 토큰이 아닌 값:
+//   - 마커 칸 2.2rem×2.6rem(1·2단계) · 1.6rem×2.3rem(기호 3단계) · 2.4rem×2.3rem(번호
+//     3단계), 채운 원 0.6rem, 빈 원 0.4rem: 모두 시안 값이나 Figma가 변수로
+//     export하지 않는다(사용자 승인).
+//   - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로
+//     export하지 않는다.
+//   - list-style / flex 관련: 구조 규칙.
+
+@use "abstracts" as fox;
+
+.fox-bullet-list-item {
+  display: flex;
+  flex-direction: column;
+  gap: fox.gap(4);
+  inline-size: 100%;
+  list-style: none;
+
+  &__row {
+    display: flex;
+    align-items: flex-start;
+    gap: fox.gap(1);
+    inline-size: 100%;
+  }
+
+  // 시안 값이나 변수로 export되지 않는다 — 파일 상단 참고.
+  &__marker {
+    box-sizing: border-box;
+    display: flex;
+    flex-shrink: 0;
+    align-items: center;
+    justify-content: center;
+    inline-size: 2.2rem;
+    block-size: 2.6rem;
+    color: fox.color(font-neutral-default);
+    font-family: fox.font-family(body);
+    font-weight: fox.font-weight(regular);
+    line-height: 1.5;
+    white-space: nowrap;
+  }
+
+  // 글만 남는 폭을 먹는다.
+  &__text {
+    flex: 1 0 0;
+    min-inline-size: 0;
+    color: fox.color(font-neutral-default);
+    font-family: fox.font-family(body);
+    font-size: fox.font-size(body-md);
+    font-weight: fox.font-weight(regular);
+    line-height: 1.5;
+    letter-spacing: -0.025em;
+    word-break: break-word;
+  }
+
+  // ── 기호 마커 ─────────────────────────────────────────────────────────────
+  // 1·3단계는 시안에서 도형이라 가상 요소로 그린다. 2단계는 글자 `-`라 React가 넣는다.
+  &--unordered.fox-bullet-list-item--lv1 &__marker::before,
+  &--unordered.fox-bullet-list-item--lv3 &__marker::before {
+    content: "";
+    display: block;
+    background: fox.color(element-neutral-default);
+  }
+
+  &--unordered.fox-bullet-list-item--lv2 &__marker {
+    font-size: fox.font-size(body-md);
+    letter-spacing: -0.025em;
+  }
+
+  &--unordered.fox-bullet-list-item--lv1 &__marker::before {
+    inline-size: 0.6rem;
+    block-size: 0.6rem;
+    border-radius: fox.radius(max);
+  }
+
+  &--unordered.fox-bullet-list-item--lv3 &__marker::before {
+    box-sizing: border-box;
+    inline-size: 0.4rem;
+    block-size: 0.4rem;
+    border: fox.border(1) solid fox.color(element-neutral-default);
+    border-radius: fox.radius(max);
+    background: none;
+  }
+
+  // ── 번호 마커 ─────────────────────────────────────────────────────────────
+  &--ordered.fox-bullet-list-item--lv1 &__marker {
+    font-size: fox.font-size(body-md);
+    letter-spacing: -0.025em;
+  }
+
+  &--ordered.fox-bullet-list-item--lv2 &__marker {
+    font-size: fox.font-size(body-sm);
+    letter-spacing: -0.025em;
+  }
+
+  &--ordered.fox-bullet-list-item--lv3 &__marker {
+    font-size: fox.font-size(body-xsm);
+    letter-spacing: -0.025em;
+  }
+
+  // ── 3단계 ─────────────────────────────────────────────────────────────────
+  // 마커 칸이 한 단계 좁아지고 왼쪽으로 조금 밀려 있다.
+  &--lv3 &__marker {
+    block-size: 2.3rem;
+    padding-inline-start: fox.padding(2);
+  }
+
+  &--unordered.fox-bullet-list-item--lv3 &__marker {
+    inline-size: 1.6rem;
+  }
+
+  &--ordered.fox-bullet-list-item--lv3 &__marker {
+    inline-size: 2.4rem;
+  }
+
+  &--lv3 &__text {
+    font-size: fox.font-size(body-sm);
+  }
+}
 
@fox/styles/_fox-bullet-list.scss (added)
+++ @fox/styles/_fox-bullet-list.scss
@@ -0,0 +1,44 @@
+// FoxBulletList — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                       bullet-list (484:5650 · 484:5723 · 섹션 484:5722)
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <ul class="fox-bullet-list">
+//     <li class="fox-bullet-list-item …">…</li>
+//     <li class="fox-bullet-list-item …">…
+//       <ul class="fox-bullet-list fox-bullet-list--nested">…</ul>
+//     </li>
+//   </ul>
+//   번호 목록은 `<ul>` 자리에 `<ol>`을 쓴다.
+//
+// 시안은 2·3단계를 형제 프레임으로 두고 왼쪽 여백만 20·40px 줬지만, 여기서는 진짜 중첩
+// (`<ul>` 안의 `<ul>`)으로 낸다 — 목록의 단계를 스크린리더에 알리려면 그래야 한다.
+// 한 단계마다 20px씩 들어가므로 3단계는 40px이 되어 시안과 같은 자리에 선다.
+//
+// 항목 사이 간격은 뿌리에서 12px, 중첩 안에서 8px이다(시안 그대로).
+//
+// 토큰이 아닌 값:
+//   - 목록의 margin/padding/list-style 0: `<ul>`·`<ol>`의 브라우저 기본값 지우기.
+
+@use "abstracts" as fox;
+
+.fox-bullet-list {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: fox.gap(4);
+  inline-size: 100%;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+
+  // 안쪽 목록은 한 단계 들여쓰고 줄 간격이 좁다.
+  &--nested {
+    gap: fox.gap(3);
+    padding-inline-start: fox.padding(7);
+  }
+
+  // 첫 줄부터 2단계로 시작할 때 중첩 목록을 매다는 빈 칸. 보이는 것이 없다.
+  &__hanger {
+    inline-size: 100%;
+  }
+}
@fox/styles/components.scss
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
@@ -33,3 +33,5 @@
 @use "fox-carousel-button";
 @use "fox-carousel-area";
 @use "fox-toggle-switch";
+@use "fox-bullet-list";
+@use "fox-bullet-list-item";
Add a comment
List