@highpixel-co/palda-design-system 0.4.1 → 0.6.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.
Files changed (38) hide show
  1. package/README.md +21 -2
  2. package/dist/{chunk-GPCPGABI.js → chunk-6JZPK2W5.js} +10 -2
  3. package/dist/{chunk-Q4UXASZH.js → chunk-QW2SD3XV.js} +13 -5
  4. package/dist/guide/LICENSE +21 -0
  5. package/dist/guide/NOTICE +29 -0
  6. package/dist/guide/README.md +57 -0
  7. package/dist/guide/catalog/components.yml +272 -0
  8. package/dist/guide/catalog/drafts.yml +333 -0
  9. package/dist/guide/catalog/icons.yml +309 -0
  10. package/dist/guide/catalog/patterns.yml +132 -0
  11. package/dist/guide/catalog/tokens.yml +14 -0
  12. package/dist/guide/docs/ACCESSIBILITY.md +36 -0
  13. package/dist/guide/docs/AI_UI_DESIGNER_HANDOFF.md +258 -0
  14. package/dist/guide/docs/COMPONENT_POLICY.md +105 -0
  15. package/dist/guide/docs/CONSUMER_GUIDE.md +131 -0
  16. package/dist/guide/docs/CONTENT.md +148 -0
  17. package/dist/guide/docs/DESIGN_GRAMMAR.md +480 -0
  18. package/dist/guide/docs/DESIGN_PRINCIPLES.md +263 -0
  19. package/dist/guide/docs/FIGMA_ALIGNMENT_DELTA.md +141 -0
  20. package/dist/guide/docs/FIGMA_NAME_MAPPING.md +84 -0
  21. package/dist/guide/docs/FIGMA_WORKFLOW.md +33 -0
  22. package/dist/guide/docs/ICON_POLICY.md +175 -0
  23. package/dist/guide/docs/LAYOUT.md +221 -0
  24. package/dist/guide/docs/PATTERN_POLICY.md +13 -0
  25. package/dist/guide/docs/TOKEN_POLICY.md +266 -0
  26. package/dist/guide/icons/manifest.json +572 -0
  27. package/dist/harness/check.mjs +330 -0
  28. package/dist/harness/cli.mjs +88 -0
  29. package/dist/harness/metadata.json +1325 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +2 -2
  32. package/dist/patterns.d.ts +15 -2
  33. package/dist/patterns.js +2 -2
  34. package/dist/scripts/check-examples.mjs +406 -0
  35. package/dist/styles.css +11 -0
  36. package/dist/ui.d.ts +6 -1
  37. package/dist/ui.js +1 -1
  38. package/package.json +13 -2
