@handled-ai/design-system 0.11.3 → 0.13.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.
- package/dist/charts/chart.d.ts +1 -1
- package/dist/components/account-contacts-popover.js +7 -2
- package/dist/components/account-contacts-popover.js.map +1 -1
- package/dist/components/score-feedback.js +17 -28
- package/dist/components/score-feedback.js.map +1 -1
- package/dist/components/signal-feedback-inline.d.ts +23 -2
- package/dist/components/signal-feedback-inline.js +42 -14
- package/dist/components/signal-feedback-inline.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +5 -2
- package/dist/prototype/prototype-inbox-view.js +7 -1
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/package.json +3 -1
- package/src/components/__tests__/score-feedback-initial.test.tsx +16 -11
- package/src/components/account-contacts-popover.tsx +7 -2
- package/src/components/score-feedback.tsx +15 -37
- package/src/components/signal-feedback-inline.tsx +69 -8
- package/src/prototype/prototype-inbox-view.tsx +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handled-ai/design-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Handled UI component library (shadcn-style, New York)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@9.12.0",
|
|
@@ -171,9 +171,11 @@
|
|
|
171
171
|
"eslint": "^9.32.0",
|
|
172
172
|
"eslint-config-next": "15.3.1",
|
|
173
173
|
"happy-dom": "^20.9.0",
|
|
174
|
+
"lucide-react": "^1.14.0",
|
|
174
175
|
"next": "15.5.9",
|
|
175
176
|
"react": "19.1.0",
|
|
176
177
|
"react-dom": "19.1.0",
|
|
178
|
+
"recharts": "^3.8.1",
|
|
177
179
|
"shadcn": "^3.0.0",
|
|
178
180
|
"tailwindcss": "^4.1.11",
|
|
179
181
|
"three": "^0.183.1",
|
|
@@ -85,7 +85,7 @@ describe("ScoreFeedback.Root — initialFeedback prop", () => {
|
|
|
85
85
|
expect(screen.queryByText("Noted")).toBeNull();
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
it("
|
|
88
|
+
it("does NOT render pills inline in submitted state (compact trigger)", () => {
|
|
89
89
|
render(
|
|
90
90
|
<Wrapper
|
|
91
91
|
initialFeedback={makeInitialScoreFeedback({
|
|
@@ -93,28 +93,32 @@ describe("ScoreFeedback.Root — initialFeedback prop", () => {
|
|
|
93
93
|
})}
|
|
94
94
|
/>,
|
|
95
95
|
);
|
|
96
|
-
|
|
97
|
-
expect(screen.
|
|
96
|
+
// Pills are stored in state but not rendered in the compact trigger
|
|
97
|
+
expect(screen.queryByText("Right timing")).toBeNull();
|
|
98
|
+
expect(screen.queryByText("Accurate data")).toBeNull();
|
|
99
|
+
// Only the "Noted" label is visible
|
|
100
|
+
expect(screen.getByText("Noted")).toBeDefined();
|
|
98
101
|
});
|
|
99
102
|
|
|
100
|
-
it("
|
|
103
|
+
it("does NOT render detail text inline in submitted state (compact trigger)", () => {
|
|
101
104
|
render(
|
|
102
105
|
<Wrapper
|
|
103
106
|
initialFeedback={makeInitialScoreFeedback({ detail: "Score looks correct." })}
|
|
104
107
|
/>,
|
|
105
108
|
);
|
|
106
|
-
|
|
109
|
+
// Detail text is stored in state but not rendered in the compact trigger
|
|
110
|
+
expect(screen.queryByText("Score looks correct.")).toBeNull();
|
|
111
|
+
expect(screen.getByText("Noted")).toBeDefined();
|
|
107
112
|
});
|
|
108
113
|
|
|
109
|
-
it("minimal initialFeedback (no pills, no detail)
|
|
114
|
+
it("minimal initialFeedback (no pills, no detail) shows Noted as clickable button", () => {
|
|
110
115
|
const { container } = render(
|
|
111
116
|
<Wrapper initialFeedback={makeMinimalInitialScoreFeedback("up")} />,
|
|
112
117
|
);
|
|
113
118
|
expect(screen.getByText("Noted")).toBeDefined();
|
|
114
|
-
//
|
|
119
|
+
// The submitted state renders as a single compact button
|
|
115
120
|
const buttons = container.querySelectorAll("button");
|
|
116
|
-
|
|
117
|
-
expect(buttons.length).toBe(0);
|
|
121
|
+
expect(buttons.length).toBe(1);
|
|
118
122
|
});
|
|
119
123
|
});
|
|
120
124
|
|
|
@@ -226,7 +230,7 @@ describe("ScoreFeedback.Root — onSubmitFeedback callback", () => {
|
|
|
226
230
|
});
|
|
227
231
|
|
|
228
232
|
describe("ScoreFeedback.Root — editSubmitted", () => {
|
|
229
|
-
it("clicking the
|
|
233
|
+
it("clicking the compact 'Noted' button restores feedback into editing state", async () => {
|
|
230
234
|
const { container } = render(
|
|
231
235
|
<Wrapper
|
|
232
236
|
initialFeedback={makeInitialScoreFeedback({
|
|
@@ -236,9 +240,10 @@ describe("ScoreFeedback.Root — editSubmitted", () => {
|
|
|
236
240
|
/>,
|
|
237
241
|
);
|
|
238
242
|
|
|
239
|
-
// The
|
|
243
|
+
// The compact submitted button should be present
|
|
240
244
|
const editButton = container.querySelector("button");
|
|
241
245
|
expect(editButton).not.toBeNull();
|
|
246
|
+
expect(screen.getByText("Noted")).toBeDefined();
|
|
242
247
|
|
|
243
248
|
await act(async () => {
|
|
244
249
|
fireEvent.click(editButton!);
|
|
@@ -60,7 +60,12 @@ export function AccountContactsPopover({
|
|
|
60
60
|
let left = rect.right - popoverWidth
|
|
61
61
|
if (left < 16) left = 16
|
|
62
62
|
if (left + popoverWidth > window.innerWidth - 16) left = window.innerWidth - 16 - popoverWidth
|
|
63
|
-
|
|
63
|
+
const popoverHeight = 320;
|
|
64
|
+
const spaceBelow = window.innerHeight - rect.bottom - 8;
|
|
65
|
+
const spaceAbove = rect.top - 8;
|
|
66
|
+
const placeAbove = spaceBelow < popoverHeight && spaceAbove > spaceBelow;
|
|
67
|
+
const top = placeAbove ? rect.top - popoverHeight - 4 : rect.bottom + 4;
|
|
68
|
+
setPopoverStyle({ position: "fixed", top: Math.max(8, top), left, maxHeight: placeAbove ? spaceAbove : spaceBelow })
|
|
64
69
|
}
|
|
65
70
|
}, [open])
|
|
66
71
|
|
|
@@ -70,7 +75,7 @@ export function AccountContactsPopover({
|
|
|
70
75
|
{open && (
|
|
71
76
|
<>
|
|
72
77
|
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
|
73
|
-
<div style={popoverStyle} className="fixed bg-background border border-border rounded-lg shadow-xl z-50 w-[28rem] max-w-[calc(100vw-2rem)] py-2 animate-in fade-in slide-in-from-top-1 duration-150">
|
|
78
|
+
<div style={popoverStyle} className="fixed bg-background border border-border rounded-lg shadow-xl z-50 w-[28rem] max-w-[calc(100vw-2rem)] py-2 animate-in fade-in slide-in-from-top-1 duration-150 overflow-y-auto">
|
|
74
79
|
<div className="px-3 py-1.5 text-[11px] font-medium text-muted-foreground/60 uppercase tracking-wide">
|
|
75
80
|
Account Contacts
|
|
76
81
|
</div>
|
|
@@ -135,45 +135,23 @@ function Trigger({ className }: { className?: string }) {
|
|
|
135
135
|
const { thumbState, notedType, submittedFeedback, handleThumbClick, editSubmitted } = useScoreFeedback()
|
|
136
136
|
|
|
137
137
|
if (notedType || (submittedFeedback && !thumbState)) {
|
|
138
|
+
const label = notedType
|
|
139
|
+
? notedType === "up" ? "Noted" : "Recorded"
|
|
140
|
+
: submittedFeedback?.type === "up" ? "Noted" : "Recorded"
|
|
141
|
+
|
|
138
142
|
return (
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</span>
|
|
147
|
-
</div>
|
|
148
|
-
{submittedFeedback && (submittedFeedback.pills.length > 0 || submittedFeedback.detail) && (
|
|
149
|
-
<button
|
|
150
|
-
type="button"
|
|
151
|
-
onClick={editSubmitted}
|
|
152
|
-
className="mt-1.5 w-full text-left space-y-1 group cursor-pointer"
|
|
153
|
-
>
|
|
154
|
-
{submittedFeedback.pills.length > 0 && (
|
|
155
|
-
<div className="flex flex-wrap gap-1">
|
|
156
|
-
{submittedFeedback.pills.map((p) => (
|
|
157
|
-
<span
|
|
158
|
-
key={p}
|
|
159
|
-
className={cn(
|
|
160
|
-
"rounded-full border px-2 py-0.5 text-[10px] font-medium transition-colors group-hover:opacity-80",
|
|
161
|
-
submittedFeedback.type === "up"
|
|
162
|
-
? "border-emerald-200/60 bg-emerald-50/50 text-emerald-700/70"
|
|
163
|
-
: "border-red-200/60 bg-red-50/50 text-red-700/70"
|
|
164
|
-
)}
|
|
165
|
-
>
|
|
166
|
-
{p}
|
|
167
|
-
</span>
|
|
168
|
-
))}
|
|
169
|
-
</div>
|
|
170
|
-
)}
|
|
171
|
-
{submittedFeedback.detail && (
|
|
172
|
-
<p className="text-[11px] text-muted-foreground/70 leading-snug group-hover:text-muted-foreground transition-colors">{submittedFeedback.detail}</p>
|
|
173
|
-
)}
|
|
174
|
-
</button>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={submittedFeedback ? editSubmitted : undefined}
|
|
146
|
+
className={cn(
|
|
147
|
+
"flex items-center gap-1 shrink-0 rounded px-1.5 py-1 transition-colors",
|
|
148
|
+
submittedFeedback ? "cursor-pointer hover:bg-muted/50" : "cursor-default",
|
|
149
|
+
className,
|
|
175
150
|
)}
|
|
176
|
-
|
|
151
|
+
>
|
|
152
|
+
<Check className="w-3 h-3 text-emerald-500" />
|
|
153
|
+
<span className="text-[11px] text-muted-foreground">{label}</span>
|
|
154
|
+
</button>
|
|
177
155
|
)
|
|
178
156
|
}
|
|
179
157
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import { Check, CirclePlus, ExternalLink, Lock, ThumbsDown } from "lucide-react"
|
|
4
|
+
import { Check, CirclePlus, ExternalLink, Loader2, Lock, ThumbsDown } from "lucide-react"
|
|
5
5
|
|
|
6
6
|
interface DismissReasonNode {
|
|
7
7
|
label: string
|
|
@@ -105,6 +105,9 @@ interface SignalApprovalContextValue {
|
|
|
105
105
|
scheduledTime?: string
|
|
106
106
|
labels: Required<SignalApprovalLabels>
|
|
107
107
|
hideApproveButton?: boolean
|
|
108
|
+
approveButtonIconUrl?: string
|
|
109
|
+
opportunityPreview?: RootProps['opportunityPreview']
|
|
110
|
+
requestingApproval: boolean
|
|
108
111
|
approve: () => void
|
|
109
112
|
submitApproveFeedback: (reasons: string[], detail: string) => void
|
|
110
113
|
skipApproveFeedback: () => void
|
|
@@ -131,6 +134,23 @@ interface RootProps {
|
|
|
131
134
|
labels?: SignalApprovalLabels
|
|
132
135
|
/** When true, the approve/create-opportunity button is hidden but the dismiss button remains. */
|
|
133
136
|
hideApproveButton?: boolean
|
|
137
|
+
/** Optional icon URL for the approve button. Renders an img instead of CirclePlus when provided. */
|
|
138
|
+
approveButtonIconUrl?: string
|
|
139
|
+
/** Optional structured preview data shown in the confirmation dialog. */
|
|
140
|
+
opportunityPreview?: {
|
|
141
|
+
name: string
|
|
142
|
+
stage: string
|
|
143
|
+
closeDate: string
|
|
144
|
+
amount: string
|
|
145
|
+
accountName: string
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Async callback fired when the user clicks the approve button, BEFORE
|
|
149
|
+
* transitioning to the "confirming" state. While the promise is pending,
|
|
150
|
+
* the button shows a loading spinner. On resolve, transitions to "confirming".
|
|
151
|
+
* On reject, stays in "pending".
|
|
152
|
+
*/
|
|
153
|
+
onRequestApproval?: () => Promise<void>
|
|
134
154
|
/**
|
|
135
155
|
* Called when the user confirms the approval action.
|
|
136
156
|
*
|
|
@@ -145,9 +165,10 @@ interface RootProps {
|
|
|
145
165
|
onDismiss?: (reasons: string[], detail: string, subReason?: string) => void
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
function Root({ children, companyName, opportunityUrl, scheduledTime, initialApprovalState, labels: labelOverrides, hideApproveButton, onApprove, onApproveFeedback, onDismiss }: RootProps) {
|
|
168
|
+
function Root({ children, companyName, opportunityUrl, scheduledTime, initialApprovalState, labels: labelOverrides, hideApproveButton, approveButtonIconUrl, opportunityPreview, onRequestApproval, onApprove, onApproveFeedback, onDismiss }: RootProps) {
|
|
149
169
|
const labels = React.useMemo(() => ({ ...DEFAULT_LABELS, ...labelOverrides }), [labelOverrides])
|
|
150
170
|
const [approvalState, setApprovalState] = React.useState<ApprovalState>(initialApprovalState ?? "pending")
|
|
171
|
+
const [requestingApproval, setRequestingApproval] = React.useState(false)
|
|
151
172
|
|
|
152
173
|
// Guard against state updates after unmount (e.g. user navigates away while
|
|
153
174
|
// an async onApprove promise is still in flight).
|
|
@@ -157,8 +178,24 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
|
|
|
157
178
|
}, [])
|
|
158
179
|
|
|
159
180
|
const requestApproval = React.useCallback(() => {
|
|
160
|
-
|
|
161
|
-
|
|
181
|
+
if (onRequestApproval) {
|
|
182
|
+
setRequestingApproval(true)
|
|
183
|
+
onRequestApproval()
|
|
184
|
+
.then(() => {
|
|
185
|
+
if (mountedRef.current) {
|
|
186
|
+
setRequestingApproval(false)
|
|
187
|
+
setApprovalState("confirming")
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
.catch(() => {
|
|
191
|
+
if (mountedRef.current) {
|
|
192
|
+
setRequestingApproval(false)
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
} else {
|
|
196
|
+
setApprovalState("confirming")
|
|
197
|
+
}
|
|
198
|
+
}, [onRequestApproval])
|
|
162
199
|
|
|
163
200
|
const requestDismiss = React.useCallback(() => {
|
|
164
201
|
setApprovalState("dismissing")
|
|
@@ -210,7 +247,7 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
|
|
|
210
247
|
|
|
211
248
|
return (
|
|
212
249
|
<SignalApprovalCtx.Provider
|
|
213
|
-
value={{ approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel }}
|
|
250
|
+
value={{ approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel }}
|
|
214
251
|
>
|
|
215
252
|
{children}
|
|
216
253
|
</SignalApprovalCtx.Provider>
|
|
@@ -381,7 +418,7 @@ function SubmittedFeedback({
|
|
|
381
418
|
}
|
|
382
419
|
|
|
383
420
|
function Actions() {
|
|
384
|
-
const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
|
|
421
|
+
const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
|
|
385
422
|
useSignalApproval()
|
|
386
423
|
const [selectedTopReason, setSelectedTopReason] = React.useState<string | null>(null)
|
|
387
424
|
const [selectedSubReason, setSelectedSubReason] = React.useState<string | null>(null)
|
|
@@ -702,6 +739,22 @@ function Actions() {
|
|
|
702
739
|
<p className="text-sm text-foreground">
|
|
703
740
|
{labels.confirmPrompt} <strong>{companyName}</strong>. Confirm?
|
|
704
741
|
</p>
|
|
742
|
+
{opportunityPreview && (
|
|
743
|
+
<div className="mt-3 space-y-1.5 border-t border-border/50 pt-3">
|
|
744
|
+
{[
|
|
745
|
+
{ label: "Opportunity", value: opportunityPreview.name },
|
|
746
|
+
{ label: "Account", value: opportunityPreview.accountName },
|
|
747
|
+
{ label: "Stage", value: opportunityPreview.stage },
|
|
748
|
+
{ label: "Close Date", value: opportunityPreview.closeDate },
|
|
749
|
+
{ label: "Amount", value: opportunityPreview.amount },
|
|
750
|
+
].map(({ label, value }) => (
|
|
751
|
+
<div key={label} className="flex items-center justify-between text-xs">
|
|
752
|
+
<span className="text-muted-foreground">{label}</span>
|
|
753
|
+
<span className="font-medium text-foreground">{value}</span>
|
|
754
|
+
</div>
|
|
755
|
+
))}
|
|
756
|
+
</div>
|
|
757
|
+
)}
|
|
705
758
|
</div>
|
|
706
759
|
<div className="flex items-center gap-2">
|
|
707
760
|
<button
|
|
@@ -752,9 +805,16 @@ function Actions() {
|
|
|
752
805
|
<button
|
|
753
806
|
type="button"
|
|
754
807
|
onClick={requestApproval}
|
|
755
|
-
|
|
808
|
+
disabled={requestingApproval}
|
|
809
|
+
className="inline-flex h-7 items-center gap-1.5 rounded-md border border-border bg-foreground px-3 text-xs font-semibold text-background shadow-none transition-colors hover:bg-foreground/90 disabled:opacity-50"
|
|
756
810
|
>
|
|
757
|
-
|
|
811
|
+
{requestingApproval ? (
|
|
812
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
813
|
+
) : approveButtonIconUrl ? (
|
|
814
|
+
<img src={approveButtonIconUrl} alt="" className="h-3.5 w-3.5 object-contain" draggable={false} />
|
|
815
|
+
) : (
|
|
816
|
+
<CirclePlus className="h-3.5 w-3.5" />
|
|
817
|
+
)}
|
|
758
818
|
{labels.approveButton}
|
|
759
819
|
</button>
|
|
760
820
|
)}
|
|
@@ -801,4 +861,5 @@ export {
|
|
|
801
861
|
Gate as SignalApprovalGate,
|
|
802
862
|
}
|
|
803
863
|
export const SignalApproval = { Root, Actions, Gate }
|
|
864
|
+
export type OpportunityPreview = NonNullable<RootProps['opportunityPreview']>
|
|
804
865
|
export type { ApprovalState, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type InboxFilterCategory,
|
|
39
39
|
} from "../components/inbox-toolbar"
|
|
40
40
|
import { GroupedListView, type GroupedListGroup } from "../components/item-list"
|
|
41
|
-
import { SignalApproval, type ApprovalState } from "../components/signal-feedback-inline"
|
|
41
|
+
import { SignalApproval, type ApprovalState, type OpportunityPreview } from "../components/signal-feedback-inline"
|
|
42
42
|
import { ScoreFeedback } from "../components/score-feedback"
|
|
43
43
|
import { ScoreBreakdown } from "../components/score-breakdown"
|
|
44
44
|
import { Citation, type SourceDef } from "../components/detail-view"
|
|
@@ -133,6 +133,9 @@ export interface DetailViewProps {
|
|
|
133
133
|
/** Render extra metadata chips (e.g. assignee) inside the chips row below the title. */
|
|
134
134
|
renderMetadataExtra?: (item: QueueItem) => React.ReactNode
|
|
135
135
|
onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
|
|
136
|
+
approveButtonIconUrl?: string
|
|
137
|
+
opportunityPreview?: OpportunityPreview
|
|
138
|
+
onRequestApproval?: () => Promise<void>
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
export function DetailView({
|
|
@@ -156,6 +159,9 @@ export function DetailView({
|
|
|
156
159
|
renderDetailExtra,
|
|
157
160
|
renderMetadataExtra,
|
|
158
161
|
onScoreFeedback,
|
|
162
|
+
approveButtonIconUrl,
|
|
163
|
+
opportunityPreview,
|
|
164
|
+
onRequestApproval,
|
|
159
165
|
}: DetailViewProps) {
|
|
160
166
|
const [evidenceExpanded, setEvidenceExpanded] = React.useState(false)
|
|
161
167
|
const [showTimeline, setShowTimeline] = React.useState(false)
|
|
@@ -202,6 +208,9 @@ export function DetailView({
|
|
|
202
208
|
companyName={item.company}
|
|
203
209
|
labels={signalLabels}
|
|
204
210
|
hideApproveButton={hideApproveButton}
|
|
211
|
+
approveButtonIconUrl={approveButtonIconUrl}
|
|
212
|
+
opportunityPreview={opportunityPreview}
|
|
213
|
+
onRequestApproval={onRequestApproval}
|
|
205
214
|
initialApprovalState={getSignalApprovalState?.(item)}
|
|
206
215
|
onApprove={() => onSignalApprove?.(item)}
|
|
207
216
|
onApproveFeedback={(reasons, detail) => {
|