@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.
@@ -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
- const PRIMARY_ACTION_PLACES = new Set(['헤더 우측', 'BottomBar']);
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}\`다 — ${[...PRIMARY_ACTION_PLACES]
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 { declared: body.length > 0, asides: asides.length };
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}\`다 — ${ROLES.map((name) => `\`${name}\``).join('·')} 중 하나여야 한다`,
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
- export function validateScreenHtml(source, where, requireScreen = false) {
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
- const source = readFileSync(file, 'utf8');
426
- const where = relative(root, file);
427
- problems.push(...validateScreenHtml(source, where));
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
- padding: var(--palda-space-3) var(--palda-space-4);
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 */