@neasg/design-system 0.4.10 → 0.4.11

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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # NEA Design System
2
2
 
3
- Shared NEA UI primitives, theme tokens, and Storybook documentation.
3
+ [![npm version](https://img.shields.io/npm/v/%40neasg%2Fdesign-system?style=flat-square&label=npm)](https://www.npmjs.com/package/@neasg/design-system)
4
4
 
5
- Package: [@neasg/design-system on npm](https://www.npmjs.com/package/@neasg/design-system)
5
+ Shared NEA UI primitives, theme tokens, and Storybook documentation.
6
6
 
7
7
  ## Local Development
8
8
 
@@ -12,6 +12,8 @@ interface DateInputSharedProps {
12
12
  hideLabel?: boolean;
13
13
  placeholder?: string;
14
14
  disabled?: boolean;
15
+ clearable?: boolean;
16
+ clearLabel?: string;
15
17
  minDate?: Date;
16
18
  maxDate?: Date;
17
19
  className?: string;
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { format, parse } from "date-fns";
5
5
  import { CalendarIcon } from "lucide-react";
6
+ import { XIcon } from "./animated-icons/x";
6
7
  import { Calendar } from "./calendar";
7
8
  import { inputControlClassName } from "./input-control";
8
9
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
@@ -92,8 +93,9 @@ function formatDisplayRange(range) {
92
93
  }
93
94
  const DateInput = React.forwardRef((props, ref) => {
94
95
  var _a, _b, _c;
95
- const { label, appearance = "field", description, error, invalid = false, hideLabel = false, disabled = false, minDate, maxDate, className, triggerClassName, labelClassName, descriptionClassName, errorClassName, id, } = props;
96
+ const { label, appearance = "field", description, error, invalid = false, hideLabel = false, disabled = false, clearable = false, clearLabel, minDate, maxDate, className, triggerClassName, labelClassName, descriptionClassName, errorClassName, id, } = props;
96
97
  const [open, setOpen] = React.useState(false);
98
+ const [isSelectingRangeEnd, setIsSelectingRangeEnd] = React.useState(false);
97
99
  const generatedId = React.useId();
98
100
  const inputId = id !== null && id !== void 0 ? id : `date-input-${generatedId}`;
99
101
  const errorId = error ? `${inputId}-error` : undefined;
@@ -109,6 +111,24 @@ const DateInput = React.forwardRef((props, ref) => {
109
111
  ? formatDisplayDate(selectedDate)
110
112
  : undefined;
111
113
  const placeholder = (_a = props.placeholder) !== null && _a !== void 0 ? _a : (isRange ? "Pick range" : "Pick a date");
114
+ const showClearButton = clearable && Boolean(displayValue) && !disabled;
115
+ const resolvedClearLabel = clearLabel !== null && clearLabel !== void 0 ? clearLabel : (isRange ? "Clear date range" : "Clear date");
116
+ const handleOpenChange = (nextOpen) => {
117
+ setOpen(nextOpen);
118
+ if (!nextOpen) {
119
+ setIsSelectingRangeEnd(false);
120
+ }
121
+ };
122
+ const handleClear = () => {
123
+ if (props.mode === "range") {
124
+ props.onValueChange({});
125
+ }
126
+ else {
127
+ props.onValueChange("");
128
+ }
129
+ setIsSelectingRangeEnd(false);
130
+ setOpen(false);
131
+ };
112
132
  const handleSelect = (date) => {
113
133
  if (props.mode === "range")
114
134
  return;
@@ -118,17 +138,37 @@ const DateInput = React.forwardRef((props, ref) => {
118
138
  const handleRangeSelect = (range) => {
119
139
  if (props.mode !== "range")
120
140
  return;
141
+ if (!(range === null || range === void 0 ? void 0 : range.from)) {
142
+ props.onValueChange({});
143
+ setIsSelectingRangeEnd(false);
144
+ return;
145
+ }
146
+ const rangeFrom = range.from;
147
+ const rangeTo = range.to;
148
+ const isCompletingRange = isSelectingRangeEnd || Boolean((selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.from) && !selectedRange.to);
149
+ const isSingleDayRange = rangeTo
150
+ ? getDateTime(rangeFrom) === getDateTime(rangeTo)
151
+ : false;
152
+ if (!isCompletingRange && isSingleDayRange) {
153
+ props.onValueChange({ from: formatISODate(rangeFrom) });
154
+ setIsSelectingRangeEnd(true);
155
+ return;
156
+ }
121
157
  props.onValueChange(formatDateRangeValue(range));
122
- if ((range === null || range === void 0 ? void 0 : range.from) && range.to) {
158
+ if (rangeTo) {
159
+ setIsSelectingRangeEnd(false);
123
160
  setOpen(false);
124
161
  }
162
+ else {
163
+ setIsSelectingRangeEnd(true);
164
+ }
125
165
  };
126
166
  const disabledDays = (date) => isDateOutsideBounds(date, minDate, maxDate);
127
167
  const calendarContent = (_jsx(PopoverContent, { className: "w-auto p-0", align: "start", 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, autoFocus: true })) : (_jsx(Calendar, { mode: "single", selected: selectedDate, onSelect: handleSelect, disabled: disabledDays, defaultMonth: selectedDate, autoFocus: true })) }));
128
168
  if (appearance === "inline") {
129
- return (_jsxs("span", { className: cn("inline-flex", className), "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsxs(PopoverRoot, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _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-primary underline decoration-primary/40 underline-offset-4 transition-colors hover:decoration-primary 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 })] }) }), 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: [_jsxs(PopoverRoot, { open: open, onOpenChange: handleOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _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-primary underline decoration-primary/40 underline-offset-4 transition-colors hover:decoration-primary 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 })] }) }), 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] }));
130
170
  }
131
- 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(PopoverRoot, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _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(inputControlClassName, "relative cursor-pointer pr-9 text-left", !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 }), _jsx(CalendarIcon, { "aria-hidden": "true", className: "pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground", strokeWidth: 2 })] }) }), calendarContent] }), 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(PopoverRoot, { open: open, onOpenChange: handleOpenChange, children: [_jsxs("div", { className: "relative", children: [_jsx(PopoverTrigger, { asChild: true, children: _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 }) }) }), _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 })] })] }), calendarContent] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
132
172
  });
