@atom-forge/brickee 0.0.0-stage → 0.0.2

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 (85) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README-AI.md +286 -0
  4. package/README.md +41 -2
  5. package/dist/Editor.svelte +1956 -0
  6. package/dist/Editor.svelte.d.ts +19 -0
  7. package/dist/LinkPrompt.svelte +42 -0
  8. package/dist/LinkPrompt.svelte.d.ts +8 -0
  9. package/dist/PluginHost.svelte +41 -0
  10. package/dist/PluginHost.svelte.d.ts +13 -0
  11. package/dist/Preview.svelte +182 -0
  12. package/dist/Preview.svelte.d.ts +32 -0
  13. package/dist/PropsBar.svelte +405 -0
  14. package/dist/PropsBar.svelte.d.ts +17 -0
  15. package/dist/code.d.ts +12 -0
  16. package/dist/code.js +15 -0
  17. package/dist/dom.d.ts +29 -0
  18. package/dist/dom.js +224 -0
  19. package/dist/i18n.d.ts +7 -0
  20. package/dist/i18n.js +18 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.js +16 -0
  23. package/dist/kit/Empty.svelte +24 -0
  24. package/dist/kit/Empty.svelte.d.ts +8 -0
  25. package/dist/kit/Frame.svelte +76 -0
  26. package/dist/kit/Frame.svelte.d.ts +15 -0
  27. package/dist/kit/util.d.ts +13 -0
  28. package/dist/kit/util.js +28 -0
  29. package/dist/locales/hu.d.ts +2 -0
  30. package/dist/locales/hu.js +142 -0
  31. package/dist/markdown.d.ts +23 -0
  32. package/dist/markdown.js +219 -0
  33. package/dist/model.d.ts +21 -0
  34. package/dist/model.js +45 -0
  35. package/dist/palette.d.ts +19 -0
  36. package/dist/palette.js +23 -0
  37. package/dist/plugins/Gap.svelte +28 -0
  38. package/dist/plugins/Gap.svelte.d.ts +15 -0
  39. package/dist/plugins/GapPreview.svelte +17 -0
  40. package/dist/plugins/GapPreview.svelte.d.ts +5 -0
  41. package/dist/plugins/Map.svelte +47 -0
  42. package/dist/plugins/Map.svelte.d.ts +12 -0
  43. package/dist/plugins/MapPreview.svelte +26 -0
  44. package/dist/plugins/MapPreview.svelte.d.ts +5 -0
  45. package/dist/plugins/Table.svelte +537 -0
  46. package/dist/plugins/Table.svelte.d.ts +28 -0
  47. package/dist/plugins/TablePreview.svelte +62 -0
  48. package/dist/plugins/TablePreview.svelte.d.ts +5 -0
  49. package/dist/plugins/YouTube.svelte +52 -0
  50. package/dist/plugins/YouTube.svelte.d.ts +8 -0
  51. package/dist/plugins/YouTubePreview.svelte +76 -0
  52. package/dist/plugins/YouTubePreview.svelte.d.ts +5 -0
  53. package/dist/plugins/index.d.ts +20 -0
  54. package/dist/plugins/index.js +105 -0
  55. package/dist/plugins/util.d.ts +3 -0
  56. package/dist/plugins/util.js +8 -0
  57. package/dist/schema.d.ts +11 -0
  58. package/dist/schema.js +25 -0
  59. package/dist/scroll.d.ts +18 -0
  60. package/dist/scroll.js +57 -0
  61. package/dist/types.d.ts +132 -0
  62. package/dist/types.js +43 -0
  63. package/dist/view.d.ts +6 -0
  64. package/dist/view.js +11 -0
  65. package/docs/en/bundled-plugins.md +99 -0
  66. package/docs/en/documents-and-markdown.md +114 -0
  67. package/docs/en/editor-and-preview.md +139 -0
  68. package/docs/en/getting-started.md +85 -0
  69. package/docs/en/host-contexts.md +169 -0
  70. package/docs/en/index.md +38 -0
  71. package/docs/en/recipes-and-pitfalls.md +123 -0
  72. package/docs/en/server-side.md +95 -0
  73. package/docs/en/upgrading.md +71 -0
  74. package/docs/en/writing-plugins.md +405 -0
  75. package/docs/hu/bundled-plugins.md +99 -0
  76. package/docs/hu/documents-and-markdown.md +114 -0
  77. package/docs/hu/editor-and-preview.md +139 -0
  78. package/docs/hu/getting-started.md +85 -0
  79. package/docs/hu/host-contexts.md +169 -0
  80. package/docs/hu/index.md +38 -0
  81. package/docs/hu/recipes-and-pitfalls.md +123 -0
  82. package/docs/hu/server-side.md +95 -0
  83. package/docs/hu/upgrading.md +71 -0
  84. package/docs/hu/writing-plugins.md +405 -0
  85. package/package.json +114 -5
