@atom-forge/brickee 0.0.0-stage → 0.0.1
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 +24 -0
- package/LICENSE +21 -0
- package/README-AI.md +283 -0
- package/README.md +41 -2
- package/dist/Editor.svelte +1968 -0
- package/dist/Editor.svelte.d.ts +22 -0
- package/dist/ImagePicker.svelte +131 -0
- package/dist/ImagePicker.svelte.d.ts +13 -0
- package/dist/LinkPrompt.svelte +42 -0
- package/dist/LinkPrompt.svelte.d.ts +8 -0
- package/dist/PluginHost.svelte +41 -0
- package/dist/PluginHost.svelte.d.ts +13 -0
- package/dist/Preview.svelte +186 -0
- package/dist/Preview.svelte.d.ts +35 -0
- package/dist/PropsBar.svelte +395 -0
- package/dist/PropsBar.svelte.d.ts +20 -0
- package/dist/dom.d.ts +29 -0
- package/dist/dom.js +224 -0
- package/dist/i18n.d.ts +7 -0
- package/dist/i18n.js +18 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +16 -0
- package/dist/kit/Empty.svelte +24 -0
- package/dist/kit/Empty.svelte.d.ts +8 -0
- package/dist/kit/Frame.svelte +76 -0
- package/dist/kit/Frame.svelte.d.ts +15 -0
- package/dist/kit/util.d.ts +13 -0
- package/dist/kit/util.js +28 -0
- package/dist/locales/hu.d.ts +2 -0
- package/dist/locales/hu.js +147 -0
- package/dist/markdown.d.ts +25 -0
- package/dist/markdown.js +219 -0
- package/dist/media.d.ts +17 -0
- package/dist/media.js +12 -0
- package/dist/model.d.ts +21 -0
- package/dist/model.js +45 -0
- package/dist/palette.d.ts +19 -0
- package/dist/palette.js +23 -0
- package/dist/plugins/Gap.svelte +28 -0
- package/dist/plugins/Gap.svelte.d.ts +15 -0
- package/dist/plugins/GapPreview.svelte +17 -0
- package/dist/plugins/GapPreview.svelte.d.ts +5 -0
- package/dist/plugins/Map.svelte +47 -0
- package/dist/plugins/Map.svelte.d.ts +12 -0
- package/dist/plugins/MapPreview.svelte +26 -0
- package/dist/plugins/MapPreview.svelte.d.ts +5 -0
- package/dist/plugins/Table.svelte +563 -0
- package/dist/plugins/Table.svelte.d.ts +28 -0
- package/dist/plugins/TablePreview.svelte +62 -0
- package/dist/plugins/TablePreview.svelte.d.ts +5 -0
- package/dist/plugins/YouTube.svelte +52 -0
- package/dist/plugins/YouTube.svelte.d.ts +8 -0
- package/dist/plugins/YouTubePreview.svelte +76 -0
- package/dist/plugins/YouTubePreview.svelte.d.ts +5 -0
- package/dist/plugins/index.d.ts +20 -0
- package/dist/plugins/index.js +94 -0
- package/dist/plugins/util.d.ts +3 -0
- package/dist/plugins/util.js +8 -0
- package/dist/schema.d.ts +11 -0
- package/dist/schema.js +25 -0
- package/dist/scroll.d.ts +18 -0
- package/dist/scroll.js +57 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.js +43 -0
- package/dist/view.d.ts +6 -0
- package/dist/view.js +11 -0
- package/docs/en/bundled-plugins.md +99 -0
- package/docs/en/documents-and-markdown.md +115 -0
- package/docs/en/editor-and-preview.md +141 -0
- package/docs/en/getting-started.md +81 -0
- package/docs/en/host-contexts.md +275 -0
- package/docs/en/index.md +38 -0
- package/docs/en/recipes-and-pitfalls.md +122 -0
- package/docs/en/server-side.md +95 -0
- package/docs/en/upgrading.md +55 -0
- package/docs/en/writing-plugins.md +305 -0
- package/docs/hu/bundled-plugins.md +99 -0
- package/docs/hu/documents-and-markdown.md +115 -0
- package/docs/hu/editor-and-preview.md +141 -0
- package/docs/hu/getting-started.md +81 -0
- package/docs/hu/host-contexts.md +275 -0
- package/docs/hu/index.md +38 -0
- package/docs/hu/recipes-and-pitfalls.md +122 -0
- package/docs/hu/server-side.md +95 -0
- package/docs/hu/upgrading.md +55 -0
- package/docs/hu/writing-plugins.md +305 -0
- package/package.json +110 -5
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Host contexts
|
|
2
|
+
|
|
3
|
+
[← Previous](writing-plugins.md) · [Contents](index.md) · [Next →](server-side.md)
|
|
4
|
+
|
|
5
|
+
The editor has no props for application concepts. What a host knows — its images, the page width it is showing, 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
|
+
|
|
7
|
+
| Context | Provide | Read | Carries |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| Media | `media` prop, or `provideMedia(() => list)` | `useMedia()` | The images blocks can pick from. |
|
|
10
|
+
| View | `provideView(() => key)` | `useView()` | The view per-view fields edit. |
|
|
11
|
+
| Text | `messages` prop, or `provideText(() => messages)` | `useText()` | UI text in the host's language. |
|
|
12
|
+
|
|
13
|
+
Nested editors and previews inherit all three from their parent.
|
|
14
|
+
|
|
15
|
+
## Media
|
|
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.
|
|
138
|
+
|
|
139
|
+
## Per-view values
|
|
140
|
+
|
|
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:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import type { BlockPlugin } from '@atom-forge/brickee';
|
|
145
|
+
import { gap } from '@atom-forge/brickee/plugins';
|
|
146
|
+
import { photo } from './photo';
|
|
147
|
+
|
|
148
|
+
export const VIEWS = [
|
|
149
|
+
{ key: 'desktop', label: 'Desktop' },
|
|
150
|
+
{ key: 'tablet', label: 'Tablet' },
|
|
151
|
+
{ key: 'phone', label: 'Phone' }
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
/** The same plugin, with one of its fields set per view. */
|
|
155
|
+
const perView = (plugin: BlockPlugin, key: string): BlockPlugin => ({
|
|
156
|
+
...plugin,
|
|
157
|
+
fields: plugin.fields?.map((f) => (f.key === key ? { ...f, views: VIEWS } : f))
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export const plugins = [perView(photo, 'ratio'), perView(gap, 'lines')];
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Which views exist and which one is shown is the host's business. The host tells the editor the current view with `provideView`, typically from a width switch beside the editor:
|
|
164
|
+
|
|
165
|
+
```svelte
|
|
166
|
+
<script lang="ts">
|
|
167
|
+
import { Editor, provideView, type StoredBlock } from '@atom-forge/brickee';
|
|
168
|
+
import { plugins } from './blocks';
|
|
169
|
+
|
|
170
|
+
let { value = $bindable(), view }: { value: StoredBlock[]; view: string } = $props();
|
|
171
|
+
provideView(() => view);
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<Editor bind:value {plugins} />
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
How the values behave:
|
|
178
|
+
|
|
179
|
+
- The first view is the **base**. Each other view, while it has no value of its own, takes the value of the view before it; the field then shows the value marked as inherited.
|
|
180
|
+
- The field edits the value of the current view and shows that view's label beside its own. Without `provideView`, or with a key that is not in `views`, it edits the base value.
|
|
181
|
+
- Stored: only a base value → stored alone, as a plain value (`"ratio": "56"`); own values for other views → an object keyed by view (`"ratio": { "desktop": "56", "phone": "100" }`). Documents written before a field became per-view stay valid.
|
|
182
|
+
|
|
183
|
+
A preview reads them with `viewValues(value, keys)` — the value for each view, inheritance applied — or just the base with `baseValue(value)`. This version of the photo preview gives the browser one aspect ratio per width through CSS variables:
|
|
184
|
+
|
|
185
|
+
```svelte
|
|
186
|
+
<script lang="ts">
|
|
187
|
+
import { findMedia, useMedia, viewValues, type PluginPreviewProps } from '@atom-forge/brickee';
|
|
188
|
+
import { aspect, type PhotoProps } from './PhotoView.svelte';
|
|
189
|
+
|
|
190
|
+
let { props }: PluginPreviewProps<PhotoProps> = $props();
|
|
191
|
+
const media = useMedia();
|
|
192
|
+
const item = $derived(findMedia(media(), props.image));
|
|
193
|
+
// desktop, tablet, phone: a CSS aspect-ratio each ('auto': the image's own shape)
|
|
194
|
+
const r = $derived(viewValues(props.ratio, ['desktop', 'tablet', 'phone']).map((v) => aspect(v) ?? 'auto'));
|
|
195
|
+
</script>
|
|
196
|
+
|
|
197
|
+
{#if item}
|
|
198
|
+
<img src={item.url} alt={props.alt ?? ''} style:--r-desktop={r[0]} style:--r-tablet={r[1]} style:--r-phone={r[2]} />
|
|
199
|
+
{/if}
|
|
200
|
+
|
|
201
|
+
<style>
|
|
202
|
+
img {
|
|
203
|
+
width: 100%;
|
|
204
|
+
object-fit: cover;
|
|
205
|
+
aspect-ratio: var(--r-phone);
|
|
206
|
+
}
|
|
207
|
+
@media (min-width: 640px) {
|
|
208
|
+
img {
|
|
209
|
+
aspect-ratio: var(--r-tablet);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
@media (min-width: 1024px) {
|
|
213
|
+
img {
|
|
214
|
+
aspect-ratio: var(--r-desktop);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
> **Sliders only**
|
|
221
|
+
>
|
|
222
|
+
> `views` is supported on `slider` fields. Other field kinds ignore it.
|
|
223
|
+
|
|
224
|
+
## UI text
|
|
225
|
+
|
|
226
|
+
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:
|
|
227
|
+
|
|
228
|
+
```svelte
|
|
229
|
+
<script lang="ts">
|
|
230
|
+
import { Editor, type StoredBlock } from '@atom-forge/brickee';
|
|
231
|
+
import { PLUGINS } from '@atom-forge/brickee/plugins';
|
|
232
|
+
import { hu } from '@atom-forge/brickee/locales/hu';
|
|
233
|
+
|
|
234
|
+
let value = $state<StoredBlock[]>([]);
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<Editor bind:value plugins={PLUGINS} messages={hu} />
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`@atom-forge/brickee/locales/hu` exports `hu`, the Hungarian text of the editor and of every bundled plugin. Extend it with the text of your own plugins and fields:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import type { Messages } from '@atom-forge/brickee';
|
|
244
|
+
import { hu } from '@atom-forge/brickee/locales/hu';
|
|
245
|
+
|
|
246
|
+
export const messages: Messages = {
|
|
247
|
+
...hu,
|
|
248
|
+
Button: 'Gomb',
|
|
249
|
+
'A link as a button': 'Egy link gombként',
|
|
250
|
+
Desktop: 'Asztali',
|
|
251
|
+
Phone: 'Telefon'
|
|
252
|
+
};
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
What the editor translates by itself:
|
|
256
|
+
|
|
257
|
+
- the built-in and plugin `label`, `hint` and `keys` (the slash menu searches the English and the translated words);
|
|
258
|
+
- the parts of a plugin's `tag`, split at ` · `;
|
|
259
|
+
- field labels, placeholders, option labels, `none` labels and view labels;
|
|
260
|
+
- the text of `Empty`, and the editor's own interface.
|
|
261
|
+
|
|
262
|
+
Text a plugin renders itself — in its view or preview — goes through `useText()`:
|
|
263
|
+
|
|
264
|
+
```svelte
|
|
265
|
+
<script lang="ts">
|
|
266
|
+
import { useText, type PluginPreviewProps } from '@atom-forge/brickee';
|
|
267
|
+
|
|
268
|
+
let { props }: PluginPreviewProps = $props();
|
|
269
|
+
const tr = useText();
|
|
270
|
+
</script>
|
|
271
|
+
|
|
272
|
+
<p>{tr('Map: {place}', { place: String(props.q ?? '') })}</p>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`{name}` placeholders are filled from the second argument. Outside components, `translator(messages)` returns the same kind of function. A text missing from `messages` stays English.
|
package/docs/en/index.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Brickee
|
|
2
|
+
|
|
3
|
+
`@atom-forge/brickee` is a block editor for Svelte 5. Its core is a markdown block editor — paragraphs, headings, lists, to-dos, quotes, code, dividers — and every other block is a **plugin**: a block with an editor view, a site preview, its fields, and optionally a markdown form. Documents are stored as JSON.
|
|
4
|
+
|
|
5
|
+
These pages are derived from the [Brickee book](https://books.atom-forge.eu), which is the source of truth. A Hungarian translation is in [`../hu/index.md`](../hu/index.md); a compact reference for coding agents is in [`README-AI.md`](../../README-AI.md).
|
|
6
|
+
|
|
7
|
+
The package is built in three layers:
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart TB
|
|
11
|
+
core["Core: markdown blocks, Editor, Preview, stored form, kit"]
|
|
12
|
+
bundled["Bundled plugins: table, YouTube, map, gap"]
|
|
13
|
+
own["Your plugins: the same BlockPlugin shape"]
|
|
14
|
+
core --> bundled
|
|
15
|
+
core --> own
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **Core** (`@atom-forge/brickee`) — the markdown blocks, the `Editor` and the `Preview`, the stored form and its markdown conversions, and the **kit** plugins are written with. Without plugins it is a plain markdown block editor.
|
|
19
|
+
- **Bundled plugins** (`@atom-forge/brickee/plugins`) — plugins like any other, written only with what the core exports. You opt in to the ones you need.
|
|
20
|
+
- **Your plugins** — the same `BlockPlugin` shape, living in your application.
|
|
21
|
+
|
|
22
|
+
The editor itself knows nothing about your application. Whatever an app needs — its images, its page widths, its language — reaches the plugins through **contexts** (media, view, text) or through plugin **fields**, never through special editor props.
|
|
23
|
+
|
|
24
|
+
## Contents
|
|
25
|
+
|
|
26
|
+
1. [Getting started](getting-started.md) — the three layers, installation, host setup, and a first editor with its preview.
|
|
27
|
+
2. [The editor and the preview](editor-and-preview.md) — the `Editor` and `Preview` props, the palette, the editing features, and scroll sync.
|
|
28
|
+
3. [Documents and markdown](documents-and-markdown.md) — the stored form (`StoredBlock`), the built-in blocks, and converting from and to markdown.
|
|
29
|
+
4. [Bundled plugins](bundled-plugins.md) — what `@atom-forge/brickee/plugins` contains, and how to override a preview or add fields.
|
|
30
|
+
5. [Writing plugins](writing-plugins.md) — the `BlockPlugin` anatomy, fields, views and previews, containers, markdown forms, and the kit.
|
|
31
|
+
6. [Host contexts](host-contexts.md) — media, per-view values, and UI text.
|
|
32
|
+
7. [On the server](server-side.md) — schema validation, plain text, and gated content.
|
|
33
|
+
8. [Recipes and pitfalls](recipes-and-pitfalls.md) — complete patterns and the mistakes to avoid.
|
|
34
|
+
9. [Upgrading](upgrading.md) — what to change in your code when you move to a newer version.
|
|
35
|
+
|
|
36
|
+
The book also has an AI Notes chapter: instructions to copy into the agent instruction file of a project that uses the package.
|
|
37
|
+
|
|
38
|
+
Svelte examples are complete components unless they say otherwise. Application values such as the list of images or the current page width stand for whatever your application provides.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Recipes and pitfalls
|
|
2
|
+
|
|
3
|
+
[← Previous](server-side.md) · [Contents](index.md) · [Next →](upgrading.md)
|
|
4
|
+
|
|
5
|
+
## One place for the block setup
|
|
6
|
+
|
|
7
|
+
Keep the plugins, palettes and messages of an application in one module, and wrap the editor in a component of your own. Pages then use the wrapper and never repeat the setup.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// lib/blocks.ts
|
|
11
|
+
import type { BlockPlugin, Messages } from '@atom-forge/brickee';
|
|
12
|
+
import * as base from '@atom-forge/brickee/plugins';
|
|
13
|
+
import { hu } from '@atom-forge/brickee/locales/hu';
|
|
14
|
+
import SiteYouTube from './SiteYouTube.svelte';
|
|
15
|
+
import { photo } from './photo';
|
|
16
|
+
import { button } from './button';
|
|
17
|
+
|
|
18
|
+
const withPreview = (plugin: BlockPlugin, preview: BlockPlugin['preview']): BlockPlugin => ({ ...plugin, preview });
|
|
19
|
+
|
|
20
|
+
export const plugins: BlockPlugin[] = [
|
|
21
|
+
withPreview(base.youtube, SiteYouTube),
|
|
22
|
+
base.table,
|
|
23
|
+
base.map,
|
|
24
|
+
base.gap,
|
|
25
|
+
photo,
|
|
26
|
+
button
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** The full palette, for editors of the site's own pages. */
|
|
30
|
+
export const PAGE_PALETTE = ['md:p', 'md:h2', 'md:h3', 'md:ul', 'md:ol', 'md:quote', 'md:hr', ...plugins.map((p) => p.type)];
|
|
31
|
+
|
|
32
|
+
/** A smaller one, for user comments. */
|
|
33
|
+
export const COMMENT_PALETTE = ['md:p', 'md:ul', 'md:ol', 'md:quote', 'photo'];
|
|
34
|
+
|
|
35
|
+
export const messages: Messages = { ...hu, Button: 'Gomb', 'A link as a button': 'Egy link gombként' };
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```svelte
|
|
39
|
+
<!-- lib/BlockEditor.svelte -->
|
|
40
|
+
<script lang="ts">
|
|
41
|
+
import { Editor, provideView, type MediaItem, type StoredBlock } from '@atom-forge/brickee';
|
|
42
|
+
import { messages, PAGE_PALETTE, plugins } from './blocks';
|
|
43
|
+
|
|
44
|
+
let {
|
|
45
|
+
value = $bindable(),
|
|
46
|
+
media = [],
|
|
47
|
+
palette = PAGE_PALETTE,
|
|
48
|
+
view
|
|
49
|
+
}: { value: StoredBlock[]; media?: MediaItem[]; palette?: string[]; view?: string } = $props();
|
|
50
|
+
|
|
51
|
+
provideView(() => view);
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<Editor bind:value {plugins} {palette} {media} {messages} />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Render the published page with `<Preview blocks={doc} {plugins} {media} {messages} />` and the same `plugins`, so a block looks the same in the editor's preview and on the site.
|
|
58
|
+
|
|
59
|
+
## Copying the whole document as markdown
|
|
60
|
+
|
|
61
|
+
Copying selected blocks already puts markdown on the clipboard. For a "Copy as markdown" button for the whole document:
|
|
62
|
+
|
|
63
|
+
```svelte
|
|
64
|
+
<script lang="ts">
|
|
65
|
+
import { fromStored, toMarkdown, type MediaItem, type StoredBlock } from '@atom-forge/brickee';
|
|
66
|
+
import { plugins } from './blocks';
|
|
67
|
+
|
|
68
|
+
let { doc, media = [] }: { doc: StoredBlock[]; media?: MediaItem[] } = $props();
|
|
69
|
+
|
|
70
|
+
// Runs in a click handler, so the browser-only toMarkdown is safe here.
|
|
71
|
+
const copy = () => navigator.clipboard.writeText(toMarkdown(doc.map(fromStored), { plugins, media }));
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<button type="button" onclick={copy}>Copy as markdown</button>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Starting a document from markdown
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { fromMarkdown, toStored, type StoredBlock } from '@atom-forge/brickee';
|
|
81
|
+
|
|
82
|
+
// In the browser: toStored reads the inline HTML through the DOM.
|
|
83
|
+
export const fromText = (text: string): StoredBlock[] => fromMarkdown(text).map(toStored);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Remember that every non-empty line becomes its own block.
|
|
87
|
+
|
|
88
|
+
## Developing against a local copy
|
|
89
|
+
|
|
90
|
+
When you link a local checkout of the package into an application (`bun link`), the linked folder brings its own `node_modules`. Make Vite use the application's single copy of the shared packages, otherwise Svelte contexts — the popup manager, the theme — are not found across the two copies:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// vite.config.ts of the application
|
|
94
|
+
export default defineConfig({
|
|
95
|
+
plugins: [sveltekit()],
|
|
96
|
+
resolve: {
|
|
97
|
+
dedupe: ['svelte', '@atom-forge/ui', '@lucide/svelte', 'zod']
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The application reads the package's built `dist/`, so rebuild the package after each change.
|
|
103
|
+
|
|
104
|
+
## Pitfalls
|
|
105
|
+
|
|
106
|
+
| Symptom | Cause | Fix |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| Block fields and popups look unstyled | The `@atom-forge/ui` theme CSS or its Tailwind `@source` is missing | Set up `@atom-forge/ui` as its documentation describes and render the app beneath its `Root`. |
|
|
109
|
+
| Popups do not open, or "context not found" errors with a linked package | Two copies of `svelte` or `@atom-forge/ui` | Add them to Vite's `resolve.dedupe`. |
|
|
110
|
+
| A plugin block shows "Unknown block" | Its plugin is missing from `plugins` | Pass the same plugins to every editor and preview of a document type. |
|
|
111
|
+
| A block is missing from `Preview` | Its plugin has no `preview` | Give the plugin a `preview` (the bundled ones have a default), or render the block with your own renderer. |
|
|
112
|
+
| A plugin block is missing from the site | The preview got other `plugins`, or the image id is not in `media` | Use the same plugin list and media for the editor and the preview. |
|
|
113
|
+
| A box cannot be put inside another box | Nested editors go one level deep | Design containers to hold content blocks, not other containers. |
|
|
114
|
+
| A block is not offered inside a container | Its `accepts` or the host `palette` leaves it out | Add the palette id to `accepts` (or use `'md:*'` for every built-in). |
|
|
115
|
+
| A unique block appears twice | The schema does not enforce `unique` | Check it on the server if your application relies on it. |
|
|
116
|
+
| Gated content visible to everyone | `gate` only marks the blocks | Cut the document on the server before rendering. |
|
|
117
|
+
| `document is not defined` | `toMarkdown` or `toStored` ran during SSR | Call them in the browser only. |
|
|
118
|
+
| A colour or link from props breaks the page | Props are user input | Use `safeColor`, check links, never use `{@html}` with props. |
|
|
119
|
+
| Overridden plugin loses its blocks | Its `type` was changed | Keep `type` when spreading a plugin. |
|
|
120
|
+
| `bind:blocks` error in a custom view | `props` or `blocks` not declared bindable | Declare both with `$bindable()`. |
|
|
121
|
+
| The value is never an empty array | The editor shows (and writes) one empty paragraph for an empty document | Treat a document with only empty paragraphs as empty, for example with `blocksToPlainText(doc).trim() === ''`. |
|
|
122
|
+
| The preview does not follow the editor's scrolling | The actions are not on the boxes that scroll | Put `use:sync.source` / `use:sync.target` on the `overflow` boxes themselves. |
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# On the server
|
|
2
|
+
|
|
3
|
+
[← Previous](host-contexts.md) · [Contents](index.md) · [Next →](recipes-and-pitfalls.md)
|
|
4
|
+
|
|
5
|
+
A stored document is JSON sent by the browser, so the server must check it before saving. Two entry points have no Svelte dependency and are meant for server code:
|
|
6
|
+
|
|
7
|
+
- `@atom-forge/brickee/model` — `StoredBlock`, `MD`, `MAX_INDENT`, `MAX_DEPTH`, `isMd`, `acceptsId`, `inlineToPlainText`, `blocksToPlainText`;
|
|
8
|
+
- `@atom-forge/brickee/schema` — `blocksSchema` and `blocksSchemaFor`, built with `zod` (an optional peer dependency: install it to use this entry point).
|
|
9
|
+
|
|
10
|
+
## Validating a document
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// content.server.ts
|
|
14
|
+
import { blocksSchemaFor } from '@atom-forge/brickee/schema';
|
|
15
|
+
import { plugins } from '$lib/blocks';
|
|
16
|
+
|
|
17
|
+
const contentSchema = blocksSchemaFor(plugins);
|
|
18
|
+
|
|
19
|
+
export function parseContent(input: unknown) {
|
|
20
|
+
const result = contentSchema.safeParse(input);
|
|
21
|
+
if (!result.success) throw new Error('Invalid content');
|
|
22
|
+
return result.data; // StoredBlock[]
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`blocksSchema` checks the shape of any document; `blocksSchemaFor(plugins)` adds the containers' `accepts` lists. It needs only `type` and `accepts` of each plugin, so a server that does not want to import the plugin components can pass those alone:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const contentSchema = blocksSchemaFor([
|
|
30
|
+
{ type: 'note', accepts: ['md:*', 'youtube'] },
|
|
31
|
+
{ type: 'youtube' }
|
|
32
|
+
]);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
What the schema checks:
|
|
36
|
+
|
|
37
|
+
| Rule | Limit |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Block keys | `type`, `indent`, `text`, `props`, `blocks` only (strict objects). |
|
|
40
|
+
| `type` | `^[a-z][a-z0-9:-]{0,39}$` |
|
|
41
|
+
| `indent` | An integer, 0–6. |
|
|
42
|
+
| `text` | At most 100 000 characters. |
|
|
43
|
+
| `props` | Keys up to 60 characters, JSON values. |
|
|
44
|
+
| `blocks` | At most 500 nested blocks per container, one level deep only. |
|
|
45
|
+
| Document | At most 2000 top-level blocks and 1 000 000 characters as JSON. |
|
|
46
|
+
| `accepts` (`blocksSchemaFor`) | Every nested block is one its container takes. |
|
|
47
|
+
|
|
48
|
+
What it does **not** check: whether a type is known, whether a unique block appears once, and what the props contain. Add your own refinements for the rules your application relies on — for example, to reject unknown block types:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import type { StoredBlock } from '@atom-forge/brickee/model';
|
|
52
|
+
import { blocksSchemaFor } from '@atom-forge/brickee/schema';
|
|
53
|
+
import { plugins } from '$lib/blocks';
|
|
54
|
+
|
|
55
|
+
const BUILT_IN = ['md:p', 'md:h1', 'md:h2', 'md:h3', 'md:h4', 'md:h5', 'md:h6', 'md:ul', 'md:ol', 'md:todo', 'md:quote', 'md:code', 'md:hr'];
|
|
56
|
+
const known = new Set([...BUILT_IN, ...plugins.map((p) => p.type)]);
|
|
57
|
+
const allKnown = (list: StoredBlock[]): boolean => list.every((b) => known.has(b.type) && allKnown(b.blocks ?? []));
|
|
58
|
+
|
|
59
|
+
export const contentSchema = blocksSchemaFor(plugins).refine(allKnown, 'Unknown block type.');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
> **Shape, not safety**
|
|
63
|
+
>
|
|
64
|
+
> The schema keeps documents well-formed and bounded; it does not make props safe to render. The renderers escape every text and keep links to safe schemes, and each plugin preview must sanitize its own props (see [Writing plugins](writing-plugins.md)).
|
|
65
|
+
|
|
66
|
+
## Plain text
|
|
67
|
+
|
|
68
|
+
`blocksToPlainText(blocks)` collects every text of a document in order, separated by blank lines: block texts with their marks and link targets removed, string props that contain whitespace (titles, captions — not ids or colours), table rows, and nested blocks. Use it for search indexes, leads and meta descriptions. `inlineToPlainText(md)` does the same for a single inline markdown string.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { blocksToPlainText, type StoredBlock } from '@atom-forge/brickee/model';
|
|
72
|
+
|
|
73
|
+
export const description = (doc: StoredBlock[]) => blocksToPlainText(doc).slice(0, 160);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Rendering
|
|
77
|
+
|
|
78
|
+
`Preview` is an ordinary Svelte component and can be server-rendered; the core renderer and the bundled previews use no browser APIs. Only the markdown export path (`toMarkdown`, `toStored`) needs the browser.
|
|
79
|
+
|
|
80
|
+
## Gated content
|
|
81
|
+
|
|
82
|
+
A gate block (a plugin with `gate: true`, such as a supporters-only divider) only makes the preview mark the blocks after it. To keep the gated part from readers who may not see it, cut the document on the server before it reaches the page:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import type { StoredBlock } from '@atom-forge/brickee/model';
|
|
86
|
+
|
|
87
|
+
/** The document up to and including the first gate block, for readers without access. */
|
|
88
|
+
export function publicPart(doc: StoredBlock[], gateTypes: string[], canRead: boolean): StoredBlock[] {
|
|
89
|
+
if (canRead) return doc;
|
|
90
|
+
const at = doc.findIndex((b) => gateTypes.includes(b.type));
|
|
91
|
+
return at < 0 ? doc : doc.slice(0, at + 1);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Call it with your gate plugin types (for example `['paywall']`) and the reader's access. Readers without access see the content up to the gate block and its teaser; readers with access get the whole document, and the preview marks the gated part for them.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Upgrading
|
|
2
|
+
|
|
3
|
+
[← Previous](recipes-and-pitfalls.md) · [Contents](index.md)
|
|
4
|
+
|
|
5
|
+
This chapter lists the changes that may require action when you move to a newer version of `@atom-forge/brickee`. Each section covers one version step, newest first, and each change ends with a **What to do:** paragraph.
|
|
6
|
+
|
|
7
|
+
Changes that need no action from you — new exports, fixes, documentation updates — are listed only in the changelog (`CHANGELOG.md`, shipped with the package).
|
|
8
|
+
|
|
9
|
+
> **No migration needed yet**
|
|
10
|
+
>
|
|
11
|
+
> `@atom-forge/brickee` has not had a release that requires changes in code using it. The section below is only for applications that used the editor before it became this package.
|
|
12
|
+
|
|
13
|
+
## From the `block-editor` workspace package to Unreleased
|
|
14
|
+
|
|
15
|
+
The editor started as `block-editor`, a workspace package inside an application. It moved here as `@atom-forge/brickee` with the same stored form, so saved documents need no conversion. The import specifiers changed, the plugin kit moved from the plugins entry point into the core, and the site-specific plugins stayed with the application.
|
|
16
|
+
|
|
17
|
+
### Package name and entry points
|
|
18
|
+
|
|
19
|
+
| Before | After |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `block-editor` | `@atom-forge/brickee` |
|
|
22
|
+
| `block-editor/plugins` | `@atom-forge/brickee/plugins` |
|
|
23
|
+
| `block-editor/locales/hu` | `@atom-forge/brickee/locales/hu` |
|
|
24
|
+
|
|
25
|
+
**What to do:** replace the dependency (`"block-editor": "workspace:*"` → `"@atom-forge/brickee"`) and rename the imports. Install `@atom-forge/ui` and `@lucide/svelte` as dependencies of the application if they are not already, and `zod` if you use the schema.
|
|
26
|
+
|
|
27
|
+
### The kit lives in the core
|
|
28
|
+
|
|
29
|
+
`Frame`, `Empty` and the plugin helpers are exported from the core entry point. `safeColor`, `viewValues`, `baseValue` and `lines` are no longer exported from the plugins entry point.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// Before
|
|
33
|
+
import { safeColor, viewValues } from 'block-editor/plugins';
|
|
34
|
+
|
|
35
|
+
// After
|
|
36
|
+
import { safeColor, viewValues } from '@atom-forge/brickee';
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**What to do:** import `Frame`, `Empty`, `safeColor`, `viewValues`, `baseValue`, `lines`, `mdText`, `mdJoin` and `mdQuote` from `@atom-forge/brickee`. The helpers of the bundled plugins that remain (`youtubeId`, `mapSrc`, `gapLines`, `tableHeads`…) stay in `@atom-forge/brickee/plugins`.
|
|
40
|
+
|
|
41
|
+
### Site-specific plugins are no longer in the package
|
|
42
|
+
|
|
43
|
+
The package now bundles only four general-purpose plugins: `table`, `youtube`, `map` and `gap` (`PLUGINS` holds these four). The other plugins of `block-editor/plugins` and their helpers were application-specific and are not part of `@atom-forge/brickee`:
|
|
44
|
+
|
|
45
|
+
| Removed | Kind |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `richBox`, `feature`, `sideBox`, `callout`, `spoiler`, `faq`, `imageText` | container plugins |
|
|
48
|
+
| `image`, `gallery`, `tweet`, `form`, `banner`, `locked` | plugins |
|
|
49
|
+
| `CALLOUTS`, `calloutTone`, `CalloutTone`, `questions`, `formEmbed`, `formHeight` | their helpers |
|
|
50
|
+
| `postId`, `userName`, `RATIO_MIN`, `RATIO_MAX`, `ratioPercent`, `ratio` | helpers of the former `plugins/util` |
|
|
51
|
+
| `ImageProps`, `GalleryProps`, `CalloutProps` and the other props types of these plugins | types |
|
|
52
|
+
|
|
53
|
+
The stored form is unchanged: documents that contain these block types stay valid, as long as the application still passes plugins with the same `type` values to the editor and the preview.
|
|
54
|
+
|
|
55
|
+
**What to do:** if the application uses any of these plugins, keep its own copy of them (their views, previews and helpers) in the application, written against the core kit, and add them to its plugin list with the same `type`. Import the four remaining plugins and their helpers from `@atom-forge/brickee/plugins` as before. Add the UI text of the copied plugins to your `messages`: `@atom-forge/brickee/locales/hu` no longer contains it.
|