@quillmark/svelte 0.1.1 → 0.3.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 (124) hide show
  1. package/README.md +16 -12
  2. package/THEMING.md +48 -25
  3. package/dist/core/address.d.ts +42 -7
  4. package/dist/core/address.js +49 -21
  5. package/dist/core/bloom.d.ts +5 -5
  6. package/dist/core/bloom.js +8 -8
  7. package/dist/core/codec/anchor.d.ts +1 -1
  8. package/dist/core/codec/anchor.js +1 -1
  9. package/dist/core/codec/atoms.d.ts +10 -0
  10. package/dist/core/codec/atoms.js +78 -0
  11. package/dist/core/codec/clipboard.d.ts +59 -0
  12. package/dist/core/codec/clipboard.js +202 -0
  13. package/dist/core/codec/code.d.ts +1 -1
  14. package/dist/core/codec/code.js +4 -4
  15. package/dist/core/codec/decode.d.ts +1 -1
  16. package/dist/core/codec/decode.js +8 -8
  17. package/dist/core/codec/encode.d.ts +4 -4
  18. package/dist/core/codec/encode.js +14 -11
  19. package/dist/core/codec/field.d.ts +22 -27
  20. package/dist/core/codec/field.js +33 -32
  21. package/dist/core/codec/index.js +5 -8
  22. package/dist/core/codec/inputrules.js +56 -18
  23. package/dist/core/codec/islands.d.ts +11 -7
  24. package/dist/core/codec/islands.js +16 -10
  25. package/dist/core/codec/keymap.d.ts +5 -6
  26. package/dist/core/codec/keymap.js +8 -7
  27. package/dist/core/codec/lists.d.ts +3 -3
  28. package/dist/core/codec/lists.js +32 -13
  29. package/dist/core/codec/marks.d.ts +1 -1
  30. package/dist/core/codec/marks.js +1 -1
  31. package/dist/core/codec/positions.js +1 -1
  32. package/dist/core/codec/prose.css +266 -228
  33. package/dist/core/codec/schema.js +4 -4
  34. package/dist/core/codec/slash.d.ts +39 -41
  35. package/dist/core/codec/slash.js +0 -0
  36. package/dist/core/codec/table-view.d.ts +12 -16
  37. package/dist/core/codec/table-view.js +488 -292
  38. package/dist/core/codec/table.d.ts +42 -15
  39. package/dist/core/codec/table.js +73 -27
  40. package/dist/core/errors.d.ts +17 -7
  41. package/dist/core/index.d.ts +1 -1
  42. package/dist/core/index.js +6 -5
  43. package/dist/core/lifecycle.d.ts +21 -5
  44. package/dist/core/lifecycle.js +34 -8
  45. package/dist/core/rebind.svelte.d.ts +18 -4
  46. package/dist/core/rebind.svelte.js +30 -12
  47. package/dist/core/teardown.d.ts +1 -1
  48. package/dist/core/teardown.js +1 -1
  49. package/dist/core/theme.css +173 -58
  50. package/dist/preset/index.css +1 -1
  51. package/dist/preset/recipes.css +79 -30
  52. package/dist/preset/scale.css +40 -28
  53. package/dist/preview/Preview.svelte +22 -19
  54. package/dist/preview/Preview.svelte.d.ts +8 -8
  55. package/dist/preview/bridge.d.ts +6 -4
  56. package/dist/preview/bridge.js +19 -9
  57. package/dist/preview/controller.d.ts +13 -6
  58. package/dist/preview/controller.js +8 -5
  59. package/dist/preview/geometry.d.ts +17 -2
  60. package/dist/preview/geometry.js +21 -1
  61. package/dist/preview/index.d.ts +1 -0
  62. package/dist/preview/index.js +9 -2
  63. package/dist/preview/overlay.d.ts +2 -1
  64. package/dist/preview/overlay.js +26 -11
  65. package/dist/preview/paint.js +19 -17
  66. package/dist/visual/ArrayField.svelte +103 -83
  67. package/dist/visual/ArrayField.svelte.d.ts +5 -4
  68. package/dist/visual/BooleanField.svelte +8 -6
  69. package/dist/visual/BooleanField.svelte.d.ts +2 -2
  70. package/dist/visual/Card.svelte +161 -333
  71. package/dist/visual/Card.svelte.d.ts +5 -4
  72. package/dist/visual/CardControls.svelte +1 -1
  73. package/dist/visual/DateField.svelte +19 -19
  74. package/dist/visual/DateField.svelte.d.ts +3 -3
  75. package/dist/visual/DiagnosticList.svelte +2 -2
  76. package/dist/visual/EnumField.svelte +48 -28
  77. package/dist/visual/EnumField.svelte.d.ts +5 -4
  78. package/dist/visual/Field.svelte +207 -132
  79. package/dist/visual/Field.svelte.d.ts +11 -7
  80. package/dist/visual/FieldHint.svelte +79 -48
  81. package/dist/visual/FieldHint.svelte.d.ts +1 -1
  82. package/dist/visual/FieldLabel.svelte +8 -8
  83. package/dist/visual/FormatPopover.svelte +82 -35
  84. package/dist/visual/NumberField.svelte +5 -5
  85. package/dist/visual/NumberField.svelte.d.ts +1 -1
  86. package/dist/visual/ObjectField.svelte +28 -9
  87. package/dist/visual/ObjectField.svelte.d.ts +4 -2
  88. package/dist/visual/ProseArrayElement.svelte +12 -19
  89. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -2
  90. package/dist/visual/ProseField.svelte +62 -85
  91. package/dist/visual/ProseField.svelte.d.ts +14 -17
  92. package/dist/visual/SlashMenu.svelte +42 -16
  93. package/dist/visual/TextField.svelte +5 -5
  94. package/dist/visual/TextField.svelte.d.ts +1 -1
  95. package/dist/visual/TipsCard.svelte +43 -28
  96. package/dist/visual/VisualEditor.svelte +18 -19
  97. package/dist/visual/VisualEditor.svelte.d.ts +2 -3
  98. package/dist/visual/VisualEditorInner.svelte +251 -184
  99. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
  100. package/dist/visual/controls.css +85 -38
  101. package/dist/visual/diagnostics.d.ts +13 -21
  102. package/dist/visual/diagnostics.js +7 -19
  103. package/dist/visual/domid.js +3 -3
  104. package/dist/visual/ext.d.ts +1 -1
  105. package/dist/visual/ext.js +1 -1
  106. package/dist/visual/icons/nodes.d.ts +3 -0
  107. package/dist/visual/icons/nodes.js +2 -1
  108. package/dist/visual/index.d.ts +1 -0
  109. package/dist/visual/index.js +10 -3
  110. package/dist/visual/leaves.d.ts +47 -0
  111. package/dist/visual/leaves.js +31 -0
  112. package/dist/visual/links.d.ts +24 -0
  113. package/dist/visual/links.js +67 -0
  114. package/dist/visual/props.d.ts +33 -12
  115. package/dist/visual/signals.d.ts +6 -6
  116. package/dist/visual/strings.d.ts +14 -19
  117. package/dist/visual/strings.js +5 -7
  118. package/dist/visual/structure.d.ts +24 -17
  119. package/dist/visual/structure.js +8 -8
  120. package/dist/visual/synced.svelte.d.ts +1 -1
  121. package/dist/visual/synced.svelte.js +4 -4
  122. package/dist/visual/tips.d.ts +4 -2
  123. package/dist/visual/tips.js +7 -4
  124. package/package.json +2 -2
