@richardmcquiston01/dashboard-widgets-toolkit 0.15.0 → 0.17.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.js CHANGED
@@ -1,86 +1,3 @@
1
- // src/core/payload.ts
2
- var WIDGET_KINDS = [
3
- "TEXT",
4
- "KPI",
5
- "GAUGE",
6
- "TABLE",
7
- "BAR_LIST",
8
- "ALERT_LIST",
9
- "GRAPH"
10
- ];
11
- function isWidgetKind(value) {
12
- return typeof value === "string" && WIDGET_KINDS.includes(value);
13
- }
14
- var VALUE_FORMATS = ["number", "currency", "percent"];
15
- var MAX_GRAPH_SERIES = 8;
16
- var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
17
- function emptyWidget(text = DEFAULT_EMPTY_TEXT) {
18
- return { empty: true, text };
19
- }
20
- function isWidgetEmptyState(payload) {
21
- return "empty" in payload && payload.empty === true;
22
- }
23
- function emptyTextFor(payload) {
24
- if (isWidgetEmptyState(payload)) {
25
- return payload.text;
26
- }
27
- switch (payload.kind) {
28
- case "TABLE":
29
- return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
30
- case "BAR_LIST":
31
- return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
32
- case "ALERT_LIST":
33
- return payload.items.length === 0 ? payload.emptyText : null;
34
- case "GRAPH":
35
- return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
36
- case "TEXT":
37
- case "KPI":
38
- case "GAUGE":
39
- return null;
40
- }
41
- }
42
-
43
- // src/core/compat.ts
44
- function isStringArray(value) {
45
- return Array.isArray(value) && value.every((v) => typeof v === "string");
46
- }
47
- function upgradeLegacyWidgetData(value) {
48
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
49
- return value;
50
- }
51
- const record = value;
52
- if (record["kind"] === "TABLE" && isStringArray(record["columns"]) && Array.isArray(record["rows"]) && record["rows"].every(isStringArray)) {
53
- const rows = record["rows"];
54
- return {
55
- ...record,
56
- columns: record["columns"].map((label) => ({ label })),
57
- rows: rows.map((row) => row.map((text) => ({ text })))
58
- };
59
- }
60
- if (record["kind"] === "GRAPH" && Array.isArray(record["points"]) && record["series"] === void 0) {
61
- const { points, ...rest } = record;
62
- return {
63
- ...rest,
64
- series: [{ name: "Value", points }],
65
- chartType: "bar",
66
- valueFormat: "number"
67
- };
68
- }
69
- return value;
70
- }
71
- function fromLegacyDefinition(legacy) {
72
- if (!isWidgetKind(legacy.viewType)) {
73
- return null;
74
- }
75
- return {
76
- key: legacy.widgetKey,
77
- title: legacy.title,
78
- ...legacy.description === null ? {} : { description: legacy.description },
79
- kind: legacy.viewType,
80
- sortOrder: legacy.sortOrder
81
- };
82
- }
83
-
84
1
  // src/core/definition.ts
85
2
  var WIDGET_SIZES = ["small", "medium", "large", "full"];
86
3
  var WIDGET_FILLS = ["height", "width", "both"];
@@ -88,13 +5,15 @@ var NOT_LOCKED = Object.freeze({
88
5
  move: false,
89
6
  hide: false,
90
7
  minimize: false,
91
- options: false
8
+ options: false,
9
+ clone: false
92
10
  });
93
11
  var FULLY_LOCKED = Object.freeze({
94
12
  move: true,
95
13
  hide: true,
96
14
  minimize: true,
97
- options: true
15
+ options: true,
16
+ clone: true
98
17
  });
99
18
  function resolveWidgetLock(definition) {
100
19
  const setting = definition.locked;
@@ -104,7 +23,8 @@ function resolveWidgetLock(definition) {
104
23
  move: setting.move === true,
105
24
  hide: setting.hide === true,
106
25
  minimize: setting.minimize === true,
107
- options: setting.options === true
26
+ options: setting.options === true,
27
+ clone: setting.clone === true
108
28
  };
109
29
  }
