@neasg/design-system 0.4.95 → 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 (43) hide show
  1. package/README.md +9 -0
  2. package/dist/activity-card.d.ts +2 -0
  3. package/dist/activity-card.js +2 -2
  4. package/dist/alert.d.ts +1 -1
  5. package/dist/badge.d.ts +1 -1
  6. package/dist/button.d.ts +1 -1
  7. package/dist/button.js +2 -1
  8. package/dist/changelog-action-panel.d.ts +17 -0
  9. package/dist/changelog-action-panel.js +14 -0
  10. package/dist/checklist.d.ts +63 -0
  11. package/dist/checklist.js +30 -0
  12. package/dist/compound-search-input.d.ts +2 -0
  13. package/dist/compound-search-input.js +13 -3
  14. package/dist/date-input.js +2 -2
  15. package/dist/dialog.d.ts +2 -1
  16. package/dist/dialog.js +4 -3
  17. package/dist/dropdown-button.js +1 -0
  18. package/dist/editable-table.js +2 -2
  19. package/dist/field-counter.d.ts +7 -0
  20. package/dist/field-counter.js +5 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.js +6 -0
  23. package/dist/input.d.ts +2 -0
  24. package/dist/input.js +11 -3
  25. package/dist/message-action-panel.d.ts +12 -0
  26. package/dist/message-action-panel.js +11 -0
  27. package/dist/notification.js +8 -4
  28. package/dist/page-section.d.ts +2 -1
  29. package/dist/page-section.js +2 -2
  30. package/dist/popover.d.ts +5 -0
  31. package/dist/popover.js +4 -0
  32. package/dist/rich-text-editor.js +4 -6
  33. package/dist/routing-action-panel.d.ts +38 -0
  34. package/dist/routing-action-panel.js +19 -0
  35. package/dist/search-input.d.ts +2 -0
  36. package/dist/search-input.js +5 -3
  37. package/dist/table-add-row.d.ts +8 -0
  38. package/dist/table-add-row.js +7 -0
  39. package/dist/table.js +1 -1
  40. package/dist/tailwind-preset.js +10 -0
  41. package/dist/textarea.d.ts +4 -0
  42. package/dist/textarea.js +17 -3
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -331,3 +331,12 @@ GitHub Actions builds Storybook with `npm run build-storybook` to verify that
331
331
  the stories compile. The Chromatic publishing step is temporarily commented
332
332
  out in `.github/workflows/ci.yml` and can be uncommented when snapshot
333
333
  publishing is needed again.
334
+
335
+ ### Text length counters
336
+
337
+ Use `showCount` with `maxLength` on `Input`, `SearchInput`, `CompoundSearchInput`, or `Textarea` for a character counter. Use `maxWords` on `Textarea` for a word counter; it takes precedence if both limits are supplied. Counters appear below the field, aligned right, as `count / limit`. The unit is available to screen readers without repeating a visible maximum-length hint. Native character limits remain enforced; word limits report validation errors without truncating text.
338
+
339
+ ```tsx
340
+ const [reference, setReference] = React.useState("");
341
+ <Input label="Reference" value={reference} onChange={(event) => setReference(event.target.value)} maxLength={50} showCount />
342
+ ```
@@ -9,6 +9,8 @@ export interface ActivityCardProps extends Omit<React.HTMLAttributes<HTMLElement
9
9
  title: React.ReactNode;
10
10
  titleLines?: 1 | 2;
11
11
  meta?: React.ReactNode;
12
+ /** Optional supporting content rendered inside the card header. */
13
+ headerContent?: React.ReactNode;
12
14
  trailing?: React.ReactNode;
13
15
  href?: string;
14
16
  type?: "button" | "submit" | "reset";
@@ -38,7 +38,7 @@ function getActivityActorInitials(value) {
38
38
  .map((part) => part.charAt(0).toUpperCase())
39
39
  .join("");
40
40
  }
41
- const ActivityCard = React.forwardRef(({ as = "article", density = "default", icon, iconTone, title, titleLines = 2, meta, trailing, href, type, className, children, "data-slot": dataSlot, ...props }, ref) => {
41
+ const ActivityCard = React.forwardRef(({ as = "article", density = "default", icon, iconTone, title, titleLines = 2, meta, headerContent, trailing, href, type, className, children, "data-slot": dataSlot, ...props }, ref) => {
42
42
  const Comp = as;
43
43
  return (_jsxs(Comp, { ref: ref, "data-slot": dataSlot !== null && dataSlot !== void 0 ? dataSlot : "activity-card", "data-activity-card": "", "data-density": density, href: as === "a" ? href : undefined, type: as === "button" ? (type !== null && type !== void 0 ? type : "button") : undefined, className: cn("min-w-0 overflow-hidden rounded-surface border border-border bg-card text-card-foreground shadow-sm", className), ...props, children: [_jsxs("header", { "data-slot": "activity-card-header", className: cn("flex items-center", density === "compact"
44
44
  ? "min-h-[var(--space-12)] p-[var(--space-3)]"
@@ -50,7 +50,7 @@ const ActivityCard = React.forwardRef(({ as = "article", density = "default", ic
50
50
  : "pl-[16px]"), trailing &&
51
51
  (density === "compact"
52
52
  ? "pr-[var(--space-3)]"
53
- : "pr-[32px]")), children: [_jsx(Typography, { as: "h3", variant: "label", className: cn("font-semibold", activityCardTitleLineClasses[titleLines]), children: title }), meta ? (_jsx(Typography, { as: "div", variant: "caption", className: "font-medium text-instructional", children: meta })) : null] }), trailing ? (_jsx("div", { "data-slot": "activity-card-trailing", className: "max-w-full shrink-0", children: trailing })) : null] }), children] }));
53
+ : "pr-[32px]")), children: [_jsx(Typography, { as: "h3", variant: "label", className: cn("font-semibold", activityCardTitleLineClasses[titleLines]), children: title }), meta ? (_jsx(Typography, { as: "div", variant: "caption", className: "font-medium text-instructional", children: meta })) : null, headerContent ? (_jsx("div", { "data-slot": "activity-card-header-content", className: "mt-[var(--space-2)]", children: headerContent })) : null] }), trailing ? (_jsx("div", { "data-slot": "activity-card-trailing", className: "max-w-full shrink-0", children: trailing })) : null] }), children] }));
54
54
  });
55
55
  ActivityCard.displayName = "ActivityCard";
