@uniai-fe/uds-primitives 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +9 -0
  2. package/dist/styles.css +165 -2
  3. package/package.json +3 -3
  4. package/src/components/chip/index.tsx +2 -1
  5. package/src/components/chip/markup/{Chip.tsx → foundation/Chip.tsx} +21 -4
  6. package/src/components/chip/markup/{Label.tsx → foundation/Label.tsx} +2 -2
  7. package/src/components/chip/markup/{ListRoot.tsx → foundation/ListRoot.tsx} +20 -4
  8. package/src/components/chip/markup/{RemoveButton.tsx → foundation/RemoveButton.tsx} +2 -2
  9. package/src/components/chip/markup/index.tsx +9 -6
  10. package/src/components/chip/markup/{DefaultStyle.tsx → templates/DefaultStyle.tsx} +2 -2
  11. package/src/components/chip/markup/{InputStyle.tsx → templates/InputStyle.tsx} +3 -3
  12. package/src/components/chip/markup/templates/TextStyle.tsx +69 -0
  13. package/src/components/chip/styles/chip.scss +28 -2
  14. package/src/components/chip/styles/variables.scss +6 -0
  15. package/src/components/chip/types/options.ts +3 -2
  16. package/src/components/chip/types/props.ts +1 -1
  17. package/src/components/toast/img/error.svg +6 -0
  18. package/src/components/toast/img/success.svg +5 -0
  19. package/src/components/toast/img/warning.svg +5 -0
  20. package/src/components/toast/index.scss +1 -0
  21. package/src/components/toast/index.tsx +11 -0
  22. package/src/components/toast/markup/Host.tsx +74 -0
  23. package/src/components/toast/markup/Icon.tsx +15 -0
  24. package/src/components/toast/markup/Item.tsx +100 -0
  25. package/src/components/toast/markup/Text.tsx +21 -0
  26. package/src/components/toast/markup/index.tsx +16 -0
  27. package/src/components/toast/styles/index.scss +2 -0
  28. package/src/components/toast/styles/toast.scss +113 -0
  29. package/src/components/toast/styles/variables.scss +24 -0
  30. package/src/components/toast/types/index.ts +1 -0
  31. package/src/components/toast/types/internal.ts +71 -0
  32. package/src/components/toast/types/props.ts +128 -0
  33. package/src/index.scss +1 -0
  34. package/src/index.tsx +1 -0
