@intinyagroup/rich-text 0.1.1-alpha.16 → 0.1.1-alpha.17

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.
@@ -1,36 +1,62 @@
1
1
  <script lang="ts">
2
- import { onMount, onDestroy } from 'svelte';
3
- import { Editor, Node, mergeAttributes } from '@tiptap/core';
4
- import StarterKit from '@tiptap/starter-kit';
5
- import Placeholder from '@tiptap/extension-placeholder';
6
- import Underline from '@tiptap/extension-underline';
7
- import TextAlign from '@tiptap/extension-text-align';
8
- import Link from '@tiptap/extension-link';
9
- import Image from '@tiptap/extension-image';
10
- import Highlight from '@tiptap/extension-highlight';
11
- import Table from '@tiptap/extension-table';
12
- import TableRow from '@tiptap/extension-table-row';
13
- import TableCell from '@tiptap/extension-table-cell';
14
- import TableHeader from '@tiptap/extension-table-header';
15
- import Typography from '@tiptap/extension-typography';
16
- import BubbleMenu from '@tiptap/extension-bubble-menu';
17
- import TaskList from '@tiptap/extension-task-list';
18
- import TaskItem from '@tiptap/extension-task-item';
2
+ import { onMount, onDestroy } from "svelte";
3
+ import { Editor, Node, mergeAttributes } from "@tiptap/core";
4
+ import StarterKit from "@tiptap/starter-kit";
5
+ import Placeholder from "@tiptap/extension-placeholder";
6
+ import Underline from "@tiptap/extension-underline";
7
+ import TextAlign from "@tiptap/extension-text-align";
8
+ import Link from "@tiptap/extension-link";
9
+ import Image from "@tiptap/extension-image";
10
+ import Highlight from "@tiptap/extension-highlight";
11
+ import Table from "@tiptap/extension-table";
12
+ import TableRow from "@tiptap/extension-table-row";
13
+ import TableCell from "@tiptap/extension-table-cell";
14
+ import TableHeader from "@tiptap/extension-table-header";
15
+ import Typography from "@tiptap/extension-typography";
16
+ import BubbleMenu from "@tiptap/extension-bubble-menu";
17
+ import TaskList from "@tiptap/extension-task-list";
18
+ import TaskItem from "@tiptap/extension-task-item";
19
19
  import {
20
- Bold, Italic, Underline as UnderlineIcon, Strikethrough, Highlighter,
21
- AlignLeft, AlignCenter, AlignRight, AlignJustify,
22
- List, ListOrdered, Quote, Code, Minus, Link as LinkIcon, Image as ImageIcon,
23
- Undo, Redo, Heading1, Heading2, Heading3, TableIcon, Plus, Trash2,
24
- Video, CheckSquare, Info, AlertTriangle, Sparkles, HelpCircle, FileText
25
- } from 'lucide-svelte';
26
- import { Button, Separator } from '@intinyagroup/ui';
27
- import { cn } from '@intinyagroup/grid-core/utils';
20
+ Bold,
21
+ Italic,
22
+ Underline as UnderlineIcon,
23
+ Strikethrough,
24
+ Highlighter,
25
+ AlignLeft,
26
+ AlignCenter,
27
+ AlignRight,
28
+ AlignJustify,
29
+ List,
30
+ ListOrdered,
31
+ Quote,
32
+ Code,
33
+ Minus,
34
+ Link as LinkIcon,
35
+ Image as ImageIcon,
36
+ Undo,
37
+ Redo,
38
+ Heading1,
39
+ Heading2,
40
+ Heading3,
41
+ TableIcon,
42
+ Plus,
43
+ Trash2,
44
+ Video,
45
+ CheckSquare,
46
+ Info,
47
+ AlertTriangle,
48
+ Sparkles,
49
+ HelpCircle,
50
+ FileText,
51
+ } from "lucide-svelte";
52
+ import { Button, Separator } from "@intinyagroup/ui";
53
+ import { cn } from "@intinyagroup/grid-core/utils";
28
54
 
