@neasg/design-system 0.4.35 → 0.4.113

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 (76) hide show
  1. package/README.md +9 -0
  2. package/dist/activity-card.d.ts +31 -0
  3. package/dist/activity-card.js +65 -0
  4. package/dist/alert.d.ts +1 -1
  5. package/dist/autocomplete.d.ts +84 -0
  6. package/dist/autocomplete.js +267 -0
  7. package/dist/avatar.d.ts +3 -1
  8. package/dist/avatar.js +27 -10
  9. package/dist/badge.d.ts +1 -1
  10. package/dist/badge.js +1 -1
  11. package/dist/button.d.ts +2 -2
  12. package/dist/button.js +3 -1
  13. package/dist/calendar.js +1 -1
  14. package/dist/card.d.ts +1 -1
  15. package/dist/card.js +1 -1
  16. package/dist/changelog-action-panel.d.ts +17 -0
  17. package/dist/changelog-action-panel.js +14 -0
  18. package/dist/checklist.d.ts +63 -0
  19. package/dist/checklist.js +30 -0
  20. package/dist/compound-search-input.d.ts +10 -0
  21. package/dist/compound-search-input.js +19 -0
  22. package/dist/date-input.js +3 -3
  23. package/dist/dialog.d.ts +4 -1
  24. package/dist/dialog.js +7 -2
  25. package/dist/draggable-tabs.js +9 -8
  26. package/dist/dropdown-button.js +1 -0
  27. package/dist/editable-table.js +2 -2
  28. package/dist/field-counter.d.ts +7 -0
  29. package/dist/field-counter.js +5 -0
  30. package/dist/index.d.ts +22 -4
  31. package/dist/index.js +12 -2
  32. package/dist/input.d.ts +2 -0
  33. package/dist/input.js +11 -3
  34. package/dist/label-value-grid.js +6 -4
  35. package/dist/layout.d.ts +39 -6
  36. package/dist/layout.js +118 -42
  37. package/dist/lib/utils.js +1 -0
  38. package/dist/message-action-panel.d.ts +12 -0
  39. package/dist/message-action-panel.js +11 -0
  40. package/dist/message-item.d.ts +23 -3
  41. package/dist/message-item.js +115 -16
  42. package/dist/notification.d.ts +14 -3
  43. package/dist/notification.js +36 -7
  44. package/dist/page-section.d.ts +2 -1
  45. package/dist/page-section.js +2 -2
  46. package/dist/popover-menu.d.ts +4 -2
  47. package/dist/popover-menu.js +65 -5
  48. package/dist/popover.d.ts +10 -2
  49. package/dist/popover.js +7 -3
  50. package/dist/rich-text-editor.d.ts +2 -1
  51. package/dist/rich-text-editor.js +29 -6
  52. package/dist/routing-action-panel.d.ts +38 -0
  53. package/dist/routing-action-panel.js +19 -0
  54. package/dist/routing-timeline.d.ts +7 -2
  55. package/dist/routing-timeline.js +67 -29
  56. package/dist/search-input.d.ts +6 -0
  57. package/dist/search-input.js +6 -4
  58. package/dist/select-primitive.js +1 -1
  59. package/dist/select.d.ts +3 -1
  60. package/dist/select.js +9 -4
  61. package/dist/status-dot.d.ts +13 -0
  62. package/dist/status-dot.js +19 -0
  63. package/dist/styles.css +0 -19
  64. package/dist/table-add-row.d.ts +8 -0
  65. package/dist/table-add-row.js +7 -0
  66. package/dist/table-column-filter.js +7 -5
  67. package/dist/table.d.ts +9 -1
  68. package/dist/table.js +25 -11
  69. package/dist/tabs.d.ts +1 -1
  70. package/dist/tabs.js +6 -4
  71. package/dist/tailwind-preset.js +10 -0
  72. package/dist/textarea.d.ts +5 -0
  73. package/dist/textarea.js +18 -3
  74. package/dist/theme-switcher.d.ts +3 -1
  75. package/dist/theme-switcher.js +8 -2
  76. package/package.json +1 -1
@@ -11,6 +11,7 @@ import { TextStyle } from "@tiptap/extension-text-style";
11
11
  import { Color } from "@tiptap/extension-color";
12
12
  import Highlight from "@tiptap/extension-highlight";
13
13
  import Underline from "@tiptap/extension-underline";
14
+ import { Baseline, Reply } from "lucide-react";
14
15
  import { BoldIcon, ChevronDownIcon, HighlighterIcon, ItalicIcon, LinkIcon, ListIcon, ListOrderedIcon, MaximizeIcon, PaletteIcon, PaperclipIcon, PlusIcon, SendIcon, StrikethroughIcon, UnderlineIcon, XIcon, } from "./animated-icons";
15
16
  import { Button } from "./button";
16
17
  import { DropdownButton } from "./dropdown-button";
@@ -19,6 +20,7 @@ import { inputControlPaddingClassName } from "./input-control";
19
20
  import { Popover } from "./popover";
20
21
  import { PopoverMenuItem, PopoverMenuSection } from "./popover-menu";
21
22
  import { Tooltip } from "./tooltip";
23
+ import { Typography } from "./typography";
22
24
  import { cn } from "./lib/utils";
23
25
  const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
24
26
  const DEFAULT_MAX_FILE_SIZE_LABEL = "10MB";
