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.
- package/README.md +6 -1
- package/data/changelog/versions.json +25 -0
- package/data/registry/flutter/registry.json +1 -1
- package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
- package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
- package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
- package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
- package/data/registry/react/components/button/index.tailwind.tsx +70 -0
- package/data/registry/react/components/card/index.tailwind.tsx +111 -0
- package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
- package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
- package/data/registry/react/components/combobox/index.tailwind.tsx +160 -0
- package/data/registry/react/components/context-menu/index.tailwind.tsx +170 -0
- package/data/registry/react/components/date-picker/index.tailwind.tsx +294 -0
- package/data/registry/react/components/dialog/index.tailwind.tsx +96 -0
- package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +205 -0
- package/data/registry/react/components/input/index.tailwind.tsx +405 -0
- package/data/registry/react/components/label/index.tailwind.tsx +78 -0
- package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
- package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
- package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
- package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
- package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
- package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
- package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
- package/data/registry/react/components/select/index.tailwind.tsx +199 -0
- package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
- package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
- package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
- package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
- package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
- package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
- package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
- package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
- package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
- package/data/registry/react/peer-versions.json +1 -0
- package/data/registry/react/registry.json +530 -72
- package/data/tokens/build.mjs +66 -0
- package/package.json +1 -1
- package/src/add.mjs +54 -6
- package/src/api.d.ts +14 -0
- package/src/api.js +4 -0
- package/src/constants.js +19 -0
- package/src/create/cli-args.js +18 -2
- package/src/create/generator.js +55 -6
- package/src/create/index.mjs +3 -1
- package/src/init.mjs +25 -7
- package/src/mcp.mjs +13 -2
- package/templates/flutter-standalone/sh-ui.config.json +1 -1
- package/templates/nextjs-standalone/app/globals.css +1 -21
- package/templates/nextjs-standalone/sh-ui.config.json +1 -1
- package/templates/ui-app-template/sh-ui.config.json +1 -1
- 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
|
+
}
|