@rowakit/table 0.3.0 → 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/README.md CHANGED
@@ -14,10 +14,10 @@ RowaKit Table is a React table component designed for real-world internal applic
14
14
  ✅ **7 column types** - Text, Date, Boolean, Badge, Number, Actions, Custom
15
15
  ✅ **Column modifiers** - Width, align, truncate, minWidth, maxWidth (v0.2.0+)
16
16
  ✅ **Server-side filters** - Type-specific filter UI with auto-generated inputs (v0.2.0+)
17
- ✅ **Column resizing** - Drag-to-resize handles with constraints (v0.3.0+)
18
- ✅ **Saved views** - Save/load table state with localStorage persistence (v0.3.0+)
19
- ✅ **URL state sync** - Share URLs with exact table configuration (v0.3.0+)
20
- ✅ **Number range filters** - Min/max filtering for numeric columns (v0.3.0+)
17
+ ✅ **Column resizing** - Drag-to-resize handles with constraints (v0.4.0+)
18
+ ✅ **Saved views** - Save/load table state with localStorage persistence (v0.4.0+)
19
+ ✅ **URL state sync** - Share URLs with exact table configuration (v0.4.0+)
20
+ ✅ **Number range filters** - Min/max filtering for numeric columns (v0.4.0+)
21
21
  ✅ **State management** - Automatic loading, error, and empty states
22
22
  ✅ **Smart fetching** - Retry on error, stale request handling
23
23
 
