@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.
- package/CHANGELOG.md +40 -0
- package/README-AI.md +10 -7
- package/dist/Editor.svelte +691 -833
- package/dist/LinkPrompt.svelte +1 -14
- package/dist/Preview.svelte +27 -54
- package/dist/Preview.svelte.d.ts +2 -4
- package/dist/PropsBar.svelte +35 -219
- package/dist/code.js +1 -1
- package/dist/dom.d.ts +8 -0
- package/dist/dom.js +9 -0
- package/dist/editor/BlockMenu.svelte +102 -0
- package/dist/editor/BlockMenu.svelte.d.ts +32 -0
- package/dist/editor/FormatBar.svelte +41 -0
- package/dist/editor/FormatBar.svelte.d.ts +16 -0
- package/dist/editor/drag.svelte.d.ts +63 -0
- package/dist/editor/drag.svelte.js +95 -0
- package/dist/editor/history.d.ts +24 -0
- package/dist/editor/history.js +86 -0
- package/dist/editor/sweep.svelte.d.ts +21 -0
- package/dist/editor/sweep.svelte.js +56 -0
- package/dist/html.d.ts +12 -0
- package/dist/html.js +206 -0
- package/dist/i18n.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/kit/Empty.svelte +4 -15
- package/dist/kit/Frame.svelte +7 -37
- package/dist/locales/hu.js +4 -1
- package/dist/markdown.d.ts +20 -4
- package/dist/markdown.js +380 -85
- package/dist/model.js +2 -3
- package/dist/plugins/Gap.svelte +5 -8
- package/dist/plugins/GapPreview.svelte +4 -6
- package/dist/plugins/Map.svelte +2 -17
- package/dist/plugins/MapPreview.svelte +8 -10
- package/dist/plugins/Table.svelte +233 -256
- package/dist/plugins/TablePreview.svelte +28 -28
- package/dist/plugins/YouTube.svelte +3 -31
- package/dist/plugins/YouTubePreview.svelte +45 -43
- package/dist/plugins/index.js +3 -1
- package/dist/types.d.ts +21 -3
- package/dist/types.js +8 -1
- package/dist/view.js +1 -1
- package/docs/en/bundled-plugins.md +1 -1
- package/docs/en/documents-and-markdown.md +20 -14
- package/docs/en/editor-and-preview.md +6 -7
- package/docs/en/getting-started.md +8 -0
- package/docs/en/index.md +1 -1
- package/docs/en/recipes-and-pitfalls.md +2 -2
- package/docs/en/server-side.md +0 -17
- package/docs/en/upgrading.md +14 -0
- package/docs/en/writing-plugins.md +36 -10
- package/docs/hu/bundled-plugins.md +1 -1
- package/docs/hu/documents-and-markdown.md +20 -14
- package/docs/hu/editor-and-preview.md +6 -7
- package/docs/hu/getting-started.md +8 -0
- package/docs/hu/index.md +1 -1
- package/docs/hu/recipes-and-pitfalls.md +2 -2
- package/docs/hu/server-side.md +0 -17
- package/docs/hu/upgrading.md +14 -0
- package/docs/hu/writing-plugins.md +36 -10
- package/package.json +9 -1
package/dist/LinkPrompt.svelte
CHANGED
|
@@ -22,21 +22,8 @@
|
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
24
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
25
|
-
<div class="
|
|
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>
|
package/dist/Preview.svelte
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
<!-- Styles travel with the markup
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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>
|
package/dist/Preview.svelte.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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
|
|
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>;
|
package/dist/PropsBar.svelte
CHANGED
|
@@ -113,26 +113,36 @@
|
|
|
113
113
|
const long = $derived(fields.filter((f) => f.kind === 'textarea'));
|
|
114
114
|
</script>
|
|
115
115
|
|
|
116
|
-
<div
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
123
|
-
|
|
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="
|
|
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="
|
|
134
|
-
<span class="
|
|
135
|
-
>{tr(f.label)}{#if viewLabel(f)}
|
|
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="
|
|
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="
|
|
150
|
-
|
|
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="
|
|
162
|
-
<span class="
|
|
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="
|
|
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="
|
|
187
|
+
<span class="flex min-w-0 items-center gap-2">
|
|
177
188
|
{#if f.none && !text(f)}
|
|
178
|
-
<button type="button" class="
|
|
179
|
-
|
|
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="
|
|
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="
|
|
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 = '
|
|
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;
|