@richardmcquiston01/dashboard-widgets-toolkit 0.5.0 → 0.7.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/react.js CHANGED
@@ -3,80 +3,6 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  // src/react/card.tsx
5
5
 
6
- // src/core/grid.ts
7
- var WIDTH_COLUMNS = 12;
8
- var MIN_WIDGET_WIDTH = 2;
9
- var MAX_WIDGET_WIDTH = 12;
10
- function isWidgetWidth(value) {
11
- return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
12
- }
13
- function widthForSize(size) {
14
- switch (size) {
15
- case "small":
16
- return 3;
17
- case "large":
18
- return 6;
19
- case "full":
20
- return 12;
21
- default:
22
- return 4;
23
- }
24
- }
25
- function itemWidth(item) {
26
- return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
27
- }
28
- function usesWidthColumns(items) {
29
- return items.some((item) => item.width !== void 0);
30
- }
31
- function fillsWidth(fill) {
32
- return fill === "width" || fill === "both";
33
- }
34
- function fillsHeight(fill) {
35
- return fill === "height" || fill === "both";
36
- }
37
- function baseColumnSpan(size, columns, largeSpan = 2) {
38
- if (size === "full") return columns;
39
- if (size === "large") return Math.min(largeSpan, columns);
40
- return 1;
41
- }
42
- function fillColumnSpans(items, columns, largeSpan = 2) {
43
- return placeAndFill(
44
- items,
45
- columns,
46
- (item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
47
- );
48
- }
49
- function fillWidthSpans(items) {
50
- return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
51
- }
52
- function placeAndFill(items, columns, spanOf) {
53
- const trackCount = Math.max(1, Math.floor(columns));
54
- const spans = /* @__PURE__ */ new Map();
55
- let row = [];
56
- let used = 0;
57
- function closeRow() {
58
- const fillers = row.filter(({ item }) => fillsWidth(item.fill));
59
- const leftover = trackCount - used;
60
- if (fillers.length > 0) {
61
- const share = Math.floor(leftover / fillers.length);
62
- const remainder = leftover % fillers.length;
63
- fillers.forEach(({ item, span }, index) => {
64
- spans.set(item.key, span + share + (index < remainder ? 1 : 0));
65
- });
66
- }
67
- row = [];
68
- used = 0;
69
- }
70
- for (const item of items) {
71
- const span = Math.min(spanOf(item, trackCount), trackCount);
72
- if (used + span > trackCount && row.length > 0) closeRow();
73
- row.push({ item, span });
74
- used += span;
75
- }
76
- closeRow();
77
- return spans;
78
- }
79
-
80
6
  // src/core/format.ts
81
7
  var DEFAULT_CURRENCY = "USD";
82
8
  function localeArgument(locale) {
@@ -140,23 +66,393 @@ function formatPercentChange(change, locale) {
140
66
  signDisplay: "exceptZero"
141
67
  }).format(rounded / 100);
142
68
  }
