@lostgradient/cinder 0.9.0 → 0.12.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 (169) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/components.json +82 -4
  3. package/dist/components/access-gate/index.js +381 -239
  4. package/dist/components/chat/builders.d.ts +17 -8
  5. package/dist/components/chat/conversation-model.d.ts +1 -1
  6. package/dist/components/chat/index.d.ts +1 -1
  7. package/dist/components/chat/index.js +300 -17
  8. package/dist/components/chat/utilities/conversation.d.ts +6 -6
  9. package/dist/components/chat-conversation-header/index.js +19 -6
  10. package/dist/components/chat-conversation-list/index.js +19 -6
  11. package/dist/components/code-block/code-block.css +15 -9
  12. package/dist/components/code-block/code-block.variables.js +8 -2
  13. package/dist/components/confirm-dialog/confirm-dialog.css +2 -0
  14. package/dist/components/confirm-dialog/confirm-dialog.schema.js +10 -1
  15. package/dist/components/confirm-dialog/confirm-dialog.types.d.ts +7 -0
  16. package/dist/components/confirm-dialog/index.js +478 -106
  17. package/dist/components/connection-indicator/connection-indicator.css +136 -0
  18. package/dist/components/connection-indicator/connection-indicator.css.d.ts +1 -0
  19. package/dist/components/connection-indicator/connection-indicator.schema.d.ts +3 -0
  20. package/dist/components/connection-indicator/connection-indicator.schema.js +89 -0
  21. package/dist/components/connection-indicator/connection-indicator.svelte.d.ts +21 -0
  22. package/dist/components/connection-indicator/connection-indicator.types.d.ts +46 -0
  23. package/dist/components/connection-indicator/connection-indicator.variables.d.ts +2 -0
  24. package/dist/components/connection-indicator/connection-indicator.variables.js +61 -0
  25. package/dist/components/connection-indicator/index.d.ts +5 -0
  26. package/dist/components/connection-indicator/index.js +409 -0
  27. package/dist/components/container/container.schema.js +4 -4
  28. package/dist/components/faceted-filter-bar/faceted-filter-bar.schema.js +5 -1
  29. package/dist/components/faceted-filter-bar/faceted-filter-bar.types.d.ts +2 -0
  30. package/dist/components/faceted-filter-bar/index.js +40 -31
  31. package/dist/components/invocation-rule-builder/index.d.ts +1 -1
  32. package/dist/components/invocation-rule-builder/index.js +305 -150
  33. package/dist/components/invocation-rule-builder/invocation-rule-builder.css +28 -0
  34. package/dist/components/invocation-rule-builder/invocation-rule-builder.schema.js +89 -5
  35. package/dist/components/invocation-rule-builder/invocation-rule-builder.svelte.d.ts +2 -1
  36. package/dist/components/invocation-rule-builder/invocation-rule-builder.types.d.ts +114 -16
  37. package/dist/components/navigation-bar/index.js +11 -17
  38. package/dist/components/run-step-timeline/index.d.ts +1 -1
  39. package/dist/components/run-step-timeline/index.js +1052 -560
  40. package/dist/components/run-step-timeline/run-step-branch-disclosure.svelte.d.ts +17 -0
  41. package/dist/components/run-step-timeline/run-step-timeline.css +124 -0
  42. package/dist/components/run-step-timeline/run-step-timeline.schema.js +873 -350
  43. package/dist/components/run-step-timeline/run-step-timeline.svelte.d.ts +1 -15
  44. package/dist/components/run-step-timeline/run-step-timeline.types.d.ts +182 -5
  45. package/dist/components/run-step-timeline/run-step-timeline.utilities.d.ts +60 -0
  46. package/dist/components/schedule-builder/index.d.ts +5 -0
  47. package/dist/components/schedule-builder/index.js +3176 -0
  48. package/dist/components/schedule-builder/schedule-builder.css +121 -0
  49. package/dist/components/schedule-builder/schedule-builder.css.d.ts +1 -0
  50. package/dist/components/schedule-builder/schedule-builder.schema.d.ts +3 -0
  51. package/dist/components/schedule-builder/schedule-builder.schema.js +141 -0
  52. package/dist/components/schedule-builder/schedule-builder.svelte.d.ts +22 -0
  53. package/dist/components/schedule-builder/schedule-builder.types.d.ts +130 -0
  54. package/dist/components/schedule-builder/schedule-builder.utilities.d.ts +78 -0
  55. package/dist/components/schedule-builder/schedule-builder.variables.d.ts +2 -0
  56. package/dist/components/schedule-builder/schedule-builder.variables.js +61 -0
  57. package/dist/components/tag-input/index.js +22 -4
  58. package/dist/components/tag-input/tag-input.schema.js +5 -1
  59. package/dist/components/tag-input/tag-input.types.d.ts +4 -0
  60. package/dist/index.d.ts +6 -2
  61. package/dist/index.js +23069 -20833
  62. package/dist/server/components/access-gate/index.js +2 -1
  63. package/dist/server/components/chat/index.js +4 -4
  64. package/dist/server/components/chat-conversation-header/index.js +5 -5
  65. package/dist/server/components/chat-conversation-list/index.js +3 -3
  66. package/dist/server/components/color-field/index.js +2 -2
  67. package/dist/server/components/confirm-dialog/index.js +7 -2
  68. package/dist/server/components/connection-indicator/index.js +16 -0
  69. package/dist/server/components/data-table/index.js +3 -3
  70. package/dist/server/components/diff-viewer/index.js +4 -3
  71. package/dist/server/components/faceted-filter-bar/index.js +1 -1
  72. package/dist/server/components/invocation-rule-builder/index.js +1 -1
  73. package/dist/server/components/json-schema-editor/index.js +4 -3
  74. package/dist/server/components/navigation-bar/index.js +1 -1
  75. package/dist/server/components/review-editor/index.js +4 -3
  76. package/dist/server/components/run-step-timeline/index.js +1 -1
  77. package/dist/server/components/schedule-builder/index.js +30 -0
  78. package/dist/server/components/side-navigation/index.js +2 -2
  79. package/dist/server/components/table/index.js +3 -3
  80. package/dist/server/components/tag-input/index.js +1 -1
  81. package/dist/server/components/time-field/index.js +5 -3
  82. package/dist/server/index.js +105 -92
  83. package/dist/server/index.server-0sx92swg.js +9 -0
  84. package/dist/server/{index.server-mzzb7dnm.js → index.server-2bbwajb2.js} +1 -1
  85. package/dist/server/{index.server-q7w4jw89.js → index.server-2s5h1d05.js} +2 -2
  86. package/dist/server/{index.server-dg83ke3k.js → index.server-2zdbz3rj.js} +1 -1
  87. package/dist/server/{index.server-0jhra85x.js → index.server-3w72dxz7.js} +4 -4
  88. package/dist/server/{index.server-5tf22d6e.js → index.server-4wx0qven.js} +3 -3
  89. package/dist/server/{index.server-qh9qzd5t.js → index.server-4x4hgvqe.js} +283 -13
  90. package/dist/server/{index.server-c6pe4kaf.js → index.server-5gzyqv78.js} +1 -1
  91. package/dist/server/{index.server-mqt1ngqw.js → index.server-6hnfzc7f.js} +19 -6
  92. package/dist/server/{index.server-894t97kv.js → index.server-7yvpb6z7.js} +4 -4
  93. package/dist/server/index.server-be9bpqeh.js +93 -0
  94. package/dist/server/{index.server-f3z7tpxr.js → index.server-c42b462p.js} +2 -4
  95. package/dist/server/{index.server-7vyzen5x.js → index.server-c5qv3p61.js} +171 -63
  96. package/dist/server/index.server-cch0eed5.js +144 -0
  97. package/dist/server/{index.server-btgb3d56.js → index.server-d5a5ccz7.js} +18 -12
  98. package/dist/server/index.server-g5jgq3jp.js +793 -0
  99. package/dist/server/index.server-gmh5ar16.js +2 -0
  100. package/dist/server/{index.server-0atyznp7.js → index.server-h89zk5ev.js} +7 -7
  101. package/dist/server/{index.server-jxg42jpz.js → index.server-jdcq5zza.js} +7 -7
  102. package/dist/server/{index.server-2dga5yzr.js → index.server-jfrssta6.js} +1 -1
  103. package/dist/server/{index.server-fy068yne.js → index.server-jj76374p.js} +1 -1
  104. package/dist/server/{index.server-39ne5cd1.js → index.server-pxq277t5.js} +32 -1
  105. package/dist/server/index.server-qbkxz7r2.js +750 -0
  106. package/dist/server/{index.server-marjz8ps.js → index.server-tcay903b.js} +3 -2
  107. package/dist/server/{index.server-yz4k36cw.js → index.server-z7hxtqs1.js} +1 -1
  108. package/package.json +87 -97
  109. package/src/components/chat/builders.ts +445 -14
  110. package/src/components/chat/conversation-model.ts +1 -0
  111. package/src/components/chat/index.ts +1 -0
  112. package/src/components/chat/utilities/conversation.ts +33 -15
  113. package/src/components/code-block/code-block.css +15 -9
  114. package/src/components/code-block/code-block.variables.json +7 -1
  115. package/src/components/code-block/code-block.variables.ts +7 -1
  116. package/src/components/confirm-dialog/confirm-dialog.css +2 -0
  117. package/src/components/confirm-dialog/confirm-dialog.schema.json +8 -0
  118. package/src/components/confirm-dialog/confirm-dialog.schema.ts +10 -0
  119. package/src/components/confirm-dialog/confirm-dialog.svelte +31 -3
  120. package/src/components/confirm-dialog/confirm-dialog.types.ts +7 -0
  121. package/src/components/connection-indicator/connection-indicator.css +136 -0
  122. package/src/components/connection-indicator/connection-indicator.examples.json +31 -0
  123. package/src/components/connection-indicator/connection-indicator.schema.json +29 -0
  124. package/src/components/connection-indicator/connection-indicator.schema.ts +36 -0
  125. package/src/components/connection-indicator/connection-indicator.svelte +63 -0
  126. package/src/components/connection-indicator/connection-indicator.types.ts +58 -0
  127. package/src/components/connection-indicator/connection-indicator.variables.json +1 -0
  128. package/src/components/connection-indicator/connection-indicator.variables.ts +3 -0
  129. package/src/components/connection-indicator/index.ts +10 -0
  130. package/src/components/container/container.schema.json +3 -3
  131. package/src/components/container/container.schema.ts +3 -3
  132. package/src/components/faceted-filter-bar/faceted-filter-bar.schema.json +4 -0
  133. package/src/components/faceted-filter-bar/faceted-filter-bar.schema.ts +4 -0
  134. package/src/components/faceted-filter-bar/faceted-filter-bar.svelte +13 -11
  135. package/src/components/faceted-filter-bar/faceted-filter-bar.types.ts +2 -0
  136. package/src/components/invocation-rule-builder/index.ts +4 -0
  137. package/src/components/invocation-rule-builder/invocation-rule-builder.css +28 -0
  138. package/src/components/invocation-rule-builder/invocation-rule-builder.examples.json +6 -0
  139. package/src/components/invocation-rule-builder/invocation-rule-builder.schema.json +76 -3
  140. package/src/components/invocation-rule-builder/invocation-rule-builder.schema.ts +83 -3
  141. package/src/components/invocation-rule-builder/invocation-rule-builder.svelte +326 -113
  142. package/src/components/invocation-rule-builder/invocation-rule-builder.types.ts +157 -43
  143. package/src/components/navigation-bar/navigation-bar.svelte +2 -2
  144. package/src/components/run-step-timeline/index.ts +8 -0
  145. package/src/components/run-step-timeline/run-step-branch-disclosure.svelte +16 -0
  146. package/src/components/run-step-timeline/run-step-timeline.css +124 -0
  147. package/src/components/run-step-timeline/run-step-timeline.examples.json +18 -0
  148. package/src/components/run-step-timeline/run-step-timeline.schema.json +872 -349
  149. package/src/components/run-step-timeline/run-step-timeline.schema.ts +931 -349
  150. package/src/components/run-step-timeline/run-step-timeline.svelte +388 -269
  151. package/src/components/run-step-timeline/run-step-timeline.types.ts +192 -5
  152. package/src/components/run-step-timeline/run-step-timeline.utilities.ts +310 -0
  153. package/src/components/schedule-builder/index.ts +14 -0
  154. package/src/components/schedule-builder/schedule-builder.css +121 -0
  155. package/src/components/schedule-builder/schedule-builder.examples.json +31 -0
  156. package/src/components/schedule-builder/schedule-builder.schema.json +78 -0
  157. package/src/components/schedule-builder/schedule-builder.schema.ts +86 -0
  158. package/src/components/schedule-builder/schedule-builder.svelte +629 -0
  159. package/src/components/schedule-builder/schedule-builder.types.ts +137 -0
  160. package/src/components/schedule-builder/schedule-builder.utilities.ts +344 -0
  161. package/src/components/schedule-builder/schedule-builder.variables.json +1 -0
  162. package/src/components/schedule-builder/schedule-builder.variables.ts +3 -0
  163. package/src/components/tag-input/tag-input.schema.json +4 -0
  164. package/src/components/tag-input/tag-input.schema.ts +4 -0
  165. package/src/components/tag-input/tag-input.svelte +20 -3
  166. package/src/components/tag-input/tag-input.types.ts +4 -0
  167. package/src/index.ts +23 -0
  168. package/src/styles/components.css +2 -0
  169. package/dist/server/index.server-qk1h0n02.js +0 -393
