@atom-forge/brickee 0.0.1 → 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 (49) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README-AI.md +24 -21
  3. package/dist/Editor.svelte +52 -64
  4. package/dist/Editor.svelte.d.ts +0 -3
  5. package/dist/LinkPrompt.svelte +3 -3
  6. package/dist/Preview.svelte +1 -5
  7. package/dist/Preview.svelte.d.ts +0 -3
  8. package/dist/PropsBar.svelte +120 -110
  9. package/dist/PropsBar.svelte.d.ts +0 -3
  10. package/dist/code.d.ts +12 -0
  11. package/dist/code.js +15 -0
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/kit/Empty.svelte +2 -2
  15. package/dist/kit/Frame.svelte +3 -3
  16. package/dist/locales/hu.js +2 -7
  17. package/dist/markdown.d.ts +0 -2
  18. package/dist/markdown.js +1 -1
  19. package/dist/plugins/Gap.svelte +1 -1
  20. package/dist/plugins/Map.svelte +1 -1
  21. package/dist/plugins/Table.svelte +36 -62
  22. package/dist/plugins/TablePreview.svelte +2 -2
  23. package/dist/plugins/YouTubePreview.svelte +1 -1
  24. package/dist/plugins/index.js +12 -1
  25. package/dist/schema.js +1 -1
  26. package/dist/types.d.ts +6 -7
  27. package/docs/en/bundled-plugins.md +1 -1
  28. package/docs/en/documents-and-markdown.md +4 -5
  29. package/docs/en/editor-and-preview.md +1 -3
  30. package/docs/en/getting-started.md +6 -2
  31. package/docs/en/host-contexts.md +26 -132
  32. package/docs/en/index.md +4 -4
  33. package/docs/en/recipes-and-pitfalls.md +10 -9
  34. package/docs/en/upgrading.md +20 -4
  35. package/docs/en/writing-plugins.md +111 -11
  36. package/docs/hu/bundled-plugins.md +1 -1
  37. package/docs/hu/documents-and-markdown.md +4 -5
  38. package/docs/hu/editor-and-preview.md +1 -3
  39. package/docs/hu/getting-started.md +6 -2
  40. package/docs/hu/host-contexts.md +26 -132
  41. package/docs/hu/index.md +4 -4
  42. package/docs/hu/recipes-and-pitfalls.md +10 -9
  43. package/docs/hu/upgrading.md +20 -4
  44. package/docs/hu/writing-plugins.md +111 -11
  45. package/package.json +6 -2
  46. package/dist/ImagePicker.svelte +0 -131
  47. package/dist/ImagePicker.svelte.d.ts +0 -13
  48. package/dist/media.d.ts +0 -17
  49. package/dist/media.js +0 -12
package/CHANGELOG.md CHANGED
@@ -7,9 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.2] - 2026-11-10
11
+
12
+ ### Added
13
+
14
+ - Showcase site in `src/routes` (`bun run dev`), styled like the `@atom-forge/ui` site: a playground (editor beside its preview, stored JSON and markdown), the guide (editor and preview, split view, markdown, stored form and schema), a page per bundled plugin, writing, nesting and overriding plugins, image blocks, and the host contexts (per-view values, code languages, localisation), each with live examples and their code. It resolves `@atom-forge/brickee` to the local source and prerenders to `build/` (`@sveltejs/adapter-static`); it is not part of the npm package.
15
+ - A `toggles` field kind (`PropField`): a row of icon buttons, each switching one of `options` on or off (`options[].icon` is the button, `label` its tooltip), stored as the list of the ones on.
16
+ - `provideCodeLanguages(() => list)` (with `useCodeLanguages` and the `CodeLanguage` type): a host context that turns a code block's language field into a dropdown of the given languages, with *None* first; without it the language stays free text. Nested editors inherit the list, and a stored language missing from it stays an option.
17
+
10
18
  ### Changed
11
- - Configure releases with Ship.
12
19
 
