임동욱 임동욱 08-18
feat: 목록 화면 뼈대 FoxListContainer 추가 (+ FoxListToolbar · FoxListSearch)
시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) container 3019:8608.

시안 조각 대부분은 이미 @fox에 있었다(FoxPageHeader·FoxTable·FoxPagination·FoxSelectText·
FoxChipArea·FoxBadge·FoxButtonGroup). 없던 것은 list-toolbar 하나였고, 그 안의 검색 상자까지
합쳐 셋을 새로 만들었다.

- FoxListSearch: 앞쪽에 검색 대상 셀렉트가 들어가는 상자. FoxInput에는 그 슬롯이 없어 따로
  만들었다. `
`라 Enter가 그대로 제출이 된다. 제출 버튼(32×32)은 시안에서 btn-ico 인스턴스가 아니라 로컬 프레임이고 FoxIconButton 크기 체계(24·28·40·56)와 맞지 않아 직접 그렸다. - FoxListToolbar: 자리만 잡는 슬롯 컴포넌트(count·leading·sorts·search·actions·filter). 무엇이 들어갈지는 화면마다 달라 내용을 알지 못한다. 총 건수는 숫자만 굵고 색이 달라 FoxListCount로 분리했다. - FoxListContainer: 머리말·도구 줄·표·페이지네이션을 한 벌로 묶는다. 데이터를 가져오지도 상태를 갖지도 않는다. 표는 `columns` 정의로 그린다. `render`가 ReactNode를 돌려주므로 셀에 배지든 버튼 묶음이든 들어간다 — 값의 형식을 컨테이너가 알 필요가 없다(시안이 그렇게 생겼다). 이벤트는 두 갈래를 다 지원한다(사용자 확정). `buildHref`를 주면 페이지 이동이 링크가 되어 목록이 Server Component로 남고 새로고침·뒤로가기·URL 공유가 동작한다 — 이 저장소의 목록 화면 6개가 이미 쓰는 방식이다. `onQueryChange`만 주면 콜백으로 알린다(모달 안 목록 등 URL이 없는 자리). FoxPagination이 이미 같은 관례라 그걸 따랐다. 구현 중 바로잡은 것 — FoxTable은 thead/tbody를 스스로 만들고 머리행은 `head`만 보고 제자리로 보낸다. 처음에 직접 감쌌더니 표 안에 표 구조가 이중으로 생겨 본문이 4행으로 세어졌다. 시안이 tbl-list라 `type="list"`도 함께 지정했다. 검증 — 데모로 렌더해 확인했다. caption+thead(1행)+tbody(3행) 구조, fox-table--list, 머리열 6개, 셀 안 배지 3·버튼 6, 총 건수·정렬·검색(대상 셀렉트 포함)·툴바 액션·필터 칩 2개· 페이지네이션 링크(`?page=1`)가 모두 나온다. Co-Authored-By: Claude Opus 5
@e7e47a0e7587a54158a46de33471c095d23da1da
 
