najm-kit 2.2.8 → 2.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.0
4
+
5
+ - `NTablePagination` renders numbered page buttons instead of `Page X of Y`. The window shows the first and last page, the current page, and one page either side, collapsing the rest into at most two gaps. The slot count is constant for any result longer than the window, so the bar does not change width as the reader pages through it, and a gap never stands in for a single page — that slot goes to the page instead.
6
+ - Added `paginationVariant`, defaulting to `"numbered"`. Pass `"compact"` to keep the previous position text with first/previous/next/last controls. **This changes the default appearance of every paginated `NTable`.**
7
+ - The numbered variant drops the first/last double chevrons, because page 1 and page N are now single-click targets of their own. Previous and next remain. The compact variant is unchanged.
8
+ - Numbered pages fall back to compact on their own when the page count is not trustworthy — that is, under `manualPagination` with no `pageCount` supplied, where TanStack infers a count from the rows it happens to hold rather than from a result total. Numbering that would invite clicks on pages that may not exist is not rendered.
9
+ - Below the `sm` breakpoint the numbers give way to the position text; seven page buttons plus the rows-per-page select do not fit a phone.
10
+ - Added `paginationLabels` so the bar can be localized: `rowsPerPage`, `pagination`, `goToPage`, `currentPage`, `firstPage`, `previousPage`, `nextPage`, `lastPage`, `pageOf`, and `rowsSelected`. All optional, all falling back to the previous English strings.
11
+ - Pagination chevrons now mirror under `dir="rtl"`. They previously pointed against the reading direction in right-to-left layouts.
12
+ - The page controls are wrapped in a labelled `nav`, and the current page carries `aria-current="page"`.
13
+ - Exported `buildPageItems` and `NTablePageItem` for consumers that need the same windowing outside the table.
14
+
3
15
  ## 2.2.1
4
16
 
5
17
  - Fixed a regression in 2.2.0: the dynamic page size reported under `manualPagination` could oscillate. Card row height is measured from rendered cards, so it grows as images decode; feeding that back into the page size refetched, re-rendered, re-measured, and refetched again. A list visibly settled from one page size to another with the loading skeleton flashing twice. The report is now allowed once per container geometry, which does not depend on the rows inside it, so it terminates. A resize still re-arms it, and the debounce still waits for the measurement to settle before reporting.
package/dist/index.d.ts CHANGED
@@ -2771,6 +2771,42 @@ type StepSubmitResult = {
2771
2771
  };
2772
2772
  declare function useFormSubmission({ steps, schema, defaultValues, onSubmit, currentStep, isLastStep, handleNext, markStepCompleted, reset, }: UseFormSubmissionOptions): FormSubmissionState;
2773
2773
 
