@neasg/design-system 0.4.113 → 0.4.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/button.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
10
10
  * Use this for links or custom interactive elements that should look like a button.
11
11
  */
12
12
  asChild?: boolean;
13
+ /** Allow the label to wrap within its container and the button to grow in height. */
14
+ wrap?: boolean;
13
15
  /**
14
16
  * Shows a loading indicator, disables interaction, and sets `aria-busy`.
15
17
  * When used with `asChild`, the component also prevents click handling and sets `aria-disabled`.
package/dist/button.js CHANGED
@@ -102,7 +102,7 @@ function attachAnimatedIconRefs(node, refs) {
102
102
  }
103
103
  return node;
104
104
  }
105
- const Button = React.forwardRef(({ className, variant, size, asChild = false, loading = false, loadingText, loadingIndicator, disabled, children, onClick, onMouseEnter, onMouseLeave, onFocus, onBlur, tabIndex, ...props }, ref) => {
105
+ const Button = React.forwardRef(({ className, variant, size, asChild = false, wrap = false, loading = false, loadingText, loadingIndicator, disabled, children, onClick, onMouseEnter, onMouseLeave, onFocus, onBlur, tabIndex, ...props }, ref) => {
106
106
  const Comp = asChild ? Slot : "button";
107
107
  const isDisabled = disabled || loading;
108
108
  const loadingLabel = loadingText !== null && loadingText !== void 0 ? loadingText : children;
@@ -147,7 +147,9 @@ const Button = React.forwardRef(({ className, variant, size, asChild = false, lo
147
147
  stopAnimatedIcons();
148
148
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
149
149
  };
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
+ return (_jsx(Comp, { "data-slot": "button", className: cn(buttonVariants({ variant, size }), wrap && "h-auto max-w-full whitespace-normal break-words py-1.5", wrap && (size === "sm" || size === "compact" || size === "compactIcon"
151
+ ? "min-h-8"
152
+ : size === "comfortable" ? "min-h-[46px]" : "min-h-control"), 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 }));
151
153
  });
152
154
  Button.displayName = "Button";
153
155
  export { Button, buttonVariants };
package/dist/card.d.ts CHANGED
@@ -13,6 +13,8 @@ export interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "t
13
13
  accent?: "primary";
14
14
  /** Optional visual affordance shown in the top-right corner, such as a filter icon. */
15
15
  icon?: React.ReactNode;
16
+ /** Interactive action beside the card title. */
17
+ headerAction?: React.ReactNode;
16
18
  /** Replaces title/stat/subMetric with skeleton placeholders while data is loading. */
17
19
  loading?: boolean;
18
20
  }
package/dist/card.js CHANGED
@@ -55,7 +55,7 @@ function attachCardIconRef(node, ref) {
55
55
  ref: mergeRefs(node.ref, refCallback),
56
56
  });
57
57
  }
58
- const Card = React.forwardRef(({ title, description, variant = "default", size = "default", stat, subMetric, footer, active, accent, icon, loading = false, children, className, onClick, onKeyDown, onMouseEnter, onMouseLeave, onFocus, onBlur, role, tabIndex, "aria-pressed": ariaPressed, ...props }, ref) => {
58
+ const Card = React.forwardRef(({ title, description, variant = "default", size = "default", stat, subMetric, footer, active, accent, icon, headerAction, loading = false, children, className, onClick, onKeyDown, onMouseEnter, onMouseLeave, onFocus, onBlur, role, tabIndex, "aria-pressed": ariaPressed, ...props }, ref) => {
59
59
  const interactive = Boolean(onClick);
60
60
  const hasStat = stat !== undefined;
61
61
  const iconRef = React.useRef(null);
@@ -111,7 +111,7 @@ const Card = React.forwardRef(({ title, description, variant = "default", size =
111
111
  "border-[hsl(var(--card-border))] shadow-[inset_4px_0_0_0_hsl(var(--primary))]", interactive &&
112
112
  "cursor-pointer transition-[border-color,background-color,box-shadow,transform] duration-150 ease-out hover:border-primary/50 hover:bg-primary/5 hover:shadow-md motion-safe:hover:-translate-y-0.5 motion-safe:active:translate-y-0 motion-safe:active:scale-[0.99] focus-visible:border-primary/50 focus-visible:bg-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", className), ...props, children: [_jsxs("div", { className: cn("relative flex flex-col gap-2", size === "default" && "p-4", size === "compact" && "p-3", showIcon && size === "default" && "pr-12", showIcon && size === "compact" && "pr-10"), children: [showIcon ? (_jsx("span", { "aria-hidden": "true", "data-slot": "card-icon", className: cn("pointer-events-none absolute right-3 top-3 inline-flex size-7 items-center justify-center rounded-control border border-[hsl(var(--card-border))] bg-background/80 text-muted-foreground shadow-sm transition-[background-color,color,transform] duration-150 ease-out group-hover/card:scale-105 group-hover/card:bg-highlight group-hover/card:text-highlight-foreground group-focus-visible/card:bg-highlight group-focus-visible/card:text-highlight-foreground motion-reduce:transition-colors [&_svg]:size-3.5", size === "compact" && "right-2.5 top-2.5 size-6 [&_svg]:size-3", active &&
113
113
  "border-primary/30 bg-highlight text-highlight-foreground", variant === "gradient" &&
114
- "border-white/25 bg-white/10 text-current group-hover/card:bg-white/20 group-hover/card:text-current group-focus-visible/card:bg-white/20 group-focus-visible/card:text-current"), children: renderedIcon })) : null, loading ? (_jsx(Skeleton, { className: "h-3.5 w-24" })) : (renderedTitle), loading
114
+ "border-white/25 bg-white/10 text-current group-hover/card:bg-white/20 group-hover/card:text-current group-focus-visible/card:bg-white/20 group-focus-visible/card:text-current"), children: renderedIcon })) : null, loading ? (_jsx(Skeleton, { className: "h-3.5 w-24" })) : (headerAction ? (_jsxs("div", { className: "flex items-start justify-between gap-3", children: [renderedTitle, _jsx("div", { className: "shrink-0", children: headerAction })] })) : renderedTitle), loading
115
115
  ? description != null && _jsx(Skeleton, { className: "h-5 w-40" })
116
116
  : renderedDescription, loading ? (stat !== undefined && _jsx(Skeleton, { className: "h-8 w-20" })) : stat ? (_jsx(Typography, { as: "p", variant: "title", color: variant === "gradient" ? "inherit" : undefined, children: stat })) : null, loading ? (subMetric !== undefined && _jsx(Skeleton, { className: "h-5 w-28" })) : subMetric ? (_jsx(Typography, { as: "p", variant: "caption", color: variant === "gradient" ? "inherit" : undefined, className: variant === "gradient" ? "opacity-80" : undefined, children: subMetric })) : null] }), children ? (_jsx("div", { className: cn(size === "default" && "p-4 pt-0", size === "compact" && "p-3 pt-0"), children: children })) : null, footer ? (_jsx("div", { className: cn("flex items-center", size === "default" && "p-4 pt-0", size === "compact" && "p-3 pt-0"), children: footer })) : null] }));