20
+ - The fields bar is more compact: smaller controls and type, text fields show their label as the placeholder (with the field's own placeholder after it), the other fields a short label beside the control.
21
+ - The table's header and footer rows are two icon toggles in its fields bar (a `toggles` field), like any plugin field; the toolbar above the table holds only the row and column tools of the cell being edited, and says to click a cell while none is. The info icon with the keyboard hint is gone.
22
+ - The editor follows the host's light and dark mode: its colours (block fields, popups, frames, the table editor, the block actions) come from the `@atom-forge/ui` theme tokens with the former colours as fallbacks, and the props bar and the link prompt no longer force the light theme. The previews' table borders, video captions and the gap's stripes are drawn from the text colour.
23
+ - A code block's language is edited in the same fields bar as a plugin's (its header shows `code · <language>`), instead of a small input in its corner; read-only, the corner still shows the language.
24
+ - The block handles keep 1px free above and below, so the handles of neighbouring blocks do not touch.
25
+ - The docs note that the block handles are drawn about 22 px left of the editor's box, so the element around it needs that much left padding.
26
+ - Releases are configured with `ship-it init`: the publish workflow comes from Ship's template, `package.json` gains `pub-validate` and `publishConfig`, and npm Trusted Publishing lets CI publish the next versions.
27
+
28
+ ### Removed
29
+
30
+ - **Breaking:** images and media are no longer the library's concept — whether a block stores a URL or an id, how the user picks an image and how it is resolved is the application's decision. Removed: the `image` and `images` field kinds with their picker, the `media` prop of `Editor` and `Preview`, `provideMedia`, `useMedia`, `findMedia`, the `MediaItem` type, `toMarkdown`'s `media` option and `MarkdownContext.media`. An image block is an app plugin that picks in its own view or dialog and resolves with the app's own data; stored documents are unaffected. See the upgrading chapter.
13
31
 
14
32
  ## [0.0.1] - 2026-11-10
15
33
 
package/README-AI.md CHANGED
@@ -9,8 +9,10 @@ Brickee is a block editor. Its core is a markdown block editor (`md:p`, `md:h1`
9
9
  - Peers: `svelte` 5, `@atom-forge/ui` (popups and field controls), `@lucide/svelte`; `zod` only for `@atom-forge/brickee/schema`. The app must set up `@atom-forge/ui` (Tailwind 4, its theme CSS and `@source`, the app beneath its `Root`).
10
10
  - Store the editor's `value` (`StoredBlock[]`) as it is. Built-in types start with `md:`; any other `type` is a plugin's. Never rename a plugin's `type`: stored documents refer to it.
11
11
  - Pass the same `plugins` to the `Editor` and the `Preview` of a document type. Restrict what an editor offers with `palette` (palette ids: `md:*` kinds and plugin types), not by dropping plugins.
12
- - No app-specific editor props: app data reaches plugins through contexts (`media` / `provideMedia`, `provideView`, `messages` / `provideText`) or plugin fields.
13
- - Plugin props are untrusted user input. Previews sanitize: `safeColor` for colours, check links, clamp numbers, look images up by id in the media list, never `{@html}` with props.
12
+ - The editor's colours come from the `@atom-forge/ui` theme tokens (light/dark follow the host). Its block handles sit ~22 px left of its box: leave that much left padding around it.
13
+ - No app-specific editor props: app data reaches plugins through contexts (`provideView`, `provideCodeLanguages`, `messages` / `provideText`), plugin fields, or the app's own modules and contexts.
14
+ - No image concept: no image field, picker or media list. An image block is an app plugin that stores what the app chooses (a URL, or an id of its own media library), picks it in its view or the app's own dialog, and resolves ids with the app's own data.
15
+ - Plugin props are untrusted user input. Previews sanitize: `safeColor` for colours, check links and image addresses (`http(s)` or site-relative), clamp numbers, never `{@html}` with props.
14
16
  - A plugin view declares both `props` and `blocks` with `$bindable()`, even if unused.
15
17
  - Containers (`content: 'blocks'`) nest one level only; nested editors never offer containers or unique plugins. `accepts` narrows their content (`'md:*'` = every built-in).
16
18
  - `unique`: one per document, top level; not enforced by the schema. `gate`: the preview only *marks* blocks after it; hide gated content on the server.
@@ -24,9 +26,9 @@ Brickee is a block editor. Its core is a markdown block editor (`md:p`, `md:h1`
24
26
 
25
27
  ```ts
26
28
  // @atom-forge/brickee — components
27
- // <Editor bind:value={StoredBlock[]} plugins?={BlockPlugin[]} palette?={string[]} media?={MediaItem[]}
28
- // messages?={Messages} readonly?={boolean} placeholder?={string} /> // `nested` is for Frame only
29
- // <Preview blocks={StoredBlock[]} plugins={BlockPlugin[]} media?={MediaItem[]} messages?={Messages}
29
+ // <Editor bind:value={StoredBlock[]} plugins?={BlockPlugin[]} palette?={string[]} messages?={Messages}
30
+ // readonly?={boolean} placeholder?={string} /> // `nested` is for Frame only
31
+ // <Preview blocks={StoredBlock[]} plugins={BlockPlugin[]} messages?={Messages}
30
32
  // markGated?={boolean /* default true */} nested?={boolean} />
31
33
  // <Frame bind:blocks {readonly} {palette} {plugins} heading?={string} empty?={string} image?={string} placeholder?={string} />
32
34
  // <Empty icon={Icon} text={string} />
@@ -43,15 +45,14 @@ function inline(md: string): string; // inline markdown -> safe
43
45
  function fromStored(s: StoredBlock): Block;
44
46
  function toStored(b: Block): StoredBlock; // browser only
45
47
  function fromMarkdown(src: string): Block[]; // one block per non-empty line; built-in blocks only
46
- function toMarkdown(blocks: Block[], opts?: { plugins?: BlockPlugin[]; media?: MediaItem[] }): string; // browser only
48
+ function toMarkdown(blocks: Block[], opts?: { plugins?: BlockPlugin[] }): string; // browser only
47
49
 
48
50
  // contexts (call provide* in component init, above the editor/preview; use* in plugin components)
49
- interface MediaItem { id: string; name: string; url: string; group?: string }
50
- function provideMedia(own: () => MediaItem[] | undefined): void;
51
- function useMedia(): () => MediaItem[];
52
- function findMedia(list: MediaItem[], id: unknown): MediaItem | undefined;
53
51
  function provideView(get: () => string | undefined): void;
54
52
  function useView(): () => string | undefined;
53
+ interface CodeLanguage { value: string; label?: string } // value: stored as the code block's props.lang
54
+ function provideCodeLanguages(own: () => (string | CodeLanguage)[] | undefined): void; // list: the language field is a dropdown
55
+ function useCodeLanguages(): () => CodeLanguage[]; // empty: free text
55
56
  type Messages = Record<string, string>;
56
57
  type Translate = (text: string, vars?: Record<string, string | number>) => string; // fills {name}
57
58
  function provideText(own: () => Messages | undefined): void;
@@ -99,12 +100,12 @@ interface BlockPlugin<P extends Props = Props> {
99
100
  }
100
101
  interface PluginViewProps<P> { props: P; blocks: StoredBlock[]; readonly: boolean; palette: string[]; plugins: BlockPlugin[] }
101
102
  interface PluginPreviewProps<P> { props: P; blocks: StoredBlock[]; plugins: BlockPlugin[] }
102
- interface MarkdownContext { inner: string; media: MediaItem[] }
103
+ interface MarkdownContext { inner: string }
103
104
  interface PropField {
104
105
  key: string; label: string;
105
- kind: 'text' | 'textarea' | 'color' | 'select' | 'slider' | 'image' | 'images';
106
+ kind: 'text' | 'textarea' | 'color' | 'select' | 'slider' | 'toggles'; // toggles: icon buttons, stores the list of the ones on
106
107
  placeholder?: string;
107
- options?: { value: string; label: string }[]; // select; stepped slider; label may use {n}
108
+ options?: { value: string; label: string; icon?: Icon }[]; // select; stepped slider (label may use {n}); toggles (icon + tooltip)
108
109
  min?: number; max?: number; step?: number; unit?: string; // number slider (stored as string)
109
110
  none?: string; // unset label (number slider notch, optional colour)
110
111
  views?: { key: string; label: string }[]; // per-view slider values
@@ -113,10 +114,10 @@ interface PropField {
113
114
  }
114
115
 
115
116
  // @atom-forge/brickee/plugins
116
- // table, youtube, map, gap — the only bundled plugins (none holds nested blocks or uses media); PLUGINS = [table, youtube, map, gap]
117
+ // table, youtube, map, gap — the only bundled plugins (none holds nested blocks or images); PLUGINS = [table, youtube, map, gap]
117
118
  // types: TableProps, YouTubeProps, MapProps, GapProps, Head
118
119
  // helpers: youtubeId, mapSrc, gapLines, GAP_LINES, GAP_MAX, tableHeads, rowHead, HEADS, square
119
- // Image blocks, boxes and other containers are app plugins you write yourself (see docs/en/writing-plugins.md, host-contexts.md).
120
+ // Image blocks, boxes and other containers are app plugins you write yourself (see docs/en/writing-plugins.md).
120
121
 
121
122
  // @atom-forge/brickee/schema (zod)
122
123
  const blocksSchema: ZodType<StoredBlock[]>;
@@ -130,16 +131,15 @@ function blocksSchemaFor(plugins: readonly { type: string; accepts?: readonly st
130
131
 
131
132
  ```svelte
132
133
  <script lang="ts">
133
- import { Editor, Preview, type MediaItem, type StoredBlock } from '@atom-forge/brickee';
134
+ import { Editor, Preview, type StoredBlock } from '@atom-forge/brickee';
134
135
  import { PLUGINS } from '@atom-forge/brickee/plugins';
135
136
  import { hu } from '@atom-forge/brickee/locales/hu';
136
137
 
137
- let { media = [] }: { media?: MediaItem[] } = $props();
138
138
  let value = $state<StoredBlock[]>([]);
139
139
  </script>
140
140
 
141
- <Editor bind:value plugins={PLUGINS} {media} messages={hu} />
142
- <Preview blocks={value} plugins={PLUGINS} {media} messages={hu} />
141
+ <Editor bind:value plugins={PLUGINS} messages={hu} />
142
+ <Preview blocks={value} plugins={PLUGINS} messages={hu} />
143
143
  ```
144
144
 
145
145
  ## Common extensions
@@ -214,7 +214,7 @@ export const plugins: BlockPlugin[] = [
214
214
  ];
215
215
  ```
216
216
 
217
- **Markdown form and conversion** — `markdown(props, { inner, media })` returns markdown or `''`; use `mdText`, `mdJoin`, `mdQuote`, and `findMedia(media, id)?.url` for images. Whole document: `toMarkdown(doc.map(fromStored), { plugins, media })`; import: `fromMarkdown(text).map(toStored)` (both in the browser).
217
+ **Markdown form and conversion** — `markdown(props, { inner })` returns markdown or `''`; use `mdText`, `mdJoin`, `mdQuote`. Whole document: `toMarkdown(doc.map(fromStored), { plugins })`; import: `fromMarkdown(text).map(toStored)` (both in the browser).
218
218
 
219
219
  **Per-view fields** — add `views` to a slider field and provide the current view:
220
220
 
@@ -256,9 +256,12 @@ const result = blocksSchemaFor(plugins).safeParse(input); // or pass [{ type, ac
256
256
  | Symptom | Cause | Fix |
257
257
  |---|---|---|
258
258
  | Fields and popups unstyled | `@atom-forge/ui` theme CSS / `@source` missing | Set up `@atom-forge/ui`; render beneath its `Root`. |
259
+ | Block handles stick out on the left | Handles are drawn ~22 px left of the editor's box | Give the editor's container ~22 px left padding. |
259
260
  | Context errors with a linked local copy | Duplicate `svelte` / `@atom-forge/ui` | Vite `resolve.dedupe: ['svelte', '@atom-forge/ui', '@lucide/svelte', 'zod']`. |
260
261
  | A block is missing from `<Preview>` | Its plugin has no `preview` | Add one (bundled plugins have a default) or render it yourself. |
261
- | "Unknown block" in the editor, block missing on the site | Plugin missing from `plugins`, or image id not in `media` | Same plugins and media for editor and preview. |
262
+ | "Unknown block" in the editor, block missing on the site | Plugin missing from `plugins` | Same plugins for editor and preview. |
263
+ | Image block renders nothing | Preview rejects the stored address, or the app's lookup lacks the id | Store `http(s)` / site-relative addresses; resolve ids from the same app data everywhere. |
264
+ | `media` prop, `useMedia`, `findMedia`, `kind: 'image'` missing | Removed: images are the app's business | Own picking and lookup in the app's image plugin (see `docs/en/upgrading.md`). |
262
265
  | Box not offered inside a box | One nesting level | By design. |
263
266
  | Block not offered inside a container | `accepts` or host `palette` excludes it | Add its palette id (or `'md:*'`). |
264
267
  | Gated content visible to all | `gate` only marks | Cut the document on the server. |
@@ -19,7 +19,7 @@
19
19
  plainText
20
20
  } from './dom';
21
21
  import { fromMarkdown, fromStored, plainHtml, toMarkdown, toStored } from './markdown';
22
- import { provideMedia, useMedia, type MediaItem } from './media';
22
+ import { useCodeLanguages } from './code';
23
23
  import { provideText, useText, type Messages } from './i18n';
24
24
  import { PopupContainer, createPopupManager, getPopupManager } from '@atom-forge/ui';
25
25
  import LinkPrompt from './LinkPrompt.svelte';
@@ -41,8 +41,6 @@
41
41
  plugins?: BlockPlugin[];
42
42
  /** Set by plugins with nested blocks: a compact editor inside another block. */
43
43
  nested?: boolean;
44
- /** Images the blocks can pick from (the page's attachments); nested editors share their parent's. */
45
- media?: MediaItem[];
46
44
  /** UI text in the host's language (English text -> translation); nested editors share their parent's. */
47
45
  messages?: Messages;
48
46
  }
@@ -53,18 +51,15 @@
53
51
  palette: paletteProp,
54
52
  plugins = [],
55
53
  nested = false,
56
- media,
57
54
  messages
58
55
  }: Props = $props();