2774
+ /**
2775
+ * How the page controls present position within the result.
2776
+ *
2777
+ * `numbered` renders a windowed list of page buttons. `compact` renders the
2778
+ * `Page X of Y` text with first/previous/next/last controls.
2779
+ *
2780
+ * `numbered` needs a trustworthy page count. Under `manualPagination` that
2781
+ * means the application must pass a `pageCount` derived from a real result
2782
+ * total; without one, the bar falls back to `compact` on its own rather than
2783
+ * inviting clicks on pages that may not exist.
2784
+ */
2785
+ type NTablePaginationVariant = "numbered" | "compact";
2786
+ /**
2787
+ * Accessible names and visible copy for the page controls.
2788
+ *
2789
+ * Every field is optional and falls back to English. Supply them to localize —
2790
+ * the numbered variant is mostly digits, but its controls still need names.
2791
+ */
2792
+ interface NTablePaginationLabels {
2793
+ /** Labels the rows-per-page select. Defaults to `"Rows/page"`. */
2794
+ rowsPerPage?: string;
2795
+ /** Accessible name of the whole page control group. Defaults to `"Pagination"`. */
2796
+ pagination?: string;
2797
+ /** Accessible name for one page button, given a 1-based page. */
2798
+ goToPage?: (page: number) => string;
2799
+ /** Accessible name of the current page button, given a 1-based page. */
2800
+ currentPage?: (page: number) => string;
2801
+ firstPage?: string;
2802
+ previousPage?: string;
2803
+ nextPage?: string;
2804
+ lastPage?: string;
2805
+ /** The `compact` variant's position text, given 1-based values. */
2806
+ pageOf?: (page: number, pageCount: number) => string;
2807
+ /** The selection summary, given selected and total row counts. */
2808
+ rowsSelected?: (selected: number, total: number) => string;
2809
+ }
2774
2810
  interface NTableLoadMorePagination {
2775
2811
  /** Render the supplied rows as one card list with an explicit continuation control. */
2776
2812
  mode: "load-more";
@@ -2933,6 +2969,8 @@ interface TableState {
2933
2969
  availableModes: readonly ViewMode[];
2934
2970
  setViewMode: (mode: ViewMode) => void;
2935
2971
  hasSyncedFromProps: boolean;
2972
+ paginationVariant: NTablePaginationVariant;
2973
+ paginationLabels: NTablePaginationLabels;
2936
2974
  manualPagination: boolean;
2937
2975
  pageCount: number | undefined;
2938
2976
  rowCount: number | undefined;
@@ -3071,6 +3109,8 @@ declare const createTableStore: (seed?: Partial<TableState>) => {
3071
3109
  availableModes: () => readonly ViewMode[];
3072
3110
  setViewMode: () => (mode: ViewMode) => void;
3073
3111
  hasSyncedFromProps: () => boolean;
3112
+ paginationVariant: () => NTablePaginationVariant;
3113
+ paginationLabels: () => NTablePaginationLabels;
3074
3114
  manualPagination: () => boolean;
3075
3115
  pageCount: () => number;
3076
3116
  rowCount: () => number;
@@ -3262,6 +3302,14 @@ interface NTableProps<T = any, M extends ViewMode = ViewMode> {
3262
3302
  }) => void;
3263
3303
  /** Pagination presentation used whenever NTable is actually rendering cards. */
3264
3304
  cardPagination?: NTableCardPagination;
3305
+ /**
3306
+ * How the page controls present position. Defaults to `"numbered"`, which
3307
+ * falls back to `"compact"` on its own when the page count is not
3308
+ * trustworthy. Pass `"compact"` for the `Page X of Y` text everywhere.
3309
+ */
3310
+ paginationVariant?: NTablePaginationVariant;
3311
+ /** Accessible names and visible copy for the page controls. */
3312
+ paginationLabels?: NTablePaginationLabels;
3265
3313
  rowSelection?: RowSelectionState;
3266
3314
  defaultRowSelection?: RowSelectionState;
3267
3315
  onRowSelectionChange?: (state: RowSelectionState) => void;
@@ -3519,6 +3567,8 @@ declare const TableStoreContext: React$1.Context<{
3519
3567
  availableModes: () => readonly ViewMode[];
3520
3568
  setViewMode: () => (mode: ViewMode) => void;
3521
3569
  hasSyncedFromProps: () => boolean;
3570
+ paginationVariant: () => NTablePaginationVariant;
3571
+ paginationLabels: () => NTablePaginationLabels;
3522
3572
  manualPagination: () => boolean;
3523
3573
  pageCount: () => number;
3524
3574
  rowCount: () => number;
@@ -3652,6 +3702,8 @@ declare function useStoreSync(props: any): {
3652
3702
  availableModes: () => readonly ViewMode[];
3653
3703
  setViewMode: () => (mode: ViewMode) => void;
3654
3704
  hasSyncedFromProps: () => boolean;
3705
+ paginationVariant: () => NTablePaginationVariant;
3706
+ paginationLabels: () => NTablePaginationLabels;
3655
3707
  manualPagination: () => boolean;
3656
3708
  pageCount: () => number;
3657
3709
  rowCount: () => number;
package/dist/index.mjs CHANGED
@@ -12222,6 +12222,8 @@ var createTableStore = (seed) => {
12222
12222
  hasMeasuredLayout: false,
12223
12223
  skeletonRowCount: 6,
12224
12224
  maxHeight: null,
12225
+ paginationVariant: "numbered",
12226
+ paginationLabels: {},
12225
12227
  bodyWidth: 0,
12226
12228
  bodyHeight: 0,
12227
12229
  tableHeaderHeight: 48,
@@ -12507,9 +12509,9 @@ function useDynamicPageSize(containerRef, effectiveViewMode) {
12507
12509
  const calculatePageSize = () => {
12508
12510
  const container2 = containerRef.current;
12509
12511
  if (!container2) return;
12512
+ if (error || hasNoData && !isFilteredEmpty && !isLoading) return;
12510
12513
  const bodyEl = container2.querySelector("[data-ntable-body]");
12511
12514
  const tableHeaderEl = container2.querySelector("[data-ntable-table-header]");
12512
- const loadingHeaderEl = container2.querySelector("[data-ntable-loading-header]");
12513
12515
  const cardsGridEl = Array.from(
12514
12516
  container2.querySelectorAll(
12515
12517
  "[data-ntable-loading-cards-grid], [data-ntable-cards-grid]"
@@ -12525,11 +12527,6 @@ function useDynamicPageSize(containerRef, effectiveViewMode) {
12525
12527
  const gap = Number.parseFloat(rootStyles.rowGap || rootStyles.gap || "0") || 0;
12526
12528
  bodyHeight = rootHeight - headerHeight - paginationHeight - gap * ROOT_SECTION_GAP_COUNT;
12527
12529
  }
12528
- if (loadingHeaderEl && bodyEl) {
12529
- const bodyStyles = window.getComputedStyle(bodyEl);
12530
- const bodyGap = Number.parseFloat(bodyStyles.rowGap || bodyStyles.gap || "0") || 0;
12531
- bodyHeight = Math.max(0, bodyHeight - loadingHeaderEl.offsetHeight - bodyGap);
12532
- }
12533
12530
  const tableHeaderHeight = tableHeaderEl?.offsetHeight ?? DEFAULT_TABLE_HEADER_HEIGHT;
12534
12531
  const newPageSize = calculateDynamicPageSize({ bodyHeight, tableHeaderHeight });
12535
12532
  const calculatedMaxHeight = tableHeaderHeight + newPageSize * ROW_HEIGHT;
@@ -12578,7 +12575,7 @@ function useDynamicPageSize(containerRef, effectiveViewMode) {
12578
12575
  "[data-ntable-header], [data-ntable-body], [data-ntable-pagination], [data-ntable-table-header]"
12579
12576
  ).forEach((el) => resizeObserver.observe(el));
12580
12577
  container.querySelectorAll(
12581
- "[data-ntable-loading-header], [data-ntable-loading-cards-grid], [data-ntable-loading-card], [data-ntable-cards-grid]"
12578
+ "[data-ntable-loading-cards-grid], [data-ntable-loading-card], [data-ntable-cards-grid]"
12582
12579
  ).forEach((el) => resizeObserver.observe(el));
12583
12580
  if (container.parentElement) resizeObserver.observe(container.parentElement);
12584
12581
  return () => resizeObserver.disconnect();
@@ -12612,6 +12609,7 @@ function useTable(effectiveViewModeOverride) {
12612
12609
  const onStateChange = useTableStore.use.onStateChange();
12613
12610
  const getRowId = useTableStore.use.getRowId();
12614
12611
  const manualPagination = useTableStore.use.manualPagination();
12612
+ const hasDataRows = useTableStore.use.hasData();
12615
12613
  const pageCount = useTableStore.use.pageCount();
12616
12614
  const rowCount = useTableStore.use.rowCount();
12617
12615
  const storePagination = useTableStore.use.pagination();
@@ -12725,6 +12723,11 @@ function useTable(effectiveViewModeOverride) {
12725
12723
  tableConfig.getRowCanExpand = userGetRowCanExpand ? (row) => userGetRowCanExpand(row.original) : () => true;
12726
12724
  }
12727
12725
  const table = useReactTable(tableConfig);
12726
+ const hasSeededTableRef = useRef(false);
12727
+ if (!hasSeededTableRef.current) {
12728
+ hasSeededTableRef.current = true;
12729
+ syncWithProps({ table });
12730
+ }
12728
12731
  useLayoutEffect(() => {
12729
12732
  syncWithProps({ table });
12730
12733
  }, [table]);
@@ -12745,12 +12748,17 @@ function useTable(effectiveViewModeOverride) {
12745
12748
  if (!dynamicPageSizeTarget || dynamicPageSizeTarget < 1) return;
12746
12749
  if (dynamicPageSizeTarget === storePagination.pageSize) return;
12747
12750
  const reported = reportedGeometryRef.current;
12751
+ if (reported?.key === geometryKey && reported.target === dynamicPageSizeTarget) return;
12748
12752
  if (reported?.key === geometryKey && reported.count >= MAX_PAGE_SIZE_REPORTS_PER_GEOMETRY) return;
12749
12753
  const commit = () => {
12750
12754
  const current = reportedGeometryRef.current;
12751
- reportedGeometryRef.current = current?.key === geometryKey ? { key: geometryKey, count: current.count + 1 } : { key: geometryKey, count: 1 };
12755
+ reportedGeometryRef.current = current?.key === geometryKey ? { key: geometryKey, count: current.count + 1, target: dynamicPageSizeTarget } : { key: geometryKey, count: 1, target: dynamicPageSizeTarget };
12752
12756
  setPagination({ pageIndex: storePagination.pageIndex, pageSize: dynamicPageSizeTarget });
12753
12757
  };
12758
+ if (!hasDataRows) {
12759
+ commit();
12760
+ return;
12761
+ }
12754
12762
  if (!reported) {
12755
12763
  commit();
12756
12764
  return;
@@ -12767,6 +12775,7 @@ function useTable(effectiveViewModeOverride) {
12767
12775
  dynamicPageSizeTarget,
12768
12776
  storePagination.pageIndex,
12769
12777
  storePagination.pageSize,
12778
+ hasDataRows,
12770
12779
  setPagination
12771
12780
  ]);
12772
12781
  return { table, finalColumns, sorting, setSorting, columnFilters, setColumnFilters, columnVisibility, setColumnVisibility, globalFilter, setGlobalFilter };
@@ -12960,6 +12969,11 @@ function NTableContent({ effectiveMode }) {
12960
12969
  const onCellEdit = useTableStore.use.onCellEdit();
12961
12970
  useTableStore.use.isLoading();
12962
12971
  const error = useTableStore.use.error();
12972
+ const contentDynamicHeight = useTableStore.use.dynamicHeight();
12973
+ const contentManualPagination = useTableStore.use.manualPagination();
12974
+ const contentCardPagination = useTableStore.use.cardPagination();
12975
+ const contentCalculatedPageSize = useTableStore.use.calculatedPageSize();
12976
+ const contentHasMeasuredLayout = useTableStore.use.hasMeasuredLayout();
12963
12977
  const hasNoData = useTableStore.use.hasNoData();
12964
12978
  const showContent = useTableStore.use.showContent();
12965
12979
  const classNames = useTableStore.use.classNames();
@@ -12981,6 +12995,9 @@ function NTableContent({ effectiveMode }) {
12981
12995
  if (error || hasNoData || !showContent || !table) return null;
12982
12996
  const isTableView = effectiveMode ? effectiveMode === "table" : storeIsTableView;
12983
12997
  if (!isTableView) return null;
12998
+ const allRows = table.getRowModel().rows ?? [];
12999
+ const clampRowsToMeasuredPage = contentDynamicHeight && contentManualPagination && contentCardPagination.mode === "paged" && contentHasMeasuredLayout && contentCalculatedPageSize > 0;
13000
+ const visibleRows = clampRowsToMeasuredPage ? allRows.slice(0, contentCalculatedPageSize) : allRows;
12984
13001
  const getSortIcon = (column) => {
12985
13002
  const dir = column.getIsSorted();
12986
13003
  if (dir === "asc") return /* @__PURE__ */ jsx(ArrowUp, { className: "h-4 w-4" });
@@ -12993,13 +13010,22 @@ function NTableContent({ effectiveMode }) {
12993
13010
  axis: "both",
12994
13011
  "data-bordered": bordered === false ? "false" : bordered ? "true" : void 0,
12995
13012
  className: cn(
12996
- "min-h-0 flex-1 overflow-hidden rounded-md",
13013
+ "min-h-0 overflow-hidden rounded-md",
13014
+ // Under `dynamicHeight` the page size is the largest row count that
13015
+ // fits, so the rows are always a little shorter than the space they
13016
+ // were measured against — up to one row's worth. Growing into that
13017
+ // remainder leaves the container's bottom edge floating below the last
13018
+ // row. Sizing to content instead ends the border on the last row; the
13019
+ // leftover belongs to the page, not to the table. Without
13020
+ // `dynamicHeight` the row count is the caller's, so the container has
13021
+ // to keep filling its height to stay a scroll viewport.
13022
+ contentDynamicHeight ? "max-h-full shrink" : "flex-1",
12997
13023
  surface.className,
12998
13024
  classNames?.content
12999
13025
  ),
13000
13026
  style: surface.style,
13001
13027
  onContextMenu: handleBackgroundContextMenu,
13002
- children: /* @__PURE__ */ jsxs(Table, { children: [
13028
+ children: /* @__PURE__ */ jsxs(Table, { className: "table-fixed", children: [
13003
13029
  /* @__PURE__ */ jsx(TableHeader, { "data-ntable-table-header": true, className: cn("bg-card sticky top-0 z-10", headerClassName, bordered === true && "[&_tr]:border-border", classNames?.tableHeader), children: table.getHeaderGroups().map((hg) => /* @__PURE__ */ jsxs(TableRow, { style: rowBorderStyle, className: cn("hover:bg-transparent", bordered === true && "border-border"), children: [
13004
13030
  showCheckbox && /* @__PURE__ */ jsx(
13005
13031
  TableHead,
@@ -13030,7 +13056,7 @@ function NTableContent({ effectiveMode }) {
13030
13056
  return /* @__PURE__ */ jsx(
13031
13057
  TableHead,
13032
13058
  {
13033
- className: cn("text-foreground h-12", responsiveClass),
13059
+ className: cn("text-foreground h-12 overflow-hidden text-ellipsis", responsiveClass),
13034
13060
  style: headerCellStyle,
13035
13061
  children: header.isPlaceholder ? null : /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", header.column.getCanSort() && showSorting && "cursor-pointer select-none"), onClick: header.column.getToggleSortingHandler(), children: [
13036
13062
  flexRender(header.column.columnDef.header, header.getContext()),
@@ -13041,7 +13067,7 @@ function NTableContent({ effectiveMode }) {
13041
13067
  );
13042
13068
  })
13043
13069
  ] }, hg.id)) }),
13044
- /* @__PURE__ */ jsx(TableBody, { children: table.getRowModel().rows?.length ? table.getRowModel().rows.map((row) => {
13070
+ /* @__PURE__ */ jsx(TableBody, { children: visibleRows.length ? visibleRows.map((row) => {
13045
13071
  const isSelectedByRowId = Boolean(selectedRowId && row.original?.id === selectedRowId);
13046
13072
  const canExpand = hasExpansion && row.getCanExpand();
13047
13073
  const isExpanded = canExpand && row.getIsExpanded();
@@ -13094,7 +13120,7 @@ function NTableContent({ effectiveMode }) {
13094
13120
  const meta = columnDef.meta || {};
13095
13121
  const isEditable = Boolean(onCellEdit) && Boolean(meta.editable);
13096
13122
  const responsiveClass = resolveHiddenBelowClass(meta.hiddenBelow);
13097
- return /* @__PURE__ */ jsx(TableCell, { className: cn("h-14", responsiveClass), children: isEditable ? /* @__PURE__ */ jsx(EditableCell, { cell, onCellEdit }) : flexRender(columnDef.cell, cell.getContext()) }, cell.id);
13123
+ return /* @__PURE__ */ jsx(TableCell, { title: typeof cell.getValue?.() === "string" ? cell.getValue() : void 0, className: cn("h-14 overflow-hidden text-ellipsis", responsiveClass), children: isEditable ? /* @__PURE__ */ jsx(EditableCell, { cell, onCellEdit }) : flexRender(columnDef.cell, cell.getContext()) }, cell.id);
13098
13124
  })
13099
13125
  ]
13100
13126
  }
@@ -13234,6 +13260,17 @@ function NDataCardShell({ row, onClick, onContextMenu, actions, children, classN
13234
13260
  var DEFAULT_ROWS2 = 6;
13235
13261
  var UNMEASURED_DYNAMIC_ROWS = 12;
13236
13262
  var DEFAULT_CARD_COUNT = 48;
13263
+ var VISUALLY_HIDDEN = {
13264
+ position: "absolute",
13265
+ width: 1,
13266
+ height: 1,
13267
+ margin: -1,
13268
+ padding: 0,
13269
+ overflow: "hidden",
13270
+ clip: "rect(0,0,0,0)",
13271
+ whiteSpace: "nowrap",
13272
+ borderWidth: 0
13273
+ };
13237
13274
  function NTableCardSkeleton({ surface }) {
13238
13275
  return /* @__PURE__ */ jsx(
13239
13276
  "div",
@@ -13293,75 +13330,6 @@ function NTableCardSkeleton({ surface }) {
13293
13330
  }
13294
13331
  );
13295
13332
  }
13296
- function NTableHeaderSkeleton() {
13297
- const filters = useTableStore.use.filters();
13298
- const showViewToggle = useTableStore.use.showViewToggle();
13299
- const showColumnVisibility = useTableStore.use.showColumnVisibility();
13300
- const showAddButton = useTableStore.use.showAddButton();
13301
- const hasHeaderSlot = Boolean(useTableStore.use.headerSlot());
13302
- const hasToolbar = Boolean(useTableStore.use.renderToolbar());
13303
- const filterCount = Math.min(Math.max(filters?.length ?? 0, 1), 3);
13304
- const hasActions = showViewToggle || showColumnVisibility || showAddButton || hasHeaderSlot || hasToolbar;
13305
- const hasSettings = showViewToggle || showColumnVisibility || hasHeaderSlot || hasToolbar;
13306
- if (!filters?.length && !hasActions) return null;
13307
- return /* @__PURE__ */ jsxs(
13308
- "div",
13309
- {
13310
- "data-ntable-loading-header": true,
13311
- className: "flex shrink-0 flex-wrap items-center justify-between gap-2",
13312
- children: [
13313
- filters?.length ? /* @__PURE__ */ jsx(
13314
- "div",
13315
- {
13316
- "data-ntable-loading-desktop-filters": true,
13317
- className: "hidden min-w-0 flex-1 flex-wrap gap-2 md:flex",
13318
- children: Array.from({ length: filterCount }).map((_, index) => /* @__PURE__ */ jsx(
13319
- NSkeleton,
13320
- {
13321
- className: cn("h-10 w-full rounded-lg", index < 2 ? "max-w-64" : "max-w-48")
13322
- },
13323
- index
13324
- ))
13325
- }
13326
- ) : /* @__PURE__ */ jsx("span", { className: "hidden min-w-0 flex-1 md:block" }),
13327
- /* @__PURE__ */ jsxs(
13328
- "div",
13329
- {
13330
- "data-ntable-loading-mobile-toolbar": true,
13331
- className: "flex w-full min-w-0 items-center gap-2 md:hidden",
13332
- children: [
13333
- filters?.length ? /* @__PURE__ */ jsx(
13334
- NSkeleton,
13335
- {
13336
- "data-ntable-loading-mobile-primary": true,
13337
- className: "h-10 min-w-0 flex-1 rounded-lg"
13338
- }
13339
- ) : null,
13340
- filters?.length > 1 ? /* @__PURE__ */ jsx(
13341
- NSkeleton,
13342
- {
13343
- "data-ntable-loading-mobile-filter-button": true,
13344
- className: "h-10 w-10 shrink-0 rounded-lg"
13345
- }
13346
- ) : null,
13347
- showAddButton ? /* @__PURE__ */ jsx(
13348
- NSkeleton,
13349
- {
13350
- "data-ntable-loading-mobile-add-button": true,
13351
- className: "h-10 w-10 shrink-0 rounded-lg"
13352
- }
13353
- ) : null
13354
- ]
13355
- }
13356
- ),
13357
- hasActions && /* @__PURE__ */ jsxs("div", { className: "hidden shrink-0 gap-2 md:flex", children: [
13358
- hasSettings && /* @__PURE__ */ jsx(NSkeleton, { className: "h-10 w-10 rounded-lg" }),
13359
- showAddButton && /* @__PURE__ */ jsx(NSkeleton, { className: "h-10 w-10 rounded-lg" })
13360
- ] })
13361
- ]
13362
- }
13363
- );
13364
- }
13365
13333
  function NTableLoadingSkeleton({ rows }) {
13366
13334
  const rawColumns = useTableStore.use.columns();
13367
13335
  const responsiveColumns = React__default.useMemo(() => filterResponsiveColumns(rawColumns), [rawColumns]);
@@ -13381,64 +13349,62 @@ function NTableLoadingSkeleton({ rows }) {
13381
13349
  const loadingText = useTableStore.use.loadingText();
13382
13350
  const rowCount = rows ?? (dynamicHeight ? bodyHeight > 0 ? skeletonRowCount : UNMEASURED_DYNAMIC_ROWS : DEFAULT_ROWS2);
13383
13351
  const renderHeaderLabel = (header) => typeof header === "string" ? header : null;
13384
- return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2", children: [
13385
- /* @__PURE__ */ jsx(NTableHeaderSkeleton, {}),
13386
- /* @__PURE__ */ jsxs(
13387
- "div",
13388
- {
13389
- "data-testid": "ntable-loading-skeleton",
13390
- "data-ntable-loading-row-count": rowCount,
13391
- "data-bordered": surface.bordered === false ? "false" : surface.bordered ? "true" : void 0,
13392
- "aria-busy": "true",
13393
- "aria-label": loadingText,
13394
- role: "status",
13395
- style: surface.style,
13396
- className: cn("min-h-0 flex-1 rounded-md p-0", surface.className, dynamicHeight ? "overflow-hidden" : "najm-overlay-scroll", classNames?.content),
13397
- children: [
13398
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: loadingText }),
13399
- /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: dynamicHeight ? "najm-overlay-scroll h-full" : void 0, children: /* @__PURE__ */ jsxs(Table, { children: [
13400
- /* @__PURE__ */ jsx(TableHeader, { "data-ntable-table-header": true, className: cn(headerClassName, "sticky top-0 z-10", classNames?.tableHeader), children: /* @__PURE__ */ jsxs(TableRow, { className: "hover:bg-muted/30", children: [
13401
- showCheckbox && /* @__PURE__ */ jsx(TableHead, { "aria-label": "Select column", className: "w-10 text-foreground h-12" }),
13402
- hasExpansion && /* @__PURE__ */ jsx(TableHead, { "aria-label": "Expand column", className: "w-10 text-foreground h-12" }),
13403
- columns.map((col, i) => /* @__PURE__ */ jsx(
13404
- TableHead,
13405
- {
13406
- className: cn("text-foreground h-12", resolveHiddenBelowClass(col?.meta?.hiddenBelow)),
13407
- style: col?.size ? { width: col.size } : void 0,
13408
- children: renderHeaderLabel(col?.header)
13409
- },
13410
- col?.id ?? col?.accessorKey ?? i
13411
- ))
13412
- ] }) }),
13413
- /* @__PURE__ */ jsx(TableBody, { children: Array.from({ length: rowCount }).map((_, r) => /* @__PURE__ */ jsxs(TableRow, { children: [
13414
- showCheckbox && /* @__PURE__ */ jsx(TableCell, { className: "h-14 w-10", children: /* @__PURE__ */ jsx(NSkeleton, { className: "h-4 w-4" }) }),
13415
- hasExpansion && /* @__PURE__ */ jsx(TableCell, { className: "h-14 w-10", children: /* @__PURE__ */ jsx(NSkeleton, { className: "h-4 w-4" }) }),
13416
- columns.map((col, c) => /* @__PURE__ */ jsx(
13417
- TableCell,
13418
- {
13419
- className: cn("h-14", resolveHiddenBelowClass(col?.meta?.hiddenBelow)),
13420
- style: col?.size ? { width: col.size } : void 0,
13421
- children: /* @__PURE__ */ jsx(NSkeleton, { className: "h-4 w-full" })
13422
- },
13423
- `skeleton-${r}-${col?.id ?? col?.accessorKey ?? c}`
13424
- ))
13425
- ] }, `skeleton-${r}`)) })
13426
- ] }) })
13427
- ]
13428
- }
13429
- )
13430
- ] });
13352
+ return /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-2", children: /* @__PURE__ */ jsxs(
13353
+ "div",
13354
+ {
13355
+ "data-testid": "ntable-loading-skeleton",
13356
+ "data-ntable-loading-row-count": rowCount,
13357
+ "data-bordered": surface.bordered === false ? "false" : surface.bordered ? "true" : void 0,
13358
+ "aria-busy": "true",
13359
+ "aria-label": loadingText,
13360
+ role: "status",
13361
+ style: surface.style,
13362
+ className: cn(
13363
+ "min-h-0 rounded-md p-0",
13364
+ surface.className,
13365
+ // Sizes to its rows for the same reason `NTableContent` does, and it
13366
+ // has to match: the skeleton draws exactly the row count the real
13367
+ // table will render, so a box that fills its container here and hugs
13368
+ // its rows there would visibly resize the instant the rows arrive
13369
+ // the one thing this whole layout is meant to avoid.
13370
+ dynamicHeight ? "max-h-full shrink overflow-hidden" : "flex-1 najm-overlay-scroll",
13371
+ classNames?.content
13372
+ ),
13373
+ children: [
13374
+ /* @__PURE__ */ jsx("span", { style: VISUALLY_HIDDEN, children: loadingText }),
13375
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: dynamicHeight ? "najm-overlay-scroll" : void 0, children: /* @__PURE__ */ jsxs(Table, { className: "table-fixed", children: [
13376
+ /* @__PURE__ */ jsx(TableHeader, { "data-ntable-table-header": true, className: cn(headerClassName, "sticky top-0 z-10", classNames?.tableHeader), children: /* @__PURE__ */ jsxs(TableRow, { className: "hover:bg-muted/30", children: [
13377
+ showCheckbox && /* @__PURE__ */ jsx(TableHead, { "aria-label": "Select column", className: "w-10 text-foreground h-12" }),
13378
+ hasExpansion && /* @__PURE__ */ jsx(TableHead, { "aria-label": "Expand column", className: "w-10 text-foreground h-12" }),
13379
+ columns.map((col, i) => /* @__PURE__ */ jsx(
13380
+ TableHead,
13381
+ {
13382
+ className: cn("text-foreground h-12", resolveHiddenBelowClass(col?.meta?.hiddenBelow)),
13383
+ style: col?.size ? { width: col.size } : void 0,
13384
+ children: renderHeaderLabel(col?.header)
13385
+ },
13386
+ col?.id ?? col?.accessorKey ?? i
13387
+ ))
13388
+ ] }) }),
13389
+ /* @__PURE__ */ jsx(TableBody, { children: Array.from({ length: rowCount }).map((_, r) => /* @__PURE__ */ jsxs(TableRow, { children: [
13390
+ showCheckbox && /* @__PURE__ */ jsx(TableCell, { className: "h-14 w-10", children: /* @__PURE__ */ jsx(NSkeleton, { className: "h-4 w-4" }) }),
13391
+ hasExpansion && /* @__PURE__ */ jsx(TableCell, { className: "h-14 w-10", children: /* @__PURE__ */ jsx(NSkeleton, { className: "h-4 w-4" }) }),
13392
+ columns.map((col, c) => /* @__PURE__ */ jsx(
13393
+ TableCell,
13394
+ {
13395
+ className: cn("h-14", resolveHiddenBelowClass(col?.meta?.hiddenBelow)),
13396
+ style: col?.size ? { width: col.size } : void 0,
13397
+ children: /* @__PURE__ */ jsx(NSkeleton, { className: "h-4 w-full" })
13398
+ },
13399
+ `skeleton-${r}-${col?.id ?? col?.accessorKey ?? c}`
13400
+ ))
13401
+ ] }, `skeleton-${r}`)) })
13402
+ ] }) })
13403
+ ]
13404
+ }
13405
+ ) });
13431
13406
  }
13432
13407
  function NTableCardsLoadingSkeleton({ rows }) {
13433
- const filters = useTableStore.use.filters();
13434
- const showViewToggle = useTableStore.use.showViewToggle();
13435
- const showColumnVisibility = useTableStore.use.showColumnVisibility();
13436
- const showAddButton = useTableStore.use.showAddButton();
13437
- const hasHeaderSlot = Boolean(useTableStore.use.headerSlot());
13438
- const hasToolbar = Boolean(useTableStore.use.renderToolbar());
13439
- const hasHeaderSkeleton = Boolean(
13440
- filters?.length || showViewToggle || showColumnVisibility || showAddButton || hasHeaderSlot || hasToolbar
13441
- );
13442
13408
  const classNames = useTableStore.use.classNames();
13443
13409
  const bordered = useTableStore.use.bordered();
13444
13410
  const borderColor = useTableStore.use.borderColor();
@@ -13458,33 +13424,30 @@ function NTableCardsLoadingSkeleton({ rows }) {
13458
13424
  }) : DEFAULT_CARD_COUNT);
13459
13425
  const defaultContainerClass = "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4";
13460
13426
  const containerClass = classNames?.cards ?? defaultContainerClass;
13461
- return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2", children: [
13462
- hasHeaderSkeleton && /* @__PURE__ */ jsx(NTableHeaderSkeleton, {}),
13463
- /* @__PURE__ */ jsxs(
13464
- NajmScroll,
13465
- {
13466
- axis: "y",
13467
- "aria-busy": "true",
13468
- "aria-label": loadingText,
13469
- role: "status",
13470
- className: "min-h-0 flex-1 overflow-hidden",
13471
- children: [
13472
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: loadingText }),
13473
- /* @__PURE__ */ jsx(
13474
- "div",
13475
- {
13476
- "data-testid": "ntable-cards-loading-skeleton",
13477
- "data-ntable-loading-cards-grid": true,
13478
- "data-ntable-loading-card-count": cardCount,
13479
- "aria-hidden": "true",
13480
- className: cn(containerClass),
13481
- children: Array.from({ length: cardCount }).map((_, index) => CardSkeletonComponent ? /* @__PURE__ */ jsx("div", { "data-ntable-loading-card": true, children: /* @__PURE__ */ jsx(CardSkeletonComponent, {}) }, index) : /* @__PURE__ */ jsx(NTableCardSkeleton, { surface }, index))
13482
- }
13483
- )
13484
- ]
13485
- }
13486
- )
13487
- ] });
13427
+ return /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-2", children: /* @__PURE__ */ jsxs(
13428
+ NajmScroll,
13429
+ {
13430
+ axis: "y",
13431
+ "aria-busy": "true",
13432
+ "aria-label": loadingText,
13433
+ role: "status",
13434
+ className: "min-h-0 flex-1 overflow-hidden",
13435
+ children: [
13436
+ /* @__PURE__ */ jsx("span", { style: VISUALLY_HIDDEN, children: loadingText }),
13437
+ /* @__PURE__ */ jsx(
13438
+ "div",
13439
+ {
13440
+ "data-testid": "ntable-cards-loading-skeleton",
13441
+ "data-ntable-loading-cards-grid": true,
13442
+ "data-ntable-loading-card-count": cardCount,
13443
+ "aria-hidden": "true",
13444
+ className: cn(containerClass),
13445
+ children: Array.from({ length: cardCount }).map((_, index) => CardSkeletonComponent ? /* @__PURE__ */ jsx("div", { "data-ntable-loading-card": true, children: /* @__PURE__ */ jsx(CardSkeletonComponent, {}) }, index) : /* @__PURE__ */ jsx(NTableCardSkeleton, { surface }, index))
13446
+ }
13447
+ )
13448
+ ]
13449
+ }
13450
+ ) });
13488
13451
  }
13489
13452
  var DEFAULT_ROOT_MARGIN = "80px";
13490
13453
  function useCardContinuation({
@@ -13767,6 +13730,51 @@ function NTableCards({ effectiveMode }) {
13767
13730
  ) : null
13768
13731
  ] });
13769
13732
  }
13733
+
13734
+ // src/components/table/paginationPages.ts
13735
+ function buildPageItems(pageIndex, pageCount, siblingCount = 1) {
13736
+ const pages = Math.max(0, Math.floor(pageCount));
13737
+ if (pages <= 0) return [];
13738
+ const current = Math.min(Math.max(0, Math.floor(pageIndex)), pages - 1);
13739
+ const siblings = Math.max(0, Math.floor(siblingCount));
13740
+ const lastIndex = pages - 1;
13741
+ const windowSize = siblings * 2 + 5;
13742
+ if (pages <= windowSize) {
13743
+ return range(0, lastIndex);
13744
+ }
13745
+ const left = Math.max(current - siblings, 0);
13746
+ const right = Math.min(current + siblings, lastIndex);
13747
+ const showStartGap = left - 1 >= 2;
13748
+ const showEndGap = lastIndex - 1 - right >= 2;
13749
+ const runLength = siblings * 2 + 3;
13750
+ if (!showStartGap && showEndGap) {
13751
+ return [
13752
+ ...range(0, runLength - 1),
13753
+ { type: "gap", key: "end" },
13754
+ page(lastIndex)
13755
+ ];
13756
+ }
13757
+ if (showStartGap && !showEndGap) {
13758
+ return [
13759
+ page(0),
13760
+ { type: "gap", key: "start" },
13761
+ ...range(lastIndex - runLength + 1, lastIndex)
13762
+ ];
13763
+ }
13764
+ return [
13765
+ page(0),
13766
+ { type: "gap", key: "start" },
13767
+ ...range(left, right),
13768
+ { type: "gap", key: "end" },
13769
+ page(lastIndex)
13770
+ ];
13771
+ }
13772
+ function page(pageIndex) {
13773
+ return { type: "page", pageIndex };
13774
+ }
13775
+ function range(from, to) {
13776
+ return Array.from({ length: to - from + 1 }, (_, index) => page(from + index));
13777
+ }
13770
13778
  function CardLoadMorePagination({
13771
13779
  config,
13772
13780
  rowCount,
@@ -13860,6 +13868,44 @@ function CardLoadMorePagination({
13860
13868
  }
13861
13869
  );
13862
13870
  }
13871
+ var navButtonClass = "h-8 w-8 p-0 text-foreground disabled:text-muted-foreground disabled:opacity-70";
13872
+ var chevronClass = "h-4 w-4 rtl:-scale-x-100";
13873
+ function PageNumbers({
13874
+ pageIndex,
13875
+ pageCount,
13876
+ bordered,
13877
+ labels,
13878
+ onSelect
13879
+ }) {
13880
+ return /* @__PURE__ */ jsx(Fragment, { children: buildPageItems(pageIndex, pageCount).map((item) => {
13881
+ if (item.type === "gap") {
13882
+ return /* @__PURE__ */ jsx(
13883
+ "span",
13884
+ {
13885
+ "aria-hidden": "true",
13886
+ className: "flex h-8 w-8 items-center justify-center text-sm text-muted-foreground",
13887
+ children: "\u2026"
13888
+ },
13889
+ `gap-${item.key}`
13890
+ );
13891
+ }
13892
+ const page2 = item.pageIndex + 1;
13893
+ const isCurrent = item.pageIndex === pageIndex;
13894
+ return /* @__PURE__ */ jsx(
13895
+ Button,
13896
+ {
13897
+ bordered,
13898
+ variant: isCurrent ? "default" : "outline",
13899
+ className: cn(navButtonClass, "tabular-nums"),
13900
+ "aria-label": isCurrent ? labels.currentPage?.(page2) ?? `Page ${page2}, current page` : labels.goToPage?.(page2) ?? `Go to page ${page2}`,
13901
+ "aria-current": isCurrent ? "page" : void 0,
13902
+ onClick: () => onSelect(item.pageIndex),
13903
+ children: page2
13904
+ },
13905
+ item.pageIndex
13906
+ );
13907
+ }) });
13908
+ }
13863
13909
  function NTablePagination() {
13864
13910
  const table = useTableStore.use.table();
13865
13911
  const showPagination = useTableStore.use.showPagination();
@@ -13876,7 +13922,9 @@ function NTablePagination() {
13876
13922
  const setPagination = useTableStore.use.setPagination();
13877
13923
  const isPaginationControlled = useTableStore.use.isPaginationControlled();
13878
13924
  const bordered = useTableStore.use.bordered();
13879
- if (!table || !showPagination || effectiveViewMode === "json" || effectiveViewMode === "files") return null;
13925
+ const paginationVariant = useTableStore.use.paginationVariant();
13926
+ const labels = useTableStore.use.paginationLabels();
13927
+ if (!showPagination || effectiveViewMode === "json" || effectiveViewMode === "files") return null;
13880
13928
  if (cardPagination.mode === "all") return null;
13881
13929
  if (effectiveViewMode === "cards" && cardPagination.mode === "infinite") return null;
13882
13930
  if (effectiveViewMode === "cards" && cardPagination.mode === "load-more") {
@@ -13890,10 +13938,10 @@ function NTablePagination() {
13890
13938
  }
13891
13939
  );
13892
13940
  }
13893
- const filteredRows = table.getFilteredRowModel().rows;
13894
- const selectedRows = table.getFilteredSelectedRowModel().rows;
13895
- const { pageIndex, pageSize } = table.getState().pagination;
13896
- const effectivePageCount = manualPagination && pageCount !== void 0 ? pageCount : table.getPageCount();
13941
+ const filteredRows = table ? table.getFilteredRowModel().rows : [];
13942
+ const selectedRows = table ? table.getFilteredSelectedRowModel().rows : [];
13943
+ const { pageIndex, pageSize } = table ? table.getState().pagination : pagination;
13944
+ const effectivePageCount = manualPagination && pageCount !== void 0 ? pageCount : table ? table.getPageCount() : 1;
13897
13945
  const currentPagination = pagination ?? { pageIndex, pageSize };
13898
13946
  const currentPageSizeOptions = pageSizeOptions.includes(pageSize) ? pageSizeOptions : [...pageSizeOptions, pageSize].sort((a, b) => a - b);
13899
13947
  const navigate = (direction) => {
@@ -13918,10 +13966,15 @@ function NTablePagination() {
13918
13966
  const newSize = Number(value);
13919
13967
  setPagination({ pageIndex: 0, pageSize: newSize });
13920
13968
  };
13969
+ const hasTrustworthyPageCount = manualPagination ? pageCount !== void 0 && pageCount > 0 : effectivePageCount > 0;
13970
+ const showNumbers = paginationVariant === "numbered" && hasTrustworthyPageCount;
13971
+ const canPrevious = (table?.getCanPreviousPage?.() ?? pageIndex > 0) && pageIndex > 0;
13972
+ const canNext = (table?.getCanNextPage?.() ?? true) && pageIndex < effectivePageCount - 1;
13973
+ const selectedTotal = manualPagination && rowCount !== void 0 ? rowCount : filteredRows.length;
13921
13974
  return /* @__PURE__ */ jsxs("div", { className: cn("flex w-full min-w-0 flex-wrap items-center justify-between gap-x-4 gap-y-2 py-1 text-foreground", classNames?.pagination), children: [
13922
13975
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-4 lg:gap-6", children: [
13923
13976
  (!isPaginationControlled || manualPagination) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-foreground", children: [
13924
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: "Rows/page" }),
13977
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: labels.rowsPerPage ?? "Rows/page" }),
13925
13978
  /* @__PURE__ */ jsxs(Select, { value: `${pageSize}`, onValueChange: handlePageSizeChange, children: [
13926
13979
  /* @__PURE__ */ jsx(
13927
13980
  SelectTrigger,
@@ -13934,31 +13987,42 @@ function NTablePagination() {
13934
13987
  /* @__PURE__ */ jsx(SelectContent, { side: "top", children: currentPageSizeOptions.map((size) => /* @__PURE__ */ jsx(SelectItem, { value: `${size}`, children: size }, size)) })
13935
13988
  ] })
13936
13989
  ] }),
13937
- /* @__PURE__ */ jsxs("div", { className: "text-sm font-medium text-foreground", children: [
13938
- "Page ",
13939
- pageIndex + 1,
13940
- " of ",
13941
- effectivePageCount
13942
- ] }),
13943
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
13944
- /* @__PURE__ */ jsx(Button, { bordered, variant: "outline", className: "hidden h-8 w-8 p-0 text-foreground disabled:text-muted-foreground disabled:opacity-70 lg:flex", "aria-label": "First page", onClick: () => navigate("first"), disabled: !table.getCanPreviousPage?.() || pageIndex === 0, children: /* @__PURE__ */ jsx(ChevronsLeft, { className: "h-4 w-4" }) }),
13945
- /* @__PURE__ */ jsx(Button, { bordered, variant: "outline", className: "h-8 w-8 p-0 text-foreground disabled:text-muted-foreground disabled:opacity-70", "aria-label": "Previous", onClick: () => navigate("prev"), disabled: !table.getCanPreviousPage?.() || pageIndex === 0, children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" }) }),
13946
- /* @__PURE__ */ jsx(Button, { bordered, variant: "outline", className: "h-8 w-8 p-0 text-foreground disabled:text-muted-foreground disabled:opacity-70", "aria-label": "Next", onClick: () => navigate("next"), disabled: !table.getCanNextPage?.() || pageIndex >= effectivePageCount - 1, children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" }) }),
13947
- /* @__PURE__ */ jsx(Button, { bordered, variant: "outline", className: "hidden h-8 w-8 p-0 text-foreground disabled:text-muted-foreground disabled:opacity-70 lg:flex", "aria-label": "Last page", onClick: () => navigate("last"), disabled: !table.getCanNextPage?.() || pageIndex >= effectivePageCount - 1, children: /* @__PURE__ */ jsx(ChevronsRight, { className: "h-4 w-4" }) })
13948
- ] })
13990
+ /* @__PURE__ */ jsx("div", { className: cn("text-sm font-medium text-foreground", showNumbers && "sm:hidden"), children: labels.pageOf?.(pageIndex + 1, effectivePageCount) ?? `Page ${pageIndex + 1} of ${effectivePageCount}` }),
13991
+ /* @__PURE__ */ jsxs(
13992
+ "nav",
13993
+ {
13994
+ "aria-label": labels.pagination ?? "Pagination",
13995
+ className: "flex items-center gap-2",
13996
+ children: [
13997
+ !showNumbers && /* @__PURE__ */ jsx(Button, { bordered, variant: "outline", className: cn(navButtonClass, "hidden lg:flex"), "aria-label": labels.firstPage ?? "First page", onClick: () => navigate("first"), disabled: !canPrevious, children: /* @__PURE__ */ jsx(ChevronsLeft, { className: chevronClass }) }),
13998
+ /* @__PURE__ */ jsx(Button, { bordered, variant: "outline", className: navButtonClass, "aria-label": labels.previousPage ?? "Previous", onClick: () => navigate("prev"), disabled: !canPrevious, children: /* @__PURE__ */ jsx(ChevronLeft, { className: chevronClass }) }),
13999
+ showNumbers && /* @__PURE__ */ jsx("div", { className: "hidden items-center gap-2 sm:flex", children: /* @__PURE__ */ jsx(
14000
+ PageNumbers,
14001
+ {
14002
+ pageIndex,
14003
+ pageCount: effectivePageCount,
14004
+ bordered,
14005
+ labels,
14006
+ onSelect: (next) => setPagination({ ...currentPagination, pageIndex: next })
14007
+ }
14008
+ ) }),
14009
+ /* @__PURE__ */ jsx(Button, { bordered, variant: "outline", className: navButtonClass, "aria-label": labels.nextPage ?? "Next", onClick: () => navigate("next"), disabled: !canNext, children: /* @__PURE__ */ jsx(ChevronRight, { className: chevronClass }) }),
14010
+ !showNumbers && /* @__PURE__ */ jsx(Button, { bordered, variant: "outline", className: cn(navButtonClass, "hidden lg:flex"), "aria-label": labels.lastPage ?? "Last page", onClick: () => navigate("last"), disabled: !canNext, children: /* @__PURE__ */ jsx(ChevronsRight, { className: chevronClass }) })
14011
+ ]
14012
+ }
14013
+ )
13949
14014
  ] }),
13950
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-none whitespace-nowrap text-sm text-muted-foreground max-sm:hidden", children: [
13951
- selectedRows.length,
13952
- " of ",
13953
- manualPagination && rowCount !== void 0 ? rowCount : filteredRows.length,
13954
- " row(s) selected."
13955
- ] })
14015
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-none whitespace-nowrap text-sm text-muted-foreground max-sm:hidden", children: labels.rowsSelected?.(selectedRows.length, selectedTotal) ?? `${selectedRows.length} of ${selectedTotal} row(s) selected.` })
13956
14016
  ] });
