@uniai-fe/uds-primitives 0.1.13 → 0.2.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 (114) hide show
  1. package/README.md +2 -2
  2. package/dist/styles.css +1112 -385
  3. package/package.json +12 -15
  4. package/src/components/button/index.scss +1 -0
  5. package/src/components/button/markup/{ButtonRounded.tsx → Rounded.tsx} +1 -1
  6. package/src/components/button/markup/{ButtonText.tsx → Text.tsx} +1 -1
  7. package/src/components/button/markup/index.ts +3 -3
  8. package/src/components/button/styles/button.scss +113 -229
  9. package/src/components/button/styles/round-button.scss +11 -14
  10. package/src/components/button/styles/text-button.scss +23 -23
  11. package/src/components/button/styles/variables.scss +145 -0
  12. package/src/components/dropdown/index.tsx +3 -3
  13. package/src/components/dropdown/markup/Template.tsx +57 -0
  14. package/src/components/dropdown/markup/foundation/Container.tsx +125 -0
  15. package/src/components/dropdown/markup/foundation/MenuItem.tsx +107 -0
  16. package/src/components/dropdown/markup/foundation/MenuList.tsx +27 -0
  17. package/src/components/dropdown/markup/foundation/Provider.tsx +46 -0
  18. package/src/components/dropdown/markup/foundation/Root.tsx +30 -0
  19. package/src/components/dropdown/markup/foundation/Trigger.tsx +34 -0
  20. package/src/components/dropdown/markup/foundation/index.tsx +25 -0
  21. package/src/components/dropdown/markup/index.tsx +8 -2
  22. package/src/components/dropdown/styles/dropdown.scss +166 -0
  23. package/src/components/dropdown/styles/index.scss +2 -0
  24. package/src/components/dropdown/styles/variables.scss +40 -0
  25. package/src/components/dropdown/types/base.ts +31 -0
  26. package/src/components/dropdown/types/index.ts +2 -4
  27. package/src/components/dropdown/types/props.ts +170 -0
  28. package/src/components/dropdown/utils/index.ts +1 -4
  29. package/src/components/dropdown/utils/refs.ts +20 -0
  30. package/src/components/form/index.scss +1 -0
  31. package/src/components/form/index.tsx +18 -2
  32. package/src/components/form/markup/form-field/Body.tsx +18 -0
  33. package/src/components/form/markup/form-field/Container.tsx +58 -0
  34. package/src/components/form/markup/form-field/Footer.tsx +21 -0
  35. package/src/components/form/markup/form-field/Header.tsx +39 -0
  36. package/src/components/form/markup/form-field/Template.tsx +56 -0
  37. package/src/components/form/markup/form-field/index.tsx +22 -0
  38. package/src/components/form/styles/form-field/layout.scss +67 -0
  39. package/src/components/form/styles/form-field/variables.scss +17 -0
  40. package/src/components/form/styles/index.scss +2 -0
  41. package/src/components/form/types/index.ts +1 -0
  42. package/src/components/form/types/props.ts +125 -0
  43. package/src/components/form/utils/form-field.ts +42 -0
  44. package/src/components/input/hooks/index.ts +1 -4
  45. package/src/components/input/hooks/useDigitField.ts +63 -0
  46. package/src/components/input/img/calendar/calendar.svg +7 -0
  47. package/src/components/input/img/calendar/chevron-down.svg +3 -0
  48. package/src/components/input/img/calendar/chevron-left.svg +3 -0
  49. package/src/components/input/img/calendar/chevron-right.svg +3 -0
  50. package/src/components/input/img/calendar/chevron-up.svg +3 -0
  51. package/src/components/input/index.tsx +2 -1
  52. package/src/components/input/markup/calendar/Base.tsx +329 -0
  53. package/src/components/input/markup/calendar/index.tsx +8 -0
  54. package/src/components/input/markup/{text/InputUtilityButton.tsx → foundation/Button.tsx} +5 -15
  55. package/src/components/input/markup/foundation/Input.tsx +245 -0
  56. package/src/components/input/markup/foundation/SideSlot.tsx +30 -0
  57. package/src/components/input/markup/foundation/StatusIcon.tsx +21 -0
  58. package/src/components/input/markup/foundation/Utility.tsx +103 -0
  59. package/src/components/input/markup/foundation/index.tsx +15 -0
  60. package/src/components/input/markup/index.tsx +11 -1
  61. package/src/components/input/markup/text/AuthCode.tsx +41 -59
  62. package/src/components/input/markup/text/Email.tsx +25 -115
  63. package/src/components/input/markup/text/Password.tsx +30 -39
  64. package/src/components/input/markup/text/Phone.tsx +35 -122
  65. package/src/components/input/markup/text/Search.tsx +17 -18
  66. package/src/components/input/markup/text/index.ts +15 -12
  67. package/src/components/input/styles/calendar.scss +110 -0
  68. package/src/components/input/styles/foundation.scss +345 -0
  69. package/src/components/input/styles/index.scss +4 -476
  70. package/src/components/input/styles/text.scss +89 -0
  71. package/src/components/input/styles/variables.scss +41 -0
  72. package/src/components/input/types/calendar.ts +208 -0
  73. package/src/components/input/types/foundation.ts +194 -0
  74. package/src/components/input/types/hooks.ts +43 -0
  75. package/src/components/input/types/index.ts +5 -87
  76. package/src/components/input/types/text.ts +203 -0
  77. package/src/components/input/types/verification.ts +23 -0
  78. package/src/components/input/utils/index.tsx +1 -0
  79. package/src/components/input/utils/verification.tsx +35 -0
  80. package/src/components/select/hooks/index.ts +43 -2
  81. package/src/components/select/img/chevron/primary/large.svg +3 -0
  82. package/src/components/select/img/chevron/primary/medium.svg +3 -0
  83. package/src/components/select/img/chevron/primary/small.svg +3 -0
  84. package/src/components/select/img/chevron/secondary/large.svg +3 -0
  85. package/src/components/select/img/chevron/secondary/medium.svg +3 -0
  86. package/src/components/select/img/chevron/secondary/small.svg +3 -0
  87. package/src/components/select/img/remove.svg +3 -0
  88. package/src/components/select/index.scss +2 -1
  89. package/src/components/select/index.tsx +5 -0
  90. package/src/components/select/markup/Default.tsx +154 -0
  91. package/src/components/select/markup/foundation/Base.tsx +90 -0
  92. package/src/components/select/markup/foundation/Container.tsx +30 -0
  93. package/src/components/select/markup/foundation/Icon.tsx +78 -0
  94. package/src/components/select/markup/foundation/Selected.tsx +34 -0
  95. package/src/components/select/markup/foundation/index.ts +2 -0
  96. package/src/components/select/markup/index.tsx +36 -2
  97. package/src/components/select/markup/multiple/Multiple.tsx +205 -0
  98. package/src/components/select/markup/multiple/SelectedChip.tsx +58 -0
  99. package/src/components/select/markup/multiple/index.ts +2 -0
  100. package/src/components/select/styles/select.scss +316 -0
  101. package/src/components/select/styles/variables.scss +91 -0
  102. package/src/components/select/types/base.ts +34 -0
  103. package/src/components/select/types/icon.ts +45 -0
  104. package/src/components/select/types/index.ts +6 -4
  105. package/src/components/select/types/multiple.ts +57 -0
  106. package/src/components/select/types/option.ts +43 -0
  107. package/src/components/select/types/props.ts +209 -0
  108. package/src/components/select/types/trigger.ts +196 -0
  109. package/src/index.scss +3 -2
  110. package/src/components/input/markup/text/Base.tsx +0 -454
  111. package/src/components/input/utils/index.ts +0 -60
  112. package/src/components/select/styles/index.scss +0 -0
  113. /package/src/components/button/markup/{ButtonDefault.tsx → Base.tsx} +0 -0
  114. /package/src/components/form/{Provider.tsx → markup/Provider.tsx} +0 -0
