@marktiderman/genesis-ui 2.1.0 → 2.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.
@@ -167,11 +167,34 @@ interface DataGridProps<T> {
167
167
  }
168
168
  declare function DataGrid<T>({ items, viewMode, batchSize, gridCols, renderCard, renderTable, renderListItem, staggerAnimation, getKey, focusedIndex, keyboardContainerRef, }: DataGridProps<T>): React.JSX.Element;
169
169
 
170
+ /**
171
+ * One button in the bulk action bar.
172
+ *
173
+ * @stability Stable
174
+ */
170
175
  interface BulkAction {
171
176
  label: string;
172
177
  icon: LucideIcon;
173
178
  onClick: () => void;
174
179
  variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
180
+ /**
181
+ * Makes this action inert: it renders with the Button primitive's native
182
+ * `disabled` attribute, so it is neither clickable nor focusable, and it
183
+ * picks up the disabled styling.
184
+ *
185
+ * The case this exists for is an in-flight bulk mutation. A consumer that
186
+ * fires a move/duplicate/delete over the selected ids wants every bulk
187
+ * action dead until it settles, so a user cannot double-fire a destructive
188
+ * operation on the same selection. Without this field the only guard
189
+ * available is an early return inside `onClick` — which does stop the
190
+ * second call, but leaves the button looking live, so the affordance lies
191
+ * about what pressing it will do. Concretely, that is what blocks a real
192
+ * consumer's hand-rolled bulk bar from migrating to `DataBulkBar`: it
193
+ * would have to give up its double-submit guard to do so.
194
+ *
195
+ * Optional and defaulted off — omitting it behaves exactly as before.
196
+ */
197
+ disabled?: boolean;
175
198
  }
176
199
  interface DataBulkBarProps {
177
200
  selected: Set<string>;
@@ -405,6 +428,26 @@ interface ResourcePageProps<T extends Record<string, unknown> = Record<string, u
405
428
  searchPlaceholder?: string;
406
429
  initialFilters?: FilterParam[];
407
430
  onFiltersChange?: (filters: FilterParam[]) => void;
431
+ /**
432
+ * Extra multi-select filter comboboxes, handed straight to `DataFilters`
433
+ * and rendered beside the status chips. Each config owns its own
434
+ * `selected` array and `onChange` — ResourcePage never reads or writes
435
+ * them, because it cannot know what a caller's "Grade" or "Subject"
436
+ * combobox means against the row shape. That is deliberate rather than a
437
+ * gap: the built-in machinery (`searchFields`, `statusFilter`) covers the
438
+ * two shapes it *can* interpret, and this prop covers everything else by
439
+ * getting out of the way.
440
+ *
441
+ * Two consequences of that hands-off stance, stated rather than hidden:
442
+ * - These selections are invisible to `page.hasActiveFilters`, so the
443
+ * header's "Clear filters" affordance does not appear for them alone,
444
+ * and `onClearFilters` does not reset them. DataFilters' own "Clear all"
445
+ * does call each config's `onChange([])`, so wire real clearing there.
446
+ * - In client-side mode the rows are NOT re-filtered by these selections.
447
+ * A caller using them is expected to filter its own `data` (or ask the
448
+ * server to), exactly as it already does in server-controlled mode.
449
+ */
450
+ filters?: DataFilterConfig[];
408
451
  /** Current page, 1-based. Server-controlled mode only. */
409
452
  page?: number;
410
453
  /**
@@ -448,6 +491,35 @@ interface ResourcePageProps<T extends Record<string, unknown> = Record<string, u
448
491
  onSortChange?: (sort: SortParam | null) => void;
449
492
  /** Called when the user changes the search text (including clearing it). */
450
493
  onSearchChange?: (search: string) => void;
494
+ /**
495
+ * Server-data mode: the caller's fetch is in flight, so render the shell's
496
+ * loading skeleton instead of the (as yet meaningless) `data`. Ignored in
497
+ * client-side mode, where `useResource`'s own loading state is used.
498
+ */
499
+ loading?: boolean;
500
+ /**
501
+ * Server-data mode: the caller's fetch failed, so render the shell's error
502
+ * panel instead of an empty table. Takes effect even when `data` is
503
+ * non-empty — stale rows beside no indication of failure are exactly the
504
+ * silent-failure this prop exists to prevent. Ignored in client-side mode,
505
+ * where `useResource`'s own error state is used.
506
+ */
507
+ error?: boolean;
508
+ /**
509
+ * Message shown in the error panel, in place of the shell's generic
510
+ * "Failed to load …". Forwarded in both modes — it is inert unless an
511
+ * error state is actually showing — so a client-side page can also give
512
+ * its failure a human sentence.
513
+ */
514
+ errorMessage?: string;
515
+ /**
516
+ * Server-data mode: invoked by the error panel's Retry button. The button
517
+ * only appears when this is supplied, so a caller with no way to retry
518
+ * simply omits it and gets an error panel without a dead control. In
519
+ * client-side mode the button is already wired to `useResource`'s own
520
+ * `refetch` and this prop is ignored.
521
+ */
522
+ onRetry?: () => void;
451
523
  /** Saved filter views to offer. Pair with `useSavedViews` for per-user storage. */
452
524
  savedViews?: SavedViewItem[];
453
525
  onLoadView?: (filters: Record<string, unknown>) => void;
@@ -496,6 +568,13 @@ interface ResourcePageProps<T extends Record<string, unknown> = Record<string, u
496
568
  icon?: LucideIcon;
497
569
  variant?: "default" | "destructive";
498
570
  onAction: (ids: string[]) => void | Promise<void>;
571
+ /**
572
+ * Forwarded straight to the rendered bulk button (see `BulkAction`).
573
+ * Exists so a consumer can kill every bulk action while one of them is
574
+ * in flight — an early return inside `onAction` blocks the second call
575
+ * but leaves the button looking live, which misstates what it will do.
576
+ */
577
+ disabled?: boolean;
499
578
  }>;