59
56
 
60
- provideMedia(() => media);
61
57
  provideText(() => messages);
62
58
  // Popups (the link field) are atom-forge popups; the top editor has their container, nested editors share it.
63
59
  // svelte-ignore state_referenced_locally
64
60
  const popups = nested ? getPopupManager() : createPopupManager(getPopupManager());
65
61
  const tr = useText();
66
62
  const placeholder = $derived(placeholderProp ?? tr("Write something, or press '/' for blocks…"));
67
- const getMedia = useMedia();
68
63
 
69
64
  // One level of nesting: an editor inside a block offers no block types that hold blocks themselves, nor unique
70
65
  // ones (those belong to the top level).
@@ -818,7 +813,7 @@
818
813
  function onCopy(e: ClipboardEvent, cut = false) {
819
814
  if (!selSet.size) return;
820
815
  e.preventDefault();
821
- e.clipboardData?.setData('text/plain', toMarkdown(value.filter((b) => selSet.has(b.id)), { plugins, media: getMedia() }));
816
+ e.clipboardData?.setData('text/plain', toMarkdown(value.filter((b) => selSet.has(b.id)), { plugins }));
822
817
  if (cut) deleteSelected();
823
818
  }
824
819
 
@@ -843,11 +838,22 @@
843
838
 
