@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.
Files changed (41) hide show
  1. package/dist/components/badge.d.ts +1 -1
  2. package/dist/components/button.d.ts +1 -1
  3. package/dist/components/feedback-primitives.d.ts +41 -2
  4. package/dist/components/feedback-primitives.js +241 -6
  5. package/dist/components/feedback-primitives.js.map +1 -1
  6. package/dist/components/pill.d.ts +1 -1
  7. package/dist/components/score-why-chips.d.ts +1 -1
  8. package/dist/components/score-why-chips.js +26 -5
  9. package/dist/components/score-why-chips.js.map +1 -1
  10. package/dist/components/signal-priority-popover.d.ts +1 -1
  11. package/dist/components/signal-priority-popover.js +32 -6
  12. package/dist/components/signal-priority-popover.js.map +1 -1
  13. package/dist/components/tabs.d.ts +1 -1
  14. package/dist/components/timeline-activity.d.ts +16 -1
  15. package/dist/components/timeline-activity.js +69 -1
  16. package/dist/components/timeline-activity.js.map +1 -1
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/prototype/index.d.ts +1 -1
  21. package/dist/prototype/prototype-accounts-view.d.ts +1 -1
  22. package/dist/prototype/prototype-admin-view.d.ts +1 -1
  23. package/dist/prototype/prototype-config.d.ts +1 -1
  24. package/dist/prototype/prototype-inbox-view.d.ts +15 -3
  25. package/dist/prototype/prototype-inbox-view.js +160 -37
  26. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  27. package/dist/prototype/prototype-insights-view.d.ts +1 -1
  28. package/dist/prototype/prototype-shell.d.ts +1 -1
  29. package/dist/{signal-priority-popover-DQ_VuHac.d.ts → signal-priority-popover-BT6CPYNs.d.ts} +43 -3
  30. package/package.json +2 -1
  31. package/src/components/__tests__/timeline-activity.test.tsx +152 -0
  32. package/src/components/__tests__/wit-636-feedback-states.test.tsx +546 -0
  33. package/src/components/feedback-primitives.tsx +333 -26
  34. package/src/components/score-why-chips.tsx +28 -2
  35. package/src/components/signal-priority-popover.tsx +44 -4
  36. package/src/components/timeline-activity.tsx +112 -1
  37. package/src/index.ts +2 -2
  38. package/src/prototype/__tests__/detail-view-attention.test.tsx +2 -2
  39. package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +409 -0
  40. package/src/prototype/prototype-config.ts +32 -1
  41. 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
+ &middot; 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
- <div className="mb-8">
356
- <button
357
- type="button"
358
- onClick={() => setShowTimeline((prev) => !prev)}
359
- 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 cursor-pointer"
360
- >
361
- <div className="flex items-center gap-2">
362
- <h3 className="text-xs font-bold text-muted-foreground uppercase tracking-wider group-hover/timeline:text-foreground transition-colors">Activity timeline</h3>
363
- {!showTimeline && attentionCount != null && attentionCount > 0 && (
364
- <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">
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
- &middot; 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,