117
117
  });
package/dist/checklist.js CHANGED
@@ -6,6 +6,7 @@ import { Camera, ChevronDown, ClipboardList, LoaderCircle, MessageSquareText, Tr
6
6
  import { Button } from "./button";
7
7
  import { Select } from "./select";
8
8
  import { Textarea } from "./textarea";
9
+ import { Tooltip } from "./tooltip";
9
10
  import { cn } from "./lib/utils";
10
11
  export function ChecklistPanel({ title, action, children }) {
11
12
  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] });
@@ -19,11 +20,11 @@ export function ChecklistSelection({ title, description, typeOptions, subtypeOpt
19
20
  }
20
21
  export function ChecklistQuestion({ label, options, value, onValueChange, disabled, remarks, onRemarksChange, remarksLimit = 500, remarksMaxLength, remarksRequired = false, evidence, evidenceCount = 0 }) {
21
22
  const name = React.useId();
22
- React.useEffect(() => { if (remarksRequired)
23
- setShowRemarks(true); }, [remarksRequired, value]);
24
- const [showRemarks, setShowRemarks] = React.useState(Boolean(remarks));
23
+ const [showRemarks, setShowRemarks] = React.useState(false);
25
24
  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] });
25
+ const remarksToggleLabel = showRemarks ? "Hide remarks" : "Show remarks";
26
+ const evidenceToggleLabel = showEvidence ? "Hide photos" : "Show photos";
27
+ return _jsxs("div", { "data-slot": "checklist-question", className: "space-y-4 rounded-xl border border-border bg-card p-4 shadow-xs", children: [_jsx("p", { id: `${name}-label`, className: "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(Tooltip, { content: remarksToggleLabel, children: _jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", "aria-label": remarksToggleLabel, "aria-expanded": showRemarks, "aria-controls": `${name}-remarks`, onClick: () => setShowRemarks((open) => !open), children: _jsx(MessageSquareText, { "aria-hidden": "true", size: 16 }) }) }), evidence ? _jsx(Tooltip, { content: `${evidenceToggleLabel}${evidenceCount > 0 ? ` (${evidenceCount})` : ""}`, children: _jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", "aria-label": evidenceToggleLabel, "aria-expanded": showEvidence, "aria-controls": `${name}-evidence`, onClick: () => setShowEvidence((open) => !open), children: _jsx(Camera, { "aria-hidden": "true", size: 16 }) }) }) : null] })] }), showEvidence && evidence ? _jsx("div", { id: `${name}-evidence`, children: evidence }) : null, showRemarks ? _jsx("div", { id: `${name}-remarks`, children: _jsx(Textarea, { label: "Remarks", hideLabel: true, placeholder: "Type...", value: remarks, disabled: disabled, "aria-required": remarksRequired, rows: 5, onChange: (event) => onRemarksChange(event.target.value), maxWords: remarksMaxLength === undefined ? remarksLimit : undefined, maxLength: remarksMaxLength, showCount: remarksMaxLength !== undefined }) }) : null] });
27
28
  }
28
29
  export function ChecklistAttachments({ items, onRemove }) {
29
30
  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)) });
package/dist/dialog.d.ts CHANGED
@@ -9,7 +9,9 @@ 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
+ variant?: "default" | "confirmation" | "routingConfirmation";
13
+ /** Standard dialogs are 32rem wide; larger sizes are for complex editors. */
14
+ size?: "standard" | "large" | "extraLarge" | "full";
13
15
  open?: boolean;
14
16
  defaultOpen?: boolean;
15
17
  onOpenChange?: (open: boolean) => void;
@@ -27,5 +29,5 @@ export interface DialogProps {
27
29
  /** Whether a pointer interaction outside the dialog closes it. */
28
30
  closeOnOutsideClick?: boolean;
29
31
  }
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;
32
+ declare function Dialog({ variant, size, open, defaultOpen, onOpenChange, modal, trigger, title, description, children, footer, primaryAction, secondaryAction, contentClassName, bodyClassName, hideCloseButton, closeOnOutsideClick, }: DialogProps): import("react/jsx-runtime").JSX.Element;
31
33
  export { Dialog };
package/dist/dialog.js CHANGED
@@ -32,7 +32,7 @@ function renderDialogAction({ action, close, defaultVariant, }) {
32
32
  }
33
33
  }, children: label }));
34
34
  }