@fox/core/components/fox-list-container/fox-list-container.tsx (added)
+++ @fox/core/components/fox-list-container/fox-list-container.tsx
@@ -0,0 +1,245 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { cx } from "../../utils";
+import { FoxPageHeader } from "../fox-page-header";
+import type { FoxBreadcrumbItem } from "../fox-breadcrumb";
+import { FoxTable } from "../fox-table";
+import { FoxTableRow } from "../fox-table-row";
+import { FoxTableCell } from "../fox-table-cell";
+import type { FoxTableCellAlign } from "../fox-table-cell";
+import { FoxPagination } from "../fox-pagination";
+import { FoxListCount, FoxListToolbar } from "../fox-list-toolbar";
+import { FoxListSearch } from "../fox-list-search";
+import type { FoxSelectItem } from "../fox-select";
+
+/** 열 하나의 정의. `render`가 있으면 그 결과가 셀 내용이 된다. */
+export interface FoxListColumn<T> {
+  /** 열 식별자. `render`가 없으면 `row[key]`를 그대로 그린다. */
+  key: string;
+  header: ReactNode;
+  /** 셀을 직접 그린다 — 배지·버튼 무엇이든 넣을 수 있다. */
+  render?: (row: T, index: number) => ReactNode;
+  width?: string | number;
+  align?: FoxTableCellAlign;
+  /** 참이면 그 열의 본문 글자가 진해진다(시안의 강조 열). */
+  emphasis?: boolean;
+}
+
+/**
+ * 목록 상태의 변경분. 컨테이너는 **바뀐 것만** 담아 넘긴다 — 호출부가 기존 값과 합쳐
+ * 쓰라는 뜻이다. 화면마다 필요한 정렬·필터 키가 달라 열려 있는 모양이다.
+ */
+export interface FoxListQueryPatch {
+  page?: number;
+  keyword?: string;
+  searchField?: string;
+  [key: string]: unknown;
+}
+
+export interface FoxListContainerProps<T> {
+  // ── 머리말 ────────────────────────────────────────────────────────────────
+  title?: ReactNode;
+  breadcrumb?: FoxBreadcrumbItem[];
+  headerActions?: ReactNode;
+
+  // ── 표 ────────────────────────────────────────────────────────────────────
+  columns: FoxListColumn<T>[];
+  rows: T[];
+  /** 행의 안정적인 키. 인덱스를 쓰면 정렬·페이지 이동에서 어긋난다. */
+  rowKey: (row: T, index: number) => string;
+  caption?: string;
+  /** 행이 없을 때 표 안에 보여줄 말. */
+  emptyMessage?: ReactNode;
+
+  // ── 도구 줄 ───────────────────────────────────────────────────────────────
+  /** 주면 "총 N건"이 나온다. 없으면 그 자리를 비운다. */
+  totalCount?: number;
+  toolbarLeading?: ReactNode;
+  sorts?: ReactNode;
+  toolbarActions?: ReactNode;
+  filter?: ReactNode;
+
+  /** 검색 대상 목록. 주면 검색 상자가 나온다. 대상이 하나뿐이면 빈 배열로 두면 된다. */
+  searchFields?: FoxSelectItem[];
+  searchField?: string;
+  keyword?: string;
+  searchPlaceholder?: string;
+  /** 검색 상자를 아예 숨긴다. */
+  searchHidden?: boolean;
+
+  // ── 페이지 ────────────────────────────────────────────────────────────────
+  page?: number;
+  totalPages?: number;
+
+  // ── 이벤트 ────────────────────────────────────────────────────────────────
+  /**
+   * 주면 페이지 이동이 **링크**가 된다 — 목록이 Server Component로 남고 새로고침·뒤로가기·
+   * URL 공유가 그대로 동작한다. 이 저장소의 목록 화면들이 쓰는 방식이다.
+   */
+  buildHref?: (patch: FoxListQueryPatch) => string;
+  /** `buildHref`가 없을 때(모달 안 목록 등) 쓰는 콜백. 둘 다 주면 링크가 이긴다. */
+  onQueryChange?: (patch: FoxListQueryPatch) => void;
+
+  hidden?: boolean;
+  className?: string;
+}
+
+/**
+ * @fox 목록 화면 뼈대 — 시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) container (3019:8608)
+ *
+ * 머리말 · 도구 줄 · 표 · 페이지네이션을 한 벌로 묶는다. **데이터를 가져오지도, 상태를 갖지도
+ * 않는다** — 받은 것을 그리고 바뀐 것을 알릴 뿐이라 어떤 목록에도 그대로 쓸 수 있다.
+ *
+ * 표는 `columns` 정의로 그린다. `render`가 ReactNode를 돌려주므로 셀 안에 배지든 버튼 묶음이든
+ * 들어간다(시안이 그렇다) — 값의 형식을 컨테이너가 알 필요가 없다.
+ *
+ * 이벤트는 두 갈래다. `buildHref`를 주면 링크로 그려 URL이 상태가 되고, `onQueryChange`만
+ * 주면 콜백으로 알린다. FoxPagination이 이미 같은 방식이라 그 관례를 따랐다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxListContainer<T>({
+  title,
+  breadcrumb,
+  headerActions,
+  columns,
+  rows,
+  rowKey,
+  caption,
+  emptyMessage = "조회된 내용이 없습니다.",
+  totalCount,
+  toolbarLeading,
+  sorts,
+  toolbarActions,
+  filter,
+  searchFields,
+  searchField,
+  keyword,
+  searchPlaceholder,
+  searchHidden = false,
+  page = 1,
+  totalPages = 1,
+  buildHref,
+  onQueryChange,
+  hidden = false,
+  className,
+}: FoxListContainerProps<T>) {
+  if (hidden) {
+    return null;
+  }
+
+  const hasToolbar =
+    totalCount !== undefined ||
+    toolbarLeading !== undefined ||
+    sorts !== undefined ||
+    toolbarActions !== undefined ||
+    filter !== undefined ||
+    !searchHidden;
+
+  return (
+    <div className={cx("fox-list-container", className)}>
+      {(title !== undefined || breadcrumb !== undefined) && (
+        <FoxPageHeader
+          title={title}
+          breadcrumb={breadcrumb}
+          actions={headerActions}
+        />
+      )}
+
+      <section className="fox-list-container__section">
+        {hasToolbar && (
+          <FoxListToolbar
+            count={
+              totalCount !== undefined ? (
+                <FoxListCount total={totalCount} />
+              ) : undefined
+            }
+            leading={toolbarLeading}
+            sorts={sorts}
+            search={
+              searchHidden ? undefined : (
+                <FoxListSearch
+                  fields={searchFields}
+                  field={searchField}
+                  keyword={keyword}
+                  placeholder={searchPlaceholder}
+                  onSearch={(nextKeyword, nextField) =>
+                    onQueryChange?.({
+                      keyword: nextKeyword,
+                      searchField: nextField,
+                      // 검색 결과는 항상 첫 장부터 본다 — 남아 있던 페이지 번호를 지운다.
+                      page: 1,
+                    })
+                  }
+                />
+              )
+            }
+            actions={toolbarActions}
+            filter={filter}
+          />
+        )}
+
+        {/* `thead`/`tbody`로 감싸지 않는다 — FoxTable이 스스로 만들고, 머리행은 `head`만 보고
+            제자리로 보낸다. 감싸면 표 안에 표 구조가 이중으로 생긴다. */}
+        <FoxTable type="list" caption={caption}>
+          <FoxTableRow head>
+            {columns.map((column) => (
+              <FoxTableCell
+                key={column.key}
+                head
+                width={column.width}
+                align={column.align}
+              >
+                {column.header}
+              </FoxTableCell>
+            ))}
+          </FoxTableRow>
+
+          {rows.length === 0 ? (
+            <FoxTableRow>
+              <FoxTableCell colSpan={columns.length} align="center">
+                {emptyMessage}
+              </FoxTableCell>
+            </FoxTableRow>
+          ) : (
+            rows.map((row, index) => (
+              <FoxTableRow key={rowKey(row, index)}>
+                {columns.map((column) => (
+                  <FoxTableCell
+                    key={column.key}
+                    align={column.align}
+                    emphasis={column.emphasis}
+                    label={
+                      typeof column.header === "string"
+                        ? column.header
+                        : undefined
+                    }
+                  >
+                    {column.render
+                      ? column.render(row, index)
+                      : String(
+                          (row as Record<string, unknown>)[column.key] ?? ""
+                        )}
+                  </FoxTableCell>
+                ))}
+              </FoxTableRow>
+            ))
+          )}
+        </FoxTable>
+
+        {totalPages > 1 && (
+          <FoxPagination
+            currentPage={page}
+            totalPages={totalPages}
+            buildHref={buildHref ? (next) => buildHref({ page: next }) : undefined}
+            onPageChange={
+              buildHref ? undefined : (next) => onQueryChange?.({ page: next })
+            }
+          />
+        )}
+      </section>
+    </div>
+  );
+}
 
