sh-ui-cli 0.46.0 → 0.47.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 (85) hide show
  1. package/data/changelog/versions.json +13 -0
  2. package/data/registry/react/components/accordion/index.module.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.module.css +111 -0
  4. package/data/registry/react/components/avatar/index.module.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.module.css +36 -0
  6. package/data/registry/react/components/badge/index.module.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.module.css +57 -0
  8. package/data/registry/react/components/breadcrumb/index.module.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.module.css +82 -0
  10. package/data/registry/react/components/calendar/index.module.tsx +806 -0
  11. package/data/registry/react/components/calendar/styles.module.css +213 -0
  12. package/data/registry/react/components/carousel/index.module.tsx +430 -0
  13. package/data/registry/react/components/carousel/styles.module.css +155 -0
  14. package/data/registry/react/components/checkbox/index.module.tsx +96 -0
  15. package/data/registry/react/components/checkbox/styles.module.css +75 -0
  16. package/data/registry/react/components/code-editor/index.module.tsx +230 -0
  17. package/data/registry/react/components/code-editor/styles.module.css +76 -0
  18. package/data/registry/react/components/code-panel/index.module.tsx +191 -0
  19. package/data/registry/react/components/code-panel/styles.module.css +124 -0
  20. package/data/registry/react/components/color-picker/index.module.tsx +467 -0
  21. package/data/registry/react/components/color-picker/styles.module.css +166 -0
  22. package/data/registry/react/components/combobox/index.module.tsx +165 -0
  23. package/data/registry/react/components/combobox/styles.module.css +151 -0
  24. package/data/registry/react/components/context-menu/index.module.tsx +251 -0
  25. package/data/registry/react/components/context-menu/styles.module.css +140 -0
  26. package/data/registry/react/components/date-picker/index.module.tsx +520 -0
  27. package/data/registry/react/components/date-picker/styles.module.css +103 -0
  28. package/data/registry/react/components/dialog/index.module.tsx +95 -0
  29. package/data/registry/react/components/dialog/styles.module.css +127 -0
  30. package/data/registry/react/components/dropdown-menu/index.module.tsx +255 -0
  31. package/data/registry/react/components/dropdown-menu/styles.module.css +150 -0
  32. package/data/registry/react/components/file-upload/index.module.tsx +487 -0
  33. package/data/registry/react/components/file-upload/styles.module.css +170 -0
  34. package/data/registry/react/components/form/index.module.tsx +61 -0
  35. package/data/registry/react/components/form/styles.module.css +47 -0
  36. package/data/registry/react/components/header/index.module.tsx +805 -0
  37. package/data/registry/react/components/header/styles.module.css +350 -0
  38. package/data/registry/react/components/label/index.module.tsx +52 -0
  39. package/data/registry/react/components/label/styles.module.css +90 -0
  40. package/data/registry/react/components/markdown-editor/index.module.tsx +119 -0
  41. package/data/registry/react/components/markdown-editor/styles.module.css +160 -0
  42. package/data/registry/react/components/menubar/index.module.tsx +32 -0
  43. package/data/registry/react/components/menubar/styles.module.css +45 -0
  44. package/data/registry/react/components/numeric-input/index.module.tsx +148 -0
  45. package/data/registry/react/components/numeric-input/styles.module.css +56 -0
  46. package/data/registry/react/components/page-toc/index.module.tsx +174 -0
  47. package/data/registry/react/components/page-toc/styles.module.css +82 -0
  48. package/data/registry/react/components/pagination/index.module.tsx +269 -0
  49. package/data/registry/react/components/pagination/styles.module.css +105 -0
  50. package/data/registry/react/components/popover/index.module.tsx +113 -0
  51. package/data/registry/react/components/popover/styles.module.css +65 -0
  52. package/data/registry/react/components/progress/index.module.tsx +54 -0
  53. package/data/registry/react/components/progress/styles.module.css +41 -0
  54. package/data/registry/react/components/radio/index.module.tsx +65 -0
  55. package/data/registry/react/components/radio/styles.module.css +80 -0
  56. package/data/registry/react/components/rich-text-editor/index.module.tsx +348 -0
  57. package/data/registry/react/components/rich-text-editor/styles.module.css +196 -0
  58. package/data/registry/react/components/select/index.module.tsx +234 -0
  59. package/data/registry/react/components/select/styles.module.css +193 -0
  60. package/data/registry/react/components/separator/index.module.tsx +46 -0
  61. package/data/registry/react/components/separator/styles.module.css +15 -0
  62. package/data/registry/react/components/sidebar/index.module.tsx +1067 -0
  63. package/data/registry/react/components/sidebar/styles.module.css +502 -0
  64. package/data/registry/react/components/skeleton/index.module.tsx +22 -0
  65. package/data/registry/react/components/skeleton/styles.module.css +24 -0
  66. package/data/registry/react/components/slider/index.module.tsx +298 -0
  67. package/data/registry/react/components/slider/styles.module.css +64 -0
  68. package/data/registry/react/components/spinner/index.module.tsx +38 -0
  69. package/data/registry/react/components/spinner/styles.module.css +37 -0
  70. package/data/registry/react/components/switch/index.module.tsx +39 -0
  71. package/data/registry/react/components/switch/styles.module.css +83 -0
  72. package/data/registry/react/components/tabs/index.module.tsx +91 -0
  73. package/data/registry/react/components/tabs/styles.module.css +148 -0
  74. package/data/registry/react/components/textarea/index.module.tsx +23 -0
  75. package/data/registry/react/components/textarea/styles.module.css +54 -0
  76. package/data/registry/react/components/toast/index.module.tsx +258 -0
  77. package/data/registry/react/components/toast/styles.module.css +290 -0
  78. package/data/registry/react/components/toggle/index.module.tsx +131 -0
  79. package/data/registry/react/components/toggle/styles.module.css +85 -0
  80. package/data/registry/react/components/tooltip/index.module.tsx +83 -0
  81. package/data/registry/react/components/tooltip/styles.module.css +44 -0
  82. package/data/registry/react/registry.json +560 -0
  83. package/package.json +1 -1
  84. package/src/api.d.ts +4 -3
  85. 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 styles from "./styles.module.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(styles.radio, className)}
