@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/README-AI.md +34 -28
  3. package/dist/Editor.svelte +713 -867
  4. package/dist/Editor.svelte.d.ts +0 -3
  5. package/dist/LinkPrompt.svelte +1 -14
  6. package/dist/Preview.svelte +27 -58
  7. package/dist/Preview.svelte.d.ts +2 -7
  8. package/dist/PropsBar.svelte +75 -249
  9. package/dist/PropsBar.svelte.d.ts +0 -3
  10. package/dist/code.d.ts +12 -0
  11. package/dist/code.js +15 -0
  12. package/dist/dom.d.ts +8 -0
  13. package/dist/dom.js +9 -0
  14. package/dist/editor/BlockMenu.svelte +102 -0
  15. package/dist/editor/BlockMenu.svelte.d.ts +32 -0
  16. package/dist/editor/FormatBar.svelte +41 -0
  17. package/dist/editor/FormatBar.svelte.d.ts +16 -0
  18. package/dist/editor/drag.svelte.d.ts +63 -0
  19. package/dist/editor/drag.svelte.js +95 -0
  20. package/dist/editor/history.d.ts +24 -0
  21. package/dist/editor/history.js +86 -0
  22. package/dist/editor/sweep.svelte.d.ts +21 -0
  23. package/dist/editor/sweep.svelte.js +56 -0
  24. package/dist/html.d.ts +12 -0
  25. package/dist/html.js +206 -0
  26. package/dist/i18n.js +1 -1
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.js +4 -3
  29. package/dist/kit/Empty.svelte +4 -15
  30. package/dist/kit/Frame.svelte +7 -37
  31. package/dist/locales/hu.js +6 -8
  32. package/dist/markdown.d.ts +20 -6
  33. package/dist/markdown.js +380 -85
  34. package/dist/model.js +2 -3
  35. package/dist/plugins/Gap.svelte +5 -8
  36. package/dist/plugins/GapPreview.svelte +4 -6
  37. package/dist/plugins/Map.svelte +2 -17
  38. package/dist/plugins/MapPreview.svelte +8 -10
  39. package/dist/plugins/Table.svelte +233 -282
  40. package/dist/plugins/TablePreview.svelte +28 -28
  41. package/dist/plugins/YouTube.svelte +3 -31
  42. package/dist/plugins/YouTubePreview.svelte +45 -43
  43. package/dist/plugins/index.js +15 -2
  44. package/dist/schema.js +1 -1
  45. package/dist/types.d.ts +27 -10
  46. package/dist/types.js +8 -1
  47. package/dist/view.js +1 -1
  48. package/docs/en/bundled-plugins.md +1 -1
  49. package/docs/en/documents-and-markdown.md +24 -19
  50. package/docs/en/editor-and-preview.md +7 -10
  51. package/docs/en/getting-started.md +14 -2
  52. package/docs/en/host-contexts.md +26 -132
  53. package/docs/en/index.md +5 -5
  54. package/docs/en/recipes-and-pitfalls.md +12 -11
  55. package/docs/en/server-side.md +0 -17
  56. package/docs/en/upgrading.md +34 -4
  57. package/docs/en/writing-plugins.md +147 -21
  58. package/docs/hu/bundled-plugins.md +1 -1
  59. package/docs/hu/documents-and-markdown.md +24 -19
  60. package/docs/hu/editor-and-preview.md +7 -10
  61. package/docs/hu/getting-started.md +14 -2
  62. package/docs/hu/host-contexts.md +26 -132
  63. package/docs/hu/index.md +5 -5
  64. package/docs/hu/recipes-and-pitfalls.md +12 -11
  65. package/docs/hu/server-side.md +0 -17
  66. package/docs/hu/upgrading.md +34 -4
  67. package/docs/hu/writing-plugins.md +147 -21
  68. package/package.json +14 -2
  69. package/dist/ImagePicker.svelte +0 -131
  70. package/dist/ImagePicker.svelte.d.ts +0 -13
  71. package/dist/media.d.ts +0 -17
  72. package/dist/media.js +0 -12
@@ -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 — 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.
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
- ## 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 ? `![${mdText(p.alt)}](${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 { findMedia, useMedia, viewValues, type PluginPreviewProps } from '@atom-forge/brickee';
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 media = useMedia();
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 item}
198
- <img src={item.url} alt={props.alt ?? ''} style:--r-desktop={r[0]} style:--r-tablet={r[1]} style:--r-phone={r[2]} />
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 images, its page widths, its language — reaches the plugins through **contexts** (media, view, text) or through plugin **fields**, never through special editor props.
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) — media, per-view values, and UI text.
32
- 7. [On the server](server-side.md) — schema validation, plain text, and gated content.
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 list of images or the current page width stand for whatever your application provides.
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 MediaItem, type StoredBlock } from '@atom-forge/brickee';
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[]; media?: MediaItem[]; palette?: string[]; view?: string } = $props();
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} {media} {messages} />
53
+ <Editor bind:value {plugins} {palette} {messages} />
55
54
  ```
56
55
 
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.
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 MediaItem, type StoredBlock } from '@atom-forge/brickee';
64
+ import { fromStored, toMarkdown, type StoredBlock } from '@atom-forge/brickee';
66
65
  import { plugins } from './blocks';
67
66
 
68
- let { doc, media = [] }: { doc: StoredBlock[]; media?: MediaItem[] } = $props();
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, media }));
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
- Remember that every non-empty line becomes its own block.
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`, or the image id is not in `media` | Use the same plugin list and media for the editor and the preview. |
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. |
@@ -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.
@@ -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
- > **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.
9
+ ## From 0.0.2 to 0.0.3
12
10
 
13
- ## From the `block-editor` workspace package to Unreleased
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, clamp numbers, and look images up in the media list instead of using stored URLs. Svelte escapes text in `{...}`; never put a prop into `{@html}`.
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`, `image` or `images`. |
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
- | `image` | The picked media item's `id`; picking it again removes it. |
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. Media, messages and the view are available through the contexts (`useMedia`, `useText`, `useView`; see [Host contexts](host-contexts.md)).
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 and gate blocks
263
+ ## Unique blocks
266
264
 
267
- - **`unique: true`** — at most one per document, at the top level. Once the document has one, the plugin leaves the slash and Turn-into menus; nested editors never offer it.
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, media })` returns the block as markdown, used when blocks are copied in the editor and by `toMarkdown`:
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 ? `![${mdText(p.alt)}](${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` | — (helyben szerkesztve) | GFM-táblázat | Inline formázott cellák sorai és oszlopai; a sorokat és az oszlopokat a fogantyújuknál fogva húzhatod. Az első sor lehet fejléc, az utolsó lábléc. |
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. |