@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
@@ -52,12 +52,9 @@
52
52
  BetweenVerticalEnd,
53
53
  BetweenVerticalStart,
54
54
  Columns3,
55
- Info,
56
55
  Rows3,
57
56
  GripHorizontal,
58
57
  GripVertical,
59
- PanelBottom,
60
- PanelTop,
61
58
  Plus,
62
59
  TextAlignCenter,
63
60
  TextAlignEnd,
@@ -78,17 +75,6 @@
78
75
  let at = $state<{ r: number; c: number } | null>(null);
79
76
  const heads = $derived(tableHeads(props));
80
77
  const headOf = (r: number) => rowHead(heads, r, rows.length);
81
- function toggleHead(h: Head) {
82
- const next = new Set(heads);
83
- if (!next.delete(h)) next.add(h);
84
- const { header: _, ...rest } = props;
85
- props = { ...rest, heads: HEADS.filter((x) => next.has(x)) } as TableProps;
86
- }
87
- const HEAD_TOGGLES: { head: Head; icon: typeof PanelTop; label: string }[] = [
88
- { head: 'top', icon: PanelTop, label: 'Header row' },
89
- { head: 'bottom', icon: PanelBottom, label: 'Footer row (e.g. sums)' }
90
- ];
91
-
92
78
  /* Dragging a row or a column by its grip: `to` is the slot it would go to (0…count), shown as a blue line. */
93
79
  let drag = $state<{ kind: 'row' | 'col'; from: number; to: number } | null>(null);
94
80
 
@@ -250,13 +236,8 @@
250
236
  {/each}
251
237
  </div>
252
238
  {:else}
253
- <span class="hint" title={tr('Enter: next row · Shift+Enter: line break · paste from a spreadsheet')}><Info size={14} /></span>
239
+ <span class="idle">{tr('Click a cell to edit its row and column')}</span>
254
240
  {/if}
