sh-ui-cli 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/data/changelog/versions.json +27 -0
  2. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.css.ts +131 -0
  4. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.css.ts +68 -0
  6. package/data/registry/react/components/badge/index.vanilla-extract.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.css.ts +71 -0
  8. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.css.ts +95 -0
  10. package/data/registry/react/components/button/index.vanilla-extract.tsx +45 -0
  11. package/data/registry/react/components/button/styles.css.ts +120 -0
  12. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +806 -0
  13. package/data/registry/react/components/calendar/styles.css.ts +250 -0
  14. package/data/registry/react/components/card/index.vanilla-extract.tsx +63 -0
  15. package/data/registry/react/components/card/styles.css.ts +88 -0
  16. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +430 -0
  17. package/data/registry/react/components/carousel/styles.css.ts +169 -0
  18. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +96 -0
  19. package/data/registry/react/components/checkbox/styles.css.ts +74 -0
  20. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +230 -0
  21. package/data/registry/react/components/code-editor/styles.css.ts +97 -0
  22. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +191 -0
  23. package/data/registry/react/components/code-panel/styles.css.ts +151 -0
  24. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +467 -0
  25. package/data/registry/react/components/color-picker/styles.css.ts +169 -0
  26. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +165 -0
  27. package/data/registry/react/components/combobox/styles.css.ts +174 -0
  28. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +251 -0
  29. package/data/registry/react/components/context-menu/styles.css.ts +167 -0
  30. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +520 -0
  31. package/data/registry/react/components/date-picker/styles.css.ts +111 -0
  32. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +95 -0
  33. package/data/registry/react/components/dialog/styles.css.ts +140 -0
  34. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +255 -0
  35. package/data/registry/react/components/dropdown-menu/styles.css.ts +175 -0
  36. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +487 -0
  37. package/data/registry/react/components/file-upload/styles.css.ts +193 -0
  38. package/data/registry/react/components/form/index.vanilla-extract.tsx +61 -0
  39. package/data/registry/react/components/form/styles.css.ts +56 -0
  40. package/data/registry/react/components/header/index.vanilla-extract.tsx +805 -0
  41. package/data/registry/react/components/header/styles.css.ts +413 -0
  42. package/data/registry/react/components/input/index.vanilla-extract.tsx +425 -0
  43. package/data/registry/react/components/input/styles.css.ts +202 -0
  44. package/data/registry/react/components/label/index.vanilla-extract.tsx +52 -0
  45. package/data/registry/react/components/label/styles.css.ts +141 -0
  46. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +119 -0
  47. package/data/registry/react/components/markdown-editor/styles.css.ts +231 -0
  48. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +32 -0
  49. package/data/registry/react/components/menubar/styles.css.ts +53 -0
  50. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +148 -0
  51. package/data/registry/react/components/numeric-input/styles.css.ts +65 -0
  52. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +174 -0
  53. package/data/registry/react/components/page-toc/styles.css.ts +97 -0
  54. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +269 -0
  55. package/data/registry/react/components/pagination/styles.css.ts +113 -0
  56. package/data/registry/react/components/popover/index.vanilla-extract.tsx +113 -0
  57. package/data/registry/react/components/popover/styles.css.ts +78 -0
  58. package/data/registry/react/components/progress/index.vanilla-extract.tsx +54 -0
  59. package/data/registry/react/components/progress/styles.css.ts +53 -0
  60. package/data/registry/react/components/radio/index.vanilla-extract.tsx +65 -0
  61. package/data/registry/react/components/radio/styles.css.ts +79 -0
  62. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +348 -0
  63. package/data/registry/react/components/rich-text-editor/styles.css.ts +243 -0
  64. package/data/registry/react/components/select/index.vanilla-extract.tsx +234 -0
  65. package/data/registry/react/components/select/styles.css.ts +225 -0
  66. package/data/registry/react/components/separator/index.vanilla-extract.tsx +46 -0
  67. package/data/registry/react/components/separator/styles.css.ts +24 -0
  68. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +1067 -0
  69. package/data/registry/react/components/sidebar/styles.css.ts +578 -0
  70. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +22 -0
  71. package/data/registry/react/components/skeleton/styles.css.ts +30 -0
  72. package/data/registry/react/components/slider/index.vanilla-extract.tsx +298 -0
  73. package/data/registry/react/components/slider/styles.css.ts +75 -0
  74. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +38 -0
  75. package/data/registry/react/components/spinner/styles.css.ts +60 -0
  76. package/data/registry/react/components/switch/index.vanilla-extract.tsx +39 -0
  77. package/data/registry/react/components/switch/styles.css.ts +87 -0
  78. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +91 -0
  79. package/data/registry/react/components/tabs/styles.css.ts +145 -0
  80. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +23 -0
  81. package/data/registry/react/components/textarea/styles.css.ts +55 -0
  82. package/data/registry/react/components/toast/index.vanilla-extract.tsx +258 -0
  83. package/data/registry/react/components/toast/styles.css.ts +307 -0
  84. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +131 -0
  85. package/data/registry/react/components/toggle/styles.css.ts +109 -0
  86. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +83 -0
  87. package/data/registry/react/components/tooltip/styles.css.ts +59 -0
  88. package/data/registry/react/peer-versions.json +1 -0
  89. package/data/registry/react/registry.json +922 -42
  90. package/data/tokens/build.mjs +3 -0
  91. package/package.json +1 -1
  92. package/src/api.d.ts +4 -3
  93. package/src/constants.js +4 -3
