@x-plat/design-system 0.12.0 → 0.13.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 (50) hide show
  1. package/dist/components/AutoResizeTextArea/index.cjs +2 -2
  2. package/dist/components/AutoResizeTextArea/index.js +2 -2
  3. package/dist/components/ChatInput/index.cjs +2 -2
  4. package/dist/components/ChatInput/index.js +2 -2
  5. package/dist/components/CheckBox/index.cjs +2 -2
  6. package/dist/components/CheckBox/index.js +2 -2
  7. package/dist/components/DatePicker/index.cjs +109 -75
  8. package/dist/components/DatePicker/index.d.cts +19 -0
  9. package/dist/components/DatePicker/index.d.ts +19 -0
  10. package/dist/components/DatePicker/index.js +109 -75
  11. package/dist/components/Editor/index.css +3 -0
  12. package/dist/components/FieldMessage/index.cjs +2 -2
  13. package/dist/components/FieldMessage/index.d.cts +8 -0
  14. package/dist/components/FieldMessage/index.d.ts +8 -0
  15. package/dist/components/FieldMessage/index.js +2 -2
  16. package/dist/components/FileUpload/index.cjs +10 -6
  17. package/dist/components/FileUpload/index.d.cts +15 -2
  18. package/dist/components/FileUpload/index.d.ts +15 -2
  19. package/dist/components/FileUpload/index.js +10 -6
  20. package/dist/components/ImageSelector/index.cjs +7 -6
  21. package/dist/components/ImageSelector/index.d.cts +15 -2
  22. package/dist/components/ImageSelector/index.d.ts +15 -2
  23. package/dist/components/ImageSelector/index.js +7 -6
  24. package/dist/components/Input/index.cjs +2 -2
  25. package/dist/components/Input/index.d.cts +4 -0
  26. package/dist/components/Input/index.d.ts +4 -0
  27. package/dist/components/Input/index.js +2 -2
  28. package/dist/components/Radio/index.cjs +2 -2
  29. package/dist/components/Radio/index.js +2 -2
  30. package/dist/components/Select/index.cjs +2 -2
  31. package/dist/components/Select/index.d.cts +1 -2
  32. package/dist/components/Select/index.d.ts +1 -2
  33. package/dist/components/Select/index.js +2 -2
  34. package/dist/components/Switch/index.cjs +2 -2
  35. package/dist/components/Switch/index.js +2 -2
  36. package/dist/components/TextArea/index.cjs +2 -2
  37. package/dist/components/TextArea/index.js +2 -2
  38. package/dist/components/TimePicker/index.cjs +2 -2
  39. package/dist/components/TimePicker/index.js +2 -2
  40. package/dist/components/index.cjs +122 -83
  41. package/dist/components/index.css +3 -0
  42. package/dist/components/index.js +122 -83
  43. package/dist/index.cjs +122 -83
  44. package/dist/index.css +3 -0
  45. package/dist/index.js +122 -83
  46. package/guidelines/Guidelines.md +12 -0
  47. package/guidelines/MIGRATION.md +270 -0
  48. package/guidelines/components/datepicker.md +26 -0
  49. package/guidelines/components/file-media.md +21 -4
  50. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5622,11 +5622,11 @@ var FieldMessage = (props) => {
5622
5622
  FieldMessage.displayName = "FieldMessage";
5623
5623
  var FieldMessage_default = FieldMessage;
5624
5624
  var Field = (props) => {
5625
- const { label, required = false, htmlFor, validations, children } = props;
5625
+ const { label, required = false, htmlFor, labelId, validations, children } = props;
5626
5626
  const hasMessages = !!validations && validations.length > 0;
5627
5627
  if (!label && !hasMessages) return /* @__PURE__ */ jsx297(Fragment, { children });
5628
5628
  return /* @__PURE__ */ jsxs191("div", { className: "lib-xplat-field", children: [
5629
- label && /* @__PURE__ */ jsxs191("label", { className: "lib-xplat-field-label", htmlFor, children: [
5629
+ label && /* @__PURE__ */ jsxs191("label", { className: "lib-xplat-field-label", id: labelId, htmlFor, children: [
5630
5630
  label,
5631
5631
  required && /* @__PURE__ */ jsx297("span", { className: "required", "aria-hidden": "true", children: "*" })
5632
5632
  ] }),
@@ -7406,8 +7406,12 @@ var SingleDatePicker = (props) => {
7406
7406
  highlightDates = [],
7407
7407
  locale = "ko",
7408
7408
  rangeStart,
7409
- rangeEnd
7409
+ rangeEnd,
7410
+ label,
7411
+ required,
7412
+ validations
7410
7413
  } = props;
7414
+ const labelId = React12.useId();
7411
7415
  const initialYear = value?.getFullYear();
7412
7416
  const initialMonth = value?.getMonth();
7413
7417
  const { year, month, days, goToPrevMonth, goToNextMonth, setYear, setMonth } = useCalendar(
@@ -7470,10 +7474,12 @@ var SingleDatePicker = (props) => {
7470
7474
  const monthLabels = MONTH_LABELS[locale];
7471
7475
  const titleText = pickerMode === "days" ? locale === "ko" ? `${year}\uB144 ${monthLabels[month]}` : `${monthLabels[month]} ${year}` : pickerMode === "months" ? `${year}` : `${yearRangeStart} - ${yearRangeStart + 11}`;
7472
7476
  const hasRange = rangeStart != null && rangeEnd != null;
7473
- return /* @__PURE__ */ jsxs200(
7477
+ const calendar = /* @__PURE__ */ jsxs200(
7474
7478
  "div",
7475
7479
  {
7476
7480
  className: clsx_default("lib-xplat-datepicker", "single"),
7481
+ role: "group",
7482
+ "aria-labelledby": label ? labelId : void 0,
7477
7483
  children: [
7478
7484
  /* @__PURE__ */ jsxs200("div", { className: "datepicker-header", children: [
7479
7485
  /* @__PURE__ */ jsx314("button", { className: "datepicker-nav", onClick: handlePrev, type: "button", children: /* @__PURE__ */ jsx314(ChevronLeftIcon_default, {}) }),
@@ -7494,18 +7500,18 @@ var SingleDatePicker = (props) => {
7494
7500
  y
7495
7501
  );
7496
7502
  }) }),
7497
- pickerMode === "months" && /* @__PURE__ */ jsx314("div", { className: "datepicker-picker-grid", children: monthLabels.map((label, i) => /* @__PURE__ */ jsx314(
7503
+ pickerMode === "months" && /* @__PURE__ */ jsx314("div", { className: "datepicker-picker-grid", children: monthLabels.map((label2, i) => /* @__PURE__ */ jsx314(
7498
7504
  "button",
7499
7505
  {
7500
7506
  type: "button",
7501
7507
  className: clsx_default("datepicker-picker-cell", i === month && "active"),
7502
7508
  onClick: () => handleMonthSelect(i),
7503
- children: label
7509
+ children: label2
7504
7510
  },
7505
7511
  i
7506
7512
  )) }),
7507
7513
  pickerMode === "days" && /* @__PURE__ */ jsxs200(Fragment4, { children: [
7508
- /* @__PURE__ */ jsx314("div", { className: "datepicker-weekdays", children: weekdays.map((label, i) => /* @__PURE__ */ jsx314(
7514
+ /* @__PURE__ */ jsx314("div", { className: "datepicker-weekdays", children: weekdays.map((label2, i) => /* @__PURE__ */ jsx314(
7509
7515
  "div",
7510
7516
  {
7511
7517
  className: clsx_default(
@@ -7513,9 +7519,9 @@ var SingleDatePicker = (props) => {
7513
7519
  i === 0 && "sunday",
7514
7520
  i === 6 && "saturday"
7515
7521
  ),
7516
- children: label
7522
+ children: label2
7517
7523
  },
7518
- label
7524
+ label2
7519
7525
  )) }),
7520
7526
  /* @__PURE__ */ jsx314("div", { className: "datepicker-grid", children: days.map((day, idx) => {
7521
7527
  const t = day.date.getTime();
@@ -7547,6 +7553,7 @@ var SingleDatePicker = (props) => {
7547
7553
  ]
7548
7554
  }
7549
7555
  );
7556
+ return /* @__PURE__ */ jsx314(Field, { label, required, labelId, validations, children: calendar });
7550
7557
  };
7551
7558
  SingleDatePicker.displayName = "SingleDatePicker";
7552
7559
  var SingleDatePicker_default = SingleDatePicker;
@@ -7561,7 +7568,18 @@ var formatDate = (date) => {
7561
7568
  return `${y}/${m}/${d}`;
7562
7569
  };
7563
7570
  var InputDatePicker = (props) => {
7564
- const { value, onChange, minDate, maxDate, disabled, locale = "ko", placeholder } = props;
7571
+ const {
7572
+ value,
7573
+ onChange,
7574
+ minDate,
7575
+ maxDate,
7576
+ disabled,
7577
+ locale = "ko",
7578
+ placeholder,
7579
+ label,
7580
+ required,
7581
+ validations
7582
+ } = props;
7565
7583
  const [isOpen, setIsOpen] = React13.useState(false);
7566
7584
  const [tempDate, setTempDate] = React13.useState(value ?? /* @__PURE__ */ new Date());
7567
7585
  const handleOpen = () => {
@@ -7587,6 +7605,9 @@ var InputDatePicker = (props) => {
7587
7605
  placeholder,
7588
7606
  suffix: /* @__PURE__ */ jsx315(CalenderIcon_default, {}),
7589
7607
  disabled,
7608
+ label,
7609
+ required,
7610
+ validations,
7590
7611
  readOnly: true
7591
7612
  }
7592
7613
  ) }),
@@ -7718,8 +7739,12 @@ var RangePicker = (props) => {
7718
7739
  onChange,
7719
7740
  minDate,
7720
7741
  maxDate,
7721
- locale = "ko"
7742
+ locale = "ko",
7743
+ label,
7744
+ required,
7745
+ validations
7722
7746
  } = props;
7747
+ const labelId = React16.useId();
7723
7748
  const [activeTab, setActiveTab] = React16.useState("start");
7724
7749
  const handleStartChange = (date) => {
7725
7750
  if (!date) return;
@@ -7733,70 +7758,79 @@ var RangePicker = (props) => {
7733
7758
  };
7734
7759
  const startMaxDate = maxDate && endDate < maxDate ? endDate : endDate;
7735
7760
  const endMinDate = minDate && startDate > minDate ? startDate : startDate;
7736
- return /* @__PURE__ */ jsxs203("div", { className: clsx_default("lib-xplat-datepicker", "range"), children: [
7737
- /* @__PURE__ */ jsx318("div", { className: "datepicker-range-tabs", children: /* @__PURE__ */ jsx318(
7738
- Tab_default,
7739
- {
7740
- activeIndex: activeTab === "start" ? 0 : 1,
7741
- tabs: [
7742
- { value: "start", title: locale === "ko" ? "\uC2DC\uC791\uC77C" : "Start" },
7743
- { value: "end", title: locale === "ko" ? "\uC885\uB8CC\uC77C" : "End" }
7744
- ],
7745
- onChange: (tab) => setActiveTab(tab.value),
7746
- type: "toggle",
7747
- size: "sm"
7748
- }
7749
- ) }),
7750
- /* @__PURE__ */ jsxs203("div", { className: "datepicker-range-panels", children: [
7751
- /* @__PURE__ */ jsx318(
7752
- SingleDatePicker_default,
7753
- {
7754
- value: startDate,
7755
- onChange: handleStartChange,
7756
- minDate,
7757
- maxDate: startMaxDate,
7758
- rangeStart: startDate,
7759
- rangeEnd: endDate,
7760
- locale
7761
- }
7762
- ),
7763
- /* @__PURE__ */ jsx318(
7764
- SingleDatePicker_default,
7765
- {
7766
- value: endDate,
7767
- onChange: handleEndChange,
7768
- minDate: endMinDate,
7769
- maxDate,
7770
- rangeStart: startDate,
7771
- rangeEnd: endDate,
7772
- locale
7773
- }
7774
- )
7775
- ] }),
7776
- /* @__PURE__ */ jsx318("div", { className: "datepicker-range-mobile", children: activeTab === "start" ? /* @__PURE__ */ jsx318(
7777
- SingleDatePicker_default,
7778
- {
7779
- value: startDate,
7780
- onChange: handleStartChange,
7781
- minDate,
7782
- maxDate: startMaxDate,
7783
- rangeStart: startDate,
7784
- rangeEnd: endDate,
7785
- locale
7786
- }
7787
- ) : /* @__PURE__ */ jsx318(
7788
- SingleDatePicker_default,
7789
- {
7790
- value: endDate,
7791
- onChange: handleEndChange,
7792
- minDate: endMinDate,
7793
- maxDate,
7794
- rangeStart: startDate,
7795
- rangeEnd: endDate,
7796
- locale
7797
- }
7798
- ) })
7799
- ] });
7761
+ const calendar = /* @__PURE__ */ jsxs203(
7762
+ "div",
7763
+ {
7764
+ className: clsx_default("lib-xplat-datepicker", "range"),
7765
+ role: "group",
7766
+ "aria-labelledby": label ? labelId : void 0,
7767
+ children: [
7768
+ /* @__PURE__ */ jsx318("div", { className: "datepicker-range-tabs", children: /* @__PURE__ */ jsx318(
7769
+ Tab_default,
7770
+ {
7771
+ activeIndex: activeTab === "start" ? 0 : 1,
7772
+ tabs: [
7773
+ { value: "start", title: locale === "ko" ? "\uC2DC\uC791\uC77C" : "Start" },
7774
+ { value: "end", title: locale === "ko" ? "\uC885\uB8CC\uC77C" : "End" }
7775
+ ],
7776
+ onChange: (tab) => setActiveTab(tab.value),
7777
+ type: "toggle",
7778
+ size: "sm"
7779
+ }
7780
+ ) }),
7781
+ /* @__PURE__ */ jsxs203("div", { className: "datepicker-range-panels", children: [
7782
+ /* @__PURE__ */ jsx318(
7783
+ SingleDatePicker_default,
7784
+ {
7785
+ value: startDate,
7786
+ onChange: handleStartChange,
7787
+ minDate,
7788
+ maxDate: startMaxDate,
7789
+ rangeStart: startDate,
7790
+ rangeEnd: endDate,
7791
+ locale
7792
+ }
7793
+ ),
7794
+ /* @__PURE__ */ jsx318(
7795
+ SingleDatePicker_default,
7796
+ {
7797
+ value: endDate,
7798
+ onChange: handleEndChange,
7799
+ minDate: endMinDate,
7800
+ maxDate,
7801
+ rangeStart: startDate,
7802
+ rangeEnd: endDate,
7803
+ locale
7804
+ }
7805
+ )
7806
+ ] }),
7807
+ /* @__PURE__ */ jsx318("div", { className: "datepicker-range-mobile", children: activeTab === "start" ? /* @__PURE__ */ jsx318(
7808
+ SingleDatePicker_default,
7809
+ {
7810
+ value: startDate,
7811
+ onChange: handleStartChange,
7812
+ minDate,
7813
+ maxDate: startMaxDate,
7814
+ rangeStart: startDate,
7815
+ rangeEnd: endDate,
7816
+ locale
7817
+ }
7818
+ ) : /* @__PURE__ */ jsx318(
7819
+ SingleDatePicker_default,
7820
+ {
7821
+ value: endDate,
7822
+ onChange: handleEndChange,
7823
+ minDate: endMinDate,
7824
+ maxDate,
7825
+ rangeStart: startDate,
7826
+ rangeEnd: endDate,
7827
+ locale
7828
+ }
7829
+ ) })
7830
+ ]
7831
+ }
7832
+ );
7833
+ return /* @__PURE__ */ jsx318(Field, { label, required, labelId, validations, children: calendar });
7800
7834
  };
7801
7835
  RangePicker.displayName = "RangePicker";
7802
7836
  var RangePicker_default = RangePicker;
@@ -9569,10 +9603,13 @@ var FileUpload = (props) => {
9569
9603
  multiple = false,
9570
9604
  maxSize,
9571
9605
  onChange,
9572
- label = "\uD30C\uC77C\uC744 \uB4DC\uB798\uADF8\uD558\uAC70\uB098 \uD074\uB9AD\uD558\uC5EC \uC5C5\uB85C\uB4DC",
9606
+ placeholder = "\uD30C\uC77C\uC744 \uB4DC\uB798\uADF8\uD558\uAC70\uB098 \uD074\uB9AD\uD558\uC5EC \uC5C5\uB85C\uB4DC",
9573
9607
  description,
9574
- validations
9608
+ validations,
9609
+ label,
9610
+ required
9575
9611
  } = props;
9612
+ const fieldId = React23.useId();
9576
9613
  const [isDragOver, setIsDragOver] = React23.useState(false);
9577
9614
  const inputRef = React23.useRef(null);
9578
9615
  const handleFiles = (fileList) => {
@@ -9617,6 +9654,7 @@ var FileUpload = (props) => {
9617
9654
  "input",
9618
9655
  {
9619
9656
  ref: inputRef,
9657
+ id: fieldId,
9620
9658
  type: "file",
9621
9659
  accept,
9622
9660
  multiple,
@@ -9625,12 +9663,12 @@ var FileUpload = (props) => {
9625
9663
  }
9626
9664
  ),
9627
9665
  /* @__PURE__ */ jsx325("div", { className: "upload-icon", children: /* @__PURE__ */ jsx325(UploadIcon_default, {}) }),
9628
- /* @__PURE__ */ jsx325("p", { className: "upload-label", children: label }),
9666
+ /* @__PURE__ */ jsx325("p", { className: "upload-label", children: placeholder }),
9629
9667
  description && /* @__PURE__ */ jsx325("p", { className: "upload-description", children: description })
9630
9668
  ]
9631
9669
  }
9632
9670
  );
9633
- return /* @__PURE__ */ jsx325(Field, { validations, children: control });
9671
+ return /* @__PURE__ */ jsx325(Field, { label, required, htmlFor: fieldId, validations, children: control });
9634
9672
  };
9635
9673
  FileUpload.displayName = "FileUpload";
9636
9674
  var FileUpload_default = FileUpload;
@@ -9773,7 +9811,8 @@ var HtmlTypeWriter_default = HtmlTypeWriter;
9773
9811
  import React26 from "react";
9774
9812
  import { jsx as jsx327, jsxs as jsxs210 } from "react/jsx-runtime";
9775
9813
  var ImageSelector = (props) => {
9776
- const { value, label, onChange, validations } = props;
9814
+ const { value, placeholder, onChange, validations, label, required } = props;
9815
+ const fieldId = React26.useId();
9777
9816
  const [previewUrl, setPreviewUrl] = React26.useState();
9778
9817
  React26.useEffect(() => {
9779
9818
  if (!value) {
@@ -9803,7 +9842,7 @@ var ImageSelector = (props) => {
9803
9842
  {
9804
9843
  type: "file",
9805
9844
  accept: "image/*",
9806
- id: "image-input",
9845
+ id: fieldId,
9807
9846
  onChange: handleFileChange,
9808
9847
  ref: inputRef,
9809
9848
  "aria-invalid": hasError(validations) || void 0
@@ -9815,10 +9854,10 @@ var ImageSelector = (props) => {
9815
9854
  ] }),
9816
9855
  /* @__PURE__ */ jsx327("div", { className: "content", children: previewUrl ? /* @__PURE__ */ jsx327("img", { src: previewUrl, alt: "preview" }) : /* @__PURE__ */ jsxs210("div", { className: "skeleton", onClick: handleOpenFileDialog, children: [
9817
9856
  /* @__PURE__ */ jsx327("div", { className: "icon-wrapper", children: /* @__PURE__ */ jsx327(ImageIcon_default, {}) }),
9818
- /* @__PURE__ */ jsx327("div", { className: "label", children: label || "\uC774\uBBF8\uC9C0 \uCD94\uAC00\uD558\uAE30" })
9857
+ /* @__PURE__ */ jsx327("div", { className: "label", children: placeholder || "\uC774\uBBF8\uC9C0 \uCD94\uAC00\uD558\uAE30" })
9819
9858
  ] }) })
9820
9859
  ] });
9821
- return /* @__PURE__ */ jsx327(Field, { validations, children: control });
9860
+ return /* @__PURE__ */ jsx327(Field, { label, required, htmlFor: fieldId, validations, children: control });
9822
9861
  };
9823
9862
  ImageSelector.displayName = "ImageSelector";
9824
9863
  var ImageSelector_default = ImageSelector;
@@ -74,3 +74,15 @@ React 기반 디자인 시스템 라이브러리이다. Figma Make Kit과 1:1
74
74
  4. **컴포지션**
75
75
  - [Grid](./composition/grid.md) - Grid 시스템, 위젯 패턴
76
76
  - [Layout](./composition/layout.md) - Layout, Header, SideBar
77
+
78
+ ---
79
+
80
+ ## 버전을 올릴 때
81
+
82
+ [마이그레이션 노트](./MIGRATION.md) — 0.5.x 이후 **실제로 깨지는 것만** 버전별로
83
+ 모아 뒀다. 자기 버전부터 아래로 읽으면 된다.
84
+
85
+ - 최신본: `https://unpkg.com/@x-plat/design-system/guidelines/MIGRATION.md`
86
+ - 커밋 제목만 보고 판단하지 말 것. `refactor!` 인데 별칭이 살아 있어 안 깨지는
87
+ 것이 있고, 평범한 `refactor` 인데 prop 이 사라진 것이 있다.
88
+
@@ -0,0 +1,270 @@
1
+ # @x-plat/design-system — 마이그레이션 노트
2
+
3
+ 버전을 올릴 때 **실제로 깨지는 것만** 모았다. 추가된 기능은 여기 적지 않는다 —
4
+ 그건 각 컴포넌트 문서에 있다.
5
+
6
+ - 최신본 URL: `https://unpkg.com/@x-plat/design-system/guidelines/MIGRATION.md`
7
+ - 자기 버전부터 아래로 읽어 내려가면 된다.
8
+
9
+ ## 이 문서를 믿어도 되는 범위
10
+
11
+ 각 항목은 **커밋 해시**를 달아 뒀다. 커밋 제목만 보고 판단하지 말 것 —
12
+ `refactor!` 라고 외치지만 별칭이 살아 있어 안 깨지는 것이 있고(0.7.1 `Badge`),
13
+ 평범한 `refactor` 인데 prop 이 사라진 것이 있다(0.8.0 `Button`).
14
+
15
+ 「실제로 걸린 것」 표는 진짜 이관에서 확인된 것이고, 나머지는 소스에서 확인한 것이다.
16
+
17
+ ---
18
+
19
+ ## 0.5.x → 0.6.0 — CSS 변수 이름 전면 교체
20
+
21
+ **`92dac35` · 가장 큰 변경이다.** Figma 변수 컬렉션 구조에 맞춰 컬러 레이어를
22
+ 4단계에서 3단계로 줄이면서 semantic 이름을 전부 바꿨다.
23
+
24
+ `var(--semantic-*)` 를 직접 쓰는 CSS 가 있으면 **조용히 깨진다** — 정의되지 않은
25
+ 변수는 오류를 내지 않고 색만 사라진다. 이관 후 화면을 눈으로 확인할 것.
26
+
27
+ | 예전 | 지금 |
28
+ |---|---|
29
+ | `--semantic-*-strong` | `--semantic-*-primary` |
30
+ | `--semantic-*-subtle` | `--semantic-*-secondary` |
31
+ | `--semantic-*-muted` | `--semantic-*-tertiary` |
32
+ | `--semantic-*-brand` | `--semantic-*-brand-primary` |
33
+ | `--semantic-*-sunday` | `--semantic-*-accent-red` |
34
+ | `--semantic-*-saturday` | `--semantic-*-accent-blue` |
35
+ | `--semantic-*-emphasis-*` | 삭제 → `--semantic-surface-*-strong` |
36
+ | `--brand-base-black-alpha-10/25` | `--brand-base-overlay-5/10/25/50/75/85` |
37
+
38
+ `color/primitive.ts` 가 삭제되고 `brand`/`system` 이 hex 실값을 직접 갖는다.
39
+ `var(--primitive-*)` 를 참조하던 곳은 전부 끊긴다.
40
+
41
+ **값이 바뀐 것 6건** (이름은 그대로인데 색이 달라진다):
42
+ `text-brand-primary` · `border-brand-primary` · `border-warning` ·
43
+ `surface-info-default` · `icon-success` · `icon-info`.
44
+ `system.link` 는 purple → deep-purple.
45
+
46
+ > 컴포넌트만 쓰고 CSS 변수를 직접 참조하지 않는다면 이 절은 건너뛰어도 된다.
47
+
48
+ ## 0.6.0 → 0.7.0 — effects · typography 토큰 정합
49
+
50
+ **`6429909`** 컴포넌트가 하드코딩하던 그림자·폰트를 토큰으로 옮겼다.
51
+ 컴포넌트를 그대로 쓰면 영향 없다. 그림자나 폰트 크기를 CSS 로 덮어쓰고 있었다면
52
+ 우선순위가 달라질 수 있다.
53
+
54
+ ## 0.7.0 → 0.7.1 — Badge 이름 변경
55
+
56
+ **`c95ee21` · `6430c9c`**
57
+
58
+ | 대상 | 깨지나 | 조치 |
59
+ |---|---|---|
60
+ | `import { Badge }` | **안 깨짐** | `Badge` 가 `@deprecated` 별칭으로 계속 나간다. 새로 쓰는 곳만 `NotificationBadge` |
61
+ | CSS 클래스 `.lib-xplat-badge` | **깨짐** | `.lib-xplat-notification-badge` 로. 별칭 없음 |
62
+
63
+ **`NotificationBadge` 는 알약(pill) 라벨이 아니다.** 아이콘 위에 올라타는 개수
64
+ 인디케이터다. `count` 도 `dot` 도 없으면 **뱃지가 렌더되지 않고 children 만 통과한다.**
65
+
66
+ ```tsx
67
+ <Badge type="error">품절</Badge> // ✗ 아무것도 안 그려지고 "품절" 만 나온다
68
+ <NotificationBadge count={3}><BellIcon /></NotificationBadge> // ○
69
+ ```
70
+
71
+ 알약 라벨이 필요하면 `Tag` 를 쓴다. 이 오용은 이름이 바뀌기 전부터 조용히
72
+ 실패하고 있었으므로, 올리기 전에 `<Badge>` 사용처를 훑어 볼 것.
73
+
74
+ ## 0.7.2 → 0.8.0 — `Button` 이 `<button>` 전용으로 복귀
75
+
76
+ **`f5668dd` · `0a4ab14` · 실제로 제일 많이 깨지는 항목이다.**
77
+
78
+ `Button` 은 이제 `<button>` 하나만 렌더한다. `href` prop 이 없다.
79
+
80
+ ```tsx
81
+ <Button href="/about">소개</Button> // ✗ 더 이상 없다
82
+ <LinkButton href="/about">소개</LinkButton> // ○
83
+ <Link href="/about">소개</Link> // ○ 본문 안 링크
84
+ ```
85
+
86
+ > ⚠ 커밋 `f5668dd` 에 나오는 **`buttonClass()` 는 쓰지 말 것.** 바로 다음 커밋
87
+ > `0a4ab14` 에서 `LinkButton` 으로 대체됐고 지금 소스에 없다(export 0건).
88
+ > 커밋을 순서대로 훑다 보면 유효한 경로로 오해하기 쉽다.
89
+
90
+ ## 0.8.0 → 0.8.1 — 오버레이가 항상 `document.body` 로 portal
91
+
92
+ **`17004e7`** Modal · Select · Dropdown · PopOver · TimePicker 의 팝업이 조상
93
+ DOM 이 아니라 항상 `body` 로 들어간다. 조상에 `transform`/`filter`/`will-change`/
94
+ `contain` 이 있으면 `position: fixed` 의 기준이 그 조상으로 바뀌어 오버레이가
95
+ 엉뚱한 자리에 뜨던 것을 고친 것이다.
96
+
97
+ **오버레이를 조상 기준으로 위치 잡는 CSS 가 있으면 깨진다.** 그런 CSS 는 지워야 한다 —
98
+ 위치는 DS 가 계산한다.
99
+
100
+ ## 0.8.1 → 0.8.2 — `modal-box` 에 `position: relative`
101
+
102
+ **`c3611ee`** 0.8.1 에서 모달의 `transform` 을 걷어내면서 모달 **안쪽**
103
+ `position: absolute` 요소들의 기준이 모달이 아니라 화면 전체(`.dim`)로 튀었다.
104
+ 그 회귀를 되돌린 것이다. 0.8.1 을 건너뛰고 0.8.2 이상으로 올리면 겪지 않는다.
105
+
106
+ ## 0.8.3 → 0.9.0 — 폼 검증 API 가 `validations` 로 통일
107
+
108
+ **`6e64fbc`** 컴포넌트마다 제각각이던 에러 표시 방식을 하나로 모았다.
109
+ **추가이므로 기존 코드를 깨지 않는다.** 다만 이제 직접 그릴 필요가 없다.
110
+
111
+ ```tsx
112
+ validations?: { status: "error" | "warning" | "success" | "default"; message: string }[]
113
+ ```
114
+
115
+ - 렌더는 `FieldMessage` 가 전담한다. 아이콘·색·간격이 DS 전체에서 같아진다.
116
+ - `aria-invalid` 도 DS 가 붙인다 — 직접 붙이던 코드는 걷어낼 것.
117
+ - `hasError(validations)` 가 배럴에서 나간다.
118
+ - `Input/InputValidations` 는 **삭제됐다.** 직접 import 하던 곳이 있으면 끊긴다.
119
+
120
+ `Select.error?: boolean` 은 하위호환으로 남아 있다. `validations` 와 OR 로
121
+ 합쳐지므로 둘 다 써도 되지만, 새로 쓰는 곳은 `validations` 를 쓴다.
122
+
123
+ ## 0.9.0 → 0.10.0 — `type` = 상태색 / `variant` = 모양 변형
124
+
125
+ **`12f4319`** 한 컴포넌트에서 `type` 이 색을 뜻하기도 하고 모양을 뜻하기도 하던
126
+ 것을 두 축으로 갈랐다. **별칭이 살아 있어 깨지지 않는다.**
127
+
128
+ | 축 | prop | 값 |
129
+ |---|---|---|
130
+ | 상태 색 | `type` | `primary` `success` `error` `warning` `info` |
131
+ | 모양 변형 | `variant` | 컴포넌트마다 다름 |
132
+
133
+ - 모양 변형을 `type` 으로 주던 곳(`Tooltip` `Chart` `Tab` `Tag`)은 `variant` 로
134
+ 옮기는 게 맞지만, `variant ?? type ?? 기본값` 으로 받으므로 **지금 동작한다.**
135
+ - `type="brand"` → `type="primary"`. `normalizeStatus()` 가 `brand` 를 계속 받는다.
136
+
137
+ **별칭 제거 시점은 아직 정해지지 않았다.** 업그레이드와 무관한 diff 를 섞기
138
+ 싫으면 나중에 한 번에 정리해도 된다. 제거가 정해지면 이 문서에 적는다.
139
+
140
+ ## 0.10.0 → 0.11.0 — 폼 컨트롤에 `label` / `required`
141
+
142
+ **`9f7700d` · 추가지만 기존 마크업과 겹칠 수 있다.**
143
+
144
+ `Field` 가 **진짜 `<label htmlFor>` 를 그린다.** 바깥에서 `<label>` 로 감싸던
145
+ 코드에 DS 의 `label` prop 을 같이 주면 `<label>` 안에 `<label>` 이 들어간다 —
146
+ 유효하지 않은 HTML 이고, 클릭 시 포커스가 어디로 갈지 브라우저마다 다르다.
147
+
148
+ > **「DS `label` 도입」과 「기존 `<label>` 제거」는 같은 커밋에 넣을 것.**
149
+ > 한쪽만 하면 중첩되거나 라벨이 사라진다.
150
+
151
+ 같이 걷어낼 것:
152
+
153
+ - `htmlFor` / `id` — DS 가 `React.useId()` 로 만들어 잇는다. 직접 넘길 필요 없다.
154
+ - `aria-invalid` — DS 가 붙인다.
155
+
156
+ `Field` 는 **붙일 게 없으면 래퍼를 만들지 않고 `children` 을 그대로 통과시킨다.**
157
+ 안 쓰는 곳의 DOM 은 늘지 않으므로 그대로 둬도 된다.
158
+
159
+ ## 0.11.0 → 0.12.0 — Editor 확충
160
+
161
+ **`7bf563f` · `97e668c` · 추가만 있다.** 되돌리기/다시하기, 표, 체크리스트,
162
+ 정렬, 문법 강조, 형광펜, 위/아래 첨자가 들어갔다.
163
+
164
+ **저장된 HTML 을 직접 다루는 쪽은 허용 태그 목록을 갱신해야 한다.**
165
+ 저장 값에 표·체크리스트·`mark`·`sub`/`sup` 이 새로 나타난다. 태그 목록을
166
+ 직접 들고 있다면(예: "이 값이 HTML 인가" 판별) 아래 **31개를 그대로** 복사할 것.
167
+
168
+ ```
169
+ p br div span
170
+ h1 h2 h3 h4 h5 h6
171
+ ul ol li
172
+ blockquote pre code
173
+ strong b em i u s strike del
174
+ a img hr
175
+ sub sup mark
176
+ table thead tbody tr th td
177
+ ```
178
+
179
+ 주의할 점 둘:
180
+
181
+ - **`b` `i` `u` `strike` 는 옛날 태그가 아니라 평상시 출력이다.** DS 는
182
+ `styleWithCSS` 를 켜지 않으므로 브라우저 `execCommand` 가 `<strong>` 이 아니라
183
+ `<b>` 를 낸다. 굵게 쓴 글이 흔하므로 이게 빠지면 자주 밟힌다.
184
+ - **`input` 은 필요 없다.** 체크리스트는 `<li data-checked>` + CSS `::before` 로
185
+ 그린다. `<input>` 은 한 번도 나오지 않는다.
186
+
187
+ 허용 목록은 `Editor.tsx` 안의 상수이고 prop 으로 열려 있지 않다. 버전을 올리면
188
+ 따라온다. `readOnly` 로 그릴 때도 같은 태그가 전부 나온다 — sanitize 경로와
189
+ 스타일이 편집 모드와 같다.
190
+
191
+ HTML 을 검색 색인에 넣는다면 **닫는 태그를 공백이나 줄바꿈으로 치환**할 것.
192
+ `</td>` `</th>` `</tr>` `</li>` `</p>` `</h1>`~`</h6>` `</blockquote>` `<br>` 을
193
+ 그냥 지우면 칸 글자가 붙어 `가격수량` 같은 낱말이 생긴다.
194
+
195
+ ## 0.12.0 → 0.13.0 — `FileUpload` / `ImageSelector` 의 `label` 뜻이 바뀜
196
+
197
+ **깨진다. 그런데 타입 오류가 나지 않는다 — 글자 위치만 바뀐다.**
198
+
199
+ 이 둘의 `label` 은 DS 안에서 혼자 다른 뜻이었다. 다른 폼 컨트롤에서는 컨트롤
200
+ **위**에 붙는 필드 라벨인데, 여기서는 상자 **안**에 찍히는 안내 문구였다.
201
+ 그 문구를 `placeholder` 로 옮기고 `label` 을 필드 라벨로 되돌렸다.
202
+
203
+ ```tsx
204
+ // 0.12.0 이하
205
+ <FileUpload label="파일을 드래그하거나 클릭하여 업로드" />
206
+
207
+ // 0.13.0 — 같은 화면을 얻으려면
208
+ <FileUpload placeholder="파일을 드래그하거나 클릭하여 업로드" />
209
+
210
+ // 이제 가능해진 것
211
+ <FileUpload label="첨부파일" required placeholder="끌어다 놓으세요" />
212
+ ```
213
+
214
+ `label` 을 그대로 두면 **그 글자가 상자 안이 아니라 상자 위 라벨로 올라간다.**
215
+ `label?: string` 에서 `label?: React.ReactNode` 로 넓어졌으므로 기존 문자열도
216
+ 타입은 통과한다. `grep -rn 'FileUpload\|ImageSelector'` 로 훑어 `label=` 이 있는
217
+ 곳을 `placeholder=` 로 바꾸면 된다.
218
+
219
+ 같이 들어간 것:
220
+
221
+ - `required` — 라벨 뒤에 `*`
222
+ - `htmlFor` 연결 — 라벨을 클릭하면 파일 선택창이 열린다
223
+ - **`ImageSelector` 의 하드코딩된 `id="image-input"` 제거.** 한 화면에 둘 이상
224
+ 놓으면 DOM id 가 겹치던 버그였다. 이제 `React.useId()` 로 만든다.
225
+ `#image-input` 을 잡는 CSS 나 테스트 셀렉터가 있으면 끊긴다.
226
+
227
+ ### 같은 릴리스: DatePicker 3종에 `label` / `required` / `validations`
228
+
229
+ **추가만 있다.** `SingleDatePicker` · `RangeDatePicker` · `InputDatePicker` 가
230
+ 다른 폼 컨트롤과 같은 방식으로 라벨·검증을 받는다.
231
+
232
+ `PopupDatePicker` 와 `Dropdown` 은 받지 않는다 — 전자는 트리거를 `component` 로
233
+ 직접 넘기는 구조라 라벨이 가리킬 대상이 없고, 후자는 `role="menu"` 인 액션
234
+ 메뉴라 검증할 값이 없다.
235
+
236
+ ---
237
+
238
+ ## 실제 이관에서 걸린 것 / 안 걸린 것
239
+
240
+ `xplat-shop-templat` (pnpm 모노레포, 워크스페이스 4개) 을 **0.6.1 → 0.12.0** 으로
241
+ 올렸을 때의 결과다. DS 를 어댑터 한 곳에서만 쓰는 구조였다.
242
+
243
+ | 항목 | 결과 | 이유 |
244
+ |---|---|---|
245
+ | `Button` href 제거 | 안 걸림 | `<Button href>` 사용 0곳 |
246
+ | `Badge` → `NotificationBadge` | 안 걸림 | `@deprecated` 별칭으로 통과 |
247
+ | CSS 클래스 rename | 안 걸림 | `.lib-xplat-*` 을 덮어쓰는 CSS 0곳 |
248
+ | 오버레이 body portal | 안 걸림 | 오버레이 기준을 잡는 CSS 없음 |
249
+ | **`label` 중첩** | **걸림** | 어댑터가 `<label htmlFor>` 를 직접 그리고 있었다 |
250
+
251
+ **목록을 훑기 전에 자기 저장소에서 먼저 세어 볼 것.** 위 표에서 보듯 대부분은
252
+ 해당 사항이 없고, 진짜 걸리는 건 한둘이다. `<Button href`, `.lib-xplat-`,
253
+ `<label` 세 가지를 grep 하는 것으로 대부분 판별된다.
254
+
255
+ 결과: 어댑터 241줄 → 216줄, 쓰이지 않던 CSS 30줄 삭제. DS 가 `label`·
256
+ `aria-invalid`·`useId`·검증 메시지를 맡으면서 어댑터가 얇아진다.
257
+
258
+ ---
259
+
260
+ ## 아직 확인되지 않은 것
261
+
262
+ **Editor 는 브라우저에서 검증되지 않았다.** `contentEditable`·`execCommand`·IME 는
263
+ 자동 테스트로 흉내 낼 수 없다. 특히 아래 넷은 직접 눌러 봐야 한다.
264
+
265
+ - 한글 조합 중 `⌘Z` / `Ctrl+Z`
266
+ - 툴바 서식 버튼
267
+ - 표 안에서 Enter · Backspace
268
+ - 체크박스 클릭 판정
269
+
270
+ 이상이 보이면 재현 조건과 함께 알려 주면 좋겠다.
@@ -50,6 +50,32 @@ Input + 드롭다운 형태이다.
50
50
  />
51
51
  ```
52
52
 
53
+ ## 라벨과 검증
54
+
55
+ `SingleDatePicker` · `RangeDatePicker` · `InputDatePicker` 는 다른 폼 컨트롤과
56
+ 똑같이 `label` · `required` · `validations` 를 받는다.
57
+
58
+ ```tsx
59
+ <InputDatePicker
60
+ value={date}
61
+ onChange={setDate}
62
+ label="계약일"
63
+ required
64
+ validations={[{ status: "error", message: "계약일을 선택하세요" }]}
65
+ />
66
+ ```
67
+
68
+ 검증 메시지는 `FieldMessage` 가 그린다 — 아이콘·색·간격이 Input·Select 와 같다.
69
+ 직접 `<p>` 로 그리지 말 것.
70
+
71
+ 달력을 펼쳐 놓는 `SingleDatePicker` · `RangeDatePicker` 는 **포커스 대상이 날짜
72
+ 칸마다 있어 `htmlFor` 로 하나를 고를 수 없다.** 그래서 `role="group"` +
73
+ `aria-labelledby` 로 라벨과 묶는다. 쓰는 쪽에서 신경 쓸 것은 없다.
74
+
75
+ `PopupDatePicker` 는 라벨·검증을 받지 않는다 — 트리거를 `component` 로 직접
76
+ 넘기는 구조라 라벨이 가리킬 대상이 정해지지 않는다. 라벨이 필요하면 트리거로
77
+ `InputDatePicker` 를 쓰거나 바깥에서 `Field` 로 감싼다.
78
+
53
79
  ---
54
80
 
55
81
  ## 의사결정