500
579
  /**
501
580
  * Override the default card renderer used for grid view (and, absent a
@@ -512,6 +591,23 @@ interface ResourcePageProps<T extends Record<string, unknown> = Record<string, u
512
591
  * call `actions.open()` from wherever your card should activate.
513
592
  */
514
593
  renderCard?: (item: T, index: number, actions: ResourceCardActions<T>) => ReactNode;
594
+ /**
595
+ * Replace the built-in `DataTable` that table view renders, keeping every
596
+ * other piece of the page — header, counts, view settings, filters,
597
+ * bulk bar, detail panel, form, pager — exactly as it is. The counterpart
598
+ * to `renderCard`, which does the same for grid and list view.
599
+ *
600
+ * Receives the rows for the current view, which in server-controlled mode
601
+ * is precisely the page the server returned. Ownership is total: row
602
+ * click-through, selection, density, column visibility and resizing are
603
+ * all things the built-in table wires for you and a custom table must
604
+ * wire for itself. That is the trade this prop makes, and it is why
605
+ * `renderCard` remains the lighter option when only the *cells* need to
606
+ * change — a `ResourceColumnDef.render` is lighter still.
607
+ *
608
+ * Omitted, the default `DataTable` render is used unchanged.
609
+ */
610
+ renderTable?: (items: T[]) => ReactNode;
515
611
  /**
516
612
  * Tailwind grid-column classes for the card grid, passed through to
517
613
  * `DataGrid`'s `gridCols` — e.g. `"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"`.
@@ -15,17 +15,17 @@ import '../../chunk-B2GIBUQD.js';
15
15
  import { Badge } from '../../chunk-NPNADZHM.js';
16
16
  import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../../chunk-FK42ZGGV.js';
17
17
  import { Popover, PopoverTrigger, PopoverContent } from '../../chunk-22XB3Q6H.js';
18
+ import { PageLoading } from '../../chunk-YRYQXU3G.js';
19
+ import { Spinner } from '../../chunk-OVU3Z2MT.js';
18
20
  import { Switch } from '../../chunk-27ELYTGE.js';
21
+ import { DetailPage } from '../../chunk-AFZ76Q26.js';
22
+ import '../../chunk-D2LIXA2Y.js';
23
+ import '../../chunk-AVFZ4P4B.js';
19
24
  import { EmptyState } from '../../chunk-65SPT7EA.js';
20
25
  export { EmptyState } from '../../chunk-65SPT7EA.js';
21
26
  import { Button } from '../../chunk-NWSCYLPT.js';
22
- import { PageLoading } from '../../chunk-YRYQXU3G.js';
23
- import { Spinner } from '../../chunk-OVU3Z2MT.js';
24
27
  import { FormField, FormLabel, FormError, FormDescription } from '../../chunk-R7QFP6EL.js';
25
28
  import '../../chunk-F47U5KNW.js';
26
- import { DetailPage } from '../../chunk-AFZ76Q26.js';
27
- import '../../chunk-D2LIXA2Y.js';
28
- import '../../chunk-AVFZ4P4B.js';
29
29
  import { titleCase, useResourcePage } from '../../chunk-GDFNAHI4.js';
30
30
  export { useSavedViews } from '../../chunk-GCQFVMPI.js';
31
31
  import '../../chunk-FTTNGGLA.js';
@@ -1146,6 +1146,7 @@ function DataBulkBar({
1146
1146
  size: "sm",
1147
1147
  variant: action.variant || "outline",
1148
1148
  onClick: action.onClick,
1149
+ disabled: action.disabled,
1149
1150
  children: [
1150
1151
  /* @__PURE__ */ jsx(ActionIcon, { className: "h-3.5 w-3.5 mr-1" }),
