@startsimpli/ui 0.4.5 → 0.4.7

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 (69) hide show
  1. package/package.json +2 -1
  2. package/src/components/ActivityTimeline.tsx +173 -0
  3. package/src/components/LogActivityDialog.tsx +303 -0
  4. package/src/components/QuickLogButtons.tsx +32 -0
  5. package/src/components/badge/StageBadge.tsx +31 -0
  6. package/src/components/badge/index.ts +3 -0
  7. package/src/components/command-palette/CommandPalette.tsx +344 -0
  8. package/src/components/command-palette/command-palette-context.tsx +51 -0
  9. package/src/components/command-palette/index.ts +3 -0
  10. package/src/components/compose/compose-header.tsx +72 -0
  11. package/src/components/compose/compose-loading.tsx +13 -0
  12. package/src/components/compose/index.ts +6 -0
  13. package/src/components/compose/save-status-indicator.tsx +57 -0
  14. package/src/components/compose/send-confirmation-dialog.tsx +87 -0
  15. package/src/components/compose/subject-input.tsx +25 -0
  16. package/src/components/compose/useAutoSave.ts +93 -0
  17. package/src/components/dashboard/DashboardGrid.tsx +32 -0
  18. package/src/components/dashboard/DashboardSection.tsx +32 -0
  19. package/src/components/dashboard/MetricCard.tsx +129 -0
  20. package/src/components/dashboard/PeriodSelector.tsx +55 -0
  21. package/src/components/dashboard/SparklineTrend.tsx +102 -0
  22. package/src/components/dashboard/index.ts +14 -0
  23. package/src/components/email-dialogs/index.ts +14 -0
  24. package/src/components/email-dialogs/merge-fields.tsx +196 -0
  25. package/src/components/email-dialogs/preview-dialog.tsx +194 -0
  26. package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
  27. package/src/components/email-dialogs/template-picker.tsx +225 -0
  28. package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
  29. package/src/components/email-editor/add-block-menu.tsx +151 -0
  30. package/src/components/email-editor/block-toolbar.tsx +73 -0
  31. package/src/components/email-editor/blocks/button-block.tsx +44 -0
  32. package/src/components/email-editor/blocks/divider-block.tsx +43 -0
  33. package/src/components/email-editor/blocks/footer-block.tsx +39 -0
  34. package/src/components/email-editor/blocks/header-block.tsx +39 -0
  35. package/src/components/email-editor/blocks/image-block.tsx +61 -0
  36. package/src/components/email-editor/blocks/index.ts +9 -0
  37. package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
  38. package/src/components/email-editor/blocks/social-block.tsx +75 -0
  39. package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
  40. package/src/components/email-editor/blocks/text-block.tsx +75 -0
  41. package/src/components/email-editor/editor-sidebar.tsx +791 -0
  42. package/src/components/email-editor/email-editor.tsx +886 -0
  43. package/src/components/email-editor/index.ts +50 -0
  44. package/src/components/email-editor/renderer/block-renderers.ts +209 -0
  45. package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
  46. package/src/components/email-editor/types.ts +413 -0
  47. package/src/components/email-editor/utils/defaults.ts +116 -0
  48. package/src/components/email-editor/utils/undo-redo.ts +59 -0
  49. package/src/components/enrichment/EnrichButton.tsx +33 -0
  50. package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
  51. package/src/components/enrichment/QualityBadge.tsx +43 -0
  52. package/src/components/enrichment/index.ts +8 -0
  53. package/src/components/gantt/GanttChart.tsx +25 -25
  54. package/src/components/gantt/types.ts +5 -5
  55. package/src/components/index.ts +46 -0
  56. package/src/components/integrations/ConnectionStatus.tsx +77 -0
  57. package/src/components/integrations/IntegrationCard.tsx +92 -0
  58. package/src/components/integrations/index.ts +5 -0
  59. package/src/components/kanban/KanbanBoard.tsx +103 -0
  60. package/src/components/kanban/index.ts +2 -0
  61. package/src/components/lists/CreateListDialog.tsx +158 -0
  62. package/src/components/lists/ListCard.tsx +77 -0
  63. package/src/components/lists/index.ts +5 -0
  64. package/src/components/pipeline/StageTransitionModal.tsx +146 -0
  65. package/src/components/pipeline/index.ts +2 -0
  66. package/src/components/settings/SettingsCard.tsx +33 -0
  67. package/src/components/settings/SettingsLayout.tsx +28 -0
  68. package/src/components/settings/SettingsNav.tsx +42 -0
  69. package/src/components/settings/index.ts +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/ui",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Shared UI components package for StartSimpli applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -13,6 +13,7 @@
