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