@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 +2 -2
- package/dist/date-input.d.ts +2 -0
- package/dist/date-input.js +44 -4
- package/dist/filter-popover.d.ts +2 -1
- package/dist/filter-popover.js +2 -2
- package/dist/table-column-visibility.d.ts +1 -0
- package/dist/table-column-visibility.js +71 -25
- package/dist/table.d.ts +1 -1
- package/dist/table.js +20 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# NEA Design System
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@neasg/design-system)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Shared NEA UI primitives, theme tokens, and Storybook documentation.
|
|
6
6
|
|
|
7
7
|
## Local Development
|
|
8
8
|
|
package/dist/date-input.d.ts
CHANGED
package/dist/date-input.js
CHANGED
|
@@ -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 (
|
|
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:
|
|
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:
|
|
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 };
|
package/dist/filter-popover.d.ts
CHANGED
|
@@ -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 };
|
package/dist/filter-popover.js
CHANGED
|
@@ -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 {
|
|
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
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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(() =>
|
|
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(() =>
|
|
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 =
|
|
85
|
+
const filteredColumns = orderedSelectableColumns.filter((column) => getColumnVisibilityLabel(column).toLowerCase().includes(normalizedQuery));
|
|
49
86
|
const commitVisibleColumnKeys = React.useCallback((nextVisibleColumnKeys) => {
|
|
50
|
-
const orderedKeys =
|
|
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 =
|
|
58
|
-
if (
|
|
59
|
-
nextVisibleColumnKeys.
|
|
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,
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 --
|
|
26
|
+
"chromatic": "chromatic --build-script-name build-storybook"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public",
|