@marktiderman/genesis-ui 2.0.0 → 2.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{AppShell-BJarYZzz.d.ts → AppShell-sQT0UbXg.d.ts} +4 -4
  3. package/dist/{chunk-Q4I4SFBD.js → chunk-GDFNAHI4.js} +9 -5
  4. package/dist/chunk-GDFNAHI4.js.map +1 -0
  5. package/dist/components/data/index.d.ts +134 -27
  6. package/dist/components/data/index.js +234 -18
  7. package/dist/components/data/index.js.map +1 -1
  8. package/dist/components/layout/index.d.ts +2 -3
  9. package/dist/components/patterns/empty-state.d.ts +4 -5
  10. package/dist/components/patterns/form-field.d.ts +3 -4
  11. package/dist/components/patterns/index.d.ts +2 -3
  12. package/dist/components/patterns/page-loading.d.ts +2 -2
  13. package/dist/components/patterns/settings-row.d.ts +1 -2
  14. package/dist/components/patterns/status-badge.d.ts +1 -2
  15. package/dist/components/patterns/toggle-row.d.ts +1 -2
  16. package/dist/components/patterns/user-avatar.d.ts +2 -2
  17. package/dist/components/ui/accessible-icon.d.ts +1 -2
  18. package/dist/components/ui/alert-dialog.d.ts +2 -3
  19. package/dist/components/ui/badge.d.ts +1 -2
  20. package/dist/components/ui/breadcrumb.d.ts +2 -3
  21. package/dist/components/ui/button-group.d.ts +1 -2
  22. package/dist/components/ui/calendar.d.ts +2 -3
  23. package/dist/components/ui/card.d.ts +6 -7
  24. package/dist/components/ui/carousel.d.ts +2 -3
  25. package/dist/components/ui/chart.d.ts +4 -5
  26. package/dist/components/ui/command-palette.d.ts +1 -2
  27. package/dist/components/ui/command.d.ts +2 -3
  28. package/dist/components/ui/context-menu.d.ts +1 -2
  29. package/dist/components/ui/date-picker.d.ts +2 -2
  30. package/dist/components/ui/dialog.d.ts +2 -3
  31. package/dist/components/ui/drawer.d.ts +3 -4
  32. package/dist/components/ui/dropdown-menu.d.ts +1 -2
  33. package/dist/components/ui/kbd.d.ts +1 -2
  34. package/dist/components/ui/menubar.d.ts +1 -2
  35. package/dist/components/ui/pagination.d.ts +5 -6
  36. package/dist/components/ui/password-toggle-field.d.ts +2 -3
  37. package/dist/components/ui/progress.d.ts +1 -2
  38. package/dist/components/ui/resizable.d.ts +2 -3
  39. package/dist/components/ui/separator.d.ts +1 -2
  40. package/dist/components/ui/sheet.d.ts +2 -3
  41. package/dist/components/ui/skeleton.d.ts +1 -2
  42. package/dist/components/ui/sonner.d.ts +1 -2
  43. package/dist/components/ui/spinner.d.ts +1 -2
  44. package/dist/components/ui/tooltip.d.ts +1 -2
  45. package/dist/components/ui/typography.d.ts +9 -10
  46. package/dist/hooks/index.d.ts +2 -3
  47. package/dist/hooks/index.js +1 -1
  48. package/dist/hooks/use-resource-page.d.ts +2 -3
  49. package/dist/hooks/use-resource-page.js +1 -1
  50. package/dist/hooks/use-view-preference.d.ts +2 -2
  51. package/dist/index.d.ts +0 -1
  52. package/dist/{use-resource-page-CPs6wjKt.d.ts → use-resource-page-DTQS011Q.d.ts} +18 -6
  53. package/dist/{view-settings--KXNT_jE.d.ts → view-settings-kHKCHMjj.d.ts} +2 -2
  54. package/dist/{view-toggle-DDe_Luj2.d.ts → view-toggle-CahQ87PW.d.ts} +2 -2
  55. package/package.json +15 -15
  56. package/dist/chunk-Q4I4SFBD.js.map +0 -1
