feat(pkg-ui): 디자인 시스템을 패키지로 옮기고 스토리북을 붙인다 - #26
Merged
Conversation
원시 색과 시맨틱 색을 packages/ui가 소유하고, 앱은 panda.config.ts의 presets로 확장한다. 값은 그대로다. preset을 값이 아니라 함수로 둔 이유 — bg.chrome의 기본 알파를 앱에서 받아야 하는데, 그 값을 가진 shared/config/glass.ts는 Rust window_glass.rs와 짝이라 옮길 수 없다. preset이 앱을 import하면 패키지가 플랫폼 중립을 잃는다. presets를 지정하면 @pandacss/preset-panda가 자동으로 빠져 spacing·shadows 같은 프리셋 토큰이 사라진다. @pandacss/dev/presets로 다시 명시해 새 의존성 없이 되살렸다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
폰트·프리뷰 크기·행간·모서리와 keyframes·layerStyles·조건(dark·glass)이 preset으로 간다. 앱 config에는 추출 대상·생성물 위치·전역 CSS·훅만 남는다. 조건을 함께 옮긴 이유 — bg.canvas가 _glass를 쓰기 때문에, 토큰과 조건이 다른 파일에 있으면 preset만으로 그 토큰이 풀리지 않는다. 폰트 토큰은 이름만 갖는다. woff2 파일과 @font-face는 앱이 서빙한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
definePreset에 config:resolved를 선언하고 radii.lg를 지우게 했으나 생성물에 그대로 남았다. 같은 omit이 앱 config에서는 동작한다 — 훅은 소비 측 설정에만 둘 수 있다. 열린 결정에서 이 항목을 덜어내고 styled-system 생성 위치만 남긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
컴포넌트가 css()를 import하려면 패키지에도 codegen이 필요하다. 패키지는 자기 설정으로 css()와 타입을 만들고, 앱은 그것을 쓰지 않고 패키지 소스를 include에 더해 같은 규칙을 자기 CSS에 담는다. turbo가 typecheck·test·build 앞에 codegen을 세운다. 두 설정의 스케일이 갈리면 같은 스타일이 다른 클래스 이름을 얻는다. 지우는 단계 목록을 panda-scale.ts로 빼 양쪽이 같은 것을 쓰게 했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Button·IconButton의 구현과 스타일 정의 테스트가 @norii/ui로 가고, shared/ui는 재노출만 한다. 렌더를 보는 browser 테스트는 앱에 남는다 — 앱이 빌드한 CSS를 검증하는 자리다. 패키지 vitest가 styled-system을 풀지 못해 별칭을 더했다. 앱 쪽은 vite-tsconfig-paths가 앱 tsconfig 밖 파일을 다루지 않아 같은 별칭이 필요했고, vite.config과 vitest.config이 각자 갖고 있던 katex 별칭과 함께 vite-aliases.ts로 모았다. 두 설정의 해석이 갈리면 테스트가 통과한 것과 앱이 번들하는 것이 달라진다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
셋 다 react와 styled-system만 쓰는 컴포넌트다. shared/ui에는 재노출만 남는다. 껍데기 스타일 테스트도 함께 옮기고, 렌더를 보는 browser 테스트는 앱에 남긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
svg 원본과 생성물, svgr 설정, mise 태스크가 packages/ui로 간다. 앱은 shared/ui 배럴로 재노출한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
배너를 쓰던 네 곳이 각자 div와 클래스를 조립하고 있었다. @norii/ui의 Banner·BannerBody가 껍데기를 갖고, 무엇을 언제 띄울지는 각 기능이 정한다. danger 표시는 tone 변형으로 옮기고, 스타일만 있던 banner-styles.ts는 지운다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
패키지 panda.config가 유리 불투명도를 손으로 적고 있었다. 그 값은 bg.chrome 토큰과 한 몸이라 preset이 소유하고, 앱의 glass.ts는 재노출만 한다. createNoriiPreset을 앱에서 세 번 부르고 있었다. 인자가 갈리면 대비 게이트가 빌드에 없는 값을 검사하게 되므로, shared/config/norii-preset.ts가 하나를 조립해 셋이 공유한다. 재노출 방식이 배럴과 스텁 파일 둘로 갈려 있었다. 스텁을 없애고 배럴 하나를 경계로 둔다. 아이콘 이름도 세 곳에 손으로 유지되던 것을 icons/index.ts 한 곳으로 줄인다. turbo codegen 태스크에 inputs가 없어 소스를 고칠 때마다 두 패키지의 codegen이 다시 돌았다. 실제로 읽는 파일만 입력으로 잡는다. 값의 소유자가 panda.config.ts라고 가리키던 문서·주석 아홉 곳을 preset으로 옮긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
컴포넌트 하나가 폴더 하나를 갖고 구현과 테스트가 그 안에 산다. 폴더 안에 배럴은 두지 않는다 — src/index.ts가 ./button/button을 직접 집는다. 배럴을 겹치면 공개 면이 두 겹이 된다. 경계 규칙 문장도 고친다. "상태를 알지 않는다"는 사실이 아니었다 — Dialog는 닫힘 전환과 포커스 복원을 위해 상태를 갖는다. 실제 선은 앱을 아느냐다: 스토어·IPC·UI 문자열을 모르면 패키지, 알면 앱이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
네 기준으로 걸렀다 — 지워도 안 깨지는 것, 게이트·타입·테스트가 잡는 것, 틀리면 화면에 바로 보이는 것, 문서가 이미 소유한 것. import하는 쪽이 알아야 하는 것은 TSDoc으로 올렸다. 남은 주석의 주장 셋이 사실과 달라 고쳤다. - 패키지 preflight가 리셋을 두 번 넣는다고 적혀 있었으나, panda codegen은 CSS를 만들지 않고 기본값도 false다. 줄과 주석을 지웠다. - compoundVariants를 spread도 반복문도 못 쓴다고 적혀 있었으나 .map()은 출력이 같고 spread만 px_0.5·px_1을 잃는다. spread만 금지로 좁혔다. - omit 목록이 다르면 클래스 이름이 갈린다고 적혀 있었으나 클래스는 같고 토큰 타입 union이 좁아진다. 실제 결과로 바꿨다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
storybook · @storybook/react-vite · @storybook/addon-docs 10.5.7을 packages/ui의 devDependency로 둔다. addon-docs는 tags: ["autodocs"]가 요구하는데 프레임워크가 끌어오지 않아 명시했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.storybook/main.ts가 react-vite 프레임워크와 addon-docs를 물린다. preview.tsx는 data-theme을 바꾸는 툴바와 캔버스 배경 데코레이터를, preview.css는 Panda 레이어 순서를 둔다. 카탈로그가 도는 Vite는 앱 밖이라 tsconfig 경로가 닿지 않는다. viteFinal에서 styled-system 별칭을 붙였다. 패키지 CSS를 카탈로그가 그대로 쓰기 때문에 panda.config의 preflight를 켠다. tsconfig include와 panda include에 .storybook을 더해 타입·추출 범위에 넣는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mise run storybook은 6006 포트로 띄우고, mise run storybook-build는 정적 빌드를 낸다. 생성물 storybook-static/은 VCS에서 제외한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Button·IconButton에 개요를 먼저 두어 변형×크기를 한 화면에서 비교하게 하고, 그 뒤에 Controls로 조작하는 낱개 스토리를 둔다. 두 컴포넌트가 함께 쓰는 props에 toggle 의미와 className 범위를 TSDoc으로 적는다. .storybook/grid.tsx는 Section·Row·Cell·Grid를 인라인 스타일로 둔다. 스토리 배치용이라 패키지가 내보내는 컴포넌트와 섞이지 않게 .storybook 아래에 뒀다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
네 컴포넌트 모두 개요를 먼저 두어 변형과 상태를 한 화면에 늘어놓고, 그 뒤에 Controls로 조작하는 낱개 스토리를 둔다. Dialog는 열림 상태를 스토리가 들고 있어야 해 render에서 useState로 연다. Tooltip은 기준 요소에 position: relative가 없으면 엉뚱한 조상에 붙어, 그 조건을 갖춘 래퍼를 함께 둔다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
exports에 "./icons"를 더해 @norii/ui/icons로 아이콘만 가져올 수 있다. 배럴(".")로도
계속 나가므로 기존 import는 그대로 동작한다.
아이콘 스토리는 배럴을 읽어 이름과 모양을 함께 보여준다. 라벨이 export 이름이라 보고
바로 import에 쓸 수 있고, 아이콘을 더하면 목록에 저절로 들어온다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
색 토큰을 배경·상태색으로 묶어 늘어놓고, 앱 설정과 같은 범위(0~1, 0.01 단위)의 불투명도 슬라이더를 둔다. 슬라이더는 --norii-glass-opacity를 바꾼다. 그 값이 닿는 토큰은 bg.chrome 하나이고 패키지 컴포넌트는 쓰지 않아 화면에 나타날 곳이 없다. 그 토큰을 직접 칠한 면을 스토리에서 만들고, 뒤에 격자를 깔아 bg.canvas가 유리에서 투명해지는 것도 함께 보이게 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
테스트 전략에 Storybook이 테스트가 아니라는 것과, 시각 회귀의 단일 출처는 Vitest Browser Mode 스크린샷이라는 것을 적는다. 도입 여부를 묻던 열린 결정을 지우고, 파일/폴더 구조의 packages/ui 설명에 카탈로그와 실행 방법을 더한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
기본 react-docgen이 Button·IconButton의 props를 풀지 못해 Docs에 표가 나오지 않았고, Tooltip은 @PARAM·@description 태그가 설명란에 그대로 찍혔다. react-docgen-typescript로 바꿔 props별 TSDoc이 표의 설명 열로 들어가게 했다. 상속받은 HTML 속성이 표를 덮지 않도록 propFilter로 node_modules를 뺀다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Button·IconButton·Dialog·Select에 설명이 없어 Docs 첫 줄이 비어 있었다. Dialog의 open·size·dialogRef, Select의 wrapClassName, Banner의 tone에 설명을 붙여 props 표에서 읽히게 한다. Tooltip은 @PARAM으로 적혀 있어 표에 들어가지 않았다 — react-docgen-typescript는 멤버 TSDoc만 읽으므로 props 각각으로 옮겼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
스토리 설명에서 화면에 보이는 것·props 표가 이미 말하는 것·같은 문장이 반복되는 것을 지우고, 판정 기준과 API 사실만 남긴다. controls.exclude가 Controls 패널뿐 아니라 Docs의 props 표에서도 그 행을 지워, Button 표에 variant·size가 통째로 빠져 있었다. argTypes의 control: false로 바꿔 행은 남기고 조작만 막는다. Dialog Trigger에 있던 "showModal이라 동시에 띄울 수 없다"는 사실이 아니라(modal dialog는 top layer에 쌓인다) 지운다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
배경 프리셋을 bg.canvas·bg.chrome·bg.paper로 둬 컴포넌트를 앱에서 실제로 놓이는 면 위에서 본다. var()로 적어 테마 툴바를 따라간다. 창 폭 프리셋을 사이드바 240px·분할 480px·창 기본 1024px로 둔다. 기본값은 휴대폰 크기라 데스크톱 앱에 쓸 데가 없다. 여백은 데코레이터에서 빼고 parameters.layout에 맡긴다. 사이드바 순서는 Tokens·Icons를 앞으로 올려 토큰을 먼저 보게 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
손으로 만든 스와치와 아이콘 그리드를 ColorPalette·IconGallery로 교체한다. 쓰이지 않게 된 Grid 헬퍼는 지운다. Banner meta에 subcomponents로 BannerBody를 걸어 props 표에 함께 나오게 한다. Doc Block이 Canvas에서 렌더 중 예외를 던졌다 — Storybook 테마 컨텍스트를 읽는데 그것이 Docs에만 있다. 데코레이터에서 ThemeProvider를 넣어 해결했다. ColorItem에 var()를 넘기면 캡션에 변수 이름만 찍힌다. getComputedStyle로 읽은 계산값을 넘겨 테마별 실제 색과 알파가 보이게 했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@storybook/addon-a11y가 렌더된 DOM에 axe를 돌린다. 토큰 대비 게이트는 색 쌍만 보므로 실제 조합에서 나오는 위반은 잡지 못한다. 검사에서 두 건이 걸려 함께 고쳤다. - 스토리 라벨을 임의의 opacity로 흐려 대비 3.94 (AA 4.5 미달) — text.muted 토큰으로 교체 - Tooltip 스토리의 input이 라벨 없이 aria-describedby만 가져 Critical — aria-label 추가 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
네 스토리에 play를 붙인다. 모두 화면만 봐서는 갈리지 않는 것들이다. - Dialog 전환: 열리고, 취소를 누르면 전환이 끝난 뒤 사라진다 - Select Playground: 감싸는 요소를 하나 더 두는데도 값이 바뀌고 onChange가 불린다 - IconButton Playground: label이 접근성 이름이 되어 그 이름으로 버튼을 찾을 수 있다 - Tooltip Playground: id가 aria-describedby와 이어져 접근성 설명이 된다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
액션이 둘인 배너가 267px에서 둘째 버튼을 상자 밖으로 내보내 누를 수 없었다. 본문만 계속 줄어 279px에서는 한 줄에 한 글자가 됐다. 충돌 배너가 그 모양이면 어느 쪽을 남길지 고를 수 없다. flexWrap으로 액션을 아랫줄로 내리고, 본문에 하한을 둬 그 전에 본문이 다 눌리지 않게 했다. 좁은_폭 스토리에 240·280·400px을 나란히 둔다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
창 최소 크기가 없어 얼마든지 좁힐 수 있었고, 그 폭에서 액션이 둘인 배너가 무너졌다. 사이드바는 240px 고정이고 문서 칸이 나머지를 가져가므로, 640에서 문서 칸은 400px이다. 배너는 분할 칸 위에 있어 프리뷰를 나눠도 좁아지지 않는다 — 분할은 이 계산에 들어가지 않는다. minHeight는 두지 않는다. 계약 문서인 창 표면 계약에 값과 근거를 함께 적는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
분할 한쪽 480px은 어디서도 나오지 않는 값이었다. 두 칸은 flex 1로 반씩 나누므로 창 최소 폭 640에서 사이드바 240을 뺀 400의 절반, 200이 실제 값이다. 컴포넌트가 놓이는 칸을 넷으로 나눠 둔다 — 분할 한 칸 200 · 사이드바 240 · 문서 칸 400 · 창 전체 1024. 문서 칸은 탭바와 배너가 받는 가장 좁은 폭이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
279px·267px은 배너를 고치기 전에 잰 값이라 지금 동작을 설명하지 않는다. 고친 뒤에는 어느 폭에서도 그렇게 되지 않고, 382px 아래에서 액션이 아랫줄로 내려갈 뿐이다. 최소 창의 문서 칸 400px과는 18px 차이다. 칸 이름을 문서 칸으로 맞춘다 — 편집 영역은 에디터 칸과 헷갈리고, 문서 칸은 프리뷰 전략이 이미 쓰는 이름이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@storybook/addon-vitest가 스토리마다 렌더 테스트를 만들고, play를 단 스토리는 상호작용 까지 본다. 브라우저는 앱과 같은 WebKit이다. 패키지 테스트가 15개에서 37개가 됐다. Dialog 전환 play가 헤드리스에서 실패했다. dialogIn이 opacity 0에서 시작해 클릭 직후에 재면 jest-dom이 안 보이는 것으로 판정한다 — 대화상자를 찾은 뒤 보일 때까지 기다리게 고쳤다. 테스트 전략의 레이어 표에 스토리 테스트를 넣는다. 시각 회귀는 Vitest Browser Mode의 스크린샷이 단일 출처로 남는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@storybook/addon-mcp가 개발 서버에 localhost:6006/mcp를 연다. 에이전트가 컴포넌트 문서를 읽고 스토리 테스트를 돌릴 수 있다. 정적 빌드에는 들어가지 않는다. MCP 목록의 설명은 컴포넌트 TSDoc에서 나오는데 Tokens·Icons는 component가 없어 비어 있었다. 두 meta에 설명을 준다. project-structure의 "테스트가 아니다"는 스토리 테스트를 붙이면서 사실이 아니게 됐다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BANNER_STYLES·BUTTON_STYLES·DIALOG_STYLES가 배럴로 나가고 있었으나 앱에도 패키지 안에도 소비처가 없다. cva에 넘길 정의 객체라 컴포넌트와 옆의 테스트만 쓰고, 테스트는 모듈에서 직접 가져온다. 내보낸 채로 두면 recipe 구조가 계약이 되어 base·variants 모양을 바꿀 때 공개 API 변경이 된다. 세 모듈의 export는 그대로 두고 배럴에서만 뺀다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MCP 엔드포인트가 정적 빌드에 없다는 것과 브라우저가 WebKit인 이유는 기술 스택 표와 테스트 전략이 소유한다. 설정 파일에서 되풀이하지 않는다. vitest.config의 node·브라우저 분리 설명은 설정 자체가 말한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Panda 조건에 light를 더해, 다크인 트리 안에서 하위만 라이트로 되돌릴 수 있게 한다. 지금까지 조건은 dark뿐이라 라이트는 기본값으로만 있었다. 그래서 data-theme="light"를 하위 요소에 걸어도 맞는 규칙이 없어 바깥에서 물려받은 다크 값이 그대로 남았다 — 루트가 다크일 때 light와 dark를 따로 읽으면 두 결과가 같았다(text가 양쪽 다 #e5e5e5). 값은 base와 _light 두 곳으로 나가지만 themed()가 인자로 한 번만 받는다. 두 곳에 손으로 적으면 한쪽만 고쳐 어긋나므로, _light가 base와 같은지 테스트가 지킨다. 기본값은 그대로 두므로 data-theme이 없을 때의 값은 바뀌지 않는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
버튼·아이콘버튼 스토리가 변형과 크기를, 배너 스토리가 톤을 손으로 적은 배열 대신 recipe에서 읽는다. 변형을 더해도 스토리를 고치지 않고, 목록이 실제 recipe와 어긋나지 않는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
색 토큰 카탈로그를 스토리에서 Foundations/Colors 문서 페이지로 옮긴다. 그룹마다 light 띠와 dark 띠를 위아래로 두고 값을 각 색 바로 밑에 적어, 두 모드를 한 화면에서 견준다. 모드로 갈리는 그룹을 앞에 모은다. ColorPalette·ColorItem을 스토리 안에서 쓰면 "Cannot read properties of undefined (reading 'fonts')"로 렌더가 실패한다 — Doc Block은 문서 페이지의 컨텍스트를 전제한다. 그래서 .mdx로 옮기고 main.ts의 stories에 .mdx glob을 더했다. 문서 페이지에는 데코레이터가 돌지 않아 루트에 data-theme이 걸리지 않는다. 잠깐 붙였다 떼는 요소에 테마를 걸어 두 모드의 계산값을 읽는다. CSS 변수 이름은 Panda codegen의 token.var가 준 것을 그대로 쓴다. 손으로 케밥으로 바꾸면 Panda 규칙(대문자 앞마다 대시)과 어긋난 토큰이 빈 값을 받아 빈 칸으로 조용히 통과한다. 유리 불투명도 스토리가 같은 파일에 있었으므로 Foundations/Glass로 나눈다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
아이콘 카탈로그를 스토리에서 Foundations/Icons 문서 페이지로 옮긴다. 배럴이 내보내는 아이콘을 그대로 훑어 갤러리·크기·색 세 절로 보인다. IconGallery·IconItem도 Doc Block이라 스토리 안에서 렌더가 실패하므로 .mdx로 옮겼다. 색 절은 라이트와 다크를 나란히 놓는다. 문서 페이지에는 데코레이터가 돌지 않아 테마 툴바가 닿지 않는다 — 툴바를 기다리는 대신 두 모드를 각각 data-theme으로 감싼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사이드바를 두 묶음으로 나눈다. 색·유리·아이콘은 Foundations, 컴포넌트 여섯은 Components다. 전에는 버튼만 슬래시 제목이라 혼자 대문자 머리를 달고 한 겹 더 들어가 있었고, 이름이 Button/Button으로 겹쳤다. 나머지는 평평해서 문서 페이지와 컴포넌트가 같은 층위에 섞였다. storySort는 Foundations만 나열해 Colors를 첫 장으로 두고, Components는 가나다순으로 둔다 — 컴포넌트가 늘어도 이 파일을 고치지 않는다. method 없이는 파일을 읽은 순서라 IconButton이 Dialog 앞에 왔다. 다이얼로그 Trigger의 size는 손으로 적은 유니온 대신 DialogProps에서 가져온다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
테마를 스토리에만 걸어, 문서 페이지는 Storybook 자신의 밝은 화면으로 남긴다. 다크 스토리를 본 뒤 사이드바에서 문서 페이지로 넘어가면 화면이 통째로 검게 나왔다. 데코레이터가 루트에 찍은 data-theme이 남아 배경 애드온이 bg.canvas를 #0a0a0a로 칠하는데, 문서 본문 글자는 어두운 색이라 아무것도 보이지 않았다. 데코레이터는 이제 자기 요소에 data-theme을 걸고, 캔버스일 때만 루트에도 건다 — 배경 툴바가 루트에서 토큰을 읽기 때문이다. 스토리가 없는 문서 페이지에는 데코레이터가 돌지 않으므로 docs.container가 루트에서 그 표식을 지운다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
색·아이콘 카탈로그가 그리는 코드를 사이드바에 내보내지 않는 스토리로 돌린다. 모든 토큰이 값을 갖는지, 배럴의 아이콘이 모두 그려지는지 게이트가 판정한다. 카탈로그 페이지는 `.mdx`라 게이트가 돌지 않는다 — addon-vitest가 `.mdx`를 빈 모듈로 바꿔치기한다. `.mdx`여야 하는 것은 색표·아이콘표를 그리는 Storybook 컴포넌트뿐이고, 그 밖의 코드는 평범한 스토리로 돌릴 수 있다. 토큰 이름이 preset과 어긋나면 계산값이 빈 문자열이 되어 띠에 빈 칸이 남는다. 예외가 나지 않아 카탈로그를 열기 전에는 드러나지 않던 것을 이 스토리가 잡는다. token.var에 넘기는 경로가 codegen의 리터럴 타입과 맞지 않아 TS2345가 나 있었다. 경로는 preset에서 만들어 정적으로 알 수 없으므로 캐스팅하고, 없는 이름이면 실행 중 예외를 던진다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dialog는 여백을 갖지 않는다. DialogHeader·DialogBody·DialogFooter를 넣어 조립하며 셋 다 선택이다. - size(폭+여백) → width(폭) - 각 영역에 divider를 주면 본문과 나누는 선이 생긴다 폭과 여백이 한 값에 묶여 있어 넓으면서 여백 있는 상자를 만들 수 없었다. 설정 화면은 머리글과 왼쪽 목록이 상자 가장자리에 닿아야 해서 lg를 썼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
버튼은 BannerActions 안에 넣는다. 자리가 모자라면 버튼이 하나씩이 아니라 덩어리째 아랫줄로 내려가고, 내려간 줄에서도 오른쪽에 붙는다. 배너를 쓰는 네 곳(충돌·삭제됨· 정규화·알림)을 옮겼다. 버튼이 배너의 직접 자식이던 때는 액션이 둘인 배너를 267px에서 재니 둘째 버튼이 상자 밖으로 나가 누를 수 없었다. 충돌 배너가 그 모양이면 어느 쪽을 남길지 고를 수 없다. - 버튼 사이 간격 16px → 8px. 240px에서 두 버튼이 한 줄에 들어간다 - 좁은_폭 스토리의 play가 400·280·240px에서 위치를 재서 확인한다 - 스타일 객체를 다시 읽던 banner.test.ts의 wrap·minWidth 검사를 그 play로 옮겼다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
방향키 처리 함수의 주석에서 함수 이름·코드가 이미 말하는 것을 덜고, 브라우저가 방향키로는 포커스를 옮기지 않는다는 것만 남긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
whiteSpace를 nowrap에서 pre로 바꾼다. nowrap은 \n을 공백으로 만들어 줄바꿈을 넣어도 한 줄로 이어졌다. pre는 저절로 줄을 바꾸지 않는 것은 같고 \n만 살린다. 스토리에 줄바꿈 칸을 더하고 play가 세 툴팁의 높이를 비교한다 — 긴 글은 짧은 글과 높이가 같고, \n을 넣은 것만 높다. 스토리의 툴팁이 입력칸 아래가 아니라 오른쪽에 뜨던 것도 고쳤다. 컴포넌트는 가로 위치를 정하지 않으므로 앱과 같이 left를 넘긴다. top:100%가 padding까지 세고 내려가 칸 이름을 덮던 것은 자리를 비우는 요소와 기준이 되는 요소를 나눠 고쳤다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
360px 상자에 넣기만 하면 셀렉트가 따라 늘어난다고 보여주던 칸이 실제로는 139px에 머물렀다. 감싸는 요소가 inline-flex라 flex 줄 안에서 내용 폭에 머문다. 폭은 wrapClassName으로 감싸는 요소에 주어야 하고 앱도 그렇게 쓴다. 칸을 그 방식으로 바꾸고 play가 두 칸의 폭을 잰다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
slot recipe(sva)를 쓴다고 적혀 있었으나 코드에 sva가 없다. 여러 요소로 이루어진 컴포넌트는 요소마다 따로 내보내고 부모는 여백을 갖지 않는다. 컴포넌트가 사는 곳도 packages/ui로 맞춘다 — 같은 문서의 FSD 그림과 규칙 목록이 서로 달랐다. 문서 경계 목록에 스토리북을 넣는다. 컴포넌트 하나하나의 모양·props·조립 방법은 스토리북이 소유하므로 이 문서에 쓰지 않는다. 같은 규칙이 두세 번 나오던 것(시맨틱 토큰·recipe·컴포넌트 위치·bg.canvas·활성 탭)과 바로 위 그림이 말하는 것을 덜어냈다. 폰트 토큰 이름 규칙은 테마 절에 있어 토큰 계층으로 옮겼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- `mise run ui-coverage` — 스토리가 실행하지 않는 코드를 드러낸다. 임계값은 두지 않는다.
브라우저가 WebKit이라 v8 대신 istanbul로 잰다.
- 다이얼로그에 `globals: { theme: "dark" }` 짝을 둔다. 스토리 테스트는 기본 테마로만 돌아
다크에서 달라지는 값이 검사되지 않았다.
- CI가 `storybook-build`를 돌린다. `.mdx` 카탈로그 페이지는 게이트가 컴파일하지 않는다 —
import를 깨뜨려도 test·typecheck는 통과하고 빌드만 실패한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
기술 스택의 테마·배경·창 폭 툴바 열거를 뺀다. 스토리북을 열면 보인다. 창 표면 계약의 minWidth 근거에서 배너 폭 실측을 뺀다. 액션을 묶은 뒤로 500px 아래에서 아랫줄로 내려가므로 "최소 창에서도 한 줄"이 성립하지 않는다. 좁은 폭에서의 모습은 좁은_폭 스토리가 400·280·240px로 보여준다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codegen 태스크가 자기 패키지 안의 파일만 해시해, @norii/ui의 preset을 고쳐도 앱의 codegen이 캐시를 그대로 replay했다. 토큰 값을 바꾸고 돌리면 앱 styled-system은 옛 값을 들고 있고 그 위에서 typecheck·test가 통과한다. dependsOn: ["^codegen"]으로 의존 패키지의 codegen 해시를 물린다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@norii/ui를 두 줄로 나눠 가져오던 두 곳을 한 줄로 둔다. oxlint에 no-duplicate-imports가 없어 걸리지 않았다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
한 리포에 두 버전이 핀되면 브라우저 캐시가 갈린다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
배너·버튼·셀렉트·툴팁에 다크 짝을 붙여 다섯 컴포넌트가 모두 검사된다. 토큰을 안 쓰고 색을 박으면 라이트 스토리는 통과하고 이쪽만 실패한다. 값은 적지 않고 resolvedColor로 같은 트리에서 변수를 풀어 잰다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
안에 넣으면 select·button처럼 div를 담을 수 없는 요소에 들어가고, 재는 대상의 DOM을 건드린다. 형제로 두어도 같은 테마 트리라 값은 같다. 버튼 스토리는 각 버튼이 자기 토큰을 읽게 한다 — accent 값을 outline 버튼에서 읽고 있었다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
요약 / 의도
토큰과 공용 컴포넌트를 앱에서
@norii/ui로 옮기고, 그 패키지에 스토리북 카탈로그를 붙인다. 카탈로그는 눈으로 보는 자리에서 끝나지 않고 스토리가 곧 게이트의 렌더 테스트가 된다.범위
이번에 한 것
packages/ui로 이동. 앱은 preset을 확장하고shared/ui가 재노출한다addon-vitest로 스토리를 WebKit 렌더 테스트에 올리고, 눈으로 못 보는 계약은play가 계산된 값으로 확인width+DialogHeader·DialogBody·DialogFooter), 배너 액션을BannerActions로 묶음, 툴팁이\n에서 줄바꿈mise run ui-coverage)·다크 테마 스토리·카탈로그 정적 빌드(CI)를 검사에 추가의도적으로 안 한 것
testing.md에 적었다주요 변경점
shared/ui가@norii/ui를 재노출하는 얇은 층이 됨.dialog.tsx·banner-styles.ts는 패키지로 이동해 삭제packages/ui):createNoriiPreset(알파를 인자로 받는 함수)·컴포넌트·스토리·아이콘 코드젠. 자체styled-system을 만든다mise run storybook·storybook-build·ui-coverage, CI에 정적 빌드 스텝, turbocodegen태스크설계 · 결정
bg.chrome의 알파를 인자로 받아 소비 측이 덮어쓴다. 앱은shared/config/norii-preset.ts에서 한 번 조립해panda.config와 대비 게이트 테스트가 같은 인스턴스를 쓴다codegen에dependsOn: ["^codegen"]— 없으면 패키지 preset을 고쳐도 앱이 캐시를 replay해 옛 토큰으로 검사가 통과한다(실증)문서와 달라진 점
design-system.md가 "여러 요소는 slot recipe(sva)"라고 했으나 코드에sva가 없다 → 조립형으로 문서를 고침window-chrome.md의minWidth근거였던 배너 폭 수치가 실측과 어긋나 뺌(실측 500px, 문서 382px)검증 (체크가 아니라 값으로)
mise run check: 통과 — typecheck 6/6 · test 6/6 · rust 89 passed · docs-drift 정합(플랫폼 상수 3 · Rust 커맨드 15 · npm 53 · 크레이트 14)play는 모두 깨뜨려 실패를 확인한 뒤 남겼다(nowrap·minWidth: 0·marginLeft제거·flex: 0 1 auto·하드코딩 색)pnpm --filter desktop build통과, 975KB)관련 문서 (단일 출처)
.claude/docs/design/design-system.md.claude/docs/design/window-chrome.md.claude/docs/testing.md.claude/docs/tech-stack.md.claude/docs/development-commands.md.claude/docs/project-structure.md리뷰 포인트 / 위험 지점
shared/ui/index.ts가export *로 바뀌어 앱의 공개 표면이 패키지를 따라간다packages/ui와 앱이 각자 codegen을 돈다 — 스케일 목록(panda-scale.ts)을 공유해야 클래스 이름이 갈리지 않는다체크리스트 (프로젝트 규칙)
.claude/rules/non-goals.md)를 넘지 않음마일스톤 / 비고
계획 범위 밖 작업이라
implementation-plan.md에 해당 단계가 없다.