@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.cjs CHANGED
@@ -634,21 +634,44 @@ function fillColumnSpans(items, columns, largeSpan = 2) {
634
634
  function fillWidthSpans(items) {
635
635
  return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
636
636
  }
637
+ function placeRows(items) {
638
+ return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
639
+ }
640
+ function fitCount(items, maxRows) {
641
+ const rows = placeRows(items);
642
+ let count = 0;
643
+ for (const row of rows.slice(0, Math.max(0, Math.floor(maxRows)))) {
644
+ count += row.keys.length;
645
+ }
646
+ return count;
647
+ }
637
648
  function placeAndFill(items, columns, spanOf) {
649
+ return placeAndFillRows(items, columns, spanOf).spans;
650
+ }
651
+ function placeAndFillRows(items, columns, spanOf) {
638
652
  const trackCount = Math.max(1, Math.floor(columns));
639
653
  const spans = /* @__PURE__ */ new Map();
654
+ const rows = [];
640
655
  let row = [];
641
656
  let used = 0;
642
657
  function closeRow() {
658
+ if (row.length === 0) return;
643
659
  const fillers = row.filter(({ item }) => fillsWidth(item.fill));
644
660
  const leftover = trackCount - used;
661
+ const finalSpans = /* @__PURE__ */ new Map();
645
662
  if (fillers.length > 0) {
646
663
  const share = Math.floor(leftover / fillers.length);
647
664
  const remainder = leftover % fillers.length;
648
665
  fillers.forEach(({ item, span }, index) => {
649
- spans.set(item.key, span + share + (index < remainder ? 1 : 0));
666
+ const grown = span + share + (index < remainder ? 1 : 0);
667
+ spans.set(item.key, grown);
668
+ finalSpans.set(item.key, grown);
650
669
  });
651
670
  }
671
+ rows.push({
672
+ keys: row.map(({ item }) => item.key),
673
+ spans: row.map(({ item, span }) => finalSpans.get(item.key) ?? span)
674
+ });
652
675
  row = [];
653
676
  used = 0;
654
677
  }
@@ -659,10 +682,13 @@ function placeAndFill(items, columns, spanOf) {
659
682
  used += span;
660
683
  }
661
684
  closeRow();
662
- return spans;
685
+ return { spans, rows };
663
686
  }
664
687
 
665
688
  // src/core/layout.ts
689
+ var MAX_PAGE_TITLE_LENGTH = 30;
690
+ var MAX_PARSED_PAGES = 50;
691
+ var MAX_PAGE_ROWS = 20;
666
692
  var EMPTY_LAYOUT = Object.freeze({
667
693
  order: Object.freeze([]),
668
694
  hidden: Object.freeze([]),
@@ -692,16 +718,66 @@ function parseLayout(raw) {
692
718
  const order = uniqueStrings(record["order"]);
693
719
  const hidden = uniqueStrings(record["hidden"]);
694
720
  const minimized = uniqueStrings(record["minimized"]);
721
+ const pages = parsePages(record["pages"]);
722
+ if (pages.length > 0) {
723
+ return {
724
+ order: EMPTY_LAYOUT.order,
725
+ hidden: EMPTY_LAYOUT.hidden,
726
+ minimized: EMPTY_LAYOUT.minimized,
727
+ pages
728
+ };
729
+ }
695
730
  if (order.length === 0 && hidden.length === 0 && minimized.length === 0) {
696
731
  return EMPTY_LAYOUT;
697
732
  }
698
733
  return { order, hidden, minimized };
699
734
  }
735
+ function parsePages(value) {
736
+ if (!Array.isArray(value)) return [];
737
+ const pages = [];
738
+ const keys = /* @__PURE__ */ new Set();
739
+ for (const entry of value.slice(0, MAX_PARSED_PAGES)) {
740
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
741
+ continue;
742
+ }
743
+ const record = entry;
744
+ const key = record["key"];
745
+ if (typeof key !== "string" || key === "" || keys.has(key)) continue;
746
+ keys.add(key);
747
+ const rawTitle = record["title"];
748
+ const title = typeof rawTitle === "string" && rawTitle.trim() !== "" ? rawTitle.trim().slice(0, MAX_PAGE_TITLE_LENGTH) : `Page ${pages.length + 1}`;
749
+ const maxRows = record["maxRows"];
750
+ const held = /* @__PURE__ */ new Set();
751
+ for (const earlier of pages) {
752
+ for (const k of [...earlier.order, ...earlier.hidden]) held.add(k);
753
+ }
754
+ const keep = (list) => uniqueStrings(list).filter((k) => !held.has(k));
755
+ pages.push({
756
+ key,
757
+ title,
758
+ order: keep(record["order"]),
759
+ hidden: keep(record["hidden"]),
760
+ minimized: keep(record["minimized"]),
761
+ ...typeof maxRows === "number" && Number.isInteger(maxRows) && maxRows >= 1 && maxRows <= MAX_PAGE_ROWS ? { maxRows } : {}
762
+ });
763
+ }
764
+ return pages;
765
+ }
700
766
  function serializeLayout(layout) {
701
767
  return JSON.stringify({
702
768
  order: [...layout.order],
703
769
  hidden: [...layout.hidden],
704
- minimized: [...layout.minimized]
770
+ minimized: [...layout.minimized],
771
+ ...layout.pages === void 0 || layout.pages.length === 0 ? {} : {
772
+ pages: layout.pages.map((page) => ({
773
+ key: page.key,
774
+ title: page.title,
775
+ order: [...page.order],
776
+ hidden: [...page.hidden],
777
+ minimized: [...page.minimized],
778
+ ...page.maxRows === void 0 ? {} : { maxRows: page.maxRows }
779
+ }))
780
+ }
705
781
  });
706
782
  }
707
783
  function bySortOrder(items) {
@@ -824,13 +900,28 @@ function toggleMinimized(layout, key) {
824
900
  }
825
901
  function pruneLayout(definitions, layout) {
826
902
  const known = new Set(definitions.map((d) => d.key));
827
- const keep = (keys) => keys.filter((k) => known.has(k));
828
- const next = {
829
- order: keep(layout.order),
830
- hidden: keep(layout.hidden),
831
- minimized: keep(layout.minimized)
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 }
832
924
  };
833
- return next.order.length === layout.order.length && next.hidden.length === layout.hidden.length && next.minimized.length === layout.minimized.length ? layout : next;
834
925
  }
835
926
  function enforceLocks(definitions, layout, options = {}) {
836
927
  if (options.overrideLocks === true) return layout;
@@ -843,11 +934,12 @@ function enforceLocks(definitions, layout, options = {}) {
843
934
  if (lock.hide) noHide.add(definition.key);
844
935
  if (lock.minimize) noMinimize.add(definition.key);
845
936
  }
846
- const without = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
847
- const order = without(layout.order, noMove);
848
- const hidden = without(layout.hidden, noHide);
849
- const minimized = without(layout.minimized, noMinimize);
850
- return order === layout.order && hidden === layout.hidden && minimized === layout.minimized ? layout : { ...layout, order, hidden, minimized };
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
+ }));
851
943
  }
