@handled-ai/design-system 0.18.5 → 0.18.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.
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/feedback-primitives.d.ts +41 -2
- package/dist/components/feedback-primitives.js +241 -6
- package/dist/components/feedback-primitives.js.map +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/score-why-chips.js +26 -5
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +32 -6
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/timeline-activity.d.ts +16 -1
- package/dist/components/timeline-activity.js +69 -1
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +15 -3
- package/dist/prototype/prototype-inbox-view.js +160 -37
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-DQ_VuHac.d.ts → signal-priority-popover-BT6CPYNs.d.ts} +43 -3
- package/package.json +2 -1
- package/src/components/__tests__/timeline-activity.test.tsx +152 -0
- package/src/components/__tests__/wit-636-feedback-states.test.tsx +546 -0
- package/src/components/feedback-primitives.tsx +333 -26
- package/src/components/score-why-chips.tsx +28 -2
- package/src/components/signal-priority-popover.tsx +44 -4
- package/src/components/timeline-activity.tsx +112 -1
- package/src/index.ts +2 -2
- package/src/prototype/__tests__/detail-view-attention.test.tsx +2 -2
- package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +409 -0
- package/src/prototype/prototype-config.ts +32 -1
- package/src/prototype/prototype-inbox-view.tsx +230 -30
|
@@ -53,6 +53,7 @@ import type {
|
|
|
53
53
|
InboxDetailSections,
|
|
54
54
|
SignalScoreData,
|
|
55
55
|
BriefStyleVariant,
|
|
56
|
+
TimelineSystemEventsConfig,
|
|
56
57
|
} from "./prototype-config"
|
|
57
58
|
|
|
58
59
|
// ---------------------------------------------------------------------------
|
|
@@ -150,8 +151,151 @@ export interface DetailViewProps {
|
|
|
150
151
|
onRequestApproval?: () => Promise<void>
|
|
151
152
|
/** Number of important/attention-worthy events to highlight on the collapsed timeline header. */
|
|
152
153
|
attentionCount?: number
|
|
154
|
+
/** Configuration for the system-noise events toggle (score changes, etc.). */
|
|
155
|
+
timelineSystemEventsConfig?: TimelineSystemEventsConfig
|
|
156
|
+
|
|
157
|
+
// ── Deprecated individual props (use timelineSystemEventsConfig instead) ──
|
|
158
|
+
/** @deprecated Use `timelineSystemEventsConfig.toggleLabel`. */
|
|
159
|
+
timelineSystemEventsToggleLabel?: string
|
|
160
|
+
/** @deprecated Use `timelineSystemEventsConfig.storageKey`. */
|
|
161
|
+
timelineSystemEventsStorageKey?: string
|
|
162
|
+
/** @deprecated Use `timelineSystemEventsConfig.defaultVisible`. */
|
|
163
|
+
timelineSystemEventsDefaultVisible?: boolean
|
|
164
|
+
/** @deprecated Use `timelineSystemEventsConfig.hiddenHint`. */
|
|
165
|
+
timelineSystemEventsHiddenHint?: string
|
|
166
|
+
/** @deprecated Use `timelineSystemEventsConfig.visibleHint`. */
|
|
167
|
+
timelineSystemEventsVisibleHint?: string
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// TimelineSection — extracted from the IIFE in DetailView for readability
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
function TimelineSection({
|
|
175
|
+
timelineEvents,
|
|
176
|
+
showTimeline,
|
|
177
|
+
setShowTimeline,
|
|
178
|
+
showSystemEvents,
|
|
179
|
+
setShowSystemEvents,
|
|
180
|
+
attentionCount,
|
|
181
|
+
sysEvtConfig,
|
|
182
|
+
lastActivityTime,
|
|
183
|
+
}: {
|
|
184
|
+
timelineEvents: TimelineEvent[]
|
|
185
|
+
showTimeline: boolean
|
|
186
|
+
setShowTimeline: React.Dispatch<React.SetStateAction<boolean>>
|
|
187
|
+
showSystemEvents: boolean
|
|
188
|
+
setShowSystemEvents: React.Dispatch<React.SetStateAction<boolean>>
|
|
189
|
+
attentionCount?: number
|
|
190
|
+
sysEvtConfig?: TimelineSystemEventsConfig
|
|
191
|
+
lastActivityTime?: string
|
|
192
|
+
}) {
|
|
193
|
+
// Single-pass partition: compute visibleEvents and hiddenCount together
|
|
194
|
+
const visibleEvents: TimelineEvent[] = []
|
|
195
|
+
let hiddenCount = 0
|
|
196
|
+
for (const e of timelineEvents) {
|
|
197
|
+
if (e.isSystemNoise) hiddenCount++
|
|
198
|
+
if (!e.isSystemNoise || showSystemEvents) visibleEvents.push(e)
|
|
199
|
+
}
|
|
200
|
+
const hasSystemNoise = hiddenCount > 0
|
|
201
|
+
|
|
202
|
+
// The toggle renders whenever there are system-noise events — even if no
|
|
203
|
+
// config was provided — so consumers that emit `isSystemNoise: true` always
|
|
204
|
+
// give users a way to reveal those events.
|
|
205
|
+
const toggleLabel = sysEvtConfig?.toggleLabel ?? "System events"
|
|
206
|
+
|
|
207
|
+
// Derive "Last activity" from the first *visible* event so the collapsed
|
|
208
|
+
// header never points at a hidden score-update. The caller-supplied
|
|
209
|
+
// `lastActivityTime` is only used when system-noise filtering is NOT active
|
|
210
|
+
// (i.e. all events are visible) since it may come from an unfiltered source
|
|
211
|
+
// such as `case.last_activity_at`.
|
|
212
|
+
const firstVisibleTime =
|
|
213
|
+
(!hasSystemNoise || showSystemEvents) && lastActivityTime
|
|
214
|
+
? lastActivityTime
|
|
215
|
+
: visibleEvents.length > 0
|
|
216
|
+
? visibleEvents[0].time
|
|
217
|
+
: ""
|
|
218
|
+
|
|
219
|
+
const visibleCount = visibleEvents.length
|
|
220
|
+
const eventCountLabel = `${visibleCount} ${visibleCount === 1 ? "event" : "events"}`
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div className="mb-8">
|
|
224
|
+
{/* Header — outer non-interactive container */}
|
|
225
|
+
<div
|
|
226
|
+
className="group/timeline flex w-full items-center justify-between gap-2 py-2 rounded-md transition-colors hover:bg-muted/40 -mx-2 px-2"
|
|
227
|
+
data-testid="timeline-header"
|
|
228
|
+
>
|
|
229
|
+
{/* Left: collapse/expand button */}
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
onClick={() => setShowTimeline((prev) => !prev)}
|
|
233
|
+
className="flex items-center gap-2 cursor-pointer bg-transparent border-0 p-0"
|
|
234
|
+
data-testid="timeline-collapse-btn"
|
|
235
|
+
>
|
|
236
|
+
<h3 className="text-xs font-bold text-muted-foreground uppercase tracking-wider group-hover/timeline:text-foreground transition-colors">Activity timeline</h3>
|
|
237
|
+
{!showTimeline && attentionCount != null && attentionCount > 0 && (
|
|
238
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-destructive/10 px-1.5 py-0.5 text-[10px] font-semibold text-destructive border border-destructive/20">
|
|
239
|
+
{attentionCount} new
|
|
240
|
+
</span>
|
|
241
|
+
)}
|
|
242
|
+
{!showTimeline && firstVisibleTime && (
|
|
243
|
+
<span className="text-[11px] text-muted-foreground/60" data-testid="last-activity-hint">
|
|
244
|
+
· Last activity {firstVisibleTime}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
<div className="flex items-center gap-1.5">
|
|
248
|
+
<span className="text-[11px] font-medium text-muted-foreground" data-testid="event-count">{eventCountLabel}</span>
|
|
249
|
+
<ChevronDown className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-200 ${showTimeline ? "rotate-180" : ""}`} />
|
|
250
|
+
</div>
|
|
251
|
+
</button>
|
|
252
|
+
|
|
253
|
+
{/* Right: system-events toggle — always rendered when noise events exist */}
|
|
254
|
+
{hasSystemNoise && (
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
onClick={() => setShowSystemEvents((prev) => !prev)}
|
|
258
|
+
className="flex shrink-0 items-center gap-1.5 rounded-full border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground cursor-pointer"
|
|
259
|
+
aria-pressed={showSystemEvents}
|
|
260
|
+
data-testid="system-events-toggle"
|
|
261
|
+
>
|
|
262
|
+
{toggleLabel}
|
|
263
|
+
<span
|
|
264
|
+
className="inline-flex items-center justify-center rounded-full bg-muted px-1.5 text-[10px] font-semibold min-w-[18px] tabular-nums"
|
|
265
|
+
data-testid="hidden-count-badge"
|
|
266
|
+
>
|
|
267
|
+
{hiddenCount}
|
|
268
|
+
</span>
|
|
269
|
+
</button>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Timeline body */}
|
|
274
|
+
{showTimeline && visibleEvents.length > 0 && (
|
|
275
|
+
<div className="mt-3">
|
|
276
|
+
<TimelineActivity events={visibleEvents} />
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
{/* Footer hint */}
|
|
281
|
+
{showTimeline && !showSystemEvents && sysEvtConfig?.hiddenHint && hasSystemNoise && (
|
|
282
|
+
<p className="mt-2 text-[11px] text-muted-foreground/60 border-t border-dashed border-border pt-2" data-testid="timeline-footer-hint">
|
|
283
|
+
{sysEvtConfig.hiddenHint}
|
|
284
|
+
</p>
|
|
285
|
+
)}
|
|
286
|
+
{showTimeline && showSystemEvents && sysEvtConfig?.visibleHint && hasSystemNoise && (
|
|
287
|
+
<p className="mt-2 text-[11px] text-muted-foreground/60 border-t border-dashed border-border pt-2" data-testid="timeline-footer-hint">
|
|
288
|
+
{sysEvtConfig.visibleHint.replace("{count}", String(hiddenCount))}
|
|
289
|
+
</p>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
)
|
|
153
293
|
}
|
|
154
294
|
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Detail View
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
155
299
|
export function DetailView({
|
|
156
300
|
item,
|
|
157
301
|
sections,
|
|
@@ -184,10 +328,79 @@ export function DetailView({
|
|
|
184
328
|
opportunityPreview,
|
|
185
329
|
onRequestApproval,
|
|
186
330
|
attentionCount,
|
|
331
|
+
timelineSystemEventsConfig: configProp,
|
|
332
|
+
timelineSystemEventsToggleLabel,
|
|
333
|
+
timelineSystemEventsStorageKey,
|
|
334
|
+
timelineSystemEventsDefaultVisible,
|
|
335
|
+
timelineSystemEventsHiddenHint,
|
|
336
|
+
timelineSystemEventsVisibleHint,
|
|
187
337
|
}: DetailViewProps) {
|
|
338
|
+
// Resolve system-events config: prefer the config object, fall back to deprecated individual props.
|
|
339
|
+
const sysEvtConfig = React.useMemo<TimelineSystemEventsConfig | undefined>(() => {
|
|
340
|
+
if (configProp) return configProp
|
|
341
|
+
// Build from deprecated individual props if any are provided
|
|
342
|
+
if (
|
|
343
|
+
timelineSystemEventsToggleLabel ||
|
|
344
|
+
timelineSystemEventsStorageKey ||
|
|
345
|
+
timelineSystemEventsDefaultVisible !== undefined ||
|
|
346
|
+
timelineSystemEventsHiddenHint ||
|
|
347
|
+
timelineSystemEventsVisibleHint
|
|
348
|
+
) {
|
|
349
|
+
return {
|
|
350
|
+
toggleLabel: timelineSystemEventsToggleLabel,
|
|
351
|
+
storageKey: timelineSystemEventsStorageKey,
|
|
352
|
+
defaultVisible: timelineSystemEventsDefaultVisible,
|
|
353
|
+
hiddenHint: timelineSystemEventsHiddenHint,
|
|
354
|
+
visibleHint: timelineSystemEventsVisibleHint,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return undefined
|
|
358
|
+
}, [
|
|
359
|
+
configProp,
|
|
360
|
+
timelineSystemEventsToggleLabel,
|
|
361
|
+
timelineSystemEventsStorageKey,
|
|
362
|
+
timelineSystemEventsDefaultVisible,
|
|
363
|
+
timelineSystemEventsHiddenHint,
|
|
364
|
+
timelineSystemEventsVisibleHint,
|
|
365
|
+
])
|
|
366
|
+
|
|
188
367
|
const [showTimeline, setShowTimeline] = React.useState(false)
|
|
189
368
|
const [extraActions, setExtraActions] = React.useState<SuggestedAction[]>([])
|
|
190
369
|
|
|
370
|
+
// ---- System-noise toggle state ----
|
|
371
|
+
const sysEvtDefaultVisible = sysEvtConfig?.defaultVisible ?? false
|
|
372
|
+
const sysEvtStorageKey = sysEvtConfig?.storageKey
|
|
373
|
+
const [showSystemEvents, setShowSystemEvents] = React.useState(sysEvtDefaultVisible)
|
|
374
|
+
const initialReadDoneRef = React.useRef(false)
|
|
375
|
+
|
|
376
|
+
// Read persisted value from localStorage on mount
|
|
377
|
+
React.useEffect(() => {
|
|
378
|
+
if (!sysEvtStorageKey) {
|
|
379
|
+
initialReadDoneRef.current = true
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const stored = localStorage.getItem(sysEvtStorageKey)
|
|
384
|
+
if (stored !== null) {
|
|
385
|
+
setShowSystemEvents(stored === "true")
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
// localStorage unavailable — ignore
|
|
389
|
+
}
|
|
390
|
+
initialReadDoneRef.current = true
|
|
391
|
+
}, [sysEvtStorageKey])
|
|
392
|
+
|
|
393
|
+
// Write to localStorage when the toggle changes (skip initial if matching default)
|
|
394
|
+
React.useEffect(() => {
|
|
395
|
+
if (!sysEvtStorageKey) return
|
|
396
|
+
if (!initialReadDoneRef.current) return
|
|
397
|
+
try {
|
|
398
|
+
localStorage.setItem(sysEvtStorageKey, String(showSystemEvents))
|
|
399
|
+
} catch {
|
|
400
|
+
// localStorage unavailable — ignore
|
|
401
|
+
}
|
|
402
|
+
}, [showSystemEvents, sysEvtStorageKey])
|
|
403
|
+
|
|
191
404
|
React.useEffect(() => {
|
|
192
405
|
setShowTimeline(false)
|
|
193
406
|
setExtraActions([])
|
|
@@ -272,6 +485,9 @@ export function DetailView({
|
|
|
272
485
|
metaText={undefined}
|
|
273
486
|
feedbackChips={signalData.priorityFeedbackChips}
|
|
274
487
|
onFeedbackSubmit={signalData.onPriorityFeedback}
|
|
488
|
+
initialFactorFeedback={signalData.initialFactorPopoverFeedback}
|
|
489
|
+
onFactorFeedback={signalData.onFactorFeedback}
|
|
490
|
+
initialPriorityFeedback={signalData.initialPriorityFeedback}
|
|
275
491
|
/>
|
|
276
492
|
{signalData.timeChipLabel && (
|
|
277
493
|
<Badge variant="outline" title={signalData.timeChipDetail ?? undefined}>
|
|
@@ -352,36 +568,16 @@ export function DetailView({
|
|
|
352
568
|
|
|
353
569
|
{/* Activity Timeline */}
|
|
354
570
|
{sections.timeline && timelineEvents.length > 0 && (
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
{attentionCount} new
|
|
366
|
-
</span>
|
|
367
|
-
)}
|
|
368
|
-
{!showTimeline && (lastActivityTime || (timelineEvents.length > 0 && timelineEvents[0].time)) && (
|
|
369
|
-
<span className="text-[11px] text-muted-foreground/60">
|
|
370
|
-
· Last activity {lastActivityTime ?? timelineEvents[0]?.time ?? ''}
|
|
371
|
-
</span>
|
|
372
|
-
)}
|
|
373
|
-
</div>
|
|
374
|
-
<div className="flex items-center gap-1.5">
|
|
375
|
-
<span className="text-[11px] font-medium text-muted-foreground">{timelineEvents.length} events</span>
|
|
376
|
-
<ChevronDown className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-200 ${showTimeline ? "rotate-180" : ""}`} />
|
|
377
|
-
</div>
|
|
378
|
-
</button>
|
|
379
|
-
{showTimeline && (
|
|
380
|
-
<div className="mt-3">
|
|
381
|
-
<TimelineActivity events={timelineEvents} />
|
|
382
|
-
</div>
|
|
383
|
-
)}
|
|
384
|
-
</div>
|
|
571
|
+
<TimelineSection
|
|
572
|
+
timelineEvents={timelineEvents}
|
|
573
|
+
showTimeline={showTimeline}
|
|
574
|
+
setShowTimeline={setShowTimeline}
|
|
575
|
+
showSystemEvents={showSystemEvents}
|
|
576
|
+
setShowSystemEvents={setShowSystemEvents}
|
|
577
|
+
attentionCount={attentionCount}
|
|
578
|
+
sysEvtConfig={sysEvtConfig}
|
|
579
|
+
lastActivityTime={lastActivityTime}
|
|
580
|
+
/>
|
|
385
581
|
)}
|
|
386
582
|
</div>
|
|
387
583
|
|
|
@@ -450,6 +646,8 @@ export function PrototypeInboxView({
|
|
|
450
646
|
renderBeforeScore,
|
|
451
647
|
renderAfterScore,
|
|
452
648
|
lastActivityTime,
|
|
649
|
+
timelineSystemEventsConfig,
|
|
650
|
+
attentionCount,
|
|
453
651
|
renderTitleExtra,
|
|
454
652
|
renderTitleSubtext,
|
|
455
653
|
sortOptions,
|
|
@@ -690,6 +888,8 @@ export function PrototypeInboxView({
|
|
|
690
888
|
renderBeforeScore,
|
|
691
889
|
renderAfterScore,
|
|
692
890
|
lastActivityTime,
|
|
891
|
+
timelineSystemEventsConfig,
|
|
892
|
+
attentionCount,
|
|
693
893
|
renderTitleExtra,
|
|
694
894
|
renderTitleSubtext,
|
|
695
895
|
onOpenSignalBucket,
|