1151
1152
  action.label
@@ -2424,12 +2425,17 @@ function ResourcePage(props) {
2424
2425
  searchPlaceholder,
2425
2426
  initialFilters,
2426
2427
  onFiltersChange,
2428
+ filters: filtersProp,
2427
2429
  page: pageProp,
2428
2430
  perPage: perPageProp,
2429
2431
  onPageChange,
2430
2432
  onPerPageChange,
2431
2433
  onSortChange,
2432
2434
  onSearchChange,
2435
+ loading: loadingProp,
2436
+ error: errorProp,
2437
+ errorMessage,
2438
+ onRetry: onRetryProp,
2433
2439
  savedViews,
2434
2440
  onLoadView,
2435
2441
  onSaveView,
@@ -2455,6 +2461,7 @@ function ResourcePage(props) {
2455
2461
  formLayout: formLayoutProp,
2456
2462
  bulkActions,
2457
2463
  renderCard: renderCardProp,
2464
+ renderTable: renderTableProp,
2458
2465
  gridCols: gridColsProp,
2459
2466
  keyboardNavigation = true,
2460
2467
  toolbar,
@@ -2475,8 +2482,8 @@ function ResourcePage(props) {
2475
2482
  );
2476
2483
  const displayData = isClientMode ? resourceHook.list.data?.data ?? [] : dataProp ?? [];
2477
2484
  const displayTotal = isClientMode ? resourceHook.list.data?.total ?? 0 : totalProp ?? 0;
2478
- const isLoading = isClientMode ? resourceHook.list.isLoading : false;
2479
- const hasError = isClientMode ? resourceHook.list.isError : false;
2485
+ const isLoading = isClientMode ? resourceHook.list.isLoading : loadingProp ?? false;
2486
+ const hasError = isClientMode ? resourceHook.list.isError : errorProp ?? false;
2480
2487
  const page = useResourcePage({
2481
2488
  resource: resourceName,
2482
2489
  data: displayData,
@@ -2807,7 +2814,8 @@ function ResourcePage(props) {
2807
2814
  isLoading,
2808
2815
  hasError,
2809
2816
  isEmpty,
2810
- onRetry: isClientMode ? () => resourceHook.list.refetch() : void 0,
2817
+ errorMessage,
2818
+ onRetry: isClientMode ? () => resourceHook.list.refetch() : onRetryProp,
2811
2819
  viewMode: page.viewMode,
2812
2820
  onViewModeChange: page.setViewMode,
2813
2821
  pageSize: effectivePageSize,
@@ -2827,6 +2835,7 @@ function ResourcePage(props) {
2827
2835
  sort: page.filterConfig.sort,
2828
2836
  onSortChange: handleSortSelect,
2829
2837
  sortOptions: page.filterConfig.sortOptions,
2838
+ filters: filtersProp,
2830
2839
  statusChips: statusChipsConfig,
2831
2840
  savedViews,
2832
2841
  onLoadView,
@@ -2848,6 +2857,7 @@ function ResourcePage(props) {
2848
2857
  label: a.label,
2849
2858
  icon: a.icon ?? Trash2,
2850
2859
  variant: a.variant,
2860
+ disabled: a.disabled,
2851
2861
  onClick: () => {
2852
2862
  void a.onAction(Array.from(selectedIds));
2853
2863
  }
@@ -2863,7 +2873,7 @@ function ResourcePage(props) {
2863
2873
  getKey: (item) => String(item.id ?? ""),
2864
2874
  focusedIndex: keyboardNav.focusedIndex,
2865
2875
  keyboardContainerRef: keyboardNav.containerRef,
2866
- renderTable: (items) => /* @__PURE__ */ jsx(
2876
+ renderTable: (items) => renderTableProp ? renderTableProp(items) : /* @__PURE__ */ jsx(
2867
2877
  DataTable,
2868
2878
  {
2869
2879
  items,