@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.
@@ -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