@handled-ai/design-system 0.20.16 → 0.20.17
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/comment-composer.d.ts +1 -3
- package/dist/components/comment-composer.js +4 -8
- package/dist/components/comment-composer.js.map +1 -1
- package/dist/components/rich-text-toolbar.d.ts +9 -2
- package/dist/components/rich-text-toolbar.js +75 -16
- package/dist/components/rich-text-toolbar.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.js +14 -28
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/comment-composer.test.tsx +0 -15
- package/src/components/__tests__/rich-text-toolbar.test.tsx +28 -1
- package/src/components/comment-composer.tsx +4 -18
- package/src/components/rich-text-toolbar.tsx +77 -13
- package/src/prototype/__tests__/detail-view-case-panel-v2.test.tsx +0 -18
- package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +0 -52
- package/src/prototype/prototype-inbox-view.tsx +18 -41
|
@@ -13,8 +13,16 @@ type RichTextAction =
|
|
|
13
13
|
| "align" | "list"
|
|
14
14
|
| "delete"
|
|
15
15
|
|
|
16
|
+
interface RichTextFontOption {
|
|
17
|
+
label: string
|
|
18
|
+
value: string
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
interface RichTextToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
17
22
|
onAction?: (action: RichTextAction) => void
|
|
23
|
+
fontOptions?: RichTextFontOption[]
|
|
24
|
+
selectedFontFamily?: string
|
|
25
|
+
onFontFamilyChange?: (fontFamily: string) => void
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
function ToolbarButton({
|
|
@@ -43,7 +51,32 @@ function ToolbarButton({
|
|
|
43
51
|
)
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
function RichTextToolbar({
|
|
54
|
+
function RichTextToolbar({
|
|
55
|
+
onAction,
|
|
56
|
+
className,
|
|
57
|
+
fontOptions,
|
|
58
|
+
selectedFontFamily,
|
|
59
|
+
onFontFamilyChange,
|
|
60
|
+
...rest
|
|
61
|
+
}: RichTextToolbarProps) {
|
|
62
|
+
const [fontMenuOpen, setFontMenuOpen] = React.useState(false)
|
|
63
|
+
const fontMenuRef = React.useRef<HTMLDivElement | null>(null)
|
|
64
|
+
const selectedFontOption = fontOptions?.find((option) => option.value === selectedFontFamily) ?? fontOptions?.[0]
|
|
65
|
+
const fontLabel = selectedFontOption?.label ?? "Sans Serif"
|
|
66
|
+
const hasFontMenu = Boolean(fontOptions?.length && onFontFamilyChange)
|
|
67
|
+
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
if (!fontMenuOpen) return
|
|
70
|
+
|
|
71
|
+
function handleDocumentMouseDown(event: MouseEvent) {
|
|
72
|
+
if (fontMenuRef.current?.contains(event.target as Node)) return
|
|
73
|
+
setFontMenuOpen(false)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
document.addEventListener("mousedown", handleDocumentMouseDown)
|
|
77
|
+
return () => document.removeEventListener("mousedown", handleDocumentMouseDown)
|
|
78
|
+
}, [fontMenuOpen])
|
|
79
|
+
|
|
47
80
|
return (
|
|
48
81
|
<div
|
|
49
82
|
data-slot="rich-text-toolbar"
|
|
@@ -58,17 +91,48 @@ function RichTextToolbar({ onAction, className, ...rest }: RichTextToolbarProps)
|
|
|
58
91
|
|
|
59
92
|
<div className="w-px h-4 bg-border mx-1" aria-hidden="true" />
|
|
60
93
|
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
<div className="relative" ref={fontMenuRef}>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
data-slot="rich-text-toolbar-button"
|
|
98
|
+
onClick={() => {
|
|
99
|
+
onAction?.("font")
|
|
100
|
+
if (hasFontMenu) setFontMenuOpen((open) => !open)
|
|
101
|
+
}}
|
|
102
|
+
aria-label="Font family"
|
|
103
|
+
aria-haspopup="menu"
|
|
104
|
+
aria-expanded={hasFontMenu ? fontMenuOpen : undefined}
|
|
105
|
+
className="text-[11px] text-muted-foreground px-1.5 py-0.5 rounded hover:bg-muted/50 cursor-pointer flex items-center gap-1"
|
|
106
|
+
>
|
|
107
|
+
{fontLabel}
|
|
108
|
+
<ChevronDown size={10} />
|
|
109
|
+
</button>
|
|
110
|
+
|
|
111
|
+
{hasFontMenu && fontMenuOpen ? (
|
|
112
|
+
<div
|
|
113
|
+
role="menu"
|
|
114
|
+
aria-label="Font family"
|
|
115
|
+
className="absolute left-0 bottom-full z-50 mb-1 min-w-32 overflow-hidden rounded-md border border-border bg-background py-1 shadow-md"
|
|
116
|
+
>
|
|
117
|
+
{fontOptions!.map((option) => (
|
|
118
|
+
<button
|
|
119
|
+
key={option.value}
|
|
120
|
+
type="button"
|
|
121
|
+
role="menuitemradio"
|
|
122
|
+
aria-checked={option.value === selectedFontFamily}
|
|
123
|
+
className="block w-full px-2.5 py-1.5 text-left text-xs text-foreground hover:bg-muted/60"
|
|
124
|
+
style={{ fontFamily: option.value }}
|
|
125
|
+
onClick={() => {
|
|
126
|
+
onFontFamilyChange?.(option.value)
|
|
127
|
+
setFontMenuOpen(false)
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{option.label}
|
|
131
|
+
</button>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
) : null}
|
|
135
|
+
</div>
|
|
72
136
|
|
|
73
137
|
<div className="w-px h-4 bg-border mx-1" aria-hidden="true" />
|
|
74
138
|
|
|
@@ -87,4 +151,4 @@ function RichTextToolbar({ onAction, className, ...rest }: RichTextToolbarProps)
|
|
|
87
151
|
)
|
|
88
152
|
}
|
|
89
153
|
|
|
90
|
-
export { RichTextToolbar, type RichTextToolbarProps, type RichTextAction }
|
|
154
|
+
export { RichTextToolbar, type RichTextToolbarProps, type RichTextAction, type RichTextFontOption }
|
|
@@ -152,24 +152,6 @@ describe("DetailView case-panel-v2 section layout", () => {
|
|
|
152
152
|
)
|
|
153
153
|
})
|
|
154
154
|
|
|
155
|
-
it("adds a consistent stack around the case-panel workflow sections", () => {
|
|
156
|
-
const { container } = renderDetailView({
|
|
157
|
-
sectionLayout: "case-panel-v2",
|
|
158
|
-
renderPrimaryAction: () => <section>Primary action marker</section>,
|
|
159
|
-
renderCommentArea: () => <section>Comment area marker</section>,
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
const stack = container.querySelector('[data-slot="case-panel-workflow-stack"]')
|
|
163
|
-
const commentArea = container.querySelector('[data-slot="case-panel-comment-area"]')
|
|
164
|
-
expect(stack?.className).toContain("space-y-10")
|
|
165
|
-
expect(commentArea).not.toBeNull()
|
|
166
|
-
expectInDocumentOrder(
|
|
167
|
-
screen.getByText("Primary action marker"),
|
|
168
|
-
screen.getByText("Comment area marker"),
|
|
169
|
-
screen.getByText(/activity timeline/i),
|
|
170
|
-
)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
155
|
it("renders signal brief and chip-backed Why section without a separate The why block", () => {
|
|
174
156
|
renderDetailView({ sectionLayout: "case-panel-v2" })
|
|
175
157
|
|
|
@@ -142,58 +142,6 @@ describe("DetailView timeline system-events toggle", () => {
|
|
|
142
142
|
expect(container.textContent).not.toContain("Score updated -1")
|
|
143
143
|
})
|
|
144
144
|
|
|
145
|
-
it("keeps case-panel system events off by default even when localStorage has a stale visible value", async () => {
|
|
146
|
-
store["test-show-score-changes"] = "true"
|
|
147
|
-
const { container } = render(<DetailView {...baseProps({ sectionLayout: "case-panel-v2" })} />)
|
|
148
|
-
expandTimeline(container)
|
|
149
|
-
|
|
150
|
-
const toggle = container.querySelector('[data-testid="system-events-toggle"]') as HTMLElement
|
|
151
|
-
const eventCount = container.querySelector('[data-testid="event-count"]')
|
|
152
|
-
expect(toggle).toHaveAttribute("aria-pressed", "false")
|
|
153
|
-
expect(eventCount?.textContent).toBe("2 events")
|
|
154
|
-
expect(container.textContent).not.toContain("Score updated +3")
|
|
155
|
-
expect(localStorageMock.setItem).not.toHaveBeenCalledWith("test-show-score-changes", "false")
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it("resets case-panel system events to the default when the selected item changes", () => {
|
|
159
|
-
const { container, rerender } = render(
|
|
160
|
-
<DetailView {...baseProps({ sectionLayout: "case-panel-v2" })} />,
|
|
161
|
-
)
|
|
162
|
-
expandTimeline(container)
|
|
163
|
-
let toggle = container.querySelector('[data-testid="system-events-toggle"]') as HTMLElement
|
|
164
|
-
|
|
165
|
-
fireEvent.click(toggle)
|
|
166
|
-
expect(toggle).toHaveAttribute("aria-pressed", "true")
|
|
167
|
-
expect(container.textContent).toContain("Score updated +3")
|
|
168
|
-
|
|
169
|
-
rerender(
|
|
170
|
-
<DetailView
|
|
171
|
-
{...baseProps({
|
|
172
|
-
sectionLayout: "case-panel-v2",
|
|
173
|
-
item: { ...baseItem, id: "2", title: "Second Signal" },
|
|
174
|
-
})}
|
|
175
|
-
/>,
|
|
176
|
-
)
|
|
177
|
-
expandTimeline(container)
|
|
178
|
-
toggle = container.querySelector('[data-testid="system-events-toggle"]') as HTMLElement
|
|
179
|
-
|
|
180
|
-
expect(toggle).toHaveAttribute("aria-pressed", "false")
|
|
181
|
-
expect(container.textContent).not.toContain("Score updated +3")
|
|
182
|
-
expect(localStorageMock.setItem).not.toHaveBeenCalledWith("test-show-score-changes", "true")
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it("uses a shorter, quieter system-events control in the case-panel variant", () => {
|
|
186
|
-
const { container } = render(<DetailView {...baseProps({ sectionLayout: "case-panel-v2" })} />)
|
|
187
|
-
const toggle = container.querySelector('[data-testid="system-events-toggle"]') as HTMLElement
|
|
188
|
-
const indicator = container.querySelector('[data-testid="system-events-indicator"]') as HTMLElement
|
|
189
|
-
const count = container.querySelector('[data-testid="hidden-count-badge"]') as HTMLElement
|
|
190
|
-
|
|
191
|
-
expect(toggle.className).toContain("py-1")
|
|
192
|
-
expect(toggle.className).toContain("text-xs")
|
|
193
|
-
expect(indicator.className).toContain("h-3.5")
|
|
194
|
-
expect(count.className).toContain("text-[11px]")
|
|
195
|
-
})
|
|
196
|
-
|
|
197
145
|
it("reveals system-noise events when toggle is clicked", () => {
|
|
198
146
|
const { container } = render(<DetailView {...baseProps()} />)
|
|
199
147
|
expandTimeline(container)
|
|
@@ -309,12 +309,9 @@ function TimelineSection({
|
|
|
309
309
|
type="button"
|
|
310
310
|
onClick={() => setShowSystemEvents((prev) => !prev)}
|
|
311
311
|
className={cn(
|
|
312
|
-
"inline-flex shrink-0 cursor-pointer items-center rounded-full border transition-colors",
|
|
313
|
-
isCasePanel ? "gap-2 px-2.5 py-1 text-xs font-medium" : "gap-3 px-3.5 py-2 text-sm font-semibold",
|
|
312
|
+
"inline-flex shrink-0 cursor-pointer items-center gap-3 rounded-full border px-3.5 py-2 text-sm font-semibold transition-colors",
|
|
314
313
|
showSystemEvents
|
|
315
|
-
?
|
|
316
|
-
? "border-border bg-muted text-foreground shadow-sm hover:bg-muted/80"
|
|
317
|
-
: "border-foreground bg-foreground text-background shadow-sm hover:bg-foreground/90"
|
|
314
|
+
? "border-foreground bg-foreground text-background shadow-sm hover:bg-foreground/90"
|
|
318
315
|
: "border-border bg-background text-muted-foreground shadow-sm hover:bg-muted/40 hover:text-foreground"
|
|
319
316
|
)}
|
|
320
317
|
aria-pressed={showSystemEvents}
|
|
@@ -324,25 +321,23 @@ function TimelineSection({
|
|
|
324
321
|
>
|
|
325
322
|
<span
|
|
326
323
|
className={cn(
|
|
327
|
-
"relative inline-flex shrink-0 items-center rounded-full p-0.5 transition-colors",
|
|
328
|
-
|
|
329
|
-
showSystemEvents ? "bg-teal-600" : "bg-muted-foreground/25"
|
|
324
|
+
"relative inline-flex h-4 w-8 shrink-0 items-center rounded-full p-0.5 transition-colors",
|
|
325
|
+
showSystemEvents ? "bg-teal-600" : "bg-muted-foreground/30"
|
|
330
326
|
)}
|
|
331
327
|
aria-hidden="true"
|
|
332
328
|
data-testid="system-events-indicator"
|
|
333
329
|
>
|
|
334
330
|
<span
|
|
335
331
|
className={cn(
|
|
336
|
-
"block rounded-full bg-white shadow-sm transition-transform",
|
|
337
|
-
|
|
338
|
-
showSystemEvents ? (isCasePanel ? "translate-x-3.5" : "translate-x-4") : "translate-x-0"
|
|
332
|
+
"block h-3 w-3 rounded-full bg-white shadow-sm transition-transform",
|
|
333
|
+
showSystemEvents ? "translate-x-4" : "translate-x-0"
|
|
339
334
|
)}
|
|
340
335
|
/>
|
|
341
336
|
</span>
|
|
342
337
|
<span>{toggleLabel}</span>
|
|
343
338
|
{!showSystemEvents ? (
|
|
344
339
|
<span
|
|
345
|
-
className=
|
|
340
|
+
className="inline-flex min-w-[22px] items-center justify-center rounded-full bg-muted px-1.5 text-xs font-bold tabular-nums text-muted-foreground"
|
|
346
341
|
data-testid="hidden-count-badge"
|
|
347
342
|
>
|
|
348
343
|
{hiddenCount}
|
|
@@ -455,8 +450,6 @@ export function DetailView({
|
|
|
455
450
|
timelineSystemEventsVisibleHint,
|
|
456
451
|
])
|
|
457
452
|
|
|
458
|
-
const isCasePanelV2 = sectionLayout === "case-panel-v2"
|
|
459
|
-
|
|
460
453
|
const [showTimeline, setShowTimeline] = React.useState(false)
|
|
461
454
|
const [extraActions, setExtraActions] = React.useState<SuggestedAction[]>([])
|
|
462
455
|
|
|
@@ -466,17 +459,9 @@ export function DetailView({
|
|
|
466
459
|
const [showSystemEvents, setShowSystemEvents] = React.useState(sysEvtDefaultVisible)
|
|
467
460
|
const initialReadDoneRef = React.useRef(false)
|
|
468
461
|
|
|
469
|
-
// Read persisted value from localStorage on mount
|
|
470
|
-
// start from the configured default so stale local state cannot make system
|
|
471
|
-
// events prominent by default in the inbox detail panel.
|
|
462
|
+
// Read persisted value from localStorage on mount
|
|
472
463
|
React.useEffect(() => {
|
|
473
|
-
if (isCasePanelV2) {
|
|
474
|
-
setShowSystemEvents(sysEvtDefaultVisible)
|
|
475
|
-
initialReadDoneRef.current = true
|
|
476
|
-
return
|
|
477
|
-
}
|
|
478
464
|
if (!sysEvtStorageKey) {
|
|
479
|
-
setShowSystemEvents(sysEvtDefaultVisible)
|
|
480
465
|
initialReadDoneRef.current = true
|
|
481
466
|
return
|
|
482
467
|
}
|
|
@@ -484,27 +469,23 @@ export function DetailView({
|
|
|
484
469
|
const stored = localStorage.getItem(sysEvtStorageKey)
|
|
485
470
|
if (stored !== null) {
|
|
486
471
|
setShowSystemEvents(stored === "true")
|
|
487
|
-
} else {
|
|
488
|
-
setShowSystemEvents(sysEvtDefaultVisible)
|
|
489
472
|
}
|
|
490
473
|
} catch {
|
|
491
474
|
// localStorage unavailable — ignore
|
|
492
|
-
setShowSystemEvents(sysEvtDefaultVisible)
|
|
493
475
|
}
|
|
494
476
|
initialReadDoneRef.current = true
|
|
495
|
-
}, [
|
|
477
|
+
}, [sysEvtStorageKey])
|
|
496
478
|
|
|
497
|
-
// Write to localStorage when the toggle changes (skip
|
|
498
|
-
// they stay off by default on the next case/session).
|
|
479
|
+
// Write to localStorage when the toggle changes (skip initial if matching default)
|
|
499
480
|
React.useEffect(() => {
|
|
500
|
-
if (!sysEvtStorageKey
|
|
481
|
+
if (!sysEvtStorageKey) return
|
|
501
482
|
if (!initialReadDoneRef.current) return
|
|
502
483
|
try {
|
|
503
484
|
localStorage.setItem(sysEvtStorageKey, String(showSystemEvents))
|
|
504
485
|
} catch {
|
|
505
486
|
// localStorage unavailable — ignore
|
|
506
487
|
}
|
|
507
|
-
}, [
|
|
488
|
+
}, [showSystemEvents, sysEvtStorageKey])
|
|
508
489
|
|
|
509
490
|
React.useEffect(() => {
|
|
510
491
|
setShowTimeline(false)
|
|
@@ -547,6 +528,8 @@ export function DetailView({
|
|
|
547
528
|
? "border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-50"
|
|
548
529
|
: "hover:bg-muted/50"
|
|
549
530
|
|
|
531
|
+
const isCasePanelV2 = sectionLayout === "case-panel-v2"
|
|
532
|
+
|
|
550
533
|
// The metadata chips row (priority · deadline · account · renderMetadataExtra). Rendered above
|
|
551
534
|
// the brief by default, or beneath it when `metadataLayout === "below-brief"` (case-panel redesign).
|
|
552
535
|
const metadataChips = (
|
|
@@ -779,16 +762,10 @@ export function DetailView({
|
|
|
779
762
|
</>
|
|
780
763
|
) : null}
|
|
781
764
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
{renderCommentArea ? (
|
|
787
|
-
<div data-slot="case-panel-comment-area">
|
|
788
|
-
{renderCommentArea(item)}
|
|
789
|
-
</div>
|
|
790
|
-
) : null}
|
|
791
|
-
</div>
|
|
765
|
+
{/* After-score content slot (e.g. OpportunityPanel) */}
|
|
766
|
+
{renderAfterScore?.(item)}
|
|
767
|
+
{renderPrimaryAction?.(item)}
|
|
768
|
+
{renderCommentArea?.(item)}
|
|
792
769
|
{timelineSection}
|
|
793
770
|
</>
|
|
794
771
|
) : (
|