143
- var DEFAULT_COMPARISON_LABEL = "previous period";
144
- function kpiDelta(current, previous, options = {}) {
145
- const percent = percentChange(current, previous);
146
- if (percent === null) {
147
- return null;
69
+ var DEFAULT_COMPARISON_LABEL = "previous period";
70
+ function kpiDelta(current, previous, options = {}) {
71
+ const percent = percentChange(current, previous);
72
+ if (percent === null) {
73
+ return null;
74
+ }
75
+ const text = formatPercentChange(percent, options.locale);
76
+ const direction = text === "\xB10%" ? "flat" : percent > 0 ? "up" : "down";
77
+ const higherIsBetter = options.higherIsBetter ?? true;
78
+ const sentiment = direction === "flat" ? "neutral" : direction === "up" === higherIsBetter ? "good" : "bad";
79
+ return {
80
+ percent,
81
+ direction,
82
+ sentiment,
83
+ text,
84
+ description: `${text} vs ${options.comparisonLabel ?? DEFAULT_COMPARISON_LABEL}`
85
+ };
86
+ }
87
+
88
+ // src/core/result.ts
89
+ function ok(value) {
90
+ return { ok: true, value };
91
+ }
92
+ function err(error) {
93
+ return { ok: false, error };
94
+ }
95
+ function describeUnknownError(cause) {
96
+ if (cause instanceof Error) {
97
+ return cause.message === "" ? cause.name : cause.message;
98
+ }
99
+ if (typeof cause === "string") {
100
+ return cause;
101
+ }
102
+ try {
103
+ const json = JSON.stringify(cause);
104
+ return json ?? String(cause);
105
+ } catch {
106
+ return String(cause);
107
+ }
108
+ }
109
+
110
+ // src/core/url.ts
111
+ var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
112
+ var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
113
+ function schemeOf(url) {
114
+ const match = SCHEME_PATTERN.exec(
115
+ url.replace(IGNORED_IN_SCHEME, "")
116
+ );
117
+ return match === null ? null : (match[1] ?? "").toLowerCase();
118
+ }
119
+ function isSafeHref(url) {
120
+ if (url.trim() === "") {
121
+ return false;
122
+ }
123
+ const scheme = schemeOf(url);
124
+ return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
125
+ }
126
+ function isSafeImageUrl(url) {
127
+ if (url.trim() === "") {
128
+ return false;
129
+ }
130
+ const scheme = schemeOf(url);
131
+ return scheme === null || scheme === "http" || scheme === "https";
132
+ }
133
+
134
+ // src/core/detail.ts
135
+ var DEFAULT_DETAIL_PAGE_SIZE = 25;
136
+ function resolveDetailOptions(definition) {
137
+ const detail = definition.detail;
138
+ if (detail === void 0 || detail === false) return void 0;
139
+ const options = detail === true ? {} : detail;
140
+ return {
141
+ title: options.title ?? definition.title,
142
+ pageSize: options.pageSize ?? DEFAULT_DETAIL_PAGE_SIZE,
143
+ mode: options.mode ?? "client"
144
+ };
145
+ }
146
+ function resolveTableControls(definition) {
147
+ const setting = definition.tableControls;
148
+ if (setting === void 0 || setting === false) return void 0;
149
+ const options = setting === true ? {} : setting;
150
+ const resolved = {
151
+ search: options.search ?? true,
152
+ sort: options.sort ?? true
153
+ };
154
+ return resolved.search || resolved.sort ? resolved : void 0;
155
+ }
156
+ function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
157
+ return { page: 1, pageSize };
158
+ }
159
+ function fold(text) {
160
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
161
+ }
162
+ function cellValue(cell) {
163
+ return cell.value ?? cell.text;
164
+ }
165
+ function cellMatches(cell, needle) {
166
+ return fold(String(cellValue(cell))).includes(needle);
167
+ }
168
+ function compareCells(a, b, collator) {
169
+ const left = cellValue(a);
170
+ const right = cellValue(b);
171
+ if (typeof left === "number" && typeof right === "number") {
172
+ return left - right;
173
+ }
174
+ if (typeof left === "number") return -1;
175
+ if (typeof right === "number") return 1;
176
+ return collator.compare(left, right);
177
+ }
178
+ function queryRows(data, query, locale) {
179
+ const indexOf = new Map(
180
+ data.columns.map((column, index) => [column.key, index])
181
+ );
182
+ const filterEntries = [];
183
+ for (const [key, text] of Object.entries(query.filters ?? {})) {
184
+ const index = indexOf.get(key);
185
+ if (index === void 0) {
186
+ return err(
187
+ `Detail query: filter column "${key}" does not exist. Columns: ${listKeys(data)}.`
188
+ );
189
+ }
190
+ const needle = fold(text.trim());
191
+ if (needle !== "") filterEntries.push({ index, needle });
192
+ }
193
+ let sortIndex = -1;
194
+ if (query.sort !== void 0) {
195
+ const found = indexOf.get(query.sort.column);
196
+ const column = found === void 0 ? void 0 : data.columns[found];
197
+ if (found === void 0 || column === void 0) {
198
+ return err(
199
+ `Detail query: sort column "${query.sort.column}" does not exist. Columns: ${listKeys(data)}.`
200
+ );
201
+ }
202
+ if (column.sortable === false) {
203
+ return err(
204
+ `Detail query: column "${column.key}" is not sortable (sortable: false).`
205
+ );
206
+ }
207
+ sortIndex = found;
208
+ }
209
+ const search = fold((query.search ?? "").trim());
210
+ let rows = data.rows.filter(
211
+ (row) => filterEntries.every(({ index, needle }) => {
212
+ const cell = row[index];
213
+ return cell !== void 0 && cellMatches(cell, needle);
214
+ }) && (search === "" || row.some((cell) => cellMatches(cell, search)))
215
+ );
216
+ if (query.sort !== void 0 && sortIndex >= 0) {
217
+ const collator = new Intl.Collator(locale, {
218
+ numeric: true,
219
+ sensitivity: "base"
220
+ });
221
+ const sign = query.sort.direction === "desc" ? -1 : 1;
222
+ rows = rows.map((row, position) => ({ row, position })).sort((a, b) => {
223
+ const left = a.row[sortIndex];
224
+ const right = b.row[sortIndex];
225
+ const order = left === void 0 || right === void 0 ? 0 : sign * compareCells(left, right, collator);
226
+ return order || a.position - b.position;
227
+ }).map(({ row }) => row);
228
+ }
229
+ const pageSize = Math.max(1, Math.floor(query.pageSize));
230
+ const pageCount = Math.max(1, Math.ceil(rows.length / pageSize));
231
+ const page = Math.min(Math.max(1, Math.floor(query.page)), pageCount);
232
+ return ok({
233
+ rows: rows.slice((page - 1) * pageSize, page * pageSize),
234
+ totalRows: rows.length,
235
+ page,
236
+ pageCount
237
+ });
238
+ }
239
+ function listKeys(data) {
240
+ return data.columns.map((column) => `"${column.key}"`).join(", ");
241
+ }
242
+ function describeType(value) {
243
+ if (value === null) return "null";
244
+ if (Array.isArray(value)) return "array";
245
+ return typeof value;
246
+ }
247
+ function validateDetailData(data, subject = "Detail data") {
248
+ const problems = [];
249
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
250
+ return err(`${subject}: must be an object, got ${describeType(data)}.`);
251
+ }
252
+ const record = data;
253
+ const columns = record["columns"];
254
+ const rows = record["rows"];
255
+ const keys = /* @__PURE__ */ new Set();
256
+ let columnCount = 0;
257
+ if (!Array.isArray(columns) || columns.length === 0) {
258
+ problems.push(
259
+ `columns must be a non-empty array, got ${describeType(columns)}.`
260
+ );
261
+ } else {
262
+ columnCount = columns.length;
263
+ columns.forEach((column, index) => {
264
+ const at = `columns[${index}]`;
265
+ if (typeof column !== "object" || column === null) {
266
+ problems.push(`${at} must be an object, got ${describeType(column)}.`);
267
+ return;
268
+ }
269
+ const fields = column;
270
+ const key = fields["key"];
271
+ if (typeof key !== "string" || key === "") {
272
+ problems.push(`${at}.key must be a non-empty string.`);
273
+ } else if (keys.has(key)) {
274
+ problems.push(`${at}.key "${key}" is used by an earlier column.`);
275
+ } else {
276
+ keys.add(key);
277
+ }
278
+ if (typeof fields["label"] !== "string") {
279
+ problems.push(
280
+ `${at}.label must be a string, got ${describeType(fields["label"])}.`
281
+ );
282
+ }
283
+ for (const flag of ["numeric", "sortable", "filterable"]) {
284
+ const flagValue = fields[flag];
285
+ if (flagValue !== void 0 && typeof flagValue !== "boolean") {
286
+ problems.push(
287
+ `${at}.${flag} must be a boolean, got ${describeType(flagValue)}.`
288
+ );
289
+ }
290
+ }
291
+ });
292
+ }
293
+ if (!Array.isArray(rows)) {
294
+ problems.push(`rows must be an array, got ${describeType(rows)}.`);
295
+ } else {
296
+ rows.forEach((row, rowIndex) => {
297
+ if (!Array.isArray(row)) {
298
+ problems.push(
299
+ `rows[${rowIndex}] must be an array of cells, got ${describeType(row)}.`
300
+ );
301
+ return;
302
+ }
303
+ if (columnCount > 0 && row.length !== columnCount) {
304
+ problems.push(
305
+ `rows[${rowIndex}] has ${row.length} cells but there are ${columnCount} columns.`
306
+ );
307
+ }
308
+ row.forEach((cell, cellIndex) => {
309
+ const at = `rows[${rowIndex}][${cellIndex}]`;
310
+ if (typeof cell !== "object" || cell === null) {
311
+ problems.push(`${at} must be an object, got ${describeType(cell)}.`);
312
+ return;
313
+ }
314
+ const fields = cell;
315
+ if (typeof fields["text"] !== "string") {
316
+ problems.push(
317
+ `${at}.text must be a string, got ${describeType(fields["text"])}.`
318
+ );
319
+ }
320
+ const value = fields["value"];
321
+ if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
322
+ problems.push(
323
+ `${at}.value must be a string or a finite number, got ${describeType(value)}.`
324
+ );
325
+ }
326
+ const href = fields["href"];
327
+ if (href !== void 0) {
328
+ if (typeof href !== "string") {
329
+ problems.push(
330
+ `${at}.href must be a string, got ${describeType(href)}.`
331
+ );
332
+ } else if (!isSafeHref(href)) {
333
+ problems.push(
334
+ `${at}.href "${href}" is not allowed (only http, https, mailto and relative URLs).`
335
+ );
336
+ }
337
+ }
338
+ });
339
+ });
340
+ }
341
+ const totalRows = record["totalRows"];
342
+ if (totalRows !== void 0 && !(typeof totalRows === "number" && Number.isInteger(totalRows) && totalRows >= 0)) {
343
+ problems.push(
344
+ `totalRows must be a non-negative whole number, got ${describeType(totalRows)}.`
345
+ );
346
+ }
347
+ if (problems.length > 0) {
348
+ return err(`${subject}: ${problems.join(" ")}`);
349
+ }
350
+ return ok(data);
351
+ }
352
+ function tableDetailData(data) {
353
+ return {
354
+ columns: data.columns.map((column, index) => ({
355
+ key: `c${index}`,
356
+ label: column.label,
357
+ ...column.numeric === void 0 ? {} : { numeric: column.numeric }
358
+ })),
359
+ rows: data.rows
360
+ };
361
+ }
362
+ function deriveDetailData(data, locale) {
363
+ if (data.kind === "TABLE") {
364
+ return data.footer === void 0 ? tableDetailData(data) : void 0;
365
+ }
366
+ if (data.kind === "BAR_LIST") {
367
+ return {
368
+ columns: [
369
+ { key: "label", label: "Name" },
370
+ { key: "value", label: "Value", numeric: true }
371
+ ],
372
+ rows: data.items.map((item) => [
373
+ { text: item.label },
374
+ {
375
+ text: item.display ?? formatValue(item.value, "number", { locale }),
376
+ value: item.value
377
+ }
378
+ ])
379
+ };
380
+ }
381
+ return void 0;
382
+ }
383
+
384
+ // src/core/grid.ts
385
+ var WIDTH_COLUMNS = 12;
386
+ var MIN_WIDGET_WIDTH = 2;
387
+ var MAX_WIDGET_WIDTH = 12;
388
+ function isWidgetWidth(value) {
389
+ return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
390
+ }
391
+ function widthForSize(size) {
392
+ switch (size) {
393
+ case "small":
394
+ return 3;
395
+ case "large":
396
+ return 6;
397
+ case "full":
398
+ return 12;
399
+ default:
400
+ return 4;
401
+ }
402
+ }
403
+ function itemWidth(item) {
404
+ return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
405
+ }
406
+ function usesWidthColumns(items) {
407
+ return items.some((item) => item.width !== void 0);
408
+ }
409
+ function fillsWidth(fill) {
410
+ return fill === "width" || fill === "both";
411
+ }
412
+ function fillsHeight(fill) {
413
+ return fill === "height" || fill === "both";
414
+ }
415
+ function baseColumnSpan(size, columns, largeSpan = 2) {
416
+ if (size === "full") return columns;
417
+ if (size === "large") return Math.min(largeSpan, columns);
418
+ return 1;
419
+ }
420
+ function fillColumnSpans(items, columns, largeSpan = 2) {
421
+ return placeAndFill(
422
+ items,
423
+ columns,
424
+ (item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
425
+ );
426
+ }
427
+ function fillWidthSpans(items) {
428
+ return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
429
+ }
430
+ function placeAndFill(items, columns, spanOf) {
431
+ const trackCount = Math.max(1, Math.floor(columns));
432
+ const spans = /* @__PURE__ */ new Map();
433
+ let row = [];
434
+ let used = 0;
435
+ function closeRow() {
436
+ const fillers = row.filter(({ item }) => fillsWidth(item.fill));
437
+ const leftover = trackCount - used;
438
+ if (fillers.length > 0) {
439
+ const share = Math.floor(leftover / fillers.length);
440
+ const remainder = leftover % fillers.length;
441
+ fillers.forEach(({ item, span }, index) => {
442
+ spans.set(item.key, span + share + (index < remainder ? 1 : 0));
443
+ });
444
+ }
445
+ row = [];
446
+ used = 0;
148
447
  }
149
- const text = formatPercentChange(percent, options.locale);
150
- const direction = text === "\xB10%" ? "flat" : percent > 0 ? "up" : "down";
151
- const higherIsBetter = options.higherIsBetter ?? true;
152
- const sentiment = direction === "flat" ? "neutral" : direction === "up" === higherIsBetter ? "good" : "bad";
153
- return {
154
- percent,
155
- direction,
156
- sentiment,
157
- text,
158
- description: `${text} vs ${options.comparisonLabel ?? DEFAULT_COMPARISON_LABEL}`
159
- };
448
+ for (const item of items) {
449
+ const span = Math.min(spanOf(item, trackCount), trackCount);
450
+ if (used + span > trackCount && row.length > 0) closeRow();
451
+ row.push({ item, span });
452
+ used += span;
453
+ }
454
+ closeRow();
455
+ return spans;
160
456
  }
161
457
 
162
458
  // src/core/scale.ts
@@ -728,30 +1024,6 @@ function GraphWidget({
728
1024
  }
729
1025
  );
730
1026
  }