@@ -0,0 +1,480 @@
1
+ # Design Grammar
2
+
3
+ **화면을 조립하며 갈리는 판단 — 어느 pattern을 쓰나, 무엇을 카드로 감싸나, 상태는 누가 갖나 — 을
4
+ 한 곳에서 정하는 문서다.**
5
+
6
+ ## 무엇을 언제 읽나
7
+
8
+ 화면을 조립할 때 여는 표다. **문서가 아니라 절 단위로 연다** — `LAYOUT.md`와 이 문서는 각각
9
+ 10KB라 통째로 읽으면 읽지 않는 것이 없어진다.
10
+
11
+ **순서는 되돌리기 비싼 것부터다.** 셸을 잘못 잡으면 전부 다시 짜야 하고, 색은 나중에 바꿔도 싸다.
12
+
13
+ | 질문 | 어디 |
14
+ | ------------------------- | ------------------------------------------------------------------------- |
15
+ | 무엇을 먼저 보여주나? | [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md) §4 — 강조는 한 화면에 하나 |
16
+ | 이거 하면 안 되는 건가? | 같은 문서 §6 지양할 디자인 — 시각·구조 두 표 |
17
+ | 규칙끼리 부딪히면? | 같은 문서 §7 판단이 충돌할 때 |
18
+ | 이 화면의 틀은? | [`LAYOUT.md`](LAYOUT.md) §셸 — 모든 화면이 같다. 한 번만 읽으면 된다 |
19
+ | 페이지인가 모달인가? | 아래 §띄우나 마나 §언제 모달인가 — 세 관문 |
20
+ | 무엇으로 조립하나? | 아래 §어느 pattern을 고르나 |
21
+ | 이 자리에 이 패턴이 맞나? | [`catalog/patterns.yml`](../catalog/patterns.yml)의 `use_for`·`avoid_for` |
22
+ | 이게 이 화면 것인가? | 아래 §곁을 어디 두나 — 박스 질문보다 **먼저** 묻는다 |
23
+ | 무엇을 크게 그리나? | 아래 §층이 무게를 정한다 — 그리기 전에 기능 위계부터 |
24
+ | 값이 나쁠 때는? | 아래 §값이 나쁘면 잠깐 올라온다 |
25
+ | 박스로 감싸나? | 아래 §판단 한 줄 → §쓰는 경우 |
26
+ | 면인가 행인가? | [`TOKEN_POLICY.md`](TOKEN_POLICY.md) §간격 마지막 줄 + §라운드 표 |
27
+ | 이 카드는 눌리나? | 아래 §세 번째 질문 — 고르는 카드 / 담는 카드 |
28
+ | 로딩·빈·오류는 누가? | 아래 §상태는 누가 갖나 |
29
+ | 여백은 얼마? | [`LAYOUT.md`](LAYOUT.md) §고를 때 (§토큰 표는 그다음) |
30
+ | 색은? | [`TOKEN_POLICY.md`](TOKEN_POLICY.md) §색 — §켤레와 §글자와 아이콘 표만 |
31
+ | 글자 크기는? | [`TOKEN_POLICY.md`](TOKEN_POLICY.md) §타이포 |
32
+ | props는? | 타입. 문서에 없는 것이 의도된 것이다 |
33
+ | 좁아지면? | [`LAYOUT.md`](LAYOUT.md) §좁아질 때 |
34
+
35
+ **ADR은 여기서 읽지 않는다.** 근거 보관소지 지시서가 아니고, 대체된 값이 그대로 남아 있다.
36
+ 기준은 [`AGENTS.md`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/AGENTS.md)의 §ADR은 화면 만들 때 읽지 않는다에 있다.
37
+
38
+ ### 아직 답할 문서가 없는 질문
39
+
40
+ 지어내지 말고 멈춘다. 여기 질문이 있으면 화면에서 마주쳐도 값을 고르지 않는다.
41
+
42
+ **지금은 비어 있다.** 마지막으로 남아 있던 제목·본문·보조 정보의 위계는 2026-09-02에
43
+ [`TOKEN_POLICY.md`](TOKEN_POLICY.md) §타이포가 받았다 — 페이지 `headline`(24) → 섹션·카드
44
+ `body1`(16) → 행 `body2`(14) → 보조 `label`(12)이다. 2026-09-03에 행이 두 칸으로 갈렸다.
45
+ **화면의 본문인 목록은 제목 `body1`(16) + 설명 `body2`(14)로 한 칸 올라간다**
46
+ ([`decisions/0034`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0034-the-body-list-lifts-one-step.md)) — `ListRow`의 `emphasis`가
47
+ 그것이고, 판정은 "선언서의 본문 문장이 가리키는 목록인가" 하나다.
48
+
49
+ ### 주 액션을 어디 두나
50
+
51
+ **시스템이 고르지 않는다. 화면이 고르고 밝힌다.** 페이지 헤더 우측 상단(`LAYOUT.md` §셸의 바디)과
52
+ 아래 `BottomBar` 둘 다 맞는 자리이고, 어느 쪽인지는 서비스 성격이 정한다 — 읽다가 가끔 누르는
53
+ 화면은 위, 채워 넣고 마지막에 확정하는 화면은 아래다.
54
+
55
+ **고르는 것은 자유지만 개수는 하나다.** 한 화면의 주 액션은 하나이고, 고른 자리도 하나다. 위아래에
56
+ 같은 액션을 두 번 두지 않는다. 근거는
57
+ [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md) §4의 주 액션의 자리에 있다.
58
+
59
+ **0개도 된다.** 목록을 훑고 행을 눌러 들어가는 화면은 행이 곧 액션이라 세울 것이 없다. 선언서에
60
+ `없음`이라 적는다 ([`decisions/0033`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0033-a-screen-may-have-no-primary-action.md)).
61
+ **자리를 채우려고 다른 층의 기능을 끌어올리지 않는다** — 스토어 알림톡 목록이 그렇게 결제
62
+ 기능(3층)을 헤더 우측(1층)으로 올렸다가 되돌렸다.
63
+
64
+ ## 곁을 어디 두나
65
+
66
+ **박스를 두를지 묻기 전에 이것부터 묻는다.** 같은 블록이라도 이 화면 것이냐 아니냐로 규칙이
67
+ 갈린다. 근거는 [`decisions/0022`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0022-screens-declare-their-body.md)에 있다.
68
+
69
+ > 이것은 **이 화면이 선언한 본문**인가?
70
+
71
+ **모든 화면은 만들기 전에 본문을 한 문장으로 선언한다.** 본문은 이 화면이 하는 일이고 **하나다**
72
+ — 둘이면 화면을 쪼갠다. 선언은 승인 화면 문서(`examples/approved/{화면}.md`)가 받는다.
73
+
74
+ **선언에 들지 않은 것은 전부 곁이다.** 다른 기능의 요약이거나 그리로 가는 입구다. 판단이 아니라
75
+ 뺄셈이라, 그릴 때마다 다시 고르지 않는다.
76
+
77
+ ### 손잡이가 둘이다
78
+
79
+ 하나로 보면 자리는 많이 먹으면서 눈에는 안 띄는 블록이 나온다.
80
+
81
+ | 손잡이 | 무엇 | 곁에서 |
82
+ | ------ | ---------------------------------- | ------ |
83
+ | 무게 | 자리를 얼마나 먹고 얼마나 강조되나 | 내린다 |
84
+ | 구분 | 다른 종류로 읽히나 | 낸다 |
85
+
86
+ ### 곁의 규칙 넷
87
+
88
+ 1. **기능 하나에 블록 하나다.** 기능이 다르면 블록도 다르다. 묶는 기준은 기능이지 층이
89
+ 아니다 — 발송 채널과 잔여 알림톡은 둘 다 곁이지만 다른 기능이라 블록이 나뉜다. (구분)
90
+ 2. **본문이 쓰는 pattern을 쓰지 않는다.** 본문이 행 나열이면 곁은 행이 아니다. (구분)
91
+ 3. **강조 예산을 쓰지 않는다.** `primary`·`accent` 채움을 주지 않는다.
92
+ [`DESIGN_PRINCIPLES.md`](DESIGN_PRINCIPLES.md) §4의 "강한 강조는 하나"는 본문이 갖는다. (무게)
93
+ 4. **stroke 없이 shade만.** `surface/raised` 필에 `radius/container`, **보더는 주지 않는다.**
94
+ (구분)
95
+
96
+ **면이 층을 가른다.** 본문은 흰 캔버스 위에서 디바이더로 갈리고 곁은 회색 면에 얹힌다. 글자는
97
+ 양쪽 다 `text/primary`라 대비를 낮추지 않고도 층이 갈린다 — 규칙 1이 "세로로 한 줄이다"였을 때는
98
+ 곁이 쓸 수 있는 것이 텍스트와 `Link`뿐이라 색 말고 가를 것이 없었다. 근거는
99
+ [`decisions/0032`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0032-asides-are-one-filled-block.md)에 있다.
100
+
101
+ | 자리 | 면 | 경계 | 글자 | 대비 |
102
+ | ---- | ---------------- | -------- | -------------- | --------- |
103
+ | 본문 | `surface/base` | 디바이더 | `text/primary` | `12.76:1` |
104
+ | 곁 | `surface/raised` | **없음** | `text/primary` | `11.72:1` |
105
+
106
+ **`ListRow`의 `tone="filled"`를 쓰지 않는다.** 같은 면이지만 본문 pattern이라 규칙 2에 걸린다.
107
+ 곁 블록은 화면이 자기 클래스로 만든다.
108
+
109
+ **무게는 블록 개수가 아니라 줄 수가 지킨다.** 각 블록은 한두 줄을 넘지 않는다. 넘으면 그것은
110
+ 곁이 아니라 본문이거나 다른 화면으로 가야 할 것이다.
111
+
112
+ ### 층이 무게를 정한다
113
+
114
+ **규칙 셋은 "본문보다 가볍게"만 말하고 얼마나 가볍게는 말하지 않는다.** 그리기 전에 **기능
115
+ 위계**를 정한다 — 이 페이지가 하는 일에 얼마나 직접 기여하는가. 근거는
116
+ [`decisions/0031`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0031-function-hierarchy-before-drawing.md)에 있다.
117
+
118
+ ```
119
+ 1. 페이지의 기능·텍스트를 **기능 단위로** 묶는다
120
+ 2. 각 그룹이 페이지 목적에 얼마나 직접 기여하는지로 층을 정한다 ← 여기까지가 위계
121
+ 3. 층이 UI 배정을 정한다 — pattern · 자리 · 면적 · 채움 · 타이포
122
+ 4. 배정된 값이 읽히는지 확인한다 ← 위계와 무관한 하한
123
+ ```
124
+
125
+ 판정은 화면이 선언한 본문(0022)을 기준으로 한다.
126
+
127
+ | 층 | 판정 질문 | pattern | 나가는 자리 |
128
+ | ---------------------- | ------------------------------------- | -------------------- | ---------------------- |
129
+ | **1 · 이 페이지의 일** | 본문 문장이 말하는 그것인가? | 목록·폼·카드 그리드 | 주 액션 `primary` 하나 |
130
+ | **2 · 일의 전제** | 아래 셋 중 하나라도 예인가? | 상태 바 또는 곁 블록 | **`outline` Button** |
131
+ | **3 · 다른 곳의 일** | 참조하거나 그리로 나가는 문일 뿐인가? | 곁 블록 | **`Link`** |
132
+
133
+ **2층과 3층은 셋으로 가른다.** "여기서 하느냐"만 물으면 애매한 자리가 남아서다.
134
+
135
+ | 기준 | 묻는 것 | 발송 채널 | 잔여 알림톡 |
136
+ | ----------- | -------------------------------------------- | ------------------------ | ------------------- |
137
+ | **1. 소유** | 이 기능의 설정 화면이 이 페이지 아래에 있나? | 예 — `/alimtalk/channel` | 아니오 — `/payment` |
138
+ | **2. 변형** | 값이 바뀌면 본문의 **형태**가 바뀌나? | 예 — 템플릿 고르는 법 | 아니오 |
139
+ | **3. 성립** | 없으면 본문이 아예 안 뜨나? | 예 — `EmptyState` | 아니오 |
140
+
141
+ **하나라도 예면 2층, 셋 다 아니면 3층이다.** "잔액이 조건을 잠그지 않나"는 이 축의 물음이
142
+ 아니다 — 잠그는 것은 **나쁜 값일 때**의 일이고 아래 §값이 나쁘면 잠깐 올라온다가 받는다.
143
+
144
+ **2층과 3층은 나가는 자리의 모양으로 갈린다.** 형태를 가진 버튼이 텍스트 링크보다 위다.
145
+
146
+ ```
147
+ primary 채움 > outline 버튼 > subtle 버튼 > 텍스트 링크
148
+ 1층 2층 3층
149
+ ```
150
+
151
+ 곁은 `primary`·`accent` 채움을 쓰지 못하므로 2층이 쓸 수 있는 가장 높은 칸이 `outline`이다.
152
+ **버튼의 보더는 곁 규칙 4와 부딪히지 않는다** — 그 규칙은 블록의 경계에 대한 것이고 버튼은 블록
153
+ 안의 컨트롤이다. 2층이 셋을 넘으면 곁이 본문과 겨루기 시작하므로 한둘로 둔다.
154
+
155
+ **화면이 고르는 것은 층뿐이다.** "얼마나 눈에 띄게 그릴까"를 그리면서 판단하지 않는다. 안 보이면
156
+ 색을 올리고 튀면 내리는 식으로는 화면마다 값이 달라진다.
157
+
158
+ **대비는 위계가 아니다.** 3층도 `text/primary`(12.76:1)를 쓴다 — `text/primary-sub`는 3.62:1로
159
+ 14px 본문의 하한에 미달한다([`ACCESSIBILITY.md`](ACCESSIBILITY.md) §대비). 층 차이는 색이 아니라
160
+ pattern·자리·면적·채움이 낸다.
161
+
162
+ ### 값이 나쁘면 잠깐 올라온다
163
+
164
+ 곁에 실리는 정보는 시점에 따라 값이 다르다. **나쁜 값의 기준은 하나다 — 사용자가 무언가 해야만
165
+ 정상으로 돌아가는가.** 잔액 `0개`, 구독 `만료`, 연동 `실패`가 그것이다.
166
+
167
+ | 값 | 무엇으로 |
168
+ | ------- | -------------------------------------------- |
169
+ | 좋은 값 | 자기 층의 무게 그대로. 대개 곁 한 줄 |
170
+ | 나쁜 값 | `Alert`. 무엇을 해야 하는지 액션을 함께 둔다 |
171
+
172
+ **올라온 것은 화면에 하나다.** 나쁜 값이 둘이면 더 먼저 손대야 하는 것만 올린다. 근거는
173
+ [`decisions/0030`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0030-asides-rise-only-on-bad-values.md)에 있다.
174
+
175
+ > 위계가 자리와 무게를 정하고, 상태는 그 자리에서 잠깐 올라올 뿐 위계를 바꾸지 않는다.
176
+
177
+ "잔액이 0이면 발송이 멈추니 항상 무겁게"는 틀렸다. 잔액이 0이 됐다고 잔액 관리가 이 페이지의
178
+ 주요 기능이 되지 않는다.
179
+
180
+ ### 자리는 시스템이 정하지 않는다
181
+
182
+ 화면이 고르고 선언서에 밝힌다. 규칙 넷을 지키면 위든 아래든 무게는 이미 내려가 있고, 구분은
183
+ 자리가 아니라 문법이 낸다.
184
+
185
+ **곁은 본문보다 먼저 올 수 있다. 넷째 규칙은 없다.** 순서를 규칙으로 세우면 "무게를 내리면 자리는
186
+ 문제가 되지 않는다"는 전제가 무너지고, 곁을 아래로 밀어 두면 규칙 셋을 어겨도 넘어가게 된다.
187
+ 검사가 순서로 막는 것은 헤더 하나뿐이다(`check:examples`). 2026-09-02에 시안 주석이 "규칙 넷 —
188
+ 본문보다 먼저 오지 않는다"로 새어 나간 것을 이 문장으로 되돌렸다.
189
+
190
+ **곁이 위에 있는 것이 문제로 보이면 자리가 아니라 무게를 의심한다.** 한 줄이 아니거나, 본문
191
+ pattern을 쓰고 있거나, 강조를 갖고 있다.
192
+
193
+ **곁은 자리를 요구하지 않는다.** 상태·활성화 바(2단)에 얹는 것은 좋지만 둘을 지킨다.
194
+
195
+ - **곁 때문에 2단을 켜지 않는다.** 2단은 화면 전역을 켜고 끄는 것이 있을 때만 깔린다
196
+ ([`LAYOUT.md`](LAYOUT.md) §콘텐츠 세로 순서).
197
+ - **곁이 2단의 배치를 바꾸지 않는다.** `band--inset`은 `space-between`이라 자리가 양끝 둘뿐이고,
198
+ 전역 토글이 이미 그 둘을 쓴다. 얹을 수 있는 것은 한 자리가 실제로 비어 있을 때뿐이다.
199
+
200
+ 바디 안에 두면 면이 캔버스와 같아지므로 구분을 문법이 전부 져야 한다. 읽는 문구는
201
+ `text/primary-sub`로 물러나고, 갈리는 표시는 `Link`의 밑줄 같은 것이 낸다.
202
+
203
+ ## Card·border·shadow를 쓰는 조건
204
+
205
+ **기본값은 박스가 없는 것이다.** 정보는 여백·디바이더·타이포 위계로 구조화한다. 보더나 카드로
206
+ 모든 것을 감싸지 않는다.
207
+
208
+ ### 판단 한 줄
209
+
210
+ > 이 박스는 사용자가 **고르거나, 열리거나, 떠 있는** 것인가?
211
+
212
+ 아니면 보더와 카드를 빼고 여백 + 디바이더 + 제목으로 만든다.
213
+
214
+ ### 두 번째 질문 — 어느 층에 놓이나
215
+
216
+ 박스를 두르기로 했으면 한 번 더 묻는다. **같은 내용이라도 층이 다르면 다른 것이다.**
217
+
218
+ > 이것은 **페이지 바디 최상위 블록**인가, **섹션 안의 행**인가?
219
+
220
+ | 층 | 무엇 | 안쪽 여백 | 라운드 | 대표 |
221
+ | ------------------ | ------- | -------------- | --------------- | ----------------------- |
222
+ | 페이지 바디 최상위 | 면·패널 | `inset/md`(16) | `container`(16) | `Card` |
223
+ | 섹션 안 | 행·요소 | `inset/md`(16) | `control`(10) | `ListRow tone="filled"` |
224
+
225
+ **층을 가르는 것은 라운드다.** 안쪽 여백은 둘 다 16이라 층을 가르지 못한다 — 행의 상하 여백을
226
+ 16으로 올리면서 같아졌다([`decisions/0019`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0019-list-row-vertical-inset.md)). 값의
227
+ 근거는 [`TOKEN_POLICY.md`](TOKEN_POLICY.md) §라운드 표이고, 여백은 같은 문서 §간격에 있다.
228
+
229
+ **첫 질문은 상호작용만 가르고 층위는 가르지 않는다.** 고르지도 열리지도 떠 있지도 않은 요약
230
+ 블록이라도, 페이지 바디 최상위에서 `Card`와 나란히 서면 면이다.
231
+
232
+ 옆에 무엇이 서는지로 확인한다. 나란한 블록끼리 `border-radius`가 같은 칸이 아니면 층을 잘못 잡은
233
+ 것이다. 눈으로는 6px 차이라 잘 넘어가므로 브라우저에서 계산된 값을 실제로 재본다.
234
+
235
+ ### 쓰는 경우 — 이때만
236
+
237
+ | 자리 | 예 | 표면 |
238
+ | ------------------------------ | -------------------------------------- | ---------------------------------- |
239
+ | 고를 수 있는 개별 객체 | 템플릿 카드, 갤러리 아이템, 옵션 카드 | `surface/base` + 보더 |
240
+ | 페이지 바디 최상위의 요약 블록 | 익스텐션 연결 상태, 계정 요약 | `surface/base` + 보더 (`Card`) |
241
+ | 그중 앞으로 끌어낼 카드 하나 | 지금 해야 할 일을 든 카드 | `surface/raised` (`tone="filled"`) |
242
+ | 상태를 보여야 하는 순간 | 선택됨(accent 보더) · 포커스 · 활성 | 보더 색으로 표현 |
243
+ | 실제로 떠 있는 표면 | 드롭다운 메뉴 · 라이브 프리뷰 · 토스트 | `surface/raised` + `shadow/100` |
244
+ | 스크림 위에 뜨는 표면 | 모달 | `surface/base` + `shadow/100` |
245
+ | 입력 컨트롤 자체 | TextField | 컨트롤 규칙을 따른다 |
246
+
247
+ **카드의 기본 채움은 흰색이다.** 카드가 여럿 선 화면에서 회색 면이 반복되면 전부 똑같이
248
+ 무거워져 어느 것도 앞에 서지 못한다. 그래서 `Card`의 기본은 `tone="plain"`이고, 강조는
249
+ 기본값이 아니라 **한 화면에서 하나를 골라 `tone="filled"`로 올리는 것**이다. 채움만 갈리고
250
+ 보더·라운드·여백은 둘이 같다 — 흰 캔버스 위에서는 보더가 유일한 경계라 `plain`에서도 빼지
251
+ 않는다. `ListRow`의 `tone`과 같은 축이고 같은 뜻이다.
252
+
253
+ ### 세 번째 질문 — 무엇이 눌리나
254
+
255
+ **"액션이 있나"로 가르지 않는다.** 고르는 것도 액션이고 버튼을 품은 것도 액션이라 그 축은 두
256
+ 경우를 한 칸에 넣는다. 갈리는 것은 **무엇이 눌리는가**다.
257
+
258
+ > 이 카드 **자체**를 고르나, 카드 **안의 것**을 누르나?
259
+
260
+ | 이름 | 무엇 | 코드 | 그려지는 것 |
261
+ | --------------- | ------------------------------ | ------------------------- | --------------------------------------------- |
262
+ | **고르는 카드** | 카드 자신이 선택 대상 | `onClick` + `selected` 짝 | `button`, `aria-pressed`, 선택 시 accent 보더 |
263
+ | **담는 카드** | 카드는 면. 누를 것은 안에 둔다 | `onClick` 없음 | `section` |
264
+
265
+ `onClick`은 "누를 수 있다"가 아니라 **"고를 수 있다"**는 뜻이고 `selected` 없이 혼자 쓰지 않는다.
266
+ 주는 순간 `aria-pressed`가 붙어, 고르는 것이 아닌 카드에 잘못된 상태를 알린다.
267
+
268
+ > **눌러서 무언가를 여는 카드는 없다.** 여는 것은 카드 안의 버튼이 한다.
269
+
270
+ 시안이 "카드 전체 클릭"을 요구해도 이 문장을 따른다. 카드는 담는 카드로 두고 안에 `Button`을
271
+ 놓는다.
272
+
273
+ `shadow/100`은 **떠 있는 표면에만** 준다. 고를 수 있는 객체는 보더까지다. 그림자는 "이 면이 다른
274
+ 면 위에 있다"는 뜻이고, 페이지에 붙어 있는 블록은 떠 있지 않다.
275
+
276
+ **읽는 면이 가장 밝다.** 페이지 캔버스(`surface/base`)가 흰색이고, 틀과 조작하는 것
277
+ (`surface/raised` — 사이드바·모달·드롭다운·입력)이 회색으로 물러난다. 값을 강조해 담는 **요약 행은
278
+ 카드가 아니라 옅은 회색 필**(`surface/raised`)이고 보더를 주지 않는다 — `ListRow`의
279
+ `tone="filled"`다. 전체 표는 [`LAYOUT.md`](LAYOUT.md)의 §배경 위계에 있다.
280
+
281
+ **채운 면은 `Card`든 `ListRow`든 같은 회색이다.** `tone="filled"`가 두 곳에서 같은 뜻이고 같은
282
+ 값이며, 갈리는 것은 색이 아니라 층이다 — 여백과 라운드가 다르다
283
+ ([`decisions/0018`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0018-summary-row-shares-the-filled-surface.md)).
284
+
285
+ ### 쓰지 않는 경우 — 기본값
286
+
287
+ | 자리 | 대신 |
288
+ | ------------------------------------------------ | ------------------------------------------------------ |
289
+ | 설정 섹션 묶기 | 제목(bold) + 설명(sub) + 콘텐츠, 섹션 간 `space-8`(32) |
290
+ | 정보 표시 (통계, 상태/남은 일수/기한, 잔여 건수) | 행 나열 + `border/default` 1px 디바이더 |
291
+ | 리스트 (주문완료·발송처리·배송완료) | 디바이더로 구분, 우측에 상태 배지 + chevron |
292
+ | 섹션 안에서 값을 강조해 담는 요약 행 (발송 채널) | `surface/raised` 필, 보더 없음 (`tone="filled"`) |
293
+ | 이미 제목과 여백으로 갈리는 그룹 | 아무것도 더하지 않는다 |
294
+
295
+ ### 절대 규칙
296
+
297
+ **컨테이너 중첩 금지.** 카드 안에 또 보더 박스를 넣지 않는다. 최대 1 depth다. 카드 안의 목록은
298
+ 디바이더로 가른다.
299
+
300
+ **강조는 컨테이너가 아니라 상태·타이포·색으로 만든다.** 정적인 블록을 보더로 둘러 강조하지 않는다.
301
+
302
+ | 강조하려는 것 | 방법 |
303
+ | ------------- | ------------------------------------------------------- |
304
+ | 활성 탭 | 2px 먹색 언더라인 + `text/primary` (`Tab`) |
305
+ | 선택된 카드 | `border/accent` |
306
+ | 상태 | 배지 — 발송 전은 중립(`primary`), 발송 중은 `secondary` |
307
+ | 주요 CTA | 다크 `primary` 풀폭 버튼 |
308
+
309
+ **필터는 탭이다.** 언더라인으로 활성을 표시한다. 채운 칩 박스로 만들지 않는다. 탭은 `Tab`
310
+ 하나이고(0023), `Chip`은 토글과 선택 태그 자리에만 쓴다.
311
+
312
+ **벤토·카드 그리드는 개별 객체 모음에만.** 템플릿 갤러리 같은 자리다. 설정 폼과 정보 패널을
313
+ 벤토로 만들지 않는다.
314
+
315
+ **앱 셸을 쓴다.** 화면은 좌측 사이드바 + 콘텐츠 구성이다. 풀폭 카드 스택으로 만들지 않는다.
316
+
317
+ **배경 위계.** 읽는 면(캔버스·바디)이 가장 밝고 틀과 조작하는 것이 회색으로 물러난다. 정보
318
+ 블록에 불필요한 채움과 보더를 주지 않는다. 표는 [`LAYOUT.md`](LAYOUT.md)에 있다.
319
+
320
+ ### pattern에 어떻게 들어가 있나
321
+
322
+ | pattern | 보더 | 그림자 | 자리 |
323
+ | ------------- | ---- | ------ | --------------------------------------------------------------------- |
324
+ | `FormSection` | 없음 | 없음 | **설정·정보 섹션의 기본**. 제목 + 설명 + 콘텐츠 |
325
+ | `ListRow` | 없음 | 없음 | 행 사이 디바이더만. 박스로 감싸지 않는다 |
326
+ | `Card` | 있음 | 없음 | 고를 수 있는 객체 · 페이지 최상위 요약 블록. 채움은 `tone`(기본 흰색) |
327
+ | `LivePreview` | 있음 | 있음 | 떠 있는 표면 |
328
+ | `BottomBar` | 있음 | 있음 | 화면 아래에 떠 있는 바 |
329
+ | `AppShell` | — | — | 사이드바 + 콘텐츠 틀 |
330
+
331
+ `Card`를 섹션 묶기에 쓰지 않는다. 섹션은 `FormSection`이다. 근거는
332
+ [`decisions/0011`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0011-no-over-carding.md)에 있다.
333
+
334
+ ## 띄우나 마나
335
+
336
+ **깊이와 끊김은 다른 축이다.** 위 §두 번째 질문은 페이지 **안쪽** 층(면인가 행인가)을 가르고,
337
+ 이 절은 페이지 **위로** 뜨는 층을 가른다. 근거는
338
+ [`decisions/0020`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0020-modal-depth-and-scrim.md)에 있다.
339
+
340
+ ### 층
341
+
342
+ | 층 | 무엇 | 표면 | z | 흐름 | 대표 |
343
+ | ------------- | --------------------- | ------------------------------- | --- | -------- | ------------------------------- |
344
+ | 0 페이지 | 캔버스 위 콘텐츠 | `surface/base` | — | 안 끊음 | `AppShell` 바디 |
345
+ | 1 인라인 펼침 | 같은 흐름 안에서 열림 | 상속 | — | 안 끊음 | 접었다 펴는 행 |
346
+ | 2 붙어 뜸 | 트리거에 앵커 | `surface/raised` + `shadow/100` | 1 | 안 끊음 | `DropdownButton` 메뉴·`Tooltip` |
347
+ | 3 모서리에 뜸 | 화면 모서리에 고정 | `surface/raised` + `shadow/100` | 101 | 안 끊음 | `Toast` |
348
+ | 4 스크림 위 | 뒤를 막고 가운데 | `surface/base` + `shadow/100` | 100 | **끊음** | `Modal` |
349
+
350
+ **끊는 층은 4 하나뿐이다.** 나머지는 전부 뒤 화면을 계속 볼 수 있다. 표면 값의 근거는 위
351
+ §쓰는 경우에 있고, 모달만 `surface/base`인 이유는
352
+ [`TOKEN_POLICY.md`](TOKEN_POLICY.md) §표면과 오버레이에 있다.
353
+
354
+ **끊음을 실제로 만드는 것은 스크롤 잠금과 포커스 트랩이다**(`Modal.tsx`). 마지막 열은 성격
355
+ 설명이 아니라 이 둘을 가리킨다. 접근성 요구라 시안 대상이 아니고 디자인 판단으로 뒤집지 않는다.
356
+ 포커스 트랩이 모달 하나를 전제로 감기 때문에, 아래 §겹침의 이중 모달 금지와 뿌리가 같다.
357
+
358
+ **딤은 `overlay/dim`(70% 검정)이다.** 시안에서 온 값은 아니지만 2026-08-31에 이대로 확정했다.
359
+ 옅게 잡으면 두 가지가 같이 무너진다 — 4층을 3층과 가르는 시각 신호가 딤뿐이고, **모달 카드가
360
+ 흰색인 것**([`decisions/0013`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0013-surface-base-is-white.md))**이 짙은 딤 위에서만
361
+ 성립한다.** 카드와 딤 처리된 뒤 캔버스의 대비가 70%에서 8.5:1인데 30%에서는 2.1:1로 떨어져,
362
+ 카드가 떠 있는 것으로 읽히지 않는다. 뒤 본문 대비도 70%에서 2.25:1이라 내용을 읽을 수 없고,
363
+ 그래서 아래 1번 관문의 기준선이 흐려지지 않는다.
364
+
365
+ ### 언제 모달인가 — 세 관문
366
+
367
+ > 1. **뒤 화면을 보면서 할 수 있나?** → 그렇다면 모달이 아니다. 1층(인라인 펼침)
368
+ > 2. **지금 답하지 않으면 다음으로 못 가나?** → 아니라면 모달이 아니다. 3층(`Toast`)·`Alert`
369
+ > 3. **화면을 새로 열 만큼 큰가?** → 그렇다면 모달이 아니다. 새 페이지
370
+
371
+ 셋을 다 통과할 때만 모달이다. 하나라도 걸리면 오른쪽으로 간다.
372
+
373
+ **3번을 가장 자주 놓친다.** 필드가 여러 개인 편집 폼은 모달에 넣지 않는다. 모달 안에서 또 무언가를
374
+ 골라야 하는 순간 이중 모달이 되고, 그건 아래에서 금지한다. 알림톡 템플릿 편집이 모달이 아니라
375
+ 전체 페이지인 이유다.
376
+
377
+ ### 겹침
378
+
379
+ | 규칙 | 값 |
380
+ | ----------------------- | ----------------------------- |
381
+ | 모달 위에 모달 | **금지.** 예외 없다 |
382
+ | 모달 위에 토스트 | **띄운다** |
383
+ | 모달 안의 드롭다운·툴팁 | 모달의 스택 안에서 뜬다 (2층) |
384
+
385
+ **모달 위에 모달을 금지하는 것이 3번 관문을 지탱한다.** 겹칠 수 있으면 "일단 모달에 넣고 안에서
386
+ 또 띄우면 된다"가 되어 3번이 무의미해진다.
387
+
388
+ **z는 토큰으로 올리지 않는다.** 층이 다섯이고 값이 셋(`—`·`1`·`100`·`101`)뿐이라 스케일을 만들
389
+ 만큼이 아니다. 리터럴로 두고 각 CSS 주석이 근거를 든다 — 보더 `1px`과 같은 예외다.
390
+
391
+ `Toast`가 `Modal`보다 1 높은 것은 순서에 기대지 않기 위해서다. 둘을 같은 값으로 두면 DOM 순서로
392
+ 갈리는데, `Modal`은 `body`로 포털하고 `Toaster`는 제자리에 그려서 **동률이면 나중에 붙는 `Modal`이
393
+ 이긴다** — 규칙과 반대다. `Toaster`를 stacking context를 만드는 조상 안에 두면 이 값도 듣지
394
+ 않으므로, 앱 최상위에 하나만 둔다.
395
+
396
+ ## 어느 pattern을 고르나
397
+
398
+ **하려는 일에서 고른다. 생김새가 비슷하다고 고르지 않는다.** 아래 오른쪽 열은 같은 자리에서
399
+ 잘못 고르기 쉬운 것이다.
400
+
401
+ | 하려는 것 | pattern | 여기에 쓰지 않는 것 |
402
+ | ----------------------------------- | --------------------------- | --------------------------------- |
403
+ | 화면의 틀 (사이드바 + 콘텐츠 3단) | `AppShell` | — |
404
+ | 설정·정보 섹션을 제목 아래로 묶기 | `FormSection` | `Card` |
405
+ | 정보 나열, 통계, 목록의 한 줄 | `ListRow` + 디바이더 | `Card` |
406
+ | 그 목록이 화면의 본문일 때 | `ListRow emphasis="strong"` | 행마다 크기를 따로 고르는 것 |
407
+ | 섹션 안의 요약 행 | `ListRow tone="filled"` | `Card` (흰 카드가 아니라 회색 필) |
408
+ | 페이지 바디 최상위의 요약 블록 | `Card` | `ListRow tone="filled"` |
409
+ | 고를 수 있는 개별 객체 | `Card` | — |
410
+ | 같은 자리에서 갈래를 갈아 끼우기 | `Tab` | `Chip` |
411
+ | 여러 개를 동시에 거는 토글 | `Chip` | `Tab` |
412
+ | 폼 옆에서 입력 결과를 그대로 비추기 | `LivePreview` | — |
413
+ | 페이지 전체의 액션 | `BottomBar` | — |
414
+ | 목록·검색 결과가 비었을 때 | `EmptyState` | — |
415
+
416
+ **`emphasis="strong"`과 `tone="filled"`를 겹쳐 쓰지 않는다.** 곁의 요약 행이 본문 크기를 갖는
417
+ 일이고 [`0032`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0032-asides-are-one-filled-block.md)와 어긋난다. 한 화면에 `strong`
418
+ 목록은 하나다 ([`0034`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0034-the-body-list-lifts-one-step.md)).
419
+
420
+ `Card`를 섹션 묶기에 쓰지 않는 근거는 [`decisions/0011`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0011-no-over-carding.md)에 있다.
421
+ `Tab`과 `Chip`이 갈리는 이유는 위 §절대 규칙의 "필터는 탭이다"와 같다 — 필터는 고르는 객체가
422
+ 아니라 갈래 전환이라 선택된 카드와 같은 문법을 쓰지 않는다.
423
+
424
+ ### 탭은 하나다
425
+
426
+ **한때 둘이었다.** `Tab`(컴포넌트)과 `FilterTabs`(pattern)가 "내용이 바뀌나, 같은 내용이
427
+ 좁혀지나"로 갈렸는데 그 구분을 없앴다 — Figma 원본에 탭 컴포넌트 세트가 하나뿐이고, 두 자리는
428
+ 실제로 같이 쓰인다. 어느 값이 남았는지와 근거는
429
+ [`decisions/0023`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0023-one-tab.md)에 있다.
430
+
431
+ | 무엇 | 값 |
432
+ | ---------- | --------------------------------------------------------- |
433
+ | 활성 표시 | 2px 먹색 밑줄 (`primary/default`) |
434
+ | 안 고른 것 | `text/primary-sub` |
435
+ | hover | `state/hover-pressed` 채움 |
436
+ | 높이 | 48px 고정 |
437
+ | 주는 단위 | 탭 **하나**. 줄(`role="tablist"`)과 키보드 이동은 쓰는 쪽 |
438
+
439
+ **줄을 시스템이 주지 않는다.** `Tab`은 항목 하나짜리라 `role="tablist"` 컨테이너와 화살표 키
440
+ 이동(roving tabindex), 패널 연결(`aria-controls`)은 쓰는 쪽이 만든다. 줄 전체를 받치는 1px 선도
441
+ 그 컨테이너가 갖는다.
442
+
443
+ **갈래를 제목으로 세우지 않는다.** 같은 종류가 여러 갈래로 나뉜 목록은 제목을 갈래 수만큼 세우는
444
+ 대신 탭이 가른다. 서로 다른 종류의 섹션은 그대로 제목과 여백으로 가른다(위 §쓰지 않는 경우).
445
+
446
+ ## 상태는 누가 갖나
447
+
448
+ **틀은 상태를 갖지 않는다. 늘 그려지고, 상태는 안에 놓인 것이 가진다.** `AppShell`과 `Card`에
449
+ loading·empty·error를 두지 않는 이유다. 틀에 상태를 두면 같은 빈 화면을 틀과 내용이 각자
450
+ 그리게 되고, 어느 쪽이 이겼는지가 중첩 순서로 정해진다.
451
+
452
+ | pattern | 갖는 상태 | 맡기는 곳 |
453
+ | ------------- | --------------------------------- | ------------------------------------------------------------------- |
454
+ | `AppShell` | 없음 | `children` |
455
+ | `Card` | 없음 | `children` |
456
+ | `ListRow` | 행 하나의 선택과 상태(`Badge`) | 목록 전체의 empty·error는 `EmptyState` |
457
+ | `FormSection` | 섹션 전체의 실패 (`role="alert"`) | 행 하나의 실패는 그 행의 `Badge`, 필드 하나는 `TextField`의 `error` |
458
+ | `BottomBar` | 없음 | 진행 중 표시는 안에 놓인 `Button`의 `disabled` |
459
+ | `LivePreview` | ready·loading·empty·error 네 가지 | — |
460
+ | `EmptyState` | 비어 있음 자체 | — |
461
+
462
+ `LivePreview`가 네 상태를 다 갖는 것은 그것이 틀이 아니라 **내용을 그리는 자리**이기 때문이다.
463
+
464
+ **상태는 그것을 가진 것에 가장 가까운 자리가 든다.** 한 단계 위로 올리는 것은 그 단계 전체가 못
465
+ 쓰게 되었을 때뿐이다. 행 하나가 실패한 것을 섹션 배너로 올리면 같은 사실을 두 번 말하게 되고,
466
+ 면적이 큰 배너가 본문보다 강해져 정작 봐야 할 목록을 덮는다. 근거는
467
+ [`decisions/0024`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0024-state-belongs-to-the-nearest-owner.md)에 있다.
468
+
469
+ 위 표는 pattern에서 **누가 소유하는가**만 정한다. 소유가 정해진 상태를 컴포넌트 하나 안에서
470
+ 어떻게 표현하는지는 [`decisions/0004`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0004-component-state-naming.md)의 5분류를
471
+ 따른다 — 그중 prop으로 빼는 것은 "부모가 정함"(`selected`·`error`) 하나뿐이고, 나머지는 CSS
472
+ 의사클래스·네이티브 속성·값에서 파생·내부 state다.
473
+
474
+ ## 작성할 내용
475
+
476
+ - CTA 배치와 강조 우선순위
477
+ - 승인 화면과 anti-pattern 링크
478
+
479
+ 페이지와 section의 기본 구조, 여백과 밀도의 기준은 [`LAYOUT.md`](LAYOUT.md)가 갖는다 — §셸과
480
+ §토큰이다. 여기서 다시 쓰지 않는다.