852
944
 
853
945
  // src/core/validate.ts
@@ -1212,6 +1304,13 @@ function validateWidgetDefinition(value) {
1212
1304
  checkDetailOption(value["detail"], problems);
1213
1305
  checkTableControlsOption(value["tableControls"], value["kind"], problems);
1214
1306
  checkLockedOption(value["locked"], problems);
1307
+ if (value["page"] !== void 0) {
1308
+ if (typeof value["page"] !== "string" || value["page"].trim() === "") {
1309
+ problems.push(
1310
+ `page must be a non-empty page key or title, got ${describeType2(value["page"])}.`
1311
+ );
1312
+ }
1313
+ }
1215
1314
  if (value["fill"] !== void 0) {
1216
1315
  requireOneOf(value, "fill", WIDGET_FILLS, "", problems);
1217
1316
  }
@@ -1465,6 +1564,475 @@ function createWidgetLoader(options) {
1465
1564
  };
1466
1565
  }
1467
1566
 
1567
+ // src/core/pages.ts
1568
+ var DEFAULT_PAGE_KEY = "default";
1569
+ var DEFAULT_PAGE_ROWS = 4;
1570
+ function rowsAllowed(page, options) {
1571
+ const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
1572
+ return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
1573
+ }
1574
+ function hasPages(layout) {
1575
+ return layout.pages !== void 0 && layout.pages.length > 0;
1576
+ }
1577
+ function pageList(layout) {
1578
+ if (layout.pages !== void 0 && layout.pages.length > 0) {
1579
+ return layout.pages;
1580
+ }
1581
+ return [
1582
+ {
1583
+ key: DEFAULT_PAGE_KEY,
1584
+ title: "Page 1",
1585
+ order: layout.order,
1586
+ hidden: layout.hidden,
1587
+ minimized: layout.minimized
1588
+ }
1589
+ ];
1590
+ }
1591
+ function withExplicitPages(layout) {
1592
+ if (hasPages(layout)) return layout;
1593
+ return {
1594
+ ...layout,
1595
+ order: [],
1596
+ hidden: [],
1597
+ minimized: [],
1598
+ pages: pageList(layout)
1599
+ };
1600
+ }
1601
+ function sameKeys(a, b) {
1602
+ return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
1603
+ }
1604
+ function pageLayout(layout, pageKey) {
1605
+ if (!hasPages(layout))
1606
+ return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
1607
+ const page = layout.pages?.find(
1608
+ (p) => p.key === pageKey
1609
+ );
1610
+ if (page === void 0) return void 0;
1611
+ const { pages, ...rest } = layout;
1612
+ return {
1613
+ ...rest,
1614
+ order: page.order,
1615
+ hidden: page.hidden,
1616
+ minimized: page.minimized
1617
+ };
1618
+ }
1619
+ function withPageLayout(layout, pageKey, next) {
1620
+ const current = pageLayout(layout, pageKey);
1621
+ if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
1622
+ return layout;
1623
+ }
1624
+ if (!hasPages(layout)) {
1625
+ return {
1626
+ ...layout,
1627
+ order: next.order,
1628
+ hidden: next.hidden,
1629
+ minimized: next.minimized
1630
+ };
1631
+ }
1632
+ return {
1633
+ ...layout,
1634
+ pages: (layout.pages ?? []).map(
1635
+ (page) => page.key === pageKey ? {
1636
+ ...page,
1637
+ order: next.order,
1638
+ hidden: next.hidden,
1639
+ minimized: next.minimized
1640
+ } : page
1641
+ )
1642
+ };
1643
+ }
1644
+ function normalizedTitle(title) {
1645
+ return title.trim().normalize("NFKC").toLowerCase();
1646
+ }
1647
+ function resolvePageKey(layout, reference) {
1648
+ if (reference === void 0 || reference === "") return void 0;
1649
+ const pages = pageList(layout);
1650
+ const byKey = pages.find((p) => p.key === reference);
1651
+ if (byKey !== void 0) return byKey.key;
1652
+ const wanted = normalizedTitle(reference);
1653
+ return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
1654
+ }
1655
+ function homePageKey(layout, item) {
1656
+ return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
1657
+ }
1658
+ function assignPages(definitions, layout) {
1659
+ const pages = pageList(layout);
1660
+ const assigned = new Map(pages.map((p) => [p.key, []]));
1661
+ for (const definition of definitions) {
1662
+ let pageKey;
1663
+ if (!resolveWidgetLock(definition).move) {
1664
+ pageKey = pages.find(
1665
+ (p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
1666
+ )?.key;
1667
+ }
1668
+ assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
1669
+ }
1670
+ return assigned;
1671
+ }
1672
+ function pageOf(definitions, layout, key) {
1673
+ for (const [pageKey, widgets] of assignPages(definitions, layout)) {
1674
+ if (widgets.some((w) => w.key === key)) return pageKey;
1675
+ }
1676
+ return void 0;
1677
+ }
1678
+ function pageWidgets(definitions, layout, pageKey) {
1679
+ const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
1680
+ const view = pageLayout(layout, pageKey);
1681
+ return view === void 0 ? [] : visibleWidgets(onPage, view);
1682
+ }
1683
+ function pageRoom(definitions, layout, pageKey, options = {}) {
1684
+ const page = pageList(layout).find(
1685
+ (p) => p.key === pageKey
1686
+ );
1687
+ if (page === void 0) return void 0;
1688
+ const maxRows = rowsAllowed(page, options);
1689
+ const rowsUsed = placeRows(
1690
+ pageWidgets(definitions, layout, pageKey)
1691
+ ).length;
1692
+ return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
1693
+ }
1694
+ function describePage(page) {
1695
+ return `Page "${page.title}"`;
1696
+ }
1697
+ function titleOf(item) {
1698
+ return item.title ?? item.key;
1699
+ }
1700
+ function checkTitle(layout, title, exceptKey) {
1701
+ const trimmed = title.trim();
1702
+ if (trimmed === "") return err("A page title must not be empty.");
1703
+ if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
1704
+ return err(
1705
+ `Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
1706
+ );
1707
+ }
1708
+ const wanted = normalizedTitle(trimmed);
1709
+ const clash = pageList(layout).find(
1710
+ (p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
1711
+ );
1712
+ if (clash !== void 0) {
1713
+ return err(`Another page is already called "${clash.title}".`);
1714
+ }
1715
+ return ok(trimmed);
1716
+ }
1717
+ function nextPageKey(layout) {
1718
+ const keys = new Set(pageList(layout).map((p) => p.key));
1719
+ let n = 1;
1720
+ while (keys.has(`page-${n}`)) n += 1;
1721
+ return `page-${n}`;
1722
+ }
1723
+ function nextPageTitle(layout) {
1724
+ const used = new Set(
1725
+ pageList(layout).map((p) => normalizedTitle(p.title))
1726
+ );
1727
+ let n = pageList(layout).length + 1;
1728
+ while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
1729
+ return `Page ${n}`;
1730
+ }
1731
+ function addPage(layout, options = {}) {
1732
+ if (pageList(layout).length >= MAX_PARSED_PAGES) {
1733
+ return err(
1734
+ `The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
1735
+ );
1736
+ }
1737
+ const key = options.key ?? nextPageKey(layout);
1738
+ if (pageList(layout).some((p) => p.key === key)) {
1739
+ return err(`A page with the key "${key}" already exists.`);
1740
+ }
1741
+ const title = checkTitle(
1742
+ layout,
1743
+ options.title ?? nextPageTitle(layout)
1744
+ );
1745
+ if (!title.ok) return title;
1746
+ const base = withExplicitPages(layout);
1747
+ const page = {
1748
+ key,
1749
+ title: title.value,
1750
+ order: [],
1751
+ hidden: [],
1752
+ minimized: []
1753
+ };
1754
+ return ok({ ...base, pages: [...base.pages ?? [], page] });
1755
+ }
1756
+ function renamePage(layout, pageKey, title) {
1757
+ const page = pageList(layout).find(
1758
+ (p) => p.key === pageKey
1759
+ );
1760
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
1761
+ const checked = checkTitle(layout, title, pageKey);
1762
+ if (!checked.ok) return checked;
1763
+ if (checked.value === page.title) return ok(layout);
1764
+ const base = withExplicitPages(layout);
1765
+ return ok({
1766
+ ...base,
1767
+ pages: (base.pages ?? []).map(
1768
+ (p) => p.key === pageKey ? { ...p, title: checked.value } : p
1769
+ )
1770
+ });
1771
+ }
1772
+ function movePage(layout, pageKey, offset) {
1773
+ const pages = layout.pages ?? [];
1774
+ const from = pages.findIndex((p) => p.key === pageKey);
1775
+ if (from === -1) return layout;
1776
+ const to = Math.max(
1777
+ 0,
1778
+ Math.min(pages.length - 1, from + Math.trunc(offset))
1779
+ );
1780
+ if (to === from) return layout;
1781
+ const next = [...pages];
1782
+ const [moved] = next.splice(from, 1);
1783
+ next.splice(to, 0, moved);
1784
+ return { ...layout, pages: next };
1785
+ }
1786
+ function overflowing(definitions, layout, options) {
1787
+ return pageList(layout).find(
1788
+ (page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
1789
+ );
1790
+ }
1791
+ function removePage(definitions, layout, pageKey, options = {}) {
1792
+ const pages = pageList(layout);
1793
+ const index = pages.findIndex((p) => p.key === pageKey);
1794
+ const page = pages[index];
1795
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
1796
+ if (pages.length === 1) {
1797
+ return err(
1798
+ `${describePage(page)} is the only page, so it can't be removed.`
1799
+ );
1800
+ }
1801
+ const target = pages[index > 0 ? index - 1 : 1];
1802
+ const merged = {
1803
+ ...layout,
1804
+ pages: pages.filter((p) => p.key !== pageKey).map(
1805
+ (p) => p.key === target.key ? {
1806
+ ...p,
1807
+ order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
1808
+ hidden: [...p.hidden, ...page.hidden],
1809
+ minimized: [...p.minimized, ...page.minimized]
1810
+ } : p
1811
+ )
1812
+ };
1813
+ const full = overflowing(
1814
+ definitions,
1815
+ merged,
1816
+ options
1817
+ );
1818
+ if (full !== void 0) {
1819
+ return err(
1820
+ `${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.`
1821
+ );
1822
+ }
1823
+ return ok(merged);
1824
+ }
1825
+ function without(keys, key) {
1826
+ return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
1827
+ }
1828
+ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
1829
+ const widget = definitions.find((d) => d.key === widgetKey);
1830
+ if (widget === void 0) {
1831
+ return err(
1832
+ `Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
1833
+ );
1834
+ }
1835
+ const pages = pageList(layout);
1836
+ const target = pages.find((p) => p.key === targetKey);
1837
+ if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
1838
+ const sourceKey = pageOf(definitions, layout, widgetKey);
1839
+ if (sourceKey === targetKey) return ok(layout);
1840
+ if (resolveWidgetLock(widget).move) {
1841
+ return err(
1842
+ `Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
1843
+ );
1844
+ }
1845
+ const base = withExplicitPages(layout);
1846
+ const source = (base.pages ?? []).find(
1847
+ (p) => p.key === sourceKey
1848
+ );
1849
+ const hidden = source.hidden.includes(widgetKey);
1850
+ const minimized = source.minimized.includes(widgetKey);
1851
+ const targetOrder = pageWidgets(
1852
+ definitions,
1853
+ base,
1854
+ targetKey
1855
+ ).map((d) => d.key);
1856
+ const moved = {
1857
+ ...base,
1858
+ pages: (base.pages ?? []).map((p) => {
1859
+ if (p.key === sourceKey) {
1860
+ return {
1861
+ ...p,
1862
+ order: without(p.order, widgetKey),
1863
+ hidden: without(p.hidden, widgetKey),
1864
+ minimized: without(p.minimized, widgetKey)
1865
+ };
1866
+ }
1867
+ if (p.key === targetKey) {
1868
+ return {
1869
+ ...p,
1870
+ order: hidden ? p.order : [...targetOrder, widgetKey],
1871
+ hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
1872
+ minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
1873
+ };
1874
+ }
1875
+ return p;
1876
+ })
1877
+ };
1878
+ if (!hidden) {
1879
+ const allowed = rowsAllowed(target, options);
1880
+ const needed = placeRows(
1881
+ pageWidgets(definitions, moved, targetKey)
1882
+ ).length;
1883
+ if (needed > allowed) {
1884
+ return err(
1885
+ `${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
1886
+ );
1887
+ }
1888
+ }
1889
+ return ok(moved);
1890
+ }
1891
+ function withoutLockField(definition) {
1892
+ if (definition.locked === void 0) return definition;
1893
+ const copy = { ...definition };
1894
+ delete copy.locked;
1895
+ return copy;
1896
+ }
1897
+ function uniqueTitle(taken, title, index) {
1898
+ let base = title.trim().slice(0, MAX_PAGE_TITLE_LENGTH);
1899
+ if (base === "") base = `Page ${index + 1}`;
1900
+ let candidate = base;
1901
+ let n = 1;
1902
+ while (taken.has(normalizedTitle(candidate))) {
1903
+ const suffix = ` (#${n})`;
1904
+ candidate = `${base.slice(0, MAX_PAGE_TITLE_LENGTH - suffix.length)}${suffix}`;
1905
+ n += 1;
1906
+ }
1907
+ taken.add(normalizedTitle(candidate));
1908
+ return candidate;
1909
+ }
1910
+ function samePages(a, b) {
1911
+ return a.length === b.length && a.every((p, i) => {
1912
+ const q = b[i];
1913
+ 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);
1914
+ });
1915
+ }
1916
+ function normalizeLayout(definitions, layout, options = {}) {
1917
+ const known = pruneLayout(definitions, layout);
1918
+ const locked = enforceLocks(definitions, known, options);
1919
+ if (!hasPages(locked)) return locked === layout ? layout : locked;
1920
+ const defs = options.overrideLocks === true ? definitions.map(withoutLockField) : definitions;
1921
+ const taken = /* @__PURE__ */ new Set();
1922
+ const seenKeys = /* @__PURE__ */ new Set();
1923
+ const held = /* @__PURE__ */ new Set();
1924
+ let pages = [];
1925
+ (locked.pages ?? []).forEach((page, index) => {
1926
+ if (seenKeys.has(page.key)) return;
1927
+ seenKeys.add(page.key);
1928
+ const keep = (list) => [
1929
+ ...new Set(list.filter((k) => !held.has(k)))
1930
+ ];
1931
+ const next = {
1932
+ ...page,
1933
+ title: uniqueTitle(taken, page.title, index),
1934
+ order: keep(page.order),
1935
+ hidden: keep(page.hidden),
1936
+ minimized: keep(page.minimized)
1937
+ };
1938
+ for (const k of [...next.order, ...next.hidden]) held.add(k);
1939
+ pages.push(next);
1940
+ });
1941
+ const work = () => ({ ...locked, pages });
1942
+ const pinned = defs.filter((d) => resolveWidgetLock(d).move);
1943
+ for (const widget of pinned) {
1944
+ const home = homePageKey(work(), widget);
1945
+ const keysHere = (p, list) => p.key !== home && p[list].includes(widget.key);
1946
+ const wasHidden = pages.some((p) => keysHere(p, "hidden"));
1947
+ const wasMinimized = pages.some((p) => keysHere(p, "minimized"));
1948
+ if (!wasHidden && !wasMinimized) continue;
1949
+ pages = pages.map((p) => {
1950
+ let next = p;
1951
+ if (keysHere(p, "hidden")) {
1952
+ next = { ...next, hidden: without(next.hidden, widget.key) };
1953
+ }
1954
+ if (keysHere(p, "minimized")) {
1955
+ next = { ...next, minimized: without(next.minimized, widget.key) };
1956
+ }
1957
+ if (p.key === home) {
1958
+ next = {
1959
+ ...next,
1960
+ hidden: wasHidden && !next.hidden.includes(widget.key) ? [...next.hidden, widget.key] : next.hidden,
1961
+ minimized: wasMinimized && !next.minimized.includes(widget.key) ? [...next.minimized, widget.key] : next.minimized
1962
+ };
1963
+ }
1964
+ return next;
1965
+ });
1966
+ }
1967
+ const placedKeys = /* @__PURE__ */ new Set();
1968
+ for (const p of pages) {
1969
+ for (const k of [...p.order, ...p.hidden]) placedKeys.add(k);
1970
+ }
1971
+ const pinnedKeys = new Set(pinned.map((d) => d.key));
1972
+ const unplaced = defs.filter((d) => !placedKeys.has(d.key) && !pinnedKeys.has(d.key)).map((definition, index) => ({ definition, index })).sort(
1973
+ (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
1974
+ ).map(({ definition }) => definition);
1975
+ for (const widget of unplaced) {
1976
+ const home = homePageKey(work(), widget);
1977
+ pages = pages.map(
1978
+ (p) => p.key === home ? { ...p, order: [...p.order, widget.key] } : p
1979
+ );
1980
+ }
1981
+ for (let i = 0; i < pages.length; i += 1) {
1982
+ const page = pages[i];
1983
+ const shown = pageWidgets(defs, work(), page.key);
1984
+ const fits = fitCount(shown, rowsAllowed(page, options));
1985
+ if (fits >= shown.length) continue;
1986
+ const moving = shown.slice(fits).filter((d) => !pinnedKeys.has(d.key)).map((d) => d.key);
1987
+ if (moving.length === 0) continue;
1988
+ const moveSet = new Set(moving);
1989
+ const movedMinimized = page.minimized.filter(
1990
+ (k) => moveSet.has(k)
1991
+ );
1992
+ pages = pages.map(
1993
+ (p) => p.key === page.key ? {
1994
+ ...p,
1995
+ order: p.order.filter((k) => !moveSet.has(k)),
1996
+ minimized: p.minimized.filter((k) => !moveSet.has(k))
1997
+ } : p
1998
+ );
1999
+ let placed = false;
2000
+ for (let j = i + 1; j < pages.length && !placed; j += 1) {
2001
+ const candidate = pages[j];
2002
+ const trial = pages.map(
2003
+ (p) => p.key === candidate.key ? {
2004
+ ...p,
2005
+ order: [...moving, ...p.order],
2006
+ minimized: [...p.minimized, ...movedMinimized]
2007
+ } : p
2008
+ );
2009
+ const items = pageWidgets(
2010
+ defs,
2011
+ { ...locked, pages: trial },
2012
+ candidate.key
2013
+ );
2014
+ if (placeRows(items).length <= rowsAllowed(candidate, options)) {
2015
+ pages = trial;
2016
+ placed = true;
2017
+ }
2018
+ }
2019
+ if (!placed) {
2020
+ const key = nextPageKey(work());
2021
+ const title = uniqueTitle(
2022
+ new Set(pages.map((p) => normalizedTitle(p.title))),
2023
+ `Page ${pages.length + 1}`,
2024
+ pages.length
2025
+ );
2026
+ pages = [
2027
+ ...pages,
2028
+ { key, title, order: moving, hidden: [], minimized: movedMinimized }
2029
+ ];
2030
+ }
2031
+ }
2032
+ const result = work();
2033
+ return samePages(layout.pages ?? [], pages) && sameKeys(layout.order, result.order) ? layout : result;
2034
+ }
2035
+
1468
2036
  // src/core/scale.ts
1469
2037
  function niceStep(value) {
1470
2038
  if (!(value > 0) || !Number.isFinite(value)) return 1;
@@ -1499,10 +2067,15 @@ exports.DEFAULT_COMPARISON_LABEL = DEFAULT_COMPARISON_LABEL;
1499
2067
  exports.DEFAULT_CURRENCY = DEFAULT_CURRENCY;
1500
2068
  exports.DEFAULT_DETAIL_PAGE_SIZE = DEFAULT_DETAIL_PAGE_SIZE;
1501
2069
  exports.DEFAULT_EMPTY_TEXT = DEFAULT_EMPTY_TEXT;
2070
+ exports.DEFAULT_PAGE_KEY = DEFAULT_PAGE_KEY;
2071
+ exports.DEFAULT_PAGE_ROWS = DEFAULT_PAGE_ROWS;
1502
2072
  exports.DETAIL_MODES = DETAIL_MODES;
1503
2073
  exports.EMPTY_LAYOUT = EMPTY_LAYOUT;
1504
2074
  exports.MAX_DETAIL_PAGE_SIZE = MAX_DETAIL_PAGE_SIZE;
1505
2075
  exports.MAX_GRAPH_SERIES = MAX_GRAPH_SERIES;
2076
+ exports.MAX_PAGE_ROWS = MAX_PAGE_ROWS;
2077
+ exports.MAX_PAGE_TITLE_LENGTH = MAX_PAGE_TITLE_LENGTH;
2078
+ exports.MAX_PARSED_PAGES = MAX_PARSED_PAGES;
1506
2079
  exports.MAX_REPORTED_PROBLEMS = MAX_REPORTED_PROBLEMS;
1507
2080
  exports.MAX_WIDGET_WIDTH = MAX_WIDGET_WIDTH;
1508
2081
  exports.MIN_WIDGET_WIDTH = MIN_WIDGET_WIDTH;
@@ -1511,6 +2084,8 @@ exports.WIDGET_FILLS = WIDGET_FILLS;
1511
2084
  exports.WIDGET_KINDS = WIDGET_KINDS;
1512
2085
  exports.WIDGET_SIZES = WIDGET_SIZES;
1513
2086
  exports.WIDTH_COLUMNS = WIDTH_COLUMNS;
2087
+ exports.addPage = addPage;
2088
+ exports.assignPages = assignPages;
1514
2089
  exports.baseColumnSpan = baseColumnSpan;
1515
2090
  exports.cacheKeyFor = cacheKeyFor;
1516
2091
  exports.createWidgetLoader = createWidgetLoader;
@@ -1527,10 +2102,12 @@ exports.fillColumnSpans = fillColumnSpans;
1527
2102
  exports.fillWidthSpans = fillWidthSpans;
1528
2103
  exports.fillsHeight = fillsHeight;
1529
2104
  exports.fillsWidth = fillsWidth;
2105
+ exports.fitCount = fitCount;
1530
2106
  exports.formatCount = formatCount;
1531
2107
  exports.formatPercentChange = formatPercentChange;
1532
2108
  exports.formatValue = formatValue;
1533
2109
  exports.fromLegacyDefinition = fromLegacyDefinition;
2110
+ exports.hasPages = hasPages;
1534
2111
  exports.hiddenWidgets = hiddenWidgets;
1535
2112
  exports.hideWidget = hideWidget;
1536
2113
  exports.isHidden = isHidden;
@@ -1546,19 +2123,32 @@ exports.kpiDelta = kpiDelta;
1546
2123
  exports.labelStride = labelStride;
1547
2124
  exports.loadingWidgets = loadingWidgets;
1548
2125
  exports.minimizeWidget = minimizeWidget;
2126
+ exports.movePage = movePage;
1549
2127
  exports.moveWidget = moveWidget;
1550
2128
  exports.moveWidgetBy = moveWidgetBy;
1551
2129
  exports.moveWidgetDown = moveWidgetDown;
2130
+ exports.moveWidgetToPage = moveWidgetToPage;
1552
2131
  exports.moveWidgetUp = moveWidgetUp;
2132
+ exports.nextPageKey = nextPageKey;
1553
2133
  exports.niceDomain = niceDomain;
1554
2134
  exports.niceStep = niceStep;
2135
+ exports.normalizeLayout = normalizeLayout;
1555
2136
  exports.ok = ok;
2137
+ exports.pageLayout = pageLayout;
2138
+ exports.pageList = pageList;
2139
+ exports.pageOf = pageOf;
2140
+ exports.pageRoom = pageRoom;
2141
+ exports.pageWidgets = pageWidgets;
1556
2142
  exports.parseDetailQuery = parseDetailQuery;
1557
2143
  exports.parseLayout = parseLayout;
1558
2144
  exports.percentChange = percentChange;
2145
+ exports.placeRows = placeRows;
1559
2146
  exports.pruneLayout = pruneLayout;
1560
2147
  exports.queryRows = queryRows;
2148
+ exports.removePage = removePage;
2149
+ exports.renamePage = renamePage;
1561
2150
  exports.resolveDetailOptions = resolveDetailOptions;
2151
+ exports.resolvePageKey = resolvePageKey;
1562
2152
  exports.resolvePayload = resolvePayload;
1563
2153
  exports.resolveTableControls = resolveTableControls;
1564
2154
  exports.resolveWidget = resolveWidget;
@@ -1581,5 +2171,6 @@ exports.validateWidgetDefinition = validateWidgetDefinition;
1581
2171
  exports.visibleWidgets = visibleWidgets;
1582
2172
  exports.widgetsFor = widgetsFor;
1583
2173
  exports.widthForSize = widthForSize;
2174
+ exports.withPageLayout = withPageLayout;
1584
2175
  //# sourceMappingURL=core.cjs.map
1585
2176
  //# sourceMappingURL=core.cjs.map