@quillmark/svelte 0.1.0 → 0.2.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.
Files changed (64) hide show
  1. package/README.md +15 -11
  2. package/THEMING.md +42 -23
  3. package/dist/core/address.d.ts +39 -4
  4. package/dist/core/address.js +47 -19
  5. package/dist/core/codec/encode.js +4 -1
  6. package/dist/core/codec/field.d.ts +1 -1
  7. package/dist/core/codec/prose.css +41 -18
  8. package/dist/core/codec/table.js +3 -1
  9. package/dist/core/errors.d.ts +15 -5
  10. package/dist/core/index.d.ts +1 -1
  11. package/dist/core/index.js +5 -4
  12. package/dist/core/lifecycle.d.ts +21 -5
  13. package/dist/core/lifecycle.js +34 -8
  14. package/dist/core/rebind.svelte.d.ts +16 -2
  15. package/dist/core/rebind.svelte.js +26 -8
  16. package/dist/core/theme.css +50 -18
  17. package/dist/preset/recipes.css +55 -8
  18. package/dist/preset/scale.css +5 -4
  19. package/dist/preview/Preview.svelte +10 -9
  20. package/dist/preview/Preview.svelte.d.ts +5 -5
  21. package/dist/preview/bridge.d.ts +6 -4
  22. package/dist/preview/bridge.js +17 -7
  23. package/dist/preview/controller.d.ts +13 -6
  24. package/dist/preview/controller.js +5 -2
  25. package/dist/preview/geometry.d.ts +16 -1
  26. package/dist/preview/geometry.js +20 -0
  27. package/dist/preview/index.d.ts +1 -0
  28. package/dist/preview/index.js +7 -0
  29. package/dist/preview/overlay.d.ts +2 -1
  30. package/dist/preview/overlay.js +24 -9
  31. package/dist/preview/paint.js +4 -0
  32. package/dist/visual/ArrayField.svelte +26 -20
  33. package/dist/visual/ArrayField.svelte.d.ts +4 -3
  34. package/dist/visual/Card.svelte +21 -19
  35. package/dist/visual/Card.svelte.d.ts +8 -3
  36. package/dist/visual/DiagnosticList.svelte +1 -1
  37. package/dist/visual/EnumField.svelte +34 -14
  38. package/dist/visual/EnumField.svelte.d.ts +4 -3
  39. package/dist/visual/Field.svelte +87 -25
  40. package/dist/visual/Field.svelte.d.ts +11 -7
  41. package/dist/visual/FieldHint.svelte +59 -10
  42. package/dist/visual/FormatPopover.svelte +3 -1
  43. package/dist/visual/ObjectField.svelte +21 -2
  44. package/dist/visual/ObjectField.svelte.d.ts +3 -1
  45. package/dist/visual/ProseArrayElement.svelte +1 -6
  46. package/dist/visual/ProseArrayElement.svelte.d.ts +0 -1
  47. package/dist/visual/ProseField.svelte +20 -12
  48. package/dist/visual/ProseField.svelte.d.ts +5 -3
  49. package/dist/visual/TipsCard.svelte +1 -1
  50. package/dist/visual/VisualEditor.svelte +12 -13
  51. package/dist/visual/VisualEditor.svelte.d.ts +2 -3
  52. package/dist/visual/VisualEditorInner.svelte +140 -66
  53. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
  54. package/dist/visual/controls.css +14 -11
  55. package/dist/visual/diagnostics.d.ts +12 -20
  56. package/dist/visual/diagnostics.js +6 -18
  57. package/dist/visual/index.d.ts +1 -0
  58. package/dist/visual/index.js +7 -0
  59. package/dist/visual/leaves.d.ts +47 -0
  60. package/dist/visual/leaves.js +31 -0
  61. package/dist/visual/props.d.ts +27 -6
  62. package/dist/visual/tips.d.ts +3 -1
  63. package/dist/visual/tips.js +5 -2
  64. package/package.json +3 -5