29
55
  let {
30
- content = '',
31
- placeholder = 'Start writing...',
56
+ content = "",
57
+ placeholder = "Start writing...",
32
58
  editable = true,
33
- mode = 'classic',
59
+ mode = "classic",
34
60
  height = 400,
35
61
  class: className,
36
62
  onUpdate,
@@ -41,7 +67,7 @@
41
67
  placeholder?: string;
42
68
  editable?: boolean;
43
69
  /** Toolbar mode: 'classic' (fixed top toolbar), 'bubble' (Notion-style floating toolbar only), or 'none' */
44
- mode?: 'classic' | 'bubble' | 'none';
70
+ mode?: "classic" | "bubble" | "none";
45
71
  height?: number;
46
72
  class?: string;
47
73
  onUpdate?: (html: string) => void;
@@ -58,32 +84,103 @@
58
84
 
59
85
  // Slash commands state
60
86
  let showSlashMenu = $state(false);
61
- let slashSearch = $state('');
87
+ let slashSearch = $state("");
62
88
  let slashIndex = $state(0);
63
89
  let slashMenuPos = $state({ top: 0, left: 0 });
64
90
 
65
91
  const slashCommands = [
66
- { title: 'Sub-page', desc: 'Embed a sub-page inside this page', icon: FileText, action: () => insertSubPage() },
67
- { title: 'Heading 1', desc: 'Big section heading', icon: Heading1, action: () => setHeading(1) },
68
- { title: 'Heading 2', desc: 'Medium section heading', icon: Heading2, action: () => setHeading(2) },
69
- { title: 'Heading 3', desc: 'Small subsection heading', icon: Heading3, action: () => setHeading(3) },
70
- { title: 'To-do list', desc: 'Track tasks with a checklist', icon: CheckSquare, action: () => editor?.chain().focus().toggleTaskList().run() },
71
- { title: 'Bullet list', desc: 'Create a bulleted list', icon: List, action: () => toggleBulletList() },
72
- { title: 'Numbered list', desc: 'Create a numbered list', icon: ListOrdered, action: () => toggleOrderedList() },
73
- { title: 'Quote', desc: 'Capture a quote or blockquote', icon: Quote, action: () => toggleBlockquote() },
74
- { title: 'Code block', desc: 'Code snippet with syntax highlight', icon: Code, action: () => toggleCodeBlock() },
75
- { title: 'Callout Info', desc: 'Informational highlight block', icon: Info, action: () => insertCallout('info') },
76
- { title: 'Callout Warning', desc: 'Warning or caution block', icon: AlertTriangle, action: () => insertCallout('warning') },
77
- { title: 'Callout Tip', desc: 'Tip or recommendation block', icon: Sparkles, action: () => insertCallout('tip') },
78
- { title: 'Table', desc: 'Insert 3x3 table grid', icon: TableIcon, action: () => insertTable() },
79
- { title: 'Divider', desc: 'Visually divide sections', icon: Minus, action: () => insertHorizontalRule() },
92
+ {
93
+ title: "Sub-page",
94
+ desc: "Embed a sub-page inside this page",
95
+ icon: FileText,
96
+ action: () => insertSubPage(),
97
+ },
98
+ {
99
+ title: "Heading 1",
100
+ desc: "Big section heading",
101
+ icon: Heading1,
102
+ action: () => setHeading(1),
103
+ },
104
+ {
105
+ title: "Heading 2",
106
+ desc: "Medium section heading",
107
+ icon: Heading2,
108
+ action: () => setHeading(2),
109
+ },
110
+ {
111
+ title: "Heading 3",
112
+ desc: "Small subsection heading",
113
+ icon: Heading3,
114
+ action: () => setHeading(3),
115
+ },
116
+ {
117
+ title: "To-do list",
118
+ desc: "Track tasks with a checklist",
119
+ icon: CheckSquare,
120
+ action: () => editor?.chain().focus().toggleTaskList().run(),
121
+ },
122
+ {
123
+ title: "Bullet list",
124
+ desc: "Create a bulleted list",
125
+ icon: List,
126
+ action: () => toggleBulletList(),
127
+ },
128
+ {
129
+ title: "Numbered list",
130
+ desc: "Create a numbered list",
131
+ icon: ListOrdered,
132
+ action: () => toggleOrderedList(),
133
+ },
134
+ {
135
+ title: "Quote",
136
+ desc: "Capture a quote or blockquote",
137
+ icon: Quote,
138
+ action: () => toggleBlockquote(),
139
+ },
140
+ {
141
+ title: "Code block",
142
+ desc: "Code snippet with syntax highlight",
143
+ icon: Code,
144
+ action: () => toggleCodeBlock(),
145
+ },
146
+ {
147
+ title: "Callout Info",
148
+ desc: "Informational highlight block",
149
+ icon: Info,
150
+ action: () => insertCallout("info"),
151
+ },
152
+ {
153
+ title: "Callout Warning",
154
+ desc: "Warning or caution block",
155
+ icon: AlertTriangle,
156
+ action: () => insertCallout("warning"),
157
+ },
158
+ {
159
+ title: "Callout Tip",
160
+ desc: "Tip or recommendation block",
161
+ icon: Sparkles,
162
+ action: () => insertCallout("tip"),
163
+ },
164
+ {
165
+ title: "Table",
166
+ desc: "Insert 3x3 table grid",
167
+ icon: TableIcon,
168
+ action: () => insertTable(),
169
+ },
170
+ {
171
+ title: "Divider",
172
+ desc: "Visually divide sections",
173
+ icon: Minus,
174
+ action: () => insertHorizontalRule(),
175
+ },
80
176
  ];
81
177
 
82
178
  const slashFilteredCommands = $derived(
83
- slashCommands.filter((c) =>
84
- c.title.toLowerCase().includes(slashSearch.toLowerCase()) ||
85
- c.desc.toLowerCase().includes(slashSearch.toLowerCase())
86
- )
179
+ slashCommands.filter(
180
+ (c) =>
181
+ c.title.toLowerCase().includes(slashSearch.toLowerCase()) ||
182
+ c.desc.toLowerCase().includes(slashSearch.toLowerCase()),
183
+ ),
87
184
  );
88
185
  // ---------------------------------------------------------------------------
89
186
  // Custom Extensions
@@ -95,16 +192,19 @@
95
192
  return {
96
193
  ...this.parent?.(),
97
194
  href: { default: null },
98
- alt: { default: '' },
99
- title: { default: '' },
100
- target: { default: '_blank' },
195
+ alt: { default: "" },
196
+ title: { default: "" },
197
+ target: { default: "_blank" },
101
198
  };
102
199
  },
103
200
  renderHTML({ HTMLAttributes }) {
104
201
  const { href, target, alt, title, ...rest } = HTMLAttributes;
105
- const img = ['img', mergeAttributes(this.options.HTMLAttributes, rest, { alt, title })];
202
+ const img = [
203
+ "img",
204
+ mergeAttributes(this.options.HTMLAttributes, rest, { alt, title }),
205
+ ];
106
206
  if (href) {
107
- return ['a', { href, target, class: 'image-link' }, img];
207
+ return ["a", { href, target, class: "image-link" }, img];
108
208
  }
109
209
  return img;
110
210
  },
@@ -112,8 +212,8 @@
112
212
 
113
213
  /** YouTube iframe embed node */
