@richardmcquiston01/dashboard-widgets-toolkit 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,277 @@ 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 isKnownView(item, key) {
341
+ return key !== void 0 && (key === BASE_VIEW_KEY || findView(item, key) !== void 0);
342
+ }
343
+ function effectiveViewKey(item, chosen) {
344
+ if (!hasViews(item)) return BASE_VIEW_KEY;
345
+ if (isKnownView(item, chosen)) return chosen;
346
+ if (isKnownView(item, item.defaultView)) return item.defaultView;
347
+ return BASE_VIEW_KEY;
348
+ }
349
+ function viewKind(item, chosen) {
350
+ return findView(item, effectiveViewKey(item, chosen))?.kind ?? item.kind;
351
+ }
352
+ function cellValue(cell) {
353
+ const value = cell?.value;
354
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
355
+ }
356
+ function columnName(table, index) {
357
+ return table.columns[index]?.label ?? `#${index + 1}`;
358
+ }
359
+ function checkColumn(table, index, field, context) {
360
+ if (!Number.isInteger(index) || index < 0 || index >= table.columns.length) {
361
+ return `${context.where}: ${field} column ${index} does not exist (the table has ${table.columns.length} columns, numbered from 0).`;
362
+ }
363
+ return void 0;
364
+ }
365
+ function numericColumn(table, index, context) {
366
+ const numbers = [];
367
+ for (const [rowIndex, row] of table.rows.entries()) {
368
+ const value = cellValue(row[index]);
369
+ if (value === void 0) {
370
+ return err(
371
+ `${context.where}: row ${rowIndex + 1}, column ${index + 1} ("${columnName(table, index)}") has no numeric value. Add value to the cells.`
372
+ );
373
+ }
374
+ numbers.push(value);
375
+ }
376
+ return ok(numbers);
377
+ }
378
+ function tableToBarList(table, conversion, context) {
379
+ const bad = checkColumn(table, conversion.label, "label", context) ?? checkColumn(table, conversion.value, "value", context);
380
+ if (bad !== void 0) return err(bad);
381
+ const numbers = numericColumn(table, conversion.value, context);
382
+ if (!numbers.ok) return numbers;
383
+ const items = table.rows.map((row, index) => ({
384
+ label: row[conversion.label]?.text ?? "",
385
+ value: numbers.value[index],
386
+ display: row[conversion.value]?.text ?? String(numbers.value[index])
387
+ }));
388
+ return ok({ kind: "BAR_LIST", items });
389
+ }
390
+ function tableToGraph(table, conversion, context) {
391
+ if (conversion.values.length === 0 || conversion.values.length > MAX_GRAPH_SERIES) {
392
+ return err(
393
+ `${context.where}: a chart needs 1 to ${MAX_GRAPH_SERIES} series columns, got ${conversion.values.length}.`
394
+ );
395
+ }
396
+ let bad = checkColumn(
397
+ table,
398
+ conversion.label,
399
+ "label",
400
+ context
401
+ );
402
+ for (const index of conversion.values) {
403
+ bad ??= checkColumn(table, index, "series", context);
404
+ }
405
+ if (bad !== void 0) return err(bad);
406
+ const series = [];
407
+ for (const index of conversion.values) {
408
+ const numbers = numericColumn(table, index, context);
409
+ if (!numbers.ok) return numbers;
410
+ series.push({
411
+ name: columnName(table, index),
412
+ points: table.rows.map((row, rowIndex) => ({
413
+ label: row[conversion.label]?.text ?? "",
414
+ value: numbers.value[rowIndex]
415
+ }))
416
+ });
417
+ }
418
+ return ok({
419
+ kind: "GRAPH",
420
+ series,
421
+ chartType: conversion.chartType,
422
+ valueFormat: conversion.valueFormat ?? "number",
423
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency },
424
+ xLabel: columnName(table, conversion.label)
425
+ });
426
+ }
427
+ function barListToTable(list, conversion) {
428
+ return ok({
429
+ kind: "TABLE",
430
+ columns: [
431
+ { label: conversion.labelHeading ?? "Name" },
432
+ { label: conversion.valueHeading ?? "Value", numeric: true }
433
+ ],
434
+ rows: list.items.map((item) => [
435
+ { text: item.label },
436
+ { text: item.display ?? String(item.value), value: item.value }
437
+ ])
438
+ });
439
+ }
440
+ function graphToTable(graph, context) {
441
+ const first = graph.series[0];
442
+ if (first === void 0)
443
+ return err(`${context.where}: the chart has no series.`);
444
+ const format = {
445
+ ...graph.currency === void 0 ? {} : { currency: graph.currency },
446
+ ...context.locale === void 0 ? {} : { locale: context.locale }
447
+ };
448
+ return ok({
449
+ kind: "TABLE",
450
+ columns: [
451
+ { label: graph.xLabel ?? "Label" },
452
+ ...graph.series.map((series) => ({ label: series.name, numeric: true }))
453
+ ],
454
+ rows: first.points.map((point, index) => [
455
+ { text: point.label },
456
+ ...graph.series.map((series) => {
457
+ const value = series.points[index]?.value;
458
+ return value === void 0 ? { text: "\u2014" } : { text: formatValue(value, graph.valueFormat, format), value };
459
+ })
460
+ ])
461
+ });
462
+ }
463
+ function barListToGraph(list, conversion) {
464
+ return ok({
465
+ kind: "GRAPH",
466
+ series: [
467
+ {
468
+ name: conversion.seriesName ?? "Value",
469
+ points: list.items.map((item) => ({
470
+ label: item.label,
471
+ value: item.value
472
+ }))
473
+ }
474
+ ],
475
+ chartType: conversion.chartType,
476
+ valueFormat: conversion.valueFormat ?? "number",
477
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency }
478
+ });
479
+ }
480
+ function convertView(widgetKey, view, data, options = {}) {
481
+ const conversion = view.convert;
482
+ const context = {
483
+ where: `Widget ${JSON.stringify(widgetKey)} view ${JSON.stringify(view.key)}`,
484
+ ...options.locale === void 0 ? {} : { locale: options.locale }
485
+ };
486
+ const expected = VIEW_CONVERSIONS[conversion.type]?.from;
487
+ if (expected === void 0) {
488
+ return err(
489
+ `${context.where}: unknown conversion type ${JSON.stringify(conversion.type)}.`
490
+ );
491
+ }
492
+ if (data.kind !== expected) {
493
+ return err(
494
+ `${context.where}: ${conversion.type} reads a ${expected} payload, but the widget returned ${data.kind}.`
495
+ );
496
+ }
497
+ switch (conversion.type) {
498
+ case "tableToBarList":
499
+ return tableToBarList(data, conversion, context);
500
+ case "tableToGraph":
501
+ return tableToGraph(data, conversion, context);
502
+ case "barListToTable":
503
+ return barListToTable(data, conversion);
504
+ case "graphToTable":
505
+ return graphToTable(data, context);
506
+ case "barListToGraph":
507
+ return barListToGraph(data, conversion);
508
+ }
509
+ }
510
+ function viewNote(view, data) {
511
+ if (data?.kind !== "TABLE") return "";
512
+ const conversion = view.convert;
513
+ let indexes;
514
+ if (conversion.type === "tableToBarList") {
515
+ indexes = [conversion.label, conversion.value];
516
+ } else if (conversion.type === "tableToGraph") {
517
+ indexes = [conversion.label, ...conversion.values];
518
+ } else {
519
+ return "";
520
+ }
521
+ const names = indexes.map(
522
+ (index) => data.columns[index]?.label ?? `#${index + 1}`
523
+ );
524
+ return `Shows ${names.join(", ")} only.`;
525
+ }
526
+ function applyView(widget, chosen, options = {}) {
527
+ const key = effectiveViewKey(widget.definition, chosen);
528
+ const view = findView(widget.definition, key);
529
+ if (view === void 0) return widget;
530
+ const definition = {
531
+ ...widget.definition,
532
+ kind: view.kind
533
+ };
534
+ if (widget.status === "loading") return { ...widget, definition };
535
+ if (widget.status === "empty") return { ...widget, definition };
536
+ if (widget.status === "error") return { ...widget, definition };
537
+ const converted = convertView(
538
+ widget.definition.key,
539
+ view,
540
+ widget.data,
541
+ options
542
+ );
543
+ if (!converted.ok) {
544
+ return {
545
+ definition,
546
+ status: "error",
547
+ error: converted.error,
548
+ ...widget.options === void 0 ? {} : { options: widget.options }
549
+ };
550
+ }
551
+ return { ...widget, definition, data: converted.value };
552
+ }
553
+
554
+ // src/core/url.ts
555
+ var SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
556
+ var IGNORED_IN_SCHEME = /[\u0000- \u007f]/g;
557
+ function schemeOf(url) {
558
+ const match = SCHEME_PATTERN.exec(
559
+ url.replace(IGNORED_IN_SCHEME, "")
560
+ );
561
+ return match === null ? null : (match[1] ?? "").toLowerCase();
562
+ }
563
+ function isSafeHref(url) {
564
+ if (url.trim() === "") {
565
+ return false;
566
+ }
567
+ const scheme = schemeOf(url);
252
568
  return scheme === null || scheme === "http" || scheme === "https" || scheme === "mailto";
253
569
  }
