@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.
- package/package.json +9 -31
- package/src/api/README.md +507 -0
- package/src/api/adapter.ts +106 -0
- package/src/api/client.test.ts +640 -0
- package/src/api/client.ts +385 -0
- package/src/api/default-adapter.ts +243 -0
- package/src/api/index.ts +24 -0
- package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
- package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
- package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
- package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
- package/src/components/FilterRuleEditor/README.md +291 -0
- package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
- package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
- package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
- package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
- package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
- package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
- package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
- package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
- package/src/components/FilterRuleEditor/constants.ts +64 -0
- package/src/components/FilterRuleEditor/index.ts +14 -0
- package/src/components/FunnelCard/DESIGN.md +447 -0
- package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
- package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
- package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
- package/src/components/FunnelCard/FunnelCard.tsx +204 -0
- package/src/components/FunnelCard/FunnelStats.tsx +68 -0
- package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
- package/src/components/FunnelCard/INSTALLATION.md +304 -0
- package/src/components/FunnelCard/MatchBar.tsx +49 -0
- package/src/components/FunnelCard/README.md +294 -0
- package/src/components/FunnelCard/StageIndicator.tsx +62 -0
- package/src/components/FunnelCard/StatusBadge.tsx +52 -0
- package/src/components/FunnelCard/index.ts +14 -0
- package/src/components/FunnelPreview/EntityCard.tsx +72 -0
- package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
- package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
- package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
- package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
- package/src/components/FunnelPreview/README.md +337 -0
- package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
- package/src/components/FunnelPreview/example.tsx +286 -0
- package/src/components/FunnelPreview/index.ts +14 -0
- package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
- package/src/components/FunnelRunHistory/README.md +325 -0
- package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
- package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
- package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
- package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
- package/src/components/FunnelRunHistory/index.ts +51 -0
- package/src/components/FunnelRunHistory/types.ts +40 -0
- package/src/components/FunnelRunHistory/utils.test.ts +126 -0
- package/src/components/FunnelRunHistory/utils.ts +100 -0
- package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
- package/src/components/FunnelStageBuilder/README.md +341 -0
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
- package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
- package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
- package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
- package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
- package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
- package/src/components/FunnelStageBuilder/index.ts +21 -0
- package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
- package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
- package/src/components/FunnelVisualFlow/README.md +323 -0
- package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
- package/src/components/FunnelVisualFlow/example.tsx +227 -0
- package/src/components/FunnelVisualFlow/index.ts +10 -0
- package/src/components/index.ts +102 -0
- package/src/core/README.md +307 -0
- package/src/core/engine.test.ts +1087 -0
- package/src/core/engine.ts +329 -0
- package/src/core/evaluator.example.ts +353 -0
- package/src/core/evaluator.test.ts +639 -0
- package/src/core/evaluator.ts +261 -0
- package/src/core/field-resolver.example.ts +175 -0
- package/src/core/field-resolver.test.ts +541 -0
- package/src/core/field-resolver.ts +247 -0
- package/src/core/index.ts +34 -0
- package/src/core/operators.test.ts +539 -0
- package/src/core/operators.ts +241 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useDebouncedValue.ts +28 -0
- package/src/index.ts +155 -0
- package/src/store/README.md +342 -0
- package/src/store/create-funnel-store.test.ts +686 -0
- package/src/store/create-funnel-store.ts +538 -0
- package/src/store/index.ts +9 -0
- package/src/store/types.ts +294 -0
- package/src/stories/CrossDomain.stories.tsx +149 -0
- package/src/stories/Welcome.stories.tsx +81 -0
- package/src/stories/demo-data/index.ts +3 -0
- package/src/stories/demo-data/investors.ts +216 -0
- package/src/stories/demo-data/leads.ts +223 -0
- package/src/stories/demo-data/recipes.ts +217 -0
- package/src/test/setup.ts +5 -0
- package/src/types/index.ts +843 -0
- package/dist/client-3ESO2NHy.d.ts +0 -310
- package/dist/client-CZu03ACp.d.cts +0 -310
- package/dist/components/index.cjs +0 -3243
- package/dist/components/index.cjs.map +0 -1
- package/dist/components/index.css.map +0 -1
- package/dist/components/index.d.cts +0 -726
- package/dist/components/index.d.ts +0 -726
- package/dist/components/index.js +0 -3196
- package/dist/components/index.js.map +0 -1
- package/dist/core/index.cjs +0 -500
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -359
- package/dist/core/index.d.ts +0 -359
- package/dist/core/index.js +0 -486
- package/dist/core/index.js.map +0 -1
- package/dist/hooks/index.cjs +0 -21
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.d.cts +0 -11
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.js +0 -19
- package/dist/hooks/index.js.map +0 -1
- package/dist/index-BGDEXbuz.d.cts +0 -434
- package/dist/index-BGDEXbuz.d.ts +0 -434
- package/dist/index.cjs +0 -4499
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -198
- package/dist/index.css.map +0 -1
- package/dist/index.d.cts +0 -99
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -4421
- package/dist/index.js.map +0 -1
- package/dist/store/index.cjs +0 -391
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -225
- package/dist/store/index.d.ts +0 -225
- package/dist/store/index.js +0 -388
- 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
|
+
}
|