@papernote/ui 1.10.26 → 1.11.1
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/ActionCard.d.ts +48 -0
- package/dist/components/ActionCard.d.ts.map +1 -0
- package/dist/components/AnomalyBanner.d.ts +27 -0
- package/dist/components/AnomalyBanner.d.ts.map +1 -0
- package/dist/components/CaseQueueItem.d.ts +35 -0
- package/dist/components/CaseQueueItem.d.ts.map +1 -0
- package/dist/components/ConfidenceBadge.d.ts +19 -0
- package/dist/components/ConfidenceBadge.d.ts.map +1 -0
- package/dist/components/ConfidenceIndicator.d.ts +25 -0
- package/dist/components/ConfidenceIndicator.d.ts.map +1 -0
- package/dist/components/EntityCard.d.ts +46 -0
- package/dist/components/EntityCard.d.ts.map +1 -0
- package/dist/components/FunnelChart.d.ts +31 -0
- package/dist/components/FunnelChart.d.ts.map +1 -0
- package/dist/components/MatchIndicator.d.ts +20 -0
- package/dist/components/MatchIndicator.d.ts.map +1 -0
- package/dist/components/PageLayout.d.ts.map +1 -1
- package/dist/components/PersonaDashboard.d.ts +39 -0
- package/dist/components/PersonaDashboard.d.ts.map +1 -0
- package/dist/components/ProcessHealthBar.d.ts +28 -0
- package/dist/components/ProcessHealthBar.d.ts.map +1 -0
- package/dist/components/ProcessIndicator.d.ts +38 -0
- package/dist/components/ProcessIndicator.d.ts.map +1 -0
- package/dist/components/ReviewDecisionCard.d.ts +53 -0
- package/dist/components/ReviewDecisionCard.d.ts.map +1 -0
- package/dist/components/SLAIndicator.d.ts +24 -0
- package/dist/components/SLAIndicator.d.ts.map +1 -0
- package/dist/components/SplitPane.d.ts +33 -0
- package/dist/components/SplitPane.d.ts.map +1 -0
- package/dist/components/SystemActionEntry.d.ts +42 -0
- package/dist/components/SystemActionEntry.d.ts.map +1 -0
- package/dist/components/VarianceDisplay.d.ts +26 -0
- package/dist/components/VarianceDisplay.d.ts.map +1 -0
- package/dist/components/index.d.ts +32 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +529 -2
- package/dist/index.esm.js +664 -31
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +678 -29
- package/dist/index.js.map +1 -1
- package/dist/styles.css +367 -10
- package/package.json +1 -1
- package/src/components/ActionCard.tsx +176 -0
- package/src/components/AnomalyBanner.tsx +113 -0
- package/src/components/CaseQueueItem.tsx +145 -0
- package/src/components/ConfidenceBadge.tsx +62 -0
- package/src/components/ConfidenceIndicator.tsx +96 -0
- package/src/components/EntityCard.tsx +216 -0
- package/src/components/FunnelChart.tsx +160 -0
- package/src/components/MatchIndicator.tsx +73 -0
- package/src/components/Page.tsx +2 -2
- package/src/components/PageLayout.tsx +1 -1
- package/src/components/PersonaDashboard.tsx +105 -0
- package/src/components/ProcessHealthBar.tsx +107 -0
- package/src/components/ProcessIndicator.tsx +167 -0
- package/src/components/ReviewDecisionCard.tsx +186 -0
- package/src/components/SLAIndicator.tsx +108 -0
- package/src/components/SplitPane.tsx +150 -0
- package/src/components/SystemActionEntry.tsx +175 -0
- package/src/components/VarianceDisplay.tsx +116 -0
- package/src/components/index.ts +48 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChevronRight } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Types & Interfaces
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
export interface ProcessStage {
|
|
9
|
+
/** Unique stage identifier */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Display name */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Number of items at this stage */
|
|
14
|
+
count: number;
|
|
15
|
+
/** Color variant for the stage */
|
|
16
|
+
color?: 'slate' | 'blue' | 'indigo' | 'purple' | 'green' | 'red' | 'yellow' | 'orange' | 'teal' | 'primary';
|
|
17
|
+
/** Health status derived from thresholds */
|
|
18
|
+
health?: 'good' | 'warning' | 'critical';
|
|
19
|
+
/** Optional icon */
|
|
20
|
+
icon?: React.ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProcessIndicatorProps {
|
|
24
|
+
/** Stages to display in order */
|
|
25
|
+
stages: ProcessStage[];
|
|
26
|
+
/** Currently selected/filtered stage ID */
|
|
27
|
+
activeStageId?: string;
|
|
28
|
+
/** Callback when a stage is clicked */
|
|
29
|
+
onStageClick?: (stageId: string) => void;
|
|
30
|
+
/** Show conversion rate between stages */
|
|
31
|
+
showConversion?: boolean;
|
|
32
|
+
/** Size variant */
|
|
33
|
+
size?: 'sm' | 'md' | 'lg';
|
|
34
|
+
/** Additional className */
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Color Mappings
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
const stageColors: Record<string, { bg: string; text: string; border: string; activeBg: string }> = {
|
|
43
|
+
slate: { bg: 'bg-slate-50', text: 'text-slate-700', border: 'border-slate-200', activeBg: 'bg-slate-100' },
|
|
44
|
+
blue: { bg: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-200', activeBg: 'bg-blue-100' },
|
|
45
|
+
indigo: { bg: 'bg-indigo-50', text: 'text-indigo-700', border: 'border-indigo-200', activeBg: 'bg-indigo-100' },
|
|
46
|
+
purple: { bg: 'bg-purple-50', text: 'text-purple-700', border: 'border-purple-200', activeBg: 'bg-purple-100' },
|
|
47
|
+
green: { bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-200', activeBg: 'bg-green-100' },
|
|
48
|
+
red: { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-200', activeBg: 'bg-red-100' },
|
|
49
|
+
yellow: { bg: 'bg-yellow-50', text: 'text-yellow-700', border: 'border-yellow-200', activeBg: 'bg-yellow-100' },
|
|
50
|
+
orange: { bg: 'bg-orange-50', text: 'text-orange-700', border: 'border-orange-200', activeBg: 'bg-orange-100' },
|
|
51
|
+
teal: { bg: 'bg-teal-50', text: 'text-teal-700', border: 'border-teal-200', activeBg: 'bg-teal-100' },
|
|
52
|
+
primary: { bg: 'bg-primary-50', text: 'text-primary-700', border: 'border-primary-200', activeBg: 'bg-primary-100' },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const healthIndicator: Record<string, string> = {
|
|
56
|
+
good: 'bg-success-500',
|
|
57
|
+
warning: 'bg-warning-500',
|
|
58
|
+
critical: 'bg-error-500',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const sizeClasses = {
|
|
62
|
+
sm: { wrapper: 'py-2 px-3', count: 'text-lg', label: 'text-xs', chevron: 'h-3 w-3' },
|
|
63
|
+
md: { wrapper: 'py-3 px-4', count: 'text-2xl', label: 'text-sm', chevron: 'h-4 w-4' },
|
|
64
|
+
lg: { wrapper: 'py-4 px-5', count: 'text-3xl', label: 'text-base', chevron: 'h-5 w-5' },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Component
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* ProcessIndicator — Horizontal process flow visualization
|
|
73
|
+
*
|
|
74
|
+
* Shows stages as connected blocks with names, item counts, and health coloring.
|
|
75
|
+
* Stages are clickable for filtering. Used at the top of process views
|
|
76
|
+
* (Sales Pipeline, Support Center, Commission Lifecycle).
|
|
77
|
+
*/
|
|
78
|
+
export default function ProcessIndicator({
|
|
79
|
+
stages,
|
|
80
|
+
activeStageId,
|
|
81
|
+
onStageClick,
|
|
82
|
+
showConversion = false,
|
|
83
|
+
size = 'md',
|
|
84
|
+
className = '',
|
|
85
|
+
}: ProcessIndicatorProps) {
|
|
86
|
+
const sizes = sizeClasses[size];
|
|
87
|
+
const totalCount = stages.reduce((sum, s) => sum + s.count, 0);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className={`flex items-stretch overflow-x-auto ${className}`} role="navigation" aria-label="Process stages">
|
|
91
|
+
{stages.map((stage, index) => {
|
|
92
|
+
const colors = stageColors[stage.color || 'slate'];
|
|
93
|
+
const isActive = stage.id === activeStageId;
|
|
94
|
+
const isClickable = !!onStageClick;
|
|
95
|
+
const conversionRate = showConversion && index > 0 && stages[index - 1].count > 0
|
|
96
|
+
? Math.round((stage.count / stages[index - 1].count) * 100)
|
|
97
|
+
: null;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<React.Fragment key={stage.id}>
|
|
101
|
+
{/* Chevron connector between stages */}
|
|
102
|
+
{index > 0 && (
|
|
103
|
+
<div className="flex flex-col items-center justify-center px-1 flex-shrink-0">
|
|
104
|
+
<ChevronRight className={`${sizes.chevron} text-ink-300`} />
|
|
105
|
+
{conversionRate !== null && (
|
|
106
|
+
<span className="text-[10px] text-ink-400 mt-0.5">{conversionRate}%</span>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{/* Stage block */}
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={() => onStageClick?.(stage.id)}
|
|
115
|
+
disabled={!isClickable}
|
|
116
|
+
className={`
|
|
117
|
+
flex flex-col items-center justify-center ${sizes.wrapper}
|
|
118
|
+
rounded-lg border transition-all min-w-0 flex-1
|
|
119
|
+
${isActive ? `${colors.activeBg} ${colors.border} border-2 shadow-sm` : `${colors.bg} ${colors.border}`}
|
|
120
|
+
${isClickable ? 'cursor-pointer hover:shadow-sm hover:border-2' : 'cursor-default'}
|
|
121
|
+
`}
|
|
122
|
+
aria-current={isActive ? 'step' : undefined}
|
|
123
|
+
aria-label={`${stage.name}: ${stage.count} items`}
|
|
124
|
+
>
|
|
125
|
+
{/* Health indicator dot */}
|
|
126
|
+
{stage.health && (
|
|
127
|
+
<div className={`w-2 h-2 rounded-full ${healthIndicator[stage.health]} mb-1`} />
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Stage icon */}
|
|
131
|
+
{stage.icon && (
|
|
132
|
+
<div className={`${colors.text} mb-1 opacity-60`}>
|
|
133
|
+
{stage.icon}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{/* Count */}
|
|
138
|
+
<span className={`${sizes.count} font-bold ${colors.text} leading-none`}>
|
|
139
|
+
{stage.count.toLocaleString()}
|
|
140
|
+
</span>
|
|
141
|
+
|
|
142
|
+
{/* Stage name */}
|
|
143
|
+
<span className={`${sizes.label} ${colors.text} opacity-80 mt-1 whitespace-nowrap`}>
|
|
144
|
+
{stage.name}
|
|
145
|
+
</span>
|
|
146
|
+
</button>
|
|
147
|
+
</React.Fragment>
|
|
148
|
+
);
|
|
149
|
+
})}
|
|
150
|
+
|
|
151
|
+
{/* Total */}
|
|
152
|
+
{stages.length > 0 && (
|
|
153
|
+
<>
|
|
154
|
+
<div className="flex items-center justify-center px-2 flex-shrink-0">
|
|
155
|
+
<div className="w-px h-8 bg-ink-200" />
|
|
156
|
+
</div>
|
|
157
|
+
<div className={`flex flex-col items-center justify-center ${sizes.wrapper} min-w-0`}>
|
|
158
|
+
<span className={`${sizes.count} font-bold text-ink-900 leading-none`}>
|
|
159
|
+
{totalCount.toLocaleString()}
|
|
160
|
+
</span>
|
|
161
|
+
<span className={`${sizes.label} text-ink-500 mt-1`}>Total</span>
|
|
162
|
+
</div>
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ConfidenceIndicator from './ConfidenceIndicator';
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Types & Interfaces
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
export interface ConfidenceBreakdown {
|
|
9
|
+
/** Factor name */
|
|
10
|
+
factor: string;
|
|
11
|
+
/** Factor score (0-100) */
|
|
12
|
+
score: number;
|
|
13
|
+
/** Factor description */
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ReviewAction {
|
|
18
|
+
/** Button label */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Button variant */
|
|
21
|
+
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
22
|
+
/** Click handler */
|
|
23
|
+
onClick: () => void;
|
|
24
|
+
/** Icon */
|
|
25
|
+
icon?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReviewDecisionCardProps {
|
|
29
|
+
/** Card title — what needs review */
|
|
30
|
+
title: string;
|
|
31
|
+
/** Description */
|
|
32
|
+
description?: string;
|
|
33
|
+
/** Overall confidence score (0-100) */
|
|
34
|
+
confidence: number;
|
|
35
|
+
/** Confidence breakdown by factor */
|
|
36
|
+
breakdown?: ConfidenceBreakdown[];
|
|
37
|
+
/** System recommendation */
|
|
38
|
+
recommendation?: string;
|
|
39
|
+
/** Available actions (approve, reject, etc.) */
|
|
40
|
+
actions: ReviewAction[];
|
|
41
|
+
/** Entity type and identifier */
|
|
42
|
+
entityType?: string;
|
|
43
|
+
entityLabel?: string;
|
|
44
|
+
/** Click handler for entity link */
|
|
45
|
+
onEntityClick?: () => void;
|
|
46
|
+
/** Additional notes/context */
|
|
47
|
+
notes?: string;
|
|
48
|
+
/** Timestamp */
|
|
49
|
+
timestamp?: string;
|
|
50
|
+
/** Additional className */
|
|
51
|
+
className?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// Helpers
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
const buttonVariants: Record<string, string> = {
|
|
59
|
+
primary: 'bg-primary-600 text-white hover:bg-primary-700',
|
|
60
|
+
secondary: 'bg-paper-100 text-ink-700 hover:bg-paper-200 border border-paper-300',
|
|
61
|
+
danger: 'bg-error-600 text-white hover:bg-error-700',
|
|
62
|
+
ghost: 'text-ink-600 hover:bg-paper-100',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Component
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* ReviewDecisionCard — Action card with confidence breakdown
|
|
71
|
+
*
|
|
72
|
+
* Extended ActionCard pattern that includes a confidence gauge and
|
|
73
|
+
* per-factor confidence breakdown. Used in the Approvals & Review queue
|
|
74
|
+
* for items requiring human judgment.
|
|
75
|
+
*/
|
|
76
|
+
export default function ReviewDecisionCard({
|
|
77
|
+
title,
|
|
78
|
+
description,
|
|
79
|
+
confidence,
|
|
80
|
+
breakdown,
|
|
81
|
+
recommendation,
|
|
82
|
+
actions,
|
|
83
|
+
entityType,
|
|
84
|
+
entityLabel,
|
|
85
|
+
onEntityClick,
|
|
86
|
+
notes,
|
|
87
|
+
timestamp,
|
|
88
|
+
className = '',
|
|
89
|
+
}: ReviewDecisionCardProps) {
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
className={`
|
|
93
|
+
rounded-lg border bg-white dark:bg-ink-900 border-paper-200 dark:border-ink-700 p-5
|
|
94
|
+
${className}
|
|
95
|
+
`}
|
|
96
|
+
>
|
|
97
|
+
<div className="flex gap-4">
|
|
98
|
+
{/* Confidence gauge */}
|
|
99
|
+
<div className="flex-shrink-0">
|
|
100
|
+
<ConfidenceIndicator score={confidence} size={72} strokeWidth={5} />
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Content */}
|
|
104
|
+
<div className="flex-1 min-w-0">
|
|
105
|
+
{/* Title + entity */}
|
|
106
|
+
<h4 className="text-base font-medium text-ink-900 dark:text-ink-100">{title}</h4>
|
|
107
|
+
{description && (
|
|
108
|
+
<p className="text-sm text-ink-500 mt-0.5">{description}</p>
|
|
109
|
+
)}
|
|
110
|
+
{entityType && entityLabel && (
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={onEntityClick}
|
|
114
|
+
className="text-xs text-primary-600 hover:underline mt-1"
|
|
115
|
+
>
|
|
116
|
+
{entityType}: {entityLabel}
|
|
117
|
+
</button>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Recommendation */}
|
|
121
|
+
{recommendation && (
|
|
122
|
+
<div className="mt-3 px-3 py-2 bg-primary-50 dark:bg-primary-900/20 rounded text-sm text-primary-700 dark:text-primary-300">
|
|
123
|
+
<span className="font-medium">Recommendation: </span>{recommendation}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* Confidence breakdown */}
|
|
130
|
+
{breakdown && breakdown.length > 0 && (
|
|
131
|
+
<div className="mt-4 pt-3 border-t border-paper-200 dark:border-ink-700">
|
|
132
|
+
<h5 className="text-xs font-medium text-ink-500 mb-2">Confidence Breakdown</h5>
|
|
133
|
+
<div className="space-y-2">
|
|
134
|
+
{breakdown.map((factor, idx) => (
|
|
135
|
+
<div key={idx} className="flex items-center gap-3">
|
|
136
|
+
<span className="text-xs text-ink-600 w-28 flex-shrink-0">{factor.factor}</span>
|
|
137
|
+
<div className="flex-1 h-2 bg-paper-200 rounded-full overflow-hidden">
|
|
138
|
+
<div
|
|
139
|
+
className={`h-full rounded-full transition-all ${
|
|
140
|
+
factor.score >= 80 ? 'bg-success-500' :
|
|
141
|
+
factor.score >= 50 ? 'bg-warning-500' :
|
|
142
|
+
'bg-error-500'
|
|
143
|
+
}`}
|
|
144
|
+
style={{ width: `${factor.score}%` }}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<span className="text-xs text-ink-400 w-10 text-right">{factor.score}%</span>
|
|
148
|
+
</div>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Notes */}
|
|
155
|
+
{notes && (
|
|
156
|
+
<div className="mt-3 px-3 py-2 bg-paper-50 dark:bg-ink-800 rounded text-xs text-ink-600 border-l-2 border-ink-200">
|
|
157
|
+
{notes}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Actions + timestamp */}
|
|
162
|
+
<div className="flex items-center justify-between gap-2 mt-4 pt-3 border-t border-paper-200 dark:border-ink-700">
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
{actions.map((action, idx) => (
|
|
165
|
+
<button
|
|
166
|
+
key={idx}
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={action.onClick}
|
|
169
|
+
className={`
|
|
170
|
+
inline-flex items-center gap-1.5 px-4 py-2 rounded text-sm font-medium
|
|
171
|
+
transition-colors
|
|
172
|
+
${buttonVariants[action.variant || 'secondary']}
|
|
173
|
+
`}
|
|
174
|
+
>
|
|
175
|
+
{action.icon}
|
|
176
|
+
{action.label}
|
|
177
|
+
</button>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
{timestamp && (
|
|
181
|
+
<span className="text-xs text-ink-400">{timestamp}</span>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Clock, AlertTriangle, CheckCircle } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Types & Interfaces
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
export interface SLAIndicatorProps {
|
|
9
|
+
/** Time remaining display string (e.g., "2h 15m", "45m") */
|
|
10
|
+
timeRemaining?: string;
|
|
11
|
+
/** Total SLA duration in minutes (for progress calculation) */
|
|
12
|
+
totalMinutes?: number;
|
|
13
|
+
/** Elapsed minutes (for progress calculation) */
|
|
14
|
+
elapsedMinutes?: number;
|
|
15
|
+
/** Current SLA status */
|
|
16
|
+
status: 'on-track' | 'at-risk' | 'breached' | 'met';
|
|
17
|
+
/** Size variant */
|
|
18
|
+
size?: 'sm' | 'md' | 'lg';
|
|
19
|
+
/** Show as compact inline badge */
|
|
20
|
+
compact?: boolean;
|
|
21
|
+
/** Additional className */
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Helpers
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
const statusConfig: Record<string, { icon: React.ReactNode; color: string; bg: string; text: string; label: string }> = {
|
|
30
|
+
'on-track': { icon: <Clock className="h-4 w-4" />, color: 'text-success-600', bg: 'bg-success-50', text: 'text-success-700', label: 'On Track' },
|
|
31
|
+
'at-risk': { icon: <Clock className="h-4 w-4" />, color: 'text-warning-600', bg: 'bg-warning-50', text: 'text-warning-700', label: 'At Risk' },
|
|
32
|
+
'breached': { icon: <AlertTriangle className="h-4 w-4" />, color: 'text-error-600', bg: 'bg-error-50', text: 'text-error-700', label: 'Breached' },
|
|
33
|
+
'met': { icon: <CheckCircle className="h-4 w-4" />, color: 'text-success-600', bg: 'bg-success-50', text: 'text-success-700', label: 'Met' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const progressColors: Record<string, string> = {
|
|
37
|
+
'on-track': 'bg-success-500',
|
|
38
|
+
'at-risk': 'bg-warning-500',
|
|
39
|
+
'breached': 'bg-error-500',
|
|
40
|
+
'met': 'bg-success-500',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Component
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* SLAIndicator — Visual SLA countdown with urgency coloring
|
|
49
|
+
*
|
|
50
|
+
* Shows time remaining, progress bar, and status icon.
|
|
51
|
+
* Used in Support Center case queue items and case detail views.
|
|
52
|
+
*/
|
|
53
|
+
export default function SLAIndicator({
|
|
54
|
+
timeRemaining,
|
|
55
|
+
totalMinutes,
|
|
56
|
+
elapsedMinutes,
|
|
57
|
+
status,
|
|
58
|
+
size = 'md',
|
|
59
|
+
compact = false,
|
|
60
|
+
className = '',
|
|
61
|
+
}: SLAIndicatorProps) {
|
|
62
|
+
const config = statusConfig[status] || statusConfig['on-track'];
|
|
63
|
+
const progressPercent = totalMinutes && elapsedMinutes
|
|
64
|
+
? Math.min(100, (elapsedMinutes / totalMinutes) * 100)
|
|
65
|
+
: undefined;
|
|
66
|
+
|
|
67
|
+
if (compact) {
|
|
68
|
+
return (
|
|
69
|
+
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${config.bg} ${config.text} ${className}`}>
|
|
70
|
+
{config.icon}
|
|
71
|
+
<span>{timeRemaining || config.label}</span>
|
|
72
|
+
</span>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sizeClasses = {
|
|
77
|
+
sm: { text: 'text-sm', icon: 'h-4 w-4', bar: 'h-1' },
|
|
78
|
+
md: { text: 'text-base', icon: 'h-5 w-5', bar: 'h-1.5' },
|
|
79
|
+
lg: { text: 'text-lg', icon: 'h-6 w-6', bar: 'h-2' },
|
|
80
|
+
};
|
|
81
|
+
const sizes = sizeClasses[size];
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={`${className}`}>
|
|
85
|
+
<div className="flex items-center justify-between gap-2 mb-1">
|
|
86
|
+
<div className={`flex items-center gap-1.5 ${config.color}`}>
|
|
87
|
+
{config.icon}
|
|
88
|
+
<span className={`${sizes.text} font-medium`}>{config.label}</span>
|
|
89
|
+
</div>
|
|
90
|
+
{timeRemaining && (
|
|
91
|
+
<span className={`${sizes.text} font-mono ${config.color}`}>
|
|
92
|
+
{timeRemaining}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Progress bar */}
|
|
98
|
+
{progressPercent !== undefined && (
|
|
99
|
+
<div className={`${sizes.bar} bg-paper-200 rounded-full overflow-hidden`}>
|
|
100
|
+
<div
|
|
101
|
+
className={`h-full ${progressColors[status]} rounded-full transition-all`}
|
|
102
|
+
style={{ width: `${progressPercent}%` }}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Types & Interfaces
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
export interface SplitPaneProps {
|
|
8
|
+
/** Content for the left/primary panel */
|
|
9
|
+
left: React.ReactNode;
|
|
10
|
+
/** Content for the right/secondary panel */
|
|
11
|
+
right: React.ReactNode;
|
|
12
|
+
/** Default width of the left panel as a fraction (0.0 - 1.0) */
|
|
13
|
+
defaultSplit?: number;
|
|
14
|
+
/** Minimum left panel width in pixels */
|
|
15
|
+
minLeftWidth?: number;
|
|
16
|
+
/** Minimum right panel width in pixels */
|
|
17
|
+
minRightWidth?: number;
|
|
18
|
+
/** Whether the divider is draggable */
|
|
19
|
+
resizable?: boolean;
|
|
20
|
+
/** Whether to show the right panel */
|
|
21
|
+
showRight?: boolean;
|
|
22
|
+
/** Orientation */
|
|
23
|
+
orientation?: 'horizontal' | 'vertical';
|
|
24
|
+
/** Callback when split ratio changes */
|
|
25
|
+
onSplitChange?: (ratio: number) => void;
|
|
26
|
+
/** Additional className */
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Component
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* SplitPane — Resizable two-panel layout
|
|
36
|
+
*
|
|
37
|
+
* Renders a left/primary panel and right/secondary panel with a draggable divider.
|
|
38
|
+
* Used for list+detail patterns (case queue + case detail, statements + line items).
|
|
39
|
+
*
|
|
40
|
+
* The right panel can be toggled on/off via `showRight` prop.
|
|
41
|
+
*/
|
|
42
|
+
export default function SplitPane({
|
|
43
|
+
left,
|
|
44
|
+
right,
|
|
45
|
+
defaultSplit = 0.4,
|
|
46
|
+
minLeftWidth = 200,
|
|
47
|
+
minRightWidth = 300,
|
|
48
|
+
resizable = true,
|
|
49
|
+
showRight = true,
|
|
50
|
+
orientation = 'horizontal',
|
|
51
|
+
onSplitChange,
|
|
52
|
+
className = '',
|
|
53
|
+
}: SplitPaneProps) {
|
|
54
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
55
|
+
const [splitRatio, setSplitRatio] = useState(defaultSplit);
|
|
56
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
57
|
+
|
|
58
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
59
|
+
if (!resizable) return;
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
setIsDragging(true);
|
|
62
|
+
}, [resizable]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!isDragging) return;
|
|
66
|
+
|
|
67
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
68
|
+
if (!containerRef.current) return;
|
|
69
|
+
|
|
70
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
71
|
+
const isHorizontal = orientation === 'horizontal';
|
|
72
|
+
const containerSize = isHorizontal ? rect.width : rect.height;
|
|
73
|
+
const position = isHorizontal ? e.clientX - rect.left : e.clientY - rect.top;
|
|
74
|
+
|
|
75
|
+
// Enforce min widths
|
|
76
|
+
const minLeft = minLeftWidth / containerSize;
|
|
77
|
+
const maxLeft = 1 - (minRightWidth / containerSize);
|
|
78
|
+
const newRatio = Math.min(maxLeft, Math.max(minLeft, position / containerSize));
|
|
79
|
+
|
|
80
|
+
setSplitRatio(newRatio);
|
|
81
|
+
onSplitChange?.(newRatio);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleMouseUp = () => {
|
|
85
|
+
setIsDragging(false);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
89
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
93
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
94
|
+
};
|
|
95
|
+
}, [isDragging, orientation, minLeftWidth, minRightWidth, onSplitChange]);
|
|
96
|
+
|
|
97
|
+
const isHorizontal = orientation === 'horizontal';
|
|
98
|
+
|
|
99
|
+
if (!showRight) {
|
|
100
|
+
return (
|
|
101
|
+
<div className={`h-full ${className}`}>
|
|
102
|
+
{left}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const leftSize = `${(splitRatio * 100).toFixed(1)}%`;
|
|
108
|
+
const rightSize = `${((1 - splitRatio) * 100).toFixed(1)}%`;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
ref={containerRef}
|
|
113
|
+
className={`
|
|
114
|
+
flex h-full overflow-hidden
|
|
115
|
+
${isHorizontal ? 'flex-row' : 'flex-col'}
|
|
116
|
+
${isDragging ? 'select-none' : ''}
|
|
117
|
+
${className}
|
|
118
|
+
`}
|
|
119
|
+
>
|
|
120
|
+
{/* Left/Primary panel */}
|
|
121
|
+
<div
|
|
122
|
+
className="overflow-auto"
|
|
123
|
+
style={isHorizontal ? { width: leftSize } : { height: leftSize }}
|
|
124
|
+
>
|
|
125
|
+
{left}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Divider */}
|
|
129
|
+
<div
|
|
130
|
+
onMouseDown={handleMouseDown}
|
|
131
|
+
className={`
|
|
132
|
+
flex-shrink-0 bg-paper-200 dark:bg-ink-700
|
|
133
|
+
${isHorizontal ? 'w-px hover:w-1' : 'h-px hover:h-1'}
|
|
134
|
+
${resizable ? 'cursor-col-resize hover:bg-primary-300 transition-all' : ''}
|
|
135
|
+
${isDragging ? 'bg-primary-400 w-1' : ''}
|
|
136
|
+
`}
|
|
137
|
+
role="separator"
|
|
138
|
+
aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
{/* Right/Secondary panel */}
|
|
142
|
+
<div
|
|
143
|
+
className="overflow-auto"
|
|
144
|
+
style={isHorizontal ? { width: rightSize } : { height: rightSize }}
|
|
145
|
+
>
|
|
146
|
+
{right}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|