@marktiderman/genesis-ui 2.5.0 → 2.6.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.
@@ -179,8 +179,12 @@ interface DataFiltersProps {
179
179
  onClearAll?: () => void;
180
180
  /** Expose the search input ref for keyboard nav "/" shortcut */
181
181
  searchInputRef?: React.RefObject<HTMLInputElement | null>;
182
+ /** Show the search box. Default `true`; turn off when the data source can't search. */
183
+ showSearch?: boolean;
184
+ /** Show the sort select. Default `true`; turn off when the data source can't sort. */
185
+ showSort?: boolean;
182
186
  }
183
- declare function DataFilters({ search, onSearchChange, searchPlaceholder, sort, onSortChange, sortOptions, filters, statusChips, savedViews, onLoadView, onSaveView, onDeleteView, canSaveViews, onViewSaved, onClearAll, searchInputRef, }: DataFiltersProps): React.JSX.Element;
187
+ declare function DataFilters({ search, onSearchChange, searchPlaceholder, sort, onSortChange, sortOptions, filters, statusChips, savedViews, onLoadView, onSaveView, onDeleteView, canSaveViews, onViewSaved, onClearAll, searchInputRef, showSearch, showSort, }: DataFiltersProps): React.JSX.Element;
184
188
 
185
189
  interface DataGridProps<T> {
186
190
  items: T[];
@@ -309,14 +313,36 @@ interface KanbanColumn<T> {
309
313
  /** Raw bg color class (used if status not provided) */
310
314
  bgColor?: string;
311
315
  items: T[];
316
+ /** How many items the column holds in all, when `items` is one page of them. */
317
+ total?: number;
312
318
  }
313
319
  interface KanbanBoardProps<T> {
314
320
  columns: KanbanColumn<T>[];
315
321
  renderCard: (item: T) => ReactNode;
316
322
  emptyMessage?: string;
317
323
  className?: string;
324
+ /** A stable key per item. Without one, cards are keyed by position. */
325
+ getKey?: (item: T) => string;
326
+ /**
327
+ * Move an item to another column. When set, cards can be dragged between
328
+ * columns, and each card gets a "Move to…" menu that does the same from
329
+ * the keyboard. A rejected promise leaves the card where it was.
330
+ */
331
+ onMoveItem?: (item: T, toColumn: string) => void | Promise<void>;
332
+ /** A name for an item, for the move menu's label ("Move Ana Ruiz"). */
333
+ getItemLabel?: (item: T) => string;
334
+ /** Under a column's cards, e.g. a "Load more" button. */
335
+ renderColumnFooter?: (column: KanbanColumn<T>) => ReactNode;
318
336
  }
319
- declare function KanbanBoard<T>({ columns, renderCard, emptyMessage, className, }: KanbanBoardProps<T>): React.JSX.Element;
337
+ /**
338
+ * Columns of cards, optionally movable between columns.
339
+ *
340
+ * Why the keyboard path isn't Radix (docs/FRAMEWORK.md rule 3): Radix has no
341
+ * drag and drop, and native drag and drop can't be done from a keyboard. The
342
+ * "Move to…" menu (a Radix DropdownMenu) is the equivalent path for keyboard
343
+ * and screen-reader users, so dragging is never the only way to move a card.
344
+ */
345
+ declare function KanbanBoard<T>({ columns, renderCard, emptyMessage, className, getKey, onMoveItem, getItemLabel, renderColumnFooter, }: KanbanBoardProps<T>): React.JSX.Element;
320
346
 
321
347
  interface DataListProps<T> {
322
348
  items: T[];
@@ -648,6 +674,16 @@ interface ResourcePageProps<T extends Record<string, unknown> = Record<string, u
648
674
  onDeleteView?: (id: string) => void;
649
675
  /** Show the "save current view" affordance. */
650
676
  canSaveViews?: boolean;
677
+ /**
678
+ * What the data source can do. A control it can't serve is hidden rather
679
+ * than left to error (DATA-STUDIO.md decision 9): `search: false` hides the
680
+ * search box, `sort: false` the sort select. Pass a provider's
681
+ * `capabilities()` straight in. Omitted, both show, as before.
682
+ */
683
+ capabilities?: {
684
+ search?: boolean;
685
+ sort?: boolean;
686
+ };
651
687
  /** Show the column-visibility dropdown. DataTable's default is `true`. */
652
688
  columnVisibility?: boolean;
653
689
  /** Persist key for the user's column-visibility picks. */
@@ -11,7 +11,7 @@ import { Progress } from '../../chunk-W5Y3LG5K.js';
11
11
  import { Input } from '../../chunk-QSXVL5UB.js';
12
12
  import { Checkbox } from '../../chunk-SEBU6JCK.js';
13
13
  import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
14
- import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuSeparator, DropdownMenuItem } from '../../chunk-QJ6P2DW6.js';
14
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuLabel } from '../../chunk-QJ6P2DW6.js';
15
15
  import '../../chunk-B2GIBUQD.js';
16
16
  import { Badge } from '../../chunk-NPNADZHM.js';
