feat: FoxSpinner 추가하고 버튼 로딩 자리를 채움
관리자페이지 파일의 img-loading(5384:9710)을 옮겼다. 시안은 80px 그림 한 장으로 왔지만 래스터를 그대로 쓰지 않았다 — 크기를 바꾸면 뭉개지고 색도 토큰을 못 쓴다. 원본 픽셀을 재서 같은 도형을 CSS로 다시 그렸다. 잰 값: 바깥 반지름 40 · 안쪽 28 → 굵기 12px(지름의 15%), 앞머리 원 12px이 6시 자리 고리 중심선 위. 색은 6시에서 시작해 시계방향 60°까지 알파 1.00이고 나머지 300°에 걸쳐 0까지 선형으로 준다(5°마다 샘플링해 확인). 굵기·앞머리를 퍼센트로 잡아 어떤 지름에서도 비율이 같다. `size`만 주면 된다. FoxButton의 로딩 자리는 "시안 미정"으로 비어 있었는데 이제 이 스피너가 들어간다. 버튼 안에서는 `color: inherit`으로 되돌려 계열별 글자색을 따라간다 — 요소에 준 색이 상속보다 강해서, 기본색을 두는 한 감싸기만으로는 바뀌지 않기 때문이다. 승인된 비토큰 값: 지름 8rem, 굵기·앞머리 15%, 마스크 정지점 70%, 그라디언트 60°. 한 바퀴 도는 시간 1초는 시안이 정지 그림이라 정보가 없어 흔한 기본값을 썼다. 검증: 지름 80 · 색 #256ef4 · 앞머리 12×12가 중심에서 아래로 34px(시안과 동일) · 그라디언트 from 180deg 0~60deg 불투명. 버튼 세 크기에서 스피너 색이 글자색과 일치하고 아이콘 슬롯 크기를 그대로 채운다. Co-Authored-By: Claude Opus 5
@3a30710db0e36bd908560e8726cf9e5eb88cd107
+++ @fox/core/components/fox-spinner/fox-spinner.tsx
... | ... | @@ -0,0 +1,47 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { cx } from "../../utils"; | |
| 4 | + | |
| 5 | +export interface FoxSpinnerProps { | |
| 6 | + /** | |
| 7 | + * 지름. CSS 길이면 무엇이든 되고, 굵기와 끝점도 함께 비율로 줄어든다. | |
| 8 | + * 넘기지 않으면 시안 크기(80px)다. 이미 크기가 정해진 자리에 넣을 때는 `"100%"`를 준다. | |
| 9 | + */ | |
| 10 | + size?: string | number; | |
| 11 | + /** | |
| 12 | + * 스크린리더가 읽을 말. 빈 문자열을 주면 잠재운다 — 옆 글자가 이미 "불러오는 중"이라고 | |
| 13 | + * 말하고 있을 때 쓴다. | |
| 14 | + */ | |
| 15 | + label?: string; | |
| 16 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 17 | + hidden?: boolean; | |
| 18 | + /** 배치 조정용. */ | |
| 19 | + className?: string; | |
| 20 | +} | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * @fox 로딩 표시. 한 바퀴 도는 고리와 그 앞머리의 동그라미로 이뤄진다. | |
| 24 | + * | |
| 25 | + * 색은 감싼 쪽의 `color`를 따른다 — 버튼 안에서는 글자색과 같아지고, 홀로 쓰면 테마 | |
| 26 | + * 기본색이다. | |
| 27 | + * | |
| 28 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 29 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 30 | + */ | |
| 31 | +export function FoxSpinner({ size, label = "불러오는 중", hidden = false, className }: FoxSpinnerProps) { | |
| 32 | + if (hidden) { | |
| 33 | + return null; | |
| 34 | + } | |
| 35 | + | |
| 36 | + return ( | |
| 37 | + <span | |
| 38 | + className={cx("fox-spinner", className)} | |
| 39 | + style={size === undefined ? undefined : { inlineSize: size, blockSize: size }} | |
| 40 | + role={label ? "status" : undefined} | |
| 41 | + aria-label={label || undefined} | |
| 42 | + > | |
| 43 | + <span className="fox-spinner__arc" /> | |
| 44 | + <span className="fox-spinner__head" /> | |
| 45 | + </span> | |
| 46 | + ); | |
| 47 | +} |
+++ @fox/core/components/fox-spinner/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxSpinner, type FoxSpinnerProps } from "./fox-spinner"; |
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -30,6 +30,7 @@ |
| 30 | 30 |
export * from "./fox-select"; |
| 31 | 31 |
export * from "./fox-select-option"; |
| 32 | 32 |
export * from "./fox-select-option-item"; |
| 33 |
+export * from "./fox-spinner"; |
|
| 33 | 34 |
export * from "./fox-tab"; |
| 34 | 35 |
export * from "./fox-tab-item"; |
| 35 | 36 |
export * from "./fox-table"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -49,6 +49,7 @@ |
| 49 | 49 |
import { FoxTableRow } from "../core/components/fox-table-row";
|
| 50 | 50 |
import { FoxTab } from "../core/components/fox-tab";
|
| 51 | 51 |
import { FoxTabItem } from "../core/components/fox-tab-item";
|
| 52 |
+import { FoxSpinner } from "../core/components/fox-spinner";
|
|
| 52 | 53 |
import styles from "./dev-test.module.scss"; |
| 53 | 54 |
|
| 54 | 55 |
export interface ComponentExample {
|
... | ... | @@ -2806,4 +2807,45 @@ |
| 2806 | 2807 |
}, |
| 2807 | 2808 |
], |
| 2808 | 2809 |
}, |
| 2810 |
+ {
|
|
| 2811 |
+ id: "fox-spinner", |
|
| 2812 |
+ name: "FoxSpinner", |
|
| 2813 |
+ description: |
|
| 2814 |
+ "Figma 시안(img-loading 5384:9710, 관리자페이지 파일)을 옮겼습니다. 시안은 80px 그림 한 장으로 왔지만 래스터를 그대로 쓰지 않고 픽셀을 재서 같은 도형을 CSS로 다시 그렸습니다 — 크기를 바꿔도 뭉개지지 않고 색이 토큰을 따릅니다. 색은 기본이 테마 기본색이고, 버튼 안에서는 계열별 글자색을 따라갑니다.", |
|
| 2815 |
+ variants: [ |
|
| 2816 |
+ {
|
|
| 2817 |
+ label: "시안 크기 (80px)", |
|
| 2818 |
+ node: <FoxSpinner />, |
|
| 2819 |
+ }, |
|
| 2820 |
+ {
|
|
| 2821 |
+ label: "크기 (size) — 굵기와 앞머리가 비율로 함께 줄어듭니다", |
|
| 2822 |
+ node: ( |
|
| 2823 |
+ <div className={styles.demoRow}>
|
|
| 2824 |
+ <FoxSpinner size={80} />
|
|
| 2825 |
+ <FoxSpinner size={48} />
|
|
| 2826 |
+ <FoxSpinner size={32} />
|
|
| 2827 |
+ <FoxSpinner size={20} />
|
|
| 2828 |
+ </div> |
|
| 2829 |
+ ), |
|
| 2830 |
+ }, |
|
| 2831 |
+ {
|
|
| 2832 |
+ label: "기본은 테마 기본색 — 버튼 안에서는 글자색을 따라갑니다", |
|
| 2833 |
+ node: ( |
|
| 2834 |
+ <div className={styles.demoRow}>
|
|
| 2835 |
+ <FoxSpinner size={40} />
|
|
| 2836 |
+ </div> |
|
| 2837 |
+ ), |
|
| 2838 |
+ }, |
|
| 2839 |
+ {
|
|
| 2840 |
+ label: "버튼 로딩 — 앞 아이콘 자리에 들어갑니다", |
|
| 2841 |
+ node: ( |
|
| 2842 |
+ <div className={styles.demoRow}>
|
|
| 2843 |
+ <FoxButton size="lg" type="primary" label="저장" loading /> |
|
| 2844 |
+ <FoxButton size="md" type="secondary" label="저장" loading /> |
|
| 2845 |
+ <FoxButton size="sm" label="저장" loading /> |
|
| 2846 |
+ </div> |
|
| 2847 |
+ ), |
|
| 2848 |
+ }, |
|
| 2849 |
+ ], |
|
| 2850 |
+ }, |
|
| 2809 | 2851 |
]; |
+++ @fox/styles/_fox-spinner.scss
... | ... | @@ -0,0 +1,82 @@ |
| 1 | +// FoxSpinner — 시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) img-loading (5384:9710) | |
| 2 | +// | |
| 3 | +// 마크업 계약 (React 밖 소비자용): | |
| 4 | +// <span class="fox-spinner" role="status" aria-label="불러오는 중"> | |
| 5 | +// <span class="fox-spinner__arc"></span> | |
| 6 | +// <span class="fox-spinner__head"></span> | |
| 7 | +// </span> | |
| 8 | +// | |
| 9 | +// 시안은 80px 그림 한 장으로 왔지만 래스터를 그대로 쓰지 않는다 — 크기를 바꾸면 뭉개지고 | |
| 10 | +// 색도 토큰을 못 쓴다. 픽셀을 재서 같은 도형을 CSS로 다시 그렸다. | |
| 11 | +// - 고리: 바깥 반지름 40 · 안쪽 28 → 굵기 12px (지름의 15%) | |
| 12 | +// - 색: 6시에서 시작해 시계방향으로 60°까지 진하고, 나머지 300°에 걸쳐 투명해진다 | |
| 13 | +// - 앞머리: 지름 12px 원이 6시 자리 고리 중심선 위에 얹힌다 (고리 끝을 둥글게 맺는다) | |
| 14 | +// 원본 그림의 알파를 5°마다 재서 확인한 값이다(185~240° = 1.00, 이후 180°까지 선형 감소). | |
| 15 | +// | |
| 16 | +// 크기는 지름 하나로 정해진다. 굵기·앞머리를 퍼센트로 잡아 어떤 지름에서도 비율이 같다. | |
| 17 | +// | |
| 18 | +// 색은 기본이 테마 기본색이다(시안). 버튼처럼 글자색을 따라야 하는 자리는 감싼 쪽에서 | |
| 19 | +// `color: inherit`으로 되돌린다 — 요소에 색을 직접 주는 편이 상속보다 강해서, 기본값을 | |
| 20 | +// 두는 한 감싸기만으로는 바뀌지 않는다. | |
| 21 | +// | |
| 22 | +// 토큰이 아닌 값: | |
| 23 | +// - 지름 8rem, 굵기·앞머리 15%, 마스크 정지점 70%, 그라디언트 60°: 시안에서 잰 값이나 | |
| 24 | +// Figma가 변수로 export하지 않는다(사용자 승인). | |
| 25 | +// - 한 바퀴 도는 시간 1초: 시안이 정지 그림이라 정보가 없다. 흔한 기본값을 쓴다. | |
| 26 | +// - transform / mask / animation: 구조·움직임 규칙. | |
| 27 | + | |
| 28 | +@use "abstracts" as fox; | |
| 29 | + | |
| 30 | +@keyframes fox-spinner-turn { | |
| 31 | + to { | |
| 32 | + transform: rotate(1turn); | |
| 33 | + } | |
| 34 | +} | |
| 35 | + | |
| 36 | +.fox-spinner { | |
| 37 | + position: relative; | |
| 38 | + display: inline-block; | |
| 39 | + flex-shrink: 0; | |
| 40 | + // 시안 값이나 변수로 export되지 않는다 — 파일 상단 참고. | |
| 41 | + inline-size: 8rem; | |
| 42 | + block-size: 8rem; | |
| 43 | + color: fox.color(element-theme-primary); | |
| 44 | + animation: fox-spinner-turn 1s linear infinite; | |
| 45 | + | |
| 46 | + // 버튼 안에서는 계열별 글자색을 따라간다. | |
| 47 | + .fox-button__spinner > & { | |
| 48 | + color: inherit; | |
| 49 | + } | |
| 50 | + | |
| 51 | + // 고리. 원뿔 그라디언트를 그린 뒤 가운데를 도려내 굵기를 만든다. | |
| 52 | + &__arc { | |
| 53 | + position: absolute; | |
| 54 | + inset: 0; | |
| 55 | + border-radius: fox.radius(max); | |
| 56 | + background: conic-gradient( | |
| 57 | + from 180deg, | |
| 58 | + currentColor 0deg, | |
| 59 | + currentColor 60deg, | |
| 60 | + transparent 360deg | |
| 61 | + ); | |
| 62 | + // 안쪽 70%를 지운다 — 반지름의 30%가 곧 지름의 15%다. | |
| 63 | + mask: radial-gradient(farthest-side, transparent 70%, #000 70%); | |
| 64 | + } | |
| 65 | + | |
| 66 | + // 고리 앞머리. 끝을 둥글게 맺어 준다. | |
| 67 | + &__head { | |
| 68 | + position: absolute; | |
| 69 | + inset-block-end: 0; | |
| 70 | + inset-inline-start: 50%; | |
| 71 | + inline-size: 15%; | |
| 72 | + block-size: 15%; | |
| 73 | + transform: translateX(-50%); | |
| 74 | + border-radius: fox.radius(max); | |
| 75 | + background: currentColor; | |
| 76 | + } | |
| 77 | + | |
| 78 | + // 움직임을 줄이려는 사용자에게는 천천히 돈다 — 멈추면 무엇을 기다리는지 알 수 없다. | |
| 79 | + @media (prefers-reduced-motion: reduce) { | |
| 80 | + animation-duration: 3s; | |
| 81 | + } | |
| 82 | +} |
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -43,3 +43,4 @@ |
| 43 | 43 |
@use "fox-table-cell"; |
| 44 | 44 |
@use "fox-tab"; |
| 45 | 45 |
@use "fox-tab-item"; |
| 46 |
+@use "fox-spinner"; |
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?