@handled-ai/design-system 0.18.52 → 0.19.0-rc.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.
Files changed (47) hide show
  1. package/dist/components/badge.d.ts +1 -1
  2. package/dist/components/button.d.ts +1 -1
  3. package/dist/components/case-panel-activity-timeline.d.ts +2 -0
  4. package/dist/components/case-panel-activity-timeline.js +22 -1
  5. package/dist/components/case-panel-activity-timeline.js.map +1 -1
  6. package/dist/components/comment-composer.d.ts +29 -0
  7. package/dist/components/comment-composer.js +102 -0
  8. package/dist/components/comment-composer.js.map +1 -0
  9. package/dist/components/conversation-panel.d.ts +95 -0
  10. package/dist/components/conversation-panel.js +636 -0
  11. package/dist/components/conversation-panel.js.map +1 -0
  12. package/dist/components/data-table-filter.d.ts +18 -1
  13. package/dist/components/data-table-filter.js +20 -6
  14. package/dist/components/data-table-filter.js.map +1 -1
  15. package/dist/components/entity-panel.js +5 -5
  16. package/dist/components/entity-panel.js.map +1 -1
  17. package/dist/components/owner-chips.d.ts +59 -0
  18. package/dist/components/owner-chips.js +256 -0
  19. package/dist/components/owner-chips.js.map +1 -0
  20. package/dist/components/pill.d.ts +1 -1
  21. package/dist/components/tabs.d.ts +1 -1
  22. package/dist/components/timeline-activity.d.ts +7 -0
  23. package/dist/components/timeline-activity.js +22 -1
  24. package/dist/components/timeline-activity.js.map +1 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +3 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/internal/safe-html.d.ts +11 -0
  29. package/dist/internal/safe-html.js +222 -0
  30. package/dist/internal/safe-html.js.map +1 -0
  31. package/package.json +1 -1
  32. package/src/components/__tests__/comment-composer.test.tsx +57 -0
  33. package/src/components/__tests__/conversation-panel.test.tsx +157 -0
  34. package/src/components/__tests__/data-table-filter.test.tsx +72 -0
  35. package/src/components/__tests__/entity-metadata-grid.test.tsx +27 -1
  36. package/src/components/__tests__/owner-chips.test.tsx +100 -0
  37. package/src/components/__tests__/timeline-activity.test.tsx +55 -0
  38. package/src/components/case-panel-activity-timeline.tsx +20 -0
  39. package/src/components/comment-composer.tsx +119 -0
  40. package/src/components/conversation-panel.tsx +790 -0
  41. package/src/components/data-table-filter.tsx +53 -10
  42. package/src/components/entity-panel.tsx +7 -5
  43. package/src/components/owner-chips.tsx +335 -0
  44. package/src/components/timeline-activity.tsx +37 -3
  45. package/src/index.ts +3 -0
  46. package/src/internal/__tests__/safe-html.test.ts +53 -0
  47. package/src/internal/safe-html.ts +284 -0
@@ -5,7 +5,7 @@ import { Tabs as Tabs$1 } from 'radix-ui';
5
5
 
6
6
  declare function Tabs({ className, orientation, ...props }: React.ComponentProps<typeof Tabs$1.Root>): React.JSX.Element;
7
7
  declare const tabsListVariants: (props?: ({
8
- variant?: "line" | "default" | null | undefined;
8
+ variant?: "default" | "line" | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
10
10
  declare function TabsList({ className, variant, ...props }: React.ComponentProps<typeof Tabs$1.List> & VariantProps<typeof tabsListVariants>): React.JSX.Element;
11
11
  declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof Tabs$1.Trigger>): React.JSX.Element;
@@ -23,6 +23,13 @@ interface TimelineEvent {
23
23
  date?: string;
24
24
  subject?: string;
25
25
  body: React.ReactNode;
26
+ /**
27
+ * HTML body. When provided, the card renders formatted, Gmail-like HTML
28
+ * instead of the plain-text `body`. The component sanitizes it before
29
+ * rendering. Opt-in: when absent, the plain-text `body` path is used and
30
+ * existing consumers are unaffected.
31
+ */
32
+ bodyHtml?: string;
26
33
  };
27
34
  content?: React.ReactNode;
28
35
  source?: {
@@ -4,7 +4,17 @@
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import * as React from "react";
6
6
  import { cn } from "../lib/utils.js";
7
+ import { sanitizeHtml } from "../internal/safe-html.js";
7
8
  import { ChevronDown, ChevronUp, ExternalLink } from "lucide-react";
9
+ const EMAIL_HTML_CLASS = cn(
10
+ "text-sm leading-[1.62] text-foreground/90 break-words",
11
+ "[&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0",
12
+ "[&_a]:text-[#1a73e8] [&_a]:underline-offset-2 hover:[&_a]:underline",
13
+ "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5",
14
+ "[&_img]:max-w-full [&_img]:h-auto",
15
+ "[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground [&_blockquote]:text-[13px]",
16
+ "[&_.gmail_quote]:border-l-2 [&_.gmail_quote]:border-border [&_.gmail_quote]:pl-3 [&_.gmail_quote]:text-muted-foreground [&_.gmail_quote]:text-[13px]"
17
+ );
8
18
  const TONE_CLASSES = {
9
19
  red: {
10
20
  dot: "bg-red-50 border-red-200 dark:bg-red-950/30 dark:border-red-900/40",
@@ -143,7 +153,18 @@ function TimelineItem({ event, isLast }) {
143
153
  )
144
154
  ] })
145
155
  ] }),