133
173
  DateInput.displayName = "DateInput";
134
174
  export { DateInput };
@@ -30,8 +30,9 @@ export interface FilterPopoverProps extends Pick<PopoverProps, "align" | "defaul
30
30
  emptyLabel?: React.ReactNode;
31
31
  onReset?: () => void;
32
32
  contentClassName?: string;
33
+ viewClassName?: string;
33
34
  maxHeight?: string;
34
35
  }
35
36
  declare function getFilterSelectionLabel(count: number): string;
36
- declare function FilterPopover({ align, contentClassName, defaultOpen, emptyLabel, groupListLabel, groups, maxHeight, modal, onOpenChange, onReset, open, resetLabel, selectedCount, side, trigger, triggerLabel, }: FilterPopoverProps): import("react/jsx-runtime").JSX.Element;
37
+ declare function FilterPopover({ align, contentClassName, defaultOpen, emptyLabel, groupListLabel, groups, maxHeight, modal, onOpenChange, onReset, open, resetLabel, selectedCount, side, trigger, triggerLabel, viewClassName, }: FilterPopoverProps): import("react/jsx-runtime").JSX.Element;
37
38
  export { FilterPopover, getFilterSelectionLabel };
@@ -32,7 +32,7 @@ function getFilterSelectionLabel(count) {
32
32
  }
33
33
  return count === 1 ? "1 selected" : `${count} selected`;
34
34
  }
