@atom-forge/brickee 0.0.1 → 0.0.2
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.
- package/CHANGELOG.md +19 -1
- package/README-AI.md +24 -21
- package/dist/Editor.svelte +52 -64
- package/dist/Editor.svelte.d.ts +0 -3
- package/dist/LinkPrompt.svelte +3 -3
- package/dist/Preview.svelte +1 -5
- package/dist/Preview.svelte.d.ts +0 -3
- package/dist/PropsBar.svelte +120 -110
- package/dist/PropsBar.svelte.d.ts +0 -3
- package/dist/code.d.ts +12 -0
- package/dist/code.js +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/kit/Empty.svelte +2 -2
- package/dist/kit/Frame.svelte +3 -3
- package/dist/locales/hu.js +2 -7
- package/dist/markdown.d.ts +0 -2
- package/dist/markdown.js +1 -1
- package/dist/plugins/Gap.svelte +1 -1
- package/dist/plugins/Map.svelte +1 -1
- package/dist/plugins/Table.svelte +36 -62
- package/dist/plugins/TablePreview.svelte +2 -2
- package/dist/plugins/YouTubePreview.svelte +1 -1
- package/dist/plugins/index.js +12 -1
- package/dist/schema.js +1 -1
- package/dist/types.d.ts +6 -7
- package/docs/en/bundled-plugins.md +1 -1
- package/docs/en/documents-and-markdown.md +4 -5
- package/docs/en/editor-and-preview.md +1 -3
- package/docs/en/getting-started.md +6 -2
- package/docs/en/host-contexts.md +26 -132
- package/docs/en/index.md +4 -4
- package/docs/en/recipes-and-pitfalls.md +10 -9
- package/docs/en/upgrading.md +20 -4
- package/docs/en/writing-plugins.md +111 -11
- package/docs/hu/bundled-plugins.md +1 -1
- package/docs/hu/documents-and-markdown.md +4 -5
- package/docs/hu/editor-and-preview.md +1 -3
- package/docs/hu/getting-started.md +6 -2
- package/docs/hu/host-contexts.md +26 -132
- package/docs/hu/index.md +4 -4
- package/docs/hu/recipes-and-pitfalls.md +10 -9
- package/docs/hu/upgrading.md +20 -4
- package/docs/hu/writing-plugins.md +111 -11
- package/package.json +6 -2
- package/dist/ImagePicker.svelte +0 -131
- package/dist/ImagePicker.svelte.d.ts +0 -13
- package/dist/media.d.ts +0 -17
- package/dist/media.js +0 -12
|
@@ -52,12 +52,9 @@
|
|
|
52
52
|
BetweenVerticalEnd,
|
|
53
53
|
BetweenVerticalStart,
|
|
54
54
|
Columns3,
|
|
55
|
-
Info,
|
|
56
55
|
Rows3,
|
|
57
56
|
GripHorizontal,
|
|
58
57
|
GripVertical,
|
|
59
|
-
PanelBottom,
|
|
60
|
-
PanelTop,
|
|
61
58
|
Plus,
|
|
62
59
|
TextAlignCenter,
|
|
63
60
|
TextAlignEnd,
|
|
@@ -78,17 +75,6 @@
|
|
|
78
75
|
let at = $state<{ r: number; c: number } | null>(null);
|
|
79
76
|
const heads = $derived(tableHeads(props));
|
|
80
77
|
const headOf = (r: number) => rowHead(heads, r, rows.length);
|
|
81
|
-
function toggleHead(h: Head) {
|
|
82
|
-
const next = new Set(heads);
|
|
83
|
-
if (!next.delete(h)) next.add(h);
|
|
84
|
-
const { header: _, ...rest } = props;
|
|
85
|
-
props = { ...rest, heads: HEADS.filter((x) => next.has(x)) } as TableProps;
|
|
86
|
-
}
|
|
87
|
-
const HEAD_TOGGLES: { head: Head; icon: typeof PanelTop; label: string }[] = [
|
|
88
|
-
{ head: 'top', icon: PanelTop, label: 'Header row' },
|
|
89
|
-
{ head: 'bottom', icon: PanelBottom, label: 'Footer row (e.g. sums)' }
|
|
90
|
-
];
|
|
91
|
-
|
|
92
78
|
/* Dragging a row or a column by its grip: `to` is the slot it would go to (0…count), shown as a blue line. */
|
|
93
79
|
let drag = $state<{ kind: 'row' | 'col'; from: number; to: number } | null>(null);
|
|
94
80
|
|
|
@@ -250,13 +236,8 @@
|
|
|
250
236
|
{/each}
|
|
251
237
|
</div>
|
|
252
238
|
{:else}
|
|
253
|
-
<span class="
|
|
239
|
+
<span class="idle">{tr('Click a cell to edit its row and column')}</span>
|
|
254
240
|
{/if}
|
|
255
|
-
<div class="seg heads">
|
|
256
|
-
{#each HEAD_TOGGLES as h (h.head)}
|
|
257
|
-
<button type="button" title={tr(h.label)} aria-pressed={heads.has(h.head)} class:on={heads.has(h.head)} onmousedown={(e) => e.preventDefault()} onclick={() => toggleHead(h.head)}><h.icon size={15} /></button>
|
|
258
|
-
{/each}
|
|
259
|
-
</div>
|
|
260
241
|
</div>
|
|
261
242
|
{/if}
|
|
262
243
|
<div class="grid">
|
|
@@ -321,9 +302,9 @@
|
|
|
321
302
|
.table {
|
|
322
303
|
font: 15px/1.45 system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
323
304
|
}
|
|
324
|
-
/*
|
|
305
|
+
/* The row and column tools of the cell being edited; when it wraps, it pushes the table down. */
|
|
325
306
|
.bar {
|
|
326
|
-
margin: 0
|
|
307
|
+
margin: 0 0 8px;
|
|
327
308
|
display: flex;
|
|
328
309
|
flex-wrap: wrap;
|
|
329
310
|
align-items: center;
|
|
@@ -336,13 +317,17 @@
|
|
|
336
317
|
gap: 1px;
|
|
337
318
|
padding: 2px;
|
|
338
319
|
border-radius: 8px;
|
|
339
|
-
background: #f4f4f6;
|
|
320
|
+
background: var(--color-muted, #f4f4f6);
|
|
321
|
+
}
|
|
322
|
+
.idle {
|
|
323
|
+
font-size: 12px;
|
|
324
|
+
color: var(--color-muted-contrast, #a0a0a8);
|
|
340
325
|
}
|
|
341
326
|
.label {
|
|
342
327
|
display: inline-grid;
|
|
343
328
|
place-items: center;
|
|
344
329
|
padding: 0 4px 0 5px;
|
|
345
|
-
color: #a0a0a8;
|
|
330
|
+
color: var(--color-muted-contrast, #a0a0a8);
|
|
346
331
|
}
|
|
347
332
|
.bar button {
|
|
348
333
|
display: inline-grid;
|
|
@@ -353,16 +338,16 @@
|
|
|
353
338
|
border: 0;
|
|
354
339
|
border-radius: 6px;
|
|
355
340
|
background: none;
|
|
356
|
-
color: #4b4b55;
|
|
341
|
+
color: var(--color-surface-contrast, #4b4b55);
|
|
357
342
|
cursor: pointer;
|
|
358
343
|
}
|
|
359
344
|
.bar button:hover:not(:disabled) {
|
|
360
|
-
background: #fff;
|
|
361
|
-
color: #1d1d1f;
|
|
345
|
+
background: var(--color-surface, #fff);
|
|
346
|
+
color: var(--color-surface-contrast, #1d1d1f);
|
|
362
347
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
|
363
348
|
}
|
|
364
349
|
.bar button.on {
|
|
365
|
-
background: #fff;
|
|
350
|
+
background: var(--color-surface, #fff);
|
|
366
351
|
color: #2563eb;
|
|
367
352
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
|
368
353
|
}
|
|
@@ -378,27 +363,16 @@
|
|
|
378
363
|
gap: 5px;
|
|
379
364
|
height: 28px;
|
|
380
365
|
padding: 0 10px;
|
|
381
|
-
border: 1px solid #e5e5ea;
|
|
366
|
+
border: 1px solid var(--color-frame, #e5e5ea);
|
|
382
367
|
border-radius: 8px;
|
|
383
368
|
font-size: 12px;
|
|
384
369
|
font-weight: 500;
|
|
385
370
|
}
|
|
386
371
|
.bar .toggle.on {
|
|
387
|
-
border-color: #
|
|
388
|
-
background: #
|
|
372
|
+
border-color: color-mix(in srgb, #3b82f6 35%, transparent);
|
|
373
|
+
background: color-mix(in srgb, #3b82f6 8%, transparent);
|
|
389
374
|
box-shadow: none;
|
|
390
375
|
}
|
|
391
|
-
.hint {
|
|
392
|
-
display: inline-grid;
|
|
393
|
-
place-items: center;
|
|
394
|
-
color: #a0a0a8;
|
|
395
|
-
cursor: help;
|
|
396
|
-
opacity: 0;
|
|
397
|
-
transition: opacity 0.12s;
|
|
398
|
-
}
|
|
399
|
-
.table:hover .hint {
|
|
400
|
-
opacity: 1;
|
|
401
|
-
}
|
|
402
376
|
|
|
403
377
|
/* The table, with a + bar on the right (column) and below (row) that shows on hover. */
|
|
404
378
|
.grid {
|
|
@@ -409,9 +383,9 @@
|
|
|
409
383
|
}
|
|
410
384
|
.frame {
|
|
411
385
|
overflow-x: auto;
|
|
412
|
-
border: 1px solid #e3e3e8;
|
|
386
|
+
border: 1px solid var(--color-frame, #e3e3e8);
|
|
413
387
|
border-radius: 10px;
|
|
414
|
-
background: #fff;
|
|
388
|
+
background: var(--color-surface, #fff);
|
|
415
389
|
}
|
|
416
390
|
table {
|
|
417
391
|
width: 100%;
|
|
@@ -421,8 +395,8 @@
|
|
|
421
395
|
td {
|
|
422
396
|
height: 1px;
|
|
423
397
|
padding: 0;
|
|
424
|
-
border-right: 1px solid #ececf0;
|
|
425
|
-
border-bottom: 1px solid #ececf0;
|
|
398
|
+
border-right: 1px solid var(--color-frame, #ececf0);
|
|
399
|
+
border-bottom: 1px solid var(--color-frame, #ececf0);
|
|
426
400
|
vertical-align: top;
|
|
427
401
|
transition: background 0.1s;
|
|
428
402
|
}
|
|
@@ -434,16 +408,16 @@
|
|
|
434
408
|
}
|
|
435
409
|
tr.row-on td,
|
|
436
410
|
td.col-on {
|
|
437
|
-
background: #
|
|
411
|
+
background: color-mix(in srgb, #3b82f6 4%, transparent);
|
|
438
412
|
}
|
|
439
413
|
/* The header and the footer as on the site: a band above, the sums bold on grey behind a thicker line. */
|
|
440
414
|
tr.h-top td:not(.rgrip) {
|
|
441
|
-
background: #f1f1f4;
|
|
442
|
-
border-bottom: 2px solid #d8d8de;
|
|
415
|
+
background: var(--color-muted, #f1f1f4);
|
|
416
|
+
border-bottom: 2px solid var(--color-frame, #d8d8de);
|
|
443
417
|
}
|
|
444
418
|
tr.h-bottom td:not(.rgrip) {
|
|
445
|
-
background: #f1f1f4;
|
|
446
|
-
border-top: 2px solid #d8d8de;
|
|
419
|
+
background: var(--color-muted, #f1f1f4);
|
|
420
|
+
border-top: 2px solid var(--color-frame, #d8d8de);
|
|
447
421
|
}
|
|
448
422
|
textarea {
|
|
449
423
|
display: block;
|
|
@@ -461,10 +435,10 @@
|
|
|
461
435
|
outline: none;
|
|
462
436
|
}
|
|
463
437
|
textarea::placeholder {
|
|
464
|
-
color: #b8b8c0;
|
|
438
|
+
color: var(--color-muted-contrast, #b8b8c0);
|
|
465
439
|
}
|
|
466
440
|
textarea:focus {
|
|
467
|
-
background: #fff;
|
|
441
|
+
background: var(--color-surface, #fff);
|
|
468
442
|
box-shadow: inset 0 0 0 2px #3b82f6;
|
|
469
443
|
}
|
|
470
444
|
tr.h-top textarea {
|
|
@@ -472,21 +446,21 @@
|
|
|
472
446
|
font-size: 13px;
|
|
473
447
|
letter-spacing: 0.04em;
|
|
474
448
|
text-transform: uppercase;
|
|
475
|
-
color: #3a3a44;
|
|
449
|
+
color: var(--color-surface-contrast, #3a3a44);
|
|
476
450
|
}
|
|
477
451
|
tr.h-bottom textarea {
|
|
478
452
|
font-weight: 600;
|
|
479
|
-
color: #1d1d1f;
|
|
453
|
+
color: var(--color-surface-contrast, #1d1d1f);
|
|
480
454
|
}
|
|
481
455
|
/* grips: a thin row above the columns and a thin column before the rows, shown on hover */
|
|
482
456
|
td.corner,
|
|
483
457
|
td.rgrip {
|
|
484
458
|
width: 14px;
|
|
485
|
-
border-right: 1px solid #ececf0;
|
|
459
|
+
border-right: 1px solid var(--color-frame, #ececf0);
|
|
486
460
|
}
|
|
487
461
|
tr.grips td {
|
|
488
462
|
height: 14px;
|
|
489
|
-
border-bottom: 1px solid #ececf0;
|
|
463
|
+
border-bottom: 1px solid var(--color-frame, #ececf0);
|
|
490
464
|
}
|
|
491
465
|
.grip {
|
|
492
466
|
display: grid;
|
|
@@ -497,7 +471,7 @@
|
|
|
497
471
|
padding: 0;
|
|
498
472
|
border: 0;
|
|
499
473
|
background: none;
|
|
500
|
-
color: #b0b0b8;
|
|
474
|
+
color: var(--color-muted-contrast, #b0b0b8);
|
|
501
475
|
cursor: grab;
|
|
502
476
|
opacity: 0;
|
|
503
477
|
touch-action: none;
|
|
@@ -509,7 +483,7 @@
|
|
|
509
483
|
}
|
|
510
484
|
.grip:hover {
|
|
511
485
|
color: #2563eb;
|
|
512
|
-
background: #
|
|
486
|
+
background: color-mix(in srgb, #3b82f6 9%, transparent);
|
|
513
487
|
}
|
|
514
488
|
table.dragging,
|
|
515
489
|
table.dragging * {
|
|
@@ -540,8 +514,8 @@
|
|
|
540
514
|
padding: 0;
|
|
541
515
|
border: 0;
|
|
542
516
|
border-radius: 6px;
|
|
543
|
-
background: #f4f4f6;
|
|
544
|
-
color: #8a8a94;
|
|
517
|
+
background: var(--color-muted, #f4f4f6);
|
|
518
|
+
color: var(--color-muted-contrast, #8a8a94);
|
|
545
519
|
cursor: pointer;
|
|
546
520
|
opacity: 0;
|
|
547
521
|
transition: opacity 0.12s, background 0.12s;
|
|
@@ -557,7 +531,7 @@
|
|
|
557
531
|
}
|
|
558
532
|
.add-col:hover,
|
|
559
533
|
.add-row:hover {
|
|
560
|
-
background: #
|
|
534
|
+
background: color-mix(in srgb, #3b82f6 12%, transparent);
|
|
561
535
|
color: #2563eb;
|
|
562
536
|
}
|
|
563
537
|
</style>
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
th,
|
|
47
47
|
td {
|
|
48
48
|
padding: 0.45em 0.7em;
|
|
49
|
-
border-bottom: 1px solid
|
|
49
|
+
border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
|
|
50
50
|
text-align: left;
|
|
51
51
|
vertical-align: top;
|
|
52
52
|
}
|
|
@@ -57,6 +57,6 @@
|
|
|
57
57
|
border-bottom-width: 2px;
|
|
58
58
|
}
|
|
59
59
|
th.bottom {
|
|
60
|
-
border-top: 2px solid
|
|
60
|
+
border-top: 2px solid color-mix(in srgb, currentColor 22%, transparent);
|
|
61
61
|
}
|
|
62
62
|
</style>
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The bundled plugins: plugins like any other (written with the kit), an app picks the ones it wants. Only general
|
|
2
2
|
// blocks live here (video, map, table, gap); anything an app gives its own meaning to belongs in the app.
|
|
3
|
-
import { MapPin, SquarePlay, Table, UnfoldVertical } from '@lucide/svelte';
|
|
3
|
+
import { MapPin, PanelBottom, PanelTop, SquarePlay, Table, UnfoldVertical } from '@lucide/svelte';
|
|
4
4
|
import { mdText } from '../kit/util';
|
|
5
5
|
import { youtubeId } from './util';
|
|
6
6
|
import Gap, { GAP_LINES } from './Gap.svelte';
|
|
@@ -42,6 +42,17 @@ export const table = {
|
|
|
42
42
|
keys: 'table grid rows columns price list',
|
|
43
43
|
icon: Table,
|
|
44
44
|
props: () => ({ rows: [['', ''], ['', '']], heads: ['top'] }),
|
|
45
|
+
fields: [
|
|
46
|
+
{
|
|
47
|
+
key: 'heads',
|
|
48
|
+
label: 'Header and footer',
|
|
49
|
+
kind: 'toggles',
|
|
50
|
+
options: [
|
|
51
|
+
{ value: 'top', label: 'Header row', icon: PanelTop },
|
|
52
|
+
{ value: 'bottom', label: 'Footer row (e.g. sums)', icon: PanelBottom }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
45
56
|
tag: (p) => `table · ${p.rows?.length ?? 0}×${p.rows?.[0]?.length ?? 0}`,
|
|
46
57
|
content: 'none',
|
|
47
58
|
markdown: (p) => {
|
package/dist/schema.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Server-side validation of a stored document (zod). Only the shape is checked: the renderers escape every text,
|
|
2
|
-
// keep links to http(s)/mailto/tel/site paths, take colours only as #hex
|
|
2
|
+
// keep links to http(s)/mailto/tel/site paths, take colours only as #hex.
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { acceptsId, MAX_DEPTH, MAX_INDENT } from './model';
|
|
5
5
|
const block = z.lazy(() => z.strictObject({
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { MediaItem } from './media';
|
|
2
1
|
import type { Component } from 'svelte';
|
|
3
2
|
import type { StoredBlock } from './model';
|
|
4
3
|
import type { Icon } from './palette';
|
|
@@ -31,10 +30,11 @@ export interface PropField {
|
|
|
31
30
|
key: string;
|
|
32
31
|
label: string;
|
|
33
32
|
/**
|
|
34
|
-
* '
|
|
33
|
+
* 'toggles' is a row of icon buttons, each switching one of `options` on or off; stored as the list of the ones on
|
|
34
|
+
* (in `options` order).
|
|
35
35
|
* 'slider' steps through `options` in their order (e.g. heights), or, with `min` and `max`, sets a number freely.
|
|
36
36
|
*/
|
|
37
|
-
kind: 'text' | 'textarea' | 'color' | 'select' | 'slider' | '
|
|
37
|
+
kind: 'text' | 'textarea' | 'color' | 'select' | 'slider' | 'toggles';
|
|
38
38
|
placeholder?: string;
|
|
39
39
|
/** A number slider: its range, step (default 1) and unit shown after the value. */
|
|
40
40
|
min?: number;
|
|
@@ -56,10 +56,11 @@ export interface PropField {
|
|
|
56
56
|
key: string;
|
|
57
57
|
label: string;
|
|
58
58
|
}[];
|
|
59
|
-
/** For 'select' and '
|
|
59
|
+
/** For 'select', 'slider' and 'toggles' (where `icon` is the button and `label` its tooltip). */
|
|
60
60
|
options?: {
|
|
61
61
|
value: string;
|
|
62
62
|
label: string;
|
|
63
|
+
icon?: Icon;
|
|
63
64
|
}[];
|
|
64
65
|
/** The field's text from the stored value; default: the value as a string. */
|
|
65
66
|
format?: (value: unknown) => string;
|
|
@@ -86,8 +87,6 @@ export interface PluginPreviewProps<P extends Props = Props> {
|
|
|
86
87
|
export interface MarkdownContext {
|
|
87
88
|
/** The nested blocks as markdown ('' when it has none). */
|
|
88
89
|
inner: string;
|
|
89
|
-
/** The media of the document, to turn an attachment id into its url. */
|
|
90
|
-
media: MediaItem[];
|
|
91
90
|
}
|
|
92
91
|
export interface BlockPlugin<P extends Props = Props> {
|
|
93
92
|
/** Stored `type`; must not start with 'md:'. */
|
|
@@ -112,7 +111,7 @@ export interface BlockPlugin<P extends Props = Props> {
|
|
|
112
111
|
*/
|
|
113
112
|
preview?: Component<PluginPreviewProps<any>>;
|
|
114
113
|
/**
|
|
115
|
-
* For a plugin with nested blocks: the blocks it takes, as palette ids ('md:p', '
|
|
114
|
+
* For a plugin with nested blocks: the blocks it takes, as palette ids ('md:p', 'youtube'…); 'md:*' stands for every
|
|
116
115
|
* built-in block. Its nested editor offers the host's palette narrowed to these. Default: everything the host has.
|
|
117
116
|
*/
|
|
118
117
|
accepts?: string[];
|
|
@@ -13,7 +13,7 @@ import { table, youtube } from '@atom-forge/brickee/plugins'; // or some
|
|
|
13
13
|
|
|
14
14
|
| Export | `type` | Fields | Markdown form | What it is |
|
|
15
15
|
|---|---|---|---|---|
|
|
16
|
-
| `table` | `table` |
|
|
16
|
+
| `table` | `table` | header and footer (the cells are edited in place) | a GFM table | Rows and columns of inline-formatted cells; click a cell for its row and column tools (insert, delete, align), drag rows and columns by their grips. The first row can be a header, the last a footer. |
|
|
17
17
|
| `youtube` | `youtube` | video link or id, title | a link | A YouTube video, loaded on click; any YouTube link is reduced to the video id. |
|
|
18
18
|
| `map` | `map` | place or address, zoom slider | a link | A Google Maps embed of a place or an address. |
|
|
19
19
|
| `gap` | `gap` | height slider | none | Empty space between blocks, 1–8 lines high. |
|
|
@@ -79,18 +79,17 @@ Markdown is a first-class format: the clipboard carries it, and a host can impor
|
|
|
79
79
|
| `fromStored(block)` | `StoredBlock` → `Block` | anywhere |
|
|
80
80
|
| `toStored(block)` | `Block` → `StoredBlock` | browser only |
|
|
81
81
|
| `fromMarkdown(text)` | markdown → `Block[]` | anywhere |
|
|
82
|
-
| `toMarkdown(blocks, { plugins
|
|
82
|
+
| `toMarkdown(blocks, { plugins })` | `Block[]` → markdown | browser only |
|
|
83
83
|
| `inline(md)` | inline markdown → inline HTML | anywhere |
|
|
84
84
|
|
|
85
85
|
The markdown functions work on the editor form (`Block[]`), so stored documents go through `fromStored` and `toStored`:
|
|
86
86
|
|
|
87
87
|
```ts
|
|
88
|
-
import { fromMarkdown, fromStored, toMarkdown, toStored, type
|
|
88
|
+
import { fromMarkdown, fromStored, toMarkdown, toStored, type StoredBlock } from '@atom-forge/brickee';
|
|
89
89
|
import { PLUGINS } from '@atom-forge/brickee/plugins';
|
|
90
90
|
|
|
91
91
|
// Stored document -> markdown (browser only).
|
|
92
|
-
export const exportMarkdown = (doc: StoredBlock[],
|
|
93
|
-
toMarkdown(doc.map(fromStored), { plugins: PLUGINS, media });
|
|
92
|
+
export const exportMarkdown = (doc: StoredBlock[]) => toMarkdown(doc.map(fromStored), { plugins: PLUGINS });
|
|
94
93
|
|
|
95
94
|
// Markdown -> stored document (browser only).
|
|
96
95
|
export const importMarkdown = (text: string): StoredBlock[] => fromMarkdown(text).map(toStored);
|
|
@@ -110,6 +109,6 @@ What `toMarkdown` writes:
|
|
|
110
109
|
|
|
111
110
|
- consecutive list items on consecutive lines, every other block separated by a blank line;
|
|
112
111
|
- list items and paragraphs indented by two spaces per level;
|
|
113
|
-
- a plugin block through its plugin's `markdown(props, { inner
|
|
112
|
+
- a plugin block through its plugin's `markdown(props, { inner })`, where `inner` is its nested blocks already converted; a plugin without a markdown form, or one returning `''`, is left out.
|
|
114
113
|
|
|
115
114
|
Writing a markdown form for your own plugin is covered in [Writing plugins](writing-plugins.md#markdown-form).
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
| `value` | `StoredBlock[]` (bindable) | `[]` | The document. Written back on every change; a document replaced from outside is loaded again. |
|
|
10
10
|
| `plugins` | `BlockPlugin[]` | `[]` | The plugin blocks this editor can show and offer. |
|
|
11
11
|
| `palette` | `string[]` | every built-in block, then every plugin | The block types the slash menu offers, in this order (see below). |
|
|
12
|
-
| `media` | `MediaItem[]` | — | The images blocks can pick from (see [Host contexts](host-contexts.md)). |
|
|
13
12
|
| `messages` | `Messages` | — | UI text in the host's language: English text → translation. |
|
|
14
13
|
| `readonly` | `boolean` | `false` | Shows the document without editing; plugin fields are hidden. |
|
|
15
14
|
| `placeholder` | `string` | `Write something, or press '/' for blocks…` | The hint of an empty block. |
|
|
@@ -23,7 +22,6 @@ An empty document is shown as one empty paragraph, so the value the editor write
|
|
|
23
22
|
|---|---|---|---|
|
|
24
23
|
| `blocks` | `StoredBlock[]` | — | The document to render. |
|
|
25
24
|
| `plugins` | `BlockPlugin[]` | — | The plugins whose previews render the plugin blocks. |
|
|
26
|
-
| `media` | `MediaItem[]` | — | The images the blocks refer to by id. |
|
|
27
25
|
| `messages` | `Messages` | — | UI text in the host's language. |
|
|
28
26
|
| `markGated` | `boolean` | `true` | Marks the blocks after a gate block (see [Unique and gate blocks](writing-plugins.md#unique-and-gate-blocks)). |
|
|
29
27
|
| `nested` | `boolean` | `false` | Set by plugin previews that render their nested blocks. |
|
|
@@ -32,7 +30,7 @@ The preview renders built-in blocks as plain HTML (`p`, `h1`…`h6`, `ul`/`ol` w
|
|
|
32
30
|
|
|
33
31
|
Using `Preview` at all is your choice: the editor does not depend on it. An application with its own renderer can render the stored blocks itself, and its plugins can leave `preview` out.
|
|
34
32
|
|
|
35
|
-
`
|
|
33
|
+
`messages` given to the top-level editor or preview are inherited by every nested editor, nested preview and plugin component below it.
|
|
36
34
|
|
|
37
35
|
## The palette
|
|
38
36
|
|
|
@@ -10,7 +10,7 @@ Everything else is a plugin. A plugin is a plain object (`BlockPlugin`) that nam
|
|
|
10
10
|
|
|
11
11
|
- a **view** — a Svelte component shown in the editor;
|
|
12
12
|
- a **preview** — a Svelte component that renders the block on the site;
|
|
13
|
-
- **fields** — the settings shown on the top of the block (text, colour, select, slider
|
|
13
|
+
- **fields** — the settings shown on the top of the block (text, colour, select, slider…);
|
|
14
14
|
- optionally a **markdown form**, used when the document is copied or exported as markdown.
|
|
15
15
|
|
|
16
16
|
The package ships four general-purpose plugins — table, YouTube video, map and gap — but they are plugins like any other: nothing in the core depends on them, and you pass only the ones you want. Image blocks, boxes and other site-specific blocks are plugins you write yourself.
|
|
@@ -24,7 +24,7 @@ bun add @atom-forge/brickee @atom-forge/ui @lucide/svelte
|
|
|
24
24
|
| Peer dependency | Why |
|
|
25
25
|
|---|---|
|
|
26
26
|
| `svelte` `^5.0.0` | The editor, the preview and the plugins are Svelte 5 components. |
|
|
27
|
-
| `@atom-forge/ui` `>=0.0.37` | The popups (link prompt
|
|
27
|
+
| `@atom-forge/ui` `>=0.0.37` | The popups (link prompt) and the form controls of the block fields. |
|
|
28
28
|
| `@lucide/svelte` `>=1.0.0` | The block icons. |
|
|
29
29
|
| `zod` `^4.0.0` (optional) | Only for the server-side schema in `@atom-forge/brickee/schema`. |
|
|
30
30
|
|
|
@@ -37,6 +37,10 @@ The block fields and popups are `@atom-forge/ui` components, so the application
|
|
|
37
37
|
|
|
38
38
|
The top-level `Editor` creates its own popup manager and renders its own popup container; editors nested in plugin blocks share it.
|
|
39
39
|
|
|
40
|
+
The editor takes its colours from the `@atom-forge/ui` theme tokens (`--color-surface`, `--color-frame`, `--color-muted-contrast`…), so it follows the host's light and dark mode. The preview's borders and captions are drawn from the text colour of the page.
|
|
41
|
+
|
|
42
|
+
The block handles are drawn in a gutter **left of the editor's box**, about 22 px wide: give the element around the editor that much padding on the left, or the handles stick out of it.
|
|
43
|
+
|
|
40
44
|
> **Missing theme**
|
|
41
45
|
>
|
|
42
46
|
> Without the `@atom-forge/ui` theme CSS the editor still works, but the block fields, the colour picker, the slider and the popups appear unstyled.
|
package/docs/en/host-contexts.md
CHANGED
|
@@ -2,143 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
[← Previous](writing-plugins.md) · [Contents](index.md) · [Next →](server-side.md)
|
|
4
4
|
|
|
5
|
-
The editor has no props for application concepts. What a host knows —
|
|
5
|
+
The editor has no props for application concepts. What a host knows — the page width it is showing, the code languages it highlights, its language — reaches the plugins through three Svelte contexts. Each has a `provide…` function, called in a component's initialization above the editor or preview, and a `use…` function for plugin components.
|
|
6
6
|
|
|
7
7
|
| Context | Provide | Read | Carries |
|
|
8
8
|
|---|---|---|---|
|
|
9
|
-
| Media | `media` prop, or `provideMedia(() => list)` | `useMedia()` | The images blocks can pick from. |
|
|
10
9
|
| View | `provideView(() => key)` | `useView()` | The view per-view fields edit. |
|
|
10
|
+
| Code languages | `provideCodeLanguages(() => list)` | `useCodeLanguages()` | The languages a code block offers. |
|
|
11
11
|
| Text | `messages` prop, or `provideText(() => messages)` | `useText()` | UI text in the host's language. |
|
|
12
12
|
|
|
13
13
|
Nested editors and previews inherit all three from their parent.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Blocks never store image URLs. An image field stores the **id** of a `MediaItem`, and the editor, the preview and the plugins look the item up in the document's media:
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
interface MediaItem {
|
|
21
|
-
id: string; // stored in the block's props
|
|
22
|
-
name: string; // file name or title, shown in the picker
|
|
23
|
-
url: string; // the image's address
|
|
24
|
-
group?: string; // a heading in the picker (e.g. the attachment category)
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The bundled plugins use no images; image blocks are yours to write, because their layout is the site's. A minimal `photo` plugin — an image field, alt text, and a height slider:
|
|
29
|
-
|
|
30
|
-
```svelte
|
|
31
|
-
<!-- PhotoView.svelte -->
|
|
32
|
-
<script lang="ts" module>
|
|
33
|
-
export interface PhotoProps extends Record<string, unknown> {
|
|
34
|
-
/** A MediaItem id. */
|
|
35
|
-
image: string;
|
|
36
|
-
alt: string;
|
|
37
|
-
/** The height in % of the width (20–150), or unset for the image's own shape. */
|
|
38
|
-
ratio?: unknown;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** A CSS aspect-ratio from a stored height, or undefined. */
|
|
42
|
-
export const aspect = (v: unknown) => {
|
|
43
|
-
const n = Number(v);
|
|
44
|
-
return n >= 20 && n <= 150 ? `100 / ${n}` : undefined;
|
|
45
|
-
};
|
|
46
|
-
</script>
|
|
47
|
-
|
|
48
|
-
<script lang="ts">
|
|
49
|
-
import { ImageIcon } from '@lucide/svelte';
|
|
50
|
-
import { Empty, findMedia, useMedia, type PluginViewProps } from '@atom-forge/brickee';
|
|
51
|
-
|
|
52
|
-
let { props = $bindable(), blocks = $bindable() }: PluginViewProps<PhotoProps> = $props();
|
|
53
|
-
const media = useMedia();
|
|
54
|
-
const item = $derived(findMedia(media(), props.image));
|
|
55
|
-
</script>
|
|
56
|
-
|
|
57
|
-
{#if item}
|
|
58
|
-
<img src={item.url} alt={props.alt} style="max-width: 100%" />
|
|
59
|
-
{:else}
|
|
60
|
-
<Empty icon={ImageIcon} text="Pick an image above" />
|
|
61
|
-
{/if}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
```svelte
|
|
65
|
-
<!-- PhotoPreview.svelte -->
|
|
66
|
-
<script lang="ts">
|
|
67
|
-
import { baseValue, findMedia, useMedia, type PluginPreviewProps } from '@atom-forge/brickee';
|
|
68
|
-
import { aspect, type PhotoProps } from './PhotoView.svelte';
|
|
69
|
-
|
|
70
|
-
let { props }: PluginPreviewProps<PhotoProps> = $props();
|
|
71
|
-
const media = useMedia();
|
|
72
|
-
const item = $derived(findMedia(media(), props.image));
|
|
73
|
-
</script>
|
|
74
|
-
|
|
75
|
-
{#if item}
|
|
76
|
-
<img src={item.url} alt={props.alt ?? ''} style:aspect-ratio={aspect(baseValue(props.ratio))} />
|
|
77
|
-
{/if}
|
|
78
|
-
|
|
79
|
-
<style>
|
|
80
|
-
img {
|
|
81
|
-
width: 100%;
|
|
82
|
-
object-fit: cover;
|
|
83
|
-
}
|
|
84
|
-
</style>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
// photo.ts
|
|
89
|
-
import { ImageIcon } from '@lucide/svelte';
|
|
90
|
-
import { findMedia, mdText, type BlockPlugin } from '@atom-forge/brickee';
|
|
91
|
-
import PhotoView, { type PhotoProps } from './PhotoView.svelte';
|
|
92
|
-
import PhotoPreview from './PhotoPreview.svelte';
|
|
93
|
-
|
|
94
|
-
export const photo: BlockPlugin<PhotoProps> = {
|
|
95
|
-
type: 'photo',
|
|
96
|
-
label: 'Photo',
|
|
97
|
-
hint: 'One picked image',
|
|
98
|
-
keys: 'photo image picture',
|
|
99
|
-
icon: ImageIcon,
|
|
100
|
-
props: () => ({ image: '', alt: '' }),
|
|
101
|
-
fields: [
|
|
102
|
-
{ key: 'image', label: 'Image', kind: 'image' },
|
|
103
|
-
{ key: 'alt', label: 'Alt text', kind: 'text', placeholder: 'What the image shows' },
|
|
104
|
-
{ key: 'ratio', label: 'Height', kind: 'slider', min: 20, max: 150, unit: '%', none: 'Original' }
|
|
105
|
-
],
|
|
106
|
-
tag: () => 'photo',
|
|
107
|
-
content: 'none',
|
|
108
|
-
markdown: (p, { media }) => {
|
|
109
|
-
const url = findMedia(media, p.image)?.url;
|
|
110
|
-
return url ? `` : '';
|
|
111
|
-
},
|
|
112
|
-
view: PhotoView,
|
|
113
|
-
preview: PhotoPreview
|
|
114
|
-
};
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Give the editor and the preview the same media:
|
|
118
|
-
|
|
119
|
-
```svelte
|
|
120
|
-
<script lang="ts">
|
|
121
|
-
import { Editor, Preview, type MediaItem, type StoredBlock } from '@atom-forge/brickee';
|
|
122
|
-
import { photo } from './photo';
|
|
123
|
-
|
|
124
|
-
let { value = $bindable(), media }: { value: StoredBlock[]; media: MediaItem[] } = $props();
|
|
125
|
-
</script>
|
|
126
|
-
|
|
127
|
-
<Editor bind:value plugins={[photo]} {media} />
|
|
128
|
-
<Preview blocks={value} plugins={[photo]} {media} />
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
In a plugin component, read the list with `useMedia()` — it returns a function, so the list stays current — and find an item with `findMedia(list, id)`, as the photo's view and preview do.
|
|
132
|
-
|
|
133
|
-
> **Thumbnails in the editor**
|
|
134
|
-
>
|
|
135
|
-
> The editor only needs small images. Because blocks store ids, you can give the editor a list of thumbnail URLs and the preview on the site a list of full-size URLs with the same ids.
|
|
136
|
-
|
|
137
|
-
Typical sources are a page's uploaded attachments or a media library. Every image a document refers to must be in the list, or its block renders nothing.
|
|
15
|
+
Images are not a context: what an image block stores and where its pictures come from is decided by the app's own plugin (see [Images](writing-plugins.md#images)).
|
|
138
16
|
|
|
139
17
|
## Per-view values
|
|
140
18
|
|
|
141
|
-
Some settings depend on how the page is shown: an image may need a different aspect ratio on a phone than on a wide screen. A `slider` field with `views` stores a value per view
|
|
19
|
+
Some settings depend on how the page is shown: an image may need a different aspect ratio on a phone than on a wide screen. A `slider` field with `views` stores a value per view. Here it is the height of the `photo` plugin from [Writing plugins](writing-plugins.md#images) and the bundled `gap`:
|
|
142
20
|
|
|
143
21
|
```ts
|
|
144
22
|
import type { BlockPlugin } from '@atom-forge/brickee';
|
|
@@ -184,18 +62,17 @@ A preview reads them with `viewValues(value, keys)` — the value for each view,
|
|
|
184
62
|
|
|
185
63
|
```svelte
|
|
186
64
|
<script lang="ts">
|
|
187
|
-
import {
|
|
188
|
-
import { aspect, type PhotoProps } from './PhotoView.svelte';
|
|
65
|
+
import { viewValues, type PluginPreviewProps } from '@atom-forge/brickee';
|
|
66
|
+
import { aspect, imageSrc, type PhotoProps } from './PhotoView.svelte';
|
|
189
67
|
|
|
190
68
|
let { props }: PluginPreviewProps<PhotoProps> = $props();
|
|
191
|
-
const
|
|
192
|
-
const item = $derived(findMedia(media(), props.image));
|
|
69
|
+
const src = $derived(imageSrc(props.url));
|
|
193
70
|
// desktop, tablet, phone: a CSS aspect-ratio each ('auto': the image's own shape)
|
|
194
71
|
const r = $derived(viewValues(props.ratio, ['desktop', 'tablet', 'phone']).map((v) => aspect(v) ?? 'auto'));
|
|
195
72
|
</script>
|
|
196
73
|
|
|
197
|
-
{#if
|
|
198
|
-
<img src
|
|
74
|
+
{#if src}
|
|
75
|
+
<img {src} alt={props.alt ?? ''} style:--r-desktop={r[0]} style:--r-tablet={r[1]} style:--r-phone={r[2]} />
|
|
199
76
|
{/if}
|
|
200
77
|
|
|
201
78
|
<style>
|
|
@@ -221,6 +98,23 @@ A preview reads them with `viewValues(value, keys)` — the value for each view,
|
|
|
221
98
|
>
|
|
222
99
|
> `views` is supported on `slider` fields. Other field kinds ignore it.
|
|
223
100
|
|
|
101
|
+
## Code languages
|
|
102
|
+
|
|
103
|
+
A code block's language (`props.lang`, the info string of its markdown fence) is free text by default. When the host knows which languages its site can highlight, it gives the list, and the field in the code block's fields bar becomes a dropdown of them, with *None* first:
|
|
104
|
+
|
|
105
|
+
```svelte
|
|
106
|
+
<script lang="ts">
|
|
107
|
+
import { Editor, provideCodeLanguages, type StoredBlock } from '@atom-forge/brickee';
|
|
108
|
+
|
|
109
|
+
let { value = $bindable() }: { value: StoredBlock[] } = $props();
|
|
110
|
+
provideCodeLanguages(() => [{ value: 'ts', label: 'TypeScript' }, { value: 'svelte', label: 'Svelte' }, 'css', 'bash']);
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<Editor bind:value />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
An item is a string or `{ value, label? }`; the value is what is stored. Nested editors inherit the list. A stored language missing from the list stays an option, so opening an older document loses nothing.
|
|
117
|
+
|
|
224
118
|
## UI text
|
|
225
119
|
|
|
226
120
|
The editor and the plugins are written in English, and the English text is the key of its translation. Pass `messages` — an object from English text to your language — to the top-level editor and preview:
|