@kaydash9999/banker-plugins 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ ---
2
+ name: theme-selection
3
+ description: "프로젝트에 테마 74종을 라이트와 다크 한 쌍씩(148 조합) CSS 사용자 지정 속성으로 입힘. 대비 보정을 거친 값을 내고, 테마 고르개(HTML과 스크립트)와 대비 감사(check)도 냄. 프로젝트의 DESIGN.md 도 읽어 테마로 더함. Node 만 쓰고 인터넷을 쓰지 않음. 'theme-selection'/'테마 선택'/'테마 추가'/'테마 입히기'/'다크모드 추가' 시 사용. 일반 UI 작업에서는 자동 실행 금지."
4
+ argument-hint: "list | css | json | widget | check [--design-md PATH] [--only SLUGS] [--default SLUG] [--prefix STR] [--dark-via attr|media|class] [--out FILE] [--json]"
5
+ ---
6
+
7
+ # theme-selection - 테마 74종, 라이트와 다크, 대비 보정
8
+
9
+ 테마 74종, 라이트와 다크 148 조합, 대비 보정 포함, 인터넷 없음. 엔진은 Node 하나(`scripts/themes.mjs`)이고 의존성이 없다.
10
+ 붙이는 방법은 `references/integrate.md`, 보정이 이 모양인 이유는 `references/contrast.md` 에 있다.
11
+ 번들 팔레트의 출처와 라이선스는 `assets/NOTICE.md` 에 있다.
12
+
13
+ 인자: `$ARGUMENTS`
14
+ - 명령과 옵션이 있으면 그것으로 아래 절차를 따른다. 비어 있으면 절차대로 묻는다.
15
+ - Codex: 사용자 메시지에서 읽는다.
16
+ - 사용자 입력을 셸 명령에 그대로 붙이지 않는다. 명령과 옵션은 아래 표에 있는 것만 쓴다.
17
+ - 🔴 경로는 작은따옴표로 감싼다(bash, zsh, PowerShell 모두). 큰따옴표 안의 `$(...)` 와 백틱은 셸이 엔진보다 먼저 실행한다.
18
+ - 경로에 작은따옴표(`'`, 그리고 PowerShell 이 같게 읽는 `‘` `’` `‚` `‛`), `$`, 백틱, 줄바꿈이 있으면 실행하지 않고 사용자에게 알린다.
19
+
20
+ ## 절차
21
+
22
+ 1. 스킬 디렉터리의 절대경로를 정한다. 경로를 추측하지 않는다. 아래 `<스킬>` 은 그 경로다.
23
+
24
+ | 런타임 | 스킬 디렉터리 |
25
+ |---|---|
26
+ | Claude Code | 이 파일 맨 위에 넣어 준 "Base directory for this skill:" 줄의 경로 |
27
+ | Codex | `~/.codex/skills/banker-theme-selection/` 의 절대경로. 프로젝트 범위 설치면 `./.codex/skills/banker-theme-selection/` |
28
+
29
+ - `~` 는 따옴표 안에서 펼쳐지지 않으니 절대경로를 쓴다.
30
+ 2. 프로젝트에 `DESIGN.md` 가 있으면 `--design-md` 로 함께 먹인다. 없으면 번들 74종만 쓴다.
31
+ - 파일, `DESIGN.md` 를 담은 폴더, `<이름>/DESIGN.md` 를 담은 폴더를 모두 받는다.
32
+ - `<이름>/DESIGN.md` 가 여럿이면 그 부모 폴더 하나를 넘긴다. 하위 경로를 하나씩 셸에 적지 않는다. 폴더 이름은 엔진이 검사한다.
33
+ - 여러 번 줄 수 있고 뒤가 이긴다. 슬러그가 같으면 번들을 덮는다.
34
+ - 슬러그(폴더 이름이나 확장자를 뗀 파일 이름)는 글자(한글 포함), 숫자, `.`, `-`, `_` 만 받는다. 빈칸, 다른 기호, 보이지 않는 글자가 있으면 종료 코드 2 로 멈춘다. 이름을 바꾸자고 사용자에게 알린다.
35
+ - 한글 이름은 NFC 로 맞춰 읽는다. macOS 의 분해형 폴더 이름도 `--only 쇼핑몰` 과 맞는다.
36
+ 3. 쓸 테마를 정한다. 사용자가 고르면 `--only` 로 줄인다.
37
+ - 74종 전부를 내면 테마 규칙만 148 개다. 쓸 것만 `--only` 로 고르는 쪽을 먼저 권한다.
38
+ - 모르면 `node <스킬>/scripts/themes.mjs list` 로 목록을 보고 사용자에게 묻는다. 2단계에서 `--design-md` 를 정했으면 `list` 에도 준다.
39
+ 4. CSS 를 낸다.
40
+ - `node <스킬>/scripts/themes.mjs css --only a,b,c --out <프로젝트 CSS 경로>`
41
+ - `--out` 은 이미 있는 파일을 묻지 않고 덮어쓴다. 그 경로에 파일이 있으면 먼저 사용자에게 묻는다.
42
+ - 프로젝트가 다크를 `class` 나 `@media` 로 다루면 `--dark-via class` 나 `--dark-via media` 를 준다.
43
+ 5. 테마를 고르는 사람이 있는 화면이면 고르개를 붙인다.
44
+ - `node <스킬>/scripts/themes.mjs widget --only a,b,c` 가 세 덩이를 낸다. CSS 를 낼 때와 같은 `--only`, `--default`, `--dark-via` 를 준다. `--default` 가 다르면 첫 방문의 고르개 테마와 `:root` 테마가 갈린다.
45
+ - 모드 버튼은 `data-mode` 속성과 `.dark` 클래스를 함께 바꾼다. 그래서 `attr`, `class` 둘 다 맞는다. `media` 면 모드 버튼 없이 낸다(다크는 OS 설정을 따름).
46
+ - 어디에 두는지는 `references/integrate.md` 의 고르개 절을 따른다.
47
+ - 모드만 쓰는 화면은 고르개 없이 `--dark-via media` 로 끝낸다. `:root` 가 기본 테마의 다크도 들고 있어 `<html>` 에 속성이 없어도 OS 가 다크면 다크가 된다.
48
+ 6. 프로젝트 CSS 가 변수를 읽게 한다. 순서는 `references/integrate.md` 에 있다.
49
+ 7. `check` 로 확인한다. CSS 를 낼 때 준 `--only`, `--design-md`, `--target`, `--label-target` 을 같게 준다.
50
+ - 🔴 `--out` 은 주지 않는다. 주면 그 파일이 감사 결과로 덮인다.
51
+ - 종료 코드 0 이 되고, 실제 화면까지 본 뒤에 끝났다고 말한다. 확인 절을 따른다.
52
+
53
+ ## 경고
54
+
55
+ - 🔴 배선 스크립트를 `ready` 래퍼 없이 넣으면 고르개가 영원히 안 움직인다.
56
+ - 이 스크립트는 자기가 배선할 마크업보다 먼저 돌 수 있다. `getElementById` 가 빈손이 된다.
57
+ - `widget` 이 낸 배선 스크립트는 `readyState` 를 보고 즉시 또는 `DOMContentLoaded` 에서 부르는 래퍼로 감싸져 있다. 래퍼를 떼지 않는다.
58
+ - head 스크립트는 DOM 을 안 보므로 `<head>` 에서 즉시 돈다. 배선 스크립트와 합치지 않는다.
59
+ - 🔴 `-on-surface` 가 붙은 변수는 `--bg` 위 규칙에 쓰지 않는다. 본문이 흐려진다.
60
+ - 🔴 `--target` 을 4.5 로 올리지 않는다. 74 테마의 색이 전부 평평해져 테마가 테마가 아니게 된다.
61
+ - 🔴 `--rule` 은 대비가 보장되지 않는다. 경계선만으로 정보를 전달하지 않는다.
62
+ - 148 조합 중 122 조합이 `--bg` 대비 3.0 아래이고, 35 조합은 1.5 아래다.
63
+ - `lamborghini/light` 은 `--rule` 과 `--bg` 가 같은 색이라 대비가 1.00 이다. 경계선이 안 보인다.
64
+ - 누를 수 있는 것, 고른 것, 틀린 칸은 경계선 말고 글자색이나 배경색으로도 표시한다.
65
+ - `--no-correct` 는 대조군 전용이다. 프로젝트에 나가는 CSS 에 쓰지 않는다.
66
+
67
+ ## 명령
68
+
69
+ 실행 형식: `node <스킬>/scripts/themes.mjs <명령> [옵션]`
70
+
71
+ | 명령 | 내는 것 |
72
+ |---|---|
73
+ | `list` | `슬러그<탭>표시명` 한 줄에 하나 |
74
+ | `css` | CSS 사용자 지정 속성. 라이트와 다크 |
75
+ | `json` | 보정된 팔레트를 JSON 으로 |
76
+ | `widget` | 테마 고르개 HTML 과 스크립트 둘 |
77
+ | `check` | 대비 감사. 하나라도 어긋나면 종료 코드 1 |
78
+
79
+ | 옵션 | 뜻 |
80
+ |---|---|
81
+ | `--design-md PATH` | DESIGN.md 를 읽어 테마를 더하거나 덮는다. 여러 번 줄 수 있고 뒤가 이김 |
82
+ | `--only SLUGS` | 쉼표로 가른 허용 목록. 기본은 전부 |
83
+ | `--default SLUG` | `:root` 가 들 테마. 기본 `superhuman`. 걸러져 없으면 첫 슬러그 |
84
+ | `--prefix STR` | 변수 13개와 `--shadow` 앞에 붙는 문자열. 영문, 숫자, `-`, `_` 만. 기본은 빈 문자열 |
85
+ | `--dark-via MODE` | `attr`(기본), `media`, `class`. `css` 와 `widget` 에 같은 값을 준다 |
86
+ | `--target N` | `-on-surface` 와 `accent` 의 목표 대비. 기본 3.0 |
87
+ | `--label-target N` | 버튼 글자 목표 대비. 기본 4.5. 4.58 을 넘기면 답 없는 조합이 생기고 `check` 가 그 자리를 보고함 |
88
+ | `--no-correct` | 보정 없이 원본 값을 냄. 대조군 전용 |
89
+ | `--out FILE` | 표준출력 대신 파일로. 있는 파일은 덮어씀. 그 파일 자체가 심볼릭 링크면 쓰지 않음(종료 2). 상위 폴더의 링크는 따라감 |
90
+ | `--json` | `check` 에서만. 기계가 읽는 감사 결과 |
91
+
92
+ | 종료 코드 | 뜻 |
93
+ |---|---|
94
+ | 0 | 정상 |
95
+ | 1 | `check` 가 어긋난 자리를 찾음 |
96
+ | 2 | 인자가 틀림(허용 밖 슬러그와 접두사 포함). 파일을 읽거나 쓰지 못함(`--out` 이 심볼릭 링크 포함) |
97
+
98
+ `check --json` 은 아래 필드를 낸다. 사람이 읽는 출력 대신 쓸 수 있다.
99
+
100
+ | 필드 | 뜻 |
101
+ |---|---|
102
+ | `themes` | 잰 테마 수 |
103
+ | `combos` | 잰 조합 수. 테마당 둘 |
104
+ | `checks` | 단언 수 |
105
+ | `corrected` | 보정을 켰는지 |
106
+ | `violations` | 어긋난 자리 수 |
107
+ | `bad` | 어긋난 자리 목록 |
108
+ | `detail` | 조합마다 `slug`, `mode`, `body`(본문 대비), `violations` |
109
+
110
+ 선택자는 `--dark-via` 에 따라 갈린다. `:root` 는 언제나 먼저 나오고 기본 테마의 라이트와 다크를 든다.
111
+ JS 가 꺼졌거나, 저장된 이름이 목록 밖이거나, `<html>` 에 `data-theme` 이 없어도 화면이 읽히는 이유다.
112
+ 테마 규칙은 같은 모드의 `:root` 규칙보다 특정성이 높아 이긴다.
113
+
114
+ | `--dark-via` | 라이트 | 다크 | `:root` 의 다크 |
115
+ |---|---|---|---|
116
+ | `attr` | `html[data-theme="<슬러그>"]` | `html[data-theme="<슬러그>"][data-mode="dark"]` | `:root[data-mode="dark"]` |
117
+ | `class` | `html[data-theme="<슬러그>"]` | `html[data-theme="<슬러그>"].dark` | `:root.dark` |
118
+ | `media` | `html[data-theme="<슬러그>"]` | `@media (prefers-color-scheme: dark){html[data-theme="<슬러그>"]{...}}` | `@media (prefers-color-scheme: dark){:root{...}}` |
119
+
120
+ ## 변수
121
+
122
+ 조합 하나가 13개를 낸다. 순서도 이대로다.
123
+
124
+ 🔴 `-on-surface` 가 붙은 넷은 배경이 `--surface` 인 규칙에서만 쓴다. `--bg` 위 규칙이 이것을 쓰면 본문이 흐려진다.
125
+
126
+ | 변수 | 쓰는 자리 |
127
+ |---|---|
128
+ | `--bg` | 페이지 배경 |
129
+ | `--surface` | 카드, 상자, 패널 배경 |
130
+ | `--ink` | `--bg` 위 본문 글자 |
131
+ | `--dim` | `--bg` 위 보조 글자 |
132
+ | `--rule` | 구분선, 경계선 |
133
+ | `--accent` | `--bg` 위 링크, 강조, 버튼 경계 |
134
+ | `--warn` | `--bg` 위 경고, 위험 표시 |
135
+ | `--ink-on-surface` | `--surface` 위 본문 글자 |
136
+ | `--dim-on-surface` | `--surface` 위 보조 글자 |
137
+ | `--accent-on-surface` | `--surface` 위 강조, 버튼 배경 |
138
+ | `--warn-on-surface` | `--surface` 위 경고, 위험 버튼 |
139
+ | `--label-on-accent` | `--accent-on-surface` 를 배경으로 한 버튼 글자 |
140
+ | `--label-on-page-accent` | `--accent` 를 배경으로 한 버튼 글자 |
141
+
142
+ `--shadow` 는 테마가 아니라 모드의 성질이라 테마마다 넣지 않고 한 번만 낸다.
143
+
144
+ html{--shadow:rgba(0,0,0,.08)}
145
+ html[data-mode="dark"]{--shadow:rgba(0,0,0,.45)}
146
+
147
+ 148 조합 중 `ink` 와 `dim` 값이 `-on-surface` 쪽과 실제로 갈라지는 것은 3 조합(값 넷)뿐이다. 나머지 145 조합은 두 변수가 같은 값이라 화면이 안 바뀐다.
148
+
149
+ ## 확인
150
+
151
+ 끝났다고 말하기 전에 둘 다 한다.
152
+
153
+ 1. `check` 종료 코드.
154
+ - `node <스킬>/scripts/themes.mjs check` 에 CSS 를 낼 때 준 `--only`, `--design-md`, `--target`, `--label-target` 을 같게 준다. `--out` 은 주지 않는다.
155
+ - 0 이면 통과다. 1 이면 출력의 어긋난 자리를 사용자에게 보여 준다.
156
+ - 2 이면 인자를 고쳐 다시 돌린다.
157
+ 2. 실제 화면.
158
+ - 브라우저로 페이지를 열고 테마를 바꿔 본다. 라이트와 다크를 둘 다 본다.
159
+ - 🔴 마크업에 속성이 있다는 것과 그 속성을 읽는 코드가 돌았다는 것은 다른 사실이다. JS 가 하는 일은 JS 를 돌려서 잰다.
160
+ - 고르개로 테마를 고른 뒤 `document.documentElement.dataset.theme` 이 바뀌었는지 읽는다.
161
+ - 다크는 고르개의 모드 버튼으로 본다. 고르개가 없으면 OS 다크 설정이나 브라우저 개발 도구의 색 구성 흉내로 본다.
162
+ - 새로고침한 뒤 같은 테마가 첫 그림부터 나오는지 본다.
163
+ - 브라우저를 쓸 수 없으면 확인하지 못했다고 적는다. 통과로 적지 않는다.
164
+
165
+ `check` 는 본문 대비(`--ink` 대 `--bg`)를 보고만 하고 판정하지 않는다.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VoltAgent
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ # NOTICE - 서드파티 출처 (theme-selection)
2
+
3
+ 이 스킬의 번들 팔레트 `palettes.json` 은 서드파티 문서에서 색 값을 읽어 만든 것이다.
4
+ 아래는 그 출처와 라이선스를 실제로 쓴 범위에 한정해 밝힌 것이다.
5
+
6
+ ## 색 값을 읽어 만든 것
7
+
8
+ 대상: `palettes.json` 의 테마 74종
9
+
10
+ | 항목 | 내용 |
11
+ |---|---|
12
+ | 출처 | awesome-design-md (https://github.com/VoltAgent/awesome-design-md) 의 `design-md/<슬러그>/DESIGN.md` |
13
+ | 라이선스 | MIT, Copyright (c) 2026 VoltAgent. 전문은 이 폴더의 `LICENSE` |
14
+ | 쓴 범위 | DESIGN.md 의 색 값. 테마마다 라이트와 다크 7색으로 다시 계산함 |
15
+ | 슬러그 | 원본의 폴더 이름 |
16
+
17
+ - 문서 본문은 옮기지 않았다.
18
+ - MIT 조건에 따라 원본의 저작권 고지와 허가 고지를 `LICENSE` 로 함께 배포한다.
19
+ - `LICENSE` 는 2026-10-15 에 원본 저장소의 `LICENSE`(GitHub blob `5cea5d78a41df83eafe1fb9fc0710675a5b7acf6`)를 글자 그대로 옮긴 것이다.
20
+
21
+ ## 상표
22
+
23
+ - 테마 이름(예: Airbnb, Apple)은 그 디자인이 참고한 사이트를 가리킬 뿐이다.
24
+ - 각 이름과 상표는 그 소유자의 것이다.
25
+ - banker 는 그 회사들과 관계가 없고, 그 회사들의 보증을 받지 않았다.
26
+
27
+ ## 이 스킬의 자작 파일
28
+
29
+ - `SKILL.md`, `scripts/`, `references/` 는 banker 의 파일이다.
30
+ - banker 의 MIT 라이선스(저장소 루트 `LICENSE`)를 따른다.
@@ -0,0 +1,81 @@
1
+ {
2
+ "fields": ["bg", "surface", "ink", "dim", "rule", "accent", "warn"],
3
+ "default": "superhuman",
4
+ "source": "VoltAgent/awesome-design-md (MIT) design-md/<slug>/DESIGN.md, see assets/NOTICE.md",
5
+ "themes": {
6
+ "airbnb": {"label":"Airbnb","light":["#ffffff","#ffffff","#222222","#6a6a6a","#dddddd","#ff385c","#9c4a12"],"dark":["#0f0f0f","#202020","#ffffff","#939393","#444444","#ff385c","#e0a04a"]},
7
+ "airtable": {"label":"Airtable","light":["#ffffff","#f8fafc","#181d26","#41454d","#dddddd","#181d26","#9c4a12"],"dark":["#181d26","#1d1f25","#ffffff","#97999d","#4b4f56","#696c72","#e0a04a"]},
8
+ "apple": {"label":"Apple","light":["#ffffff","#ffffff","#1d1d1f","#1d1d1f","#e0e0e0","#0066cc","#9c4a12"],"dark":["#0d0d0e","#1e1e1f","#ffffff","#929293","#424243","#599cde","#e0a04a"]},
9
+ "binance": {"label":"Binance","light":["#eaeef2","#ffffff","#181a20","#707a8a","#c0c4c8","#fcd535","#9c4a12"],"dark":["#0b0c0e","#1c1d1f","#ffffff","#919293","#414143","#fcd535","#e0a04a"]},
10
+ "bmw": {"label":"Bmw","light":["#ffffff","#fafafa","#262626","#6b6b6b","#e6e6e6","#1c69d4","#f59e0b"],"dark":["#1a2129","#262e38","#ffffff","#bbbbbb","#4c5258","#6b9ee3","#f59e0b"]},
11
+ "bmw-m": {"label":"Bmw M","light":["#ededed","#ffffff","#262626","#7e7e7e","#3c3c3c","#ffffff","#f4b400"],"dark":["#111111","#222222","#ffffff","#949494","#454545","#ffffff","#f4b400"]},
12
+ "bugatti": {"label":"Bugatti","light":["#ededed","#ffffff","#262626","#6c6c6c","#262626","#ffffff","#d4a017"],"dark":["#111111","#222222","#ffffff","#949494","#454545","#ffffff","#d4a017"]},
13
+ "cal": {"label":"Cal","light":["#ffffff","#f5f5f5","#111111","#6b7280","#e5e7eb","#111111","#f59e0b"],"dark":["#101010","#1a1a1a","#ffffff","#a1a1aa","#454545","#646464","#f59e0b"]},
14
+ "claude": {"label":"Claude","light":["#faf9f5","#efe9de","#141413","#6c6a64","#e6dfd8","#cc785c","#d4a017"],"dark":["#181715","#252320","#faf9f5","#a09d96","#4a4946","#cc785c","#d4a017"]},
15
+ "clay": {"label":"Clay","light":["#fffaf0","#f5f0e0","#0a0a0a","#6a6a6a","#e5e5e5","#0a0a0a","#f59e0b"],"dark":["#0a1a1a","#1a2a2a","#ffffff","#a0a0a0","#404c4c","#606060","#f59e0b"]},
16
+ "clickhouse": {"label":"Clickhouse","light":["#eeeeee","#ffffff","#262626","#888888","#2a2a2a","#faff69","#f59e0b"],"dark":["#111111","#222222","#ffffff","#949494","#454545","#faff69","#f59e0b"]},
17
+ "cohere": {"label":"Cohere","light":["#ffffff","#ffffff","#212121","#93939f","#d9d9dd","#17171c","#b30000"],"dark":["#0f0f0f","#202020","#ffffff","#939393","#444444","#68686b","#b30000"]},
18
+ "coinbase": {"label":"Coinbase","light":["#ffffff","#ffffff","#0a0b0d","#7c828a","#dee1e6","#0052ff","#9c4a12"],"dark":["#0a0b0d","#16181c","#ffffff","#a8acb3","#404142","#598fff","#e0a04a"]},
19
+ "composio": {"label":"Composio","light":["#eeeeee","#ffffff","#262626","#888888","#222222","#0007cd","#9c4a12"],"dark":["#111111","#222222","#ffffff","#949494","#454545","#595ede","#e0a04a"]},
20
+ "cursor": {"label":"Cursor","light":["#f7f7f4","#ffffff","#26251e","#807d72","#e6e5e0","#f54e00","#9c4a12"],"dark":["#11110e","#22221f","#fbfbfa","#929290","#444442","#f54e00","#e0a04a"]},
21
+ "dell-1996": {"label":"Dell 1996","light":["#ffffff","#ffffff","#000000","#737373","#cccccc","#e91d2a","#9c4a12"],"dark":["#000000","#121212","#ffffff","#8c8c8c","#383838","#e91d2a","#e0a04a"]},
22
+ "elevenlabs": {"label":"Elevenlabs","light":["#f5f5f5","#ffffff","#0c0a09","#777169","#e7e5e4","#292524","#9c4a12"],"dark":["#0c0a09","#1c1917","#ffffff","#a8a29e","#41403f","#747171","#e0a04a"]},
23
+ "expo": {"label":"Expo","light":["#ffffff","#ffffff","#171717","#686868","#f0f0f3","#000000","#9c4a12"],"dark":["#171717","#1a1a1a","#ffffff","#b0b4ba","#4a4a4a","#595959","#e0a04a"]},
24
+ "ferrari": {"label":"Ferrari","light":["#efefef","#ffffff","#262626","#666666","#303030","#da291c","#9c4a12"],"dark":["#111111","#222222","#ffffff","#949494","#454545","#e7746b","#e0a04a"]},
25
+ "figma": {"label":"Figma","light":["#ffffff","#f7f7f5","#000000","#737373","#e6e6e6","#000000","#9c4a12"],"dark":["#000000","#121212","#ffffff","#8c8c8c","#383838","#595959","#e0a04a"]},
26
+ "framer": {"label":"Framer","light":["#eeeeee","#ffffff","#262626","#6c6c6c","#262626","#ffffff","#9c4a12"],"dark":["#111111","#222222","#f7f7f7","#909090","#444444","#ffffff","#e0a04a"]},
27
+ "hashicorp": {"label":"Hashicorp","light":["#ededed","#ffffff","#262626","#6c6c6c","#3b3d45","#000000","#9c4a12"],"dark":["#111111","#222222","#f7f7f7","#909090","#444444","#595959","#e0a04a"]},
28
+ "hp": {"label":"HP","light":["#ffffff","#ffffff","#1a1a1a","#818181","#e8e8e8","#024ad8","#b3262b"],"dark":["#0c0c0c","#1d1d1d","#ffffff","#929292","#414141","#5b89e6","#b3262b"]},
29
+ "ibm": {"label":"Ibm","light":["#ffffff","#ffffff","#161616","#7f7f7f","#e0e0e0","#0f62fe","#9c4a12"],"dark":["#0a0a0a","#1b1b1b","#ffffff","#919191","#404040","#6399fe","#e0a04a"]},
30
+ "intercom": {"label":"Intercom","light":["#f5f1ec","#ffffff","#111111","#787674","#d3cec6","#111111","#9c4a12"],"dark":["#080808","#191919","#faf9f6","#8d8d8b","#3d3d3c","#646464","#e0a04a"]},
31
+ "kraken": {"label":"Kraken","light":["#f5f1fe","#ffffff","#020303","#101114","#5741d8","#7132f5","#9c4a12"],"dark":["#010101","#131313","#faf9ff","#8a898d","#383839","#a37af8","#e0a04a"]},
32
+ "lamborghini": {"label":"Lamborghini","light":["#ffc000","#000000","#000000","#000000","#ffc000","#ffc000","#9c4a12"],"dark":["#000000","#121212","#ffe38c","#8c7d4d","#38321f","#ffc000","#e0a04a"]},
33
+ "linear.app": {"label":"Linear.app","light":["#ededed","#ffffff","#252525","#6b6b6b","#23252a","#5e6ad2","#9c4a12"],"dark":["#111111","#222222","#f7f7f7","#909090","#444444","#969ee2","#e0a04a"]},
34
+ "lovable": {"label":"Lovable","light":["#f7f4ed","#f7f4ed","#000000","#1c1c1c","#1c1c1c","#1c1c1c","#9c4a12"],"dark":["#6f6e6b","#797875","#fbfaf7","#cac9c6","#8e8d8a","#6b6b6b","#e0a04a"]},
35
+ "mastercard": {"label":"Mastercard","light":["#f3f0ee","#f3f0ee","#141413","#626160","#141413","#141413","#9c4a12"],"dark":["#090909","#1a1a1a","#faf8f7","#8e8c8c","#3e3e3d","#666666","#e0a04a"]},
36
+ "meta": {"label":"Meta","light":["#ffffff","#f1f4f7","#1c1e21","#8595a4","#ced0d4","#0064e0","#f7b928"],"dark":["#0d0e0f","#1e1f20","#ffffff","#929393","#424344","#599aeb","#f7b928"]},
37
+ "minimax": {"label":"Minimax","light":["#ffffff","#f2f3f5","#0a0a0a","#606060","#e5e7eb","#0a0a0a","#9c4a12"],"dark":["#040404","#161616","#ffffff","#8e8e8e","#3b3b3b","#606060","#e0a04a"]},
38
+ "mintlify": {"label":"Mintlify","light":["#ffffff","#fafafa","#0a0a0a","#606060","#e5e5e5","#0a0a0a","#9c4a12"],"dark":["#040404","#161616","#ffffff","#8e8e8e","#3b3b3b","#606060","#e0a04a"]},
39
+ "miro": {"label":"Miro","light":["#ffffff","#fafbfc","#1c1c1e","#6b6b6d","#e0e2e8","#1c1c1e","#9c4a12"],"dark":["#0d0d0e","#1e1e1f","#ffffff","#929293","#424243","#6b6b6d","#e0a04a"]},
40
+ "mistral.ai": {"label":"Mistral.ai","light":["#ffffff","#fafafa","#1f1f1f","#6d6d6d","#e5e5e5","#fa520f","#9c4a12"],"dark":["#0e0e0e","#1f1f1f","#ffffff","#939393","#434343","#fa520f","#e0a04a"]},
41
+ "mongodb": {"label":"Mongodb","light":["#ffffff","#f4f7f6","#001e2b","#596d75","#e1e5e8","#00ed64","#9c4a12"],"dark":["#000e13","#121f24","#ffffff","#8c9395","#384347","#00ed64","#e0a04a"]},
42
+ "nike": {"label":"Nike","light":["#ffffff","#ffffff","#111111","#707072","#cacacb","#111111","#9c4a12"],"dark":["#080808","#191919","#ffffff","#909090","#3e3e3e","#646464","#e0a04a"]},
43
+ "nintendo-2001": {"label":"Nintendo 2001","light":["#f6f7fa","#ffffff","#050507","#49526d","#5a5f8c","#e60012","#e60012"],"dark":["#020203","#141415","#fbfbfd","#8b8b8c","#39393a","#ef5965","#e60012"]},
44
+ "notion": {"label":"Notion","light":["#ffffff","#fafaf9","#1a1a1a","#6a6a6a","#e5e3df","#5645d4","#9c4a12"],"dark":["#0c0c0c","#1d1d1d","#ffffff","#929292","#414141","#9186e3","#e0a04a"]},
45
+ "nvidia": {"label":"Nvidia","light":["#ffffff","#f7f7f7","#000000","#757575","#cccccc","#76b900","#df6500"],"dark":["#000000","#121212","#ffffff","#8c8c8c","#383838","#76b900","#df6500"]},
46
+ "ollama": {"label":"Ollama","light":["#ffffff","#ffffff","#000000","#595959","#e5e5e5","#000000","#9c4a12"],"dark":["#171717","#272727","#ffffff","#979797","#4a4a4a","#595959","#e0a04a"]},
47
+ "opencode.ai": {"label":"Opencode.ai","light":["#fdfcfc","#f1eeee","#201d1d","#646262","#646262","#201d1d","#ff9f0a"],"dark":["#201d1d","#302c2c","#fdfcfc","#9a9898","#514e4e","#6e6c6c","#ff9f0a"]},
48
+ "pinterest": {"label":"Pinterest","light":["#ffffff","#f6f6f3","#000000","#62625b","#dadad3","#e60023","#9e0a0a"],"dark":["#262622","#353531","#ffffff","#9d9d9c","#565653","#ef5970","#9e0a0a"]},
49
+ "playstation": {"label":"Playstation","light":["#f3f3f3","#f5f7fa","#000000","#6d6d6d","#c2c2c2","#0070d1","#c81b3a"],"dark":["#000000","#121314","#ffffff","#8c8c8c","#383838","#59a2e1","#c81b3a"]},
50
+ "posthog": {"label":"Posthog","light":["#eeefe9","#ffffff","#23251d","#6c6e63","#bfc1b7","#f7a501","#9c4a12"],"dark":["#23251d","#32342d","#ffffff","#9c9d99","#53554f","#f7a501","#e0a04a"]},
51
+ "raycast": {"label":"Raycast","light":["#eeeeee","#ffffff","#252525","#6b6b6b","#242728","#ffffff","#9c4a12"],"dark":["#111111","#222222","#ffffff","#949494","#454545","#ffffff","#e0a04a"]},
52
+ "renault": {"label":"Renault","light":["#ffffff","#ffffff","#000000","#666666","#f2f2f2","#ffed00","#f0ad4e"],"dark":["#000000","#121212","#ffffff","#8c8c8c","#383838","#ffed00","#f0ad4e"]},
53
+ "replicate": {"label":"Replicate","light":["#f9f7f3","#ffffff","#202020","#646464","#202020","#ea2804","#9c4a12"],"dark":["#202020","#303030","#fcfcfc","#999999","#505050","#ea2804","#e0a04a"]},
54
+ "resend": {"label":"Resend","light":["#ededed","#ffffff","#262626","#6c6c6c","#323333","#fcfdff","#9c4a12"],"dark":["#111111","#222222","#f7f7f7","#909090","#444444","#fcfdff","#e0a04a"]},
55
+ "revolut": {"label":"Revolut","light":["#f4f4f4","#ffffff","#191c1f","#505a63","#191c1f","#494fdf","#9c4a12"],"dark":["#0b0d0e","#1c1e1f","#ffffff","#919293","#414243","#898dea","#e0a04a"]},
56
+ "runwayml": {"label":"Runwayml","light":["#ededed","#ffffff","#000000","#535353","#27272a","#0b6e99","#9c4a12"],"dark":["#000000","#121212","#f7f7f7","#888888","#363636","#60a1bd","#e0a04a"]},
57
+ "sanity": {"label":"Sanity","light":["#eeeeee","#ffffff","#020202","#0b0b0b","#000000","#f36458","#dd0000"],"dark":["#010101","#131313","#f7f7f7","#888888","#373737","#f36458","#dd0000"]},
58
+ "sentry": {"label":"Sentry","light":["#eaeef2","#ffffff","#1f1633","#7a7789","#c1c3cc","#150f23","#9c4a12"],"dark":["#0e0a17","#1f1b27","#f6f7f9","#8e8c93","#413e49","#676370","#e0a04a"]},
59
+ "shopify": {"label":"Shopify","light":["#eaeef2","#ffffff","#000000","#696b6d","#bbbec2","#000000","#9c4a12"],"dark":["#000000","#121212","#ffffff","#8c8c8c","#383838","#595959","#e0a04a"]},
60
+ "slack": {"label":"Slack","light":["#ffffff","#ffffff","#1d1d1d","#838383","#e6e6e6","#4a154b","#9c4a12"],"dark":["#0d0d0d","#1e1e1e","#ffffff","#929292","#424242","#89678a","#e0a04a"]},
61
+ "spacex": {"label":"Spacex","light":["#eaeef2","#ffffff","#000000","#696b6d","#bbbec2","#000000","#9c4a12"],"dark":["#000000","#121212","#f6f7f9","#878889","#363637","#595959","#e0a04a"]},
62
+ "spotify": {"label":"Spotify","light":["#eeeeee","#ffffff","#030303","#555555","#4d4d4d","#1ed760","#f3727f"],"dark":["#010101","#131313","#f7f7f7","#888888","#373737","#1ed760","#f3727f"]},
63
+ "starbucks": {"label":"Starbucks","light":["#f2f0eb","#f2f0eb","#1e3932","#687973","#00754a","#cba258","#c82014"],"dark":["#0e1a16","#1f2a26","#f9f8f6","#8f9491","#424b47","#cba258","#c82014"]},
64
+ "stripe": {"label":"Stripe","light":["#ffffff","#ffffff","#0d253d","#7a8794","#e3e8ee","#533afd","#9c4a12"],"dark":["#06111b","#17222b","#ffffff","#8f9498","#3d454d","#8f7ffe","#e0a04a"]},
65
+ "supabase": {"label":"Supabase","light":["#ffffff","#ffffff","#171717","#7f7f7f","#dfdfdf","#3ecf8e","#9c4a12"],"dark":["#0a0a0a","#1b1b1b","#ffffff","#919191","#404040","#3ecf8e","#e0a04a"]},
66
+ "superhuman": {"label":"Superhuman","light":["#ffffff","#ffffff","#292827","#898988","#e8e4dd","#1b1938","#9c4a12"],"dark":["#121212","#232323","#ffffff","#949494","#464646","#6b6a7e","#e0a04a"]},
67
+ "tesla": {"label":"Tesla","light":["#f1f5fd","#ffffff","#091022","#3e6ae1","#3e6ae1","#3e6ae1","#9c4a12"],"dark":["#04070f","#161820","#f9fafe","#8b8d92","#3a3c44","#829eec","#e0a04a"]},
68
+ "theverge": {"label":"Theverge","light":["#eeeeee","#ffffff","#030303","#131313","#ffffff","#131313","#5200ff"],"dark":["#010101","#131313","#f7f7f7","#888888","#373737","#666666","#5200ff"]},
69
+ "together.ai": {"label":"Together.ai","light":["#ffffff","#ffffff","#000000","#959494","#959494","#000000","#9c4a12"],"dark":["#313641","#3f444e","#ffffff","#a2a5aa","#5e626b","#595959","#e0a04a"]},
70
+ "uber": {"label":"Uber","light":["#ffffff","#ffffff","#000000","#595959","#cccccc","#000000","#9c4a12"],"dark":["#000000","#121212","#ffffff","#8c8c8c","#383838","#595959","#e0a04a"]},
71
+ "vercel": {"label":"Vercel","light":["#ffffff","#ffffff","#171717","#888888","#ebebeb","#171717","#f5a623"],"dark":["#0a0a0a","#1b1b1b","#ffffff","#919191","#404040","#686868","#f5a623"]},
72
+ "vodafone": {"label":"Vodafone","light":["#ffffff","#ffffff","#25282b","#717375","#d3d4d5","#e60000","#9c4a12"],"dark":["#111213","#222324","#ffffff","#949495","#454647","#ef5959","#e0a04a"]},
73
+ "voltagent": {"label":"Voltagent","light":["#eeeeee","#ffffff","#242424","#6b6b6b","#3d3a39","#00d992","#9c4a12"],"dark":["#101010","#212121","#f7f7f7","#8f8f8f","#434343","#00d992","#e0a04a"]},
74
+ "warp": {"label":"Warp","light":["#f0f0f0","#ffffff","#252524","#6c6c6b","#3f3a36","#f7f5f0","#9c4a12"],"dark":["#111110","#222221","#f8f8f8","#909090","#444443","#f7f5f0","#e0a04a"]},
75
+ "webflow": {"label":"Webflow","light":["#ffffff","#ffffff","#080808","#898989","#d8d8d8","#080808","#9c4a12"],"dark":["#040404","#161616","#ffffff","#8e8e8e","#3b3b3b","#5e5e5e","#e0a04a"]},
76
+ "wired": {"label":"Wired","light":["#ffffff","#ffffff","#000000","#757575","#e0e0e0","#000000","#9c4a12"],"dark":["#000000","#121212","#ffffff","#8c8c8c","#383838","#595959","#e0a04a"]},
77
+ "wise": {"label":"Wise","light":["#ffffff","#ffffff","#0e0f0c","#868685","#cfcfce","#9fe870","#ffd11a"],"dark":["#060705","#171816","#ffffff","#8f8f8e","#3d3e3c","#9fe870","#ffd11a"]},
78
+ "x.ai": {"label":"X.ai","light":["#eeeeee","#ffffff","#262626","#7d8187","#212327","#ffffff","#9c4a12"],"dark":["#111111","#222222","#f7f7f7","#909090","#444444","#ffffff","#e0a04a"]},
79
+ "zapier": {"label":"Zapier","light":["#fffefb","#ffffff","#201515","#6e6766","#d2cfcd","#ff4f00","#9c4a12"],"dark":["#0e0909","#1f1a1a","#fffffd","#93908f","#433f3f","#ff4f00","#e0a04a"]}
80
+ }
81
+ }
@@ -0,0 +1,138 @@
1
+ # 대비 보정이 이 모양인 이유
2
+
3
+ 대상 독자: 보정 규칙을 바꾸려 하거나, 왜 변수가 13개인지 묻는 사람.
4
+ 값의 원천은 이 엔진의 파이썬 원본에 적힌 주석이다. 아래 실측값은 그 주석에서 옮겼다.
5
+
6
+ ## 한 줄 요약
7
+
8
+ - 목표는 "안 보이면 기능이 사라지는 것"만 3.0 으로 올리는 것이다.
9
+ - 배경과 `rule` 은 건드리지 않는다.
10
+ - 본문 색은 제자리에서 옮기지 않고 새 이름으로 나눈다.
11
+
12
+ ## 상수
13
+
14
+ | 상수 | 값 | 뜻 |
15
+ |---|---|---|
16
+ | `TARGET` | 3.0 | 링크, 강조, 버튼 경계의 목표 대비 |
17
+ | `LABEL_TARGET` | 4.5 | 버튼 글자의 목표 대비 |
18
+ | `CONTRAST_JOBS` | `accent` 를 `bg` 에서 3.0, `surface` 에서 나빠지지 않게 | 제자리 보정 |
19
+ | `SURFACE_JOBS` | `ink`, `dim`, `accent`, `warn` 을 `surface` 에서 3.0, `bg` 에서 나빠지지 않게 | 새 이름으로 분리 |
20
+ | `LABEL_JOBS` | `label-on-accent` 는 `accent-on-surface` 위, `label-on-page-accent` 는 `accent` 위 | 글자색 선택 |
21
+
22
+ ## 한 변수가 두 배경에 쓰이면 한쪽 기준으로 못 고친다
23
+
24
+ 받기 버튼은 `--bg` 위에 테두리와 글자만으로 서 있다. 눌리는 순간 `--accent` 가 배경이 되고 글자는 `--surface` 로 뒤집힌다.
25
+ 그래서 `--accent` 는 배경을 둘 만난다. 한쪽만 보고 고치면 다른 쪽이 깨진다(실측 23개 조합).
26
+
27
+ - 두 배경을 함께 보는 것이 보정의 전부다.
28
+ - 나머지는 명도를 훑는 반복문뿐이다.
29
+ - 같은 변수가 두 배경에 쓰이는데 두 배경이 요구하는 값이 다르면 답은 이름을 나누는 것이다.
30
+
31
+ ## 왜 `--ink` 는 제자리에서 안 옮기는가
32
+
33
+ `--ink` 는 테마 고르개의 글자색이면서 `body` 의 글자색이기도 하다.
34
+ 검은 `--surface` 위에서 안 보이는 것을 고치려고 `--ink` 값을 옮기면, 멀쩡하던 `--bg` 위 본문까지 흐려진다.
35
+
36
+ | 조합 | 위치 | 본문 대비 |
37
+ |---|---|---|
38
+ | `lamborghini/light` | 보정 전 | 12.79 |
39
+ | `lamborghini/light` | `--ink` 를 제자리 보정한 뒤 | 4.20 |
40
+
41
+ 2026-09-08 크로미움 실측이다. 승인된 범위는 "기능이 사라지는 것만"이었고 본문은 사라지지 않았다.
42
+
43
+ 그래서 원본 `--ink` 는 그대로 두고 `--ink-on-surface` 를 따로 굽는다. `--dim` 도 같다.
44
+
45
+ - `--surface` 가 배경인 규칙만 `-on-surface` 변수를 쓴다.
46
+ - `--bg` 위 규칙이 이것을 쓰면 방금 막은 번짐이 돌아온다.
47
+ - 148 조합 중 `ink` 와 `dim` 값이 실제로 갈라지는 것은 3 조합(값 넷)뿐이다. `lamborghini/light` 의 `ink` 와 `dim`, `lovable/dark` 의 `dim`, `meta/light` 의 `dim` 이다.
48
+ - 나머지 145 조합은 두 변수가 같은 값이라 화면이 한 픽셀도 안 바뀐다.
49
+
50
+ `accent` 와 `warn` 이 목록에 든 이유는 다음과 같다.
51
+
52
+ | 변수 | 들어온 이유 |
53
+ |---|---|
54
+ | `accent-on-surface` | 의견 상자의 고른 별(2026-10-01). 제자리 `--accent` 는 `--bg` 기준이라 `--surface` 위에서 23 조합이 3.0 아래였음 |
55
+ | `warn-on-surface` | 2026-10-15 전수 조사. 상자 안 위험 버튼, 경고 상자, 실패 문구가 원본 `--warn` 이었고 최저 1.46 이었음 |
56
+
57
+ ## 왜 목표가 3.0 인가
58
+
59
+ - 올리는 대상은 안 보이면 기능이 통째로 사라지는 셋이다.
60
+ - 흐린 보조 텍스트와 흐린 경계선은 읽기 나쁠 뿐이다. 어디를 눌러야 하는지는 말해 준다.
61
+ - 팔레트 74개를 전부 4.5 로 끌어올리면 색이 전부 평평해진다. 테마가 테마가 아니게 된다.
62
+
63
+ ## 왜 배경과 `rule` 은 건드리지 않는가
64
+
65
+ 배경을 옮기면 그 배경을 쓰는 모든 색의 대비가 같이 움직인다.
66
+ 고칠 대상은 언제나 배경 위에 얹히는 색이고, 배경은 기준으로만 쓴다.
67
+
68
+ ## `--rule` 의 대비는 보장되지 않는다
69
+
70
+ 보정에서 뺀 것은 의도다. 그래서 소비자가 알아야 하는 수치가 있다.
71
+
72
+ | 재는 자리 | 3.0 미만 | 1.5 미만 | 최저 |
73
+ |---|---|---|---|
74
+ | `--rule` 대 `--bg` | 122 / 148 | 35 | 1.00 (`lamborghini/light`) |
75
+ | `--rule` 대 `--surface` | 121 / 148 | 37 | 1.00 (`theverge/light`) |
76
+
77
+ - 대비 1.00 은 두 색이 같다는 뜻이다. 경계선이 화면에 없다.
78
+ - `check` 는 이 자리를 재지 않는다. 종료 코드 0 이 경계선이 보인다는 뜻은 아니다.
79
+ - 경계선만으로 정보를 전달하지 않는다. 누를 수 있는 것과 고른 것은 글자색이나 배경색으로도 표시한다.
80
+ - 포커스 테두리는 `--accent-on-surface` 나 `--accent` 를 쓴다. 그 둘은 보정을 거친다.
81
+
82
+ ## `keep_bg` 의 하한은 왜 `min(원래 대비, 3.0)` 인가
83
+
84
+ `keep_bg` 는 "여기서는 나빠지면 안 되는 배경"이다.
85
+
86
+ - 원래 대비가 2.7 이었으면 2.7 만 지키면 된다. 더 좋아지라고 요구하지 않는다.
87
+ - 양쪽 배경에 3.0 을 걸면 낮은 쪽을 올리려다 높은 쪽이 3.0 아래로 떨어진다. 답이 없는 조합이 생긴다.
88
+
89
+ ## 왜 명도를 전부 훑는가
90
+
91
+ 명도를 0.000 에서 1.000 까지 1001 단계로 전부 훑어 원래 명도에서 가장 가까운 후보를 고른다. 방향을 미리 정하지 않는다.
92
+
93
+ - 밝은 `bg` 와 검은 `surface` 사이에 낀 색은 위도 아래도 아닌 가운데로 가야 한다.
94
+ - "어두우면 밝힌다" 같은 규칙은 그 자리에서 틀린다.
95
+ - 색조와 채도는 유지하고 명도만 옮긴다(HLS). 라즈베리는 여전히 라즈베리이고 노랑은 여전히 노랑이다.
96
+ - 같은 입력에는 같은 출력이다.
97
+
98
+ 판정의 입력은 언제나 원본 팔레트다. 고친 값을 다음 계산에 먹이면 결과가 목록 순서에 딸려 간다.
99
+
100
+ ## 왜 버튼 글자가 `--label-on-accent` 인가
101
+
102
+ 버튼 배경은 `--accent-on-surface` 이고 상자 배경 위에서 3.0 을 만족한다. 그 위 글자는 본문 글자라 4.5 가 필요하다.
103
+
104
+ - 경계 3.0 과 글자 4.5 가 같은 쌍을 쓰면 버튼 배경을 4.5 까지 밀어야 한다.
105
+ - 그러면 같은 변수를 쓰는 고른 별까지 딸려 간다.
106
+ - 그래서 글자는 `--label-on-accent` 라는 새 이름으로 따로 고른다.
107
+
108
+ 글자 배경은 자리마다 하나씩이다(2026-10-15 전수 조사).
109
+
110
+ | 변수 | 올라가는 배경 | 자리 |
111
+ |---|---|---|
112
+ | `--label-on-accent` | `--accent-on-surface` | 상자 안 버튼 |
113
+ | `--label-on-page-accent` | `--accent` | 페이지 위 받기 버튼의 hover |
114
+
115
+ 같은 이름 하나로 두 배경을 맞추면 한쪽이 4.5 아래로 떨어진다.
116
+
117
+ 글자색 후보는 명도를 훑지 않는다. `surface`, `bg`, `ink`, `#ffffff`, `#000000` 순서로 4.5 를 넘는 첫 후보를 고른다.
118
+
119
+ ## 답이 없을 때
120
+
121
+ 답이 없으면 원래 값을 그대로 돌려준다. 조용히 포기하는 것이 아니다.
122
+ `check` 가 출력되는 값을 재므로 그 자리를 빨강(종료 코드 1)으로 보고한다.
123
+
124
+ 버튼 글자는 기본 목표 4.5 에서 답이 늘 있다. 어떤 색이든 흰색과 검정 중 하나와는 4.58 이상이기 때문이다.
125
+ `--label-target` 을 4.58 넘게 주면 답이 없는 조합이 생긴다. 그때는 후보 중 대비가 가장 높은 색을 내고, `check` 가 그 자리를 빨강으로 보고한다.
126
+
127
+ ## check 가 재는 것
128
+
129
+ | 자리 | 기준 |
130
+ |---|---|
131
+ | `accent` 대 `bg` | `TARGET` 이상 |
132
+ | `accent` 대 `surface` | `min(원래 대비, TARGET)` 이상 |
133
+ | `ink-on-surface`, `dim-on-surface`, `accent-on-surface`, `warn-on-surface` 대 `surface` | `TARGET` 이상 |
134
+ | 위 넷 대 `bg` | `min(원래 대비, TARGET)` 이상 |
135
+ | `label-on-accent` 대 `accent-on-surface` | `LABEL_TARGET` 이상 |
136
+ | `label-on-page-accent` 대 `accent` | `LABEL_TARGET` 이상 |
137
+
138
+ 본문 대비(`ink` 대 `bg`)는 보고만 하고 판정하지 않는다.
@@ -0,0 +1,161 @@
1
+ # 프로젝트에 테마 붙이기
2
+
3
+ 대상 독자: 자기 프로젝트에 `theme-selection` 출력을 넣는 사람.
4
+ `<스킬>` 은 스킬 디렉터리 절대경로다. 명령과 옵션은 `SKILL.md` 의 표를 따른다.
5
+
6
+ ## 공통 순서
7
+ 1. `css` 로 CSS 파일을 낸다.
8
+ 2. 프로젝트가 그 파일을 읽는다.
9
+ 3. `<html>` 에 `data-theme` 과 `data-mode` 속성이 걸린다. 걸지 않아도 `:root` 가 기본 테마의 라이트와 다크를 들고 있어 화면이 읽힌다. 다크는 `--dark-via` 로 고른 자리(속성, 클래스, OS 설정)를 따른다.
10
+ 4. 색을 쓰는 자리는 하드코딩 대신 `var(--ink)` 처럼 변수를 쓴다.
11
+
12
+ ```
13
+ node <스킬>/scripts/themes.mjs css --only superhuman,airbnb,stripe --out public/themes.css
14
+ ```
15
+
16
+ ## 순수 CSS
17
+
18
+ `<link>` 하나와 `var(--ink)` 면 된다.
19
+
20
+ ```html
21
+ <link rel="stylesheet" href="/themes.css">
22
+ ```
23
+
24
+ ```css
25
+ body { background: var(--bg); color: var(--ink); }
26
+ .card { background: var(--surface); color: var(--ink-on-surface); border: 1px solid var(--rule); }
27
+ .card .muted { color: var(--dim-on-surface); }
28
+ a { color: var(--accent); }
29
+ .card a { color: var(--accent-on-surface); }
30
+ .card .danger { color: var(--warn-on-surface); box-shadow: 0 1px 4px var(--shadow); }
31
+ .btn-primary { background: var(--accent-on-surface); color: var(--label-on-accent); }
32
+ .get:hover { background: var(--accent); color: var(--label-on-page-accent); }
33
+ ```
34
+
35
+ 배경이 `--surface` 인 규칙만 `-on-surface` 변수를 쓴다. `--bg` 위 규칙은 `--ink`, `--dim`, `--accent`, `--warn` 이다.
36
+
37
+ ## Tailwind
38
+
39
+ CSS 변수를 `theme.extend.colors` 에 매핑한다. 변수는 `#rrggbb` 값이라 그대로 `var()` 로 연결한다.
40
+
41
+ ```js
42
+ // tailwind.config.js
43
+ module.exports = {
44
+ theme: {
45
+ extend: {
46
+ colors: {
47
+ bg: "var(--bg)",
48
+ surface: "var(--surface)",
49
+ ink: "var(--ink)",
50
+ dim: "var(--dim)",
51
+ rule: "var(--rule)",
52
+ accent: "var(--accent)",
53
+ warn: "var(--warn)",
54
+ "ink-on-surface": "var(--ink-on-surface)",
55
+ "dim-on-surface": "var(--dim-on-surface)",
56
+ "accent-on-surface": "var(--accent-on-surface)",
57
+ "warn-on-surface": "var(--warn-on-surface)",
58
+ "label-on-accent": "var(--label-on-accent)",
59
+ "label-on-page-accent": "var(--label-on-page-accent)",
60
+ },
61
+ },
62
+ },
63
+ };
64
+ ```
65
+
66
+ 이 방식은 `bg-surface text-ink-on-surface` 처럼 쓴다. Tailwind 의 `dark:` 변형을 쓰려면 `--dark-via class` 로 CSS 를 내고 `darkMode: "class"` 를 맞춘다.
67
+ 고르개를 붙이면 `widget` 에도 `--dark-via class` 를 준다. 모드 버튼이 `data-mode` 와 `.dark` 를 함께 바꾸므로 이 설정과 맞는다.
68
+ `--prefix` 를 줬다면 `var(--<prefix>bg)` 처럼 이름을 맞춘다.
69
+
70
+ ## React 와 Vue
71
+
72
+ 속성은 `document.documentElement` 에 건다. 컴포넌트 안의 `div` 에 걸면 선택자 `html[data-theme=...]` 가 맞지 않는다.
73
+
74
+ ```jsx
75
+ // React
76
+ import { useEffect, useState } from "react";
77
+ export function useTheme(allowed, fallback) {
78
+ const [theme, setTheme] = useState(() => {
79
+ let saved = null;
80
+ // 서버 렌더에는 localStorage 가 없고, 저장소를 막은 브라우저는 예외를 던진다.
81
+ try { saved = localStorage.getItem("theme"); } catch { saved = null; }
82
+ return allowed.includes(saved) ? saved : fallback;
83
+ });
84
+ useEffect(() => {
85
+ document.documentElement.setAttribute("data-theme", theme);
86
+ try { localStorage.setItem("theme", theme); } catch { /* 막힌 저장소 */ }
87
+ }, [theme]);
88
+ return [theme, setTheme];
89
+ }
90
+ ```
91
+
92
+ Vue 3 도 같다. `ref` 에 이름을 담고 `watchEffect` 안에서 `document.documentElement.setAttribute("data-theme", theme.value)` 를 부른다.
93
+
94
+ 모드(`data-mode="dark"`)도 같은 자리에 같은 방식으로 건다. `--dark-via class` 로 냈으면 `classList.toggle("dark", 다크인지)` 로 `.dark` 를 건다. 저장된 값은 허용 목록에 대어 본 뒤에 쓴다.
95
+
96
+ 첫 그림 전의 깜빡임은 아래 head 스크립트를 `index.html` 의 `<head>` 에 두어야 막는다.
97
+
98
+ ## SCSS 와 styled-components
99
+
100
+ - SCSS: `color: var(--ink);` 처럼 변수를 그대로 쓴다. SCSS 변수에 값을 복사하지 않는다.
101
+ - styled-components: 템플릿 안에 `color: var(--ink);` 를 그대로 쓴다. `ThemeProvider` 로 값을 다시 들고 있지 않는다.
102
+
103
+ ## 고르개 붙이기
104
+
105
+ `widget` 이 세 덩이를 주석으로 가른 하나의 출력으로 낸다.
106
+
107
+ ```
108
+ node <스킬>/scripts/themes.mjs widget --only superhuman,airbnb,stripe --out widget.html
109
+ ```
110
+
111
+ CSS 를 낼 때와 같은 `--dark-via` 를 준다. 모드 버튼은 `data-mode` 와 `.dark` 를 함께 바꾸고, `--dark-via media` 면 버튼이 없다(다크는 OS 설정을 따름).
112
+
113
+ | 덩이 | 두는 곳 | 하는 일 |
114
+ |---|---|---|
115
+ | 고르개 마크업 | 페이지의 헤더 등 보이는 자리 | `<select>` 와 모드 버튼 |
116
+ | head 스크립트 | `<head>` | 저장된 이름을 `document.documentElement` 에 바로 건다 |
117
+ | 배선 스크립트 | 마크업 뒤나 `<body>` 끝 | 고르개 요소를 찾아 묶는다 |
118
+
119
+ 🔴 head 스크립트는 `<head>` 에, 배선 스크립트는 `ready` 래퍼와 함께 둔다. 둘을 합치지 않는다.
120
+
121
+ - head 스크립트는 DOM 을 보지 않는다. 그래서 `<head>` 에서 즉시 돌아도 된다. 즉시 도는 것이 첫 그림의 깜빡임을 막는다.
122
+ - 배선 스크립트는 마크업을 본다. 마크업이 아직 없을 때 돌면 `getElementById` 가 빈손이고 고르개가 영원히 안 움직인다.
123
+ - 배선 스크립트는 `readyState` 를 보고 즉시 또는 `DOMContentLoaded` 에서 부르는 래퍼로 감싸져 있다. 이 래퍼를 떼지 않는다.
124
+
125
+ 저장된 이름은 닫힌 집합(박혀 있는 테마 목록)에 대어 본다. 목록 밖이면 기본 테마로 되돌린다.
126
+
127
+ CSP(Content-Security-Policy)를 쓰는 사이트는 인라인 스크립트 둘에 nonce 나 해시를 붙인다. `'unsafe-inline'` 으로 풀지 않는다.
128
+
129
+ ## 모드만 쓰는 화면
130
+
131
+ 테마를 고르는 사람이 없고 라이트와 다크만 OS 설정을 따르면 되면 고르개가 필요 없다.
132
+
133
+ ```
134
+ node <스킬>/scripts/themes.mjs css --only superhuman --dark-via media --out public/themes.css
135
+ ```
136
+
137
+ `@media (prefers-color-scheme: dark)` 안에 다크 값이 들어간다. `:root` 도 기본 테마의 다크를 그 안에 들고 있어, `<html>` 에 속성을 걸지 않아도 OS 가 다크면 다크가 된다. JS 가 없어도 된다.
138
+
139
+ ## 서버 렌더
140
+
141
+ 저장된 이름을 쿠키로 받아 `<html>` 속성에 미리 박는 쪽이 깜빡임이 없다.
142
+
143
+ ```js
144
+ // 예: Express
145
+ const THEMES = new Set(["superhuman", "airbnb", "stripe"]);
146
+ app.get("/", (req, res) => {
147
+ const t = THEMES.has(req.cookies.theme) ? req.cookies.theme : "superhuman";
148
+ const m = req.cookies.mode === "dark" ? "dark" : "light";
149
+ res.send(`<!doctype html><html data-theme="${t}" data-mode="${m}">...`);
150
+ });
151
+ ```
152
+
153
+ - 쿠키 값은 허용 목록에 대어 본 값만 속성에 넣는다. 그대로 넣으면 속성 주입이 된다.
154
+
155
+ ## CSS 크기
156
+
157
+ - 테마당 규칙이 둘이다. 라이트 하나, 다크 하나.
158
+ - 규칙마다 13 선언이다.
159
+ - 74종 전부면 148 규칙이다.
160
+ - 여기에 `:root` 의 기본 테마 라이트와 다크 둘, `--shadow` 둘이 더해진다.
161
+ - 쓸 테마만 `--only` 로 줄이는 쪽을 권한다.