@@ -5,10 +5,150 @@ export {};
5
5
  <script lang="ts">
6
6
  import { tick } from 'svelte';
7
7
  import { classNames } from '../../utilities/class-names.ts';
8
- let { rules, onchange, fieldOptions, operatorOptions, actionOptions, readonly = false, addRuleLabel = 'Add rule', addConditionLabel = 'Add condition', addActionLabel = 'Add action', label, class: className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...rest } = $props();
8
+ /**
9
+ * Fixed operator vocabulary for conditions-only mode (`mode="conditions"`).
10
+ * Cinder supplies these with default labels; the `operatorOptions` prop is
11
+ * not accepted in this mode.
12
+ */
13
+ const CONDITIONS_ONLY_OPERATOR_OPTIONS = [
14
+ { value: 'eq', label: 'equals' },
15
+ { value: 'gt', label: 'greater than' },
16
+ { value: 'lt', label: 'less than' },
17
+ { value: 'gte', label: 'greater than or equal' },
18
+ { value: 'lte', label: 'less than or equal' },
19
+ ];
20
+ let { rules, onchange, fieldOptions, operatorOptions, actionOptions, mode = 'full', readonly = false, addRuleLabel = 'Add rule', addConditionLabel = 'Add condition', addActionLabel = 'Add action', label, class: className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...rest } = $props();
9
21
  const baseId = $props.id();
