@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
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:
|
package/docs/en/index.md
CHANGED
|
@@ -19,7 +19,7 @@ flowchart TB
|
|
|
19
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
20
|
- **Your plugins** — the same `BlockPlugin` shape, living in your application.
|
|
21
21
|
|
|
22
|
-
The editor itself knows nothing about your application. Whatever an app needs — its
|
|
22
|
+
The editor itself knows nothing about your application. Whatever an app needs — its page widths, its code languages, its language — reaches the plugins through **contexts** (view, code languages, text) or through plugin **fields**, never through special editor props. Images are no exception: an image block is a plugin of your own, which stores, picks and resolves its images the way your application does.
|
|
23
23
|
|
|
24
24
|
## Contents
|
|
25
25
|
|
|
@@ -27,12 +27,12 @@ The editor itself knows nothing about your application. Whatever an app needs
|
|
|
27
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
28
|
3. [Documents and markdown](documents-and-markdown.md) — the stored form (`StoredBlock`), the built-in blocks, and converting from and to markdown.
|
|
29
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) —
|
|
32
|
-
7. [On the server](server-side.md) — schema validation
|
|
30
|
+
5. [Writing plugins](writing-plugins.md) — the `BlockPlugin` anatomy, fields, views and previews, containers, markdown forms, image blocks, and the kit.
|
|
31
|
+
6. [Host contexts](host-contexts.md) — per-view values, code languages, and UI text.
|
|
32
|
+
7. [On the server](server-side.md) — schema validation and plain text.
|
|
33
33
|
8. [Recipes and pitfalls](recipes-and-pitfalls.md) — complete patterns and the mistakes to avoid.
|
|
34
34
|
9. [Upgrading](upgrading.md) — what to change in your code when you move to a newer version.
|
|
35
35
|
|
|
36
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
37
|
|
|
38
|
-
Svelte examples are complete components unless they say otherwise. Application values such as the
|
|
38
|
+
Svelte examples are complete components unless they say otherwise. Application values such as the current page width stand for whatever your application provides.
|
|
@@ -38,23 +38,22 @@ export const messages: Messages = { ...hu, Button: 'Gomb', 'A link as a button':
|
|
|
38
38
|
```svelte
|
|
39
39
|
<!-- lib/BlockEditor.svelte -->
|
|
40
40
|
<script lang="ts">
|
|
41
|
-
import { Editor, provideView, type
|
|
41
|
+
import { Editor, provideView, type StoredBlock } from '@atom-forge/brickee';
|
|
42
42
|
import { messages, PAGE_PALETTE, plugins } from './blocks';
|
|
43
43
|
|
|
44
44
|
let {
|
|
45
45
|
value = $bindable(),
|
|
46
|
-
media = [],
|
|
47
46
|
palette = PAGE_PALETTE,
|
|
48
47
|
view
|
|
49
|
-
}: { value: StoredBlock[];
|
|
48
|
+
}: { value: StoredBlock[]; palette?: string[]; view?: string } = $props();
|
|
50
49
|
|
|
51
50
|
provideView(() => view);
|
|
52
51
|
</script>
|
|
53
52
|
|
|
54
|
-
<Editor bind:value {plugins} {palette} {
|
|
53
|
+
<Editor bind:value {plugins} {palette} {messages} />
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
Render the published page with `<Preview blocks={doc} {plugins} {
|
|
56
|
+
Render the published page with `<Preview blocks={doc} {plugins} {messages} />` and the same `plugins`, so a block looks the same in the editor's preview and on the site.
|
|
58
57
|
|
|
59
58
|
## Copying the whole document as markdown
|
|
60
59
|
|
|
@@ -62,13 +61,13 @@ Copying selected blocks already puts markdown on the clipboard. For a "Copy as m
|
|
|
62
61
|
|
|
63
62
|
```svelte
|
|
64
63
|
<script lang="ts">
|
|
65
|
-
import { fromStored, toMarkdown, type
|
|
64
|
+
import { fromStored, toMarkdown, type StoredBlock } from '@atom-forge/brickee';
|
|
66
65
|
import { plugins } from './blocks';
|
|
67
66
|
|
|
68
|
-
let { doc
|
|
67
|
+
let { doc }: { doc: StoredBlock[] } = $props();
|
|
69
68
|
|
|
70
69
|
// Runs in a click handler, so the browser-only toMarkdown is safe here.
|
|
71
|
-
const copy = () => navigator.clipboard.writeText(toMarkdown(doc.map(fromStored), { plugins
|
|
70
|
+
const copy = () => navigator.clipboard.writeText(toMarkdown(doc.map(fromStored), { plugins }));
|
|
72
71
|
</script>
|
|
73
72
|
|
|
74
73
|
<button type="button" onclick={copy}>Copy as markdown</button>
|
|
@@ -83,7 +82,7 @@ import { fromMarkdown, toStored, type StoredBlock } from '@atom-forge/brickee';
|
|
|
83
82
|
export const fromText = (text: string): StoredBlock[] => fromMarkdown(text).map(toStored);
|
|
84
83
|
```
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
A paragraph's lines stay one block, with line breaks between them; a blank line starts a new block.
|
|
87
86
|
|
|
88
87
|
## Developing against a local copy
|
|
89
88
|
|
|
@@ -105,15 +104,17 @@ The application reads the package's built `dist/`, so rebuild the package after
|
|
|
105
104
|
|
|
106
105
|
| Symptom | Cause | Fix |
|
|
107
106
|
|---|---|---|
|
|
107
|
+
| The editor's rows, menus or table look unstyled or shrunk | The `@atom-forge/brickee` `dist` folder is not a Tailwind `@source` | Add `@source "../node_modules/@atom-forge/brickee/dist";` beside the `@atom-forge/ui` one. |
|
|
108
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
|
+
| Block handles stick out on the left | The handles are drawn left of the editor's box | Give the element around the editor about 22 px of left padding. |
|
|
109
110
|
| 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
111
|
| 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
112
|
| 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
|
|
113
|
+
| A plugin block is missing from the site | The preview got other `plugins` | Use the same plugin list for the editor and the preview. |
|
|
114
|
+
| An image block renders nothing | Its preview rejects the stored address, or the app's own lookup does not know the stored id | Store addresses the preview accepts (`http(s)` or site-relative); resolve ids from the same data in the editor and on the site. |
|
|
113
115
|
| A box cannot be put inside another box | Nested editors go one level deep | Design containers to hold content blocks, not other containers. |
|
|
114
116
|
| 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
117
|
| 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
118
|
| `document is not defined` | `toMarkdown` or `toStored` ran during SSR | Call them in the browser only. |
|
|
118
119
|
| A colour or link from props breaks the page | Props are user input | Use `safeColor`, check links, never use `{@html}` with props. |
|
|
119
120
|
| Overridden plugin loses its blocks | Its `type` was changed | Keep `type` when spreading a plugin. |
|
package/docs/en/server-side.md
CHANGED
|
@@ -76,20 +76,3 @@ export const description = (doc: StoredBlock[]) => blocksToPlainText(doc).slice(
|
|
|
76
76
|
## Rendering
|
|
77
77
|
|
|
78
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.
|
package/docs/en/upgrading.md
CHANGED
|
@@ -6,11 +6,41 @@ This chapter lists the changes that may require action when you move to a newer
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
-
|
|
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.
|
|
9
|
+
## From 0.0.2 to 0.0.3
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
### The editor needs its `dist` folder as a Tailwind `@source`
|
|
12
|
+
|
|
13
|
+
The editor's own styles moved from scoped CSS to Tailwind classes (with the `@atom-forge/ui` theme's colours) and SCSS. Tailwind 4 does not look into `node_modules`, so without a `@source` for the package the editor's rows, menus, fields bar and table lose their look. The preview is unaffected: its styles still travel with its markup.
|
|
14
|
+
|
|
15
|
+
**What to do:** in the CSS file where you set up `@atom-forge/ui`, add `@source "../node_modules/@atom-forge/brickee/dist";` (with the path from that file) beside the `@atom-forge/ui` one. Nothing changes for pages that only show the `Preview`.
|
|
16
|
+
|
|
17
|
+
### Gate blocks are gone
|
|
18
|
+
|
|
19
|
+
Gating content (a supporters-only or paywall divider) is an application's concept, not the editor's. `BlockPlugin.gate`, the `Preview`'s `markGated` prop, its "For supporters only" section with the `.supporters` and `.supporters-label` classes, and the `For supporters only` text of the Hungarian locale are removed. The `Preview` now renders every block it gets. `unique` stays.
|
|
20
|
+
|
|
21
|
+
**What to do:** remove `gate: true` from your plugin and `markGated` from your previews. Your divider stays an ordinary (unique) plugin block, so stored documents still load. Where you showed the gated part marked, render the document in two parts yourself — `const at = doc.findIndex((b) => b.type === 'divider')`, then `doc.slice(0, at + 1)` and `doc.slice(at + 1)` in two `<Preview>`s inside your own markup — and keep cutting the gated part for readers without access before it reaches the page. Add your own translation for any label you show.
|
|
22
|
+
|
|
23
|
+
## From 0.0.1 to 0.0.2
|
|
24
|
+
|
|
25
|
+
### Images and media are no longer part of the package
|
|
26
|
+
|
|
27
|
+
The package has no notion of images any more. Whether a block stores an image's URL or an id, how the user picks the image, and how the stored value becomes an address are decided by the application's own image plugin (see [Images](writing-plugins.md#images)).
|
|
28
|
+
|
|
29
|
+
| Removed | Was |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `kind: 'image'`, `kind: 'images'` | Field kinds that stored the id (or ids) of a picked media item, with the built-in image picker popup |
|
|
32
|
+
| `media` prop of `Editor` and `Preview` | The images blocks could pick from and refer to |
|
|
33
|
+
| `provideMedia`, `useMedia`, `findMedia`, `MediaItem` | The media context, its lookup and its type |
|
|
34
|
+
| `media` option of `toMarkdown` | `toMarkdown(blocks, { plugins, media })` is now `toMarkdown(blocks, { plugins })` |
|
|
35
|
+
| `ctx.media` in a plugin's `markdown(props, ctx)` | `MarkdownContext` is now `{ inner }` |
|
|
36
|
+
|
|
37
|
+
`PropField['kind']` is now `'text' | 'textarea' | 'color' | 'select' | 'slider'`. Stored documents are unaffected: a block's props keep whatever they stored, ids included.
|
|
38
|
+
|
|
39
|
+
**What to do:** in your image plugins, replace each `image` or `images` field with your own picking — a button in the plugin's view that opens your media dialog or an upload and writes the result into `props`, or a plain `text` field for a URL — and keep the prop keys, so stored documents still load. Remove the `media` prop from your editors and previews, and the `media` option from your `toMarkdown` calls. Where a view, a preview or a `markdown` form turned an id into an address with `useMedia`, `findMedia` or `ctx.media`, look it up in your application's own data instead: a module of your app, or a Svelte context of your own provided above the editor and the preview (`markdown` runs outside components, so it needs module data). Define a `MediaItem`-like type in your app if you still need one.
|
|
40
|
+
|
|
41
|
+
## From the `block-editor` workspace package to 0.0.1
|
|
42
|
+
|
|
43
|
+
This section is only for applications that used the editor before it became this package.
|
|
14
44
|
|
|
15
45
|
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
46
|
|
|
@@ -21,7 +21,6 @@ A plugin is a `BlockPlugin` object plus two Svelte components: a **view** for th
|
|
|
21
21
|
| `accepts` | `string[]` | A container's allowed nested blocks, as palette ids; `'md:*'` is every built-in block. Default: everything the host offers. |
|
|
22
22
|
| `markdown` | `(props: P, ctx: MarkdownContext) => string` | Its markdown form (optional). |
|
|
23
23
|
| `unique` | `boolean` | At most one per document, top level only. |
|
|
24
|
-
| `gate` | `boolean` | The blocks after it are gated; the preview marks them. |
|
|
25
24
|
|
|
26
25
|
`P` is the props type of the plugin; declare it as an interface extending `Record<string, unknown>`.
|
|
27
26
|
|
|
@@ -141,7 +140,7 @@ Register it beside the others:
|
|
|
141
140
|
|
|
142
141
|
> **Props come from users**
|
|
143
142
|
>
|
|
144
|
-
> A plugin's props are whatever was stored — typed by an editor, pasted, or sent to your API by anyone who can save a document. The schema only checks the shape. Every preview must treat props as untrusted: take colours through `safeColor`, check links
|
|
143
|
+
> A plugin's props are whatever was stored — typed by an editor, pasted, or sent to your API by anyone who can save a document. The schema only checks the shape. Every preview must treat props as untrusted: take colours through `safeColor`, check links and image addresses, and clamp numbers. Svelte escapes text in `{...}`; never put a prop into `{@html}`.
|
|
145
144
|
|
|
146
145
|
## Fields
|
|
147
146
|
|
|
@@ -151,9 +150,9 @@ Each `PropField` is one setting on the top of the block. The editor draws them a
|
|
|
151
150
|
|---|---|
|
|
152
151
|
| `key` | The prop it edits. |
|
|
153
152
|
| `label` | Its label (translated through `messages`). |
|
|
154
|
-
| `kind` | `text`, `textarea`, `color`, `select`, `slider
|
|
153
|
+
| `kind` | `text`, `textarea`, `color`, `select`, `slider` or `toggles`. |
|
|
155
154
|
| `placeholder` | Hint of an empty text field; for an optional colour, the colour a click on "none" starts from. |
|
|
156
|
-
| `options` | `{ value, label }[]` for `select`, and for a `slider` that steps through values. An option label may use `{n}`, filled with its value. |
|
|
155
|
+
| `options` | `{ value, label }[]` for `select`, and for a `slider` that steps through values. An option label may use `{n}`, filled with its value. For `toggles`, `{ value, label, icon }[]`: `icon` is the button, `label` its tooltip. |
|
|
157
156
|
| `min`, `max`, `step`, `unit` | A number `slider`: its range, step (default 1) and the unit shown after the value. |
|
|
158
157
|
| `none` | The label of the unset state: a number slider's notch left of `min`, or an optional colour's "none" button. |
|
|
159
158
|
| `views` | Per-view values for a `slider` (see [Per-view values](host-contexts.md#per-view-values)). |
|
|
@@ -169,8 +168,7 @@ How each kind stores its value:
|
|
|
169
168
|
| `slider` with `options` | The value of the option at the slider's position. |
|
|
170
169
|
| `slider` with `min` and `max` | The number as a string (for example `'56'`); the `none` notch removes the key. |
|
|
171
170
|
| `color` | A `#rrggbb` string; with `none`, clearing it removes the key. |
|
|
172
|
-
| `
|
|
173
|
-
| `images` | An array of ids, in picking order; picking a picked one takes it out. |
|
|
171
|
+
| `toggles` | The list of the options switched on, in `options` order (`[]` when none); the bundled table's header and footer are one. |
|
|
174
172
|
|
|
175
173
|
An empty value (`''`, `null` or `undefined`, also when returned by `parse`) removes the key from `props`, so a preview must always handle a missing prop. A field's `parse` is also a good place to normalize input: the bundled YouTube field accepts any YouTube link and stores only the video id.
|
|
176
174
|
|
|
@@ -189,7 +187,7 @@ Declare `props` and `blocks` with `$bindable()` even when the view does not use
|
|
|
189
187
|
|
|
190
188
|
## The preview
|
|
191
189
|
|
|
192
|
-
The preview receives `PluginPreviewProps`: `props`, `blocks` and `plugins`. It is a plain Svelte component that renders the block for the site.
|
|
190
|
+
The preview receives `PluginPreviewProps`: `props`, `blocks` and `plugins`. It is a plain Svelte component that renders the block for the site. Messages and the view are available through the contexts (`useText`, `useView`; see [Host contexts](host-contexts.md)); images are the plugin's own business (see [Images](#images)).
|
|
193
191
|
|
|
194
192
|
## Containers
|
|
195
193
|
|
|
@@ -262,23 +260,13 @@ How nesting works:
|
|
|
262
260
|
- Wrapping blocks into a container from the Turn-into menu, and turning a container into another, is offered only when the container accepts every block going in.
|
|
263
261
|
- `blocksSchemaFor(plugins)` checks `accepts` on the server (see [On the server](server-side.md)).
|
|
264
262
|
|
|
265
|
-
## Unique
|
|
263
|
+
## Unique blocks
|
|
266
264
|
|
|
267
|
-
|
|
268
|
-
- **`gate: true`** — the blocks after the first top-level gate block are gated. The `Preview` renders them inside a section marked "For supporters only" with a dashed line; `markGated={false}` turns the marking off. Nested previews never split.
|
|
269
|
-
|
|
270
|
-
A supporters-only divider is a typical example of both: one per document, with the gated content behind it.
|
|
271
|
-
|
|
272
|
-
> **Gating only marks**
|
|
273
|
-
>
|
|
274
|
-
> `gate` does not hide anything: the preview still renders every block, and the stored document still holds them. Deciding who may read the gated part — and leaving it out of the page for everyone else — is the host's job, on the server (see [Gated content](server-side.md#gated-content)). `unique` is not enforced by the schema either.
|
|
265
|
+
**`unique: true`** — at most one per document, at the top level (a table of contents, for example). Once the document has one, the plugin leaves the slash and Turn-into menus, a second one pasted in falls back to its markdown form, and nested editors never offer it. The schema does not enforce it.
|
|
275
266
|
|
|
276
267
|
## Markdown form
|
|
277
268
|
|
|
278
|
-
`markdown(props, { inner
|
|
279
|
-
|
|
280
|
-
- `inner` — the nested blocks already as markdown (`''` without any);
|
|
281
|
-
- `media` — the document's media, to turn an image id into its URL with `findMedia(media, id)?.url`.
|
|
269
|
+
`markdown(props, { inner })` returns the block as markdown, used when blocks are copied in the editor and by `toMarkdown`. `inner` is the nested blocks already as markdown (`''` without any).
|
|
282
270
|
|
|
283
271
|
Return `''` to leave the block out. Plugins whose output has no meaning as text (an ad slot, the bundled `gap`) simply have no markdown form. Three helpers keep the output well-formed:
|
|
284
272
|
|
|
@@ -288,6 +276,145 @@ Return `''` to leave the block out. Plugins whose output has no meaning as text
|
|
|
288
276
|
| `mdJoin(...parts)` | Joins parts as separate blocks (blank line between), leaving out empty and falsy ones. |
|
|
289
277
|
| `mdQuote(md)` | Prefixes every line with `> `, for a box rendered as a blockquote. |
|
|
290
278
|
|
|
279
|
+
## Taking pasted tables
|
|
280
|
+
|
|
281
|
+
When markdown with a GFM table is pasted into the editor, or imported with `fromMarkdown(text, { plugins })`, the table goes to the first plugin that has `fromTable`. It gets the table and returns the new block's props:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
interface MarkdownTable {
|
|
285
|
+
rows: string[][]; // the cells as inline markdown, row by row; the first row is the header
|
|
286
|
+
align: ('left' | 'center' | 'right')[]; // per column, from the delimiter row (:--, :-:, --:)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fromTable?(table: MarkdownTable): P;
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The bundled `table` has it (`fromTable: ({ rows, align }) => ({ rows, heads: ['top'], align })`), so a table pasted into an editor that offers it becomes a table block. Without such a plugin the table stays text.
|
|
293
|
+
|
|
294
|
+
## Taking files
|
|
295
|
+
|
|
296
|
+
When files are pasted into the editor (a copied image, a screenshot) or dropped on it from outside the page, the first plugin the editor offers that has `fromFiles` and returns props gets one block of its own:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
fromFiles?(files: File[]): P | null | Promise<P | null>;
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Return `null` to leave the files to the next plugin. Uploading and storing them is the plugin's business, so it may return a promise — upload, then return the props with the stored URL or id. A pasted file goes after the block with the caret (or in place of an empty one); a dropped one where the drop line shows.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
fromFiles: async (files) => {
|
|
306
|
+
const file = files.find((f) => f.type.startsWith('image/'));
|
|
307
|
+
if (!file) return null;
|
|
308
|
+
const url = await uploadToMyStorage(file); // your app's upload
|
|
309
|
+
return { url, alt: file.name };
|
|
310
|
+
},
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Images
|
|
314
|
+
|
|
315
|
+
The editor has no notion of images: no image field, no picker, no list of media. Whether a block stores an image's URL or the id of an image in the app's own media library, how the user picks it, and how the stored value becomes an address are the application's decisions, so an image block is an ordinary plugin of the app.
|
|
316
|
+
|
|
317
|
+
A minimal `photo` plugin stores the image's address, typed into a text field, with alt text and a height slider:
|
|
318
|
+
|
|
319
|
+
```svelte
|
|
320
|
+
<!-- PhotoView.svelte -->
|
|
321
|
+
<script lang="ts" module>
|
|
322
|
+
export interface PhotoProps extends Record<string, unknown> {
|
|
323
|
+
/** The image's address. */
|
|
324
|
+
url: string;
|
|
325
|
+
alt: string;
|
|
326
|
+
/** The height in % of the width (20–150), or unset for the image's own shape. */
|
|
327
|
+
ratio?: unknown;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The stored address if it is http(s) or site-relative, else ''. */
|
|
331
|
+
export const imageSrc = (u: unknown) => {
|
|
332
|
+
const s = String(u ?? '').trim();
|
|
333
|
+
return /^(https?:|\/(?!\/))/.test(s) ? s : '';
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** A CSS aspect-ratio from a stored height, or undefined. */
|
|
337
|
+
export const aspect = (v: unknown) => {
|
|
338
|
+
const n = Number(v);
|
|
339
|
+
return n >= 20 && n <= 150 ? `100 / ${n}` : undefined;
|
|
340
|
+
};
|
|
341
|
+
</script>
|
|
342
|
+
|
|
343
|
+
<script lang="ts">
|
|
344
|
+
import { ImageIcon } from '@lucide/svelte';
|
|
345
|
+
import { Empty, type PluginViewProps } from '@atom-forge/brickee';
|
|
346
|
+
|
|
347
|
+
let { props = $bindable(), blocks = $bindable() }: PluginViewProps<PhotoProps> = $props();
|
|
348
|
+
const src = $derived(imageSrc(props.url));
|
|
349
|
+
</script>
|
|
350
|
+
|
|
351
|
+
{#if src}
|
|
352
|
+
<img {src} alt={props.alt} style="max-width: 100%" />
|
|
353
|
+
{:else}
|
|
354
|
+
<Empty icon={ImageIcon} text="Set the image's address above" />
|
|
355
|
+
{/if}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
```svelte
|
|
359
|
+
<!-- PhotoPreview.svelte -->
|
|
360
|
+
<script lang="ts">
|
|
361
|
+
import { baseValue, type PluginPreviewProps } from '@atom-forge/brickee';
|
|
362
|
+
import { aspect, imageSrc, type PhotoProps } from './PhotoView.svelte';
|
|
363
|
+
|
|
364
|
+
let { props }: PluginPreviewProps<PhotoProps> = $props();
|
|
365
|
+
const src = $derived(imageSrc(props.url));
|
|
366
|
+
</script>
|
|
367
|
+
|
|
368
|
+
{#if src}
|
|
369
|
+
<img {src} alt={props.alt ?? ''} style:aspect-ratio={aspect(baseValue(props.ratio))} />
|
|
370
|
+
{/if}
|
|
371
|
+
|
|
372
|
+
<style>
|
|
373
|
+
img {
|
|
374
|
+
width: 100%;
|
|
375
|
+
object-fit: cover;
|
|
376
|
+
}
|
|
377
|
+
</style>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
// photo.ts
|
|
382
|
+
import { ImageIcon } from '@lucide/svelte';
|
|
383
|
+
import { mdText, type BlockPlugin } from '@atom-forge/brickee';
|
|
384
|
+
import PhotoView, { type PhotoProps } from './PhotoView.svelte';
|
|
385
|
+
import PhotoPreview from './PhotoPreview.svelte';
|
|
386
|
+
|
|
387
|
+
export const photo: BlockPlugin<PhotoProps> = {
|
|
388
|
+
type: 'photo',
|
|
389
|
+
label: 'Photo',
|
|
390
|
+
hint: 'One image',
|
|
391
|
+
keys: 'photo image picture',
|
|
392
|
+
icon: ImageIcon,
|
|
393
|
+
props: () => ({ url: '', alt: '' }),
|
|
394
|
+
fields: [
|
|
395
|
+
{ key: 'url', label: 'Image', kind: 'text', placeholder: 'https://…', parse: (s) => s.trim() },
|
|
396
|
+
{ key: 'alt', label: 'Alt text', kind: 'text', placeholder: 'What the image shows' },
|
|
397
|
+
{ key: 'ratio', label: 'Height', kind: 'slider', min: 20, max: 150, unit: '%', none: 'Original' }
|
|
398
|
+
],
|
|
399
|
+
tag: () => 'photo',
|
|
400
|
+
content: 'none',
|
|
401
|
+
markdown: (p) => {
|
|
402
|
+
const src = imageSrc(p.url);
|
|
403
|
+
return src ? `` : '';
|
|
404
|
+
},
|
|
405
|
+
view: PhotoView,
|
|
406
|
+
preview: PhotoPreview
|
|
407
|
+
};
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The address is a prop like any other, so it is untrusted: the view and the preview render it only when it is an `http(s)` or site-relative address, as the button checks its link.
|
|
411
|
+
|
|
412
|
+
Other apps choose differently, all within the plugin:
|
|
413
|
+
|
|
414
|
+
- **A media library.** The block stores the id of an image in the app's library instead of a URL. The view, the preview and `markdown` turn the id into an address with the app's own lookup: a module of the app, or the app's own Svelte context provided above the editor and the preview (plugin components are rendered beneath them). `markdown` runs outside components, when blocks are copied and in `toMarkdown`, so it can only use module data.
|
|
415
|
+
- **Picking.** Instead of a text field, the view can show its own picker — a button that opens the app's media dialog or an upload — and write the result into `props`, which is bindable.
|
|
416
|
+
- **Several images.** A gallery stores a list (of URLs or ids) in one prop and edits it in its view the same way.
|
|
417
|
+
|
|
291
418
|
## The kit
|
|
292
419
|
|
|
293
420
|
| Export | Kind | Use |
|
|
@@ -299,7 +426,6 @@ Return `''` to leave the block out. Plugins whose output has no meaning as text
|
|
|
299
426
|
| `baseValue(value)` | function | The base value of a per-view prop. |
|
|
300
427
|
| `viewValues(value, keys)` | function | A per-view prop's value for each view. |
|
|
301
428
|
| `mdText`, `mdJoin`, `mdQuote` | functions | Markdown forms (above). |
|
|
302
|
-
| `useMedia`, `findMedia` | functions | The document's images (see [Host contexts](host-contexts.md)). |
|
|
303
429
|
| `useText` | function | Translates the plugin's own UI text. |
|
|
304
430
|
| `useView` | function | The view the host is showing. |
|
|
305
431
|
| `Preview` | component | Renders a container's nested blocks (with `nested`). |
|
|
@@ -13,7 +13,7 @@ import { table, youtube } from '@atom-forge/brickee/plugins'; // vagy néhány
|
|
|
13
13
|
|
|
14
14
|
| Export | `type` | Mezők | Markdown alak | Mi ez |
|
|
15
15
|
|---|---|---|---|---|
|
|
16
|
-
| `table` | `table` |
|
|
16
|
+
| `table` | `table` | fejléc és lábléc (a cellák helyben szerkeszthetők) | GFM-táblázat (egy beillesztett GFM-táblázatból ilyen lesz) | Inline formázott cellák sorai és oszlopai; egy cellába kattintva megjelennek a sora és oszlopa eszközei (beszúrás, törlés, igazítás), a sorokat és az oszlopokat a fogantyújuknál fogva húzhatod. A cella formázottan mutatja a szövegét; szerkesztéskor a markdown látszik, és a `Ctrl/Cmd` + `B`, `I`, `E`, `Shift+X`, `K` jelöli a kijelölt szöveget. Az első sor lehet fejléc, az utolsó lábléc. |
|
|
17
17
|
| `youtube` | `youtube` | videóhivatkozás vagy -azonosító, cím | hivatkozás | YouTube-videó, kattintásra betöltve; bármely YouTube-hivatkozásból csak a videó azonosítója marad meg. |
|
|
18
18
|
| `map` | `map` | hely vagy cím, nagyításcsúszka | hivatkozás | Egy hely vagy cím beágyazott Google Maps-térképe. |
|
|
19
19
|
| `gap` | `gap` | magasságcsúszka | nincs | Üres hely a blokkok között, 1–8 sor magas. |
|