@@ -1,3 +1,23 @@
1
+ /**
2
+ * An address's boxes: `fieldBoxes`'s union, else its own `regions()` rects, else the
3
+ * rects of everything UNDER it. The one rule the overlay and the scroll both read, so
4
+ * an address `regions()` names never draws nothing (PREVIEW.md §"Three
5
+ * responsibilities").
6
+ *
7
+ * `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
8
+ * its content and for a `richtext[]` element; runtime.d.ts says what such a field's
9
+ * box is, a single `regions()` rect. The third rung is that sentence one granularity
10
+ * down — an array's ink is its elements' — and is a fallback, not a union, so an
11
+ * address with rects of its own never also draws its children's. The `.` keeps the
12
+ * prefix a path boundary: `main.references` matches `main.references.0`, not
13
+ * `main.references_note`.
14
+ */
15
+ export function boxesForField(field, boxes, regions) {
16
+ if (boxes.length)
17
+ return boxes;
18
+ const own = regions.filter((r) => r.field === field);
19
+ return own.length ? own : regions.filter((r) => r.field.startsWith(`${field}.`));
20
+ }
1
21
  /**
2
22
  * Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
3
23
  * the page's displayed size across DPR and container resize for free; this is
@@ -1,3 +1,4 @@
1
+ import '../core/theme.css';
1
2
  export { createPreview } from './controller.js';
2
3
  export type { PreviewOptions, PreviewController } from './controller.js';
3
4
  export { default as Preview } from './Preview.svelte';
@@ -5,6 +5,13 @@
5
5
  // (the codec, the VisualEditor) so a `@quillmark/preview` promotion stays a
6
6
  // re-export, not a refactor: it may reach `/core` (the shared WASM boundary) and
7
7
  // nothing else. Enforced by `check:deps`, which walks the subpath's import graph.
8
+ // The theme derivation, which the page slots and the overlay read through `var()`.
9
+ // Imported at the BARREL because a subpath is what a consumer gets: this surface
10
+ // reaches modules inside `core/` and never its entry, so a sheet hanging off that
11
+ // entry arrives only for a consumer importing `/core` for some other reason. The
12
+ // sheet rather than the entry, because the barrel needs the derivation and not
13
+ // `init`. `check:deps` (rule 4) walks this file's own graph and holds the reach.
14
+ import '../core/theme.css';
8
15
  export { createPreview } from './controller.js';
9
16
  export { default as Preview } from './Preview.svelte';
10
17
  // What the preview SAYS when there is nothing to paint. Keyed and partial; the
@@ -3,7 +3,8 @@ import type { PageSlot } from './paint.js';
3
3
  export interface OverlayController {
4
4
  /** Re-read geometry and rebuild every box; call after a layout-affecting refresh. */
5
5
  refresh(): void;
6
- /** Bloom `field`'s boxes: transient, and a no-op when `field` is already the marked one. */
6
+ /** Bloom the boxes at `field` AND under it: transient, and a no-op when `field`
7
+ * is already the marked one. */
7
8
  flashField(field: string): void;
8
9
  destroy(): void;
9
10
  }
@@ -1,5 +1,5 @@
1
1
  import { bloom, bloomTiming, primeWash } from '../core/bloom.js';
2
- import { rectToPercent, applyPercentRect } from './geometry.js';
2
+ import { boxesForField, rectToPercent, applyPercentRect } from './geometry.js';
3
3
  const FIELD_ATTR = 'data-qm-field';