@@ -26,7 +26,7 @@ import '../../chunk-F47U5KNW.js';
26
26
  import { DetailPage } from '../../chunk-AFZ76Q26.js';
27
27
  import '../../chunk-D2LIXA2Y.js';
28
28
  import '../../chunk-AVFZ4P4B.js';
29
- import { titleCase, useResourcePage } from '../../chunk-Q4I4SFBD.js';
29
+ import { titleCase, useResourcePage } from '../../chunk-GDFNAHI4.js';
30
30
  export { useSavedViews } from '../../chunk-GCQFVMPI.js';
31
31
  import '../../chunk-FTTNGGLA.js';
32
32
  import { useViewSettings } from '../../chunk-OZ7ZAYCS.js';
@@ -40,7 +40,7 @@ import { useKeyboardNavigation } from '../../chunk-YJ6MPS6U.js';
40
40
  import { useIsMobile } from '../../chunk-M7AU5DDM.js';
41
41
  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
42
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
43
- import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
43
+ import { useState, useRef, useEffect, useCallback, useMemo, Fragment as Fragment$1 } from 'react';
44
44
  import { formatDistanceToNow, format } from 'date-fns';
45
45
  import { useFieldArray, Controller, useWatch } from 'react-hook-form';
46
46
  import { useResource, useWizard, useResourceForm, useOne, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
@@ -2424,6 +2424,24 @@ function ResourcePage(props) {
2424
2424
  searchPlaceholder,
2425
2425
  initialFilters,
2426
2426
  onFiltersChange,
2427
+ page: pageProp,
2428
+ perPage: perPageProp,
2429
+ onPageChange,
2430
+ onPerPageChange,
2431
+ onSortChange,
2432
+ onSearchChange,
2433
+ savedViews,
2434
+ onLoadView,
2435
+ onSaveView,
2436
+ onDeleteView,
2437
+ canSaveViews,
2438
+ columnVisibility,
2439
+ columnVisibilityKey,
2440
+ resizableColumns,
2441
+ resizeKey,
2442
+ stickyHeader,
2443
+ stickyFirstColumn,
2444
+ pageSizeOptions,
2427
2445
  onCreate,
2428
2446
  onEdit,
2429
2447
  onDelete,
@@ -2447,8 +2465,10 @@ function ResourcePage(props) {
2447
2465
  viewSettingsKey: viewSettingsKeyProp
2448
2466
  } = props;
2449
2467
  const isClientMode = !!resourceName;
2468
+ const isServerControlled = pageProp !== void 0 || perPageProp !== void 0 || onPageChange !== void 0 || onPerPageChange !== void 0 || onSortChange !== void 0 || onSearchChange !== void 0;
2450
2469
  const viewSettingsKey = viewSettingsKeyProp ?? resourceName ?? title.toLowerCase().replace(/\s+/g, "-");
2451
2470
  const { settings, updateSetting } = useViewSettings(viewSettingsKey);
2471
+ const effectivePageSize = perPageProp ?? settings.pageSize;
2452
2472
  const resourceHook = useResource(
2453
2473
  resourceName ?? "__noop__",
2454
2474
  isClientMode ? resourceOptions : { enabled: false }
@@ -2469,7 +2489,8 @@ function ResourcePage(props) {
2469
2489
  defaultView,
2470
2490
  detail,
2471
2491
  titleField: titleFieldProp,
2472
- subtitleField: subtitleFieldProp
2492
+ subtitleField: subtitleFieldProp,
2493
+ controlled: isServerControlled
2473
2494
  });
2474
2495
  const actions = useMemo(() => {
2475
2496
  if (isClientMode) {
@@ -2481,15 +2502,24 @@ function ResourcePage(props) {
2481
2502
  }
2482
2503
  return {
2483
2504
  update: async () => {
2484
- throw new Error("ResourceActions.update not available in server-data mode");
2505
+ throw new Error(
2506
+ "ResourceActions.update not available in server-data mode"
2507
+ );
2485
2508
  },
2486
2509
  remove: async () => {
2487
- throw new Error("ResourceActions.remove not available in server-data mode");
2510
+ throw new Error(
2511
+ "ResourceActions.remove not available in server-data mode"
2512
+ );
2488
2513
  },
2489
2514
  refetch: () => {
2490
2515
  }
2491
2516
  };
2492
- }, [isClientMode, resourceHook.update, resourceHook.remove, resourceHook.list]);
2517
+ }, [
2518
+ isClientMode,
2519
+ resourceHook.update,
2520
+ resourceHook.remove,
2521
+ resourceHook.list
2522
+ ]);
2493
2523
  const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
2494
2524
  const handleToggleSelect = useCallback((id) => {
2495
2525
  setSelectedIds((prev) => {
@@ -2539,15 +2569,101 @@ function ResourcePage(props) {
2539
2569
  [useBuiltInEdit, onEdit]
2540
2570
  );
2541
2571
  const tableColumns = useMemo(() => {
2542
- if (!isClientMode) return page.tableColumns;
2543
- return page.columns.map((col) => ({
2572
+ const base = isClientMode ? page.columns.map((col) => ({
2544
2573
  key: col.key,
2545
2574
  header: col.header ?? col.key,
2546
2575
  sortable: col.sortable ?? true,
2547
2576
  hideBelow: col.hideBelow,
2548
2577
  render: col.render ? (item, value) => col.render(item, value, actions) : void 0
2549
- }));
2550
- }, [isClientMode, page.columns, page.tableColumns, actions]);
2578
+ })) : page.tableColumns;
2579
+ if (!isServerControlled) return base;
2580
+ return base.map((col) => ({ ...col, sortable: false }));
2581
+ }, [
2582
+ isClientMode,
2583
+ isServerControlled,
2584
+ page.columns,
2585
+ page.tableColumns,
2586
+ actions
2587
+ ]);
2588
+ const currentPage = pageProp ?? 1;
2589
+ const handleSearchChange = useCallback(
2590
+ (value) => {
2591
+ page.filterConfig.onSearchChange(value);
2592
+ if (!isServerControlled) return;
2593
+ onSearchChange?.(value);
2594
+ if (currentPage !== 1) onPageChange?.(1);
2595
+ },
2596
+ [
2597
+ page.filterConfig,
2598
+ isServerControlled,
2599
+ onSearchChange,
2600
+ onPageChange,
2601
+ currentPage
2602
+ ]
2603
+ );
2604
+ const handleSortSelect = useCallback(
2605
+ (value) => {
2606
+ page.filterConfig.onSortChange(value);
2607
+ if (!isServerControlled) return;
2608
+ const [field, order] = value.split(":");
2609
+ onSortChange?.(
2610
+ field && (order === "asc" || order === "desc") ? { field, order } : null
2611
+ );
2612
+ onPageChange?.(1);
2613
+ },
2614
+ [page.filterConfig, isServerControlled, onSortChange, onPageChange]
2615
+ );
2616
+ const handleClearFilters = useCallback(() => {
2617
+ page.clearFilters();
2618
+ if (!isServerControlled) return;
2619
+ onSearchChange?.("");
2620
+ onSortChange?.(null);
2621
+ onPageChange?.(1);
2622
+ }, [page, isServerControlled, onSearchChange, onSortChange, onPageChange]);
2623
+ const handleStatusChange = useCallback(
2624
+ (values) => {
2625
+ page.filterConfig.statusChips?.onChange(values);
2626
+ if (!isServerControlled) return;
2627
+ if (currentPage !== 1) onPageChange?.(1);
2628
+ },
2629
+ [page.filterConfig, isServerControlled, onPageChange, currentPage]
2630
+ );
2631
+ const statusChipsConfig = useMemo(() => {
2632
+ const base = page.filterConfig.statusChips;
2633
+ if (!base) return void 0;
2634
+ return { ...base, onChange: handleStatusChange };
2635
+ }, [page.filterConfig.statusChips, handleStatusChange]);
2636
+ const handlePageSizeChange = useCallback(
2637
+ (size) => {
2638
+ updateSetting("pageSize", size);
2639
+ onPerPageChange?.(size);
2640
+ if (isServerControlled) onPageChange?.(1);
2641
+ },
2642
+ [updateSetting, onPerPageChange, isServerControlled, onPageChange]
2643
+ );
2644
+ const missingPerPage = isServerControlled && !!onPageChange && perPageProp === void 0;
2645
+ const missingTotal = isServerControlled && !!onPageChange && totalProp === void 0;
2646
+ const pagerMisconfigured = missingPerPage || missingTotal;
2647
+ const missingPagerProps = [
2648
+ ...missingPerPage ? ["perPage"] : [],
2649
+ ...missingTotal ? ["total"] : []
2650
+ ];
2651
+ const loggedMissingPerPageRef = useRef(false);
2652
+ useEffect(() => {
2653
+ if (!pagerMisconfigured) return;
2654
+ if (loggedMissingPerPageRef.current) return;
2655
+ loggedMissingPerPageRef.current = true;
2656
+ console.error(
2657
+ `ResourcePage: ${missingPagerProps.map((name) => `\`${name}\``).join(" and ")} ${missingPagerProps.length > 1 ? "are" : "is"} required when \`onPageChange\` is supplied.`
2658
+ );
2659
+ }, [pagerMisconfigured]);
2660
+ const pagerPageSize = perPageProp ?? effectivePageSize;
2661
+ const totalPages = Math.max(
2662
+ 1,
2663
+ Math.ceil(displayTotal / Math.max(1, pagerPageSize))
2664
+ );
2665
+ const rangeFrom = page.data.length === 0 ? 0 : (currentPage - 1) * pagerPageSize + 1;
2666
+ const rangeTo = rangeFrom === 0 ? 0 : rangeFrom + page.data.length - 1;
2551
2667
  const detailTitle = page.selectedItem ? String(
2552
2668
  page.selectedItem[page.titleField] ?? ""
2553
2669
  ) : "";
@@ -2694,25 +2810,30 @@ function ResourcePage(props) {
2694
2810
  onRetry: isClientMode ? () => resourceHook.list.refetch() : void 0,
2695
2811
  viewMode: page.viewMode,
2696
2812
  onViewModeChange: page.setViewMode,
2697
- pageSize: settings.pageSize,
2698
- onPageSizeChange: (size) => updateSetting("pageSize", size),
2813
+ pageSize: effectivePageSize,
2814
+ onPageSizeChange: handlePageSizeChange,
2699
2815
  density: settings.density,
2700
2816
  onDensityChange: (d) => updateSetting("density", d),
2701
2817
  createAction,
2702
2818
  hasActiveFilters: page.hasActiveFilters,
2703
- onClearFilters: page.clearFilters,
2819
+ onClearFilters: handleClearFilters,
2704
2820
  emptyAction: emptyState,
2705
2821
  filters: /* @__PURE__ */ jsx(
2706
2822
  DataFilters,
2707
2823
  {
2708
2824
  search: page.filterConfig.search,
2709
- onSearchChange: page.filterConfig.onSearchChange,
2825
+ onSearchChange: handleSearchChange,
2710
2826
  searchPlaceholder,
2711
2827
  sort: page.filterConfig.sort,
2712
- onSortChange: page.filterConfig.onSortChange,
2828
+ onSortChange: handleSortSelect,
2713
2829
  sortOptions: page.filterConfig.sortOptions,
2714
- statusChips: page.filterConfig.statusChips,
2715
- onClearAll: page.hasActiveFilters ? page.clearFilters : void 0
2830
+ statusChips: statusChipsConfig,
2831
+ savedViews,
2832
+ onLoadView,
2833
+ onSaveView,
2834
+ onDeleteView,
2835
+ canSaveViews,
2836
+ onClearAll: page.hasActiveFilters ? handleClearFilters : void 0
2716
2837
  }
2717
2838
  ),
2718
2839
  children: [
@@ -2754,13 +2875,108 @@ function ResourcePage(props) {
2754
2875
  onToggleSelect: handleToggleSelect,
2755
2876
  onToggleAll: handleToggleAll,
2756
2877
  density: settings.density,
2757
- pageSize: settings.pageSize
2878
+ pageSize: effectivePageSize,
2879
+ pageSizeOptions,
2880
+ columnVisibility,
2881
+ columnVisibilityKey,
2882
+ resizableColumns,
2883
+ resizeKey,
2884
+ stickyHeader,
2885
+ stickyFirstColumn,
2886
+ pagination: isServerControlled ? false : void 0
2758
2887
  }
2759
2888
  ),
2760
2889
  renderCard: (item, index) => renderCardProp ? renderCardProp(item, index, cardActionsFor(item)) : renderDefaultCard(item),
2761
2890
  renderListItem: (item, index) => renderCardProp ? renderCardProp(item, index, cardActionsFor(item)) : renderDefaultListItem(item)
2762
2891
  }
2763
2892
  ),