731
-
732
- // src/core/url.ts
733
- var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
734
- var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
735
- function schemeOf(url) {
736
- const match = SCHEME_PATTERN.exec(
737
- url.replace(IGNORED_IN_SCHEME, "")
738
- );
739
- return match === null ? null : (match[1] ?? "").toLowerCase();
740
- }
741
- function isSafeHref(url) {
742
- if (url.trim() === "") {
743
- return false;
744
- }
745
- const scheme = schemeOf(url);
746
- return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
747
- }
748
- function isSafeImageUrl(url) {
749
- if (url.trim() === "") {
750
- return false;
751
- }
752
- const scheme = schemeOf(url);
753
- return scheme === null || scheme === "http" || scheme === "https";
754
- }
755
1027
  function isNewTabTarget(target) {
756
1028
  return target !== void 0 && target !== "" && target !== "_self";
757
1029
  }
@@ -914,29 +1186,120 @@ function GaugeWidget({
914
1186
  ] });
915
1187
  }
916
1188
  function TableWidget({
917
- data
1189
+ data,
1190
+ controls
1191
+ }) {
1192
+ if (controls === void 0) {
1193
+ return /* @__PURE__ */ jsxs("div", { className: "dwt-table-wrap", children: [
1194
+ /* @__PURE__ */ jsx(TableView, { data }),
1195
+ data.footer !== void 0 && /* @__PURE__ */ jsx("p", { className: "dwt-note", children: data.footer })
1196
+ ] });
1197
+ }
1198
+ return /* @__PURE__ */ jsx(InteractiveTable, { data, controls });
1199
+ }
1200
+ function TableView({
1201
+ data,
1202
+ rows = data.rows,
1203
+ sort,
1204
+ onSort
918
1205
  }) {
919
1206
  const slot = useSlotClassName();
920
- return /* @__PURE__ */ jsxs("div", { className: "dwt-table-wrap", children: [
921
- /* @__PURE__ */ jsxs("table", { className: slot("table", "dwt-table"), children: [
922
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: data.columns.map((column, index) => /* @__PURE__ */ jsx(
1207
+ const { labels } = useWidgetSettings();
1208
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("table", { className: slot("table", "dwt-table"), children: [
1209
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: data.columns.map((column, index) => {
1210
+ const key = `c${index}`;
1211
+ const sorted = sort?.column === key;
1212
+ return /* @__PURE__ */ jsx(
923
1213
  "th",
924
1214
  {
925
1215
  scope: "col",
926
1216
  className: column.numeric === true ? "dwt-numeric" : void 0,
927
- children: column.label
1217
+ "aria-sort": sorted ? sort?.direction === "asc" ? "ascending" : "descending" : void 0,
1218
+ children: onSort === void 0 ? column.label : /* @__PURE__ */ jsxs(
1219
+ "button",
1220
+ {
1221
+ type: "button",
1222
+ className: "dwt-detail-sort",
1223
+ "aria-label": labels.sortBy(column.label),
1224
+ onClick: () => onSort(key),
1225
+ children: [
1226
+ column.label,
1227
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: !sorted ? " \u2195" : sort?.direction === "asc" ? " \u2191" : " \u2193" })
1228
+ ]
1229
+ }
1230
+ )
928
1231
  },
929
1232
  index
930
- )) }) }),
931
- /* @__PURE__ */ jsx("tbody", { children: data.rows.map((row, rowIndex) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, cellIndex) => /* @__PURE__ */ jsx(
932
- "td",
1233
+ );
1234
+ }) }) }),
1235
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, cellIndex) => /* @__PURE__ */ jsx(
1236
+ "td",
1237
+ {
1238
+ className: data.columns[cellIndex]?.numeric === true ? "dwt-numeric" : void 0,
1239
+ children: cell.href === void 0 ? cell.text : /* @__PURE__ */ jsx(WidgetLink, { href: cell.href, className: "dwt-link", children: cell.text })
1240
+ },
1241
+ cellIndex
1242
+ )) }, rowIndex)) })
1243
+ ] }) });
1244
+ }
1245
+ function nextTableSort(current, column) {
1246
+ if (current?.column !== column) return { column, direction: "asc" };
1247
+ return current.direction === "asc" ? { column, direction: "desc" } : void 0;
1248
+ }
1249
+ function InteractiveTable({
1250
+ data,
1251
+ controls
1252
+ }) {
1253
+ const slot = useSlotClassName();
1254
+ const { labels, locale } = useWidgetSettings();
1255
+ const searchId = useId();
1256
+ const [search, setSearch] = useState("");
1257
+ const [sort, setSort] = useState(void 0);
1258
+ const localeTag = typeof locale === "string" ? locale : locale?.[0];
1259
+ const query = {
1260
+ page: 1,
1261
+ pageSize: Math.max(1, data.rows.length),
1262
+ ...controls.search && search !== "" ? { search } : {},
1263
+ ...controls.sort && sort !== void 0 ? { sort } : {}
1264
+ };
1265
+ const result = queryRows(tableDetailData(data), query, localeTag);
1266
+ const page = result.ok ? result.value : void 0;
1267
+ return /* @__PURE__ */ jsxs("div", { className: "dwt-table-wrap", children: [
1268
+ controls.search && /* @__PURE__ */ jsxs("div", { className: "dwt-table-search", children: [
1269
+ /* @__PURE__ */ jsx("label", { htmlFor: searchId, className: "dwt-visually-hidden", children: labels.search }),
1270
+ /* @__PURE__ */ jsx(
1271
+ "input",
933
1272
  {
934
- className: data.columns[cellIndex]?.numeric === true ? "dwt-numeric" : void 0,
935
- children: cell.href === void 0 ? cell.text : /* @__PURE__ */ jsx(WidgetLink, { href: cell.href, className: "dwt-link", children: cell.text })
936
- },
937
- cellIndex
938
- )) }, rowIndex)) })
1273
+ id: searchId,
1274
+ type: "search",
1275
+ placeholder: labels.search,
1276
+ value: search,
1277
+ onChange: (event) => setSearch(event.target.value)
1278
+ }
1279
+ )
939
1280
  ] }),