254
570
  function isSafeImageUrl(url) {
@@ -289,15 +605,15 @@ function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
289
605
  function fold(text) {
290
606
  return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
291
607
  }
292
- function cellValue(cell) {
608
+ function cellValue2(cell) {
293
609
  return cell.value ?? cell.text;
294
610
  }
295
611
  function cellMatches(cell, needle) {
296
- return fold(String(cellValue(cell))).includes(needle);
612
+ return fold(String(cellValue2(cell))).includes(needle);
297
613
  }
298
614
  function compareCells(a, b, collator) {
299
- const left = cellValue(a);
300
- const right = cellValue(b);
615
+ const left = cellValue2(a);
616
+ const right = cellValue2(b);
301
617
  if (typeof left === "number" && typeof right === "number") {
302
618
  return left - right;
303
619
  }
@@ -589,1267 +905,1643 @@ function deriveDetailData(data, locale) {
589
905
  return void 0;
590
906
  }
591
907
 
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;
908
+ // src/core/options.ts
909
+ var DATE_RANGE_PRESETS = [
910
+ "today",
911
+ "yesterday",
912
+ "last7",
913
+ "last30",
914
+ "last90",
915
+ "thisMonth",
916
+ "lastMonth",
917
+ "thisYear",
918
+ "lastYear"
919
+ ];
920
+ var WIDGET_OPTION_TYPES = [
921
+ "choice",
922
+ "number",
923
+ "boolean",
924
+ "text",
925
+ "dateRange",
926
+ "color",
927
+ "sort",
928
+ "columns"
929
+ ];
930
+ var MAX_WIDGET_OPTIONS = 20;
931
+ var MAX_TEXT_OPTION_LENGTH = 500;
932
+ var OPTION_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,39}$/;
933
+ var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
934
+ var INTERVAL_PATTERN = /^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/;
935
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
936
+ function isColumnKeyFor(kind, key) {
937
+ if (kind === "TABLE") return /^c\d+$/.test(key);
938
+ if (kind === "BAR_LIST") return key === "label" || key === "value";
939
+ return false;
598
940
  }
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;
941
+ function isClientApplied(option) {
942
+ return (option.type === "sort" || option.type === "columns") && option.apply === "client";
943
+ }
944
+ function civilToday(context) {
945
+ const now = context.now ?? Date.now();
946
+ let timeZone = context.timeZone ?? "UTC";
947
+ try {
948
+ new Intl.DateTimeFormat("en-US", { timeZone });
949
+ } catch {
950
+ timeZone = "UTC";
609
951
  }
952
+ const parts = new Intl.DateTimeFormat("en-US", {
953
+ timeZone,
954
+ year: "numeric",
955
+ month: "2-digit",
956
+ day: "2-digit"
957
+ }).formatToParts(new Date(now));
958
+ const pick = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
959
+ return { year: pick("year"), month: pick("month"), day: pick("day") };
610
960
  }
611
- function itemWidth(item) {
612
- return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
961
+ function toIso(epochMs) {
962
+ return new Date(epochMs).toISOString().slice(0, 10);
613
963
  }
614
- function usesWidthColumns(items) {
615
- return items.some((item) => item.width !== void 0);
964
+ var DAY_MS = 864e5;
965
+ function parseIsoDay(text) {
966
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
967
+ if (match === null) return void 0;
968
+ const epoch = Date.UTC(
969
+ Number(match[1]),
970
+ Number(match[2]) - 1,
971
+ Number(match[3])
972
+ );
973
+ return toIso(epoch) === text ? epoch : void 0;
616
974
  }
617
- function fillsWidth(fill) {
618
- return fill === "width" || fill === "both";
975
+ function isIsoInterval(value) {
976
+ const match = INTERVAL_PATTERN.exec(value);
977
+ if (match === null) return false;
978
+ const start = parseIsoDay(match[1]);
979
+ const end = parseIsoDay(match[2]);
980
+ return start !== void 0 && end !== void 0 && start <= end;
619
981
  }
620
- function fillsHeight(fill) {
621
- return fill === "height" || fill === "both";
982
+ function isPreset(value) {
983
+ return DATE_RANGE_PRESETS.includes(value);
622
984
  }
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;
985
+ function resolveDateRange(value, context = {}) {
986
+ if (isIsoInterval(value)) return value;
987
+ if (!isPreset(value)) return void 0;
988
+ const today = civilToday(context);
989
+ const todayMs = Date.UTC(today.year, today.month - 1, today.day);
990
+ const range = (startMs, endMs) => `${toIso(startMs)}/${toIso(endMs)}`;
991
+ switch (value) {
992
+ case "today":
993
+ return range(todayMs, todayMs);
994
+ case "yesterday":
995
+ return range(todayMs - DAY_MS, todayMs - DAY_MS);
996
+ case "last7":
997
+ return range(todayMs - 6 * DAY_MS, todayMs);
998
+ case "last30":
999
+ return range(todayMs - 29 * DAY_MS, todayMs);
1000
+ case "last90":
1001
+ return range(todayMs - 89 * DAY_MS, todayMs);
1002
+ case "thisMonth":
1003
+ return range(Date.UTC(today.year, today.month - 1, 1), todayMs);
1004
+ case "lastMonth":
1005
+ return range(
1006
+ Date.UTC(today.year, today.month - 2, 1),
1007
+ Date.UTC(today.year, today.month - 1, 1) - DAY_MS
1008
+ );
1009
+ case "thisYear":
1010
+ return range(Date.UTC(today.year, 0, 1), todayMs);
1011
+ case "lastYear":
1012
+ return range(
1013
+ Date.UTC(today.year - 1, 0, 1),
1014
+ Date.UTC(today.year, 0, 1) - DAY_MS
1015
+ );
1016
+ }
627
1017
  }
628
- function fillColumnSpans(items, columns, largeSpan = 2) {
629
- return placeAndFill(
630
- items,
631
- columns,
632
- (item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
633
- );
1018
+ function onStepGrid(option, value) {
1019
+ if (option.step === void 0) return true;
1020
+ const steps = (value - option.min) / option.step;
1021
+ return Math.abs(steps - Math.round(steps)) < 1e-9;
634
1022
  }
635
- function fillWidthSpans(items) {
636
- return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
1023
+ function parseSortValue(value) {
1024
+ const separator = value.lastIndexOf(":");
1025
+ if (separator <= 0) return void 0;
1026
+ const column = value.slice(0, separator);
1027
+ const direction = value.slice(separator + 1);
1028
+ if (direction !== "asc" && direction !== "desc") return void 0;
1029
+ return { column, direction };
637
1030
  }
638
- function placeRows(items) {
639
- return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
1031
+ function sortFromOptions(definition, values) {
1032
+ if (values === void 0) return void 0;
1033
+ const sortOption = definition.options?.find(
1034
+ (option) => option.type === "sort"
1035
+ );
1036
+ if (sortOption === void 0) return void 0;
1037
+ const chosen = values[sortOption.key];
1038
+ return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
640
1039
  }
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;
1040
+ function coerceOptionValue(option, value, context = {}) {
1041
+ switch (option.type) {
1042
+ case "choice":
1043
+ return typeof value === "string" && option.choices.some((choice) => choice.value === value) ? value : void 0;
1044
+ case "number":
1045
+ return typeof value === "number" && Number.isFinite(value) && value >= option.min && value <= option.max && onStepGrid(option, value) ? value : void 0;
1046
+ case "boolean":
1047
+ return typeof value === "boolean" ? value : void 0;
1048
+ case "text": {
1049
+ if (typeof value !== "string") return void 0;
1050
+ const trimmed = value.trim();
1051
+ return trimmed.length <= option.maxLength && !CONTROL_CHARACTERS.test(trimmed) ? trimmed : void 0;
1052
+ }
1053
+ case "dateRange": {
1054
+ if (typeof value !== "string") return void 0;
1055
+ if (isPreset(value) && option.presets !== void 0 && !option.presets.some((preset) => preset.value === value)) {
1056
+ return void 0;
1057
+ }
1058
+ return resolveDateRange(value, context);
1059
+ }
1060
+ case "color":
1061
+ return typeof value === "string" && COLOR_PATTERN.test(value) ? value.toLowerCase() : void 0;
1062
+ case "sort": {
1063
+ if (typeof value !== "string") return void 0;
1064
+ const sort = parseSortValue(value);
1065
+ return sort !== void 0 && option.columns.some((column) => column.key === sort.column) ? value : void 0;
1066
+ }
1067
+ case "columns": {
1068
+ if (!Array.isArray(value) || value.length === 0) return void 0;
1069
+ const keys = value;
1070
+ const allowed = new Set(option.columns.map((c) => c.key));
1071
+ const seen = /* @__PURE__ */ new Set();
1072
+ for (const key of keys) {
1073
+ if (typeof key !== "string" || !allowed.has(key) || seen.has(key)) {
1074
+ return void 0;
1075
+ }
1076
+ seen.add(key);
1077
+ }
1078
+ return [...seen];
1079
+ }
646
1080
  }
647
- return count;
648
- }
649
- function placeAndFill(items, columns, spanOf) {
650
- return placeAndFillRows(items, columns, spanOf).spans;
651
1081
  }
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
- });
671
- }
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;
1082
+ function defaultOptionValue(option, context = {}) {
1083
+ switch (option.type) {
1084
+ case "dateRange":
1085
+ return resolveDateRange(option.default, context);
1086
+ case "sort":
1087
+ return option.default;
1088
+ case "columns":
1089
+ return [...option.default];
1090
+ default:
1091
+ return option.default;
678
1092
  }
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;
1093
+ }
1094
+ var NO_VALUES = Object.freeze({});
1095
+ function resolveOptionValues(definition, chosen, context = {}) {
1096
+ const options = definition.options;
1097
+ if (options === void 0 || options.length === 0) return NO_VALUES;
1098
+ const values = {};
1099
+ for (const option of options) {
1100
+ const picked = chosen?.[option.key];
1101
+ const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked, context)) ?? defaultOptionValue(option, context);
1102
+ if (value !== void 0) values[option.key] = value;
684
1103
  }
685
- closeRow();
686
- return { spans, rows };
1104
+ return values;
687
1105
  }
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;
713
- }
714
- function findView(item, key) {
715
- return item.views?.find((view) => view.key === key);
1106
+ function sameValue(a, b) {
1107
+ if (Array.isArray(a) && Array.isArray(b)) {
1108
+ return a.length === b.length && a.every((item, i) => item === b[i]);
1109
+ }
1110
+ return a === b;
716
1111
  }
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
- ];
1112
+ function sameOptionValues(a, b) {
1113
+ const keys = Object.keys({ ...a, ...b });
1114
+ return keys.every((key) => sameValue(a[key], b[key]));
731
1115
  }
732
- function isKnownView(item, key) {
733
- return key !== void 0 && (key === BASE_VIEW_KEY || findView(item, key) !== void 0);
1116
+ function optionsCacheSuffix(definition, values, context = {}) {
1117
+ const parts = [];
1118
+ for (const option of definition.options ?? []) {
1119
+ if (isClientApplied(option)) continue;
1120
+ const value = values[option.key];
1121
+ if (sameValue(value, defaultOptionValue(option, context))) continue;
1122
+ parts.push(
1123
+ `${option.key}=${encodeURIComponent(JSON.stringify(value ?? null))}`
1124
+ );
1125
+ }
1126
+ parts.sort();
1127
+ return parts.length === 0 ? "" : `?${parts.join("&")}`;
734
1128
  }
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;
1129
+ function tableColumnIndex(key) {
1130
+ const match = /^c(\d+)$/.exec(key);
1131
+ return match === null ? void 0 : Number(match[1]);
740
1132
  }
741
- function viewKind(item, chosen) {
742
- return findView(item, effectiveViewKey(item, chosen))?.kind ?? item.kind;
1133
+ function sortBarItems(items, sort) {
1134
+ const sign = sort.direction === "desc" ? -1 : 1;
1135
+ return items.map((item, index) => ({ item, index })).sort((a, b) => {
1136
+ const order = sort.column === "value" ? a.item.value - b.item.value : a.item.label.localeCompare(b.item.label);
1137
+ return order * sign || a.index - b.index;
1138
+ }).map(({ item }) => item);
743
1139
  }
744
- function cellValue2(cell) {
745
- const value = cell?.value;
746
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1140
+ function applyClientOptions(definition, data, values) {
1141
+ const options = definition.options ?? [];
1142
+ const sortOption = options.find(
1143
+ (o) => o.type === "sort" && o.apply === "client"
1144
+ );
1145
+ const columnsOption = options.find(
1146
+ (o) => o.type === "columns" && o.apply === "client"
1147
+ );
1148
+ const sortValue = sortOption === void 0 ? void 0 : values[sortOption.key];
1149
+ const columnsValue = columnsOption === void 0 ? void 0 : values[columnsOption.key];
1150
+ let next = data;
1151
+ if (typeof sortValue === "string") {
1152
+ const sort = parseSortValue(sortValue);
1153
+ if (sort !== void 0 && next.kind === "BAR_LIST") {
1154
+ if (sort.column === "label" || sort.column === "value") {
1155
+ next = { ...next, items: sortBarItems(next.items, sort) };
1156
+ }
1157
+ } else if (sort !== void 0 && next.kind === "TABLE") {
1158
+ const index = tableColumnIndex(sort.column);
1159
+ if (index !== void 0 && index < next.columns.length) {
1160
+ const sorted = queryRows(tableDetailData(next), {
1161
+ page: 1,
1162
+ pageSize: Math.max(1, next.rows.length),
1163
+ sort
1164
+ });
1165
+ if (sorted.ok) next = { ...next, rows: sorted.value.rows };
1166
+ }
1167
+ }
1168
+ }
1169
+ if (Array.isArray(columnsValue) && next.kind === "TABLE") {
1170
+ const table = next;
1171
+ const indexes = columnsValue.map(tableColumnIndex).filter(
1172
+ (index) => index !== void 0 && index < table.columns.length
1173
+ );
1174
+ if (indexes.length > 0) {
1175
+ next = {
1176
+ ...table,
1177
+ columns: indexes.map((i) => table.columns[i]),
1178
+ rows: table.rows.map((row) => indexes.map((i) => row[i]))
1179
+ };
1180
+ }
1181
+ }
1182
+ return next;
747
1183
  }
748
- function columnName(table, index) {
749
- return table.columns[index]?.label ?? `#${index + 1}`;
1184
+
1185
+ // src/core/settings.ts
1186
+ var MAX_WIDGET_TITLE_LENGTH = 80;
1187
+ var MAX_PARSED_SETTINGS = 500;
1188
+ var MAX_COLUMN_LIST = 100;
1189
+ var MAX_VALUE_LENGTH = 500;
1190
+ function isRecord(value) {
1191
+ return typeof value === "object" && value !== null && !Array.isArray(value);
750
1192
  }
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).`;
1193
+ function parseOptionValue(value) {
1194
+ if (typeof value === "string") {
1195
+ return value.length <= MAX_VALUE_LENGTH ? value : void 0;
1196
+ }
1197
+ if (typeof value === "number")
1198
+ return Number.isFinite(value) ? value : void 0;
1199
+ if (typeof value === "boolean") return value;
1200
+ if (Array.isArray(value) && value.length <= MAX_COLUMN_LIST) {
1201
+ return value.every(
1202
+ (entry) => typeof entry === "string" && entry.length <= MAX_VALUE_LENGTH
1203
+ ) ? value : void 0;
754
1204
  }
755
1205
  return void 0;
756
1206
  }
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
- );
1207
+ function parseOne(raw) {
1208
+ if (!isRecord(raw)) return void 0;
1209
+ const title = raw["title"];
1210
+ const width = raw["width"];
1211
+ const flexible = raw["flexible"];
1212
+ const options = raw["options"];
1213
+ const view = raw["view"];
1214
+ const cleanTitle = typeof title === "string" ? title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH) : "";
1215
+ const chosen = {};
1216
+ if (isRecord(options)) {
1217
+ for (const [key, value] of Object.entries(options)) {
1218
+ const parsed = parseOptionValue(value);
1219
+ if (parsed !== void 0) chosen[key] = parsed;
765
1220
  }
766
- numbers.push(value);
767
1221
  }
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 });
1222
+ const settings = {
1223
+ ...cleanTitle === "" ? {} : { title: cleanTitle },
1224
+ ...isWidgetWidth(width) ? { width } : {},
1225
+ ...typeof flexible === "boolean" ? { flexible } : {},
1226
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
1227
+ ...typeof view === "string" && view !== "" && view.length <= 80 ? { view } : {}
1228
+ };
1229
+ return Object.keys(settings).length === 0 ? void 0 : settings;
781
1230
  }
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);
1231
+ function parseSettings(raw) {
1232
+ if (!isRecord(raw)) return void 0;
1233
+ const parsed = {};
1234
+ for (const [key, value] of Object.entries(raw).slice(
1235
+ 0,
1236
+ MAX_PARSED_SETTINGS
1237
+ )) {
1238
+ if (key === "") continue;
1239
+ const one = parseOne(value);
1240
+ if (one !== void 0) parsed[key] = one;
796
1241
  }
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
- });
1242
+ return Object.keys(parsed).length === 0 ? void 0 : parsed;
1243
+ }
1244
+ function serializeSettings(settings) {
1245
+ if (settings === void 0) return void 0;
1246
+ const out = {};
1247
+ for (const key of Object.keys(settings)) {
1248
+ const one = settings[key];
1249
+ if (one === void 0) continue;
1250
+ const piece = {
1251
+ ...one.title === void 0 ? {} : { title: one.title },
1252
+ ...one.width === void 0 ? {} : { width: one.width },
1253
+ ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1254
+ ...one.options === void 0 || Object.keys(one.options).length === 0 ? {} : { options: { ...one.options } },
1255
+ ...one.view === void 0 ? {} : { view: one.view }
1256
+ };
1257
+ if (Object.keys(piece).length > 0) out[key] = piece;
809
1258
  }
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
- });
1259
+ return Object.keys(out).length === 0 ? void 0 : out;
818
1260
  }
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
- });
1261
+ function widthRange(item) {
1262
+ const min = isWidgetWidth(item.minWidth) ? item.minWidth : MIN_WIDGET_WIDTH;
1263
+ const max = isWidgetWidth(item.maxWidth) ? item.maxWidth : MAX_WIDGET_WIDTH;
1264
+ return { min, max: Math.max(min, max) };
831
1265
  }
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
- });
1266
+ var WIDTH_SHARES = {
1267
+ 2: "1/6",
1268
+ 3: "1/4",
1269
+ 4: "1/3",
1270
+ 5: "5/12",
1271
+ 6: "1/2",
1272
+ 7: "7/12",
1273
+ 8: "2/3",
1274
+ 9: "3/4",
1275
+ 10: "5/6",
1276
+ 11: "11/12",
1277
+ 12: "Full"
1278
+ };
1279
+ function widthShareLabel(width) {
1280
+ return WIDTH_SHARES[width] ?? `${width}/12`;
854
1281
  }
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
- });
1282
+ function isOptionsLocked(item) {
1283
+ return resolveWidgetLock(item).options;
871
1284
  }
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
- );
1285
+ function fillWith(fill, flexible) {
1286
+ const heightToo = fill === "height" || fill === "both";
1287
+ if (flexible) return heightToo ? "both" : "width";
1288
+ return heightToo ? "height" : void 0;
1289
+ }
1290
+ function isFlexible(item, settings) {
1291
+ if (settings?.flexible !== void 0) return settings.flexible;
1292
+ return item.fill === "width" || item.fill === "both";
1293
+ }
1294
+ function applySettings(definition, settings) {
1295
+ if (settings === void 0) return definition;
1296
+ const range = widthRange(definition);
1297
+ const width = settings.width === void 0 ? void 0 : Math.min(range.max, Math.max(range.min, settings.width));
1298
+ const title = settings.title === void 0 || settings.title.trim() === "" ? void 0 : settings.title;
1299
+ const flexibleChanged = settings.flexible !== void 0 && settings.flexible !== isFlexible(definition, void 0);
1300
+ if (title === void 0 && width === void 0 && !flexibleChanged) {
1301
+ return definition;
883
1302
  }
884
- if (data.kind !== expected) {
885
- return err(
886
- `${context.where}: ${conversion.type} reads a ${expected} payload, but the widget returned ${data.kind}.`
1303
+ const next = { ...definition };
1304
+ if (title !== void 0) next.title = title;
1305
+ if (width !== void 0) next.width = width;
1306
+ if (flexibleChanged) {
1307
+ const fill = fillWith(
1308
+ definition.fill,
1309
+ settings.flexible === true
887
1310
  );
1311
+ if (fill === void 0) {
1312
+ delete next.fill;
1313
+ } else {
1314
+ next.fill = fill;
1315
+ }
888
1316
  }
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
- }
1317
+ return next;
901
1318
  }
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}`
1319
+ function effectiveDefinitions(definitions, layout, options = {}) {
1320
+ return definitions.map(
1321
+ (definition) => options.overrideLocks !== true && isOptionsLocked(definition) ? definition : applySettings(definition, layout.settings?.[definition.key])
915
1322
  );
916
- return `Shows ${names.join(", ")} only.`;
917
1323
  }
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
- };
1324
+ function widgetOptionValues(definition, settings) {
1325
+ const declared = definition.options ?? [];
1326
+ const values = {};
1327
+ for (const option of declared) {
1328
+ const picked = settings?.options?.[option.key];
1329
+ const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked)) ?? defaultOptionValue(option);
1330
+ if (value !== void 0) values[option.key] = value;
942
1331
  }
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;
978
- }
979
- function isClientApplied(option) {
980
- return (option.type === "sort" || option.type === "columns") && option.apply === "client";
1332
+ return values;
981
1333
  }
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";
1334
+ function optionValuesFromLayout(definitions, layout, options = {}) {
1335
+ const out = {};
1336
+ for (const definition of definitions) {
1337
+ if (definition.options === void 0 || definition.options.length === 0) {
1338
+ continue;
1339
+ }
1340
+ const locked = options.overrideLocks !== true && isOptionsLocked(definition);
1341
+ out[definition.key] = locked ? {} : layout.settings?.[definition.key]?.options ?? {};
989
1342
  }
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") };
998
- }
999
- function toIso(epochMs) {
1000
- return new Date(epochMs).toISOString().slice(0, 10);
1343
+ return out;
1001
1344
  }
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;
1345
+ function sameSettings(a, b) {
1346
+ if (a === b) return true;
1347
+ return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && a?.view === b?.view && sameOptionValues(a?.options ?? {}, b?.options ?? {});
1012
1348
  }
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
- );
1349
+ function setWidgetSettings(layout, key, patch) {
1350
+ const before = layout.settings?.[key];
1351
+ const draft = { ...before };
1352
+ for (const field of [
1353
+ "title",
1354
+ "width",
1355
+ "flexible",
1356
+ "options",
1357
+ "view"
1358
+ ]) {
1359
+ if (!(field in patch)) continue;
1360
+ const value = patch[field];
1361
+ if (value === void 0) {
1362
+ delete draft[field];
1363
+ } else {
1364
+ draft[field] = value;
1365
+ }
1366
+ }
1367
+ const merged = parseOne(draft);
1368
+ if (sameSettings(before, merged)) return layout;
1369
+ const rest = { ...layout.settings };
1370
+ if (merged === void 0) {
1371
+ delete rest[key];
1372
+ } else {
1373
+ rest[key] = merged;
1054
1374
  }
1375
+ const { settings: _previous, ...others } = layout;
1376
+ return Object.keys(rest).length === 0 ? others : { ...others, settings: rest };
1055
1377
  }
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;
1378
+ function clearWidgetSettings(layout, key) {
1379
+ if (layout.settings?.[key] === void 0) return layout;
1380
+ return setWidgetSettings(layout, key, {
1381
+ title: void 0,
1382
+ width: void 0,
1383
+ flexible: void 0,
1384
+ options: void 0,
1385
+ view: void 0
1386
+ });
1060
1387
  }
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 };
1388
+ function normalizeTitle(title) {
1389
+ return title.normalize("NFKC").trim().toLowerCase();
1068
1390
  }
1069
- function sortFromOptions(definition, values) {
1070
- if (values === void 0) return void 0;
1071
- const sortOption = definition.options?.find(
1072
- (option) => option.type === "sort"
1391
+ function isTitleInUse(definitions, layout, key, title) {
1392
+ const wanted = normalizeTitle(title);
1393
+ if (wanted === "") return false;
1394
+ return effectiveDefinitions(definitions, layout, {
1395
+ overrideLocks: true
1396
+ }).some(
1397
+ (other) => other.key !== key && normalizeTitle(other.title ?? other.key) === wanted
1073
1398
  );
1074
- if (sortOption === void 0) return void 0;
1075
- const chosen = values[sortOption.key];
1076
- return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
1077
1399
  }
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;
1400
+ function uniqueTitle(taken, title) {
1401
+ if (!taken.has(normalizeTitle(title))) return title;
1402
+ for (let n = 2; ; n += 1) {
1403
+ const suffix = ` (#${n})`;
1404
+ const candidate = `${title.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
1405
+ if (!taken.has(normalizeTitle(candidate))) return candidate;
1406
+ }
1407
+ }
1408
+ function normalizeSettings(definitions, layout, options = {}) {
1409
+ const settings = layout.settings;
1410
+ if (settings === void 0) return layout;
1411
+ const cleaned = {};
1412
+ const taken = /* @__PURE__ */ new Set();
1413
+ for (const definition of definitions) {
1414
+ const renamed = settings[definition.key]?.title !== void 0 && (options.overrideLocks === true || !isOptionsLocked(definition));
1415
+ if (!renamed) taken.add(normalizeTitle(definition.title ?? definition.key));
1416
+ }
1417
+ for (const definition of definitions) {
1418
+ const one = settings[definition.key];
1419
+ if (one === void 0) continue;
1420
+ if (options.overrideLocks !== true && isOptionsLocked(definition)) continue;
1421
+ const range = widthRange(definition);
1422
+ const chosen = {};
1423
+ for (const option of definition.options ?? []) {
1424
+ const picked = one.options?.[option.key];
1425
+ if (picked === void 0) continue;
1426
+ const value = coerceOptionValue(option, picked);
1427
+ if (value !== void 0) {
1428
+ chosen[option.key] = option.type === "dateRange" ? picked : value;
1095
1429
  }
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
1430
  }
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);
1431
+ let title = one.title;
1432
+ if (title !== void 0) {
1433
+ title = title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH);
1434
+ if (title === "") {
1435
+ title = void 0;
1436
+ } else {
1437
+ title = uniqueTitle(taken, title);
1438
+ taken.add(normalizeTitle(title));
1115
1439
  }
1116
- return [...seen];
1440
+ }
1441
+ const next = {
1442
+ ...title === void 0 ? {} : { title },
1443
+ ...one.width === void 0 ? {} : { width: Math.min(range.max, Math.max(range.min, one.width)) },
1444
+ ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1445
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
1446
+ ...one.view !== void 0 && (one.view === BASE_VIEW_KEY || findView(definition, one.view) !== void 0) ? { view: one.view } : {}
1447
+ };
1448
+ if (Object.keys(next).length > 0) {
1449
+ cleaned[definition.key] = sameSettings(one, next) ? one : next;
1117
1450
  }
1118
1451
  }
1452
+ const keys = Object.keys(cleaned);
1453
+ const unchanged = keys.length === Object.keys(settings).length && keys.every((key) => cleaned[key] === settings[key]);
1454
+ if (unchanged) return layout;
1455
+ const { settings: _previous, ...others } = layout;
1456
+ return keys.length === 0 ? others : { ...others, settings: cleaned };
1119
1457
  }
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
- }
1458
+
1459
+ // src/core/clones.ts
1460
+ var MAX_PARSED_CLONES = 500;
1461
+ function isRecord2(value) {
1462
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1131
1463
  }
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;
1464
+ function parseClones(raw) {
1465
+ if (!Array.isArray(raw)) return void 0;
1466
+ const seen = /* @__PURE__ */ new Set();
1467
+ const clones = [];
1468
+ for (const entry of raw.slice(0, MAX_PARSED_CLONES)) {
1469
+ if (!isRecord2(entry)) continue;
1470
+ const key = entry["key"];
1471
+ const from = entry["from"];
1472
+ if (typeof key !== "string" || typeof from !== "string" || key === "" || from === "" || key === from || seen.has(key)) {
1473
+ continue;
1474
+ }
1475
+ seen.add(key);
1476
+ clones.push({ key, from });
1141
1477
  }
1142
- return values;
1478
+ return clones.length === 0 ? void 0 : clones;
1143
1479
  }
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;
1480
+ function serializeClones(clones) {
1481
+ if (clones === void 0 || clones.length === 0) return void 0;
1482
+ return clones.map(({ key, from }) => ({ key, from }));
1149
1483
  }
1150
- function sameOptionValues(a, b) {
1151
- const keys = Object.keys({ ...a, ...b });
1152
- return keys.every((key) => sameValue(a[key], b[key]));
1484
+ function cloneNumber(key) {
1485
+ const match = /#(\d+)$/.exec(key);
1486
+ return match === null ? void 0 : Number(match[1]);
1153
1487
  }
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
- );
1488
+ function cleanClones(definitions, clones, options = {}) {
1489
+ if (clones === void 0) return void 0;
1490
+ const byKey = new Map(definitions.map((d) => [d.key, d]));
1491
+ const seen = /* @__PURE__ */ new Set();
1492
+ const kept = [];
1493
+ for (const clone of clones) {
1494
+ if (seen.has(clone.key)) continue;
1495
+ const original = byKey.get(clone.from);
1496
+ if (original === void 0 || original.sourceKey !== void 0) continue;
1497
+ const existing = byKey.get(clone.key);
1498
+ if (existing !== void 0 && existing.sourceKey !== clone.from) continue;
1499
+ if (options.overrideLocks !== true && resolveWidgetLock(original).clone) {
1500
+ continue;
1501
+ }
1502
+ seen.add(clone.key);
1503
+ kept.push(clone);
1163
1504
  }
1164
- parts.sort();
1165
- return parts.length === 0 ? "" : `?${parts.join("&")}`;
1166
- }
1167
- function tableColumnIndex(key) {
1168
- const match = /^c(\d+)$/.exec(key);
1169
- return match === null ? void 0 : Number(match[1]);
1505
+ if (kept.length === 0) return void 0;
1506
+ return kept.length === clones.length && kept.every((c, i) => c === clones[i]) ? clones : kept;
1170
1507
  }
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);
1508
+ function defaultCloneTitle(original, clone, index) {
1509
+ const base = original.title ?? original.key;
1510
+ const suffix = ` (#${cloneNumber(clone.key) ?? index + 1})`;
1511
+ return `${base.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
1177
1512
  }
1178
- function applyClientOptions(definition, data, values) {
1179
- const options = definition.options ?? [];
1180
- const sortOption = options.find(
1181
- (o) => o.type === "sort" && o.apply === "client"
1513
+ function withClones(definitions, layout, options = {}) {
1514
+ const valid = cleanClones(
1515
+ definitions,
1516
+ layout.clones,
1517
+ options
1182
1518
  );
1183
- const columnsOption = options.find(
1184
- (o) => o.type === "columns" && o.apply === "client"
1185
- );
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
- }
1519
+ if (valid === void 0) return definitions;
1520
+ const present = new Set(definitions.map((d) => d.key));
1521
+ const toAdd = valid.filter((c) => !present.has(c.key));
1522
+ if (toAdd.length === 0) return definitions;
1523
+ const out = [];
1524
+ for (const definition of definitions) {
1525
+ out.push(definition);
1526
+ toAdd.forEach((clone, index) => {
1527
+ if (clone.from !== definition.key) return;
1528
+ const copy = { ...definition };
1529
+ copy.key = clone.key;
1530
+ copy.sourceKey = definition.key;
1531
+ copy.title = defaultCloneTitle(definition, clone, index);
1532
+ delete copy.locked;
1533
+ out.push(copy);
1534
+ });
1219
1535
  }
1220
- return next;
1536
+ return out;
1221
1537
  }
1222
1538
 
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);
1539
+ // src/core/compat.ts
1540
+ function isStringArray(value) {
1541
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
1230
1542
  }
1231
- function parseOptionValue(value) {
1232
- if (typeof value === "string") {
1233
- return value.length <= MAX_VALUE_LENGTH ? value : void 0;
1543
+ function upgradeLegacyWidgetData(value) {
1544
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1545
+ return value;
1234
1546
  }
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;
1547
+ const record = value;
1548
+ if (record["kind"] === "TABLE" && isStringArray(record["columns"]) && Array.isArray(record["rows"]) && record["rows"].every(isStringArray)) {
1549
+ const rows = record["rows"];
1550
+ return {
1551
+ ...record,
1552
+ columns: record["columns"].map((label) => ({ label })),
1553
+ rows: rows.map((row) => row.map((text) => ({ text })))
1554
+ };
1242
1555
  }
1243
- return void 0;
1556
+ if (record["kind"] === "GRAPH" && Array.isArray(record["points"]) && record["series"] === void 0) {
1557
+ const { points, ...rest } = record;
1558
+ return {
1559
+ ...rest,
1560
+ series: [{ name: "Value", points }],
1561
+ chartType: "bar",
1562
+ valueFormat: "number"
1563
+ };
1564
+ }
1565
+ return value;
1244
1566
  }
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
- }
1567
+ function fromLegacyDefinition(legacy) {
1568
+ if (!isWidgetKind(legacy.viewType)) {
1569
+ return null;
1259
1570
  }
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 } : {}
1571
+ return {
1572
+ key: legacy.widgetKey,
1573
+ title: legacy.title,
1574
+ ...legacy.description === null ? {} : { description: legacy.description },
1575
+ kind: legacy.viewType,
1576
+ sortOrder: legacy.sortOrder
1266
1577
  };
1267
- return Object.keys(settings).length === 0 ? void 0 : settings;
1268
1578
  }
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;
1579
+
1580
+ // src/core/layout.ts
1581
+ var MAX_PAGE_TITLE_LENGTH = 30;
1582
+ var MAX_PARSED_PAGES = 50;
1583
+ var MAX_PAGE_ROWS = 20;
1584
+ var EMPTY_LAYOUT = Object.freeze({
1585
+ order: Object.freeze([]),
1586
+ hidden: Object.freeze([]),
1587
+ minimized: Object.freeze([])
1588
+ });
1589
+ function uniqueStrings(value) {
1590
+ if (!Array.isArray(value)) return [];
1591
+ const seen = /* @__PURE__ */ new Set();
1592
+ for (const entry of value) {
1593
+ if (typeof entry === "string" && entry !== "") seen.add(entry);
1279
1594
  }
1280
- return Object.keys(parsed).length === 0 ? void 0 : parsed;
1595
+ return [...seen];
1281
1596
  }
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 }
1597
+ function parseLayout(raw) {
1598
+ let value = raw;
1599
+ if (typeof raw === "string") {
1600
+ try {
1601
+ value = JSON.parse(raw);
1602
+ } catch {
1603
+ return EMPTY_LAYOUT;
1604
+ }
1605
+ }
1606
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1607
+ return EMPTY_LAYOUT;
1608
+ }
1609
+ const record = value;
1610
+ const order = uniqueStrings(record["order"]);
1611
+ const hidden = uniqueStrings(record["hidden"]);
1612
+ const minimized = uniqueStrings(record["minimized"]);
1613
+ const pages = parsePages(record["pages"]);
1614
+ const settings = parseSettings(
1615
+ record["settings"]
1616
+ );
1617
+ const clones = parseClones(
1618
+ record["clones"]
1619
+ );
1620
+ const extras = {
1621
+ ...settings === void 0 ? {} : { settings },
1622
+ ...clones === void 0 ? {} : { clones }
1623
+ };
1624
+ if (pages.length > 0) {
1625
+ return {
1626
+ order: EMPTY_LAYOUT.order,
1627
+ hidden: EMPTY_LAYOUT.hidden,
1628
+ minimized: EMPTY_LAYOUT.minimized,
1629
+ pages,
1630
+ ...extras
1294
1631
  };
1295
- if (Object.keys(piece).length > 0) out[key] = piece;
1296
1632
  }
1297
- return Object.keys(out).length === 0 ? void 0 : out;
1633
+ if (order.length === 0 && hidden.length === 0 && minimized.length === 0) {
1634
+ return Object.keys(extras).length === 0 ? EMPTY_LAYOUT : { ...EMPTY_LAYOUT, ...extras };
1635
+ }
1636
+ return { order, hidden, minimized, ...extras };
1298
1637
  }
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) };
1638
+ function parsePages(value) {
1639
+ if (!Array.isArray(value)) return [];
1640
+ const pages = [];
1641
+ const keys = /* @__PURE__ */ new Set();
1642
+ for (const entry of value.slice(0, MAX_PARSED_PAGES)) {
1643
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
1644
+ continue;
1645
+ }
1646
+ const record = entry;
1647
+ const key = record["key"];
1648
+ if (typeof key !== "string" || key === "" || keys.has(key)) continue;
1649
+ keys.add(key);
1650
+ const rawTitle = record["title"];
1651
+ const title = typeof rawTitle === "string" && rawTitle.trim() !== "" ? rawTitle.trim().slice(0, MAX_PAGE_TITLE_LENGTH) : `Page ${pages.length + 1}`;
1652
+ const maxRows = record["maxRows"];
1653
+ const held = /* @__PURE__ */ new Set();
1654
+ for (const earlier of pages) {
1655
+ for (const k of [...earlier.order, ...earlier.hidden]) held.add(k);
1656
+ }
1657
+ const keep = (list) => uniqueStrings(list).filter((k) => !held.has(k));
1658
+ pages.push({
1659
+ key,
1660
+ title,
1661
+ order: keep(record["order"]),
1662
+ hidden: keep(record["hidden"]),
1663
+ minimized: keep(record["minimized"]),
1664
+ ...typeof maxRows === "number" && Number.isInteger(maxRows) && maxRows >= 1 && maxRows <= MAX_PAGE_ROWS ? { maxRows } : {}
1665
+ });
1666
+ }
1667
+ return pages;
1303
1668
  }
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`;
1669
+ function serializeLayout(layout) {
1670
+ return JSON.stringify({
1671
+ order: [...layout.order],
1672
+ hidden: [...layout.hidden],
1673
+ minimized: [...layout.minimized],
1674
+ ...layout.pages === void 0 || layout.pages.length === 0 ? {} : {
1675
+ pages: layout.pages.map((page) => ({
1676
+ key: page.key,
1677
+ title: page.title,
1678
+ order: [...page.order],
1679
+ hidden: [...page.hidden],
1680
+ minimized: [...page.minimized],
1681
+ ...page.maxRows === void 0 ? {} : { maxRows: page.maxRows }
1682
+ }))
1683
+ },
1684
+ ...serializeSettings(layout.settings) === void 0 ? {} : { settings: serializeSettings(layout.settings) },
1685
+ ...serializeClones(layout.clones) === void 0 ? {} : { clones: serializeClones(layout.clones) }
1686
+ });
1319
1687
  }
1320
- function isOptionsLocked(item) {
1321
- return resolveWidgetLock(item).options;
1688
+ function bySortOrder(items) {
1689
+ return items.map((item, index) => ({ item, index })).sort(
1690
+ (a, b) => (a.item.sortOrder ?? 0) - (b.item.sortOrder ?? 0) || a.index - b.index
1691
+ ).map(({ item }) => item);
1322
1692
  }
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;
1327
- }
1328
- function isFlexible(item, settings) {
1329
- if (settings?.flexible !== void 0) return settings.flexible;
1330
- return item.fill === "width" || item.fill === "both";
1693
+ function isPinned(item) {
1694
+ return resolveWidgetLock(item).move;
1331
1695
  }
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;
1696
+ function viewerOrder(visible, layout) {
1697
+ const byKey = new Map(visible.map((d) => [d.key, d]));
1698
+ const ordered = [];
1699
+ for (const key of layout.order) {
1700
+ const definition = byKey.get(key);
1701
+ if (definition !== void 0) {
1702
+ ordered.push(definition);
1703
+ byKey.delete(key);
1353
1704
  }
1354
1705
  }
1355
- return next;
1706
+ for (const definition of bySortOrder(visible)) {
1707
+ if (byKey.has(definition.key)) ordered.push(definition);
1708
+ }
1709
+ return ordered;
1356
1710
  }
1357
- function effectiveDefinitions(definitions, layout, options = {}) {
1358
- return definitions.map(
1359
- (definition) => options.overrideLocks !== true && isOptionsLocked(definition) ? definition : applySettings(definition, layout.settings?.[definition.key])
1711
+ function placeAroundPinned(visible, free) {
1712
+ const slots = bySortOrder(visible).map(
1713
+ (item) => isPinned(item) ? item : void 0
1360
1714
  );
1715
+ let next = 0;
1716
+ return slots.map((pinned) => pinned ?? free[next++]);
1361
1717
  }
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;
1718
+ function visibleWidgets(definitions, layout) {
1719
+ const hidden = new Set(layout.hidden);
1720
+ const visible = definitions.filter((d) => !hidden.has(d.key));
1721
+ const ordered = viewerOrder(visible, layout);
1722
+ if (!visible.some(isPinned)) return ordered;
1723
+ return placeAroundPinned(
1724
+ visible,
1725
+ ordered.filter((item) => !isPinned(item))
1726
+ );
1382
1727
  }
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 ?? {});
1728
+ function hiddenWidgets(definitions, layout) {
1729
+ const hidden = new Set(layout.hidden);
1730
+ return bySortOrder(definitions.filter((d) => hidden.has(d.key)));
1386
1731
  }
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;
1732
+ function moveWidget(definitions, layout, key, toIndex) {
1733
+ const current = visibleWidgets(definitions, layout);
1734
+ const fromIndex = current.findIndex((d) => d.key === key);
1735
+ const mover = current[fromIndex];
1736
+ if (mover === void 0 || !Number.isInteger(toIndex) || toIndex < 0 || toIndex >= current.length || toIndex === fromIndex || isPinned(mover) || isPinned(current[toIndex])) {
1737
+ return layout;
1412
1738
  }
1413
- const { settings: _previous, ...others } = layout;
1414
- return Object.keys(rest).length === 0 ? others : { ...others, settings: rest };
1739
+ const freeSlots = freeSlotIndexes(current);
1740
+ const free = current.filter((item) => !isPinned(item));
1741
+ const fromFree = freeSlots.indexOf(fromIndex);
1742
+ const toFree = freeSlots.indexOf(toIndex);
1743
+ free.splice(fromFree, 1);
1744
+ free.splice(toFree, 0, mover);
1745
+ const next = placeAroundPinned(current, free);
1746
+ return { ...layout, order: next.map((d) => d.key) };
1415
1747
  }
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
1748
+ function freeSlotIndexes(ordered) {
1749
+ const slots = [];
1750
+ ordered.forEach((item, index) => {
1751
+ if (!isPinned(item)) slots.push(index);
1424
1752
  });
1753
+ return slots;
1425
1754
  }
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
1755
+ function moveWidgetBy(definitions, layout, key, offset) {
1756
+ const current = visibleWidgets(definitions, layout);
1757
+ const fromIndex = current.findIndex((d) => d.key === key);
1758
+ if (fromIndex === -1) return layout;
1759
+ const freeSlots = freeSlotIndexes(current);
1760
+ const fromFree = freeSlots.indexOf(fromIndex);
1761
+ if (fromFree === -1) return layout;
1762
+ const toFree = Math.max(
1763
+ 0,
1764
+ Math.min(freeSlots.length - 1, fromFree + Math.trunc(offset))
1436
1765
  );
1766
+ return moveWidget(definitions, layout, key, freeSlots[toFree]);
1437
1767
  }
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;
1768
+ function moveWidgetUp(definitions, layout, key) {
1769
+ return moveWidgetBy(definitions, layout, key, -1);
1770
+ }
1771
+ function moveWidgetDown(definitions, layout, key) {
1772
+ return moveWidgetBy(definitions, layout, key, 1);
1773
+ }
1774
+ function isHidden(layout, key) {
1775
+ return layout.hidden.includes(key);
1776
+ }
1777
+ function isMinimized(layout, key) {
1778
+ return layout.minimized.includes(key);
1779
+ }
1780
+ function hideWidget(layout, key) {
1781
+ if (isHidden(layout, key)) return layout;
1782
+ return { ...layout, hidden: [...layout.hidden, key] };
1783
+ }
1784
+ function showWidget(layout, key) {
1785
+ if (!isHidden(layout, key)) return layout;
1786
+ return {
1787
+ ...layout,
1788
+ hidden: layout.hidden.filter((k) => k !== key),
1789
+ minimized: layout.minimized.filter((k) => k !== key)
1790
+ };
1791
+ }
1792
+ function toggleHidden(layout, key) {
1793
+ return isHidden(layout, key) ? showWidget(layout, key) : hideWidget(layout, key);
1794
+ }
1795
+ function minimizeWidget(layout, key) {
1796
+ if (isMinimized(layout, key)) return layout;
1797
+ return { ...layout, minimized: [...layout.minimized, key] };
1798
+ }
1799
+ function restoreWidget(layout, key) {
1800
+ if (!isMinimized(layout, key)) return layout;
1801
+ return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
1802
+ }
1803
+ function toggleMinimized(layout, key) {
1804
+ return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
1805
+ }
1806
+ function pruneLayout(definitions, layout) {
1807
+ const known = new Set(definitions.map((d) => d.key));
1808
+ const keep = (keys) => keys.every((k) => known.has(k)) ? keys : keys.filter((k) => known.has(k));
1809
+ return mapLayoutLists(layout, (lists) => ({
1810
+ order: keep(lists.order),
1811
+ hidden: keep(lists.hidden),
1812
+ minimized: keep(lists.minimized)
1813
+ }));
1814
+ }
1815
+ function mapLayoutLists(layout, change) {
1816
+ const top = change(layout);
1817
+ const pages = layout.pages?.map((page) => {
1818
+ const next = change(page);
1819
+ return next.order === page.order && next.hidden === page.hidden && next.minimized === page.minimized ? page : { ...page, ...next };
1820
+ });
1821
+ const pagesChanged = pages !== void 0 && pages.some((page, i) => page !== layout.pages?.[i]);
1822
+ if (top.order === layout.order && top.hidden === layout.hidden && top.minimized === layout.minimized && !pagesChanged) {
1823
+ return layout;
1444
1824
  }
1825
+ return {
1826
+ ...layout,
1827
+ ...top,
1828
+ ...pages === void 0 ? {} : { pages }
1829
+ };
1445
1830
  }
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();
1831
+ function enforceLocks(definitions, layout, options = {}) {
1832
+ if (options.overrideLocks === true) return layout;
1833
+ const noMove = /* @__PURE__ */ new Set();
1834
+ const noHide = /* @__PURE__ */ new Set();
1835
+ const noMinimize = /* @__PURE__ */ new Set();
1451
1836
  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));
1837
+ const lock = resolveWidgetLock(definition);
1838
+ if (lock.move) noMove.add(definition.key);
1839
+ if (lock.hide) noHide.add(definition.key);
1840
+ if (lock.minimize) noMinimize.add(definition.key);
1454
1841
  }
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 };
1842
+ const without2 = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
1843
+ return mapLayoutLists(layout, (lists) => ({
1844
+ order: without2(lists.order, noMove),
1845
+ hidden: without2(lists.hidden, noHide),
1846
+ minimized: without2(lists.minimized, noMinimize)
1847
+ }));
1495
1848
  }
1496
1849
 
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];
1850
+ // src/core/pages.ts
1851
+ var DEFAULT_PAGE_KEY = "default";
1852
+ var DEFAULT_PAGE_ROWS = 4;
1853
+ function rowsAllowed(page, options) {
1854
+ const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
1855
+ return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
1513
1856
  }
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;
1857
+ function hasPages(layout) {
1858
+ return layout.pages !== void 0 && layout.pages.length > 0;
1859
+ }
1860
+ function pageList(layout) {
1861
+ if (layout.pages !== void 0 && layout.pages.length > 0) {
1862
+ return layout.pages;
1525
1863
  }
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"]
1864
+ return [
1865
+ {
1866
+ key: DEFAULT_PAGE_KEY,
1867
+ title: "Page 1",
1868
+ order: layout.order,
1869
+ hidden: layout.hidden,
1870
+ minimized: layout.minimized
1871
+ }
1872
+ ];
1873
+ }
1874
+ function withExplicitPages(layout) {
1875
+ if (hasPages(layout)) return layout;
1876
+ return {
1877
+ ...layout,
1878
+ order: [],
1879
+ hidden: [],
1880
+ minimized: [],
1881
+ pages: pageList(layout)
1882
+ };
1883
+ }
1884
+ function sameKeys(a, b) {
1885
+ return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
1886
+ }
1887
+ function pageLayout(layout, pageKey) {
1888
+ if (!hasPages(layout))
1889
+ return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
1890
+ const page = layout.pages?.find(
1891
+ (p) => p.key === pageKey
1533
1892
  );
1534
- const withSettings = settings === void 0 ? {} : { settings };
1535
- if (pages.length > 0) {
1893
+ if (page === void 0) return void 0;
1894
+ const { pages, ...rest } = layout;
1895
+ return {
1896
+ ...rest,
1897
+ order: page.order,
1898
+ hidden: page.hidden,
1899
+ minimized: page.minimized
1900
+ };
1901
+ }
1902
+ function withPageLayout(layout, pageKey, next) {
1903
+ const current = pageLayout(layout, pageKey);
1904
+ if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
1905
+ return layout;
1906
+ }
1907
+ if (!hasPages(layout)) {
1536
1908
  return {
1537
- order: EMPTY_LAYOUT.order,
1538
- hidden: EMPTY_LAYOUT.hidden,
1539
- minimized: EMPTY_LAYOUT.minimized,
1540
- pages,
1541
- ...withSettings
1909
+ ...layout,
1910
+ order: next.order,
1911
+ hidden: next.hidden,
1912
+ minimized: next.minimized
1542
1913
  };
1543
1914
  }
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;
1915
+ return {
1916
+ ...layout,
1917
+ pages: (layout.pages ?? []).map(
1918
+ (page) => page.key === pageKey ? {
1919
+ ...page,
1920
+ order: next.order,
1921
+ hidden: next.hidden,
1922
+ minimized: next.minimized
1923
+ } : page
1924
+ )
1925
+ };
1579
1926
  }
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
- });
1927
+ function normalizedTitle(title) {
1928
+ return title.trim().normalize("NFKC").toLowerCase();
1597
1929
  }
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);
1930
+ function resolvePageKey(layout, reference) {
1931
+ if (reference === void 0 || reference === "") return void 0;
1932
+ const pages = pageList(layout);
1933
+ const byKey = pages.find((p) => p.key === reference);
1934
+ if (byKey !== void 0) return byKey.key;
1935
+ const wanted = normalizedTitle(reference);
1936
+ return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
1602
1937
  }
1603
- function isPinned(item) {
1604
- return resolveWidgetLock(item).move;
1938
+ function homePageKey(layout, item) {
1939
+ return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
1605
1940
  }
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);
1941
+ function assignPages(definitions, layout) {
1942
+ const pages = pageList(layout);
1943
+ const assigned = new Map(pages.map((p) => [p.key, []]));
1944
+ for (const definition of definitions) {
1945
+ let pageKey;
1946
+ if (!resolveWidgetLock(definition).move) {
1947
+ pageKey = pages.find(
1948
+ (p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
1949
+ )?.key;
1614
1950
  }
1951
+ assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
1615
1952
  }
1616
- for (const definition of bySortOrder(visible)) {
1617
- if (byKey.has(definition.key)) ordered.push(definition);
1953
+ return assigned;
1954
+ }
1955
+ function pageOf(definitions, layout, key) {
1956
+ for (const [pageKey, widgets] of assignPages(definitions, layout)) {
1957
+ if (widgets.some((w) => w.key === key)) return pageKey;
1618
1958
  }
1619
- return ordered;
1959
+ return void 0;
1620
1960
  }
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++]);
1961
+ function pageWidgets(definitions, layout, pageKey) {
1962
+ const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
1963
+ const view = pageLayout(layout, pageKey);
1964
+ return view === void 0 ? [] : visibleWidgets(onPage, view);
1627
1965
  }
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))
1966
+ function pageRoom(definitions, layout, pageKey, options = {}) {
1967
+ const page = pageList(layout).find(
1968
+ (p) => p.key === pageKey
1636
1969
  );
1970
+ if (page === void 0) return void 0;
1971
+ const maxRows = rowsAllowed(page, options);
1972
+ const rowsUsed = placeRows(
1973
+ pageWidgets(definitions, layout, pageKey)
1974
+ ).length;
1975
+ return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
1637
1976
  }
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) };
1977
+ function describePage(page) {
1978
+ return `Page "${page.title}"`;
1657
1979
  }