26
+ {...props}
27
+ >
28
+ <BaseRadio.Indicator className={styles.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(styles["radio-group"], className)}
60
+ data-orientation={orientation}
61
+ {...props}
62
+ />
63
+ ),
64
+ );
65
+ RadioGroup.displayName = "RadioGroup";
@@ -0,0 +1,80 @@
1
+ /* ───────────── Radio ───────────── */
2
+
3
+ .radio {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ width: 1.125rem;
8
+ height: 1.125rem;
9
+ border: 1px solid var(--border-strong);
10
+ border-radius: 999px;
11
+ background: var(--background);
12
+ cursor: pointer;
13
+ flex-shrink: 0;
14
+ transition: border-color var(--duration-fast);
15
+ -webkit-tap-highlight-color: transparent;
16
+ }
17
+
18
+ .radio:hover:not([data-disabled]) {
19
+ border-color: var(--foreground);
20
+ }
21
+
22
+ .radio:focus-visible {
23
+ outline: var(--border-width-strong) solid var(--foreground);
24
+ outline-offset: 2px;
25
+ }
26
+
27
+ .radio[data-checked] {
28
+ border-color: var(--primary);
29
+ }
30
+
31
+ .radio[data-disabled] {
32
+ opacity: var(--opacity-disabled);
33
+ cursor: not-allowed;
34
+ }
35
+
36
+ /* 모바일/터치: 최소 탭 영역 */
37
+ @media (hover: none) and (pointer: coarse) {
38
+ .radio {
39
+ width: 1.25rem;
40
+ height: 1.25rem;
41
+ }
42
+ }
43
+
44
+ /* ───────────── Indicator (내부 원) ───────────── */
45
+
46
+ .radio__indicator {
47
+ width: 0.5rem;
48
+ height: 0.5rem;
49
+ border-radius: 999px;
50
+ background: var(--primary);
51
+ transform: scale(0);
52
+ transition: transform var(--duration-fast) ease-out;
53
+ }
54
+
55
+ .radio[data-checked] .radio__indicator {
56
+ transform: scale(1);
57
+ }
58
+
59
+ /* ───────────── RadioGroup ───────────── */
60
+
61
+ .radio-group {
62
+ display: flex;
63
+ gap: 0.625rem;
64
+ }
65
+
66
+ .radio-group[data-orientation="vertical"] {
67
+ flex-direction: column;
68
+ }
69
+
70
+ .radio-group[data-orientation="horizontal"] {
71
+ flex-direction: row;
72
+ flex-wrap: wrap;
73
+ }
74
+
75
+ @media (prefers-reduced-motion: reduce) {
76
+ .radio,
77
+ .radio__indicator {
78
+ transition: none;
79
+ }
80
+ }
@@ -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 styles from "./styles.module.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: styles["rte__is-empty"],
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: styles.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(styles.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={styles.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={styles.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(styles.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={styles.rte__sep} />;
348
+ }
@@ -0,0 +1,196 @@
1
+ .rte {
2
+ display: flex;
3
+ flex-direction: column;
4
+ border: 1px solid var(--border);
5
+ border-radius: var(--radius);
6
+ background: var(--background);
7
+ overflow: hidden;
8
+ transition: border-color var(--duration-fast);
9
+ }
10
+ .rte:focus-within {
11
+ border-color: var(--foreground);
12
+ outline: var(--border-width-strong) solid var(--foreground);
13
+ outline-offset: 2px;
14
+ }
15
+ .rte[data-readonly] {
16
+ background: var(--background-subtle);
17
+ }
18
+
19
+ /* ─── Toolbar ─── */
20
+ .rte__toolbar {
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ align-items: center;
24
+ gap: 0.125rem;
25
+ padding: var(--space-1) var(--space-2);
26
+ background: var(--background-muted);
27
+ border-bottom: 1px solid var(--border);
28
+ }
29
+ .rte__btn {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ width: 1.875rem;
34
+ height: 1.875rem;
35
+ padding: 0;
36
+ background: transparent;
37
+ color: var(--foreground-muted);
38
+ border: 1px solid transparent;
39
+ border-radius: calc(var(--radius) - 2px);
40
+ cursor: pointer;
41
+ transition:
42
+ color var(--duration-fast),
43
+ background-color var(--duration-fast),
44
+ border-color var(--duration-fast);
45
+ -webkit-tap-highlight-color: transparent;
46
+ }
47
+ .rte__btn:hover:not(:disabled) {
48
+ color: var(--foreground);
49
+ background: var(--background);
50
+ border-color: var(--border);
51
+ }
52
+ .rte__btn:focus-visible {
53
+ outline: var(--border-width-strong) solid var(--foreground);
54
+ outline-offset: 1px;
55
+ }
56
+ .rte__btn.is-active {
57
+ color: var(--foreground);
58
+ background: var(--background);
59
+ border-color: var(--border-strong);
60
+ }
61
+ .rte__btn:disabled {
62
+ opacity: 0.5;
63
+ cursor: not-allowed;
64
+ }
65
+ .rte__sep {
66
+ display: inline-block;
67
+ width: 1px;
68
+ height: 1.25rem;
69
+ margin: 0 var(--space-1);
70
+ background: var(--border);
71
+ }
72
+
73
+ /* ─── Content ─── */
74
+ .rte__viewport {
75
+ display: flex;
76
+ min-height: var(--sh-ui-rte-min-height, 9rem);
77
+ max-height: var(--sh-ui-rte-max-height, 28rem);
78
+ overflow-y: auto;
79
+ }
80
+ .rte__viewport > .ProseMirror {
81
+ flex: 1;
82
+ }
83
+ .rte__content {
84
+ outline: none;
85
+ padding: var(--space-3) var(--space-4);
86
+ font-size: 0.9375rem;
87
+ line-height: 1.65;
88
+ color: var(--foreground);
89
+ }
90
+
91
+ .rte__content > :first-child {
92
+ margin-top: 0;
93
+ }
94
+ .rte__content > :last-child {
95
+ margin-bottom: 0;
96
+ }
97
+
98
+ .rte__content p {
99
+ margin: 0 0 var(--space-3);
100
+ }
101
+ .rte__content h1,
102
+ .rte__content h2,
103
+ .rte__content h3,
104
+ .rte__content h4,
105
+ .rte__content h5,
106
+ .rte__content h6 {
107
+ margin: var(--space-4) 0 var(--space-2);
108
+ font-weight: 600;
109
+ line-height: 1.3;
110
+ }
111
+ .rte__content h1 { font-size: 1.5rem; }
112
+ .rte__content h2 { font-size: 1.25rem; }
113
+ .rte__content h3 { font-size: 1.125rem; }
114
+
115
+ .rte__content ul,
116
+ .rte__content ol {
117
+ margin: 0 0 var(--space-3);
118
+ padding-left: var(--space-5);
119
+ }
120
+ .rte__content li {
121
+ margin-bottom: var(--space-1);
122
+ }
123
+ .rte__content li > p {
124
+ margin: 0;
125
+ }
126
+
127
+ .rte__content blockquote {
128
+ margin: 0 0 var(--space-3);
129
+ padding: var(--space-2) var(--space-3);
130
+ border-left: 3px solid var(--border-strong);
131
+ background: var(--background-subtle);
132
+ color: var(--foreground-muted);
133
+ border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;
134
+ }
135
+ .rte__content blockquote > :last-child {
136
+ margin-bottom: 0;
137
+ }
138
+
139
+ .rte__content code {
140
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
141
+ font-size: 0.875em;
142
+ padding: 0.125rem 0.375rem;
143
+ border-radius: calc(var(--radius) - 4px);
144
+ background: var(--background-muted);
145
+ color: var(--foreground);
146
+ }
147
+ .rte__content pre {
148
+ margin: 0 0 var(--space-3);
149
+ padding: var(--space-3);
150
+ border: 1px solid var(--border);
151
+ border-radius: var(--radius);
152
+ background: var(--background-subtle);
153
+ overflow-x: auto;
154
+ font-size: 0.8125rem;
155
+ line-height: 1.6;
156
+ }
157
+ .rte__content pre code {
158
+ padding: 0;
159
+ background: transparent;
160
+ font-size: inherit;
161
+ }
162
+
163
+ .rte__content hr {
164
+ border: 0;
165
+ border-top: 1px solid var(--border);
166
+ margin: var(--space-4) 0;
167
+ }
168
+
169
+ .rte__content a {
170
+ color: var(--primary);
171
+ text-decoration: underline;
172
+ text-underline-offset: 2px;
173
+ }
174
+ .rte__content a:hover {
175
+ text-decoration-thickness: 2px;
176
+ }
177
+
178
+ /* Placeholder (Tiptap extension) */
179
+ .rte__content p.is-editor-empty:first-child::before,
180
+ .rte__content .is-editor-empty:first-child::before {
181
+ content: attr(data-placeholder);
182
+ color: var(--foreground-muted);
183
+ float: left;
184
+ pointer-events: none;
185
+ height: 0;
186
+ }
187
+
188
+ .rte__content del,
189
+ .rte__content s {
190
+ color: var(--foreground-muted);
191
+ }
192
+
193
+ /* ProseMirror selection */
194
+ .rte__content ::selection {
195
+ background: var(--background-muted);
196
+ }