@fox/core/components/fox-list-container/index.ts (added)
+++ @fox/core/components/fox-list-container/index.ts
@@ -0,0 +1,1 @@
+export * from "./fox-list-container";
 
@fox/core/components/fox-list-search/fox-list-search.tsx (added)
+++ @fox/core/components/fox-list-search/fox-list-search.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { useState, type FormEvent } from "react";
+import { cx } from "../../utils";
+import { FoxSelectText } from "../fox-select-text";
+import type { FoxSelectItem } from "../fox-select";
+import { FoxMagnifyingGlassIcon } from "../../icons";
+
+export interface FoxListSearchProps {
+  /** 앞쪽 셀렉트에 넣을 검색 대상. 비우면 셀렉트 자체가 나오지 않는다. */
+  fields?: FoxSelectItem[];
+  /** 선택된 검색 대상(제어). */
+  field?: string;
+  onFieldChange?: (field: string) => void;
+  /** 입력값(제어). 주지 않으면 컴포넌트가 스스로 들고 있다. */
+  keyword?: string;
+  onKeywordChange?: (keyword: string) => void;
+  /** 돋보기를 누르거나 Enter를 쳤을 때. 현재 검색어와 대상을 함께 넘긴다. */
+  onSearch?: (keyword: string, field?: string) => void;
+  placeholder?: string;
+  /** 글자가 없으므로 이 값이 검색 버튼의 접근성 이름이 된다. */
+  label?: string;
+  disabled?: boolean;
+  hidden?: boolean;
+  className?: string;
+}
+
+/**
+ * @fox 목록 검색 상자 — 시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) list-toolbar의 search
+ *
+ * 마크업 계약 (React 밖 소비자용):
+ *   <form class="fox-list-search">
+ *     <div class="fox-list-search__box">
+ *       <span class="fox-list-search__field">…select-txt…</span>   <!-- 선택 -->
+ *       <input class="fox-list-search__input">
+ *       <button class="fox-list-search__submit">…svg…</button>
+ *     </div>
+ *   </form>
+ *
+ * `FoxInput`을 쓰지 않는다 — 시안은 입력칸 **앞쪽**에 검색 대상 셀렉트가 들어가는데 FoxInput에
+ * 그 슬롯이 없다. 크기·radius·테두리는 시안이 지정한 값이 FoxInput md와 같지만, 좌우 여백만
+ * `form/padding/sm`으로 더 좁다(안에 셀렉트와 버튼이 들어가기 때문이다).
+ *
+ * `<form>`이라 Enter가 그대로 제출이 된다 — 키 이벤트를 따로 듣지 않는다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxListSearch({
+  fields,
+  field,
+  onFieldChange,
+  keyword,
+  onKeywordChange,
+  onSearch,
+  placeholder = "검색어를 입력하세요.",
+  label = "검색",
+  disabled = false,
+  hidden = false,
+  className,
+}: FoxListSearchProps) {
+  const [innerKeyword, setInnerKeyword] = useState(keyword ?? "");
+  const controlled = keyword !== undefined;
+  const value = controlled ? keyword : innerKeyword;
+
+  if (hidden) {
+    return null;
+  }
+
+  function handleChange(next: string) {
+    if (!controlled) {
+      setInnerKeyword(next);
+    }
+    onKeywordChange?.(next);
+  }
+
+  function handleSubmit(event: FormEvent<HTMLFormElement>) {
+    event.preventDefault();
+    onSearch?.(value, field);
+  }
+
+  return (
+    <form
+      className={cx("fox-list-search", className)}
+      role="search"
+      onSubmit={handleSubmit}
+    >
+      <div className="fox-list-search__box">
+        {fields && fields.length > 0 && (
+          <span className="fox-list-search__field">
+            <FoxSelectText
+              size="sm"
+              options={fields}
+              value={field}
+              onValueChange={onFieldChange}
+              ariaLabel="검색 대상"
+              disabled={disabled}
+            />
+          </span>
+        )}
+        <input
+          className="fox-list-search__input"
+          type="search"
+          value={value}
+          placeholder={placeholder}
+          disabled={disabled}
+          aria-label={placeholder}
+          onChange={(event) => handleChange(event.target.value)}
+        />
+        {/* 시안의 이 버튼은 btn-ico 인스턴스가 아니라 로컬 프레임(32×32)이라 FoxIconButton의
+            크기 체계(24·28·40·56)와 맞지 않는다. 그래서 직접 그린다. */}
+        <button
+          className="fox-list-search__submit"
+          type="submit"
+          aria-label={label}
+          disabled={disabled}
+        >
+          <FoxMagnifyingGlassIcon />
+        </button>
+      </div>
+    </form>
+  );
+}
 
