@quillmark/svelte 0.4.1 → 0.4.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.
@@ -65,7 +65,8 @@
65
65
  // rest per codec"), and the read that decodes a resting value to `Content` addresses
66
66
  // a field, never an element (`getContent`; borb-sh/quillmark#1243), so a string is
67
67
  // what the row is handed and a string is what it commits. The prose element stays
68
- // the `richtext` arm, whose elements rest as `Content` and have one.
68
+ // the `richtext` arm: `Content` after a bound load or a commit, the authored
69
+ // string through the transport door, lowered in the row for the same reason.
69
70
  //
70
71
  // An array declaring no `items` has text elements.
71
72
  const control = $derived(items && items.type !== 'plaintext' ? controlKind(items) : 'text');
@@ -107,6 +108,7 @@
107
108
  const els: Record<string, { focus: () => void } | undefined> = $state({});
108
109
  let addEl: HTMLButtonElement | undefined = $state();
109
110
  let rootEl: HTMLElement | undefined = $state();
111
+ let rowsEl: HTMLElement | undefined = $state();
110
112
 
111
113
  // The awaited flush below is the only work that outlives a gesture here, so the
112
114
  // span carries no cancellers: it is the liveness `focusAfterFlush` asks for.
@@ -165,6 +167,14 @@
165
167
  // nothing about focusing it that the DOM does not already know.
166
168
  rootEl?.querySelector<HTMLTextAreaElement>('.qm-array-row textarea')?.focus();
167
169
  }
170
+ /** The box an arrival wash blooms in (`leaves.ts`, `core/bloom.ts`): the elements,
171
+ * not the header above them. This component owns the field's label, so the wrapper
172
+ * `Field` blooms every other control inside would wash the label here too. Empty,
173
+ * the box is `display: none` and the landing is answered by the focus the add
174
+ * affordance takes. */
175
+ export function washBox(): HTMLElement | undefined {
176
+ return rowsEl;
177
+ }
168
178
  /** Take the caret to element `k`: what a landing on an element address resolves to
169
179
  * (`leaves.ts`). The index resolves to the element's session id here, at the call,
170
180
  * never carried as one — an index is stale the moment anything above it splices.
@@ -188,10 +198,15 @@
188
198
  }
189
199
  /** Whether element `k` reads empty to the user. A text element's committed value
190
200
  * lags the input: a cleared field commits at `change`, not per keystroke
191
- * ({@link TextField}); so the input's own value is the truth; a prose element
192
- * commits every edit, so the committed `Content` is. */
201
+ * ({@link TextField}); so the input's own value is the truth. A prose element
202
+ * commits every edit, so the committed `Content` is; an authored string, the
203
+ * transport-door rest, is empty when it has no characters. */
193
204
  function elementEmpty(k: number, target: EventTarget | null): boolean {
194
- if (control === 'prose') return !(arr[k] as Content | undefined)?.text;
205
+ if (control === 'prose') {
206
+ const el = arr[k];
207
+ if (typeof el === 'string') return el.length === 0;
208
+ return !(el as Content | undefined)?.text;
209
+ }
195
210
  return target instanceof HTMLInputElement && !target.value;
196
211
  }