17
17
  import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../../chunk-FK42ZGGV.js';
@@ -39,7 +39,7 @@ import { DefaultLink, defaultNavigate } from '../../chunk-CDYZRZ6U.js';
39
39
  export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation } from '../../chunk-CDYZRZ6U.js';
40
40
  import { useKeyboardNavigation } from '../../chunk-YJ6MPS6U.js';
41
41
  import { useIsMobile } from '../../chunk-VFO3I7PV.js';
42
- import { AlertTriangle, RefreshCw, ChevronDown, Check, Search, X, ArrowUpDown, Bookmark, Trash2, BookmarkPlus, Loader2, Columns3, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ExternalLink, Pencil, ChevronsUpDown, ImageIcon, Upload, AlertCircle, FileIcon, Plus } from 'lucide-react';
42
+ import { AlertTriangle, RefreshCw, ChevronDown, Check, Search, X, ArrowUpDown, Bookmark, Trash2, BookmarkPlus, Loader2, Columns3, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ExternalLink, ArrowRightLeft, Pencil, ChevronsUpDown, ImageIcon, Upload, AlertCircle, FileIcon, Plus } from 'lucide-react';
43
43
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
44
44
  import { useState, useRef, useEffect, useCallback, useMemo, Fragment as Fragment$1 } from 'react';
45
45
  import { formatDistanceToNow, format } from 'date-fns';
@@ -279,7 +279,9 @@ function DataFilters({
279
279
  canSaveViews = false,
280
280
  onViewSaved,
281
281
  onClearAll,
282
- searchInputRef
282
+ searchInputRef,
283
+ showSearch = true,
284
+ showSort = true
283
285
  }) {
284
286
  const internalSearchRef = useRef(null);
285
287
  const effectiveSearchRef = searchInputRef || internalSearchRef;
@@ -335,8 +337,8 @@ function DataFilters({
335
337
  })
336
338
  }
337
339
  ),
338
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row gap-2", children: [
339
- /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
340
+ (showSearch || showSort) && /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row gap-2", children: [
341
+ showSearch && /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
340
342
  /* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }),
341
343
  /* @__PURE__ */ jsx(
342
344
  Input,
@@ -357,7 +359,7 @@ function DataFilters({
357
359
  }
358
360
  )
359
361
  ] }),
