@geomak/ui 7.0.1 → 7.1.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.cjs CHANGED
@@ -5310,22 +5310,78 @@ function createDatasets(rows, perPage) {
5310
5310
  }
5311
5311
  var defaultGetRowKey = (_row, index) => index;
5312
5312
  var cellAlign = (align) => align === "left" ? "text-left" : align === "right" ? "text-right" : "text-center";
5313
+ function compareValues(a, b) {
5314
+ if (a == null && b == null) return 0;
5315
+ if (a == null) return -1;
5316
+ if (b == null) return 1;
5317
+ if (typeof a === "number" && typeof b === "number") return a - b;
5318
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
5319
+ if (typeof a === "boolean" && typeof b === "boolean") return a === b ? 0 : a ? 1 : -1;
5320
+ return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
5321
+ }
5322
+ function SortGlyph({ direction }) {
5323
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", width: "14", height: "14", fill: "none", "aria-hidden": "true", className: "shrink-0", children: [
5324
+ /* @__PURE__ */ jsxRuntime.jsx(
5325
+ "path",
5326
+ {
5327
+ d: "M8 11l4-4 4 4",
5328
+ stroke: "currentColor",
5329
+ strokeWidth: 2,
5330
+ strokeLinecap: "round",
5331
+ strokeLinejoin: "round",
5332
+ className: direction === "asc" ? "text-accent" : "text-foreground-muted",
5333
+ opacity: direction === "asc" ? 1 : 0.45
5334
+ }
5335
+ ),
5336
+ /* @__PURE__ */ jsxRuntime.jsx(
5337
+ "path",
5338
+ {
5339
+ d: "M8 13l4 4 4-4",
5340
+ stroke: "currentColor",
5341
+ strokeWidth: 2,
5342
+ strokeLinecap: "round",
5343
+ strokeLinejoin: "round",
5344
+ className: direction === "desc" ? "text-accent" : "text-foreground-muted",
5345
+ opacity: direction === "desc" ? 1 : 0.45
5346
+ }
5347
+ )
5348
+ ] });
5349
+ }
5313
5350
  function TableHeader({
5314
5351
  columns,
5315
- hasExpand
5352
+ hasExpand,
5353
+ sort,
5354
+ onSort
5316
5355
  }) {
5317
5356
  return /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-surface-raised border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
5318
5357
  hasExpand && /* @__PURE__ */ jsxRuntime.jsx("th", { "aria-hidden": "true", className: "w-9" }),
5319
- columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(
5320
- "th",
5321
- {
5322
- scope: "col",
5323
- className: `${cellAlign(col.align)} text-sm font-semibold text-foreground py-3 px-3`,
5324
- style: col.width != null ? { width: col.width } : void 0,
5325
- children: col.label
5326
- },
5327
- col.key
5328
- ))
5358
+ columns.map((col) => {
5359
+ const active = sort?.key === col.keyBind;
5360
+ const dir = active ? sort.direction : void 0;
5361
+ const justify = col.align === "left" ? "justify-start" : col.align === "right" ? "justify-end" : "justify-center";
5362
+ return /* @__PURE__ */ jsxRuntime.jsx(
5363
+ "th",
5364
+ {
5365
+ scope: "col",
5366
+ "aria-sort": col.sortable ? active ? dir === "asc" ? "ascending" : "descending" : "none" : void 0,
5367
+ className: `${cellAlign(col.align)} text-sm font-semibold text-foreground py-3 px-3`,
5368
+ style: col.width != null ? { width: col.width } : void 0,
5369
+ children: col.sortable ? /* @__PURE__ */ jsxRuntime.jsxs(
5370
+ "button",
5371
+ {
5372
+ type: "button",
5373
+ onClick: () => onSort(col),
5374
+ className: `inline-flex items-center gap-1.5 ${justify} w-full select-none rounded transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${active ? "text-accent" : "hover:text-accent"}`,
5375
+ children: [
5376
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: col.label }),
5377
+ /* @__PURE__ */ jsxRuntime.jsx(SortGlyph, { direction: dir })
5378
+ ]
5379
+ }
5380
+ ) : col.label
5381
+ },
5382
+ col.key
5383
+ );
5384
+ })
5329
5385
  ] }) });
5330
5386
  }