197
212
  /**
@@ -243,46 +258,48 @@
243
258
  onclick={add}>{t.strings.arrayAdd}</button
244
259
  >
245
260
  </div>
246
- {#each ids as id, k (id)}
247
- <div class="qm-array-row">
248
- {#if control === 'prose'}
249
- <ProseArrayElement
250
- bind:this={els[id]}
251
- value={(arr[k] ?? emptyElement()) as Content}
252
- label={label != null ? `${label} ${k + 1}` : undefined}
253
- onChange={(rt) => commitElement(k, rt)}
254
- onKey={(e) => onElementKey(e, k)}
255
- />
256
- {:else if control === 'object'}
257
- <textarea
258
- class="qm-input qm-json qm-focus-ring"
259
- aria-label={label != null ? `${label} ${k + 1}` : undefined}
260
- value={JSON.stringify(arr[k] ?? {})}
261
- onchange={(e) => {
262
- try {
263
- commitElement(k, JSON.parse((e.currentTarget as HTMLTextAreaElement).value));
264
- } catch {
265
- /* keep prior value on invalid JSON */
266
- }
267
- }}
268
- ></textarea>
269
- {:else}
270
- <TextField
271
- bind:this={els[id]}
272
- value={String(arr[k] ?? '')}
273
- label={label != null ? `${label} ${k + 1}` : undefined}
274
- onCommit={(v) => commitElement(k, v)}
275
- onKey={(e) => onElementKey(e, k)}
276
- />
277
- {/if}
278
- <button
279
- type="button"
280
- class="qm-icon-btn qm-remove qm-focus-ring"
281
- title={t.strings.arrayRemove}
282
- onclick={() => remove(k)}><Icon name="minus" /></button
283
- >
284
- </div>
285
- {/each}
261
+ <div class="qm-array-rows" class:empty={ids.length === 0} bind:this={rowsEl}>
262
+ {#each ids as id, k (id)}
263
+ <div class="qm-array-row">
264
+ {#if control === 'prose'}
265
+ <ProseArrayElement
266
+ bind:this={els[id]}
267
+ value={(arr[k] ?? emptyElement()) as Content | string}
268
+ label={label != null ? `${label} ${k + 1}` : undefined}
269
+ onChange={(rt) => commitElement(k, rt)}
270
+ onKey={(e) => onElementKey(e, k)}
271
+ />
272
+ {:else if control === 'object'}
273
+ <textarea
274
+ class="qm-input qm-json qm-focus-ring"
275
+ aria-label={label != null ? `${label} ${k + 1}` : undefined}
276
+ value={JSON.stringify(arr[k] ?? {})}
277
+ onchange={(e) => {
278
+ try {
279
+ commitElement(k, JSON.parse((e.currentTarget as HTMLTextAreaElement).value));
280
+ } catch {
281
+ /* keep prior value on invalid JSON */
282
+ }
283
+ }}
284
+ ></textarea>
285
+ {:else}
286
+ <TextField
287
+ bind:this={els[id]}
288
+ value={String(arr[k] ?? '')}
289
+ label={label != null ? `${label} ${k + 1}` : undefined}
290
+ onCommit={(v) => commitElement(k, v)}
291
+ onKey={(e) => onElementKey(e, k)}
292
+ />
293
+ {/if}
294
+ <button
295
+ type="button"
296
+ class="qm-icon-btn qm-remove qm-focus-ring"
297
+ title={t.strings.arrayRemove}
298
+ onclick={() => remove(k)}><Icon name="minus" /></button
299
+ >
300
+ </div>
301
+ {/each}
302
+ </div>
286
303
  </div>
287
304
 
288
305
  <style>
@@ -302,6 +319,24 @@
302
319
  justify-content: space-between;
303
320
  gap: var(--_qm-space-2);
304
321
  }
322
+ /* The elements, in a box of their own: the arrival wash blooms here rather than over
323
+ the wrapper `Field` hands this component (`washBox`), which is the label's box too.
324
+ Positioned for the wash's inset child, and rounded to the rung a row's own box
325
+ draws, the way `.qm-field-control` is for every other control.
326
+
327
+ `display: none` when there are no elements, so the header does not stand a gap above
328
+ an empty box; a wash over it then paints nothing, which is what an array with
329
+ nothing in it has to show. */
330
+ .qm-array-rows {
331
+ position: relative;
332
+ display: flex;
333
+ flex-direction: column;
334
+ gap: var(--_qm-space);
335
+ border-radius: var(--_qm-radius-inner);
336
+ }
337
+ .qm-array-rows.empty {
338
+ display: none;
339
+ }
305
340
  /* A grid rather than a block: an `<input>` and a `<textarea>` are inline-level and
306
341
  would sit on a baseline, standing the row a descender taller than the box the slab
307
342
  measures itself against. `minmax(0, …)` because a long unbroken value grows an `auto`
@@ -19,6 +19,7 @@ interface Props {
19
19
  }
20
20
  declare const ArrayField: import("svelte").Component<Props, {
21
21
  focus: () => void;
22
+ washBox: () => HTMLElement | undefined;
22
23
  focusElement: (k: number) => void;
23
24
  }, "">;
24
25
  type ArrayField = ReturnType<typeof ArrayField>;
@@ -117,7 +117,14 @@
117
117
  // object on its first property.
118
118
  let proseEl = $state<{ focus: () => void } | undefined>();
119
119
  let dateEl = $state<{ focus: () => void } | undefined>();
120
- let arrayEl = $state<{ focus: () => void; focusElement: (k: number) => void } | undefined>();
120
+ let arrayEl = $state<
121
+ | {
122
+ focus: () => void;
123
+ focusElement: (k: number) => void;
124
+ washBox: () => HTMLElement | undefined;
125
+ }
126
+ | undefined
127
+ >();
121
128
  let objectEl = $state<{ focus: () => void } | undefined>();
122
129
  function focusControl(): void {
123
130
  const owner = proseEl ?? dateEl ?? arrayEl ?? objectEl;
@@ -135,7 +142,11 @@
135
142
 
136
143
  /**
137
144
  * This field's landing handle. The wrapper is the bloom host rather than the
138
- * control: `bloomInside` appends an inset child and an `<input>` holds none.
145
+ * control: `bloomInside` appends an inset child and an `<input>` holds none. The
146
+ * label is outside it, and stays out of the wash: an arrival marks where the caret
147
+ * landed, which is the control. The one control that owns its label is the array, so
148
+ * that one names its own box (`ArrayField.washBox`), read at the bloom the way
149
+ * `focusElement` is read at the call.
139
150
  *
140
151
  * A prose leaf is absent here — it registers its own controller from inside
141
152
  * `ProseField`, carrying the codec seam this handle has no half of — and reactive
@@ -150,10 +161,13 @@
150
161
  if (field.control === 'prose' || !controlEl || !leaves) return;
151
162
  const key = leafKey;
152
163
  const registry = leaves;
164
+ const wrapper = controlEl;
153
165
  registry.registerControl(key, {
154
166
  focus: focusControl,
155
167
  focusElement: field.control === 'array' ? focusElement : undefined,
156
- el: controlEl
168
+ get el() {
169
+ return arrayEl?.washBox() ?? wrapper;
170
+ }
157
171
  });
158
172
  return () => registry.unregisterControl(key);
159
173
  });
@@ -8,17 +8,25 @@
8
8
  by value (`writer.set(field, arrayWithElementReplaced)`). Anchors within an
9
9
  element are dropped on that value write: acceptable for inline refs. Mounts
10
10
  once per stable element id (no reset on the parent's re-derive).
11
+
12
+ A transport-door document rests each element as the authored string. A scalar
13
+ leaf reads that through `getContent`; `getContent` names a field, never an
14
+ element, so this row calls the boundary's `importMarkdown` itself. That is
15
+ the richtext codec, which is this row's `items` type; `decode` is the PM
16
+ mapping and assumes `Content`. A string here is the missing element read,
17
+ not a codec failure.
11
18
  -->
12
19
  <script lang="ts">
13
20
  import { onMount } from 'svelte';
14
21
  import { EditorState } from 'prosemirror-state';
15
22
  import { EditorView } from 'prosemirror-view';
16
23
  import { decode, pmToContent, inlineSchema, proseLeafPlugins } from '../core/codec/index.js';
24
+ import { core } from '../core/lifecycle.js';
17
25
  import './controls.css';
18
26
  import type { Content } from '@quillmark/wasm';
19
27
 
20
28
  interface Props {
21
- value: Content;
29
+ value: Content | string;
22
30
  /** Accessible name for the editable region (the array has no per-element label). */
23
31
  label?: string;
24
32
  onChange: (rt: Content) => void;
@@ -44,7 +52,8 @@
44
52
  // plugin stack a `createField` leaf mounts (shared `proseLeafPlugins`), minus
45
53
  // the anchor-position plugin (element anchors are dropped on the array's value
46
54
  // write, per the header). Its own `dispatchTransaction` hands `Content` up.
47
- const pmDoc = decode(value, inlineSchema);
55
+ const rt = typeof value === 'string' ? core().importMarkdown(value) : value;
56
+ const pmDoc = decode(rt, inlineSchema);
48
57
  const state = EditorState.create({
49
58
  doc: pmDoc,
50
59
  plugins: proseLeafPlugins(inlineSchema, { inline: true })
@@ -1,7 +1,7 @@
1
1
  import './controls.css';
2
2
  import type { Content } from '@quillmark/wasm';
3
3
  interface Props {
4
- value: Content;
4
+ value: Content | string;
5
5
  /** Accessible name for the editable region (the array has no per-element label). */
6
6
  label?: string;
7
7
  onChange: (rt: Content) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
5
5
  "repository": {
6
6
  "type": "git",