1658
- function freeSlotIndexes(ordered) {
1659
- const slots = [];
1660
- ordered.forEach((item, index) => {
1661
- if (!isPinned(item)) slots.push(index);
1662
- });
1663
- return slots;
1980
+ function titleOf(item) {
1981
+ return item.title ?? item.key;
1664
1982
  }
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))
1983
+ function checkTitle(layout, title, exceptKey) {
1984
+ const trimmed = title.trim();
1985
+ if (trimmed === "") return err("A page title must not be empty.");
1986
+ if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
1987
+ return err(
1988
+ `Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
1989
+ );
1990
+ }
1991
+ const wanted = normalizedTitle(trimmed);
1992
+ const clash = pageList(layout).find(
1993
+ (p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
1675
1994
  );
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);
1995
+ if (clash !== void 0) {
1996
+ return err(`Another page is already called "${clash.title}".`);
1997
+ }
1998
+ return ok(trimmed);
1686
1999
  }
1687
- function isMinimized(layout, key) {
1688
- return layout.minimized.includes(key);
2000
+ function nextPageKey(layout) {
2001
+ const keys = new Set(pageList(layout).map((p) => p.key));
2002
+ let n = 1;
2003
+ while (keys.has(`page-${n}`)) n += 1;
2004
+ return `page-${n}`;
1689
2005
  }
1690
- function hideWidget(layout, key) {
1691
- if (isHidden(layout, key)) return layout;
1692
- return { ...layout, hidden: [...layout.hidden, key] };
2006
+ function nextPageTitle(layout) {
2007
+ const used = new Set(
2008
+ pageList(layout).map((p) => normalizedTitle(p.title))
2009
+ );
2010
+ let n = pageList(layout).length + 1;
2011
+ while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
2012
+ return `Page ${n}`;
1693
2013
  }
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)
2014
+ function addPage(layout, options = {}) {
2015
+ if (pageList(layout).length >= MAX_PARSED_PAGES) {
2016
+ return err(
2017
+ `The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
2018
+ );
2019
+ }
2020
+ const key = options.key ?? nextPageKey(layout);
2021
+ if (pageList(layout).some((p) => p.key === key)) {
2022
+ return err(`A page with the key "${key}" already exists.`);
2023
+ }
2024
+ const title = checkTitle(
2025
+ layout,
2026
+ options.title ?? nextPageTitle(layout)
2027
+ );
2028
+ if (!title.ok) return title;
2029
+ const base = withExplicitPages(layout);
2030
+ const page = {
2031
+ key,
2032
+ title: title.value,
2033
+ order: [],
2034
+ hidden: [],
2035
+ minimized: []
1700
2036
  };