5331
5387
  var DefaultExpandIcon = /* @__PURE__ */ jsxRuntime.jsx(
@@ -5333,24 +5389,66 @@ var DefaultExpandIcon = /* @__PURE__ */ jsxRuntime.jsx(
5333
5389
  {
5334
5390
  xmlns: "http://www.w3.org/2000/svg",
5335
5391
  viewBox: "0 0 24 24",
5336
- fill: "currentColor",
5337
- className: "w-5 h-5 text-foreground-muted",
5392
+ fill: "none",
5393
+ stroke: "currentColor",
5394
+ strokeWidth: 2,
5395
+ className: "w-4 h-4 text-foreground-muted",
5338
5396
  "aria-hidden": "true",
5339
- children: /* @__PURE__ */ jsxRuntime.jsx(
5340
- "path",
5397
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19.5 8.25l-7.5 7.5-7.5-7.5" })
5398
+ }
5399
+ );
5400
+ function EditableCell({
5401
+ col,
5402
+ row,
5403
+ rowIndex,
5404
+ onCellEdit
5405
+ }) {
5406
+ const [editing, setEditing] = React28.useState(false);
5407
+ const value = row[col.keyBind];
5408
+ const commit = (next) => {
5409
+ setEditing(false);
5410
+ onCellEdit?.({ row, key: col.keyBind, value: next, rowIndex });
5411
+ };
5412
+ const cancel = () => setEditing(false);
5413
+ if (editing) {
5414
+ if (col.editor) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: col.editor({ value, row, commit, cancel }) });
5415
+ return /* @__PURE__ */ jsxRuntime.jsx(
5416
+ "input",
5341
5417
  {
5342
- fillRule: "evenodd",
5343
- d: "M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zM12.75 9a.75.75 0 00-1.5 0v2.25H9a.75.75 0 000 1.5h2.25V15a.75.75 0 001.5 0v-2.25H15a.75.75 0 000-1.5h-2.25V9z",
5344
- clipRule: "evenodd"
5418
+ autoFocus: true,
5419
+ defaultValue: value == null ? "" : String(value),
5420
+ onBlur: (e) => commit(e.target.value),
5421
+ onKeyDown: (e) => {
5422
+ if (e.key === "Enter") {
5423
+ e.preventDefault();
5424
+ commit(e.target.value);
5425
+ } else if (e.key === "Escape") {
5426
+ e.preventDefault();
5427
+ cancel();
5428
+ }
5429
+ },
5430
+ "aria-label": `Edit ${typeof col.label === "string" ? col.label : col.keyBind}`,
5431
+ className: "w-full rounded border border-accent bg-surface px-2 py-1 text-sm text-foreground outline-none"
5345
5432
  }
5346
- )
5433
+ );
5347
5434
  }
5348
- );
5435
+ return /* @__PURE__ */ jsxRuntime.jsx(
5436
+ "button",
5437
+ {
5438
+ type: "button",
5439
+ onClick: () => setEditing(true),
5440
+ title: "Click to edit",
5441
+ className: `${cellAlign(col.align)} w-full cursor-text rounded px-1 py-0.5 hover:bg-background focus:outline-none focus-visible:ring-2 focus-visible:ring-accent`,
5442
+ children: col.component ? col.component(value, row) : value
5443
+ }
5444
+ );
5445
+ }
5349
5446
  function TableBody({
5350
5447
  columns,
5351
5448
  rows,
5352
5449
  expandRow,
5353
- getRowKey
5450
+ getRowKey,
5451
+ onCellEdit
5354
5452
  }) {
5355
5453
  const [expanded, setExpanded] = React28.useState(() => /* @__PURE__ */ new Set());
5356
5454
  const reduced = framerMotion.useReducedMotion();
@@ -5380,15 +5478,15 @@ function TableBody({
5380
5478
  onClick: () => toggleRow(rowKey),
5381
5479
  "aria-expanded": isExpanded,
5382
5480
  "aria-label": isExpanded ? "Collapse row" : "Expand row",
5383
- className: `w-9 h-9 inline-flex items-center justify-center rounded-md hover:bg-surface/80 transition-transform duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${isExpanded ? "rotate-180" : ""}`,
5384
- children: expandRow.expandIcon ?? DefaultExpandIcon
5481
+ className: `w-9 h-9 inline-flex items-center justify-center rounded-md hover:bg-background transition-transform duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${isExpanded && !expandRow.collapseIcon ? "rotate-180" : ""}`,
5482
+ children: isExpanded ? expandRow.collapseIcon ?? expandRow.expandIcon ?? DefaultExpandIcon : expandRow.expandIcon ?? DefaultExpandIcon
5385
5483
  }
5386
5484
  ) }),
