sh-ui-cli 0.43.0 → 0.45.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 (46) hide show
  1. package/data/changelog/versions.json +24 -0
  2. package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
  3. package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
  4. package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
  5. package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
  6. package/data/registry/react/components/calendar/index.tailwind.tsx +498 -0
  7. package/data/registry/react/components/carousel/index.tailwind.tsx +309 -0
  8. package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
  9. package/data/registry/react/components/code-editor/index.tailwind.tsx +168 -0
  10. package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
  11. package/data/registry/react/components/color-picker/index.tailwind.tsx +309 -0
  12. package/data/registry/react/components/combobox/index.tailwind.tsx +160 -0
  13. package/data/registry/react/components/context-menu/index.tailwind.tsx +170 -0
  14. package/data/registry/react/components/date-picker/index.tailwind.tsx +294 -0
  15. package/data/registry/react/components/dialog/index.tailwind.tsx +96 -0
  16. package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +205 -0
  17. package/data/registry/react/components/file-upload/index.tailwind.tsx +290 -0
  18. package/data/registry/react/components/form/field.tailwind.tsx +165 -0
  19. package/data/registry/react/components/form/form.tailwind.tsx +129 -0
  20. package/data/registry/react/components/form/index.tailwind.tsx +49 -0
  21. package/data/registry/react/components/header/index.tailwind.tsx +550 -0
  22. package/data/registry/react/components/label/index.tailwind.tsx +78 -0
  23. package/data/registry/react/components/markdown-editor/index.tailwind.tsx +118 -0
  24. package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
  25. package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
  26. package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
  27. package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
  28. package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
  29. package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
  30. package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
  31. package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +211 -0
  32. package/data/registry/react/components/select/index.tailwind.tsx +199 -0
  33. package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
  34. package/data/registry/react/components/sidebar/index.tailwind.tsx +635 -0
  35. package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
  36. package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
  37. package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
  38. package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
  39. package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
  40. package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
  41. package/data/registry/react/components/toast/index.tailwind.tsx +215 -0
  42. package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
  43. package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
  44. package/data/registry/react/registry.json +696 -98
  45. package/package.json +1 -1
  46. package/src/mcp.mjs +1 -1
