@uniai-fe/uds-primitives 0.5.6 → 0.6.1

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 (42) hide show
  1. package/README.md +25 -4
  2. package/dist/styles.css +237 -16
  3. package/package.json +1 -1
  4. package/src/components/alternate/{markup/index.tsx → Alternate.tsx} +7 -4
  5. package/src/components/alternate/index.tsx +2 -1
  6. package/src/components/alternate/layout/Button.tsx +29 -0
  7. package/src/components/alternate/layout/Container.tsx +24 -0
  8. package/src/components/alternate/layout/Contents.tsx +23 -0
  9. package/src/components/alternate/layout/Figure.tsx +24 -0
  10. package/src/components/alternate/layout/Layout.tsx +27 -0
  11. package/src/components/alternate/layout/TextButton.tsx +50 -0
  12. package/src/components/alternate/layout/Title.tsx +25 -0
  13. package/src/components/alternate/layout/index.tsx +1 -0
  14. package/src/components/alternate/styles/index.scss +3 -1
  15. package/src/components/alternate/styles/layout.scss +63 -0
  16. package/src/components/alternate/styles/{alternate.scss → unit.scss} +16 -16
  17. package/src/components/alternate/styles/variables.scss +30 -0
  18. package/src/components/alternate/types/index.ts +2 -77
  19. package/src/components/alternate/types/layout.ts +126 -0
  20. package/src/components/alternate/types/unit.ts +77 -0
  21. package/src/components/alternate/{markup → unit}/empty/Data.tsx +1 -1
  22. package/src/components/alternate/{markup → unit}/loading/Default.tsx +1 -1
  23. package/src/components/toast/img/error.svg +6 -0
  24. package/src/components/toast/img/success.svg +5 -0
  25. package/src/components/toast/img/warning.svg +5 -0
  26. package/src/components/toast/index.scss +1 -0
  27. package/src/components/toast/index.tsx +11 -0
  28. package/src/components/toast/markup/Host.tsx +74 -0
  29. package/src/components/toast/markup/Icon.tsx +15 -0
  30. package/src/components/toast/markup/Item.tsx +100 -0
  31. package/src/components/toast/markup/Text.tsx +21 -0
  32. package/src/components/toast/markup/index.tsx +16 -0
  33. package/src/components/toast/styles/index.scss +2 -0
  34. package/src/components/toast/styles/toast.scss +113 -0
  35. package/src/components/toast/styles/variables.scss +24 -0
  36. package/src/components/toast/types/index.ts +1 -0
  37. package/src/components/toast/types/internal.ts +71 -0
  38. package/src/components/toast/types/props.ts +128 -0
  39. package/src/index.scss +1 -0
  40. package/src/index.tsx +1 -0
  41. /package/src/components/alternate/{markup/Label.tsx → unit/Text.tsx} +0 -0
  42. /package/src/components/alternate/{markup → unit}/loading/Icon.tsx +0 -0
