@startsimpli/funnels 0.1.3 → 0.1.5

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 (151) hide show
  1. package/package.json +9 -31
  2. package/src/api/README.md +507 -0
  3. package/src/api/adapter.ts +106 -0
  4. package/src/api/client.test.ts +640 -0
  5. package/src/api/client.ts +385 -0
  6. package/src/api/default-adapter.ts +243 -0
  7. package/src/api/index.ts +24 -0
  8. package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
  9. package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
  10. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
  11. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
  12. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
  13. package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
  14. package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
  15. package/src/components/FilterRuleEditor/README.md +291 -0
  16. package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
  17. package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
  18. package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
  19. package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
  20. package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
  21. package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
  22. package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
  23. package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
  24. package/src/components/FilterRuleEditor/constants.ts +64 -0
  25. package/src/components/FilterRuleEditor/index.ts +14 -0
  26. package/src/components/FunnelCard/DESIGN.md +447 -0
  27. package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
  28. package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
  29. package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
  30. package/src/components/FunnelCard/FunnelCard.tsx +204 -0
  31. package/src/components/FunnelCard/FunnelStats.tsx +68 -0
  32. package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
  33. package/src/components/FunnelCard/INSTALLATION.md +304 -0
  34. package/src/components/FunnelCard/MatchBar.tsx +49 -0
  35. package/src/components/FunnelCard/README.md +294 -0
  36. package/src/components/FunnelCard/StageIndicator.tsx +62 -0
  37. package/src/components/FunnelCard/StatusBadge.tsx +52 -0
  38. package/src/components/FunnelCard/index.ts +14 -0
  39. package/src/components/FunnelPreview/EntityCard.tsx +72 -0
  40. package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
  41. package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
  42. package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
  43. package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
  44. package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
  45. package/src/components/FunnelPreview/README.md +337 -0
  46. package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
  47. package/src/components/FunnelPreview/example.tsx +286 -0
  48. package/src/components/FunnelPreview/index.ts +14 -0
  49. package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
  50. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
  51. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
  52. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
  53. package/src/components/FunnelRunHistory/README.md +325 -0
  54. package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
  55. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
  56. package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
  57. package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
  58. package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
  59. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
  60. package/src/components/FunnelRunHistory/index.ts +51 -0
  61. package/src/components/FunnelRunHistory/types.ts +40 -0
  62. package/src/components/FunnelRunHistory/utils.test.ts +126 -0
  63. package/src/components/FunnelRunHistory/utils.ts +100 -0
  64. package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
  65. package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
  66. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
  67. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
  68. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
  69. package/src/components/FunnelStageBuilder/README.md +341 -0
  70. package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
  71. package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
  72. package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
  73. package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
  74. package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
  75. package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
  76. package/src/components/FunnelStageBuilder/index.ts +21 -0
  77. package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
  78. package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
  79. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
  80. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
  81. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
  82. package/src/components/FunnelVisualFlow/README.md +323 -0
  83. package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
  84. package/src/components/FunnelVisualFlow/example.tsx +227 -0
  85. package/src/components/FunnelVisualFlow/index.ts +10 -0
  86. package/src/components/index.ts +102 -0
  87. package/src/core/README.md +307 -0
  88. package/src/core/engine.test.ts +1087 -0
  89. package/src/core/engine.ts +329 -0
  90. package/src/core/evaluator.example.ts +353 -0
  91. package/src/core/evaluator.test.ts +639 -0
  92. package/src/core/evaluator.ts +261 -0
  93. package/src/core/field-resolver.example.ts +175 -0
  94. package/src/core/field-resolver.test.ts +541 -0
  95. package/src/core/field-resolver.ts +247 -0
  96. package/src/core/index.ts +34 -0
  97. package/src/core/operators.test.ts +539 -0
  98. package/src/core/operators.ts +241 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/useDebouncedValue.ts +28 -0
  101. package/src/index.ts +155 -0
  102. package/src/store/README.md +342 -0
  103. package/src/store/create-funnel-store.test.ts +686 -0
  104. package/src/store/create-funnel-store.ts +538 -0
  105. package/src/store/index.ts +9 -0
  106. package/src/store/types.ts +294 -0
  107. package/src/stories/CrossDomain.stories.tsx +149 -0
  108. package/src/stories/Welcome.stories.tsx +81 -0
  109. package/src/stories/demo-data/index.ts +3 -0
  110. package/src/stories/demo-data/investors.ts +216 -0
  111. package/src/stories/demo-data/leads.ts +223 -0
  112. package/src/stories/demo-data/recipes.ts +217 -0
  113. package/src/test/setup.ts +5 -0
  114. package/src/types/index.ts +843 -0
  115. package/dist/client-3ESO2NHy.d.ts +0 -310
  116. package/dist/client-CZu03ACp.d.cts +0 -310
  117. package/dist/components/index.cjs +0 -3243
  118. package/dist/components/index.cjs.map +0 -1
  119. package/dist/components/index.css.map +0 -1
  120. package/dist/components/index.d.cts +0 -726
  121. package/dist/components/index.d.ts +0 -726
  122. package/dist/components/index.js +0 -3196
  123. package/dist/components/index.js.map +0 -1
  124. package/dist/core/index.cjs +0 -500
  125. package/dist/core/index.cjs.map +0 -1
  126. package/dist/core/index.d.cts +0 -359
  127. package/dist/core/index.d.ts +0 -359
  128. package/dist/core/index.js +0 -486
  129. package/dist/core/index.js.map +0 -1
  130. package/dist/hooks/index.cjs +0 -21
  131. package/dist/hooks/index.cjs.map +0 -1
  132. package/dist/hooks/index.d.cts +0 -11
  133. package/dist/hooks/index.d.ts +0 -11
  134. package/dist/hooks/index.js +0 -19
  135. package/dist/hooks/index.js.map +0 -1
  136. package/dist/index-BGDEXbuz.d.cts +0 -434
  137. package/dist/index-BGDEXbuz.d.ts +0 -434
  138. package/dist/index.cjs +0 -4499
  139. package/dist/index.cjs.map +0 -1
  140. package/dist/index.css +0 -198
  141. package/dist/index.css.map +0 -1
  142. package/dist/index.d.cts +0 -99
  143. package/dist/index.d.ts +0 -99
  144. package/dist/index.js +0 -4421
  145. package/dist/index.js.map +0 -1
  146. package/dist/store/index.cjs +0 -391
  147. package/dist/store/index.cjs.map +0 -1
  148. package/dist/store/index.d.cts +0 -225
  149. package/dist/store/index.d.ts +0 -225
  150. package/dist/store/index.js +0 -388
  151. package/dist/store/index.js.map +0 -1
