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