sh-ui-cli 0.43.0 → 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/data/changelog/versions.json +12 -0
- 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/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/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/registry.json +509 -74
- package/package.json +1 -1
- package/src/mcp.mjs +1 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
+
);
|