@fox/core/components/fox-list-search/index.ts (added)
+++ @fox/core/components/fox-list-search/index.ts
@@ -0,0 +1,1 @@
+export * from "./fox-list-search";
 
@fox/core/components/fox-list-toolbar/fox-list-toolbar.tsx (added)
+++ @fox/core/components/fox-list-toolbar/fox-list-toolbar.tsx
@@ -0,0 +1,98 @@
+import type { ReactNode } from "react";
+import { cx } from "../../utils";
+
+export interface FoxListToolbarProps {
+  /** 좌측 첫 자리. 보통 "총 N건"이다 — `FoxListCount`가 시안 서식을 그린다. */
+  count?: ReactNode;
+  /** 총 건수 옆 액션(전체 선택·선택 삭제 등). */
+  leading?: ReactNode;
+  /** 우측 정렬·조건 셀렉트 묶음. */
+  sorts?: ReactNode;
+  /** 우측 검색 상자. */
+  search?: ReactNode;
+  /** 우측 끝 액션(등록·엑셀 등). */
+  actions?: ReactNode;
+  /** 아래 줄의 필터. 주지 않으면 그 줄 자체가 렌더되지 않는다(구분선도 함께 사라진다). */
+  filter?: ReactNode;
+  hidden?: boolean;
+  className?: string;
+}
+
+/**
+ * @fox 목록 도구 줄 — 시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) list-toolbar (3002:8151)
+ *
+ * 마크업 계약 (React 밖 소비자용):
+ *   <div class="fox-list-toolbar">
+ *     <div class="fox-list-toolbar__base">
+ *       <div class="fox-list-toolbar__left">…총 N건 · 액션…</div>
+ *       <div class="fox-list-toolbar__right">…셀렉트 · 검색 · 액션…</div>
+ *     </div>
+ *     <div class="fox-list-toolbar__filter">…필터…</div>   <!-- 선택 -->
+ *   </div>
+ *
+ * **자리만 잡는다.** 무엇이 들어갈지는 화면마다 다르므로 전부 슬롯으로 받는다 — 셀렉트가
+ * 몇 개인지, 액션이 버튼인지 아이콘인지 이 컴포넌트는 알지 못한다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxListToolbar({
+  count,
+  leading,
+  sorts,
+  search,
+  actions,
+  filter,
+  hidden = false,
+  className,
+}: FoxListToolbarProps) {
+  if (hidden) {
+    return null;
+  }
+
+  return (
+    <div className={cx("fox-list-toolbar", className)}>
+      <div className="fox-list-toolbar__base">
+        <div className="fox-list-toolbar__left">
+          {count}
+          {leading}
+        </div>
+        <div className="fox-list-toolbar__right">
+          {sorts && <div className="fox-list-toolbar__sorts">{sorts}</div>}
+          {search}
+          {actions}
+        </div>
+      </div>
+
+      {filter && <div className="fox-list-toolbar__filter">{filter}</div>}
+    </div>
+  );
+}
+
+export interface FoxListCountProps {
+  /** 전체 건수. */
+  total: number;
+  /** 숫자 앞뒤 문구. 기본은 시안의 "총 N건". */
+  prefix?: ReactNode;
+  suffix?: ReactNode;
+  className?: string;
+}
+
+/**
+ * 총 건수 표시 — 시안이 숫자만 굵고 다른 색이라 별도 조각으로 둔다. 천 단위 구분은
+ * 호출부의 로케일을 타지 않도록 `toLocaleString("ko-KR")`로 고정한다.
+ */
+export function FoxListCount({
+  total,
+  prefix = "총 ",
+  suffix = "건",
+  className,
+}: FoxListCountProps) {
+  return (
+    <p className={cx("fox-list-count", className)}>
+      {prefix}
+      <b className="fox-list-count__value">{total.toLocaleString("ko-KR")}</b>
+      {suffix}
+    </p>
+  );
+}
 
