najm-kit 2.2.9 → 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,
@@ -13728,6 +13730,51 @@ function NTableCards({ effectiveMode }) {
13728
13730
  ) : null
13729
13731
  ] });
13730
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
+ }
13731
13778
  function CardLoadMorePagination({
13732
13779
  config,
13733
13780
  rowCount,
@@ -13821,6 +13868,44 @@ function CardLoadMorePagination({
13821
13868
  }
13822
13869
  );
13823
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
+ }
13824
13909
  function NTablePagination() {
13825
13910
  const table = useTableStore.use.table();
13826
13911
  const showPagination = useTableStore.use.showPagination();
@@ -13837,6 +13922,8 @@ function NTablePagination() {
13837
13922
  const setPagination = useTableStore.use.setPagination();
13838
13923
  const isPaginationControlled = useTableStore.use.isPaginationControlled();
13839
13924
  const bordered = useTableStore.use.bordered();
13925
+ const paginationVariant = useTableStore.use.paginationVariant();
13926
+ const labels = useTableStore.use.paginationLabels();
13840
13927
  if (!showPagination || effectiveViewMode === "json" || effectiveViewMode === "files") return null;
13841
13928
  if (cardPagination.mode === "all") return null;
13842
13929
  if (effectiveViewMode === "cards" && cardPagination.mode === "infinite") return null;
@@ -13879,10 +13966,15 @@ function NTablePagination() {
13879
13966
  const newSize = Number(value);
13880
13967
  setPagination({ pageIndex: 0, pageSize: newSize });
13881
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;
13882
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: [
13883
13975
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-4 lg:gap-6", children: [
13884
13976
  (!isPaginationControlled || manualPagination) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-foreground", children: [
13885
- /* @__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" }),
13886
13978
  /* @__PURE__ */ jsxs(Select, { value: `${pageSize}`, onValueChange: handlePageSizeChange, children: [
13887
13979
  /* @__PURE__ */ jsx(
13888
13980
  SelectTrigger,
@@ -13895,25 +13987,32 @@ function NTablePagination() {
13895
13987
  /* @__PURE__ */ jsx(SelectContent, { side: "top", children: currentPageSizeOptions.map((size) => /* @__PURE__ */ jsx(SelectItem, { value: `${size}`, children: size }, size)) })
13896
13988
  ] })
13897
13989
  ] }),
13898
- /* @__PURE__ */ jsxs("div", { className: "text-sm font-medium text-foreground", children: [
13899
- "Page ",
13900
- pageIndex + 1,
13901
- " of ",
13902
- effectivePageCount
13903
- ] }),
13904
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
13905
- /* @__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" }) }),
13906
- /* @__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" }) }),
13907
- /* @__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" }) }),
13908
- /* @__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" }) })
13909
- ] })
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
+ )
13910
14014
  ] }),
13911
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-none whitespace-nowrap text-sm text-muted-foreground max-sm:hidden", children: [
13912
- selectedRows.length,
13913
- " of ",
13914
- manualPagination && rowCount !== void 0 ? rowCount : filteredRows.length,
13915
- " row(s) selected."
13916
- ] })
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.` })
13917
14016
  ] });
13918
14017
  }
13919
14018
  function PendingFilter({ placeholder, icon, bordered }) {
@@ -14501,6 +14600,8 @@ function NTable(props) {
14501
14600
  defaultPagination: props.defaultPagination,
14502
14601
  onPaginationChange: props.onPaginationChange ?? null,
14503
14602
  cardPagination: props.cardPagination ?? { mode: "paged" },
14603
+ paginationVariant: props.paginationVariant ?? "numbered",
14604
+ paginationLabels: props.paginationLabels ?? {},
14504
14605
  // Row selection
14505
14606
  rowSelection: props.rowSelection,
14506
14607
  defaultRowSelection: props.defaultRowSelection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-kit",
3
- "version": "2.2.9",
3
+ "version": "2.4.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Reusable React UI component package for Najm framework",