@elabs-ai/components-data 4.1.0 → 4.2.0
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/index.d.ts +7 -2
- package/dist/index.js +71 -53
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/__contract__/filter-chip.contract.test.tsx +49 -0
- package/src/column-picker/column-picker.tsx +3 -2
- package/src/data-table/data-table.stories.tsx +15 -0
- package/src/data-table/data-table.test.tsx +61 -11
- package/src/data-table/data-table.tsx +54 -15
- package/src/facet-filter/facet-filter.stories.tsx +4 -1
- package/src/facet-filter/facet-filter.test.tsx +3 -3
- package/src/search-input/search-input.test.tsx +35 -0
- package/src/search-input/search-input.tsx +44 -19
- package/src/to-csv.test.ts +13 -0
- package/src/to-csv.ts +7 -30
package/dist/index.d.ts
CHANGED
|
@@ -266,6 +266,11 @@ interface DataTableProps<TData, TValue> extends Omit<React.HTMLAttributes<HTMLDi
|
|
|
266
266
|
* (a `border-border-strong` divider between rows, no stripes).
|
|
267
267
|
*/
|
|
268
268
|
zebra?: boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Draw a quiet `--rule` hairline between columns (header and body). Off by
|
|
271
|
+
* default. Pinned cells keep their own seam and never take a divider.
|
|
272
|
+
*/
|
|
273
|
+
columnDividers?: boolean;
|
|
269
274
|
/**
|
|
270
275
|
* Opt-in row drag-reorder. Off by default — an existing table renders
|
|
271
276
|
* byte-identical markup with no extra DOM per row until this is set.
|
|
@@ -366,7 +371,7 @@ declare const DataTableWithRef: <TData, TValue>(props: DataTableProps<TData, TVa
|
|
|
366
371
|
interface SearchInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value"> {
|
|
367
372
|
value: string;
|
|
368
373
|
onValueChange: (value: string) => void;
|
|
369
|
-
/** Visually-hidden accessible label. Defaults to "Search". */
|
|
374
|
+
/** Visually-hidden accessible label. Defaults to the localized "Search" microcopy. */
|
|
370
375
|
label?: string;
|
|
371
376
|
containerClassName?: string;
|
|
372
377
|
}
|
|
@@ -379,7 +384,7 @@ interface SearchInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "
|
|
|
379
384
|
* `<Input>` explicitly AND gates the clear button — while disabled the clear
|
|
380
385
|
* affordance is hidden so it can't mutate the filter mid-request (#269/#8).
|
|
381
386
|
*/
|
|
382
|
-
declare
|
|
387
|
+
declare const SearchInput: react.ForwardRefExoticComponent<SearchInputProps & react.RefAttributes<HTMLInputElement>>;
|
|
383
388
|
|
|
384
389
|
interface FilterBarProps {
|
|
385
390
|
/** Left cluster: search + facet filters. */
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,7 @@ function isActiveTextSelection() {
|
|
|
51
51
|
return window.getSelection()?.type === "Range";
|
|
52
52
|
}
|
|
53
53
|
var PINNED_SEAM_CLASS = "after:pointer-events-none after:absolute after:inset-y-0 after:w-px after:bg-border-strong after:content-['']";
|
|
54
|
+
var COLUMN_DIVIDER_CLASS = "border-e border-rule last:border-e-0";
|
|
54
55
|
function unsizedColumnIds(defs) {
|
|
55
56
|
const out = /* @__PURE__ */ new Set();
|
|
56
57
|
const walk = (list) => {
|
|
@@ -189,6 +190,7 @@ function DataTableInner({
|
|
|
189
190
|
overscan = 8,
|
|
190
191
|
maxBodyHeight = "32rem",
|
|
191
192
|
zebra = true,
|
|
193
|
+
columnDividers = false,
|
|
192
194
|
// Row drag-reorder (#13)
|
|
193
195
|
enableRowReorder = false,
|
|
194
196
|
onRowReorder,
|
|
@@ -702,7 +704,7 @@ function DataTableInner({
|
|
|
702
704
|
// must stay byte-identical to the body's so an
|
|
703
705
|
// end-aligned numeric column's header lines up with its
|
|
704
706
|
// own values.
|
|
705
|
-
"h-10 px-3 text-start align-middle font-
|
|
707
|
+
"h-10 px-3 text-start align-middle font-table-header text-muted-foreground",
|
|
706
708
|
// #69: a numeric column's `meta` overrides the default
|
|
707
709
|
// `text-start` — placed right after the base string so
|
|
708
710
|
// tailwind-merge lets it win over that default.
|
|
@@ -733,7 +735,8 @@ function DataTableInner({
|
|
|
733
735
|
// structural cue between the frozen and scrolling blocks, so
|
|
734
736
|
// it must not read as a "boundary + fill in one class string"
|
|
735
737
|
// redundancy (separation:check).
|
|
736
|
-
geometry?.edgeClass
|
|
738
|
+
geometry?.edgeClass,
|
|
739
|
+
columnDividers && !geometry && COLUMN_DIVIDER_CLASS
|
|
737
740
|
),
|
|
738
741
|
children: [
|
|
739
742
|
header.isPlaceholder ? null : canSort ? /* @__PURE__ */ jsxs(
|
|
@@ -847,14 +850,19 @@ function DataTableInner({
|
|
|
847
850
|
}
|
|
848
851
|
function rowSeparationClass(rowIndex) {
|
|
849
852
|
if (!zebra) return "border-b border-border-strong last:border-b-0";
|
|
850
|
-
return
|
|
853
|
+
return cn(
|
|
854
|
+
"border-b-(length:--table-row-rule-width) border-border-strong last:border-b-0",
|
|
855
|
+
// Separate cn() argument: the stripe and the (theme-gated) rule are
|
|
856
|
+
// alternative cues, never both at once — see the jsdoc above.
|
|
857
|
+
rowIndex % 2 === 1 && "bg-table-stripe"
|
|
858
|
+
);
|
|
851
859
|
}
|
|
852
860
|
function pinnedCellFillClass(rowIndex) {
|
|
853
861
|
return cn(
|
|
854
862
|
"bg-card",
|
|
855
863
|
"before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:content-['']",
|
|
856
|
-
zebra && rowIndex % 2 === 1 && "before:bg-
|
|
857
|
-
"group-hover/row:before:bg-
|
|
864
|
+
zebra && rowIndex % 2 === 1 && "before:bg-table-stripe",
|
|
865
|
+
"group-hover/row:before:bg-table-row-hover",
|
|
858
866
|
"group-data-[state=selected]/row:before:bg-accent"
|
|
859
867
|
);
|
|
860
868
|
}
|
|
@@ -883,7 +891,7 @@ function DataTableInner({
|
|
|
883
891
|
// (only movement is neutralized); the gated duration-fast/ease-standard
|
|
884
892
|
// pair already collapses toward ~0ms via --motion-factor when the user
|
|
885
893
|
// or OS asks for reduced motion, matching the header sort button.
|
|
886
|
-
"transition-colors duration-fast ease-standard hover:bg-
|
|
894
|
+
"transition-colors duration-fast ease-standard hover:bg-table-row-hover data-[state=selected]:bg-accent",
|
|
887
895
|
// #13: the dragged row's live `transform` (set inline via `extras.style`,
|
|
888
896
|
// see `SortableDataRow`) is what actually MOVES it — this class only
|
|
889
897
|
// makes that movement glide instead of snapping, through the gated
|
|
@@ -944,7 +952,8 @@ function DataTableInner({
|
|
|
944
952
|
geometry && "sticky z-10",
|
|
945
953
|
geometry && pinnedCellFillClass(rowIndex),
|
|
946
954
|
// Separate cn() argument — see pinnedCellGeometry's edgeClass.
|
|
947
|
-
geometry?.edgeClass
|
|
955
|
+
geometry?.edgeClass,
|
|
956
|
+
columnDividers && !geometry && COLUMN_DIVIDER_CLASS
|
|
948
957
|
),
|
|
949
958
|
children: [
|
|
950
959
|
clickable && cellIndex === 0 && /* @__PURE__ */ jsx(
|
|
@@ -975,7 +984,11 @@ function DataTableInner({
|
|
|
975
984
|
visibleColumns.map((column) => /* @__PURE__ */ jsx(
|
|
976
985
|
"td",
|
|
977
986
|
{
|
|
978
|
-
className: cn(
|
|
987
|
+
className: cn(
|
|
988
|
+
"px-3 py-2 align-middle",
|
|
989
|
+
numericColumnClasses(column.columnDef.meta),
|
|
990
|
+
columnDividers && COLUMN_DIVIDER_CLASS
|
|
991
|
+
),
|
|
979
992
|
children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" })
|
|
980
993
|
},
|
|
981
994
|
column.id
|
|
@@ -1111,6 +1124,7 @@ function DataTableInner({
|
|
|
1111
1124
|
{
|
|
1112
1125
|
ref: scrollRef,
|
|
1113
1126
|
tabIndex: 0,
|
|
1127
|
+
role: "group",
|
|
1114
1128
|
"aria-label": t("data.table.scrollRegion"),
|
|
1115
1129
|
"aria-busy": loading || void 0,
|
|
1116
1130
|
className: "relative overflow-auto rounded-lg border bg-card focus-ring",
|
|
@@ -1124,7 +1138,7 @@ function DataTableInner({
|
|
|
1124
1138
|
className: "absolute inset-0 z-40 flex items-center justify-center rounded-lg bg-card/80",
|
|
1125
1139
|
children: [
|
|
1126
1140
|
/* @__PURE__ */ jsx(Spinner, { "aria-hidden": "true", className: "text-foreground" }),
|
|
1127
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "
|
|
1141
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t("data.table.loading") })
|
|
1128
1142
|
]
|
|
1129
1143
|
}
|
|
1130
1144
|
),
|
|
@@ -1162,7 +1176,7 @@ function DataTableInner({
|
|
|
1162
1176
|
className: "absolute inset-0 z-40 flex items-center justify-center rounded-lg bg-card/80",
|
|
1163
1177
|
children: [
|
|
1164
1178
|
/* @__PURE__ */ jsx(Spinner, { "aria-hidden": "true", className: "text-foreground" }),
|
|
1165
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "
|
|
1179
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t("data.table.loading") })
|
|
1166
1180
|
]
|
|
1167
1181
|
}
|
|
1168
1182
|
),
|
|
@@ -1172,6 +1186,7 @@ function DataTableInner({
|
|
|
1172
1186
|
ref: plainScrollRef,
|
|
1173
1187
|
"data-slot": "data-table-scroll-region",
|
|
1174
1188
|
tabIndex: scrollOverflows ? 0 : void 0,
|
|
1189
|
+
role: scrollOverflows ? "group" : void 0,
|
|
1175
1190
|
"aria-label": scrollOverflows ? t("data.table.scrollRegion") : void 0,
|
|
1176
1191
|
onScroll: updateScrollAffordance,
|
|
1177
1192
|
className: "overflow-auto rounded-lg focus-ring-inset",
|
|
@@ -1240,24 +1255,39 @@ function DataTableInner({
|
|
|
1240
1255
|
var DataTableWithRef = forwardRef(DataTableInner);
|
|
1241
1256
|
|
|
1242
1257
|
// src/search-input/search-input.tsx
|
|
1243
|
-
import { useId } from "react";
|
|
1244
|
-
import { Input } from "@elabs-ai/components-ui";
|
|
1258
|
+
import { forwardRef as forwardRef2, useId, useRef as useRef2 } from "react";
|
|
1259
|
+
import { Input, useLocale as useLocale2 } from "@elabs-ai/components-ui";
|
|
1245
1260
|
import { cn as cn2 } from "@elabs-ai/components-ui/lib/cn";
|
|
1246
1261
|
import { SearchIcon } from "@elabs-ai/components-icons";
|
|
1247
1262
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1248
|
-
|
|
1263
|
+
var SearchInput = forwardRef2(function SearchInput2({
|
|
1249
1264
|
value,
|
|
1250
1265
|
onValueChange,
|
|
1251
|
-
label
|
|
1252
|
-
placeholder
|
|
1266
|
+
label,
|
|
1267
|
+
placeholder,
|
|
1253
1268
|
className,
|
|
1254
1269
|
containerClassName,
|
|
1255
1270
|
disabled,
|
|
1271
|
+
id: idProp,
|
|
1256
1272
|
...props
|
|
1257
|
-
}) {
|
|
1258
|
-
const
|
|
1273
|
+
}, forwardedRef) {
|
|
1274
|
+
const { t } = useLocale2();
|
|
1275
|
+
const resolvedLabel = label ?? t("data.searchInput.label");
|
|
1276
|
+
const resolvedPlaceholder = placeholder ?? t("data.searchInput.placeholder");
|
|
1277
|
+
const generatedId = useId();
|
|
1278
|
+
const id = idProp ?? generatedId;
|
|
1279
|
+
const inputRef = useRef2(null);
|
|
1280
|
+
const setRefs = (node) => {
|
|
1281
|
+
inputRef.current = node;
|
|
1282
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
1283
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
1284
|
+
};
|
|
1285
|
+
const handleClear = () => {
|
|
1286
|
+
onValueChange("");
|
|
1287
|
+
inputRef.current?.focus();
|
|
1288
|
+
};
|
|
1259
1289
|
return /* @__PURE__ */ jsxs2("div", { className: cn2("relative w-full max-w-xs", containerClassName), children: [
|
|
1260
|
-
/* @__PURE__ */ jsx2("label", { htmlFor: id, className: "sr-only", children:
|
|
1290
|
+
/* @__PURE__ */ jsx2("label", { htmlFor: id, className: "sr-only", children: resolvedLabel }),
|
|
1261
1291
|
/* @__PURE__ */ jsx2(
|
|
1262
1292
|
SearchIcon,
|
|
1263
1293
|
{
|
|
@@ -1268,10 +1298,11 @@ function SearchInput({
|
|
|
1268
1298
|
/* @__PURE__ */ jsx2(
|
|
1269
1299
|
Input,
|
|
1270
1300
|
{
|
|
1301
|
+
ref: setRefs,
|
|
1271
1302
|
id,
|
|
1272
1303
|
value,
|
|
1273
1304
|
onChange: (e) => onValueChange(e.target.value),
|
|
1274
|
-
placeholder,
|
|
1305
|
+
placeholder: resolvedPlaceholder,
|
|
1275
1306
|
disabled,
|
|
1276
1307
|
className: cn2("ps-8", value && "pe-8", className),
|
|
1277
1308
|
...props
|
|
@@ -1281,8 +1312,8 @@ function SearchInput({
|
|
|
1281
1312
|
"button",
|
|
1282
1313
|
{
|
|
1283
1314
|
type: "button",
|
|
1284
|
-
onClick:
|
|
1285
|
-
"aria-label": "
|
|
1315
|
+
onClick: handleClear,
|
|
1316
|
+
"aria-label": t("data.searchInput.clear"),
|
|
1286
1317
|
className: "absolute end-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-fast ease-standard hover:text-foreground focus-ring animate-in fade-in zoom-in-95 duration-fast ease-entrance",
|
|
1287
1318
|
children: /* @__PURE__ */ jsx2(
|
|
1288
1319
|
"svg",
|
|
@@ -1302,7 +1333,7 @@ function SearchInput({
|
|
|
1302
1333
|
}
|
|
1303
1334
|
) : null
|
|
1304
1335
|
] });
|
|
1305
|
-
}
|
|
1336
|
+
});
|
|
1306
1337
|
|
|
1307
1338
|
// src/filter-bar/filter-bar.tsx
|
|
1308
1339
|
import "react";
|
|
@@ -1316,14 +1347,14 @@ function FilterBar({ children, actions, className }) {
|
|
|
1316
1347
|
}
|
|
1317
1348
|
|
|
1318
1349
|
// src/filter-bar/filter-chip.tsx
|
|
1319
|
-
import { forwardRef as
|
|
1350
|
+
import { forwardRef as forwardRef3 } from "react";
|
|
1320
1351
|
import {
|
|
1321
1352
|
FilterChip as BaseFilterChip,
|
|
1322
|
-
useLocale as
|
|
1353
|
+
useLocale as useLocale3
|
|
1323
1354
|
} from "@elabs-ai/components-ui";
|
|
1324
1355
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1325
|
-
var FilterChip =
|
|
1326
|
-
const { formatNumber } =
|
|
1356
|
+
var FilterChip = forwardRef3(function FilterChip2({ label, count, countLabel, ...props }, ref) {
|
|
1357
|
+
const { formatNumber } = useLocale3();
|
|
1327
1358
|
const countText = count === void 0 ? void 0 : countLabel ? `${countLabel} ${formatNumber(count)}` : formatNumber(count);
|
|
1328
1359
|
const { trailing: _ignoredTrailing, ...restProps } = props;
|
|
1329
1360
|
return /* @__PURE__ */ jsx4(
|
|
@@ -1339,7 +1370,7 @@ var FilterChip = forwardRef2(function FilterChip2({ label, count, countLabel, ..
|
|
|
1339
1370
|
});
|
|
1340
1371
|
|
|
1341
1372
|
// src/facet-filter/facet-filter.tsx
|
|
1342
|
-
import { forwardRef as
|
|
1373
|
+
import { forwardRef as forwardRef4 } from "react";
|
|
1343
1374
|
import {
|
|
1344
1375
|
Badge,
|
|
1345
1376
|
Button as Button2,
|
|
@@ -1349,12 +1380,12 @@ import {
|
|
|
1349
1380
|
DropdownMenuLabel,
|
|
1350
1381
|
DropdownMenuSeparator,
|
|
1351
1382
|
DropdownMenuTrigger,
|
|
1352
|
-
useLocale as
|
|
1383
|
+
useLocale as useLocale4
|
|
1353
1384
|
} from "@elabs-ai/components-ui";
|
|
1354
1385
|
import { cn as cn4 } from "@elabs-ai/components-ui/lib/cn";
|
|
1355
1386
|
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1356
|
-
var FacetFilter =
|
|
1357
|
-
const { t } =
|
|
1387
|
+
var FacetFilter = forwardRef4(function FacetFilter2({ title, options, selected, onSelectedChange, className, ...props }, ref) {
|
|
1388
|
+
const { t } = useLocale4();
|
|
1358
1389
|
const selectedSet = new Set(selected);
|
|
1359
1390
|
const toggle = (value) => {
|
|
1360
1391
|
const next = new Set(selectedSet);
|
|
@@ -1411,7 +1442,7 @@ FacetFilter.displayName = "FacetFilter";
|
|
|
1411
1442
|
|
|
1412
1443
|
// src/column-picker/column-picker.tsx
|
|
1413
1444
|
import "@tanstack/react-table";
|
|
1414
|
-
import { forwardRef as
|
|
1445
|
+
import { forwardRef as forwardRef5 } from "react";
|
|
1415
1446
|
import {
|
|
1416
1447
|
Button as Button3,
|
|
1417
1448
|
DropdownMenu as DropdownMenu2,
|
|
@@ -1420,15 +1451,16 @@ import {
|
|
|
1420
1451
|
DropdownMenuLabel as DropdownMenuLabel2,
|
|
1421
1452
|
DropdownMenuSeparator as DropdownMenuSeparator2,
|
|
1422
1453
|
DropdownMenuTrigger as DropdownMenuTrigger2,
|
|
1423
|
-
useLocale as
|
|
1454
|
+
useLocale as useLocale5
|
|
1424
1455
|
} from "@elabs-ai/components-ui";
|
|
1425
1456
|
import { cn as cn5 } from "@elabs-ai/components-ui/lib/cn";
|
|
1426
1457
|
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1427
|
-
function ColumnPickerInner({ table, label
|
|
1428
|
-
const { t } =
|
|
1458
|
+
function ColumnPickerInner({ table, label, className, ...props }, ref) {
|
|
1459
|
+
const { t } = useLocale5();
|
|
1460
|
+
const resolvedLabel = label ?? t("data.columnPicker.label");
|
|
1429
1461
|
const columns = table.getAllColumns().filter((c) => c.getCanHide());
|
|
1430
1462
|
return /* @__PURE__ */ jsxs5(DropdownMenu2, { children: [
|
|
1431
|
-
/* @__PURE__ */ jsx6(DropdownMenuTrigger2, { asChild: true, children: /* @__PURE__ */ jsx6(Button3, { ref, variant: "outline", size: "sm", className: cn5(className), ...props, children:
|
|
1463
|
+
/* @__PURE__ */ jsx6(DropdownMenuTrigger2, { asChild: true, children: /* @__PURE__ */ jsx6(Button3, { ref, variant: "outline", size: "sm", className: cn5(className), ...props, children: resolvedLabel }) }),
|
|
1432
1464
|
/* @__PURE__ */ jsxs5(DropdownMenuContent2, { align: "end", className: "min-w-[12rem]", children: [
|
|
1433
1465
|
/* @__PURE__ */ jsx6(DropdownMenuLabel2, { children: t("data.columnPicker.toggleColumns") }),
|
|
1434
1466
|
/* @__PURE__ */ jsx6(DropdownMenuSeparator2, {}),
|
|
@@ -1457,26 +1489,12 @@ function ColumnPickerInner({ table, label = "Columns", className, ...props }, re
|
|
|
1457
1489
|
] });
|
|
1458
1490
|
}
|
|
1459
1491
|
ColumnPickerInner.displayName = "ColumnPicker";
|
|
1460
|
-
var ColumnPicker =
|
|
1492
|
+
var ColumnPicker = forwardRef5(ColumnPickerInner);
|
|
1461
1493
|
|
|
1462
1494
|
// src/to-csv.ts
|
|
1463
|
-
import { downloadBlob } from "@elabs-ai/components-ui";
|
|
1464
|
-
var
|
|
1465
|
-
|
|
1466
|
-
if (value === null || value === void 0) return "";
|
|
1467
|
-
if (value instanceof Date) return value.toISOString();
|
|
1468
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
1469
|
-
return String(value);
|
|
1470
|
-
}
|
|
1471
|
-
function quoteField(field, delimiter) {
|
|
1472
|
-
if (INJECTION_PREFIXES.some((p) => field.startsWith(p))) {
|
|
1473
|
-
field = "'" + field;
|
|
1474
|
-
}
|
|
1475
|
-
if (field.includes(delimiter) || field.includes('"') || field.includes("\n") || field.includes("\r")) {
|
|
1476
|
-
return '"' + field.replaceAll('"', '""') + '"';
|
|
1477
|
-
}
|
|
1478
|
-
return field;
|
|
1479
|
-
}
|
|
1495
|
+
import { csvQuoteField, csvStringifyValue, downloadBlob } from "@elabs-ai/components-ui";
|
|
1496
|
+
var stringifyValue = csvStringifyValue;
|
|
1497
|
+
var quoteField = csvQuoteField;
|
|
1480
1498
|
function toCsv(rows, opts) {
|
|
1481
1499
|
const delimiter = opts?.delimiter ?? ",";
|
|
1482
1500
|
const includeHeader = opts?.header !== false;
|