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