@lobb-js/studio 0.3.3 → 0.4.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.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import CodeEditor from "../codeEditor.svelte";
3
+ import RichTextEditor from "../richTextEditor.svelte";
3
4
 
4
5
  interface Props {
5
6
  value: any;
@@ -20,4 +21,6 @@
20
21
 
21
22
  {#if type === "code"}
22
23
  <CodeEditor name={field.key} {...args} bind:value />
24
+ {:else if type === "richtext"}
25
+ <RichTextEditor name={field.key} bind:value />
23
26
  {/if}
@@ -0,0 +1,284 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { Editor } from '@tiptap/core';
4
+ import StarterKit from '@tiptap/starter-kit';
5
+ import UnderlineExt from '@tiptap/extension-underline';
6
+ import LinkExt from '@tiptap/extension-link';
7
+ import {
8
+ Bold,
9
+ Italic,
10
+ Underline,
11
+ Strikethrough,
12
+ Heading1,
13
+ Heading2,
14
+ Heading3,
15
+ List,
16
+ ListOrdered,
17
+ Quote,
18
+ Minus,
19
+ Undo2,
20
+ Redo2,
21
+ Link
22
+ } from 'lucide-svelte';
23
+ import { cn } from '../utils.js';
24
+
25
+ interface Props {
26
+ value?: string;
27
+ name?: string;
28
+ }
29
+
30
+ let { value = $bindable(''), name }: Props = $props();
31
+
32
+ let editorElement = $state<HTMLDivElement | undefined>(undefined);
33
+ let editor = $state<Editor | null>(null);
34
+
35
+ // Individual states for toolbar buttons.
36
+ // Updated in onUpdate/onSelectionUpdate — these callbacks never fire during
37
+ // the Editor constructor, so no $state writes happen mid-construction and
38
+ // the onMount effect cannot be re-queued by Svelte's scheduler.
39
+ let isBold = $state(false);
40
+ let isItalic = $state(false);
41
+ let isUnderline = $state(false);
42
+ let isStrike = $state(false);
43
+ let isH1 = $state(false);
44
+ let isH2 = $state(false);
45
+ let isH3 = $state(false);
46
+ let isBulletList = $state(false);
47
+ let isOrderedList = $state(false);
48
+ let isBlockquote = $state(false);
49
+ let isLink = $state(false);
50
+ let canUndo = $state(false);
51
+ let canRedo = $state(false);
52
+
53
+ function syncButtonStates(e: Editor) {
54
+ isBold = e.isActive('bold');
55
+ isItalic = e.isActive('italic');
56
+ isUnderline = e.isActive('underline');
57
+ isStrike = e.isActive('strike');
58
+ isH1 = e.isActive('heading', { level: 1 });
59
+ isH2 = e.isActive('heading', { level: 2 });
60
+ isH3 = e.isActive('heading', { level: 3 });
61
+ isBulletList = e.isActive('bulletList');
62
+ isOrderedList = e.isActive('orderedList');
63
+ isBlockquote = e.isActive('blockquote');
64
+ isLink = e.isActive('link');
65
+ canUndo = e.can().undo();
66
+ canRedo = e.can().redo();
67
+ }
68
+
69
+ onMount(() => {
70
+ const e = new Editor({
71
+ element: editorElement,
72
+ extensions: [
73
+ StarterKit.configure({
74
+ heading: { levels: [1, 2, 3] },
75
+ }),
76
+ UnderlineExt,
77
+ LinkExt.configure({ openOnClick: false }),
78
+ ],
79
+ content: value,
80
+ editorProps: {
81
+ attributes: { dir: 'auto' },
82
+ },
83
+ onUpdate: ({ editor: e }) => {
84
+ value = e.getHTML();
85
+ syncButtonStates(e);
86
+ },
87
+ onSelectionUpdate: ({ editor: e }) => {
88
+ syncButtonStates(e);
89
+ },
90
+ });
91
+ editor = e;
92
+ });
93
+
94
+ onDestroy(() => {
95
+ editor?.destroy();
96
+ });
97
+
98
+ // Syncs an externally-changed `value` prop into the editor.
99
+ // Treats TipTap's empty-doc representation ('<p></p>') as equivalent to ''
100
+ // so the effect doesn't endlessly call setContent on an empty editor.
101
+ $effect(() => {
102
+ if (!editor) return;
103
+ const incoming = value;
104
+ const current = editor.getHTML();
105
+ const normalizedCurrent = current === '<p></p>' ? '' : current;
106
+ if (normalizedCurrent !== incoming) {
107
+ editor.commands.setContent(incoming);
108
+ syncButtonStates(editor);
109
+ }
110
+ });
111
+
112
+ function handleLink() {
113
+ if (!editor) return;
114
+ if (editor.isActive('link')) {
115
+ editor.chain().focus().unsetLink().run();
116
+ } else {
117
+ const url = window.prompt('URL');
118
+ if (url) editor.chain().focus().setLink({ href: url }).run();
119
+ }
120
+ }
121
+ </script>
122
+
123
+ <div class="flex flex-col rounded-md border bg-muted/30">
124
+ <div class="flex flex-wrap items-center gap-0.5 border-b p-1.5">
125
+ {#if editor}
126
+ <button
127
+ type="button"
128
+ onclick={() => editor?.chain().focus().toggleBold().run()}
129
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isBold ? 'bg-muted text-foreground' : 'text-muted-foreground')}
130
+ ><Bold size={14} /></button>
131
+
132
+ <button
133
+ type="button"
134
+ onclick={() => editor?.chain().focus().toggleItalic().run()}
135
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isItalic ? 'bg-muted text-foreground' : 'text-muted-foreground')}
136
+ ><Italic size={14} /></button>
137
+
138
+ <button
139
+ type="button"
140
+ onclick={() => editor?.chain().focus().toggleUnderline().run()}
141
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isUnderline ? 'bg-muted text-foreground' : 'text-muted-foreground')}
142
+ ><Underline size={14} /></button>
143
+
144
+ <button
145
+ type="button"
146
+ onclick={() => editor?.chain().focus().toggleStrike().run()}
147
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isStrike ? 'bg-muted text-foreground' : 'text-muted-foreground')}
148
+ ><Strikethrough size={14} /></button>
149
+
150
+ <div class="mx-0.5 self-stretch w-px bg-border"></div>
151
+
152
+ <button
153
+ type="button"
154
+ onclick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}
155
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isH1 ? 'bg-muted text-foreground' : 'text-muted-foreground')}
156
+ ><Heading1 size={14} /></button>
157
+
158
+ <button
159
+ type="button"
160
+ onclick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
161
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isH2 ? 'bg-muted text-foreground' : 'text-muted-foreground')}
162
+ ><Heading2 size={14} /></button>
163
+
164
+ <button
165
+ type="button"
166
+ onclick={() => editor?.chain().focus().toggleHeading({ level: 3 }).run()}
167
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isH3 ? 'bg-muted text-foreground' : 'text-muted-foreground')}
168
+ ><Heading3 size={14} /></button>
169
+
170
+ <div class="mx-0.5 self-stretch w-px bg-border"></div>
171
+
172
+ <button
173
+ type="button"
174
+ onclick={() => editor?.chain().focus().toggleBulletList().run()}
175
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isBulletList ? 'bg-muted text-foreground' : 'text-muted-foreground')}
176
+ ><List size={14} /></button>
177
+
178
+ <button
179
+ type="button"
180
+ onclick={() => editor?.chain().focus().toggleOrderedList().run()}
181
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isOrderedList ? 'bg-muted text-foreground' : 'text-muted-foreground')}
182
+ ><ListOrdered size={14} /></button>
183
+
184
+ <button
185
+ type="button"
186
+ onclick={() => editor?.chain().focus().toggleBlockquote().run()}
187
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isBlockquote ? 'bg-muted text-foreground' : 'text-muted-foreground')}
188
+ ><Quote size={14} /></button>
189
+
190
+ <button
191
+ type="button"
192
+ onclick={() => editor?.chain().focus().setHorizontalRule().run()}
193
+ class="rounded p-1.5 text-muted-foreground transition-colors hover:bg-muted"
194
+ ><Minus size={14} /></button>
195
+
196
+ <button
197
+ type="button"
198
+ onclick={handleLink}
199
+ class={cn('rounded p-1.5 transition-colors hover:bg-muted', isLink ? 'bg-muted text-foreground' : 'text-muted-foreground')}
200
+ ><Link size={14} /></button>
201
+
202
+ <div class="mx-0.5 self-stretch w-px bg-border"></div>
203
+
204
+ <button
205
+ type="button"
206
+ onclick={() => editor?.chain().focus().undo().run()}
207
+ disabled={!canUndo}
208
+ class="rounded p-1.5 text-muted-foreground transition-colors hover:bg-muted disabled:opacity-40"
209
+ ><Undo2 size={14} /></button>
210
+
211
+ <button
212
+ type="button"
213
+ onclick={() => editor?.chain().focus().redo().run()}
214
+ disabled={!canRedo}
215
+ class="rounded p-1.5 text-muted-foreground transition-colors hover:bg-muted disabled:opacity-40"
216
+ ><Redo2 size={14} /></button>
217
+ {/if}
218
+ </div>
219
+
220
+ <div bind:this={editorElement} class="rte-content min-h-48 p-3 text-sm"></div>
221
+ </div>
222
+
223
+ <style>
224
+ .rte-content :global(.ProseMirror) {
225
+ outline: none;
226
+ min-height: 180px;
227
+ }
228
+ .rte-content :global(.ProseMirror > * + *) {
229
+ margin-top: 0.4em;
230
+ }
231
+ .rte-content :global(.ProseMirror h1) {
232
+ font-size: 1.5rem;
233
+ font-weight: 700;
234
+ }
235
+ .rte-content :global(.ProseMirror h2) {
236
+ font-size: 1.25rem;
237
+ font-weight: 600;
238
+ }
239
+ .rte-content :global(.ProseMirror h3) {
240
+ font-size: 1.1rem;
241
+ font-weight: 600;
242
+ }
243
+ .rte-content :global(.ProseMirror ul) {
244
+ list-style-type: disc;
245
+ padding-left: 1.5rem;
246
+ }
247
+ .rte-content :global(.ProseMirror ol) {
248
+ list-style-type: decimal;
249
+ padding-left: 1.5rem;
250
+ }
251
+ .rte-content :global(.ProseMirror blockquote) {
252
+ border-left: 3px solid var(--border);
253
+ padding-left: 1rem;
254
+ color: var(--muted-foreground);
255
+ font-style: italic;
256
+ }
257
+ .rte-content :global(.ProseMirror hr) {
258
+ border: none;
259
+ border-top: 1px solid var(--border);
260
+ margin: 0.75rem 0;
261
+ }
262
+ .rte-content :global(.ProseMirror a) {
263
+ color: var(--primary);
264
+ text-decoration: underline;
265
+ cursor: pointer;
266
+ }
267
+ .rte-content :global(.ProseMirror code) {
268
+ background-color: var(--muted);
269
+ padding: 0.1em 0.3em;
270
+ border-radius: 3px;
271
+ font-size: 0.875em;
272
+ font-family: monospace;
273
+ }
274
+ .rte-content :global(.ProseMirror pre) {
275
+ background-color: var(--muted);
276
+ padding: 0.75rem 1rem;
277
+ border-radius: 6px;
278
+ overflow-x: auto;
279
+ }
280
+ .rte-content :global(.ProseMirror pre code) {
281
+ background: none;
282
+ padding: 0;
283
+ }
284
+ </style>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ value?: string;
3
+ name?: string;
4
+ }
5
+ declare const RichTextEditor: import("svelte").Component<Props, {}, "value">;
6
+ type RichTextEditor = ReturnType<typeof RichTextEditor>;
7
+ export default RichTextEditor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobb-js/studio",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -67,6 +67,11 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@andrewbranch/untar.js": "^1.0.3",
70
+ "@tiptap/core": "^3.0.0",
71
+ "@tiptap/extension-link": "^3.0.0",
72
+ "@tiptap/extension-underline": "^3.0.0",
73
+ "@tiptap/pm": "^3.0.0",
74
+ "@tiptap/starter-kit": "^3.0.0",
70
75
  "@codemirror/lang-javascript": "^6.2.4",
71
76
  "@codemirror/lang-sql": "^6.10.0",
72
77
  "@codemirror/merge": "^6.11.2",
@@ -74,7 +79,7 @@
74
79
  "@codemirror/theme-one-dark": "^6.1.3",
75
80
  "@codemirror/view": "^6.39.12",
76
81
  "@dagrejs/dagre": "^1.1.5",
77
- "@lobb-js/sdk": "0.1.2",
82
+ "@lobb-js/sdk": "0.1.3",
78
83
  "@lucide/svelte": "^0.563.1",
79
84
  "@tailwindcss/vite": "^4.1.18",
80
85
  "@wjfe/n-savant": "^0.3.0",