@richardmcquiston01/dashboard-widgets-toolkit 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -429,21 +429,36 @@ function fillColumnSpans(items, columns, largeSpan = 2) {
429
429
  function fillWidthSpans(items) {
430
430
  return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
431
431
  }
432
+ function placeRows(items) {
433
+ return placeAndFillRows(items, WIDTH_COLUMNS, itemWidth).rows;
434
+ }
432
435
  function placeAndFill(items, columns, spanOf) {
436
+ return placeAndFillRows(items, columns, spanOf).spans;
437
+ }
438
+ function placeAndFillRows(items, columns, spanOf) {
433
439
  const trackCount = Math.max(1, Math.floor(columns));
434
440
  const spans = /* @__PURE__ */ new Map();
441
+ const rows = [];
435
442
  let row = [];
436
443
  let used = 0;
437
444
  function closeRow() {
445
+ if (row.length === 0) return;
438
446
  const fillers = row.filter(({ item }) => fillsWidth(item.fill));
439
447
  const leftover = trackCount - used;
448
+ const finalSpans = /* @__PURE__ */ new Map();
440
449
  if (fillers.length > 0) {
441
450
  const share = Math.floor(leftover / fillers.length);
442
451
  const remainder = leftover % fillers.length;
443
452
  fillers.forEach(({ item, span }, index) => {
444
- spans.set(item.key, span + share + (index < remainder ? 1 : 0));
453
+ const grown = span + share + (index < remainder ? 1 : 0);
454
+ spans.set(item.key, grown);
455
+ finalSpans.set(item.key, grown);
445
456
  });
446
457
  }
458
+ rows.push({
459
+ keys: row.map(({ item }) => item.key),
460
+ spans: row.map(({ item, span }) => finalSpans.get(item.key) ?? span)
461
+ });
447
462
  row = [];
448
463
  used = 0;
449
464
  }
@@ -454,7 +469,266 @@ function placeAndFill(items, columns, spanOf) {
454
469
  used += span;
455
470
  }
456
471
  closeRow();
457
- return spans;
472
+ return { spans, rows };
473
+ }
474
+
475
+ // src/core/options.ts
476
+ var DATE_RANGE_PRESETS = [
477
+ "today",
478
+ "yesterday",
479
+ "last7",
480
+ "last30",
481
+ "last90",
482
+ "thisMonth",
483
+ "lastMonth",
484
+ "thisYear",
485
+ "lastYear"
486
+ ];
487
+ var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
488
+ var INTERVAL_PATTERN = /^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/;
489
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
490
+ function isClientApplied(option) {
491
+ return (option.type === "sort" || option.type === "columns") && option.apply === "client";
492
+ }
493
+ function civilToday(context) {
494
+ const now = context.now ?? Date.now();
495
+ let timeZone = context.timeZone ?? "UTC";
496
+ try {
497
+ new Intl.DateTimeFormat("en-US", { timeZone });
498
+ } catch {
499
+ timeZone = "UTC";
500
+ }
501
+ const parts = new Intl.DateTimeFormat("en-US", {
502
+ timeZone,
503
+ year: "numeric",
504
+ month: "2-digit",
505
+ day: "2-digit"
506
+ }).formatToParts(new Date(now));
507
+ const pick = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
508
+ return { year: pick("year"), month: pick("month"), day: pick("day") };
509
+ }
510
+ function toIso(epochMs) {
511
+ return new Date(epochMs).toISOString().slice(0, 10);
512
+ }
513
+ var DAY_MS = 864e5;
514
+ function parseIsoDay(text) {
515
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
516
+ if (match === null) return void 0;
517
+ const epoch = Date.UTC(
518
+ Number(match[1]),
519
+ Number(match[2]) - 1,
520
+ Number(match[3])
521
+ );
522
+ return toIso(epoch) === text ? epoch : void 0;
523
+ }
524
+ function isIsoInterval(value) {
525
+ const match = INTERVAL_PATTERN.exec(value);
526
+ if (match === null) return false;
527
+ const start = parseIsoDay(match[1]);
528
+ const end = parseIsoDay(match[2]);
529
+ return start !== void 0 && end !== void 0 && start <= end;
530
+ }
531
+ function isPreset(value) {
532
+ return DATE_RANGE_PRESETS.includes(value);
533
+ }
534
+ function resolveDateRange(value, context = {}) {
535
+ if (isIsoInterval(value)) return value;
536
+ if (!isPreset(value)) return void 0;
537
+ const today = civilToday(context);
538
+ const todayMs = Date.UTC(today.year, today.month - 1, today.day);
539
+ const range = (startMs, endMs) => `${toIso(startMs)}/${toIso(endMs)}`;
540
+ switch (value) {
541
+ case "today":
542
+ return range(todayMs, todayMs);
543
+ case "yesterday":
544
+ return range(todayMs - DAY_MS, todayMs - DAY_MS);
545
+ case "last7":
546
+ return range(todayMs - 6 * DAY_MS, todayMs);
547
+ case "last30":
548
+ return range(todayMs - 29 * DAY_MS, todayMs);
549
+ case "last90":
550
+ return range(todayMs - 89 * DAY_MS, todayMs);
551
+ case "thisMonth":
552
+ return range(Date.UTC(today.year, today.month - 1, 1), todayMs);
553
+ case "lastMonth":
554
+ return range(
555
+ Date.UTC(today.year, today.month - 2, 1),
556
+ Date.UTC(today.year, today.month - 1, 1) - DAY_MS
557
+ );
558
+ case "thisYear":
559
+ return range(Date.UTC(today.year, 0, 1), todayMs);
560
+ case "lastYear":
561
+ return range(
562
+ Date.UTC(today.year - 1, 0, 1),
563
+ Date.UTC(today.year, 0, 1) - DAY_MS
564
+ );
565
+ }
566
+ }
567
+ function onStepGrid(option, value) {
568
+ if (option.step === void 0) return true;
569
+ const steps = (value - option.min) / option.step;
570
+ return Math.abs(steps - Math.round(steps)) < 1e-9;
571
+ }
572
+ function parseSortValue(value) {
573
+ const separator = value.lastIndexOf(":");
574
+ if (separator <= 0) return void 0;
575
+ const column = value.slice(0, separator);
576
+ const direction = value.slice(separator + 1);
577
+ if (direction !== "asc" && direction !== "desc") return void 0;
578
+ return { column, direction };
579
+ }
580
+ function sortFromOptions(definition, values) {
581
+ if (values === void 0) return void 0;
582
+ const sortOption = definition.options?.find(
583
+ (option) => option.type === "sort"
584
+ );
585
+ if (sortOption === void 0) return void 0;
586
+ const chosen = values[sortOption.key];
587
+ return typeof chosen === "string" ? parseSortValue(chosen) : void 0;
588
+ }
589
+ function coerceOptionValue(option, value, context = {}) {
590
+ switch (option.type) {
591
+ case "choice":
592
+ return typeof value === "string" && option.choices.some((choice) => choice.value === value) ? value : void 0;
593
+ case "number":
594
+ return typeof value === "number" && Number.isFinite(value) && value >= option.min && value <= option.max && onStepGrid(option, value) ? value : void 0;
595
+ case "boolean":
596
+ return typeof value === "boolean" ? value : void 0;
597
+ case "text": {
598
+ if (typeof value !== "string") return void 0;
599
+ const trimmed = value.trim();
600
+ return trimmed.length <= option.maxLength && !CONTROL_CHARACTERS.test(trimmed) ? trimmed : void 0;
601
+ }
602
+ case "dateRange": {
603
+ if (typeof value !== "string") return void 0;
604
+ if (isPreset(value) && option.presets !== void 0 && !option.presets.some((preset) => preset.value === value)) {
605
+ return void 0;
606
+ }
607
+ return resolveDateRange(value, context);
608
+ }
609
+ case "color":
610
+ return typeof value === "string" && COLOR_PATTERN.test(value) ? value.toLowerCase() : void 0;
611
+ case "sort": {
612
+ if (typeof value !== "string") return void 0;
613
+ const sort = parseSortValue(value);
614
+ return sort !== void 0 && option.columns.some((column) => column.key === sort.column) ? value : void 0;
615
+ }
616
+ case "columns": {
617
+ if (!Array.isArray(value) || value.length === 0) return void 0;
618
+ const keys = value;
619
+ const allowed = new Set(option.columns.map((c) => c.key));
620
+ const seen = /* @__PURE__ */ new Set();
621
+ for (const key of keys) {
622
+ if (typeof key !== "string" || !allowed.has(key) || seen.has(key)) {
623
+ return void 0;
624
+ }
625
+ seen.add(key);
626
+ }
627
+ return [...seen];
628
+ }
629
+ }
630
+ }
631
+ function defaultOptionValue(option, context = {}) {
632
+ switch (option.type) {
633
+ case "dateRange":
634
+ return resolveDateRange(option.default, context);
635
+ case "sort":
636
+ return option.default;
637
+ case "columns":
638
+ return [...option.default];
639
+ default:
640
+ return option.default;
641
+ }
642
+ }
643
+ var NO_VALUES = Object.freeze({});
644
+ function resolveOptionValues(definition, chosen, context = {}) {
645
+ const options = definition.options;
646
+ if (options === void 0 || options.length === 0) return NO_VALUES;
647
+ const values = {};
648
+ for (const option of options) {
649
+ const picked = chosen?.[option.key];
650
+ const value = (picked === void 0 ? void 0 : coerceOptionValue(option, picked, context)) ?? defaultOptionValue(option, context);
651
+ if (value !== void 0) values[option.key] = value;
652
+ }
653
+ return values;
654
+ }
655
+ function sameValue(a, b) {
656
+ if (Array.isArray(a) && Array.isArray(b)) {
657
+ return a.length === b.length && a.every((item, i) => item === b[i]);
658
+ }
659
+ return a === b;
660
+ }
661
+ function sameOptionValues(a, b) {
662
+ const keys = Object.keys({ ...a, ...b });
663
+ return keys.every((key) => sameValue(a[key], b[key]));
664
+ }
665
+ function optionsCacheSuffix(definition, values, context = {}) {
666
+ const parts = [];
667
+ for (const option of definition.options ?? []) {
668
+ if (isClientApplied(option)) continue;
669
+ const value = values[option.key];
670
+ if (sameValue(value, defaultOptionValue(option, context))) continue;
671
+ parts.push(
672
+ `${option.key}=${encodeURIComponent(JSON.stringify(value ?? null))}`
673
+ );
674
+ }
675
+ parts.sort();
676
+ return parts.length === 0 ? "" : `?${parts.join("&")}`;
677
+ }
678
+ function tableColumnIndex(key) {
679
+ const match = /^c(\d+)$/.exec(key);
680
+ return match === null ? void 0 : Number(match[1]);
681
+ }
682
+ function sortBarItems(items, sort) {
683
+ const sign = sort.direction === "desc" ? -1 : 1;
684
+ return items.map((item, index) => ({ item, index })).sort((a, b) => {
685
+ const order = sort.column === "value" ? a.item.value - b.item.value : a.item.label.localeCompare(b.item.label);
686
+ return order * sign || a.index - b.index;
687
+ }).map(({ item }) => item);
688
+ }
689
+ function applyClientOptions(definition, data, values) {
690
+ const options = definition.options ?? [];
691
+ const sortOption = options.find(
692
+ (o) => o.type === "sort" && o.apply === "client"
693
+ );
694
+ const columnsOption = options.find(
695
+ (o) => o.type === "columns" && o.apply === "client"
696
+ );
697
+ const sortValue = sortOption === void 0 ? void 0 : values[sortOption.key];
698
+ const columnsValue = columnsOption === void 0 ? void 0 : values[columnsOption.key];
699
+ let next = data;
700
+ if (typeof sortValue === "string") {
701
+ const sort = parseSortValue(sortValue);
702
+ if (sort !== void 0 && next.kind === "BAR_LIST") {
703
+ if (sort.column === "label" || sort.column === "value") {
704
+ next = { ...next, items: sortBarItems(next.items, sort) };
705
+ }
706
+ } else if (sort !== void 0 && next.kind === "TABLE") {
707
+ const index = tableColumnIndex(sort.column);
708
+ if (index !== void 0 && index < next.columns.length) {
709
+ const sorted = queryRows(tableDetailData(next), {
710
+ page: 1,
711
+ pageSize: Math.max(1, next.rows.length),
712
+ sort
713
+ });
714
+ if (sorted.ok) next = { ...next, rows: sorted.value.rows };
715
+ }
716
+ }
717
+ }
718
+ if (Array.isArray(columnsValue) && next.kind === "TABLE") {
719
+ const table = next;
720
+ const indexes = columnsValue.map(tableColumnIndex).filter(
721
+ (index) => index !== void 0 && index < table.columns.length
722
+ );
723
+ if (indexes.length > 0) {
724
+ next = {
725
+ ...table,
726
+ columns: indexes.map((i) => table.columns[i]),
727
+ rows: table.rows.map((row) => indexes.map((i) => row[i]))
728
+ };
729
+ }
730
+ }
731
+ return next;
458
732
  }
459
733
 
460
734
  // src/core/scale.ts
@@ -537,6 +811,27 @@ var DEFAULT_LABELS = {
537
811
  dashboardControls: "Dashboard controls",
538
812
  locked: (title) => `${title} is locked`,
539
813
  lockedForViewers: (title) => `${title} is locked for viewers`,
814
+ pageBar: "Pages",
815
+ pageTabName: (index, count, title) => `Page ${index} of ${count}: ${title}`,
816
+ previousPageButton: "Previous page",
817
+ nextPageButton: "Next page",
818
+ managePages: "Manage pages",
819
+ addPage: "Add page",
820
+ renamePage: (title) => `Rename ${title}`,
821
+ pageTitle: "Page title",
822
+ savePageTitle: "Save page title",
823
+ cancelPageTitle: "Cancel renaming",
824
+ deletePage: (title) => `Delete ${title}`,
825
+ confirmDeletePage: (title) => `Delete the page "${title}" and move its widgets to another page?`,
826
+ movePageLeft: (title) => `Move ${title} page left`,
827
+ movePageRight: (title) => `Move ${title} page right`,
828
+ moveToPage: (title) => `Move ${title} to another page`,
829
+ moveToPagePlaceholder: "Move to page\u2026",
830
+ moveToPageOption: (title, rowsFree) => `${title} (${rowsFree} ${rowsFree === 1 ? "row" : "rows"} free)`,
831
+ newPageOption: "New page\u2026",
832
+ movedToPage: (title, pageTitle) => `${title} moved to ${pageTitle}`,
833
+ pageFull: (pageTitle, maxRows) => `Page "${pageTitle}" is full (${maxRows} rows). Move or hide a widget first.`,
834
+ emptyPage: "This page has no widgets. Move one here from another page.",
540
835
  opensInNewTab: "(opens in a new tab)",
541
836
  view: (title) => `View ${title}`,
542
837
  close: "Close",
@@ -1202,7 +1497,8 @@ function GaugeWidget({
1202
1497
  }
1203
1498
  function TableWidget({
1204
1499
  data,
1205
- controls
1500
+ controls,
1501
+ initialSort
1206
1502
  }) {
1207
1503
  if (controls === void 0) {
1208
1504
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-table-wrap", children: [
@@ -1210,7 +1506,14 @@ function TableWidget({
1210
1506
  data.footer !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "dwt-note", children: data.footer })
1211
1507
  ] });
1212
1508
  }
1213
- return /* @__PURE__ */ jsxRuntime.jsx(InteractiveTable, { data, controls });
1509
+ return /* @__PURE__ */ jsxRuntime.jsx(
1510
+ InteractiveTable,
1511
+ {
1512
+ data,
1513
+ controls,
1514
+ initialSort
1515
+ }
1516
+ );
1214
1517
  }
