Figma 시안 다섯을 한 컴포넌트로 옮긴다 — file-upload(391:3677) ·
file-upload-area(392:5968) · file-upload-img(391:3732) · file-list(392:5938) ·
file-list-item(391:3509). 고르는 방식만 다르고 목록은 같아서다.
시안에서 버튼·입력이 모두 기존 컴포넌트의 인스턴스라 그대로 조합한다.
썸네일 삭제 버튼은 FoxIconButton overlay xsm이 시안과 정확히 같다(24px ·
모서리 4 · 검정 50% · 배경흐림 10 · 흰 아이콘).
업로드는 하지 않는다. 파일을 고르면 원본 File을 넘길 뿐이고 진행률·서버
오류를 목록에 어떻게 비출지는 files로 정한다. files를 안 주면 안에서 들고
있어 간단한 폼에 그대로 쓴다.
default 모드는 시안의 두 상태를 모두 갖는다 — 비었으면 흰 칸에
파일선택(secondary), 파일 하나를 목록 없이 들고 있으면 칸이 회색으로 잠기고
버튼이 삭제(default 계열 · 휴지통)로 바뀐다. 흰 칸 규칙은 FoxInput의 읽기
전용 규칙(0,6,0)보다 특이도를 높여 로드 순서와 무관하게 이기도록 적었다.
끌어다 놓기는 prop으로 켜되 area는 안내문이 그렇게 말하므로 언제나 받는다.
입력칸 폭과 썸네일 크기는 토큰에 없어 prop 기본값으로 뒀다(사용자 승인).
Co-Authored-By: Claude Opus 5
Figma form-email(136:3092)과 form-address(5346:5823)를 옮긴다. 두 시안 모두
상자가 input·select 인스턴스이고 버튼도 btn 인스턴스라 FoxInput·FoxSelect·
FoxButton을 그대로 조합했다 — 두 SCSS는 배치와 `@` 구분자만 갖는다.
FoxEmail
값은 아이디@도메인이 합쳐진 문자열 하나다. 도메인을 목록에서 고르면 옆 칸이
그 값으로 덮이고 읽기 전용이 되며, "직접입력"을 고르면 잠금만 풀린다.
확인 버튼은 onVerify를 넘겼을 때만 렌더된다.
칸은 시안의 240px을 최소폭으로 두고 남는 폭을 나눠 갖는다 — 좁아지면 줄이
넘어간다(사용자 승인). 토큰 스케일에 240이 없어 이 한 줄만 리터럴이다.
FoxAddress
우편번호와 주소는 검색 결과로만 채워지는 읽기 전용이고 상세주소만 입력한다.
어떤 우편번호 서비스를 쓰는지 @fox는 모른다 — 호출부가 onSearch에서
FoxAddressSearchResult(zipCode·address·선택 detail)로 바꿔 돌려주고, 취소는
null이다. detail을 안 넘기면 사용자가 적어 둔 상세주소를 건드리지 않는다.
Co-Authored-By: Claude Opus 5
Figma form-certified(190:2017)의 세 줄을 옮긴다. 전화번호는 FoxPhoneNumber
(combine), 두 버튼은 FoxButton(secondary·primary 전체폭)을 그대로 쓴다 —
시안도 그 인스턴스다. 인증번호 상자만 자체로 그리는데, 오른쪽 남은 시간이
FoxInput의 아이콘 슬롯(정사각 20px)보다 넓기 때문이다.
보내기·확인은 호출부가 Promise로 넘긴다. resolve면 성공, throw·reject면
실패로 보고 이벤트를 올린다 — onSendSuccess/onSendFail/onVerifySuccess/
onVerifyFail/onExpire.
화면은 최소한만 반응한다. 보내기가 성공해야 인증번호 칸과 확인 버튼이 열리고,
인증에 성공하면 전화번호·인증번호·두 버튼이 전부 잠긴다. 실패 문구는 호출부가
보여준다.
남은 시간은 remainingSeconds를 주면 그 값을 그리고, 없으면 expiresIn부터
컴포넌트가 센다. 0에 닿는 순간 onExpire를 한 번만 올린다.
Co-Authored-By: Claude Opus 5
Figma form-phonenumber(391:3156)의 두 모양을 옮긴다. unit은 상자 셋을
하이픈으로 잇고, combine은 상자 하나에 세 칸을 둔다.
값은 숫자만 담긴 문자열 하나다 — 하이픈은 화면에만 있고 값에는 없다. 화면의
세 칸이 진실 공급원이라 입력 도중 "0101"처럼 어디까지가 앞자리인지 알 수 없는
값이 생겨도 칸이 흔들리지 않고, 밖에서 value를 바꿨을 때만 다시 나눈다.
나눌 때 앞자리는 prefixOptions 중 가장 긴 일치를, 뒷자리는 항상 마지막 네
자리를 쓴다 — 010-123-4567과 010-1234-5678이 둘 다 맞게 갈린다. 목록이 없으면
maxLengths[0]만큼을 앞자리로 보므로 지역번호 02는 목록에 넣어야 한다.
unit의 조각은 시안에서도 select·input 인스턴스라 FoxSelect·FoxInput을 그대로
조합한다. 상태·포커스가 저절로 따라오고 이 파일은 줄 배치와 하이픈만 갖는다.
combine 상자만 자체 규칙을 둔다.
칸을 다 채우면 다음 칸으로 옮기고 빈 칸에서 지우면 앞칸으로 돌아간다
(autoAdvance로 끌 수 있다).
Co-Authored-By: Claude Opus 5
FoxTextArea와 별도로 들어온 FoxSelect 계열이 등록 목록 세 곳에서 부딪쳤다.
셋 다 추가만 하는 목록이라 양쪽을 합쳤다.
components.scss의 select 세 줄은 나머지와 같은 상대 경로 표기로 맞췄다 —
같은 폴더의 파티셜이라 결과는 같다.
component-registry.tsx의 FoxInput 임포트는 위쪽 배럴 임포트와 겹쳐 뺐다.
Figma textarea(135:1655)의 6상태를 옮긴다. 시안에 크기 축이 없어 상자는
하나뿐이고 높이는 rows가 정한다(기본 5줄, 시안과 같음).
상태 판정은 FoxInput과 같은 두 갈래다 — 기본은 브라우저가 판단하고(포커스·
값·네이티브 속성·aria-invalid), data-state를 주면 그 모양으로 고정된다.
상태 매트릭스도 FoxInput과 동일해 규칙을 그대로 옮겼다.
하단은 왼쪽 헬퍼와 오른쪽 글자수 카운터로 나뉜다. 카운터는 현재 길이만 굵고
색이 다르며, 오류일 때 빨개진다(시안의 error 변형이 100/100을 빨갛게 둔다).
maxLength가 없으면 개수만 적는다. 비제어 입력의 길이는 직접 따라간다.
리사이즈는 시안에 신호가 없어 폭이 흔들리지 않게 세로만 허용한다.
Co-Authored-By: Claude Opus 5
Figma input(135:621)의 2타입 × 3크기 × 6상태를 옮긴다. 네이티브 을
감싸므로 시안에 없는 password·email·number 등 모든 타입이 그대로 통한다.
상태는 두 갈래다. 기본은 브라우저가 판단한다 — 포커스는 :focus-within,
비활성·읽기전용은 네이티브 속성, 오류는 aria-invalid. 시안의 completed는
default와 글자색만 다른데 그건 플레이스홀더와 입력값의 네이티브 차이라 규칙이
필요 없다. 모양을 못박아야 하면 루트에 data-state를 준다.
date는 브라우저가 그리는 달력 아이콘과 피커를 그대로 쓴다(사용자 승인).
지우기 버튼은 포커스 중이고 값이 있을 때만 뜨고 누르면 값을 비운다 —
비제어 입력은 DOM을 직접 비우고 제어 입력은 onChange()로 알린다.
헬퍼 메시지 색은 상태를 따라간다(기본 회색 · 포커스 파랑 · 완료 초록 ·
오류 빨강). 아이콘은 @fox에 아이콘 세트가 없어 슬롯으로 두었다.
Co-Authored-By: Claude Opus 5
Figma btn-page(404:5121)의 네 변형을 옮긴다. 자식은 버튼 그룹이고 여럿이면
양끝, 하나면 가운데다.
자식 수를 React에서 세지 않고 CSS `:has(> :only-child)`로 판단한다. 프래그먼트나
조건부 렌더로 자식이 갈려도 실제 DOM이 기준이라 어긋나지 않고, React를 안 쓰는
소비자도 표시할 클래스가 없다.
패널이 자식에게 강제하는 두 가지(사용자 승인):
- 패널 안 그룹 간격은 gap-3(8px). 단독으로 쓰는 그룹의 gap-2(4px)는 그대로다.
- 모바일에서는 패널도 그룹도 세로로 쌓이고 버튼이 폭을 채운다.
시안이 패널 안 버튼에만 주는 min-width 160px은 변수 바인딩이 없고 토큰 스케일에도
160이 없어 리터럴로 둔다(사용자 승인). 상단 80px 여백은 spacing-top-2xlg이며
offset={false}로 끌 수 있다.
Co-Authored-By: Claude Opus 5
새 Figma 파일(KE9UILWhB8qTBe14uXUn4F)의 다섯 시안을 이전 파일과 대조했다.
노드 ID·변형 목록·토큰 값·구조가 전부 같아 스타일 변경분은 없다. 컴파일된
CSS는 바이트 단위로 이전과 동일하다.
btn 컴포넌트 세트 48변형 전체 대조 — 일치
btn-txt 생성 코드가 노드 ID까지 이전 파일과 동일
btn-lnk 생성 코드 바이트 비교 — 동일(에셋 URL만 재발급)
btn-ico 변수 세트·메타데이터 + ghost/overlay/disabled 대표 변형 — 일치
toggle default·ghost·overlay의 active 3종 + 아이콘 fill — 일치
btn-txt의 아이콘 크기는 컴포넌트 세트가 여전히 리터럴이다. 다만 전시 섹션의
인스턴스가 icon/4·3·2를 쓰고 있어 우리 매핑과 의도가 같음을 확인했고 그
근거를 주석에 남긴다.
Co-Authored-By: Claude Opus 5
Figma btn-group(398:4854)에서 가져온 건 간격뿐이다 — 크기·방향과 무관하게
gap-2(4px) 하나여서 그룹에 크기별 스타일이 없다. 배치 규칙(방향·정렬·줄바꿈)은
지시받은 사양이다.
크기는 그룹이 단일 진실 공급원이다. 자식을 cloneElement로 덮어써서 자식이
저마다 다른 size를 줘도 그룹 값으로 통일된다. 세로일 때는 fullWidth까지 함께
내려주고 CSS도 stretch라, React를 안 쓰는 소비자도 같은 결과를 얻는다.
Children.map이 프래그먼트 안으로 들어가지 않아 직접 내려간다. 그러지 않으면
<>…로 감싼 버튼이 덮어쓰기를 피해 가고 Fragment에 size가 붙어 경고가 난다.
호스트 요소(div·span)는 DOM에 없는 속성이 넘어가지 않도록 건드리지 않는다.
Co-Authored-By: Claude Opus 5
Figma 시안(관리자페이지 node 390:439)을 토큰만으로 옮겼다. 컴파일된 CSS의 토큰 참조
50개, 원시 hex/px/rem 리터럴 0개다.
시안에서 확인한 것(추측하지 않음): gap은 lg·md·sm이 gap-2이고 xsm만 gap-1이다.
primary에는 테두리가 없고 secondary·default만 border-1을 갖는다. disabled는 계열과
무관하게 테두리가 사라진다(시안에서 disabled-border 미사용). 높이만 고정이고 좌우
padding만 지정돼 너비는 가변이다.
토큰이 아닌 코드 세 가지를 사용자 승인 후 넣었다.
- line-height: 1 / letter-spacing: -0.025em — 시안 텍스트 스타일 label/*-strong의 값이나
Figma에서 변수로 export되지 않아 토큰이 없다. 네 크기가 같은 값이라 규칙 하나로 끝난다.
- box-sizing: border-box — 높이 고정 + secondary·default의 1px 테두리 때문에 필수다.
없으면 계열별 바깥 높이가 2px 어긋난다.
- cursor·white-space·flex-shrink — 구조·상호작용 규칙. 버튼 스코프에만 적용한다.
모양 규칙은 @fox/styles/_fox-button.scss가 소유하고 module이 @use로 끌어올린다 —
클래스명이 모듈 스코프로 감싸져 전역 충돌이 없고, index.scss는 이 파티셜을 포함하지
않아 CSS가 중복 출력되지 않는다.
아이콘은 계열별 색을 토큰으로 입히기 위해 ReactNode로 받는다(currentColor 전제).
크기는 슬롯이 form-icon-* 토큰으로 정한다.
error 계열은 시안 미정이라 클래스를 비워 뒀다.
Co-Authored-By: Claude Opus 5
값의 출처가 컴파일된 CSS로 옮겨간 뒤 매니페스트가 제공하던 것은 Figma 변수 경로와
"원본에선 참조였으나 컬렉션이 없어 값으로 굳은 것"(chart-alt 18개) 표시 둘뿐이었다.
둘 다 쓰지 않기로 해 생성물·화면·변환기에서 모두 걷어낸다.
변환기는 이제 SCSS만 생성한다. 화면은 스타일시트 외에 아무것도 참조하지 않는다.
Co-Authored-By: Claude Opus 5
SCSS를 고치면 화면에 즉시 반영되게 하려는 것이다. 매니페스트를 값의 출처로 두면
생성물과 화면이 어긋날 수 있는데, 스타일시트를 직접 읽으면 그 여지가 없다.
핵심은 계산값이 아니라 **선언 원문**을 읽는 것이다. 계산값은 참조가 이미 풀려 있고
현재 테마·뷰포트 한쪽만 보이지만, 원문에는 참조(var(--fox-…))와 light/dark 양쪽,
PC/모바일 양쪽이 모두 남아 있다. 덕분에 다크 테마에서도 Light 섹션이, 넓은 창에서도
Mobile 섹션이 정확한 값을 보여준다.
light-dark()는 Lightning CSS가 폴리필한 형태로 나오므로 그 형태를 먼저 파싱하고,
폴리필이 없는 설정을 대비해 원형도 함께 처리한다. 괄호 짝을 세어 자르는 이유는
값 안에 var()가 중첩돼 정규식으로는 안전하게 나눌 수 없기 때문이다.
매니페스트는 CSS가 담지 못하는 두 가지만 제공한다 — Figma 변수 경로, 그리고
"원본에선 참조였으나 컬렉션이 없어 값으로 굳은 것"(chart-alt 18개) 표시.
Co-Authored-By: Claude Opus 5
5px에서는 밝은 회색 계열(#FFFFFF · #F4F5F6 · #E6E8EA)이 서로 구분되지 않았고
반투명 토큰의 알파 여부도 보이지 않았다.
16px는 icon-3(1.6rem)과 같은 값이라 리터럴 대신 토큰을 쓴다 — 5px일 때는 스케일에
맞는 값이 없어 리터럴이 불가피했던 예외가 사라졌다.
Co-Authored-By: Claude Opus 5
Figma 변수 페이지와 1:1로 대조하기 위한 감사 화면으로 바꾼다. 분류를 우리 CSS 그룹이
아니라 원본 컬렉션/모드로 두고 — Mode(Light/Dark) · Responsive(PC/Mobile) · Size ·
Theme · Primitive — 파일에 값이 적혀 있으면 그 값을, 다른 토큰을 가리키면 참조 토큰을
표시한다. 토큰 이름 아래에 Figma 변수 경로를 함께 적는다.
CSSOM 읽기로는 이 요구를 만족할 수 없어 걷어냈다. CSS는 alias를 var()로 흡수하고
light/dark를 light-dark() 한 줄로 합치므로 "원본이 값을 적었는지 토큰을 가리켰는지"를
되살릴 수 없다. 대신 변환기가 원본 구조 그대로를 담은 매니페스트를 함께 생성한다 —
여전히 자동 생성이라 목록을 손으로 관리하지 않는다.
미리보기도 CSS 변수가 아니라 매니페스트의 해석값을 쓴다. 변수를 쓰면 PC 섹션을 좁은
창에서 볼 때 모바일 값이, Light 섹션을 다크 테마에서 볼 때 다크 색이 그려진다.
⚠️ 매니페스트를 만들며 잡은 것: 이 export에는 `theme`이라는 이름의 컬렉션이 둘이고
(제공된 파랑 계열, 미제공 청록 계열) 변수 경로까지 같아, 이름만으로 매칭하면 chart-alt
18개가 엉뚱하게 해결된 것처럼 보인다. 값 대조를 넣어 "미제공 컬렉션"으로 표시한다.
SCSS 생성 쪽은 원래부터 값 대조를 하고 있어 영향이 없었다.
Co-Authored-By: Claude Opus 5
토큰 표기를 `fox.color(background-default)`에서 `color-background-default`로 바꾼다.
`--fox-` 접두사만 뗀 실제 CSS 변수 이름이라 devtools에서 보는 것과 1:1로 대응하고,
함수 표기처럼 그룹명이 괄호 밖으로 빠지지 않아 목록을 훑을 때 이름이 바로 읽힌다.
표기를 만드는 함수가 하나뿐이라 스와치·서체 목록·값 표가 갈릴 일이 없다.
그 결과 쓰이지 않게 된 fn prop을 제거한다.
사이드바의 "추가 방법"은 컴포넌트 등록 여부와 무관하게 목록 맨 아래에 고정한다 —
이전에는 컴포넌트가 하나라도 생기면 사라져 추가 절차를 찾을 수 없었다. 컴포넌트가
아니라 안내이므로 점선 테두리와 흐린 글자로 등록된 탭과 층위를 구분한다.
Co-Authored-By: Claude Opus 5
값만 보여주던 막대를 걷어내고 그룹별로 실제 적용 결과를 보인다 — 서체는 그 속성을
적용한 문장, 패딩·간격은 문단 두 개 사이의 공간, 반경·테두리는 실제 테두리를 가진
div, 아이콘은 브라우저 기본 체크박스, 그림자는 실제 box-shadow, 배경 흐림은 사진 위
절반에만 건 backdrop-filter다. 원시 수치는 값만 남긴다.
그림자는 Figma에 offset·blur 수치만 있고 조합과 색이 정의돼 있지 않다. 각 수치의
효과를 보이려고 location은 `0 값 0`, blur는 `0 0 값`으로 걸고 색은 실재 토큰인
alpha-default-inverse25를 쓴다 — 조합 자체는 토큰이 아니며 섹션 설명에도 적었다.
배경 흐림 예제 이미지는 next/image가 아니라 다. @fox가 Next에 의존하면 폴더째
복사되는 이식성이 깨지고 외부 호스트마다 remotePatterns 설정이 필요해진다.
Co-Authored-By: Claude Opus 5
토큰 배치 확인과 컴포넌트 예제를 한 페이지에서 본다. 사이드바는 토큰/컴포넌트
두 대분류이며, 토큰 그룹은 CSSOM에서 읽으므로 목록을 코드에 두지 않는다.
컴포넌트는 component-registry의 배열에 항목 하나만 추가하면 사이드바와 본문이
함께 갱신된다 — 만들 때마다 다른 파일을 고칠 필요가 없다.
하니스를 앱이 아니라 @fox에 두는 이유는 폴더를 다른 프로젝트로 복사할 때 함께
따라가게 하기 위함이다. 그래서 앱 상수(테마 저장 키 등)를 import 하지 않고,
테마 전환은 @fox가 소유한 data-theme 계약만 건드린다(저장하지 않는다).
선택 상태는 URL 해시가 SSOT라 새로고침해도 보던 섹션이 유지된다.
같은 목적의 카탈로그를 두 벌 두면 갈라지므로 구 app/design은 함께 제거한다.
Co-Authored-By: Claude Opus 5