110
30
  function defineWidget(definition) {
@@ -128,6 +48,103 @@ function isWidgetVisibleTo(definition, roles) {
128
48
  return definition.roles.some((role) => roles.includes(role));
129
49
  }
130
50
 
51
+ // src/core/grid.ts
52
+ var WIDTH_COLUMNS = 12;
53
+ var MIN_WIDGET_WIDTH = 2;
54
+ var MAX_WIDGET_WIDTH = 12;
55
+ function isWidgetWidth(value) {
56
+ return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
57
+ }
58
+ function widthForSize(size) {
59
+ switch (size) {
60
+ case "small":
61
+ return 3;
62
+ case "large":
63
+ return 6;
64
+ case "full":
65
+ return 12;
66
+ default:
67
+ return 4;
68
+ }
69
+ }
70
+ function itemWidth(item) {
71
+ return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
72
+ }
73
+ function usesWidthColumns(items) {
74
+ return items.some((item) => item.width !== void 0);
75
+ }
76
+ function fillsWidth(fill) {
77
+ return fill === "width" || fill === "both";
78
+ }
79
+ function fillsHeight(fill) {
80
+ return fill === "height" || fill === "both";
81
+ }
82
+ function baseColumnSpan(size, columns, largeSpan = 2) {
83
+ if (size === "full") return columns;
84
+ if (size === "large") return Math.min(largeSpan, columns);
85
+ return 1;
86
+ }
87
+ function fillColumnSpans(items, columns, largeSpan = 2) {
88
+ return placeAndFill(
89
+ items,
90
+ columns,
91
+ (item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
92
+ );
93
+ }
94
+ function fillWidthSpans(items) {
95
+ return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
96
+ }
97
+ function placeRows(items) {
98
+ return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
99
+ }
100
+ function fitCount(items, maxRows) {
101
+ const rows = placeRows(items);
102
+ let count = 0;
103
+ for (const row of rows.slice(0, Math.max(0, Math.floor(maxRows)))) {
104
+ count += row.keys.length;
105
+ }
106
+ return count;
107
+ }
108
+ function placeAndFill(items, columns, spanOf) {
109
+ return placeAndFillRows(items, columns, spanOf).spans;
110
+ }
111
+ function placeAndFillRows(items, columns, spanOf) {
112
+ const trackCount = Math.max(1, Math.floor(columns));
113
+ const spans = /* @__PURE__ */ new Map();
114
+ const rows = [];
115
+ let row = [];
116
+ let used = 0;
117
+ function closeRow() {
118
+ if (row.length === 0) return;
119
+ const fillers = row.filter(({ item }) => fillsWidth(item.fill));
120
+ const leftover = trackCount - used;
121
+ const finalSpans = /* @__PURE__ */ new Map();
122
+ if (fillers.length > 0) {
123
+ const share = Math.floor(leftover / fillers.length);
124
+ const remainder = leftover % fillers.length;
125
+ fillers.forEach(({ item, span }, index) => {
126
+ const grown = span + share + (index < remainder ? 1 : 0);
127
+ spans.set(item.key, grown);
128
+ finalSpans.set(item.key, grown);
129
+ });
130
+ }
131
+ rows.push({
132
+ keys: row.map(({ item }) => item.key),
133
+ spans: row.map(({ item, span }) => finalSpans.get(item.key) ?? span)
134
+ });
135
+ row = [];
136
+ used = 0;
137
+ }
138
+ for (const item of items) {
139
+ const span = Math.min(spanOf(item, trackCount), trackCount);
140
+ if (used + span > trackCount && row.length > 0) closeRow();
141
+ row.push({ item, span });
142
+ used += span;
143
+ }
144
+ closeRow();
145
+ return { spans, rows };
146
+ }
147
+
131
148
  // src/core/format.ts
132
149
  var DEFAULT_CURRENCY = "USD";
133
150
  function localeArgument(locale) {
@@ -213,6 +230,48 @@ function kpiDelta(current, previous, options = {}) {
213
230
  };
214
231
  }
215
232
 
233
+ // src/core/payload.ts
234
+ var WIDGET_KINDS = [
235
+ "TEXT",
236
+ "KPI",
237
+ "GAUGE",
238
+ "TABLE",
239
+ "BAR_LIST",
240
+ "ALERT_LIST",
241
+ "GRAPH"
242
+ ];
243
+ function isWidgetKind(value) {
244
+ return typeof value === "string" && WIDGET_KINDS.includes(value);
245
+ }
246
+ var VALUE_FORMATS = ["number", "currency", "percent"];
247
+ var MAX_GRAPH_SERIES = 8;
248
+ var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
249
+ function emptyWidget(text = DEFAULT_EMPTY_TEXT) {
250
+ return { empty: true, text };
251
+ }
252
+ function isWidgetEmptyState(payload) {
253
+ return "empty" in payload && payload.empty === true;
254
+ }
255
+ function emptyTextFor(payload) {
256
+ if (isWidgetEmptyState(payload)) {
257
+ return payload.text;
258
+ }
259
+ switch (payload.kind) {
260
+ case "TABLE":
261
+ return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
262
+ case "BAR_LIST":
263
+ return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
264
+ case "ALERT_LIST":
265
+ return payload.items.length === 0 ? payload.emptyText : null;
266
+ case "GRAPH":
267
+ return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
268
+ case "TEXT":
269
+ case "KPI":
270
+ case "GAUGE":
271
+ return null;
272
+ }
273
+ }
274
+
216
275
  // src/core/result.ts
217
276
  function ok(value) {
218
277
  return { ok: true, value };
@@ -235,20 +294,308 @@ function describeUnknownError(cause) {
235
294
  }
236
295
  }
237
296
 
238
- // src/core/url.ts
239
- var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
240
- var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
241
- function schemeOf(url) {
242
- const match = SCHEME_PATTERN.exec(
243
- url.replace(IGNORED_IN_SCHEME, "")
244
- );
245
- return match === null ? null : (match[1] ?? "").toLowerCase();
297
+ // src/core/views.ts
298
+ var BASE_VIEW_KEY = "base";
299
+ var MAX_WIDGET_VIEWS = 8;
300
+ var VIEW_CONVERSIONS = {
301
+ tableToBarList: { from: "TABLE", to: "BAR_LIST" },
302
+ tableToGraph: { from: "TABLE", to: "GRAPH" },
303
+ barListToTable: { from: "BAR_LIST", to: "TABLE" },
304
+ graphToTable: { from: "GRAPH", to: "TABLE" },
305
+ barListToGraph: { from: "BAR_LIST", to: "GRAPH" }
306
+ };
307
+ var KIND_LABELS = {
308
+ TEXT: "Text",
309
+ KPI: "Number",
310
+ GAUGE: "Gauge",
311
+ TABLE: "Table",
312
+ BAR_LIST: "Bars",
313
+ ALERT_LIST: "List",
314
+ GRAPH: "Chart"
315
+ };
316
+ function kindLabel(kind) {
317
+ return KIND_LABELS[kind];
246
318
  }
247
- function isSafeHref(url) {
248
- if (url.trim() === "") {
249
- return false;
250
- }
251
- const scheme = schemeOf(url);
319
+ function hasViews(item) {
320
+ return item.views !== void 0 && item.views.length > 0;
321
+ }
322
+ function findView(item, key) {
323
+ return item.views?.find((view) => view.key === key);
324
+ }
325
+ function viewChoices(item) {
326
+ if (!hasViews(item)) return [];
327
+ return [
328
+ {
329
+ key: BASE_VIEW_KEY,
330
+ label: item.baseViewLabel ?? kindLabel(item.kind),
331
+ kind: item.kind
332
+ },
333
+ ...(item.views ?? []).map(({ key, label, kind }) => ({
334
+ key,
335
+ label,
336
+ kind
337
+ }))
338
+ ];
339
+ }
340
+ function isProviderView(view) {
341
+ return view.convert === void 0;
342
+ }
343
+ function isKnownView(item, key) {
344
+ return key !== void 0 && (key === BASE_VIEW_KEY || findView(item, key) !== void 0);
345
+ }
346
+ function effectiveViewKey(item, chosen) {
347
+ if (!hasViews(item)) return BASE_VIEW_KEY;
348
+ if (isKnownView(item, chosen)) return chosen;
349
+ if (isKnownView(item, item.defaultView)) return item.defaultView;
350
+ return BASE_VIEW_KEY;
351
+ }
352
+ function viewKind(item, chosen) {
353
+ return findView(item, effectiveViewKey(item, chosen))?.kind ?? item.kind;
354
+ }
355
+ function loadViewKey(item, chosen) {
356
+ const key = effectiveViewKey(item, chosen);
357
+ const view = findView(item, key);
358
+ return view !== void 0 && isProviderView(view) ? key : BASE_VIEW_KEY;
359
+ }
360
+ function loadViewKind(item, chosen) {
361
+ return findView(item, loadViewKey(item, chosen))?.kind ?? item.kind;
362
+ }
363
+ function cellValue(cell) {
364
+ const value = cell?.value;
365
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
366
+ }
367
+ function columnName(table, index) {
368
+ return table.columns[index]?.label ?? `#${index + 1}`;
369
+ }
370
+ function checkColumn(table, index, field, context) {
371
+ if (!Number.isInteger(index) || index < 0 || index >= table.columns.length) {
372
+ return `${context.where}: ${field} column ${index} does not exist (the table has ${table.columns.length} columns, numbered from 0).`;
373
+ }
374
+ return void 0;
375
+ }
376
+ function numericColumn(table, index, context) {
377
+ const numbers = [];
378
+ for (const [rowIndex, row] of table.rows.entries()) {
379
+ const value = cellValue(row[index]);
380
+ if (value === void 0) {
381
+ return err(
382
+ `${context.where}: row ${rowIndex + 1}, column ${index + 1} ("${columnName(table, index)}") has no numeric value. Add value to the cells.`
383
+ );
384
+ }
385
+ numbers.push(value);
386
+ }
387
+ return ok(numbers);
388
+ }
389
+ function tableToBarList(table, conversion, context) {
390
+ const bad = checkColumn(table, conversion.label, "label", context) ?? checkColumn(table, conversion.value, "value", context);
391
+ if (bad !== void 0) return err(bad);
392
+ const numbers = numericColumn(table, conversion.value, context);
393
+ if (!numbers.ok) return numbers;
394
+ const items = table.rows.map((row, index) => ({
395
+ label: row[conversion.label]?.text ?? "",
396
+ value: numbers.value[index],
397
+ display: row[conversion.value]?.text ?? String(numbers.value[index])
398
+ }));
399
+ return ok({ kind: "BAR_LIST", items });
400
+ }
401
+ function tableToGraph(table, conversion, context) {
402
+ if (conversion.values.length === 0 || conversion.values.length > MAX_GRAPH_SERIES) {
403
+ return err(
404
+ `${context.where}: a chart needs 1 to ${MAX_GRAPH_SERIES} series columns, got ${conversion.values.length}.`
405
+ );
406
+ }
407
+ let bad = checkColumn(
408
+ table,
409
+ conversion.label,
410
+ "label",
411
+ context
412
+ );
413
+ for (const index of conversion.values) {
414
+ bad ??= checkColumn(table, index, "series", context);
415
+ }
416
+ if (bad !== void 0) return err(bad);
417
+ const series = [];
418
+ for (const index of conversion.values) {
419
+ const numbers = numericColumn(table, index, context);
420
+ if (!numbers.ok) return numbers;
421
+ series.push({
422
+ name: columnName(table, index),
423
+ points: table.rows.map((row, rowIndex) => ({
424
+ label: row[conversion.label]?.text ?? "",
425
+ value: numbers.value[rowIndex]
426
+ }))
427
+ });
428
+ }
429
+ return ok({
430
+ kind: "GRAPH",
431
+ series,
432
+ chartType: conversion.chartType,
433
+ valueFormat: conversion.valueFormat ?? "number",
434
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency },
435
+ xLabel: columnName(table, conversion.label)
436
+ });
437
+ }
438
+ function barListToTable(list, conversion) {
439
+ return ok({
440
+ kind: "TABLE",
441
+ columns: [
442
+ { label: conversion.labelHeading ?? "Name" },
443
+ { label: conversion.valueHeading ?? "Value", numeric: true }
444
+ ],
445
+ rows: list.items.map((item) => [
446
+ { text: item.label },
447
+ { text: item.display ?? String(item.value), value: item.value }
448
+ ])
449
+ });
450
+ }
451
+ function graphToTable(graph, context) {
452
+ const first = graph.series[0];
453
+ if (first === void 0)
454
+ return err(`${context.where}: the chart has no series.`);
455
+ const format = {
456
+ ...graph.currency === void 0 ? {} : { currency: graph.currency },
457
+ ...context.locale === void 0 ? {} : { locale: context.locale }
458
+ };
459
+ return ok({
460
+ kind: "TABLE",
461
+ columns: [
462
+ { label: graph.xLabel ?? "Label" },
463
+ ...graph.series.map((series) => ({ label: series.name, numeric: true }))
464
+ ],
465
+ rows: first.points.map((point, index) => [
466
+ { text: point.label },
467
+ ...graph.series.map((series) => {
468
+ const value = series.points[index]?.value;
469
+ return value === void 0 ? { text: "\u2014" } : { text: formatValue(value, graph.valueFormat, format), value };
470
+ })
471
+ ])
472
+ });
473
+ }
474
+ function barListToGraph(list, conversion) {
475
+ return ok({
476
+ kind: "GRAPH",
477
+ series: [
478
+ {
479
+ name: conversion.seriesName ?? "Value",
480
+ points: list.items.map((item) => ({
481
+ label: item.label,
482
+ value: item.value
483
+ }))
484
+ }
485
+ ],
486
+ chartType: conversion.chartType,
487
+ valueFormat: conversion.valueFormat ?? "number",
488
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency }
489
+ });
490
+ }
491
+ function convertView(widgetKey, view, data, options = {}) {
492
+ const conversion = view.convert;
493
+ const context = {
494
+ where: `Widget ${JSON.stringify(widgetKey)} view ${JSON.stringify(view.key)}`,
495
+ ...options.locale === void 0 ? {} : { locale: options.locale }
496
+ };
497
+ if (conversion === void 0) {
498
+ return err(
499
+ `${context.where}: the provider supplies this view, so there is nothing to convert.`
500
+ );
501
+ }
502
+ const expected = VIEW_CONVERSIONS[conversion.type]?.from;
503
+ if (expected === void 0) {
504
+ return err(
505
+ `${context.where}: unknown conversion type ${JSON.stringify(conversion.type)}.`
506
+ );
507
+ }
508
+ if (data.kind !== expected) {
509
+ return err(
510
+ `${context.where}: ${conversion.type} reads a ${expected} payload, but the widget returned ${data.kind}.`
511
+ );
512
+ }
513
+ switch (conversion.type) {
514
+ case "tableToBarList":
515
+ return tableToBarList(data, conversion, context);
516
+ case "tableToGraph":
517
+ return tableToGraph(data, conversion, context);
518
+ case "barListToTable":
519
+ return barListToTable(data, conversion);
520
+ case "graphToTable":
521
+ return graphToTable(data, context);
522
+ case "barListToGraph":
523
+ return barListToGraph(data, conversion);
524
+ }
525
+ }
526
+ function viewNote(view, data) {
527
+ const conversion = view.convert;
528
+ if (data?.kind !== "TABLE" || conversion === void 0) return "";
529
+ let indexes;
530
+ if (conversion.type === "tableToBarList") {
531
+ indexes = [conversion.label, conversion.value];
532
+ } else if (conversion.type === "tableToGraph") {
533
+ indexes = [conversion.label, ...conversion.values];
534
+ } else {
535
+ return "";
536
+ }
537
+ const names = indexes.map(
538
+ (index) => data.columns[index]?.label ?? `#${index + 1}`
539
+ );
540
+ return `Shows ${names.join(", ")} only.`;
541
+ }
542
+ function applyView(widget, chosen, options = {}) {
543
+ const key = effectiveViewKey(widget.definition, chosen);
544
+ const view = findView(widget.definition, key);
545
+ const wanted = loadViewKey(widget.definition, chosen);
546
+ const loaded = widget.status === "ok" ? widget.view ?? BASE_VIEW_KEY : void 0;
547
+ const mismatched = loaded !== void 0 && loaded !== wanted;
548
+ if (view === void 0) {
549
+ return mismatched ? {
550
+ definition: widget.definition,
551
+ status: "loading",
552
+ ...widget.options === void 0 ? {} : { options: widget.options }
553
+ } : widget;
554
+ }
555
+ const definition = {
556
+ ...widget.definition,
557
+ kind: view.kind
558
+ };
559
+ if (widget.status !== "ok") return { ...widget, definition };
560
+ if (mismatched) {
561
+ return {
562
+ definition,
563
+ status: "loading",
564
+ ...widget.options === void 0 ? {} : { options: widget.options }
565
+ };
566
+ }
567
+ if (isProviderView(view)) return { ...widget, definition };
568
+ const converted = convertView(
569
+ widget.definition.key,
570
+ view,
571
+ widget.data,
572
+ options
573
+ );
574
+ if (!converted.ok) {
575
+ return {
576
+ definition,
577
+ status: "error",
578
+ error: converted.error,
579
+ ...widget.options === void 0 ? {} : { options: widget.options }
580
+ };
581
+ }
582
+ return { ...widget, definition, data: converted.value };
583
+ }
584
+
585
+ // src/core/url.ts
586
+ var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
587
+ var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
588
+ function schemeOf(url) {
589
+ const match = SCHEME_PATTERN.exec(
590
+ url.replace(IGNORED_IN_SCHEME, "")
591
+ );
592
+ return match === null ? null : (match[1] ?? "").toLowerCase();
593
+ }
594
+ function isSafeHref(url) {
595
+ if (url.trim() === "") {
596
+ return false;
597
+ }
598
+ const scheme = schemeOf(url);
252
599
  return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
253
600
  }
254
601
  function isSafeImageUrl(url) {
@@ -289,15 +636,15 @@ function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
289
636
  function fold(text) {
290
637
  return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
291
638
  }
292
- function cellValue(cell) {
639
+ function cellValue2(cell) {
293
640
  return cell.value ?? cell.text;
294
641
  }
295
642
  function cellMatches(cell, needle) {
296
- return fold(String(cellValue(cell))).includes(needle);
643
+ return fold(String(cellValue2(cell))).includes(needle);
297
644
  }
298
645
  function compareCells(a, b, collator) {
299
- const left = cellValue(a);
300
- const right = cellValue(b);
646
+ const left = cellValue2(a);
647
+ const right = cellValue2(b);
301
648
  if (typeof left === "number" && typeof right === "number") {
302
649
  return left - right;
303
650
  }
@@ -589,1492 +936,1878 @@ function deriveDetailData(data, locale) {
589
936
  return void 0;
590
937
  }
591
938
 
592
- // src/core/grid.ts
593
- var WIDTH_COLUMNS = 12;
594
- var MIN_WIDGET_WIDTH = 2;
595
- var MAX_WIDGET_WIDTH = 12;
596
- function isWidgetWidth(value) {
597
- return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
598
- }
599
- function widthForSize(size) {
600
- switch (size) {
601
- case "small":
602
- return 3;
603
- case "large":
604
- return 6;
605
- case "full":
606
- return 12;
607
- default:
608
- return 4;
609
- }
610
- }
611
- function itemWidth(item) {
612
- return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
613
- }
614
- function usesWidthColumns(items) {
615
- return items.some((item) => item.width !== void 0);
939
+ // src/core/options.ts
940
+ var DATE_RANGE_PRESETS = [
941
+ "today",
942
+ "yesterday",
943
+ "last7",
944
+ "last30",
945
+ "last90",
946
+ "thisMonth",
947
+ "lastMonth",
948
+ "thisYear",
949
+ "lastYear"
950
+ ];
951
+ var WIDGET_OPTION_TYPES = [
952
+ "choice",
953
+ "number",
954
+ "boolean",
955
+ "text",
956
+ "dateRange",
957
+ "color",
958
+ "sort",
959
+ "columns"
960
+ ];
961
+ var MAX_WIDGET_OPTIONS = 20;
962
+ var MAX_TEXT_OPTION_LENGTH = 500;
963
+ var OPTION_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,39}$/;
964
+ var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
965
+ var INTERVAL_PATTERN = /^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/;
966
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
967
+ function isColumnKeyFor(kind, key) {
968
+ if (kind === "TABLE") return /^c\d+$/.test(key);
969
+ if (kind === "BAR_LIST") return key === "label" || key === "value";
970
+ return false;
616
971
  }
617
- function fillsWidth(fill) {
618
- return fill === "width" || fill === "both";
972
+ function isClientApplied(option) {
973
+ return (option.type === "sort" || option.type === "columns") && option.apply === "client";
619
974
  }
620
- function fillsHeight(fill) {
621
- return fill === "height" || fill === "both";
975
+ function civilToday(context) {
976
+ const now = context.now ?? Date.now();
977
+ let timeZone = context.timeZone ?? "UTC";
978
+ try {
979
+ new Intl.DateTimeFormat("en-US", { timeZone });
980
+ } catch {
981
+ timeZone = "UTC";
982
+ }
983
+ const parts = new Intl.DateTimeFormat("en-US", {
984
+ timeZone,
985
+ year: "numeric",
986
+ month: "2-digit",
987
+ day: "2-digit"
988
+ }).formatToParts(new Date(now));
989
+ const pick = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
990
+ return { year: pick("year"), month: pick("month"), day: pick("day") };
622
991
  }
623
- function baseColumnSpan(size, columns, largeSpan = 2) {
624
- if (size === "full") return columns;
625
- if (size === "large") return Math.min(largeSpan, columns);
626
- return 1;
992
+ function toIso(epochMs) {
993
+ return new Date(epochMs).toISOString().slice(0, 10);
627
994
  }
628
- function fillColumnSpans(items, columns, largeSpan = 2) {
629
- return placeAndFill(
630
- items,
631
- columns,
632
- (item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
995
+ var DAY_MS = 864e5;
996
+ function parseIsoDay(text) {
997
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
998
+ if (match === null) return void 0;
999
+ const epoch = Date.UTC(
1000
+ Number(match[1]),
1001
+ Number(match[2]) - 1,
1002
+ Number(match[3])
633
1003
  );
1004
+ return toIso(epoch) === text ? epoch : void 0;
634
1005
  }
635
- function fillWidthSpans(items) {
636
- return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
1006
+ function isIsoInterval(value) {
1007
+ const match = INTERVAL_PATTERN.exec(value);
1008
+ if (match === null) return false;
1009
+ const start = parseIsoDay(match[1]);
1010
+ const end = parseIsoDay(match[2]);
1011
+ return start !== void 0 && end !== void 0 && start <= end;
637
1012
  }
638
- function placeRows(items) {
639
- return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
1013
+ function isPreset(value) {
1014
+ return DATE_RANGE_PRESETS.includes(value);
640
1015
  }
641
- function fitCount(items, maxRows) {
642
- const rows = placeRows(items);
643
- let count = 0;
644
- for (const row of rows.slice(0, Math.max(0, Math.floor(maxRows)))) {
645
- count += row.keys.length;
1016
+ function resolveDateRange(value, context = {}) {
1017
+ if (isIsoInterval(value)) return value;
1018
+ if (!isPreset(value)) return void 0;
1019
+ const today = civilToday(context);
1020
+ const todayMs = Date.UTC(today.year, today.month - 1, today.day);
1021
+ const range = (startMs, endMs) => `${toIso(startMs)}/${toIso(endMs)}`;
1022
+ switch (value) {
1023
+ case "today":
1024
+ return range(todayMs, todayMs);
1025
+ case "yesterday":
1026
+ return range(todayMs - DAY_MS, todayMs - DAY_MS);
1027
+ case "last7":
1028
+ return range(todayMs - 6 * DAY_MS, todayMs);
1029
+ case "last30":
1030
+ return range(todayMs - 29 * DAY_MS, todayMs);
1031
+ case "last90":
1032
+ return range(todayMs - 89 * DAY_MS, todayMs);
1033
+ case "thisMonth":
1034
+ return range(Date.UTC(today.year, today.month - 1, 1), todayMs);
1035
+ case "lastMonth":
1036
+ return range(
1037
+ Date.UTC(today.year, today.month - 2, 1),
1038
+ Date.UTC(today.year, today.month - 1, 1) - DAY_MS
1039
+ );
1040
+ case "thisYear":
1041
+ return range(Date.UTC(today.year, 0, 1), todayMs);
1042
+ case "lastYear":
1043
+ return range(
1044
+ Date.UTC(today.year - 1, 0, 1),
1045
+ Date.UTC(today.year, 0, 1) - DAY_MS
1046
+ );
646
1047
  }
647
- return count;
648
1048
  }
649
- function placeAndFill(items, columns, spanOf) {
650
- return placeAndFillRows(items, columns, spanOf).spans;
1049
+ function onStepGrid(option, value) {
1050
+ if (option.step === void 0) return true;
1051
+ const steps = (value - option.min) / option.step;
1052
+ return Math.abs(steps - Math.round(steps)) < 1e-9;
651
1053
  }
652
- function placeAndFillRows(items, columns, spanOf) {
653
- const trackCount = Math.max(1, Math.floor(columns));
654
- const spans = /* @__PURE__ */ new Map();
655
- const rows = [];
656
- let row = [];
657
- let used = 0;
658
- function closeRow() {
659
- if (row.length === 0) return;
660
- const fillers = row.filter(({ item }) => fillsWidth(item.fill));
661
- const leftover = trackCount - used;
662
- const finalSpans = /* @__PURE__ */ new Map();
663
- if (fillers.length > 0) {
664
- const share = Math.floor(leftover / fillers.length);
665
- const remainder = leftover % fillers.length;
666
- fillers.forEach(({ item, span }, index) => {
667
- const grown = span + share + (index < remainder ? 1 : 0);
668
- spans.set(item.key, grown);
669
- finalSpans.set(item.key, grown);
670
- });
1054
+ function parseSortValue(value) {
1055
+ const separator = value.lastIndexOf(":");
1056
+ if (separator <= 0) return void 0;
1057
+ const column = value.slice(0, separator);
1058
+ const direction = value.slice(separator + 1);
1059
+ if (direction !== "asc" && direction !== "desc") return void 0;
1060
+ return { column, direction };
1061
+ }
1062
+ function sortFromOptions(definition, values) {
1063
+ if (values === void 0) return void 0;
1064
+ const sortOption = definition.options?.find(
1065
+ (option) => option.type === "sort"
1066
+ );
1067
+ if (sortOption === void 0) return void 0;
1068
+ const chosen = values[sortOption.key];
1069
+ return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
1070
+ }
1071
+ function coerceOptionValue(option, value, context = {}) {
1072
+ switch (option.type) {
1073
+ case "choice":
1074
+ return typeof value === "string" && option.choices.some((choice) => choice.value === value) ? value : void 0;
1075
+ case "number":
1076
+ return typeof value === "number" && Number.isFinite(value) && value >= option.min && value <= option.max && onStepGrid(option, value) ? value : void 0;
1077
+ case "boolean":
1078
+ return typeof value === "boolean" ? value : void 0;
1079
+ case "text": {
1080
+ if (typeof value !== "string") return void 0;
1081
+ const trimmed = value.trim();
1082
+ return trimmed.length <= option.maxLength && !CONTROL_CHARACTERS.test(trimmed) ? trimmed : void 0;
1083
+ }
1084
+ case "dateRange": {
1085
+ if (typeof value !== "string") return void 0;
1086
+ if (isPreset(value) && option.presets !== void 0 && !option.presets.some((preset) => preset.value === value)) {
1087
+ return void 0;
1088
+ }
1089
+ return resolveDateRange(value, context);
1090
+ }
1091
+ case "color":
1092
+ return typeof value === "string" && COLOR_PATTERN.test(value) ? value.toLowerCase() : void 0;
1093
+ case "sort": {
1094
+ if (typeof value !== "string") return void 0;
1095
+ const sort = parseSortValue(value);
1096
+ return sort !== void 0 && option.columns.some((column) => column.key === sort.column) ? value : void 0;
1097
+ }
1098
+ case "columns": {
1099
+ if (!Array.isArray(value) || value.length === 0) return void 0;
1100
+ const keys = value;
1101
+ const allowed = new Set(option.columns.map((c) => c.key));
1102
+ const seen = /* @__PURE__ */ new Set();
1103
+ for (const key of keys) {
1104
+ if (typeof key !== "string" || !allowed.has(key) || seen.has(key)) {
1105
+ return void 0;
1106
+ }
1107
+ seen.add(key);
1108
+ }
1109
+ return [...seen];
671
1110
  }
672
- rows.push({
673
- keys: row.map(({ item }) => item.key),
674
- spans: row.map(({ item, span }) => finalSpans.get(item.key) ?? span)
675
- });
676
- row = [];
677
- used = 0;
678
1111
  }
679
- for (const item of items) {
680
- const span = Math.min(spanOf(item, trackCount), trackCount);
681
- if (used + span > trackCount && row.length > 0) closeRow();
682
- row.push({ item, span });
683
- used += span;
1112
+ }
1113
+ function defaultOptionValue(option, context = {}) {
1114
+ switch (option.type) {
1115
+ case "dateRange":
1116
+ return resolveDateRange(option.default, context);
1117
+ case "sort":
1118
+ return option.default;
1119
+ case "columns":
1120
+ return [...option.default];
1121
+ default:
1122
+ return option.default;
684
1123
  }
685
- closeRow();
686
- return { spans, rows };
687
1124
  }
688
-
689
- // src/core/views.ts
690
- var BASE_VIEW_KEY = "base";
691
- var MAX_WIDGET_VIEWS = 8;
692
- var VIEW_CONVERSIONS = {
693
- tableToBarList: { from: "TABLE", to: "BAR_LIST" },
694
- tableToGraph: { from: "TABLE", to: "GRAPH" },
695
- barListToTable: { from: "BAR_LIST", to: "TABLE" },
696
- graphToTable: { from: "GRAPH", to: "TABLE" },
697
- barListToGraph: { from: "BAR_LIST", to: "GRAPH" }
698
- };
699
- var KIND_LABELS = {
700
- TEXT: "Text",
701
- KPI: "Number",
702
- GAUGE: "Gauge",
703
- TABLE: "Table",
704
- BAR_LIST: "Bars",
705
- ALERT_LIST: "List",
706
- GRAPH: "Chart"
707
- };
708
- function kindLabel(kind) {
709
- return KIND_LABELS[kind];
710
- }
711
- function hasViews(item) {
712
- return item.views !== void 0 && item.views.length > 0;
1125
+ var NO_VALUES = Object.freeze({});
1126
+ function resolveOptionValues(definition, chosen, context = {}) {
1127
+ const options = definition.options;
1128
+ if (options === void 0 || options.length === 0) return NO_VALUES;
1129
+ const values = {};
1130
+ for (const option of options) {
1131
+ const picked = chosen?.[option.key];
1132
+ const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked, context)) ?? defaultOptionValue(option, context);
1133
+ if (value !== void 0) values[option.key] = value;
1134
+ }
1135
+ return values;
713
1136
  }
714
- function findView(item, key) {
715
- return item.views?.find((view) => view.key === key);
1137
+ function sameValue(a, b) {
1138
+ if (Array.isArray(a) && Array.isArray(b)) {
1139
+ return a.length === b.length && a.every((item, i) => item === b[i]);
1140
+ }
1141
+ return a === b;
716
1142
  }
717
- function viewChoices(item) {
718
- if (!hasViews(item)) return [];
719
- return [
720
- {
721
- key: BASE_VIEW_KEY,
722
- label: item.baseViewLabel ?? kindLabel(item.kind),
723
- kind: item.kind
724
- },
725
- ...(item.views ?? []).map(({ key, label, kind }) => ({
726
- key,
727
- label,
728
- kind
729
- }))
730
- ];
1143
+ function sameOptionValues(a, b) {
1144
+ const keys = Object.keys({ ...a, ...b });
1145
+ return keys.every((key) => sameValue(a[key], b[key]));
731
1146
  }
732
- function isKnownView(item, key) {
733
- return key !== void 0 && (key === BASE_VIEW_KEY || findView(item, key) !== void 0);
1147
+ function optionsCacheSuffix(definition, values, context = {}) {
1148
+ const parts = [];
1149
+ for (const option of definition.options ?? []) {
1150
+ if (isClientApplied(option)) continue;
1151
+ const value = values[option.key];
1152
+ if (sameValue(value, defaultOptionValue(option, context))) continue;
1153
+ parts.push(
1154
+ `${option.key}=${encodeURIComponent(JSON.stringify(value ?? null))}`
1155
+ );
1156
+ }
1157
+ parts.sort();
1158
+ return parts.length === 0 ? "" : `?${parts.join("&")}`;
734
1159
  }
735
- function effectiveViewKey(item, chosen) {
736
- if (!hasViews(item)) return BASE_VIEW_KEY;
737
- if (isKnownView(item, chosen)) return chosen;
738
- if (isKnownView(item, item.defaultView)) return item.defaultView;
739
- return BASE_VIEW_KEY;
1160
+ function tableColumnIndex(key) {
1161
+ const match = /^c(\d+)$/.exec(key);
1162
+ return match === null ? void 0 : Number(match[1]);
740
1163
  }
741
- function viewKind(item, chosen) {
742
- return findView(item, effectiveViewKey(item, chosen))?.kind ?? item.kind;
1164
+ function sortBarItems(items, sort) {
1165
+ const sign = sort.direction === "desc" ? -1 : 1;
1166
+ return items.map((item, index) => ({ item, index })).sort((a, b) => {
1167
+ const order = sort.column === "value" ? a.item.value - b.item.value : a.item.label.localeCompare(b.item.label);
1168
+ return order * sign || a.index - b.index;
1169
+ }).map(({ item }) => item);
743
1170
  }
744
- function cellValue2(cell) {
745
- const value = cell?.value;
746
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1171
+ function applyClientOptions(definition, data, values) {
1172
+ const options = definition.options ?? [];
1173
+ const sortOption = options.find(
1174
+ (o) => o.type === "sort" && o.apply === "client"
1175
+ );
1176
+ const columnsOption = options.find(
1177
+ (o) => o.type === "columns" && o.apply === "client"
1178
+ );
1179
+ const sortValue = sortOption === void 0 ? void 0 : values[sortOption.key];
1180
+ const columnsValue = columnsOption === void 0 ? void 0 : values[columnsOption.key];
1181
+ let next = data;
1182
+ if (typeof sortValue === "string") {
1183
+ const sort = parseSortValue(sortValue);
1184
+ if (sort !== void 0 && next.kind === "BAR_LIST") {
1185
+ if (sort.column === "label" || sort.column === "value") {
1186
+ next = { ...next, items: sortBarItems(next.items, sort) };
1187
+ }
1188
+ } else if (sort !== void 0 && next.kind === "TABLE") {
1189
+ const index = tableColumnIndex(sort.column);
1190
+ if (index !== void 0 && index < next.columns.length) {
1191
+ const sorted = queryRows(tableDetailData(next), {
1192
+ page: 1,
1193
+ pageSize: Math.max(1, next.rows.length),
1194
+ sort
1195
+ });
1196
+ if (sorted.ok) next = { ...next, rows: sorted.value.rows };
1197
+ }
1198
+ }
1199
+ }
1200
+ if (Array.isArray(columnsValue) && next.kind === "TABLE") {
1201
+ const table = next;
1202
+ const indexes = columnsValue.map(tableColumnIndex).filter(
1203
+ (index) => index !== void 0 && index < table.columns.length
1204
+ );
1205
+ if (indexes.length > 0) {
1206
+ next = {
1207
+ ...table,
1208
+ columns: indexes.map((i) => table.columns[i]),
1209
+ rows: table.rows.map((row) => indexes.map((i) => row[i]))
1210
+ };
1211
+ }
1212
+ }
1213
+ return next;
747
1214
  }
748
- function columnName(table, index) {
749
- return table.columns[index]?.label ?? `#${index + 1}`;
1215
+
1216
+ // src/core/settings.ts
1217
+ var MAX_WIDGET_TITLE_LENGTH = 80;
1218
+ var MAX_PARSED_SETTINGS = 500;
1219
+ var MAX_COLUMN_LIST = 100;
1220
+ var MAX_VALUE_LENGTH = 500;
1221
+ function isRecord(value) {
1222
+ return typeof value === "object" && value !== null && !Array.isArray(value);
750
1223
  }
751
- function checkColumn(table, index, field, context) {
752
- if (!Number.isInteger(index) || index < 0 || index >= table.columns.length) {
753
- return `${context.where}: ${field} column ${index} does not exist (the table has ${table.columns.length} columns, numbered from 0).`;
1224
+ function parseOptionValue(value) {
1225
+ if (typeof value === "string") {
1226
+ return value.length <= MAX_VALUE_LENGTH ? value : void 0;
1227
+ }
1228
+ if (typeof value === "number")
1229
+ return Number.isFinite(value) ? value : void 0;
1230
+ if (typeof value === "boolean") return value;
1231
+ if (Array.isArray(value) && value.length <= MAX_COLUMN_LIST) {
1232
+ return value.every(
1233
+ (entry) => typeof entry === "string" && entry.length <= MAX_VALUE_LENGTH
1234
+ ) ? value : void 0;
754
1235
  }
755
1236
  return void 0;
756
1237
  }
757
- function numericColumn(table, index, context) {
758
- const numbers = [];
759
- for (const [rowIndex, row] of table.rows.entries()) {
760
- const value = cellValue2(row[index]);
761
- if (value === void 0) {
762
- return err(
763
- `${context.where}: row ${rowIndex + 1}, column ${index + 1} ("${columnName(table, index)}") has no numeric value. Add value to the cells.`
764
- );
1238
+ function parseOne(raw) {
1239
+ if (!isRecord(raw)) return void 0;
1240
+ const title = raw["title"];
1241
+ const width = raw["width"];
1242
+ const flexible = raw["flexible"];
1243
+ const options = raw["options"];
1244
+ const view = raw["view"];
1245
+ const cleanTitle = typeof title === "string" ? title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH) : "";
1246
+ const chosen = {};
1247
+ if (isRecord(options)) {
1248
+ for (const [key, value] of Object.entries(options)) {
1249
+ const parsed = parseOptionValue(value);
1250
+ if (parsed !== void 0) chosen[key] = parsed;
765
1251
  }
766
- numbers.push(value);
767
1252
  }
768
- return ok(numbers);
769
- }
770
- function tableToBarList(table, conversion, context) {
771
- const bad = checkColumn(table, conversion.label, "label", context) ?? checkColumn(table, conversion.value, "value", context);
772
- if (bad !== void 0) return err(bad);
773
- const numbers = numericColumn(table, conversion.value, context);
774
- if (!numbers.ok) return numbers;
775
- const items = table.rows.map((row, index) => ({
776
- label: row[conversion.label]?.text ?? "",
777
- value: numbers.value[index],
778
- display: row[conversion.value]?.text ?? String(numbers.value[index])
779
- }));
780
- return ok({ kind: "BAR_LIST", items });
1253
+ const settings = {
1254
+ ...cleanTitle === "" ? {} : { title: cleanTitle },
1255
+ ...isWidgetWidth(width) ? { width } : {},
1256
+ ...typeof flexible === "boolean" ? { flexible } : {},
1257
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
1258
+ ...typeof view === "string" && view !== "" && view.length <= 80 ? { view } : {}
1259
+ };
1260
+ return Object.keys(settings).length === 0 ? void 0 : settings;
781
1261
  }
782
- function tableToGraph(table, conversion, context) {
783
- if (conversion.values.length === 0 || conversion.values.length > MAX_GRAPH_SERIES) {
784
- return err(
785
- `${context.where}: a chart needs 1 to ${MAX_GRAPH_SERIES} series columns, got ${conversion.values.length}.`
786
- );
787
- }
788
- let bad = checkColumn(
789
- table,
790
- conversion.label,
791
- "label",
792
- context
793
- );
794
- for (const index of conversion.values) {
795
- bad ??= checkColumn(table, index, "series", context);
796
- }
797
- if (bad !== void 0) return err(bad);
798
- const series = [];
799
- for (const index of conversion.values) {
800
- const numbers = numericColumn(table, index, context);
801
- if (!numbers.ok) return numbers;
802
- series.push({
803
- name: columnName(table, index),
804
- points: table.rows.map((row, rowIndex) => ({
805
- label: row[conversion.label]?.text ?? "",
806
- value: numbers.value[rowIndex]
807
- }))
808
- });
1262
+ function parseSettings(raw) {
1263
+ if (!isRecord(raw)) return void 0;
1264
+ const parsed = {};
1265
+ for (const [key, value] of Object.entries(raw).slice(
1266
+ 0,
1267
+ MAX_PARSED_SETTINGS
1268
+ )) {
1269
+ if (key === "") continue;
1270
+ const one = parseOne(value);
1271
+ if (one !== void 0) parsed[key] = one;
809
1272
  }
810
- return ok({
811
- kind: "GRAPH",
812
- series,
813
- chartType: conversion.chartType,
814
- valueFormat: conversion.valueFormat ?? "number",
815
- ...conversion.currency === void 0 ? {} : { currency: conversion.currency },
816
- xLabel: columnName(table, conversion.label)
817
- });
1273
+ return Object.keys(parsed).length === 0 ? void 0 : parsed;
818
1274
  }
819
- function barListToTable(list, conversion) {
820
- return ok({
821
- kind: "TABLE",
822
- columns: [
823
- { label: conversion.labelHeading ?? "Name" },
824
- { label: conversion.valueHeading ?? "Value", numeric: true }
825
- ],
826
- rows: list.items.map((item) => [
827
- { text: item.label },
828
- { text: item.display ?? String(item.value), value: item.value }
829
- ])
830
- });
1275
+ function serializeSettings(settings) {
1276
+ if (settings === void 0) return void 0;
1277
+ const out = {};
1278
+ for (const key of Object.keys(settings)) {
1279
+ const one = settings[key];
1280
+ if (one === void 0) continue;
1281
+ const piece = {
1282
+ ...one.title === void 0 ? {} : { title: one.title },
1283
+ ...one.width === void 0 ? {} : { width: one.width },
1284
+ ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1285
+ ...one.options === void 0 || Object.keys(one.options).length === 0 ? {} : { options: { ...one.options } },
1286
+ ...one.view === void 0 ? {} : { view: one.view }
1287
+ };
1288
+ if (Object.keys(piece).length > 0) out[key] = piece;
1289
+ }
1290
+ return Object.keys(out).length === 0 ? void 0 : out;
831
1291
  }
832
- function graphToTable(graph, context) {
833
- const first = graph.series[0];
834
- if (first === void 0)
835
- return err(`${context.where}: the chart has no series.`);
836
- const format = {
837
- ...graph.currency === void 0 ? {} : { currency: graph.currency },
838
- ...context.locale === void 0 ? {} : { locale: context.locale }
839
- };
840
- return ok({
841
- kind: "TABLE",
842
- columns: [
843
- { label: graph.xLabel ?? "Label" },
844
- ...graph.series.map((series) => ({ label: series.name, numeric: true }))
845
- ],
846
- rows: first.points.map((point, index) => [
847
- { text: point.label },
848
- ...graph.series.map((series) => {
849
- const value = series.points[index]?.value;
850
- return value === void 0 ? { text: "\u2014" } : { text: formatValue(value, graph.valueFormat, format), value };
851
- })
852
- ])
853
- });
1292
+ function widthRange(item) {
1293
+ const min = isWidgetWidth(item.minWidth) ? item.minWidth : MIN_WIDGET_WIDTH;
1294
+ const max = isWidgetWidth(item.maxWidth) ? item.maxWidth : MAX_WIDGET_WIDTH;
1295
+ return { min, max: Math.max(min, max) };
854
1296
  }
855
- function barListToGraph(list, conversion) {
856
- return ok({
857
- kind: "GRAPH",
858
- series: [
859
- {
860
- name: conversion.seriesName ?? "Value",
861
- points: list.items.map((item) => ({
862
- label: item.label,
863
- value: item.value
864
- }))
865
- }
866
- ],
867
- chartType: conversion.chartType,
868
- valueFormat: conversion.valueFormat ?? "number",
869
- ...conversion.currency === void 0 ? {} : { currency: conversion.currency }
870
- });
1297
+ var WIDTH_SHARES = {
1298
+ 2: "1/6",
1299
+ 3: "1/4",
1300
+ 4: "1/3",
1301
+ 5: "5/12",
1302
+ 6: "1/2",
1303
+ 7: "7/12",
1304
+ 8: "2/3",
1305
+ 9: "3/4",
1306
+ 10: "5/6",
1307
+ 11: "11/12",
1308
+ 12: "Full"
1309
+ };
1310
+ function widthShareLabel(width) {
1311
+ return WIDTH_SHARES[width] ?? `${width}/12`;
871
1312
  }
872
- function convertView(widgetKey, view, data, options = {}) {
873
- const conversion = view.convert;
874
- const context = {
875
- where: `Widget ${JSON.stringify(widgetKey)} view ${JSON.stringify(view.key)}`,
876
- ...options.locale === void 0 ? {} : { locale: options.locale }
877
- };
878
- const expected = VIEW_CONVERSIONS[conversion.type]?.from;
879
- if (expected === void 0) {
880
- return err(
881
- `${context.where}: unknown conversion type ${JSON.stringify(conversion.type)}.`
882
- );
1313
+ function isOptionsLocked(item) {
1314
+ return resolveWidgetLock(item).options;
1315
+ }
1316
+ function fillWith(fill, flexible) {
1317
+ const heightToo = fill === "height" || fill === "both";
1318
+ if (flexible) return heightToo ? "both" : "width";
1319
+ return heightToo ? "height" : void 0;
1320
+ }
1321
+ function isFlexible(item, settings) {
1322
+ if (settings?.flexible !== void 0) return settings.flexible;
1323
+ return item.fill === "width" || item.fill === "both";
1324
+ }
1325
+ function applySettings(definition, settings) {
1326
+ if (settings === void 0) return definition;
1327
+ const range = widthRange(definition);
1328
+ const width = settings.width === void 0 ? void 0 : Math.min(range.max, Math.max(range.min, settings.width));
1329
+ const title = settings.title === void 0 || settings.title.trim() === "" ? void 0 : settings.title;
1330
+ const flexibleChanged = settings.flexible !== void 0 && settings.flexible !== isFlexible(definition, void 0);
1331
+ if (title === void 0 && width === void 0 && !flexibleChanged) {
1332
+ return definition;
883
1333
  }
884
- if (data.kind !== expected) {
885
- return err(
886
- `${context.where}: ${conversion.type} reads a ${expected} payload, but the widget returned ${data.kind}.`
1334
+ const next = { ...definition };
1335
+ if (title !== void 0) next.title = title;
1336
+ if (width !== void 0) next.width = width;
1337
+ if (flexibleChanged) {
1338
+ const fill = fillWith(
1339
+ definition.fill,
1340
+ settings.flexible === true
887
1341
  );
1342
+ if (fill === void 0) {
1343
+ delete next.fill;
1344
+ } else {
1345
+ next.fill = fill;
1346
+ }
888
1347
  }
889
- switch (conversion.type) {
890
- case "tableToBarList":
891
- return tableToBarList(data, conversion, context);
892
- case "tableToGraph":
893
- return tableToGraph(data, conversion, context);
894
- case "barListToTable":
895
- return barListToTable(data, conversion);
896
- case "graphToTable":
897
- return graphToTable(data, context);
898
- case "barListToGraph":
899
- return barListToGraph(data, conversion);
900
- }
1348
+ return next;
901
1349
  }
902
- function viewNote(view, data) {
903
- if (data?.kind !== "TABLE") return "";
904
- const conversion = view.convert;
905
- let indexes;
906
- if (conversion.type === "tableToBarList") {
907
- indexes = [conversion.label, conversion.value];
908
- } else if (conversion.type === "tableToGraph") {
909
- indexes = [conversion.label, ...conversion.values];
910
- } else {
911
- return "";
912
- }
913
- const names = indexes.map(
914
- (index) => data.columns[index]?.label ?? `#${index + 1}`
1350
+ function effectiveDefinitions(definitions, layout, options = {}) {
1351
+ return definitions.map(
1352
+ (definition) => options.overrideLocks !== true && isOptionsLocked(definition) ? definition : applySettings(definition, layout.settings?.[definition.key])
915
1353
  );
916
- return `Shows ${names.join(", ")} only.`;
917
1354
  }
918
- function applyView(widget, chosen, options = {}) {
919
- const key = effectiveViewKey(widget.definition, chosen);
920
- const view = findView(widget.definition, key);
921
- if (view === void 0) return widget;
922
- const definition = {
923
- ...widget.definition,
924
- kind: view.kind
925
- };
926
- if (widget.status === "loading") return { ...widget, definition };
927
- if (widget.status === "empty") return { ...widget, definition };
928
- if (widget.status === "error") return { ...widget, definition };
929
- const converted = convertView(
930
- widget.definition.key,
931
- view,
932
- widget.data,
933
- options
934
- );
935
- if (!converted.ok) {
936
- return {
937
- definition,
938
- status: "error",
939
- error: converted.error,
940
- ...widget.options === void 0 ? {} : { options: widget.options }
941
- };
1355
+ function widgetOptionValues(definition, settings) {
1356
+ const declared = definition.options ?? [];
1357
+ const values = {};
1358
+ for (const option of declared) {
1359
+ const picked = settings?.options?.[option.key];
1360
+ const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked)) ?? defaultOptionValue(option);
1361
+ if (value !== void 0) values[option.key] = value;
942
1362
  }
943
- return { ...widget, definition, data: converted.value };
944
- }
945
-
946
- // src/core/options.ts
947
- var DATE_RANGE_PRESETS = [
948
- "today",
949
- "yesterday",
950
- "last7",
951
- "last30",
952
- "last90",
953
- "thisMonth",
954
- "lastMonth",
955
- "thisYear",
956
- "lastYear"
957
- ];
958
- var WIDGET_OPTION_TYPES = [
959
- "choice",
960
- "number",
961
- "boolean",
962
- "text",
963
- "dateRange",
964
- "color",
965
- "sort",
966
- "columns"
967
- ];
968
- var MAX_WIDGET_OPTIONS = 20;
969
- var MAX_TEXT_OPTION_LENGTH = 500;
970
- var OPTION_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,39}$/;
971
- var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
972
- var INTERVAL_PATTERN = /^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/;
973
- var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
974
- function isColumnKeyFor(kind, key) {
975
- if (kind === "TABLE") return /^c\d+$/.test(key);
976
- if (kind === "BAR_LIST") return key === "label" || key === "value";
977
- return false;
1363
+ return values;
978
1364
  }
979
- function isClientApplied(option) {
980
- return (option.type === "sort" || option.type === "columns") && option.apply === "client";
1365
+ function optionValuesFromLayout(definitions, layout, options = {}) {
1366
+ const out = {};
1367
+ for (const definition of definitions) {
1368
+ if (definition.options === void 0 || definition.options.length === 0) {
1369
+ continue;
1370
+ }
1371
+ const locked = options.overrideLocks !== true && isOptionsLocked(definition);
1372
+ out[definition.key] = locked ? {} : layout.settings?.[definition.key]?.options ?? {};
1373
+ }
1374
+ return out;
981
1375
  }
982
- function civilToday(context) {
983
- const now = context.now ?? Date.now();
984
- let timeZone = context.timeZone ?? "UTC";
985
- try {
986
- new Intl.DateTimeFormat("en-US", { timeZone });
987
- } catch {
988
- timeZone = "UTC";
1376
+ function viewValuesFromLayout(definitions, layout, options = {}) {
1377
+ const out = {};
1378
+ for (const definition of definitions) {
1379
+ if (!(definition.views ?? []).some(isProviderView)) continue;
1380
+ const locked = options.overrideLocks !== true && isOptionsLocked(definition);
1381
+ out[definition.key] = locked ? "" : layout.settings?.[definition.key]?.view ?? "";
989
1382
  }
990
- const parts = new Intl.DateTimeFormat("en-US", {
991
- timeZone,
992
- year: "numeric",
993
- month: "2-digit",
994
- day: "2-digit"
995
- }).formatToParts(new Date(now));
996
- const pick = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
997
- return { year: pick("year"), month: pick("month"), day: pick("day") };
1383
+ return out;
998
1384
  }
999
- function toIso(epochMs) {
1000
- return new Date(epochMs).toISOString().slice(0, 10);
1385
+ function sameSettings(a, b) {
1386
+ if (a === b) return true;
1387
+ return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && a?.view === b?.view && sameOptionValues(a?.options ?? {}, b?.options ?? {});
1001
1388
  }
1002
- var DAY_MS = 864e5;
1003
- function parseIsoDay(text) {
1004
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
1005
- if (match === null) return void 0;
1006
- const epoch = Date.UTC(
1007
- Number(match[1]),
1008
- Number(match[2]) - 1,
1009
- Number(match[3])
1010
- );
1011
- return toIso(epoch) === text ? epoch : void 0;
1012
- }
1013
- function isIsoInterval(value) {
1014
- const match = INTERVAL_PATTERN.exec(value);
1015
- if (match === null) return false;
1016
- const start = parseIsoDay(match[1]);
1017
- const end = parseIsoDay(match[2]);
1018
- return start !== void 0 && end !== void 0 && start <= end;
1019
- }
1020
- function isPreset(value) {
1021
- return DATE_RANGE_PRESETS.includes(value);
1022
- }
1023
- function resolveDateRange(value, context = {}) {
1024
- if (isIsoInterval(value)) return value;
1025
- if (!isPreset(value)) return void 0;
1026
- const today = civilToday(context);
1027
- const todayMs = Date.UTC(today.year, today.month - 1, today.day);
1028
- const range = (startMs, endMs) => `${toIso(startMs)}/${toIso(endMs)}`;
1029
- switch (value) {
1030
- case "today":
1031
- return range(todayMs, todayMs);
1032
- case "yesterday":
1033
- return range(todayMs - DAY_MS, todayMs - DAY_MS);
1034
- case "last7":
1035
- return range(todayMs - 6 * DAY_MS, todayMs);
1036
- case "last30":
1037
- return range(todayMs - 29 * DAY_MS, todayMs);
1038
- case "last90":
1039
- return range(todayMs - 89 * DAY_MS, todayMs);
1040
- case "thisMonth":
1041
- return range(Date.UTC(today.year, today.month - 1, 1), todayMs);
1042
- case "lastMonth":
1043
- return range(
1044
- Date.UTC(today.year, today.month - 2, 1),
1045
- Date.UTC(today.year, today.month - 1, 1) - DAY_MS
1046
- );
1047
- case "thisYear":
1048
- return range(Date.UTC(today.year, 0, 1), todayMs);
1049
- case "lastYear":
1050
- return range(
1051
- Date.UTC(today.year - 1, 0, 1),
1052
- Date.UTC(today.year, 0, 1) - DAY_MS
1053
- );
1389
+ function setWidgetSettings(layout, key, patch) {
1390
+ const before = layout.settings?.[key];
1391
+ const draft = { ...before };
1392
+ for (const field of [
1393
+ "title",
1394
+ "width",
1395
+ "flexible",
1396
+ "options",
1397
+ "view"
1398
+ ]) {
1399
+ if (!(field in patch)) continue;
1400
+ const value = patch[field];
1401
+ if (value === void 0) {
1402
+ delete draft[field];
1403
+ } else {
1404
+ draft[field] = value;
1405
+ }
1406
+ }
1407
+ const merged = parseOne(draft);
1408
+ if (sameSettings(before, merged)) return layout;
1409
+ const rest = { ...layout.settings };
1410
+ if (merged === void 0) {
1411
+ delete rest[key];
1412
+ } else {
1413
+ rest[key] = merged;
1054
1414
  }
1415
+ const { settings: _previous, ...others } = layout;
1416
+ return Object.keys(rest).length === 0 ? others : { ...others, settings: rest };
1055
1417
  }
1056
- function onStepGrid(option, value) {
1057
- if (option.step === void 0) return true;
1058
- const steps = (value - option.min) / option.step;
1059
- return Math.abs(steps - Math.round(steps)) < 1e-9;
1418
+ function clearWidgetSettings(layout, key) {
1419
+ if (layout.settings?.[key] === void 0) return layout;
1420
+ return setWidgetSettings(layout, key, {
1421
+ title: void 0,
1422
+ width: void 0,
1423
+ flexible: void 0,
1424
+ options: void 0,
1425
+ view: void 0
1426
+ });
1060
1427
  }
1061
- function parseSortValue(value) {
1062
- const separator = value.lastIndexOf(":");
1063
- if (separator <= 0) return void 0;
1064
- const column = value.slice(0, separator);
1065
- const direction = value.slice(separator + 1);
1066
- if (direction !== "asc" && direction !== "desc") return void 0;
1067
- return { column, direction };
1428
+ function normalizeTitle(title) {
1429
+ return title.normalize("NFKC").trim().toLowerCase();
1068
1430
  }
1069
- function sortFromOptions(definition, values) {
1070
- if (values === void 0) return void 0;
1071
- const sortOption = definition.options?.find(
1072
- (option) => option.type === "sort"
1431
+ function isTitleInUse(definitions, layout, key, title) {
1432
+ const wanted = normalizeTitle(title);
1433
+ if (wanted === "") return false;
1434
+ return effectiveDefinitions(definitions, layout, {
1435
+ overrideLocks: true
1436
+ }).some(
1437
+ (other) => other.key !== key && normalizeTitle(other.title ?? other.key) === wanted
1073
1438
  );
1074
- if (sortOption === void 0) return void 0;
1075
- const chosen = values[sortOption.key];
1076
- return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
1077
1439
  }
1078
- function coerceOptionValue(option, value, context = {}) {
1079
- switch (option.type) {
1080
- case "choice":
1081
- return typeof value === "string" && option.choices.some((choice) => choice.value === value) ? value : void 0;
1082
- case "number":
1083
- return typeof value === "number" && Number.isFinite(value) && value >= option.min && value <= option.max && onStepGrid(option, value) ? value : void 0;
1084
- case "boolean":
1085
- return typeof value === "boolean" ? value : void 0;
1086
- case "text": {
1087
- if (typeof value !== "string") return void 0;
1088
- const trimmed = value.trim();
1089
- return trimmed.length <= option.maxLength && !CONTROL_CHARACTERS.test(trimmed) ? trimmed : void 0;
1090
- }
1091
- case "dateRange": {
1092
- if (typeof value !== "string") return void 0;
1093
- if (isPreset(value) && option.presets !== void 0 && !option.presets.some((preset) => preset.value === value)) {
1094
- return void 0;
1440
+ function uniqueTitle(taken, title) {
1441
+ if (!taken.has(normalizeTitle(title))) return title;
1442
+ for (let n = 2; ; n += 1) {
1443
+ const suffix = ` (#${n})`;
1444
+ const candidate = `${title.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
1445
+ if (!taken.has(normalizeTitle(candidate))) return candidate;
1446
+ }
1447
+ }
1448
+ function normalizeSettings(definitions, layout, options = {}) {
1449
+ const settings = layout.settings;
1450
+ if (settings === void 0) return layout;
1451
+ const cleaned = {};
1452
+ const taken = /* @__PURE__ */ new Set();
1453
+ for (const definition of definitions) {
1454
+ const renamed = settings[definition.key]?.title !== void 0 && (options.overrideLocks === true || !isOptionsLocked(definition));
1455
+ if (!renamed) taken.add(normalizeTitle(definition.title ?? definition.key));
1456
+ }
1457
+ for (const definition of definitions) {
1458
+ const one = settings[definition.key];
1459
+ if (one === void 0) continue;
1460
+ if (options.overrideLocks !== true && isOptionsLocked(definition)) continue;
1461
+ const range = widthRange(definition);
1462
+ const chosen = {};
1463
+ for (const option of definition.options ?? []) {
1464
+ const picked = one.options?.[option.key];
1465
+ if (picked === void 0) continue;
1466
+ const value = coerceOptionValue(option, picked);
1467
+ if (value !== void 0) {
1468
+ chosen[option.key] = option.type === "dateRange" ? picked : value;
1095
1469
  }
1096
- return resolveDateRange(value, context);
1097
- }
1098
- case "color":
1099
- return typeof value === "string" && COLOR_PATTERN.test(value) ? value.toLowerCase() : void 0;
1100
- case "sort": {
1101
- if (typeof value !== "string") return void 0;
1102
- const sort = parseSortValue(value);
1103
- return sort !== void 0 && option.columns.some((column) => column.key === sort.column) ? value : void 0;
1104
1470
  }
1105
- case "columns": {
1106
- if (!Array.isArray(value) || value.length === 0) return void 0;
1107
- const keys = value;
1108
- const allowed = new Set(option.columns.map((c) => c.key));
1109
- const seen = /* @__PURE__ */ new Set();
1110
- for (const key of keys) {
1111
- if (typeof key !== "string" || !allowed.has(key) || seen.has(key)) {
1112
- return void 0;
1113
- }
1114
- seen.add(key);
1471
+ let title = one.title;
1472
+ if (title !== void 0) {
1473
+ title = title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH);
1474
+ if (title === "") {
1475
+ title = void 0;
1476
+ } else {
1477
+ title = uniqueTitle(taken, title);
1478
+ taken.add(normalizeTitle(title));
1115
1479
  }
1116
- return [...seen];
1480
+ }
1481
+ const next = {
1482
+ ...title === void 0 ? {} : { title },
1483
+ ...one.width === void 0 ? {} : { width: Math.min(range.max, Math.max(range.min, one.width)) },
1484
+ ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1485
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
1486
+ ...one.view !== void 0 && (one.view === BASE_VIEW_KEY || findView(definition, one.view) !== void 0) ? { view: one.view } : {}
1487
+ };
1488
+ if (Object.keys(next).length > 0) {
1489
+ cleaned[definition.key] = sameSettings(one, next) ? one : next;
1117
1490
  }
1118
1491
  }
1492
+ const keys = Object.keys(cleaned);
1493
+ const unchanged = keys.length === Object.keys(settings).length && keys.every((key) => cleaned[key] === settings[key]);
1494
+ if (unchanged) return layout;
1495
+ const { settings: _previous, ...others } = layout;
1496
+ return keys.length === 0 ? others : { ...others, settings: cleaned };
1119
1497
  }
1120
- function defaultOptionValue(option, context = {}) {
1121
- switch (option.type) {
1122
- case "dateRange":
1123
- return resolveDateRange(option.default, context);
1124
- case "sort":
1125
- return option.default;
1126
- case "columns":
1127
- return [...option.default];
1128
- default:
1129
- return option.default;
1130
- }
1498
+
1499
+ // src/core/clones.ts
1500
+ var MAX_PARSED_CLONES = 500;
1501
+ function isRecord2(value) {
1502
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1131
1503
  }
1132
- var NO_VALUES = Object.freeze({});
1133
- function resolveOptionValues(definition, chosen, context = {}) {
1134
- const options = definition.options;
1135
- if (options === void 0 || options.length === 0) return NO_VALUES;
1136
- const values = {};
1137
- for (const option of options) {
1138
- const picked = chosen?.[option.key];
1139
- const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked, context)) ?? defaultOptionValue(option, context);
1140
- if (value !== void 0) values[option.key] = value;
1504
+ function parseClones(raw) {
1505
+ if (!Array.isArray(raw)) return void 0;
1506
+ const seen = /* @__PURE__ */ new Set();
1507
+ const clones = [];
1508
+ for (const entry of raw.slice(0, MAX_PARSED_CLONES)) {
1509
+ if (!isRecord2(entry)) continue;
1510
+ const key = entry["key"];
1511
+ const from = entry["from"];
1512
+ if (typeof key !== "string" || typeof from !== "string" || key === "" || from === "" || key === from || seen.has(key)) {
1513
+ continue;
1514
+ }
1515
+ seen.add(key);
1516
+ clones.push({ key, from });
1141
1517
  }
1142
- return values;
1518
+ return clones.length === 0 ? void 0 : clones;
1143
1519
  }
1144
- function sameValue(a, b) {
1145
- if (Array.isArray(a) && Array.isArray(b)) {
1146
- return a.length === b.length && a.every((item, i) => item === b[i]);
1147
- }
1148
- return a === b;
1520
+ function serializeClones(clones) {
1521
+ if (clones === void 0 || clones.length === 0) return void 0;
1522
+ return clones.map(({ key, from }) => ({ key, from }));
1149
1523
  }
1150
- function sameOptionValues(a, b) {
1151
- const keys = Object.keys({ ...a, ...b });
1152
- return keys.every((key) => sameValue(a[key], b[key]));
1524
+ function cloneNumber(key) {
1525
+ const match = /#(\d+)$/.exec(key);
1526
+ return match === null ? void 0 : Number(match[1]);
1153
1527
  }
1154
- function optionsCacheSuffix(definition, values, context = {}) {
1155
- const parts = [];
1156
- for (const option of definition.options ?? []) {
1157
- if (isClientApplied(option)) continue;
1158
- const value = values[option.key];
1159
- if (sameValue(value, defaultOptionValue(option, context))) continue;
1160
- parts.push(
1161
- `${option.key}=${encodeURIComponent(JSON.stringify(value ?? null))}`
1162
- );
1528
+ function cleanClones(definitions, clones, options = {}) {
1529
+ if (clones === void 0) return void 0;
1530
+ const byKey = new Map(definitions.map((d) => [d.key, d]));
1531
+ const seen = /* @__PURE__ */ new Set();
1532
+ const kept = [];
1533
+ for (const clone of clones) {
1534
+ if (seen.has(clone.key)) continue;
1535
+ const original = byKey.get(clone.from);
1536
+ if (original === void 0 || original.sourceKey !== void 0) continue;
1537
+ const existing = byKey.get(clone.key);
1538
+ if (existing !== void 0 && existing.sourceKey !== clone.from) continue;
1539
+ if (options.overrideLocks !== true && resolveWidgetLock(original).clone) {
1540
+ continue;
1541
+ }
1542
+ seen.add(clone.key);
1543
+ kept.push(clone);
1163
1544
  }
1164
- parts.sort();
1165
- return parts.length === 0 ? "" : `?${parts.join("&")}`;
1545
+ if (kept.length === 0) return void 0;
1546
+ return kept.length === clones.length && kept.every((c, i) => c === clones[i]) ? clones : kept;
1166
1547
  }
1167
- function tableColumnIndex(key) {
1168
- const match = /^c(\d+)$/.exec(key);
1169
- return match === null ? void 0 : Number(match[1]);
1548
+ function defaultCloneTitle(original, clone, index) {
1549
+ const base = original.title ?? original.key;
1550
+ const suffix = ` (#${cloneNumber(clone.key) ?? index + 1})`;
1551
+ return `${base.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
1170
1552
  }
1171
- function sortBarItems(items, sort) {
1172
- const sign = sort.direction === "desc" ? -1 : 1;
1173
- return items.map((item, index) => ({ item, index })).sort((a, b) => {
1174
- const order = sort.column === "value" ? a.item.value - b.item.value : a.item.label.localeCompare(b.item.label);
1175
- return order * sign || a.index - b.index;
1176
- }).map(({ item }) => item);
1177
- }
1178
- function applyClientOptions(definition, data, values) {
1179
- const options = definition.options ?? [];
1180
- const sortOption = options.find(
1181
- (o) => o.type === "sort" && o.apply === "client"
1182
- );
1183
- const columnsOption = options.find(
1184
- (o) => o.type === "columns" && o.apply === "client"
1553
+ function withClones(definitions, layout, options = {}) {
1554
+ const valid = cleanClones(
1555
+ definitions,
1556
+ layout.clones,
1557
+ options
1185
1558
  );
1186
- const sortValue = sortOption === void 0 ? void 0 : values[sortOption.key];
1187
- const columnsValue = columnsOption === void 0 ? void 0 : values[columnsOption.key];
1188
- let next = data;
1189
- if (typeof sortValue === "string") {
1190
- const sort = parseSortValue(sortValue);
1191
- if (sort !== void 0 && next.kind === "BAR_LIST") {
1192
- if (sort.column === "label" || sort.column === "value") {
1193
- next = { ...next, items: sortBarItems(next.items, sort) };
1194
- }
1195
- } else if (sort !== void 0 && next.kind === "TABLE") {
1196
- const index = tableColumnIndex(sort.column);
1197
- if (index !== void 0 && index < next.columns.length) {
1198
- const sorted = queryRows(tableDetailData(next), {
1199
- page: 1,
1200
- pageSize: Math.max(1, next.rows.length),
1201
- sort
1202
- });
1203
- if (sorted.ok) next = { ...next, rows: sorted.value.rows };
1204
- }
1205
- }
1206
- }
1207
- if (Array.isArray(columnsValue) && next.kind === "TABLE") {
1208
- const table = next;
1209
- const indexes = columnsValue.map(tableColumnIndex).filter(
1210
- (index) => index !== void 0 && index < table.columns.length
1211
- );
1212
- if (indexes.length > 0) {
1213
- next = {
1214
- ...table,
1215
- columns: indexes.map((i) => table.columns[i]),
1216
- rows: table.rows.map((row) => indexes.map((i) => row[i]))
1217
- };
1218
- }
1559
+ if (valid === void 0) return definitions;
1560
+ const present = new Set(definitions.map((d) => d.key));
1561
+ const toAdd = valid.filter((c) => !present.has(c.key));
1562
+ if (toAdd.length === 0) return definitions;
1563
+ const out = [];
1564
+ for (const definition of definitions) {
1565
+ out.push(definition);
1566
+ toAdd.forEach((clone, index) => {
1567
+ if (clone.from !== definition.key) return;
1568
+ const copy = { ...definition };
1569
+ copy.key = clone.key;
1570
+ copy.sourceKey = definition.key;
1571
+ copy.title = defaultCloneTitle(definition, clone, index);
1572
+ delete copy.locked;
1573
+ out.push(copy);
1574
+ });
1219
1575
  }
1220
- return next;
1576
+ return out;
1221
1577
  }
1222
1578
 
1223
- // src/core/settings.ts
1224
- var MAX_WIDGET_TITLE_LENGTH = 80;
1225
- var MAX_PARSED_SETTINGS = 500;
1226
- var MAX_COLUMN_LIST = 100;
1227
- var MAX_VALUE_LENGTH = 500;
1228
- function isRecord(value) {
1229
- return typeof value === "object" && value !== null && !Array.isArray(value);
1579
+ // src/core/compat.ts
1580
+ function isStringArray(value) {
1581
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
1230
1582
  }
1231
- function parseOptionValue(value) {
1232
- if (typeof value === "string") {
1233
- return value.length <= MAX_VALUE_LENGTH ? value : void 0;
1583
+ function upgradeLegacyWidgetData(value) {
1584
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1585
+ return value;
1234
1586
  }
1235
- if (typeof value === "number")
1236
- return Number.isFinite(value) ? value : void 0;
1237
- if (typeof value === "boolean") return value;
1238
- if (Array.isArray(value) && value.length <= MAX_COLUMN_LIST) {
1239
- return value.every(
1240
- (entry) => typeof entry === "string" && entry.length <= MAX_VALUE_LENGTH
1241
- ) ? value : void 0;
1587
+ const record = value;
1588
+ if (record["kind"] === "TABLE" && isStringArray(record["columns"]) && Array.isArray(record["rows"]) && record["rows"].every(isStringArray)) {
1589
+ const rows = record["rows"];
1590
+ return {
1591
+ ...record,
1592
+ columns: record["columns"].map((label) => ({ label })),
1593
+ rows: rows.map((row) => row.map((text) => ({ text })))
1594
+ };
1242
1595
  }
1243
- return void 0;
1596
+ if (record["kind"] === "GRAPH" && Array.isArray(record["points"]) && record["series"] === void 0) {
1597
+ const { points, ...rest } = record;
1598
+ return {
1599
+ ...rest,
1600
+ series: [{ name: "Value", points }],
1601
+ chartType: "bar",
1602
+ valueFormat: "number"
1603
+ };
1604
+ }
1605
+ return value;
1244
1606
  }
1245
- function parseOne(raw) {
1246
- if (!isRecord(raw)) return void 0;
1247
- const title = raw["title"];
1248
- const width = raw["width"];
1249
- const flexible = raw["flexible"];
1250
- const options = raw["options"];
1251
- const view = raw["view"];
1252
- const cleanTitle = typeof title === "string" ? title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH) : "";
1253
- const chosen = {};
1254
- if (isRecord(options)) {
1255
- for (const [key, value] of Object.entries(options)) {
1256
- const parsed = parseOptionValue(value);
1257
- if (parsed !== void 0) chosen[key] = parsed;
1258
- }
1607
+ function fromLegacyDefinition(legacy) {
1608
+ if (!isWidgetKind(legacy.viewType)) {
1609
+ return null;
1259
1610
  }
1260
- const settings = {
1261
- ...cleanTitle === "" ? {} : { title: cleanTitle },
1262
- ...isWidgetWidth(width) ? { width } : {},
1263
- ...typeof flexible === "boolean" ? { flexible } : {},
1264
- ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
1265
- ...typeof view === "string" && view !== "" && view.length <= 80 ? { view } : {}
1611
+ return {
1612
+ key: legacy.widgetKey,
1613
+ title: legacy.title,
1614
+ ...legacy.description === null ? {} : { description: legacy.description },
1615
+ kind: legacy.viewType,
1616
+ sortOrder: legacy.sortOrder
1266
1617
  };
1267
- return Object.keys(settings).length === 0 ? void 0 : settings;
1268
1618
  }
1269
- function parseSettings(raw) {
1270
- if (!isRecord(raw)) return void 0;
1271
- const parsed = {};
1272
- for (const [key, value] of Object.entries(raw).slice(
1273
- 0,
1274
- MAX_PARSED_SETTINGS
1275
- )) {
1276
- if (key === "") continue;
1277
- const one = parseOne(value);
1278
- if (one !== void 0) parsed[key] = one;
1619
+
1620
+ // src/core/layout.ts
1621
+ var MAX_PAGE_TITLE_LENGTH = 30;
1622
+ var MAX_PARSED_PAGES = 50;
1623
+ var MAX_PAGE_ROWS = 20;
1624
+ var EMPTY_LAYOUT = Object.freeze({
1625
+ order: Object.freeze([]),
1626
+ hidden: Object.freeze([]),
1627
+ minimized: Object.freeze([])
1628
+ });
1629
+ function uniqueStrings(value) {
1630
+ if (!Array.isArray(value)) return [];
1631
+ const seen = /* @__PURE__ */ new Set();
1632
+ for (const entry of value) {
1633
+ if (typeof entry === "string" && entry !== "") seen.add(entry);
1279
1634
  }
1280
- return Object.keys(parsed).length === 0 ? void 0 : parsed;
1635
+ return [...seen];
1281
1636
  }
1282
- function serializeSettings(settings) {
1283
- if (settings === void 0) return void 0;
1284
- const out = {};
1285
- for (const key of Object.keys(settings)) {
1286
- const one = settings[key];
1287
- if (one === void 0) continue;
1288
- const piece = {
1289
- ...one.title === void 0 ? {} : { title: one.title },
1290
- ...one.width === void 0 ? {} : { width: one.width },
1291
- ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1292
- ...one.options === void 0 || Object.keys(one.options).length === 0 ? {} : { options: { ...one.options } },
1293
- ...one.view === void 0 ? {} : { view: one.view }
1637
+ function parseLayout(raw) {
1638
+ let value = raw;
1639
+ if (typeof raw === "string") {
1640
+ try {
1641
+ value = JSON.parse(raw);
1642
+ } catch {
1643
+ return EMPTY_LAYOUT;
1644
+ }
1645
+ }
1646
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1647
+ return EMPTY_LAYOUT;
1648
+ }
1649
+ const record = value;
1650
+ const order = uniqueStrings(record["order"]);
1651
+ const hidden = uniqueStrings(record["hidden"]);
1652
+ const minimized = uniqueStrings(record["minimized"]);
1653
+ const pages = parsePages(record["pages"]);
1654
+ const settings = parseSettings(
1655
+ record["settings"]
1656
+ );
1657
+ const clones = parseClones(
1658
+ record["clones"]
1659
+ );
1660
+ const extras = {
1661
+ ...settings === void 0 ? {} : { settings },
1662
+ ...clones === void 0 ? {} : { clones }
1663
+ };
1664
+ if (pages.length > 0) {
1665
+ return {
1666
+ order: EMPTY_LAYOUT.order,
1667
+ hidden: EMPTY_LAYOUT.hidden,
1668
+ minimized: EMPTY_LAYOUT.minimized,
1669
+ pages,
1670
+ ...extras
1294
1671
  };
1295
- if (Object.keys(piece).length > 0) out[key] = piece;
1296
1672
  }
1297
- return Object.keys(out).length === 0 ? void 0 : out;
1673
+ if (order.length === 0 && hidden.length === 0 && minimized.length === 0) {
1674
+ return Object.keys(extras).length === 0 ? EMPTY_LAYOUT : { ...EMPTY_LAYOUT, ...extras };
1675
+ }
1676
+ return { order, hidden, minimized, ...extras };
1298
1677
  }
1299
- function widthRange(item) {
1300
- const min = isWidgetWidth(item.minWidth) ? item.minWidth : MIN_WIDGET_WIDTH;
1301
- const max = isWidgetWidth(item.maxWidth) ? item.maxWidth : MAX_WIDGET_WIDTH;
1302
- return { min, max: Math.max(min, max) };
1678
+ function parsePages(value) {
1679
+ if (!Array.isArray(value)) return [];
1680
+ const pages = [];
1681
+ const keys = /* @__PURE__ */ new Set();
1682
+ for (const entry of value.slice(0, MAX_PARSED_PAGES)) {
1683
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
1684
+ continue;
1685
+ }
1686
+ const record = entry;
1687
+ const key = record["key"];
1688
+ if (typeof key !== "string" || key === "" || keys.has(key)) continue;
1689
+ keys.add(key);
1690
+ const rawTitle = record["title"];
1691
+ const title = typeof rawTitle === "string" && rawTitle.trim() !== "" ? rawTitle.trim().slice(0, MAX_PAGE_TITLE_LENGTH) : `Page ${pages.length + 1}`;
1692
+ const maxRows = record["maxRows"];
1693
+ const held = /* @__PURE__ */ new Set();
1694
+ for (const earlier of pages) {
1695
+ for (const k of [...earlier.order, ...earlier.hidden]) held.add(k);
1696
+ }
1697
+ const keep = (list) => uniqueStrings(list).filter((k) => !held.has(k));
1698
+ pages.push({
1699
+ key,
1700
+ title,
1701
+ order: keep(record["order"]),
1702
+ hidden: keep(record["hidden"]),
1703
+ minimized: keep(record["minimized"]),
1704
+ ...typeof maxRows === "number" && Number.isInteger(maxRows) && maxRows >= 1 && maxRows <= MAX_PAGE_ROWS ? { maxRows } : {}
1705
+ });
1706
+ }
1707
+ return pages;
1303
1708
  }
1304
- var WIDTH_SHARES = {
1305
- 2: "1/6",
1306
- 3: "1/4",
1307
- 4: "1/3",
1308
- 5: "5/12",
1309
- 6: "1/2",
1310
- 7: "7/12",
1311
- 8: "2/3",
1312
- 9: "3/4",
1313
- 10: "5/6",
1314
- 11: "11/12",
1315
- 12: "Full"
1316
- };
1317
- function widthShareLabel(width) {
1318
- return WIDTH_SHARES[width] ?? `${width}/12`;
1709
+ function serializeLayout(layout) {
1710
+ return JSON.stringify({
1711
+ order: [...layout.order],
1712
+ hidden: [...layout.hidden],
1713
+ minimized: [...layout.minimized],
1714
+ ...layout.pages === void 0 || layout.pages.length === 0 ? {} : {
1715
+ pages: layout.pages.map((page) => ({
1716
+ key: page.key,
1717
+ title: page.title,
1718
+ order: [...page.order],
1719
+ hidden: [...page.hidden],
1720
+ minimized: [...page.minimized],
1721
+ ...page.maxRows === void 0 ? {} : { maxRows: page.maxRows }
1722
+ }))
1723
+ },
1724
+ ...serializeSettings(layout.settings) === void 0 ? {} : { settings: serializeSettings(layout.settings) },
1725
+ ...serializeClones(layout.clones) === void 0 ? {} : { clones: serializeClones(layout.clones) }
1726
+ });
1319
1727
  }
1320
- function isOptionsLocked(item) {
1321
- return resolveWidgetLock(item).options;
1322
- }
1323
- function fillWith(fill, flexible) {
1324
- const heightToo = fill === "height" || fill === "both";
1325
- if (flexible) return heightToo ? "both" : "width";
1326
- return heightToo ? "height" : void 0;
1728
+ function bySortOrder(items) {
1729
+ return items.map((item, index) => ({ item, index })).sort(
1730
+ (a, b) => (a.item.sortOrder ?? 0) - (b.item.sortOrder ?? 0) || a.index - b.index
1731
+ ).map(({ item }) => item);
1327
1732
  }
1328
- function isFlexible(item, settings) {
1329
- if (settings?.flexible !== void 0) return settings.flexible;
1330
- return item.fill === "width" || item.fill === "both";
1733
+ function isPinned(item) {
1734
+ return resolveWidgetLock(item).move;
1331
1735
  }
1332
- function applySettings(definition, settings) {
1333
- if (settings === void 0) return definition;
1334
- const range = widthRange(definition);
1335
- const width = settings.width === void 0 ? void 0 : Math.min(range.max, Math.max(range.min, settings.width));
1336
- const title = settings.title === void 0 || settings.title.trim() === "" ? void 0 : settings.title;
1337
- const flexibleChanged = settings.flexible !== void 0 && settings.flexible !== isFlexible(definition, void 0);
1338
- if (title === void 0 && width === void 0 && !flexibleChanged) {
1339
- return definition;
1340
- }
1341
- const next = { ...definition };
1342
- if (title !== void 0) next.title = title;
1343
- if (width !== void 0) next.width = width;
1344
- if (flexibleChanged) {
1345
- const fill = fillWith(
1346
- definition.fill,
1347
- settings.flexible === true
1348
- );
1349
- if (fill === void 0) {
1350
- delete next.fill;
1351
- } else {
1352
- next.fill = fill;
1736
+ function viewerOrder(visible, layout) {
1737
+ const byKey = new Map(visible.map((d) => [d.key, d]));
1738
+ const ordered = [];
1739
+ for (const key of layout.order) {
1740
+ const definition = byKey.get(key);
1741
+ if (definition !== void 0) {
1742
+ ordered.push(definition);
1743
+ byKey.delete(key);
1353
1744
  }
1354
1745
  }
1355
- return next;
1746
+ for (const definition of bySortOrder(visible)) {
1747
+ if (byKey.has(definition.key)) ordered.push(definition);
1748
+ }
1749
+ return ordered;
1356
1750
  }
1357
- function effectiveDefinitions(definitions, layout, options = {}) {
1358
- return definitions.map(
1359
- (definition) => options.overrideLocks !== true && isOptionsLocked(definition) ? definition : applySettings(definition, layout.settings?.[definition.key])
1751
+ function placeAroundPinned(visible, free) {
1752
+ const slots = bySortOrder(visible).map(
1753
+ (item) => isPinned(item) ? item : void 0
1360
1754
  );
1755
+ let next = 0;
1756
+ return slots.map((pinned) => pinned ?? free[next++]);
1361
1757
  }
1362
- function widgetOptionValues(definition, settings) {
1363
- const declared = definition.options ?? [];
1364
- const values = {};
1365
- for (const option of declared) {
1366
- const picked = settings?.options?.[option.key];
1367
- const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked)) ?? defaultOptionValue(option);
1368
- if (value !== void 0) values[option.key] = value;
1369
- }
1370
- return values;
1371
- }
1372
- function optionValuesFromLayout(definitions, layout, options = {}) {
1373
- const out = {};
1374
- for (const definition of definitions) {
1375
- if (definition.options === void 0 || definition.options.length === 0) {
1376
- continue;
1377
- }
1378
- const locked = options.overrideLocks !== true && isOptionsLocked(definition);
1379
- out[definition.key] = locked ? {} : layout.settings?.[definition.key]?.options ?? {};
1380
- }
1381
- return out;
1758
+ function visibleWidgets(definitions, layout) {
1759
+ const hidden = new Set(layout.hidden);
1760
+ const visible = definitions.filter((d) => !hidden.has(d.key));
1761
+ const ordered = viewerOrder(visible, layout);
1762
+ if (!visible.some(isPinned)) return ordered;
1763
+ return placeAroundPinned(
1764
+ visible,
1765
+ ordered.filter((item) => !isPinned(item))
1766
+ );
1382
1767
  }
1383
- function sameSettings(a, b) {
1384
- if (a === b) return true;
1385
- return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && a?.view === b?.view && sameOptionValues(a?.options ?? {}, b?.options ?? {});
1768
+ function hiddenWidgets(definitions, layout) {
1769
+ const hidden = new Set(layout.hidden);
1770
+ return bySortOrder(definitions.filter((d) => hidden.has(d.key)));
1386
1771
  }
1387
- function setWidgetSettings(layout, key, patch) {
1388
- const before = layout.settings?.[key];
1389
- const draft = { ...before };
1390
- for (const field of [
1391
- "title",
1392
- "width",
1393
- "flexible",
1394
- "options",
1395
- "view"
1396
- ]) {
1397
- if (!(field in patch)) continue;
1398
- const value = patch[field];
1399
- if (value === void 0) {
1400
- delete draft[field];
1401
- } else {
1402
- draft[field] = value;
1403
- }
1404
- }
1405
- const merged = parseOne(draft);
1406
- if (sameSettings(before, merged)) return layout;
1407
- const rest = { ...layout.settings };
1408
- if (merged === void 0) {
1409
- delete rest[key];
1410
- } else {
1411
- rest[key] = merged;
1772
+ function moveWidget(definitions, layout, key, toIndex) {
1773
+ const current = visibleWidgets(definitions, layout);
1774
+ const fromIndex = current.findIndex((d) => d.key === key);
1775
+ const mover = current[fromIndex];
1776
+ if (mover === void 0 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex || isPinned(mover) || isPinned(current[toIndex])) {
1777
+ return layout;
1412
1778
  }
1413
- const { settings: _previous, ...others } = layout;
1414
- return Object.keys(rest).length === 0 ? others : { ...others, settings: rest };
1779
+ const freeSlots = freeSlotIndexes(current);
1780
+ const free = current.filter((item) => !isPinned(item));
1781
+ const fromFree = freeSlots.indexOf(fromIndex);
1782
+ const toFree = freeSlots.indexOf(toIndex);
1783
+ free.splice(fromFree, 1);
1784
+ free.splice(toFree, 0, mover);
1785
+ const next = placeAroundPinned(current, free);
1786
+ return { ...layout, order: next.map((d) => d.key) };
1415
1787
  }
1416
- function clearWidgetSettings(layout, key) {
1417
- if (layout.settings?.[key] === void 0) return layout;
1418
- return setWidgetSettings(layout, key, {
1419
- title: void 0,
1420
- width: void 0,
1421
- flexible: void 0,
1422
- options: void 0,
1423
- view: void 0
1788
+ function freeSlotIndexes(ordered) {
1789
+ const slots = [];
1790
+ ordered.forEach((item, index) => {
1791
+ if (!isPinned(item)) slots.push(index);
1424
1792
  });
1793
+ return slots;
1425
1794
  }
1426
- function normalizeTitle(title) {
1427
- return title.normalize("NFKC").trim().toLowerCase();
1428
- }
1429
- function isTitleInUse(definitions, layout, key, title) {
1430
- const wanted = normalizeTitle(title);
1431
- if (wanted === "") return false;
1432
- return effectiveDefinitions(definitions, layout, {
1433
- overrideLocks: true
1434
- }).some(
1435
- (other) => other.key !== key && normalizeTitle(other.title ?? other.key) === wanted
1795
+ function moveWidgetBy(definitions, layout, key, offset) {
1796
+ const current = visibleWidgets(definitions, layout);
1797
+ const fromIndex = current.findIndex((d) => d.key === key);
1798
+ if (fromIndex === -1) return layout;
1799
+ const freeSlots = freeSlotIndexes(current);
1800
+ const fromFree = freeSlots.indexOf(fromIndex);
1801
+ if (fromFree === -1) return layout;
1802
+ const toFree = Math.max(
1803
+ 0,
1804
+ Math.min(freeSlots.length - 1, fromFree + Math.trunc(offset))
1436
1805
  );
1806
+ return moveWidget(definitions, layout, key, freeSlots[toFree]);
1437
1807
  }
1438
- function uniqueTitle(taken, title) {
1439
- if (!taken.has(normalizeTitle(title))) return title;
1440
- for (let n = 2; ; n += 1) {
1441
- const suffix = ` (#${n})`;
1442
- const candidate = `${title.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
1443
- if (!taken.has(normalizeTitle(candidate))) return candidate;
1808
+ function moveWidgetUp(definitions, layout, key) {
1809
+ return moveWidgetBy(definitions, layout, key, -1);
1810
+ }
1811
+ function moveWidgetDown(definitions, layout, key) {
1812
+ return moveWidgetBy(definitions, layout, key, 1);
1813
+ }
1814
+ function isHidden(layout, key) {
1815
+ return layout.hidden.includes(key);
1816
+ }
1817
+ function isMinimized(layout, key) {
1818
+ return layout.minimized.includes(key);
1819
+ }
1820
+ function hideWidget(layout, key) {
1821
+ if (isHidden(layout, key)) return layout;
1822
+ return { ...layout, hidden: [...layout.hidden, key] };
1823
+ }
1824
+ function showWidget(layout, key) {
1825
+ if (!isHidden(layout, key)) return layout;
1826
+ return {
1827
+ ...layout,
1828
+ hidden: layout.hidden.filter((k) => k !== key),
1829
+ minimized: layout.minimized.filter((k) => k !== key)
1830
+ };
1831
+ }
1832
+ function toggleHidden(layout, key) {
1833
+ return isHidden(layout, key) ? showWidget(layout, key) : hideWidget(layout, key);
1834
+ }
1835
+ function minimizeWidget(layout, key) {
1836
+ if (isMinimized(layout, key)) return layout;
1837
+ return { ...layout, minimized: [...layout.minimized, key] };
1838
+ }
1839
+ function restoreWidget(layout, key) {
1840
+ if (!isMinimized(layout, key)) return layout;
1841
+ return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
1842
+ }
1843
+ function toggleMinimized(layout, key) {
1844
+ return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
1845
+ }
1846
+ function pruneLayout(definitions, layout) {
1847
+ const known = new Set(definitions.map((d) => d.key));
1848
+ const keep = (keys) => keys.every((k) => known.has(k)) ? keys : keys.filter((k) => known.has(k));
1849
+ return mapLayoutLists(layout, (lists) => ({
1850
+ order: keep(lists.order),
1851
+ hidden: keep(lists.hidden),
1852
+ minimized: keep(lists.minimized)
1853
+ }));
1854
+ }
1855
+ function mapLayoutLists(layout, change) {
1856
+ const top = change(layout);
1857
+ const pages = layout.pages?.map((page) => {
1858
+ const next = change(page);
1859
+ return next.order === page.order && next.hidden === page.hidden && next.minimized === page.minimized ? page : { ...page, ...next };
1860
+ });
1861
+ const pagesChanged = pages !== void 0 && pages.some((page, i) => page !== layout.pages?.[i]);
1862
+ if (top.order === layout.order && top.hidden === layout.hidden && top.minimized === layout.minimized && !pagesChanged) {
1863
+ return layout;
1444
1864
  }
1865
+ return {
1866
+ ...layout,
1867
+ ...top,
1868
+ ...pages === void 0 ? {} : { pages }
1869
+ };
1445
1870
  }
1446
- function normalizeSettings(definitions, layout, options = {}) {
1447
- const settings = layout.settings;
1448
- if (settings === void 0) return layout;
1449
- const cleaned = {};
1450
- const taken = /* @__PURE__ */ new Set();
1871
+ function enforceLocks(definitions, layout, options = {}) {
1872
+ if (options.overrideLocks === true) return layout;
1873
+ const noMove = /* @__PURE__ */ new Set();
1874
+ const noHide = /* @__PURE__ */ new Set();
1875
+ const noMinimize = /* @__PURE__ */ new Set();
1451
1876
  for (const definition of definitions) {
1452
- const renamed = settings[definition.key]?.title !== void 0 && (options.overrideLocks === true || !isOptionsLocked(definition));
1453
- if (!renamed) taken.add(normalizeTitle(definition.title ?? definition.key));
1877
+ const lock = resolveWidgetLock(definition);
1878
+ if (lock.move) noMove.add(definition.key);
1879
+ if (lock.hide) noHide.add(definition.key);
1880
+ if (lock.minimize) noMinimize.add(definition.key);
1454
1881
  }
1455
- for (const definition of definitions) {
1456
- const one = settings[definition.key];
1457
- if (one === void 0) continue;
1458
- if (options.overrideLocks !== true && isOptionsLocked(definition)) continue;
1459
- const range = widthRange(definition);
1460
- const chosen = {};
1461
- for (const option of definition.options ?? []) {
1462
- const picked = one.options?.[option.key];
1463
- if (picked === void 0) continue;
1464
- const value = coerceOptionValue(option, picked);
1465
- if (value !== void 0) {
1466
- chosen[option.key] = option.type === "dateRange" ? picked : value;
1467
- }
1468
- }
1469
- let title = one.title;
1470
- if (title !== void 0) {
1471
- title = title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH);
1472
- if (title === "") {
1473
- title = void 0;
1474
- } else {
1475
- title = uniqueTitle(taken, title);
1476
- taken.add(normalizeTitle(title));
1477
- }
1478
- }
1479
- const next = {
1480
- ...title === void 0 ? {} : { title },
1481
- ...one.width === void 0 ? {} : { width: Math.min(range.max, Math.max(range.min, one.width)) },
1482
- ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1483
- ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
1484
- ...one.view !== void 0 && (one.view === BASE_VIEW_KEY || findView(definition, one.view) !== void 0) ? { view: one.view } : {}
1485
- };
1486
- if (Object.keys(next).length > 0) {
1487
- cleaned[definition.key] = sameSettings(one, next) ? one : next;
1488
- }
1489
- }
1490
- const keys = Object.keys(cleaned);
1491
- const unchanged = keys.length === Object.keys(settings).length && keys.every((key) => cleaned[key] === settings[key]);
1492
- if (unchanged) return layout;
1493
- const { settings: _previous, ...others } = layout;
1494
- return keys.length === 0 ? others : { ...others, settings: cleaned };
1882
+ const without2 = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
1883
+ return mapLayoutLists(layout, (lists) => ({
1884
+ order: without2(lists.order, noMove),
1885
+ hidden: without2(lists.hidden, noHide),
1886
+ minimized: without2(lists.minimized, noMinimize)
1887
+ }));
1495
1888
  }
1496
1889
 
1497
- // src/core/layout.ts
1498
- var MAX_PAGE_TITLE_LENGTH = 30;
1499
- var MAX_PARSED_PAGES = 50;
1500
- var MAX_PAGE_ROWS = 20;
1501
- var EMPTY_LAYOUT = Object.freeze({
1502
- order: Object.freeze([]),
1503
- hidden: Object.freeze([]),
1504
- minimized: Object.freeze([])
1505
- });
1506
- function uniqueStrings(value) {
1507
- if (!Array.isArray(value)) return [];
1508
- const seen = /* @__PURE__ */ new Set();
1509
- for (const entry of value) {
1510
- if (typeof entry === "string" && entry !== "") seen.add(entry);
1511
- }
1512
- return [...seen];
1890
+ // src/core/pages.ts
1891
+ var DEFAULT_PAGE_KEY = "default";
1892
+ var DEFAULT_PAGE_ROWS = 4;
1893
+ function rowsAllowed(page, options) {
1894
+ const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
1895
+ return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
1513
1896
  }
1514
- function parseLayout(raw) {
1515
- let value = raw;
1516
- if (typeof raw === "string") {
1517
- try {
1518
- value = JSON.parse(raw);
1519
- } catch {
1520
- return EMPTY_LAYOUT;
1521
- }
1522
- }
1523
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1524
- return EMPTY_LAYOUT;
1897
+ function hasPages(layout) {
1898
+ return layout.pages !== void 0 && layout.pages.length > 0;
1899
+ }
1900
+ function pageList(layout) {
1901
+ if (layout.pages !== void 0 && layout.pages.length > 0) {
1902
+ return layout.pages;
1525
1903
  }
1526
- const record = value;
1527
- const order = uniqueStrings(record["order"]);
1528
- const hidden = uniqueStrings(record["hidden"]);
1529
- const minimized = uniqueStrings(record["minimized"]);
1530
- const pages = parsePages(record["pages"]);
1531
- const settings = parseSettings(
1532
- record["settings"]
1904
+ return [
1905
+ {
1906
+ key: DEFAULT_PAGE_KEY,
1907
+ title: "Page 1",
1908
+ order: layout.order,
1909
+ hidden: layout.hidden,
1910
+ minimized: layout.minimized
1911
+ }
1912
+ ];
1913
+ }
1914
+ function withExplicitPages(layout) {
1915
+ if (hasPages(layout)) return layout;
1916
+ return {
1917
+ ...layout,
1918
+ order: [],
1919
+ hidden: [],
1920
+ minimized: [],
1921
+ pages: pageList(layout)
1922
+ };
1923
+ }
1924
+ function sameKeys(a, b) {
1925
+ return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
1926
+ }
1927
+ function pageLayout(layout, pageKey) {
1928
+ if (!hasPages(layout))
1929
+ return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
1930
+ const page = layout.pages?.find(
1931
+ (p) => p.key === pageKey
1533
1932
  );
1534
- const withSettings = settings === void 0 ? {} : { settings };
1535
- if (pages.length > 0) {
1933
+ if (page === void 0) return void 0;
1934
+ const { pages, ...rest } = layout;
1935
+ return {
1936
+ ...rest,
1937
+ order: page.order,
1938
+ hidden: page.hidden,
1939
+ minimized: page.minimized
1940
+ };
1941
+ }
1942
+ function withPageLayout(layout, pageKey, next) {
1943
+ const current = pageLayout(layout, pageKey);
1944
+ if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
1945
+ return layout;
1946
+ }
1947
+ if (!hasPages(layout)) {
1536
1948
  return {
1537
- order: EMPTY_LAYOUT.order,
1538
- hidden: EMPTY_LAYOUT.hidden,
1539
- minimized: EMPTY_LAYOUT.minimized,
1540
- pages,
1541
- ...withSettings
1949
+ ...layout,
1950
+ order: next.order,
1951
+ hidden: next.hidden,
1952
+ minimized: next.minimized
1542
1953
  };
1543
1954
  }
1544
- if (order.length === 0 && hidden.length === 0 && minimized.length === 0) {
1545
- return settings === void 0 ? EMPTY_LAYOUT : { ...EMPTY_LAYOUT, settings };
1546
- }
1547
- return { order, hidden, minimized, ...withSettings };
1548
- }
1549
- function parsePages(value) {
1550
- if (!Array.isArray(value)) return [];
1551
- const pages = [];
1552
- const keys = /* @__PURE__ */ new Set();
1553
- for (const entry of value.slice(0, MAX_PARSED_PAGES)) {
1554
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
1555
- continue;
1556
- }
1557
- const record = entry;
1558
- const key = record["key"];
1559
- if (typeof key !== "string" || key === "" || keys.has(key)) continue;
1560
- keys.add(key);
1561
- const rawTitle = record["title"];
1562
- const title = typeof rawTitle === "string" && rawTitle.trim() !== "" ? rawTitle.trim().slice(0, MAX_PAGE_TITLE_LENGTH) : `Page ${pages.length + 1}`;
1563
- const maxRows = record["maxRows"];
1564
- const held = /* @__PURE__ */ new Set();
1565
- for (const earlier of pages) {
1566
- for (const k of [...earlier.order, ...earlier.hidden]) held.add(k);
1567
- }
1568
- const keep = (list) => uniqueStrings(list).filter((k) => !held.has(k));
1569
- pages.push({
1570
- key,
1571
- title,
1572
- order: keep(record["order"]),
1573
- hidden: keep(record["hidden"]),
1574
- minimized: keep(record["minimized"]),
1575
- ...typeof maxRows === "number" && Number.isInteger(maxRows) && maxRows >= 1 && maxRows <= MAX_PAGE_ROWS ? { maxRows } : {}
1576
- });
1577
- }
1578
- return pages;
1955
+ return {
1956
+ ...layout,
1957
+ pages: (layout.pages ?? []).map(
1958
+ (page) => page.key === pageKey ? {
1959
+ ...page,
1960
+ order: next.order,
1961
+ hidden: next.hidden,
1962
+ minimized: next.minimized
1963
+ } : page
1964
+ )
1965
+ };
1579
1966
  }
1580
- function serializeLayout(layout) {
1581
- return JSON.stringify({
1582
- order: [...layout.order],
1583
- hidden: [...layout.hidden],
1584
- minimized: [...layout.minimized],
1585
- ...layout.pages === void 0 || layout.pages.length === 0 ? {} : {
1586
- pages: layout.pages.map((page) => ({
1587
- key: page.key,
1588
- title: page.title,
1589
- order: [...page.order],
1590
- hidden: [...page.hidden],
1591
- minimized: [...page.minimized],
1592
- ...page.maxRows === void 0 ? {} : { maxRows: page.maxRows }
1593
- }))
1594
- },
1595
- ...serializeSettings(layout.settings) === void 0 ? {} : { settings: serializeSettings(layout.settings) }
1596
- });
1967
+ function normalizedTitle(title) {
1968
+ return title.trim().normalize("NFKC").toLowerCase();
1597
1969
  }
1598
- function bySortOrder(items) {
1599
- return items.map((item, index) => ({ item, index })).sort(
1600
- (a, b) => (a.item.sortOrder ?? 0) - (b.item.sortOrder ?? 0) || a.index - b.index
1601
- ).map(({ item }) => item);
1970
+ function resolvePageKey(layout, reference) {
1971
+ if (reference === void 0 || reference === "") return void 0;
1972
+ const pages = pageList(layout);
1973
+ const byKey = pages.find((p) => p.key === reference);
1974
+ if (byKey !== void 0) return byKey.key;
1975
+ const wanted = normalizedTitle(reference);
1976
+ return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
1602
1977
  }
1603
- function isPinned(item) {
1604
- return resolveWidgetLock(item).move;
1978
+ function homePageKey(layout, item) {
1979
+ return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
1605
1980
  }
1606
- function viewerOrder(visible, layout) {
1607
- const byKey = new Map(visible.map((d) => [d.key, d]));
1608
- const ordered = [];
1609
- for (const key of layout.order) {
1610
- const definition = byKey.get(key);
1611
- if (definition !== void 0) {
1612
- ordered.push(definition);
1613
- byKey.delete(key);
1981
+ function assignPages(definitions, layout) {
1982
+ const pages = pageList(layout);
1983
+ const assigned = new Map(pages.map((p) => [p.key, []]));
1984
+ for (const definition of definitions) {
1985
+ let pageKey;
1986
+ if (!resolveWidgetLock(definition).move) {
1987
+ pageKey = pages.find(
1988
+ (p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
1989
+ )?.key;
1614
1990
  }
1991
+ assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
1615
1992
  }
1616
- for (const definition of bySortOrder(visible)) {
1617
- if (byKey.has(definition.key)) ordered.push(definition);
1993
+ return assigned;
1994
+ }
1995
+ function pageOf(definitions, layout, key) {
1996
+ for (const [pageKey, widgets] of assignPages(definitions, layout)) {
1997
+ if (widgets.some((w) => w.key === key)) return pageKey;
1618
1998
  }
1619
- return ordered;
1999
+ return void 0;
1620
2000
  }
1621
- function placeAroundPinned(visible, free) {
1622
- const slots = bySortOrder(visible).map(
1623
- (item) => isPinned(item) ? item : void 0
1624
- );
1625
- let next = 0;
1626
- return slots.map((pinned) => pinned ?? free[next++]);
2001
+ function pageWidgets(definitions, layout, pageKey) {
2002
+ const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
2003
+ const view = pageLayout(layout, pageKey);
2004
+ return view === void 0 ? [] : visibleWidgets(onPage, view);
1627
2005
  }
1628
- function visibleWidgets(definitions, layout) {
1629
- const hidden = new Set(layout.hidden);
1630
- const visible = definitions.filter((d) => !hidden.has(d.key));
1631
- const ordered = viewerOrder(visible, layout);
1632
- if (!visible.some(isPinned)) return ordered;
1633
- return placeAroundPinned(
1634
- visible,
1635
- ordered.filter((item) => !isPinned(item))
2006
+ function pageRoom(definitions, layout, pageKey, options = {}) {
2007
+ const page = pageList(layout).find(
2008
+ (p) => p.key === pageKey
1636
2009
  );
2010
+ if (page === void 0) return void 0;
2011
+ const maxRows = rowsAllowed(page, options);
2012
+ const rowsUsed = placeRows(
2013
+ pageWidgets(definitions, layout, pageKey)
2014
+ ).length;
2015
+ return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
1637
2016
  }
1638
- function hiddenWidgets(definitions, layout) {
1639
- const hidden = new Set(layout.hidden);
1640
- return bySortOrder(definitions.filter((d) => hidden.has(d.key)));
1641
- }
1642
- function moveWidget(definitions, layout, key, toIndex) {
1643
- const current = visibleWidgets(definitions, layout);
1644
- const fromIndex = current.findIndex((d) => d.key === key);
1645
- const mover = current[fromIndex];
1646
- if (mover === void 0 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex || isPinned(mover) || isPinned(current[toIndex])) {
1647
- return layout;
1648
- }
1649
- const freeSlots = freeSlotIndexes(current);
1650
- const free = current.filter((item) => !isPinned(item));
1651
- const fromFree = freeSlots.indexOf(fromIndex);
1652
- const toFree = freeSlots.indexOf(toIndex);
1653
- free.splice(fromFree, 1);
1654
- free.splice(toFree, 0, mover);
1655
- const next = placeAroundPinned(current, free);
1656
- return { ...layout, order: next.map((d) => d.key) };
2017
+ function describePage(page) {
2018
+ return `Page "${page.title}"`;
1657
2019
  }
1658
- function freeSlotIndexes(ordered) {
1659
- const slots = [];
1660
- ordered.forEach((item, index) => {
1661
- if (!isPinned(item)) slots.push(index);
1662
- });
1663
- return slots;
2020
+ function titleOf(item) {
2021
+ return item.title ?? item.key;
1664
2022
  }
1665
- function moveWidgetBy(definitions, layout, key, offset) {
1666
- const current = visibleWidgets(definitions, layout);
1667
- const fromIndex = current.findIndex((d) => d.key === key);
1668
- if (fromIndex === -1) return layout;
1669
- const freeSlots = freeSlotIndexes(current);
1670
- const fromFree = freeSlots.indexOf(fromIndex);
1671
- if (fromFree === -1) return layout;
1672
- const toFree = Math.max(
1673
- 0,
1674
- Math.min(freeSlots.length - 1, fromFree + Math.trunc(offset))
2023
+ function checkTitle(layout, title, exceptKey) {
2024
+ const trimmed = title.trim();
2025
+ if (trimmed === "") return err("A page title must not be empty.");
2026
+ if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
2027
+ return err(
2028
+ `Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
2029
+ );
2030
+ }
2031
+ const wanted = normalizedTitle(trimmed);
2032
+ const clash = pageList(layout).find(
2033
+ (p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
1675
2034
  );
1676
- return moveWidget(definitions, layout, key, freeSlots[toFree]);
1677
- }
1678
- function moveWidgetUp(definitions, layout, key) {
1679
- return moveWidgetBy(definitions, layout, key, -1);
1680
- }
1681
- function moveWidgetDown(definitions, layout, key) {
1682
- return moveWidgetBy(definitions, layout, key, 1);
1683
- }
1684
- function isHidden(layout, key) {
1685
- return layout.hidden.includes(key);
2035
+ if (clash !== void 0) {
2036
+ return err(`Another page is already called "${clash.title}".`);
2037
+ }
2038
+ return ok(trimmed);
1686
2039
  }
1687
- function isMinimized(layout, key) {
1688
- return layout.minimized.includes(key);
2040
+ function nextPageKey(layout) {
2041
+ const keys = new Set(pageList(layout).map((p) => p.key));
2042
+ let n = 1;
2043
+ while (keys.has(`page-${n}`)) n += 1;
2044
+ return `page-${n}`;
1689
2045
  }
1690
- function hideWidget(layout, key) {
1691
- if (isHidden(layout, key)) return layout;
1692
- return { ...layout, hidden: [...layout.hidden, key] };
2046
+ function nextPageTitle(layout) {
2047
+ const used = new Set(
2048
+ pageList(layout).map((p) => normalizedTitle(p.title))
2049
+ );
2050
+ let n = pageList(layout).length + 1;
2051
+ while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
2052
+ return `Page ${n}`;
1693
2053
  }
1694
- function showWidget(layout, key) {
1695
- if (!isHidden(layout, key)) return layout;
1696
- return {
1697
- ...layout,
1698
- hidden: layout.hidden.filter((k) => k !== key),
1699
- minimized: layout.minimized.filter((k) => k !== key)
2054
+ function addPage(layout, options = {}) {
2055
+ if (pageList(layout).length >= MAX_PARSED_PAGES) {
2056
+ return err(
2057
+ `The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
2058
+ );
2059
+ }
2060
+ const key = options.key ?? nextPageKey(layout);
2061
+ if (pageList(layout).some((p) => p.key === key)) {
2062
+ return err(`A page with the key "${key}" already exists.`);
2063
+ }
2064
+ const title = checkTitle(
2065
+ layout,
2066
+ options.title ?? nextPageTitle(layout)
2067
+ );
2068
+ if (!title.ok) return title;
2069
+ const base = withExplicitPages(layout);
2070
+ const page = {
2071
+ key,
2072
+ title: title.value,
2073
+ order: [],
2074
+ hidden: [],
2075
+ minimized: []
1700
2076
  };
2077
+ return ok({ ...base, pages: [...base.pages ?? [], page] });
1701
2078
  }
1702
- function toggleHidden(layout, key) {
1703
- return isHidden(layout, key) ? showWidget(layout, key) : hideWidget(layout, key);
1704
- }
1705
- function minimizeWidget(layout, key) {
1706
- if (isMinimized(layout, key)) return layout;
1707
- return { ...layout, minimized: [...layout.minimized, key] };
1708
- }
1709
- function restoreWidget(layout, key) {
1710
- if (!isMinimized(layout, key)) return layout;
1711
- return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
2079
+ function renamePage(layout, pageKey, title) {
2080
+ const page = pageList(layout).find(
2081
+ (p) => p.key === pageKey
2082
+ );
2083
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2084
+ const checked = checkTitle(layout, title, pageKey);
2085
+ if (!checked.ok) return checked;
2086
+ if (checked.value === page.title) return ok(layout);
2087
+ const base = withExplicitPages(layout);
2088
+ return ok({
2089
+ ...base,
2090
+ pages: (base.pages ?? []).map(
2091
+ (p) => p.key === pageKey ? { ...p, title: checked.value } : p
2092
+ )
2093
+ });
1712
2094
  }
1713
- function toggleMinimized(layout, key) {
1714
- return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
2095
+ function movePage(layout, pageKey, offset) {
2096
+ const pages = layout.pages ?? [];
2097
+ const from = pages.findIndex((p) => p.key === pageKey);
2098
+ if (from === -1) return layout;
2099
+ const to = Math.max(
2100
+ 0,
2101
+ Math.min(pages.length - 1, from + Math.trunc(offset))
2102
+ );
2103
+ if (to === from) return layout;
2104
+ const next = [...pages];
2105
+ const [moved] = next.splice(from, 1);
2106
+ next.splice(to, 0, moved);
2107
+ return { ...layout, pages: next };
1715
2108
  }
1716
- function pruneLayout(definitions, layout) {
1717
- const known = new Set(definitions.map((d) => d.key));
1718
- const keep = (keys) => keys.every((k) => known.has(k)) ? keys : keys.filter((k) => known.has(k));
1719
- return mapLayoutLists(layout, (lists) => ({
1720
- order: keep(lists.order),
1721
- hidden: keep(lists.hidden),
1722
- minimized: keep(lists.minimized)
1723
- }));
2109
+ function overflowing(definitions, layout, options) {
2110
+ return pageList(layout).find(
2111
+ (page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
2112
+ );
1724
2113
  }
1725
- function mapLayoutLists(layout, change) {
1726
- const top = change(layout);
1727
- const pages = layout.pages?.map((page) => {
1728
- const next = change(page);
1729
- return next.order === page.order && next.hidden === page.hidden && next.minimized === page.minimized ? page : { ...page, ...next };
1730
- });
1731
- const pagesChanged = pages !== void 0 && pages.some((page, i) => page !== layout.pages?.[i]);
1732
- if (top.order === layout.order && top.hidden === layout.hidden && top.minimized === layout.minimized && !pagesChanged) {
1733
- return layout;
2114
+ function removePage(definitions, layout, pageKey, options = {}) {
2115
+ const pages = pageList(layout);
2116
+ const index = pages.findIndex((p) => p.key === pageKey);
2117
+ const page = pages[index];
2118
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2119
+ if (pages.length === 1) {
2120
+ return err(
2121
+ `${describePage(page)} is the only page, so it can't be removed.`
2122
+ );
1734
2123
  }
1735
- return {
2124
+ const target = pages[index > 0 ? index - 1 : 1];
2125
+ const merged = {
1736
2126
  ...layout,
1737
- ...top,
1738
- ...pages === void 0 ? {} : { pages }
2127
+ pages: pages.filter((p) => p.key !== pageKey).map(
2128
+ (p) => p.key === target.key ? {
2129
+ ...p,
2130
+ order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
2131
+ hidden: [...p.hidden, ...page.hidden],
2132
+ minimized: [...p.minimized, ...page.minimized]
2133
+ } : p
2134
+ )
1739
2135
  };
1740
- }
1741
- function enforceLocks(definitions, layout, options = {}) {
1742
- if (options.overrideLocks === true) return layout;
1743
- const noMove = /* @__PURE__ */ new Set();
1744
- const noHide = /* @__PURE__ */ new Set();
1745
- const noMinimize = /* @__PURE__ */ new Set();
1746
- for (const definition of definitions) {
1747
- const lock = resolveWidgetLock(definition);
1748
- if (lock.move) noMove.add(definition.key);
1749
- if (lock.hide) noHide.add(definition.key);
1750
- if (lock.minimize) noMinimize.add(definition.key);
1751
- }
1752
- const without2 = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
1753
- return mapLayoutLists(layout, (lists) => ({
1754
- order: without2(lists.order, noMove),
1755
- hidden: without2(lists.hidden, noHide),
1756
- minimized: without2(lists.minimized, noMinimize)
1757
- }));
1758
- }
1759
-
1760
- // src/core/validate.ts
1761
- var MAX_REPORTED_PROBLEMS = 10;
1762
- function describeType2(value) {
1763
- if (value === null) return "null";
1764
- if (Array.isArray(value)) return "array";
1765
- if (typeof value === "number" && !Number.isFinite(value)) {
1766
- return String(value);
1767
- }
1768
- return typeof value;
1769
- }
1770
- function isRecord2(value) {
1771
- return typeof value === "object" && value !== null && !Array.isArray(value);
1772
- }
1773
- function join(path, field) {
1774
- return path === "" ? field : `${path}.${field}`;
1775
- }
1776
- function requireString(record, field, path, problems) {
1777
- const value = record[field];
1778
- if (typeof value !== "string") {
1779
- problems.push(
1780
- `${join(path, field)} must be a string, got ${describeType2(value)}.`
2136
+ const full = overflowing(
2137
+ definitions,
2138
+ merged,
2139
+ options
2140
+ );
2141
+ if (full !== void 0) {
2142
+ return err(
2143
+ `${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.`
1781
2144
  );
1782
2145
  }
2146
+ return ok(merged);
1783
2147
  }
1784
- function optionalString(record, field, path, problems) {
1785
- if (record[field] !== void 0) {
1786
- requireString(record, field, path, problems);
1787
- }
1788
- }
1789
- function requireNumber(record, field, path, problems) {
1790
- const value = record[field];
1791
- if (typeof value !== "number" || !Number.isFinite(value)) {
1792
- problems.push(
1793
- `${join(path, field)} must be a finite number, got ${describeType2(value)}.`
1794
- );
1795
- }
1796
- }
1797
- function optionalNumber(record, field, path, problems) {
1798
- if (record[field] !== void 0) {
1799
- requireNumber(record, field, path, problems);
1800
- }
2148
+ function without(keys, key) {
2149
+ return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
1801
2150
  }
1802
- function optionalSortValue(record, path, problems) {
1803
- const value = record["value"];
1804
- if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
1805
- problems.push(
1806
- `${path}.value must be a string or a finite number, got ${describeType2(value)}.`
2151
+ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
2152
+ const widget = definitions.find((d) => d.key === widgetKey);
2153
+ if (widget === void 0) {
2154
+ return err(
2155
+ `Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
1807
2156
  );
1808
2157
  }
1809
- }
1810
- function optionalBoolean(record, field, path, problems) {
1811
- const value = record[field];
1812
- if (value !== void 0 && typeof value !== "boolean") {
1813
- problems.push(
1814
- `${join(path, field)} must be a boolean, got ${describeType2(value)}.`
2158
+ const pages = pageList(layout);
2159
+ const target = pages.find((p) => p.key === targetKey);
2160
+ if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
2161
+ const sourceKey = pageOf(definitions, layout, widgetKey);
2162
+ if (sourceKey === targetKey) return ok(layout);
2163
+ if (resolveWidgetLock(widget).move) {
2164
+ return err(
2165
+ `Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
1815
2166
  );
1816
2167
  }
1817
- }
1818
- function optionalUrl(record, field, path, problems, isSafe, allowed) {
1819
- const value = record[field];
1820
- if (value === void 0) return;
1821
- if (typeof value !== "string") {
1822
- problems.push(
1823
- `${join(path, field)} must be a string, got ${describeType2(value)}.`
1824
- );
1825
- } else if (!isSafe(value)) {
1826
- problems.push(
1827
- `${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
1828
- );
2168
+ const base = withExplicitPages(layout);
2169
+ const source = (base.pages ?? []).find(
2170
+ (p) => p.key === sourceKey
2171
+ );
2172
+ const hidden = source.hidden.includes(widgetKey);
2173
+ const minimized = source.minimized.includes(widgetKey);
2174
+ const targetOrder = pageWidgets(
2175
+ definitions,
2176
+ base,
2177
+ targetKey
2178
+ ).map((d) => d.key);
2179
+ const moved = {
2180
+ ...base,
2181
+ pages: (base.pages ?? []).map((p) => {
2182
+ if (p.key === sourceKey) {
2183
+ return {
2184
+ ...p,
2185
+ order: without(p.order, widgetKey),
2186
+ hidden: without(p.hidden, widgetKey),
2187
+ minimized: without(p.minimized, widgetKey)
2188
+ };
2189
+ }
2190
+ if (p.key === targetKey) {
2191
+ return {
2192
+ ...p,
2193
+ order: hidden ? p.order : [...targetOrder, widgetKey],
2194
+ hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
2195
+ minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
2196
+ };
2197
+ }
2198
+ return p;
2199
+ })
2200
+ };
2201
+ if (!hidden) {
2202
+ const allowed = rowsAllowed(target, options);
2203
+ const needed = placeRows(
2204
+ pageWidgets(definitions, moved, targetKey)
2205
+ ).length;
2206
+ if (needed > allowed) {
2207
+ return err(
2208
+ `${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
2209
+ );
2210
+ }
1829
2211
  }
2212
+ return ok(moved);
1830
2213
  }
1831
- function requireArray(record, field, path, problems) {
1832
- const value = record[field];
1833
- if (!Array.isArray(value)) {
1834
- problems.push(
1835
- `${join(path, field)} must be an array, got ${describeType2(value)}.`
1836
- );
1837
- return null;
1838
- }
1839
- return value;
2214
+ function withoutLockField(definition) {
2215
+ if (definition.locked === void 0) return definition;
2216
+ const copy = { ...definition };
2217
+ delete copy.locked;
2218
+ return copy;
1840
2219
  }
1841
- function requireOneOf(record, field, options, path, problems) {
1842
- const value = record[field];
1843
- if (typeof value !== "string" || !options.includes(value)) {
1844
- problems.push(
1845
- `${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType2(value)}.`
1846
- );
2220
+ function uniqueTitle2(taken, title, index) {
2221
+ let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
2222
+ if (base === "") base = `Page ${index + 1}`;
2223
+ let candidate = base;
2224
+ let n = 1;
2225
+ while (taken.has(normalizedTitle(candidate))) {
2226
+ const suffix = ` (#${n})`;
2227
+ candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
2228
+ n += 1;
1847
2229
  }
2230
+ taken.add(normalizedTitle(candidate));
2231
+ return candidate;
1848
2232
  }
1849
- function eachRecord(items, path, problems, check) {
1850
- items.forEach((item, index) => {
1851
- const itemPath = `${path}[${index}]`;
1852
- if (isRecord2(item)) {
1853
- check(item, itemPath);
1854
- } else {
1855
- problems.push(
1856
- `${itemPath} must be an object, got ${describeType2(item)}.`
1857
- );
1858
- }
2233
+ function samePages(a, b) {
2234
+ return a.length === b.length && a.every((p, i) => {
2235
+ const q = b[i];
2236
+ 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);
1859
2237
  });
1860
2238
  }
1861
- var HREF_RULE = "an http(s), mailto or relative URL";
1862
- var IMAGE_RULE = "an http(s) or relative URL";
1863
- function checkPayloadFields(kind, data, problems) {
1864
- switch (kind) {
1865
- case "TEXT":
1866
- requireString(data, "value", "", problems);
1867
- requireString(data, "label", "", problems);
1868
- return;
1869
- case "KPI":
1870
- requireNumber(data, "value", "", problems);
1871
- if (data["previous"] !== null) {
1872
- optionalNumber(data, "previous", "", problems);
2239
+ function normalizeLayout(definitions, layout, options = {}) {
2240
+ const clones = cleanClones(definitions, layout.clones, options);
2241
+ let base = layout;
2242
+ if (clones !== layout.clones) {
2243
+ const { clones: _dropped, ...others } = layout;
2244
+ base = clones === void 0 ? others : { ...others, clones };
2245
+ }
2246
+ return normalizeWidgets(
2247
+ withClones(definitions, base, options),
2248
+ base,
2249
+ options
2250
+ );
2251
+ }
2252
+ function normalizeWidgets(definitions, layout, options) {
2253
+ const known = pruneLayout(definitions, layout);
2254
+ const enforced = enforceLocks(definitions, known, options);
2255
+ const locked = normalizeSettings(
2256
+ definitions,
2257
+ enforced,
2258
+ options
2259
+ );
2260
+ if (!hasPages(locked)) return locked === layout ? layout : locked;
2261
+ const defs = effectiveDefinitions(
2262
+ options.overrideLocks === true ? definitions.map(withoutLockField) : definitions,
2263
+ locked,
2264
+ options
2265
+ );
2266
+ const taken = /* @__PURE__ */ new Set();
2267
+ const seenKeys = /* @__PURE__ */ new Set();
2268
+ const held = /* @__PURE__ */ new Set();
2269
+ let pages = [];
2270
+ (locked.pages ?? []).forEach((page, index) => {
2271
+ if (seenKeys.has(page.key)) return;
2272
+ seenKeys.add(page.key);
2273
+ const keep = (list) => [
2274
+ ...new Set(list.filter((k) => !held.has(k)))
2275
+ ];
2276
+ const next = {
2277
+ ...page,
2278
+ title: uniqueTitle2(taken, page.title, index),
2279
+ order: keep(page.order),
2280
+ hidden: keep(page.hidden),
2281
+ minimized: keep(page.minimized)
2282
+ };
2283
+ for (const k of [...next.order, ...next.hidden]) held.add(k);
2284
+ pages.push(next);
2285
+ });
2286
+ const work = () => ({ ...locked, pages });
2287
+ const pinned = defs.filter((d) => resolveWidgetLock(d).move);
2288
+ for (const widget of pinned) {
2289
+ const home = homePageKey(work(), widget);
2290
+ const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
2291
+ const wasHidden = pages.some((p) => keysHere(p, "hidden"));
2292
+ const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
2293
+ if (!wasHidden && !wasMinimized) continue;
2294
+ pages = pages.map((p) => {
2295
+ let next = p;
2296
+ if (keysHere(p, "hidden")) {
2297
+ next = { ...next, hidden: without(next.hidden, widget.key) };
1873
2298
  }
1874
- requireOneOf(data, "format", VALUE_FORMATS, "", problems);
1875
- optionalString(data, "currency", "", problems);
1876
- requireString(data, "label", "", problems);
1877
- optionalString(data, "hint", "", problems);
1878
- optionalBoolean(data, "higherIsBetter", "", problems);
1879
- return;
1880
- case "GAUGE":
1881
- requireNumber(data, "value", "", problems);
1882
- requireNumber(data, "max", "", problems);
1883
- requireString(data, "label", "", problems);
1884
- if (typeof data["max"] === "number" && data["max"] <= 0) {
1885
- problems.push(`max must be greater than 0, got ${data["max"]}.`);
2299
+ if (keysHere(p, "minimized")) {
2300
+ next = { ...next, minimized: without(next.minimized, widget.key) };
1886
2301
  }
1887
- return;
1888
- case "TABLE": {
1889
- const columns = requireArray(data, "columns", "", problems);
1890
- if (columns !== null) {
1891
- eachRecord(columns, "columns", problems, (column, path) => {
1892
- requireString(column, "label", path, problems);
1893
- optionalBoolean(column, "numeric", path, problems);
1894
- });
2302
+ if (p.key === home) {
2303
+ next = {
2304
+ ...next,
2305
+ hidden: wasHidden && !next.hidden.includes(widget.key) ? [...next.hidden, widget.key] : next.hidden,
2306
+ minimized: wasMinimized && !next.minimized.includes(widget.key) ? [...next.minimized, widget.key] : next.minimized
2307
+ };
1895
2308
  }
1896
- const rows = requireArray(data, "rows", "", problems);
1897
- if (rows !== null) {
1898
- rows.forEach((row, rowIndex) => {
1899
- const rowPath = `rows[${rowIndex}]`;
1900
- if (!Array.isArray(row)) {
1901
- problems.push(
1902
- `${rowPath} must be an array of cells, got ${describeType2(row)}.`
1903
- );
1904
- return;
1905
- }
1906
- if (columns !== null && row.length !== columns.length) {
1907
- problems.push(
1908
- `${rowPath} has ${row.length} cell(s) but there are ${columns.length} column(s).`
1909
- );
1910
- }
1911
- eachRecord(row, rowPath, problems, (cell, cellPath) => {
1912
- requireString(cell, "text", cellPath, problems);
1913
- optionalSortValue(cell, cellPath, problems);
1914
- optionalUrl(
1915
- cell,
1916
- "href",
1917
- cellPath,
1918
- problems,
1919
- isSafeHref,
1920
- HREF_RULE
1921
- );
1922
- });
1923
- });
1924
- }
1925
- optionalString(data, "footer", "", problems);
1926
- return;
1927
- }
1928
- case "BAR_LIST": {
1929
- const items = requireArray(data, "items", "", problems);
1930
- if (items !== null) {
1931
- eachRecord(items, "items", problems, (item, path) => {
1932
- requireString(item, "label", path, problems);
1933
- requireNumber(item, "value", path, problems);
1934
- optionalString(item, "display", path, problems);
1935
- });
2309
+ return next;
2310
+ });
2311
+ }
2312
+ const placedKeys = /* @__PURE__ */ new Set();
2313
+ for (const p of pages) {
2314
+ for (const k of [...p.order, ...p.hidden]) placedKeys.add(k);
2315
+ }
2316
+ const pinnedKeys = new Set(pinned.map((d) => d.key));
2317
+ const unplaced = defs.filter((d) => !placedKeys.has(d.key) && !pinnedKeys.has(d.key)).map((definition, index) => ({ definition, index })).sort(
2318
+ (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
2319
+ ).map(({ definition }) => definition);
2320
+ for (const widget of unplaced) {
2321
+ const home = homePageKey(work(), widget);
2322
+ pages = pages.map(
2323
+ (p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
2324
+ );
2325
+ }
2326
+ for (let i = 0; i < pages.length; i += 1) {
2327
+ const page = pages[i];
2328
+ const shown = pageWidgets(defs, work(), page.key);
2329
+ const fits = fitCount(shown, rowsAllowed(page, options));
2330
+ if (fits >= shown.length) continue;
2331
+ const moving = shown.slice(fits).filter((d) => !pinnedKeys.has(d.key)).map((d) => d.key);
2332
+ if (moving.length === 0) continue;
2333
+ const moveSet = new Set(moving);
2334
+ const movedMinimized = page.minimized.filter(
2335
+ (k) => moveSet.has(k)
2336
+ );
2337
+ pages = pages.map(
2338
+ (p) => p.key === page.key ? {
2339
+ ...p,
2340
+ order: p.order.filter((k) => !moveSet.has(k)),
2341
+ minimized: p.minimized.filter((k) => !moveSet.has(k))
2342
+ } : p
2343
+ );
2344
+ let placed = false;
2345
+ for (let j = i + 1; j < pages.length && !placed; j += 1) {
2346
+ const candidate = pages[j];
2347
+ const trial = pages.map(
2348
+ (p) => p.key === candidate.key ? {
2349
+ ...p,
2350
+ order: [...moving, ...p.order],
2351
+ minimized: [...p.minimized, ...movedMinimized]
2352
+ } : p
2353
+ );
2354
+ const items = pageWidgets(
2355
+ defs,
2356
+ { ...locked, pages: trial },
2357
+ candidate.key
2358
+ );
2359
+ if (placeRows(items).length <= rowsAllowed(candidate, options)) {
2360
+ pages = trial;
2361
+ placed = true;
1936
2362
  }
1937
- optionalNumber(data, "total", "", problems);
1938
- return;
1939
2363
  }
1940
- case "ALERT_LIST": {
1941
- const items = requireArray(data, "items", "", problems);
1942
- if (items !== null) {
1943
- eachRecord(items, "items", problems, (item, path) => {
1944
- requireString(item, "title", path, problems);
1945
- optionalUrl(item, "href", path, problems, isSafeHref, HREF_RULE);
1946
- optionalUrl(
1947
- item,
1948
- "thumbnailUrl",
1949
- path,
1950
- problems,
1951
- isSafeImageUrl,
1952
- IMAGE_RULE
1953
- );
1954
- requireString(item, "valueLabel", path, problems);
1955
- optionalString(item, "detail", path, problems);
1956
- });
1957
- }
1958
- requireNumber(data, "total", "", problems);
1959
- requireString(data, "emptyText", "", problems);
1960
- if (items !== null && typeof data["total"] === "number" && data["total"] < items.length) {
1961
- problems.push(
1962
- `total (${data["total"]}) must be at least the number of items (${items.length}).`
1963
- );
1964
- }
1965
- return;
2364
+ if (!placed) {
2365
+ const key = nextPageKey(work());
2366
+ const title = uniqueTitle2(
2367
+ new Set(pages.map((p) => normalizedTitle(p.title))),
2368
+ `Page ${pages.length + 1}`,
2369
+ pages.length
2370
+ );
2371
+ pages = [
2372
+ ...pages,
2373
+ { key, title, order: moving, hidden: [], minimized: movedMinimized }
2374
+ ];
1966
2375
  }
1967
- case "GRAPH": {
1968
- const series = requireArray(data, "series", "", problems);
1969
- if (series !== null) {
1970
- if (series.length > MAX_GRAPH_SERIES) {
1971
- problems.push(
1972
- `series has ${series.length} entries; at most ${MAX_GRAPH_SERIES} are allowed (fold the rest into "Other").`
1973
- );
1974
- }
1975
- eachRecord(series, "series", problems, (entry, path) => {
1976
- requireString(entry, "name", path, problems);
1977
- const points = requireArray(entry, "points", path, problems);
1978
- if (points !== null) {
1979
- eachRecord(
1980
- points,
1981
- `${path}.points`,
1982
- problems,
1983
- (point, pointPath) => {
1984
- requireString(point, "label", pointPath, problems);
1985
- requireNumber(point, "value", pointPath, problems);
1986
- }
1987
- );
1988
- }
1989
- });
1990
- }
1991
- requireOneOf(data, "chartType", ["bar", "line"], "", problems);
1992
- requireOneOf(data, "valueFormat", VALUE_FORMATS, "", problems);
1993
- optionalString(data, "currency", "", problems);
1994
- optionalString(data, "xLabel", "", problems);
1995
- return;
2376
+ }
2377
+ const result = work();
2378
+ return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) && locked.settings === layout.settings ? layout : result;
2379
+ }
2380
+
2381
+ // src/core/duplicate.ts
2382
+ function canDuplicate(definitions, key, options = {}) {
2383
+ const definition = definitions.find(
2384
+ (d) => d.key === key
2385
+ );
2386
+ if (definition === void 0) return false;
2387
+ if (options.overrideLocks === true) return true;
2388
+ const sourceKey = definition.sourceKey ?? definition.key;
2389
+ const original = definitions.find(
2390
+ (d) => d.key === sourceKey
2391
+ );
2392
+ return original !== void 0 && !resolveWidgetLock(original).clone;
2393
+ }
2394
+ function nextCloneKey(definitions, layout, fromKey) {
2395
+ const taken = /* @__PURE__ */ new Set([
2396
+ ...definitions.map((d) => d.key),
2397
+ ...(layout.clones ?? []).map((c) => c.key)
2398
+ ]);
2399
+ let highest = 0;
2400
+ for (const clone of layout.clones ?? []) {
2401
+ if (clone.from === fromKey) {
2402
+ highest = Math.max(highest, cloneNumber(clone.key) ?? 0);
1996
2403
  }
1997
2404
  }
2405
+ let n = highest + 1;
2406
+ while (taken.has(`${fromKey}#${n}`)) n += 1;
2407
+ return `${fromKey}#${n}`;
1998
2408
  }
1999
- function checkDetailOption(detail, problems) {
2000
- if (detail === void 0 || typeof detail === "boolean") return;
2001
- if (!isRecord2(detail)) {
2002
- problems.push(
2003
- `detail must be true, false or an options object, got ${describeType2(detail)}.`
2004
- );
2005
- return;
2409
+ function suggestedViewTitle(title, viewLabel) {
2410
+ return `${title} ${viewLabel}`.trim().slice(0, MAX_WIDGET_TITLE_LENGTH);
2411
+ }
2412
+ function duplicateWidget(definitions, layout, key, options = {}) {
2413
+ const widget = definitions.find((d) => d.key === key);
2414
+ if (widget === void 0) {
2415
+ return err(`Widget ${JSON.stringify(key)} isn't on this dashboard.`);
2006
2416
  }
2007
- optionalString(detail, "title", "detail", problems);
2008
- const pageSize = detail["pageSize"];
2009
- if (pageSize !== void 0 && !(typeof pageSize === "number" && Number.isInteger(pageSize) && pageSize >= 1 && pageSize <= MAX_DETAIL_PAGE_SIZE)) {
2010
- problems.push(
2011
- `detail.pageSize must be a whole number from 1 to ${MAX_DETAIL_PAGE_SIZE}, got ${typeof pageSize === "number" ? String(pageSize) : describeType2(pageSize)}.`
2417
+ const originalKey = widget.sourceKey ?? widget.key;
2418
+ if (!canDuplicate(definitions, key, options)) {
2419
+ return err(
2420
+ `Widget ${JSON.stringify(originalKey)} is locked against duplicating.`
2012
2421
  );
2013
2422
  }
2014
- if (detail["mode"] !== void 0) {
2015
- requireOneOf(detail, "mode", DETAIL_MODES, "detail", problems);
2423
+ const cloneKey = nextCloneKey(definitions, layout, originalKey);
2424
+ const copiedSettings = (() => {
2425
+ const mine = layout.settings?.[key];
2426
+ if (mine === void 0) return void 0;
2427
+ const { title: _title, ...rest } = mine;
2428
+ return Object.keys(rest).length === 0 ? void 0 : rest;
2429
+ })();
2430
+ const settings = copiedSettings === void 0 ? layout.settings : { ...layout.settings, [cloneKey]: copiedSettings };
2431
+ let next = {
2432
+ ...layout,
2433
+ clones: [...layout.clones ?? [], { key: cloneKey, from: originalKey }],
2434
+ ...settings === void 0 ? {} : { settings }
2435
+ };
2436
+ const all = withClones(definitions, next, options);
2437
+ const assigned = assignPages(all, next);
2438
+ const pageKey = [...assigned].find(
2439
+ ([, widgets]) => widgets.some((w) => w.key === key)
2440
+ )?.[0];
2441
+ if (pageKey !== void 0) {
2442
+ const view = pageLayout(next, pageKey);
2443
+ const onPage = assigned.get(pageKey) ?? [];
2444
+ if (view !== void 0) {
2445
+ const shown = visibleWidgets(onPage, view).filter((d) => !resolveWidgetLock(d).move && d.key !== cloneKey).map((d) => d.key);
2446
+ const at = shown.indexOf(key);
2447
+ const order = at === -1 ? [...shown, cloneKey] : [...shown.slice(0, at + 1), cloneKey, ...shown.slice(at + 1)];
2448
+ next = withPageLayout(next, pageKey, { ...view, order });
2449
+ }
2450
+ }
2451
+ if (hasPages(next) && pageKey !== void 0) {
2452
+ const effective = effectiveDefinitions(all, next, options);
2453
+ const room = pageRoom(effective, next, pageKey, options);
2454
+ if (room !== void 0 && room.rowsUsed > room.maxRows) {
2455
+ const page = next.pages?.find(
2456
+ (p) => p.key === pageKey
2457
+ );
2458
+ const width = effective.find(
2459
+ (d) => d.key === cloneKey
2460
+ )?.width;
2461
+ return err(
2462
+ `Page "${page?.title ?? pageKey}" has no room for a widget${width === void 0 ? "" : ` ${width} wide`} (it holds ${room.maxRows} rows).`
2463
+ );
2464
+ }
2016
2465
  }
2466
+ return ok({ layout: next, key: cloneKey });
2017
2467
  }
2018
- function checkTableControlsOption(controls, kind, problems) {
2019
- if (controls === void 0 || controls === false) return;
2020
- if (typeof controls !== "boolean" && !isRecord2(controls)) {
2468
+ function deleteClone(layout, key) {
2469
+ if (layout.clones?.some((clone) => clone.key === key) !== true) return layout;
2470
+ const without2 = (keys) => keys.includes(key) ? keys.filter((k) => k !== key) : keys;
2471
+ const { clones, settings, ...rest } = layout;
2472
+ const remaining = (clones ?? []).filter((c) => c.key !== key);
2473
+ const { [key]: _gone, ...otherSettings } = settings ?? {};
2474
+ return {
2475
+ ...rest,
2476
+ order: without2(layout.order),
2477
+ hidden: without2(layout.hidden),
2478
+ minimized: without2(layout.minimized),
2479
+ ...layout.pages === void 0 ? {} : {
2480
+ pages: layout.pages.map((page) => ({
2481
+ ...page,
2482
+ order: without2(page.order),
2483
+ hidden: without2(page.hidden),
2484
+ minimized: without2(page.minimized)
2485
+ }))
2486
+ },
2487
+ ...Object.keys(otherSettings).length === 0 ? {} : { settings: otherSettings },
2488
+ ...remaining.length === 0 ? {} : { clones: remaining }
2489
+ };
2490
+ }
2491
+
2492
+ // src/core/validate.ts
2493
+ var MAX_REPORTED_PROBLEMS = 10;
2494
+ function describeType2(value) {
2495
+ if (value === null) return "null";
2496
+ if (Array.isArray(value)) return "array";
2497
+ if (typeof value === "number" && !Number.isFinite(value)) {
2498
+ return String(value);
2499
+ }
2500
+ return typeof value;
2501
+ }
2502
+ function isRecord3(value) {
2503
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2504
+ }
2505
+ function join(path, field) {
2506
+ return path === "" ? field : `${path}.${field}`;
2507
+ }
2508
+ function requireString(record, field, path, problems) {
2509
+ const value = record[field];
2510
+ if (typeof value !== "string") {
2021
2511
  problems.push(
2022
- `tableControls must be true, false or an options object, got ${describeType2(controls)}.`
2512
+ `${join(path, field)} must be a string, got ${describeType2(value)}.`
2023
2513
  );
2024
- return;
2025
2514
  }
2026
- if (isRecord2(controls)) {
2027
- optionalBoolean(controls, "search", "tableControls", problems);
2028
- optionalBoolean(controls, "sort", "tableControls", problems);
2515
+ }
2516
+ function optionalString(record, field, path, problems) {
2517
+ if (record[field] !== void 0) {
2518
+ requireString(record, field, path, problems);
2029
2519
  }
2030
- if (typeof kind === "string" && kind !== "TABLE") {
2520
+ }
2521
+ function requireNumber(record, field, path, problems) {
2522
+ const value = record[field];
2523
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2031
2524
  problems.push(
2032
- `tableControls only applies to TABLE widgets, but kind is "${kind}".`
2525
+ `${join(path, field)} must be a finite number, got ${describeType2(value)}.`
2033
2526
  );
2034
2527
  }
2035
2528
  }
2036
- function checkWidthLimits(value, problems) {
2037
- for (const field of ["minWidth", "maxWidth"]) {
2038
- const limit = value[field];
2039
- if (limit !== void 0 && !isWidgetWidth(limit)) {
2040
- problems.push(
2041
- `${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)}.`
2042
- );
2043
- return;
2044
- }
2529
+ function optionalNumber(record, field, path, problems) {
2530
+ if (record[field] !== void 0) {
2531
+ requireNumber(record, field, path, problems);
2045
2532
  }
2046
- const { minWidth, maxWidth, width } = value;
2047
- if (isWidgetWidth(minWidth) && isWidgetWidth(maxWidth) && minWidth > maxWidth) {
2533
+ }
2534
+ function optionalSortValue(record, path, problems) {
2535
+ const value = record["value"];
2536
+ if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
2048
2537
  problems.push(
2049
- `minWidth (${minWidth}) must not be more than maxWidth (${maxWidth}).`
2538
+ `${path}.value must be a string or a finite number, got ${describeType2(value)}.`
2050
2539
  );
2051
- return;
2052
- }
2053
- if (isWidgetWidth(width)) {
2054
- if (isWidgetWidth(minWidth) && width < minWidth) {
2055
- problems.push(
2056
- `width (${width}) must not be less than minWidth (${minWidth}).`
2057
- );
2058
- }
2059
- if (isWidgetWidth(maxWidth) && width > maxWidth) {
2060
- problems.push(
2061
- `width (${width}) must not be more than maxWidth (${maxWidth}).`
2062
- );
2063
- }
2064
2540
  }
2065
2541
  }
2066
- function checkLockedOption(locked, problems) {
2067
- if (locked === void 0 || typeof locked === "boolean") return;
2068
- if (!isRecord2(locked)) {
2542
+ function optionalBoolean(record, field, path, problems) {
2543
+ const value = record[field];
2544
+ if (value !== void 0 && typeof value !== "boolean") {
2069
2545
  problems.push(
2070
- `locked must be true, false or an object, got ${describeType2(locked)}.`
2546
+ `${join(path, field)} must be a boolean, got ${describeType2(value)}.`
2071
2547
  );
2072
- return;
2073
2548
  }
2074
- optionalBoolean(locked, "move", "locked", problems);
2549
+ }
2550
+ function optionalUrl(record, field, path, problems, isSafe, allowed) {
2551
+ const value = record[field];
2552
+ if (value === void 0) return;
2553
+ if (typeof value !== "string") {
2554
+ problems.push(
2555
+ `${join(path, field)} must be a string, got ${describeType2(value)}.`
2556
+ );
2557
+ } else if (!isSafe(value)) {
2558
+ problems.push(
2559
+ `${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
2560
+ );
2561
+ }
2562
+ }
2563
+ function requireArray(record, field, path, problems) {
2564
+ const value = record[field];
2565
+ if (!Array.isArray(value)) {
2566
+ problems.push(
2567
+ `${join(path, field)} must be an array, got ${describeType2(value)}.`
2568
+ );
2569
+ return null;
2570
+ }
2571
+ return value;
2572
+ }
2573
+ function requireOneOf(record, field, options, path, problems) {
2574
+ const value = record[field];
2575
+ if (typeof value !== "string" || !options.includes(value)) {
2576
+ problems.push(
2577
+ `${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType2(value)}.`
2578
+ );
2579
+ }
2580
+ }
2581
+ function eachRecord(items, path, problems, check) {
2582
+ items.forEach((item, index) => {
2583
+ const itemPath = `${path}[${index}]`;
2584
+ if (isRecord3(item)) {
2585
+ check(item, itemPath);
2586
+ } else {
2587
+ problems.push(
2588
+ `${itemPath} must be an object, got ${describeType2(item)}.`
2589
+ );
2590
+ }
2591
+ });
2592
+ }
2593
+ var HREF_RULE = "an http(s), mailto or relative URL";
2594
+ var IMAGE_RULE = "an http(s) or relative URL";
2595
+ function checkPayloadFields(kind, data, problems) {
2596
+ switch (kind) {
2597
+ case "TEXT":
2598
+ requireString(data, "value", "", problems);
2599
+ requireString(data, "label", "", problems);
2600
+ return;
2601
+ case "KPI":
2602
+ requireNumber(data, "value", "", problems);
2603
+ if (data["previous"] !== null) {
2604
+ optionalNumber(data, "previous", "", problems);
2605
+ }
2606
+ requireOneOf(data, "format", VALUE_FORMATS, "", problems);
2607
+ optionalString(data, "currency", "", problems);
2608
+ requireString(data, "label", "", problems);
2609
+ optionalString(data, "hint", "", problems);
2610
+ optionalBoolean(data, "higherIsBetter", "", problems);
2611
+ return;
2612
+ case "GAUGE":
2613
+ requireNumber(data, "value", "", problems);
2614
+ requireNumber(data, "max", "", problems);
2615
+ requireString(data, "label", "", problems);
2616
+ if (typeof data["max"] === "number" && data["max"] <= 0) {
2617
+ problems.push(`max must be greater than 0, got ${data["max"]}.`);
2618
+ }
2619
+ return;
2620
+ case "TABLE": {
2621
+ const columns = requireArray(data, "columns", "", problems);
2622
+ if (columns !== null) {
2623
+ eachRecord(columns, "columns", problems, (column, path) => {
2624
+ requireString(column, "label", path, problems);
2625
+ optionalBoolean(column, "numeric", path, problems);
2626
+ });
2627
+ }
2628
+ const rows = requireArray(data, "rows", "", problems);
2629
+ if (rows !== null) {
2630
+ rows.forEach((row, rowIndex) => {
2631
+ const rowPath = `rows[${rowIndex}]`;
2632
+ if (!Array.isArray(row)) {
2633
+ problems.push(
2634
+ `${rowPath} must be an array of cells, got ${describeType2(row)}.`
2635
+ );
2636
+ return;
2637
+ }
2638
+ if (columns !== null && row.length !== columns.length) {
2639
+ problems.push(
2640
+ `${rowPath} has ${row.length} cell(s) but there are ${columns.length} column(s).`
2641
+ );
2642
+ }
2643
+ eachRecord(row, rowPath, problems, (cell, cellPath) => {
2644
+ requireString(cell, "text", cellPath, problems);
2645
+ optionalSortValue(cell, cellPath, problems);
2646
+ optionalUrl(
2647
+ cell,
2648
+ "href",
2649
+ cellPath,
2650
+ problems,
2651
+ isSafeHref,
2652
+ HREF_RULE
2653
+ );
2654
+ });
2655
+ });
2656
+ }
2657
+ optionalString(data, "footer", "", problems);
2658
+ return;
2659
+ }
2660
+ case "BAR_LIST": {
2661
+ const items = requireArray(data, "items", "", problems);
2662
+ if (items !== null) {
2663
+ eachRecord(items, "items", problems, (item, path) => {
2664
+ requireString(item, "label", path, problems);
2665
+ requireNumber(item, "value", path, problems);
2666
+ optionalString(item, "display", path, problems);
2667
+ });
2668
+ }
2669
+ optionalNumber(data, "total", "", problems);
2670
+ return;
2671
+ }
2672
+ case "ALERT_LIST": {
2673
+ const items = requireArray(data, "items", "", problems);
2674
+ if (items !== null) {
2675
+ eachRecord(items, "items", problems, (item, path) => {
2676
+ requireString(item, "title", path, problems);
2677
+ optionalUrl(item, "href", path, problems, isSafeHref, HREF_RULE);
2678
+ optionalUrl(
2679
+ item,
2680
+ "thumbnailUrl",
2681
+ path,
2682
+ problems,
2683
+ isSafeImageUrl,
2684
+ IMAGE_RULE
2685
+ );
2686
+ requireString(item, "valueLabel", path, problems);
2687
+ optionalString(item, "detail", path, problems);
2688
+ });
2689
+ }
2690
+ requireNumber(data, "total", "", problems);
2691
+ requireString(data, "emptyText", "", problems);
2692
+ if (items !== null && typeof data["total"] === "number" && data["total"] < items.length) {
2693
+ problems.push(
2694
+ `total (${data["total"]}) must be at least the number of items (${items.length}).`
2695
+ );
2696
+ }
2697
+ return;
2698
+ }
2699
+ case "GRAPH": {
2700
+ const series = requireArray(data, "series", "", problems);
2701
+ if (series !== null) {
2702
+ if (series.length > MAX_GRAPH_SERIES) {
2703
+ problems.push(
2704
+ `series has ${series.length} entries; at most ${MAX_GRAPH_SERIES} are allowed (fold the rest into "Other").`
2705
+ );
2706
+ }
2707
+ eachRecord(series, "series", problems, (entry, path) => {
2708
+ requireString(entry, "name", path, problems);
2709
+ const points = requireArray(entry, "points", path, problems);
2710
+ if (points !== null) {
2711
+ eachRecord(
2712
+ points,
2713
+ `${path}.points`,
2714
+ problems,
2715
+ (point, pointPath) => {
2716
+ requireString(point, "label", pointPath, problems);
2717
+ requireNumber(point, "value", pointPath, problems);
2718
+ }
2719
+ );
2720
+ }
2721
+ });
2722
+ }
2723
+ requireOneOf(data, "chartType", ["bar", "line"], "", problems);
2724
+ requireOneOf(data, "valueFormat", VALUE_FORMATS, "", problems);
2725
+ optionalString(data, "currency", "", problems);
2726
+ optionalString(data, "xLabel", "", problems);
2727
+ return;
2728
+ }
2729
+ }
2730
+ }
2731
+ function checkDetailOption(detail, problems) {
2732
+ if (detail === void 0 || typeof detail === "boolean") return;
2733
+ if (!isRecord3(detail)) {
2734
+ problems.push(
2735
+ `detail must be true, false or an options object, got ${describeType2(detail)}.`
2736
+ );
2737
+ return;
2738
+ }
2739
+ optionalString(detail, "title", "detail", problems);
2740
+ const pageSize = detail["pageSize"];
2741
+ if (pageSize !== void 0 && !(typeof pageSize === "number" && Number.isInteger(pageSize) && pageSize >= 1 && pageSize <= MAX_DETAIL_PAGE_SIZE)) {
2742
+ problems.push(
2743
+ `detail.pageSize must be a whole number from 1 to ${MAX_DETAIL_PAGE_SIZE}, got ${typeof pageSize === "number" ? String(pageSize) : describeType2(pageSize)}.`
2744
+ );
2745
+ }
2746
+ if (detail["mode"] !== void 0) {
2747
+ requireOneOf(detail, "mode", DETAIL_MODES, "detail", problems);
2748
+ }
2749
+ }
2750
+ function checkTableControlsOption(controls, kind, problems) {
2751
+ if (controls === void 0 || controls === false) return;
2752
+ if (typeof controls !== "boolean" && !isRecord3(controls)) {
2753
+ problems.push(
2754
+ `tableControls must be true, false or an options object, got ${describeType2(controls)}.`
2755
+ );
2756
+ return;
2757
+ }
2758
+ if (isRecord3(controls)) {
2759
+ optionalBoolean(controls, "search", "tableControls", problems);
2760
+ optionalBoolean(controls, "sort", "tableControls", problems);
2761
+ }
2762
+ if (typeof kind === "string" && kind !== "TABLE") {
2763
+ problems.push(
2764
+ `tableControls only applies to TABLE widgets, but kind is "${kind}".`
2765
+ );
2766
+ }
2767
+ }
2768
+ function checkWidthLimits(value, problems) {
2769
+ for (const field of ["minWidth", "maxWidth"]) {
2770
+ const limit = value[field];
2771
+ if (limit !== void 0 && !isWidgetWidth(limit)) {
2772
+ problems.push(
2773
+ `${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)}.`
2774
+ );
2775
+ return;
2776
+ }
2777
+ }
2778
+ const { minWidth, maxWidth, width } = value;
2779
+ if (isWidgetWidth(minWidth) && isWidgetWidth(maxWidth) && minWidth > maxWidth) {
2780
+ problems.push(
2781
+ `minWidth (${minWidth}) must not be more than maxWidth (${maxWidth}).`
2782
+ );
2783
+ return;
2784
+ }
2785
+ if (isWidgetWidth(width)) {
2786
+ if (isWidgetWidth(minWidth) && width < minWidth) {
2787
+ problems.push(
2788
+ `width (${width}) must not be less than minWidth (${minWidth}).`
2789
+ );
2790
+ }
2791
+ if (isWidgetWidth(maxWidth) && width > maxWidth) {
2792
+ problems.push(
2793
+ `width (${width}) must not be more than maxWidth (${maxWidth}).`
2794
+ );
2795
+ }
2796
+ }
2797
+ }
2798
+ function checkLockedOption(locked, problems) {
2799
+ if (locked === void 0 || typeof locked === "boolean") return;
2800
+ if (!isRecord3(locked)) {
2801
+ problems.push(
2802
+ `locked must be true, false or an object, got ${describeType2(locked)}.`
2803
+ );
2804
+ return;
2805
+ }
2806
+ optionalBoolean(locked, "move", "locked", problems);
2075
2807
  optionalBoolean(locked, "hide", "locked", problems);
2076
2808
  optionalBoolean(locked, "minimize", "locked", problems);
2077
2809
  optionalBoolean(locked, "options", "locked", problems);
2810
+ optionalBoolean(locked, "clone", "locked", problems);
2078
2811
  }
2079
2812
  function checkOptionColumns(option, path, kind, problems) {
2080
2813
  const columns = option["columns"];
@@ -2087,7 +2820,7 @@ function checkOptionColumns(option, path, kind, problems) {
2087
2820
  const keys = [];
2088
2821
  columns.forEach((entry, index) => {
2089
2822
  const entryPath = `${path}.columns[${index}]`;
2090
- if (!isRecord2(entry)) {
2823
+ if (!isRecord3(entry)) {
2091
2824
  problems.push(
2092
2825
  `${entryPath} must be an object, got ${describeType2(entry)}.`
2093
2826
  );
@@ -2112,7 +2845,7 @@ function checkOptionColumns(option, path, kind, problems) {
2112
2845
  return keys;
2113
2846
  }
2114
2847
  function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
2115
- if (!isRecord2(option)) {
2848
+ if (!isRecord3(option)) {
2116
2849
  problems.push(`${path} must be an object, got ${describeType2(option)}.`);
2117
2850
  return;
2118
2851
  }
@@ -2144,7 +2877,7 @@ function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
2144
2877
  } else {
2145
2878
  choices.forEach((choice, index) => {
2146
2879
  const choicePath = `${path}.choices[${index}]`;
2147
- if (!isRecord2(choice)) {
2880
+ if (!isRecord3(choice)) {
2148
2881
  problems.push(
2149
2882
  `${choicePath} must be an object, got ${describeType2(choice)}.`
2150
2883
  );
@@ -2234,7 +2967,7 @@ function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
2234
2967
  } else {
2235
2968
  presets.forEach((preset, index) => {
2236
2969
  const presetPath = `${path}.presets[${index}]`;
2237
- if (!isRecord2(preset)) {
2970
+ if (!isRecord3(preset)) {
2238
2971
  problems.push(
2239
2972
  `${presetPath} must be an object, got ${describeType2(preset)}.`
2240
2973
  );
@@ -2363,7 +3096,7 @@ function checkColumnIndex(value, path, problems) {
2363
3096
  }
2364
3097
  }
2365
3098
  function checkConversion(conversion, path, baseKind, viewKind2, problems) {
2366
- if (!isRecord2(conversion)) {
3099
+ if (!isRecord3(conversion)) {
2367
3100
  problems.push(
2368
3101
  `${path} must be an object with a type, got ${describeType2(conversion)}.`
2369
3102
  );
@@ -2441,7 +3174,7 @@ function checkViews(value, problems) {
2441
3174
  } else {
2442
3175
  views.forEach((view, index) => {
2443
3176
  const path = `views[${index}]`;
2444
- if (!isRecord2(view)) {
3177
+ if (!isRecord3(view)) {
2445
3178
  problems.push(
2446
3179
  `${path} must be an object, got ${describeType2(view)}.`
2447
3180
  );
@@ -2461,11 +3194,7 @@ function checkViews(value, problems) {
2461
3194
  problems.push(`${path}.label must be a non-empty string.`);
2462
3195
  }
2463
3196
  requireOneOf(view, "kind", WIDGET_KINDS, path, problems);
2464
- if (view["convert"] === void 0) {
2465
- problems.push(
2466
- `${path}.convert is required: views supplied by a provider are not supported yet.`
2467
- );
2468
- } else {
3197
+ if (view["convert"] !== void 0) {
2469
3198
  checkConversion(
2470
3199
  view["convert"],
2471
3200
  `${path}.convert`,
@@ -2492,7 +3221,7 @@ function formatProblems(prefix, problems) {
2492
3221
  }
2493
3222
  function validateWidgetData(value, options = {}) {
2494
3223
  const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
2495
- if (!isRecord2(value)) {
3224
+ if (!isRecord3(value)) {
2496
3225
  return err(`${subject}: must be an object, got ${describeType2(value)}.`);
2497
3226
  }
2498
3227
  if (value["empty"] === true) {
@@ -2522,7 +3251,7 @@ function validateWidgetData(value, options = {}) {
2522
3251
  return ok(value);
2523
3252
  }
2524
3253
  function validateWidgetDefinition(value) {
2525
- if (!isRecord2(value)) {
3254
+ if (!isRecord3(value)) {
2526
3255
  return err(
2527
3256
  `Widget definition: must be an object, got ${describeType2(value)}.`
2528
3257
  );
@@ -2586,7 +3315,10 @@ function resolvePayload(definition, payload, options = {}) {
2586
3315
  } else {
2587
3316
  const result = validateWidgetData(payload, {
2588
3317
  widgetKey: definition.key,
2589
- expectedKind: definition.kind
3318
+ expectedKind: loadViewKind(
3319
+ definition,
3320
+ options.viewValues?.[definition.key]
3321
+ )
2590
3322
  });
2591
3323
  if (!result.ok) {
2592
3324
  return { definition, status: "error", error: result.error };
@@ -2597,7 +3329,14 @@ function resolvePayload(definition, payload, options = {}) {
2597
3329
  if (emptyText !== null) {
2598
3330
  return { definition, status: "empty", emptyText };
2599
3331
  }
2600
- return { definition, status: "ok", data: checked };
3332
+ return {
3333
+ definition,
3334
+ status: "ok",
3335
+ data: checked,
3336
+ ...hasViews(definition) ? {
3337
+ view: loadViewKey(definition, options.viewValues?.[definition.key])
3338
+ } : {}
3339
+ };
2601
3340
  }
2602
3341
  function failedWidget(definition, error) {
2603
3342
  return { definition, status: "error", error };
@@ -2606,8 +3345,15 @@ function loadingWidgets(definitions) {
2606
3345
  return definitions.map((definition) => ({ definition, status: "loading" }));
2607
3346
  }
2608
3347
  function cacheKeyFor(definition, options, values) {
2609
- const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
2610
- return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
3348
+ const base = options.cacheKey === void 0 ? definition.sourceKey ?? definition.key : options.cacheKey(definition);
3349
+ const view = loadViewKey(
3350
+ definition,
3351
+ options.viewValues?.[definition.key]
3352
+ );
3353
+ const viewPart = view === BASE_VIEW_KEY ? "" : `view=${encodeURIComponent(view)}`;
3354
+ const optionPart = values === void 0 ? "" : optionsCacheSuffix(definition, values, options);
3355
+ if (viewPart === "") return `${base}${optionPart}`;
3356
+ return optionPart === "" ? `${base}?${viewPart}` : `${base}?${viewPart}&${optionPart.slice(1)}`;
2611
3357
  }
2612
3358
  function optionValuesFor(definition, options) {
2613
3359
  return resolveOptionValues(
@@ -2618,6 +3364,9 @@ function optionValuesFor(definition, options) {
2618
3364
  }
2619
3365
  function finalizeWidget(widget, values) {
2620
3366
  if (Object.keys(values).length === 0) return widget;
3367
+ if (widget.status === "ok" && widget.view !== void 0 && widget.view !== BASE_VIEW_KEY) {
3368
+ return { ...widget, options: values };
3369
+ }
2621
3370
  if (widget.status === "ok") {
2622
3371
  const data = applyClientOptions(
2623
3372
  widget.definition,
@@ -2675,7 +3424,8 @@ async function resolveWidgetUnapplied(definition, provider, context, options = {
2675
3424
  if (controller.signal.aborted) throw new LoadInterruption();
2676
3425
  return provider(context, definition, {
2677
3426
  signal: controller.signal,
2678
- options: values
3427
+ options: values,
3428
+ view: loadViewKey(definition, options.viewValues?.[definition.key])
2679
3429
  });
2680
3430
  }),
2681
3431
  interrupted
@@ -2720,7 +3470,10 @@ async function resolveWidgets(definitions, providers, context, options = {}) {
2720
3470
  visible.map(
2721
3471
  (definition) => resolveWidget(
2722
3472
  definition,
2723
- Object.prototype.hasOwnProperty.call(providers, definition.key) ? providers[definition.key] : void 0,
3473
+ Object.prototype.hasOwnProperty.call(
3474
+ providers,
3475
+ definition.sourceKey ?? definition.key
3476
+ ) ? providers[definition.sourceKey ?? definition.key] : void 0,
2724
3477
  context,
2725
3478
  options
2726
3479
  )
@@ -2736,9 +3489,18 @@ function createWidgetLoader(options) {
2736
3489
  ([key, values]) => values === void 0 ? [] : [[key, values]]
2737
3490
  )
2738
3491
  );
3492
+ const chosenViews = new Map(
3493
+ Object.entries(baseOptions.viewValues ?? {}).flatMap(
3494
+ ([key, view]) => view === void 0 ? [] : [[key, view]]
3495
+ )
3496
+ );
2739
3497
  const resolveOptions = baseOptions;
2740
3498
  function currentOptions() {
2741
- return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
3499
+ return {
3500
+ ...resolveOptions,
3501
+ optionValues: Object.fromEntries(chosen),
3502
+ viewValues: Object.fromEntries(chosenViews)
3503
+ };
2742
3504
  }
2743
3505
  const unapplied = /* @__PURE__ */ new Map();
2744
3506
  const visible = widgetsFor(definitions, context.roles);
@@ -2800,7 +3562,11 @@ function createWidgetLoader(options) {
2800
3562
  }
2801
3563
  }
2802
3564
  if (!isCurrent()) return;
2803
- const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
3565
+ const providerKey = definition.sourceKey ?? key;
3566
+ const provider = Object.prototype.hasOwnProperty.call(
3567
+ providers,
3568
+ providerKey
3569
+ ) ? providers[providerKey] : void 0;
2804
3570
  const result = await resolveWidgetUnapplied(definition, provider, context, {
2805
3571
  ...runOptions,
2806
3572
  signal: controller.signal
@@ -2867,6 +3633,23 @@ function createWidgetLoader(options) {
2867
3633
  }
2868
3634
  start(definition, true);
2869
3635
  }
3636
+ function setView(key, view) {
3637
+ if (disposed) return;
3638
+ const definition = definitionFor(key);
3639
+ if (definition === void 0) return;
3640
+ const before = loadViewKey(definition, chosenViews.get(key));
3641
+ if (view === void 0 || view === "") chosenViews.delete(key);
3642
+ else chosenViews.set(key, view);
3643
+ const after = loadViewKey(definition, chosenViews.get(key));
3644
+ if (before === after || !started.has(key)) return;
3645
+ const current = snapshot.find(
3646
+ (widget) => widget.definition.key === key
3647
+ );
3648
+ if (current !== void 0 && current.status !== "loading") {
3649
+ replace(key, { definition, status: "loading" });
3650
+ }
3651
+ start(definition, true);
3652
+ }
2870
3653
  function dispose() {
2871
3654
  disposed = true;
2872
3655
  abortAll(new Error("The widget loader was disposed."));
@@ -2885,488 +3668,11 @@ function createWidgetLoader(options) {
2885
3668
  loadAll,
2886
3669
  refresh,
2887
3670
  setOptions,
3671
+ setView,
2888
3672
  dispose
2889
3673
  };
2890
3674
  }
2891
3675
 
2892
- // src/core/pages.ts
2893
- var DEFAULT_PAGE_KEY = "default";
2894
- var DEFAULT_PAGE_ROWS = 4;
2895
- function rowsAllowed(page, options) {
2896
- const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
2897
- return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
2898
- }
2899
- function hasPages(layout) {
2900
- return layout.pages !== void 0 && layout.pages.length > 0;
2901
- }
2902
- function pageList(layout) {
2903
- if (layout.pages !== void 0 && layout.pages.length > 0) {
2904
- return layout.pages;
2905
- }
2906
- return [
2907
- {
2908
- key: DEFAULT_PAGE_KEY,
2909
- title: "Page 1",
2910
- order: layout.order,
2911
- hidden: layout.hidden,
2912
- minimized: layout.minimized
2913
- }
2914
- ];
2915
- }
2916
- function withExplicitPages(layout) {
2917
- if (hasPages(layout)) return layout;
2918
- return {
2919
- ...layout,
2920
- order: [],
2921
- hidden: [],
2922
- minimized: [],
2923
- pages: pageList(layout)
2924
- };
2925
- }
2926
- function sameKeys(a, b) {
2927
- return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
2928
- }
2929
- function pageLayout(layout, pageKey) {
2930
- if (!hasPages(layout))
2931
- return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
2932
- const page = layout.pages?.find(
2933
- (p) => p.key === pageKey
2934
- );
2935
- if (page === void 0) return void 0;
2936
- const { pages, ...rest } = layout;
2937
- return {
2938
- ...rest,
2939
- order: page.order,
2940
- hidden: page.hidden,
2941
- minimized: page.minimized
2942
- };
2943
- }
2944
- function withPageLayout(layout, pageKey, next) {
2945
- const current = pageLayout(layout, pageKey);
2946
- if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
2947
- return layout;
2948
- }
2949
- if (!hasPages(layout)) {
2950
- return {
2951
- ...layout,
2952
- order: next.order,
2953
- hidden: next.hidden,
2954
- minimized: next.minimized
2955
- };
2956
- }
2957
- return {
2958
- ...layout,
2959
- pages: (layout.pages ?? []).map(
2960
- (page) => page.key === pageKey ? {
2961
- ...page,
2962
- order: next.order,
2963
- hidden: next.hidden,
2964
- minimized: next.minimized
2965
- } : page
2966
- )
2967
- };
2968
- }
2969
- function normalizedTitle(title) {
2970
- return title.trim().normalize("NFKC").toLowerCase();
2971
- }
2972
- function resolvePageKey(layout, reference) {
2973
- if (reference === void 0 || reference === "") return void 0;
2974
- const pages = pageList(layout);
2975
- const byKey = pages.find((p) => p.key === reference);
2976
- if (byKey !== void 0) return byKey.key;
2977
- const wanted = normalizedTitle(reference);
2978
- return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
2979
- }
2980
- function homePageKey(layout, item) {
2981
- return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
2982
- }
2983
- function assignPages(definitions, layout) {
2984
- const pages = pageList(layout);
2985
- const assigned = new Map(pages.map((p) => [p.key, []]));
2986
- for (const definition of definitions) {
2987
- let pageKey;
2988
- if (!resolveWidgetLock(definition).move) {
2989
- pageKey = pages.find(
2990
- (p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
2991
- )?.key;
2992
- }
2993
- assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
2994
- }
2995
- return assigned;
2996
- }
2997
- function pageOf(definitions, layout, key) {
2998
- for (const [pageKey, widgets] of assignPages(definitions, layout)) {
2999
- if (widgets.some((w) => w.key === key)) return pageKey;
3000
- }
3001
- return void 0;
3002
- }
3003
- function pageWidgets(definitions, layout, pageKey) {
3004
- const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
3005
- const view = pageLayout(layout, pageKey);
3006
- return view === void 0 ? [] : visibleWidgets(onPage, view);
3007
- }
3008
- function pageRoom(definitions, layout, pageKey, options = {}) {
3009
- const page = pageList(layout).find(
3010
- (p) => p.key === pageKey
3011
- );
3012
- if (page === void 0) return void 0;
3013
- const maxRows = rowsAllowed(page, options);
3014
- const rowsUsed = placeRows(
3015
- pageWidgets(definitions, layout, pageKey)
3016
- ).length;
3017
- return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
3018
- }
3019
- function describePage(page) {
3020
- return `Page "${page.title}"`;
3021
- }
3022
- function titleOf(item) {
3023
- return item.title ?? item.key;
3024
- }
3025
- function checkTitle(layout, title, exceptKey) {
3026
- const trimmed = title.trim();
3027
- if (trimmed === "") return err("A page title must not be empty.");
3028
- if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
3029
- return err(
3030
- `Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
3031
- );
3032
- }
3033
- const wanted = normalizedTitle(trimmed);
3034
- const clash = pageList(layout).find(
3035
- (p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
3036
- );
3037
- if (clash !== void 0) {
3038
- return err(`Another page is already called "${clash.title}".`);
3039
- }
3040
- return ok(trimmed);
3041
- }
3042
- function nextPageKey(layout) {
3043
- const keys = new Set(pageList(layout).map((p) => p.key));
3044
- let n = 1;
3045
- while (keys.has(`page-${n}`)) n += 1;
3046
- return `page-${n}`;
3047
- }
3048
- function nextPageTitle(layout) {
3049
- const used = new Set(
3050
- pageList(layout).map((p) => normalizedTitle(p.title))
3051
- );
3052
- let n = pageList(layout).length + 1;
3053
- while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
3054
- return `Page ${n}`;
3055
- }
3056
- function addPage(layout, options = {}) {
3057
- if (pageList(layout).length >= MAX_PARSED_PAGES) {
3058
- return err(
3059
- `The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
3060
- );
3061
- }
3062
- const key = options.key ?? nextPageKey(layout);
3063
- if (pageList(layout).some((p) => p.key === key)) {
3064
- return err(`A page with the key "${key}" already exists.`);
3065
- }
3066
- const title = checkTitle(
3067
- layout,
3068
- options.title ?? nextPageTitle(layout)
3069
- );
3070
- if (!title.ok) return title;
3071
- const base = withExplicitPages(layout);
3072
- const page = {
3073
- key,
3074
- title: title.value,
3075
- order: [],
3076
- hidden: [],
3077
- minimized: []
3078
- };
3079
- return ok({ ...base, pages: [...base.pages ?? [], page] });
3080
- }
3081
- function renamePage(layout, pageKey, title) {
3082
- const page = pageList(layout).find(
3083
- (p) => p.key === pageKey
3084
- );
3085
- if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
3086
- const checked = checkTitle(layout, title, pageKey);
3087
- if (!checked.ok) return checked;
3088
- if (checked.value === page.title) return ok(layout);
3089
- const base = withExplicitPages(layout);
3090
- return ok({
3091
- ...base,
3092
- pages: (base.pages ?? []).map(
3093
- (p) => p.key === pageKey ? { ...p, title: checked.value } : p
3094
- )
3095
- });
3096
- }
3097
- function movePage(layout, pageKey, offset) {
3098
- const pages = layout.pages ?? [];
3099
- const from = pages.findIndex((p) => p.key === pageKey);
3100
- if (from === -1) return layout;
3101
- const to = Math.max(
3102
- 0,
3103
- Math.min(pages.length - 1, from + Math.trunc(offset))
3104
- );
3105
- if (to === from) return layout;
3106
- const next = [...pages];
3107
- const [moved] = next.splice(from, 1);
3108
- next.splice(to, 0, moved);
3109
- return { ...layout, pages: next };
3110
- }
3111
- function overflowing(definitions, layout, options) {
3112
- return pageList(layout).find(
3113
- (page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
3114
- );
3115
- }
3116
- function removePage(definitions, layout, pageKey, options = {}) {
3117
- const pages = pageList(layout);
3118
- const index = pages.findIndex((p) => p.key === pageKey);
3119
- const page = pages[index];
3120
- if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
3121
- if (pages.length === 1) {
3122
- return err(
3123
- `${describePage(page)} is the only page, so it can't be removed.`
3124
- );
3125
- }
3126
- const target = pages[index > 0 ? index - 1 : 1];
3127
- const merged = {
3128
- ...layout,
3129
- pages: pages.filter((p) => p.key !== pageKey).map(
3130
- (p) => p.key === target.key ? {
3131
- ...p,
3132
- order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
3133
- hidden: [...p.hidden, ...page.hidden],
3134
- minimized: [...p.minimized, ...page.minimized]
3135
- } : p
3136
- )
3137
- };
3138
- const full = overflowing(
3139
- definitions,
3140
- merged,
3141
- options
3142
- );
3143
- if (full !== void 0) {
3144
- return err(
3145
- `${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.`
3146
- );
3147
- }
3148
- return ok(merged);
3149
- }
3150
- function without(keys, key) {
3151
- return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
3152
- }
3153
- function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
3154
- const widget = definitions.find((d) => d.key === widgetKey);
3155
- if (widget === void 0) {
3156
- return err(
3157
- `Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
3158
- );
3159
- }
3160
- const pages = pageList(layout);
3161
- const target = pages.find((p) => p.key === targetKey);
3162
- if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
3163
- const sourceKey = pageOf(definitions, layout, widgetKey);
3164
- if (sourceKey === targetKey) return ok(layout);
3165
- if (resolveWidgetLock(widget).move) {
3166
- return err(
3167
- `Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
3168
- );
3169
- }
3170
- const base = withExplicitPages(layout);
3171
- const source = (base.pages ?? []).find(
3172
- (p) => p.key === sourceKey
3173
- );
3174
- const hidden = source.hidden.includes(widgetKey);
3175
- const minimized = source.minimized.includes(widgetKey);
3176
- const targetOrder = pageWidgets(
3177
- definitions,
3178
- base,
3179
- targetKey
3180
- ).map((d) => d.key);
3181
- const moved = {
3182
- ...base,
3183
- pages: (base.pages ?? []).map((p) => {
3184
- if (p.key === sourceKey) {
3185
- return {
3186
- ...p,
3187
- order: without(p.order, widgetKey),
3188
- hidden: without(p.hidden, widgetKey),
3189
- minimized: without(p.minimized, widgetKey)
3190
- };
3191
- }
3192
- if (p.key === targetKey) {
3193
- return {
3194
- ...p,
3195
- order: hidden ? p.order : [...targetOrder, widgetKey],
3196
- hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
3197
- minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
3198
- };
3199
- }
3200
- return p;
3201
- })
3202
- };
3203
- if (!hidden) {
3204
- const allowed = rowsAllowed(target, options);
3205
- const needed = placeRows(
3206
- pageWidgets(definitions, moved, targetKey)
3207
- ).length;
3208
- if (needed > allowed) {
3209
- return err(
3210
- `${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
3211
- );
3212
- }
3213
- }
3214
- return ok(moved);
3215
- }
3216
- function withoutLockField(definition) {
3217
- if (definition.locked === void 0) return definition;
3218
- const copy = { ...definition };
3219
- delete copy.locked;
3220
- return copy;
3221
- }
3222
- function uniqueTitle2(taken, title, index) {
3223
- let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
3224
- if (base === "") base = `Page ${index + 1}`;
3225
- let candidate = base;
3226
- let n = 1;
3227
- while (taken.has(normalizedTitle(candidate))) {
3228
- const suffix = ` (#${n})`;
3229
- candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
3230
- n += 1;
3231
- }
3232
- taken.add(normalizedTitle(candidate));
3233
- return candidate;
3234
- }
3235
- function samePages(a, b) {
3236
- return a.length === b.length && a.every((p, i) => {
3237
- const q = b[i];
3238
- 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);
3239
- });
3240
- }
3241
- function normalizeLayout(definitions, layout, options = {}) {
3242
- const known = pruneLayout(definitions, layout);
3243
- const enforced = enforceLocks(definitions, known, options);
3244
- const locked = normalizeSettings(
3245
- definitions,
3246
- enforced,
3247
- options
3248
- );
3249
- if (!hasPages(locked)) return locked === layout ? layout : locked;
3250
- const defs = effectiveDefinitions(
3251
- options.overrideLocks === true ? definitions.map(withoutLockField) : definitions,
3252
- locked,
3253
- options
3254
- );
3255
- const taken = /* @__PURE__ */ new Set();
3256
- const seenKeys = /* @__PURE__ */ new Set();
3257
- const held = /* @__PURE__ */ new Set();
3258
- let pages = [];
3259
- (locked.pages ?? []).forEach((page, index) => {
3260
- if (seenKeys.has(page.key)) return;
3261
- seenKeys.add(page.key);
3262
- const keep = (list) => [
3263
- ...new Set(list.filter((k) => !held.has(k)))
3264
- ];
3265
- const next = {
3266
- ...page,
3267
- title: uniqueTitle2(taken, page.title, index),
3268
- order: keep(page.order),
3269
- hidden: keep(page.hidden),
3270
- minimized: keep(page.minimized)
3271
- };
3272
- for (const k of [...next.order, ...next.hidden]) held.add(k);
3273
- pages.push(next);
3274
- });
3275
- const work = () => ({ ...locked, pages });
3276
- const pinned = defs.filter((d) => resolveWidgetLock(d).move);
3277
- for (const widget of pinned) {
3278
- const home = homePageKey(work(), widget);
3279
- const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
3280
- const wasHidden = pages.some((p) => keysHere(p, "hidden"));
3281
- const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
3282
- if (!wasHidden && !wasMinimized) continue;
3283
- pages = pages.map((p) => {
3284
- let next = p;
3285
- if (keysHere(p, "hidden")) {
3286
- next = { ...next, hidden: without(next.hidden, widget.key) };
3287
- }
3288
- if (keysHere(p, "minimized")) {
3289
- next = { ...next, minimized: without(next.minimized, widget.key) };
3290
- }
3291
- if (p.key === home) {
3292
- next = {
3293
- ...next,
3294
- hidden: wasHidden && !next.hidden.includes(widget.key) ? [...next.hidden, widget.key] : next.hidden,
3295
- minimized: wasMinimized && !next.minimized.includes(widget.key) ? [...next.minimized, widget.key] : next.minimized
3296
- };
3297
- }
3298
- return next;
3299
- });
3300
- }
3301
- const placedKeys = /* @__PURE__ */ new Set();
3302
- for (const p of pages) {
3303
- for (const k of [...p.order, ...p.hidden]) placedKeys.add(k);
3304
- }
3305
- const pinnedKeys = new Set(pinned.map((d) => d.key));
3306
- const unplaced = defs.filter((d) => !placedKeys.has(d.key) && !pinnedKeys.has(d.key)).map((definition, index) => ({ definition, index })).sort(
3307
- (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
3308
- ).map(({ definition }) => definition);
3309
- for (const widget of unplaced) {
3310
- const home = homePageKey(work(), widget);
3311
- pages = pages.map(
3312
- (p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
3313
- );
3314
- }
3315
- for (let i = 0; i < pages.length; i += 1) {
3316
- const page = pages[i];
3317
- const shown = pageWidgets(defs, work(), page.key);
3318
- const fits = fitCount(shown, rowsAllowed(page, options));
3319
- if (fits >= shown.length) continue;
3320
- const moving = shown.slice(fits).filter((d) => !pinnedKeys.has(d.key)).map((d) => d.key);
3321
- if (moving.length === 0) continue;
3322
- const moveSet = new Set(moving);
3323
- const movedMinimized = page.minimized.filter(
3324
- (k) => moveSet.has(k)
3325
- );
3326
- pages = pages.map(
3327
- (p) => p.key === page.key ? {
3328
- ...p,
3329
- order: p.order.filter((k) => !moveSet.has(k)),
3330
- minimized: p.minimized.filter((k) => !moveSet.has(k))
3331
- } : p
3332
- );
3333
- let placed = false;
3334
- for (let j = i + 1; j < pages.length && !placed; j += 1) {
3335
- const candidate = pages[j];
3336
- const trial = pages.map(
3337
- (p) => p.key === candidate.key ? {
3338
- ...p,
3339
- order: [...moving, ...p.order],
3340
- minimized: [...p.minimized, ...movedMinimized]
3341
- } : p
3342
- );
3343
- const items = pageWidgets(
3344
- defs,
3345
- { ...locked, pages: trial },
3346
- candidate.key
3347
- );
3348
- if (placeRows(items).length <= rowsAllowed(candidate, options)) {
3349
- pages = trial;
3350
- placed = true;
3351
- }
3352
- }
3353
- if (!placed) {
3354
- const key = nextPageKey(work());
3355
- const title = uniqueTitle2(
3356
- new Set(pages.map((p) => normalizedTitle(p.title))),
3357
- `Page ${pages.length + 1}`,
3358
- pages.length
3359
- );
3360
- pages = [
3361
- ...pages,
3362
- { key, title, order: moving, hidden: [], minimized: movedMinimized }
3363
- ];
3364
- }
3365
- }
3366
- const result = work();
3367
- return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) && locked.settings === layout.settings ? layout : result;
3368
- }
3369
-
3370
3676
  // src/core/scale.ts
3371
3677
  function niceStep(value) {
3372
3678
  if (!(value > 0) || !Number.isFinite(value)) return 1;
@@ -4196,6 +4502,6 @@ function indent(block) {
4196
4502
  return block.split("\n").map((line) => ` ${line}`).join("\n");
4197
4503
  }
4198
4504
 
4199
- export { BASE_VIEW_KEY, DATE_RANGE_PRESETS, DEFAULT_COMPARISON_LABEL, DEFAULT_CURRENCY, DEFAULT_DETAIL_PAGE_SIZE, DEFAULT_EMPTY_TEXT, DEFAULT_MAX_LAYOUT_BYTES, DEFAULT_PAGE_KEY, DEFAULT_PAGE_ROWS, DETAIL_MODES, EMPTY_LAYOUT, LAYOUT_ENVELOPE_VERSION, MAX_DETAIL_PAGE_SIZE, MAX_GRAPH_SERIES, MAX_PAGE_ROWS, MAX_PAGE_TITLE_LENGTH, MAX_PARSED_PAGES, MAX_PARSED_SETTINGS, MAX_REPORTED_PROBLEMS, MAX_TEXT_OPTION_LENGTH, MAX_WIDGET_OPTIONS, MAX_WIDGET_TITLE_LENGTH, MAX_WIDGET_VIEWS, MAX_WIDGET_WIDTH, MIN_WIDGET_WIDTH, OPTION_KEY_PATTERN, THEME_TOKEN_VARIABLES, VALUE_FORMATS, VIEW_CONVERSIONS, WIDGET_FILLS, WIDGET_KINDS, WIDGET_OPTION_TYPES, WIDGET_SIZES, WIDTH_COLUMNS, addPage, applyClientOptions, applySettings, applyView, assignPages, baseColumnSpan, cacheKeyFor, clearWidgetSettings, coerceOptionValue, convertView, createLayoutPersistence, createTheme, createWidgetLoader, defaultDetailQuery, defaultOptionValue, defineWidget, deriveDetailData, describeUnknownError, effectiveDefinitions, effectiveViewKey, emptyTextFor, emptyWidget, enforceLocks, err, failedWidget, fillColumnSpans, fillWidthSpans, fillsHeight, fillsWidth, finalizeWidget, findView, fitCount, formatCount, formatPercentChange, formatValue, fromLegacyDefinition, hasPages, hasViews, hiddenWidgets, hideWidget, isClientApplied, isColumnKeyFor, isFlexible, isHidden, isIsoInterval, isMinimized, isOptionsLocked, isSafeHref, isSafeImageUrl, isTitleInUse, isWidgetEmptyState, isWidgetKind, isWidgetVisibleTo, isWidgetWidth, itemWidth, kindLabel, kpiDelta, labelStride, loadingWidgets, memoryAdapter, minimizeWidget, movePage, moveWidget, moveWidgetBy, moveWidgetDown, moveWidgetToPage, moveWidgetUp, nextPageKey, niceDomain, niceStep, normalizeLayout, normalizeSettings, normalizeTitle, ok, optionValuesFor, optionValuesFromLayout, optionsCacheSuffix, pageLayout, pageList, pageOf, pageRoom, pageWidgets, parseDetailQuery, parseLayout, parseSettings, parseSortValue, percentChange, placeRows, pruneLayout, queryRows, readOnly, removePage, renamePage, resolveDateRange, resolveDetailOptions, resolveOptionValues, resolvePageKey, resolvePayload, resolveTableControls, resolveWidget, resolveWidgetLock, resolveWidgetUnapplied, resolveWidgets, restoreWidget, sameOptionValues, serializeDetailQuery, serializeLayout, serializeSettings, setWidgetSettings, shareOf, showWidget, sortDefinitions, sortFromOptions, tableDetailData, toggleHidden, toggleMinimized, upgradeLegacyWidgetData, usesWidthColumns, utf8Length, validateDetailData, validateWidgetData, validateWidgetDefinition, viewChoices, viewKind, viewNote, visibleWidgets, widgetOptionValues, widgetsFor, widthForSize, widthRange, widthShareLabel, withEncoding, withFallback, withLogging, withPageLayout, withPrefix, withReadCache, withRetry };
4505
+ export { BASE_VIEW_KEY, DATE_RANGE_PRESETS, DEFAULT_COMPARISON_LABEL, DEFAULT_CURRENCY, DEFAULT_DETAIL_PAGE_SIZE, DEFAULT_EMPTY_TEXT, DEFAULT_MAX_LAYOUT_BYTES, DEFAULT_PAGE_KEY, DEFAULT_PAGE_ROWS, DETAIL_MODES, EMPTY_LAYOUT, LAYOUT_ENVELOPE_VERSION, MAX_DETAIL_PAGE_SIZE, MAX_GRAPH_SERIES, MAX_PAGE_ROWS, MAX_PAGE_TITLE_LENGTH, MAX_PARSED_CLONES, MAX_PARSED_PAGES, MAX_PARSED_SETTINGS, MAX_REPORTED_PROBLEMS, MAX_TEXT_OPTION_LENGTH, MAX_WIDGET_OPTIONS, MAX_WIDGET_TITLE_LENGTH, MAX_WIDGET_VIEWS, MAX_WIDGET_WIDTH, MIN_WIDGET_WIDTH, OPTION_KEY_PATTERN, THEME_TOKEN_VARIABLES, VALUE_FORMATS, VIEW_CONVERSIONS, WIDGET_FILLS, WIDGET_KINDS, WIDGET_OPTION_TYPES, WIDGET_SIZES, WIDTH_COLUMNS, addPage, applyClientOptions, applySettings, applyView, assignPages, baseColumnSpan, cacheKeyFor, canDuplicate, cleanClones, clearWidgetSettings, cloneNumber, coerceOptionValue, convertView, createLayoutPersistence, createTheme, createWidgetLoader, defaultDetailQuery, defaultOptionValue, defineWidget, deleteClone, deriveDetailData, describeUnknownError, duplicateWidget, effectiveDefinitions, effectiveViewKey, emptyTextFor, emptyWidget, enforceLocks, err, failedWidget, fillColumnSpans, fillWidthSpans, fillsHeight, fillsWidth, finalizeWidget, findView, fitCount, formatCount, formatPercentChange, formatValue, fromLegacyDefinition, hasPages, hasViews, hiddenWidgets, hideWidget, isClientApplied, isColumnKeyFor, isFlexible, isHidden, isIsoInterval, isMinimized, isOptionsLocked, isProviderView, isSafeHref, isSafeImageUrl, isTitleInUse, isWidgetEmptyState, isWidgetKind, isWidgetVisibleTo, isWidgetWidth, itemWidth, kindLabel, kpiDelta, labelStride, loadViewKey, loadViewKind, loadingWidgets, memoryAdapter, minimizeWidget, movePage, moveWidget, moveWidgetBy, moveWidgetDown, moveWidgetToPage, moveWidgetUp, nextCloneKey, nextPageKey, niceDomain, niceStep, normalizeLayout, normalizeSettings, normalizeTitle, ok, optionValuesFor, optionValuesFromLayout, optionsCacheSuffix, pageLayout, pageList, pageOf, pageRoom, pageWidgets, parseClones, parseDetailQuery, parseLayout, parseSettings, parseSortValue, percentChange, placeRows, pruneLayout, queryRows, readOnly, removePage, renamePage, resolveDateRange, resolveDetailOptions, resolveOptionValues, resolvePageKey, resolvePayload, resolveTableControls, resolveWidget, resolveWidgetLock, resolveWidgetUnapplied, resolveWidgets, restoreWidget, sameOptionValues, serializeClones, serializeDetailQuery, serializeLayout, serializeSettings, setWidgetSettings, shareOf, showWidget, sortDefinitions, sortFromOptions, suggestedViewTitle, tableDetailData, toggleHidden, toggleMinimized, upgradeLegacyWidgetData, usesWidthColumns, utf8Length, validateDetailData, validateWidgetData, validateWidgetDefinition, viewChoices, viewKind, viewNote, viewValuesFromLayout, visibleWidgets, widgetOptionValues, widgetsFor, widthForSize, widthRange, widthShareLabel, withClones, withEncoding, withFallback, withLogging, withPageLayout, withPrefix, withReadCache, withRetry };
4200
4506
  //# sourceMappingURL=core.js.map
4201
4507
  //# sourceMappingURL=core.js.map