56
56
  const ActivityCardContent = React.forwardRef(({ density = "default", className, ...props }, ref) => (_jsx("div", { ref: ref, "data-slot": "activity-card-content", "data-density": density, className: cn("border-t border-border", density === "compact"
package/dist/alert.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type VariantProps } from "class-variance-authority";
3
3
  import { type ButtonProps } from "./button";
4
4
  import { type CheckboxProps } from "./checkbox";
5
5
  declare const alertVariants: (props?: ({
6
- variant?: "destructive" | "info" | "success" | "warning" | null | undefined;
6
+ variant?: "warning" | "destructive" | "info" | "success" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  export interface AlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">, VariantProps<typeof alertVariants> {
9
9
  title?: React.ReactNode;
package/dist/badge.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const badgeVariants: (props?: ({
4
- variant?: "accent" | "muted" | "destructive" | "outline" | "secondary" | "info" | "success" | "warning" | null | undefined;
4
+ variant?: "accent" | "muted" | "warning" | "destructive" | "outline" | "secondary" | "info" | "success" | null | undefined;
5
5
  size?: "default" | "icon" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  /**
package/dist/button.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
4
+ variant?: "link" | "default" | "warning" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
5
5
  size?: "default" | "sm" | "compact" | "comfortable" | "icon" | "compactIcon" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
package/dist/button.js CHANGED
@@ -7,6 +7,7 @@ const buttonVariants = cva("inline-flex cursor-pointer items-center justify-cent
7
7
  variants: {
8
8
  variant: {
9
9
  default: "bg-button-primary text-button-primary-foreground hover:bg-button-primary-hover",
10
+ warning: "border border-[hsl(var(--status-warning-foreground))]/30 bg-[hsl(var(--status-warning))] text-[hsl(var(--status-warning-foreground))] hover:bg-[hsl(var(--status-warning))]/80 focus-visible:ring-[hsl(var(--status-warning-foreground))]",
10
11
  destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
11
12
  outline: "border border-[hsl(var(--button-outline-border))] bg-[hsl(var(--button-outline-background))] text-foreground hover:bg-[hsl(var(--button-outline-hover-background))] hover:text-accent-foreground",
12
13
  secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
@@ -146,7 +147,7 @@ const Button = React.forwardRef(({ className, variant, size, asChild = false, lo
146
147
  stopAnimatedIcons();
147
148
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
148
149
  };
149
- return (_jsx(Comp, { className: cn(buttonVariants({ variant, size, className })), ref: ref, "aria-busy": loading || undefined, "aria-disabled": isDisabled || undefined, "data-loading": loading ? "" : undefined, "data-disabled": isDisabled ? "" : undefined, onClick: handleClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onFocus: handleFocus, onBlur: handleBlur, tabIndex: isDisabled && asChild ? -1 : tabIndex, disabled: !asChild ? isDisabled : undefined, ...props, children: content }));
150
+ return (_jsx(Comp, { "data-slot": "button", className: cn(buttonVariants({ variant, size, className })), ref: ref, "aria-busy": loading || undefined, "aria-disabled": isDisabled || undefined, "data-loading": loading ? "" : undefined, "data-disabled": isDisabled ? "" : undefined, onClick: handleClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onFocus: handleFocus, onBlur: handleBlur, tabIndex: isDisabled && asChild ? -1 : tabIndex, disabled: !asChild ? isDisabled : undefined, ...props, children: content }));
150
151
  });
151
152
  Button.displayName = "Button";
152
153
  export { Button, buttonVariants };
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ import { type DashboardActionPanelProps } from "./layout";
3
+ import { type NotificationItemData } from "./notification";
4
+ export interface ChangelogActionPanelProps extends Omit<DashboardActionPanelProps, "children" | "headerSpacing" | "title"> {
5
+ title?: React.ReactNode;
6
+ notifications: NotificationItemData[];
7
+ isLoading?: boolean;
8
+ loadingLabel?: string;
9
+ emptyMessage?: React.ReactNode;
10
+ emptyDescription?: React.ReactNode;
11
+ }
12
+ /**
13
+ * Shared change-log panel. Consumers map their domain events to notification
14
+ * items; this component owns the complete list, loading, and empty-state UI.
15
+ */
16
+ declare function ChangelogActionPanel({ title, subtitle, notifications, isLoading, loadingLabel, emptyMessage, emptyDescription, ...panelProps }: ChangelogActionPanelProps): import("react/jsx-runtime").JSX.Element;
17
+ export { ChangelogActionPanel };
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { History } from "lucide-react";
4
+ import { DashboardActionPanel, DashboardActionPanelEmptyState, DashboardRightPanelBody, } from "./layout";
5
+ import { NotificationList, } from "./notification";
6
+ import { Spinner } from "./spinner";
7
+ /**
8
+ * Shared change-log panel. Consumers map their domain events to notification
9
+ * items; this component owns the complete list, loading, and empty-state UI.
10
+ */
11
+ function ChangelogActionPanel({ title = "Change log", subtitle = "Review all activity recorded here.", notifications, isLoading = false, loadingLabel = "Loading change log", emptyMessage = "No change log entries yet", emptyDescription = "Changes will be recorded here.", ...panelProps }) {
12
+ return (_jsx(DashboardActionPanel, { headerSpacing: "comfortable", title: title, subtitle: subtitle, ...panelProps, children: _jsx(DashboardRightPanelBody, { spacing: "comfortable", topInset: "none", children: isLoading ? (_jsx("div", { "data-slot": "changelog-action-panel-loading", className: "absolute inset-0 flex items-center justify-center", children: _jsx(Spinner, { size: "md", label: loadingLabel }) })) : notifications.length === 0 ? (_jsx(DashboardActionPanelEmptyState, { icon: History, message: emptyMessage, description: emptyDescription })) : (_jsx(NotificationList, { density: "compact", notifications: notifications, descriptionLines: 3, emptyState: null, variant: "bordered" })) }) }));
13
+ }
14
+ export { ChangelogActionPanel };
@@ -0,0 +1,63 @@
1
+ import * as React from "react";
2
+ import { type SelectOption } from "./select";
3
+ export interface ChecklistPanelProps {
4
+ title: React.ReactNode;
5
+ action?: React.ReactNode;
6
+ children: React.ReactNode;
7
+ }
8
+ export declare function ChecklistPanel({ title, action, children }: ChecklistPanelProps): import("react/jsx-runtime").JSX.Element;
9
+ export interface ChecklistSectionProps {
10
+ title: string;
11
+ progress?: string;
12
+ /** Independent control displayed beside the section chevron. */
13
+ action?: React.ReactNode;
14
+ children: React.ReactNode;
15
+ defaultOpen?: boolean;
16
+ open?: boolean;
17
+ onOpenChange?: (open: boolean) => void;
18
+ }
19
+ export declare function ChecklistSection({ title, progress, action, children, defaultOpen, open, onOpenChange }: ChecklistSectionProps): import("react/jsx-runtime").JSX.Element;
20
+ export interface ChecklistSelectionProps {
21
+ title: string;
22
+ description: string;
23
+ typeOptions: SelectOption[];
24
+ subtypeOptions: SelectOption[];
25
+ type: string;
26
+ subtype: string;
27
+ onTypeChange: (value: string) => void;
28
+ onSubtypeChange: (value: string) => void;
29
+ disabled?: boolean;
30
+ loading?: boolean;
31
+ action?: React.ReactNode;
32
+ }
33
+ export declare function ChecklistSelection({ title, description, typeOptions, subtypeOptions, type, subtype, onTypeChange, onSubtypeChange, disabled, loading, action }: ChecklistSelectionProps): import("react/jsx-runtime").JSX.Element;
34
+ export interface ChecklistQuestionOption {
35
+ value: string;
36
+ label: string;
37
+ tone?: "positive" | "negative" | "neutral";
38
+ }
39
+ export interface ChecklistQuestionProps {
40
+ label: string;
41
+ options: ChecklistQuestionOption[];
42
+ value: string;
43
+ onValueChange: (value: string) => void;
44
+ disabled?: boolean;
45
+ remarks: string;
46
+ onRemarksChange: (value: string) => void;
47
+ remarksLimit?: number;
48
+ /** Count and limit characters instead of words when supplied. */
49
+ remarksMaxLength?: number;
50
+ remarksRequired?: boolean;
51
+ evidence?: React.ReactNode;
52
+ evidenceCount?: number;
53
+ }
54
+ export declare function ChecklistQuestion({ label, options, value, onValueChange, disabled, remarks, onRemarksChange, remarksLimit, remarksMaxLength, remarksRequired, evidence, evidenceCount }: ChecklistQuestionProps): import("react/jsx-runtime").JSX.Element;
55
+ export interface ChecklistAttachmentsProps {
56
+ items: {
57
+ key: string;
58
+ name: string;
59
+ url: string;
60
+ }[];
61
+ onRemove?: (key: string) => void;
62
+ }
63
+ export declare function ChecklistAttachments({ items, onRemove }: ChecklistAttachmentsProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
5
+ import { Camera, ChevronDown, ClipboardList, LoaderCircle, MessageSquareText, Trash2 } from "lucide-react";
6
+ import { Button } from "./button";
7
+ import { Select } from "./select";
8
+ import { Textarea } from "./textarea";
9
+ import { cn } from "./lib/utils";
10
+ export function ChecklistPanel({ title, action, children }) {
11
+ return _jsxs("div", { "data-slot": "checklist-panel", className: "rounded-xl border border-border bg-card p-4 shadow-xs", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between gap-4", children: [_jsx("h3", { className: "text-base font-medium text-card-foreground", children: title }), action] }), children] });
12
+ }
13
+ export function ChecklistSection({ title, progress, action, children, defaultOpen = false, open, onOpenChange }) {
14
+ return _jsxs(CollapsiblePrimitive.Root, { open: open, onOpenChange: onOpenChange, defaultOpen: defaultOpen, className: "border-b border-border last:border-0", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(CollapsiblePrimitive.Trigger, { className: "group flex min-w-0 flex-1 items-center gap-1.5 py-3 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx("span", { className: "text-base font-medium", children: title }), progress ? _jsx("span", { className: "text-sm text-muted-foreground", children: progress }) : null, !action ? _jsx(ChevronDown, { "aria-hidden": "true", className: "ml-auto size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" }) : null] }), action ? _jsxs(_Fragment, { children: [_jsx("div", { "data-slot": "checklist-section-action", className: "shrink-0", children: action }), _jsx(CollapsiblePrimitive.Trigger, { "aria-label": `Toggle ${title}`, className: "group py-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(ChevronDown, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" }) })] }) : null] }), _jsx(CollapsiblePrimitive.Content, { className: "overflow-hidden data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up motion-reduce:data-[state=open]:animate-none motion-reduce:data-[state=closed]:animate-none", children: _jsx("div", { className: "flex flex-col gap-4 pb-4", children: children }) })] });
15
+ }
16
+ export function ChecklistSelection({ title, description, typeOptions, subtypeOptions, type, subtype, onTypeChange, onSubtypeChange, disabled, loading, action }) {
17
+ const Icon = loading ? LoaderCircle : ClipboardList;
18
+ return _jsxs("div", { "data-slot": "checklist-selection", "aria-busy": loading, className: "flex flex-col items-center gap-4 rounded-lg bg-muted/30 p-6 text-center", children: [_jsx(Icon, { "aria-hidden": "true", className: cn("size-8", loading && "animate-spin") }), _jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-sm font-medium", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: description })] }), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-4", children: [_jsx(Select, { className: "w-auto", triggerClassName: "w-auto min-w-0", label: "Type", hideLabel: true, placeholder: "Type", options: typeOptions, value: type, onValueChange: onTypeChange, disabled: disabled || loading }), _jsx(Select, { className: "w-auto", triggerClassName: "w-auto min-w-0", label: "Sub-Type", hideLabel: true, placeholder: "Sub-Type", options: subtypeOptions, value: subtype, onValueChange: onSubtypeChange, disabled: disabled || loading || !type }), action] })] });
19
+ }
20
+ export function ChecklistQuestion({ label, options, value, onValueChange, disabled, remarks, onRemarksChange, remarksLimit = 500, remarksMaxLength, remarksRequired = false, evidence, evidenceCount = 0 }) {
21
+ const name = React.useId();
22
+ React.useEffect(() => { if (remarksRequired)
23
+ setShowRemarks(true); }, [remarksRequired, value]);
24
+ const [showRemarks, setShowRemarks] = React.useState(Boolean(remarks));
25
+ const [showEvidence, setShowEvidence] = React.useState(false);
26
+ return _jsxs("div", { "data-slot": "checklist-question", className: "rounded-xl border border-border bg-card p-4 shadow-xs", children: [_jsx("p", { id: `${name}-label`, className: "mb-4 text-sm font-medium", children: label }), _jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [_jsx("fieldset", { disabled: disabled, "aria-labelledby": `${name}-label`, className: cn("flex min-w-0 border-0 p-0", disabled && "opacity-50"), children: options.map((option, index) => _jsxs("label", { className: cn("relative cursor-pointer border border-input px-2.5 py-2 text-sm font-medium focus-within:z-10 focus-within:ring-2 focus-within:ring-ring", index > 0 && "-ml-px", index === 0 && "rounded-l-md", index === options.length - 1 && "rounded-r-md", disabled && "cursor-not-allowed", value === option.value ? option.tone === "negative" ? "bg-destructive/20" : option.tone === "positive" ? "bg-primary/20" : "bg-muted" : "bg-background"), children: [_jsx("input", { className: "absolute inset-0 size-full cursor-inherit opacity-0", type: "radio", name: name, value: option.value, checked: value === option.value, onChange: () => onValueChange(option.value) }), option.label] }, option.value)) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "ghost", size: "compactIcon", "aria-label": "Add remarks", disabled: disabled, onClick: () => setShowRemarks(true), children: _jsx(MessageSquareText, { "aria-hidden": "true", size: 16 }) }), _jsx(Button, { variant: "ghost", size: "compactIcon", "aria-label": "Add photos", disabled: disabled, onClick: () => setShowEvidence((open) => !open), children: _jsx(Camera, { "aria-hidden": "true", size: 16 }) })] })] }), showRemarks || remarks ? _jsx("div", { className: "mt-2 space-y-2", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsx(Textarea, { label: "Remarks", hideLabel: true, placeholder: "Type...", value: remarks, disabled: disabled, rows: 2, onChange: (event) => onRemarksChange(event.target.value), maxWords: remarksMaxLength === undefined ? remarksLimit : undefined, maxLength: remarksMaxLength, showCount: remarksMaxLength !== undefined }) }), _jsx(Button, { variant: "ghost", size: "compactIcon", "aria-label": "Remove remarks", disabled: disabled, onClick: () => { onRemarksChange(""); setShowRemarks(false); }, children: _jsx(Trash2, { "aria-hidden": "true", size: 16 }) })] }) }) : null, (showEvidence || evidenceCount > 0) && evidence ? _jsx("div", { className: "mt-2", children: evidence }) : null] });
27
+ }
28
+ export function ChecklistAttachments({ items, onRemove }) {
29
+ return _jsx("div", { className: "flex flex-wrap gap-2", children: items.map((item) => _jsxs("div", { className: "relative", children: [_jsx("a", { href: item.url, target: "_blank", rel: "noreferrer", children: _jsx("img", { src: item.url, alt: item.name, className: "size-20 rounded-md border border-border object-cover" }) }), onRemove ? _jsx(Button, { variant: "outline", size: "compactIcon", "aria-label": `Remove ${item.name}`, onClick: () => onRemove(item.key), children: _jsx(Trash2, { "aria-hidden": "true", size: 16 }) }) : null] }, item.key)) });
30
+ }
@@ -1,5 +1,7 @@
1
1
  import * as React from "react";
2
2
  export interface CompoundSearchInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
3
+ /** Display a character counter when maxLength is set. */
4
+ showCount?: boolean;
3
5
  actions?: React.ReactNode;
4
6
  criteria?: React.ReactNode;
5
7
  selected?: boolean;
@@ -1,9 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { SearchIcon } from "lucide-react";
4
+ import { FieldCounter } from "./field-counter";
4
5
  import { cn } from "./lib/utils";
5
- const CompoundSearchInput = React.forwardRef(({ actions, "aria-invalid": ariaInvalid, className, criteria, selected = false, ...props }, ref) => (_jsxs("div", { "data-slot": "compound-search-input", "data-selected": selected ? "true" : "false", className: cn("flex min-h-12 w-full min-w-0 flex-wrap items-center gap-1.5 rounded-control border border-input bg-background p-1.5 transition-[border-color,box-shadow] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 data-[selected=true]:border-interactive data-[selected=true]:bg-highlight data-[selected=true]:ring-[3px] data-[selected=true]:ring-interactive/20", ariaInvalid === true || ariaInvalid === "true"
6
- ? "border-destructive ring-destructive/20"
7
- : undefined), children: [_jsx(SearchIcon, { "aria-hidden": "true", className: "ml-1.5 h-4 w-4 shrink-0 text-muted-foreground" }), criteria, _jsx("input", { ref: ref, "aria-invalid": ariaInvalid, className: cn("h-8 min-w-44 basis-52 flex-1 bg-transparent px-1.5 text-sm outline-none placeholder:text-muted-foreground", className), ...props }), actions ? (_jsx("span", { className: "ml-auto inline-flex h-8 shrink-0 items-center gap-1 text-muted-foreground", children: actions })) : null] })));
6
+ const CompoundSearchInput = React.forwardRef(({ actions, showCount = false, value, defaultValue, onChange, "aria-invalid": ariaInvalid, className, criteria, selected = false, ...props }, ref) => {
7
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue !== null && defaultValue !== void 0 ? defaultValue : "");
8
+ const generatedId = React.useId();
9
+ const limitId = showCount && props.maxLength !== undefined ? `search-limit-${generatedId}` : undefined;
10
+ return (_jsxs("div", { className: "grid gap-2", children: [_jsxs("div", { "data-slot": "compound-search-input", "data-selected": selected ? "true" : "false", className: cn("flex min-h-12 w-full min-w-0 flex-wrap items-center gap-1.5 rounded-control border border-input bg-background p-1.5 transition-[border-color,box-shadow] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 data-[selected=true]:border-interactive data-[selected=true]:bg-highlight data-[selected=true]:ring-[3px] data-[selected=true]:ring-interactive/20", ariaInvalid === true || ariaInvalid === "true"
11
+ ? "border-destructive ring-destructive/20"
12
+ : undefined), children: [_jsx(SearchIcon, { "aria-hidden": "true", className: "ml-1.5 h-4 w-4 shrink-0 text-muted-foreground" }), criteria, _jsx("input", { ref: ref, "aria-invalid": ariaInvalid, className: cn("h-8 min-w-44 basis-52 flex-1 bg-transparent px-1.5 text-sm outline-none placeholder:text-muted-foreground", className), ...props, "aria-describedby": [props["aria-describedby"], limitId].filter(Boolean).join(" ") || undefined, value: value, defaultValue: defaultValue, onChange: (event) => {
13
+ if (value === undefined)
14
+ setUncontrolledValue(event.target.value);
15
+ onChange === null || onChange === void 0 ? void 0 : onChange(event);
16
+ } }), actions ? (_jsx("span", { className: "ml-auto inline-flex h-8 shrink-0 items-center gap-1 text-muted-foreground", children: actions })) : null] }), limitId && _jsx(FieldCounter, { id: limitId, count: String(value !== null && value !== void 0 ? value : uncontrolledValue).length, limit: props.maxLength })] }));
17
+ });
8
18
  CompoundSearchInput.displayName = "CompoundSearchInput";
9
19
  export { CompoundSearchInput };
@@ -166,9 +166,9 @@ const DateInput = React.forwardRef((props, ref) => {
166
166
  const disabledDays = (date) => isDateOutsideBounds(date, minDate, maxDate);
167
167
  const calendarContent = (_jsx(_Fragment, { children: props.mode === "range" ? (_jsx(Calendar, { mode: "range", selected: selectedRange, onSelect: handleRangeSelect, disabled: disabledDays, defaultMonth: (_b = selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.from) !== null && _b !== void 0 ? _b : selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.to, numberOfMonths: (_c = props.numberOfMonths) !== null && _c !== void 0 ? _c : 2, showOutsideDays: false, className: "[&_.rdp-month:not(:first-of-type)]:hidden sm:[&_.rdp-month:not(:first-of-type)]:flex", autoFocus: true })) : (_jsx(Calendar, { mode: "single", selected: selectedDate, onSelect: handleSelect, disabled: disabledDays, defaultMonth: selectedDate, autoFocus: true })) }));
168
168
  if (appearance === "inline") {
169
- return (_jsxs("span", { className: cn("inline-flex", className), "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(Popover, { open: open, onOpenChange: handleOpenChange, align: "start", title: popoverTitle, surface: popoverTitle ? "menu" : "panel", collisionPadding: 8, contentClassName: "max-h-[var(--radix-popover-content-available-height)] w-auto overflow-auto p-0 [&_[data-slot=popover-title]]:px-3", trigger: _jsxs("button", { ref: ref, type: "button", id: inputId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("inline-flex cursor-pointer items-baseline rounded-sm border-0 bg-transparent p-0 font-medium text-interactive underline decoration-interactive/40 underline-offset-4 transition-colors hover:decoration-interactive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", !displayValue && "text-muted-foreground decoration-border", isInvalid && "text-destructive decoration-destructive", triggerClassName), children: [_jsxs("span", { className: "sr-only", children: [label, ": "] }), _jsx("span", { children: displayValue !== null && displayValue !== void 0 ? displayValue : placeholder })] }), children: calendarContent }), showDescription ? (_jsx("span", { id: descriptionId, className: "sr-only", children: description })) : null, error ? (_jsx("span", { id: errorId, role: "alert", className: "sr-only", children: error })) : null] }));
169
+ return (_jsxs("span", { className: cn("inline-flex", className), "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(Popover, { open: open, onOpenChange: handleOpenChange, align: "start", title: popoverTitle, surface: popoverTitle ? "menu" : "panel", collisionPadding: 8, contentClassName: "max-h-[var(--radix-popover-content-available-height)] w-auto overflow-auto p-0 [&_[data-slot=popover-title]]:p-3", trigger: _jsxs("button", { ref: ref, type: "button", id: inputId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("inline-flex cursor-pointer items-baseline rounded-sm border-0 bg-transparent p-0 font-medium text-interactive underline decoration-interactive/40 underline-offset-4 transition-colors hover:decoration-interactive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", !displayValue && "text-muted-foreground decoration-border", isInvalid && "text-destructive decoration-destructive", triggerClassName), children: [_jsxs("span", { className: "sr-only", children: [label, ": "] }), _jsx("span", { children: displayValue !== null && displayValue !== void 0 ? displayValue : placeholder })] }), children: calendarContent }), showDescription ? (_jsx("span", { id: descriptionId, className: "sr-only", children: description })) : null, error ? (_jsx("span", { id: errorId, role: "alert", className: "sr-only", children: error })) : null] }));
170
170
  }
171
- return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs("div", { className: "relative", children: [_jsx(Popover, { open: open, onOpenChange: handleOpenChange, align: "start", title: popoverTitle, surface: popoverTitle ? "menu" : "panel", collisionPadding: 8, contentClassName: "max-h-[var(--radix-popover-content-available-height)] w-auto overflow-auto p-0 [&_[data-slot=popover-title]]:px-3", trigger: _jsx("button", { ref: ref, type: "button", id: inputId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn(inputControlClassName, "relative cursor-pointer pr-9 text-left", showClearButton && "pr-16", !displayValue && "text-muted-foreground", isInvalid && "border-destructive focus-visible:ring-destructive", triggerClassName), children: _jsx("span", { className: "block truncate", children: displayValue !== null && displayValue !== void 0 ? displayValue : placeholder }) }), children: calendarContent }), _jsxs("span", { "aria-hidden": showClearButton ? undefined : true, className: "pointer-events-none absolute inset-y-0 right-3 flex items-center gap-2 text-muted-foreground", children: [showClearButton ? (_jsx("button", { type: "button", "aria-label": resolvedClearLabel, onClick: handleClear, className: "pointer-events-auto inline-flex h-4 w-4 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed", children: _jsx(XIcon, { size: 14, className: "flex items-center justify-center" }) })) : null, _jsx(CalendarIcon, { "aria-hidden": "true", className: "h-4 w-4 shrink-0", strokeWidth: 2 })] })] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
171
+ return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs("div", { className: "relative", children: [_jsx(Popover, { open: open, onOpenChange: handleOpenChange, align: "start", title: popoverTitle, surface: popoverTitle ? "menu" : "panel", collisionPadding: 8, contentClassName: "max-h-[var(--radix-popover-content-available-height)] w-auto overflow-auto p-0 [&_[data-slot=popover-title]]:p-3", trigger: _jsx("button", { ref: ref, type: "button", id: inputId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn(inputControlClassName, "relative cursor-pointer pr-9 text-left", showClearButton && "pr-16", !displayValue && "text-muted-foreground", isInvalid && "border-destructive focus-visible:ring-destructive", triggerClassName), children: _jsx("span", { className: "block truncate", children: displayValue !== null && displayValue !== void 0 ? displayValue : placeholder }) }), children: calendarContent }), _jsxs("span", { "aria-hidden": showClearButton ? undefined : true, className: "pointer-events-none absolute inset-y-0 right-3 flex items-center gap-2 text-muted-foreground", children: [showClearButton ? (_jsx("button", { type: "button", "aria-label": resolvedClearLabel, onClick: handleClear, className: "pointer-events-auto inline-flex h-4 w-4 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed", children: _jsx(XIcon, { size: 14, className: "flex items-center justify-center" }) })) : null, _jsx(CalendarIcon, { "aria-hidden": "true", className: "h-4 w-4 shrink-0", strokeWidth: 2 })] })] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
172
172
  });
173
173
  DateInput.displayName = "DateInput";
174
174
  export { DateInput };
package/dist/dialog.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface DialogAction extends Omit<ButtonProps, "children"> {
9
9
  }
10
10
  type DialogRenderable = React.ReactNode | ((helpers: DialogRenderHelpers) => React.ReactNode);
11
11
  export interface DialogProps {
12
+ variant?: "default" | "confirmation";
12
13
  open?: boolean;
13
14
  defaultOpen?: boolean;
14
15
  onOpenChange?: (open: boolean) => void;
@@ -26,5 +27,5 @@ export interface DialogProps {
26
27
  /** Whether a pointer interaction outside the dialog closes it. */
27
28
  closeOnOutsideClick?: boolean;
28
29
  }
29
- declare function Dialog({ open, defaultOpen, onOpenChange, modal, trigger, title, description, children, footer, primaryAction, secondaryAction, contentClassName, bodyClassName, hideCloseButton, closeOnOutsideClick, }: DialogProps): import("react/jsx-runtime").JSX.Element;
30
+ declare function Dialog({ variant, open, defaultOpen, onOpenChange, modal, trigger, title, description, children, footer, primaryAction, secondaryAction, contentClassName, bodyClassName, hideCloseButton, closeOnOutsideClick, }: DialogProps): import("react/jsx-runtime").JSX.Element;
30
31
  export { Dialog };
package/dist/dialog.js CHANGED
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import * as React from "react";
4
4
  import { Button } from "./button";
5
5
  import { DialogDescription, DialogFooter, DialogHeader, DialogRoot, DialogSurface, DialogTitle, DialogTrigger, } from "./dialog-primitive";
6
+ import { CircleAlert } from "lucide-react";
6
7
  import { cn } from "./lib/utils";
7
8
  function useControllableOpen({ open, defaultOpen, onOpenChange, }) {
8
9
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen !== null && defaultOpen !== void 0 ? defaultOpen : false);
@@ -31,7 +32,7 @@ function renderDialogAction({ action, close, defaultVariant, }) {
31
32
  }
32
33
  }, children: label }));
33
34
  }
34
- function Dialog({ open, defaultOpen, onOpenChange, modal, trigger, title, description, children, footer, primaryAction, secondaryAction, contentClassName, bodyClassName, hideCloseButton = false, closeOnOutsideClick = false, }) {
35
+ function Dialog({ variant = "default", open, defaultOpen, onOpenChange, modal, trigger, title, description, children, footer, primaryAction, secondaryAction, contentClassName, bodyClassName, hideCloseButton = false, closeOnOutsideClick = false, }) {
35
36
  const [isOpen, setIsOpen] = useControllableOpen({
36
37
  open,
37
38
  defaultOpen,
@@ -43,11 +44,11 @@ function Dialog({ open, defaultOpen, onOpenChange, modal, trigger, title, descri
43
44
  const body = resolveDialogRenderable(children, helpers);
44
45
  const customFooter = resolveDialogRenderable(footer, helpers);
45
46
  const hasFooter = Boolean(customFooter || primaryAction || secondaryAction);
46
- return (_jsxs(DialogRoot, { open: isOpen, onOpenChange: setIsOpen, modal: modal, children: [trigger ? _jsx(DialogTrigger, { asChild: true, children: trigger }) : null, _jsxs(DialogSurface, { ...(!description ? { "aria-describedby": undefined } : {}), className: contentClassName, hideCloseButton: hideCloseButton, onPointerDownOutside: (event) => {
47
+ return (_jsxs(DialogRoot, { open: isOpen, onOpenChange: setIsOpen, modal: modal, children: [trigger ? _jsx(DialogTrigger, { asChild: true, children: trigger }) : null, _jsxs(DialogSurface, { ...(!description ? { "aria-describedby": undefined } : {}), className: cn(variant === "confirmation" && "max-w-sm gap-0 overflow-hidden rounded-xl p-0", contentClassName), hideCloseButton: hideCloseButton || variant === "confirmation", onPointerDownOutside: (event) => {
47
48
  if (!closeOnOutsideClick) {
48
49
  event.preventDefault();
49
50
  }
50
- }, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title }), description ? (_jsx(DialogDescription, { children: description })) : null] }), body ? _jsx("div", { className: cn("grid gap-4", bodyClassName), children: body }) : null, hasFooter ? (_jsx(DialogFooter, { children: customFooter !== null && customFooter !== void 0 ? customFooter : (_jsxs(_Fragment, { children: [secondaryAction
51
+ }, children: [_jsxs("div", { className: cn(variant === "confirmation" && "flex items-start gap-4 p-4"), children: [variant === "confirmation" ? _jsx("div", { className: "rounded-lg bg-muted p-2", children: _jsx(CircleAlert, { "aria-hidden": "true", className: "size-6" }) }) : null, _jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title }), description ? _jsx(DialogDescription, { children: description }) : null] })] }), body ? _jsx("div", { className: cn("grid gap-4", bodyClassName), children: body }) : null, hasFooter ? (_jsx(DialogFooter, { className: variant === "confirmation" ? "border-t bg-muted/30 p-4" : undefined, children: customFooter !== null && customFooter !== void 0 ? customFooter : (_jsxs(_Fragment, { children: [secondaryAction
51
52
  ? renderDialogAction({
52
53
  action: secondaryAction,
53
54
  close: helpers.close,
@@ -8,6 +8,7 @@ import { cn } from "./lib/utils";
8
8
  const splitButtonDividerClassNames = {
9
9
  default: "before:bg-button-primary-foreground/30",
10
10
  destructive: "before:bg-destructive-foreground/30",
11
+ warning: "before:bg-[hsl(var(--status-warning-foreground))]/30",
11
12
  outline: "before:bg-[hsl(var(--button-outline-border))]",
12
13
  secondary: "before:bg-secondary-foreground/20",
13
14
  ghost: "before:bg-border",
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Button } from "./button";
4
- import { PlusIcon } from "./animated-icons/plus";
4
+ import { TableAddRow } from "./table-add-row";
5
5
  import { TrashIcon } from "./animated-icons/trash";
6
6
  import { Checkbox } from "./checkbox";
7
7
  import { EmptyState } from "./empty-state";
@@ -145,7 +145,7 @@ function EditableTable({ value, onChange, columns, isEditing = false, className,
145
145
  insertRow: (nextRow, index = rowIndex + 1) => handleInsertRow(nextRow, index),
146
146
  removeRow: () => handleDeleteRow(rowIndex),
147
147
  })
148
- : null, isEditing ? (_jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "text-muted-foreground hover:bg-destructive/10 hover:text-destructive focus-visible:text-destructive", onClick: () => handleDeleteRow(rowIndex), "aria-label": `Delete row ${rowIndex + 1}`, children: _jsx(TrashIcon, { size: 16 }) })) : null] }) })) : null] }, `row-${rowIndex}`)))) })] }) }), isEditing ? (_jsxs(Button, { type: "button", variant: "outline", onClick: handleAddRow, className: "w-full justify-center", children: [_jsx(PlusIcon, { size: 16 }), addRowLabel] })) : null] }));
148
+ : null, isEditing ? (_jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "text-muted-foreground hover:bg-destructive/10 hover:text-destructive focus-visible:text-destructive", onClick: () => handleDeleteRow(rowIndex), "aria-label": `Delete row ${rowIndex + 1}`, children: _jsx(TrashIcon, { size: 16 }) })) : null] }) })) : null] }, `row-${rowIndex}`)))) })] }) }), isEditing ? (_jsx(TableAddRow, { onClick: handleAddRow, children: addRowLabel })) : null] }));
149
149
  }