114
214
  const CustomYoutube = Node.create({
115
- name: 'youtube',
116
- group: 'block',
215
+ name: "youtube",
216
+ group: "block",
117
217
  atom: true,
118
218
  addAttributes() {
119
219
  return { src: { default: null } };
@@ -126,14 +226,14 @@
126
226
  },
127
227
  renderHTML({ HTMLAttributes }) {
128
228
  return [
129
- 'div',
130
- { class: 'video-wrapper' },
229
+ "div",
230
+ { class: "video-wrapper" },
131
231
  [
132
- 'iframe',
232
+ "iframe",
133
233
  mergeAttributes(HTMLAttributes, {
134
- width: '100%',
135
- height: '315',
136
- allowfullscreen: 'true',
234
+ width: "100%",
235
+ height: "315",
236
+ allowfullscreen: "true",
137
237
  }),
138
238
  ],
139
239
  ];
@@ -152,8 +252,8 @@
152
252
  reader.onload = (e) => {
153
253
  const img = new Image();
154
254
  img.onload = () => {
155
- const canvas = document.createElement('canvas');
156
- const ctx = canvas.getContext('2d');
255
+ const canvas = document.createElement("canvas");
256
+ const ctx = canvas.getContext("2d");
157
257
  if (!ctx) {
158
258
  resolve(file);
159
259
  return;
@@ -179,13 +279,13 @@
179
279
  canvas.toBlob(
180
280
  (blob) => {
181
281
  if (blob) {
182
- resolve(new File([blob], file.name, { type: 'image/jpeg' }));
282
+ resolve(new File([blob], file.name, { type: "image/jpeg" }));
183
283
  } else {
184
284
  resolve(file);
185
285
  }
186
286
  },
187
- 'image/jpeg',
188
- 0.85
287
+ "image/jpeg",
288
+ 0.85,
189
289
  );
190
290
  };
191
291
  img.src = e.target?.result as string;
@@ -218,15 +318,15 @@
218
318
 
219
319
  function handleFiles(files: File[]) {
220
320
  for (const file of files) {
221
- if (file.type.startsWith('image/')) {
321
+ if (file.type.startsWith("image/")) {
222
322
  uploadAndInsertImage(file);
223
323
  }
224
324
  }
225
325
  }
226
326
 
227
327
  const CustomYoutube = Node.create({
228
- name: 'youtube',
229
- group: 'block',
328
+ name: "youtube",
329
+ group: "block",
230
330
  atom: true,
231
331
  addAttributes() {
232
332
  return {
@@ -238,15 +338,17 @@
238
338
  },
239
339
  renderHTML({ HTMLAttributes }) {
240
340
  return [
241
- 'div',
242
- { class: 'video-container my-4 aspect-video rounded-xl overflow-hidden' },
341
+ "div",
342
+ {
343
+ class: "video-container my-4 aspect-video rounded-xl overflow-hidden",
344
+ },
243
345
  [
244
- 'iframe',
346
+ "iframe",
245
347
  mergeAttributes(HTMLAttributes, {
246
- class: 'w-full h-full border-0',
348
+ class: "w-full h-full border-0",
247
349
  allow:
248
- 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
249
- allowfullscreen: 'true',
350
+ "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
351
+ allowfullscreen: "true",
250
352
  }),
251
353
  ],
252
354
  ];
@@ -254,14 +356,14 @@
254
356
  });
255
357
  /** SubPage block extension (Page-in-page card) */
256
358
  const SubPage = Node.create({
257
- name: 'subpage',
258
- group: 'block',
359
+ name: "subpage",
360
+ group: "block",
259
361
  atom: true,
260
362
  addAttributes() {
261
363
  return {
262
364
  id: { default: () => `page-${Date.now()}` },
263
- title: { default: 'Untitled Sub-page' },
264
- icon: { default: '📄' }
365
+ title: { default: "Untitled Sub-page" },
366
+ icon: { default: "📄" },
265
367
  };
266
368
  },
267
369
  parseHTML() {
@@ -269,28 +371,40 @@
269
371
  },
270
372
  renderHTML({ HTMLAttributes }) {
271
373
  return [
272
- 'div',
374
+ "div",
273
375
  mergeAttributes(HTMLAttributes, {
274
- 'data-type': 'subpage',
275
- class: 'subpage-block my-2 flex items-center gap-2.5 px-3 py-2 rounded-lg border border-[var(--ui-border)] bg-[var(--ui-card)] hover:bg-[var(--ui-secondary)]/50 transition-colors cursor-pointer shadow-xs group',
276
- 'data-page-id': HTMLAttributes.id,
277
- 'data-page-title': HTMLAttributes.title
376
+ "data-type": "subpage",
377
+ class:
378
+ "subpage-block my-2 flex items-center gap-2.5 px-3 py-2 rounded-lg border border-[var(--ui-border)] bg-[var(--ui-card)] hover:bg-[var(--ui-secondary)]/50 transition-colors cursor-pointer shadow-xs group",
379
+ "data-page-id": HTMLAttributes.id,
380
+ "data-page-title": HTMLAttributes.title,
278
381
  }),
279
- ['span', { class: 'text-base shrink-0 select-none' }, HTMLAttributes.icon || '📄'],
280
- ['span', { class: 'text-sm font-semibold text-[var(--ui-foreground)] underline-offset-4 group-hover:underline truncate' }, HTMLAttributes.title || 'Untitled Sub-page']
382
+ [
383
+ "span",
384
+ { class: "text-base shrink-0 select-none" },
385
+ HTMLAttributes.icon || "📄",
386
+ ],
387
+ [
388
+ "span",
389
+ {
390
+ class:
391
+ "text-sm font-semibold text-[var(--ui-foreground)] underline-offset-4 group-hover:underline truncate",
392
+ },
393
+ HTMLAttributes.title || "Untitled Sub-page",
394
+ ],
281
395
  ];
282
396
  },
283
397
  });
284
398
 
285
399
  /** Callout block extension with type icon & colored border */
286
400
  const Callout = Node.create({
287
- name: 'callout',
288
- group: 'block',
289
- content: 'block+',
401
+ name: "callout",
402
+ group: "block",
403
+ content: "block+",
290
404
  defining: true,
291
405
  addAttributes() {
292
406
  return {
293
- type: { default: 'info' }, // info | warning | tip
407
+ type: { default: "info" }, // info | warning | tip
294
408
  };
295
409
  },
296
410
  parseHTML() {
@@ -298,10 +412,10 @@
298
412
  },
299
413
  renderHTML({ HTMLAttributes }) {
300
414
  return [
301
- 'div',
415
+ "div",
302
416
  mergeAttributes(HTMLAttributes, {
303
- 'data-type': 'callout',
304
- class: `callout-box callout-${HTMLAttributes.type || 'info'} my-3 p-3.5 rounded-xl border flex gap-3`,
417
+ "data-type": "callout",
418
+ class: `callout-box callout-${HTMLAttributes.type || "info"} my-3 p-3.5 rounded-xl border flex gap-3`,
305
419
  }),
306
420
  0,
307
421
  ];
@@ -309,10 +423,10 @@
309
423
  });
310
424
 
311
425
  function addYoutube() {
312
- const url = window.prompt('Enter YouTube URL:');
426
+ const url = window.prompt("Enter YouTube URL:");
313
427
  if (!url || !editor) return;
314
428
  const match = url.match(
315
- /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|shorts\/)([^#&?]*).*/
429
+ /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|shorts\/)([^#&?]*).*/,
316
430
  );
317
431
  const id = match && match[2].length === 11 ? match[2] : null;
318
432
  if (id) {
@@ -320,7 +434,7 @@
320
434
  .chain()
321
435
  .focus()
322
436
  .insertContent({
323
- type: 'youtube',
437
+ type: "youtube",
324
438
  attrs: { src: `https://www.youtube.com/embed/${id}` },
325
439
  })
326
440
  .run();
@@ -333,82 +447,138 @@
333
447
 
334
448
  function getActiveStates(e: Editor): Record<string, boolean> {
335
449
  return {
336
- bold: e.isActive('bold'),
337
- italic: e.isActive('italic'),
338
- underline: e.isActive('underline'),
339
- strike: e.isActive('strike'),
340
- highlight: e.isActive('highlight'),
341
- h1: e.isActive('heading', { level: 1 }),
342
- h2: e.isActive('heading', { level: 2 }),
343
- h3: e.isActive('heading', { level: 3 }),
344
- bulletList: e.isActive('bulletList'),
345
- orderedList: e.isActive('orderedList'),
346
- blockquote: e.isActive('blockquote'),
347
- codeBlock: e.isActive('codeBlock'),
348
- alignLeft: e.isActive({ textAlign: 'left' }),
349
- alignCenter: e.isActive({ textAlign: 'center' }),
350
- alignRight: e.isActive({ textAlign: 'right' }),
351
- alignJustify: e.isActive({ textAlign: 'justify' }),
352
- link: e.isActive('link'),
450
+ bold: e.isActive("bold"),
451
+ italic: e.isActive("italic"),
452
+ underline: e.isActive("underline"),
453
+ strike: e.isActive("strike"),
454
+ highlight: e.isActive("highlight"),
455
+ h1: e.isActive("heading", { level: 1 }),
456
+ h2: e.isActive("heading", { level: 2 }),
457
+ h3: e.isActive("heading", { level: 3 }),
458
+ bulletList: e.isActive("bulletList"),
459
+ orderedList: e.isActive("orderedList"),
460
+ blockquote: e.isActive("blockquote"),
461
+ codeBlock: e.isActive("codeBlock"),
462
+ alignLeft: e.isActive({ textAlign: "left" }),
463
+ alignCenter: e.isActive({ textAlign: "center" }),
464
+ alignRight: e.isActive({ textAlign: "right" }),
465
+ alignJustify: e.isActive({ textAlign: "justify" }),
466
+ link: e.isActive("link"),
353
467
  };
354
468
  }
355
469
 
356
- function toggleBold() { editor?.chain().focus().toggleBold().run(); }
357
- function toggleItalic() { editor?.chain().focus().toggleItalic().run(); }
358
- function toggleUnderline() { editor?.chain().focus().toggleUnderline().run(); }
359
- function toggleStrike() { editor?.chain().focus().toggleStrike().run(); }
360
- function setHeading(level: 1 | 2 | 3) { editor?.chain().focus().toggleHeading({ level }).run(); }
361
- function toggleBulletList() { editor?.chain().focus().toggleBulletList().run(); }
362
- function toggleOrderedList() { editor?.chain().focus().toggleOrderedList().run(); }
363
- function toggleBlockquote() { editor?.chain().focus().toggleBlockquote().run(); }
364
- function toggleCodeBlock() { editor?.chain().focus().toggleCodeBlock().run(); }
365
- function setAlign(align: 'left' | 'center' | 'right' | 'justify') {
470
+ function toggleBold() {
471
+ editor?.chain().focus().toggleBold().run();
472
+ }
473
+ function toggleItalic() {
474
+ editor?.chain().focus().toggleItalic().run();
475
+ }
476
+ function toggleUnderline() {
477
+ editor?.chain().focus().toggleUnderline().run();
478
+ }
479
+ function toggleStrike() {
480
+ editor?.chain().focus().toggleStrike().run();
481
+ }
482
+ function setHeading(level: 1 | 2 | 3) {
483
+ editor?.chain().focus().toggleHeading({ level }).run();
484
+ }
485
+ function toggleBulletList() {
486
+ editor?.chain().focus().toggleBulletList().run();
487
+ }
488
+ function toggleOrderedList() {
489
+ editor?.chain().focus().toggleOrderedList().run();
490
+ }
491
+ function toggleBlockquote() {
492
+ editor?.chain().focus().toggleBlockquote().run();
493
+ }
494
+ function toggleCodeBlock() {
495
+ editor?.chain().focus().toggleCodeBlock().run();
496
+ }
497
+ function setAlign(align: "left" | "center" | "right" | "justify") {
366
498
  editor?.chain().focus().setTextAlign(align).run();
367
499
  }
368
500
  function setLink() {
369
- const url = window.prompt('Enter URL:');
501
+ const url = window.prompt("Enter URL:");
370
502
  if (url) editor?.chain().focus().setLink({ href: url }).run();
371
503
  }
372
504
  function setImage() {
373
- const url = window.prompt('Enter image URL:');
505
+ const url = window.prompt("Enter image URL:");
374
506
  if (url) editor?.chain().focus().setImage({ src: url }).run();
375
507
  }
376
- function insertHorizontalRule() { editor?.chain().focus().setHorizontalRule().run(); }
508
+ function insertHorizontalRule() {
509
+ editor?.chain().focus().setHorizontalRule().run();
510
+ }
377
511
  function insertTable() {
378
- editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
512
+ editor
513
+ ?.chain()
514
+ .focus()
515
+ .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
516
+ .run();
379
517
  }
380
- function insertCallout(type: 'info' | 'warning' | 'tip') {
381
- editor?.chain().focus().insertContent({
382
- type: 'callout',
383
- attrs: { type },
384
- content: [{ type: 'paragraph', text: 'Tulis catatan penting di sini...' }]
385
- }).run();
518
+ function insertCallout(type: "info" | "warning" | "tip") {
519
+ editor
520
+ ?.chain()
521
+ .focus()
522
+ .insertContent({
523
+ type: "callout",
524
+ attrs: { type },
525
+ content: [
526
+ { type: "paragraph", text: "Tulis catatan penting di sini..." },
527
+ ],
528
+ })
529
+ .run();
386
530
  }
387
531
  function insertSubPage() {
388
- const title = window.prompt('Enter Sub-page title:') || 'Untitled Sub-page';
389
- editor?.chain().focus().insertContent({
390
- type: 'subpage',
391
- attrs: { id: `page-${Date.now()}`, title, icon: '📄' }
392
- }).run();
532
+ const title = window.prompt("Enter Sub-page title:") || "Untitled Sub-page";
533
+ editor
534
+ ?.chain()
535
+ .focus()
536
+ .insertContent({
537
+ type: "subpage",
538
+ attrs: { id: `page-${Date.now()}`, title, icon: "📄" },
539
+ })
540
+ .run();
393
541
  }
394
542
 
395
- function executeSlashCommand(cmd: typeof slashCommands[0]) {
543
+ function executeSlashCommand(cmd: (typeof slashCommands)[0]) {
396
544
  if (!editor) return;
397
545
  const { from } = editor.state.selection;
398
546
  const deleteFrom = from - (slashSearch.length + 1);
399
- editor.chain().focus().deleteRange({ from: Math.max(0, deleteFrom), to: from }).run();
547
+ editor
548
+ .chain()
549
+ .focus()
550
+ .deleteRange({ from: Math.max(0, deleteFrom), to: from })
551
+ .run();
400
552
  cmd.action();
401
553
  showSlashMenu = false;
402
554
  }
403
- function addColumnBefore() { editor?.chain().focus().addColumnBefore().run(); }
404
- function addColumnAfter() { editor?.chain().focus().addColumnAfter().run(); }
405
- function deleteColumn() { editor?.chain().focus().deleteColumn().run(); }
406
- function addRowBefore() { editor?.chain().focus().addRowBefore().run(); }
407
- function addRowAfter() { editor?.chain().focus().addRowAfter().run(); }
408
- function deleteRow() { editor?.chain().focus().deleteRow().run(); }
409
- function deleteTable() { editor?.chain().focus().deleteTable().run(); }
410
- function undo() { editor?.chain().focus().undo().run(); }
411
- function redo() { editor?.chain().focus().redo().run(); }
555
+ function addColumnBefore() {
556
+ editor?.chain().focus().addColumnBefore().run();
557
+ }
558
+ function addColumnAfter() {
559
+ editor?.chain().focus().addColumnAfter().run();
560
+ }
561
+ function deleteColumn() {
562
+ editor?.chain().focus().deleteColumn().run();
563
+ }
564
+ function addRowBefore() {
565
+ editor?.chain().focus().addRowBefore().run();
566
+ }
567
+ function addRowAfter() {
568
+ editor?.chain().focus().addRowAfter().run();
569
+ }
570
+ function deleteRow() {
571
+ editor?.chain().focus().deleteRow().run();
572
+ }
573
+ function deleteTable() {
574
+ editor?.chain().focus().deleteTable().run();
575
+ }
576
+ function undo() {
577
+ editor?.chain().focus().undo().run();
578
+ }
579
+ function redo() {
580
+ editor?.chain().focus().redo().run();
581
+ }
412
582
 
413
583
  // ---------------------------------------------------------------------------
414
584
  // Lifecycle
@@ -423,12 +593,12 @@
423
593
  StarterKit,
424
594
  Placeholder.configure({ placeholder }),
425
595
  Underline,
426
- TextAlign.configure({ types: ['heading', 'paragraph'] }),
596
+ TextAlign.configure({ types: ["heading", "paragraph"] }),
427
597
  Link.configure({ openOnClick: false }),
428
598
  CustomImage.configure({
429
599
  inline: false,
430
600
  HTMLAttributes: {
431
- class: 'rounded-lg mx-auto block max-w-full h-auto cursor-pointer',
601
+ class: "rounded-lg mx-auto block max-w-full h-auto cursor-pointer",
432
602
  },
433
603
  }),
434
604
  Highlight.configure({ multicolor: true }),
@@ -439,10 +609,10 @@
439
609
  TableHeader,
440
610
  BubbleMenu.configure({
441
611
  element: bubbleMenuEl!,
442
- pluginKey: 'bubbleMenu',
612
+ pluginKey: "bubbleMenu",
443
613
  shouldShow: ({ editor: e }) =>
444
614
  e.isEditable &&
445
- !e.isActive('table') &&
615
+ !e.isActive("table") &&
446
616
  e.view.state.selection.content().size > 0,
447
617
  }),
448
618
  CustomYoutube,
@@ -461,7 +631,12 @@
461
631
  // Detect slash command trigger
462
632
  const { state } = e;
463
633
  const { from } = state.selection;
464
- const textBefore = state.doc.textBetween(Math.max(0, from - 20), from, '\n', '\0');
634
+ const textBefore = state.doc.textBetween(
635
+ Math.max(0, from - 20),
636
+ from,
637
+ "\n",
638
+ "\0",
639
+ );
465
640
  const slashMatch = textBefore.match(/\/([a-zA-Z0-9]*)$/);
466
641
 
467
642
  if (slashMatch) {
@@ -473,7 +648,13 @@
473
648
  if (parentRect) {
474
649
  slashMenuPos = {
475
650
  top: coords.bottom - parentRect.top + 8,
476
- left: Math.max(16, Math.min(coords.left - parentRect.left, parentRect.width - 260))
651
+ left: Math.max(
652
+ 16,
653
+ Math.min(
654
+ coords.left - parentRect.left,
655
+ parentRect.width - 260,
656
+ ),
657
+ ),
477
658
  };
478
659
  }
479
660
  } catch {
@@ -489,7 +670,8 @@
489
670
  },
490
671
  editorProps: {
491
672
  attributes: {
492
- class: 'prose prose-sm max-w-none focus:outline-none',
673
+ class:
674
+ "rich-text-content prose prose-sm max-w-none focus:outline-none",
493
675
  },
494
676
  handleDOMEvents: {
495
677
  paste: (_view, event) => {
@@ -497,7 +679,7 @@
497
679
  if (!items) return false;
498
680
  const imageFiles: File[] = [];
499
681
  for (let i = 0; i < items.length; i++) {
500
- if (items[i].type.startsWith('image/')) {
682
+ if (items[i].type.startsWith("image/")) {
501
683
  const file = items[i].getAsFile();
502
684
  if (file) imageFiles.push(file);
503
685
  }
@@ -513,7 +695,7 @@
513
695
  const files = event.dataTransfer?.files;
514
696
  if (!files?.length) return false;
515
697
  const imageFiles = Array.from(files).filter((f) =>
516
- f.type.startsWith('image/')
698
+ f.type.startsWith("image/"),
517
699
  );
518
700
  if (imageFiles.length) {
519
701
  event.preventDefault();
@@ -523,10 +705,12 @@
523
705
  return false;
524
706
  },
525
707
  click: (_view, event) => {
526
- const target = (event.target as HTMLElement)?.closest('.subpage-block') as HTMLElement | null;
708
+ const target = (event.target as HTMLElement)?.closest(
709
+ ".subpage-block",
710
+ ) as HTMLElement | null;
527
711
  if (target) {
528
- const id = target.getAttribute('data-page-id') || '';
529
- const title = target.getAttribute('data-page-title') || '';
712
+ const id = target.getAttribute("data-page-id") || "";
713
+ const title = target.getAttribute("data-page-title") || "";
530
714
  onOpenSubPage?.({ id, title });
531
715
  return true;
532
716
  }
@@ -534,30 +718,32 @@
534
718
  },
535
719
  keydown: (_view, event) => {
536
720
  if (showSlashMenu) {
537
- if (event.key === 'ArrowDown') {
721
+ if (event.key === "ArrowDown") {
538
722
  event.preventDefault();
539
723
  slashIndex = (slashIndex + 1) % slashFilteredCommands.length;
540
724
  return true;
541
725
  }
542
- if (event.key === 'ArrowUp') {
726
+ if (event.key === "ArrowUp") {
543
727
  event.preventDefault();
544
- slashIndex = (slashIndex - 1 + slashFilteredCommands.length) % slashFilteredCommands.length;
728
+ slashIndex =
729
+ (slashIndex - 1 + slashFilteredCommands.length) %
730
+ slashFilteredCommands.length;
545
731
  return true;
546
732
  }
547
- if (event.key === 'Enter') {
733
+ if (event.key === "Enter") {
548
734
  event.preventDefault();
549
735
  const cmd = slashFilteredCommands[slashIndex];
550
736
  if (cmd) executeSlashCommand(cmd);
551
737
  return true;
552
738
  }
553
- if (event.key === 'Escape') {
739
+ if (event.key === "Escape") {
554
740
  event.preventDefault();
555
741
  showSlashMenu = false;
556
742
  return true;
557
743
  }
558
744
  }
559
745
  return false;
560
- }
746
+ },
561
747
  },
562
748
  },
563
749
  });
@@ -570,14 +756,16 @@
570
756
 
571
757
  <div
572
758
  class={cn(
573
- 'rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] shadow-xs overflow-hidden transition-colors',
574
- className
759
+ "rich-text-editor group rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] shadow-sm overflow-hidden transition-[border-color,box-shadow] focus-within:border-[var(--ui-ring)] focus-within:ring-2 focus-within:ring-[var(--ui-ring)]/15",
760
+ className,
575
761
  )}
576
762
  >
577
763
  <!-- Fixed Top Toolbar (only shown in 'classic' mode) -->
578
- {#if editable && mode === 'classic'}
764
+ {#if editable && mode === "classic"}
579
765
  <div
580
- class="flex flex-wrap items-center gap-1 px-3 py-2 border-b border-[var(--ui-border)] bg-[var(--ui-secondary)]/25 backdrop-blur-xs"
766
+ class="rich-text-toolbar sticky top-0 z-20 flex min-h-12 items-center gap-1 overflow-x-auto border-b border-[var(--ui-border)] bg-[var(--ui-card)]/95 px-2.5 py-2 backdrop-blur-md sm:px-3"
767
+ role="toolbar"
768
+ aria-label="Text formatting"
581
769
  >
582
770
  <div class="flex items-center gap-0.5">
583
771
  <Button
@@ -607,10 +795,10 @@
607
795
  variant="ghost"
608
796
  size="sm"
609
797
  class={cn(
610
- 'size-7.5 p-0 rounded-md transition-colors',
798
+ "size-7.5 p-0 rounded-md transition-colors",
611
799
  isActive.h1
612
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
613
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
800
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
801
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
614
802
  )}
615
803
  onclick={() => setHeading(1)}
616
804
  title="Heading 1"
@@ -621,10 +809,10 @@
621
809
  variant="ghost"
622
810
  size="sm"
623
811
  class={cn(
624
- 'size-7.5 p-0 rounded-md transition-colors',
812
+ "size-7.5 p-0 rounded-md transition-colors",
625
813
  isActive.h2
626
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
627
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
814
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
815
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
628
816
  )}
629
817
  onclick={() => setHeading(2)}
630
818
  title="Heading 2"
@@ -635,10 +823,10 @@
635
823
  variant="ghost"
636
824
  size="sm"
637
825
  class={cn(
638
- 'size-7.5 p-0 rounded-md transition-colors',
826
+ "size-7.5 p-0 rounded-md transition-colors",
639
827
  isActive.h3
640
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
641
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
828
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
829
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
642
830
  )}
643
831
  onclick={() => setHeading(3)}
644
832
  title="Heading 3"
@@ -654,10 +842,10 @@
654
842
  variant="ghost"
655
843
  size="sm"
656
844
  class={cn(
657
- 'size-7.5 p-0 rounded-md transition-colors',
845
+ "size-7.5 p-0 rounded-md transition-colors",
658
846
  isActive.bold
659
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
660
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
847
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
848
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
661
849
  )}
662
850
  onclick={toggleBold}
663
851
  title="Bold (Ctrl+B)"
@@ -668,10 +856,10 @@
668
856
  variant="ghost"
669
857
  size="sm"
670
858
  class={cn(
671
- 'size-7.5 p-0 rounded-md transition-colors',
859
+ "size-7.5 p-0 rounded-md transition-colors",
672
860
  isActive.italic
673
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
674
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
861
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
862
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
675
863
  )}
676
864
  onclick={toggleItalic}
677
865
  title="Italic (Ctrl+I)"
@@ -682,10 +870,10 @@
682
870
  variant="ghost"
683
871
  size="sm"
684
872
  class={cn(
685
- 'size-7.5 p-0 rounded-md transition-colors',
873
+ "size-7.5 p-0 rounded-md transition-colors",
686
874
  isActive.underline
687
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
688
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
875
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
876
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
689
877
  )}
690
878
  onclick={toggleUnderline}
691
879
  title="Underline (Ctrl+U)"
@@ -696,10 +884,10 @@
696
884
  variant="ghost"
697
885
  size="sm"
698
886
  class={cn(
699
- 'size-7.5 p-0 rounded-md transition-colors',
887
+ "size-7.5 p-0 rounded-md transition-colors",
700
888
  isActive.strike
701
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
702
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
889
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
890
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
703
891
  )}
704
892
  onclick={toggleStrike}
705
893
  title="Strikethrough"
@@ -710,12 +898,12 @@
710
898
  variant="ghost"
711
899
  size="sm"
712
900
  class={cn(
713
- 'size-7.5 p-0 rounded-md transition-colors',
901
+ "size-7.5 p-0 rounded-md transition-colors",
714
902
  isActive.highlight
715
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
716
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
903
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
904
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
717
905
  )}
718
- onclick={() => toggleHighlight('#fef08a')}
906
+ onclick={() => toggleHighlight("#fef08a")}
719
907
  title="Highlight text"
720
908
  >
721
909
  <Highlighter class="size-3.5" />
@@ -728,10 +916,10 @@
728
916
  variant="ghost"
729
917
  size="sm"
730
918
  class={cn(
731
- 'size-7.5 p-0 rounded-md transition-colors',
919
+ "size-7.5 p-0 rounded-md transition-colors",
732
920
  isActive.bulletList
733
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
734
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
921
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
922
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
735
923
  )}
736
924
  onclick={toggleBulletList}
737
925
  title="Bullet list"
@@ -742,10 +930,10 @@
742
930
  variant="ghost"
743
931
  size="sm"
744
932
  class={cn(
745
- 'size-7.5 p-0 rounded-md transition-colors',
933
+ "size-7.5 p-0 rounded-md transition-colors",
746
934
  isActive.orderedList
747
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
748
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
935
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
936
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
749
937
  )}
750
938
  onclick={toggleOrderedList}
751
939
  title="Numbered list"
@@ -756,10 +944,10 @@
756
944
  variant="ghost"
757
945
  size="sm"
758
946
  class={cn(
759
- 'size-7.5 p-0 rounded-md transition-colors',
947
+ "size-7.5 p-0 rounded-md transition-colors",
760
948
  isActive.blockquote
761
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
762
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
949
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
950
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
763
951
  )}
764
952
  onclick={toggleBlockquote}
765
953
  title="Quote"
@@ -770,10 +958,10 @@
770
958
  variant="ghost"
771
959
  size="sm"
772
960
  class={cn(
773
- 'size-7.5 p-0 rounded-md transition-colors',
961
+ "size-7.5 p-0 rounded-md transition-colors",
774
962
  isActive.codeBlock
775
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
776
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
963
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
964
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
777
965
  )}
778
966
  onclick={toggleCodeBlock}
779
967
  title="Code block"
@@ -789,12 +977,12 @@
789
977
  variant="ghost"
790
978
  size="sm"
791
979
  class={cn(
792
- 'size-7.5 p-0 rounded-md transition-colors',
980
+ "size-7.5 p-0 rounded-md transition-colors",
793
981
  isActive.alignLeft
794
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
795
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
982
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
983
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
796
984
  )}
797
- onclick={() => setAlign('left')}
985
+ onclick={() => setAlign("left")}
798
986
  title="Align left"
799
987
  >
800
988
  <AlignLeft class="size-3.5" />
@@ -803,12 +991,12 @@
803
991
  variant="ghost"
804
992
  size="sm"
805
993
  class={cn(
806
- 'size-7.5 p-0 rounded-md transition-colors',
994
+ "size-7.5 p-0 rounded-md transition-colors",
807
995
  isActive.alignCenter
808
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
809
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
996
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
997
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
810
998
  )}
811
- onclick={() => setAlign('center')}
999
+ onclick={() => setAlign("center")}
812
1000
  title="Align center"
813
1001
  >
814
1002
  <AlignCenter class="size-3.5" />
@@ -817,12 +1005,12 @@
817
1005
  variant="ghost"
818
1006
  size="sm"
819
1007
  class={cn(
820
- 'size-7.5 p-0 rounded-md transition-colors',
1008
+ "size-7.5 p-0 rounded-md transition-colors",
821
1009
  isActive.alignRight
822
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
823
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
1010
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
1011
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
824
1012
  )}
825
- onclick={() => setAlign('right')}
1013
+ onclick={() => setAlign("right")}
826
1014
  title="Align right"
827
1015
  >
828
1016
  <AlignRight class="size-3.5" />
@@ -831,12 +1019,12 @@
831
1019
  variant="ghost"
832
1020
  size="sm"
833
1021
  class={cn(
834
- 'size-7.5 p-0 rounded-md transition-colors',
1022
+ "size-7.5 p-0 rounded-md transition-colors",
835
1023
  isActive.alignJustify
836
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
837
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
1024
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
1025
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
838
1026
  )}
839
- onclick={() => setAlign('justify')}
1027
+ onclick={() => setAlign("justify")}
840
1028
  title="Align justify"
841
1029
  >
842
1030
  <AlignJustify class="size-3.5" />
@@ -850,10 +1038,10 @@
850
1038
  variant="ghost"
851
1039
  size="sm"
852
1040
  class={cn(
853
- 'size-7.5 p-0 rounded-md transition-colors',
1041
+ "size-7.5 p-0 rounded-md transition-colors",
854
1042
  isActive.link
855
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
856
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
1043
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
1044
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
857
1045
  )}
858
1046
  onclick={setLink}
859
1047
  title="Insert link"
@@ -898,17 +1086,17 @@
898
1086
  </Button>
899
1087
  </div>
900
1088
  {#if uploadingCount > 0}
901
- <span class="ml-auto text-xs text-[var(--ui-muted-foreground)] font-medium">
902
- Uploading {uploadingCount} image{uploadingCount > 1 ? 's' : ''}...
1089
+ <span
1090
+ class="ml-auto text-xs text-[var(--ui-muted-foreground)] font-medium"
1091
+ >
1092
+ Uploading {uploadingCount} image{uploadingCount > 1 ? "s" : ""}...
903
1093
  </span>
904
1094
  {/if}
905
1095
  </div>
906
1096
  {/if}
907
-
908
- <!-- Editor -->
909
1097
  <div
910
1098
  bind:this={editorEl}
911
- class="prose prose-sm max-w-none p-4 focus:outline-none"
1099
+ class="rich-text-content prose prose-sm max-w-none p-5 sm:p-7 focus:outline-none"
912
1100
  style="min-height: {height}px;"
913
1101
  ></div>
914
1102
 
@@ -920,7 +1108,9 @@
920
1108
  class="absolute z-50 w-64 max-h-72 overflow-y-auto rounded-xl border border-[var(--ui-border)] bg-[var(--ui-popover)] p-1.5 shadow-2xl text-[var(--ui-popover-foreground)] animate-in fade-in-50 zoom-in-95"
921
1109
  style="top: {slashMenuPos.top}px; left: {slashMenuPos.left}px;"
922
1110
  >
923
- <div class="px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-[var(--ui-muted-foreground)]">
1111
+ <div
1112
+ class="px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-[var(--ui-muted-foreground)]"
1113
+ >
924
1114
  Basic blocks
925
1115
  </div>
926
1116
  {#each slashFilteredCommands as cmd, i (cmd.title)}
@@ -928,18 +1118,26 @@
928
1118
  <button
929
1119
  type="button"
930
1120
  class={cn(
931
- 'flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left transition-colors cursor-pointer',
932
- i === slashIndex ? 'bg-[var(--ui-accent)] text-[var(--ui-accent-foreground)]' : 'hover:bg-[var(--ui-secondary)]'
1121
+ "flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left transition-colors cursor-pointer",
1122
+ i === slashIndex
1123
+ ? "bg-[var(--ui-accent)] text-[var(--ui-accent-foreground)]"
1124
+ : "hover:bg-[var(--ui-secondary)]",
933
1125
  )}
934
1126
  onmouseenter={() => (slashIndex = i)}
935
1127
  onclick={() => executeSlashCommand(cmd)}
936
1128
  >
937
- <div class="flex size-7 items-center justify-center rounded-md border border-[var(--ui-border)] bg-[var(--ui-card)] shrink-0">
1129
+ <div
1130
+ class="flex size-7 items-center justify-center rounded-md border border-[var(--ui-border)] bg-[var(--ui-card)] shrink-0"
1131
+ >
938
1132
  <Icon class="size-4 text-[var(--ui-foreground)]" />
939
1133
  </div>
940
1134
  <div class="flex flex-col min-w-0">
941
- <span class="text-xs font-semibold text-[var(--ui-foreground)]">{cmd.title}</span>
942
- <span class="text-[10px] text-[var(--ui-muted-foreground)] truncate">{cmd.desc}</span>
1135
+ <span class="text-xs font-semibold text-[var(--ui-foreground)]"
1136
+ >{cmd.title}</span
1137
+ >
1138
+ <span class="text-[10px] text-[var(--ui-muted-foreground)] truncate"
1139
+ >{cmd.desc}</span
1140
+ >
943
1141
  </div>
944
1142
  </button>
945
1143
  {/each}
@@ -956,10 +1154,10 @@
956
1154
  variant="ghost"
957
1155
  size="sm"
958
1156
  class={cn(
959
- 'size-7 p-0 rounded-md transition-colors',
960
- editor.isActive('bold')
961
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
962
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
1157
+ "size-7 p-0 rounded-md transition-colors",
1158
+ editor.isActive("bold")
1159
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
1160
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
963
1161
  )}
964
1162
  onclick={toggleBold}
965
1163
  >
@@ -969,10 +1167,10 @@
969
1167
  variant="ghost"
970
1168
  size="sm"
971
1169
  class={cn(
972
- 'size-7 p-0 rounded-md transition-colors',
973
- editor.isActive('italic')
974
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
975
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
1170
+ "size-7 p-0 rounded-md transition-colors",
1171
+ editor.isActive("italic")
1172
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
1173
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
976
1174
  )}
977
1175
  onclick={toggleItalic}
978
1176
  >
@@ -982,10 +1180,10 @@
982
1180
  variant="ghost"
983
1181
  size="sm"
984
1182
  class={cn(
985
- 'size-7 p-0 rounded-md transition-colors',
986
- editor.isActive('underline')
987
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
988
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
1183
+ "size-7 p-0 rounded-md transition-colors",
1184
+ editor.isActive("underline")
1185
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
1186
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
989
1187
  )}
990
1188
  onclick={toggleUnderline}
991
1189
  >
@@ -995,10 +1193,10 @@
995
1193
  variant="ghost"
996
1194
  size="sm"
997
1195
  class={cn(
998
- 'size-7 p-0 rounded-md transition-colors',
999
- editor.isActive('strike')
1000
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
1001
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
1196
+ "size-7 p-0 rounded-md transition-colors",
1197
+ editor.isActive("strike")
1198
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
1199
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
1002
1200
  )}
1003
1201
  onclick={toggleStrike}
1004
1202
  >
@@ -1008,12 +1206,12 @@
1008
1206
  variant="ghost"
1009
1207
  size="sm"
1010
1208
  class={cn(
1011
- 'size-7 p-0 rounded-md transition-colors',
1012
- editor.isActive('highlight')
1013
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
1014
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
1209
+ "size-7 p-0 rounded-md transition-colors",
1210
+ editor.isActive("highlight")
1211
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
1212
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
1015
1213
  )}
1016
- onclick={() => toggleHighlight('#fef08a')}
1214
+ onclick={() => toggleHighlight("#fef08a")}
1017
1215
  >
1018
1216
  <Highlighter class="size-3.5" />
1019
1217
  </Button>
@@ -1021,10 +1219,10 @@
1021
1219
  variant="ghost"
1022
1220
  size="sm"
1023
1221
  class={cn(
1024
- 'size-7 p-0 rounded-md transition-colors',
1025
- editor.isActive('link')
1026
- ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs'
1027
- : 'text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
1222
+ "size-7 p-0 rounded-md transition-colors",
1223
+ editor.isActive("link")
1224
+ ? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
1225
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
1028
1226
  )}
1029
1227
  onclick={setLink}
1030
1228
  >
@@ -1179,7 +1377,7 @@
1179
1377
  background: color-mix(in srgb, var(--ui-primary) 10%, transparent);
1180
1378
  }
1181
1379
  :global(.tiptap .selectedCell::after) {
1182
- content: '';
1380
+ content: "";
1183
1381
  position: absolute;
1184
1382
  inset: 0;
1185
1383
  background: rgba(0, 0, 0, 0.05);
@@ -1220,4 +1418,180 @@
1220
1418
  padding: 0.1em 0.2em;
1221
1419
  border-radius: 2px;
1222
1420
  }
1421
+
1422
+ /* Rich-text surface contract: survives host Tailwind scanning boundaries. */
1423
+ :global(.rich-text-editor) {
1424
+ --rich-text-content-width: 72ch;
1425
+ color: var(--ui-foreground);
1426
+ }
1427
+ :global(.rich-text-toolbar) {
1428
+ scrollbar-width: thin;
1429
+ scrollbar-color: var(--ui-border) transparent;
1430
+ }
1431
+ :global(.rich-text-toolbar > div) {
1432
+ flex-shrink: 0;
1433
+ }
1434
+ :global(.rich-text-content) {
1435
+ overflow-y: auto;
1436
+ overscroll-behavior: contain;
1437
+ background: var(--ui-card);
1438
+ }
1439
+ :global(.rich-text-content .tiptap) {
1440
+ max-width: var(--rich-text-content-width);
1441
+ margin: 0 auto;
1442
+ }
1443
+ :global(.rich-text-content .tiptap ::selection) {
1444
+ background: color-mix(in oklch, var(--ui-primary) 24%, transparent);
1445
+ }
1446
+ :global(.rich-text-content .tiptap p.is-editor-empty:first-child::before) {
1447
+ color: color-mix(in oklch, var(--ui-muted-foreground) 80%, transparent);
1448
+ }
1449
+ :global(.rich-text-content .tiptap:focus) {
1450
+ outline: none;
1451
+ }
1452
+
1453
+ /* Polished document surface */
1454
+ :global(.rich-text-content .tiptap) {
1455
+ color: var(--ui-foreground);
1456
+ caret-color: var(--ui-primary);
1457
+ line-height: 1.75;
1458
+ font-size: 0.9375rem;
1459
+ }
1460
+ :global(.rich-text-content .tiptap > :first-child) {
1461
+ margin-top: 0;
1462
+ }
1463
+ :global(.rich-text-content .tiptap > :last-child) {
1464
+ margin-bottom: 0;
1465
+ }
1466
+ :global(.tiptap p) {
1467
+ margin: 0.65rem 0;
1468
+ }
1469
+ :global(.tiptap h1),
1470
+ :global(.tiptap h2),
1471
+ :global(.tiptap h3) {
1472
+ color: var(--ui-foreground);
1473
+ letter-spacing: -0.025em;
1474
+ line-height: 1.25;
1475
+ }
1476
+ :global(.tiptap h1) {
1477
+ font-size: clamp(1.5rem, 3vw, 2rem);
1478
+ margin: 1.75rem 0 0.75rem;
1479
+ }
1480
+ :global(.tiptap h2) {
1481
+ font-size: clamp(1.25rem, 2.5vw, 1.5rem);
1482
+ margin: 1.5rem 0 0.65rem;
1483
+ }
1484
+ :global(.tiptap h3) {
1485
+ font-size: 1.125rem;
1486
+ margin: 1.25rem 0 0.5rem;
1487
+ }
1488
+ :global(.tiptap strong) {
1489
+ font-weight: 700;
1490
+ color: var(--ui-foreground);
1491
+ }
1492
+ :global(.tiptap a:not(.image-link)) {
1493
+ color: var(--ui-primary);
1494
+ text-decoration: underline;
1495
+ text-decoration-color: color-mix(
1496
+ in oklch,
1497
+ var(--ui-primary) 35%,
1498
+ transparent
1499
+ );
1500
+ text-underline-offset: 3px;
1501
+ }
1502
+ :global(.tiptap a:not(.image-link):hover) {
1503
+ text-decoration-color: var(--ui-primary);
1504
+ }
1505
+ :global(.tiptap ul),
1506
+ :global(.tiptap ol) {
1507
+ margin: 0.75rem 0;
1508
+ }
1509
+ :global(.tiptap li + li) {
1510
+ margin-top: 0.25rem;
1511
+ }
1512
+ :global(.tiptap blockquote) {
1513
+ background: color-mix(in oklch, var(--ui-primary) 5%, transparent);
1514
+ border-left-width: 3px;
1515
+ border-radius: 0 0.5rem 0.5rem 0;
1516
+ padding: 0.75rem 1rem;
1517
+ margin: 1rem 0;
1518
+ }
1519
+ :global(.tiptap pre) {
1520
+ border: 1px solid var(--ui-border);
1521
+ background: var(--ui-secondary);
1522
+ color: var(--ui-foreground);
1523
+ box-shadow: inset 0 1px 0 color-mix(in oklch, white 5%, transparent);
1524
+ overflow-x: auto;
1525
+ }
1526
+ :global(.tiptap code:not(pre code)) {
1527
+ color: var(--ui-primary);
1528
+ border: 1px solid color-mix(in oklch, var(--ui-primary) 15%, transparent);
1529
+ }
1530
+ :global(.tiptap table) {
1531
+ display: block;
1532
+ overflow-x: auto;
1533
+ border: 1px solid var(--ui-border);
1534
+ border-radius: 0.625rem;
1535
+ background: var(--ui-card);
1536
+ }
1537
+ :global(.tiptap th),
1538
+ :global(.tiptap td) {
1539
+ min-width: 7rem;
1540
+ border-color: var(--ui-border);
1541
+ padding: 0.625rem 0.75rem;
1542
+ }
1543
+ :global(.tiptap th) {
1544
+ color: var(--ui-foreground);
1545
+ background: var(--ui-secondary);
1546
+ font-size: 0.8125rem;
1547
+ }
1548
+ :global(.tiptap td) {
1549
+ color: var(--ui-foreground);
1550
+ }
1551
+ :global(.tiptap .selectedCell) {
1552
+ background: color-mix(in oklch, var(--ui-primary) 12%, transparent);
1553
+ }
1554
+ :global(.tiptap .selectedCell::after) {
1555
+ background: color-mix(in oklch, var(--ui-primary) 10%, transparent);
1556
+ }
1557
+ :global(.tiptap img) {
1558
+ border: 1px solid var(--ui-border);
1559
+ box-shadow: 0 8px 24px
1560
+ color-mix(in oklch, var(--ui-foreground) 10%, transparent);
1561
+ transition: box-shadow 150ms ease-out;
1562
+ }
1563
+ :global(.tiptap img:hover) {
1564
+ box-shadow: 0 12px 30px
1565
+ color-mix(in oklch, var(--ui-foreground) 16%, transparent);
1566
+ }
1567
+ :global(.tiptap .callout-box) {
1568
+ box-shadow: 0 1px 2px
1569
+ color-mix(in oklch, var(--ui-foreground) 6%, transparent);
1570
+ }
1571
+ :global(.tiptap hr) {
1572
+ border-top-color: var(--ui-border);
1573
+ margin: 1.5rem 0;
1574
+ }
1575
+ :global(.tiptap:focus-visible) {
1576
+ outline: none;
1577
+ }
1578
+
1579
+ @media (max-width: 640px) {
1580
+ :global(.rich-text-toolbar) {
1581
+ scrollbar-width: none;
1582
+ }
1583
+ :global(.rich-text-toolbar::-webkit-scrollbar) {
1584
+ display: none;
1585
+ }
1586
+ :global(.rich-text-content .tiptap) {
1587
+ font-size: 0.875rem;
1588
+ }
1589
+ }
1590
+
1591
+ @media (prefers-reduced-motion: reduce) {
1592
+ :global(.rich-text-editor),
1593
+ :global(.tiptap img) {
1594
+ transition: none;
1595
+ }
1596
+ }
1223
1597
  </style>