@@ -125,7 +127,8 @@ function ToolbarIconButton({ active, label, onClick, children, }) {
125
127
  function getTemplateCategory(template) {
126
128
  return template.category || DEFAULT_TEMPLATE_CATEGORY;
127
129
  }
128
- export function RichTextEditor({ placeholder = "Type message here...", editorAriaLabel = "Message body", header, isExpanded = false, onExpandToggle, onSend, className, maxFileSize = DEFAULT_MAX_FILE_SIZE, maxFileSizeLabel = DEFAULT_MAX_FILE_SIZE_LABEL, acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES, acceptedFileTypesLabel = DEFAULT_ACCEPTED_FILE_TYPES_LABEL, isSending = false, showTemplates = true, templates = [], templateContext = {}, templateCategoryLabels = {}, templateCategoryOrder, templateCreateHref, templateCreateLabel = "Create new template", onFileRejected, colorPalette = DEFAULT_COLOR_PALETTE, sendLabel = "Send", sendingLabel = "Sending...", sendActions, }) {
130
+ export function RichTextEditor({ variant = "default", placeholder = "Type message here...", editorAriaLabel = "Message body", header, isExpanded = false, onExpandToggle, onSend, className, maxFileSize = DEFAULT_MAX_FILE_SIZE, maxFileSizeLabel = DEFAULT_MAX_FILE_SIZE_LABEL, acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES, acceptedFileTypesLabel = DEFAULT_ACCEPTED_FILE_TYPES_LABEL, isSending = false, showTemplates = true, templates = [], templateContext = {}, templateCategoryLabels = {}, templateCategoryOrder, templateCreateHref, templateCreateLabel = "Create new template", onFileRejected, colorPalette = DEFAULT_COLOR_PALETTE, sendLabel = "Send", sendingLabel = "Sending...", sendActions, }) {
131
+ var _a;
129
132
  const [isEmpty, setIsEmpty] = React.useState(true);
130
133
  const [attachments, setAttachments] = React.useState([]);
131
134
  const [isDragging, setIsDragging] = React.useState(false);
@@ -133,6 +136,7 @@ export function RichTextEditor({ placeholder = "Type message here...", editorAri
133
136
  const [linkUrl, setLinkUrl] = React.useState("");
134
137
  const [linkMenuPosition, setLinkMenuPosition] = React.useState(null);
135
138
  const [templatePopoverOpen, setTemplatePopoverOpen] = React.useState(false);
139
+ const [isFormattingOpen, setIsFormattingOpen] = React.useState(false);
136
140
  const fileInputRef = React.useRef(null);
137
141
  const dropZoneRef = React.useRef(null);
138
142
  const linkInputRef = React.useRef(null);
@@ -192,7 +196,9 @@ export function RichTextEditor({ placeholder = "Type message here...", editorAri
192
196
  role: "textbox",
193
197
  "aria-multiline": "true",
194
198
  "aria-label": editorAriaLabel,
195
- class: cn("prose prose-sm max-w-none focus:outline-none", "min-h-[120px]", inputControlPaddingClassName),
199
+ class: cn("prose prose-sm max-w-none focus:outline-none", variant === "message"
200
+ ? "min-h-[48px] px-[10px] py-[8px] text-sm"
201
+ : cn("min-h-[120px]", inputControlPaddingClassName)),
196
202
  },
197
203
  },
198
204
  onUpdate: ({ editor }) => {
@@ -345,13 +351,30 @@ export function RichTextEditor({ placeholder = "Type message here...", editorAri
345
351
  setTemplatePopoverOpen(false);
346
352
  };
347
353
  const hasSendActions = Boolean(sendActions === null || sendActions === void 0 ? void 0 : sendActions.length);
348
- const sendAction = hasSendActions ? (_jsx(DropdownButton, { className: "w-full", align: "end", disabled: sendDisabled, label: isSending ? sendingLabel : sendLabel, leadingIcon: _jsx(SendIcon, { size: 16 }), split: true, onClick: () => {
354
+ const renderSendAction = ({ compact = false, fullWidth = false, } = {}) => hasSendActions ? (_jsx(DropdownButton, { className: fullWidth ? "w-full" : undefined, align: "end", disabled: sendDisabled, label: isSending ? sendingLabel : sendLabel, leadingIcon: _jsx(SendIcon, { size: 16 }), size: compact ? "sm" : undefined, split: true, onClick: () => {
349
355
  void handleSend();
350
356
  }, children: _jsx(PopoverMenuSection, { children: sendActions === null || sendActions === void 0 ? void 0 : sendActions.map((action) => (_jsx(PopoverMenuItem, { disabled: sendDisabled || action.disabled, onClick: () => {
351
357
  void handleSend(action.metadata);
352
- }, children: action.label }, action.id))) }) })) : (_jsxs(Button, { type: "button", className: "w-full", disabled: sendDisabled, onClick: () => {
358
+ }, children: action.label }, action.id))) }) })) : (_jsxs(Button, { type: "button", className: fullWidth ? "w-full" : undefined, size: compact ? "sm" : undefined, disabled: sendDisabled, "aria-label": isSending ? sendingLabel : sendLabel, onClick: () => {
353
359
  void handleSend();
354
360
  }, children: [_jsx(SendIcon, { size: 16 }), isSending ? sendingLabel : sendLabel] }));
361
+ if (variant === "message") {
362
+ return (_jsxs("div", { "data-slot": "message-composer", ref: dropZoneRef, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, className: cn("box-border flex min-h-[164px] flex-col overflow-hidden rounded-[8px] border border-input bg-background transition-all focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50", !isExpanded && attachments.length === 0 && "h-[164px]", isDragging && "border-2 border-primary bg-primary/5", className), children: [header ? (_jsxs("div", { "data-slot": "message-composer-header", className: "flex min-h-[48px] items-center justify-between px-[10px] py-[8px] text-muted-foreground", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-[8px]", children: [_jsx(Reply, { "aria-hidden": "true", className: "size-[16px] shrink-0" }), _jsx(Typography, { as: "span", variant: "caption", className: "min-w-0 truncate font-medium text-muted-foreground", "data-slot": "message-composer-header-label", children: header })] }), onExpandToggle ? (_jsx(Tooltip, { content: isExpanded ? "Collapse editor" : "Expand editor", side: "top", children: _jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "h-[28px] w-[28px] shrink-0", onClick: onExpandToggle, "aria-label": isExpanded ? "Collapse editor" : "Expand editor", children: _jsx(MaximizeIcon, { size: 16 }) }) })) : null] })) : null, _jsx("div", { "data-slot": "message-composer-editor", className: cn("min-h-[64px]", isExpanded && "min-h-[200px]"), children: _jsx(EditorContent, { editor: editor, className: cn("min-h-[64px] text-sm", isExpanded && "min-h-[200px]", "[&_.ProseMirror]:min-h-[64px] [&_.ProseMirror]:outline-none", isExpanded && "[&_.ProseMirror]:min-h-[200px]", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:float-left", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:h-0", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:pointer-events-none", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:text-muted-foreground", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:content-[attr(data-placeholder)]") }) }), attachments.length > 0 ? (_jsx("div", { className: "space-y-[8px] px-[10px] pb-[8px]", children: attachments.map((attachment, index) => (_jsxs("div", { className: "flex w-full items-center gap-[8px] rounded-md border bg-muted px-[12px] py-[8px] text-sm", children: [_jsx(PaperclipIcon, { size: 14, className: "shrink-0 text-muted-foreground" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: attachment.name }), _jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatFileSize(attachment.size) }), _jsx("button", { type: "button", onClick: () => removeAttachment(index), className: "shrink-0 text-muted-foreground hover:text-foreground", "aria-label": `Remove ${attachment.name}`, children: _jsx(XIcon, { size: 14 }) })] }, `${attachment.name}-${attachment.size}-${index}`))) })) : null, _jsx("input", { ref: fileInputRef, type: "file", accept: acceptedFileTypes, multiple: true, onChange: handleFileSelect, className: "hidden" }), linkPopoverOpen &&
363
+ linkMenuPosition &&
364
+ typeof document !== "undefined" &&
365
+ createPortal(_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-[99]", onClick: () => {
366
+ setLinkPopoverOpen(false);
367
+ setLinkUrl("");
368
+ setLinkMenuPosition(null);
369
+ editor === null || editor === void 0 ? void 0 : editor.chain().focus().run();
370
+ } }), _jsx("div", { ref: linkMenuRef, className: "fixed z-[100] w-80 animate-in fade-in-0 zoom-in-95 rounded-md border bg-popover p-3 shadow-md", style: {
371
+ top: linkMenuPosition.top,
372
+ left: linkMenuPosition.left,
373
+ }, children: _jsxs("div", { className: "flex items-end gap-2", children: [_jsx(Input, { ref: linkInputRef, label: "URL", id: "message-link-url", type: "url", placeholder: "https://example.com", value: linkUrl, onChange: (event) => setLinkUrl(event.target.value), onKeyDown: handleLinkKeyDown, inputClassName: "h-8 text-sm", autoFocus: true }), _jsx(Button, { type: "button", className: "h-8 px-3", onClick: handleLinkSubmit, disabled: !linkUrl.trim(), children: "Add" })] }) })] }), document.body), isFormattingOpen ? (_jsxs("div", { className: "flex flex-wrap items-center gap-[4px] border-t border-border px-[10px] py-[8px]", children: [_jsx(ToolbarIconButton, { label: "Bold", active: editor === null || editor === void 0 ? void 0 : editor.isActive("bold"), onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleBold().run(), children: _jsx(BoldIcon, { size: 14 }) }), _jsx(ToolbarIconButton, { label: "Italic", active: editor === null || editor === void 0 ? void 0 : editor.isActive("italic"), onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleItalic().run(), children: _jsx(ItalicIcon, { size: 14 }) }), _jsx(ToolbarIconButton, { label: "Underline", active: editor === null || editor === void 0 ? void 0 : editor.isActive("underline"), onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleUnderline().run(), children: _jsx(UnderlineIcon, { size: 14 }) }), _jsx(ToolbarIconButton, { label: "Bullet list", active: editor === null || editor === void 0 ? void 0 : editor.isActive("bulletList"), onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleBulletList().run(), children: _jsx(ListIcon, { size: 14 }) }), _jsx(ToolbarIconButton, { label: "Ordered list", active: editor === null || editor === void 0 ? void 0 : editor.isActive("orderedList"), onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrderedIcon, { size: 14 }) })] })) : null, _jsxs("div", { "data-slot": "message-composer-toolbar", className: "flex min-h-[52px] items-center justify-between px-[10px] py-[8px]", children: [_jsxs("div", { className: "flex items-center gap-[8px]", children: [_jsx(Tooltip, { content: `Attach files (max ${maxFileSizeLabel}) - ${acceptedFileTypesLabel}`, side: "top", contentClassName: "max-w-[240px] text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "h-[28px] w-[28px]", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, "aria-label": "Attach files", children: _jsx(PaperclipIcon, { size: 16 }) }) }), _jsxs("div", { className: "inline-flex h-[32px] overflow-hidden rounded-control border border-input bg-background", children: [_jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "h-[32px] w-[32px] rounded-none border-r border-input", onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleBold().run(), "aria-pressed": (_a = editor === null || editor === void 0 ? void 0 : editor.isActive("bold")) !== null && _a !== void 0 ? _a : false, "aria-label": "Bold", children: _jsx(BoldIcon, { size: 16 }) }), _jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "h-[32px] w-[32px] rounded-none border-r border-input", onClick: openLinkMenu, "aria-label": "Add link", children: _jsx(LinkIcon, { size: 16 }) }), _jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "h-[32px] w-[32px] rounded-none", onClick: () => setIsFormattingOpen((open) => !open), "aria-expanded": isFormattingOpen, "aria-label": "Formatting options", children: _jsx(Baseline, { "aria-hidden": "true", className: "size-[16px]" }) })] })] }), _jsxs("div", { className: "flex items-center gap-[8px]", children: [showTemplates && (templates.length > 0 || templateCreateHref) ? (_jsxs(Popover, { open: templatePopoverOpen, onOpenChange: setTemplatePopoverOpen, align: "end", contentClassName: "w-96 p-0", trigger: _jsxs(Button, { type: "button", variant: "outline", size: "sm", children: ["Templates", _jsx(ChevronDownIcon, { size: 16 })] }), children: [_jsx("div", { className: "max-h-80 overflow-y-auto", children: templatesByCategory.map(({ category, templates }) => {
374
+ var _a;
375
+ return (_jsxs("div", { children: [_jsx("div", { className: "sticky top-0 bg-muted px-3 py-2 text-sm font-semibold text-foreground", children: (_a = templateCategoryLabels[category]) !== null && _a !== void 0 ? _a : category }), templates.map((template) => (_jsxs("button", { type: "button", className: "w-full border-b border-border/50 px-3 py-2 text-left text-sm transition-colors last:border-0 hover:bg-muted", onClick: () => applyTemplate(template), children: [_jsx("div", { className: "truncate font-medium", children: template.name }), template.description ? (_jsx("div", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: template.description })) : null] }, template.id)))] }, category));
376
+ }) }), templateCreateHref ? (_jsx("div", { className: "border-t p-2", children: _jsxs("a", { href: templateCreateHref, target: "_blank", rel: "noopener noreferrer", className: "flex w-full items-center justify-center gap-1.5 rounded-md px-3 py-2 text-sm text-interactive transition-colors hover:bg-muted", children: [_jsx(PlusIcon, { size: 16 }), templateCreateLabel] }) })) : null] })) : null, renderSendAction({ compact: true })] })] })] }));
377
+ }
355
378
  return (_jsxs("div", { ref: dropZoneRef, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, className: cn("overflow-hidden rounded-control border bg-background transition-all focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50", isExpanded && "shadow-md", isDragging && "border-2 border-primary bg-primary/5", className), children: [header && (_jsxs("div", { className: cn("flex items-center justify-between border-b bg-muted dark:bg-muted/30", inputControlPaddingClassName), children: [_jsx("div", { className: "flex-1", children: header }), _jsx("div", { className: "flex items-center gap-2", children: onExpandToggle && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "h-6 w-6", onClick: onExpandToggle, "aria-label": "Expand editor", children: _jsx(MaximizeIcon, { size: 14 }) })) })] })), _jsxs("div", { className: cn("bg-background transition-all", isExpanded ? "min-h-[200px]" : "min-h-[120px]"), children: [_jsx(EditorContent, { editor: editor, className: cn("text-sm", "[&_.ProseMirror]:min-h-[120px] [&_.ProseMirror]:outline-none", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:text-muted-foreground", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:content-[attr(data-placeholder)]", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:float-left", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:pointer-events-none", "[&_.ProseMirror_p.is-editor-empty:first-child::before]:h-0", "[&_.ProseMirror_ul]:my-2 [&_.ProseMirror_ul]:list-disc [&_.ProseMirror_ul]:pl-6", "[&_.ProseMirror_ol]:my-2 [&_.ProseMirror_ol]:list-decimal [&_.ProseMirror_ol]:pl-6", "[&_.ProseMirror_li]:my-1", "[&_.ProseMirror_ul_ul]:list-[circle] [&_.ProseMirror_ul_ul_ul]:list-[square]") }), linkPopoverOpen &&
356
379
  linkMenuPosition &&
357
380
  typeof document !== "undefined" &&
@@ -387,8 +410,8 @@ export function RichTextEditor({ placeholder = "Type message here...", editorAri
387
410
  else {
388
411
  editor === null || editor === void 0 ? void 0 : editor.chain().focus().setHighlight({ color: color.value }).run();
389
412
  }
390
- }, children: color.value === "" && (_jsx("span", { className: "text-xs text-muted-foreground", children: "A" })) }, `bg-${color.name}`))) }) }), _jsx(ToolbarIconButton, { label: "Bullet list", active: editor === null || editor === void 0 ? void 0 : editor.isActive("bulletList"), onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleBulletList().run(), children: _jsx(ListIcon, { size: 14 }) }), _jsx(ToolbarIconButton, { label: "Ordered list", active: editor === null || editor === void 0 ? void 0 : editor.isActive("orderedList"), onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrderedIcon, { size: 14 }) }), _jsx(Tooltip, { content: `Attach files (max ${maxFileSizeLabel}) - ${acceptedFileTypesLabel}`, side: "top", contentClassName: "max-w-[240px] text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "h-8 w-8", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, "aria-label": "Attach files", children: _jsx(PaperclipIcon, { size: 14 }) }) }), _jsx(ToolbarIconButton, { label: "Add link", active: editor === null || editor === void 0 ? void 0 : editor.isActive("link"), onClick: openLinkMenu, children: _jsx(LinkIcon, { size: 14 }) }), showTemplates && templates.length > 0 && (_jsx(_Fragment, { children: _jsxs(Popover, { open: templatePopoverOpen, onOpenChange: setTemplatePopoverOpen, align: "end", contentClassName: "w-96 p-0", trigger: _jsxs(Button, { type: "button", variant: "ghost", className: "h-8 gap-1 text-muted-foreground", children: ["Template", _jsx(ChevronDownIcon, { size: 14 })] }), children: [_jsx("div", { className: "max-h-80 overflow-y-auto", children: templatesByCategory.map(({ category, templates }) => {
413
+ }, children: color.value === "" && (_jsx("span", { className: "text-xs text-muted-foreground", children: "A" })) }, `bg-${color.name}`))) }) }), _jsx(ToolbarIconButton, { label: "Bullet list", active: editor === null || editor === void 0 ? void 0 : editor.isActive("bulletList"), onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleBulletList().run(), children: _jsx(ListIcon, { size: 14 }) }), _jsx(ToolbarIconButton, { label: "Ordered list", active: editor === null || editor === void 0 ? void 0 : editor.isActive("orderedList"), onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrderedIcon, { size: 14 }) }), _jsx(Tooltip, { content: `Attach files (max ${maxFileSizeLabel}) - ${acceptedFileTypesLabel}`, side: "top", contentClassName: "max-w-[240px] text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "h-8 w-8", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, "aria-label": "Attach files", children: _jsx(PaperclipIcon, { size: 14 }) }) }), _jsx(ToolbarIconButton, { label: "Add link", active: editor === null || editor === void 0 ? void 0 : editor.isActive("link"), onClick: openLinkMenu, children: _jsx(LinkIcon, { size: 14 }) }), showTemplates && (templates.length > 0 || templateCreateHref) && (_jsx(_Fragment, { children: _jsxs(Popover, { open: templatePopoverOpen, onOpenChange: setTemplatePopoverOpen, align: "end", contentClassName: "w-96 p-0", trigger: _jsxs(Button, { type: "button", variant: "ghost", className: "h-8 gap-1 text-muted-foreground", children: ["Template", _jsx(ChevronDownIcon, { size: 14 })] }), children: [_jsx("div", { className: "max-h-80 overflow-y-auto", children: templatesByCategory.map(({ category, templates }) => {
391
414
  var _a;
392
415
  return (_jsxs("div", { children: [_jsx("div", { className: "sticky top-0 bg-muted px-3 py-2 text-sm font-semibold text-foreground", children: (_a = templateCategoryLabels[category]) !== null && _a !== void 0 ? _a : category }), templates.map((template) => (_jsxs("button", { type: "button", className: "w-full border-b border-border/50 px-3 py-2 text-left text-sm transition-colors last:border-0 hover:bg-muted", onClick: () => applyTemplate(template), children: [_jsx("div", { className: "truncate font-medium", children: template.name }), template.description && (_jsx("div", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: template.description }))] }, template.id)))] }, category));
393
- }) }), templateCreateHref && (_jsx("div", { className: "border-t p-2", children: _jsxs("a", { href: templateCreateHref, target: "_blank", rel: "noopener noreferrer", className: "flex w-full items-center justify-center gap-1.5 rounded-md px-3 py-2 text-sm text-interactive transition-colors hover:bg-muted", children: [_jsx(PlusIcon, { size: 16 }), templateCreateLabel] }) }))] }) }))] }) }), _jsx("div", { className: "bg-muted px-3 pb-2 pt-1 dark:bg-background", children: sendAction })] }));
416
+ }) }), templateCreateHref && (_jsx("div", { className: "border-t p-2", children: _jsxs("a", { href: templateCreateHref, target: "_blank", rel: "noopener noreferrer", className: "flex w-full items-center justify-center gap-1.5 rounded-md px-3 py-2 text-sm text-interactive transition-colors hover:bg-muted", children: [_jsx(PlusIcon, { size: 16 }), templateCreateLabel] }) }))] }) }))] }) }), _jsx("div", { className: "bg-muted px-3 pb-2 pt-1 dark:bg-background", children: renderSendAction({ fullWidth: true }) })] }));
394
417
  }