@@ -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). */
@@ -21,19 +21,20 @@ interface CardOps {
21
21
  interface Props {
22
22
  card: CardModel;
23
23
  doc: Document;
24
- /** The schema a prose leaf reads its content through (`ProseField`). */
25
24
  quill: Quill;
26
25
  index: number;
27
26
  isFirst: boolean;
28
27
  isLast: boolean;
29
28
  kinds: string[];
30
29
  ops: CardOps;
30
+ enumDisallowed?: 'hide' | 'disable';
31
31
  onFocus?: (addr: Addr) => void;
32
32
  onCaretMove?: (addr: Addr, pos: number) => void;
33
33
  onChange?: (addr: Addr) => void;
34
34
  onError?: EditorErrorHandler;
35
- register?: (key: string, controller: FieldController) => void;
36
- unregister?: (key: string) => void;
35
+ /** Every field in this card registers its landing handle here, which is what the
36
+ * editor's `focusField`/`setCaret` resolve a `DocPath` to. */
37
+ leaves?: LeafRegistry;
37
38
  }
38
39
  declare const Card: import("svelte").Component<Props, {
39
40
  revealLeaf: (key: string) => void;
@@ -2,7 +2,7 @@
2
2
  Card controls, composable cards only.
3
3
  Right-aligned in the header: a move-up/move-down chevron pair revealed while the
4
4
  pointer or the caret is in the card (each disabled at its edge), then an
5
- always-visible delete. Reorder is BUTTONS, not drag (V1). The card scopes that
5
+ always-visible delete. Reorder is buttons, not drag (V1). The card scopes that
6
6
  reveal, so its condition lives in the parent's CSS; the pair keeps the narrower
7
7
  guarantee here, that a focused chevron is a visible one. The card also hangs this
8
8
  cluster to land these glyphs on its right gutter, and reads `--_qm-glyph-control`
@@ -11,26 +11,26 @@
11
11
  calendar) rather than `DatePicker`: the segments are the entry affordance, and a
12
12
  calendar is a second surface this field does not need.
13
13
 
14
- THE BOUNDARY IS A STRING, AND THE LOCAL IS TOO. The primitive speaks
14
+ The boundary is A string, and the local is too. The primitive speaks
15
15
  `CalendarDate`; the document speaks `YYYY-MM-DD`. `CalendarDate` carries no time
16
16
  and no zone, so the round-trip is lossless and no local-midnight shift can occur:
17
17
  the hazard that makes `new Date('2026-07-25')` the wrong tool here. Authored
18
- values are data, not input: `parseDate` THROWS on anything malformed, so a bad
18
+ values are data, not input: `parseDate` throws on anything malformed, so a bad
19
19
  string degrades to an empty field rather than taking the editor down with it.
20
20
 
21
- `syncedLocal` reconciles by IDENTITY, so the local must hold the string, not the
21
+ `syncedLocal` reconciles by identity, so the local must hold the string, not the
22
22
  parsed value: a fresh `CalendarDate` is never `===` the last one, which would
23
23
  make every reconcile fire and re-render all seven segments on each commit.
24
24
 
25
- THE GHOST IS THE DEFAULT'S DIGITS, NOT A FORMAT HINT. An unset field
25
+ The ghost is the default'S digits, not A format hint. An unset field
26
26
  carrying a `default:` prints the default's digits in the segments, ghost-toned,
27
27
  instead of the primitive's `mm`/`dd`/`yyyy` hints, which say "empty" where the
28
- rung says "will render 2026-01-01". The ghost is painted in the SEGMENT SNIPPET,
28
+ rung says "will render 2026-01-01". The ghost is painted in the segment snippet,
29
29
  over an unset primitive: substituting the default for `value` instead would make
30
30
  the field indistinguishable from an authored one to every path that reads it
31
31
  (`areAllSegmentsFilled`, Backspace, the hidden input), and the primitive shadows
32
32
  a written-back `value` prop it was not `bind:`-ed to, so re-seating the ghost
33
- after a clear never lands. `placeholder` (the `DateValue` the segments COUNT
33
+ after a clear never lands. `placeholder` (the `DateValue` the segments count
34
34
  from, never one they display) carries the default too, so arrowing an empty
35
35
  segment starts at the render's date rather than today's.
36
36
  -->
@@ -45,15 +45,15 @@
45
45
  /** The resolved `default:` in the boundary's currency (`YYYY-MM-DD`): parsed
46
46
  * for display only, shown while unset, never written. */
47
47
  fallback?: string;
48
- /** Accessible name for a field NOTHING else names: an object property, whose
48
+ /** Accessible name for a field nothing else names: an object property, whose
49
49
  * name is the field label plus the property's. A field's own date takes
50
50
  * `labelledBy` instead. */
51
51
  label?: string;
52
- /** The field label's own id. `for` CANNOT reach this control: the segment
52
+ /** The field label's own id. `for` cannot reach this control: the segment
53
53
  * container is not a labelable element; so the association runs the other way,
54
54
  * and the label's click comes back through {@link focus}. It lands on the
55
55
  * container, which the primitive gives `role="group"`: the name belongs to the
56
- * SET of segments, and entering any of them announces it. (bits builds each
56
+ * set of segments, and entering any of them announces it. (bits builds each
57
57
  * segment's own `aria-labelledby` from a `DateField.Label` inside its root:
58
58
  * unreachable from here, since the field's label is a grid child of the field,
59
59
  * not of the control.) */
@@ -66,7 +66,7 @@
66
66
 
67
67
  let wrapEl: HTMLElement | undefined = $state();
68
68
  /** Take the caret: what the label click, and a parent placing focus here, calls.
69
- * The FIRST segment, not the field: focus lives on a segment (which is why the
69
+ * The first segment, not the field: focus lives on a segment (which is why the
70
70
  * ring is `.qm-focus-ring-within`), and the container holds none. `literal` is the
71
71
  * separator between segments: present, never focusable. */
72
72
  export function focus(): void {
@@ -85,14 +85,14 @@
85
85
  }
86
86
  }
87
87
 
88
- // Local value synced to `value` as a STRING (see the identity note above);
88
+ // Local value synced to `value` as a string (see the identity note above);
89
89
  // own-edits stay local, only an external change reconciles back in. Driven
90
- // CONTROLLED (`value` + `onValueChange`, never `bind:`) so reconciliation stays
90
+ // controlled (`value` + `onValueChange`, never `bind:`) so reconciliation stays
91
91
  // the package's.
92
92
  const local = syncedLocal(() => value?.slice(0, 10) ?? '');
93
93
  const parsed = $derived(toDateValue(local.value));
94
94
  // The date the empty segments ghost, or undefined when there is nothing to ghost:
95
- // the field is unset AND the default has a date form. A non-blank local that
95
+ // the field is unset and the default has a date form. A non-blank local that
96
96
  // fails to parse is AUTHORED-but-malformed, which the empty field states
97
97
  // honestly: the ghost would claim it unset. Held as the parsed value rather than
98
98
  // a boolean so the substitution below narrows on the one fact it needs.
@@ -101,10 +101,10 @@
101
101
 
102
102
  // What one segment prints, and whether that text is shown-never-written.
103
103
  //
104
- // An UNFILLED segment is always shown-never-written, whether it prints the
104
+ // An unfilled segment is always shown-never-written, whether it prints the
105
105
  // default's digits or the primitive's own `mm`/`dd`/`yyyy` hint: both state
106
106
  // "nothing authored here". Only an unfilled segment ghosts: a half-entered date
107
- // holds digits while the VALUE is still undefined (one unfilled segment unsets
107
+ // holds digits while the value is still undefined (one unfilled segment unsets
108
108
  // the whole field), so ghosting unconditionally would paint over, and dim, the
109
109
  // digits just typed. The segment's own text is the tell: its unfilled hint is
110
110
  // alphabetic in every locale bits ships (`mm`/`yyyy`, `аа`, `年`), a filled one
@@ -142,7 +142,7 @@
142
142
  >
143
143
  <!-- `data-ghosted` states the rung the way the enum trigger does. The date
144
144
  primitive emits no per-segment placeholder marker (only bits' `select`
145
- does), so the TONE rides the same attribute, set per segment from the
145
+ does), so the tone rides the same attribute, set per segment from the
146
146
  substitution itself; which is also what keeps a half-typed date's own