35
- function Dialog({ variant = "default", open, defaultOpen, onOpenChange, modal, trigger, title, description, children, footer, primaryAction, secondaryAction, contentClassName, bodyClassName, hideCloseButton = false, closeOnOutsideClick = false, }) {
35
+ function Dialog({ variant = "default", size = "standard", open, defaultOpen, onOpenChange, modal, trigger, title, description, children, footer, primaryAction, secondaryAction, contentClassName, bodyClassName, hideCloseButton = false, closeOnOutsideClick = false, }) {
36
36
  const [isOpen, setIsOpen] = useControllableOpen({
37
37
  open,
38
38
  defaultOpen,
@@ -44,11 +44,16 @@ function Dialog({ variant = "default", open, defaultOpen, onOpenChange, modal, t
44
44
  const body = resolveDialogRenderable(children, helpers);
45
45
  const customFooter = resolveDialogRenderable(footer, helpers);
46
46
  const hasFooter = Boolean(customFooter || primaryAction || secondaryAction);
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
+ const isCompactConfirmation = variant === "confirmation";
48
+ const isRoutingConfirmation = variant === "routingConfirmation";
49
+ 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(isCompactConfirmation &&
50
+ "gap-0 overflow-hidden rounded-xl p-0", isRoutingConfirmation &&
51
+ "gap-0 rounded-overlay p-[var(--space-8)]", "w-[calc(100vw-2rem)]", { standard: "max-w-lg", large: "max-w-3xl", extraLarge: "max-w-5xl", full: "max-w-[96rem]" }[size], contentClassName), hideCloseButton: hideCloseButton || isCompactConfirmation || isRoutingConfirmation, onPointerDownOutside: (event) => {
48
52
  if (!closeOnOutsideClick) {
49
53
  event.preventDefault();
50
54
  }
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
55
+ }, children: [_jsxs("div", { className: cn(isCompactConfirmation && "flex items-start gap-4 p-4"), children: [isCompactConfirmation ? (_jsx("div", { className: "rounded-lg bg-muted p-2", children: _jsx(CircleAlert, { "aria-hidden": "true", className: "size-6" }) })) : null, _jsxs(DialogHeader, { className: cn(isRoutingConfirmation && "gap-3 space-y-0"), children: [_jsx(DialogTitle, { className: cn(isRoutingConfirmation && "text-2xl leading-8"), children: title }), description ? (_jsx(DialogDescription, { className: cn(isRoutingConfirmation &&
56
+ "text-base leading-6 text-foreground"), children: description })) : null] })] }), body ? (_jsx("div", { className: cn("grid gap-4", isRoutingConfirmation && "mt-3 gap-[var(--space-8)]", bodyClassName), children: body })) : null, hasFooter ? (_jsx(DialogFooter, { className: cn(isCompactConfirmation && "border-t bg-muted/30 p-4", isRoutingConfirmation && "mt-[var(--space-8)] gap-3"), children: customFooter !== null && customFooter !== void 0 ? customFooter : (_jsxs(_Fragment, { children: [secondaryAction
52
57
  ? renderDialogAction({
53
58
  action: secondaryAction,
54
59
  close: helpers.close,
@@ -4,7 +4,7 @@ import { Button } from "./button";
4
4
  import { TableAddRow } from "./table-add-row";
5
5
  import { TrashIcon } from "./animated-icons/trash";
6
6
  import { Checkbox } from "./checkbox";
7
- import { EmptyState } from "./empty-state";
7
+ import { TableEmptyState } from "./table-empty-state";
8
8
  import { InputControl } from "./input-control";
9
9
  import { TableRoot as Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "./table";
10
10
  import { cn } from "./lib/utils";
@@ -121,7 +121,7 @@ function EditableTable({ value, onChange, columns, isEditing = false, className,
121
121
  const checkableColumnWidth = checkableHeader ? "9rem" : "48px";
122
122
  return (_jsxs("div", { className: cn("space-y-2", className), children: [_jsx("div", { className: "overflow-x-auto rounded-surface border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [checkable ? (_jsx(TableHead, { width: checkableColumnWidth, minWidth: checkableColumnWidth, children: checkableHeader })) : null, columns.map((column, index) => (_jsx(TableHead, { width: column.width, minWidth: getColumnMinWidth(column), children: _jsx("div", { className: "whitespace-nowrap", children: column.header }) }, `header-${index}`))), hasActionsColumn ? (_jsx(TableHead, { width: renderRowActions ? undefined : "72px", minWidth: renderRowActions ? "120px" : "72px" })) : null] }) }), _jsx(TableBody, { children: value.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length +
123
123
  (checkable ? 1 : 0) +
124
- (hasActionsColumn ? 1 : 0), className: "p-0", children: _jsx(EmptyState, { message: "No data available", description: "There are no rows to display in this table yet.", className: "border-0 bg-transparent px-0 py-[var(--space-6)]" }) }) })) : (value.map((row, rowIndex) => (_jsxs(TableRow, { className: rowIndex % 2 === 0
124
+ (hasActionsColumn ? 1 : 0), className: "p-0", children: _jsx(TableEmptyState, {}) }) })) : (value.map((row, rowIndex) => (_jsxs(TableRow, { className: rowIndex % 2 === 0
125
125
  ? "bg-[hsl(var(--table-row-striped-background))]"
126
126
  : undefined, children: [checkable ? (_jsx(TableCell, { width: checkableColumnWidth, minWidth: checkableColumnWidth, children: _jsx("div", { className: "flex items-center", onClick: (event) => event.stopPropagation(), children: _jsx(Checkbox, { checked: Boolean(row[activeField]), disabled: !isEditing, onChange: isEditing
127
127
  ? (event) => handleToggleRow(rowIndex, event.currentTarget.checked)
@@ -1,5 +1,9 @@
1
1
  import * as React from "react";
2
2
  export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
3
+ /** Muted panel treatment for section empty states. */
4
+ variant?: "default" | "muted";
5
+ /** Controls spacing between the icon, copy, and action. */
6
+ density?: "default" | "compact";
3
7
  message: React.ReactNode;
4
8
  description?: React.ReactNode;
5
9
  icon?: React.ComponentType<{
@@ -7,5 +11,5 @@ export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
7
11
  }>;
8
12
  action?: React.ReactNode;
9
13
  }
10
- declare function EmptyState({ message, description, icon: Icon, action, className, ...props }: EmptyStateProps): import("react/jsx-runtime").JSX.Element;
14
+ declare function EmptyState({ variant, density, message, description, icon: Icon, action, className, ...props }: EmptyStateProps): import("react/jsx-runtime").JSX.Element;
11
15
  export { EmptyState };
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "./lib/utils";
3
3
  import { Typography } from "./typography";
4
- function EmptyState({ message, description, icon: Icon, action, className, ...props }) {
5
- return (_jsxs("div", { className: cn("flex flex-col items-center justify-center px-6 py-10 text-center", className), ...props, children: [Icon ? (_jsx(Icon, { className: "mb-4 h-10 w-10 stroke-[1.5] text-muted-foreground/70" })) : null, _jsxs("div", { className: "space-y-1.5", children: [_jsx(Typography, { as: "p", variant: "bodySm", className: "font-semibold", children: message }), description ? (_jsx(Typography, { as: "p", variant: "caption", className: "max-w-md", children: description })) : null] }), action ? _jsx("div", { className: "pt-6", children: action }) : null] }));
4
+ function EmptyState({ variant = "default", density = "default", message, description, icon: Icon, action, className, ...props }) {
5
+ return (_jsxs("div", { className: cn("flex flex-col items-center justify-center whitespace-normal px-6 py-10 text-center", variant === "muted" && "rounded-lg bg-muted/30", className), ...props, children: [Icon ? (_jsx(Icon, { className: cn("h-10 w-10 stroke-[1.5] text-muted-foreground/70", density === "compact" ? "mb-2" : "mb-4") })) : null, _jsxs("div", { className: "max-w-full space-y-1.5", children: [_jsx(Typography, { as: "p", variant: "bodySm", className: "font-semibold", children: message }), description ? (_jsx(Typography, { as: "p", variant: "caption", className: "mx-auto max-w-md", children: description })) : null] }), action ? _jsx("div", { className: density === "compact" ? "pt-4" : "pt-6", children: action }) : null] }));
6
6
  }
7
7
  export { EmptyState };
@@ -0,0 +1,14 @@
1
+ import type { FileUploadStatus } from "./use-file-upload";
2
+ export declare function FileUploadStatusRow({ file, onCancel, onRemove, disabled, }: {
3
+ file: {
4
+ name: string;
5
+ type?: string;
6
+ size?: number | string;
7
+ status?: FileUploadStatus;
8
+ progress?: number;
9
+ error?: string;
10
+ };
11
+ onCancel?: () => void;
12
+ onRemove?: () => void;
13
+ disabled?: boolean;
14
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AlertCircle, CheckCircle2, File, FileText, ImageIcon, Trash2, X } from "lucide-react";
4
+ import { Button } from "./button";
5
+ import { cn } from "./lib/utils";
6
+ import { Progress } from "./progress";
7
+ import { Typography } from "./typography";
8
+ function formatUploadFileSize(size) {
9
+ if (typeof size === "string")
10
+ return size;
11
+ if (typeof size !== "number")
12
+ return "";
13
+ if (size === 0)
14
+ return "0 B";
15
+ const k = 1024;
16
+ const sizes = ["B", "KB", "MB", "GB"];
17
+ const i = Math.min(Math.floor(Math.log(size) / Math.log(k)), sizes.length - 1);
18
+ return `${parseFloat((size / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
19
+ }
20
+ function getStatusIcon(status, type) {
21
+ if (status === "complete") {
22
+ return (_jsx(CheckCircle2, { "aria-hidden": "true", className: "size-5 text-[hsl(var(--status-success-foreground))]" }));
23
+ }
24
+ if (status === "error") {
25
+ return (_jsx(AlertCircle, { "aria-hidden": "true", className: "size-5 text-[hsl(var(--status-destructive-foreground))]" }));
26
+ }
27
+ const Icon = (type === null || type === void 0 ? void 0 : type.startsWith("image/")) ? ImageIcon : type === "application/pdf" ? FileText : File;
28
+ return (_jsx(Icon, { "aria-hidden": "true", className: "size-5 text-muted-foreground" }));
29
+ }
30
+ export function FileUploadStatusRow({ file, onCancel, onRemove, disabled = false, }) {
31
+ var _a;
32
+ const isUploading = file.status === "uploading";
33
+ const isPending = file.status === "pending";
34
+ const isComplete = file.status === "complete";
35
+ const isError = file.status === "error";
36
+ const sizeLabel = formatUploadFileSize(file.size);
37
+ const canCancel = (isUploading || isPending) && onCancel;
38
+ const canRemove = !isUploading && onRemove;
39
+ const action = canCancel ? "cancel" : canRemove ? "remove" : null;
40
+ return (_jsxs("li", { className: cn("rounded-control border bg-background px-2.5 py-2", isError && "border-destructive/80 bg-destructive/5"), children: [_jsxs("div", { className: "grid grid-cols-[1.5rem_minmax(0,1fr)_auto_1.75rem] min-h-7 items-start gap-x-2", children: [_jsx("span", { className: "flex h-7 w-6 shrink-0 items-center justify-center", children: getStatusIcon(file.status, file.type) }), _jsxs("div", { className: "flex min-w-0 flex-col gap-1 py-1", children: [_jsx(Typography, { as: "p", variant: "label", className: cn("truncate", isComplete && "text-[hsl(var(--status-success-foreground))]"), title: file.name, children: file.name }), isError ? (_jsx(Typography, { as: "p", variant: "caption", className: "break-words text-xs leading-4 text-destructive", children: (_a = file.error) !== null && _a !== void 0 ? _a : "Upload failed" })) : null] }), sizeLabel ? (_jsx(Typography, { as: "span", variant: "bodySm", className: "col-start-3 mt-1.5 whitespace-nowrap text-xs leading-4 text-muted-foreground", children: sizeLabel })) : null, action ? (_jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "col-start-4 h-7 w-7 shrink-0", disabled: disabled, onClick: (event) => {
41
+ event.stopPropagation();
42
+ if (action === "cancel") {
43
+ onCancel === null || onCancel === void 0 ? void 0 : onCancel();
44
+ }
45
+ else {
46
+ onRemove === null || onRemove === void 0 ? void 0 : onRemove();
47
+ }
48
+ }, "aria-label": action === "cancel"
49
+ ? `Cancel upload for ${file.name}`
50
+ : `Remove ${file.name}`, children: action === "cancel" ? (_jsx(X, { "aria-hidden": "true" })) : (_jsx(Trash2, { "aria-hidden": "true" })) })) : null] }), (isUploading || isPending) ? (_jsx(Progress, { className: "mt-2", value: file.progress, size: "sm", label: `Uploading ${file.name}` })) : null] }));
51
+ }
@@ -1,24 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { AlertCircle, CheckCircle2, Paperclip, Trash2, X, } from "lucide-react";
3
+ import { X } from "lucide-react";
4
+ import { FileUploadStatusRow } from "./file-upload-status-row";
4
5
  import { Button } from "./button";
5
6
  import { cn } from "./lib/utils";
6
- import { Progress } from "./progress";
7
7
  import { toast } from "./toaster";
8
8
  import { Typography } from "./typography";
9
9
  const DEFAULT_TOAST_ID = "file-upload-status";
10
- function formatUploadFileSize(size) {
11
- if (typeof size === "string")
12
- return size;
13
- if (typeof size !== "number")
14
- return "";
15
- if (size === 0)
16
- return "0 B";
17
- const k = 1024;
18
- const sizes = ["B", "KB", "MB", "GB"];
19
- const i = Math.min(Math.floor(Math.log(size) / Math.log(k)), sizes.length - 1);
20
- return `${parseFloat((size / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
21
- }
22
10
  function getProgress(uploadProgress, fileName) {
23
11
  if (!uploadProgress)
24
12
  return undefined;
@@ -34,6 +22,7 @@ function normalizeFiles({ files, uploadProgress, }) {
34
22
  const result = (_a = progress === null || progress === void 0 ? void 0 : progress.result) !== null && _a !== void 0 ? _a : file.result;
35
23
  return {
36
24
  ...file,
25
+ name: file.name,
37
26
  progress: (_c = (_b = progress === null || progress === void 0 ? void 0 : progress.progress) !== null && _b !== void 0 ? _b : file.progress) !== null && _c !== void 0 ? _c : 0,
38
27
  status: (_e = (_d = progress === null || progress === void 0 ? void 0 : progress.status) !== null && _d !== void 0 ? _d : file.status) !== null && _e !== void 0 ? _e : "pending",
39
28
  error: (_f = progress === null || progress === void 0 ? void 0 : progress.error) !== null && _f !== void 0 ? _f : file.error,
@@ -72,43 +61,15 @@ function getDefaultDescription(files) {
72
61
  ].filter(Boolean);
73
62
  return parts.length > 0 ? parts.join(" / ") : undefined;
74
63
  }
75
- function getStatusIcon(status) {
76
- if (status === "complete") {
77
- return (_jsx(CheckCircle2, { "aria-hidden": "true", className: "h-4 w-4 text-[hsl(var(--status-success-foreground))]" }));
78
- }
79
- if (status === "error") {
80
- return (_jsx(AlertCircle, { "aria-hidden": "true", className: "h-4 w-4 text-[hsl(var(--status-destructive-foreground))]" }));
81
- }
82
- return (_jsx(Paperclip, { "aria-hidden": "true", className: "h-4 w-4 text-muted-foreground" }));
83
- }
84
- function FileUploadStatusRow({ file, onCancel, onRemove, }) {
85
- var _a;
86
- const isUploading = file.status === "uploading";
87
- const isPending = file.status === "pending";
88
- const isComplete = file.status === "complete";
89
- const isError = file.status === "error";
90
- const sizeLabel = formatUploadFileSize(file.size);
91
- const canCancel = (isUploading || isPending) && onCancel;
92
- const canRemove = (isComplete || isError) && onRemove;
93
- const action = canCancel ? "cancel" : canRemove ? "remove" : null;
94
- return (_jsxs("li", { className: cn("rounded-control border bg-background px-2.5 py-2", isError && "border-destructive/80 bg-destructive/5"), children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center", children: getStatusIcon(file.status) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(Typography, { as: "p", variant: "label", className: cn("truncate", isComplete && "text-[hsl(var(--status-success-foreground))]"), title: file.name, children: file.name }), isError ? (_jsx(Typography, { as: "p", variant: "bodySm", className: "mt-0.5 text-destructive", children: (_a = file.error) !== null && _a !== void 0 ? _a : "Upload failed" })) : null] }), sizeLabel ? (_jsx(Typography, { as: "span", variant: "bodySm", className: "shrink-0 text-muted-foreground", children: sizeLabel })) : null, action ? (_jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "h-7 w-7 shrink-0", onClick: () => {
95
- if (action === "cancel") {
96
- onCancel === null || onCancel === void 0 ? void 0 : onCancel(file);
97
- }
98
- else {
99
- onRemove === null || onRemove === void 0 ? void 0 : onRemove(file);
100
- }
101
- }, "aria-label": action === "cancel"
102
- ? `Cancel upload for ${file.name}`
103
- : `Remove ${file.name}`, children: action === "cancel" ? (_jsx(X, { "aria-hidden": "true" })) : (_jsx(Trash2, { "aria-hidden": "true" })) })) : null] }), (isUploading || isPending) ? (_jsx(Progress, { className: "mt-2", value: file.progress, size: "sm", label: `Uploading ${file.name}` })) : null] }));
104
- }
105
64
  export function FileUploadStatusToast({ files, uploadProgress, title, description, className, onCancel, onRemove, onDismiss, }) {
106
65
  const normalizedFiles = normalizeFiles({ files, uploadProgress });
107
66
  const resolvedTitle = title !== null && title !== void 0 ? title : getDefaultTitle(normalizedFiles);
108
67
  const resolvedDescription = description !== null && description !== void 0 ? description : getDefaultDescription(normalizedFiles);
109
- return (_jsxs("section", { className: cn("w-[min(40rem,calc(100vw-2rem))] overflow-hidden rounded-overlay border border-border bg-background text-foreground shadow-overlay", className), "aria-label": "File upload status", children: [_jsxs("div", { className: "flex items-start gap-3 border-b px-3 py-2.5", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(Typography, { as: "p", variant: "label", className: "truncate", children: resolvedTitle }), resolvedDescription ? (_jsx(Typography, { as: "p", variant: "caption", className: "mt-0.5", children: resolvedDescription })) : null] }), onDismiss ? (_jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "h-7 w-7 shrink-0", onClick: onDismiss, "aria-label": "Dismiss file upload status", children: _jsx(X, { "aria-hidden": "true" }) })) : null] }), _jsx("ul", { className: "max-h-80 space-y-2 overflow-y-auto p-2.5", children: normalizedFiles.map((file, index) => {
68
+ return (_jsxs("section", { className: cn("w-[min(40rem,calc(100vw-2rem))] overflow-hidden rounded-overlay border border-border bg-background text-foreground shadow-overlay", className), "aria-label": "File upload status", children: [_jsxs("div", { className: "flex items-start gap-3 border-b px-3 py-2.5", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(Typography, { as: "p", variant: "label", className: "truncate", children: resolvedTitle }), resolvedDescription ? (_jsx(Typography, { as: "p", variant: "caption", className: "mt-0.5 text-xs leading-4", children: resolvedDescription })) : null] }), onDismiss ? (_jsx(Button, { type: "button", variant: "ghost", size: "compactIcon", className: "h-7 w-7 shrink-0", onClick: onDismiss, "aria-label": "Dismiss file upload status", children: _jsx(X, { "aria-hidden": "true" }) })) : null] }), _jsx("ul", { className: "max-h-80 space-y-2 overflow-y-auto p-2.5", children: normalizedFiles.map((file, index) => {
110
69
  var _a;
111
- return (_jsx(FileUploadStatusRow, { file: file, onCancel: onCancel, onRemove: onRemove }, `${file.name}-${(_a = file.size) !== null && _a !== void 0 ? _a : "unknown"}-${index}`));
70
+ return (_jsx(FileUploadStatusRow, { file: file, onCancel: onCancel ? () => onCancel(file) : undefined, onRemove: onRemove && (file.status === "complete" || file.status === "error")
71
+ ? () => onRemove(file)
72
+ : undefined }, `${file.name}-${(_a = file.size) !== null && _a !== void 0 ? _a : "unknown"}-${index}`));
112
73
  }) })] }));
113
74
  }
114
75
  export function showFileUploadStatusToast({ id = DEFAULT_TOAST_ID, duration = Number.POSITIVE_INFINITY, dismissible = true, onDismiss, ...props }) {
@@ -2,14 +2,9 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useCallback, useState } from "react";
5
- import { File as FileIcon, ImageIcon } from "lucide-react";
6
- import { CheckIcon } from "./animated-icons/check";
7
- import { FileTextIcon } from "./animated-icons/file-text";
8
5
  import { UploadIcon } from "./animated-icons/upload";
9
- import { XIcon } from "./animated-icons/x";
10
- import { Button } from "./button";
11
6
  import { cn } from "./lib/utils";
12
- import { Progress } from "./progress";
7
+ import { FileUploadStatusRow } from "./file-upload-status-row";
13
8
  import { Typography } from "./typography";
14
9
  const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
15
10
  function formatFileSize(bytes) {
@@ -20,15 +15,6 @@ function formatFileSize(bytes) {
20
15
  const i = Math.floor(Math.log(bytes) / Math.log(k));
21
16
  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
22
17
  }
23
- function getFileIcon(file) {
24
- if (file.type.startsWith("image/")) {
25
- return _jsx(ImageIcon, { className: "h-4 w-4 text-muted-foreground" });
26
- }
27
- if (file.type === "application/pdf") {
28
- return _jsx(FileTextIcon, { className: "text-muted-foreground", size: 16 });
29
- }
30
- return _jsx(FileIcon, { className: "h-4 w-4 text-muted-foreground" });
31
- }
32
18
  export function FileUpload({ accept, maxSize = DEFAULT_MAX_FILE_SIZE, maxFiles = 10, onFilesChange, value = [], disabled = false, className, uploadProgress, }) {
33
19
  const [isDragOver, setIsDragOver] = useState(false);
34
20
  const [error, setError] = useState(null);
@@ -92,16 +78,15 @@ export function FileUpload({ accept, maxSize = DEFAULT_MAX_FILE_SIZE, maxFiles =
92
78
  const fileInput = (_jsx("input", { ref: inputRef, type: "file", accept: accept, multiple: maxFiles > 1, onChange: handleInputChange, disabled: disabled, className: "hidden" }));
93
79
  return (_jsxs("div", { className: cn("space-y-2", className), children: [fileInput, _jsxs("div", { onDrop: handleDrop, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onClick: openFilePicker, className: cn("relative flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-surface border border-dashed p-4 transition-colors", isDragOver && !disabled
94
80
  ? "border-primary bg-primary/5"
95
- : "border-muted-foreground/25 hover:border-muted-foreground/50", disabled && "cursor-not-allowed opacity-50"), children: [_jsx(UploadIcon, { className: cn(isDragOver ? "text-interactive" : "text-muted-foreground"), size: 28 }), _jsxs("div", { className: "text-center", children: [_jsx(Typography, { as: "p", variant: "label", children: isDragOver ? "Drop files here" : "Drag & drop files here" }), _jsxs(Typography, { as: "p", variant: "caption", className: "mt-1", children: ["or click to browse (max ", formatFileSize(maxSize), " each)"] })] })] }), error ? (_jsx(Typography, { as: "p", variant: "bodySm", className: "text-destructive", children: error })) : null, value.length > 0 ? (_jsx("div", { className: "space-y-2", children: value.map((file, index) => {
96
- var _a;
81
+ : "border-muted-foreground/25 hover:border-muted-foreground/50", disabled && "cursor-not-allowed opacity-50"), children: [_jsx(UploadIcon, { className: cn(isDragOver ? "text-interactive" : "text-muted-foreground"), size: 28 }), _jsxs("div", { className: "text-center", children: [_jsx(Typography, { as: "p", variant: "label", children: isDragOver ? "Drop files here" : "Drag & drop files here" }), _jsxs(Typography, { as: "p", variant: "caption", className: "mt-1", children: ["or click to browse (max ", formatFileSize(maxSize), " each)"] })] })] }), error ? (_jsx(Typography, { as: "p", variant: "bodySm", className: "text-destructive", children: error })) : null, value.length > 0 ? (_jsx("ul", { className: "space-y-2", children: value.map((file, index) => {
97
82
  const progress = uploadProgress === null || uploadProgress === void 0 ? void 0 : uploadProgress.get(file.name);
98
- const isUploading = (progress === null || progress === void 0 ? void 0 : progress.status) === "uploading";
99
- const isComplete = (progress === null || progress === void 0 ? void 0 : progress.status) === "complete";
100
- const isError = (progress === null || progress === void 0 ? void 0 : progress.status) === "error";
101
- return (_jsxs("div", { className: "rounded-control border bg-muted/30 px-2.5 py-1.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [getFileIcon(file), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(Typography, { as: "p", variant: "label", className: "truncate", children: file.name }), _jsxs(Typography, { as: "p", variant: "caption", children: [formatFileSize(file.size), isUploading && ` • ${progress === null || progress === void 0 ? void 0 : progress.progress}%`, isComplete && " • Complete", isError && ` • ${(progress === null || progress === void 0 ? void 0 : progress.error) || "Error"}`] })] }), isComplete ? (_jsx("div", { className: "flex h-6 w-6 shrink-0 items-center justify-center text-[hsl(var(--status-success-foreground))]", children: _jsx(CheckIcon, { size: 16 }) })) : (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0", onClick: (e) => {
102
- e.stopPropagation();
103
- removeFile(index);
104
- }, disabled: disabled || isUploading, "aria-label": `Remove ${file.name}`, children: _jsx(XIcon, { size: 16 }) }))] }), (isUploading ||
105
- (progress && progress.status === "pending")) && (_jsx("div", { className: "mt-2", children: _jsx(Progress, { value: (_a = progress === null || progress === void 0 ? void 0 : progress.progress) !== null && _a !== void 0 ? _a : 0, size: "sm", variant: isError ? "error" : "default", label: `Uploading ${file.name}` }) }))] }, `${file.name}-${file.size}-${index}`));
83
+ return (_jsx(FileUploadStatusRow, { file: {
84
+ name: file.name,
85
+ type: file.type,
86
+ size: file.size,
87
+ status: progress === null || progress === void 0 ? void 0 : progress.status,
88
+ progress: progress === null || progress === void 0 ? void 0 : progress.progress,
89
+ error: progress === null || progress === void 0 ? void 0 : progress.error,
90
+ }, disabled: disabled, onRemove: () => removeFile(index) }, `${file.name}-${file.size}-${index}`));
106
91
  }) })) : null] }));
107
92
  }
package/dist/index.d.ts CHANGED
@@ -40,6 +40,7 @@ export { CountBadge } from "./count-badge";
40
40
  export type { CountBadgeProps } from "./count-badge";
41
41
  export { StatusDot } from "./status-dot";
42
42
  export type { StatusDotProps, StatusDotSize, StatusDotVariant, } from "./status-dot";
43
+ export { TableEmptyState } from "./table-empty-state";
43
44
  export { EmptyState } from "./empty-state";
44
45
  export type { EmptyStateProps } from "./empty-state";
45
46
  export { EditableTable } from "./editable-table";
@@ -171,3 +172,4 @@ export { ChecklistAttachments, ChecklistPanel, ChecklistSection, ChecklistSelect
171
172
  export type { ChecklistAttachmentsProps, ChecklistPanelProps, ChecklistSectionProps, ChecklistSelectionProps, ChecklistQuestionProps, ChecklistQuestionOption } from "./checklist";
172
173
  export { TableAddRow, type TableAddRowProps } from "./table-add-row";
173
174
  export { PopoverList, type PopoverListProps } from "./popover";
175
+ export { StatusBanner, type StatusBannerProps } from "./status-banner";
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ export { ThemeSwitcher } from "./theme-switcher";
20
20
  export { DateInput } from "./date-input";
21
21
  export { CountBadge } from "./count-badge";
22
22
  export { StatusDot } from "./status-dot";
23
+ export { TableEmptyState } from "./table-empty-state";
23
24
  export { EmptyState } from "./empty-state";
24
25
  export { EditableTable } from "./editable-table";
25
26
  export { EditButton } from "./edit-button";
@@ -90,3 +91,4 @@ export { parseDateTime, formatDateTime } from "./lib/date-utils";
90
91
  export { ChecklistAttachments, ChecklistPanel, ChecklistSection, ChecklistSelection, ChecklistQuestion } from "./checklist";
91
92
  export { TableAddRow } from "./table-add-row";
92
93
  export { PopoverList } from "./popover";
94
+ export { StatusBanner } from "./status-banner";
@@ -66,7 +66,9 @@ function LabelValueGrid({ items, columns = 4, density = "default", className, it
66
66
  }, [columns, density, resolvedMinColumnWidth]);
67
67
  return (_jsx("div", { ref: containerRef, className: cn("grid items-start", density === "compact"
68
68
  ? "auto-rows-auto gap-x-4 gap-y-3"
69
- : "auto-rows-fr gap-x-6 gap-y-6", className), style: {
69
+ : resolvedColumns === 1
70
+ ? "auto-rows-auto gap-x-6 gap-y-6"
71
+ : "auto-rows-fr gap-x-6 gap-y-6", className), style: {
70
72
  ...style,
71
73
  gridTemplateColumns: `repeat(${resolvedColumns}, minmax(0, 1fr))`,
72
74
  }, ...props, children: resolvedItems.map((item, index) => {
package/dist/popover.d.ts CHANGED
@@ -13,7 +13,8 @@ export interface PopoverProps extends Pick<PopoverRootProps, "defaultOpen" | "mo
13
13
  children: React.ReactNode;
14
14
  /** Visible popover heading. Also labels the popover for assistive technology. */
15
15
  title?: React.ReactNode;
16
- surface?: "panel" | "menu" | "message";
16
+ /** List surfaces scroll at the overlay edge and stay within the available viewport height. */
17
+ surface?: "panel" | "menu" | "message" | "list";
17
18
  /** Controls whether Tab stays within the popover or closes it and continues in the page Tab order. */
18
19
  tabBehavior?: PopoverTabBehavior;
19
20
  contentClassName?: string;
package/dist/popover.js CHANGED
@@ -79,7 +79,7 @@ function Popover({ defaultOpen, modal, onOpenChange, open, trigger, virtualAncho
79
79
  contentOnCloseAutoFocus === null || contentOnCloseAutoFocus === void 0 ? void 0 : contentOnCloseAutoFocus(event);
80
80
  handleCloseAutoFocus(event);
81
81
  }, [contentOnCloseAutoFocus, handleCloseAutoFocus]);
82
- return (_jsxs(PopoverRoot, { modal: modal, onOpenChange: setOpen, open: currentOpen, children: [virtualAnchorRef ? (_jsx(PopoverAnchor, { virtualRef: virtualAnchorRef })) : null, triggerTooltip ? (_jsx(Tooltip, { content: triggerTooltip, side: triggerTooltipSide, sideOffset: triggerTooltipSideOffset, contentClassName: triggerTooltipClassName, children: triggerElement })) : (triggerElement), _jsxs(PopoverContent, { ref: contentRef, align: align, sideOffset: sideOffset, "aria-labelledby": titleId !== null && titleId !== void 0 ? titleId : contentProps["aria-labelledby"], className: cn(surface === "menu" && "overflow-hidden p-1", surface === "message" && "overflow-hidden p-0", contentClassName), style: style, ...contentProps, onCloseAutoFocus: handleContentCloseAutoFocus, onKeyDown: handleContentKeyDown, onKeyDownCapture: handleContentKeyDownCapture, children: [title ? (_jsx(PopoverTitle, { id: titleId, className: cn(surface === "menu"
82
+ return (_jsxs(PopoverRoot, { modal: modal, onOpenChange: setOpen, open: currentOpen, children: [virtualAnchorRef ? (_jsx(PopoverAnchor, { virtualRef: virtualAnchorRef })) : null, triggerTooltip ? (_jsx(Tooltip, { content: triggerTooltip, side: triggerTooltipSide, sideOffset: triggerTooltipSideOffset, contentClassName: triggerTooltipClassName, children: triggerElement })) : (triggerElement), _jsxs(PopoverContent, { ref: contentRef, align: align, sideOffset: sideOffset, "aria-labelledby": titleId !== null && titleId !== void 0 ? titleId : contentProps["aria-labelledby"], className: cn(surface === "menu" && "overflow-hidden p-1", surface === "message" && "overflow-hidden p-0", surface === "list" && "max-h-[min(18rem,var(--radix-popover-content-available-height))] overflow-y-auto overscroll-contain", contentClassName), style: style, ...contentProps, onCloseAutoFocus: handleContentCloseAutoFocus, onKeyDown: handleContentKeyDown, onKeyDownCapture: handleContentKeyDownCapture, children: [title ? (_jsx(PopoverTitle, { id: titleId, className: cn(surface === "menu"
83
83
  ? "px-2 py-1.5"
84
84
  : "-mx-4 -mt-4 mb-4 border-b px-4 py-3"), children: title })) : null, children] })] }));
85
85
  }
@@ -89,5 +89,5 @@ function PopoverTitle({ className, children, ...props }) {
89
89
  export { Popover, PopoverTitle };
90
90
  /** Read-only information rows for a popover, without implying menu actions. */
91
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))) }));
92
+ return (_jsx("ul", { "data-slot": "popover-list", className: "space-y-3 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
93
  }
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+ export interface StatusBannerProps extends React.HTMLAttributes<HTMLDivElement> {
3
+ label: string;
4
+ detail?: React.ReactNode;
5
+ icon?: React.ComponentType<{
6
+ className?: string;
7
+ "aria-hidden"?: React.AriaAttributes["aria-hidden"];
8
+ }>;
9
+ trailingIcon?: React.ComponentType<{
10
+ className?: string;
11
+ "aria-hidden"?: React.AriaAttributes["aria-hidden"];
12
+ }>;
13
+ }
14
+ /** Compact primary-colour status strip for an ongoing activity. */
15
+ export declare function StatusBanner({ label, detail, icon: Icon, trailingIcon: TrailingIcon, className, ...props }: StatusBannerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "./lib/utils";
3
+ /** Compact primary-colour status strip for an ongoing activity. */
4
+ export function StatusBanner({ label, detail, icon: Icon, trailingIcon: TrailingIcon, className, ...props }) {
5
+ return (_jsxs("div", { ...props, className: cn("flex w-full items-center justify-center gap-2 rounded-none border border-border bg-primary px-4 py-1.5 text-sm text-primary-foreground shadow-sm", className), children: [Icon ? _jsx(Icon, { className: "h-4 w-4 shrink-0", "aria-hidden": true }) : null, _jsx("span", { className: "font-medium", children: label }), detail, TrailingIcon ? (_jsx(TrailingIcon, { className: "h-4 w-4 shrink-0 opacity-50", "aria-hidden": true })) : null] }));
6
+ }
@@ -0,0 +1,3 @@
1
+ import { type EmptyStateProps } from "./empty-state";
2
+ /** Shared empty-table presentation, with an icon and explanatory copy by default. */
3
+ export declare function TableEmptyState({ message, description, icon, ...props }: Partial<EmptyStateProps>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Inbox } from "lucide-react";
3
+ import { EmptyState } from "./empty-state";
4
+ /** Shared empty-table presentation, with an icon and explanatory copy by default. */
5
+ export function TableEmptyState({ message = "No data available", description = "There are no rows to display in this table yet.", icon = Inbox, ...props }) {
6
+ return _jsx(EmptyState, { ...props, message: message, description: description, icon: icon });
7
+ }
package/dist/table.js CHANGED
@@ -6,7 +6,7 @@ import { CSS } from "@dnd-kit/utilities";
6
6
  import { ArrowUpDown, ChevronRight } from "lucide-react";
7
7
  import { ArrowDownIcon } from "./animated-icons/arrow-down";
8
8
  import { ArrowUpIcon } from "./animated-icons/arrow-up";
9
- import { EmptyState } from "./empty-state";
9
+ import { TableEmptyState } from "./table-empty-state";
10
10
  import { Pagination } from "./pagination";
11
11
  import { Skeleton } from "./skeleton";
12
12
  import { TableColumnFilterControl, } from "./table-column-filter";
@@ -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 [&_[data-slot=button]]:px-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 [&:not([data-slot=table-empty-cell])_[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,
@@ -399,7 +399,7 @@ function getColumnPairResizeRange(column, nextColumn, columnWidths) {
399
399
  maxWidth: Math.round(currentWidth + maxDelta),
400
400
  };
401
401
  }
402
- function Table({ accessibleLabel, columns, rows, getRowKey, emptyState = (_jsx(EmptyState, { message: "No data available", description: "There are no rows to display in this table yet.", className: "border-0 bg-transparent px-0 py-6" })), toolbar, caption, className, containerClassName, tableClassName, rowClassName, rowTone, maxHeight, minHeight, sorting, pagination, loading = false, loadingLabel = "Loading table data", loadingRows = 5, visibleColumnKeys, resizableColumns = false, columnResizeMode = "pair", columnWidths, defaultColumnWidths, onColumnWidthsChange, renderColumnResizeHandle, tableLayout, centerEmptyState = false, emptyStateMinHeight = DEFAULT_CENTERED_EMPTY_STATE_MIN_HEIGHT, striped = false, rowIndicator, rowExpansion, reorderableColumns = false, onColumnOrderChange, onScroll, style, ...props }) {
402
+ function Table({ accessibleLabel, columns, rows, getRowKey, emptyState = (_jsx(TableEmptyState, {})), toolbar, caption, className, containerClassName, tableClassName, rowClassName, rowTone, maxHeight, minHeight, sorting, pagination, loading = false, loadingLabel = "Loading table data", loadingRows = 5, visibleColumnKeys, resizableColumns = false, columnResizeMode = "pair", columnWidths, defaultColumnWidths, onColumnWidthsChange, renderColumnResizeHandle, tableLayout, centerEmptyState = false, emptyStateMinHeight = DEFAULT_CENTERED_EMPTY_STATE_MIN_HEIGHT, striped = false, rowIndicator, rowExpansion, reorderableColumns = false, onColumnOrderChange, onScroll, style, ...props }) {
403
403
  var _a;
404
404
  const visibleColumns = getVisibleColumns(columns, visibleColumnKeys);
405
405
  const hasRowIndicatorColumn = Boolean(rowIndicator);
@@ -746,7 +746,7 @@ function Table({ accessibleLabel, columns, rows, getRowKey, emptyState = (_jsx(E
746
746
  })] }), isExpanded ? (_jsx(TableRow, { id: expandedContentId, "data-slot": "table-expanded-row", className: "bg-[hsl(var(--table-row-striped-background))] hover:bg-[hsl(var(--table-row-striped-background))]", children: _jsx(TableCell, { colSpan: Math.max(visibleColumns.length +
747
747
  (hasRowIndicatorColumn ? 1 : 0) +
748
748
  (hasExpansionColumn ? 1 : 0), 1), className: "h-auto whitespace-normal p-4", children: rowExpansion === null || rowExpansion === void 0 ? void 0 : rowExpansion.renderExpandedContent(row, rowIndex) }) })) : null] }, rowKey));
749
- })) : (_jsx(TableRow, { className: "cursor-default hover:bg-card", children: _jsx(TableCell, { colSpan: Math.max(visibleColumns.length +
749
+ })) : (_jsx(TableRow, { className: "cursor-default hover:bg-card", children: _jsx(TableCell, { "data-slot": "table-empty-cell", colSpan: Math.max(visibleColumns.length +
750
750
  (hasRowIndicatorColumn ? 1 : 0) +
751
751
  (hasExpansionColumn ? 1 : 0), 1), className: "p-6", children: resolvedEmptyState }) })) })] }), _jsx("div", { role: "status", className: "sr-only", children: loading ? loadingLabel : null })] }));
752
752
  const reorderableContainer = reorderableColumns ? (_jsx(DndContext, { sensors: columnReorderSensors, collisionDetection: closestCenter, onDragEnd: handleColumnDragEnd, children: _jsx(SortableContext, { items: reorderableColumnIds, strategy: horizontalListSortingStrategy, children: container }) })) : (container);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neasg/design-system",
3
- "version": "0.4.113",
3
+ "version": "0.4.121",
4
4
  "description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
5
5
  "repository": {
6
6
  "type": "git",