844
839
  /* ------------------------------------------------------ properties panel */
845
840
 
846
- // Plugin blocks show their tag and fields on their top (PropsBar); a code block its language in its corner.
847
- const CODE_FIELDS: PropField[] = [
848
- // Only characters an opening fence can carry (```c++, ```objective-c).
849
- { key: 'lang', label: 'Language', kind: 'text', placeholder: 'e.g. ts, py, css', parse: (s) => s.replace(/[^\w+#.-]/g, '') }
850
- ];
841
+ // Plugin blocks and code blocks show their tag and fields on their top (PropsBar); read-only, a code block shows its
842
+ // language in its corner.
843
+ // Free text, or a dropdown when the host gives a list (provideCodeLanguages). A stored language missing from the
844
+ // list stays an option, so opening an older document loses nothing.
845
+ const codeLanguages = useCodeLanguages();
846
+ const codeFields = (b: Block): PropField[] => {
847
+ const list = codeLanguages();
848
+ if (!list.length)
849
+ // Only characters an opening fence can carry (```c++, ```objective-c).
850
+ return [{ key: 'lang', label: 'Language', kind: 'text', placeholder: 'e.g. ts, py, css', parse: (s) => s.replace(/[^\w+#.-]/g, '') }];
851
+ const lang = typeof b.props?.lang === 'string' ? b.props.lang : '';
852
+ const options = [{ value: '', label: 'None' }, ...list.map((l) => ({ value: l.value, label: l.label ?? l.value }))];
853
+ if (lang && !list.some((l) => l.value === lang)) options.push({ value: lang, label: lang });
854
+ return [{ key: 'lang', label: 'Language', kind: 'select', options }];
855
+ };
856
+ const codeTag = (b: Block) => [tr('code'), b.props?.lang].filter(Boolean).join(' · ');
851
857
  function tagOf(b: Block) {
852
858
  const p = pluginOf(b.plugin);
853
859
  return p ? p.tag(b.props ?? {}).split(' · ').map((s) => tr(s)).join(' · ') : (b.plugin ?? '');
@@ -858,19 +864,6 @@
858
864
  return f.format ? f.format(v) : v == null ? '' : String(v);
859
865
  };
860
866
 
861
- /** Image fields: picking a picked image again takes it out. */
862
- function pickMedia(b: Block, f: PropField, id: string) {
863
- typed();
864
- const next = { ...(b.props ?? {}) };
865
- if (f.kind === 'image') {
866
- if (next[f.key] === id) delete next[f.key];
867
- else next[f.key] = id;
868
- } else {
869
- const ids = (next[f.key] as string[] | undefined) ?? [];
870
- next[f.key] = ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id];
871
- }
872
- b.props = next;
873
- }
874
867
  /** Sets one property to a value as it is ('' or undefined removes it). */
875
868
  function setValue(b: Block, key: string, v: unknown) {
876
869
  typed();
@@ -1274,7 +1267,7 @@
1274
1267
  {#if plugin}
1275
1268
  <div class="plugin">
1276
1269
  {#snippet propsBar()}
1277
- <PropsBar block={b} tag={tagOf(b)} fields={plugin.fields ?? []} media={getMedia()} {tr} {setProp} {setValue} {pickMedia} open={fresh.has(b.id)} />
1270
+ <PropsBar block={b} tag={tagOf(b)} fields={plugin.fields ?? []} {tr} {setProp} {setValue} open={fresh.has(b.id)} />
1278
1271
  {/snippet}
1279
1272
  <PluginHost
1280
1273
  {plugin}
@@ -1307,6 +1300,10 @@
1307
1300
  onclick={() => toggleTodo(b)}
1308
1301
  >{#if b.checked}<Check size={12} strokeWidth={3} />{/if}</button
1309
1302
  >
1303
+ {:else if b.type === 'code' && !readonly}
1304
+ <div class="code-fields">
1305
+ <PropsBar block={b} tag={codeTag(b)} fields={codeFields(b)} {tr} {setProp} {setValue} />
1306
+ </div>
1310
1307
  {/if}
1311
1308
  <div
1312
1309
  class="ce"
@@ -1334,20 +1331,12 @@
1334
1331
  }}
1335
1332
  ></div>
1336
1333
  {/if}
1337
- {#if b.type === 'code'}
1338
- <!-- the language of a code block, in its corner -->
1339
- <label class="badge">
1334
+ {#if b.type === 'code' && readonly}
1335
+ <!-- read-only: the language of a code block, in its corner -->
1336
+ <div class="badge">
1340
1337
  <span class="tag">{tr('code')}</span>
1341
- {#if !readonly}
1342
- <input
1343
- class="lang"
1344
- placeholder={tr('language')}
1345
- aria-label={tr('Language')}
1346
- size="6"
1347
- bind:value={() => fieldText(b, CODE_FIELDS[0]), (v: string) => setProp(b, CODE_FIELDS[0], v)}
1348
- />
1349
- {:else if b.props?.lang}<span class="tag">{b.props.lang}</span>{/if}
1350
- </label>
1338
+ {#if b.props?.lang}<span class="tag">{b.props.lang}</span>{/if}
1339
+ </div>
1351
1340
  {:else if b.type === 'plugin' && (readonly || !pluginOf(b.plugin)?.fields?.length)}
1352
1341
  <div class="badge"><span class="tag">{tagOf(b)}</span></div>
1353
1342
  {/if}
@@ -1448,12 +1437,12 @@
1448
1437
 
1449
1438
  <style>
1450
1439
  .editor {
1451
- --fg: #1d1d1f;
1452
- --muted: #a0a0a8;
1440
+ --fg: var(--color-surface-contrast, #1d1d1f);
1441
+ --muted: var(--color-muted-contrast, #a0a0a8);
1453
1442
  --accent: #3b82f6;
1454
1443
  --sel: rgba(59, 130, 246, 0.14);
1455
- --surface: #fff;
1456
- --border: #e5e5ea;
1444
+ --surface: var(--color-surface, #fff);
1445
+ --border: var(--color-frame, #e5e5ea);
1457
1446
  --code-bg: rgba(120, 120, 140, 0.14);
1458
1447
  position: relative;
1459
1448
  outline: none;
@@ -1518,11 +1507,12 @@
1518
1507
  bottom: -2px;
1519
1508
  }
1520
1509
 
1510
+ /* 1px short at both ends, so the bars of neighbouring blocks do not touch. */
1521
1511
  .gutter {
1522
1512
  position: absolute;
1523
1513
  left: -22px;
1524
- top: 0;
1525
- bottom: 0;
1514
+ top: 1px;
1515
+ bottom: 1px;
1526
1516
  width: 20px;
1527
1517
  }
1528
1518
  .handle {
@@ -1610,7 +1600,7 @@
1610
1600
  top: 50%;
1611
1601
  translate: 0 -50%;
1612
1602
  border: 4px solid transparent;
1613
- border-right-color: #1d1d1f;
1603
+ border-right-color: var(--color-surface-contrast, #1d1d1f);
1614
1604
  border-left: 0;
1615
1605
  }
1616
1606
  .grab:has(.grip:hover) .bubble,
@@ -1635,8 +1625,9 @@
1635
1625
  height: 26px;
1636
1626
  padding: 0;
1637
1627
  border: 0;
1638
- color: #fff;
1639
- background: #1d1d1f;
1628
+ /* inverted: the theme's text colour as the background */
1629
+ color: var(--color-surface, #fff);
1630
+ background: var(--color-surface-contrast, #1d1d1f);
1640
1631
  cursor: pointer;
1641
1632
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.2);
1642
1633
  }
@@ -1649,7 +1640,7 @@
1649
1640
  padding-right: 2px;
1650
1641
  }
1651
1642
  .bubble button:hover {
1652
- background: #3a3a40;
1643
+ background: color-mix(in srgb, var(--color-surface-contrast, #1d1d1f) 82%, var(--color-surface, #fff));
1653
1644
  }
1654
1645
  .bubble button.del:hover {
1655
1646
  background: #c62828;
@@ -1768,7 +1759,7 @@
1768
1759
  .t-plugin > .badge {
1769
1760
  top: calc(0.35em + 5px);
1770
1761
  /* Readable over a map or an image. */
1771
- background: rgba(255, 255, 255, 0.92);
1762
+ background: color-mix(in srgb, var(--color-surface, #fff) 92%, transparent);
1772
1763
  border-radius: 6px;
1773
1764
  }
1774
1765
  .tag {
@@ -1782,25 +1773,22 @@
1782
1773
  font: 500 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
1783
1774
  user-select: none;
1784
1775
  }
1785
- .lang {
1786
- width: 7em;
1787
- padding: 1px 6px;
1788
- border: 1px solid transparent;
1789
- border-radius: 5px;
1790
- background: none;
1791
- color: var(--fg);
1792
- font: 500 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
1793
- outline: none;
1776
+ /* the fields bar on its own row above the code */
1777
+ .t-code {
1778
+ flex-wrap: wrap;
1779
+ row-gap: 0;
1780
+ }
1781
+ .code-fields,
1782
+ .t-code .ce {
1783
+ flex-basis: 100%;
1794
1784
  }
1795
- .lang:hover,
1796
- .lang:focus {
1797
- border-color: var(--border);
1798
- background: var(--bg, #fff);
1785
+ .t-code:has(> .badge) .ce {
1786
+ padding-right: 7.5em;
1799
1787
  }
1800
1788
  .t-code .ce {
1801
1789
  background: var(--code-bg);
1802
1790
  border-radius: 8px;
1803
- padding: 0.7em 7.5em 0.7em 0.9em;
1791
+ padding: 0.7em 0.9em;
1804
1792
  font: 0.88em/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
1805
1793
  white-space: pre-wrap;
1806
1794
  }
@@ -1,4 +1,3 @@
1
- import { type MediaItem } from './media';
2
1
  import { type Messages } from './i18n';
3
2
  import { type BlockPlugin, type StoredBlock } from './types';
4
3
  interface Props {
@@ -12,8 +11,6 @@ interface Props {
12
11
  plugins?: BlockPlugin[];
13
12
  /** Set by plugins with nested blocks: a compact editor inside another block. */
14
13
  nested?: boolean;
15
- /** Images the blocks can pick from (the page's attachments); nested editors share their parent's. */
16
- media?: MediaItem[];
17
14
  /** UI text in the host's language (English text -> translation); nested editors share their parent's. */
18
15
  messages?: Messages;
19
16
  }
@@ -22,7 +22,7 @@
22
22
  </script>
23
23
 
24
24
  <!-- svelte-ignore a11y_no_static_element_interactions -->
25
- <div class="light link" {onkeydown}>
25
+ <div class="link" {onkeydown}>
26
26
  <Input bind:this={input} compact monospace bind:value={url} placeholder="https://…" />
27
27
  <Button compact accent label={tr('OK')} onclick={() => popups.resolve(url.trim())} />
28
28
  {#if href}<Button compact ghost label={tr('Remove')} onclick={() => popups.resolve('')} />{/if}
@@ -34,8 +34,8 @@
34
34
  gap: 6px;
35
35
  width: 340px;
36
36
  padding: 8px;
37
- background: #fff;
38
- border: 1px solid #e5e5ea;
37
+ background: var(--color-surface, #fff);
38
+ border: 1px solid var(--color-frame, #e5e5ea);
39
39
  border-radius: 12px;
40
40
  box-shadow: 0 10px 36px rgba(0, 0, 0, 0.2);
41
41
  }
@@ -49,7 +49,6 @@
49
49
  <script lang="ts">
50
50
  import { useText } from './i18n';
51
51
  import { inline } from './markdown';
52
- import { provideMedia, type MediaItem } from './media';
53
52
  import { provideText, type Messages } from './i18n';
54
53
  import type { BlockPlugin } from './types';
55
54
 
@@ -58,16 +57,13 @@
58
57
  plugins: BlockPlugin[];
59
58
  /** Set by plugins rendering their nested blocks; only the top level splits at the supporters-only divider. */
60
59
  nested?: boolean;
61
- /** The images blocks refer to by id; nested previews share their parent's. */
62
- media?: MediaItem[];
63
60
  /** UI text in the host's language; nested previews share their parent's. */
64
61
  messages?: Messages;
65
62
  /** Mark the blocks after a gate (the supporters-only divider) with a label and a dashed line; default: yes. */
66
63
  markGated?: boolean;
67
64
  }
68
- let { blocks, plugins, nested = false, media, messages, markGated = true }: Props = $props();
65
+ let { blocks, plugins, nested = false, messages, markGated = true }: Props = $props();
69
66
 
70
- provideMedia(() => media);
71
67
  provideText(() => messages);
72
68
  const tr = useText();
73
69
 
@@ -15,7 +15,6 @@ export type Part = {
15
15
  lists: ListNode[];
16
16
  };
17
17
  export declare function parts(blocks: StoredBlock[]): Part[];
18
- import { type MediaItem } from './media';
19
18
  import { type Messages } from './i18n';
20
19
  import type { BlockPlugin } from './types';
21
20
  interface Props {
@@ -23,8 +22,6 @@ interface Props {
23
22
  plugins: BlockPlugin[];
24
23
  /** Set by plugins rendering their nested blocks; only the top level splits at the supporters-only divider. */
25
24
  nested?: boolean;
26
- /** The images blocks refer to by id; nested previews share their parent's. */
27
- media?: MediaItem[];
28
25
  /** UI text in the host's language; nested previews share their parent's. */
29
26
  messages?: Messages;
30
27
  /** Mark the blocks after a gate (the supporters-only divider) with a label and a dashed line; default: yes. */