35
- function FilterPopover({ align = "start", contentClassName, defaultOpen, emptyLabel = "No options found.", groupListLabel = "Filter by", groups, maxHeight = "max-h-80", modal, onOpenChange, onReset, open, resetLabel = "Reset filters", selectedCount, side, trigger, triggerLabel = "Filters", }) {
35
+ function FilterPopover({ align = "start", contentClassName, defaultOpen, emptyLabel = "No options found.", groupListLabel = "Filter by", groups, maxHeight = "max-h-80", modal, onOpenChange, onReset, open, resetLabel = "Reset filters", selectedCount, side, trigger, triggerLabel = "Filters", viewClassName, }) {
36
36
  var _a, _b, _c, _d, _e;
37
37
  const [activeGroupId, setActiveGroupId] = React.useState(null);
38
38
  const [transitionDirection, setTransitionDirection] = React.useState("forward");
@@ -63,7 +63,7 @@ function FilterPopover({ align = "start", contentClassName, defaultOpen, emptyLa
63
63
  setActiveGroupId(null);
64
64
  };
65
65
  const defaultTrigger = (_jsxs(Button, { type: "button", variant: "outline", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), triggerLabel, resolvedSelectedCount > 0 ? (_jsx(CountBadge, { count: resolvedSelectedCount, size: "sm" })) : null] }));
66
- return (_jsxs(Popover, { align: align, contentClassName: cn("w-[360px]", contentClassName), defaultOpen: defaultOpen, modal: modal, onOpenChange: onOpenChange, open: open, side: side, surface: "menu", trigger: trigger !== null && trigger !== void 0 ? trigger : defaultTrigger, children: [_jsx(PopoverMenuView, { className: "-mx-1 -mt-1 px-1 pt-1", direction: transitionDirection, viewKey: activeGroup ? `group-${activeGroup.id}` : "filter-groups", children: activeGroup ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "-mx-1 -mt-1 border-b p-1", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "w-full justify-start", onClick: showFilterGroups, children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), activeGroup.label] }) }), activeGroup.searchable ? (_jsx(PopoverMenuSearch, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: (_c = activeGroup.searchPlaceholder) !== null && _c !== void 0 ? _c : `Search ${getGroupLabel(activeGroup).toLowerCase()}...`, "aria-label": (_d = activeGroup.searchPlaceholder) !== null && _d !== void 0 ? _d : `Search ${getGroupLabel(activeGroup).toLowerCase()}` })) : null, _jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsx(PopoverMenuSection, { children: filteredOptions.length ? (filteredOptions.map((option) => (_jsx(PopoverMenuItem, { active: option.selected, "aria-label": option.ariaLabel, "aria-pressed": option.selected, disabled: option.disabled, onClick: option.onSelect, children: option.label }, option.id)))) : (_jsx(Typography, { as: "p", variant: "caption", className: "px-2 py-2", children: (_e = activeGroup.emptyLabel) !== null && _e !== void 0 ? _e : emptyLabel })) }) })] })) : (_jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsxs(PopoverMenuGroup, { children: [_jsx(PopoverMenuGroupLabel, { children: groupListLabel }), _jsx(PopoverMenuSection, { children: groups.map((group) => {
66
+ return (_jsxs(Popover, { align: align, contentClassName: cn("w-[360px]", contentClassName), defaultOpen: defaultOpen, modal: modal, onOpenChange: onOpenChange, open: open, side: side, surface: "menu", trigger: trigger !== null && trigger !== void 0 ? trigger : defaultTrigger, children: [_jsx(PopoverMenuView, { className: cn("-mx-1 -mt-1 px-1 pt-1", viewClassName), direction: transitionDirection, viewKey: activeGroup ? `group-${activeGroup.id}` : "filter-groups", children: activeGroup ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "-mx-1 -mt-1 border-b p-1", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "w-full justify-start", onClick: showFilterGroups, children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), activeGroup.label] }) }), activeGroup.searchable ? (_jsx(PopoverMenuSearch, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: (_c = activeGroup.searchPlaceholder) !== null && _c !== void 0 ? _c : `Search ${getGroupLabel(activeGroup).toLowerCase()}...`, "aria-label": (_d = activeGroup.searchPlaceholder) !== null && _d !== void 0 ? _d : `Search ${getGroupLabel(activeGroup).toLowerCase()}` })) : null, _jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsx(PopoverMenuSection, { children: filteredOptions.length ? (filteredOptions.map((option) => (_jsx(PopoverMenuItem, { active: option.selected, "aria-label": option.ariaLabel, "aria-pressed": option.selected, disabled: option.disabled, onClick: option.onSelect, children: option.label }, option.id)))) : (_jsx(Typography, { as: "p", variant: "caption", className: "px-2 py-2", children: (_e = activeGroup.emptyLabel) !== null && _e !== void 0 ? _e : emptyLabel })) }) })] })) : (_jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsxs(PopoverMenuGroup, { children: [_jsx(PopoverMenuGroupLabel, { children: groupListLabel }), _jsx(PopoverMenuSection, { children: groups.map((group) => {
67
67
  var _a;
68
68
  const groupSelectedCount = getGroupSelectedCount(group);
69
69
  const selectionLabel = (_a = group.selectionLabel) !== null && _a !== void 0 ? _a : getFilterSelectionLabel(groupSelectedCount);
@@ -4,6 +4,7 @@ import type { TableColumn } from "./table";
4
4
  type PopoverPlacementProps = Pick<PopoverProps, "align" | "side" | "sideOffset">;
5
5
  export interface TableColumnVisibilityMenuProps<Row> extends PopoverPlacementProps {
6
6
  columns: TableColumn<Row>[];
7
+ /** Ordered visible column keys. Reordering in the menu emits the next order through `onVisibleColumnKeysChange`. */
7
8
  visibleColumnKeys: readonly string[];
8
9
  onVisibleColumnKeysChange: (visibleColumnKeys: string[]) => void;
9
10
  defaultVisibleColumnKeys?: readonly string[];
@@ -1,7 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { Columns3 } from "lucide-react";
4
+ import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from "@dnd-kit/core";
5
+ import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
6
+ import { CSS } from "@dnd-kit/utilities";
7
+ import { Columns3, GripVertical } from "lucide-react";
5
8
  import { Button } from "./button";
6
9
  import { Checkbox } from "./checkbox";
7
10
  import { InputControl } from "./input-control";
@@ -19,11 +22,18 @@ function getColumnVisibilityLabel(column) {
19
22
  }
20
23
  return column.key;
21
24
  }
22
- function orderVisibleColumnKeys(columns, visibleColumnKeys) {
23
- const visibleColumnKeySet = new Set(visibleColumnKeys);
24
- return columns
25
- .map((column) => column.key)
26
- .filter((columnKey) => visibleColumnKeySet.has(columnKey));
25
+ function orderColumnKeys(columns, columnKeys) {
26
+ const columnKeySet = new Set(columns.map((column) => column.key));
27
+ const seenColumnKeys = new Set();
28
+ const orderedColumnKeys = [];
29
+ Array.from(columnKeys).forEach((columnKey) => {
30
+ if (!columnKeySet.has(columnKey) || seenColumnKeys.has(columnKey)) {
31
+ return;
32
+ }
33
+ orderedColumnKeys.push(columnKey);
34
+ seenColumnKeys.add(columnKey);
35
+ });
36
+ return orderedColumnKeys;
27
37
  }
28
38
  function arraysMatch(left, right) {
29
39
  if (left.length !== right.length) {
@@ -31,44 +41,80 @@ function arraysMatch(left, right) {
31
41
  }
32
42
  return left.every((value, index) => value === right[index]);
33
43
  }
44
+ function SortableColumnOption({ columnKey, label, checked, disabled, onToggle, }) {
45
+ const checkboxId = React.useId();
46
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: columnKey, disabled: !checked });
47
+ return (_jsxs("div", { ref: setNodeRef, style: {
48
+ transform: CSS.Transform.toString(transform),
49
+ transition,
50
+ zIndex: isDragging ? 50 : undefined,
51
+ }, className: cn("flex h-9 items-center gap-2 rounded-sm px-1.5 pr-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground", isDragging && "opacity-50 shadow-sm"), children: [_jsx("button", { type: "button", "aria-label": `Reorder ${label}`, disabled: !checked, className: cn("inline-flex size-6 shrink-0 touch-none items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-background hover:text-foreground", checked
52
+ ? "cursor-grab active:cursor-grabbing"
53
+ : "cursor-not-allowed opacity-40"), ...(checked ? { ...attributes, ...listeners } : {}), children: _jsx(GripVertical, { className: "h-4 w-4", "aria-hidden": "true" }) }), _jsx(Checkbox, { id: checkboxId, checked: checked, disabled: disabled, onChange: onToggle, "aria-label": label }), _jsx("label", { htmlFor: checkboxId, className: cn("min-w-0 flex-1 cursor-pointer truncate", disabled && "cursor-not-allowed text-muted-foreground"), children: label })] }));
54
+ }
34
55
  function TableColumnVisibilityMenu({ columns, visibleColumnKeys, onVisibleColumnKeysChange, defaultVisibleColumnKeys, trigger, triggerLabel = "Customize columns", triggerClassName, contentClassName, className, searchPlaceholder = "Search columns", restoreDefaultsLabel = "Restore defaults", emptyMessage = "No columns found", minVisibleColumns = 1, align = "end", sideOffset = 4, side, }) {
35
56
  const [query, setQuery] = React.useState("");
57
+ const sensors = useSensors(useSensor(PointerSensor, {
58
+ activationConstraint: {
59
+ distance: 8,
60
+ },
61
+ }), useSensor(KeyboardSensor, {
62
+ coordinateGetter: sortableKeyboardCoordinates,
63
+ }));
36
64
  const searchInputId = React.useId();
37
65
  const requiredVisibleColumns = Math.max(0, minVisibleColumns);
38
- const visibleColumnKeySet = React.useMemo(() => new Set(visibleColumnKeys), [visibleColumnKeys]);
39
- const visibleColumnCount = columns.filter((column) => visibleColumnKeySet.has(column.key)).length;
40
66
  const selectableColumns = React.useMemo(() => columns.filter((column) => column.hideable !== false), [columns]);
41
- const defaultKeys = React.useMemo(() => orderVisibleColumnKeys(columns, defaultVisibleColumnKeys !== null && defaultVisibleColumnKeys !== void 0 ? defaultVisibleColumnKeys : columns
67
+ const defaultKeys = React.useMemo(() => orderColumnKeys(columns, defaultVisibleColumnKeys !== null && defaultVisibleColumnKeys !== void 0 ? defaultVisibleColumnKeys : columns
42
68
  .filter((column) => column.defaultVisible !== false)
43
69
  .map((column) => column.key)), [columns, defaultVisibleColumnKeys]);
44
- const currentKeys = React.useMemo(() => orderVisibleColumnKeys(columns, visibleColumnKeys), [columns, visibleColumnKeys]);
70
+ const currentKeys = React.useMemo(() => orderColumnKeys(columns, visibleColumnKeys), [columns, visibleColumnKeys]);
71
+ const visibleColumnKeySet = React.useMemo(() => new Set(currentKeys), [currentKeys]);
72
+ const visibleColumnCount = currentKeys.length;
73
+ const orderedSelectableColumns = React.useMemo(() => {
74
+ const selectableColumnsByKey = new Map(selectableColumns.map((column) => [column.key, column]));
75
+ const orderedVisibleColumns = currentKeys
76
+ .map((columnKey) => selectableColumnsByKey.get(columnKey))
77
+ .filter((column) => Boolean(column));
78
+ const orderedVisibleColumnKeySet = new Set(orderedVisibleColumns.map((column) => column.key));
79
+ const hiddenColumns = selectableColumns.filter((column) => !orderedVisibleColumnKeySet.has(column.key));
80
+ return [...orderedVisibleColumns, ...hiddenColumns];
81
+ }, [currentKeys, selectableColumns]);
45
82
  const canRestoreDefaults = defaultKeys.length >= requiredVisibleColumns &&
46
83
  !arraysMatch(currentKeys, defaultKeys);
47
84
  const normalizedQuery = query.trim().toLowerCase();
48
- const filteredColumns = selectableColumns.filter((column) => getColumnVisibilityLabel(column).toLowerCase().includes(normalizedQuery));
85
+ const filteredColumns = orderedSelectableColumns.filter((column) => getColumnVisibilityLabel(column).toLowerCase().includes(normalizedQuery));
49
86
  const commitVisibleColumnKeys = React.useCallback((nextVisibleColumnKeys) => {
50
- const orderedKeys = orderVisibleColumnKeys(columns, nextVisibleColumnKeys);
87
+ const orderedKeys = orderColumnKeys(columns, nextVisibleColumnKeys);
51
88
  if (orderedKeys.length < requiredVisibleColumns) {
52
89
  return;
53
90
  }
54
91
  onVisibleColumnKeysChange(orderedKeys);
55
92
  }, [columns, onVisibleColumnKeysChange, requiredVisibleColumns]);
56
93
  const toggleColumn = React.useCallback((columnKey) => {
57
- const nextVisibleColumnKeys = new Set(visibleColumnKeys);
58
- if (nextVisibleColumnKeys.has(columnKey)) {
59
- nextVisibleColumnKeys.delete(columnKey);
60
- }
61
- else {
62
- nextVisibleColumnKeys.add(columnKey);
94
+ const nextVisibleColumnKeys = currentKeys.filter((currentColumnKey) => currentColumnKey !== columnKey);
95
+ if (!visibleColumnKeySet.has(columnKey)) {
96
+ nextVisibleColumnKeys.push(columnKey);
63
97
  }
64
98
  commitVisibleColumnKeys(nextVisibleColumnKeys);
65
- }, [commitVisibleColumnKeys, visibleColumnKeys]);
99
+ }, [commitVisibleColumnKeys, currentKeys, visibleColumnKeySet]);
100
+ const handleDragEnd = React.useCallback((event) => {
101
+ const { active, over } = event;
102
+ if (!over || active.id === over.id) {
103
+ return;
104
+ }
105
+ const oldIndex = currentKeys.indexOf(String(active.id));
106
+ const newIndex = currentKeys.indexOf(String(over.id));
107
+ if (oldIndex < 0 || newIndex < 0) {
108
+ return;
109
+ }
110
+ commitVisibleColumnKeys(arrayMove(currentKeys, oldIndex, newIndex));
111
+ }, [commitVisibleColumnKeys, currentKeys]);
66
112
  const defaultTrigger = (_jsx(Button, { type: "button", variant: "outline", size: "icon", "aria-label": triggerLabel, className: triggerClassName, children: _jsx(Columns3, { "aria-hidden": "true" }) }));
67
- return (_jsx(Popover, { trigger: trigger !== null && trigger !== void 0 ? trigger : defaultTrigger, align: align, side: side, sideOffset: sideOffset, contentClassName: cn("w-80 p-0", contentClassName), children: _jsxs("div", { className: cn("flex max-h-96 flex-col", className), children: [_jsxs("div", { className: "border-b p-2", children: [_jsx("label", { htmlFor: searchInputId, className: "sr-only", children: searchPlaceholder }), _jsx(SearchInputShell, { children: _jsx(InputControl, { id: searchInputId, value: query, onChange: (event) => setQuery(event.target.value), placeholder: searchPlaceholder, className: "h-8 bg-background pl-8 pr-2" }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-1", children: filteredColumns.length ? (filteredColumns.map((column) => {
68
- const label = getColumnVisibilityLabel(column);
69
- const checked = visibleColumnKeySet.has(column.key);
70
- const disabled = checked && visibleColumnCount <= requiredVisibleColumns;
71
- return (_jsxs("label", { className: cn("flex h-9 cursor-pointer items-center gap-2 rounded-sm px-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground", disabled && "cursor-not-allowed opacity-50 hover:bg-transparent hover:text-inherit"), children: [_jsx(Checkbox, { checked: checked, disabled: disabled, onChange: () => toggleColumn(column.key), "aria-label": label }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: label })] }, column.key));
72
- })) : (_jsx("div", { className: "px-3 py-6 text-center text-sm text-muted-foreground", children: emptyMessage })) }), _jsxs(PopoverMenuFooter, { inset: "flush", className: "justify-between", children: [_jsx(PopoverMenuFooterAction, { onClick: () => commitVisibleColumnKeys(defaultKeys), disabled: !canRestoreDefaults, className: "w-auto min-w-0 flex-1", children: restoreDefaultsLabel }), _jsxs(PopoverMenuFooterMeta, { children: [visibleColumnCount, " of ", columns.length] })] })] }) }));
113
+ return (_jsx(Popover, { trigger: trigger !== null && trigger !== void 0 ? trigger : defaultTrigger, align: align, side: side, sideOffset: sideOffset, contentClassName: cn("w-80 p-0", contentClassName), children: _jsxs("div", { className: cn("flex max-h-96 flex-col", className), children: [_jsxs("div", { className: "border-b p-2", children: [_jsx("label", { htmlFor: searchInputId, className: "sr-only", children: searchPlaceholder }), _jsx(SearchInputShell, { children: _jsx(InputControl, { id: searchInputId, value: query, onChange: (event) => setQuery(event.target.value), placeholder: searchPlaceholder, className: "h-8 bg-background pl-8 pr-2" }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-1", children: filteredColumns.length ? (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: filteredColumns.map((column) => column.key), strategy: verticalListSortingStrategy, children: filteredColumns.map((column) => {
114
+ const label = getColumnVisibilityLabel(column);
115
+ const checked = visibleColumnKeySet.has(column.key);
116
+ const disabled = checked && visibleColumnCount <= requiredVisibleColumns;
117
+ return (_jsx(SortableColumnOption, { columnKey: column.key, label: label, checked: checked, disabled: disabled, onToggle: () => toggleColumn(column.key) }, column.key));
118
+ }) }) })) : (_jsx("div", { className: "px-3 py-6 text-center text-sm text-muted-foreground", children: emptyMessage })) }), _jsxs(PopoverMenuFooter, { inset: "flush", className: "justify-between", children: [_jsx(PopoverMenuFooterAction, { onClick: () => commitVisibleColumnKeys(defaultKeys), disabled: !canRestoreDefaults, className: "w-auto min-w-0 flex-1", children: restoreDefaultsLabel }), _jsxs(PopoverMenuFooterMeta, { children: [visibleColumnCount, " of ", columns.length] })] })] }) }));
73
119
  }
74
120
  export { TableColumnVisibilityMenu };
package/dist/table.d.ts CHANGED
@@ -115,7 +115,7 @@ export interface TableProps<Row> extends Omit<React.HTMLAttributes<HTMLDivElemen
115
115
  pagination?: Omit<PaginationProps, "className">;
116
116
  loading?: boolean;
117
117
  loadingRows?: number;
118
- /** Optional controlled set of visible column keys. Omit to render every column. */
118
+ /** Optional controlled ordered list of visible column keys. Omit to render every column in config order. */
119
119
  visibleColumnKeys?: readonly string[];
120
120
  /** When true, applies alternating row backgrounds for easier scanning. */
121
121
  striped?: boolean;
package/dist/table.js CHANGED
@@ -130,16 +130,30 @@ function tableRowToneClassName(tone) {
130
130
  return undefined;
131
131
  }
132
132
  }
133
+ function getVisibleColumns(columns, visibleColumnKeys) {
134
+ if (!visibleColumnKeys) {
135
+ return columns;
136
+ }
137
+ const columnsByKey = new Map(columns.map((column) => [column.key, column]));
138
+ const seenColumnKeys = new Set();
139
+ const visibleColumns = [];
140
+ visibleColumnKeys.forEach((columnKey) => {
141
+ if (seenColumnKeys.has(columnKey)) {
142
+ return;
143
+ }
144
+ const column = columnsByKey.get(columnKey);
145
+ if (column) {
146
+ visibleColumns.push(column);
147
+ seenColumnKeys.add(columnKey);
148
+ }
149
+ });
150
+ return visibleColumns;
151
+ }
133
152
  function Table({ 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, sorting, pagination, loading = false, loadingRows = 5, visibleColumnKeys, striped = false, onRowClick, style, ...props }) {
134
153
  // Use auto layout so the browser can grow a column to contain its (nowrap)
135
154
  // header. Caller can opt back into fixed layout via `tableClassName="table-fixed"`.
136
155
  const shouldUseFixedLayout = false;
137
- const visibleColumnSet = visibleColumnKeys
138
- ? new Set(visibleColumnKeys)
139
- : undefined;
140
- const visibleColumns = visibleColumnSet
141
- ? columns.filter((column) => visibleColumnSet.has(column.key))
142
- : columns;
156
+ const visibleColumns = getVisibleColumns(columns, visibleColumnKeys);
143
157
  const container = (_jsx(TableContainer, { className: cn("w-full", loading && "pointer-events-none select-none", className, containerClassName), style: {
144
158
  ...style,
145
159
  ...(maxHeight !== undefined ? { maxHeight } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neasg/design-system",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "build-storybook": "storybook build -c .storybook",
24
24
  "test": "vitest run",
25
25
  "prepublishOnly": "npm test && npm run build",
26
- "chromatic": "chromatic --storybook-build-dir storybook-static --exit-once-uploaded"
26
+ "chromatic": "chromatic --build-script-name build-storybook"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public",