@@ -0,0 +1,65 @@
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
+ import { byKey, radio, radio__indicator, radioGroup } from "./styles.css";
5
+
6
+
7
+ import { cn } from "@SH_UI_UTILS@";
8
+ /* ───────────── Radio ───────────── */
9
+
10
+ export type RadioProps = Omit<
11
+ React.ComponentPropsWithoutRef<typeof BaseRadio.Root>,
12
+ "className"
13
+ > & {
14
+ className?: string;
15
+ };
16
+
17
+ /**
18
+ * 단일 선택지. 단독으로 쓰지 않고 반드시 `RadioGroup` 안에 두 개 이상을 묶어 사용한다.
19
+ * 단일 선택이지만 즉시 적용되는 설정에는 Switch를, 다중 선택에는 Checkbox를 권장.
20
+ */
21
+ export const Radio = React.forwardRef<HTMLElement, RadioProps>(
22
+ ({ className, ...props }, ref) => (
23
+ <BaseRadio.Root
24
+ ref={ref}
25
+ className={cn(radio, className)}
26
+ {...props}
27
+ >
28
+ <BaseRadio.Indicator className={radio__indicator} />
29
+ </BaseRadio.Root>
30
+ ),
31
+ );
32
+ Radio.displayName = "Radio";
33
+
34
+ /* ───────────── RadioGroup ───────────── */
35
+
36
+ export type RadioGroupProps = Omit<
37
+ React.ComponentPropsWithoutRef<typeof BaseRadioGroup>,
38
+ "className"
39
+ > & {
40
+ className?: string;
41
+ /**
42
+ * 그룹 내 항목 배치 방향.
43
+ * - `vertical` — 세로 나열 (기본)
44
+ * - `horizontal` — 가로 나열. 짧은 라벨 2~3개에만 권장
45
+ *
46
+ * @default "vertical"
47
+ */
48
+ orientation?: "horizontal" | "vertical";
49
+ };
50
+
51
+ /**
52
+ * 여러 Radio를 묶는 컨테이너. 같은 `name` 아래 단일 선택을 보장하고,
53
+ * 키보드 화살표로 항목 간 이동이 가능하다. 그룹 라벨은 외부 `<Label>`로 제공할 것.
54
+ */
55
+ export const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
56
+ ({ className, orientation = "vertical", ...props }, ref) => (
57
+ <BaseRadioGroup
58
+ ref={ref}
59
+ className={cn(radioGroup, className)}
60
+ data-orientation={orientation}
61
+ {...props}
62
+ />
63
+ ),
64
+ );
65
+ RadioGroup.displayName = "RadioGroup";
@@ -0,0 +1,79 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const radio = style({
4
+ display: "inline-flex",
5
+ alignItems: "center",
6
+ justifyContent: "center",
7
+ width: "1.125rem",
8
+ height: "1.125rem",
9
+ border: "1px solid var(--border-strong)",
10
+ borderRadius: "999px",
11
+ background: "var(--background)",
12
+ cursor: "pointer",
13
+ flexShrink: 0,
14
+ transition: "border-color var(--duration-fast)",
15
+ WebkitTapHighlightColor: "transparent",
16
+ selectors: {
17
+ "&:hover:not([data-disabled])": {
18
+ borderColor: "var(--foreground)",
19
+ },
20
+ "&:focus-visible": {
21
+ outline: "var(--border-width-strong) solid var(--foreground)",
22
+ outlineOffset: "2px",
23
+ },
24
+ "&[data-checked]": {
25
+ borderColor: "var(--primary)",
26
+ },
27
+ "&[data-disabled]": {
28
+ opacity: "var(--opacity-disabled)",
29
+ cursor: "not-allowed",
30
+ },
31
+ [`&[data-checked] ${radio__indicator}`]: {
32
+ transform: "scale(1)",
33
+ },
34
+ },
35
+ "@media": {
36
+ "(hover: none) and (pointer: coarse)": {
37
+ width: "1.25rem",
38
+ height: "1.25rem",
39
+ },
40
+ "(prefers-reduced-motion: reduce)": {
41
+ transition: "none",
42
+ },
43
+ },
44
+ });
45
+
46
+ export const radio__indicator = style({
47
+ width: "0.5rem",
48
+ height: "0.5rem",
49
+ borderRadius: "999px",
50
+ background: "var(--primary)",
51
+ transform: "scale(0)",
52
+ transition: "transform var(--duration-fast) ease-out",
53
+ "@media": {
54
+ "(prefers-reduced-motion: reduce)": {
55
+ transition: "none",
56
+ },
57
+ },
58
+ });
59
+
60
+ export const radioGroup = style({
61
+ display: "flex",
62
+ gap: "0.625rem",
63
+ selectors: {
64
+ "&[data-orientation="vertical"]": {
65
+ flexDirection: "column",
66
+ },
67
+ "&[data-orientation="horizontal"]": {
68
+ flexDirection: "row",
69
+ flexWrap: "wrap",
70
+ },
71
+ },
72
+ });
73
+
74
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
75
+ export const byKey: Record<string, string> = {
76
+ "radio": radio,
77
+ "radio__indicator": radio__indicator,
78
+ "radio-group": radioGroup,
79
+ };
@@ -0,0 +1,348 @@
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 { cn } from "@SH_UI_UTILS@";
9
+ import {
10
+ BoldIcon,
11
+ ItalicIcon,
12
+ StrikethroughIcon,
13
+ Heading1Icon,
14
+ Heading2Icon,
15
+ Heading3Icon,
16
+ ListIcon,
17
+ ListOrderedIcon,
18
+ QuoteIcon,
19
+ CodeIcon,
20
+ Code2Icon,
21
+ LinkIcon,
22
+ MinusIcon,
23
+ Undo2Icon,
24
+ Redo2Icon,
25
+ } from "lucide-react";
26
+ import { byKey, rte, rte__toolbar, rte__btn, rte__sep, rte__viewport, rte__content, rteIsEmpty } from "./styles.css";
27
+
28
+ export interface RichTextEditorProps {
29
+ /**
30
+ * Controlled — 현재 HTML. 명시 시 외부 상태가 진실원천이 되고 onChange 로 갱신한다.
31
+ * 미지정이면 uncontrolled — Tiptap editor 가 자체 doc 으로 동작.
32
+ */
33
+ value?: string;
34
+ /**
35
+ * Uncontrolled 초기 HTML. value 미지정 시에만 사용.
36
+ * @default ""
37
+ */
38
+ defaultValue?: string;
39
+ /** 본문이 바뀔 때마다 호출 (controlled · uncontrolled 모두). HTML 문자열을 그대로 넘긴다. */
40
+ onChange?: (html: string) => void;
41
+ /** 비어 있을 때 표시할 placeholder. */
42
+ placeholder?: string;
43
+ /** 읽기 전용. 키 입력·툴바 차단. */
44
+ readOnly?: boolean;
45
+ /** 상단 툴바 숨기기. 본문 영역만 렌더. */
46
+ hideToolbar?: boolean;
47
+ /** 본문 영역의 최소 높이. */
48
+ minHeight?: string;
49
+ /** 본문 영역의 최대 높이. 초과 시 내부 스크롤. */
50
+ maxHeight?: string;
51
+ className?: string;
52
+ "aria-label"?: string;
53
+ }
54
+
55
+
56
+ /**
57
+ * Tiptap 기반 리치 텍스트 에디터.
58
+ *
59
+ * Controlled (value/onChange) · Uncontrolled (defaultValue) 모두 지원. 라우터·외부 상태와
60
+ * 동기화할 게 없는 단일 입력 폼이라면 defaultValue 한 줄로 끝 — useState 불필요.
61
+ *
62
+ * 기본 toolbar 는 StarterKit 의 표준 마크업(헤딩·리스트·인용·코드·링크 등) 을 다룬다.
63
+ */
64
+ export function RichTextEditor({
65
+ value: valueProp,
66
+ defaultValue,
67
+ onChange,
68
+ placeholder,
69
+ readOnly = false,
70
+ hideToolbar = false,
71
+ minHeight,
72
+ maxHeight,
73
+ className,
74
+ "aria-label": ariaLabel = "Rich text editor",
75
+ }: RichTextEditorProps) {
76
+ const isControlled = valueProp !== undefined;
77
+ const editor = useEditor({
78
+ extensions: [
79
+ StarterKit,
80
+ Placeholder.configure({
81
+ placeholder: placeholder ?? "",
82
+ emptyEditorClass: rteIsEmpty,
83
+ }),
84
+ Link.configure({
85
+ openOnClick: false,
86
+ autolink: true,
87
+ HTMLAttributes: { rel: "noopener noreferrer", target: "_blank" },
88
+ }),
89
+ ],
90
+ content: valueProp ?? defaultValue ?? "",
91
+ editable: !readOnly,
92
+ immediatelyRender: false,
93
+ onUpdate: ({ editor }) => {
94
+ onChange?.(editor.getHTML());
95
+ },
96
+ editorProps: {
97
+ attributes: {
98
+ class: rte__content,
99
+ "aria-label": ariaLabel,
100
+ },
101
+ },
102
+ });
103
+
104
+ // controlled 모드에서만 외부 value 를 에디터 doc 에 동기화
105
+ useEffect(() => {
106
+ if (!isControlled) return;
107
+ if (!editor) return;
108
+ if (editor.getHTML() === valueProp) return;
109
+ editor.commands.setContent(valueProp ?? "", { emitUpdate: false });
110
+ }, [isControlled, valueProp, editor]);
111
+
112
+ useEffect(() => {
113
+ editor?.setEditable(!readOnly);
114
+ }, [readOnly, editor]);
115
+
116
+ return (
117
+ <div
118
+ className={cn(rte, className)}
119
+ data-readonly={readOnly || undefined}
120
+ style={
121
+ {
122
+ "--sh-ui-rte-min-height": minHeight,
123
+ "--sh-ui-rte-max-height": maxHeight,
124
+ } as React.CSSProperties
125
+ }
126
+ >
127
+ {!hideToolbar && <Toolbar editor={editor} disabled={readOnly} />}
128
+ <EditorContent editor={editor} className={rte__viewport} />
129
+ </div>
130
+ );
131
+ }
132
+
133
+ interface ToolbarProps {
134
+ editor: Editor | null;
135
+ disabled: boolean;
136
+ }
137
+
138
+ function Toolbar({ editor, disabled }: ToolbarProps) {
139
+ const promptLink = useCallback(() => {
140
+ if (!editor) return;
141
+ const previous = editor.getAttributes("link").href as string | undefined;
142
+ const url = window.prompt("URL", previous ?? "https://");
143
+ if (url === null) return;
144
+ if (url === "") {
145
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
146
+ return;
147
+ }
148
+ const { empty } = editor.state.selection;
149
+ const inLink = editor.isActive("link");
150
+ if (empty && !inLink) {
151
+ // 선택 없이 호출되면 URL 자체를 anchor 텍스트로 삽입 — 빈 링크 마크가 남는 걸 방지
152
+ editor
153
+ .chain()
154
+ .focus()
155
+ .insertContent({
156
+ type: "text",
157
+ text: url,
158
+ marks: [{ type: "link", attrs: { href: url } }],
159
+ })
160
+ .run();
161
+ return;
162
+ }
163
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
164
+ }, [editor]);
165
+
166
+ return (
167
+ <div className={rte__toolbar} role="toolbar" aria-label="Formatting" aria-disabled={disabled || undefined}>
168
+ <ToolbarButton
169
+ editor={editor}
170
+ label="Bold"
171
+ icon={<BoldIcon size={16} />}
172
+ isActive={editor?.isActive("bold")}
173
+ canRun={() => !!editor?.can().toggleBold()}
174
+ run={() => editor?.chain().focus().toggleBold().run()}
175
+ disabled={disabled}
176
+ />
177
+ <ToolbarButton
178
+ editor={editor}
179
+ label="Italic"
180
+ icon={<ItalicIcon size={16} />}
181
+ isActive={editor?.isActive("italic")}
182
+ canRun={() => !!editor?.can().toggleItalic()}
183
+ run={() => editor?.chain().focus().toggleItalic().run()}
184
+ disabled={disabled}
185
+ />
186
+ <ToolbarButton
187
+ editor={editor}
188
+ label="Strikethrough"
189
+ icon={<StrikethroughIcon size={16} />}
190
+ isActive={editor?.isActive("strike")}
191
+ canRun={() => !!editor?.can().toggleStrike()}
192
+ run={() => editor?.chain().focus().toggleStrike().run()}
193
+ disabled={disabled}
194
+ />
195
+ <ToolbarButton
196
+ editor={editor}
197
+ label="Inline code"
198
+ icon={<CodeIcon size={16} />}
199
+ isActive={editor?.isActive("code")}
200
+ canRun={() => !!editor?.can().toggleCode()}
201
+ run={() => editor?.chain().focus().toggleCode().run()}
202
+ disabled={disabled}
203
+ />
204
+
205
+ <ToolbarSeparator />
206
+
207
+ <ToolbarButton
208
+ editor={editor}
209
+ label="Heading 1"
210
+ icon={<Heading1Icon size={16} />}
211
+ isActive={editor?.isActive("heading", { level: 1 })}
212
+ run={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}
213
+ disabled={disabled}
214
+ />
215
+ <ToolbarButton
216
+ editor={editor}
217
+ label="Heading 2"
218
+ icon={<Heading2Icon size={16} />}
219
+ isActive={editor?.isActive("heading", { level: 2 })}
220
+ run={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
221
+ disabled={disabled}
222
+ />
223
+ <ToolbarButton
224
+ editor={editor}
225
+ label="Heading 3"
226
+ icon={<Heading3Icon size={16} />}
227
+ isActive={editor?.isActive("heading", { level: 3 })}
228
+ run={() => editor?.chain().focus().toggleHeading({ level: 3 }).run()}
229
+ disabled={disabled}
230
+ />
231
+
232
+ <ToolbarSeparator />
233
+
234
+ <ToolbarButton
235
+ editor={editor}
236
+ label="Bulleted list"
237
+ icon={<ListIcon size={16} />}
238
+ isActive={editor?.isActive("bulletList")}
239
+ run={() => editor?.chain().focus().toggleBulletList().run()}
240
+ disabled={disabled}
241
+ />
242
+ <ToolbarButton
243
+ editor={editor}
244
+ label="Ordered list"
245
+ icon={<ListOrderedIcon size={16} />}
246
+ isActive={editor?.isActive("orderedList")}
247
+ run={() => editor?.chain().focus().toggleOrderedList().run()}
248
+ disabled={disabled}
249
+ />
250
+ <ToolbarButton
251
+ editor={editor}
252
+ label="Blockquote"
253
+ icon={<QuoteIcon size={16} />}
254
+ isActive={editor?.isActive("blockquote")}
255
+ run={() => editor?.chain().focus().toggleBlockquote().run()}
256
+ disabled={disabled}
257
+ />
258
+ <ToolbarButton
259
+ editor={editor}
260
+ label="Code block"
261
+ icon={<Code2Icon size={16} />}
262
+ isActive={editor?.isActive("codeBlock")}
263
+ run={() => editor?.chain().focus().toggleCodeBlock().run()}
264
+ disabled={disabled}
265
+ />
266
+
267
+ <ToolbarSeparator />
268
+
269
+ <ToolbarButton
270
+ editor={editor}
271
+ label="Link"
272
+ icon={<LinkIcon size={16} />}
273
+ isActive={editor?.isActive("link")}
274
+ run={promptLink}
275
+ disabled={disabled}
276
+ />
277
+ <ToolbarButton
278
+ editor={editor}
279
+ label="Horizontal rule"
280
+ icon={<MinusIcon size={16} />}
281
+ run={() => editor?.chain().focus().setHorizontalRule().run()}
282
+ disabled={disabled}
283
+ />
284
+
285
+ <ToolbarSeparator />
286
+
287
+ <ToolbarButton
288
+ editor={editor}
289
+ label="Undo"
290
+ icon={<Undo2Icon size={16} />}
291
+ canRun={() => !!editor?.can().undo()}
292
+ run={() => editor?.chain().focus().undo().run()}
293
+ disabled={disabled}
294
+ />
295
+ <ToolbarButton
296
+ editor={editor}
297
+ label="Redo"
298
+ icon={<Redo2Icon size={16} />}
299
+ canRun={() => !!editor?.can().redo()}
300
+ run={() => editor?.chain().focus().redo().run()}
301
+ disabled={disabled}
302
+ />
303
+ </div>
304
+ );
305
+ }
306
+
307
+ interface ToolbarButtonProps {
308
+ editor: Editor | null;
309
+ label: string;
310
+ icon: React.ReactNode;
311
+ isActive?: boolean;
312
+ /** false 를 반환하면 비활성. */
313
+ canRun?: () => boolean;
314
+ run: () => void;
315
+ disabled: boolean;
316
+ }
317
+
318
+ function ToolbarButton({
319
+ editor,
320
+ label,
321
+ icon,
322
+ isActive,
323
+ canRun,
324
+ run,
325
+ disabled,
326
+ }: ToolbarButtonProps) {
327
+ const isDisabled = disabled || !editor || (canRun ? !canRun() : false);
328
+ return (
329
+ <button
330
+ type="button"
331
+ className={cn(rte__btn, isActive && "is-active")}
332
+ aria-label={label}
333
+ aria-pressed={isActive || undefined}
334
+ title={label}
335
+ disabled={isDisabled}
336
+ onMouseDown={(e) => {
337
+ e.preventDefault();
338
+ }}
339
+ onClick={run}
340
+ >
341
+ {icon}
342
+ </button>
343
+ );
344
+ }
345
+
346
+ function ToolbarSeparator() {
347
+ return <span aria-hidden className={rte__sep} />;
348
+ }