@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>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/studio",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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",
|