@quillmark/svelte 0.0.0
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/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +241 -0
- package/THEMING.md +153 -0
- package/dist/core/address.d.ts +56 -0
- package/dist/core/address.js +95 -0
- package/dist/core/bloom.d.ts +37 -0
- package/dist/core/bloom.js +116 -0
- package/dist/core/codec/anchor.d.ts +19 -0
- package/dist/core/codec/anchor.js +36 -0
- package/dist/core/codec/code.d.ts +12 -0
- package/dist/core/codec/code.js +118 -0
- package/dist/core/codec/decode.d.ts +24 -0
- package/dist/core/codec/decode.js +270 -0
- package/dist/core/codec/encode.d.ts +71 -0
- package/dist/core/codec/encode.js +480 -0
- package/dist/core/codec/field.d.ts +171 -0
- package/dist/core/codec/field.js +493 -0
- package/dist/core/codec/index.d.ts +22 -0
- package/dist/core/codec/index.js +43 -0
- package/dist/core/codec/inputrules.d.ts +7 -0
- package/dist/core/codec/inputrules.js +168 -0
- package/dist/core/codec/islands.d.ts +36 -0
- package/dist/core/codec/islands.js +82 -0
- package/dist/core/codec/keymap.d.ts +13 -0
- package/dist/core/codec/keymap.js +29 -0
- package/dist/core/codec/lists.d.ts +22 -0
- package/dist/core/codec/lists.js +104 -0
- package/dist/core/codec/marks.d.ts +37 -0
- package/dist/core/codec/marks.js +74 -0
- package/dist/core/codec/positions.d.ts +17 -0
- package/dist/core/codec/positions.js +108 -0
- package/dist/core/codec/prose.css +458 -0
- package/dist/core/codec/reconcile.d.ts +26 -0
- package/dist/core/codec/reconcile.js +64 -0
- package/dist/core/codec/schema.d.ts +13 -0
- package/dist/core/codec/schema.js +155 -0
- package/dist/core/codec/slash.d.ts +79 -0
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +50 -0
- package/dist/core/codec/table-view.js +928 -0
- package/dist/core/codec/table.d.ts +85 -0
- package/dist/core/codec/table.js +224 -0
- package/dist/core/errors.d.ts +73 -0
- package/dist/core/errors.js +24 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +19 -0
- package/dist/core/lifecycle.d.ts +7 -0
- package/dist/core/lifecycle.js +21 -0
- package/dist/core/rebind.svelte.d.ts +18 -0
- package/dist/core/rebind.svelte.js +44 -0
- package/dist/core/teardown.d.ts +20 -0
- package/dist/core/teardown.js +37 -0
- package/dist/core/theme.css +276 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -0
- package/dist/preset/index.css +10 -0
- package/dist/preset/recipes.css +188 -0
- package/dist/preset/scale.css +97 -0
- package/dist/preview/Preview.svelte +118 -0
- package/dist/preview/Preview.svelte.d.ts +39 -0
- package/dist/preview/bridge.d.ts +13 -0
- package/dist/preview/bridge.js +88 -0
- package/dist/preview/controller.d.ts +40 -0
- package/dist/preview/controller.js +141 -0
- package/dist/preview/geometry.d.ts +37 -0
- package/dist/preview/geometry.js +45 -0
- package/dist/preview/index.d.ts +5 -0
- package/dist/preview/index.js +14 -0
- package/dist/preview/overlay.d.ts +10 -0
- package/dist/preview/overlay.js +82 -0
- package/dist/preview/paint.d.ts +17 -0
- package/dist/preview/paint.js +262 -0
- package/dist/preview/strings.d.ts +14 -0
- package/dist/preview/strings.js +22 -0
- package/dist/visual/ArrayField.svelte +362 -0
- package/dist/visual/ArrayField.svelte.d.ts +24 -0
- package/dist/visual/BooleanField.svelte +88 -0
- package/dist/visual/BooleanField.svelte.d.ts +20 -0
- package/dist/visual/Card.svelte +849 -0
- package/dist/visual/Card.svelte.d.ts +43 -0
- package/dist/visual/CardControls.svelte +92 -0
- package/dist/visual/CardControls.svelte.d.ts +11 -0
- package/dist/visual/DateField.svelte +203 -0
- package/dist/visual/DateField.svelte.d.ts +28 -0
- package/dist/visual/DiagnosticList.svelte +50 -0
- package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
- package/dist/visual/EnumField.svelte +169 -0
- package/dist/visual/EnumField.svelte.d.ts +25 -0
- package/dist/visual/Field.svelte +270 -0
- package/dist/visual/Field.svelte.d.ts +37 -0
- package/dist/visual/FieldHint.svelte +185 -0
- package/dist/visual/FieldHint.svelte.d.ts +13 -0
- package/dist/visual/FieldLabel.svelte +141 -0
- package/dist/visual/FieldLabel.svelte.d.ts +19 -0
- package/dist/visual/FormatPopover.svelte +396 -0
- package/dist/visual/FormatPopover.svelte.d.ts +9 -0
- package/dist/visual/NumberField.svelte +72 -0
- package/dist/visual/NumberField.svelte.d.ts +19 -0
- package/dist/visual/ObjectField.svelte +131 -0
- package/dist/visual/ObjectField.svelte.d.ts +17 -0
- package/dist/visual/ProseArrayElement.svelte +98 -0
- package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
- package/dist/visual/ProseField.svelte +213 -0
- package/dist/visual/ProseField.svelte.d.ts +50 -0
- package/dist/visual/SlashMenu.svelte +126 -0
- package/dist/visual/SlashMenu.svelte.d.ts +14 -0
- package/dist/visual/TextField.svelte +63 -0
- package/dist/visual/TextField.svelte.d.ts +23 -0
- package/dist/visual/TipsCard.svelte +191 -0
- package/dist/visual/TipsCard.svelte.d.ts +13 -0
- package/dist/visual/VisualEditor.svelte +96 -0
- package/dist/visual/VisualEditor.svelte.d.ts +14 -0
- package/dist/visual/VisualEditorInner.svelte +1032 -0
- package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
- package/dist/visual/controls.css +272 -0
- package/dist/visual/diagnostics.d.ts +52 -0
- package/dist/visual/diagnostics.js +83 -0
- package/dist/visual/domid.d.ts +21 -0
- package/dist/visual/domid.js +36 -0
- package/dist/visual/ext.d.ts +19 -0
- package/dist/visual/ext.js +29 -0
- package/dist/visual/icons/Icon.svelte +39 -0
- package/dist/visual/icons/Icon.svelte.d.ts +12 -0
- package/dist/visual/icons/nodes.d.ts +103 -0
- package/dist/visual/icons/nodes.js +55 -0
- package/dist/visual/index.d.ts +9 -0
- package/dist/visual/index.js +24 -0
- package/dist/visual/motion.d.ts +18 -0
- package/dist/visual/motion.js +53 -0
- package/dist/visual/props.d.ts +77 -0
- package/dist/visual/props.js +1 -0
- package/dist/visual/signals.d.ts +59 -0
- package/dist/visual/signals.js +1 -0
- package/dist/visual/strings.d.ts +123 -0
- package/dist/visual/strings.js +80 -0
- package/dist/visual/structure.d.ts +224 -0
- package/dist/visual/structure.js +270 -0
- package/dist/visual/synced.svelte.d.ts +15 -0
- package/dist/visual/synced.svelte.js +34 -0
- package/dist/visual/tips.d.ts +24 -0
- package/dist/visual/tips.js +50 -0
- package/package.json +102 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Vertical breathing room between stacked pages; cosmetic only, and a rung like
|
|
2
|
+
// any other gap: the stack breathes on the same rhythm the cards do.
|
|
3
|
+
const PAGE_GAP = 'var(--_qm-space-4)';
|
|
4
|
+
export function createPaintLoop(session, container, margin,
|
|
5
|
+
// Notified when a slot's `session.paint` throws: the loop swallows the throw
|
|
6
|
+
// (see `paintSlot`) so one bad page never aborts the band sweep; the caller
|
|
7
|
+
// decides how to surface it.
|
|
8
|
+
onPaintError) {
|
|
9
|
+
// The IntersectionObserver root must be a scrollable ancestor of the slots;
|
|
10
|
+
// respect a consumer's own choice if they already set one (computed style, so
|
|
11
|
+
// a stylesheet rule counts as a choice, not just an inline style).
|
|
12
|
+
const computed = getComputedStyle(container);
|
|
13
|
+
if (computed.overflowY === 'visible')
|
|
14
|
+
container.style.overflowY = 'auto';
|
|
15
|
+
if (computed.position === 'static')
|
|
16
|
+
container.style.position = 'relative';
|
|
17
|
+
const slots = [];
|
|
18
|
+
const pageByEl = new Map();
|
|
19
|
+
const canvases = new Map();
|
|
20
|
+
const visible = new Set();
|
|
21
|
+
let zoom = 1;
|
|
22
|
+
const observer = new IntersectionObserver((entries) => {
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const idx = pageByEl.get(entry.target);
|
|
25
|
+
if (idx === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
if (entry.isIntersecting)
|
|
28
|
+
visible.add(idx);
|
|
29
|
+
else
|
|
30
|
+
visible.delete(idx);
|
|
31
|
+
}
|
|
32
|
+
updateBand();
|
|
33
|
+
}, { root: container, threshold: 0 });
|
|
34
|
+
function makeSlot(page) {
|
|
35
|
+
const size = session.pageSize(page);
|
|
36
|
+
// The sheet's own edge, and the only thing separating it from the desk: the
|
|
37
|
+
// page is at `--_qm-surface`, and the tone behind it is the host's, which
|
|
38
|
+
// THEMING permits to be plain `--qm-bg` (the same value). A hairline holds at
|
|
39
|
+
// both poles where a shadow's offset does not, and the
|
|
40
|
+
// boundary of the paper is not a mark on it, so the overlay's no-ink-at-rest
|
|
41
|
+
// rule is untouched (PREVIEW §Overlay). `border-box`, so the stroke lands
|
|
42
|
+
// inside the width the container gives and `clientWidth` below still measures
|
|
43
|
+
// the paper.
|
|
44
|
+
const el = document.createElement('div');
|
|
45
|
+
el.className = 'qm-page';
|
|
46
|
+
Object.assign(el.style, {
|
|
47
|
+
position: 'relative',
|
|
48
|
+
boxSizing: 'border-box',
|
|
49
|
+
width: '100%',
|
|
50
|
+
aspectRatio: `${size.widthPt} / ${size.heightPt}`,
|
|
51
|
+
background: 'var(--_qm-surface)',
|
|
52
|
+
border: 'var(--_qm-border-width) solid var(--_qm-border)'
|
|
53
|
+
});
|
|
54
|
+
if (page > 0)
|
|
55
|
+
el.style.marginTop = PAGE_GAP;
|
|
56
|
+
container.appendChild(el);
|
|
57
|
+
pageByEl.set(el, page);
|
|
58
|
+
observer.observe(el);
|
|
59
|
+
return { page, size, el };
|
|
60
|
+
}
|
|
61
|
+
// Rasterize `slot` into its canvas (creating the canvas if this is its first
|
|
62
|
+
// paint since mounting). `layoutScale` is derived from the slot's OWN current
|
|
63
|
+
// CSS width, so layout width tracks the container per the zoom settled
|
|
64
|
+
// decision; `canvas.style.*` is then set from the authoritative PaintResult,
|
|
65
|
+
// never guessed.
|
|
66
|
+
function paintSlot(slot) {
|
|
67
|
+
let canvas = canvases.get(slot.page);
|
|
68
|
+
if (!canvas) {
|
|
69
|
+
canvas = document.createElement('canvas');
|
|
70
|
+
canvas.className = 'qm-page-canvas';
|
|
71
|
+
Object.assign(canvas.style, { position: 'absolute', inset: '0', display: 'block' });
|
|
72
|
+
}
|
|
73
|
+
// Contextless canvas: bail BEFORE registering, so `updateBand` still sees
|
|
74
|
+
// the page as unmounted and retries instead of keeping a blank page.
|
|
75
|
+
const ctx = canvas.getContext('2d');
|
|
76
|
+
if (!ctx)
|
|
77
|
+
return;
|
|
78
|
+
if (!canvases.has(slot.page)) {
|
|
79
|
+
slot.el.insertBefore(canvas, slot.el.firstChild);
|
|
80
|
+
canvases.set(slot.page, canvas);
|
|
81
|
+
}
|
|
82
|
+
const layoutScale = (slot.el.clientWidth || slot.size.widthPt) / slot.size.widthPt;
|
|
83
|
+
const densityScale = (window.devicePixelRatio || 1) * zoom;
|
|
84
|
+
try {
|
|
85
|
+
const result = session.paint(ctx, slot.page, { layoutScale, densityScale });
|
|
86
|
+
canvas.style.width = `${result.layoutWidth}px`;
|
|
87
|
+
canvas.style.height = `${result.layoutHeight}px`;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// A paint that throws must not abort the band loop; `updateBand` runs in
|
|
91
|
+
// the IntersectionObserver callback and sweeps every entry, so an
|
|
92
|
+
// uncaught throw would strand the rest of the band unpainted. Unmount the
|
|
93
|
+
// half-mounted canvas (so the page is a retryable unmounted slot, not a
|
|
94
|
+
// blank registered one) and surface the failure; the sweep moves on.
|
|
95
|
+
unmountPage(slot.page);
|
|
96
|
+
onPaintError?.(slot.page, err);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function unmountPage(page) {
|
|
100
|
+
const canvas = canvases.get(page);
|
|
101
|
+
if (!canvas)
|
|
102
|
+
return;
|
|
103
|
+
canvas.remove();
|
|
104
|
+
canvases.delete(page);
|
|
105
|
+
}
|
|
106
|
+
// Mount pages newly inside [min(visible)-margin, max(visible)+margin], unmount
|
|
107
|
+
// pages that fell outside it. Only touches pages CHANGING band membership;
|
|
108
|
+
// an already-mounted page in the band is left alone (paint contract: an idle
|
|
109
|
+
// canvas keeps its pixels for free; repainting it here would be pure waste).
|
|
110
|
+
function updateBand() {
|
|
111
|
+
const painted = new Set();
|
|
112
|
+
if (visible.size === 0 || slots.length === 0)
|
|
113
|
+
return painted;
|
|
114
|
+
const min = Math.min(...visible);
|
|
115
|
+
const max = Math.max(...visible);
|
|
116
|
+
const lo = Math.max(0, min - margin);
|
|
117
|
+
const hi = Math.min(slots.length - 1, max + margin);
|
|
118
|
+
for (const slot of slots) {
|
|
119
|
+
const inBand = slot.page >= lo && slot.page <= hi;
|
|
120
|
+
if (inBand && !canvases.has(slot.page)) {
|
|
121
|
+
paintSlot(slot);
|
|
122
|
+
painted.add(slot.page);
|
|
123
|
+
}
|
|
124
|
+
else if (!inBand && canvases.has(slot.page))
|
|
125
|
+
unmountPage(slot.page);
|
|
126
|
+
}
|
|
127
|
+
return painted;
|
|
128
|
+
}
|
|
129
|
+
// Grow/shrink the slot array to `pageCount` (trailing add/remove; a recompile
|
|
130
|
+
// reports the new count, and dirtyPages already covers any reflow), then
|
|
131
|
+
// re-cache every page's geometry (cheap report-only reads; simplest way to
|
|
132
|
+
// stay correct across any recompile, not just a count change).
|
|
133
|
+
function reconcile(pageCount) {
|
|
134
|
+
while (slots.length > pageCount) {
|
|
135
|
+
const slot = slots.pop();
|
|
136
|
+
if (!slot)
|
|
137
|
+
break;
|
|
138
|
+
observer.unobserve(slot.el);
|
|
139
|
+
pageByEl.delete(slot.el);
|
|
140
|
+
unmountPage(slot.page);
|
|
141
|
+
visible.delete(slot.page);
|
|
142
|
+
slot.el.remove();
|
|
143
|
+
}
|
|
144
|
+
while (slots.length < pageCount) {
|
|
145
|
+
slots.push(makeSlot(slots.length));
|
|
146
|
+
}
|
|
147
|
+
for (let i = 0; i < slots.length; i++) {
|
|
148
|
+
const size = session.pageSize(i);
|
|
149
|
+
slots[i] = { ...slots[i], size };
|
|
150
|
+
// Keep the page box's shape in step with the re-read size: a recompile
|
|
151
|
+
// can change a page's dimensions without changing the count, and every
|
|
152
|
+
// %-space overlay/click transform assumes box shape matches PageSize.
|
|
153
|
+
slots[i].el.style.aspectRatio = `${size.widthPt} / ${size.heightPt}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Slots are built on the first `refresh` (the controller calls it immediately),
|
|
157
|
+
// NOT here: reconcile → `session.pageSize` must run AFTER the controller's
|
|
158
|
+
// `supportsCanvas`/`pageCount` gate, since a `supportsCanvas: false` compile with
|
|
159
|
+
// pages would throw on `pageSize` (runtime.d.ts: it succeeds iff `supportsCanvas`)
|
|
160
|
+
// before the controller can surface its "unsupported" message.
|
|
161
|
+
// ── Keep mounted rasters in step with the display ───────────────────────────
|
|
162
|
+
// Every paint freezes `canvas.style.width/height` to the box width AT THAT
|
|
163
|
+
// PAINT (paintSlot). A page that stays mounted while the container's CSS width
|
|
164
|
+
// or `devicePixelRatio` shifts would otherwise keep a stale raster the %-space
|
|
165
|
+
// overlay/click math silently drifts off of: the box tracks the container, the
|
|
166
|
+
// ink does not. Two observers close the gap by repainting mounted pages from
|
|
167
|
+
// their current box; canvases are `position:absolute`, so a repaint can't feed
|
|
168
|
+
// back into layout (and thus can't re-trigger the resize observer).
|
|
169
|
+
// Repaint mounted pages (all, or a given subset) from their live geometry: the
|
|
170
|
+
// one path resize, DPR, and zoom share; each `paintSlot` re-reads the slot's
|
|
171
|
+
// current `clientWidth`. A no-op for an unmounted page.
|
|
172
|
+
function repaint(pages) {
|
|
173
|
+
for (const page of pages ?? canvases.keys()) {
|
|
174
|
+
const slot = slots[page];
|
|
175
|
+
if (slot && canvases.has(page))
|
|
176
|
+
paintSlot(slot);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Coalesce a burst of observer callbacks into one repaint on the next frame.
|
|
180
|
+
// `force` (a DPR change) bypasses the width guard, since a DPR change moves no
|
|
181
|
+
// CSS-px width; a plain resize repaints only when the width actually moved, so
|
|
182
|
+
// a height-only or no-op tick costs nothing.
|
|
183
|
+
let rafId = 0;
|
|
184
|
+
let lastWidth = container.clientWidth;
|
|
185
|
+
let forcePending = false;
|
|
186
|
+
function scheduleRepaint(force) {
|
|
187
|
+
if (force)
|
|
188
|
+
forcePending = true;
|
|
189
|
+
if (rafId)
|
|
190
|
+
return;
|
|
191
|
+
rafId = requestAnimationFrame(() => {
|
|
192
|
+
rafId = 0;
|
|
193
|
+
const forced = forcePending;
|
|
194
|
+
forcePending = false;
|
|
195
|
+
const width = container.clientWidth;
|
|
196
|
+
if (!forced && width === lastWidth)
|
|
197
|
+
return;
|
|
198
|
+
lastWidth = width;
|
|
199
|
+
repaint();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Container CSS-width changes (drag the pane wider, resize the window). Guarded
|
|
203
|
+
// for jsdom, which ships no ResizeObserver; the unit suite drives no resize, so
|
|
204
|
+
// skipping it there is correct; a real browser takes the observed path.
|
|
205
|
+
const resizeObserver = typeof ResizeObserver === 'undefined'
|
|
206
|
+
? undefined
|
|
207
|
+
: new ResizeObserver(() => scheduleRepaint(false));
|
|
208
|
+
resizeObserver?.observe(container);
|
|
209
|
+
// `devicePixelRatio` changes (window dragged to a different-DPI monitor, browser
|
|
210
|
+
// zoom) leave CSS-px width untouched, so the ResizeObserver never sees them;
|
|
211
|
+
// watch DPR directly. A `(resolution: …dppx)` query pins the CURRENT ratio and
|
|
212
|
+
// is therefore one-shot: re-arm it against the new ratio on each change. Guarded
|
|
213
|
+
// for environments (jsdom) without `matchMedia`.
|
|
214
|
+
let dprQuery;
|
|
215
|
+
function onDprChange() {
|
|
216
|
+
scheduleRepaint(true);
|
|
217
|
+
armDprListener();
|
|
218
|
+
}
|
|
219
|
+
function armDprListener() {
|
|
220
|
+
if (typeof matchMedia === 'undefined')
|
|
221
|
+
return;
|
|
222
|
+
dprQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
223
|
+
dprQuery.addEventListener('change', onDprChange, { once: true });
|
|
224
|
+
}
|
|
225
|
+
armDprListener();
|
|
226
|
+
return {
|
|
227
|
+
get slots() {
|
|
228
|
+
return slots;
|
|
229
|
+
},
|
|
230
|
+
refresh(dirtyPages, pageCount) {
|
|
231
|
+
reconcile(pageCount);
|
|
232
|
+
// Slot count may have moved the band (pages added/removed); a page the
|
|
233
|
+
// band just painted is already post-apply, so a second full
|
|
234
|
+
// rasterization from the dirty loop would add nothing.
|
|
235
|
+
const painted = updateBand();
|
|
236
|
+
for (const page of dirtyPages) {
|
|
237
|
+
if (painted.has(page))
|
|
238
|
+
continue;
|
|
239
|
+
const slot = slots[page];
|
|
240
|
+
if (slot && canvases.has(page))
|
|
241
|
+
paintSlot(slot);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
setDensityZoom(z) {
|
|
245
|
+
zoom = z;
|
|
246
|
+
repaint();
|
|
247
|
+
},
|
|
248
|
+
destroy() {
|
|
249
|
+
if (rafId)
|
|
250
|
+
cancelAnimationFrame(rafId);
|
|
251
|
+
resizeObserver?.disconnect();
|
|
252
|
+
dprQuery?.removeEventListener('change', onDprChange);
|
|
253
|
+
observer.disconnect();
|
|
254
|
+
for (const page of [...canvases.keys()])
|
|
255
|
+
unmountPage(page);
|
|
256
|
+
for (const slot of slots)
|
|
257
|
+
slot.el.remove();
|
|
258
|
+
slots.length = 0;
|
|
259
|
+
pageByEl.clear();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PreviewStrings {
|
|
2
|
+
/** A page the backend refused to raster. */
|
|
3
|
+
renderFailed: string;
|
|
4
|
+
/** A compile with zero pages: recoverable, and the commonest state of a fresh seed. */
|
|
5
|
+
noPages: string;
|
|
6
|
+
/** A compile whose pages this backend cannot raster at all. */
|
|
7
|
+
unsupported: string;
|
|
8
|
+
}
|
|
9
|
+
/** The package's English. Every key, so the merge below is total. */
|
|
10
|
+
export declare const DEFAULT_PREVIEW_STRINGS: PreviewStrings;
|
|
11
|
+
/** A consumer's wording: any subset, the rest the package's. */
|
|
12
|
+
export type PreviewStringsInput = Partial<PreviewStrings>;
|
|
13
|
+
/** Fill a partial override to the whole key set. */
|
|
14
|
+
export declare function mergePreviewStrings(custom: PreviewStringsInput | undefined): PreviewStrings;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Every word the preview says. Three, because the surface is a canvas and its
|
|
2
|
+
// only prose is the message states that stand in when there is nothing to paint.
|
|
3
|
+
//
|
|
4
|
+
// Separate from the visual surface's set rather than pooled with it: `/preview`
|
|
5
|
+
// reaches `/core` and nothing editor-side, which is what lets it promote to its own
|
|
6
|
+
// package, and a shared strings module would be an edge back across that line. The
|
|
7
|
+
// shape is the same, so a consumer wording both writes the same kind of object
|
|
8
|
+
// twice rather than learning two idioms.
|
|
9
|
+
//
|
|
10
|
+
// A plain options field rather than a context channel, unlike the visual surface's:
|
|
11
|
+
// these three are read in one module by one function, so there is no tree to make
|
|
12
|
+
// them ambient for.
|
|
13
|
+
/** The package's English. Every key, so the merge below is total. */
|
|
14
|
+
export const DEFAULT_PREVIEW_STRINGS = {
|
|
15
|
+
renderFailed: 'Preview failed to render.',
|
|
16
|
+
noPages: 'No pages to preview.',
|
|
17
|
+
unsupported: 'Preview is not available for this document.'
|
|
18
|
+
};
|
|
19
|
+
/** Fill a partial override to the whole key set. */
|
|
20
|
+
export function mergePreviewStrings(custom) {
|
|
21
|
+
return custom ? { ...DEFAULT_PREVIEW_STRINGS, ...custom } : DEFAULT_PREVIEW_STRINGS;
|
|
22
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
An `array` field → an add/remove repeater. Elements commit by VALUE: every
|
|
3
|
+
edit / add / remove rebuilds the whole array and hands it to the parent's typed
|
|
4
|
+
`writer.set(field, wholeArray)` (arrays are not op-addressed). Element control
|
|
5
|
+
by `items.type`: `string` → text input, `richtext` → a prose element
|
|
6
|
+
({@link ProseArrayElement}), `object` → a minimal JSON editor, keeping the prior
|
|
7
|
+
element value on an entry that does not parse. The add affordance sits in the
|
|
8
|
+
label header row (space-between with the field label); {@link Field} skips its
|
|
9
|
+
own label for array controls and hands this component the label track with it.
|
|
10
|
+
|
|
11
|
+
A row ends in the reserved action column (`--action-col`), which the field insets
|
|
12
|
+
and this reaches back across: the element controls stop where a scalar's does and
|
|
13
|
+
the remove sits beyond them, on the right edge the field's track keeps.
|
|
14
|
+
|
|
15
|
+
Keys carry the list without the mouse: Enter inserts a
|
|
16
|
+
sibling below and takes the caret there, Backspace on an EMPTY element removes it
|
|
17
|
+
and hands focus back up the list.
|
|
18
|
+
|
|
19
|
+
No reorder: an array's order is fixed at declaration/entry order. Elements
|
|
20
|
+
carry a parallel session-id list, spliced with the values as one operation at
|
|
21
|
+
whatever index a mutation names; so an element holds its id for life and the
|
|
22
|
+
surviving order never permutes, which is what lets a keyed prose element survive an
|
|
23
|
+
insert or a remove ABOVE it rather than remounting.
|
|
24
|
+
-->
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import { wording } from './strings.js';
|
|
27
|
+
|
|
28
|
+
// The surface's words, ambient from the editor root; the package's English
|
|
29
|
+
// off-tree, so this component renders standalone too.
|
|
30
|
+
const t = wording();
|
|
31
|
+
import { onDestroy, tick } from 'svelte';
|
|
32
|
+
import type { Content, QuillFieldSchema } from '@quillmark/wasm';
|
|
33
|
+
import { emptyContent } from '../core/codec/index.js';
|
|
34
|
+
import { createLifespan } from '../core/teardown.js';
|
|
35
|
+
import { IdSeq, controlKind } from './structure.js';
|
|
36
|
+
import Icon from './icons/Icon.svelte';
|
|
37
|
+
import TextField from './TextField.svelte';
|
|
38
|
+
import ProseArrayElement from './ProseArrayElement.svelte';
|
|
39
|
+
import FieldLabel from './FieldLabel.svelte';
|
|
40
|
+
import './controls.css';
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
value: unknown[] | undefined;
|
|
44
|
+
items: QuillFieldSchema | undefined;
|
|
45
|
+
/** Accessible-name prefix for the element controls (`label` + 1-based index). */
|
|
46
|
+
label?: string;
|
|
47
|
+
/** No-default field → a persistent required `*` on the label. */
|
|
48
|
+
required?: boolean;
|
|
49
|
+
/** Schema `description`: the label's help affordance. */
|
|
50
|
+
description?: string;
|
|
51
|
+
/** The label's own DOM id. An array is a GROUP (N inputs, no single `for`
|
|
52
|
+
* target) so the label names the set and each element keeps its indexed
|
|
53
|
+
* `aria-label`. */
|
|
54
|
+
labelId?: string;
|
|
55
|
+
/** Where the description parks, for the group's `aria-describedby`. */
|
|
56
|
+
descriptionId?: string;
|
|
57
|
+
onCommit: (arr: unknown[]) => void;
|
|
58
|
+
/** A prose element gained focus: joins the field in the focus federation. */
|
|
59
|
+
onFocusEl?: () => void;
|
|
60
|
+
}
|
|
61
|
+
let {
|
|
62
|
+
value,
|
|
63
|
+
items,
|
|
64
|
+
label,
|
|
65
|
+
required,
|
|
66
|
+
description,
|
|
67
|
+
labelId,
|
|
68
|
+
descriptionId,
|
|
69
|
+
onCommit,
|
|
70
|
+
onFocusEl
|
|
71
|
+
}: Props = $props();
|
|
72
|
+
|
|
73
|
+
// The ELEMENT control is the item schema's own; an array declaring no `items`
|
|
74
|
+
// has text elements.
|
|
75
|
+
const control = $derived(items ? controlKind(items) : 'text');
|
|
76
|
+
const plaintext = $derived(items?.type === 'plaintext');
|
|
77
|
+
const arr = $derived((value ?? []) as unknown[]);
|
|
78
|
+
|
|
79
|
+
// Parallel stable ids, one per element, kept in lockstep with the data below.
|
|
80
|
+
// Seeded eagerly so a non-empty array renders its rows on the FIRST pass:
|
|
81
|
+
// an effect-only seed mounts every element editor in a second render.
|
|
82
|
+
const seq = new IdSeq();
|
|
83
|
+
// svelte-ignore state_referenced_locally
|
|
84
|
+
let ids = $state<string[]>(seq.take((value ?? []).length));
|
|
85
|
+
// Length reconcile (defend against an out-of-band length change);
|
|
86
|
+
// order is maintained by the mutators, not here.
|
|
87
|
+
$effect(() => {
|
|
88
|
+
const n = arr.length;
|
|
89
|
+
if (ids.length === n) return;
|
|
90
|
+
if (ids.length < n) ids = [...ids, ...seq.take(n - ids.length)];
|
|
91
|
+
else {
|
|
92
|
+
for (const id of ids.slice(n)) delete els[id];
|
|
93
|
+
ids = ids.slice(0, n);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The focus targets, keyed by element ID rather than index: an index goes stale on
|
|
98
|
+
// the splice that focus is chasing. An element control exposes `focus()` because a
|
|
99
|
+
// text element and a prose element disagree on what focusing is: the difference
|
|
100
|
+
// is stated on `ProseArrayElement.focus`, which owns it.
|
|
101
|
+
//
|
|
102
|
+
// Every path that drops an id deletes its entry: `bind:this` teardown nulls the
|
|
103
|
+
// VALUE on unmount and leaves the key, so a card that outlives its elements
|
|
104
|
+
// accumulates one dead key per element ever created.
|
|
105
|
+
const els: Record<string, { focus: () => void } | undefined> = {};
|
|
106
|
+
let addEl: HTMLButtonElement | undefined = $state();
|
|
107
|
+
let rootEl: HTMLElement | undefined = $state();
|
|
108
|
+
|
|
109
|
+
// The awaited flush below is the only work that outlives a gesture here, so the
|
|
110
|
+
// span carries no cancellers: it is the liveness `focusAfterFlush` asks for.
|
|
111
|
+
const span = createLifespan();
|
|
112
|
+
onDestroy(() => span.end());
|
|
113
|
+
|
|
114
|
+
function emptyElement(): unknown {
|
|
115
|
+
if (control === 'prose') return emptyContent();
|
|
116
|
+
if (control === 'object') return {};
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function commitElement(k: number, next: unknown): void {
|
|
121
|
+
const copy = arr.slice();
|
|
122
|
+
// A cleared element control commits `undefined` (the unset rung), but an
|
|
123
|
+
// array slot is positional: an array defaults as a whole (`[]`), no
|
|
124
|
+
// per-element `default:` to fall back to. Keep the slot as the type's empty
|
|
125
|
+
// element, not an array hole.
|
|
126
|
+
copy[k] = next === undefined ? emptyElement() : next;
|
|
127
|
+
onCommit(copy);
|
|
128
|
+
}
|
|
129
|
+
/** Insert an empty element after `k` (`-1` prepends) and take focus to it. */
|
|
130
|
+
function insertAfter(k: number): void {
|
|
131
|
+
const id = seq.next();
|
|
132
|
+
const at = k + 1;
|
|
133
|
+
ids = [...ids.slice(0, at), id, ...ids.slice(at)];
|
|
134
|
+
const next = arr.slice();
|
|
135
|
+
next.splice(at, 0, emptyElement());
|
|
136
|
+
onCommit(next);
|
|
137
|
+
focusAfterFlush(id);
|
|
138
|
+
}
|
|
139
|
+
function add(): void {
|
|
140
|
+
insertAfter(ids.length - 1);
|
|
141
|
+
}
|
|
142
|
+
function remove(k: number): void {
|
|
143
|
+
const dropped = ids[k];
|
|
144
|
+
const next = ids.filter((_, i) => i !== k);
|
|
145
|
+
ids = next;
|
|
146
|
+
delete els[dropped];
|
|
147
|
+
onCommit(arr.filter((_, i) => i !== k));
|
|
148
|
+
// Focus lands on the element before the removed one, or on the one that slid
|
|
149
|
+
// into its place; on the add affordance once the list is empty, which is then
|
|
150
|
+
// the only thing left to hold it. Clicking the remove needs this as much as the
|
|
151
|
+
// key does: the button under the pointer is part of what it destroys.
|
|
152
|
+
focusAfterFlush(next[Math.max(k - 1, 0)]);
|
|
153
|
+
}
|
|
154
|
+
/** A label click on an array, resolved rather than left to dangle: the FIRST
|
|
155
|
+
* element, or the add affordance when the list is empty; which is then the only
|
|
156
|
+
* thing there is to land on, and the next thing the user wants anyway. */
|
|
157
|
+
function activate(): void {
|
|
158
|
+
if (ids.length === 0) return void addEl?.focus();
|
|
159
|
+
const first = els[ids[0]];
|
|
160
|
+
if (first) return first.focus();
|
|
161
|
+
// The JSON element registers no controller: it is a plain textarea, with
|
|
162
|
+
// nothing about focusing it that the DOM does not already know.
|
|
163
|
+
rootEl?.querySelector<HTMLTextAreaElement>('.qm-array-row textarea')?.focus();
|
|
164
|
+
}
|
|
165
|
+
/** Focus element `id` after the flush, never in the same tick: a mutation commits
|
|
166
|
+
* the array BY VALUE, so the parent re-derives and the row does not exist until
|
|
167
|
+
* then. `undefined` is the empty list: the add affordance.
|
|
168
|
+
*
|
|
169
|
+
* The commit that schedules this can also remove the CARD holding the field, which
|
|
170
|
+
* unmounts this component inside the window (core/teardown.ts). */
|
|
171
|
+
async function focusAfterFlush(id: string | undefined): Promise<void> {
|
|
172
|
+
if (!(await span.resumes(tick()))) return;
|
|
173
|
+
if (id === undefined) addEl?.focus();
|
|
174
|
+
else els[id]?.focus();
|
|
175
|
+
}
|
|
176
|
+
/** Whether element `k` reads empty to the user. A text element's committed value
|
|
177
|
+
* LAGS the input: a cleared field commits at `change`, not per keystroke
|
|
178
|
+
* ({@link TextField}); so the input's own value is the truth; a prose element
|
|
179
|
+
* commits every edit, so the committed `Content` is. */
|
|
180
|
+
function elementEmpty(k: number, target: EventTarget | null): boolean {
|
|
181
|
+
if (control === 'prose') return !(arr[k] as Content | undefined)?.text;
|
|
182
|
+
return target instanceof HTMLInputElement && !target.value;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The element keyboard contract. Both keys ride the element control's own keydown
|
|
186
|
+
* (the input's, or the PM view's through `handleDOMEvents`) since neither
|
|
187
|
+
* surface is a place a keymap of this component's could sit. The JSON element is
|
|
188
|
+
* out: Enter is a newline in a textarea.
|
|
189
|
+
*/
|
|
190
|
+
function onElementKey(e: KeyboardEvent, k: number): void {
|
|
191
|
+
if (control === 'object' || e.isComposing) return;
|
|
192
|
+
if (e.key === 'Enter') {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
insertAfter(k);
|
|
195
|
+
} else if (e.key === 'Backspace' && !e.repeat && elementEmpty(k, e.target)) {
|
|
196
|
+
// Destructive with nothing to undo it, so it takes a deliberate press:
|
|
197
|
+
// `repeat` is a held key running on past the character it just cleared, and
|
|
198
|
+
// the emptiness test reads the state BEFORE this keystroke applies; so the
|
|
199
|
+
// press that empties an element never also removes it.
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
remove(k);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
</script>
|
|
205
|
+
|
|
206
|
+
<div
|
|
207
|
+
bind:this={rootEl}
|
|
208
|
+
class="qm-array"
|
|
209
|
+
role="group"
|
|
210
|
+
aria-labelledby={label != null ? labelId : undefined}
|
|
211
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
212
|
+
>
|
|
213
|
+
<div class="qm-array-header">
|
|
214
|
+
{#if label != null}
|
|
215
|
+
<FieldLabel
|
|
216
|
+
{label}
|
|
217
|
+
id={labelId}
|
|
218
|
+
{descriptionId}
|
|
219
|
+
onActivate={activate}
|
|
220
|
+
{required}
|
|
221
|
+
{description}
|
|
222
|
+
/>
|
|
223
|
+
{:else}
|
|
224
|
+
<span></span>
|
|
225
|
+
{/if}
|
|
226
|
+
<button type="button" class="qm-add-el qm-add-affordance" bind:this={addEl} onclick={add}
|
|
227
|
+
>{t.strings.arrayAdd}</button
|
|
228
|
+
>
|
|
229
|
+
</div>
|
|
230
|
+
{#each ids as id, k (id)}
|
|
231
|
+
<div class="qm-array-row">
|
|
232
|
+
{#if control === 'prose'}
|
|
233
|
+
<ProseArrayElement
|
|
234
|
+
bind:this={els[id]}
|
|
235
|
+
value={(arr[k] ?? emptyElement()) as Content}
|
|
236
|
+
{plaintext}
|
|
237
|
+
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
238
|
+
onChange={(rt) => commitElement(k, rt)}
|
|
239
|
+
onKey={(e) => onElementKey(e, k)}
|
|
240
|
+
{onFocusEl}
|
|
241
|
+
/>
|
|
242
|
+
{:else if control === 'object'}
|
|
243
|
+
<textarea
|
|
244
|
+
class="qm-input qm-json qm-focus-ring"
|
|
245
|
+
aria-label={label != null ? `${label} ${k + 1}` : undefined}
|
|
246
|
+
value={JSON.stringify(arr[k] ?? {})}
|
|
247
|
+
onchange={(e) => {
|
|
248
|
+
try {
|
|
249
|
+
commitElement(k, JSON.parse((e.currentTarget as HTMLTextAreaElement).value));
|
|
250
|
+
} catch {
|
|
251
|
+
/* keep prior value on invalid JSON */
|
|
252
|
+
}
|
|
253
|
+
}}
|
|
254
|
+
></textarea>
|
|
255
|
+
{:else}
|
|
256
|
+
<TextField
|
|
257
|
+
bind:this={els[id]}
|
|
258
|
+
value={String(arr[k] ?? '')}
|
|
259
|
+
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
260
|
+
onCommit={(v) => commitElement(k, v)}
|
|
261
|
+
onKey={(e) => onElementKey(e, k)}
|
|
262
|
+
/>
|
|
263
|
+
{/if}
|
|
264
|
+
<button
|
|
265
|
+
type="button"
|
|
266
|
+
class="qm-icon-btn qm-remove"
|
|
267
|
+
title={t.strings.arrayRemove}
|
|
268
|
+
onclick={() => remove(k)}><Icon name="x" size={14} /></button
|
|
269
|
+
>
|
|
270
|
+
</div>
|
|
271
|
+
{/each}
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<style>
|
|
275
|
+
.qm-array {
|
|
276
|
+
display: flex;
|
|
277
|
+
flex-direction: column;
|
|
278
|
+
gap: var(--_qm-space);
|
|
279
|
+
/* The one field that reaches across the reserved action column: its rows END in
|
|
280
|
+
that column, so an element control stops exactly where the scalar beside it
|
|
281
|
+
does and the remove never sits over a long value. The reservation is the
|
|
282
|
+
FIELD's inset (Field.svelte), so this cancels it wherever the field landed:
|
|
283
|
+
its own row, or one track of a shared one. */
|
|
284
|
+
margin-right: calc(-1 * var(--action-col));
|
|
285
|
+
}
|
|
286
|
+
/* The array's first line IS the row's label line: this component owns the label
|
|
287
|
+
track (Field.svelte), so the header has to measure what a `.qm-field-label-row`
|
|
288
|
+
measures or an array shares a row with a scalar and their labels sit apart. The
|
|
289
|
+
label is the same component; what had to give was the add affordance's height,
|
|
290
|
+
below. */
|
|
291
|
+
.qm-array-header {
|
|
292
|
+
display: flex;
|
|
293
|
+
align-items: center;
|
|
294
|
+
justify-content: space-between;
|
|
295
|
+
gap: var(--_qm-space-2);
|
|
296
|
+
}
|
|
297
|
+
/* The element takes the section's tracks, the remove takes the action column, and
|
|
298
|
+
the gutter between them is the section's own: the row is that column's only
|
|
299
|
+
occupant, so it reads as one more column of the grid rather than a nested layout.
|
|
300
|
+
`minmax(0, …)` because an element control must be allowed to be narrower than its
|
|
301
|
+
content: a long unbroken value grows the track otherwise, and the edge with it. */
|
|
302
|
+
.qm-array-row {
|
|
303
|
+
display: grid;
|
|
304
|
+
grid-template-columns: minmax(0, 1fr) var(--_qm-tap-min);
|
|
305
|
+
column-gap: var(--_qm-space-2);
|
|
306
|
+
align-items: start;
|
|
307
|
+
}
|
|
308
|
+
.qm-remove {
|
|
309
|
+
align-self: center;
|
|
310
|
+
}
|
|
311
|
+
/* The JSON element is a `.qm-input` (controls.css): box, focus ring, and all;
|
|
312
|
+
what a textarea adds over an input is the face and a floor on its height. */
|
|
313
|
+
.qm-json {
|
|
314
|
+
font-family: var(--_qm-font-mono);
|
|
315
|
+
min-height: 2.5rem;
|
|
316
|
+
}
|
|
317
|
+
/* Chrome, hover fill and target come from `.qm-add-affordance` (controls.css);
|
|
318
|
+
what is here is this trigger's own type, box and recede ladder. It rests dim like
|
|
319
|
+
the card stack's gap triggers and comes up on hover of the FIELD or on focus: one
|
|
320
|
+
trigger at the head of a row of controls is found by looking at the array, not by
|
|
321
|
+
grazing its first line.
|
|
322
|
+
|
|
323
|
+
THE BOX IS THE LABEL'S LINE BOX, and the target is the `::after`: the same split
|
|
324
|
+
the field label's guidance marker takes, for the same reason and one row over.
|
|
325
|
+
This is the other affordance that sits IN a line of text rather than in a row of
|
|
326
|
+
its own, and the line is a label's, so a
|
|
327
|
+
target-sized box would stand the header 8px taller than the `.qm-field-label-row`
|
|
328
|
+
beside it in a shared row and put the two labels on different lines. So the family's
|
|
329
|
+
floor comes off the box and goes out of flow, unpainted and centred on it: the row
|
|
330
|
+
keeps the label's line box, the press keeps the floor. The horizontal edges are the
|
|
331
|
+
button's own, the word being already wider than the threshold.
|
|
332
|
+
|
|
333
|
+
The type is the LABEL rung in place of the family's body rung, which the family
|
|
334
|
+
invites (controls.css: a caller whose button carries a label restates the rung it
|
|
335
|
+
wants). It reads as one more thing on the label line rather than as a control that
|
|
336
|
+
wandered up from the row below. */
|
|
337
|
+
.qm-add-el {
|
|
338
|
+
position: relative;
|
|
339
|
+
min-height: 0;
|
|
340
|
+
padding: 0 var(--_qm-space);
|
|
341
|
+
font-size: var(--_qm-text-label);
|
|
342
|
+
opacity: var(--_qm-opacity-idle);
|
|
343
|
+
}
|
|
344
|
+
.qm-add-el::after {
|
|
345
|
+
content: '';
|
|
346
|
+
position: absolute;
|
|
347
|
+
inset-inline: 0;
|
|
348
|
+
top: 50%;
|
|
349
|
+
height: var(--_qm-tap-min);
|
|
350
|
+
transform: translateY(-50%);
|
|
351
|
+
}
|
|
352
|
+
.qm-array:hover .qm-add-el,
|
|
353
|
+
.qm-add-el:focus-visible {
|
|
354
|
+
opacity: 1;
|
|
355
|
+
}
|
|
356
|
+
/* Touch has no hover: keep a faint always-on affordance so add stays reachable. */
|
|
357
|
+
@media (hover: none) {
|
|
358
|
+
.qm-add-el {
|
|
359
|
+
opacity: var(--_qm-opacity-muted);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
2
|
+
import './controls.css';
|
|
3
|
+
interface Props {
|
|
4
|
+
value: unknown[] | undefined;
|
|
5
|
+
items: QuillFieldSchema | undefined;
|
|
6
|
+
/** Accessible-name prefix for the element controls (`label` + 1-based index). */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** No-default field → a persistent required `*` on the label. */
|
|
9
|
+
required?: boolean;
|
|
10
|
+
/** Schema `description`: the label's help affordance. */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** The label's own DOM id. An array is a GROUP (N inputs, no single `for`
|
|
13
|
+
* target) so the label names the set and each element keeps its indexed
|
|
14
|
+
* `aria-label`. */
|
|
15
|
+
labelId?: string;
|
|
16
|
+
/** Where the description parks, for the group's `aria-describedby`. */
|
|
17
|
+
descriptionId?: string;
|
|
18
|
+
onCommit: (arr: unknown[]) => void;
|
|
19
|
+
/** A prose element gained focus: joins the field in the focus federation. */
|
|
20
|
+
onFocusEl?: () => void;
|
|
21
|
+
}
|
|
22
|
+
declare const ArrayField: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
type ArrayField = ReturnType<typeof ArrayField>;
|
|
24
|
+
export default ArrayField;
|