10
22
  const sectionAriaLabel = $derived(ariaLabelledby ? undefined : (ariaLabel ?? label ?? 'Invocation rules'));
11
23
  const effectiveReadonly = $derived(readonly || onchange === undefined);
24
+ const conditionsOnly = $derived(mode === 'conditions');
25
+ /**
26
+ * The operator options actually rendered and used for new conditions.
27
+ * Fixed in conditions-only mode; consumer-supplied in full mode.
28
+ */
29
+ const resolvedOperatorOptions = $derived(conditionsOnly ? CONDITIONS_ONLY_OPERATOR_OPTIONS : (operatorOptions ?? []));
30
+ const resolvedActionOptions = $derived(actionOptions ?? []);
31
+ /** The declared value type for a field, defaulting to `'string'`. */
32
+ function fieldValueType(fieldValue) {
33
+ return fieldOptions.find((option) => option.value === fieldValue)?.type ?? 'string';
34
+ }
35
+ /** Enum choices declared on a field option, or an empty list. */
36
+ function fieldEnumOptions(fieldValue) {
37
+ return fieldOptions.find((option) => option.value === fieldValue)?.options ?? [];
38
+ }
39
+ /**
40
+ * The type-appropriate default `value` for a field in conditions-only mode —
41
+ * used both when a condition is first added and when its `field` changes to
42
+ * one of a different (or enum-incompatible) type, so the stored value always
43
+ * matches what the typed control actually renders.
44
+ */
45
+ function defaultConditionValue(fieldValue) {
46
+ const type = fieldValueType(fieldValue);
47
+ switch (type) {
48
+ case 'boolean':
49
+ // Matches the checkbox's own read: `checked={condition.value === 'true'}`.
50
+ return 'false';
51
+ case 'enum':
52
+ return fieldEnumOptions(fieldValue)[0]?.value ?? '';
53
+ case 'number':
54
+ case 'string':
55
+ return '';
56
+ }
57
+ }
58
+ /**
59
+ * A number-LIKE string — permissive enough to accept every intermediate
60
+ * state a user legitimately passes through while typing into a native
61
+ * `<input type="number">` (a lone `-`, a trailing `.` as in `'1.'`, a
62
+ * partial exponent like `'1e'` or `'1e5'`, `'-2.5'`, and so on), so
63
+ * `emitChange` — which runs on every value edit, not just field changes —
64
+ * never fights live typing by blanking a value mid-keystroke.
65
+ *
66
+ * It still rejects strings a number input would never produce and that
67
+ * only arrive as stale prop data: hex (`'0x10'`), units (`'5px'`), plain
68
+ * words (`'foo'`), exponent-only fragments with no mantissa (`'e'`, `'+e'`,
69
+ * `'e+'`), or whitespace mixed with letters. The first alternative covers a
70
+ * digit-bearing number with an optional exponent; the second covers the bare
71
+ * sign/decimal intermediates (`''`, `'-'`, `'+'`, `'.'`, `'-.'`) that a number
72
+ * input surfaces mid-typing — an exponent is only ever valid glued to a
73
+ * mantissa, never on its own.
74
+ */
75
+ const NUMBER_LIKE_VALUE_PATTERN = /^([+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d*)?|[+-]?\.?)$/i;
76
+ /**
77
+ * Whether a stored condition value is still a faithful representation of
78
+ * what the typed control for `type` would render — i.e. whether it's safe
79
+ * to keep across a field change instead of resetting to the type's default.
80
+ */
81
+ function isValueValidForFieldType(value, type, fieldValue) {
82
+ switch (type) {
83
+ case 'boolean':
84
+ return value === 'true' || value === 'false';
85
+ case 'enum':
86
+ return fieldEnumOptions(fieldValue).some((option) => option.value === value);
87
+ case 'number':
88
+ // Blank is allowed; otherwise a number-like string is valid — lenient
89
+ // enough to not fight live typing, strict enough to still reject stale
90
+ // non-numeric data like hex or unit-suffixed strings. A whitespace-only
91
+ // value renders blank in the number input, so it is treated as invalid
92
+ // (normalized to '') rather than persisting the hidden spaces.
93
+ return (value === '' || (value.trim() !== '' && NUMBER_LIKE_VALUE_PATTERN.test(value.trim())));
94
+ case 'string':
95
+ return true;
96
+ }
97
+ }
98
+ /** Whether `operator` is one of the fixed conditions-only mode operators. */
99
+ function isConditionsOnlyOperator(operator) {
100
+ return CONDITIONS_ONLY_OPERATOR_OPTIONS.some((option) => option.value === operator);
101
+ }
102
+ /**
103
+ * The operator to display for a condition — in the readonly summary label
104
+ * and as the selected value of the editable operator `<select>`. In full
105
+ * mode this is always the raw stored operator. In conditions-only mode, a
106
+ * rule can arrive carrying a legacy operator outside the fixed
107
+ * eq/gt/lt/gte/lte set (e.g. a leftover `'matches'` from full mode); this
108
+ * displays `'eq'` in that case instead of an operator the mode forbids —
109
+ * matching what `emitChange` would coerce it to on the next edit, and
110
+ * keeping the operator `<select>` always showing a valid selected option.
111
+ */
112
+ function displayOperator(operator) {
113
+ return conditionsOnly && !isConditionsOnlyOperator(operator) ? 'eq' : operator;
114
+ }
115
+ /**
116
+ * Emits `onchange`. In conditions-only mode, normalizes every emitted rule
117
+ * first so it's internally consistent regardless of what the incoming
118
+ * `rules` prop originally carried (e.g. a consumer switching existing
119
+ * full-mode rules into conditions-only mode):
120
+ *
121
+ * - Strips `actions` — conditions-only rules never carry an action target.
122
+ * - Coerces any condition `operator` outside the fixed eq/gt/lt/gte/lte set
123
+ * to `'eq'` (e.g. a leftover full-mode operator like `'matches'`).
124
+ * - Coerces any condition `value` that isn't valid for its field's
125
+ * inferred type to that type's default (e.g. a boolean field whose value
126
+ * is an arbitrary string, or an enum field whose value isn't one of its
127
+ * choices) — the same check and defaults used for field-type changes.
128
+ *
129
+ * Full mode passes rules through unchanged.
130
+ */
131
+ function emitChange(nextRules, change) {
132
+ const rulesToEmit = conditionsOnly
133
+ ? nextRules.map((rule) => ({
134
+ ...rule,
135
+ actions: [],
136
+ conditions: rule.conditions.map((condition) => {
137
+ const operator = isConditionsOnlyOperator(condition.operator)
138
+ ? condition.operator
139
+ : 'eq';
140
+ const type = fieldValueType(condition.field);
141
+ const value = isValueValidForFieldType(condition.value, type, condition.field)
142
+ ? condition.value
143
+ : defaultConditionValue(condition.field);
144
+ return operator === condition.operator && value === condition.value
145
+ ? condition
146
+ : { ...condition, operator, value };
147
+ }),
148
+ }))
149
+ : nextRules;
150
+ onchange?.(rulesToEmit, change);
151
+ }
12
152
  /** Announcement text for the live region. */