@@ -0,0 +1,38 @@
1
+ import * as React from "react";
2
+ import { type DashboardActionPanelProps } from "./layout";
3
+ import { type RoutingTimelineItem } from "./routing-timeline";
4
+ import { type SelectOption } from "./select";
5
+ export interface RoutingActionPanelProps extends Omit<DashboardActionPanelProps, "children"> {
6
+ items: RoutingTimelineItem[];
7
+ isLoading?: boolean;
8
+ loadingLabel?: string;
9
+ emptyMessage?: React.ReactNode;
10
+ emptyDescription?: React.ReactNode;
11
+ routeOptions: SelectOption[];
12
+ routeValue?: string;
13
+ routePlaceholder?: string;
14
+ routeDescription?: React.ReactNode;
15
+ routeDisabled?: boolean;
16
+ onRouteValueChange?: (value: string) => void;
17
+ /** Workflow-specific fields rendered before the route target. */
18
+ supplementaryFields?: React.ReactNode;
19
+ remarks: string;
20
+ remarksDescription?: React.ReactNode;
21
+ remarksPlaceholder?: string;
22
+ remarksRequired?: boolean;
23
+ remarksRows?: number;
24
+ /** Blocking validation messages, collapsed above the submit action. */
25
+ validationMessages?: readonly string[];
26
+ onRemarksChange: (value: string) => void;
27
+ submitLabel?: React.ReactNode;
28
+ submitDisabled?: boolean;
29
+ submitLoading?: boolean;
30
+ onSubmit: () => void;
31
+ }
32
+ /**
33
+ * Shared routing surface for constrained dashboard popovers and right panels.
34
+ * Consumers provide workflow data and actions; the component owns the complete
35
+ * visual composition so different records render routing consistently.
36
+ */
37
+ declare function RoutingActionPanel({ items, isLoading, loadingLabel, emptyMessage, emptyDescription, routeOptions, routeValue, routePlaceholder, routeDescription, routeDisabled, onRouteValueChange, supplementaryFields, remarks, remarksDescription, remarksPlaceholder, remarksRequired, remarksRows, validationMessages, onRemarksChange, submitLabel, submitDisabled, submitLoading, onSubmit, ...panelProps }: RoutingActionPanelProps): import("react/jsx-runtime").JSX.Element;
38
+ export { RoutingActionPanel };
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Route } from "lucide-react";
4
+ import { Button } from "./button";
5
+ import { DashboardActionPanel, DashboardActionPanelEmptyState, DashboardRightPanelBody, } from "./layout";
6
+ import { RoutingTimeline, } from "./routing-timeline";
7
+ import { Select } from "./select";
8
+ import { Spinner } from "./spinner";
9
+ import { Textarea } from "./textarea";
10
+ /**
11
+ * Shared routing surface for constrained dashboard popovers and right panels.
12
+ * Consumers provide workflow data and actions; the component owns the complete
13
+ * visual composition so different records render routing consistently.
14
+ */
15
+ function RoutingActionPanel({ items, isLoading = false, loadingLabel = "Loading routing activity", emptyMessage = "No routing activity yet", emptyDescription = "Workflow routes will appear here.", routeOptions, routeValue, routePlaceholder = "Select Officer Role", routeDescription, routeDisabled = false, onRouteValueChange, supplementaryFields, remarks, remarksDescription, remarksPlaceholder = "Type remarks", remarksRequired = false, remarksRows = 2, validationMessages = [], onRemarksChange, submitLabel = "Route", submitDisabled = false, submitLoading = false, onSubmit, ...panelProps }) {
16
+ const footer = (_jsxs("div", { "data-slot": "routing-action-panel-form", className: "grid gap-[var(--space-4)]", children: [supplementaryFields, _jsx(Select, { label: "Route To", options: routeOptions, value: routeValue, placeholder: routePlaceholder, disabled: routeDisabled || submitLoading, description: routeDescription, onValueChange: onRouteValueChange }), _jsx(Textarea, { label: "Remarks", value: remarks, onChange: (event) => onRemarksChange(event.target.value), placeholder: remarksPlaceholder, description: remarksDescription, disabled: routeDisabled || submitLoading, required: remarksRequired, rows: remarksRows }), validationMessages.length > 0 ? (_jsxs("details", { className: "text-sm text-muted-foreground", children: [_jsxs("summary", { className: "cursor-pointer font-medium text-foreground", children: [validationMessages.length, " required ", validationMessages.length === 1 ? "item" : "items", " remaining \u2014 view details"] }), _jsx("ul", { className: "mt-2 max-h-40 list-disc space-y-1 overflow-y-auto pl-5", children: validationMessages.map((message, index) => _jsx("li", { children: message }, index)) })] })) : null, _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", disabled: routeDisabled || submitDisabled || validationMessages.length > 0, loading: submitLoading, onClick: onSubmit, children: [_jsx(Route, { "aria-hidden": "true", className: "size-4" }), submitLabel] }) })] }));
17
+ return (_jsx(DashboardActionPanel, { headerSpacing: "comfortable", ...panelProps, children: _jsx(DashboardRightPanelBody, { spacing: "comfortable", topInset: "none", footerVariant: "separated", footer: footer, children: isLoading ? (_jsx("div", { "data-slot": "routing-action-panel-loading", className: "absolute inset-0 flex items-center justify-center", children: _jsx(Spinner, { size: "md", label: loadingLabel }) })) : items.length === 0 ? (_jsx(DashboardActionPanelEmptyState, { icon: Route, message: emptyMessage, description: emptyDescription })) : (_jsx(RoutingTimeline, { density: "compact", items: items })) }) }));
18
+ }
19
+ export { RoutingActionPanel };
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- export type RoutingTimelineItemKind = "assigned" | "reassigned" | "returned" | "routed" | "system";
2
+ export type RoutingTimelineItemKind = "assigned" | "reassigned" | "returned" | "routed" | "message-received" | "message-sent" | "system";
3
3
  export interface RoutingTimelineItem {
4
4
  id: string;
5
5
  kind?: RoutingTimelineItemKind;
@@ -8,15 +8,20 @@ export interface RoutingTimelineItem {
8
8
  actor?: React.ReactNode;
9
9
  remark?: React.ReactNode;
10
10
  description?: React.ReactNode;
11
+ detailLabel?: React.ReactNode;
11
12
  from?: React.ReactNode;
12
13
  to?: React.ReactNode;
13
14
  fromLabel?: React.ReactNode;
14
15
  toLabel?: React.ReactNode;
15
16
  badgeLabel?: React.ReactNode;
17
+ badge?: React.ReactNode;
18
+ icon?: React.ReactNode;
16
19
  }
17
20
  export interface RoutingTimelineProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
18
21
  items: RoutingTimelineItem[];
19
22
  emptyMessage?: React.ReactNode;
23
+ /** Reduces typography, icon, padding, and event spacing for constrained panels. */
24
+ density?: "default" | "compact";
20
25
  }
21
- declare function RoutingTimeline({ items, emptyMessage, className, ...props }: RoutingTimelineProps): import("react/jsx-runtime").JSX.Element;
26
+ declare function RoutingTimeline({ items, emptyMessage, density, className, ...props }: RoutingTimelineProps): import("react/jsx-runtime").JSX.Element;
22
27
  export { RoutingTimeline };
@@ -1,58 +1,96 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { ArrowDown } from "lucide-react";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowRight, Mail, Route } from "lucide-react";
3
+ import { ActivityCard, ActivityCardContent } from "./activity-card";
4
+ import { AvatarProfile } from "./avatar";
3
5
  import { Badge } from "./badge";
4
6
  import { EmptyState } from "./empty-state";
5
7
  import { cn } from "./lib/utils";
6
8
  import { Typography } from "./typography";
7
9
  const routingKindConfig = {
8
10
  assigned: {
9
- title: "Assigned Case",
10
- label: "Assigned",
11
- badgeVariant: "info",
12
- markerClassName: "border-[hsl(var(--status-info-foreground)/0.35)] bg-[hsl(var(--status-info))]",
11
+ title: "Assigned case",
12
+ label: "ASSIGNED",
13
+ icon: "route",
13
14
  },
14
15
  reassigned: {
15
- title: "Reassigned Case",
16
- label: "Reassigned",
17
- badgeVariant: "accent",
18
- markerClassName: "border-[hsl(var(--status-accent-foreground)/0.35)] bg-[hsl(var(--status-accent))]",
16
+ title: "Reassigned case",
17
+ label: "REASSIGNED",
18
+ icon: "route",
19
19
  },
20
20
  returned: {
21
- title: "Returned Case",
22
- label: "Returned",
23
- badgeVariant: "warning",
24
- markerClassName: "border-[hsl(var(--status-warning-foreground)/0.35)] bg-[hsl(var(--status-warning))]",
21
+ title: "Case returned",
22
+ label: "RETURNED",
23
+ icon: "route",
25
24
  },
26
25
  routed: {
27
- title: "Case Transferred",
28
- label: "Routed",
29
- badgeVariant: "secondary",
30
- markerClassName: "border-primary/30 bg-primary/15",
26
+ title: "Case routed",
27
+ label: "ROUTED",
28
+ icon: "route",
29
+ },
30
+ "message-received": {
31
+ title: "Received from applicant",
32
+ label: "RECEIVED",
33
+ detailLabel: "Response from applicant:",
34
+ icon: "mail",
35
+ },
36
+ "message-sent": {
37
+ title: "Sent to applicant",
38
+ label: "SENT",
39
+ detailLabel: "Officer sent a message:",
40
+ icon: "mail",
31
41
  },
32
42
  system: {
33
- title: "System Update",
34
- label: "System",
35
- badgeVariant: "muted",
36
- markerClassName: "border-muted-foreground/30 bg-muted",
43
+ title: "System update",
44
+ label: "SYSTEM",
45
+ icon: "route",
37
46
  },
38
47
  };
39
- function RoutingTimelineHandoffEndpoint({ value, }) {
48
+ function RoutingTimelineHandoffEndpoint({ value, label, }) {
40
49
  const isTextValue = typeof value === "string" || typeof value === "number";
41
- return isTextValue ? (_jsx("span", { className: "min-w-0 truncate font-medium text-card-foreground", children: value })) : (_jsx("div", { className: "min-w-0", children: value }));
50
+ if (!isTextValue) {
51
+ return (_jsx("div", { "data-slot": "routing-timeline-profile", "data-avatar-size": "xs", className: "min-w-0", children: value }));
52
+ }
53
+ const name = String(value);
54
+ const fallback = name
55
+ .split(/\s+/)
56
+ .filter(Boolean)
57
+ .slice(0, 2)
58
+ .map((part) => part.charAt(0))
59
+ .join("")
60
+ .toUpperCase();
61
+ return (_jsx("div", { "data-slot": "routing-timeline-profile", "data-avatar-size": "xs", className: "min-w-0", children: _jsx(AvatarProfile, { size: "xs", fallback: fallback || "?", name: name, role: label, rolePlacement: "below" }) }));
62
+ }
63
+ function RoutingEventIcon({ icon, kind, density, }) {
64
+ if (icon)
65
+ return _jsx(_Fragment, { children: icon });
66
+ return routingKindConfig[kind].icon === "mail" ? (_jsx(Mail, { "aria-hidden": "true", className: density === "compact" ? "size-4" : "size-6" })) : (_jsx(Route, { "aria-hidden": "true", className: density === "compact" ? "size-4" : "size-6" }));
42
67
  }
43
- function RoutingTimeline({ items, emptyMessage = "No routing activity yet.", className, ...props }) {
68
+ function RoutingTimeline({ items, emptyMessage = "No routing activity yet.", density = "default", className, ...props }) {
44
69
  if (items.length === 0) {
45
- return (_jsx(EmptyState, { "data-slot": "routing-timeline", message: emptyMessage, className: cn("border-0 bg-transparent px-0 py-[var(--space-6)]", className), ...props }));
70
+ return (_jsx(EmptyState, { "data-slot": "routing-timeline", message: emptyMessage, className: cn("border-0 bg-transparent px-0", density === "compact"
71
+ ? "py-[var(--space-4)]"
72
+ : "py-[var(--space-6)]", className), ...props }));
46
73
  }
47
- return (_jsx("div", { "data-slot": "routing-timeline", className: cn("min-w-0", className), ...props, children: _jsx("ol", { "data-slot": "routing-timeline-list", className: "relative space-y-0", children: items.map((item) => {
48
- var _a, _b, _c, _d;
74
+ return (_jsx("div", { "data-slot": "routing-timeline", "data-density": density, className: cn("min-w-0", className), ...props, children: _jsx("ol", { "data-slot": "routing-timeline-list", className: cn(density === "compact"
75
+ ? "space-y-[var(--space-4)]"
76
+ : "space-y-[32px]"), children: items.map((item) => {
77
+ var _a, _b, _c, _d, _e;
49
78
  const kind = (_a = item.kind) !== null && _a !== void 0 ? _a : "routed";
50
79
  const config = routingKindConfig[kind];
51
80
  const hasRouteEndpoints = Boolean(item.from || item.to);
52
81
  const hasCompleteRoute = Boolean(item.from && item.to);
53
82
  const title = (_b = item.title) !== null && _b !== void 0 ? _b : config.title;
54
83
  const remark = (_c = item.remark) !== null && _c !== void 0 ? _c : item.description;
55
- return (_jsxs("li", { className: "relative grid grid-cols-[var(--space-3)_minmax(0,1fr)] gap-[var(--space-3)] pb-[var(--space-5)] last:pb-0", children: [_jsx("div", { className: "relative flex justify-center", children: _jsx("span", { "aria-hidden": "true", "data-slot": "routing-timeline-marker", className: cn("relative z-10 mt-[var(--space-1)] size-[var(--space-2)] rounded-full border", config.markerClassName) }) }), _jsxs("div", { "data-slot": "routing-timeline-event-card", className: "min-w-0 rounded-surface border border-border/70 bg-card p-[var(--space-3)] text-card-foreground", children: [_jsxs("div", { className: "min-w-0 space-y-[var(--space-1)]", children: [_jsxs("div", { className: "flex min-w-0 items-start justify-between gap-[var(--space-2)]", children: [_jsx(Typography, { as: "p", variant: "label", className: "min-w-0 flex-1 font-semibold", children: title }), _jsx(Badge, { variant: config.badgeVariant, bordered: true, className: "max-w-full shrink-0 self-start", children: (_d = item.badgeLabel) !== null && _d !== void 0 ? _d : config.label })] }), item.timestamp || item.actor ? (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-[var(--space-1)] gap-y-[var(--space-1)]", children: [item.timestamp ? (_jsx(Typography, { as: "time", variant: "caption", className: "shrink-0 text-muted-foreground/60", children: item.timestamp })) : null, item.actor ? (_jsxs(Typography, { as: "p", variant: "caption", className: "inline-flex min-w-0 items-center gap-x-[var(--space-1)]", children: [item.timestamp ? (_jsx("span", { "aria-hidden": "true", children: "\u00B7" })) : null, "By ", item.actor] })) : null] })) : null] }), remark ? (_jsx(Typography, { as: "div", variant: "bodySm", "data-slot": "routing-timeline-remark", className: "mt-[var(--space-4)] font-normal", children: remark })) : null, hasRouteEndpoints ? (_jsxs("footer", { "data-slot": "routing-timeline-handoff", "data-complete-route": hasCompleteRoute ? "true" : undefined, className: "mt-[var(--space-4)] flex flex-col space-y-[var(--space-2)] text-xs text-muted-foreground", children: [item.from ? (_jsx(RoutingTimelineHandoffEndpoint, { value: item.from })) : null, item.from && item.to ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sr-only", children: "to" }), _jsx(ArrowDown, { "aria-hidden": "true", "data-slot": "routing-timeline-handoff-arrow", className: "ml-1 size-4 shrink-0 text-muted-foreground" })] })) : null, item.to ? (_jsx(RoutingTimelineHandoffEndpoint, { value: item.to })) : null] })) : null] })] }, item.id));
84
+ const detailLabel = (_d = item.detailLabel) !== null && _d !== void 0 ? _d : config.detailLabel;
85
+ const showDefaultBadge = item.badgeLabel !== null;
86
+ return (_jsx("li", { children: _jsx(ActivityCard, { "data-slot": "routing-timeline-event-card", density: density, icon: _jsx(RoutingEventIcon, { icon: item.icon, kind: kind, density: density }), title: title, meta: item.timestamp, trailing: item.badge ? (item.badge) : showDefaultBadge ? (_jsx(Badge, { variant: "outline", bordered: true, className: "max-w-full shrink-0", children: (_e = item.badgeLabel) !== null && _e !== void 0 ? _e : config.label })) : null, children: hasRouteEndpoints || remark ? (_jsxs(ActivityCardContent, { density: density, children: [hasRouteEndpoints ? (_jsxs("div", { "data-slot": "routing-timeline-handoff", "data-complete-route": hasCompleteRoute ? "true" : undefined, className: cn("flex min-w-0 items-center", density === "compact"
87
+ ? "gap-[var(--space-3)]"
88
+ : "gap-[24px]"), children: [item.from ? (_jsx(RoutingTimelineHandoffEndpoint, { value: item.from, label: item.fromLabel })) : null, item.from && item.to ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sr-only", children: "to" }), _jsx(ArrowRight, { "aria-hidden": "true", "data-slot": "routing-timeline-handoff-arrow", className: cn("shrink-0 text-foreground", density === "compact" ? "size-4" : "size-6") })] })) : null, item.to ? (_jsx(RoutingTimelineHandoffEndpoint, { value: item.to, label: item.toLabel })) : null] })) : null, remark ? (_jsxs("div", { "data-slot": "routing-timeline-remark", className: cn("bg-muted", density === "compact"
89
+ ? "rounded-control p-[var(--space-2)]"
90
+ : "rounded-md p-[12px]"), children: [detailLabel ? (_jsx(Typography, { as: "p", variant: "bodySm", className: "font-semibold text-instructional", children: detailLabel })) : null, _jsx(Typography, { as: "div", variant: "bodySm", className: cn(detailLabel &&
91
+ (density === "compact"
92
+ ? "mt-[var(--space-1)]"
93
+ : "mt-[4px]")), children: remark })] })) : null] })) : null }) }, item.id));
56
94
  }) }) }));
57
95
  }
58
96
  export { RoutingTimeline };
@@ -1,12 +1,18 @@
1
1
  import * as React from "react";
2
2
  import { InputControl } from "./input-control";
3
3
  export interface SearchInputProps extends Omit<React.ComponentProps<typeof InputControl>, "onChange" | "type" | "value"> {
4
+ /** Display a character counter when maxLength is set. */
5
+ showCount?: boolean;
4
6
  value: string;
5
7
  onValueChange: (value: string) => void;
6
8
  label?: React.ReactNode;
9
+ /** Visually hides the label while keeping it available to assistive technology. */
10
+ hideLabel?: boolean;
7
11
  description?: React.ReactNode;
8
12
  error?: React.ReactNode;
9
13
  invalid?: boolean;
14
+ /** Keeps the control visually marked after a search result is selected. */
15
+ selected?: boolean;
10
16
  containerClassName?: string;
11
17
  labelClassName?: string;
12
18
  descriptionClassName?: string;
@@ -4,9 +4,10 @@ import * as React from "react";
4
4
  import { XIcon } from "./animated-icons/x";
5
5
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
6
6
  import { InputControl } from "./input-control";
7
+ import { FieldCounter } from "./field-counter";
7
8
  import { cn } from "./lib/utils";
8
9
  import { SearchInputShell } from "./search-input-shell";
9
- const SearchInput = React.forwardRef(({ value, onValueChange, label = "Search", description, error, invalid = false, placeholder = "Search...", containerClassName, labelClassName, descriptionClassName, errorClassName, onSearch, searchDelay = 300, searchOnChange = true, searchOnEnter = true, className, id, disabled, onKeyDown, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, ...props }, ref) => {
10
+ const SearchInput = React.forwardRef(({ value, showCount = false, onValueChange, label = "Search", hideLabel = true, description, error, invalid = false, selected = false, placeholder = "Search...", containerClassName, labelClassName, descriptionClassName, errorClassName, onSearch, searchDelay = 300, searchOnChange = true, searchOnEnter = true, className, id, disabled, onKeyDown, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, ...props }, ref) => {
10
11
  const searchTimeoutRef = React.useRef();
11
12
  React.useEffect(() => {
12
13
  return () => {
@@ -17,6 +18,7 @@ const SearchInput = React.forwardRef(({ value, onValueChange, label = "Search",
17
18
  }, []);
18
19
  const generatedId = React.useId();
19
20
  const inputId = id !== null && id !== void 0 ? id : `search-input-${generatedId}`;
21
+ const limitId = showCount && props.maxLength !== undefined ? `${inputId}-limit` : undefined;
20
22
  const errorId = error ? `${inputId}-error` : undefined;
21
23
  const showDescription = Boolean(description) && !error;
22
24
  const descriptionId = showDescription ? `${inputId}-description` : undefined;
@@ -24,7 +26,7 @@ const SearchInput = React.forwardRef(({ value, onValueChange, label = "Search",
24
26
  ariaInvalid === true ||
25
27
  ariaInvalid === "true" ||
26
28
  Boolean(error);
27
- const describedBy = [ariaDescribedBy, descriptionId, errorId]
29
+ const describedBy = [ariaDescribedBy, descriptionId, errorId, limitId]
28
30
  .filter(Boolean)
29
31
  .join(" ") || undefined;
30
32
  const runSearch = React.useCallback((nextValue) => {
@@ -64,10 +66,10 @@ const SearchInput = React.forwardRef(({ value, onValueChange, label = "Search",
64
66
  }
65
67
  onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
66
68
  }, [onKeyDown, runSearch, searchOnEnter]);
67
- return (_jsxs(Field, { className: containerClassName, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, className: cn("sr-only", labelClassName), children: label }), _jsxs(SearchInputShell, { children: [_jsx(InputControl, { ref: ref, id: inputId, type: "text", placeholder: placeholder, value: value, onChange: handleChange, onKeyDown: handleKeyDown, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": describedBy, className: cn("bg-background pl-8 pr-7", className), ...props }), value ? (_jsx("button", { type: "button", onClick: () => {
69
+ return (_jsxs(Field, { className: containerClassName, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs(SearchInputShell, { children: [_jsx(InputControl, { ref: ref, id: inputId, type: "text", placeholder: placeholder, value: value, onChange: handleChange, onKeyDown: handleKeyDown, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": describedBy, "data-selected": selected ? "true" : undefined, className: cn("bg-background pl-8 pr-7 data-[selected=true]:border-interactive data-[selected=true]:bg-highlight data-[selected=true]:ring-[3px] data-[selected=true]:ring-interactive/20", className), ...props }), value ? (_jsx("button", { type: "button", onClick: () => {
68
70
  onValueChange("");
69
71
  runSearch("");
70
- }, disabled: disabled, className: "absolute right-1.5 top-1/2 -translate-y-1/2 cursor-pointer rounded-sm p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:pointer-events-none", "aria-label": "Clear search", children: _jsx(XIcon, { size: 14 }) })) : null] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
72
+ }, disabled: disabled, className: "absolute right-1.5 top-1/2 -translate-y-1/2 cursor-pointer rounded-sm p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:pointer-events-none", "aria-label": "Clear search", children: _jsx(XIcon, { size: 14 }) })) : null] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, limitId && _jsx(FieldCounter, { id: limitId, count: value.length, limit: props.maxLength }), _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
71
73
  });
72
74
  SearchInput.displayName = "SearchInput";
73
75
  export { SearchInput };
@@ -36,7 +36,7 @@ const SelectContent = React.forwardRef(({ className, children, header, position
36
36
  SelectContent.displayName = SelectPrimitive.Content.displayName;
37
37
  const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Label, { ref: ref, "data-slot": "select-label", className: cn("px-2 py-1.5 text-xs text-muted-foreground", className), ...props })));
38
38
  SelectLabel.displayName = SelectPrimitive.Label.displayName;
39
- const SelectItem = React.forwardRef(({ className, children, description, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, "data-slot": "select-item", className: cn("relative flex min-h-8 w-full cursor-pointer select-none items-center gap-1.5 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[state=checked]:bg-highlight data-[state=checked]:text-highlight-foreground data-[state=checked]:hover:bg-highlight/80 data-[state=checked]:hover:text-highlight-foreground data-[state=checked]:focus:bg-highlight/80 data-[state=checked]:focus:text-highlight-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { "data-slot": "select-item-indicator", className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(SelectPrimitive.ItemText, { className: "break-words font-normal", children: children }), description ? (_jsx(Typography, { as: "span", variant: "caption", children: description })) : null] })] })));
39
+ const SelectItem = React.forwardRef(({ className, children, description, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, "data-slot": "select-item", className: cn("relative flex min-h-8 w-full cursor-pointer select-none items-center gap-1.5 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[state=checked]:bg-highlight data-[state=checked]:text-highlight-foreground data-[state=checked]:hover:bg-highlight/80 data-[state=checked]:hover:text-highlight-foreground data-[state=checked]:focus:bg-highlight/80 data-[state=checked]:focus:text-highlight-foreground data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:hover:bg-transparent data-[disabled]:hover:text-foreground", className), ...props, children: [_jsx("span", { "data-slot": "select-item-indicator", className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(SelectPrimitive.ItemText, { className: "break-words font-normal", children: children }), description ? (_jsx(Typography, { as: "span", variant: "caption", children: description })) : null] })] })));
40
40
  SelectItem.displayName = SelectPrimitive.Item.displayName;
41
41
  const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Separator, { ref: ref, "data-slot": "select-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props })));
42
42
  SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
package/dist/select.d.ts CHANGED
@@ -5,6 +5,7 @@ export interface SelectOption {
5
5
  value: string;
6
6
  description?: React.ReactNode;
7
7
  disabled?: boolean;
8
+ disabledReason?: React.ReactNode;
8
9
  group?: string;
9
10
  textValue?: string;
10
11
  }
@@ -12,6 +13,7 @@ export interface SelectProps extends Pick<React.ComponentProps<typeof SelectRoot
12
13
  label: React.ReactNode;
13
14
  options: SelectOption[];
14
15
  placeholder?: string;
16
+ size?: "default" | "comfortable";
15
17
  description?: React.ReactNode;
16
18
  error?: React.ReactNode;
17
19
  invalid?: boolean;
@@ -28,5 +30,5 @@ export interface SelectProps extends Pick<React.ComponentProps<typeof SelectRoot
28
30
  errorClassName?: string;
29
31
  id?: string;
30
32
  }
31
- declare function Select({ label, options, placeholder, description, error, invalid, hideLabel, searchable, searchPlaceholder, searchEmptyMessage, searchInputAriaLabel, className, triggerClassName, contentClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, open, onOpenChange, ...props }: SelectProps): import("react/jsx-runtime").JSX.Element;
33
+ declare function Select({ label, options, placeholder, size, description, error, invalid, hideLabel, searchable, searchPlaceholder, searchEmptyMessage, searchInputAriaLabel, className, triggerClassName, contentClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, open, onOpenChange, ...props }: SelectProps): import("react/jsx-runtime").JSX.Element;
32
34
  export { Select };
package/dist/select.js CHANGED
@@ -5,6 +5,7 @@ import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
5
5
  import { InputControl } from "./input-control";
6
6
  import { cn } from "./lib/utils";
7
7
  import { usePopoverTabExit } from "./popover-focus";
8
+ import { Tooltip } from "./tooltip";
8
9
  import { SearchHighlight } from "./use-search-highlight";
9
10
  import { SelectContent, SelectGroup, SelectItem, SelectLabel, SelectRoot, SelectSeparator, SelectTrigger, SelectValue, } from "./select-primitive";
10
11
  function groupOptions(options) {
@@ -40,7 +41,7 @@ function renderHighlightedLabel(label, query) {
40
41
  }
41
42
  return label;
42
43
  }
43
- function Select({ label, options, placeholder = "Select an option", description, error, invalid = false, hideLabel = false, searchable = false, searchPlaceholder = "Search options...", searchEmptyMessage = "No options found.", searchInputAriaLabel = "Search options", className, triggerClassName, contentClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, open, onOpenChange, ...props }) {
44
+ function Select({ label, options, placeholder = "Select an option", size = "default", description, error, invalid = false, hideLabel = false, searchable = false, searchPlaceholder = "Search options...", searchEmptyMessage = "No options found.", searchInputAriaLabel = "Search options", className, triggerClassName, contentClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, open, onOpenChange, ...props }) {
44
45
  const generatedId = React.useId();
45
46
  const selectId = id !== null && id !== void 0 ? id : `select-${generatedId}`;
46
47
  const triggerRef = React.useRef(null);
@@ -153,7 +154,8 @@ function Select({ label, options, placeholder = "Select an option", description,
153
154
  searchable,
154
155
  searchQuery,
155
156
  ]);
156
- return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: selectId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs(SelectRoot, { disabled: disabled, open: isOpen, onOpenChange: handleOpenChange, ...props, children: [_jsx(SelectTrigger, { ref: triggerRef, id: selectId, className: triggerClassName, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { ref: contentRef, className: contentClassName, onCloseAutoFocus: handleCloseAutoFocus, onKeyDownCapture: handleContentKeyDownCapture, header: searchable ? (_jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), placeholder: searchPlaceholder, "aria-label": searchInputAriaLabel, autoComplete: "off", className: "h-control border-0 bg-transparent px-2 py-0 text-sm shadow-none", onKeyDown: (event) => {
157
+ return (_jsxs(Field, { className: cn(size === "comfortable" && "gap-[8px]", className), "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: selectId, className: cn(size === "comfortable" && "text-[16px] leading-[24px]", hideLabel && "sr-only", labelClassName), children: label }), _jsxs(SelectRoot, { disabled: disabled, open: isOpen, onOpenChange: handleOpenChange, ...props, children: [_jsx(SelectTrigger, { ref: triggerRef, id: selectId, className: cn(size === "comfortable" &&
158
+ "h-[46px] px-[16px] py-[11px] text-[16px] leading-[24px]", triggerClassName), "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { ref: contentRef, className: contentClassName, onCloseAutoFocus: handleCloseAutoFocus, onKeyDownCapture: handleContentKeyDownCapture, header: searchable ? (_jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), placeholder: searchPlaceholder, "aria-label": searchInputAriaLabel, autoComplete: "off", className: "h-control border-0 bg-transparent px-2 py-0 text-sm shadow-none", onKeyDown: (event) => {
157
159
  if (event.key === "ArrowDown" ||
158
160
  event.key === "ArrowUp") {
159
161
  event.preventDefault();
@@ -167,7 +169,10 @@ function Select({ label, options, placeholder = "Select an option", description,
167
169
  event.stopPropagation();
168
170
  } })) : undefined, viewportClassName: searchable ? "max-h-64" : undefined, children: hasVisibleOptions ? (optionGroups.map((group, groupIndex) => {
169
171
  var _a;
170
- return (_jsxs(React.Fragment, { children: [_jsxs(SelectGroup, { children: [group.group ? (_jsx(SelectLabel, { children: _jsx(SearchHighlight, { text: group.group, query: searchQuery }) })) : null, group.options.map((option) => (_jsx(SelectItem, { value: option.value, disabled: option.disabled, textValue: getOptionTextValue(option), description: option.description, children: renderHighlightedLabel(option.label, searchQuery) }, option.value)))] }), groupIndex < optionGroups.length - 1 ? _jsx(SelectSeparator, {}) : null] }, (_a = group.group) !== null && _a !== void 0 ? _a : `group-${groupIndex}`));
171
- })) : (_jsx("div", { className: "px-2.5 py-1.5 text-sm text-muted-foreground", children: searchEmptyMessage })) })] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
172
+ return (_jsxs(React.Fragment, { children: [_jsxs(SelectGroup, { children: [group.group ? (_jsx(SelectLabel, { children: _jsx(SearchHighlight, { text: group.group, query: searchQuery }) })) : null, group.options.map((option) => {
173
+ const optionItem = (_jsx(SelectItem, { value: option.value, disabled: option.disabled, textValue: getOptionTextValue(option), description: option.description, children: renderHighlightedLabel(option.label, searchQuery) }));
174
+ return option.disabled && option.disabledReason ? (_jsx(Tooltip, { content: option.disabledReason, side: "right", children: optionItem }, option.value)) : (_jsx(React.Fragment, { children: optionItem }, option.value));
175
+ })] }), groupIndex < optionGroups.length - 1 ? (_jsx(SelectSeparator, {})) : null] }, (_a = group.group) !== null && _a !== void 0 ? _a : `group-${groupIndex}`));
176
+ })) : (_jsx("div", { className: "px-2.5 py-1.5 text-sm text-muted-foreground", children: searchEmptyMessage })) })] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: cn(size === "comfortable" && "text-[14px] leading-[20px]", descriptionClassName), children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
172
177
  }
173
178
  export { Select };
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ export type StatusDotVariant = "default" | "muted" | "success" | "warning" | "info" | "destructive";
3
+ export type StatusDotSize = "sm" | "md" | "lg";
4
+ export interface StatusDotProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
5
+ /** Semantic colour of the status. */
6
+ variant?: StatusDotVariant;
7
+ /** Diameter of the dot. */
8
+ size?: StatusDotSize;
9
+ /** Accessible name. Omit when nearby text already describes the status. */
10
+ label?: string;
11
+ }
12
+ declare const StatusDot: React.ForwardRefExoticComponent<StatusDotProps & React.RefAttributes<HTMLSpanElement>>;
13
+ export { StatusDot };
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "./lib/utils";
4
+ const sizeClasses = {
5
+ sm: "size-[6px]",
6
+ md: "size-[8px]",
7
+ lg: "size-[10px]",
8
+ };
9
+ const variantClasses = {
10
+ default: "bg-primary",
11
+ muted: "bg-muted-foreground",
12
+ success: "bg-[hsl(var(--nea-green-400))]",
13
+ warning: "bg-[hsl(var(--nea-amber-400))]",
14
+ info: "bg-[hsl(var(--nea-blue-400))]",
15
+ destructive: "bg-[hsl(var(--nea-red-400))]",
16
+ };
17
+ const StatusDot = React.forwardRef(({ variant = "default", size = "md", label, className, ...props }, ref) => (_jsx("span", { ref: ref, "data-slot": "status-dot", role: label ? "img" : undefined, "aria-label": label, "aria-hidden": label ? undefined : true, className: cn("inline-block shrink-0 rounded-full", sizeClasses[size], variantClasses[variant], className), ...props })));
18
+ StatusDot.displayName = "StatusDot";
19
+ export { StatusDot };
package/dist/styles.css CHANGED
@@ -413,25 +413,6 @@
413
413
  }
414
414
 
415
415
  @layer components {
416
- [data-slot="routing-timeline"] {
417
- --routing-timeline-background: var(--layout-right-panel-background);
418
- }
419
-
420
- [data-slot="routing-timeline-list"]::before {
421
- position: absolute;
422
- top: var(--space-2);
423
- bottom: var(--space-0);
424
- left: calc(var(--space-3) / 2);
425
- width: 1px;
426
- content: "";
427
- transform: translateX(-50%);
428
- background: hsl(var(--border));
429
- }
430
-
431
- [data-slot="routing-timeline-marker"] {
432
- box-shadow: 0 0 0 var(--space-1) hsl(var(--routing-timeline-background));
433
- }
434
-
435
416
  [data-slot="routing-timeline-event-card"] {
436
417
  container-type: inline-size;
437
418
  }
@@ -0,0 +1,8 @@
1
+ import * as React from "react";
2
+ export interface TableAddRowProps {
3
+ children?: React.ReactNode;
4
+ onClick: React.MouseEventHandler<HTMLButtonElement>;
5
+ disabled?: boolean;
6
+ }
7
+ /** Shared full-width add action beneath editable tables. */
8
+ export declare function TableAddRow({ children, onClick, disabled }: TableAddRowProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "./button";
3
+ import { PlusIcon } from "./animated-icons/plus";
4
+ /** Shared full-width add action beneath editable tables. */
5
+ export function TableAddRow({ children = "Add Row", onClick, disabled }) {
6
+ return (_jsxs(Button, { type: "button", variant: "outline", onClick: onClick, disabled: disabled, className: "w-full justify-center", children: [_jsx(PlusIcon, { size: 16 }), children] }));
7
+ }