@@ -0,0 +1,76 @@
1
+ <svelte:options css="injected" />
2
+
3
+ <script lang="ts">
4
+ import { useText } from '../i18n';
5
+ import type { PluginPreviewProps } from '../types';
6
+ import type { YouTubeProps } from './YouTube.svelte';
7
+
8
+ let { props }: PluginPreviewProps<YouTubeProps> = $props();
9
+ const tr = useText();
10
+ const id = $derived(/^[\w-]{11}$/.test(props.id ?? '') ? props.id : '');
11
+ // The player loads only on click (no YouTube request before that but the thumbnail).
12
+ let playing = $state(false);
13
+ </script>
14
+
15
+ {#if id}
16
+ <figure class="yt">
17
+ {#if playing}
18
+ <iframe
19
+ src="https://www.youtube-nocookie.com/embed/{id}?autoplay=1"
20
+ title={props.title || tr('YouTube video')}
21
+ allow="autoplay; encrypted-media; picture-in-picture"
22
+ allowfullscreen
23
+ ></iframe>
24
+ {:else}
25
+ <button type="button" onclick={() => (playing = true)} aria-label={tr('Play {title}', { title: props.title || tr('video') })}>
26
+ <img src="https://i.ytimg.com/vi/{id}/hqdefault.jpg" alt="" />
27
+ <span class="play" aria-hidden="true">▶</span>
28
+ </button>
29
+ {/if}
30
+ {#if props.title}<figcaption>{props.title}</figcaption>{/if}
31
+ </figure>
32
+ {/if}
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>
@@ -0,0 +1,5 @@
1
+ import type { PluginPreviewProps } from '../types';
2
+ import type { YouTubeProps } from './YouTube.svelte';
3
+ declare const YouTubePreview: import("svelte").Component<PluginPreviewProps<YouTubeProps>, {}, "">;
4
+ type YouTubePreview = ReturnType<typeof YouTubePreview>;
5
+ export default YouTubePreview;
@@ -0,0 +1,20 @@
1
+ import type { BlockPlugin } from '../types';
2
+ import { type GapProps } from './Gap.svelte';
3
+ import { type MapProps } from './Map.svelte';
4
+ import { type TableProps } from './Table.svelte';
5
+ import { type YouTubeProps } from './YouTube.svelte';
6
+ /** A YouTube video, loaded on click. */
7
+ export declare const youtube: BlockPlugin<YouTubeProps>;
8
+ /** Rows and columns of inline-formatted cells (price lists, fines…); edited in place, rows and columns dragged by their
9
+ * grips. The first row can be a header, the last a footer (sums). */
10
+ export declare const table: BlockPlugin<TableProps>;
11
+ /** A Google Maps embed of a place or an address. */
12
+ export declare const map: BlockPlugin<MapProps>;
13
+ /** Empty space between blocks, some lines of text high; only its height can be set. */
14
+ export declare const gap: BlockPlugin<GapProps>;
15
+ export declare const PLUGINS: BlockPlugin[];
16
+ export { youtubeId } from './util';
17
+ export type { GapProps, MapProps, TableProps, YouTubeProps };
18
+ export { square, tableHeads, rowHead, HEADS, type Head } from './Table.svelte';
19
+ export { GAP_LINES, GAP_MAX, gapLines } from './Gap.svelte';
20
+ export { mapSrc } from './Map.svelte';
@@ -0,0 +1,105 @@
1
+ // The bundled plugins: plugins like any other (written with the kit), an app picks the ones it wants. Only general
2
+ // blocks live here (video, map, table, gap); anything an app gives its own meaning to belongs in the app.
3
+ import { MapPin, PanelBottom, PanelTop, SquarePlay, Table, UnfoldVertical } from '@lucide/svelte';
4
+ import { mdText } from '../kit/util';
5
+ import { youtubeId } from './util';
6
+ import Gap, { GAP_LINES } from './Gap.svelte';
7
+ import GapPreview from './GapPreview.svelte';
8
+ import MapView, {} from './Map.svelte';
9
+ import MapPreview from './MapPreview.svelte';
10
+ import TableView, { tableHeads } from './Table.svelte';
11
+ import TablePreview from './TablePreview.svelte';
12
+ import YouTube, {} from './YouTube.svelte';
13
+ import YouTubePreview from './YouTubePreview.svelte';
14
+ // Markdown forms (BlockPlugin.markdown).
15
+ const mdLink = (text, url) => `[${mdText(text) || url}](${url})`;
16
+ /** One table row in GFM; the cells are inline markdown already, only the pipes are escaped. */
17
+ const mdRow = (cells) => `| ${cells.map((c) => (c ?? '').replace(/\|/g, '\\|').replace(/\n/g, '<br>')).join(' | ')} |`;
18
+ /** A YouTube video, loaded on click. */
19
+ export const youtube = {
20
+ type: 'youtube',
21
+ label: 'YouTube',
22
+ hint: 'Video from a link',
23
+ keys: 'youtube video embed',
24
+ icon: SquarePlay,
25
+ props: () => ({ id: '', title: '' }),
26
+ fields: [
27
+ { key: 'id', label: 'Video link or id', kind: 'text', placeholder: 'https://youtu.be/…', parse: youtubeId },
28
+ { key: 'title', label: 'Title', kind: 'text' }
29
+ ],
30
+ tag: () => 'youtube',
31
+ content: 'none',
32
+ markdown: (p) => (p.id ? mdLink(p.title, `https://www.youtube.com/watch?v=${p.id}`) : ''),
33
+ view: YouTube,
34
+ preview: YouTubePreview
35
+ };
36
+ /** Rows and columns of inline-formatted cells (price lists, fines…); edited in place, rows and columns dragged by their
37
+ * grips. The first row can be a header, the last a footer (sums). */
38
+ export const table = {
39
+ type: 'table',
40
+ label: 'Table',
41
+ hint: 'Rows and columns',
42
+ keys: 'table grid rows columns price list',
43
+ icon: Table,
44
+ props: () => ({ rows: [['', ''], ['', '']], heads: ['top'] }),
45
+ fields: [
46
+ {
47
+ key: 'heads',
48
+ label: 'Header and footer',
49
+ kind: 'toggles',
50
+ options: [
51
+ { value: 'top', label: 'Header row', icon: PanelTop },
52
+ { value: 'bottom', label: 'Footer row (e.g. sums)', icon: PanelBottom }
53
+ ]
54
+ }
55
+ ],
56
+ tag: (p) => `table · ${p.rows?.length ?? 0}×${p.rows?.[0]?.length ?? 0}`,
57
+ content: 'none',
58
+ markdown: (p) => {
59
+ const rows = p.rows ?? [];
60
+ if (!rows.length)
61
+ return '';
62
+ const body = tableHeads(p).has('top') ? rows.slice(1) : rows;
63
+ const head = tableHeads(p).has('top') ? rows[0] : rows[0].map(() => '');
64
+ return [mdRow(head), mdRow(head.map(() => '---')), ...body.map(mdRow)].join('\n');
65
+ },
66
+ view: TableView,
67
+ preview: TablePreview
68
+ };
69
+ /** A Google Maps embed of a place or an address. */
70
+ export const map = {
71
+ type: 'map',
72
+ label: 'Map',
73
+ hint: 'Google Maps for a place',
74
+ keys: 'map google location address place',
75
+ icon: MapPin,
76
+ props: () => ({ q: '', zoom: '15' }),
77
+ fields: [
78
+ { key: 'q', label: 'Place or address', kind: 'text', placeholder: 'e.g. Pécs, Széchenyi tér' },
79
+ { key: 'zoom', label: 'Zoom', kind: 'slider', options: Array.from({ length: 20 }, (_, i) => ({ value: String(i + 1), label: String(i + 1) })) }
80
+ ],
81
+ tag: (p) => (p.q ? `map · ${p.q}` : 'map'),
82
+ content: 'none',
83
+ markdown: (p) => (p.q ? mdLink(p.q, `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(String(p.q))}`) : ''),
84
+ view: MapView,
85
+ preview: MapPreview
86
+ };
87
+ /** Empty space between blocks, some lines of text high; only its height can be set. */
88
+ export const gap = {
89
+ type: 'gap',
90
+ label: 'Gap',
91
+ hint: 'Empty space',
92
+ keys: 'gap space spacer whitespace',
93
+ icon: UnfoldVertical,
94
+ props: () => ({ lines: '1' }),
95
+ fields: [{ key: 'lines', label: 'Height', kind: 'slider', options: GAP_LINES }],
96
+ tag: (p) => `gap · ${p.lines ?? '1'}`,
97
+ content: 'none',
98
+ view: Gap,
99
+ preview: GapPreview
100
+ };
101
+ export const PLUGINS = [table, youtube, map, gap];
102
+ export { youtubeId } from './util';
103
+ export { square, tableHeads, rowHead, HEADS } from './Table.svelte';
104
+ export { GAP_LINES, GAP_MAX, gapLines } from './Gap.svelte';
105
+ export { mapSrc } from './Map.svelte';
@@ -0,0 +1,3 @@
1
+ export { lines, safeColor } from '../kit/util';
2
+ /** A YouTube video id from an id or any YouTube link. */
3
+ export declare function youtubeId(s: string): string;
@@ -0,0 +1,8 @@
1
+ // Helpers of the bundled plugins (the general ones are in kit/util.ts).
2
+ export { lines, safeColor } from '../kit/util';
3
+ /** A YouTube video id from an id or any YouTube link. */
4
+ export function youtubeId(s) {
5
+ const t = s.trim();
6
+ const m = /(?:v=|youtu\.be\/|embed\/|shorts\/)([\w-]{11})/.exec(t);
7
+ return m ? m[1] : t.replace(/[^\w-]/g, '').slice(0, 11);
8
+ }
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ import { type StoredBlock } from './model';
3
+ export declare const blocksSchema: z.ZodArray<z.ZodType<StoredBlock, unknown, z.core.$ZodTypeInternals<StoredBlock, unknown>>>;
4
+ /**
5
+ * The stored blocks, also checked against the containers' `accepts` (pass the editor's plugins, or just their
6
+ * `type` and `accepts`): a container may hold only the blocks it takes.
7
+ */
8
+ export declare const blocksSchemaFor: (plugins: readonly {
9
+ type: string;
10
+ accepts?: readonly string[];
11
+ }[]) => z.ZodArray<z.ZodType<StoredBlock, unknown, z.core.$ZodTypeInternals<StoredBlock, unknown>>>;
package/dist/schema.js ADDED
@@ -0,0 +1,25 @@
1
+ // Server-side validation of a stored document (zod). Only the shape is checked: the renderers escape every text,
2
+ // keep links to http(s)/mailto/tel/site paths, take colours only as #hex.
3
+ import { z } from 'zod';
4
+ import { acceptsId, MAX_DEPTH, MAX_INDENT } from './model';
5
+ const block = z.lazy(() => z.strictObject({
6
+ type: z.string().regex(/^[a-z][a-z0-9:-]{0,39}$/),
7
+ indent: z.number().int().min(0).max(MAX_INDENT),
8
+ text: z.string().max(100_000).optional(),
9
+ props: z.record(z.string().max(60), z.json()).optional(),
10
+ blocks: z.array(block).max(500).optional()
11
+ }));
12
+ const depth = (blocks) => Math.max(0, ...blocks.map((b) => (b.blocks ? 1 + depth(b.blocks) : 0)));
13
+ export const blocksSchema = z
14
+ .array(block)
15
+ .max(2000)
16
+ .refine((blocks) => depth(blocks) <= MAX_DEPTH - 1, 'Blocks may hold blocks one level deep only.')
17
+ .refine((blocks) => JSON.stringify(blocks).length <= 1_000_000, 'The content is too large.');
18
+ /**
19
+ * The stored blocks, also checked against the containers' `accepts` (pass the editor's plugins, or just their
20
+ * `type` and `accepts`): a container may hold only the blocks it takes.
21
+ */
22
+ export const blocksSchemaFor = (plugins) => {
23
+ const accepts = new Map(plugins.map((p) => [p.type, p.accepts]));
24
+ return blocksSchema.refine((blocks) => blocks.every((b) => !b.blocks || b.blocks.every((inner) => acceptsId(accepts.get(b.type), inner.type))), 'A block holds a block it does not take.');
25
+ };
@@ -0,0 +1,18 @@
1
+ import type { Action } from 'svelte/action';
2
+ /** How far a box is scrolled, 0…1 (0 when it does not scroll). */
3
+ export declare const scrollFraction: (box: HTMLElement) => number;
4
+ /** Scrolls a box to a fraction (0…1) of its scroll range. */
5
+ export declare const setScrollFraction: (box: HTMLElement, f: number) => void;
6
+ export interface ScrollSync {
7
+ /** The box the reader scrolls (e.g. the editor's). */
8
+ source: Action<HTMLElement>;
9
+ /**
10
+ * The box that follows it (e.g. the preview's); its content changing size keeps it in step too. Its parameter turns
11
+ * it on and off (default on), for a host that switches between two boxes.
12
+ */
13
+ target: Action<HTMLElement, boolean | undefined>;
14
+ /** Puts the target in step now (e.g. after its width changed). */
15
+ align(): void;
16
+ }
17
+ /** Keeps a target box scrolled to the same fraction as the source box. One way: scrolling the target moves nothing else. */
18
+ export declare function scrollSync(): ScrollSync;
package/dist/scroll.js ADDED
@@ -0,0 +1,57 @@
1
+ /** How far a box is scrolled, 0…1 (0 when it does not scroll). */
2
+ export const scrollFraction = (box) => {
3
+ const max = box.scrollHeight - box.clientHeight;
4
+ return max > 0 ? box.scrollTop / max : 0;
5
+ };
6
+ /** Scrolls a box to a fraction (0…1) of its scroll range. */
7
+ export const setScrollFraction = (box, f) => {
8
+ box.scrollTop = Math.min(1, Math.max(0, f)) * (box.scrollHeight - box.clientHeight);
9
+ };
10
+ /** Keeps a target box scrolled to the same fraction as the source box. One way: scrolling the target moves nothing else. */
11
+ export function scrollSync() {
12
+ let source = null;
13
+ let target = null;
14
+ let frame = 0;
15
+ const align = () => {
16
+ frame = 0;
17
+ if (source && target)
18
+ setScrollFraction(target, scrollFraction(source));
19
+ };
20
+ const later = () => (frame ||= requestAnimationFrame(align));
21
+ return {
22
+ source(node) {
23
+ source = node;
24
+ node.addEventListener('scroll', later, { passive: true });
25
+ return {
26
+ destroy() {
27
+ node.removeEventListener('scroll', later);
28
+ if (source === node)
29
+ source = null;
30
+ }
31
+ };
32
+ },
33
+ target(node, on = true) {
34
+ const watch = new ResizeObserver(later);
35
+ const use = (yes = true) => {
36
+ watch.disconnect();
37
+ if (!yes)
38
+ return void (target === node && (target = null));
39
+ target = node;
40
+ for (const child of node.children)
41
+ watch.observe(child);
42
+ watch.observe(node);
43
+ later();
44
+ };
45
+ use(on);
46
+ return {
47
+ update: use,
48
+ destroy() {
49
+ watch.disconnect();
50
+ if (target === node)
51
+ target = null;
52
+ }
53
+ };
54
+ },
55
+ align
56
+ };
57
+ }
@@ -0,0 +1,132 @@
1
+ import type { Component } from 'svelte';
2
+ import type { StoredBlock } from './model';
3
+ import type { Icon } from './palette';
4
+ export type BlockType = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'ul' | 'ol' | 'todo' | 'quote' | 'code' | 'hr' | 'plugin';
5
+ export type Props = Record<string, unknown>;
6
+ export interface Block {
7
+ /** Editor-only identity (rendering, focus, selection, drag); never stored. */
8
+ id: string;
9
+ type: BlockType;
10
+ /** Inline HTML (b, i, u, strike, code, a, br). Always sanitized by construction, never raw paste. */
11
+ html: string;
12
+ indent: number;
13
+ checked?: boolean;
14
+ /** Properties edited in the block's fields: a code block's `lang`, or a plugin's own. */
15
+ props?: Props;
16
+ /** Plugin blocks only: the plugin's stored type and its nested document. */
17
+ plugin?: string;
18
+ blocks?: StoredBlock[];
19
+ }
20
+ export { isMd, MD, type StoredBlock } from './model';
21
+ /**
22
+ * What a block holds: nothing (divider), inline-formatted text (paragraph, heading, list item…), plain text
23
+ * without formatting (code), or a nested list of blocks (box).
24
+ */
25
+ export type Content = 'none' | 'text' | 'plain' | 'blocks';
26
+ /** Content of the built-in block types. */
27
+ export declare const BUILT_IN_CONTENT: Record<Exclude<BlockType, 'plugin'>, Content>;
28
+ /** One editable property, a field on the top of the block. */
29
+ export interface PropField {
30
+ key: string;
31
+ label: string;
32
+ /**
33
+ * 'toggles' is a row of icon buttons, each switching one of `options` on or off; stored as the list of the ones on
34
+ * (in `options` order).
35
+ * 'slider' steps through `options` in their order (e.g. heights), or, with `min` and `max`, sets a number freely.
36
+ */
37
+ kind: 'text' | 'textarea' | 'color' | 'select' | 'slider' | 'toggles';
38
+ placeholder?: string;
39
+ /** A number slider: its range, step (default 1) and unit shown after the value. */
40
+ min?: number;
41
+ max?: number;
42
+ step?: number;
43
+ unit?: string;
44
+ /**
45
+ * The label of the unset state, where the key is removed: a number slider's position left of `min` (e.g. 'Original'),
46
+ * or an optional colour ('None'; `placeholder` is the colour a click on it starts from).
47
+ */
48
+ none?: string;
49
+ /**
50
+ * A slider with a value per view (e.g. the page's widths); the host sets the current view with `provideView`, a badge
51
+ * shows which one is edited. The first view is
52
+ * the base; each other view, while it has no value of its own, takes the one before it. Stored as the base value
53
+ * alone, or, with own values for other views, as an object by view key.
54
+ */
55
+ views?: {
56
+ key: string;
57
+ label: string;
58
+ }[];
59
+ /** For 'select', 'slider' and 'toggles' (where `icon` is the button and `label` its tooltip). */
60
+ options?: {
61
+ value: string;
62
+ label: string;
63
+ icon?: Icon;
64
+ }[];
65
+ /** The field's text from the stored value; default: the value as a string. */
66
+ format?: (value: unknown) => string;
67
+ /** The stored value from the field's text (e.g. a list from lines, an id from a URL); default: the text. '' removes the key. */
68
+ parse?: (text: string) => unknown;
69
+ }
70
+ /** What a plugin component receives. `props` and `blocks` are bindable. */
71
+ export interface PluginViewProps<P extends Props = Props> {
72
+ props: P;
73
+ blocks: StoredBlock[];
74
+ readonly: boolean;
75
+ /** The host editor's palette and plugins, for a nested editor. */
76
+ palette: string[];
77
+ plugins: BlockPlugin[];
78
+ }
79
+ /** What a plugin's preview receives: the block as stored, and the plugins to render its nested blocks with. */
80
+ export interface PluginPreviewProps<P extends Props = Props> {
81
+ props: P;
82
+ blocks: StoredBlock[];
83
+ plugins: BlockPlugin[];
84
+ }
85
+ /** A custom block for the slash menu. */
86
+ /** What a plugin's `markdown` gets beside its props. */
87
+ export interface MarkdownContext {
88
+ /** The nested blocks as markdown ('' when it has none). */
89
+ inner: string;
90
+ }
91
+ export interface BlockPlugin<P extends Props = Props> {
92
+ /** Stored `type`; must not start with 'md:'. */
93
+ type: string;
94
+ label: string;
95
+ hint: string;
96
+ keys: string;
97
+ icon: Icon;
98
+ /** Properties of a new block. */
99
+ props: () => P;
100
+ /** Its fields, on the top of the block. */
101
+ fields?: PropField[];
102
+ /** The mini label on the top of the block (before its fields). */
103
+ tag(props: P): string;
104
+ /** What the block holds; decides what "Turn into" can offer. 'blocks' gets a nested document. */
105
+ content: Content;
106
+ /** In the editor. */
107
+ view: Component<PluginViewProps<any>, {}, 'props' | 'blocks'>;
108
+ /**
109
+ * On the site: the block as it will be rendered by `<Preview>`. Optional: an app with its own renderer leaves it out,
110
+ * and `<Preview>` then skips the block.
111
+ */
112
+ preview?: Component<PluginPreviewProps<any>>;
113
+ /**
114
+ * For a plugin with nested blocks: the blocks it takes, as palette ids ('md:p', 'youtube'…); 'md:*' stands for every
115
+ * built-in block. Its nested editor offers the host's palette narrowed to these. Default: everything the host has.
116
+ */
117
+ accepts?: string[];
118
+ /**
119
+ * Its markdown form (copying as text, export); `inner` holds its nested blocks already as markdown. Without it, or
120
+ * when it gives '', the block is left out of the markdown.
121
+ */
122
+ markdown?(props: P, ctx: MarkdownContext): string;
123
+ /** At most one per document, and only at the top level (e.g. the supporters-only divider). */
124
+ 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
+ }
128
+ export { MAX_INDENT } from './model';
129
+ export declare const uid: () => string;
130
+ export declare const newBlock: (type?: BlockType, html?: string, extra?: Partial<Block>) => Block;
131
+ /** Ordinal for every `ol` block, restarting after interruptions and per indent level. */
132
+ export declare function numbering(blocks: Block[]): Record<string, number>;
package/dist/types.js ADDED
@@ -0,0 +1,43 @@
1
+ // The stored form is shared with the server (validation, search text, the HTML converter).
2
+ export { isMd, MD } from './model';
3
+ /** Content of the built-in block types. */
4
+ export const BUILT_IN_CONTENT = {
5
+ p: 'text',
6
+ h1: 'text',
7
+ h2: 'text',
8
+ h3: 'text',
9
+ h4: 'text',
10
+ h5: 'text',
11
+ h6: 'text',
12
+ ul: 'text',
13
+ ol: 'text',
14
+ todo: 'text',
15
+ quote: 'text',
16
+ code: 'plain',
17
+ hr: 'none'
18
+ };
19
+ export { MAX_INDENT } from './model';
20
+ export const uid = () => Math.random().toString(36).slice(2, 10);
21
+ export const newBlock = (type = 'p', html = '', extra = {}) => ({
22
+ id: uid(),
23
+ type,
24
+ html,
25
+ indent: 0,
26
+ ...extra
27
+ });
28
+ /** Ordinal for every `ol` block, restarting after interruptions and per indent level. */
29
+ export function numbering(blocks) {
30
+ const out = {};
31
+ const counters = [];
32
+ for (const b of blocks) {
33
+ if (b.type === 'ol') {
34
+ counters.length = b.indent + 1;
35
+ counters[b.indent] = (counters[b.indent] ?? 0) + 1;
36
+ out[b.id] = counters[b.indent];
37
+ }
38
+ else {
39
+ counters.length = Math.min(counters.length, b.indent);
40
+ }
41
+ }
42
+ return out;
43
+ }
package/dist/view.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ type Get = () => string | undefined;
2
+ /** Call in a component's init (around the editor); the function returns the current view's key. */
3
+ export declare function provideView(get: Get): void;
4
+ /** Call in a component's init; the returned function reads the current view's key (undefined: none given). */
5
+ export declare const useView: () => Get;
6
+ export {};
package/dist/view.js ADDED
@@ -0,0 +1,11 @@
1
+ // The view the per-view fields edit (see PropField.views). Which views there are and which one is shown is the
2
+ // host's business (e.g. the width its preview shows): it gives the current one through the context. Without it, a
3
+ // per-view field simply edits its base value.
4
+ import { getContext, setContext } from 'svelte';
5
+ const KEY = 'block-editor:view';
6
+ /** Call in a component's init (around the editor); the function returns the current view's key. */
7
+ export function provideView(get) {
8
+ setContext(KEY, get);
9
+ }
10
+ /** Call in a component's init; the returned function reads the current view's key (undefined: none given). */
11
+ export const useView = () => getContext(KEY) ?? (() => undefined);
@@ -0,0 +1,99 @@
1
+ # Bundled plugins
2
+
3
+ [← Previous](documents-and-markdown.md) · [Contents](index.md) · [Next →](writing-plugins.md)
4
+
5
+ `@atom-forge/brickee/plugins` holds four general-purpose plugins. They are written only with what the core exports to any plugin author, so they double as examples, and you take only the ones you need.
6
+
7
+ ```ts
8
+ import { PLUGINS } from '@atom-forge/brickee/plugins'; // all four
9
+ import { table, youtube } from '@atom-forge/brickee/plugins'; // or some
10
+ ```
11
+
12
+ ## The plugins
13
+
14
+ | Export | `type` | Fields | Markdown form | What it is |
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. |
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
+ | `map` | `map` | place or address, zoom slider | a link | A Google Maps embed of a place or an address. |
19
+ | `gap` | `gap` | height slider | none | Empty space between blocks, 1–8 lines high. |
20
+
21
+ None of them holds nested blocks. Image blocks, boxes and other containers are application-specific in look and behaviour, so the package leaves them to you: [Writing plugins](writing-plugins.md) shows a container, and [Host contexts](host-contexts.md) an image block.
22
+
23
+ Besides the plugins, the entry point exports their props types (`TableProps`, `YouTubeProps`, `MapProps`, `GapProps`) and the helpers their previews use, which are useful when you write your own previews: `youtubeId` (a video id from any YouTube link), `mapSrc` (the embed address of a place), `gapLines`, `GAP_LINES` and `GAP_MAX`, and `tableHeads`, `rowHead`, `HEADS` and `square` for tables.
24
+
25
+ ## Overriding a preview
26
+
27
+ A plugin is a plain object, so adapting one is a spread. The most common change is the preview: the editor view stays the package's, and the site renders the block with your own component — so the editor's preview and the published page are exactly the same.
28
+
29
+ ```ts
30
+ // blocks.ts
31
+ import type { BlockPlugin } from '@atom-forge/brickee';
32
+ import { gap, table, youtube } from '@atom-forge/brickee/plugins';
33
+ import SiteYouTube from './SiteYouTube.svelte';
34
+
35
+ const withPreview = (plugin: BlockPlugin, preview: BlockPlugin['preview']): BlockPlugin => ({ ...plugin, preview });
36
+
37
+ export const plugins: BlockPlugin[] = [withPreview(youtube, SiteYouTube), table, gap];
38
+ ```
39
+
40
+ A replacement preview receives the same props as the original (`PluginPreviewProps`: `props`, `blocks`, `plugins`). This one shows a thumbnail that links to the video instead of embedding a player:
41
+
42
+ ```svelte
43
+ <!-- SiteYouTube.svelte -->
44
+ <script lang="ts">
45
+ import type { PluginPreviewProps } from '@atom-forge/brickee';
46
+ import { youtubeId, type YouTubeProps } from '@atom-forge/brickee/plugins';
47
+
48
+ let { props }: PluginPreviewProps<YouTubeProps> = $props();
49
+ // props are user input: keep only a well-formed video id
50
+ const id = $derived(youtubeId(String(props.id ?? '')));
51
+ </script>
52
+
53
+ {#if id.length === 11}
54
+ <a class="video" href="https://www.youtube.com/watch?v={id}" target="_blank" rel="noopener noreferrer">
55
+ <img src="https://img.youtube.com/vi/{id}/hqdefault.jpg" alt={props.title || 'YouTube video'} />
56
+ </a>
57
+ {/if}
58
+ ```
59
+
60
+ ## Adding fields
61
+
62
+ New settings are new fields and new initial props. The plugin's view does not need to change: the editor draws every field on the top of the block, and your preview reads the new props.
63
+
64
+ ```ts
65
+ import type { BlockPlugin } from '@atom-forge/brickee';
66
+ import { map } from '@atom-forge/brickee/plugins';
67
+ import SiteMap from './SiteMap.svelte';
68
+
69
+ export const siteMap: BlockPlugin = {
70
+ ...map,
71
+ props: () => ({ ...map.props(), caption: '' }),
72
+ fields: [...(map.fields ?? []), { key: 'caption', label: 'Caption', kind: 'text' }],
73
+ preview: SiteMap
74
+ };
75
+ ```
76
+
77
+ ```svelte
78
+ <!-- SiteMap.svelte -->
79
+ <script lang="ts">
80
+ import type { PluginPreviewProps } from '@atom-forge/brickee';
81
+ import { mapSrc, type MapProps } from '@atom-forge/brickee/plugins';
82
+
83
+ let { props }: PluginPreviewProps<MapProps> = $props();
84
+ const src = $derived(mapSrc(props));
85
+ </script>
86
+
87
+ {#if src}
88
+ <figure>
89
+ <iframe title={props.q} {src} loading="lazy"></iframe>
90
+ {#if props.caption}<figcaption>{props.caption}</figcaption>{/if}
91
+ </figure>
92
+ {/if}
93
+ ```
94
+
95
+ Add the new field labels to your `messages` if the editor is not in English (see [UI text](host-contexts.md#ui-text)). To make an existing slider per-view, map over `fields` and add `views` to it (see [Per-view values](host-contexts.md#per-view-values)).
96
+
97
+ > **Keep the type**
98
+ >
99
+ > Keep the plugin's `type` when you override it: stored documents refer to blocks by type. A new `type` makes a new block, and existing documents will not find their blocks.