150
150
  EditableTable.displayName = "EditableTable";
151
151
  export { EditableTable };
@@ -0,0 +1,7 @@
1
+ /** Internal shared presentation for field length counters. */
2
+ export declare function FieldCounter({ id, count, limit, unit }: {
3
+ id?: string;
4
+ count: number;
5
+ limit: number;
6
+ unit?: "words" | "characters";
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Internal shared presentation for field length counters. */
3
+ export function FieldCounter({ id, count, limit, unit = "characters" }) {
4
+ return (_jsxs("div", { "data-slot": "field-counter", className: "flex items-start justify-between gap-3 text-sm text-muted-foreground", children: [_jsxs("span", { id: id, className: "sr-only", children: ["Maximum ", limit, " ", unit] }), _jsxs("span", { className: "ml-auto shrink-0 tabular-nums", "aria-live": "polite", "aria-atomic": "true", "aria-label": `${count} of ${limit} ${unit}`, children: [count, " / ", limit] })] }));
5
+ }
package/dist/index.d.ts CHANGED
@@ -9,16 +9,22 @@ export { AlertStack } from "./alert-stack";
9
9
  export type { AlertStackProps } from "./alert-stack";
10
10
  export { ActivityCard, ActivityCardActor, ActivityCardContent, } from "./activity-card";
11
11
  export type { ActivityCardActorProps, ActivityCardContentProps, ActivityCardDensity, ActivityCardIconTone, ActivityCardProps, } from "./activity-card";
12
+ export { ChangelogActionPanel } from "./changelog-action-panel";
13
+ export type { ChangelogActionPanelProps } from "./changelog-action-panel";
12
14
  export { Badge, badgeVariants } from "./badge";
13
15
  export { Card, CardGrid, CardSplitGrid, CardList, CardListItem, } from "./card";
14
16
  export type { CardGridProps, CardListItemProps, CardListProps, CardProps, CardSplitGridProps, } from "./card";
15
17
  export type { BadgeProps } from "./badge";
16
18
  export { MessageAttachmentRow, MessageItem, MessageNewDivider, MessageThread, formatMessageCardTimestamp, formatMessageDate, } from "./message-item";
17
19
  export type { MessageAttachmentRowProps, MessageItemAttachment, MessageItemData, MessageItemProps, MessageNewDividerProps, MessageThreadProps, } from "./message-item";
20
+ export { MessageActionPanel } from "./message-action-panel";
21
+ export type { MessageActionPanelProps } from "./message-action-panel";
18
22
  export { formatNotificationTimestamp, NotificationItem, NotificationList, NotificationPopover, NotificationTrigger, } from "./notification";
19
23
  export type { NotificationAction, NotificationFilter, NotificationIcon, NotificationItemData, NotificationItemProps, NotificationKind, NotificationListProps, NotificationPopoverProps, NotificationTimestamp, NotificationTriggerProps, NotificationTone, FormatNotificationTimestampOptions, } from "./notification";
20
24
  export { RoutingTimeline } from "./routing-timeline";
21
25
  export type { RoutingTimelineItem, RoutingTimelineItemKind, RoutingTimelineProps, } from "./routing-timeline";
26
+ export { RoutingActionPanel } from "./routing-action-panel";
27
+ export type { RoutingActionPanelProps } from "./routing-action-panel";
22
28
  export { RichTextEditor, formatFileSize, replaceTemplateVariables, sanitizeRichTextHtml, } from "./rich-text-editor";
23
29
  export type { AttachmentFile, RichTextEditorColor, RichTextEditorProps, RichTextEditorSendAction, RichTextEditorSendMetadata, RichTextEditorTemplate, TemplateContext, } from "./rich-text-editor";
24
30
  export { Calendar, CalendarDayButton } from "./calendar";
@@ -161,3 +167,7 @@ export type { CollapsibleProps } from "./collapsible";
161
167
  export { cn } from "./lib/utils";
162
168
  export { parseDateTime, formatDateTime } from "./lib/date-utils";
163
169
  export type { ParseDateTimeOptions } from "./lib/date-utils";
170
+ export { ChecklistAttachments, ChecklistPanel, ChecklistSection, ChecklistSelection, ChecklistQuestion } from "./checklist";
171
+ export type { ChecklistAttachmentsProps, ChecklistPanelProps, ChecklistSectionProps, ChecklistSelectionProps, ChecklistQuestionProps, ChecklistQuestionOption } from "./checklist";
172
+ export { TableAddRow, type TableAddRowProps } from "./table-add-row";
173
+ export { PopoverList, type PopoverListProps } from "./popover";
package/dist/index.js CHANGED
@@ -4,11 +4,14 @@ export * from "./animated-icons";
4
4
  export { Alert, AlertAction, AlertConsent, alertVariants, } from "./alert";
5
5
  export { AlertStack } from "./alert-stack";
6
6
  export { ActivityCard, ActivityCardActor, ActivityCardContent, } from "./activity-card";
7
+ export { ChangelogActionPanel } from "./changelog-action-panel";
7
8
  export { Badge, badgeVariants } from "./badge";
8
9
  export { Card, CardGrid, CardSplitGrid, CardList, CardListItem, } from "./card";
9
10
  export { MessageAttachmentRow, MessageItem, MessageNewDivider, MessageThread, formatMessageCardTimestamp, formatMessageDate, } from "./message-item";
11
+ export { MessageActionPanel } from "./message-action-panel";
10
12
  export { formatNotificationTimestamp, NotificationItem, NotificationList, NotificationPopover, NotificationTrigger, } from "./notification";
11
13
  export { RoutingTimeline } from "./routing-timeline";
14
+ export { RoutingActionPanel } from "./routing-action-panel";
12
15
  export { RichTextEditor, formatFileSize, replaceTemplateVariables, sanitizeRichTextHtml, } from "./rich-text-editor";
13
16
  export { Calendar, CalendarDayButton } from "./calendar";
14
17
  export { Checkbox } from "./checkbox";
@@ -84,3 +87,6 @@ export { Spinner, PageLoader, FullPageLoader } from "./spinner";
84
87
  export { Collapsible, CollapsibleSection } from "./collapsible";
85
88
  export { cn } from "./lib/utils";
86
89
  export { parseDateTime, formatDateTime } from "./lib/date-utils";
90
+ export { ChecklistAttachments, ChecklistPanel, ChecklistSection, ChecklistSelection, ChecklistQuestion } from "./checklist";
91
+ export { TableAddRow } from "./table-add-row";
92
+ export { PopoverList } from "./popover";
package/dist/input.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { InputControl } from "./input-control";
3
3
  export interface InputProps extends Omit<React.ComponentProps<typeof InputControl>, "className"> {
4
+ /** Display a character counter when maxLength is set. */
5
+ showCount?: boolean;
4
6
  label: React.ReactNode;
5
7
  description?: React.ReactNode;
6
8
  error?: React.ReactNode;
package/dist/input.js CHANGED
@@ -2,10 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
4
4
  import { InputControl } from "./input-control";
5
+ import { FieldCounter } from "./field-counter";
5
6
  import { cn } from "./lib/utils";
6
- const Input = React.forwardRef(({ label, description, error, invalid = false, hideLabel = false, className, inputClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, required, ...props }, ref) => {
7
+ const Input = React.forwardRef(({ label, showCount = false, value, defaultValue, onChange, description, error, invalid = false, hideLabel = false, className, inputClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, required, ...props }, ref) => {
8
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue !== null && defaultValue !== void 0 ? defaultValue : "");
9
+ const count = String(value !== null && value !== void 0 ? value : uncontrolledValue).length;
7
10
  const generatedId = React.useId();
8
11
  const inputId = id !== null && id !== void 0 ? id : `input-${generatedId}`;
12
+ const limitId = showCount && props.maxLength !== undefined ? `${inputId}-limit` : undefined;
9
13
  const errorId = error ? `${inputId}-error` : undefined;
10
14
  const showDescription = Boolean(description) && !error;
11
15
  const descriptionId = showDescription ? `${inputId}-description` : undefined;
@@ -13,10 +17,14 @@ const Input = React.forwardRef(({ label, description, error, invalid = false, hi
13
17
  ariaInvalid === true ||
14
18
  ariaInvalid === "true" ||
15
19
  Boolean(error);
16
- const describedBy = [ariaDescribedBy, descriptionId, errorId]
20
+ const describedBy = [ariaDescribedBy, descriptionId, errorId, limitId]
17
21
  .filter(Boolean)
18
22
  .join(" ") || undefined;
19
- return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, required: required, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsx(InputControl, { ref: ref, id: inputId, disabled: disabled, required: required, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": describedBy, className: inputClassName, ...props }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
23
+ return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, required: required, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsx(InputControl, { ref: ref, id: inputId, disabled: disabled, required: required, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": describedBy, className: inputClassName, ...props, value: value, defaultValue: defaultValue, onChange: (event) => {
24
+ if (value === undefined)
25
+ setUncontrolledValue(event.target.value);
26
+ onChange === null || onChange === void 0 ? void 0 : onChange(event);
27
+ } }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, limitId && _jsx(FieldCounter, { id: limitId, count: count, limit: props.maxLength }), _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
20
28
  });
21
29
  Input.displayName = "Input";
22
30
  export { Input };
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ import { type DashboardActionPanelProps } from "./layout";
3
+ export interface MessageActionPanelProps extends Omit<DashboardActionPanelProps, "children" | "headerSpacing" | "title"> {
4
+ title?: React.ReactNode;
5
+ children: React.ReactNode;
6
+ }
7
+ /**
8
+ * Shared message-panel frame. Message data and composer behaviour remain with
9
+ * the consuming application while this component keeps the panel UI stable.
10
+ */
11
+ declare function MessageActionPanel({ title, subtitle, children, ...panelProps }: MessageActionPanelProps): import("react/jsx-runtime").JSX.Element;
12
+ export { MessageActionPanel };
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { DashboardActionPanel, } from "./layout";
4
+ /**
5
+ * Shared message-panel frame. Message data and composer behaviour remain with
6
+ * the consuming application while this component keeps the panel UI stable.
7
+ */
8
+ function MessageActionPanel({ title = "Messages", subtitle = "Find all correspondence here.", children, ...panelProps }) {
9
+ return (_jsx(DashboardActionPanel, { headerSpacing: "comfortable", title: title, subtitle: subtitle, ...panelProps, children: children }));
10
+ }
11
+ export { MessageActionPanel };
@@ -144,8 +144,12 @@ function NotificationItem({ notification, density = "default", variant = "row",
144
144
  };
145
145
  if (variant === "card") {
146
146
  const activityDensity = (_d = notification.activityDensity) !== null && _d !== void 0 ? _d : density;
147
- const hasCardContent = notification.description !== undefined ||
148
- notification.actor !== undefined ||
147
+ const hasDescription = notification.description !== undefined &&
148
+ notification.description !== null &&
149
+ notification.description !== "";
150
+ const actorBelongsInHeader = notification.actor !== undefined && !hasDescription && !hasActions;
151
+ const hasCardContent = hasDescription ||
152
+ (notification.actor !== undefined && !actorBelongsInHeader) ||
149
153
  hasActions;
150
154
  const activityElement = notification.href && !hasActions
151
155
  ? "a"
@@ -154,8 +158,8 @@ function NotificationItem({ notification, density = "default", variant = "row",
154
158
  : "article";
155
159
  const unreadIndicator = isUnread && unreadLabel !== null ? (_jsx("span", { "data-slot": "notification-unread-indicator", className: "block size-2.5 rounded-full bg-primary ring-2 ring-background", children: _jsx("span", { className: "sr-only", children: unreadLabel }) })) : null;
156
160
  const cardTrailing = notification.badge || unreadIndicator ? (_jsxs("span", { className: "flex items-center gap-[var(--space-2)]", children: [notification.badge, unreadIndicator] })) : null;
157
- return (_jsx(ActivityCard, { as: activityElement, "data-slot": "notification-item", "data-unread": isUnread ? "" : undefined, density: activityDensity, icon: _jsx(Icon, { className: activityDensity === "compact" ? "size-3.5" : "size-[24px]" }), iconTone: notification.iconAppearance === "plain" ? undefined : tone, title: notification.title, titleLines: labelLines, meta: meta, trailing: cardTrailing, href: notification.href, className: cn("group relative w-full text-left outline-none transition-colors", isUnread && "bg-primary/[0.045]", hasHoverSurface && "hover:bg-accent/70", isInteractive &&
158
- "cursor-pointer focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", className), onClick: isInteractive ? handleClick : undefined, ...props, children: hasCardContent ? (_jsxs(ActivityCardContent, { density: activityDensity, children: [notification.description !== undefined ? (_jsx(Typography, { as: "div", variant: "bodySm", className: cn("text-left", isUnread ? "text-foreground/75" : "text-muted-foreground", descriptionLineClasses[descriptionLines]), children: notification.description })) : null, notification.actor !== undefined ? (_jsx(ActivityCardActor, { actor: notification.actor, label: actorLabel })) : null, hasActions ? (_jsx("div", { className: "flex flex-wrap gap-2", children: actions.map((action, index) => {
161
+ return (_jsx(ActivityCard, { as: activityElement, "data-slot": "notification-item", "data-unread": isUnread ? "" : undefined, density: activityDensity, icon: _jsx(Icon, { className: activityDensity === "compact" ? "size-3.5" : "size-[24px]" }), iconTone: notification.iconAppearance === "plain" ? undefined : tone, title: notification.title, titleLines: labelLines, meta: meta, headerContent: actorBelongsInHeader ? (_jsx(ActivityCardActor, { actor: notification.actor, label: actorLabel })) : undefined, trailing: cardTrailing, href: notification.href, className: cn("group relative w-full text-left outline-none transition-colors", isUnread && "bg-primary/[0.045]", hasHoverSurface && "hover:bg-accent/70", isInteractive &&
162
+ "cursor-pointer focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", className), onClick: isInteractive ? handleClick : undefined, ...props, children: hasCardContent ? (_jsxs(ActivityCardContent, { density: activityDensity, children: [hasDescription ? (_jsx(Typography, { as: "div", variant: "bodySm", className: cn("text-left", isUnread ? "text-foreground/75" : "text-muted-foreground", descriptionLineClasses[descriptionLines]), children: notification.description })) : null, notification.actor !== undefined && !actorBelongsInHeader ? (_jsx(ActivityCardActor, { actor: notification.actor, label: actorLabel })) : null, hasActions ? (_jsx("div", { className: "flex flex-wrap gap-2", children: actions.map((action, index) => {
159
163
  var _a, _b;
160
164
  return (_jsx(Button, { type: "button", variant: (_a = action.variant) !== null && _a !== void 0 ? _a : (index === 0 ? "outline" : "default"), size: "sm", disabled: action.disabled, className: cn("min-w-24", action.className), onClick: (event) => {
161
165
  var _a;
@@ -8,8 +8,9 @@ export interface PageSectionProps extends Omit<React.HTMLAttributes<HTMLElement>
8
8
  description?: React.ReactNode;
9
9
  headerAction?: React.ReactNode;
10
10
  titleAdornment?: React.ReactNode;
11
+ titleAdornmentSpacing?: "default" | "comfortable";
11
12
  headerClassName?: string;
12
13
  contentClassName?: string;
13
14
  }
14
- declare function PageSection({ title, description, headerAction, titleAdornment, className, headerClassName, contentClassName, children, ...props }: PageSectionProps): import("react/jsx-runtime").JSX.Element;
15
+ declare function PageSection({ title, description, headerAction, titleAdornment, titleAdornmentSpacing, className, headerClassName, contentClassName, children, ...props }: PageSectionProps): import("react/jsx-runtime").JSX.Element;
15
16
  export { PageSection, PageSectionGroup };
@@ -6,9 +6,9 @@ function PageSectionGroup({ variant = "divided", className, ...props }) {
6
6
  return (_jsx("div", { className: cn(variant === "divided" && "divide-y divide-border", variant === "cards" &&
7
7
  "space-y-4 [&>[data-slot=page-section]]:rounded-surface [&>[data-slot=page-section]]:border [&>[data-slot=page-section]]:border-border/70 [&>[data-slot=page-section]]:bg-card [&>[data-slot=page-section]]:p-5", className), ...props }));
8
8
  }
9
- function PageSection({ title, description, headerAction, titleAdornment, className, headerClassName, contentClassName, children, ...props }) {
9
+ function PageSection({ title, description, headerAction, titleAdornment, titleAdornmentSpacing = "default", className, headerClassName, contentClassName, children, ...props }) {
10
10
  const renderedTitle = React.isValidElement(title) ? (title) : (_jsx(Typography, { as: "h3", variant: "subheading", children: title }));
11
11
  const renderedDescription = description == null ? null : React.isValidElement(description) ? (description) : (_jsx(Typography, { as: "p", variant: "caption", children: description }));
12
- return (_jsxs("section", { "data-slot": "page-section", className: cn("py-6", className), ...props, children: [_jsxs("div", { className: cn("flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between", headerClassName), children: [_jsxs("div", { className: "min-w-0 space-y-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [renderedTitle, titleAdornment] }), renderedDescription] }), headerAction ? (_jsx("div", { className: "flex flex-wrap items-center justify-end gap-3 sm:shrink-0", children: headerAction })) : null] }), children ? (_jsx("div", { className: cn("pt-4", contentClassName), children: children })) : null] }));
12
+ return (_jsxs("section", { "data-slot": "page-section", className: cn("py-6", className), ...props, children: [_jsxs("div", { className: cn("flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between", headerClassName), children: [_jsxs("div", { className: "min-w-0 space-y-1", children: [_jsxs("div", { className: cn("flex flex-wrap items-center", titleAdornmentSpacing === "comfortable" ? "gap-4" : "gap-1.5"), children: [renderedTitle, titleAdornment] }), renderedDescription] }), headerAction ? (_jsx("div", { className: "flex flex-wrap items-center justify-end gap-3 sm:shrink-0", children: headerAction })) : null] }), children ? (_jsx("div", { className: cn("pt-4", contentClassName), children: children })) : null] }));
13
13
  }
14
14
  export { PageSection, PageSectionGroup };
package/dist/popover.d.ts CHANGED
@@ -27,3 +27,8 @@ export interface PopoverTitleProps extends Omit<React.ComponentPropsWithoutRef<t
27
27
  }
28
28
  declare function PopoverTitle({ className, children, ...props }: PopoverTitleProps): import("react/jsx-runtime").JSX.Element;
29
29
  export { Popover, PopoverTitle };
30
+ export interface PopoverListProps {
31
+ items: readonly string[];
32
+ }
33
+ /** Read-only information rows for a popover, without implying menu actions. */
34
+ export declare function PopoverList({ items }: PopoverListProps): import("react/jsx-runtime").JSX.Element;
package/dist/popover.js CHANGED
@@ -87,3 +87,7 @@ function PopoverTitle({ className, children, ...props }) {
87
87
  return (_jsx(Typography, { as: "p", variant: "label", "data-slot": "popover-title", className: className, ...props, children: children }));
88
88
  }
89
89
  export { Popover, PopoverTitle };
90
+ /** Read-only information rows for a popover, without implying menu actions. */
91
+ export function PopoverList({ items }) {
92
+ return (_jsx("ul", { "data-slot": "popover-list", className: "max-h-64 space-y-3 overflow-y-auto text-sm text-foreground", children: items.map((item, index) => (_jsxs("li", { className: "flex items-start gap-2", children: [_jsx("span", { "aria-hidden": "true", className: "mt-2 size-1 shrink-0 rounded-full bg-muted-foreground" }), _jsx("span", { className: "min-w-0 break-words", children: item })] }, index))) }));
93
+ }
@@ -351,11 +351,11 @@ export function RichTextEditor({ variant = "default", placeholder = "Type messag
351
351
  setTemplatePopoverOpen(false);
352
352
  };
353
353
  const hasSendActions = Boolean(sendActions === null || sendActions === void 0 ? void 0 : sendActions.length);
354
- 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: () => {
355
355
  void handleSend();
356
356
  }, children: _jsx(PopoverMenuSection, { children: sendActions === null || sendActions === void 0 ? void 0 : sendActions.map((action) => (_jsx(PopoverMenuItem, { disabled: sendDisabled || action.disabled, onClick: () => {
357
357
  void handleSend(action.metadata);
358
- }, 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: () => {
359
359
  void handleSend();
360
360
  }, children: [_jsx(SendIcon, { size: 16 }), isSending ? sendingLabel : sendLabel] }));
361
361
  if (variant === "message") {
@@ -373,9 +373,7 @@ export function RichTextEditor({ variant = "default", placeholder = "Type messag
373
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
374
  var _a;
375
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, _jsx(Button, { type: "button", size: "sm", disabled: isSending || !onSend, "aria-label": isSending ? sendingLabel : sendLabel, onClick: () => {
377
- void handleSend();
378
- }, children: isSending ? sendingLabel : sendLabel })] })] })] }));
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 })] })] })] }));
379
377
  }
380
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 &&
381
379
  linkMenuPosition &&
@@ -415,5 +413,5 @@ export function RichTextEditor({ variant = "default", placeholder = "Type messag
415
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 }) => {
416
414
  var _a;
417
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));
418
- }) }), 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 }) })] }));
419
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,6 +1,8 @@
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;
@@ -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", 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
+ 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) => {
@@ -67,7 +69,7 @@ const SearchInput = React.forwardRef(({ value, onValueChange, label = "Search",
67
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 };
@@ -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
+ }
package/dist/table.js CHANGED
@@ -171,7 +171,7 @@ const TableHead = React.forwardRef(({ className, style, width, minWidth, maxWidt
171
171
  treatWidthAsMinWidth,
172
172
  }), ...props })));
173
173
  TableHead.displayName = "TableHead";
174
- const TableCell = React.forwardRef(({ className, style, width, minWidth, maxWidth, wrap = false, treatWidthAsMinWidth, containOverflow, ...props }, ref) => (_jsx("td", { ref: ref, className: cn("h-12 px-4 py-[4px] align-middle text-card-foreground [&:has([role=checkbox])]:pr-0", wrap ? "whitespace-normal" : "whitespace-nowrap", overflowContainmentClassName({ containOverflow, wrap }), className), style: getTableColumnSizingStyle({
174
+ const TableCell = React.forwardRef(({ className, style, width, minWidth, maxWidth, wrap = false, treatWidthAsMinWidth, containOverflow, ...props }, ref) => (_jsx("td", { ref: ref, className: cn("h-12 px-4 py-[4px] align-middle text-card-foreground [&:has([role=checkbox])]:pr-0 [&_[data-slot=button]]:px-0", wrap ? "whitespace-normal" : "whitespace-nowrap", overflowContainmentClassName({ containOverflow, wrap }), className), style: getTableColumnSizingStyle({
175
175
  style,
176
176
  width,
177
177
  minWidth,
@@ -147,6 +147,14 @@ export const neaTailwindPreset = {
147
147
  12: "var(--space-12)",
148
148
  },
149
149
  keyframes: {
150
+ "collapsible-down": {
151
+ from: { height: "0" },
152
+ to: { height: "var(--radix-collapsible-content-height)" },
153
+ },
154
+ "collapsible-up": {
155
+ from: { height: "var(--radix-collapsible-content-height)" },
156
+ to: { height: "0" },
157
+ },
150
158
  "accordion-down": {
151
159
  from: { height: "0" },
152
160
  to: { height: "var(--radix-accordion-content-height)" },
@@ -157,6 +165,8 @@ export const neaTailwindPreset = {
157
165
  },
158
166
  },
159
167
  animation: {
168
+ "collapsible-down": "collapsible-down 0.2s ease-out",
169
+ "collapsible-up": "collapsible-up 0.2s ease-out",
160
170
  "accordion-down": "accordion-down 0.2s ease-out",
161
171
  "accordion-up": "accordion-up 0.2s ease-out",
162
172
  },
@@ -4,6 +4,10 @@ export interface TextareaProps extends Omit<React.ComponentProps<typeof Textarea
4
4
  label: React.ReactNode;
5
5
  size?: "default" | "comfortable";
6
6
  description?: React.ReactNode;
7
+ /** Show a word counter and validate this limit without truncating the entered text. */
8
+ maxWords?: number;
9
+ /** Display a character counter for maxLength; maxWords takes precedence. */
10
+ showCount?: boolean;
7
11
  error?: React.ReactNode;
8
12
  invalid?: boolean;
9
13
  hideLabel?: boolean;
package/dist/textarea.js CHANGED
@@ -2,12 +2,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
4
4
  import { inputControlPaddingClassName } from "./input-control";
5
+ import { FieldCounter } from "./field-counter";
5
6
  import { cn } from "./lib/utils";
6
7
  const TextareaControl = React.forwardRef(({ className, ...props }, ref) => (_jsx("textarea", { ref: ref, "data-slot": "textarea", className: cn("flex min-h-16 w-full rounded-control border border-input bg-background text-sm transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20", inputControlPaddingClassName, className), ...props })));
7
8
  TextareaControl.displayName = "TextareaControl";
8
- const Textarea = React.forwardRef(({ label, size = "default", description, error, invalid = false, hideLabel = false, className, textareaClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, ...props }, ref) => {
9
+ const Textarea = React.forwardRef(({ label, size = "default", description, maxWords, showCount = false, value, defaultValue, onChange, error: suppliedError, invalid = false, hideLabel = false, className, textareaClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, ...props }, ref) => {
10
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue !== null && defaultValue !== void 0 ? defaultValue : "");
11
+ const text = String(value !== null && value !== void 0 ? value : uncontrolledValue);
12
+ const words = text.trim() ? text.trim().split(/\s+/).length : 0;
13
+ const hasWordLimit = maxWords !== undefined;
14
+ const error = suppliedError || (hasWordLimit && words > maxWords
15
+ ? `Use ${maxWords} words or fewer.`
16
+ : undefined);
9
17
  const generatedId = React.useId();
10
18
  const textareaId = id !== null && id !== void 0 ? id : `textarea-${generatedId}`;
19
+ const limit = maxWords !== null && maxWords !== void 0 ? maxWords : (showCount ? props.maxLength : undefined);
20
+ const wordLimitId = limit !== undefined ? `${textareaId}-word-limit` : undefined;
11
21
  const errorId = error ? `${textareaId}-error` : undefined;
12
22
  const showDescription = Boolean(description) && !error;
13
23
  const descriptionId = showDescription ? `${textareaId}-description` : undefined;
@@ -15,11 +25,15 @@ const Textarea = React.forwardRef(({ label, size = "default", description, error
15
25
  ariaInvalid === true ||
16
26
  ariaInvalid === "true" ||
17
27
  Boolean(error);
18
- const describedBy = [ariaDescribedBy, descriptionId, errorId]
28
+ const describedBy = [ariaDescribedBy, descriptionId, errorId, wordLimitId]
19
29
  .filter(Boolean)
20
30
  .join(" ") || undefined;
21
31
  return (_jsxs(Field, { className: cn(size === "comfortable" && "gap-[8px]", className), "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: textareaId, className: cn(size === "comfortable" && "text-[16px] leading-[24px]", hideLabel && "sr-only", labelClassName), children: label }), _jsx(TextareaControl, { ref: ref, id: textareaId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": describedBy, className: cn(size === "comfortable" &&
22
- "min-h-[76px] px-[16px] py-[12px] text-[16px] leading-[24px]", textareaClassName), ...props }), 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 })] }));
32
+ "min-h-[76px] px-[16px] py-[12px] text-[16px] leading-[24px]", textareaClassName), ...props, value: value, defaultValue: defaultValue, onChange: (event) => {
33
+ if (value === undefined)
34
+ setUncontrolledValue(event.target.value);
35
+ onChange === null || onChange === void 0 ? void 0 : onChange(event);
36
+ } }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: cn(size === "comfortable" && "text-[14px] leading-[20px]", descriptionClassName), children: description })) : null, limit !== undefined && _jsx(FieldCounter, { id: wordLimitId, count: hasWordLimit ? words : text.length, limit: limit, unit: hasWordLimit ? "words" : "characters" }), _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
23
37
  });
24
38
  Textarea.displayName = "Textarea";
25
39
  export { Textarea };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neasg/design-system",
3
- "version": "0.4.95",
3
+ "version": "0.4.113",
4
4
  "description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
5
5
  "repository": {
6
6
  "type": "git",