@atom-forge/brickee 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README-AI.md +10 -7
  3. package/dist/Editor.svelte +691 -833
  4. package/dist/LinkPrompt.svelte +1 -14
  5. package/dist/Preview.svelte +27 -54
  6. package/dist/Preview.svelte.d.ts +2 -4
  7. package/dist/PropsBar.svelte +35 -219
  8. package/dist/code.js +1 -1
  9. package/dist/dom.d.ts +8 -0
  10. package/dist/dom.js +9 -0
  11. package/dist/editor/BlockMenu.svelte +102 -0
  12. package/dist/editor/BlockMenu.svelte.d.ts +32 -0
  13. package/dist/editor/FormatBar.svelte +41 -0
  14. package/dist/editor/FormatBar.svelte.d.ts +16 -0
  15. package/dist/editor/drag.svelte.d.ts +63 -0
  16. package/dist/editor/drag.svelte.js +95 -0
  17. package/dist/editor/history.d.ts +24 -0
  18. package/dist/editor/history.js +86 -0
  19. package/dist/editor/sweep.svelte.d.ts +21 -0
  20. package/dist/editor/sweep.svelte.js +56 -0
  21. package/dist/html.d.ts +12 -0
  22. package/dist/html.js +206 -0
  23. package/dist/i18n.js +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +3 -2
  26. package/dist/kit/Empty.svelte +4 -15
  27. package/dist/kit/Frame.svelte +7 -37
  28. package/dist/locales/hu.js +4 -1
  29. package/dist/markdown.d.ts +20 -4
  30. package/dist/markdown.js +380 -85
  31. package/dist/model.js +2 -3
  32. package/dist/plugins/Gap.svelte +5 -8
  33. package/dist/plugins/GapPreview.svelte +4 -6
  34. package/dist/plugins/Map.svelte +2 -17
  35. package/dist/plugins/MapPreview.svelte +8 -10
  36. package/dist/plugins/Table.svelte +233 -256
  37. package/dist/plugins/TablePreview.svelte +28 -28
  38. package/dist/plugins/YouTube.svelte +3 -31
  39. package/dist/plugins/YouTubePreview.svelte +45 -43
  40. package/dist/plugins/index.js +3 -1
  41. package/dist/types.d.ts +21 -3
  42. package/dist/types.js +8 -1
  43. package/dist/view.js +1 -1
  44. package/docs/en/bundled-plugins.md +1 -1
  45. package/docs/en/documents-and-markdown.md +20 -14
  46. package/docs/en/editor-and-preview.md +6 -7
  47. package/docs/en/getting-started.md +8 -0
  48. package/docs/en/index.md +1 -1
  49. package/docs/en/recipes-and-pitfalls.md +2 -2
  50. package/docs/en/server-side.md +0 -17
  51. package/docs/en/upgrading.md +14 -0
  52. package/docs/en/writing-plugins.md +36 -10
  53. package/docs/hu/bundled-plugins.md +1 -1
  54. package/docs/hu/documents-and-markdown.md +20 -14
  55. package/docs/hu/editor-and-preview.md +6 -7
  56. package/docs/hu/getting-started.md +8 -0
  57. package/docs/hu/index.md +1 -1
  58. package/docs/hu/recipes-and-pitfalls.md +2 -2
  59. package/docs/hu/server-side.md +0 -17
  60. package/docs/hu/upgrading.md +14 -0
  61. package/docs/hu/writing-plugins.md +36 -10
  62. package/package.json +9 -1
@@ -22,21 +22,8 @@
22
22
  </script>
23
23
 
24
24
  <!-- svelte-ignore a11y_no_static_element_interactions -->
25
- <div class="link" {onkeydown}>
25
+ <div class="flex w-[340px] gap-1.5 rounded-xl border border-frame bg-surface p-2 shadow-[0_10px_36px_rgb(0_0_0/0.2)]" {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}
29
29
  </div>