4
4
  export function createOverlay(session, slots) {
5
5
  const layers = slots.map((slot) => {
@@ -20,32 +20,47 @@ export function createOverlay(session, slots) {
20
20
  let flash;
21
21
  /** field → its boxes, rebuilt with them. The bloom's only lookup. */
22
22
  let byField = new Map();
23
+ // The boxes a flash covers: the address's own, plus every one UNDER it, since the
24
+ // boxes are keyed as `regions()` names them (`main.references.0`) and an
25
+ // editor-side signal names the declared field (`main.references`). Resolved per
26
+ // apply rather than at `flashField`, so a rebuild picks up boxes the recompile
27
+ // moved or added.
28
+ function flashed(field) {
29
+ const els = [];
30
+ for (const [key, group] of byField) {
31
+ if (key === field || key.startsWith(`${field}.`))
32
+ els.push(...group);
33
+ }
34
+ return els;
35
+ }
23
36
  // Start or RESUME the bloom on whatever boxes exist for it now: one path, because
24
37
  // starting is resuming at ~0. A field surfaces several boxes and they share
25
38
  // `startedAt`, so they bloom in step rather than shimmering unevenly.
26
39
  function applyFlash() {
27
- const els = flash && byField.get(flash.field);
28
- if (!flash || !els?.length)
40
+ if (!flash)
41
+ return;
42
+ const els = flashed(flash.field);
43
+ if (!els.length)
29
44
  return;
30
45
  const elapsed = performance.now() - flash.startedAt;
31
46
  const timing = bloomTiming(els[0]);
32
47
  for (const el of els)
33
48
  bloom(el, elapsed, timing);
34
49
  }
35
- // Field names come from `regions()` (the only session query that enumerates
36
- // them; Preview carries no schema); the boxes themselves come from
37
- // `fieldBoxes(field)`, which is content-only and `[]` for a scalar-reference
38
- // or widget field, so a nameless-of-content field contributes no boxes.
50
+ // Field names come from `regions()` (the only session query that enumerates them;
51
+ // Preview carries no schema); the rects from `boxesForField`, whose fallback is
52
+ // what makes every address `regions()` names draw.
39
53
  function build() {
40
54
  for (const layer of layers)
41
55
  layer.replaceChildren();
42
56
  byField = new Map();
57
+ const regions = session.regions();
43
58
  const fields = new Set();
44
- for (const region of session.regions())
59
+ for (const region of regions)
45
60
  fields.add(region.field);
46
61
  for (const field of fields) {
47
62
  const els = [];
48
- for (const box of session.fieldBoxes(field)) {
63
+ for (const box of boxesForField(field, session.fieldBoxes(field), regions)) {
49
64
  const layer = layers[box.page];
50
65
  const slot = slots[box.page];
51
66
  if (!layer || !slot)
@@ -43,6 +43,10 @@ onPaintError) {
43
43
  // the paper.
44
44
  const el = document.createElement('div');
45
45
  el.className = 'qm-page';
46
+ // The page's index, in the DOM. `pageByEl` is this module's and a consumer
47
+ // drawing an overlay of its own needs the number: position among siblings is
48
+ // the only handle without it, which is right today and is not a contract.
49
+ el.dataset.page = String(page);
46
50
  Object.assign(el.style, {
47
51
  position: 'relative',
48
52
  boxSizing: 'border-box',
@@ -55,20 +55,9 @@
55
55
  /** Where the description parks, for the group's `aria-describedby`. */
56
56
  descriptionId?: string;
57
57
  onCommit: (arr: unknown[]) => void;
58
- /** A prose element gained focus: joins the field in the focus federation. */
59
- onFocusEl?: () => void;
60
58
  }
61
- let {
62
- value,
63
- items,
64
- label,
65
- required,
66
- description,
67
- labelId,
68
- descriptionId,
69
- onCommit,
70
- onFocusEl
71
- }: Props = $props();
59
+ let { value, items, label, required, description, labelId, descriptionId, onCommit }: Props =
60
+ $props();
72
61
 
73
62
  // The ELEMENT control is the item schema's own; an array declaring no `items`
74
63
  // has text elements.
@@ -102,7 +91,14 @@
102
91
  // Every path that drops an id deletes its entry: `bind:this` teardown nulls the
103
92
  // VALUE on unmount and leaves the key, so a card that outlives its elements
104
93
  // accumulates one dead key per element ever created.
105
- const els: Record<string, { focus: () => void } | undefined> = {};
94
+ //
95
+ // `$state` for the BINDING's sake, not this component's: nothing here reads `els`
96
+ // reactively (every read is inside an event handler or a post-flush focus hop), but
97
+ // `bind:this` into a property of a plain object is a write Svelte cannot track, and
98
+ // it says so once per element per render. Thirteen lines on one memo's first paint,
99
+ // in the console a consumer is reading to find its own defects. Same shape
100
+ // {@link Card} keeps its header/panel refs in.
101
+ const els: Record<string, { focus: () => void } | undefined> = $state({});
106
102
  let addEl: HTMLButtonElement | undefined = $state();
107
103
  let rootEl: HTMLElement | undefined = $state();
108
104
 
@@ -151,10 +147,11 @@
151
147
  // key does: the button under the pointer is part of what it destroys.
152
148
  focusAfterFlush(next[Math.max(k - 1, 0)]);
153
149
  }
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 {
150
+ /** Take the caret: the FIRST element, or the add affordance when the list is empty;
151
+ * which is then the only thing there is to land on, and the next thing the user
152
+ * wants anyway. Reached by a label click and by the editor's landing verbs, which
153
+ * ask one function so they cannot disagree (`Field`, `leaves.ts`). */
154
+ export function focus(): void {
158
155
  if (ids.length === 0) return void addEl?.focus();
159
156
  const first = els[ids[0]];
160
157
  if (first) return first.focus();
@@ -162,6 +159,16 @@
162
159
  // nothing about focusing it that the DOM does not already know.
163
160
  rootEl?.querySelector<HTMLTextAreaElement>('.qm-array-row textarea')?.focus();
164
161
  }
162
+ /** Take the caret to element `k`: what a landing on an element ADDRESS resolves to
163
+ * (`leaves.ts`). The index resolves to the element's session id here, at the call,
164
+ * never carried as one — an index is stale the moment anything above it splices.
165
+ * Past the live list it falls back to {@link focus}: the field is right and the row
166
+ * is gone, which is a landing off a compile the document has moved past. */
167
+ export function focusElement(k: number): void {
168
+ const el = els[ids[k]];
169
+ if (el) el.focus();
170
+ else focus();
171
+ }
165
172
  /** Focus element `id` after the flush, never in the same tick: a mutation commits
166
173
  * the array BY VALUE, so the parent re-derives and the row does not exist until
167
174
  * then. `undefined` is the empty list: the add affordance.
@@ -216,7 +223,7 @@
216
223
  {label}
217
224
  id={labelId}
218
225
  {descriptionId}
219
- onActivate={activate}
226
+ onActivate={focus}
220
227
  {required}
221
228
  {description}
222
229
  />
@@ -237,7 +244,6 @@
237
244
  label={label != null ? `${label} ${k + 1}` : undefined}
238
245
  onChange={(rt) => commitElement(k, rt)}
239
246
  onKey={(e) => onElementKey(e, k)}
240
- {onFocusEl}
241
247
  />
242
248
  {:else if control === 'object'}
243
249
  <textarea
@@ -16,9 +16,10 @@ interface Props {
16
16
  /** Where the description parks, for the group's `aria-describedby`. */
17
17
  descriptionId?: string;
18
18
  onCommit: (arr: unknown[]) => void;
19
- /** A prose element gained focus: joins the field in the focus federation. */
20
- onFocusEl?: () => void;
21
19
  }
22
- declare const ArrayField: import("svelte").Component<Props, {}, "">;
20
+ declare const ArrayField: import("svelte").Component<Props, {
21
+ focus: () => void;
22
+ focusElement: (k: number) => void;
23
+ }, "">;
23
24
  type ArrayField = ReturnType<typeof ArrayField>;
24
25
  export default ArrayField;
@@ -16,7 +16,7 @@
16
16
  import Icon from './icons/Icon.svelte';
17
17
  import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
18
18
  import type { EditorErrorHandler } from '../core/errors.js';
19
- import type { FieldController } from '../core/codec/index.js';
19
+ import type { LeafRegistry } from './leaves.js';
20
20
  import type { CardModel, FieldModel } from './structure.js';
21
21
  import { placeFields, humanize, initialExpandedGroup } from './structure.js';
22
22
  import type { FieldDomIds } from './domid.js';
@@ -51,12 +51,17 @@
51
51
  isLast: boolean;
52
52
  kinds: string[];
53
53
  ops: CardOps;
54
+ /** How an option `ops.enumAllowed` refuses draws. Nothing about it is per-card,
55
+ * so it sits beside the bundle rather than in it. */
56
+ enumDisallowed?: 'hide' | 'disable';
54
57
  onFocus?: (addr: Addr) => void;
55
58
  onCaretMove?: (addr: Addr, pos: number) => void;
56
59
  onChange?: (addr: Addr) => void;
57
60
  onError?: EditorErrorHandler;
58
- register?: (key: string, controller: FieldController) => void;
59
- unregister?: (key: string) => void;
61
+ /** The editor's leaf registry (`leaves.ts`): every field in this card registers
62
+ * its landing handle, which is what the editor's `focusField`/`setCaret`
63
+ * resolve a `DocPath` to. */
64
+ leaves?: LeafRegistry;
60
65
  }
61
66
  let {
62
67
  card,
@@ -67,12 +72,12 @@
67
72
  isLast,
68
73
  kinds,
69
74
  ops,
75
+ enumDisallowed,
70
76
  onFocus,
71
77
  onCaretMove,
72
78
  onChange,
73
79
  onError,
74
- register,
75
- unregister
80
+ leaves
76
81
  }: Props = $props();
77
82
 
78
83
  // Local title reconcile (external change only), like the scalar controls.
@@ -229,10 +234,9 @@
229
234
  not a persistent box. `data-value` falls back to the
230
235
  placeholder so an empty title still reserves its resolved-title width. -->
231
236
  <span class="qm-card-title-sizer" data-value={localTitle || card.titlePlaceholder}>
232
- <!-- `qm-focus-ring` is what the box used to do: it was this control's ONLY
233
- focus indicator, and the ink step that replaced it cannot be one, since
234
- hover already takes that step. `:focus-visible`, so a press enters the
235
- edit unringed while a tab into it draws one. -->
237
+ <!-- `qm-focus-ring` is this control's ONLY focus indicator: the ink step
238
+ cannot be one, since hover already takes that step. `:focus-visible`, so
239
+ a press enters the edit unringed while a tab into it draws one. -->
236
240
  <input
237
241
  class="qm-card-title qm-focus-ring"
238
242
  value={localTitle}
@@ -361,8 +365,7 @@
361
365
  {onCaretMove}
362
366
  {onChange}
363
367
  {onError}
364
- {register}
365
- {unregister}
368
+ {leaves}
366
369
  />
367
370
  <DiagnosticList diagnostics={ops.diagFor(undefined)} />
368
371
  </div>
@@ -385,17 +388,17 @@
385
388
  provenance={card.provenance[f.name]}
386
389
  {doc}
387
390
  {quill}
388
- proseAddr={ops.makeAddr(f.name)}
391
+ addr={ops.makeAddr(f.name)}
389
392
  leafKey={ops.leafKey(f.name)}
390
393
  domIds={ops.domIds(f.name)}
391
394
  onCommitScalar={(v) => ops.commit(f.name, v)}
392
395
  optionAllowed={(v) => ops.enumAllowed(f.name, v)}
396
+ {enumDisallowed}
393
397
  {onFocus}
394
398
  {onCaretMove}
395
399
  {onChange}
396
400
  {onError}
397
- {register}
398
- {unregister}
401
+ {leaves}
399
402
  diagnostics={ops.diagFor(f.name)}
400
403
  />
401
404
  {/each}
@@ -636,9 +639,8 @@
636
639
  The ACTION COLUMN is minted here and spent one level down, on the field
637
640
  (`Field.svelte` holds why it is the field's inset and not this container's). It
638
641
  is declared on the query container because both of its consumers hang off one:
639
- the fields inside, and the capacity ramp below, which has to carry the column in
640
- its own arithmetic now that the query no longer reads a content box the
641
- reservation already narrowed. */
642
+ the fields inside, and the capacity ramp below, which carries the column in its
643
+ own arithmetic because the query reads a box the reservation has not narrowed. */
642
644
  .qm-section,
643
645
  .qm-group-panel-inner {
644
646
  --action-col: calc(var(--_qm-tap-min) + var(--_qm-space-2));
@@ -741,10 +743,10 @@
741
743
  padding: var(--_qm-space) 0 var(--_qm-space) var(--_qm-space);
742
744
  font: inherit;
743
745
  font-size: var(--_qm-text-label);
744
- font-weight: var(--_qm-weight-soft);
746
+ font-weight: var(--_qm-weight-label);
745
747
  line-height: var(--_qm-leading-tight);
746
748
  cursor: pointer;
747
- color: var(--_qm-ink-meta);
749
+ color: var(--_qm-ink-label);
748
750
  text-align: left;
749
751
  transition: color var(--_qm-duration-fast) var(--_qm-ease-reverse);
750
752
  }
@@ -1,6 +1,6 @@
1
1
  import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
2
2
  import type { EditorErrorHandler } from '../core/errors.js';
3
- import type { FieldController } from '../core/codec/index.js';
3
+ import type { LeafRegistry } from './leaves.js';
4
4
  import type { CardModel } from './structure.js';
5
5
  import type { FieldDomIds } from './domid.js';
6
6
  /** The per-card operation bundle the VisualEditor builds (all resolve id→index lazily). */
@@ -28,12 +28,17 @@ interface Props {
28
28
  isLast: boolean;
29
29
  kinds: string[];
30
30
  ops: CardOps;
31
+ /** How an option `ops.enumAllowed` refuses draws. Nothing about it is per-card,
32
+ * so it sits beside the bundle rather than in it. */
33
+ enumDisallowed?: 'hide' | 'disable';
31
34
  onFocus?: (addr: Addr) => void;
32
35
  onCaretMove?: (addr: Addr, pos: number) => void;
33
36
  onChange?: (addr: Addr) => void;
34
37
  onError?: EditorErrorHandler;
35
- register?: (key: string, controller: FieldController) => void;
36
- unregister?: (key: string) => void;
38
+ /** The editor's leaf registry (`leaves.ts`): every field in this card registers
39
+ * its landing handle, which is what the editor's `focusField`/`setCaret`
40
+ * resolve a `DocPath` to. */
41
+ leaves?: LeafRegistry;
37
42
  }
38
43
  declare const Card: import("svelte").Component<Props, {
39
44
  revealLeaf: (key: string) => void;
@@ -41,7 +41,7 @@
41
41
  gap: var(--_qm-space-half);
42
42
  }
43
43
  .qm-diag-line {
44
- font-size: var(--_qm-text-meta);
44
+ font-size: var(--_qm-text-label);
45
45
  color: var(--_qm-warning);
46
46
  }
47
47
  .qm-diag-line[data-severity='error'] {
@@ -34,13 +34,23 @@
34
34
  /** The parked `description` (FieldLabel): announced after the name. */
35
35
  describedBy?: string;
36
36
  onCommit: (v: string | undefined) => void;
37
- /** Consumer policy: `false` disables an option so it can't be picked.
38
- * A disallowed value already authored stays SELECTED and visible (disabled), never
39
- * stripped or mutated. The UNSET sentinel is exempt: clear-to-default always works. */
37
+ /** Consumer policy: `false` marks an option unavailable, so it can't be picked.
38
+ * The UNSET sentinel is exempt: clear-to-default always works. */
40
39
  optionAllowed?: (value: string) => boolean;
40
+ /** How a refused option draws: greyed and unpickable, or left out of the list. */
41
+ enumDisallowed?: 'hide' | 'disable';
41
42
  }
42
- let { value, values, fallback, label, id, describedBy, onCommit, optionAllowed }: Props =
43
- $props();
43
+ let {
44
+ value,
45
+ values,
46
+ fallback,
47
+ label,
48
+ id,
49
+ describedBy,
50
+ onCommit,
51
+ optionAllowed,
52
+ enumDisallowed = 'disable'
53
+ }: Props = $props();
44
54
 
45
55
  // The sentinel's option value: a namespaced marker that no schema-authored
46
56
  // enum member would ever be (`values` are classification markings, seal ids, and
@@ -107,15 +117,25 @@
107
117
  <Select.Item class="qm-menu-item qm-select-item" value={UNSET} label={ghostText}>
108
118
  <span class="qm-select-ghost">{ghostText}</span>
109
119
  </Select.Item>
120
+ <!-- A refused option still draws under `'disable'`, and under EITHER
121
+ policy when it is the one selected: a listbox whose selected value
122
+ has no row shows nothing for what the document says, and offers none
123
+ for the primitive to mark, type-ahead to, or key onto
124
+ (VISUAL_EDITOR §"Enum policy"). Off `local.value`, not `value`: the
125
+ row that exists is the row the control has selected, through the
126
+ window where an own pick has not reconciled back. -->
110
127
  {#each values as v (v)}
111
- <Select.Item
112
- class="qm-menu-item qm-select-item"
113
- value={v}
114
- label={dash(v)}
115
- disabled={optionAllowed?.(v) === false}
116
- >
117
- {dash(v)}
118
- </Select.Item>
128
+ {@const allowed = optionAllowed?.(v) !== false}
129
+ {#if allowed || enumDisallowed === 'disable' || v === local.value}
130
+ <Select.Item
131
+ class="qm-menu-item qm-select-item"
132
+ value={v}
133
+ label={dash(v)}
134
+ disabled={!allowed}
135
+ >
136
+ {dash(v)}
137
+ </Select.Item>
138
+ {/if}
119
139
  {/each}
120
140
  </Select.Viewport>
121
141
  </div>
@@ -158,7 +178,7 @@
158
178
  .qm-select-content :global(.qm-select-item[data-selected]) {
159
179
  font-weight: var(--_qm-weight-label);
160
180
  }
161
- /* Consumer policy: offered but unpickable, never hidden. */
181
+ /* Consumer policy, for a refused option the list still draws: offered, unpickable. */
162
182
  .qm-select-content :global(.qm-select-item[data-disabled]) {
163
183
  opacity: var(--_qm-opacity-muted);
164
184
  cursor: default;
@@ -15,10 +15,11 @@ interface Props {
15
15
  /** The parked `description` (FieldLabel): announced after the name. */
16
16
  describedBy?: string;
17
17
  onCommit: (v: string | undefined) => void;
18
- /** Consumer policy: `false` disables an option so it can't be picked.
19
- * A disallowed value already authored stays SELECTED and visible (disabled), never
20
- * stripped or mutated. The UNSET sentinel is exempt: clear-to-default always works. */
18
+ /** Consumer policy: `false` marks an option unavailable, so it can't be picked.
19
+ * The UNSET sentinel is exempt: clear-to-default always works. */
21
20
  optionAllowed?: (value: string) => boolean;
21
+ /** How a refused option draws: greyed and unpickable, or left out of the list. */
22
+ enumDisallowed?: 'hide' | 'disable';
22
23
  }
23
24
  declare const EnumField: import("svelte").Component<Props, {}, "">;
24
25
  type EnumField = ReturnType<typeof EnumField>;