146
- /* @__PURE__ */ jsx("div", { className: "whitespace-pre-line text-sm leading-relaxed text-foreground/90", children: event.email.body }),
156
+ event.email.bodyHtml ? (
157
+ // Gmail reading-pane typography; quoted history
158
+ // (blockquote.gmail_quote) is de-emphasized with a left rule.
159
+ /* @__PURE__ */ jsx(
160
+ "div",
161
+ {
162
+ "data-slot": "timeline-email-html",
163
+ className: EMAIL_HTML_CLASS,
164
+ dangerouslySetInnerHTML: { __html: sanitizeHtml(event.email.bodyHtml) }
165
+ }
166
+ )
167
+ ) : /* @__PURE__ */ jsx("div", { className: "whitespace-pre-line text-sm leading-relaxed text-foreground/90", children: event.email.body }),
147
168
  /* @__PURE__ */ jsxs(
148
169
  "button",
149
170
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/timeline-activity.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../lib/utils\"\nimport { ChevronDown, ChevronUp, ExternalLink } from \"lucide-react\"\n\nexport type TimelineEventTone =\n | \"red\"\n | \"amber\"\n | \"emerald\"\n | \"violet\"\n | \"blue\"\n | \"slate\"\n | \"salesforce\"\n | \"gmail\"\n\nexport interface TimelineEventActor {\n kind: \"user\" | \"integration\" | \"system\"\n name?: string\n initials?: string\n avatarUrl?: string\n verb?: string\n}\n\nexport interface TimelineEvent {\n id: string\n icon: React.ReactNode\n title: React.ReactNode\n time: string\n preview?: React.ReactNode\n email?: {\n from: string\n fromEmail?: string\n to: string\n cc?: string\n bcc?: string\n date?: string\n subject?: string\n body: React.ReactNode\n }\n content?: React.ReactNode\n source?: {\n label: string\n url: string\n }\n defaultExpanded?: boolean\n isInteractive?: boolean\n onSourceClick?: () => void\n tone?: TimelineEventTone\n actor?: TimelineEventActor\n isSystemNoise?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Tone class map — every class is a complete static string literal so\n// Tailwind's JIT scanner can detect them. NO interpolation.\n// ---------------------------------------------------------------------------\n\nexport const TONE_CLASSES: Record<\n TimelineEventTone,\n { dot: string; icon: string }\n> = {\n red: {\n dot: \"bg-red-50 border-red-200 dark:bg-red-950/30 dark:border-red-900/40\",\n icon: \"text-red-600 dark:text-red-300\",\n },\n amber: {\n dot: \"bg-amber-50 border-amber-200 dark:bg-amber-950/30 dark:border-amber-900/40\",\n icon: \"text-amber-600 dark:text-amber-300\",\n },\n emerald: {\n dot: \"bg-emerald-50 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900/40\",\n icon: \"text-emerald-600 dark:text-emerald-300\",\n },\n violet: {\n dot: \"bg-violet-50 border-violet-200 dark:bg-violet-950/30 dark:border-violet-900/40\",\n icon: \"text-violet-600 dark:text-violet-300\",\n },\n blue: {\n dot: \"bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-900/40\",\n icon: \"text-blue-600 dark:text-blue-300\",\n },\n slate: {\n dot: \"bg-slate-100 border-slate-200 dark:bg-slate-800/50 dark:border-slate-700\",\n icon: \"text-slate-500 dark:text-slate-300\",\n },\n salesforce: {\n dot: \"bg-white border-[#00A1E0]/25 dark:bg-background dark:border-[#00A1E0]/25\",\n icon: \"text-[#00A1E0]\",\n },\n gmail: {\n dot: \"bg-white border-red-200 dark:bg-background dark:border-red-900/40\",\n icon: \"text-red-500 dark:text-red-300\",\n },\n}\n\nconst NEUTRAL_DOT_CLASSES = \"border-border/60 bg-background\"\nconst NEUTRAL_ICON_CLASSES = \"text-muted-foreground\"\n\nexport interface TimelineActivityProps {\n events: TimelineEvent[]\n className?: string\n}\n\nexport function TimelineActivity({ events, className }: TimelineActivityProps) {\n return (\n <div className={cn(\"space-y-0\", className)}>\n {events.map((event, index) => (\n <TimelineItem\n key={event.id}\n event={event}\n isLast={index === events.length - 1}\n />\n ))}\n </div>\n )\n}\n\nfunction ActorByline({ actor, time }: { actor: TimelineEventActor; time: string }) {\n if (actor.kind === \"system\") return null\n\n if (actor.kind === \"integration\") {\n return (\n <div className=\"mt-1 flex items-center gap-1.5 text-xs text-muted-foreground\" data-testid=\"actor-byline\">\n <span>Integration</span>\n <span className=\"text-muted-foreground/40\">&middot;</span>\n <span>{time}</span>\n </div>\n )\n }\n\n // actor.kind === \"user\"\n const verb = actor.verb ?? \"performed this action\"\n const displayInitials = actor.initials ?? (actor.name ? actor.name.charAt(0).toUpperCase() : \"?\")\n\n return (\n <div className=\"mt-1 flex items-center gap-1.5 text-xs text-muted-foreground\" data-testid=\"actor-byline\">\n {actor.avatarUrl ? (\n <img\n src={actor.avatarUrl}\n alt={actor.name ?? \"User\"}\n className=\"h-4 w-4 rounded-full object-cover\"\n />\n ) : (\n <span className=\"flex h-4 w-4 items-center justify-center rounded-full bg-muted-foreground/10 text-[8px] font-semibold text-muted-foreground\">\n {displayInitials}\n </span>\n )}\n {actor.name && <span className=\"text-foreground font-medium\">{actor.name}</span>}\n <span>{verb}</span>\n <span className=\"text-muted-foreground/40\">&middot;</span>\n <span>{time}</span>\n </div>\n )\n}\n\nfunction TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean }) {\n const [expanded, setExpanded] = React.useState(event.defaultExpanded ?? false)\n const [showAllRecipients, setShowAllRecipients] = React.useState(false)\n const hasContent = !!event.content\n const hasEmail = !!event.email\n\n const toneStyle = event.tone ? TONE_CLASSES[event.tone] : null\n const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES\n const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES\n\n return (\n <div className=\"group relative flex gap-3.5\">\n {!isLast && (\n <div className=\"absolute left-[9px] top-5 bottom-[-6px] w-px bg-border/60\" />\n )}\n\n <div className=\"relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-background\">\n <div className={cn(\"flex h-4.5 w-4.5 items-center justify-center rounded-full border ring-4 ring-background\", dotClasses, iconClasses)} data-testid=\"timeline-dot\">\n {event.icon}\n </div>\n </div>\n\n <div className=\"flex-1 pb-5 pt-0.5\">\n <div className=\"flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"pr-4 text-[13px] leading-relaxed text-foreground\">\n {event.title}\n </div>\n <span className=\"mt-0.5 shrink-0 whitespace-nowrap text-[11px] text-muted-foreground/70\">\n {event.time}\n </span>\n </div>\n\n {event.actor && <ActorByline actor={event.actor} time={event.time} />}\n\n {(hasContent || hasEmail) && (\n <div className=\"mt-2\">\n {event.isInteractive ? (\n hasEmail ? (\n <div className=\"overflow-hidden rounded-md border border-border/80 bg-muted/20\">\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\"\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded && event.email ? (\n <div className=\"space-y-3\">\n <div>\n <div className=\"flex items-center justify-between gap-4\">\n <div className=\"flex min-w-0 items-baseline gap-1.5\">\n <span className=\"font-semibold text-foreground text-[13px] whitespace-nowrap\">{event.email.from}</span>\n {event.email.fromEmail && (\n <span className=\"text-muted-foreground/60 text-xs truncate\">{event.email.fromEmail}</span>\n )}\n </div>\n {event.email.date && (\n <span className=\"shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap\">{event.email.date}</span>\n )}\n </div>\n <div className=\"mt-0.5 flex items-center gap-1 text-xs text-muted-foreground\">\n <span className=\"truncate\">\n To {event.email.to}\n {!showAllRecipients && (event.email.cc || event.email.bcc) ? (\n <>, ...</>\n ) : null}\n {showAllRecipients && event.email.cc ? (\n <>, {event.email.cc}</>\n ) : null}\n {showAllRecipients && event.email.bcc ? (\n <> <span className=\"text-muted-foreground/40\">bcc</span> {event.email.bcc}</>\n ) : null}\n </span>\n {(event.email.cc || event.email.bcc) && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setShowAllRecipients((prev) => !prev)\n }}\n className=\"shrink-0 text-muted-foreground/40 hover:text-muted-foreground transition-colors\"\n >\n <ChevronDown className={cn(\"h-3 w-3 transition-transform\", showAllRecipients && \"rotate-180\")} />\n </button>\n )}\n </div>\n </div>\n\n <div className=\"whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {event.email.body}\n </div>\n\n <button\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"mt-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n Show less <ChevronUp className=\"h-3 w-3\" />\n </button>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 text-muted-foreground\">\n <span className=\"line-clamp-1 pr-3 text-[13px]\">\n <span className=\"text-muted-foreground\">{event.email?.from}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span>\n {event.email?.subject ? (\n <>\n <span className=\"text-muted-foreground\">{event.email.subject}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span>\n </>\n ) : null}\n <span className=\"text-muted-foreground\">{event.preview}</span>\n </span>\n <button className=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\">\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )}\n </div>\n </div>\n ) : (\n <div className=\"overflow-hidden rounded-md border border-border/80 bg-muted/20\">\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\"\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded ? (\n <div className=\"space-y-2\">\n {event.content}\n <div className=\"mt-2 flex items-center gap-3\">\n {event.source ? (\n event.onSourceClick ? (\n <button\n type=\"button\"\n onClick={(e) => { e.stopPropagation(); event.onSourceClick?.(); }}\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n Open in {event.source.label}\n <ExternalLink className=\"h-3 w-3\" />\n </button>\n ) : (\n <a\n href={event.source.url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n Open in {event.source.label}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n )\n ) : null}\n <button\n onClick={(e) => { e.stopPropagation(); setExpanded(false); }}\n className=\"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n Show less <ChevronUp className=\"h-3 w-3\" />\n </button>\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 text-muted-foreground\">\n <span className=\"line-clamp-1 pr-3\">\n {event.preview ?? event.content}\n </span>\n <button className=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\">\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )}\n </div>\n </div>\n )\n ) : (\n <div className=\"pr-2 text-sm leading-relaxed text-muted-foreground\">\n {event.content}\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":["_a"]}
1
+ {"version":3,"sources":["../../src/components/timeline-activity.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../lib/utils\"\nimport { sanitizeHtml } from \"../internal/safe-html\"\nimport { ChevronDown, ChevronUp, ExternalLink } from \"lucide-react\"\n\n/**\n * Gmail-like reading-pane typography for rendered, sanitized email HTML.\n * Self-contained Tailwind child-variant utilities — no global CSS. Links use\n * Gmail blue; quoted history (`blockquote.gmail_quote`) is de-emphasized with a\n * left rule and muted, slightly smaller text.\n */\nconst EMAIL_HTML_CLASS = cn(\n \"text-sm leading-[1.62] text-foreground/90 break-words\",\n \"[&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0\",\n \"[&_a]:text-[#1a73e8] [&_a]:underline-offset-2 hover:[&_a]:underline\",\n \"[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5\",\n \"[&_img]:max-w-full [&_img]:h-auto\",\n \"[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground [&_blockquote]:text-[13px]\",\n \"[&_.gmail_quote]:border-l-2 [&_.gmail_quote]:border-border [&_.gmail_quote]:pl-3 [&_.gmail_quote]:text-muted-foreground [&_.gmail_quote]:text-[13px]\"\n)\n\nexport type TimelineEventTone =\n | \"red\"\n | \"amber\"\n | \"emerald\"\n | \"violet\"\n | \"blue\"\n | \"slate\"\n | \"salesforce\"\n | \"gmail\"\n\nexport interface TimelineEventActor {\n kind: \"user\" | \"integration\" | \"system\"\n name?: string\n initials?: string\n avatarUrl?: string\n verb?: string\n}\n\nexport interface TimelineEvent {\n id: string\n icon: React.ReactNode\n title: React.ReactNode\n time: string\n preview?: React.ReactNode\n email?: {\n from: string\n fromEmail?: string\n to: string\n cc?: string\n bcc?: string\n date?: string\n subject?: string\n body: React.ReactNode\n /**\n * HTML body. When provided, the card renders formatted, Gmail-like HTML\n * instead of the plain-text `body`. The component sanitizes it before\n * rendering. Opt-in: when absent, the plain-text `body` path is used and\n * existing consumers are unaffected.\n */\n bodyHtml?: string\n }\n content?: React.ReactNode\n source?: {\n label: string\n url: string\n }\n defaultExpanded?: boolean\n isInteractive?: boolean\n onSourceClick?: () => void\n tone?: TimelineEventTone\n actor?: TimelineEventActor\n isSystemNoise?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Tone class map — every class is a complete static string literal so\n// Tailwind's JIT scanner can detect them. NO interpolation.\n// ---------------------------------------------------------------------------\n\nexport const TONE_CLASSES: Record<\n TimelineEventTone,\n { dot: string; icon: string }\n> = {\n red: {\n dot: \"bg-red-50 border-red-200 dark:bg-red-950/30 dark:border-red-900/40\",\n icon: \"text-red-600 dark:text-red-300\",\n },\n amber: {\n dot: \"bg-amber-50 border-amber-200 dark:bg-amber-950/30 dark:border-amber-900/40\",\n icon: \"text-amber-600 dark:text-amber-300\",\n },\n emerald: {\n dot: \"bg-emerald-50 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900/40\",\n icon: \"text-emerald-600 dark:text-emerald-300\",\n },\n violet: {\n dot: \"bg-violet-50 border-violet-200 dark:bg-violet-950/30 dark:border-violet-900/40\",\n icon: \"text-violet-600 dark:text-violet-300\",\n },\n blue: {\n dot: \"bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-900/40\",\n icon: \"text-blue-600 dark:text-blue-300\",\n },\n slate: {\n dot: \"bg-slate-100 border-slate-200 dark:bg-slate-800/50 dark:border-slate-700\",\n icon: \"text-slate-500 dark:text-slate-300\",\n },\n salesforce: {\n dot: \"bg-white border-[#00A1E0]/25 dark:bg-background dark:border-[#00A1E0]/25\",\n icon: \"text-[#00A1E0]\",\n },\n gmail: {\n dot: \"bg-white border-red-200 dark:bg-background dark:border-red-900/40\",\n icon: \"text-red-500 dark:text-red-300\",\n },\n}\n\nconst NEUTRAL_DOT_CLASSES = \"border-border/60 bg-background\"\nconst NEUTRAL_ICON_CLASSES = \"text-muted-foreground\"\n\nexport interface TimelineActivityProps {\n events: TimelineEvent[]\n className?: string\n}\n\nexport function TimelineActivity({ events, className }: TimelineActivityProps) {\n return (\n <div className={cn(\"space-y-0\", className)}>\n {events.map((event, index) => (\n <TimelineItem\n key={event.id}\n event={event}\n isLast={index === events.length - 1}\n />\n ))}\n </div>\n )\n}\n\nfunction ActorByline({ actor, time }: { actor: TimelineEventActor; time: string }) {\n if (actor.kind === \"system\") return null\n\n if (actor.kind === \"integration\") {\n return (\n <div className=\"mt-1 flex items-center gap-1.5 text-xs text-muted-foreground\" data-testid=\"actor-byline\">\n <span>Integration</span>\n <span className=\"text-muted-foreground/40\">&middot;</span>\n <span>{time}</span>\n </div>\n )\n }\n\n // actor.kind === \"user\"\n const verb = actor.verb ?? \"performed this action\"\n const displayInitials = actor.initials ?? (actor.name ? actor.name.charAt(0).toUpperCase() : \"?\")\n\n return (\n <div className=\"mt-1 flex items-center gap-1.5 text-xs text-muted-foreground\" data-testid=\"actor-byline\">\n {actor.avatarUrl ? (\n <img\n src={actor.avatarUrl}\n alt={actor.name ?? \"User\"}\n className=\"h-4 w-4 rounded-full object-cover\"\n />\n ) : (\n <span className=\"flex h-4 w-4 items-center justify-center rounded-full bg-muted-foreground/10 text-[8px] font-semibold text-muted-foreground\">\n {displayInitials}\n </span>\n )}\n {actor.name && <span className=\"text-foreground font-medium\">{actor.name}</span>}\n <span>{verb}</span>\n <span className=\"text-muted-foreground/40\">&middot;</span>\n <span>{time}</span>\n </div>\n )\n}\n\nfunction TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean }) {\n const [expanded, setExpanded] = React.useState(event.defaultExpanded ?? false)\n const [showAllRecipients, setShowAllRecipients] = React.useState(false)\n const hasContent = !!event.content\n const hasEmail = !!event.email\n\n const toneStyle = event.tone ? TONE_CLASSES[event.tone] : null\n const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES\n const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES\n\n return (\n <div className=\"group relative flex gap-3.5\">\n {!isLast && (\n <div className=\"absolute left-[9px] top-5 bottom-[-6px] w-px bg-border/60\" />\n )}\n\n <div className=\"relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-background\">\n <div className={cn(\"flex h-4.5 w-4.5 items-center justify-center rounded-full border ring-4 ring-background\", dotClasses, iconClasses)} data-testid=\"timeline-dot\">\n {event.icon}\n </div>\n </div>\n\n <div className=\"flex-1 pb-5 pt-0.5\">\n <div className=\"flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"pr-4 text-[13px] leading-relaxed text-foreground\">\n {event.title}\n </div>\n <span className=\"mt-0.5 shrink-0 whitespace-nowrap text-[11px] text-muted-foreground/70\">\n {event.time}\n </span>\n </div>\n\n {event.actor && <ActorByline actor={event.actor} time={event.time} />}\n\n {(hasContent || hasEmail) && (\n <div className=\"mt-2\">\n {event.isInteractive ? (\n hasEmail ? (\n <div className=\"overflow-hidden rounded-md border border-border/80 bg-muted/20\">\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\"\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded && event.email ? (\n <div className=\"space-y-3\">\n <div>\n <div className=\"flex items-center justify-between gap-4\">\n <div className=\"flex min-w-0 items-baseline gap-1.5\">\n <span className=\"font-semibold text-foreground text-[13px] whitespace-nowrap\">{event.email.from}</span>\n {event.email.fromEmail && (\n <span className=\"text-muted-foreground/60 text-xs truncate\">{event.email.fromEmail}</span>\n )}\n </div>\n {event.email.date && (\n <span className=\"shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap\">{event.email.date}</span>\n )}\n </div>\n <div className=\"mt-0.5 flex items-center gap-1 text-xs text-muted-foreground\">\n <span className=\"truncate\">\n To {event.email.to}\n {!showAllRecipients && (event.email.cc || event.email.bcc) ? (\n <>, ...</>\n ) : null}\n {showAllRecipients && event.email.cc ? (\n <>, {event.email.cc}</>\n ) : null}\n {showAllRecipients && event.email.bcc ? (\n <> <span className=\"text-muted-foreground/40\">bcc</span> {event.email.bcc}</>\n ) : null}\n </span>\n {(event.email.cc || event.email.bcc) && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setShowAllRecipients((prev) => !prev)\n }}\n className=\"shrink-0 text-muted-foreground/40 hover:text-muted-foreground transition-colors\"\n >\n <ChevronDown className={cn(\"h-3 w-3 transition-transform\", showAllRecipients && \"rotate-180\")} />\n </button>\n )}\n </div>\n </div>\n\n {event.email.bodyHtml ? (\n // Gmail reading-pane typography; quoted history\n // (blockquote.gmail_quote) is de-emphasized with a left rule.\n <div\n data-slot=\"timeline-email-html\"\n className={EMAIL_HTML_CLASS}\n dangerouslySetInnerHTML={{ __html: sanitizeHtml(event.email.bodyHtml) }}\n />\n ) : (\n <div className=\"whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {event.email.body}\n </div>\n )}\n\n <button\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"mt-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n Show less <ChevronUp className=\"h-3 w-3\" />\n </button>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 text-muted-foreground\">\n <span className=\"line-clamp-1 pr-3 text-[13px]\">\n <span className=\"text-muted-foreground\">{event.email?.from}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span>\n {event.email?.subject ? (\n <>\n <span className=\"text-muted-foreground\">{event.email.subject}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span>\n </>\n ) : null}\n <span className=\"text-muted-foreground\">{event.preview}</span>\n </span>\n <button className=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\">\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )}\n </div>\n </div>\n ) : (\n <div className=\"overflow-hidden rounded-md border border-border/80 bg-muted/20\">\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\"\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded ? (\n <div className=\"space-y-2\">\n {event.content}\n <div className=\"mt-2 flex items-center gap-3\">\n {event.source ? (\n event.onSourceClick ? (\n <button\n type=\"button\"\n onClick={(e) => { e.stopPropagation(); event.onSourceClick?.(); }}\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n Open in {event.source.label}\n <ExternalLink className=\"h-3 w-3\" />\n </button>\n ) : (\n <a\n href={event.source.url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n Open in {event.source.label}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n )\n ) : null}\n <button\n onClick={(e) => { e.stopPropagation(); setExpanded(false); }}\n className=\"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n Show less <ChevronUp className=\"h-3 w-3\" />\n </button>\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 text-muted-foreground\">\n <span className=\"line-clamp-1 pr-3\">\n {event.preview ?? event.content}\n </span>\n <button className=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\">\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )}\n </div>\n </div>\n )\n ) : (\n <div className=\"pr-2 text-sm leading-relaxed text-muted-foreground\">\n {event.content}\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":["_a"]}
package/dist/index.d.ts CHANGED
@@ -19,10 +19,12 @@ export { CasePanelActivityActor, CasePanelActivityEvent, CasePanelActivityPayloa
19
19
  export { CasePanelDetail, CasePanelDetailProps, CasePanelDetailSlots, CasePanelDetailSlotsProps, CasePanelDetailWidth, CasePanelHeader, CasePanelHeaderProps, CasePanelIdentityLink, CasePanelIdentitySubline, CasePanelIdentitySublineProps, CasePanelMetadataRow, CasePanelMetadataRowProps, CasePanelSignalBrief, CasePanelSignalBriefProps } from './components/case-panel-detail.js';
20
20
  export { CasePanelSignalApprovalActions, CasePanelSignalApprovalActionsProps, CasePanelWhy, CasePanelWhyItem, CasePanelWhyProps, CasePanelWhyRow, CasePanelWhyTone } from './components/case-panel-why.js';
21
21
  export { CollapsibleSection, CollapsibleSectionProps } from './components/collapsible-section.js';
22
+ export { CommentComposer, CommentComposerProps } from './components/comment-composer.js';
22
23
  export { ComplianceBadge, ComplianceBadgeProps, ComplianceStatus } from './components/compliance-badge.js';
23
24
  export { ContactChip, ContactChipProps } from './components/contact-chip.js';
24
25
  export { ContactChannel, ContactItem, ContactList, ContactListProps } from './components/contact-list.js';
25
26
  export { ContextualQuickActionContextLabel, ContextualQuickActionContextLabelProps, ContextualQuickActionItem, ContextualQuickActionLauncher, ContextualQuickActionLauncherProps, ContextualQuickActionLauncherVariant } from './components/contextual-quick-action-launcher.js';
27
+ export { ConvMessage, ConvParticipant, ConvStatus, ConversationPanel, ConversationPanelProps, ConversationReplyPayload, ConversationThread } from './components/conversation-panel.js';
26
28
  export { CheckInsCard, RecentlyCompletedCard, TopTasksCard, UpcomingMeetingsCard } from './components/dashboard-cards.js';
27
29
  export { DataRow, DataTable, DataTableProps } from './components/data-table.js';
28
30
  export { ConditionFieldDef, ConditionFieldOption, ConditionFilterValue, ConditionOperator, ConditionOptionObject, DEFAULT_OPERATORS, DataTableConditionFilter, DataTableConditionFilterProps, OPERATOR_LABELS, generateConditionId, getOperators, shouldShowOptionSearch } from './components/data-table-condition-filter.js';
@@ -55,6 +57,7 @@ export { KbdHint } from './components/kbd-hint.js';
55
57
  export { Label } from './components/label.js';
56
58
  export { Message, MessageAvatar, MessageAvatarProps, MessageContent, MessageContentProps, MessageProps } from './components/message.js';
57
59
  export { KpiStrip, KpiStripItem, KpiStripProps, MetricCard, MetricCardProps, MetricDataPoint } from './components/metric-card.js';
60
+ export { AccountOwnerChip, AccountOwnerChipProps, OwnerChips, OwnerChipsProps, OwnerPerson, SignalOwnerChip, SignalOwnerChipProps } from './components/owner-chips.js';
58
61
  export { PerformanceMetricsTable, PerformanceMetricsTableRow, PerformanceMetricsTableSortOption } from './components/performance-metrics-table.js';
59
62
  export { Pill, PillProps, PillStatus, StatusPill, StatusPillProps, pillVariants } from './components/pill.js';
60
63
  export { PreviewList, PreviewListItem, PreviewListItemProps } from './components/preview-list.js';
package/dist/index.js CHANGED
@@ -19,10 +19,12 @@ export * from "./components/case-panel-activity-timeline.js";
19
19
  export * from "./components/case-panel-detail.js";
20
20
  export * from "./components/case-panel-why.js";
21
21
  import { CollapsibleSection } from "./components/collapsible-section.js";
22
+ export * from "./components/comment-composer.js";
22
23
  export * from "./components/compliance-badge.js";
23
24
  export * from "./components/contact-chip.js";
24
25
  export * from "./components/contact-list.js";
25
26
  export * from "./components/contextual-quick-action-launcher.js";
27
+ export * from "./components/conversation-panel.js";
26
28
  export * from "./components/dashboard-cards.js";
27
29
  export * from "./components/data-table.js";
28
30
  export * from "./components/data-table-condition-filter.js";
@@ -55,6 +57,7 @@ export * from "./components/kbd-hint.js";
55
57
  export * from "./components/label.js";
56
58
  export * from "./components/message.js";
57
59
  export * from "./components/metric-card.js";
60
+ export * from "./components/owner-chips.js";
58
61
  export * from "./components/performance-metrics-table.js";
59
62
  export * from "./components/pill.js";
60
63
  export * from "./components/preview-list.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @handled-ai/design-system\n * UI components and utilities (shadcn-style, New York)\n */\n\n// Utilities\nexport { cn } from \"./lib/utils\"\nexport { BRAND_ICONS, BRAND_GRAPHICS } from \"./lib/icons\"\nexport { displayName, getInitials, shortName, type ProfileLike } from \"./lib/user-display\"\n\n// Hooks\nexport { useIsMobile } from \"./hooks/use-mobile\"\n\n// Components (light — no recharts/nivo/three transitive deps)\nexport * from \"./components/activity-detail\"\nexport * from \"./components/activity-log\"\nexport * from \"./components/agent-popover\"\nexport * from \"./components/agent-widget\"\nexport * from \"./components/avatar\"\nexport * from \"./components/badge\"\nexport * from \"./components/button\"\nexport * from \"./components/card\"\nexport * from \"./components/case-panel-email-composer\"\nexport * from \"./components/email-composer-row\"\nexport * from \"./components/email-preview-card\"\nexport * from \"./components/email-recipient-field\"\nexport * from \"./components/email-send-bar\"\nexport * from \"./components/case-panel-activity-timeline\"\nexport * from \"./components/case-panel-detail\"\nexport * from \"./components/case-panel-why\"\nexport { CollapsibleSection, type CollapsibleSectionProps } from \"./components/collapsible-section\"\nexport * from \"./components/compliance-badge\"\nexport * from \"./components/contact-chip\"\nexport * from \"./components/contact-list\"\nexport * from \"./components/contextual-quick-action-launcher\"\nexport * from \"./components/dashboard-cards\"\nexport * from \"./components/data-table\"\nexport * from \"./components/data-table-condition-filter\"\nexport * from \"./components/data-table-display\"\nexport * from \"./components/data-table-filter\"\nexport * from \"./components/data-table-quick-views\"\nexport * from \"./components/data-table-toolbar\"\nexport * from \"./components/detail-view\"\nexport * from \"./components/detail-drawer\"\nexport * from \"./components/dialog\"\nexport * from \"./components/dropdown-menu\"\nexport * from \"./components/empty-state\"\nexport * from \"./components/entity-panel\"\nexport * from \"./components/related-record-action-card\"\nexport { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions, InlineFeedbackControl } from \"./components/feedback-primitives\"\nexport type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData, PersistedFeedbackData, InlineFeedbackControlProps } from \"./components/feedback-primitives\"\nexport { SignalPriorityPopover } from \"./components/signal-priority-popover\"\nexport type { SignalPriorityPopoverProps, PriorityFactor } from \"./components/signal-priority-popover\"\nexport * from \"./components/filter-chip\"\nexport * from \"./components/inbox-row\"\nexport * from \"./components/inbox-toolbar\"\nexport * from \"./components/inline-banner\"\nexport * from \"./components/input\"\nexport * from \"./components/insights-filter-bar\"\nexport * from \"./components/days-open-cell\"\nexport * from \"./components/linked-entity-cell\"\nexport * from \"./components/item-list\"\nexport * from \"./components/item-list-display\"\nexport * from \"./components/item-list-filter\"\nexport * from \"./components/item-list-toolbar\"\nexport * from \"./components/kbd-hint\"\nexport * from \"./components/label\"\nexport * from \"./components/message\"\nexport * from \"./components/metric-card\"\nexport * from \"./components/performance-metrics-table\"\nexport * from \"./components/pill\"\nexport * from \"./components/preview-list\"\nexport * from \"./components/progress\"\nexport * from \"./components/quick-action-chat-area\"\nexport * from \"./components/quick-segment\"\nexport {\n QuickActionModal,\n type QuickActionPriority,\n type QuickActionTaskDraft,\n type QuickActionTemplate,\n} from \"./components/quick-action-modal\"\nexport * from \"./components/quick-action-sidebar-nav\"\nexport * from \"./components/recommended-actions-section\"\nexport * from \"./components/report-card\"\nexport * from \"./components/rich-text-toolbar\"\nexport * from \"./components/score-analysis-modal\"\nexport * from \"./components/score-breakdown\"\nexport * from \"./components/score-feedback\"\nexport * from \"./components/score-semantics\"\nexport * from \"./components/score-why-chips\"\nexport * from \"./components/score-ring\"\nexport * from \"./components/scroll-area\"\nexport * from \"./components/select\"\nexport * from \"./components/separator\"\nexport * from \"./components/sheet\"\nexport * from \"./components/sidebar\"\nexport * from \"./components/signal-feedback-inline\"\nexport * from \"./components/simple-data-table\"\nexport * from \"./components/skeleton\"\nexport * from \"./components/status-badge\"\nexport * from \"./components/step-timeline\"\nexport * from \"./components/sticky-action-bar\"\nexport * from \"./components/styled-bar-list\"\nexport { DraftFeedbackInline } from \"./components/draft-feedback-inline\"\nexport type { DraftFeedbackInlineProps } from \"./components/draft-feedback-inline\"\nexport { AccountContactsPopover, BrandIcon } from \"./components/account-contacts-popover\"\nexport type { AccountContactsPopoverProps } from \"./components/account-contacts-popover\"\nexport * from \"./components/suggested-actions\"\nexport * from \"./components/switch\"\nexport * from \"./components/table\"\nexport * from \"./components/tabs\"\nexport * from \"./components/textarea\"\nexport * from \"./components/timeline-activity\"\nexport * from \"./components/tooltip\"\nexport * from \"./components/user-display\"\nexport * from \"./components/variable-autocomplete\"\nexport * from \"./components/view-mode-toggle\"\nexport * from \"./components/virtualized-data-table\"\nexport type { ColumnSizingState } from \"@tanstack/react-table\"\n\n// Charts (re-exported for backward compatibility with root imports)\nexport * from \"./charts/index\"\n\n// Prototype template system (re-exported for backward compatibility)\nexport * from \"./prototype/prototype-config\"\nexport * from \"./prototype/prototype-shell\"\nexport * from \"./prototype/prototype-inbox-view\"\nexport * from \"./prototype/prototype-insights-view\"\nexport * from \"./prototype/prototype-accounts-view\"\nexport * from \"./prototype/prototype-admin-view\"\nexport * from \"./prototype/prototype-work-queue-view\"\n"],"mappings":"AAMA,SAAS,UAAU;AACnB,SAAS,aAAa,sBAAsB;AAC5C,SAAS,aAAa,aAAa,iBAAmC;AAGtE,SAAS,mBAAmB;AAG5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,0BAAwD;AACjE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,gBAAgB,mBAAmB,eAAe,iBAAiB,6BAA6B;AAEzG,SAAS,6BAA6B;AAEtC,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EACE;AAAA,OAIK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,2BAA2B;AAEpC,SAAS,wBAAwB,iBAAiB;AAElD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @handled-ai/design-system\n * UI components and utilities (shadcn-style, New York)\n */\n\n// Utilities\nexport { cn } from \"./lib/utils\"\nexport { BRAND_ICONS, BRAND_GRAPHICS } from \"./lib/icons\"\nexport { displayName, getInitials, shortName, type ProfileLike } from \"./lib/user-display\"\n\n// Hooks\nexport { useIsMobile } from \"./hooks/use-mobile\"\n\n// Components (light — no recharts/nivo/three transitive deps)\nexport * from \"./components/activity-detail\"\nexport * from \"./components/activity-log\"\nexport * from \"./components/agent-popover\"\nexport * from \"./components/agent-widget\"\nexport * from \"./components/avatar\"\nexport * from \"./components/badge\"\nexport * from \"./components/button\"\nexport * from \"./components/card\"\nexport * from \"./components/case-panel-email-composer\"\nexport * from \"./components/email-composer-row\"\nexport * from \"./components/email-preview-card\"\nexport * from \"./components/email-recipient-field\"\nexport * from \"./components/email-send-bar\"\nexport * from \"./components/case-panel-activity-timeline\"\nexport * from \"./components/case-panel-detail\"\nexport * from \"./components/case-panel-why\"\nexport { CollapsibleSection, type CollapsibleSectionProps } from \"./components/collapsible-section\"\nexport * from \"./components/comment-composer\"\nexport * from \"./components/compliance-badge\"\nexport * from \"./components/contact-chip\"\nexport * from \"./components/contact-list\"\nexport * from \"./components/contextual-quick-action-launcher\"\nexport * from \"./components/conversation-panel\"\nexport * from \"./components/dashboard-cards\"\nexport * from \"./components/data-table\"\nexport * from \"./components/data-table-condition-filter\"\nexport * from \"./components/data-table-display\"\nexport * from \"./components/data-table-filter\"\nexport * from \"./components/data-table-quick-views\"\nexport * from \"./components/data-table-toolbar\"\nexport * from \"./components/detail-view\"\nexport * from \"./components/detail-drawer\"\nexport * from \"./components/dialog\"\nexport * from \"./components/dropdown-menu\"\nexport * from \"./components/empty-state\"\nexport * from \"./components/entity-panel\"\nexport * from \"./components/related-record-action-card\"\nexport { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions, InlineFeedbackControl } from \"./components/feedback-primitives\"\nexport type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData, PersistedFeedbackData, InlineFeedbackControlProps } from \"./components/feedback-primitives\"\nexport { SignalPriorityPopover } from \"./components/signal-priority-popover\"\nexport type { SignalPriorityPopoverProps, PriorityFactor } from \"./components/signal-priority-popover\"\nexport * from \"./components/filter-chip\"\nexport * from \"./components/inbox-row\"\nexport * from \"./components/inbox-toolbar\"\nexport * from \"./components/inline-banner\"\nexport * from \"./components/input\"\nexport * from \"./components/insights-filter-bar\"\nexport * from \"./components/days-open-cell\"\nexport * from \"./components/linked-entity-cell\"\nexport * from \"./components/item-list\"\nexport * from \"./components/item-list-display\"\nexport * from \"./components/item-list-filter\"\nexport * from \"./components/item-list-toolbar\"\nexport * from \"./components/kbd-hint\"\nexport * from \"./components/label\"\nexport * from \"./components/message\"\nexport * from \"./components/metric-card\"\nexport * from \"./components/owner-chips\"\nexport * from \"./components/performance-metrics-table\"\nexport * from \"./components/pill\"\nexport * from \"./components/preview-list\"\nexport * from \"./components/progress\"\nexport * from \"./components/quick-action-chat-area\"\nexport * from \"./components/quick-segment\"\nexport {\n QuickActionModal,\n type QuickActionPriority,\n type QuickActionTaskDraft,\n type QuickActionTemplate,\n} from \"./components/quick-action-modal\"\nexport * from \"./components/quick-action-sidebar-nav\"\nexport * from \"./components/recommended-actions-section\"\nexport * from \"./components/report-card\"\nexport * from \"./components/rich-text-toolbar\"\nexport * from \"./components/score-analysis-modal\"\nexport * from \"./components/score-breakdown\"\nexport * from \"./components/score-feedback\"\nexport * from \"./components/score-semantics\"\nexport * from \"./components/score-why-chips\"\nexport * from \"./components/score-ring\"\nexport * from \"./components/scroll-area\"\nexport * from \"./components/select\"\nexport * from \"./components/separator\"\nexport * from \"./components/sheet\"\nexport * from \"./components/sidebar\"\nexport * from \"./components/signal-feedback-inline\"\nexport * from \"./components/simple-data-table\"\nexport * from \"./components/skeleton\"\nexport * from \"./components/status-badge\"\nexport * from \"./components/step-timeline\"\nexport * from \"./components/sticky-action-bar\"\nexport * from \"./components/styled-bar-list\"\nexport { DraftFeedbackInline } from \"./components/draft-feedback-inline\"\nexport type { DraftFeedbackInlineProps } from \"./components/draft-feedback-inline\"\nexport { AccountContactsPopover, BrandIcon } from \"./components/account-contacts-popover\"\nexport type { AccountContactsPopoverProps } from \"./components/account-contacts-popover\"\nexport * from \"./components/suggested-actions\"\nexport * from \"./components/switch\"\nexport * from \"./components/table\"\nexport * from \"./components/tabs\"\nexport * from \"./components/textarea\"\nexport * from \"./components/timeline-activity\"\nexport * from \"./components/tooltip\"\nexport * from \"./components/user-display\"\nexport * from \"./components/variable-autocomplete\"\nexport * from \"./components/view-mode-toggle\"\nexport * from \"./components/virtualized-data-table\"\nexport type { ColumnSizingState } from \"@tanstack/react-table\"\n\n// Charts (re-exported for backward compatibility with root imports)\nexport * from \"./charts/index\"\n\n// Prototype template system (re-exported for backward compatibility)\nexport * from \"./prototype/prototype-config\"\nexport * from \"./prototype/prototype-shell\"\nexport * from \"./prototype/prototype-inbox-view\"\nexport * from \"./prototype/prototype-insights-view\"\nexport * from \"./prototype/prototype-accounts-view\"\nexport * from \"./prototype/prototype-admin-view\"\nexport * from \"./prototype/prototype-work-queue-view\"\n"],"mappings":"AAMA,SAAS,UAAU;AACnB,SAAS,aAAa,sBAAsB;AAC5C,SAAS,aAAa,aAAa,iBAAmC;AAGtE,SAAS,mBAAmB;AAG5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,0BAAwD;AACjE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,gBAAgB,mBAAmB,eAAe,iBAAiB,6BAA6B;AAEzG,SAAS,6BAA6B;AAEtC,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EACE;AAAA,OAIK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,2BAA2B;AAEpC,SAAS,wBAAwB,iBAAiB;AAElD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Conservative, deterministic sanitizer for user/email supplied HTML rendered by