360
- /* @__PURE__ */ jsxs(Select, { value: sort, onValueChange: onSortChange, children: [
362
+ showSort && /* @__PURE__ */ jsxs(Select, { value: sort, onValueChange: onSortChange, children: [
361
363
  /* @__PURE__ */ jsxs(SelectTrigger, { className: "w-full sm:w-[180px] shrink-0", children: [
362
364
  /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-3.5 w-3.5 mr-1.5 text-muted-foreground" }),
363
365
  /* @__PURE__ */ jsx(SelectValue, { placeholder: "Sort by..." })
@@ -1414,12 +1416,39 @@ var statusColorMap = {
1414
1416
  destructive: { color: "text-destructive", bgColor: "bg-destructive/15" },
1415
1417
  primary: { color: "text-primary", bgColor: "bg-primary/15" }
1416
1418
  };
1419
+ var DRAG_TYPE = "application/x-genesis-kanban-item";
1417
1420
  function KanbanBoard({
1418
1421
  columns,
1419
1422
  renderCard,
1420
1423
  emptyMessage = "No items",
1421
- className
1424
+ className,
1425
+ getKey,
1426
+ onMoveItem,
1427
+ getItemLabel,
1428
+ renderColumnFooter
1422
1429
  }) {
1430
+ const [dragging, setDragging] = useState(null);
1431
+ const [over, setOver] = useState(null);
1432
+ const keyOf = (item, index) => getKey ? getKey(item) : String(index);
1433
+ const move = (item, to) => {
1434
+ void Promise.resolve(onMoveItem?.(item, to)).catch(() => {
1435
+ });
1436
+ };
1437
+ const onDrop = (event, to) => {
1438
+ event.preventDefault();
1439
+ setOver(null);
1440
+ const key = event.dataTransfer.getData(DRAG_TYPE) || dragging?.key;
1441
+ setDragging(null);
1442
+ if (!key || !getKey) return;
1443
+ for (const column of columns) {
1444
+ const item = column.items.find((i) => getKey(i) === key);
1445
+ if (item) {
1446
+ if (column.key !== to) move(item, to);
1447
+ return;
1448
+ }
1449
+ }
1450
+ };
1451
+ const movable = Boolean(onMoveItem && getKey);
1423
1452
  return /* @__PURE__ */ jsx(
1424
1453
  "div",
1425
1454
  {
@@ -1429,20 +1458,80 @@ function KanbanBoard({
1429
1458
  ),
1430
1459
  children: columns.map((column) => {
1431
1460
  const resolved = column.status ? statusColorMap[column.status] : { color: column.color ?? "", bgColor: column.bgColor ?? "" };
1432
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
1433
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1434
- /* @__PURE__ */ jsx("span", { className: cn("text-sm font-semibold", resolved.color), children: column.label }),
1435
- /* @__PURE__ */ jsx(
1436
- Badge,
1437
- {
1438
- variant: "secondary",
1439
- className: cn("text-[10px] px-1.5 py-0", resolved.bgColor),
1440
- children: column.items.length
1441
- }
1442
- )
1443
- ] }),
1444
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 min-h-[80px]", children: column.items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-lg border border-dashed border-border p-6 text-xs text-muted-foreground", children: emptyMessage }) : column.items.map((item, i) => /* @__PURE__ */ jsx("div", { className: "animate-fade-in", style: { animationDelay: `${i * 30}ms`, animationFillMode: "both" }, children: renderCard(item) }, i)) })
1445
- ] }, column.key);
1461
+ return /* @__PURE__ */ jsxs(
1462
+ "section",
1463
+ {
1464
+ "aria-label": column.label,
1465
+ "data-kanban-column": column.key,
1466
+ className: cn(
1467
+ "flex flex-col gap-3 rounded-lg",
1468
+ movable && over === column.key && dragging?.from !== column.key && "bg-accent/40 ring-2 ring-primary/40"
1469
+ ),
1470
+ onDragOver: movable ? (e) => {
1471
+ if (!dragging) return;
1472
+ e.preventDefault();
1473
+ e.dataTransfer.dropEffect = "move";
1474
+ if (over !== column.key) setOver(column.key);
1475
+ } : void 0,
1476
+ onDragLeave: movable ? (e) => {
1477
+ if (!e.currentTarget.contains(e.relatedTarget)) setOver(null);
1478
+ } : void 0,
1479
+ onDrop: movable ? (e) => onDrop(e, column.key) : void 0,
1480
+ children: [
1481
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1482
+ /* @__PURE__ */ jsx("span", { className: cn("text-sm font-semibold", resolved.color), children: column.label }),
1483
+ /* @__PURE__ */ jsx(
1484
+ Badge,
1485
+ {
1486
+ variant: "secondary",
1487
+ className: cn("text-[10px] px-1.5 py-0", resolved.bgColor),
1488
+ children: column.total ?? column.items.length
1489
+ }
1490
+ )
1491
+ ] }),
1492
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 min-h-[80px]", children: column.items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-lg border border-dashed border-border p-6 text-xs text-muted-foreground", children: emptyMessage }) : column.items.map((item, i) => {
1493
+ const key = keyOf(item, i);
1494
+ return /* @__PURE__ */ jsxs(
1495
+ "div",
1496
+ {
1497
+ className: cn("animate-fade-in", movable && "group relative", dragging?.key === key && "opacity-50"),
1498
+ style: { animationDelay: `${i * 30}ms`, animationFillMode: "both" },
1499
+ draggable: movable || void 0,
1500
+ onDragStart: movable ? (e) => {
1501
+ e.dataTransfer.setData(DRAG_TYPE, key);
1502
+ e.dataTransfer.effectAllowed = "move";
1503
+ setDragging({ key, from: column.key });
1504
+ } : void 0,
1505
+ onDragEnd: movable ? () => {
1506
+ setDragging(null);
1507
+ setOver(null);
1508
+ } : void 0,
1509
+ children: [
1510
+ renderCard(item),
1511
+ movable && columns.length > 1 && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1512
+ /* @__PURE__ */ jsx(
1513
+ DropdownMenuTrigger,
1514
+ {
1515
+ "aria-label": `Move ${getItemLabel?.(item) ?? "card"}`,
1516
+ className: "absolute right-1.5 top-1.5 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1517
+ children: /* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-3.5 w-3.5", "aria-hidden": true })
1518
+ }
1519
+ ),
1520
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
1521
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Move to" }),
1522
+ columns.filter((c) => c.key !== column.key).map((c) => /* @__PURE__ */ jsx(DropdownMenuItem, { onSelect: () => move(item, c.key), children: c.label }, c.key))
1523
+ ] })
1524
+ ] })
1525
+ ]
1526
+ },
1527
+ key
1528
+ );
1529
+ }) }),
1530
+ renderColumnFooter?.(column)
1531
+ ]
1532
+ },
1533
+ column.key
1534
+ );
1446
1535
  })
1447
1536
  }
1448
1537
  );
@@ -2614,6 +2703,7 @@ function ResourcePage(props) {
2614
2703
  renderTable: renderTableProp,
2615
2704
  gridCols: gridColsProp,
2616
2705
  keyboardNavigation = true,
2706
+ capabilities,
2617
2707
  toolbar,
2618
2708
  emptyState,
2619
2709
  resourceOptions,
@@ -2999,7 +3089,9 @@ function ResourcePage(props) {
2999
3089
  onSaveView,
3000
3090
  onDeleteView,
3001
3091
  canSaveViews,
3002
- onClearAll: page.hasActiveFilters ? handleClearFilters : void 0
3092
+ onClearAll: page.hasActiveFilters ? handleClearFilters : void 0,
3093
+ showSearch: capabilities?.search !== false,
3094
+ showSort: capabilities?.sort !== false
3003
3095
  },
3004
3096
  slotProps?.filters
3005
3097
  ),