@tensor_1/react 2.5.0 → 2.6.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.
@@ -0,0 +1,185 @@
1
+ function compare(a, b) {
2
+ if (a == null && b == null) return 0;
3
+ if (a == null) return -1;
4
+ if (b == null) return 1;
5
+ if (typeof a === "number" && typeof b === "number") return a - b;
6
+ return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
7
+ }
8
+ function multiSort(rows, keys) {
9
+ if (!keys.length) return rows.slice();
10
+ return rows.map((row, i) => ({ row, i })).sort((x, y) => {
11
+ for (const k of keys) {
12
+ const c = compare(x.row[k.field], y.row[k.field]);
13
+ if (c !== 0) return k.dir === "ascending" ? c : -c;
14
+ }
15
+ return x.i - y.i;
16
+ }).map((w) => w.row);
17
+ }
18
+ function nextSortDir(current) {
19
+ if (current === "ascending") return "descending";
20
+ if (current === "descending") return "none";
21
+ return "ascending";
22
+ }
23
+ function toggleSort(keys, field, additive = false) {
24
+ const existing = keys.find((k) => k.field === field);
25
+ const dir = nextSortDir(existing?.dir);
26
+ if (!additive) return dir === "none" ? [] : [{ field, dir }];
27
+ const rest = keys.filter((k) => k.field !== field);
28
+ return dir === "none" ? rest : [...rest, { field, dir }];
29
+ }
30
+ function asNum(v) {
31
+ return typeof v === "number" ? v : Number(v);
32
+ }
33
+ function matchFilter(row, f) {
34
+ const cell = row[f.field];
35
+ switch (f.op) {
36
+ case "isAnyOf":
37
+ return Array.isArray(f.value) && f.value.includes(cell);
38
+ case "isNoneOf":
39
+ return Array.isArray(f.value) && !f.value.includes(cell);
40
+ case "contains":
41
+ return String(cell ?? "").toLowerCase().includes(String(f.value ?? "").toLowerCase());
42
+ case "equals":
43
+ return String(cell ?? "") === String(f.value ?? "");
44
+ case "startsWith":
45
+ return String(cell ?? "").toLowerCase().startsWith(String(f.value ?? "").toLowerCase());
46
+ case "eq":
47
+ return asNum(cell) === asNum(f.value);
48
+ case "lt":
49
+ return asNum(cell) < asNum(f.value);
50
+ case "gt":
51
+ return asNum(cell) > asNum(f.value);
52
+ case "between": {
53
+ const [lo, hi] = f.value.map(asNum);
54
+ return asNum(cell) >= lo && asNum(cell) <= hi;
55
+ }
56
+ case "is":
57
+ return Boolean(cell) === Boolean(f.value);
58
+ default:
59
+ return true;
60
+ }
61
+ }
62
+ function applyFilters(rows, filters) {
63
+ if (!filters.length) return rows.slice();
64
+ return rows.filter((r) => filters.every((f) => matchFilter(r, f)));
65
+ }
66
+ function searchRows(rows, query, fields) {
67
+ const q = query.trim().toLowerCase();
68
+ if (!q) return rows.slice();
69
+ return rows.filter((r) => {
70
+ const vals = fields ? fields.map((f) => r[f]) : Object.values(r);
71
+ return vals.some((v) => v != null && String(v).toLowerCase().includes(q));
72
+ });
73
+ }
74
+ function highlightSegments(text, query) {
75
+ const q = query.trim();
76
+ if (!q) return [{ text, match: false }];
77
+ const out = [];
78
+ const lower = text.toLowerCase();
79
+ const ql = q.toLowerCase();
80
+ let i = 0;
81
+ for (; ; ) {
82
+ const hit = lower.indexOf(ql, i);
83
+ if (hit === -1) {
84
+ if (i < text.length) out.push({ text: text.slice(i), match: false });
85
+ break;
86
+ }
87
+ if (hit > i) out.push({ text: text.slice(i, hit), match: false });
88
+ out.push({ text: text.slice(hit, hit + q.length), match: true });
89
+ i = hit + q.length;
90
+ }
91
+ return out;
92
+ }
93
+ function serializeView(v) {
94
+ const p = new URLSearchParams();
95
+ if (v.sort?.length) p.set("sort", v.sort.map((s) => `${s.field}:${s.dir === "descending" ? "d" : "a"}`).join(","));
96
+ if (v.columns?.length) p.set("cols", v.columns.join(","));
97
+ if (v.density && v.density !== "standard") p.set("density", v.density);
98
+ if (v.q) p.set("q", v.q);
99
+ if (v.filters?.length) p.set("filter", v.filters.map((f) => `${f.field}~${f.op}~${Array.isArray(f.value) ? f.value.join("|") : f.value ?? ""}`).join(","));
100
+ return p.toString();
101
+ }
102
+ function parseView(qs) {
103
+ const p = new URLSearchParams(qs.replace(/^\?/, ""));
104
+ const v = {};
105
+ const sort = p.get("sort");
106
+ if (sort) v.sort = sort.split(",").filter(Boolean).map((tok) => {
107
+ const [field, d] = tok.split(":");
108
+ return { field, dir: d === "d" ? "descending" : "ascending" };
109
+ });
110
+ const cols = p.get("cols");
111
+ if (cols) v.columns = cols.split(",").filter(Boolean);
112
+ const density = p.get("density");
113
+ if (density === "compact" || density === "comfortable" || density === "standard") v.density = density;
114
+ const q = p.get("q");
115
+ if (q) v.q = q;
116
+ const filter = p.get("filter");
117
+ if (filter) v.filters = filter.split(",").filter(Boolean).map((tok) => {
118
+ const [field, op, raw] = tok.split("~");
119
+ const value = raw?.includes("|") ? raw.split("|") : raw;
120
+ return { field, kind: "text", op, value };
121
+ });
122
+ return v;
123
+ }
124
+ function virtualWindow(scrollTop, viewportH, rowH, count, overscan = 6) {
125
+ if (rowH <= 0 || count <= 0) return { start: 0, end: count, padTop: 0, padBottom: 0, total: 0 };
126
+ const first = Math.max(0, Math.floor(scrollTop / rowH) - overscan);
127
+ const visible = Math.ceil(viewportH / rowH) + overscan * 2;
128
+ const end = Math.min(count, first + visible);
129
+ return { start: first, end, padTop: first * rowH, padBottom: (count - end) * rowH, total: count * rowH };
130
+ }
131
+ function shouldVirtualize(count, threshold = 100) {
132
+ return count > threshold;
133
+ }
134
+ function clampWidth(width, min = 64, max = Infinity) {
135
+ return Math.max(min, Math.min(max, Math.round(width)));
136
+ }
137
+ function reorder(arr, from, to) {
138
+ const next = arr.slice();
139
+ if (from < 0 || from >= next.length || to < 0 || to >= next.length) return next;
140
+ const [moved] = next.splice(from, 1);
141
+ next.splice(to, 0, moved);
142
+ return next;
143
+ }
144
+ function toggleId(set, id) {
145
+ const next = new Set(set);
146
+ next.has(id) ? next.delete(id) : next.add(id);
147
+ return next;
148
+ }
149
+ function selectAllState(selected, pageIds) {
150
+ const on = pageIds.filter((id) => selected.has(id)).length;
151
+ if (on === 0) return "none";
152
+ return on === pageIds.length ? "all" : "some";
153
+ }
154
+ function togglePage(selected, pageIds) {
155
+ const next = new Set(selected);
156
+ const all = pageIds.every((id) => next.has(id));
157
+ for (const id of pageIds) all ? next.delete(id) : next.add(id);
158
+ return next;
159
+ }
160
+ function mergePinned(all, visible, pinnedIds, idField = "id") {
161
+ if (!pinnedIds.size) return visible.slice();
162
+ const pinned = all.filter((r) => pinnedIds.has(String(r[idField])));
163
+ const pinnedSet = new Set(pinned.map((r) => String(r[idField])));
164
+ const rest = visible.filter((r) => !pinnedSet.has(String(r[idField])));
165
+ return [...pinned, ...rest];
166
+ }
167
+ export {
168
+ applyFilters,
169
+ clampWidth,
170
+ highlightSegments,
171
+ matchFilter,
172
+ mergePinned,
173
+ multiSort,
174
+ nextSortDir,
175
+ parseView,
176
+ reorder,
177
+ searchRows,
178
+ selectAllState,
179
+ serializeView,
180
+ shouldVirtualize,
181
+ toggleId,
182
+ togglePage,
183
+ toggleSort,
184
+ virtualWindow
185
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensor_1/react",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Thin React wrappers over the UIx v2 CSS component library.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -31,7 +31,8 @@
31
31
  "scripts": {
32
32
  "build": "tsup",
33
33
  "dev": "tsup --watch",
34
- "typecheck": "tsc --noEmit"
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "node --test"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "react": "^18.0.0 || ^19.0.0",