@@ -858,7 +858,7 @@ col.actions([
858
858
 
859
859
  ---
860
860
 
861
- ## Advanced Features (v0.3.0+)
861
+ ## Advanced Features (v0.4.0+)
862
862
 
863
863
  ### Column Resizing (C-01)
864
864
 
@@ -1475,17 +1475,20 @@ Full TypeScript support. Your data model drives type checking throughout.
1475
1475
  - ✅ Fixed numeric filter value coercion
1476
1476
  - ✅ Production hardening and comprehensive documentation
1477
1477
 
1478
- **Stage C - Advanced Features (v0.3.0)** ✅ Complete (2026-01-03)
1478
+ **Stage C - Advanced Features (v0.4.0)** ✅ Complete (2026-01-03)
1479
1479
  - ✅ C-01: Column resizing with drag handles (minWidth/maxWidth constraints)
1480
1480
  - ✅ C-02: Saved views with localStorage persistence
1481
1481
  - ✅ C-02: URL state sync (page, pageSize, sort, filters, columnWidths)
1482
1482
  - ✅ C-03: Number range filters with optional filterTransform
1483
1483
 
1484
- **Stage D - Future** (Demand-Driven)
1485
- - Multi-column sorting
1486
- - Row selection + bulk actions
1487
- - Export CSV (server-triggered)
1488
- - Column visibility toggle
1484
+ **Stage D - Polish + Correctness (v0.4.0)** ✅ Complete (2026-01-05)
1485
+ - ✅ D-01: Prevent accidental sort while resizing (stopPropagation, suppression window)
1486
+ - D-02: Pointer Events resizing (mouse, touch, pen) with pointer capture and cleanup
1487
+ - D-03: Column width model hardening (apply widths to th+td, fixed layout, truncation)
1488
+ - D-04: Saved views persistence (index, hydration, corruption-safe)
1489
+ - ✅ D-05: URL sync hardening (validation, debounce, backward compatible)
1490
+
1491
+ See [ROADMAP.md](./docs/ROADMAP.md) and `docs/ROWAKIT_STAGE_D_ISSUES_v3.md` for implementation details and rationale.
1489
1492
 
1490
1493
  ## Changelog
1491
1494
 
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": {
@@ -236,60 +388,96 @@ function RowaKitTable({
236
388
  const resizeRafRef = react.useRef(null);
237
389
  const resizePendingRef = react.useRef(null);
238
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);
239
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]);
240
407
  const [confirmState, setConfirmState] = react.useState(null);
241
408
  const requestIdRef = react.useRef(0);
242
409
  react.useEffect(() => {
243
- if (!syncToUrl) return;
244
- const params = new URLSearchParams();
245
- params.set("page", String(query.page));
246
- params.set("pageSize", String(query.pageSize));
247
- if (query.sort) {
248
- params.set("sortField", query.sort.field);
249
- params.set("sortDirection", query.sort.direction);
410
+ if (!syncToUrl) {
411
+ didSkipInitialUrlSyncRef.current = false;
412
+ return;
250
413
  }
251
- if (query.filters && Object.keys(query.filters).length > 0) {
252
- params.set("filters", JSON.stringify(query.filters));
414
+ if (!didSkipInitialUrlSyncRef.current) {
415
+ didSkipInitialUrlSyncRef.current = true;
416
+ return;
253
417
  }
254
- if (enableColumnResizing && Object.keys(columnWidths).length > 0) {
255
- params.set("columnWidths", JSON.stringify(columnWidths));
418
+ if (urlSyncDebounceRef.current) {
419
+ clearTimeout(urlSyncDebounceRef.current);
420
+ urlSyncDebounceRef.current = null;
256
421
  }
257
- window.history.replaceState(null, "", `?${params.toString()}`);
258
- }, [query, columnWidths, syncToUrl, enableColumnResizing]);
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]);
259
426
  react.useEffect(() => {
260
- if (!syncToUrl) return;
261
- const params = new URLSearchParams(window.location.search);
262
- const page = parseInt(params.get("page") ?? "1", 10);
263
- const pageSize = parseInt(params.get("pageSize") ?? String(defaultPageSize), 10);
264
- const sortField = params.get("sortField");
265
- const sortDirection = params.get("sortDirection");
266
- const filtersStr = params.get("filters");
267
- const columnWidthsStr = params.get("columnWidths");
268
- const newQuery = {
269
- page: Math.max(1, page),
270
- pageSize: Math.max(1, pageSize)
271
- };
272
- if (sortField && sortDirection) {
273
- newQuery.sort = { field: sortField, direction: sortDirection };
427
+ if (!syncToUrl || !enableColumnResizing) return;
428
+ if (!didSkipInitialUrlSyncRef.current) return;
429
+ if (urlSyncDebounceRef.current) {
430
+ clearTimeout(urlSyncDebounceRef.current);
274
431
  }
275
- if (filtersStr) {
276
- try {
277
- const parsedFilters = JSON.parse(filtersStr);
278
- if (parsedFilters && typeof parsedFilters === "object") {
279
- setFilters(parsedFilters);
280
- newQuery.filters = parsedFilters;
281
- }
282
- } catch {
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;
283
442
  }
443
+ };
444
+ }, [columnWidths, syncToUrl, enableColumnResizing, query, filters, defaultPageSize]);
445
+ react.useEffect(() => {
446
+ if (!syncToUrl) {
447
+ didHydrateUrlRef.current = false;
448
+ return;
284
449
  }
285
- if (enableColumnResizing && columnWidthsStr) {
286
- try {
287
- setColumnWidths(JSON.parse(columnWidthsStr));
288
- } catch {
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;
289
477
  }
478
+ setColumnWidths(clamped);
290
479
  }
291
- setQuery(newQuery);
292
- }, [syncToUrl, defaultPageSize, enableColumnResizing]);
480
+ }, [syncToUrl, defaultPageSize, enableColumnResizing, pageSizeOptions, columns]);
293
481
  react.useEffect(() => {
294
482
  if (!enableFilters) return;
295
483
  const activeFilters = {};
@@ -392,15 +580,52 @@ function RowaKitTable({
392
580
  if (maxWidth) {
393
581
  finalWidth = Math.min(finalWidth, maxWidth);
394
582
  }
583
+ if (columnWidths[columnId] === finalWidth) {
584
+ return;
585
+ }
395
586
  setColumnWidths((prev) => ({
396
587
  ...prev,
397
588
  [columnId]: finalWidth
398
589
  }));
399
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
+ };
400
609
  const startColumnResize = (e, columnId) => {
401
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;
402
627
  const startX = e.clientX;
403
- const th = e.currentTarget.parentElement;
628
+ const th = target.parentElement;
404
629
  let startWidth = columnWidths[columnId] ?? th.offsetWidth;
405
630
  const MIN_DRAG_WIDTH = 80;
406
631
  if (startWidth < MIN_DRAG_WIDTH) {
@@ -412,33 +637,38 @@ function RowaKitTable({
412
637
  }
413
638
  }
414
639
  document.body.classList.add("rowakit-resizing");
415
- const handleMouseMove = (moveEvent) => {
640
+ const handlePointerMove = (moveEvent) => {
416
641
  const delta = moveEvent.clientX - startX;
417
642
  const newWidth = startWidth + delta;
418
643
  scheduleColumnWidthUpdate(columnId, newWidth);
419
644
  };
420
- const handleMouseUp = () => {
421
- document.removeEventListener("mousemove", handleMouseMove);
422
- document.removeEventListener("mouseup", handleMouseUp);
645
+ const cleanupResize = () => {
646
+ target.removeEventListener("pointermove", handlePointerMove);
647
+ target.removeEventListener("pointerup", handlePointerUp);
648
+ target.removeEventListener("pointercancel", handlePointerCancel);
423
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();
424
663
  };
425
- document.addEventListener("mousemove", handleMouseMove);
426
- document.addEventListener("mouseup", handleMouseUp);
664
+ target.addEventListener("pointermove", handlePointerMove);
665
+ target.addEventListener("pointerup", handlePointerUp);
666
+ target.addEventListener("pointercancel", handlePointerCancel);
427
667
  };
428
- const handleColumnResizeDoubleClick = (columnId) => {
429
- const tableEl = tableRef.current;
430
- if (!tableEl) return;
431
- const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
432
- if (!th) return;
433
- const tds = Array.from(tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`));
434
- const headerW = th.scrollWidth;
435
- const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
436
- const padding = 24;
437
- const raw = Math.max(headerW, cellsMaxW) + padding;
438
- const minW = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
439
- const maxW = columns.find((c) => c.id === columnId)?.maxWidth ?? 600;
440
- const finalW = Math.max(minW, Math.min(raw, maxW));
441
- setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
668
+ const handleColumnResizeDoubleClick = (e, columnId) => {
669
+ e.preventDefault();
670
+ e.stopPropagation();
671
+ autoFitColumnWidth(columnId);
442
672
  };
443
673
  const saveCurrentView = (name) => {
444
674
  const viewState = {
@@ -455,6 +685,10 @@ function RowaKitTable({
455
685
  if (typeof window !== "undefined" && window.localStorage) {
456
686
  try {
457
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);
458
692
  } catch {
459
693
  }
460
694
  }
@@ -479,6 +713,9 @@ function RowaKitTable({
479
713
  if (typeof window !== "undefined" && window.localStorage) {
480
714
  try {
481
715
  localStorage.removeItem(`rowakit-view-${name}`);
716
+ const index = getSavedViewsIndex();
717
+ const filtered = index.filter((v) => v.name !== name);
718
+ setSavedViewsIndex(filtered);
482
719
  } catch {
483
720
  }
484
721
  }
@@ -542,22 +779,127 @@ function RowaKitTable({
542
779
  const canGoPrevious = query.page > 1 && !isLoading;
543
780
  const canGoNext = query.page < totalPages && !isLoading;
544
781
  const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
545
- 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: [
546
788
  enableSavedViews && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-views-group", children: [
547
- /* @__PURE__ */ jsxRuntime.jsx(
789
+ !showSaveViewForm ? /* @__PURE__ */ jsxRuntime.jsx(
548
790
  "button",
549
791
  {
550
792
  onClick: () => {
551
- const name = typeof window !== "undefined" ? window.prompt("Enter view name:") : null;
552
- if (name) {
553
- saveCurrentView(name);
554
- }
793
+ setShowSaveViewForm(true);
794
+ setSaveViewInput("");
795
+ setSaveViewError("");
796
+ setOverwriteConfirmName(null);
555
797
  },
556
798
  className: "rowakit-saved-view-button",
557
799
  type: "button",
558
800
  children: "Save View"
559
801
  }
560
- ),
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
+ ] }) }),
561
903
  savedViews.map((view) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-view-item", children: [
562
904
  /* @__PURE__ */ jsxRuntime.jsx(
563
905
  "button",
@@ -609,7 +951,11 @@ function RowaKitTable({
609
951
  "th",
610
952
  {
611
953
  "data-col-id": column.id,
612
- onClick: isSortable ? () => handleSort(String(field)) : void 0,
954
+ onClick: isSortable ? () => {
955
+ if (isResizingRef.current) return;
956
+ if (Date.now() - lastResizeEndTsRef.current < 150) return;
957
+ handleSort(String(field));
958
+ } : void 0,
613
959
  role: isSortable ? "button" : void 0,
614
960
  tabIndex: isSortable ? 0 : void 0,
615
961
  onKeyDown: isSortable ? (e) => {
@@ -620,11 +966,15 @@ function RowaKitTable({
620
966
  } : void 0,
621
967
  "aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
622
968
  style: {
623
- width: actualWidth ? `${actualWidth}px` : void 0,
969
+ width: actualWidth != null ? `${actualWidth}px` : void 0,
624
970
  textAlign: column.align,
625
971
  position: isResizable ? "relative" : void 0
626
972
  },
627
- className: column.truncate && !isResizable ? "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,
628
978
  children: [
629
979
  getHeaderLabel(column),
630
980
  isSortable && getSortIndicator(String(field)),
@@ -632,8 +982,8 @@ function RowaKitTable({
632
982
  "div",
633
983
  {
634
984
  className: "rowakit-column-resize-handle",
635
- onMouseDown: (e) => startColumnResize(e, column.id),
636
- onDoubleClick: () => handleColumnResizeDoubleClick(column.id),
985
+ onPointerDown: (e) => startColumnResize(e, column.id),
986
+ onDoubleClick: (e) => handleColumnResizeDoubleClick(e, column.id),
637
987
  title: "Drag to resize | Double-click to auto-fit content"
638
988
  }
639
989
  )
@@ -832,14 +1182,14 @@ function RowaKitTable({
832
1182
  column.kind === "number" ? "rowakit-cell-number" : "",
833
1183
  column.truncate ? "rowakit-cell-truncate" : ""
834
1184
  ].filter(Boolean).join(" ") || void 0;
835
- const actualWidth = columnWidths[column.id];
1185
+ const actualWidth = columnWidths[column.id] ?? column.width;
836
1186
  return /* @__PURE__ */ jsxRuntime.jsx(
837
1187
  "td",
838
1188
  {
839
1189
  "data-col-id": column.id,
840
1190
  className: cellClass,
841
1191
  style: {
842
- width: actualWidth ? `${actualWidth}px` : void 0,
1192
+ width: actualWidth != null ? `${actualWidth}px` : void 0,
843
1193
  textAlign: column.align || (column.kind === "number" ? "right" : void 0)
844
1194
  },
845
1195
  children: renderCell(column, row, isLoading, setConfirmState)
@@ -944,7 +1294,7 @@ function RowaKitTable({
944
1294
  var SmartTable = RowaKitTable;
945
1295
 
946
1296
  // src/index.ts
947
- var VERSION = "0.1.0";
1297
+ var VERSION = "0.4.0";
948
1298
 
949
1299
  exports.RowaKitTable = RowaKitTable;
950
1300
  exports.SmartTable = SmartTable;