5387
5485
  columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(
5388
5486
  "td",
5389
5487
  {
5390
5488
  className: `${cellAlign(col.align)} text-sm text-foreground py-2 px-3 align-middle`,
5391
- children: col.component ? col.component(row[col.keyBind], row) : row[col.keyBind]
5489
+ children: col.editable ? /* @__PURE__ */ jsxRuntime.jsx(EditableCell, { col, row, rowIndex: i, onCellEdit }) : col.component ? col.component(row[col.keyBind], row) : row[col.keyBind]
5392
5490
  },
5393
5491
  col.key
5394
5492
  ))
@@ -5482,6 +5580,10 @@ function Table({
5482
5580
  pagination = DEFAULT_PAGINATION,
5483
5581
  expandRow = DEFAULT_EXPAND,
5484
5582
  hasSearch = true,
5583
+ search,
5584
+ defaultSort = null,
5585
+ onSortChange,
5586
+ onCellEdit,
5485
5587
  footer = null,
5486
5588
  header = null,
5487
5589
  loading = false,
@@ -5495,20 +5597,54 @@ function Table({
5495
5597
  typeof pagination.perPage === "number" ? pagination.perPage : 15
5496
5598
  );
5497
5599
  const [activePage, setActivePage] = React28.useState(0);
5600
+ const [sortState, setSortState] = React28.useState(defaultSort);
5498
5601
  const isServerSide = !!(pagination.enabled && pagination.serverSide);
5602
+ const handleSort = (col) => {
5603
+ const key = col.keyBind;
5604
+ let next;
5605
+ if (!sortState || sortState.key !== key) next = { key, direction: "asc" };
5606
+ else if (sortState.direction === "asc") next = { key, direction: "desc" };
5607
+ else next = null;
5608
+ setSortState(next);
5609
+ onSortChange?.(next);
5610
+ };
5611
+ const debounceMs = search?.debounceMs ?? 0;
5612
+ const [debouncedTerm, setDebouncedTerm] = React28.useState("");
5613
+ React28.useEffect(() => {
5614
+ if (debounceMs <= 0) {
5615
+ setDebouncedTerm(searchTerm);
5616
+ return;
5617
+ }
5618
+ const t = setTimeout(() => setDebouncedTerm(searchTerm), debounceMs);
5619
+ return () => clearTimeout(t);
5620
+ }, [searchTerm, debounceMs]);
5621
+ const term = debounceMs > 0 ? debouncedTerm : searchTerm;
5499
5622
  const filteredRows = React28.useMemo(() => {
5500
- if (isServerSide || !searchTerm) return rows;
5501
- const term = searchTerm.toLowerCase();
5502
- return rows.filter(
5503
- (row) => Object.values(row).some(
5504
- (v) => v != null && String(v).toLowerCase().includes(term)
5505
- )
5506
- );
5507
- }, [rows, searchTerm, isServerSide]);
5623
+ if (isServerSide || !term) return rows;
5624
+ if (search?.predicate) return rows.filter((row) => search.predicate(row, term));
5625
+ const cs = !!search?.caseSensitive;
5626
+ const needle = cs ? term : term.toLowerCase();
5627
+ const mode = search?.matchMode ?? "contains";
5628
+ const keys = search?.keys;
5629
+ const test = (raw) => {
5630
+ if (raw == null) return false;
5631
+ const s = cs ? String(raw) : String(raw).toLowerCase();
5632
+ return mode === "startsWith" ? s.startsWith(needle) : mode === "equals" ? s === needle : s.includes(needle);
5633
+ };
5634
+ return rows.filter((row) => keys ? keys.some((k) => test(row[k])) : Object.values(row).some(test));
5635
+ }, [rows, term, isServerSide, search?.predicate, search?.caseSensitive, search?.matchMode, search?.keys]);
5636
+ const sortedRows = React28.useMemo(() => {
5637
+ if (isServerSide || !sortState) return filteredRows;
5638
+ const col = columns.find((c) => c.keyBind === sortState.key);
5639
+ const accessor = col?.sortAccessor ?? ((r) => r[sortState.key]);
5640
+ const out = [...filteredRows].sort((a, b) => compareValues(accessor(a), accessor(b)));
5641
+ if (sortState.direction === "desc") out.reverse();
5642
+ return out;
5643
+ }, [filteredRows, sortState, isServerSide, columns]);
5508
5644
  const datasets = React28.useMemo(() => {
5509
5645
  if (isServerSide) return [rows];
5510
- return createDatasets(filteredRows, pagination.enabled ? perPage : null);
5511
- }, [filteredRows, perPage, pagination.enabled, isServerSide, rows]);
5646
+ return createDatasets(sortedRows, pagination.enabled ? perPage : null);
5647
+ }, [sortedRows, perPage, pagination.enabled, isServerSide, rows]);
5512
5648
  const MAX_PAGE = React28.useMemo(() => {
5513
5649
  if (isServerSide && typeof pagination.maxPage === "number") return Math.max(0, pagination.maxPage);
5514
5650
  if (isServerSide && typeof pagination.totalCount === "number")
@@ -5547,32 +5683,36 @@ function Table({
5547
5683
  }
5548
5684
  setActivePage(newPage);
5549
5685
  };
5686
+ const pagPos = pagination.position ?? "top";
5687
+ const showTopPager = !!pagination.enabled && (pagPos === "top" || pagPos === "both");
5688
+ const showBottomPager = !!pagination.enabled && (pagPos === "bottom" || pagPos === "both");
5689
+ const pager = /* @__PURE__ */ jsxRuntime.jsx(
5690
+ Pagination,
5691
+ {
5692
+ activePage,
5693
+ onPageChange: handlePageChange,
5694
+ maxPage: MAX_PAGE,
5695
+ onPerPageChange: onPaginationChange,
5696
+ options: pagination,
5697
+ serverSide: isServerSide
5698
+ }
5699
+ );
5550
5700
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `w-full h-max rounded-lg ${className}`.trim(), style, children: [
5551
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
5552
- hasSearch && /* @__PURE__ */ jsxRuntime.jsx(
5701
+ (hasSearch || showTopPager) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 mb-2", children: [
5702
+ hasSearch ? /* @__PURE__ */ jsxRuntime.jsx(
5553
5703
  SearchInput_default,
5554
5704
  {
5555
5705
  ref: searchRef,
5556
5706
  value: searchTerm,
5557
5707
  onChange: onSearchChange,
5558
- placeholder: "Search term..."
5708
+ placeholder: search?.placeholder ?? "Search term..."
5559
5709
  }
5560
- ),
5561
- pagination.enabled && /* @__PURE__ */ jsxRuntime.jsx(
5562
- Pagination,
5563
- {
5564
- activePage,
5565
- onPageChange: handlePageChange,
5566
- maxPage: MAX_PAGE,
5567
- onPerPageChange: onPaginationChange,
5568
- options: pagination,
5569
- serverSide: isServerSide
5570
- }
5571
- )
5710
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", {}),
5711
+ showTopPager && pager
5572
5712
  ] }),
5573
5713
  /* @__PURE__ */ jsxRuntime.jsx("div", { children: header }),
5574
5714
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse", "aria-busy": loading || void 0, children: [
5575
- /* @__PURE__ */ jsxRuntime.jsx(TableHeader, { columns, hasExpand: !!expandRow.enabled }),
5715
+ /* @__PURE__ */ jsxRuntime.jsx(TableHeader, { columns, hasExpand: !!expandRow.enabled, sort: sortState, onSort: handleSort }),
5576
5716
  loading ? /* @__PURE__ */ jsxRuntime.jsx(
5577
5717
  TableSkeletonBody,
5578
5718
  {
@@ -5586,10 +5726,12 @@ function Table({
5586
5726
  columns,
5587
5727
  rows: currentPageRows,
5588
5728
  expandRow,
5589
- getRowKey
5729
+ getRowKey,
5730
+ onCellEdit
5590
5731
  }
5591
5732
  )
5592
5733
  ] }) }),
5734
+ showBottomPager && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 flex justify-end", children: pager }),
5593
5735
  /* @__PURE__ */ jsxRuntime.jsx("div", { children: footer })
5594
5736
  ] });
5595
5737
  }