@atom-forge/brickee 0.0.2 → 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 (62) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README-AI.md +10 -7
  3. package/dist/Editor.svelte +691 -833
  4. package/dist/LinkPrompt.svelte +1 -14
  5. package/dist/Preview.svelte +27 -54
  6. package/dist/Preview.svelte.d.ts +2 -4
  7. package/dist/PropsBar.svelte +35 -219
  8. package/dist/code.js +1 -1
  9. package/dist/dom.d.ts +8 -0
  10. package/dist/dom.js +9 -0
  11. package/dist/editor/BlockMenu.svelte +102 -0
  12. package/dist/editor/BlockMenu.svelte.d.ts +32 -0
  13. package/dist/editor/FormatBar.svelte +41 -0
  14. package/dist/editor/FormatBar.svelte.d.ts +16 -0
  15. package/dist/editor/drag.svelte.d.ts +63 -0
  16. package/dist/editor/drag.svelte.js +95 -0
  17. package/dist/editor/history.d.ts +24 -0
  18. package/dist/editor/history.js +86 -0
  19. package/dist/editor/sweep.svelte.d.ts +21 -0
  20. package/dist/editor/sweep.svelte.js +56 -0
  21. package/dist/html.d.ts +12 -0
  22. package/dist/html.js +206 -0
  23. package/dist/i18n.js +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +3 -2
  26. package/dist/kit/Empty.svelte +4 -15
  27. package/dist/kit/Frame.svelte +7 -37
  28. package/dist/locales/hu.js +4 -1
  29. package/dist/markdown.d.ts +20 -4
  30. package/dist/markdown.js +380 -85
  31. package/dist/model.js +2 -3
  32. package/dist/plugins/Gap.svelte +5 -8
  33. package/dist/plugins/GapPreview.svelte +4 -6
  34. package/dist/plugins/Map.svelte +2 -17
  35. package/dist/plugins/MapPreview.svelte +8 -10
  36. package/dist/plugins/Table.svelte +233 -256
  37. package/dist/plugins/TablePreview.svelte +28 -28
  38. package/dist/plugins/YouTube.svelte +3 -31
  39. package/dist/plugins/YouTubePreview.svelte +45 -43
  40. package/dist/plugins/index.js +3 -1
  41. package/dist/types.d.ts +21 -3
  42. package/dist/types.js +8 -1
  43. package/dist/view.js +1 -1
  44. package/docs/en/bundled-plugins.md +1 -1
  45. package/docs/en/documents-and-markdown.md +20 -14
  46. package/docs/en/editor-and-preview.md +6 -7
  47. package/docs/en/getting-started.md +8 -0
  48. package/docs/en/index.md +1 -1
  49. package/docs/en/recipes-and-pitfalls.md +2 -2
  50. package/docs/en/server-side.md +0 -17
  51. package/docs/en/upgrading.md +14 -0
  52. package/docs/en/writing-plugins.md +36 -10
  53. package/docs/hu/bundled-plugins.md +1 -1
  54. package/docs/hu/documents-and-markdown.md +20 -14
  55. package/docs/hu/editor-and-preview.md +6 -7
  56. package/docs/hu/getting-started.md +8 -0
  57. package/docs/hu/index.md +1 -1
  58. package/docs/hu/recipes-and-pitfalls.md +2 -2
  59. package/docs/hu/server-side.md +0 -17
  60. package/docs/hu/upgrading.md +14 -0
  61. package/docs/hu/writing-plugins.md +36 -10
  62. package/package.json +9 -1
@@ -15,38 +15,10 @@
15
15
  </script>
16
16
 
