@handled-ai/design-system 0.20.26 → 0.20.28

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.
@@ -126,7 +126,7 @@ export interface ConvMessage {
126
126
  export type ConvStatus = "responded" | "awaiting" | "viewing" | "draft"
127
127
 
128
128
  /**
129
- * A reply template offered in the in-composer "Apply template" picker (WIT-970).
129
+ * A follow-up template offered in the in-composer "Templates" picker (WIT-970).
130
130
  * Presentational: the consumer fills variables for THIS thread's recipient
131
131
  * before passing it in, so `body` is composer-ready text. `tags` (e.g. "Reply")
132
132
  * are shown as chips so reps can recognize the right template at a glance.
@@ -166,7 +166,7 @@ export interface ConversationThread {
166
166
  /** Signature text appended to replies (plain text). */
167
167
  signature?: string
168
168
  /**
169
- * Reply templates offered in the composer's "Apply template" picker, already
169
+ * Follow-up templates offered in the composer's "Templates" picker, already
170
170
  * personalized for this thread's recipient (WIT-970). Omit/empty hides the
171
171
  * picker.
172
172
  */
@@ -561,6 +561,14 @@ type PreviewState = {
561
561
 
562
562
  const IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }
563
563
 
564
+ /**
565
+ * Matches an author-declared manual-fill placeholder ({{personalize}},
566
+ * {{personalization_recent_news}}, …). These have no resolver and must be
567
+ * replaced by the rep before sending; the composer blocks send while any remain.
568
+ * Non-global so repeated `.test()` calls during render are stateless.
569
+ */
570
+ const MANUAL_FILL_TOKEN_RE = /\{\{\s*personaliz(?:e|ation)(?:_[a-z0-9]+)*\s*\}\}/i
571
+
564
572
  function ReplyComposer({
565
573
  thread,
566
574
  me,
@@ -588,6 +596,10 @@ function ReplyComposer({
588
596
  const [appliedTemplate, setAppliedTemplate] = React.useState<string | null>(null)
589
597
  const [sig, setSig] = React.useState(true)
590
598
  const replyTemplates = thread.replyTemplates ?? []
599
+ // Manual-fill placeholders ({{personalize…}}) are author-required: the rep
600
+ // must replace them before sending. While any remain in the body we block
601
+ // send/preview so the personalization is never sent as a literal token.
602
+ const hasManualFillToken = MANUAL_FILL_TOKEN_RE.test(body)
591
603
 
592
604
  const applyTemplate = (template: ConversationReplyTemplate) => {
593
605
  setBody(template.body)
@@ -719,7 +731,7 @@ function ReplyComposer({
719
731
  onKeyDown={(e) => {
720
732
  if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
721
733
  e.preventDefault()
722
- void openPreview()
734
+ if (!hasManualFillToken) void openPreview()
723
735
  }
724
736
  }}
725
737
  />
@@ -736,11 +748,11 @@ function ReplyComposer({
736
748
  <DropdownMenu>
737
749
  <DropdownMenuTrigger asChild>
738
750
  <Button type="button" variant="outline" size="sm" disabled={sending} data-slot="conv-reply-template-trigger">
739
- <FileText size={14} /> Apply template
751
+ <FileText size={14} /> Templates
740
752
  </Button>
741
753
  </DropdownMenuTrigger>
742
754
  <DropdownMenuContent align="start" className="max-w-xs">
743
- <DropdownMenuLabel>Reply templates</DropdownMenuLabel>
755
+ <DropdownMenuLabel>Templates</DropdownMenuLabel>
744
756
  <DropdownMenuSeparator />
745
757
  {replyTemplates.map((template) => (
746
758
  <DropdownMenuItem
@@ -773,16 +785,33 @@ function ReplyComposer({
773
785
  </div>
774
786
  ) : null}
775
787
 
788
+ {hasManualFillToken ? (
789
+ <p data-slot="conv-reply-manual-fill" role="alert" className="text-destructive mt-2 text-[12px]">
790
+ Fill in the personalize placeholder before sending. Replace each {"{{personalize}}"} with a real, account-specific line.
791
+ </p>
792
+ ) : null}
793
+
776
794
  <div className="mt-2 flex flex-wrap items-center gap-2">
777
795
  <RichTextToolbar />
778
796
  <label className="text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]">
779
797
  <Switch checked={sig} onCheckedChange={setSig} aria-label="Toggle signature" />
780
798
  Signature
781
799
  </label>
782
- <Button type="button" variant="outline" size="sm" disabled={sending} onClick={() => void openPreview()}>
800
+ <Button
801
+ type="button"
802
+ variant="outline"
803
+ size="sm"
804
+ disabled={sending || hasManualFillToken}
805
+ onClick={() => void openPreview()}
806
+ >
783
807
  <Eye size={14} /> Preview
784
808
  </Button>
785
- <Button type="button" size="sm" disabled={sending} onClick={() => void openPreview()}>
809
+ <Button
810
+ type="button"
811
+ size="sm"
812
+ disabled={sending || hasManualFillToken}
813
+ onClick={() => void openPreview()}
814
+ >
786
815
  <Send size={14} /> Send
787
816
  </Button>
788
817
  </div>
@@ -795,7 +824,7 @@ function ReplyComposer({
795
824
  </DialogTitle>
796
825
  <DialogDescription>
797
826
  {previewState.local
798
- ? "Local draft preview only the server prepares the exact message on send."
827
+ ? "Local draft preview only. The server prepares the exact message on send."
799
828
  : <>Stays on the {subject.replace(/^Re:\s*/i, "")} thread. Gmail keeps it threaded.</>}
800
829
  </DialogDescription>
801
830
  </DialogHeader>
@@ -855,7 +884,7 @@ function ReplyComposer({
855
884
  <Button
856
885
  type="button"
857
886
  size="sm"
858
- disabled={sending || previewState.loading}
887
+ disabled={sending || previewState.loading || hasManualFillToken}
859
888
  onClick={handleSend}
860
889
  >
861
890
  <Send size={14} /> {sending ? "Sending..." : "Send now"}
@@ -35,6 +35,7 @@ import { AccountContactsPopover, BrandIcon } from "./account-contacts-popover"
35
35
 
36
36
  export interface SuggestedActionsIconMap {
37
37
  gmail?: string
38
+ outlook?: string
38
39
  slack?: string
39
40
  zendesk?: string
40
41
  salesforce?: string
@@ -43,9 +44,11 @@ export interface SuggestedActionsIconMap {
43
44
  function getActionTypeIcon(type: string, className?: string, iconMap?: SuggestedActionsIconMap) {
44
45
  switch (type) {
45
46
  case "email":
46
- return iconMap?.gmail
47
- ? <BrandIcon src={iconMap.gmail} alt="Gmail" className={className} />
48
- : <Mail className={className} />
47
+ return iconMap?.outlook
48
+ ? <BrandIcon src={iconMap.outlook} alt="Outlook" className={className} />
49
+ : iconMap?.gmail
50
+ ? <BrandIcon src={iconMap.gmail} alt="Gmail" className={className} />
51
+ : <Mail className={className} />
49
52
  case "slack":
50
53
  return iconMap?.slack
51
54
  ? <BrandIcon src={iconMap.slack} alt="Slack" className={className} />