@highpixel-co/palda-design-system 0.6.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.
@@ -1,6 +1,7 @@
1
1
  import {
2
- Button
3
- } from "./chunk-6JZPK2W5.js";
2
+ Button,
3
+ Skeleton
4
+ } from "./chunk-PWAQFOM3.js";
4
5
  import {
5
6
  ChevronRightIcon
6
7
  } from "./chunk-TWYJ2UTC.js";
@@ -91,8 +92,15 @@ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
91
92
  var TITLE_TYPOGRAPHY_CLASS3 = "palda-text-body1-regular-semibold";
92
93
  var DESCRIPTION_TYPOGRAPHY_CLASS3 = "palda-text-body2-reading-medium";
93
94
  var ERROR_TYPOGRAPHY_CLASS = "palda-text-label-reading-medium";
94
- function FormSection({ children, description, error, footer, title }) {
95
- return /* @__PURE__ */ jsxs5("section", { className: "palda-form-section", children: [
95
+ function FormSection({
96
+ children,
97
+ description,
98
+ error,
99
+ footer,
100
+ title,
101
+ tone = "plain"
102
+ }) {
103
+ return /* @__PURE__ */ jsxs5("section", { className: `palda-form-section palda-form-section--${tone}`, children: [
96
104
  /* @__PURE__ */ jsxs5("header", { className: "palda-form-section__header", children: [
97
105
  /* @__PURE__ */ jsx5("h2", { className: TITLE_TYPOGRAPHY_CLASS3, children: title }),
98
106
  description ? /* @__PURE__ */ jsx5("p", { className: DESCRIPTION_TYPOGRAPHY_CLASS3, children: description }) : null
@@ -113,6 +121,14 @@ var DESCRIPTION_TYPOGRAPHY_CLASS4 = {
113
121
  default: "palda-text-label-reading-medium",
114
122
  strong: "palda-text-body2-reading-medium"
115
123
  };
124
+ var SKELETON_TITLE_TEXT = {
125
+ default: "body2",
126
+ strong: "body1"
127
+ };
128
+ var SKELETON_DESCRIPTION_TEXT = {
129
+ default: "label",
130
+ strong: "body2"
131
+ };
116
132
  function ListRow({
117
133
  chevron,
118
134
  children,
@@ -135,8 +151,8 @@ function ListRow({
135
151
  ].filter(Boolean).join(" ");
136
152
  if (loading) {
137
153
  return /* @__PURE__ */ jsxs6("div", { "aria-hidden": "true", className: classes, children: [
138
- /* @__PURE__ */ jsx6("span", { className: "palda-list-row__skeleton palda-list-row__skeleton--title" }),
139
- /* @__PURE__ */ jsx6("span", { className: "palda-list-row__skeleton palda-list-row__skeleton--description" })
154
+ /* @__PURE__ */ jsx6(Skeleton, { text: SKELETON_TITLE_TEXT[emphasis], width: "narrow" }),
155
+ /* @__PURE__ */ jsx6(Skeleton, { text: SKELETON_DESCRIPTION_TEXT[emphasis], width: "wide" })
140
156
  ] });
141
157
  }
142
158
  const showChevron = chevron ?? Boolean(onClick);
@@ -485,31 +485,57 @@ function Modal({
485
485
  }
486
486
 
487
487
  // ../../components/src/NavigationButton/NavigationButton.tsx
488
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
489
- function NavigationButton({
490
- children,
491
- className = "",
492
- icon,
493
- selected = false,
494
- ...props
495
- }) {
496
- const classes = [
488
+ import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
489
+ var TYPOGRAPHY_CLASS6 = {
490
+ md: "palda-text-body2-regular-semibold",
491
+ sm: "palda-text-body2-regular-medium"
492
+ };
493
+ function classesFor(size, selected, className) {
494
+ return [
497
495
  "palda-navigation-button",
496
+ `palda-navigation-button--${size}`,
498
497
  selected ? "palda-navigation-button--selected" : "",
499
- "palda-text-body2-regular-semibold",
498
+ TYPOGRAPHY_CLASS6[size],
500
499
  className
501
500
  ].filter(Boolean).join(" ");
502
- return /* @__PURE__ */ jsxs9(
501
+ }
502
+ function contentFor(icon, children) {
503
+ return /* @__PURE__ */ jsxs9(Fragment, { children: [
504
+ icon ? /* @__PURE__ */ jsx10("span", { "aria-hidden": "true", className: "palda-navigation-button__icon", children: icon }) : null,
505
+ /* @__PURE__ */ jsx10("span", { className: "palda-navigation-button__label", children })
506
+ ] });
507
+ }
508
+ function NavigationButton(props) {
509
+ if (props.href !== void 0) {
510
+ const {
511
+ children: children2,
512
+ className: className2 = "",
513
+ href,
514
+ icon: icon2,
515
+ selected: selected2 = false,
516
+ size: size2 = "md",
517
+ ...anchorProps
518
+ } = props;
519
+ return /* @__PURE__ */ jsx10(
520
+ "a",
521
+ {
522
+ ...anchorProps,
523
+ "aria-current": selected2 ? "page" : void 0,
524
+ className: classesFor(size2, selected2, className2),
525
+ href,
526
+ children: contentFor(icon2, children2)
527
+ }
528
+ );
529
+ }
530
+ const { children, className = "", icon, selected = false, size = "md", ...buttonProps } = props;
531
+ return /* @__PURE__ */ jsx10(
503
532
  "button",
504
533
  {
505
- ...props,
534
+ ...buttonProps,
506
535
  "aria-current": selected ? "page" : void 0,
507
- className: classes,
536
+ className: classesFor(size, selected, className),
508
537
  type: "button",
509
- children: [
510
- icon ? /* @__PURE__ */ jsx10("span", { "aria-hidden": "true", className: "palda-navigation-button__icon", children: icon }) : null,
511
- /* @__PURE__ */ jsx10("span", { className: "palda-navigation-button__label", children })
512
- ]
538
+ children: contentFor(icon, children)
513
539
  }
514
540
  );
515
541
  }
@@ -546,8 +572,27 @@ function Radio({ children, className = "", disabled, ...props }) {
546
572
  ] });
547
573
  }
548
574
 
575
+ // ../../components/src/Skeleton/Skeleton.tsx
576
+ import { jsx as jsx13 } from "react/jsx-runtime";
577
+ function Skeleton({
578
+ className = "",
579
+ shape = "text",
580
+ text = "body2",
581
+ width = "full",
582
+ ...props
583
+ }) {
584
+ const classes = [
585
+ "palda-skeleton",
586
+ `palda-skeleton--${shape}`,
587
+ shape === "text" ? `palda-skeleton--${text}` : "",
588
+ shape === "text" ? `palda-skeleton--${width}` : "",
589
+ className
590
+ ].filter(Boolean).join(" ");
591
+ return /* @__PURE__ */ jsx13("span", { ...props, "aria-hidden": "true", className: classes });
592
+ }
593
+
549
594
  // ../../components/src/Switch/Switch.tsx
550
- import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
595
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
551
596
  function Switch({ className = "", disabled, size = "md", ...props }) {
552
597
  const classes = [
553
598
  "palda-switch",
@@ -556,7 +601,7 @@ function Switch({ className = "", disabled, size = "md", ...props }) {
556
601
  className
557
602
  ].filter(Boolean).join(" ");
558
603
  return /* @__PURE__ */ jsxs11("label", { className: classes, children: [
559
- /* @__PURE__ */ jsx13(
604
+ /* @__PURE__ */ jsx14(
560
605
  "input",
561
606
  {
562
607
  ...props,
@@ -566,12 +611,12 @@ function Switch({ className = "", disabled, size = "md", ...props }) {
566
611
  type: "checkbox"
567
612
  }
568
613
  ),
569
- /* @__PURE__ */ jsx13("span", { "aria-hidden": "true", className: "palda-switch__track", children: /* @__PURE__ */ jsx13("span", { className: "palda-switch__knob" }) })
614
+ /* @__PURE__ */ jsx14("span", { "aria-hidden": "true", className: "palda-switch__track", children: /* @__PURE__ */ jsx14("span", { className: "palda-switch__knob" }) })
570
615
  ] });
571
616
  }
572
617
 
573
618
  // ../../components/src/Tab/Tab.tsx
574
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
619
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
575
620
  function Tab({
576
621
  children,
577
622
  className = "",
@@ -587,15 +632,15 @@ function Tab({
587
632
  className
588
633
  ].filter(Boolean).join(" ");
589
634
  return /* @__PURE__ */ jsxs12("button", { ...props, "aria-selected": selected, className: classes, role: "tab", type: "button", children: [
590
- icon ? /* @__PURE__ */ jsx14("span", { "aria-hidden": "true", className: "palda-tab__icon", children: icon }) : null,
591
- /* @__PURE__ */ jsx14("span", { className: "palda-tab__label", children }),
592
- badge ? /* @__PURE__ */ jsx14("span", { className: "palda-tab__badge", children: badge }) : null
635
+ icon ? /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", className: "palda-tab__icon", children: icon }) : null,
636
+ /* @__PURE__ */ jsx15("span", { className: "palda-tab__label", children }),
637
+ badge ? /* @__PURE__ */ jsx15("span", { className: "palda-tab__badge", children: badge }) : null
593
638
  ] });
594
639
  }
595
640
 
596
641
  // ../../components/src/TextField/TextField.tsx
597
642
  import { useId as useId3, useState as useState2 } from "react";
598
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
643
+ import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
599
644
  var LABEL_TYPOGRAPHY_CLASS3 = "palda-text-body2-regular-semibold";
600
645
  var FOOTER_TYPOGRAPHY_CLASS = "palda-text-label-regular-semibold";
601
646
  var CONTROL_TYPOGRAPHY_CLASS = {
@@ -657,17 +702,17 @@ function TextField({
657
702
  return /* @__PURE__ */ jsxs13("div", { className: classes, children: [
658
703
  label === void 0 || label === null || label === "" ? null : /* @__PURE__ */ jsxs13("label", { className: `palda-text-field__label ${LABEL_TYPOGRAPHY_CLASS3}`, htmlFor: controlId, children: [
659
704
  label,
660
- required ? /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", className: "palda-text-field__required", children: "*" }) : null
705
+ required ? /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "palda-text-field__required", children: "*" }) : null
661
706
  ] }),
662
707
  /* @__PURE__ */ jsxs13("div", { className: "palda-text-field__field", children: [
663
708
  size === "md" ? (
664
709
  // input과 textarea는 같은 prop을 받지만 이벤트 타입만 다르다.
665
- /* @__PURE__ */ jsx15("textarea", { ...controlProps })
666
- ) : /* @__PURE__ */ jsx15("input", { ...controlProps }),
667
- icon ? /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", className: "palda-text-field__icon", children: icon }) : null
710
+ /* @__PURE__ */ jsx16("textarea", { ...controlProps })
711
+ ) : /* @__PURE__ */ jsx16("input", { ...controlProps }),
712
+ icon ? /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "palda-text-field__icon", children: icon }) : null
668
713
  ] }),
669
714
  description || showCount ? /* @__PURE__ */ jsxs13("div", { className: `palda-text-field__footer ${FOOTER_TYPOGRAPHY_CLASS}`, children: [
670
- /* @__PURE__ */ jsx15("span", { className: "palda-text-field__description", id: descriptionId, children: description }),
715
+ /* @__PURE__ */ jsx16("span", { className: "palda-text-field__description", id: descriptionId, children: description }),
671
716
  showCount ? /* @__PURE__ */ jsxs13("span", { className: "palda-text-field__count", id: countId, children: [
672
717
  length,
673
718
  "/",
@@ -677,6 +722,22 @@ function TextField({
677
722
  ] });
678
723
  }
679
724
 
725
+ // ../../components/src/Thumbnail/Thumbnail.tsx
726
+ import { jsx as jsx17 } from "react/jsx-runtime";
727
+ function Thumbnail({
728
+ alt,
729
+ className = "",
730
+ // 상품 목록은 수백 개까지 가므로 기본을 lazy로 둔다. 첫 화면에 보이는 자리는 쓰는 쪽이
731
+ // `loading="eager"`로 되돌린다.
732
+ loading = "lazy",
733
+ size = "md",
734
+ src,
735
+ ...props
736
+ }) {
737
+ const classes = ["palda-thumbnail", `palda-thumbnail--${size}`, className].filter(Boolean).join(" ");
738
+ return /* @__PURE__ */ jsx17("img", { ...props, alt, className: classes, loading, src });
739
+ }
740
+
680
741
  // ../../components/src/Toast/toastStore.ts
681
742
  var queue = [];
682
743
  var lastId = 0;
@@ -711,8 +772,8 @@ import { useSyncExternalStore } from "react";
711
772
 
712
773
  // ../../components/src/Toast/Toast.tsx
713
774
  import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
714
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
715
- var TYPOGRAPHY_CLASS6 = "palda-text-body2-reading-medium";
775
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
776
+ var TYPOGRAPHY_CLASS7 = "palda-text-body2-reading-medium";
716
777
  var DEFAULT_DURATION_MS = 5e3;
717
778
  function Toast({
718
779
  actionLabel,
@@ -741,7 +802,7 @@ function Toast({
741
802
  remainingRef.current = Math.max(remainingRef.current - (Date.now() - startedAt), 0);
742
803
  };
743
804
  }, [duration, onClose, paused]);
744
- const classes = ["palda-toast", `palda-toast--${variant}`, TYPOGRAPHY_CLASS6, className].filter(Boolean).join(" ");
805
+ const classes = ["palda-toast", `palda-toast--${variant}`, TYPOGRAPHY_CLASS7, className].filter(Boolean).join(" ");
745
806
  return /* @__PURE__ */ jsxs14(
746
807
  "div",
747
808
  {
@@ -752,17 +813,17 @@ function Toast({
752
813
  onMouseLeave: () => setPaused(false),
753
814
  role: variant === "danger" ? "alert" : "status",
754
815
  children: [
755
- icon ? /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "palda-toast__icon", children: icon }) : null,
756
- /* @__PURE__ */ jsx16("span", { className: "palda-toast__content", children }),
757
- actionLabel ? /* @__PURE__ */ jsx16("button", { className: "palda-toast__action", onClick: onAction, type: "button", children: actionLabel }) : null,
758
- onClose ? /* @__PURE__ */ jsx16(
816
+ icon ? /* @__PURE__ */ jsx18("span", { "aria-hidden": "true", className: "palda-toast__icon", children: icon }) : null,
817
+ /* @__PURE__ */ jsx18("span", { className: "palda-toast__content", children }),
818
+ actionLabel ? /* @__PURE__ */ jsx18("button", { className: "palda-toast__action", onClick: onAction, type: "button", children: actionLabel }) : null,
819
+ onClose ? /* @__PURE__ */ jsx18(
759
820
  "button",
760
821
  {
761
822
  "aria-label": closeLabel,
762
823
  className: "palda-toast__close",
763
824
  onClick: onClose,
764
825
  type: "button",
765
- children: /* @__PURE__ */ jsx16(CloseIcon, {})
826
+ children: /* @__PURE__ */ jsx18(CloseIcon, {})
766
827
  }
767
828
  ) : null
768
829
  ]
@@ -771,12 +832,12 @@ function Toast({
771
832
  }
772
833
 
773
834
  // ../../components/src/Toast/Toaster.tsx
774
- import { jsx as jsx17 } from "react/jsx-runtime";
835
+ import { jsx as jsx19 } from "react/jsx-runtime";
775
836
  import { createElement } from "react";
776
837
  function Toaster({ className = "", placement = "bottom-center" }) {
777
838
  const toasts = useSyncExternalStore(subscribeToToasts, getToasts, getToasts);
778
839
  const classes = ["palda-toaster", `palda-toaster--${placement}`, className].filter(Boolean).join(" ");
779
- return /* @__PURE__ */ jsx17("div", { className: classes, children: toasts.map(({ id, message, onAction, ...options }) => /* @__PURE__ */ createElement(
840
+ return /* @__PURE__ */ jsx19("div", { className: classes, children: toasts.map(({ id, message, onAction, ...options }) => /* @__PURE__ */ createElement(
780
841
  Toast,
781
842
  {
782
843
  ...options,
@@ -802,7 +863,7 @@ import {
802
863
  useRef as useRef4,
803
864
  useState as useState4
804
865
  } from "react";
805
- import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
866
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
806
867
  var OPPOSITE = {
807
868
  bottom: "top",
808
869
  left: "right",
@@ -952,7 +1013,7 @@ function Tooltip({ children, className = "", label, placement = "top" }) {
952
1013
  ),
953
1014
  children: [
954
1015
  label,
955
- /* @__PURE__ */ jsx18("span", { "aria-hidden": "true", className: "palda-tooltip__arrow" })
1016
+ /* @__PURE__ */ jsx20("span", { "aria-hidden": "true", className: "palda-tooltip__arrow" })
956
1017
  ]
957
1018
  }
958
1019
  ) : null
@@ -975,9 +1036,11 @@ export {
975
1036
  NavigationButton,
976
1037
  ProgressBar,
977
1038
  Radio,
1039
+ Skeleton,
978
1040
  Switch,
979
1041
  Tab,
980
1042
  TextField,
1043
+ Thumbnail,
981
1044
  toast,
982
1045
  Toaster,
983
1046
  Tooltip
@@ -35,13 +35,13 @@ import { Button } from '@highpixel-co/palda-design-system/ui';
35
35
  `styles.css`를 함께 불러오지 않으면 토큰이 없어 색이 나오지 않습니다.
36
36
 
37
37
  `tokens/`, `icons/`, `components/`, `patterns/`는 저장소 내부 모듈이며 개별 배포하지 않습니다.
38
- 배포와 소비 설정은 [`packages/palda-design-system/README.md`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/packages/palda-design-system/README.md)를
38
+ 배포와 소비 설정은 [`packages/palda-design-system/README.md`](https://github.com/highpixel-co/palda-design-system/blob/abee826d0afda16bd3776f015931e0a820a8876e/packages/palda-design-system/README.md)를
39
39
  따릅니다.
40
40
 
41
- - 디자이너: [`docs/REPOSITORY_GUIDE.md`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/REPOSITORY_GUIDE.md)부터 읽습니다.
41
+ - 디자이너: [`docs/REPOSITORY_GUIDE.md`](https://github.com/highpixel-co/palda-design-system/blob/abee826d0afda16bd3776f015931e0a820a8876e/docs/REPOSITORY_GUIDE.md)부터 읽습니다.
42
42
  - 디자인 원칙: [`docs/DESIGN_PRINCIPLES.md`](docs/DESIGN_PRINCIPLES.md)
43
43
  - Figma 운영: [`docs/FIGMA_WORKFLOW.md`](docs/FIGMA_WORKFLOW.md)
44
- - 기여 절차: [`docs/CONTRIBUTING.md`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/CONTRIBUTING.md)
44
+ - 기여 절차: [`docs/CONTRIBUTING.md`](https://github.com/highpixel-co/palda-design-system/blob/abee826d0afda16bd3776f015931e0a820a8876e/docs/CONTRIBUTING.md)
45
45
 
46
46
  현재 토큰과 컴포넌트 값은 구조를 보여주기 위한 예시이며, 디자이너 검토 후 확정해야 합니다.
47
47
 
@@ -98,7 +98,15 @@ dropdown-button:
98
98
  outline: 테두리가 있는 기본 트리거
99
99
  subtle: 테두리 없는 저강도 트리거. 툴바와 인라인
100
100
  note: 트리거는 시안 확정. 메뉴 패널·align·chevron 아이콘은 확정 전이다.
101
- planned: 다중 선택은 아직 없다. 들일 때 선택된 항목은 체크 아이콘(`CheckIcon`)으로 표시한다(2026-07-31 확정). 아이콘은 2026-07-31에 추가했다. 남은 것은 `DropdownButtonItem`에 선택 상태를 어떻게 두고 `onSelect`가 무엇을 돌려줄지 정하는 것이며, 공개 API가 바뀌므로 결정 기록을 함께 남긴다.
101
+ planned: >-
102
+ 다중 선택은 아직 없다. 들일 때 선택된 항목은 체크 아이콘(`CheckIcon`)으로 표시한다(2026-07-31
103
+ 확정). 아이콘은 2026-07-31에 추가했다. 남은 것은 `DropdownButtonItem`에 선택 상태를 어떻게
104
+ 두고 `onSelect`가 무엇을 돌려줄지 정하는 것이며, 공개 API가 바뀌므로 결정 기록을 함께 남긴다.
105
+ 상단 행의 스토어 스위처를 여기로 흡수하는 것은 메뉴 패널이 확정된 뒤로 미뤘다(2026-09-05).
106
+ 하려는 일은 같지만(단일 선택) 트리거가 셋 다르다 — 라벨 앞 아이콘 슬롯이 없고, 라벨이
107
+ body1/semibold가 아니라 body2/medium 고정이고, chevron이 border/strong이 아니라
108
+ primary/default다. 축 이름부터 걸린다: 둘 다 높이 36px이라 `size`가 아니고 `variant`는 이미
109
+ 보더 유무다. 그때까지 스위처는 preview 시안의 손 CSS로 남는다.
102
110
 
103
111
  icon-button:
104
112
  status: stable
@@ -156,13 +164,28 @@ navigation-button:
156
164
  story: components/src/NavigationButton/NavigationButton.stories.tsx
157
165
  figma: TODO
158
166
  use_for:
159
- - 사이드바·내비게이션의 화면 이동 항목
167
+ - 사이드바·내비게이션의 화면 이동 항목 (size="md")
168
+ - 상단 행의 마이페이지 진입점 — 내 아이디가 든다 (size="sm")
160
169
  - 지금 보고 있는 화면을 목록에서 표시하기
161
170
  avoid_for:
162
171
  - 화면을 옮기지 않는 액션 (Button을 쓴다)
163
172
  - 목록을 좁히는 필터 토글 (Chip을 쓴다)
164
- note: 현재 위치는 색만으로 전달하지 않도록 `aria-current="page"`로 함께 노출한다. 아이콘은 라벨 앞에 온다. 폭은 부모를 채우고 라벨은 넘치면 잘린다.
165
- planned: 시안에 hover/pressed가 없어 default 위에 `state/hover-pressed` 채움만 얹는 것으로 정의했고 확정 전이다. 선택 안 된 항목의 텍스트는 시안대로 `text/disabled`라 흰 배경에서 대비가 2.3:1로 미달이다. `<a href>` 렌더는 두지 않았다.
173
+ - 문장 안이나 보조 안내 줄의 이동 (Link를 쓴다 아이콘 슬롯도 hover 채움도 없는 자리다)
174
+ size:
175
+ md: 사이드바 항목. 48px · 아이콘 24 · semibold · 부모 폭을 채운다. 기본값이다
176
+ sm: 상단 행 항목. 36px · 아이콘 20 · medium · 내용만큼
177
+ note: >-
178
+ 현재 위치는 색만으로 전달하지 않도록 `aria-current="page"`로 함께 노출한다. 아이콘은 라벨
179
+ 앞에 오고 라벨은 넘치면 잘린다. `href`를 주면 `<a>`로 그려진다 — 화면을 옮기는 항목은
180
+ 링크여야 새 탭·주소 복사·가운데 클릭이 동작한다(Link와 같은 근거). 링크에는 `disabled`가
181
+ 없다. 갈 수 없는 링크는 링크로 두지 않는다.
182
+ size는 크기만 가르지 않고 색도 함께 간다 — md는 회색 사이드바 위 목록이라 안 고른 항목이
183
+ 흐리고(text/disabled · 아이콘 border/strong), sm은 흰 상단 행의 단독 항목이라 흐리지
184
+ 않다(text/primary · 아이콘 primary/default). 두 조합은 각각 시안에 있고, 축을 색과 크기로
185
+ 쪼개면 시안에 없는 조합이 열린다(0003).
186
+ selected 채움은 md에만 있다. 선택 표시는 형제들 사이에서만 뜻이 있는데 상단 행의 항목은
187
+ 하나뿐이라 대조할 형제가 없다 — sm은 aria-current만으로 현재 위치를 전한다(2026-09-05 확정).
188
+ planned: 시안에 hover/pressed가 없어 default 위에 `state/hover-pressed` 채움만 얹는 것으로 정의했고 확정 전이다. md에서 선택 안 된 항목의 텍스트는 시안대로 `text/disabled`라 흰 배경에서 대비가 2.3:1로 미달이다 — sm에 물려주지 않았고, md 쪽은 그대로 남아 있다.
166
189
 
167
190
  progress-bar:
168
191
  status: draft
@@ -196,6 +219,30 @@ radio:
196
219
  - 선택지를 펼쳐 놓을 자리가 좁을 때 (DropdownButton을 쓴다)
197
220
  note: 라벨을 넘기지 않으면 접근 가능한 이름을 `aria-label`로 준다.
198
221
 
222
+ skeleton:
223
+ status: draft
224
+ source: components/src/Skeleton/Skeleton.tsx
225
+ story: components/src/Skeleton/Skeleton.stories.tsx
226
+ figma: TODO
227
+ use_for:
228
+ - 값이 오기 전 그 자리를 잡아 두기
229
+ - 판정이 안 끝난 화면에서 본문 자리 세우기 (게이팅·잔액 등)
230
+ avoid_for:
231
+ - 얼마나 걸릴지 모르는 대기 (자리를 잡을 것이 없다)
232
+ - 버튼을 누른 뒤의 진행 표시 (Button의 `disabled`를 쓴다)
233
+ - 끝이 정해진 작업의 진행률 (ProgressBar를 쓴다)
234
+ - 값이 비어 있는 것 (EmptyState를 쓴다 — 비었다는 것은 이미 도착한 답이다)
235
+ note: >-
236
+ 글자 자리(`shape="text"`)의 높이는 대신할 텍스트 스타일의 글자 크기다. 갈리면 값이 도착할 때
237
+ 자리가 튄다. 면 자리(`shape="block"`)는 크기를 스스로 갖지 않고 놓인 자리를 채운다. 폭은
238
+ `full`·`wide`·`narrow` 셋뿐이고 값으로 받지 않는다 — 글자 폭에 정확한 값이 없고, 자리마다
239
+ 다른 값이 들어가면 같은 목록의 뼈대가 갈린다. 여러 줄은 화면이 조립한다. 늘 `aria-hidden`이라
240
+ 진행 중이라는 사실은 이것을 놓은 자리가 알린다. 결정 근거는 0038이다.
241
+ planned: >-
242
+ 시안 없이 만든 초안. 박동은 opacity 1↔0.4이고 `duration/normal`을 쓴다 — ListRow의 뼈대가
243
+ 쓰던 값 그대로다. shimmer(쓸고 지나가는 빛)는 두지 않았다. size가 없다 — 높이는 대신할 글자가
244
+ 정하고 면은 자리가 정해서, 고를 축이 아니다.
245
+
199
246
  switch:
200
247
  status: stable
201
248
  source: components/src/Switch/Switch.tsx
@@ -242,6 +289,26 @@ textfield:
242
289
  subtle: 배경과 보더 없음. 인라인과 저강도
243
290
  note: 크기가 렌더 요소를 정한다. `md`는 120px textarea, `sm`은 40px input이다. 글자 수 표시는 `maxLength`로 켠다.
244
291
 
292
+ thumbnail:
293
+ status: draft
294
+ source: components/src/Thumbnail/Thumbnail.tsx
295
+ story: components/src/Thumbnail/Thumbnail.stories.tsx
296
+ figma: TODO
297
+ use_for:
298
+ - 목록 행이나 카드 안에서 항목을 알아보게 하는 작은 사각 이미지
299
+ - 고른 상품처럼 여러 개를 나란히 늘어놓는 자리
300
+ avoid_for:
301
+ - 사람을 나타내는 원형 아바타 (다른 것이다. 지금 시스템에 없다)
302
+ - 읽어야 하는 크기의 이미지 (템플릿 미리보기는 화면이 직접 그린다)
303
+ note: >-
304
+ `alt`가 필수다. 장식이면 빈 문자열을 명시적으로 넘긴다 — 빠뜨린 것과 장식인 것이 코드에서
305
+ 갈려야 한다. 목록이 길어질 수 있어 `loading`의 기본이 `lazy`이며, 첫 화면에 보이는 자리는
306
+ 쓰는 쪽이 `eager`로 되돌린다.
307
+ planned: >-
308
+ md 40 · sm 32는 2026-09-04에 고른 값이지 실측이 아니다. Figma에 대응하는 컴포넌트가 없어
309
+ 대조할 것이 없었고, 시안이 다른 값을 요구하면 다시 본다. 이미지가 없거나 깨졌을 때는 회색
310
+ 면만 남긴다 — 대체 아이콘을 둘지와 어떤 아이콘인지는 확정 전이다.
311
+
245
312
  toast:
246
313
  status: draft
247
314
  source: components/src/Toast/Toaster.tsx
@@ -151,6 +151,32 @@ progress-bar:
151
151
  - 높이 8px은 명세 실측값이고 대응 토큰이 없다. 반복이 확인되면 토큰으로 올릴 것인가
152
152
  - 색으로 상태를 가르는 variant가 필요한가 — 지금은 채움이 accent가 아니라 primary 하나다
153
153
 
154
+ skeleton:
155
+ catalog: components
156
+ entry: skeleton
157
+ based_on_none_reason: 값이 오기 전 자리를 잡아 두는 Stable 요소가 없다 — ListRow가 뼈대를 자기 안에 감춰 두고 있었다
158
+ reused:
159
+ - ListRow 뼈대가 쓰던 색·라운드·박동 값 그대로 (surface/sunken, radius/control-sm, duration/normal, opacity 1↔0.4)
160
+ - 폭 40%·70% — ListRow 뼈대의 제목·설명 폭이 narrow·wide가 됐다
161
+ - 높이를 글자 크기로 잡는 규칙 — ListRow가 emphasis에 따라 뼈대를 같이 올리던 것과 같다
162
+ gap: 잔액·게이팅 판정을 기다리는 동안 화면이 세울 것이 없어, 없는 값으로 본문을 그렸다가 지웠다
163
+ rejected_alternatives:
164
+ - 판정 전까지 아무것도 안 그리기 — 흰 화면이 남고 머리만 뜬 채로 기다린다
165
+ - ‘불러오는 중’ 글자 — 자리를 잡지 못해 값이 오면 그대로 튄다
166
+ - ProgressBar — 끝이 정해진 값의 진행률이라 얼마나 걸릴지 모르는 대기에 못 쓴다
167
+ - FormSection에 loading prop — 틀은 상태를 갖지 않는다 (DESIGN_GRAMMAR §상태는 누가 갖나)
168
+ - shimmer(쓸고 지나가는 빛) — 아직 오지 않은 값이 움직이는 것처럼 보인다
169
+ added:
170
+ - shape text·block 둘과 text body1·body2·label 셋, width full·wide·narrow 셋
171
+ - 면 자리(block) — ListRow 뼈대에는 글자 줄만 있었다. 카드·썸네일 자리를 이것이 받는다
172
+ unchanged:
173
+ - ListRow의 loading prop과 그 마크업 구조. 안에 든 뼈대만 이 컴포넌트로 갈아 끼운다
174
+ - 로딩을 컨트롤의 별도 상태로 두지 않는 규칙 (COMPONENT_POLICY §로딩) — Button은 그대로 disabled다
175
+ feedback_questions:
176
+ - 폭 셋(100·70·40%)으로 충분한가 — 목록 뼈대에서 줄마다 길이를 다르게 주고 싶은 자리가 나오는가
177
+ - 박동 주기 180ms가 맞는가 — ListRow에서 물려받은 값이고 뼈대 여럿이 함께 뛰면 산만할 수 있다
178
+ - text에 headline을 더할 자리가 있는가 — 지금은 머리가 늘 즉시 그려져서 안 뒀다
179
+
154
180
  tab:
155
181
  catalog: components
156
182
  entry: tab
@@ -331,3 +357,23 @@ empty-state:
331
357
  - Button action contract와 semantic text hierarchy
332
358
  feedback_questions:
333
359
  - 최대 폭과 vertical spacing과 action 강조가 적절한가
360
+
361
+ thumbnail:
362
+ catalog: components
363
+ entry: thumbnail
364
+ based_on_none_reason: 이미지를 담는 Stable 컴포넌트가 없다. Card는 면이고 Badge는 라벨이라 둘 다 이미지 자리가 아니다
365
+ reused:
366
+ - radius/control과 surface/raised — 목록 행의 채운 면과 같은 눈금이다
367
+ - size를 md·sm 두 단계로 두는 기존 컴포넌트 규칙
368
+ gap: 상품처럼 사진으로 알아보는 항목을 목록과 카드에 놓을 자리가 없다
369
+ rejected_alternatives:
370
+ - 화면마다 img에 클래스를 직접 주는 방식은 라운드와 크기가 화면마다 갈린다
371
+ - 원형 아바타로 겸용하는 것은 사람과 사물을 한 컴포넌트에 넣는 일이라 뜻이 흐려진다
372
+ - 대체 아이콘을 얹는 것은 어떤 아이콘인지 근거가 없어 자산을 지어내게 된다
373
+ added:
374
+ - md 40 · sm 32 두 단계의 사각 썸네일과 필수 alt
375
+ unchanged:
376
+ - 채운 면의 회색은 한 종류라는 배경 위계와 기존 radius 스케일
377
+ feedback_questions:
378
+ - md 40 · sm 32가 맞는 눈금인가 — 실측이 아니라 고른 값이다
379
+ - 이미지가 없거나 깨졌을 때 회색 면만 남기면 되는가, 대체 아이콘이 필요한가
@@ -87,11 +87,18 @@ form-section:
87
87
  - 설정·정보 섹션을 제목 아래로 묶기 — 0011 이후 섹션의 기본 단위다
88
88
  - 입력 필드를 제목 아래로 묶는 폼의 한 덩어리
89
89
  - 섹션 단위 오류 표시
90
+ tone:
91
+ plain: 면이 없다. 0011의 기본값이고, 제목과 여백만으로 갈린다
92
+ filled: >-
93
+ 내용만 옅은 회색 필(surface/raised)에 담고 제목은 캔버스에 남긴다. 섹션이 여럿 쌓여 제목과
94
+ 여백만으로 묶음이 안 갈릴 때만 쓴다. 면 위에서 TextField는 흰 면으로 한 단계 밝아진다
90
95
  note: >-
91
- 0011에 따라 보더와 배경을 갖지 않는다. 섹션을 Card로 감싸지 않고 이것을 쓴다. 섹션 사이 여백은
92
- AppShell이 --space-8(32)로 준다. 라벨↔컨트롤 간격은 TextField가 갖고 있어 여기서 다시 주지
93
- 않는다. 섹션 오류는 role="alert"이고, 필드 하나의 오류는 TextField의 error를 쓴다.
94
- 폼이 아닌 섹션에도 쓰이므로 이름이 좁다 — 개명 여부 확정 대기.
96
+ 섹션을 Card로 감싸지 않고 이것을 쓴다. 섹션 사이 여백은 AppShell이 --space-8(32)로 준다.
97
+ 라벨↔컨트롤 간격은 TextField가 갖고 있어 여기서 다시 주지 않는다. 섹션 오류는 role="alert"이고,
98
+ 필드 하나의 오류는 TextField의 error를 쓴다. 폼이 아닌 섹션에도 쓰이므로 이름이 좁다 —
99
+ 개명 여부 확정 대기.
100
+ tone="filled"는 0011(박스 없는 것이 기본값)에서 벗어나는 자리다. 기본값은 여전히 plain이고,
101
+ 쓰는 조건과 근거는 ADR 0037에 있다.
95
102
 
96
103
  live-preview:
97
104
  status: draft
@@ -53,7 +53,7 @@ Palda가 목표로 하는 것은 단순히 컴포넌트를 많이 보유하는
53
53
  → 섹션·카드 `body1`(16) → 행 `body2`(14) → 보조 `label`(12). 화면이 고르는 것은 페이지 제목과
54
54
  **본문 목록 여부** 둘이고 나머지는 pattern이 이미 갖고 있다. 선언서의 본문 문장이 가리키는
55
55
  목록은 `ListRow`의 `emphasis="strong"`으로 제목 `body1`(16) + 설명 `body2`(14)가 된다
56
- ([`decisions/0034`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0034-the-body-list-lifts-one-step.md)).
56
+ ([`decisions/0034`](https://github.com/highpixel-co/palda-design-system/blob/abee826d0afda16bd3776f015931e0a820a8876e/docs/decisions/0034-the-body-list-lifts-one-step.md)).
57
57
 
58
58
  **주 액션의 자리**는 [`DESIGN_GRAMMAR.md`](DESIGN_GRAMMAR.md) §주 액션을 어디 두나가 갖는다 —
59
59
  페이지 헤더 우측과 `BottomBar` 둘 다 맞는 자리이고, 읽다가 가끔 누르는 화면은 위, 채워 넣고
@@ -85,7 +85,7 @@ AI에게 가장 효과적인 기준은 긴 설명보다 실제 정답 화면이
85
85
  - 데스크톱과 모바일에서 달라지는 부분
86
86
  - loading·empty·error 처리 방식
87
87
 
88
- 산출물은 [`../examples/approved/`](https://github.com/highpixel-co/palda-design-system/tree/ad23980481cd81dcf09a1044504d2295f0e4a0f3/examples/approved)에 보관한다.
88
+ 산출물은 [`../examples/approved/`](https://github.com/highpixel-co/palda-design-system/tree/abee826d0afda16bd3776f015931e0a820a8876e/examples/approved)에 보관한다.
89
89
 
90
90
  **완료 기준:** AI가 새 화면을 만들 때 가장 가까운 승인 화면을 골라 구조와 판단 기준을 재사용할
91
91
  수 있다.
@@ -103,7 +103,7 @@ AI가 자주 만들 수 있는 잘못된 화면도 시각적으로 남긴다.
103
103
  - 모바일에서 버튼이나 긴 한글이 잘리는 화면
104
104
 
105
105
  각 사례에는 문제가 되는 이유와 올바른 대안을 함께 기록한다. 산출물은
106
- [`../examples/anti-patterns/`](https://github.com/highpixel-co/palda-design-system/tree/ad23980481cd81dcf09a1044504d2295f0e4a0f3/examples/anti-patterns)에 보관한다.
106
+ [`../examples/anti-patterns/`](https://github.com/highpixel-co/palda-design-system/tree/abee826d0afda16bd3776f015931e0a820a8876e/examples/anti-patterns)에 보관한다.
107
107
 
108
108
  **완료 기준:** AI가 동일한 실수를 반복하지 않고, 잘못된 구현을 구체적인 사례와 비교해 수정할 수
109
109
  있다.
@@ -15,7 +15,7 @@
15
15
  적는다. prop 이름과 기본값은 타입이 원본이므로 문서로 옮겨 적지 않는다.
16
16
 
17
17
  **새 컴포넌트는 코드보다 먼저 property 목록과 상태 배치표를 낸다.** 상태는
18
- [`decisions/0004`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0004-component-state-naming.md)의 다섯 종류(상호작용 · 네이티브 ·
18
+ [`decisions/0004`](https://github.com/highpixel-co/palda-design-system/blob/abee826d0afda16bd3776f015931e0a820a8876e/docs/decisions/0004-component-state-naming.md)의 다섯 종류(상호작용 · 네이티브 ·
19
19
  값에서 파생 · 컴포넌트 소유 · 부모가 정함)에 배치하고, **prop으로 빼도 되는 것은 "부모가 정함"
20
20
  하나뿐이다.** `hovered`·`focused`·`filled` 같은 prop은 만들지 않는다. pattern에서 어느 것이
21
21
  상태를 소유하는지는 [`DESIGN_GRAMMAR.md`](DESIGN_GRAMMAR.md)의 §상태는 누가 갖나에 있다.
@@ -44,7 +44,7 @@
44
44
  ([`DESIGN_GRAMMAR.md`](DESIGN_GRAMMAR.md) §자리는 시스템이 정하지 않는다). 컴포넌트 시안은 셸이 없어 이 검사를 받지 않는다.
45
45
  - **렌더해서 흐리게 본다.** 시안을 실제로 그린 뒤, 스크린샷을 흐리게 봤을 때 가장 먼저 보이는
46
46
  것이 본문이어야 한다. 상태 배너·경고처럼 면적이 큰 것이 본문보다 먼저 보이면 그것을 뺀다
47
- ([`decisions/0024`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/docs/decisions/0024-state-belongs-to-the-nearest-owner.md)). 검사가 읽지
47
+ ([`decisions/0024`](https://github.com/highpixel-co/palda-design-system/blob/abee826d0afda16bd3776f015931e0a820a8876e/docs/decisions/0024-state-belongs-to-the-nearest-owner.md)). 검사가 읽지
48
48
  못하는 자리라 이 확인은 사람이 한다.
49
49
  - **시안은 자기 클래스만 정의한다.** 시스템 클래스(`.palda-*`)를 셀렉터에 쓰지 않는다 — 붙이는
50
50
  것은 마크업이 하고, 늘리거나 자리를 잡는 것은 감싸는 자리가 한다. pattern은 `className`을 받지
@@ -60,7 +60,7 @@
60
60
  `Toast`다.
61
61
 
62
62
  **진행 중은 글자가 아니라 `disabled`로 표현한다.** `Button`에 로딩 전용 prop이 없는 것이
63
- 그래서다([`components/src/Button/Button.tsx`](https://github.com/highpixel-co/palda-design-system/blob/ad23980481cd81dcf09a1044504d2295f0e4a0f3/components/src/Button/Button.tsx)).
63
+ 그래서다([`components/src/Button/Button.tsx`](https://github.com/highpixel-co/palda-design-system/blob/abee826d0afda16bd3776f015931e0a820a8876e/components/src/Button/Button.tsx)).
64
64
 
65
65
  ### "~기"로 끝난다
66
66