17
17
  {#if id}
18
- <div class="yt">
19
- <img src="https://i.ytimg.com/vi/{id}/hqdefault.jpg" alt={props.title} draggable="false" />
20
- <span class="play"><Play size={22} /></span>
18
+ <div class="relative overflow-hidden rounded-[10px] bg-black">
19
+ <img class="block aspect-video w-full object-cover opacity-85" src="https://i.ytimg.com/vi/{id}/hqdefault.jpg" alt={props.title} draggable="false" />
20
+ <span class="absolute inset-0 m-auto grid h-10 w-14 place-items-center rounded-[10px] bg-red-600 text-white"><Play size={22} /></span>
21
21
  </div>
22
22
  {:else}
23
23
  <Empty icon={SquarePlay} text="Paste a YouTube link above" />
24
24
  {/if}
25
-
26
- <style>
27
- .yt {
28
- position: relative;
29
- border-radius: 10px;
30
- overflow: hidden;
31
- background: #000;
32
- }
33
- img {
34
- display: block;
35
- width: 100%;
36
- aspect-ratio: 16 / 9;
37
- object-fit: cover;
38
- opacity: 0.85;
39
- }
40
- .play {
41
- position: absolute;
42
- inset: 0;
43
- margin: auto;
44
- width: 56px;
45
- height: 40px;
46
- display: grid;
47
- place-items: center;
48
- border-radius: 10px;
49
- background: #dc2626;
50
- color: #fff;
51
- }
52
- </style>
@@ -31,46 +31,48 @@
31
31
  </figure>
32
32
  {/if}
33
33
 
34
- <style>
35
- .yt {
36
- margin: 0 0 1em;
37
- }
38
- iframe,
39
- button {
40
- display: block;
41
- width: 100%;
42
- aspect-ratio: 16 / 9;
43
- border: 0;
44
- border-radius: 8px;
45
- }
46
- button {
47
- position: relative;
48
- padding: 0;
49
- overflow: hidden;
50
- background: #000;
51
- cursor: pointer;
52
- }
53
- img {
54
- width: 100%;
55
- height: 100%;
56
- object-fit: cover;
57
- }
58
- .play {
59
- position: absolute;
60
- inset: 0;
61
- margin: auto;
62
- width: 64px;
63
- height: 44px;
64
- display: grid;
65
- place-items: center;
66
- border-radius: 12px;
67
- background: #dc2626;
68
- color: #fff;
69
- font-size: 20px;
70
- }
71
- figcaption {
72
- margin-top: 0.4em;
73
- color: color-mix(in srgb, currentColor 65%, transparent);
74
- font: 13px/1.4 system-ui, sans-serif;
75
- }
76
- </style>
34
+ <style>.yt {
35
+ margin: 0 0 1em;
36
+ }
37
+
38
+ iframe,
39
+ button {
40
+ display: block;
41
+ width: 100%;
42
+ aspect-ratio: 16/9;
43
+ border: 0;
44
+ border-radius: 8px;
45
+ }
46
+
47
+ button {
48
+ position: relative;
49
+ padding: 0;
50
+ overflow: hidden;
51
+ background: #000;
52
+ cursor: pointer;
53
+ }
54
+ button img {
55
+ width: 100%;
56
+ height: 100%;
57
+ object-fit: cover;
58
+ }
59
+
60
+ .play {
61
+ position: absolute;
62
+ inset: 0;
63
+ margin: auto;
64
+ width: 64px;
65
+ height: 44px;
66
+ display: grid;
67
+ place-items: center;
68
+ border-radius: 12px;
69
+ background: #dc2626;
70
+ color: #fff;
71
+ font-size: 20px;
72
+ }
73
+
74
+ figcaption {
75
+ margin-top: 0.4em;
76
+ color: color-mix(in srgb, currentColor 65%, transparent);
77
+ font: 13px/1.4 system-ui, sans-serif;
78
+ }</style>
@@ -7,7 +7,7 @@ import Gap, { GAP_LINES } from './Gap.svelte';
7
7
  import GapPreview from './GapPreview.svelte';
8
8
  import MapView, {} from './Map.svelte';
9
9
  import MapPreview from './MapPreview.svelte';
10
- import TableView, { tableHeads } from './Table.svelte';
10
+ import TableView, { square, tableHeads } from './Table.svelte';
11
11
  import TablePreview from './TablePreview.svelte';
12
12
  import YouTube, {} from './YouTube.svelte';
13
13
  import YouTubePreview from './YouTubePreview.svelte';
@@ -55,6 +55,8 @@ export const table = {
55
55
  ],
56
56
  tag: (p) => `table · ${p.rows?.length ?? 0}×${p.rows?.[0]?.length ?? 0}`,
57
57
  content: 'none',
58
+ // A pasted or imported GFM table: its first row is the header.
59
+ fromTable: ({ rows, align }) => ({ rows: square(rows), heads: ['top'], ...(align.some((a) => a !== 'left') ? { align } : {}) }),
58
60
  markdown: (p) => {
59
61
  const rows = p.rows ?? [];
60
62
  if (!rows.length)
package/dist/types.d.ts CHANGED
@@ -83,6 +83,12 @@ export interface PluginPreviewProps<P extends Props = Props> {
83
83
  plugins: BlockPlugin[];
84
84
  }
85
85
  /** A custom block for the slash menu. */
86
+ /** A GFM table read from markdown: the cells as inline markdown, row by row (the first row is the header), and the
87
+ * alignment of each column. */
88
+ export interface MarkdownTable {
89
+ rows: string[][];
90
+ align: ('left' | 'center' | 'right')[];
91
+ }
86
92
  /** What a plugin's `markdown` gets beside its props. */
87
93
  export interface MarkdownContext {
88
94
  /** The nested blocks as markdown ('' when it has none). */
@@ -120,13 +126,25 @@ export interface BlockPlugin<P extends Props = Props> {
120
126
  * when it gives '', the block is left out of the markdown.
121
127
  */
122
128
  markdown?(props: P, ctx: MarkdownContext): string;
123
- /** At most one per document, and only at the top level (e.g. the supporters-only divider). */
129
+ /**
130
+ * Takes a GFM table when markdown is pasted or imported (`fromMarkdown(src, { plugins })`): returns the block's props
131
+ * from the table's cells (inline markdown, the first row is the header) and its column alignments. The first plugin
132
+ * that has it gets the table; without one, the table stays text.
133
+ */
134
+ fromTable?(table: MarkdownTable): P;
135
+ /**
136
+ * Takes files pasted into the editor or dropped on it (an image, a PDF…): returns the new block's props, or null to
137
+ * leave them to the next plugin. Uploading and storing them is the plugin's own business; it may take its time (a
138
+ * promise). The first plugin the editor offers that takes them gets one block.
139
+ */
140
+ fromFiles?(files: File[]): P | null | Promise<P | null>;
141
+ /** At most one per document, and only at the top level (e.g. a table of contents). */
124
142
  unique?: boolean;
125
- /** The blocks after it are gated (e.g. for supporters only): the preview marks them. Implies nothing about `unique`. */
126
- gate?: boolean;
127
143
  }
128
144
  export { MAX_INDENT } from './model';
129
145
  export declare const uid: () => string;
130
146
  export declare const newBlock: (type?: BlockType, html?: string, extra?: Partial<Block>) => Block;
147
+ /** The start number a numbered list's first item carries (`props.start`), or 1. */
148
+ export declare const olStart: (props: Record<string, unknown> | undefined) => number;
131
149
  /** Ordinal for every `ol` block, restarting after interruptions and per indent level. */
132
150
  export declare function numbering(blocks: Block[]): Record<string, number>;
package/dist/types.js CHANGED
@@ -25,14 +25,21 @@ export const newBlock = (type = 'p', html = '', extra = {}) => ({
25
25
  indent: 0,
26
26
  ...extra
27
27
  });
28
+ /** The start number a numbered list's first item carries (`props.start`), or 1. */
29
+ export const olStart = (props) => {
30
+ const n = Math.floor(Number(props?.start));
31
+ return n >= 0 && n < 1e9 ? n : 1;
32
+ };
28
33
  /** Ordinal for every `ol` block, restarting after interruptions and per indent level. */
29
34
  export function numbering(blocks) {
30
35
  const out = {};
31
36
  const counters = [];
32
37
  for (const b of blocks) {
33
38
  if (b.type === 'ol') {
39
+ const fresh = counters[b.indent] === undefined;
34
40
  counters.length = b.indent + 1;
35
- counters[b.indent] = (counters[b.indent] ?? 0) + 1;
41
+ // A list's first item may carry its start number (an imported "5. …").
42
+ counters[b.indent] = fresh ? olStart(b.props) : counters[b.indent] + 1;
36
43
  out[b.id] = counters[b.indent];
37
44
  }
38
45
  else {
package/dist/view.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // host's business (e.g. the width its preview shows): it gives the current one through the context. Without it, a
3
3
  // per-view field simply edits its base value.
4
4
  import { getContext, setContext } from 'svelte';
5
- const KEY = 'block-editor:view';
5
+ const KEY = 'brickee:view';
6
6
  /** Call in a component's init (around the editor); the function returns the current view's key. */
7
7
  export function provideView(get) {
8
8
  setContext(KEY, get);
@@ -13,7 +13,7 @@ import { table, youtube } from '@atom-forge/brickee/plugins'; // or some
13
13
 
14
14
  | Export | `type` | Fields | Markdown form | What it is |
15
15
  |---|---|---|---|---|
16
- | `table` | `table` | header and footer (the cells are edited in place) | a GFM table | Rows and columns of inline-formatted cells; click a cell for its row and column tools (insert, delete, align), drag rows and columns by their grips. The first row can be a header, the last a footer. |
16
+ | `table` | `table` | header and footer (the cells are edited in place) | a GFM table (a pasted GFM table becomes one) | Rows and columns of inline-formatted cells; click a cell for its row and column tools (insert, delete, align), drag rows and columns by their grips. A cell shows its text formatted; editing it shows the markdown, and `Ctrl/Cmd` + `B`, `I`, `E`, `Shift+X`, `K` mark the selected text. The first row can be a header, the last a footer. |
17
17
  | `youtube` | `youtube` | video link or id, title | a link | A YouTube video, loaded on click; any YouTube link is reduced to the video id. |
18
18
  | `map` | `map` | place or address, zoom slider | a link | A Google Maps embed of a place or an address. |
19
19
  | `gap` | `gap` | height slider | none | Empty space between blocks, 1–8 lines high. |
@@ -46,13 +46,13 @@ A short document with a paragraph, a list, a video and a box (the `note` contain
46
46
  | `md:p` | inline markdown | — | the text |
47
47
  | `md:h1` … `md:h6` | inline markdown | — | `#` … `######` |
48
48
  | `md:ul` | inline markdown | — | `- item` |
49
- | `md:ol` | inline markdown | — | `1. item` (numbered per run and indent level) |
49
+ | `md:ol` | inline markdown | `{ start?: number }` on a list's first item | `1. item` (numbered per run and indent level, from `start` when given) |
50
50
  | `md:todo` | inline markdown | `{ checked: boolean }` | `- [x] item` |
51
51
  | `md:quote` | inline markdown | — | `> quote` |
52
52
  | `md:code` | raw text | `{ lang?: string }` | fenced block with the language |
53
53
  | `md:hr` | — | — | `---` |
54
54
 
55
- `indent` nests list items (consecutive list blocks become properly nested `ul`/`ol` elements in the preview) and indents paragraphs and quotes. Headings, code and dividers ignore it.
55
+ `indent` nests list items (consecutive list blocks become properly nested `ul`/`ol` elements in the preview), puts paragraphs and code blocks under a list item, and nests quotes (`> > …`). Headings and dividers ignore it.
56
56
 
57
57
  ### Inline markdown
58
58
 
@@ -60,15 +60,15 @@ The text of a built-in block is a small, safe subset of markdown:
60
60
 
61
61
  | Mark | Stored as |
62
62
  |---|---|
63
- | bold | `**text**` |
64
- | italic | `*text*` |
63
+ | bold | `**text**` (read also: `__text__`) |
64
+ | italic | `*text*` (read also: `_text_`, not inside a word) |
65
65
  | underline, subscript, superscript | `<u>text</u>`, `<sub>text</sub>`, `<sup>text</sup>` |
66
66
  | strikethrough | `~~text~~` |
67
- | code | `` `text` `` |
68
- | link | `[text](url)` — only `http:`, `https:`, `mailto:`, `tel:` and site-relative `/…` targets become links |
69
- | line break | a newline |
67
+ | code | `` `text` `` (a longer backtick run when the code holds backticks: ``` `` a`b `` ```) |
68
+ | link | `[text](url)` — only `http:`, `https:`, `mailto:`, `tel:` and site-relative `/…` targets become links; parentheses in a target must be balanced. Read also: a title (dropped), `<https://…>` autolinks, and an image `![alt](url)` as a link |
69
+ | line break | a newline (in markdown out: a backslash at the line's end) |
70
70
 
71
- Special characters in plain text are backslash-escaped, so they never turn into markup. `inline(md)` turns inline markdown into HTML: it escapes the text first and only then adds the tags above, so its output is safe to render with `{@html}` — this is what the preview does.
71
+ Special characters in plain text are backslash-escaped, so they never turn into markup; any ASCII punctuation can be escaped. Reading stays linear on any input, with these limits: a marked span is at most 2000 characters, a link label has no brackets of its own and is at most 1000 characters, a link target at most 2000. When reading, inline HTML maps onto the marks (`<strong>`, `<em>`, `<del>`, `<br>`…), other tags are dropped with their text kept, and a footnote reference `[^1]` becomes a superscript. `inline(md)` turns inline markdown into HTML: it escapes the text first and only then adds the tags above, so its output is safe to render with `{@html}` — this is what the preview does.
72
72
 
73
73
  ## Markdown in and out
74
74
 
@@ -78,7 +78,8 @@ Markdown is a first-class format: the clipboard carries it, and a host can impor
78
78
  |---|---|---|
79
79
  | `fromStored(block)` | `StoredBlock` → `Block` | anywhere |
80
80
  | `toStored(block)` | `Block` → `StoredBlock` | browser only |
81
- | `fromMarkdown(text)` | markdown → `Block[]` | anywhere |
81
+ | `fromMarkdown(text, { plugins })` | markdown → `Block[]` | anywhere |
82
+ | `fromHtml(html, { plugins })` | HTML (a formatted paste, a page) → `Block[]` | browser only |
82
83
  | `toMarkdown(blocks, { plugins })` | `Block[]` → markdown | browser only |
83
84
  | `inline(md)` | inline markdown → inline HTML | anywhere |
84
85
 
@@ -101,14 +102,19 @@ export const importMarkdown = (text: string): StoredBlock[] => fromMarkdown(text
101
102
 
102
103
  What `fromMarkdown` reads:
103
104
 
104
- - headings, bulleted, numbered and to-do items, quotes, fenced code blocks with their language, dividers, and the inline marks above;
105
- - leading spaces as indentation (two spaces or one tab per level);
106
- - every non-empty line as its own block — it does not join wrapped lines into one paragraph, and it creates built-in blocks only (no tables, images or plugin blocks).
105
+ - headings (also setext: a line underlined with `===` or `---`, and with closing `#`s), bulleted, numbered (with their start number) and to-do items, quotes (nested ones one level deeper), fenced code (backticks or tildes, any length, closed by a fence at least as long) and indented code, dividers, and the inline marks above;
106
+ - list nesting by the items' markers: a line indented past an item's marker is inside the item (so `1. ` items nest with two, three or four spaces);
107
+ - the lines of a paragraph, a list item or a quote as one block, with line breaks between them; a blank line, or `>` alone in a quote, starts a new one;
108
+ - reference links (`[text][id]` with `[id]: url`) resolved, footnote definitions as a paragraph after their number;
109
+ - a GFM table as a block of the first of `plugins` that takes tables (`fromTable`, see [Writing plugins](writing-plugins.md#taking-pasted-tables); the bundled `table` does), otherwise as text. The editor passes the plugins it offers, so a pasted table becomes a table block.
110
+
111
+ What `fromHtml` reads — the editor uses it for a formatted paste from a web page, Google Docs or Word: headings, paragraphs, lists (nested, numbered with their start, to-dos from checkboxes), quotes, code (`pre`, with a `language-…` class), rules, tables (for a plugin that takes them), and the inline marks — also Google Docs' bold, italic and struck-through spans. Images become links (the package has no images), and scripts, styles and form controls are dropped. The HTML is only read: it goes through markdown and the same safe rendering as everything else.
107
112
 
108
113
  What `toMarkdown` writes:
109
114
 
110
- - consecutive list items on consecutive lines, every other block separated by a blank line;
111
- - list items and paragraphs indented by two spaces per level;
115
+ - consecutive list items on consecutive lines, consecutive quotes as one quote (`>` between their paragraphs), every other block separated by a blank line;
116
+ - nested items, and paragraphs and code under an item, indented to the item's content (two spaces under `- `, three under `1. `, four under `10. `);
117
+ - code in a fence longer than any backtick run in it, and plain text escaped where markdown would read it as markup (`\_`, `1\.` at a line's start…);
112
118
  - a plugin block through its plugin's `markdown(props, { inner })`, where `inner` is its nested blocks already converted; a plugin without a markdown form, or one returning `''`, is left out.
113
119
 
114
120
  Writing a markdown form for your own plugin is covered in [Writing plugins](writing-plugins.md#markdown-form).
@@ -6,11 +6,11 @@
6
6
 
7
7
  | Prop | Type | Default | Meaning |
8
8
  |---|---|---|---|
9
- | `value` | `StoredBlock[]` (bindable) | `[]` | The document. Written back on every change; a document replaced from outside is loaded again. |
9
+ | `value` | `StoredBlock[]` (bindable) | `[]` | The document. Written back on every change (unchanged blocks keep their objects); a document replaced from outside — a new array, not one changed in place — is loaded again, unless it equals the current one. |
10
10
  | `plugins` | `BlockPlugin[]` | `[]` | The plugin blocks this editor can show and offer. |
11
11
  | `palette` | `string[]` | every built-in block, then every plugin | The block types the slash menu offers, in this order (see below). |
12
12
  | `messages` | `Messages` | — | UI text in the host's language: English text → translation. |
13
- | `readonly` | `boolean` | `false` | Shows the document without editing; plugin fields are hidden. |
13
+ | `readonly` | `boolean` | `false` | Shows the document without editing; plugin fields are hidden. A read-only editor never writes `value`. |
14
14
  | `placeholder` | `string` | `Write something, or press '/' for blocks…` | The hint of an empty block. |
15
15
  | `nested` | `boolean` | `false` | Set by plugins with nested blocks for their inner editor; a host never sets it. |
16
16
 
@@ -23,7 +23,6 @@ An empty document is shown as one empty paragraph, so the value the editor write
23
23
  | `blocks` | `StoredBlock[]` | — | The document to render. |
24
24
  | `plugins` | `BlockPlugin[]` | — | The plugins whose previews render the plugin blocks. |
25
25
  | `messages` | `Messages` | — | UI text in the host's language. |
26
- | `markGated` | `boolean` | `true` | Marks the blocks after a gate block (see [Unique and gate blocks](writing-plugins.md#unique-and-gate-blocks)). |
27
26
  | `nested` | `boolean` | `false` | Set by plugin previews that render their nested blocks. |
28
27
 
29
28
  The preview renders built-in blocks as plain HTML (`p`, `h1`…`h6`, `ul`/`ol` with nested items, `blockquote`, `pre > code` with a `language-<lang>` class, `hr`) and each plugin block with its plugin's `preview`. A block whose type has no plugin in `plugins`, or whose plugin has no `preview`, is not rendered.
@@ -72,10 +71,10 @@ A plain paragraph (`md:p`) is always allowed as the fallback.
72
71
  | Inline formatting | Select text for the toolbar, or `Ctrl/Cmd` + `B` bold, `I` italic, `U` underline, `Shift+X` strikethrough, `E` code, `K` link, `\` remove formatting. Subscript and superscript are on the toolbar. |
73
72
  | Indent | `Tab` / `Shift+Tab` (at most 6 levels). |
74
73
  | Line break | `Shift+Enter`; in a code block `Enter` breaks the line and `Shift+Enter` or `Ctrl/Cmd+Enter` starts a new block. |
75
- | Select blocks | Click a block's handle, `Esc` in a block, `Shift+↑/↓` past the edge of a block, or drag a text selection across blocks. `Ctrl/Cmd+A` in an empty block selects every block. |
76
- | Move blocks | Drag the handle (also into and out of nested editors), or `Ctrl/Cmd+Shift+↑/↓`. |
77
- | Turn into | Click the handle of a block or a selection for the Turn-into menu. |
78
- | Copy, cut | Copying selected blocks puts their markdown on the clipboard (plugin blocks through their markdown form). |
74
+ | Select blocks | Click (or tap) a block's handle, `Esc` in a block, `Shift+↑/↓` past the edge of a block, or drag a text selection across blocks. `Ctrl/Cmd+A` in an empty block selects every block. |
75
+ | Move blocks | Drag the handle (also into and out of nested editors), `Ctrl/Cmd+Shift+↑/↓`, or *Move up* / *Move down* in the Turn-into menu (without dragging: touch screens, keyboard). |
76
+ | Turn into | Click the handle of a block or a selection for the Turn-into menu; from the keyboard `/` (or `Ctrl/Cmd+/`, the menu key, `Shift+F10`) on selected blocks, or `Ctrl/Cmd+/` in a block's text. On a touch screen a tapped block shows its actions (add below, turn into, delete) beside its handle. |
77
+ | Copy, cut, paste | Copying selected blocks puts their markdown on the clipboard (plugin blocks through their markdown form) and, beside it, the blocks themselves (`application/x-brickee+json`). Formatted text from elsewhere (a web page, Google Docs, Word) keeps its headings, lists, tables and marks (`fromHtml`); plain text is read as markdown. Pasted into a Brickee editor, copied blocks come back as they were; a plugin block that editor does not offer (or a second unique one) falls back to its markdown form. Pasted anywhere else, the markdown arrives. Pasting while blocks are selected puts the blocks after them. |
79
78
  | Paste | Multi-line text is read as markdown and becomes blocks. |
80
79
  | Undo, redo | `Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z` or `Ctrl/Cmd+Y`. |
81
80
 
@@ -33,6 +33,14 @@ bun add @atom-forge/brickee @atom-forge/ui @lucide/svelte
33
33
  The block fields and popups are `@atom-forge/ui` components, so the application must be set up the way `@atom-forge/ui` requires:
34
34
 
35
35
  - Tailwind CSS 4 with the `@atom-forge/ui` theme CSS imported and its `dist` folder added as a Tailwind `@source`;
36
+ - the `dist` folder of `@atom-forge/brickee` added as a Tailwind `@source` too: the editor is styled with Tailwind classes and the `@atom-forge/ui` theme's colours, so Tailwind has to see them:
37
+
38
+ ```css
39
+ @source "../node_modules/@atom-forge/ui/dist";
40
+ @source "../node_modules/@atom-forge/brickee/dist";
41
+ ```
42
+
43
+ The preview needs neither: its few styles travel with its markup, so a public page without Tailwind can show documents;
36
44
  - the application rendered beneath the `@atom-forge/ui` `Root` component, which provides the theme and the overlay contexts.
37
45
 
38
46
  The top-level `Editor` creates its own popup manager and renders its own popup container; editors nested in plugin blocks share it.
package/docs/en/index.md CHANGED
@@ -29,7 +29,7 @@ The editor itself knows nothing about your application. Whatever an app needs
29
29
  4. [Bundled plugins](bundled-plugins.md) — what `@atom-forge/brickee/plugins` contains, and how to override a preview or add fields.
30
30
  5. [Writing plugins](writing-plugins.md) — the `BlockPlugin` anatomy, fields, views and previews, containers, markdown forms, image blocks, and the kit.
31
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, plain text, and gated content.
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
 
@@ -82,7 +82,7 @@ import { fromMarkdown, toStored, type StoredBlock } from '@atom-forge/brickee';
82
82
  export const fromText = (text: string): StoredBlock[] => fromMarkdown(text).map(toStored);
83
83
  ```
84
84
 
85
- 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.
86
86
 
87
87
  ## Developing against a local copy
88
88
 
@@ -104,6 +104,7 @@ The application reads the package's built `dist/`, so rebuild the package after
104
104
 
105
105
  | Symptom | Cause | Fix |
106
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. |
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`. |
108
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`. |
@@ -114,7 +115,6 @@ The application reads the package's built `dist/`, so rebuild the package after
114
115
  | A box cannot be put inside another box | Nested editors go one level deep | Design containers to hold content blocks, not other containers. |
115
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). |
116
117
  | A unique block appears twice | The schema does not enforce `unique` | Check it on the server if your application relies on it. |
117
- | Gated content visible to everyone | `gate` only marks the blocks | Cut the document on the server before rendering. |
118
118
  | `document is not defined` | `toMarkdown` or `toStored` ran during SSR | Call them in the browser only. |
119
119
  | A colour or link from props breaks the page | Props are user input | Use `safeColor`, check links, never use `{@html}` with props. |
120
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,6 +6,20 @@ 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
+ ## From 0.0.2 to 0.0.3
10
+
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
+
9
23
  ## From 0.0.1 to 0.0.2
10
24
 
11
25
  ### Images and media are no longer part of the package
@@ -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
 
@@ -261,16 +260,9 @@ How nesting works:
261
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.
262
261
  - `blocksSchemaFor(plugins)` checks `accepts` on the server (see [On the server](server-side.md)).
263
262
 
264
- ## Unique and gate blocks
263
+ ## Unique blocks
265
264
 
266
- - **`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.
267
- - **`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.
268
-
269
- A supporters-only divider is a typical example of both: one per document, with the gated content behind it.
270
-
271
- > **Gating only marks**
272
- >
273
- > `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.
274
266
 
275
267
  ## Markdown form
276
268
 
@@ -284,6 +276,40 @@ Return `''` to leave the block out. Plugins whose output has no meaning as text
284
276
  | `mdJoin(...parts)` | Joins parts as separate blocks (blank line between), leaving out empty and falsy ones. |
285
277
  | `mdQuote(md)` | Prefixes every line with `> `, for a box rendered as a blockquote. |
286
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
+
287
313
  ## Images
288
314
 
289
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.
@@ -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` | fejléc és lábléc (a cellák helyben szerkeszthetők) | GFM-táblázat | 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. 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. |
@@ -46,13 +46,13 @@ Egy rövid dokumentum egy bekezdéssel, egy listával, egy videóval és egy dob
46
46
  | `md:p` | inline markdown | — | maga a szöveg |
47
47
  | `md:h1` … `md:h6` | inline markdown | — | `#` … `######` |
48
48
  | `md:ul` | inline markdown | — | `- item` |
49
- | `md:ol` | inline markdown | — | `1. item` (sorozatonként és behúzási szintenként számozva) |
49
+ | `md:ol` | inline markdown | `{ start?: number }` a lista első elemén | `1. item` (sorozatonként és behúzási szintenként számozva, megadott `start`-tól) |
50
50
  | `md:todo` | inline markdown | `{ checked: boolean }` | `- [x] item` |
51
51
  | `md:quote` | inline markdown | — | `> quote` |
52
52
  | `md:code` | nyers szöveg | `{ lang?: string }` | kerített blokk a nyelvvel |
53
53
  | `md:hr` | — | — | `---` |
54
54
 
55
- Az `indent` egymásba ágyazza a listaelemeket (az egymást követő lista-blokkokból az előnézetben helyesen egymásba ágyazott `ul`/`ol` elemek lesznek), a bekezdéseket és az idézeteket pedig behúzza. A címsorok, a kódblokkok és az elválasztók figyelmen kívül hagyják.
55
+ Az `indent` egymásba ágyazza a listaelemeket (az egymást követő lista-blokkokból az előnézetben helyesen egymásba ágyazott `ul`/`ol` elemek lesznek), a bekezdéseket és a kódblokkokat egy listaelem alá teszi, az idézeteket pedig egymásba ágyazza (`> > …`). A címsorok és az elválasztók figyelmen kívül hagyják.
56
56
 
57
57
  ### Inline markdown
58
58
 
@@ -60,15 +60,15 @@ A beépített blokkok szövege a markdown egy kicsi, biztonságos részhalmaza:
60
60
 
61
61
  | Jelölés | Tárolt alak |
62
62
  |---|---|
63
- | félkövér | `**text**` |
64
- | dőlt | `*text*` |
63
+ | félkövér | `**text**` (beolvasva a `__text__` is) |
64
+ | dőlt | `*text*` (beolvasva a `_text_` is, szón belül nem) |
65
65
  | aláhúzott, alsó index, felső index | `<u>text</u>`, `<sub>text</sub>`, `<sup>text</sup>` |
66
66
  | áthúzott | `~~text~~` |
67
- | kód | `` `text` `` |
68
- | hivatkozás | `[text](url)` – csak a `http:`, `https:`, `mailto:`, `tel:` és a webhelyen belüli `/…` célok lesznek hivatkozások |
69
- | sortörés | egy újsor karakter |
67
+ | kód | `` `text` `` (hosszabb backtick-sorozattal, ha a kód backticket tartalmaz: ``` `` a`b `` ```) |
68
+ | hivatkozás | `[text](url)` – csak a `http:`, `https:`, `mailto:`, `tel:` és a webhelyen belüli `/…` célok lesznek hivatkozások; a célban a zárójeleknek párosnak kell lenniük. Beolvasva a cím (elhagyva), a `<https://…>` autolink és a kép (`![alt](url)`) hivatkozásként is |
69
+ | sortörés | egy újsor karakter (markdown-kimenetben: visszaperjel a sor végén) |
70
70
 
71
- Az egyszerű szövegben a speciális karakterek visszaperjeles escape-elést kapnak, így soha nem válnak jelöléssé. Az `inline(md)` az inline markdownt HTML-lé alakítja: előbb escape-eli a szöveget, és csak utána teszi hozzá a fenti tageket, így a kimenete biztonságosan megjeleníthető `{@html}`-lel – az előnézet is így tesz.
71
+ Az egyszerű szövegben a speciális karakterek visszaperjeles escape-elést kapnak, így soha nem válnak jelöléssé; bármely ASCII írásjel escape-elhető. A beolvasás bármilyen bemeneten lineáris időben fut, ezekkel a korlátokkal: egy jelölt szakasz legfeljebb 2000 karakter, egy link címkéjében nem lehet saját szögletes zárójel, és legfeljebb 1000 karakter, egy link célja legfeljebb 2000. Beolvasáskor az inline HTML a jelölésekre képeződik le (`<strong>`, `<em>`, `<del>`, `<br>`…), a többi tag elmarad (a szövege megmarad), a `[^1]` lábjegyzet-hivatkozásból pedig felső index lesz. Az `inline(md)` az inline markdownt HTML-lé alakítja: előbb escape-eli a szöveget, és csak utána teszi hozzá a fenti tageket, így a kimenete biztonságosan megjeleníthető `{@html}`-lel – az előnézet is így tesz.
72
72
 
73
73
  ## Markdown be és ki
74
74
 
@@ -78,7 +78,8 @@ A markdown teljes értékű formátum: a vágólap ezt hordozza, és a befogadó
78
78
  |---|---|---|
79
79
  | `fromStored(block)` | `StoredBlock` → `Block` | bárhol |
80
80
  | `toStored(block)` | `Block` → `StoredBlock` | csak böngészőben |
81
- | `fromMarkdown(text)` | markdown → `Block[]` | bárhol |
81
+ | `fromMarkdown(text, { plugins })` | markdown → `Block[]` | bárhol |
82
+ | `fromHtml(html, { plugins })` | HTML (formázott beillesztés, egy oldal) → `Block[]` | csak böngészőben |
82
83
  | `toMarkdown(blocks, { plugins })` | `Block[]` → markdown | csak böngészőben |
83
84
  | `inline(md)` | inline markdown → inline HTML | bárhol |
84
85
 
@@ -101,14 +102,19 @@ export const importMarkdown = (text: string): StoredBlock[] => fromMarkdown(text
101
102
 
102
103
  Amit a `fromMarkdown` beolvas:
103
104
 
104
- - címsorok, felsorolás-, számozott és teendő-elemek, idézetek, kerített kódblokkok a nyelvükkel, elválasztók és a fenti inline jelölések;
105
- - a sor eleji szóközök behúzásként (szintenként két szóköz vagy egy tabulátor);
106
- - minden nem üres sor külön blokként – a tördelt sorokat nem vonja össze egy bekezdéssé, és csak beépített blokkokat hoz létre (táblázatot, képet vagy plugin-blokkot nem).
105
+ - címsorok (setext alakban is: `===`-vel vagy `---`-val aláhúzott sor, és záró `#`-ekkel), felsorolás-, számozott (a kezdőszámukkal) és teendő-elemek, idézetek (a beágyazottak egy szinttel mélyebben), kerített kód (backtick vagy tilde, tetszőleges hosszal, legalább ugyanilyen hosszú kerítés zárja) és behúzott kód, elválasztók és a fenti inline jelölések;
106
+ - a listák egymásba ágyazását az elemek jelölői szerint: az a sor, amely beljebb kezdődik egy elem jelölőjénél, az elemen belül van (így a `1. ` elemek alá két, három vagy négy szóközzel is lehet ágyazni);
107
+ - egy bekezdés, listaelem vagy idézet sorait egy blokként, köztük sortöréssel; üres sor, idézetben a magában álló `>` új blokkot kezd;
108
+ - a hivatkozásos linkeket (`[text][id]` és `[id]: url`) feloldva, a lábjegyzet-definíciókat a számuk utáni bekezdésként;
109
+ - a GFM-táblázatot a `plugins` közül az első olyan plugin blokkjaként, amelyik fogad táblázatot (`fromTable`, lásd [Pluginok írása](writing-plugins.md#beillesztett-táblázatok-fogadása); a beépített `table` ilyen), különben szövegként. A szerkesztő átadja a felkínált pluginjait, így egy beillesztett táblázatból táblázat blokk lesz.
110
+
111
+ Amit a `fromHtml` beolvas – a szerkesztő ezt használja weboldalról, Google Docsból vagy Wordből érkező formázott beillesztéshez: címsorok, bekezdések, listák (beágyazva, a számozottak a kezdőszámukkal, a teendők a jelölőnégyzetekből), idézetek, kód (`pre`, `language-…` osztállyal), elválasztók, táblázatok (egy azt fogadó pluginnak) és az inline jelölések – a Google Docs félkövér, dőlt és áthúzott spanjei is. A képekből link lesz (a csomagban nincs képfogalom), a scriptek, stílusok és űrlapelemek kimaradnak. A HTML-t csak olvassa: markdownon és ugyanazon a biztonságos megjelenítésen megy át, mint minden más.
107
112
 
108
113
  Amit a `toMarkdown` kiír:
109
114
 
110
- - az egymást követő listaelemeket egymást követő sorokba, minden más blokkot üres sorral elválasztva;
111
- - a listaelemeket és a bekezdéseket szintenként két szóközzel behúzva;
115
+ - az egymást követő listaelemeket egymást követő sorokba, az egymást követő idézeteket egyetlen idézetként (bekezdéseik között `>`), minden más blokkot üres sorral elválasztva;
116
+ - a beágyazott elemeket, valamint egy elem alatti bekezdéseket és kódot az elem tartalmáig behúzva (`- ` alatt két, `1. ` alatt három, `10. ` alatt négy szóközzel);
117
+ - a kódot minden benne lévő backtick-sorozatnál hosszabb kerítésben, az egyszerű szöveget pedig ott escape-elve, ahol a markdown jelölésnek olvasná (`\_`, sor elején `1\.`…);
112
118
  - a plugin-blokkot a pluginja `markdown(props, { inner })` függvényével, ahol az `inner` a már átalakított beágyazott blokkjai; a markdown alak nélküli plugin, illetve az, amelyik `''`-t ad vissza, kimarad.
113
119
 
114
120
  Saját pluginod markdown alakjának megírásáról a [Pluginok írása](writing-plugins.md#markdown-alak) fejezet szól.