2037
+ return ok({ ...base, pages: [...base.pages ?? [], page] });
1701
2038
  }
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) };
2039
+ function renamePage(layout, pageKey, title) {
2040
+ const page = pageList(layout).find(
2041
+ (p) => p.key === pageKey
2042
+ );
2043
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2044
+ const checked = checkTitle(layout, title, pageKey);
2045
+ if (!checked.ok) return checked;
2046
+ if (checked.value === page.title) return ok(layout);
2047
+ const base = withExplicitPages(layout);
2048
+ return ok({
2049
+ ...base,
2050
+ pages: (base.pages ?? []).map(
2051
+ (p) => p.key === pageKey ? { ...p, title: checked.value } : p
2052
+ )
2053
+ });
1712
2054
  }
1713
- function toggleMinimized(layout, key) {
1714
- return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
2055
+ function movePage(layout, pageKey, offset) {
2056
+ const pages = layout.pages ?? [];
2057
+ const from = pages.findIndex((p) => p.key === pageKey);
2058
+ if (from === -1) return layout;
2059
+ const to = Math.max(
2060
+ 0,
2061
+ Math.min(pages.length - 1, from + Math.trunc(offset))
2062
+ );
2063
+ if (to === from) return layout;
2064
+ const next = [...pages];
2065
+ const [moved] = next.splice(from, 1);
2066
+ next.splice(to, 0, moved);
2067
+ return { ...layout, pages: next };
1715
2068
  }
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
- }));
2069
+ function overflowing(definitions, layout, options) {
2070
+ return pageList(layout).find(
2071
+ (page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
2072
+ );
1724
2073
  }
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;
2074
+ function removePage(definitions, layout, pageKey, options = {}) {
2075
+ const pages = pageList(layout);
2076
+ const index = pages.findIndex((p) => p.key === pageKey);
2077
+ const page = pages[index];
2078
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2079
+ if (pages.length === 1) {
2080
+ return err(
2081
+ `${describePage(page)} is the only page, so it can't be removed.`
2082
+ );
1734
2083
  }
1735
- return {
2084
+ const target = pages[index > 0 ? index - 1 : 1];
2085
+ const merged = {
1736
2086
  ...layout,
1737
- ...top,
1738
- ...pages === void 0 ? {} : { pages }
2087
+ pages: pages.filter((p) => p.key !== pageKey).map(
2088
+ (p) => p.key === target.key ? {
2089
+ ...p,
2090
+ order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
2091
+ hidden: [...p.hidden, ...page.hidden],
2092
+ minimized: [...p.minimized, ...page.minimized]
2093
+ } : p
2094
+ )
1739
2095
  };
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);
2096
+ const full = overflowing(
2097
+ definitions,
2098
+ merged,
2099
+ options
2100
+ );
2101
+ if (full !== void 0) {
2102
+ return err(
2103
+ `${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.`
2104
+ );
1767
2105
  }
1768
- return typeof value;
1769
- }
1770
- function isRecord2(value) {
1771
- return typeof value === "object" && value !== null && !Array.isArray(value);
2106
+ return ok(merged);
1772
2107
  }
1773
- function join(path, field) {
1774
- return path === "" ? field : `${path}.${field}`;
2108
+ function without(keys, key) {
2109
+ return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
1775
2110
  }
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)}.`
2111
+ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
2112
+ const widget = definitions.find((d) => d.key === widgetKey);
2113
+ if (widget === void 0) {
2114
+ return err(
2115
+ `Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
1781
2116
  );