3
+ * design-system components. It keeps common email formatting tags while removing
4
+ * executable tags, event handlers, inline styles, and unsafe URLs. This stays
5
+ * dependency-free for the shared package and intentionally favors stripping
6
+ * ambiguous email content over preserving every possible HTML feature.
7
+ */
8
+ declare function sanitizeHtml(html: string): string;
9
+ declare function htmlToTextSnippet(html: string, maxLength?: number): string;
10
+
11
+ export { htmlToTextSnippet, sanitizeHtml };
@@ -0,0 +1,222 @@
1
+ const DANGEROUS_BLOCK_TAGS = /* @__PURE__ */ new Set([
2
+ "script",
3
+ "style",
4
+ "iframe",
5
+ "object",
6
+ "embed",
7
+ "svg",
8
+ "math",
9
+ "template",
10
+ "noscript",
11
+ "textarea",
12
+ "select"
13
+ ]);
14
+ const ALLOWED_TAGS = /* @__PURE__ */ new Set([
15
+ "a",
16
+ "b",
17
+ "blockquote",
18
+ "br",
19
+ "code",
20
+ "del",
21
+ "div",
22
+ "em",
23
+ "hr",
24
+ "i",
25
+ "img",
26
+ "li",
27
+ "ol",
28
+ "p",
29
+ "pre",
30
+ "s",
31
+ "span",
32
+ "strong",
33
+ "table",
34
+ "tbody",
35
+ "td",
36
+ "th",
37
+ "thead",
38
+ "tr",
39
+ "u",
40
+ "ul"
41
+ ]);
42
+ const VOID_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img"]);
43
+ const SAFE_GLOBAL_ATTRS = /* @__PURE__ */ new Set(["aria-label", "role", "title"]);
44
+ const SAFE_URL_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
45
+ function escapeHtml(value) {
46
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
47
+ }
48
+ function escapeAttribute(value) {
49
+ return escapeHtml(value).replace(/"/g, "&quot;");
50
+ }
51
+ function safeCodePoint(value) {
52
+ return Number.isInteger(value) && value >= 0 && value <= 1114111 ? String.fromCodePoint(value) : "";
53
+ }
54
+ function decodeHtmlEntities(value) {
55
+ const namedEntities = {
56
+ amp: "&",
57
+ apos: "'",
58
+ colon: ":",
59
+ gt: ">",
60
+ lt: "<",
61
+ newline: "\n",
62
+ quot: '"',
63
+ tab: " "
64
+ };
65
+ let decoded = value;
66
+ for (let i = 0; i < 4; i += 1) {
67
+ const next = decoded.replace(/&#x([0-9a-f]+);?/gi, (_match, hex) => {
68
+ const codePoint = Number.parseInt(hex, 16);
69
+ return safeCodePoint(codePoint);
70
+ }).replace(/&#(\d+);?/g, (_match, decimal) => {
71
+ const codePoint = Number.parseInt(decimal, 10);
72
+ return safeCodePoint(codePoint);
73
+ }).replace(/&([a-z]+);/gi, (match, name) => {
74
+ var _a;
75
+ return (_a = namedEntities[name.toLowerCase()]) != null ? _a : match;
76
+ });
77
+ if (next === decoded) return decoded;
78
+ decoded = next;
79
+ }
80
+ return decoded;
81
+ }
82
+ function isSafeUrl(value) {
83
+ const decoded = decodeHtmlEntities(value).replace(/[\u0000-\u001f\u007f\s]+/g, "").trim();
84
+ if (!decoded) return false;
85
+ if (decoded.startsWith("//")) return false;
86
+ if (decoded.startsWith("#") || decoded.startsWith("/") || decoded.startsWith("./") || decoded.startsWith("../")) {
87
+ return true;
88
+ }
89
+ try {
90
+ return SAFE_URL_PROTOCOLS.has(new URL(decoded, "https://handled.local").protocol);
91
+ } catch (e) {
92
+ return false;
93
+ }
94
+ }
95
+ function sanitizeClassName(value) {
96
+ const safeTokens = value.split(/\s+/).map((token) => token.trim()).filter((token) => /^[A-Za-z0-9_-]+$/.test(token));
97
+ return safeTokens.length ? safeTokens.join(" ") : null;
98
+ }
99
+ function sanitizeAttribute(tagName, name, value) {
100
+ const attr = name.toLowerCase();
101
+ if (attr.startsWith("on") || attr === "style" || attr === "srcdoc" || attr === "formaction" || attr === "xlink:href" || attr === "xmlns") {
102
+ return null;
103
+ }
104
+ if (attr === "class") {
105
+ const safeClassName = sanitizeClassName(value);
106
+ return safeClassName ? `class="${escapeAttribute(safeClassName)}"` : null;
107
+ }
108
+ if (SAFE_GLOBAL_ATTRS.has(attr) || attr.startsWith("aria-")) {
109
+ return `${attr}="${escapeAttribute(value)}"`;
110
+ }
111
+ if (tagName === "a" && attr === "href" && isSafeUrl(value)) {
112
+ return `href="${escapeAttribute(value)}"`;
113
+ }
114
+ if (tagName === "img" && attr === "src" && isSafeUrl(value)) {
115
+ return `src="${escapeAttribute(value)}"`;
116
+ }
117
+ if (tagName === "img" && (attr === "alt" || attr === "width" || attr === "height")) {
118
+ return `${attr}="${escapeAttribute(value)}"`;
119
+ }
120
+ if ((tagName === "td" || tagName === "th") && (attr === "colspan" || attr === "rowspan")) {
121
+ return `${attr}="${escapeAttribute(value)}"`;
122
+ }
123
+ return null;
124
+ }
125
+ function sanitizeAttributes(tagName, rawAttributes = "") {
126
+ var _a, _b;
127
+ const attributes = [];
128
+ const attrPattern = /([A-Za-z_:][-A-Za-z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
129
+ let match;
130
+ while ((match = attrPattern.exec(rawAttributes)) !== null) {
131
+ const [, name, doubleQuotedValue, singleQuotedValue, unquotedValue] = match;
132
+ const value = (_b = (_a = doubleQuotedValue != null ? doubleQuotedValue : singleQuotedValue) != null ? _a : unquotedValue) != null ? _b : "";
133
+ const safeAttribute = sanitizeAttribute(tagName, name, value);
134
+ if (safeAttribute) attributes.push(safeAttribute);
135
+ }
136
+ if (tagName === "a" && attributes.some((attr) => attr.startsWith("href="))) {
137
+ attributes.push('target="_blank"', 'rel="noopener noreferrer"');
138
+ }
139
+ return attributes.length ? ` ${attributes.join(" ")}` : "";
140
+ }
141
+ function findTagEnd(html, startIndex) {
142
+ let quote = null;
143
+ for (let i = startIndex + 1; i < html.length; i += 1) {
144
+ const char = html[i];
145
+ if (quote) {
146
+ if (char === quote) quote = null;
147
+ continue;
148
+ }
149
+ if (char === '"' || char === "'") {
150
+ quote = char;
151
+ continue;
152
+ }
153
+ if (char === ">") return i;
154
+ }
155
+ return -1;
156
+ }
157
+ function parseTag(rawTag) {
158
+ var _a;
159
+ const match = rawTag.match(/^<\s*(\/)?\s*([A-Za-z][A-Za-z0-9:-]*)\b([\s\S]*?)\/?>$/);
160
+ if (!match) return null;
161
+ return {
162
+ closing: !!match[1],
163
+ name: match[2].toLowerCase(),
164
+ attributes: (_a = match[3]) != null ? _a : ""
165
+ };
166
+ }
167
+ function findDangerousClose(html, tagName, fromIndex) {
168
+ const closePattern = new RegExp(`</\\s*${tagName}\\s*>`, "ig");
169
+ closePattern.lastIndex = fromIndex;
170
+ const match = closePattern.exec(html);
171
+ return match ? closePattern.lastIndex : -1;
172
+ }
173
+ function sanitizeHtml(html) {
174
+ let output = "";
175
+ let cursor = 0;
176
+ while (cursor < html.length) {
177
+ const tagStart = html.indexOf("<", cursor);
178
+ if (tagStart === -1) {
179
+ output += html.slice(cursor);
180
+ break;
181
+ }
182
+ output += html.slice(cursor, tagStart);
183
+ if (html.startsWith("<!--", tagStart)) {
184
+ const commentEnd = html.indexOf("-->", tagStart + 4);
185
+ cursor = commentEnd === -1 ? html.length : commentEnd + 3;
186
+ continue;
187
+ }
188
+ const tagEnd = findTagEnd(html, tagStart);
189
+ if (tagEnd === -1) {
190
+ output += escapeHtml(html.slice(tagStart));
191
+ break;
192
+ }
193
+ const rawTag = html.slice(tagStart, tagEnd + 1);
194
+ const parsed = parseTag(rawTag);
195
+ if (!parsed) {
196
+ cursor = tagEnd + 1;
197
+ continue;
198
+ }
199
+ if (DANGEROUS_BLOCK_TAGS.has(parsed.name)) {
200
+ const closeEnd = parsed.closing ? -1 : findDangerousClose(html, parsed.name, tagEnd + 1);
201
+ cursor = closeEnd === -1 ? tagEnd + 1 : closeEnd;
202
+ continue;
203
+ }
204
+ if (ALLOWED_TAGS.has(parsed.name)) {
205
+ if (parsed.closing) {
206
+ if (!VOID_TAGS.has(parsed.name)) output += `</${parsed.name}>`;
207
+ } else {
208
+ output += `<${parsed.name}${sanitizeAttributes(parsed.name, parsed.attributes)}>`;
209
+ }
210
+ }
211
+ cursor = tagEnd + 1;
212
+ }
213
+ return output;
214
+ }
215
+ function htmlToTextSnippet(html, maxLength = 140) {
216
+ return sanitizeHtml(html).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength);
217
+ }
218
+ export {
219
+ htmlToTextSnippet,
220
+ sanitizeHtml
221
+ };
222
+ //# sourceMappingURL=safe-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/internal/safe-html.ts"],"sourcesContent":["const DANGEROUS_BLOCK_TAGS = new Set([\n \"script\",\n \"style\",\n \"iframe\",\n \"object\",\n \"embed\",\n \"svg\",\n \"math\",\n \"template\",\n \"noscript\",\n \"textarea\",\n \"select\",\n])\n\nconst ALLOWED_TAGS = new Set([\n \"a\",\n \"b\",\n \"blockquote\",\n \"br\",\n \"code\",\n \"del\",\n \"div\",\n \"em\",\n \"hr\",\n \"i\",\n \"img\",\n \"li\",\n \"ol\",\n \"p\",\n \"pre\",\n \"s\",\n \"span\",\n \"strong\",\n \"table\",\n \"tbody\",\n \"td\",\n \"th\",\n \"thead\",\n \"tr\",\n \"u\",\n \"ul\",\n])\n\nconst VOID_TAGS = new Set([\"br\", \"hr\", \"img\"])\nconst SAFE_GLOBAL_ATTRS = new Set([\"aria-label\", \"role\", \"title\"])\nconst SAFE_URL_PROTOCOLS = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\"])\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n}\n\nfunction escapeAttribute(value: string): string {\n return escapeHtml(value).replace(/\"/g, \"&quot;\")\n}\n\nfunction safeCodePoint(value: number): string {\n return Number.isInteger(value) && value >= 0 && value <= 0x10ffff ? String.fromCodePoint(value) : \"\"\n}\n\nfunction decodeHtmlEntities(value: string): string {\n const namedEntities: Record<string, string> = {\n amp: \"&\",\n apos: \"'\",\n colon: \":\",\n gt: \">\",\n lt: \"<\",\n newline: \"\\n\",\n quot: '\"',\n tab: \"\\t\",\n }\n\n let decoded = value\n for (let i = 0; i < 4; i += 1) {\n const next = decoded\n .replace(/&#x([0-9a-f]+);?/gi, (_match, hex: string) => {\n const codePoint = Number.parseInt(hex, 16)\n return safeCodePoint(codePoint)\n })\n .replace(/&#(\\d+);?/g, (_match, decimal: string) => {\n const codePoint = Number.parseInt(decimal, 10)\n return safeCodePoint(codePoint)\n })\n .replace(/&([a-z]+);/gi, (match, name: string) => namedEntities[name.toLowerCase()] ?? match)\n\n if (next === decoded) return decoded\n decoded = next\n }\n\n return decoded\n}\n\nfunction isSafeUrl(value: string): boolean {\n const decoded = decodeHtmlEntities(value).replace(/[\\u0000-\\u001f\\u007f\\s]+/g, \"\").trim()\n if (!decoded) return false\n if (decoded.startsWith(\"//\")) return false\n if (decoded.startsWith(\"#\") || decoded.startsWith(\"/\") || decoded.startsWith(\"./\") || decoded.startsWith(\"../\")) {\n return true\n }\n\n try {\n return SAFE_URL_PROTOCOLS.has(new URL(decoded, \"https://handled.local\").protocol)\n } catch {\n return false\n }\n}\n\nfunction sanitizeClassName(value: string): string | null {\n const safeTokens = value\n .split(/\\s+/)\n .map((token) => token.trim())\n .filter((token) => /^[A-Za-z0-9_-]+$/.test(token))\n\n return safeTokens.length ? safeTokens.join(\" \") : null\n}\n\nfunction sanitizeAttribute(tagName: string, name: string, value: string): string | null {\n const attr = name.toLowerCase()\n\n if (\n attr.startsWith(\"on\") ||\n attr === \"style\" ||\n attr === \"srcdoc\" ||\n attr === \"formaction\" ||\n attr === \"xlink:href\" ||\n attr === \"xmlns\"\n ) {\n return null\n }\n\n if (attr === \"class\") {\n const safeClassName = sanitizeClassName(value)\n return safeClassName ? `class=\"${escapeAttribute(safeClassName)}\"` : null\n }\n\n if (SAFE_GLOBAL_ATTRS.has(attr) || attr.startsWith(\"aria-\")) {\n return `${attr}=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"a\" && attr === \"href\" && isSafeUrl(value)) {\n return `href=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"img\" && attr === \"src\" && isSafeUrl(value)) {\n return `src=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"img\" && (attr === \"alt\" || attr === \"width\" || attr === \"height\")) {\n return `${attr}=\"${escapeAttribute(value)}\"`\n }\n\n if ((tagName === \"td\" || tagName === \"th\") && (attr === \"colspan\" || attr === \"rowspan\")) {\n return `${attr}=\"${escapeAttribute(value)}\"`\n }\n\n return null\n}\n\nfunction sanitizeAttributes(tagName: string, rawAttributes = \"\"): string {\n const attributes: string[] = []\n const attrPattern = /([A-Za-z_:][-A-Za-z0-9_:.]*)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g\n let match: RegExpExecArray | null\n\n while ((match = attrPattern.exec(rawAttributes)) !== null) {\n const [, name, doubleQuotedValue, singleQuotedValue, unquotedValue] = match\n const value = doubleQuotedValue ?? singleQuotedValue ?? unquotedValue ?? \"\"\n const safeAttribute = sanitizeAttribute(tagName, name, value)\n if (safeAttribute) attributes.push(safeAttribute)\n }\n\n if (tagName === \"a\" && attributes.some((attr) => attr.startsWith(\"href=\"))) {\n attributes.push('target=\"_blank\"', 'rel=\"noopener noreferrer\"')\n }\n\n return attributes.length ? ` ${attributes.join(\" \")}` : \"\"\n}\n\nfunction findTagEnd(html: string, startIndex: number): number {\n let quote: '\"' | \"'\" | null = null\n\n for (let i = startIndex + 1; i < html.length; i += 1) {\n const char = html[i]\n if (quote) {\n if (char === quote) quote = null\n continue\n }\n\n if (char === '\"' || char === \"'\") {\n quote = char\n continue\n }\n\n if (char === \">\") return i\n }\n\n return -1\n}\n\nfunction parseTag(rawTag: string): { closing: boolean; name: string; attributes: string } | null {\n const match = rawTag.match(/^<\\s*(\\/)?\\s*([A-Za-z][A-Za-z0-9:-]*)\\b([\\s\\S]*?)\\/?>$/)\n if (!match) return null\n\n return {\n closing: !!match[1],\n name: match[2].toLowerCase(),\n attributes: match[3] ?? \"\",\n }\n}\n\nfunction findDangerousClose(html: string, tagName: string, fromIndex: number): number {\n const closePattern = new RegExp(`</\\\\s*${tagName}\\\\s*>`, \"ig\")\n closePattern.lastIndex = fromIndex\n const match = closePattern.exec(html)\n return match ? closePattern.lastIndex : -1\n}\n\n/**\n * Conservative, deterministic sanitizer for user/email supplied HTML rendered by\n * design-system components. It keeps common email formatting tags while removing\n * executable tags, event handlers, inline styles, and unsafe URLs. This stays\n * dependency-free for the shared package and intentionally favors stripping\n * ambiguous email content over preserving every possible HTML feature.\n */\nexport function sanitizeHtml(html: string): string {\n let output = \"\"\n let cursor = 0\n\n while (cursor < html.length) {\n const tagStart = html.indexOf(\"<\", cursor)\n if (tagStart === -1) {\n output += html.slice(cursor)\n break\n }\n\n output += html.slice(cursor, tagStart)\n\n if (html.startsWith(\"<!--\", tagStart)) {\n const commentEnd = html.indexOf(\"-->\", tagStart + 4)\n cursor = commentEnd === -1 ? html.length : commentEnd + 3\n continue\n }\n\n const tagEnd = findTagEnd(html, tagStart)\n if (tagEnd === -1) {\n output += escapeHtml(html.slice(tagStart))\n break\n }\n\n const rawTag = html.slice(tagStart, tagEnd + 1)\n const parsed = parseTag(rawTag)\n if (!parsed) {\n cursor = tagEnd + 1\n continue\n }\n\n if (DANGEROUS_BLOCK_TAGS.has(parsed.name)) {\n const closeEnd = parsed.closing ? -1 : findDangerousClose(html, parsed.name, tagEnd + 1)\n cursor = closeEnd === -1 ? tagEnd + 1 : closeEnd\n continue\n }\n\n if (ALLOWED_TAGS.has(parsed.name)) {\n if (parsed.closing) {\n if (!VOID_TAGS.has(parsed.name)) output += `</${parsed.name}>`\n } else {\n output += `<${parsed.name}${sanitizeAttributes(parsed.name, parsed.attributes)}>`\n }\n }\n\n cursor = tagEnd + 1\n }\n\n return output\n}\n\nexport function htmlToTextSnippet(html: string, maxLength = 140): string {\n return sanitizeHtml(html)\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim()\n .slice(0, maxLength)\n}\n"],"mappings":"AAAA,MAAM,uBAAuB,oBAAI,IAAI;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,MAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,MAAM,YAAY,oBAAI,IAAI,CAAC,MAAM,MAAM,KAAK,CAAC;AAC7C,MAAM,oBAAoB,oBAAI,IAAI,CAAC,cAAc,QAAQ,OAAO,CAAC;AACjE,MAAM,qBAAqB,oBAAI,IAAI,CAAC,SAAS,UAAU,WAAW,MAAM,CAAC;AAEzE,SAAS,WAAW,OAAuB;AACzC,SAAO,MACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM;AACzB;AAEA,SAAS,gBAAgB,OAAuB;AAC9C,SAAO,WAAW,KAAK,EAAE,QAAQ,MAAM,QAAQ;AACjD;AAEA,SAAS,cAAc,OAAuB;AAC5C,SAAO,OAAO,UAAU,KAAK,KAAK,SAAS,KAAK,SAAS,UAAW,OAAO,cAAc,KAAK,IAAI;AACpG;AAEA,SAAS,mBAAmB,OAAuB;AACjD,QAAM,gBAAwC;AAAA,IAC5C,KAAK;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA,EACP;AAEA,MAAI,UAAU;AACd,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;AAC7B,UAAM,OAAO,QACV,QAAQ,sBAAsB,CAAC,QAAQ,QAAgB;AACtD,YAAM,YAAY,OAAO,SAAS,KAAK,EAAE;AACzC,aAAO,cAAc,SAAS;AAAA,IAChC,CAAC,EACA,QAAQ,cAAc,CAAC,QAAQ,YAAoB;AAClD,YAAM,YAAY,OAAO,SAAS,SAAS,EAAE;AAC7C,aAAO,cAAc,SAAS;AAAA,IAChC,CAAC,EACA,QAAQ,gBAAgB,CAAC,OAAO,SAAc;AArFrD;AAqFwD,iCAAc,KAAK,YAAY,CAAC,MAAhC,YAAqC;AAAA,KAAK;AAE9F,QAAI,SAAS,QAAS,QAAO;AAC7B,cAAU;AAAA,EACZ;AAEA,SAAO;AACT;AAEA,SAAS,UAAU,OAAwB;AACzC,QAAM,UAAU,mBAAmB,KAAK,EAAE,QAAQ,6BAA6B,EAAE,EAAE,KAAK;AACxF,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,QAAQ,WAAW,IAAI,EAAG,QAAO;AACrC,MAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,IAAI,KAAK,QAAQ,WAAW,KAAK,GAAG;AAC/G,WAAO;AAAA,EACT;AAEA,MAAI;AACF,WAAO,mBAAmB,IAAI,IAAI,IAAI,SAAS,uBAAuB,EAAE,QAAQ;AAAA,EAClF,SAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,OAA8B;AACvD,QAAM,aAAa,MAChB,MAAM,KAAK,EACX,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,OAAO,CAAC,UAAU,mBAAmB,KAAK,KAAK,CAAC;AAEnD,SAAO,WAAW,SAAS,WAAW,KAAK,GAAG,IAAI;AACpD;AAEA,SAAS,kBAAkB,SAAiB,MAAc,OAA8B;AACtF,QAAM,OAAO,KAAK,YAAY;AAE9B,MACE,KAAK,WAAW,IAAI,KACpB,SAAS,WACT,SAAS,YACT,SAAS,gBACT,SAAS,gBACT,SAAS,SACT;AACA,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,SAAS;AACpB,UAAM,gBAAgB,kBAAkB,KAAK;AAC7C,WAAO,gBAAgB,UAAU,gBAAgB,aAAa,CAAC,MAAM;AAAA,EACvE;AAEA,MAAI,kBAAkB,IAAI,IAAI,KAAK,KAAK,WAAW,OAAO,GAAG;AAC3D,WAAO,GAAG,IAAI,KAAK,gBAAgB,KAAK,CAAC;AAAA,EAC3C;AAEA,MAAI,YAAY,OAAO,SAAS,UAAU,UAAU,KAAK,GAAG;AAC1D,WAAO,SAAS,gBAAgB,KAAK,CAAC;AAAA,EACxC;AAEA,MAAI,YAAY,SAAS,SAAS,SAAS,UAAU,KAAK,GAAG;AAC3D,WAAO,QAAQ,gBAAgB,KAAK,CAAC;AAAA,EACvC;AAEA,MAAI,YAAY,UAAU,SAAS,SAAS,SAAS,WAAW,SAAS,WAAW;AAClF,WAAO,GAAG,IAAI,KAAK,gBAAgB,KAAK,CAAC;AAAA,EAC3C;AAEA,OAAK,YAAY,QAAQ,YAAY,UAAU,SAAS,aAAa,SAAS,YAAY;AACxF,WAAO,GAAG,IAAI,KAAK,gBAAgB,KAAK,CAAC;AAAA,EAC3C;AAEA,SAAO;AACT;AAEA,SAAS,mBAAmB,SAAiB,gBAAgB,IAAY;AAhKzE;AAiKE,QAAM,aAAuB,CAAC;AAC9B,QAAM,cAAc;AACpB,MAAI;AAEJ,UAAQ,QAAQ,YAAY,KAAK,aAAa,OAAO,MAAM;AACzD,UAAM,CAAC,EAAE,MAAM,mBAAmB,mBAAmB,aAAa,IAAI;AACtE,UAAM,SAAQ,2DAAqB,sBAArB,YAA0C,kBAA1C,YAA2D;AACzE,UAAM,gBAAgB,kBAAkB,SAAS,MAAM,KAAK;AAC5D,QAAI,cAAe,YAAW,KAAK,aAAa;AAAA,EAClD;AAEA,MAAI,YAAY,OAAO,WAAW,KAAK,CAAC,SAAS,KAAK,WAAW,OAAO,CAAC,GAAG;AAC1E,eAAW,KAAK,mBAAmB,2BAA2B;AAAA,EAChE;AAEA,SAAO,WAAW,SAAS,IAAI,WAAW,KAAK,GAAG,CAAC,KAAK;AAC1D;AAEA,SAAS,WAAW,MAAc,YAA4B;AAC5D,MAAI,QAA0B;AAE9B,WAAS,IAAI,aAAa,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;AACpD,UAAM,OAAO,KAAK,CAAC;AACnB,QAAI,OAAO;AACT,UAAI,SAAS,MAAO,SAAQ;AAC5B;AAAA,IACF;AAEA,QAAI,SAAS,OAAO,SAAS,KAAK;AAChC,cAAQ;AACR;AAAA,IACF;AAEA,QAAI,SAAS,IAAK,QAAO;AAAA,EAC3B;AAEA,SAAO;AACT;AAEA,SAAS,SAAS,QAA+E;AAxMjG;AAyME,QAAM,QAAQ,OAAO,MAAM,wDAAwD;AACnF,MAAI,CAAC,MAAO,QAAO;AAEnB,SAAO;AAAA,IACL,SAAS,CAAC,CAAC,MAAM,CAAC;AAAA,IAClB,MAAM,MAAM,CAAC,EAAE,YAAY;AAAA,IAC3B,aAAY,WAAM,CAAC,MAAP,YAAY;AAAA,EAC1B;AACF;AAEA,SAAS,mBAAmB,MAAc,SAAiB,WAA2B;AACpF,QAAM,eAAe,IAAI,OAAO,SAAS,OAAO,SAAS,IAAI;AAC7D,eAAa,YAAY;AACzB,QAAM,QAAQ,aAAa,KAAK,IAAI;AACpC,SAAO,QAAQ,aAAa,YAAY;AAC1C;AASO,SAAS,aAAa,MAAsB;AACjD,MAAI,SAAS;AACb,MAAI,SAAS;AAEb,SAAO,SAAS,KAAK,QAAQ;AAC3B,UAAM,WAAW,KAAK,QAAQ,KAAK,MAAM;AACzC,QAAI,aAAa,IAAI;AACnB,gBAAU,KAAK,MAAM,MAAM;AAC3B;AAAA,IACF;AAEA,cAAU,KAAK,MAAM,QAAQ,QAAQ;AAErC,QAAI,KAAK,WAAW,QAAQ,QAAQ,GAAG;AACrC,YAAM,aAAa,KAAK,QAAQ,OAAO,WAAW,CAAC;AACnD,eAAS,eAAe,KAAK,KAAK,SAAS,aAAa;AACxD;AAAA,IACF;AAEA,UAAM,SAAS,WAAW,MAAM,QAAQ;AACxC,QAAI,WAAW,IAAI;AACjB,gBAAU,WAAW,KAAK,MAAM,QAAQ,CAAC;AACzC;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,MAAM,UAAU,SAAS,CAAC;AAC9C,UAAM,SAAS,SAAS,MAAM;AAC9B,QAAI,CAAC,QAAQ;AACX,eAAS,SAAS;AAClB;AAAA,IACF;AAEA,QAAI,qBAAqB,IAAI,OAAO,IAAI,GAAG;AACzC,YAAM,WAAW,OAAO,UAAU,KAAK,mBAAmB,MAAM,OAAO,MAAM,SAAS,CAAC;AACvF,eAAS,aAAa,KAAK,SAAS,IAAI;AACxC;AAAA,IACF;AAEA,QAAI,aAAa,IAAI,OAAO,IAAI,GAAG;AACjC,UAAI,OAAO,SAAS;AAClB,YAAI,CAAC,UAAU,IAAI,OAAO,IAAI,EAAG,WAAU,KAAK,OAAO,IAAI;AAAA,MAC7D,OAAO;AACL,kBAAU,IAAI,OAAO,IAAI,GAAG,mBAAmB,OAAO,MAAM,OAAO,UAAU,CAAC;AAAA,MAChF;AAAA,IACF;AAEA,aAAS,SAAS;AAAA,EACpB;AAEA,SAAO;AACT;AAEO,SAAS,kBAAkB,MAAc,YAAY,KAAa;AACvE,SAAO,aAAa,IAAI,EACrB,QAAQ,YAAY,GAAG,EACvB,QAAQ,QAAQ,GAAG,EACnB,KAAK,EACL,MAAM,GAAG,SAAS;AACvB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handled-ai/design-system",
3
- "version": "0.18.52",
3
+ "version": "0.19.0-rc.0",
4
4
  "description": "Handled UI component library (shadcn-style, New York)",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.0",
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import React from "react";
3
+ import { render, screen, fireEvent } from "@testing-library/react";
4
+ import { CommentComposer } from "../comment-composer";
5
+
6
+ describe("CommentComposer", () => {
7
+ it("renders the input with the default placeholder", () => {
8
+ render(<CommentComposer onPost={() => {}} />);
9
+ expect(screen.getByPlaceholderText("Add a comment or internal note…")).toBeDefined();
10
+ });
11
+
12
+ it("is collapsed until focused (no footer actions)", () => {
13
+ render(<CommentComposer onPost={() => {}} />);
14
+ expect(screen.queryByText("Comment")).toBeNull();
15
+ fireEvent.focus(screen.getByRole("textbox"));
16
+ expect(screen.getByText("Comment")).toBeDefined();
17
+ expect(screen.getByText(/Internal note/i)).toBeDefined();
18
+ });
19
+
20
+ it("disables Comment until there is non-whitespace text", () => {
21
+ render(<CommentComposer onPost={() => {}} />);
22
+ const input = screen.getByRole("textbox");
23
+ fireEvent.focus(input);
24
+ const btn = screen.getByText("Comment").closest("button")!;
25
+ expect(btn.disabled).toBe(true);
26
+ fireEvent.change(input, { target: { value: " hi " } });
27
+ expect(btn.disabled).toBe(false);
28
+ });
29
+
30
+ it("posts the trimmed text and clears the input", () => {
31
+ const onPost = vi.fn();
32
+ render(<CommentComposer onPost={onPost} />);
33
+ const input = screen.getByRole("textbox") as HTMLTextAreaElement;
34
+ fireEvent.change(input, { target: { value: " please follow up " } });
35
+ fireEvent.click(screen.getByText("Comment").closest("button")!);
36
+ expect(onPost).toHaveBeenCalledWith("please follow up");
37
+ expect(input.value).toBe("");
38
+ });
39
+
40
+ it("posts on ⌘↵ / Ctrl↵", () => {
41
+ const onPost = vi.fn();
42
+ render(<CommentComposer onPost={onPost} />);
43
+ const input = screen.getByRole("textbox");
44
+ fireEvent.change(input, { target: { value: "quick note" } });
45
+ fireEvent.keyDown(input, { key: "Enter", metaKey: true });
46
+ expect(onPost).toHaveBeenCalledWith("quick note");
47
+ });
48
+
49
+ it("does not post empty/whitespace text", () => {
50
+ const onPost = vi.fn();
51
+ render(<CommentComposer onPost={onPost} />);
52
+ const input = screen.getByRole("textbox");
53
+ fireEvent.change(input, { target: { value: " " } });
54
+ fireEvent.keyDown(input, { key: "Enter", metaKey: true });
55
+ expect(onPost).not.toHaveBeenCalled();
56
+ });
57
+ });