@quillmark/svelte 0.4.1 → 0.4.2

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.
@@ -107,6 +107,7 @@
107
107
  const els: Record<string, { focus: () => void } | undefined> = $state({});
108
108
  let addEl: HTMLButtonElement | undefined = $state();
109
109
  let rootEl: HTMLElement | undefined = $state();
110
+ let rowsEl: HTMLElement | undefined = $state();
110
111
 
111
112
  // The awaited flush below is the only work that outlives a gesture here, so the
112
113
  // span carries no cancellers: it is the liveness `focusAfterFlush` asks for.
@@ -165,6 +166,14 @@
165
166
  // nothing about focusing it that the DOM does not already know.
166
167
  rootEl?.querySelector<HTMLTextAreaElement>('.qm-array-row textarea')?.focus();
167
168
  }
169
+ /** The box an arrival wash blooms in (`leaves.ts`, `core/bloom.ts`): the elements,
170
+ * not the header above them. This component owns the field's label, so the wrapper
171
+ * `Field` blooms every other control inside would wash the label here too. Empty,
172
+ * the box is `display: none` and the landing is answered by the focus the add
173
+ * affordance takes. */
174
+ export function washBox(): HTMLElement | undefined {
175
+ return rowsEl;
176
+ }
168
177
  /** Take the caret to element `k`: what a landing on an element address resolves to
169
178
  * (`leaves.ts`). The index resolves to the element's session id here, at the call,
170
179
  * never carried as one — an index is stale the moment anything above it splices.
@@ -243,46 +252,48 @@
243
252
  onclick={add}>{t.strings.arrayAdd}</button
244
253
  >
245
254
  </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}
255
+ <div class="qm-array-rows" class:empty={ids.length === 0} bind:this={rowsEl}>
256
+ {#each ids as id, k (id)}
257
+ <div class="qm-array-row">
258
+ {#if control === 'prose'}
259
+ <ProseArrayElement
260
+ bind:this={els[id]}
261
+ value={(arr[k] ?? emptyElement()) as Content}
262
+ label={label != null ? `${label} ${k + 1}` : undefined}
263
+ onChange={(rt) => commitElement(k, rt)}
264
+ onKey={(e) => onElementKey(e, k)}
265
+ />
266
+ {:else if control === 'object'}
267
+ <textarea
268
+ class="qm-input qm-json qm-focus-ring"
269
+ aria-label={label != null ? `${label} ${k + 1}` : undefined}
270
+ value={JSON.stringify(arr[k] ?? {})}
271
+ onchange={(e) => {
272
+ try {
273
+ commitElement(k, JSON.parse((e.currentTarget as HTMLTextAreaElement).value));
274
+ } catch {
275
+ /* keep prior value on invalid JSON */
276
+ }
277
+ }}
278
+ ></textarea>
279
+ {:else}
280
+ <TextField
281
+ bind:this={els[id]}
282
+ value={String(arr[k] ?? '')}
283
+ label={label != null ? `${label} ${k + 1}` : undefined}
284
+ onCommit={(v) => commitElement(k, v)}
285
+ onKey={(e) => onElementKey(e, k)}
286
+ />
287
+ {/if}
288
+ <button
289
+ type="button"
290
+ class="qm-icon-btn qm-remove qm-focus-ring"
291
+ title={t.strings.arrayRemove}
292
+ onclick={() => remove(k)}><Icon name="minus" /></button
293
+ >
294
+ </div>
295
+ {/each}
296
+ </div>
286
297
  </div>
287
298
 
288
299
  <style>
@@ -302,6 +313,24 @@
302
313
  justify-content: space-between;
303
314
  gap: var(--_qm-space-2);
304
315
  }
316
+ /* The elements, in a box of their own: the arrival wash blooms here rather than over
317
+ the wrapper `Field` hands this component (`washBox`), which is the label's box too.
318
+ Positioned for the wash's inset child, and rounded to the rung a row's own box
319
+ draws, the way `.qm-field-control` is for every other control.
320
+
321
+ `display: none` when there are no elements, so the header does not stand a gap above
322
+ an empty box; a wash over it then paints nothing, which is what an array with
323
+ nothing in it has to show. */
324
+ .qm-array-rows {
325
+ position: relative;
326
+ display: flex;
327
+ flex-direction: column;
328
+ gap: var(--_qm-space);
329
+ border-radius: var(--_qm-radius-inner);
330
+ }
331
+ .qm-array-rows.empty {
332
+ display: none;
333
+ }
305
334
  /* A grid rather than a block: an `<input>` and a `<textarea>` are inline-level and
306
335
  would sit on a baseline, standing the row a descender taller than the box the slab
307
336
  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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
5
5
  "repository": {
6
6
  "type": "git",