twico-ui 1.17.2 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to Twico UI are documented here (newest first). This file is generated automatically on each release by [semantic-release](https://semantic-release.gitbook.io/) from [Conventional Commits](https://www.conventionalcommits.org/) — do not edit it by hand. The same notes appear on each [GitHub Release](https://github.com/EncryptionL/twico-ui/releases).
4
4
 
5
+ # [1.18.0](https://github.com/EncryptionL/twico-ui/compare/v1.17.2...v1.18.0) (2026-07-17)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **hooks:** copy off a secure origin via execCommand fallback ([2786e74](https://github.com/EncryptionL/twico-ui/commit/2786e7414aec19832eacb8277830f72ae935307e)), closes [#255](https://github.com/EncryptionL/twico-ui/issues/255)
11
+
12
+
13
+ ### Features
14
+
15
+ * **datatable:** persist & restore view state via stateKey/initialState/onStateChange ([0830d59](https://github.com/EncryptionL/twico-ui/commit/0830d591e9bfb21395efa72ad67286326471371b)), closes [#45](https://github.com/EncryptionL/twico-ui/issues/45) [#259](https://github.com/EncryptionL/twico-ui/issues/259)
16
+
5
17
  ## [1.17.2](https://github.com/EncryptionL/twico-ui/compare/v1.17.1...v1.17.2) (2026-07-17)
6
18
 
7
19
 
package/dist/index.cjs CHANGED
@@ -9011,6 +9011,9 @@ function Datatable({
9011
9011
  onBatchUpdate,
9012
9012
  showBatchEdit = true,
9013
9013
  batchEditFields = null,
9014
+ stateKey,
9015
+ initialState,
9016
+ onStateChange,
9014
9017
  showPageJumper = true,
9015
9018
  selectionMode = "none",
9016
9019
  onRowClick,
@@ -9173,6 +9176,75 @@ function Datatable({
9173
9176
  const [rowGrab, setRowGrab] = React51__namespace.default.useState(null);
9174
9177
  const [reorderMsg, setReorderMsg] = React51__namespace.default.useState("");
9175
9178
  const rowFocusRef = React51__namespace.default.useRef(null);
9179
+ const serializeState = () => ({
9180
+ filters: filters.map(({ id, ...f }) => f),
9181
+ // strip the internal row id
9182
+ sort,
9183
+ quickFilter: quick,
9184
+ page: pageVal,
9185
+ pageSize: sizeVal,
9186
+ columnOrder: order,
9187
+ columnWidths: widths,
9188
+ columnVisibility: Object.fromEntries([...hidden].map((f) => [f, false])),
9189
+ // absent = visible
9190
+ columnPinning: Object.fromEntries([...pins.left.map((f) => [f, "left"]), ...pins.right.map((f) => [f, "right"])]),
9191
+ density
9192
+ });
9193
+ const applyState = (s) => {
9194
+ if (!s || typeof s !== "object") return;
9195
+ const known = new Set(cols.map((c) => c.field).filter(Boolean));
9196
+ if (Array.isArray(s.filters)) setFilters(s.filters.filter((f) => f && known.has(f.field)).map((f, i) => ({ id: Date.now() + Math.random() + i, field: f.field, op: f.op, value: f.value })));
9197
+ if ("sort" in s) setSort(s.sort && known.has(s.sort.field) ? { field: s.sort.field, dir: s.sort.dir === "desc" ? "desc" : "asc" } : null);
9198
+ if (typeof s.quickFilter === "string" && quickFilter === void 0) setInternalQuick(s.quickFilter);
9199
+ if (typeof s.page === "number" && !pageControlled) setInternalPage(Math.max(0, Math.floor(s.page)));
9200
+ if (typeof s.pageSize === "number" && !pageSizeControlled) setInternalRpp(s.pageSize > 0 ? s.pageSize : 10);
9201
+ if (Array.isArray(s.columnOrder)) {
9202
+ const savedOrder = s.columnOrder.filter((f) => known.has(f));
9203
+ const missing = cols.map((c) => c.field).filter((f) => f && !savedOrder.includes(f));
9204
+ setOrder([...savedOrder, ...missing]);
9205
+ }
9206
+ if (s.columnWidths && typeof s.columnWidths === "object") setWidths(Object.fromEntries(Object.entries(s.columnWidths).filter(([f, w]) => known.has(f) && typeof w === "number")));
9207
+ if (s.columnVisibility && typeof s.columnVisibility === "object") setHidden(new Set(Object.keys(s.columnVisibility).filter((f) => known.has(f) && s.columnVisibility[f] === false)));
9208
+ if (s.columnPinning && typeof s.columnPinning === "object") {
9209
+ const l = [], r2 = [];
9210
+ for (const [f, side] of Object.entries(s.columnPinning)) {
9211
+ if (!known.has(f)) continue;
9212
+ if (side === "left") l.push(f);
9213
+ else if (side === "right") r2.push(f);
9214
+ }
9215
+ setPins({ left: l, right: r2 });
9216
+ }
9217
+ if (s.density === "compact" || s.density === "standard" || s.density === "comfortable") setDensity(s.density);
9218
+ };
9219
+ const stateReadyRef = React51__namespace.default.useRef(false);
9220
+ const onStateChangeRef = React51__namespace.default.useRef(onStateChange);
9221
+ onStateChangeRef.current = onStateChange;
9222
+ React51__namespace.default.useEffect(() => {
9223
+ if (!stateReadyRef.current) return void 0;
9224
+ if (!stateKey && !onStateChange) return void 0;
9225
+ const state = serializeState();
9226
+ onStateChangeRef.current?.(state);
9227
+ if (stateKey) {
9228
+ try {
9229
+ window.localStorage.setItem(stateKey, JSON.stringify(state));
9230
+ } catch {
9231
+ }
9232
+ }
9233
+ return void 0;
9234
+ }, [filters, sort, quick, pageVal, sizeVal, order, widths, hidden, pins, density, stateKey]);
9235
+ React51__namespace.default.useEffect(() => {
9236
+ let saved = null;
9237
+ if (stateKey) {
9238
+ try {
9239
+ const raw = window.localStorage.getItem(stateKey);
9240
+ if (raw) saved = JSON.parse(raw);
9241
+ } catch {
9242
+ }
9243
+ }
9244
+ if (!saved && initialState) saved = initialState;
9245
+ if (saved) applyState(saved);
9246
+ stateReadyRef.current = true;
9247
+ }, []);
9176
9248
  const rootRef = React51__namespace.default.useRef(null);
9177
9249
  const [compact, setCompact] = React51__namespace.default.useState(false);
9178
9250
  React51__namespace.default.useEffect(() => {
@@ -9214,13 +9286,20 @@ function Datatable({
9214
9286
  return [...kept, ...added];
9215
9287
  });
9216
9288
  }, [columns]);
9289
+ const densitySyncedRef = React51__namespace.default.useRef(false);
9217
9290
  React51__namespace.default.useEffect(() => {
9218
- setDensity(densityProp);
9291
+ if (densitySyncedRef.current) setDensity(densityProp);
9292
+ else densitySyncedRef.current = true;
9219
9293
  }, [densityProp]);
9220
9294
  React51__namespace.default.useEffect(() => {
9221
9295
  setAggOn(showAggregation);
9222
9296
  }, [showAggregation]);
9297
+ const pageSizeSyncedRef = React51__namespace.default.useRef(false);
9223
9298
  React51__namespace.default.useEffect(() => {
9299
+ if (!pageSizeSyncedRef.current) {
9300
+ pageSizeSyncedRef.current = true;
9301
+ return;
9302
+ }
9224
9303
  if (pageSizeControlled) return;
9225
9304
  setInternalRpp(pageSize > 0 ? pageSize : 10);
9226
9305
  setInternalPage(0);
@@ -17809,21 +17888,47 @@ function useLocalStorage(key, initialValue) {
17809
17888
  );
17810
17889
  return [stored, setValue];
17811
17890
  }
17891
+ function legacyCopy(text) {
17892
+ try {
17893
+ const ta = document.createElement("textarea");
17894
+ ta.value = text;
17895
+ ta.setAttribute("readonly", "");
17896
+ ta.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;padding:0;border:none;opacity:0;pointer-events:none;";
17897
+ document.body.appendChild(ta);
17898
+ const sel = document.getSelection();
17899
+ const prev = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
17900
+ ta.focus();
17901
+ ta.select();
17902
+ ta.setSelectionRange(0, ta.value.length);
17903
+ const ok = document.execCommand("copy");
17904
+ document.body.removeChild(ta);
17905
+ if (prev && sel) {
17906
+ sel.removeAllRanges();
17907
+ sel.addRange(prev);
17908
+ }
17909
+ return ok;
17910
+ } catch {
17911
+ return false;
17912
+ }
17913
+ }
17812
17914
  function useCopyToClipboard(timeout = 1500) {
17813
17915
  const [copied, setCopied] = React51__namespace.default.useState(false);
17814
17916
  const copy = React51__namespace.default.useCallback(async (text) => {
17815
- if (!canUseDOM2 || !navigator.clipboard) {
17917
+ if (!canUseDOM2) {
17816
17918
  setCopied(false);
17817
17919
  return false;
17818
17920
  }
17819
- try {
17820
- await navigator.clipboard.writeText(text);
17821
- setCopied(true);
17822
- return true;
17823
- } catch (e) {
17824
- setCopied(false);
17825
- return false;
17921
+ if (navigator.clipboard && navigator.clipboard.writeText) {
17922
+ try {
17923
+ await navigator.clipboard.writeText(text);
17924
+ setCopied(true);
17925
+ return true;
17926
+ } catch {
17927
+ }
17826
17928
  }
17929
+ const ok = legacyCopy(String(text ?? ""));
17930
+ setCopied(ok);
17931
+ return ok;
17827
17932
  }, []);
17828
17933
  React51__namespace.default.useEffect(() => {
17829
17934
  if (!copied) return void 0;