@richardmcquiston01/dashboard-widgets-toolkit 0.14.0 → 0.16.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/core.cjs CHANGED
@@ -1,88 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // src/core/payload.ts
4
- var WIDGET_KINDS = [
5
- "TEXT",
6
- "KPI",
7
- "GAUGE",
8
- "TABLE",
9
- "BAR_LIST",
10
- "ALERT_LIST",
11
- "GRAPH"
12
- ];
13
- function isWidgetKind(value) {
14
- return typeof value === "string" && WIDGET_KINDS.includes(value);
15
- }
16
- var VALUE_FORMATS = ["number", "currency", "percent"];
17
- var MAX_GRAPH_SERIES = 8;
18
- var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
19
- function emptyWidget(text = DEFAULT_EMPTY_TEXT) {
20
- return { empty: true, text };
21
- }
22
- function isWidgetEmptyState(payload) {
23
- return "empty" in payload && payload.empty === true;
24
- }
25
- function emptyTextFor(payload) {
26
- if (isWidgetEmptyState(payload)) {
27
- return payload.text;
28
- }
29
- switch (payload.kind) {
30
- case "TABLE":
31
- return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
32
- case "BAR_LIST":
33
- return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
34
- case "ALERT_LIST":
35
- return payload.items.length === 0 ? payload.emptyText : null;
36
- case "GRAPH":
37
- return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
38
- case "TEXT":
39
- case "KPI":
40
- case "GAUGE":
41
- return null;
42
- }
43
- }
44
-
45
- // src/core/compat.ts
46
- function isStringArray(value) {
47
- return Array.isArray(value) && value.every((v) => typeof v === "string");
48
- }
49
- function upgradeLegacyWidgetData(value) {
50
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
51
- return value;
52
- }
53
- const record = value;
54
- if (record["kind"] === "TABLE" && isStringArray(record["columns"]) && Array.isArray(record["rows"]) && record["rows"].every(isStringArray)) {
55
- const rows = record["rows"];
56
- return {
57
- ...record,
58
- columns: record["columns"].map((label) => ({ label })),
59
- rows: rows.map((row) => row.map((text) => ({ text })))
60
- };
61
- }
62
- if (record["kind"] === "GRAPH" && Array.isArray(record["points"]) && record["series"] === void 0) {
63
- const { points, ...rest } = record;
64
- return {
65
- ...rest,
66
- series: [{ name: "Value", points }],
67
- chartType: "bar",
68
- valueFormat: "number"
69
- };
70
- }
71
- return value;
72
- }
73
- function fromLegacyDefinition(legacy) {
74
- if (!isWidgetKind(legacy.viewType)) {
75
- return null;
76
- }
77
- return {
78
- key: legacy.widgetKey,
79
- title: legacy.title,
80
- ...legacy.description === null ? {} : { description: legacy.description },
81
- kind: legacy.viewType,
82
- sortOrder: legacy.sortOrder
83
- };
84
- }
85
-
86
3
  // src/core/definition.ts
87
4
  var WIDGET_SIZES = ["small", "medium", "large", "full"];
88
5
  var WIDGET_FILLS = ["height", "width", "both"];
@@ -90,13 +7,15 @@ var NOT_LOCKED = Object.freeze({
90
7
  move: false,
91
8
  hide: false,
92
9
  minimize: false,
93
- options: false
10
+ options: false,
11
+ clone: false
94
12
  });
95
13
  var FULLY_LOCKED = Object.freeze({
96
14
  move: true,
97
15
  hide: true,
98
16
  minimize: true,
99
- options: true
17
+ options: true,
18
+ clone: true
100
19
  });
101
20
  function resolveWidgetLock(definition) {
102
21
  const setting = definition.locked;
@@ -106,7 +25,8 @@ function resolveWidgetLock(definition) {
106
25
  move: setting.move === true,
107
26
  hide: setting.hide === true,
108
27
  minimize: setting.minimize === true,
109
- options: setting.options === true
28
+ options: setting.options === true,
29
+ clone: setting.clone === true
110
30
  };
111
31
  }
