@atom-forge/brickee 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README-AI.md +283 -0
  4. package/README.md +41 -2
  5. package/dist/Editor.svelte +1968 -0
  6. package/dist/Editor.svelte.d.ts +22 -0
  7. package/dist/ImagePicker.svelte +131 -0
  8. package/dist/ImagePicker.svelte.d.ts +13 -0
  9. package/dist/LinkPrompt.svelte +42 -0
  10. package/dist/LinkPrompt.svelte.d.ts +8 -0
  11. package/dist/PluginHost.svelte +41 -0
  12. package/dist/PluginHost.svelte.d.ts +13 -0
  13. package/dist/Preview.svelte +186 -0
  14. package/dist/Preview.svelte.d.ts +35 -0
  15. package/dist/PropsBar.svelte +395 -0
  16. package/dist/PropsBar.svelte.d.ts +20 -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 +147 -0
  31. package/dist/markdown.d.ts +25 -0
  32. package/dist/markdown.js +219 -0
  33. package/dist/media.d.ts +17 -0
  34. package/dist/media.js +12 -0
  35. package/dist/model.d.ts +21 -0
  36. package/dist/model.js +45 -0
  37. package/dist/palette.d.ts +19 -0
  38. package/dist/palette.js +23 -0
  39. package/dist/plugins/Gap.svelte +28 -0
  40. package/dist/plugins/Gap.svelte.d.ts +15 -0
  41. package/dist/plugins/GapPreview.svelte +17 -0
  42. package/dist/plugins/GapPreview.svelte.d.ts +5 -0
  43. package/dist/plugins/Map.svelte +47 -0
  44. package/dist/plugins/Map.svelte.d.ts +12 -0
  45. package/dist/plugins/MapPreview.svelte +26 -0
  46. package/dist/plugins/MapPreview.svelte.d.ts +5 -0
  47. package/dist/plugins/Table.svelte +563 -0
  48. package/dist/plugins/Table.svelte.d.ts +28 -0
  49. package/dist/plugins/TablePreview.svelte +62 -0
  50. package/dist/plugins/TablePreview.svelte.d.ts +5 -0
  51. package/dist/plugins/YouTube.svelte +52 -0
  52. package/dist/plugins/YouTube.svelte.d.ts +8 -0
  53. package/dist/plugins/YouTubePreview.svelte +76 -0
  54. package/dist/plugins/YouTubePreview.svelte.d.ts +5 -0
  55. package/dist/plugins/index.d.ts +20 -0
  56. package/dist/plugins/index.js +94 -0
  57. package/dist/plugins/util.d.ts +3 -0
  58. package/dist/plugins/util.js +8 -0
  59. package/dist/schema.d.ts +11 -0
  60. package/dist/schema.js +25 -0
  61. package/dist/scroll.d.ts +18 -0
  62. package/dist/scroll.js +57 -0
  63. package/dist/types.d.ts +133 -0
  64. package/dist/types.js +43 -0
  65. package/dist/view.d.ts +6 -0
  66. package/dist/view.js +11 -0
  67. package/docs/en/bundled-plugins.md +99 -0
  68. package/docs/en/documents-and-markdown.md +115 -0
  69. package/docs/en/editor-and-preview.md +141 -0
  70. package/docs/en/getting-started.md +81 -0
  71. package/docs/en/host-contexts.md +275 -0
  72. package/docs/en/index.md +38 -0
  73. package/docs/en/recipes-and-pitfalls.md +122 -0
  74. package/docs/en/server-side.md +95 -0
  75. package/docs/en/upgrading.md +55 -0
  76. package/docs/en/writing-plugins.md +305 -0
  77. package/docs/hu/bundled-plugins.md +99 -0
  78. package/docs/hu/documents-and-markdown.md +115 -0
  79. package/docs/hu/editor-and-preview.md +141 -0
  80. package/docs/hu/getting-started.md +81 -0
  81. package/docs/hu/host-contexts.md +275 -0
  82. package/docs/hu/index.md +38 -0
  83. package/docs/hu/recipes-and-pitfalls.md +122 -0
  84. package/docs/hu/server-side.md +95 -0
  85. package/docs/hu/upgrading.md +55 -0
  86. package/docs/hu/writing-plugins.md +305 -0
  87. package/package.json +110 -5