@fox/core/components/fox-list-toolbar/index.ts (added)
+++ @fox/core/components/fox-list-toolbar/index.ts
@@ -0,0 +1,1 @@
+export * from "./fox-list-toolbar";
@fox/core/components/index.ts
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
@@ -52,3 +52,6 @@
 export * from "./fox-text-button";
 export * from "./fox-toggle-quantity";
 export * from "./fox-toggle-switch";
+export * from "./fox-list-search";
+export * from "./fox-list-toolbar";
+export * from "./fox-list-container";
 
@fox/styles/_fox-list-container.scss (added)
+++ @fox/styles/_fox-list-container.scss
@@ -0,0 +1,26 @@
+// FoxListContainer — 시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) container (3019:8608)
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-list-container">
+//     …fox-page-header…
+//     <section class="fox-list-container__section">
+//       …fox-list-toolbar… …fox-table… …fox-pagination…
+//     </section>
+//   </div>
+//
+// 세로로만 쌓는다 — 도구 줄과 페이지네이션이 각자 자기 여백을 갖고 있어 여기서 간격을 더하지
+// 않는다(더하면 시안보다 벌어진다).
+
+@use "abstracts" as fox;
+
+.fox-list-container {
+  display: flex;
+  inline-size: 100%;
+  flex-direction: column;
+
+  &__section {
+    display: flex;
+    inline-size: 100%;
+    flex-direction: column;
+  }
+}
 
