@richardmcquiston01/dashboard-widgets-toolkit 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js CHANGED
@@ -632,21 +632,44 @@ function fillColumnSpans(items, columns, largeSpan = 2) {
632
632
  function fillWidthSpans(items) {
633
633
  return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
634
634
  }
635
+ function placeRows(items) {
636
+ return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
637
+ }
638
+ function fitCount(items, maxRows) {
639
+ const rows = placeRows(items);
640
+ let count = 0;
641
+ for (const row of rows.slice(0, Math.max(0, Math.floor(maxRows)))) {
642
+ count += row.keys.length;
643
+ }
644
+ return count;
645
+ }
635
646
  function placeAndFill(items, columns, spanOf) {
647
+ return placeAndFillRows(items, columns, spanOf).spans;
648
+ }
649
+ function placeAndFillRows(items, columns, spanOf) {
636
650
  const trackCount = Math.max(1, Math.floor(columns));
637
651
  const spans = /* @__PURE__ */ new Map();
652
+ const rows = [];
638
653
  let row = [];
639
654
  let used = 0;
640
655
  function closeRow() {
656
+ if (row.length === 0) return;
641
657
  const fillers = row.filter(({ item }) => fillsWidth(item.fill));
642
658
  const leftover = trackCount - used;
659
+ const finalSpans = /* @__PURE__ */ new Map();
643
660
  if (fillers.length > 0) {
644
661
  const share = Math.floor(leftover / fillers.length);
645
662
  const remainder = leftover % fillers.length;
646
663
  fillers.forEach(({ item, span }, index) => {
647
- spans.set(item.key, span + share + (index < remainder ? 1 : 0));
664
+ const grown = span + share + (index < remainder ? 1 : 0);
665
+ spans.set(item.key, grown);
666
+ finalSpans.set(item.key, grown);
648
667
  });
649
668
  }
669
+ rows.push({
670
+ keys: row.map(({ item }) => item.key),
671
+ spans: row.map(({ item, span }) => finalSpans.get(item.key) ?? span)
672
+ });
650
673
  row = [];
651
674
  used = 0;
652
675
  }
@@ -657,10 +680,13 @@ function placeAndFill(items, columns, spanOf) {
657
680
  used += span;
658
681
  }
659
682
  closeRow();
660
- return spans;
683
+ return { spans, rows };
661
684
  }
662
685
 
663
686
  // src/core/layout.ts
687
+ var MAX_PAGE_TITLE_LENGTH = 30;
688
+ var MAX_PARSED_PAGES = 50;
689
+ var MAX_PAGE_ROWS = 20;
664
690
  var EMPTY_LAYOUT = Object.freeze({
665
691
  order: Object.freeze([]),
666
692
  hidden: Object.freeze([]),
@@ -690,16 +716,66 @@ function parseLayout(raw) {
690
716
  const order = uniqueStrings(record["order"]);
691
717
  const hidden = uniqueStrings(record["hidden"]);
692
718
  const minimized = uniqueStrings(record["minimized"]);
719
+ const pages = parsePages(record["pages"]);
720
+ if (pages.length > 0) {
721
+ return {
722
+ order: EMPTY_LAYOUT.order,
723
+ hidden: EMPTY_LAYOUT.hidden,
724
+ minimized: EMPTY_LAYOUT.minimized,
725
+ pages
726
+ };
727
+ }
693
728
  if (order.length === 0 && hidden.length === 0 && minimized.length === 0) {
694
729
  return EMPTY_LAYOUT;
695
730
  }
696
731
  return { order, hidden, minimized };
697
732
  }
733
+ function parsePages(value) {
734
+ if (!Array.isArray(value)) return [];
735
+ const pages = [];
736
+ const keys = /* @__PURE__ */ new Set();
737
+ for (const entry of value.slice(0, MAX_PARSED_PAGES)) {
738
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
739
+ continue;
740
+ }
741
+ const record = entry;
742
+ const key = record["key"];
743
+ if (typeof key !== "string" || key === "" || keys.has(key)) continue;
744
+ keys.add(key);
745
+ const rawTitle = record["title"];
746
+ const title = typeof rawTitle === "string" && rawTitle.trim() !== "" ? rawTitle.trim().slice(0, MAX_PAGE_TITLE_LENGTH) : `Page ${pages.length + 1}`;
747
+ const maxRows = record["maxRows"];
748
+ const held = /* @__PURE__ */ new Set();
749
+ for (const earlier of pages) {
750
+ for (const k of [...earlier.order, ...earlier.hidden]) held.add(k);
751
+ }
752
+ const keep = (list) => uniqueStrings(list).filter((k) => !held.has(k));
753
+ pages.push({
754
+ key,
755
+ title,
756
+ order: keep(record["order"]),
757
+ hidden: keep(record["hidden"]),
758
+ minimized: keep(record["minimized"]),
759
+ ...typeof maxRows === "number" && Number.isInteger(maxRows) && maxRows >= 1 && maxRows <= MAX_PAGE_ROWS ? { maxRows } : {}
760
+ });
761
+ }
762
+ return pages;
763
+ }
698
764
  function serializeLayout(layout) {
699
765
  return JSON.stringify({
700
766
  order: [...layout.order],
701
767
  hidden: [...layout.hidden],
702
- minimized: [...layout.minimized]
768
+ minimized: [...layout.minimized],
769
+ ...layout.pages === void 0 || layout.pages.length === 0 ? {} : {
770
+ pages: layout.pages.map((page) => ({
771
+ key: page.key,
772
+ title: page.title,
773
+ order: [...page.order],
774
+ hidden: [...page.hidden],
775
+ minimized: [...page.minimized],
776
+ ...page.maxRows === void 0 ? {} : { maxRows: page.maxRows }
777
+ }))
778
+ }
703
779
  });
704
780
  }
705
781
  function bySortOrder(items) {
@@ -822,13 +898,28 @@ function toggleMinimized(layout, key) {
822
898
  }
823
899
  function pruneLayout(definitions, layout) {
824
900
  const known = new Set(definitions.map((d) => d.key));
825
- const keep = (keys) => keys.filter((k) => known.has(k));
826
- const next = {
827
- order: keep(layout.order),
828
- hidden: keep(layout.hidden),
829
- minimized: keep(layout.minimized)
901
+ const keep = (keys) => keys.every((k) => known.has(k)) ? keys : keys.filter((k) => known.has(k));
902
+ return mapLayoutLists(layout, (lists) => ({
903
+ order: keep(lists.order),
904
+ hidden: keep(lists.hidden),
905
+ minimized: keep(lists.minimized)
906
+ }));
907
+ }
908
+ function mapLayoutLists(layout, change) {
909
+ const top = change(layout);
910
+ const pages = layout.pages?.map((page) => {
911
+ const next = change(page);
912
+ return next.order === page.order && next.hidden === page.hidden && next.minimized === page.minimized ? page : { ...page, ...next };
913
+ });
914
+ const pagesChanged = pages !== void 0 && pages.some((page, i) => page !== layout.pages?.[i]);
915
+ if (top.order === layout.order && top.hidden === layout.hidden && top.minimized === layout.minimized && !pagesChanged) {
916
+ return layout;
917
+ }
918
+ return {
919
+ ...layout,
920
+ ...top,
921
+ ...pages === void 0 ? {} : { pages }
830
922
  };
831
- return next.order.length === layout.order.length && next.hidden.length === layout.hidden.length && next.minimized.length === layout.minimized.length ? layout : next;
832
923
  }
833
924
  function enforceLocks(definitions, layout, options = {}) {
834
925
  if (options.overrideLocks === true) return layout;
@@ -841,11 +932,289 @@ function enforceLocks(definitions, layout, options = {}) {
841
932
  if (lock.hide) noHide.add(definition.key);
842
933
  if (lock.minimize) noMinimize.add(definition.key);
843
934
  }
844
- const without = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
845
- const order = without(layout.order, noMove);
846
- const hidden = without(layout.hidden, noHide);
847
- const minimized = without(layout.minimized, noMinimize);
848
- return order === layout.order && hidden === layout.hidden && minimized === layout.minimized ? layout : { ...layout, order, hidden, minimized };
935
+ const without2 = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
936
+ return mapLayoutLists(layout, (lists) => ({
937
+ order: without2(lists.order, noMove),
938
+ hidden: without2(lists.hidden, noHide),
939
+ minimized: without2(lists.minimized, noMinimize)
940
+ }));
941
+ }
942
+
943
+ // src/core/options.ts
944
+ var DATE_RANGE_PRESETS = [
945
+ "today",
946
+ "yesterday",
947
+ "last7",
948
+ "last30",
949
+ "last90",
950
+ "thisMonth",
951
+ "lastMonth",
952
+ "thisYear",
953
+ "lastYear"
954
+ ];
955
+ var WIDGET_OPTION_TYPES = [
956
+ "choice",
957
+ "number",
958
+ "boolean",
959
+ "text",
960
+ "dateRange",
961
+ "color",
962
+ "sort",
963
+ "columns"
964
+ ];
965
+ var MAX_WIDGET_OPTIONS = 20;
966
+ var MAX_TEXT_OPTION_LENGTH = 500;
967
+ var OPTION_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,39}$/;
968
+ var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
969
+ var INTERVAL_PATTERN = /^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/;
970
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
971
+ function isColumnKeyFor(kind, key) {
972
+ if (kind === "TABLE") return /^c\d+$/.test(key);
973
+ if (kind === "BAR_LIST") return key === "label" || key === "value";
974
+ return false;
975
+ }
976
+ function isClientApplied(option) {
977
+ return (option.type === "sort" || option.type === "columns") && option.apply === "client";
978
+ }
979
+ function civilToday(context) {
980
+ const now = context.now ?? Date.now();
981
+ let timeZone = context.timeZone ?? "UTC";
982
+ try {
983
+ new Intl.DateTimeFormat("en-US", { timeZone });
984
+ } catch {
985
+ timeZone = "UTC";
986
+ }
987
+ const parts = new Intl.DateTimeFormat("en-US", {
988
+ timeZone,
989
+ year: "numeric",
990
+ month: "2-digit",
991
+ day: "2-digit"
992
+ }).formatToParts(new Date(now));
993
+ const pick = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
994
+ return { year: pick("year"), month: pick("month"), day: pick("day") };
995
+ }
996
+ function toIso(epochMs) {
997
+ return new Date(epochMs).toISOString().slice(0, 10);
998
+ }
999
+ var DAY_MS = 864e5;
1000
+ function parseIsoDay(text) {
1001
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
1002
+ if (match === null) return void 0;
1003
+ const epoch = Date.UTC(
1004
+ Number(match[1]),
1005
+ Number(match[2]) - 1,
1006
+ Number(match[3])
1007
+ );
1008
+ return toIso(epoch) === text ? epoch : void 0;
1009
+ }
1010
+ function isIsoInterval(value) {
1011
+ const match = INTERVAL_PATTERN.exec(value);
1012
+ if (match === null) return false;
1013
+ const start = parseIsoDay(match[1]);
1014
+ const end = parseIsoDay(match[2]);
1015
+ return start !== void 0 && end !== void 0 && start <= end;
1016
+ }
1017
+ function isPreset(value) {
1018
+ return DATE_RANGE_PRESETS.includes(value);
1019
+ }
1020
+ function resolveDateRange(value, context = {}) {
1021
+ if (isIsoInterval(value)) return value;
1022
+ if (!isPreset(value)) return void 0;
1023
+ const today = civilToday(context);
1024
+ const todayMs = Date.UTC(today.year, today.month - 1, today.day);
1025
+ const range = (startMs, endMs) => `${toIso(startMs)}/${toIso(endMs)}`;
1026
+ switch (value) {
1027
+ case "today":
1028
+ return range(todayMs, todayMs);
1029
+ case "yesterday":
1030
+ return range(todayMs - DAY_MS, todayMs - DAY_MS);
1031
+ case "last7":
1032
+ return range(todayMs - 6 * DAY_MS, todayMs);
1033
+ case "last30":
1034
+ return range(todayMs - 29 * DAY_MS, todayMs);
1035
+ case "last90":
1036
+ return range(todayMs - 89 * DAY_MS, todayMs);
1037
+ case "thisMonth":
1038
+ return range(Date.UTC(today.year, today.month - 1, 1), todayMs);
1039
+ case "lastMonth":
1040
+ return range(
1041
+ Date.UTC(today.year, today.month - 2, 1),
1042
+ Date.UTC(today.year, today.month - 1, 1) - DAY_MS
1043
+ );
1044
+ case "thisYear":
1045
+ return range(Date.UTC(today.year, 0, 1), todayMs);
1046
+ case "lastYear":
1047
+ return range(
1048
+ Date.UTC(today.year - 1, 0, 1),
1049
+ Date.UTC(today.year, 0, 1) - DAY_MS
1050
+ );
1051
+ }
1052
+ }
1053
+ function onStepGrid(option, value) {
1054
+ if (option.step === void 0) return true;
1055
+ const steps = (value - option.min) / option.step;
1056
+ return Math.abs(steps - Math.round(steps)) < 1e-9;
1057
+ }
1058
+ function parseSortValue(value) {
1059
+ const separator = value.lastIndexOf(":");
1060
+ if (separator <= 0) return void 0;
1061
+ const column = value.slice(0, separator);
1062
+ const direction = value.slice(separator + 1);
1063
+ if (direction !== "asc" && direction !== "desc") return void 0;
1064
+ return { column, direction };
1065
+ }
1066
+ function sortFromOptions(definition, values) {
1067
+ if (values === void 0) return void 0;
1068
+ const sortOption = definition.options?.find(
1069
+ (option) => option.type === "sort"
1070
+ );
1071
+ if (sortOption === void 0) return void 0;
1072
+ const chosen = values[sortOption.key];
1073
+ return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
1074
+ }
1075
+ function coerceOptionValue(option, value, context = {}) {
1076
+ switch (option.type) {
1077
+ case "choice":
1078
+ return typeof value === "string" && option.choices.some((choice) => choice.value === value) ? value : void 0;
1079
+ case "number":
1080
+ return typeof value === "number" && Number.isFinite(value) && value >= option.min && value <= option.max && onStepGrid(option, value) ? value : void 0;
1081
+ case "boolean":
1082
+ return typeof value === "boolean" ? value : void 0;
1083
+ case "text": {
1084
+ if (typeof value !== "string") return void 0;
1085
+ const trimmed = value.trim();
1086
+ return trimmed.length <= option.maxLength && !CONTROL_CHARACTERS.test(trimmed) ? trimmed : void 0;
1087
+ }
1088
+ case "dateRange": {
1089
+ if (typeof value !== "string") return void 0;
1090
+ if (isPreset(value) && option.presets !== void 0 && !option.presets.some((preset) => preset.value === value)) {
1091
+ return void 0;
1092
+ }
1093
+ return resolveDateRange(value, context);
1094
+ }
1095
+ case "color":
1096
+ return typeof value === "string" && COLOR_PATTERN.test(value) ? value.toLowerCase() : void 0;
1097
+ case "sort": {
1098
+ if (typeof value !== "string") return void 0;
1099
+ const sort = parseSortValue(value);
1100
+ return sort !== void 0 && option.columns.some((column) => column.key === sort.column) ? value : void 0;
1101
+ }
1102
+ case "columns": {
1103
+ if (!Array.isArray(value) || value.length === 0) return void 0;
1104
+ const keys = value;
1105
+ const allowed = new Set(option.columns.map((c) => c.key));
1106
+ const seen = /* @__PURE__ */ new Set();
1107
+ for (const key of keys) {
1108
+ if (typeof key !== "string" || !allowed.has(key) || seen.has(key)) {
1109
+ return void 0;
1110
+ }
1111
+ seen.add(key);
1112
+ }
1113
+ return [...seen];
1114
+ }
1115
+ }
1116
+ }
1117
+ function defaultOptionValue(option, context = {}) {
1118
+ switch (option.type) {
1119
+ case "dateRange":
1120
+ return resolveDateRange(option.default, context);
1121
+ case "sort":
1122
+ return option.default;
1123
+ case "columns":
1124
+ return [...option.default];
1125
+ default:
1126
+ return option.default;
1127
+ }
1128
+ }
1129
+ var NO_VALUES = Object.freeze({});
1130
+ function resolveOptionValues(definition, chosen, context = {}) {
1131
+ const options = definition.options;
1132
+ if (options === void 0 || options.length === 0) return NO_VALUES;
1133
+ const values = {};
1134
+ for (const option of options) {
1135
+ const picked = chosen?.[option.key];
1136
+ const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked, context)) ?? defaultOptionValue(option, context);
1137
+ if (value !== void 0) values[option.key] = value;
1138
+ }
1139
+ return values;
1140
+ }
1141
+ function sameValue(a, b) {
1142
+ if (Array.isArray(a) && Array.isArray(b)) {
1143
+ return a.length === b.length && a.every((item, i) => item === b[i]);
1144
+ }
1145
+ return a === b;
1146
+ }
1147
+ function sameOptionValues(a, b) {
1148
+ const keys = Object.keys({ ...a, ...b });
1149
+ return keys.every((key) => sameValue(a[key], b[key]));
1150
+ }
1151
+ function optionsCacheSuffix(definition, values, context = {}) {
1152
+ const parts = [];
1153
+ for (const option of definition.options ?? []) {
1154
+ if (isClientApplied(option)) continue;
1155
+ const value = values[option.key];
1156
+ if (sameValue(value, defaultOptionValue(option, context))) continue;
1157
+ parts.push(
1158
+ `${option.key}=${encodeURIComponent(JSON.stringify(value ?? null))}`
1159
+ );
1160
+ }
1161
+ parts.sort();
1162
+ return parts.length === 0 ? "" : `?${parts.join("&")}`;
1163
+ }
1164
+ function tableColumnIndex(key) {
1165
+ const match = /^c(\d+)$/.exec(key);
1166
+ return match === null ? void 0 : Number(match[1]);
1167
+ }
1168
+ function sortBarItems(items, sort) {
1169
+ const sign = sort.direction === "desc" ? -1 : 1;
1170
+ return items.map((item, index) => ({ item, index })).sort((a, b) => {
1171
+ const order = sort.column === "value" ? a.item.value - b.item.value : a.item.label.localeCompare(b.item.label);
1172
+ return order * sign || a.index - b.index;
1173
+ }).map(({ item }) => item);
1174
+ }
1175
+ function applyClientOptions(definition, data, values) {
1176
+ const options = definition.options ?? [];
1177
+ const sortOption = options.find(
1178
+ (o) => o.type === "sort" && o.apply === "client"
1179
+ );
1180
+ const columnsOption = options.find(
1181
+ (o) => o.type === "columns" && o.apply === "client"
1182
+ );
1183
+ const sortValue = sortOption === void 0 ? void 0 : values[sortOption.key];
1184
+ const columnsValue = columnsOption === void 0 ? void 0 : values[columnsOption.key];
1185
+ let next = data;
1186
+ if (typeof sortValue === "string") {
1187
+ const sort = parseSortValue(sortValue);
1188
+ if (sort !== void 0 && next.kind === "BAR_LIST") {
1189
+ if (sort.column === "label" || sort.column === "value") {
1190
+ next = { ...next, items: sortBarItems(next.items, sort) };
1191
+ }
1192
+ } else if (sort !== void 0 && next.kind === "TABLE") {
1193
+ const index = tableColumnIndex(sort.column);
1194
+ if (index !== void 0 && index < next.columns.length) {
1195
+ const sorted = queryRows(tableDetailData(next), {
1196
+ page: 1,
1197
+ pageSize: Math.max(1, next.rows.length),
1198
+ sort
1199
+ });
1200
+ if (sorted.ok) next = { ...next, rows: sorted.value.rows };
1201
+ }
1202
+ }
1203
+ }
1204
+ if (Array.isArray(columnsValue) && next.kind === "TABLE") {
1205
+ const table = next;
1206
+ const indexes = columnsValue.map(tableColumnIndex).filter(
1207
+ (index) => index !== void 0 && index < table.columns.length
1208
+ );
1209
+ if (indexes.length > 0) {
1210
+ next = {
1211
+ ...table,
1212
+ columns: indexes.map((i) => table.columns[i]),
1213
+ rows: table.rows.map((row) => indexes.map((i) => row[i]))
1214
+ };
1215
+ }
1216
+ }
1217
+ return next;
849
1218
  }
850
1219
 
851
1220
  // src/core/validate.ts
@@ -1136,6 +1505,284 @@ function checkLockedOption(locked, problems) {
1136
1505
  optionalBoolean(locked, "hide", "locked", problems);
1137
1506
  optionalBoolean(locked, "minimize", "locked", problems);
1138
1507
  }
1508
+ function checkOptionColumns(option, path, kind, problems) {
1509
+ const columns = option["columns"];
1510
+ if (!Array.isArray(columns) || columns.length === 0) {
1511
+ problems.push(
1512
+ `${join(path, "columns")} must be a non-empty array of { key, label }, got ${describeType2(columns)}.`
1513
+ );
1514
+ return [];
1515
+ }
1516
+ const keys = [];
1517
+ columns.forEach((entry, index) => {
1518
+ const entryPath = `${path}.columns[${index}]`;
1519
+ if (!isRecord(entry)) {
1520
+ problems.push(
1521
+ `${entryPath} must be an object, got ${describeType2(entry)}.`
1522
+ );
1523
+ return;
1524
+ }
1525
+ requireString(entry, "label", entryPath, problems);
1526
+ const key = entry["key"];
1527
+ if (typeof key !== "string") {
1528
+ problems.push(
1529
+ `${entryPath}.key must be a string, got ${describeType2(key)}.`
1530
+ );
1531
+ } else if (typeof kind === "string" && !isColumnKeyFor(kind, key)) {
1532
+ problems.push(
1533
+ `${entryPath}.key "${key}" is not a column of a ${kind} widget (${kind === "TABLE" ? '"c0", "c1", ...' : '"label" or "value"'}).`
1534
+ );
1535
+ } else if (keys.includes(key)) {
1536
+ problems.push(`${entryPath}.key "${key}" is used twice.`);
1537
+ } else {
1538
+ keys.push(key);
1539
+ }
1540
+ });
1541
+ return keys;
1542
+ }
1543
+ function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
1544
+ if (!isRecord(option)) {
1545
+ problems.push(`${path} must be an object, got ${describeType2(option)}.`);
1546
+ return;
1547
+ }
1548
+ const type = option["type"];
1549
+ if (typeof type !== "string" || !WIDGET_OPTION_TYPES.includes(type)) {
1550
+ requireOneOf(option, "type", WIDGET_OPTION_TYPES, path, problems);
1551
+ return;
1552
+ }
1553
+ const key = option["key"];
1554
+ if (typeof key !== "string" || !OPTION_KEY_PATTERN.test(key)) {
1555
+ problems.push(
1556
+ `${path}.key must be letters, digits, "_" or "-", starting with a letter (40 characters at most), got ${typeof key === "string" ? JSON.stringify(key) : describeType2(key)}.`
1557
+ );
1558
+ } else if (seenKeys.has(key)) {
1559
+ problems.push(`${path}.key "${key}" is used by another option.`);
1560
+ } else {
1561
+ seenKeys.add(key);
1562
+ }
1563
+ requireString(option, "label", path, problems);
1564
+ const fallback = option["default"];
1565
+ switch (type) {
1566
+ case "choice": {
1567
+ const choices = option["choices"];
1568
+ const values = [];
1569
+ if (!Array.isArray(choices) || choices.length === 0) {
1570
+ problems.push(
1571
+ `${path}.choices must be a non-empty array of { value, label }, got ${describeType2(choices)}.`
1572
+ );
1573
+ } else {
1574
+ choices.forEach((choice, index) => {
1575
+ const choicePath = `${path}.choices[${index}]`;
1576
+ if (!isRecord(choice)) {
1577
+ problems.push(
1578
+ `${choicePath} must be an object, got ${describeType2(choice)}.`
1579
+ );
1580
+ return;
1581
+ }
1582
+ requireString(choice, "label", choicePath, problems);
1583
+ const value = choice["value"];
1584
+ if (typeof value !== "string" || value === "") {
1585
+ problems.push(
1586
+ `${choicePath}.value must be a non-empty string, got ${describeType2(value)}.`
1587
+ );
1588
+ } else if (values.includes(value)) {
1589
+ problems.push(`${choicePath}.value "${value}" is used twice.`);
1590
+ } else {
1591
+ values.push(value);
1592
+ }
1593
+ });
1594
+ }
1595
+ if (values.length > 0)
1596
+ requireOneOf(option, "default", values, path, problems);
1597
+ break;
1598
+ }
1599
+ case "number": {
1600
+ const { min, max, step } = option;
1601
+ requireNumber(option, "min", path, problems);
1602
+ requireNumber(option, "max", path, problems);
1603
+ if (typeof min === "number" && typeof max === "number" && min > max) {
1604
+ problems.push(
1605
+ `${path}.min (${min}) must not be more than max (${max}).`
1606
+ );
1607
+ }
1608
+ if (step !== void 0 && !(typeof step === "number" && Number.isFinite(step) && step > 0)) {
1609
+ problems.push(
1610
+ `${path}.step must be a number above 0, got ${describeType2(step)}.`
1611
+ );
1612
+ }
1613
+ if (typeof fallback !== "number" || !Number.isFinite(fallback)) {
1614
+ problems.push(
1615
+ `${path}.default must be a finite number, got ${describeType2(fallback)}.`
1616
+ );
1617
+ } else if (typeof min === "number" && typeof max === "number" && (fallback < min || fallback > max)) {
1618
+ problems.push(
1619
+ `${path}.default (${fallback}) must be between min (${min}) and max (${max}).`
1620
+ );
1621
+ } else if (typeof min === "number" && typeof step === "number" && step > 0 && Math.abs(
1622
+ (fallback - min) / step - Math.round((fallback - min) / step)
1623
+ ) > 1e-9) {
1624
+ problems.push(
1625
+ `${path}.default (${fallback}) must be ${min} plus a whole number of steps of ${step}.`
1626
+ );
1627
+ }
1628
+ break;
1629
+ }
1630
+ case "boolean":
1631
+ if (typeof fallback !== "boolean") {
1632
+ problems.push(
1633
+ `${path}.default must be a boolean, got ${describeType2(fallback)}.`
1634
+ );
1635
+ }
1636
+ break;
1637
+ case "text": {
1638
+ const maxLength = option["maxLength"];
1639
+ if (typeof maxLength !== "number" || !Number.isInteger(maxLength) || maxLength < 1 || maxLength > MAX_TEXT_OPTION_LENGTH) {
1640
+ problems.push(
1641
+ `${path}.maxLength must be a whole number from 1 to ${MAX_TEXT_OPTION_LENGTH}, got ${typeof maxLength === "number" ? maxLength : describeType2(maxLength)}.`
1642
+ );
1643
+ }
1644
+ if (typeof fallback !== "string") {
1645
+ problems.push(
1646
+ `${path}.default must be a string, got ${describeType2(fallback)}.`
1647
+ );
1648
+ } else if (typeof maxLength === "number" && fallback.length > maxLength) {
1649
+ problems.push(
1650
+ `${path}.default is ${fallback.length} characters; maxLength is ${maxLength}.`
1651
+ );
1652
+ }
1653
+ break;
1654
+ }
1655
+ case "dateRange": {
1656
+ const presets = option["presets"];
1657
+ const offered = [];
1658
+ if (presets !== void 0) {
1659
+ if (!Array.isArray(presets) || presets.length === 0) {
1660
+ problems.push(
1661
+ `${path}.presets must be a non-empty array of { value, label }, got ${describeType2(presets)}.`
1662
+ );
1663
+ } else {
1664
+ presets.forEach((preset, index) => {
1665
+ const presetPath = `${path}.presets[${index}]`;
1666
+ if (!isRecord(preset)) {
1667
+ problems.push(
1668
+ `${presetPath} must be an object, got ${describeType2(preset)}.`
1669
+ );
1670
+ return;
1671
+ }
1672
+ requireString(preset, "label", presetPath, problems);
1673
+ requireOneOf(
1674
+ preset,
1675
+ "value",
1676
+ DATE_RANGE_PRESETS,
1677
+ presetPath,
1678
+ problems
1679
+ );
1680
+ if (typeof preset["value"] === "string")
1681
+ offered.push(preset["value"]);
1682
+ });
1683
+ }
1684
+ }
1685
+ const allowed = offered.length > 0 ? offered : DATE_RANGE_PRESETS;
1686
+ if (typeof fallback !== "string" || !(allowed.includes(fallback) || isIsoInterval(fallback))) {
1687
+ problems.push(
1688
+ `${path}.default must be one of ${allowed.map((p) => `"${p}"`).join(", ")} or an interval like "2026-01-01/2026-01-31", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
1689
+ );
1690
+ }
1691
+ break;
1692
+ }
1693
+ case "color":
1694
+ if (typeof fallback !== "string" || !/^#[0-9a-fA-F]{6}$/.test(fallback)) {
1695
+ problems.push(
1696
+ `${path}.default must be a color like "#1c5cab", got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
1697
+ );
1698
+ }
1699
+ break;
1700
+ case "sort":
1701
+ case "columns": {
1702
+ const supported = type === "sort" ? kind === "TABLE" || kind === "BAR_LIST" : kind === "TABLE";
1703
+ if (!supported && typeof kind === "string") {
1704
+ problems.push(
1705
+ `${path} is a ${type} option, which only applies to ${type === "sort" ? "TABLE and BAR_LIST" : "TABLE"} widgets, but kind is "${kind}".`
1706
+ );
1707
+ }
1708
+ if (seenTypes.has(type)) {
1709
+ problems.push(
1710
+ `${path} is a second ${type} option; a widget has at most one.`
1711
+ );
1712
+ }
1713
+ seenTypes.add(type);
1714
+ const columnKeys = checkOptionColumns(
1715
+ option,
1716
+ path,
1717
+ supported ? kind : void 0,
1718
+ problems
1719
+ );
1720
+ if (option["apply"] !== void 0) {
1721
+ requireOneOf(option, "apply", ["provider", "client"], path, problems);
1722
+ }
1723
+ if (type === "sort") {
1724
+ if (fallback !== void 0) {
1725
+ const sort = typeof fallback === "string" ? parseSortValue(fallback) : void 0;
1726
+ if (sort === void 0) {
1727
+ problems.push(
1728
+ `${path}.default must look like "c1:desc" ("column:asc" or "column:desc"), got ${typeof fallback === "string" ? JSON.stringify(fallback) : describeType2(fallback)}.`
1729
+ );
1730
+ } else if (columnKeys.length > 0 && !columnKeys.includes(sort.column)) {
1731
+ requireOneOf(
1732
+ { default: sort.column },
1733
+ "default",
1734
+ columnKeys,
1735
+ path,
1736
+ problems
1737
+ );
1738
+ }
1739
+ }
1740
+ } else if (!Array.isArray(fallback) || fallback.length === 0 || fallback.some((k) => typeof k !== "string")) {
1741
+ problems.push(
1742
+ `${path}.default must be a non-empty array of column keys, got ${describeType2(fallback)}.`
1743
+ );
1744
+ } else {
1745
+ const shown = /* @__PURE__ */ new Set();
1746
+ for (const columnKey of fallback) {
1747
+ if (columnKeys.length > 0 && !columnKeys.includes(columnKey)) {
1748
+ problems.push(
1749
+ `${path}.default has "${columnKey}", which is not one of the columns (${columnKeys.map((c) => `"${c}"`).join(", ")}).`
1750
+ );
1751
+ } else if (shown.has(columnKey)) {
1752
+ problems.push(`${path}.default lists "${columnKey}" twice.`);
1753
+ }
1754
+ shown.add(columnKey);
1755
+ }
1756
+ }
1757
+ break;
1758
+ }
1759
+ }
1760
+ }
1761
+ function checkOptions(options, kind, problems) {
1762
+ if (options === void 0) return;
1763
+ if (!Array.isArray(options)) {
1764
+ problems.push(`options must be an array, got ${describeType2(options)}.`);
1765
+ return;
1766
+ }
1767
+ if (options.length > MAX_WIDGET_OPTIONS) {
1768
+ problems.push(
1769
+ `options lists ${options.length} options; the most a widget may declare is ${MAX_WIDGET_OPTIONS}.`
1770
+ );
1771
+ return;
1772
+ }
1773
+ const seenKeys = /* @__PURE__ */ new Set();
1774
+ const seenTypes = /* @__PURE__ */ new Set();
1775
+ options.forEach((option, index) => {
1776
+ checkOption(
1777
+ option,
1778
+ `options[${index}]`,
1779
+ kind,
1780
+ seenKeys,
1781
+ seenTypes,
1782
+ problems
1783
+ );
1784
+ });
1785
+ }
1139
1786
  function formatProblems(prefix, problems) {
1140
1787
  const shown = problems.slice(0, MAX_REPORTED_PROBLEMS);
1141
1788
  const more = problems.length - shown.length;
@@ -1210,6 +1857,14 @@ function validateWidgetDefinition(value) {
1210
1857
  checkDetailOption(value["detail"], problems);
1211
1858
  checkTableControlsOption(value["tableControls"], value["kind"], problems);
1212
1859
  checkLockedOption(value["locked"], problems);
1860
+ checkOptions(value["options"], value["kind"], problems);
1861
+ if (value["page"] !== void 0) {
1862
+ if (typeof value["page"] !== "string" || value["page"].trim() === "") {
1863
+ problems.push(
1864
+ `page must be a non-empty page key or title, got ${describeType2(value["page"])}.`
1865
+ );
1866
+ }
1867
+ }
1213
1868
  if (value["fill"] !== void 0) {
1214
1869
  requireOneOf(value, "fill", WIDGET_FILLS, "", problems);
1215
1870
  }
@@ -1246,12 +1901,40 @@ function failedWidget(definition, error) {
1246
1901
  function loadingWidgets(definitions) {
1247
1902
  return definitions.map((definition) => ({ definition, status: "loading" }));
1248
1903
  }
1249
- function cacheKeyFor(definition, options) {
1250
- return options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
1904
+ function cacheKeyFor(definition, options, values) {
1905
+ const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
1906
+ return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
1907
+ }
1908
+ function optionValuesFor(definition, options) {
1909
+ return resolveOptionValues(
1910
+ definition,
1911
+ options.optionValues?.[definition.key],
1912
+ options
1913
+ );
1914
+ }
1915
+ function finalizeWidget(widget, values) {
1916
+ if (Object.keys(values).length === 0) return widget;
1917
+ if (widget.status === "ok") {
1918
+ const data = applyClientOptions(
1919
+ widget.definition,
1920
+ widget.data,
1921
+ values
1922
+ );
1923
+ return { ...widget, data, options: values };
1924
+ }
1925
+ return { ...widget, options: values };
1251
1926
  }
1252
1927
  var LoadInterruption = class extends Error {
1253
1928
  };
1254
1929
  async function resolveWidget(definition, provider, context, options = {}) {
1930
+ const values = optionValuesFor(definition, options);
1931
+ return finalizeWidget(
1932
+ await resolveWidgetUnapplied(definition, provider, context, options),
1933
+ values
1934
+ );
1935
+ }
1936
+ async function resolveWidgetUnapplied(definition, provider, context, options = {}) {
1937
+ const values = optionValuesFor(definition, options);
1255
1938
  const quotedKey = JSON.stringify(definition.key);
1256
1939
  if (provider === void 0) {
1257
1940
  return failedWidget(
@@ -1286,7 +1969,10 @@ async function resolveWidget(definition, provider, context, options = {}) {
1286
1969
  payload = await Promise.race([
1287
1970
  Promise.resolve().then(() => {
1288
1971
  if (controller.signal.aborted) throw new LoadInterruption();
1289
- return provider(context, definition, { signal: controller.signal });
1972
+ return provider(context, definition, {
1973
+ signal: controller.signal,
1974
+ options: values
1975
+ });
1290
1976
  }),
1291
1977
  interrupted
1292
1978
  ]);
@@ -1310,7 +1996,7 @@ async function resolveWidget(definition, provider, context, options = {}) {
1310
1996
  const storedAt = Date.now();
1311
1997
  if (options.cache !== void 0) {
1312
1998
  try {
1313
- await options.cache.set(cacheKeyFor(definition, options), {
1999
+ await options.cache.set(cacheKeyFor(definition, options, values), {
1314
2000
  payload: resolved.data,
1315
2001
  storedAt
1316
2002
  });
@@ -1340,7 +2026,17 @@ async function resolveWidgets(definitions, providers, context, options = {}) {
1340
2026
 
1341
2027
  // src/core/loader.ts
1342
2028
  function createWidgetLoader(options) {
1343
- const { definitions, providers, context, ...resolveOptions } = options;
2029
+ const { definitions, providers, context, ...baseOptions } = options;
2030
+ const chosen = new Map(
2031
+ Object.entries(baseOptions.optionValues ?? {}).flatMap(
2032
+ ([key, values]) => values === void 0 ? [] : [[key, values]]
2033
+ )
2034
+ );
2035
+ const resolveOptions = baseOptions;
2036
+ function currentOptions() {
2037
+ return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
2038
+ }
2039
+ const unapplied = /* @__PURE__ */ new Map();
1344
2040
  const visible = widgetsFor(definitions, context.roles);
1345
2041
  let snapshot = loadingWidgets(visible);
1346
2042
  const listeners = /* @__PURE__ */ new Set();
@@ -1381,19 +2077,19 @@ function createWidgetLoader(options) {
1381
2077
  const controller = new AbortController();
1382
2078
  inFlight.set(key, controller);
1383
2079
  const isCurrent = () => !disposed && generations.get(key) === generation;
1384
- if (readCache && resolveOptions.cache !== void 0) {
2080
+ const runOptions = currentOptions();
2081
+ const values = optionValuesFor(definition, runOptions);
2082
+ if (readCache && runOptions.cache !== void 0) {
1385
2083
  try {
1386
- const entry = await resolveOptions.cache.get(
1387
- cacheKeyFor(definition, resolveOptions)
2084
+ const entry = await runOptions.cache.get(
2085
+ cacheKeyFor(definition, runOptions, values)
1388
2086
  );
1389
2087
  if (entry !== void 0 && isCurrent()) {
1390
- const cached = resolvePayload(
1391
- definition,
1392
- entry.payload,
1393
- resolveOptions
1394
- );
2088
+ const cached = resolvePayload(definition, entry.payload, runOptions);
1395
2089
  if (cached.status === "ok") {
1396
- replace(key, { ...cached, updatedAt: entry.storedAt, stale: true });
2090
+ const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
2091
+ unapplied.set(key, stale);
2092
+ replace(key, finalizeWidget(stale, values));
1397
2093
  }
1398
2094
  }
1399
2095
  } catch {
@@ -1401,13 +2097,15 @@ function createWidgetLoader(options) {
1401
2097
  }
1402
2098
  if (!isCurrent()) return;
1403
2099
  const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
1404
- const result = await resolveWidget(definition, provider, context, {
1405
- ...resolveOptions,
2100
+ const result = await resolveWidgetUnapplied(definition, provider, context, {
2101
+ ...runOptions,
1406
2102
  signal: controller.signal
1407
2103
  });
1408
2104
  if (!isCurrent()) return;
1409
2105
  inFlight.delete(key);
1410
- replace(key, result);
2106
+ if (result.status === "ok") unapplied.set(key, result);
2107
+ else unapplied.delete(key);
2108
+ replace(key, finalizeWidget(result, values));
1411
2109
  }
1412
2110
  function start(definition, readCache) {
1413
2111
  started.add(definition.key);
@@ -1442,6 +2140,29 @@ function createWidgetLoader(options) {
1442
2140
  start(definition, false);
1443
2141
  }
1444
2142
  }
2143
+ function setOptions(key, values) {
2144
+ if (disposed) return;
2145
+ const definition = definitionFor(key);
2146
+ if (definition === void 0) return;
2147
+ const before = optionValuesFor(definition, currentOptions());
2148
+ chosen.set(key, { ...values });
2149
+ const runOptions = currentOptions();
2150
+ const after = optionValuesFor(definition, runOptions);
2151
+ if (sameOptionValues(before, after) || !started.has(key)) return;
2152
+ const kept = unapplied.get(key);
2153
+ if (kept !== void 0 && optionsCacheSuffix(definition, before, runOptions) === optionsCacheSuffix(definition, after, runOptions)) {
2154
+ replace(key, finalizeWidget(kept, after));
2155
+ return;
2156
+ }
2157
+ const current = snapshot.find(
2158
+ (widget) => widget.definition.key === key
2159
+ );
2160
+ if (current?.status === "ok") replace(key, { ...current, stale: true });
2161
+ else if (current?.status === "error") {
2162
+ replace(key, { definition, status: "loading" });
2163
+ }
2164
+ start(definition, true);
2165
+ }
1445
2166
  function dispose() {
1446
2167
  disposed = true;
1447
2168
  abortAll(new Error("The widget loader was disposed."));
@@ -1459,10 +2180,480 @@ function createWidgetLoader(options) {
1459
2180
  load,
1460
2181
  loadAll,
1461
2182
  refresh,
2183
+ setOptions,
1462
2184
  dispose
1463
2185
  };
1464
2186
  }
1465
2187
 
2188
+ // src/core/pages.ts
2189
+ var DEFAULT_PAGE_KEY = "default";
2190
+ var DEFAULT_PAGE_ROWS = 4;
2191
+ function rowsAllowed(page, options) {
2192
+ const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
2193
+ return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
2194
+ }
2195
+ function hasPages(layout) {
2196
+ return layout.pages !== void 0 && layout.pages.length > 0;
2197
+ }
2198
+ function pageList(layout) {
2199
+ if (layout.pages !== void 0 && layout.pages.length > 0) {
2200
+ return layout.pages;
2201
+ }
2202
+ return [
2203
+ {
2204
+ key: DEFAULT_PAGE_KEY,
2205
+ title: "Page 1",
2206
+ order: layout.order,
2207
+ hidden: layout.hidden,
2208
+ minimized: layout.minimized
2209
+ }
2210
+ ];
2211
+ }
2212
+ function withExplicitPages(layout) {
2213
+ if (hasPages(layout)) return layout;
2214
+ return {
2215
+ ...layout,
2216
+ order: [],
2217
+ hidden: [],
2218
+ minimized: [],
2219
+ pages: pageList(layout)
2220
+ };
2221
+ }
2222
+ function sameKeys(a, b) {
2223
+ return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
2224
+ }
2225
+ function pageLayout(layout, pageKey) {
2226
+ if (!hasPages(layout))
2227
+ return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
2228
+ const page = layout.pages?.find(
2229
+ (p) => p.key === pageKey
2230
+ );
2231
+ if (page === void 0) return void 0;
2232
+ const { pages, ...rest } = layout;
2233
+ return {
2234
+ ...rest,
2235
+ order: page.order,
2236
+ hidden: page.hidden,
2237
+ minimized: page.minimized
2238
+ };
2239
+ }
2240
+ function withPageLayout(layout, pageKey, next) {
2241
+ const current = pageLayout(layout, pageKey);
2242
+ if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
2243
+ return layout;
2244
+ }
2245
+ if (!hasPages(layout)) {
2246
+ return {
2247
+ ...layout,
2248
+ order: next.order,
2249
+ hidden: next.hidden,
2250
+ minimized: next.minimized
2251
+ };
2252
+ }
2253
+ return {
2254
+ ...layout,
2255
+ pages: (layout.pages ?? []).map(
2256
+ (page) => page.key === pageKey ? {
2257
+ ...page,
2258
+ order: next.order,
2259
+ hidden: next.hidden,
2260
+ minimized: next.minimized
2261
+ } : page
2262
+ )
2263
+ };
2264
+ }
2265
+ function normalizedTitle(title) {
2266
+ return title.trim().normalize("NFKC").toLowerCase();
2267
+ }
2268
+ function resolvePageKey(layout, reference) {
2269
+ if (reference === void 0 || reference === "") return void 0;
2270
+ const pages = pageList(layout);
2271
+ const byKey = pages.find((p) => p.key === reference);
2272
+ if (byKey !== void 0) return byKey.key;
2273
+ const wanted = normalizedTitle(reference);
2274
+ return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
2275
+ }
2276
+ function homePageKey(layout, item) {
2277
+ return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
2278
+ }
2279
+ function assignPages(definitions, layout) {
2280
+ const pages = pageList(layout);
2281
+ const assigned = new Map(pages.map((p) => [p.key, []]));
2282
+ for (const definition of definitions) {
2283
+ let pageKey;
2284
+ if (!resolveWidgetLock(definition).move) {
2285
+ pageKey = pages.find(
2286
+ (p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
2287
+ )?.key;
2288
+ }
2289
+ assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
2290
+ }
2291
+ return assigned;
2292
+ }
2293
+ function pageOf(definitions, layout, key) {
2294
+ for (const [pageKey, widgets] of assignPages(definitions, layout)) {
2295
+ if (widgets.some((w) => w.key === key)) return pageKey;
2296
+ }
2297
+ return void 0;
2298
+ }
2299
+ function pageWidgets(definitions, layout, pageKey) {
2300
+ const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
2301
+ const view = pageLayout(layout, pageKey);
2302
+ return view === void 0 ? [] : visibleWidgets(onPage, view);
2303
+ }
2304
+ function pageRoom(definitions, layout, pageKey, options = {}) {
2305
+ const page = pageList(layout).find(
2306
+ (p) => p.key === pageKey
2307
+ );
2308
+ if (page === void 0) return void 0;
2309
+ const maxRows = rowsAllowed(page, options);
2310
+ const rowsUsed = placeRows(
2311
+ pageWidgets(definitions, layout, pageKey)
2312
+ ).length;
2313
+ return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
2314
+ }
2315
+ function describePage(page) {
2316
+ return `Page "${page.title}"`;
2317
+ }
2318
+ function titleOf(item) {
2319
+ return item.title ?? item.key;
2320
+ }
2321
+ function checkTitle(layout, title, exceptKey) {
2322
+ const trimmed = title.trim();
2323
+ if (trimmed === "") return err("A page title must not be empty.");
2324
+ if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
2325
+ return err(
2326
+ `Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
2327
+ );
2328
+ }
2329
+ const wanted = normalizedTitle(trimmed);
2330
+ const clash = pageList(layout).find(
2331
+ (p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
2332
+ );
2333
+ if (clash !== void 0) {
2334
+ return err(`Another page is already called "${clash.title}".`);
2335
+ }
2336
+ return ok(trimmed);
2337
+ }
2338
+ function nextPageKey(layout) {
2339
+ const keys = new Set(pageList(layout).map((p) => p.key));
2340
+ let n = 1;
2341
+ while (keys.has(`page-${n}`)) n += 1;
2342
+ return `page-${n}`;
2343
+ }
2344
+ function nextPageTitle(layout) {
2345
+ const used = new Set(
2346
+ pageList(layout).map((p) => normalizedTitle(p.title))
2347
+ );
2348
+ let n = pageList(layout).length + 1;
2349
+ while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
2350
+ return `Page ${n}`;
2351
+ }
2352
+ function addPage(layout, options = {}) {
2353
+ if (pageList(layout).length >= MAX_PARSED_PAGES) {
2354
+ return err(
2355
+ `The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
2356
+ );
2357
+ }
2358
+ const key = options.key ?? nextPageKey(layout);
2359
+ if (pageList(layout).some((p) => p.key === key)) {
2360
+ return err(`A page with the key "${key}" already exists.`);
2361
+ }
2362
+ const title = checkTitle(
2363
+ layout,
2364
+ options.title ?? nextPageTitle(layout)
2365
+ );
2366
+ if (!title.ok) return title;
2367
+ const base = withExplicitPages(layout);
2368
+ const page = {
2369
+ key,
2370
+ title: title.value,
2371
+ order: [],
2372
+ hidden: [],
2373
+ minimized: []
2374
+ };
2375
+ return ok({ ...base, pages: [...base.pages ?? [], page] });
2376
+ }
2377
+ function renamePage(layout, pageKey, title) {
2378
+ const page = pageList(layout).find(
2379
+ (p) => p.key === pageKey
2380
+ );
2381
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2382
+ const checked = checkTitle(layout, title, pageKey);
2383
+ if (!checked.ok) return checked;
2384
+ if (checked.value === page.title) return ok(layout);
2385
+ const base = withExplicitPages(layout);
2386
+ return ok({
2387
+ ...base,
2388
+ pages: (base.pages ?? []).map(
2389
+ (p) => p.key === pageKey ? { ...p, title: checked.value } : p
2390
+ )
2391
+ });
2392
+ }
2393
+ function movePage(layout, pageKey, offset) {
2394
+ const pages = layout.pages ?? [];
2395
+ const from = pages.findIndex((p) => p.key === pageKey);
2396
+ if (from === -1) return layout;
2397
+ const to = Math.max(
2398
+ 0,
2399
+ Math.min(pages.length - 1, from + Math.trunc(offset))
2400
+ );
2401
+ if (to === from) return layout;
2402
+ const next = [...pages];
2403
+ const [moved] = next.splice(from, 1);
2404
+ next.splice(to, 0, moved);
2405
+ return { ...layout, pages: next };
2406
+ }
2407
+ function overflowing(definitions, layout, options) {
2408
+ return pageList(layout).find(
2409
+ (page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
2410
+ );
2411
+ }
2412
+ function removePage(definitions, layout, pageKey, options = {}) {
2413
+ const pages = pageList(layout);
2414
+ const index = pages.findIndex((p) => p.key === pageKey);
2415
+ const page = pages[index];
2416
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2417
+ if (pages.length === 1) {
2418
+ return err(
2419
+ `${describePage(page)} is the only page, so it can't be removed.`
2420
+ );
2421
+ }
2422
+ const target = pages[index > 0 ? index - 1 : 1];
2423
+ const merged = {
2424
+ ...layout,
2425
+ pages: pages.filter((p) => p.key !== pageKey).map(
2426
+ (p) => p.key === target.key ? {
2427
+ ...p,
2428
+ order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
2429
+ hidden: [...p.hidden, ...page.hidden],
2430
+ minimized: [...p.minimized, ...page.minimized]
2431
+ } : p
2432
+ )
2433
+ };
2434
+ const full = overflowing(
2435
+ definitions,
2436
+ merged,
2437
+ options
2438
+ );
2439
+ if (full !== void 0) {
2440
+ return err(
2441
+ `${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.`
2442
+ );
2443
+ }
2444
+ return ok(merged);
2445
+ }
2446
+ function without(keys, key) {
2447
+ return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
2448
+ }
2449
+ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
2450
+ const widget = definitions.find((d) => d.key === widgetKey);
2451
+ if (widget === void 0) {
2452
+ return err(
2453
+ `Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
2454
+ );
2455
+ }
2456
+ const pages = pageList(layout);
2457
+ const target = pages.find((p) => p.key === targetKey);
2458
+ if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
2459
+ const sourceKey = pageOf(definitions, layout, widgetKey);
2460
+ if (sourceKey === targetKey) return ok(layout);
2461
+ if (resolveWidgetLock(widget).move) {
2462
+ return err(
2463
+ `Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
2464
+ );
2465
+ }
2466
+ const base = withExplicitPages(layout);
2467
+ const source = (base.pages ?? []).find(
2468
+ (p) => p.key === sourceKey
2469
+ );
2470
+ const hidden = source.hidden.includes(widgetKey);
2471
+ const minimized = source.minimized.includes(widgetKey);
2472
+ const targetOrder = pageWidgets(
2473
+ definitions,
2474
+ base,
2475
+ targetKey
2476
+ ).map((d) => d.key);
2477
+ const moved = {
2478
+ ...base,
2479
+ pages: (base.pages ?? []).map((p) => {
2480
+ if (p.key === sourceKey) {
2481
+ return {
2482
+ ...p,
2483
+ order: without(p.order, widgetKey),
2484
+ hidden: without(p.hidden, widgetKey),
2485
+ minimized: without(p.minimized, widgetKey)
2486
+ };
2487
+ }
2488
+ if (p.key === targetKey) {
2489
+ return {
2490
+ ...p,
2491
+ order: hidden ? p.order : [...targetOrder, widgetKey],
2492
+ hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
2493
+ minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
2494
+ };
2495
+ }
2496
+ return p;
2497
+ })
2498
+ };
2499
+ if (!hidden) {
2500
+ const allowed = rowsAllowed(target, options);
2501
+ const needed = placeRows(
2502
+ pageWidgets(definitions, moved, targetKey)
2503
+ ).length;
2504
+ if (needed > allowed) {
2505
+ return err(
2506
+ `${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
2507
+ );
2508
+ }
2509
+ }
2510
+ return ok(moved);
2511
+ }
2512
+ function withoutLockField(definition) {
2513
+ if (definition.locked === void 0) return definition;
2514
+ const copy = { ...definition };
2515
+ delete copy.locked;
2516
+ return copy;
2517
+ }
2518
+ function uniqueTitle(taken, title, index) {
2519
+ let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
2520
+ if (base === "") base = `Page ${index + 1}`;
2521
+ let candidate = base;
2522
+ let n = 1;
2523
+ while (taken.has(normalizedTitle(candidate))) {
2524
+ const suffix = ` (#${n})`;
2525
+ candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
2526
+ n += 1;
2527
+ }
2528
+ taken.add(normalizedTitle(candidate));
2529
+ return candidate;
2530
+ }
2531
+ function samePages(a, b) {
2532
+ return a.length === b.length && a.every((p, i) => {
2533
+ const q = b[i];
2534
+ 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);
2535
+ });
2536
+ }
2537
+ function normalizeLayout(definitions, layout, options = {}) {
2538
+ const known = pruneLayout(definitions, layout);
2539
+ const locked = enforceLocks(definitions, known, options);
2540
+ if (!hasPages(locked)) return locked === layout ? layout : locked;
2541
+ const defs = options.overrideLocks === true ? definitions.map(withoutLockField) : definitions;
2542
+ const taken = /* @__PURE__ */ new Set();
2543
+ const seenKeys = /* @__PURE__ */ new Set();
2544
+ const held = /* @__PURE__ */ new Set();
2545
+ let pages = [];
2546
+ (locked.pages ?? []).forEach((page, index) => {
2547
+ if (seenKeys.has(page.key)) return;
2548
+ seenKeys.add(page.key);
2549
+ const keep = (list) => [
2550
+ ...new Set(list.filter((k) => !held.has(k)))
2551
+ ];
2552
+ const next = {
2553
+ ...page,
2554
+ title: uniqueTitle(taken, page.title, index),
2555
+ order: keep(page.order),
2556
+ hidden: keep(page.hidden),
2557
+ minimized: keep(page.minimized)
2558
+ };
2559
+ for (const k of [...next.order, ...next.hidden]) held.add(k);
2560
+ pages.push(next);
2561
+ });
2562
+ const work = () => ({ ...locked, pages });
2563
+ const pinned = defs.filter((d) => resolveWidgetLock(d).move);
2564
+ for (const widget of pinned) {
2565
+ const home = homePageKey(work(), widget);
2566
+ const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
2567
+ const wasHidden = pages.some((p) => keysHere(p, "hidden"));
2568
+ const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
2569
+ if (!wasHidden && !wasMinimized) continue;
2570
+ pages = pages.map((p) => {
2571
+ let next = p;
2572
+ if (keysHere(p, "hidden")) {
2573
+ next = { ...next, hidden: without(next.hidden, widget.key) };
2574
+ }
2575
+ if (keysHere(p, "minimized")) {
2576
+ next = { ...next, minimized: without(next.minimized, widget.key) };
2577
+ }
2578
+ if (p.key === home) {
2579
+ next = {
2580
+ ...next,
2581
+ hidden: wasHidden && !next.hidden.includes(widget.key) ? [...next.hidden, widget.key] : next.hidden,
2582
+ minimized: wasMinimized && !next.minimized.includes(widget.key) ? [...next.minimized, widget.key] : next.minimized
2583
+ };
2584
+ }
2585
+ return next;
2586
+ });
2587
+ }
2588
+ const placedKeys = /* @__PURE__ */ new Set();
2589
+ for (const p of pages) {
2590
+ for (const k of [...p.order, ...p.hidden]) placedKeys.add(k);
2591
+ }
2592
+ const pinnedKeys = new Set(pinned.map((d) => d.key));
2593
+ const unplaced = defs.filter((d) => !placedKeys.has(d.key) && !pinnedKeys.has(d.key)).map((definition, index) => ({ definition, index })).sort(
2594
+ (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
2595
+ ).map(({ definition }) => definition);
2596
+ for (const widget of unplaced) {
2597
+ const home = homePageKey(work(), widget);
2598
+ pages = pages.map(
2599
+ (p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
2600
+ );
2601
+ }
2602
+ for (let i = 0; i < pages.length; i += 1) {
2603
+ const page = pages[i];
2604
+ const shown = pageWidgets(defs, work(), page.key);
2605
+ const fits = fitCount(shown, rowsAllowed(page, options));
2606
+ if (fits >= shown.length) continue;
2607
+ const moving = shown.slice(fits).filter((d) => !pinnedKeys.has(d.key)).map((d) => d.key);
2608
+ if (moving.length === 0) continue;
2609
+ const moveSet = new Set(moving);
2610
+ const movedMinimized = page.minimized.filter(
2611
+ (k) => moveSet.has(k)
2612
+ );
2613
+ pages = pages.map(
2614
+ (p) => p.key === page.key ? {
2615
+ ...p,
2616
+ order: p.order.filter((k) => !moveSet.has(k)),
2617
+ minimized: p.minimized.filter((k) => !moveSet.has(k))
2618
+ } : p
2619
+ );
2620
+ let placed = false;
2621
+ for (let j = i + 1; j < pages.length && !placed; j += 1) {
2622
+ const candidate = pages[j];
2623
+ const trial = pages.map(
2624
+ (p) => p.key === candidate.key ? {
2625
+ ...p,
2626
+ order: [...moving, ...p.order],
2627
+ minimized: [...p.minimized, ...movedMinimized]
2628
+ } : p
2629
+ );
2630
+ const items = pageWidgets(
2631
+ defs,
2632
+ { ...locked, pages: trial },
2633
+ candidate.key
2634
+ );
2635
+ if (placeRows(items).length <= rowsAllowed(candidate, options)) {
2636
+ pages = trial;
2637
+ placed = true;
2638
+ }
2639
+ }
2640
+ if (!placed) {
2641
+ const key = nextPageKey(work());
2642
+ const title = uniqueTitle(
2643
+ new Set(pages.map((p) => normalizedTitle(p.title))),
2644
+ `Page ${pages.length + 1}`,
2645
+ pages.length
2646
+ );
2647
+ pages = [
2648
+ ...pages,
2649
+ { key, title, order: moving, hidden: [], minimized: movedMinimized }
2650
+ ];
2651
+ }
2652
+ }
2653
+ const result = work();
2654
+ return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) ? layout : result;
2655
+ }
2656
+
1466
2657
  // src/core/scale.ts
1467
2658
  function niceStep(value) {
1468
2659
  if (!(value > 0) || !Number.isFinite(value)) return 1;
@@ -1493,6 +2684,556 @@ function labelStride(count, maxLabels = 6) {
1493
2684
  return Math.max(1, Math.ceil(count / Math.max(1, maxLabels)));
1494
2685
  }
1495
2686
 
1496
- export { DEFAULT_COMPARISON_LABEL, DEFAULT_CURRENCY, DEFAULT_DETAIL_PAGE_SIZE, DEFAULT_EMPTY_TEXT, DETAIL_MODES, EMPTY_LAYOUT, MAX_DETAIL_PAGE_SIZE, MAX_GRAPH_SERIES, MAX_REPORTED_PROBLEMS, MAX_WIDGET_WIDTH, MIN_WIDGET_WIDTH, VALUE_FORMATS, WIDGET_FILLS, WIDGET_KINDS, WIDGET_SIZES, WIDTH_COLUMNS, baseColumnSpan, cacheKeyFor, createWidgetLoader, defaultDetailQuery, defineWidget, deriveDetailData, describeUnknownError, emptyTextFor, emptyWidget, enforceLocks, err, failedWidget, fillColumnSpans, fillWidthSpans, fillsHeight, fillsWidth, formatCount, formatPercentChange, formatValue, fromLegacyDefinition, hiddenWidgets, hideWidget, isHidden, isMinimized, isSafeHref, isSafeImageUrl, isWidgetEmptyState, isWidgetKind, isWidgetVisibleTo, isWidgetWidth, itemWidth, kpiDelta, labelStride, loadingWidgets, minimizeWidget, moveWidget, moveWidgetBy, moveWidgetDown, moveWidgetUp, niceDomain, niceStep, ok, parseDetailQuery, parseLayout, percentChange, pruneLayout, queryRows, resolveDetailOptions, resolvePayload, resolveTableControls, resolveWidget, resolveWidgetLock, resolveWidgets, restoreWidget, serializeDetailQuery, serializeLayout, shareOf, showWidget, sortDefinitions, tableDetailData, toggleHidden, toggleMinimized, upgradeLegacyWidgetData, usesWidthColumns, validateDetailData, validateWidgetData, validateWidgetDefinition, visibleWidgets, widgetsFor, widthForSize };
2687
+ // src/core/storage.ts
2688
+ function failure(code, error) {
2689
+ return { ok: false, code, error };
2690
+ }
2691
+ function utf8Length(text) {
2692
+ let bytes = 0;
2693
+ for (const char of text) {
2694
+ const point = char.codePointAt(0) ?? 0;
2695
+ bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
2696
+ }
2697
+ return bytes;
2698
+ }
2699
+ function memoryAdapter(initial = {}) {
2700
+ const entries = /* @__PURE__ */ new Map();
2701
+ const listeners = /* @__PURE__ */ new Map();
2702
+ let counter = 0;
2703
+ for (const [key, value] of Object.entries(initial)) {
2704
+ counter += 1;
2705
+ entries.set(key, { value, revision: String(counter) });
2706
+ }
2707
+ const notify = (key, stored) => {
2708
+ for (const listener of listeners.get(key) ?? []) listener(stored);
2709
+ };
2710
+ return {
2711
+ get: (key) => Promise.resolve({ ok: true, value: entries.get(key) ?? null }),
2712
+ set: (key, value, options) => {
2713
+ const current = entries.get(key);
2714
+ if (options.ifRevision !== void 0 && (current?.revision ?? void 0) !== options.ifRevision) {
2715
+ return Promise.resolve(
2716
+ failure(
2717
+ "conflict",
2718
+ `Could not save "${key}": it changed since revision ${options.ifRevision} (now ${current?.revision ?? "none"}).`
2719
+ )
2720
+ );
2721
+ }
2722
+ counter += 1;
2723
+ const stored = { value, revision: String(counter) };
2724
+ entries.set(key, stored);
2725
+ notify(key, stored);
2726
+ return Promise.resolve({ ok: true, value: stored });
2727
+ },
2728
+ remove: (key) => {
2729
+ entries.delete(key);
2730
+ notify(key, null);
2731
+ return Promise.resolve({ ok: true, value: null });
2732
+ },
2733
+ subscribe: (key, onChange) => {
2734
+ const set = listeners.get(key) ?? /* @__PURE__ */ new Set();
2735
+ set.add(onChange);
2736
+ listeners.set(key, set);
2737
+ return () => {
2738
+ set.delete(onChange);
2739
+ };
2740
+ }
2741
+ };
2742
+ }
2743
+ function withPrefix(adapter, prefix) {
2744
+ return {
2745
+ get: (key, options) => adapter.get(prefix + key, options),
2746
+ set: (key, value, options) => adapter.set(prefix + key, value, options),
2747
+ ...adapter.remove === void 0 ? {} : {
2748
+ remove: (key, options) => adapter.remove?.(prefix + key, options) ?? Promise.resolve(failure("invalid", "remove is not supported."))
2749
+ },
2750
+ ...adapter.subscribe === void 0 ? {} : {
2751
+ subscribe: (key, onChange) => adapter.subscribe?.(prefix + key, onChange) ?? (() => void 0)
2752
+ }
2753
+ };
2754
+ }
2755
+ function withFallback(primary, secondary) {
2756
+ return {
2757
+ async get(key, options) {
2758
+ const first = await primary.get(key, options);
2759
+ if (first.ok && first.value !== null) return first;
2760
+ const second = await secondary.get(key, options);
2761
+ if (second.ok && second.value !== null) return second;
2762
+ return first;
2763
+ },
2764
+ async set(key, value, options) {
2765
+ const first = await primary.set(key, value, options);
2766
+ if (!first.ok && first.code === "conflict") return first;
2767
+ const second = await secondary.set(key, value, {
2768
+ signal: options.signal
2769
+ });
2770
+ if (first.ok) return first;
2771
+ return second.ok ? second : first;
2772
+ },
2773
+ async remove(key, options) {
2774
+ const first = await primary.remove?.(key, options) ?? {
2775
+ ok: true,
2776
+ value: null
2777
+ };
2778
+ await secondary.remove?.(key, options);
2779
+ return first;
2780
+ },
2781
+ subscribe: (key, onChange) => {
2782
+ const stops = [primary, secondary].flatMap((adapter) => {
2783
+ const stop = adapter.subscribe?.(
2784
+ key,
2785
+ onChange
2786
+ );
2787
+ return stop === void 0 ? [] : [stop];
2788
+ });
2789
+ return () => {
2790
+ for (const stop of stops) stop();
2791
+ };
2792
+ }
2793
+ };
2794
+ }
2795
+ function withReadCache(adapter) {
2796
+ const cache = /* @__PURE__ */ new Map();
2797
+ return {
2798
+ async get(key, options) {
2799
+ const cached = cache.get(key);
2800
+ if (cached !== void 0) return { ok: true, value: cached };
2801
+ const result = await adapter.get(key, options);
2802
+ if (result.ok) cache.set(key, result.value);
2803
+ return result;
2804
+ },
2805
+ async set(key, value, options) {
2806
+ const result = await adapter.set(key, value, options);
2807
+ if (result.ok) cache.set(key, result.value);
2808
+ else cache.delete(key);
2809
+ return result;
2810
+ },
2811
+ async remove(key, options) {
2812
+ cache.delete(key);
2813
+ return await adapter.remove?.(key, options) ?? failure(
2814
+ "invalid",
2815
+ `Could not remove "${key}": remove is not supported.`
2816
+ );
2817
+ },
2818
+ ...adapter.subscribe === void 0 ? {} : {
2819
+ subscribe: (key, onChange) => adapter.subscribe?.(key, (stored) => {
2820
+ cache.set(key, stored);
2821
+ onChange(stored);
2822
+ }) ?? (() => void 0)
2823
+ }
2824
+ };
2825
+ }
2826
+ function readOnly(adapter) {
2827
+ return {
2828
+ get: (key, options) => adapter.get(key, options),
2829
+ set: (key) => Promise.resolve(
2830
+ failure(
2831
+ "forbidden",
2832
+ `Could not save "${key}": this store is read-only.`
2833
+ )
2834
+ ),
2835
+ remove: (key) => Promise.resolve(
2836
+ failure(
2837
+ "forbidden",
2838
+ `Could not remove "${key}": this store is read-only.`
2839
+ )
2840
+ ),
2841
+ ...adapter.subscribe === void 0 ? {} : { subscribe: adapter.subscribe.bind(adapter) }
2842
+ };
2843
+ }
2844
+ function withRetry(adapter, options) {
2845
+ const attempts = Math.max(1, Math.floor(options.attempts ?? 3));
2846
+ const baseDelay = options.delayMs ?? 200;
2847
+ const retrying = async (signal, run) => {
2848
+ let result = await run();
2849
+ for (let attempt = 1; attempt < attempts && !result.ok && result.code === "unavailable" && !signal.aborted; attempt += 1) {
2850
+ await options.sleep(baseDelay * 2 ** (attempt - 1), signal);
2851
+ result = await run();
2852
+ }
2853
+ return result;
2854
+ };
2855
+ return {
2856
+ get: (key, o) => retrying(o.signal, () => adapter.get(key, o)),
2857
+ set: (key, value, o) => retrying(o.signal, () => adapter.set(key, value, o)),
2858
+ ...adapter.remove === void 0 ? {} : {
2859
+ remove: (key, o) => retrying(
2860
+ o.signal,
2861
+ () => adapter.remove?.(key, o) ?? Promise.resolve(failure("invalid", "remove is not supported."))
2862
+ )
2863
+ },
2864
+ ...adapter.subscribe === void 0 ? {} : { subscribe: adapter.subscribe.bind(adapter) }
2865
+ };
2866
+ }
2867
+ function withEncoding(adapter, codec) {
2868
+ const decodeStored = async (key, stored) => {
2869
+ try {
2870
+ const value = await codec.decode(stored.value);
2871
+ return {
2872
+ ok: true,
2873
+ value: stored.revision === void 0 ? { value } : { value, revision: stored.revision }
2874
+ };
2875
+ } catch (cause) {
2876
+ return failure(
2877
+ "corrupt",
2878
+ `Could not decode the value stored at "${key}": ${describeUnknownError(cause)}`
2879
+ );
2880
+ }
2881
+ };
2882
+ return {
2883
+ async get(key, options) {
2884
+ const result = await adapter.get(key, options);
2885
+ if (!result.ok || result.value === null) return result;
2886
+ return decodeStored(key, result.value);
2887
+ },
2888
+ async set(key, value, options) {
2889
+ let encoded;
2890
+ try {
2891
+ encoded = await codec.encode(value);
2892
+ } catch (cause) {
2893
+ return failure(
2894
+ "invalid",
2895
+ `Could not encode the value for "${key}": ${describeUnknownError(cause)}`
2896
+ );
2897
+ }
2898
+ const result = await adapter.set(key, encoded, options);
2899
+ if (!result.ok) return result;
2900
+ return result.value.revision === void 0 ? { ok: true, value: { value } } : { ok: true, value: { value, revision: result.value.revision } };
2901
+ },
2902
+ ...adapter.remove === void 0 ? {} : { remove: adapter.remove.bind(adapter) },
2903
+ ...adapter.subscribe === void 0 ? {} : {
2904
+ subscribe: (key, onChange) => adapter.subscribe?.(key, (stored) => {
2905
+ if (stored === null) {
2906
+ onChange(null);
2907
+ return;
2908
+ }
2909
+ void decodeStored(key, stored).then((decoded) => {
2910
+ if (decoded.ok) onChange(decoded.value);
2911
+ });
2912
+ }) ?? (() => void 0)
2913
+ }
2914
+ };
2915
+ }
2916
+ function withLogging(adapter, log) {
2917
+ const report = (operation, key, result) => {
2918
+ try {
2919
+ log(
2920
+ result.ok ? { operation, key, ok: true } : { operation, key, ok: false, code: result.code }
2921
+ );
2922
+ } catch {
2923
+ }
2924
+ return result;
2925
+ };
2926
+ return {
2927
+ get: async (key, o) => report("get", key, await adapter.get(key, o)),
2928
+ set: async (key, value, o) => report("set", key, await adapter.set(key, value, o)),
2929
+ ...adapter.remove === void 0 ? {} : {
2930
+ remove: async (key, o) => report(
2931
+ "remove",
2932
+ key,
2933
+ await adapter.remove?.(key, o) ?? failure("invalid", "remove is not supported.")
2934
+ )
2935
+ },
2936
+ ...adapter.subscribe === void 0 ? {} : { subscribe: adapter.subscribe.bind(adapter) }
2937
+ };
2938
+ }
2939
+ var LAYOUT_ENVELOPE_VERSION = 1;
2940
+ var DEFAULT_MAX_LAYOUT_BYTES = 64 * 1024;
2941
+ function keyPart(text) {
2942
+ return encodeURIComponent(text);
2943
+ }
2944
+ function readEnvelope(key, stored) {
2945
+ let parsed;
2946
+ try {
2947
+ parsed = JSON.parse(stored.value);
2948
+ } catch (cause) {
2949
+ return failure(
2950
+ "corrupt",
2951
+ `The layout stored at "${key}" is not valid JSON: ${describeUnknownError(cause)}`
2952
+ );
2953
+ }
2954
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2955
+ return failure(
2956
+ "corrupt",
2957
+ `The layout stored at "${key}" is not an object.`
2958
+ );
2959
+ }
2960
+ const record = parsed;
2961
+ const version = record["v"];
2962
+ if (version === void 0) {
2963
+ return { ok: true, value: { layout: parseLayout(parsed) } };
2964
+ }
2965
+ if (typeof version !== "number" || !Number.isInteger(version)) {
2966
+ return failure(
2967
+ "corrupt",
2968
+ `The layout stored at "${key}" has an invalid version ${JSON.stringify(version)}.`
2969
+ );
2970
+ }
2971
+ if (version > LAYOUT_ENVELOPE_VERSION) {
2972
+ return failure(
2973
+ "invalid",
2974
+ `The layout stored at "${key}" is version ${version}, newer than this toolkit reads (${LAYOUT_ENVELOPE_VERSION}). Update the package.`
2975
+ );
2976
+ }
2977
+ const envelope = record;
2978
+ return {
2979
+ ok: true,
2980
+ value: {
2981
+ layout: parseLayout(envelope.layout),
2982
+ ...typeof envelope.savedAt === "string" ? { savedAt: envelope.savedAt } : {}
2983
+ }
2984
+ };
2985
+ }
2986
+ function createLayoutPersistence(adapter, options = {}) {
2987
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_LAYOUT_BYTES;
2988
+ const policy = options.onConflict ?? "ask";
2989
+ const fallbackSignal = new AbortController().signal;
2990
+ const signal = options.signal ?? fallbackSignal;
2991
+ const now = options.now ?? Date.now;
2992
+ const revisions = /* @__PURE__ */ new Map();
2993
+ const lastWritten = /* @__PURE__ */ new Map();
2994
+ const inFlight = /* @__PURE__ */ new Set();
2995
+ const pending = /* @__PURE__ */ new Map();
2996
+ const keyFor = (scope, part = "layout") => `dwt:${keyPart(scope.dashboardKey)}:${keyPart(scope.userKey ?? "_")}:${part}`;
2997
+ const checkScope = (scope) => scope.dashboardKey.trim() === "" ? failure("invalid", "The storage scope needs a non-empty dashboardKey.") : { ok: true, value: null };
2998
+ const encode = (layout, key) => {
2999
+ const envelope = {
3000
+ v: LAYOUT_ENVELOPE_VERSION,
3001
+ savedAt: new Date(now()).toISOString(),
3002
+ layout: JSON.parse(serializeLayout(layout))
3003
+ };
3004
+ const text = JSON.stringify(envelope);
3005
+ const size = utf8Length(text);
3006
+ if (size > maxBytes) {
3007
+ return failure(
3008
+ "invalid",
3009
+ `Could not save "${key}": the layout is ${size} bytes, over the ${maxBytes} byte limit.`
3010
+ );
3011
+ }
3012
+ return { ok: true, value: text };
3013
+ };
3014
+ const guarded = async (key, action, run) => {
3015
+ try {
3016
+ return await run();
3017
+ } catch (cause) {
3018
+ return failure(
3019
+ "unavailable",
3020
+ `Could not ${action} "${key}": the adapter threw ${describeUnknownError(cause)}`
3021
+ );
3022
+ }
3023
+ };
3024
+ const readLayout = async (scope, part) => {
3025
+ const key = keyFor(scope, part);
3026
+ const result = await guarded(
3027
+ key,
3028
+ "load",
3029
+ () => adapter.get(key, { signal })
3030
+ );
3031
+ if (!result.ok) return result;
3032
+ if (result.value === null) {
3033
+ if (part === "layout") revisions.delete(key);
3034
+ return { ok: true, value: { layout: null } };
3035
+ }
3036
+ const read = readEnvelope(key, result.value);
3037
+ if (!read.ok) return read;
3038
+ if (part === "layout" && result.value.revision !== void 0) {
3039
+ revisions.set(key, result.value.revision);
3040
+ }
3041
+ return {
3042
+ ok: true,
3043
+ value: {
3044
+ layout: read.value.layout,
3045
+ ...result.value.revision === void 0 ? {} : { revision: result.value.revision },
3046
+ ...read.value.savedAt === void 0 ? {} : { savedAt: read.value.savedAt }
3047
+ }
3048
+ };
3049
+ };
3050
+ const repair = (loaded, definitions, normalizeOptions) => loaded.layout === null ? loaded : {
3051
+ ...loaded,
3052
+ layout: normalizeLayout(definitions, loaded.layout, normalizeOptions)
3053
+ };
3054
+ const write = async (scope, layout, force) => {
3055
+ const key = keyFor(scope);
3056
+ const text = encode(layout, key);
3057
+ if (!text.ok) return text;
3058
+ const known = revisions.get(key);
3059
+ lastWritten.set(key, text.value);
3060
+ const stored = await guarded(
3061
+ key,
3062
+ "save",
3063
+ () => adapter.set(key, text.value, {
3064
+ signal,
3065
+ ...force || known === void 0 ? {} : { ifRevision: known }
3066
+ })
3067
+ );
3068
+ if (stored.ok) {
3069
+ if (stored.value.revision !== void 0) {
3070
+ revisions.set(key, stored.value.revision);
3071
+ }
3072
+ return {
3073
+ ok: true,
3074
+ value: {
3075
+ outcome: "saved",
3076
+ layout,
3077
+ ...stored.value.revision === void 0 ? {} : { revision: stored.value.revision }
3078
+ }
3079
+ };
3080
+ }
3081
+ if (stored.code !== "conflict") return stored;
3082
+ const theirs = await readLayout(scope, "layout");
3083
+ if (!theirs.ok) return theirs;
3084
+ const theirLayout = theirs.value.layout ?? layout;
3085
+ const theirRevision = theirs.value.revision;
3086
+ const asResult = (outcome) => ({
3087
+ ok: true,
3088
+ value: {
3089
+ outcome,
3090
+ layout: theirLayout,
3091
+ ...theirRevision === void 0 ? {} : { revision: theirRevision }
3092
+ }
3093
+ });
3094
+ if (policy === "overwrite") return write(scope, layout, true);
3095
+ if (policy === "keep-theirs") return asResult("kept-theirs");
3096
+ if (policy === "ask") return asResult("conflict");
3097
+ let merged;
3098
+ try {
3099
+ merged = policy(layout, theirLayout);
3100
+ } catch (cause) {
3101
+ return failure(
3102
+ "invalid",
3103
+ `Could not save "${key}": the conflict merge function threw ${describeUnknownError(cause)}`
3104
+ );
3105
+ }
3106
+ const second = await write(scope, merged, false);
3107
+ if (second.ok && second.value.outcome === "saved") return second;
3108
+ return second.ok ? asResult("conflict") : second;
3109
+ };
3110
+ const drain = async (scope, key, first) => {
3111
+ let job = first;
3112
+ inFlight.add(key);
3113
+ while (job !== void 0) {
3114
+ const current = job;
3115
+ pending.delete(key);
3116
+ const result = await write(
3117
+ scope,
3118
+ current.layout,
3119
+ current.force
3120
+ );
3121
+ for (const waiter of current.waiters) waiter(result);
3122
+ job = pending.get(key);
3123
+ }
3124
+ inFlight.delete(key);
3125
+ };
3126
+ return {
3127
+ keyFor,
3128
+ async load(scope, definitions, normalizeOptions) {
3129
+ const valid = checkScope(scope);
3130
+ if (!valid.ok) return valid;
3131
+ const read = await readLayout(scope, "layout");
3132
+ if (!read.ok) return read;
3133
+ return {
3134
+ ok: true,
3135
+ value: repair(read.value, definitions, normalizeOptions)
3136
+ };
3137
+ },
3138
+ save(scope, layout, saveOptions = {}) {
3139
+ const valid = checkScope(scope);
3140
+ if (!valid.ok) return Promise.resolve(valid);
3141
+ const key = keyFor(scope);
3142
+ const force = saveOptions.force === true;
3143
+ return new Promise((resolve) => {
3144
+ const queued = pending.get(key);
3145
+ if (queued !== void 0) {
3146
+ queued.layout = layout;
3147
+ queued.force = queued.force || force;
3148
+ queued.waiters.push(resolve);
3149
+ return;
3150
+ }
3151
+ const job = { layout, force, waiters: [resolve] };
3152
+ if (inFlight.has(key)) {
3153
+ pending.set(key, job);
3154
+ return;
3155
+ }
3156
+ void drain(scope, key, job);
3157
+ });
3158
+ },
3159
+ async loadBackup(scope) {
3160
+ const valid = checkScope(scope);
3161
+ if (!valid.ok) return valid;
3162
+ const read = await readLayout(scope, "backup");
3163
+ return read.ok ? { ok: true, value: read.value.layout } : read;
3164
+ },
3165
+ async saveBackup(scope, layout) {
3166
+ const valid = checkScope(scope);
3167
+ if (!valid.ok) return valid;
3168
+ const key = keyFor(scope, "backup");
3169
+ const text = encode(layout, key);
3170
+ if (!text.ok) return text;
3171
+ const stored = await guarded(
3172
+ key,
3173
+ "save",
3174
+ () => adapter.set(key, text.value, { signal })
3175
+ );
3176
+ return stored.ok ? { ok: true, value: null } : stored;
3177
+ },
3178
+ async reset(scope) {
3179
+ const valid = checkScope(scope);
3180
+ if (!valid.ok) return valid;
3181
+ const layoutKey = keyFor(scope);
3182
+ const backupKey = keyFor(scope, "backup");
3183
+ if (adapter.remove === void 0) {
3184
+ return failure(
3185
+ "invalid",
3186
+ `Could not reset "${layoutKey}": the adapter has no remove method.`
3187
+ );
3188
+ }
3189
+ const remove = adapter.remove.bind(adapter);
3190
+ const first = await guarded(
3191
+ layoutKey,
3192
+ "remove",
3193
+ () => remove(layoutKey, { signal })
3194
+ );
3195
+ revisions.delete(layoutKey);
3196
+ if (!first.ok) return first;
3197
+ const second = await guarded(
3198
+ backupKey,
3199
+ "remove",
3200
+ () => remove(backupKey, { signal })
3201
+ );
3202
+ return second.ok ? { ok: true, value: null } : second;
3203
+ },
3204
+ watch(scope, definitions, onChange, normalizeOptions) {
3205
+ const key = keyFor(scope);
3206
+ const stop = adapter.subscribe?.(
3207
+ key,
3208
+ (stored) => {
3209
+ if (stored === null) {
3210
+ revisions.delete(key);
3211
+ onChange({ layout: null });
3212
+ return;
3213
+ }
3214
+ if (stored.value === lastWritten.get(key) || stored.revision !== void 0 && stored.revision === revisions.get(key)) {
3215
+ return;
3216
+ }
3217
+ const read = readEnvelope(key, stored);
3218
+ if (!read.ok) return;
3219
+ onChange(
3220
+ repair(
3221
+ {
3222
+ layout: read.value.layout,
3223
+ ...stored.revision === void 0 ? {} : { revision: stored.revision },
3224
+ ...read.value.savedAt === void 0 ? {} : { savedAt: read.value.savedAt }
3225
+ },
3226
+ definitions,
3227
+ normalizeOptions
3228
+ )
3229
+ );
3230
+ }
3231
+ );
3232
+ return stop ?? (() => void 0);
3233
+ }
3234
+ };
3235
+ }
3236
+
3237
+ export { 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_REPORTED_PROBLEMS, MAX_TEXT_OPTION_LENGTH, MAX_WIDGET_OPTIONS, MAX_WIDGET_WIDTH, MIN_WIDGET_WIDTH, OPTION_KEY_PATTERN, VALUE_FORMATS, WIDGET_FILLS, WIDGET_KINDS, WIDGET_OPTION_TYPES, WIDGET_SIZES, WIDTH_COLUMNS, addPage, applyClientOptions, assignPages, baseColumnSpan, cacheKeyFor, coerceOptionValue, createLayoutPersistence, createWidgetLoader, defaultDetailQuery, defaultOptionValue, defineWidget, deriveDetailData, describeUnknownError, emptyTextFor, emptyWidget, enforceLocks, err, failedWidget, fillColumnSpans, fillWidthSpans, fillsHeight, fillsWidth, finalizeWidget, fitCount, formatCount, formatPercentChange, formatValue, fromLegacyDefinition, hasPages, hiddenWidgets, hideWidget, isClientApplied, isColumnKeyFor, isHidden, isIsoInterval, isMinimized, isSafeHref, isSafeImageUrl, isWidgetEmptyState, isWidgetKind, isWidgetVisibleTo, isWidgetWidth, itemWidth, kpiDelta, labelStride, loadingWidgets, memoryAdapter, minimizeWidget, movePage, moveWidget, moveWidgetBy, moveWidgetDown, moveWidgetToPage, moveWidgetUp, nextPageKey, niceDomain, niceStep, normalizeLayout, ok, optionValuesFor, optionsCacheSuffix, pageLayout, pageList, pageOf, pageRoom, pageWidgets, parseDetailQuery, parseLayout, parseSortValue, percentChange, placeRows, pruneLayout, queryRows, readOnly, removePage, renamePage, resolveDateRange, resolveDetailOptions, resolveOptionValues, resolvePageKey, resolvePayload, resolveTableControls, resolveWidget, resolveWidgetLock, resolveWidgetUnapplied, resolveWidgets, restoreWidget, sameOptionValues, serializeDetailQuery, serializeLayout, shareOf, showWidget, sortDefinitions, sortFromOptions, tableDetailData, toggleHidden, toggleMinimized, upgradeLegacyWidgetData, usesWidthColumns, utf8Length, validateDetailData, validateWidgetData, validateWidgetDefinition, visibleWidgets, widgetsFor, widthForSize, withEncoding, withFallback, withLogging, withPageLayout, withPrefix, withReadCache, withRetry };
1497
3238
  //# sourceMappingURL=core.js.map
1498
3239
  //# sourceMappingURL=core.js.map