najm-kit 2.1.47 → 2.1.49

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/dist/index.mjs CHANGED
@@ -7274,7 +7274,7 @@ function NSkeleton({ className, ...props }) {
7274
7274
  return /* @__PURE__ */ jsx(
7275
7275
  "div",
7276
7276
  {
7277
- className: cn("animate-pulse rounded-md bg-accent", className),
7277
+ className: cn("animate-pulse rounded-md bg-accent motion-reduce:animate-none", className),
7278
7278
  "aria-hidden": "true",
7279
7279
  ...props
7280
7280
  }
@@ -9850,12 +9850,74 @@ function UploaderRow({ item, onCancel, onRemove }) {
9850
9850
  }
9851
9851
  );
9852
9852
  }
9853
+
9854
+ // src/components/inputs/imagePreview.ts
9855
+ var NON_APPENDABLE_PREFIXES = ["data:", "blob:", "javascript:", "file:"];
9856
+ function isNonAppendable(src) {
9857
+ const lower = src.toLowerCase();
9858
+ return NON_APPENDABLE_PREFIXES.some((prefix) => lower.startsWith(prefix));
9859
+ }
9860
+ function isMeaningful(src) {
9861
+ return typeof src === "string" && src.length > 0;
9862
+ }
9863
+ function appendImageVersion(src, version) {
9864
+ if (!isMeaningful(src)) return src;
9865
+ if (version == null || version === "") return src;
9866
+ if (isNonAppendable(src)) return src;
9867
+ const fragmentIndex = src.indexOf("#");
9868
+ const beforeFragment = fragmentIndex === -1 ? src : src.slice(0, fragmentIndex);
9869
+ const fragment = fragmentIndex === -1 ? "" : src.slice(fragmentIndex);
9870
+ const queryIndex = beforeFragment.indexOf("?");
9871
+ const base = queryIndex === -1 ? beforeFragment : beforeFragment.slice(0, queryIndex);
9872
+ const existingQuery = queryIndex === -1 ? "" : beforeFragment.slice(queryIndex);
9873
+ const separator = existingQuery ? "&" : "?";
9874
+ const versionString = `${separator}v=${encodeURIComponent(String(version))}`;
9875
+ if (!existingQuery && !fragment) {
9876
+ return `${base}${versionString}`;
9877
+ }
9878
+ if (!existingQuery) {
9879
+ return `${base}${versionString}${fragment}`;
9880
+ }
9881
+ if (!fragment) {
9882
+ return `${base}${existingQuery}${versionString}`;
9883
+ }
9884
+ return `${base}${existingQuery}${versionString}${fragment}`;
9885
+ }
9886
+ function appendVersionToCandidate(src, version) {
9887
+ if (version == null || version === "") return src;
9888
+ if (isNonAppendable(src)) return src;
9889
+ return appendImageVersion(src, version);
9890
+ }
9891
+ function buildPreviewCandidates(options) {
9892
+ const seen = /* @__PURE__ */ new Set();
9893
+ const result = [];
9894
+ const push = (src, source) => {
9895
+ if (!isMeaningful(src)) return;
9896
+ if (seen.has(src)) return;
9897
+ seen.add(src);
9898
+ result.push({ src, source });
9899
+ };
9900
+ push(options.value ?? null, "value");
9901
+ push(options.fallback ?? null, "fallback");
9902
+ push(options.defaultImage ?? null, "default");
9903
+ const version = options.imageVersion;
9904
+ if (version == null || version === "") return result;
9905
+ return result.map((candidate) => ({
9906
+ src: appendVersionToCandidate(candidate.src, version),
9907
+ source: candidate.source
9908
+ }));
9909
+ }
9910
+ function candidatesKey(candidates) {
9911
+ return candidates.map((candidate) => `${candidate.source}:${candidate.src}`).join("|");
9912
+ }
9853
9913
  var IMAGE_SIZE_MAP = {
9854
9914
  sm: "w-16 h-16",
9855
9915
  md: "w-24 h-24",
9856
9916
  lg: "w-32 h-32",
9857
9917
  xl: "w-40 h-40"
9858
9918
  };