112
32
  function defineWidget(definition) {
@@ -130,6 +50,103 @@ function isWidgetVisibleTo(definition, roles) {
130
50
  return definition.roles.some((role) => roles.includes(role));
131
51
  }
132
52
 
53
+ // src/core/grid.ts
54
+ var WIDTH_COLUMNS = 12;
55
+ var MIN_WIDGET_WIDTH = 2;
56
+ var MAX_WIDGET_WIDTH = 12;
57
+ function isWidgetWidth(value) {
58
+ return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
59
+ }
60
+ function widthForSize(size) {
61
+ switch (size) {
62
+ case "small":
63
+ return 3;
64
+ case "large":
65
+ return 6;
66
+ case "full":
67
+ return 12;
68
+ default:
69
+ return 4;
70
+ }
71
+ }
72
+ function itemWidth(item) {
73
+ return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
74
+ }
75
+ function usesWidthColumns(items) {
76
+ return items.some((item) => item.width !== void 0);
77
+ }
78
+ function fillsWidth(fill) {
79
+ return fill === "width" || fill === "both";
80
+ }
81
+ function fillsHeight(fill) {
82
+ return fill === "height" || fill === "both";
83
+ }
84
+ function baseColumnSpan(size, columns, largeSpan = 2) {
85
+ if (size === "full") return columns;
86
+ if (size === "large") return Math.min(largeSpan, columns);
87
+ return 1;
88
+ }
89
+ function fillColumnSpans(items, columns, largeSpan = 2) {
90
+ return placeAndFill(
91
+ items,
92
+ columns,
93
+ (item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
94
+ );
95
+ }
96
+ function fillWidthSpans(items) {
97
+ return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
98
+ }
99
+ function placeRows(items) {
100
+ return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
101
+ }
102
+ function fitCount(items, maxRows) {
103
+ const rows = placeRows(items);
104
+ let count = 0;
105
+ for (const row of rows.slice(0, Math.max(0, Math.floor(maxRows)))) {
106
+ count += row.keys.length;
107
+ }
108
+ return count;
109
+ }
110
+ function placeAndFill(items, columns, spanOf) {
111
+ return placeAndFillRows(items, columns, spanOf).spans;
112
+ }
113
+ function placeAndFillRows(items, columns, spanOf) {
114
+ const trackCount = Math.max(1, Math.floor(columns));
115
+ const spans = /* @__PURE__ */ new Map();
116
+ const rows = [];
117
+ let row = [];
118
+ let used = 0;
119
+ function closeRow() {
120
+ if (row.length === 0) return;
121
+ const fillers = row.filter(({ item }) => fillsWidth(item.fill));
122
+ const leftover = trackCount - used;
123
+ const finalSpans = /* @__PURE__ */ new Map();
124
+ if (fillers.length > 0) {
125
+ const share = Math.floor(leftover / fillers.length);
126
+ const remainder = leftover % fillers.length;
127
+ fillers.forEach(({ item, span }, index) => {
128
+ const grown = span + share + (index < remainder ? 1 : 0);
129
+ spans.set(item.key, grown);
130
+ finalSpans.set(item.key, grown);
131
+ });
132
+ }
133
+ rows.push({
134
+ keys: row.map(({ item }) => item.key),
135
+ spans: row.map(({ item, span }) => finalSpans.get(item.key) ?? span)
136
+ });
137
+ row = [];
138
+ used = 0;
139
+ }
140
+ for (const item of items) {
141
+ const span = Math.min(spanOf(item, trackCount), trackCount);
142
+ if (used + span > trackCount && row.length > 0) closeRow();
143
+ row.push({ item, span });
144
+ used += span;
145
+ }
146
+ closeRow();
147
+ return { spans, rows };
148
+ }
149
+
133
150
  // src/core/format.ts
134
151
  var DEFAULT_CURRENCY = "USD";
135
152
  function localeArgument(locale) {
@@ -215,6 +232,48 @@ function kpiDelta(current, previous, options = {}) {
215
232
  };
216
233
  }
217
234
 
235
+ // src/core/payload.ts
236
+ var WIDGET_KINDS = [
237
+ "TEXT",
238
+ "KPI",
239
+ "GAUGE",
240
+ "TABLE",
241
+ "BAR_LIST",
242
+ "ALERT_LIST",
243
+ "GRAPH"
244
+ ];
245
+ function isWidgetKind(value) {
246
+ return typeof value === "string" && WIDGET_KINDS.includes(value);
247
+ }
248
+ var VALUE_FORMATS = ["number", "currency", "percent"];
249
+ var MAX_GRAPH_SERIES = 8;
250
+ var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
251
+ function emptyWidget(text = DEFAULT_EMPTY_TEXT) {
252
+ return { empty: true, text };
253
+ }
254
+ function isWidgetEmptyState(payload) {
255
+ return "empty" in payload && payload.empty === true;
256
+ }
257
+ function emptyTextFor(payload) {
258
+ if (isWidgetEmptyState(payload)) {
259
+ return payload.text;
260
+ }
261
+ switch (payload.kind) {
262
+ case "TABLE":
263
+ return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
264
+ case "BAR_LIST":
265
+ return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
266
+ case "ALERT_LIST":
267
+ return payload.items.length === 0 ? payload.emptyText : null;
268
+ case "GRAPH":
269
+ return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
270
+ case "TEXT":
271
+ case "KPI":
272
+ case "GAUGE":
273
+ return null;
274
+ }
275
+ }
276
+
218
277
  // src/core/result.ts
219
278
  function ok(value) {
220
279
  return { ok: true, value };
@@ -237,41 +296,298 @@ function describeUnknownError(cause) {
237
296
  }
238
297
  }
239
298
 
240
- // src/core/url.ts
241
- var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
242
- var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
243
- function schemeOf(url) {
244
- const match = SCHEME_PATTERN.exec(
245
- url.replace(IGNORED_IN_SCHEME, "")
246
- );
247
- return match === null ? null : (match[1] ?? "").toLowerCase();
299
+ // src/core/views.ts
300
+ var BASE_VIEW_KEY = "base";
301
+ var MAX_WIDGET_VIEWS = 8;
302
+ var VIEW_CONVERSIONS = {
303
+ tableToBarList: { from: "TABLE", to: "BAR_LIST" },
304
+ tableToGraph: { from: "TABLE", to: "GRAPH" },
305
+ barListToTable: { from: "BAR_LIST", to: "TABLE" },
306
+ graphToTable: { from: "GRAPH", to: "TABLE" },
307
+ barListToGraph: { from: "BAR_LIST", to: "GRAPH" }
308
+ };
309
+ var KIND_LABELS = {
310
+ TEXT: "Text",
311
+ KPI: "Number",
312
+ GAUGE: "Gauge",
313
+ TABLE: "Table",
314
+ BAR_LIST: "Bars",
315
+ ALERT_LIST: "List",
316
+ GRAPH: "Chart"
317
+ };
318
+ function kindLabel(kind) {
319
+ return KIND_LABELS[kind];
248
320
  }
249
- function isSafeHref(url) {
250
- if (url.trim() === "") {
251
- return false;
252
- }
253
- const scheme = schemeOf(url);
254
- return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
321
+ function hasViews(item) {
322
+ return item.views !== void 0 && item.views.length > 0;
255
323
  }
256
- function isSafeImageUrl(url) {
257
- if (url.trim() === "") {
258
- return false;
259
- }
260
- const scheme = schemeOf(url);
261
- return scheme === null || scheme === "http" || scheme === "https";
324
+ function findView(item, key) {
325
+ return item.views?.find((view) => view.key === key);
262
326
  }
263
-
264
- // src/core/detail.ts
265
- var DETAIL_MODES = ["client", "server"];
266
- var DEFAULT_DETAIL_PAGE_SIZE = 25;
267
- var MAX_DETAIL_PAGE_SIZE = 200;
268
- function resolveDetailOptions(definition) {
269
- const detail = definition.detail;
270
- if (detail === void 0 || detail === false) return void 0;
271
- const options = detail === true ? {} : detail;
272
- return {
273
- title: options.title ?? definition.title,
274
- pageSize: options.pageSize ?? DEFAULT_DETAIL_PAGE_SIZE,
327
+ function viewChoices(item) {
328
+ if (!hasViews(item)) return [];
329
+ return [
330
+ {
331
+ key: BASE_VIEW_KEY,
332
+ label: item.baseViewLabel ?? kindLabel(item.kind),
333
+ kind: item.kind
334
+ },
335
+ ...(item.views ?? []).map(({ key, label, kind }) => ({
336
+ key,
337
+ label,
338
+ kind
339
+ }))
340
+ ];
341
+ }
342
+ function isKnownView(item, key) {
343
+ return key !== void 0 && (key === BASE_VIEW_KEY || findView(item, key) !== void 0);
344
+ }
345
+ function effectiveViewKey(item, chosen) {
346
+ if (!hasViews(item)) return BASE_VIEW_KEY;
347
+ if (isKnownView(item, chosen)) return chosen;
348
+ if (isKnownView(item, item.defaultView)) return item.defaultView;
349
+ return BASE_VIEW_KEY;
350
+ }
351
+ function viewKind(item, chosen) {
352
+ return findView(item, effectiveViewKey(item, chosen))?.kind ?? item.kind;
353
+ }
354
+ function cellValue(cell) {
355
+ const value = cell?.value;
356
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
357
+ }
358
+ function columnName(table, index) {
359
+ return table.columns[index]?.label ?? `#${index + 1}`;
360
+ }
361
+ function checkColumn(table, index, field, context) {
362
+ if (!Number.isInteger(index) || index < 0 || index >= table.columns.length) {
363
+ return `${context.where}: ${field} column ${index} does not exist (the table has ${table.columns.length} columns, numbered from 0).`;
364
+ }
365
+ return void 0;
366
+ }
367
+ function numericColumn(table, index, context) {
368
+ const numbers = [];
369
+ for (const [rowIndex, row] of table.rows.entries()) {
370
+ const value = cellValue(row[index]);
371
+ if (value === void 0) {
372
+ return err(
373
+ `${context.where}: row ${rowIndex + 1}, column ${index + 1} ("${columnName(table, index)}") has no numeric value. Add value to the cells.`
374
+ );
375
+ }
376
+ numbers.push(value);
377
+ }
378
+ return ok(numbers);
379
+ }
380
+ function tableToBarList(table, conversion, context) {
381
+ const bad = checkColumn(table, conversion.label, "label", context) ?? checkColumn(table, conversion.value, "value", context);
382
+ if (bad !== void 0) return err(bad);
383
+ const numbers = numericColumn(table, conversion.value, context);
384
+ if (!numbers.ok) return numbers;
385
+ const items = table.rows.map((row, index) => ({
386
+ label: row[conversion.label]?.text ?? "",
387
+ value: numbers.value[index],
388
+ display: row[conversion.value]?.text ?? String(numbers.value[index])
389
+ }));
390
+ return ok({ kind: "BAR_LIST", items });
391
+ }
392
+ function tableToGraph(table, conversion, context) {
393
+ if (conversion.values.length === 0 || conversion.values.length > MAX_GRAPH_SERIES) {
394
+ return err(
395
+ `${context.where}: a chart needs 1 to ${MAX_GRAPH_SERIES} series columns, got ${conversion.values.length}.`
396
+ );
397
+ }
398
+ let bad = checkColumn(
399
+ table,
400
+ conversion.label,
401
+ "label",
402
+ context
403
+ );
404
+ for (const index of conversion.values) {
405
+ bad ??= checkColumn(table, index, "series", context);
406
+ }
407
+ if (bad !== void 0) return err(bad);
408
+ const series = [];
409
+ for (const index of conversion.values) {
410
+ const numbers = numericColumn(table, index, context);
411
+ if (!numbers.ok) return numbers;
412
+ series.push({
413
+ name: columnName(table, index),
414
+ points: table.rows.map((row, rowIndex) => ({
415
+ label: row[conversion.label]?.text ?? "",
416
+ value: numbers.value[rowIndex]
417
+ }))
418
+ });
419
+ }
420
+ return ok({
421
+ kind: "GRAPH",
422
+ series,
423
+ chartType: conversion.chartType,
424
+ valueFormat: conversion.valueFormat ?? "number",
425
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency },
426
+ xLabel: columnName(table, conversion.label)
427
+ });
428
+ }
429
+ function barListToTable(list, conversion) {
430
+ return ok({
431
+ kind: "TABLE",
432
+ columns: [
433
+ { label: conversion.labelHeading ?? "Name" },
434
+ { label: conversion.valueHeading ?? "Value", numeric: true }
435
+ ],
436
+ rows: list.items.map((item) => [
437
+ { text: item.label },
438
+ { text: item.display ?? String(item.value), value: item.value }
439
+ ])
440
+ });
441
+ }
442
+ function graphToTable(graph, context) {
443
+ const first = graph.series[0];
444
+ if (first === void 0)
445
+ return err(`${context.where}: the chart has no series.`);
446
+ const format = {
447
+ ...graph.currency === void 0 ? {} : { currency: graph.currency },
448
+ ...context.locale === void 0 ? {} : { locale: context.locale }
449
+ };
450
+ return ok({
451
+ kind: "TABLE",
452
+ columns: [
453
+ { label: graph.xLabel ?? "Label" },
454
+ ...graph.series.map((series) => ({ label: series.name, numeric: true }))
455
+ ],
456
+ rows: first.points.map((point, index) => [
457
+ { text: point.label },
458
+ ...graph.series.map((series) => {
459
+ const value = series.points[index]?.value;
460
+ return value === void 0 ? { text: "\u2014" } : { text: formatValue(value, graph.valueFormat, format), value };
461
+ })
462
+ ])
463
+ });
464
+ }
465
+ function barListToGraph(list, conversion) {
466
+ return ok({
467
+ kind: "GRAPH",
468
+ series: [
469
+ {
470
+ name: conversion.seriesName ?? "Value",
471
+ points: list.items.map((item) => ({
472
+ label: item.label,
473
+ value: item.value
474
+ }))
475
+ }
476
+ ],
477
+ chartType: conversion.chartType,
478
+ valueFormat: conversion.valueFormat ?? "number",
479
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency }
480
+ });
481
+ }
482
+ function convertView(widgetKey, view, data, options = {}) {
483
+ const conversion = view.convert;
484
+ const context = {
485
+ where: `Widget ${JSON.stringify(widgetKey)} view ${JSON.stringify(view.key)}`,
486
+ ...options.locale === void 0 ? {} : { locale: options.locale }
487
+ };
488
+ const expected = VIEW_CONVERSIONS[conversion.type]?.from;
489
+ if (expected === void 0) {
490
+ return err(
491
+ `${context.where}: unknown conversion type ${JSON.stringify(conversion.type)}.`
492
+ );
493
+ }
494
+ if (data.kind !== expected) {
495
+ return err(
496
+ `${context.where}: ${conversion.type} reads a ${expected} payload, but the widget returned ${data.kind}.`
497
+ );
498
+ }
499
+ switch (conversion.type) {
500
+ case "tableToBarList":
501
+ return tableToBarList(data, conversion, context);
502
+ case "tableToGraph":
503
+ return tableToGraph(data, conversion, context);
504
+ case "barListToTable":
505
+ return barListToTable(data, conversion);
506
+ case "graphToTable":
507
+ return graphToTable(data, context);
508
+ case "barListToGraph":
509
+ return barListToGraph(data, conversion);
510
+ }
511
+ }
512
+ function viewNote(view, data) {
513
+ if (data?.kind !== "TABLE") return "";
514
+ const conversion = view.convert;
515
+ let indexes;
516
+ if (conversion.type === "tableToBarList") {
517
+ indexes = [conversion.label, conversion.value];
518
+ } else if (conversion.type === "tableToGraph") {
519
+ indexes = [conversion.label, ...conversion.values];
520
+ } else {
521
+ return "";
522
+ }
523
+ const names = indexes.map(
524
+ (index) => data.columns[index]?.label ?? `#${index + 1}`
525
+ );
526
+ return `Shows ${names.join(", ")} only.`;
527
+ }
528
+ function applyView(widget, chosen, options = {}) {
529
+ const key = effectiveViewKey(widget.definition, chosen);
530
+ const view = findView(widget.definition, key);
531
+ if (view === void 0) return widget;
532
+ const definition = {
533
+ ...widget.definition,
534
+ kind: view.kind
535
+ };
536
+ if (widget.status === "loading") return { ...widget, definition };
537
+ if (widget.status === "empty") return { ...widget, definition };
538
+ if (widget.status === "error") return { ...widget, definition };
539
+ const converted = convertView(
540
+ widget.definition.key,
541
+ view,
542
+ widget.data,
543
+ options
544
+ );
545
+ if (!converted.ok) {
546
+ return {
547
+ definition,
548
+ status: "error",
549
+ error: converted.error,
550
+ ...widget.options === void 0 ? {} : { options: widget.options }
551
+ };
552
+ }
553
+ return { ...widget, definition, data: converted.value };
554
+ }
555
+
556
+ // src/core/url.ts
557
+ var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
558
+ var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
559
+ function schemeOf(url) {
560
+ const match = SCHEME_PATTERN.exec(
561
+ url.replace(IGNORED_IN_SCHEME, "")
562
+ );
563
+ return match === null ? null : (match[1] ?? "").toLowerCase();
564
+ }
565
+ function isSafeHref(url) {
566
+ if (url.trim() === "") {
567
+ return false;
568
+ }
569
+ const scheme = schemeOf(url);
570
+ return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
571
+ }
572
+ function isSafeImageUrl(url) {
573
+ if (url.trim() === "") {
574
+ return false;
575
+ }
576
+ const scheme = schemeOf(url);
577
+ return scheme === null || scheme === "http" || scheme === "https";
578
+ }
579
+
580
+ // src/core/detail.ts
581
+ var DETAIL_MODES = ["client", "server"];
582
+ var DEFAULT_DETAIL_PAGE_SIZE = 25;
583
+ var MAX_DETAIL_PAGE_SIZE = 200;
584
+ function resolveDetailOptions(definition) {
585
+ const detail = definition.detail;
586
+ if (detail === void 0 || detail === false) return void 0;
587
+ const options = detail === true ? {} : detail;
588
+ return {
589
+ title: options.title ?? definition.title,
590
+ pageSize: options.pageSize ?? DEFAULT_DETAIL_PAGE_SIZE,
275
591
  mode: options.mode ?? "client"
276
592
  };
277
593
  }
@@ -291,15 +607,15 @@ function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
291
607
  function fold(text) {
292
608
  return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
293
609
  }
294
- function cellValue(cell) {
610
+ function cellValue2(cell) {
295
611
  return cell.value ?? cell.text;
296
612
  }
297
613
  function cellMatches(cell, needle) {
298
- return fold(String(cellValue(cell))).includes(needle);
614
+ return fold(String(cellValue2(cell))).includes(needle);
299
615
  }
300
616
  function compareCells(a, b, collator) {
301
- const left = cellValue(a);
302
- const right = cellValue(b);
617
+ const left = cellValue2(a);
618
+ const right = cellValue2(b);
303
619
  if (typeof left === "number" && typeof right === "number") {
304
620
  return left - right;
305
621
  }
@@ -591,103 +907,6 @@ function deriveDetailData(data, locale) {
591
907
  return void 0;
592
908
  }
593
909
 
594
- // src/core/grid.ts
595
- var WIDTH_COLUMNS = 12;
596
- var MIN_WIDGET_WIDTH = 2;
597
- var MAX_WIDGET_WIDTH = 12;
598
- function isWidgetWidth(value) {
599
- return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
600
- }
601
- function widthForSize(size) {
602
- switch (size) {
603
- case "small":
604
- return 3;
605
- case "large":
606
- return 6;
607
- case "full":
608
- return 12;
609
- default:
610
- return 4;
611
- }
612
- }
613
- function itemWidth(item) {
614
- return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
615
- }
616
- function usesWidthColumns(items) {
617
- return items.some((item) => item.width !== void 0);
618
- }
619
- function fillsWidth(fill) {
620
- return fill === "width" || fill === "both";
621
- }
622
- function fillsHeight(fill) {
623
- return fill === "height" || fill === "both";
624
- }
625
- function baseColumnSpan(size, columns, largeSpan = 2) {
626
- if (size === "full") return columns;
627
- if (size === "large") return Math.min(largeSpan, columns);
628
- return 1;
629
- }
630
- function fillColumnSpans(items, columns, largeSpan = 2) {
631
- return placeAndFill(
632
- items,
633
- columns,
634
- (item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
635
- );
636
- }
637
- function fillWidthSpans(items) {
638
- return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
639
- }
640
- function placeRows(items) {
641
- return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
642
- }
643
- function fitCount(items, maxRows) {
644
- const rows = placeRows(items);
645
- let count = 0;
646
- for (const row of rows.slice(0, Math.max(0, Math.floor(maxRows)))) {
647
- count += row.keys.length;
648
- }
649
- return count;
650
- }
651
- function placeAndFill(items, columns, spanOf) {
652
- return placeAndFillRows(items, columns, spanOf).spans;
653
- }
654
- function placeAndFillRows(items, columns, spanOf) {
655
- const trackCount = Math.max(1, Math.floor(columns));
656
- const spans = /* @__PURE__ */ new Map();
657
- const rows = [];
658
- let row = [];
659
- let used = 0;
660
- function closeRow() {
661
- if (row.length === 0) return;
662
- const fillers = row.filter(({ item }) => fillsWidth(item.fill));
663
- const leftover = trackCount - used;
664
- const finalSpans = /* @__PURE__ */ new Map();
665
- if (fillers.length > 0) {
666
- const share = Math.floor(leftover / fillers.length);
667
- const remainder = leftover % fillers.length;
668
- fillers.forEach(({ item, span }, index) => {
669
- const grown = span + share + (index < remainder ? 1 : 0);
670
- spans.set(item.key, grown);
671
- finalSpans.set(item.key, grown);
672
- });
673
- }
674
- rows.push({
675
- keys: row.map(({ item }) => item.key),
676
- spans: row.map(({ item, span }) => finalSpans.get(item.key) ?? span)
677
- });
678
- row = [];
679
- used = 0;
680
- }
681
- for (const item of items) {
682
- const span = Math.min(spanOf(item, trackCount), trackCount);
683
- if (used + span > trackCount && row.length > 0) closeRow();
684
- row.push({ item, span });
685
- used += span;
686
- }
687
- closeRow();
688
- return { spans, rows };
689
- }
690
-
691
910
  // src/core/options.ts
692
911
  var DATE_RANGE_PRESETS = [
693
912
  "today",
@@ -993,6 +1212,7 @@ function parseOne(raw) {
993
1212
  const width = raw["width"];
994
1213
  const flexible = raw["flexible"];
995
1214
  const options = raw["options"];
1215
+ const view = raw["view"];
996
1216
  const cleanTitle = typeof title === "string" ? title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH) : "";
997
1217
  const chosen = {};
998
1218
  if (isRecord(options)) {
@@ -1005,7 +1225,8 @@ function parseOne(raw) {
1005
1225
  ...cleanTitle === "" ? {} : { title: cleanTitle },
1006
1226
  ...isWidgetWidth(width) ? { width } : {},
1007
1227
  ...typeof flexible === "boolean" ? { flexible } : {},
1008
- ...Object.keys(chosen).length === 0 ? {} : { options: chosen }
1228
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
1229
+ ...typeof view === "string" && view !== "" && view.length <= 80 ? { view } : {}
1009
1230
  };
1010
1231
  return Object.keys(settings).length === 0 ? void 0 : settings;
1011
1232
  }
@@ -1032,7 +1253,8 @@ function serializeSettings(settings) {
1032
1253
  ...one.title === void 0 ? {} : { title: one.title },
1033
1254
  ...one.width === void 0 ? {} : { width: one.width },
1034
1255
  ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1035
- ...one.options === void 0 || Object.keys(one.options).length === 0 ? {} : { options: { ...one.options } }
1256
+ ...one.options === void 0 || Object.keys(one.options).length === 0 ? {} : { options: { ...one.options } },
1257
+ ...one.view === void 0 ? {} : { view: one.view }
1036
1258
  };
1037
1259
  if (Object.keys(piece).length > 0) out[key] = piece;
1038
1260
  }
@@ -1124,12 +1346,18 @@ function optionValuesFromLayout(definitions, layout, options = {}) {
1124
1346
  }
1125
1347
  function sameSettings(a, b) {
1126
1348
  if (a === b) return true;
1127
- return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && sameOptionValues(a?.options ?? {}, b?.options ?? {});
1349
+ return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && a?.view === b?.view && sameOptionValues(a?.options ?? {}, b?.options ?? {});
1128
1350
  }
1129
1351
  function setWidgetSettings(layout, key, patch) {
1130
1352
  const before = layout.settings?.[key];
1131
1353
  const draft = { ...before };
1132
- for (const field of ["title", "width", "flexible", "options"]) {
1354
+ for (const field of [
1355
+ "title",
1356
+ "width",
1357
+ "flexible",
1358
+ "options",
1359
+ "view"
1360
+ ]) {
1133
1361
  if (!(field in patch)) continue;
1134
1362
  const value = patch[field];
1135
1363
  if (value === void 0) {
@@ -1155,7 +1383,8 @@ function clearWidgetSettings(layout, key) {
1155
1383
  title: void 0,
1156
1384
  width: void 0,
1157
1385
  flexible: void 0,
1158
- options: void 0
1386
+ options: void 0,
1387
+ view: void 0
1159
1388
  });
1160
1389
  }
1161
1390
  function normalizeTitle(title) {
@@ -1215,7 +1444,8 @@ function normalizeSettings(definitions, layout, options = {}) {
1215
1444
  ...title === void 0 ? {} : { title },
1216
1445
  ...one.width === void 0 ? {} : { width: Math.min(range.max, Math.max(range.min, one.width)) },
1217
1446
  ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1218
- ...Object.keys(chosen).length === 0 ? {} : { options: chosen }
1447
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
1448
+ ...one.view !== void 0 && (one.view === BASE_VIEW_KEY || findView(definition, one.view) !== void 0) ? { view: one.view } : {}
1219
1449
  };
1220
1450
  if (Object.keys(next).length > 0) {
1221
1451
  cleaned[definition.key] = sameSettings(one, next) ? one : next;
@@ -1228,57 +1458,184 @@ function normalizeSettings(definitions, layout, options = {}) {
1228
1458
  return keys.length === 0 ? others : { ...others, settings: cleaned };
1229
1459
  }
1230
1460
 
1231
- // src/core/layout.ts
1232
- var MAX_PAGE_TITLE_LENGTH = 30;
1233
- var MAX_PARSED_PAGES = 50;
1234
- var MAX_PAGE_ROWS = 20;
1235
- var EMPTY_LAYOUT = Object.freeze({
1236
- order: Object.freeze([]),
1237
- hidden: Object.freeze([]),
1238
- minimized: Object.freeze([])
1239
- });
1240
- function uniqueStrings(value) {
1241
- if (!Array.isArray(value)) return [];
1461
+ // src/core/clones.ts
1462
+ var MAX_PARSED_CLONES = 500;
1463
+ function isRecord2(value) {
1464
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1465
+ }
1466
+ function parseClones(raw) {
1467
+ if (!Array.isArray(raw)) return void 0;
1242
1468
  const seen = /* @__PURE__ */ new Set();
1243
- for (const entry of value) {
1244
- if (typeof entry === "string" && entry !== "") seen.add(entry);
1469
+ const clones = [];
1470
+ for (const entry of raw.slice(0, MAX_PARSED_CLONES)) {
1471
+ if (!isRecord2(entry)) continue;
1472
+ const key = entry["key"];
1473
+ const from = entry["from"];
1474
+ if (typeof key !== "string" || typeof from !== "string" || key === "" || from === "" || key === from || seen.has(key)) {
1475
+ continue;
1476
+ }
1477
+ seen.add(key);
1478
+ clones.push({ key, from });
1245
1479
  }
1246
- return [...seen];
1480
+ return clones.length === 0 ? void 0 : clones;
1247
1481
  }
1248
- function parseLayout(raw) {
1249
- let value = raw;
1250
- if (typeof raw === "string") {
1251
- try {
1252
- value = JSON.parse(raw);
1253
- } catch {
1254
- return EMPTY_LAYOUT;
1482
+ function serializeClones(clones) {
1483
+ if (clones === void 0 || clones.length === 0) return void 0;
1484
+ return clones.map(({ key, from }) => ({ key, from }));
1485
+ }
1486
+ function cloneNumber(key) {
1487
+ const match = /#(\d+)$/.exec(key);
1488
+ return match === null ? void 0 : Number(match[1]);
1489
+ }
1490
+ function cleanClones(definitions, clones, options = {}) {
1491
+ if (clones === void 0) return void 0;
1492
+ const byKey = new Map(definitions.map((d) => [d.key, d]));
1493
+ const seen = /* @__PURE__ */ new Set();
1494
+ const kept = [];
1495
+ for (const clone of clones) {
1496
+ if (seen.has(clone.key)) continue;
1497
+ const original = byKey.get(clone.from);
1498
+ if (original === void 0 || original.sourceKey !== void 0) continue;
1499
+ const existing = byKey.get(clone.key);
1500
+ if (existing !== void 0 && existing.sourceKey !== clone.from) continue;
1501
+ if (options.overrideLocks !== true && resolveWidgetLock(original).clone) {
1502
+ continue;
1255
1503
  }
1504
+ seen.add(clone.key);
1505
+ kept.push(clone);
1506
+ }
1507
+ if (kept.length === 0) return void 0;
1508
+ return kept.length === clones.length && kept.every((c, i) => c === clones[i]) ? clones : kept;
1509
+ }
1510
+ function defaultCloneTitle(original, clone, index) {
1511
+ const base = original.title ?? original.key;
1512
+ const suffix = ` (#${cloneNumber(clone.key) ?? index + 1})`;
1513
+ return `${base.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
1514
+ }
1515
+ function withClones(definitions, layout, options = {}) {
1516
+ const valid = cleanClones(
1517
+ definitions,
1518
+ layout.clones,
1519
+ options
1520
+ );
1521
+ if (valid === void 0) return definitions;
1522
+ const present = new Set(definitions.map((d) => d.key));
1523
+ const toAdd = valid.filter((c) => !present.has(c.key));
1524
+ if (toAdd.length === 0) return definitions;
1525
+ const out = [];
1526
+ for (const definition of definitions) {
1527
+ out.push(definition);
1528
+ toAdd.forEach((clone, index) => {
1529
+ if (clone.from !== definition.key) return;
1530
+ const copy = { ...definition };
1531
+ copy.key = clone.key;
1532
+ copy.sourceKey = definition.key;
1533
+ copy.title = defaultCloneTitle(definition, clone, index);
1534
+ delete copy.locked;
1535
+ out.push(copy);
1536
+ });
1256
1537
  }
1538
+ return out;
1539
+ }
1540
+
1541
+ // src/core/compat.ts
1542
+ function isStringArray(value) {
1543
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
1544
+ }
1545
+ function upgradeLegacyWidgetData(value) {
1257
1546
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
1258
- return EMPTY_LAYOUT;
1547
+ return value;
1259
1548
  }
1260
1549
  const record = value;
1261
- const order = uniqueStrings(record["order"]);
1262
- const hidden = uniqueStrings(record["hidden"]);
1263
- const minimized = uniqueStrings(record["minimized"]);
1264
- const pages = parsePages(record["pages"]);
1265
- const settings = parseSettings(
1266
- record["settings"]
1267
- );
1268
- const withSettings = settings === void 0 ? {} : { settings };
1269
- if (pages.length > 0) {
1550
+ if (record["kind"] === "TABLE" && isStringArray(record["columns"]) && Array.isArray(record["rows"]) && record["rows"].every(isStringArray)) {
1551
+ const rows = record["rows"];
1552
+ return {
1553
+ ...record,
1554
+ columns: record["columns"].map((label) => ({ label })),
1555
+ rows: rows.map((row) => row.map((text) => ({ text })))
1556
+ };
1557
+ }
1558
+ if (record["kind"] === "GRAPH" && Array.isArray(record["points"]) && record["series"] === void 0) {
1559
+ const { points, ...rest } = record;
1560
+ return {
1561
+ ...rest,
1562
+ series: [{ name: "Value", points }],
1563
+ chartType: "bar",
1564
+ valueFormat: "number"
1565
+ };
1566
+ }
1567
+ return value;
1568
+ }
1569
+ function fromLegacyDefinition(legacy) {
1570
+ if (!isWidgetKind(legacy.viewType)) {
1571
+ return null;
1572
+ }
1573
+ return {
1574
+ key: legacy.widgetKey,
1575
+ title: legacy.title,
1576
+ ...legacy.description === null ? {} : { description: legacy.description },
1577
+ kind: legacy.viewType,
1578
+ sortOrder: legacy.sortOrder
1579
+ };
1580
+ }
1581
+
1582
+ // src/core/layout.ts
1583
+ var MAX_PAGE_TITLE_LENGTH = 30;
1584
+ var MAX_PARSED_PAGES = 50;
1585
+ var MAX_PAGE_ROWS = 20;
1586
+ var EMPTY_LAYOUT = Object.freeze({
1587
+ order: Object.freeze([]),
1588
+ hidden: Object.freeze([]),
1589
+ minimized: Object.freeze([])
1590
+ });
1591
+ function uniqueStrings(value) {
1592
+ if (!Array.isArray(value)) return [];
1593
+ const seen = /* @__PURE__ */ new Set();
1594
+ for (const entry of value) {
1595
+ if (typeof entry === "string" && entry !== "") seen.add(entry);
1596
+ }
1597
+ return [...seen];
1598
+ }
1599
+ function parseLayout(raw) {
1600
+ let value = raw;
1601
+ if (typeof raw === "string") {
1602
+ try {
1603
+ value = JSON.parse(raw);
1604
+ } catch {
1605
+ return EMPTY_LAYOUT;
1606
+ }
1607
+ }
1608
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1609
+ return EMPTY_LAYOUT;
1610
+ }
1611
+ const record = value;
1612
+ const order = uniqueStrings(record["order"]);
1613
+ const hidden = uniqueStrings(record["hidden"]);
1614
+ const minimized = uniqueStrings(record["minimized"]);
1615
+ const pages = parsePages(record["pages"]);
1616
+ const settings = parseSettings(
1617
+ record["settings"]
1618
+ );
1619
+ const clones = parseClones(
1620
+ record["clones"]
1621
+ );
1622
+ const extras = {
1623
+ ...settings === void 0 ? {} : { settings },
1624
+ ...clones === void 0 ? {} : { clones }
1625
+ };
1626
+ if (pages.length > 0) {
1270
1627
  return {
1271
1628
  order: EMPTY_LAYOUT.order,
1272
1629
  hidden: EMPTY_LAYOUT.hidden,
1273
1630
  minimized: EMPTY_LAYOUT.minimized,
1274
1631
  pages,
1275
- ...withSettings
1632
+ ...extras
1276
1633
  };
1277
1634
  }
1278
1635
  if (order.length === 0 && hidden.length === 0 && minimized.length === 0) {
1279
- return settings === void 0 ? EMPTY_LAYOUT : { ...EMPTY_LAYOUT, settings };
1636
+ return Object.keys(extras).length === 0 ? EMPTY_LAYOUT : { ...EMPTY_LAYOUT, ...extras };
1280
1637
  }
1281
- return { order, hidden, minimized, ...withSettings };
1638
+ return { order, hidden, minimized, ...extras };
1282
1639
  }
1283
1640
  function parsePages(value) {
1284
1641
  if (!Array.isArray(value)) return [];
@@ -1326,7 +1683,8 @@ function serializeLayout(layout) {
1326
1683
  ...page.maxRows === void 0 ? {} : { maxRows: page.maxRows }
1327
1684
  }))
1328
1685
  },
1329
- ...serializeSettings(layout.settings) === void 0 ? {} : { settings: serializeSettings(layout.settings) }
1686
+ ...serializeSettings(layout.settings) === void 0 ? {} : { settings: serializeSettings(layout.settings) },
1687
+ ...serializeClones(layout.clones) === void 0 ? {} : { clones: serializeClones(layout.clones) }
1330
1688
  });
1331
1689
  }
1332
1690
  function bySortOrder(items) {
@@ -1491,1482 +1849,1746 @@ function enforceLocks(definitions, layout, options = {}) {
1491
1849
  }));
1492
1850
  }
1493
1851
 
1494
- // src/core/validate.ts
1495
- var MAX_REPORTED_PROBLEMS = 10;
1496
- function describeType2(value) {
1497
- if (value === null) return "null";
1498
- if (Array.isArray(value)) return "array";
1499
- if (typeof value === "number" && !Number.isFinite(value)) {
1500
- return String(value);
1501
- }
1502
- return typeof value;
1503
- }
1504
- function isRecord2(value) {
1505
- return typeof value === "object" && value !== null && !Array.isArray(value);
1852
+ // src/core/pages.ts
1853
+ var DEFAULT_PAGE_KEY = "default";
1854
+ var DEFAULT_PAGE_ROWS = 4;
1855
+ function rowsAllowed(page, options) {
1856
+ const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
1857
+ return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
1506
1858
  }
1507
- function join(path, field) {
1508
- return path === "" ? field : `${path}.${field}`;
1859
+ function hasPages(layout) {
1860
+ return layout.pages !== void 0 && layout.pages.length > 0;
1509
1861
  }
1510
- function requireString(record, field, path, problems) {
1511
- const value = record[field];
1512
- if (typeof value !== "string") {
1513
- problems.push(
1514
- `${join(path, field)} must be a string, got ${describeType2(value)}.`
1515
- );
1862
+ function pageList(layout) {
1863
+ if (layout.pages !== void 0 && layout.pages.length > 0) {
1864
+ return layout.pages;
1516
1865
  }
1866
+ return [
1867
+ {
1868
+ key: DEFAULT_PAGE_KEY,
1869
+ title: "Page 1",
1870
+ order: layout.order,
1871
+ hidden: layout.hidden,
1872
+ minimized: layout.minimized
1873
+ }
1874
+ ];
1517
1875
  }
1518
- function optionalString(record, field, path, problems) {
1519
- if (record[field] !== void 0) {
1520
- requireString(record, field, path, problems);
1521
- }
1876
+ function withExplicitPages(layout) {
1877
+ if (hasPages(layout)) return layout;
1878
+ return {
1879
+ ...layout,
1880
+ order: [],
1881
+ hidden: [],
1882
+ minimized: [],
1883
+ pages: pageList(layout)
1884
+ };
1522
1885
  }
1523
- function requireNumber(record, field, path, problems) {
1524
- const value = record[field];
1525
- if (typeof value !== "number" || !Number.isFinite(value)) {
1526
- problems.push(
1527
- `${join(path, field)} must be a finite number, got ${describeType2(value)}.`
1528
- );
1529
- }
1886
+ function sameKeys(a, b) {
1887
+ return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
1530
1888
  }
1531
- function optionalNumber(record, field, path, problems) {
1532
- if (record[field] !== void 0) {
1533
- requireNumber(record, field, path, problems);
1534
- }
1889
+ function pageLayout(layout, pageKey) {
1890
+ if (!hasPages(layout))
1891
+ return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
1892
+ const page = layout.pages?.find(
1893
+ (p) => p.key === pageKey
1894
+ );
1895
+ if (page === void 0) return void 0;
1896
+ const { pages, ...rest } = layout;
1897
+ return {
1898
+ ...rest,
1899
+ order: page.order,
1900
+ hidden: page.hidden,
1901
+ minimized: page.minimized
1902
+ };
1535
1903
  }
1536
- function optionalSortValue(record, path, problems) {
1537
- const value = record["value"];
1538
- if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
1539
- problems.push(
1540
- `${path}.value must be a string or a finite number, got ${describeType2(value)}.`
1541
- );
1904
+ function withPageLayout(layout, pageKey, next) {
1905
+ const current = pageLayout(layout, pageKey);
1906
+ if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
1907
+ return layout;
1542
1908
  }
1543
- }
1544
- function optionalBoolean(record, field, path, problems) {
1545
- const value = record[field];
1546
- if (value !== void 0 && typeof value !== "boolean") {
1547
- problems.push(
1548
- `${join(path, field)} must be a boolean, got ${describeType2(value)}.`
1549
- );
1909
+ if (!hasPages(layout)) {
1910
+ return {
1911
+ ...layout,
1912
+ order: next.order,
1913
+ hidden: next.hidden,
1914
+ minimized: next.minimized
1915
+ };
1550
1916
  }
1917
+ return {
1918
+ ...layout,
1919
+ pages: (layout.pages ?? []).map(
1920
+ (page) => page.key === pageKey ? {
1921
+ ...page,
1922
+ order: next.order,
1923
+ hidden: next.hidden,
1924
+ minimized: next.minimized
1925
+ } : page
1926
+ )
1927
+ };
1551
1928
  }
1552
- function optionalUrl(record, field, path, problems, isSafe, allowed) {
1553
- const value = record[field];
1554
- if (value === void 0) return;
1555
- if (typeof value !== "string") {
1556
- problems.push(
1557
- `${join(path, field)} must be a string, got ${describeType2(value)}.`
1558
- );
1559
- } else if (!isSafe(value)) {
1560
- problems.push(
1561
- `${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
1562
- );
1563
- }
1929
+ function normalizedTitle(title) {
1930
+ return title.trim().normalize("NFKC").toLowerCase();
1564
1931
  }
1565
- function requireArray(record, field, path, problems) {
1566
- const value = record[field];
1567
- if (!Array.isArray(value)) {
1568
- problems.push(
1569
- `${join(path, field)} must be an array, got ${describeType2(value)}.`
1570
- );
1571
- return null;
1932
+ function resolvePageKey(layout, reference) {
1933
+ if (reference === void 0 || reference === "") return void 0;
1934
+ const pages = pageList(layout);
1935
+ const byKey = pages.find((p) => p.key === reference);
1936
+ if (byKey !== void 0) return byKey.key;
1937
+ const wanted = normalizedTitle(reference);
1938
+ return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
1939
+ }
1940
+ function homePageKey(layout, item) {
1941
+ return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
1942
+ }
1943
+ function assignPages(definitions, layout) {
1944
+ const pages = pageList(layout);
1945
+ const assigned = new Map(pages.map((p) => [p.key, []]));
1946
+ for (const definition of definitions) {
1947
+ let pageKey;
1948
+ if (!resolveWidgetLock(definition).move) {
1949
+ pageKey = pages.find(
1950
+ (p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
1951
+ )?.key;
1952
+ }
1953
+ assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
1572
1954
  }
1573
- return value;
1955
+ return assigned;
1574
1956
  }
1575
- function requireOneOf(record, field, options, path, problems) {
1576
- const value = record[field];
1577
- if (typeof value !== "string" || !options.includes(value)) {
1578
- problems.push(
1579
- `${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType2(value)}.`
1580
- );
1957
+ function pageOf(definitions, layout, key) {
1958
+ for (const [pageKey, widgets] of assignPages(definitions, layout)) {
1959
+ if (widgets.some((w) => w.key === key)) return pageKey;
1581
1960
  }
1961
+ return void 0;
1582
1962
  }
1583
- function eachRecord(items, path, problems, check) {
1584
- items.forEach((item, index) => {
1585
- const itemPath = `${path}[${index}]`;
1586
- if (isRecord2(item)) {
1587
- check(item, itemPath);
1588
- } else {
1589
- problems.push(
1590
- `${itemPath} must be an object, got ${describeType2(item)}.`
1591
- );
1592
- }
1593
- });
1963
+ function pageWidgets(definitions, layout, pageKey) {
1964
+ const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
1965
+ const view = pageLayout(layout, pageKey);
1966
+ return view === void 0 ? [] : visibleWidgets(onPage, view);
1594
1967
  }
1595
- var HREF_RULE = "an http(s), mailto or relative URL";
1596
- var IMAGE_RULE = "an http(s) or relative URL";
1597
- function checkPayloadFields(kind, data, problems) {
1598
- switch (kind) {
1599
- case "TEXT":
1600
- requireString(data, "value", "", problems);
1601
- requireString(data, "label", "", problems);
1602
- return;
1603
- case "KPI":
1604
- requireNumber(data, "value", "", problems);
1605
- if (data["previous"] !== null) {
1606
- optionalNumber(data, "previous", "", problems);
1607
- }
1608
- requireOneOf(data, "format", VALUE_FORMATS, "", problems);
1609
- optionalString(data, "currency", "", problems);
1610
- requireString(data, "label", "", problems);
1611
- optionalString(data, "hint", "", problems);
1612
- optionalBoolean(data, "higherIsBetter", "", problems);
1613
- return;
1614
- case "GAUGE":
1615
- requireNumber(data, "value", "", problems);
1616
- requireNumber(data, "max", "", problems);
1617
- requireString(data, "label", "", problems);
1618
- if (typeof data["max"] === "number" && data["max"] <= 0) {
1619
- problems.push(`max must be greater than 0, got ${data["max"]}.`);
1620
- }
1621
- return;
1622
- case "TABLE": {
1623
- const columns = requireArray(data, "columns", "", problems);
1624
- if (columns !== null) {
1625
- eachRecord(columns, "columns", problems, (column, path) => {
1626
- requireString(column, "label", path, problems);
1627
- optionalBoolean(column, "numeric", path, problems);
1628
- });
1629
- }
1630
- const rows = requireArray(data, "rows", "", problems);
1631
- if (rows !== null) {
1632
- rows.forEach((row, rowIndex) => {
1633
- const rowPath = `rows[${rowIndex}]`;
1634
- if (!Array.isArray(row)) {
1635
- problems.push(
1636
- `${rowPath} must be an array of cells, got ${describeType2(row)}.`
1637
- );
1638
- return;
1639
- }
1640
- if (columns !== null && row.length !== columns.length) {
1641
- problems.push(
1642
- `${rowPath} has ${row.length} cell(s) but there are ${columns.length} column(s).`
1643
- );
1644
- }
1645
- eachRecord(row, rowPath, problems, (cell, cellPath) => {
1646
- requireString(cell, "text", cellPath, problems);
1647
- optionalSortValue(cell, cellPath, problems);
1648
- optionalUrl(
1649
- cell,
1650
- "href",
1651
- cellPath,
1652
- problems,
1653
- isSafeHref,
1654
- HREF_RULE
1655
- );
1656
- });
1657
- });
1658
- }
1659
- optionalString(data, "footer", "", problems);
1660
- return;
1661
- }
1662
- case "BAR_LIST": {
1663
- const items = requireArray(data, "items", "", problems);
1664
- if (items !== null) {
1665
- eachRecord(items, "items", problems, (item, path) => {
1666
- requireString(item, "label", path, problems);
1667
- requireNumber(item, "value", path, problems);
1668
- optionalString(item, "display", path, problems);
1669
- });
1670
- }
1671
- optionalNumber(data, "total", "", problems);
1672
- return;
1673
- }
1674
- case "ALERT_LIST": {
1675
- const items = requireArray(data, "items", "", problems);
1676
- if (items !== null) {
1677
- eachRecord(items, "items", problems, (item, path) => {
1678
- requireString(item, "title", path, problems);
1679
- optionalUrl(item, "href", path, problems, isSafeHref, HREF_RULE);
1680
- optionalUrl(
1681
- item,
1682
- "thumbnailUrl",
1683
- path,
1684
- problems,
1685
- isSafeImageUrl,
1686
- IMAGE_RULE
1687
- );
1688
- requireString(item, "valueLabel", path, problems);
1689
- optionalString(item, "detail", path, problems);
1690
- });
1691
- }
1692
- requireNumber(data, "total", "", problems);
1693
- requireString(data, "emptyText", "", problems);
1694
- if (items !== null && typeof data["total"] === "number" && data["total"] < items.length) {
1695
- problems.push(
1696
- `total (${data["total"]}) must be at least the number of items (${items.length}).`
1697
- );
1698
- }
1699
- return;
1700
- }
1701
- case "GRAPH": {
1702
- const series = requireArray(data, "series", "", problems);
1703
- if (series !== null) {
1704
- if (series.length > MAX_GRAPH_SERIES) {
1705
- problems.push(
1706
- `series has ${series.length} entries; at most ${MAX_GRAPH_SERIES} are allowed (fold the rest into "Other").`
1707
- );
1708
- }
1709
- eachRecord(series, "series", problems, (entry, path) => {
1710
- requireString(entry, "name", path, problems);
1711
- const points = requireArray(entry, "points", path, problems);
1712
- if (points !== null) {
1713
- eachRecord(
1714
- points,
1715
- `${path}.points`,
1716
- problems,
1717
- (point, pointPath) => {
1718
- requireString(point, "label", pointPath, problems);
1719
- requireNumber(point, "value", pointPath, problems);
1720
- }
1721
- );
1722
- }
1723
- });
1724
- }
1725
- requireOneOf(data, "chartType", ["bar", "line"], "", problems);
1726
- requireOneOf(data, "valueFormat", VALUE_FORMATS, "", problems);
1727
- optionalString(data, "currency", "", problems);
1728
- optionalString(data, "xLabel", "", problems);
1729
- return;
1730
- }
1731
- }
1968
+ function pageRoom(definitions, layout, pageKey, options = {}) {
1969
+ const page = pageList(layout).find(
1970
+ (p) => p.key === pageKey
1971
+ );
1972
+ if (page === void 0) return void 0;
1973
+ const maxRows = rowsAllowed(page, options);
1974
+ const rowsUsed = placeRows(
1975
+ pageWidgets(definitions, layout, pageKey)
1976
+ ).length;
1977
+ return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
1732
1978
  }
1733
- function checkDetailOption(detail, problems) {
1734
- if (detail === void 0 || typeof detail === "boolean") return;
1735
- if (!isRecord2(detail)) {
1736
- problems.push(
1737
- `detail must be true, false or an options object, got ${describeType2(detail)}.`
1738
- );
1739
- return;
1740
- }
1741
- optionalString(detail, "title", "detail", problems);
1742
- const pageSize = detail["pageSize"];
1743
- if (pageSize !== void 0 && !(typeof pageSize === "number" && Number.isInteger(pageSize) && pageSize >= 1 && pageSize <= MAX_DETAIL_PAGE_SIZE)) {
1744
- problems.push(
1745
- `detail.pageSize must be a whole number from 1 to ${MAX_DETAIL_PAGE_SIZE}, got ${typeof pageSize === "number" ? String(pageSize) : describeType2(pageSize)}.`
1746
- );
1747
- }
1748
- if (detail["mode"] !== void 0) {
1749
- requireOneOf(detail, "mode", DETAIL_MODES, "detail", problems);
1750
- }
1979
+ function describePage(page) {
1980
+ return `Page "${page.title}"`;
1751
1981
  }
1752
- function checkTableControlsOption(controls, kind, problems) {
1753
- if (controls === void 0 || controls === false) return;
1754
- if (typeof controls !== "boolean" && !isRecord2(controls)) {
1755
- problems.push(
1756
- `tableControls must be true, false or an options object, got ${describeType2(controls)}.`
1757
- );
1758
- return;
1759
- }
1760
- if (isRecord2(controls)) {
1761
- optionalBoolean(controls, "search", "tableControls", problems);
1762
- optionalBoolean(controls, "sort", "tableControls", problems);
1763
- }
1764
- if (typeof kind === "string" && kind !== "TABLE") {
1765
- problems.push(
1766
- `tableControls only applies to TABLE widgets, but kind is "${kind}".`
1767
- );
1768
- }
1982
+ function titleOf(item) {
1983
+ return item.title ?? item.key;
1769
1984
  }
1770
- function checkWidthLimits(value, problems) {
1771
- for (const field of ["minWidth", "maxWidth"]) {
1772
- const limit = value[field];
1773
- if (limit !== void 0 && !isWidgetWidth(limit)) {
1774
- problems.push(
1775
- `${field} must be an integer from ${MIN_WIDGET_WIDTH} to ${MAX_WIDGET_WIDTH} (twelfths of the row), got ${typeof limit === "number" ? String(limit) : describeType2(limit)}.`
1776
- );
1777
- return;
1778
- }
1779
- }
1780
- const { minWidth, maxWidth, width } = value;
1781
- if (isWidgetWidth(minWidth) && isWidgetWidth(maxWidth) && minWidth > maxWidth) {
1782
- problems.push(
1783
- `minWidth (${minWidth}) must not be more than maxWidth (${maxWidth}).`
1985
+ function checkTitle(layout, title, exceptKey) {
1986
+ const trimmed = title.trim();
1987
+ if (trimmed === "") return err("A page title must not be empty.");
1988
+ if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
1989
+ return err(
1990
+ `Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
1784
1991
  );
1785
- return;
1786
1992
  }
1787
- if (isWidgetWidth(width)) {
1788
- if (isWidgetWidth(minWidth) && width < minWidth) {
1789
- problems.push(
1790
- `width (${width}) must not be less than minWidth (${minWidth}).`
1791
- );
1792
- }
1793
- if (isWidgetWidth(maxWidth) && width > maxWidth) {
1794
- problems.push(
1795
- `width (${width}) must not be more than maxWidth (${maxWidth}).`
1796
- );
1797
- }
1993
+ const wanted = normalizedTitle(trimmed);
1994
+ const clash = pageList(layout).find(
1995
+ (p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
1996
+ );
1997
+ if (clash !== void 0) {
1998
+ return err(`Another page is already called "${clash.title}".`);
1798
1999
  }
2000
+ return ok(trimmed);
1799
2001
  }
1800
- function checkLockedOption(locked, problems) {
1801
- if (locked === void 0 || typeof locked === "boolean") return;
1802
- if (!isRecord2(locked)) {
1803
- problems.push(
1804
- `locked must be true, false or an object, got ${describeType2(locked)}.`
1805
- );
1806
- return;
1807
- }
1808
- optionalBoolean(locked, "move", "locked", problems);
1809
- optionalBoolean(locked, "hide", "locked", problems);
1810
- optionalBoolean(locked, "minimize", "locked", problems);
1811
- optionalBoolean(locked, "options", "locked", problems);
2002
+ function nextPageKey(layout) {
2003
+ const keys = new Set(pageList(layout).map((p) => p.key));
2004
+ let n = 1;
2005
+ while (keys.has(`page-${n}`)) n += 1;
2006
+ return `page-${n}`;
1812
2007
  }
1813
- function checkOptionColumns(option, path, kind, problems) {
1814
- const columns = option["columns"];
1815
- if (!Array.isArray(columns) || columns.length === 0) {
1816
- problems.push(
1817
- `${join(path, "columns")} must be a non-empty array of { key, label }, got ${describeType2(columns)}.`
2008
+ function nextPageTitle(layout) {
2009
+ const used = new Set(
2010
+ pageList(layout).map((p) => normalizedTitle(p.title))
2011
+ );
2012
+ let n = pageList(layout).length + 1;
2013
+ while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
2014
+ return `Page ${n}`;
2015
+ }
2016
+ function addPage(layout, options = {}) {
2017
+ if (pageList(layout).length >= MAX_PARSED_PAGES) {
2018
+ return err(
2019
+ `The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
1818
2020
  );
1819
- return [];
1820
2021
  }
1821
- const keys = [];
1822
- columns.forEach((entry, index) => {
1823
- const entryPath = `${path}.columns[${index}]`;
1824
- if (!isRecord2(entry)) {
1825
- problems.push(
1826
- `${entryPath} must be an object, got ${describeType2(entry)}.`
1827
- );
1828
- return;
1829
- }
1830
- requireString(entry, "label", entryPath, problems);
1831
- const key = entry["key"];
1832
- if (typeof key !== "string") {
1833
- problems.push(
1834
- `${entryPath}.key must be a string, got ${describeType2(key)}.`
1835
- );
1836
- } else if (typeof kind === "string" && !isColumnKeyFor(kind, key)) {
1837
- problems.push(
1838
- `${entryPath}.key "${key}" is not a column of a ${kind} widget (${kind === "TABLE" ? '"c0", "c1", ...' : '"label" or "value"'}).`
1839
- );
1840
- } else if (keys.includes(key)) {
1841
- problems.push(`${entryPath}.key "${key}" is used twice.`);
1842
- } else {
1843
- keys.push(key);
1844
- }
2022
+ const key = options.key ?? nextPageKey(layout);
2023
+ if (pageList(layout).some((p) => p.key === key)) {
2024
+ return err(`A page with the key "${key}" already exists.`);
2025
+ }
2026
+ const title = checkTitle(
2027
+ layout,
2028
+ options.title ?? nextPageTitle(layout)
2029
+ );
2030
+ if (!title.ok) return title;
2031
+ const base = withExplicitPages(layout);
2032
+ const page = {
2033
+ key,
2034
+ title: title.value,
2035
+ order: [],
2036
+ hidden: [],
2037
+ minimized: []
2038
+ };
2039
+ return ok({ ...base, pages: [...base.pages ?? [], page] });
2040
+ }
2041
+ function renamePage(layout, pageKey, title) {
2042
+ const page = pageList(layout).find(
2043
+ (p) => p.key === pageKey
2044
+ );
2045
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2046
+ const checked = checkTitle(layout, title, pageKey);
2047
+ if (!checked.ok) return checked;
2048
+ if (checked.value === page.title) return ok(layout);
2049
+ const base = withExplicitPages(layout);
2050
+ return ok({
2051
+ ...base,
2052
+ pages: (base.pages ?? []).map(
2053
+ (p) => p.key === pageKey ? { ...p, title: checked.value } : p
2054
+ )
1845
2055
  });
1846
- return keys;
1847
2056
  }
1848
- function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
1849
- if (!isRecord2(option)) {
1850
- problems.push(`${path} must be an object, got ${describeType2(option)}.`);
1851
- return;
2057
+ function movePage(layout, pageKey, offset) {
2058
+ const pages = layout.pages ?? [];
2059
+ const from = pages.findIndex((p) => p.key === pageKey);
2060
+ if (from === -1) return layout;
2061
+ const to = Math.max(
2062
+ 0,
2063
+ Math.min(pages.length - 1, from + Math.trunc(offset))
2064
+ );
2065
+ if (to === from) return layout;
2066
+ const next = [...pages];
2067
+ const [moved] = next.splice(from, 1);
2068
+ next.splice(to, 0, moved);
2069
+ return { ...layout, pages: next };
2070
+ }
2071
+ function overflowing(definitions, layout, options) {
2072
+ return pageList(layout).find(
2073
+ (page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
2074
+ );
2075
+ }
2076
+ function removePage(definitions, layout, pageKey, options = {}) {
2077
+ const pages = pageList(layout);
2078
+ const index = pages.findIndex((p) => p.key === pageKey);
2079
+ const page = pages[index];
2080
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2081
+ if (pages.length === 1) {
2082
+ return err(
2083
+ `${describePage(page)} is the only page, so it can't be removed.`
2084
+ );
1852
2085
  }
1853
- const type = option["type"];
1854
- if (typeof type !== "string" || !WIDGET_OPTION_TYPES.includes(type)) {
1855
- requireOneOf(option, "type", WIDGET_OPTION_TYPES, path, problems);
1856
- return;
2086
+ const target = pages[index > 0 ? index - 1 : 1];
2087
+ const merged = {
2088
+ ...layout,
2089
+ pages: pages.filter((p) => p.key !== pageKey).map(
2090
+ (p) => p.key === target.key ? {
2091
+ ...p,
2092
+ order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
2093
+ hidden: [...p.hidden, ...page.hidden],
2094
+ minimized: [...p.minimized, ...page.minimized]
2095
+ } : p
2096
+ )
2097
+ };
2098
+ const full = overflowing(
2099
+ definitions,
2100
+ merged,
2101
+ options
2102
+ );
2103
+ if (full !== void 0) {
2104
+ return err(
2105
+ `${describePage(page)} has widgets that don't fit on the other pages (${describePage(full)} would need more than ${rowsAllowed(full, options)} rows). Move or hide some first.`
2106
+ );
1857
2107
  }
1858
- const key = option["key"];
1859
- if (typeof key !== "string" || !OPTION_KEY_PATTERN.test(key)) {
1860
- problems.push(
1861
- `${path}.key must be letters, digits, "_" or "-", starting with a letter (40 characters at most), got ${typeof key === "string" ? JSON.stringify(key) : describeType2(key)}.`
2108
+ return ok(merged);
2109
+ }
2110
+ function without(keys, key) {
2111
+ return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
2112
+ }
2113
+ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
2114
+ const widget = definitions.find((d) => d.key === widgetKey);
2115
+ if (widget === void 0) {
2116
+ return err(
2117
+ `Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
1862
2118
  );
1863
- } else if (seenKeys.has(key)) {
1864
- problems.push(`${path}.key "${key}" is used by another option.`);
1865
- } else {
1866
- seenKeys.add(key);
1867
2119
  }
1868
- requireString(option, "label", path, problems);
1869
- const fallback = option["default"];
1870
- switch (type) {
1871
- case "choice": {
1872
- const choices = option["choices"];
1873
- const values = [];
1874
- if (!Array.isArray(choices) || choices.length === 0) {
1875
- problems.push(
1876
- `${path}.choices must be a non-empty array of { value, label }, got ${describeType2(choices)}.`
1877
- );
1878
- } else {
1879
- choices.forEach((choice, index) => {
1880
- const choicePath = `${path}.choices[${index}]`;
1881
- if (!isRecord2(choice)) {
1882
- problems.push(
1883
- `${choicePath} must be an object, got ${describeType2(choice)}.`
1884
- );
1885
- return;
1886
- }
1887
- requireString(choice, "label", choicePath, problems);
1888
- const value = choice["value"];
1889
- if (typeof value !== "string" || value === "") {
1890
- problems.push(
1891
- `${choicePath}.value must be a non-empty string, got ${describeType2(value)}.`
1892
- );
1893
- } else if (values.includes(value)) {
1894
- problems.push(`${choicePath}.value "${value}" is used twice.`);
1895
- } else {
1896
- values.push(value);
1897
- }
1898
- });
1899
- }
1900
- if (values.length > 0)
1901
- requireOneOf(option, "default", values, path, problems);
1902
- break;
1903
- }
1904
- case "number": {
1905
- const { min, max, step } = option;
1906
- requireNumber(option, "min", path, problems);
1907
- requireNumber(option, "max", path, problems);
1908
- if (typeof min === "number" && typeof max === "number" && min > max) {
1909
- problems.push(
1910
- `${path}.min (${min}) must not be more than max (${max}).`
1911
- );
1912
- }
1913
- if (step !== void 0 && !(typeof step === "number" && Number.isFinite(step) && step > 0)) {
1914
- problems.push(
1915
- `${path}.step must be a number above 0, got ${describeType2(step)}.`
1916
- );
2120
+ const pages = pageList(layout);
2121
+ const target = pages.find((p) => p.key === targetKey);
2122
+ if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
2123
+ const sourceKey = pageOf(definitions, layout, widgetKey);
2124
+ if (sourceKey === targetKey) return ok(layout);
2125
+ if (resolveWidgetLock(widget).move) {
2126
+ return err(
2127
+ `Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
2128
+ );
2129
+ }
2130
+ const base = withExplicitPages(layout);
2131
+ const source = (base.pages ?? []).find(
2132
+ (p) => p.key === sourceKey
2133
+ );
2134
+ const hidden = source.hidden.includes(widgetKey);
2135
+ const minimized = source.minimized.includes(widgetKey);
2136
+ const targetOrder = pageWidgets(
2137
+ definitions,
2138
+ base,
2139
+ targetKey
2140
+ ).map((d) => d.key);
2141
+ const moved = {
2142
+ ...base,
2143
+ pages: (base.pages ?? []).map((p) => {
2144
+ if (p.key === sourceKey) {
2145
+ return {
2146
+ ...p,
2147
+ order: without(p.order, widgetKey),
2148
+ hidden: without(p.hidden, widgetKey),
2149
+ minimized: without(p.minimized, widgetKey)
2150
+ };
1917
2151
  }
1918
- if (typeof fallback !== "number" || !Number.isFinite(fallback)) {
1919
- problems.push(
1920
- `${path}.default must be a finite number, got ${describeType2(fallback)}.`
1921
- );
1922
- } else if (typeof min === "number" && typeof max === "number" && (fallback < min || fallback > max)) {
1923
- problems.push(
1924
- `${path}.default (${fallback}) must be between min (${min}) and max (${max}).`
1925
- );
1926
- } else if (typeof min === "number" && typeof step === "number" && step > 0 && Math.abs(
1927
- (fallback - min) / step - Math.round((fallback - min) / step)
1928
- ) > 1e-9) {
1929
- problems.push(
1930
- `${path}.default (${fallback}) must be ${min} plus a whole number of steps of ${step}.`
1931
- );
2152
+ if (p.key === targetKey) {
2153
+ return {
2154
+ ...p,
2155
+ order: hidden ? p.order : [...targetOrder, widgetKey],
2156
+ hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
2157
+ minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
2158
+ };
1932
2159
  }
1933
- break;
1934
- }
1935
- case "boolean":
1936
- if (typeof fallback !== "boolean") {
1937
- problems.push(
1938
- `${path}.default must be a boolean, got ${describeType2(fallback)}.`
1939
- );
1940
- }
1941
- break;
1942
- case "text": {
1943
- const maxLength = option["maxLength"];
1944
- if (typeof maxLength !== "number" || !Number.isInteger(maxLength) || maxLength < 1 || maxLength > MAX_TEXT_OPTION_LENGTH) {
1945
- problems.push(
1946
- `${path}.maxLength must be a whole number from 1 to ${MAX_TEXT_OPTION_LENGTH}, got ${typeof maxLength === "number" ? maxLength : describeType2(maxLength)}.`
1947
- );
1948
- }
1949
- if (typeof fallback !== "string") {
1950
- problems.push(
1951
- `${path}.default must be a string, got ${describeType2(fallback)}.`
1952
- );
1953
- } else if (typeof maxLength === "number" && fallback.length > maxLength) {
1954
- problems.push(
1955
- `${path}.default is ${fallback.length} characters; maxLength is ${maxLength}.`
1956
- );
1957
- }
1958
- break;
1959
- }
1960
- case "dateRange": {
1961
- const presets = option["presets"];
1962
- const offered = [];
1963
- if (presets !== void 0) {
1964
- if (!Array.isArray(presets) || presets.length === 0) {
1965
- problems.push(
1966
- `${path}.presets must be a non-empty array of { value, label }, got ${describeType2(presets)}.`
1967
- );
1968
- } else {
1969
- presets.forEach((preset, index) => {
1970
- const presetPath = `${path}.presets[${index}]`;
1971
- if (!isRecord2(preset)) {
1972
- problems.push(
1973
- `${presetPath} must be an object, got ${describeType2(preset)}.`
1974
- );
1975
- return;
1976
- }
1977
- requireString(preset, "label", presetPath, problems);
1978
- requireOneOf(
1979
- preset,
1980
- "value",
1981
- DATE_RANGE_PRESETS,
1982
- presetPath,
1983
- problems
1984
- );
1985
- if (typeof preset["value"] === "string")
1986
- offered.push(preset["value"]);
1987
- });
1988
- }
1989
- }
1990
- const allowed = offered.length > 0 ? offered : DATE_RANGE_PRESETS;
1991
- if (typeof fallback !== "string" || !(allowed.includes(fallback) || isIsoInterval(fallback))) {
1992
- problems.push(
1993
- `${path}.default must be one of ${allowed.map((p) => `"${p}"`).join(", ")} or an interval like "2026-01-01/2026-01-31", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
1994
- );
1995
- }
1996
- break;
1997
- }
1998
- case "color":
1999
- if (typeof fallback !== "string" || !/^#[0-9a-fA-F]{6}$/.test(fallback)) {
2000
- problems.push(
2001
- `${path}.default must be a color like "#1c5cab", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
2002
- );
2003
- }
2004
- break;
2005
- case "sort":
2006
- case "columns": {
2007
- const supported = type === "sort" ? kind === "TABLE" || kind === "BAR_LIST" : kind === "TABLE";
2008
- if (!supported && typeof kind === "string") {
2009
- problems.push(
2010
- `${path} is a ${type} option, which only applies to ${type === "sort" ? "TABLE and BAR_LIST" : "TABLE"} widgets, but kind is "${kind}".`
2011
- );
2012
- }
2013
- if (seenTypes.has(type)) {
2014
- problems.push(
2015
- `${path} is a second ${type} option; a widget has at most one.`
2016
- );
2017
- }
2018
- seenTypes.add(type);
2019
- const columnKeys = checkOptionColumns(
2020
- option,
2021
- path,
2022
- supported ? kind : void 0,
2023
- problems
2024
- );
2025
- if (option["apply"] !== void 0) {
2026
- requireOneOf(option, "apply", ["provider", "client"], path, problems);
2027
- }
2028
- if (type === "sort") {
2029
- if (fallback !== void 0) {
2030
- const sort = typeof fallback === "string" ? parseSortValue(fallback) : void 0;
2031
- if (sort === void 0) {
2032
- problems.push(
2033
- `${path}.default must look like "c1:desc" ("column:asc" or "column:desc"), got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
2034
- );
2035
- } else if (columnKeys.length > 0 && !columnKeys.includes(sort.column)) {
2036
- requireOneOf(
2037
- { default: sort.column },
2038
- "default",
2039
- columnKeys,
2040
- path,
2041
- problems
2042
- );
2043
- }
2044
- }
2045
- } else if (!Array.isArray(fallback) || fallback.length === 0 || fallback.some((k) => typeof k !== "string")) {
2046
- problems.push(
2047
- `${path}.default must be a non-empty array of column keys, got ${describeType2(fallback)}.`
2048
- );
2049
- } else {
2050
- const shown = /* @__PURE__ */ new Set();
2051
- for (const columnKey of fallback) {
2052
- if (columnKeys.length > 0 && !columnKeys.includes(columnKey)) {
2053
- problems.push(
2054
- `${path}.default has "${columnKey}", which is not one of the columns (${columnKeys.map((c) => `"${c}"`).join(", ")}).`
2055
- );
2056
- } else if (shown.has(columnKey)) {
2057
- problems.push(`${path}.default lists "${columnKey}" twice.`);
2058
- }
2059
- shown.add(columnKey);
2060
- }
2061
- }
2062
- break;
2160
+ return p;
2161
+ })
2162
+ };
2163
+ if (!hidden) {
2164
+ const allowed = rowsAllowed(target, options);
2165
+ const needed = placeRows(
2166
+ pageWidgets(definitions, moved, targetKey)
2167
+ ).length;
2168
+ if (needed > allowed) {
2169
+ return err(
2170
+ `${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
2171
+ );
2063
2172
  }
2064
2173
  }
2174
+ return ok(moved);
2065
2175
  }
2066
- function checkOptions(options, kind, problems) {
2067
- if (options === void 0) return;
2068
- if (!Array.isArray(options)) {
2069
- problems.push(`options must be an array, got ${describeType2(options)}.`);
2070
- return;
2071
- }
2072
- if (options.length > MAX_WIDGET_OPTIONS) {
2073
- problems.push(
2074
- `options lists ${options.length} options; the most a widget may declare is ${MAX_WIDGET_OPTIONS}.`
2075
- );
2076
- return;
2176
+ function withoutLockField(definition) {
2177
+ if (definition.locked === void 0) return definition;
2178
+ const copy = { ...definition };
2179
+ delete copy.locked;
2180
+ return copy;
2181
+ }
2182
+ function uniqueTitle2(taken, title, index) {
2183
+ let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
2184
+ if (base === "") base = `Page ${index + 1}`;
2185
+ let candidate = base;
2186
+ let n = 1;
2187
+ while (taken.has(normalizedTitle(candidate))) {
2188
+ const suffix = ` (#${n})`;
2189
+ candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
2190
+ n += 1;
2077
2191
  }
2078
- const seenKeys = /* @__PURE__ */ new Set();
2079
- const seenTypes = /* @__PURE__ */ new Set();
2080
- options.forEach((option, index) => {
2081
- checkOption(
2082
- option,
2083
- `options[${index}]`,
2084
- kind,
2085
- seenKeys,
2086
- seenTypes,
2087
- problems
2088
- );
2192
+ taken.add(normalizedTitle(candidate));
2193
+ return candidate;
2194
+ }
2195
+ function samePages(a, b) {
2196
+ return a.length === b.length && a.every((p, i) => {
2197
+ const q = b[i];
2198
+ return p.key === q.key && p.title === q.title && p.maxRows === q.maxRows && sameKeys(p.order, q.order) && sameKeys(p.hidden, q.hidden) && sameKeys(p.minimized, q.minimized);
2089
2199
  });
2090
2200
  }
2091
- function formatProblems(prefix, problems) {
2092
- const shown = problems.slice(0, MAX_REPORTED_PROBLEMS);
2093
- const more = problems.length - shown.length;
2094
- return `${prefix}: ${shown.join(" ")}${more > 0 ? ` (and ${more} more problem(s))` : ""}`;
2201
+ function normalizeLayout(definitions, layout, options = {}) {
2202
+ const clones = cleanClones(definitions, layout.clones, options);
2203
+ let base = layout;
2204
+ if (clones !== layout.clones) {
2205
+ const { clones: _dropped, ...others } = layout;
2206
+ base = clones === void 0 ? others : { ...others, clones };
2207
+ }
2208
+ return normalizeWidgets(
2209
+ withClones(definitions, base, options),
2210
+ base,
2211
+ options
2212
+ );
2095
2213
  }
2096
- function validateWidgetData(value, options = {}) {
2097
- const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
2098
- if (!isRecord2(value)) {
2099
- return err(`${subject}: must be an object, got ${describeType2(value)}.`);
2100
- }
2101
- if (value["empty"] === true) {
2102
- if (typeof value["text"] !== "string") {
2103
- return err(
2104
- `${subject} (empty): text must be a string, got ${describeType2(value["text"])}.`
2105
- );
2106
- }
2107
- return ok(value);
2214
+ function normalizeWidgets(definitions, layout, options) {
2215
+ const known = pruneLayout(definitions, layout);
2216
+ const enforced = enforceLocks(definitions, known, options);
2217
+ const locked = normalizeSettings(
2218
+ definitions,
2219
+ enforced,
2220
+ options
2221
+ );
2222
+ if (!hasPages(locked)) return locked === layout ? layout : locked;
2223
+ const defs = effectiveDefinitions(
2224
+ options.overrideLocks === true ? definitions.map(withoutLockField) : definitions,
2225
+ locked,
2226
+ options
2227
+ );
2228
+ const taken = /* @__PURE__ */ new Set();
2229
+ const seenKeys = /* @__PURE__ */ new Set();
2230
+ const held = /* @__PURE__ */ new Set();
2231
+ let pages = [];
2232
+ (locked.pages ?? []).forEach((page, index) => {
2233
+ if (seenKeys.has(page.key)) return;
2234
+ seenKeys.add(page.key);
2235
+ const keep = (list) => [
2236
+ ...new Set(list.filter((k) => !held.has(k)))
2237
+ ];
2238
+ const next = {
2239
+ ...page,
2240
+ title: uniqueTitle2(taken, page.title, index),
2241
+ order: keep(page.order),
2242
+ hidden: keep(page.hidden),
2243
+ minimized: keep(page.minimized)
2244
+ };
2245
+ for (const k of [...next.order, ...next.hidden]) held.add(k);
2246
+ pages.push(next);
2247
+ });
2248
+ const work = () => ({ ...locked, pages });
2249
+ const pinned = defs.filter((d) => resolveWidgetLock(d).move);
2250
+ for (const widget of pinned) {
2251
+ const home = homePageKey(work(), widget);
2252
+ const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
2253
+ const wasHidden = pages.some((p) => keysHere(p, "hidden"));
2254
+ const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
2255
+ if (!wasHidden && !wasMinimized) continue;
2256
+ pages = pages.map((p) => {
2257
+ let next = p;
2258
+ if (keysHere(p, "hidden")) {
2259
+ next = { ...next, hidden: without(next.hidden, widget.key) };
2260
+ }
2261
+ if (keysHere(p, "minimized")) {
2262
+ next = { ...next, minimized: without(next.minimized, widget.key) };
2263
+ }
2264
+ if (p.key === home) {
2265
+ next = {
2266
+ ...next,
2267
+ hidden: wasHidden && !next.hidden.includes(widget.key) ? [...next.hidden, widget.key] : next.hidden,
2268
+ minimized: wasMinimized && !next.minimized.includes(widget.key) ? [...next.minimized, widget.key] : next.minimized
2269
+ };
2270
+ }
2271
+ return next;
2272
+ });
2108
2273
  }
2109
- const kind = value["kind"];
2110
- if (!isWidgetKind(kind)) {
2111
- return err(
2112
- `${subject}: kind must be one of ${WIDGET_KINDS.join(", ")} (or the payload must be {"empty": true, "text": ...}), got ${typeof kind === "string" ? JSON.stringify(kind) : describeType2(kind)}.`
2113
- );
2274
+ const placedKeys = /* @__PURE__ */ new Set();
2275
+ for (const p of pages) {
2276
+ for (const k of [...p.order, ...p.hidden]) placedKeys.add(k);
2114
2277
  }
2115
- if (options.expectedKind !== void 0 && kind !== options.expectedKind) {
2116
- return err(
2117
- `${subject}: expected kind ${options.expectedKind} from its definition, got ${kind}.`
2278
+ const pinnedKeys = new Set(pinned.map((d) => d.key));
2279
+ const unplaced = defs.filter((d) => !placedKeys.has(d.key) && !pinnedKeys.has(d.key)).map((definition, index) => ({ definition, index })).sort(
2280
+ (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
2281
+ ).map(({ definition }) => definition);
2282
+ for (const widget of unplaced) {
2283
+ const home = homePageKey(work(), widget);
2284
+ pages = pages.map(
2285
+ (p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
2118
2286
  );
2119
2287
  }
2120
- const problems = [];
2121
- checkPayloadFields(kind, value, problems);
2122
- if (problems.length > 0) {
2123
- return err(formatProblems(`${subject} (${kind})`, problems));
2124
- }
2125
- return ok(value);
2126
- }
2127
- function validateWidgetDefinition(value) {
2128
- if (!isRecord2(value)) {
2129
- return err(
2130
- `Widget definition: must be an object, got ${describeType2(value)}.`
2288
+ for (let i = 0; i < pages.length; i += 1) {
2289
+ const page = pages[i];
2290
+ const shown = pageWidgets(defs, work(), page.key);
2291
+ const fits = fitCount(shown, rowsAllowed(page, options));
2292
+ if (fits >= shown.length) continue;
2293
+ const moving = shown.slice(fits).filter((d) => !pinnedKeys.has(d.key)).map((d) => d.key);
2294
+ if (moving.length === 0) continue;
2295
+ const moveSet = new Set(moving);
2296
+ const movedMinimized = page.minimized.filter(
2297
+ (k) => moveSet.has(k)
2131
2298
  );
2132
- }
2133
- const subject = typeof value["key"] === "string" ? `Widget definition ${JSON.stringify(value["key"])}` : "Widget definition";
2134
- const problems = [];
2135
- requireString(value, "key", "", problems);
2136
- if (value["key"] === "") {
2137
- problems.push("key must not be empty.");
2138
- }
2139
- requireString(value, "title", "", problems);
2140
- optionalString(value, "description", "", problems);
2141
- requireOneOf(value, "kind", WIDGET_KINDS, "", problems);
2142
- optionalNumber(value, "sortOrder", "", problems);
2143
- if (value["roles"] !== void 0) {
2144
- const roles = requireArray(value, "roles", "", problems);
2145
- roles?.forEach((role, index) => {
2146
- if (typeof role !== "string") {
2147
- problems.push(
2148
- `roles[${index}] must be a string, got ${describeType2(role)}.`
2149
- );
2299
+ pages = pages.map(
2300
+ (p) => p.key === page.key ? {
2301
+ ...p,
2302
+ order: p.order.filter((k) => !moveSet.has(k)),
2303
+ minimized: p.minimized.filter((k) => !moveSet.has(k))
2304
+ } : p
2305
+ );
2306
+ let placed = false;
2307
+ for (let j = i + 1; j < pages.length && !placed; j += 1) {
2308
+ const candidate = pages[j];
2309
+ const trial = pages.map(
2310
+ (p) => p.key === candidate.key ? {
2311
+ ...p,
2312
+ order: [...moving, ...p.order],
2313
+ minimized: [...p.minimized, ...movedMinimized]
2314
+ } : p
2315
+ );
2316
+ const items = pageWidgets(
2317
+ defs,
2318
+ { ...locked, pages: trial },
2319
+ candidate.key
2320
+ );
2321
+ if (placeRows(items).length <= rowsAllowed(candidate, options)) {
2322
+ pages = trial;
2323
+ placed = true;
2150
2324
  }
2151
- });
2325
+ }
2326
+ if (!placed) {
2327
+ const key = nextPageKey(work());
2328
+ const title = uniqueTitle2(
2329
+ new Set(pages.map((p) => normalizedTitle(p.title))),
2330
+ `Page ${pages.length + 1}`,
2331
+ pages.length
2332
+ );
2333
+ pages = [
2334
+ ...pages,
2335
+ { key, title, order: moving, hidden: [], minimized: movedMinimized }
2336
+ ];
2337
+ }
2152
2338
  }
2153
- optionalBoolean(value, "active", "", problems);
2154
- if (value["defaultSize"] !== void 0) {
2155
- requireOneOf(value, "defaultSize", WIDGET_SIZES, "", problems);
2339
+ const result = work();
2340
+ return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) && locked.settings === layout.settings ? layout : result;
2341
+ }
2342
+
2343
+ // src/core/duplicate.ts
2344
+ function canDuplicate(definitions, key, options = {}) {
2345
+ const definition = definitions.find(
2346
+ (d) => d.key === key
2347
+ );
2348
+ if (definition === void 0) return false;
2349
+ if (options.overrideLocks === true) return true;
2350
+ const sourceKey = definition.sourceKey ?? definition.key;
2351
+ const original = definitions.find(
2352
+ (d) => d.key === sourceKey
2353
+ );
2354
+ return original !== void 0 && !resolveWidgetLock(original).clone;
2355
+ }
2356
+ function nextCloneKey(definitions, layout, fromKey) {
2357
+ const taken = /* @__PURE__ */ new Set([
2358
+ ...definitions.map((d) => d.key),
2359
+ ...(layout.clones ?? []).map((c) => c.key)
2360
+ ]);
2361
+ let highest = 0;
2362
+ for (const clone of layout.clones ?? []) {
2363
+ if (clone.from === fromKey) {
2364
+ highest = Math.max(highest, cloneNumber(clone.key) ?? 0);
2365
+ }
2156
2366
  }
2157
- if (value["width"] !== void 0 && !isWidgetWidth(value["width"])) {
2158
- problems.push(
2159
- `width must be an integer from ${MIN_WIDGET_WIDTH} to ${MAX_WIDGET_WIDTH} (twelfths of the row), got ${typeof value["width"] === "number" ? String(value["width"]) : describeType2(value["width"])}.`
2367
+ let n = highest + 1;
2368
+ while (taken.has(`${fromKey}#${n}`)) n += 1;
2369
+ return `${fromKey}#${n}`;
2370
+ }
2371
+ function suggestedViewTitle(title, viewLabel) {
2372
+ return `${title} ${viewLabel}`.trim().slice(0, MAX_WIDGET_TITLE_LENGTH);
2373
+ }
2374
+ function duplicateWidget(definitions, layout, key, options = {}) {
2375
+ const widget = definitions.find((d) => d.key === key);
2376
+ if (widget === void 0) {
2377
+ return err(`Widget ${JSON.stringify(key)} isn't on this dashboard.`);
2378
+ }
2379
+ const originalKey = widget.sourceKey ?? widget.key;
2380
+ if (!canDuplicate(definitions, key, options)) {
2381
+ return err(
2382
+ `Widget ${JSON.stringify(originalKey)} is locked against duplicating.`
2160
2383
  );
2161
2384
  }
2162
- checkWidthLimits(value, problems);
2163
- checkDetailOption(value["detail"], problems);
2164
- checkTableControlsOption(value["tableControls"], value["kind"], problems);
2165
- checkLockedOption(value["locked"], problems);
2166
- checkOptions(value["options"], value["kind"], problems);
2167
- if (value["page"] !== void 0) {
2168
- if (typeof value["page"] !== "string" || value["page"].trim() === "") {
2169
- problems.push(
2170
- `page must be a non-empty page key or title, got ${describeType2(value["page"])}.`
2171
- );
2385
+ const cloneKey = nextCloneKey(definitions, layout, originalKey);
2386
+ const copiedSettings = (() => {
2387
+ const mine = layout.settings?.[key];
2388
+ if (mine === void 0) return void 0;
2389
+ const { title: _title, ...rest } = mine;
2390
+ return Object.keys(rest).length === 0 ? void 0 : rest;
2391
+ })();
2392
+ const settings = copiedSettings === void 0 ? layout.settings : { ...layout.settings, [cloneKey]: copiedSettings };
2393
+ let next = {
2394
+ ...layout,
2395
+ clones: [...layout.clones ?? [], { key: cloneKey, from: originalKey }],
2396
+ ...settings === void 0 ? {} : { settings }
2397
+ };
2398
+ const all = withClones(definitions, next, options);
2399
+ const assigned = assignPages(all, next);
2400
+ const pageKey = [...assigned].find(
2401
+ ([, widgets]) => widgets.some((w) => w.key === key)
2402
+ )?.[0];
2403
+ if (pageKey !== void 0) {
2404
+ const view = pageLayout(next, pageKey);
2405
+ const onPage = assigned.get(pageKey) ?? [];
2406
+ if (view !== void 0) {
2407
+ const shown = visibleWidgets(onPage, view).filter((d) => !resolveWidgetLock(d).move && d.key !== cloneKey).map((d) => d.key);
2408
+ const at = shown.indexOf(key);
2409
+ const order = at === -1 ? [...shown, cloneKey] : [...shown.slice(0, at + 1), cloneKey, ...shown.slice(at + 1)];
2410
+ next = withPageLayout(next, pageKey, { ...view, order });
2172
2411
  }
2173
2412
  }
2174
- if (value["fill"] !== void 0) {
2175
- requireOneOf(value, "fill", WIDGET_FILLS, "", problems);
2176
- }
2177
- if (problems.length > 0) {
2178
- return err(formatProblems(subject, problems));
2413
+ if (hasPages(next) && pageKey !== void 0) {
2414
+ const effective = effectiveDefinitions(all, next, options);
2415
+ const room = pageRoom(effective, next, pageKey, options);
2416
+ if (room !== void 0 && room.rowsUsed > room.maxRows) {
2417
+ const page = next.pages?.find(
2418
+ (p) => p.key === pageKey
2419
+ );
2420
+ const width = effective.find(
2421
+ (d) => d.key === cloneKey
2422
+ )?.width;
2423
+ return err(
2424
+ `Page "${page?.title ?? pageKey}" has no room for a widget${width === void 0 ? "" : ` ${width} wide`} (it holds ${room.maxRows} rows).`
2425
+ );
2426
+ }
2179
2427
  }
2180
- return ok(value);
2428
+ return ok({ layout: next, key: cloneKey });
2429
+ }
2430
+ function deleteClone(layout, key) {
2431
+ if (layout.clones?.some((clone) => clone.key === key) !== true) return layout;
2432
+ const without2 = (keys) => keys.includes(key) ? keys.filter((k) => k !== key) : keys;
2433
+ const { clones, settings, ...rest } = layout;
2434
+ const remaining = (clones ?? []).filter((c) => c.key !== key);
2435
+ const { [key]: _gone, ...otherSettings } = settings ?? {};
2436
+ return {
2437
+ ...rest,
2438
+ order: without2(layout.order),
2439
+ hidden: without2(layout.hidden),
2440
+ minimized: without2(layout.minimized),
2441
+ ...layout.pages === void 0 ? {} : {
2442
+ pages: layout.pages.map((page) => ({
2443
+ ...page,
2444
+ order: without2(page.order),
2445
+ hidden: without2(page.hidden),
2446
+ minimized: without2(page.minimized)
2447
+ }))
2448
+ },
2449
+ ...Object.keys(otherSettings).length === 0 ? {} : { settings: otherSettings },
2450
+ ...remaining.length === 0 ? {} : { clones: remaining }
2451
+ };
2181
2452
  }
2182
2453
 
2183
- // src/core/resolve.ts
2184
- function resolvePayload(definition, payload, options = {}) {
2185
- let checked;
2186
- if (options.validate === false) {
2187
- checked = payload;
2188
- } else {
2189
- const result = validateWidgetData(payload, {
2190
- widgetKey: definition.key,
2191
- expectedKind: definition.kind
2192
- });
2193
- if (!result.ok) {
2194
- return { definition, status: "error", error: result.error };
2195
- }
2196
- checked = result.value;
2197
- }
2198
- const emptyText = emptyTextFor(checked);
2199
- if (emptyText !== null) {
2200
- return { definition, status: "empty", emptyText };
2454
+ // src/core/validate.ts
2455
+ var MAX_REPORTED_PROBLEMS = 10;
2456
+ function describeType2(value) {
2457
+ if (value === null) return "null";
2458
+ if (Array.isArray(value)) return "array";
2459
+ if (typeof value === "number" && !Number.isFinite(value)) {
2460
+ return String(value);
2201
2461
  }
2202
- return { definition, status: "ok", data: checked };
2462
+ return typeof value;
2203
2463
  }
2204
- function failedWidget(definition, error) {
2205
- return { definition, status: "error", error };
2464
+ function isRecord3(value) {
2465
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2206
2466
  }
2207
- function loadingWidgets(definitions) {
2208
- return definitions.map((definition) => ({ definition, status: "loading" }));
2467
+ function join(path, field) {
2468
+ return path === "" ? field : `${path}.${field}`;
2209
2469
  }
2210
- function cacheKeyFor(definition, options, values) {
2211
- const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
2212
- return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
2470
+ function requireString(record, field, path, problems) {
2471
+ const value = record[field];
2472
+ if (typeof value !== "string") {
2473
+ problems.push(
2474
+ `${join(path, field)} must be a string, got ${describeType2(value)}.`
2475
+ );
2476
+ }
2213
2477
  }
2214
- function optionValuesFor(definition, options) {
2215
- return resolveOptionValues(
2216
- definition,
2217
- options.optionValues?.[definition.key],
2218
- options
2219
- );
2478
+ function optionalString(record, field, path, problems) {
2479
+ if (record[field] !== void 0) {
2480
+ requireString(record, field, path, problems);
2481
+ }
2220
2482
  }
2221
- function finalizeWidget(widget, values) {
2222
- if (Object.keys(values).length === 0) return widget;
2223
- if (widget.status === "ok") {
2224
- const data = applyClientOptions(
2225
- widget.definition,
2226
- widget.data,
2227
- values
2483
+ function requireNumber(record, field, path, problems) {
2484
+ const value = record[field];
2485
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2486
+ problems.push(
2487
+ `${join(path, field)} must be a finite number, got ${describeType2(value)}.`
2228
2488
  );
2229
- return { ...widget, data, options: values };
2230
2489
  }
2231
- return { ...widget, options: values };
2232
2490
  }
2233
- var LoadInterruption = class extends Error {
2234
- };
2235
- async function resolveWidget(definition, provider, context, options = {}) {
2236
- const values = optionValuesFor(definition, options);
2237
- return finalizeWidget(
2238
- await resolveWidgetUnapplied(definition, provider, context, options),
2239
- values
2240
- );
2491
+ function optionalNumber(record, field, path, problems) {
2492
+ if (record[field] !== void 0) {
2493
+ requireNumber(record, field, path, problems);
2494
+ }
2241
2495
  }
2242
- async function resolveWidgetUnapplied(definition, provider, context, options = {}) {
2243
- const values = optionValuesFor(definition, options);
2244
- const quotedKey = JSON.stringify(definition.key);
2245
- if (provider === void 0) {
2246
- return failedWidget(
2247
- definition,
2248
- `Widget ${quotedKey}: no provider is registered for this key.`
2496
+ function optionalSortValue(record, path, problems) {
2497
+ const value = record["value"];
2498
+ if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
2499
+ problems.push(
2500
+ `${path}.value must be a string or a finite number, got ${describeType2(value)}.`
2249
2501
  );
2250
2502
  }
2251
- if (options.signal?.aborted === true) {
2252
- return failedWidget(
2253
- definition,
2254
- `Widget ${quotedKey}: loading was canceled before it started.`
2503
+ }
2504
+ function optionalBoolean(record, field, path, problems) {
2505
+ const value = record[field];
2506
+ if (value !== void 0 && typeof value !== "boolean") {
2507
+ problems.push(
2508
+ `${join(path, field)} must be a boolean, got ${describeType2(value)}.`
2255
2509
  );
2256
2510
  }
2257
- const controller = new AbortController();
2258
- const onExternalAbort = () => controller.abort(options.signal?.reason);
2259
- options.signal?.addEventListener("abort", onExternalAbort, { once: true });
2260
- let timedOut = false;
2261
- const timeoutMs = options.timeoutMs !== void 0 && options.timeoutMs > 0 ? options.timeoutMs : void 0;
2262
- const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
2263
- timedOut = true;
2264
- controller.abort(new Error(`Timed out after ${timeoutMs} ms.`));
2265
- }, timeoutMs);
2266
- const interrupted = new Promise((_resolve, reject) => {
2267
- controller.signal.addEventListener(
2268
- "abort",
2269
- () => reject(new LoadInterruption()),
2270
- { once: true }
2511
+ }
2512
+ function optionalUrl(record, field, path, problems, isSafe, allowed) {
2513
+ const value = record[field];
2514
+ if (value === void 0) return;
2515
+ if (typeof value !== "string") {
2516
+ problems.push(
2517
+ `${join(path, field)} must be a string, got ${describeType2(value)}.`
2271
2518
  );
2272
- });
2273
- let payload;
2274
- try {
2275
- payload = await Promise.race([
2276
- Promise.resolve().then(() => {
2277
- if (controller.signal.aborted) throw new LoadInterruption();
2278
- return provider(context, definition, {
2279
- signal: controller.signal,
2280
- options: values
2281
- });
2282
- }),
2283
- interrupted
2284
- ]);
2285
- } catch (cause) {
2286
- if (cause instanceof LoadInterruption) {
2287
- return failedWidget(
2288
- definition,
2289
- timedOut ? `Widget ${quotedKey}: provider timed out after ${timeoutMs} ms.` : `Widget ${quotedKey}: loading was canceled.`
2290
- );
2291
- }
2292
- return failedWidget(
2293
- definition,
2294
- `Widget ${quotedKey}: provider failed: ${describeUnknownError(cause)}`
2519
+ } else if (!isSafe(value)) {
2520
+ problems.push(
2521
+ `${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
2295
2522
  );
2296
- } finally {
2297
- if (timer !== void 0) clearTimeout(timer);
2298
- options.signal?.removeEventListener("abort", onExternalAbort);
2299
2523
  }
2300
- const resolved = resolvePayload(definition, payload, options);
2301
- if (resolved.status !== "ok") return resolved;
2302
- const storedAt = Date.now();
2303
- if (options.cache !== void 0) {
2304
- try {
2305
- await options.cache.set(cacheKeyFor(definition, options, values), {
2306
- payload: resolved.data,
2307
- storedAt
2308
- });
2309
- } catch {
2310
- }
2524
+ }
2525
+ function requireArray(record, field, path, problems) {
2526
+ const value = record[field];
2527
+ if (!Array.isArray(value)) {
2528
+ problems.push(
2529
+ `${join(path, field)} must be an array, got ${describeType2(value)}.`
2530
+ );
2531
+ return null;
2311
2532
  }
2312
- return { ...resolved, updatedAt: storedAt };
2533
+ return value;
2313
2534
  }
2314
- function widgetsFor(definitions, roles) {
2315
- return sortDefinitions(
2316
- definitions.filter((definition) => isWidgetVisibleTo(definition, roles))
2317
- );
2535
+ function requireOneOf(record, field, options, path, problems) {
2536
+ const value = record[field];
2537
+ if (typeof value !== "string" || !options.includes(value)) {
2538
+ problems.push(
2539
+ `${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType2(value)}.`
2540
+ );
2541
+ }
2318
2542
  }
2319
- async function resolveWidgets(definitions, providers, context, options = {}) {
2320
- const visible = widgetsFor(definitions, context.roles);
2321
- return Promise.all(
2322
- visible.map(
2323
- (definition) => resolveWidget(
2324
- definition,
2325
- Object.prototype.hasOwnProperty.call(providers, definition.key) ? providers[definition.key] : void 0,
2326
- context,
2327
- options
2328
- )
2329
- )
2330
- );
2543
+ function eachRecord(items, path, problems, check) {
2544
+ items.forEach((item, index) => {
2545
+ const itemPath = `${path}[${index}]`;
2546
+ if (isRecord3(item)) {
2547
+ check(item, itemPath);
2548
+ } else {
2549
+ problems.push(
2550
+ `${itemPath} must be an object, got ${describeType2(item)}.`
2551
+ );
2552
+ }
2553
+ });
2331
2554
  }
2332
-
2333
- // src/core/loader.ts
2334
- function createWidgetLoader(options) {
2335
- const { definitions, providers, context, ...baseOptions } = options;
2336
- const chosen = new Map(
2337
- Object.entries(baseOptions.optionValues ?? {}).flatMap(
2338
- ([key, values]) => values === void 0 ? [] : [[key, values]]
2339
- )
2340
- );
2341
- const resolveOptions = baseOptions;
2342
- function currentOptions() {
2343
- return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
2344
- }
2345
- const unapplied = /* @__PURE__ */ new Map();
2346
- const visible = widgetsFor(definitions, context.roles);
2347
- let snapshot = loadingWidgets(visible);
2348
- const listeners = /* @__PURE__ */ new Set();
2349
- const started = /* @__PURE__ */ new Set();
2350
- const generations = /* @__PURE__ */ new Map();
2351
- const inFlight = /* @__PURE__ */ new Map();
2352
- let disposed = false;
2353
- function publish(next) {
2354
- snapshot = next;
2355
- for (const listener of [...listeners]) {
2356
- try {
2357
- listener();
2358
- } catch {
2555
+ var HREF_RULE = "an http(s), mailto or relative URL";
2556
+ var IMAGE_RULE = "an http(s) or relative URL";
2557
+ function checkPayloadFields(kind, data, problems) {
2558
+ switch (kind) {
2559
+ case "TEXT":
2560
+ requireString(data, "value", "", problems);
2561
+ requireString(data, "label", "", problems);
2562
+ return;
2563
+ case "KPI":
2564
+ requireNumber(data, "value", "", problems);
2565
+ if (data["previous"] !== null) {
2566
+ optionalNumber(data, "previous", "", problems);
2359
2567
  }
2360
- }
2361
- }
2362
- function replace(key, widget) {
2363
- publish(
2364
- snapshot.map(
2365
- (current) => current.definition.key === key ? widget : current
2366
- )
2367
- );
2368
- }
2369
- function abortAll(reason) {
2370
- for (const controller of inFlight.values()) controller.abort(reason);
2371
- inFlight.clear();
2372
- }
2373
- resolveOptions.signal?.addEventListener(
2374
- "abort",
2375
- () => abortAll(resolveOptions.signal?.reason),
2376
- { once: true }
2377
- );
2378
- async function run(definition, readCache) {
2379
- const key = definition.key;
2380
- const generation = (generations.get(key) ?? 0) + 1;
2381
- generations.set(key, generation);
2382
- inFlight.get(key)?.abort(new Error("Superseded by a newer load."));
2383
- const controller = new AbortController();
2384
- inFlight.set(key, controller);
2385
- const isCurrent = () => !disposed && generations.get(key) === generation;
2386
- const runOptions = currentOptions();
2387
- const values = optionValuesFor(definition, runOptions);
2388
- if (readCache && runOptions.cache !== void 0) {
2389
- try {
2390
- const entry = await runOptions.cache.get(
2391
- cacheKeyFor(definition, runOptions, values)
2392
- );
2393
- if (entry !== void 0 && isCurrent()) {
2394
- const cached = resolvePayload(definition, entry.payload, runOptions);
2395
- if (cached.status === "ok") {
2396
- const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
2397
- unapplied.set(key, stale);
2398
- replace(key, finalizeWidget(stale, values));
2568
+ requireOneOf(data, "format", VALUE_FORMATS, "", problems);
2569
+ optionalString(data, "currency", "", problems);
2570
+ requireString(data, "label", "", problems);
2571
+ optionalString(data, "hint", "", problems);
2572
+ optionalBoolean(data, "higherIsBetter", "", problems);
2573
+ return;
2574
+ case "GAUGE":
2575
+ requireNumber(data, "value", "", problems);
2576
+ requireNumber(data, "max", "", problems);
2577
+ requireString(data, "label", "", problems);
2578
+ if (typeof data["max"] === "number" && data["max"] <= 0) {
2579
+ problems.push(`max must be greater than 0, got ${data["max"]}.`);
2580
+ }
2581
+ return;
2582
+ case "TABLE": {
2583
+ const columns = requireArray(data, "columns", "", problems);
2584
+ if (columns !== null) {
2585
+ eachRecord(columns, "columns", problems, (column, path) => {
2586
+ requireString(column, "label", path, problems);
2587
+ optionalBoolean(column, "numeric", path, problems);
2588
+ });
2589
+ }
2590
+ const rows = requireArray(data, "rows", "", problems);
2591
+ if (rows !== null) {
2592
+ rows.forEach((row, rowIndex) => {
2593
+ const rowPath = `rows[${rowIndex}]`;
2594
+ if (!Array.isArray(row)) {
2595
+ problems.push(
2596
+ `${rowPath} must be an array of cells, got ${describeType2(row)}.`
2597
+ );
2598
+ return;
2399
2599
  }
2400
- }
2401
- } catch {
2600
+ if (columns !== null && row.length !== columns.length) {
2601
+ problems.push(
2602
+ `${rowPath} has ${row.length} cell(s) but there are ${columns.length} column(s).`
2603
+ );
2604
+ }
2605
+ eachRecord(row, rowPath, problems, (cell, cellPath) => {
2606
+ requireString(cell, "text", cellPath, problems);
2607
+ optionalSortValue(cell, cellPath, problems);
2608
+ optionalUrl(
2609
+ cell,
2610
+ "href",
2611
+ cellPath,
2612
+ problems,
2613
+ isSafeHref,
2614
+ HREF_RULE
2615
+ );
2616
+ });
2617
+ });
2402
2618
  }
2619
+ optionalString(data, "footer", "", problems);
2620
+ return;
2403
2621
  }
2404
- if (!isCurrent()) return;
2405
- const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
2406
- const result = await resolveWidgetUnapplied(definition, provider, context, {
2407
- ...runOptions,
2408
- signal: controller.signal
2409
- });
2410
- if (!isCurrent()) return;
2411
- inFlight.delete(key);
2412
- if (result.status === "ok") unapplied.set(key, result);
2413
- else unapplied.delete(key);
2414
- replace(key, finalizeWidget(result, values));
2415
- }
2416
- function start(definition, readCache) {
2417
- started.add(definition.key);
2418
- void run(definition, readCache);
2419
- }
2420
- function definitionFor(key) {
2421
- return visible.find((definition) => definition.key === key);
2422
- }
2423
- function load(key) {
2424
- if (disposed || started.has(key)) return;
2425
- const definition = definitionFor(key);
2426
- if (definition !== void 0) start(definition, true);
2427
- }
2428
- function loadAll() {
2429
- for (const definition of visible) load(definition.key);
2430
- }
2431
- function refresh(key, refreshOptions) {
2432
- if (disposed) return;
2433
- const keys = key === void 0 ? [...started] : [key];
2434
- for (const target of keys) {
2435
- const definition = definitionFor(target);
2436
- if (definition === void 0) continue;
2437
- if (refreshOptions?.skipInFlight === true && inFlight.has(target)) {
2438
- continue;
2439
- }
2440
- const current = snapshot.find(
2441
- (widget) => widget.definition.key === target
2442
- );
2443
- if (current?.status === "error") {
2444
- replace(target, { definition, status: "loading" });
2622
+ case "BAR_LIST": {
2623
+ const items = requireArray(data, "items", "", problems);
2624
+ if (items !== null) {
2625
+ eachRecord(items, "items", problems, (item, path) => {
2626
+ requireString(item, "label", path, problems);
2627
+ requireNumber(item, "value", path, problems);
2628
+ optionalString(item, "display", path, problems);
2629
+ });
2445
2630
  }
2446
- start(definition, false);
2631
+ optionalNumber(data, "total", "", problems);
2632
+ return;
2447
2633
  }
2448
- }
2449
- function setOptions(key, values) {
2450
- if (disposed) return;
2451
- const definition = definitionFor(key);
2452
- if (definition === void 0) return;
2453
- const before = optionValuesFor(definition, currentOptions());
2454
- chosen.set(key, { ...values });
2455
- const runOptions = currentOptions();
2456
- const after = optionValuesFor(definition, runOptions);
2457
- if (sameOptionValues(before, after) || !started.has(key)) return;
2458
- const kept = unapplied.get(key);
2459
- if (kept !== void 0 && optionsCacheSuffix(definition, before, runOptions) === optionsCacheSuffix(definition, after, runOptions)) {
2460
- replace(key, finalizeWidget(kept, after));
2634
+ case "ALERT_LIST": {
2635
+ const items = requireArray(data, "items", "", problems);
2636
+ if (items !== null) {
2637
+ eachRecord(items, "items", problems, (item, path) => {
2638
+ requireString(item, "title", path, problems);
2639
+ optionalUrl(item, "href", path, problems, isSafeHref, HREF_RULE);
2640
+ optionalUrl(
2641
+ item,
2642
+ "thumbnailUrl",
2643
+ path,
2644
+ problems,
2645
+ isSafeImageUrl,
2646
+ IMAGE_RULE
2647
+ );
2648
+ requireString(item, "valueLabel", path, problems);
2649
+ optionalString(item, "detail", path, problems);
2650
+ });
2651
+ }
2652
+ requireNumber(data, "total", "", problems);
2653
+ requireString(data, "emptyText", "", problems);
2654
+ if (items !== null && typeof data["total"] === "number" && data["total"] < items.length) {
2655
+ problems.push(
2656
+ `total (${data["total"]}) must be at least the number of items (${items.length}).`
2657
+ );
2658
+ }
2461
2659
  return;
2462
2660
  }
2463
- const current = snapshot.find(
2464
- (widget) => widget.definition.key === key
2465
- );
2466
- if (current?.status === "ok") replace(key, { ...current, stale: true });
2467
- else if (current?.status === "error") {
2468
- replace(key, { definition, status: "loading" });
2661
+ case "GRAPH": {
2662
+ const series = requireArray(data, "series", "", problems);
2663
+ if (series !== null) {
2664
+ if (series.length > MAX_GRAPH_SERIES) {
2665
+ problems.push(
2666
+ `series has ${series.length} entries; at most ${MAX_GRAPH_SERIES} are allowed (fold the rest into "Other").`
2667
+ );
2668
+ }
2669
+ eachRecord(series, "series", problems, (entry, path) => {
2670
+ requireString(entry, "name", path, problems);
2671
+ const points = requireArray(entry, "points", path, problems);
2672
+ if (points !== null) {
2673
+ eachRecord(
2674
+ points,
2675
+ `${path}.points`,
2676
+ problems,
2677
+ (point, pointPath) => {
2678
+ requireString(point, "label", pointPath, problems);
2679
+ requireNumber(point, "value", pointPath, problems);
2680
+ }
2681
+ );
2682
+ }
2683
+ });
2684
+ }
2685
+ requireOneOf(data, "chartType", ["bar", "line"], "", problems);
2686
+ requireOneOf(data, "valueFormat", VALUE_FORMATS, "", problems);
2687
+ optionalString(data, "currency", "", problems);
2688
+ optionalString(data, "xLabel", "", problems);
2689
+ return;
2469
2690
  }
2470
- start(definition, true);
2471
2691
  }
2472
- function dispose() {
2473
- disposed = true;
2474
- abortAll(new Error("The widget loader was disposed."));
2475
- listeners.clear();
2692
+ }
2693
+ function checkDetailOption(detail, problems) {
2694
+ if (detail === void 0 || typeof detail === "boolean") return;
2695
+ if (!isRecord3(detail)) {
2696
+ problems.push(
2697
+ `detail must be true, false or an options object, got ${describeType2(detail)}.`
2698
+ );
2699
+ return;
2476
2700
  }
2477
- function subscribe(listener) {
2478
- listeners.add(listener);
2479
- return () => {
2480
- listeners.delete(listener);
2481
- };
2701
+ optionalString(detail, "title", "detail", problems);
2702
+ const pageSize = detail["pageSize"];
2703
+ if (pageSize !== void 0 && !(typeof pageSize === "number" && Number.isInteger(pageSize) && pageSize >= 1 && pageSize <= MAX_DETAIL_PAGE_SIZE)) {
2704
+ problems.push(
2705
+ `detail.pageSize must be a whole number from 1 to ${MAX_DETAIL_PAGE_SIZE}, got ${typeof pageSize === "number" ? String(pageSize) : describeType2(pageSize)}.`
2706
+ );
2707
+ }
2708
+ if (detail["mode"] !== void 0) {
2709
+ requireOneOf(detail, "mode", DETAIL_MODES, "detail", problems);
2482
2710
  }
2483
- return {
2484
- getSnapshot: () => snapshot,
2485
- subscribe,
2486
- load,
2487
- loadAll,
2488
- refresh,
2489
- setOptions,
2490
- dispose
2491
- };
2492
- }
2493
-
2494
- // src/core/pages.ts
2495
- var DEFAULT_PAGE_KEY = "default";
2496
- var DEFAULT_PAGE_ROWS = 4;
2497
- function rowsAllowed(page, options) {
2498
- const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
2499
- return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
2500
2711
  }
2501
- function hasPages(layout) {
2502
- return layout.pages !== void 0 && layout.pages.length > 0;
2712
+ function checkTableControlsOption(controls, kind, problems) {
2713
+ if (controls === void 0 || controls === false) return;
2714
+ if (typeof controls !== "boolean" && !isRecord3(controls)) {
2715
+ problems.push(
2716
+ `tableControls must be true, false or an options object, got ${describeType2(controls)}.`
2717
+ );
2718
+ return;
2719
+ }
2720
+ if (isRecord3(controls)) {
2721
+ optionalBoolean(controls, "search", "tableControls", problems);
2722
+ optionalBoolean(controls, "sort", "tableControls", problems);
2723
+ }
2724
+ if (typeof kind === "string" && kind !== "TABLE") {
2725
+ problems.push(
2726
+ `tableControls only applies to TABLE widgets, but kind is "${kind}".`
2727
+ );
2728
+ }
2503
2729
  }
2504
- function pageList(layout) {
2505
- if (layout.pages !== void 0 && layout.pages.length > 0) {
2506
- return layout.pages;
2730
+ function checkWidthLimits(value, problems) {
2731
+ for (const field of ["minWidth", "maxWidth"]) {
2732
+ const limit = value[field];
2733
+ if (limit !== void 0 && !isWidgetWidth(limit)) {
2734
+ problems.push(
2735
+ `${field} must be an integer from ${MIN_WIDGET_WIDTH} to ${MAX_WIDGET_WIDTH} (twelfths of the row), got ${typeof limit === "number" ? String(limit) : describeType2(limit)}.`
2736
+ );
2737
+ return;
2738
+ }
2507
2739
  }
2508
- return [
2509
- {
2510
- key: DEFAULT_PAGE_KEY,
2511
- title: "Page 1",
2512
- order: layout.order,
2513
- hidden: layout.hidden,
2514
- minimized: layout.minimized
2740
+ const { minWidth, maxWidth, width } = value;
2741
+ if (isWidgetWidth(minWidth) && isWidgetWidth(maxWidth) && minWidth > maxWidth) {
2742
+ problems.push(
2743
+ `minWidth (${minWidth}) must not be more than maxWidth (${maxWidth}).`
2744
+ );
2745
+ return;
2746
+ }
2747
+ if (isWidgetWidth(width)) {
2748
+ if (isWidgetWidth(minWidth) && width < minWidth) {
2749
+ problems.push(
2750
+ `width (${width}) must not be less than minWidth (${minWidth}).`
2751
+ );
2515
2752
  }
2516
- ];
2517
- }
2518
- function withExplicitPages(layout) {
2519
- if (hasPages(layout)) return layout;
2520
- return {
2521
- ...layout,
2522
- order: [],
2523
- hidden: [],
2524
- minimized: [],
2525
- pages: pageList(layout)
2526
- };
2753
+ if (isWidgetWidth(maxWidth) && width > maxWidth) {
2754
+ problems.push(
2755
+ `width (${width}) must not be more than maxWidth (${maxWidth}).`
2756
+ );
2757
+ }
2758
+ }
2527
2759
  }
2528
- function sameKeys(a, b) {
2529
- return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
2760
+ function checkLockedOption(locked, problems) {
2761
+ if (locked === void 0 || typeof locked === "boolean") return;
2762
+ if (!isRecord3(locked)) {
2763
+ problems.push(
2764
+ `locked must be true, false or an object, got ${describeType2(locked)}.`
2765
+ );
2766
+ return;
2767
+ }
2768
+ optionalBoolean(locked, "move", "locked", problems);
2769
+ optionalBoolean(locked, "hide", "locked", problems);
2770
+ optionalBoolean(locked, "minimize", "locked", problems);
2771
+ optionalBoolean(locked, "options", "locked", problems);
2772
+ optionalBoolean(locked, "clone", "locked", problems);
2530
2773
  }
2531
- function pageLayout(layout, pageKey) {
2532
- if (!hasPages(layout))
2533
- return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
2534
- const page = layout.pages?.find(
2535
- (p) => p.key === pageKey
2536
- );
2537
- if (page === void 0) return void 0;
2538
- const { pages, ...rest } = layout;
2539
- return {
2540
- ...rest,
2541
- order: page.order,
2542
- hidden: page.hidden,
2543
- minimized: page.minimized
2544
- };
2774
+ function checkOptionColumns(option, path, kind, problems) {
2775
+ const columns = option["columns"];
2776
+ if (!Array.isArray(columns) || columns.length === 0) {
2777
+ problems.push(
2778
+ `${join(path, "columns")} must be a non-empty array of { key, label }, got ${describeType2(columns)}.`
2779
+ );
2780
+ return [];
2781
+ }
2782
+ const keys = [];
2783
+ columns.forEach((entry, index) => {
2784
+ const entryPath = `${path}.columns[${index}]`;
2785
+ if (!isRecord3(entry)) {
2786
+ problems.push(
2787
+ `${entryPath} must be an object, got ${describeType2(entry)}.`
2788
+ );
2789
+ return;
2790
+ }
2791
+ requireString(entry, "label", entryPath, problems);
2792
+ const key = entry["key"];
2793
+ if (typeof key !== "string") {
2794
+ problems.push(
2795
+ `${entryPath}.key must be a string, got ${describeType2(key)}.`
2796
+ );
2797
+ } else if (typeof kind === "string" && !isColumnKeyFor(kind, key)) {
2798
+ problems.push(
2799
+ `${entryPath}.key "${key}" is not a column of a ${kind} widget (${kind === "TABLE" ? '"c0", "c1", ...' : '"label" or "value"'}).`
2800
+ );
2801
+ } else if (keys.includes(key)) {
2802
+ problems.push(`${entryPath}.key "${key}" is used twice.`);
2803
+ } else {
2804
+ keys.push(key);
2805
+ }
2806
+ });
2807
+ return keys;
2545
2808
  }
2546
- function withPageLayout(layout, pageKey, next) {
2547
- const current = pageLayout(layout, pageKey);
2548
- if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
2549
- return layout;
2809
+ function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
2810
+ if (!isRecord3(option)) {
2811
+ problems.push(`${path} must be an object, got ${describeType2(option)}.`);
2812
+ return;
2550
2813
  }
2551
- if (!hasPages(layout)) {
2552
- return {
2553
- ...layout,
2554
- order: next.order,
2555
- hidden: next.hidden,
2556
- minimized: next.minimized
2557
- };
2814
+ const type = option["type"];
2815
+ if (typeof type !== "string" || !WIDGET_OPTION_TYPES.includes(type)) {
2816
+ requireOneOf(option, "type", WIDGET_OPTION_TYPES, path, problems);
2817
+ return;
2558
2818
  }
2559
- return {
2560
- ...layout,
2561
- pages: (layout.pages ?? []).map(
2562
- (page) => page.key === pageKey ? {
2563
- ...page,
2564
- order: next.order,
2565
- hidden: next.hidden,
2566
- minimized: next.minimized
2567
- } : page
2568
- )
2569
- };
2570
- }
2571
- function normalizedTitle(title) {
2572
- return title.trim().normalize("NFKC").toLowerCase();
2573
- }
2574
- function resolvePageKey(layout, reference) {
2575
- if (reference === void 0 || reference === "") return void 0;
2576
- const pages = pageList(layout);
2577
- const byKey = pages.find((p) => p.key === reference);
2578
- if (byKey !== void 0) return byKey.key;
2579
- const wanted = normalizedTitle(reference);
2580
- return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
2581
- }
2582
- function homePageKey(layout, item) {
2583
- return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
2584
- }
2585
- function assignPages(definitions, layout) {
2586
- const pages = pageList(layout);
2587
- const assigned = new Map(pages.map((p) => [p.key, []]));
2588
- for (const definition of definitions) {
2589
- let pageKey;
2590
- if (!resolveWidgetLock(definition).move) {
2591
- pageKey = pages.find(
2592
- (p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
2593
- )?.key;
2819
+ const key = option["key"];
2820
+ if (typeof key !== "string" || !OPTION_KEY_PATTERN.test(key)) {
2821
+ problems.push(
2822
+ `${path}.key must be letters, digits, "_" or "-", starting with a letter (40 characters at most), got ${typeof key === "string" ? JSON.stringify(key) : describeType2(key)}.`
2823
+ );
2824
+ } else if (seenKeys.has(key)) {
2825
+ problems.push(`${path}.key "${key}" is used by another option.`);
2826
+ } else {
2827
+ seenKeys.add(key);
2828
+ }
2829
+ requireString(option, "label", path, problems);
2830
+ const fallback = option["default"];
2831
+ switch (type) {
2832
+ case "choice": {
2833
+ const choices = option["choices"];
2834
+ const values = [];
2835
+ if (!Array.isArray(choices) || choices.length === 0) {
2836
+ problems.push(
2837
+ `${path}.choices must be a non-empty array of { value, label }, got ${describeType2(choices)}.`
2838
+ );
2839
+ } else {
2840
+ choices.forEach((choice, index) => {
2841
+ const choicePath = `${path}.choices[${index}]`;
2842
+ if (!isRecord3(choice)) {
2843
+ problems.push(
2844
+ `${choicePath} must be an object, got ${describeType2(choice)}.`
2845
+ );
2846
+ return;
2847
+ }
2848
+ requireString(choice, "label", choicePath, problems);
2849
+ const value = choice["value"];
2850
+ if (typeof value !== "string" || value === "") {
2851
+ problems.push(
2852
+ `${choicePath}.value must be a non-empty string, got ${describeType2(value)}.`
2853
+ );
2854
+ } else if (values.includes(value)) {
2855
+ problems.push(`${choicePath}.value "${value}" is used twice.`);
2856
+ } else {
2857
+ values.push(value);
2858
+ }
2859
+ });
2860
+ }
2861
+ if (values.length > 0)
2862
+ requireOneOf(option, "default", values, path, problems);
2863
+ break;
2864
+ }
2865
+ case "number": {
2866
+ const { min, max, step } = option;
2867
+ requireNumber(option, "min", path, problems);
2868
+ requireNumber(option, "max", path, problems);
2869
+ if (typeof min === "number" && typeof max === "number" && min > max) {
2870
+ problems.push(
2871
+ `${path}.min (${min}) must not be more than max (${max}).`
2872
+ );
2873
+ }
2874
+ if (step !== void 0 && !(typeof step === "number" && Number.isFinite(step) && step > 0)) {
2875
+ problems.push(
2876
+ `${path}.step must be a number above 0, got ${describeType2(step)}.`
2877
+ );
2878
+ }
2879
+ if (typeof fallback !== "number" || !Number.isFinite(fallback)) {
2880
+ problems.push(
2881
+ `${path}.default must be a finite number, got ${describeType2(fallback)}.`
2882
+ );
2883
+ } else if (typeof min === "number" && typeof max === "number" && (fallback < min || fallback > max)) {
2884
+ problems.push(
2885
+ `${path}.default (${fallback}) must be between min (${min}) and max (${max}).`
2886
+ );
2887
+ } else if (typeof min === "number" && typeof step === "number" && step > 0 && Math.abs(
2888
+ (fallback - min) / step - Math.round((fallback - min) / step)
2889
+ ) > 1e-9) {
2890
+ problems.push(
2891
+ `${path}.default (${fallback}) must be ${min} plus a whole number of steps of ${step}.`
2892
+ );
2893
+ }
2894
+ break;
2895
+ }
2896
+ case "boolean":
2897
+ if (typeof fallback !== "boolean") {
2898
+ problems.push(
2899
+ `${path}.default must be a boolean, got ${describeType2(fallback)}.`
2900
+ );
2901
+ }
2902
+ break;
2903
+ case "text": {
2904
+ const maxLength = option["maxLength"];
2905
+ if (typeof maxLength !== "number" || !Number.isInteger(maxLength) || maxLength < 1 || maxLength > MAX_TEXT_OPTION_LENGTH) {
2906
+ problems.push(
2907
+ `${path}.maxLength must be a whole number from 1 to ${MAX_TEXT_OPTION_LENGTH}, got ${typeof maxLength === "number" ? maxLength : describeType2(maxLength)}.`
2908
+ );
2909
+ }
2910
+ if (typeof fallback !== "string") {
2911
+ problems.push(
2912
+ `${path}.default must be a string, got ${describeType2(fallback)}.`
2913
+ );
2914
+ } else if (typeof maxLength === "number" && fallback.length > maxLength) {
2915
+ problems.push(
2916
+ `${path}.default is ${fallback.length} characters; maxLength is ${maxLength}.`
2917
+ );
2918
+ }
2919
+ break;
2920
+ }
2921
+ case "dateRange": {
2922
+ const presets = option["presets"];
2923
+ const offered = [];
2924
+ if (presets !== void 0) {
2925
+ if (!Array.isArray(presets) || presets.length === 0) {
2926
+ problems.push(
2927
+ `${path}.presets must be a non-empty array of { value, label }, got ${describeType2(presets)}.`
2928
+ );
2929
+ } else {
2930
+ presets.forEach((preset, index) => {
2931
+ const presetPath = `${path}.presets[${index}]`;
2932
+ if (!isRecord3(preset)) {
2933
+ problems.push(
2934
+ `${presetPath} must be an object, got ${describeType2(preset)}.`
2935
+ );
2936
+ return;
2937
+ }
2938
+ requireString(preset, "label", presetPath, problems);
2939
+ requireOneOf(
2940
+ preset,
2941
+ "value",
2942
+ DATE_RANGE_PRESETS,
2943
+ presetPath,
2944
+ problems
2945
+ );
2946
+ if (typeof preset["value"] === "string")
2947
+ offered.push(preset["value"]);
2948
+ });
2949
+ }
2950
+ }
2951
+ const allowed = offered.length > 0 ? offered : DATE_RANGE_PRESETS;
2952
+ if (typeof fallback !== "string" || !(allowed.includes(fallback) || isIsoInterval(fallback))) {
2953
+ problems.push(
2954
+ `${path}.default must be one of ${allowed.map((p) => `"${p}"`).join(", ")} or an interval like "2026-01-01/2026-01-31", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
2955
+ );
2956
+ }
2957
+ break;
2958
+ }
2959
+ case "color":
2960
+ if (typeof fallback !== "string" || !/^#[0-9a-fA-F]{6}$/.test(fallback)) {
2961
+ problems.push(
2962
+ `${path}.default must be a color like "#1c5cab", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
2963
+ );
2964
+ }
2965
+ break;
2966
+ case "sort":
2967
+ case "columns": {
2968
+ const supported = type === "sort" ? kind === "TABLE" || kind === "BAR_LIST" : kind === "TABLE";
2969
+ if (!supported && typeof kind === "string") {
2970
+ problems.push(
2971
+ `${path} is a ${type} option, which only applies to ${type === "sort" ? "TABLE and BAR_LIST" : "TABLE"} widgets, but kind is "${kind}".`
2972
+ );
2973
+ }
2974
+ if (seenTypes.has(type)) {
2975
+ problems.push(
2976
+ `${path} is a second ${type} option; a widget has at most one.`
2977
+ );
2978
+ }
2979
+ seenTypes.add(type);
2980
+ const columnKeys = checkOptionColumns(
2981
+ option,
2982
+ path,
2983
+ supported ? kind : void 0,
2984
+ problems
2985
+ );
2986
+ if (option["apply"] !== void 0) {
2987
+ requireOneOf(option, "apply", ["provider", "client"], path, problems);
2988
+ }
2989
+ if (type === "sort") {
2990
+ if (fallback !== void 0) {
2991
+ const sort = typeof fallback === "string" ? parseSortValue(fallback) : void 0;
2992
+ if (sort === void 0) {
2993
+ problems.push(
2994
+ `${path}.default must look like "c1:desc" ("column:asc" or "column:desc"), got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
2995
+ );
2996
+ } else if (columnKeys.length > 0 && !columnKeys.includes(sort.column)) {
2997
+ requireOneOf(
2998
+ { default: sort.column },
2999
+ "default",
3000
+ columnKeys,
3001
+ path,
3002
+ problems
3003
+ );
3004
+ }
3005
+ }
3006
+ } else if (!Array.isArray(fallback) || fallback.length === 0 || fallback.some((k) => typeof k !== "string")) {
3007
+ problems.push(
3008
+ `${path}.default must be a non-empty array of column keys, got ${describeType2(fallback)}.`
3009
+ );
3010
+ } else {
3011
+ const shown = /* @__PURE__ */ new Set();
3012
+ for (const columnKey of fallback) {
3013
+ if (columnKeys.length > 0 && !columnKeys.includes(columnKey)) {
3014
+ problems.push(
3015
+ `${path}.default has "${columnKey}", which is not one of the columns (${columnKeys.map((c) => `"${c}"`).join(", ")}).`
3016
+ );
3017
+ } else if (shown.has(columnKey)) {
3018
+ problems.push(`${path}.default lists "${columnKey}" twice.`);
3019
+ }
3020
+ shown.add(columnKey);
3021
+ }
3022
+ }
3023
+ break;
2594
3024
  }
2595
- assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
2596
3025
  }
2597
- return assigned;
2598
3026
  }
2599
- function pageOf(definitions, layout, key) {
2600
- for (const [pageKey, widgets] of assignPages(definitions, layout)) {
2601
- if (widgets.some((w) => w.key === key)) return pageKey;
3027
+ function checkOptions(options, kind, problems) {
3028
+ if (options === void 0) return;
3029
+ if (!Array.isArray(options)) {
3030
+ problems.push(`options must be an array, got ${describeType2(options)}.`);
3031
+ return;
2602
3032
  }
2603
- return void 0;
3033
+ if (options.length > MAX_WIDGET_OPTIONS) {
3034
+ problems.push(
3035
+ `options lists ${options.length} options; the most a widget may declare is ${MAX_WIDGET_OPTIONS}.`
3036
+ );
3037
+ return;
3038
+ }
3039
+ const seenKeys = /* @__PURE__ */ new Set();
3040
+ const seenTypes = /* @__PURE__ */ new Set();
3041
+ options.forEach((option, index) => {
3042
+ checkOption(
3043
+ option,
3044
+ `options[${index}]`,
3045
+ kind,
3046
+ seenKeys,
3047
+ seenTypes,
3048
+ problems
3049
+ );
3050
+ });
2604
3051
  }
2605
- function pageWidgets(definitions, layout, pageKey) {
2606
- const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
2607
- const view = pageLayout(layout, pageKey);
2608
- return view === void 0 ? [] : visibleWidgets(onPage, view);
3052
+ var MAX_VIEW_COLUMN = 1e3;
3053
+ function checkColumnIndex(value, path, problems) {
3054
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_VIEW_COLUMN) {
3055
+ problems.push(
3056
+ `${path} must be a column index (a whole number from 0), got ${typeof value === "number" ? String(value) : describeType2(value)}.`
3057
+ );
3058
+ }
2609
3059
  }
2610
- function pageRoom(definitions, layout, pageKey, options = {}) {
2611
- const page = pageList(layout).find(
2612
- (p) => p.key === pageKey
2613
- );
2614
- if (page === void 0) return void 0;
2615
- const maxRows = rowsAllowed(page, options);
2616
- const rowsUsed = placeRows(
2617
- pageWidgets(definitions, layout, pageKey)
2618
- ).length;
2619
- return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
3060
+ function checkConversion(conversion, path, baseKind, viewKind2, problems) {
3061
+ if (!isRecord3(conversion)) {
3062
+ problems.push(
3063
+ `${path} must be an object with a type, got ${describeType2(conversion)}.`
3064
+ );
3065
+ return;
3066
+ }
3067
+ const type = conversion["type"];
3068
+ const known = typeof type === "string" && Object.hasOwn(VIEW_CONVERSIONS, type) ? VIEW_CONVERSIONS[type] : void 0;
3069
+ if (known === void 0) {
3070
+ problems.push(
3071
+ `${path}.type must be one of ${Object.keys(VIEW_CONVERSIONS).map((name) => `"${name}"`).join(", ")}, got ${describeType2(type)}.`
3072
+ );
3073
+ return;
3074
+ }
3075
+ if (baseKind !== known.from) {
3076
+ problems.push(
3077
+ `${path}.type "${String(type)}" reads a ${known.from} widget, but this one is ${String(baseKind)}.`
3078
+ );
3079
+ }
3080
+ if (viewKind2 !== known.to) {
3081
+ problems.push(
3082
+ `${path}.type "${String(type)}" produces ${known.to}, but the view's kind is ${String(viewKind2)}.`
3083
+ );
3084
+ }
3085
+ switch (type) {
3086
+ case "tableToBarList":
3087
+ checkColumnIndex(conversion["label"], `${path}.label`, problems);
3088
+ checkColumnIndex(conversion["value"], `${path}.value`, problems);
3089
+ break;
3090
+ case "tableToGraph": {
3091
+ checkColumnIndex(conversion["label"], `${path}.label`, problems);
3092
+ const values = conversion["values"];
3093
+ if (!Array.isArray(values) || values.length === 0) {
3094
+ problems.push(`${path}.values must list 1 or more column indexes.`);
3095
+ } else if (values.length > MAX_GRAPH_SERIES) {
3096
+ problems.push(
3097
+ `${path}.values lists ${values.length} series; a chart carries at most ${MAX_GRAPH_SERIES}.`
3098
+ );
3099
+ } else {
3100
+ values.forEach(
3101
+ (index, at) => checkColumnIndex(index, `${path}.values[${at}]`, problems)
3102
+ );
3103
+ }
3104
+ if (conversion["chartType"] !== "bar" && conversion["chartType"] !== "line") {
3105
+ problems.push(`${path}.chartType must be "bar" or "line".`);
3106
+ }
3107
+ break;
3108
+ }
3109
+ case "barListToGraph":
3110
+ if (conversion["chartType"] !== "bar" && conversion["chartType"] !== "line") {
3111
+ problems.push(`${path}.chartType must be "bar" or "line".`);
3112
+ }
3113
+ break;
3114
+ }
3115
+ if (conversion["valueFormat"] !== void 0 && !VALUE_FORMATS.includes(conversion["valueFormat"])) {
3116
+ problems.push(
3117
+ `${path}.valueFormat must be one of ${VALUE_FORMATS.map((f) => `"${f}"`).join(", ")}.`
3118
+ );
3119
+ }
2620
3120
  }
2621
- function describePage(page) {
2622
- return `Page "${page.title}"`;
3121
+ function checkViews(value, problems) {
3122
+ const views = value["views"];
3123
+ const defaultView = value["defaultView"];
3124
+ optionalString(value, "baseViewLabel", "", problems);
3125
+ if (value["baseViewLabel"] === "") {
3126
+ problems.push("baseViewLabel must not be empty.");
3127
+ }
3128
+ const declared = /* @__PURE__ */ new Set([BASE_VIEW_KEY]);
3129
+ if (views !== void 0) {
3130
+ if (!Array.isArray(views)) {
3131
+ problems.push(`views must be an array, got ${describeType2(views)}.`);
3132
+ } else if (views.length > MAX_WIDGET_VIEWS) {
3133
+ problems.push(
3134
+ `views lists ${views.length} views; the most a widget may declare is ${MAX_WIDGET_VIEWS}.`
3135
+ );
3136
+ } else {
3137
+ views.forEach((view, index) => {
3138
+ const path = `views[${index}]`;
3139
+ if (!isRecord3(view)) {
3140
+ problems.push(
3141
+ `${path} must be an object, got ${describeType2(view)}.`
3142
+ );
3143
+ return;
3144
+ }
3145
+ const key = view["key"];
3146
+ if (typeof key !== "string" || key.trim() === "") {
3147
+ problems.push(`${path}.key must be a non-empty string.`);
3148
+ } else if (declared.has(key)) {
3149
+ problems.push(
3150
+ key === BASE_VIEW_KEY ? `${path}.key "${BASE_VIEW_KEY}" is reserved for the widget's own kind.` : `${path}.key "${key}" is used twice.`
3151
+ );
3152
+ } else {
3153
+ declared.add(key);
3154
+ }
3155
+ if (typeof view["label"] !== "string" || view["label"].trim() === "") {
3156
+ problems.push(`${path}.label must be a non-empty string.`);
3157
+ }
3158
+ requireOneOf(view, "kind", WIDGET_KINDS, path, problems);
3159
+ if (view["convert"] === void 0) {
3160
+ problems.push(
3161
+ `${path}.convert is required: views supplied by a provider are not supported yet.`
3162
+ );
3163
+ } else {
3164
+ checkConversion(
3165
+ view["convert"],
3166
+ `${path}.convert`,
3167
+ value["kind"],
3168
+ view["kind"],
3169
+ problems
3170
+ );
3171
+ }
3172
+ });
3173
+ }
3174
+ }
3175
+ if (defaultView !== void 0) {
3176
+ if (typeof defaultView !== "string" || !declared.has(defaultView)) {
3177
+ problems.push(
3178
+ `defaultView must be "${BASE_VIEW_KEY}" or the key of a declared view, got ${typeof defaultView === "string" ? JSON.stringify(defaultView) : describeType2(defaultView)}.`
3179
+ );
3180
+ }
3181
+ }
2623
3182
  }
2624
- function titleOf(item) {
2625
- return item.title ?? item.key;
3183
+ function formatProblems(prefix, problems) {
3184
+ const shown = problems.slice(0, MAX_REPORTED_PROBLEMS);
3185
+ const more = problems.length - shown.length;
3186
+ return `${prefix}: ${shown.join(" ")}${more > 0 ? ` (and ${more} more problem(s))` : ""}`;
2626
3187
  }
2627
- function checkTitle(layout, title, exceptKey) {
2628
- const trimmed = title.trim();
2629
- if (trimmed === "") return err("A page title must not be empty.");
2630
- if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
3188
+ function validateWidgetData(value, options = {}) {
3189
+ const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
3190
+ if (!isRecord3(value)) {
3191
+ return err(`${subject}: must be an object, got ${describeType2(value)}.`);
3192
+ }
3193
+ if (value["empty"] === true) {
3194
+ if (typeof value["text"] !== "string") {
3195
+ return err(
3196
+ `${subject} (empty): text must be a string, got ${describeType2(value["text"])}.`
3197
+ );
3198
+ }
3199
+ return ok(value);
3200
+ }
3201
+ const kind = value["kind"];
3202
+ if (!isWidgetKind(kind)) {
2631
3203
  return err(
2632
- `Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
3204
+ `${subject}: kind must be one of ${WIDGET_KINDS.join(", ")} (or the payload must be {"empty": true, "text": ...}), got ${typeof kind === "string" ? JSON.stringify(kind) : describeType2(kind)}.`
2633
3205
  );
2634
3206
  }
2635
- const wanted = normalizedTitle(trimmed);
2636
- const clash = pageList(layout).find(
2637
- (p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
2638
- );
2639
- if (clash !== void 0) {
2640
- return err(`Another page is already called "${clash.title}".`);
3207
+ if (options.expectedKind !== void 0 && kind !== options.expectedKind) {
3208
+ return err(
3209
+ `${subject}: expected kind ${options.expectedKind} from its definition, got ${kind}.`
3210
+ );
2641
3211
  }
2642
- return ok(trimmed);
2643
- }
2644
- function nextPageKey(layout) {
2645
- const keys = new Set(pageList(layout).map((p) => p.key));
2646
- let n = 1;
2647
- while (keys.has(`page-${n}`)) n += 1;
2648
- return `page-${n}`;
2649
- }
2650
- function nextPageTitle(layout) {
2651
- const used = new Set(
2652
- pageList(layout).map((p) => normalizedTitle(p.title))
2653
- );
2654
- let n = pageList(layout).length + 1;
2655
- while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
2656
- return `Page ${n}`;
3212
+ const problems = [];
3213
+ checkPayloadFields(kind, value, problems);
3214
+ if (problems.length > 0) {
3215
+ return err(formatProblems(`${subject} (${kind})`, problems));
3216
+ }
3217
+ return ok(value);
2657
3218
  }
2658
- function addPage(layout, options = {}) {
2659
- if (pageList(layout).length >= MAX_PARSED_PAGES) {
3219
+ function validateWidgetDefinition(value) {
3220
+ if (!isRecord3(value)) {
2660
3221
  return err(
2661
- `The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
3222
+ `Widget definition: must be an object, got ${describeType2(value)}.`
2662
3223
  );
2663
3224
  }
2664
- const key = options.key ?? nextPageKey(layout);
2665
- if (pageList(layout).some((p) => p.key === key)) {
2666
- return err(`A page with the key "${key}" already exists.`);
3225
+ const subject = typeof value["key"] === "string" ? `Widget definition ${JSON.stringify(value["key"])}` : "Widget definition";
3226
+ const problems = [];
3227
+ requireString(value, "key", "", problems);
3228
+ if (value["key"] === "") {
3229
+ problems.push("key must not be empty.");
2667
3230
  }
2668
- const title = checkTitle(
2669
- layout,
2670
- options.title ?? nextPageTitle(layout)
2671
- );
2672
- if (!title.ok) return title;
2673
- const base = withExplicitPages(layout);
2674
- const page = {
2675
- key,
2676
- title: title.value,
2677
- order: [],
2678
- hidden: [],
2679
- minimized: []
2680
- };
2681
- return ok({ ...base, pages: [...base.pages ?? [], page] });
3231
+ requireString(value, "title", "", problems);
3232
+ optionalString(value, "description", "", problems);
3233
+ requireOneOf(value, "kind", WIDGET_KINDS, "", problems);
3234
+ optionalNumber(value, "sortOrder", "", problems);
3235
+ if (value["roles"] !== void 0) {
3236
+ const roles = requireArray(value, "roles", "", problems);
3237
+ roles?.forEach((role, index) => {
3238
+ if (typeof role !== "string") {
3239
+ problems.push(
3240
+ `roles[${index}] must be a string, got ${describeType2(role)}.`
3241
+ );
3242
+ }
3243
+ });
3244
+ }
3245
+ optionalBoolean(value, "active", "", problems);
3246
+ if (value["defaultSize"] !== void 0) {
3247
+ requireOneOf(value, "defaultSize", WIDGET_SIZES, "", problems);
3248
+ }
3249
+ if (value["width"] !== void 0 && !isWidgetWidth(value["width"])) {
3250
+ problems.push(
3251
+ `width must be an integer from ${MIN_WIDGET_WIDTH} to ${MAX_WIDGET_WIDTH} (twelfths of the row), got ${typeof value["width"] === "number" ? String(value["width"]) : describeType2(value["width"])}.`
3252
+ );
3253
+ }
3254
+ checkWidthLimits(value, problems);
3255
+ checkDetailOption(value["detail"], problems);
3256
+ checkTableControlsOption(value["tableControls"], value["kind"], problems);
3257
+ checkLockedOption(value["locked"], problems);
3258
+ checkOptions(value["options"], value["kind"], problems);
3259
+ checkViews(value, problems);
3260
+ if (value["page"] !== void 0) {
3261
+ if (typeof value["page"] !== "string" || value["page"].trim() === "") {
3262
+ problems.push(
3263
+ `page must be a non-empty page key or title, got ${describeType2(value["page"])}.`
3264
+ );
3265
+ }
3266
+ }
3267
+ if (value["fill"] !== void 0) {
3268
+ requireOneOf(value, "fill", WIDGET_FILLS, "", problems);
3269
+ }
3270
+ if (problems.length > 0) {
3271
+ return err(formatProblems(subject, problems));
3272
+ }
3273
+ return ok(value);
3274
+ }
3275
+
3276
+ // src/core/resolve.ts
3277
+ function resolvePayload(definition, payload, options = {}) {
3278
+ let checked;
3279
+ if (options.validate === false) {
3280
+ checked = payload;
3281
+ } else {
3282
+ const result = validateWidgetData(payload, {
3283
+ widgetKey: definition.key,
3284
+ expectedKind: definition.kind
3285
+ });
3286
+ if (!result.ok) {
3287
+ return { definition, status: "error", error: result.error };
3288
+ }
3289
+ checked = result.value;
3290
+ }
3291
+ const emptyText = emptyTextFor(checked);
3292
+ if (emptyText !== null) {
3293
+ return { definition, status: "empty", emptyText };
3294
+ }
3295
+ return { definition, status: "ok", data: checked };
2682
3296
  }
2683
- function renamePage(layout, pageKey, title) {
2684
- const page = pageList(layout).find(
2685
- (p) => p.key === pageKey
2686
- );
2687
- if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2688
- const checked = checkTitle(layout, title, pageKey);
2689
- if (!checked.ok) return checked;
2690
- if (checked.value === page.title) return ok(layout);
2691
- const base = withExplicitPages(layout);
2692
- return ok({
2693
- ...base,
2694
- pages: (base.pages ?? []).map(
2695
- (p) => p.key === pageKey ? { ...p, title: checked.value } : p
2696
- )
2697
- });
3297
+ function failedWidget(definition, error) {
3298
+ return { definition, status: "error", error };
2698
3299
  }
2699
- function movePage(layout, pageKey, offset) {
2700
- const pages = layout.pages ?? [];
2701
- const from = pages.findIndex((p) => p.key === pageKey);
2702
- if (from === -1) return layout;
2703
- const to = Math.max(
2704
- 0,
2705
- Math.min(pages.length - 1, from + Math.trunc(offset))
2706
- );
2707
- if (to === from) return layout;
2708
- const next = [...pages];
2709
- const [moved] = next.splice(from, 1);
2710
- next.splice(to, 0, moved);
2711
- return { ...layout, pages: next };
3300
+ function loadingWidgets(definitions) {
3301
+ return definitions.map((definition) => ({ definition, status: "loading" }));
2712
3302
  }
2713
- function overflowing(definitions, layout, options) {
2714
- return pageList(layout).find(
2715
- (page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
2716
- );
3303
+ function cacheKeyFor(definition, options, values) {
3304
+ const base = options.cacheKey === void 0 ? definition.sourceKey ?? definition.key : options.cacheKey(definition);
3305
+ return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
2717
3306
  }
2718
- function removePage(definitions, layout, pageKey, options = {}) {
2719
- const pages = pageList(layout);
2720
- const index = pages.findIndex((p) => p.key === pageKey);
2721
- const page = pages[index];
2722
- if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2723
- if (pages.length === 1) {
2724
- return err(
2725
- `${describePage(page)} is the only page, so it can't be removed.`
2726
- );
2727
- }
2728
- const target = pages[index > 0 ? index - 1 : 1];
2729
- const merged = {
2730
- ...layout,
2731
- pages: pages.filter((p) => p.key !== pageKey).map(
2732
- (p) => p.key === target.key ? {
2733
- ...p,
2734
- order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
2735
- hidden: [...p.hidden, ...page.hidden],
2736
- minimized: [...p.minimized, ...page.minimized]
2737
- } : p
2738
- )
2739
- };
2740
- const full = overflowing(
2741
- definitions,
2742
- merged,
3307
+ function optionValuesFor(definition, options) {
3308
+ return resolveOptionValues(
3309
+ definition,
3310
+ options.optionValues?.[definition.key],
2743
3311
  options
2744
3312
  );
2745
- if (full !== void 0) {
2746
- return err(
2747
- `${describePage(page)} has widgets that don't fit on the other pages (${describePage(full)} would need more than ${rowsAllowed(full, options)} rows). Move or hide some first.`
3313
+ }
3314
+ function finalizeWidget(widget, values) {
3315
+ if (Object.keys(values).length === 0) return widget;
3316
+ if (widget.status === "ok") {
3317
+ const data = applyClientOptions(
3318
+ widget.definition,
3319
+ widget.data,
3320
+ values
2748
3321
  );
3322
+ return { ...widget, data, options: values };
2749
3323
  }
2750
- return ok(merged);
3324
+ return { ...widget, options: values };
2751
3325
  }
2752
- function without(keys, key) {
2753
- return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
3326
+ var LoadInterruption = class extends Error {
3327
+ };
3328
+ async function resolveWidget(definition, provider, context, options = {}) {
3329
+ const values = optionValuesFor(definition, options);
3330
+ return finalizeWidget(
3331
+ await resolveWidgetUnapplied(definition, provider, context, options),
3332
+ values
3333
+ );
2754
3334
  }
2755
- function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
2756
- const widget = definitions.find((d) => d.key === widgetKey);
2757
- if (widget === void 0) {
2758
- return err(
2759
- `Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
3335
+ async function resolveWidgetUnapplied(definition, provider, context, options = {}) {
3336
+ const values = optionValuesFor(definition, options);
3337
+ const quotedKey = JSON.stringify(definition.key);
3338
+ if (provider === void 0) {
3339
+ return failedWidget(
3340
+ definition,
3341
+ `Widget ${quotedKey}: no provider is registered for this key.`
2760
3342
  );
2761
3343
  }
2762
- const pages = pageList(layout);
2763
- const target = pages.find((p) => p.key === targetKey);
2764
- if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
2765
- const sourceKey = pageOf(definitions, layout, widgetKey);
2766
- if (sourceKey === targetKey) return ok(layout);
2767
- if (resolveWidgetLock(widget).move) {
2768
- return err(
2769
- `Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
3344
+ if (options.signal?.aborted === true) {
3345
+ return failedWidget(
3346
+ definition,
3347
+ `Widget ${quotedKey}: loading was canceled before it started.`
2770
3348
  );
2771
3349
  }
2772
- const base = withExplicitPages(layout);
2773
- const source = (base.pages ?? []).find(
2774
- (p) => p.key === sourceKey
2775
- );
2776
- const hidden = source.hidden.includes(widgetKey);
2777
- const minimized = source.minimized.includes(widgetKey);
2778
- const targetOrder = pageWidgets(
2779
- definitions,
2780
- base,
2781
- targetKey
2782
- ).map((d) => d.key);
2783
- const moved = {
2784
- ...base,
2785
- pages: (base.pages ?? []).map((p) => {
2786
- if (p.key === sourceKey) {
2787
- return {
2788
- ...p,
2789
- order: without(p.order, widgetKey),
2790
- hidden: without(p.hidden, widgetKey),
2791
- minimized: without(p.minimized, widgetKey)
2792
- };
2793
- }
2794
- if (p.key === targetKey) {
2795
- return {
2796
- ...p,
2797
- order: hidden ? p.order : [...targetOrder, widgetKey],
2798
- hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
2799
- minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
2800
- };
2801
- }
2802
- return p;
2803
- })
2804
- };
2805
- if (!hidden) {
2806
- const allowed = rowsAllowed(target, options);
2807
- const needed = placeRows(
2808
- pageWidgets(definitions, moved, targetKey)
2809
- ).length;
2810
- if (needed > allowed) {
2811
- return err(
2812
- `${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
3350
+ const controller = new AbortController();
3351
+ const onExternalAbort = () => controller.abort(options.signal?.reason);
3352
+ options.signal?.addEventListener("abort", onExternalAbort, { once: true });
3353
+ let timedOut = false;
3354
+ const timeoutMs = options.timeoutMs !== void 0 && options.timeoutMs > 0 ? options.timeoutMs : void 0;
3355
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
3356
+ timedOut = true;
3357
+ controller.abort(new Error(`Timed out after ${timeoutMs} ms.`));
3358
+ }, timeoutMs);
3359
+ const interrupted = new Promise((_resolve, reject) => {
3360
+ controller.signal.addEventListener(
3361
+ "abort",
3362
+ () => reject(new LoadInterruption()),
3363
+ { once: true }
3364
+ );
3365
+ });
3366
+ let payload;
3367
+ try {
3368
+ payload = await Promise.race([
3369
+ Promise.resolve().then(() => {
3370
+ if (controller.signal.aborted) throw new LoadInterruption();
3371
+ return provider(context, definition, {
3372
+ signal: controller.signal,
3373
+ options: values
3374
+ });
3375
+ }),
3376
+ interrupted
3377
+ ]);
3378
+ } catch (cause) {
3379
+ if (cause instanceof LoadInterruption) {
3380
+ return failedWidget(
3381
+ definition,
3382
+ timedOut ? `Widget ${quotedKey}: provider timed out after ${timeoutMs} ms.` : `Widget ${quotedKey}: loading was canceled.`
2813
3383
  );
2814
3384
  }
3385
+ return failedWidget(
3386
+ definition,
3387
+ `Widget ${quotedKey}: provider failed: ${describeUnknownError(cause)}`
3388
+ );
3389
+ } finally {
3390
+ if (timer !== void 0) clearTimeout(timer);
3391
+ options.signal?.removeEventListener("abort", onExternalAbort);
2815
3392
  }
2816
- return ok(moved);
2817
- }
2818
- function withoutLockField(definition) {
2819
- if (definition.locked === void 0) return definition;
2820
- const copy = { ...definition };
2821
- delete copy.locked;
2822
- return copy;
2823
- }
2824
- function uniqueTitle2(taken, title, index) {
2825
- let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
2826
- if (base === "") base = `Page ${index + 1}`;
2827
- let candidate = base;
2828
- let n = 1;
2829
- while (taken.has(normalizedTitle(candidate))) {
2830
- const suffix = ` (#${n})`;
2831
- candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
2832
- n += 1;
3393
+ const resolved = resolvePayload(definition, payload, options);
3394
+ if (resolved.status !== "ok") return resolved;
3395
+ const storedAt = Date.now();
3396
+ if (options.cache !== void 0) {
3397
+ try {
3398
+ await options.cache.set(cacheKeyFor(definition, options, values), {
3399
+ payload: resolved.data,
3400
+ storedAt
3401
+ });
3402
+ } catch {
3403
+ }
2833
3404
  }
2834
- taken.add(normalizedTitle(candidate));
2835
- return candidate;
3405
+ return { ...resolved, updatedAt: storedAt };
2836
3406
  }
2837
- function samePages(a, b) {
2838
- return a.length === b.length && a.every((p, i) => {
2839
- const q = b[i];
2840
- return p.key === q.key && p.title === q.title && p.maxRows === q.maxRows && sameKeys(p.order, q.order) && sameKeys(p.hidden, q.hidden) && sameKeys(p.minimized, q.minimized);
2841
- });
3407
+ function widgetsFor(definitions, roles) {
3408
+ return sortDefinitions(
3409
+ definitions.filter((definition) => isWidgetVisibleTo(definition, roles))
3410
+ );
2842
3411
  }
2843
- function normalizeLayout(definitions, layout, options = {}) {
2844
- const known = pruneLayout(definitions, layout);
2845
- const enforced = enforceLocks(definitions, known, options);
2846
- const locked = normalizeSettings(
2847
- definitions,
2848
- enforced,
2849
- options
3412
+ async function resolveWidgets(definitions, providers, context, options = {}) {
3413
+ const visible = widgetsFor(definitions, context.roles);
3414
+ return Promise.all(
3415
+ visible.map(
3416
+ (definition) => resolveWidget(
3417
+ definition,
3418
+ Object.prototype.hasOwnProperty.call(
3419
+ providers,
3420
+ definition.sourceKey ?? definition.key
3421
+ ) ? providers[definition.sourceKey ?? definition.key] : void 0,
3422
+ context,
3423
+ options
3424
+ )
3425
+ )
2850
3426
  );
2851
- if (!hasPages(locked)) return locked === layout ? layout : locked;
2852
- const defs = effectiveDefinitions(
2853
- options.overrideLocks === true ? definitions.map(withoutLockField) : definitions,
2854
- locked,
2855
- options
3427
+ }
3428
+
3429
+ // src/core/loader.ts
3430
+ function createWidgetLoader(options) {
3431
+ const { definitions, providers, context, ...baseOptions } = options;
3432
+ const chosen = new Map(
3433
+ Object.entries(baseOptions.optionValues ?? {}).flatMap(
3434
+ ([key, values]) => values === void 0 ? [] : [[key, values]]
3435
+ )
2856
3436
  );
2857
- const taken = /* @__PURE__ */ new Set();
2858
- const seenKeys = /* @__PURE__ */ new Set();
2859
- const held = /* @__PURE__ */ new Set();
2860
- let pages = [];
2861
- (locked.pages ?? []).forEach((page, index) => {
2862
- if (seenKeys.has(page.key)) return;
2863
- seenKeys.add(page.key);
2864
- const keep = (list) => [
2865
- ...new Set(list.filter((k) => !held.has(k)))
2866
- ];
2867
- const next = {
2868
- ...page,
2869
- title: uniqueTitle2(taken, page.title, index),
2870
- order: keep(page.order),
2871
- hidden: keep(page.hidden),
2872
- minimized: keep(page.minimized)
2873
- };
2874
- for (const k of [...next.order, ...next.hidden]) held.add(k);
2875
- pages.push(next);
2876
- });
2877
- const work = () => ({ ...locked, pages });
2878
- const pinned = defs.filter((d) => resolveWidgetLock(d).move);
2879
- for (const widget of pinned) {
2880
- const home = homePageKey(work(), widget);
2881
- const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
2882
- const wasHidden = pages.some((p) => keysHere(p, "hidden"));
2883
- const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
2884
- if (!wasHidden && !wasMinimized) continue;
2885
- pages = pages.map((p) => {
2886
- let next = p;
2887
- if (keysHere(p, "hidden")) {
2888
- next = { ...next, hidden: without(next.hidden, widget.key) };
2889
- }
2890
- if (keysHere(p, "minimized")) {
2891
- next = { ...next, minimized: without(next.minimized, widget.key) };
3437
+ const resolveOptions = baseOptions;
3438
+ function currentOptions() {
3439
+ return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
3440
+ }
3441
+ const unapplied = /* @__PURE__ */ new Map();
3442
+ const visible = widgetsFor(definitions, context.roles);
3443
+ let snapshot = loadingWidgets(visible);
3444
+ const listeners = /* @__PURE__ */ new Set();
3445
+ const started = /* @__PURE__ */ new Set();
3446
+ const generations = /* @__PURE__ */ new Map();
3447
+ const inFlight = /* @__PURE__ */ new Map();
3448
+ let disposed = false;
3449
+ function publish(next) {
3450
+ snapshot = next;
3451
+ for (const listener of [...listeners]) {
3452
+ try {
3453
+ listener();
3454
+ } catch {
2892
3455
  }
2893
- if (p.key === home) {
2894
- next = {
2895
- ...next,
2896
- hidden: wasHidden && !next.hidden.includes(widget.key) ? [...next.hidden, widget.key] : next.hidden,
2897
- minimized: wasMinimized && !next.minimized.includes(widget.key) ? [...next.minimized, widget.key] : next.minimized
2898
- };
3456
+ }
3457
+ }
3458
+ function replace(key, widget) {
3459
+ publish(
3460
+ snapshot.map(
3461
+ (current) => current.definition.key === key ? widget : current
3462
+ )
3463
+ );
3464
+ }
3465
+ function abortAll(reason) {
3466
+ for (const controller of inFlight.values()) controller.abort(reason);
3467
+ inFlight.clear();
3468
+ }
3469
+ resolveOptions.signal?.addEventListener(
3470
+ "abort",
3471
+ () => abortAll(resolveOptions.signal?.reason),
3472
+ { once: true }
3473
+ );
3474
+ async function run(definition, readCache) {
3475
+ const key = definition.key;
3476
+ const generation = (generations.get(key) ?? 0) + 1;
3477
+ generations.set(key, generation);
3478
+ inFlight.get(key)?.abort(new Error("Superseded by a newer load."));
3479
+ const controller = new AbortController();
3480
+ inFlight.set(key, controller);
3481
+ const isCurrent = () => !disposed && generations.get(key) === generation;
3482
+ const runOptions = currentOptions();
3483
+ const values = optionValuesFor(definition, runOptions);
3484
+ if (readCache && runOptions.cache !== void 0) {
3485
+ try {
3486
+ const entry = await runOptions.cache.get(
3487
+ cacheKeyFor(definition, runOptions, values)
3488
+ );
3489
+ if (entry !== void 0 && isCurrent()) {
3490
+ const cached = resolvePayload(definition, entry.payload, runOptions);
3491
+ if (cached.status === "ok") {
3492
+ const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
3493
+ unapplied.set(key, stale);
3494
+ replace(key, finalizeWidget(stale, values));
3495
+ }
3496
+ }
3497
+ } catch {
2899
3498
  }
2900
- return next;
3499
+ }
3500
+ if (!isCurrent()) return;
3501
+ const providerKey = definition.sourceKey ?? key;
3502
+ const provider = Object.prototype.hasOwnProperty.call(
3503
+ providers,
3504
+ providerKey
3505
+ ) ? providers[providerKey] : void 0;
3506
+ const result = await resolveWidgetUnapplied(definition, provider, context, {
3507
+ ...runOptions,
3508
+ signal: controller.signal
2901
3509
  });
3510
+ if (!isCurrent()) return;
3511
+ inFlight.delete(key);
3512
+ if (result.status === "ok") unapplied.set(key, result);
3513
+ else unapplied.delete(key);
3514
+ replace(key, finalizeWidget(result, values));
2902
3515
  }
2903
- const placedKeys = /* @__PURE__ */ new Set();
2904
- for (const p of pages) {
2905
- for (const k of [...p.order, ...p.hidden]) placedKeys.add(k);
3516
+ function start(definition, readCache) {
3517
+ started.add(definition.key);
3518
+ void run(definition, readCache);
2906
3519
  }
2907
- const pinnedKeys = new Set(pinned.map((d) => d.key));
2908
- const unplaced = defs.filter((d) => !placedKeys.has(d.key) && !pinnedKeys.has(d.key)).map((definition, index) => ({ definition, index })).sort(
2909
- (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
2910
- ).map(({ definition }) => definition);
2911
- for (const widget of unplaced) {
2912
- const home = homePageKey(work(), widget);
2913
- pages = pages.map(
2914
- (p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
2915
- );
3520
+ function definitionFor(key) {
3521
+ return visible.find((definition) => definition.key === key);
2916
3522
  }
2917
- for (let i = 0; i < pages.length; i += 1) {
2918
- const page = pages[i];
2919
- const shown = pageWidgets(defs, work(), page.key);
2920
- const fits = fitCount(shown, rowsAllowed(page, options));
2921
- if (fits >= shown.length) continue;
2922
- const moving = shown.slice(fits).filter((d) => !pinnedKeys.has(d.key)).map((d) => d.key);
2923
- if (moving.length === 0) continue;
2924
- const moveSet = new Set(moving);
2925
- const movedMinimized = page.minimized.filter(
2926
- (k) => moveSet.has(k)
2927
- );
2928
- pages = pages.map(
2929
- (p) => p.key === page.key ? {
2930
- ...p,
2931
- order: p.order.filter((k) => !moveSet.has(k)),
2932
- minimized: p.minimized.filter((k) => !moveSet.has(k))
2933
- } : p
2934
- );
2935
- let placed = false;
2936
- for (let j = i + 1; j < pages.length && !placed; j += 1) {
2937
- const candidate = pages[j];
2938
- const trial = pages.map(
2939
- (p) => p.key === candidate.key ? {
2940
- ...p,
2941
- order: [...moving, ...p.order],
2942
- minimized: [...p.minimized, ...movedMinimized]
2943
- } : p
2944
- );
2945
- const items = pageWidgets(
2946
- defs,
2947
- { ...locked, pages: trial },
2948
- candidate.key
3523
+ function load(key) {
3524
+ if (disposed || started.has(key)) return;
3525
+ const definition = definitionFor(key);
3526
+ if (definition !== void 0) start(definition, true);
3527
+ }
3528
+ function loadAll() {
3529
+ for (const definition of visible) load(definition.key);
3530
+ }
3531
+ function refresh(key, refreshOptions) {
3532
+ if (disposed) return;
3533
+ const keys = key === void 0 ? [...started] : [key];
3534
+ for (const target of keys) {
3535
+ const definition = definitionFor(target);
3536
+ if (definition === void 0) continue;
3537
+ if (refreshOptions?.skipInFlight === true && inFlight.has(target)) {
3538
+ continue;
3539
+ }
3540
+ const current = snapshot.find(
3541
+ (widget) => widget.definition.key === target
2949
3542
  );
2950
- if (placeRows(items).length <= rowsAllowed(candidate, options)) {
2951
- pages = trial;
2952
- placed = true;
3543
+ if (current?.status === "error") {
3544
+ replace(target, { definition, status: "loading" });
2953
3545
  }
3546
+ start(definition, false);
2954
3547
  }
2955
- if (!placed) {
2956
- const key = nextPageKey(work());
2957
- const title = uniqueTitle2(
2958
- new Set(pages.map((p) => normalizedTitle(p.title))),
2959
- `Page ${pages.length + 1}`,
2960
- pages.length
2961
- );
2962
- pages = [
2963
- ...pages,
2964
- { key, title, order: moving, hidden: [], minimized: movedMinimized }
2965
- ];
3548
+ }
3549
+ function setOptions(key, values) {
3550
+ if (disposed) return;
3551
+ const definition = definitionFor(key);
3552
+ if (definition === void 0) return;
3553
+ const before = optionValuesFor(definition, currentOptions());
3554
+ chosen.set(key, { ...values });
3555
+ const runOptions = currentOptions();
3556
+ const after = optionValuesFor(definition, runOptions);
3557
+ if (sameOptionValues(before, after) || !started.has(key)) return;
3558
+ const kept = unapplied.get(key);
3559
+ if (kept !== void 0 && optionsCacheSuffix(definition, before, runOptions) === optionsCacheSuffix(definition, after, runOptions)) {
3560
+ replace(key, finalizeWidget(kept, after));
3561
+ return;
3562
+ }
3563
+ const current = snapshot.find(
3564
+ (widget) => widget.definition.key === key
3565
+ );
3566
+ if (current?.status === "ok") replace(key, { ...current, stale: true });
3567
+ else if (current?.status === "error") {
3568
+ replace(key, { definition, status: "loading" });
2966
3569
  }
3570
+ start(definition, true);
2967
3571
  }
2968
- const result = work();
2969
- return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) && locked.settings === layout.settings ? layout : result;
3572
+ function dispose() {
3573
+ disposed = true;
3574
+ abortAll(new Error("The widget loader was disposed."));
3575
+ listeners.clear();
3576
+ }
3577
+ function subscribe(listener) {
3578
+ listeners.add(listener);
3579
+ return () => {
3580
+ listeners.delete(listener);
3581
+ };
3582
+ }
3583
+ return {
3584
+ getSnapshot: () => snapshot,
3585
+ subscribe,
3586
+ load,
3587
+ loadAll,
3588
+ refresh,
3589
+ setOptions,
3590
+ dispose
3591
+ };
2970
3592
  }
2971
3593
 
2972
3594
  // src/core/scale.ts
@@ -3798,6 +4420,7 @@ function indent(block) {
3798
4420
  return block.split("\n").map((line) => ` ${line}`).join("\n");
3799
4421
  }
3800
4422
 
4423
+ exports.BASE_VIEW_KEY = BASE_VIEW_KEY;
3801
4424
  exports.DATE_RANGE_PRESETS = DATE_RANGE_PRESETS;
3802
4425
  exports.DEFAULT_COMPARISON_LABEL = DEFAULT_COMPARISON_LABEL;
3803
4426
  exports.DEFAULT_CURRENCY = DEFAULT_CURRENCY;
@@ -3813,17 +4436,20 @@ exports.MAX_DETAIL_PAGE_SIZE = MAX_DETAIL_PAGE_SIZE;
3813
4436
  exports.MAX_GRAPH_SERIES = MAX_GRAPH_SERIES;
3814
4437
  exports.MAX_PAGE_ROWS = MAX_PAGE_ROWS;
3815
4438
  exports.MAX_PAGE_TITLE_LENGTH = MAX_PAGE_TITLE_LENGTH;
4439
+ exports.MAX_PARSED_CLONES = MAX_PARSED_CLONES;
3816
4440
  exports.MAX_PARSED_PAGES = MAX_PARSED_PAGES;
3817
4441
  exports.MAX_PARSED_SETTINGS = MAX_PARSED_SETTINGS;
3818
4442
  exports.MAX_REPORTED_PROBLEMS = MAX_REPORTED_PROBLEMS;
3819
4443
  exports.MAX_TEXT_OPTION_LENGTH = MAX_TEXT_OPTION_LENGTH;
3820
4444
  exports.MAX_WIDGET_OPTIONS = MAX_WIDGET_OPTIONS;
3821
4445
  exports.MAX_WIDGET_TITLE_LENGTH = MAX_WIDGET_TITLE_LENGTH;
4446
+ exports.MAX_WIDGET_VIEWS = MAX_WIDGET_VIEWS;
3822
4447
  exports.MAX_WIDGET_WIDTH = MAX_WIDGET_WIDTH;
3823
4448
  exports.MIN_WIDGET_WIDTH = MIN_WIDGET_WIDTH;
3824
4449
  exports.OPTION_KEY_PATTERN = OPTION_KEY_PATTERN;
3825
4450
  exports.THEME_TOKEN_VARIABLES = THEME_TOKEN_VARIABLES;
3826
4451
  exports.VALUE_FORMATS = VALUE_FORMATS;
4452
+ exports.VIEW_CONVERSIONS = VIEW_CONVERSIONS;
3827
4453
  exports.WIDGET_FILLS = WIDGET_FILLS;
3828
4454
  exports.WIDGET_KINDS = WIDGET_KINDS;
3829
4455
  exports.WIDGET_OPTION_TYPES = WIDGET_OPTION_TYPES;
@@ -3832,20 +4458,28 @@ exports.WIDTH_COLUMNS = WIDTH_COLUMNS;
3832
4458
  exports.addPage = addPage;
3833
4459
  exports.applyClientOptions = applyClientOptions;
3834
4460
  exports.applySettings = applySettings;
4461
+ exports.applyView = applyView;
3835
4462
  exports.assignPages = assignPages;
3836
4463
  exports.baseColumnSpan = baseColumnSpan;
3837
4464
  exports.cacheKeyFor = cacheKeyFor;
4465
+ exports.canDuplicate = canDuplicate;
4466
+ exports.cleanClones = cleanClones;
3838
4467
  exports.clearWidgetSettings = clearWidgetSettings;
4468
+ exports.cloneNumber = cloneNumber;
3839
4469
  exports.coerceOptionValue = coerceOptionValue;
4470
+ exports.convertView = convertView;
3840
4471
  exports.createLayoutPersistence = createLayoutPersistence;
3841
4472
  exports.createTheme = createTheme;
3842
4473
  exports.createWidgetLoader = createWidgetLoader;
3843
4474
  exports.defaultDetailQuery = defaultDetailQuery;
3844
4475
  exports.defaultOptionValue = defaultOptionValue;
3845
4476
  exports.defineWidget = defineWidget;
4477
+ exports.deleteClone = deleteClone;
3846
4478
  exports.deriveDetailData = deriveDetailData;
3847
4479
  exports.describeUnknownError = describeUnknownError;
4480
+ exports.duplicateWidget = duplicateWidget;
3848
4481
  exports.effectiveDefinitions = effectiveDefinitions;
4482
+ exports.effectiveViewKey = effectiveViewKey;
3849
4483
  exports.emptyTextFor = emptyTextFor;
3850
4484
  exports.emptyWidget = emptyWidget;
3851
4485
  exports.enforceLocks = enforceLocks;
@@ -3856,12 +4490,14 @@ exports.fillWidthSpans = fillWidthSpans;
3856
4490
  exports.fillsHeight = fillsHeight;
3857
4491
  exports.fillsWidth = fillsWidth;
3858
4492
  exports.finalizeWidget = finalizeWidget;
4493
+ exports.findView = findView;
3859
4494
  exports.fitCount = fitCount;
3860
4495
  exports.formatCount = formatCount;
3861
4496
  exports.formatPercentChange = formatPercentChange;
3862
4497
  exports.formatValue = formatValue;
3863
4498
  exports.fromLegacyDefinition = fromLegacyDefinition;
3864
4499
  exports.hasPages = hasPages;
4500
+ exports.hasViews = hasViews;
3865
4501
  exports.hiddenWidgets = hiddenWidgets;
3866
4502
  exports.hideWidget = hideWidget;
3867
4503
  exports.isClientApplied = isClientApplied;
@@ -3879,6 +4515,7 @@ exports.isWidgetKind = isWidgetKind;
3879
4515
  exports.isWidgetVisibleTo = isWidgetVisibleTo;
3880
4516
  exports.isWidgetWidth = isWidgetWidth;
3881
4517
  exports.itemWidth = itemWidth;
4518
+ exports.kindLabel = kindLabel;
3882
4519
  exports.kpiDelta = kpiDelta;
3883
4520
  exports.labelStride = labelStride;
3884
4521
  exports.loadingWidgets = loadingWidgets;
@@ -3890,6 +4527,7 @@ exports.moveWidgetBy = moveWidgetBy;
3890
4527
  exports.moveWidgetDown = moveWidgetDown;
3891
4528
  exports.moveWidgetToPage = moveWidgetToPage;
3892
4529
  exports.moveWidgetUp = moveWidgetUp;
4530
+ exports.nextCloneKey = nextCloneKey;
3893
4531
  exports.nextPageKey = nextPageKey;
3894
4532
  exports.niceDomain = niceDomain;
3895
4533
  exports.niceStep = niceStep;
@@ -3905,6 +4543,7 @@ exports.pageList = pageList;
3905
4543
  exports.pageOf = pageOf;
3906
4544
  exports.pageRoom = pageRoom;
3907
4545
  exports.pageWidgets = pageWidgets;
4546
+ exports.parseClones = parseClones;
3908
4547
  exports.parseDetailQuery = parseDetailQuery;
3909
4548
  exports.parseLayout = parseLayout;
3910
4549
  exports.parseSettings = parseSettings;
@@ -3928,6 +4567,7 @@ exports.resolveWidgetUnapplied = resolveWidgetUnapplied;
3928
4567
  exports.resolveWidgets = resolveWidgets;
3929
4568
  exports.restoreWidget = restoreWidget;
3930
4569
  exports.sameOptionValues = sameOptionValues;
4570
+ exports.serializeClones = serializeClones;
3931
4571
  exports.serializeDetailQuery = serializeDetailQuery;
3932
4572
  exports.serializeLayout = serializeLayout;
3933
4573
  exports.serializeSettings = serializeSettings;
@@ -3936,6 +4576,7 @@ exports.shareOf = shareOf;
3936
4576
  exports.showWidget = showWidget;
3937
4577
  exports.sortDefinitions = sortDefinitions;
3938
4578
  exports.sortFromOptions = sortFromOptions;
4579
+ exports.suggestedViewTitle = suggestedViewTitle;
3939
4580
  exports.tableDetailData = tableDetailData;
3940
4581
  exports.toggleHidden = toggleHidden;
3941
4582
  exports.toggleMinimized = toggleMinimized;
@@ -3945,12 +4586,16 @@ exports.utf8Length = utf8Length;
3945
4586
  exports.validateDetailData = validateDetailData;
3946
4587
  exports.validateWidgetData = validateWidgetData;
3947
4588
  exports.validateWidgetDefinition = validateWidgetDefinition;
4589
+ exports.viewChoices = viewChoices;
4590
+ exports.viewKind = viewKind;
4591
+ exports.viewNote = viewNote;
3948
4592
  exports.visibleWidgets = visibleWidgets;
3949
4593
  exports.widgetOptionValues = widgetOptionValues;
3950
4594
  exports.widgetsFor = widgetsFor;
3951
4595
  exports.widthForSize = widthForSize;
3952
4596
  exports.widthRange = widthRange;
3953
4597
  exports.widthShareLabel = widthShareLabel;
4598
+ exports.withClones = withClones;
3954
4599
  exports.withEncoding = withEncoding;
3955
4600
  exports.withFallback = withFallback;
3956
4601
  exports.withLogging = withLogging;