@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 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 function SearchInput({ value, onValueChange, label, placeholder, className, containerClassName, disabled, ...props }: SearchInputProps): react.JSX.Element;
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-medium text-muted-foreground",
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 rowIndex % 2 === 1 ? "bg-foreground/5" : "";
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-foreground/5",
857
- "group-hover/row:before:bg-foreground/10",
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-foreground/10 data-[state=selected]:bg-accent",
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("px-3 py-2 align-middle", numericColumnClasses(column.columnDef.meta)),
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: "Loading table data\u2026" })
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: "Loading table data\u2026" })
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
- function SearchInput({
1263
+ var SearchInput = forwardRef2(function SearchInput2({
1249
1264
  value,
1250
1265
  onValueChange,
1251
- label = "Search",
1252
- placeholder = "Search\u2026",
1266
+ label,
1267
+ placeholder,
1253
1268
  className,
1254
1269
  containerClassName,
1255
1270
  disabled,
1271
+ id: idProp,
1256
1272
  ...props
1257
- }) {
1258
- const id = useId();
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: label }),
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: () => onValueChange(""),
1285
- "aria-label": "Clear search",
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 forwardRef2 } from "react";
1350
+ import { forwardRef as forwardRef3 } from "react";
1320
1351
  import {
1321
1352
  FilterChip as BaseFilterChip,
1322
- useLocale as useLocale2
1353
+ useLocale as useLocale3
1323
1354
  } from "@elabs-ai/components-ui";
1324
1355
  import { jsx as jsx4 } from "react/jsx-runtime";
1325
- var FilterChip = forwardRef2(function FilterChip2({ label, count, countLabel, ...props }, ref) {
1326
- const { formatNumber } = useLocale2();
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 forwardRef3 } from "react";
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 useLocale3
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 = forwardRef3(function FacetFilter2({ title, options, selected, onSelectedChange, className, ...props }, ref) {
1357
- const { t } = useLocale3();
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 forwardRef4 } from "react";
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 useLocale4
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 = "Columns", className, ...props }, ref) {
1428
- const { t } = useLocale4();
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: label }) }),
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 = forwardRef4(ColumnPickerInner);
1492
+ var ColumnPicker = forwardRef5(ColumnPickerInner);
1461
1493
 
1462
1494
  // src/to-csv.ts
1463
- import { downloadBlob } from "@elabs-ai/components-ui";
1464
- var INJECTION_PREFIXES = ["=", "+", "-", "@"];
1465
- function stringifyValue(value) {
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;