@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.
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/case-panel-activity-timeline.d.ts +2 -0
- package/dist/components/case-panel-activity-timeline.js +22 -1
- package/dist/components/case-panel-activity-timeline.js.map +1 -1
- package/dist/components/comment-composer.d.ts +29 -0
- package/dist/components/comment-composer.js +102 -0
- package/dist/components/comment-composer.js.map +1 -0
- package/dist/components/conversation-panel.d.ts +95 -0
- package/dist/components/conversation-panel.js +636 -0
- package/dist/components/conversation-panel.js.map +1 -0
- package/dist/components/data-table-filter.d.ts +18 -1
- package/dist/components/data-table-filter.js +20 -6
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/entity-panel.js +5 -5
- package/dist/components/entity-panel.js.map +1 -1
- package/dist/components/owner-chips.d.ts +59 -0
- package/dist/components/owner-chips.js +256 -0
- package/dist/components/owner-chips.js.map +1 -0
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/timeline-activity.d.ts +7 -0
- package/dist/components/timeline-activity.js +22 -1
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/safe-html.d.ts +11 -0
- package/dist/internal/safe-html.js +222 -0
- package/dist/internal/safe-html.js.map +1 -0
- package/package.json +1 -1
- package/src/components/__tests__/comment-composer.test.tsx +57 -0
- package/src/components/__tests__/conversation-panel.test.tsx +157 -0
- package/src/components/__tests__/data-table-filter.test.tsx +72 -0
- package/src/components/__tests__/entity-metadata-grid.test.tsx +27 -1
- package/src/components/__tests__/owner-chips.test.tsx +100 -0
- package/src/components/__tests__/timeline-activity.test.tsx +55 -0
- package/src/components/case-panel-activity-timeline.tsx +20 -0
- package/src/components/comment-composer.tsx +119 -0
- package/src/components/conversation-panel.tsx +790 -0
- package/src/components/data-table-filter.tsx +53 -10
- package/src/components/entity-panel.tsx +7 -5
- package/src/components/owner-chips.tsx +335 -0
- package/src/components/timeline-activity.tsx +37 -3
- package/src/index.ts +3 -0
- package/src/internal/__tests__/safe-html.test.ts +53 -0
- package/src/internal/safe-html.ts +284 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/conversation-panel.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * conversation-panel.tsx — in-case email-thread reader + reply, for the case\n * panel (\"Email response detected\" hub).\n *\n * v1 scope (WIT-853 / WIT-802): INLINE thread reading + reply only.\n * - A collapsible hub header with a pulse badge whose color reflects state:\n * responded (a reply needs you) / awaiting (sent, waiting) / viewing (read-only).\n * - A list of thread rows; clicking one opens a Gmail-style reader inline,\n * right under the row, with collapsible messages + quoted-history toggle.\n * - Reply / Reply-all composer with a signature toggle and two send paths:\n * Preview→Send (in-app) and \"Open draft in Gmail\" (deep link).\n * - A \"playbook stopped\" banner when the customer's reply halted the sequence,\n * and a read-only notice when the operator is not a thread participant.\n *\n * The bottom-right floating dock / side-by-side compare is intentionally OUT of\n * scope here and tracked separately (WIT-855).\n *\n * Presentational: all data + side effects come from the consumer. Email bodies\n * (`bodyHtml`, `quoted.html`) are sanitized before rendering.\n */\n\nimport * as React from \"react\"\nimport {\n ChevronDown,\n ChevronUp,\n CornerUpLeft,\n CheckCheck,\n MailOpen,\n Reply,\n ReplyAll,\n Eye,\n Send,\n Lock,\n Pause,\n GitMerge,\n Check,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { getInitials } from \"../lib/user-display\"\nimport { BRAND_ICONS } from \"../lib/icons\"\nimport { htmlToTextSnippet, sanitizeHtml } from \"../internal/safe-html\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./avatar\"\nimport { Button } from \"./button\"\nimport { Switch } from \"./switch\"\nimport { Textarea } from \"./textarea\"\nimport { RichTextToolbar } from \"./rich-text-toolbar\"\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogFooter,\n} from \"./dialog\"\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\nexport interface ConvParticipant {\n name: string\n email: string\n avatarUrl?: string | null\n role?: string\n}\n\nexport interface ConvMessage {\n id: string\n direction: \"inbound\" | \"outbound\"\n from: ConvParticipant\n to: ConvParticipant\n /** Absolute timestamp label, e.g. \"Jun 1, 2026, 9:12 AM\". */\n date: string\n /** Relative label, e.g. \"2 days ago\". */\n ago?: string\n receipt?: { kind: \"new\" | \"read\" | \"opened\" | \"sent\"; label: string }\n /** HTML body (preferred). Sanitized by the component before rendering. */\n bodyHtml?: string\n /** Plain-text fallback when `bodyHtml` is absent. */\n body?: string\n /** Quoted prior message, collapsed behind a toggle. Sanitized before rendering. */\n quoted?: { attr: string; html: string }\n}\n\nexport type ConvStatus = \"responded\" | \"awaiting\" | \"viewing\"\n\nexport interface ConversationThread {\n threadId: string\n subject: string\n status: ConvStatus\n /** Relative label for the most recent activity. */\n lastWhen?: string\n contact: ConvParticipant\n cc?: ConvParticipant[]\n /** Set when this thread's reply halted a playbook (terminal). */\n paused?: { playbook: string } | null\n /** false => operator is not a participant; reply disabled (read-only). */\n canReply?: boolean\n messages: ConvMessage[]\n /** Prefilled reply draft body. */\n draft?: string\n /** Signature text appended to replies (plain text). */\n signature?: string\n}\n\nexport interface ConversationReplyPayload {\n threadId: string\n body: string\n includeSignature: boolean\n replyAll: boolean\n}\n\nexport interface ConversationPanelProps {\n threads: ConversationThread[]\n /** Current operator: drives \"to me\" + the reply avatar. */\n me?: ConvParticipant\n /** Deployment brand, used in the paused-playbook copy. */\n tenantName?: string\n onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (payload: ConversationReplyPayload) => void\n onOpenInGmail?: (threadId: string) => void\n /** Inline-open this thread initially (defaults to the first responded one). */\n defaultOpenThreadId?: string\n className?: string\n}\n\n/* ── Shared helpers ──────────────────────────────────────────────────────── */\n\n/** Gmail-like reading-pane typography (mirrors timeline-activity's email mode). */\nconst PROSE = 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)\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")\n}\n\n/** Plain-text -> simple paragraph HTML for the Preview / sent-message body. */\nfunction textToHtml(text: string): string {\n return text\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean)\n .map((p) => `<p>${escapeHtml(p).replace(/\\n/g, \"<br>\")}</p>`)\n .join(\"\")\n}\n\nfunction GmailMark({ size = 14 }: { size?: number }) {\n return (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={BRAND_ICONS.gmail.icon}\n alt=\"Gmail\"\n width={size}\n height={size}\n style={{ width: size, height: size, objectFit: \"contain\", display: \"block\" }}\n />\n )\n}\n\nfunction PersonAvatar({ person, size = \"sm\" }: { person: ConvParticipant; size?: \"sm\" | \"default\" }) {\n return (\n <Avatar size={size}>\n {person.avatarUrl ? <AvatarImage src={person.avatarUrl} alt={person.name} /> : null}\n <AvatarFallback className=\"bg-muted text-muted-foreground text-[10px] font-medium uppercase\">\n {getInitials({ name: person.name, email: person.email })}\n </AvatarFallback>\n </Avatar>\n )\n}\n\nfunction firstName(name: string): string {\n return name.split(\" \")[0] || name\n}\n\nconst STATUS_PILL: Record<ConvStatus, { label: string; cls: string }> = {\n responded: { label: \"New reply\", cls: \"bg-status-active-bg text-status-active-fg border-status-active-border\" },\n awaiting: { label: \"Awaiting\", cls: \"bg-status-pending-bg text-status-pending-fg border-status-pending-border\" },\n viewing: { label: \"Viewing\", cls: \"bg-muted text-muted-foreground border-border\" },\n}\n\nconst STATUS_DOT: Record<ConvStatus, string> = {\n responded: \"bg-status-active-fg\",\n awaiting: \"bg-status-pending-fg\",\n viewing: \"bg-muted-foreground/50\",\n}\n\nfunction effectiveStatus(t: ConversationThread): ConvStatus {\n return t.canReply === false ? \"viewing\" : t.status\n}\n\n/* ── One message (collapsible) ──────────────────────────────────────────── */\n\nfunction MessageView({\n message,\n expanded,\n onToggle,\n me,\n}: {\n message: ConvMessage\n expanded: boolean\n onToggle: () => void\n me?: ConvParticipant\n}) {\n const [quoteOpen, setQuoteOpen] = React.useState(false)\n const snippet =\n message.body?.split(\"\\n\").find(Boolean) ??\n (message.bodyHtml ? htmlToTextSnippet(message.bodyHtml, 140) : \"\")\n\n if (!expanded) {\n return (\n <button\n type=\"button\"\n data-slot=\"conv-message-collapsed\"\n onClick={onToggle}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left hover:bg-muted/40\"\n >\n <PersonAvatar person={message.from} />\n <span className=\"text-muted-foreground min-w-0 flex-1 truncate text-[13px]\">\n <b className=\"text-foreground\">{firstName(message.from.name)}</b> · {snippet}\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{message.ago ?? message.date}</span>\n <ChevronDown size={13} className=\"text-muted-foreground shrink-0\" />\n </button>\n )\n }\n\n const toLabel = me && message.to.email === me.email ? \"me\" : firstName(message.to.name)\n\n return (\n <div data-slot=\"conv-message\" className=\"rounded-md border border-border bg-background\">\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"flex w-full items-start gap-2 px-3 py-2 text-left\"\n >\n <PersonAvatar person={message.from} size=\"default\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex flex-wrap items-baseline gap-x-1.5\">\n <span className=\"text-[13px] font-semibold\">{message.from.name}</span>\n <span className=\"text-muted-foreground/60 truncate text-xs\"><{message.from.email}></span>\n </span>\n <span className=\"text-muted-foreground block text-xs\">\n to <b>{toLabel}</b>\n </span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {message.receipt ? (\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n {message.receipt.kind === \"new\" ? (\n <CornerUpLeft size={11} />\n ) : message.receipt.kind === \"read\" ? (\n <CheckCheck size={11} />\n ) : (\n <MailOpen size={11} />\n )}\n {message.receipt.label}\n </span>\n ) : null}\n <span className=\"text-muted-foreground/60 text-xs\">{message.date}</span>\n <ChevronUp size={13} className=\"text-muted-foreground\" />\n </span>\n </button>\n\n <div className=\"px-3 pb-3\">\n {message.bodyHtml ? (\n <div className={PROSE} dangerouslySetInnerHTML={{ __html: sanitizeHtml(message.bodyHtml) }} />\n ) : (\n <div className={cn(PROSE, \"whitespace-pre-line\")}>{message.body}</div>\n )}\n\n {message.quoted ? (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={() => setQuoteOpen((v) => !v)}\n className=\"text-muted-foreground hover:bg-muted rounded px-1.5 text-xs leading-5\"\n title={quoteOpen ? \"Hide quoted text\" : \"Show quoted text\"}\n >\n •••\n </button>\n {quoteOpen ? (\n <div className=\"border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]\">\n <p className=\"mb-1\" dangerouslySetInnerHTML={{ __html: sanitizeHtml(message.quoted.attr) }} />\n <div className={PROSE} dangerouslySetInnerHTML={{ __html: sanitizeHtml(message.quoted.html) }} />\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n\n/* ── Reply composer ─────────────────────────────────────────────────────── */\n\nfunction ReplyComposer({\n thread,\n me,\n replyAll,\n tenantName,\n onClose,\n onSend,\n onDraft,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n replyAll: boolean\n tenantName?: string\n onClose: () => void\n onSend: (body: string, includeSignature: boolean) => void | Promise<void>\n onDraft: (body: string, includeSignature: boolean) => void\n}) {\n const [body, setBody] = React.useState(thread.draft ?? \"\")\n const [sig, setSig] = React.useState(true)\n const [preview, setPreview] = React.useState(false)\n const [sending, setSending] = React.useState(false)\n const [sendError, setSendError] = React.useState<string | null>(null)\n const ccList = replyAll ? thread.cc ?? [] : []\n const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`\n\n const previewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : \"\")\n\n const handleSend = async () => {\n setSending(true)\n setSendError(null)\n try {\n await onSend(body, sig)\n setPreview(false)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not send this reply. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n return (\n <div data-slot=\"conv-reply\" className=\"border-border bg-muted/20 rounded-md border p-3\">\n <div className=\"mb-2 flex items-center gap-2\">\n {me ? <PersonAvatar person={me} /> : null}\n <span className=\"flex-1 text-[13px] font-medium\">\n {replyAll ? (\n <>\n Reply all{\" \"}\n <span className=\"text-muted-foreground font-normal\">· {1 + ccList.length} recipients</span>\n </>\n ) : (\n <>\n Reply to <b>{firstName(thread.contact.name)}</b>\n </>\n )}\n </span>\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <GitMerge size={11} /> Same thread\n </span>\n <button type=\"button\" onClick={onClose} title=\"Discard reply\" className=\"text-muted-foreground hover:text-foreground\">\n <X size={15} />\n </button>\n </div>\n\n <div className=\"border-border mb-2 space-y-1 border-b pb-2 text-[13px]\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">To</span>\n <span className=\"font-medium\">{thread.contact.name}</span>\n <span className=\"text-muted-foreground/60 truncate text-xs\">{thread.contact.email}</span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"flex items-start gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Cc</span>\n <span className=\"text-muted-foreground text-xs\">\n {ccList.map((c) => c.name).join(\", \")}\n </span>\n </div>\n ) : null}\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Subject</span>\n <span className=\"truncate\">{subject}</span>\n <span className=\"text-muted-foreground/60 ml-auto inline-flex items-center gap-1 text-[11px]\">\n <Lock size={10} /> on thread\n </span>\n </div>\n </div>\n\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Write your reply…\"\n className=\"min-h-28 resize-y text-sm\"\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\") {\n e.preventDefault()\n setPreview(true)\n }\n }}\n />\n\n {sig && thread.signature ? (\n <div className=\"text-muted-foreground mt-2 whitespace-pre-line border-l-2 border-border pl-3 text-[13px]\">\n <span className=\"text-muted-foreground/60 mr-1\">--</span>\n {thread.signature}\n </div>\n ) : null}\n\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <RichTextToolbar />\n <label className=\"text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]\">\n <Switch checked={sig} onCheckedChange={setSig} aria-label=\"Toggle signature\" />\n Signature\n </label>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => setPreview(true)}>\n <Eye size={14} /> Preview\n </Button>\n <Button type=\"button\" size=\"sm\" disabled={sending} onClick={() => setPreview(true)}>\n <Send size={14} /> Send\n </Button>\n </div>\n\n <Dialog open={preview} onOpenChange={(open) => { if (!sending) setPreview(open) }}>\n <DialogContent className=\"max-w-xl\">\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-1.5 text-[15px]\">\n <Eye size={15} /> Preview: this is exactly what sends\n </DialogTitle>\n <DialogDescription>\n Stays on the {subject.replace(/^Re:\\s*/i, \"\")} thread. Gmail keeps it threaded.\n </DialogDescription>\n </DialogHeader>\n <div className=\"border-border space-y-1 rounded-md border p-3 text-[13px]\">\n <div>\n <span className=\"text-muted-foreground\">To </span>\n <b>{thread.contact.name}</b>{\" \"}\n <span className=\"text-muted-foreground/60\"><{thread.contact.email}></span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"text-muted-foreground\">Cc {ccList.map((c) => c.name).join(\", \")}</div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">Subject </span>\n {subject}\n </div>\n </div>\n <div className={cn(PROSE, \"max-h-72 overflow-auto\")} dangerouslySetInnerHTML={{ __html: previewHtml }} />\n {sendError ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {sendError}\n </p>\n ) : null}\n <DialogFooter className=\"sm:justify-between\">\n <button\n type=\"button\"\n disabled={sending}\n onClick={() => {\n setPreview(false)\n onDraft(body, sig)\n }}\n className=\"text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50\"\n >\n <GmailMark size={14} /> Open draft in Gmail\n </button>\n <span className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => setPreview(false)}>\n Keep editing\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={sending}\n onClick={handleSend}\n >\n <Send size={14} /> {sending ? \"Sending...\" : \"Send now\"}\n </Button>\n </span>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n {tenantName ? (\n <p className=\"text-muted-foreground/70 mt-2 text-[11px]\">Sends via Gmail · playbooks stay stopped.</p>\n ) : null}\n </div>\n )\n}\n\n/* ── Thread body (messages + footer/composer/done states) ───────────────── */\n\ntype ThreadMode = \"idle\" | \"replying\" | \"sent\" | \"draft\"\n\nfunction ThreadBody({\n thread,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onOpenInGmail,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n tenantName?: string\n onSendReply?: (p: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (p: ConversationReplyPayload) => void\n onOpenInGmail?: (threadId: string) => void\n}) {\n const canReply = thread.canReply !== false\n const hasCc = !!(thread.cc && thread.cc.length)\n const [mode, setMode] = React.useState<ThreadMode>(\"idle\")\n const [replyAll, setReplyAll] = React.useState(false)\n const [expanded, setExpanded] = React.useState<Record<string, boolean>>(() => {\n const o: Record<string, boolean> = {}\n thread.messages.forEach((m, i) => {\n o[m.id] = i === thread.messages.length - 1\n })\n return o\n })\n\n const toggle = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] }))\n\n return (\n <div data-slot=\"conv-thread-body\" className=\"space-y-2\">\n {canReply && thread.paused ? (\n <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]\">\n <Pause size={13} className=\"mt-0.5 shrink-0\" />\n <span>\n <b>Follow-up actions stopped.</b> Your {thread.paused.playbook} next steps won’t send\n automatically while this conversation is live. Continue it in {tenantName ?? \"the app\"} or Gmail.\n </span>\n </div>\n ) : null}\n\n <div className=\"space-y-1.5\">\n {thread.messages.map((m) => (\n <MessageView key={m.id} message={m} expanded={!!expanded[m.id]} onToggle={() => toggle(m.id)} me={me} />\n ))}\n </div>\n\n {!canReply ? (\n <div className=\"border-border bg-muted/30 text-muted-foreground flex items-start gap-2 rounded-md border p-2.5 text-[12px]\">\n <Eye size={14} className=\"mt-0.5 shrink-0\" />\n <span>\n <b>Viewing only.</b> You’re not a participant on this thread, so replying is disabled here.\n </span>\n </div>\n ) : null}\n\n {canReply && mode === \"idle\" ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button type=\"button\" size=\"sm\" onClick={() => { setReplyAll(false); setMode(\"replying\") }}>\n <Reply size={15} /> Reply\n </Button>\n {hasCc ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => { setReplyAll(true); setMode(\"replying\") }}>\n <ReplyAll size={14} /> Reply all\n </Button>\n ) : null}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => onOpenInGmail?.(thread.threadId)}>\n <GmailMark size={14} /> Open in Gmail\n </Button>\n <span className=\"text-muted-foreground/70 ml-auto inline-flex items-center gap-1 text-[11px]\">\n <GitMerge size={12} /> Stays on this thread\n </span>\n </div>\n ) : null}\n\n {canReply && mode === \"replying\" ? (\n <ReplyComposer\n thread={thread}\n me={me}\n replyAll={replyAll}\n tenantName={tenantName}\n onClose={() => setMode(\"idle\")}\n onSend={async (body, includeSignature) => {\n await onSendReply?.({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"sent\")\n }}\n onDraft={(body, includeSignature) => {\n onCreateGmailDraft?.({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"draft\")\n }}\n />\n ) : null}\n\n {canReply && mode === \"sent\" ? (\n <div className=\"border-status-active-border bg-status-active-bg flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <Check size={16} className=\"text-status-active-fg shrink-0\" />\n <span className=\"flex-1\">\n <b>{replyAll ? \"Reply all sent\" : \"Reply sent\"}</b> · added to the thread. Delivered to{\" \"}\n <b>{thread.contact.name}</b>. This action stays <b>Pending</b>; playbooks remain stopped.\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n\n {canReply && mode === \"draft\" ? (\n <div className=\"border-border bg-muted/30 flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <GmailMark size={16} />\n <span className=\"flex-1\">\n <b>Draft saved to Gmail.</b> Waiting on the <b>Re: {thread.subject}</b> thread; open it there to finish. Nothing was sent.\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => onOpenInGmail?.(thread.threadId)}>\n <GmailMark size={14} /> Open in Gmail\n </Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── A thread row + its inline reader ───────────────────────────────────── */\n\nfunction ThreadRow({\n thread,\n open,\n onToggleOpen,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onOpenInGmail,\n}: {\n thread: ConversationThread\n open: boolean\n onToggleOpen: () => void\n} & Pick<ConversationPanelProps, \"me\" | \"tenantName\" | \"onSendReply\" | \"onCreateGmailDraft\" | \"onOpenInGmail\">) {\n const status = effectiveStatus(thread)\n const last = thread.messages[thread.messages.length - 1]\n const who = last?.direction === \"inbound\" ? firstName(last.from.name) : \"You\"\n const lastSnippet =\n last?.body?.split(\"\\n\").find(Boolean) ??\n (last?.bodyHtml ? htmlToTextSnippet(last.bodyHtml, 120) : \"\")\n const pill = STATUS_PILL[status]\n\n return (\n <div data-slot=\"conv-thread\" data-open={open ? \"true\" : undefined} className=\"border-border border-b last:border-b-0\">\n <button\n type=\"button\"\n onClick={onToggleOpen}\n aria-expanded={open}\n className=\"flex w-full items-center gap-2.5 px-3 py-2.5 text-left hover:bg-muted/30\"\n >\n <span className={cn(\"size-2 shrink-0 rounded-full\", STATUS_DOT[status])} aria-hidden />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span className=\"truncate text-[13px] font-semibold\">{thread.subject}</span>\n <span className={cn(\"shrink-0 rounded border px-1.5 text-[10px] font-medium leading-4\", pill.cls)}>\n {pill.label}\n </span>\n </span>\n <span className=\"text-muted-foreground block truncate text-xs\">\n <b className=\"text-foreground/80\">{thread.contact.name}</b> · {who}: {lastSnippet}\n </span>\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{thread.lastWhen}</span>\n {open ? (\n <ChevronUp size={15} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={15} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n\n {open ? (\n <div className=\"px-3 pb-3\">\n <ThreadBody\n thread={thread}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onOpenInGmail={onOpenInGmail}\n />\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── The hub ─────────────────────────────────────────────────────────────── */\n\nfunction ConversationPanel({\n threads,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onOpenInGmail,\n defaultOpenThreadId,\n className,\n}: ConversationPanelProps) {\n const responded = threads.filter((t) => t.status === \"responded\" && t.canReply !== false).length\n const awaiting = threads.filter((t) => effectiveStatus(t) === \"awaiting\").length\n const anyPaused = threads.some((t) => t.paused)\n\n const [hubOpen, setHubOpen] = React.useState(true)\n const [openId, setOpenId] = React.useState<string | null>(() => {\n if (defaultOpenThreadId) return defaultOpenThreadId\n const firstResponded = threads.find((t) => t.status === \"responded\" && t.canReply !== false)\n return firstResponded ? firstResponded.threadId : null\n })\n\n if (!threads.length) return null\n\n // Header badge state: a responded reply leads; else an awaiting state; else neutral.\n const badge =\n responded > 0\n ? { label: \"Email response detected\", dot: \"bg-status-active-fg\", ring: \"bg-status-active-fg/30\" }\n : awaiting > 0\n ? { label: \"Awaiting response\", dot: \"bg-status-pending-fg\", ring: \"bg-status-pending-fg/30\" }\n : { label: \"Conversations\", dot: \"bg-muted-foreground/50\", ring: \"bg-muted-foreground/20\" }\n\n const headTitle =\n responded > 0\n ? `${responded} ${responded === 1 ? \"reply needs\" : \"replies need\"} your response`\n : awaiting > 0\n ? `Awaiting response on ${awaiting} ${awaiting === 1 ? \"thread\" : \"threads\"}`\n : `${threads.length} email ${threads.length === 1 ? \"thread\" : \"threads\"}`\n\n return (\n <section\n data-slot=\"conversation-panel\"\n data-responded={responded > 0 ? \"true\" : undefined}\n className={cn(\"border-border bg-background overflow-hidden rounded-xl border\", className)}\n >\n <button\n type=\"button\"\n onClick={() => setHubOpen((v) => !v)}\n aria-expanded={hubOpen}\n className=\"flex w-full items-center gap-3 px-3 py-2.5 text-left\"\n >\n <span\n data-slot=\"conversation-badge\"\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\",\n responded > 0\n ? \"bg-status-active-bg text-status-active-fg\"\n : awaiting > 0\n ? \"bg-status-pending-bg text-status-pending-fg\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n <span className=\"relative inline-flex size-2\">\n <span className={cn(\"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75\", badge.ring)} />\n <span className={cn(\"relative inline-flex size-2 rounded-full\", badge.dot)} />\n </span>\n {badge.label}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-[13px] font-semibold\">{headTitle}</span>\n <span className=\"text-muted-foreground block truncate text-xs\">\n {threads.length} {threads.length === 1 ? \"thread\" : \"threads\"} on this action\n {anyPaused ? <> · <b>playbook stopped</b></> : null}\n </span>\n </span>\n {hubOpen ? (\n <ChevronUp size={16} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={16} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n\n {hubOpen ? (\n <div className=\"border-border border-t\">\n {threads.map((t) => (\n <ThreadRow\n key={t.threadId}\n thread={t}\n open={openId === t.threadId}\n onToggleOpen={() => setOpenId((cur) => (cur === t.threadId ? null : t.threadId))}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onOpenInGmail={onOpenInGmail}\n />\n ))}\n </div>\n ) : null}\n </section>\n )\n}\n\nexport { ConversationPanel }\n"],"mappings":"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","names":[]}
|
|
@@ -24,6 +24,14 @@ interface DataTableOptionFilterCategory extends DataTableFilterCategoryBase {
|
|
|
24
24
|
options: (string | FilterOption)[];
|
|
25
25
|
/** Filter behavior. Defaults to "multi" (checkbox multi-select). */
|
|
26
26
|
type?: "multi" | "single" | "boolean";
|
|
27
|
+
/**
|
|
28
|
+
* When true, the submenu search box is parent-driven: typing fires
|
|
29
|
+
* `onOptionSearch(categoryId, query)` and the parent is responsible for
|
|
30
|
+
* supplying the matching `options` (e.g. a server-backed lookup over a large
|
|
31
|
+
* set). The built-in client-side option filtering is skipped, the search box
|
|
32
|
+
* is always shown, and `optionSearchLoading[categoryId]` drives a loading row.
|
|
33
|
+
*/
|
|
34
|
+
remoteSearch?: boolean;
|
|
27
35
|
}
|
|
28
36
|
interface DataTableTextFilterCategory extends DataTableFilterCategoryBase {
|
|
29
37
|
/** Free-text filter behavior. Renders a top-level submenu with a text input. */
|
|
@@ -59,7 +67,16 @@ interface DataTableFilterProps {
|
|
|
59
67
|
textFilters?: Record<string, string>;
|
|
60
68
|
/** Callback when a free-text filter value is applied or cleared. */
|
|
61
69
|
onTextFilterChange?: (categoryId: string, value: string) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Fired when the submenu search input changes for a category with
|
|
72
|
+
* `remoteSearch: true`. The parent should debounce, fetch matching options,
|
|
73
|
+
* and feed them back via that category's `options`. The empty string is sent
|
|
74
|
+
* when the box is cleared (or the submenu re-opens) so the parent can reset.
|
|
75
|
+
*/
|
|
76
|
+
onOptionSearch?: (categoryId: string, query: string) => void;
|
|
77
|
+
/** Per-category loading state for remote option search, keyed by category id. */
|
|
78
|
+
optionSearchLoading?: Record<string, boolean>;
|
|
62
79
|
}
|
|
63
|
-
declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, className, optionSearchThreshold, presetFilters, onTogglePreset, presetLabel, conditionFields, conditionFilters, onConditionFiltersChange, conditionBuilderLabel, textFilters, onTextFilterChange, }: DataTableFilterProps): React.JSX.Element;
|
|
80
|
+
declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, className, optionSearchThreshold, presetFilters, onTogglePreset, presetLabel, conditionFields, conditionFilters, onConditionFiltersChange, conditionBuilderLabel, textFilters, onTextFilterChange, onOptionSearch, optionSearchLoading, }: DataTableFilterProps): React.JSX.Element;
|
|
64
81
|
|
|
65
82
|
export { DataTableFilter, type DataTableFilterCategory, type DataTableFilterProps, type DataTableOptionFilterCategory, type DataTableTextFilterCategory, type FilterOption };
|
|
@@ -163,7 +163,9 @@ function DataTableFilter({
|
|
|
163
163
|
onConditionFiltersChange,
|
|
164
164
|
conditionBuilderLabel = "Add filter",
|
|
165
165
|
textFilters = {},
|
|
166
|
-
onTextFilterChange
|
|
166
|
+
onTextFilterChange,
|
|
167
|
+
onOptionSearch,
|
|
168
|
+
optionSearchLoading = {}
|
|
167
169
|
}) {
|
|
168
170
|
const [query, setQuery] = React.useState("");
|
|
169
171
|
const [subQueries, setSubQueries] = React.useState({});
|
|
@@ -291,15 +293,17 @@ function DataTableFilter({
|
|
|
291
293
|
category.id
|
|
292
294
|
);
|
|
293
295
|
}
|
|
296
|
+
const isRemote = category.remoteSearch === true;
|
|
294
297
|
const subQuery = ((_e = subQueries[category.id]) != null ? _e : "").trim().toLowerCase();
|
|
295
|
-
const filteredOptions = subQuery ? category.options.filter(
|
|
298
|
+
const filteredOptions = isRemote || !subQuery ? category.options : category.options.filter(
|
|
296
299
|
(opt) => getOptionLabel(opt).toLowerCase().includes(subQuery)
|
|
297
|
-
)
|
|
298
|
-
const shouldShowSubmenuSearch = shouldShowOptionSearch(
|
|
300
|
+
);
|
|
301
|
+
const shouldShowSubmenuSearch = isRemote || shouldShowOptionSearch(
|
|
299
302
|
category.searchable,
|
|
300
303
|
category.options.length,
|
|
301
304
|
optionSearchThreshold
|
|
302
305
|
);
|
|
306
|
+
const isSearching = optionSearchLoading[category.id] === true;
|
|
303
307
|
return /* @__PURE__ */ jsxs(
|
|
304
308
|
DropdownMenuSub,
|
|
305
309
|
{
|
|
@@ -310,6 +314,7 @@ function DataTableFilter({
|
|
|
310
314
|
delete next[category.id];
|
|
311
315
|
return next;
|
|
312
316
|
});
|
|
317
|
+
if (isRemote) onOptionSearch == null ? void 0 : onOptionSearch(category.id, "");
|
|
313
318
|
}
|
|
314
319
|
},
|
|
315
320
|
children: [
|
|
@@ -326,7 +331,11 @@ function DataTableFilter({
|
|
|
326
331
|
className: "h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted",
|
|
327
332
|
placeholder: "Search...",
|
|
328
333
|
value: (_f = subQueries[category.id]) != null ? _f : "",
|
|
329
|
-
onChange: (e) =>
|
|
334
|
+
onChange: (e) => {
|
|
335
|
+
const next = e.target.value;
|
|
336
|
+
setSubQueries((prev) => __spreadProps(__spreadValues({}, prev), { [category.id]: next }));
|
|
337
|
+
if (isRemote) onOptionSearch == null ? void 0 : onOptionSearch(category.id, next);
|
|
338
|
+
},
|
|
330
339
|
onClick: (e) => e.stopPropagation(),
|
|
331
340
|
onKeyDown: (e) => {
|
|
332
341
|
const navKeys = ["ArrowDown", "ArrowUp", "Enter", "Escape", "Tab"];
|
|
@@ -359,7 +368,12 @@ function DataTableFilter({
|
|
|
359
368
|
value
|
|
360
369
|
);
|
|
361
370
|
}),
|
|
362
|
-
|
|
371
|
+
isSearching ? /* @__PURE__ */ jsx("div", { className: "p-2 text-center text-xs text-muted-foreground", children: "Searching\u2026" }) : filteredOptions.length === 0 ? (() => {
|
|
372
|
+
var _a2;
|
|
373
|
+
const typed = ((_a2 = subQueries[category.id]) != null ? _a2 : "").trim().length > 0;
|
|
374
|
+
const message = isRemote ? typed ? "No matches" : "Type to search" : category.options.length > 0 ? "No matches" : null;
|
|
375
|
+
return message ? /* @__PURE__ */ jsx("div", { className: "p-2 text-center text-xs text-muted-foreground", children: message }) : null;
|
|
376
|
+
})() : null
|
|
363
377
|
] })
|
|
364
378
|
]
|
|
365
379
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ListFilter, Plus, Search } from \"lucide-react\"\n\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DataTableConditionFilter,\n shouldShowOptionSearch,\n type ConditionFieldDef,\n type ConditionFilterValue,\n} from \"./data-table-condition-filter\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\ninterface DataTableFilterCategoryBase {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n /**\n * Submenu search behavior. Defaults to the DataTableFilter\n * optionSearchThreshold prop. Use true to always show search or false to\n * hide it for a specific category.\n */\n searchable?: boolean | { threshold?: number }\n}\n\nexport interface DataTableOptionFilterCategory extends DataTableFilterCategoryBase {\n options: (string | FilterOption)[]\n /** Filter behavior. Defaults to \"multi\" (checkbox multi-select). */\n type?: \"multi\" | \"single\" | \"boolean\"\n}\n\nexport interface DataTableTextFilterCategory extends DataTableFilterCategoryBase {\n /** Free-text filter behavior. Renders a top-level submenu with a text input. */\n type: \"text\"\n /** Placeholder shown in the text filter input. */\n valuePlaceholder?: string\n /** Not used for text filters; optional for backwards-compatible category shapes. */\n options?: (string | FilterOption)[]\n}\n\nexport type DataTableFilterCategory =\n | DataTableOptionFilterCategory\n | DataTableTextFilterCategory\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\nfunction isTextFilterCategory(\n category: DataTableFilterCategory\n): category is DataTableTextFilterCategory {\n return category.type === \"text\"\n}\n\nfunction TextFilterSubmenu({\n category,\n value,\n onValueChange,\n}: {\n category: DataTableTextFilterCategory\n value: string\n onValueChange?: (categoryId: string, value: string) => void\n}) {\n const [draftValue, setDraftValue] = React.useState(value)\n\n React.useEffect(() => {\n setDraftValue(value)\n }, [value])\n\n const active = value.trim().length > 0\n const applyValue = React.useCallback(() => {\n onValueChange?.(category.id, draftValue.trim())\n }, [category.id, draftValue, onValueChange])\n\n return (\n <DropdownMenuSub\n onOpenChange={(open) => {\n if (!open) {\n setDraftValue(value)\n }\n }}\n >\n <DropdownMenuSubTrigger\n className={cn(\n \"cursor-pointer py-1.5 text-xs\",\n active && \"text-brand-purple\"\n )}\n >\n <category.icon\n className={cn(\n \"mr-2 h-3.5 w-3.5 text-muted-foreground\",\n active && \"text-brand-purple\"\n )}\n />\n {category.label}\n {active ? <Check className=\"ml-auto h-4 w-4\" /> : null}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"w-64 p-2\">\n <div className=\"space-y-2\">\n <input\n aria-label={category.label}\n className=\"h-8 w-full rounded-md bg-muted/50 px-2 py-1 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder={\n category.valuePlaceholder ??\n `Enter ${category.label.toLowerCase()}...`\n }\n value={draftValue}\n onChange={(event) => setDraftValue(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => {\n event.stopPropagation()\n if (event.key === \"Enter\") {\n event.preventDefault()\n applyValue()\n }\n }}\n />\n <div className=\"flex items-center justify-end gap-2\">\n {active ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n setDraftValue(\"\")\n onValueChange?.(category.id, \"\")\n }}\n >\n Clear\n </Button>\n ) : null}\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n applyValue()\n }}\n >\n Apply\n </Button>\n </div>\n </div>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n}\n\nexport interface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n /** Minimum number of options before showing the sub-menu search input. Defaults to 8. */\n optionSearchThreshold?: number\n /** Filters applied by default. Shown as distinct chips that can be toggled off but not dismissed. */\n presetFilters?: Record<string, string[]>\n /** Callback when a preset filter is toggled on/off. */\n onTogglePreset?: (categoryId: string, option: string) => void\n /** Label shown on preset chips to distinguish from user-applied filters. Default: \"Default\". */\n presetLabel?: string\n /** Fields exposed in the unified condition-builder panel. */\n conditionFields?: ConditionFieldDef[]\n /** Active builder-managed field/operator/value conditions. */\n conditionFilters?: ConditionFilterValue[]\n /** Callback when builder-managed conditions are applied, removed, or cleared. */\n onConditionFiltersChange?: (conditions: ConditionFilterValue[]) => void\n /** Dropdown entry label for the condition-builder panel. Default: \"Add filter\". */\n conditionBuilderLabel?: string\n /** Active free-text filters keyed by category id. */\n textFilters?: Record<string, string>\n /** Callback when a free-text filter value is applied or cleared. */\n onTextFilterChange?: (categoryId: string, value: string) => void\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n optionSearchThreshold = 8,\n presetFilters,\n onTogglePreset,\n presetLabel = \"Default\",\n conditionFields = [],\n conditionFilters = [],\n onConditionFiltersChange,\n conditionBuilderLabel = \"Add filter\",\n textFilters = {},\n onTextFilterChange,\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n const [conditionBuilderOpen, setConditionBuilderOpen] = React.useState(false)\n const hasConditionBuilder = conditionFields.length > 0\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n if (isTextFilterCategory(category)) {\n return false\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n /** Check if a specific option is a preset filter */\n const isPresetOption = React.useCallback(\n (categoryId: string, value: string): boolean => {\n return presetFilters?.[categoryId]?.includes(value) ?? false\n },\n [presetFilters]\n )\n\n const activeCount = React.useMemo(() => {\n const userCount = Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n )\n\n const textCount = categories.reduce((count, category) => {\n if (!isTextFilterCategory(category)) {\n return count\n }\n\n return textFilters[category.id]?.trim() ? count + 1 : count\n }, 0)\n\n return userCount + conditionFilters.length + textCount\n }, [categories, selectedFilters, conditionFilters.length, textFilters])\n\n /** Collect all preset chips to render */\n const presetChips = React.useMemo(() => {\n if (!presetFilters) return []\n\n const chips: { categoryId: string; value: string; label: string; active: boolean }[] = []\n\n for (const [categoryId, values] of Object.entries(presetFilters)) {\n const category = categories.find((c) => c.id === categoryId)\n for (const value of values) {\n const option = category && !isTextFilterCategory(category)\n ? category.options.find((opt) => getOptionValue(opt) === value)\n : undefined\n const label = option ? getOptionLabel(option) : value\n const active = selectedFilters[categoryId]?.includes(value) ?? false\n chips.push({ categoryId, value, label, active })\n }\n }\n\n return chips\n }, [presetFilters, categories, selectedFilters])\n\n const triggerButton = (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold text-foreground\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const filterType = category.type ?? \"multi\"\n\n /* ── Boolean toggle ─────────────────────────────────── */\n if (filterType === \"boolean\") {\n const active = selectedFilters[category.id]?.includes(\"true\") ?? false\n return (\n <DropdownMenuItem\n key={category.id}\n className={cn(\n \"cursor-pointer py-1.5 text-xs\",\n active && \"text-brand-purple\"\n )}\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, \"true\")\n }}\n >\n <category.icon className=\"mr-2 h-3.5 w-3.5\" />\n {category.label}\n {active ? <Check className=\"ml-auto h-4 w-4\" /> : null}\n </DropdownMenuItem>\n )\n }\n\n /* ── Free-text submenu ───────────────────────────────── */\n if (isTextFilterCategory(category)) {\n return (\n <TextFilterSubmenu\n key={category.id}\n category={category}\n value={textFilters[category.id] ?? \"\"}\n onValueChange={onTextFilterChange}\n />\n )\n }\n\n /* ── Sub-menu (single / multi) ──────────────────────── */\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n const filteredOptions = subQuery\n ? category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n : category.options\n const shouldShowSubmenuSearch = shouldShowOptionSearch(\n category.searchable,\n category.options.length,\n optionSearchThreshold,\n )\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — shown for long lists or categories that opt in. */}\n {shouldShowSubmenuSearch && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) =>\n setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))\n }\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n // Allow navigation keys to propagate to Radix menu handling\n // so keyboard users can move to and select filtered options.\n const navKeys = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Escape\", \"Tab\"]\n if (!navKeys.includes(e.key)) {\n e.stopPropagation()\n }\n }}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n const isPreset = isPresetOption(category.id, value)\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n isPreset ? (\n <span className=\"text-brand-purple text-[10px] font-semibold\">\n {presetLabel}\n </span>\n ) : filterType === \"single\" ? (\n <span className=\"h-1.5 w-1.5 rounded-full bg-current\" />\n ) : (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n )\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {filteredOptions.length === 0 && category.options.length > 0 && (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No matches\n </div>\n )}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n\n {hasConditionBuilder ? (\n <div className=\"border-t border-border p-1\">\n <PopoverPrimitive.Root\n open={conditionBuilderOpen}\n onOpenChange={setConditionBuilderOpen}\n >\n <PopoverPrimitive.Trigger asChild>\n <DropdownMenuItem\n className=\"cursor-pointer py-1.5 text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n setConditionBuilderOpen(true)\n }}\n >\n <Plus className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {conditionBuilderLabel}\n {conditionFilters.length > 0 ? (\n <span className=\"ml-auto rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {conditionFilters.length}\n </span>\n ) : null}\n </DropdownMenuItem>\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align=\"start\"\n side=\"right\"\n sideOffset={8}\n className=\"z-50 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\"\n onEscapeKeyDown={() => setConditionBuilderOpen(false)}\n onInteractOutside={(event) => {\n const target = event.target as HTMLElement | null\n if (target?.closest('[data-slot=\"dropdown-menu-content\"]')) {\n event.preventDefault()\n }\n }}\n >\n <DataTableConditionFilter\n fields={conditionFields}\n conditions={conditionFilters}\n onConditionsChange={onConditionFiltersChange ?? (() => {})}\n />\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n </div>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n\n // If there are preset chips, wrap trigger + chips together\n if (presetChips.length > 0) {\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {triggerButton}\n {presetChips.map((chip) => (\n <button\n key={`${chip.categoryId}-${chip.value}`}\n type=\"button\"\n onClick={() => onTogglePreset?.(chip.categoryId, chip.value)}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium transition-colors\",\n chip.active\n ? \"border-dashed border-brand-purple/30 bg-brand-purple/5 text-brand-purple/80\"\n : \"border-border/40 bg-transparent text-muted-foreground/60 line-through\"\n )}\n >\n <span className=\"text-brand-purple/50 text-[10px]\">\n {presetLabel}:{\" \"}\n </span>\n {chip.label}\n </button>\n ))}\n </div>\n )\n }\n\n return triggerButton\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsGM,SAME,KANF;AApGN,YAAY,WAAW;AACvB,SAAS,OAAO,YAAY,MAAM,cAAc;AAEhD,SAAS,WAAW,wBAAwB;AAE5C,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAsCP,SAAS,eAAe,QAAuC;AAC7D,SAAO,OAAO,WAAW,WAAW,SAAS,OAAO;AACtD;AACA,SAAS,eAAe,QAAuC;AAC7D,SAAO,OAAO,WAAW,WAAW,SAAS,OAAO;AACtD;AAEA,SAAS,qBACP,UACyC;AACzC,SAAO,SAAS,SAAS;AAC3B;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AAlFH;AAmFE,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AAExD,QAAM,UAAU,MAAM;AACpB,kBAAc,KAAK;AAAA,EACrB,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,SAAS,MAAM,KAAK,EAAE,SAAS;AACrC,QAAM,aAAa,MAAM,YAAY,MAAM;AACzC,mDAAgB,SAAS,IAAI,WAAW,KAAK;AAAA,EAC/C,GAAG,CAAC,SAAS,IAAI,YAAY,aAAa,CAAC;AAE3C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAc,CAAC,SAAS;AACtB,YAAI,CAAC,MAAM;AACT,wBAAc,KAAK;AAAA,QACrB;AAAA,MACF;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,UAAU;AAAA,YACZ;AAAA,YAEA;AAAA;AAAA,gBAAC,SAAS;AAAA,gBAAT;AAAA,kBACC,WAAW;AAAA,oBACT;AAAA,oBACA,UAAU;AAAA,kBACZ;AAAA;AAAA,cACF;AAAA,cACC,SAAS;AAAA,cACT,SAAS,oBAAC,SAAM,WAAU,mBAAkB,IAAK;AAAA;AAAA;AAAA,QACpD;AAAA,QACA,oBAAC,0BAAuB,WAAU,YAChC,+BAAC,SAAI,WAAU,aACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,cAAY,SAAS;AAAA,cACrB,WAAU;AAAA,cACV,cACE,cAAS,qBAAT,YACA,SAAS,SAAS,MAAM,YAAY,CAAC;AAAA,cAEvC,OAAO;AAAA,cACP,UAAU,CAAC,UAAU,cAAc,MAAM,OAAO,KAAK;AAAA,cACrD,SAAS,CAAC,UAAU,MAAM,gBAAgB;AAAA,cAC1C,WAAW,CAAC,UAAU;AACpB,sBAAM,gBAAgB;AACtB,oBAAI,MAAM,QAAQ,SAAS;AACzB,wBAAM,eAAe;AACrB,6BAAW;AAAA,gBACb;AAAA,cACF;AAAA;AAAA,UACF;AAAA,UACA,qBAAC,SAAI,WAAU,uCACZ;AAAA,qBACC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,SAAS,CAAC,UAAU;AAClB,wBAAM,eAAe;AACrB,wBAAM,gBAAgB;AACtB,gCAAc,EAAE;AAChB,iEAAgB,SAAS,IAAI;AAAA,gBAC/B;AAAA,gBACD;AAAA;AAAA,YAED,IACE;AAAA,YACJ;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,SAAS,CAAC,UAAU;AAClB,wBAAM,eAAe;AACrB,wBAAM,gBAAgB;AACtB,6BAAW;AAAA,gBACb;AAAA,gBACD;AAAA;AAAA,YAED;AAAA,aACF;AAAA,WACF,GACF;AAAA;AAAA;AAAA,EACF;AAEJ;AA6BO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,wBAAwB;AAAA,EACxB;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,kBAAkB,CAAC;AAAA,EACnB,mBAAmB,CAAC;AAAA,EACpB;AAAA,EACA,wBAAwB;AAAA,EACxB,cAAc,CAAC;AAAA,EACf;AACF,GAAyB;AACvB,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAiC,CAAC,CAAC;AAC7E,QAAM,CAAC,sBAAsB,uBAAuB,IAAI,MAAM,SAAS,KAAK;AAC5E,QAAM,sBAAsB,gBAAgB,SAAS;AAErD,QAAM,oBAAoB,MAAM,QAAQ,MAAM;AAC5C,UAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AAEA,WAAO,WAAW,OAAO,CAAC,aAAa;AACrC,UAAI,SAAS,MAAM,YAAY,EAAE,SAAS,UAAU,GAAG;AACrD,eAAO;AAAA,MACT;AAEA,UAAI,qBAAqB,QAAQ,GAAG;AAClC,eAAO;AAAA,MACT;AAEA,aAAO,SAAS,QAAQ;AAAA,QAAK,CAAC,WAC5B,eAAe,MAAM,EAAE,YAAY,EAAE,SAAS,UAAU;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,YAAY,KAAK,CAAC;AAGtB,QAAM,iBAAiB,MAAM;AAAA,IAC3B,CAAC,YAAoB,UAA2B;AApPpD;AAqPM,cAAO,0DAAgB,gBAAhB,mBAA6B,SAAS,WAAtC,YAAgD;AAAA,IACzD;AAAA,IACA,CAAC,aAAa;AAAA,EAChB;AAEA,QAAM,cAAc,MAAM,QAAQ,MAAM;AACtC,UAAM,YAAY,OAAO,OAAO,eAAe,EAAE;AAAA,MAC/C,CAAC,OAAO,aAAa,QAAQ,SAAS;AAAA,MACtC;AAAA,IACF;AAEA,UAAM,YAAY,WAAW,OAAO,CAAC,OAAO,aAAa;AAhQ7D;AAiQM,UAAI,CAAC,qBAAqB,QAAQ,GAAG;AACnC,eAAO;AAAA,MACT;AAEA,eAAO,iBAAY,SAAS,EAAE,MAAvB,mBAA0B,UAAS,QAAQ,IAAI;AAAA,IACxD,GAAG,CAAC;AAEJ,WAAO,YAAY,iBAAiB,SAAS;AAAA,EAC/C,GAAG,CAAC,YAAY,iBAAiB,iBAAiB,QAAQ,WAAW,CAAC;AAGtE,QAAM,cAAc,MAAM,QAAQ,MAAM;AA5Q1C;AA6QI,QAAI,CAAC,cAAe,QAAO,CAAC;AAE5B,UAAM,QAAiF,CAAC;AAExF,eAAW,CAAC,YAAY,MAAM,KAAK,OAAO,QAAQ,aAAa,GAAG;AAChE,YAAM,WAAW,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,UAAU;AAC3D,iBAAW,SAAS,QAAQ;AAC1B,cAAM,SAAS,YAAY,CAAC,qBAAqB,QAAQ,IACrD,SAAS,QAAQ,KAAK,CAAC,QAAQ,eAAe,GAAG,MAAM,KAAK,IAC5D;AACJ,cAAM,QAAQ,SAAS,eAAe,MAAM,IAAI;AAChD,cAAM,UAAS,2BAAgB,UAAU,MAA1B,mBAA6B,SAAS,WAAtC,YAAgD;AAC/D,cAAM,KAAK,EAAE,YAAY,OAAO,OAAO,OAAO,CAAC;AAAA,MACjD;AAAA,IACF;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,eAAe,YAAY,eAAe,CAAC;AAE/C,QAAM,gBACJ,qBAAC,gBACC;AAAA,wBAAC,uBAAoB,SAAO,MAC1B;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEA;AAAA,8BAAC,cAAW,WAAU,eAAc;AAAA,UAAE;AAAA,UAErC,cAAc,IACb,oBAAC,UAAK,WAAU,0EACb,uBACH,IACE;AAAA;AAAA;AAAA,IACN,GACF;AAAA,IACA,qBAAC,uBAAoB,OAAM,SAAQ,WAAU,iBAC3C;AAAA,0BAAC,SAAI,WAAU,2DACb,+BAAC,SAAI,WAAU,YACb;AAAA,4BAAC,UAAO,WAAU,8EAA6E;AAAA,QAC/F;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,aAAY;AAAA,YACZ,OAAO;AAAA,YACP,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,YAChD,SAAS,CAAC,UAAU,MAAM,gBAAgB;AAAA,YAC1C,WAAW,CAAC,UAAU,MAAM,gBAAgB;AAAA;AAAA,QAC9C;AAAA,SACF,GACF;AAAA,MAEA,qBAAC,SAAI,WAAU,qCACZ;AAAA,0BAAkB,IAAI,CAAC,aAAa;AApU/C;AAqUY,gBAAM,cAAa,cAAS,SAAT,YAAiB;AAGpC,cAAI,eAAe,WAAW;AAC5B,kBAAM,UAAS,2BAAgB,SAAS,EAAE,MAA3B,mBAA8B,SAAS,YAAvC,YAAkD;AACjE,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAW;AAAA,kBACT;AAAA,kBACA,UAAU;AAAA,gBACZ;AAAA,gBACA,UAAU,CAAC,UAAU;AACnB,wBAAM,eAAe;AACrB,iCAAe,SAAS,IAAI,MAAM;AAAA,gBACpC;AAAA,gBAEA;AAAA,sCAAC,SAAS,MAAT,EAAc,WAAU,oBAAmB;AAAA,kBAC3C,SAAS;AAAA,kBACT,SAAS,oBAAC,SAAM,WAAU,mBAAkB,IAAK;AAAA;AAAA;AAAA,cAZ7C,SAAS;AAAA,YAahB;AAAA,UAEJ;AAGA,cAAI,qBAAqB,QAAQ,GAAG;AAClC,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC;AAAA,gBACA,QAAO,iBAAY,SAAS,EAAE,MAAvB,YAA4B;AAAA,gBACnC,eAAe;AAAA;AAAA,cAHV,SAAS;AAAA,YAIhB;AAAA,UAEJ;AAGA,gBAAM,aAAY,gBAAW,SAAS,EAAE,MAAtB,YAA2B,IAAI,KAAK,EAAE,YAAY;AACpE,gBAAM,kBAAkB,WACpB,SAAS,QAAQ;AAAA,YAAO,CAAC,QACvB,eAAe,GAAG,EAAE,YAAY,EAAE,SAAS,QAAQ;AAAA,UACrD,IACA,SAAS;AACb,gBAAM,0BAA0B;AAAA,YAC9B,SAAS;AAAA,YACT,SAAS,QAAQ;AAAA,YACjB;AAAA,UACF;AAEA,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,cAAc,CAAC,SAAS;AACtB,oBAAI,CAAC,MAAM;AACT,gCAAc,CAAC,SAAS;AACtB,0BAAM,OAAO,mBAAK;AAClB,2BAAO,KAAK,SAAS,EAAE;AACvB,2BAAO;AAAA,kBACT,CAAC;AAAA,gBACH;AAAA,cACF;AAAA,cAEA;AAAA,qCAAC,0BAAuB,WAAU,iCAChC;AAAA,sCAAC,SAAS,MAAT,EAAc,WAAU,0CAAyC;AAAA,kBACjE,SAAS;AAAA,mBACZ;AAAA,gBACA,qBAAC,0BAAuB,WAAU,0CAE/B;AAAA,6CACC,oBAAC,SAAI,WAAU,6DACb,+BAAC,SAAI,WAAU,YACb;AAAA,wCAAC,UAAO,WAAU,0EAAyE;AAAA,oBAC3F;AAAA,sBAAC;AAAA;AAAA,wBACC,WAAU;AAAA,wBACV,aAAY;AAAA,wBACZ,QAAO,gBAAW,SAAS,EAAE,MAAtB,YAA2B;AAAA,wBAClC,UAAU,CAAC,MACT,cAAc,CAAC,SAAU,iCAAK,OAAL,EAAW,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,MAAM,EAAE;AAAA,wBAEtE,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,wBAClC,WAAW,CAAC,MAAM;AAGhB,gCAAM,UAAU,CAAC,aAAa,WAAW,SAAS,UAAU,KAAK;AACjE,8BAAI,CAAC,QAAQ,SAAS,EAAE,GAAG,GAAG;AAC5B,8BAAE,gBAAgB;AAAA,0BACpB;AAAA,wBACF;AAAA;AAAA,oBACF;AAAA,qBACF,GACF;AAAA,kBAGD,gBAAgB,IAAI,CAAC,WAAW;AAlanD,wBAAAA,KAAAC;AAmaoB,0BAAM,QAAQ,eAAe,MAAM;AACnC,0BAAM,QAAQ,eAAe,MAAM;AACnC,0BAAM,YAAWA,OAAAD,MAAA,gBAAgB,SAAS,EAAE,MAA3B,gBAAAA,IAA8B,SAAS,WAAvC,OAAAC,MAAiD;AAClE,0BAAM,WAAW,eAAe,SAAS,IAAI,KAAK;AAClD,2BACE;AAAA,sBAAC;AAAA;AAAA,wBAEC,WAAU;AAAA,wBACV,UAAU,CAAC,UAAU;AACnB,gCAAM,eAAe;AACrB,yCAAe,SAAS,IAAI,KAAK;AAAA,wBACnC;AAAA,wBAEC;AAAA;AAAA,0BACA,WACC,WACE,oBAAC,UAAK,WAAU,+CACb,uBACH,IACE,eAAe,WACjB,oBAAC,UAAK,WAAU,uCAAsC,IAEtD,oBAAC,UAAK,WAAU,+CAA8C,qBAE9D,IAEA;AAAA;AAAA;AAAA,sBApBC;AAAA,oBAqBP;AAAA,kBAEJ,CAAC;AAAA,kBACA,gBAAgB,WAAW,KAAK,SAAS,QAAQ,SAAS,KACzD,oBAAC,SAAI,WAAU,iDAAgD,wBAE/D;AAAA,mBAEJ;AAAA;AAAA;AAAA,YA9EK,SAAS;AAAA,UA+EhB;AAAA,QAEJ,CAAC;AAAA,QAEA,kBAAkB,WAAW,IAC5B,oBAAC,SAAI,WAAU,iDAAgD,8BAE/D,IACE;AAAA,SACN;AAAA,MAEC,sBACC,oBAAC,SAAI,WAAU,8BACb;AAAA,QAAC,iBAAiB;AAAA,QAAjB;AAAA,UACC,MAAM;AAAA,UACN,cAAc;AAAA,UAEd;AAAA,gCAAC,iBAAiB,SAAjB,EAAyB,SAAO,MAC/B;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,UAAU,CAAC,UAAU;AACnB,wBAAM,eAAe;AACrB,0CAAwB,IAAI;AAAA,gBAC9B;AAAA,gBAEA;AAAA,sCAAC,QAAK,WAAU,0CAAyC;AAAA,kBACxD;AAAA,kBACA,iBAAiB,SAAS,IACzB,oBAAC,UAAK,WAAU,kEACb,2BAAiB,QACpB,IACE;AAAA;AAAA;AAAA,YACN,GACF;AAAA,YACA,oBAAC,iBAAiB,QAAjB,EACC;AAAA,cAAC,iBAAiB;AAAA,cAAjB;AAAA,gBACC,OAAM;AAAA,gBACN,MAAK;AAAA,gBACL,YAAY;AAAA,gBACZ,WAAU;AAAA,gBACV,iBAAiB,MAAM,wBAAwB,KAAK;AAAA,gBACpD,mBAAmB,CAAC,UAAU;AAC5B,wBAAM,SAAS,MAAM;AACrB,sBAAI,iCAAQ,QAAQ,wCAAwC;AAC1D,0BAAM,eAAe;AAAA,kBACvB;AAAA,gBACF;AAAA,gBAEA;AAAA,kBAAC;AAAA;AAAA,oBACC,QAAQ;AAAA,oBACR,YAAY;AAAA,oBACZ,oBAAoB,+DAA6B,MAAM;AAAA,oBAAC;AAAA;AAAA,gBAC1D;AAAA;AAAA,YACF,GACF;AAAA;AAAA;AAAA,MACF,GACF,IACE;AAAA,OACN;AAAA,KACF;AAIF,MAAI,YAAY,SAAS,GAAG;AAC1B,WACE,qBAAC,SAAI,WAAU,uCACZ;AAAA;AAAA,MACA,YAAY,IAAI,CAAC,SAChB;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,SAAS,MAAM,iDAAiB,KAAK,YAAY,KAAK;AAAA,UACtD,WAAW;AAAA,YACT;AAAA,YACA,KAAK,SACD,gFACA;AAAA,UACN;AAAA,UAEA;AAAA,iCAAC,UAAK,WAAU,oCACb;AAAA;AAAA,cAAY;AAAA,cAAE;AAAA,eACjB;AAAA,YACC,KAAK;AAAA;AAAA;AAAA,QAbD,GAAG,KAAK,UAAU,IAAI,KAAK,KAAK;AAAA,MAcvC,CACD;AAAA,OACH;AAAA,EAEJ;AAEA,SAAO;AACT;","names":["_a","_b"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ListFilter, Plus, Search } from \"lucide-react\"\n\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DataTableConditionFilter,\n shouldShowOptionSearch,\n type ConditionFieldDef,\n type ConditionFilterValue,\n} from \"./data-table-condition-filter\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\ninterface DataTableFilterCategoryBase {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n /**\n * Submenu search behavior. Defaults to the DataTableFilter\n * optionSearchThreshold prop. Use true to always show search or false to\n * hide it for a specific category.\n */\n searchable?: boolean | { threshold?: number }\n}\n\nexport interface DataTableOptionFilterCategory extends DataTableFilterCategoryBase {\n options: (string | FilterOption)[]\n /** Filter behavior. Defaults to \"multi\" (checkbox multi-select). */\n type?: \"multi\" | \"single\" | \"boolean\"\n /**\n * When true, the submenu search box is parent-driven: typing fires\n * `onOptionSearch(categoryId, query)` and the parent is responsible for\n * supplying the matching `options` (e.g. a server-backed lookup over a large\n * set). The built-in client-side option filtering is skipped, the search box\n * is always shown, and `optionSearchLoading[categoryId]` drives a loading row.\n */\n remoteSearch?: boolean\n}\n\nexport interface DataTableTextFilterCategory extends DataTableFilterCategoryBase {\n /** Free-text filter behavior. Renders a top-level submenu with a text input. */\n type: \"text\"\n /** Placeholder shown in the text filter input. */\n valuePlaceholder?: string\n /** Not used for text filters; optional for backwards-compatible category shapes. */\n options?: (string | FilterOption)[]\n}\n\nexport type DataTableFilterCategory =\n | DataTableOptionFilterCategory\n | DataTableTextFilterCategory\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\nfunction isTextFilterCategory(\n category: DataTableFilterCategory\n): category is DataTableTextFilterCategory {\n return category.type === \"text\"\n}\n\nfunction TextFilterSubmenu({\n category,\n value,\n onValueChange,\n}: {\n category: DataTableTextFilterCategory\n value: string\n onValueChange?: (categoryId: string, value: string) => void\n}) {\n const [draftValue, setDraftValue] = React.useState(value)\n\n React.useEffect(() => {\n setDraftValue(value)\n }, [value])\n\n const active = value.trim().length > 0\n const applyValue = React.useCallback(() => {\n onValueChange?.(category.id, draftValue.trim())\n }, [category.id, draftValue, onValueChange])\n\n return (\n <DropdownMenuSub\n onOpenChange={(open) => {\n if (!open) {\n setDraftValue(value)\n }\n }}\n >\n <DropdownMenuSubTrigger\n className={cn(\n \"cursor-pointer py-1.5 text-xs\",\n active && \"text-brand-purple\"\n )}\n >\n <category.icon\n className={cn(\n \"mr-2 h-3.5 w-3.5 text-muted-foreground\",\n active && \"text-brand-purple\"\n )}\n />\n {category.label}\n {active ? <Check className=\"ml-auto h-4 w-4\" /> : null}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"w-64 p-2\">\n <div className=\"space-y-2\">\n <input\n aria-label={category.label}\n className=\"h-8 w-full rounded-md bg-muted/50 px-2 py-1 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder={\n category.valuePlaceholder ??\n `Enter ${category.label.toLowerCase()}...`\n }\n value={draftValue}\n onChange={(event) => setDraftValue(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => {\n event.stopPropagation()\n if (event.key === \"Enter\") {\n event.preventDefault()\n applyValue()\n }\n }}\n />\n <div className=\"flex items-center justify-end gap-2\">\n {active ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n setDraftValue(\"\")\n onValueChange?.(category.id, \"\")\n }}\n >\n Clear\n </Button>\n ) : null}\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n applyValue()\n }}\n >\n Apply\n </Button>\n </div>\n </div>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n}\n\nexport interface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n /** Minimum number of options before showing the sub-menu search input. Defaults to 8. */\n optionSearchThreshold?: number\n /** Filters applied by default. Shown as distinct chips that can be toggled off but not dismissed. */\n presetFilters?: Record<string, string[]>\n /** Callback when a preset filter is toggled on/off. */\n onTogglePreset?: (categoryId: string, option: string) => void\n /** Label shown on preset chips to distinguish from user-applied filters. Default: \"Default\". */\n presetLabel?: string\n /** Fields exposed in the unified condition-builder panel. */\n conditionFields?: ConditionFieldDef[]\n /** Active builder-managed field/operator/value conditions. */\n conditionFilters?: ConditionFilterValue[]\n /** Callback when builder-managed conditions are applied, removed, or cleared. */\n onConditionFiltersChange?: (conditions: ConditionFilterValue[]) => void\n /** Dropdown entry label for the condition-builder panel. Default: \"Add filter\". */\n conditionBuilderLabel?: string\n /** Active free-text filters keyed by category id. */\n textFilters?: Record<string, string>\n /** Callback when a free-text filter value is applied or cleared. */\n onTextFilterChange?: (categoryId: string, value: string) => void\n /**\n * Fired when the submenu search input changes for a category with\n * `remoteSearch: true`. The parent should debounce, fetch matching options,\n * and feed them back via that category's `options`. The empty string is sent\n * when the box is cleared (or the submenu re-opens) so the parent can reset.\n */\n onOptionSearch?: (categoryId: string, query: string) => void\n /** Per-category loading state for remote option search, keyed by category id. */\n optionSearchLoading?: Record<string, boolean>\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n optionSearchThreshold = 8,\n presetFilters,\n onTogglePreset,\n presetLabel = \"Default\",\n conditionFields = [],\n conditionFilters = [],\n onConditionFiltersChange,\n conditionBuilderLabel = \"Add filter\",\n textFilters = {},\n onTextFilterChange,\n onOptionSearch,\n optionSearchLoading = {},\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n const [conditionBuilderOpen, setConditionBuilderOpen] = React.useState(false)\n const hasConditionBuilder = conditionFields.length > 0\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n if (isTextFilterCategory(category)) {\n return false\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n /** Check if a specific option is a preset filter */\n const isPresetOption = React.useCallback(\n (categoryId: string, value: string): boolean => {\n return presetFilters?.[categoryId]?.includes(value) ?? false\n },\n [presetFilters]\n )\n\n const activeCount = React.useMemo(() => {\n const userCount = Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n )\n\n const textCount = categories.reduce((count, category) => {\n if (!isTextFilterCategory(category)) {\n return count\n }\n\n return textFilters[category.id]?.trim() ? count + 1 : count\n }, 0)\n\n return userCount + conditionFilters.length + textCount\n }, [categories, selectedFilters, conditionFilters.length, textFilters])\n\n /** Collect all preset chips to render */\n const presetChips = React.useMemo(() => {\n if (!presetFilters) return []\n\n const chips: { categoryId: string; value: string; label: string; active: boolean }[] = []\n\n for (const [categoryId, values] of Object.entries(presetFilters)) {\n const category = categories.find((c) => c.id === categoryId)\n for (const value of values) {\n const option = category && !isTextFilterCategory(category)\n ? category.options.find((opt) => getOptionValue(opt) === value)\n : undefined\n const label = option ? getOptionLabel(option) : value\n const active = selectedFilters[categoryId]?.includes(value) ?? false\n chips.push({ categoryId, value, label, active })\n }\n }\n\n return chips\n }, [presetFilters, categories, selectedFilters])\n\n const triggerButton = (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold text-foreground\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const filterType = category.type ?? \"multi\"\n\n /* ── Boolean toggle ─────────────────────────────────── */\n if (filterType === \"boolean\") {\n const active = selectedFilters[category.id]?.includes(\"true\") ?? false\n return (\n <DropdownMenuItem\n key={category.id}\n className={cn(\n \"cursor-pointer py-1.5 text-xs\",\n active && \"text-brand-purple\"\n )}\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, \"true\")\n }}\n >\n <category.icon className=\"mr-2 h-3.5 w-3.5\" />\n {category.label}\n {active ? <Check className=\"ml-auto h-4 w-4\" /> : null}\n </DropdownMenuItem>\n )\n }\n\n /* ── Free-text submenu ───────────────────────────────── */\n if (isTextFilterCategory(category)) {\n return (\n <TextFilterSubmenu\n key={category.id}\n category={category}\n value={textFilters[category.id] ?? \"\"}\n onValueChange={onTextFilterChange}\n />\n )\n }\n\n /* ── Sub-menu (single / multi) ──────────────────────── */\n const isRemote = category.remoteSearch === true\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n // Remote-search categories are filtered by the parent (server-side);\n // never client-filter their options.\n const filteredOptions = isRemote || !subQuery\n ? category.options\n : category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n const shouldShowSubmenuSearch = isRemote || shouldShowOptionSearch(\n category.searchable,\n category.options.length,\n optionSearchThreshold,\n )\n const isSearching = optionSearchLoading[category.id] === true\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n // Reset the parent's remote results so re-opening starts clean.\n if (isRemote) onOptionSearch?.(category.id, \"\")\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — shown for long lists or categories that opt in. */}\n {shouldShowSubmenuSearch && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) => {\n const next = e.target.value\n setSubQueries((prev) => ({ ...prev, [category.id]: next }))\n if (isRemote) onOptionSearch?.(category.id, next)\n }}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n // Allow navigation keys to propagate to Radix menu handling\n // so keyboard users can move to and select filtered options.\n const navKeys = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Escape\", \"Tab\"]\n if (!navKeys.includes(e.key)) {\n e.stopPropagation()\n }\n }}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n const isPreset = isPresetOption(category.id, value)\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n isPreset ? (\n <span className=\"text-brand-purple text-[10px] font-semibold\">\n {presetLabel}\n </span>\n ) : filterType === \"single\" ? (\n <span className=\"h-1.5 w-1.5 rounded-full bg-current\" />\n ) : (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n )\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {isSearching ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n Searching…\n </div>\n ) : filteredOptions.length === 0 ? (\n (() => {\n const typed = (subQueries[category.id] ?? \"\").trim().length > 0\n const message = isRemote\n ? typed\n ? \"No matches\"\n : \"Type to search\"\n : category.options.length > 0\n ? \"No matches\"\n : null\n return message ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n {message}\n </div>\n ) : null\n })()\n ) : null}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n\n {hasConditionBuilder ? (\n <div className=\"border-t border-border p-1\">\n <PopoverPrimitive.Root\n open={conditionBuilderOpen}\n onOpenChange={setConditionBuilderOpen}\n >\n <PopoverPrimitive.Trigger asChild>\n <DropdownMenuItem\n className=\"cursor-pointer py-1.5 text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n setConditionBuilderOpen(true)\n }}\n >\n <Plus className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {conditionBuilderLabel}\n {conditionFilters.length > 0 ? (\n <span className=\"ml-auto rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {conditionFilters.length}\n </span>\n ) : null}\n </DropdownMenuItem>\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align=\"start\"\n side=\"right\"\n sideOffset={8}\n className=\"z-50 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\"\n onEscapeKeyDown={() => setConditionBuilderOpen(false)}\n onInteractOutside={(event) => {\n const target = event.target as HTMLElement | null\n if (target?.closest('[data-slot=\"dropdown-menu-content\"]')) {\n event.preventDefault()\n }\n }}\n >\n <DataTableConditionFilter\n fields={conditionFields}\n conditions={conditionFilters}\n onConditionsChange={onConditionFiltersChange ?? (() => {})}\n />\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n </div>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n\n // If there are preset chips, wrap trigger + chips together\n if (presetChips.length > 0) {\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {triggerButton}\n {presetChips.map((chip) => (\n <button\n key={`${chip.categoryId}-${chip.value}`}\n type=\"button\"\n onClick={() => onTogglePreset?.(chip.categoryId, chip.value)}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium transition-colors\",\n chip.active\n ? \"border-dashed border-brand-purple/30 bg-brand-purple/5 text-brand-purple/80\"\n : \"border-border/40 bg-transparent text-muted-foreground/60 line-through\"\n )}\n >\n <span className=\"text-brand-purple/50 text-[10px]\">\n {presetLabel}:{\" \"}\n </span>\n {chip.label}\n </button>\n ))}\n </div>\n )\n }\n\n return triggerButton\n}\n"],"mappings":"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","names":["_a","_b"]}
|
|
@@ -174,12 +174,12 @@ function EntityPanelTabs({
|
|
|
174
174
|
)) });
|
|
175
175
|
}
|
|
176
176
|
function EntityMetadataGrid({ fields }) {
|
|
177
|
-
return /* @__PURE__ */ jsx("div", { className: "grid
|
|
178
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-
|
|
179
|
-
/* @__PURE__ */ jsx(field.icon, { className: "
|
|
180
|
-
/* @__PURE__ */ jsx("span", { children: field.label })
|
|
177
|
+
return /* @__PURE__ */ jsx("div", { className: "mb-7 grid min-w-0 grid-cols-1 gap-x-4 gap-y-3 overflow-hidden text-[13px] md:grid-cols-[minmax(0,140px)_minmax(0,1fr)]", children: fields.map((field, idx) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
178
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-start gap-1.5 text-[13px] font-normal text-muted-foreground", children: [
|
|
179
|
+
/* @__PURE__ */ jsx(field.icon, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
|
|
180
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 break-words leading-relaxed [overflow-wrap:anywhere]", children: field.label })
|
|
181
181
|
] }),
|
|
182
|
-
/* @__PURE__ */ jsx("div", { className: "min-w-0
|
|
182
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 whitespace-normal break-words leading-relaxed text-foreground [overflow-wrap:anywhere] [&_*]:max-w-full [&_*]:[overflow-wrap:anywhere] [&_a]:break-all", children: field.value })
|
|
183
183
|
] }, idx)) });
|
|
184
184
|
}
|
|
185
185
|
function EntitySection({
|