sh-ui-cli 0.42.1 → 0.44.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 (53) hide show
  1. package/README.md +6 -1
  2. package/data/changelog/versions.json +25 -0
  3. package/data/registry/flutter/registry.json +1 -1
  4. package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
  5. package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
  6. package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
  7. package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
  8. package/data/registry/react/components/button/index.tailwind.tsx +70 -0
  9. package/data/registry/react/components/card/index.tailwind.tsx +111 -0
  10. package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
  11. package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -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/input/index.tailwind.tsx +405 -0
  18. package/data/registry/react/components/label/index.tailwind.tsx +78 -0
  19. package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
  20. package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
  21. package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
  22. package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
  23. package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
  24. package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
  25. package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
  26. package/data/registry/react/components/select/index.tailwind.tsx +199 -0
  27. package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
  28. package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
  29. package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
  30. package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
  31. package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
  32. package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
  33. package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
  34. package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
  35. package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
  36. package/data/registry/react/peer-versions.json +1 -0
  37. package/data/registry/react/registry.json +530 -72
  38. package/data/tokens/build.mjs +66 -0
  39. package/package.json +1 -1
  40. package/src/add.mjs +54 -6
  41. package/src/api.d.ts +14 -0
  42. package/src/api.js +4 -0
  43. package/src/constants.js +19 -0
  44. package/src/create/cli-args.js +18 -2
  45. package/src/create/generator.js +55 -6
  46. package/src/create/index.mjs +3 -1
  47. package/src/init.mjs +25 -7
  48. package/src/mcp.mjs +13 -2
  49. package/templates/flutter-standalone/sh-ui.config.json +1 -1
  50. package/templates/nextjs-standalone/app/globals.css +1 -21
  51. package/templates/nextjs-standalone/sh-ui.config.json +1 -1
  52. package/templates/ui-app-template/sh-ui.config.json +1 -1
  53. package/templates/ui-app-template/src/styles/globals.css +1 -21
