@richardmcquiston01/dashboard-widgets-toolkit 0.9.0 → 0.10.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,12 @@ 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
+ }));
849
941
  }
850
942
 
851
943
  // src/core/validate.ts
@@ -1210,6 +1302,13 @@ function validateWidgetDefinition(value) {
1210
1302
  checkDetailOption(value["detail"], problems);
1211
1303
  checkTableControlsOption(value["tableControls"], value["kind"], problems);
1212
1304
  checkLockedOption(value["locked"], problems);
1305
+ if (value["page"] !== void 0) {
1306
+ if (typeof value["page"] !== "string" || value["page"].trim() === "") {
1307
+ problems.push(
1308
+ `page must be a non-empty page key or title, got ${describeType2(value["page"])}.`
1309
+ );
1310
+ }
1311
+ }
1213
1312
  if (value["fill"] !== void 0) {
1214
1313
  requireOneOf(value, "fill", WIDGET_FILLS, "", problems);
1215
1314
  }
@@ -1463,6 +1562,475 @@ function createWidgetLoader(options) {
1463
1562
  };
1464
1563
  }
1465
1564
 
1565
+ // src/core/pages.ts
1566
+ var DEFAULT_PAGE_KEY = "default";
1567
+ var DEFAULT_PAGE_ROWS = 4;
1568
+ function rowsAllowed(page, options) {
1569
+ const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
1570
+ return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
1571
+ }
1572
+ function hasPages(layout) {
1573
+ return layout.pages !== void 0 && layout.pages.length > 0;
1574
+ }
1575
+ function pageList(layout) {
1576
+ if (layout.pages !== void 0 && layout.pages.length > 0) {
1577
+ return layout.pages;
1578
+ }
1579
+ return [
1580
+ {
1581
+ key: DEFAULT_PAGE_KEY,
1582
+ title: "Page 1",
1583
+ order: layout.order,
1584
+ hidden: layout.hidden,
1585
+ minimized: layout.minimized
1586
+ }
1587
+ ];
1588
+ }
1589
+ function withExplicitPages(layout) {
1590
+ if (hasPages(layout)) return layout;
1591
+ return {
1592
+ ...layout,
1593
+ order: [],
1594
+ hidden: [],
1595
+ minimized: [],
1596
+ pages: pageList(layout)
1597
+ };
1598
+ }
1599
+ function sameKeys(a, b) {
1600
+ return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
1601
+ }
1602
+ function pageLayout(layout, pageKey) {
1603
+ if (!hasPages(layout))
1604
+ return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
1605
+ const page = layout.pages?.find(
1606
+ (p) => p.key === pageKey
1607
+ );
1608
+ if (page === void 0) return void 0;
1609
+ const { pages, ...rest } = layout;
1610
+ return {
1611
+ ...rest,
1612
+ order: page.order,
1613
+ hidden: page.hidden,
1614
+ minimized: page.minimized
1615
+ };
1616
+ }
1617
+ function withPageLayout(layout, pageKey, next) {
1618
+ const current = pageLayout(layout, pageKey);
1619
+ if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
1620
+ return layout;
1621
+ }
1622
+ if (!hasPages(layout)) {
1623
+ return {
1624
+ ...layout,
1625
+ order: next.order,
1626
+ hidden: next.hidden,
1627
+ minimized: next.minimized
1628
+ };
1629
+ }
1630
+ return {
1631
+ ...layout,
1632
+ pages: (layout.pages ?? []).map(
1633
+ (page) => page.key === pageKey ? {
1634
+ ...page,
1635
+ order: next.order,
1636
+ hidden: next.hidden,
1637
+ minimized: next.minimized
1638
+ } : page
1639
+ )
1640
+ };
1641
+ }
1642
+ function normalizedTitle(title) {
1643
+ return title.trim().normalize("NFKC").toLowerCase();
1644
+ }
1645
+ function resolvePageKey(layout, reference) {
1646
+ if (reference === void 0 || reference === "") return void 0;
1647
+ const pages = pageList(layout);
1648
+ const byKey = pages.find((p) => p.key === reference);
1649
+ if (byKey !== void 0) return byKey.key;
1650
+ const wanted = normalizedTitle(reference);
1651
+ return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
1652
+ }
1653
+ function homePageKey(layout, item) {
1654
+ return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
1655
+ }
1656
+ function assignPages(definitions, layout) {
1657
+ const pages = pageList(layout);
1658
+ const assigned = new Map(pages.map((p) => [p.key, []]));
1659
+ for (const definition of definitions) {
1660
+ let pageKey;
1661
+ if (!resolveWidgetLock(definition).move) {
1662
+ pageKey = pages.find(
1663
+ (p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
1664
+ )?.key;
1665
+ }
1666
+ assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
1667
+ }
1668
+ return assigned;
1669
+ }
1670
+ function pageOf(definitions, layout, key) {
1671
+ for (const [pageKey, widgets] of assignPages(definitions, layout)) {
1672
+ if (widgets.some((w) => w.key === key)) return pageKey;
1673
+ }
1674
+ return void 0;
1675
+ }
1676
+ function pageWidgets(definitions, layout, pageKey) {
1677
+ const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
1678
+ const view = pageLayout(layout, pageKey);
1679
+ return view === void 0 ? [] : visibleWidgets(onPage, view);
1680
+ }
1681
+ function pageRoom(definitions, layout, pageKey, options = {}) {
1682
+ const page = pageList(layout).find(
1683
+ (p) => p.key === pageKey
1684
+ );
1685
+ if (page === void 0) return void 0;
1686
+ const maxRows = rowsAllowed(page, options);
1687
+ const rowsUsed = placeRows(
1688
+ pageWidgets(definitions, layout, pageKey)
1689
+ ).length;
1690
+ return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
1691
+ }
1692
+ function describePage(page) {
1693
+ return `Page "${page.title}"`;
1694
+ }
1695
+ function titleOf(item) {
1696
+ return item.title ?? item.key;
1697
+ }
1698
+ function checkTitle(layout, title, exceptKey) {
1699
+ const trimmed = title.trim();
1700
+ if (trimmed === "") return err("A page title must not be empty.");
1701
+ if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
1702
+ return err(
1703
+ `Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
1704
+ );
1705
+ }
1706
+ const wanted = normalizedTitle(trimmed);
1707
+ const clash = pageList(layout).find(
1708
+ (p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
1709
+ );
1710
+ if (clash !== void 0) {
1711
+ return err(`Another page is already called "${clash.title}".`);
1712
+ }
1713
+ return ok(trimmed);
1714
+ }
1715
+ function nextPageKey(layout) {
1716
+ const keys = new Set(pageList(layout).map((p) => p.key));
1717
+ let n = 1;
1718
+ while (keys.has(`page-${n}`)) n += 1;
1719
+ return `page-${n}`;
1720
+ }
1721
+ function nextPageTitle(layout) {
1722
+ const used = new Set(
1723
+ pageList(layout).map((p) => normalizedTitle(p.title))
1724
+ );
1725
+ let n = pageList(layout).length + 1;
1726
+ while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
1727
+ return `Page ${n}`;
1728
+ }
1729
+ function addPage(layout, options = {}) {
1730
+ if (pageList(layout).length >= MAX_PARSED_PAGES) {
1731
+ return err(
1732
+ `The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
1733
+ );
1734
+ }
1735
+ const key = options.key ?? nextPageKey(layout);
1736
+ if (pageList(layout).some((p) => p.key === key)) {
1737
+ return err(`A page with the key "${key}" already exists.`);
1738
+ }
1739
+ const title = checkTitle(
1740
+ layout,
1741
+ options.title ?? nextPageTitle(layout)
1742
+ );
1743
+ if (!title.ok) return title;
1744
+ const base = withExplicitPages(layout);
1745
+ const page = {
1746
+ key,
1747
+ title: title.value,
1748
+ order: [],
1749
+ hidden: [],
1750
+ minimized: []
1751
+ };
1752
+ return ok({ ...base, pages: [...base.pages ?? [], page] });
1753
+ }
1754
+ function renamePage(layout, pageKey, title) {
1755
+ const page = pageList(layout).find(
1756
+ (p) => p.key === pageKey
1757
+ );
1758
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
1759
+ const checked = checkTitle(layout, title, pageKey);
1760
+ if (!checked.ok) return checked;
1761
+ if (checked.value === page.title) return ok(layout);
1762
+ const base = withExplicitPages(layout);
1763
+ return ok({
1764
+ ...base,
1765
+ pages: (base.pages ?? []).map(
1766
+ (p) => p.key === pageKey ? { ...p, title: checked.value } : p
1767
+ )
1768
+ });
1769
+ }
1770
+ function movePage(layout, pageKey, offset) {
1771
+ const pages = layout.pages ?? [];
1772
+ const from = pages.findIndex((p) => p.key === pageKey);
1773
+ if (from === -1) return layout;
1774
+ const to = Math.max(
1775
+ 0,
1776
+ Math.min(pages.length - 1, from + Math.trunc(offset))
1777
+ );
1778
+ if (to === from) return layout;
1779
+ const next = [...pages];
1780
+ const [moved] = next.splice(from, 1);
1781
+ next.splice(to, 0, moved);
1782
+ return { ...layout, pages: next };
1783
+ }
1784
+ function overflowing(definitions, layout, options) {
1785
+ return pageList(layout).find(
1786
+ (page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
1787
+ );
1788
+ }
1789
+ function removePage(definitions, layout, pageKey, options = {}) {
1790
+ const pages = pageList(layout);
1791
+ const index = pages.findIndex((p) => p.key === pageKey);
1792
+ const page = pages[index];
1793
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
1794
+ if (pages.length === 1) {
1795
+ return err(
1796
+ `${describePage(page)} is the only page, so it can't be removed.`
1797
+ );
1798
+ }
1799
+ const target = pages[index > 0 ? index - 1 : 1];
1800
+ const merged = {
1801
+ ...layout,
1802
+ pages: pages.filter((p) => p.key !== pageKey).map(
1803
+ (p) => p.key === target.key ? {
1804
+ ...p,
1805
+ order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
1806
+ hidden: [...p.hidden, ...page.hidden],
1807
+ minimized: [...p.minimized, ...page.minimized]
1808
+ } : p
1809
+ )
1810
+ };
1811
+ const full = overflowing(
1812
+ definitions,
1813
+ merged,
1814
+ options
1815
+ );
1816
+ if (full !== void 0) {
1817
+ return err(
1818
+ `${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.`
1819
+ );
1820
+ }
1821
+ return ok(merged);
1822
+ }
1823
+ function without(keys, key) {
1824
+ return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
1825
+ }
1826
+ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
1827
+ const widget = definitions.find((d) => d.key === widgetKey);
1828
+ if (widget === void 0) {
1829
+ return err(
1830
+ `Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
1831
+ );
1832
+ }
1833
+ const pages = pageList(layout);
1834
+ const target = pages.find((p) => p.key === targetKey);
1835
+ if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
1836
+ const sourceKey = pageOf(definitions, layout, widgetKey);
1837
+ if (sourceKey === targetKey) return ok(layout);
1838
+ if (resolveWidgetLock(widget).move) {
1839
+ return err(
1840
+ `Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
1841
+ );
1842
+ }
1843
+ const base = withExplicitPages(layout);
1844
+ const source = (base.pages ?? []).find(
1845
+ (p) => p.key === sourceKey
1846
+ );
1847
+ const hidden = source.hidden.includes(widgetKey);
1848
+ const minimized = source.minimized.includes(widgetKey);
1849
+ const targetOrder = pageWidgets(
1850
+ definitions,
1851
+ base,
1852
+ targetKey
1853
+ ).map((d) => d.key);
1854
+ const moved = {
1855
+ ...base,
1856
+ pages: (base.pages ?? []).map((p) => {
1857
+ if (p.key === sourceKey) {
1858
+ return {
1859
+ ...p,
1860
+ order: without(p.order, widgetKey),
1861
+ hidden: without(p.hidden, widgetKey),
1862
+ minimized: without(p.minimized, widgetKey)
1863
+ };
1864
+ }
1865
+ if (p.key === targetKey) {
1866
+ return {
1867
+ ...p,
1868
+ order: hidden ? p.order : [...targetOrder, widgetKey],
1869
+ hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
1870
+ minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
1871
+ };
1872
+ }
1873
+ return p;
1874
+ })
1875
+ };
1876
+ if (!hidden) {
1877
+ const allowed = rowsAllowed(target, options);
1878
+ const needed = placeRows(
1879
+ pageWidgets(definitions, moved, targetKey)
1880
+ ).length;
1881
+ if (needed > allowed) {
1882
+ return err(
1883
+ `${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
1884
+ );
1885
+ }
1886
+ }
1887
+ return ok(moved);
1888
+ }
1889
+ function withoutLockField(definition) {
1890
+ if (definition.locked === void 0) return definition;
1891
+ const copy = { ...definition };
1892
+ delete copy.locked;
1893
+ return copy;
1894
+ }
1895
+ function uniqueTitle(taken, title, index) {
1896
+ let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
1897
+ if (base === "") base = `Page ${index + 1}`;
1898
+ let candidate = base;
1899
+ let n = 1;
1900
+ while (taken.has(normalizedTitle(candidate))) {
1901
+ const suffix = ` (#${n})`;
1902
+ candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
1903
+ n += 1;
1904
+ }
1905
+ taken.add(normalizedTitle(candidate));
1906
+ return candidate;
1907
+ }
1908
+ function samePages(a, b) {
1909
+ return a.length === b.length && a.every((p, i) => {
1910
+ const q = b[i];
1911
+ 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);
1912
+ });
1913
+ }
1914
+ function normalizeLayout(definitions, layout, options = {}) {
1915
+ const known = pruneLayout(definitions, layout);
1916
+ const locked = enforceLocks(definitions, known, options);
1917
+ if (!hasPages(locked)) return locked === layout ? layout : locked;
1918
+ const defs = options.overrideLocks === true ? definitions.map(withoutLockField) : definitions;
1919
+ const taken = /* @__PURE__ */ new Set();
1920
+ const seenKeys = /* @__PURE__ */ new Set();
1921
+ const held = /* @__PURE__ */ new Set();
1922
+ let pages = [];
1923
+ (locked.pages ?? []).forEach((page, index) => {
1924
+ if (seenKeys.has(page.key)) return;
1925
+ seenKeys.add(page.key);
1926
+ const keep = (list) => [
1927
+ ...new Set(list.filter((k) => !held.has(k)))
1928
+ ];
1929
+ const next = {
1930
+ ...page,
1931
+ title: uniqueTitle(taken, page.title, index),
1932
+ order: keep(page.order),
1933
+ hidden: keep(page.hidden),
1934
+ minimized: keep(page.minimized)
1935
+ };
1936
+ for (const k of [...next.order, ...next.hidden]) held.add(k);
1937
+ pages.push(next);
1938
+ });
1939
+ const work = () => ({ ...locked, pages });
1940
+ const pinned = defs.filter((d) => resolveWidgetLock(d).move);
1941
+ for (const widget of pinned) {
1942
+ const home = homePageKey(work(), widget);
1943
+ const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
1944
+ const wasHidden = pages.some((p) => keysHere(p, "hidden"));
1945
+ const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
1946
+ if (!wasHidden && !wasMinimized) continue;
1947
+ pages = pages.map((p) => {
1948
+ let next = p;
1949
+ if (keysHere(p, "hidden")) {
1950
+ next = { ...next, hidden: without(next.hidden, widget.key) };
1951
+ }
1952
+ if (keysHere(p, "minimized")) {
1953
+ next = { ...next, minimized: without(next.minimized, widget.key) };
1954
+ }
1955
+ if (p.key === home) {
1956
+ next = {
1957
+ ...next,
1958
+ hidden: wasHidden && !next.hidden.includes(widget.key) ? [...next.hidden, widget.key] : next.hidden,
1959
+ minimized: wasMinimized && !next.minimized.includes(widget.key) ? [...next.minimized, widget.key] : next.minimized
1960
+ };
1961
+ }
1962
+ return next;
1963
+ });
1964
+ }
1965
+ const placedKeys = /* @__PURE__ */ new Set();
1966
+ for (const p of pages) {
1967
+ for (const k of [...p.order, ...p.hidden]) placedKeys.add(k);
1968
+ }
1969
+ const pinnedKeys = new Set(pinned.map((d) => d.key));
1970
+ const unplaced = defs.filter((d) => !placedKeys.has(d.key) && !pinnedKeys.has(d.key)).map((definition, index) => ({ definition, index })).sort(
1971
+ (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
1972
+ ).map(({ definition }) => definition);
1973
+ for (const widget of unplaced) {
1974
+ const home = homePageKey(work(), widget);
1975
+ pages = pages.map(
1976
+ (p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
1977
+ );
1978
+ }
1979
+ for (let i = 0; i < pages.length; i += 1) {
1980
+ const page = pages[i];
1981
+ const shown = pageWidgets(defs, work(), page.key);
1982
+ const fits = fitCount(shown, rowsAllowed(page, options));
1983
+ if (fits >= shown.length) continue;
1984
+ const moving = shown.slice(fits).filter((d) => !pinnedKeys.has(d.key)).map((d) => d.key);
1985
+ if (moving.length === 0) continue;
1986
+ const moveSet = new Set(moving);
1987
+ const movedMinimized = page.minimized.filter(
1988
+ (k) => moveSet.has(k)
1989
+ );
1990
+ pages = pages.map(
1991
+ (p) => p.key === page.key ? {
1992
+ ...p,
1993
+ order: p.order.filter((k) => !moveSet.has(k)),
1994
+ minimized: p.minimized.filter((k) => !moveSet.has(k))
1995
+ } : p
1996
+ );
1997
+ let placed = false;
1998
+ for (let j = i + 1; j < pages.length && !placed; j += 1) {
1999
+ const candidate = pages[j];
2000
+ const trial = pages.map(
2001
+ (p) => p.key === candidate.key ? {
2002
+ ...p,
2003
+ order: [...moving, ...p.order],
2004
+ minimized: [...p.minimized, ...movedMinimized]
2005
+ } : p
2006
+ );
2007
+ const items = pageWidgets(
2008
+ defs,
2009
+ { ...locked, pages: trial },
2010
+ candidate.key
2011
+ );
2012
+ if (placeRows(items).length <= rowsAllowed(candidate, options)) {
2013
+ pages = trial;
2014
+ placed = true;
2015
+ }
2016
+ }
2017
+ if (!placed) {
2018
+ const key = nextPageKey(work());
2019
+ const title = uniqueTitle(
2020
+ new Set(pages.map((p) => normalizedTitle(p.title))),
2021
+ `Page ${pages.length + 1}`,
2022
+ pages.length
2023
+ );
2024
+ pages = [
2025
+ ...pages,
2026
+ { key, title, order: moving, hidden: [], minimized: movedMinimized }
2027
+ ];
2028
+ }
2029
+ }
2030
+ const result = work();
2031
+ return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) ? layout : result;
2032
+ }
2033
+
1466
2034
  // src/core/scale.ts
1467
2035
  function niceStep(value) {
1468
2036
  if (!(value > 0) || !Number.isFinite(value)) return 1;
@@ -1493,6 +2061,6 @@ function labelStride(count, maxLabels = 6) {
1493
2061
  return Math.max(1, Math.ceil(count / Math.max(1, maxLabels)));
1494
2062
  }
1495
2063
 
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 };
2064
+ export { DEFAULT_COMPARISON_LABEL, DEFAULT_CURRENCY, DEFAULT_DETAIL_PAGE_SIZE, DEFAULT_EMPTY_TEXT, DEFAULT_PAGE_KEY, DEFAULT_PAGE_ROWS, DETAIL_MODES, EMPTY_LAYOUT, MAX_DETAIL_PAGE_SIZE, MAX_GRAPH_SERIES, MAX_PAGE_ROWS, MAX_PAGE_TITLE_LENGTH, MAX_PARSED_PAGES, MAX_REPORTED_PROBLEMS, MAX_WIDGET_WIDTH, MIN_WIDGET_WIDTH, VALUE_FORMATS, WIDGET_FILLS, WIDGET_KINDS, WIDGET_SIZES, WIDTH_COLUMNS, addPage, assignPages, baseColumnSpan, cacheKeyFor, createWidgetLoader, defaultDetailQuery, defineWidget, deriveDetailData, describeUnknownError, emptyTextFor, emptyWidget, enforceLocks, err, failedWidget, fillColumnSpans, fillWidthSpans, fillsHeight, fillsWidth, fitCount, formatCount, formatPercentChange, formatValue, fromLegacyDefinition, hasPages, hiddenWidgets, hideWidget, isHidden, isMinimized, isSafeHref, isSafeImageUrl, isWidgetEmptyState, isWidgetKind, isWidgetVisibleTo, isWidgetWidth, itemWidth, kpiDelta, labelStride, loadingWidgets, minimizeWidget, movePage, moveWidget, moveWidgetBy, moveWidgetDown, moveWidgetToPage, moveWidgetUp, nextPageKey, niceDomain, niceStep, normalizeLayout, ok, pageLayout, pageList, pageOf, pageRoom, pageWidgets, parseDetailQuery, parseLayout, percentChange, placeRows, pruneLayout, queryRows, removePage, renamePage, resolveDetailOptions, resolvePageKey, resolvePayload, resolveTableControls, resolveWidget, resolveWidgetLock, resolveWidgets, restoreWidget, serializeDetailQuery, serializeLayout, shareOf, showWidget, sortDefinitions, tableDetailData, toggleHidden, toggleMinimized, upgradeLegacyWidgetData, usesWidthColumns, validateDetailData, validateWidgetData, validateWidgetDefinition, visibleWidgets, widgetsFor, widthForSize, withPageLayout };
1497
2065
  //# sourceMappingURL=core.js.map
1498
2066
  //# sourceMappingURL=core.js.map