@highpixel-co/palda-design-system 0.5.0 → 0.8.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.
- package/dist/{chunk-Q4UXASZH.js → chunk-3OXUONDZ.js} +34 -10
- package/dist/{chunk-GPCPGABI.js → chunk-PWAQFOM3.js} +113 -42
- package/dist/guide/README.md +3 -3
- package/dist/guide/catalog/components.yml +71 -4
- package/dist/guide/catalog/drafts.yml +46 -0
- package/dist/guide/catalog/patterns.yml +20 -5
- package/dist/guide/docs/ACCESSIBILITY.md +27 -1
- package/dist/guide/docs/AI_UI_DESIGNER_HANDOFF.md +6 -4
- package/dist/guide/docs/COMPONENT_POLICY.md +2 -2
- package/dist/guide/docs/CONTENT.md +1 -1
- package/dist/guide/docs/DESIGN_GRAMMAR.md +239 -37
- package/dist/guide/docs/DESIGN_PRINCIPLES.md +18 -9
- package/dist/guide/docs/FIGMA_ALIGNMENT_DELTA.md +4 -4
- package/dist/guide/docs/FIGMA_NAME_MAPPING.md +4 -4
- package/dist/guide/docs/FIGMA_WORKFLOW.md +3 -3
- package/dist/guide/docs/ICON_POLICY.md +3 -3
- package/dist/guide/docs/LAYOUT.md +17 -11
- package/dist/guide/docs/TOKEN_POLICY.md +27 -16
- package/dist/harness/check.mjs +13 -7
- package/dist/harness/metadata.json +38 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -2
- package/dist/patterns.d.ts +27 -3
- package/dist/patterns.js +2 -2
- package/dist/scripts/check-examples.mjs +262 -57
- package/dist/styles.css +105 -35
- package/dist/ui.d.ts +73 -6
- package/dist/ui.js +5 -1
- package/package.json +1 -1
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* (`docs/DESIGN_GRAMMAR.md` §자리는 시스템이 정하지 않는다).
|
|
32
32
|
*/
|
|
33
33
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
34
|
-
import { dirname, join, relative, resolve } from 'node:path';
|
|
34
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
35
35
|
import { fileURLToPath } from 'node:url';
|
|
36
36
|
|
|
37
37
|
const DEFAULT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -41,7 +41,13 @@ const ROW = /^\|([^|]*)\|([^|]*)\|/;
|
|
|
41
41
|
/** 표의 구분선(`| --- | --- |`)은 항목이 아니다. */
|
|
42
42
|
const DIVIDER = /^[\s:-]*$/;
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* 주 액션의 자리. `없음`은 고르는 행위가 본문 안에서 끝나는 화면이다 — 목록을 훑고 행을 눌러
|
|
46
|
+
* 들어가는 화면은 행이 곧 액션이라 헤더나 하단에 세울 것이 없다
|
|
47
|
+
* (`docs/decisions/0033-a-screen-may-have-no-primary-action.md`). 행 자체는 여전히 필수라
|
|
48
|
+
* "적기를 잊은 것"과 "정말 없는 것"이 갈린다.
|
|
49
|
+
*/
|
|
50
|
+
const PRIMARY_ACTION_PLACES = new Set(['헤더 우측', 'BottomBar', '없음']);
|
|
45
51
|
|
|
46
52
|
/** 시안을 찾는 곳. `preview/`는 untracked라 CI에서는 비어 있고 로컬에서만 걸린다. */
|
|
47
53
|
const SCREEN_DIRS = ['preview', 'examples'];
|
|
@@ -79,41 +85,6 @@ const EMPHASIS_CLASSES = [
|
|
|
79
85
|
'palda-chip--accent',
|
|
80
86
|
];
|
|
81
87
|
|
|
82
|
-
/**
|
|
83
|
-
* 곁이 세로로 한 줄인지(0022 규칙 1)를 정적으로 근사한다. 블록 요소가 하나라도 들어 있으면
|
|
84
|
-
* 줄이 나뉜다 — 곁에 남는 표현은 텍스트와 `Link`라(0022 §대가) 인라인만으로 충분하다.
|
|
85
|
-
* 줄바꿈 자체는 폭에 달려 있어 여전히 못 읽는다. 이 검사가 잡는 것은 마크업으로 이미 갈라 둔
|
|
86
|
-
* 경우다.
|
|
87
|
-
*/
|
|
88
|
-
const BLOCK_ELEMENTS = new Set([
|
|
89
|
-
'address',
|
|
90
|
-
'article',
|
|
91
|
-
'aside',
|
|
92
|
-
'blockquote',
|
|
93
|
-
'div',
|
|
94
|
-
'dl',
|
|
95
|
-
'fieldset',
|
|
96
|
-
'figure',
|
|
97
|
-
'footer',
|
|
98
|
-
'form',
|
|
99
|
-
'h1',
|
|
100
|
-
'h2',
|
|
101
|
-
'h3',
|
|
102
|
-
'h4',
|
|
103
|
-
'h5',
|
|
104
|
-
'h6',
|
|
105
|
-
'header',
|
|
106
|
-
'hr',
|
|
107
|
-
'li',
|
|
108
|
-
'main',
|
|
109
|
-
'nav',
|
|
110
|
-
'ol',
|
|
111
|
-
'p',
|
|
112
|
-
'section',
|
|
113
|
-
'table',
|
|
114
|
-
'ul',
|
|
115
|
-
]);
|
|
116
|
-
|
|
117
88
|
/**
|
|
118
89
|
* 상태를 드는 자리 (0024 — 상태는 가장 가까운 자리가 든다).
|
|
119
90
|
* 한 섹션이 둘을 같이 쓰면 행 하나의 실패를 섹션이 대신 말한 것이다. `\b`가 `_`를 경계로 보지
|
|
@@ -123,6 +94,46 @@ const FORM_SECTION_CLASS = 'palda-form-section';
|
|
|
123
94
|
const SECTION_FAILURE_CLASS = 'palda-form-section__error';
|
|
124
95
|
const ROW_FAILURE_CLASS = 'palda-badge--danger';
|
|
125
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 본문을 여러 묶음으로 가르는 축 (`decisions/0023`·`0037`). 선언서가 밝히고 마크업이 따라온다.
|
|
99
|
+
*
|
|
100
|
+
* 같은 종류가 여러 갈래로 나뉘면 `탭`이고, 서로 다른 종류가 나열되면 `섹션`이다. 판정은 사람이
|
|
101
|
+
* 하지만 **고른 결과와 마크업이 어긋나는 것**은 셀 수 있다 — 갈래를 제목 여러 개로 세워 놓고
|
|
102
|
+
* 탭이라 적거나, 탭을 그려 놓고 섹션이라 적는 경우다.
|
|
103
|
+
*/
|
|
104
|
+
const GROUPING_LABEL = '갈래';
|
|
105
|
+
const GROUPING_VALUES = ['탭', '섹션', '탭+섹션', '없음'];
|
|
106
|
+
const TABLIST = /role\s*=\s*"tablist"/;
|
|
107
|
+
/** 섹션 하나를 여는 자리. `--plain`·`--filled`가 붙어도 한 번만 센다. */
|
|
108
|
+
const SECTION_OPEN = /class="palda-form-section[\s"]/g;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 섹션의 채움 (`decisions/0037`). 면은 갈라야 할 형제가 있을 때만 켜고, 면 위에 다른 면을
|
|
112
|
+
* 얹지 않는다. 요약 행(`ListRow tone="filled"`)은 컴포넌트가 스스로 한 단계 올리므로 여기서
|
|
113
|
+
* 세지 않는다 — 세는 것은 **면이 스스로 못 피하는 겹침**뿐이다.
|
|
114
|
+
*/
|
|
115
|
+
const SECTION_FILLED_CLASS = 'palda-form-section--filled';
|
|
116
|
+
const NESTED_SURFACE_CLASS = 'palda-card';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 면을 켜야 하는데 안 켠 자리 (`decisions/0037`).
|
|
120
|
+
*
|
|
121
|
+
* 0037은 "제목과 여백만으로 안 갈릴 때"를 조건으로 두고, §적용에서 그 갈림을 못 센다고 남겨
|
|
122
|
+
* 뒀다. 몇 px 차이부터 안 갈리는지 수치가 없어서다. **다만 차이가 0인 경우는 셀 수 있다** —
|
|
123
|
+
* 섹션 제목과 그 안 행 제목이 같은 텍스트 스타일 클래스를 그대로 쓰고 있으면, 갈릴 여지가
|
|
124
|
+
* 애초에 없다.
|
|
125
|
+
*
|
|
126
|
+
* 0034를 적용해 본문 목록을 한 칸 올린 화면(`emphasis="strong"` → 제목 `body1`)이 전부 이
|
|
127
|
+
* 상태가 된다. 섹션 제목도 `body1`이라 두 글자가 문자열까지 같아진다. 2026-09-06 연동 도우미
|
|
128
|
+
* 시안이 그렇게 나왔고, 0037이 예로 든 내 계정 화면(16 대 14)보다 심한 판이라 승격했다.
|
|
129
|
+
*
|
|
130
|
+
* 2px 차이는 여전히 못 센다. 그것은 리뷰가 본다.
|
|
131
|
+
*/
|
|
132
|
+
const TYPOGRAPHY_CLASS = /\bpalda-text-[a-z0-9-]+\b/;
|
|
133
|
+
const SECTION_HEADING_CLASS = /palda-form-section__header[\s\S]*?<h2[^>]*\bclass="([^"]*)"/;
|
|
134
|
+
const SECTION_FIELDS_CLASS = 'palda-form-section__fields';
|
|
135
|
+
const SECTION_FOOTER_CLASS = 'palda-form-section__footer';
|
|
136
|
+
|
|
126
137
|
/** 닫는 태그가 없는 요소. 깊이를 올리지 않는다. */
|
|
127
138
|
const VOID_ELEMENTS = new Set([
|
|
128
139
|
'area',
|
|
@@ -173,18 +184,36 @@ function checkDeclaration(rows, where, problems) {
|
|
|
173
184
|
problems.push(`${where}: 주 액션이 ${primary.length}개다 — 자리는 하나다`);
|
|
174
185
|
} else if (!PRIMARY_ACTION_PLACES.has(primary[0].value)) {
|
|
175
186
|
problems.push(
|
|
176
|
-
`${where}: 주 액션 자리가 \`${primary[0].value}
|
|
187
|
+
`${where}: 주 액션 자리가 \`${primary[0].value}\`이다 — ${[...PRIMARY_ACTION_PLACES]
|
|
177
188
|
.map((place) => `\`${place}\``)
|
|
178
189
|
.join(' 또는 ')} 중 하나여야 한다`,
|
|
179
190
|
);
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
/** 본문을 무엇으로 가르는지. 적는 행위가 탭이냐 섹션이냐의 판단을 남긴다 (0023). */
|
|
194
|
+
const grouping = rows.filter((row) => row.label === GROUPING_LABEL);
|
|
195
|
+
if (grouping.length === 0) {
|
|
196
|
+
problems.push(
|
|
197
|
+
`${where}: 선언서에 \`${GROUPING_LABEL}\` 행이 없다 — ${GROUPING_VALUES.map((value) => `\`${value}\``).join('·')} 중 하나를 밝힌다`,
|
|
198
|
+
);
|
|
199
|
+
} else if (grouping.length > 1) {
|
|
200
|
+
problems.push(`${where}: ${GROUPING_LABEL}가 ${grouping.length}개다 — 축은 하나다`);
|
|
201
|
+
} else if (!GROUPING_VALUES.includes(grouping[0].value)) {
|
|
202
|
+
problems.push(
|
|
203
|
+
`${where}: ${GROUPING_LABEL}가 \`${grouping[0].value}\`이다 — ${GROUPING_VALUES.map((value) => `\`${value}\``).join('·')} 중 하나여야 한다`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
182
207
|
/** `곁` 행에 `없음`이라고 적으면 곁이 0개라는 뜻이다. */
|
|
183
208
|
const asides = rows
|
|
184
209
|
.filter((row) => row.label === '곁')
|
|
185
210
|
.filter((row) => row.value !== '' && row.value !== '없음');
|
|
186
211
|
|
|
187
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
declared: body.length > 0,
|
|
214
|
+
asides: asides.length,
|
|
215
|
+
grouping: grouping.length === 1 ? grouping[0].value : null,
|
|
216
|
+
};
|
|
188
217
|
}
|
|
189
218
|
|
|
190
219
|
/** 주석을 같은 길이의 공백으로 지운다. 주석 안의 예시 마크업이 태그로 세지지 않게 한다. */
|
|
@@ -299,7 +328,7 @@ function checkBlocks(blocks, declaredAsides, where, problems) {
|
|
|
299
328
|
}
|
|
300
329
|
if (!ROLES.includes(role)) {
|
|
301
330
|
problems.push(
|
|
302
|
-
`${where}: 바디 ${index + 1}번째 블록의 data-screen-role이 \`${role}
|
|
331
|
+
`${where}: 바디 ${index + 1}번째 블록의 data-screen-role이 \`${role}\`이다 — ${ROLES.map((name) => `\`${name}\``).join('·')} 중 하나여야 한다`,
|
|
303
332
|
);
|
|
304
333
|
roles.push(null);
|
|
305
334
|
continue;
|
|
@@ -325,16 +354,6 @@ function checkBlocks(blocks, declaredAsides, where, problems) {
|
|
|
325
354
|
);
|
|
326
355
|
}
|
|
327
356
|
}
|
|
328
|
-
|
|
329
|
-
// 규칙 1 — 세로로 한 줄이다. 블록 요소가 들어 있으면 이미 여러 줄이다.
|
|
330
|
-
const inner = scanTags(block.html)
|
|
331
|
-
.slice(1)
|
|
332
|
-
.filter((tag) => !tag.closing && BLOCK_ELEMENTS.has(tag.name));
|
|
333
|
-
if (inner.length > 0) {
|
|
334
|
-
problems.push(
|
|
335
|
-
`${where}: 곁 안에 블록 요소(<${inner[0].name}>)가 있다 — 곁은 세로로 한 줄이다 (0022 규칙 1)`,
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
357
|
}
|
|
339
358
|
|
|
340
359
|
// 상태는 가장 가까운 자리가 든다 (0024). 소속과 무관하게 바디 전체를 본다.
|
|
@@ -368,7 +387,8 @@ function checkBlocks(blocks, declaredAsides, where, problems) {
|
|
|
368
387
|
);
|
|
369
388
|
}
|
|
370
389
|
|
|
371
|
-
// 선언되지 않은 곁은 화면에 들어오지 못한다.
|
|
390
|
+
// 선언되지 않은 곁은 화면에 들어오지 못한다. 곁이 몇 개여야 하는지는 세지 않는다 — 기능이
|
|
391
|
+
// 다르면 블록도 다르고(0032 규칙 1), 개수는 기능 수가 정한다.
|
|
372
392
|
const drawn = roles.filter((role) => role === '곁').length;
|
|
373
393
|
if (drawn !== declaredAsides) {
|
|
374
394
|
problems.push(
|
|
@@ -386,7 +406,169 @@ function htmlFiles(dir, out = []) {
|
|
|
386
406
|
return out;
|
|
387
407
|
}
|
|
388
408
|
|
|
389
|
-
|
|
409
|
+
/**
|
|
410
|
+
* 화면의 섹션 이름. `FormSection`의 헤더에 있는 `h2`가 그것이다 — 화면이 무엇으로 나뉘는지를
|
|
411
|
+
* 사람이 읽는 자리와 같다. 주석 안의 예시 마크업은 세지 않으므로 주석을 먼저 지운다.
|
|
412
|
+
*/
|
|
413
|
+
const SECTION_HEADING = /palda-form-section__header[\s\S]*?<h2[^>]*>([^<]*)<\/h2>/g;
|
|
414
|
+
|
|
415
|
+
/** 뒤에 붙는 개수 표기는 값이라 이름이 아니다 — `상품 템플릿 3/10`과 `상품 템플릿`은 같다. */
|
|
416
|
+
const stripCount = (name) => name.replace(/\s*\d+\s*\/\s*\d+\s*$/, '').trim();
|
|
417
|
+
|
|
418
|
+
function screenSections(source) {
|
|
419
|
+
return [...stripComments(source).matchAll(SECTION_HEADING)]
|
|
420
|
+
.map((match) => stripCount(match[1]))
|
|
421
|
+
.filter(Boolean);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* 선언 주석에 등록한 자매와의 차이. `1. [섹션]` 또는 `1. [이쪽] ↔ [저쪽]` 형태다.
|
|
426
|
+
* 적는 행위가 판단을 남긴다 — 왜 갈라졌는지가 그 자리에 남아 다음에 다시 판단하지 않는다.
|
|
427
|
+
*/
|
|
428
|
+
const REGISTERED_DIFF = /^\s*\d+\.\s*\[([^\]]+)\](?:\s*↔\s*\[([^\]]+)\])?/gm;
|
|
429
|
+
|
|
430
|
+
function registeredDiffs(source) {
|
|
431
|
+
return new Set(
|
|
432
|
+
[...source.matchAll(REGISTERED_DIFF)].flatMap((match) =>
|
|
433
|
+
[match[1], match[2]].filter(Boolean).map(stripCount),
|
|
434
|
+
),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const siblingOf = (rows) => rows.find((row) => row.label === '자매')?.value || null;
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 자매로 선언한 두 화면의 섹션 집합을 비교한다 (`decisions/0036`).
|
|
442
|
+
* 차이를 금지하지 않고 **적지 않은 차이**를 막는다. 문자열 집합 비교라 판단이 들어가지 않는다.
|
|
443
|
+
*/
|
|
444
|
+
function checkSiblings(source, where, rows, siblings, problems) {
|
|
445
|
+
const name = siblingOf(rows);
|
|
446
|
+
if (!name) return;
|
|
447
|
+
|
|
448
|
+
const mate = siblings.get(name);
|
|
449
|
+
if (mate === undefined) {
|
|
450
|
+
problems.push(`${where}: 자매로 적은 \`${name}\`을 찾을 수 없다 — 같은 시안 디렉터리에 둔다`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 짝이 맞는지는 상대의 `자매` 행이 나를 가리키는지다. 한쪽만 적으면 반대편이 모른다.
|
|
455
|
+
const self = basename(where, '.html');
|
|
456
|
+
const back = siblingOf(declarationRows(mate));
|
|
457
|
+
if (back !== self) {
|
|
458
|
+
problems.push(
|
|
459
|
+
`${where}: 자매 선언이 짝이 안 맞는다 — \`${name}\`의 자매는 \`${back ?? '없음'}\`이다. 양쪽 다 적는다`,
|
|
460
|
+
);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const theirs = new Set(screenSections(mate));
|
|
465
|
+
const registered = registeredDiffs(source);
|
|
466
|
+
for (const section of screenSections(source)) {
|
|
467
|
+
if (theirs.has(section) || registered.has(section)) continue;
|
|
468
|
+
problems.push(
|
|
469
|
+
`${where}: 섹션 [${section}]이 자매(${name})에 없는데 선언에 등록되지 않았다 — 차이는 \`1. [${section}]\` 형태로 적는다`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* 선언한 갈래와 마크업이 맞는지 (0023). 탭 줄은 `role="tablist"`로, 섹션 나열은 형제
|
|
476
|
+
* `FormSection` 둘 이상으로 읽는다. 섹션 하나는 나누는 축이 아니라 그냥 본문이다.
|
|
477
|
+
*/
|
|
478
|
+
function checkGrouping(blocks, declared, where, problems) {
|
|
479
|
+
if (!declared) return;
|
|
480
|
+
|
|
481
|
+
const html = blocks.map((block) => block.html).join('\n');
|
|
482
|
+
const drawnTabs = TABLIST.test(html);
|
|
483
|
+
const drawnSections = (html.match(SECTION_OPEN) ?? []).length >= 2;
|
|
484
|
+
const expectTabs = declared.includes('탭');
|
|
485
|
+
const expectSections = declared.includes('섹션');
|
|
486
|
+
|
|
487
|
+
if (expectTabs && !drawnTabs) {
|
|
488
|
+
problems.push(
|
|
489
|
+
`${where}: ${GROUPING_LABEL}를 \`${declared}\`로 적었는데 본문에 \`role="tablist"\`가 없다 — 탭 줄은 쓰는 쪽이 만든다 (0023)`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
if (!expectTabs && drawnTabs) {
|
|
493
|
+
problems.push(
|
|
494
|
+
`${where}: 본문이 탭으로 갈리는데 ${GROUPING_LABEL}가 \`${declared}\`이다 — 같은 종류의 갈래를 탭이 가른다면 그렇게 적는다`,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (expectSections && !drawnSections) {
|
|
498
|
+
problems.push(
|
|
499
|
+
`${where}: ${GROUPING_LABEL}를 \`${declared}\`로 적었는데 형제 섹션이 둘 미만이다 — 섹션 하나는 나누는 축이 아니다`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (!expectSections && drawnSections) {
|
|
503
|
+
problems.push(
|
|
504
|
+
`${where}: 본문이 섹션 여럿으로 나뉘는데 ${GROUPING_LABEL}가 \`${declared}\`이다 — 같은 종류의 갈래라면 제목을 여러 개 세우는 대신 탭으로 가른다 (0023)`,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* 섹션의 내용 자리. `__fields`부터 `__footer` 앞까지다 — 면이 붙는 자리가 거기 하나이고,
|
|
511
|
+
* 제목·오류·footer는 면 밖에 남는다 (0037).
|
|
512
|
+
*/
|
|
513
|
+
function sectionFields(section) {
|
|
514
|
+
const start = section.indexOf(SECTION_FIELDS_CLASS);
|
|
515
|
+
if (start === -1) return '';
|
|
516
|
+
const footer = section.indexOf(SECTION_FOOTER_CLASS, start);
|
|
517
|
+
return section.slice(start, footer === -1 ? undefined : footer);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** 섹션의 채움이 제 조건에서 켜졌는지 (0037). */
|
|
521
|
+
function checkSectionFill(blocks, where, problems) {
|
|
522
|
+
const html = blocks.map((block) => block.html).join('\n');
|
|
523
|
+
const sections = formSections(html);
|
|
524
|
+
const filled = sections.filter((section) =>
|
|
525
|
+
new RegExp(`\\b${SECTION_FILLED_CLASS}\\b`).test(section.slice(0, section.indexOf('>') + 1)),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// 켜야 하는데 안 켠 자리. 형제가 있고, 제목과 내용의 텍스트 스타일이 문자열까지 같으면
|
|
529
|
+
// 여백 말고 갈릴 것이 없다. 형제가 하나뿐이면 애초에 켜지 못하므로(아래) 세지 않는다.
|
|
530
|
+
if (sections.length >= 2) {
|
|
531
|
+
for (const section of sections) {
|
|
532
|
+
const head = section.slice(0, section.indexOf('>') + 1);
|
|
533
|
+
if (new RegExp(`\\b${SECTION_FILLED_CLASS}\\b`).test(head)) continue;
|
|
534
|
+
|
|
535
|
+
const heading = SECTION_HEADING_CLASS.exec(section)?.[1] ?? '';
|
|
536
|
+
const style = TYPOGRAPHY_CLASS.exec(heading)?.[0];
|
|
537
|
+
if (!style) continue;
|
|
538
|
+
|
|
539
|
+
const fields = sectionFields(section);
|
|
540
|
+
// 내용이 이미 덩어리면 면이 할 일이 없다. 카드 그리드가 그 자리다 (0037) — 아래에서
|
|
541
|
+
// 면을 켜지 못하게 막는 것과 같은 표시를 쓴다. 결제의 `요금제`·`구매 수량`이 그렇다.
|
|
542
|
+
if (new RegExp(`\\b${NESTED_SURFACE_CLASS}\\b`).test(fields)) continue;
|
|
543
|
+
|
|
544
|
+
if (new RegExp(`\\b${style}\\b`).test(fields)) {
|
|
545
|
+
problems.push(
|
|
546
|
+
`${where}: 섹션 제목과 그 내용이 같은 \`${style}\`인데 면이 없다 — 제목과 여백만으로 갈리지 않는다 (0037)`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (filled.length === 0) return;
|
|
553
|
+
|
|
554
|
+
// 면은 형제를 가르는 장치다. 혼자면 가를 것이 없고 곁만 본문에 흡수된다.
|
|
555
|
+
if (sections.length < 2) {
|
|
556
|
+
problems.push(
|
|
557
|
+
`${where}: 섹션이 하나인데 \`${SECTION_FILLED_CLASS}\`이다 — 면은 갈라야 할 형제가 있을 때 켠다 (0037)`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 내용이 이미 덩어리면 면이 한 겹 겹친다. 카드 그리드가 그 자리다.
|
|
562
|
+
for (const section of filled) {
|
|
563
|
+
if (new RegExp(`\\b${NESTED_SURFACE_CLASS}\\b`).test(section)) {
|
|
564
|
+
problems.push(
|
|
565
|
+
`${where}: \`${SECTION_FILLED_CLASS}\` 안에 \`${NESTED_SURFACE_CLASS}\`가 있다 — 내용이 이미 덩어리면 면을 주지 않는다 (0037)`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function validateScreenHtml(source, where, requireScreen = false, siblings = null) {
|
|
390
572
|
const problems = [];
|
|
391
573
|
const blocks = bodyBlocks(source);
|
|
392
574
|
if (blocks === null) {
|
|
@@ -398,11 +580,25 @@ export function validateScreenHtml(source, where, requireScreen = false) {
|
|
|
398
580
|
problems.push(`${where}: 화면 선언 표가 없다 — 파일 맨 위 주석에 둔다`);
|
|
399
581
|
return problems;
|
|
400
582
|
}
|
|
401
|
-
const { asides } = checkDeclaration(rows, where, problems);
|
|
583
|
+
const { asides, grouping } = checkDeclaration(rows, where, problems);
|
|
402
584
|
checkBlocks(blocks, asides, where, problems);
|
|
585
|
+
checkGrouping(blocks, grouping, where, problems);
|
|
586
|
+
checkSectionFill(blocks, where, problems);
|
|
587
|
+
// 자매는 짝이 있어야 셀 수 있다. 파일 하나만 주면 이 검사는 건너뛴다.
|
|
588
|
+
if (siblings) checkSiblings(source, where, rows, siblings, problems);
|
|
403
589
|
return problems;
|
|
404
590
|
}
|
|
405
591
|
|
|
592
|
+
/**
|
|
593
|
+
* 자매를 찾는 색인. 키는 확장자를 뺀 파일 이름이고, 선언서가 그 이름으로 상대를 가리킨다.
|
|
594
|
+
* 이름이 겹치면 먼저 읽은 쪽이 남는다 — 시안 이름은 화면 하나를 가리키므로 겹치지 않는다.
|
|
595
|
+
*/
|
|
596
|
+
export function siblingIndex(entries) {
|
|
597
|
+
const index = new Map();
|
|
598
|
+
for (const { name, source } of entries) if (!index.has(name)) index.set(name, source);
|
|
599
|
+
return index;
|
|
600
|
+
}
|
|
601
|
+
|
|
406
602
|
export function validateExamples(root = DEFAULT_ROOT) {
|
|
407
603
|
const problems = [];
|
|
408
604
|
|
|
@@ -416,18 +612,27 @@ export function validateExamples(root = DEFAULT_ROOT) {
|
|
|
416
612
|
}
|
|
417
613
|
}
|
|
418
614
|
|
|
419
|
-
// 2. 시안 HTML — 선언서 + 바디 블록의
|
|
615
|
+
// 2. 시안 HTML — 선언서 + 바디 블록의 소속 + 자매의 섹션 집합.
|
|
616
|
+
// 자매는 짝이 필요해서 전부 읽어 색인을 만든 뒤에 검사한다.
|
|
617
|
+
const screens = [];
|
|
420
618
|
for (const dir of SCREEN_DIRS) {
|
|
421
619
|
const full = join(root, dir);
|
|
422
620
|
if (!existsSync(full)) continue;
|
|
423
621
|
|
|
424
622
|
for (const file of htmlFiles(full)) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
623
|
+
screens.push({
|
|
624
|
+
name: basename(file, '.html'),
|
|
625
|
+
source: readFileSync(file, 'utf8'),
|
|
626
|
+
where: relative(root, file),
|
|
627
|
+
});
|
|
428
628
|
}
|
|
429
629
|
}
|
|
430
630
|
|
|
631
|
+
const siblings = siblingIndex(screens);
|
|
632
|
+
for (const screen of screens) {
|
|
633
|
+
problems.push(...validateScreenHtml(screen.source, screen.where, false, siblings));
|
|
634
|
+
}
|
|
635
|
+
|
|
431
636
|
return problems;
|
|
432
637
|
}
|
|
433
638
|
|
package/dist/styles.css
CHANGED
|
@@ -1091,8 +1091,6 @@
|
|
|
1091
1091
|
|
|
1092
1092
|
/* src/NavigationButton/NavigationButton.css */
|
|
1093
1093
|
.palda-navigation-button {
|
|
1094
|
-
--palda-navigation-button-icon-color: var(--palda-color-border-strong);
|
|
1095
|
-
--palda-navigation-button-text-color: var(--palda-color-text-disabled);
|
|
1096
1094
|
align-items: center;
|
|
1097
1095
|
background: var(--palda-navigation-button-background, transparent);
|
|
1098
1096
|
border: 0;
|
|
@@ -1100,34 +1098,55 @@
|
|
|
1100
1098
|
box-sizing: border-box;
|
|
1101
1099
|
color: var(--palda-navigation-button-text-color);
|
|
1102
1100
|
cursor: pointer;
|
|
1103
|
-
display: flex;
|
|
1104
1101
|
gap: var(--palda-space-gap-sm);
|
|
1105
|
-
height: 48px;
|
|
1106
1102
|
justify-content: flex-start;
|
|
1107
|
-
|
|
1103
|
+
min-width: 0;
|
|
1108
1104
|
text-align: left;
|
|
1105
|
+
text-decoration: none;
|
|
1109
1106
|
transition: background-color var(--palda-duration-fast) var(--palda-easing-standard), color var(--palda-duration-fast) var(--palda-easing-standard);
|
|
1107
|
+
}
|
|
1108
|
+
.palda-navigation-button--md {
|
|
1109
|
+
--palda-navigation-button-icon-color: var(--palda-color-border-strong);
|
|
1110
|
+
--palda-navigation-button-text-color: var(--palda-color-text-disabled);
|
|
1111
|
+
display: flex;
|
|
1112
|
+
height: 48px;
|
|
1113
|
+
padding: var(--palda-space-3) var(--palda-space-4);
|
|
1110
1114
|
width: 100%;
|
|
1111
1115
|
}
|
|
1116
|
+
.palda-navigation-button--sm {
|
|
1117
|
+
--palda-navigation-button-icon-color: var(--palda-color-primary-default);
|
|
1118
|
+
--palda-navigation-button-text-color: var(--palda-color-text-primary);
|
|
1119
|
+
display: inline-flex;
|
|
1120
|
+
height: 36px;
|
|
1121
|
+
padding: var(--palda-space-2) var(--palda-space-3);
|
|
1122
|
+
}
|
|
1112
1123
|
.palda-navigation-button__icon {
|
|
1113
1124
|
align-items: center;
|
|
1114
1125
|
color: var(--palda-navigation-button-icon-color);
|
|
1115
1126
|
display: inline-flex;
|
|
1116
1127
|
flex-shrink: 0;
|
|
1128
|
+
justify-content: center;
|
|
1129
|
+
}
|
|
1130
|
+
.palda-navigation-button--md .palda-navigation-button__icon {
|
|
1117
1131
|
font-size: 24px;
|
|
1118
1132
|
height: 24px;
|
|
1119
|
-
justify-content: center;
|
|
1120
1133
|
width: 24px;
|
|
1121
1134
|
}
|
|
1135
|
+
.palda-navigation-button--sm .palda-navigation-button__icon {
|
|
1136
|
+
font-size: 20px;
|
|
1137
|
+
height: 20px;
|
|
1138
|
+
width: 20px;
|
|
1139
|
+
}
|
|
1122
1140
|
.palda-navigation-button__label {
|
|
1123
1141
|
overflow: hidden;
|
|
1124
1142
|
text-overflow: ellipsis;
|
|
1125
1143
|
white-space: nowrap;
|
|
1126
1144
|
}
|
|
1127
|
-
.palda-navigation-button:not(:disabled, .palda-navigation-button--selected):is(:hover, :active)
|
|
1145
|
+
.palda-navigation-button--md:not(:disabled, .palda-navigation-button--selected):is(:hover, :active),
|
|
1146
|
+
.palda-navigation-button--sm:not(:disabled):is(:hover, :active) {
|
|
1128
1147
|
--palda-navigation-button-background: var(--palda-color-state-hover-pressed);
|
|
1129
1148
|
}
|
|
1130
|
-
.palda-navigation-button--selected {
|
|
1149
|
+
.palda-navigation-button--md.palda-navigation-button--selected {
|
|
1131
1150
|
--palda-navigation-button-background: var(--palda-color-accent-subtle);
|
|
1132
1151
|
--palda-navigation-button-icon-color: var(--palda-color-accent-default);
|
|
1133
1152
|
--palda-navigation-button-text-color: var(--palda-color-text-accent);
|
|
@@ -1245,6 +1264,49 @@
|
|
|
1245
1264
|
}
|
|
1246
1265
|
}
|
|
1247
1266
|
|
|
1267
|
+
/* src/Skeleton/Skeleton.css */
|
|
1268
|
+
.palda-skeleton {
|
|
1269
|
+
animation: palda-skeleton-pulse var(--palda-duration-normal) var(--palda-easing-standard) infinite alternate;
|
|
1270
|
+
background: var(--palda-color-surface-sunken);
|
|
1271
|
+
border-radius: var(--palda-radius-control-sm);
|
|
1272
|
+
display: block;
|
|
1273
|
+
}
|
|
1274
|
+
.palda-skeleton--body1 {
|
|
1275
|
+
height: var(--palda-font-size-md);
|
|
1276
|
+
}
|
|
1277
|
+
.palda-skeleton--body2 {
|
|
1278
|
+
height: var(--palda-font-size-sm);
|
|
1279
|
+
}
|
|
1280
|
+
.palda-skeleton--label {
|
|
1281
|
+
height: var(--palda-font-size-xs);
|
|
1282
|
+
}
|
|
1283
|
+
.palda-skeleton--full {
|
|
1284
|
+
width: 100%;
|
|
1285
|
+
}
|
|
1286
|
+
.palda-skeleton--wide {
|
|
1287
|
+
width: 70%;
|
|
1288
|
+
}
|
|
1289
|
+
.palda-skeleton--narrow {
|
|
1290
|
+
width: 40%;
|
|
1291
|
+
}
|
|
1292
|
+
.palda-skeleton--block {
|
|
1293
|
+
block-size: 100%;
|
|
1294
|
+
inline-size: 100%;
|
|
1295
|
+
}
|
|
1296
|
+
@keyframes palda-skeleton-pulse {
|
|
1297
|
+
from {
|
|
1298
|
+
opacity: 1;
|
|
1299
|
+
}
|
|
1300
|
+
to {
|
|
1301
|
+
opacity: 0.4;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1305
|
+
.palda-skeleton {
|
|
1306
|
+
animation: none;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1248
1310
|
/* src/Switch/Switch.css */
|
|
1249
1311
|
.palda-switch {
|
|
1250
1312
|
align-items: center;
|
|
@@ -1355,6 +1417,11 @@
|
|
|
1355
1417
|
justify-content: center;
|
|
1356
1418
|
width: 24px;
|
|
1357
1419
|
}
|
|
1420
|
+
.palda-tab__badge {
|
|
1421
|
+
align-items: center;
|
|
1422
|
+
display: inline-flex;
|
|
1423
|
+
flex-shrink: 0;
|
|
1424
|
+
}
|
|
1358
1425
|
.palda-tab__label {
|
|
1359
1426
|
white-space: nowrap;
|
|
1360
1427
|
}
|
|
@@ -1503,6 +1570,24 @@
|
|
|
1503
1570
|
}
|
|
1504
1571
|
}
|
|
1505
1572
|
|
|
1573
|
+
/* src/Thumbnail/Thumbnail.css */
|
|
1574
|
+
.palda-thumbnail {
|
|
1575
|
+
background: var(--palda-color-surface-raised);
|
|
1576
|
+
border-radius: var(--palda-radius-control);
|
|
1577
|
+
box-sizing: border-box;
|
|
1578
|
+
display: block;
|
|
1579
|
+
flex-shrink: 0;
|
|
1580
|
+
object-fit: cover;
|
|
1581
|
+
}
|
|
1582
|
+
.palda-thumbnail--md {
|
|
1583
|
+
height: 40px;
|
|
1584
|
+
width: 40px;
|
|
1585
|
+
}
|
|
1586
|
+
.palda-thumbnail--sm {
|
|
1587
|
+
height: 32px;
|
|
1588
|
+
width: 32px;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1506
1591
|
/* src/Toast/Toast.css */
|
|
1507
1592
|
.palda-toast {
|
|
1508
1593
|
align-items: center;
|
|
@@ -1886,7 +1971,7 @@ button.palda-card {
|
|
|
1886
1971
|
text-align: start;
|
|
1887
1972
|
width: 100%;
|
|
1888
1973
|
}
|
|
1889
|
-
button.palda-card:hover {
|
|
1974
|
+
button.palda-card:not(.palda-card--selected):hover {
|
|
1890
1975
|
border-color: var(--palda-color-border-strong);
|
|
1891
1976
|
}
|
|
1892
1977
|
button.palda-card:focus-visible {
|
|
@@ -2005,6 +2090,16 @@ button.palda-card:focus-visible {
|
|
|
2005
2090
|
gap: var(--palda-space-gap-lg);
|
|
2006
2091
|
min-width: 0;
|
|
2007
2092
|
}
|
|
2093
|
+
.palda-form-section--filled .palda-form-section__fields {
|
|
2094
|
+
background: var(--palda-color-surface-raised);
|
|
2095
|
+
border-radius: var(--palda-radius-container);
|
|
2096
|
+
padding: var(--palda-space-inset-md);
|
|
2097
|
+
--palda-list-row-filled-background: var(--palda-color-surface-base);
|
|
2098
|
+
}
|
|
2099
|
+
.palda-form-section--filled .palda-text-field--outline,
|
|
2100
|
+
.palda-form-section--filled .palda-text-field--accent {
|
|
2101
|
+
--palda-text-field-background: var(--palda-color-surface-base);
|
|
2102
|
+
}
|
|
2008
2103
|
.palda-form-section__footer {
|
|
2009
2104
|
align-items: center;
|
|
2010
2105
|
display: flex;
|
|
@@ -2033,7 +2128,7 @@ button.palda-card:focus-visible {
|
|
|
2033
2128
|
border-block-end: 0;
|
|
2034
2129
|
}
|
|
2035
2130
|
.palda-list-row--filled {
|
|
2036
|
-
background: var(--palda-color-surface-raised);
|
|
2131
|
+
background: var(--palda-list-row-filled-background, var(--palda-color-surface-raised));
|
|
2037
2132
|
border-radius: var(--palda-radius-control);
|
|
2038
2133
|
padding-inline: var(--palda-space-inset-md);
|
|
2039
2134
|
}
|
|
@@ -2098,36 +2193,11 @@ button.palda-list-row:focus-visible {
|
|
|
2098
2193
|
flex-direction: column;
|
|
2099
2194
|
gap: var(--palda-space-gap-xs);
|
|
2100
2195
|
}
|
|
2101
|
-
.palda-list-row__skeleton {
|
|
2102
|
-
animation: palda-list-row-pulse var(--palda-duration-normal) var(--palda-easing-standard) infinite alternate;
|
|
2103
|
-
background: var(--palda-color-surface-sunken);
|
|
2104
|
-
border-radius: var(--palda-radius-control-sm);
|
|
2105
|
-
display: block;
|
|
2106
|
-
}
|
|
2107
|
-
.palda-list-row__skeleton--title {
|
|
2108
|
-
height: var(--palda-font-size-sm);
|
|
2109
|
-
width: 40%;
|
|
2110
|
-
}
|
|
2111
|
-
.palda-list-row__skeleton--description {
|
|
2112
|
-
height: var(--palda-font-size-xs);
|
|
2113
|
-
width: 70%;
|
|
2114
|
-
}
|
|
2115
|
-
@keyframes palda-list-row-pulse {
|
|
2116
|
-
from {
|
|
2117
|
-
opacity: 1;
|
|
2118
|
-
}
|
|
2119
|
-
to {
|
|
2120
|
-
opacity: 0.4;
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
2196
|
@media (prefers-reduced-motion: reduce) {
|
|
2124
2197
|
.palda-list-row,
|
|
2125
2198
|
.palda-list-row__chevron {
|
|
2126
2199
|
transition: none;
|
|
2127
2200
|
}
|
|
2128
|
-
.palda-list-row__skeleton {
|
|
2129
|
-
animation: none;
|
|
2130
|
-
}
|
|
2131
2201
|
}
|
|
2132
2202
|
|
|
2133
2203
|
/* src/LivePreview/LivePreview.css */
|