@@ -0,0 +1,309 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ interface HSV { h: number; s: number; v: number; }
6
+ interface HSVA extends HSV { a: number; }
7
+
8
+ export interface ColorPickerProps
9
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue" | "children"> {
10
+ value?: string;
11
+ onChange?: (hex: string) => void;
12
+ defaultValue?: string;
13
+ children?: React.ReactNode;
14
+ }
15
+
16
+ function clamp(n: number, min: number, max: number) { return Math.min(max, Math.max(min, n)); }
17
+
18
+ function hexToRgb(hex: string): [number, number, number] {
19
+ const m = hex.replace("#", "");
20
+ const full = m.length === 3 ? m.split("").map((c) => c + c).join("") : m;
21
+ const n = parseInt(full, 16);
22
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
23
+ }
24
+ function rgbToHex(r: number, g: number, b: number): string {
25
+ const toHex = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0");
26
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
27
+ }
28
+ function rgbToHsv(r: number, g: number, b: number): HSV {
29
+ const rn = r / 255, gn = g / 255, bn = b / 255;
30
+ const max = Math.max(rn, gn, bn); const min = Math.min(rn, gn, bn);
31
+ const d = max - min; let h = 0;
32
+ if (d !== 0) {
33
+ if (max === rn) h = ((gn - bn) / d) % 6;
34
+ else if (max === gn) h = (bn - rn) / d + 2;
35
+ else h = (rn - gn) / d + 4;
36
+ h *= 60; if (h < 0) h += 360;
37
+ }
38
+ const s = max === 0 ? 0 : d / max; const v = max;
39
+ return { h, s, v };
40
+ }
41
+ function hsvToRgb({ h, s, v }: HSV): [number, number, number] {
42
+ const c = v * s; const hh = h / 60;
43
+ const x = c * (1 - Math.abs((hh % 2) - 1));
44
+ let r = 0, g = 0, b = 0;
45
+ if (hh >= 0 && hh < 1) [r, g, b] = [c, x, 0];
46
+ else if (hh < 2) [r, g, b] = [x, c, 0];
47
+ else if (hh < 3) [r, g, b] = [0, c, x];
48
+ else if (hh < 4) [r, g, b] = [0, x, c];
49
+ else if (hh < 5) [r, g, b] = [x, 0, c];
50
+ else [r, g, b] = [c, 0, x];
51
+ const m = v - c;
52
+ return [(r + m) * 255, (g + m) * 255, (b + m) * 255];
53
+ }
54
+ function hexToHsv(hex: string): HSV { const [r, g, b] = hexToRgb(hex); return rgbToHsv(r, g, b); }
55
+ function hsvToHex(hsv: HSV): string { const [r, g, b] = hsvToRgb(hsv); return rgbToHex(r, g, b); }
56
+
57
+ const HEX_RE = /^#?[0-9a-f]{6}$/i;
58
+
59
+ function useDrag(onMove: (e: PointerEvent, el: HTMLElement) => void) {
60
+ const ref = React.useRef<HTMLDivElement>(null);
61
+ const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
62
+ const el = ref.current; if (!el) return;
63
+ el.setPointerCapture(e.pointerId);
64
+ onMove(e.nativeEvent, el);
65
+ const onPointerMove = (ev: PointerEvent) => onMove(ev, el);
66
+ const onPointerUp = (ev: PointerEvent) => {
67
+ el.releasePointerCapture(ev.pointerId);
68
+ el.removeEventListener("pointermove", onPointerMove);
69
+ el.removeEventListener("pointerup", onPointerUp);
70
+ };
71
+ el.addEventListener("pointermove", onPointerMove);
72
+ el.addEventListener("pointerup", onPointerUp);
73
+ };
74
+ return { ref, onPointerDown };
75
+ }
76
+
77
+ interface ColorPickerContextValue {
78
+ hsva: HSVA; hex: string; pureHueHex: string;
79
+ setHsv: (next: Partial<HSV>) => void;
80
+ setAlpha: (a: number) => void;
81
+ commitHex: (raw: string) => boolean;
82
+ }
83
+ const ColorPickerContext = React.createContext<ColorPickerContextValue | null>(null);
84
+ function useColorPicker() {
85
+ const ctx = React.useContext(ColorPickerContext);
86
+ if (!ctx) throw new Error("ColorPicker 하위 컴포넌트는 <ColorPicker> 내부에서만 사용할 수 있습니다.");
87
+ return ctx;
88
+ }
89
+
90
+ export function ColorPicker({
91
+ value: valueProp, onChange, defaultValue = "#000000", className, children, ...rest
92
+ }: ColorPickerProps) {
93
+ const isControlled = valueProp !== undefined;
94
+ const [internal, setInternal] = React.useState(defaultValue);
95
+ const value = isControlled ? valueProp! : internal;
96
+ const [hsva, setHsva] = React.useState<HSVA>(() => ({ ...hexToHsv(value), a: 1 }));
97
+
98
+ const lastEmittedRef = React.useRef(value);
99
+ React.useEffect(() => {
100
+ if (value === lastEmittedRef.current) return;
101
+ setHsva((prev) => ({ ...hexToHsv(value), a: prev.a }));
102
+ }, [value]);
103
+
104
+ const setHsv = React.useCallback((partial: Partial<HSV>) => {
105
+ const next: HSVA = { ...hsva, ...partial };
106
+ const hex = hsvToHex(next);
107
+ lastEmittedRef.current = hex;
108
+ setHsva(next);
109
+ if (!isControlled) setInternal(hex);
110
+ onChange?.(hex);
111
+ }, [hsva, isControlled, onChange]);
112
+
113
+ const setAlpha = React.useCallback((a: number) => {
114
+ setHsva((prev) => ({ ...prev, a: clamp(a, 0, 1) }));
115
+ }, []);
116
+
117
+ const commitHex = React.useCallback((raw: string) => {
118
+ const v = raw.trim();
119
+ if (!HEX_RE.test(v)) return false;
120
+ const normalized = (v.startsWith("#") ? v : `#${v}`).toUpperCase();
121
+ const nextHsv = hexToHsv(normalized);
122
+ const next: HSVA = { ...nextHsv, a: hsva.a };
123
+ const hex = hsvToHex(next);
124
+ lastEmittedRef.current = hex;
125
+ setHsva(next);
126
+ if (!isControlled) setInternal(hex);
127
+ onChange?.(hex);
128
+ return true;
129
+ }, [hsva.a, isControlled, onChange]);
130
+
131
+ const pureHueHex = React.useMemo(() => hsvToHex({ h: hsva.h, s: 1, v: 1 }), [hsva.h]);
132
+ const ctx = React.useMemo<ColorPickerContextValue>(
133
+ () => ({ hsva, hex: value, pureHueHex, setHsv, setAlpha, commitHex }),
134
+ [hsva, value, pureHueHex, setHsv, setAlpha, commitHex],
135
+ );
136
+
137
+ return (
138
+ <ColorPickerContext.Provider value={ctx}>
139
+ <div className={["flex flex-col gap-2.5 w-full select-none", className].filter(Boolean).join(" ")} {...rest}>
140
+ {children ?? (<><ColorPickerSaturation /><ColorPickerHue /><ColorPickerHex /></>)}
141
+ </div>
142
+ </ColorPickerContext.Provider>
143
+ );
144
+ }
145
+
146
+ export interface ColorPickerSaturationProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onPointerDown"> {}
147
+
148
+ export function ColorPickerSaturation({ className, style, ...rest }: ColorPickerSaturationProps) {
149
+ const { hsva, hex, pureHueHex, setHsv } = useColorPicker();
150
+ const drag = useDrag((e, el) => {
151
+ const r = el.getBoundingClientRect();
152
+ const x = clamp((e.clientX - r.left) / r.width, 0, 1);
153
+ const y = clamp((e.clientY - r.top) / r.height, 0, 1);
154
+ setHsv({ s: x, v: 1 - y });
155
+ });
156
+ return (
157
+ <div
158
+ ref={drag.ref}
159
+ onPointerDown={drag.onPointerDown}
160
+ className={["relative w-full aspect-[4/3] rounded-[var(--radius)] cursor-crosshair overflow-hidden touch-none", className].filter(Boolean).join(" ")}
161
+ style={{ background: pureHueHex, ...style }}
162
+ role="slider"
163
+ aria-label="채도/명도"
164
+ aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(hsva.s * 100)}
165
+ {...rest}
166
+ >
167
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,#fff,transparent)]" />
168
+ <div className="absolute inset-0 bg-[linear-gradient(to_top,#000,transparent)]" />
169
+ <div
170
+ className="absolute w-3.5 h-3.5 -ml-[0.4375rem] -mt-[0.4375rem] border-2 border-white rounded-full shadow-[0_0_0_1px_rgba(0,0,0,0.4)] pointer-events-none"
171
+ style={{ left: `${hsva.s * 100}%`, top: `${(1 - hsva.v) * 100}%`, background: hex }}
172
+ />
173
+ </div>
174
+ );
175
+ }
176
+
177
+ export interface ColorPickerHueProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onPointerDown"> {}
178
+
179
+ export function ColorPickerHue({ className, ...rest }: ColorPickerHueProps) {
180
+ const { hsva, setHsv } = useColorPicker();
181
+ const drag = useDrag((e, el) => {
182
+ const r = el.getBoundingClientRect();
183
+ const x = clamp((e.clientX - r.left) / r.width, 0, 1);
184
+ setHsv({ h: x * 360 });
185
+ });
186
+ return (
187
+ <div
188
+ ref={drag.ref}
189
+ onPointerDown={drag.onPointerDown}
190
+ className={["relative w-full h-3.5 rounded-full cursor-pointer touch-none bg-[linear-gradient(to_right,#f00_0%,#ff0_16.66%,#0f0_33.33%,#0ff_50%,#00f_66.66%,#f0f_83.33%,#f00_100%)]", className].filter(Boolean).join(" ")}
191
+ role="slider"
192
+ aria-label="색조"
193
+ aria-valuemin={0} aria-valuemax={360} aria-valuenow={Math.round(hsva.h)}
194
+ {...rest}
195
+ >
196
+ <div
197
+ className="absolute top-1/2 w-3.5 h-3.5 -ml-[0.4375rem] -translate-y-1/2 bg-white rounded-full shadow-[0_0_0_1px_rgba(0,0,0,0.4)] pointer-events-none"
198
+ style={{ left: `${(hsva.h / 360) * 100}%` }}
199
+ />
200
+ </div>
201
+ );
202
+ }
203
+
204
+ export interface ColorPickerAlphaProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onPointerDown"> {}
205
+
206
+ export function ColorPickerAlpha({ className, style, ...rest }: ColorPickerAlphaProps) {
207
+ const { hsva, hex, setAlpha } = useColorPicker();
208
+ const drag = useDrag((e, el) => {
209
+ const r = el.getBoundingClientRect();
210
+ const x = clamp((e.clientX - r.left) / r.width, 0, 1);
211
+ setAlpha(x);
212
+ });
213
+ const gradient = `linear-gradient(to right, rgba(0,0,0,0) 0%, ${hex} 100%)`;
214
+ return (
215
+ <div
216
+ ref={drag.ref}
217
+ onPointerDown={drag.onPointerDown}
218
+ className={["relative w-full h-3.5 rounded-full cursor-pointer touch-none overflow-hidden bg-white bg-[length:8px_8px] bg-[position:0_0,0_4px,4px_-4px,-4px_0] [background-image:linear-gradient(45deg,#ccc_25%,transparent_25%),linear-gradient(-45deg,#ccc_25%,transparent_25%),linear-gradient(45deg,transparent_75%,#ccc_75%),linear-gradient(-45deg,transparent_75%,#ccc_75%)]", className].filter(Boolean).join(" ")}
219
+ role="slider"
220
+ aria-label="투명도"
221
+ aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(hsva.a * 100)}
222
+ style={style}
223
+ {...rest}
224
+ >
225
+ <div className="absolute inset-0 rounded-[inherit] pointer-events-none" style={{ backgroundImage: gradient }} />
226
+ <div
227
+ className="absolute top-1/2 w-3.5 h-3.5 -ml-[0.4375rem] -translate-y-1/2 bg-white rounded-full shadow-[0_0_0_1px_rgba(0,0,0,0.4)] pointer-events-none"
228
+ style={{ left: `${hsva.a * 100}%` }}
229
+ />
230
+ </div>
231
+ );
232
+ }
233
+
234
+ export interface ColorPickerHexProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
235
+ showSwatch?: boolean;
236
+ }
237
+
238
+ export function ColorPickerHex({ className, showSwatch = true, ...rest }: ColorPickerHexProps) {
239
+ const { hex, commitHex } = useColorPicker();
240
+ const [draft, setDraft] = React.useState(hex);
241
+
242
+ const inputRef = React.useRef<HTMLInputElement>(null);
243
+ React.useEffect(() => {
244
+ if (document.activeElement !== inputRef.current) setDraft(hex);
245
+ }, [hex]);
246
+
247
+ const onCommit = () => { if (!commitHex(draft)) setDraft(hex); };
248
+
249
+ return (
250
+ <div
251
+ className={["flex items-center gap-[var(--space-2)]", className].filter(Boolean).join(" ")}
252
+ {...rest}
253
+ >
254
+ {showSwatch && (
255
+ <div
256
+ className="w-7 h-7 rounded-[calc(var(--radius)-2px)] border border-border shrink-0"
257
+ style={{ background: hex }}
258
+ aria-hidden
259
+ />
260
+ )}
261
+ <input
262
+ ref={inputRef}
263
+ type="text"
264
+ className="flex-1 min-w-0 h-7 px-[var(--space-2)] border border-border rounded-[calc(var(--radius)-2px)] bg-background text-foreground font-mono text-[0.8125rem] uppercase focus:outline-none focus:border-foreground focus:shadow-[0_0_0_1px_var(--foreground)]"
265
+ value={draft}
266
+ onChange={(e) => setDraft(e.target.value)}
267
+ onBlur={onCommit}
268
+ onKeyDown={(e) => {
269
+ if (e.key === "Enter") { e.preventDefault(); (e.target as HTMLInputElement).blur(); }
270
+ }}
271
+ spellCheck={false}
272
+ aria-label="Hex"
273
+ />
274
+ </div>
275
+ );
276
+ }
277
+
278
+ export interface ColorPickerSwatchesProps extends React.HTMLAttributes<HTMLDivElement> {
279
+ colors: string[];
280
+ }
281
+
282
+ export function ColorPickerSwatches({ className, colors, ...rest }: ColorPickerSwatchesProps) {
283
+ const { hex, commitHex } = useColorPicker();
284
+ return (
285
+ <div
286
+ role="group"
287
+ aria-label="미리 준비된 색상"
288
+ className={["flex flex-wrap gap-1.5", className].filter(Boolean).join(" ")}
289
+ {...rest}
290
+ >
291
+ {colors.map((c) => {
292
+ const normalized = c.toUpperCase();
293
+ const selected = normalized === hex.toUpperCase();
294
+ return (
295
+ <button
296
+ key={c}
297
+ type="button"
298
+ className="w-5 h-5 p-0 border border-border rounded-[calc(var(--radius)-4px)] cursor-pointer transition-[transform,box-shadow] duration-[var(--duration-fast)] hover:scale-110 focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 data-[selected]:shadow-[0_0_0_2px_var(--background),0_0_0_3.5px_var(--foreground)]"
299
+ aria-label={c}
300
+ aria-pressed={selected}
301
+ data-selected={selected || undefined}
302
+ style={{ background: c }}
303
+ onClick={() => commitHex(c)}
304
+ />
305
+ );
306
+ })}
307
+ </div>
308
+ );
309
+ }
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Combobox as BaseCombobox } from "@base-ui/react/combobox";
5
+
6
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
7
+
8
+ function cx(...args: (string | undefined | false)[]) {
9
+ return args.filter(Boolean).join(" ");
10
+ }
11
+
12
+ export const Combobox = BaseCombobox.Root;
13
+ export const ComboboxIcon = BaseCombobox.Icon;
14
+ export const ComboboxTrigger = BaseCombobox.Trigger;
15
+ export const ComboboxClear = BaseCombobox.Clear;
16
+ export const ComboboxValue = BaseCombobox.Value;
17
+ export const ComboboxGroup = BaseCombobox.Group;
18
+ export const ComboboxChips = BaseCombobox.Chips;
19
+
20
+ export const ComboboxInput = React.forwardRef<
21
+ HTMLInputElement,
22
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseCombobox.Input>>
23
+ >(function ComboboxInput({ className, ...props }, ref) {
24
+ return (
25
+ <BaseCombobox.Input
26
+ ref={ref}
27
+ className={cx(
28
+ "inline-flex w-full min-w-40 h-[var(--control-md)] px-[var(--space-3)] bg-background text-foreground border border-border rounded-[var(--radius)] text-[length:var(--text-sm)] leading-none outline-none transition-[border-color] duration-[var(--duration-fast)] placeholder:text-foreground-subtle hover:not-disabled:border-border-strong focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 disabled:opacity-[var(--opacity-disabled)] disabled:pointer-events-none",
29
+ className,
30
+ )}
31
+ {...props}
32
+ />
33
+ );
34
+ });
35
+
36
+ export const ComboboxContent = React.forwardRef<
37
+ HTMLDivElement,
38
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseCombobox.Popup>> & {
39
+ container?: React.ComponentPropsWithoutRef<typeof BaseCombobox.Portal>["container"];
40
+ sideOffset?: number;
41
+ }
42
+ >(function ComboboxContent({ className, children, container, sideOffset = 4, ...props }, ref) {
43
+ return (
44
+ <BaseCombobox.Portal container={container}>
45
+ <BaseCombobox.Positioner
46
+ className="z-[var(--z-dropdown)] outline-none w-[var(--anchor-width)]"
47
+ sideOffset={sideOffset}
48
+ align="start"
49
+ >
50
+ <BaseCombobox.Popup
51
+ ref={ref}
52
+ className={cx(
53
+ "max-h-[min(20rem,var(--available-height))] overflow-y-auto p-[var(--space-1)] bg-background text-foreground border border-border rounded-[var(--radius)] shadow-[0_8px_24px_rgba(0,0,0,0.08)] outline-none origin-[var(--transform-origin)] transition-[opacity,transform] duration-[140ms] ease-out motion-reduce:transition-none data-[starting-style]:opacity-0 data-[starting-style]:scale-[0.97] data-[ending-style]:opacity-0 data-[ending-style]:scale-[0.97]",
54
+ className,
55
+ )}
56
+ {...props}
57
+ >
58
+ {children}
59
+ </BaseCombobox.Popup>
60
+ </BaseCombobox.Positioner>
61
+ </BaseCombobox.Portal>
62
+ );
63
+ });
64
+
65
+ export const ComboboxList = BaseCombobox.List;
66
+
67
+ export const ComboboxItem = React.forwardRef<
68
+ HTMLDivElement,
69
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseCombobox.Item>>
70
+ >(function ComboboxItem({ className, children, ...props }, ref) {
71
+ return (
72
+ <BaseCombobox.Item
73
+ ref={ref}
74
+ className={cx(
75
+ "flex items-center gap-[var(--space-2)] py-1.5 px-3 text-[length:var(--text-sm)] leading-snug rounded-[calc(var(--radius)-2px)] cursor-pointer select-none outline-none data-[highlighted]:bg-background-muted hover:bg-background-muted data-[selected]:text-foreground data-[selected]:font-medium data-[disabled]:opacity-[var(--opacity-disabled)] data-[disabled]:pointer-events-none",
76
+ className,
77
+ )}
78
+ {...props}
79
+ >
80
+ <BaseCombobox.ItemIndicator className="order-1 ml-auto inline-flex items-center justify-center text-foreground">
81
+ <CheckIcon />
82
+ </BaseCombobox.ItemIndicator>
83
+ <span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{children}</span>
84
+ </BaseCombobox.Item>
85
+ );
86
+ });
87
+
88
+ export const ComboboxEmpty = React.forwardRef<
89
+ HTMLDivElement,
90
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseCombobox.Empty>>
91
+ >(function ComboboxEmpty({ className, ...props }, ref) {
92
+ return (
93
+ <BaseCombobox.Empty
94
+ ref={ref}
95
+ className={cx(
96
+ "py-[var(--space-3)] px-[var(--space-2)] text-center text-[0.8125rem] text-foreground-muted",
97
+ className,
98
+ )}
99
+ {...props}
100
+ />
101
+ );
102
+ });
103
+
104
+ export const ComboboxGroupLabel = React.forwardRef<
105
+ HTMLDivElement,
106
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseCombobox.GroupLabel>>
107
+ >(function ComboboxGroupLabel({ className, ...props }, ref) {
108
+ return (
109
+ <BaseCombobox.GroupLabel
110
+ ref={ref}
111
+ className={cx(
112
+ "py-1.5 px-[var(--space-2)] pb-[var(--space-1)] text-[length:var(--text-xs)] font-semibold text-foreground-muted uppercase tracking-[0.04em]",
113
+ className,
114
+ )}
115
+ {...props}
116
+ />
117
+ );
118
+ });
119
+
120
+ export const ComboboxChip = React.forwardRef<
121
+ HTMLDivElement,
122
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseCombobox.Chip>>
123
+ >(function ComboboxChip({ className, ...props }, ref) {
124
+ return (
125
+ <BaseCombobox.Chip
126
+ ref={ref}
127
+ className={cx(
128
+ "inline-flex items-center gap-[var(--space-1)] py-0.5 pr-1.5 pl-[var(--space-2)] mr-[var(--space-1)] text-[length:var(--text-xs)] leading-5 bg-background-muted rounded-[calc(var(--radius)-2px)] whitespace-nowrap",
129
+ className,
130
+ )}
131
+ {...props}
132
+ />
133
+ );
134
+ });
135
+
136
+ export const ComboboxChipRemove = React.forwardRef<
137
+ HTMLButtonElement,
138
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseCombobox.ChipRemove>>
139
+ >(function ComboboxChipRemove({ className, children, ...props }, ref) {
140
+ return (
141
+ <BaseCombobox.ChipRemove
142
+ ref={ref}
143
+ className={cx(
144
+ "inline-flex items-center justify-center w-4 h-4 p-0 border-0 rounded-full bg-transparent text-foreground-muted text-[length:var(--text-sm)] leading-none cursor-pointer transition-[background-color,color] duration-[var(--duration-fast)] hover:bg-background hover:text-foreground motion-reduce:transition-none",
145
+ className,
146
+ )}
147
+ {...props}
148
+ >
149
+ {children ?? "×"}
150
+ </BaseCombobox.ChipRemove>
151
+ );
152
+ });
153
+
154
+ function CheckIcon() {
155
+ return (
156
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden>
157
+ <path d="M3 8.5l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
158
+ </svg>
159
+ );
160
+ }
@@ -0,0 +1,170 @@
1
+ import * as React from "react";
2
+ import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu";
3
+
4
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
5
+
6
+ function cx(...args: (string | undefined | false | null)[]) {
7
+ return args.filter(Boolean).join(" ");
8
+ }
9
+
10
+ const itemBase =
11
+ "relative flex items-center gap-[var(--space-2)] py-2 px-3 rounded-[calc(var(--radius)-2px)] cursor-pointer outline-none select-none transition-colors duration-[80ms] data-[highlighted]:bg-background-muted hover:bg-background-muted data-[disabled]:opacity-[var(--opacity-disabled)] data-[disabled]:pointer-events-none motion-reduce:transition-none";
12
+ const itemCheck = "pl-7";
13
+ const contentClasses =
14
+ "min-w-40 max-h-[min(24rem,var(--available-height,24rem))] overflow-y-auto p-[var(--space-1)] bg-background text-foreground border border-border rounded-[var(--radius)] shadow-[0_4px_6px_-1px_rgba(0,0,0,0.08),0_2px_4px_-2px_rgba(0,0,0,0.05)] text-[length:var(--text-sm)] origin-[var(--transform-origin)] animate-[sh-ui-cm-in_140ms_ease-out] data-[ending-style]:animate-[sh-ui-cm-out_100ms_ease-in_forwards] outline-none motion-reduce:animate-none motion-reduce:data-[ending-style]:animate-none";
15
+
16
+ export const ContextMenu = BaseContextMenu.Root;
17
+
18
+ export const ContextMenuTrigger = React.forwardRef<
19
+ HTMLDivElement,
20
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseContextMenu.Trigger>>
21
+ >(function ContextMenuTrigger({ className, ...props }, ref) {
22
+ return <BaseContextMenu.Trigger ref={ref} className={cx("contents", className)} {...props} />;
23
+ });
24
+
25
+ export interface ContextMenuContentProps
26
+ extends WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseContextMenu.Popup>> {
27
+ container?: React.ComponentPropsWithoutRef<typeof BaseContextMenu.Portal>["container"];
28
+ }
29
+
30
+ export const ContextMenuContent = React.forwardRef<HTMLDivElement, ContextMenuContentProps>(
31
+ function ContextMenuContent({ className, children, container, ...props }, ref) {
32
+ return (
33
+ <BaseContextMenu.Portal container={container}>
34
+ <BaseContextMenu.Positioner className="outline-none z-[var(--z-dropdown)]">
35
+ <BaseContextMenu.Popup ref={ref} className={cx(contentClasses, className)} {...props}>
36
+ {children}
37
+ </BaseContextMenu.Popup>
38
+ </BaseContextMenu.Positioner>
39
+ </BaseContextMenu.Portal>
40
+ );
41
+ },
42
+ );
43
+
44
+ export const ContextMenuItem = React.forwardRef<
45
+ HTMLDivElement,
46
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseContextMenu.Item>>
47
+ >(function ContextMenuItem({ className, ...props }, ref) {
48
+ return <BaseContextMenu.Item ref={ref} className={cx(itemBase, className)} {...props} />;
49
+ });
50
+
51
+ export const ContextMenuCheckboxItem = React.forwardRef<
52
+ HTMLDivElement,
53
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseContextMenu.CheckboxItem>>
54
+ >(function ContextMenuCheckboxItem({ className, children, ...props }, ref) {
55
+ return (
56
+ <BaseContextMenu.CheckboxItem ref={ref} className={cx(itemBase, itemCheck, className)} {...props}>
57
+ <span className="absolute left-2 inline-flex items-center justify-center w-4 h-4 text-foreground" aria-hidden>
58
+ <BaseContextMenu.CheckboxItemIndicator>
59
+ <CheckIcon />
60
+ </BaseContextMenu.CheckboxItemIndicator>
61
+ </span>
62
+ <span className="flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">{children}</span>
63
+ </BaseContextMenu.CheckboxItem>
64
+ );
65
+ });
66
+
67
+ export const ContextMenuRadioGroup = BaseContextMenu.RadioGroup;
68
+
69
+ export const ContextMenuRadioItem = React.forwardRef<
70
+ HTMLDivElement,
71
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseContextMenu.RadioItem>>
72
+ >(function ContextMenuRadioItem({ className, children, ...props }, ref) {
73
+ return (
74
+ <BaseContextMenu.RadioItem ref={ref} className={cx(itemBase, itemCheck, className)} {...props}>
75
+ <span className="absolute left-2 inline-flex items-center justify-center w-4 h-4 text-foreground" aria-hidden>
76
+ <BaseContextMenu.RadioItemIndicator>
77
+ <DotIcon />
78
+ </BaseContextMenu.RadioItemIndicator>
79
+ </span>
80
+ <span className="flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">{children}</span>
81
+ </BaseContextMenu.RadioItem>
82
+ );
83
+ });
84
+
85
+ export const ContextMenuGroup = React.forwardRef<
86
+ HTMLDivElement,
87
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseContextMenu.Group>>
88
+ >(function ContextMenuGroup({ className, ...props }, ref) {
89
+ return <BaseContextMenu.Group ref={ref} className={cx("p-0", className)} {...props} />;
90
+ });
91
+
92
+ export const ContextMenuLabel = React.forwardRef<
93
+ HTMLDivElement,
94
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseContextMenu.GroupLabel>>
95
+ >(function ContextMenuLabel({ className, ...props }, ref) {
96
+ return (
97
+ <BaseContextMenu.GroupLabel
98
+ ref={ref}
99
+ className={cx(
100
+ "py-[var(--space-2)] px-[var(--space-2)] pb-[var(--space-1)] text-[length:var(--text-xs)] font-semibold text-foreground-muted uppercase tracking-[0.04em]",
101
+ className,
102
+ )}
103
+ {...props}
104
+ />
105
+ );
106
+ });
107
+
108
+ export const ContextMenuSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
109
+ function ContextMenuSeparator({ className, ...props }, ref) {
110
+ return (
111
+ <div
112
+ ref={ref}
113
+ role="separator"
114
+ aria-orientation="horizontal"
115
+ className={cx("h-px bg-border my-[var(--space-1)]", className)}
116
+ {...props}
117
+ />
118
+ );
119
+ },
120
+ );
121
+
122
+ export const ContextMenuSub = BaseContextMenu.SubmenuRoot;
123
+
124
+ export const ContextMenuSubTrigger = React.forwardRef<
125
+ HTMLDivElement,
126
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseContextMenu.SubmenuTrigger>>
127
+ >(function ContextMenuSubTrigger({ className, children, ...props }, ref) {
128
+ return (
129
+ <BaseContextMenu.SubmenuTrigger
130
+ ref={ref}
131
+ className={cx(itemBase, "data-[popup-open]:bg-background-muted", className)}
132
+ {...props}
133
+ >
134
+ <span className="flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">{children}</span>
135
+ <span className="inline-flex items-center justify-center ml-auto text-foreground-muted" aria-hidden>
136
+ <ChevronRightIcon />
137
+ </span>
138
+ </BaseContextMenu.SubmenuTrigger>
139
+ );
140
+ });
141
+
142
+ export const ContextMenuSubContent = ContextMenuContent;
143
+
144
+ function CheckIcon() {
145
+ return (
146
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden>
147
+ <path d="M3.5 8.5l3 3 6-7" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
148
+ </svg>
149
+ );
150
+ }
151
+ function DotIcon() {
152
+ return <svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor" aria-hidden><circle cx="4" cy="4" r="3" /></svg>;
153
+ }
154
+ function ChevronRightIcon() {
155
+ return (
156
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden>
157
+ <path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
158
+ </svg>
159
+ );
160
+ }
161
+
162
+ if (typeof document !== "undefined" && !document.querySelector("style[data-sh-ui-cm]")) {
163
+ const style = document.createElement("style");
164
+ style.setAttribute("data-sh-ui-cm", "");
165
+ style.textContent = `
166
+ @keyframes sh-ui-cm-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
167
+ @keyframes sh-ui-cm-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.96); } }
168
+ `;
169
+ document.head.appendChild(style);
170
+ }