package/README.md CHANGED
@@ -194,6 +194,15 @@ export default function Page() {
194
194
  - `TableRootProps`
195
195
  - `TableContainerProps`
196
196
  - `TableCellProps`
197
+ - `Toast.Host`
198
+ - `Toast.Item`
199
+ - `ToastIcon`
200
+ - `ToastItemData`
201
+ - `ToastHostProps`
202
+ - `ToastItemProps`
203
+ - `ToastState`
204
+ - `ToastHorizontal`
205
+ - `ToastVertical`
197
206
  - `Button.Default`
198
207
  - `Button.Text`
199
208
  - `Button.Rounded`
package/dist/styles.css CHANGED
@@ -223,7 +223,11 @@
223
223
  --theme-checkbox-icon-disabled-selected: var(--color-common-100);
224
224
  --theme-checkbox-focus-ring: rgba(2, 84, 255, 0.32);
225
225
  --theme-chip-height: var(--theme-size-small-3);
226
+ /* 변경: Figma text menu item은 기본 chip보다 낮은 24px height를 사용한다. */
227
+ --theme-chip-height-text: 24px;
226
228
  --theme-chip-padding-horizontal: var(--spacing-padding-5);
229
+ /* 변경: Figma text menu item은 8px horizontal padding을 사용한다. */
230
+ --theme-chip-padding-horizontal-text: var(--spacing-padding-4);
227
231
  --theme-chip-gap: var(--spacing-gap-1);
228
232
  --theme-chip-assist-gap: var(--spacing-gap-2);
229
233
  --theme-chip-input-gap: var(--spacing-gap-1);
@@ -250,6 +254,8 @@
250
254
  --theme-chip-font-family: var(--font-body-medium-family);
251
255
  --theme-chip-font-size: var(--font-body-xsmall-size);
252
256
  --theme-chip-font-weight: var(--font-body-xsmall-weight);
257
+ /* 변경: text selected chip은 Figma semibold 상태를 foundation typography weight로 매핑한다. */
258
+ --theme-chip-text-font-weight-selected: var(--font-caption-large-weight);
253
259
  --divider-width: 1px;
254
260
  --divider-height: 12px;
255
261
  --divider-color: var(--color-border-standard-cool-gray, #e4e5e7);
@@ -773,6 +779,26 @@
773
779
  --table-td-text-size: 15px;
774
780
  --table-td-text-weight: var(--font-body-xsmall-weight);
775
781
  --table-td-text-line-height: var(--font-body-xsmall-line-height);
782
+ --toast-stack-margin: var(--spacing-padding-7);
783
+ --toast-stack-gap: var(--spacing-gap-3);
784
+ --toast-stack-z-index: 10000;
785
+ --toast-width: 361px;
786
+ --toast-background-color: var(--color-surface-heavy);
787
+ --toast-foreground-color: var(--color-common-100);
788
+ --toast-radius: var(--theme-radius-large-1);
789
+ --toast-padding-block: var(--spacing-padding-5);
790
+ --toast-padding-inline: var(--spacing-padding-7);
791
+ --toast-gap: var(--spacing-gap-5);
792
+ --toast-icon-size: 24px;
793
+ --toast-transition-duration: 0.6s;
794
+ --toast-transition-easing: cubic-bezier(0.16, 1, 0.3, 1);
795
+ --toast-transition-distance: -24%;
796
+ --toast-transition-x: 0;
797
+ --toast-transition-y: 0;
798
+ --toast-message-font-size: var(--font-body-xsmall-size);
799
+ --toast-message-line-height: var(--font-body-xsmall-line-height);
800
+ --toast-message-letter-spacing: var(--font-body-xsmall-letter-spacing);
801
+ --toast-message-font-weight: var(--font-body-xsmall-weight);
776
802
  --tooltip-message-background: var(--color-cool-gray-20);
777
803
  --tooltip-message-foreground: var(--color-common-100);
778
804
  --tooltip-message-radius: var(--theme-radius-medium-3);
@@ -1976,6 +2002,7 @@
1976
2002
 
1977
2003
  .chip {
1978
2004
  --chip-current-height: var(--theme-chip-height);
2005
+ --chip-padding-horizontal: var(--theme-chip-padding-horizontal);
1979
2006
  --chip-gap: var(--theme-chip-gap);
1980
2007
  --chip-bg: transparent;
1981
2008
  --chip-border-color: transparent;
@@ -1985,8 +2012,8 @@
1985
2012
  justify-content: center;
1986
2013
  gap: var(--chip-gap);
1987
2014
  height: var(--chip-current-height);
1988
- padding-left: var(--theme-chip-padding-horizontal);
1989
- padding-right: var(--theme-chip-padding-horizontal);
2015
+ padding-left: var(--chip-padding-horizontal);
2016
+ padding-right: var(--chip-padding-horizontal);
1990
2017
  padding-block: 0;
1991
2018
  border-radius: var(--theme-chip-radius);
1992
2019
  border: 1px solid var(--chip-border-color);
@@ -2036,6 +2063,27 @@ figure.chip {
2036
2063
  --chip-border-color: transparent;
2037
2064
  }
2038
2065
 
2066
+ .chip:where([data-style=text]) {
2067
+ --chip-current-height: var(--theme-chip-height-text);
2068
+ --chip-padding-horizontal: var(--theme-chip-padding-horizontal-text);
2069
+ --chip-bg: transparent;
2070
+ --chip-border-color: transparent;
2071
+ --chip-label-color: var(--color-label-standard);
2072
+ }
2073
+
2074
+ .chip:where([data-style=text]) .chip-label {
2075
+ text-decoration: none;
2076
+ }
2077
+
2078
+ .chip:where([data-style=text][data-selected=true]) {
2079
+ --chip-bg: var(--color-surface-standard);
2080
+ font-weight: var(--theme-chip-text-font-weight-selected);
2081
+ }
2082
+
2083
+ .chip:where([data-style=text][data-selected=true]) .chip-label {
2084
+ text-decoration: underline;
2085
+ }
2086
+
2039
2087
  .chip:where([data-style][data-rounded=true]) {
2040
2088
  border-radius: calc(var(--chip-current-height) / 2);
2041
2089
  }
@@ -2089,6 +2137,10 @@ figure.chip {
2089
2137
  line-height: var(--theme-chip-line-height-table);
2090
2138
  }
2091
2139
 
2140
+ .chip:where([data-size=table][data-style=text][data-selected=true]) {
2141
+ font-weight: var(--theme-chip-text-font-weight-selected);
2142
+ }
2143
+
2092
2144
  .chip:where([data-size=table][data-style=input]) {
2093
2145
  padding-left: var(--theme-chip-padding-left-table);
2094
2146
  padding-right: var(--theme-chip-padding-right-table);
@@ -5065,6 +5117,117 @@ figure.chip {
5065
5117
 
5066
5118
 
5067
5119
 
5120
+ .toast-stack {
5121
+ --toast-stack-translate-x: 0;
5122
+ --toast-stack-translate-y: 0;
5123
+ position: fixed;
5124
+ z-index: var(--toast-stack-z-index);
5125
+ display: flex;
5126
+ flex-direction: column;
5127
+ gap: var(--toast-stack-gap);
5128
+ pointer-events: none;
5129
+ transform: translate(var(--toast-stack-translate-x), var(--toast-stack-translate-y));
5130
+ }
5131
+
5132
+ .toast-stack:where([data-x=left]) {
5133
+ left: var(--toast-stack-margin);
5134
+ --toast-transition-x: calc(var(--toast-transition-distance) * -1);
5135
+ }
5136
+
5137
+ .toast-stack:where([data-x=center]) {
5138
+ left: 50%;
5139
+ --toast-stack-translate-x: -50%;
5140
+ --toast-transition-distance: -80%;
5141
+ }
5142
+
5143
+ .toast-stack:where([data-x=right]) {
5144
+ right: var(--toast-stack-margin);
5145
+ --toast-transition-x: var(--toast-transition-distance);
5146
+ }
5147
+
5148
+ .toast-stack:where([data-y=top]) {
5149
+ top: var(--toast-stack-margin);
5150
+ }
5151
+
5152
+ .toast-stack:where([data-y=center]) {
5153
+ top: 50%;
5154
+ --toast-stack-translate-y: -50%;
5155
+ }
5156
+
5157
+ .toast-stack:where([data-y=bottom]) {
5158
+ bottom: var(--toast-stack-margin);
5159
+ }
5160
+
5161
+ .toast-stack:where([data-x=center][data-y=top]) {
5162
+ --toast-transition-y: calc(var(--toast-transition-distance) * -1);
5163
+ }
5164
+
5165
+ .toast-stack:where([data-x=center][data-y=bottom]) {
5166
+ --toast-transition-y: var(--toast-transition-distance);
5167
+ }
5168
+
5169
+ .toast {
5170
+ display: flex;
5171
+ align-items: center;
5172
+ width: min(var(--toast-width), 100vw - var(--toast-stack-margin) * 2);
5173
+ box-sizing: border-box;
5174
+ gap: var(--toast-gap);
5175
+ overflow: hidden;
5176
+ padding: var(--toast-padding-block) var(--toast-padding-inline);
5177
+ border-radius: var(--toast-radius);
5178
+ background-color: var(--toast-background-color);
5179
+ opacity: 0;
5180
+ pointer-events: auto;
5181
+ transform: translate(var(--toast-transition-x), var(--toast-transition-y));
5182
+ transition: opacity var(--toast-transition-duration) var(--toast-transition-easing), transform var(--toast-transition-duration) var(--toast-transition-easing);
5183
+ }
5184
+
5185
+ .toast:where([data-phase=open]) {
5186
+ opacity: 1;
5187
+ transform: translateY(0);
5188
+ }
5189
+
5190
+ .toast-icon {
5191
+ display: flex;
5192
+ align-items: center;
5193
+ justify-content: center;
5194
+ flex-shrink: 0;
5195
+ width: var(--toast-icon-size);
5196
+ height: var(--toast-icon-size);
5197
+ margin: 0;
5198
+ }
5199
+
5200
+ .toast-icon svg {
5201
+ display: block;
5202
+ width: 100%;
5203
+ height: 100%;
5204
+ }
5205
+
5206
+ .toast-content {
5207
+ display: flex;
5208
+ flex: 1;
5209
+ min-width: 0;
5210
+ flex-direction: column;
5211
+ }
5212
+
5213
+ .toast-text {
5214
+ overflow: hidden;
5215
+ margin: 0;
5216
+ color: var(--toast-foreground-color);
5217
+ font-size: var(--toast-message-font-size);
5218
+ line-height: var(--toast-message-line-height);
5219
+ letter-spacing: var(--toast-message-letter-spacing);
5220
+ font-weight: var(--toast-message-font-weight);
5221
+ text-overflow: ellipsis;
5222
+ white-space: nowrap;
5223
+ }
5224
+
5225
+ .toast-description {
5226
+ opacity: 0.82;
5227
+ }
5228
+
5229
+
5230
+
5068
5231
  .tooltip-trigger {
5069
5232
  display: inline-flex;
5070
5233
  align-items: center;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -102,9 +102,9 @@
102
102
  "@uniai-fe/uds-foundation": "workspace:*",
103
103
  "@uniai-fe/util-functions": "workspace:*",
104
104
  "eslint": "^9.39.2",
105
- "prettier": "^3.8.2",
105
+ "prettier": "^3.8.3",
106
106
  "react-hook-form": "^7.72.1",
107
107
  "sass": "^1.99.0",
108
- "typescript": "~5.9.3"
108
+ "typescript": "5.9.3"
109
109
  }
110
110
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Chip; filter/assist/input chip 카테고리 배럴
2
+ * Chip; filter/assist/input/text chip 카테고리 배럴
3
3
  * @desc
4
4
  * - `Chip.Default`: style 분기 루트 컴포넌트다.
5
5
  * - `Chip.ClickableStyle`: filter/assist button 렌더 전용 leaf다.
6
6
  * - `Chip.InputStyle`: input figure 렌더 전용 leaf다.
7
+ * - `Chip.TextStyle`: text button 렌더 전용 leaf다.
7
8
  * - `Chip.List`: chip 목록 템플릿이다.
8
9
  * - `ChipProps`, `ChipListRootProps`, `ChipListItemData`: public contract 타입이다.
9
10
  */
@@ -1,18 +1,23 @@
1
1
  import { forwardRef, type Ref } from "react";
2
2
  import clsx from "clsx";
3
- import type { ChipInputProps, ChipProps } from "../types";
4
- import ChipClickableStyle from "./DefaultStyle";
5
- import ChipInputStyle from "./InputStyle";
3
+ import type { ChipInputProps, ChipProps } from "../../types";
4
+ import ChipClickableStyle from "../templates/DefaultStyle";
5
+ import ChipInputStyle from "../templates/InputStyle";
6
+ import ChipTextStyle from "../templates/TextStyle";
6
7
 
7
8
  // chipStyle === "input" 조합에서 props를 ChipInputProps로 좁히기 위한 type guard.
8
9
  const isChipInputProps = (props: ChipProps): props is ChipInputProps =>
9
10
  props.chipStyle === "input";
10
11
 
12
+ // chipStyle === "text" 조합에서 text leaf 분기를 고정하기 위한 type guard.
13
+ const isChipTextProps = (props: ChipProps): boolean =>
14
+ props.chipStyle === "text";
15
+
11
16
  /**
12
17
  * Chip Markup; Style별 렌더 분기 Orchestrator 컴포넌트
13
18
  * @component
14
19
  * @param {ChipProps} props Chip 공통/종류별 props
15
- * @param {"filter" | "assist" | "input"} [props.chipStyle="filter"] Chip 스타일
20
+ * @param {"filter" | "assist" | "input" | "text"} [props.chipStyle="filter"] Chip 스타일
16
21
  * @param {"solid" | "outlined"} [props.fill="solid"] Chip 채움 스타일
17
22
  * @param {boolean} [props.rounded=false] pill radius 적용 여부
18
23
  * @param {React.ReactNode} [props.children] Chip 라벨/콘텐츠
@@ -29,6 +34,7 @@ const isChipInputProps = (props: ChipProps): props is ChipInputProps =>
29
34
  * @param {string} [props.title] root title
30
35
  * @description
31
36
  * - `chipStyle="input"`이면 `<figure>`를 렌더하고, 나머지 스타일은 `<button>`을 렌더한다.
37
+ * - `chipStyle="text"`일 때는 menu item 전용 text leaf를 렌더한다.
32
38
  * - `chipStyle="input"`일 때 remove 버튼 클릭 이벤트는 root click으로 버블링되지 않도록 내부에서 차단한다.
33
39
  * - native HTML attrs는 style에 맞는 root 요소로 그대로 전달된다.
34
40
  * @returns {JSX.Element} style에 맞는 Chip root 요소
@@ -56,6 +62,17 @@ const ChipDefault = forwardRef<HTMLElement, ChipProps>((props, ref) => {
56
62
  );
57
63
  }
58
64
 
65
+ if (isChipTextProps(props)) {
66
+ return (
67
+ <ChipTextStyle
68
+ {...props}
69
+ // 변경: text leaf도 root 역할 className을 동일하게 누적한다.
70
+ className={mergedClassName}
71
+ ref={ref as Ref<HTMLButtonElement>}
72
+ />
73
+ );
74
+ }
75
+
59
76
  return (
60
77
  // 변경: 역할 className을 루트 컴포넌트에서 누적 전달한다.
61
78
  // 변경: interactive 경로도 forwardRef 기반 ref 전달로 통일한다.
@@ -1,6 +1,6 @@
1
1
  import clsx from "clsx";
2
- import { Slot } from "../../slot";
3
- import type { ChipLabelProps } from "../types/props";
2
+ import { Slot } from "../../../slot";
3
+ import type { ChipLabelProps } from "../../types/props";
4
4
 
5
5
  /**
6
6
  * Chip Label; Chip 라벨 텍스트 렌더 컴포넌트.
@@ -1,15 +1,16 @@
1
1
  import { type CSSProperties } from "react";
2
2
  import clsx from "clsx";
3
- import type { ChipListRootProps } from "../types";
4
- import ChipClickableStyle from "./DefaultStyle";
5
- import ChipInputStyle from "./InputStyle";
3
+ import type { ChipListRootProps } from "../../types";
4
+ import ChipClickableStyle from "../templates/DefaultStyle";
5
+ import ChipInputStyle from "../templates/InputStyle";
6
+ import ChipTextStyle from "../templates/TextStyle";
6
7
 
7
8
  /**
8
9
  * Chip Markup; List Root 템플릿 컴포넌트
9
10
  * @component
10
11
  * @param {ChipListRootProps<OptionData>} props list root props
11
12
  * @param {ChipListItemData<OptionData>[]} props.items 렌더링할 chip 엔트리 목록
12
- * @param {"filter" | "assist" | "input"} [props.chipStyle="filter"] item 공통 chip 스타일
13
+ * @param {"filter" | "assist" | "input" | "text"} [props.chipStyle="filter"] item 공통 chip 스타일
13
14
  * @param {"solid" | "outlined"} [props.fill="solid"] item 공통 chip 채움 스타일
14
15
  * @param {boolean} [props.rounded=false] item 공통 pill radius 적용 여부
15
16
  * @param {"default" | "table"} [props.size="default"] item 공통 chip 사이즈 축
@@ -35,6 +36,7 @@ function ChipListRoot<OptionData = unknown>({
35
36
  style,
36
37
  ...restProps
37
38
  }: ChipListRootProps<OptionData>) {
39
+ // 변경: text list는 title/header 없이 item row만 담당하고, 외부에서 헤더를 조합한다.
38
40
  const mergedStyle = {
39
41
  ...style,
40
42
  ...(typeof gap === "undefined" ? {} : { gap }),
@@ -64,6 +66,20 @@ function ChipListRoot<OptionData = unknown>({
64
66
  >
65
67
  {item.label}
66
68
  </ChipInputStyle>
69
+ ) : chipStyle === "text" ? (
70
+ <ChipTextStyle
71
+ chipStyle="text"
72
+ fill={fill}
73
+ rounded={rounded}
74
+ size={size}
75
+ selected={item.selected}
76
+ disabled={item.disabled}
77
+ title={item.title}
78
+ className={clsx("chip-list-chip", item.className)}
79
+ onClick={item.onClick}
80
+ >
81
+ {item.label}
82
+ </ChipTextStyle>
67
83
  ) : (
68
84
  <ChipClickableStyle
69
85
  chipStyle={chipStyle}
@@ -1,6 +1,6 @@
1
1
  import clsx from "clsx";
2
- import RemoveIcon from "../img/remove.svg";
3
- import type { ChipRemoveButtonProps } from "../types";
2
+ import RemoveIcon from "../../img/remove.svg";
3
+ import type { ChipRemoveButtonProps } from "../../types";
4
4
 
5
5
  /**
6
6
  * Chip Markup; Input remove 버튼 컴포넌트
@@ -1,15 +1,17 @@
1
- import { ChipDefault } from "./Chip";
2
- import ChipClickableStyle from "./DefaultStyle";
3
- import ChipInputStyle from "./InputStyle";
4
- import ChipLabel from "./Label";
5
- import { ChipListRoot } from "./ListRoot";
1
+ import { ChipDefault } from "./foundation/Chip";
2
+ import ChipClickableStyle from "./templates/DefaultStyle";
3
+ import ChipInputStyle from "./templates/InputStyle";
4
+ import ChipTextStyle from "./templates/TextStyle";
5
+ import ChipLabel from "./foundation/Label";
6
+ import { ChipListRoot } from "./foundation/ListRoot";
6
7
 
7
8
  /**
8
- * Chip; filter/assist/input namespace
9
+ * Chip; filter/assist/input/text namespace
9
10
  * @desc
10
11
  * - `Chip.Default`: style 분기 루트 컴포넌트다.
11
12
  * - `Chip.ClickableStyle`: filter/assist button 렌더 전용 leaf다.
12
13
  * - `Chip.InputStyle`: input figure 렌더 전용 leaf다.
14
+ * - `Chip.TextStyle`: text button 렌더 전용 leaf다.
13
15
  * - `Chip.Label`: chip 텍스트 슬롯이다.
14
16
  * - `Chip.List`: chip 목록 템플릿이다.
15
17
  */
@@ -17,6 +19,7 @@ export const Chip = {
17
19
  Default: ChipDefault,
18
20
  ClickableStyle: ChipClickableStyle,
19
21
  InputStyle: ChipInputStyle,
22
+ TextStyle: ChipTextStyle,
20
23
  Label: ChipLabel,
21
24
  List: ChipListRoot,
22
25
  };
@@ -1,7 +1,7 @@
1
1
  import { forwardRef } from "react";
2
2
  import clsx from "clsx";
3
- import type { ChipClickableStyleComponentProps } from "../types";
4
- import ChipLabel from "./Label";
3
+ import type { ChipClickableStyleComponentProps } from "../../types";
4
+ import ChipLabel from "../foundation/Label";
5
5
 
6
6
  /**
7
7
  * Chip Markup; Default/Interactive 스타일 렌더 컴포넌트
@@ -1,9 +1,9 @@
1
1
  import { forwardRef } from "react";
2
2
  import clsx from "clsx";
3
3
  import type { MouseEvent } from "react";
4
- import type { ChipInputStyleProps } from "../types";
5
- import ChipRemoveButton from "./RemoveButton";
6
- import ChipLabel from "./Label";
4
+ import type { ChipInputStyleProps } from "../../types";
5
+ import ChipRemoveButton from "../foundation/RemoveButton";
6
+ import ChipLabel from "../foundation/Label";
7
7
 
8
8
  /**
9
9
  * Chip Markup; Input 스타일 렌더 컴포넌트
@@ -0,0 +1,69 @@
1
+ import { forwardRef } from "react";
2
+ import clsx from "clsx";
3
+ import type { ChipClickableStyleComponentProps } from "../../types";
4
+ import ChipLabel from "../foundation/Label";
5
+
6
+ /**
7
+ * Chip Markup; Text 스타일 렌더 컴포넌트
8
+ * @component
9
+ * @param {ChipClickableStyleComponentProps} props text 스타일 props
10
+ * @param {Ref<HTMLButtonElement>} ref button ref
11
+ * @param {React.ReactNode} [props.children] chip 라벨 콘텐츠
12
+ * @param {boolean} [props.selected] 선택 상태
13
+ * @param {string} [props.className] root className
14
+ * @param {"text"} [props.chipStyle] text style
15
+ * @param {"default" | "table"} [props.size] size 축
16
+ * @param {"solid" | "outlined"} [props.fill] 채움 스타일
17
+ * @param {boolean} [props.rounded] pill radius 적용 여부
18
+ * @param {"button" | "submit" | "reset"} [props.type] button type
19
+ * @example
20
+ * <ChipTextStyle chipStyle="text" selected ref={buttonRef}>
21
+ * 내용
22
+ * </ChipTextStyle>
23
+ */
24
+ const ChipTextStyle = forwardRef<
25
+ HTMLButtonElement,
26
+ ChipClickableStyleComponentProps
27
+ >(
28
+ (
29
+ {
30
+ children,
31
+ selected,
32
+ className,
33
+ chipStyle: _chipStyle,
34
+ leading: _leading,
35
+ size = "default",
36
+ fill = "solid",
37
+ rounded = false,
38
+ type,
39
+ ...restProps
40
+ },
41
+ ref,
42
+ ) => {
43
+ void _chipStyle;
44
+ void _leading;
45
+
46
+ return (
47
+ <button
48
+ {...restProps}
49
+ // 변경: text chip도 기존 interactive 계열과 동일한 forwardRef/button 계약을 유지한다.
50
+ ref={ref}
51
+ type={type ?? "button"}
52
+ className={clsx("chip", "chip-text-style", "chip-text-root", className)}
53
+ data-style="text"
54
+ data-fill={fill}
55
+ data-rounded={rounded ? "true" : undefined}
56
+ data-size={size}
57
+ data-selected={selected ? "true" : undefined}
58
+ aria-pressed={typeof selected === "boolean" ? selected : undefined}
59
+ >
60
+ {/* 변경: text style은 Figma 기준으로 leading slot을 열지 않고 label만 렌더한다. */}
61
+ <ChipLabel className="chip-text-label">{children}</ChipLabel>
62
+ </button>
63
+ );
64
+ },
65
+ );
66
+
67
+ ChipTextStyle.displayName = "ChipTextStyle";
68
+
69
+ export default ChipTextStyle;
@@ -1,5 +1,6 @@
1
1
  .chip {
2
2
  --chip-current-height: var(--theme-chip-height);
3
+ --chip-padding-horizontal: var(--theme-chip-padding-horizontal);
3
4
  --chip-gap: var(--theme-chip-gap);
4
5
  --chip-bg: transparent;
5
6
  --chip-border-color: transparent;
@@ -9,8 +10,8 @@
9
10
  justify-content: center;
10
11
  gap: var(--chip-gap);
11
12
  height: var(--chip-current-height);
12
- padding-left: var(--theme-chip-padding-horizontal);
13
- padding-right: var(--theme-chip-padding-horizontal);
13
+ padding-left: var(--chip-padding-horizontal);
14
+ padding-right: var(--chip-padding-horizontal);
14
15
  padding-block: 0;
15
16
  border-radius: var(--theme-chip-radius);
16
17
  border: 1px solid var(--chip-border-color);
@@ -64,6 +65,27 @@ figure.chip {
64
65
  --chip-border-color: transparent;
65
66
  }
66
67
 
68
+ .chip:where([data-style="text"]) {
69
+ --chip-current-height: var(--theme-chip-height-text);
70
+ --chip-padding-horizontal: var(--theme-chip-padding-horizontal-text);
71
+ --chip-bg: transparent;
72
+ --chip-border-color: transparent;
73
+ --chip-label-color: var(--color-label-standard);
74
+ }
75
+
76
+ .chip:where([data-style="text"]) .chip-label {
77
+ text-decoration: none;
78
+ }
79
+
80
+ .chip:where([data-style="text"][data-selected="true"]) {
81
+ --chip-bg: var(--color-surface-standard);
82
+ font-weight: var(--theme-chip-text-font-weight-selected);
83
+ }
84
+
85
+ .chip:where([data-style="text"][data-selected="true"]) .chip-label {
86
+ text-decoration: underline;
87
+ }
88
+
67
89
  .chip:where([data-style][data-rounded="true"]) {
68
90
  border-radius: calc(var(--chip-current-height) / 2);
69
91
  }
@@ -118,6 +140,10 @@ figure.chip {
118
140
  line-height: var(--theme-chip-line-height-table);
119
141
  }
120
142
 
143
+ .chip:where([data-size="table"][data-style="text"][data-selected="true"]) {
144
+ font-weight: var(--theme-chip-text-font-weight-selected);
145
+ }
146
+
121
147
  .chip:where([data-size="table"][data-style="input"]) {
122
148
  padding-left: var(--theme-chip-padding-left-table);
123
149
  padding-right: var(--theme-chip-padding-right-table);
@@ -1,7 +1,11 @@
1
1
  /* Chip 기본 토큰 래핑 */
2
2
  :root {
3
3
  --theme-chip-height: var(--theme-size-small-3);
4
+ /* 변경: Figma text menu item은 기본 chip보다 낮은 24px height를 사용한다. */
5
+ --theme-chip-height-text: 24px;
4
6
  --theme-chip-padding-horizontal: var(--spacing-padding-5);
7
+ /* 변경: Figma text menu item은 8px horizontal padding을 사용한다. */
8
+ --theme-chip-padding-horizontal-text: var(--spacing-padding-4);
5
9
  --theme-chip-gap: var(--spacing-gap-1);
6
10
  --theme-chip-assist-gap: var(--spacing-gap-2);
7
11
  --theme-chip-input-gap: var(--spacing-gap-1);
@@ -28,4 +32,6 @@
28
32
  --theme-chip-font-family: var(--font-body-medium-family);
29
33
  --theme-chip-font-size: var(--font-body-xsmall-size);
30
34
  --theme-chip-font-weight: var(--font-body-xsmall-weight);
35
+ /* 변경: text selected chip은 Figma semibold 상태를 foundation typography weight로 매핑한다. */
36
+ --theme-chip-text-font-weight-selected: var(--font-caption-large-weight);
31
37
  }
@@ -4,8 +4,9 @@
4
4
  * - filter
5
5
  * - assist
6
6
  * - input
7
+ * - text
7
8
  */
8
- export const CHIP_STYLES = ["filter", "assist", "input"] as const;
9
+ export const CHIP_STYLES = ["filter", "assist", "input", "text"] as const;
9
10
 
10
11
  /**
11
12
  * Chip Types; fill 옵션 목록
@@ -41,6 +42,6 @@ export type ChipSize = (typeof CHIP_SIZES)[number];
41
42
 
42
43
  /**
43
44
  * Chip Types; interactive style union 타입
44
- * @typedef {"filter" | "assist"} ChipInteractiveStyle
45
+ * @typedef {"filter" | "assist" | "text"} ChipInteractiveStyle
45
46
  */
46
47
  export type ChipInteractiveStyle = Exclude<ChipStyle, "input">;
@@ -71,7 +71,7 @@ export interface ChipCommonProps {
71
71
  export interface ChipInteractiveProps extends ChipCommonProps, ButtonProps {
72
72
  /**
73
73
  * input을 제외한 interactive 스타일.
74
- * "filter" | "assist"
74
+ * "filter" | "assist" | "text"
75
75
  */
76
76
  chipStyle?: ChipInteractiveStyle;
77
77
  }
@@ -0,0 +1,6 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <circle cx="12" cy="12" r="10" fill="#313235"/>
3
+ <circle cx="12" cy="12" r="10" fill="#DA1D0B"/>
4
+ <path d="M12 15C12.5523 15 13 15.4477 13 16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16C11 15.4477 11.4477 15 12 15Z" fill="white"/>
5
+ <path d="M12 7.2002C12.4418 7.2002 12.7998 7.55817 12.7998 8V13C12.7998 13.4418 12.4418 13.7998 12 13.7998C11.5582 13.7998 11.2002 13.4418 11.2002 13V8C11.2002 7.55817 11.5582 7.2002 12 7.2002Z" fill="white"/>
6
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <circle cx="12" cy="12" r="10" fill="#313235"/>
3
+ <circle cx="12" cy="12" r="10" fill="#1AB24D"/>
4
+ <path d="M7.19152 11.8688C6.8791 11.5564 6.8791 11.0501 7.19152 10.7377C7.50394 10.4253 8.0102 10.4253 8.32262 10.7377L10.9391 13.3541L15.6768 8.61636C15.9892 8.30394 16.4955 8.30394 16.8079 8.61636C17.1203 8.92877 17.1203 9.43503 16.8079 9.74745L11.5046 15.0508C11.1922 15.3632 10.6859 15.3632 10.3735 15.0508L7.19152 11.8688Z" fill="white"/>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M2.6303 18.0362L10.2478 4.18631C11.0076 2.80476 12.9928 2.80476 13.7526 4.18631L21.3701 18.0362C22.1032 19.3691 21.1389 21.0001 19.6176 21.0001H4.38273C2.86153 21.0001 1.8972 19.3691 2.6303 18.0362Z" fill="#F2CC0D"/>
3
+ <path d="M12 16C12.5523 16 13 16.4477 13 17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16Z" fill="#313235"/>
4
+ <path d="M12 8.2002C12.4418 8.2002 12.7998 8.55817 12.7998 9V14C12.7998 14.4418 12.4418 14.7998 12 14.7998C11.5582 14.7998 11.2002 14.4418 11.2002 14V9C11.2002 8.55817 11.5582 8.2002 12 8.2002Z" fill="#313235"/>
5
+ </svg>
@@ -0,0 +1 @@
1
+ @use "./styles";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Toast; semantic feedback toast 카테고리 배럴
3
+ * @desc
4
+ * - `Toast.Host`: 트리거 로직이 만든 item data를 위치별 stack으로 렌더링한다.
5
+ * - `Toast.Item`: state icon/content/timer close를 담당하는 개별 toast다.
6
+ * - `ToastIcon`, `ToastItemData`, `ToastState`: public contract 도구다.
7
+ */
8
+ import "./index.scss";
9
+
10
+ export * from "./markup";
11
+ export type * from "./types";
@@ -0,0 +1,74 @@
1
+ import { Fragment } from "react";
2
+
3
+ import type { ToastHostProps, ToastItemData } from "../types";
4
+ import type { ToastStackGroup, ToastStackStyle } from "../types/internal";
5
+ import { ToastItem } from "./Item";
6
+
7
+ /**
8
+ * Toast Host; x/y/margin 기준으로 Toast stack을 렌더링하는 Host
9
+ * @component
10
+ * @param {ToastHostProps} props
11
+ * @param {ToastItemData[]} props.items Toast item 데이터 목록
12
+ * @param {(toastKey: string) => void} props.onClose 닫힘 요청 핸들러
13
+ * @example
14
+ * <Toast.Host items={toastItems} onClose={onToastClose} />
15
+ */
16
+ export function ToastHost({ items, onClose }: ToastHostProps) {
17
+ const stackGroups = items.reduce<ToastStackGroup[]>((groups, item) => {
18
+ const stackKey = `${item.y}-${item.x}-${String(item.margin)}`;
19
+ const nextGroup = groups.find(group => group.stackKey === stackKey);
20
+
21
+ if (nextGroup) {
22
+ nextGroup.items.push(item);
23
+ return groups;
24
+ }
25
+
26
+ groups.push({
27
+ stackKey,
28
+ x: item.x,
29
+ y: item.y,
30
+ margin: item.margin,
31
+ items: [item],
32
+ });
33
+
34
+ return groups;
35
+ }, []);
36
+
37
+ if (!stackGroups.length) {
38
+ return null;
39
+ }
40
+
41
+ return (
42
+ <Fragment>
43
+ {stackGroups.map(group => {
44
+ const stackMargin =
45
+ typeof group.margin === "number" ? `${group.margin}px` : group.margin;
46
+ const stackStyle: ToastStackStyle = {
47
+ "--toast-stack-margin": stackMargin,
48
+ };
49
+
50
+ return (
51
+ <div
52
+ key={group.stackKey}
53
+ className="toast-stack"
54
+ data-x={group.x}
55
+ data-y={group.y}
56
+ style={stackStyle}
57
+ >
58
+ {group.items.map((item: ToastItemData) => (
59
+ <ToastItem
60
+ key={item.toastKey}
61
+ toastKey={item.toastKey}
62
+ duration={item.duration}
63
+ state={item.state}
64
+ message={item.message}
65
+ description={item.description}
66
+ onClose={onClose}
67
+ />
68
+ ))}
69
+ </div>
70
+ );
71
+ })}
72
+ </Fragment>
73
+ );
74
+ }
@@ -0,0 +1,15 @@
1
+ import ErrorIcon from "../img/error.svg";
2
+ import SuccessIcon from "../img/success.svg";
3
+ import WarningIcon from "../img/warning.svg";
4
+
5
+ /**
6
+ * Toast Icon Set; 상태별 아이콘 컴포넌트 맵
7
+ * @desc
8
+ * - standard는 Figma 기준 아이콘을 렌더링하지 않는다.
9
+ * - success/warning/error는 제공된 state SVG를 사용한다.
10
+ */
11
+ export const ToastIcon = {
12
+ success: SuccessIcon,
13
+ warning: WarningIcon,
14
+ error: ErrorIcon,
15
+ } as const;
@@ -0,0 +1,100 @@
1
+ import clsx from "clsx";
2
+ import { createElement, forwardRef, useEffect, useState } from "react";
3
+
4
+ import type { ToastItemProps } from "../types";
5
+ import type { ToastItemStyle } from "../types/internal";
6
+ import { ToastIcon } from "./Icon";
7
+ import { ToastText } from "./Text";
8
+
9
+ const TOAST_TRANSITION_MS = 600;
10
+
11
+ /**
12
+ * Toast Item; 개별 Toast 메시지 렌더러
13
+ * @component
14
+ * @param {ToastItemProps} props
15
+ * @param {string} props.toastKey 트리거 지점을 드러내는 semantic key
16
+ * @param {number} props.duration 자동 닫힘까지의 시간(ms)
17
+ * @param {"standard" | "success" | "warning" | "error"} props.state Toast 피드백 성격
18
+ * @param {React.ReactNode} props.message 한 줄 메시지 콘텐츠
19
+ * @param {React.ReactNode} [props.description] 선택 보조 콘텐츠
20
+ * @param {(toastKey: string) => void} props.onClose 닫힘 요청 핸들러
21
+ * @example
22
+ * <Toast.Item toastKey="save-success" duration={3000} state="success" message="저장되었습니다." onClose={onClose} />
23
+ */
24
+ const ToastItem = forwardRef<HTMLElementTagNameMap["section"], ToastItemProps>(
25
+ (
26
+ {
27
+ toastKey,
28
+ duration,
29
+ state,
30
+ message,
31
+ description,
32
+ onClose,
33
+ className,
34
+ role,
35
+ style,
36
+ ...restProps
37
+ },
38
+ ref,
39
+ ) => {
40
+ const [isVisible, setIsVisible] = useState(false);
41
+ const toastStyle: ToastItemStyle = {
42
+ ...style,
43
+ "--toast-transition-duration": `${TOAST_TRANSITION_MS}ms`,
44
+ };
45
+
46
+ useEffect(() => {
47
+ // 변경: mount 직후 open phase를 분리해 enter transition이 실제 렌더 프레임에서 실행되게 한다.
48
+ const enterFrameId = window.requestAnimationFrame(() => {
49
+ setIsVisible(true);
50
+ });
51
+ let exitTimerId: number | undefined;
52
+
53
+ // 변경: duration 만료 후 즉시 제거하지 않고 closed phase를 거쳐 exit transition을 보장한다.
54
+ const closeTimerId = window.setTimeout(() => {
55
+ setIsVisible(false);
56
+ exitTimerId = window.setTimeout(() => {
57
+ onClose(toastKey);
58
+ }, TOAST_TRANSITION_MS);
59
+ }, duration);
60
+
61
+ return () => {
62
+ window.cancelAnimationFrame(enterFrameId);
63
+ window.clearTimeout(closeTimerId);
64
+ if (typeof exitTimerId === "number") {
65
+ window.clearTimeout(exitTimerId);
66
+ }
67
+ };
68
+ }, [duration, onClose, toastKey]);
69
+
70
+ return (
71
+ <section
72
+ {...restProps}
73
+ ref={ref}
74
+ className={clsx("toast", className)}
75
+ data-phase={isVisible ? "open" : "closed"}
76
+ data-state={state}
77
+ data-toast-key={toastKey}
78
+ role={role ?? "status"}
79
+ aria-live="polite"
80
+ style={toastStyle}
81
+ >
82
+ {state === "standard" ? null : (
83
+ <figure className="toast-icon" aria-hidden="true">
84
+ {createElement(ToastIcon[state])}
85
+ </figure>
86
+ )}
87
+ <div className="toast-content">
88
+ <ToastText>{message}</ToastText>
89
+ {description ? (
90
+ <ToastText className="toast-description">{description}</ToastText>
91
+ ) : null}
92
+ </div>
93
+ </section>
94
+ );
95
+ },
96
+ );
97
+
98
+ ToastItem.displayName = "Toast.Item";
99
+
100
+ export { ToastItem };
@@ -0,0 +1,21 @@
1
+ import clsx from "clsx";
2
+ import { Slot } from "../../slot";
3
+ import type { ToastTextProps } from "../types/internal";
4
+
5
+ /**
6
+ * Toast Text; Toast 텍스트 className 계약을 고정하는 전용 마크업
7
+ * @component
8
+ * @param {object} props
9
+ * @param {React.ReactNode} [props.children] 문자열/숫자는 p 태그로 감싸고, ReactNode는 그대로 렌더링한다.
10
+ * @param {string} [props.className] 텍스트 className
11
+ * @example
12
+ * <ToastText>저장되었습니다.</ToastText>
13
+ */
14
+ export function ToastText({ children, className }: ToastTextProps) {
15
+ // 변경: Toast 메시지 텍스트는 Slot.Text로 위임해 string | number 래핑 규칙을 유지한다.
16
+ return (
17
+ <Slot.Text as="p" className={clsx("toast-text", className)}>
18
+ {children}
19
+ </Slot.Text>
20
+ );
21
+ }
@@ -0,0 +1,16 @@
1
+ import { ToastHost } from "./Host";
2
+ import { ToastItem } from "./Item";
3
+
4
+ /**
5
+ * Toast; semantic feedback toast namespace
6
+ * @desc
7
+ * - `Toast.Host`: item data를 x/y/margin별 stack으로 렌더링한다.
8
+ * - `Toast.Item`: 개별 toast content와 timer close를 담당한다.
9
+ */
10
+ export const Toast = {
11
+ Host: ToastHost,
12
+ Item: ToastItem,
13
+ };
14
+
15
+ export { ToastHost, ToastItem };
16
+ export * from "./Icon";
@@ -0,0 +1,2 @@
1
+ @use "./variables";
2
+ @use "./toast";
@@ -0,0 +1,113 @@
1
+ .toast-stack {
2
+ --toast-stack-translate-x: 0;
3
+ --toast-stack-translate-y: 0;
4
+ position: fixed;
5
+ z-index: var(--toast-stack-z-index);
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--toast-stack-gap);
9
+ pointer-events: none;
10
+ transform: translate(
11
+ var(--toast-stack-translate-x),
12
+ var(--toast-stack-translate-y)
13
+ );
14
+ }
15
+
16
+ .toast-stack:where([data-x="left"]) {
17
+ left: var(--toast-stack-margin);
18
+ --toast-transition-x: calc(var(--toast-transition-distance) * -1);
19
+ }
20
+
21
+ .toast-stack:where([data-x="center"]) {
22
+ left: 50%;
23
+ --toast-stack-translate-x: -50%;
24
+ --toast-transition-distance: -80%;
25
+ }
26
+
27
+ .toast-stack:where([data-x="right"]) {
28
+ right: var(--toast-stack-margin);
29
+ --toast-transition-x: var(--toast-transition-distance);
30
+ }
31
+
32
+ .toast-stack:where([data-y="top"]) {
33
+ top: var(--toast-stack-margin);
34
+ }
35
+
36
+ .toast-stack:where([data-y="center"]) {
37
+ top: 50%;
38
+ --toast-stack-translate-y: -50%;
39
+ }
40
+
41
+ .toast-stack:where([data-y="bottom"]) {
42
+ bottom: var(--toast-stack-margin);
43
+ }
44
+
45
+ .toast-stack:where([data-x="center"][data-y="top"]) {
46
+ --toast-transition-y: calc(var(--toast-transition-distance) * -1);
47
+ }
48
+
49
+ .toast-stack:where([data-x="center"][data-y="bottom"]) {
50
+ --toast-transition-y: var(--toast-transition-distance);
51
+ }
52
+
53
+ .toast {
54
+ display: flex;
55
+ align-items: center;
56
+ width: min(var(--toast-width), calc(100vw - (var(--toast-stack-margin) * 2)));
57
+ box-sizing: border-box;
58
+ gap: var(--toast-gap);
59
+ overflow: hidden;
60
+ padding: var(--toast-padding-block) var(--toast-padding-inline);
61
+ border-radius: var(--toast-radius);
62
+ background-color: var(--toast-background-color);
63
+ opacity: 0;
64
+ pointer-events: auto;
65
+ transform: translate(var(--toast-transition-x), var(--toast-transition-y));
66
+ transition:
67
+ opacity var(--toast-transition-duration) var(--toast-transition-easing),
68
+ transform var(--toast-transition-duration) var(--toast-transition-easing);
69
+ }
70
+
71
+ .toast:where([data-phase="open"]) {
72
+ opacity: 1;
73
+ transform: translateY(0);
74
+ }
75
+
76
+ .toast-icon {
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ flex-shrink: 0;
81
+ width: var(--toast-icon-size);
82
+ height: var(--toast-icon-size);
83
+ margin: 0;
84
+ }
85
+
86
+ .toast-icon svg {
87
+ display: block;
88
+ width: 100%;
89
+ height: 100%;
90
+ }
91
+
92
+ .toast-content {
93
+ display: flex;
94
+ flex: 1;
95
+ min-width: 0;
96
+ flex-direction: column;
97
+ }
98
+
99
+ .toast-text {
100
+ overflow: hidden;
101
+ margin: 0;
102
+ color: var(--toast-foreground-color);
103
+ font-size: var(--toast-message-font-size);
104
+ line-height: var(--toast-message-line-height);
105
+ letter-spacing: var(--toast-message-letter-spacing);
106
+ font-weight: var(--toast-message-font-weight);
107
+ text-overflow: ellipsis;
108
+ white-space: nowrap;
109
+ }
110
+
111
+ .toast-description {
112
+ opacity: 0.82;
113
+ }
@@ -0,0 +1,24 @@
1
+ :root {
2
+ --toast-stack-margin: var(--spacing-padding-7);
3
+ --toast-stack-gap: var(--spacing-gap-3);
4
+ --toast-stack-z-index: 10000;
5
+
6
+ --toast-width: 361px;
7
+ --toast-background-color: var(--color-surface-heavy);
8
+ --toast-foreground-color: var(--color-common-100);
9
+ --toast-radius: var(--theme-radius-large-1);
10
+ --toast-padding-block: var(--spacing-padding-5);
11
+ --toast-padding-inline: var(--spacing-padding-7);
12
+ --toast-gap: var(--spacing-gap-5);
13
+ --toast-icon-size: 24px;
14
+ --toast-transition-duration: 0.6s;
15
+ --toast-transition-easing: cubic-bezier(0.16, 1, 0.3, 1);
16
+ --toast-transition-distance: -24%;
17
+ --toast-transition-x: 0;
18
+ --toast-transition-y: 0;
19
+
20
+ --toast-message-font-size: var(--font-body-xsmall-size);
21
+ --toast-message-line-height: var(--font-body-xsmall-line-height);
22
+ --toast-message-letter-spacing: var(--font-body-xsmall-letter-spacing);
23
+ --toast-message-font-weight: var(--font-body-xsmall-weight);
24
+ }
@@ -0,0 +1 @@
1
+ export type * from "./props";
@@ -0,0 +1,71 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import type { ToastHorizontal, ToastItemData, ToastVertical } from "./props";
3
+
4
+ /**
5
+ * Toast text props; Toast 메시지 텍스트 렌더링 props
6
+ * @property {ReactNode} [children] 문자열/숫자는 p 태그로 감싸고, ReactNode는 그대로 렌더링한다.
7
+ * @property {string} [className] 텍스트 className
8
+ */
9
+ export interface ToastTextProps {
10
+ /**
11
+ * 문자열/숫자는 p 태그로 감싸고, ReactNode는 그대로 렌더링한다.
12
+ */
13
+ children?: ReactNode;
14
+ /**
15
+ * 텍스트 className
16
+ */
17
+ className?: string;
18
+ }
19
+
20
+ /**
21
+ * Toast stack group; Host 내부 placement bucket
22
+ * @property {string} stackKey x/y/margin 조합으로 만든 stack key
23
+ * @property {"left" | "center" | "right"} x Toast stack 가로 위치
24
+ * @property {"top" | "center" | "bottom"} y Toast stack 세로 위치
25
+ * @property {number | string} margin page frame 기준 위치 간격
26
+ * @property {ToastItemData[]} items 같은 위치에 stack될 Toast 목록
27
+ */
28
+ export interface ToastStackGroup {
29
+ /**
30
+ * x/y/margin 조합으로 만든 stack key
31
+ */
32
+ stackKey: string;
33
+ /**
34
+ * Toast stack 가로 위치
35
+ */
36
+ x: ToastHorizontal;
37
+ /**
38
+ * Toast stack 세로 위치
39
+ */
40
+ y: ToastVertical;
41
+ /**
42
+ * page frame 기준 위치 간격
43
+ */
44
+ margin: number | string;
45
+ /**
46
+ * 같은 위치에 stack될 Toast 목록
47
+ */
48
+ items: ToastItemData[];
49
+ }
50
+
51
+ /**
52
+ * Toast stack style; 런타임 margin CSS 변수 주입 타입
53
+ * @property {string} --toast-stack-margin page frame 기준 위치 간격 CSS 값
54
+ */
55
+ export interface ToastStackStyle extends CSSProperties {
56
+ /**
57
+ * page frame 기준 위치 간격 CSS 값
58
+ */
59
+ ["--toast-stack-margin"]: string;
60
+ }
61
+
62
+ /**
63
+ * Toast item style; transition duration CSS 변수 주입 타입
64
+ * @property {string} --toast-transition-duration Toast enter/exit transition 시간 CSS 값
65
+ */
66
+ export interface ToastItemStyle extends CSSProperties {
67
+ /**
68
+ * Toast enter/exit transition 시간 CSS 값
69
+ */
70
+ ["--toast-transition-duration"]: string;
71
+ }
@@ -0,0 +1,128 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+
3
+ /**
4
+ * Toast state; Toast 피드백 성격 축
5
+ * @typedef {"standard" | "success" | "warning" | "error"} ToastState
6
+ */
7
+ export type ToastState = "standard" | "success" | "warning" | "error";
8
+
9
+ /**
10
+ * Toast horizontal position; Toast stack 가로 위치 축
11
+ * @typedef {"left" | "center" | "right"} ToastHorizontal
12
+ */
13
+ export type ToastHorizontal = "left" | "center" | "right";
14
+
15
+ /**
16
+ * Toast vertical position; Toast stack 세로 위치 축
17
+ * @typedef {"top" | "center" | "bottom"} ToastVertical
18
+ */
19
+ export type ToastVertical = "top" | "center" | "bottom";
20
+
21
+ /**
22
+ * Toast item data; 트리거 로직이 생성하는 Toast 렌더링 데이터
23
+ * @property {string} toastKey 트리거 지점을 드러내는 semantic key
24
+ * @property {"left" | "center" | "right"} x Toast stack 가로 위치
25
+ * @property {"top" | "center" | "bottom"} y Toast stack 세로 위치
26
+ * @property {number | string} margin page frame 기준 위치 간격
27
+ * @property {number} duration 자동 닫힘까지의 시간(ms)
28
+ * @property {"standard" | "success" | "warning" | "error"} state Toast 피드백 성격
29
+ * @property {ReactNode} message 한 줄 메시지 콘텐츠
30
+ * @property {ReactNode} [description] 선택 보조 콘텐츠
31
+ */
32
+ export interface ToastItemData {
33
+ /**
34
+ * 트리거 지점을 드러내는 semantic key
35
+ */
36
+ toastKey: string;
37
+ /**
38
+ * Toast stack 가로 위치
39
+ */
40
+ x: ToastHorizontal;
41
+ /**
42
+ * Toast stack 세로 위치
43
+ */
44
+ y: ToastVertical;
45
+ /**
46
+ * page frame 기준 위치 간격
47
+ */
48
+ margin: number | string;
49
+ /**
50
+ * 자동 닫힘까지의 시간(ms)
51
+ */
52
+ duration: number;
53
+ /**
54
+ * Toast 피드백 성격
55
+ */
56
+ state: ToastState;
57
+ /**
58
+ * 한 줄 메시지 콘텐츠
59
+ */
60
+ message: ReactNode;
61
+ /**
62
+ * 선택 보조 콘텐츠
63
+ */
64
+ description?: ReactNode;
65
+ }
66
+
67
+ type NativeToastItemProps = Omit<
68
+ ComponentPropsWithoutRef<"section">,
69
+ "children"
70
+ >;
71
+
72
+ /**
73
+ * Toast item props; 개별 Toast 렌더링 props
74
+ * @property {string} toastKey 트리거 지점을 드러내는 semantic key
75
+ * @property {number} duration 자동 닫힘까지의 시간(ms)
76
+ * @property {"standard" | "success" | "warning" | "error"} state Toast 피드백 성격
77
+ * @property {ReactNode} message 한 줄 메시지 콘텐츠
78
+ * @property {ReactNode} [description] 선택 보조 콘텐츠
79
+ * @property {(toastKey: string) => void} onClose 닫힘 요청 핸들러
80
+ * @property {string} [className] Toast item className
81
+ * @see React.ComponentPropsWithoutRef<"section">
82
+ */
83
+ export interface ToastItemProps extends NativeToastItemProps {
84
+ /**
85
+ * 트리거 지점을 드러내는 semantic key
86
+ */
87
+ toastKey: string;
88
+ /**
89
+ * 자동 닫힘까지의 시간(ms)
90
+ */
91
+ duration: number;
92
+ /**
93
+ * Toast 피드백 성격
94
+ */
95
+ state: ToastState;
96
+ /**
97
+ * 한 줄 메시지 콘텐츠
98
+ */
99
+ message: ReactNode;
100
+ /**
101
+ * 선택 보조 콘텐츠
102
+ */
103
+ description?: ReactNode;
104
+ /**
105
+ * 닫힘 요청 핸들러
106
+ */
107
+ onClose: (toastKey: string) => void;
108
+ /**
109
+ * Toast item className
110
+ */
111
+ className?: string;
112
+ }
113
+
114
+ /**
115
+ * Toast host props; placement별 Toast stack 렌더링 props
116
+ * @property {ToastItemData[]} items Toast item 데이터 목록
117
+ * @property {(toastKey: string) => void} onClose 닫힘 요청 핸들러
118
+ */
119
+ export interface ToastHostProps {
120
+ /**
121
+ * Toast item 데이터 목록
122
+ */
123
+ items: ToastItemData[];
124
+ /**
125
+ * 닫힘 요청 핸들러
126
+ */
127
+ onClose: (toastKey: string) => void;
128
+ }
package/src/index.scss CHANGED
@@ -22,4 +22,5 @@
22
22
  @use "./components/switch";
23
23
  @use "./components/tab";
24
24
  @use "./components/table";
25
+ @use "./components/toast";
25
26
  @use "./components/tooltip";
package/src/index.tsx CHANGED
@@ -28,5 +28,6 @@ export * from "./components/spinner";
28
28
  export * from "./components/switch";
29
29
  export * from "./components/tab";
30
30
  export * from "./components/table";
31
+ export * from "./components/toast";
31
32
  export * from "./components/tooltip";
32
33
  export type * from "./types";