1215
1518
  function TableView({
1216
1519
  data,
@@ -1263,13 +1566,14 @@ function nextTableSort(current, column) {
1263
1566
  }
1264
1567
  function InteractiveTable({
1265
1568
  data,
1266
- controls
1569
+ controls,
1570
+ initialSort
1267
1571
  }) {
1268
1572
  const slot = useSlotClassName();
1269
1573
  const { labels, locale } = useWidgetSettings();
1270
1574
  const searchId = react.useId();
1271
1575
  const [search, setSearch] = react.useState("");
1272
- const [sort, setSort] = react.useState(void 0);
1576
+ const [sort, setSort] = react.useState(initialSort);
1273
1577
  const localeTag = typeof locale === "string" ? locale : locale?.[0];
1274
1578
  const query = {
1275
1579
  page: 1,
@@ -1371,7 +1675,8 @@ function AlertListWidget({
1371
1675
  }
1372
1676
  function WidgetContent({
1373
1677
  data,
1374
- tableControls
1678
+ tableControls,
1679
+ initialSort
1375
1680
  }) {
1376
1681
  switch (data.kind) {
1377
1682
  case "TEXT":
@@ -1381,7 +1686,14 @@ function WidgetContent({
1381
1686
  case "GAUGE":
1382
1687
  return /* @__PURE__ */ jsxRuntime.jsx(GaugeWidget, { data });
1383
1688
  case "TABLE":
1384
- return /* @__PURE__ */ jsxRuntime.jsx(TableWidget, { data, controls: tableControls });
1689
+ return /* @__PURE__ */ jsxRuntime.jsx(
1690
+ TableWidget,
1691
+ {
1692
+ data,
1693
+ controls: tableControls,
1694
+ initialSort
1695
+ }
1696
+ );
1385
1697
  case "BAR_LIST":
1386
1698
  return /* @__PURE__ */ jsxRuntime.jsx(BarListWidget, { data });
1387
1699
  case "ALERT_LIST":
@@ -1578,7 +1890,8 @@ function ResolvedWidgetCard({
1578
1890
  WidgetContent,
1579
1891
  {
1580
1892
  data: widget.data,
1581
- tableControls: resolveTableControls(definition)
1893
+ tableControls: resolveTableControls(definition),
1894
+ initialSort: sortFromOptions(definition, widget.options)
1582
1895
  }
1583
1896
  ) : null
1584
1897
  }
@@ -1625,6 +1938,9 @@ function isWidgetVisibleTo(definition, roles) {
1625
1938
  }
1626
1939
 
1627
1940
  // src/core/layout.ts
1941
+ var MAX_PAGE_TITLE_LENGTH = 30;
1942
+ var MAX_PARSED_PAGES = 50;
1943
+ var MAX_PAGE_ROWS = 20;
1628
1944
  var EMPTY_LAYOUT = Object.freeze({
1629
1945
  order: Object.freeze([]),
1630
1946
  hidden: Object.freeze([]),
@@ -1739,6 +2055,22 @@ function restoreWidget(layout, key) {
1739
2055
  function toggleMinimized(layout, key) {
1740
2056
  return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
1741
2057
  }
2058
+ function mapLayoutLists(layout, change) {
2059
+ const top = change(layout);
2060
+ const pages = layout.pages?.map((page) => {
2061
+ const next = change(page);
2062
+ return next.order === page.order && next.hidden === page.hidden && next.minimized === page.minimized ? page : { ...page, ...next };
2063
+ });
2064
+ const pagesChanged = pages !== void 0 && pages.some((page, i) => page !== layout.pages?.[i]);
2065
+ if (top.order === layout.order && top.hidden === layout.hidden && top.minimized === layout.minimized && !pagesChanged) {
2066
+ return layout;
2067
+ }
2068
+ return {
2069
+ ...layout,
2070
+ ...top,
2071
+ ...pages === void 0 ? {} : { pages }
2072
+ };
2073
+ }
1742
2074
  function enforceLocks(definitions, layout, options = {}) {
1743
2075
  if (options.overrideLocks === true) return layout;
1744
2076
  const noMove = /* @__PURE__ */ new Set();
@@ -1750,11 +2082,337 @@ function enforceLocks(definitions, layout, options = {}) {
1750
2082
  if (lock.hide) noHide.add(definition.key);
1751
2083
  if (lock.minimize) noMinimize.add(definition.key);
1752
2084
  }
1753
- const without = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
1754
- const order = without(layout.order, noMove);
1755
- const hidden = without(layout.hidden, noHide);
1756
- const minimized = without(layout.minimized, noMinimize);
1757
- return order === layout.order && hidden === layout.hidden && minimized === layout.minimized ? layout : { ...layout, order, hidden, minimized };
2085
+ const without2 = (keys, drop) => keys.some((key) => drop.has(key)) ? keys.filter((key) => !drop.has(key)) : keys;
2086
+ return mapLayoutLists(layout, (lists) => ({
2087
+ order: without2(lists.order, noMove),
2088
+ hidden: without2(lists.hidden, noHide),
2089
+ minimized: without2(lists.minimized, noMinimize)
2090
+ }));
2091
+ }
2092
+
2093
+ // src/core/pages.ts
2094
+ var DEFAULT_PAGE_KEY = "default";
2095
+ var DEFAULT_PAGE_ROWS = 4;
2096
+ function rowsAllowed(page, options) {
2097
+ const rows = page.maxRows ?? options.maxRows ?? DEFAULT_PAGE_ROWS;
2098
+ return Math.max(1, Math.min(MAX_PAGE_ROWS, Math.floor(rows)));
2099
+ }
2100
+ function hasPages(layout) {
2101
+ return layout.pages !== void 0 && layout.pages.length > 0;
2102
+ }
2103
+ function pageList(layout) {
2104
+ if (layout.pages !== void 0 && layout.pages.length > 0) {
2105
+ return layout.pages;
2106
+ }
2107
+ return [
2108
+ {
2109
+ key: DEFAULT_PAGE_KEY,
2110
+ title: "Page 1",
2111
+ order: layout.order,
2112
+ hidden: layout.hidden,
2113
+ minimized: layout.minimized
2114
+ }
2115
+ ];
2116
+ }
2117
+ function withExplicitPages(layout) {
2118
+ if (hasPages(layout)) return layout;
2119
+ return {
2120
+ ...layout,
2121
+ order: [],
2122
+ hidden: [],
2123
+ minimized: [],
2124
+ pages: pageList(layout)
2125
+ };
2126
+ }
2127
+ function sameKeys(a, b) {
2128
+ return a === b || a.length === b.length && a.every((k, i) => k === b[i]);
2129
+ }
2130
+ function pageLayout(layout, pageKey) {
2131
+ if (!hasPages(layout))
2132
+ return pageKey === DEFAULT_PAGE_KEY ? layout : void 0;
2133
+ const page = layout.pages?.find(
2134
+ (p) => p.key === pageKey
2135
+ );
2136
+ if (page === void 0) return void 0;
2137
+ const { pages, ...rest } = layout;
2138
+ return {
2139
+ ...rest,
2140
+ order: page.order,
2141
+ hidden: page.hidden,
2142
+ minimized: page.minimized
2143
+ };
2144
+ }
2145
+ function withPageLayout(layout, pageKey, next) {
2146
+ const current = pageLayout(layout, pageKey);
2147
+ if (current === void 0 || sameKeys(current.order, next.order) && sameKeys(current.hidden, next.hidden) && sameKeys(current.minimized, next.minimized)) {
2148
+ return layout;
2149
+ }
2150
+ if (!hasPages(layout)) {
2151
+ return {
2152
+ ...layout,
2153
+ order: next.order,
2154
+ hidden: next.hidden,
2155
+ minimized: next.minimized
2156
+ };
2157
+ }
2158
+ return {
2159
+ ...layout,
2160
+ pages: (layout.pages ?? []).map(
2161
+ (page) => page.key === pageKey ? {
2162
+ ...page,
2163
+ order: next.order,
2164
+ hidden: next.hidden,
2165
+ minimized: next.minimized
2166
+ } : page
2167
+ )
2168
+ };
2169
+ }
2170
+ function normalizedTitle(title) {
2171
+ return title.trim().normalize("NFKC").toLowerCase();
2172
+ }
2173
+ function resolvePageKey(layout, reference) {
2174
+ if (reference === void 0 || reference === "") return void 0;
2175
+ const pages = pageList(layout);
2176
+ const byKey = pages.find((p) => p.key === reference);
2177
+ if (byKey !== void 0) return byKey.key;
2178
+ const wanted = normalizedTitle(reference);
2179
+ return pages.find((p) => normalizedTitle(p.title) === wanted)?.key;
2180
+ }
2181
+ function homePageKey(layout, item) {
2182
+ return resolvePageKey(layout, item.page) ?? pageList(layout)[0].key;
2183
+ }
2184
+ function assignPages(definitions, layout) {
2185
+ const pages = pageList(layout);
2186
+ const assigned = new Map(pages.map((p) => [p.key, []]));
2187
+ for (const definition of definitions) {
2188
+ let pageKey;
2189
+ if (!resolveWidgetLock(definition).move) {
2190
+ pageKey = pages.find(
2191
+ (p) => p.order.includes(definition.key) || p.hidden.includes(definition.key)
2192
+ )?.key;
2193
+ }
2194
+ assigned.get(pageKey ?? homePageKey(layout, definition))?.push(definition);
2195
+ }
2196
+ return assigned;
2197
+ }
2198
+ function pageOf(definitions, layout, key) {
2199
+ for (const [pageKey, widgets] of assignPages(definitions, layout)) {
2200
+ if (widgets.some((w) => w.key === key)) return pageKey;
2201
+ }
2202
+ return void 0;
2203
+ }
2204
+ function pageWidgets(definitions, layout, pageKey) {
2205
+ const onPage = assignPages(definitions, layout).get(pageKey) ?? [];
2206
+ const view = pageLayout(layout, pageKey);
2207
+ return view === void 0 ? [] : visibleWidgets(onPage, view);
2208
+ }
2209
+ function pageRoom(definitions, layout, pageKey, options = {}) {
2210
+ const page = pageList(layout).find(
2211
+ (p) => p.key === pageKey
2212
+ );
2213
+ if (page === void 0) return void 0;
2214
+ const maxRows = rowsAllowed(page, options);
2215
+ const rowsUsed = placeRows(
2216
+ pageWidgets(definitions, layout, pageKey)
2217
+ ).length;
2218
+ return { maxRows, rowsUsed, rowsFree: Math.max(0, maxRows - rowsUsed) };
2219
+ }
2220
+ function describePage(page) {
2221
+ return `Page "${page.title}"`;
2222
+ }
2223
+ function titleOf(item) {
2224
+ return item.title ?? item.key;
2225
+ }
2226
+ function checkTitle(layout, title, exceptKey) {
2227
+ const trimmed = title.trim();
2228
+ if (trimmed === "") return err("A page title must not be empty.");
2229
+ if (trimmed.length > MAX_PAGE_TITLE_LENGTH) {
2230
+ return err(
2231
+ `Page title "${trimmed}" is ${trimmed.length} characters; the most is ${MAX_PAGE_TITLE_LENGTH}.`
2232
+ );
2233
+ }
2234
+ const wanted = normalizedTitle(trimmed);
2235
+ const clash = pageList(layout).find(
2236
+ (p) => p.key !== exceptKey && normalizedTitle(p.title) === wanted
2237
+ );
2238
+ if (clash !== void 0) {
2239
+ return err(`Another page is already called "${clash.title}".`);
2240
+ }
2241
+ return ok(trimmed);
2242
+ }
2243
+ function nextPageKey(layout) {
2244
+ const keys = new Set(pageList(layout).map((p) => p.key));
2245
+ let n = 1;
2246
+ while (keys.has(`page-${n}`)) n += 1;
2247
+ return `page-${n}`;
2248
+ }
2249
+ function nextPageTitle(layout) {
2250
+ const used = new Set(
2251
+ pageList(layout).map((p) => normalizedTitle(p.title))
2252
+ );
2253
+ let n = pageList(layout).length + 1;
2254
+ while (used.has(normalizedTitle(`Page ${n}`))) n += 1;
2255
+ return `Page ${n}`;
2256
+ }
2257
+ function addPage(layout, options = {}) {
2258
+ if (pageList(layout).length >= MAX_PARSED_PAGES) {
2259
+ return err(
2260
+ `The layout already has ${MAX_PARSED_PAGES} pages, the most it can keep.`
2261
+ );
2262
+ }
2263
+ const key = options.key ?? nextPageKey(layout);
2264
+ if (pageList(layout).some((p) => p.key === key)) {
2265
+ return err(`A page with the key "${key}" already exists.`);
2266
+ }
2267
+ const title = checkTitle(
2268
+ layout,
2269
+ options.title ?? nextPageTitle(layout)
2270
+ );
2271
+ if (!title.ok) return title;
2272
+ const base = withExplicitPages(layout);
2273
+ const page = {
2274
+ key,
2275
+ title: title.value,
2276
+ order: [],
2277
+ hidden: [],
2278
+ minimized: []
2279
+ };
2280
+ return ok({ ...base, pages: [...base.pages ?? [], page] });
2281
+ }
2282
+ function renamePage(layout, pageKey, title) {
2283
+ const page = pageList(layout).find(
2284
+ (p) => p.key === pageKey
2285
+ );
2286
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2287
+ const checked = checkTitle(layout, title, pageKey);
2288
+ if (!checked.ok) return checked;
2289
+ if (checked.value === page.title) return ok(layout);
2290
+ const base = withExplicitPages(layout);
2291
+ return ok({
2292
+ ...base,
2293
+ pages: (base.pages ?? []).map(
2294
+ (p) => p.key === pageKey ? { ...p, title: checked.value } : p
2295
+ )
2296
+ });
2297
+ }
2298
+ function movePage(layout, pageKey, offset) {
2299
+ const pages = layout.pages ?? [];
2300
+ const from = pages.findIndex((p) => p.key === pageKey);
2301
+ if (from === -1) return layout;
2302
+ const to = Math.max(
2303
+ 0,
2304
+ Math.min(pages.length - 1, from + Math.trunc(offset))
2305
+ );
2306
+ if (to === from) return layout;
2307
+ const next = [...pages];
2308
+ const [moved] = next.splice(from, 1);
2309
+ next.splice(to, 0, moved);
2310
+ return { ...layout, pages: next };
2311
+ }
2312
+ function overflowing(definitions, layout, options) {
2313
+ return pageList(layout).find(
2314
+ (page) => placeRows(pageWidgets(definitions, layout, page.key)).length > rowsAllowed(page, options)
2315
+ );
2316
+ }
2317
+ function removePage(definitions, layout, pageKey, options = {}) {
2318
+ const pages = pageList(layout);
2319
+ const index = pages.findIndex((p) => p.key === pageKey);
2320
+ const page = pages[index];
2321
+ if (page === void 0) return err(`Page "${pageKey}" doesn't exist.`);
2322
+ if (pages.length === 1) {
2323
+ return err(
2324
+ `${describePage(page)} is the only page, so it can't be removed.`
2325
+ );
2326
+ }
2327
+ const target = pages[index > 0 ? index - 1 : 1];
2328
+ const merged = {
2329
+ ...layout,
2330
+ pages: pages.filter((p) => p.key !== pageKey).map(
2331
+ (p) => p.key === target.key ? {
2332
+ ...p,
2333
+ order: index > 0 ? [...p.order, ...page.order] : [...page.order, ...p.order],
2334
+ hidden: [...p.hidden, ...page.hidden],
2335
+ minimized: [...p.minimized, ...page.minimized]
2336
+ } : p
2337
+ )
2338
+ };
2339
+ const full = overflowing(
2340
+ definitions,
2341
+ merged,
2342
+ options
2343
+ );
2344
+ if (full !== void 0) {
2345
+ return err(
2346
+ `${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.`
2347
+ );
2348
+ }
2349
+ return ok(merged);
2350
+ }
2351
+ function without(keys, key) {
2352
+ return keys.includes(key) ? keys.filter((k) => k !== key) : keys;
2353
+ }
2354
+ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {}) {
2355
+ const widget = definitions.find((d) => d.key === widgetKey);
2356
+ if (widget === void 0) {
2357
+ return err(
2358
+ `Widget "${widgetKey}" isn't one of the widgets, so it can't be moved to a page.`
2359
+ );
2360
+ }
2361
+ const pages = pageList(layout);
2362
+ const target = pages.find((p) => p.key === targetKey);
2363
+ if (target === void 0) return err(`Page "${targetKey}" doesn't exist.`);
2364
+ const sourceKey = pageOf(definitions, layout, widgetKey);
2365
+ if (sourceKey === targetKey) return ok(layout);
2366
+ if (resolveWidgetLock(widget).move) {
2367
+ return err(
2368
+ `Widget "${titleOf(widget)}" is locked against moving, so it can't be moved to ${describePage(target)}.`
2369
+ );
2370
+ }
2371
+ const base = withExplicitPages(layout);
2372
+ const source = (base.pages ?? []).find(
2373
+ (p) => p.key === sourceKey
2374
+ );
2375
+ const hidden = source.hidden.includes(widgetKey);
2376
+ const minimized = source.minimized.includes(widgetKey);
2377
+ const targetOrder = pageWidgets(
2378
+ definitions,
2379
+ base,
2380
+ targetKey
2381
+ ).map((d) => d.key);
2382
+ const moved = {
2383
+ ...base,
2384
+ pages: (base.pages ?? []).map((p) => {
2385
+ if (p.key === sourceKey) {
2386
+ return {
2387
+ ...p,
2388
+ order: without(p.order, widgetKey),
2389
+ hidden: without(p.hidden, widgetKey),
2390
+ minimized: without(p.minimized, widgetKey)
2391
+ };
2392
+ }
2393
+ if (p.key === targetKey) {
2394
+ return {
2395
+ ...p,
2396
+ order: hidden ? p.order : [...targetOrder, widgetKey],
2397
+ hidden: hidden ? [...p.hidden, widgetKey] : p.hidden,
2398
+ minimized: minimized ? [...p.minimized, widgetKey] : p.minimized
2399
+ };
2400
+ }
2401
+ return p;
2402
+ })
2403
+ };
2404
+ if (!hidden) {
2405
+ const allowed = rowsAllowed(target, options);
2406
+ const needed = placeRows(
2407
+ pageWidgets(definitions, moved, targetKey)
2408
+ ).length;
2409
+ if (needed > allowed) {
2410
+ return err(
2411
+ `${describePage(target)} has no room for "${titleOf(widget)}": it would need row ${needed}, and the page allows ${allowed}.`
2412
+ );
2413
+ }
2414
+ }
2415
+ return ok(moved);
1758
2416
  }
1759
2417
  function nextSort(current, column) {
1760
2418
  if (current?.column !== column) return { column, direction: "asc" };
@@ -1995,7 +2653,8 @@ function WidgetDetailDialog({
1995
2653
  }
1996
2654
  );
1997
2655
  }
1998
- function useDetailData(definition, load, enabled, fallback) {
2656
+ var NO_OPTION_VALUES = Object.freeze({});
2657
+ function useDetailData(definition, load, enabled, fallback, optionValues = NO_OPTION_VALUES) {
1999
2658
  const [state, setState] = react.useState({ status: "loading" });
2000
2659
  const [attempt, setAttempt] = react.useState(0);
2001
2660
  react.useEffect(() => {
@@ -2005,6 +2664,7 @@ function useDetailData(definition, load, enabled, fallback) {
2005
2664
  Promise.resolve().then(
2006
2665
  () => load === void 0 ? void 0 : load(definition, {
2007
2666
  signal: controller.signal,
2667
+ options: optionValues,
2008
2668
  query: defaultDetailQuery()
2009
2669
  })
2010
2670
  ).then((raw) => {
@@ -2032,9 +2692,28 @@ function useDetailData(definition, load, enabled, fallback) {
2032
2692
  });
2033
2693
  });
2034
2694
  return () => controller.abort();
2035
- }, [enabled, load, fallback, definition, attempt]);
2695
+ }, [enabled, load, fallback, definition, optionValues, attempt]);
2036
2696
  return { state, reload: () => setAttempt((count) => count + 1) };
2037
2697
  }
2698
+ var NEW_PAGE_VALUE = "__new-page__";
2699
+ function PagePanel({
2700
+ paged,
2701
+ id,
2702
+ labelledBy,
2703
+ children
2704
+ }) {
2705
+ if (!paged) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
2706
+ return /* @__PURE__ */ jsxRuntime.jsx(
2707
+ "div",
2708
+ {
2709
+ role: "tabpanel",
2710
+ id,
2711
+ "aria-labelledby": labelledBy,
2712
+ className: "dwt-page-panel",
2713
+ children
2714
+ }
2715
+ );
2716
+ }
2038
2717
  var NARROW_VIEWPORT_QUERY = "(max-width: 640px)";
2039
2718
  var WIDE_GRID_PX = 900;
2040
2719
  function useFillSpans(definitions, gridRef, twelve) {
@@ -2129,7 +2808,14 @@ function DetailHost({
2129
2808
  const { definition } = widget;
2130
2809
  const options = resolveDetailOptions(definition);
2131
2810
  const pageSize = options?.pageSize ?? 25;
2132
- const [query, setQuery] = react.useState(defaultDetailQuery(pageSize));
2811
+ const optionSort = sortFromOptions(
2812
+ definition,
2813
+ widget.options
2814
+ );
2815
+ const [query, setQuery] = react.useState({
2816
+ ...defaultDetailQuery(pageSize),
2817
+ ...optionSort === void 0 ? {} : { sort: optionSort }
2818
+ });
2133
2819
  const derived = react.useMemo(
2134
2820
  () => widget.status === "ok" ? deriveDetailData(widget.data, locale) : void 0,
2135
2821
  [widget, locale]
@@ -2138,7 +2824,8 @@ function DetailHost({
2138
2824
  definition,
2139
2825
  loadDetail,
2140
2826
  true,
2141
- derived
2827
+ derived,
2828
+ widget.options
2142
2829
  );
2143
2830
  const title = options?.title ?? definition.title;
2144
2831
  return /* @__PURE__ */ jsxRuntime.jsx(WidgetDetailDialog, { title, open: true, onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -2205,6 +2892,181 @@ function InlineConfirm({
2205
2892
  }
2206
2893
  );
2207
2894
  }
2895
+ function PageTabs({
2896
+ pages,
2897
+ activeKey,
2898
+ idPrefix,
2899
+ panelId,
2900
+ announcement,
2901
+ onSelect
2902
+ }) {
2903
+ const slot = useSlotClassName();
2904
+ const { labels } = useWidgetSettings();
2905
+ const tabs = react.useRef(/* @__PURE__ */ new Map());
2906
+ const activeIndex = pages.findIndex((p) => p.key === activeKey);
2907
+ function onKeyDown(event, index) {
2908
+ const last = pages.length - 1;
2909
+ let next;
2910
+ switch (event.key) {
2911
+ case "ArrowRight":
2912
+ case "ArrowDown":
2913
+ next = index === last ? 0 : index + 1;
2914
+ break;
2915
+ case "ArrowLeft":
2916
+ case "ArrowUp":
2917
+ next = index === 0 ? last : index - 1;
2918
+ break;
2919
+ case "Home":
2920
+ next = 0;
2921
+ break;
2922
+ case "End":
2923
+ next = last;
2924
+ break;
2925
+ default:
2926
+ return;
2927
+ }
2928
+ event.preventDefault();
2929
+ const target = pages[next];
2930
+ if (target === void 0) return;
2931
+ onSelect(target.key);
2932
+ tabs.current.get(target.key)?.focus();
2933
+ }
2934
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: slot("pageBar", "dwt-pages"), children: [
2935
+ /* @__PURE__ */ jsxRuntime.jsx(
2936
+ "button",
2937
+ {
2938
+ type: "button",
2939
+ className: slot(
2940
+ "button",
2941
+ "dwt-button",
2942
+ "dwt-icon-button",
2943
+ "dwt-page-nav"
2944
+ ),
2945
+ "aria-label": labels.previousPageButton,
2946
+ disabled: activeIndex <= 0,
2947
+ onClick: () => onSelect(pages[activeIndex - 1].key),
2948
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2039" })
2949
+ }
2950
+ ),
2951
+ /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tablist", "aria-label": labels.pageBar, className: "dwt-page-tabs", children: pages.map((page, index) => {
2952
+ const selected = page.key === activeKey;
2953
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2954
+ "button",
2955
+ {
2956
+ ref: (element) => {
2957
+ if (element === null) tabs.current.delete(page.key);
2958
+ else tabs.current.set(page.key, element);
2959
+ },
2960
+ type: "button",
2961
+ role: "tab",
2962
+ id: `${idPrefix}-tab-${index}`,
2963
+ "aria-selected": selected,
2964
+ "aria-controls": panelId,
2965
+ "aria-label": labels.pageTabName(
2966
+ index + 1,
2967
+ pages.length,
2968
+ page.title
2969
+ ),
2970
+ title: page.title,
2971
+ tabIndex: selected ? 0 : -1,
2972
+ className: slot("button", "dwt-page-tab"),
2973
+ onClick: () => onSelect(page.key),
2974
+ onKeyDown: (event) => onKeyDown(event, index),
2975
+ children: [
2976
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "dwt-page-tab-title", children: page.title }),
2977
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "dwt-page-dot", "aria-hidden": "true" })
2978
+ ]
2979
+ },
2980
+ page.key
2981
+ );
2982
+ }) }),
2983
+ /* @__PURE__ */ jsxRuntime.jsx(
2984
+ "button",
2985
+ {
2986
+ type: "button",
2987
+ className: slot(
2988
+ "button",
2989
+ "dwt-button",
2990
+ "dwt-icon-button",
2991
+ "dwt-page-nav"
2992
+ ),
2993
+ "aria-label": labels.nextPageButton,
2994
+ disabled: activeIndex === pages.length - 1,
2995
+ onClick: () => onSelect(pages[activeIndex + 1].key),
2996
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u203A" })
2997
+ }
2998
+ ),
2999
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "dwt-visually-hidden", "aria-live": "polite", children: announcement })
3000
+ ] });
3001
+ }
3002
+ function PageRenameForm({
3003
+ initial,
3004
+ onSubmit,
3005
+ onCancel
3006
+ }) {
3007
+ const slot = useSlotClassName();
3008
+ const { labels } = useWidgetSettings();
3009
+ const [title, setTitle] = react.useState(initial);
3010
+ const [error, setError] = react.useState(void 0);
3011
+ const inputRef = react.useRef(null);
3012
+ react.useEffect(() => {
3013
+ inputRef.current?.focus();
3014
+ inputRef.current?.select();
3015
+ }, []);
3016
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3017
+ "form",
3018
+ {
3019
+ className: "dwt-page-rename",
3020
+ onSubmit: (event) => {
3021
+ event.preventDefault();
3022
+ setError(onSubmit(title));
3023
+ },
3024
+ onKeyDown: (event) => {
3025
+ if (event.key === "Escape") {
3026
+ event.stopPropagation();
3027
+ onCancel();
3028
+ }
3029
+ },
3030
+ children: [
3031
+ /* @__PURE__ */ jsxRuntime.jsx(
3032
+ "input",
3033
+ {
3034
+ ref: inputRef,
3035
+ type: "text",
3036
+ className: "dwt-page-title-input",
3037
+ "aria-label": labels.pageTitle,
3038
+ "aria-invalid": error !== void 0,
3039
+ maxLength: MAX_PAGE_TITLE_LENGTH,
3040
+ value: title,
3041
+ onChange: (event) => setTitle(event.target.value)
3042
+ }
3043
+ ),
3044
+ /* @__PURE__ */ jsxRuntime.jsx(
3045
+ "button",
3046
+ {
3047
+ type: "submit",
3048
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3049
+ "aria-label": labels.savePageTitle,
3050
+ title: labels.savePageTitle,
3051
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2713" })
3052
+ }
3053
+ ),
3054
+ /* @__PURE__ */ jsxRuntime.jsx(
3055
+ "button",
3056
+ {
3057
+ type: "button",
3058
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3059
+ "aria-label": labels.cancelPageTitle,
3060
+ title: labels.cancelPageTitle,
3061
+ onClick: onCancel,
3062
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" })
3063
+ }
3064
+ ),
3065
+ error !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { role: "alert", className: "dwt-page-error", children: error })
3066
+ ]
3067
+ }
3068
+ );
3069
+ }
2208
3070
  function withoutLock(definition) {
2209
3071
  if (definition.locked === void 0) return definition;
2210
3072
  const copy = { ...definition };
@@ -2222,6 +3084,11 @@ function DashboardInner({
2222
3084
  onEditingChange,
2223
3085
  defaultLayout,
2224
3086
  toolbar = true,
3087
+ activePage: activePageProp,
3088
+ defaultActivePage,
3089
+ onActivePageChange,
3090
+ maxRows,
3091
+ maxPages,
2225
3092
  onRetry,
2226
3093
  loadDetail,
2227
3094
  onOpenDetail,
@@ -2237,8 +3104,19 @@ function DashboardInner({
2237
3104
  const byKey = new Map(
2238
3105
  widgets.map((w) => [w.definition.key, w])
2239
3106
  );
2240
- const visible = visibleWidgets(definitions, layout);
2241
- const hidden = hiddenWidgets(definitions, layout);
3107
+ const pages = pageList(layout);
3108
+ const paged = pages.length >= 2;
3109
+ const [activeState, setActiveState] = react.useState(
3110
+ defaultActivePage
3111
+ );
3112
+ const requested = activePageProp ?? activeState;
3113
+ const activePage = pages.find((page) => page.key === requested) ?? pages[0];
3114
+ const activeKey = activePage.key;
3115
+ const pageDefs = assignPages(definitions, layout).get(activeKey) ?? [];
3116
+ const view = pageLayout(layout, activeKey) ?? layout;
3117
+ const roomOptions = maxRows === void 0 ? {} : { maxRows };
3118
+ const visible = visibleWidgets(pageDefs, view);
3119
+ const hidden = hiddenWidgets(pageDefs, view);
2242
3120
  const editable = onLayoutChange !== void 0;
2243
3121
  const toggleMode = editMode === "toggle";
2244
3122
  const [editingState, setEditingState] = react.useState(defaultEditing);
@@ -2257,10 +3135,16 @@ function DashboardInner({
2257
3135
  );
2258
3136
  const pending = editing ? confirming : void 0;
2259
3137
  const [announcement, setAnnouncement] = react.useState("");
3138
+ const [pageAnnouncement, setPageAnnouncement] = react.useState("");
3139
+ const [pageError, setPageError] = react.useState(void 0);
3140
+ const [pageEditor, setPageEditor] = react.useState(void 0);
3141
+ const editorMode = pageEditor?.key === activeKey ? pageEditor.mode : void 0;
3142
+ const instanceId = react.useId();
3143
+ const panelId = `${instanceId}-panel`;
2260
3144
  const toggleRef = react.useRef(null);
2261
3145
  const resetRef = react.useRef(null);
2262
- const minimized = editable ? visible.filter((definition) => isMinimized(layout, definition.key)) : [];
2263
- const shown = editable ? visible.filter((definition) => !isMinimized(layout, definition.key)) : visible;
3146
+ const minimized = editable ? visible.filter((definition) => isMinimized(view, definition.key)) : [];
3147
+ const shown = editable ? visible.filter((definition) => !isMinimized(view, definition.key)) : visible;
2264
3148
  const [detailKey, setDetailKey] = react.useState(void 0);
2265
3149
  const gridRef = react.useRef(null);
2266
3150
  const twelve = usesWidthColumns(shown);
@@ -2272,6 +3156,110 @@ function DashboardInner({
2272
3156
  function change(next) {
2273
3157
  if (next !== layout) onLayoutChange?.(next);
2274
3158
  }
3159
+ function changePage(next) {
3160
+ setPageError(void 0);
3161
+ change(withPageLayout(layout, activeKey, next));
3162
+ }
3163
+ function selectPage(pageKey) {
3164
+ const index = pages.findIndex((page) => page.key === pageKey);
3165
+ if (index === -1) return;
3166
+ if (activePageProp === void 0) setActiveState(pageKey);
3167
+ onActivePageChange?.(pageKey);
3168
+ setPageError(void 0);
3169
+ setPageEditor(void 0);
3170
+ setPageAnnouncement(
3171
+ labels.pageTabName(
3172
+ index + 1,
3173
+ pages.length,
3174
+ pages[index].title
3175
+ )
3176
+ );
3177
+ }
3178
+ function addPageAction() {
3179
+ const added = addPage(layout);
3180
+ if (!added.ok) {
3181
+ setPageError(added.error);
3182
+ return;
3183
+ }
3184
+ change(added.value);
3185
+ const created = added.value.pages?.at(-1);
3186
+ if (created !== void 0) {
3187
+ if (activePageProp === void 0) setActiveState(created.key);
3188
+ onActivePageChange?.(created.key);
3189
+ setPageError(void 0);
3190
+ setPageEditor({ mode: "rename", key: created.key });
3191
+ }
3192
+ }
3193
+ function renamePageAction(title) {
3194
+ const renamed = renamePage(layout, activeKey, title);
3195
+ if (!renamed.ok) return renamed.error;
3196
+ change(renamed.value);
3197
+ setPageEditor(void 0);
3198
+ return void 0;
3199
+ }
3200
+ function deletePageAction() {
3201
+ const index = pages.findIndex((page) => page.key === activeKey);
3202
+ const removed = removePage(definitions, layout, activeKey, roomOptions);
3203
+ setPageEditor(void 0);
3204
+ if (!removed.ok) {
3205
+ setPageError(removed.error);
3206
+ return;
3207
+ }
3208
+ const neighbor = pages[index > 0 ? index - 1 : 1];
3209
+ change(removed.value);
3210
+ if (neighbor !== void 0) selectPage(neighbor.key);
3211
+ }
3212
+ function moveWidgetAction(widgetKey, targetKey) {
3213
+ let target = targetKey;
3214
+ let base = layout;
3215
+ if (targetKey === NEW_PAGE_VALUE) {
3216
+ const added = addPage(layout);
3217
+ if (!added.ok) {
3218
+ setPageError(added.error);
3219
+ return;
3220
+ }
3221
+ base = added.value;
3222
+ target = (added.value.pages?.at(-1)).key;
3223
+ }
3224
+ const moved = moveWidgetToPage(
3225
+ definitions,
3226
+ base,
3227
+ widgetKey,
3228
+ target,
3229
+ roomOptions
3230
+ );
3231
+ if (!moved.ok) {
3232
+ setPageError(moved.error);
3233
+ return;
3234
+ }
3235
+ setPageError(void 0);
3236
+ change(moved.value);
3237
+ const destination = moved.value.pages?.find(
3238
+ (page) => page.key === target
3239
+ );
3240
+ setPageAnnouncement(
3241
+ labels.movedToPage(
3242
+ byKey.get(widgetKey)?.definition.title ?? widgetKey,
3243
+ destination?.title ?? target
3244
+ )
3245
+ );
3246
+ }
3247
+ function showAction(widgetKey) {
3248
+ const next = withPageLayout(
3249
+ layout,
3250
+ activeKey,
3251
+ showWidget(view, widgetKey)
3252
+ );
3253
+ if (hasPages(next)) {
3254
+ const room = pageRoom(definitions, next, activeKey, roomOptions);
3255
+ if (room !== void 0 && room.rowsUsed > room.maxRows) {
3256
+ setPageError(labels.pageFull(activePage.title, room.maxRows));
3257
+ return;
3258
+ }
3259
+ }
3260
+ setPageError(void 0);
3261
+ change(next);
3262
+ }
2275
3263
  function setEditing(next) {
2276
3264
  if (editingProp === void 0) setEditingState(next);
2277
3265
  onEditingChange?.(next);
@@ -2349,176 +3337,312 @@ function DashboardInner({
2349
3337
  ]
2350
3338
  }
2351
3339
  ),
2352
- controlsShown && hidden.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: slot("hiddenBar", "dwt-hidden-bar"), children: [
2353
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "dwt-hidden-label", children: labels.hiddenWidgets }),
2354
- hidden.map((definition) => /* @__PURE__ */ jsxRuntime.jsxs(
2355
- "button",
2356
- {
2357
- type: "button",
2358
- className: slot("button", "dwt-button", "dwt-show-button"),
2359
- "aria-label": labels.show(definition.title),
2360
- onClick: () => change(showWidget(layout, definition.key)),
2361
- children: [
2362
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "+ " }),
2363
- definition.title
2364
- ]
2365
- },
2366
- definition.key
2367
- ))
2368
- ] }),
2369
- minimized.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
3340
+ paged && /* @__PURE__ */ jsxRuntime.jsx(
3341
+ PageTabs,
3342
+ {
3343
+ pages,
3344
+ activeKey,
3345
+ idPrefix: instanceId,
3346
+ panelId,
3347
+ announcement: pageAnnouncement,
3348
+ onSelect: selectPage
3349
+ }
3350
+ ),
3351
+ controlsShown && /* @__PURE__ */ jsxRuntime.jsxs(
2370
3352
  "div",
2371
3353
  {
2372
- className: slot("hiddenBar", "dwt-hidden-bar", "dwt-minimized-bar"),
3354
+ role: "group",
3355
+ "aria-label": labels.managePages,
3356
+ className: "dwt-page-manage",
2373
3357
  children: [
2374
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "dwt-hidden-label", children: labels.minimizedWidgets }),
2375
- minimized.map((definition) => /* @__PURE__ */ jsxRuntime.jsxs(
3358
+ /* @__PURE__ */ jsxRuntime.jsx(
2376
3359
  "button",
2377
3360
  {
2378
3361
  type: "button",
2379
- className: slot("button", "dwt-button", "dwt-show-button"),
2380
- "aria-label": labels.expand(definition.title),
2381
- onClick: () => change(toggleMinimized(layout, definition.key)),
2382
- children: [
2383
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u25B8 " }),
2384
- definition.title
2385
- ]
3362
+ className: slot("button", "dwt-button"),
3363
+ disabled: maxPages !== void 0 && pages.length >= maxPages,
3364
+ onClick: addPageAction,
3365
+ children: labels.addPage
3366
+ }
3367
+ ),
3368
+ paged && editorMode === "rename" && /* @__PURE__ */ jsxRuntime.jsx(
3369
+ PageRenameForm,
3370
+ {
3371
+ initial: activePage.title,
3372
+ onSubmit: renamePageAction,
3373
+ onCancel: () => setPageEditor(void 0)
2386
3374
  },
2387
- definition.key
2388
- ))
3375
+ activeKey
3376
+ ),
3377
+ paged && editorMode !== "rename" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3378
+ /* @__PURE__ */ jsxRuntime.jsx(
3379
+ "button",
3380
+ {
3381
+ type: "button",
3382
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3383
+ "aria-label": labels.renamePage(activePage.title),
3384
+ title: labels.renamePage(activePage.title),
3385
+ onClick: () => setPageEditor({ mode: "rename", key: activeKey }),
3386
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u270E" })
3387
+ }
3388
+ ),
3389
+ /* @__PURE__ */ jsxRuntime.jsx(
3390
+ "button",
3391
+ {
3392
+ type: "button",
3393
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3394
+ "aria-label": labels.movePageLeft(activePage.title),
3395
+ title: labels.movePageLeft(activePage.title),
3396
+ disabled: pages[0]?.key === activeKey,
3397
+ onClick: () => change(movePage(layout, activeKey, -1)),
3398
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2190" })
3399
+ }
3400
+ ),
3401
+ /* @__PURE__ */ jsxRuntime.jsx(
3402
+ "button",
3403
+ {
3404
+ type: "button",
3405
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3406
+ "aria-label": labels.movePageRight(activePage.title),
3407
+ title: labels.movePageRight(activePage.title),
3408
+ disabled: pages.at(-1)?.key === activeKey,
3409
+ onClick: () => change(movePage(layout, activeKey, 1)),
3410
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2192" })
3411
+ }
3412
+ ),
3413
+ /* @__PURE__ */ jsxRuntime.jsx(
3414
+ "button",
3415
+ {
3416
+ type: "button",
3417
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3418
+ "aria-label": labels.deletePage(activePage.title),
3419
+ title: labels.deletePage(activePage.title),
3420
+ onClick: () => setPageEditor({ mode: "delete", key: activeKey }),
3421
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xD7" })
3422
+ }
3423
+ )
3424
+ ] }),
3425
+ paged && editorMode === "delete" && /* @__PURE__ */ jsxRuntime.jsx(
3426
+ InlineConfirm,
3427
+ {
3428
+ message: labels.confirmDeletePage(activePage.title),
3429
+ onAnswer: (yes) => yes ? deletePageAction() : setPageEditor(void 0)
3430
+ }
3431
+ ),
3432
+ pageError !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { role: "alert", className: "dwt-page-error", children: pageError })
2389
3433
  ]