@@ -4,42 +4,39 @@
4
4
  $round-size-map: (
5
5
  small: (
6
6
  min-height: var(
7
- --theme-button-round-min-height-small,
7
+ --button-round-height-small,
8
8
  var(--theme-size-small-2, 24px)
9
9
  ),
10
10
  padding-inline: var(
11
- --theme-button-round-padding-inline-small,
11
+ --button-round-padding-inline-small,
12
12
  var(--spacing-padding-5, 12px)
13
13
  ),
14
- radius: var(
15
- --theme-button-round-radius-small,
16
- var(--theme-radius-xlarge-2, 16px)
17
- ),
14
+ radius: var(--button-round-radius-small, var(--theme-radius-xlarge-2, 16px)),
18
15
  ),
19
16
  medium: (
20
17
  min-height: var(
21
- --theme-button-round-min-height-medium,
18
+ --button-round-height-medium,
22
19
  var(--theme-size-small-3, 32px)
23
20
  ),
24
21
  padding-inline: var(
25
- --theme-button-round-padding-inline-medium,
22
+ --button-round-padding-inline-medium,
26
23
  var(--spacing-padding-6, 16px)
27
24
  ),
28
25
  radius: var(
29
- --theme-button-round-radius-medium,
26
+ --button-round-radius-medium,
30
27
  var(--theme-radius-xlarge-2, 16px)
31
28
  ),
32
29
  ),
33
30
  large: (
34
31
  min-height: var(
35
- --theme-button-round-min-height-large,
32
+ --button-round-height-large,
36
33
  var(--theme-size-medium-1, 40px)
37
34
  ),
38
35
  padding-inline: var(
39
- --theme-button-round-padding-inline-large,
36
+ --button-round-padding-inline-large,
40
37
  var(--spacing-padding-7, 20px)
41
38
  ),
42
- radius: var(--theme-button-round-radius-large, 30px),
39
+ radius: var(--button-round-radius-large, 30px),
43
40
  ),
44
41
  );
