@handled-ai/design-system 0.20.3 → 0.20.4
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.js +4 -4
- package/dist/components/comment-composer.js.map +1 -1
- package/dist/components/conversation-panel.d.ts +20 -2
- package/dist/components/conversation-panel.js +80 -23
- package/dist/components/conversation-panel.js.map +1 -1
- package/dist/components/owner-chips.js +9 -9
- package/dist/components/owner-chips.js.map +1 -1
- package/dist/components/timeline-activity.js +14 -14
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/comment-composer.test.tsx +28 -0
- package/src/components/__tests__/conversation-panel.test.tsx +169 -0
- package/src/components/__tests__/owner-chips.test.tsx +27 -0
- package/src/components/__tests__/timeline-activity.test.tsx +33 -0
- package/src/components/comment-composer.tsx +4 -4
- package/src/components/conversation-panel.tsx +114 -21
- package/src/components/owner-chips.tsx +12 -10
- package/src/components/timeline-activity.tsx +14 -14
|
@@ -113,6 +113,17 @@ export interface ConversationReplyPayload {
|
|
|
113
113
|
replyAll: boolean
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Result of a server-side reply preview. `htmlBody` is the exact HTML the server
|
|
118
|
+
* prepared for this reply (sanitized by the component before render).
|
|
119
|
+
* `confirmationToken`, when present, identifies the prepared confirmation so a
|
|
120
|
+
* consumer can reuse it for the final send instead of re-preparing the message.
|
|
121
|
+
*/
|
|
122
|
+
export interface ConversationReplyPreview {
|
|
123
|
+
htmlBody: string
|
|
124
|
+
confirmationToken?: string
|
|
125
|
+
}
|
|
126
|
+
|
|
116
127
|
export interface ConversationPanelProps {
|
|
117
128
|
threads: ConversationThread[]
|
|
118
129
|
/** Current operator: drives "to me" + the reply avatar. */
|
|
@@ -121,6 +132,14 @@ export interface ConversationPanelProps {
|
|
|
121
132
|
tenantName?: string
|
|
122
133
|
onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>
|
|
123
134
|
onCreateGmailDraft?: (payload: ConversationReplyPayload) => void | Promise<void>
|
|
135
|
+
/**
|
|
136
|
+
* Server-side preview contract. When provided, the reply preview requests the
|
|
137
|
+
* exact send HTML from the server, the component sanitizes it before render,
|
|
138
|
+
* and retains any returned `confirmationToken` in preview state so a consumer
|
|
139
|
+
* can reuse it for the final send. When omitted, the composer falls back to a
|
|
140
|
+
* clearly labeled local draft preview only.
|
|
141
|
+
*/
|
|
142
|
+
onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>
|
|
124
143
|
onOpenInGmail?: (threadId: string) => void
|
|
125
144
|
/** Inline-open this thread initially (defaults to the first responded one). */
|
|
126
145
|
defaultOpenThreadId?: string
|
|
@@ -486,7 +505,7 @@ function firstName(name: string): string {
|
|
|
486
505
|
|
|
487
506
|
const STATUS_PILL: Record<ConvStatus, { label: string; cls: string }> = {
|
|
488
507
|
responded: { label: "New reply", cls: "bg-status-active-bg text-status-active-fg border-status-active-border" },
|
|
489
|
-
draft: { label: "Draft", cls: "bg-
|
|
508
|
+
draft: { label: "Draft", cls: "bg-background text-foreground/80 border-border" },
|
|
490
509
|
awaiting: { label: "Awaiting", cls: "bg-status-pending-bg text-status-pending-fg border-status-pending-border" },
|
|
491
510
|
viewing: { label: "Viewing", cls: "bg-muted text-muted-foreground border-border" },
|
|
492
511
|
}
|
|
@@ -634,6 +653,19 @@ function MessageView({
|
|
|
634
653
|
|
|
635
654
|
/* ── Reply composer ─────────────────────────────────────────────────────── */
|
|
636
655
|
|
|
656
|
+
type PreviewState = {
|
|
657
|
+
loading: boolean
|
|
658
|
+
/** Sanitized HTML ready to render. */
|
|
659
|
+
html: string | null
|
|
660
|
+
/** Confirmation token returned by the server preview, retained for reuse on send. */
|
|
661
|
+
confirmationToken: string | null
|
|
662
|
+
error: string | null
|
|
663
|
+
/** True when `html` came from the local fallback rather than the server. */
|
|
664
|
+
local: boolean
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }
|
|
668
|
+
|
|
637
669
|
function ReplyComposer({
|
|
638
670
|
thread,
|
|
639
671
|
me,
|
|
@@ -642,6 +674,7 @@ function ReplyComposer({
|
|
|
642
674
|
onClose,
|
|
643
675
|
onSend,
|
|
644
676
|
onDraft,
|
|
677
|
+
onPreviewReply,
|
|
645
678
|
}: {
|
|
646
679
|
thread: ConversationThread
|
|
647
680
|
me?: ConvParticipant
|
|
@@ -650,16 +683,49 @@ function ReplyComposer({
|
|
|
650
683
|
onClose: () => void
|
|
651
684
|
onSend: (body: string, includeSignature: boolean) => void | Promise<void>
|
|
652
685
|
onDraft: (body: string, includeSignature: boolean) => void | Promise<void>
|
|
686
|
+
onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>
|
|
653
687
|
}) {
|
|
654
688
|
const [body, setBody] = React.useState(thread.draft ?? "")
|
|
655
689
|
const [sig, setSig] = React.useState(true)
|
|
656
690
|
const [preview, setPreview] = React.useState(false)
|
|
691
|
+
const [previewState, setPreviewState] = React.useState<PreviewState>(IDLE_PREVIEW)
|
|
657
692
|
const [sending, setSending] = React.useState(false)
|
|
658
693
|
const [sendError, setSendError] = React.useState<string | null>(null)
|
|
659
694
|
const ccList = replyAll ? thread.cc ?? [] : []
|
|
660
695
|
const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`
|
|
661
696
|
|
|
662
|
-
const
|
|
697
|
+
const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : "")
|
|
698
|
+
|
|
699
|
+
const openPreview = async () => {
|
|
700
|
+
setPreview(true)
|
|
701
|
+
setSendError(null)
|
|
702
|
+
|
|
703
|
+
if (!onPreviewReply) {
|
|
704
|
+
// No server preview contract: render a sanitized local draft preview only.
|
|
705
|
+
setPreviewState({ loading: false, html: sanitizeHtml(localPreviewHtml), confirmationToken: null, error: null, local: true })
|
|
706
|
+
return
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false })
|
|
710
|
+
try {
|
|
711
|
+
const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll })
|
|
712
|
+
setPreviewState({
|
|
713
|
+
loading: false,
|
|
714
|
+
html: sanitizeHtml(result.htmlBody ?? ""),
|
|
715
|
+
confirmationToken: result.confirmationToken ?? null,
|
|
716
|
+
error: null,
|
|
717
|
+
local: false,
|
|
718
|
+
})
|
|
719
|
+
} catch (error) {
|
|
720
|
+
setPreviewState({
|
|
721
|
+
loading: false,
|
|
722
|
+
html: null,
|
|
723
|
+
confirmationToken: null,
|
|
724
|
+
error: error instanceof Error ? error.message : "Could not load the preview. Please try again.",
|
|
725
|
+
local: false,
|
|
726
|
+
})
|
|
727
|
+
}
|
|
728
|
+
}
|
|
663
729
|
|
|
664
730
|
const handleSend = async () => {
|
|
665
731
|
setSending(true)
|
|
@@ -667,6 +733,7 @@ function ReplyComposer({
|
|
|
667
733
|
try {
|
|
668
734
|
await onSend(body, sig)
|
|
669
735
|
setPreview(false)
|
|
736
|
+
setPreviewState(IDLE_PREVIEW)
|
|
670
737
|
} catch (error) {
|
|
671
738
|
setSendError(error instanceof Error ? error.message : "Could not send this reply. Please try again.")
|
|
672
739
|
} finally {
|
|
@@ -680,6 +747,7 @@ function ReplyComposer({
|
|
|
680
747
|
try {
|
|
681
748
|
await onDraft(body, sig)
|
|
682
749
|
setPreview(false)
|
|
750
|
+
setPreviewState(IDLE_PREVIEW)
|
|
683
751
|
} catch (error) {
|
|
684
752
|
setSendError(error instanceof Error ? error.message : "Could not create the Gmail draft. Please try again.")
|
|
685
753
|
} finally {
|
|
@@ -742,7 +810,7 @@ function ReplyComposer({
|
|
|
742
810
|
onKeyDown={(e) => {
|
|
743
811
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
744
812
|
e.preventDefault()
|
|
745
|
-
|
|
813
|
+
void openPreview()
|
|
746
814
|
}
|
|
747
815
|
}}
|
|
748
816
|
/>
|
|
@@ -760,22 +828,24 @@ function ReplyComposer({
|
|
|
760
828
|
<Switch checked={sig} onCheckedChange={setSig} aria-label="Toggle signature" />
|
|
761
829
|
Signature
|
|
762
830
|
</label>
|
|
763
|
-
<Button type="button" variant="outline" size="sm" disabled={sending} onClick={() =>
|
|
831
|
+
<Button type="button" variant="outline" size="sm" disabled={sending} onClick={() => void openPreview()}>
|
|
764
832
|
<Eye size={14} /> Preview
|
|
765
833
|
</Button>
|
|
766
|
-
<Button type="button" size="sm" disabled={sending} onClick={() =>
|
|
834
|
+
<Button type="button" size="sm" disabled={sending} onClick={() => void openPreview()}>
|
|
767
835
|
<Send size={14} /> Send
|
|
768
836
|
</Button>
|
|
769
837
|
</div>
|
|
770
838
|
|
|
771
|
-
<Dialog open={preview} onOpenChange={(open) => { if (!sending) setPreview(open) }}>
|
|
839
|
+
<Dialog open={preview} onOpenChange={(open) => { if (!sending) { setPreview(open); if (!open) setPreviewState(IDLE_PREVIEW) } }}>
|
|
772
840
|
<DialogContent className="max-w-xl">
|
|
773
841
|
<DialogHeader>
|
|
774
842
|
<DialogTitle className="flex items-center gap-1.5 text-[15px]">
|
|
775
|
-
<Eye size={15} />
|
|
843
|
+
<Eye size={15} /> {previewState.local ? "Local draft preview" : "Reply preview"}
|
|
776
844
|
</DialogTitle>
|
|
777
845
|
<DialogDescription>
|
|
778
|
-
|
|
846
|
+
{previewState.local
|
|
847
|
+
? "Local draft preview only — the server prepares the exact message on send."
|
|
848
|
+
: <>Stays on the {subject.replace(/^Re:\s*/i, "")} thread. Gmail keeps it threaded.</>}
|
|
779
849
|
</DialogDescription>
|
|
780
850
|
</DialogHeader>
|
|
781
851
|
<div className="border-border space-y-1 rounded-md border p-3 text-[13px]">
|
|
@@ -792,7 +862,23 @@ function ReplyComposer({
|
|
|
792
862
|
{subject}
|
|
793
863
|
</div>
|
|
794
864
|
</div>
|
|
795
|
-
|
|
865
|
+
{previewState.loading ? (
|
|
866
|
+
<div data-slot="conv-preview-loading" role="status" className="text-muted-foreground flex items-center gap-2 px-1 py-6 text-[13px]">
|
|
867
|
+
<span className="border-muted-foreground/40 border-t-foreground size-4 animate-spin rounded-full border-2" aria-hidden />
|
|
868
|
+
Loading preview…
|
|
869
|
+
</div>
|
|
870
|
+
) : previewState.error ? (
|
|
871
|
+
<p role="alert" className="text-destructive text-sm">
|
|
872
|
+
{previewState.error}
|
|
873
|
+
</p>
|
|
874
|
+
) : (
|
|
875
|
+
<div
|
|
876
|
+
data-slot="conv-preview-body"
|
|
877
|
+
data-confirmation-token={previewState.confirmationToken ?? undefined}
|
|
878
|
+
className={cn(PROSE, "max-h-72 overflow-auto")}
|
|
879
|
+
dangerouslySetInnerHTML={{ __html: previewState.html ?? "" }}
|
|
880
|
+
/>
|
|
881
|
+
)}
|
|
796
882
|
{sendError ? (
|
|
797
883
|
<p role="alert" className="text-destructive text-sm">
|
|
798
884
|
{sendError}
|
|
@@ -801,20 +887,20 @@ function ReplyComposer({
|
|
|
801
887
|
<DialogFooter className="sm:justify-between">
|
|
802
888
|
<button
|
|
803
889
|
type="button"
|
|
804
|
-
disabled={sending}
|
|
890
|
+
disabled={sending || previewState.loading}
|
|
805
891
|
onClick={handleDraft}
|
|
806
892
|
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50"
|
|
807
893
|
>
|
|
808
894
|
<GmailMark size={14} /> Open draft in Gmail
|
|
809
895
|
</button>
|
|
810
896
|
<span className="flex items-center gap-2">
|
|
811
|
-
<Button type="button" variant="outline" size="sm" disabled={sending} onClick={() => setPreview(false)}>
|
|
897
|
+
<Button type="button" variant="outline" size="sm" disabled={sending} onClick={() => { setPreview(false); setPreviewState(IDLE_PREVIEW) }}>
|
|
812
898
|
Keep editing
|
|
813
899
|
</Button>
|
|
814
900
|
<Button
|
|
815
901
|
type="button"
|
|
816
902
|
size="sm"
|
|
817
|
-
disabled={sending}
|
|
903
|
+
disabled={sending || previewState.loading}
|
|
818
904
|
onClick={handleSend}
|
|
819
905
|
>
|
|
820
906
|
<Send size={14} /> {sending ? "Sending..." : "Send now"}
|
|
@@ -841,6 +927,7 @@ function ThreadBody({
|
|
|
841
927
|
tenantName,
|
|
842
928
|
onSendReply,
|
|
843
929
|
onCreateGmailDraft,
|
|
930
|
+
onPreviewReply,
|
|
844
931
|
onOpenInGmail,
|
|
845
932
|
}: {
|
|
846
933
|
thread: ConversationThread
|
|
@@ -848,6 +935,7 @@ function ThreadBody({
|
|
|
848
935
|
tenantName?: string
|
|
849
936
|
onSendReply?: (p: ConversationReplyPayload) => void | Promise<void>
|
|
850
937
|
onCreateGmailDraft?: (p: ConversationReplyPayload) => void | Promise<void>
|
|
938
|
+
onPreviewReply?: (p: ConversationReplyPayload) => Promise<ConversationReplyPreview>
|
|
851
939
|
onOpenInGmail?: (threadId: string) => void
|
|
852
940
|
}) {
|
|
853
941
|
const canReply = thread.canReply !== false
|
|
@@ -865,7 +953,7 @@ function ThreadBody({
|
|
|
865
953
|
const toggle = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] }))
|
|
866
954
|
|
|
867
955
|
return (
|
|
868
|
-
<div data-slot="conv-thread-body" className="space-y-
|
|
956
|
+
<div data-slot="conv-thread-body" className="space-y-2">
|
|
869
957
|
{canReply && thread.paused ? (
|
|
870
958
|
<div className="border-status-pending-border bg-status-pending-bg text-status-pending-fg flex items-start gap-2 rounded-md border p-2.5 text-[12px]">
|
|
871
959
|
<Pause size={13} className="mt-0.5 shrink-0" />
|
|
@@ -892,7 +980,7 @@ function ThreadBody({
|
|
|
892
980
|
) : null}
|
|
893
981
|
|
|
894
982
|
{canReply && mode === "idle" ? (
|
|
895
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
983
|
+
<div data-slot="conv-action-row" className="border-border/70 mt-1 flex flex-wrap items-center gap-x-3 gap-y-2 border-t pt-3">
|
|
896
984
|
<Button type="button" size="sm" onClick={() => { setReplyAll(false); setMode("replying") }}>
|
|
897
985
|
<Reply size={15} /> Reply
|
|
898
986
|
</Button>
|
|
@@ -904,8 +992,8 @@ function ThreadBody({
|
|
|
904
992
|
<Button type="button" variant="ghost" size="sm" onClick={() => onOpenInGmail?.(thread.threadId)}>
|
|
905
993
|
<GmailMark size={14} /> Open in Gmail
|
|
906
994
|
</Button>
|
|
907
|
-
<span className="text-muted-foreground/70 ml-auto inline-flex items-center gap-1 text-[
|
|
908
|
-
<GitMerge size={
|
|
995
|
+
<span className="text-muted-foreground/70 ml-auto inline-flex items-center gap-1.5 text-[12px]">
|
|
996
|
+
<GitMerge size={13} /> Stays on this thread
|
|
909
997
|
</span>
|
|
910
998
|
</div>
|
|
911
999
|
) : null}
|
|
@@ -916,6 +1004,7 @@ function ThreadBody({
|
|
|
916
1004
|
me={me}
|
|
917
1005
|
replyAll={replyAll}
|
|
918
1006
|
tenantName={tenantName}
|
|
1007
|
+
onPreviewReply={onPreviewReply}
|
|
919
1008
|
onClose={() => setMode("idle")}
|
|
920
1009
|
onSend={async (body, includeSignature) => {
|
|
921
1010
|
await onSendReply?.({ threadId: thread.threadId, body, includeSignature, replyAll })
|
|
@@ -969,12 +1058,13 @@ function ThreadRow({
|
|
|
969
1058
|
tenantName,
|
|
970
1059
|
onSendReply,
|
|
971
1060
|
onCreateGmailDraft,
|
|
1061
|
+
onPreviewReply,
|
|
972
1062
|
onOpenInGmail,
|
|
973
1063
|
}: {
|
|
974
1064
|
thread: ConversationThread
|
|
975
1065
|
open: boolean
|
|
976
1066
|
onToggleOpen: () => void
|
|
977
|
-
} & Pick<ConversationPanelProps, "me" | "tenantName" | "onSendReply" | "onCreateGmailDraft" | "onOpenInGmail">) {
|
|
1067
|
+
} & Pick<ConversationPanelProps, "me" | "tenantName" | "onSendReply" | "onCreateGmailDraft" | "onPreviewReply" | "onOpenInGmail">) {
|
|
978
1068
|
const status = effectiveStatus(thread)
|
|
979
1069
|
const last = thread.messages[thread.messages.length - 1]
|
|
980
1070
|
const who = last?.direction === "inbound" ? firstName(last.from.name) : "You"
|
|
@@ -992,8 +1082,8 @@ function ThreadRow({
|
|
|
992
1082
|
<span className={cn("size-2 shrink-0 rounded-full", STATUS_DOT[status])} aria-hidden />
|
|
993
1083
|
<span className="min-w-0 flex-1">
|
|
994
1084
|
<span className="flex items-center gap-2">
|
|
995
|
-
<span className="truncate text-
|
|
996
|
-
<span className={cn("shrink-0 rounded border px-1.5 text-[10px] font-medium leading-4", pill.cls)}>
|
|
1085
|
+
<span className="truncate text-sm font-semibold">{thread.subject}</span>
|
|
1086
|
+
<span className={cn("shrink-0 rounded-md border px-1.5 py-px text-[10px] font-medium leading-4", pill.cls)}>
|
|
997
1087
|
{pill.label}
|
|
998
1088
|
</span>
|
|
999
1089
|
</span>
|
|
@@ -1017,6 +1107,7 @@ function ThreadRow({
|
|
|
1017
1107
|
tenantName={tenantName}
|
|
1018
1108
|
onSendReply={onSendReply}
|
|
1019
1109
|
onCreateGmailDraft={onCreateGmailDraft}
|
|
1110
|
+
onPreviewReply={onPreviewReply}
|
|
1020
1111
|
onOpenInGmail={onOpenInGmail}
|
|
1021
1112
|
/>
|
|
1022
1113
|
</div>
|
|
@@ -1033,6 +1124,7 @@ function ConversationPanel({
|
|
|
1033
1124
|
tenantName,
|
|
1034
1125
|
onSendReply,
|
|
1035
1126
|
onCreateGmailDraft,
|
|
1127
|
+
onPreviewReply,
|
|
1036
1128
|
onOpenInGmail,
|
|
1037
1129
|
defaultOpenThreadId,
|
|
1038
1130
|
className,
|
|
@@ -1100,8 +1192,8 @@ function ConversationPanel({
|
|
|
1100
1192
|
{badge.label}
|
|
1101
1193
|
</span>
|
|
1102
1194
|
<span className="min-w-0 flex-1">
|
|
1103
|
-
<span className="block truncate text-
|
|
1104
|
-
<span className="text-muted-foreground block truncate text-xs">
|
|
1195
|
+
<span className="block truncate text-sm font-semibold leading-tight">{headTitle}</span>
|
|
1196
|
+
<span className="text-muted-foreground mt-0.5 block truncate text-xs">
|
|
1105
1197
|
{threads.length} {threads.length === 1 ? "thread" : "threads"} on this action
|
|
1106
1198
|
{anyPaused ? <> · <b>playbook stopped</b></> : null}
|
|
1107
1199
|
</span>
|
|
@@ -1125,6 +1217,7 @@ function ConversationPanel({
|
|
|
1125
1217
|
tenantName={tenantName}
|
|
1126
1218
|
onSendReply={onSendReply}
|
|
1127
1219
|
onCreateGmailDraft={onCreateGmailDraft}
|
|
1220
|
+
onPreviewReply={onPreviewReply}
|
|
1128
1221
|
onOpenInGmail={onOpenInGmail}
|
|
1129
1222
|
/>
|
|
1130
1223
|
))}
|
|
@@ -58,7 +58,7 @@ export interface OwnerPerson {
|
|
|
58
58
|
|
|
59
59
|
/* ── shared bits ─────────────────────────────────────────────────────────── */
|
|
60
60
|
|
|
61
|
-
function SalesforceMark({ size =
|
|
61
|
+
function SalesforceMark({ size = 13 }: { size?: number }) {
|
|
62
62
|
return (
|
|
63
63
|
// eslint-disable-next-line @next/next/no-img-element
|
|
64
64
|
<img
|
|
@@ -82,8 +82,10 @@ function OwnerAvatar({ person, size = "sm" }: { person: OwnerPerson; size?: "sm"
|
|
|
82
82
|
)
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Component-owned compact sizing. Set directly here (not via consumer
|
|
86
|
+
// className) because cn()/tailwind-merge can strip conflicting overrides.
|
|
85
87
|
const chipBase =
|
|
86
|
-
"inline-flex h-
|
|
88
|
+
"inline-flex h-7 items-center gap-1 rounded-md border border-border bg-background px-2 text-[12px] " +
|
|
87
89
|
"shadow-[0_1px_1px_rgba(0,0,0,0.03)]"
|
|
88
90
|
|
|
89
91
|
/* ── Signal owner (Handled assignment, editable) ─────────────────────────── */
|
|
@@ -115,14 +117,14 @@ function SignalOwnerChip({
|
|
|
115
117
|
{owner ? (
|
|
116
118
|
<OwnerAvatar person={owner} />
|
|
117
119
|
) : (
|
|
118
|
-
<span className="text-muted-foreground inline-flex size-
|
|
119
|
-
<UserPlus size={
|
|
120
|
+
<span className="text-muted-foreground inline-flex size-4 items-center justify-center">
|
|
121
|
+
<UserPlus size={12} />
|
|
120
122
|
</span>
|
|
121
123
|
)}
|
|
122
|
-
<span className="text-muted-foreground text-[
|
|
124
|
+
<span className="text-muted-foreground text-[10px] font-medium tracking-wide uppercase">
|
|
123
125
|
Signal owner
|
|
124
126
|
</span>
|
|
125
|
-
<span className="bg-border/70 mx-0.5 h-3
|
|
127
|
+
<span className="bg-border/70 mx-0.5 h-3 w-px" aria-hidden />
|
|
126
128
|
<span className={cn("font-medium", owner ? "text-foreground" : "text-muted-foreground")}>
|
|
127
129
|
{owner ? owner.name : "Unassigned"}
|
|
128
130
|
</span>
|
|
@@ -155,7 +157,7 @@ function SignalOwnerChip({
|
|
|
155
157
|
>
|
|
156
158
|
{value}
|
|
157
159
|
<span className="text-muted-foreground ml-0.5">
|
|
158
|
-
{open ? <ChevronUp size={
|
|
160
|
+
{open ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
|
159
161
|
</span>
|
|
160
162
|
</button>
|
|
161
163
|
</DropdownMenuTrigger>
|
|
@@ -289,7 +291,7 @@ function AccountOwnerChip({ owners, className }: AccountOwnerChipProps) {
|
|
|
289
291
|
<OwnerAvatar person={only} />
|
|
290
292
|
<span className="text-foreground font-medium">{only.name}</span>
|
|
291
293
|
<span className="text-muted-foreground ml-0.5">
|
|
292
|
-
{open ? <ChevronUp size={
|
|
294
|
+
{open ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
|
293
295
|
</span>
|
|
294
296
|
</button>
|
|
295
297
|
</DropdownMenuTrigger>
|
|
@@ -327,11 +329,11 @@ function AccountOwnerChip({ owners, className }: AccountOwnerChipProps) {
|
|
|
327
329
|
))}
|
|
328
330
|
</span>
|
|
329
331
|
<span className="text-foreground font-medium">Account owners</span>
|
|
330
|
-
<span className="bg-muted text-muted-foreground rounded px-1 text-[
|
|
332
|
+
<span className="bg-muted text-muted-foreground rounded px-1 text-[10px] font-semibold tabular-nums">
|
|
331
333
|
×{owners.length}
|
|
332
334
|
</span>
|
|
333
335
|
<span className="text-muted-foreground ml-0.5">
|
|
334
|
-
{open ? <ChevronUp size={
|
|
336
|
+
{open ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
|
335
337
|
</span>
|
|
336
338
|
</button>
|
|
337
339
|
</DropdownMenuTrigger>
|
|
@@ -163,21 +163,21 @@ const TIMELINE_VARIANT_CLASSES: Record<TimelineActivityVariant, TimelineVariantC
|
|
|
163
163
|
},
|
|
164
164
|
"case-panel": {
|
|
165
165
|
outerRowGap: "group relative flex gap-3",
|
|
166
|
-
connector: "absolute left-[
|
|
167
|
-
dotWrapperSize: "relative z-10 mt-
|
|
168
|
-
dot: "flex h-
|
|
169
|
-
contentPadding: "flex-1 pb-
|
|
170
|
-
titleRowSpacing: "flex min-w-0
|
|
171
|
-
title: "pr-
|
|
172
|
-
time: "
|
|
173
|
-
cardContainer: "overflow-hidden rounded-lg border border-border/70 bg-card
|
|
174
|
-
cardHeader: "border-b border-border/60 bg-
|
|
175
|
-
cardBody: "px-3 py-2.5 text-
|
|
176
|
-
cardFooter: "border-t border-border/60 bg-
|
|
177
|
-
collapsedPreview: "flex items-center justify-between gap-2 px-3 py-2 text-
|
|
166
|
+
connector: "absolute left-[11px] top-6 bottom-[-2px] w-px bg-border/50",
|
|
167
|
+
dotWrapperSize: "relative z-10 mt-0.5 flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-background",
|
|
168
|
+
dot: "flex h-[22px] w-[22px] items-center justify-center rounded-full border ring-4 ring-background [&>svg]:h-3 [&>svg]:w-3",
|
|
169
|
+
contentPadding: "flex-1 min-w-0 pb-5 pt-px",
|
|
170
|
+
titleRowSpacing: "flex min-w-0 items-start justify-between gap-3",
|
|
171
|
+
title: "min-w-0 pr-1 text-[13.5px] font-medium leading-tight text-foreground",
|
|
172
|
+
time: "shrink-0 whitespace-nowrap pt-px text-[11px] leading-tight text-muted-foreground/60",
|
|
173
|
+
cardContainer: "overflow-hidden rounded-lg border border-border/70 bg-card",
|
|
174
|
+
cardHeader: "flex items-center justify-between border-b border-border/60 bg-muted/30 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70",
|
|
175
|
+
cardBody: "px-3 py-2.5 text-[13px] leading-relaxed",
|
|
176
|
+
cardFooter: "border-t border-border/60 bg-muted/10 px-3 py-1.5",
|
|
177
|
+
collapsedPreview: "flex items-center justify-between gap-2 px-3 py-2 text-[13px] text-muted-foreground",
|
|
178
178
|
actionLinkRow: "flex items-center justify-end gap-2 px-3 py-1.5",
|
|
179
|
-
actionLink: "inline-flex items-center gap-1
|
|
180
|
-
nonInteractiveContent: "pr-2 text-[13px] leading-
|
|
179
|
+
actionLink: "inline-flex items-center gap-1 text-[11px] font-medium text-muted-foreground/70 transition-colors hover:text-foreground",
|
|
180
|
+
nonInteractiveContent: "pr-2 text-[13px] leading-relaxed text-muted-foreground",
|
|
181
181
|
},
|
|
182
182
|
}
|
|
183
183
|
|