2390
3434
  }
2391
3435
  ),
2392
- visible.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: slot("empty", "dwt-empty", "dwt-dashboard-empty"), children: definitions.length === 0 ? labels.noWidgets : labels.allHidden }) : shown.length === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(
2393
- "div",
3436
+ /* @__PURE__ */ jsxRuntime.jsxs(
3437
+ PagePanel,
2394
3438
  {
2395
- ref: gridRef,
2396
- className: slot("grid", "dwt-grid", twelve && "dwt-grid--twelve"),
2397
- children: shown.map((definition, index) => {
2398
- const widget = byKey.get(
2399
- definition.key
2400
- );
2401
- if (widget === void 0) return null;
2402
- const key = definition.key;
2403
- const title = definition.title;
2404
- const lock = resolveWidgetLock(definition);
2405
- const ownLock = resolveWidgetLock(
2406
- widget.definition
2407
- );
2408
- const isLocked = ownLock.move || ownLock.hide || ownLock.minimize;
2409
- const lockLabel = overrideLocks ? labels.lockedForViewers(title) : labels.locked(title);
2410
- const canMoveEarlier = !lock.move && moveWidgetBy(definitions, layout, key, -1) !== layout;
2411
- const canMoveLater = !lock.move && moveWidgetBy(definitions, layout, key, 1) !== layout;
2412
- const actions = controlsShown ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2413
- isLocked && /* @__PURE__ */ jsxRuntime.jsx(
2414
- "span",
3439
+ paged,
3440
+ id: panelId,
3441
+ labelledBy: `${instanceId}-tab-${pages.findIndex((p) => p.key === activeKey)}`,
3442
+ children: [
3443
+ controlsShown && hidden.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: slot("hiddenBar", "dwt-hidden-bar"), children: [
3444
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "dwt-hidden-label", children: labels.hiddenWidgets }),
3445
+ hidden.map((definition) => /* @__PURE__ */ jsxRuntime.jsxs(
3446
+ "button",
2415
3447
  {
2416
- role: "img",
2417
- className: "dwt-lock",
2418
- "aria-label": lockLabel,
2419
- title: lockLabel,
2420
- children: /* @__PURE__ */ jsxRuntime.jsxs(
2421
- "svg",
3448
+ type: "button",
3449
+ className: slot("button", "dwt-button", "dwt-show-button"),
3450
+ "aria-label": labels.show(definition.title),
3451
+ onClick: () => showAction(definition.key),
3452
+ children: [
3453
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "+ " }),
3454
+ definition.title
3455
+ ]
3456
+ },
3457
+ definition.key
3458
+ ))
3459
+ ] }),
3460
+ minimized.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
3461
+ "div",
3462
+ {
3463
+ className: slot("hiddenBar", "dwt-hidden-bar", "dwt-minimized-bar"),
3464
+ children: [
3465
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "dwt-hidden-label", children: labels.minimizedWidgets }),
3466
+ minimized.map((definition) => /* @__PURE__ */ jsxRuntime.jsxs(
3467
+ "button",
2422
3468
  {
2423
- viewBox: "0 0 16 16",
2424
- width: "14",
2425
- height: "14",
2426
- "aria-hidden": "true",
2427
- focusable: "false",
3469
+ type: "button",
3470
+ className: slot("button", "dwt-button", "dwt-show-button"),
3471
+ "aria-label": labels.expand(definition.title),
3472
+ onClick: () => changePage(toggleMinimized(view, definition.key)),
2428
3473
  children: [
2429
- /* @__PURE__ */ jsxRuntime.jsx(
2430
- "rect",
2431
- {
2432
- x: "3",
2433
- y: "7",
2434
- width: "10",
2435
- height: "7",
2436
- rx: "1.5",
2437
- fill: "currentColor"
2438
- }
2439
- ),
2440
- /* @__PURE__ */ jsxRuntime.jsx(
2441
- "path",
3474
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u25B8 " }),
3475
+ definition.title
3476
+ ]
3477
+ },
3478
+ definition.key
3479
+ ))
3480
+ ]
3481
+ }
3482
+ ),
3483
+ visible.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: slot("empty", "dwt-empty", "dwt-dashboard-empty"), children: paged && pageDefs.length === 0 ? labels.emptyPage : definitions.length === 0 ? labels.noWidgets : labels.allHidden }) : shown.length === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(
3484
+ "div",
3485
+ {
3486
+ ref: gridRef,
3487
+ className: slot("grid", "dwt-grid", twelve && "dwt-grid--twelve"),
3488
+ children: shown.map((definition, index) => {
3489
+ const widget = byKey.get(
3490
+ definition.key
3491
+ );
3492
+ if (widget === void 0) return null;
3493
+ const key = definition.key;
3494
+ const title = definition.title;
3495
+ const lock = resolveWidgetLock(definition);
3496
+ const ownLock = resolveWidgetLock(
3497
+ widget.definition
3498
+ );
3499
+ const isLocked = ownLock.move || ownLock.hide || ownLock.minimize;
3500
+ const lockLabel = overrideLocks ? labels.lockedForViewers(title) : labels.locked(title);
3501
+ const canMoveEarlier = !lock.move && moveWidgetBy(pageDefs, view, key, -1) !== view;
3502
+ const canMoveLater = !lock.move && moveWidgetBy(pageDefs, view, key, 1) !== view;
3503
+ const actions = controlsShown ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3504
+ isLocked && /* @__PURE__ */ jsxRuntime.jsx(
3505
+ "span",
3506
+ {
3507
+ role: "img",
3508
+ className: "dwt-lock",
3509
+ "aria-label": lockLabel,
3510
+ title: lockLabel,
3511
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
3512
+ "svg",
2442
3513
  {
2443
- d: "M5.5 7V5a2.5 2.5 0 0 1 5 0v2",
2444
- fill: "none",
2445
- stroke: "currentColor",
2446
- strokeWidth: "1.4"
3514
+ viewBox: "0 0 16 16",
3515
+ width: "14",
3516
+ height: "14",
3517
+ "aria-hidden": "true",
3518
+ focusable: "false",
3519
+ children: [
3520
+ /* @__PURE__ */ jsxRuntime.jsx(
3521
+ "rect",
3522
+ {
3523
+ x: "3",
3524
+ y: "7",
3525
+ width: "10",
3526
+ height: "7",
3527
+ rx: "1.5",
3528
+ fill: "currentColor"
3529
+ }
3530
+ ),
3531
+ /* @__PURE__ */ jsxRuntime.jsx(
3532
+ "path",
3533
+ {
3534
+ d: "M5.5 7V5a2.5 2.5 0 0 1 5 0v2",
3535
+ fill: "none",
3536
+ stroke: "currentColor",
3537
+ strokeWidth: "1.4"
3538
+ }
3539
+ )
3540
+ ]
2447
3541
  }
2448
3542
  )
2449
- ]
2450
- }
2451
- )
2452
- }
2453
- ),
2454
- !lock.move && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2455
- /* @__PURE__ */ jsxRuntime.jsx(
2456
- "button",
2457
- {
2458
- type: "button",
2459
- className: slot(
2460
- "button",
2461
- "dwt-button",
2462
- "dwt-icon-button"
3543
+ }
2463
3544
  ),
2464
- "aria-label": labels.moveEarlier(title),
2465
- title: labels.moveEarlier(title),
2466
- disabled: index === 0 || !canMoveEarlier,
2467
- onClick: () => change(moveWidgetBy(definitions, layout, key, -1)),
2468
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2191" })
2469
- }
2470
- ),
2471
- /* @__PURE__ */ jsxRuntime.jsx(
2472
- "button",
2473
- {
2474
- type: "button",
2475
- className: slot(
3545
+ !lock.move && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3546
+ /* @__PURE__ */ jsxRuntime.jsx(
3547
+ "button",
3548
+ {
3549
+ type: "button",
3550
+ className: slot(
3551
+ "button",
3552
+ "dwt-button",
3553
+ "dwt-icon-button"
3554
+ ),
3555
+ "aria-label": labels.moveEarlier(title),
3556
+ title: labels.moveEarlier(title),
3557
+ disabled: index === 0 || !canMoveEarlier,
3558
+ onClick: () => changePage(moveWidgetBy(pageDefs, view, key, -1)),
3559
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2191" })
3560
+ }
3561
+ ),
3562
+ /* @__PURE__ */ jsxRuntime.jsx(
3563
+ "button",
3564
+ {
3565
+ type: "button",
3566
+ className: slot(
3567
+ "button",
3568
+ "dwt-button",
3569
+ "dwt-icon-button"
3570
+ ),
3571
+ "aria-label": labels.moveLater(title),
3572
+ title: labels.moveLater(title),
3573
+ disabled: index === shown.length - 1 || !canMoveLater,
3574
+ onClick: () => changePage(moveWidgetBy(pageDefs, view, key, 1)),
3575
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2193" })
3576
+ }
3577
+ ),
3578
+ paged && /* @__PURE__ */ jsxRuntime.jsxs(
3579
+ "select",
3580
+ {
3581
+ className: "dwt-move-select",
3582
+ "aria-label": labels.moveToPage(title),
3583
+ value: "",
3584
+ onChange: (event) => {
3585
+ if (event.target.value !== "") {
3586
+ moveWidgetAction(key, event.target.value);
3587
+ }
3588
+ },
3589
+ children: [
3590
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: labels.moveToPagePlaceholder }),
3591
+ pages.filter((page) => page.key !== activeKey).map((page) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: page.key, children: labels.moveToPageOption(
3592
+ page.title,
3593
+ pageRoom(
3594
+ definitions,
3595
+ layout,
3596
+ page.key,
3597
+ roomOptions
3598
+ )?.rowsFree ?? 0
3599
+ ) }, page.key)),
3600
+ (maxPages === void 0 || pages.length < maxPages) && /* @__PURE__ */ jsxRuntime.jsx("option", { value: NEW_PAGE_VALUE, children: labels.newPageOption })
3601
+ ]
3602
+ }
3603
+ )
3604
+ ] }),
3605
+ !lock.hide && /* @__PURE__ */ jsxRuntime.jsx(
2476
3606
  "button",
2477
- "dwt-button",
2478
- "dwt-icon-button"
2479
- ),
2480
- "aria-label": labels.moveLater(title),
2481
- title: labels.moveLater(title),
2482
- disabled: index === shown.length - 1 || !canMoveLater,
2483
- onClick: () => change(moveWidgetBy(definitions, layout, key, 1)),
2484
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2193" })
2485
- }
2486
- )
2487
- ] }),
2488
- !lock.hide && /* @__PURE__ */ jsxRuntime.jsx(
2489
- "button",
2490
- {
2491
- type: "button",
2492
- className: slot("button", "dwt-button", "dwt-icon-button"),
2493
- "aria-label": labels.hide(title),
2494
- title: labels.hide(title),
2495
- onClick: () => change(hideWidget(layout, key)),
2496
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xD7" })
2497
- }
2498
- )
2499
- ] }) : void 0;
2500
- const viewable = resolveDetailOptions(definition) !== void 0 && (onOpenDetail !== void 0 || loadDetail !== void 0 || widget.status === "ok" && deriveDetailData(widget.data) !== void 0);
2501
- return /* @__PURE__ */ jsxRuntime.jsx(
2502
- ResolvedWidgetCard,
2503
- {
2504
- widget,
2505
- ...viewable ? {
2506
- onView: () => onOpenDetail === void 0 ? setDetailKey(key) : onOpenDetail(key)
2507
- } : {},
2508
- ...spanProps(spans, key),
2509
- ...widthProps(twelve, definition),
2510
- minimized: isMinimized(layout, key),
2511
- editing: toggleMode && controlsShown,
2512
- ...actions === void 0 ? {} : { actions },
2513
- ...editable && !lock.minimize ? {
2514
- onToggleMinimized: () => change(toggleMinimized(layout, key))
2515
- } : {},
2516
- ...headingLevel === void 0 ? {} : { headingLevel },
2517
- ...onRetry === void 0 || widget.status !== "error" ? {} : { onRetry: () => onRetry(key) }
2518
- },
2519
- key
2520
- );
2521
- })
3607
+ {
3608
+ type: "button",
3609
+ className: slot(
3610
+ "button",
3611
+ "dwt-button",
3612
+ "dwt-icon-button"
3613
+ ),
3614
+ "aria-label": labels.hide(title),
3615
+ title: labels.hide(title),
3616
+ onClick: () => changePage(hideWidget(view, key)),
3617
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xD7" })
3618
+ }
3619
+ )
3620
+ ] }) : void 0;
3621
+ const viewable = resolveDetailOptions(definition) !== void 0 && (onOpenDetail !== void 0 || loadDetail !== void 0 || widget.status === "ok" && deriveDetailData(widget.data) !== void 0);
3622
+ return /* @__PURE__ */ jsxRuntime.jsx(
3623
+ ResolvedWidgetCard,
3624
+ {
3625
+ widget,
3626
+ ...viewable ? {
3627
+ onView: () => onOpenDetail === void 0 ? setDetailKey(key) : onOpenDetail(key)
3628
+ } : {},
3629
+ ...spanProps(spans, key),
3630
+ ...widthProps(twelve, definition),
3631
+ minimized: isMinimized(view, key),
3632
+ editing: toggleMode && controlsShown,
3633
+ ...actions === void 0 ? {} : { actions },
3634
+ ...editable && !lock.minimize ? {
3635
+ onToggleMinimized: () => changePage(toggleMinimized(view, key))
3636
+ } : {},
3637
+ ...headingLevel === void 0 ? {} : { headingLevel },
3638
+ ...onRetry === void 0 || widget.status !== "error" ? {} : { onRetry: () => onRetry(key) }
3639
+ },
3640
+ key
3641
+ );
3642
+ })
3643
+ }
3644
+ )
3645
+ ]
2522
3646
  }
