sh-ui-cli 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/changelog/versions.json +24 -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/calendar/index.tailwind.tsx +498 -0
- package/data/registry/react/components/carousel/index.tailwind.tsx +309 -0
- package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
- package/data/registry/react/components/code-editor/index.tailwind.tsx +168 -0
- package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
- package/data/registry/react/components/color-picker/index.tailwind.tsx +309 -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/file-upload/index.tailwind.tsx +290 -0
- package/data/registry/react/components/form/field.tailwind.tsx +165 -0
- package/data/registry/react/components/form/form.tailwind.tsx +129 -0
- package/data/registry/react/components/form/index.tailwind.tsx +49 -0
- package/data/registry/react/components/header/index.tailwind.tsx +550 -0
- package/data/registry/react/components/label/index.tailwind.tsx +78 -0
- package/data/registry/react/components/markdown-editor/index.tailwind.tsx +118 -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/rich-text-editor/index.tailwind.tsx +211 -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/sidebar/index.tailwind.tsx +635 -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/toast/index.tailwind.tsx +215 -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 +696 -98
- package/package.json +1 -1
- package/src/mcp.mjs +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
function cx(...args: (string | undefined | false | null)[]) {
|
|
4
|
+
return args.filter(Boolean).join(" ");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function clamp(n: number, min: number, max: number) {
|
|
8
|
+
return Math.min(max, Math.max(min, n));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ProgressProps
|
|
12
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "role"> {
|
|
13
|
+
value?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
"aria-label"?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
|
|
19
|
+
function Progress(
|
|
20
|
+
{ value, max = 100, className, "aria-label": ariaLabel, ...props },
|
|
21
|
+
ref,
|
|
22
|
+
) {
|
|
23
|
+
const isDeterminate = value !== undefined;
|
|
24
|
+
const normalized = isDeterminate ? clamp((value / max) * 100, 0, 100) : 0;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
ref={ref}
|
|
29
|
+
role="progressbar"
|
|
30
|
+
aria-label={ariaLabel}
|
|
31
|
+
aria-valuemin={isDeterminate ? 0 : undefined}
|
|
32
|
+
aria-valuemax={isDeterminate ? max : undefined}
|
|
33
|
+
aria-valuenow={isDeterminate ? value : undefined}
|
|
34
|
+
data-state={isDeterminate ? "determinate" : "indeterminate"}
|
|
35
|
+
className={cx(
|
|
36
|
+
"relative w-full h-2 overflow-hidden bg-background-muted rounded-full",
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
className={cx(
|
|
43
|
+
"h-full bg-primary rounded-full transition-[width] duration-[var(--duration-base)] ease-out motion-reduce:transition-none",
|
|
44
|
+
!isDeterminate &&
|
|
45
|
+
"w-2/5 animate-[sh-ui-progress-slide_1.2s_ease-in-out_infinite] motion-reduce:animate-none motion-reduce:translate-x-3/4",
|
|
46
|
+
)}
|
|
47
|
+
style={isDeterminate ? { width: `${normalized}%` } : undefined}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
Progress.displayName = "Progress";
|
|
54
|
+
|
|
55
|
+
if (typeof document !== "undefined" && !document.querySelector("style[data-sh-ui-progress]")) {
|
|
56
|
+
const style = document.createElement("style");
|
|
57
|
+
style.setAttribute("data-sh-ui-progress", "");
|
|
58
|
+
style.textContent = `@keyframes sh-ui-progress-slide { 0% { transform: translateX(-100%) } 100% { transform: translateX(250%) } }`;
|
|
59
|
+
document.head.appendChild(style);
|
|
60
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Radio as BaseRadio } from "@base-ui/react/radio";
|
|
3
|
+
import { RadioGroup as BaseRadioGroup } from "@base-ui/react/radio-group";
|
|
4
|
+
|
|
5
|
+
function cx(...args: (string | undefined | false)[]) {
|
|
6
|
+
return args.filter(Boolean).join(" ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type RadioProps = Omit<
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof BaseRadio.Root>,
|
|
11
|
+
"className"
|
|
12
|
+
> & {
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const Radio = React.forwardRef<HTMLElement, RadioProps>(
|
|
17
|
+
({ className, ...props }, ref) => (
|
|
18
|
+
<BaseRadio.Root
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cx(
|
|
21
|
+
"inline-flex items-center justify-center w-[1.125rem] h-[1.125rem] border border-border-strong rounded-full bg-background cursor-pointer shrink-0 transition-[border-color] duration-[var(--duration-fast)] hover:not-data-[disabled]:border-foreground focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 data-[checked]:border-primary data-[disabled]:opacity-[var(--opacity-disabled)] data-[disabled]:cursor-not-allowed motion-reduce:transition-none [@media(hover:none)_and_(pointer:coarse)]:w-5 [@media(hover:none)_and_(pointer:coarse)]:h-5",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<BaseRadio.Indicator className="w-2 h-2 rounded-full bg-primary scale-0 transition-transform duration-[var(--duration-fast)] ease-out data-[checked]:scale-100 motion-reduce:transition-none" />
|
|
27
|
+
</BaseRadio.Root>
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
Radio.displayName = "Radio";
|
|
31
|
+
|
|
32
|
+
export type RadioGroupProps = Omit<
|
|
33
|
+
React.ComponentPropsWithoutRef<typeof BaseRadioGroup>,
|
|
34
|
+
"className"
|
|
35
|
+
> & {
|
|
36
|
+
className?: string;
|
|
37
|
+
orientation?: "horizontal" | "vertical";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
|
|
41
|
+
({ className, orientation = "vertical", ...props }, ref) => (
|
|
42
|
+
<BaseRadioGroup
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cx(
|
|
45
|
+
"flex gap-2.5",
|
|
46
|
+
orientation === "vertical" ? "flex-col" : "flex-row flex-wrap",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
data-orientation={orientation}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
RadioGroup.displayName = "RadioGroup";
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect } from "react";
|
|
4
|
+
import { useEditor, EditorContent, type Editor } from "@tiptap/react";
|
|
5
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
6
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
7
|
+
import Link from "@tiptap/extension-link";
|
|
8
|
+
import {
|
|
9
|
+
BoldIcon, ItalicIcon, StrikethroughIcon,
|
|
10
|
+
Heading1Icon, Heading2Icon, Heading3Icon,
|
|
11
|
+
ListIcon, ListOrderedIcon, QuoteIcon,
|
|
12
|
+
CodeIcon, Code2Icon, LinkIcon, MinusIcon,
|
|
13
|
+
Undo2Icon, Redo2Icon,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
|
|
16
|
+
export interface RichTextEditorProps {
|
|
17
|
+
value?: string;
|
|
18
|
+
defaultValue?: string;
|
|
19
|
+
onChange?: (html: string) => void;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
readOnly?: boolean;
|
|
22
|
+
hideToolbar?: boolean;
|
|
23
|
+
minHeight?: string;
|
|
24
|
+
maxHeight?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
"aria-label"?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cx(...args: (string | undefined | false | null)[]) {
|
|
30
|
+
return args.filter(Boolean).join(" ");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function RichTextEditor({
|
|
34
|
+
value: valueProp, defaultValue, onChange, placeholder, readOnly = false, hideToolbar = false,
|
|
35
|
+
minHeight, maxHeight, className,
|
|
36
|
+
"aria-label": ariaLabel = "Rich text editor",
|
|
37
|
+
}: RichTextEditorProps) {
|
|
38
|
+
const isControlled = valueProp !== undefined;
|
|
39
|
+
const editor = useEditor({
|
|
40
|
+
extensions: [
|
|
41
|
+
StarterKit,
|
|
42
|
+
Placeholder.configure({
|
|
43
|
+
placeholder: placeholder ?? "",
|
|
44
|
+
emptyEditorClass: "sh-ui-rte__is-empty",
|
|
45
|
+
}),
|
|
46
|
+
Link.configure({
|
|
47
|
+
openOnClick: false, autolink: true,
|
|
48
|
+
HTMLAttributes: { rel: "noopener noreferrer", target: "_blank" },
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
content: valueProp ?? defaultValue ?? "",
|
|
52
|
+
editable: !readOnly,
|
|
53
|
+
immediatelyRender: false,
|
|
54
|
+
onUpdate: ({ editor }) => { onChange?.(editor.getHTML()); },
|
|
55
|
+
editorProps: {
|
|
56
|
+
attributes: { class: "sh-ui-rte__content", "aria-label": ariaLabel },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!isControlled) return;
|
|
62
|
+
if (!editor) return;
|
|
63
|
+
if (editor.getHTML() === valueProp) return;
|
|
64
|
+
editor.commands.setContent(valueProp ?? "", { emitUpdate: false });
|
|
65
|
+
}, [isControlled, valueProp, editor]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => { editor?.setEditable(!readOnly); }, [readOnly, editor]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
className={cx(
|
|
72
|
+
"sh-ui-rte flex flex-col border border-border rounded-[var(--radius)] bg-background overflow-hidden transition-[border-color] duration-[var(--duration-fast)] focus-within:border-foreground focus-within:outline-[length:var(--border-width-strong)] focus-within:outline-foreground focus-within:outline-offset-2 data-[readonly]:bg-background-subtle motion-reduce:transition-none",
|
|
73
|
+
className,
|
|
74
|
+
)}
|
|
75
|
+
data-readonly={readOnly || undefined}
|
|
76
|
+
style={{ "--sh-ui-rte-min-height": minHeight, "--sh-ui-rte-max-height": maxHeight } as React.CSSProperties}
|
|
77
|
+
>
|
|
78
|
+
{!hideToolbar && <Toolbar editor={editor} disabled={readOnly} />}
|
|
79
|
+
<EditorContent editor={editor} className="sh-ui-rte__viewport" />
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ToolbarProps { editor: Editor | null; disabled: boolean; }
|
|
85
|
+
|
|
86
|
+
function Toolbar({ editor, disabled }: ToolbarProps) {
|
|
87
|
+
const promptLink = useCallback(() => {
|
|
88
|
+
if (!editor) return;
|
|
89
|
+
const previous = editor.getAttributes("link").href as string | undefined;
|
|
90
|
+
const url = window.prompt("URL", previous ?? "https://");
|
|
91
|
+
if (url === null) return;
|
|
92
|
+
if (url === "") {
|
|
93
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const { empty } = editor.state.selection;
|
|
97
|
+
const inLink = editor.isActive("link");
|
|
98
|
+
if (empty && !inLink) {
|
|
99
|
+
editor.chain().focus().insertContent({
|
|
100
|
+
type: "text", text: url,
|
|
101
|
+
marks: [{ type: "link", attrs: { href: url } }],
|
|
102
|
+
}).run();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
106
|
+
}, [editor]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
className="flex flex-wrap items-center gap-0.5 py-[var(--space-1)] px-[var(--space-2)] bg-background-muted border-b border-border"
|
|
111
|
+
role="toolbar"
|
|
112
|
+
aria-label="Formatting"
|
|
113
|
+
aria-disabled={disabled || undefined}
|
|
114
|
+
>
|
|
115
|
+
<ToolbarButton editor={editor} label="Bold" icon={<BoldIcon size={16} />} isActive={editor?.isActive("bold")} canRun={() => !!editor?.can().toggleBold()} run={() => editor?.chain().focus().toggleBold().run()} disabled={disabled} />
|
|
116
|
+
<ToolbarButton editor={editor} label="Italic" icon={<ItalicIcon size={16} />} isActive={editor?.isActive("italic")} canRun={() => !!editor?.can().toggleItalic()} run={() => editor?.chain().focus().toggleItalic().run()} disabled={disabled} />
|
|
117
|
+
<ToolbarButton editor={editor} label="Strikethrough" icon={<StrikethroughIcon size={16} />} isActive={editor?.isActive("strike")} canRun={() => !!editor?.can().toggleStrike()} run={() => editor?.chain().focus().toggleStrike().run()} disabled={disabled} />
|
|
118
|
+
<ToolbarButton editor={editor} label="Inline code" icon={<CodeIcon size={16} />} isActive={editor?.isActive("code")} canRun={() => !!editor?.can().toggleCode()} run={() => editor?.chain().focus().toggleCode().run()} disabled={disabled} />
|
|
119
|
+
|
|
120
|
+
<ToolbarSeparator />
|
|
121
|
+
|
|
122
|
+
<ToolbarButton editor={editor} label="Heading 1" icon={<Heading1Icon size={16} />} isActive={editor?.isActive("heading", { level: 1 })} run={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()} disabled={disabled} />
|
|
123
|
+
<ToolbarButton editor={editor} label="Heading 2" icon={<Heading2Icon size={16} />} isActive={editor?.isActive("heading", { level: 2 })} run={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()} disabled={disabled} />
|
|
124
|
+
<ToolbarButton editor={editor} label="Heading 3" icon={<Heading3Icon size={16} />} isActive={editor?.isActive("heading", { level: 3 })} run={() => editor?.chain().focus().toggleHeading({ level: 3 }).run()} disabled={disabled} />
|
|
125
|
+
|
|
126
|
+
<ToolbarSeparator />
|
|
127
|
+
|
|
128
|
+
<ToolbarButton editor={editor} label="Bulleted list" icon={<ListIcon size={16} />} isActive={editor?.isActive("bulletList")} run={() => editor?.chain().focus().toggleBulletList().run()} disabled={disabled} />
|
|
129
|
+
<ToolbarButton editor={editor} label="Ordered list" icon={<ListOrderedIcon size={16} />} isActive={editor?.isActive("orderedList")} run={() => editor?.chain().focus().toggleOrderedList().run()} disabled={disabled} />
|
|
130
|
+
<ToolbarButton editor={editor} label="Blockquote" icon={<QuoteIcon size={16} />} isActive={editor?.isActive("blockquote")} run={() => editor?.chain().focus().toggleBlockquote().run()} disabled={disabled} />
|
|
131
|
+
<ToolbarButton editor={editor} label="Code block" icon={<Code2Icon size={16} />} isActive={editor?.isActive("codeBlock")} run={() => editor?.chain().focus().toggleCodeBlock().run()} disabled={disabled} />
|
|
132
|
+
|
|
133
|
+
<ToolbarSeparator />
|
|
134
|
+
|
|
135
|
+
<ToolbarButton editor={editor} label="Link" icon={<LinkIcon size={16} />} isActive={editor?.isActive("link")} run={promptLink} disabled={disabled} />
|
|
136
|
+
<ToolbarButton editor={editor} label="Horizontal rule" icon={<MinusIcon size={16} />} run={() => editor?.chain().focus().setHorizontalRule().run()} disabled={disabled} />
|
|
137
|
+
|
|
138
|
+
<ToolbarSeparator />
|
|
139
|
+
|
|
140
|
+
<ToolbarButton editor={editor} label="Undo" icon={<Undo2Icon size={16} />} canRun={() => !!editor?.can().undo()} run={() => editor?.chain().focus().undo().run()} disabled={disabled} />
|
|
141
|
+
<ToolbarButton editor={editor} label="Redo" icon={<Redo2Icon size={16} />} canRun={() => !!editor?.can().redo()} run={() => editor?.chain().focus().redo().run()} disabled={disabled} />
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface ToolbarButtonProps {
|
|
147
|
+
editor: Editor | null;
|
|
148
|
+
label: string;
|
|
149
|
+
icon: React.ReactNode;
|
|
150
|
+
isActive?: boolean;
|
|
151
|
+
canRun?: () => boolean;
|
|
152
|
+
run: () => void;
|
|
153
|
+
disabled: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ToolbarButton({ editor, label, icon, isActive, canRun, run, disabled }: ToolbarButtonProps) {
|
|
157
|
+
const isDisabled = disabled || !editor || (canRun ? !canRun() : false);
|
|
158
|
+
return (
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
className={cx(
|
|
162
|
+
"inline-flex items-center justify-center w-7 h-7 p-0 bg-transparent text-foreground-muted border border-transparent rounded-[calc(var(--radius)-2px)] cursor-pointer transition-[color,background-color,border-color] duration-[var(--duration-fast)] hover:not-disabled:text-foreground hover:not-disabled:bg-background hover:not-disabled:border-border focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-1 disabled:opacity-50 disabled:cursor-not-allowed motion-reduce:transition-none",
|
|
163
|
+
isActive && "text-foreground bg-background border-border-strong",
|
|
164
|
+
)}
|
|
165
|
+
aria-label={label}
|
|
166
|
+
aria-pressed={isActive || undefined}
|
|
167
|
+
title={label}
|
|
168
|
+
disabled={isDisabled}
|
|
169
|
+
onMouseDown={(e) => { e.preventDefault(); }}
|
|
170
|
+
onClick={run}
|
|
171
|
+
>
|
|
172
|
+
{icon}
|
|
173
|
+
</button>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function ToolbarSeparator() {
|
|
178
|
+
return <span aria-hidden className="inline-block w-px h-5 mx-[var(--space-1)] bg-border" />;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof document !== "undefined" && !document.querySelector("style[data-sh-ui-rte]")) {
|
|
182
|
+
const style = document.createElement("style");
|
|
183
|
+
style.setAttribute("data-sh-ui-rte", "");
|
|
184
|
+
style.textContent = `
|
|
185
|
+
.sh-ui-rte__viewport { display: flex; min-height: var(--sh-ui-rte-min-height, 9rem); max-height: var(--sh-ui-rte-max-height, 28rem); overflow-y: auto; }
|
|
186
|
+
.sh-ui-rte__viewport > .ProseMirror { flex: 1; }
|
|
187
|
+
.sh-ui-rte__content { outline: none; padding: var(--space-3) var(--space-4); font-size: 0.9375rem; line-height: 1.65; color: var(--foreground); }
|
|
188
|
+
.sh-ui-rte__content > :first-child { margin-top: 0; }
|
|
189
|
+
.sh-ui-rte__content > :last-child { margin-bottom: 0; }
|
|
190
|
+
.sh-ui-rte__content p { margin: 0 0 var(--space-3); }
|
|
191
|
+
.sh-ui-rte__content h1, .sh-ui-rte__content h2, .sh-ui-rte__content h3, .sh-ui-rte__content h4, .sh-ui-rte__content h5, .sh-ui-rte__content h6 { margin: var(--space-4) 0 var(--space-2); font-weight: 600; line-height: 1.3; }
|
|
192
|
+
.sh-ui-rte__content h1 { font-size: 1.5rem; }
|
|
193
|
+
.sh-ui-rte__content h2 { font-size: 1.25rem; }
|
|
194
|
+
.sh-ui-rte__content h3 { font-size: 1.125rem; }
|
|
195
|
+
.sh-ui-rte__content ul, .sh-ui-rte__content ol { margin: 0 0 var(--space-3); padding-left: var(--space-5); }
|
|
196
|
+
.sh-ui-rte__content li { margin-bottom: var(--space-1); }
|
|
197
|
+
.sh-ui-rte__content li > p { margin: 0; }
|
|
198
|
+
.sh-ui-rte__content blockquote { margin: 0 0 var(--space-3); padding: var(--space-2) var(--space-3); border-left: 3px solid var(--border-strong); background: var(--background-subtle); color: var(--foreground-muted); border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0; }
|
|
199
|
+
.sh-ui-rte__content blockquote > :last-child { margin-bottom: 0; }
|
|
200
|
+
.sh-ui-rte__content code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.875em; padding: 0.125rem 0.375rem; border-radius: calc(var(--radius) - 4px); background: var(--background-muted); color: var(--foreground); }
|
|
201
|
+
.sh-ui-rte__content pre { margin: 0 0 var(--space-3); padding: var(--space-3); border: 1px solid var(--border); border-radius: var(--radius); background: var(--background-subtle); overflow-x: auto; font-size: 0.8125rem; line-height: 1.6; }
|
|
202
|
+
.sh-ui-rte__content pre code { padding: 0; background: transparent; font-size: inherit; }
|
|
203
|
+
.sh-ui-rte__content hr { border: 0; border-top: 1px solid var(--border); margin: var(--space-4) 0; }
|
|
204
|
+
.sh-ui-rte__content a { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
|
|
205
|
+
.sh-ui-rte__content a:hover { text-decoration-thickness: 2px; }
|
|
206
|
+
.sh-ui-rte__content p.is-editor-empty:first-child::before, .sh-ui-rte__content .is-editor-empty:first-child::before { content: attr(data-placeholder); color: var(--foreground-muted); float: left; pointer-events: none; height: 0; }
|
|
207
|
+
.sh-ui-rte__content del, .sh-ui-rte__content s { color: var(--foreground-muted); }
|
|
208
|
+
.sh-ui-rte__content ::selection { background: var(--background-muted); }
|
|
209
|
+
`;
|
|
210
|
+
document.head.appendChild(style);
|
|
211
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Select as BaseSelect } from "@base-ui/react/select";
|
|
5
|
+
|
|
6
|
+
function cx(...args: (string | undefined | false)[]) {
|
|
7
|
+
return args.filter(Boolean).join(" ");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Select = BaseSelect.Root;
|
|
11
|
+
|
|
12
|
+
export function SelectValue({ placeholder, className, ...props }: { placeholder?: string; className?: string } & Omit<
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof BaseSelect.Value>, "children"
|
|
14
|
+
>) {
|
|
15
|
+
return (
|
|
16
|
+
<BaseSelect.Value className={cx("flex-1 text-left overflow-hidden text-ellipsis whitespace-nowrap", className)} {...props}>
|
|
17
|
+
{(value) =>
|
|
18
|
+
value !== null && value !== undefined && value !== "" ? (
|
|
19
|
+
(value as React.ReactNode)
|
|
20
|
+
) : (
|
|
21
|
+
<span className="text-foreground-subtle">{placeholder}</span>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
</BaseSelect.Value>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SelectTrigger = React.forwardRef<
|
|
29
|
+
HTMLButtonElement,
|
|
30
|
+
Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Trigger>, "className"> & { className?: string }
|
|
31
|
+
>(({ className, children, ...props }, ref) => (
|
|
32
|
+
<BaseSelect.Trigger
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cx(
|
|
35
|
+
"inline-flex items-center justify-between gap-[var(--space-2)] 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 cursor-pointer transition-[border-color,background-color] duration-[var(--duration-fast)] select-none hover:not-disabled:border-border-strong focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 data-[popup-open]:border-border-strong disabled:opacity-[var(--opacity-disabled)] disabled:pointer-events-none",
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
<BaseSelect.Icon
|
|
42
|
+
className="inline-flex items-center justify-center text-foreground-muted shrink-0 transition-transform duration-[var(--duration-fast)] [[data-popup-open]_&]:rotate-180"
|
|
43
|
+
aria-hidden
|
|
44
|
+
>
|
|
45
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="none">
|
|
46
|
+
<path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
47
|
+
</svg>
|
|
48
|
+
</BaseSelect.Icon>
|
|
49
|
+
</BaseSelect.Trigger>
|
|
50
|
+
));
|
|
51
|
+
SelectTrigger.displayName = "SelectTrigger";
|
|
52
|
+
|
|
53
|
+
export const SelectContent = React.forwardRef<
|
|
54
|
+
HTMLDivElement,
|
|
55
|
+
Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Popup>, "className"> & {
|
|
56
|
+
className?: string;
|
|
57
|
+
container?: React.ComponentPropsWithoutRef<typeof BaseSelect.Portal>["container"];
|
|
58
|
+
}
|
|
59
|
+
>(({ className, children, container, ...props }, ref) => (
|
|
60
|
+
<BaseSelect.Portal container={container}>
|
|
61
|
+
<BaseSelect.Positioner className="outline-none z-[var(--z-dropdown)]" sideOffset={4} align="start">
|
|
62
|
+
<BaseSelect.Popup
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cx(
|
|
65
|
+
"min-w-[var(--anchor-width,10rem)] 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-select-in_140ms_ease-out] data-[ending-style]:animate-[sh-ui-select-out_100ms_ease-in_forwards] motion-reduce:animate-none motion-reduce:data-[ending-style]:animate-none",
|
|
66
|
+
className,
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
{children}
|
|
71
|
+
</BaseSelect.Popup>
|
|
72
|
+
</BaseSelect.Positioner>
|
|
73
|
+
</BaseSelect.Portal>
|
|
74
|
+
));
|
|
75
|
+
SelectContent.displayName = "SelectContent";
|
|
76
|
+
|
|
77
|
+
export const SelectGroup = BaseSelect.Group;
|
|
78
|
+
|
|
79
|
+
export const SelectLabel = React.forwardRef<
|
|
80
|
+
HTMLDivElement,
|
|
81
|
+
Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.GroupLabel>, "className"> & { className?: string }
|
|
82
|
+
>(({ className, ...props }, ref) => (
|
|
83
|
+
<BaseSelect.GroupLabel
|
|
84
|
+
ref={ref}
|
|
85
|
+
className={cx(
|
|
86
|
+
"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]",
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
));
|
|
92
|
+
SelectLabel.displayName = "SelectLabel";
|
|
93
|
+
|
|
94
|
+
export const SelectItem = React.forwardRef<
|
|
95
|
+
HTMLDivElement,
|
|
96
|
+
Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Item>, "className"> & { className?: string }
|
|
97
|
+
>(({ className, children, ...props }, ref) => (
|
|
98
|
+
<BaseSelect.Item
|
|
99
|
+
ref={ref}
|
|
100
|
+
className={cx(
|
|
101
|
+
"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",
|
|
102
|
+
className,
|
|
103
|
+
)}
|
|
104
|
+
{...props}
|
|
105
|
+
>
|
|
106
|
+
<BaseSelect.ItemIndicator
|
|
107
|
+
className="order-1 ml-auto inline-flex items-center justify-center text-foreground"
|
|
108
|
+
aria-hidden
|
|
109
|
+
>
|
|
110
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="none">
|
|
111
|
+
<path d="M3.5 8.5l3 3 6-7" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
|
|
112
|
+
</svg>
|
|
113
|
+
</BaseSelect.ItemIndicator>
|
|
114
|
+
<BaseSelect.ItemText className="flex-1">{children}</BaseSelect.ItemText>
|
|
115
|
+
</BaseSelect.Item>
|
|
116
|
+
));
|
|
117
|
+
SelectItem.displayName = "SelectItem";
|
|
118
|
+
|
|
119
|
+
export const SelectSeparator = React.forwardRef<
|
|
120
|
+
HTMLDivElement,
|
|
121
|
+
Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Separator>, "className"> & { className?: string }
|
|
122
|
+
>(({ className, ...props }, ref) => (
|
|
123
|
+
<BaseSelect.Separator
|
|
124
|
+
ref={ref}
|
|
125
|
+
className={cx("h-px bg-border my-[var(--space-1)]", className)}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
));
|
|
129
|
+
SelectSeparator.displayName = "SelectSeparator";
|
|
130
|
+
|
|
131
|
+
/* MultiSelect — 동일 로직, 위 컴포넌트 재사용 */
|
|
132
|
+
|
|
133
|
+
type BaseRootProps = React.ComponentPropsWithoutRef<typeof BaseSelect.Root>;
|
|
134
|
+
|
|
135
|
+
type MultiSelectCtx = { values: string[]; remove: (value: string) => void; clear: () => void; };
|
|
136
|
+
const MultiSelectContext = React.createContext<MultiSelectCtx | null>(null);
|
|
137
|
+
export const useMultiSelect = () => {
|
|
138
|
+
const ctx = React.useContext(MultiSelectContext);
|
|
139
|
+
if (!ctx) throw new Error("useMultiSelect는 MultiSelect 하위에서만 사용할 수 있습니다.");
|
|
140
|
+
return ctx;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const MultiSelect = React.forwardRef<
|
|
144
|
+
HTMLDivElement,
|
|
145
|
+
Omit<BaseRootProps, "multiple" | "value" | "defaultValue" | "onValueChange"> & {
|
|
146
|
+
value?: string[]; defaultValue?: string[]; onValueChange?: (value: string[]) => void;
|
|
147
|
+
}
|
|
148
|
+
>(({ value: valueProp, defaultValue, onValueChange, children, ...props }, _ref) => {
|
|
149
|
+
const isControlled = valueProp !== undefined;
|
|
150
|
+
const [internal, setInternal] = React.useState<string[]>(defaultValue ?? []);
|
|
151
|
+
const values = isControlled ? valueProp! : internal;
|
|
152
|
+
|
|
153
|
+
const commit = React.useCallback((next: string[]) => {
|
|
154
|
+
if (!isControlled) setInternal(next);
|
|
155
|
+
onValueChange?.(next);
|
|
156
|
+
}, [isControlled, onValueChange]);
|
|
157
|
+
|
|
158
|
+
const ctx = React.useMemo<MultiSelectCtx>(() => ({
|
|
159
|
+
values,
|
|
160
|
+
remove: (v) => commit(values.filter((x) => x !== v)),
|
|
161
|
+
clear: () => commit([]),
|
|
162
|
+
}), [values, commit]);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<MultiSelectContext.Provider value={ctx}>
|
|
166
|
+
<BaseSelect.Root multiple value={values} onValueChange={commit} {...props}>{children}</BaseSelect.Root>
|
|
167
|
+
</MultiSelectContext.Provider>
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
MultiSelect.displayName = "MultiSelect";
|
|
171
|
+
|
|
172
|
+
export function MultiSelectValue({
|
|
173
|
+
placeholder, render, separator = ", ", className, ...props
|
|
174
|
+
}: {
|
|
175
|
+
placeholder?: string;
|
|
176
|
+
render?: (values: string[], handlers: { remove: (value: string) => void; clear: () => void }) => React.ReactNode;
|
|
177
|
+
separator?: string; className?: string;
|
|
178
|
+
} & Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Value>, "children" | "render">) {
|
|
179
|
+
const { remove, clear } = useMultiSelect();
|
|
180
|
+
return (
|
|
181
|
+
<BaseSelect.Value className={cx("flex-1 text-left overflow-hidden text-ellipsis whitespace-nowrap", className)} {...props}>
|
|
182
|
+
{(value) => {
|
|
183
|
+
const arr = Array.isArray(value) ? (value as string[]) : [];
|
|
184
|
+
if (arr.length === 0) return <span className="text-foreground-subtle">{placeholder}</span>;
|
|
185
|
+
return render ? render(arr, { remove, clear }) : arr.join(separator);
|
|
186
|
+
}}
|
|
187
|
+
</BaseSelect.Value>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (typeof document !== "undefined" && !document.querySelector("style[data-sh-ui-select]")) {
|
|
192
|
+
const style = document.createElement("style");
|
|
193
|
+
style.setAttribute("data-sh-ui-select", "");
|
|
194
|
+
style.textContent = `
|
|
195
|
+
@keyframes sh-ui-select-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
|
|
196
|
+
@keyframes sh-ui-select-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.96); } }
|
|
197
|
+
`;
|
|
198
|
+
document.head.appendChild(style);
|
|
199
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
function cx(...args: (string | undefined | false | null)[]) {
|
|
4
|
+
return args.filter(Boolean).join(" ");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type SeparatorOrientation = "horizontal" | "vertical";
|
|
8
|
+
|
|
9
|
+
export interface SeparatorProps
|
|
10
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "role"> {
|
|
11
|
+
orientation?: SeparatorOrientation;
|
|
12
|
+
/**
|
|
13
|
+
* 의미 없는 시각적 구분선인지 여부. 기본 true(aria-hidden).
|
|
14
|
+
* 스크린리더에도 섹션 구분을 알려야 하면 false.
|
|
15
|
+
*/
|
|
16
|
+
decorative?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 시각적 구분선 (Tailwind utility 변종). 가로(height=1px) / 세로(width=1px).
|
|
21
|
+
*/
|
|
22
|
+
export const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
|
|
23
|
+
function Separator(
|
|
24
|
+
{ className, orientation = "horizontal", decorative = true, ...props },
|
|
25
|
+
ref,
|
|
26
|
+
) {
|
|
27
|
+
const sizing =
|
|
28
|
+
orientation === "horizontal" ? "w-full h-px" : "w-px h-full self-stretch";
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
ref={ref}
|
|
32
|
+
role={decorative ? undefined : "separator"}
|
|
33
|
+
aria-orientation={decorative ? undefined : orientation}
|
|
34
|
+
aria-hidden={decorative || undefined}
|
|
35
|
+
data-orientation={orientation}
|
|
36
|
+
className={cx("bg-border shrink-0", sizing, className)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
Separator.displayName = "Separator";
|