1281
+ result.ok ? /* @__PURE__ */ jsxs(Fragment, { children: [
1282
+ /* @__PURE__ */ jsx(
1283
+ "p",
1284
+ {
1285
+ className: "dwt-visually-hidden",
1286
+ role: "status",
1287
+ "aria-live": "polite",
1288
+ "aria-atomic": "true",
1289
+ children: page !== void 0 && page.totalRows === 0 ? labels.noResults : page === void 0 ? "" : labels.showingRows(1, page.rows.length, page.totalRows)
1290
+ }
1291
+ ),
1292
+ /* @__PURE__ */ jsx(
1293
+ TableView,
1294
+ {
1295
+ data,
1296
+ rows: page?.rows ?? data.rows,
1297
+ sort,
1298
+ onSort: controls.sort ? (column) => setSort(nextTableSort(sort, column)) : void 0
1299
+ }
1300
+ ),
1301
+ page !== void 0 && page.totalRows === 0 && /* @__PURE__ */ jsx("p", { className: "dwt-note", children: labels.noResults })
1302
+ ] }) : /* @__PURE__ */ jsx("p", { role: "alert", className: slot("error", "dwt-error"), children: result.error }),
940
1303
  data.footer !== void 0 && /* @__PURE__ */ jsx("p", { className: "dwt-note", children: data.footer })