13
153
  let announcement = $state('');
14
154
  let ruleLabelDrafts = $state({});
@@ -39,13 +179,13 @@ function handleAddRule() {
39
179
  { id: ruleId, label: `Rule ${rules.length + 1}`, conditions: [], actions: [] },
40
180
  ];
41
181
  const change = { type: 'add-rule', ruleId };
42
- onchange?.(nextRules, change);
182
+ emitChange(nextRules, change);
43
183
  announce('Rule added.');
44
184
  }
45
185
  function handleRemoveRule(ruleId, ruleLabel, ruleIndex) {
46
186
  const nextRules = rules.filter((rule) => rule.id !== ruleId);
47
187
  const change = { type: 'remove-rule', ruleId };
48
- onchange?.(nextRules, change);
188
+ emitChange(nextRules, change);
49
189
  announce(`${ruleLabel} removed.`);
50
190
  // Move focus to the previous rule's remove button, or the add-rule button.
51
191
  tick().then(() => {
@@ -64,7 +204,7 @@ function handleMoveRule(ruleId, fromIndex, direction) {
64
204
  const [moved] = nextRules.splice(fromIndex, 1);
65
205
  nextRules.splice(toIndex, 0, moved);
66
206
  const change = { type: 'move-rule', ruleId, fromIndex, toIndex };
67
- onchange?.(nextRules, change);
207
+ emitChange(nextRules, change);
68
208
  announce(`${moved.label} moved to position ${toIndex + 1} of ${nextRules.length}.`);
69
209
  }
70
210
  function handleRenameRule(ruleId, label) {
@@ -76,7 +216,7 @@ function handleRenameRule(ruleId, label) {
76
216
  return;
77
217
  const nextRules = updateRules(ruleId, (rule) => ({ ...rule, label: nextLabel }));
78
218
  const change = { type: 'rename-rule', ruleId };
79
- onchange?.(nextRules, change);
219
+ emitChange(nextRules, change);
80
220
  }
81
221
  function ruleLabelDraft(rule) {
82
222
  const draft = ruleLabelDrafts[rule.id];
@@ -88,16 +228,20 @@ function ruleLabelDraft(rule) {
88
228
  function handleAddCondition(ruleId) {
89
229
  const conditionId = generateId();
90
230
  const firstField = fieldOptions[0]?.value ?? '';
91
- const firstOperator = operatorOptions[0]?.value ?? '';
231
+ const firstOperator = resolvedOperatorOptions[0]?.value ?? '';
232
+ // In conditions-only mode, seed a type-appropriate default (e.g. 'false'
233
+ // for a boolean field, the first choice for an enum field) so the stored
234
+ // value matches what the typed control renders immediately on add.
235
+ const initialValue = conditionsOnly ? defaultConditionValue(firstField) : '';
92
236
  const nextRules = updateRules(ruleId, (rule) => ({
93
237
  ...rule,
94
238
  conditions: [
95
239
  ...rule.conditions,
96
- { id: conditionId, field: firstField, operator: firstOperator, value: '' },
240
+ { id: conditionId, field: firstField, operator: firstOperator, value: initialValue },
97
241
  ],
98
242
  }));
99
243
  const change = { type: 'add-condition', ruleId, conditionId };
100
- onchange?.(nextRules, change);
244
+ emitChange(nextRules, change);
101
245
  announce('Condition added.');
102
246
  }
103
247
  function handleRemoveCondition(ruleId, conditionId, conditionIndex, ruleIndex) {
@@ -106,7 +250,7 @@ function handleRemoveCondition(ruleId, conditionId, conditionIndex, ruleIndex) {
106
250
  conditions: rule.conditions.filter((condition) => condition.id !== conditionId),
107
251
  }));
108
252
  const change = { type: 'remove-condition', ruleId, conditionId };
109
- onchange?.(nextRules, change);
253
+ emitChange(nextRules, change);
110
254
  announce('Condition removed.');
111
255
  tick().then(() => {
112
256
  const ruleEl = document.querySelector(`[data-irb-region="${baseId}"] [data-irb-rule="${ruleIndex}"]`);
@@ -119,7 +263,25 @@ function handleRemoveCondition(ruleId, conditionId, conditionIndex, ruleIndex) {
119
263
  function handleUpdateCondition(ruleId, conditionId, field, value) {
120
264
  const nextRules = updateRules(ruleId, (rule) => ({
121
265
  ...rule,
122
- conditions: rule.conditions.map((condition) => condition.id === conditionId ? { ...condition, [field]: value } : condition),
266
+ conditions: rule.conditions.map((condition) => {
267
+ if (condition.id !== conditionId)
268
+ return condition;
269
+ // Changing which field a condition tests, in conditions-only mode: if
270
+ // the current value is no longer a faithful representation of the new
271
+ // field's type (e.g. an arbitrary string against a field that's now
272
+ // boolean, or a value outside a new enum field's choices), reset it to
273
+ // that type's default so the stored value matches the typed control
274
+ // that's about to render. Same-type (and enum-with-matching-value)
275
+ // field changes keep the value the user already entered.
276
+ if (field === 'field' && conditionsOnly) {
277
+ const nextType = fieldValueType(value);
278
+ const nextValue = isValueValidForFieldType(condition.value, nextType, value)
279
+ ? condition.value
280
+ : defaultConditionValue(value);
281
+ return { ...condition, field: value, value: nextValue };
282
+ }
283
+ return { ...condition, [field]: value };
284
+ }),
123
285
  }));
124
286
  const change = {
125
287
  type: 'update-condition',
@@ -127,20 +289,20 @@ function handleUpdateCondition(ruleId, conditionId, field, value) {
127
289
  conditionId,
128
290
  field,
129
291
  };
130
- onchange?.(nextRules, change);
292
+ emitChange(nextRules, change);
131
293
  }
132
294
  // ---------------------------------------------------------------------------
133
295
  // Action handlers
134
296
  // ---------------------------------------------------------------------------
135
297
  function handleAddAction(ruleId) {
136
298
  const actionId = generateId();
137
- const firstTarget = actionOptions[0]?.value ?? '';
299
+ const firstTarget = resolvedActionOptions[0]?.value ?? '';
138
300
  const nextRules = updateRules(ruleId, (rule) => ({
139
301
  ...rule,
140
302
  actions: [...rule.actions, { id: actionId, target: firstTarget }],
141
303
  }));
142
304
  const change = { type: 'add-action', ruleId, actionId };
143
- onchange?.(nextRules, change);
305
+ emitChange(nextRules, change);
144
306
  announce('Action added.');
145
307
  }
146
308
  function handleRemoveAction(ruleId, actionId, actionIndex, ruleIndex) {
@@ -149,7 +311,7 @@ function handleRemoveAction(ruleId, actionId, actionIndex, ruleIndex) {
149
311
  actions: rule.actions.filter((action) => action.id !== actionId),
150
312
  }));
151
313
  const change = { type: 'remove-action', ruleId, actionId };
152
- onchange?.(nextRules, change);
314
+ emitChange(nextRules, change);
153
315
  announce('Action removed.');
154
316
  tick().then(() => {
155
317
  const ruleEl = document.querySelector(`[data-irb-region="${baseId}"] [data-irb-rule="${ruleIndex}"]`);
@@ -165,7 +327,7 @@ function handleUpdateAction(ruleId, actionId, target) {
165
327
  actions: rule.actions.map((action) => action.id === actionId ? { ...action, target } : action),
166
328
  }));
167
329
  const change = { type: 'update-action', ruleId, actionId };
168
- onchange?.(nextRules, change);
330
+ emitChange(nextRules, change);
169
331
  }
170
332
  // ---------------------------------------------------------------------------
171
333
  // Label helpers
@@ -174,10 +336,11 @@ function fieldLabel(value) {
174
336
  return fieldOptions.find((option) => option.value === value)?.label ?? value;
175
337
  }
176
338
  function operatorLabel(value) {
177
- return operatorOptions.find((option) => option.value === value)?.label ?? value;
339
+ const displayValue = displayOperator(value);
340
+ return (resolvedOperatorOptions.find((option) => option.value === displayValue)?.label ?? displayValue);
178
341
  }
179
342
  function actionTargetLabel(value) {
180
- return actionOptions.find((option) => option.value === value)?.label ?? value;
343
+ return resolvedActionOptions.find((option) => option.value === value)?.label ?? value;
181
344
  }
182
345
  function conditionSummary(condition) {
183
346
  return `${fieldLabel(condition.field)} ${operatorLabel(condition.operator)} "${condition.value}"`;
@@ -324,7 +487,7 @@ function actionSummary(action) {
324
487
  <select
325
488
  class="cinder-invocation-rule-builder__condition-select"
326
489
  aria-label={`Operator for condition ${conditionIndex + 1} of ${rule.label}`}
327
- value={condition.operator}
490
+ value={displayOperator(condition.operator)}
328
491
  onchange={(event) =>
329
492
  handleUpdateCondition(
330
493
  rule.id,
@@ -333,25 +496,73 @@ function actionSummary(action) {
333
496
  (event.target as HTMLSelectElement).value,
334
497
  )}
335
498
  >
336
- {#each operatorOptions as option (option.value)}
499
+ {#each resolvedOperatorOptions as option (option.value)}
337
500
  <option value={option.value}>{option.label}</option>
338
501
  {/each}
339
502
  </select>
340
503
 
341
- <input
342
- type="text"
343
- class="cinder-invocation-rule-builder__condition-value"
344
- aria-label={`Value for condition ${conditionIndex + 1} of ${rule.label}`}
345
- placeholder="Value to compare"
346
- value={condition.value}
347
- oninput={(event) =>
348
- handleUpdateCondition(
349
- rule.id,
350
- condition.id,
351
- 'value',
352
- (event.target as HTMLInputElement).value,
353
- )}
354
- />
504
+ {#if conditionsOnly && fieldValueType(condition.field) === 'number'}
505
+ <input
506
+ type="number"
507
+ class="cinder-invocation-rule-builder__condition-value"
508
+ aria-label={`Value for condition ${conditionIndex + 1} of ${rule.label}`}
509
+ placeholder="Value to compare"
510
+ value={condition.value}
511
+ oninput={(event) =>
512
+ handleUpdateCondition(
513
+ rule.id,
514
+ condition.id,
515
+ 'value',
516
+ (event.target as HTMLInputElement).value,
517
+ )}
518
+ />
519
+ {:else if conditionsOnly && fieldValueType(condition.field) === 'boolean'}
520
+ <input
521
+ type="checkbox"
522
+ class="cinder-invocation-rule-builder__condition-value-checkbox"
523
+ aria-label={`Value for condition ${conditionIndex + 1} of ${rule.label}`}
524
+ checked={condition.value === 'true'}
525
+ onchange={(event) =>
526
+ handleUpdateCondition(
527
+ rule.id,
528
+ condition.id,
529
+ 'value',
530
+ String((event.target as HTMLInputElement).checked),
531
+ )}
532
+ />
533
+ {:else if conditionsOnly && fieldValueType(condition.field) === 'enum'}
534
+ <select
535
+ class="cinder-invocation-rule-builder__condition-select cinder-invocation-rule-builder__condition-value-select"
536
+ aria-label={`Value for condition ${conditionIndex + 1} of ${rule.label}`}
537
+ value={condition.value}
538
+ onchange={(event) =>
539
+ handleUpdateCondition(
540
+ rule.id,
541
+ condition.id,
542
+ 'value',
543
+ (event.target as HTMLSelectElement).value,
544
+ )}
545
+ >
546
+ {#each fieldEnumOptions(condition.field) as option (option.value)}
547
+ <option value={option.value}>{option.label}</option>
548
+ {/each}
549
+ </select>
550
+ {:else}
551
+ <input
552
+ type="text"
553
+ class="cinder-invocation-rule-builder__condition-value"
554
+ aria-label={`Value for condition ${conditionIndex + 1} of ${rule.label}`}
555
+ placeholder="Value to compare"
556
+ value={condition.value}
557
+ oninput={(event) =>
558
+ handleUpdateCondition(
559
+ rule.id,
560
+ condition.id,
561
+ 'value',
562
+ (event.target as HTMLInputElement).value,
563
+ )}
564
+ />
565
+ {/if}
355
566
 
356
567
  <button
357
568
  type="button"
@@ -386,90 +597,92 @@ function actionSummary(action) {
386
597
  {/if}
387
598
  </div>
388
599
 
389
- <!-- Actions section -->
390
- <div>
391
- <span
392
- id={`${baseId}-rule-${ruleIndex}-actions-label`}
393
- class="cinder-invocation-rule-builder__section-heading"
394
- >
395
- Actions
396
- </span>
397
- {#if !effectiveReadonly && rule.actions.length === 0}
398
- <p class="cinder-invocation-rule-builder__validation" role="status">
399
- Add at least one action for this rule.
400
- </p>
401
- {/if}
402
-
403
- {#if effectiveReadonly}
404
- <div
405
- class="cinder-invocation-rule-builder__summary"
406
- aria-label={`Actions for ${rule.label}`}
600
+ <!-- Actions section — hidden entirely in conditions-only mode. -->
601
+ {#if mode === 'full'}
602
+ <div>
603
+ <span
604
+ id={`${baseId}-rule-${ruleIndex}-actions-label`}
605
+ class="cinder-invocation-rule-builder__section-heading"
407
606
  >
408
- {#if rule.actions.length === 0}
409
- <p class="cinder-invocation-rule-builder__empty">No actions configured.</p>
410
- {:else}
411
- {#each rule.actions as action (action.id)}
412
- <div class="cinder-invocation-rule-builder__summary-row">
413
- {actionSummary(action)}
607
+ Actions
608
+ </span>
609
+ {#if !effectiveReadonly && rule.actions.length === 0}
610
+ <p class="cinder-invocation-rule-builder__validation" role="status">
611
+ Add at least one action for this rule.
612
+ </p>
613
+ {/if}
614
+
615
+ {#if effectiveReadonly}
616
+ <div
617
+ class="cinder-invocation-rule-builder__summary"
618
+ aria-label={`Actions for ${rule.label}`}
619
+ >
620
+ {#if rule.actions.length === 0}
621
+ <p class="cinder-invocation-rule-builder__empty">No actions configured.</p>
622
+ {:else}
623
+ {#each rule.actions as action (action.id)}
624
+ <div class="cinder-invocation-rule-builder__summary-row">
625
+ {actionSummary(action)}
626
+ </div>
627
+ {/each}
628
+ {/if}
629
+ </div>
630
+ {:else}
631
+ <div
632
+ class="cinder-invocation-rule-builder__actions"
633
+ role="list"
634
+ aria-labelledby={`${baseId}-rule-${ruleIndex}-actions-label`}
635
+ >
636
+ {#each rule.actions as action, actionIndex (action.id)}
637
+ <div class="cinder-invocation-rule-builder__action" role="listitem">
638
+ <select
639
+ class="cinder-invocation-rule-builder__action-select"
640
+ aria-label={`Action ${actionIndex + 1} target for ${rule.label}`}
641
+ value={action.target}
642
+ onchange={(event) =>
643
+ handleUpdateAction(
644
+ rule.id,
645
+ action.id,
646
+ (event.target as HTMLSelectElement).value,
647
+ )}
648
+ >
649
+ {#each resolvedActionOptions as option (option.value)}
650
+ <option value={option.value}>{option.label}</option>
651
+ {/each}
652
+ </select>
653
+
654
+ <button
655
+ type="button"
656
+ class="cinder-invocation-rule-builder__icon-btn"
657
+ aria-label={`Remove action ${actionIndex + 1} of ${rule.label}`}
658
+ data-irb-action-remove
659
+ onclick={() => handleRemoveAction(rule.id, action.id, actionIndex, ruleIndex)}
660
+ >
661
+ <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
662
+ <path
663
+ d="M4 4l8 8M12 4l-8 8"
664
+ stroke="currentColor"
665
+ stroke-width="2"
666
+ fill="none"
667
+ />
668
+ </svg>
669
+ </button>
414
670
  </div>
415
671
  {/each}
416
- {/if}
417
- </div>
418
- {:else}
419
- <div
420
- class="cinder-invocation-rule-builder__actions"
421
- role="list"
422
- aria-labelledby={`${baseId}-rule-${ruleIndex}-actions-label`}
423
- >
424
- {#each rule.actions as action, actionIndex (action.id)}
425
- <div class="cinder-invocation-rule-builder__action" role="listitem">
426
- <select
427
- class="cinder-invocation-rule-builder__action-select"
428
- aria-label={`Action ${actionIndex + 1} target for ${rule.label}`}
429
- value={action.target}
430
- onchange={(event) =>
431
- handleUpdateAction(
432
- rule.id,
433
- action.id,
434
- (event.target as HTMLSelectElement).value,
435
- )}
436
- >
437
- {#each actionOptions as option (option.value)}
438
- <option value={option.value}>{option.label}</option>
439
- {/each}
440
- </select>
441
-
442
- <button
443
- type="button"
444
- class="cinder-invocation-rule-builder__icon-btn"
445
- aria-label={`Remove action ${actionIndex + 1} of ${rule.label}`}
446
- data-irb-action-remove
447
- onclick={() => handleRemoveAction(rule.id, action.id, actionIndex, ruleIndex)}
448
- >
449
- <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
450
- <path
451
- d="M4 4l8 8M12 4l-8 8"
452
- stroke="currentColor"
453
- stroke-width="2"
454
- fill="none"
455
- />
456
- </svg>
457
- </button>
458
- </div>
459
- {/each}
460
- </div>
672
+ </div>
461
673
 
462
- <button
463
- type="button"
464
- class="cinder-invocation-rule-builder__add-btn"
465
- aria-label={`${addActionLabel} to ${rule.label}`}
466
- data-irb-add-action
467
- onclick={() => handleAddAction(rule.id)}
468
- >
469
- + {addActionLabel}
470
- </button>
471
- {/if}
472
- </div>
674
+ <button
675
+ type="button"
676
+ class="cinder-invocation-rule-builder__add-btn"
677
+ aria-label={`${addActionLabel} to ${rule.label}`}
678
+ data-irb-add-action
679
+ onclick={() => handleAddAction(rule.id)}
680
+ >
681
+ + {addActionLabel}
682
+ </button>
683
+ {/if}
684
+ </div>
685
+ {/if}
473
686
  </div>
474
687
  {/each}
475
688