255
- <div class="seg heads">
256
- {#each HEAD_TOGGLES as h (h.head)}
257
- <button type="button" title={tr(h.label)} aria-pressed={heads.has(h.head)} class:on={heads.has(h.head)} onmousedown={(e) => e.preventDefault()} onclick={() => toggleHead(h.head)}><h.icon size={15} /></button>
258
- {/each}
259
- </div>
260
241
  </div>
261
242
  {/if}
262
243
  <div class="grid">
@@ -321,9 +302,9 @@
321
302
  .table {
322
303
  font: 15px/1.45 system-ui, -apple-system, 'Segoe UI', sans-serif;
323
304
  }
324
- /* Beside the tag and the […] button in the corner; when it wraps, it pushes the table down. */
305
+ /* The row and column tools of the cell being edited; when it wraps, it pushes the table down. */
325
306
  .bar {
326
- margin: 0 9em 8px 0;
307
+ margin: 0 0 8px;
327
308
  display: flex;
328
309
  flex-wrap: wrap;
329
310
  align-items: center;
@@ -336,13 +317,17 @@
336
317
  gap: 1px;
337
318
  padding: 2px;
338
319
  border-radius: 8px;
339
- background: #f4f4f6;
320
+ background: var(--color-muted, #f4f4f6);
321
+ }
322
+ .idle {
323
+ font-size: 12px;
324
+ color: var(--color-muted-contrast, #a0a0a8);
340
325
  }
341
326
  .label {
342
327
  display: inline-grid;
343
328
  place-items: center;
344
329
  padding: 0 4px 0 5px;
345
- color: #a0a0a8;
330
+ color: var(--color-muted-contrast, #a0a0a8);
346
331
  }
347
332
  .bar button {
348
333
  display: inline-grid;
@@ -353,16 +338,16 @@
353
338
  border: 0;
354
339
  border-radius: 6px;
355
340
  background: none;
356
- color: #4b4b55;
341
+ color: var(--color-surface-contrast, #4b4b55);
357
342
  cursor: pointer;
358
343
  }
359
344
  .bar button:hover:not(:disabled) {
360
- background: #fff;
361
- color: #1d1d1f;
345
+ background: var(--color-surface, #fff);
346
+ color: var(--color-surface-contrast, #1d1d1f);
362
347
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
363
348
  }
364
349
  .bar button.on {
365
- background: #fff;
350
+ background: var(--color-surface, #fff);
366
351
  color: #2563eb;
367
352
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
368
353
  }
@@ -378,27 +363,16 @@
378
363
  gap: 5px;
379
364
  height: 28px;
380
365
  padding: 0 10px;
381
- border: 1px solid #e5e5ea;
366
+ border: 1px solid var(--color-frame, #e5e5ea);
382
367
  border-radius: 8px;
383
368
  font-size: 12px;
384
369
  font-weight: 500;
385
370
  }
386
371
  .bar .toggle.on {
387
- border-color: #bfdbfe;
388
- background: #eff6ff;
372
+ border-color: color-mix(in srgb, #3b82f6 35%, transparent);
373
+ background: color-mix(in srgb, #3b82f6 8%, transparent);
389
374
  box-shadow: none;
390
375
  }
391
- .hint {
392
- display: inline-grid;
393
- place-items: center;
394
- color: #a0a0a8;
395
- cursor: help;
396
- opacity: 0;
397
- transition: opacity 0.12s;
398
- }
399
- .table:hover .hint {
400
- opacity: 1;
401
- }
402
376
 
403
377
  /* The table, with a + bar on the right (column) and below (row) that shows on hover. */
404
378
  .grid {
@@ -409,9 +383,9 @@
409
383
  }
410
384
  .frame {
411
385
  overflow-x: auto;
412
- border: 1px solid #e3e3e8;
386
+ border: 1px solid var(--color-frame, #e3e3e8);
413
387
  border-radius: 10px;
414
- background: #fff;
388
+ background: var(--color-surface, #fff);
415
389
  }
416
390
  table {
417
391
  width: 100%;
@@ -421,8 +395,8 @@
421
395
  td {
422
396
  height: 1px;
423
397
  padding: 0;
424
- border-right: 1px solid #ececf0;
425
- border-bottom: 1px solid #ececf0;
398
+ border-right: 1px solid var(--color-frame, #ececf0);
399
+ border-bottom: 1px solid var(--color-frame, #ececf0);
426
400
  vertical-align: top;
427
401
  transition: background 0.1s;
428
402
  }
@@ -434,16 +408,16 @@
434
408
  }
435
409
  tr.row-on td,
436
410
  td.col-on {
437
- background: #f8faff;
411
+ background: color-mix(in srgb, #3b82f6 4%, transparent);
438
412
  }
439
413
  /* The header and the footer as on the site: a band above, the sums bold on grey behind a thicker line. */
440
414
  tr.h-top td:not(.rgrip) {
441
- background: #f1f1f4;
442
- border-bottom: 2px solid #d8d8de;
415
+ background: var(--color-muted, #f1f1f4);
416
+ border-bottom: 2px solid var(--color-frame, #d8d8de);
443
417
  }
444
418
  tr.h-bottom td:not(.rgrip) {
445
- background: #f1f1f4;
446
- border-top: 2px solid #d8d8de;
419
+ background: var(--color-muted, #f1f1f4);
420
+ border-top: 2px solid var(--color-frame, #d8d8de);
447
421
  }
448
422
  textarea {
449
423
  display: block;
@@ -461,10 +435,10 @@
461
435
  outline: none;
462
436
  }
463
437
  textarea::placeholder {
464
- color: #b8b8c0;
438
+ color: var(--color-muted-contrast, #b8b8c0);
465
439
  }
466
440
  textarea:focus {
467
- background: #fff;
441
+ background: var(--color-surface, #fff);
468
442
  box-shadow: inset 0 0 0 2px #3b82f6;
469
443
  }
470
444
  tr.h-top textarea {
@@ -472,21 +446,21 @@
472
446
  font-size: 13px;
473
447
  letter-spacing: 0.04em;
474
448
  text-transform: uppercase;
475
- color: #3a3a44;
449
+ color: var(--color-surface-contrast, #3a3a44);
476
450
  }
477
451
  tr.h-bottom textarea {
478
452
  font-weight: 600;
479
- color: #1d1d1f;
453
+ color: var(--color-surface-contrast, #1d1d1f);
480
454
  }
481
455
  /* grips: a thin row above the columns and a thin column before the rows, shown on hover */
482
456
  td.corner,
483
457
  td.rgrip {
484
458
  width: 14px;
485
- border-right: 1px solid #ececf0;
459
+ border-right: 1px solid var(--color-frame, #ececf0);
486
460
  }
487
461
  tr.grips td {
488
462
  height: 14px;
489
- border-bottom: 1px solid #ececf0;
463
+ border-bottom: 1px solid var(--color-frame, #ececf0);
490
464
  }
491
465
  .grip {
492
466
  display: grid;
@@ -497,7 +471,7 @@
497
471
  padding: 0;
498
472
  border: 0;
499
473
  background: none;
500
- color: #b0b0b8;
474
+ color: var(--color-muted-contrast, #b0b0b8);
501
475
  cursor: grab;
502
476
  opacity: 0;
503
477
  touch-action: none;
@@ -509,7 +483,7 @@
509
483
  }
510
484
  .grip:hover {
511
485
  color: #2563eb;
512
- background: #eef3fd;
486
+ background: color-mix(in srgb, #3b82f6 9%, transparent);
513
487
  }
514
488
  table.dragging,
515
489
  table.dragging * {
@@ -540,8 +514,8 @@
540
514
  padding: 0;
541
515
  border: 0;
542
516
  border-radius: 6px;
543
- background: #f4f4f6;
544
- color: #8a8a94;
517
+ background: var(--color-muted, #f4f4f6);
518
+ color: var(--color-muted-contrast, #8a8a94);
545
519
  cursor: pointer;
546
520
  opacity: 0;
547
521
  transition: opacity 0.12s, background 0.12s;
@@ -557,7 +531,7 @@
557
531
  }
558
532
  .add-col:hover,
559
533
  .add-row:hover {
560
- background: #e8eefc;
534
+ background: color-mix(in srgb, #3b82f6 12%, transparent);
561
535
  color: #2563eb;
562
536
  }
563
537
  </style>
@@ -46,7 +46,7 @@
46
46
  th,
47
47
  td {
48
48
  padding: 0.45em 0.7em;
49
- border-bottom: 1px solid #e5e5ea;
49
+ border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
50
50
  text-align: left;
51
51
  vertical-align: top;
52
52
  }
@@ -57,6 +57,6 @@
57
57
  border-bottom-width: 2px;
58
58
  }
59
59
  th.bottom {
60
- border-top: 2px solid #e5e5ea;
60
+ border-top: 2px solid color-mix(in srgb, currentColor 22%, transparent);
61
61
  }
62
62
  </style>
@@ -70,7 +70,7 @@
70
70
  }
71
71
  figcaption {
72
72
  margin-top: 0.4em;
73
- color: #6b6b76;
73
+ color: color-mix(in srgb, currentColor 65%, transparent);
74
74
  font: 13px/1.4 system-ui, sans-serif;
75
75
  }
76
76
  </style>
@@ -1,6 +1,6 @@
1
1
  // The bundled plugins: plugins like any other (written with the kit), an app picks the ones it wants. Only general
2
2
  // blocks live here (video, map, table, gap); anything an app gives its own meaning to belongs in the app.
3
- import { MapPin, SquarePlay, Table, UnfoldVertical } from '@lucide/svelte';
3
+ import { MapPin, PanelBottom, PanelTop, SquarePlay, Table, UnfoldVertical } from '@lucide/svelte';
4
4
  import { mdText } from '../kit/util';
5
5
  import { youtubeId } from './util';
6
6
  import Gap, { GAP_LINES } from './Gap.svelte';
@@ -42,6 +42,17 @@ export const table = {
42
42
  keys: 'table grid rows columns price list',
43
43
  icon: Table,
44
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
+ ],
45
56
  tag: (p) => `table · ${p.rows?.length ?? 0}×${p.rows?.[0]?.length ?? 0}`,
46
57
  content: 'none',
47
58
  markdown: (p) => {
package/dist/schema.js CHANGED
@@ -1,5 +1,5 @@
1
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 and images only from the host's media.
2
+ // keep links to http(s)/mailto/tel/site paths, take colours only as #hex.
3
3
  import { z } from 'zod';
4
4
  import { acceptsId, MAX_DEPTH, MAX_INDENT } from './model';
5
5
  const block = z.lazy(() => z.strictObject({
package/dist/types.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { MediaItem } from './media';
2
1
  import type { Component } from 'svelte';
3
2
  import type { StoredBlock } from './model';
4
3
  import type { Icon } from './palette';
@@ -31,10 +30,11 @@ export interface PropField {
31
30
  key: string;
32
31
  label: string;
33
32
  /**
34
- * 'image' picks one of the document's images (stores its id), 'images' several (an array of ids, in picking order).
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
35
  * 'slider' steps through `options` in their order (e.g. heights), or, with `min` and `max`, sets a number freely.
36
36
  */
37
- kind: 'text' | 'textarea' | 'color' | 'select' | 'slider' | 'image' | 'images';
37
+ kind: 'text' | 'textarea' | 'color' | 'select' | 'slider' | 'toggles';
38
38
  placeholder?: string;
39
39
  /** A number slider: its range, step (default 1) and unit shown after the value. */
40
40
  min?: number;
@@ -56,10 +56,11 @@ export interface PropField {
56
56
  key: string;
57
57
  label: string;
58
58
  }[];
59
- /** For 'select' and 'slider'. */
59
+ /** For 'select', 'slider' and 'toggles' (where `icon` is the button and `label` its tooltip). */
60
60
  options?: {
61
61
  value: string;
62
62
  label: string;
63
+ icon?: Icon;
63
64
  }[];
64
65
  /** The field's text from the stored value; default: the value as a string. */
65
66
  format?: (value: unknown) => string;
@@ -86,8 +87,6 @@ export interface PluginPreviewProps<P extends Props = Props> {
86
87
  export interface MarkdownContext {
87
88
  /** The nested blocks as markdown ('' when it has none). */
88
89
  inner: string;
89
- /** The media of the document, to turn an attachment id into its url. */
90
- media: MediaItem[];
91
90
  }
92
91
  export interface BlockPlugin<P extends Props = Props> {
93
92
  /** Stored `type`; must not start with 'md:'. */
@@ -112,7 +111,7 @@ export interface BlockPlugin<P extends Props = Props> {
112
111
  */
113
112
  preview?: Component<PluginPreviewProps<any>>;
114
113
  /**
115
- * For a plugin with nested blocks: the blocks it takes, as palette ids ('md:p', 'image'…); 'md:*' stands for every
114
+ * For a plugin with nested blocks: the blocks it takes, as palette ids ('md:p', 'youtube'…); 'md:*' stands for every
116
115
  * built-in block. Its nested editor offers the host's palette narrowed to these. Default: everything the host has.
117
116
  */
118
117
  accepts?: string[];
@@ -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` | — (edited in place) | a GFM table | Rows and columns of inline-formatted cells; rows and columns are dragged 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 | 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
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. |
@@ -79,18 +79,17 @@ Markdown is a first-class format: the clipboard carries it, and a host can impor
79
79
  | `fromStored(block)` | `StoredBlock` → `Block` | anywhere |
80
80
  | `toStored(block)` | `Block` → `StoredBlock` | browser only |
81
81
  | `fromMarkdown(text)` | markdown → `Block[]` | anywhere |
82
- | `toMarkdown(blocks, { plugins, media })` | `Block[]` → markdown | browser only |
82
+ | `toMarkdown(blocks, { plugins })` | `Block[]` → markdown | browser only |
83
83
  | `inline(md)` | inline markdown → inline HTML | anywhere |
84
84
 
85
85
  The markdown functions work on the editor form (`Block[]`), so stored documents go through `fromStored` and `toStored`:
86
86
 
87
87
  ```ts
88
- import { fromMarkdown, fromStored, toMarkdown, toStored, type MediaItem, type StoredBlock } from '@atom-forge/brickee';
88
+ import { fromMarkdown, fromStored, toMarkdown, toStored, type StoredBlock } from '@atom-forge/brickee';
89
89
  import { PLUGINS } from '@atom-forge/brickee/plugins';
90
90
 
91
91
  // Stored document -> markdown (browser only).
92
- export const exportMarkdown = (doc: StoredBlock[], media: MediaItem[]) =>
93
- toMarkdown(doc.map(fromStored), { plugins: PLUGINS, media });
92
+ export const exportMarkdown = (doc: StoredBlock[]) => toMarkdown(doc.map(fromStored), { plugins: PLUGINS });
94
93
 
95
94
  // Markdown -> stored document (browser only).
96
95
  export const importMarkdown = (text: string): StoredBlock[] => fromMarkdown(text).map(toStored);
@@ -110,6 +109,6 @@ What `toMarkdown` writes:
110
109
 
111
110
  - consecutive list items on consecutive lines, every other block separated by a blank line;
112
111
  - list items and paragraphs indented by two spaces per level;
113
- - a plugin block through its plugin's `markdown(props, { inner, media })`, where `inner` is its nested blocks already converted; a plugin without a markdown form, or one returning `''`, is left out.
112
+ - 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.
114
113
 
115
114
  Writing a markdown form for your own plugin is covered in [Writing plugins](writing-plugins.md#markdown-form).
@@ -9,7 +9,6 @@
9
9
  | `value` | `StoredBlock[]` (bindable) | `[]` | The document. Written back on every change; a document replaced from outside is loaded again. |
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
- | `media` | `MediaItem[]` | — | The images blocks can pick from (see [Host contexts](host-contexts.md)). |
13
12
  | `messages` | `Messages` | — | UI text in the host's language: English text → translation. |
14
13
  | `readonly` | `boolean` | `false` | Shows the document without editing; plugin fields are hidden. |
15
14
  | `placeholder` | `string` | `Write something, or press '/' for blocks…` | The hint of an empty block. |
@@ -23,7 +22,6 @@ An empty document is shown as one empty paragraph, so the value the editor write
23
22
  |---|---|---|---|
24
23
  | `blocks` | `StoredBlock[]` | — | The document to render. |
25
24
  | `plugins` | `BlockPlugin[]` | — | The plugins whose previews render the plugin blocks. |
26
- | `media` | `MediaItem[]` | — | The images the blocks refer to by id. |
27
25
  | `messages` | `Messages` | — | UI text in the host's language. |
28
26
  | `markGated` | `boolean` | `true` | Marks the blocks after a gate block (see [Unique and gate blocks](writing-plugins.md#unique-and-gate-blocks)). |
29
27
  | `nested` | `boolean` | `false` | Set by plugin previews that render their nested blocks. |
@@ -32,7 +30,7 @@ The preview renders built-in blocks as plain HTML (`p`, `h1`…`h6`, `ul`/`ol` w
32
30
 
33
31
  Using `Preview` at all is your choice: the editor does not depend on it. An application with its own renderer can render the stored blocks itself, and its plugins can leave `preview` out.
34
32
 
35
- `media` and `messages` given to the top-level editor or preview are inherited by every nested editor, nested preview and plugin component below it.
33
+ `messages` given to the top-level editor or preview are inherited by every nested editor, nested preview and plugin component below it.
36
34
 
37
35
  ## The palette
38
36
 
@@ -10,7 +10,7 @@ Everything else is a plugin. A plugin is a plain object (`BlockPlugin`) that nam
10
10
 
11
11
  - a **view** — a Svelte component shown in the editor;
12
12
  - a **preview** — a Svelte component that renders the block on the site;
13
- - **fields** — the settings shown on the top of the block (text, colour, select, slider, image…);
13
+ - **fields** — the settings shown on the top of the block (text, colour, select, slider…);
14
14
  - optionally a **markdown form**, used when the document is copied or exported as markdown.
15
15
 
16
16
  The package ships four general-purpose plugins — table, YouTube video, map and gap — but they are plugins like any other: nothing in the core depends on them, and you pass only the ones you want. Image blocks, boxes and other site-specific blocks are plugins you write yourself.
@@ -24,7 +24,7 @@ bun add @atom-forge/brickee @atom-forge/ui @lucide/svelte
24
24
  | Peer dependency | Why |
25
25
  |---|---|
26
26
  | `svelte` `^5.0.0` | The editor, the preview and the plugins are Svelte 5 components. |
27
- | `@atom-forge/ui` `>=0.0.37` | The popups (link prompt, image picker) and the form controls of the block fields. |
27
+ | `@atom-forge/ui` `>=0.0.37` | The popups (link prompt) and the form controls of the block fields. |
28
28
  | `@lucide/svelte` `>=1.0.0` | The block icons. |
29
29
  | `zod` `^4.0.0` (optional) | Only for the server-side schema in `@atom-forge/brickee/schema`. |
30
30
 
@@ -37,6 +37,10 @@ The block fields and popups are `@atom-forge/ui` components, so the application
37
37
 
38
38
  The top-level `Editor` creates its own popup manager and renders its own popup container; editors nested in plugin blocks share it.
39
39
 
40
+ The editor takes its colours from the `@atom-forge/ui` theme tokens (`--color-surface`, `--color-frame`, `--color-muted-contrast`…), so it follows the host's light and dark mode. The preview's borders and captions are drawn from the text colour of the page.
41
+
42
+ The block handles are drawn in a gutter **left of the editor's box**, about 22 px wide: give the element around the editor that much padding on the left, or the handles stick out of it.
43
+
40
44
  > **Missing theme**
41
45
  >
42
46
  > Without the `@atom-forge/ui` theme CSS the editor still works, but the block fields, the colour picker, the slider and the popups appear unstyled.
@@ -2,143 +2,21 @@
2
2
 
3
3
  [← Previous](writing-plugins.md) · [Contents](index.md) · [Next →](server-side.md)
4
4
 
5
- The editor has no props for application concepts. What a host knows — its images, the page width it is showing, its language — reaches the plugins through three Svelte contexts. Each has a `provide…` function, called in a component's initialization above the editor or preview, and a `use…` function for plugin components.
5
+ The editor has no props for application concepts. What a host knows — the page width it is showing, the code languages it highlights, its language — reaches the plugins through three Svelte contexts. Each has a `provide…` function, called in a component's initialization above the editor or preview, and a `use…` function for plugin components.
6
6
 
7
7
  | Context | Provide | Read | Carries |
8
8
  |---|---|---|---|
9
- | Media | `media` prop, or `provideMedia(() => list)` | `useMedia()` | The images blocks can pick from. |
10
9
  | View | `provideView(() => key)` | `useView()` | The view per-view fields edit. |
10
+ | Code languages | `provideCodeLanguages(() => list)` | `useCodeLanguages()` | The languages a code block offers. |
11
11
  | Text | `messages` prop, or `provideText(() => messages)` | `useText()` | UI text in the host's language. |
12
12
 
13
13
  Nested editors and previews inherit all three from their parent.
14
14
 
15
- ## Media
16
-
17
- Blocks never store image URLs. An image field stores the **id** of a `MediaItem`, and the editor, the preview and the plugins look the item up in the document's media:
18
-
19
- ```ts
20
- interface MediaItem {
21
- id: string; // stored in the block's props
22
- name: string; // file name or title, shown in the picker
23
- url: string; // the image's address
24
- group?: string; // a heading in the picker (e.g. the attachment category)
25
- }
26
- ```
27
-
28
- The bundled plugins use no images; image blocks are yours to write, because their layout is the site's. A minimal `photo` plugin — an image field, alt text, and a height slider:
29
-
30
- ```svelte
31
- <!-- PhotoView.svelte -->
32
- <script lang="ts" module>
33
- export interface PhotoProps extends Record<string, unknown> {
34
- /** A MediaItem id. */
35
- image: string;
36
- alt: string;
37
- /** The height in % of the width (20–150), or unset for the image's own shape. */
38
- ratio?: unknown;
39
- }
40
-
41
- /** A CSS aspect-ratio from a stored height, or undefined. */
42
- export const aspect = (v: unknown) => {
43
- const n = Number(v);
44
- return n >= 20 && n <= 150 ? `100 / ${n}` : undefined;
45
- };
46
- </script>
47
-
48
- <script lang="ts">
49
- import { ImageIcon } from '@lucide/svelte';
50
- import { Empty, findMedia, useMedia, type PluginViewProps } from '@atom-forge/brickee';
51
-
52
- let { props = $bindable(), blocks = $bindable() }: PluginViewProps<PhotoProps> = $props();
53
- const media = useMedia();
54
- const item = $derived(findMedia(media(), props.image));
55
- </script>
56
-
57
- {#if item}
58
- <img src={item.url} alt={props.alt} style="max-width: 100%" />
59
- {:else}
60
- <Empty icon={ImageIcon} text="Pick an image above" />
61
- {/if}
62
- ```
63
-
64
- ```svelte
65
- <!-- PhotoPreview.svelte -->
66
- <script lang="ts">
67
- import { baseValue, findMedia, useMedia, type PluginPreviewProps } from '@atom-forge/brickee';
68
- import { aspect, type PhotoProps } from './PhotoView.svelte';
69
-
70
- let { props }: PluginPreviewProps<PhotoProps> = $props();
71
- const media = useMedia();
72
- const item = $derived(findMedia(media(), props.image));
73
- </script>
74
-
75
- {#if item}
76
- <img src={item.url} alt={props.alt ?? ''} style:aspect-ratio={aspect(baseValue(props.ratio))} />
77
- {/if}
78
-
79
- <style>
80
- img {
81
- width: 100%;
82
- object-fit: cover;
83
- }
84
- </style>
85
- ```
86
-
87
- ```ts
88
- // photo.ts
89
- import { ImageIcon } from '@lucide/svelte';
90
- import { findMedia, mdText, type BlockPlugin } from '@atom-forge/brickee';
91
- import PhotoView, { type PhotoProps } from './PhotoView.svelte';
92
- import PhotoPreview from './PhotoPreview.svelte';
93
-
94
- export const photo: BlockPlugin<PhotoProps> = {
95
- type: 'photo',
96
- label: 'Photo',
97
- hint: 'One picked image',
98
- keys: 'photo image picture',
99
- icon: ImageIcon,
100
- props: () => ({ image: '', alt: '' }),
101
- fields: [
102
- { key: 'image', label: 'Image', kind: 'image' },
103
- { key: 'alt', label: 'Alt text', kind: 'text', placeholder: 'What the image shows' },
104
- { key: 'ratio', label: 'Height', kind: 'slider', min: 20, max: 150, unit: '%', none: 'Original' }
105
- ],
106
- tag: () => 'photo',
107
- content: 'none',
108
- markdown: (p, { media }) => {
109
- const url = findMedia(media, p.image)?.url;
110
- return url ? `![${mdText(p.alt)}](${url})` : '';
111
- },
112
- view: PhotoView,
113
- preview: PhotoPreview
114
- };
115
- ```
116
-
117
- Give the editor and the preview the same media:
118
-
119
- ```svelte
120
- <script lang="ts">
121
- import { Editor, Preview, type MediaItem, type StoredBlock } from '@atom-forge/brickee';
122
- import { photo } from './photo';
123
-
124
- let { value = $bindable(), media }: { value: StoredBlock[]; media: MediaItem[] } = $props();
125
- </script>
126
-
127
- <Editor bind:value plugins={[photo]} {media} />
128
- <Preview blocks={value} plugins={[photo]} {media} />
129
- ```
130
-
131
- In a plugin component, read the list with `useMedia()` — it returns a function, so the list stays current — and find an item with `findMedia(list, id)`, as the photo's view and preview do.
132
-
133
- > **Thumbnails in the editor**
134
- >
135
- > The editor only needs small images. Because blocks store ids, you can give the editor a list of thumbnail URLs and the preview on the site a list of full-size URLs with the same ids.
136
-
137
- Typical sources are a page's uploaded attachments or a media library. Every image a document refers to must be in the list, or its block renders nothing.
15
+ Images are not a context: what an image block stores and where its pictures come from is decided by the app's own plugin (see [Images](writing-plugins.md#images)).
138
16
 
139
17
  ## Per-view values
140
18
 
141
- Some settings depend on how the page is shown: an image may need a different aspect ratio on a phone than on a wide screen. A `slider` field with `views` stores a value per view:
19
+ Some settings depend on how the page is shown: an image may need a different aspect ratio on a phone than on a wide screen. A `slider` field with `views` stores a value per view. Here it is the height of the `photo` plugin from [Writing plugins](writing-plugins.md#images) and the bundled `gap`:
142
20
 
143
21
  ```ts
144
22
  import type { BlockPlugin } from '@atom-forge/brickee';
@@ -184,18 +62,17 @@ A preview reads them with `viewValues(value, keys)` — the value for each view,
184
62
 
185
63
  ```svelte
186
64
  <script lang="ts">
187
- import { findMedia, useMedia, viewValues, type PluginPreviewProps } from '@atom-forge/brickee';
188
- import { aspect, type PhotoProps } from './PhotoView.svelte';
65
+ import { viewValues, type PluginPreviewProps } from '@atom-forge/brickee';
66
+ import { aspect, imageSrc, type PhotoProps } from './PhotoView.svelte';
189
67
 
190
68
  let { props }: PluginPreviewProps<PhotoProps> = $props();
191
- const media = useMedia();
192
- const item = $derived(findMedia(media(), props.image));
69
+ const src = $derived(imageSrc(props.url));
193
70
  // desktop, tablet, phone: a CSS aspect-ratio each ('auto': the image's own shape)
194
71
  const r = $derived(viewValues(props.ratio, ['desktop', 'tablet', 'phone']).map((v) => aspect(v) ?? 'auto'));
195
72
  </script>
196
73
 
197
- {#if item}
198
- <img src={item.url} alt={props.alt ?? ''} style:--r-desktop={r[0]} style:--r-tablet={r[1]} style:--r-phone={r[2]} />
74
+ {#if src}
75
+ <img {src} alt={props.alt ?? ''} style:--r-desktop={r[0]} style:--r-tablet={r[1]} style:--r-phone={r[2]} />
199
76
  {/if}
200
77
 
201
78
  <style>
@@ -221,6 +98,23 @@ A preview reads them with `viewValues(value, keys)` — the value for each view,
221
98
  >
222
99
  > `views` is supported on `slider` fields. Other field kinds ignore it.
223
100
 
101
+ ## Code languages
102
+
103
+ A code block's language (`props.lang`, the info string of its markdown fence) is free text by default. When the host knows which languages its site can highlight, it gives the list, and the field in the code block's fields bar becomes a dropdown of them, with *None* first:
104
+
105
+ ```svelte
106
+ <script lang="ts">
107
+ import { Editor, provideCodeLanguages, type StoredBlock } from '@atom-forge/brickee';
108
+
109
+ let { value = $bindable() }: { value: StoredBlock[] } = $props();
110
+ provideCodeLanguages(() => [{ value: 'ts', label: 'TypeScript' }, { value: 'svelte', label: 'Svelte' }, 'css', 'bash']);
111
+ </script>
112
+
113
+ <Editor bind:value />
114
+ ```
115
+
116
+ An item is a string or `{ value, label? }`; the value is what is stored. Nested editors inherit the list. A stored language missing from the list stays an option, so opening an older document loses nothing.
117
+
224
118
  ## UI text
225
119
 
226
120
  The editor and the plugins are written in English, and the English text is the key of its translation. Pass `messages` — an object from English text to your language — to the top-level editor and preview: