@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.
@@ -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-status-pending-bg text-status-pending-fg border-status-pending-border" },
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 previewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : "")
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
- setPreview(true)
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={() => setPreview(true)}>
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={() => setPreview(true)}>
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} /> Preview: this is exactly what sends
843
+ <Eye size={15} /> {previewState.local ? "Local draft preview" : "Reply preview"}
776
844
  </DialogTitle>
777
845
  <DialogDescription>
778
- Stays on the {subject.replace(/^Re:\s*/i, "")} thread. Gmail keeps it threaded.
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
- <div className={cn(PROSE, "max-h-72 overflow-auto")} dangerouslySetInnerHTML={{ __html: previewHtml }} />
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-1.5">
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-[11px]">
908
- <GitMerge size={12} /> Stays on this thread
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-[13px] font-semibold">{thread.subject}</span>
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-[13px] font-semibold">{headTitle}</span>
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 = 14 }: { size?: number }) {
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-8 items-center gap-1.5 rounded-lg border border-border bg-background px-2.5 text-[13px] " +
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-[18px] items-center justify-center">
119
- <UserPlus size={13} />
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-[11px] font-medium tracking-wide uppercase">
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.5 w-px" aria-hidden />
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={13} /> : <ChevronDown size={13} />}
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={13} /> : <ChevronDown size={13} />}
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-[11px] font-semibold tabular-nums">
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={13} /> : <ChevronDown size={13} />}
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-[7px] top-[18px] bottom-[-4px] w-px bg-border/60",
167
- dotWrapperSize: "relative z-10 mt-1 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-background",
168
- dot: "flex h-3.5 w-3.5 items-center justify-center rounded-full border ring-[3px] ring-background",
169
- contentPadding: "flex-1 pb-4 pt-0.5",
170
- titleRowSpacing: "flex min-w-0 flex-col gap-0.5 sm:flex-row sm:items-start sm:justify-between",
171
- title: "pr-3 text-[13px] leading-snug text-foreground",
172
- time: "mt-0.5 shrink-0 whitespace-nowrap text-[11px] leading-snug text-muted-foreground/70",
173
- cardContainer: "overflow-hidden rounded-lg border border-border/70 bg-card shadow-sm",
174
- cardHeader: "border-b border-border/60 bg-background px-3 py-2",
175
- cardBody: "px-3 py-2.5 text-sm",
176
- cardFooter: "border-t border-border/60 bg-background/50 px-3 py-1.5",
177
- collapsedPreview: "flex items-center justify-between gap-2 px-3 py-2 text-sm text-muted-foreground",
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.5 text-[11px] font-medium text-muted-foreground/70 transition-colors hover:text-foreground",
180
- nonInteractiveContent: "pr-2 text-[13px] leading-snug text-muted-foreground",
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