Skip to content

공통 시각 기반 — token·layer·scope·overlay 계약 U01 v1

  • 계획: U01 / #1289. props·접근성 정본, 총괄.
  • 기준선: main 16b8e99684a4cb152b2ff5d89b5aea10a7f055e0, 2026-09-07. 새 코드 ui/shared/styles/ui-primitives.cssui/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.csshtml/body/#vite-root·viewport·부팅 지면, 의도적인 unlayeredU03/B02/N01 호스트 책임, 이동하지 않음
shared styles/shell.csslogin/splash/data gate/toastbase primitive import 한 줄, 기존 shell 규칙 보존
mobile lib/Modal.tsx.app root portal; 스크롤/transform에 잘리는 scrim 방지U02가 이식, 기존 포털 계약 유지
desktop lib/DkOverlay.tsxscrim/바깥 클릭/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/resetapp-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-scopeshell 담당, 최종 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, 플랫폼 밖 #5268abfocus 표시 U01; 해당 지면에서 대비 검증
--ui-error-text#a32927, 밝은 기본 지면의 오류 문구U01; 상태 아이콘용 --miss를 본문에 그대로 쓰지 않음
--ui-gap / --ui-radius--gap / --r-sm플랫폼 간격·형태 독립
--ui-target-min44px, 신규 input/dialog action의 최소 높이U01; 기존 버튼 전체 크기는 이 PR에서 바꾸지 않음
--ui-focus-width / --ui-focus-offset2px / 3pxU01, 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·중첩 sheetU02/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-stateempty/typing/valid/invalidU01, 판정 A12/A13
data-ui-stateloading/empty/errorU01, 판정 feature
data-ui-persistencedevice_persisted/server_committed/stats_published/unknownU01, 판정 A08/A10·S
:focus-visible, dialog:modal, ::backdrop, forced-colorsnative 상호작용·접근성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.cssU03 Desktop document 스크롤, 모바일·부팅 기존 규칙 유지. host는 의도적으로 unlayeredbackgroundTokens/viewportTiers 및 실제 browser
desktop.cssbase/token 범위 .dk-app, sidebar/본문 Grid, dk-main container5 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, pickerviewport-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.cssbase의 sidebar·Grid·dk-main, screen의 공통 dialog 크기·focus·복구; 1100px sidebar와 공통 상태 범위 유지
screens/plan-editor.cssdk-editor container·편집 grid/행·분할 입력; 좁은 container에서 감싸기와 긴 이름 줄바꿈
screens/session.csscalendar/document 스크롤·compose dialog 본문·leave dialog·세트 입력. .dk-ms-syncblocked는 고유 접두의 기존 unlayered 예외
screens/pr.css, volume.cssPR의 표 내부 가로 스크롤·반응형 board, 리포트 auto-fit·본문 document 스크롤
mobile base/core·overlays, screens/profile·workout.app 범위 안의 같은 의미 중복만 병합. tokens·safe-area·bottomChrome 유도식 유지
shared shell.css, app-host.cssshell 중복 선언만 정리. 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 .fillPR 미완성 상태의 inline 진행 너비를 0으로 표시. 상태 너비의 표현 책임을 이식하고 같은 상태를 확인할 때만 제거
.dk-prb-dd .row .st:hover, .dk-prd2-hero .hl .fav2:hoverPR 검색·상세의 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의 입력과 실행·미실행 구분은 작업 기록에 남긴다.