sh-ui-cli 0.48.0 → 0.50.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 (93) hide show
  1. package/data/changelog/versions.json +27 -0
  2. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.css.ts +131 -0
  4. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.css.ts +68 -0
  6. package/data/registry/react/components/badge/index.vanilla-extract.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.css.ts +71 -0
  8. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.css.ts +95 -0
  10. package/data/registry/react/components/button/index.vanilla-extract.tsx +45 -0
  11. package/data/registry/react/components/button/styles.css.ts +120 -0
  12. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +806 -0
  13. package/data/registry/react/components/calendar/styles.css.ts +250 -0
  14. package/data/registry/react/components/card/index.vanilla-extract.tsx +63 -0
  15. package/data/registry/react/components/card/styles.css.ts +88 -0
  16. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +430 -0
  17. package/data/registry/react/components/carousel/styles.css.ts +169 -0
  18. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +96 -0
  19. package/data/registry/react/components/checkbox/styles.css.ts +74 -0
  20. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +230 -0
  21. package/data/registry/react/components/code-editor/styles.css.ts +97 -0
  22. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +191 -0
  23. package/data/registry/react/components/code-panel/styles.css.ts +151 -0
  24. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +467 -0
  25. package/data/registry/react/components/color-picker/styles.css.ts +169 -0
  26. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +165 -0
  27. package/data/registry/react/components/combobox/styles.css.ts +174 -0
  28. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +251 -0
  29. package/data/registry/react/components/context-menu/styles.css.ts +167 -0
  30. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +520 -0
  31. package/data/registry/react/components/date-picker/styles.css.ts +111 -0
  32. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +95 -0
  33. package/data/registry/react/components/dialog/styles.css.ts +140 -0
  34. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +255 -0
  35. package/data/registry/react/components/dropdown-menu/styles.css.ts +175 -0
  36. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +487 -0
  37. package/data/registry/react/components/file-upload/styles.css.ts +193 -0
  38. package/data/registry/react/components/form/index.vanilla-extract.tsx +61 -0
  39. package/data/registry/react/components/form/styles.css.ts +56 -0
  40. package/data/registry/react/components/header/index.vanilla-extract.tsx +805 -0
  41. package/data/registry/react/components/header/styles.css.ts +413 -0
  42. package/data/registry/react/components/input/index.vanilla-extract.tsx +425 -0
  43. package/data/registry/react/components/input/styles.css.ts +202 -0
  44. package/data/registry/react/components/label/index.vanilla-extract.tsx +52 -0
  45. package/data/registry/react/components/label/styles.css.ts +141 -0
  46. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +119 -0
  47. package/data/registry/react/components/markdown-editor/styles.css.ts +231 -0
  48. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +32 -0
  49. package/data/registry/react/components/menubar/styles.css.ts +53 -0
  50. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +148 -0
  51. package/data/registry/react/components/numeric-input/styles.css.ts +65 -0
  52. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +174 -0
  53. package/data/registry/react/components/page-toc/styles.css.ts +97 -0
  54. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +269 -0
  55. package/data/registry/react/components/pagination/styles.css.ts +113 -0
  56. package/data/registry/react/components/popover/index.vanilla-extract.tsx +113 -0
  57. package/data/registry/react/components/popover/styles.css.ts +78 -0
  58. package/data/registry/react/components/progress/index.vanilla-extract.tsx +54 -0
  59. package/data/registry/react/components/progress/styles.css.ts +53 -0
  60. package/data/registry/react/components/radio/index.vanilla-extract.tsx +65 -0
  61. package/data/registry/react/components/radio/styles.css.ts +79 -0
  62. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +348 -0
  63. package/data/registry/react/components/rich-text-editor/styles.css.ts +243 -0
  64. package/data/registry/react/components/select/index.vanilla-extract.tsx +234 -0
  65. package/data/registry/react/components/select/styles.css.ts +225 -0
  66. package/data/registry/react/components/separator/index.vanilla-extract.tsx +46 -0
  67. package/data/registry/react/components/separator/styles.css.ts +24 -0
  68. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +1067 -0
  69. package/data/registry/react/components/sidebar/styles.css.ts +578 -0
  70. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +22 -0
  71. package/data/registry/react/components/skeleton/styles.css.ts +30 -0
  72. package/data/registry/react/components/slider/index.vanilla-extract.tsx +298 -0
  73. package/data/registry/react/components/slider/styles.css.ts +75 -0
  74. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +38 -0
  75. package/data/registry/react/components/spinner/styles.css.ts +60 -0
  76. package/data/registry/react/components/switch/index.vanilla-extract.tsx +39 -0
  77. package/data/registry/react/components/switch/styles.css.ts +87 -0
  78. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +91 -0
  79. package/data/registry/react/components/tabs/styles.css.ts +145 -0
  80. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +23 -0
  81. package/data/registry/react/components/textarea/styles.css.ts +55 -0
  82. package/data/registry/react/components/toast/index.vanilla-extract.tsx +258 -0
  83. package/data/registry/react/components/toast/styles.css.ts +307 -0
  84. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +131 -0
  85. package/data/registry/react/components/toggle/styles.css.ts +109 -0
  86. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +83 -0
  87. package/data/registry/react/components/tooltip/styles.css.ts +59 -0
  88. package/data/registry/react/peer-versions.json +1 -0
  89. package/data/registry/react/registry.json +922 -42
  90. package/data/tokens/build.mjs +3 -0
  91. package/package.json +1 -1
  92. package/src/api.d.ts +4 -3
  93. package/src/constants.js +4 -3
