공통 시각 기반 — token·layer·scope·overlay 계약 U01 v1
- 계획: U01 / #1289. props·접근성 정본, 총괄.
- 기준선: main
16b8e99684a4cb152b2ff5d89b5aea10a7f055e0, 2026-09-07. 새 코드ui/shared/styles/ui-primitives.css와ui/shared/primitives/**가 이 계약을 소비한다.
기존 구조와 선택
| 실제 코드 | 현재 책임 | U01 판단 |
|---|---|---|
mobile styles/styles.css, base/tokens.css | .app, .lg-shell-scope의 색·간격, CSS import manifest | 이름·로드 순서 보존 |
desktop styles/desktop.css | .dk-app, .adm 토큰; mobile과 값 독립, dk-/adm- 접두 | 독립 진화 유지, 공통 의미만 공유 |
shared styles/app-host.css | html/body/#vite-root·viewport·부팅 지면, 의도적인 unlayered | U03/B02/N01 호스트 책임, 이동하지 않음 |
shared styles/shell.css | login/splash/data gate/toast | base primitive import 한 줄, 기존 shell 규칙 보존 |
mobile lib/Modal.tsx | .app root portal; 스크롤/transform에 잘리는 scrim 방지 | U02가 이식, 기존 포털 계약 유지 |
desktop lib/DkOverlay.tsx | scrim/바깥 클릭/Escape, desktop 토큰 | U03가 이식, 신규 dialog와 혼합 중첩 금지 |
mobile shell/bottomChrome.tsx, base/core.css | 채널과 --bottom-clearance 유도식 | 하단 크롬 계약 그대로 |
초기 조사에서 shared에는 button/input/dialog 접근성을 묶는 primitive가 없고 플랫폼별 껍데기가 있었다. U01은 신규 작은 primitive와 계약을 추가한다. 기존 색·간격을 하나의 전역 palette로 복사하거나 모든 화면을 바꾸지 않는다.
레이어와 범위
개념적인 순서는 host/reset → tokens → primitive → feature → 플랫폼 예외다. 실제 이름은 기존 tokens < base < screen < shell < overrides를 유지한다. host는 앱 바깥 지면 소유라는 뜻이며 현재 unlayered 규칙의 CSS 우선순위를 낮춘다는 뜻이 아니다.
| 층 | 실제 선언/범위 | 소유 |
|---|---|---|
| host/reset | app-host.css의 제한된 html/body/#vite-root/gym-app-*; base/core reset | 호스트 U03/B02/N01, 공용 reset U07 |
| token | 기존 tokens layer; 플랫폼 루트마다 독립 원값 | 공통 의미 U01, 플랫폼 값 U02/U03 |
| primitive | 신규 @layer base.primitives, .lg-ui-*만 | U01 → 정리 U07 |
| feature | 기존 screen layer; 모바일 .app + 화면 접두, desktop .dk-*/.adm-* | 각 화면 U02/U03 |
| shell | 기존 shell layer·.lg-shell-scope | shell 담당, 최종 wiring A15 |
| 예외 | overrides 또는 문서화된 host unlayered | 해당 플랫폼 담당 + U07 |
base.primitives는 기존 base 안의 sublayer다. base의 기존 비중첩 규칙이 그보다 우선하므로 전역 reset이 새 input 속성을 덮는지 소비 화면에서 확인한다. 모든 lazy CSS는 기존 top-level layer 순서 선언을 유지한다. 새 top-level layer를 파일 하나에만 추가하지 않는다. shell.css가 공통 CSS를 로드하므로 앱 entry/root Vite 설정을 바꾸지 않는다. standalone host error에서는 기존 host 스타일이 계속 기준이다.
새 CSS는 element 전체/button 전체/:root palette를 재정의하지 않는다. 다른 플랫폼의 클래스를 선택하거나 내부 DOM을 가로질러 손보지 않는다. U02/U03는 새로운 전체 화면을 하나의 공통 layout으로 맞출 의무가 없다.
의미 token
별칭은 소비 요소 자신에 선언해 그 위치의 플랫폼 값을 해석한다. 루트에서 한번 계산된 커스텀 속성이 다른 scope/portal에서 굳는 문제를 피한다. native dialog는 top layer로 그려져도 원래 DOM 조상의 토큰을 상속한다. 플랫폼 scope 바깥에서는 독립적인 기본값이 적용된다.
| token | 의미/원값 | 변경 소유 |
|---|---|---|
| --ui-text / --ui-text-secondary | --ink / --ink-2 | 의미 U01, 플랫폼 값 U02/U03 |
| --ui-surface / --ui-border | --surface / --line-2 | 위와 같음 |
| --ui-focus | --accent, 플랫폼 밖 #5268ab | focus 표시 U01; 해당 지면에서 대비 검증 |
| --ui-error-text | #a32927, 밝은 기본 지면의 오류 문구 | U01; 상태 아이콘용 --miss를 본문에 그대로 쓰지 않음 |
| --ui-gap / --ui-radius | --gap / --r-sm | 플랫폼 간격·형태 독립 |
| --ui-target-min | 44px, 신규 input/dialog action의 최소 높이 | U01; 기존 버튼 전체 크기는 이 PR에서 바꾸지 않음 |
| --ui-focus-width / --ui-focus-offset | 2px / 3px | U01, outline 삭제 금지 |
밀도/타이포/화면 여백·desktop canvas zoom·tier palette는 위 token에 합치지 않는다. 간격은 같은 의미 이름을 써도 플랫폼 값이 달라도 된다. 새 theme는 실제 조합에서 일반 글자 4.5:1, 큰 글자·focus/컨트롤 구별 3:1 기준을 확인한다. disabled를 제외한 오류·상태는 색만으로 의미를 전달하지 않는다. forced-colors에서는 시스템 색과 오류 점선을 쓰며 새 primitive에는 필수 동작 animation이 없다.
overlay·z-index·필요 예외
서로 다른 stacking context의 숫자를 단일 전역 순서로 비교하지 않는다. 신규 UiDialog는 browser top layer라 임의의 z-index: 99999가 필요 없다. 기존 absolute root portal과 viewport/transform 해법은 각 소비자가 이식될 때까지 보존한다.
| 현재 자원/예외 | 근거 | 소유·제거 조건 |
|---|---|---|
| mobile confirm-scrim z=70, picker sheet 80/81, sheet-z +1/+2 | 기존 root portal·중첩 sheet | U02/U07, 실제 스택 이식과 회귀 검사 후 |
| desktop --z-scrim·--z-pop 등 | 캔버스 안의 desktop overlay 순서 | U03/U07, 창 크기 독립 전환 후 |
| app-host unlayered·부팅 z=100/desktop splash z=60 | 호스트 밖 부팅 지면·기존 우선순위 | U03/N01, startup/viewport 검증 후 |
| mobile .sheet-scrim .over 및 상태별 :has | 기존 스택의 표현; 신규 업무 판단은 CSS에 추가하지 않음 | U02/U07, 입력/overlay 상태 props로 이식 시 |
| 기존 !important(플랫폼 guard·특정 상태 override 등) | 존재만으로 삭제 근거가 아님 | U07이 selector·경쟁 규칙·실측 이유·소유자별 검토 |
| 하단 chrome 값·safe-area·--bottom-clearance | #1250의 한 채널·한 유도식 | 기존 소유자; 임의 바닥 padding/z-index로 우회 금지 |
새 modal과 기존 document-level Escape 리스너를 동시에 열지 않는다. U02/U03가 overlay coordinator에 topmost dismiss를 연결한 뒤 해당 화면을 옮긴다. 신규 modal의 열린 상태에서는 background interaction을 막고, 긴 내용은 dialog 자체에서 스크롤한다. 하단 탭바를 숨기는 모바일 표현은 bottomChrome claim을 이용한다. 다이얼로그 본체에 기존 dock 높이를 중복 padding으로 더하지 않는다.
동적 selector 인계
| selector | 표현 의미/유효 값 | 소유 |
|---|---|---|
| .lg-ui-button / .lg-ui-input / .lg-ui-field / .lg-ui-dialog / .lg-ui-state / .lg-ui-hint / .lg-ui-error | 정적 클래스 전체 목록 | U01 |
| [aria-disabled=true], [aria-busy=true], :disabled | 행위 pending/native 불가 | U01 primitive, 판정 feature |
| [aria-invalid=true], data-ui-input-state | empty/typing/valid/invalid | U01, 판정 A12/A13 |
| data-ui-state | loading/empty/error | U01, 판정 feature |
| data-ui-persistence | device_persisted/server_committed/stats_published/unknown | U01, 판정 A08/A10·S |
| :focus-visible, dialog:modal, ::backdrop, forced-colors | native 상호작용·접근성 | U01 |
data-ui-*는 primitive 표현 상태이며 신규 data-lg-* semantic marker가 아니다. 기존 designContract.ts의 version·marker를 바꾸지 않는다. 기능 자동화는 accessible role/name과 기존 등록 marker를 사용한다. 신규 클래스는 문자열 조합 prefix가 없어 dead-CSS 예외를 추가하지 않는다.
U02/U03의 각 화면 PR은 사용 클래스·동적 suffix의 닫힌 값 목록·상태 속성의 소유자·CSS import 위치·scope·남긴 override 이유를 제공한다. U07은 이 목록과 실제 사용 그래프로 정리하며 check:dead-css allowlist를 폭넓게 늘리지 않는다. U06은 최종 화면/키보드/실기기/접근성 결과로 닫는다. 코드 검색의 0건만으로 런타임 CSS가 안전하다고 판정하지 않는다.
U03 selector·scroll 인계 (2026-09-10)
U07은 #1421의 release 통합 뒤 아래 파일을 기준으로 후속 정리한다. mobile/native selector 또는 토큰 원값을 바꾸지 않았다. 관리자는 독립 문서 저장소 범위로 제외했다.
| 파일/범위 | 소유와 허용 예외 | 검증 |
|---|---|---|
| app-host.css | U03 Desktop document 스크롤, 모바일·부팅 기존 규칙 유지. host는 의도적으로 unlayered | backgroundTokens/viewportTiers 및 실제 browser |
| desktop.css | base/token 범위 .dk-app, sidebar/본문 Grid, dk-main container | 5 viewport; 홈/일지/PR/리포트 |
| screens/{plan-editor,session,pr,volume}.css | 기존 화면 클래스·screen layer 유지, desktop 진입점에서만 import하므로 900px 전체 wrapper 제거 | viewport와 실제 200% zoom |
| U03의 intrinsic.css → U07에서 소유 파일로 통합 | base sidebar는 desktop.css, editor는 plan-editor.css, compose/calendar는 session.css, PR/리포트는 각 screen CSS. 별도 intrinsic import는 제거 | 마지막 30번째 세트, focus/Tab/Escape, 복구 및 U07 순서 교란 |
| .dk-prb-table-scroll | 열의 의미 보존을 위해 헤더·행만 최소 76rem, region 내부 가로 스크롤. 문서 전체 숨김 없음 | 200% zoom에서 ArrowRight 실제 scrollLeft 증가 |
| .dk-scrim/.dk-cbuild-scrim, picker | viewport-32px 범위의 선택 목록/overlay 내부 스크롤 | compose 접근·저장·취소. 모든 picker 조합을 완전 검증한 것은 아님 |
동적 suffix는 기존 카드의 err/done/active/dragging, compound comp/bf/bwro 및 data-pos 0/1/2·data-on 1을 유지한다. 신규 JS 동적 클래스 생성 규칙은 없다. compose 내부 .dk-setrow-edit/.bf/.comp와 duration/rest 입력이 공통 원문 상태를 표현한다. 홈·일지·리포트의 내부 overflow와 숨겨진 전역 scrollbar 규칙을 걷어냈고 bounded chart/표/선택 목록은 컴포넌트 책임으로 남겼다. U07은 아래 CSS 잔여 정리를 완료했다. 남은 DkOverlay 소비자 전체 이식은 수행하지 않았으며 CSS 정리 완료로 집계하지 않는다.
U06/N01/R04 인계: Chromium desktop 27+추가 브라우저 검사와 실제 setZoom(2), resize 계측을 남겼다. WebKit/Firefox·실제 iOS/Android WebView·네이티브 키보드·스크린리더 음성·Production 인증 여정은 이번 작업에서 실행하지 않았다. 증거/한계.
U07 CSS 소유·예외·후속 인계 (2026-09-10)
U07 #1532 작업 기록. 기존 tokens < base < screen < shell < overrides와 플랫폼별 token 값, 소비 지점의 semantic alias를 유지한다. U03의 intrinsic.css 46개 선택자는 같은 layer의 원 소유 규칙에 통합했다. 분리 파일을 마지막에 붙이는 우선순위 대신 같은 파일의 base → media/container 순서로 표현한다. PR board의 1180px 분기는 기본 rule 뒤에 두어 border/padding의 important 3개를 제거했다.
| 소유 파일 | 책임과 유지 조건 |
|---|---|
| desktop.css | base의 sidebar·Grid·dk-main, screen의 공통 dialog 크기·focus·복구; 1100px sidebar와 공통 상태 범위 유지 |
| screens/plan-editor.css | dk-editor container·편집 grid/행·분할 입력; 좁은 container에서 감싸기와 긴 이름 줄바꿈 |
| screens/session.css | calendar/document 스크롤·compose dialog 본문·leave dialog·세트 입력. .dk-ms-syncblocked는 고유 접두의 기존 unlayered 예외 |
| screens/pr.css, volume.css | PR의 표 내부 가로 스크롤·반응형 board, 리포트 auto-fit·본문 document 스크롤 |
| mobile base/core·overlays, screens/profile·workout | .app 범위 안의 같은 의미 중복만 병합. tokens·safe-area·bottomChrome 유도식 유지 |
| shared shell.css, app-host.css | shell 중복 선언만 정리. app-host의 unlayered reset/부팅/viewport 책임은 유지 |
| mobile screens/group.css | .app .gp* 기능 범위의 기존 unlayered CSS와 로컬 @font-face; 다른 플랫폼 클래스 선택 없음. NanumSquareRoundB loaded/swap 검증 |
남긴 important와 동적/fallback 예외
| 선택자·소비 화면 | 유지 이유·제거 조건 |
|---|---|
.dk-prb-me .ladder.incomplete .trk .fill | PR 미완성 상태의 inline 진행 너비를 0으로 표시. 상태 너비의 표현 책임을 이식하고 같은 상태를 확인할 때만 제거 |
.dk-prb-dd .row .st:hover, .dk-prd2-hero .hl .fav2:hover | PR 검색·상세의 inline 즐겨찾기 색보다 hover 색을 우선. inline 상태 색을 소유 rule로 옮기는 작업과 함께 검증할 때 제거 |
mobilePrFavList, mobilePrFavReorder 훅 아래 .row + .row | 실제 선택자는 mobilePrFavList, mobilePrFavReorder 두 갈래다. 모바일 즐겨찾기 목록의 screen 행 경계보다 우선하는 base 예외. 해당 목록 표현을 소유 layer로 이식하고 모든 목록 상태를 확인할 때 제거 |
.app .sj-mo-sb .sess-flat | 세션 상세 overlay에서 screen의 본문 padding을 덮는 base 예외. overlay와 일반 세션의 간격을 함께 확인한 뒤 소유 rule로 이식 |
prc-tier-, hm-t-, dk-tier-, dk-ct-, wk- | 기존 닫힌 동적 suffix 생성자와 연결. dead-css-baseline.json의 5개 prefix·32개 명시적 동적 정의(현재 CSS 매칭은 prefix 39개+exact 24개=63개 클래스) 유지. 생성자와 소비 DOM을 함께 제거할 때만 축소 |
| hex/oklch·gradient·display 및 native 조건 | 구 브라우저 fallback과 플랫폼 조건은 단순 같은 property라는 이유로 제거하지 않음. 지원 환경에서 동치가 확인되어야 축소 |
반복 selector 그룹 28개는 경쟁 규칙을 가로질러 옮겼을 때 동일 specificity·shorthand·상태 순서의 동치가 확인되지 않아 보존했다. mobile tokens의 두 :root는 네 방향 safe-area 계약을 유지한다. 파일·선택자별 목록은 잔여 목록에 있다. 별도 CSS 도구·허용 목록을 추가하지 않았다.
U04/U06/R01이 소비할 경계
- U04:
.dk-prb-table-scroll이 가로 스크롤 소유자이고 내부 헤더·목록의 최소 폭은 76rem이다. 홈/일지/리포트의 세로 스크롤은 document가 소유한다..dk-setrow-edit, 긴 제목/이름·복합 입력은 container와 텍스트에 따라 높이가 바뀌므로 가상 목록에서 고정 높이를 가정하지 말고 실제 행을 측정한다. U07은 useListWindow나 가상화 API를 바꾸지 않았다. - U06: U04 변경 이후 실제 200% 확대·긴 한글/영문·30번째 입력 접근·dialog 내부 스크롤·Tab/Escape/포커스 복귀·중첩 leave dialog를 다시 대조한다. Chromium fixture 검사는 아래 기록에 있고 실제 iOS/Android WebView·native keyboard/safe-area·스크린리더 음성·Production 인증 여정은 미실행이다. U06 완료로 올리지 않는다.
- R01: import/빌드에 없는
vite-scaffold.css만 삭제했다. 미연결vite/AppRoot.tsx와 타입은 R01에 남긴다. 문자열 기반 dead CSS gate의 0건은 entry 도달성의 증명이 아니다. baseline 0→0이며 #1478 와드업/관리자 검사 제외도 그대로다.
검증은 U07 10건(실제 홈/PR/리포트 이동 960·1440px, 320px 편집/overlay, style reverse/rotate와 실제 잘못된 padding 음성 대조, lazy/opposite-platform scope 및 로컬 font), U03 29건(실제 Chrome 200% zoom 포함), U02 22건, U01 16건이다. controller/data는 기존 fixture, 제품 component·desktop root·styles는 실제 코드다. 화면 전체 인증 E2E 또는 모든 native/font 환경을 검증한 결과가 아니다.
U04 긴 목록 인계 (2026-09-10)
viewport·identity 계약을 따른다. 기존 scroll root와 가변 높이 CSS를 유지하며, rc-fs 첫 행 경계는 가상 행 wrapper 아래에서도 첫 canonical 위치에만 적용한다. U05/U06/R04/R01의 입력과 실행·미실행 구분은 작업 기록에 남긴다.