@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,205 @@
1
+ /**
2
+ * StageActions tests
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { render, screen, fireEvent } from '@testing-library/react';
7
+ import { StageActions } from './StageActions';
8
+ import { FunnelStage } from '../../types';
9
+
10
+ // Create test stage
11
+ function createTestStage(): FunnelStage {
12
+ return {
13
+ id: 'stage-1',
14
+ order: 0,
15
+ name: 'Test Stage',
16
+ filter_logic: 'AND',
17
+ rules: [],
18
+ match_action: 'continue',
19
+ no_match_action: 'continue',
20
+ };
21
+ }
22
+
23
+ describe('StageActions', () => {
24
+ it('renders match action dropdown', () => {
25
+ const stage = createTestStage();
26
+ const onMatchActionChange = vi.fn();
27
+ const onNoMatchActionChange = vi.fn();
28
+
29
+ render(
30
+ <StageActions
31
+ stage={stage}
32
+ onMatchActionChange={onMatchActionChange}
33
+ onNoMatchActionChange={onNoMatchActionChange}
34
+ />
35
+ );
36
+
37
+ expect(screen.getByLabelText(/Action on Match/i)).toBeInTheDocument();
38
+ });
39
+
40
+ it('renders no match action dropdown', () => {
41
+ const stage = createTestStage();
42
+ const onMatchActionChange = vi.fn();
43
+ const onNoMatchActionChange = vi.fn();
44
+
45
+ render(
46
+ <StageActions
47
+ stage={stage}
48
+ onMatchActionChange={onMatchActionChange}
49
+ onNoMatchActionChange={onNoMatchActionChange}
50
+ />
51
+ );
52
+
53
+ expect(screen.getByLabelText(/Action on No Match/i)).toBeInTheDocument();
54
+ });
55
+
56
+ it('displays all match action options', () => {
57
+ const stage = createTestStage();
58
+ const onMatchActionChange = vi.fn();
59
+ const onNoMatchActionChange = vi.fn();
60
+
61
+ render(
62
+ <StageActions
63
+ stage={stage}
64
+ onMatchActionChange={onMatchActionChange}
65
+ onNoMatchActionChange={onNoMatchActionChange}
66
+ />
67
+ );
68
+
69
+ const matchSelect = screen.getByLabelText(/Action on Match/i) as HTMLSelectElement;
70
+ const options = Array.from(matchSelect.options).map(opt => opt.value);
71
+
72
+ expect(options).toEqual(['continue', 'tag', 'tag_continue', 'output']);
73
+ });
74
+
75
+ it('displays all no match action options', () => {
76
+ const stage = createTestStage();
77
+ const onMatchActionChange = vi.fn();
78
+ const onNoMatchActionChange = vi.fn();
79
+
80
+ render(
81
+ <StageActions
82
+ stage={stage}
83
+ onMatchActionChange={onMatchActionChange}
84
+ onNoMatchActionChange={onNoMatchActionChange}
85
+ />
86
+ );
87
+
88
+ const noMatchSelect = screen.getByLabelText(/Action on No Match/i) as HTMLSelectElement;
89
+ const options = Array.from(noMatchSelect.options).map(opt => opt.value);
90
+
91
+ expect(options).toEqual(['continue', 'exclude', 'tag_exclude']);
92
+ });
93
+
94
+ it('calls onMatchActionChange when match action changes', () => {
95
+ const stage = createTestStage();
96
+ const onMatchActionChange = vi.fn();
97
+ const onNoMatchActionChange = vi.fn();
98
+
99
+ render(
100
+ <StageActions
101
+ stage={stage}
102
+ onMatchActionChange={onMatchActionChange}
103
+ onNoMatchActionChange={onNoMatchActionChange}
104
+ />
105
+ );
106
+
107
+ const matchSelect = screen.getByLabelText(/Action on Match/i);
108
+ fireEvent.change(matchSelect, { target: { value: 'output' } });
109
+
110
+ expect(onMatchActionChange).toHaveBeenCalledWith('output');
111
+ });
112
+
113
+ it('calls onNoMatchActionChange when no match action changes', () => {
114
+ const stage = createTestStage();
115
+ const onMatchActionChange = vi.fn();
116
+ const onNoMatchActionChange = vi.fn();
117
+
118
+ render(
119
+ <StageActions
120
+ stage={stage}
121
+ onMatchActionChange={onMatchActionChange}
122
+ onNoMatchActionChange={onNoMatchActionChange}
123
+ />
124
+ );
125
+
126
+ const noMatchSelect = screen.getByLabelText(/Action on No Match/i);
127
+ fireEvent.change(noMatchSelect, { target: { value: 'exclude' } });
128
+
129
+ expect(onNoMatchActionChange).toHaveBeenCalledWith('exclude');
130
+ });
131
+
132
+ it('displays current match action', () => {
133
+ const stage = createTestStage();
134
+ stage.match_action = 'tag_continue';
135
+
136
+ const onMatchActionChange = vi.fn();
137
+ const onNoMatchActionChange = vi.fn();
138
+
139
+ render(
140
+ <StageActions
141
+ stage={stage}
142
+ onMatchActionChange={onMatchActionChange}
143
+ onNoMatchActionChange={onNoMatchActionChange}
144
+ />
145
+ );
146
+
147
+ const matchSelect = screen.getByLabelText(/Action on Match/i) as HTMLSelectElement;
148
+ expect(matchSelect.value).toBe('tag_continue');
149
+ });
150
+
151
+ it('displays current no match action', () => {
152
+ const stage = createTestStage();
153
+ stage.no_match_action = 'tag_exclude';
154
+
155
+ const onMatchActionChange = vi.fn();
156
+ const onNoMatchActionChange = vi.fn();
157
+
158
+ render(
159
+ <StageActions
160
+ stage={stage}
161
+ onMatchActionChange={onMatchActionChange}
162
+ onNoMatchActionChange={onNoMatchActionChange}
163
+ />
164
+ );
165
+
166
+ const noMatchSelect = screen.getByLabelText(/Action on No Match/i) as HTMLSelectElement;
167
+ expect(noMatchSelect.value).toBe('tag_exclude');
168
+ });
169
+
170
+ it('shows description for match action', () => {
171
+ const stage = createTestStage();
172
+ stage.match_action = 'output';
173
+
174
+ const onMatchActionChange = vi.fn();
175
+ const onNoMatchActionChange = vi.fn();
176
+
177
+ render(
178
+ <StageActions
179
+ stage={stage}
180
+ onMatchActionChange={onMatchActionChange}
181
+ onNoMatchActionChange={onNoMatchActionChange}
182
+ />
183
+ );
184
+
185
+ expect(screen.getByText(/Add to output and stop processing/i)).toBeInTheDocument();
186
+ });
187
+
188
+ it('shows description for no match action', () => {
189
+ const stage = createTestStage();
190
+ stage.no_match_action = 'exclude';
191
+
192
+ const onMatchActionChange = vi.fn();
193
+ const onNoMatchActionChange = vi.fn();
194
+
195
+ render(
196
+ <StageActions
197
+ stage={stage}
198
+ onMatchActionChange={onMatchActionChange}
199
+ onNoMatchActionChange={onNoMatchActionChange}
200
+ />
201
+ );
202
+
203
+ expect(screen.getByText(/Exclude from output and stop processing/i)).toBeInTheDocument();
204
+ });
205
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * StageActions - Action dropdowns and toggles
3
+ *
4
+ * Features:
5
+ * - Match action dropdown
6
+ * - No match action dropdown
7
+ * - Clear labels and descriptions
8
+ */
9
+
10
+ import React from 'react';
11
+ import { FunnelStage, MatchAction, NoMatchAction } from '../../types';
12
+
13
+ export interface StageActionsProps {
14
+ /** The stage being configured */
15
+ stage: FunnelStage;
16
+
17
+ /** Match action change handler */
18
+ onMatchActionChange: (action: MatchAction) => void;
19
+
20
+ /** No match action change handler */
21
+ onNoMatchActionChange: (action: NoMatchAction) => void;
22
+ }
23
+
24
+ /**
25
+ * Match action options with descriptions
26
+ */
27
+ const MATCH_ACTIONS: Array<{ value: MatchAction; label: string; description: string }> = [
28
+ {
29
+ value: 'continue',
30
+ label: 'Continue',
31
+ description: 'Continue to next stage without tagging',
32
+ },
33
+ {
34
+ value: 'tag',
35
+ label: 'Tag & Stop',
36
+ description: 'Add tags and stop processing',
37
+ },
38
+ {
39
+ value: 'tag_continue',
40
+ label: 'Tag & Continue',
41
+ description: 'Add tags and continue to next stage',
42
+ },
43
+ {
44
+ value: 'output',
45
+ label: 'Output',
46
+ description: 'Add to output and stop processing',
47
+ },
48
+ ];
49
+
50
+ /**
51
+ * No match action options with descriptions
52
+ */
53
+ const NO_MATCH_ACTIONS: Array<{ value: NoMatchAction; label: string; description: string }> = [
54
+ {
55
+ value: 'continue',
56
+ label: 'Continue',
57
+ description: 'Continue to next stage',
58
+ },
59
+ {
60
+ value: 'exclude',
61
+ label: 'Exclude',
62
+ description: 'Exclude from output and stop processing',
63
+ },
64
+ {
65
+ value: 'tag_exclude',
66
+ label: 'Tag & Exclude',
67
+ description: 'Add tags, exclude from output, and stop',
68
+ },
69
+ ];
70
+
71
+ /**
72
+ * StageActions component
73
+ */
74
+ export function StageActions({
75
+ stage,
76
+ onMatchActionChange,
77
+ onNoMatchActionChange,
78
+ }: StageActionsProps) {
79
+ return (
80
+ <div className="stage-actions">
81
+ {/* Match action */}
82
+ <div className="form-group">
83
+ <label htmlFor={`match-action-${stage.id}`} className="form-label">
84
+ Action on Match
85
+ </label>
86
+ <select
87
+ id={`match-action-${stage.id}`}
88
+ value={stage.match_action}
89
+ onChange={(e) => onMatchActionChange(e.target.value as MatchAction)}
90
+ className="form-select"
91
+ >
92
+ {MATCH_ACTIONS.map((option) => (
93
+ <option key={option.value} value={option.value}>
94
+ {option.label}
95
+ </option>
96
+ ))}
97
+ </select>
98
+ <p className="form-hint">
99
+ {MATCH_ACTIONS.find((a) => a.value === stage.match_action)?.description}
100
+ </p>
101
+ </div>
102
+
103
+ {/* No match action */}
104
+ <div className="form-group">
105
+ <label htmlFor={`no-match-action-${stage.id}`} className="form-label">
106
+ Action on No Match
107
+ </label>
108
+ <select
109
+ id={`no-match-action-${stage.id}`}
110
+ value={stage.no_match_action}
111
+ onChange={(e) => onNoMatchActionChange(e.target.value as NoMatchAction)}
112
+ className="form-select"
113
+ >
114
+ {NO_MATCH_ACTIONS.map((option) => (
115
+ <option key={option.value} value={option.value}>
116
+ {option.label}
117
+ </option>
118
+ ))}
119
+ </select>
120
+ <p className="form-hint">
121
+ {NO_MATCH_ACTIONS.find((a) => a.value === stage.no_match_action)?.description}
122
+ </p>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * StageCard - Individual stage card with drag handle
3
+ *
4
+ * Features:
5
+ * - Drag handle for reordering
6
+ * - Expand/collapse
7
+ * - Delete button
8
+ * - Stage configuration form when expanded
9
+ * - Validation errors and warnings
10
+ */
11
+
12
+ import React from 'react';
13
+ import { useSortable } from '@dnd-kit/sortable';
14
+ import { CSS } from '@dnd-kit/utilities';
15
+ import { FunnelStage, FieldDefinition } from '../../types';
16
+ import { StageForm } from './StageForm';
17
+
18
+ export interface StageCardProps {
19
+ /** The stage to display */
20
+ stage: FunnelStage;
21
+
22
+ /** Whether stage is expanded */
23
+ expanded: boolean;
24
+
25
+ /** Toggle expanded state */
26
+ onToggleExpanded: () => void;
27
+
28
+ /** Update stage */
29
+ onUpdate: (stage: FunnelStage) => void;
30
+
31
+ /** Remove stage */
32
+ onRemove: () => void;
33
+
34
+ /** Available fields */
35
+ fieldRegistry: FieldDefinition[];
36
+
37
+ /** Validation error */
38
+ error?: string;
39
+
40
+ /** Show warnings */
41
+ showWarnings?: boolean;
42
+ }
43
+
44
+ /**
45
+ * StageCard component
46
+ */
47
+ export function StageCard({
48
+ stage,
49
+ expanded,
50
+ onToggleExpanded,
51
+ onUpdate,
52
+ onRemove,
53
+ fieldRegistry,
54
+ error,
55
+ showWarnings = false,
56
+ }: StageCardProps) {
57
+ const {
58
+ attributes,
59
+ listeners,
60
+ setNodeRef,
61
+ transform,
62
+ transition,
63
+ isDragging,
64
+ } = useSortable({ id: stage.id });
65
+
66
+ const style = {
67
+ transform: CSS.Transform.toString(transform),
68
+ transition,
69
+ opacity: isDragging ? 0.5 : 1,
70
+ };
71
+
72
+ return (
73
+ <div
74
+ ref={setNodeRef}
75
+ style={style}
76
+ className={`stage-card ${isDragging ? 'dragging' : ''} ${error ? 'error' : ''}`}
77
+ >
78
+ {/* Header */}
79
+ <div className="stage-header">
80
+ {/* Drag handle */}
81
+ <button
82
+ {...attributes}
83
+ {...listeners}
84
+ className="drag-handle"
85
+ aria-label="Drag to reorder"
86
+ >
87
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
88
+ <path
89
+ d="M7 4h6M7 10h6M7 16h6"
90
+ stroke="currentColor"
91
+ strokeWidth="2"
92
+ strokeLinecap="round"
93
+ />
94
+ </svg>
95
+ </button>
96
+
97
+ {/* Stage title (clickable to expand/collapse) */}
98
+ <button
99
+ onClick={onToggleExpanded}
100
+ className="stage-title-button"
101
+ aria-expanded={expanded}
102
+ >
103
+ <span className="stage-number">Stage {stage.order + 1}:</span>
104
+ <span className="stage-name">{stage.name || 'Untitled Stage'}</span>
105
+
106
+ {/* Expand/collapse icon */}
107
+ <svg
108
+ width="20"
109
+ height="20"
110
+ viewBox="0 0 20 20"
111
+ fill="none"
112
+ className={`expand-icon ${expanded ? 'expanded' : ''}`}
113
+ >
114
+ <path
115
+ d="M6 8l4 4 4-4"
116
+ stroke="currentColor"
117
+ strokeWidth="2"
118
+ strokeLinecap="round"
119
+ strokeLinejoin="round"
120
+ />
121
+ </svg>
122
+ </button>
123
+
124
+ {/* Delete button */}
125
+ <button
126
+ onClick={onRemove}
127
+ className="delete-button"
128
+ aria-label="Delete stage"
129
+ >
130
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
131
+ <path
132
+ d="M6 6l8 8M14 6l-8 8"
133
+ stroke="currentColor"
134
+ strokeWidth="2"
135
+ strokeLinecap="round"
136
+ />
137
+ </svg>
138
+ </button>
139
+ </div>
140
+
141
+ {/* Validation error */}
142
+ {error && (
143
+ <div className="error-message">
144
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
145
+ <path
146
+ d="M8 1l7 13H1L8 1z"
147
+ stroke="currentColor"
148
+ strokeWidth="2"
149
+ strokeLinejoin="round"
150
+ />
151
+ <path d="M8 6v3M8 11h.01" stroke="currentColor" strokeWidth="2" />
152
+ </svg>
153
+ <span>{error}</span>
154
+ </div>
155
+ )}
156
+
157
+ {/* Warning (no rules) */}
158
+ {showWarnings && !error && (
159
+ <div className="warning-message">
160
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
161
+ <circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="2" />
162
+ <path d="M8 5v3M8 10h.01" stroke="currentColor" strokeWidth="2" />
163
+ </svg>
164
+ <span>Stage has no filter rules</span>
165
+ </div>
166
+ )}
167
+
168
+ {/* Collapsed summary */}
169
+ {!expanded && (
170
+ <div className="stage-summary">
171
+ <div className="summary-item">
172
+ <span className="summary-label">Rules:</span>
173
+ <span className="summary-value">{stage.rules.length}</span>
174
+ </div>
175
+ <div className="summary-item">
176
+ <span className="summary-label">Logic:</span>
177
+ <span className="summary-value">{stage.filter_logic}</span>
178
+ </div>
179
+ <div className="summary-item">
180
+ <span className="summary-label">On Match:</span>
181
+ <span className="summary-value">{stage.match_action}</span>
182
+ </div>
183
+ <div className="summary-item">
184
+ <span className="summary-label">On No Match:</span>
185
+ <span className="summary-value">{stage.no_match_action}</span>
186
+ </div>
187
+ </div>
188
+ )}
189
+
190
+ {/* Expanded form */}
191
+ {expanded && (
192
+ <div className="stage-form-wrapper">
193
+ <StageForm
194
+ stage={stage}
195
+ onUpdate={onUpdate}
196
+ fieldRegistry={fieldRegistry}
197
+ />
198
+ </div>
199
+ )}
200
+ </div>
201
+ );
202
+ }