@fox/styles/_fox-list-search.scss (added)
+++ @fox/styles/_fox-list-search.scss
@@ -0,0 +1,82 @@
+// FoxListSearch — 시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) list-toolbar의 search (5111:4099)
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <form class="fox-list-search" role="search">
+//     <div class="fox-list-search__box">
+//       <span class="fox-list-search__field">…select-txt…</span>   <!-- 선택 -->
+//       <input class="fox-list-search__input">
+//       <button class="fox-list-search__submit">…svg…</button>
+//     </div>
+//   </form>
+//
+// 상자 규격은 FoxInput md와 같지만(높이 40 · radius 8 · 테두리 input-border) 좌우 여백만
+// `form/padding/sm`으로 좁다 — 안에 셀렉트와 버튼이 들어가기 때문이다.
+//
+// 토큰이 아닌 값:
+//   - 폭 320, 제출 버튼 32: 시안 값이나 Figma가 변수로 export하지 않는다.
+
+@use "abstracts" as fox;
+@use "form-field" as field;
+
+.fox-list-search {
+  display: flex;
+  flex-shrink: 0;
+  align-items: center;
+  gap: fox.gap(3);
+  // 시안 값이나 변수로 export되지 않는다.
+  inline-size: 32rem;
+
+  &__box {
+    display: flex;
+    inline-size: 100%;
+    align-items: center;
+    gap: fox.gap(2);
+    block-size: fox.form(height-md);
+    padding-inline: fox.form(padding-sm);
+    border: fox.border(1) solid fox.color(input-border);
+    border-radius: fox.form(radius-md);
+    background: fox.color(input-surface);
+  }
+
+  &__field {
+    display: inline-flex;
+    flex-shrink: 0;
+    align-items: center;
+  }
+
+  &__input {
+    @include field.field(label-md);
+
+    // 네이티브 `search`가 붙이는 기본 장식을 지운다 — 지우기 X는 제출 버튼과 겹친다.
+    &::-webkit-search-cancel-button,
+    &::-webkit-search-decoration {
+      appearance: none;
+    }
+  }
+
+  &__submit {
+    display: inline-flex;
+    flex-shrink: 0;
+    align-items: center;
+    justify-content: center;
+    // 시안 값이나 변수로 export되지 않는다.
+    inline-size: 3.2rem;
+    block-size: 3.2rem;
+    padding: 0;
+    border: none;
+    border-radius: fox.form(radius-xsm);
+    background: none;
+    color: fox.color(icon-neutral-default);
+    cursor: pointer;
+
+    &:disabled {
+      color: fox.color(font-neutral-disabled-strong);
+      cursor: default;
+    }
+  }
+
+  &__submit > svg {
+    inline-size: fox.form(icon-md);
+    block-size: fox.form(icon-md);
+  }
+}
 
