@atom-forge/brickee 0.0.1 → 0.0.3
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 +59 -1
- package/README-AI.md +34 -28
- package/dist/Editor.svelte +713 -867
- package/dist/Editor.svelte.d.ts +0 -3
- package/dist/LinkPrompt.svelte +1 -14
- package/dist/Preview.svelte +27 -58
- package/dist/Preview.svelte.d.ts +2 -7
- package/dist/PropsBar.svelte +75 -249
- package/dist/PropsBar.svelte.d.ts +0 -3
- package/dist/code.d.ts +12 -0
- package/dist/code.js +15 -0
- package/dist/dom.d.ts +8 -0
- package/dist/dom.js +9 -0
- package/dist/editor/BlockMenu.svelte +102 -0
- package/dist/editor/BlockMenu.svelte.d.ts +32 -0
- package/dist/editor/FormatBar.svelte +41 -0
- package/dist/editor/FormatBar.svelte.d.ts +16 -0
- package/dist/editor/drag.svelte.d.ts +63 -0
- package/dist/editor/drag.svelte.js +95 -0
- package/dist/editor/history.d.ts +24 -0
- package/dist/editor/history.js +86 -0
- package/dist/editor/sweep.svelte.d.ts +21 -0
- package/dist/editor/sweep.svelte.js +56 -0
- package/dist/html.d.ts +12 -0
- package/dist/html.js +206 -0
- package/dist/i18n.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -3
- package/dist/kit/Empty.svelte +4 -15
- package/dist/kit/Frame.svelte +7 -37
- package/dist/locales/hu.js +6 -8
- package/dist/markdown.d.ts +20 -6
- package/dist/markdown.js +380 -85
- package/dist/model.js +2 -3
- package/dist/plugins/Gap.svelte +5 -8
- package/dist/plugins/GapPreview.svelte +4 -6
- package/dist/plugins/Map.svelte +2 -17
- package/dist/plugins/MapPreview.svelte +8 -10
- package/dist/plugins/Table.svelte +233 -282
- package/dist/plugins/TablePreview.svelte +28 -28
- package/dist/plugins/YouTube.svelte +3 -31
- package/dist/plugins/YouTubePreview.svelte +45 -43
- package/dist/plugins/index.js +15 -2
- package/dist/schema.js +1 -1
- package/dist/types.d.ts +27 -10
- package/dist/types.js +8 -1
- package/dist/view.js +1 -1
- package/docs/en/bundled-plugins.md +1 -1
- package/docs/en/documents-and-markdown.md +24 -19
- package/docs/en/editor-and-preview.md +7 -10
- package/docs/en/getting-started.md +14 -2
- package/docs/en/host-contexts.md +26 -132
- package/docs/en/index.md +5 -5
- package/docs/en/recipes-and-pitfalls.md +12 -11
- package/docs/en/server-side.md +0 -17
- package/docs/en/upgrading.md +34 -4
- package/docs/en/writing-plugins.md +147 -21
- package/docs/hu/bundled-plugins.md +1 -1
- package/docs/hu/documents-and-markdown.md +24 -19
- package/docs/hu/editor-and-preview.md +7 -10
- package/docs/hu/getting-started.md +14 -2
- package/docs/hu/host-contexts.md +26 -132
- package/docs/hu/index.md +5 -5
- package/docs/hu/recipes-and-pitfalls.md +12 -11
- package/docs/hu/server-side.md +0 -17
- package/docs/hu/upgrading.md +34 -4
- package/docs/hu/writing-plugins.md +147 -21
- package/package.json +14 -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
|
@@ -32,31 +32,31 @@
|
|
|
32
32
|
</div>
|
|
33
33
|
{/if}
|
|
34
34
|
|
|
35
|
-
<style
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
</style>
|
|
35
|
+
<style>.wrap {
|
|
36
|
+
margin: 0 0 1em;
|
|
37
|
+
overflow-x: auto;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
table {
|
|
41
|
+
width: 100%;
|
|
42
|
+
border-collapse: collapse;
|
|
43
|
+
font: 15px/1.5 system-ui, sans-serif;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
th,
|
|
47
|
+
td {
|
|
48
|
+
padding: 0.45em 0.7em;
|
|
49
|
+
border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
|
|
50
|
+
text-align: left;
|
|
51
|
+
vertical-align: top;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
th {
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
}
|
|
57
|
+
th.top {
|
|
58
|
+
border-bottom-width: 2px;
|
|
59
|
+
}
|
|
60
|
+
th.bottom {
|
|
61
|
+
border-top: 2px solid color-mix(in srgb, currentColor 22%, transparent);
|
|
62
|
+
}</style>
|
|
@@ -15,38 +15,10 @@
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
{#if id}
|
|
18
|
-
<div class="
|
|
19
|
-
<img src="https://i.ytimg.com/vi/{id}/hqdefault.jpg" alt={props.title} draggable="false" />
|
|
20
|
-
<span class="
|
|
18
|
+
<div class="relative overflow-hidden rounded-[10px] bg-black">
|
|
19
|
+
<img class="block aspect-video w-full object-cover opacity-85" src="https://i.ytimg.com/vi/{id}/hqdefault.jpg" alt={props.title} draggable="false" />
|
|
20
|
+
<span class="absolute inset-0 m-auto grid h-10 w-14 place-items-center rounded-[10px] bg-red-600 text-white"><Play size={22} /></span>
|
|
21
21
|
</div>
|
|
22
22
|
{:else}
|
|
23
23
|
<Empty icon={SquarePlay} text="Paste a YouTube link above" />
|
|
24
24
|
{/if}
|
|
25
|
-
|
|
26
|
-
<style>
|
|
27
|
-
.yt {
|
|
28
|
-
position: relative;
|
|
29
|
-
border-radius: 10px;
|
|
30
|
-
overflow: hidden;
|
|
31
|
-
background: #000;
|
|
32
|
-
}
|
|
33
|
-
img {
|
|
34
|
-
display: block;
|
|
35
|
-
width: 100%;
|
|
36
|
-
aspect-ratio: 16 / 9;
|
|
37
|
-
object-fit: cover;
|
|
38
|
-
opacity: 0.85;
|
|
39
|
-
}
|
|
40
|
-
.play {
|
|
41
|
-
position: absolute;
|
|
42
|
-
inset: 0;
|
|
43
|
-
margin: auto;
|
|
44
|
-
width: 56px;
|
|
45
|
-
height: 40px;
|
|
46
|
-
display: grid;
|
|
47
|
-
place-items: center;
|
|
48
|
-
border-radius: 10px;
|
|
49
|
-
background: #dc2626;
|
|
50
|
-
color: #fff;
|
|
51
|
-
}
|
|
52
|
-
</style>
|
|
@@ -31,46 +31,48 @@
|
|
|
31
31
|
</figure>
|
|
32
32
|
{/if}
|
|
33
33
|
|
|
34
|
-
<style
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
34
|
+
<style>.yt {
|
|
35
|
+
margin: 0 0 1em;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
iframe,
|
|
39
|
+
button {
|
|
40
|
+
display: block;
|
|
41
|
+
width: 100%;
|
|
42
|
+
aspect-ratio: 16/9;
|
|
43
|
+
border: 0;
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
button {
|
|
48
|
+
position: relative;
|
|
49
|
+
padding: 0;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
background: #000;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
}
|
|
54
|
+
button img {
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 100%;
|
|
57
|
+
object-fit: cover;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.play {
|
|
61
|
+
position: absolute;
|
|
62
|
+
inset: 0;
|
|
63
|
+
margin: auto;
|
|
64
|
+
width: 64px;
|
|
65
|
+
height: 44px;
|
|
66
|
+
display: grid;
|
|
67
|
+
place-items: center;
|
|
68
|
+
border-radius: 12px;
|
|
69
|
+
background: #dc2626;
|
|
70
|
+
color: #fff;
|
|
71
|
+
font-size: 20px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
figcaption {
|
|
75
|
+
margin-top: 0.4em;
|
|
76
|
+
color: color-mix(in srgb, currentColor 65%, transparent);
|
|
77
|
+
font: 13px/1.4 system-ui, sans-serif;
|
|
78
|
+
}</style>
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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';
|
|
7
7
|
import GapPreview from './GapPreview.svelte';
|
|
8
8
|
import MapView, {} from './Map.svelte';
|
|
9
9
|
import MapPreview from './MapPreview.svelte';
|
|
10
|
-
import TableView, { tableHeads } from './Table.svelte';
|
|
10
|
+
import TableView, { square, tableHeads } from './Table.svelte';
|
|
11
11
|
import TablePreview from './TablePreview.svelte';
|
|
12
12
|
import YouTube, {} from './YouTube.svelte';
|
|
13
13
|
import YouTubePreview from './YouTubePreview.svelte';
|
|
@@ -42,8 +42,21 @@ 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',
|
|
58
|
+
// A pasted or imported GFM table: its first row is the header.
|
|
59
|
+
fromTable: ({ rows, align }) => ({ rows: square(rows), heads: ['top'], ...(align.some((a) => a !== 'left') ? { align } : {}) }),
|
|
47
60
|
markdown: (p) => {
|
|
48
61
|
const rows = p.rows ?? [];
|
|
49
62
|
if (!rows.length)
|
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;
|
|
@@ -82,12 +83,16 @@ export interface PluginPreviewProps<P extends Props = Props> {
|
|
|
82
83
|
plugins: BlockPlugin[];
|
|
83
84
|
}
|
|
84
85
|
/** A custom block for the slash menu. */
|
|
86
|
+
/** A GFM table read from markdown: the cells as inline markdown, row by row (the first row is the header), and the
|
|
87
|
+
* alignment of each column. */
|
|
88
|
+
export interface MarkdownTable {
|
|
89
|
+
rows: string[][];
|
|
90
|
+
align: ('left' | 'center' | 'right')[];
|
|
91
|
+
}
|
|
85
92
|
/** What a plugin's `markdown` gets beside its props. */
|
|
86
93
|
export interface MarkdownContext {
|
|
87
94
|
/** The nested blocks as markdown ('' when it has none). */
|
|
88
95
|
inner: string;
|
|
89
|
-
/** The media of the document, to turn an attachment id into its url. */
|
|
90
|
-
media: MediaItem[];
|
|
91
96
|
}
|
|
92
97
|
export interface BlockPlugin<P extends Props = Props> {
|
|
93
98
|
/** Stored `type`; must not start with 'md:'. */
|
|
@@ -112,7 +117,7 @@ export interface BlockPlugin<P extends Props = Props> {
|
|
|
112
117
|
*/
|
|
113
118
|
preview?: Component<PluginPreviewProps<any>>;
|
|
114
119
|
/**
|
|
115
|
-
* For a plugin with nested blocks: the blocks it takes, as palette ids ('md:p', '
|
|
120
|
+
* For a plugin with nested blocks: the blocks it takes, as palette ids ('md:p', 'youtube'…); 'md:*' stands for every
|
|
116
121
|
* built-in block. Its nested editor offers the host's palette narrowed to these. Default: everything the host has.
|
|
117
122
|
*/
|
|
118
123
|
accepts?: string[];
|
|
@@ -121,13 +126,25 @@ export interface BlockPlugin<P extends Props = Props> {
|
|
|
121
126
|
* when it gives '', the block is left out of the markdown.
|
|
122
127
|
*/
|
|
123
128
|
markdown?(props: P, ctx: MarkdownContext): string;
|
|
124
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Takes a GFM table when markdown is pasted or imported (`fromMarkdown(src, { plugins })`): returns the block's props
|
|
131
|
+
* from the table's cells (inline markdown, the first row is the header) and its column alignments. The first plugin
|
|
132
|
+
* that has it gets the table; without one, the table stays text.
|
|
133
|
+
*/
|
|
134
|
+
fromTable?(table: MarkdownTable): P;
|
|
135
|
+
/**
|
|
136
|
+
* Takes files pasted into the editor or dropped on it (an image, a PDF…): returns the new block's props, or null to
|
|
137
|
+
* leave them to the next plugin. Uploading and storing them is the plugin's own business; it may take its time (a
|
|
138
|
+
* promise). The first plugin the editor offers that takes them gets one block.
|
|
139
|
+
*/
|
|
140
|
+
fromFiles?(files: File[]): P | null | Promise<P | null>;
|
|
141
|
+
/** At most one per document, and only at the top level (e.g. a table of contents). */
|
|
125
142
|
unique?: boolean;
|
|
126
|
-
/** The blocks after it are gated (e.g. for supporters only): the preview marks them. Implies nothing about `unique`. */
|
|
127
|
-
gate?: boolean;
|
|
128
143
|
}
|
|
129
144
|
export { MAX_INDENT } from './model';
|
|
130
145
|
export declare const uid: () => string;
|
|
131
146
|
export declare const newBlock: (type?: BlockType, html?: string, extra?: Partial<Block>) => Block;
|
|
147
|
+
/** The start number a numbered list's first item carries (`props.start`), or 1. */
|
|
148
|
+
export declare const olStart: (props: Record<string, unknown> | undefined) => number;
|
|
132
149
|
/** Ordinal for every `ol` block, restarting after interruptions and per indent level. */
|
|
133
150
|
export declare function numbering(blocks: Block[]): Record<string, number>;
|
package/dist/types.js
CHANGED
|
@@ -25,14 +25,21 @@ export const newBlock = (type = 'p', html = '', extra = {}) => ({
|
|
|
25
25
|
indent: 0,
|
|
26
26
|
...extra
|
|
27
27
|
});
|
|
28
|
+
/** The start number a numbered list's first item carries (`props.start`), or 1. */
|
|
29
|
+
export const olStart = (props) => {
|
|
30
|
+
const n = Math.floor(Number(props?.start));
|
|
31
|
+
return n >= 0 && n < 1e9 ? n : 1;
|
|
32
|
+
};
|
|
28
33
|
/** Ordinal for every `ol` block, restarting after interruptions and per indent level. */
|
|
29
34
|
export function numbering(blocks) {
|
|
30
35
|
const out = {};
|
|
31
36
|
const counters = [];
|
|
32
37
|
for (const b of blocks) {
|
|
33
38
|
if (b.type === 'ol') {
|
|
39
|
+
const fresh = counters[b.indent] === undefined;
|
|
34
40
|
counters.length = b.indent + 1;
|
|
35
|
-
|
|
41
|
+
// A list's first item may carry its start number (an imported "5. …").
|
|
42
|
+
counters[b.indent] = fresh ? olStart(b.props) : counters[b.indent] + 1;
|
|
36
43
|
out[b.id] = counters[b.indent];
|
|
37
44
|
}
|
|
38
45
|
else {
|
package/dist/view.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// host's business (e.g. the width its preview shows): it gives the current one through the context. Without it, a
|
|
3
3
|
// per-view field simply edits its base value.
|
|
4
4
|
import { getContext, setContext } from 'svelte';
|
|
5
|
-
const KEY = '
|
|
5
|
+
const KEY = 'brickee:view';
|
|
6
6
|
/** Call in a component's init (around the editor); the function returns the current view's key. */
|
|
7
7
|
export function provideView(get) {
|
|
8
8
|
setContext(KEY, get);
|
|
@@ -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 (a pasted GFM table becomes one) | 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. A cell shows its text formatted; editing it shows the markdown, and `Ctrl/Cmd` + `B`, `I`, `E`, `Shift+X`, `K` mark the selected text. 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. |
|
|
@@ -46,13 +46,13 @@ A short document with a paragraph, a list, a video and a box (the `note` contain
|
|
|
46
46
|
| `md:p` | inline markdown | — | the text |
|
|
47
47
|
| `md:h1` … `md:h6` | inline markdown | — | `#` … `######` |
|
|
48
48
|
| `md:ul` | inline markdown | — | `- item` |
|
|
49
|
-
| `md:ol` | inline markdown |
|
|
49
|
+
| `md:ol` | inline markdown | `{ start?: number }` on a list's first item | `1. item` (numbered per run and indent level, from `start` when given) |
|
|
50
50
|
| `md:todo` | inline markdown | `{ checked: boolean }` | `- [x] item` |
|
|
51
51
|
| `md:quote` | inline markdown | — | `> quote` |
|
|
52
52
|
| `md:code` | raw text | `{ lang?: string }` | fenced block with the language |
|
|
53
53
|
| `md:hr` | — | — | `---` |
|
|
54
54
|
|
|
55
|
-
`indent` nests list items (consecutive list blocks become properly nested `ul`/`ol` elements in the preview) and
|
|
55
|
+
`indent` nests list items (consecutive list blocks become properly nested `ul`/`ol` elements in the preview), puts paragraphs and code blocks under a list item, and nests quotes (`> > …`). Headings and dividers ignore it.
|
|
56
56
|
|
|
57
57
|
### Inline markdown
|
|
58
58
|
|
|
@@ -60,15 +60,15 @@ The text of a built-in block is a small, safe subset of markdown:
|
|
|
60
60
|
|
|
61
61
|
| Mark | Stored as |
|
|
62
62
|
|---|---|
|
|
63
|
-
| bold | `**text**` |
|
|
64
|
-
| italic | `*text*` |
|
|
63
|
+
| bold | `**text**` (read also: `__text__`) |
|
|
64
|
+
| italic | `*text*` (read also: `_text_`, not inside a word) |
|
|
65
65
|
| underline, subscript, superscript | `<u>text</u>`, `<sub>text</sub>`, `<sup>text</sup>` |
|
|
66
66
|
| strikethrough | `~~text~~` |
|
|
67
|
-
| code | `` `text` `` |
|
|
68
|
-
| link | `[text](url)` — only `http:`, `https:`, `mailto:`, `tel:` and site-relative `/…` targets become links |
|
|
69
|
-
| line break | a newline |
|
|
67
|
+
| code | `` `text` `` (a longer backtick run when the code holds backticks: ``` `` a`b `` ```) |
|
|
68
|
+
| link | `[text](url)` — only `http:`, `https:`, `mailto:`, `tel:` and site-relative `/…` targets become links; parentheses in a target must be balanced. Read also: a title (dropped), `<https://…>` autolinks, and an image `` as a link |
|
|
69
|
+
| line break | a newline (in markdown out: a backslash at the line's end) |
|
|
70
70
|
|
|
71
|
-
Special characters in plain text are backslash-escaped, so they never turn into markup. `inline(md)` turns inline markdown into HTML: it escapes the text first and only then adds the tags above, so its output is safe to render with `{@html}` — this is what the preview does.
|
|
71
|
+
Special characters in plain text are backslash-escaped, so they never turn into markup; any ASCII punctuation can be escaped. Reading stays linear on any input, with these limits: a marked span is at most 2000 characters, a link label has no brackets of its own and is at most 1000 characters, a link target at most 2000. When reading, inline HTML maps onto the marks (`<strong>`, `<em>`, `<del>`, `<br>`…), other tags are dropped with their text kept, and a footnote reference `[^1]` becomes a superscript. `inline(md)` turns inline markdown into HTML: it escapes the text first and only then adds the tags above, so its output is safe to render with `{@html}` — this is what the preview does.
|
|
72
72
|
|
|
73
73
|
## Markdown in and out
|
|
74
74
|
|
|
@@ -78,19 +78,19 @@ Markdown is a first-class format: the clipboard carries it, and a host can impor
|
|
|
78
78
|
|---|---|---|
|
|
79
79
|
| `fromStored(block)` | `StoredBlock` → `Block` | anywhere |
|
|
80
80
|
| `toStored(block)` | `Block` → `StoredBlock` | browser only |
|
|
81
|
-
| `fromMarkdown(text)` | markdown → `Block[]` | anywhere |
|
|
82
|
-
| `
|
|
81
|
+
| `fromMarkdown(text, { plugins })` | markdown → `Block[]` | anywhere |
|
|
82
|
+
| `fromHtml(html, { plugins })` | HTML (a formatted paste, a page) → `Block[]` | browser only |
|
|
83
|
+
| `toMarkdown(blocks, { plugins })` | `Block[]` → markdown | browser only |
|
|
83
84
|
| `inline(md)` | inline markdown → inline HTML | anywhere |
|
|
84
85
|
|
|
85
86
|
The markdown functions work on the editor form (`Block[]`), so stored documents go through `fromStored` and `toStored`:
|
|
86
87
|
|
|
87
88
|
```ts
|
|
88
|
-
import { fromMarkdown, fromStored, toMarkdown, toStored, type
|
|
89
|
+
import { fromMarkdown, fromStored, toMarkdown, toStored, type StoredBlock } from '@atom-forge/brickee';
|
|
89
90
|
import { PLUGINS } from '@atom-forge/brickee/plugins';
|
|
90
91
|
|
|
91
92
|
// Stored document -> markdown (browser only).
|
|
92
|
-
export const exportMarkdown = (doc: StoredBlock[],
|
|
93
|
-
toMarkdown(doc.map(fromStored), { plugins: PLUGINS, media });
|
|
93
|
+
export const exportMarkdown = (doc: StoredBlock[]) => toMarkdown(doc.map(fromStored), { plugins: PLUGINS });
|
|
94
94
|
|
|
95
95
|
// Markdown -> stored document (browser only).
|
|
96
96
|
export const importMarkdown = (text: string): StoredBlock[] => fromMarkdown(text).map(toStored);
|
|
@@ -102,14 +102,19 @@ export const importMarkdown = (text: string): StoredBlock[] => fromMarkdown(text
|
|
|
102
102
|
|
|
103
103
|
What `fromMarkdown` reads:
|
|
104
104
|
|
|
105
|
-
- headings, bulleted, numbered and to-do items, quotes, fenced code
|
|
106
|
-
-
|
|
107
|
-
-
|
|
105
|
+
- headings (also setext: a line underlined with `===` or `---`, and with closing `#`s), bulleted, numbered (with their start number) and to-do items, quotes (nested ones one level deeper), fenced code (backticks or tildes, any length, closed by a fence at least as long) and indented code, dividers, and the inline marks above;
|
|
106
|
+
- list nesting by the items' markers: a line indented past an item's marker is inside the item (so `1. ` items nest with two, three or four spaces);
|
|
107
|
+
- the lines of a paragraph, a list item or a quote as one block, with line breaks between them; a blank line, or `>` alone in a quote, starts a new one;
|
|
108
|
+
- reference links (`[text][id]` with `[id]: url`) resolved, footnote definitions as a paragraph after their number;
|
|
109
|
+
- a GFM table as a block of the first of `plugins` that takes tables (`fromTable`, see [Writing plugins](writing-plugins.md#taking-pasted-tables); the bundled `table` does), otherwise as text. The editor passes the plugins it offers, so a pasted table becomes a table block.
|
|
110
|
+
|
|
111
|
+
What `fromHtml` reads — the editor uses it for a formatted paste from a web page, Google Docs or Word: headings, paragraphs, lists (nested, numbered with their start, to-dos from checkboxes), quotes, code (`pre`, with a `language-…` class), rules, tables (for a plugin that takes them), and the inline marks — also Google Docs' bold, italic and struck-through spans. Images become links (the package has no images), and scripts, styles and form controls are dropped. The HTML is only read: it goes through markdown and the same safe rendering as everything else.
|
|
108
112
|
|
|
109
113
|
What `toMarkdown` writes:
|
|
110
114
|
|
|
111
|
-
- consecutive list items on consecutive lines, every other block separated by a blank line;
|
|
112
|
-
-
|
|
113
|
-
- a
|
|
115
|
+
- consecutive list items on consecutive lines, consecutive quotes as one quote (`>` between their paragraphs), every other block separated by a blank line;
|
|
116
|
+
- nested items, and paragraphs and code under an item, indented to the item's content (two spaces under `- `, three under `1. `, four under `10. `);
|
|
117
|
+
- code in a fence longer than any backtick run in it, and plain text escaped where markdown would read it as markup (`\_`, `1\.` at a line's start…);
|
|
118
|
+
- 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
119
|
|
|
115
120
|
Writing a markdown form for your own plugin is covered in [Writing plugins](writing-plugins.md#markdown-form).
|
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
| Prop | Type | Default | Meaning |
|
|
8
8
|
|---|---|---|---|
|
|
9
|
-
| `value` | `StoredBlock[]` (bindable) | `[]` | The document. Written back on every change; a document replaced from outside is loaded again. |
|
|
9
|
+
| `value` | `StoredBlock[]` (bindable) | `[]` | The document. Written back on every change (unchanged blocks keep their objects); a document replaced from outside — a new array, not one changed in place — is loaded again, unless it equals the current one. |
|
|
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
|
-
| `readonly` | `boolean` | `false` | Shows the document without editing; plugin fields are hidden. |
|
|
13
|
+
| `readonly` | `boolean` | `false` | Shows the document without editing; plugin fields are hidden. A read-only editor never writes `value`. |
|
|
15
14
|
| `placeholder` | `string` | `Write something, or press '/' for blocks…` | The hint of an empty block. |
|
|
16
15
|
| `nested` | `boolean` | `false` | Set by plugins with nested blocks for their inner editor; a host never sets it. |
|
|
17
16
|
|
|
@@ -23,16 +22,14 @@ 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
|
-
| `markGated` | `boolean` | `true` | Marks the blocks after a gate block (see [Unique and gate blocks](writing-plugins.md#unique-and-gate-blocks)). |
|
|
29
26
|
| `nested` | `boolean` | `false` | Set by plugin previews that render their nested blocks. |
|
|
30
27
|
|
|
31
28
|
The preview renders built-in blocks as plain HTML (`p`, `h1`…`h6`, `ul`/`ol` with nested items, `blockquote`, `pre > code` with a `language-<lang>` class, `hr`) and each plugin block with its plugin's `preview`. A block whose type has no plugin in `plugins`, or whose plugin has no `preview`, is not rendered.
|
|
32
29
|
|
|
33
30
|
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
31
|
|
|
35
|
-
`
|
|
32
|
+
`messages` given to the top-level editor or preview are inherited by every nested editor, nested preview and plugin component below it.
|
|
36
33
|
|
|
37
34
|
## The palette
|
|
38
35
|
|
|
@@ -74,10 +71,10 @@ A plain paragraph (`md:p`) is always allowed as the fallback.
|
|
|
74
71
|
| Inline formatting | Select text for the toolbar, or `Ctrl/Cmd` + `B` bold, `I` italic, `U` underline, `Shift+X` strikethrough, `E` code, `K` link, `\` remove formatting. Subscript and superscript are on the toolbar. |
|
|
75
72
|
| Indent | `Tab` / `Shift+Tab` (at most 6 levels). |
|
|
76
73
|
| Line break | `Shift+Enter`; in a code block `Enter` breaks the line and `Shift+Enter` or `Ctrl/Cmd+Enter` starts a new block. |
|
|
77
|
-
| Select blocks | Click a block's handle, `Esc` in a block, `Shift+↑/↓` past the edge of a block, or drag a text selection across blocks. `Ctrl/Cmd+A` in an empty block selects every block. |
|
|
78
|
-
| Move blocks | Drag the handle (also into and out of nested editors),
|
|
79
|
-
| Turn into | Click the handle of a block or a selection for the Turn-into menu. |
|
|
80
|
-
| Copy, cut | Copying selected blocks puts their markdown on the clipboard (plugin blocks through their markdown form). |
|
|
74
|
+
| Select blocks | Click (or tap) a block's handle, `Esc` in a block, `Shift+↑/↓` past the edge of a block, or drag a text selection across blocks. `Ctrl/Cmd+A` in an empty block selects every block. |
|
|
75
|
+
| Move blocks | Drag the handle (also into and out of nested editors), `Ctrl/Cmd+Shift+↑/↓`, or *Move up* / *Move down* in the Turn-into menu (without dragging: touch screens, keyboard). |
|
|
76
|
+
| Turn into | Click the handle of a block or a selection for the Turn-into menu; from the keyboard `/` (or `Ctrl/Cmd+/`, the menu key, `Shift+F10`) on selected blocks, or `Ctrl/Cmd+/` in a block's text. On a touch screen a tapped block shows its actions (add below, turn into, delete) beside its handle. |
|
|
77
|
+
| Copy, cut, paste | Copying selected blocks puts their markdown on the clipboard (plugin blocks through their markdown form) and, beside it, the blocks themselves (`application/x-brickee+json`). Formatted text from elsewhere (a web page, Google Docs, Word) keeps its headings, lists, tables and marks (`fromHtml`); plain text is read as markdown. Pasted into a Brickee editor, copied blocks come back as they were; a plugin block that editor does not offer (or a second unique one) falls back to its markdown form. Pasted anywhere else, the markdown arrives. Pasting while blocks are selected puts the blocks after them. |
|
|
81
78
|
| Paste | Multi-line text is read as markdown and becomes blocks. |
|
|
82
79
|
| Undo, redo | `Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z` or `Ctrl/Cmd+Y`. |
|
|
83
80
|
|
|
@@ -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
|
|
|
@@ -33,10 +33,22 @@ bun add @atom-forge/brickee @atom-forge/ui @lucide/svelte
|
|
|
33
33
|
The block fields and popups are `@atom-forge/ui` components, so the application must be set up the way `@atom-forge/ui` requires:
|
|
34
34
|
|
|
35
35
|
- Tailwind CSS 4 with the `@atom-forge/ui` theme CSS imported and its `dist` folder added as a Tailwind `@source`;
|
|
36
|
+
- the `dist` folder of `@atom-forge/brickee` added as a Tailwind `@source` too: the editor is styled with Tailwind classes and the `@atom-forge/ui` theme's colours, so Tailwind has to see them:
|
|
37
|
+
|
|
38
|
+
```css
|
|
39
|
+
@source "../node_modules/@atom-forge/ui/dist";
|
|
40
|
+
@source "../node_modules/@atom-forge/brickee/dist";
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The preview needs neither: its few styles travel with its markup, so a public page without Tailwind can show documents;
|
|
36
44
|
- the application rendered beneath the `@atom-forge/ui` `Root` component, which provides the theme and the overlay contexts.
|
|
37
45
|
|
|
38
46
|
The top-level `Editor` creates its own popup manager and renders its own popup container; editors nested in plugin blocks share it.
|
|
39
47
|
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
40
52
|
> **Missing theme**
|
|
41
53
|
>
|
|
42
54
|
> 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.
|