13
13
  "./utils": "./src/utils/index.ts",
14
14
  "./theme": "./src/theme/index.ts",
15
15
  "./theme/contract": "./theme/contract.css",
16
+ "./email-editor": "./src/components/email-editor/index.ts",
16
17
  "./tailwind": "./tailwind.preset.js"
17
18
  },
18
19
  "files": [
@@ -0,0 +1,173 @@
1
+ 'use client';
2
+
3
+ import { ComponentType } from 'react';
4
+ import { formatDistanceToNow } from 'date-fns';
5
+ import { Clock, User } from 'lucide-react';
6
+ import { EmptyState } from './states';
7
+
8
+ export interface ActivityTimelineItem {
9
+ id: string;
10
+ type: string;
11
+ title: string;
12
+ description?: string;
13
+ occurredAt: string;
14
+ createdBy: { name: string };
15
+ durationMinutes?: number;
16
+ outcome?: string;
17
+ isAutomated?: boolean;
18
+ participants?: Array<{ name?: string }>;
19
+ }
20
+
21
+ export interface ActivityTimelineProps {
22
+ activities: ActivityTimelineItem[];
23
+ loading: boolean;
24
+ emptyTitle?: string;
25
+ emptyDescription?: string;
26
+ onLoadMore?: () => void;
27
+ hasMore?: boolean;
28
+ activityIcons?: Record<string, ComponentType<any>>;
29
+ activityColors?: Record<string, string>;
30
+ formatTime?: (date: string) => string;
31
+ }
32
+
33
+ function defaultFormatTime(date: string): string {
34
+ try {
35
+ return formatDistanceToNow(new Date(date), { addSuffix: true });
36
+ } catch {
37
+ return '';
38
+ }
39
+ }
40
+
41
+ export function ActivityTimeline({
42
+ activities,
43
+ loading,
44
+ emptyTitle = 'No activities yet',
45
+ emptyDescription = 'Log calls, meetings, and notes to track your interactions',
46
+ onLoadMore,
47
+ hasMore = false,
48
+ activityIcons = {},
49
+ activityColors = {},
50
+ formatTime = defaultFormatTime,
51
+ }: ActivityTimelineProps) {
52
+ if (loading) {
53
+ return (
54
+ <div className="space-y-4">
55
+ {[...Array(3)].map((_, i) => (
56
+ <div key={i} className="flex gap-4 animate-pulse">
57
+ <div className="w-10 h-10 bg-gray-200 rounded-full" />
58
+ <div className="flex-1">
59
+ <div className="h-4 bg-gray-200 rounded w-1/3 mb-2" />
60
+ <div className="h-3 bg-gray-200 rounded w-2/3" />
61
+ </div>
62
+ </div>
63
+ ))}
64
+ </div>
65
+ );
66
+ }
67
+
68
+ return (
69
+ <div className="space-y-4">
70
+ {/* Timeline */}
71
+ <div className="relative">
72
+ {/* Timeline line */}
73
+ <div className="absolute left-5 top-0 bottom-0 w-0.5 bg-gray-200" />
74
+
75
+ {/* Activities */}
76
+ <div className="space-y-6">
77
+ {activities.length === 0 ? (
78
+ <EmptyState title={emptyTitle} description={emptyDescription} />
79
+ ) : (
80
+ activities.map((activity) => {
81
+ const Icon = activityIcons[activity.type];
82
+ const colorClass =
83
+ activityColors[activity.type] || 'bg-gray-100 text-gray-600';
84
+
85
+ return (
86
+ <div key={activity.id} className="relative flex gap-4">
87
+ {/* Icon */}
88
+ <div
89
+ className={`relative z-10 flex items-center justify-center w-10 h-10 rounded-full ${colorClass}`}
90
+ >
91
+ {Icon ? <Icon className="w-5 h-5" /> : null}
92
+ </div>
93
+
94
+ {/* Content */}
95
+ <div className="flex-1 bg-white rounded-lg border border-gray-200 p-4">
96
+ <div className="flex items-start justify-between mb-2">
97
+ <div className="flex-1">
98
+ <h4 className="font-medium text-gray-900">
99
+ {activity.title}
100
+ </h4>
101
+ {activity.description && (
102
+ <p className="text-sm text-gray-600 mt-1">
103
+ {activity.description}
104
+ </p>
105
+ )}
106
+ </div>
107
+ <div className="text-xs text-gray-500 ml-4 whitespace-nowrap">
108
+ {formatTime(activity.occurredAt)}
109
+ </div>
110
+ </div>
111
+
112
+ {/* Metadata */}
113
+ <div className="flex items-center gap-4 text-sm text-gray-600">
114
+ <div className="flex items-center gap-1">
115
+ <User className="w-3 h-3" />
116
+ {activity.createdBy.name}
117
+ </div>
118
+
119
+ {activity.durationMinutes && (
120
+ <div className="flex items-center gap-1">
121
+ <Clock className="w-3 h-3" />
122
+ {activity.durationMinutes}m
123
+ </div>
124
+ )}
125
+
126
+ {activity.outcome && (
127
+ <div className="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 rounded">
128
+ {activity.outcome.replace(/_/g, ' ')}
129
+ </div>
130
+ )}
131
+
132
+ {activity.isAutomated && (
133
+ <div className="text-xs text-gray-500 italic">
134
+ Auto-tracked
135
+ </div>
136
+ )}
137
+ </div>
138
+
139
+ {/* Participants */}
140
+ {activity.participants &&
141
+ activity.participants.length > 0 && (
142
+ <div className="mt-2 pt-2 border-t border-gray-100">
143
+ <div className="text-xs text-gray-500">
144
+ With:{' '}
145
+ {activity.participants
146
+ .map((p) => p.name)
147
+ .filter(Boolean)
148
+ .join(', ')}
149
+ </div>
150
+ </div>
151
+ )}
152
+ </div>
153
+ </div>
154
+ );
155
+ })
156
+ )}
157
+ </div>
158
+ </div>
159
+
160
+ {/* Load More */}
161
+ {hasMore && onLoadMore && (
162
+ <div className="text-center pt-4">
163
+ <button
164
+ onClick={onLoadMore}
165
+ className="px-4 py-2 text-sm text-primary-600 hover:text-primary-700 hover:bg-primary-50 rounded-md transition-colors"
166
+ >
167
+ Load more activities
168
+ </button>
169
+ </div>
170
+ )}
171
+ </div>
172
+ );
173
+ }
@@ -0,0 +1,303 @@
1
+ 'use client';
2
+
3
+ import { useState, ComponentType } from 'react';
4
+ import { X } from 'lucide-react';
5
+
6
+ export interface ActivityTypeOption {
7
+ value: string;
8
+ label: string;
9
+ icon: ComponentType<any>;
10
+ }
11
+
12
+ export interface OutcomeOption {
13
+ value: string;
14
+ label: string;
15
+ }
16
+
17
+ export interface LogActivityFormData {
18
+ type: string;
19
+ title: string;
20
+ description: string;
21
+ outcome: string | undefined;
22
+ durationMinutes: number | undefined;
23
+ occurredAt: string;
24
+ }
25
+
26
+ export interface LogActivityDialogProps {
27
+ activityTypes: ActivityTypeOption[];
28
+ outcomeOptions: OutcomeOption[];
29
+ defaultType?: string;
30
+ onSubmit: (data: LogActivityFormData) => Promise<void>;
31
+ onClose: () => void;
32
+ isSubmitting?: boolean;
33
+ /** Activity types for which outcome is required */
34
+ outcomeRequiredTypes?: string[];
35
+ /** Activity types for which duration field is shown */
36
+ durationTypes?: string[];
37
+ /** Activity types with follow-up checkbox, mapped to outcome values that trigger it */
38
+ followUpOutcomes?: string[];
39
+ title?: string;
40
+ submitLabel?: string;
41
+ submittingLabel?: string;
42
+ }
43
+
44
+ export function LogActivityDialog({
45
+ activityTypes,
46
+ outcomeOptions,
47
+ defaultType,
48
+ onSubmit,
49
+ onClose,
50
+ isSubmitting = false,
51
+ outcomeRequiredTypes = ['call', 'meeting'],
52
+ durationTypes = ['call', 'meeting'],
53
+ followUpOutcomes = ['callback_later', 'scheduled_meeting'],
54
+ title = 'Log Activity',
55
+ submitLabel = 'Log Activity',
56
+ submittingLabel = 'Logging...',
57
+ }: LogActivityDialogProps) {
58
+ const initialType = defaultType || activityTypes[0]?.value || '';
59
+ const [activityType, setActivityType] = useState(initialType);
60
+ const [formData, setFormData] = useState({
61
+ title: '',
62
+ description: '',
63
+ outcome: undefined as string | undefined,
64
+ durationMinutes: undefined as number | undefined,
65
+ occurredAt: new Date().toISOString().slice(0, 16),
66
+ });
67
+ const [errors, setErrors] = useState<Record<string, string>>({});
68
+ const [createFollowUp, setCreateFollowUp] = useState(false);
69
+
70
+ const showOutcome = outcomeRequiredTypes.includes(activityType);
71
+ const showDuration = durationTypes.includes(activityType);
72
+ const showFollowUp =
73
+ followUpOutcomes.length > 0 &&
74
+ formData.outcome !== undefined &&
75
+ followUpOutcomes.includes(formData.outcome);
76
+
77
+ const handleSubmit = async (e: React.FormEvent) => {
78
+ e.preventDefault();
79
+
80
+ const newErrors: Record<string, string> = {};
81
+ if (!formData.title.trim()) {
82
+ newErrors.title = 'Title is required';
83
+ }
84
+ if (showOutcome && !formData.outcome) {
85
+ newErrors.outcome = 'Outcome is required for this activity type';
86
+ }
87
+
88
+ if (Object.keys(newErrors).length > 0) {
89
+ setErrors(newErrors);
90
+ return;
91
+ }
92
+
93
+ try {
94
+ await onSubmit({
95
+ type: activityType,
96
+ title: formData.title,
97
+ description: formData.description,
98
+ outcome: formData.outcome,
99
+ durationMinutes: formData.durationMinutes,
100
+ occurredAt: formData.occurredAt
101
+ ? new Date(formData.occurredAt).toISOString()
102
+ : new Date().toISOString(),
103
+ });
104
+ } catch (error) {
105
+ console.error('Failed to log activity:', error);
106
+ }
107
+ };
108
+
109
+ const handleChange = (
110
+ field: keyof typeof formData,
111
+ value: string | number | undefined
112
+ ) => {
113
+ setFormData((prev) => ({ ...prev, [field]: value }));
114
+ if (errors[field]) {
115
+ setErrors((prev) => ({ ...prev, [field]: undefined as any }));
116
+ }
117
+ };
118
+
119
+ const currentTypeLabel =
120
+ activityTypes.find((t) => t.value === activityType)?.label || '';
121
+
122
+ return (
123
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
124
+ <div className="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
125
+ {/* Header */}
126
+ <div className="flex items-center justify-between p-6 border-b border-gray-200">
127
+ <h2 className="text-xl font-semibold text-gray-900">{title}</h2>
128
+ <button
129
+ onClick={onClose}
130
+ className="text-gray-400 hover:text-gray-600 transition-colors"
131
+ >
132
+ <X className="w-5 h-5" />
133
+ </button>
134
+ </div>
135
+
136
+ {/* Form */}
137
+ <form onSubmit={handleSubmit} className="p-6 space-y-4">
138
+ {/* Activity Type Selection */}
139
+ <div>
140
+ <label className="block text-sm font-medium text-gray-700 mb-2">
141
+ Activity Type *
142
+ </label>
143
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
144
+ {activityTypes.map(({ value, label, icon: Icon }) => (
145
+ <button
146
+ key={value}
147
+ type="button"
148
+ onClick={() => setActivityType(value)}
149
+ className={`flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium rounded-lg border-2 transition-colors ${
150
+ activityType === value
151
+ ? 'border-primary-600 bg-primary-50 text-primary-700'
152
+ : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50'
153
+ }`}
154
+ >
155
+ <Icon className="w-4 h-4" />
156
+ {label}
157
+ </button>
158
+ ))}
159
+ </div>
160
+ </div>
161
+
162
+ {/* Title */}
163
+ <div>
164
+ <label className="block text-sm font-medium text-gray-700 mb-1">
165
+ Title *
166
+ </label>
167
+ <input
168
+ type="text"
169
+ value={formData.title}
170
+ onChange={(e) => handleChange('title', e.target.value)}
171
+ className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
172
+ errors.title ? 'border-red-500' : 'border-gray-300'
173
+ }`}
174
+ placeholder={`${currentTypeLabel} with...`}
175
+ />
176
+ {errors.title && (
177
+ <p className="mt-1 text-sm text-red-600">{errors.title}</p>
178
+ )}
179
+ </div>
180
+
181
+ {/* Date/Time and Duration */}
182
+ <div className="grid grid-cols-2 gap-4">
183
+ <div>
184
+ <label className="block text-sm font-medium text-gray-700 mb-1">
185
+ Date & Time
186
+ </label>
187
+ <input
188
+ type="datetime-local"
189
+ value={formData.occurredAt?.slice(0, 16) || ''}
190
+ onChange={(e) => handleChange('occurredAt', e.target.value)}
191
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
192
+ />
193
+ </div>
194
+
195
+ {showDuration && (
196
+ <div>
197
+ <label className="block text-sm font-medium text-gray-700 mb-1">
198
+ Duration (minutes)
199
+ </label>
200
+ <input
201
+ type="number"
202
+ value={formData.durationMinutes || ''}
203
+ onChange={(e) =>
204
+ handleChange(
205
+ 'durationMinutes',
206
+ e.target.value ? parseInt(e.target.value) : undefined
207
+ )
208
+ }
209
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
210
+ placeholder="30"
211
+ min="1"
212
+ />
213
+ </div>
214
+ )}
215
+ </div>
216
+
217
+ {/* Outcome */}
218
+ {showOutcome && (
219
+ <div>
220
+ <label className="block text-sm font-medium text-gray-700 mb-1">
221
+ Outcome *
222
+ </label>
223
+ <select
224
+ value={formData.outcome || ''}
225
+ onChange={(e) =>
226
+ handleChange('outcome', e.target.value || undefined)
227
+ }
228
+ className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
229
+ errors.outcome ? 'border-red-500' : 'border-gray-300'
230
+ }`}
231
+ >
232
+ <option value="">Select outcome...</option>
233
+ {outcomeOptions.map((outcome) => (
234
+ <option key={outcome.value} value={outcome.value}>
235
+ {outcome.label}
236
+ </option>
237
+ ))}
238
+ </select>
239
+ {errors.outcome && (
240
+ <p className="mt-1 text-sm text-red-600">{errors.outcome}</p>
241
+ )}
242
+ </div>
243
+ )}
244
+
245
+ {/* Description */}
246
+ <div>
247
+ <label className="block text-sm font-medium text-gray-700 mb-1">
248
+ Notes
249
+ </label>
250
+ <textarea
251
+ value={formData.description || ''}
252
+ onChange={(e) => handleChange('description', e.target.value)}
253
+ rows={4}
254
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
255
+ placeholder="Add details about this activity..."
256
+ />
257
+ </div>
258
+
259
+ {/* Follow-up Task */}
260
+ {showFollowUp && (
261
+ <div className="bg-primary-50 border border-primary-200 rounded-lg p-4">
262
+ <label className="flex items-center gap-2 cursor-pointer">
263
+ <input
264
+ type="checkbox"
265
+ checked={createFollowUp}
266
+ onChange={(e) => setCreateFollowUp(e.target.checked)}
267
+ className="w-4 h-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
268
+ />
269
+ <span className="text-sm font-medium text-blue-900">
270
+ Create follow-up task
271
+ </span>
272
+ </label>
273
+ {createFollowUp && (
274
+ <p className="text-xs text-blue-700 mt-2">
275
+ A task reminder will be created for this follow-up
276
+ </p>
277
+ )}
278
+ </div>
279
+ )}
280
+
281
+ {/* Actions */}
282
+ <div className="flex justify-end gap-3 pt-4">
283
+ <button
284
+ type="button"
285
+ onClick={onClose}
286
+ disabled={isSubmitting}
287
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
288
+ >
289
+ Cancel
290
+ </button>
291
+ <button
292
+ type="submit"
293
+ disabled={isSubmitting}
294
+ className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-md hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
295
+ >
296
+ {isSubmitting ? submittingLabel : submitLabel}
297
+ </button>
298
+ </div>
299
+ </form>
300
+ </div>
301
+ </div>
302
+ );
303
+ }
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { ComponentType } from 'react';
4
+
5
+ export interface QuickLogAction {
6
+ type: string;
7
+ label: string;
8
+ icon: ComponentType<any>;
9
+ color: string;
10
+ }
11
+
12
+ export interface QuickLogButtonsProps {
13
+ actions: QuickLogAction[];
14
+ onAction: (type: string) => void;
15
+ }
16
+
17
+ export function QuickLogButtons({ actions, onAction }: QuickLogButtonsProps) {
18
+ return (
19
+ <div className="flex flex-wrap gap-2">
20
+ {actions.map(({ type, label, icon: Icon, color }) => (
21
+ <button
22
+ key={type}
23
+ onClick={() => onAction(type)}
24
+ className={`flex items-center gap-2 px-4 py-2 text-sm font-medium text-white rounded-md transition-colors ${color}`}
25
+ >
26
+ <Icon className="w-4 h-4" />
27
+ {label}
28
+ </button>
29
+ ))}
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,31 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ export interface StageBadgeConfig {
6
+ label: string
7
+ className: string
8
+ }
9
+
10
+ export interface StageBadgeProps {
11
+ stage: string
12
+ configMap: Record<string, StageBadgeConfig>
13
+ className?: string
14
+ }
15
+
16
+ const DEFAULT_CONFIG: StageBadgeConfig = {
17
+ label: '',
18
+ className: 'bg-gray-100 text-gray-700 border-gray-200',
19
+ }
20
+
21
+ export function StageBadge({ stage, configMap, className = '' }: StageBadgeProps) {
22
+ const config = configMap[stage] ?? { ...DEFAULT_CONFIG, label: stage }
23
+
24
+ return (
25
+ <span
26
+ className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${config.className} ${className}`}
27
+ >
28
+ {config.label}
29
+ </span>
30
+ )
31
+ }
@@ -1,2 +1,5 @@
1
1
  export { StatusBadge } from './StatusBadge'
2
2
  export type { StatusBadgeProps, StatusBadgeConfig } from './StatusBadge'
3
+
4
+ export { StageBadge } from './StageBadge'
5
+ export type { StageBadgeProps, StageBadgeConfig } from './StageBadge'