9919
+ var CONTROL_VISIBILITY = "nimage-input-control";
9920
+ var COMPACT_OVERLAY_VISIBILITY = "nimage-input-compact-overlay";
9859
9921
  function ImageInput({
9860
9922
  value,
9861
9923
  onChange,
@@ -9863,11 +9925,19 @@ function ImageInput({
9863
9925
  previewClassName,
9864
9926
  previewStyle,
9865
9927
  contentClassName,
9928
+ imageClassName,
9866
9929
  showPreview = true,
9867
9930
  previewPosition = "top",
9868
9931
  allowClear = true,
9869
9932
  accept = "image/*",
9870
9933
  defaultImage,
9934
+ fallbackImage,
9935
+ previewAlt,
9936
+ fallbackAlt,
9937
+ unavailableContent,
9938
+ onPreviewError,
9939
+ replaceAriaLabel,
9940
+ clearAriaLabel,
9871
9941
  imageSize = "md",
9872
9942
  imageVersion,
9873
9943
  disabled = false,
@@ -9882,117 +9952,242 @@ function ImageInput({
9882
9952
  buttonLabel = "Upload"
9883
9953
  }) {
9884
9954
  const fileInputRef = useRef(null);
9885
- const [preview, setPreview] = useState(null);
9955
+ const [localFilePreview, setLocalFilePreview] = useState(null);
9956
+ const [failedSources, setFailedSources] = useState(() => /* @__PURE__ */ new Set());
9957
+ const readerTokenRef = useRef(0);
9958
+ const previewCandidates = useMemo(() => {
9959
+ if (value instanceof File) return [];
9960
+ if (typeof value === "string" && value) {
9961
+ return buildPreviewCandidates({
9962
+ value,
9963
+ fallback: fallbackImage ?? null,
9964
+ defaultImage: defaultImage ?? null,
9965
+ imageVersion
9966
+ });
9967
+ }
9968
+ return buildPreviewCandidates({
9969
+ value: null,
9970
+ fallback: null,
9971
+ defaultImage: defaultImage ?? null,
9972
+ imageVersion
9973
+ });
9974
+ }, [value, fallbackImage, defaultImage, imageVersion]);
9975
+ const candidateKeyValue = useMemo(
9976
+ () => candidatesKey(previewCandidates),
9977
+ [previewCandidates]
9978
+ );
9979
+ const [trackedKey, setTrackedKey] = useState(candidateKeyValue);
9980
+ if (trackedKey !== candidateKeyValue) {
9981
+ setTrackedKey(candidateKeyValue);
9982
+ setFailedSources(/* @__PURE__ */ new Set());
9983
+ }
9886
9984
  useEffect(() => {
9887
- if (value instanceof File) {
9888
- const reader = new FileReader();
9889
- reader.onloadend = () => setPreview(reader.result);
9890
- reader.readAsDataURL(value);
9891
- } else if (typeof value === "string" && value) {
9892
- const url = imageVersion != null ? `${value}?v=${imageVersion}` : value;
9893
- setPreview(url);
9894
- } else {
9895
- setPreview(null);
9985
+ if (!(value instanceof File)) {
9986
+ setLocalFilePreview(null);
9987
+ return void 0;
9988
+ }
9989
+ const token = ++readerTokenRef.current;
9990
+ const reader = new FileReader();
9991
+ reader.onloadend = () => {
9992
+ if (token !== readerTokenRef.current) return;
9993
+ if (typeof reader.result === "string") {
9994
+ setLocalFilePreview(reader.result);
9995
+ }
9996
+ };
9997
+ reader.onerror = () => {
9998
+ if (token !== readerTokenRef.current) return;
9999
+ setLocalFilePreview(null);
10000
+ };
10001
+ reader.readAsDataURL(value);
10002
+ return () => {
10003
+ if (token === readerTokenRef.current) {
10004
+ readerTokenRef.current = token - 1;
10005
+ }
10006
+ };
10007
+ }, [value]);
10008
+ const activeCandidate = useMemo(() => {
10009
+ if (value instanceof File) return null;
10010
+ for (const candidate of previewCandidates) {
10011
+ if (!failedSources.has(candidate.src)) return candidate;
9896
10012
  }
9897
- }, [value, imageVersion]);
10013
+ return null;
10014
+ }, [previewCandidates, failedSources, value]);
10015
+ const handleCandidateError = (candidate) => {
10016
+ setFailedSources((prev) => {
10017
+ if (prev.has(candidate.src)) return prev;
10018
+ const next = new Set(prev);
10019
+ next.add(candidate.src);
10020
+ return next;
10021
+ });
10022
+ onPreviewError?.({ source: candidate.source, src: candidate.src });
10023
+ };
9898
10024
  const handleClick = () => {
9899
- if (!disabled) fileInputRef.current?.click();
10025
+ if (disabled) return;
10026
+ fileInputRef.current?.click();
9900
10027
  };
9901
10028
  const handleChange = (e) => {
9902
10029
  const file = e.target.files?.[0] || null;
9903
10030
  if (file) onChange(file);
10031
+ e.target.value = "";
9904
10032
  };
9905
10033
  const handleClear = (e) => {
9906
10034
  e.stopPropagation();
10035
+ if (disabled) return;
9907
10036
  onChange(null);
9908
- setPreview(null);
10037
+ setLocalFilePreview(null);
9909
10038
  if (fileInputRef.current) fileInputRef.current.value = "";
9910
10039
  };
10040
+ const handleImgError = () => {
10041
+ if (value instanceof File) return;
10042
+ if (activeCandidate) handleCandidateError(activeCandidate);
10043
+ };
9911
10044
  const effectiveSize = previewClassName || IMAGE_SIZE_MAP[imageSize];
9912
10045
  const isDropzone = !!previewClassName;
9913
10046
  const effectiveReplaceSubtitle = replaceSubtitle ?? subtitle;
10047
+ const primaryAlt = previewAlt ?? replaceTitle ?? "Preview";
10048
+ const secondaryAlt = fallbackAlt ?? previewAlt ?? replaceTitle ?? "Preview";
10049
+ const replaceAccessibleName = replaceAriaLabel ?? replaceTitle;
10050
+ const clearAccessibleName = clearAriaLabel ?? "Remove image";
10051
+ let dataState = "empty";
10052
+ let previewSrc = null;
10053
+ let previewAltText = primaryAlt;
10054
+ if (value instanceof File) {
10055
+ dataState = localFilePreview ? "preview" : "empty";
10056
+ previewSrc = localFilePreview;
10057
+ previewAltText = primaryAlt;
10058
+ } else if (activeCandidate) {
10059
+ dataState = activeCandidate.source === "value" ? "preview" : "fallback";
10060
+ previewSrc = activeCandidate.src;
10061
+ previewAltText = activeCandidate.source === "value" ? primaryAlt : secondaryAlt;
10062
+ } else if (previewCandidates.length > 0) {
10063
+ dataState = "unavailable";
10064
+ }
10065
+ const renderUnavailableContent = () => unavailableContent ?? /* @__PURE__ */ jsxs(Fragment, { children: [
10066
+ /* @__PURE__ */ jsx(Image, { className: "h-10 w-10 text-muted-foreground/50", "aria-hidden": true }),
10067
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Image unavailable" })
10068
+ ] });
10069
+ const renderClearButton = (compact) => {
10070
+ if (!allowClear) return null;
10071
+ return /* @__PURE__ */ jsx(
10072
+ "button",
10073
+ {
10074
+ type: "button",
10075
+ onClick: handleClear,
10076
+ disabled,
10077
+ "aria-label": clearAccessibleName,
10078
+ className: cn(
10079
+ "absolute top-2 end-2 z-10 flex items-center justify-center rounded-full bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
10080
+ compact ? "p-1.5" : "h-6 w-6",
10081
+ CONTROL_VISIBILITY
10082
+ ),
10083
+ children: /* @__PURE__ */ jsx(X, { className: compact ? "h-4 w-4" : "h-3.5 w-3.5" })
10084
+ }
10085
+ );
10086
+ };
9914
10087
  const renderPreview = () => /* @__PURE__ */ jsx(
9915
10088
  "div",
9916
10089
  {
9917
10090
  style: previewStyle,
10091
+ "data-image-input-state": dataState,
9918
10092
  className: cn(
9919
- "flex relative group rounded-lg overflow-hidden border-2 border-dashed border-muted-foreground/60 hover:border-primary transition-colors",
10093
+ "group/image flex relative rounded-lg overflow-hidden border-2 border-dashed border-muted-foreground/60 hover:border-primary transition-colors",
9920
10094
  effectiveSize
9921
10095
  ),
9922
- children: preview ? isDropzone ? /* @__PURE__ */ jsxs(Fragment, { children: [
9923
- /* @__PURE__ */ jsx("img", { src: preview, alt: "Preview", className: "absolute inset-0 w-full h-full object-cover" }),
9924
- /* @__PURE__ */ jsxs("div", { className: cn("relative w-full h-full flex flex-col items-center justify-center gap-1.5 bg-black/40 text-white px-6 py-8 text-center", contentClassName), children: [
9925
- /* @__PURE__ */ jsx("span", { className: cn("text-sm font-medium", titleClassName), children: replaceTitle }),
9926
- effectiveReplaceSubtitle ? /* @__PURE__ */ jsx("span", { className: cn("text-xs opacity-80", subtitleClassName), children: effectiveReplaceSubtitle }) : null,
9927
- allowClear && /* @__PURE__ */ jsx(
9928
- "button",
9929
- {
9930
- type: "button",
9931
- onClick: handleClear,
9932
- disabled,
9933
- className: "absolute top-2 right-2 flex h-6 w-6 items-center justify-center rounded-full bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
9934
- "aria-label": "Remove image",
9935
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
9936
- }
9937
- )
9938
- ] })
9939
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
9940
- /* @__PURE__ */ jsx("img", { src: preview, alt: "Preview", className: "w-full h-full object-cover" }),
10096
+ children: previewSrc ? isDropzone ? /* @__PURE__ */ jsxs(Fragment, { children: [
9941
10097
  /* @__PURE__ */ jsx(
9942
- "div",
10098
+ "img",
9943
10099
  {
9944
- onClick: handleClick,
9945
- className: "absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer",
9946
- children: /* @__PURE__ */ jsx(Upload, { className: "h-8 w-8 text-white" })
10100
+ src: previewSrc,
10101
+ alt: previewAltText,
10102
+ onError: handleImgError,
10103
+ className: cn(
10104
+ "absolute inset-0 w-full h-full object-cover",
10105
+ imageClassName
10106
+ )
9947
10107
  }
9948
10108
  ),
9949
- allowClear && /* @__PURE__ */ jsx(
10109
+ /* @__PURE__ */ jsxs(
9950
10110
  "button",
9951
10111
  {
9952
10112
  type: "button",
9953
- onClick: handleClear,
10113
+ onClick: handleClick,
9954
10114
  disabled,
9955
- className: "absolute top-2 right-2 p-1.5 bg-destructive text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-md hover:bg-destructive/90 z-10",
9956
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
10115
+ "aria-label": replaceAccessibleName,
10116
+ className: cn(
10117
+ "relative w-full h-full flex flex-col items-center justify-center gap-1.5 bg-black/40 text-white px-6 py-8 text-center cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
10118
+ contentClassName
10119
+ ),
10120
+ children: [
10121
+ /* @__PURE__ */ jsx("span", { className: cn("text-sm font-medium", titleClassName), children: replaceTitle }),
10122
+ effectiveReplaceSubtitle ? /* @__PURE__ */ jsx("span", { className: cn("text-xs opacity-80", subtitleClassName), children: effectiveReplaceSubtitle }) : null
10123
+ ]
9957
10124
  }
9958
- )
9959
- ] }) : defaultImage ? /* @__PURE__ */ jsxs(Fragment, { children: [
9960
- /* @__PURE__ */ jsx("img", { src: defaultImage, alt: "Default", className: "w-full h-full object-cover" }),
10125
+ ),
10126
+ renderClearButton(false)
10127
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
9961
10128
  /* @__PURE__ */ jsx(
9962
- "div",
10129
+ "img",
10130
+ {
10131
+ src: previewSrc,
10132
+ alt: previewAltText,
10133
+ onError: handleImgError,
10134
+ className: cn("w-full h-full", imageClassName ?? "object-cover")
10135
+ }
10136
+ ),
10137
+ /* @__PURE__ */ jsx(
10138
+ "button",
9963
10139
  {
10140
+ type: "button",
9964
10141
  onClick: handleClick,
9965
- className: "absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer",
9966
- children: /* @__PURE__ */ jsx(Upload, { className: "h-8 w-8 text-white" })
10142
+ disabled,
10143
+ "aria-label": replaceAccessibleName,
10144
+ className: cn(
10145
+ "absolute inset-0 bg-black/50 flex items-center justify-center cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
10146
+ COMPACT_OVERLAY_VISIBILITY
10147
+ ),
10148
+ children: /* @__PURE__ */ jsx(Upload, { className: "h-8 w-8 text-white", "aria-hidden": true })
9967
10149
  }
9968
- )
9969
- ] }) : isDropzone ? /* @__PURE__ */ jsxs(
10150
+ ),
10151
+ renderClearButton(true)
10152
+ ] }) : dataState === "unavailable" ? /* @__PURE__ */ jsx(
9970
10153
  "div",
9971
10154
  {
10155
+ "data-image-input-unavailable": true,
10156
+ className: cn(
10157
+ "w-full h-full flex flex-col items-center justify-center gap-2 bg-muted/30 text-muted-foreground px-6 py-8 text-center",
10158
+ contentClassName
10159
+ ),
10160
+ children: renderUnavailableContent()
10161
+ }
10162
+ ) : isDropzone ? /* @__PURE__ */ jsxs(
10163
+ "button",
10164
+ {
10165
+ type: "button",
9972
10166
  onClick: handleClick,
9973
- className: cn("w-full h-full flex flex-col items-center justify-center gap-2 cursor-pointer bg-muted/30 hover:bg-muted/50 transition-colors px-6 py-8 text-center", contentClassName),
10167
+ disabled,
10168
+ "aria-label": replaceAriaLabel ?? title,
10169
+ className: cn(
10170
+ "w-full h-full flex flex-col items-center justify-center gap-2 cursor-pointer bg-muted/30 hover:bg-muted/50 transition-colors px-6 py-8 text-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
10171
+ contentClassName
10172
+ ),
9974
10173
  children: [
9975
10174
  (trigger === "icon" || trigger === "both") && /* @__PURE__ */ jsx("div", { className: "text-primary", children: uploadIcon ?? /* @__PURE__ */ jsx(Plus, { className: "h-8 w-8" }) }),
9976
- (trigger === "button" || trigger === "both") && /* @__PURE__ */ jsx(
9977
- "span",
9978
- {
9979
- role: "button",
9980
- className: "inline-flex items-center justify-center rounded-md border border-input bg-background px-3 py-1.5 text-xs font-medium text-foreground shadow-sm hover:bg-accent hover:text-accent-foreground",
9981
- children: buttonLabel
9982
- }
9983
- ),
10175
+ (trigger === "button" || trigger === "both") && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center justify-center rounded-md border border-input bg-background px-3 py-1.5 text-xs font-medium text-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", children: buttonLabel }),
9984
10176
  /* @__PURE__ */ jsx("span", { className: cn("text-sm font-medium text-foreground", titleClassName), children: title }),
9985
10177
  subtitle ? /* @__PURE__ */ jsx("span", { className: cn("text-xs text-muted-foreground", subtitleClassName), children: subtitle }) : null
9986
10178
  ]
9987
10179
  }
9988
10180
  ) : /* @__PURE__ */ jsxs(
9989
- "div",
10181
+ "button",
9990
10182
  {
10183
+ type: "button",
9991
10184
  onClick: handleClick,
9992
- className: "w-full h-full flex flex-col items-center justify-center cursor-pointer bg-muted/30 hover:bg-muted/50 transition-colors",
10185
+ disabled,
10186
+ "aria-label": replaceAriaLabel ?? title,
10187
+ className: "w-full h-full flex flex-col items-center justify-center cursor-pointer bg-muted/30 hover:bg-muted/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
9993
10188
  children: [
9994
- /* @__PURE__ */ jsx(Image, { className: "h-12 w-12 text-muted-foreground/50 mb-2" }),
9995
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground text-center px-2", children: "Click to upload" })
10189
+ /* @__PURE__ */ jsx(Image, { className: "h-12 w-12 text-muted-foreground/50 mb-2", "aria-hidden": true }),
10190
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground text-center px-2", children: title })
9996
10191
  ]
9997
10192
  }
9998
10193
  )
@@ -10006,20 +10201,42 @@ function ImageInput({
10006
10201
  onChange: handleChange,
10007
10202
  className: "hidden",
10008
10203
  accept,
10009
- disabled
10204
+ disabled,
10205
+ "aria-hidden": true,
10206
+ tabIndex: -1
10010
10207
  }
10011
10208
  );
10012
10209
  if (!showPreview) return renderFileInput();
10013
10210
  if (previewPosition === "left" || previewPosition === "right") {
10014
- return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-4", previewPosition === "right" && "flex-row-reverse", containerClassName), children: [
10015
- renderPreview(),
10016
- /* @__PURE__ */ jsx("div", { className: "flex-1", children: renderFileInput() })
10017
- ] });
10211
+ return /* @__PURE__ */ jsxs(
10212
+ "div",
10213
+ {
10214
+ className: cn(
10215
+ "flex items-center gap-4",
10216
+ previewPosition === "right" && "flex-row-reverse",
10217
+ containerClassName
10218
+ ),
10219
+ children: [
10220
+ renderPreview(),
10221
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: renderFileInput() })
10222
+ ]
10223
+ }
10224
+ );
10018
10225
  }
10019
- return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-3", previewPosition === "bottom" && "flex-col-reverse", containerClassName), children: [
10020
- renderPreview(),
10021
- renderFileInput()
10022
- ] });
10226
+ return /* @__PURE__ */ jsxs(
10227
+ "div",
10228
+ {
10229
+ className: cn(
10230
+ "flex flex-col gap-3",
10231
+ previewPosition === "bottom" && "flex-col-reverse",
10232
+ containerClassName
10233
+ ),
10234
+ children: [
10235
+ renderPreview(),
10236
+ renderFileInput()
10237
+ ]
10238
+ }
10239
+ );
10023
10240
  }