@@ -0,0 +1,243 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const rte = style({
4
+ display: "flex",
5
+ flexDirection: "column",
6
+ border: "1px solid var(--border)",
7
+ borderRadius: "var(--radius)",
8
+ background: "var(--background)",
9
+ overflow: "hidden",
10
+ transition: "border-color var(--duration-fast)",
11
+ selectors: {
12
+ "&:focus-within": {
13
+ borderColor: "var(--foreground)",
14
+ outline: "var(--border-width-strong) solid var(--foreground)",
15
+ outlineOffset: "2px",
16
+ },
17
+ "&[data-readonly]": {
18
+ background: "var(--background-subtle)",
19
+ },
20
+ },
21
+ });
22
+
23
+ export const rte__toolbar = style({
24
+ display: "flex",
25
+ flexWrap: "wrap",
26
+ alignItems: "center",
27
+ gap: "0.125rem",
28
+ padding: "var(--space-1) var(--space-2)",
29
+ background: "var(--background-muted)",
30
+ borderBottom: "1px solid var(--border)",
31
+ });
32
+
33
+ export const rte__btn = style({
34
+ display: "inline-flex",
35
+ alignItems: "center",
36
+ justifyContent: "center",
37
+ width: "1.875rem",
38
+ height: "1.875rem",
39
+ padding: 0,
40
+ background: "transparent",
41
+ color: "var(--foreground-muted)",
42
+ border: "1px solid transparent",
43
+ borderRadius: "calc(var(--radius) - 2px)",
44
+ cursor: "pointer",
45
+ transition: "color var(--duration-fast),\n background-color var(--duration-fast),\n border-color var(--duration-fast)",
46
+ WebkitTapHighlightColor: "transparent",
47
+ selectors: {
48
+ "&:hover:not(:disabled)": {
49
+ color: "var(--foreground)",
50
+ background: "var(--background)",
51
+ borderColor: "var(--border)",
52
+ },
53
+ "&:focus-visible": {
54
+ outline: "var(--border-width-strong) solid var(--foreground)",
55
+ outlineOffset: "1px",
56
+ },
57
+ "&.is-active": {
58
+ color: "var(--foreground)",
59
+ background: "var(--background)",
60
+ borderColor: "var(--border-strong)",
61
+ },
62
+ "&:disabled": {
63
+ opacity: 0.5,
64
+ cursor: "not-allowed",
65
+ },
66
+ },
67
+ });
68
+
69
+ export const rte__sep = style({
70
+ display: "inline-block",
71
+ width: "1px",
72
+ height: "1.25rem",
73
+ margin: "0 var(--space-1)",
74
+ background: "var(--border)",
75
+ });
76
+
77
+ export const rte__viewport = style({
78
+ display: "flex",
79
+ minHeight: "var(--sh-ui-rte-min-height, 9rem)",
80
+ maxHeight: "var(--sh-ui-rte-max-height, 28rem)",
81
+ overflowY: "auto",
82
+ selectors: {
83
+ "& > .ProseMirror": {
84
+ flex: 1,
85
+ },
86
+ },
87
+ });
88
+
89
+ export const rte__content = style({
90
+ outline: "none",
91
+ padding: "var(--space-3) var(--space-4)",
92
+ fontSize: "0.9375rem",
93
+ lineHeight: 1.65,
94
+ color: "var(--foreground)",
95
+ selectors: {
96
+ "& > :first-child": {
97
+ marginTop: 0,
98
+ },
99
+ "& > :last-child": {
100
+ marginBottom: 0,
101
+ },
102
+ "& p": {
103
+ margin: "0 0 var(--space-3)",
104
+ },
105
+ "& h1": {
106
+ margin: "var(--space-4) 0 var(--space-2)",
107
+ fontWeight: 600,
108
+ lineHeight: 1.3,
109
+ },
110
+ "& h2": {
111
+ margin: "var(--space-4) 0 var(--space-2)",
112
+ fontWeight: 600,
113
+ lineHeight: 1.3,
114
+ },
115
+ "& h3": {
116
+ margin: "var(--space-4) 0 var(--space-2)",
117
+ fontWeight: 600,
118
+ lineHeight: 1.3,
119
+ },
120
+ "& h4": {
121
+ margin: "var(--space-4) 0 var(--space-2)",
122
+ fontWeight: 600,
123
+ lineHeight: 1.3,
124
+ },
125
+ "& h5": {
126
+ margin: "var(--space-4) 0 var(--space-2)",
127
+ fontWeight: 600,
128
+ lineHeight: 1.3,
129
+ },
130
+ "& h6": {
131
+ margin: "var(--space-4) 0 var(--space-2)",
132
+ fontWeight: 600,
133
+ lineHeight: 1.3,
134
+ },
135
+ "& h1": {
136
+ fontSize: "1.5rem",
137
+ },
138
+ "& h2": {
139
+ fontSize: "1.25rem",
140
+ },
141
+ "& h3": {
142
+ fontSize: "1.125rem",
143
+ },
144
+ "& ul": {
145
+ margin: "0 0 var(--space-3)",
146
+ paddingLeft: "var(--space-5)",
147
+ },
148
+ "& ol": {
149
+ margin: "0 0 var(--space-3)",
150
+ paddingLeft: "var(--space-5)",
151
+ },
152
+ "& li": {
153
+ marginBottom: "var(--space-1)",
154
+ },
155
+ "& li > p": {
156
+ margin: 0,
157
+ },
158
+ "& blockquote": {
159
+ margin: "0 0 var(--space-3)",
160
+ padding: "var(--space-2) var(--space-3)",
161
+ borderLeft: "3px solid var(--border-strong)",
162
+ background: "var(--background-subtle)",
163
+ color: "var(--foreground-muted)",
164
+ borderRadius: "0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0",
165
+ },
166
+ "& blockquote > :last-child": {
167
+ marginBottom: 0,
168
+ },
169
+ "& code": {
170
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
171
+ fontSize: "0.875em",
172
+ padding: "0.125rem 0.375rem",
173
+ borderRadius: "calc(var(--radius) - 4px)",
174
+ background: "var(--background-muted)",
175
+ color: "var(--foreground)",
176
+ },
177
+ "& pre": {
178
+ margin: "0 0 var(--space-3)",
179
+ padding: "var(--space-3)",
180
+ border: "1px solid var(--border)",
181
+ borderRadius: "var(--radius)",
182
+ background: "var(--background-subtle)",
183
+ overflowX: "auto",
184
+ fontSize: "0.8125rem",
185
+ lineHeight: 1.6,
186
+ },
187
+ "& pre code": {
188
+ padding: 0,
189
+ background: "transparent",
190
+ fontSize: "inherit",
191
+ },
192
+ "& hr": {
193
+ border: 0,
194
+ borderTop: "1px solid var(--border)",
195
+ margin: "var(--space-4) 0",
196
+ },
197
+ "& a": {
198
+ color: "var(--primary)",
199
+ textDecoration: "underline",
200
+ textUnderlineOffset: "2px",
201
+ },
202
+ "& a:hover": {
203
+ textDecorationThickness: "2px",
204
+ },
205
+ "& p.is-editor-empty:first-child::before": {
206
+ content: "attr(data-placeholder)",
207
+ color: "var(--foreground-muted)",
208
+ float: "left",
209
+ pointerEvents: "none",
210
+ height: 0,
211
+ },
212
+ "& .is-editor-empty:first-child::before": {
213
+ content: "attr(data-placeholder)",
214
+ color: "var(--foreground-muted)",
215
+ float: "left",
216
+ pointerEvents: "none",
217
+ height: 0,
218
+ },
219
+ "& del": {
220
+ color: "var(--foreground-muted)",
221
+ },
222
+ "& s": {
223
+ color: "var(--foreground-muted)",
224
+ },
225
+ "& ::selection": {
226
+ background: "var(--background-muted)",
227
+ },
228
+ },
229
+ });
230
+
231
+ export const rteIsEmpty = style({
232
+ });
233
+
234
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
235
+ export const byKey: Record<string, string> = {
236
+ "rte": rte,
237
+ "rte__toolbar": rte__toolbar,
238
+ "rte__btn": rte__btn,
239
+ "rte__sep": rte__sep,
240
+ "rte__viewport": rte__viewport,
241
+ "rte__content": rte__content,
242
+ "rte__is-empty": rteIsEmpty,
243
+ };
@@ -0,0 +1,234 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Select as BaseSelect } from "@base-ui/react/select";
5
+ import { byKey, select__trigger, select__value, select__placeholder, select__icon, select__positioner, select__content, select__label, select__item, selectItemText, select__indicator, select__separator, select__chips, select__chip, selectChipRemove } from "./styles.css";
6
+
7
+
8
+ import { cn } from "@SH_UI_UTILS@";
9
+ export const Select = BaseSelect.Root;
10
+
11
+ /** shadcn 호환: <SelectValue placeholder="..." /> */
12
+ export function SelectValue({
13
+ placeholder,
14
+ className,
15
+ ...props
16
+ }: { placeholder?: string; className?: string } & Omit<
17
+ React.ComponentPropsWithoutRef<typeof BaseSelect.Value>,
18
+ "children"
19
+ >) {
20
+ return (
21
+ <BaseSelect.Value className={cn(select__value, className)} {...props}>
22
+ {(value) =>
23
+ value !== null && value !== undefined && value !== "" ? (
24
+ (value as React.ReactNode)
25
+ ) : (
26
+ <span className={select__placeholder}>{placeholder}</span>
27
+ )
28
+ }
29
+ </BaseSelect.Value>
30
+ );
31
+ }
32
+
33
+ export const SelectTrigger = React.forwardRef<
34
+ HTMLButtonElement,
35
+ Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Trigger>, "className"> & { className?: string }
36
+ >(({ className, children, ...props }, ref) => (
37
+ <BaseSelect.Trigger
38
+ ref={ref}
39
+ className={cn(select__trigger, className)}
40
+ {...props}
41
+ >
42
+ {children}
43
+ <BaseSelect.Icon className={select__icon} aria-hidden>
44
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none">
45
+ <path
46
+ d="M4 6l4 4 4-4"
47
+ stroke="currentColor"
48
+ strokeWidth="1.5"
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ />
52
+ </svg>
53
+ </BaseSelect.Icon>
54
+ </BaseSelect.Trigger>
55
+ ));
56
+ SelectTrigger.displayName = "SelectTrigger";
57
+
58
+ /** Portal + Positioner + Popup을 한 번에.
59
+ * container: portal이 마운트될 DOM 노드. 기본 body. 토큰 스코프 안에 띄우려면 해당 컨테이너 ref 전달. */
60
+ export const SelectContent = React.forwardRef<
61
+ HTMLDivElement,
62
+ Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Popup>, "className"> & {
63
+ className?: string;
64
+ container?: React.ComponentPropsWithoutRef<typeof BaseSelect.Portal>["container"];
65
+ }
66
+ >(({ className, children, container, ...props }, ref) => (
67
+ <BaseSelect.Portal container={container}>
68
+ <BaseSelect.Positioner
69
+ className={select__positioner}
70
+ sideOffset={4}
71
+ align="start"
72
+ >
73
+ <BaseSelect.Popup
74
+ ref={ref}
75
+ className={cn(select__content, className)}
76
+ {...props}
77
+ >
78
+ {children}
79
+ </BaseSelect.Popup>
80
+ </BaseSelect.Positioner>
81
+ </BaseSelect.Portal>
82
+ ));
83
+ SelectContent.displayName = "SelectContent";
84
+
85
+ export const SelectGroup = BaseSelect.Group;
86
+
87
+ export const SelectLabel = React.forwardRef<
88
+ HTMLDivElement,
89
+ Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.GroupLabel>, "className"> & { className?: string }
90
+ >(({ className, ...props }, ref) => (
91
+ <BaseSelect.GroupLabel
92
+ ref={ref}
93
+ className={cn(select__label, className)}
94
+ {...props}
95
+ />
96
+ ));
97
+ SelectLabel.displayName = "SelectLabel";
98
+
99
+ export const SelectItem = React.forwardRef<
100
+ HTMLDivElement,
101
+ Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Item>, "className"> & { className?: string }
102
+ >(({ className, children, ...props }, ref) => (
103
+ <BaseSelect.Item
104
+ ref={ref}
105
+ className={cn(select__item, className)}
106
+ {...props}
107
+ >
108
+ <BaseSelect.ItemIndicator className={select__indicator} aria-hidden>
109
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none">
110
+ <path
111
+ d="M3.5 8.5l3 3 6-7"
112
+ stroke="currentColor"
113
+ strokeWidth="1.75"
114
+ strokeLinecap="round"
115
+ strokeLinejoin="round"
116
+ />
117
+ </svg>
118
+ </BaseSelect.ItemIndicator>
119
+ <BaseSelect.ItemText className={selectItemText}>
120
+ {children}
121
+ </BaseSelect.ItemText>
122
+ </BaseSelect.Item>
123
+ ));
124
+ SelectItem.displayName = "SelectItem";
125
+
126
+ export const SelectSeparator = React.forwardRef<
127
+ HTMLDivElement,
128
+ Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Separator>, "className"> & { className?: string }
129
+ >(({ className, ...props }, ref) => (
130
+ <BaseSelect.Separator
131
+ ref={ref}
132
+ className={cn(select__separator, className)}
133
+ {...props}
134
+ />
135
+ ));
136
+ SelectSeparator.displayName = "SelectSeparator";
137
+
138
+ /* ───────── Multi-select ─────────
139
+ *
140
+ * Base UI Select의 `multiple` 모드를 얇게 래핑한 것.
141
+ * Trigger/Content/Item/Group/Label/Separator는 그대로 재사용한다.
142
+ */
143
+
144
+ type BaseRootProps = React.ComponentPropsWithoutRef<typeof BaseSelect.Root>;
145
+
146
+ /** 칩 X 버튼 등에서 개별 항목을 제거할 수 있도록 MultiSelect 내부 상태를 expose한다. */
147
+ type MultiSelectCtx = {
148
+ values: string[];
149
+ remove: (value: string) => void;
150
+ clear: () => void;
151
+ };
152
+ const MultiSelectContext = React.createContext<MultiSelectCtx | null>(null);
153
+ export const useMultiSelect = () => {
154
+ const ctx = React.useContext(MultiSelectContext);
155
+ if (!ctx) throw new Error("useMultiSelect는 MultiSelect 하위에서만 사용할 수 있습니다.");
156
+ return ctx;
157
+ };
158
+
159
+ export const MultiSelect = React.forwardRef<
160
+ HTMLDivElement,
161
+ Omit<BaseRootProps, "multiple" | "value" | "defaultValue" | "onValueChange"> & {
162
+ value?: string[];
163
+ defaultValue?: string[];
164
+ onValueChange?: (value: string[]) => void;
165
+ }
166
+ >(({ value: valueProp, defaultValue, onValueChange, children, ...props }, _ref) => {
167
+ const isControlled = valueProp !== undefined;
168
+ const [internal, setInternal] = React.useState<string[]>(defaultValue ?? []);
169
+ const values = isControlled ? valueProp! : internal;
170
+
171
+ const commit = React.useCallback(
172
+ (next: string[]) => {
173
+ if (!isControlled) setInternal(next);
174
+ onValueChange?.(next);
175
+ },
176
+ [isControlled, onValueChange],
177
+ );
178
+
179
+ const ctx = React.useMemo<MultiSelectCtx>(
180
+ () => ({
181
+ values,
182
+ remove: (v) => commit(values.filter((x) => x !== v)),
183
+ clear: () => commit([]),
184
+ }),
185
+ [values, commit],
186
+ );
187
+
188
+ return (
189
+ <MultiSelectContext.Provider value={ctx}>
190
+ <BaseSelect.Root multiple value={values} onValueChange={commit} {...props}>
191
+ {children}
192
+ </BaseSelect.Root>
193
+ </MultiSelectContext.Provider>
194
+ );
195
+ });
196
+ MultiSelect.displayName = "MultiSelect";
197
+
198
+ /**
199
+ * 다중 선택 값 표시. 배열이 비면 placeholder, 있으면 join 또는 사용자 정의 renderer.
200
+ *
201
+ * <MultiSelectValue placeholder="과일" /> // "Apple, Banana"
202
+ * <MultiSelectValue placeholder="과일" render={(arr) => ...} /> // 커스텀 (칩 등)
203
+ */
204
+ export function MultiSelectValue({
205
+ placeholder,
206
+ render,
207
+ separator = ", ",
208
+ className,
209
+ ...props
210
+ }: {
211
+ placeholder?: string;
212
+ render?: (
213
+ values: string[],
214
+ handlers: { remove: (value: string) => void; clear: () => void },
215
+ ) => React.ReactNode;
216
+ separator?: string;
217
+ className?: string;
218
+ } & Omit<
219
+ React.ComponentPropsWithoutRef<typeof BaseSelect.Value>,
220
+ "children" | "render"
221
+ >) {
222
+ const { remove, clear } = useMultiSelect();
223
+ return (
224
+ <BaseSelect.Value className={cn(select__value, className)} {...props}>
225
+ {(value) => {
226
+ const arr = Array.isArray(value) ? (value as string[]) : [];
227
+ if (arr.length === 0) {
228
+ return <span className={select__placeholder}>{placeholder}</span>;
229
+ }
230
+ return render ? render(arr, { remove, clear }) : arr.join(separator);
231
+ }}
232
+ </BaseSelect.Value>
233
+ );
234
+ }
@@ -0,0 +1,225 @@
1
+ import { style, keyframes } from "@vanilla-extract/css";
2
+
3
+ export const shUiSelectIn = keyframes({
4
+ "from": {
5
+ opacity: 0,
6
+ transform: "scale(0.96)",
7
+ },
8
+ "to": {
9
+ opacity: 1,
10
+ transform: "scale(1)",
11
+ },
12
+ });
13
+
14
+ export const shUiSelectOut = keyframes({
15
+ "from": {
16
+ opacity: 1,
17
+ transform: "scale(1)",
18
+ },
19
+ "to": {
20
+ opacity: 0,
21
+ transform: "scale(0.96)",
22
+ },
23
+ });
24
+
25
+ export const select__trigger = style({
26
+ display: "inline-flex",
27
+ alignItems: "center",
28
+ justifyContent: "space-between",
29
+ gap: "var(--space-2)",
30
+ minWidth: "10rem",
31
+ height: "var(--control-md)",
32
+ padding: "0 var(--space-3)",
33
+ background: "var(--background)",
34
+ color: "var(--foreground)",
35
+ border: "1px solid var(--border)",
36
+ borderRadius: "var(--radius)",
37
+ fontSize: "var(--text-sm)",
38
+ lineHeight: 1,
39
+ cursor: "pointer",
40
+ transition: "border-color var(--duration-fast), background-color var(--duration-fast)",
41
+ userSelect: "none",
42
+ WebkitTapHighlightColor: "transparent",
43
+ selectors: {
44
+ "&:hover:not(:disabled)": {
45
+ borderColor: "var(--border-strong)",
46
+ },
47
+ "&:focus-visible": {
48
+ outline: "var(--border-width-strong) solid var(--foreground)",
49
+ outlineOffset: "2px",
50
+ },
51
+ "&[data-popup-open]": {
52
+ borderColor: "var(--border-strong)",
53
+ },
54
+ "&:disabled": {
55
+ opacity: "var(--opacity-disabled)",
56
+ pointerEvents: "none",
57
+ },
58
+ [`&[data-popup-open] ${select__icon}`]: {
59
+ transform: "rotate(180deg)",
60
+ },
61
+ },
62
+ });
63
+
64
+ export const select__value = style({
65
+ flex: "1 1 auto",
66
+ textAlign: "left",
67
+ overflow: "hidden",
68
+ textOverflow: "ellipsis",
69
+ whiteSpace: "nowrap",
70
+ });
71
+
72
+ export const select__placeholder = style({
73
+ color: "var(--foreground-subtle)",
74
+ });
75
+
76
+ export const select__icon = style({
77
+ display: "inline-flex",
78
+ alignItems: "center",
79
+ justifyContent: "center",
80
+ color: "var(--foreground-muted)",
81
+ flexShrink: 0,
82
+ transition: "transform var(--duration-fast)",
83
+ });
84
+
85
+ export const select__positioner = style({
86
+ outline: "none",
87
+ zIndex: "var(--z-dropdown)",
88
+ });
89
+
90
+ export const select__content = style({
91
+ minWidth: "var(--anchor-width, 10rem)",
92
+ maxHeight: "min(24rem, var(--available-height, 24rem))",
93
+ overflowY: "auto",
94
+ padding: "var(--space-1)",
95
+ background: "var(--background)",
96
+ color: "var(--foreground)",
97
+ border: "1px solid var(--border)",
98
+ borderRadius: "var(--radius)",
99
+ boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.08),\n 0 2px 4px -2px rgba(0, 0, 0, 0.05)",
100
+ fontSize: "var(--text-sm)",
101
+ transformOrigin: "var(--transform-origin)",
102
+ animation: "sh-ui-select-in 140ms ease-out",
103
+ selectors: {
104
+ "&[data-ending-style]": {
105
+ animation: "sh-ui-select-out 100ms ease-in forwards",
106
+ },
107
+ },
108
+ });
109
+
110
+ export const select__label = style({
111
+ padding: "var(--space-2) var(--space-2) var(--space-1)",
112
+ fontSize: "var(--text-xs)",
113
+ fontWeight: "var(--weight-semibold)",
114
+ color: "var(--foreground-muted)",
115
+ textTransform: "uppercase",
116
+ letterSpacing: "0.04em",
117
+ });
118
+
119
+ export const select__item = style({
120
+ display: "flex",
121
+ alignItems: "center",
122
+ gap: "var(--space-2)",
123
+ padding: "0.5rem 0.75rem",
124
+ borderRadius: "calc(var(--radius) - 2px)",
125
+ cursor: "pointer",
126
+ outline: "none",
127
+ userSelect: "none",
128
+ transition: "background-color 80ms",
129
+ selectors: {
130
+ "&[data-highlighted]": {
131
+ background: "var(--background-muted)",
132
+ },
133
+ "&:hover": {
134
+ background: "var(--background-muted)",
135
+ },
136
+ "&[data-disabled]": {
137
+ opacity: "var(--opacity-disabled)",
138
+ pointerEvents: "none",
139
+ },
140
+ },
141
+ });
142
+
143
+ export const selectItemText = style({
144
+ flex: 1,
145
+ });
146
+
147
+ export const select__indicator = style({
148
+ order: 1,
149
+ marginLeft: "auto",
150
+ display: "inline-flex",
151
+ alignItems: "center",
152
+ justifyContent: "center",
153
+ color: "var(--foreground)",
154
+ });
155
+
156
+ export const select__separator = style({
157
+ height: "1px",
158
+ background: "var(--border)",
159
+ margin: "var(--space-1) 0",
160
+ });
161
+
162
+ export const select__chips = style({
163
+ display: "inline-flex",
164
+ alignItems: "center",
165
+ gap: "var(--space-1)",
166
+ flexWrap: "nowrap",
167
+ overflow: "hidden",
168
+ });
169
+
170
+ export const select__chip = style({
171
+ display: "inline-flex",
172
+ alignItems: "center",
173
+ gap: "var(--space-1)",
174
+ padding: "0.125rem 0.375rem 0.125rem var(--space-2)",
175
+ fontSize: "var(--text-xs)",
176
+ lineHeight: "1.25rem",
177
+ background: "var(--background-muted)",
178
+ borderRadius: "calc(var(--radius) - 2px)",
179
+ whiteSpace: "nowrap",
180
+ });
181
+
182
+ export const selectChipRemove = style({
183
+ display: "inline-flex",
184
+ alignItems: "center",
185
+ justifyContent: "center",
186
+ width: "1rem",
187
+ height: "1rem",
188
+ padding: 0,
189
+ border: 0,
190
+ borderRadius: "999px",
191
+ background: "transparent",
192
+ color: "var(--foreground-muted)",
193
+ fontSize: "var(--text-sm)",
194
+ lineHeight: 1,
195
+ cursor: "pointer",
196
+ transition: "background-color var(--duration-fast), color var(--duration-fast)",
197
+ selectors: {
198
+ "&:hover": {
199
+ background: "var(--background)",
200
+ color: "var(--foreground)",
201
+ },
202
+ "&:focus-visible": {
203
+ outline: "var(--border-width-strong) solid var(--foreground)",
204
+ outlineOffset: "1px",
205
+ },
206
+ },
207
+ });
208
+
209
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
210
+ export const byKey: Record<string, string> = {
211
+ "select__trigger": select__trigger,
212
+ "select__value": select__value,
213
+ "select__placeholder": select__placeholder,
214
+ "select__icon": select__icon,
215
+ "select__positioner": select__positioner,
216
+ "select__content": select__content,
217
+ "select__label": select__label,
218
+ "select__item": select__item,
219
+ "select__item-text": selectItemText,
220
+ "select__indicator": select__indicator,
221
+ "select__separator": select__separator,
222
+ "select__chips": select__chips,
223
+ "select__chip": select__chip,
224
+ "select__chip-remove": selectChipRemove,
225
+ };