@llui/markdown-editor 0.2.11 → 0.2.13

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.
@@ -0,0 +1,554 @@
1
+ /**
2
+ * Default theme for @llui/markdown-editor. Headless components target
3
+ * data-scope / data-part attributes; this stylesheet gives a polished default.
4
+ * Override the `--md-*` custom properties to retheme (e.g. for light mode).
5
+ *
6
+ * import '@llui/markdown-editor/styles/editor.css'
7
+ */
8
+ :root {
9
+ --md-bg: #0f1117;
10
+ --md-surface: #181b24;
11
+ --md-surface-2: #1f2330;
12
+ --md-border: #2b3040;
13
+ --md-text: #e6e8ee;
14
+ --md-muted: #9aa3b2;
15
+ --md-primary: #5b8cff;
16
+ --md-primary-soft: #2a3656;
17
+ --md-radius: 8px;
18
+ --md-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;
19
+ }
20
+
21
+ /* ── Editor surface (Lexical's contentEditable host) ──────────────────── */
22
+ [data-lexical-editor] {
23
+ position: relative;
24
+ outline: none;
25
+ padding: 1.1rem 1.25rem;
26
+ min-height: 8rem;
27
+ color: var(--md-text);
28
+ }
29
+ [data-lexical-editor] > * {
30
+ margin: 0 0 0.7rem;
31
+ }
32
+ [data-lexical-editor][data-empty='true']::before {
33
+ content: attr(data-placeholder);
34
+ position: absolute;
35
+ top: 1.1rem;
36
+ left: 1.25rem;
37
+ color: var(--md-muted);
38
+ pointer-events: none;
39
+ }
40
+ [data-lexical-editor] h1 {
41
+ font-size: 1.6rem;
42
+ letter-spacing: -0.02em;
43
+ }
44
+ [data-lexical-editor] h2 {
45
+ font-size: 1.3rem;
46
+ }
47
+ [data-lexical-editor] h3 {
48
+ font-size: 1.1rem;
49
+ }
50
+ [data-lexical-editor] blockquote {
51
+ border-left: 3px solid var(--md-primary);
52
+ margin-inline: 0;
53
+ padding-left: 0.9rem;
54
+ color: var(--md-muted);
55
+ }
56
+ [data-lexical-editor] ul,
57
+ [data-lexical-editor] ol {
58
+ padding-left: 1.4rem;
59
+ }
60
+ [data-lexical-editor] a {
61
+ color: var(--md-primary);
62
+ }
63
+ [data-lexical-editor] code[data-lexical-text='true'] {
64
+ background: var(--md-surface-2);
65
+ padding: 0.1em 0.35em;
66
+ border-radius: 5px;
67
+ font-family: var(--md-mono);
68
+ }
69
+ [data-lexical-editor] code:not([data-lexical-text='true']) {
70
+ display: block;
71
+ white-space: pre;
72
+ background: var(--md-bg);
73
+ border: 1px solid var(--md-border);
74
+ border-radius: 6px;
75
+ padding: 0.7rem 0.9rem;
76
+ font-size: 0.85rem;
77
+ font-family: var(--md-mono);
78
+ overflow: auto;
79
+ }
80
+
81
+ /* Text-decoration formats Lexical renders as a bare <span> (no semantic tag):
82
+ * the runtime tags them with the default theme classes — see src/theme.ts. */
83
+ [data-lexical-editor] .md-strikethrough {
84
+ text-decoration: line-through;
85
+ }
86
+ [data-lexical-editor] .md-underline {
87
+ text-decoration: underline;
88
+ }
89
+ [data-lexical-editor] .md-underline-strikethrough {
90
+ text-decoration: underline line-through;
91
+ }
92
+
93
+ /* Task-list items (li[role="checkbox"]) with a clickable checkbox. */
94
+ [data-lexical-editor] li[role='checkbox'] {
95
+ position: relative;
96
+ list-style: none;
97
+ margin-left: -0.4rem;
98
+ padding-left: 1.5rem;
99
+ outline: none;
100
+ }
101
+ [data-lexical-editor] li[role='checkbox']::before {
102
+ content: '';
103
+ position: absolute;
104
+ left: 0;
105
+ top: 0.25em;
106
+ width: 1rem;
107
+ height: 1rem;
108
+ border: 1px solid var(--md-muted);
109
+ border-radius: 3px;
110
+ cursor: pointer;
111
+ }
112
+ [data-lexical-editor] li[role='checkbox'][aria-checked='true'] {
113
+ color: var(--md-muted);
114
+ text-decoration: line-through;
115
+ }
116
+ [data-lexical-editor] li[role='checkbox'][aria-checked='true']::before {
117
+ background: var(--md-primary);
118
+ border-color: var(--md-primary);
119
+ }
120
+ [data-lexical-editor] li[role='checkbox'][aria-checked='true']::after {
121
+ content: '';
122
+ position: absolute;
123
+ left: 0.34rem;
124
+ top: 0.3em;
125
+ width: 0.28rem;
126
+ height: 0.55rem;
127
+ border: solid #fff;
128
+ border-width: 0 2px 2px 0;
129
+ transform: rotate(45deg);
130
+ cursor: pointer;
131
+ }
132
+
133
+ /* ── Editor shell + toolbar ───────────────────────────────────────────── */
134
+ [data-scope='md-editor'][data-part='root'] {
135
+ display: flex;
136
+ flex-direction: column;
137
+ background: var(--md-surface);
138
+ }
139
+ [data-scope='md-toolbar'][data-part='root'] {
140
+ display: flex;
141
+ flex-wrap: wrap;
142
+ gap: 0.5rem;
143
+ padding: 0.5rem 0.6rem;
144
+ border-bottom: 1px solid var(--md-border);
145
+ background: var(--md-surface-2);
146
+ position: sticky;
147
+ top: 0;
148
+ z-index: 1;
149
+ }
150
+ [data-scope='md-toolbar'][data-part='group'] {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 0.15rem;
154
+ padding-right: 0.5rem;
155
+ border-right: 1px solid var(--md-border);
156
+ }
157
+ [data-scope='md-toolbar'][data-part='group']:last-child {
158
+ border-right: none;
159
+ padding-right: 0;
160
+ }
161
+ [data-scope='md-toolbar'] [data-part='item'] {
162
+ font: inherit;
163
+ font-size: 0.85rem;
164
+ min-width: 2rem;
165
+ height: 2rem;
166
+ padding: 0 0.4rem;
167
+ color: var(--md-text);
168
+ background: transparent;
169
+ border: 1px solid transparent;
170
+ border-radius: 6px;
171
+ cursor: pointer;
172
+ }
173
+ [data-scope='md-toolbar'] [data-part='item']:hover {
174
+ background: var(--md-surface);
175
+ }
176
+ [data-scope='md-toolbar'] [data-part='item'][data-active] {
177
+ background: var(--md-primary-soft);
178
+ color: #fff;
179
+ border-color: var(--md-primary);
180
+ }
181
+ [data-scope='md-toolbar'] [data-part='item']:disabled {
182
+ opacity: 0.4;
183
+ cursor: default;
184
+ }
185
+ [data-scope='md-toolbar'][data-part='presence'] {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 0.35rem;
189
+ margin-left: auto;
190
+ padding-left: 0.5rem;
191
+ font-size: 0.8rem;
192
+ color: var(--md-text-soft, var(--md-text));
193
+ }
194
+ [data-scope='md-toolbar'] [data-part='presence-status'] {
195
+ width: 0.5rem;
196
+ height: 0.5rem;
197
+ border-radius: 50%;
198
+ background: var(--md-border);
199
+ flex: none;
200
+ }
201
+ [data-scope='md-toolbar'] [data-part='presence-status'][data-status='synced'] {
202
+ background: #16a34a;
203
+ }
204
+ [data-scope='md-toolbar'] [data-part='presence-status'][data-status='connecting'] {
205
+ background: #d97706;
206
+ }
207
+ [data-scope='md-toolbar'] [data-part='presence-status'][data-status='offline'] {
208
+ background: #9ca3af;
209
+ }
210
+ [data-scope='md-toolbar'] [data-part='block-select'] {
211
+ font: inherit;
212
+ font-size: 0.85rem;
213
+ height: 2rem;
214
+ padding: 0 0.4rem;
215
+ color: var(--md-text);
216
+ background: var(--md-surface);
217
+ border: 1px solid var(--md-border);
218
+ border-radius: 6px;
219
+ cursor: pointer;
220
+ }
221
+ [data-part='glyph'] {
222
+ display: inline-flex;
223
+ align-items: center;
224
+ font-weight: 600;
225
+ }
226
+ [data-part='glyph'] svg {
227
+ display: block;
228
+ }
229
+
230
+ /* ── Callout decorator ────────────────────────────────────────────────── */
231
+ [data-scope='md-callout'][data-part='root'] {
232
+ display: flex;
233
+ align-items: flex-start;
234
+ gap: 0.7rem;
235
+ padding: 0.7rem 0.9rem;
236
+ border-radius: 8px;
237
+ border: 1px solid var(--md-border);
238
+ border-left-width: 4px;
239
+ background: var(--md-surface-2);
240
+ margin: 0.6rem 0;
241
+ }
242
+ [data-scope='md-callout'] [data-part='badge'] {
243
+ font: inherit;
244
+ font-size: 0.72rem;
245
+ font-weight: 700;
246
+ letter-spacing: 0.04em;
247
+ text-transform: uppercase;
248
+ color: #fff;
249
+ border: none;
250
+ border-radius: 5px;
251
+ padding: 0.2rem 0.5rem;
252
+ cursor: pointer;
253
+ flex: none;
254
+ }
255
+ [data-scope='md-callout'] [data-part='text'] {
256
+ flex: 1;
257
+ padding-top: 0.1rem;
258
+ outline: none;
259
+ border-radius: 4px;
260
+ }
261
+ [data-scope='md-callout'] [data-part='text']:focus {
262
+ box-shadow: 0 0 0 2px var(--md-primary-soft);
263
+ }
264
+ [data-scope='md-callout'] [data-part='text']:empty::before {
265
+ content: 'Callout text…';
266
+ color: var(--md-muted);
267
+ }
268
+ [data-scope='md-callout'][data-kind='note'] {
269
+ border-left-color: #5b8cff;
270
+ }
271
+ [data-scope='md-callout'][data-kind='note'] [data-part='badge'] {
272
+ background: #5b8cff;
273
+ }
274
+ [data-scope='md-callout'][data-kind='tip'] {
275
+ border-left-color: #34d399;
276
+ }
277
+ [data-scope='md-callout'][data-kind='tip'] [data-part='badge'] {
278
+ background: #34d399;
279
+ }
280
+ [data-scope='md-callout'][data-kind='warning'] {
281
+ border-left-color: #fbbf24;
282
+ }
283
+ [data-scope='md-callout'][data-kind='warning'] [data-part='badge'] {
284
+ background: #b8860b;
285
+ }
286
+ [data-scope='md-callout'][data-kind='danger'] {
287
+ border-left-color: #f87171;
288
+ }
289
+ [data-scope='md-callout'][data-kind='danger'] [data-part='badge'] {
290
+ background: #ef4444;
291
+ }
292
+
293
+ /* ── Image + horizontal-rule decorators ───────────────────────────────── */
294
+ [data-scope='md-image'][data-part='root'] {
295
+ margin: 0.6rem 0;
296
+ }
297
+ [data-scope='md-image'] img {
298
+ max-width: 100%;
299
+ height: auto;
300
+ border-radius: 6px;
301
+ border: 1px solid var(--md-border);
302
+ display: block;
303
+ }
304
+ [data-md-hr] {
305
+ border: none;
306
+ border-top: 1px solid var(--md-border);
307
+ margin: 1.2rem 0;
308
+ }
309
+
310
+ /* ── Tables ───────────────────────────────────────────────────────────── */
311
+ [data-lexical-editor] table {
312
+ border-collapse: collapse;
313
+ margin: 0.6rem 0;
314
+ width: 100%;
315
+ font-size: 0.9rem;
316
+ }
317
+ [data-lexical-editor] th,
318
+ [data-lexical-editor] td {
319
+ border: 1px solid var(--md-border);
320
+ padding: 0.4rem 0.6rem;
321
+ text-align: left;
322
+ vertical-align: top;
323
+ min-width: 4rem;
324
+ }
325
+ [data-lexical-editor] th {
326
+ background: var(--md-surface-2);
327
+ font-weight: 600;
328
+ }
329
+ [data-scope='md-table-tools'][data-part='bar'] {
330
+ display: flex;
331
+ gap: 0.1rem;
332
+ padding: 0.2rem;
333
+ background: var(--md-surface);
334
+ border: 1px solid var(--md-border);
335
+ border-radius: 7px;
336
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
337
+ }
338
+ [data-scope='md-table-tools'][data-part='tool'] {
339
+ font: inherit;
340
+ font-size: 0.78rem;
341
+ min-width: 1.9rem;
342
+ height: 1.8rem;
343
+ padding: 0 0.3rem;
344
+ color: var(--md-text);
345
+ background: transparent;
346
+ border: 1px solid transparent;
347
+ border-radius: 5px;
348
+ cursor: pointer;
349
+ }
350
+ [data-scope='md-table-tools'][data-part='tool']:hover {
351
+ background: var(--md-surface-2);
352
+ }
353
+
354
+ /* ── Math block ───────────────────────────────────────────────────────── */
355
+ [data-scope='md-math'][data-part='root'] {
356
+ display: flex;
357
+ flex-direction: column;
358
+ gap: 0.4rem;
359
+ margin: 0.6rem 0;
360
+ padding: 0.7rem 0.9rem;
361
+ border: 1px solid var(--md-border);
362
+ border-radius: 8px;
363
+ background: var(--md-surface-2);
364
+ }
365
+ [data-scope='md-math'] [data-part='source'] {
366
+ font-family: var(--md-mono);
367
+ font-size: 0.85rem;
368
+ color: var(--md-muted);
369
+ outline: none;
370
+ }
371
+ [data-scope='md-math'] [data-part='source']:focus {
372
+ color: var(--md-text);
373
+ }
374
+ [data-scope='md-math'] [data-part='preview'] {
375
+ text-align: center;
376
+ }
377
+
378
+ /* ── Mermaid block ────────────────────────────────────────────────────── */
379
+ [data-scope='md-mermaid'][data-part='root'] {
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: 0.4rem;
383
+ margin: 0.6rem 0;
384
+ padding: 0.7rem 0.9rem;
385
+ border: 1px solid var(--md-border);
386
+ border-radius: 8px;
387
+ background: var(--md-surface-2);
388
+ }
389
+ [data-scope='md-mermaid'] [data-part='source'] {
390
+ font-family: var(--md-mono);
391
+ font-size: 0.82rem;
392
+ white-space: pre-wrap;
393
+ color: var(--md-muted);
394
+ outline: none;
395
+ }
396
+ [data-scope='md-mermaid'] [data-part='source']:focus {
397
+ color: var(--md-text);
398
+ }
399
+ [data-md-image='alt'] {
400
+ margin-top: 0.5rem;
401
+ }
402
+ [data-md-image='file'] {
403
+ margin-top: 0.6rem;
404
+ font-size: 0.8rem;
405
+ color: var(--md-muted);
406
+ }
407
+
408
+ /* ── Slash command menu ───────────────────────────────────────────────── */
409
+ [data-scope='md-slash'][data-part='root'] {
410
+ min-width: 12rem;
411
+ max-height: 16rem;
412
+ overflow: auto;
413
+ padding: 0.25rem;
414
+ background: var(--md-surface);
415
+ border: 1px solid var(--md-border);
416
+ border-radius: 8px;
417
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
418
+ }
419
+ [data-scope='md-slash'][data-part='option'] {
420
+ padding: 0.4rem 0.6rem;
421
+ border-radius: 5px;
422
+ font-size: 0.88rem;
423
+ color: var(--md-text);
424
+ cursor: pointer;
425
+ }
426
+ [data-scope='md-slash'][data-part='option'][data-active] {
427
+ background: var(--md-primary-soft);
428
+ }
429
+
430
+ /* ── Context menu ─────────────────────────────────────────────────────── */
431
+ [data-scope='md-context'][data-part='backdrop'] {
432
+ position: fixed;
433
+ inset: 0;
434
+ z-index: 60;
435
+ }
436
+ [data-scope='md-context'][data-part='root'] {
437
+ min-width: 11rem;
438
+ max-height: 18rem;
439
+ overflow: auto;
440
+ padding: 0.25rem;
441
+ background: var(--md-surface);
442
+ border: 1px solid var(--md-border);
443
+ border-radius: 8px;
444
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
445
+ }
446
+ [data-scope='md-context'][data-part='option'] {
447
+ padding: 0.35rem 0.6rem;
448
+ border-radius: 5px;
449
+ font-size: 0.85rem;
450
+ color: var(--md-text);
451
+ cursor: pointer;
452
+ }
453
+ [data-scope='md-context'][data-part='option']:hover {
454
+ background: var(--md-primary-soft);
455
+ }
456
+
457
+ /* ── Floating selection toolbar ───────────────────────────────────────── */
458
+ [data-scope='md-floating'][data-part='bar'] {
459
+ display: flex;
460
+ gap: 0.1rem;
461
+ padding: 0.2rem;
462
+ background: var(--md-surface);
463
+ border: 1px solid var(--md-border);
464
+ border-radius: 7px;
465
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
466
+ }
467
+ [data-scope='md-floating'][data-part='item'] {
468
+ font: inherit;
469
+ font-size: 0.85rem;
470
+ min-width: 1.8rem;
471
+ height: 1.8rem;
472
+ padding: 0 0.35rem;
473
+ color: var(--md-text);
474
+ background: transparent;
475
+ border: 1px solid transparent;
476
+ border-radius: 5px;
477
+ cursor: pointer;
478
+ }
479
+ [data-scope='md-floating'][data-part='item']:hover {
480
+ background: var(--md-surface-2);
481
+ }
482
+ [data-scope='md-floating'][data-part='item'][data-active] {
483
+ background: var(--md-primary-soft);
484
+ color: #fff;
485
+ }
486
+
487
+ /* ── Link dialog ──────────────────────────────────────────────────────── */
488
+ [data-scope='dialog'][data-part='backdrop'] {
489
+ position: fixed;
490
+ inset: 0;
491
+ background: rgba(8, 10, 16, 0.6);
492
+ z-index: 50;
493
+ }
494
+ [data-scope='dialog'][data-part='positioner'] {
495
+ position: fixed;
496
+ inset: 0;
497
+ display: grid;
498
+ place-items: center;
499
+ padding: 1rem;
500
+ z-index: 51;
501
+ }
502
+ [data-md-link='box'] {
503
+ width: min(28rem, 100%);
504
+ background: var(--md-surface);
505
+ border: 1px solid var(--md-border);
506
+ border-radius: 10px;
507
+ padding: 1.1rem 1.2rem;
508
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55);
509
+ color: var(--md-text);
510
+ }
511
+ [data-md-link='title'] {
512
+ font-size: 1rem;
513
+ font-weight: 600;
514
+ margin-bottom: 0.7rem;
515
+ }
516
+ [data-md-link='input'] {
517
+ width: 100%;
518
+ box-sizing: border-box;
519
+ font: inherit;
520
+ font-size: 0.9rem;
521
+ background: var(--md-bg);
522
+ color: var(--md-text);
523
+ border: 1px solid var(--md-border);
524
+ border-radius: 6px;
525
+ padding: 0.5rem 0.6rem;
526
+ outline: none;
527
+ }
528
+ [data-md-link='input']:focus {
529
+ border-color: var(--md-primary);
530
+ }
531
+ [data-md-link='actions'] {
532
+ display: flex;
533
+ justify-content: flex-end;
534
+ gap: 0.5rem;
535
+ margin-top: 1rem;
536
+ }
537
+ [data-md-link='cancel'],
538
+ [data-md-link='apply'] {
539
+ font: inherit;
540
+ font-size: 0.85rem;
541
+ border-radius: 6px;
542
+ padding: 0.4rem 0.85rem;
543
+ cursor: pointer;
544
+ border: 1px solid var(--md-border);
545
+ }
546
+ [data-md-link='cancel'] {
547
+ background: var(--md-surface);
548
+ color: var(--md-text);
549
+ }
550
+ [data-md-link='apply'] {
551
+ background: var(--md-primary);
552
+ color: #fff;
553
+ border-color: var(--md-primary);
554
+ }
@@ -0,0 +1,67 @@
1
+ // The link editor — a modal dialog (built on the @llui/components `dialog`) for
2
+ // entering/editing a URL. Pure view: it's driven by `{ open }` + `url` signals
3
+ // and reports changes through callbacks, so any owner (the link plugin) can wire
4
+ // it to its own messages.
5
+
6
+ import { button, div, input, text, type Mountable, type Signal } from '@llui/dom'
7
+ import {
8
+ connect as connectDialog,
9
+ overlay as overlayDialog,
10
+ type DialogMsg,
11
+ type DialogState,
12
+ } from '@llui/components/dialog'
13
+
14
+ export interface LinkDialogOptions {
15
+ /** The `{ open }` slice driving the modal. */
16
+ dialog: Signal<DialogState>
17
+ /** The URL input value. */
18
+ url: Signal<string>
19
+ /** Called as the user edits the URL. */
20
+ onInput: (url: string) => void
21
+ /** Called on Apply / Enter. */
22
+ onSubmit: () => void
23
+ /** Called when the dialog requests open/close (dismiss, close button). */
24
+ onDialog: (msg: DialogMsg) => void
25
+ /** Dialog instance id for ARIA wiring (default 'md-link-dialog'). */
26
+ id?: string
27
+ }
28
+
29
+ /** Render the link dialog. Hidden (portal, nothing inline) until `dialog.open`. */
30
+ export function linkDialog(opts: LinkDialogOptions): Mountable {
31
+ const parts = connectDialog(opts.dialog, opts.onDialog, {
32
+ id: opts.id ?? 'md-link-dialog',
33
+ closeLabel: 'Cancel',
34
+ })
35
+
36
+ return overlayDialog({
37
+ state: opts.dialog,
38
+ send: opts.onDialog,
39
+ parts,
40
+ closeOnEscape: true,
41
+ closeOnOutsideClick: true,
42
+ content: () => [
43
+ div({ ...parts.content, 'data-md-link': 'box' }, [
44
+ div({ ...parts.title, 'data-md-link': 'title' }, [text('Insert link')]),
45
+ input({
46
+ 'data-md-link': 'input',
47
+ type: 'url',
48
+ placeholder: 'https://example.com',
49
+ value: opts.url,
50
+ onInput: (e: Event) => opts.onInput((e.target as HTMLInputElement).value),
51
+ onKeyDown: (e: KeyboardEvent) => {
52
+ if (e.key === 'Enter') {
53
+ e.preventDefault()
54
+ opts.onSubmit()
55
+ }
56
+ },
57
+ }),
58
+ div({ 'data-md-link': 'actions' }, [
59
+ button({ ...parts.closeTrigger, 'data-md-link': 'cancel' }, [text('Cancel')]),
60
+ button({ type: 'button', 'data-md-link': 'apply', onClick: () => opts.onSubmit() }, [
61
+ text('Apply'),
62
+ ]),
63
+ ]),
64
+ ]),
65
+ ],
66
+ })
67
+ }