CSS 거버넌스
Tabula의 지역성 주장 — 엘리먼트의 외관은 그 자신의 마크업으로부터 결정 가능하다 — 은 모두 class 속성에 관한 것이다. 스타일시트는 다른 방식으로, 즉 셀렉터로 엘리먼트에 닿는다. 그래서 프로젝트 자신의 .css 파일은 전체 모델이 한 줄로 무너질 수 있는 유일한 표면이며, 이 레이어가 존재하기 전까지는 이 시스템의 어떤 명령도 그것을 열어보지 않았다.
이 페이지는 이제 그것들을 무엇이 통제하는지, 무엇을 커버하지 않는지, 그리고 두 게이트가 어디서 어긋날 수 있는지를 다룬다.
다섯 가지 규칙
두 게이트 모두 같은 코드를 실행한다 — 규칙 커널은 packages/stylelint-plugin/src/kernel.ts에 있다. stylelint 플러그인은 에디터를 위해 이를 감싸고, tabula check:css는 CI를 위해 파일들을 그 위로 순회한다. 두 번 작성했다면 어긋났을 것이고, 그 어긋남은 가장 중요한 방향으로 조용히 일어났을 것이다: 에디터가 표시하는 것을 고치고, CI는 다른 무언가를 표시하며, 어떤 에디터도 언급하지 않는 규칙은 규칙이기를 멈춘다.
| 코드 | stylelint 규칙 | 금지 대상 | 이유 |
|---|---|---|---|
TAB-E221 | tabula/no-apply | @apply | 클래스 목록을 손으로 작성한 셀렉터로 조합한다 — 프로파일이 제거하는 바로 그 간접성이다. Tailwind 창시자 스스로도 부인한 방식이다(draft-c §3.2 ✚16). |
TAB-E222 | tabula/no-raw-rules | 승인된 엔트리 밖, 프로젝트 CSS 안의 손으로 작성된 규칙 | .card .title { color: red }는 셀렉터로 스타일링하므로, 엘리먼트가 자신의 마크업만으로 읽히지 않게 된다(draft-c §3.2 ✚18). |
TAB-E223 | tabula/no-scoped-custom-property | :root/html 이외에 정의된 --tb-* / --d-* | T17 — 시스템 전체에서 가장 중요한 검사다(SPEC J6). 토큰이 <div> 위에서 재정의될 수 있는 순간, 그것을 쓰는 모든 자손은 자신의 마크업만으로는 읽을 수 없게 된다. |
TAB-E224 | tabula/no-theme-inline | @theme inline | T20 — inline은 빌드 시점에 토큰 값을 유틸리티에 대입하여, 모든 조건부 토큰을 컴파일해서 없애 버린다. 다크 모드는 아무 진단도 없이 동작을 멈춘다. shadcn 생태계의 기본값이므로, 그 생태계로 훈련된 에이전트는 이를 향해 손을 뻗는다. |
TAB-E225 | tabula/no-important-css | 프로젝트 CSS 안의 !important | 모든 유틸리티는 특정성 (0,1,0)으로 정규화되어 있어 오직 rank만이 충돌을 결정한다. !important 하나가 declaration을 그 증명 밖으로 내보낸다. |
따라서 프로젝트 .css 파일은 다음만을 담을 수 있다: @import, @source, @utility, @custom-variant, @charset, 본문 없는 @layer a, b; 순서 선언, 그리고 주석. 그 외에는 아무것도 없다.
두 개의 게이트
CI / 빌드 시점에서
tabula check:css # every project .css file + the emitted stylesheets
tabula build --check # drift + scan + check:css, the one command CI runs
tabula build --check --no-css # drift + scan only; the CSS gate must be opted OUT of
tabula check:css --css-entry src/theme/entry.css # designate a non-conventional entry에디터에서 — .stylelintrc.json, 이 저장소 자신의 것을 그대로 미러링:
{
"plugins": ["@tabula-css/stylelint-plugin"],
"rules": {
"tabula/no-apply": true,
"tabula/no-raw-rules": true,
"tabula/no-scoped-custom-property": true,
"tabula/no-theme-inline": true,
"tabula/no-important-css": true
},
"overrides": [
{ "files": ["src/app.css"], "rules": { "tabula/no-raw-rules": [true, { "sanctionedEntry": true }] } }
]
}@tabula-css/stylelint-plugin/config는 이 규칙 블록을 내보내므로, 복사하는 대신 스프레드할 수 있다. stylelint는 선택적 peer dependency다: 플러그인의 커널은 import 그래프에 stylelint를 전혀 갖고 있지 않으며, 이것이 CLI가 런타임 의존성으로 저작 도구(authoring tool)를 끌어들이지 않고도 이를 사용할 수 있게 해준다.
승인된 엔트리
모든 앱에는 @import "../.tabula/source.css";와 앱 자신의 루트 레벨 커스텀 프로퍼티를 담는 스타일시트가 하나 필요하다. 그 파일 — 오직 그 파일만 — 이 TAB-E222에서 면제된다.
나머지 네 가지에서는 면제되지 않는다. 엔트리 안의 :root 블록은 괜찮지만, 엔트리 안의 --tb-color-accent: red는 어디에 나타나든 TAB-E223이다. examples/reference-ui/src/app.css를 참고하라, 이것이 풀어본(worked) 예제다.
파일이 엔트리가 되는 방법:
- 관례에 의해 —
src/app.css,src/index.css,src/styles.css,app/globals.css와 몇 개 더(packages/cli/src/commands/check-css.ts의DEFAULT_CSS_ENTRIES); - 또는 명시적으로 —
tabula check:css --css-entry <path>, 반복 가능.
두 게이트가 어긋날 수 있는 유일한 지점은 바로 이 목록이다: check:css는 플래그와 관례에서 이를 읽고, stylelint는 .stylelintrc의 overrides 블록에서 이를 읽는다. 둘을 동일하게 유지하라. 만약 서로 어긋난다면, 배포 여부를 결정하는 것은 CI 게이트다.
결정 사항과 불일치, 기록됨
1. Draft C의 코드는 존중되지 않는다. Draft C는 이 불변식들에 PLANAR-E210(T17), PLANAR-E211(T20), PLANAR-E212(✚18)이라는 번호를 부여한다. 이 세 번호는 CSS 레이어가 작성되기 전에 이미 이 카탈로그의 빌드 불변식 I1/I2/I3에 사용되고 있었다. 두 가지를 뜻하는 코드는 아무도 알아보지 못하는 코드보다 더 나쁘다 — tabula explain TAB-E212는 하나의 설명 텍스트만 출력할 수 있고, 그것은 독자를 손으로 쓴 .card p가 아니라 rank 충돌 쪽으로 잘못 이끌 것이다. 그래서 이 계열은 대신 E221–E225에서 새로 만들어졌으며, 각 카탈로그 항목은 draft와의 대응 관계를 기록한다.
2. T17은 생성된 스타일시트에 대해서는 다르게 읽혀야 하고, 실제로 그래야 한다. 문서에 쓰인 대로의 T17은 모든 프로파일 커스텀 프로퍼티가 오직 :root에서만 정의될 수 있다고 말한다. 프로파일 자신의 출력은 의도적으로 그것을 위반한다: .ring-accent { --tb-ring-color: … }와 .shadow-sm { --tb-shadow: … }는 엘리먼트별 합성(composition) 프로퍼티로, inherits: false와 초기값을 갖고 선언된다. 그래서:
- 프로젝트 CSS — 루트 대상 밖에서의 어떤
--tb-*/--d-*정의든 예외 없이 위반이다. 프로젝트 CSS가 프로파일 프로퍼티를 정의해야 할 정당한 이유는 전혀 없다; 엘리먼트별 값을 위한 승인된 채널은dyn()이며, 이는 스타일시트가 아니라 인라인 스타일을 쓴다. - 생성된 CSS — 루트 대상 밖에서의 정의는, 그 프로퍼티가 생성된 세트 어딘가의 루트 대상에서 정의되어 있는 경우에만 위반이다, 즉 테마 토큰인 경우다. 이것이 T17이 잡으려는 것(루트 아래에서 재정의된 토큰)을 정확히 잡아내면서도 프로파일 자신의 올바른 출력을 실패시키지 않는다.
SPEC은 두 번째 것을 말해야 한다. 그런데 첫 번째 것을 말하고 있다. 검사를 약화시켜 해결하는 대신 여기 기록해 둔다.
3. @utility는 엘리먼트 바인딩이다. @utility card-shell { --tb-color-accent: red }는 그것을 가진 모든 엘리먼트에서 테마 토큰을 재정의하는 클래스로 컴파일된다 — 프로젝트 CSS가 담을 수 있는 그 유일한 at-rule을 입은, 같은 T17 위반이다. 커널은 이런 이유로 @utility 안의 declaration을 엘리먼트 스코프로 취급한다.
4. 엔트리 목록은 tabula.config.json 안에 있어야 한다. 그것의 영구적인 자리는 설정 스키마 (packages/core/src/schemas/config.ts) 안의 scan.sources 옆에 있는 css.entries 키이며, 그 스키마의 additionalProperties: false는 인식되지 않는 css 키가 무시되는 대신 검증에서 실패하게 만든다. 이 레이어를 만든 태스크는 그 파일을 소유하지 않았으므로, 목록은 오늘은 관례와 --css-entry다. 이를 옮기는 것은 한 개 키를 바꾸는 일이며, 위에서 언급한 어긋남 지점을 제거할 것이다.
5. 파싱 불가능한 스타일시트는 건너뛸 대상이 아니라 발견 사항이다. PostCSS가 파싱할 수 없는 .css 파일은 TAB-E222로 보고된다. 건너뛴다면 "문법적으로 깨져 있음"이 이 페이지의 모든 규칙을 피해가는 가장 저렴한 방법이 될 것이다.
이 레이어가 여전히 커버하지 않는 것
- T18 — 루트 아래의 축 속성.
<div data-theme="dark">는 비활성이며(생성된 셀렉터는:root[data-theme=…]이므로 그 속성은 아예 효과가 없다), ESLint 규칙tabula/no-axis-attribute-below-root가 저자에게 이를 설명해줄 것이다. CSS에서는.panel[data-theme="dark"]셀렉터가 축 위반이 아니라 손으로 작성된 규칙(TAB-E222)으로 걸린다 — 잘못된 이름이지만 올바른 결과다. - 프로젝트가 작성한
@source줄 — 폐쇄성의 구멍, 여전히 열려 있음. 스타일시트 중 하나에 있는@source "./src"는 Tailwind의 소스 스캐닝을 다시 켜며, 테마 네임스페이스는 여전히 정의되어 있으므로bg-red-500과p-4가 다시 방출되기 시작한다.check:css는 이를 표시하지 않는다, draft-c §3.2 ✚18이@source를 프로젝트 CSS가 담을 수 있는 at-rule 목록에 포함시키기 때문이다. 그 목록과 review A7의 폐쇄성 논증은 서로 모순되며, 이 레이어는 그 목록을 구현한다. SPEC에서 이것이 해결되기 전까지는:.tabula/source.css의@import를 프로젝트 안의 유일한 Tailwind 엔트리로 유지하고,@source줄을 추가하지 말라.grep -rn '@source' src가 그 검사다. - 임포트된 서드파티 CSS. 게이트는 프로젝트 트리를 순회할 뿐
node_modules는 순회하지 않는다. 임포트한 벤더 스타일시트는 이전과 마찬가지로 전혀 검토되지 않는다. - 런타임
<style>주입. 컴포넌트가 런타임에 문서에 써넣는 무엇이든 여기의 모든 게이트 밖에 있다.