30
-
31
- <style>
32
- .link {
33
- display: flex;
34
- gap: 6px;
35
- width: 340px;
36
- padding: 8px;
37
- background: var(--color-surface, #fff);
38
- border: 1px solid var(--color-frame, #e5e5ea);
39
- border-radius: 12px;
40
- box-shadow: 0 10px 36px rgba(0, 0, 0, 0.2);
41
- }
42
- </style>
@@ -1,8 +1,9 @@
1
- <!-- Styles travel with the markup: the sites show the blocks as server-rendered HTML (see content.server.ts). -->
1
+ <!-- Renders a document the way a site shows it. Styles travel with the markup (injected), so the preview looks the same
2
+ wherever it is mounted. -->
2
3
  <svelte:options css="injected" />
3
4
 
4
5
  <script lang="ts" module>
5
- import type { StoredBlock } from './types';
6
+ import { olStart, type StoredBlock } from './types';
6
7
 
7
8
  const LIST = new Set(['md:ul', 'md:ol', 'md:todo']);
8
9
 
@@ -55,25 +56,18 @@
55
56
  interface Props {
56
57
  blocks: StoredBlock[];
57
58
  plugins: BlockPlugin[];
58
- /** Set by plugins rendering their nested blocks; only the top level splits at the supporters-only divider. */
59
+ /** Set by plugins rendering their nested blocks (a container's preview). */
59
60
  nested?: boolean;
60
61
  /** UI text in the host's language; nested previews share their parent's. */
61
62
  messages?: Messages;
62
- /** Mark the blocks after a gate (the supporters-only divider) with a label and a dashed line; default: yes. */
63
- markGated?: boolean;
64
63
  }
65
- let { blocks, plugins, nested = false, messages, markGated = true }: Props = $props();
64
+ let { blocks, plugins, nested = false, messages }: Props = $props();
66
65
 
67
66
  provideText(() => messages);
68
67
  const tr = useText();
69
68
 
70
69
  const pluginOf = (type: string) => plugins.find((p) => p.type === type);
71
70
 
72
- // Everything after a unique supporters-only divider is for supporters: shown here, marked as such.
73
- const lockAt = $derived(nested || !markGated ? -1 : blocks.findIndex((b) => pluginOf(b.type)?.gate));
74
- const open = $derived(lockAt < 0 ? blocks : blocks.slice(0, lockAt + 1));
75
- const locked = $derived(lockAt < 0 ? [] : blocks.slice(lockAt + 1));
76
-
77
71
  // Inline markdown -> html: text is escaped first, links are http(s), mailto, tel or site-relative only (see inline()).
78
72
  const html = (b: StoredBlock) => inline(b.text ?? '');
79
73
  const indent = (b: StoredBlock) => (b.indent ? `${b.indent * 1.5}em` : undefined);
@@ -81,7 +75,8 @@
81
75
 
82
76
  {#snippet list(node: ListNode)}
83
77
  {#if node.type === 'md:ol'}
84
- <ol>{@render items(node)}</ol>
78
+ {@const start = olStart(node.items[0]?.block.props)}
79
+ <ol start={start === 1 ? undefined : start}>{@render items(node)}</ol>
85
80
  {:else}
86
81
  <ul class:todo={node.type === 'md:todo'}>{@render items(node)}</ul>
87
82
  {/if}
@@ -136,47 +131,25 @@
136
131
  {/snippet}
137
132
 
138
133
  <div class="preview" class:nested>
139
- {@render run(open)}
140
- {#if locked.length}
141
- <div class="supporters" aria-label={tr('For supporters only')}>
142
- <p class="supporters-label">{tr('For supporters only')}</p>
143
- {@render run(locked)}
144
- </div>
145
- {/if}
134
+ {@render run(blocks)}
146
135
  </div>
147
136
 
148
- <style>
149
- /* Only what the renderer itself adds; the article typography comes from the page that shows it. */
150
- .supporters {
151
- margin-top: 1.5em;
152
- padding-top: 1em;
153
- border-top: 2px dashed #f96854;
154
- }
155
- .supporters-label {
156
- font: 600 11px/1 system-ui, sans-serif;
157
- letter-spacing: 0.08em;
158
- text-transform: uppercase;
159
- color: #f96854;
160
- }
161
- .preview :global(ul.todo) {
162
- list-style: none;
163
- padding-left: 0.2em;
164
- }
165
- .preview :global(ul.todo input) {
166
- margin-right: 0.5em;
167
- }
168
- .preview :global(img) {
169
- max-width: 100%;
170
- }
171
- /* The content starts at its top edge, whatever its first block is (pages often align it with a sidebar). */
172
- .preview:not(.nested) > :global(:first-child),
173
- .preview:not(.nested) > :global(:first-child > :first-child) {
174
- margin-top: 0;
175
- }
176
- /* Floating images and side boxes stay inside the content. */
177
- .preview:not(.nested)::after {
178
- content: '';
179
- display: block;
180
- clear: both;
181
- }
182
- </style>
137
+ <style>.preview :global(ul.todo) {
138
+ list-style: none;
139
+ padding-left: 0.2em;
140
+ }
141
+ .preview :global(ul.todo input) {
142
+ margin-right: 0.5em;
143
+ }
144
+ .preview :global(img) {
145
+ max-width: 100%;
146
+ }
147
+ .preview:not(.nested) > :global(:first-child),
148
+ .preview:not(.nested) > :global(:first-child > :first-child) {
149
+ margin-top: 0;
150
+ }
151
+ .preview:not(.nested)::after {
152
+ content: "";
153
+ display: block;
154
+ clear: both;
155
+ }</style>
@@ -1,4 +1,4 @@
1
- import type { StoredBlock } from './types';
1
+ import { type StoredBlock } from './types';
2
2
  /** Consecutive list blocks as properly nested lists: an item one level deeper goes into the previous item. */
3
3
  export type ListNode = {
4
4
  type: string;
@@ -20,12 +20,10 @@ import type { BlockPlugin } from './types';
20
20
  interface Props {
21
21
  blocks: StoredBlock[];
22
22
  plugins: BlockPlugin[];
23
- /** Set by plugins rendering their nested blocks; only the top level splits at the supporters-only divider. */
23
+ /** Set by plugins rendering their nested blocks (a container's preview). */
24
24
  nested?: boolean;
25
25
  /** UI text in the host's language; nested previews share their parent's. */
26
26
  messages?: Messages;
27
- /** Mark the blocks after a gate (the supporters-only divider) with a label and a dashed line; default: yes. */
28
- markGated?: boolean;
29
27
  }
30
28
  declare const Preview: import("svelte").Component<Props, {}, "">;
31
29
  type Preview = ReturnType<typeof Preview>;
@@ -113,26 +113,36 @@
113
113
  const long = $derived(fields.filter((f) => f.kind === 'textarea'));
114
114
  </script>
115
115
 
116
- <div class="bar" class:open>
117
- <button type="button" class="head" aria-expanded={open} title={tr(open ? 'Hide settings' : 'Settings')} onmousedown={(e) => e.preventDefault()} onclick={() => (open = !open)}>
118
- <span class="tag">{tag}</span>
119
- <ChevronDown size={13} class="chev" />
116
+ <div
117
+ data-fields
118
+ class="grid gap-1.5 rounded-lg font-sans text-xs leading-[1.4] text-surface-contrast transition-[padding,background] duration-[180ms] {open
119
+ ? 'mb-2 justify-items-stretch bg-[rgb(120_120_140/0.07)] px-2 pt-1.5 pb-2'
120
+ : 'mb-1 justify-items-start'}"
121
+ >
122
+ <button type="button" data-fields-head class="group flex cursor-pointer items-center gap-0.5 justify-self-start border-0 bg-transparent p-0 text-muted-contrast" aria-expanded={open} title={tr(open ? 'Hide settings' : 'Settings')} onmousedown={(e) => e.preventDefault()} onclick={() => (open = !open)}>
123
+ <span class="rounded-[5px] bg-[rgb(120_120_140/0.12)] px-1.5 py-px font-mono text-[11px] leading-[1.5] font-medium whitespace-nowrap text-muted-contrast select-none group-hover:bg-[rgb(120_120_140/0.2)] group-hover:text-surface-contrast">{tag}</span>
124
+ <ChevronDown size={13} class="transition-[rotate] duration-150 {open ? 'rotate-180' : ''}" />
120
125
  </button>
121
126
  {#if open}
122
- <div class="body" transition:slide={{ duration: 180 }}>
123
- <div class="grid">
127
+ <div transition:slide={{ duration: 180 }}>
128
+ <!-- the fields in columns as wide as the box allows; text fields carry their label as placeholder, the others a short
129
+ label beside the control; the bar's small type in the controls too -->
130
+ <div class="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-x-2.5 gap-y-1.5 pt-1.5 [&_input:not([type=range])]:text-[12px] [&_select]:text-[12px] [&_textarea]:text-[12px]">
124
131
  {#each short as f (f.key)}
125
132
  {#if f.kind === 'text'}
126
133
  <!-- the field shows the value as kept (a YouTube link becomes its id); its label is the placeholder -->
127
- <label class="field" title={tr(f.label)}>
128
- <span class="sr">{tr(f.label)}</span>
134
+ <label class="grid min-w-0 content-start" title={tr(f.label)}>
135
+ <span class="sr-only">{tr(f.label)}</span>
129
136
  <Input small monospace placeholder={hint(f)} bind:value={() => text(f), (v: string) => setProp(block, f, v)} />
130
137
  </label>
131
138
  {:else}
132
139
  <!-- not a label around buttons (a slider's view buttons, a colour's clear, toggles): a click on a label goes to its first button -->
133
- <svelte:element this={f.kind === 'select' ? 'label' : 'div'} class="field inline" role={f.kind === 'toggles' ? 'group' : undefined} aria-label={f.kind === 'toggles' ? tr(f.label) : undefined}>
134
- <span class="name"
135
- >{tr(f.label)}{#if viewLabel(f)}<span class="view" class:own={current(f).mine}>{viewLabel(f)}</span>{/if}</span
140
+ <svelte:element this={f.kind === 'select' ? 'label' : 'div'} class="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] content-start items-center gap-2" role={f.kind === 'toggles' ? 'group' : undefined} aria-label={f.kind === 'toggles' ? tr(f.label) : undefined}>
141
+ <span class="text-[11.5px] font-semibold whitespace-nowrap text-muted-contrast select-none"
142
+ >{tr(f.label)}{#if viewLabel(f)}<!-- the view a per-view field edits; dashed while it only takes a wider view's value --><span
143
+ class="ml-1.5 rounded border px-[5px] font-medium {current(f).mine ? 'border-solid border-surface-contrast text-surface-contrast' : 'border-dashed border-frame text-muted-contrast'}"
144
+ >{viewLabel(f)}</span
145
+ >{/if}</span
136
146
  >
137
147
  {#if f.kind === 'select'}
138
148
  <NativeSelect
@@ -142,12 +152,13 @@
142
152
  />
143
153
  {:else if f.kind === 'toggles'}
144
154
  {@const on = toggled(f)}
145
- <span class="toggles">
155
+ <span class="inline-flex gap-px justify-self-start rounded-[7px] bg-muted p-0.5">
146
156
  {#each f.options ?? [] as opt (opt.value)}
147
157
  <button
148
158
  type="button"
149
- class="toggle"
150
- class:on={on.includes(opt.value)}
159
+ class="inline-grid h-6 min-w-[26px] cursor-pointer place-items-center rounded-[5px] border-0 px-1 {on.includes(opt.value)
160
+ ? 'bg-surface text-accent shadow-[0_1px_2px_rgb(0_0_0/0.08)]'
161
+ : 'bg-transparent text-muted-contrast hover:text-surface-contrast'}"
151
162
  aria-pressed={on.includes(opt.value)}
152
163
  title={tr(opt.label)}
153
164
  aria-label={tr(opt.label)}
@@ -158,8 +169,8 @@
158
169
  {/each}
159
170
  </span>
160
171
  {:else if f.kind === 'slider'}
161
- <span class="line">
162
- <span class="slider">
172
+ <span class="flex min-w-0 items-center gap-2">
173
+ <span class="block min-w-20 flex-1">
163
174
  <Slider
164
175
  small
165
176
  drawStops={!free(f) && (f.options?.length ?? 0) <= 10}
@@ -169,19 +180,20 @@
169
180
  bind:value={() => pos(f), (v: number) => setPos(f, v)}
170
181
  />
171
182
  </span>
172
- <span class="value">{posLabel(f)}</span>
183
+ <span class="min-w-[4.5em] whitespace-nowrap text-surface-contrast tabular-nums">{posLabel(f)}</span>
173
184
  </span>
174
185
  {:else if f.kind === 'color'}
175
186
  <!-- with `none`: an unset colour shows as "none" (a click sets one); a set one can be cleared -->
176
- <span class="line">
187
+ <span class="flex min-w-0 items-center gap-2">
177
188
  {#if f.none && !text(f)}
178
- <button type="button" class="nocolor" onclick={() => setProp(block, f, f.placeholder || '#000000')}>
179
- <span class="swatch" aria-hidden="true"></span>{tr(f.none)}
189
+ <button type="button" class="flex h-[30px] cursor-pointer items-center gap-1.5 rounded-[7px] border border-dashed border-frame bg-surface pr-2.5 pl-1.5 text-muted-contrast hover:border-accent hover:text-surface-contrast" onclick={() => setProp(block, f, f.placeholder || '#000000')}>
190
+ <!-- an empty swatch, struck through -->
191
+ <span class="size-4 rounded border border-frame bg-[linear-gradient(to_top_right,transparent_calc(50%-1px),#e5484d_calc(50%-1px),#e5484d_calc(50%+1px),transparent_calc(50%+1px))]" aria-hidden="true"></span>{tr(f.none)}
180
192
  </button>
181
193
  {:else}
182
194
  <ColorPicker small bind:value={() => text(f) || '#000000', (v: string) => setProp(block, f, v)} />
183
195
  {#if f.none}
184
- <button type="button" class="clear" title={tr(f.none)} aria-label={tr(f.none)} onclick={() => setProp(block, f, '')}>×</button>
196
+ <button type="button" class="size-[22px] cursor-pointer rounded-md border-0 bg-transparent p-0 text-base leading-none text-muted-contrast hover:bg-[rgb(120_120_140/0.14)] hover:text-surface-contrast" title={tr(f.none)} aria-label={tr(f.none)} onclick={() => setProp(block, f, '')}>×</button>
185
197
  {/if}
186
198
  {/if}
187
199
  </span>
@@ -190,8 +202,8 @@
190
202
  {/if}
191
203
  {/each}
192
204
  {#each long as f (f.key)}
193
- <label class="field long" title={tr(f.label)} onfocusin={() => (editing = { key: f.key, text: text(f) })} onfocusout={() => (editing = null)}>
194
- <span class="sr">{tr(f.label)}</span>
205
+ <label class="col-span-full grid min-w-0 content-start" title={tr(f.label)} onfocusin={() => (editing = { key: f.key, text: text(f) })} onfocusout={() => (editing = null)}>
206
+ <span class="sr-only">{tr(f.label)}</span>
195
207
  <Textarea
196
208
  small
197
209
  rows={2}
@@ -207,199 +219,3 @@
207
219
  </div>
208
220
  {/if}
209
221
  </div>
210
-
211
- <style>
212
- .bar {
213
- display: grid;
214
- justify-items: start;
215
- gap: 6px;
216
- margin-bottom: 4px;
217
- border-radius: 8px;
218
- font: 12px/1.4 system-ui, sans-serif;
219
- color: var(--color-surface-contrast, #1d1d1f);
220
- }
221
- .bar.open {
222
- justify-items: stretch;
223
- margin-bottom: 8px;
224
- padding: 6px 8px 8px;
225
- background: rgba(120, 120, 140, 0.07);
226
- }
227
- .bar {
228
- transition:
229
- padding 0.18s,
230
- background 0.18s;
231
- }
232
- /* the fields in columns as wide as the box allows; text fields carry their label as placeholder, the others a short
233
- label beside the control */
234
- .grid {
235
- display: grid;
236
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
237
- gap: 6px 10px;
238
- padding-top: 6px;
239
- }
240
- .head {
241
- display: flex;
242
- align-items: center;
243
- gap: 2px;
244
- padding: 0;
245
- border: 0;
246
- background: none;
247
- color: var(--color-muted-contrast, #8e8e96);
248
- cursor: pointer;
249
- justify-self: start;
250
- }
251
- .head:hover .tag {
252
- background: rgba(120, 120, 140, 0.2);
253
- color: var(--color-surface-contrast, #1d1d1f);
254
- }
255
- .head :global(.chev) {
256
- transition: rotate 0.15s;
257
- }
258
- .open .head :global(.chev) {
259
- rotate: 180deg;
260
- }
261
- .tag {
262
- padding: 1px 6px;
263
- border-radius: 5px;
264
- background: rgba(120, 120, 140, 0.12);
265
- color: var(--color-muted-contrast, #6e6e76);
266
- font: 500 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
267
- white-space: nowrap;
268
- user-select: none;
269
- }
270
- .field {
271
- display: grid;
272
- align-content: start;
273
- min-width: 0;
274
- }
275
- .field.inline {
276
- grid-template-columns: auto minmax(0, 1fr);
277
- align-items: center;
278
- gap: 8px;
279
- }
280
- /* compact controls: the bar's small type in the inputs too */
281
- .field :global(input:not([type='range'])),
282
- .field :global(textarea),
283
- .field :global(select) {
284
- font-size: 12px;
285
- }
286
- /* the label of a field whose placeholder shows it */
287
- .sr {
288
- position: absolute;
289
- width: 1px;
290
- height: 1px;
291
- overflow: hidden;
292
- clip-path: inset(50%);
293
- white-space: nowrap;
294
- }
295
- .toggles {
296
- display: inline-flex;
297
- justify-self: start;
298
- gap: 1px;
299
- padding: 2px;
300
- border-radius: 7px;
301
- background: var(--color-muted, #f4f4f6);
302
- }
303
- .toggle {
304
- display: inline-grid;
305
- place-items: center;
306
- min-width: 26px;
307
- height: 24px;
308
- padding: 0 4px;
309
- border: 0;
310
- border-radius: 5px;
311
- background: none;
312
- color: var(--color-muted-contrast, #6e6e76);
313
- cursor: pointer;
314
- }
315
- .toggle:hover {
316
- color: var(--color-surface-contrast, #1d1d1f);
317
- }
318
- .toggle.on {
319
- background: var(--color-surface, #fff);
320
- color: #3b82f6;
321
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
322
- }
323
- /* text areas take a whole row */
324
- .field.long {
325
- grid-column: 1 / -1;
326
- }
327
- .line {
328
- display: flex;
329
- align-items: center;
330
- gap: 8px;
331
- min-width: 0;
332
- }
333
- .name {
334
- font-size: 11.5px;
335
- font-weight: 600;
336
- color: var(--color-muted-contrast, #8e8e96);
337
- white-space: nowrap;
338
- user-select: none;
339
- }
340
- /* the view a per-view field edits (the host's current one); dashed while it only takes a wider view's value */
341
- .view {
342
- margin-left: 6px;
343
- padding: 0 5px;
344
- border: 1px dashed var(--color-frame, #c8c8d0);
345
- border-radius: 4px;
346
- color: var(--color-muted-contrast, #6e6e76);
347
- font-weight: 500;
348
- }
349
- .view.own {
350
- border-style: solid;
351
- border-color: var(--color-surface-contrast, #1d1d1f);
352
- color: var(--color-surface-contrast, #1d1d1f);
353
- }
354
- .slider {
355
- display: block;
356
- flex: 1;
357
- min-width: 80px;
358
- }
359
- .value {
360
- min-width: 4.5em;
361
- color: var(--color-surface-contrast, #3a3a40);
362
- font-variant-numeric: tabular-nums;
363
- white-space: nowrap;
364
- }
365
- .nocolor {
366
- display: flex;
367
- align-items: center;
368
- gap: 6px;
369
- height: 30px;
370
- padding: 0 10px 0 6px;
371
- border: 1px dashed var(--color-frame, #c8c8d0);
372
- border-radius: 7px;
373
- background: var(--color-surface, #fff);
374
- color: var(--color-muted-contrast, #8e8e96);
375
- cursor: pointer;
376
- }
377
- .nocolor:hover {
378
- border-color: var(--color-accent, #ea580c);
379
- color: var(--color-surface-contrast, #3a3a40);
380
- }
381
- /* an empty swatch, struck through */
382
- .swatch {
383
- width: 16px;
384
- height: 16px;
385
- border: 1px solid var(--color-frame, #c8c8d0);
386
- border-radius: 4px;
387
- background: linear-gradient(to top right, transparent calc(50% - 1px), #e5484d calc(50% - 1px), #e5484d calc(50% + 1px), transparent calc(50% + 1px));
388
- }
389
- .clear {
390
- width: 22px;
391
- height: 22px;
392
- padding: 0;
393
- border: 0;
394
- border-radius: 6px;
395
- background: none;
396
- color: var(--color-muted-contrast, #8e8e96);
397
- font-size: 16px;
398
- line-height: 1;
399
- cursor: pointer;
400
- }
401
- .clear:hover {
402
- background: rgba(120, 120, 140, 0.14);
403
- color: var(--color-surface-contrast, #1d1d1f);
404
- }
405
- </style>
package/dist/code.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // The languages a code block offers. Without a list (the default) its language is free text; with one, the field is a
2
2
  // dropdown of these. Which languages make sense is the host's business (what its site can highlight).
3
3
  import { getContext, setContext } from 'svelte';
4
- const KEY = 'block-editor:code-languages';
4
+ const KEY = 'brickee:code-languages';
5
5
  const normalize = (list) => list.map((l) => (typeof l === 'string' ? { value: l } : l));
6
6
  /** Call in a component's init (around the editor). Without its own list, a nested editor keeps its parent's. */
7
7
  export function provideCodeLanguages(own) {
package/dist/dom.d.ts CHANGED
@@ -2,6 +2,14 @@
2
2
  export declare function rich(node: HTMLElement, value: string): {
3
3
  update(v: string): void;
4
4
  };
5
+ /**
6
+ * Svelte action: moves the element to the end of the body. A floating menu positioned with `position: fixed` is then
7
+ * placed against the viewport even when the editor sits in a transformed parent (an animated drawer, a dialog), where
8
+ * `fixed` would be relative to that parent.
9
+ */
10
+ export declare function portal(node: HTMLElement): {
11
+ destroy: () => void;
12
+ };
5
13
  export declare function serialize(frag: DocumentFragment): string;
6
14
  /** Returns '' for html that has no visible text (e.g. `<b></b>`, `<br>`). */
7
15
  export declare function clean(h: string): string;
package/dist/dom.js CHANGED
@@ -8,6 +8,15 @@ export function rich(node, value) {
8
8
  }
9
9
  };
10
10
  }
11
+ /**
12
+ * Svelte action: moves the element to the end of the body. A floating menu positioned with `position: fixed` is then
13
+ * placed against the viewport even when the editor sits in a transformed parent (an animated drawer, a dialog), where
14
+ * `fixed` would be relative to that parent.
15
+ */
16
+ export function portal(node) {
17
+ document.body.appendChild(node);
18
+ return { destroy: () => node.remove() };
19
+ }
11
20
  export function serialize(frag) {
12
21
  const d = document.createElement('div');
13
22
  d.append(frag);
@@ -0,0 +1,102 @@
1
+ <!-- A block menu of the editor: the slash menu (block types to insert) and the turn-into menu (what the selected
2
+ blocks can become). Compact one-line rows: icon, label, and the hint after it in grey. The editor keeps the state
3
+ (which row is active, where the menu is) and handles the keys. -->
4
+ <script lang="ts" module>
5
+ import type { Icon } from '../palette';
6
+
7
+ export interface MenuRow {
8
+ key: string;
9
+ icon: Icon;
10
+ label: string;
11
+ hint: string;
12
+ /** The hint as a warning (e.g. "Loses formatting"). */
13
+ warn?: boolean;
14
+ /** A destructive row (delete), in red. */
15
+ danger?: boolean;
16
+ /** The current type of the selection: a check mark. */
17
+ current?: boolean;
18
+ /** A separator line above this row. */
19
+ gap?: boolean;
20
+ }
21
+ </script>
22
+
23
+ <script lang="ts">
24
+ import { Check } from '@lucide/svelte';
25
+ import { portal } from '../dom';
26
+
27
+ let {
28
+ rows,
29
+ index,
30
+ x,
31
+ y,
32
+ up,
33
+ turn = false,
34
+ caption,
35
+ label,
36
+ onpick,
37
+ onhover
38
+ }: {
39
+ rows: MenuRow[];
40
+ index: number;
41
+ x: number;
42
+ y: number;
43
+ /** Opens upwards from `y` (no room below). */
44
+ up: boolean;
45
+ /** The turn-into menu: a little narrower and taller. */
46
+ turn?: boolean;
47
+ caption?: string;
48
+ label: string;
49
+ onpick: (n: number) => void;
50
+ onhover: (n: number) => void;
51
+ } = $props();
52
+
53
+ let menu: HTMLDivElement;
54
+ // Keep the highlighted row visible while moving through the menu with the arrow keys.
55
+ $effect(() => {
56
+ void index;
57
+ menu?.querySelector('.active')?.scrollIntoView({ block: 'nearest' });
58
+ });
59
+ </script>
60
+
61
+ <!-- `menu`, `turn` and `active` are hooks (closing on an outside click, scrolling the active row into view), not styles.
62
+ It lives in the body (portal), so it keeps the editor's type size and colour itself. -->
63
+ <div
64
+ bind:this={menu}
65
+ use:portal
66
+ class="menu fixed z-30 overflow-auto text-[17px] leading-[1.65] text-surface-contrast rounded-xl border border-frame bg-surface p-1.5 shadow-[0_10px_36px_rgb(0_0_0/0.2)] {turn
67
+ ? 'turn max-h-[340px] w-60'
68
+ : 'max-h-[300px] w-[260px]'} {up ? '-translate-y-full' : ''}"
69
+ style:left="{x}px"
70
+ style:top="{y}px"
71
+ role="listbox"
72
+ aria-label={label}
73
+ >
74
+ {#if caption}
75
+ <div class="px-2 pt-1 pb-1.5 font-[system-ui] text-[11px] leading-[normal] font-semibold tracking-[0.06em] text-muted-contrast uppercase">{caption}</div>
76
+ {/if}
77
+ {#each rows as row, n (row.key)}
78
+ {#if row.gap}<div class="mx-1 my-1.5 h-px bg-frame"></div>{/if}
79
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
80
+ <div
81
+ class="item flex cursor-pointer items-center gap-2 rounded-md px-1.5 py-[3px] {n === index ? 'active bg-accent/14' : ''} {row.danger ? 'text-[#dc2626]' : ''}"
82
+ role="option"
83
+ tabindex="-1"
84
+ aria-selected={n === index}
85
+ onmousedown={(e) => {
86
+ e.preventDefault();
87
+ onpick(n);
88
+ }}
89
+ onmouseenter={() => onhover(n)}
90
+ >
91
+ <span class="grid size-6 flex-none place-items-center rounded-md border border-frame {row.danger ? 'text-[#dc2626]' : 'text-surface-contrast'}"
92
+ ><row.icon size={14} /></span
93
+ >
94
+ <span class="flex min-w-0 items-baseline gap-1.5 overflow-hidden whitespace-nowrap"
95
+ ><b class="font-[system-ui] text-[13px] leading-[normal] font-medium">{row.label}</b><small
96
+ class="overflow-hidden text-[11px] text-ellipsis {row.warn ? 'text-[#b45309]' : 'text-muted-contrast'}">{row.hint}</small
97
+ ></span
98
+ >
99
+ {#if row.current}<span class="ml-auto flex text-accent"><Check size={16} /></span>{/if}
100
+ </div>
101
+ {/each}
102
+ </div>
@@ -0,0 +1,32 @@
1
+ import type { Icon } from '../palette';
2
+ export interface MenuRow {
3
+ key: string;
4
+ icon: Icon;
5
+ label: string;
6
+ hint: string;
7
+ /** The hint as a warning (e.g. "Loses formatting"). */
8
+ warn?: boolean;
9
+ /** A destructive row (delete), in red. */
10
+ danger?: boolean;
11
+ /** The current type of the selection: a check mark. */
12
+ current?: boolean;
13
+ /** A separator line above this row. */
14
+ gap?: boolean;
15
+ }
16
+ type $$ComponentProps = {
17
+ rows: MenuRow[];
18
+ index: number;
19
+ x: number;
20
+ y: number;
21
+ /** Opens upwards from `y` (no room below). */
22
+ up: boolean;
23
+ /** The turn-into menu: a little narrower and taller. */
24
+ turn?: boolean;
25
+ caption?: string;
26
+ label: string;
27
+ onpick: (n: number) => void;
28
+ onhover: (n: number) => void;
29
+ };
30
+ declare const BlockMenu: import("svelte").Component<$$ComponentProps, {}, "">;
31
+ type BlockMenu = ReturnType<typeof BlockMenu>;
32
+ export default BlockMenu;