package/README.md CHANGED
@@ -53,10 +53,22 @@ export default function Page() {
53
53
  - `Alternate.LoadingDefault`
54
54
  - `Alternate.LoadingIcon`
55
55
  - `Alternate.Text`
56
+ - `Alternate.Layout.Container`
57
+ - `Alternate.Layout.Figure`
58
+ - `Alternate.Layout.Title`
59
+ - `Alternate.Layout.Contents`
60
+ - `Alternate.Layout.TextButton`
61
+ - `Alternate.Layout.Button`
56
62
  - `AlternateEmptyDataProps`
57
63
  - `AlternateLoadingDefaultProps`
58
64
  - `AlternateLoadingIconProps`
59
65
  - `AlternateTextProps`
66
+ - `AlternateLayoutContainerProps`
67
+ - `AlternateLayoutFigureProps`
68
+ - `AlternateLayoutTitleProps`
69
+ - `AlternateLayoutContentsProps`
70
+ - `AlternateLayoutTextButtonProps`
71
+ - `AlternateLayoutButtonProps`
60
72
  - `Badge`
61
73
  - `BadgeProps`
62
74
  - `Chip.Default`
@@ -194,6 +206,15 @@ export default function Page() {
194
206
  - `TableRootProps`
195
207
  - `TableContainerProps`
196
208
  - `TableCellProps`
209
+ - `Toast.Host`
210
+ - `Toast.Item`
211
+ - `ToastIcon`
212
+ - `ToastItemData`
213
+ - `ToastHostProps`
214
+ - `ToastItemProps`
215
+ - `ToastState`
216
+ - `ToastHorizontal`
217
+ - `ToastVertical`
197
218
  - `Button.Default`
198
219
  - `Button.Text`
199
220
  - `Button.Rounded`
@@ -413,10 +434,10 @@ export default function RootLayout({ children }: { children: ReactNode }) {
413
434
 
414
435
  ```plaintext
415
436
  src/components/{category}/
416
- markup/ // 컴포넌트 구현
417
- types/ // 외부 노출 타입
418
- styles/ // SCSS (foundation 토큰 기반)
419
- hooks/ // 카테고리 전용 훅
437
+ markup|unit|layout/ // 컴포넌트 구현
438
+ types/ // 외부 노출 타입
439
+ styles/ // SCSS (foundation 토큰 기반)
440
+ hooks/ // 카테고리 전용 훅
420
441
  ```
421
442
 
422
443
  - 배럴(`components/{category}/index.tsx`)은 항상 `import "./index.scss"`를 포함합니다.
package/dist/styles.css CHANGED
@@ -1,5 +1,32 @@
1
1
  @charset "UTF-8";
2
2
  :root {
3
+ --alternate-unit-text-color: var(--color-cool-gray-70);
4
+ --alternate-unit-text-font-size: 1.4rem;
5
+ --alternate-unit-text-line-height: 1.5;
6
+ --alternate-unit-gap: var(--spacing-gap-6);
7
+ --alternate-loading-icon-small-size: 2.4rem;
8
+ --alternate-loading-icon-medium-size: 3.6rem;
9
+ --alternate-loading-icon-large-size: 5.2rem;
10
+ --alternate-layout-min-block-size: 320px;
11
+ --alternate-layout-gap: var(--spacing-gap-7);
12
+ --alternate-layout-content-gap: var(--spacing-gap-4);
13
+ --alternate-layout-figure-size: 120px;
14
+ --alternate-layout-title-color: var(--color-label-strong);
15
+ --alternate-layout-title-font-size: var(--font-heading-small-size);
16
+ --alternate-layout-title-line-height: var(--font-heading-small-line-height);
17
+ --alternate-layout-title-letter-spacing: var(
18
+ --font-heading-small-letter-spacing
19
+ );
20
+ --alternate-layout-title-font-weight: var(--font-heading-small-weight);
21
+ --alternate-layout-contents-color: var(--color-label-neutral);
22
+ --alternate-layout-contents-font-size: var(--font-body-small-size);
23
+ --alternate-layout-contents-line-height: var(--font-body-small-line-height);
24
+ --alternate-layout-contents-letter-spacing: var(
25
+ --font-body-small-letter-spacing
26
+ );
27
+ --alternate-layout-contents-font-weight: var(--font-body-small-weight);
28
+ --alternate-layout-text-button-color: var(--color-primary-standard);
29
+ --alternate-layout-text-button-underline-offset: 0.2em;
3
30
  --theme-badge-height-xsmall: var(--theme-size-small-1);
4
31
  --theme-badge-height-small: var(--theme-size-small-2);
5
32
  /* 변경: Figma 2421:619 기준 medium 배지 높이 32px를 추가한다. */
@@ -779,6 +806,26 @@
779
806
  --table-td-text-size: 15px;
780
807
  --table-td-text-weight: var(--font-body-xsmall-weight);
781
808
  --table-td-text-line-height: var(--font-body-xsmall-line-height);
809
+ --toast-stack-margin: var(--spacing-padding-7);
810
+ --toast-stack-gap: var(--spacing-gap-3);
811
+ --toast-stack-z-index: 10000;
812
+ --toast-width: 361px;
813
+ --toast-background-color: var(--color-surface-heavy);
814
+ --toast-foreground-color: var(--color-common-100);
815
+ --toast-radius: var(--theme-radius-large-1);
816
+ --toast-padding-block: var(--spacing-padding-5);
817
+ --toast-padding-inline: var(--spacing-padding-7);
818
+ --toast-gap: var(--spacing-gap-5);
819
+ --toast-icon-size: 24px;
820
+ --toast-transition-duration: 0.6s;
821
+ --toast-transition-easing: cubic-bezier(0.16, 1, 0.3, 1);
822
+ --toast-transition-distance: -24%;
823
+ --toast-transition-x: 0;
824
+ --toast-transition-y: 0;
825
+ --toast-message-font-size: var(--font-body-xsmall-size);
826
+ --toast-message-line-height: var(--font-body-xsmall-line-height);
827
+ --toast-message-letter-spacing: var(--font-body-xsmall-letter-spacing);
828
+ --toast-message-font-weight: var(--font-body-xsmall-weight);
782
829
  --tooltip-message-background: var(--color-cool-gray-20);
783
830
  --tooltip-message-foreground: var(--color-common-100);
784
831
  --tooltip-message-radius: var(--theme-radius-medium-3);
@@ -801,17 +848,17 @@
801
848
  }
802
849
  }
803
850
  .empty-text {
804
- font-size: 1.4rem;
805
- color: var(--color-cool-gray-70);
806
- line-height: 1.5;
851
+ color: var(--alternate-unit-text-color);
852
+ font-size: var(--alternate-unit-text-font-size);
853
+ line-height: var(--alternate-unit-text-line-height);
807
854
  word-break: keep-all;
808
855
  text-align: center;
809
856
  }
810
857
 
811
858
  .alternate-text {
812
- font-size: 1.4rem;
813
- color: var(--color-cool-gray-70);
814
- line-height: 1.5;
859
+ color: var(--alternate-unit-text-color);
860
+ font-size: var(--alternate-unit-text-font-size);
861
+ line-height: var(--alternate-unit-text-line-height);
815
862
  word-break: keep-all;
816
863
  text-align: center;
817
864
  }
@@ -829,25 +876,25 @@
829
876
  }
830
877
  .alternate.is-horizontal.loading .empty-text {
831
878
  transform: translateY(0.2rem);
832
- margin-left: 1rem;
879
+ margin-left: var(--alternate-unit-gap);
833
880
  }
834
881
  .alternate.is-horizontal.loading .alternate-text {
835
882
  transform: translateY(0.2rem);
836
- margin-left: 1rem;
883
+ margin-left: var(--alternate-unit-gap);
837
884
  }
838
885
  .alternate.is-vertical {
839
886
  flex-direction: column;
840
887
  }
841
888
  .alternate.is-vertical .empty-text {
842
- margin-top: 1rem;
889
+ margin-top: var(--alternate-unit-gap);
843
890
  }
844
891
  .alternate.is-vertical .alternate-text {
845
- margin-top: 1rem;
892
+ margin-top: var(--alternate-unit-gap);
846
893
  }
847
894
 
848
895
  .alternate-loading-icon {
849
- width: 2.4rem;
850
- height: 2.4rem;
896
+ width: var(--alternate-loading-icon-small-size);
897
+ height: var(--alternate-loading-icon-small-size);
851
898
  position: relative;
852
899
  margin: 0;
853
900
  display: flex;
@@ -856,18 +903,81 @@
856
903
  animation: alternate-loading-spin 1s linear infinite;
857
904
  }
858
905
  .alternate-loading-icon.is-medium {
859
- width: 3.6rem;
860
- height: 3.6rem;
906
+ width: var(--alternate-loading-icon-medium-size);
907
+ height: var(--alternate-loading-icon-medium-size);
861
908
  }
862
909
  .alternate-loading-icon.is-large {
863
- width: 5.2rem;
864
- height: 5.2rem;
910
+ width: var(--alternate-loading-icon-large-size);
911
+ height: var(--alternate-loading-icon-large-size);
865
912
  }
866
913
  .alternate-loading-icon svg {
867
914
  width: 100%;
868
915
  height: 100%;
869
916
  }
870
917
 
918
+ .alternate-layout {
919
+ display: flex;
920
+ flex-direction: column;
921
+ align-items: center;
922
+ justify-content: center;
923
+ width: 100%;
924
+ min-height: var(--alternate-layout-min-block-size);
925
+ box-sizing: border-box;
926
+ gap: var(--alternate-layout-gap);
927
+ text-align: center;
928
+ }
929
+
930
+ .alternate-layout-figure {
931
+ display: flex;
932
+ align-items: center;
933
+ justify-content: center;
934
+ width: var(--alternate-layout-figure-size);
935
+ height: var(--alternate-layout-figure-size);
936
+ margin: 0;
937
+ }
938
+
939
+ .alternate-layout-title {
940
+ margin: 0;
941
+ color: var(--alternate-layout-title-color);
942
+ font-size: var(--alternate-layout-title-font-size);
943
+ line-height: var(--alternate-layout-title-line-height);
944
+ letter-spacing: var(--alternate-layout-title-letter-spacing);
945
+ font-weight: var(--alternate-layout-title-font-weight);
946
+ word-break: keep-all;
947
+ }
948
+
949
+ .alternate-layout-contents {
950
+ max-width: 48rem;
951
+ margin: 0;
952
+ color: var(--alternate-layout-contents-color);
953
+ font-size: var(--alternate-layout-contents-font-size);
954
+ line-height: var(--alternate-layout-contents-line-height);
955
+ letter-spacing: var(--alternate-layout-contents-letter-spacing);
956
+ font-weight: var(--alternate-layout-contents-font-weight);
957
+ word-break: keep-all;
958
+ }
959
+
960
+ .alternate-layout-text-button {
961
+ display: inline;
962
+ padding: 0;
963
+ border: 0;
964
+ background: transparent;
965
+ color: var(--alternate-layout-text-button-color);
966
+ font: inherit;
967
+ text-decoration: underline;
968
+ text-underline-offset: var(--alternate-layout-text-button-underline-offset);
969
+ cursor: pointer;
970
+ }
971
+
972
+ .alternate-layout-text-button:where(:disabled) {
973
+ cursor: default;
974
+ opacity: 0.4;
975
+ }
976
+
977
+ .alternate-layout-button {
978
+ flex-shrink: 0;
979
+ }
980
+
871
981
  /* Badge 기본 토큰 래핑 */
872
982
 
873
983
 
@@ -5097,6 +5207,117 @@ figure.chip {
5097
5207
 
5098
5208
 
5099
5209
 
5210
+ .toast-stack {
5211
+ --toast-stack-translate-x: 0;
5212
+ --toast-stack-translate-y: 0;
5213
+ position: fixed;
5214
+ z-index: var(--toast-stack-z-index);
5215
+ display: flex;
5216
+ flex-direction: column;
5217
+ gap: var(--toast-stack-gap);
5218
+ pointer-events: none;
5219
+ transform: translate(var(--toast-stack-translate-x), var(--toast-stack-translate-y));
5220
+ }
5221
+
5222
+ .toast-stack:where([data-x=left]) {
5223
+ left: var(--toast-stack-margin);
5224
+ --toast-transition-x: calc(var(--toast-transition-distance) * -1);
5225
+ }
5226
+
5227
+ .toast-stack:where([data-x=center]) {
5228
+ left: 50%;
5229
+ --toast-stack-translate-x: -50%;
5230
+ --toast-transition-distance: -80%;
5231
+ }
5232
+
5233
+ .toast-stack:where([data-x=right]) {
5234
+ right: var(--toast-stack-margin);
5235
+ --toast-transition-x: var(--toast-transition-distance);
5236
+ }
5237
+
5238
+ .toast-stack:where([data-y=top]) {
5239
+ top: var(--toast-stack-margin);
5240
+ }
5241
+
5242
+ .toast-stack:where([data-y=center]) {
5243
+ top: 50%;
5244
+ --toast-stack-translate-y: -50%;
5245
+ }
5246
+
5247
+ .toast-stack:where([data-y=bottom]) {
5248
+ bottom: var(--toast-stack-margin);
5249
+ }
5250
+
5251
+ .toast-stack:where([data-x=center][data-y=top]) {
5252
+ --toast-transition-y: calc(var(--toast-transition-distance) * -1);
5253
+ }
5254
+
5255
+ .toast-stack:where([data-x=center][data-y=bottom]) {
5256
+ --toast-transition-y: var(--toast-transition-distance);
5257
+ }
5258
+
5259
+ .toast {
5260
+ display: flex;
5261
+ align-items: center;
5262
+ width: min(var(--toast-width), 100vw - var(--toast-stack-margin) * 2);
5263
+ box-sizing: border-box;
5264
+ gap: var(--toast-gap);
5265
+ overflow: hidden;
5266
+ padding: var(--toast-padding-block) var(--toast-padding-inline);
5267
+ border-radius: var(--toast-radius);
5268
+ background-color: var(--toast-background-color);
5269
+ opacity: 0;
5270
+ pointer-events: auto;
5271
+ transform: translate(var(--toast-transition-x), var(--toast-transition-y));
5272
+ transition: opacity var(--toast-transition-duration) var(--toast-transition-easing), transform var(--toast-transition-duration) var(--toast-transition-easing);
5273
+ }
5274
+
5275
+ .toast:where([data-phase=open]) {
5276
+ opacity: 1;
5277
+ transform: translateY(0);
5278
+ }
5279
+
5280
+ .toast-icon {
5281
+ display: flex;
5282
+ align-items: center;
5283
+ justify-content: center;
5284
+ flex-shrink: 0;
5285
+ width: var(--toast-icon-size);
5286
+ height: var(--toast-icon-size);
5287
+ margin: 0;
5288
+ }
5289
+
5290
+ .toast-icon svg {
5291
+ display: block;
5292
+ width: 100%;
5293
+ height: 100%;
5294
+ }
5295
+
5296
+ .toast-content {
5297
+ display: flex;
5298
+ flex: 1;
5299
+ min-width: 0;
5300
+ flex-direction: column;
5301
+ }
5302
+
5303
+ .toast-text {
5304
+ overflow: hidden;
5305
+ margin: 0;
5306
+ color: var(--toast-foreground-color);
5307
+ font-size: var(--toast-message-font-size);
5308
+ line-height: var(--toast-message-line-height);
5309
+ letter-spacing: var(--toast-message-letter-spacing);
5310
+ font-weight: var(--toast-message-font-weight);
5311
+ text-overflow: ellipsis;
5312
+ white-space: nowrap;
5313
+ }
5314
+
5315
+ .toast-description {
5316
+ opacity: 0.82;
5317
+ }
5318
+
5319
+
5320
+
5100
5321
  .tooltip-trigger {
5101
5322
  display: inline-flex;
5102
5323
  align-items: center;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.5.6",
3
+ "version": "0.6.1",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -1,7 +1,8 @@
1
- import AlternateEmptyData from "./empty/Data";
2
- import AlternateText from "./Label";
3
- import AlternateLoadingDefault from "./loading/Default";
4
- import AlternateLoadingIcon from "./loading/Icon";
1
+ import AlternateLayout from "./layout/Layout";
2
+ import AlternateEmptyData from "./unit/empty/Data";
3
+ import AlternateText from "./unit/Text";
4
+ import AlternateLoadingDefault from "./unit/loading/Default";
5
+ import AlternateLoadingIcon from "./unit/loading/Icon";
5
6
 
6
7
  /**
7
8
  * Alternate; empty/loading fallback namespace
@@ -10,12 +11,14 @@ import AlternateLoadingIcon from "./loading/Icon";
10
11
  * - `Alternate.LoadingDefault`: 로딩 안내 레이아웃이다.
11
12
  * - `Alternate.LoadingIcon`: 단독 스피너 아이콘이다.
12
13
  * - `Alternate.Text`: 안내 문구 슬롯이다.
14
+ * - `Alternate.Layout`: edge-case 조합을 위한 anatomy namespace다.
13
15
  */
14
16
  const Alternate = {
15
17
  EmptyData: AlternateEmptyData,
16
18
  LoadingDefault: AlternateLoadingDefault,
17
19
  LoadingIcon: AlternateLoadingIcon,
18
20
  Text: AlternateText,
21
+ Layout: AlternateLayout,
19
22
  };
20
23
 
21
24
  export default Alternate;
@@ -5,6 +5,7 @@
5
5
  * - `Alternate.LoadingDefault`: 로딩 아이콘과 안내 문구를 함께 렌더링한다.
6
6
  * - `Alternate.LoadingIcon`: 단독 스피너 아이콘이다.
7
7
  * - `Alternate.Text`: fallback 문구 스타일 슬롯이다.
8
+ * - `Alternate.Layout`: edge-case 조합 anatomy다.
8
9
  */
9
- export { default as Alternate } from "./markup";
10
+ export { default as Alternate } from "./Alternate";
10
11
  export type * from "./types";
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { clsx } from "clsx";
4
+ import { Button } from "../../button";
5
+ import type { AlternateLayoutButtonProps } from "../types";
6
+
7
+ /**
8
+ * Alternate Layout Button; Button.Default preset adapter
9
+ * @component
10
+ * @param {AlternateLayoutButtonProps} props
11
+ * @param {React.ReactNode} [props.children] 버튼 라벨 또는 콘텐츠다.
12
+ * @param {string} [props.className] 버튼 className override다.
13
+ * @example
14
+ * <Alternate.Layout.Button as="a" href="/">홈으로 이동</Alternate.Layout.Button>
15
+ */
16
+ export default function AlternateLayoutButton({
17
+ className,
18
+ ...buttonProps
19
+ }: AlternateLayoutButtonProps) {
20
+ return (
21
+ <Button.Default
22
+ {...buttonProps}
23
+ className={clsx("alternate-layout-button", className)}
24
+ fill="outlined"
25
+ priority="secondary"
26
+ size="small"
27
+ />
28
+ );
29
+ }
@@ -0,0 +1,24 @@
1
+ import { clsx } from "clsx";
2
+ import type { AlternateLayoutContainerProps } from "../types";
3
+
4
+ /**
5
+ * Alternate Layout Container; edge-case anatomy 루트
6
+ * @component
7
+ * @param {AlternateLayoutContainerProps} props
8
+ * @param {string} [props.className] 루트 className override다.
9
+ * @param {React.ReactNode} [props.children] layout 내부 콘텐츠다.
10
+ * @example
11
+ * <Alternate.Layout.Container>
12
+ * <Alternate.Layout.Title>데이터가 없습니다.</Alternate.Layout.Title>
13
+ * </Alternate.Layout.Container>
14
+ */
15
+ export default function AlternateLayoutContainer({
16
+ className,
17
+ children,
18
+ }: AlternateLayoutContainerProps) {
19
+ return (
20
+ <section className={clsx("alternate-layout", className)}>
21
+ {children}
22
+ </section>
23
+ );
24
+ }
@@ -0,0 +1,23 @@
1
+ import { clsx } from "clsx";
2
+ import { Slot } from "../../slot";
3
+ import type { AlternateLayoutContentsProps } from "../types";
4
+
5
+ /**
6
+ * Alternate Layout Contents; edge-case 본문
7
+ * @component
8
+ * @param {AlternateLayoutContentsProps} props
9
+ * @param {string} [props.className] 본문 className override다.
10
+ * @param {React.ReactNode} [props.children] 본문 콘텐츠다.
11
+ * @example
12
+ * <Alternate.Layout.Contents>다시 시도해 주세요.</Alternate.Layout.Contents>
13
+ */
14
+ export default function AlternateLayoutContents({
15
+ className,
16
+ children,
17
+ }: AlternateLayoutContentsProps) {
18
+ return (
19
+ <Slot.Text as="p" className={clsx("alternate-layout-contents", className)}>
20
+ {children}
21
+ </Slot.Text>
22
+ );
23
+ }
@@ -0,0 +1,24 @@
1
+ import { clsx } from "clsx";
2
+ import type { AlternateLayoutFigureProps } from "../types";
3
+
4
+ /**
5
+ * Alternate Layout Figure; edge-case visual 영역
6
+ * @component
7
+ * @param {AlternateLayoutFigureProps} props
8
+ * @param {string} [props.className] figure className override다.
9
+ * @param {React.ReactNode} [props.children] 도입 측이 직접 제어하는 SVG, 이미지, 아이콘 콘텐츠다.
10
+ * @example
11
+ * <Alternate.Layout.Figure>
12
+ * <img src="/empty.svg" alt="" />
13
+ * </Alternate.Layout.Figure>
14
+ */
15
+ export default function AlternateLayoutFigure({
16
+ className,
17
+ children,
18
+ }: AlternateLayoutFigureProps) {
19
+ return (
20
+ <figure className={clsx("alternate-layout-figure", className)}>
21
+ {children}
22
+ </figure>
23
+ );
24
+ }
@@ -0,0 +1,27 @@
1
+ import AlternateLayoutButton from "./Button";
2
+ import AlternateLayoutContainer from "./Container";
3
+ import AlternateLayoutContents from "./Contents";
4
+ import AlternateLayoutFigure from "./Figure";
5
+ import AlternateLayoutTextButton from "./TextButton";
6
+ import AlternateLayoutTitle from "./Title";
7
+
8
+ /**
9
+ * Alternate Layout; edge-case anatomy namespace
10
+ * @desc
11
+ * - `Container`: 화면 상태 조합 루트다.
12
+ * - `Figure`: 도입 측이 제어하는 visual 영역이다.
13
+ * - `Title`: 제목 텍스트 영역이다.
14
+ * - `Contents`: 본문 텍스트 영역이다.
15
+ * - `TextButton`: paragraph 내부 text action이다.
16
+ * - `Button`: Button.Default 기반 action adapter다.
17
+ */
18
+ const AlternateLayout = {
19
+ Container: AlternateLayoutContainer,
20
+ Figure: AlternateLayoutFigure,
21
+ Title: AlternateLayoutTitle,
22
+ Contents: AlternateLayoutContents,
23
+ TextButton: AlternateLayoutTextButton,
24
+ Button: AlternateLayoutButton,
25
+ };
26
+
27
+ export default AlternateLayout;
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { clsx } from "clsx";
4
+ import type { AlternateLayoutTextButtonProps } from "../types";
5
+
6
+ /**
7
+ * Alternate Layout TextButton; paragraph 내부 text action
8
+ * @component
9
+ * @param {AlternateLayoutTextButtonProps} props
10
+ * @param {string} [props.className] action className override다.
11
+ * @param {React.ReactNode} [props.children] action 라벨 콘텐츠다.
12
+ * @param {string} [props.href] 링크 action으로 렌더링할 href다.
13
+ * @param {string} [props.target] 링크 target 값이다.
14
+ * @example
15
+ * <Alternate.Layout.TextButton onClick={onRetry}>다시 시도</Alternate.Layout.TextButton>
16
+ */
17
+ export default function AlternateLayoutTextButton({
18
+ className,
19
+ children,
20
+ href,
21
+ target,
22
+ type: typeProp,
23
+ ...htmlAttrs
24
+ }: AlternateLayoutTextButtonProps) {
25
+ const textButtonClassName = clsx("alternate-layout-text-button", className);
26
+
27
+ if (href) {
28
+ return (
29
+ <a
30
+ className={textButtonClassName}
31
+ href={href}
32
+ target={target}
33
+ {...htmlAttrs}
34
+ >
35
+ {children}
36
+ </a>
37
+ );
38
+ }
39
+
40
+ // 변경: href가 없으면 문장 내부 action을 native button으로 고정한다.
41
+ return (
42
+ <button
43
+ className={textButtonClassName}
44
+ type={typeProp ?? "button"}
45
+ {...htmlAttrs}
46
+ >
47
+ {children}
48
+ </button>
49
+ );
50
+ }
@@ -0,0 +1,25 @@
1
+ import { clsx } from "clsx";
2
+ import { Slot } from "../../slot";
3
+ import type { AlternateLayoutTitleProps } from "../types";
4
+
5
+ /**
6
+ * Alternate Layout Title; edge-case 제목
7
+ * @component
8
+ * @param {AlternateLayoutTitleProps} props
9
+ * @param {React.ElementType} [props.as="strong"] 렌더링할 제목 요소다.
10
+ * @param {string} [props.className] 제목 className override다.
11
+ * @param {React.ReactNode} [props.children] 제목 콘텐츠다.
12
+ * @example
13
+ * <Alternate.Layout.Title as="h1">페이지를 찾을 수 없습니다.</Alternate.Layout.Title>
14
+ */
15
+ export default function AlternateLayoutTitle({
16
+ as = "strong",
17
+ className,
18
+ children,
19
+ }: AlternateLayoutTitleProps) {
20
+ return (
21
+ <Slot.Text as={as} className={clsx("alternate-layout-title", className)}>
22
+ {children}
23
+ </Slot.Text>
24
+ );
25
+ }
@@ -0,0 +1 @@
1
+ export { default as AlternateLayout } from "./Layout";
@@ -1 +1,3 @@
1
- @forward "./alternate";
1
+ @forward "./variables";
2
+ @forward "./unit";
3
+ @forward "./layout";