1782
2117
  }
1783
- }
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)}.`
2118
+ const pages = pageList(layout);
2119
+ const target = pages.find((p) => p.key === targetKey);
2120
+ if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
2121
+ const sourceKey = pageOf(definitions, layout, widgetKey);
2122
+ if (sourceKey === targetKey) return ok(layout);
2123
+ if (resolveWidgetLock(widget).move) {
2124
+ return err(
2125
+ `Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
1794
2126
  );
1795
2127
  }
1796
- }
1797
- function optionalNumber(record, field, path, problems) {
1798
- if (record[field] !== void 0) {
1799
- requireNumber(record, field, path, problems);
2128
+ const base = withExplicitPages(layout);
2129
+ const source = (base.pages ?? []).find(
2130
+ (p) => p.key === sourceKey
2131
+ );
2132
+ const hidden = source.hidden.includes(widgetKey);
2133
+ const minimized = source.minimized.includes(widgetKey);
2134
+ const targetOrder = pageWidgets(
2135
+ definitions,
2136
+ base,
2137
+ targetKey
2138
+ ).map((d) => d.key);
2139
+ const moved = {
2140
+ ...base,
2141
+ pages: (base.pages ?? []).map((p) => {
2142
+ if (p.key === sourceKey) {
2143
+ return {
2144
+ ...p,
2145
+ order: without(p.order, widgetKey),
2146
+ hidden: without(p.hidden, widgetKey),
2147
+ minimized: without(p.minimized, widgetKey)
2148
+ };
2149
+ }
2150
+ if (p.key === targetKey) {
2151
+ return {
2152
+ ...p,
2153
+ order: hidden ? p.order : [...targetOrder, widgetKey],
2154
+ hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
2155
+ minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
2156
+ };
2157
+ }
2158
+ return p;
2159
+ })
2160
+ };
2161
+ if (!hidden) {
2162
+ const allowed = rowsAllowed(target, options);
2163
+ const needed = placeRows(
2164
+ pageWidgets(definitions, moved, targetKey)
2165
+ ).length;
2166
+ if (needed > allowed) {
2167
+ return err(
2168
+ `${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
2169
+ );
2170
+ }
1800
2171
  }
2172
+ return ok(moved);
1801
2173
  }
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)}.`
1807
- );
2174
+ function withoutLockField(definition) {
2175
+ if (definition.locked === void 0) return definition;
2176
+ const copy = { ...definition };
2177
+ delete copy.locked;
2178
+ return copy;
2179
+ }
2180
+ function uniqueTitle2(taken, title, index) {
2181
+ let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
2182
+ if (base === "") base = `Page ${index + 1}`;
2183
+ let candidate = base;
2184
+ let n = 1;
2185
+ while (taken.has(normalizedTitle(candidate))) {
2186
+ const suffix = ` (#${n})`;
2187
+ candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
2188
+ n += 1;
1808
2189
  }
2190
+ taken.add(normalizedTitle(candidate));
2191
+ return candidate;
1809
2192
  }
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)}.`
2193
+ function samePages(a, b) {
2194
+ return a.length === b.length && a.every((p, i) => {
2195
+ const q = b[i];
2196
+ 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);
2197
+ });
2198
+ }
2199
+ function normalizeLayout(definitions, layout, options = {}) {
2200
+ const clones = cleanClones(definitions, layout.clones, options);
2201
+ let base = layout;
2202
+ if (clones !== layout.clones) {
2203
+ const { clones: _dropped, ...others } = layout;
2204
+ base = clones === void 0 ? others : { ...others, clones };
2205
+ }
2206
+ return normalizeWidgets(
2207
+ withClones(definitions, base, options),
2208
+ base,
2209
+ options
2210
+ );
2211
+ }
2212
+ function normalizeWidgets(definitions, layout, options) {
2213
+ const known = pruneLayout(definitions, layout);
2214
+ const enforced = enforceLocks(definitions, known, options);
2215
+ const locked = normalizeSettings(
2216
+ definitions,
2217
+ enforced,
2218
+ options
2219
+ );
2220
+ if (!hasPages(locked)) return locked === layout ? layout : locked;
2221
+ const defs = effectiveDefinitions(
2222
+ options.overrideLocks === true ? definitions.map(withoutLockField) : definitions,
2223
+ locked,
2224
+ options
2225
+ );
2226
+ const taken = /* @__PURE__ */ new Set();
2227
+ const seenKeys = /* @__PURE__ */ new Set();
2228
+ const held = /* @__PURE__ */ new Set();
2229
+ let pages = [];
2230
+ (locked.pages ?? []).forEach((page, index) => {
2231
+ if (seenKeys.has(page.key)) return;
2232
+ seenKeys.add(page.key);
2233
+ const keep = (list) => [
2234
+ ...new Set(list.filter((k) => !held.has(k)))
2235
+ ];
2236
+ const next = {
2237
+ ...page,
2238
+ title: uniqueTitle2(taken, page.title, index),
2239
+ order: keep(page.order),
2240
+ hidden: keep(page.hidden),
2241
+ minimized: keep(page.minimized)
2242
+ };
2243
+ for (const k of [...next.order, ...next.hidden]) held.add(k);
2244
+ pages.push(next);
2245
+ });
2246
+ const work = () => ({ ...locked, pages });
2247
+ const pinned = defs.filter((d) => resolveWidgetLock(d).move);
2248
+ for (const widget of pinned) {
2249
+ const home = homePageKey(work(), widget);
2250
+ const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
2251
+ const wasHidden = pages.some((p) => keysHere(p, "hidden"));
2252
+ const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
2253
+ if (!wasHidden && !wasMinimized) continue;
2254
+ pages = pages.map((p) => {
2255
+ let next = p;
2256
+ if (keysHere(p, "hidden")) {
2257
+ next = { ...next, hidden: without(next.hidden, widget.key) };
2258
+ }
2259
+ if (keysHere(p, "minimized")) {
2260
+ next = { ...next, minimized: without(next.minimized, widget.key) };
2261
+ }
2262
+ if (p.key === home) {
2263
+ next = {
2264
+ ...next,
2265
+ hidden: wasHidden && !next.hidden.includes(widget.key) ? [...next.hidden, widget.key] : next.hidden,
2266
+ minimized: wasMinimized && !next.minimized.includes(widget.key) ? [...next.minimized, widget.key] : next.minimized
2267
+ };
2268
+ }
2269
+ return next;
2270
+ });
2271
+ }
2272
+ const placedKeys = /* @__PURE__ */ new Set();
2273
+ for (const p of pages) {
2274
+ for (const k of [...p.order, ...p.hidden]) placedKeys.add(k);
2275
+ }
2276
+ const pinnedKeys = new Set(pinned.map((d) => d.key));
2277
+ const unplaced = defs.filter((d) => !placedKeys.has(d.key) && !pinnedKeys.has(d.key)).map((definition, index) => ({ definition, index })).sort(
2278
+ (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
2279
+ ).map(({ definition }) => definition);
2280
+ for (const widget of unplaced) {
2281
+ const home = homePageKey(work(), widget);
2282
+ pages = pages.map(
2283
+ (p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
1815
2284
  );
1816
2285
  }
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)}.`
2286
+ for (let i = 0; i < pages.length; i += 1) {
2287
+ const page = pages[i];
2288
+ const shown = pageWidgets(defs, work(), page.key);
2289
+ const fits = fitCount(shown, rowsAllowed(page, options));
2290
+ if (fits >= shown.length) continue;
2291
+ const moving = shown.slice(fits).filter((d) => !pinnedKeys.has(d.key)).map((d) => d.key);
2292
+ if (moving.length === 0) continue;
2293
+ const moveSet = new Set(moving);
2294
+ const movedMinimized = page.minimized.filter(
2295
+ (k) => moveSet.has(k)
1824
2296
  );
1825
- } else if (!isSafe(value)) {
1826
- problems.push(
1827
- `${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
2297
+ pages = pages.map(
2298
+ (p) => p.key === page.key ? {
2299
+ ...p,
2300
+ order: p.order.filter((k) => !moveSet.has(k)),
2301
+ minimized: p.minimized.filter((k) => !moveSet.has(k))
2302
+ } : p
1828
2303
  );
2304
+ let placed = false;
2305
+ for (let j = i + 1; j < pages.length && !placed; j += 1) {
2306
+ const candidate = pages[j];
2307
+ const trial = pages.map(
2308
+ (p) => p.key === candidate.key ? {
2309
+ ...p,
2310
+ order: [...moving, ...p.order],
2311
+ minimized: [...p.minimized, ...movedMinimized]
2312
+ } : p
2313
+ );
2314
+ const items = pageWidgets(
2315
+ defs,
2316
+ { ...locked, pages: trial },
2317
+ candidate.key
2318
+ );
2319
+ if (placeRows(items).length <= rowsAllowed(candidate, options)) {
2320
+ pages = trial;
2321
+ placed = true;
2322
+ }
2323
+ }
2324
+ if (!placed) {
2325
+ const key = nextPageKey(work());
2326
+ const title = uniqueTitle2(
2327
+ new Set(pages.map((p) => normalizedTitle(p.title))),
2328
+ `Page ${pages.length + 1}`,
2329
+ pages.length
2330
+ );
2331
+ pages = [
2332
+ ...pages,
2333
+ { key, title, order: moving, hidden: [], minimized: movedMinimized }
2334
+ ];
2335
+ }
1829
2336
  }
2337
+ const result = work();
2338
+ return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) && locked.settings === layout.settings ? layout : result;
1830
2339
  }
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;
2340
+
2341
+ // src/core/duplicate.ts
2342
+ function canDuplicate(definitions, key, options = {}) {
2343
+ const definition = definitions.find(
2344
+ (d) => d.key === key
2345
+ );
2346
+ if (definition === void 0) return false;
2347
+ if (options.overrideLocks === true) return true;
2348
+ const sourceKey = definition.sourceKey ?? definition.key;
2349
+ const original = definitions.find(
2350
+ (d) => d.key === sourceKey
2351
+ );
2352
+ return original !== void 0 && !resolveWidgetLock(original).clone;
2353
+ }
2354
+ function nextCloneKey(definitions, layout, fromKey) {
2355
+ const taken = /* @__PURE__ */ new Set([
2356
+ ...definitions.map((d) => d.key),
2357
+ ...(layout.clones ?? []).map((c) => c.key)
2358
+ ]);
2359
+ let highest = 0;
2360
+ for (const clone of layout.clones ?? []) {
2361
+ if (clone.from === fromKey) {
2362
+ highest = Math.max(highest, cloneNumber(clone.key) ?? 0);
2363
+ }
1838
2364
  }
1839
- return value;
2365
+ let n = highest + 1;
2366
+ while (taken.has(`${fromKey}#${n}`)) n += 1;
2367
+ return `${fromKey}#${n}`;
1840
2368
  }
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)}.`
2369
+ function suggestedViewTitle(title, viewLabel) {
2370
+ return `${title} ${viewLabel}`.trim().slice(0, MAX_WIDGET_TITLE_LENGTH);
2371
+ }
2372
+ function duplicateWidget(definitions, layout, key, options = {}) {
2373
+ const widget = definitions.find((d) => d.key === key);
2374
+ if (widget === void 0) {
2375
+ return err(`Widget ${JSON.stringify(key)} isn't on this dashboard.`);
2376
+ }
2377
+ const originalKey = widget.sourceKey ?? widget.key;
2378
+ if (!canDuplicate(definitions, key, options)) {
2379
+ return err(
2380
+ `Widget ${JSON.stringify(originalKey)} is locked against duplicating.`
1846
2381
  );
1847
2382
  }
