Merge branch 'claude/design-system-setup-46d8aa' into hub
@9e7030ac65113d8b6eeca5ed003f7361ba7a0771
+++ @fox/core/components/fox-carousel-area/fox-carousel-area.tsx
... | ... | @@ -0,0 +1,30 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import type { ReactNode } from "react"; | |
| 4 | +import { cx } from "../../utils"; | |
| 5 | + | |
| 6 | +export interface FoxCarouselAreaProps { | |
| 7 | + /** 위치 표시와 조작 버튼을 넣는다 — 시안은 [표시][이전][다음][재생·정지 또는 더보기]다. */ | |
| 8 | + children: ReactNode; | |
| 9 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 10 | + hidden?: boolean; | |
| 11 | + /** 배치 조정용. */ | |
| 12 | + className?: string; | |
| 13 | +} | |
| 14 | + | |
| 15 | +/** | |
| 16 | + * @fox 캐러셀 조작 묶음. 위치 표시와 버튼을 한 줄에 같은 간격으로 세운다. | |
| 17 | + * | |
| 18 | + * 무엇이 들어갈지는 정하지 않는다 — 시안에서도 조합이 두 가지(점+이전·다음·정지 / | |
| 19 | + * 숫자+이전·다음·더보기)라 담는 그릇만 갖는다. | |
| 20 | + * | |
| 21 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 22 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 23 | + */ | |
| 24 | +export function FoxCarouselArea({ children, hidden = false, className }: FoxCarouselAreaProps) { | |
| 25 | + if (hidden) { | |
| 26 | + return null; | |
| 27 | + } | |
| 28 | + | |
| 29 | + return <div className={cx("fox-carousel-area", className)}>{children}</div>; | |
| 30 | +} |
+++ @fox/core/components/fox-carousel-area/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxCarouselArea, type FoxCarouselAreaProps } from "./fox-carousel-area"; |
+++ @fox/core/components/fox-carousel-indicator/fox-carousel-indicator.tsx
... | ... | @@ -0,0 +1,98 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { cx } from "../../utils"; | |
| 4 | + | |
| 5 | +/** `dot`은 시안 carousel-dot, `number`는 carousel-number다. */ | |
| 6 | +export type FoxCarouselIndicatorType = "dot" | "number"; | |
| 7 | + | |
| 8 | +export interface FoxCarouselIndicatorProps { | |
| 9 | + type?: FoxCarouselIndicatorType; | |
| 10 | + /** 1부터 센다. */ | |
| 11 | + current: number; | |
| 12 | + total: number; | |
| 13 | + /** | |
| 14 | + * 점을 눌러 옮길 수 있게 한다. 넘기지 않으면 표시만 하는 조각이라 누를 곳이 없다. | |
| 15 | + * `number`형에는 누를 점이 없어 쓰이지 않는다. | |
| 16 | + */ | |
| 17 | + onSelect?: (page: number) => void; | |
| 18 | + /** 스크린리더가 읽을 이름. */ | |
| 19 | + label?: string; | |
| 20 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 21 | + hidden?: boolean; | |
| 22 | + /** 배치 조정용. */ | |
| 23 | + className?: string; | |
| 24 | +} | |
| 25 | + | |
| 26 | +/** | |
| 27 | + * @fox 캐러셀 현재 위치 표시. 흰 알약 안에 점 또는 숫자가 들어간다. | |
| 28 | + * | |
| 29 | + * 시안의 carousel-dot과 carousel-number를 하나로 둔다 — 껍데기(흰 알약·높이·좌우 여백· | |
| 30 | + * 간격)가 같고 안에 든 것만 다르다. | |
| 31 | + * | |
| 32 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 33 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 34 | + */ | |
| 35 | +export function FoxCarouselIndicator({ | |
| 36 | + type = "dot", | |
| 37 | + current, | |
| 38 | + total, | |
| 39 | + onSelect, | |
| 40 | + label = "현재 위치", | |
| 41 | + hidden = false, | |
| 42 | + className, | |
| 43 | +}: FoxCarouselIndicatorProps) { | |
| 44 | + if (hidden || total < 1) { | |
| 45 | + return null; | |
| 46 | + } | |
| 47 | + | |
| 48 | + const safe = Math.min(Math.max(current, 1), total); | |
| 49 | + | |
| 50 | + if (type === "number") { | |
| 51 | + return ( | |
| 52 | + <p | |
| 53 | + className={cx("fox-carousel-indicator", "fox-carousel-indicator--number", className)} | |
| 54 | + aria-label={`${label} ${safe} / ${total}`} | |
| 55 | + > | |
| 56 | + <span className="fox-carousel-indicator__current" aria-hidden="true"> | |
| 57 | + {safe} | |
| 58 | + </span> | |
| 59 | + <span className="fox-carousel-indicator__separator" aria-hidden="true" /> | |
| 60 | + <span className="fox-carousel-indicator__total" aria-hidden="true"> | |
| 61 | + {total} | |
| 62 | + </span> | |
| 63 | + </p> | |
| 64 | + ); | |
| 65 | + } | |
| 66 | + | |
| 67 | + const dots = Array.from({ length: total }, (unused, index) => index + 1); | |
| 68 | + | |
| 69 | + return ( | |
| 70 | + <div | |
| 71 | + className={cx("fox-carousel-indicator", "fox-carousel-indicator--dot", className)} | |
| 72 | + role="group" | |
| 73 | + aria-label={`${label} ${safe} / ${total}`} | |
| 74 | + > | |
| 75 | + {dots.map((page) => { | |
| 76 | + const active = page === safe; | |
| 77 | + const dotClass = cx( | |
| 78 | + "fox-carousel-indicator__dot", | |
| 79 | + active && "fox-carousel-indicator__dot--active" | |
| 80 | + ); | |
| 81 | + | |
| 82 | + // 옮길 수 없으면 버튼으로 두지 않는다 — 눌리지 않는 버튼이 초점을 먹는다. | |
| 83 | + return onSelect ? ( | |
| 84 | + <button | |
| 85 | + key={page} | |
| 86 | + className={dotClass} | |
| 87 | + type="button" | |
| 88 | + aria-label={`${page}번째로 이동`} | |
| 89 | + aria-current={active || undefined} | |
| 90 | + onClick={() => onSelect(page)} | |
| 91 | + /> | |
| 92 | + ) : ( | |
| 93 | + <span key={page} className={dotClass} /> | |
| 94 | + ); | |
| 95 | + })} | |
| 96 | + </div> | |
| 97 | + ); | |
| 98 | +} |
+++ @fox/core/components/fox-carousel-indicator/index.ts
... | ... | @@ -0,0 +1,5 @@ |
| 1 | +export { | |
| 2 | + FoxCarouselIndicator, | |
| 3 | + type FoxCarouselIndicatorProps, | |
| 4 | + type FoxCarouselIndicatorType, | |
| 5 | +} from "./fox-carousel-indicator"; |
+++ @fox/core/components/fox-pagination/fox-pagination.tsx
... | ... | @@ -0,0 +1,177 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import type { ReactNode } from "react"; | |
| 4 | +import { cx } from "../../utils"; | |
| 5 | +import { FoxCaretLeftIcon, FoxCaretRightIcon } from "../../icons"; | |
| 6 | + | |
| 7 | +export interface FoxPaginationProps { | |
| 8 | + /** 1부터 센다. */ | |
| 9 | + currentPage: number; | |
| 10 | + totalPages: number; | |
| 11 | + onPageChange?: (page: number) => void; | |
| 12 | + /** | |
| 13 | + * 페이지 번호를 링크로 만들 때. 주면 `<a>`가 되고, 없으면 `<button>`이 된다. | |
| 14 | + * 라우팅은 호출부 몫이다. | |
| 15 | + */ | |
| 16 | + buildHref?: (page: number) => string; | |
| 17 | + /** 한 번에 보여줄 번호 개수. 기본 5. */ | |
| 18 | + windowSize?: number; | |
| 19 | + /** | |
| 20 | + * 창 밖에 페이지가 더 있으면 `…`을 붙인다. 시안의 pagination-item에 있는 모양이다. | |
| 21 | + * 기본값은 꺼짐 — 시안의 pagination 조합에는 없다. | |
| 22 | + */ | |
| 23 | + ellipsis?: boolean; | |
| 24 | + | |
| 25 | + previousLabel?: string; | |
| 26 | + nextLabel?: string; | |
| 27 | + /** 스크린리더가 읽을 이름. 한 화면에 둘 이상 두면 서로 다르게 준다. */ | |
| 28 | + label?: string; | |
| 29 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 30 | + hidden?: boolean; | |
| 31 | + /** 배치 조정용. */ | |
| 32 | + className?: string; | |
| 33 | +} | |
| 34 | + | |
| 35 | +/** 현재 페이지를 가운데 두고 `size`개를 [1, total] 안에서 민다. */ | |
| 36 | +function pageWindow(current: number, total: number, size: number): number[] { | |
| 37 | + if (total < 1) { | |
| 38 | + return []; | |
| 39 | + } | |
| 40 | + const start = Math.max(1, Math.min(current - Math.floor(size / 2), total - size + 1)); | |
| 41 | + const end = Math.min(total, start + size - 1); | |
| 42 | + | |
| 43 | + const pages: number[] = []; | |
| 44 | + for (let page = Math.max(1, start); page <= end; page += 1) { | |
| 45 | + pages.push(page); | |
| 46 | + } | |
| 47 | + return pages; | |
| 48 | +} | |
| 49 | + | |
| 50 | +/** | |
| 51 | + * @fox 페이지네이션. | |
| 52 | + * | |
| 53 | + * PC는 [이전] [번호들] [다음]이 한 줄이고, 모바일은 이전·다음이 위로 빠지고 번호가 아래 | |
| 54 | + * 줄에 선다(시안의 device 축). 그 전환은 CSS가 화면 폭을 보고 하므로 프롭이 없다. | |
| 55 | + * | |
| 56 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 57 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 58 | + */ | |
| 59 | +export function FoxPagination({ | |
| 60 | + currentPage, | |
| 61 | + totalPages, | |
| 62 | + onPageChange, | |
| 63 | + buildHref, | |
| 64 | + windowSize = 5, | |
| 65 | + ellipsis = false, | |
| 66 | + previousLabel = "이전", | |
| 67 | + nextLabel = "다음", | |
| 68 | + label = "페이지", | |
| 69 | + hidden = false, | |
| 70 | + className, | |
| 71 | +}: FoxPaginationProps) { | |
| 72 | + if (hidden || totalPages < 1) { | |
| 73 | + return null; | |
| 74 | + } | |
| 75 | + | |
| 76 | + const pages = pageWindow(currentPage, totalPages, windowSize); | |
| 77 | + const first = pages[0] ?? 1; | |
| 78 | + const last = pages[pages.length - 1] ?? 1; | |
| 79 | + | |
| 80 | + const step = (target: number, disabled: boolean, direction: "prev" | "next", text: string) => { | |
| 81 | + const icon = ( | |
| 82 | + <span className="fox-pagination__step-icon" aria-hidden="true"> | |
| 83 | + {direction === "prev" ? <FoxCaretLeftIcon /> : <FoxCaretRightIcon />} | |
| 84 | + </span> | |
| 85 | + ); | |
| 86 | + const body: ReactNode = | |
| 87 | + direction === "prev" ? ( | |
| 88 | + <> | |
| 89 | + {icon} | |
| 90 | + {text} | |
| 91 | + </> | |
| 92 | + ) : ( | |
| 93 | + <> | |
| 94 | + {text} | |
| 95 | + {icon} | |
| 96 | + </> | |
| 97 | + ); | |
| 98 | + const itemClass = cx("fox-pagination__item", "fox-pagination__step"); | |
| 99 | + | |
| 100 | + // 갈 곳이 없으면 링크로 두지 않는다 — 눌리지 않는 링크는 스크린리더에서도 걸린다. | |
| 101 | + if (disabled) { | |
| 102 | + return ( | |
| 103 | + <span className={itemClass} aria-disabled="true"> | |
| 104 | + {body} | |
| 105 | + </span> | |
| 106 | + ); | |
| 107 | + } | |
| 108 | + return buildHref ? ( | |
| 109 | + <a className={itemClass} href={buildHref(target)} onClick={() => onPageChange?.(target)}> | |
| 110 | + {body} | |
| 111 | + </a> | |
| 112 | + ) : ( | |
| 113 | + <button className={itemClass} type="button" onClick={() => onPageChange?.(target)}> | |
| 114 | + {body} | |
| 115 | + </button> | |
| 116 | + ); | |
| 117 | + }; | |
| 118 | + | |
| 119 | + const number = (page: number) => { | |
| 120 | + const current = page === currentPage; | |
| 121 | + const itemClass = cx( | |
| 122 | + "fox-pagination__item", | |
| 123 | + "fox-pagination__number", | |
| 124 | + current && "fox-pagination__number--current" | |
| 125 | + ); | |
| 126 | + | |
| 127 | + if (current) { | |
| 128 | + return ( | |
| 129 | + <span className={itemClass} aria-current="page"> | |
| 130 | + {page} | |
| 131 | + </span> | |
| 132 | + ); | |
| 133 | + } | |
| 134 | + return buildHref ? ( | |
| 135 | + <a | |
| 136 | + className={itemClass} | |
| 137 | + href={buildHref(page)} | |
| 138 | + aria-label={`${page}페이지`} | |
| 139 | + onClick={() => onPageChange?.(page)} | |
| 140 | + > | |
| 141 | + {page} | |
| 142 | + </a> | |
| 143 | + ) : ( | |
| 144 | + <button | |
| 145 | + className={itemClass} | |
| 146 | + type="button" | |
| 147 | + aria-label={`${page}페이지`} | |
| 148 | + onClick={() => onPageChange?.(page)} | |
| 149 | + > | |
| 150 | + {page} | |
| 151 | + </button> | |
| 152 | + ); | |
| 153 | + }; | |
| 154 | + | |
| 155 | + const more = ( | |
| 156 | + <span className="fox-pagination__item fox-pagination__ellipsis" aria-hidden="true"> | |
| 157 | + … | |
| 158 | + </span> | |
| 159 | + ); | |
| 160 | + | |
| 161 | + // 이전·목록·다음이 형제다. 모바일에서 목록만 한 줄을 통째로 차지해 아래로 밀린다. | |
| 162 | + return ( | |
| 163 | + <nav className={cx("fox-pagination", className)} aria-label={label}> | |
| 164 | + {step(currentPage - 1, currentPage <= 1, "prev", previousLabel)} | |
| 165 | + | |
| 166 | + <ul className="fox-pagination__list"> | |
| 167 | + {ellipsis && first > 1 && <li>{more}</li>} | |
| 168 | + {pages.map((page) => ( | |
| 169 | + <li key={page}>{number(page)}</li> | |
| 170 | + ))} | |
| 171 | + {ellipsis && last < totalPages && <li>{more}</li>} | |
| 172 | + </ul> | |
| 173 | + | |
| 174 | + {step(currentPage + 1, currentPage >= totalPages, "next", nextLabel)} | |
| 175 | + </nav> | |
| 176 | + ); | |
| 177 | +} |
+++ @fox/core/components/fox-pagination/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxPagination, type FoxPaginationProps } from "./fox-pagination"; |
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -1,13 +1,18 @@ |
| 1 | 1 |
export * from "./fox-address"; |
| 2 |
+export * from "./fox-breadcrumb"; |
|
| 2 | 3 |
export * from "./fox-button"; |
| 3 | 4 |
export * from "./fox-button-group"; |
| 4 | 5 |
export * from "./fox-button-panel"; |
| 6 |
+export * from "./fox-carousel-area"; |
|
| 7 |
+export * from "./fox-carousel-button"; |
|
| 8 |
+export * from "./fox-carousel-indicator"; |
|
| 5 | 9 |
export * from "./fox-certified"; |
| 6 | 10 |
export * from "./fox-email"; |
| 7 | 11 |
export * from "./fox-file-upload"; |
| 8 | 12 |
export * from "./fox-icon-button"; |
| 9 | 13 |
export * from "./fox-input"; |
| 10 | 14 |
export * from "./fox-link-button"; |
| 15 |
+export * from "./fox-pagination"; |
|
| 11 | 16 |
export * from "./fox-phone-number"; |
| 12 | 17 |
export * from "./fox-select"; |
| 13 | 18 |
export * from "./fox-select-option"; |
+++ @fox/core/icons/icons.tsx
... | ... | @@ -0,0 +1,83 @@ |
| 1 | +// @fox 아이콘 — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)의 `ico/*` 컴포넌트 | |
| 2 | +// | |
| 3 | +// Figma export 자산의 path를 그대로 옮겼다. 손으로 그리지 않았으므로 굵기·곡률이 시안과 같다. | |
| 4 | +// 시안은 아이콘을 정사각 박스(12·16·24·32px) 안에 여백을 두고 앉히는데, 그 여백까지 | |
| 5 | +// `transform: translate`로 재현했다 — 박스만 맞추고 path를 늘리면 굵기가 어긋난다. | |
| 6 | +// | |
| 7 | +// 크기는 갖지 않는다. 감싼 쪽이 `inline-size`/`block-size`로 정하고 `viewBox`가 따라 늘어난다. | |
| 8 | +// 색도 갖지 않는다 — `fill: currentColor`라 감싼 쪽의 `color`를 쓴다. | |
| 9 | +// | |
| 10 | +// CaretLeft/CaretRight는 두 벌이다. 시안이 브레드크럼·페이지네이션에는 Regular 굵기를, | |
| 11 | +// 캐러셀 버튼에는 Bold 굵기를 물려 놓았다(같은 아이콘의 다른 variant). | |
| 12 | + | |
| 13 | +/** 시안 ico/House. 아이콘 박스 16px 안에서의 위치까지 그대로 옮겼다. */ | |
| 14 | +export function FoxHouseIcon() { | |
| 15 | + return ( | |
| 16 | + <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> | |
| 17 | + <path transform="translate(2 1.4992)" d="M11.7069 5.29266L6.70688 0.292662C6.51936 0.105267 6.26511 0 6.00001 0C5.7349 0 5.48065 0.105267 5.29313 0.292662L0.293131 5.29266C0.199822 5.38528 0.125855 5.49552 0.0755335 5.61698C0.0252116 5.73844 -0.00046166 5.86869 6.28343e-06 6.00016V12.0002C6.28343e-06 12.1328 0.0526848 12.2599 0.146453 12.3537C0.240221 12.4475 0.367398 12.5002 0.500006 12.5002H4.50001C4.63261 12.5002 4.75979 12.4475 4.85356 12.3537C4.94733 12.2599 5.00001 12.1328 5.00001 12.0002V8.50016H7.00001V12.0002C7.00001 12.1328 7.05268 12.2599 7.14645 12.3537C7.24022 12.4475 7.3674 12.5002 7.50001 12.5002H11.5C11.6326 12.5002 11.7598 12.4475 11.8536 12.3537C11.9473 12.2599 12 12.1328 12 12.0002V6.00016C12.0005 5.86869 11.9748 5.73844 11.9245 5.61698C11.8742 5.49552 11.8002 5.38528 11.7069 5.29266ZM11 11.5002H8.00001V8.00016C8.00001 7.86755 7.94733 7.74038 7.85356 7.64661C7.75979 7.55284 7.63261 7.50016 7.50001 7.50016H4.50001C4.3674 7.50016 4.24022 7.55284 4.14645 7.64661C4.05268 7.74038 4.00001 7.86755 4.00001 8.00016V11.5002H1.00001V6.00016L6.00001 1.00016L11 6.00016V11.5002Z" /> | |
| 18 | + </svg> | |
| 19 | + ); | |
| 20 | +} | |
| 21 | + | |
| 22 | +/** 시안 ico/CaretLeft. 아이콘 박스 16px 안에서의 위치까지 그대로 옮겼다. */ | |
| 23 | +export function FoxCaretLeftIcon() { | |
| 24 | + return ( | |
| 25 | + <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> | |
| 26 | + <path transform="translate(4.4992 2.4992)" d="M5.85414 10.1465C5.9006 10.193 5.93745 10.2481 5.96259 10.3088C5.98773 10.3695 6.00067 10.4346 6.00067 10.5003C6.00067 10.566 5.98773 10.631 5.96259 10.6917C5.93745 10.7524 5.9006 10.8076 5.85414 10.854C5.80769 10.9005 5.75254 10.9373 5.69184 10.9625C5.63115 10.9876 5.56609 11.0006 5.50039 11.0006C5.4347 11.0006 5.36964 10.9876 5.30895 10.9625C5.24825 10.9373 5.1931 10.9005 5.14664 10.854L0.146643 5.85403C0.100155 5.80759 0.0632757 5.75245 0.0381136 5.69175C0.0129514 5.63105 0 5.56599 0 5.50028C0 5.43457 0.0129514 5.36951 0.0381136 5.30881C0.0632757 5.24811 0.100155 5.19296 0.146643 5.14653L5.14664 0.146528C5.24046 0.0527077 5.36771 -2.61548e-09 5.50039 0C5.63308 2.61548e-09 5.76032 0.0527077 5.85414 0.146528C5.94796 0.240348 6.00067 0.367596 6.00067 0.500278C6.00067 0.63296 5.94796 0.760208 5.85414 0.854028L1.20727 5.50028L5.85414 10.1465Z" /> | |
| 27 | + </svg> | |
| 28 | + ); | |
| 29 | +} | |
| 30 | + | |
| 31 | +/** 시안 ico/CaretRight. 아이콘 박스 16px 안에서의 위치까지 그대로 옮겼다. */ | |
| 32 | +export function FoxCaretRightIcon() { | |
| 33 | + return ( | |
| 34 | + <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> | |
| 35 | + <path transform="translate(5.4992 2.4992)" d="M5.85403 5.85403L0.854028 10.854C0.807573 10.9005 0.752423 10.9373 0.691726 10.9625C0.63103 10.9876 0.565975 11.0006 0.500278 11.0006C0.434581 11.0006 0.369526 10.9876 0.30883 10.9625C0.248133 10.9373 0.192983 10.9005 0.146528 10.854C0.100073 10.8076 0.0632225 10.7524 0.0380812 10.6917C0.0129398 10.631 0 10.566 0 10.5003C0 10.4346 0.0129398 10.3695 0.0380812 10.3088C0.0632225 10.2481 0.100073 10.193 0.146528 10.1465L4.7934 5.50028L0.146528 0.854028C0.0527074 0.760208 -9.88558e-10 0.63296 0 0.500278C9.88559e-10 0.367596 0.0527074 0.240348 0.146528 0.146528C0.240348 0.0527077 0.367596 9.88558e-10 0.500278 0C0.63296 -9.88558e-10 0.760208 0.0527077 0.854028 0.146528L5.85403 5.14653C5.90052 5.19296 5.9374 5.24811 5.96256 5.30881C5.98772 5.36951 6.00067 5.43457 6.00067 5.50028C6.00067 5.56599 5.98772 5.63105 5.96256 5.69175C5.9374 5.75245 5.90052 5.80759 5.85403 5.85403Z" /> | |
| 36 | + </svg> | |
| 37 | + ); | |
| 38 | +} | |
| 39 | + | |
| 40 | +/** 시안 ico/CaretLeft(bold). 아이콘 박스 24px 안에서의 위치까지 그대로 옮겼다. */ | |
| 41 | +export function FoxCaretLeftBoldIcon() { | |
| 42 | + return ( | |
| 43 | + <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> | |
| 44 | + <path transform="translate(6.372 3.3744)" d="M9.42464 15.3301C9.63598 15.5414 9.75471 15.8281 9.75471 16.127C9.75471 16.4258 9.63598 16.7125 9.42464 16.9238C9.21329 17.1352 8.92665 17.2539 8.62776 17.2539C8.32888 17.2539 8.04223 17.1352 7.83089 16.9238L0.330887 9.42383C0.226007 9.31931 0.142791 9.19512 0.0860095 9.05837C0.0292284 8.92163 0 8.77502 0 8.62695C0 8.47889 0.0292284 8.33228 0.0860095 8.19553C0.142791 8.05878 0.226007 7.93459 0.330887 7.83008L7.83089 0.330076C8.04223 0.118732 8.32888 -4.45375e-09 8.62776 0C8.92665 4.45375e-09 9.21329 0.118732 9.42464 0.330077C9.63598 0.541421 9.75471 0.828065 9.75471 1.12695C9.75471 1.42584 9.63598 1.71248 9.42464 1.92383L2.72245 8.62601L9.42464 15.3301Z" /> | |
| 45 | + </svg> | |
| 46 | + ); | |
| 47 | +} | |
| 48 | + | |
| 49 | +/** 시안 ico/CaretRight(bold). 아이콘 박스 24px 안에서의 위치까지 그대로 옮겼다. */ | |
| 50 | +export function FoxCaretRightBoldIcon() { | |
| 51 | + return ( | |
| 52 | + <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> | |
| 53 | + <path transform="translate(7.872 3.372)" d="M9.42383 9.42383L1.92383 16.9238C1.71248 17.1352 1.42584 17.2539 1.12695 17.2539C0.828065 17.2539 0.541421 17.1352 0.330076 16.9238C0.118732 16.7125 2.22687e-09 16.4258 0 16.127C-2.22687e-09 15.8281 0.118732 15.5414 0.330076 15.3301L7.03414 8.62789L0.331952 1.92383C0.227304 1.81918 0.144293 1.69495 0.0876589 1.55822C0.0310243 1.42149 0.00187469 1.27494 0.00187469 1.12695C0.00187468 0.978958 0.0310243 0.832414 0.0876589 0.695686C0.144293 0.558958 0.227304 0.434724 0.331952 0.330077C0.436599 0.225429 0.560833 0.142419 0.697561 0.0857842C0.834289 0.0291496 0.980833 -1.10264e-09 1.12883 0C1.27682 1.10264e-09 1.42336 0.0291496 1.56009 0.0857842C1.69682 0.142419 1.82105 0.225429 1.9257 0.330077L9.4257 7.83008C9.53046 7.93472 9.61353 8.05901 9.67015 8.19582C9.72677 8.33263 9.75582 8.47928 9.75565 8.62734C9.75548 8.77541 9.72608 8.92198 9.66913 9.05866C9.61219 9.19534 9.52883 9.31943 9.42383 9.42383Z" /> | |
| 54 | + </svg> | |
| 55 | + ); | |
| 56 | +} | |
| 57 | + | |
| 58 | +/** 시안 ico/Play. 아이콘 박스 24px 안에서의 위치까지 그대로 옮겼다. */ | |
| 59 | +export function FoxPlayIcon() { | |
| 60 | + return ( | |
| 61 | + <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> | |
| 62 | + <path transform="translate(6 2.2488)" d="M16.5 9.75047C16.5006 10.0051 16.4353 10.2556 16.3105 10.4775C16.1856 10.6995 16.0055 10.8853 15.7875 11.017L2.28 19.2802C2.05227 19.4196 1.79144 19.4957 1.52445 19.5007C1.25746 19.5056 0.993989 19.4392 0.76125 19.3083C0.530728 19.1794 0.338696 18.9914 0.204904 18.7637C0.0711107 18.536 0.000385179 18.2768 0 18.0127V1.48828C0.000385179 1.22417 0.0711107 0.964934 0.204904 0.737222C0.338696 0.509511 0.530728 0.321545 0.76125 0.192655C0.993989 0.0617322 1.25746 -0.00468706 1.52445 0.0002572C1.79144 0.00520146 2.05227 0.0813301 2.28 0.220781L15.7875 8.4839C16.0055 8.6156 16.1856 8.80145 16.3105 9.0234C16.4353 9.24534 16.5006 9.49582 16.5 9.75047Z" /> | |
| 63 | + </svg> | |
| 64 | + ); | |
| 65 | +} | |
| 66 | + | |
| 67 | +/** 시안 ico/Pause. 아이콘 박스 24px 안에서의 위치까지 그대로 옮겼다. */ | |
| 68 | +export function FoxPauseIcon() { | |
| 69 | + return ( | |
| 70 | + <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> | |
| 71 | + <path transform="translate(3.7512 3)" d="M16.5 1.5V16.5C16.5 16.8978 16.342 17.2794 16.0607 17.5607C15.7794 17.842 15.3978 18 15 18H11.25C10.8522 18 10.4706 17.842 10.1893 17.5607C9.90804 17.2794 9.75 16.8978 9.75 16.5V1.5C9.75 1.10218 9.90804 0.720644 10.1893 0.43934C10.4706 0.158035 10.8522 0 11.25 0H15C15.3978 0 15.7794 0.158035 16.0607 0.43934C16.342 0.720644 16.5 1.10218 16.5 1.5ZM5.25 0H1.5C1.10218 0 0.720644 0.158035 0.43934 0.43934C0.158035 0.720644 0 1.10218 0 1.5V16.5C0 16.8978 0.158035 17.2794 0.43934 17.5607C0.720644 17.842 1.10218 18 1.5 18H5.25C5.64782 18 6.02936 17.842 6.31066 17.5607C6.59196 17.2794 6.75 16.8978 6.75 16.5V1.5C6.75 1.10218 6.59196 0.720644 6.31066 0.43934C6.02936 0.158035 5.64782 0 5.25 0Z" /> | |
| 72 | + </svg> | |
| 73 | + ); | |
| 74 | +} | |
| 75 | + | |
| 76 | +/** 시안 ico/Plus. 아이콘 박스 24px 안에서의 위치까지 그대로 옮겼다. */ | |
| 77 | +export function FoxPlusIcon() { | |
| 78 | + return ( | |
| 79 | + <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> | |
| 80 | + <path transform="translate(2.6256 2.6256)" d="M18.75 9.375C18.75 9.67337 18.6315 9.95952 18.4205 10.1705C18.2095 10.3815 17.9234 10.5 17.625 10.5H10.5V17.625C10.5 17.9234 10.3815 18.2095 10.1705 18.4205C9.95952 18.6315 9.67337 18.75 9.375 18.75C9.07663 18.75 8.79048 18.6315 8.5795 18.4205C8.36853 18.2095 8.25 17.9234 8.25 17.625V10.5H1.125C0.826631 10.5 0.540483 10.3815 0.329505 10.1705C0.118526 9.95952 0 9.67337 0 9.375C0 9.07663 0.118526 8.79048 0.329505 8.5795C0.540483 8.36853 0.826631 8.25 1.125 8.25H8.25V1.125C8.25 0.826631 8.36853 0.540483 8.5795 0.329505C8.79048 0.118526 9.07663 0 9.375 0C9.67337 0 9.95952 0.118526 10.1705 0.329505C10.3815 0.540483 10.5 0.826631 10.5 1.125V8.25H17.625C17.9234 8.25 18.2095 8.36853 18.4205 8.5795C18.6315 8.79048 18.75 9.07663 18.75 9.375Z" /> | |
| 81 | + </svg> | |
| 82 | + ); | |
| 83 | +} |
+++ @fox/core/icons/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export * from "./icons"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -14,6 +14,18 @@ |
| 14 | 14 |
import { FoxPhoneNumber, type FoxPhoneNumberState } from "../core/components/fox-phone-number";
|
| 15 | 15 |
import { FoxTextArea, type FoxTextAreaState } from "../core/components/fox-text-area";
|
| 16 | 16 |
import { FoxTextButton } from "../core/components/fox-text-button";
|
| 17 |
+import { FoxBreadcrumb } from "../core/components/fox-breadcrumb";
|
|
| 18 |
+import { FoxCarouselArea } from "../core/components/fox-carousel-area";
|
|
| 19 |
+import { FoxCarouselButton } from "../core/components/fox-carousel-button";
|
|
| 20 |
+import { FoxCarouselIndicator } from "../core/components/fox-carousel-indicator";
|
|
| 21 |
+import { FoxPagination } from "../core/components/fox-pagination";
|
|
| 22 |
+import {
|
|
| 23 |
+ FoxCaretLeftBoldIcon, |
|
| 24 |
+ FoxCaretRightBoldIcon, |
|
| 25 |
+ FoxPauseIcon, |
|
| 26 |
+ FoxPlayIcon, |
|
| 27 |
+ FoxPlusIcon, |
|
| 28 |
+} from "../core/icons"; |
|
| 17 | 29 |
import styles from "./dev-test.module.scss"; |
| 18 | 30 |
|
| 19 | 31 |
export interface ComponentExample {
|
... | ... | @@ -326,6 +338,61 @@ |
| 326 | 338 |
{ value: "b", label: "아이콘 없는 옵션" },
|
| 327 | 339 |
{ value: "c", label: "아주 긴 옵션 라벨 — 트리거에서는 말줄임으로 잘린다" },
|
| 328 | 340 |
]; |
| 341 |
+ |
|
| 342 |
+const BREADCRUMB_ITEMS = [ |
|
| 343 |
+ { label: "홈", href: "#" },
|
|
| 344 |
+ { label: "1Depth", href: "#" },
|
|
| 345 |
+ { label: "2Depth", href: "#" },
|
|
| 346 |
+ { label: "3Depth" },
|
|
| 347 |
+]; |
|
| 348 |
+ |
|
| 349 |
+function PaginationLiveDemo() {
|
|
| 350 |
+ const [page, setPage] = useState(1); |
|
| 351 |
+ return ( |
|
| 352 |
+ <div className={styles.demoStack}>
|
|
| 353 |
+ <FoxPagination currentPage={page} totalPages={12} onPageChange={setPage} ellipsis />
|
|
| 354 |
+ <p> |
|
| 355 |
+ 현재 {page} / 12 페이지
|
|
| 356 |
+ </p> |
|
| 357 |
+ </div> |
|
| 358 |
+ ); |
|
| 359 |
+} |
|
| 360 |
+ |
|
| 361 |
+function CarouselDotPickDemo() {
|
|
| 362 |
+ const [page, setPage] = useState(1); |
|
| 363 |
+ return <FoxCarouselIndicator current={page} total={5} onSelect={setPage} />;
|
|
| 364 |
+} |
|
| 365 |
+ |
|
| 366 |
+function CarouselLiveDemo() {
|
|
| 367 |
+ const total = 5; |
|
| 368 |
+ const [page, setPage] = useState(1); |
|
| 369 |
+ const [playing, setPlaying] = useState(true); |
|
| 370 |
+ const move = (step: number) => setPage((prev) => ((prev - 1 + step + total) % total) + 1); |
|
| 371 |
+ |
|
| 372 |
+ return ( |
|
| 373 |
+ <FoxCarouselArea> |
|
| 374 |
+ <FoxCarouselIndicator current={page} total={total} onSelect={setPage} />
|
|
| 375 |
+ <FoxCarouselButton |
|
| 376 |
+ type="plain" |
|
| 377 |
+ label="이전" |
|
| 378 |
+ icon={<FoxCaretLeftBoldIcon />}
|
|
| 379 |
+ onAction={() => move(-1)}
|
|
| 380 |
+ /> |
|
| 381 |
+ <FoxCarouselButton |
|
| 382 |
+ type="plain" |
|
| 383 |
+ label="다음" |
|
| 384 |
+ icon={<FoxCaretRightBoldIcon />}
|
|
| 385 |
+ onAction={() => move(1)}
|
|
| 386 |
+ /> |
|
| 387 |
+ <FoxCarouselButton |
|
| 388 |
+ type="plain" |
|
| 389 |
+ label={playing ? "정지" : "재생"}
|
|
| 390 |
+ icon={playing ? <FoxPauseIcon /> : <FoxPlayIcon />}
|
|
| 391 |
+ onAction={() => setPlaying((prev) => !prev)}
|
|
| 392 |
+ /> |
|
| 393 |
+ </FoxCarouselArea> |
|
| 394 |
+ ); |
|
| 395 |
+} |
|
| 329 | 396 |
|
| 330 | 397 |
/** 컴포넌트를 만들 때마다 항목 하나를 추가한다 — 다른 파일은 고치지 않는다. */ |
| 331 | 398 |
export const COMPONENT_EXAMPLES: ComponentExample[] = [ |
... | ... | @@ -1392,4 +1459,231 @@ |
| 1392 | 1459 |
}, |
| 1393 | 1460 |
], |
| 1394 | 1461 |
}, |
| 1462 |
+ {
|
|
| 1463 |
+ id: "fox-breadcrumb", |
|
| 1464 |
+ name: "FoxBreadcrumb", |
|
| 1465 |
+ description: |
|
| 1466 |
+ "Figma 시안(breadcrumb 141:3204)을 옮겼습니다. 마지막 항목이 현재 위치라 링크가 되지 않고 aria-current=\"page\"가 붙습니다. href를 주면 <a>, 없으면 <button>입니다. 항목 바탕이 투명이라 hover·pressed는 마우스를 올려 확인하세요.", |
|
| 1467 |
+ variants: [ |
|
| 1468 |
+ {
|
|
| 1469 |
+ label: "깊이 — 1 · 2 · 3 · 4단계", |
|
| 1470 |
+ node: ( |
|
| 1471 |
+ <div className={styles.demoStack}>
|
|
| 1472 |
+ <FoxBreadcrumb items={[{ label: "홈" }]} />
|
|
| 1473 |
+ <FoxBreadcrumb items={[{ label: "홈", href: "#" }, { label: "1Depth" }]} />
|
|
| 1474 |
+ <FoxBreadcrumb |
|
| 1475 |
+ items={[
|
|
| 1476 |
+ { label: "홈", href: "#" },
|
|
| 1477 |
+ { label: "1Depth", href: "#" },
|
|
| 1478 |
+ { label: "2Depth" },
|
|
| 1479 |
+ ]} |
|
| 1480 |
+ /> |
|
| 1481 |
+ <FoxBreadcrumb items={BREADCRUMB_ITEMS} />
|
|
| 1482 |
+ </div> |
|
| 1483 |
+ ), |
|
| 1484 |
+ }, |
|
| 1485 |
+ {
|
|
| 1486 |
+ label: "집 아이콘 끄기", |
|
| 1487 |
+ node: <FoxBreadcrumb items={BREADCRUMB_ITEMS} homeIcon={false} />,
|
|
| 1488 |
+ }, |
|
| 1489 |
+ {
|
|
| 1490 |
+ label: "긴 이름 — 좁아지면 줄이 넘어갑니다", |
|
| 1491 |
+ node: ( |
|
| 1492 |
+ <div className={styles.demoNarrow}>
|
|
| 1493 |
+ <FoxBreadcrumb |
|
| 1494 |
+ items={[
|
|
| 1495 |
+ { label: "홈", href: "#" },
|
|
| 1496 |
+ { label: "회원관리", href: "#" },
|
|
| 1497 |
+ { label: "학생 회원 목록", href: "#" },
|
|
| 1498 |
+ { label: "상세 조회" },
|
|
| 1499 |
+ ]} |
|
| 1500 |
+ /> |
|
| 1501 |
+ </div> |
|
| 1502 |
+ ), |
|
| 1503 |
+ }, |
|
| 1504 |
+ ], |
|
| 1505 |
+ }, |
|
| 1506 |
+ {
|
|
| 1507 |
+ id: "fox-pagination", |
|
| 1508 |
+ name: "FoxPagination", |
|
| 1509 |
+ description: |
|
| 1510 |
+ "Figma 시안(pagination 162:374)을 옮겼습니다. 시안의 device 축은 프롭이 아니라 화면 폭이 정합니다 — 768px 미만에서 이전·다음이 첫 줄로 빠지고 번호가 아래 줄에 섭니다(창을 좁혀 확인하세요). 숫자칸은 PC 32px / 모바일 24px입니다.", |
|
| 1511 |
+ variants: [ |
|
| 1512 |
+ {
|
|
| 1513 |
+ label: "눌러서 확인", |
|
| 1514 |
+ node: <PaginationLiveDemo />, |
|
| 1515 |
+ }, |
|
| 1516 |
+ {
|
|
| 1517 |
+ label: "위치 — 첫 · 중간 · 마지막", |
|
| 1518 |
+ node: ( |
|
| 1519 |
+ <div className={styles.demoStack}>
|
|
| 1520 |
+ <FoxPagination currentPage={1} totalPages={5} />
|
|
| 1521 |
+ <FoxPagination currentPage={3} totalPages={5} />
|
|
| 1522 |
+ <FoxPagination currentPage={5} totalPages={5} />
|
|
| 1523 |
+ </div> |
|
| 1524 |
+ ), |
|
| 1525 |
+ }, |
|
| 1526 |
+ {
|
|
| 1527 |
+ label: "한 쪽만 있을 때 — 이전·다음이 잠깁니다", |
|
| 1528 |
+ node: ( |
|
| 1529 |
+ <div className={styles.demoStack}>
|
|
| 1530 |
+ <FoxPagination currentPage={1} totalPages={1} />
|
|
| 1531 |
+ </div> |
|
| 1532 |
+ ), |
|
| 1533 |
+ }, |
|
| 1534 |
+ {
|
|
| 1535 |
+ label: "말줄임 (ellipsis) — 창 밖에 페이지가 더 있을 때", |
|
| 1536 |
+ node: ( |
|
| 1537 |
+ <div className={styles.demoStack}>
|
|
| 1538 |
+ <FoxPagination currentPage={1} totalPages={20} ellipsis />
|
|
| 1539 |
+ <FoxPagination currentPage={10} totalPages={20} ellipsis />
|
|
| 1540 |
+ <FoxPagination currentPage={20} totalPages={20} ellipsis />
|
|
| 1541 |
+ </div> |
|
| 1542 |
+ ), |
|
| 1543 |
+ }, |
|
| 1544 |
+ {
|
|
| 1545 |
+ label: "링크형 (buildHref) — 번호가 <a>가 됩니다", |
|
| 1546 |
+ node: <FoxPagination currentPage={2} totalPages={5} buildHref={(page) => `#page-${page}`} />,
|
|
| 1547 |
+ }, |
|
| 1548 |
+ {
|
|
| 1549 |
+ label: "보이는 개수 (windowSize)", |
|
| 1550 |
+ node: ( |
|
| 1551 |
+ <div className={styles.demoStack}>
|
|
| 1552 |
+ <FoxPagination currentPage={5} totalPages={20} windowSize={3} />
|
|
| 1553 |
+ <FoxPagination currentPage={5} totalPages={20} windowSize={10} />
|
|
| 1554 |
+ </div> |
|
| 1555 |
+ ), |
|
| 1556 |
+ }, |
|
| 1557 |
+ ], |
|
| 1558 |
+ }, |
|
| 1559 |
+ {
|
|
| 1560 |
+ id: "fox-carousel-indicator", |
|
| 1561 |
+ name: "FoxCarouselIndicator", |
|
| 1562 |
+ description: |
|
| 1563 |
+ "Figma 시안(carousel-dot 162:403 · carousel-number 162:408)을 한 컴포넌트로 두었습니다. 흰 알약이 같고 안에 든 것만 달라서입니다. 흰 조각이라 아래 예제는 회색 판 위에 올렸습니다.", |
|
| 1564 |
+ variants: [ |
|
| 1565 |
+ {
|
|
| 1566 |
+ label: "형태 (type) — dot · number", |
|
| 1567 |
+ node: ( |
|
| 1568 |
+ <div className={styles.carouselStage}>
|
|
| 1569 |
+ <FoxCarouselIndicator current={1} total={5} />
|
|
| 1570 |
+ <FoxCarouselIndicator type="number" current={1} total={3} />
|
|
| 1571 |
+ </div> |
|
| 1572 |
+ ), |
|
| 1573 |
+ }, |
|
| 1574 |
+ {
|
|
| 1575 |
+ label: "현재 위치", |
|
| 1576 |
+ node: ( |
|
| 1577 |
+ <div className={styles.carouselStage}>
|
|
| 1578 |
+ <FoxCarouselIndicator current={1} total={4} />
|
|
| 1579 |
+ <FoxCarouselIndicator current={2} total={4} />
|
|
| 1580 |
+ <FoxCarouselIndicator current={4} total={4} />
|
|
| 1581 |
+ </div> |
|
| 1582 |
+ ), |
|
| 1583 |
+ }, |
|
| 1584 |
+ {
|
|
| 1585 |
+ label: "점을 눌러 옮기기 (onSelect) — 점이 <button>이 됩니다", |
|
| 1586 |
+ node: ( |
|
| 1587 |
+ <div className={styles.carouselStage}>
|
|
| 1588 |
+ <CarouselDotPickDemo /> |
|
| 1589 |
+ </div> |
|
| 1590 |
+ ), |
|
| 1591 |
+ }, |
|
| 1592 |
+ ], |
|
| 1593 |
+ }, |
|
| 1594 |
+ {
|
|
| 1595 |
+ id: "fox-carousel-button", |
|
| 1596 |
+ name: "FoxCarouselButton", |
|
| 1597 |
+ description: |
|
| 1598 |
+ "Figma 시안의 carousel-arrow(163:420)와 carousel-atomic(680:5710)을 한 컴포넌트로 두었습니다. 원형·흰 바탕·아이콘 하나까지 같고 테두리와 그림자만 달라 그 차이를 type으로 뒀습니다. outlined만 hover·pressed에서 떠오르고 아이콘이 진해집니다 — 마우스를 올려 확인하세요.", |
|
| 1599 |
+ variants: [ |
|
| 1600 |
+ {
|
|
| 1601 |
+ label: "계열 (type) — outlined · plain", |
|
| 1602 |
+ node: ( |
|
| 1603 |
+ <div className={styles.carouselStage}>
|
|
| 1604 |
+ <FoxCarouselButton label="이전" icon={<FoxCaretLeftBoldIcon />} />
|
|
| 1605 |
+ <FoxCarouselButton type="plain" label="이전" icon={<FoxCaretLeftBoldIcon />} />
|
|
| 1606 |
+ </div> |
|
| 1607 |
+ ), |
|
| 1608 |
+ }, |
|
| 1609 |
+ {
|
|
| 1610 |
+ label: "크기 (size) — lg · md · sm", |
|
| 1611 |
+ node: ( |
|
| 1612 |
+ <div className={styles.carouselStage}>
|
|
| 1613 |
+ <FoxCarouselButton size="lg" label="이전" icon={<FoxCaretLeftBoldIcon />} />
|
|
| 1614 |
+ <FoxCarouselButton size="md" label="이전" icon={<FoxCaretLeftBoldIcon />} />
|
|
| 1615 |
+ <FoxCarouselButton size="sm" label="이전" icon={<FoxCaretLeftBoldIcon />} />
|
|
| 1616 |
+ <FoxCarouselButton size="lg" label="다음" icon={<FoxCaretRightBoldIcon />} />
|
|
| 1617 |
+ <FoxCarouselButton size="md" label="다음" icon={<FoxCaretRightBoldIcon />} />
|
|
| 1618 |
+ <FoxCarouselButton size="sm" label="다음" icon={<FoxCaretRightBoldIcon />} />
|
|
| 1619 |
+ </div> |
|
| 1620 |
+ ), |
|
| 1621 |
+ }, |
|
| 1622 |
+ {
|
|
| 1623 |
+ label: "시안의 아이콘 5종 (plain)", |
|
| 1624 |
+ node: ( |
|
| 1625 |
+ <div className={styles.carouselStage}>
|
|
| 1626 |
+ <FoxCarouselButton type="plain" label="이전" icon={<FoxCaretLeftBoldIcon />} />
|
|
| 1627 |
+ <FoxCarouselButton type="plain" label="다음" icon={<FoxCaretRightBoldIcon />} />
|
|
| 1628 |
+ <FoxCarouselButton type="plain" label="재생" icon={<FoxPlayIcon />} />
|
|
| 1629 |
+ <FoxCarouselButton type="plain" label="정지" icon={<FoxPauseIcon />} />
|
|
| 1630 |
+ <FoxCarouselButton type="plain" label="더보기" icon={<FoxPlusIcon />} />
|
|
| 1631 |
+ </div> |
|
| 1632 |
+ ), |
|
| 1633 |
+ }, |
|
| 1634 |
+ {
|
|
| 1635 |
+ label: "비활성", |
|
| 1636 |
+ node: ( |
|
| 1637 |
+ <div className={styles.carouselStage}>
|
|
| 1638 |
+ <FoxCarouselButton size="lg" label="이전" icon={<FoxCaretLeftBoldIcon />} disabled />
|
|
| 1639 |
+ <FoxCarouselButton size="md" label="이전" icon={<FoxCaretLeftBoldIcon />} disabled />
|
|
| 1640 |
+ <FoxCarouselButton size="sm" label="이전" icon={<FoxCaretLeftBoldIcon />} disabled />
|
|
| 1641 |
+ <FoxCarouselButton type="plain" label="이전" icon={<FoxCaretLeftBoldIcon />} disabled />
|
|
| 1642 |
+ </div> |
|
| 1643 |
+ ), |
|
| 1644 |
+ }, |
|
| 1645 |
+ ], |
|
| 1646 |
+ }, |
|
| 1647 |
+ {
|
|
| 1648 |
+ id: "fox-carousel-area", |
|
| 1649 |
+ name: "FoxCarouselArea", |
|
| 1650 |
+ description: |
|
| 1651 |
+ "Figma 시안(carousel-area 683:5902 · 683:5923)의 두 조합입니다. 담는 그릇만 갖고 무엇이 들어갈지는 정하지 않습니다 — 시안에서도 안에 든 것이 전부 다른 컴포넌트의 인스턴스입니다.", |
|
| 1652 |
+ variants: [ |
|
| 1653 |
+ {
|
|
| 1654 |
+ label: "눌러서 확인", |
|
| 1655 |
+ node: ( |
|
| 1656 |
+ <div className={styles.carouselStage}>
|
|
| 1657 |
+ <CarouselLiveDemo /> |
|
| 1658 |
+ </div> |
|
| 1659 |
+ ), |
|
| 1660 |
+ }, |
|
| 1661 |
+ {
|
|
| 1662 |
+ label: "시안 조합 — 점 + 이전·다음·정지", |
|
| 1663 |
+ node: ( |
|
| 1664 |
+ <div className={styles.carouselStage}>
|
|
| 1665 |
+ <FoxCarouselArea> |
|
| 1666 |
+ <FoxCarouselIndicator current={1} total={5} />
|
|
| 1667 |
+ <FoxCarouselButton type="plain" label="이전" icon={<FoxCaretLeftBoldIcon />} />
|
|
| 1668 |
+ <FoxCarouselButton type="plain" label="다음" icon={<FoxCaretRightBoldIcon />} />
|
|
| 1669 |
+ <FoxCarouselButton type="plain" label="정지" icon={<FoxPauseIcon />} />
|
|
| 1670 |
+ </FoxCarouselArea> |
|
| 1671 |
+ </div> |
|
| 1672 |
+ ), |
|
| 1673 |
+ }, |
|
| 1674 |
+ {
|
|
| 1675 |
+ label: "시안 조합 — 숫자 + 이전·다음·더보기", |
|
| 1676 |
+ node: ( |
|
| 1677 |
+ <div className={styles.carouselStage}>
|
|
| 1678 |
+ <FoxCarouselArea> |
|
| 1679 |
+ <FoxCarouselIndicator type="number" current={1} total={3} />
|
|
| 1680 |
+ <FoxCarouselButton type="plain" label="이전" icon={<FoxCaretLeftBoldIcon />} />
|
|
| 1681 |
+ <FoxCarouselButton type="plain" label="다음" icon={<FoxCaretRightBoldIcon />} />
|
|
| 1682 |
+ <FoxCarouselButton type="plain" label="더보기" icon={<FoxPlusIcon />} />
|
|
| 1683 |
+ </FoxCarouselArea> |
|
| 1684 |
+ </div> |
|
| 1685 |
+ ), |
|
| 1686 |
+ }, |
|
| 1687 |
+ ], |
|
| 1688 |
+ }, |
|
| 1395 | 1689 |
]; |
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
... | ... | @@ -477,3 +477,14 @@ |
| 477 | 477 |
inline-size: 80rem; |
| 478 | 478 |
max-inline-size: 100%; |
| 479 | 479 |
} |
| 480 |
+ |
|
| 481 |
+// 캐러셀 조각은 흰 알약이라 흰 바탕에서는 보이지 않는다 — 시안처럼 어두운 판 위에 올린다. |
|
| 482 |
+.carouselStage {
|
|
| 483 |
+ display: flex; |
|
| 484 |
+ flex-wrap: wrap; |
|
| 485 |
+ gap: fox.gap(5); |
|
| 486 |
+ align-items: center; |
|
| 487 |
+ padding: fox.padding(6); |
|
| 488 |
+ border-radius: fox.radius(3); |
|
| 489 |
+ background: fox.color(surface-neutral-gray-stronger); |
|
| 490 |
+} |
+++ @fox/styles/_fox-carousel-area.scss
... | ... | @@ -0,0 +1,18 @@ |
| 1 | +// FoxCarouselArea — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) | |
| 2 | +// carousel-area (683:5902 · 683:5923 · 섹션 683:5921) | |
| 3 | +// | |
| 4 | +// 마크업 계약 (React 밖 소비자용): | |
| 5 | +// <div class="fox-carousel-area"> | |
| 6 | +// …fox-carousel-indicator… | |
| 7 | +// …fox-carousel-button ×3… | |
| 8 | +// </div> | |
| 9 | +// | |
| 10 | +// 시안에서 안에 든 것이 전부 다른 컴포넌트의 인스턴스라 이 파일은 줄 배치만 갖는다. | |
| 11 | + | |
| 12 | +@use "abstracts" as fox; | |
| 13 | + | |
| 14 | +.fox-carousel-area { | |
| 15 | + display: inline-flex; | |
| 16 | + align-items: center; | |
| 17 | + gap: fox.gap(2); | |
| 18 | +} |
+++ @fox/styles/_fox-carousel-indicator.scss
... | ... | @@ -0,0 +1,98 @@ |
| 1 | +// FoxCarouselIndicator — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) | |
| 2 | +// carousel-dot (162:403) · carousel-number (162:408) | |
| 3 | +// 섹션 392:5776 | |
| 4 | +// | |
| 5 | +// 마크업 계약 (React 밖 소비자용): | |
| 6 | +// <div class="fox-carousel-indicator fox-carousel-indicator--dot" role="group"> | |
| 7 | +// <span class="fox-carousel-indicator__dot fox-carousel-indicator__dot--active"></span> | |
| 8 | +// <span class="fox-carousel-indicator__dot"></span> | |
| 9 | +// </div> | |
| 10 | +// <p class="fox-carousel-indicator fox-carousel-indicator--number"> | |
| 11 | +// <span class="fox-carousel-indicator__current">1</span> | |
| 12 | +// <span class="fox-carousel-indicator__separator"></span> | |
| 13 | +// <span class="fox-carousel-indicator__total">3</span> | |
| 14 | +// </p> | |
| 15 | +// | |
| 16 | +// 두 시안이 한 컴포넌트다 — 흰 알약(높이·좌우 여백·간격·모서리)이 같고 안에 든 것만 다르다. | |
| 17 | +// 높이 40px은 시안이 리터럴로 적었지만 `form/height/md`와 값이 같고, carousel-area에서 | |
| 18 | +// md 버튼과 나란히 서야 하므로 토큰으로 맞췄다. | |
| 19 | +// | |
| 20 | +// 시안에 상태 변형이 없다. 점을 눌러 옮길 수 있게 하면 `<button>`이 되는데, 그때 쓸 | |
| 21 | +// hover·pressed 표시가 시안에 없어 두지 않았다. | |
| 22 | +// | |
| 23 | +// 토큰이 아닌 값: | |
| 24 | +// - 점 0.8rem · 활성 막대 2.4rem×0.8rem · 숫자형 가운뎃점 0.4rem: 시안 값이나 Figma가 | |
| 25 | +// 변수로 export하지 않는다(사용자 승인). | |
| 26 | +// - line-height / letter-spacing: 위와 같다. | |
| 27 | +// - 버튼 초기화(appearance/cursor): 구조 규칙. | |
| 28 | + | |
| 29 | +@use "abstracts" as fox; | |
| 30 | + | |
| 31 | +.fox-carousel-indicator { | |
| 32 | + box-sizing: border-box; | |
| 33 | + display: inline-flex; | |
| 34 | + flex-shrink: 0; | |
| 35 | + align-items: center; | |
| 36 | + gap: fox.gap(2); | |
| 37 | + block-size: fox.form(height-md); | |
| 38 | + margin: 0; | |
| 39 | + padding-inline: fox.padding(6); | |
| 40 | + border-radius: fox.radius(max); | |
| 41 | + background: fox.color(surface-neutral-default); | |
| 42 | + | |
| 43 | + // ── 점 ──────────────────────────────────────────────────────────────────── | |
| 44 | + // 시안 값이나 변수로 export되지 않는다 — 파일 상단 참고. | |
| 45 | + &__dot { | |
| 46 | + display: block; | |
| 47 | + flex-shrink: 0; | |
| 48 | + inline-size: 0.8rem; | |
| 49 | + block-size: 0.8rem; | |
| 50 | + margin: 0; | |
| 51 | + padding: 0; | |
| 52 | + border: none; | |
| 53 | + border-radius: fox.radius(max); | |
| 54 | + background: fox.color(element-neutral-default); | |
| 55 | + } | |
| 56 | + | |
| 57 | + // 현재 위치만 길쭉해지고 색이 바뀐다. | |
| 58 | + &__dot--active { | |
| 59 | + inline-size: 2.4rem; | |
| 60 | + background: fox.color(element-theme-primary); | |
| 61 | + } | |
| 62 | + | |
| 63 | + button.fox-carousel-indicator__dot { | |
| 64 | + cursor: pointer; | |
| 65 | + } | |
| 66 | + | |
| 67 | + // ── 숫자 ────────────────────────────────────────────────────────────────── | |
| 68 | + &--number { | |
| 69 | + justify-content: center; | |
| 70 | + } | |
| 71 | + | |
| 72 | + &__current, | |
| 73 | + &__total { | |
| 74 | + font-family: fox.font-family(body); | |
| 75 | + font-size: fox.font-size(label-md); | |
| 76 | + line-height: 1; | |
| 77 | + letter-spacing: -0.025em; | |
| 78 | + } | |
| 79 | + | |
| 80 | + // 현재 위치만 굵고 색이 다르다. | |
| 81 | + &__current { | |
| 82 | + color: fox.color(font-theme-primary); | |
| 83 | + font-weight: fox.font-weight(bold); | |
| 84 | + } | |
| 85 | + | |
| 86 | + &__total { | |
| 87 | + color: fox.color(font-neutral-subtle); | |
| 88 | + font-weight: fox.font-weight(regular); | |
| 89 | + } | |
| 90 | + | |
| 91 | + &__separator { | |
| 92 | + flex-shrink: 0; | |
| 93 | + inline-size: 0.4rem; | |
| 94 | + block-size: 0.4rem; | |
| 95 | + border-radius: fox.radius(max); | |
| 96 | + background: fox.color(element-neutral-default); | |
| 97 | + } | |
| 98 | +} |
+++ @fox/styles/_fox-pagination.scss
... | ... | @@ -0,0 +1,154 @@ |
| 1 | +// FoxPagination — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) | |
| 2 | +// Figma pagination (162:374 · 섹션 391:3925) | |
| 3 | +// pagination-item (섹션 391:3817) | |
| 4 | +// | |
| 5 | +// 마크업 계약 (React 밖 소비자용): | |
| 6 | +// <nav class="fox-pagination" aria-label="페이지"> | |
| 7 | +// <a class="fox-pagination__item fox-pagination__step"> | |
| 8 | +// <span class="fox-pagination__step-icon">…svg…</span>이전 | |
| 9 | +// </a> | |
| 10 | +// <ul class="fox-pagination__list"> | |
| 11 | +// <li><span class="fox-pagination__item fox-pagination__number | |
| 12 | +// fox-pagination__number--current" aria-current="page">1</span></li> | |
| 13 | +// <li><a class="fox-pagination__item fox-pagination__number">2</a></li> | |
| 14 | +// </ul> | |
| 15 | +// <a class="fox-pagination__item fox-pagination__step"> | |
| 16 | +// 다음<span class="fox-pagination__step-icon">…svg…</span> | |
| 17 | +// </a> | |
| 18 | +// </nav> | |
| 19 | +// | |
| 20 | +// 시안의 device 축을 프롭이 아니라 화면 폭으로 나눈다. | |
| 21 | +// - 모바일: 이전·다음이 첫 줄, 번호가 둘째 줄. 번호칸만 폭을 다 먹어 줄이 넘어간다. | |
| 22 | +// - PC: 이전·번호·다음이 한 줄. `order`로 번호를 가운데로 되돌린다. | |
| 23 | +// 마크업은 한 벌이고 DOM 순서(이전 → 번호 → 다음)는 읽는 순서 그대로다. | |
| 24 | +// | |
| 25 | +// 번호칸끼리는 간격이 없다(시안 list에 gap이 없다) — 칸 자체가 정사각이라 붙어도 벌어져 보인다. | |
| 26 | +// | |
| 27 | +// 토큰이 아닌 값: | |
| 28 | +// - 번호칸 정사각 크기 3.2rem(PC) / 2.4rem(모바일): 시안 값이나 Figma가 변수로 | |
| 29 | +// export하지 않는다(사용자 승인). | |
| 30 | +// - line-height / letter-spacing: 위와 같다. | |
| 31 | +// - 목록의 margin/padding/list-style 0: `<ul>`의 브라우저 기본값 지우기. | |
| 32 | +// - 버튼·링크 초기화(appearance/cursor/색 상속): 구조 규칙. | |
| 33 | + | |
| 34 | +@use "abstracts" as fox; | |
| 35 | + | |
| 36 | +.fox-pagination { | |
| 37 | + display: flex; | |
| 38 | + flex-wrap: wrap; | |
| 39 | + align-items: center; | |
| 40 | + justify-content: center; | |
| 41 | + gap: fox.gap(3); | |
| 42 | + inline-size: 100%; | |
| 43 | + padding-block-start: fox.spacing(top-lg); | |
| 44 | + | |
| 45 | + // `<ul>`이라 브라우저 기본 여백·글머리표를 먼저 지운다. | |
| 46 | + &__list { | |
| 47 | + display: flex; | |
| 48 | + order: 1; | |
| 49 | + align-items: center; | |
| 50 | + justify-content: center; | |
| 51 | + inline-size: 100%; | |
| 52 | + margin: 0; | |
| 53 | + padding: 0; | |
| 54 | + list-style: none; | |
| 55 | + } | |
| 56 | + | |
| 57 | + &__item { | |
| 58 | + display: inline-flex; | |
| 59 | + align-items: center; | |
| 60 | + margin: 0; | |
| 61 | + border: none; | |
| 62 | + border-radius: fox.radius(2); | |
| 63 | + background: fox.color(action-secondary); | |
| 64 | + font-family: fox.font-family(body); | |
| 65 | + font-size: fox.font-size(label-md); | |
| 66 | + font-weight: fox.font-weight(regular); | |
| 67 | + line-height: 1; | |
| 68 | + letter-spacing: -0.025em; | |
| 69 | + text-decoration: none; | |
| 70 | + white-space: nowrap; | |
| 71 | + } | |
| 72 | + | |
| 73 | + // ── 이전 · 다음 ──────────────────────────────────────────────────────────── | |
| 74 | + &__step { | |
| 75 | + gap: fox.gap(1); | |
| 76 | + padding: fox.padding(2); | |
| 77 | + color: fox.color(font-neutral-default); | |
| 78 | + } | |
| 79 | + | |
| 80 | + &__step-icon { | |
| 81 | + display: inline-flex; | |
| 82 | + flex-shrink: 0; | |
| 83 | + color: fox.color(element-neutral-strong); | |
| 84 | + | |
| 85 | + > * { | |
| 86 | + inline-size: fox.icon(3); | |
| 87 | + block-size: fox.icon(3); | |
| 88 | + } | |
| 89 | + } | |
| 90 | + | |
| 91 | + // ── 번호 ────────────────────────────────────────────────────────────────── | |
| 92 | + // 시안 값이나 변수로 export되지 않는다 — 파일 상단 참고. | |
| 93 | + &__number { | |
| 94 | + box-sizing: border-box; | |
| 95 | + justify-content: center; | |
| 96 | + inline-size: 2.4rem; | |
| 97 | + block-size: 2.4rem; | |
| 98 | + color: fox.color(font-neutral-subtle); | |
| 99 | + } | |
| 100 | + | |
| 101 | + &__number--current { | |
| 102 | + background: fox.color(action-secondary-active); | |
| 103 | + color: fox.color(font-neutral-static-inverse); | |
| 104 | + font-weight: fox.font-weight(bold); | |
| 105 | + cursor: default; | |
| 106 | + } | |
| 107 | + | |
| 108 | + &__ellipsis { | |
| 109 | + justify-content: center; | |
| 110 | + inline-size: 2.4rem; | |
| 111 | + block-size: 2.4rem; | |
| 112 | + color: fox.color(font-neutral-subtle); | |
| 113 | + } | |
| 114 | + | |
| 115 | + // ── 상태 ────────────────────────────────────────────────────────────────── | |
| 116 | + a.fox-pagination__item, | |
| 117 | + button.fox-pagination__item { | |
| 118 | + cursor: pointer; | |
| 119 | + | |
| 120 | + &:hover { | |
| 121 | + background: fox.color(action-secondary-hover); | |
| 122 | + } | |
| 123 | + | |
| 124 | + &:active { | |
| 125 | + background: fox.color(action-secondary-pressed); | |
| 126 | + } | |
| 127 | + } | |
| 128 | + | |
| 129 | + // 갈 곳이 없는 이전·다음. `aria-disabled`가 곧 표시 조건이다. | |
| 130 | + &__item[aria-disabled="true"] { | |
| 131 | + color: fox.color(font-neutral-disabled); | |
| 132 | + cursor: default; | |
| 133 | + | |
| 134 | + .fox-pagination__step-icon { | |
| 135 | + color: fox.color(icon-neutral-disabled); | |
| 136 | + } | |
| 137 | + } | |
| 138 | + | |
| 139 | + @include fox.pc { | |
| 140 | + flex-wrap: nowrap; | |
| 141 | + gap: fox.gap(5); | |
| 142 | + | |
| 143 | + &__list { | |
| 144 | + order: 0; | |
| 145 | + inline-size: auto; | |
| 146 | + } | |
| 147 | + | |
| 148 | + &__number, | |
| 149 | + &__ellipsis { | |
| 150 | + inline-size: 3.2rem; | |
| 151 | + block-size: 3.2rem; | |
| 152 | + } | |
| 153 | + } | |
| 154 | +} |
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -17,3 +17,8 @@ |
| 17 | 17 |
@use "fox-select-option"; |
| 18 | 18 |
@use "fox-select-option-item"; |
| 19 | 19 |
@use "fox-text-area"; |
| 20 |
+@use "fox-breadcrumb"; |
|
| 21 |
+@use "fox-pagination"; |
|
| 22 |
+@use "fox-carousel-indicator"; |
|
| 23 |
+@use "fox-carousel-button"; |
|
| 24 |
+@use "fox-carousel-area"; |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?