@richardmcquiston01/dashboard-widgets-toolkit 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.cjs CHANGED
@@ -89,12 +89,14 @@ var WIDGET_FILLS = ["height", "width", "both"];
89
89
  var NOT_LOCKED = Object.freeze({
90
90
  move: false,
91
91
  hide: false,
92
- minimize: false
92
+ minimize: false,
93
+ options: false
93
94
  });
94
95
  var FULLY_LOCKED = Object.freeze({
95
96
  move: true,
96
97
  hide: true,
97
- minimize: true
98
+ minimize: true,
99
+ options: true
98
100
  });
99
101
  function resolveWidgetLock(definition) {
100
102
  const setting = definition.locked;
@@ -103,7 +105,8 @@ function resolveWidgetLock(definition) {
103
105
  return {
104
106
  move: setting.move === true,
105
107
  hide: setting.hide === true,
106
- minimize: setting.minimize === true
108
+ minimize: setting.minimize === true,
109
+ options: setting.options === true
107
110
  };
108
111
  }
109
112
  function defineWidget(definition) {
@@ -685,6 +688,546 @@ function placeAndFillRows(items, columns, spanOf) {
685
688
  return { spans, rows };
686
689
  }
687
690
 
691
+ // src/core/options.ts
692
+ var DATE_RANGE_PRESETS = [
693
+ "today",
694
+ "yesterday",
695
+ "last7",
696
+ "last30",
697
+ "last90",
698
+ "thisMonth",
699
+ "lastMonth",
700
+ "thisYear",
701
+ "lastYear"
702
+ ];
703
+ var WIDGET_OPTION_TYPES = [
704
+ "choice",
705
+ "number",
706
+ "boolean",
707
+ "text",
708
+ "dateRange",
709
+ "color",
710
+ "sort",
711
+ "columns"
712
+ ];
713
+ var MAX_WIDGET_OPTIONS = 20;
714
+ var MAX_TEXT_OPTION_LENGTH = 500;
715
+ var OPTION_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,39}$/;
716
+ var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
717
+ var INTERVAL_PATTERN = /^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/;
718
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
719
+ function isColumnKeyFor(kind, key) {
720
+ if (kind === "TABLE") return /^c\d+$/.test(key);
721
+ if (kind === "BAR_LIST") return key === "label" || key === "value";
722
+ return false;
723
+ }
724
+ function isClientApplied(option) {
725
+ return (option.type === "sort" || option.type === "columns") && option.apply === "client";
726
+ }
727
+ function civilToday(context) {
728
+ const now = context.now ?? Date.now();
729
+ let timeZone = context.timeZone ?? "UTC";
730
+ try {
731
+ new Intl.DateTimeFormat("en-US", { timeZone });
732
+ } catch {
733
+ timeZone = "UTC";
734
+ }
735
+ const parts = new Intl.DateTimeFormat("en-US", {
736
+ timeZone,
737
+ year: "numeric",
738
+ month: "2-digit",
739
+ day: "2-digit"
740
+ }).formatToParts(new Date(now));
741
+ const pick = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
742
+ return { year: pick("year"), month: pick("month"), day: pick("day") };
743
+ }
744
+ function toIso(epochMs) {
745
+ return new Date(epochMs).toISOString().slice(0, 10);
746
+ }
747
+ var DAY_MS = 864e5;
748
+ function parseIsoDay(text) {
749
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
750
+ if (match === null) return void 0;
751
+ const epoch = Date.UTC(
752
+ Number(match[1]),
753
+ Number(match[2]) - 1,
754
+ Number(match[3])
755
+ );
756
+ return toIso(epoch) === text ? epoch : void 0;
757
+ }
758
+ function isIsoInterval(value) {
759
+ const match = INTERVAL_PATTERN.exec(value);
760
+ if (match === null) return false;
761
+ const start = parseIsoDay(match[1]);
762
+ const end = parseIsoDay(match[2]);
763
+ return start !== void 0 && end !== void 0 && start <= end;
764
+ }
765
+ function isPreset(value) {
766
+ return DATE_RANGE_PRESETS.includes(value);
767
+ }
768
+ function resolveDateRange(value, context = {}) {
769
+ if (isIsoInterval(value)) return value;
770
+ if (!isPreset(value)) return void 0;
771
+ const today = civilToday(context);
772
+ const todayMs = Date.UTC(today.year, today.month - 1, today.day);
773
+ const range = (startMs, endMs) => `${toIso(startMs)}/${toIso(endMs)}`;
774
+ switch (value) {
775
+ case "today":
776
+ return range(todayMs, todayMs);
777
+ case "yesterday":
778
+ return range(todayMs - DAY_MS, todayMs - DAY_MS);
779
+ case "last7":
780
+ return range(todayMs - 6 * DAY_MS, todayMs);
781
+ case "last30":
782
+ return range(todayMs - 29 * DAY_MS, todayMs);
783
+ case "last90":
784
+ return range(todayMs - 89 * DAY_MS, todayMs);
785
+ case "thisMonth":
786
+ return range(Date.UTC(today.year, today.month - 1, 1), todayMs);
787
+ case "lastMonth":
788
+ return range(
789
+ Date.UTC(today.year, today.month - 2, 1),
790
+ Date.UTC(today.year, today.month - 1, 1) - DAY_MS
791
+ );
792
+ case "thisYear":
793
+ return range(Date.UTC(today.year, 0, 1), todayMs);
794
+ case "lastYear":
795
+ return range(
796
+ Date.UTC(today.year - 1, 0, 1),
797
+ Date.UTC(today.year, 0, 1) - DAY_MS
798
+ );
799
+ }
800
+ }
801
+ function onStepGrid(option, value) {
802
+ if (option.step === void 0) return true;
803
+ const steps = (value - option.min) / option.step;
804
+ return Math.abs(steps - Math.round(steps)) < 1e-9;
805
+ }
806
+ function parseSortValue(value) {
807
+ const separator = value.lastIndexOf(":");
808
+ if (separator <= 0) return void 0;
809
+ const column = value.slice(0, separator);
810
+ const direction = value.slice(separator + 1);
811
+ if (direction !== "asc" && direction !== "desc") return void 0;
812
+ return { column, direction };
813
+ }
814
+ function sortFromOptions(definition, values) {
815
+ if (values === void 0) return void 0;
816
+ const sortOption = definition.options?.find(
817
+ (option) => option.type === "sort"
818
+ );
819
+ if (sortOption === void 0) return void 0;
820
+ const chosen = values[sortOption.key];
821
+ return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
822
+ }
823
+ function coerceOptionValue(option, value, context = {}) {
824
+ switch (option.type) {
825
+ case "choice":
826
+ return typeof value === "string" && option.choices.some((choice) => choice.value === value) ? value : void 0;
827
+ case "number":
828
+ return typeof value === "number" && Number.isFinite(value) && value >= option.min && value <= option.max && onStepGrid(option, value) ? value : void 0;
829
+ case "boolean":
830
+ return typeof value === "boolean" ? value : void 0;
831
+ case "text": {
832
+ if (typeof value !== "string") return void 0;
833
+ const trimmed = value.trim();
834
+ return trimmed.length <= option.maxLength && !CONTROL_CHARACTERS.test(trimmed) ? trimmed : void 0;
835
+ }
836
+ case "dateRange": {
837
+ if (typeof value !== "string") return void 0;
838
+ if (isPreset(value) && option.presets !== void 0 && !option.presets.some((preset) => preset.value === value)) {
839
+ return void 0;
840
+ }
841
+ return resolveDateRange(value, context);
842
+ }
843
+ case "color":
844
+ return typeof value === "string" && COLOR_PATTERN.test(value) ? value.toLowerCase() : void 0;
845
+ case "sort": {
846
+ if (typeof value !== "string") return void 0;
847
+ const sort = parseSortValue(value);
848
+ return sort !== void 0 && option.columns.some((column) => column.key === sort.column) ? value : void 0;
849
+ }
850
+ case "columns": {
851
+ if (!Array.isArray(value) || value.length === 0) return void 0;
852
+ const keys = value;
853
+ const allowed = new Set(option.columns.map((c) => c.key));
854
+ const seen = /* @__PURE__ */ new Set();
855
+ for (const key of keys) {
856
+ if (typeof key !== "string" || !allowed.has(key) || seen.has(key)) {
857
+ return void 0;
858
+ }
859
+ seen.add(key);
860
+ }
861
+ return [...seen];
862
+ }
863
+ }
864
+ }
865
+ function defaultOptionValue(option, context = {}) {
866
+ switch (option.type) {
867
+ case "dateRange":
868
+ return resolveDateRange(option.default, context);
869
+ case "sort":
870
+ return option.default;
871
+ case "columns":
872
+ return [...option.default];
873
+ default:
874
+ return option.default;
875
+ }
876
+ }
877
+ var NO_VALUES = Object.freeze({});
878
+ function resolveOptionValues(definition, chosen, context = {}) {
879
+ const options = definition.options;
880
+ if (options === void 0 || options.length === 0) return NO_VALUES;
881
+ const values = {};
882
+ for (const option of options) {
883
+ const picked = chosen?.[option.key];
884
+ const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked, context)) ?? defaultOptionValue(option, context);
885
+ if (value !== void 0) values[option.key] = value;
886
+ }
887
+ return values;
888
+ }
889
+ function sameValue(a, b) {
890
+ if (Array.isArray(a) && Array.isArray(b)) {
891
+ return a.length === b.length && a.every((item, i) => item === b[i]);
892
+ }
893
+ return a === b;
894
+ }
895
+ function sameOptionValues(a, b) {
896
+ const keys = Object.keys({ ...a, ...b });
897
+ return keys.every((key) => sameValue(a[key], b[key]));
898
+ }
899
+ function optionsCacheSuffix(definition, values, context = {}) {
900
+ const parts = [];
901
+ for (const option of definition.options ?? []) {
902
+ if (isClientApplied(option)) continue;
903
+ const value = values[option.key];
904
+ if (sameValue(value, defaultOptionValue(option, context))) continue;
905
+ parts.push(
906
+ `${option.key}=${encodeURIComponent(JSON.stringify(value ?? null))}`
907
+ );
908
+ }
909
+ parts.sort();
910
+ return parts.length === 0 ? "" : `?${parts.join("&")}`;
911
+ }
912
+ function tableColumnIndex(key) {
913
+ const match = /^c(\d+)$/.exec(key);
914
+ return match === null ? void 0 : Number(match[1]);
915
+ }
916
+ function sortBarItems(items, sort) {
917
+ const sign = sort.direction === "desc" ? -1 : 1;
918
+ return items.map((item, index) => ({ item, index })).sort((a, b) => {
919
+ const order = sort.column === "value" ? a.item.value - b.item.value : a.item.label.localeCompare(b.item.label);
920
+ return order * sign || a.index - b.index;
921
+ }).map(({ item }) => item);
922
+ }
923
+ function applyClientOptions(definition, data, values) {
924
+ const options = definition.options ?? [];
925
+ const sortOption = options.find(
926
+ (o) => o.type === "sort" && o.apply === "client"
927
+ );
928
+ const columnsOption = options.find(
929
+ (o) => o.type === "columns" && o.apply === "client"
930
+ );
931
+ const sortValue = sortOption === void 0 ? void 0 : values[sortOption.key];
932
+ const columnsValue = columnsOption === void 0 ? void 0 : values[columnsOption.key];
933
+ let next = data;
934
+ if (typeof sortValue === "string") {
935
+ const sort = parseSortValue(sortValue);
936
+ if (sort !== void 0 && next.kind === "BAR_LIST") {
937
+ if (sort.column === "label" || sort.column === "value") {
938
+ next = { ...next, items: sortBarItems(next.items, sort) };
939
+ }
940
+ } else if (sort !== void 0 && next.kind === "TABLE") {
941
+ const index = tableColumnIndex(sort.column);
942
+ if (index !== void 0 && index < next.columns.length) {
943
+ const sorted = queryRows(tableDetailData(next), {
944
+ page: 1,
945
+ pageSize: Math.max(1, next.rows.length),
946
+ sort
947
+ });
948
+ if (sorted.ok) next = { ...next, rows: sorted.value.rows };
949
+ }
950
+ }
951
+ }
952
+ if (Array.isArray(columnsValue) && next.kind === "TABLE") {
953
+ const table = next;
954
+ const indexes = columnsValue.map(tableColumnIndex).filter(
955
+ (index) => index !== void 0 && index < table.columns.length
956
+ );
957
+ if (indexes.length > 0) {
958
+ next = {
959
+ ...table,
960
+ columns: indexes.map((i) => table.columns[i]),
961
+ rows: table.rows.map((row) => indexes.map((i) => row[i]))
962
+ };
963
+ }
964
+ }
965
+ return next;
966
+ }
967
+
968
+ // src/core/settings.ts
969
+ var MAX_WIDGET_TITLE_LENGTH = 80;
970
+ var MAX_PARSED_SETTINGS = 500;
971
+ var MAX_COLUMN_LIST = 100;
972
+ var MAX_VALUE_LENGTH = 500;
973
+ function isRecord(value) {
974
+ return typeof value === "object" && value !== null && !Array.isArray(value);
975
+ }
976
+ function parseOptionValue(value) {
977
+ if (typeof value === "string") {
978
+ return value.length <= MAX_VALUE_LENGTH ? value : void 0;
979
+ }
980
+ if (typeof value === "number")
981
+ return Number.isFinite(value) ? value : void 0;
982
+ if (typeof value === "boolean") return value;
983
+ if (Array.isArray(value) && value.length <= MAX_COLUMN_LIST) {
984
+ return value.every(
985
+ (entry) => typeof entry === "string" && entry.length <= MAX_VALUE_LENGTH
986
+ ) ? value : void 0;
987
+ }
988
+ return void 0;
989
+ }
990
+ function parseOne(raw) {
991
+ if (!isRecord(raw)) return void 0;
992
+ const title = raw["title"];
993
+ const width = raw["width"];
994
+ const flexible = raw["flexible"];
995
+ const options = raw["options"];
996
+ const cleanTitle = typeof title === "string" ? title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH) : "";
997
+ const chosen = {};
998
+ if (isRecord(options)) {
999
+ for (const [key, value] of Object.entries(options)) {
1000
+ const parsed = parseOptionValue(value);
1001
+ if (parsed !== void 0) chosen[key] = parsed;
1002
+ }
1003
+ }
1004
+ const settings = {
1005
+ ...cleanTitle === "" ? {} : { title: cleanTitle },
1006
+ ...isWidgetWidth(width) ? { width } : {},
1007
+ ...typeof flexible === "boolean" ? { flexible } : {},
1008
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen }
1009
+ };
1010
+ return Object.keys(settings).length === 0 ? void 0 : settings;
1011
+ }
1012
+ function parseSettings(raw) {
1013
+ if (!isRecord(raw)) return void 0;
1014
+ const parsed = {};
1015
+ for (const [key, value] of Object.entries(raw).slice(
1016
+ 0,
1017
+ MAX_PARSED_SETTINGS
1018
+ )) {
1019
+ if (key === "") continue;
1020
+ const one = parseOne(value);
1021
+ if (one !== void 0) parsed[key] = one;
1022
+ }
1023
+ return Object.keys(parsed).length === 0 ? void 0 : parsed;
1024
+ }
1025
+ function serializeSettings(settings) {
1026
+ if (settings === void 0) return void 0;
1027
+ const out = {};
1028
+ for (const key of Object.keys(settings)) {
1029
+ const one = settings[key];
1030
+ if (one === void 0) continue;
1031
+ const piece = {
1032
+ ...one.title === void 0 ? {} : { title: one.title },
1033
+ ...one.width === void 0 ? {} : { width: one.width },
1034
+ ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1035
+ ...one.options === void 0 || Object.keys(one.options).length === 0 ? {} : { options: { ...one.options } }
1036
+ };
1037
+ if (Object.keys(piece).length > 0) out[key] = piece;
1038
+ }
1039
+ return Object.keys(out).length === 0 ? void 0 : out;
1040
+ }
1041
+ function widthRange(item) {
1042
+ const min = isWidgetWidth(item.minWidth) ? item.minWidth : MIN_WIDGET_WIDTH;
1043
+ const max = isWidgetWidth(item.maxWidth) ? item.maxWidth : MAX_WIDGET_WIDTH;
1044
+ return { min, max: Math.max(min, max) };
1045
+ }
1046
+ var WIDTH_SHARES = {
1047
+ 2: "1/6",
1048
+ 3: "1/4",
1049
+ 4: "1/3",
1050
+ 5: "5/12",
1051
+ 6: "1/2",
1052
+ 7: "7/12",
1053
+ 8: "2/3",
1054
+ 9: "3/4",
1055
+ 10: "5/6",
1056
+ 11: "11/12",
1057
+ 12: "Full"
1058
+ };
1059
+ function widthShareLabel(width) {
1060
+ return WIDTH_SHARES[width] ?? `${width}/12`;
1061
+ }
1062
+ function isOptionsLocked(item) {
1063
+ return resolveWidgetLock(item).options;
1064
+ }
1065
+ function fillWith(fill, flexible) {
1066
+ const heightToo = fill === "height" || fill === "both";
1067
+ if (flexible) return heightToo ? "both" : "width";
1068
+ return heightToo ? "height" : void 0;
1069
+ }
1070
+ function isFlexible(item, settings) {
1071
+ if (settings?.flexible !== void 0) return settings.flexible;
1072
+ return item.fill === "width" || item.fill === "both";
1073
+ }
1074
+ function applySettings(definition, settings) {
1075
+ if (settings === void 0) return definition;
1076
+ const range = widthRange(definition);
1077
+ const width = settings.width === void 0 ? void 0 : Math.min(range.max, Math.max(range.min, settings.width));
1078
+ const title = settings.title === void 0 || settings.title.trim() === "" ? void 0 : settings.title;
1079
+ const flexibleChanged = settings.flexible !== void 0 && settings.flexible !== isFlexible(definition, void 0);
1080
+ if (title === void 0 && width === void 0 && !flexibleChanged) {
1081
+ return definition;
1082
+ }
1083
+ const next = { ...definition };
1084
+ if (title !== void 0) next.title = title;
1085
+ if (width !== void 0) next.width = width;
1086
+ if (flexibleChanged) {
1087
+ const fill = fillWith(
1088
+ definition.fill,
1089
+ settings.flexible === true
1090
+ );
1091
+ if (fill === void 0) {
1092
+ delete next.fill;
1093
+ } else {
1094
+ next.fill = fill;
1095
+ }
1096
+ }
1097
+ return next;
1098
+ }
1099
+ function effectiveDefinitions(definitions, layout, options = {}) {
1100
+ return definitions.map(
1101
+ (definition) => options.overrideLocks !== true && isOptionsLocked(definition) ? definition : applySettings(definition, layout.settings?.[definition.key])
1102
+ );
1103
+ }
1104
+ function widgetOptionValues(definition, settings) {
1105
+ const declared = definition.options ?? [];
1106
+ const values = {};
1107
+ for (const option of declared) {
1108
+ const picked = settings?.options?.[option.key];
1109
+ const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked)) ?? defaultOptionValue(option);
1110
+ if (value !== void 0) values[option.key] = value;
1111
+ }
1112
+ return values;
1113
+ }
1114
+ function optionValuesFromLayout(definitions, layout, options = {}) {
1115
+ const out = {};
1116
+ for (const definition of definitions) {
1117
+ if (definition.options === void 0 || definition.options.length === 0) {
1118
+ continue;
1119
+ }
1120
+ const locked = options.overrideLocks !== true && isOptionsLocked(definition);
1121
+ out[definition.key] = locked ? {} : layout.settings?.[definition.key]?.options ?? {};
1122
+ }
1123
+ return out;
1124
+ }
1125
+ function sameSettings(a, b) {
1126
+ if (a === b) return true;
1127
+ return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && sameOptionValues(a?.options ?? {}, b?.options ?? {});
1128
+ }
1129
+ function setWidgetSettings(layout, key, patch) {
1130
+ const before = layout.settings?.[key];
1131
+ const draft = { ...before };
1132
+ for (const field of ["title", "width", "flexible", "options"]) {
1133
+ if (!(field in patch)) continue;
1134
+ const value = patch[field];
1135
+ if (value === void 0) {
1136
+ delete draft[field];
1137
+ } else {
1138
+ draft[field] = value;
1139
+ }
1140
+ }
1141
+ const merged = parseOne(draft);
1142
+ if (sameSettings(before, merged)) return layout;
1143
+ const rest = { ...layout.settings };
1144
+ if (merged === void 0) {
1145
+ delete rest[key];
1146
+ } else {
1147
+ rest[key] = merged;
1148
+ }
1149
+ const { settings: _previous, ...others } = layout;
1150
+ return Object.keys(rest).length === 0 ? others : { ...others, settings: rest };
1151
+ }
1152
+ function clearWidgetSettings(layout, key) {
1153
+ if (layout.settings?.[key] === void 0) return layout;
1154
+ return setWidgetSettings(layout, key, {
1155
+ title: void 0,
1156
+ width: void 0,
1157
+ flexible: void 0,
1158
+ options: void 0
1159
+ });
1160
+ }
1161
+ function normalizeTitle(title) {
1162
+ return title.normalize("NFKC").trim().toLowerCase();
1163
+ }
1164
+ function isTitleInUse(definitions, layout, key, title) {
1165
+ const wanted = normalizeTitle(title);
1166
+ if (wanted === "") return false;
1167
+ return effectiveDefinitions(definitions, layout, {
1168
+ overrideLocks: true
1169
+ }).some(
1170
+ (other) => other.key !== key && normalizeTitle(other.title ?? other.key) === wanted
1171
+ );
1172
+ }
1173
+ function uniqueTitle(taken, title) {
1174
+ if (!taken.has(normalizeTitle(title))) return title;
1175
+ for (let n = 2; ; n += 1) {
1176
+ const suffix = ` (#${n})`;
1177
+ const candidate = `${title.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
1178
+ if (!taken.has(normalizeTitle(candidate))) return candidate;
1179
+ }
1180
+ }
1181
+ function normalizeSettings(definitions, layout, options = {}) {
1182
+ const settings = layout.settings;
1183
+ if (settings === void 0) return layout;
1184
+ const cleaned = {};
1185
+ const taken = /* @__PURE__ */ new Set();
1186
+ for (const definition of definitions) {
1187
+ const renamed = settings[definition.key]?.title !== void 0 && (options.overrideLocks === true || !isOptionsLocked(definition));
1188
+ if (!renamed) taken.add(normalizeTitle(definition.title ?? definition.key));
1189
+ }
1190
+ for (const definition of definitions) {
1191
+ const one = settings[definition.key];
1192
+ if (one === void 0) continue;
1193
+ if (options.overrideLocks !== true && isOptionsLocked(definition)) continue;
1194
+ const range = widthRange(definition);
1195
+ const chosen = {};
1196
+ for (const option of definition.options ?? []) {
1197
+ const picked = one.options?.[option.key];
1198
+ if (picked === void 0) continue;
1199
+ const value = coerceOptionValue(option, picked);
1200
+ if (value !== void 0) {
1201
+ chosen[option.key] = option.type === "dateRange" ? picked : value;
1202
+ }
1203
+ }
1204
+ let title = one.title;
1205
+ if (title !== void 0) {
1206
+ title = title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH);
1207
+ if (title === "") {
1208
+ title = void 0;
1209
+ } else {
1210
+ title = uniqueTitle(taken, title);
1211
+ taken.add(normalizeTitle(title));
1212
+ }
1213
+ }
1214
+ const next = {
1215
+ ...title === void 0 ? {} : { title },
1216
+ ...one.width === void 0 ? {} : { width: Math.min(range.max, Math.max(range.min, one.width)) },
1217
+ ...one.flexible === void 0 ? {} : { flexible: one.flexible },
1218
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen }
1219
+ };
1220
+ if (Object.keys(next).length > 0) {
1221
+ cleaned[definition.key] = sameSettings(one, next) ? one : next;
1222
+ }
1223
+ }
1224
+ const keys = Object.keys(cleaned);
1225
+ const unchanged = keys.length === Object.keys(settings).length && keys.every((key) => cleaned[key] === settings[key]);
1226
+ if (unchanged) return layout;
1227
+ const { settings: _previous, ...others } = layout;
1228
+ return keys.length === 0 ? others : { ...others, settings: cleaned };
1229
+ }
1230
+
688
1231
  // src/core/layout.ts
689
1232
  var MAX_PAGE_TITLE_LENGTH = 30;
690
1233
  var MAX_PARSED_PAGES = 50;
@@ -719,18 +1262,23 @@ function parseLayout(raw) {
719
1262
  const hidden = uniqueStrings(record["hidden"]);
720
1263
  const minimized = uniqueStrings(record["minimized"]);
721
1264
  const pages = parsePages(record["pages"]);
1265
+ const settings = parseSettings(
1266
+ record["settings"]
1267
+ );
1268
+ const withSettings = settings === void 0 ? {} : { settings };
722
1269
  if (pages.length > 0) {
723
1270
  return {
724
1271
  order: EMPTY_LAYOUT.order,
725
1272
  hidden: EMPTY_LAYOUT.hidden,
726
1273
  minimized: EMPTY_LAYOUT.minimized,
727
- pages
1274
+ pages,
1275
+ ...withSettings
728
1276
  };
729
1277
  }
730
1278
  if (order.length === 0 && hidden.length === 0 && minimized.length === 0) {
731
- return EMPTY_LAYOUT;
1279
+ return settings === void 0 ? EMPTY_LAYOUT : { ...EMPTY_LAYOUT, settings };
732
1280
  }
733
- return { order, hidden, minimized };
1281
+ return { order, hidden, minimized, ...withSettings };
734
1282
  }
735
1283
  function parsePages(value) {
736
1284
  if (!Array.isArray(value)) return [];
@@ -777,7 +1325,8 @@ function serializeLayout(layout) {
777
1325
  minimized: [...page.minimized],
778
1326
  ...page.maxRows === void 0 ? {} : { maxRows: page.maxRows }
779
1327
  }))
780
- }
1328
+ },
1329
+ ...serializeSettings(layout.settings) === void 0 ? {} : { settings: serializeSettings(layout.settings) }
781
1330
  });
782
1331
  }
783
1332
  function bySortOrder(items) {
@@ -845,378 +1394,101 @@ function freeSlotIndexes(ordered) {
845
1394
  ordered.forEach((item, index) => {
846
1395
  if (!isPinned(item)) slots.push(index);
847
1396
  });
848
- return slots;
849
- }
850
- function moveWidgetBy(definitions, layout, key, offset) {
851
- const current = visibleWidgets(definitions, layout);
852
- const fromIndex = current.findIndex((d) => d.key === key);
853
- if (fromIndex === -1) return layout;
854
- const freeSlots = freeSlotIndexes(current);
855
- const fromFree = freeSlots.indexOf(fromIndex);
856
- if (fromFree === -1) return layout;
857
- const toFree = Math.max(
858
- 0,
859
- Math.min(freeSlots.length - 1, fromFree + Math.trunc(offset))
860
- );
861
- return moveWidget(definitions, layout, key, freeSlots[toFree]);
862
- }
863
- function moveWidgetUp(definitions, layout, key) {
864
- return moveWidgetBy(definitions, layout, key, -1);
865
- }
866
- function moveWidgetDown(definitions, layout, key) {
867
- return moveWidgetBy(definitions, layout, key, 1);
868
- }
869
- function isHidden(layout, key) {
870
- return layout.hidden.includes(key);
871
- }
872
- function isMinimized(layout, key) {
873
- return layout.minimized.includes(key);
874
- }
875
- function hideWidget(layout, key) {
876
- if (isHidden(layout, key)) return layout;
877
- return { ...layout, hidden: [...layout.hidden, key] };
878
- }
879
- function showWidget(layout, key) {
880
- if (!isHidden(layout, key)) return layout;
881
- return {
882
- ...layout,
883
- hidden: layout.hidden.filter((k) => k !== key),
884
- minimized: layout.minimized.filter((k) => k !== key)
885
- };
886
- }
887
- function toggleHidden(layout, key) {
888
- return isHidden(layout, key) ? showWidget(layout, key) : hideWidget(layout, key);
889
- }
890
- function minimizeWidget(layout, key) {
891
- if (isMinimized(layout, key)) return layout;
892
- return { ...layout, minimized: [...layout.minimized, key] };
893
- }
894
- function restoreWidget(layout, key) {
895
- if (!isMinimized(layout, key)) return layout;
896
- return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
897
- }
898
- function toggleMinimized(layout, key) {
899
- return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
900
- }
901
- function pruneLayout(definitions, layout) {
902
- const known = new Set(definitions.map((d) => d.key));
903
- const keep = (keys) => keys.every((k) => known.has(k)) ? keys : keys.filter((k) => known.has(k));
904
- return mapLayoutLists(layout, (lists) => ({
905
- order: keep(lists.order),
906
- hidden: keep(lists.hidden),
907
- minimized: keep(lists.minimized)
908
- }));
909
- }
910
- function mapLayoutLists(layout, change) {
911
- const top = change(layout);
912
- const pages = layout.pages?.map((page) => {
913
- const next = change(page);
914
- return next.order === page.order && next.hidden === page.hidden && next.minimized === page.minimized ? page : { ...page, ...next };
915
- });
916
- const pagesChanged = pages !== void 0 && pages.some((page, i) => page !== layout.pages?.[i]);
917
- if (top.order === layout.order && top.hidden === layout.hidden && top.minimized === layout.minimized && !pagesChanged) {
918
- return layout;
919
- }
920
- return {
921
- ...layout,
922
- ...top,
923
- ...pages === void 0 ? {} : { pages }
924
- };
925
- }
926
- function enforceLocks(definitions, layout, options = {}) {
927
- if (options.overrideLocks === true) return layout;
928
- const noMove = /* @__PURE__ */ new Set();
929
- const noHide = /* @__PURE__ */ new Set();
930
- const noMinimize = /* @__PURE__ */ new Set();
931
- for (const definition of definitions) {
932
- const lock = resolveWidgetLock(definition);
933
- if (lock.move) noMove.add(definition.key);
934
- if (lock.hide) noHide.add(definition.key);
935
- if (lock.minimize) noMinimize.add(definition.key);
936
- }
937
- const without2 = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
938
- return mapLayoutLists(layout, (lists) => ({
939
- order: without2(lists.order, noMove),
940
- hidden: without2(lists.hidden, noHide),
941
- minimized: without2(lists.minimized, noMinimize)
942
- }));
943
- }
944
-
945
- // src/core/options.ts
946
- var DATE_RANGE_PRESETS = [
947
- "today",
948
- "yesterday",
949
- "last7",
950
- "last30",
951
- "last90",
952
- "thisMonth",
953
- "lastMonth",
954
- "thisYear",
955
- "lastYear"
956
- ];
957
- var WIDGET_OPTION_TYPES = [
958
- "choice",
959
- "number",
960
- "boolean",
961
- "text",
962
- "dateRange",
963
- "color",
964
- "sort",
965
- "columns"
966
- ];
967
- var MAX_WIDGET_OPTIONS = 20;
968
- var MAX_TEXT_OPTION_LENGTH = 500;
969
- var OPTION_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,39}$/;
970
- var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
971
- var INTERVAL_PATTERN = /^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/;
972
- var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
973
- function isColumnKeyFor(kind, key) {
974
- if (kind === "TABLE") return /^c\d+$/.test(key);
975
- if (kind === "BAR_LIST") return key === "label" || key === "value";
976
- return false;
977
- }
978
- function isClientApplied(option) {
979
- return (option.type === "sort" || option.type === "columns") && option.apply === "client";
980
- }
981
- function civilToday(context) {
982
- const now = context.now ?? Date.now();
983
- let timeZone = context.timeZone ?? "UTC";
984
- try {
985
- new Intl.DateTimeFormat("en-US", { timeZone });
986
- } catch {
987
- timeZone = "UTC";
988
- }
989
- const parts = new Intl.DateTimeFormat("en-US", {
990
- timeZone,
991
- year: "numeric",
992
- month: "2-digit",
993
- day: "2-digit"
994
- }).formatToParts(new Date(now));
995
- const pick = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
996
- return { year: pick("year"), month: pick("month"), day: pick("day") };
997
- }
998
- function toIso(epochMs) {
999
- return new Date(epochMs).toISOString().slice(0, 10);
1000
- }
1001
- var DAY_MS = 864e5;
1002
- function parseIsoDay(text) {
1003
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
1004
- if (match === null) return void 0;
1005
- const epoch = Date.UTC(
1006
- Number(match[1]),
1007
- Number(match[2]) - 1,
1008
- Number(match[3])
1009
- );
1010
- return toIso(epoch) === text ? epoch : void 0;
1011
- }
1012
- function isIsoInterval(value) {
1013
- const match = INTERVAL_PATTERN.exec(value);
1014
- if (match === null) return false;
1015
- const start = parseIsoDay(match[1]);
1016
- const end = parseIsoDay(match[2]);
1017
- return start !== void 0 && end !== void 0 && start <= end;
1018
- }
1019
- function isPreset(value) {
1020
- return DATE_RANGE_PRESETS.includes(value);
1021
- }
1022
- function resolveDateRange(value, context = {}) {
1023
- if (isIsoInterval(value)) return value;
1024
- if (!isPreset(value)) return void 0;
1025
- const today = civilToday(context);
1026
- const todayMs = Date.UTC(today.year, today.month - 1, today.day);
1027
- const range = (startMs, endMs) => `${toIso(startMs)}/${toIso(endMs)}`;
1028
- switch (value) {
1029
- case "today":
1030
- return range(todayMs, todayMs);
1031
- case "yesterday":
1032
- return range(todayMs - DAY_MS, todayMs - DAY_MS);
1033
- case "last7":
1034
- return range(todayMs - 6 * DAY_MS, todayMs);
1035
- case "last30":
1036
- return range(todayMs - 29 * DAY_MS, todayMs);
1037
- case "last90":
1038
- return range(todayMs - 89 * DAY_MS, todayMs);
1039
- case "thisMonth":
1040
- return range(Date.UTC(today.year, today.month - 1, 1), todayMs);
1041
- case "lastMonth":
1042
- return range(
1043
- Date.UTC(today.year, today.month - 2, 1),
1044
- Date.UTC(today.year, today.month - 1, 1) - DAY_MS
1045
- );
1046
- case "thisYear":
1047
- return range(Date.UTC(today.year, 0, 1), todayMs);
1048
- case "lastYear":
1049
- return range(
1050
- Date.UTC(today.year - 1, 0, 1),
1051
- Date.UTC(today.year, 0, 1) - DAY_MS
1052
- );
1053
- }
1397
+ return slots;
1054
1398
  }
1055
- function onStepGrid(option, value) {
1056
- if (option.step === void 0) return true;
1057
- const steps = (value - option.min) / option.step;
1058
- return Math.abs(steps - Math.round(steps)) < 1e-9;
1399
+ function moveWidgetBy(definitions, layout, key, offset) {
1400
+ const current = visibleWidgets(definitions, layout);
1401
+ const fromIndex = current.findIndex((d) => d.key === key);
1402
+ if (fromIndex === -1) return layout;
1403
+ const freeSlots = freeSlotIndexes(current);
1404
+ const fromFree = freeSlots.indexOf(fromIndex);
1405
+ if (fromFree === -1) return layout;
1406
+ const toFree = Math.max(
1407
+ 0,
1408
+ Math.min(freeSlots.length - 1, fromFree + Math.trunc(offset))
1409
+ );
1410
+ return moveWidget(definitions, layout, key, freeSlots[toFree]);
1059
1411
  }
1060
- function parseSortValue(value) {
1061
- const separator = value.lastIndexOf(":");
1062
- if (separator <= 0) return void 0;
1063
- const column = value.slice(0, separator);
1064
- const direction = value.slice(separator + 1);
1065
- if (direction !== "asc" && direction !== "desc") return void 0;
1066
- return { column, direction };
1412
+ function moveWidgetUp(definitions, layout, key) {
1413
+ return moveWidgetBy(definitions, layout, key, -1);
1067
1414
  }
1068
- function sortFromOptions(definition, values) {
1069
- if (values === void 0) return void 0;
1070
- const sortOption = definition.options?.find(
1071
- (option) => option.type === "sort"
1072
- );
1073
- if (sortOption === void 0) return void 0;
1074
- const chosen = values[sortOption.key];
1075
- return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
1415
+ function moveWidgetDown(definitions, layout, key) {
1416
+ return moveWidgetBy(definitions, layout, key, 1);
1076
1417
  }
1077
- function coerceOptionValue(option, value, context = {}) {
1078
- switch (option.type) {
1079
- case "choice":
1080
- return typeof value === "string" && option.choices.some((choice) => choice.value === value) ? value : void 0;
1081
- case "number":
1082
- return typeof value === "number" && Number.isFinite(value) && value >= option.min && value <= option.max && onStepGrid(option, value) ? value : void 0;
1083
- case "boolean":
1084
- return typeof value === "boolean" ? value : void 0;
1085
- case "text": {
1086
- if (typeof value !== "string") return void 0;
1087
- const trimmed = value.trim();
1088
- return trimmed.length <= option.maxLength && !CONTROL_CHARACTERS.test(trimmed) ? trimmed : void 0;
1089
- }
1090
- case "dateRange": {
1091
- if (typeof value !== "string") return void 0;
1092
- if (isPreset(value) && option.presets !== void 0 && !option.presets.some((preset) => preset.value === value)) {
1093
- return void 0;
1094
- }
1095
- return resolveDateRange(value, context);
1096
- }
1097
- case "color":
1098
- return typeof value === "string" && COLOR_PATTERN.test(value) ? value.toLowerCase() : void 0;
1099
- case "sort": {
1100
- if (typeof value !== "string") return void 0;
1101
- const sort = parseSortValue(value);
1102
- return sort !== void 0 && option.columns.some((column) => column.key === sort.column) ? value : void 0;
1103
- }
1104
- case "columns": {
1105
- if (!Array.isArray(value) || value.length === 0) return void 0;
1106
- const keys = value;
1107
- const allowed = new Set(option.columns.map((c) => c.key));
1108
- const seen = /* @__PURE__ */ new Set();
1109
- for (const key of keys) {
1110
- if (typeof key !== "string" || !allowed.has(key) || seen.has(key)) {
1111
- return void 0;
1112
- }
1113
- seen.add(key);
1114
- }
1115
- return [...seen];
1116
- }
1117
- }
1418
+ function isHidden(layout, key) {
1419
+ return layout.hidden.includes(key);
1118
1420
  }
1119
- function defaultOptionValue(option, context = {}) {
1120
- switch (option.type) {
1121
- case "dateRange":
1122
- return resolveDateRange(option.default, context);
1123
- case "sort":
1124
- return option.default;
1125
- case "columns":
1126
- return [...option.default];
1127
- default:
1128
- return option.default;
1129
- }
1421
+ function isMinimized(layout, key) {
1422
+ return layout.minimized.includes(key);
1130
1423
  }
1131
- var NO_VALUES = Object.freeze({});
1132
- function resolveOptionValues(definition, chosen, context = {}) {
1133
- const options = definition.options;
1134
- if (options === void 0 || options.length === 0) return NO_VALUES;
1135
- const values = {};
1136
- for (const option of options) {
1137
- const picked = chosen?.[option.key];
1138
- const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked, context)) ?? defaultOptionValue(option, context);
1139
- if (value !== void 0) values[option.key] = value;
1140
- }
1141
- return values;
1424
+ function hideWidget(layout, key) {
1425
+ if (isHidden(layout, key)) return layout;
1426
+ return { ...layout, hidden: [...layout.hidden, key] };
1142
1427
  }
1143
- function sameValue(a, b) {
1144
- if (Array.isArray(a) && Array.isArray(b)) {
1145
- return a.length === b.length && a.every((item, i) => item === b[i]);
1146
- }
1147
- return a === b;
1428
+ function showWidget(layout, key) {
1429
+ if (!isHidden(layout, key)) return layout;
1430
+ return {
1431
+ ...layout,
1432
+ hidden: layout.hidden.filter((k) => k !== key),
1433
+ minimized: layout.minimized.filter((k) => k !== key)
1434
+ };
1148
1435
  }
1149
- function sameOptionValues(a, b) {
1150
- const keys = Object.keys({ ...a, ...b });
1151
- return keys.every((key) => sameValue(a[key], b[key]));
1436
+ function toggleHidden(layout, key) {
1437
+ return isHidden(layout, key) ? showWidget(layout, key) : hideWidget(layout, key);
1152
1438
  }
1153
- function optionsCacheSuffix(definition, values, context = {}) {
1154
- const parts = [];
1155
- for (const option of definition.options ?? []) {
1156
- if (isClientApplied(option)) continue;
1157
- const value = values[option.key];
1158
- if (sameValue(value, defaultOptionValue(option, context))) continue;
1159
- parts.push(
1160
- `${option.key}=${encodeURIComponent(JSON.stringify(value ?? null))}`
1161
- );
1162
- }
1163
- parts.sort();
1164
- return parts.length === 0 ? "" : `?${parts.join("&")}`;
1439
+ function minimizeWidget(layout, key) {
1440
+ if (isMinimized(layout, key)) return layout;
1441
+ return { ...layout, minimized: [...layout.minimized, key] };
1165
1442
  }
1166
- function tableColumnIndex(key) {
1167
- const match = /^c(\d+)$/.exec(key);
1168
- return match === null ? void 0 : Number(match[1]);
1443
+ function restoreWidget(layout, key) {
1444
+ if (!isMinimized(layout, key)) return layout;
1445
+ return { ...layout, minimized: layout.minimized.filter((k) => k !== key) };
1169
1446
  }
1170
- function sortBarItems(items, sort) {
1171
- const sign = sort.direction === "desc" ? -1 : 1;
1172
- return items.map((item, index) => ({ item, index })).sort((a, b) => {
1173
- const order = sort.column === "value" ? a.item.value - b.item.value : a.item.label.localeCompare(b.item.label);
1174
- return order * sign || a.index - b.index;
1175
- }).map(({ item }) => item);
1447
+ function toggleMinimized(layout, key) {
1448
+ return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
1176
1449
  }
1177
- function applyClientOptions(definition, data, values) {
1178
- const options = definition.options ?? [];
1179
- const sortOption = options.find(
1180
- (o) => o.type === "sort" && o.apply === "client"
1181
- );
1182
- const columnsOption = options.find(
1183
- (o) => o.type === "columns" && o.apply === "client"
1184
- );
1185
- const sortValue = sortOption === void 0 ? void 0 : values[sortOption.key];
1186
- const columnsValue = columnsOption === void 0 ? void 0 : values[columnsOption.key];
1187
- let next = data;
1188
- if (typeof sortValue === "string") {
1189
- const sort = parseSortValue(sortValue);
1190
- if (sort !== void 0 && next.kind === "BAR_LIST") {
1191
- if (sort.column === "label" || sort.column === "value") {
1192
- next = { ...next, items: sortBarItems(next.items, sort) };
1193
- }
1194
- } else if (sort !== void 0 && next.kind === "TABLE") {
1195
- const index = tableColumnIndex(sort.column);
1196
- if (index !== void 0 && index < next.columns.length) {
1197
- const sorted = queryRows(tableDetailData(next), {
1198
- page: 1,
1199
- pageSize: Math.max(1, next.rows.length),
1200
- sort
1201
- });
1202
- if (sorted.ok) next = { ...next, rows: sorted.value.rows };
1203
- }
1204
- }
1450
+ function pruneLayout(definitions, layout) {
1451
+ const known = new Set(definitions.map((d) => d.key));
1452
+ const keep = (keys) => keys.every((k) => known.has(k)) ? keys : keys.filter((k) => known.has(k));
1453
+ return mapLayoutLists(layout, (lists) => ({
1454
+ order: keep(lists.order),
1455
+ hidden: keep(lists.hidden),
1456
+ minimized: keep(lists.minimized)
1457
+ }));
1458
+ }
1459
+ function mapLayoutLists(layout, change) {
1460
+ const top = change(layout);
1461
+ const pages = layout.pages?.map((page) => {
1462
+ const next = change(page);
1463
+ return next.order === page.order && next.hidden === page.hidden && next.minimized === page.minimized ? page : { ...page, ...next };
1464
+ });
1465
+ const pagesChanged = pages !== void 0 && pages.some((page, i) => page !== layout.pages?.[i]);
1466
+ if (top.order === layout.order && top.hidden === layout.hidden && top.minimized === layout.minimized && !pagesChanged) {
1467
+ return layout;
1205
1468
  }
1206
- if (Array.isArray(columnsValue) && next.kind === "TABLE") {
1207
- const table = next;
1208
- const indexes = columnsValue.map(tableColumnIndex).filter(
1209
- (index) => index !== void 0 && index < table.columns.length
1210
- );
1211
- if (indexes.length > 0) {
1212
- next = {
1213
- ...table,
1214
- columns: indexes.map((i) => table.columns[i]),
1215
- rows: table.rows.map((row) => indexes.map((i) => row[i]))
1216
- };
1217
- }
1469
+ return {
1470
+ ...layout,
1471
+ ...top,
1472
+ ...pages === void 0 ? {} : { pages }
1473
+ };
1474
+ }
1475
+ function enforceLocks(definitions, layout, options = {}) {
1476
+ if (options.overrideLocks === true) return layout;
1477
+ const noMove = /* @__PURE__ */ new Set();
1478
+ const noHide = /* @__PURE__ */ new Set();
1479
+ const noMinimize = /* @__PURE__ */ new Set();
1480
+ for (const definition of definitions) {
1481
+ const lock = resolveWidgetLock(definition);
1482
+ if (lock.move) noMove.add(definition.key);
1483
+ if (lock.hide) noHide.add(definition.key);
1484
+ if (lock.minimize) noMinimize.add(definition.key);
1218
1485
  }
1219
- return next;
1486
+ const without2 = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
1487
+ return mapLayoutLists(layout, (lists) => ({
1488
+ order: without2(lists.order, noMove),
1489
+ hidden: without2(lists.hidden, noHide),
1490
+ minimized: without2(lists.minimized, noMinimize)
1491
+ }));
1220
1492
  }
1221
1493
 
1222
1494
  // src/core/validate.ts
@@ -1229,7 +1501,7 @@ function describeType2(value) {
1229
1501
  }
1230
1502
  return typeof value;
1231
1503
  }
1232
- function isRecord(value) {
1504
+ function isRecord2(value) {
1233
1505
  return typeof value === "object" && value !== null && !Array.isArray(value);
1234
1506
  }
1235
1507
  function join(path, field) {
@@ -1311,7 +1583,7 @@ function requireOneOf(record, field, options, path, problems) {
1311
1583
  function eachRecord(items, path, problems, check) {
1312
1584
  items.forEach((item, index) => {
1313
1585
  const itemPath = `${path}[${index}]`;
1314
- if (isRecord(item)) {
1586
+ if (isRecord2(item)) {
1315
1587
  check(item, itemPath);
1316
1588
  } else {
1317
1589
  problems.push(
@@ -1460,7 +1732,7 @@ function checkPayloadFields(kind, data, problems) {
1460
1732
  }
1461
1733
  function checkDetailOption(detail, problems) {
1462
1734
  if (detail === void 0 || typeof detail === "boolean") return;
1463
- if (!isRecord(detail)) {
1735
+ if (!isRecord2(detail)) {
1464
1736
  problems.push(
1465
1737
  `detail must be true, false or an options object, got ${describeType2(detail)}.`
1466
1738
  );
@@ -1479,13 +1751,13 @@ function checkDetailOption(detail, problems) {
1479
1751
  }
1480
1752
  function checkTableControlsOption(controls, kind, problems) {
1481
1753
  if (controls === void 0 || controls === false) return;
1482
- if (typeof controls !== "boolean" && !isRecord(controls)) {
1754
+ if (typeof controls !== "boolean" && !isRecord2(controls)) {
1483
1755
  problems.push(
1484
1756
  `tableControls must be true, false or an options object, got ${describeType2(controls)}.`
1485
1757
  );
1486
1758
  return;
1487
1759
  }
1488
- if (isRecord(controls)) {
1760
+ if (isRecord2(controls)) {
1489
1761
  optionalBoolean(controls, "search", "tableControls", problems);
1490
1762
  optionalBoolean(controls, "sort", "tableControls", problems);
1491
1763
  }
@@ -1495,9 +1767,39 @@ function checkTableControlsOption(controls, kind, problems) {
1495
1767
  );
1496
1768
  }
1497
1769
  }
1770
+ function checkWidthLimits(value, problems) {
1771
+ for (const field of ["minWidth", "maxWidth"]) {
1772
+ const limit = value[field];
1773
+ if (limit !== void 0 && !isWidgetWidth(limit)) {
1774
+ problems.push(
1775
+ `${field} must be an integer from ${MIN_WIDGET_WIDTH} to ${MAX_WIDGET_WIDTH} (twelfths of the row), got ${typeof limit === "number" ? String(limit) : describeType2(limit)}.`
1776
+ );
1777
+ return;
1778
+ }
1779
+ }
1780
+ const { minWidth, maxWidth, width } = value;
1781
+ if (isWidgetWidth(minWidth) && isWidgetWidth(maxWidth) && minWidth > maxWidth) {
1782
+ problems.push(
1783
+ `minWidth (${minWidth}) must not be more than maxWidth (${maxWidth}).`
1784
+ );
1785
+ return;
1786
+ }
1787
+ if (isWidgetWidth(width)) {
1788
+ if (isWidgetWidth(minWidth) && width < minWidth) {
1789
+ problems.push(
1790
+ `width (${width}) must not be less than minWidth (${minWidth}).`
1791
+ );
1792
+ }
1793
+ if (isWidgetWidth(maxWidth) && width > maxWidth) {
1794
+ problems.push(
1795
+ `width (${width}) must not be more than maxWidth (${maxWidth}).`
1796
+ );
1797
+ }
1798
+ }
1799
+ }
1498
1800
  function checkLockedOption(locked, problems) {
1499
1801
  if (locked === void 0 || typeof locked === "boolean") return;
1500
- if (!isRecord(locked)) {
1802
+ if (!isRecord2(locked)) {
1501
1803
  problems.push(
1502
1804
  `locked must be true, false or an object, got ${describeType2(locked)}.`
1503
1805
  );
@@ -1506,6 +1808,7 @@ function checkLockedOption(locked, problems) {
1506
1808
  optionalBoolean(locked, "move", "locked", problems);
1507
1809
  optionalBoolean(locked, "hide", "locked", problems);
1508
1810
  optionalBoolean(locked, "minimize", "locked", problems);
1811
+ optionalBoolean(locked, "options", "locked", problems);
1509
1812
  }
1510
1813
  function checkOptionColumns(option, path, kind, problems) {
1511
1814
  const columns = option["columns"];
@@ -1518,7 +1821,7 @@ function checkOptionColumns(option, path, kind, problems) {
1518
1821
  const keys = [];
1519
1822
  columns.forEach((entry, index) => {
1520
1823
  const entryPath = `${path}.columns[${index}]`;
1521
- if (!isRecord(entry)) {
1824
+ if (!isRecord2(entry)) {
1522
1825
  problems.push(
1523
1826
  `${entryPath} must be an object, got ${describeType2(entry)}.`
1524
1827
  );
@@ -1543,7 +1846,7 @@ function checkOptionColumns(option, path, kind, problems) {
1543
1846
  return keys;
1544
1847
  }
1545
1848
  function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
1546
- if (!isRecord(option)) {
1849
+ if (!isRecord2(option)) {
1547
1850
  problems.push(`${path} must be an object, got ${describeType2(option)}.`);
1548
1851
  return;
1549
1852
  }
@@ -1575,7 +1878,7 @@ function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
1575
1878
  } else {
1576
1879
  choices.forEach((choice, index) => {
1577
1880
  const choicePath = `${path}.choices[${index}]`;
1578
- if (!isRecord(choice)) {
1881
+ if (!isRecord2(choice)) {
1579
1882
  problems.push(
1580
1883
  `${choicePath} must be an object, got ${describeType2(choice)}.`
1581
1884
  );
@@ -1665,7 +1968,7 @@ function checkOption(option, path, kind, seenKeys, seenTypes, problems) {
1665
1968
  } else {
1666
1969
  presets.forEach((preset, index) => {
1667
1970
  const presetPath = `${path}.presets[${index}]`;
1668
- if (!isRecord(preset)) {
1971
+ if (!isRecord2(preset)) {
1669
1972
  problems.push(
1670
1973
  `${presetPath} must be an object, got ${describeType2(preset)}.`
1671
1974
  );
@@ -1792,7 +2095,7 @@ function formatProblems(prefix, problems) {
1792
2095
  }
1793
2096
  function validateWidgetData(value, options = {}) {
1794
2097
  const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
1795
- if (!isRecord(value)) {
2098
+ if (!isRecord2(value)) {
1796
2099
  return err(`${subject}: must be an object, got ${describeType2(value)}.`);
1797
2100
  }
1798
2101
  if (value["empty"] === true) {
@@ -1822,7 +2125,7 @@ function validateWidgetData(value, options = {}) {
1822
2125
  return ok(value);
1823
2126
  }
1824
2127
  function validateWidgetDefinition(value) {
1825
- if (!isRecord(value)) {
2128
+ if (!isRecord2(value)) {
1826
2129
  return err(
1827
2130
  `Widget definition: must be an object, got ${describeType2(value)}.`
1828
2131
  );
@@ -1856,6 +2159,7 @@ function validateWidgetDefinition(value) {
1856
2159
  `width must be an integer from ${MIN_WIDGET_WIDTH} to ${MAX_WIDGET_WIDTH} (twelfths of the row), got ${typeof value["width"] === "number" ? String(value["width"]) : describeType2(value["width"])}.`
1857
2160
  );
1858
2161
  }
2162
+ checkWidthLimits(value, problems);
1859
2163
  checkDetailOption(value["detail"], problems);
1860
2164
  checkTableControlsOption(value["tableControls"], value["kind"], problems);
1861
2165
  checkLockedOption(value["locked"], problems);
@@ -2517,7 +2821,7 @@ function withoutLockField(definition) {
2517
2821
  delete copy.locked;
2518
2822
  return copy;
2519
2823
  }
2520
- function uniqueTitle(taken, title, index) {
2824
+ function uniqueTitle2(taken, title, index) {
2521
2825
  let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
2522
2826
  if (base === "") base = `Page ${index + 1}`;
2523
2827
  let candidate = base;
@@ -2538,9 +2842,18 @@ function samePages(a, b) {
2538
2842
  }
2539
2843
  function normalizeLayout(definitions, layout, options = {}) {
2540
2844
  const known = pruneLayout(definitions, layout);
2541
- const locked = enforceLocks(definitions, known, options);
2845
+ const enforced = enforceLocks(definitions, known, options);
2846
+ const locked = normalizeSettings(
2847
+ definitions,
2848
+ enforced,
2849
+ options
2850
+ );
2542
2851
  if (!hasPages(locked)) return locked === layout ? layout : locked;
2543
- const defs = options.overrideLocks === true ? definitions.map(withoutLockField) : definitions;
2852
+ const defs = effectiveDefinitions(
2853
+ options.overrideLocks === true ? definitions.map(withoutLockField) : definitions,
2854
+ locked,
2855
+ options
2856
+ );
2544
2857
  const taken = /* @__PURE__ */ new Set();
2545
2858
  const seenKeys = /* @__PURE__ */ new Set();
2546
2859
  const held = /* @__PURE__ */ new Set();
@@ -2553,7 +2866,7 @@ function normalizeLayout(definitions, layout, options = {}) {
2553
2866
  ];
2554
2867
  const next = {
2555
2868
  ...page,
2556
- title: uniqueTitle(taken, page.title, index),
2869
+ title: uniqueTitle2(taken, page.title, index),
2557
2870
  order: keep(page.order),
2558
2871
  hidden: keep(page.hidden),
2559
2872
  minimized: keep(page.minimized)
@@ -2641,7 +2954,7 @@ function normalizeLayout(definitions, layout, options = {}) {
2641
2954
  }
2642
2955
  if (!placed) {
2643
2956
  const key = nextPageKey(work());
2644
- const title = uniqueTitle(
2957
+ const title = uniqueTitle2(
2645
2958
  new Set(pages.map((p) => normalizedTitle(p.title))),
2646
2959
  `Page ${pages.length + 1}`,
2647
2960
  pages.length
@@ -2653,7 +2966,7 @@ function normalizeLayout(definitions, layout, options = {}) {
2653
2966
  }
2654
2967
  }
2655
2968
  const result = work();
2656
- return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) ? layout : result;
2969
+ return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) && locked.settings === layout.settings ? layout : result;
2657
2970
  }
2658
2971
 
2659
2972
  // src/core/scale.ts
@@ -3501,9 +3814,11 @@ exports.MAX_GRAPH_SERIES = MAX_GRAPH_SERIES;
3501
3814
  exports.MAX_PAGE_ROWS = MAX_PAGE_ROWS;
3502
3815
  exports.MAX_PAGE_TITLE_LENGTH = MAX_PAGE_TITLE_LENGTH;
3503
3816
  exports.MAX_PARSED_PAGES = MAX_PARSED_PAGES;
3817
+ exports.MAX_PARSED_SETTINGS = MAX_PARSED_SETTINGS;
3504
3818
  exports.MAX_REPORTED_PROBLEMS = MAX_REPORTED_PROBLEMS;
3505
3819
  exports.MAX_TEXT_OPTION_LENGTH = MAX_TEXT_OPTION_LENGTH;
3506
3820
  exports.MAX_WIDGET_OPTIONS = MAX_WIDGET_OPTIONS;
3821
+ exports.MAX_WIDGET_TITLE_LENGTH = MAX_WIDGET_TITLE_LENGTH;
3507
3822
  exports.MAX_WIDGET_WIDTH = MAX_WIDGET_WIDTH;
3508
3823
  exports.MIN_WIDGET_WIDTH = MIN_WIDGET_WIDTH;
3509
3824
  exports.OPTION_KEY_PATTERN = OPTION_KEY_PATTERN;
@@ -3516,9 +3831,11 @@ exports.WIDGET_SIZES = WIDGET_SIZES;
3516
3831
  exports.WIDTH_COLUMNS = WIDTH_COLUMNS;
3517
3832
  exports.addPage = addPage;
3518
3833
  exports.applyClientOptions = applyClientOptions;
3834
+ exports.applySettings = applySettings;
3519
3835
  exports.assignPages = assignPages;
3520
3836
  exports.baseColumnSpan = baseColumnSpan;
3521
3837
  exports.cacheKeyFor = cacheKeyFor;
3838
+ exports.clearWidgetSettings = clearWidgetSettings;
3522
3839
  exports.coerceOptionValue = coerceOptionValue;
3523
3840
  exports.createLayoutPersistence = createLayoutPersistence;
3524
3841
  exports.createTheme = createTheme;
@@ -3528,6 +3845,7 @@ exports.defaultOptionValue = defaultOptionValue;
3528
3845
  exports.defineWidget = defineWidget;
3529
3846
  exports.deriveDetailData = deriveDetailData;
3530
3847
  exports.describeUnknownError = describeUnknownError;
3848
+ exports.effectiveDefinitions = effectiveDefinitions;
3531
3849
  exports.emptyTextFor = emptyTextFor;
3532
3850
  exports.emptyWidget = emptyWidget;
3533
3851
  exports.enforceLocks = enforceLocks;
@@ -3548,11 +3866,14 @@ exports.hiddenWidgets = hiddenWidgets;
3548
3866
  exports.hideWidget = hideWidget;
3549
3867
  exports.isClientApplied = isClientApplied;
3550
3868
  exports.isColumnKeyFor = isColumnKeyFor;
3869
+ exports.isFlexible = isFlexible;
3551
3870
  exports.isHidden = isHidden;
3552
3871
  exports.isIsoInterval = isIsoInterval;
3553
3872
  exports.isMinimized = isMinimized;
3873
+ exports.isOptionsLocked = isOptionsLocked;
3554
3874
  exports.isSafeHref = isSafeHref;
3555
3875
  exports.isSafeImageUrl = isSafeImageUrl;
3876
+ exports.isTitleInUse = isTitleInUse;
3556
3877
  exports.isWidgetEmptyState = isWidgetEmptyState;
3557
3878
  exports.isWidgetKind = isWidgetKind;
3558
3879
  exports.isWidgetVisibleTo = isWidgetVisibleTo;
@@ -3573,8 +3894,11 @@ exports.nextPageKey = nextPageKey;
3573
3894
  exports.niceDomain = niceDomain;
3574
3895
  exports.niceStep = niceStep;
3575
3896
  exports.normalizeLayout = normalizeLayout;
3897
+ exports.normalizeSettings = normalizeSettings;
3898
+ exports.normalizeTitle = normalizeTitle;
3576
3899
  exports.ok = ok;
3577
3900
  exports.optionValuesFor = optionValuesFor;
3901
+ exports.optionValuesFromLayout = optionValuesFromLayout;
3578
3902
  exports.optionsCacheSuffix = optionsCacheSuffix;
3579
3903
  exports.pageLayout = pageLayout;
3580
3904
  exports.pageList = pageList;
@@ -3583,6 +3907,7 @@ exports.pageRoom = pageRoom;
3583
3907
  exports.pageWidgets = pageWidgets;
3584
3908
  exports.parseDetailQuery = parseDetailQuery;
3585
3909
  exports.parseLayout = parseLayout;
3910
+ exports.parseSettings = parseSettings;
3586
3911
  exports.parseSortValue = parseSortValue;
3587
3912
  exports.percentChange = percentChange;
3588
3913
  exports.placeRows = placeRows;
@@ -3605,6 +3930,8 @@ exports.restoreWidget = restoreWidget;
3605
3930
  exports.sameOptionValues = sameOptionValues;
3606
3931
  exports.serializeDetailQuery = serializeDetailQuery;
3607
3932
  exports.serializeLayout = serializeLayout;
3933
+ exports.serializeSettings = serializeSettings;
3934
+ exports.setWidgetSettings = setWidgetSettings;
3608
3935
  exports.shareOf = shareOf;
3609
3936
  exports.showWidget = showWidget;
3610
3937
  exports.sortDefinitions = sortDefinitions;
@@ -3619,8 +3946,11 @@ exports.validateDetailData = validateDetailData;
3619
3946
  exports.validateWidgetData = validateWidgetData;
3620
3947
  exports.validateWidgetDefinition = validateWidgetDefinition;
3621
3948
  exports.visibleWidgets = visibleWidgets;
3949
+ exports.widgetOptionValues = widgetOptionValues;
3622
3950
  exports.widgetsFor = widgetsFor;
3623
3951
  exports.widthForSize = widthForSize;
3952
+ exports.widthRange = widthRange;
3953
+ exports.widthShareLabel = widthShareLabel;
3624
3954
  exports.withEncoding = withEncoding;
3625
3955
  exports.withFallback = withFallback;
3626
3956
  exports.withLogging = withLogging;