13957
14017
  }
14018
+ function PendingFilter({ placeholder, icon, bordered }) {
14019
+ return /* @__PURE__ */ jsx(TextInput, { icon, value: "", onChange: () => {
14020
+ }, placeholder, bordered, disabled: true });
14021
+ }
13958
14022
  function SearchFilter({ placeholder }) {
13959
14023
  const table = useTableStore.use.table();
13960
14024
  const bordered = useTableStore.use.bordered();
13961
- if (!table) return null;
14025
+ if (!table) return /* @__PURE__ */ jsx(PendingFilter, { icon: Search, placeholder: placeholder ?? "Search\u2026", bordered });
13962
14026
  const value = table.getState().globalFilter ?? "";
13963
14027
  return /* @__PURE__ */ jsx(TextInput, { icon: Search, value, onChange: (v) => table.setGlobalFilter(v), placeholder: placeholder ?? "Search\u2026", bordered });
13964
14028
  }
@@ -13966,14 +14030,14 @@ function TextFilter({ name, placeholder, icon }) {
13966
14030
  const table = useTableStore.use.table();
13967
14031
  const bordered = useTableStore.use.bordered();
13968
14032
  const column = table?.getColumn?.(name);
13969
- if (!column) return null;
14033
+ if (!column) return /* @__PURE__ */ jsx(PendingFilter, { icon, placeholder, bordered });
13970
14034
  return /* @__PURE__ */ jsx(TextInput, { icon, value: column.getFilterValue() ?? "", onChange: (value) => column.setFilterValue(value), placeholder, bordered });
13971
14035
  }