10024
10241
  var AVATAR_SIZE_MAP = {
10025
10242
  sm: "size-16",
@@ -11629,7 +11846,14 @@ var createTableStore = () => {
11629
11846
  addButtonText: "",
11630
11847
  pageSizeOptions: [10, 20, 30, 40, 50],
11631
11848
  calculatedPageSize: 10,
11849
+ skeletonRowCount: 6,
11632
11850
  maxHeight: null,
11851
+ bodyWidth: 0,
11852
+ bodyHeight: 0,
11853
+ tableHeaderHeight: 48,
11854
+ cardColumnCount: 1,
11855
+ cardRowHeight: 0,
11856
+ cardGap: 12,
11633
11857
  // JSON mode
11634
11858
  jsonValue: void 0,
11635
11859
  jsonColors: null,
@@ -11699,6 +11923,8 @@ var createTableStore = () => {
11699
11923
  // Responsive cards
11700
11924
  responsiveCards: true,
11701
11925
  isMobile: false,
11926
+ effectiveViewMode: "table",
11927
+ cardPagination: { mode: "paged" },
11702
11928
  // Empty states
11703
11929
  isEmpty: void 0,
11704
11930
  isFilteredEmpty: false,
@@ -11774,6 +12000,8 @@ function filterResponsiveColumns(columns) {
11774
12000
  // src/components/table/hooks.ts
11775
12001
  var ROW_HEIGHT = 56;
11776
12002
  var DEFAULT_TABLE_HEADER_HEIGHT = 48;
12003
+ var DEFAULT_CARD_HEIGHT = 176;
12004
+ var DEFAULT_CARD_GAP = 12;
11777
12005
  var ROOT_SECTION_GAP_COUNT = 2;
11778
12006
  function useStoreSync(props) {
11779
12007
  const storeRef = useRef(null);
@@ -11854,23 +12082,43 @@ function calculateDynamicPageSize(input) {
11854
12082
  if (availableRowsHeight <= 0) return 1;
11855
12083
  return Math.max(1, Math.floor(availableRowsHeight / rowHeight));
11856
12084
  }
11857
- function useDynamicPageSize(containerRef) {
12085
+ function calculateCardSkeletonCount(input) {
12086
+ const columns = Math.max(1, Math.floor(input.columnCount));
12087
+ const cardHeight = Math.max(1, input.cardHeight ?? DEFAULT_CARD_HEIGHT);
12088
+ const gap = Math.max(0, input.gap ?? DEFAULT_CARD_GAP);
12089
+ if (input.bodyHeight <= 0) return columns;
12090
+ const rows = Math.max(1, Math.ceil((input.bodyHeight + gap) / (cardHeight + gap)));
12091
+ return rows * columns;
12092
+ }
12093
+ function fallbackCardColumns(width) {
12094
+ if (width >= 1280) return 4;
12095
+ if (width >= 1024) return 3;
12096
+ if (width >= 640) return 2;
12097
+ return 1;
12098
+ }
12099
+ function useDynamicPageSize(containerRef, effectiveViewMode) {
11858
12100
  const dynamicHeight = useTableStore.use.dynamicHeight();
11859
- const viewMode = useTableStore.use.viewMode();
12101
+ const viewMode = useTableStore.use.effectiveViewMode();
11860
12102
  const manualPagination = useTableStore.use.manualPagination();
11861
12103
  const isLoading = useTableStore.use.isLoading();
11862
12104
  const error = useTableStore.use.error();
11863
12105
  const hasNoData = useTableStore.use.hasNoData();
11864
12106
  const isFilteredEmpty = useTableStore.use.isFilteredEmpty();
11865
12107
  const syncWithProps = useTableStore.use.syncWithProps();
12108
+ const lastMeasurementRef = useRef("");
11866
12109
  useLayoutEffect(() => {
11867
- if (!dynamicHeight || !containerRef.current || viewMode !== "table" || manualPagination) return;
12110
+ if (!dynamicHeight || !containerRef.current) return;
11868
12111
  const calculatePageSize = () => {
11869
12112
  const container2 = containerRef.current;
11870
12113
  if (!container2) return;
11871
12114
  const bodyEl = container2.querySelector("[data-ntable-body]");
11872
12115
  const tableHeaderEl = container2.querySelector("[data-ntable-table-header]");
12116
+ const loadingHeaderEl = container2.querySelector("[data-ntable-loading-header]");
12117
+ const cardsGridEl = container2.querySelector(
12118
+ "[data-ntable-loading-cards-grid], [data-ntable-cards-grid]"
12119
+ );
11873
12120
  let bodyHeight = bodyEl?.clientHeight ?? 0;
12121
+ const bodyWidth = bodyEl?.clientWidth ?? container2.clientWidth ?? 0;
11874
12122
  if (!bodyHeight) {
11875
12123
  const rootHeight = container2.clientHeight;
11876
12124
  const headerHeight = container2.querySelector("[data-ntable-header]")?.offsetHeight ?? 0;
@@ -11879,10 +12127,36 @@ function useDynamicPageSize(containerRef) {
11879
12127
  const gap = Number.parseFloat(rootStyles.rowGap || rootStyles.gap || "0") || 0;
11880
12128
  bodyHeight = rootHeight - headerHeight - paginationHeight - gap * ROOT_SECTION_GAP_COUNT;
11881
12129
  }
12130
+ if (loadingHeaderEl && bodyEl) {
12131
+ const bodyStyles = window.getComputedStyle(bodyEl);
12132
+ const bodyGap = Number.parseFloat(bodyStyles.rowGap || bodyStyles.gap || "0") || 0;
12133
+ bodyHeight = Math.max(0, bodyHeight - loadingHeaderEl.offsetHeight - bodyGap);
12134
+ }
11882
12135
  const tableHeaderHeight = tableHeaderEl?.offsetHeight ?? DEFAULT_TABLE_HEADER_HEIGHT;
11883
12136
  const newPageSize = calculateDynamicPageSize({ bodyHeight, tableHeaderHeight });
11884
12137
  const calculatedMaxHeight = tableHeaderHeight + newPageSize * ROW_HEIGHT;
11885
- syncWithProps({ calculatedPageSize: newPageSize, maxHeight: calculatedMaxHeight });
12138
+ const gridStyles = cardsGridEl ? window.getComputedStyle(cardsGridEl) : null;
12139
+ const gridTemplateColumns = gridStyles?.gridTemplateColumns;
12140
+ const gridColumns = gridTemplateColumns && gridTemplateColumns !== "none" ? gridTemplateColumns.split(" ").filter(Boolean).length : 0;
12141
+ const cardColumnCount = gridColumns || fallbackCardColumns(bodyWidth);
12142
+ const cardGap = Number.parseFloat(gridStyles?.rowGap || gridStyles?.gap || "") || DEFAULT_CARD_GAP;
12143
+ const firstCard = cardsGridEl?.querySelector("[data-ntable-loading-card], [data-row]");
12144
+ const cardRowHeight = firstCard?.offsetHeight || firstCard?.getBoundingClientRect().height || DEFAULT_CARD_HEIGHT;
12145
+ const updates = {
12146
+ ...!manualPagination ? { calculatedPageSize: newPageSize, maxHeight: calculatedMaxHeight } : {},
12147
+ skeletonRowCount: newPageSize,
12148
+ bodyWidth,
12149
+ bodyHeight,
12150
+ tableHeaderHeight,
12151
+ cardColumnCount,
12152
+ cardRowHeight,
12153
+ cardGap
12154
+ };
12155
+ const fingerprint = JSON.stringify(updates);
12156
+ if (fingerprint !== lastMeasurementRef.current) {
12157
+ lastMeasurementRef.current = fingerprint;
12158
+ syncWithProps(updates);
12159
+ }
11886
12160
  };
11887
12161
  calculatePageSize();
11888
12162
  const resizeObserver = new ResizeObserver(calculatePageSize);
@@ -11891,11 +12165,14 @@ function useDynamicPageSize(containerRef) {
11891
12165
  container.querySelectorAll(
11892
12166
  "[data-ntable-header], [data-ntable-body], [data-ntable-pagination], [data-ntable-table-header]"
11893
12167
  ).forEach((el) => resizeObserver.observe(el));
12168
+ container.querySelectorAll(
12169
+ "[data-ntable-loading-header], [data-ntable-loading-cards-grid], [data-ntable-loading-card], [data-ntable-cards-grid]"
12170
+ ).forEach((el) => resizeObserver.observe(el));
11894
12171
  if (container.parentElement) resizeObserver.observe(container.parentElement);
11895
12172
  return () => resizeObserver.disconnect();
11896
- }, [dynamicHeight, viewMode, containerRef, syncWithProps, manualPagination, isLoading, error, hasNoData, isFilteredEmpty]);
12173
+ }, [dynamicHeight, effectiveViewMode, viewMode, containerRef, syncWithProps, manualPagination, isLoading, error, hasNoData, isFilteredEmpty]);
11897
12174
  }
11898
- function useTable() {
12175
+ function useTable(effectiveViewModeOverride) {
11899
12176
  const [sorting, setSorting] = useState([]);
11900
12177
  const [columnFilters, setColumnFilters] = useState([]);
11901
12178
  const [columnVisibility, setColumnVisibility] = useState({});
@@ -11912,6 +12189,8 @@ function useTable() {
11912
12189
  const CardComponent = useTableStore.use.CardComponent();
11913
12190
  const dynamicHeight = useTableStore.use.dynamicHeight();
11914
12191
  const viewMode = useTableStore.use.viewMode();
12192
+ const effectiveViewMode = useTableStore.use.effectiveViewMode();
12193
+ const cardPagination = useTableStore.use.cardPagination();
11915
12194
  const calculatedPageSize = useTableStore.use.calculatedPageSize();
11916
12195
  const syncWithProps = useTableStore.use.syncWithProps();
11917
12196
  const onStateChange = useTableStore.use.onStateChange();
@@ -12002,6 +12281,8 @@ function useTable() {
12002
12281
  notifyStateChange({ sorting, columnFilters, columnVisibility, rowSelection: storeRowSelection, globalFilter });
12003
12282
  }, [storePagination, storeRowSelection, setPagination, sorting, columnFilters, columnVisibility, globalFilter, notifyStateChange]);
12004
12283
  const hasExpansion = Boolean(renderSubRow || userGetRowCanExpand);
12284
+ const renderedMode = effectiveViewModeOverride ?? effectiveViewMode ?? viewMode;
12285
+ const renderAllSuppliedRows = renderedMode === "cards" && cardPagination.mode !== "paged";
12005
12286
  const tableConfig = {
12006
12287
  data,
12007
12288
  columns: finalColumns,
@@ -12018,7 +12299,7 @@ function useTable() {
12018
12299
  getPaginationRowModel: getPaginationRowModel(),
12019
12300
  getSortedRowModel: getSortedRowModel(),
12020
12301
  getExpandedRowModel: getExpandedRowModel(),
12021
- manualPagination,
12302
+ manualPagination: manualPagination || renderAllSuppliedRows,
12022
12303
  pageCount,
12023
12304
  rowCount
12024
12305
  };
@@ -12032,9 +12313,11 @@ function useTable() {
12032
12313
  }, [table]);
12033
12314
  useLayoutEffect(() => {
12034
12315
  if (manualPagination) return;
12035
- if (dynamicHeight && viewMode === "table") table.setPageSize(calculatedPageSize);
12036
- if (viewMode === "cards") table.setPageSize(data.length || 9999);
12037
- }, [calculatedPageSize, dynamicHeight, viewMode, table, data.length, manualPagination]);
12316
+ if (dynamicHeight && renderedMode === "table") table.setPageSize(calculatedPageSize);
12317
+ if (viewMode === "cards" && cardPagination.mode === "paged") {
12318
+ table.setPageSize(data.length || 9999);
12319
+ }
12320
+ }, [calculatedPageSize, dynamicHeight, renderedMode, viewMode, cardPagination.mode, table, data.length, manualPagination]);
12038
12321
  return { table, finalColumns, sorting, setSorting, columnFilters, setColumnFilters, columnVisibility, setColumnVisibility, globalFilter, setGlobalFilter };
12039
12322
  }
12040
12323
  function useTableKeyboard(options = {}) {
@@ -12128,6 +12411,26 @@ function resolveTableColor(value, fallback) {
12128
12411
  }
12129
12412
  return color;
12130
12413
  }
12414
+
12415
+ // src/components/table/tableSurface.ts
12416
+ function useTableSurfaceAppearance(bordered, borderColor) {
12417
+ const recipe = useNajmComponentStyle("table");
12418
+ const recipeRadius = resolveRadiusValue(recipe?.radius);
12419
+ const resolvedBorderColor = resolveTableColor(borderColor, DEFAULT_TABLE_BORDER_COLOR);
12420
+ const style = recipeRadius || bordered !== false && (recipe?.borderWidth || borderColor) ? {
12421
+ ...recipeRadius ? { borderRadius: recipeRadius } : {},
12422
+ ...bordered !== false && recipe?.borderWidth ? { borderWidth: recipe.borderWidth } : {},
12423
+ ...bordered !== false && borderColor ? { borderColor: resolvedBorderColor } : {}
12424
+ } : void 0;
12425
+ return {
12426
+ bordered,
12427
+ style,
12428
+ className: cn(
12429
+ "bg-card",
12430
+ bordered === true ? surfaceBorderClasses(true) : "border-0 shadow-sm"
12431
+ )
12432
+ };
12433
+ }
12131
12434
  var ROW_CONTEXT_HANDLED = "__ntableRowContextHandled";
12132
12435
  function EditableCell({ cell, onCellEdit }) {
12133
12436
  const columnDef = cell.column.columnDef;
@@ -12182,12 +12485,6 @@ function EditableCell({ cell, onCellEdit }) {
12182
12485
  ] });
12183
12486
  }
12184
12487
  function NTableContent({ effectiveMode }) {
12185
- const recipe = useNajmComponentStyle("table");
12186
- const recipeRadius = resolveRadiusValue(recipe?.radius);
12187
- const recipeStyle = recipeRadius || recipe?.borderWidth ? {
12188
- ...recipeRadius ? { borderRadius: recipeRadius } : {},
12189
- ...recipe?.borderWidth ? { borderWidth: recipe.borderWidth } : {}
12190
- } : void 0;
12191
12488
  const table = useTableStore.use.table();
12192
12489
  const storeIsTableView = useTableStore.use.isTableView();
12193
12490
  const columns = useTableStore.use.columns();
@@ -12204,10 +12501,6 @@ function NTableContent({ effectiveMode }) {
12204
12501
  backgroundColor: resolvedHeaderColor,
12205
12502
  color: resolvedHeaderTextColor
12206
12503
  };
12207
- const contentStyle = recipeStyle || tableBorderColor ? {
12208
- ...recipeStyle ?? {},
12209
- ...tableBorderColor ? { borderColor: resolvedBorderColor } : {}
12210
- } : void 0;
12211
12504
  const rowBorderStyle = tableBorderColor ? { borderColor: resolvedBorderColor } : void 0;
12212
12505
  const onRowClick = useTableStore.use.onRowClick();
12213
12506
  const onRowContextMenu = useTableStore.use.onRowContextMenu();
@@ -12220,6 +12513,7 @@ function NTableContent({ effectiveMode }) {
12220
12513
  const showContent = useTableStore.use.showContent();
12221
12514
  const classNames = useTableStore.use.classNames();
12222
12515
  const bordered = useTableStore.use.bordered();
12516
+ const surface = useTableSurfaceAppearance(bordered, tableBorderColor);
12223
12517
  const showCheckbox = useTableStore.use.showCheckbox();
12224
12518
  const selectedRowId = useTableStore.use.selectedRowId();
12225
12519
  const renderSubRow = useTableStore.use.renderSubRow();
@@ -12248,11 +12542,11 @@ function NTableContent({ effectiveMode }) {
12248
12542
  axis: "both",
12249
12543
  "data-bordered": bordered === false ? "false" : bordered ? "true" : void 0,
12250
12544
  className: cn(
12251
- "min-h-0 flex-1 overflow-hidden rounded-md bg-card",
12252
- bordered === true ? surfaceBorderClasses(true) : "shadow-sm",
12545
+ "min-h-0 flex-1 overflow-hidden rounded-md",
12546
+ surface.className,
12253
12547
  classNames?.content
12254
12548
  ),
12255
- style: contentStyle,
12549
+ style: surface.style,
12256
12550
  onContextMenu: handleBackgroundContextMenu,
12257
12551
  children: /* @__PURE__ */ jsxs(Table, { children: [
12258
12552
  /* @__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: [
@@ -12361,13 +12655,8 @@ function NTableContent({ effectiveMode }) {
12361
12655
  }
12362
12656
  );
12363
12657
  }
12364
- function NDataCardShell({ row, onClick, onContextMenu, actions, children, className, showCheckbox = true, selectedRowId, openRowMenu, menuButton, bordered }) {
12365
- const recipe = useNajmComponentStyle("table");
12366
- const recipeRadius = resolveRadiusValue(recipe?.radius);
12367
- const recipeStyle = recipeRadius || recipe?.borderWidth ? {
12368
- ...recipeRadius ? { borderRadius: recipeRadius } : {},
12369
- ...recipe?.borderWidth ? { borderWidth: recipe.borderWidth } : {}
12370
- } : void 0;
12658
+ function NDataCardShell({ row, onClick, onContextMenu, actions, children, className, showCheckbox = true, selectedRowId, openRowMenu, menuButton, bordered, borderColor }) {
12659
+ const surface = useTableSurfaceAppearance(bordered, borderColor);
12371
12660
  const canExpand = row.getCanExpand();
12372
12661
  const isExpanded = canExpand && row.getIsExpanded();
12373
12662
  const isSelected = row.getIsSelected();
@@ -12382,10 +12671,10 @@ function NDataCardShell({ row, onClick, onContextMenu, actions, children, classN
12382
12671
  "data-bordered": bordered === false ? "false" : bordered ? "true" : void 0,
12383
12672
  onClick,
12384
12673
  onContextMenu,
12385
- style: recipeStyle,
12674
+ style: surface.style,
12386
12675
  className: cn(
12387
12676
  "relative group w-full rounded-lg bg-card text-card-foreground overflow-hidden",
12388
- surfaceBorderClasses(bordered),
12677
+ surface.className,
12389
12678
  isActive && (bordered ? "border-primary" : "ring-2 ring-primary ring-offset-1 ring-offset-background"),
12390
12679
  onClick && "cursor-pointer",
12391
12680
  className
@@ -12402,7 +12691,7 @@ function NDataCardShell({ row, onClick, onContextMenu, actions, children, classN
12402
12691
  className: "h-4 w-4"
12403
12692
  }
12404
12693
  ) }),
12405
- useMenuButton ? /* @__PURE__ */ jsx("div", { className: "absolute top-2 right-2 h-auto z-10 opacity-0 transition-opacity duration-200 group-hover:opacity-100 focus-within:opacity-100", children: /* @__PURE__ */ jsx(
12694
+ useMenuButton ? /* @__PURE__ */ jsx("div", { "data-ntable-card-action": true, className: "ntable-card-action absolute end-2 top-2 z-10 h-auto transition-opacity duration-200", children: /* @__PURE__ */ jsx(
12406
12695
  "button",
12407
12696
  {
12408
12697
  type: "button",
@@ -12411,10 +12700,10 @@ function NDataCardShell({ row, onClick, onContextMenu, actions, children, classN
12411
12700
  e.stopPropagation();
12412
12701
  openRowMenu(e, row.original);
12413
12702
  },
12414
- className: "flex h-7 w-7 p-0 rounded-md cursor-pointer justify-center items-center text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground",
12703
+ className: "flex h-7 w-7 cursor-pointer items-center justify-center rounded-md p-0 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
12415
12704
  children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-4 w-4" })
12416
12705
  }
12417
- ) }) : actions && (actions.onView || actions.onEdit || actions.onDelete) ? /* @__PURE__ */ jsx("div", { className: "absolute top-2 right-2 h-auto z-10 opacity-0 transition-opacity duration-200 group-hover:opacity-100 focus-within:opacity-100", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
12706
+ ) }) : actions && (actions.onView || actions.onEdit || actions.onDelete) ? /* @__PURE__ */ jsx("div", { "data-ntable-card-action": true, className: "ntable-card-action absolute end-2 top-2 z-10 h-auto transition-opacity duration-200", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
12418
12707
  /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
12419
12708
  "div",
12420
12709
  {
@@ -12519,6 +12808,7 @@ function NTableCards({ effectiveMode }) {
12519
12808
  const showContent = useTableStore.use.showContent();
12520
12809
  const classNames = useTableStore.use.classNames();
12521
12810
  const bordered = useTableStore.use.bordered();
12811
+ const borderColor = useTableStore.use.borderColor();
12522
12812
  const renderSubRow = useTableStore.use.renderSubRow();
12523
12813
  const userGetRowCanExpand = useTableStore.use.getRowCanExpand();
12524
12814
  const handleContainerContextMenu = useCallback((e) => {
@@ -12556,7 +12846,7 @@ function NTableCards({ effectiveMode }) {
12556
12846
  const defaultContainerClass = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3";
12557
12847
  const containerClass = classNames?.cards ?? defaultContainerClass;
12558
12848
  const actions = !menuButton && (onView || onEdit || onDelete) ? { onView, onEdit, onDelete } : void 0;
12559
- return /* @__PURE__ */ jsx(NajmScroll, { axis: "y", className: "min-h-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: cn(containerClass), onContextMenu: handleContainerContextMenu, children: rows.map((row) => {
12849
+ return /* @__PURE__ */ jsx(NajmScroll, { axis: "y", className: "min-h-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx("div", { "data-ntable-cards-grid": true, className: cn(containerClass), onContextMenu: handleContainerContextMenu, children: rows.map((row) => {
12560
12850
  const noShell = Boolean(row.original?.__smsNoShell);
12561
12851
  const hasExpansion = Boolean(renderSubRow || userGetRowCanExpand);
12562
12852
  const canExpand = hasExpansion && row.getCanExpand();
@@ -12586,7 +12876,8 @@ function NTableCards({ effectiveMode }) {
12586
12876
  e.stopPropagation();
12587
12877
  openRowMenu(e, row.original);
12588
12878
  },
12589
- className: "absolute top-2 right-2 z-10 flex h-7 w-7 cursor-pointer items-center justify-center rounded-md p-0 text-muted-foreground opacity-0 transition-all duration-200 hover:bg-muted/50 hover:text-foreground group-hover:opacity-100 focus:opacity-100 focus-visible:opacity-100",
12879
+ "data-ntable-card-action": true,
12880
+ className: "ntable-card-action absolute end-2 top-2 z-10 flex h-7 w-7 cursor-pointer items-center justify-center rounded-md p-0 text-muted-foreground transition-all duration-200 hover:bg-muted/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
12590
12881
  children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-4 w-4" })
12591
12882
  }
12592
12883
  ),
@@ -12622,6 +12913,7 @@ function NTableCards({ effectiveMode }) {
12622
12913
  openRowMenu,
12623
12914
  menuButton,
12624
12915
  bordered,
12916
+ borderColor,
12625
12917
  className: rowClassName || void 0,
12626
12918
  children: /* @__PURE__ */ jsx(
12627
12919
  CardComponent,
@@ -12639,13 +12931,108 @@ function NTableCards({ effectiveMode }) {
12639
12931
  );
12640
12932
  }) }) });
12641
12933
  }
12934
+ function CardLoadMorePagination({
12935
+ config,
12936
+ rowCount,
12937
+ bordered,
12938
+ className
12939
+ }) {
12940
+ const [internalPending, setInternalPending] = React__default.useState(false);
12941
+ const [internalError, setInternalError] = React__default.useState(null);
12942
+ const [announcement, setAnnouncement] = React__default.useState("");
12943
+ const buttonRef = React__default.useRef(null);
12944
+ const pendingRef = React__default.useRef(false);
12945
+ const restoreFocusRef = React__default.useRef(false);
12946
+ const previousRowCountRef = React__default.useRef(rowCount);
12947
+ const errorId = React__default.useId();
12948
+ const pending = Boolean(config.loadingMore || internalPending);
12949
+ const error = config.loadMoreError ?? internalError;
12950
+ React__default.useEffect(() => {
12951
+ const previous = previousRowCountRef.current;
12952
+ if (rowCount > previous) {
12953
+ const appended = rowCount - previous;
12954
+ setAnnouncement(
12955
+ config.itemsLoadedLabel?.(appended) ?? `${appended} more ${appended === 1 ? "item" : "items"} loaded.`
12956
+ );
12957
+ }
12958
+ previousRowCountRef.current = rowCount;
12959
+ }, [config.itemsLoadedLabel, rowCount]);
12960
+ React__default.useEffect(() => {
12961
+ if (pending || !restoreFocusRef.current) return;
12962
+ restoreFocusRef.current = false;
12963
+ const frame = requestAnimationFrame(() => buttonRef.current?.focus());
12964
+ return () => cancelAnimationFrame(frame);
12965
+ }, [pending]);
12966
+ const loadMore = async () => {
12967
+ if (pendingRef.current || pending || !config.hasNextPage && !error) return;
12968
+ pendingRef.current = true;
12969
+ restoreFocusRef.current = document.activeElement === buttonRef.current;
12970
+ setInternalPending(true);
12971
+ setInternalError(null);
12972
+ const loadingAnnouncement = config.loadingMoreLabel ?? "Loading more items...";
12973
+ setAnnouncement(loadingAnnouncement);
12974
+ try {
12975
+ await config.onLoadMore();
12976
+ } catch {
12977
+ setInternalError(config.loadMoreErrorLabel ?? "Couldn't load more items.");
12978
+ setAnnouncement("");
12979
+ } finally {
12980
+ pendingRef.current = false;
12981
+ setInternalPending(false);
12982
+ setAnnouncement((current) => current === loadingAnnouncement ? "" : current);
12983
+ }
12984
+ };
12985
+ if (!config.hasNextPage && !pending && !error) {
12986
+ return /* @__PURE__ */ jsx(
12987
+ "div",
12988
+ {
12989
+ "data-ntable-load-more-end": true,
12990
+ role: "status",
12991
+ "aria-live": "polite",
12992
+ className: cn("py-2 text-center text-sm text-muted-foreground", className),
12993
+ children: config.endLabel ?? "No more items."
12994
+ }
12995
+ );
12996
+ }
12997
+ return /* @__PURE__ */ jsxs(
12998
+ "div",
12999
+ {
13000
+ "data-ntable-load-more": true,
13001
+ className: cn("flex min-w-0 flex-col items-center gap-2 py-2", className),
13002
+ children: [
13003
+ error ? /* @__PURE__ */ jsx("div", { id: errorId, role: "alert", className: "text-center text-sm text-destructive", children: error === true ? config.loadMoreErrorLabel ?? "Couldn't load more items." : error }) : null,
13004
+ /* @__PURE__ */ jsxs(
13005
+ Button,
13006
+ {
13007
+ ref: buttonRef,
13008
+ type: "button",
13009
+ bordered,
13010
+ variant: "outline",
13011
+ autoLoading: false,
13012
+ disabled: pending,
13013
+ "aria-describedby": error ? errorId : void 0,
13014
+ "aria-busy": pending ? "true" : void 0,
13015
+ onClick: loadMore,
13016
+ children: [
13017
+ pending ? /* @__PURE__ */ jsx(Loader2, { "aria-hidden": "true", className: "h-4 w-4 animate-spin motion-reduce:animate-none" }) : null,
13018
+ pending ? config.loadingMoreLabel ?? "Loading more..." : error ? config.retryLabel ?? "Retry" : config.loadMoreLabel ?? "Load more"
13019
+ ]
13020
+ }
13021
+ ),
13022
+ /* @__PURE__ */ jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement })
13023
+ ]
13024
+ }
13025
+ );
13026
+ }
12642
13027
  function NTablePagination() {
12643
13028
  const table = useTableStore.use.table();
12644
13029
  const showPagination = useTableStore.use.showPagination();
12645
13030
  const showContent = useTableStore.use.showContent();
12646
13031
  const pageSizeOptions = useTableStore.use.pageSizeOptions();
12647
13032
  const classNames = useTableStore.use.classNames();
12648
- const viewMode = useTableStore.use.viewMode();
13033
+ const effectiveViewMode = useTableStore.use.effectiveViewMode();
13034
+ const cardPagination = useTableStore.use.cardPagination();
13035
+ const data = useTableStore.use.data();
12649
13036
  const pagination = useTableStore.use.pagination();
12650
13037
  const manualPagination = useTableStore.use.manualPagination();
12651
13038
  const pageCount = useTableStore.use.pageCount();
@@ -12653,7 +13040,19 @@ function NTablePagination() {
12653
13040
  const setPagination = useTableStore.use.setPagination();
12654
13041
  const isPaginationControlled = useTableStore.use.isPaginationControlled();
12655
13042
  const bordered = useTableStore.use.bordered();
12656
- if (!table || !showContent || !showPagination || viewMode === "json" || viewMode === "files") return null;
13043
+ if (!table || !showContent || !showPagination || effectiveViewMode === "json" || effectiveViewMode === "files") return null;
13044
+ if (effectiveViewMode === "cards" && cardPagination.mode === "all") return null;
13045
+ if (effectiveViewMode === "cards" && cardPagination.mode === "load-more") {
13046
+ return /* @__PURE__ */ jsx(
13047
+ CardLoadMorePagination,
13048
+ {
13049
+ config: cardPagination,
13050
+ rowCount: data.length,
13051
+ bordered,
13052
+ className: classNames?.pagination
13053
+ }
13054
+ );
13055
+ }
12657
13056
  const filteredRows = table.getFilteredRowModel().rows;
12658
13057
  const selectedRows = table.getFilteredSelectedRowModel().rows;
12659
13058
  const { pageIndex, pageSize } = table.getState().pagination;
@@ -13091,7 +13490,7 @@ function NTableHeaderSkeleton() {
13091
13490
  }
13092
13491
  );
13093
13492
  }
13094
- function NTableLoadingSkeleton({ rows = DEFAULT_ROWS2 }) {
13493
+ function NTableLoadingSkeleton({ rows }) {
13095
13494
  const rawColumns = useTableStore.use.columns();
13096
13495
  const responsiveColumns = React__default.useMemo(() => filterResponsiveColumns(rawColumns), [rawColumns]);
13097
13496
  const columns = responsiveColumns;
@@ -13099,24 +13498,34 @@ function NTableLoadingSkeleton({ rows = DEFAULT_ROWS2 }) {
13099
13498
  const headerClassName = useTableStore.use.headerClassName();
13100
13499
  const classNames = useTableStore.use.classNames();
13101
13500
  const dynamicHeight = useTableStore.use.dynamicHeight();
13501
+ const bordered = useTableStore.use.bordered();
13502
+ const borderColor = useTableStore.use.borderColor();
13503
+ const surface = useTableSurfaceAppearance(bordered, borderColor);
13504
+ const bodyHeight = useTableStore.use.bodyHeight();
13505
+ const skeletonRowCount = useTableStore.use.skeletonRowCount();
13102
13506
  const renderSubRow = useTableStore.use.renderSubRow();
13103
13507
  const userGetRowCanExpand = useTableStore.use.getRowCanExpand();
13104
13508
  const hasExpansion = Boolean(renderSubRow || userGetRowCanExpand);
13105
13509
  const loadingText = useTableStore.use.loadingText();
13510
+ const rowCount = rows ?? (dynamicHeight && bodyHeight > 0 ? skeletonRowCount : DEFAULT_ROWS2);
13106
13511
  const renderHeaderLabel = (header) => typeof header === "string" ? header : null;
13107
13512
  return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2", children: [
13108
13513
  /* @__PURE__ */ jsx(NTableHeaderSkeleton, {}),
13109
13514
  /* @__PURE__ */ jsxs(
13110
- Card,
13515
+ "div",
13111
13516
  {
13112
13517
  "data-testid": "ntable-loading-skeleton",
13518
+ "data-ntable-loading-row-count": rowCount,
13519
+ "data-bordered": surface.bordered === false ? "false" : surface.bordered ? "true" : void 0,
13113
13520
  "aria-busy": "true",
13114
13521
  "aria-label": loadingText,
13115
- className: cn("rounded-md p-0 border", dynamicHeight ? "overflow-hidden" : "najm-overlay-scroll", classNames?.content),
13522
+ role: "status",
13523
+ style: surface.style,
13524
+ className: cn("min-h-0 flex-1 rounded-md p-0", surface.className, dynamicHeight ? "overflow-hidden" : "najm-overlay-scroll", classNames?.content),
13116
13525
  children: [
13117
13526
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: loadingText }),
13118
- /* @__PURE__ */ jsx("div", { className: dynamicHeight ? "najm-overlay-scroll" : void 0, children: /* @__PURE__ */ jsxs(Table, { children: [
13119
- /* @__PURE__ */ jsx(TableHeader, { className: cn(headerClassName, dynamicHeight && "sticky top-0 z-10", classNames?.tableHeader), children: /* @__PURE__ */ jsxs(TableRow, { className: "hover:bg-muted/30", children: [
13527
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: dynamicHeight ? "najm-overlay-scroll h-full" : void 0, children: /* @__PURE__ */ jsxs(Table, { children: [
13528
+ /* @__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: [
13120
13529
  showCheckbox && /* @__PURE__ */ jsx(TableHead, { "aria-label": "Select column", className: "w-10 text-foreground h-12" }),
13121
13530
  hasExpansion && /* @__PURE__ */ jsx(TableHead, { "aria-label": "Expand column", className: "w-10 text-foreground h-12" }),
13122
13531
  columns.map((col, i) => /* @__PURE__ */ jsx(
@@ -13129,7 +13538,7 @@ function NTableLoadingSkeleton({ rows = DEFAULT_ROWS2 }) {
13129
13538
  col?.id ?? col?.accessorKey ?? i
13130
13539
  ))
13131
13540
  ] }) }),
13132
- /* @__PURE__ */ jsx(TableBody, { children: Array.from({ length: rows }).map((_, r) => /* @__PURE__ */ jsxs(TableRow, { children: [
13541
+ /* @__PURE__ */ jsx(TableBody, { children: Array.from({ length: rowCount }).map((_, r) => /* @__PURE__ */ jsxs(TableRow, { children: [
13133
13542
  showCheckbox && /* @__PURE__ */ jsx(TableCell, { className: "h-14 w-10", children: /* @__PURE__ */ jsx(NSkeleton, { className: "h-4 w-4" }) }),
13134
13543
  hasExpansion && /* @__PURE__ */ jsx(TableCell, { className: "h-14 w-10", children: /* @__PURE__ */ jsx(NSkeleton, { className: "h-4 w-4" }) }),
13135
13544
  columns.map((col, c) => /* @__PURE__ */ jsx(
@@ -13160,74 +13569,105 @@ function NTableCardsLoadingSkeleton({ rows }) {
13160
13569
  );
13161
13570
  const classNames = useTableStore.use.classNames();
13162
13571
  const bordered = useTableStore.use.bordered();
13163
- const calculatedPageSize = useTableStore.use.calculatedPageSize();
13164
- const pagination = useTableStore.use.pagination();
13165
- const cardCount = rows ?? Math.max(1, calculatedPageSize || pagination?.pageSize || DEFAULT_CARD_COUNT);
13572
+ const borderColor = useTableStore.use.borderColor();
13573
+ const surface = useTableSurfaceAppearance(bordered, borderColor);
13574
+ const dynamicHeight = useTableStore.use.dynamicHeight();
13575
+ const bodyHeight = useTableStore.use.bodyHeight();
13576
+ const cardColumnCount = useTableStore.use.cardColumnCount();
13577
+ const cardRowHeight = useTableStore.use.cardRowHeight();
13578
+ const cardGap = useTableStore.use.cardGap();
13579
+ const loadingText = useTableStore.use.loadingText();
13580
+ const cardCount = rows ?? (dynamicHeight && bodyHeight > 0 ? calculateCardSkeletonCount({
13581
+ bodyHeight,
13582
+ columnCount: cardColumnCount,
13583
+ cardHeight: cardRowHeight,
13584
+ gap: cardGap
13585
+ }) : DEFAULT_CARD_COUNT);
13586
+ const defaultContainerClass = "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4";
13587
+ const containerClass = classNames?.cards ?? defaultContainerClass;
13166
13588
  return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2", children: [
13167
13589
  hasHeaderSkeleton && /* @__PURE__ */ jsx(NTableHeaderSkeleton, {}),
13168
- /* @__PURE__ */ jsx(NajmScroll, { axis: "y", className: "min-h-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx(
13169
- "div",
13590
+ /* @__PURE__ */ jsxs(
13591
+ NajmScroll,
13170
13592
  {
13171
- "data-testid": "ntable-cards-loading-skeleton",
13593
+ axis: "y",
13172
13594
  "aria-busy": "true",
13173
- className: cn("grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", classNames?.cards),
13174
- children: Array.from({ length: cardCount }).map((_, index) => /* @__PURE__ */ jsx(
13175
- Card,
13176
- {
13177
- className: cn("rounded-lg bg-card p-3 shadow-none sm:p-4", surfaceBorderClasses(bordered)),
13178
- children: /* @__PURE__ */ jsxs(
13179
- "div",
13180
- {
13181
- "data-ntable-loading-card-layout": "responsive-avatar",
13182
- className: "grid grid-cols-[80px_minmax(0,1fr)] gap-3 sm:grid-cols-[72px_minmax(0,1fr)]",
13183
- children: [
13184
- /* @__PURE__ */ jsx("div", { className: "col-start-1 row-start-1 flex items-start justify-center sm:justify-start", children: /* @__PURE__ */ jsx(
13185
- NSkeleton,
13186
- {
13187
- "data-ntable-loading-card-avatar": true,
13188
- className: "size-20 shrink-0 rounded-full sm:size-16"
13189
- }
13190
- ) }),
13191
- /* @__PURE__ */ jsxs("div", { className: "col-start-2 row-start-1 flex min-w-0 items-start justify-between gap-3", children: [
13192
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [
13193
- /* @__PURE__ */ jsx(NSkeleton, { className: "h-5 w-36 max-w-full" }),
13194
- /* @__PURE__ */ jsx(NSkeleton, { className: "hidden h-3 w-16 sm:block" })
13195
- ] }),
13196
- /* @__PURE__ */ jsx(
13197
- NSkeleton,
13198
- {
13199
- "data-ntable-loading-card-status": true,
13200
- className: "hidden h-6 w-14 shrink-0 rounded-full sm:block"
13201
- }
13202
- )
13203
- ] }),
13204
- /* @__PURE__ */ jsx(
13595
+ "aria-label": loadingText,
13596
+ role: "status",
13597
+ className: "min-h-0 flex-1 overflow-hidden",
13598
+ children: [
13599
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: loadingText }),
13600
+ /* @__PURE__ */ jsx(
13601
+ "div",
13602
+ {
13603
+ "data-testid": "ntable-cards-loading-skeleton",
13604
+ "data-ntable-loading-cards-grid": true,
13605
+ "data-ntable-loading-card-count": cardCount,
13606
+ "aria-hidden": "true",
13607
+ className: cn(containerClass),
13608
+ children: Array.from({ length: cardCount }).map((_, index) => /* @__PURE__ */ jsx(
13609
+ "div",
13610
+ {
13611
+ "data-ntable-loading-card": true,
13612
+ "data-bordered": surface.bordered === false ? "false" : surface.bordered ? "true" : void 0,
13613
+ style: surface.style,
13614
+ className: cn("rounded-lg p-3 sm:p-4", surface.className),
13615
+ children: /* @__PURE__ */ jsxs(
13205
13616
  "div",
13206
13617
  {
13207
- "data-ntable-loading-card-details": true,
13208
- className: "col-start-2 row-start-2 space-y-1 sm:col-span-full sm:col-start-1 sm:space-y-2 sm:rounded-lg sm:bg-muted/50 sm:p-3",
13209
- children: Array.from({ length: 3 }).map((_2, detailIndex) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2", children: [
13210
- /* @__PURE__ */ jsx(NSkeleton, { className: "size-3.5 shrink-0 rounded-sm sm:size-4" }),
13211
- /* @__PURE__ */ jsx(
13618
+ "data-ntable-loading-card-layout": "responsive-avatar",
13619
+ className: "grid grid-cols-[80px_minmax(0,1fr)] gap-3 sm:grid-cols-[72px_minmax(0,1fr)]",
13620
+ children: [
13621
+ /* @__PURE__ */ jsx("div", { className: "col-start-1 row-start-1 flex items-start justify-center sm:justify-start", children: /* @__PURE__ */ jsx(
13212
13622
  NSkeleton,
13213
13623
  {
13214
- className: cn(
13215
- "h-3 max-w-full sm:h-4",
13216
- detailIndex === 0 ? "w-full" : detailIndex === 1 ? "w-4/5" : "w-3/4"
13217
- )
13624
+ "data-ntable-loading-card-avatar": true,
13625
+ className: "size-20 shrink-0 rounded-full sm:size-16"
13626
+ }
13627
+ ) }),
13628
+ /* @__PURE__ */ jsxs("div", { className: "col-start-2 row-start-1 flex min-w-0 items-start justify-between gap-3", children: [
13629
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [
13630
+ /* @__PURE__ */ jsx(NSkeleton, { className: "h-5 w-36 max-w-full" }),
13631
+ /* @__PURE__ */ jsx(NSkeleton, { className: "hidden h-3 w-16 sm:block" })
13632
+ ] }),
13633
+ /* @__PURE__ */ jsx(
13634
+ NSkeleton,
13635
+ {
13636
+ "data-ntable-loading-card-status": true,
13637
+ className: "hidden h-6 w-14 shrink-0 rounded-full sm:block"
13638
+ }
13639
+ )
13640
+ ] }),
13641
+ /* @__PURE__ */ jsx(
13642
+ "div",
13643
+ {
13644
+ "data-ntable-loading-card-details": true,
13645
+ className: "col-start-2 row-start-2 space-y-1 sm:col-span-full sm:col-start-1 sm:space-y-2 sm:rounded-lg sm:bg-muted/50 sm:p-3",
13646
+ children: Array.from({ length: 3 }).map((_2, detailIndex) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2", children: [
13647
+ /* @__PURE__ */ jsx(NSkeleton, { className: "size-3.5 shrink-0 rounded-sm sm:size-4" }),
13648
+ /* @__PURE__ */ jsx(
13649
+ NSkeleton,
13650
+ {
13651
+ className: cn(
13652
+ "h-3 max-w-full sm:h-4",
13653
+ detailIndex === 0 ? "w-full" : detailIndex === 1 ? "w-4/5" : "w-3/4"
13654
+ )
13655
+ }
13656
+ )
13657
+ ] }, detailIndex))
13218
13658
  }
13219
13659
  )
13220
- ] }, detailIndex))
13660
+ ]
13221
13661
  }
13222
13662
  )
13223
- ]
13224
- }
13225
- )
13226
- },
13227
- index
13228
- ))
13663
+ },
13664
+ index
13665
+ ))
13666
+ }
13667
+ )
13668
+ ]
13229
13669
  }
13230
- ) })
13670
+ )
13231
13671
  ] });
13232
13672
  }
13233
13673
  function TableStateSlot({ children }) {
@@ -13278,7 +13718,7 @@ function TableLayout(props) {
13278
13718
  const responsiveCards = useTableStore.use.responsiveCards();
13279
13719
  const isCustomMode = useTableStore.use.isCustomMode();
13280
13720
  const renderCustomMode = useTableStore.use.renderCustomMode();
13281
- const [isMobile, setIsMobile] = useState(false);
13721
+ const [isMobile, setIsMobile] = useState(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(max-width: 639px)").matches : false);
13282
13722
  useEffect(() => {
13283
13723
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
13284
13724
  const mql = window.matchMedia("(max-width: 639px)");
@@ -13287,18 +13727,22 @@ function TableLayout(props) {
13287
13727
  mql.addEventListener("change", handler2);
13288
13728
  return () => mql.removeEventListener("change", handler2);
13289
13729
  }, []);
13290
- useDynamicPageSize(containerRef);
13291
- useTable();
13292
- useTableKeyboard({
13293
- scopeRef: containerRef,
13294
- contextMenuClose: props.contextMenuClose,
13295
- contextMenuOpen: props.contextMenuOpen
13296
- });
13297
13730
  const effectiveMode = (() => {
13298
13731
  if (viewMode === "json") return "json";
13299
13732
  if (isMobile && responsiveCards && CardComponent) return "cards";
13300
13733
  return viewMode;
13301
13734
  })();
13735
+ const syncWithProps = useTableStore.use.syncWithProps();
13736
+ useLayoutEffect(() => {
13737
+ syncWithProps({ isMobile, effectiveViewMode: effectiveMode });
13738
+ }, [effectiveMode, isMobile, syncWithProps]);
13739
+ useDynamicPageSize(containerRef, effectiveMode);
13740
+ useTable(effectiveMode);
13741
+ useTableKeyboard({
13742
+ scopeRef: containerRef,
13743
+ contextMenuClose: props.contextMenuClose,
13744
+ contextMenuOpen: props.contextMenuOpen
13745
+ });
13302
13746
  const showFilteredEmpty = isFilteredEmpty && !isLoading && !error;
13303
13747
  const showEmpty = hasNoData && !isLoading && !error && !showFilteredEmpty;
13304
13748
  const customRenderer = isCustomMode ? renderCustomMode?.[viewMode] : void 0;
@@ -13458,6 +13902,7 @@ function NTable(props) {
13458
13902
  pagination: props.pagination,
13459
13903
  defaultPagination: props.defaultPagination,
13460
13904
  onPaginationChange: props.onPaginationChange ?? null,
13905
+ cardPagination: props.cardPagination ?? { mode: "paged" },
13461
13906
  // Row selection
13462
13907
  rowSelection: props.rowSelection,
13463
13908
  defaultRowSelection: props.defaultRowSelection,