2523
3647
  ),
2524
3648
  detailKey !== void 0 && byKey.get(detailKey) !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
@@ -2555,6 +3679,196 @@ function Dashboard({
2555
3679
  }
2556
3680
  );
2557
3681
  }
3682
+ function useStoredLayout(options) {
3683
+ const {
3684
+ persistence,
3685
+ scope,
3686
+ definitions,
3687
+ defaultLayout,
3688
+ saveDelayMs = 500,
3689
+ initialLayout,
3690
+ normalizeOptions
3691
+ } = options;
3692
+ const scopeKey = persistence.keyFor(scope);
3693
+ const [layout, setLayoutState] = react.useState(
3694
+ initialLayout ?? defaultLayout
3695
+ );
3696
+ const [backup, setBackupState] = react.useState(null);
3697
+ const [status, setStatus] = react.useState("loading");
3698
+ const [error, setError] = react.useState(null);
3699
+ const [conflict, setConflict] = react.useState(null);
3700
+ const [remoteLayout, setRemoteLayout] = react.useState(
3701
+ null
3702
+ );
3703
+ const latest = react.useRef(layout);
3704
+ const scopeRef = react.useRef(scope);
3705
+ scopeRef.current = scope;
3706
+ const normalizeRef = react.useRef(normalizeOptions);
3707
+ normalizeRef.current = normalizeOptions;
3708
+ const timer = react.useRef(void 0);
3709
+ const dirty = react.useRef(false);
3710
+ const apply = react.useCallback((next) => {
3711
+ latest.current = next;
3712
+ setLayoutState(next);
3713
+ }, []);
3714
+ const finishSave = react.useCallback(
3715
+ (result, mine) => {
3716
+ if (!result.ok) {
3717
+ setError(result.error);
3718
+ setStatus("error");
3719
+ return;
3720
+ }
3721
+ const saved = result.value;
3722
+ if (saved.outcome === "saved") {
3723
+ dirty.current = latest.current !== mine;
3724
+ setError(null);
3725
+ setStatus(dirty.current ? "saving" : "ready");
3726
+ return;
3727
+ }
3728
+ if (saved.outcome === "kept-theirs") {
3729
+ dirty.current = false;
3730
+ apply(saved.layout);
3731
+ setError(null);
3732
+ setStatus("ready");
3733
+ return;
3734
+ }
3735
+ setConflict({ mine, theirs: saved.layout });
3736
+ setStatus("conflict");
3737
+ },
3738
+ [apply]
3739
+ );
3740
+ const flush = react.useCallback(() => {
3741
+ if (timer.current !== void 0) {
3742
+ clearTimeout(timer.current);
3743
+ timer.current = void 0;
3744
+ }
3745
+ if (!dirty.current) return;
3746
+ const mine = latest.current;
3747
+ setStatus("saving");
3748
+ void persistence.save(scopeRef.current, mine).then((result) => finishSave(result, mine));
3749
+ }, [persistence, finishSave]);
3750
+ react.useEffect(() => {
3751
+ let current = true;
3752
+ setStatus("loading");
3753
+ setConflict(null);
3754
+ setRemoteLayout(null);
3755
+ void persistence.load(scopeRef.current, definitions, normalizeRef.current).then((result) => {
3756
+ if (!current) return;
3757
+ if (!result.ok) {
3758
+ setError(result.error);
3759
+ setStatus("error");
3760
+ return;
3761
+ }
3762
+ setError(null);
3763
+ if (!dirty.current && result.value.layout !== null) {
3764
+ apply(result.value.layout);
3765
+ }
3766
+ setStatus(dirty.current ? "saving" : "ready");
3767
+ });
3768
+ void persistence.loadBackup(scopeRef.current).then((result) => {
3769
+ if (current && result.ok) setBackupState(result.value);
3770
+ });
3771
+ return () => {
3772
+ current = false;
3773
+ };
3774
+ }, [persistence, scopeKey, definitions, apply]);
3775
+ react.useEffect(() => {
3776
+ return persistence.watch(
3777
+ scopeRef.current,
3778
+ definitions,
3779
+ (loaded) => {
3780
+ if (loaded.layout === null) return;
3781
+ if (dirty.current) {
3782
+ setRemoteLayout(loaded.layout);
3783
+ return;
3784
+ }
3785
+ apply(loaded.layout);
3786
+ },
3787
+ normalizeRef.current
3788
+ );
3789
+ }, [persistence, scopeKey, definitions, apply]);
3790
+ react.useEffect(() => {
3791
+ const onHide = () => {
3792
+ if (typeof document === "undefined" || document.visibilityState !== "hidden") {
3793
+ return;
3794
+ }
3795
+ flush();
3796
+ };
3797
+ if (typeof window !== "undefined") {
3798
+ window.addEventListener("pagehide", flush);
3799
+ document.addEventListener("visibilitychange", onHide);
3800
+ }
3801
+ return () => {
3802
+ if (typeof window !== "undefined") {
3803
+ window.removeEventListener("pagehide", flush);
3804
+ document.removeEventListener("visibilitychange", onHide);
3805
+ }
3806
+ flush();
3807
+ };
3808
+ }, [flush]);
3809
+ const setLayout = react.useCallback(
3810
+ (next) => {
3811
+ apply(next);
3812
+ dirty.current = true;
3813
+ if (timer.current !== void 0) clearTimeout(timer.current);
3814
+ timer.current = setTimeout(flush, Math.max(0, saveDelayMs));
3815
+ },
3816
+ [apply, flush, saveDelayMs]
3817
+ );
3818
+ const setBackup = react.useCallback(
3819
+ (next) => {
3820
+ setBackupState(next);
3821
+ if (next === null) return;
3822
+ void persistence.saveBackup(scopeRef.current, next).then((result) => {
3823
+ if (!result.ok) setError(result.error);
3824
+ });
3825
+ },
3826
+ [persistence]
3827
+ );
3828
+ const resolveConflict = react.useCallback(
3829
+ (choice) => {
3830
+ const pending = conflict;
3831
+ if (pending === null) return;
3832
+ setConflict(null);
3833
+ if (choice === "theirs") {
3834
+ dirty.current = false;
3835
+ apply(pending.theirs);
3836
+ setStatus("ready");
3837
+ return;
3838
+ }
3839
+ setStatus("saving");
3840
+ void persistence.save(scopeRef.current, pending.mine, { force: true }).then((result) => finishSave(result, pending.mine));
3841
+ },
3842
+ [conflict, persistence, apply, finishSave]
3843
+ );
3844
+ const resolveRemote = react.useCallback(
3845
+ (choice) => {
3846
+ const remote = remoteLayout;
3847
+ setRemoteLayout(null);
3848
+ if (choice === "use" && remote !== null) {
3849
+ if (timer.current !== void 0) {
3850
+ clearTimeout(timer.current);
3851
+ timer.current = void 0;
3852
+ }
3853
+ dirty.current = false;
3854
+ apply(remote);
3855
+ }
3856
+ },
3857
+ [remoteLayout, apply]
3858
+ );
3859
+ return {
3860
+ layout,
3861
+ setLayout,
3862
+ backup,
3863
+ setBackup,
3864
+ status,
3865
+ error,
3866
+ conflict,
3867
+ resolveConflict,
3868
+ remoteLayout,
3869
+ resolveRemote
3870
+ };
3871
+ }
2558
3872
 