13972
14036
  function SelectFilter({ name, options, placeholder, inputType }) {
13973
14037
  const table = useTableStore.use.table();
13974
14038
  const bordered = useTableStore.use.bordered();
13975
14039
  const column = table?.getColumn?.(name);
13976
- if (!column) return null;
14040
+ if (!column) return /* @__PURE__ */ jsx(PendingFilter, { placeholder: placeholder || "Filter...", bordered });
13977
14041
  const allOptions = [{ value: "__clear__", label: "All" }, ...options.map((o) => typeof o === "string" ? { value: o, label: o } : o)];
13978
14042
  const InputComponent = inputType === "combobox" ? ComboboxInput : SelectInput;
13979
14043
  return /* @__PURE__ */ jsx(InputComponent, { value: column.getFilterValue() ?? "", onChange: (value) => column.setFilterValue(value === "" || value === "__clear__" ? void 0 : value), items: allOptions, placeholder: placeholder || "Filter...", bordered });
@@ -13982,7 +14046,7 @@ function defaultWrapperClass(filter) {
13982
14046
  return filter.type === "search" ? "flex-1 min-w-[160px] max-w-sm" : "w-full sm:w-40 xl:w-56 shrink-0";
13983
14047
  }
13984
14048
  function RenderFilter({ filter, mobilePrimary = false }) {
13985
- const table = useTableStore.use.table();
14049
+ useTableStore.use.table();
13986
14050
  const bordered = useTableStore.use.bordered();
13987
14051
  if (filter.type === "search") {
13988
14052
  return /* @__PURE__ */ jsx(SearchFilter, { placeholder: filter.placeholder });
@@ -14049,20 +14113,24 @@ function RenderFilter({ filter, mobilePrimary = false }) {
14049
14113
  if (filter.type === "text") {
14050
14114
  return /* @__PURE__ */ jsx(TextFilter, { name: filter.name, placeholder: filter.placeholder, icon: mobilePrimary ? Search : void 0 });
14051
14115
  }
14052
- if (!table) return null;
14053
14116
  return /* @__PURE__ */ jsx(SelectFilter, { name: filter.name, options: filter.options || [], placeholder: filter.placeholder, inputType: filter.type });
14054
14117
  }
14055
14118
  function TableFilters() {
14056
14119
  const filters = useTableStore.use.filters();
14057
14120
  if (!filters?.length) return null;
14058
- return /* @__PURE__ */ jsx("div", { "data-ntable-desktop-filters": true, className: "hidden flex-1 min-w-0 flex-wrap items-center gap-2 md:flex", children: filters.map((filter) => /* @__PURE__ */ jsx(
14059
- "div",
14060
- {
14061
- className: cn(defaultWrapperClass(filter), filter.className),
14062
- children: /* @__PURE__ */ jsx(RenderFilter, { filter })
14063
- },
14064
- filter.name
14065
- )) });
14121
+ return (
14122
+ // `min-h-10` is the height of one control row. Even if every filter inside
14123
+ // resolves to nothing, the row cannot collapse and change what the body
14124
+ // measures.
14125
+ /* @__PURE__ */ jsx("div", { "data-ntable-desktop-filters": true, className: "hidden min-h-10 flex-1 min-w-0 flex-wrap items-center gap-2 md:flex", children: filters.map((filter) => /* @__PURE__ */ jsx(
14126
+ "div",
14127
+ {
14128
+ className: cn(defaultWrapperClass(filter), filter.className),
14129
+ children: /* @__PURE__ */ jsx(RenderFilter, { filter })
14130
+ },
14131
+ filter.name
14132
+ )) })
14133
+ );
14066
14134
  }
14067
14135
  function TableAddButton({ mobile = false }) {
14068
14136
  const onAddClick = useTableStore.use.onAddClick();
@@ -14213,12 +14281,14 @@ function NTableHeader() {
14213
14281
  const isCustomMode = useTableStore.use.isCustomMode();
14214
14282
  const showViewToggle = useTableStore.use.showViewToggle();
14215
14283
  const showColumnVisibility = useTableStore.use.showColumnVisibility();
14216
- const hideDataChrome = isLoading && !isRefreshing || error || hasNoData && !isFilteredEmpty;
14284
+ const hideDataChrome = error || hasNoData && !isFilteredEmpty && !isLoading;
14285
+ const isFirstLoad = Boolean(isLoading) && !isRefreshing;
14286
+ const firstLoadChromeClass = isFirstLoad ? "pointer-events-none opacity-60" : void 0;
14217
14287
  if (isCustomMode) {
14218
14288
  if (!showViewToggle && !showColumnVisibility && !headerSlot && !hasControls) return null;
14219
14289
  if (hideDataChrome) return null;
14220
14290
  const justify2 = headerSlot ? "justify-between" : "justify-end";
14221
- return /* @__PURE__ */ jsxs("div", { "data-ntable-header": true, className: cn("flex shrink-0 items-center gap-0 lg:gap-3 flex-wrap lg:flex-nowrap", justify2, classNames?.header), children: [
14291
+ return /* @__PURE__ */ jsxs("div", { "data-ntable-header": true, "aria-busy": isFirstLoad ? "true" : void 0, className: cn("flex shrink-0 items-center gap-0 lg:gap-3 flex-wrap lg:flex-nowrap", justify2, firstLoadChromeClass, classNames?.header), children: [
14222
14292
  headerSlot && /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: headerSlot }),
14223
14293
  hasControls && /* @__PURE__ */ jsxs("div", { className: "flex gap-2 shrink-0", children: [
14224
14294
  /* @__PURE__ */ jsx("span", { className: "hidden md:contents", children: /* @__PURE__ */ jsx(TableSettingsMenu, {}) }),
@@ -14231,7 +14301,7 @@ function NTableHeader() {
14231
14301
  if (!hasFilters && !hasControls && !headerSlot && !hasToolbar) return null;
14232
14302
  if (hideDataChrome) return null;
14233
14303
  const justify = hasControls || headerSlot || hasToolbar ? "justify-between" : "justify-start";
14234
- return /* @__PURE__ */ jsxs("div", { "data-ntable-header": true, className: cn("flex shrink-0 items-center gap-0 lg:gap-3 flex-wrap lg:flex-nowrap", justify, classNames?.header), children: [
14304
+ return /* @__PURE__ */ jsxs("div", { "data-ntable-header": true, "aria-busy": isFirstLoad ? "true" : void 0, className: cn("flex shrink-0 items-center gap-0 lg:gap-3 flex-wrap lg:flex-nowrap", justify, firstLoadChromeClass, classNames?.header), children: [
14235
14305
  /* @__PURE__ */ jsx(TableFilters, {}),
14236
14306
  /* @__PURE__ */ jsx(TableMobileToolbar, {}),
14237
14307
  headerSlot && /* @__PURE__ */ jsx("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: headerSlot }),
@@ -14342,12 +14412,27 @@ function TableLayout(props) {
14342
14412
  "data-ntable-body": true,
14343
14413
  "data-ntable-refreshing": isRefreshing ? "true" : void 0,
14344
14414
  "aria-busy": isRefreshing ? "true" : void 0,
14345
- className: "flex min-h-0 flex-1 flex-col gap-2 overflow-hidden",
14415
+ className: "relative flex min-h-0 flex-1 flex-col gap-2 overflow-hidden",
14346
14416
  children: isCustomMode ? customRenderer ? customRenderer() : null : /* @__PURE__ */ jsxs(Fragment, { children: [
14347
- isLoading && !isRefreshing && (props.renderLoading ? /* @__PURE__ */ jsx(TableStateSlot, { children: props.renderLoading() }) : props.responsiveSkeleton ? /* @__PURE__ */ jsxs(Fragment, { children: [
14348
- /* @__PURE__ */ jsx("div", { "data-ntable-skeleton-variant": "table", className: "hidden min-h-0 flex-1 flex-col lg:flex", children: /* @__PURE__ */ jsx(NTableLoadingSkeleton, {}) }),
14349
- /* @__PURE__ */ jsx("div", { "data-ntable-skeleton-variant": "cards", className: "flex min-h-0 flex-1 flex-col lg:hidden", children: /* @__PURE__ */ jsx(NTableCardsLoadingSkeleton, {}) })
14350
- ] }) : effectiveMode === "table" ? /* @__PURE__ */ jsx(NTableLoadingSkeleton, {}) : effectiveMode === "cards" ? /* @__PURE__ */ jsx(NTableCardsLoadingSkeleton, {}) : /* @__PURE__ */ jsx(NTableLoadingSkeleton, {})),
14417
+ isLoading && !isRefreshing && // Absolutely positioned, the way every grid that measures its own
14418
+ // page size does it: AG Grid paints its loading state as an
14419
+ // overlay, and MUI's DataGrid renders `GridOverlay` on top of the
14420
+ // rows rather than in place of them. A skeleton that occupies a
14421
+ // slot in the layout is a second layout — the one the container
14422
+ // gets measured in — and the page size derived from it is a page
14423
+ // size for a table that no longer exists once rows land. Taking
14424
+ // the skeleton out of flow leaves exactly one layout to measure.
14425
+ /* @__PURE__ */ jsx(
14426
+ "div",
14427
+ {
14428
+ "data-ntable-loading-overlay": true,
14429
+ className: "absolute inset-0 z-10 flex min-h-0 flex-col bg-background",
14430
+ children: props.renderLoading ? /* @__PURE__ */ jsx(TableStateSlot, { children: props.renderLoading() }) : props.responsiveSkeleton ? /* @__PURE__ */ jsxs(Fragment, { children: [
14431
+ /* @__PURE__ */ jsx("div", { "data-ntable-skeleton-variant": "table", className: "hidden min-h-0 flex-1 flex-col lg:flex", children: /* @__PURE__ */ jsx(NTableLoadingSkeleton, {}) }),
14432
+ /* @__PURE__ */ jsx("div", { "data-ntable-skeleton-variant": "cards", className: "flex min-h-0 flex-1 flex-col lg:hidden", children: /* @__PURE__ */ jsx(NTableCardsLoadingSkeleton, {}) })
14433
+ ] }) : effectiveMode === "cards" ? /* @__PURE__ */ jsx(NTableCardsLoadingSkeleton, {}) : /* @__PURE__ */ jsx(NTableLoadingSkeleton, {})
14434
+ }
14435
+ ),
14351
14436
  error && !isLoading && /* @__PURE__ */ jsx(TableStateSlot, { children: props.renderError ? props.renderError(error) : /* @__PURE__ */ jsx(NErrorState, { message: typeof error === "string" ? error : "An error occurred" }) }),
14352
14437
  showFilteredEmpty && /* @__PURE__ */ jsx(TableStateSlot, { children: props.renderFilteredEmpty ? props.renderFilteredEmpty() : renderFilteredEmpty ? renderFilteredEmpty() : props.renderEmpty ? props.renderEmpty() : /* @__PURE__ */ jsx(DefaultTableFilteredEmptyState, {}) }),
14353
14438
  showEmpty && /* @__PURE__ */ jsx(TableStateSlot, { children: props.renderEmpty ? props.renderEmpty() : /* @__PURE__ */ jsx(DefaultTableEmptyState, { title: noDataText }) }),
@@ -14515,6 +14600,8 @@ function NTable(props) {
14515
14600
  defaultPagination: props.defaultPagination,
14516
14601
  onPaginationChange: props.onPaginationChange ?? null,
14517
14602
  cardPagination: props.cardPagination ?? { mode: "paged" },
14603
+ paginationVariant: props.paginationVariant ?? "numbered",
14604
+ paginationLabels: props.paginationLabels ?? {},
14518
14605
  // Row selection
14519
14606
  rowSelection: props.rowSelection,
14520
14607
  defaultRowSelection: props.defaultRowSelection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-kit",
3
- "version": "2.2.8",
3
+ "version": "2.4.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Reusable React UI component package for Najm framework",