@rowakit/table 0.2.2 → 0.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useEffect } from 'react';
2
- import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  // src/column-helpers.ts
5
5
  function text(field, options) {
@@ -118,6 +118,158 @@ function getRowKey(row, rowKey) {
118
118
  function getHeaderLabel(column) {
119
119
  return column.header ?? column.id;
120
120
  }
121
+ function validateViewName(name) {
122
+ const trimmed = name.trim();
123
+ if (trimmed.length === 0) {
124
+ return { valid: false, error: "Name cannot be empty" };
125
+ }
126
+ if (trimmed.length > 40) {
127
+ return { valid: false, error: "Name cannot exceed 40 characters" };
128
+ }
129
+ const invalidChars = /[/\\?%*:|"<>\x00-\x1f\x7f]/;
130
+ if (invalidChars.test(trimmed)) {
131
+ return { valid: false, error: "Name contains invalid characters" };
132
+ }
133
+ return { valid: true };
134
+ }
135
+ function getSavedViewsIndex() {
136
+ if (typeof window === "undefined" || !window.localStorage) {
137
+ return [];
138
+ }
139
+ try {
140
+ const indexStr = localStorage.getItem("rowakit-views-index");
141
+ if (indexStr) {
142
+ const index = JSON.parse(indexStr);
143
+ if (Array.isArray(index)) {
144
+ return index;
145
+ }
146
+ }
147
+ } catch {
148
+ }
149
+ const rebuilt = [];
150
+ try {
151
+ for (let i = 0; i < localStorage.length; i++) {
152
+ const key = localStorage.key(i);
153
+ if (key?.startsWith("rowakit-view-")) {
154
+ const name = key.substring("rowakit-view-".length);
155
+ rebuilt.push({
156
+ name,
157
+ updatedAt: Date.now()
158
+ });
159
+ }
160
+ }
161
+ } catch {
162
+ }
163
+ return rebuilt;
164
+ }
165
+ function setSavedViewsIndex(index) {
166
+ if (typeof window === "undefined" || !window.localStorage) {
167
+ return;
168
+ }
169
+ try {
170
+ localStorage.setItem("rowakit-views-index", JSON.stringify(index));
171
+ } catch {
172
+ }
173
+ }
174
+ function loadSavedViewsFromStorage() {
175
+ if (typeof window === "undefined" || !window.localStorage) {
176
+ return [];
177
+ }
178
+ const index = getSavedViewsIndex();
179
+ const views = [];
180
+ for (const entry of index) {
181
+ try {
182
+ const viewStr = localStorage.getItem(`rowakit-view-${entry.name}`);
183
+ if (viewStr) {
184
+ const state = JSON.parse(viewStr);
185
+ views.push({
186
+ name: entry.name,
187
+ state
188
+ });
189
+ }
190
+ } catch {
191
+ }
192
+ }
193
+ return views;
194
+ }
195
+ function parseUrlState(params, defaultPageSize, pageSizeOptions) {
196
+ const pageStr = params.get("page");
197
+ let page = 1;
198
+ if (pageStr) {
199
+ const parsed = parseInt(pageStr, 10);
200
+ page = !isNaN(parsed) && parsed >= 1 ? parsed : 1;
201
+ }
202
+ const pageSizeStr = params.get("pageSize");
203
+ let pageSize = defaultPageSize;
204
+ if (pageSizeStr) {
205
+ const parsed = parseInt(pageSizeStr, 10);
206
+ if (!isNaN(parsed) && parsed >= 1) {
207
+ if (pageSizeOptions && pageSizeOptions.length > 0) {
208
+ pageSize = pageSizeOptions.includes(parsed) ? parsed : defaultPageSize;
209
+ } else {
210
+ pageSize = parsed;
211
+ }
212
+ }
213
+ }
214
+ const result = { page, pageSize };
215
+ const sortField = params.get("sortField");
216
+ const sortDir = params.get("sortDirection");
217
+ if (sortField && (sortDir === "asc" || sortDir === "desc")) {
218
+ result.sort = { field: sortField, direction: sortDir };
219
+ }
220
+ const filtersStr = params.get("filters");
221
+ if (filtersStr) {
222
+ try {
223
+ const parsed = JSON.parse(filtersStr);
224
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
225
+ result.filters = parsed;
226
+ }
227
+ } catch {
228
+ }
229
+ }
230
+ const widthsStr = params.get("columnWidths");
231
+ if (widthsStr) {
232
+ try {
233
+ const parsed = JSON.parse(widthsStr);
234
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
235
+ const widths = {};
236
+ for (const [key, value] of Object.entries(parsed)) {
237
+ if (typeof value === "number" && value > 0) {
238
+ widths[key] = value;
239
+ }
240
+ }
241
+ if (Object.keys(widths).length > 0) {
242
+ result.columnWidths = widths;
243
+ }
244
+ }
245
+ } catch {
246
+ }
247
+ }
248
+ return result;
249
+ }
250
+ function serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing) {
251
+ const params = new URLSearchParams();
252
+ params.set("page", String(query.page));
253
+ if (query.pageSize !== defaultPageSize) {
254
+ params.set("pageSize", String(query.pageSize));
255
+ }
256
+ if (query.sort) {
257
+ params.set("sortField", query.sort.field);
258
+ params.set("sortDirection", query.sort.direction);
259
+ }
260
+ if (filters && Object.keys(filters).length > 0) {
261
+ const nonEmptyFilters = Object.fromEntries(
262
+ Object.entries(filters).filter(([, v]) => v !== void 0)
263
+ );
264
+ if (Object.keys(nonEmptyFilters).length > 0) {
265
+ params.set("filters", JSON.stringify(nonEmptyFilters));
266
+ }
267
+ }
268
+ if (enableColumnResizing && Object.keys(columnWidths).length > 0) {
269
+ params.set("columnWidths", JSON.stringify(columnWidths));
270
+ }
271
+ return params.toString();
272
+ }
121
273
  function renderCell(column, row, isLoading, setConfirmState) {
122
274
  switch (column.kind) {
123
275
  case "text": {
@@ -167,7 +319,11 @@ function renderCell(column, row, isLoading, setConfirmState) {
167
319
  return numValue.toLocaleString();
168
320
  }
169
321
  case "actions": {
170
- return /* @__PURE__ */ jsx("div", { className: "rowakit-table-actions", children: column.actions.map((action) => {
322
+ const columnWithActions = column;
323
+ if (!Array.isArray(columnWithActions.actions)) {
324
+ return null;
325
+ }
326
+ return /* @__PURE__ */ jsx("div", { className: "rowakit-table-actions", children: columnWithActions.actions.map((action) => {
171
327
  const isDisabled = isLoading || action.disabled === true || typeof action.disabled === "function" && action.disabled(row);
172
328
  const handleClick = () => {
173
329
  if (isDisabled || action.loading) {
@@ -211,7 +367,10 @@ function RowaKitTable({
211
367
  pageSizeOptions = [10, 20, 50],
212
368
  rowKey,
213
369
  className = "",
214
- enableFilters = false
370
+ enableFilters = false,
371
+ enableColumnResizing = false,
372
+ syncToUrl = false,
373
+ enableSavedViews = false
215
374
  }) {
216
375
  const [dataState, setDataState] = useState({
217
376
  state: "idle",
@@ -223,8 +382,100 @@ function RowaKitTable({
223
382
  pageSize: defaultPageSize
224
383
  });
225
384
  const [filters, setFilters] = useState({});
385
+ const [columnWidths, setColumnWidths] = useState({});
386
+ const resizeRafRef = useRef(null);
387
+ const resizePendingRef = useRef(null);
388
+ const tableRef = useRef(null);
389
+ const isResizingRef = useRef(false);
390
+ const lastResizeEndTsRef = useRef(0);
391
+ const resizingColIdRef = useRef(null);
392
+ const didHydrateUrlRef = useRef(false);
393
+ const didSkipInitialUrlSyncRef = useRef(false);
394
+ const urlSyncDebounceRef = useRef(null);
395
+ const [savedViews, setSavedViews] = useState([]);
396
+ const [showSaveViewForm, setShowSaveViewForm] = useState(false);
397
+ const [saveViewInput, setSaveViewInput] = useState("");
398
+ const [saveViewError, setSaveViewError] = useState("");
399
+ const [overwriteConfirmName, setOverwriteConfirmName] = useState(null);
400
+ useEffect(() => {
401
+ if (!enableSavedViews) return;
402
+ const views = loadSavedViewsFromStorage();
403
+ setSavedViews(views);
404
+ }, [enableSavedViews]);
226
405
  const [confirmState, setConfirmState] = useState(null);
227
406
  const requestIdRef = useRef(0);
407
+ useEffect(() => {
408
+ if (!syncToUrl) {
409
+ didSkipInitialUrlSyncRef.current = false;
410
+ return;
411
+ }
412
+ if (!didSkipInitialUrlSyncRef.current) {
413
+ didSkipInitialUrlSyncRef.current = true;
414
+ return;
415
+ }
416
+ if (urlSyncDebounceRef.current) {
417
+ clearTimeout(urlSyncDebounceRef.current);
418
+ urlSyncDebounceRef.current = null;
419
+ }
420
+ const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
421
+ const qs = urlStr ? `?${urlStr}` : "";
422
+ window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
423
+ }, [query, filters, syncToUrl, enableColumnResizing, defaultPageSize, columnWidths]);
424
+ useEffect(() => {
425
+ if (!syncToUrl || !enableColumnResizing) return;
426
+ if (!didSkipInitialUrlSyncRef.current) return;
427
+ if (urlSyncDebounceRef.current) {
428
+ clearTimeout(urlSyncDebounceRef.current);
429
+ }
430
+ urlSyncDebounceRef.current = setTimeout(() => {
431
+ const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
432
+ const qs = urlStr ? `?${urlStr}` : "";
433
+ window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
434
+ urlSyncDebounceRef.current = null;
435
+ }, 150);
436
+ return () => {
437
+ if (urlSyncDebounceRef.current) {
438
+ clearTimeout(urlSyncDebounceRef.current);
439
+ urlSyncDebounceRef.current = null;
440
+ }
441
+ };
442
+ }, [columnWidths, syncToUrl, enableColumnResizing, query, filters, defaultPageSize]);
443
+ useEffect(() => {
444
+ if (!syncToUrl) {
445
+ didHydrateUrlRef.current = false;
446
+ return;
447
+ }
448
+ if (didHydrateUrlRef.current) return;
449
+ didHydrateUrlRef.current = true;
450
+ const params = new URLSearchParams(window.location.search);
451
+ const parsed = parseUrlState(params, defaultPageSize, pageSizeOptions);
452
+ setQuery({
453
+ page: parsed.page,
454
+ pageSize: parsed.pageSize,
455
+ sort: parsed.sort,
456
+ filters: parsed.filters
457
+ });
458
+ if (parsed.filters) {
459
+ setFilters(parsed.filters);
460
+ }
461
+ if (parsed.columnWidths && enableColumnResizing) {
462
+ const clamped = {};
463
+ for (const [colId, rawWidth] of Object.entries(parsed.columnWidths)) {
464
+ const widthNum = typeof rawWidth === "number" ? rawWidth : Number(rawWidth);
465
+ if (!Number.isFinite(widthNum)) continue;
466
+ const colDef = columns.find((c) => c.id === colId);
467
+ if (!colDef) continue;
468
+ const minW = colDef.minWidth ?? 80;
469
+ const maxW = colDef.maxWidth;
470
+ let finalW = Math.max(minW, widthNum);
471
+ if (maxW != null) {
472
+ finalW = Math.min(finalW, maxW);
473
+ }
474
+ clamped[colId] = finalW;
475
+ }
476
+ setColumnWidths(clamped);
477
+ }
478
+ }, [syncToUrl, defaultPageSize, enableColumnResizing, pageSizeOptions, columns]);
228
479
  useEffect(() => {
229
480
  if (!enableFilters) return;
230
481
  const activeFilters = {};
@@ -310,10 +561,203 @@ function RowaKitTable({
310
561
  }
311
562
  return query.sort.direction === "asc" ? " \u2191" : " \u2193";
312
563
  };
564
+ const scheduleColumnWidthUpdate = (colId, width) => {
565
+ resizePendingRef.current = { colId, width };
566
+ if (resizeRafRef.current != null) return;
567
+ resizeRafRef.current = requestAnimationFrame(() => {
568
+ resizeRafRef.current = null;
569
+ const pending = resizePendingRef.current;
570
+ if (!pending) return;
571
+ handleColumnResize(pending.colId, pending.width);
572
+ });
573
+ };
574
+ const handleColumnResize = (columnId, newWidth) => {
575
+ const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
576
+ const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
577
+ let finalWidth = Math.max(minWidth, newWidth);
578
+ if (maxWidth) {
579
+ finalWidth = Math.min(finalWidth, maxWidth);
580
+ }
581
+ if (columnWidths[columnId] === finalWidth) {
582
+ return;
583
+ }
584
+ setColumnWidths((prev) => ({
585
+ ...prev,
586
+ [columnId]: finalWidth
587
+ }));
588
+ };
589
+ const autoFitColumnWidth = (columnId) => {
590
+ const tableEl = tableRef.current;
591
+ if (!tableEl) return;
592
+ const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
593
+ if (!th) return;
594
+ const tds = Array.from(
595
+ tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`)
596
+ );
597
+ const headerW = th.scrollWidth;
598
+ const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
599
+ const padding = 24;
600
+ const raw = Math.max(headerW, cellsMaxW) + padding;
601
+ const colDef = columns.find((c) => c.id === columnId);
602
+ const minW = colDef?.minWidth ?? 80;
603
+ const maxW = colDef?.maxWidth ?? 600;
604
+ const finalW = Math.max(minW, Math.min(raw, maxW));
605
+ setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
606
+ };
607
+ const startColumnResize = (e, columnId) => {
608
+ e.preventDefault();
609
+ e.stopPropagation();
610
+ if (e.detail === 2) {
611
+ autoFitColumnWidth(columnId);
612
+ return;
613
+ }
614
+ if (e.pointerType === "mouse" && e.buttons !== 1) {
615
+ return;
616
+ }
617
+ const target = e.currentTarget;
618
+ const pointerId = e.pointerId;
619
+ try {
620
+ target.setPointerCapture(pointerId);
621
+ } catch {
622
+ }
623
+ isResizingRef.current = true;
624
+ resizingColIdRef.current = columnId;
625
+ const startX = e.clientX;
626
+ const th = target.parentElement;
627
+ let startWidth = columnWidths[columnId] ?? th.offsetWidth;
628
+ const MIN_DRAG_WIDTH = 80;
629
+ if (startWidth < MIN_DRAG_WIDTH) {
630
+ const nextTh = th.nextElementSibling;
631
+ if (nextTh && nextTh.offsetWidth >= 50) {
632
+ startWidth = nextTh.offsetWidth;
633
+ } else {
634
+ startWidth = 100;
635
+ }
636
+ }
637
+ document.body.classList.add("rowakit-resizing");
638
+ const handlePointerMove = (moveEvent) => {
639
+ const delta = moveEvent.clientX - startX;
640
+ const newWidth = startWidth + delta;
641
+ scheduleColumnWidthUpdate(columnId, newWidth);
642
+ };
643
+ const cleanupResize = () => {
644
+ target.removeEventListener("pointermove", handlePointerMove);
645
+ target.removeEventListener("pointerup", handlePointerUp);
646
+ target.removeEventListener("pointercancel", handlePointerCancel);
647
+ document.body.classList.remove("rowakit-resizing");
648
+ isResizingRef.current = false;
649
+ resizingColIdRef.current = null;
650
+ lastResizeEndTsRef.current = Date.now();
651
+ try {
652
+ target.releasePointerCapture(pointerId);
653
+ } catch {
654
+ }
655
+ };
656
+ const handlePointerUp = () => {
657
+ cleanupResize();
658
+ };
659
+ const handlePointerCancel = () => {
660
+ cleanupResize();
661
+ };
662
+ target.addEventListener("pointermove", handlePointerMove);
663
+ target.addEventListener("pointerup", handlePointerUp);
664
+ target.addEventListener("pointercancel", handlePointerCancel);
665
+ };
666
+ const handleColumnResizeDoubleClick = (e, columnId) => {
667
+ e.preventDefault();
668
+ e.stopPropagation();
669
+ autoFitColumnWidth(columnId);
670
+ };
671
+ const saveCurrentView = (name) => {
672
+ const viewState = {
673
+ page: query.page,
674
+ pageSize: query.pageSize,
675
+ sort: query.sort,
676
+ filters: query.filters,
677
+ columnWidths: enableColumnResizing ? columnWidths : void 0
678
+ };
679
+ setSavedViews((prev) => {
680
+ const filtered = prev.filter((v) => v.name !== name);
681
+ return [...filtered, { name, state: viewState }];
682
+ });
683
+ if (typeof window !== "undefined" && window.localStorage) {
684
+ try {
685
+ localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
686
+ const index = getSavedViewsIndex();
687
+ const filtered = index.filter((v) => v.name !== name);
688
+ filtered.push({ name, updatedAt: Date.now() });
689
+ setSavedViewsIndex(filtered);
690
+ } catch {
691
+ }
692
+ }
693
+ };
694
+ const loadSavedView = (name) => {
695
+ const view = savedViews.find((v) => v.name === name);
696
+ if (!view) return;
697
+ const { state } = view;
698
+ setQuery({
699
+ page: state.page,
700
+ pageSize: state.pageSize,
701
+ sort: state.sort,
702
+ filters: state.filters
703
+ });
704
+ setFilters(state.filters ?? {});
705
+ if (state.columnWidths && enableColumnResizing) {
706
+ setColumnWidths(state.columnWidths);
707
+ }
708
+ };
709
+ const deleteSavedView = (name) => {
710
+ setSavedViews((prev) => prev.filter((v) => v.name !== name));
711
+ if (typeof window !== "undefined" && window.localStorage) {
712
+ try {
713
+ localStorage.removeItem(`rowakit-view-${name}`);
714
+ const index = getSavedViewsIndex();
715
+ const filtered = index.filter((v) => v.name !== name);
716
+ setSavedViewsIndex(filtered);
717
+ } catch {
718
+ }
719
+ }
720
+ };
721
+ const resetTableState = () => {
722
+ setQuery({
723
+ page: 1,
724
+ pageSize: defaultPageSize
725
+ });
726
+ setFilters({});
727
+ setColumnWidths({});
728
+ };
729
+ const transformFilterValueForColumn = (column, value) => {
730
+ if (!value || column?.kind !== "number") {
731
+ return value;
732
+ }
733
+ const numberColumn = column;
734
+ if (!numberColumn.filterTransform) {
735
+ return value;
736
+ }
737
+ if (value.op === "equals" && typeof value.value === "number") {
738
+ return {
739
+ ...value,
740
+ value: numberColumn.filterTransform(value.value)
741
+ };
742
+ }
743
+ if (value.op === "range" && typeof value.value === "object") {
744
+ const { from, to } = value.value;
745
+ return {
746
+ op: "range",
747
+ value: {
748
+ from: from !== void 0 && typeof from === "number" ? numberColumn.filterTransform(from) : from,
749
+ to: to !== void 0 && typeof to === "number" ? numberColumn.filterTransform(to) : to
750
+ }
751
+ };
752
+ }
753
+ return value;
754
+ };
313
755
  const handleFilterChange = (field, value) => {
756
+ const column = columns.find((c) => c.id === field);
757
+ const transformedValue = transformFilterValueForColumn(column, value);
314
758
  setFilters((prev) => ({
315
759
  ...prev,
316
- [field]: value
760
+ [field]: transformedValue
317
761
  }));
318
762
  };
319
763
  const handleClearFilter = (field) => {
@@ -333,7 +777,158 @@ function RowaKitTable({
333
777
  const canGoPrevious = query.page > 1 && !isLoading;
334
778
  const canGoNext = query.page < totalPages && !isLoading;
335
779
  const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
336
- return /* @__PURE__ */ jsxs("div", { className: `rowakit-table${className ? ` ${className}` : ""}`, children: [
780
+ const containerClass = [
781
+ "rowakit-table",
782
+ enableColumnResizing ? "rowakit-layout-fixed" : "",
783
+ className
784
+ ].filter(Boolean).join(" ");
785
+ return /* @__PURE__ */ jsxs("div", { className: containerClass, children: [
786
+ enableSavedViews && /* @__PURE__ */ jsxs("div", { className: "rowakit-saved-views-group", children: [
787
+ !showSaveViewForm ? /* @__PURE__ */ jsx(
788
+ "button",
789
+ {
790
+ onClick: () => {
791
+ setShowSaveViewForm(true);
792
+ setSaveViewInput("");
793
+ setSaveViewError("");
794
+ setOverwriteConfirmName(null);
795
+ },
796
+ className: "rowakit-saved-view-button",
797
+ type: "button",
798
+ children: "Save View"
799
+ }
800
+ ) : /* @__PURE__ */ jsx("div", { className: "rowakit-save-view-form", children: overwriteConfirmName ? /* @__PURE__ */ jsxs("div", { className: "rowakit-save-view-confirm", children: [
801
+ /* @__PURE__ */ jsxs("p", { children: [
802
+ 'View "',
803
+ overwriteConfirmName,
804
+ '" already exists. Overwrite?'
805
+ ] }),
806
+ /* @__PURE__ */ jsx(
807
+ "button",
808
+ {
809
+ onClick: () => {
810
+ saveCurrentView(overwriteConfirmName);
811
+ setShowSaveViewForm(false);
812
+ setSaveViewInput("");
813
+ setSaveViewError("");
814
+ setOverwriteConfirmName(null);
815
+ },
816
+ className: "rowakit-saved-view-button",
817
+ type: "button",
818
+ children: "Overwrite"
819
+ }
820
+ ),
821
+ /* @__PURE__ */ jsx(
822
+ "button",
823
+ {
824
+ onClick: () => {
825
+ setOverwriteConfirmName(null);
826
+ },
827
+ className: "rowakit-saved-view-button",
828
+ type: "button",
829
+ children: "Cancel"
830
+ }
831
+ )
832
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
833
+ /* @__PURE__ */ jsx(
834
+ "input",
835
+ {
836
+ type: "text",
837
+ value: saveViewInput,
838
+ onChange: (e) => {
839
+ setSaveViewInput(e.target.value);
840
+ setSaveViewError("");
841
+ },
842
+ onKeyDown: (e) => {
843
+ if (e.key === "Enter") {
844
+ const validation = validateViewName(saveViewInput);
845
+ if (!validation.valid) {
846
+ setSaveViewError(validation.error || "Invalid name");
847
+ return;
848
+ }
849
+ if (savedViews.some((v) => v.name === saveViewInput.trim())) {
850
+ setOverwriteConfirmName(saveViewInput.trim());
851
+ } else {
852
+ saveCurrentView(saveViewInput.trim());
853
+ setShowSaveViewForm(false);
854
+ setSaveViewInput("");
855
+ setSaveViewError("");
856
+ }
857
+ }
858
+ },
859
+ placeholder: "Enter view name...",
860
+ className: "rowakit-save-view-input"
861
+ }
862
+ ),
863
+ saveViewError && /* @__PURE__ */ jsx("div", { className: "rowakit-save-view-error", children: saveViewError }),
864
+ /* @__PURE__ */ jsx(
865
+ "button",
866
+ {
867
+ onClick: () => {
868
+ const validation = validateViewName(saveViewInput);
869
+ if (!validation.valid) {
870
+ setSaveViewError(validation.error || "Invalid name");
871
+ return;
872
+ }
873
+ if (savedViews.some((v) => v.name === saveViewInput.trim())) {
874
+ setOverwriteConfirmName(saveViewInput.trim());
875
+ } else {
876
+ saveCurrentView(saveViewInput.trim());
877
+ setShowSaveViewForm(false);
878
+ setSaveViewInput("");
879
+ setSaveViewError("");
880
+ }
881
+ },
882
+ className: "rowakit-saved-view-button",
883
+ type: "button",
884
+ children: "Save"
885
+ }
886
+ ),
887
+ /* @__PURE__ */ jsx(
888
+ "button",
889
+ {
890
+ onClick: () => {
891
+ setShowSaveViewForm(false);
892
+ setSaveViewInput("");
893
+ setSaveViewError("");
894
+ },
895
+ className: "rowakit-saved-view-button",
896
+ type: "button",
897
+ children: "Cancel"
898
+ }
899
+ )
900
+ ] }) }),
901
+ savedViews.map((view) => /* @__PURE__ */ jsxs("div", { className: "rowakit-saved-view-item", children: [
902
+ /* @__PURE__ */ jsx(
903
+ "button",
904
+ {
905
+ onClick: () => loadSavedView(view.name),
906
+ className: "rowakit-saved-view-button",
907
+ type: "button",
908
+ children: view.name
909
+ }
910
+ ),
911
+ /* @__PURE__ */ jsx(
912
+ "button",
913
+ {
914
+ onClick: () => deleteSavedView(view.name),
915
+ className: "rowakit-saved-view-button rowakit-saved-view-button-delete",
916
+ type: "button",
917
+ title: "Delete this view",
918
+ children: "\xD7"
919
+ }
920
+ )
921
+ ] }, view.name)),
922
+ (hasActiveFilters || query.page > 1 || query.sort) && /* @__PURE__ */ jsx(
923
+ "button",
924
+ {
925
+ onClick: resetTableState,
926
+ className: "rowakit-saved-view-button",
927
+ type: "button",
928
+ children: "Reset"
929
+ }
930
+ )
931
+ ] }),
337
932
  hasActiveFilters && /* @__PURE__ */ jsx("div", { className: "rowakit-table-filter-controls", children: /* @__PURE__ */ jsx(
338
933
  "button",
339
934
  {
@@ -343,15 +938,22 @@ function RowaKitTable({
343
938
  children: "Clear all filters"
344
939
  }
345
940
  ) }),
346
- /* @__PURE__ */ jsxs("table", { children: [
941
+ /* @__PURE__ */ jsxs("table", { ref: tableRef, children: [
347
942
  /* @__PURE__ */ jsxs("thead", { children: [
348
943
  /* @__PURE__ */ jsx("tr", { children: columns.map((column) => {
349
944
  const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
350
945
  const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
946
+ const isResizable = enableColumnResizing && column.kind !== "actions";
947
+ const actualWidth = columnWidths[column.id] ?? column.width;
351
948
  return /* @__PURE__ */ jsxs(
352
949
  "th",
353
950
  {
354
- onClick: isSortable ? () => handleSort(String(field)) : void 0,
951
+ "data-col-id": column.id,
952
+ onClick: isSortable ? () => {
953
+ if (isResizingRef.current) return;
954
+ if (Date.now() - lastResizeEndTsRef.current < 150) return;
955
+ handleSort(String(field));
956
+ } : void 0,
355
957
  role: isSortable ? "button" : void 0,
356
958
  tabIndex: isSortable ? 0 : void 0,
357
959
  onKeyDown: isSortable ? (e) => {
@@ -362,13 +964,27 @@ function RowaKitTable({
362
964
  } : void 0,
363
965
  "aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
364
966
  style: {
365
- width: column.width ? `${column.width}px` : void 0,
366
- textAlign: column.align
967
+ width: actualWidth != null ? `${actualWidth}px` : void 0,
968
+ textAlign: column.align,
969
+ position: isResizable ? "relative" : void 0
367
970
  },
368
- className: column.truncate ? "rowakit-cell-truncate" : void 0,
971
+ className: [
972
+ column.truncate ? "rowakit-cell-truncate" : "",
973
+ resizingColIdRef.current === column.id ? "resizing" : ""
974
+ // PRD-01
975
+ ].filter(Boolean).join(" ") || void 0,
369
976
  children: [
370
977
  getHeaderLabel(column),
371
- isSortable && getSortIndicator(String(field))
978
+ isSortable && getSortIndicator(String(field)),
979
+ isResizable && /* @__PURE__ */ jsx(
980
+ "div",
981
+ {
982
+ className: "rowakit-column-resize-handle",
983
+ onPointerDown: (e) => startColumnResize(e, column.id),
984
+ onDoubleClick: (e) => handleColumnResizeDoubleClick(e, column.id),
985
+ title: "Drag to resize | Double-click to auto-fit content"
986
+ }
987
+ )
372
988
  ]
373
989
  },
374
990
  column.id
@@ -468,6 +1084,51 @@ function RowaKitTable({
468
1084
  ] }) }, column.id);
469
1085
  }
470
1086
  const isNumberColumn = column.kind === "number";
1087
+ if (isNumberColumn) {
1088
+ const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
1089
+ const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
1090
+ const showRangeUI = !filterValue || filterValue.op === "range";
1091
+ if (showRangeUI) {
1092
+ return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs("div", { className: "rowakit-filter-number-range", children: [
1093
+ /* @__PURE__ */ jsx(
1094
+ "input",
1095
+ {
1096
+ type: "number",
1097
+ className: "rowakit-filter-input",
1098
+ placeholder: "Min",
1099
+ value: fromValue,
1100
+ onChange: (e) => {
1101
+ const from = e.target.value ? Number(e.target.value) : void 0;
1102
+ const to = toValue ? Number(toValue) : void 0;
1103
+ if (from === void 0 && to === void 0) {
1104
+ handleClearFilter(field);
1105
+ } else {
1106
+ handleFilterChange(field, { op: "range", value: { from, to } });
1107
+ }
1108
+ }
1109
+ }
1110
+ ),
1111
+ /* @__PURE__ */ jsx(
1112
+ "input",
1113
+ {
1114
+ type: "number",
1115
+ className: "rowakit-filter-input",
1116
+ placeholder: "Max",
1117
+ value: toValue,
1118
+ onChange: (e) => {
1119
+ const to = e.target.value ? Number(e.target.value) : void 0;
1120
+ const from = fromValue ? Number(fromValue) : void 0;
1121
+ if (from === void 0 && to === void 0) {
1122
+ handleClearFilter(field);
1123
+ } else {
1124
+ handleFilterChange(field, { op: "range", value: { from, to } });
1125
+ }
1126
+ }
1127
+ }
1128
+ )
1129
+ ] }) }, column.id);
1130
+ }
1131
+ }
471
1132
  return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx(
472
1133
  "input",
473
1134
  {
@@ -519,12 +1180,14 @@ function RowaKitTable({
519
1180
  column.kind === "number" ? "rowakit-cell-number" : "",
520
1181
  column.truncate ? "rowakit-cell-truncate" : ""
521
1182
  ].filter(Boolean).join(" ") || void 0;
1183
+ const actualWidth = columnWidths[column.id] ?? column.width;
522
1184
  return /* @__PURE__ */ jsx(
523
1185
  "td",
524
1186
  {
1187
+ "data-col-id": column.id,
525
1188
  className: cellClass,
526
1189
  style: {
527
- width: column.width ? `${column.width}px` : void 0,
1190
+ width: actualWidth != null ? `${actualWidth}px` : void 0,
528
1191
  textAlign: column.align || (column.kind === "number" ? "right" : void 0)
529
1192
  },
530
1193
  children: renderCell(column, row, isLoading, setConfirmState)
@@ -629,7 +1292,7 @@ function RowaKitTable({
629
1292
  var SmartTable = RowaKitTable;
630
1293
 
631
1294
  // src/index.ts
632
- var VERSION = "0.1.0";
1295
+ var VERSION = "0.4.0";
633
1296
 
634
1297
  export { RowaKitTable, SmartTable, VERSION, col };
635
1298
  //# sourceMappingURL=index.js.map