2893
+ isServerControlled && onPageChange && pagerMisconfigured && /* @__PURE__ */ jsxs(
2894
+ "div",
2895
+ {
2896
+ className: "flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive",
2897
+ "data-testid": "resource-page-pagination-error",
2898
+ role: "alert",
2899
+ children: [
2900
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
2901
+ /* @__PURE__ */ jsxs("span", { children: [
2902
+ "Pagination unavailable:",
2903
+ " ",
2904
+ missingPagerProps.map((name, index) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
2905
+ index > 0 ? " and " : null,
2906
+ /* @__PURE__ */ jsx("code", { children: name })
2907
+ ] }, name)),
2908
+ " ",
2909
+ missingPagerProps.length > 1 ? "are" : "is",
2910
+ " required when",
2911
+ " ",
2912
+ /* @__PURE__ */ jsx("code", { children: "onPageChange" }),
2913
+ " is supplied."
2914
+ ] })
2915
+ ]
2916
+ }
2917
+ ),
2918
+ isServerControlled && onPageChange && !pagerMisconfigured && /* @__PURE__ */ jsxs(
2919
+ "nav",
2920
+ {
2921
+ "aria-label": "Pagination",
2922
+ className: "flex flex-col sm:flex-row items-center justify-between gap-2 px-3 py-2 text-sm",
2923
+ "data-testid": "resource-page-pagination",
2924
+ children: [
2925
+ /* @__PURE__ */ jsx(
2926
+ "span",
2927
+ {
2928
+ className: "text-muted-foreground",
2929
+ "data-testid": "resource-page-range",
2930
+ "aria-live": "polite",
2931
+ children: displayTotal === 0 ? "No records" : `Showing ${rangeFrom}-${rangeTo} of ${displayTotal}`
2932
+ }
2933
+ ),
2934
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2935
+ /* @__PURE__ */ jsxs(
2936
+ Button,
2937
+ {
2938
+ variant: "outline",
2939
+ size: "sm",
2940
+ disabled: currentPage <= 1,
2941
+ onClick: () => onPageChange(currentPage - 1),
2942
+ testID: "resource-page-prev",
2943
+ children: [
2944
+ /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4 mr-1", "aria-hidden": "true" }),
2945
+ "Previous"
2946
+ ]
2947
+ }
2948
+ ),
2949
+ /* @__PURE__ */ jsxs(
2950
+ "span",
2951
+ {
2952
+ className: "text-muted-foreground",
2953
+ "data-testid": "resource-page-indicator",
2954
+ children: [
2955
+ "Page ",
2956
+ currentPage,
2957
+ " of ",
2958
+ totalPages
2959
+ ]
2960
+ }
2961
+ ),
2962
+ /* @__PURE__ */ jsxs(
2963
+ Button,
2964
+ {
2965
+ variant: "outline",
2966
+ size: "sm",
2967
+ disabled: currentPage >= totalPages,
2968
+ onClick: () => onPageChange(currentPage + 1),
2969
+ testID: "resource-page-next",
2970
+ children: [
2971
+ "Next",
2972
+ /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 ml-1", "aria-hidden": "true" })
2973
+ ]
2974
+ }
2975
+ )
2976
+ ] })
2977
+ ]
2978
+ }
2979
+ ),
2764
2980
  (detail === "panel" || detail === "modal") && /* @__PURE__ */ jsx(
2765
2981
  DetailPanel,
2766
2982
  {