1848
- }
1849
- function eachRecord(items, path, problems, check) {
2383
+ const cloneKey = nextCloneKey(definitions, layout, originalKey);
2384
+ const copiedSettings = (() => {
2385
+ const mine = layout.settings?.[key];
2386
+ if (mine === void 0) return void 0;
2387
+ const { title: _title, ...rest } = mine;
2388
+ return Object.keys(rest).length === 0 ? void 0 : rest;
2389
+ })();
2390
+ const settings = copiedSettings === void 0 ? layout.settings : { ...layout.settings, [cloneKey]: copiedSettings };
2391
+ let next = {
2392
+ ...layout,
2393
+ clones: [...layout.clones ?? [], { key: cloneKey, from: originalKey }],
2394
+ ...settings === void 0 ? {} : { settings }
2395
+ };
2396
+ const all = withClones(definitions, next, options);
2397
+ const assigned = assignPages(all, next);
2398
+ const pageKey = [...assigned].find(
2399
+ ([, widgets]) => widgets.some((w) => w.key === key)
2400
+ )?.[0];
2401
+ if (pageKey !== void 0) {
2402
+ const view = pageLayout(next, pageKey);
2403
+ const onPage = assigned.get(pageKey) ?? [];
2404
+ if (view !== void 0) {
2405
+ const shown = visibleWidgets(onPage, view).filter((d) => !resolveWidgetLock(d).move && d.key !== cloneKey).map((d) => d.key);
2406
+ const at = shown.indexOf(key);
2407
+ const order = at === -1 ? [...shown, cloneKey] : [...shown.slice(0, at + 1), cloneKey, ...shown.slice(at + 1)];
2408
+ next = withPageLayout(next, pageKey, { ...view, order });
2409
+ }
2410
+ }
2411
+ if (hasPages(next) && pageKey !== void 0) {
2412
+ const effective = effectiveDefinitions(all, next, options);
2413
+ const room = pageRoom(effective, next, pageKey, options);
2414
+ if (room !== void 0 && room.rowsUsed > room.maxRows) {
2415
+ const page = next.pages?.find(
2416
+ (p) => p.key === pageKey
2417
+ );
2418
+ const width = effective.find(
2419
+ (d) => d.key === cloneKey
2420
+ )?.width;
2421
+ return err(
2422
+ `Page "${page?.title ?? pageKey}" has no room for a widget${width === void 0 ? "" : ` ${width} wide`} (it holds ${room.maxRows} rows).`
2423
+ );
2424
+ }
2425
+ }
2426
+ return ok({ layout: next, key: cloneKey });
2427
+ }
2428
+ function deleteClone(layout, key) {
2429
+ if (layout.clones?.some((clone) => clone.key === key) !== true) return layout;
2430
+ const without2 = (keys) => keys.includes(key) ? keys.filter((k) => k !== key) : keys;
2431
+ const { clones, settings, ...rest } = layout;
2432
+ const remaining = (clones ?? []).filter((c) => c.key !== key);
2433
+ const { [key]: _gone, ...otherSettings } = settings ?? {};
2434
+ return {
2435
+ ...rest,
2436
+ order: without2(layout.order),
2437
+ hidden: without2(layout.hidden),
2438
+ minimized: without2(layout.minimized),
2439
+ ...layout.pages === void 0 ? {} : {
2440
+ pages: layout.pages.map((page) => ({
2441
+ ...page,
2442
+ order: without2(page.order),
2443
+ hidden: without2(page.hidden),
2444
+ minimized: without2(page.minimized)
2445
+ }))
2446
+ },
2447
+ ...Object.keys(otherSettings).length === 0 ? {} : { settings: otherSettings },
2448
+ ...remaining.length === 0 ? {} : { clones: remaining }
2449
+ };
2450
+ }
2451
+
2452
+ // src/core/validate.ts
2453
+ var MAX_REPORTED_PROBLEMS = 10;
2454
+ function describeType2(value) {
2455
+ if (value === null) return "null";
2456
+ if (Array.isArray(value)) return "array";
2457
+ if (typeof value === "number" && !Number.isFinite(value)) {
2458
+ return String(value);
2459
+ }
2460
+ return typeof value;
2461
+ }
2462
+ function isRecord3(value) {
2463
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2464
+ }
2465
+ function join(path, field) {
2466
+ return path === "" ? field : `${path}.${field}`;
2467
+ }
2468
+ function requireString(record, field, path, problems) {
2469
+ const value = record[field];
2470
+ if (typeof value !== "string") {
2471
+ problems.push(
2472
+ `${join(path, field)} must be a string, got ${describeType2(value)}.`
2473
+ );
2474
+ }
2475
+ }
2476
+ function optionalString(record, field, path, problems) {
2477
+ if (record[field] !== void 0) {
2478
+ requireString(record, field, path, problems);
2479
+ }
2480
+ }
2481
+ function requireNumber(record, field, path, problems) {
2482
+ const value = record[field];
2483
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2484
+ problems.push(
2485
+ `${join(path, field)} must be a finite number, got ${describeType2(value)}.`
2486
+ );
2487
+ }
2488
+ }
2489
+ function optionalNumber(record, field, path, problems) {
2490
+ if (record[field] !== void 0) {
2491
+ requireNumber(record, field, path, problems);
2492
+ }
2493
+ }
2494
+ function optionalSortValue(record, path, problems) {
2495
+ const value = record["value"];
2496
+ if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
2497
+ problems.push(
2498
+ `${path}.value must be a string or a finite number, got ${describeType2(value)}.`
2499
+ );
2500
+ }
2501
+ }
2502
+ function optionalBoolean(record, field, path, problems) {
2503
+ const value = record[field];
2504
+ if (value !== void 0 && typeof value !== "boolean") {
2505
+ problems.push(
2506
+ `${join(path, field)} must be a boolean, got ${describeType2(value)}.`
2507
+ );
2508
+ }
2509
+ }
2510
+ function optionalUrl(record, field, path, problems, isSafe, allowed) {
2511
+ const value = record[field];
2512
+ if (value === void 0) return;
2513
+ if (typeof value !== "string") {
2514
+ problems.push(
2515
+ `${join(path, field)} must be a string, got ${describeType2(value)}.`
2516
+ );
2517
+ } else if (!isSafe(value)) {
2518
+ problems.push(
2519
+ `${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
2520
+ );
2521
+ }
2522
+ }
2523
+ function requireArray(record, field, path, problems) {
2524
+ const value = record[field];
2525
+ if (!Array.isArray(value)) {
2526
+ problems.push(
2527
+ `${join(path, field)} must be an array, got ${describeType2(value)}.`
2528
+ );
2529
+ return null;
2530
+ }
2531
+ return value;
2532
+ }
2533
+ function requireOneOf(record, field, options, path, problems) {
2534
+ const value = record[field];
2535
+ if (typeof value !== "string" || !options.includes(value)) {
2536
+ problems.push(
2537
+ `${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType2(value)}.`
2538
+ );
2539
+ }
2540
+ }
2541
+ function eachRecord(items, path, problems, check) {
1850
2542
  items.forEach((item, index) => {
1851
2543
  const itemPath = `${path}[${index}]`;
1852
- if (isRecord2(item)) {
2544
+ if (isRecord3(item)) {
1853
2545
  check(item, itemPath);
1854
2546
  } else {
1855
2547
  problems.push(
@@ -1998,7 +2690,7 @@ function checkPayloadFields(kind, data, problems) {
1998
2690
  }
1999
2691
  function checkDetailOption(detail, problems) {
2000
2692
  if (detail === void 0 || typeof detail === "boolean") return;
2001
- if (!isRecord2(detail)) {
2693
+ if (!isRecord3(detail)) {
2002
2694
  problems.push(
2003
2695
  `detail must be true, false or an options object, got ${describeType2(detail)}.`
2004
2696
  );
@@ -2017,13 +2709,13 @@ function checkDetailOption(detail, problems) {
2017
2709
  }
2018
2710
  function checkTableControlsOption(controls, kind, problems) {
2019
2711
  if (controls === void 0 || controls === false) return;
2020
- if (typeof controls !== "boolean" && !isRecord2(controls)) {
2712
+ if (typeof controls !== "boolean" && !isRecord3(controls)) {
2021
2713
  problems.push(
2022
2714
  `tableControls must be true, false or an options object, got ${describeType2(controls)}.`
2023
2715
  );
2024
2716
  return;
2025
2717
  }
2026
- if (isRecord2(controls)) {
2718
+ if (isRecord3(controls)) {
2027
2719
  optionalBoolean(controls, "search", "tableControls", problems);
2028
2720
  optionalBoolean(controls, "sort", "tableControls", problems);
2029
2721
  }
@@ -2065,7 +2757,7 @@ function checkWidthLimits(value, problems) {
2065
2757
  }
2066
2758
  function checkLockedOption(locked, problems) {
2067
2759
  if (locked === void 0 || typeof locked === "boolean") return;
2068
- if (!isRecord2(locked)) {
2760
+ if (!isRecord3(locked)) {
2069
2761
  problems.push(
2070
2762
  `locked must be true, false or an object, got ${describeType2(locked)}.`
2071
2763
  );
@@ -2075,6 +2767,7 @@ function checkLockedOption(locked, problems) {
2075
2767
  optionalBoolean(locked, "hide", "locked", problems);
2076
2768
  optionalBoolean(locked, "minimize", "locked", problems);
2077
2769
  optionalBoolean(locked, "options", "locked", problems);
2770
+ optionalBoolean(locked, "clone", "locked", problems);
2078
2771
  }
2079
2772
  function checkOptionColumns(option, path, kind, problems) {
2080
2773
  const columns = option["columns"];
@@ -2087,7 +2780,7 @@ function checkOptionColumns(option, path, kind, problems) {
2087
2780
  const keys = [];
2088
2781
  columns.forEach((entry, index) => {
2089
2782
  const entryPath = `${path}.columns[${index}]`;
2090
- if (!isRecord2(entry)) {
2783
+ if (!isRecord3(entry)) {
2091
2784
  problems.push(
2092
2785
  `${entryPath} must be an object, got ${describeType2(entry)}.`
2093
2786
  );
@@ -2112,7 +2805,7 @@ function checkOptionColumns(option, path, kind, problems) {
2112
2805
  return keys;
2113
2806
  }
2114
2807
  function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
2115
- if (!isRecord2(option)) {
2808
+ if (!isRecord3(option)) {
2116
2809
  problems.push(`${path} must be an object, got ${describeType2(option)}.`);
2117
2810
  return;
2118
2811
  }
@@ -2144,7 +2837,7 @@ function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
2144
2837
  } else {
2145
2838
  choices.forEach((choice, index) => {
2146
2839
  const choicePath = `${path}.choices[${index}]`;
2147
- if (!isRecord2(choice)) {
2840
+ if (!isRecord3(choice)) {
2148
2841
  problems.push(
2149
2842
  `${choicePath} must be an object, got ${describeType2(choice)}.`
2150
2843
  );
@@ -2234,7 +2927,7 @@ function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
2234
2927
  } else {
2235
2928
  presets.forEach((preset, index) => {
2236
2929
  const presetPath = `${path}.presets[${index}]`;
2237
- if (!isRecord2(preset)) {
2930
+ if (!isRecord3(preset)) {
2238
2931
  problems.push(
2239
2932
  `${presetPath} must be an object, got ${describeType2(preset)}.`
2240
2933
  );
@@ -2363,7 +3056,7 @@ function checkColumnIndex(value, path, problems) {
2363
3056
  }
2364
3057
  }
2365
3058
  function checkConversion(conversion, path, baseKind, viewKind2, problems) {
2366
- if (!isRecord2(conversion)) {
3059
+ if (!isRecord3(conversion)) {
2367
3060
  problems.push(
2368
3061
  `${path} must be an object with a type, got ${describeType2(conversion)}.`
2369
3062
  );
@@ -2441,7 +3134,7 @@ function checkViews(value, problems) {
2441
3134
  } else {
2442
3135
  views.forEach((view, index) => {
2443
3136
  const path = `views[${index}]`;
2444
- if (!isRecord2(view)) {
3137
+ if (!isRecord3(view)) {
2445
3138
  problems.push(
2446
3139
  `${path} must be an object, got ${describeType2(view)}.`
2447
3140
  );
@@ -2492,7 +3185,7 @@ function formatProblems(prefix, problems) {
2492
3185
  }
2493
3186
  function validateWidgetData(value, options = {}) {
2494
3187
  const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
2495
- if (!isRecord2(value)) {
3188
+ if (!isRecord3(value)) {
2496
3189
  return err(`${subject}: must be an object, got ${describeType2(value)}.`);
2497
3190
  }
2498
3191
  if (value["empty"] === true) {
@@ -2522,7 +3215,7 @@ function validateWidgetData(value, options = {}) {
2522
3215
  return ok(value);
2523
3216
  }
2524
3217
  function validateWidgetDefinition(value) {
2525
- if (!isRecord2(value)) {
3218
+ if (!isRecord3(value)) {
2526
3219
  return err(
2527
3220
  `Widget definition: must be an object, got ${describeType2(value)}.`
2528
3221
  );
@@ -2606,7 +3299,7 @@ function loadingWidgets(definitions) {
2606
3299
  return definitions.map((definition) => ({ definition, status: "loading" }));
2607
3300
  }
2608
3301
  function cacheKeyFor(definition, options, values) {
2609
- const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
3302
+ const base = options.cacheKey === void 0 ? definition.sourceKey ?? definition.key : options.cacheKey(definition);
2610
3303
  return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
2611
3304
  }
2612
3305
  function optionValuesFor(definition, options) {
@@ -2720,7 +3413,10 @@ async function resolveWidgets(definitions, providers, context, options = {}) {
2720
3413
  visible.map(
2721
3414
  (definition) => resolveWidget(
2722
3415
  definition,
2723
- Object.prototype.hasOwnProperty.call(providers, definition.key) ? providers[definition.key] : void 0,
3416
+ Object.prototype.hasOwnProperty.call(
3417
+ providers,
3418
+ definition.sourceKey ?? definition.key
3419
+ ) ? providers[definition.sourceKey ?? definition.key] : void 0,
2724
3420
  context,
2725
3421
  options
2726
3422
  )
@@ -2800,7 +3496,11 @@ function createWidgetLoader(options) {
2800
3496
  }
2801
3497
  }
2802
3498
  if (!isCurrent()) return;
2803
- const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
3499
+ const providerKey = definition.sourceKey ?? key;
3500
+ const provider = Object.prototype.hasOwnProperty.call(
3501
+ providers,
3502
+ providerKey
3503
+ ) ? providers[providerKey] : void 0;
2804
3504
  const result = await resolveWidgetUnapplied(definition, provider, context, {
2805
3505
  ...runOptions,
2806
3506
  signal: controller.signal
@@ -2889,484 +3589,6 @@ function createWidgetLoader(options) {
2889
3589
  };
2890
3590
  }
2891
3591
 
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
3592
  // src/core/scale.ts
3371
3593
  function niceStep(value) {
3372
3594
  if (!(value > 0) || !Number.isFinite(value)) return 1;
@@ -4196,6 +4418,6 @@ function indent(block) {
4196
4418
  return block.split("\n").map((line) => ` ${line}`).join("\n");
4197
4419
  }
4198
4420
 
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 };
4421
+ 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, isSafeHref, isSafeImageUrl, isTitleInUse, isWidgetEmptyState, isWidgetKind, isWidgetVisibleTo, isWidgetWidth, itemWidth, kindLabel, kpiDelta, labelStride, 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, visibleWidgets, widgetOptionValues, widgetsFor, widthForSize, widthRange, widthShareLabel, withClones, withEncoding, withFallback, withLogging, withPageLayout, withPrefix, withReadCache, withRetry };
4200
4422
  //# sourceMappingURL=index.js.map
4201
4423
  //# sourceMappingURL=index.js.map