@@ -0,0 +1,246 @@
1
+ /**
2
+ * RuleRow Component
3
+ *
4
+ * A single filter rule row with field, operator, and value inputs.
5
+ *
6
+ * Design Goal:
7
+ * Users need to configure a complete filter rule: which field to check,
8
+ * what comparison to make, and what value to compare against.
9
+ *
10
+ * Visual Hierarchy Rationale:
11
+ * - Horizontal layout on desktop (3 columns + delete button)
12
+ * - Responsive: stacks vertically on mobile
13
+ * - Delete button (X) aligned to right for easy removal
14
+ * - Border and padding create visual grouping
15
+ * - Slight shadow on hover indicates interactivity
16
+ *
17
+ * Component Specification:
18
+ * - Field selector: Full field registry, grouped by category
19
+ * - Operator selector: Filtered by selected field type
20
+ * - Value input: Dynamic based on field type + operator
21
+ * - Delete button: Removes this rule
22
+ *
23
+ * Interaction Notes:
24
+ * - Selecting field updates available operators
25
+ * - Selecting field/operator changes value input type
26
+ * - Operators that don't need values (isnull, is_true) hide value input
27
+ * - Delete button shows on hover (desktop) or always (mobile)
28
+ *
29
+ * Responsive Behavior:
30
+ * - Desktop (lg+): 3-column grid with delete button
31
+ * - Tablet (md): 2-column grid
32
+ * - Mobile: Stacked vertically
33
+ *
34
+ * Accessibility Considerations:
35
+ * - All inputs properly labeled
36
+ * - Delete button has aria-label
37
+ * - Keyboard navigation supported
38
+ * - Focus management when adding/removing rules
39
+ */
40
+
41
+ import { FilterRule, FieldDefinition, Operator } from '../../types';
42
+ import { FieldSelector } from './FieldSelector';
43
+ import { OperatorSelector } from './OperatorSelector';
44
+ import { NULL_VALUE_OPERATORS, MULTI_VALUE_OPERATORS } from './constants';
45
+ import {
46
+ TextValueInput,
47
+ NumberValueInput,
48
+ DateValueInput,
49
+ BooleanValueInput,
50
+ ChoiceValueInput,
51
+ MultiChoiceValueInput,
52
+ } from './ValueInputs';
53
+
54
+ interface RuleRowProps {
55
+ rule: FilterRule;
56
+ onChange: (rule: FilterRule) => void;
57
+ onRemove: () => void;
58
+ fieldRegistry: FieldDefinition[];
59
+ className?: string;
60
+ }
61
+
62
+ export function RuleRow({
63
+ rule,
64
+ onChange,
65
+ onRemove,
66
+ fieldRegistry,
67
+ className = '',
68
+ }: RuleRowProps) {
69
+ // Find the selected field definition
70
+ const selectedField = fieldRegistry.find((f) => f.name === rule.field_path);
71
+
72
+ // Get valid operators for the selected field
73
+ const availableOperators = selectedField?.operators || [];
74
+
75
+ // Determine if value input is needed
76
+ const needsValue = rule.operator && !NULL_VALUE_OPERATORS.includes(rule.operator);
77
+ const needsMultiValue =
78
+ rule.operator && MULTI_VALUE_OPERATORS.includes(rule.operator);
79
+
80
+ // Handle field change
81
+ const handleFieldChange = (fieldName: string) => {
82
+ const field = fieldRegistry.find((f) => f.name === fieldName);
83
+ onChange({
84
+ ...rule,
85
+ field_path: fieldName,
86
+ operator: field?.operators[0] || ('' as Operator),
87
+ value: null,
88
+ });
89
+ };
90
+
91
+ // Handle operator change
92
+ const handleOperatorChange = (operator: Operator) => {
93
+ onChange({
94
+ ...rule,
95
+ operator,
96
+ value: MULTI_VALUE_OPERATORS.includes(operator) ? [] : null,
97
+ });
98
+ };
99
+
100
+ // Handle value change
101
+ const handleValueChange = (value: any) => {
102
+ onChange({
103
+ ...rule,
104
+ value,
105
+ });
106
+ };
107
+
108
+ // Render the appropriate value input based on field type and operator
109
+ const renderValueInput = () => {
110
+ if (!needsValue) return null;
111
+ if (!selectedField) return null;
112
+
113
+ const { type, constraints } = selectedField;
114
+
115
+ // Multi-value operators
116
+ if (needsMultiValue) {
117
+ if (constraints?.choices) {
118
+ return (
119
+ <MultiChoiceValueInput
120
+ value={Array.isArray(rule.value) ? rule.value : []}
121
+ onChange={handleValueChange}
122
+ choices={constraints.choices}
123
+ />
124
+ );
125
+ }
126
+ // Fallback: text input with comma-separated values
127
+ return (
128
+ <TextValueInput
129
+ value={Array.isArray(rule.value) ? rule.value.join(', ') : ''}
130
+ onChange={(val) => handleValueChange(val.split(',').map((v) => v.trim()))}
131
+ placeholder="Enter values, comma-separated..."
132
+ />
133
+ );
134
+ }
135
+
136
+ // Single-value operators
137
+ switch (type) {
138
+ case 'string':
139
+ if (constraints?.choices && rule.operator === 'eq') {
140
+ return (
141
+ <ChoiceValueInput
142
+ value={rule.value || ''}
143
+ onChange={handleValueChange}
144
+ choices={constraints.choices}
145
+ />
146
+ );
147
+ }
148
+ return (
149
+ <TextValueInput
150
+ value={rule.value || ''}
151
+ onChange={handleValueChange}
152
+ />
153
+ );
154
+
155
+ case 'number':
156
+ return (
157
+ <NumberValueInput
158
+ value={rule.value}
159
+ onChange={handleValueChange}
160
+ min={constraints?.min_value as number}
161
+ max={constraints?.max_value as number}
162
+ />
163
+ );
164
+
165
+ case 'date':
166
+ return (
167
+ <DateValueInput
168
+ value={rule.value || null}
169
+ onChange={handleValueChange}
170
+ min={constraints?.min_value as string}
171
+ max={constraints?.max_value as string}
172
+ />
173
+ );
174
+
175
+ case 'boolean':
176
+ return (
177
+ <BooleanValueInput
178
+ value={rule.value || false}
179
+ onChange={handleValueChange}
180
+ />
181
+ );
182
+
183
+ case 'tag':
184
+ return (
185
+ <TextValueInput
186
+ value={rule.value || ''}
187
+ onChange={handleValueChange}
188
+ placeholder="Enter tag name..."
189
+ />
190
+ );
191
+
192
+ default:
193
+ return (
194
+ <TextValueInput
195
+ value={rule.value || ''}
196
+ onChange={handleValueChange}
197
+ />
198
+ );
199
+ }
200
+ };
201
+
202
+ return (
203
+ <div
204
+ className={`
205
+ relative group
206
+ border border-gray-200 rounded-lg p-4
207
+ bg-white hover:shadow-sm transition-shadow
208
+ ${className}
209
+ `}
210
+ >
211
+ {/* Delete button */}
212
+ <button
213
+ type="button"
214
+ onClick={onRemove}
215
+ className="
216
+ absolute top-2 right-2
217
+ w-6 h-6 flex items-center justify-center
218
+ text-gray-400 hover:text-red-600 hover:bg-red-50
219
+ rounded transition-colors
220
+ focus:outline-none focus:ring-2 focus:ring-red-500
221
+ "
222
+ aria-label="Remove rule"
223
+ >
224
+ ×
225
+ </button>
226
+
227
+ {/* Grid layout: Field | Operator | Value */}
228
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 pr-8">
229
+ <FieldSelector
230
+ fields={fieldRegistry}
231
+ value={rule.field_path}
232
+ onChange={handleFieldChange}
233
+ />
234
+
235
+ <OperatorSelector
236
+ operators={availableOperators}
237
+ value={rule.operator || ''}
238
+ onChange={handleOperatorChange}
239
+ disabled={!rule.field_path}
240
+ />
241
+
242
+ {needsValue && renderValueInput()}
243
+ </div>
244
+ </div>
245
+ );
246
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * BooleanValueInput Component
3
+ *
4
+ * Checkbox input for boolean values.
5
+ *
6
+ * Design Goal:
7
+ * Simple true/false toggle for boolean fields.
8
+ *
9
+ * Visual Hierarchy Rationale:
10
+ * - Checkbox with clear label
11
+ * - Aligned with other value inputs
12
+ * - Checked state visually distinct
13
+ *
14
+ * Interaction Notes:
15
+ * - Click checkbox or label to toggle
16
+ * - Space bar toggles when focused
17
+ *
18
+ * Accessibility Considerations:
19
+ * - Proper label association
20
+ * - Keyboard accessible
21
+ * - Focus visible
22
+ */
23
+
24
+ interface BooleanValueInputProps {
25
+ value: boolean;
26
+ onChange: (value: boolean) => void;
27
+ label?: string;
28
+ error?: string;
29
+ className?: string;
30
+ }
31
+
32
+ export function BooleanValueInput({
33
+ value,
34
+ onChange,
35
+ label = 'True',
36
+ error,
37
+ className = '',
38
+ }: BooleanValueInputProps) {
39
+ return (
40
+ <div className={`flex flex-col gap-1 ${className}`}>
41
+ <label className="text-xs font-medium text-gray-700">Value</label>
42
+ <label className="flex items-center gap-2 cursor-pointer">
43
+ <input
44
+ type="checkbox"
45
+ checked={value}
46
+ onChange={(e) => onChange(e.target.checked)}
47
+ className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
48
+ />
49
+ <span className="text-sm text-gray-700">{label}</span>
50
+ </label>
51
+ {error && <span className="text-xs text-red-600">{error}</span>}
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ChoiceValueInput Component
3
+ *
4
+ * Single-select dropdown for choice fields.
5
+ *
6
+ * Design Goal:
7
+ * Select a single value from predefined choices (e.g., firm stage, recipe cuisine).
8
+ *
9
+ * Visual Hierarchy Rationale:
10
+ * - Standard select dropdown
11
+ * - Choices provided by field definition
12
+ * - Clear placeholder when nothing selected
13
+ *
14
+ * Interaction Notes:
15
+ * - Click to open dropdown
16
+ * - Keyboard navigation supported
17
+ * - Selected value shown in dropdown
18
+ *
19
+ * Accessibility Considerations:
20
+ * - Proper label association
21
+ * - Keyboard navigation (arrow keys)
22
+ * - Focus visible
23
+ */
24
+
25
+ interface ChoiceValueInputProps {
26
+ value: string;
27
+ onChange: (value: string) => void;
28
+ choices: any[];
29
+ placeholder?: string;
30
+ error?: string;
31
+ className?: string;
32
+ }
33
+
34
+ export function ChoiceValueInput({
35
+ value,
36
+ onChange,
37
+ choices,
38
+ placeholder = 'Select option...',
39
+ error,
40
+ className = '',
41
+ }: ChoiceValueInputProps) {
42
+ // Handle both string arrays and {value, label} arrays
43
+ const getChoiceValue = (choice: any): string => {
44
+ if (typeof choice === 'string') return choice;
45
+ return choice.value || choice;
46
+ };
47
+
48
+ const getChoiceLabel = (choice: any): string => {
49
+ if (typeof choice === 'string') return choice;
50
+ return choice.label || choice.value || String(choice);
51
+ };
52
+
53
+ return (
54
+ <div className={`flex flex-col gap-1 ${className}`}>
55
+ <label htmlFor="choice-value" className="text-xs font-medium text-gray-700">
56
+ Value
57
+ </label>
58
+ <select
59
+ id="choice-value"
60
+ value={value || ''}
61
+ onChange={(e) => onChange(e.target.value)}
62
+ className={`
63
+ w-full px-3 py-2 text-sm border rounded-md
64
+ bg-white
65
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
66
+ ${error ? 'border-red-500' : 'border-gray-300'}
67
+ `}
68
+ >
69
+ <option value="">{placeholder}</option>
70
+ {choices.map((choice, index) => {
71
+ const val = getChoiceValue(choice);
72
+ const label = getChoiceLabel(choice);
73
+ return (
74
+ <option key={`${val}-${index}`} value={val}>
75
+ {label}
76
+ </option>
77
+ );
78
+ })}
79
+ </select>
80
+ {error && <span className="text-xs text-red-600">{error}</span>}
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * DateValueInput Component
3
+ *
4
+ * Date input for date comparisons.
5
+ *
6
+ * Design Goal:
7
+ * Intuitive date selection with browser-native date picker.
8
+ *
9
+ * Visual Hierarchy Rationale:
10
+ * - Native date input for familiar UX
11
+ * - Calendar picker on click (browser default)
12
+ * - ISO date format for consistency
13
+ *
14
+ * Interaction Notes:
15
+ * - Click to open calendar picker
16
+ * - Keyboard entry also supported (YYYY-MM-DD)
17
+ * - Respects min/max constraints
18
+ *
19
+ * Accessibility Considerations:
20
+ * - Native date input is screen reader friendly
21
+ * - Keyboard navigation supported
22
+ */
23
+
24
+ interface DateValueInputProps {
25
+ value: string | null;
26
+ onChange: (value: string | null) => void;
27
+ min?: string;
28
+ max?: string;
29
+ placeholder?: string;
30
+ error?: string;
31
+ className?: string;
32
+ }
33
+
34
+ export function DateValueInput({
35
+ value,
36
+ onChange,
37
+ min,
38
+ max,
39
+ placeholder = 'Select date...',
40
+ error,
41
+ className = '',
42
+ }: DateValueInputProps) {
43
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
44
+ const val = e.target.value;
45
+ onChange(val || null);
46
+ };
47
+
48
+ return (
49
+ <div className={`flex flex-col gap-1 ${className}`}>
50
+ <label htmlFor="date-value" className="text-xs font-medium text-gray-700">
51
+ Value
52
+ </label>
53
+ <input
54
+ id="date-value"
55
+ type="date"
56
+ value={value || ''}
57
+ onChange={handleChange}
58
+ min={min}
59
+ max={max}
60
+ placeholder={placeholder}
61
+ className={`
62
+ w-full px-3 py-2 text-sm border rounded-md
63
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
64
+ ${error ? 'border-red-500' : 'border-gray-300'}
65
+ `}
66
+ />
67
+ {error && <span className="text-xs text-red-600">{error}</span>}
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * MultiChoiceValueInput Component
3
+ *
4
+ * Multi-select for choosing multiple values from choices.
5
+ *
6
+ * Design Goal:
7
+ * Select multiple values for 'in', 'has_any', 'has_all' operators.
8
+ * For example: "Firm stage is one of [Series A, Series B, Series C]".
9
+ *
10
+ * Visual Hierarchy Rationale:
11
+ * - Tag-based interface shows selected values as pills
12
+ * - Each tag has an X button to remove
13
+ * - Dropdown allows adding more selections
14
+ * - Compact layout for multiple values
15
+ *
16
+ * Interaction Notes:
17
+ * - Select from dropdown to add value
18
+ * - Click X on tag to remove value
19
+ * - Can select multiple values
20
+ * - Already-selected values hidden from dropdown
21
+ *
22
+ * Accessibility Considerations:
23
+ * - Keyboard navigation for dropdown
24
+ * - Focus management for tag removal
25
+ * - Screen reader announces selected items
26
+ */
27
+
28
+ interface MultiChoiceValueInputProps {
29
+ value: string[];
30
+ onChange: (value: string[]) => void;
31
+ choices: any[];
32
+ placeholder?: string;
33
+ error?: string;
34
+ className?: string;
35
+ }
36
+
37
+ export function MultiChoiceValueInput({
38
+ value = [],
39
+ onChange,
40
+ choices,
41
+ placeholder = 'Select options...',
42
+ error,
43
+ className = '',
44
+ }: MultiChoiceValueInputProps) {
45
+ // Handle both string arrays and {value, label} arrays
46
+ const getChoiceValue = (choice: any): string => {
47
+ if (typeof choice === 'string') return choice;
48
+ return choice.value || choice;
49
+ };
50
+
51
+ const getChoiceLabel = (choice: any): string => {
52
+ if (typeof choice === 'string') return choice;
53
+ return choice.label || choice.value || String(choice);
54
+ };
55
+
56
+ const handleAdd = (newValue: string) => {
57
+ if (newValue && !value.includes(newValue)) {
58
+ onChange([...value, newValue]);
59
+ }
60
+ };
61
+
62
+ const handleRemove = (removeValue: string) => {
63
+ onChange(value.filter((v) => v !== removeValue));
64
+ };
65
+
66
+ // Get label for a selected value
67
+ const getValueLabel = (val: string): string => {
68
+ const choice = choices.find((c) => getChoiceValue(c) === val);
69
+ return choice ? getChoiceLabel(choice) : val;
70
+ };
71
+
72
+ // Filter out already-selected values from dropdown
73
+ const availableChoices = choices.filter(
74
+ (choice) => !value.includes(getChoiceValue(choice))
75
+ );
76
+
77
+ return (
78
+ <div className={`flex flex-col gap-2 ${className}`}>
79
+ <label htmlFor="multi-choice-value" className="text-xs font-medium text-gray-700">
80
+ Values
81
+ </label>
82
+
83
+ {/* Selected tags */}
84
+ {value.length > 0 && (
85
+ <div className="flex flex-wrap gap-1.5">
86
+ {value.map((val) => (
87
+ <span
88
+ key={val}
89
+ className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded"
90
+ >
91
+ {getValueLabel(val)}
92
+ <button
93
+ type="button"
94
+ onClick={() => handleRemove(val)}
95
+ className="hover:text-blue-900 focus:outline-none"
96
+ aria-label={`Remove ${getValueLabel(val)}`}
97
+ >
98
+ ×
99
+ </button>
100
+ </span>
101
+ ))}
102
+ </div>
103
+ )}
104
+
105
+ {/* Dropdown to add more */}
106
+ <select
107
+ id="multi-choice-value"
108
+ value=""
109
+ onChange={(e) => handleAdd(e.target.value)}
110
+ className={`
111
+ w-full px-3 py-2 text-sm border rounded-md
112
+ bg-white
113
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
114
+ ${error ? 'border-red-500' : 'border-gray-300'}
115
+ `}
116
+ >
117
+ <option value="">{placeholder}</option>
118
+ {availableChoices.map((choice, index) => {
119
+ const val = getChoiceValue(choice);
120
+ const label = getChoiceLabel(choice);
121
+ return (
122
+ <option key={`${val}-${index}`} value={val}>
123
+ {label}
124
+ </option>
125
+ );
126
+ })}
127
+ </select>
128
+
129
+ {error && <span className="text-xs text-red-600">{error}</span>}
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * NumberValueInput Component
3
+ *
4
+ * Numeric input for number comparisons.
5
+ *
6
+ * Design Goal:
7
+ * Precise number entry with validation for numeric fields.
8
+ *
9
+ * Visual Hierarchy Rationale:
10
+ * - Standard number input with browser controls
11
+ * - Constraints (min/max) applied if provided
12
+ * - Error state clearly indicated
13
+ *
14
+ * Interaction Notes:
15
+ * - Arrow keys increment/decrement
16
+ * - Scroll wheel adjusts value (browser default)
17
+ * - Respects min/max constraints
18
+ */
19
+
20
+ interface NumberValueInputProps {
21
+ value: number | null;
22
+ onChange: (value: number | null) => void;
23
+ min?: number;
24
+ max?: number;
25
+ placeholder?: string;
26
+ error?: string;
27
+ className?: string;
28
+ }
29
+
30
+ export function NumberValueInput({
31
+ value,
32
+ onChange,
33
+ min,
34
+ max,
35
+ placeholder = 'Enter number...',
36
+ error,
37
+ className = '',
38
+ }: NumberValueInputProps) {
39
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
40
+ const val = e.target.value;
41
+ if (val === '') {
42
+ onChange(null);
43
+ } else {
44
+ const num = parseFloat(val);
45
+ if (!isNaN(num)) {
46
+ onChange(num);
47
+ }
48
+ }
49
+ };
50
+
51
+ return (
52
+ <div className={`flex flex-col gap-1 ${className}`}>
53
+ <label htmlFor="number-value" className="text-xs font-medium text-gray-700">
54
+ Value
55
+ </label>
56
+ <input
57
+ id="number-value"
58
+ type="number"
59
+ value={value ?? ''}
60
+ onChange={handleChange}
61
+ min={min}
62
+ max={max}
63
+ placeholder={placeholder}
64
+ className={`
65
+ w-full px-3 py-2 text-sm border rounded-md
66
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
67
+ ${error ? 'border-red-500' : 'border-gray-300'}
68
+ `}
69
+ />
70
+ {error && <span className="text-xs text-red-600">{error}</span>}
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * TextValueInput Component
3
+ *
4
+ * Text input for string values.
5
+ *
6
+ * Design Goal:
7
+ * Simple text entry for string comparisons (contains, equals, etc.).
8
+ *
9
+ * Visual Hierarchy Rationale:
10
+ * - Standard text input styling
11
+ * - Placeholder text provides context
12
+ * - Error state clearly indicated with red border
13
+ */
14
+
15
+ interface TextValueInputProps {
16
+ value: string;
17
+ onChange: (value: string) => void;
18
+ placeholder?: string;
19
+ error?: string;
20
+ className?: string;
21
+ }
22
+
23
+ export function TextValueInput({
24
+ value,
25
+ onChange,
26
+ placeholder = 'Enter text...',
27
+ error,
28
+ className = '',
29
+ }: TextValueInputProps) {
30
+ return (
31
+ <div className={`flex flex-col gap-1 ${className}`}>
32
+ <label htmlFor="text-value" className="text-xs font-medium text-gray-700">
33
+ Value
34
+ </label>
35
+ <input
36
+ id="text-value"
37
+ type="text"
38
+ value={value || ''}
39
+ onChange={(e) => onChange(e.target.value)}
40
+ placeholder={placeholder}
41
+ className={`
42
+ w-full px-3 py-2 text-sm border rounded-md
43
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
44
+ ${error ? 'border-red-500' : 'border-gray-300'}
45
+ `}
46
+ />
47
+ {error && <span className="text-xs text-red-600">{error}</span>}
48
+ </div>
49
+ );
50
+ }