2559
3873
  // src/core/payload.ts
2560
3874
  var WIDGET_KINDS = [
@@ -2898,12 +4212,33 @@ function failedWidget(definition, error) {
2898
4212
  function loadingWidgets(definitions) {
2899
4213
  return definitions.map((definition) => ({ definition, status: "loading" }));
2900
4214
  }
2901
- function cacheKeyFor(definition, options) {
2902
- return options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
4215
+ function cacheKeyFor(definition, options, values) {
4216
+ const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
4217
+ return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
4218
+ }
4219
+ function optionValuesFor(definition, options) {
4220
+ return resolveOptionValues(
4221
+ definition,
4222
+ options.optionValues?.[definition.key],
4223
+ options
4224
+ );
4225
+ }
4226
+ function finalizeWidget(widget, values) {
4227
+ if (Object.keys(values).length === 0) return widget;
4228
+ if (widget.status === "ok") {
4229
+ const data = applyClientOptions(
4230
+ widget.definition,
4231
+ widget.data,
4232
+ values
4233
+ );
4234
+ return { ...widget, data, options: values };
4235
+ }
4236
+ return { ...widget, options: values };
2903
4237
  }
2904
4238
  var LoadInterruption = class extends Error {
2905
4239
  };
2906
- async function resolveWidget(definition, provider, context, options = {}) {
4240
+ async function resolveWidgetUnapplied(definition, provider, context, options = {}) {
4241
+ const values = optionValuesFor(definition, options);
2907
4242
  const quotedKey = JSON.stringify(definition.key);
2908
4243
  if (provider === void 0) {
2909
4244
  return failedWidget(
@@ -2938,7 +4273,10 @@ async function resolveWidget(definition, provider, context, options = {}) {
2938
4273
  payload = await Promise.race([
2939
4274
  Promise.resolve().then(() => {
2940
4275
  if (controller.signal.aborted) throw new LoadInterruption();
2941
- return provider(context, definition, { signal: controller.signal });
4276
+ return provider(context, definition, {
4277
+ signal: controller.signal,
4278
+ options: values
4279
+ });
2942
4280
  }),
2943
4281
  interrupted
2944
4282
  ]);
@@ -2962,7 +4300,7 @@ async function resolveWidget(definition, provider, context, options = {}) {
2962
4300
  const storedAt = Date.now();
2963
4301
  if (options.cache !== void 0) {
2964
4302
  try {
2965
- await options.cache.set(cacheKeyFor(definition, options), {
4303
+ await options.cache.set(cacheKeyFor(definition, options, values), {
2966
4304
  payload: resolved.data,
2967
4305
  storedAt
2968
4306
  });
@@ -2979,7 +4317,17 @@ function widgetsFor(definitions, roles) {
2979
4317
 
2980
4318
  // src/core/loader.ts
2981
4319
  function createWidgetLoader(options) {
2982
- const { definitions, providers, context, ...resolveOptions } = options;
4320
+ const { definitions, providers, context, ...baseOptions } = options;
4321
+ const chosen = new Map(
4322
+ Object.entries(baseOptions.optionValues ?? {}).flatMap(
4323
+ ([key, values]) => values === void 0 ? [] : [[key, values]]
4324
+ )
4325
+ );
4326
+ const resolveOptions = baseOptions;
4327
+ function currentOptions() {
4328
+ return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
4329
+ }
4330
+ const unapplied = /* @__PURE__ */ new Map();
2983
4331
  const visible = widgetsFor(definitions, context.roles);
2984
4332
  let snapshot = loadingWidgets(visible);
2985
4333
  const listeners = /* @__PURE__ */ new Set();
@@ -3020,19 +4368,19 @@ function createWidgetLoader(options) {
3020
4368
  const controller = new AbortController();
3021
4369
  inFlight.set(key, controller);
3022
4370
  const isCurrent = () => !disposed && generations.get(key) === generation;
3023
- if (readCache && resolveOptions.cache !== void 0) {
4371
+ const runOptions = currentOptions();
4372
+ const values = optionValuesFor(definition, runOptions);
4373
+ if (readCache && runOptions.cache !== void 0) {
3024
4374
  try {
3025
- const entry = await resolveOptions.cache.get(
3026
- cacheKeyFor(definition, resolveOptions)
4375
+ const entry = await runOptions.cache.get(
4376
+ cacheKeyFor(definition, runOptions, values)
3027
4377
  );
3028
4378
  if (entry !== void 0 && isCurrent()) {
3029
- const cached = resolvePayload(
3030
- definition,
3031
- entry.payload,
3032
- resolveOptions
3033
- );
4379
+ const cached = resolvePayload(definition, entry.payload, runOptions);
3034
4380
  if (cached.status === "ok") {
3035
- replace(key, { ...cached, updatedAt: entry.storedAt, stale: true });
4381
+ const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
4382
+ unapplied.set(key, stale);
4383
+ replace(key, finalizeWidget(stale, values));
3036
4384
  }
3037
4385
  }
3038
4386
  } catch {
@@ -3040,13 +4388,15 @@ function createWidgetLoader(options) {
3040
4388
  }
3041
4389
  if (!isCurrent()) return;
3042
4390
  const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
3043
- const result = await resolveWidget(definition, provider, context, {
3044
- ...resolveOptions,
4391
+ const result = await resolveWidgetUnapplied(definition, provider, context, {
4392
+ ...runOptions,
3045
4393
  signal: controller.signal
3046
4394
  });
3047
4395
  if (!isCurrent()) return;
3048
4396
  inFlight.delete(key);
3049
- replace(key, result);
4397
+ if (result.status === "ok") unapplied.set(key, result);
4398
+ else unapplied.delete(key);
4399
+ replace(key, finalizeWidget(result, values));
3050
4400
  }
3051
4401
  function start(definition, readCache) {
3052
4402
  started.add(definition.key);
@@ -3081,6 +4431,29 @@ function createWidgetLoader(options) {
3081
4431
  start(definition, false);
3082
4432
  }
3083
4433
  }
4434
+ function setOptions(key, values) {
4435
+ if (disposed) return;
4436
+ const definition = definitionFor(key);
4437
+ if (definition === void 0) return;
4438
+ const before = optionValuesFor(definition, currentOptions());
4439
+ chosen.set(key, { ...values });
4440
+ const runOptions = currentOptions();
4441
+ const after = optionValuesFor(definition, runOptions);
4442
+ if (sameOptionValues(before, after) || !started.has(key)) return;
4443
+ const kept = unapplied.get(key);
4444
+ if (kept !== void 0 && optionsCacheSuffix(definition, before, runOptions) === optionsCacheSuffix(definition, after, runOptions)) {
4445
+ replace(key, finalizeWidget(kept, after));
4446
+ return;
4447
+ }
4448
+ const current = snapshot.find(
4449
+ (widget) => widget.definition.key === key
4450
+ );
4451
+ if (current?.status === "ok") replace(key, { ...current, stale: true });
4452
+ else if (current?.status === "error") {
4453
+ replace(key, { definition, status: "loading" });
4454
+ }
4455
+ start(definition, true);
4456
+ }
3084
4457
  function dispose() {
3085
4458
  disposed = true;
3086
4459
  abortAll(new Error("The widget loader was disposed."));
@@ -3098,6 +4471,7 @@ function createWidgetLoader(options) {
3098
4471
  load,
3099
4472
  loadAll,
3100
4473
  refresh,
4474
+ setOptions,
3101
4475
  dispose
3102
4476
  };
3103
4477
  }
@@ -3112,7 +4486,8 @@ function useWidgets(definitions, providers, context, options = {}) {
3112
4486
  timeoutMs,
3113
4487
  signal,
3114
4488
  cache,
3115
- cacheKey
4489
+ cacheKey,
4490
+ optionValues
3116
4491
  } = options;
3117
4492
  const placeholders = react.useMemo(
3118
4493
  () => loadingWidgets(widgetsFor(definitions, context.roles)),
@@ -3122,6 +4497,8 @@ function useWidgets(definitions, providers, context, options = {}) {
3122
4497
  const [loader, setLoader] = react.useState(null);
3123
4498
  const loaderRef = react.useRef(null);
3124
4499
  const gridRef = react.useRef(null);
4500
+ const optionValuesRef = react.useRef(optionValues);
4501
+ optionValuesRef.current = optionValues;
3125
4502
  react.useEffect(() => {
3126
4503
  const created = createWidgetLoader({
3127
4504
  definitions,
@@ -3131,7 +4508,8 @@ function useWidgets(definitions, providers, context, options = {}) {
3131
4508
  ...timeoutMs === void 0 ? {} : { timeoutMs },
3132
4509
  ...signal === void 0 ? {} : { signal },
3133
4510
  ...cache === void 0 ? {} : { cache },
3134
- ...cacheKey === void 0 ? {} : { cacheKey }
4511
+ ...cacheKey === void 0 ? {} : { cacheKey },
4512
+ ...optionValuesRef.current === void 0 ? {} : { optionValues: optionValuesRef.current }
3135
4513
  });
3136
4514
  loaderRef.current = created;
3137
4515
  setWidgets(created.getSnapshot());
@@ -3177,8 +4555,21 @@ function useWidgets(definitions, providers, context, options = {}) {
3177
4555
  },
3178
4556
  { rootMargin }
3179
4557
  );
3180
- grid.querySelectorAll("[data-widget-key]").forEach((card) => observer.observe(card));
3181
- return () => observer.disconnect();
4558
+ const observed = /* @__PURE__ */ new WeakSet();
4559
+ const observeNewCards = () => {
4560
+ grid.querySelectorAll("[data-widget-key]").forEach((card) => {
4561
+ if (observed.has(card)) return;
4562
+ observed.add(card);
4563
+ observer.observe(card);
4564
+ });
4565
+ };
4566
+ observeNewCards();
4567
+ const mutations = typeof MutationObserver === "undefined" ? void 0 : new MutationObserver(observeNewCards);
4568
+ mutations?.observe(grid, { childList: true, subtree: true });
4569
+ return () => {
4570
+ mutations?.disconnect();
4571
+ observer.disconnect();
4572
+ };
3182
4573
  }, [loader, loadWhen, rootMargin, cardKeys]);
3183
4574
  react.useEffect(() => {
3184
4575
  if (loader === null || !(refreshMs > 0)) return void 0;
@@ -3190,10 +4581,23 @@ function useWidgets(definitions, providers, context, options = {}) {
3190
4581
  }, refreshMs);
3191
4582
  return () => clearInterval(timer);
3192
4583
  }, [loader, refreshMs]);
4584
+ const optionSignature = JSON.stringify(optionValues ?? {});
4585
+ react.useEffect(() => {
4586
+ if (loader === null) return;
4587
+ for (const [key, chosen] of Object.entries(optionValuesRef.current ?? {})) {
4588
+ if (chosen !== void 0) loader.setOptions(key, chosen);
4589
+ }
4590
+ }, [loader, optionSignature]);
4591
+ const setOptions = react.useCallback(
4592
+ (key, chosen) => {
4593
+ loaderRef.current?.setOptions(key, chosen);
4594
+ },
4595
+ []
4596
+ );
3193
4597
  const refresh = react.useCallback((key) => {
3194
4598
  loaderRef.current?.refresh(key);
3195
4599
  }, []);
3196
- return { widgets, refresh, gridRef };
4600
+ return { widgets, refresh, setOptions, gridRef };
3197
4601
  }
3198
4602
 
3199
4603
  exports.AlertListWidget = AlertListWidget;
@@ -3221,6 +4625,7 @@ exports.describeChart = describeChart;
3221
4625
  exports.joinClassNames = joinClassNames;
3222
4626
  exports.seriesColor = seriesColor;
3223
4627
  exports.useDetailData = useDetailData;
4628
+ exports.useStoredLayout = useStoredLayout;
3224
4629
  exports.useWidgetSettings = useWidgetSettings;
3225
4630
  exports.useWidgets = useWidgets;
3226
4631
  exports.widgetTitle = widgetTitle;