941
1304
  ] });
942
1305
  }
@@ -992,7 +1355,8 @@ function AlertListWidget({
992
1355
  ] });
993
1356
  }
994
1357
  function WidgetContent({
995
- data
1358
+ data,
1359
+ tableControls
996
1360
  }) {
997
1361
  switch (data.kind) {
998
1362
  case "TEXT":
@@ -1002,7 +1366,7 @@ function WidgetContent({
1002
1366
  case "GAUGE":
1003
1367
  return /* @__PURE__ */ jsx(GaugeWidget, { data });
1004
1368
  case "TABLE":
1005
- return /* @__PURE__ */ jsx(TableWidget, { data });
1369
+ return /* @__PURE__ */ jsx(TableWidget, { data, controls: tableControls });
1006
1370
  case "BAR_LIST":
1007
1371
  return /* @__PURE__ */ jsx(BarListWidget, { data });
1008
1372
  case "ALERT_LIST":
@@ -1193,7 +1557,13 @@ function ResolvedWidgetCard({
1193
1557
  ...definition.fill === void 0 ? {} : { fill: definition.fill },
1194
1558
  ...widget.status === "empty" ? { emptyText: widget.emptyText } : {},
1195
1559
  ...widget.status === "error" ? { error: widget.error } : {},
1196
- children: widget.status === "ok" ? /* @__PURE__ */ jsx(WidgetContent, { data: widget.data }) : null
1560
+ children: widget.status === "ok" ? /* @__PURE__ */ jsx(
1561
+ WidgetContent,
1562
+ {
1563
+ data: widget.data,
1564
+ tableControls: resolveTableControls(definition)
1565
+ }
1566
+ ) : null
1197
1567
  }
1198
1568
  );
1199
1569
  }
@@ -1207,342 +1577,82 @@ var EMPTY_LAYOUT = Object.freeze({
1207
1577
  function bySortOrder(items) {
1208
1578
  return items.map((item, index) => ({ item, index })).sort(
1209
1579
  (a, b) => (a.item.sortOrder ?? 0) - (b.item.sortOrder ?? 0) || a.index - b.index
1210
- ).map(({ item }) => item);
1211
- }
1212
- function visibleWidgets(definitions, layout) {
1213
- const hidden = new Set(layout.hidden);
1214
- const visible = definitions.filter((d) => !hidden.has(d.key));
1215
- const byKey = new Map(visible.map((d) => [d.key, d]));
1216
- const ordered = [];
1217
- for (const key of layout.order) {
1218
- const definition = byKey.get(key);
1219
- if (definition !== void 0) {
1220
- ordered.push(definition);
1221
- byKey.delete(key);
1222
- }
1223
- }
1224
- for (const definition of bySortOrder(visible)) {
1225
- if (byKey.has(definition.key)) ordered.push(definition);
1226
- }
1227
- return ordered;
1228
- }
1229
- function hiddenWidgets(definitions, layout) {
1230
- const hidden = new Set(layout.hidden);
1231
- return bySortOrder(definitions.filter((d) => hidden.has(d.key)));
1232
- }
1233
- function moveWidget(definitions, layout, key, toIndex) {
1234
- const current = visibleWidgets(definitions, layout).map(
1235
- (d) => d.key
1236
- );
1237
- const fromIndex = current.indexOf(key);
1238
- if (fromIndex === -1 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex) {
1239
- return layout;
1240
- }
1241
- const next = [...current];
1242
- next.splice(fromIndex, 1);
1243
- next.splice(toIndex, 0, key);
1244
- return { ...layout, order: next };
1245
- }
1246
- function moveWidgetBy(definitions, layout, key, offset) {
1247
- const current = visibleWidgets(definitions, layout).map(
1248
- (d) => d.key
1249
- );
1250
- const fromIndex = current.indexOf(key);
1251
- if (fromIndex === -1) return layout;
1252
- const toIndex = Math.max(
1253
- 0,
1254
- Math.min(current.length - 1, fromIndex + Math.trunc(offset))
1255
- );
1256
- return moveWidget(definitions, layout, key, toIndex);
1257
- }
1258
- function isHidden(layout, key) {
1259
- return layout.hidden.includes(key);
1260
- }
1261
- function isMinimized(layout, key) {
1262
- return layout.minimized.includes(key);
1263
- }
1264
- function hideWidget(layout, key) {
1265
- if (isHidden(layout, key)) return layout;
1266
- return { ...layout, hidden: [...layout.hidden, key] };
1267
- }
1268
- function showWidget(layout, key) {
1269
- if (!isHidden(layout, key)) return layout;
1270
- return {
1271
- ...layout,
1272
- hidden: layout.hidden.filter((k) => k !== key),
1273
- minimized: layout.minimized.filter((k) => k !== key)
1274
- };
1275
- }
1276
- function minimizeWidget(layout, key) {
1277
- if (isMinimized(layout, key)) return layout;
1278
- return { ...layout, minimized: [...layout.minimized, key] };
1279
- }
1280
- function restoreWidget(layout, key) {
1281
- if (!isMinimized(layout, key)) return layout;
1282
- return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
1283
- }
1284
- function toggleMinimized(layout, key) {
1285
- return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
1286
- }
1287
-
1288
- // src/core/result.ts
1289
- function ok(value) {
1290
- return { ok: true, value };
1291
- }
1292
- function err(error) {
1293
- return { ok: false, error };
1294
- }
1295
- function describeUnknownError(cause) {
1296
- if (cause instanceof Error) {
1297
- return cause.message === "" ? cause.name : cause.message;
1298
- }
1299
- if (typeof cause === "string") {
1300
- return cause;
1301
- }
1302
- try {
1303
- const json = JSON.stringify(cause);
1304
- return json ?? String(cause);
1305
- } catch {
1306
- return String(cause);
1307
- }
1308
- }
1309
-
1310
- // src/core/detail.ts
1311
- var DEFAULT_DETAIL_PAGE_SIZE = 25;
1312
- function resolveDetailOptions(definition) {
1313
- const detail = definition.detail;
1314
- if (detail === void 0 || detail === false) return void 0;
1315
- const options = detail === true ? {} : detail;
1316
- return {
1317
- title: options.title ?? definition.title,
1318
- pageSize: options.pageSize ?? DEFAULT_DETAIL_PAGE_SIZE,
1319
- mode: options.mode ?? "client"
1320
- };
1321
- }
1322
- function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
1323
- return { page: 1, pageSize };
1324
- }
1325
- function fold(text) {
1326
- return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
1327
- }
1328
- function cellValue(cell) {
1329
- return cell.value ?? cell.text;
1330
- }
1331
- function cellMatches(cell, needle) {
1332
- return fold(String(cellValue(cell))).includes(needle);
1333
- }
1334
- function compareCells(a, b, collator) {
1335
- const left = cellValue(a);
1336
- const right = cellValue(b);
1337
- if (typeof left === "number" && typeof right === "number") {
1338
- return left - right;
1339
- }
1340
- if (typeof left === "number") return -1;
1341
- if (typeof right === "number") return 1;
1342
- return collator.compare(left, right);
1580
+ ).map(({ item }) => item);
1343
1581
  }
1344
- function queryRows(data, query, locale) {
1345
- const indexOf = new Map(
1346
- data.columns.map((column, index) => [column.key, index])
1347
- );
1348
- const filterEntries = [];
1349
- for (const [key, text] of Object.entries(query.filters ?? {})) {
1350
- const index = indexOf.get(key);
1351
- if (index === void 0) {
1352
- return err(
1353
- `Detail query: filter column "${key}" does not exist. Columns: ${listKeys(data)}.`
1354
- );
1582
+ function visibleWidgets(definitions, layout) {
1583
+ const hidden = new Set(layout.hidden);
1584
+ const visible = definitions.filter((d) => !hidden.has(d.key));
1585
+ const byKey = new Map(visible.map((d) => [d.key, d]));
1586
+ const ordered = [];
1587
+ for (const key of layout.order) {
1588
+ const definition = byKey.get(key);
1589
+ if (definition !== void 0) {
1590
+ ordered.push(definition);
1591
+ byKey.delete(key);
1355
1592
  }
1356
- const needle = fold(text.trim());
1357
- if (needle !== "") filterEntries.push({ index, needle });
1358
1593
  }
1359
- let sortIndex = -1;
1360
- if (query.sort !== void 0) {
1361
- const found = indexOf.get(query.sort.column);
1362
- const column = found === void 0 ? void 0 : data.columns[found];
1363
- if (found === void 0 || column === void 0) {
1364
- return err(
1365
- `Detail query: sort column "${query.sort.column}" does not exist. Columns: ${listKeys(data)}.`
1366
- );
1367
- }
1368
- if (column.sortable === false) {
1369
- return err(
1370
- `Detail query: column "${column.key}" is not sortable (sortable: false).`
1371
- );
1372
- }
1373
- sortIndex = found;
1594
+ for (const definition of bySortOrder(visible)) {
1595
+ if (byKey.has(definition.key)) ordered.push(definition);
1374
1596
  }
1375
- const search = fold((query.search ?? "").trim());
1376
- let rows = data.rows.filter(
1377
- (row) => filterEntries.every(({ index, needle }) => {
1378
- const cell = row[index];
1379
- return cell !== void 0 && cellMatches(cell, needle);
1380
- }) && (search === "" || row.some((cell) => cellMatches(cell, search)))
1597
+ return ordered;
1598
+ }
1599
+ function hiddenWidgets(definitions, layout) {
1600
+ const hidden = new Set(layout.hidden);
1601
+ return bySortOrder(definitions.filter((d) => hidden.has(d.key)));
1602
+ }
1603
+ function moveWidget(definitions, layout, key, toIndex) {
1604
+ const current = visibleWidgets(definitions, layout).map(
1605
+ (d) => d.key
1381
1606
  );
1382
- if (query.sort !== void 0 && sortIndex >= 0) {
1383
- const collator = new Intl.Collator(locale, {
1384
- numeric: true,
1385
- sensitivity: "base"
1386
- });
1387
- const sign = query.sort.direction === "desc" ? -1 : 1;
1388
- rows = rows.map((row, position) => ({ row, position })).sort((a, b) => {
1389
- const left = a.row[sortIndex];
1390
- const right = b.row[sortIndex];
1391
- const order = left === void 0 || right === void 0 ? 0 : sign * compareCells(left, right, collator);
1392
- return order || a.position - b.position;
1393
- }).map(({ row }) => row);
1607
+ const fromIndex = current.indexOf(key);
1608
+ if (fromIndex === -1 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex) {
1609
+ return layout;
1394
1610
  }
1395
- const pageSize = Math.max(1, Math.floor(query.pageSize));
1396
- const pageCount = Math.max(1, Math.ceil(rows.length / pageSize));
1397
- const page = Math.min(Math.max(1, Math.floor(query.page)), pageCount);
1398
- return ok({
1399
- rows: rows.slice((page - 1) * pageSize, page * pageSize),
1400
- totalRows: rows.length,
1401
- page,
1402
- pageCount
1403
- });
1611
+ const next = [...current];
1612
+ next.splice(fromIndex, 1);
1613
+ next.splice(toIndex, 0, key);
1614
+ return { ...layout, order: next };
1404
1615
  }
1405
- function listKeys(data) {
1406
- return data.columns.map((column) => `"${column.key}"`).join(", ");
1616
+ function moveWidgetBy(definitions, layout, key, offset) {
1617
+ const current = visibleWidgets(definitions, layout).map(
1618
+ (d) => d.key
1619
+ );
1620
+ const fromIndex = current.indexOf(key);
1621
+ if (fromIndex === -1) return layout;
1622
+ const toIndex = Math.max(
1623
+ 0,
1624
+ Math.min(current.length - 1, fromIndex + Math.trunc(offset))
1625
+ );
1626
+ return moveWidget(definitions, layout, key, toIndex);
1407
1627
  }
1408
- function describeType(value) {
1409
- if (value === null) return "null";
1410
- if (Array.isArray(value)) return "array";
1411
- return typeof value;
1628
+ function isHidden(layout, key) {
1629
+ return layout.hidden.includes(key);
1412
1630
  }
1413
- function validateDetailData(data, subject = "Detail data") {
1414
- const problems = [];
1415
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
1416
- return err(`${subject}: must be an object, got ${describeType(data)}.`);
1417
- }
1418
- const record = data;
1419
- const columns = record["columns"];
1420
- const rows = record["rows"];
1421
- const keys = /* @__PURE__ */ new Set();
1422
- let columnCount = 0;
1423
- if (!Array.isArray(columns) || columns.length === 0) {
1424
- problems.push(
1425
- `columns must be a non-empty array, got ${describeType(columns)}.`
1426
- );
1427
- } else {
1428
- columnCount = columns.length;
1429
- columns.forEach((column, index) => {
1430
- const at = `columns[${index}]`;
1431
- if (typeof column !== "object" || column === null) {
1432
- problems.push(`${at} must be an object, got ${describeType(column)}.`);
1433
- return;
1434
- }
1435
- const fields = column;
1436
- const key = fields["key"];
1437
- if (typeof key !== "string" || key === "") {
1438
- problems.push(`${at}.key must be a non-empty string.`);
1439
- } else if (keys.has(key)) {
1440
- problems.push(`${at}.key "${key}" is used by an earlier column.`);
1441
- } else {
1442
- keys.add(key);
1443
- }
1444
- if (typeof fields["label"] !== "string") {
1445
- problems.push(
1446
- `${at}.label must be a string, got ${describeType(fields["label"])}.`
1447
- );
1448
- }
1449
- for (const flag of ["numeric", "sortable", "filterable"]) {
1450
- const flagValue = fields[flag];
1451
- if (flagValue !== void 0 && typeof flagValue !== "boolean") {
1452
- problems.push(
1453
- `${at}.${flag} must be a boolean, got ${describeType(flagValue)}.`
1454
- );
1455
- }
1456
- }
1457
- });
1458
- }
1459
- if (!Array.isArray(rows)) {
1460
- problems.push(`rows must be an array, got ${describeType(rows)}.`);
1461
- } else {
1462
- rows.forEach((row, rowIndex) => {
1463
- if (!Array.isArray(row)) {
1464
- problems.push(
1465
- `rows[${rowIndex}] must be an array of cells, got ${describeType(row)}.`
1466
- );
1467
- return;
1468
- }
1469
- if (columnCount > 0 && row.length !== columnCount) {
1470
- problems.push(
1471
- `rows[${rowIndex}] has ${row.length} cells but there are ${columnCount} columns.`
1472
- );
1473
- }
1474
- row.forEach((cell, cellIndex) => {
1475
- const at = `rows[${rowIndex}][${cellIndex}]`;
1476
- if (typeof cell !== "object" || cell === null) {
1477
- problems.push(`${at} must be an object, got ${describeType(cell)}.`);
1478
- return;
1479
- }
1480
- const fields = cell;
1481
- if (typeof fields["text"] !== "string") {
1482
- problems.push(
1483
- `${at}.text must be a string, got ${describeType(fields["text"])}.`
1484
- );
1485
- }
1486
- const value = fields["value"];
1487
- if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
1488
- problems.push(
1489
- `${at}.value must be a string or a finite number, got ${describeType(value)}.`
1490
- );
1491
- }
1492
- const href = fields["href"];
1493
- if (href !== void 0) {
1494
- if (typeof href !== "string") {
1495
- problems.push(
1496
- `${at}.href must be a string, got ${describeType(href)}.`
1497
- );
1498
- } else if (!isSafeHref(href)) {
1499
- problems.push(
1500
- `${at}.href "${href}" is not allowed (only http, https, mailto and relative URLs).`
1501
- );
1502
- }
1503
- }
1504
- });
1505
- });
1506
- }
1507
- const totalRows = record["totalRows"];
1508
- if (totalRows !== void 0 && !(typeof totalRows === "number" && Number.isInteger(totalRows) && totalRows >= 0)) {
1509
- problems.push(
1510
- `totalRows must be a non-negative whole number, got ${describeType(totalRows)}.`
1511
- );
1512
- }
1513
- if (problems.length > 0) {
1514
- return err(`${subject}: ${problems.join(" ")}`);
1515
- }
1516
- return ok(data);
1631
+ function isMinimized(layout, key) {
1632
+ return layout.minimized.includes(key);
1517
1633
  }
1518
- function deriveDetailData(data, locale) {
1519
- if (data.kind === "TABLE") {
1520
- if (data.footer !== void 0) return void 0;
1521
- return {
1522
- columns: data.columns.map((column, index) => ({
1523
- key: `c${index}`,
1524
- label: column.label,
1525
- ...column.numeric === void 0 ? {} : { numeric: column.numeric }
1526
- })),
1527
- rows: data.rows
1528
- };
1529
- }
1530
- if (data.kind === "BAR_LIST") {
1531
- return {
1532
- columns: [
1533
- { key: "label", label: "Name" },
1534
- { key: "value", label: "Value", numeric: true }
1535
- ],
1536
- rows: data.items.map((item) => [
1537
- { text: item.label },
1538
- {
1539
- text: item.display ?? formatValue(item.value, "number", { locale }),
1540
- value: item.value
1541
- }
1542
- ])
1543
- };
1544
- }
1545
- return void 0;
1634
+ function hideWidget(layout, key) {
1635
+ if (isHidden(layout, key)) return layout;
1636
+ return { ...layout, hidden: [...layout.hidden, key] };
1637
+ }
1638
+ function showWidget(layout, key) {
1639
+ if (!isHidden(layout, key)) return layout;
1640
+ return {
1641
+ ...layout,
1642
+ hidden: layout.hidden.filter((k) => k !== key),
1643
+ minimized: layout.minimized.filter((k) => k !== key)
1644
+ };
1645
+ }
1646
+ function minimizeWidget(layout, key) {
1647
+ if (isMinimized(layout, key)) return layout;
1648
+ return { ...layout, minimized: [...layout.minimized, key] };
1649
+ }
1650
+ function restoreWidget(layout, key) {
1651
+ if (!isMinimized(layout, key)) return layout;
1652
+ return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
1653
+ }
1654
+ function toggleMinimized(layout, key) {
1655
+ return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
1546
1656
  }
1547
1657
  function nextSort(current, column) {
1548
1658
  if (current?.column !== column) return { column, direction: "asc" };
@@ -2220,6 +2330,14 @@ function optionalNumber(record, field, path, problems) {
2220
2330
  requireNumber(record, field, path, problems);
2221
2331
  }
2222
2332
  }
2333
+ function optionalSortValue(record, path, problems) {
2334
+ const value = record["value"];
2335
+ if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
2336
+ problems.push(
2337
+ `${path}.value must be a string or a finite number, got ${describeType2(value)}.`
2338
+ );
2339
+ }
2340
+ }
2223
2341
  function optionalBoolean(record, field, path, problems) {
2224
2342
  const value = record[field];
2225
2343
  if (value !== void 0 && typeof value !== "boolean") {
@@ -2323,6 +2441,7 @@ function checkPayloadFields(kind, data, problems) {
2323
2441
  }
2324
2442
  eachRecord(row, rowPath, problems, (cell, cellPath) => {
2325
2443
  requireString(cell, "text", cellPath, problems);
2444
+ optionalSortValue(cell, cellPath, problems);
2326
2445
  optionalUrl(
2327
2446
  cell,
2328
2447
  "href",