@@ -0,0 +1,22 @@
1
+ import { type MediaItem } from './media';
2
+ import { type Messages } from './i18n';
3
+ import { type BlockPlugin, type StoredBlock } from './types';
4
+ interface Props {
5
+ /** The document: built-in blocks as markdown snippets, plugin blocks with their props. */
6
+ value?: StoredBlock[];
7
+ readonly?: boolean;
8
+ placeholder?: string;
9
+ /** Built-in block types and plugin types offered in the slash menu, in this order. Shortcuts and paste respect it too. */
10
+ palette?: string[];
11
+ /** Custom blocks this editor can show and offer. */
12
+ plugins?: BlockPlugin[];
13
+ /** Set by plugins with nested blocks: a compact editor inside another block. */
14
+ nested?: boolean;
15
+ /** Images the blocks can pick from (the page's attachments); nested editors share their parent's. */
16
+ media?: MediaItem[];
17
+ /** UI text in the host's language (English text -> translation); nested editors share their parent's. */
18
+ messages?: Messages;
19
+ }
20
+ declare const Editor: import("svelte").Component<Props, {}, "value">;
21
+ type Editor = ReturnType<typeof Editor>;
22
+ export default Editor;
@@ -0,0 +1,131 @@
1
+ <!-- The image picker popup of an image field (see PropsBar): every image attached to the document, grouped. One image:
2
+ a click picks it and closes; several: clicks add and remove (the numbers show the order), Done or Escape closes. -->
3
+ <script lang="ts">
4
+ import { Button, getPopupManager } from '@atom-forge/ui';
5
+ import type { Translate } from './i18n';
6
+ import type { MediaItem } from './media';
7
+
8
+ interface Props {
9
+ media: MediaItem[];
10
+ multiple: boolean;
11
+ /** The picked ids as they are now, in order. */
12
+ getPicked: () => string[];
13
+ pick: (id: string) => void;
14
+ tr: Translate;
15
+ }
16
+ let { media, multiple, getPicked, pick, tr }: Props = $props();
17
+ const popups = getPopupManager();
18
+ const picked = $derived(getPicked());
19
+
20
+ function choose(id: string) {
21
+ pick(id);
22
+ if (!multiple) popups.resolve('focus');
23
+ }
24
+ function onkeydown(e: KeyboardEvent) {
25
+ if (e.key === 'Escape') {
26
+ e.preventDefault();
27
+ popups.resolve('focus');
28
+ }
29
+ }
30
+ let root = $state<HTMLElement>();
31
+ $effect(() => root?.focus());
32
+ </script>
33
+
34
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
35
+ <div class="light picker" role="dialog" aria-label={tr('Images')} tabindex="-1" {onkeydown} bind:this={root}>
36
+ {#if media.length}
37
+ <div class="grid">
38
+ {#each media as m, n (m.id)}
39
+ {@const at = picked.indexOf(m.id)}
40
+ {#if m.group && m.group !== media[n - 1]?.group}<span class="group">{m.group}</span>{/if}
41
+ <button type="button" class="thumb" class:on={at >= 0} title={m.name} aria-pressed={at >= 0} onclick={() => choose(m.id)}>
42
+ <img src={m.url} alt={m.name} />
43
+ {#if at >= 0}<span class="n">{multiple ? at + 1 : '✓'}</span>{/if}
44
+ </button>
45
+ {/each}
46
+ </div>
47
+ {#if multiple}
48
+ <div class="foot">
49
+ <span>{tr('{n} picked', { n: picked.length })}</span>
50
+ <Button compact onclick={() => popups.resolve('focus')}>{tr('Done')}</Button>
51
+ </div>
52
+ {/if}
53
+ {:else}
54
+ <p class="none">{tr('No images attached to this page yet.')}</p>
55
+ {/if}
56
+ </div>
57
+
58
+ <style>
59
+ .picker {
60
+ width: 360px;
61
+ max-height: min(440px, calc(100vh - 24px));
62
+ overflow: auto;
63
+ padding: 10px;
64
+ display: grid;
65
+ gap: 10px;
66
+ background: #fff;
67
+ color: #1d1d1f;
68
+ border: 1px solid #e5e5ea;
69
+ border-radius: 12px;
70
+ box-shadow: 0 10px 36px rgba(0, 0, 0, 0.2);
71
+ font: 13px/1.4 system-ui, sans-serif;
72
+ outline: none;
73
+ }
74
+ .grid {
75
+ display: grid;
76
+ grid-template-columns: repeat(5, 1fr);
77
+ gap: 4px;
78
+ }
79
+ .group {
80
+ grid-column: 1 / -1;
81
+ margin-top: 4px;
82
+ color: #8e8e96;
83
+ font-size: 11px;
84
+ }
85
+ .thumb {
86
+ position: relative;
87
+ padding: 0;
88
+ border: 2px solid transparent;
89
+ border-radius: 7px;
90
+ background: none;
91
+ cursor: pointer;
92
+ }
93
+ .thumb img {
94
+ display: block;
95
+ width: 100%;
96
+ aspect-ratio: 1;
97
+ object-fit: cover;
98
+ border-radius: 5px;
99
+ }
100
+ .thumb.on {
101
+ border-color: var(--color-accent, #ea580c);
102
+ }
103
+ .n {
104
+ position: absolute;
105
+ top: 2px;
106
+ right: 2px;
107
+ min-width: 16px;
108
+ height: 16px;
109
+ padding: 0 3px;
110
+ border-radius: 8px;
111
+ background: var(--color-accent, #ea580c);
112
+ color: #fff;
113
+ font: 600 10px/16px system-ui, sans-serif;
114
+ }
115
+ .foot {
116
+ position: sticky;
117
+ bottom: -10px;
118
+ margin: 0 -10px -10px;
119
+ padding: 8px 10px;
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: space-between;
123
+ background: #fff;
124
+ border-top: 1px solid #e5e5ea;
125
+ color: #8e8e96;
126
+ }
127
+ .none {
128
+ margin: 0;
129
+ color: #8e8e96;
130
+ }
131
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Translate } from './i18n';
2
+ import type { MediaItem } from './media';
3
+ interface Props {
4
+ media: MediaItem[];
5
+ multiple: boolean;
6
+ /** The picked ids as they are now, in order. */
7
+ getPicked: () => string[];
8
+ pick: (id: string) => void;
9
+ tr: Translate;
10
+ }
11
+ declare const ImagePicker: import("svelte").Component<Props, {}, "">;
12
+ type ImagePicker = ReturnType<typeof ImagePicker>;
13
+ export default ImagePicker;
@@ -0,0 +1,42 @@
1
+ <!-- The link field of the formatting toolbar (Ctrl+K), in an atom-forge popup: resolves with the address, '' to remove the link. -->
2
+ <script lang="ts">
3
+ import { Button, Input, getPopupManager } from '@atom-forge/ui';
4
+ import type { Translate } from './i18n';
5
+
6
+ let { href = '', tr }: { href?: string; tr: Translate } = $props();
7
+ const popups = getPopupManager();
8
+ // svelte-ignore state_referenced_locally
9
+ let url = $state(href);
10
+ let input = $state<{ focus: () => void }>();
11
+ $effect(() => input?.focus());
12
+
13
+ function onkeydown(e: KeyboardEvent) {
14
+ if (e.key === 'Enter') {
15
+ e.preventDefault();
16
+ popups.resolve(url.trim());
17
+ } else if (e.key === 'Escape') {
18
+ e.preventDefault();
19
+ popups.resolve(undefined);
20
+ }
21
+ }
22
+ </script>
23
+
24
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
25
+ <div class="light link" {onkeydown}>
26
+ <Input bind:this={input} compact monospace bind:value={url} placeholder="https://…" />
27
+ <Button compact accent label={tr('OK')} onclick={() => popups.resolve(url.trim())} />
28
+ {#if href}<Button compact ghost label={tr('Remove')} onclick={() => popups.resolve('')} />{/if}
29
+ </div>
30
+
31
+ <style>
32
+ .link {
33
+ display: flex;
34
+ gap: 6px;
35
+ width: 340px;
36
+ padding: 8px;
37
+ background: #fff;
38
+ border: 1px solid #e5e5ea;
39
+ border-radius: 12px;
40
+ box-shadow: 0 10px 36px rgba(0, 0, 0, 0.2);
41
+ }
42
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { Translate } from './i18n';
2
+ type $$ComponentProps = {
3
+ href?: string;
4
+ tr: Translate;
5
+ };
6
+ declare const LinkPrompt: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type LinkPrompt = ReturnType<typeof LinkPrompt>;
8
+ export default LinkPrompt;
@@ -0,0 +1,41 @@
1
+ <!-- One plugin block in the editor: its view, and its fields (PropsBar). A plugin drawn in a Frame gets the fields inside
2
+ the frame, on its top (the Frame claims them through the context); any other gets them above its view. A container
3
+ gets the host's palette narrowed to its `accepts`. -->
4
+ <script lang="ts" module>
5
+ import { getContext, setContext, type Snippet } from 'svelte';
6
+
7
+ const KEY = Symbol('block-fields');
8
+ type Host = { fields: Snippet | undefined; claimed: boolean };
9
+
10
+ /** For a frame: takes the block's fields to draw them itself; undefined when there are none (or read-only). */
11
+ export function claimFields(): Snippet | undefined {
12
+ const host = getContext<Host | undefined>(KEY);
13
+ if (!host) return undefined;
14
+ host.claimed = true;
15
+ return host.fields;
16
+ }
17
+ </script>
18
+
19
+ <script lang="ts">
20
+ import { narrowPalette } from './palette';
21
+ import type { BlockPlugin, PluginViewProps } from './types';
22
+
23
+ interface Props extends Omit<PluginViewProps, 'props' | 'blocks'> {
24
+ plugin: BlockPlugin;
25
+ props: PluginViewProps['props'];
26
+ blocks: PluginViewProps['blocks'];
27
+ fields?: Snippet;
28
+ }
29
+ let { plugin, props = $bindable(), blocks = $bindable(), readonly, palette, plugins, fields }: Props = $props();
30
+
31
+ // svelte-ignore state_referenced_locally
32
+ const host = $state<Host>({ fields, claimed: false });
33
+ setContext(KEY, host);
34
+ $effect.pre(() => {
35
+ host.fields = fields;
36
+ });
37
+ </script>
38
+
39
+ {#if fields && !host.claimed}{@render fields()}{/if}
40
+ <!-- a container's nested editor offers only the blocks it accepts -->
41
+ <plugin.view bind:props bind:blocks {readonly} palette={narrowPalette(palette, plugin.accepts)} {plugins} />
@@ -0,0 +1,13 @@
1
+ import { type Snippet } from 'svelte';
2
+ /** For a frame: takes the block's fields to draw them itself; undefined when there are none (or read-only). */
3
+ export declare function claimFields(): Snippet | undefined;
4
+ import type { BlockPlugin, PluginViewProps } from './types';
5
+ interface Props extends Omit<PluginViewProps, 'props' | 'blocks'> {
6
+ plugin: BlockPlugin;
7
+ props: PluginViewProps['props'];
8
+ blocks: PluginViewProps['blocks'];
9
+ fields?: Snippet;
10
+ }
11
+ declare const PluginHost: import("svelte").Component<Props, {}, "blocks" | "props">;
12
+ type PluginHost = ReturnType<typeof PluginHost>;
13
+ export default PluginHost;
@@ -0,0 +1,186 @@
1
+ <!-- Styles travel with the markup: the sites show the blocks as server-rendered HTML (see content.server.ts). -->
2
+ <svelte:options css="injected" />
3
+
4
+ <script lang="ts" module>
5
+ import type { StoredBlock } from './types';
6
+
7
+ const LIST = new Set(['md:ul', 'md:ol', 'md:todo']);
8
+
9
+ /** Consecutive list blocks as properly nested lists: an item one level deeper goes into the previous item. */
10
+ export type ListNode = { type: string; items: { block: StoredBlock; children: ListNode[] }[] };
11
+ export type Part = { kind: 'block'; block: StoredBlock } | { kind: 'lists'; lists: ListNode[] };
12
+
13
+ function nest(run: StoredBlock[], i: number, level: number): [ListNode[], number] {
14
+ const out: ListNode[] = [];
15
+ let cur: ListNode | null = null;
16
+ while (i < run.length && run[i].indent >= level) {
17
+ const b = run[i];
18
+ if (b.indent > level && cur) {
19
+ const [kids, next] = nest(run, i, level + 1);
20
+ cur.items[cur.items.length - 1].children.push(...kids);
21
+ i = next;
22
+ continue;
23
+ }
24
+ // A different list type at the same level starts a new list.
25
+ if (!cur || cur.type !== b.type) out.push((cur = { type: b.type, items: [] }));
26
+ cur.items.push({ block: b, children: [] });
27
+ i++;
28
+ }
29
+ return [out, i];
30
+ }
31
+
32
+ export function parts(blocks: StoredBlock[]): Part[] {
33
+ const out: Part[] = [];
34
+ for (let i = 0; i < blocks.length; ) {
35
+ if (!LIST.has(blocks[i].type)) {
36
+ out.push({ kind: 'block', block: blocks[i++] });
37
+ continue;
38
+ }
39
+ let j = i;
40
+ while (j < blocks.length && LIST.has(blocks[j].type)) j++;
41
+ const run = blocks.slice(i, j);
42
+ out.push({ kind: 'lists', lists: nest(run, 0, Math.min(...run.map((b) => b.indent)))[0] });
43
+ i = j;
44
+ }
45
+ return out;
46
+ }
47
+ </script>
48
+
49
+ <script lang="ts">
50
+ import { useText } from './i18n';
51
+ import { inline } from './markdown';
52
+ import { provideMedia, type MediaItem } from './media';
53
+ import { provideText, type Messages } from './i18n';
54
+ import type { BlockPlugin } from './types';
55
+
56
+ interface Props {
57
+ blocks: StoredBlock[];
58
+ plugins: BlockPlugin[];
59
+ /** Set by plugins rendering their nested blocks; only the top level splits at the supporters-only divider. */
60
+ nested?: boolean;
61
+ /** The images blocks refer to by id; nested previews share their parent's. */
62
+ media?: MediaItem[];
63
+ /** UI text in the host's language; nested previews share their parent's. */
64
+ messages?: Messages;
65
+ /** Mark the blocks after a gate (the supporters-only divider) with a label and a dashed line; default: yes. */
66
+ markGated?: boolean;
67
+ }
68
+ let { blocks, plugins, nested = false, media, messages, markGated = true }: Props = $props();
69
+
70
+ provideMedia(() => media);
71
+ provideText(() => messages);
72
+ const tr = useText();
73
+
74
+ const pluginOf = (type: string) => plugins.find((p) => p.type === type);
75
+
76
+ // Everything after a unique supporters-only divider is for supporters: shown here, marked as such.
77
+ const lockAt = $derived(nested || !markGated ? -1 : blocks.findIndex((b) => pluginOf(b.type)?.gate));
78
+ const open = $derived(lockAt < 0 ? blocks : blocks.slice(0, lockAt + 1));
79
+ const locked = $derived(lockAt < 0 ? [] : blocks.slice(lockAt + 1));
80
+
81
+ // Inline markdown -> html: text is escaped first, links are http(s), mailto, tel or site-relative only (see inline()).
82
+ const html = (b: StoredBlock) => inline(b.text ?? '');
83
+ const indent = (b: StoredBlock) => (b.indent ? `${b.indent * 1.5}em` : undefined);
84
+ </script>
85
+
86
+ {#snippet list(node: ListNode)}
87
+ {#if node.type === 'md:ol'}
88
+ <ol>{@render items(node)}</ol>
89
+ {:else}
90
+ <ul class:todo={node.type === 'md:todo'}>{@render items(node)}</ul>
91
+ {/if}
92
+ {/snippet}
93
+
94
+ {#snippet items(node: ListNode)}
95
+ {#each node.items as it, n (n)}
96
+ <li>
97
+ {#if node.type === 'md:todo'}
98
+ <input type="checkbox" checked={!!it.block.props?.checked} disabled aria-label={tr('Done')} />
99
+ {/if}
100
+ {@html html(it.block)}
101
+ {#each it.children as child, c (c)}{@render list(child)}{/each}
102
+ </li>
103
+ {/each}
104
+ {/snippet}
105
+
106
+ {#snippet run(items: StoredBlock[])}
107
+ {#each parts(items) as part, n (n)}
108
+ {#if part.kind === 'lists'}
109
+ {#each part.lists as l, m (m)}{@render list(l)}{/each}
110
+ {:else}
111
+ {@const b = part.block}
112
+ {@const plugin = pluginOf(b.type)}
113
+ {#if b.type === 'md:p'}
114
+ {#if b.text}<p style:margin-left={indent(b)}>{@html html(b)}</p>{/if}
115
+ {:else if b.type === 'md:h1'}
116
+ <h1>{@html html(b)}</h1>
117
+ {:else if b.type === 'md:h2'}
118
+ <h2>{@html html(b)}</h2>
119
+ {:else if b.type === 'md:h3'}
120
+ <h3>{@html html(b)}</h3>
121
+ {:else if b.type === 'md:h4'}
122
+ <h4>{@html html(b)}</h4>
123
+ {:else if b.type === 'md:h5'}
124
+ <h5>{@html html(b)}</h5>
125
+ {:else if b.type === 'md:h6'}
126
+ <h6>{@html html(b)}</h6>
127
+ {:else if b.type === 'md:quote'}
128
+ <blockquote style:margin-left={indent(b)}><p>{@html html(b)}</p></blockquote>
129
+ {:else if b.type === 'md:code'}
130
+ {@const lang = String(b.props?.lang ?? '')}
131
+ <pre><code class={lang ? `language-${lang}` : undefined}>{b.text ?? ''}</code></pre>
132
+ {:else if b.type === 'md:hr'}
133
+ <hr />
134
+ {:else if plugin?.preview}
135
+ <!-- a plugin without a preview is drawn by the app's own renderer, not here -->
136
+ <plugin.preview props={b.props ?? {}} blocks={b.blocks ?? []} {plugins} />
137
+ {/if}
138
+ {/if}
139
+ {/each}
140
+ {/snippet}
141
+
142
+ <div class="preview" class:nested>
143
+ {@render run(open)}
144
+ {#if locked.length}
145
+ <div class="supporters" aria-label={tr('For supporters only')}>
146
+ <p class="supporters-label">{tr('For supporters only')}</p>
147
+ {@render run(locked)}
148
+ </div>
149
+ {/if}
150
+ </div>
151
+
152
+ <style>
153
+ /* Only what the renderer itself adds; the article typography comes from the page that shows it. */
154
+ .supporters {
155
+ margin-top: 1.5em;
156
+ padding-top: 1em;
157
+ border-top: 2px dashed #f96854;
158
+ }
159
+ .supporters-label {
160
+ font: 600 11px/1 system-ui, sans-serif;
161
+ letter-spacing: 0.08em;
162
+ text-transform: uppercase;
163
+ color: #f96854;
164
+ }
165
+ .preview :global(ul.todo) {
166
+ list-style: none;
167
+ padding-left: 0.2em;
168
+ }
169
+ .preview :global(ul.todo input) {
170
+ margin-right: 0.5em;
171
+ }
172
+ .preview :global(img) {
173
+ max-width: 100%;
174
+ }
175
+ /* The content starts at its top edge, whatever its first block is (pages often align it with a sidebar). */
176
+ .preview:not(.nested) > :global(:first-child),
177
+ .preview:not(.nested) > :global(:first-child > :first-child) {
178
+ margin-top: 0;
179
+ }
180
+ /* Floating images and side boxes stay inside the content. */
181
+ .preview:not(.nested)::after {
182
+ content: '';
183
+ display: block;
184
+ clear: both;
185
+ }
186
+ </style>
@@ -0,0 +1,35 @@
1
+ import type { StoredBlock } from './types';
2
+ /** Consecutive list blocks as properly nested lists: an item one level deeper goes into the previous item. */
3
+ export type ListNode = {
4
+ type: string;
5
+ items: {
6
+ block: StoredBlock;
7
+ children: ListNode[];
8
+ }[];
9
+ };
10
+ export type Part = {
11
+ kind: 'block';
12
+ block: StoredBlock;
13
+ } | {
14
+ kind: 'lists';
15
+ lists: ListNode[];
16
+ };
17
+ export declare function parts(blocks: StoredBlock[]): Part[];
18
+ import { type MediaItem } from './media';
19
+ import { type Messages } from './i18n';
20
+ import type { BlockPlugin } from './types';
21
+ interface Props {
22
+ blocks: StoredBlock[];
23
+ plugins: BlockPlugin[];
24
+ /** Set by plugins rendering their nested blocks; only the top level splits at the supporters-only divider. */
25
+ nested?: boolean;
26
+ /** The images blocks refer to by id; nested previews share their parent's. */
27
+ media?: MediaItem[];
28
+ /** UI text in the host's language; nested previews share their parent's. */
29
+ messages?: Messages;
30
+ /** Mark the blocks after a gate (the supporters-only divider) with a label and a dashed line; default: yes. */
31
+ markGated?: boolean;
32
+ }
33
+ declare const Preview: import("svelte").Component<Props, {}, "">;
34
+ type Preview = ReturnType<typeof Preview>;
35
+ export default Preview;