@@ -0,0 +1,294 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Popover as BasePopover } from "@base-ui/react/popover";
5
+ import { Calendar, type DateRange } from "../calendar";
6
+
7
+ export type { DateRange };
8
+
9
+ function cx(...args: (string | undefined | false)[]) {
10
+ return args.filter(Boolean).join(" ");
11
+ }
12
+
13
+ const formatDefault = (d: Date) =>
14
+ `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
15
+ const startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);
16
+
17
+ const triggerClasses =
18
+ "inline-flex items-center justify-between w-full h-[var(--control-md)] px-[var(--space-3)] bg-background text-foreground border border-border rounded-[var(--radius)] font-[inherit] text-[length:var(--text-sm)] leading-none cursor-pointer transition-[border-color,box-shadow] duration-[var(--duration-fast)] hover:not-disabled:border-border-strong focus-visible:outline-none focus-visible:border-foreground focus-visible:shadow-[0_0_0_1px_var(--foreground)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed disabled:bg-background-subtle aria-[invalid=true]:border-danger aria-[invalid=true]:focus-visible:shadow-[0_0_0_1px_var(--danger)] [@media(hover:none)_and_(pointer:coarse)]:h-11 [@media(hover:none)_and_(pointer:coarse)]:text-[length:var(--text-base)]";
19
+
20
+ const popupClasses =
21
+ "bg-background border border-border rounded-[var(--radius)] shadow-[0_8px_24px_rgba(0,0,0,0.12)] outline-none p-[var(--space-3)] origin-[var(--transform-origin)] transition-[opacity,transform] duration-[140ms] ease-out motion-reduce:transition-none data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95";
22
+
23
+ function CalendarIcon() {
24
+ return (
25
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
26
+ <rect x="2" y="3" width="12" height="11" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
27
+ <path d="M2 6.5h12M5.5 2v2M10.5 2v2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
28
+ </svg>
29
+ );
30
+ }
31
+
32
+ interface DatePickerContextValue {
33
+ selected: Date | undefined;
34
+ setSelected: (date: Date | undefined) => void;
35
+ open: boolean;
36
+ setOpen: (open: boolean) => void;
37
+ focusedDate: Date;
38
+ setFocusedDate: (date: Date) => void;
39
+ formatDate: (date: Date) => string;
40
+ placeholder: string;
41
+ min?: Date; max?: Date; disabled?: boolean; readOnly?: boolean;
42
+ ariaInvalid?: boolean | "true";
43
+ closeOnSelect: boolean;
44
+ }
45
+
46
+ const DatePickerContext = React.createContext<DatePickerContextValue | null>(null);
47
+
48
+ function useDatePickerContext(component: string) {
49
+ const ctx = React.useContext(DatePickerContext);
50
+ if (!ctx) throw new Error(`${component}는 <DatePicker> 내부에서 사용해야 합니다.`);
51
+ return ctx;
52
+ }
53
+
54
+ export interface DatePickerProps {
55
+ value?: Date; defaultValue?: Date;
56
+ onValueChange?: (date: Date | undefined) => void;
57
+ formatDate?: (date: Date) => string;
58
+ min?: Date; max?: Date;
59
+ placeholder?: string; disabled?: boolean; readOnly?: boolean;
60
+ "aria-invalid"?: boolean | "true";
61
+ className?: string; closeOnSelect?: boolean;
62
+ container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
63
+ children?: React.ReactNode;
64
+ }
65
+
66
+ export function DatePicker({
67
+ value, defaultValue, onValueChange, formatDate = formatDefault,
68
+ min, max, placeholder = "날짜 선택", disabled, readOnly,
69
+ "aria-invalid": ariaInvalid, className, closeOnSelect = true, container, children,
70
+ }: DatePickerProps) {
71
+ const isControlled = value !== undefined;
72
+ const [internal, setInternal] = React.useState<Date | undefined>(defaultValue);
73
+ const selected = isControlled ? value : internal;
74
+ const [open, setOpen] = React.useState(false);
75
+ const [focusedDate, setFocusedDate] = React.useState<Date>(() => selected ?? new Date());
76
+
77
+ React.useEffect(() => {
78
+ if (open && selected) setFocusedDate(startOfMonth(selected));
79
+ }, [open, selected]);
80
+
81
+ const setSelected = React.useCallback((date: Date | undefined) => {
82
+ if (!isControlled) setInternal(date);
83
+ onValueChange?.(date);
84
+ }, [isControlled, onValueChange]);
85
+
86
+ const ctx = React.useMemo<DatePickerContextValue>(
87
+ () => ({
88
+ selected, setSelected, open, setOpen, focusedDate, setFocusedDate,
89
+ formatDate, placeholder, min, max, disabled, readOnly, ariaInvalid, closeOnSelect,
90
+ }),
91
+ [selected, setSelected, open, focusedDate, formatDate, placeholder, min, max, disabled, readOnly, ariaInvalid, closeOnSelect],
92
+ );
93
+
94
+ return (
95
+ <DatePickerContext.Provider value={ctx}>
96
+ <BasePopover.Root open={open} onOpenChange={setOpen}>
97
+ {children ?? (
98
+ <>
99
+ <DatePickerTrigger className={className} />
100
+ <DatePickerContent container={container}><DatePickerCalendar /></DatePickerContent>
101
+ </>
102
+ )}
103
+ </BasePopover.Root>
104
+ </DatePickerContext.Provider>
105
+ );
106
+ }
107
+
108
+ export interface DatePickerTriggerProps
109
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
110
+ children?: React.ReactNode | ((state: {
111
+ value: Date | undefined; formatted: string | undefined; placeholder: string;
112
+ }) => React.ReactNode);
113
+ }
114
+
115
+ export const DatePickerTrigger = React.forwardRef<HTMLButtonElement, DatePickerTriggerProps>(
116
+ function DatePickerTrigger({ className, children, onClick, ...props }, ref) {
117
+ const ctx = useDatePickerContext("DatePickerTrigger");
118
+ const displayText = ctx.selected ? ctx.formatDate(ctx.selected) : undefined;
119
+ const renderContent = () => {
120
+ if (typeof children === "function") return children({ value: ctx.selected, formatted: displayText, placeholder: ctx.placeholder });
121
+ if (children !== undefined) return children;
122
+ return (
123
+ <>
124
+ <span className={cx("overflow-hidden text-ellipsis whitespace-nowrap", !displayText && "text-foreground-subtle")}>
125
+ {displayText ?? ctx.placeholder}
126
+ </span>
127
+ <span className="shrink-0 inline-flex text-foreground-muted ml-[var(--space-2)]" aria-hidden>
128
+ <CalendarIcon />
129
+ </span>
130
+ </>
131
+ );
132
+ };
133
+ return (
134
+ <BasePopover.Trigger
135
+ ref={ref}
136
+ className={cx(triggerClasses, className)}
137
+ disabled={ctx.disabled}
138
+ aria-invalid={ctx.ariaInvalid}
139
+ aria-haspopup="dialog"
140
+ onClick={(e) => { if (ctx.readOnly) e.preventDefault(); onClick?.(e); }}
141
+ {...props}
142
+ >
143
+ {renderContent()}
144
+ </BasePopover.Trigger>
145
+ );
146
+ },
147
+ );
148
+
149
+ export interface DatePickerContentProps
150
+ extends Omit<React.ComponentPropsWithoutRef<typeof BasePopover.Popup>, "className"> {
151
+ className?: string;
152
+ sideOffset?: React.ComponentPropsWithoutRef<typeof BasePopover.Positioner>["sideOffset"];
153
+ side?: React.ComponentPropsWithoutRef<typeof BasePopover.Positioner>["side"];
154
+ align?: React.ComponentPropsWithoutRef<typeof BasePopover.Positioner>["align"];
155
+ container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
156
+ }
157
+
158
+ export const DatePickerContent = React.forwardRef<HTMLDivElement, DatePickerContentProps>(
159
+ function DatePickerContent(
160
+ { className, children, sideOffset = 4, side = "bottom", align = "start", container, ...props },
161
+ ref,
162
+ ) {
163
+ const ctx = useDatePickerContext("DatePickerContent");
164
+ if (ctx.disabled || ctx.readOnly) return null;
165
+ return (
166
+ <BasePopover.Portal container={container}>
167
+ <BasePopover.Positioner className="z-[var(--z-popover)] outline-none" sideOffset={sideOffset} side={side} align={align}>
168
+ <BasePopover.Popup ref={ref} className={cx(popupClasses, className)} {...props}>
169
+ {children}
170
+ </BasePopover.Popup>
171
+ </BasePopover.Positioner>
172
+ </BasePopover.Portal>
173
+ );
174
+ },
175
+ );
176
+
177
+ export function DatePickerCalendar() {
178
+ const ctx = useDatePickerContext("DatePickerCalendar");
179
+ const handleSelect = (date: Date | undefined) => {
180
+ ctx.setSelected(date);
181
+ if (date && ctx.closeOnSelect) ctx.setOpen(false);
182
+ };
183
+ return (
184
+ <Calendar
185
+ mode="single"
186
+ value={ctx.selected}
187
+ onValueChange={handleSelect}
188
+ month={ctx.focusedDate}
189
+ onMonthChange={ctx.setFocusedDate}
190
+ min={ctx.min}
191
+ max={ctx.max}
192
+ />
193
+ );
194
+ }
195
+
196
+ export interface DatePickerFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
197
+ export const DatePickerFooter = React.forwardRef<HTMLDivElement, DatePickerFooterProps>(
198
+ function DatePickerFooter({ className, ...props }, ref) {
199
+ return (
200
+ <div
201
+ ref={ref}
202
+ className={cx(
203
+ "flex items-center justify-end gap-[var(--space-2)] mt-[var(--space-2)] pt-[var(--space-2)] border-t border-border",
204
+ className,
205
+ )}
206
+ {...props}
207
+ />
208
+ );
209
+ },
210
+ );
211
+
212
+ export function useDatePicker() {
213
+ const ctx = useDatePickerContext("useDatePicker");
214
+ return {
215
+ value: ctx.selected, setValue: ctx.setSelected,
216
+ open: ctx.open, setOpen: ctx.setOpen,
217
+ focusedDate: ctx.focusedDate, setFocusedDate: ctx.setFocusedDate,
218
+ };
219
+ }
220
+
221
+ export interface DateRangePickerProps {
222
+ value?: DateRange; defaultValue?: DateRange;
223
+ onValueChange?: (range: DateRange | undefined) => void;
224
+ formatDate?: (date: Date) => string;
225
+ min?: Date; max?: Date;
226
+ placeholder?: string; disabled?: boolean; readOnly?: boolean;
227
+ "aria-invalid"?: boolean | "true";
228
+ className?: string;
229
+ container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
230
+ }
231
+
232
+ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(
233
+ function DateRangePicker(
234
+ { value, defaultValue, onValueChange, formatDate = formatDefault, min, max,
235
+ placeholder = "시작일 ~ 종료일", disabled, readOnly, "aria-invalid": ariaInvalid, className, container },
236
+ ref,
237
+ ) {
238
+ const isControlled = value !== undefined;
239
+ const [internal, setInternal] = React.useState<DateRange | undefined>(defaultValue);
240
+ const selected = isControlled ? value : internal;
241
+ const [open, setOpen] = React.useState(false);
242
+ const [calendarMonth, setCalendarMonth] = React.useState<Date>(() => selected?.from ?? new Date());
243
+
244
+ React.useEffect(() => {
245
+ if (open && selected?.from) setCalendarMonth(startOfMonth(selected.from));
246
+ }, [open, selected?.from]);
247
+
248
+ const handleRangeChange = (range: DateRange | undefined) => {
249
+ if (!isControlled) setInternal(range);
250
+ onValueChange?.(range);
251
+ if (range) setOpen(false);
252
+ };
253
+
254
+ const displayText = selected ? `${formatDate(selected.from)} ~ ${formatDate(selected.to)}` : undefined;
255
+
256
+ return (
257
+ <BasePopover.Root open={open} onOpenChange={setOpen}>
258
+ <BasePopover.Trigger
259
+ ref={ref}
260
+ className={cx(triggerClasses, className)}
261
+ disabled={disabled}
262
+ aria-invalid={ariaInvalid}
263
+ aria-haspopup="dialog"
264
+ onClick={(e) => { if (readOnly) e.preventDefault(); }}
265
+ >
266
+ <span className={cx("overflow-hidden text-ellipsis whitespace-nowrap", !displayText && "text-foreground-subtle")}>
267
+ {displayText ?? placeholder}
268
+ </span>
269
+ <span className="shrink-0 inline-flex text-foreground-muted ml-[var(--space-2)]" aria-hidden>
270
+ <CalendarIcon />
271
+ </span>
272
+ </BasePopover.Trigger>
273
+
274
+ {!disabled && !readOnly && (
275
+ <BasePopover.Portal container={container}>
276
+ <BasePopover.Positioner className="z-[var(--z-popover)] outline-none" sideOffset={4} side="bottom" align="start">
277
+ <BasePopover.Popup className={popupClasses}>
278
+ <Calendar
279
+ mode="range"
280
+ value={selected}
281
+ onValueChange={handleRangeChange}
282
+ month={calendarMonth}
283
+ onMonthChange={setCalendarMonth}
284
+ min={min}
285
+ max={max}
286
+ />
287
+ </BasePopover.Popup>
288
+ </BasePopover.Positioner>
289
+ </BasePopover.Portal>
290
+ )}
291
+ </BasePopover.Root>
292
+ );
293
+ },
294
+ );
@@ -0,0 +1,96 @@
1
+ import * as React from "react";
2
+ import { Dialog as BaseDialog } from "@base-ui/react/dialog";
3
+
4
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
5
+
6
+ function cx(...args: (string | undefined | false)[]) {
7
+ return args.filter(Boolean).join(" ");
8
+ }
9
+
10
+ export const Dialog = BaseDialog.Root;
11
+ export const DialogTrigger = BaseDialog.Trigger;
12
+ export const DialogClose = BaseDialog.Close;
13
+
14
+ export function DialogCloseX({ className, children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
15
+ return (
16
+ <BaseDialog.Close
17
+ className={cx(
18
+ "absolute top-[var(--space-3)] right-[var(--space-3)] inline-flex items-center justify-center w-8 h-8 border-0 rounded-[calc(var(--radius)-2px)] bg-transparent text-foreground-muted text-[length:var(--text-lg)] leading-none cursor-pointer transition-[background-color,color] duration-[var(--duration-fast)] hover:bg-background-muted hover:text-foreground focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 motion-reduce:transition-none",
19
+ className,
20
+ )}
21
+ aria-label="닫기"
22
+ {...props}
23
+ >
24
+ {children ?? "×"}
25
+ </BaseDialog.Close>
26
+ );
27
+ }
28
+
29
+ export function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
30
+ return (
31
+ <div
32
+ className={cx(
33
+ "flex items-center justify-end gap-[var(--space-2)] pt-[var(--space-4)] border-t border-border mt-auto",
34
+ className,
35
+ )}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ export interface DialogContentProps
42
+ extends WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseDialog.Popup>> {
43
+ container?: React.ComponentPropsWithoutRef<typeof BaseDialog.Portal>["container"];
44
+ }
45
+
46
+ export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
47
+ function DialogContent({ className, children, container, ...props }, ref) {
48
+ return (
49
+ <BaseDialog.Portal container={container}>
50
+ <BaseDialog.Backdrop className="fixed inset-0 z-[var(--z-overlay)] bg-black/25 backdrop-blur-md transition-opacity duration-[var(--duration-slow)] motion-reduce:transition-none data-[starting-style]:opacity-0 data-[ending-style]:opacity-0" />
51
+ <BaseDialog.Popup
52
+ ref={ref}
53
+ className={cx(
54
+ "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[var(--z-modal)] flex flex-col w-[calc(100%-2rem)] max-w-md max-h-[calc(100dvh-4rem)] p-[var(--space-6)] bg-background text-foreground border border-border rounded-[var(--radius)] shadow-[var(--shadow-xl)] outline-none overflow-y-auto transition-[opacity,transform] duration-[var(--duration-slow)] motion-reduce:transition-none data-[starting-style]:opacity-0 data-[starting-style]:translate-y-[calc(-50%+0.5rem)] data-[starting-style]:scale-[0.97] data-[ending-style]:opacity-0 data-[ending-style]:translate-y-[calc(-50%+0.25rem)] data-[ending-style]:scale-[0.98] motion-reduce:data-[starting-style]:translate-y-[-50%] motion-reduce:data-[starting-style]:scale-100 motion-reduce:data-[ending-style]:translate-y-[-50%] motion-reduce:data-[ending-style]:scale-100 focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2",
55
+ className,
56
+ )}
57
+ {...props}
58
+ >
59
+ {children}
60
+ </BaseDialog.Popup>
61
+ </BaseDialog.Portal>
62
+ );
63
+ },
64
+ );
65
+
66
+ export const DialogTitle = React.forwardRef<
67
+ HTMLHeadingElement,
68
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseDialog.Title>>
69
+ >(function DialogTitle({ className, ...props }, ref) {
70
+ return (
71
+ <BaseDialog.Title
72
+ ref={ref}
73
+ className={cx(
74
+ "m-0 mb-[var(--space-1)] font-semibold text-[length:var(--text-lg)] leading-snug",
75
+ className,
76
+ )}
77
+ {...props}
78
+ />
79
+ );
80
+ });
81
+
82
+ export const DialogDescription = React.forwardRef<
83
+ HTMLParagraphElement,
84
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseDialog.Description>>
85
+ >(function DialogDescription({ className, ...props }, ref) {
86
+ return (
87
+ <BaseDialog.Description
88
+ ref={ref}
89
+ className={cx(
90
+ "m-0 mb-[var(--space-5)] text-foreground-muted text-[length:var(--text-sm)] leading-normal",
91
+ className,
92
+ )}
93
+ {...props}
94
+ />
95
+ );
96
+ });
@@ -0,0 +1,205 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Menu as BaseMenu } from "@base-ui/react/menu";
5
+
6
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
7
+
8
+ function cx(...args: (string | undefined | false | null)[]) {
9
+ return args.filter(Boolean).join(" ");
10
+ }
11
+
12
+ const itemBase =
13
+ "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";
14
+ const itemCheck = "pl-7";
15
+
16
+ export const DropdownMenu = BaseMenu.Root;
17
+
18
+ export const DropdownMenuTrigger = React.forwardRef<
19
+ HTMLButtonElement,
20
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Trigger>>
21
+ >(function DropdownMenuTrigger({ className, ...props }, ref) {
22
+ return (
23
+ <BaseMenu.Trigger
24
+ ref={ref}
25
+ className={cx(
26
+ "font-[inherit] cursor-pointer focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2",
27
+ className,
28
+ )}
29
+ {...props}
30
+ />
31
+ );
32
+ });
33
+
34
+ export interface DropdownMenuContentProps
35
+ extends WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Popup>> {
36
+ side?: "top" | "right" | "bottom" | "left";
37
+ align?: "start" | "center" | "end";
38
+ sideOffset?: number;
39
+ container?: React.ComponentPropsWithoutRef<typeof BaseMenu.Portal>["container"];
40
+ }
41
+
42
+ export const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContentProps>(
43
+ function DropdownMenuContent(
44
+ { className, children, side, align, sideOffset = 6, container, ...props },
45
+ ref,
46
+ ) {
47
+ return (
48
+ <BaseMenu.Portal container={container}>
49
+ <BaseMenu.Positioner
50
+ className="outline-none z-[var(--z-dropdown)]"
51
+ side={side}
52
+ align={align}
53
+ sideOffset={sideOffset}
54
+ >
55
+ <BaseMenu.Popup
56
+ ref={ref}
57
+ className={cx(
58
+ "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-dm-in_140ms_ease-out] data-[ending-style]:animate-[sh-ui-dm-out_100ms_ease-in_forwards] outline-none motion-reduce:animate-none motion-reduce:data-[ending-style]:animate-none",
59
+ className,
60
+ )}
61
+ {...props}
62
+ >
63
+ {children}
64
+ </BaseMenu.Popup>
65
+ </BaseMenu.Positioner>
66
+ </BaseMenu.Portal>
67
+ );
68
+ },
69
+ );
70
+
71
+ export const DropdownMenuItem = React.forwardRef<
72
+ HTMLDivElement,
73
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Item>>
74
+ >(function DropdownMenuItem({ className, ...props }, ref) {
75
+ return <BaseMenu.Item ref={ref} className={cx(itemBase, className)} {...props} />;
76
+ });
77
+
78
+ export interface DropdownMenuCheckboxItemProps
79
+ extends WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.CheckboxItem>> {}
80
+
81
+ export const DropdownMenuCheckboxItem = React.forwardRef<
82
+ HTMLDivElement,
83
+ DropdownMenuCheckboxItemProps
84
+ >(function DropdownMenuCheckboxItem({ className, children, ...props }, ref) {
85
+ return (
86
+ <BaseMenu.CheckboxItem ref={ref} className={cx(itemBase, itemCheck, className)} {...props}>
87
+ <span className="absolute left-2 inline-flex items-center justify-center w-4 h-4 text-foreground" aria-hidden>
88
+ <BaseMenu.CheckboxItemIndicator>
89
+ <CheckIcon />
90
+ </BaseMenu.CheckboxItemIndicator>
91
+ </span>
92
+ <span className="flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">{children}</span>
93
+ </BaseMenu.CheckboxItem>
94
+ );
95
+ });
96
+
97
+ export const DropdownMenuRadioGroup = BaseMenu.RadioGroup;
98
+
99
+ export interface DropdownMenuRadioItemProps
100
+ extends WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.RadioItem>> {}
101
+
102
+ export const DropdownMenuRadioItem = React.forwardRef<
103
+ HTMLDivElement,
104
+ DropdownMenuRadioItemProps
105
+ >(function DropdownMenuRadioItem({ className, children, ...props }, ref) {
106
+ return (
107
+ <BaseMenu.RadioItem ref={ref} className={cx(itemBase, itemCheck, className)} {...props}>
108
+ <span className="absolute left-2 inline-flex items-center justify-center w-4 h-4 text-foreground" aria-hidden>
109
+ <BaseMenu.RadioItemIndicator>
110
+ <DotIcon />
111
+ </BaseMenu.RadioItemIndicator>
112
+ </span>
113
+ <span className="flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">{children}</span>
114
+ </BaseMenu.RadioItem>
115
+ );
116
+ });
117
+
118
+ export const DropdownMenuGroup = React.forwardRef<
119
+ HTMLDivElement,
120
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Group>>
121
+ >(function DropdownMenuGroup({ className, ...props }, ref) {
122
+ return <BaseMenu.Group ref={ref} className={cx("p-0", className)} {...props} />;
123
+ });
124
+
125
+ export const DropdownMenuLabel = React.forwardRef<
126
+ HTMLDivElement,
127
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.GroupLabel>>
128
+ >(function DropdownMenuLabel({ className, ...props }, ref) {
129
+ return (
130
+ <BaseMenu.GroupLabel
131
+ ref={ref}
132
+ className={cx(
133
+ "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]",
134
+ className,
135
+ )}
136
+ {...props}
137
+ />
138
+ );
139
+ });
140
+
141
+ export const DropdownMenuSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
142
+ function DropdownMenuSeparator({ className, ...props }, ref) {
143
+ return (
144
+ <div
145
+ ref={ref}
146
+ role="separator"
147
+ aria-orientation="horizontal"
148
+ className={cx("h-px bg-border my-[var(--space-1)]", className)}
149
+ {...props}
150
+ />
151
+ );
152
+ },
153
+ );
154
+
155
+ export const DropdownMenuSub = BaseMenu.SubmenuRoot;
156
+
157
+ export const DropdownMenuSubTrigger = React.forwardRef<
158
+ HTMLDivElement,
159
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.SubmenuTrigger>>
160
+ >(function DropdownMenuSubTrigger({ className, children, ...props }, ref) {
161
+ return (
162
+ <BaseMenu.SubmenuTrigger
163
+ ref={ref}
164
+ className={cx(itemBase, "data-[popup-open]:bg-background-muted", className)}
165
+ {...props}
166
+ >
167
+ <span className="flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">{children}</span>
168
+ <span className="inline-flex items-center justify-center ml-auto text-foreground-muted" aria-hidden>
169
+ <ChevronRightIcon />
170
+ </span>
171
+ </BaseMenu.SubmenuTrigger>
172
+ );
173
+ });
174
+
175
+ export const DropdownMenuSubContent = DropdownMenuContent;
176
+
177
+ function CheckIcon() {
178
+ return (
179
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden>
180
+ <path d="M3.5 8.5l3 3 6-7" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
181
+ </svg>
182
+ );
183
+ }
184
+
185
+ function DotIcon() {
186
+ return <svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor" aria-hidden><circle cx="4" cy="4" r="3" /></svg>;
187
+ }
188
+
189
+ function ChevronRightIcon() {
190
+ return (
191
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden>
192
+ <path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
193
+ </svg>
194
+ );
195
+ }
196
+
197
+ if (typeof document !== "undefined" && !document.querySelector("style[data-sh-ui-dm]")) {
198
+ const style = document.createElement("style");
199
+ style.setAttribute("data-sh-ui-dm", "");
200
+ style.textContent = `
201
+ @keyframes sh-ui-dm-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
202
+ @keyframes sh-ui-dm-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.96); } }
203
+ `;
204
+ document.head.appendChild(style);
205
+ }