@dilipod/ui 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/file-preview.d.ts +25 -0
- package/dist/components/file-preview.d.ts.map +1 -0
- package/dist/components/scenarios-manager.d.ts +23 -0
- package/dist/components/scenarios-manager.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +579 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +580 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/file-preview.tsx +359 -0
- package/src/components/scenarios-manager.tsx +403 -0
- package/src/index.ts +8 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Plus, PencilSimple, Trash, Warning, CheckCircle, Question, Lightning } from '@phosphor-icons/react'
|
|
5
|
+
import { cn } from '../lib/utils'
|
|
6
|
+
import { Button } from './button'
|
|
7
|
+
import { Badge } from './badge'
|
|
8
|
+
import { Input } from './input'
|
|
9
|
+
import { Select } from './select'
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
DialogDescription,
|
|
16
|
+
DialogFooter,
|
|
17
|
+
} from './dialog'
|
|
18
|
+
|
|
19
|
+
// Types
|
|
20
|
+
export type ScenarioType = 'escalation' | 'default_behavior' | 'quality_check' | 'edge_case'
|
|
21
|
+
|
|
22
|
+
export interface Scenario {
|
|
23
|
+
id: string
|
|
24
|
+
type: ScenarioType
|
|
25
|
+
situation: string
|
|
26
|
+
action: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ScenarioSuggestion {
|
|
30
|
+
type: ScenarioType
|
|
31
|
+
situation: string
|
|
32
|
+
action: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ScenariosManagerProps {
|
|
36
|
+
scenarios: Scenario[]
|
|
37
|
+
onAdd: (scenario: Omit<Scenario, 'id'>) => Promise<void>
|
|
38
|
+
onUpdate: (id: string, scenario: Omit<Scenario, 'id'>) => Promise<void>
|
|
39
|
+
onDelete: (id: string) => Promise<void>
|
|
40
|
+
suggestions?: ScenarioSuggestion[]
|
|
41
|
+
isLoading?: boolean
|
|
42
|
+
className?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Type configuration
|
|
46
|
+
const typeConfig: Record<ScenarioType, {
|
|
47
|
+
label: string
|
|
48
|
+
icon: React.ElementType
|
|
49
|
+
color: string
|
|
50
|
+
bgColor: string
|
|
51
|
+
description: string
|
|
52
|
+
}> = {
|
|
53
|
+
escalation: {
|
|
54
|
+
label: 'Escalation',
|
|
55
|
+
icon: Warning,
|
|
56
|
+
color: 'text-amber-600',
|
|
57
|
+
bgColor: 'bg-amber-50',
|
|
58
|
+
description: 'When to ask me first',
|
|
59
|
+
},
|
|
60
|
+
default_behavior: {
|
|
61
|
+
label: 'Default behavior',
|
|
62
|
+
icon: CheckCircle,
|
|
63
|
+
color: 'text-blue-600',
|
|
64
|
+
bgColor: 'bg-blue-50',
|
|
65
|
+
description: 'What to do when data is missing',
|
|
66
|
+
},
|
|
67
|
+
quality_check: {
|
|
68
|
+
label: 'Quality check',
|
|
69
|
+
icon: CheckCircle,
|
|
70
|
+
color: 'text-emerald-600',
|
|
71
|
+
bgColor: 'bg-emerald-50',
|
|
72
|
+
description: 'What "done right" looks like',
|
|
73
|
+
},
|
|
74
|
+
edge_case: {
|
|
75
|
+
label: 'Edge case',
|
|
76
|
+
icon: Question,
|
|
77
|
+
color: 'text-purple-600',
|
|
78
|
+
bgColor: 'bg-purple-50',
|
|
79
|
+
description: 'Judgment calls and past mistakes',
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Individual scenario card
|
|
84
|
+
function ScenarioCard({
|
|
85
|
+
scenario,
|
|
86
|
+
onEdit,
|
|
87
|
+
onDelete,
|
|
88
|
+
}: {
|
|
89
|
+
scenario: Scenario
|
|
90
|
+
onEdit: () => void
|
|
91
|
+
onDelete: () => void
|
|
92
|
+
}) {
|
|
93
|
+
const config = typeConfig[scenario.type]
|
|
94
|
+
const Icon = config.icon
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="group relative border border-gray-200 rounded-sm p-4 hover:border-gray-300 transition-colors">
|
|
98
|
+
<div className="flex items-start justify-between gap-3">
|
|
99
|
+
<div className="flex items-start gap-3 flex-1 min-w-0">
|
|
100
|
+
<div className={cn('w-8 h-8 rounded-sm flex items-center justify-center shrink-0', config.bgColor)}>
|
|
101
|
+
<Icon size={16} weight="fill" className={config.color} />
|
|
102
|
+
</div>
|
|
103
|
+
<div className="flex-1 min-w-0">
|
|
104
|
+
<div className="flex items-center gap-2 mb-1">
|
|
105
|
+
<Badge variant="outline" size="sm">{config.label}</Badge>
|
|
106
|
+
</div>
|
|
107
|
+
<p className="text-sm text-[var(--black)] font-medium">
|
|
108
|
+
When: <span className="font-normal">{scenario.situation}</span>
|
|
109
|
+
</p>
|
|
110
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
111
|
+
Action: <span className="text-[var(--black)]">{scenario.action}</span>
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
116
|
+
<Button
|
|
117
|
+
variant="ghost"
|
|
118
|
+
size="icon"
|
|
119
|
+
className="h-8 w-8"
|
|
120
|
+
onClick={onEdit}
|
|
121
|
+
>
|
|
122
|
+
<PencilSimple size={16} />
|
|
123
|
+
</Button>
|
|
124
|
+
<Button
|
|
125
|
+
variant="ghost"
|
|
126
|
+
size="icon"
|
|
127
|
+
className="h-8 w-8 text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
128
|
+
onClick={onDelete}
|
|
129
|
+
>
|
|
130
|
+
<Trash size={16} />
|
|
131
|
+
</Button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Suggestion chip
|
|
139
|
+
function SuggestionChip({
|
|
140
|
+
suggestion,
|
|
141
|
+
onAdd,
|
|
142
|
+
disabled,
|
|
143
|
+
}: {
|
|
144
|
+
suggestion: ScenarioSuggestion
|
|
145
|
+
onAdd: () => void
|
|
146
|
+
disabled?: boolean
|
|
147
|
+
}) {
|
|
148
|
+
const config = typeConfig[suggestion.type]
|
|
149
|
+
const Icon = config.icon
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={onAdd}
|
|
155
|
+
disabled={disabled}
|
|
156
|
+
className={cn(
|
|
157
|
+
'inline-flex items-center gap-2 px-3 py-2 rounded-sm border border-dashed border-gray-300',
|
|
158
|
+
'text-sm text-muted-foreground hover:border-[var(--cyan)] hover:text-[var(--cyan)] hover:bg-[var(--cyan)]/5',
|
|
159
|
+
'transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<Plus size={14} />
|
|
163
|
+
{suggestion.situation}
|
|
164
|
+
</button>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Add/Edit dialog
|
|
169
|
+
function ScenarioDialog({
|
|
170
|
+
open,
|
|
171
|
+
onOpenChange,
|
|
172
|
+
scenario,
|
|
173
|
+
onSave,
|
|
174
|
+
isLoading,
|
|
175
|
+
}: {
|
|
176
|
+
open: boolean
|
|
177
|
+
onOpenChange: (open: boolean) => void
|
|
178
|
+
scenario?: Scenario | null
|
|
179
|
+
onSave: (data: Omit<Scenario, 'id'>) => Promise<void>
|
|
180
|
+
isLoading?: boolean
|
|
181
|
+
}) {
|
|
182
|
+
const [type, setType] = React.useState<ScenarioType>(scenario?.type || 'escalation')
|
|
183
|
+
const [situation, setSituation] = React.useState(scenario?.situation || '')
|
|
184
|
+
const [action, setAction] = React.useState(scenario?.action || '')
|
|
185
|
+
const [isSaving, setIsSaving] = React.useState(false)
|
|
186
|
+
|
|
187
|
+
// Reset form when dialog opens/closes or scenario changes
|
|
188
|
+
React.useEffect(() => {
|
|
189
|
+
if (open) {
|
|
190
|
+
setType(scenario?.type || 'escalation')
|
|
191
|
+
setSituation(scenario?.situation || '')
|
|
192
|
+
setAction(scenario?.action || '')
|
|
193
|
+
}
|
|
194
|
+
}, [open, scenario])
|
|
195
|
+
|
|
196
|
+
const handleSave = async () => {
|
|
197
|
+
if (!situation.trim() || !action.trim()) return
|
|
198
|
+
|
|
199
|
+
setIsSaving(true)
|
|
200
|
+
try {
|
|
201
|
+
await onSave({ type, situation: situation.trim(), action: action.trim() })
|
|
202
|
+
onOpenChange(false)
|
|
203
|
+
} finally {
|
|
204
|
+
setIsSaving(false)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const isValid = situation.trim() && action.trim()
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
212
|
+
<DialogContent className="sm:max-w-md" style={{ transform: 'translate(-50%, -50%)' }}>
|
|
213
|
+
<DialogHeader>
|
|
214
|
+
<DialogTitle>{scenario ? 'Edit scenario' : 'Add scenario'}</DialogTitle>
|
|
215
|
+
<DialogDescription>
|
|
216
|
+
Define when something happens and what action to take.
|
|
217
|
+
</DialogDescription>
|
|
218
|
+
</DialogHeader>
|
|
219
|
+
|
|
220
|
+
<div className="grid gap-4 py-4">
|
|
221
|
+
<div className="space-y-2">
|
|
222
|
+
<label className="text-sm font-medium text-[var(--black)]">Type</label>
|
|
223
|
+
<Select
|
|
224
|
+
value={type}
|
|
225
|
+
onChange={(e) => setType(e.target.value as ScenarioType)}
|
|
226
|
+
>
|
|
227
|
+
{Object.entries(typeConfig).map(([key, config]) => (
|
|
228
|
+
<option key={key} value={key}>
|
|
229
|
+
{config.label} — {config.description}
|
|
230
|
+
</option>
|
|
231
|
+
))}
|
|
232
|
+
</Select>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div className="space-y-2">
|
|
236
|
+
<label className="text-sm font-medium text-[var(--black)]">When...</label>
|
|
237
|
+
<Input
|
|
238
|
+
placeholder="e.g., Invoice amount doesn't match PO"
|
|
239
|
+
value={situation}
|
|
240
|
+
onChange={(e) => setSituation(e.target.value)}
|
|
241
|
+
/>
|
|
242
|
+
<p className="text-xs text-muted-foreground">Describe the situation or trigger</p>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div className="space-y-2">
|
|
246
|
+
<label className="text-sm font-medium text-[var(--black)]">Then...</label>
|
|
247
|
+
<Input
|
|
248
|
+
placeholder="e.g., Flag for review, don't process"
|
|
249
|
+
value={action}
|
|
250
|
+
onChange={(e) => setAction(e.target.value)}
|
|
251
|
+
/>
|
|
252
|
+
<p className="text-xs text-muted-foreground">What should happen in this situation</p>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<DialogFooter>
|
|
257
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
258
|
+
Cancel
|
|
259
|
+
</Button>
|
|
260
|
+
<Button
|
|
261
|
+
onClick={handleSave}
|
|
262
|
+
disabled={!isValid || isSaving}
|
|
263
|
+
loading={isSaving}
|
|
264
|
+
>
|
|
265
|
+
{scenario ? 'Save changes' : 'Add scenario'}
|
|
266
|
+
</Button>
|
|
267
|
+
</DialogFooter>
|
|
268
|
+
</DialogContent>
|
|
269
|
+
</Dialog>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Main component
|
|
274
|
+
export function ScenariosManager({
|
|
275
|
+
scenarios,
|
|
276
|
+
onAdd,
|
|
277
|
+
onUpdate,
|
|
278
|
+
onDelete,
|
|
279
|
+
suggestions = [],
|
|
280
|
+
isLoading,
|
|
281
|
+
className,
|
|
282
|
+
}: ScenariosManagerProps) {
|
|
283
|
+
const [dialogOpen, setDialogOpen] = React.useState(false)
|
|
284
|
+
const [editingScenario, setEditingScenario] = React.useState<Scenario | null>(null)
|
|
285
|
+
const [deletingId, setDeletingId] = React.useState<string | null>(null)
|
|
286
|
+
|
|
287
|
+
const handleAddClick = () => {
|
|
288
|
+
setEditingScenario(null)
|
|
289
|
+
setDialogOpen(true)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const handleEditClick = (scenario: Scenario) => {
|
|
293
|
+
setEditingScenario(scenario)
|
|
294
|
+
setDialogOpen(true)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const handleSave = async (data: Omit<Scenario, 'id'>) => {
|
|
298
|
+
if (editingScenario) {
|
|
299
|
+
await onUpdate(editingScenario.id, data)
|
|
300
|
+
} else {
|
|
301
|
+
await onAdd(data)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const handleDelete = async (id: string) => {
|
|
306
|
+
setDeletingId(id)
|
|
307
|
+
try {
|
|
308
|
+
await onDelete(id)
|
|
309
|
+
} finally {
|
|
310
|
+
setDeletingId(null)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const handleSuggestionAdd = async (suggestion: ScenarioSuggestion) => {
|
|
315
|
+
await onAdd(suggestion)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Filter out suggestions that already exist
|
|
319
|
+
const filteredSuggestions = suggestions.filter(
|
|
320
|
+
(s) => !scenarios.some(
|
|
321
|
+
(existing) => existing.situation.toLowerCase() === s.situation.toLowerCase()
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div className={cn('space-y-4', className)}>
|
|
327
|
+
{/* Header */}
|
|
328
|
+
<div className="flex items-center justify-between">
|
|
329
|
+
<div className="flex items-center gap-3">
|
|
330
|
+
<div className="w-10 h-10 rounded-sm bg-[var(--cyan)]/10 flex items-center justify-center">
|
|
331
|
+
<Lightning size={20} weight="fill" className="text-[var(--cyan)]" />
|
|
332
|
+
</div>
|
|
333
|
+
<div>
|
|
334
|
+
<h3 className="font-semibold text-[var(--black)]">Scenarios</h3>
|
|
335
|
+
<p className="text-sm text-muted-foreground">
|
|
336
|
+
{scenarios.length === 0
|
|
337
|
+
? 'Define rules for edge cases and escalations'
|
|
338
|
+
: `${scenarios.length} scenario${scenarios.length === 1 ? '' : 's'} defined`}
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
<Button variant="outline" size="sm" onClick={handleAddClick}>
|
|
343
|
+
<Plus size={16} className="mr-1" />
|
|
344
|
+
Add scenario
|
|
345
|
+
</Button>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
{/* Scenarios list */}
|
|
349
|
+
{scenarios.length > 0 && (
|
|
350
|
+
<div className="grid gap-3">
|
|
351
|
+
{scenarios.map((scenario) => (
|
|
352
|
+
<ScenarioCard
|
|
353
|
+
key={scenario.id}
|
|
354
|
+
scenario={scenario}
|
|
355
|
+
onEdit={() => handleEditClick(scenario)}
|
|
356
|
+
onDelete={() => handleDelete(scenario.id)}
|
|
357
|
+
/>
|
|
358
|
+
))}
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
{/* Empty state */}
|
|
363
|
+
{scenarios.length === 0 && (
|
|
364
|
+
<div className="border border-dashed border-gray-300 rounded-sm p-8 text-center">
|
|
365
|
+
<Lightning size={32} className="text-gray-300 mx-auto mb-3" />
|
|
366
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
367
|
+
No scenarios yet. Add rules for how the worker should handle edge cases.
|
|
368
|
+
</p>
|
|
369
|
+
<Button variant="outline" size="sm" onClick={handleAddClick}>
|
|
370
|
+
<Plus size={16} className="mr-1" />
|
|
371
|
+
Add your first scenario
|
|
372
|
+
</Button>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{/* Suggestions */}
|
|
377
|
+
{filteredSuggestions.length > 0 && (
|
|
378
|
+
<div className="pt-2">
|
|
379
|
+
<p className="text-xs text-muted-foreground mb-2">Suggested scenarios:</p>
|
|
380
|
+
<div className="flex flex-wrap gap-2">
|
|
381
|
+
{filteredSuggestions.map((suggestion, index) => (
|
|
382
|
+
<SuggestionChip
|
|
383
|
+
key={index}
|
|
384
|
+
suggestion={suggestion}
|
|
385
|
+
onAdd={() => handleSuggestionAdd(suggestion)}
|
|
386
|
+
disabled={isLoading}
|
|
387
|
+
/>
|
|
388
|
+
))}
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
|
|
393
|
+
{/* Dialog */}
|
|
394
|
+
<ScenarioDialog
|
|
395
|
+
open={dialogOpen}
|
|
396
|
+
onOpenChange={setDialogOpen}
|
|
397
|
+
scenario={editingScenario}
|
|
398
|
+
onSave={handleSave}
|
|
399
|
+
isLoading={isLoading}
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
)
|
|
403
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -258,10 +258,18 @@ export type { BreadcrumbItem, BreadcrumbsProps, BreadcrumbLinkProps } from './co
|
|
|
258
258
|
export { DateRangePicker, DateRangeSelect, getDateRangeFromPreset } from './components/date-range-picker'
|
|
259
259
|
export type { DateRangePreset, DateRangePickerProps, DateRangeSelectProps } from './components/date-range-picker'
|
|
260
260
|
|
|
261
|
+
// File Preview Components
|
|
262
|
+
export { FilePreview } from './components/file-preview'
|
|
263
|
+
export type { FilePreviewProps, UploadedFile } from './components/file-preview'
|
|
264
|
+
|
|
261
265
|
// Settings Navigation Components
|
|
262
266
|
export { SettingsNav, SettingsNavLink } from './components/settings-nav'
|
|
263
267
|
export type { SettingsNavItem, SettingsNavGroup, SettingsNavProps, SettingsNavLinkProps } from './components/settings-nav'
|
|
264
268
|
|
|
269
|
+
// Scenarios Manager Components
|
|
270
|
+
export { ScenariosManager } from './components/scenarios-manager'
|
|
271
|
+
export type { Scenario, ScenarioType, ScenarioSuggestion, ScenariosManagerProps } from './components/scenarios-manager'
|
|
272
|
+
|
|
265
273
|
// Utilities
|
|
266
274
|
export { cn } from './lib/utils'
|
|
267
275
|
|