@atom-forge/brickee 0.0.1 → 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 +59 -1
- package/README-AI.md +34 -28
- package/dist/Editor.svelte +713 -867
- package/dist/Editor.svelte.d.ts +0 -3
- package/dist/LinkPrompt.svelte +1 -14
- package/dist/Preview.svelte +27 -58
- package/dist/Preview.svelte.d.ts +2 -7
- package/dist/PropsBar.svelte +75 -249
- package/dist/PropsBar.svelte.d.ts +0 -3
- package/dist/code.d.ts +12 -0
- package/dist/code.js +15 -0
- 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 +3 -2
- package/dist/index.js +4 -3
- package/dist/kit/Empty.svelte +4 -15
- package/dist/kit/Frame.svelte +7 -37
- package/dist/locales/hu.js +6 -8
- package/dist/markdown.d.ts +20 -6
- 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 -282
- 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 +15 -2
- package/dist/schema.js +1 -1
- package/dist/types.d.ts +27 -10
- 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 +24 -19
- package/docs/en/editor-and-preview.md +7 -10
- package/docs/en/getting-started.md +14 -2
- package/docs/en/host-contexts.md +26 -132
- package/docs/en/index.md +5 -5
- package/docs/en/recipes-and-pitfalls.md +12 -11
- package/docs/en/server-side.md +0 -17
- package/docs/en/upgrading.md +34 -4
- package/docs/en/writing-plugins.md +147 -21
- package/docs/hu/bundled-plugins.md +1 -1
- package/docs/hu/documents-and-markdown.md +24 -19
- package/docs/hu/editor-and-preview.md +7 -10
- package/docs/hu/getting-started.md +14 -2
- package/docs/hu/host-contexts.md +26 -132
- package/docs/hu/index.md +5 -5
- package/docs/hu/recipes-and-pitfalls.md +12 -11
- package/docs/hu/server-side.md +0 -17
- package/docs/hu/upgrading.md +34 -4
- package/docs/hu/writing-plugins.md +147 -21
- package/package.json +14 -2
- package/dist/ImagePicker.svelte +0 -131
- package/dist/ImagePicker.svelte.d.ts +0 -13
- package/dist/media.d.ts +0 -17
- package/dist/media.js +0 -12
package/dist/Editor.svelte.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type MediaItem } from './media';
|
|
2
1
|
import { type Messages } from './i18n';
|
|
3
2
|
import { type BlockPlugin, type StoredBlock } from './types';
|
|
4
3
|
interface Props {
|
|
@@ -12,8 +11,6 @@ interface Props {
|
|
|
12
11
|
plugins?: BlockPlugin[];
|
|
13
12
|
/** Set by plugins with nested blocks: a compact editor inside another block. */
|
|
14
13
|
nested?: boolean;
|
|
15
|
-
/** Images the blocks can pick from (the page's attachments); nested editors share their parent's. */
|
|
16
|
-
media?: MediaItem[];
|
|
17
14
|
/** UI text in the host's language (English text -> translation); nested editors share their parent's. */
|
|
18
15
|
messages?: Messages;
|
|
19
16
|
}
|
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: #fff;
|
|
38
|
-
border: 1px solid #e5e5ea;
|
|
39
|
-
border-radius: 12px;
|
|
40
|
-
box-shadow: 0 10px 36px rgba(0, 0, 0, 0.2);
|
|
41
|
-
}
|
|
42
|
-
</style>
|
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
|
|
|
@@ -49,35 +50,24 @@
|
|
|
49
50
|
<script lang="ts">
|
|
50
51
|
import { useText } from './i18n';
|
|
51
52
|
import { inline } from './markdown';
|
|
52
|
-
import { provideMedia, type MediaItem } from './media';
|
|
53
53
|
import { provideText, type Messages } from './i18n';
|
|
54
54
|
import type { BlockPlugin } from './types';
|
|
55
55
|
|
|
56
56
|
interface Props {
|
|
57
57
|
blocks: StoredBlock[];
|
|
58
58
|
plugins: BlockPlugin[];
|
|
59
|
-
/** Set by plugins rendering their nested blocks
|
|
59
|
+
/** Set by plugins rendering their nested blocks (a container's preview). */
|
|
60
60
|
nested?: boolean;
|
|
61
|
-
/** The images blocks refer to by id; nested previews share their parent's. */
|
|
62
|
-
media?: MediaItem[];
|
|
63
61
|
/** UI text in the host's language; nested previews share their parent's. */
|
|
64
62
|
messages?: Messages;
|
|
65
|
-
/** Mark the blocks after a gate (the supporters-only divider) with a label and a dashed line; default: yes. */
|
|
66
|
-
markGated?: boolean;
|
|
67
63
|
}
|
|
68
|
-
let { blocks, plugins, nested = false,
|
|
64
|
+
let { blocks, plugins, nested = false, messages }: Props = $props();
|
|
69
65
|
|
|
70
|
-
provideMedia(() => media);
|
|
71
66
|
provideText(() => messages);
|
|
72
67
|
const tr = useText();
|
|
73
68
|
|
|
74
69
|
const pluginOf = (type: string) => plugins.find((p) => p.type === type);
|
|
75
70
|
|
|
76
|
-
// Everything after a unique supporters-only divider is for supporters: shown here, marked as such.
|
|
77
|
-
const lockAt = $derived(nested || !markGated ? -1 : blocks.findIndex((b) => pluginOf(b.type)?.gate));
|
|
78
|
-
const open = $derived(lockAt < 0 ? blocks : blocks.slice(0, lockAt + 1));
|
|
79
|
-
const locked = $derived(lockAt < 0 ? [] : blocks.slice(lockAt + 1));
|
|
80
|
-
|
|
81
71
|
// Inline markdown -> html: text is escaped first, links are http(s), mailto, tel or site-relative only (see inline()).
|
|
82
72
|
const html = (b: StoredBlock) => inline(b.text ?? '');
|
|
83
73
|
const indent = (b: StoredBlock) => (b.indent ? `${b.indent * 1.5}em` : undefined);
|
|
@@ -85,7 +75,8 @@
|
|
|
85
75
|
|
|
86
76
|
{#snippet list(node: ListNode)}
|
|
87
77
|
{#if node.type === 'md:ol'}
|
|
88
|
-
|
|
78
|
+
{@const start = olStart(node.items[0]?.block.props)}
|
|
79
|
+
<ol start={start === 1 ? undefined : start}>{@render items(node)}</ol>
|
|
89
80
|
{:else}
|
|
90
81
|
<ul class:todo={node.type === 'md:todo'}>{@render items(node)}</ul>
|
|
91
82
|
{/if}
|
|
@@ -140,47 +131,25 @@
|
|
|
140
131
|
{/snippet}
|
|
141
132
|
|
|
142
133
|
<div class="preview" class:nested>
|
|
143
|
-
{@render run(
|
|
144
|
-
{#if locked.length}
|
|
145
|
-
<div class="supporters" aria-label={tr('For supporters only')}>
|
|
146
|
-
<p class="supporters-label">{tr('For supporters only')}</p>
|
|
147
|
-
{@render run(locked)}
|
|
148
|
-
</div>
|
|
149
|
-
{/if}
|
|
134
|
+
{@render run(blocks)}
|
|
150
135
|
</div>
|
|
151
136
|
|
|
152
|
-
<style
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
.preview :global(img) {
|
|
173
|
-
max-width: 100%;
|
|
174
|
-
}
|
|
175
|
-
/* The content starts at its top edge, whatever its first block is (pages often align it with a sidebar). */
|
|
176
|
-
.preview:not(.nested) > :global(:first-child),
|
|
177
|
-
.preview:not(.nested) > :global(:first-child > :first-child) {
|
|
178
|
-
margin-top: 0;
|
|
179
|
-
}
|
|
180
|
-
/* Floating images and side boxes stay inside the content. */
|
|
181
|
-
.preview:not(.nested)::after {
|
|
182
|
-
content: '';
|
|
183
|
-
display: block;
|
|
184
|
-
clear: both;
|
|
185
|
-
}
|
|
186
|
-
</style>
|
|
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;
|
|
@@ -15,20 +15,15 @@ export type Part = {
|
|
|
15
15
|
lists: ListNode[];
|
|
16
16
|
};
|
|
17
17
|
export declare function parts(blocks: StoredBlock[]): Part[];
|
|
18
|
-
import { type MediaItem } from './media';
|
|
19
18
|
import { type Messages } from './i18n';
|
|
20
19
|
import type { BlockPlugin } from './types';
|
|
21
20
|
interface Props {
|
|
22
21
|
blocks: StoredBlock[];
|
|
23
22
|
plugins: BlockPlugin[];
|
|
24
|
-
/** Set by plugins rendering their nested blocks
|
|
23
|
+
/** Set by plugins rendering their nested blocks (a container's preview). */
|
|
25
24
|
nested?: boolean;
|
|
26
|
-
/** The images blocks refer to by id; nested previews share their parent's. */
|
|
27
|
-
media?: MediaItem[];
|
|
28
25
|
/** UI text in the host's language; nested previews share their parent's. */
|
|
29
26
|
messages?: Messages;
|
|
30
|
-
/** Mark the blocks after a gate (the supporters-only divider) with a label and a dashed line; default: yes. */
|
|
31
|
-
markGated?: boolean;
|
|
32
27
|
}
|
|
33
28
|
declare const Preview: import("svelte").Component<Props, {}, "">;
|
|
34
29
|
type Preview = ReturnType<typeof Preview>;
|
package/dist/PropsBar.svelte
CHANGED
|
@@ -1,31 +1,27 @@
|
|
|
1
|
-
<!-- The fields of a plugin block, on the top of its box. Closed: only a small header (the block's
|
|
2
|
-
opens the fields in place: a wrapping row (label and control side by side), text areas on their
|
|
3
|
-
|
|
1
|
+
<!-- The fields of a plugin block (or a code block), on the top of its box. Closed: only a small header (the block's
|
|
2
|
+
tag); a click on it opens the fields in place: a wrapping row (label and control side by side), text areas on their
|
|
3
|
+
own full-width rows. -->
|
|
4
4
|
<script lang="ts">
|
|
5
|
-
import { ColorPicker, Input, NativeSelect, Slider, Textarea
|
|
5
|
+
import { ColorPicker, Input, NativeSelect, Slider, Textarea } from '@atom-forge/ui';
|
|
6
6
|
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
|
7
7
|
import { slide } from 'svelte/transition';
|
|
8
|
-
import ImagePicker from './ImagePicker.svelte';
|
|
9
8
|
import { useView } from './view';
|
|
10
9
|
import type { Translate } from './i18n';
|
|
11
|
-
import type { MediaItem } from './media';
|
|
12
10
|
import type { Block, PropField } from './types';
|
|
13
11
|
|
|
14
12
|
interface Props {
|
|
15
13
|
block: Block;
|
|
16
14
|
tag: string;
|
|
17
15
|
fields: PropField[];
|
|
18
|
-
media: MediaItem[];
|
|
19
16
|
tr: Translate;
|
|
20
17
|
/** Sets one property from its field's text and returns the text as it is now kept. */
|
|
21
18
|
setProp: (b: Block, f: PropField, raw: string) => string;
|
|
22
19
|
/** Sets one property to a value as it is ('' or undefined removes it). */
|
|
23
20
|
setValue: (b: Block, key: string, v: unknown) => void;
|
|
24
|
-
pickMedia: (b: Block, f: PropField, id: string) => void;
|
|
25
21
|
/** Start with the fields open (a block just made). */
|
|
26
22
|
open?: boolean;
|
|
27
23
|
}
|
|
28
|
-
let { block, tag, fields,
|
|
24
|
+
let { block, tag, fields, tr, setProp, setValue, open: startOpen = false }: Props = $props();
|
|
29
25
|
// svelte-ignore state_referenced_locally
|
|
30
26
|
let open = $state(startOpen);
|
|
31
27
|
|
|
@@ -33,10 +29,18 @@
|
|
|
33
29
|
const v = block.props?.[f.key];
|
|
34
30
|
return f.format ? f.format(v) : v == null ? '' : String(v);
|
|
35
31
|
};
|
|
36
|
-
|
|
32
|
+
/** A text field's placeholder: its label, and its own placeholder (an example) after it. */
|
|
33
|
+
const hint = (f: PropField) => [tr(f.label), f.placeholder && tr(f.placeholder)].filter(Boolean).join(' · ');
|
|
34
|
+
/** Toggles: the values switched on, kept in the order of the options. */
|
|
35
|
+
const toggled = (f: PropField): string[] => {
|
|
37
36
|
const v = block.props?.[f.key];
|
|
38
|
-
return Array.isArray(v) ? v
|
|
37
|
+
return Array.isArray(v) ? v.map(String) : [];
|
|
39
38
|
};
|
|
39
|
+
function toggle(f: PropField, value: string) {
|
|
40
|
+
const on = new Set(toggled(f));
|
|
41
|
+
if (!on.delete(value)) on.add(value);
|
|
42
|
+
setValue(block, f.key, (f.options ?? []).map((o) => o.value).filter((x) => on.has(x)));
|
|
43
|
+
}
|
|
40
44
|
const optionLabel = (f: PropField, i: number) => {
|
|
41
45
|
const opt = f.options?.[i];
|
|
42
46
|
return opt ? tr(opt.label, { n: opt.value }) : '';
|
|
@@ -107,57 +111,68 @@
|
|
|
107
111
|
|
|
108
112
|
const short = $derived(fields.filter((f) => f.kind !== 'textarea'));
|
|
109
113
|
const long = $derived(fields.filter((f) => f.kind === 'textarea'));
|
|
110
|
-
|
|
111
|
-
const popups = getPopupManager();
|
|
112
|
-
const urlOf = (id: string) => media.find((m) => m.id === id);
|
|
113
|
-
function openPicker(e: MouseEvent, f: PropField) {
|
|
114
|
-
popups.open.component(
|
|
115
|
-
ImagePicker,
|
|
116
|
-
{ media, multiple: f.kind === 'images', getPicked: () => picked(f), pick: (id: string) => pickMedia(block, f, id), tr },
|
|
117
|
-
{ anchor: e, align: 'left' }
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
114
|
</script>
|
|
121
115
|
|
|
122
|
-
<div
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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' : ''}" />
|
|
126
125
|
</button>
|
|
127
126
|
{#if open}
|
|
128
|
-
<div
|
|
129
|
-
|
|
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]">
|
|
130
131
|
{#each short as f (f.key)}
|
|
131
|
-
{#if f.kind === '
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
<span class="
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
{@const m = urlOf(id)}
|
|
138
|
-
{#if m}<img src={m.url} alt={m.name} />{:else}<span class="missing" title={id}>?</span>{/if}
|
|
139
|
-
{#if f.kind === 'images' && n === 5 && ids.length > 6}<span class="more">+{ids.length - 6}</span>{/if}
|
|
140
|
-
{:else}
|
|
141
|
-
<span class="empty">{tr('Choose…')}</span>
|
|
142
|
-
{/each}
|
|
143
|
-
</button>
|
|
144
|
-
</div>
|
|
132
|
+
{#if f.kind === 'text'}
|
|
133
|
+
<!-- the field shows the value as kept (a YouTube link becomes its id); its label is the placeholder -->
|
|
134
|
+
<label class="grid min-w-0 content-start" title={tr(f.label)}>
|
|
135
|
+
<span class="sr-only">{tr(f.label)}</span>
|
|
136
|
+
<Input small monospace placeholder={hint(f)} bind:value={() => text(f), (v: string) => setProp(block, f, v)} />
|
|
137
|
+
</label>
|
|
145
138
|
{:else}
|
|
146
|
-
<!-- not a label around buttons (a slider's view buttons, a colour's clear): a click on a label goes to its first button -->
|
|
147
|
-
<svelte:element this={f.kind === '
|
|
148
|
-
<span class="
|
|
149
|
-
>{tr(f.label)}{#if viewLabel(f)}
|
|
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 -->
|
|
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
|
|
150
146
|
>
|
|
151
147
|
{#if f.kind === 'select'}
|
|
152
148
|
<NativeSelect
|
|
149
|
+
small
|
|
153
150
|
options={(f.options ?? []).map((opt) => ({ value: opt.value, label: tr(opt.label, { n: opt.value }) }))}
|
|
154
151
|
bind:value={() => text(f), (v: string) => setProp(block, f, v)}
|
|
155
152
|
/>
|
|
153
|
+
{:else if f.kind === 'toggles'}
|
|
154
|
+
{@const on = toggled(f)}
|
|
155
|
+
<span class="inline-flex gap-px justify-self-start rounded-[7px] bg-muted p-0.5">
|
|
156
|
+
{#each f.options ?? [] as opt (opt.value)}
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
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'}"
|
|
162
|
+
aria-pressed={on.includes(opt.value)}
|
|
163
|
+
title={tr(opt.label)}
|
|
164
|
+
aria-label={tr(opt.label)}
|
|
165
|
+
onmousedown={(e) => e.preventDefault()}
|
|
166
|
+
onclick={() => toggle(f, opt.value)}
|
|
167
|
+
>{#if opt.icon}<opt.icon size={15} />{:else}{tr(opt.label)}{/if}</button
|
|
168
|
+
>
|
|
169
|
+
{/each}
|
|
170
|
+
</span>
|
|
156
171
|
{:else if f.kind === 'slider'}
|
|
157
|
-
<span class="
|
|
158
|
-
<span class="
|
|
172
|
+
<span class="flex min-w-0 items-center gap-2">
|
|
173
|
+
<span class="block min-w-20 flex-1">
|
|
159
174
|
<Slider
|
|
160
|
-
|
|
175
|
+
small
|
|
161
176
|
drawStops={!free(f) && (f.options?.length ?? 0) <= 10}
|
|
162
177
|
min={free(f) ? low(f) : 0}
|
|
163
178
|
max={free(f) ? f.max : (f.options?.length ?? 1) - 1}
|
|
@@ -165,35 +180,34 @@
|
|
|
165
180
|
bind:value={() => pos(f), (v: number) => setPos(f, v)}
|
|
166
181
|
/>
|
|
167
182
|
</span>
|
|
168
|
-
<span class="
|
|
183
|
+
<span class="min-w-[4.5em] whitespace-nowrap text-surface-contrast tabular-nums">{posLabel(f)}</span>
|
|
169
184
|
</span>
|
|
170
185
|
{:else if f.kind === 'color'}
|
|
171
186
|
<!-- with `none`: an unset colour shows as "none" (a click sets one); a set one can be cleared -->
|
|
172
|
-
<span class="
|
|
187
|
+
<span class="flex min-w-0 items-center gap-2">
|
|
173
188
|
{#if f.none && !text(f)}
|
|
174
|
-
<button type="button" class="
|
|
175
|
-
|
|
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)}
|
|
176
192
|
</button>
|
|
177
193
|
{:else}
|
|
178
|
-
<ColorPicker bind:value={() => text(f) || '#000000', (v: string) => setProp(block, f, v)} />
|
|
194
|
+
<ColorPicker small bind:value={() => text(f) || '#000000', (v: string) => setProp(block, f, v)} />
|
|
179
195
|
{#if f.none}
|
|
180
|
-
<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>
|
|
181
197
|
{/if}
|
|
182
198
|
{/if}
|
|
183
199
|
</span>
|
|
184
|
-
{:else}
|
|
185
|
-
<!-- the field shows the value as kept (a YouTube link becomes its id) -->
|
|
186
|
-
<Input monospace placeholder={f.placeholder && tr(f.placeholder)} bind:value={() => text(f), (v: string) => setProp(block, f, v)} />
|
|
187
200
|
{/if}
|
|
188
201
|
</svelte:element>
|
|
189
202
|
{/if}
|
|
190
203
|
{/each}
|
|
191
204
|
{#each long as f (f.key)}
|
|
192
|
-
<label class="
|
|
193
|
-
<span class="
|
|
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>
|
|
194
207
|
<Textarea
|
|
195
|
-
|
|
196
|
-
|
|
208
|
+
small
|
|
209
|
+
rows={2}
|
|
210
|
+
placeholder={hint(f)}
|
|
197
211
|
bind:value={() => (editing?.key === f.key ? editing.text : text(f)), (v: string) => {
|
|
198
212
|
if (editing?.key === f.key) editing.text = v;
|
|
199
213
|
setProp(block, f, v);
|
|
@@ -205,191 +219,3 @@
|
|
|
205
219
|
</div>
|
|
206
220
|
{/if}
|
|
207
221
|
</div>
|
|
208
|
-
|
|
209
|
-
<style>
|
|
210
|
-
.bar {
|
|
211
|
-
display: grid;
|
|
212
|
-
justify-items: start;
|
|
213
|
-
gap: 6px;
|
|
214
|
-
margin-bottom: 4px;
|
|
215
|
-
border-radius: 8px;
|
|
216
|
-
font: 12px/1.4 system-ui, sans-serif;
|
|
217
|
-
color: #1d1d1f;
|
|
218
|
-
}
|
|
219
|
-
.bar.open {
|
|
220
|
-
justify-items: stretch;
|
|
221
|
-
margin-bottom: 8px;
|
|
222
|
-
padding: 8px 12px 12px;
|
|
223
|
-
background: rgba(120, 120, 140, 0.07);
|
|
224
|
-
}
|
|
225
|
-
.bar {
|
|
226
|
-
transition:
|
|
227
|
-
padding 0.18s,
|
|
228
|
-
background 0.18s;
|
|
229
|
-
}
|
|
230
|
-
/* the fields in 1–3 columns (as wide as the box allows), each with its label above */
|
|
231
|
-
.grid {
|
|
232
|
-
display: grid;
|
|
233
|
-
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
234
|
-
gap: 12px 16px;
|
|
235
|
-
padding-top: 8px;
|
|
236
|
-
}
|
|
237
|
-
.head {
|
|
238
|
-
display: flex;
|
|
239
|
-
align-items: center;
|
|
240
|
-
gap: 2px;
|
|
241
|
-
padding: 0;
|
|
242
|
-
border: 0;
|
|
243
|
-
background: none;
|
|
244
|
-
color: #8e8e96;
|
|
245
|
-
cursor: pointer;
|
|
246
|
-
justify-self: start;
|
|
247
|
-
}
|
|
248
|
-
.head:hover .tag {
|
|
249
|
-
background: rgba(120, 120, 140, 0.2);
|
|
250
|
-
color: #1d1d1f;
|
|
251
|
-
}
|
|
252
|
-
.head :global(.chev) {
|
|
253
|
-
transition: rotate 0.15s;
|
|
254
|
-
}
|
|
255
|
-
.open .head :global(.chev) {
|
|
256
|
-
rotate: 180deg;
|
|
257
|
-
}
|
|
258
|
-
.tag {
|
|
259
|
-
padding: 1px 6px;
|
|
260
|
-
border-radius: 5px;
|
|
261
|
-
background: rgba(120, 120, 140, 0.12);
|
|
262
|
-
color: #6e6e76;
|
|
263
|
-
font: 500 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
264
|
-
white-space: nowrap;
|
|
265
|
-
user-select: none;
|
|
266
|
-
}
|
|
267
|
-
.field {
|
|
268
|
-
display: grid;
|
|
269
|
-
align-content: start;
|
|
270
|
-
gap: 4px;
|
|
271
|
-
min-width: 0;
|
|
272
|
-
}
|
|
273
|
-
/* text areas take a whole row */
|
|
274
|
-
.field.long {
|
|
275
|
-
grid-column: 1 / -1;
|
|
276
|
-
}
|
|
277
|
-
.line {
|
|
278
|
-
display: flex;
|
|
279
|
-
align-items: center;
|
|
280
|
-
gap: 8px;
|
|
281
|
-
min-width: 0;
|
|
282
|
-
}
|
|
283
|
-
.name {
|
|
284
|
-
font-size: 11.5px;
|
|
285
|
-
font-weight: 600;
|
|
286
|
-
color: #8e8e96;
|
|
287
|
-
white-space: nowrap;
|
|
288
|
-
user-select: none;
|
|
289
|
-
}
|
|
290
|
-
/* the view a per-view field edits (the host's current one); dashed while it only takes a wider view's value */
|
|
291
|
-
.view {
|
|
292
|
-
margin-left: 6px;
|
|
293
|
-
padding: 0 5px;
|
|
294
|
-
border: 1px dashed #c8c8d0;
|
|
295
|
-
border-radius: 4px;
|
|
296
|
-
color: #6e6e76;
|
|
297
|
-
font-weight: 500;
|
|
298
|
-
}
|
|
299
|
-
.view.own {
|
|
300
|
-
border-style: solid;
|
|
301
|
-
border-color: #1d1d1f;
|
|
302
|
-
color: #1d1d1f;
|
|
303
|
-
}
|
|
304
|
-
.slider {
|
|
305
|
-
display: block;
|
|
306
|
-
flex: 1;
|
|
307
|
-
min-width: 80px;
|
|
308
|
-
}
|
|
309
|
-
.value {
|
|
310
|
-
min-width: 4.5em;
|
|
311
|
-
color: #3a3a40;
|
|
312
|
-
font-variant-numeric: tabular-nums;
|
|
313
|
-
white-space: nowrap;
|
|
314
|
-
}
|
|
315
|
-
.nocolor {
|
|
316
|
-
display: flex;
|
|
317
|
-
align-items: center;
|
|
318
|
-
gap: 6px;
|
|
319
|
-
height: 30px;
|
|
320
|
-
padding: 0 10px 0 6px;
|
|
321
|
-
border: 1px dashed #c8c8d0;
|
|
322
|
-
border-radius: 7px;
|
|
323
|
-
background: #fff;
|
|
324
|
-
color: #8e8e96;
|
|
325
|
-
cursor: pointer;
|
|
326
|
-
}
|
|
327
|
-
.nocolor:hover {
|
|
328
|
-
border-color: var(--color-accent, #ea580c);
|
|
329
|
-
color: #3a3a40;
|
|
330
|
-
}
|
|
331
|
-
/* an empty swatch, struck through */
|
|
332
|
-
.swatch {
|
|
333
|
-
width: 16px;
|
|
334
|
-
height: 16px;
|
|
335
|
-
border: 1px solid #c8c8d0;
|
|
336
|
-
border-radius: 4px;
|
|
337
|
-
background: linear-gradient(to top right, transparent calc(50% - 1px), #e5484d calc(50% - 1px), #e5484d calc(50% + 1px), transparent calc(50% + 1px));
|
|
338
|
-
}
|
|
339
|
-
.clear {
|
|
340
|
-
width: 22px;
|
|
341
|
-
height: 22px;
|
|
342
|
-
padding: 0;
|
|
343
|
-
border: 0;
|
|
344
|
-
border-radius: 6px;
|
|
345
|
-
background: none;
|
|
346
|
-
color: #8e8e96;
|
|
347
|
-
font-size: 16px;
|
|
348
|
-
line-height: 1;
|
|
349
|
-
cursor: pointer;
|
|
350
|
-
}
|
|
351
|
-
.clear:hover {
|
|
352
|
-
background: rgba(120, 120, 140, 0.14);
|
|
353
|
-
color: #1d1d1f;
|
|
354
|
-
}
|
|
355
|
-
.chosen {
|
|
356
|
-
display: flex;
|
|
357
|
-
align-items: center;
|
|
358
|
-
gap: 3px;
|
|
359
|
-
min-height: 30px;
|
|
360
|
-
padding: 2px;
|
|
361
|
-
border: 1px solid #dcdce2;
|
|
362
|
-
border-radius: 7px;
|
|
363
|
-
background: #fff;
|
|
364
|
-
cursor: pointer;
|
|
365
|
-
}
|
|
366
|
-
.chosen:hover {
|
|
367
|
-
border-color: var(--color-accent, #ea580c);
|
|
368
|
-
}
|
|
369
|
-
/* images after the sixth are a "+n" */
|
|
370
|
-
.chosen > img:nth-of-type(n + 7) {
|
|
371
|
-
display: none;
|
|
372
|
-
}
|
|
373
|
-
.chosen img,
|
|
374
|
-
.chosen .missing {
|
|
375
|
-
width: 40px;
|
|
376
|
-
height: 24px;
|
|
377
|
-
object-fit: cover;
|
|
378
|
-
border-radius: 4px;
|
|
379
|
-
}
|
|
380
|
-
.chosen .missing {
|
|
381
|
-
display: grid;
|
|
382
|
-
place-items: center;
|
|
383
|
-
background: #eee;
|
|
384
|
-
color: #8e8e96;
|
|
385
|
-
}
|
|
386
|
-
.chosen .more {
|
|
387
|
-
padding: 0 4px;
|
|
388
|
-
color: #6e6e76;
|
|
389
|
-
font-size: 11px;
|
|
390
|
-
}
|
|
391
|
-
.chosen .empty {
|
|
392
|
-
padding: 0 8px;
|
|
393
|
-
color: #8e8e96;
|
|
394
|
-
}
|
|
395
|
-
</style>
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import type { Translate } from './i18n';
|
|
2
|
-
import type { MediaItem } from './media';
|
|
3
2
|
import type { Block, PropField } from './types';
|
|
4
3
|
interface Props {
|
|
5
4
|
block: Block;
|
|
6
5
|
tag: string;
|
|
7
6
|
fields: PropField[];
|
|
8
|
-
media: MediaItem[];
|
|
9
7
|
tr: Translate;
|
|
10
8
|
/** Sets one property from its field's text and returns the text as it is now kept. */
|
|
11
9
|
setProp: (b: Block, f: PropField, raw: string) => string;
|
|
12
10
|
/** Sets one property to a value as it is ('' or undefined removes it). */
|
|
13
11
|
setValue: (b: Block, key: string, v: unknown) => void;
|
|
14
|
-
pickMedia: (b: Block, f: PropField, id: string) => void;
|
|
15
12
|
/** Start with the fields open (a block just made). */
|
|
16
13
|
open?: boolean;
|
|
17
14
|
}
|
package/dist/code.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A language: its value is stored in the block (`props.lang`, the opening fence's info string). */
|
|
2
|
+
export interface CodeLanguage {
|
|
3
|
+
value: string;
|
|
4
|
+
/** Shown in the dropdown; defaults to the value. */
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
type Get = () => CodeLanguage[];
|
|
8
|
+
/** Call in a component's init (around the editor). Without its own list, a nested editor keeps its parent's. */
|
|
9
|
+
export declare function provideCodeLanguages(own: () => (string | CodeLanguage)[] | undefined): void;
|
|
10
|
+
/** Call in a component's init; the returned function reads the current list (empty: free text). */
|
|
11
|
+
export declare const useCodeLanguages: () => Get;
|
|
12
|
+
export {};
|
package/dist/code.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The languages a code block offers. Without a list (the default) its language is free text; with one, the field is a
|
|
2
|
+
// dropdown of these. Which languages make sense is the host's business (what its site can highlight).
|
|
3
|
+
import { getContext, setContext } from 'svelte';
|
|
4
|
+
const KEY = 'brickee:code-languages';
|
|
5
|
+
const normalize = (list) => list.map((l) => (typeof l === 'string' ? { value: l } : l));
|
|
6
|
+
/** Call in a component's init (around the editor). Without its own list, a nested editor keeps its parent's. */
|
|
7
|
+
export function provideCodeLanguages(own) {
|
|
8
|
+
const parent = getContext(KEY);
|
|
9
|
+
setContext(KEY, () => {
|
|
10
|
+
const list = own();
|
|
11
|
+
return list ? normalize(list) : (parent?.() ?? []);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** Call in a component's init; the returned function reads the current list (empty: free text). */
|
|
15
|
+
export const useCodeLanguages = () => getContext(KEY) ?? (() => []);
|
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;
|