45
42
 
@@ -47,11 +44,11 @@ $round-size-map: (
47
44
  &:where(.button-template-round) {
48
45
  min-width: auto;
49
46
  padding-block: var(
50
- --theme-button-round-padding-block,
47
+ --button-round-padding-block-base,
51
48
  var(--spacing-padding-2, 4px)
52
49
  );
53
50
  padding-inline: var(
54
- --theme-button-round-padding-inline-medium,
51
+ --button-round-padding-inline-medium,
55
52
  var(--spacing-padding-6, 16px)
56
53
  );
57
54
  gap: var(--spacing-gap-2, 8px);
@@ -3,32 +3,26 @@
3
3
  // 텍스트 버튼 사이즈 토큰을 맵으로 정의해 가독성을 높인다
4
4
  $text-size-map: (
5
5
  small: (
6
- min-height: var(
7
- --theme-button-text-min-height-small,
8
- var(--theme-size-small-1, 20px)
9
- ),
6
+ min-height: var(--button-text-height-small, var(--theme-size-small-1, 20px)),
10
7
  padding-inline: var(
11
- --theme-button-text-padding-inline-small,
8
+ --button-text-padding-inline-small,
12
9
  var(--spacing-padding-4, 8px)
13
10
  ),
14
11
  ),
15
12
  medium: (
16
13
  min-height: var(
17
- --theme-button-text-min-height-medium,
14
+ --button-text-height-medium,
18
15
  var(--theme-size-small-2, 24px)
19
16
  ),
20
17
  padding-inline: var(
21
- --theme-button-text-padding-inline-medium,
18
+ --button-text-padding-inline-medium,
22
19
  var(--spacing-padding-5, 12px)
23
20
  ),
24
21
  ),
25
22
  large: (
26
- min-height: var(
27
- --theme-button-text-min-height-large,
28
- var(--theme-size-small-3, 32px)
29
- ),
23
+ min-height: var(--button-text-height-large, var(--theme-size-small-3, 32px)),
30
24
  padding-inline: var(
31
- --theme-button-text-padding-inline-large,
25
+ --button-text-padding-inline-large,
32
26
  var(--spacing-padding-5, 12px)
33
27
  ),
34
28
  ),
@@ -40,17 +34,17 @@ $text-size-map: (
40
34
  border-color: transparent;
41
35
  background-color: transparent;
42
36
  padding-block: var(
43
- --theme-button-text-padding-block,
37
+ --button-text-padding-block-base,
44
38
  var(--spacing-padding-4, 8px)
45
39
  );
46
40
  padding-inline: var(
47
- --theme-button-text-padding-inline-small,
41
+ --button-text-padding-inline-small,
48
42
  var(--spacing-padding-4, 8px)
49
43
  );
50
44
  border-width: 0;
51
45
  gap: var(--spacing-gap-1, 4px);
52
46
  border-radius: var(
53
- --theme-button-text-radius,
47
+ --button-text-radius-base,
54
48
  var(--theme-radius-small, 4px)
55
49
  );
56
50
  }
@@ -69,38 +63,44 @@ $text-size-map: (
69
63
 
70
64
  &:where(.button-template-text.button-priority-secondary) {
71
65
  color: var(
72
- --theme-button-color-primary-default,
66
+ --button-text-secondary-foreground,
73
67
  var(--color-primary-default)
74
68
  );
75
69
 
76
70
  &:hover:not(:disabled),
77
71
  &[data-user-action="hover"]:not(:disabled) {
78
72
  background-color: var(
79
- --color-bg-alternative-cool-gray,
80
- var(--color-cool-gray-95)
73
+ --button-text-secondary-hover-bg,
74
+ var(--color-bg-alternative-cool-gray, var(--color-cool-gray-95))
81
75
  );
82
76
  }
83
77
 
84
78
  &:active:not(:disabled),
85
79
  &[data-user-action="pressed"]:not(:disabled) {
86
- background-color: var(--color-secondary-strong, var(--color-blue-90));
80
+ background-color: var(
81
+ --button-text-secondary-pressed-bg,
82
+ var(--color-secondary-strong, var(--color-blue-90))
83
+ );
87
84
  }
88
85
  }
89
86
 
90
87
  &:where(.button-template-text.button-priority-tertiary) {
91
- color: var(--theme-button-color-cool-gray-10, var(--color-cool-gray-10));
88
+ color: var(--button-text-tertiary-foreground, var(--color-cool-gray-10));
92
89
 
93
90
  &:hover:not(:disabled),
94
91
  &[data-user-action="hover"]:not(:disabled) {
95
92
  background-color: var(
96
- --color-bg-alternative-cool-gray,
97
- var(--color-cool-gray-95)
93
+ --button-text-tertiary-hover-bg,
94
+ var(--color-bg-alternative-cool-gray, var(--color-cool-gray-95))
98
95
  );
99
96
  }
100
97
 
101
98
  &:active:not(:disabled),
102
99
  &[data-user-action="pressed"]:not(:disabled) {
103
- background-color: var(--color-surface-strong, var(--color-cool-gray-20));
100
+ background-color: var(
101
+ --button-text-tertiary-pressed-bg,
102
+ var(--color-surface-strong, var(--color-cool-gray-20))
103
+ );
104
104
  }
105
105
  }
106
106
  }
@@ -0,0 +1,145 @@
1
+ /* 버튼 전용 토큰은 theme root에서 한 번만 정의하며, size 기반 규칙(--button-{type}-{property}-{size})을 따른다. */
2
+ :root {
3
+ /* default button spacing (size 기반) */
4
+ --button-default-gap-small: var(--spacing-gap-1);
5
+ --button-default-gap-medium: var(--spacing-gap-2);
6
+ --button-default-gap-large: var(--spacing-gap-3);
7
+
8
+ --button-default-padding-inline-base: var(--spacing-padding-4);
9
+ --button-default-padding-inline-small: var(--spacing-padding-6);
10
+ --button-default-padding-inline-medium: var(--spacing-padding-7);
11
+ --button-default-padding-inline-large: var(--spacing-padding-9);
12
+ --button-default-padding-block-base: 0px;
13
+ --button-default-padding-block-small: 0px;
14
+ --button-default-padding-block-medium: 0px;
15
+ --button-default-padding-block-large: 0px;
16
+
17
+ /* default button sizing */
18
+ --button-default-width-min-base: var(--theme-size-small-2);
19
+
20
+ --button-default-height-small: var(--theme-size-small-3, 32px);
21
+ --button-default-height-medium: var(--theme-size-medium-1, 40px);
22
+ --button-default-height-large: var(--theme-size-medium-2, 48px);
23
+ --button-default-height-xlarge: var(--theme-size-medium-3, 56px);
24
+
25
+ --button-default-radius-small: var(--theme-radius-medium-3);
26
+ --button-default-radius-medium: var(--theme-radius-medium-3);
27
+ --button-default-radius-large: var(--theme-radius-large-1);
28
+ --button-default-radius-xlarge: var(--theme-radius-large-2);
29
+
30
+ /* text buttons */
31
+ --button-text-padding-block-base: var(--spacing-padding-4, 8px);
32
+ --button-text-padding-inline-small: var(--spacing-padding-4, 8px);
33
+ --button-text-padding-inline-medium: var(--spacing-padding-5, 12px);
34
+ --button-text-padding-inline-large: var(--spacing-padding-5, 12px);
35
+ --button-text-height-small: var(--theme-size-small-1, 20px);
36
+ --button-text-height-medium: var(--theme-size-small-2, 24px);
37
+ --button-text-height-large: var(--theme-size-small-3, 32px);
38
+ --button-text-radius-base: var(--theme-radius-small, 4px);
39
+
40
+ /* round buttons */
41
+ --button-round-padding-inline-small: var(--spacing-padding-5, 12px);
42
+ --button-round-padding-inline-medium: var(--spacing-padding-6, 16px);
43
+ --button-round-padding-inline-large: var(--spacing-padding-7, 20px);
44
+ --button-round-padding-block-base: var(--spacing-padding-2, 4px);
45
+ --button-round-height-small: var(--theme-size-small-2, 24px);
46
+ --button-round-height-medium: var(--theme-size-small-3, 32px);
47
+ --button-round-height-large: var(--theme-size-medium-1, 40px);
48
+ --button-round-radius-small: var(--theme-radius-xlarge-2, 16px);
49
+ --button-round-radius-medium: var(--theme-radius-xlarge-2, 16px);
50
+ --button-round-radius-large: 30px;
51
+
52
+ /* typography */
53
+ --button-default-font-body-medium-family: var(--font-body-medium-family);
54
+ --button-default-font-body-medium-size: var(--font-body-medium-size);
55
+ --button-default-font-body-medium-weight: var(--font-body-medium-weight);
56
+ --button-default-font-body-medium-line-height: var(
57
+ --font-body-medium-line-height
58
+ );
59
+ --button-default-font-body-medium-letter-spacing: var(
60
+ --font-body-medium-letter-spacing
61
+ );
62
+ --button-default-font-label-medium-size: var(--font-label-medium-size);
63
+ --button-default-font-label-medium-weight: var(--font-label-medium-weight);
64
+ --button-default-font-label-medium-line-height: var(
65
+ --font-label-medium-line-height
66
+ );
67
+ --button-default-font-label-medium-letter-spacing: var(
68
+ --font-label-medium-letter-spacing
69
+ );
70
+ --button-default-font-label-large-size: var(--font-label-large-size);
71
+ --button-default-font-label-large-weight: var(--font-label-large-weight);
72
+ --button-default-font-label-large-line-height: var(
73
+ --font-label-large-line-height
74
+ );
75
+ --button-default-font-label-large-letter-spacing: var(
76
+ --font-label-large-letter-spacing
77
+ );
78
+ --button-default-font-body-large-size: var(--font-body-large-size);
79
+ --button-default-font-body-large-weight: var(--font-body-large-weight);
80
+ --button-default-font-body-large-line-height: var(
81
+ --font-body-large-line-height
82
+ );
83
+ --button-default-font-body-large-letter-spacing: var(
84
+ --font-body-large-letter-spacing
85
+ );
86
+ --button-default-font-weight: 400;
87
+
88
+ /* default button colors (priority 기반) */
89
+ --button-default-neutral-foreground: var(--color-neutral-20);
90
+ --button-default-neutral-disabled-bg: var(--color-surface-standard);
91
+ --button-default-neutral-disabled-foreground: var(--color-label-disabled);
92
+
93
+ --button-default-primary-solid-bg: var(--color-primary-default);
94
+ --button-default-primary-solid-hover-bg: var(--color-blue-50);
95
+ --button-default-primary-solid-active-bg: var(--color-blue-45);
96
+ --button-default-primary-solid-foreground: var(--color-common-100);
97
+ --button-default-primary-outline-border: var(--color-primary-default);
98
+ --button-default-primary-outline-foreground: var(--color-primary-default);
99
+ --button-default-primary-outline-hover-bg: var(--color-primary-default);
100
+ --button-default-primary-outline-hover-foreground: var(--color-common-100);
101
+ --button-default-primary-overlay-outlined-bg: var(--color-blue-90);
102
+
103
+ --button-default-secondary-solid-bg: var(--color-blue-95, #e5eeff);
104
+ --button-default-secondary-solid-hover-bg: #dbe9ff;
105
+ --button-default-secondary-solid-active-bg: #ccdeff;
106
+ --button-default-secondary-solid-foreground: var(--color-primary-default);
107
+ --button-default-secondary-outline-border: var(--color-cool-gray-90);
108
+ --button-default-secondary-outline-foreground: var(--color-primary-default);
109
+ --button-default-secondary-outline-hover-bg: var(--color-blue-90);
110
+ --button-default-secondary-outline-hover-foreground: var(
111
+ --color-primary-heavy
112
+ );
113
+ --button-default-secondary-overlay-outlined-bg: var(--color-cool-gray-95);
114
+
115
+ --button-default-tertiary-solid-bg: var(--color-surface-neutral);
116
+ --button-default-tertiary-solid-hover-bg: var(--color-surface-strong);
117
+ --button-default-tertiary-solid-active-bg: var(--color-surface-strong);
118
+ --button-default-tertiary-solid-foreground: var(--color-label-neutral);
119
+ --button-default-tertiary-outline-border: var(--color-cool-gray-90);
120
+ --button-default-tertiary-outline-foreground: var(--color-cool-gray-10);
121
+ --button-default-tertiary-outline-hover-bg: var(--color-cool-gray-95);
122
+ --button-default-tertiary-outline-hover-foreground: var(--color-neutral-20);
123
+ --button-default-tertiary-overlay-outlined-bg: var(--color-cool-gray-95);
124
+ --button-default-tertiary-overlay-solid-bg: var(--color-surface-strong);
125
+
126
+ /* text button colors */
127
+ --button-text-secondary-foreground: var(--color-primary-default);
128
+ --button-text-secondary-hover-bg: var(
129
+ --color-bg-alternative-cool-gray,
130
+ var(--color-cool-gray-95)
131
+ );
132
+ --button-text-secondary-pressed-bg: var(
133
+ --color-secondary-strong,
134
+ var(--color-blue-90)
135
+ );
136
+ --button-text-tertiary-foreground: var(--color-cool-gray-10);
137
+ --button-text-tertiary-hover-bg: var(
138
+ --color-bg-alternative-cool-gray,
139
+ var(--color-cool-gray-95)
140
+ );
141
+ --button-text-tertiary-pressed-bg: var(
142
+ --color-surface-strong,
143
+ var(--color-cool-gray-20)
144
+ );
145
+ }
@@ -1,4 +1,4 @@
1
- /**
2
- * dropdown 카테고리 배럴 placeholder: 실제 구현은 markup/ 하위에 추가한다.
3
- */
1
+ import "./index.scss";
2
+
4
3
  export * from "./markup";
4
+ export * from "./types";
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import type { DropdownTemplateProps } from "../types/props";
4
+
5
+ import DropdownRoot from "./foundation/Root";
6
+ import DropdownContainer from "./foundation/Container";
7
+ import DropdownMenuItem from "./foundation/MenuItem";
8
+ import DropdownMenuList from "./foundation/MenuList";
9
+ import DropdownTrigger from "./foundation/Trigger";
10
+
11
+ /**
12
+ * Dropdown reference template; trigger/panel/menu 조합을 제공한다.
13
+ * @component
14
+ * @param {DropdownTemplateProps} props Dropdown template props
15
+ */
16
+ const DropdownTemplate = ({
17
+ trigger,
18
+ items,
19
+ selectedIds = [],
20
+ onSelect,
21
+ size = "medium",
22
+ width = "match",
23
+ rootProps,
24
+ containerProps,
25
+ menuListProps,
26
+ }: DropdownTemplateProps) => {
27
+ return (
28
+ <DropdownRoot {...rootProps}>
29
+ <DropdownTrigger asChild>{trigger}</DropdownTrigger>
30
+ <DropdownContainer {...containerProps} size={size} width={width}>
31
+ <DropdownMenuList {...menuListProps}>
32
+ {items.map(item => (
33
+ <DropdownMenuItem
34
+ key={item.id}
35
+ label={item.label}
36
+ description={item.description}
37
+ disabled={item.disabled}
38
+ left={item.left}
39
+ right={item.right}
40
+ multiple={item.multiple}
41
+ isSelected={selectedIds?.includes(item.id)}
42
+ onSelect={event => {
43
+ if (item.disabled) {
44
+ event.preventDefault();
45
+ return;
46
+ }
47
+ onSelect?.(item);
48
+ }}
49
+ />
50
+ ))}
51
+ </DropdownMenuList>
52
+ </DropdownContainer>
53
+ </DropdownRoot>
54
+ );
55
+ };
56
+
57
+ export default DropdownTemplate;
@@ -0,0 +1,125 @@
1
+ "use client";
2
+
3
+ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
4
+ import clsx from "clsx";
5
+ import { forwardRef, useEffect, useMemo, useState } from "react";
6
+
7
+ import type { DropdownContainerProps } from "../../types/props";
8
+ import { useDropdownContext } from "./Provider";
9
+
10
+ /**
11
+ * Dropdown container; trigger width 동기화 및 portal 관리
12
+ * @component
13
+ * @param {DropdownContainerProps} props Dropdown container props
14
+ * @param {DropdownPanelWidth} [props.width="match"] panel width 옵션
15
+ * @param {DropdownSize} [props.size="medium"] option height scale
16
+ */
17
+ const DropdownContainer = forwardRef<HTMLDivElement, DropdownContainerProps>(
18
+ (
19
+ {
20
+ children,
21
+ className,
22
+ size = "medium",
23
+ width = "match",
24
+ portalContainer,
25
+ align = "start",
26
+ side = "bottom",
27
+ sideOffset = 4,
28
+ alignOffset,
29
+ style,
30
+ ...rest
31
+ },
32
+ ref,
33
+ ) => {
34
+ const { triggerRef } = useDropdownContext("Dropdown.Container");
35
+ const [panelWidth, setPanelWidth] = useState<number>();
36
+ const shouldMatchTriggerWidth = width === "match";
37
+
38
+ const resolvedMinWidth = useMemo(() => {
39
+ if (shouldMatchTriggerWidth) {
40
+ return undefined;
41
+ }
42
+
43
+ if (typeof width === "number") {
44
+ return `${width}px`;
45
+ }
46
+
47
+ if (typeof width === "string") {
48
+ if (width === "fit-content") {
49
+ return "fit-content";
50
+ }
51
+ if (width === "max-content") {
52
+ return "max-content";
53
+ }
54
+ if (width.trim().length > 0 && width !== "match") {
55
+ return width;
56
+ }
57
+ }
58
+
59
+ return undefined;
60
+ }, [shouldMatchTriggerWidth, width]);
61
+
62
+ useEffect(() => {
63
+ if (!shouldMatchTriggerWidth) {
64
+ return;
65
+ }
66
+
67
+ const node = triggerRef.current;
68
+ if (!node) {
69
+ return;
70
+ }
71
+
72
+ const updateWidth = () => {
73
+ setPanelWidth(node.getBoundingClientRect().width);
74
+ };
75
+
76
+ updateWidth();
77
+
78
+ if (typeof ResizeObserver === "undefined") {
79
+ return;
80
+ }
81
+
82
+ const observer = new ResizeObserver(() => updateWidth());
83
+ observer.observe(node);
84
+
85
+ return () => {
86
+ observer.disconnect();
87
+ };
88
+ }, [shouldMatchTriggerWidth, triggerRef]);
89
+
90
+ const content = (
91
+ <DropdownMenu.Content
92
+ {...rest}
93
+ ref={ref}
94
+ align={align}
95
+ side={side}
96
+ sideOffset={sideOffset}
97
+ alignOffset={alignOffset}
98
+ className={clsx("dropdown-panel", `dropdown-panel-${size}`, className)}
99
+ style={{
100
+ ...style,
101
+ width:
102
+ shouldMatchTriggerWidth && panelWidth ? panelWidth : style?.width,
103
+ minWidth:
104
+ resolvedMinWidth !== undefined ? resolvedMinWidth : style?.minWidth,
105
+ }}
106
+ >
107
+ {children}
108
+ </DropdownMenu.Content>
109
+ );
110
+
111
+ if (portalContainer === null) {
112
+ return content;
113
+ }
114
+
115
+ return (
116
+ <DropdownMenu.Portal container={portalContainer ?? undefined}>
117
+ {content}
118
+ </DropdownMenu.Portal>
119
+ );
120
+ },
121
+ );
122
+
123
+ DropdownContainer.displayName = "DropdownContainer";
124
+
125
+ export default DropdownContainer;
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
4
+ import clsx from "clsx";
5
+ import { forwardRef } from "react";
6
+
7
+ import type { DropdownMenuItemProps } from "../../types/props";
8
+ import { Checkbox } from "../../../checkbox/markup/Checkbox";
9
+ import type { CheckboxProps } from "../../../checkbox/types";
10
+
11
+ /**
12
+ * Dropdown menu item; label/description/slot 구성을 처리한다.
13
+ * @component
14
+ * @param {DropdownMenuItemProps} props dropdown menu option props
15
+ * @param {boolean} [props.isSelected] 선택 상태
16
+ * @param {boolean} [props.multiple] multi select 스타일 여부
17
+ */
18
+ const DropdownMenuItem = forwardRef<HTMLDivElement, DropdownMenuItemProps>(
19
+ (
20
+ {
21
+ label,
22
+ description,
23
+ left,
24
+ right,
25
+ isSelected = false,
26
+ multiple = false,
27
+ className,
28
+ checkboxProps,
29
+ children,
30
+ onSelect: onSelectHandler,
31
+ ...rest
32
+ },
33
+ ref,
34
+ ) => {
35
+ const labelContent = label ?? children;
36
+ const hasDescription = Boolean(description);
37
+ const shouldRenderCheckbox = multiple && !left;
38
+
39
+ const renderLeft = () => {
40
+ if (shouldRenderCheckbox) {
41
+ const checkboxClassName = clsx(
42
+ "dropdown-menu-item-checkbox",
43
+ checkboxProps?.className,
44
+ );
45
+
46
+ const checkboxAdditionalProps: CheckboxProps = {
47
+ ...checkboxProps,
48
+ checked: isSelected,
49
+ onCheckedChange: checkboxProps?.onCheckedChange ?? (() => {}),
50
+ tabIndex: -1,
51
+ "aria-hidden": true,
52
+ className: checkboxClassName,
53
+ } as CheckboxProps;
54
+
55
+ return (
56
+ <span className="dropdown-menu-item-left" aria-hidden="true">
57
+ <Checkbox size="medium" {...checkboxAdditionalProps} />
58
+ </span>
59
+ );
60
+ }
61
+
62
+ if (left) {
63
+ return <span className="dropdown-menu-item-left">{left}</span>;
64
+ }
65
+
66
+ return null;
67
+ };
68
+
69
+ return (
70
+ <li className="dropdown-menu-item">
71
+ <DropdownMenu.Item
72
+ {...rest}
73
+ ref={ref}
74
+ className={clsx("dropdown-menu-item-trigger", className)}
75
+ data-state={isSelected ? "selected" : undefined}
76
+ data-multiple={multiple ? "true" : undefined}
77
+ data-has-description={hasDescription ? "true" : undefined}
78
+ onSelect={event => {
79
+ if (multiple) {
80
+ event.preventDefault();
81
+ }
82
+ onSelectHandler?.(event);
83
+ }}
84
+ >
85
+ {renderLeft()}
86
+ <span className="dropdown-menu-item-body">
87
+ {labelContent ? (
88
+ <span className="dropdown-menu-item-label">{labelContent}</span>
89
+ ) : null}
90
+ {description ? (
91
+ <span className="dropdown-menu-item-description">
92
+ {description}
93
+ </span>
94
+ ) : null}
95
+ </span>
96
+ {right ? (
97
+ <span className="dropdown-menu-item-right">{right}</span>
98
+ ) : null}
99
+ </DropdownMenu.Item>
100
+ </li>
101
+ );
102
+ },
103
+ );
104
+
105
+ DropdownMenuItem.displayName = "DropdownMenuItem";
106
+
107
+ export default DropdownMenuItem;
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import { forwardRef } from "react";
5
+
6
+ import type { DropdownMenuListProps } from "../../types/props";
7
+
8
+ /**
9
+ * Dropdown option list wrapper
10
+ * @component
11
+ * @param {DropdownMenuListProps} props list attributes
12
+ */
13
+ const DropdownMenuList = forwardRef<HTMLUListElement, DropdownMenuListProps>(
14
+ ({ className, ...rest }, ref) => {
15
+ return (
16
+ <ul
17
+ {...rest}
18
+ ref={ref}
19
+ className={clsx("dropdown-menu-list", className)}
20
+ />
21
+ );
22
+ },
23
+ );
24
+
25
+ DropdownMenuList.displayName = "DropdownMenuList";
26
+
27
+ export default DropdownMenuList;