@fox/styles/_fox-list-toolbar.scss (added)
+++ @fox/styles/_fox-list-toolbar.scss
@@ -0,0 +1,82 @@
+// FoxListToolbar — 시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) list-toolbar (3002:8151)
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-list-toolbar">
+//     <div class="fox-list-toolbar__base">
+//       <div class="fox-list-toolbar__left">…</div>
+//       <div class="fox-list-toolbar__right">…</div>
+//     </div>
+//     <div class="fox-list-toolbar__filter">…</div>   <!-- 선택 -->
+//   </div>
+//
+// 좌우가 각각 `flex: 1`이라 반씩 나눠 갖고, 우측 내용은 오른쪽 끝으로 몰린다.
+//
+// 토큰이 아닌 값:
+//   - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로 export하지 않는다.
+
+@use "abstracts" as fox;
+
+.fox-list-toolbar {
+  display: flex;
+  inline-size: 100%;
+  flex-direction: column;
+  align-items: flex-start;
+  padding-block-end: fox.spacing(bottom-md);
+
+  &__base {
+    display: flex;
+    inline-size: 100%;
+    flex-shrink: 0;
+    align-items: center;
+  }
+
+  &__left,
+  &__right {
+    display: flex;
+    flex: 1 0 0;
+    min-inline-size: 0;
+    align-items: center;
+    gap: fox.gap(4);
+  }
+
+  &__right {
+    justify-content: flex-end;
+  }
+
+  &__sorts {
+    display: flex;
+    flex-shrink: 0;
+    align-items: center;
+    gap: fox.gap(3);
+  }
+
+  // 아래 줄 — 위 줄과 가로 구분선으로 나뉜다.
+  &__filter {
+    display: flex;
+    inline-size: 100%;
+    flex-shrink: 0;
+    align-items: center;
+    gap: fox.gap(3);
+    margin-block-start: fox.padding(5);
+    padding-block-start: fox.padding(5);
+    border-block-start: fox.border(1) solid fox.color(border-neutral-subtler);
+  }
+}
+
+// 총 건수 — 숫자만 굵고 색이 다르다.
+.fox-list-count {
+  flex-shrink: 0;
+  margin: 0;
+  color: fox.color(font-neutral-default);
+  font-family: fox.font-family(body);
+  font-size: fox.font-size(label-md);
+  font-weight: fox.font-weight(regular);
+  line-height: 1;
+  letter-spacing: -0.025em;
+  white-space: nowrap;
+
+  &__value {
+    color: fox.color(font-theme-primary);
+    font-weight: fox.font-weight(bold);
+  }
+}
@fox/styles/components.scss
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
@@ -55,3 +55,6 @@
 @use "fox-modal";
 @use "fox-accordion";
 @use "fox-card";
+@use "fox-list-search";
+@use "fox-list-toolbar";
+@use "fox-list-container";
Add a comment
List