147
147
  digits at full ink while the segments around them ghost. -->
148
148
  <BitsDateField.Input
@@ -153,7 +153,7 @@
153
153
  data-ghosted={ghost ? '' : undefined}
154
154
  >
155
155
  {#snippet children({ segments })}
156
- <!-- Keyed by INDEX: `part` repeats (the `literal` separators between
156
+ <!-- Keyed by index: `part` repeats (the `literal` separators between
157
157
  segments all carry it) so a part-keyed block collides. -->
158
158
  {#each segments as seg, i (i)}
159
159
  {@const shown = segmentText(seg.part, seg.value)}
@@ -171,7 +171,7 @@
171
171
  </span>
172
172
 
173
173
  <style>
174
- /* A primitive renders its OWN element, which a scoped selector cannot reach:
174
+ /* A primitive renders its own element, which a scoped selector cannot reach:
175
175
  styled through the wrapper with `:global`. */
176
176
  /* The box is `.qm-control-box` (controls.css), carried on the primitive's own
177
177
  element beside `.qm-focus-ring-within`; the segments inherit its size and ink
@@ -183,7 +183,7 @@
183
183
  box-sizing: border-box;
184
184
  }
185
185
  /* The ring rides `.qm-focus-ring-within` (controls.css) rather than the plain
186
- marker: focus lives on the SEGMENT, so it rings the field, not the segment
186
+ marker: focus lives on the segment, so it rings the field, not the segment
187
187
  the caret happens to be in. */
188
188
  .qm-date-wrap :global(.qm-date-segment) {
189
189
  padding: 0 var(--_qm-space-half);
@@ -4,15 +4,15 @@ interface Props {
4
4
  /** The resolved `default:` in the boundary's currency (`YYYY-MM-DD`): parsed
5
5
  * for display only, shown while unset, never written. */
6
6
  fallback?: string;
7
- /** Accessible name for a field NOTHING else names: an object property, whose
7
+ /** Accessible name for a field nothing else names: an object property, whose
8
8
  * name is the field label plus the property's. A field's own date takes
9
9
  * `labelledBy` instead. */
10
10
  label?: string;
11
- /** The field label's own id. `for` CANNOT reach this control: the segment
11
+ /** The field label's own id. `for` cannot reach this control: the segment
12
12
  * container is not a labelable element; so the association runs the other way,
13
13
  * and the label's click comes back through {@link focus}. It lands on the
14
14
  * container, which the primitive gives `role="group"`: the name belongs to the
15
- * SET of segments, and entering any of them announces it. (bits builds each
15
+ * set of segments, and entering any of them announces it. (bits builds each
16
16
  * segment's own `aria-labelledby` from a `DateField.Label` inside its root:
17
17
  * unreachable from here, since the field's label is a grid child of the field,
18
18
  * not of the control.) */
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  Inline diagnostic display (VISUAL_EDITOR §Diagnostics), severity-styled via
3
- CSS vars, NON-GATING: purely additive, never disables the field it sits
3
+ CSS vars, non-gating: purely additive, never disables the field it sits
4
4
  under. Shared by `Field.svelte` (a scalar/prose field) and `Card.svelte` (the
5
5
  body leaf, which has no `Field.svelte` wrapper), so a card's body diagnostics
6
6
  render the same way a field's do.
@@ -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'] {
@@ -1,9 +1,9 @@
1
1
  <!--
2
2
  A `string`+`enum` (or `type: 'enum'`) field → a styled listbox over `enum ??
3
- values`, on bits-ui. When nothing is authored the list shows a distinct UNSET
4
- sentinel that GHOSTS the `default:` (muted, shown-never-written), distinguishable
3
+ values`, on bits-ui. When nothing is authored the list shows a distinct unset
4
+ sentinel that ghosts the `default:` (muted, shown-never-written), distinguishable
5
5
  from an authored pick and re-selectable; so re-picking the default fires a
6
- change. The sentinel commits nothing; any real pick: INCLUDING the
6
+ change. The sentinel commits nothing; any real pick: including the
7
7
  value that equals the default: commits via the parent's typed `writer.set`.
8
8
  Explicitly picking the default is the one place "commit the default" is genuine
9
9
  intent, expressible. The sentinel stays in the list once a value is authored, as
@@ -22,7 +22,7 @@
22
22
  value: string | undefined;
23
23
  values: string[];
24
24
  fallback?: string;
25
- /** Accessible name for a trigger NOTHING else names: an object property, whose
25
+ /** Accessible name for a trigger nothing else names: an object property, whose
26
26
  * name is the field label plus the property's. A field's own trigger takes `id`
27
27
  * instead and is named by the `<label for>` beside it (the trigger is a
28
28
  * `<button>`, so `for` reaches it and a label click opens the list: what the
@@ -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
@@ -49,7 +59,7 @@
49
59
 
50
60
  // Local selection synced to `value` (sentinel when unauthored); own-picks stay
51
61
  // local, only an external change reconciles back in (see `syncedLocal`). Driven
52
- // CONTROLLED (`value` + `onValueChange`, never `bind:`) so reconciliation stays
62
+ // controlled (`value` + `onValueChange`, never `bind:`) so reconciliation stays
53
63
  // the package's rather than the primitive's.
54
64
  const local = syncedLocal(() => value ?? UNSET);
55
65
 
@@ -61,16 +71,16 @@
61
71
  /** What the closed trigger shows: the pick, or the ghosted default while unset. */
62
72
  const shown = $derived(unset ? ghostText : dash(local.value));
63
73
 
64
- /** The root to portal INTO: `document.body` would escape the consumer's dials
74
+ /** The root to portal into: `document.body` would escape the consumer's dials
65
75
  * along with the editor's subtree. `undefined` falls back to bits-ui's default. */
66
76
  let wrapEl = $state<HTMLElement | undefined>(undefined);
67
77
  const portalTarget = $derived(wrapEl?.closest<HTMLElement>('[data-qm-root]') ?? undefined);
68
78
  </script>
69
79
 
70
80
  <span class="qm-select-wrap" bind:this={wrapEl}>
71
- <!-- `allowDeselect={false}`: the UNSET sentinel is the clear-to-default
81
+ <!-- `allowDeselect={false}`: the unset sentinel is the clear-to-default
72
82
  affordance, so the primitive's own deselect must stay off. It reports a
73
- deselect as `''`: INDISTINGUISHABLE from picking the empty-string enum
83
+ deselect as `''`: indistinguishable from picking the empty-string enum
74
84
  member the reference quill actually declares. -->
75
85
  <Select.Root
76
86
  type="single"
@@ -96,26 +106,36 @@
96
106
  </Select.Trigger>
97
107
  <Select.Portal to={portalTarget}>
98
108
  <Select.Content sideOffset={4}>
99
- <!-- The list PORTALS out of the trigger's DOM but INTO the nearest
109
+ <!-- The list portals out of the trigger's DOM but into the nearest
100
110
  `[data-qm-root]`, and carries the marker itself, like FormatPopover.
101
111
  The pill is this
102
112
  element (not the primitive's) because scoped CSS keys off which
103
- component OWNS the markup: a `class` passed to a primitive is a
113
+ component owns the markup: a `class` passed to a primitive is a
104
114
  plain string and never picks up the scoping hash. -->
105
115
  <div class="qm-menu-surface qm-select-content" data-qm-root>
106
116
  <Select.Viewport>
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>
@@ -125,10 +145,10 @@
125
145
  </span>
126
146
 
127
147
  <style>
128
- /* A primitive renders its OWN element, which a scoped selector cannot reach:
148
+ /* A primitive renders its own element, which a scoped selector cannot reach:
129
149
  styled through the wrapper with `:global`. */
130
150
  /* The box is `.qm-control-box` (controls.css), carried on the primitive's own
131
- element the same way `.qm-focus-ring` is; only what a TRIGGER adds over a typed
151
+ element the same way `.qm-focus-ring` is; only what a trigger adds over a typed
132
152
  value's box is here. */
133
153
  .qm-select-wrap :global(.qm-select) {
134
154
  display: flex;
@@ -147,7 +167,7 @@
147
167
  color: var(--_qm-ink-ghost);
148
168
  }
149
169
  /* The open list is `.qm-menu-surface` and its rows `.qm-menu-item` (controls.css):
150
- the lift, the inset and the highlight are the shared menu recipe. What a LISTBOX
170
+ the lift, the inset and the highlight are the shared menu recipe. What a listbox
151
171
  adds over a menu is here: it spans its trigger rather than its own content, it
152
172
  scrolls past a screenful, and it marks the value already stored. */
153
173
  .qm-select-content {
@@ -156,9 +176,9 @@
156
176
  overflow-y: auto;
157
177
  }
158
178
  .qm-select-content :global(.qm-select-item[data-selected]) {
159
- font-weight: var(--_qm-weight-label);
179
+ font-weight: var(--_qm-weight-strong);
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;
@@ -3,7 +3,7 @@ interface Props {
3
3
  value: string | undefined;
4
4
  values: string[];
5
5
  fallback?: string;
6
- /** Accessible name for a trigger NOTHING else names: an object property, whose
6
+ /** Accessible name for a trigger nothing else names: an object property, whose
7
7
  * name is the field label plus the property's. A field's own trigger takes `id`
8
8
  * instead and is named by the `<label for>` beside it (the trigger is a
9
9
  * `<button>`, so `for` reaches it and a label click opens the list: what the
@@ -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>;