@richardmcquiston01/dashboard-widgets-toolkit 0.10.0 → 0.11.1

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
@@ -472,6 +472,265 @@ function placeAndFillRows(items, columns, spanOf) {
472
472
  return { spans, rows };
473
473
  }
474
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;
732
+ }
733
+
475
734
  // src/core/scale.ts
476
735
  function niceStep(value) {
477
736
  if (!(value > 0) || !Number.isFinite(value)) return 1;
@@ -1238,7 +1497,8 @@ function GaugeWidget({
1238
1497
  }
1239
1498
  function TableWidget({
1240
1499
  data,
1241
- controls
1500
+ controls,
1501
+ initialSort
1242
1502
  }) {
1243
1503
  if (controls === void 0) {
1244
1504
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-table-wrap", children: [
@@ -1246,7 +1506,14 @@ function TableWidget({
1246
1506
  data.footer !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "dwt-note", children: data.footer })
1247
1507
  ] });
1248
1508
  }
1249
- return /* @__PURE__ */ jsxRuntime.jsx(InteractiveTable, { data, controls });
1509
+ return /* @__PURE__ */ jsxRuntime.jsx(
1510
+ InteractiveTable,
1511
+ {
1512
+ data,
1513
+ controls,
1514
+ initialSort
1515
+ }
1516
+ );
1250
1517
  }
1251
1518
  function TableView({
1252
1519
  data,
@@ -1299,13 +1566,20 @@ function nextTableSort(current, column) {
1299
1566
  }
1300
1567
  function InteractiveTable({
1301
1568
  data,
1302
- controls
1569
+ controls,
1570
+ initialSort
1303
1571
  }) {
1304
1572
  const slot = useSlotClassName();
1305
1573
  const { labels, locale } = useWidgetSettings();
1306
1574
  const searchId = react.useId();
1307
1575
  const [search, setSearch] = react.useState("");
1308
- const [sort, setSort] = react.useState(void 0);
1576
+ const [sort, setSort] = react.useState(initialSort);
1577
+ const seed = initialSort === void 0 ? "" : `${initialSort.column}:${initialSort.direction}`;
1578
+ const [seededWith, setSeededWith] = react.useState(seed);
1579
+ if (seededWith !== seed) {
1580
+ setSeededWith(seed);
1581
+ setSort(initialSort);
1582
+ }
1309
1583
  const localeTag = typeof locale === "string" ? locale : locale?.[0];
1310
1584
  const query = {
1311
1585
  page: 1,
@@ -1407,7 +1681,8 @@ function AlertListWidget({
1407
1681
  }
1408
1682
  function WidgetContent({
1409
1683
  data,
1410
- tableControls
1684
+ tableControls,
1685
+ initialSort
1411
1686
  }) {
1412
1687
  switch (data.kind) {
1413
1688
  case "TEXT":
@@ -1417,7 +1692,14 @@ function WidgetContent({
1417
1692
  case "GAUGE":
1418
1693
  return /* @__PURE__ */ jsxRuntime.jsx(GaugeWidget, { data });
1419
1694
  case "TABLE":
1420
- return /* @__PURE__ */ jsxRuntime.jsx(TableWidget, { data, controls: tableControls });
1695
+ return /* @__PURE__ */ jsxRuntime.jsx(
1696
+ TableWidget,
1697
+ {
1698
+ data,
1699
+ controls: tableControls,
1700
+ initialSort
1701
+ }
1702
+ );
1421
1703
  case "BAR_LIST":
1422
1704
  return /* @__PURE__ */ jsxRuntime.jsx(BarListWidget, { data });
1423
1705
  case "ALERT_LIST":
@@ -1614,7 +1896,8 @@ function ResolvedWidgetCard({
1614
1896
  WidgetContent,
1615
1897
  {
1616
1898
  data: widget.data,
1617
- tableControls: resolveTableControls(definition)
1899
+ tableControls: resolveTableControls(definition),
1900
+ initialSort: sortFromOptions(definition, widget.options)
1618
1901
  }
1619
1902
  ) : null
1620
1903
  }
@@ -2376,7 +2659,8 @@ function WidgetDetailDialog({
2376
2659
  }
2377
2660
  );
2378
2661
  }
2379
- function useDetailData(definition, load, enabled, fallback) {
2662
+ var NO_OPTION_VALUES = Object.freeze({});
2663
+ function useDetailData(definition, load, enabled, fallback, optionValues = NO_OPTION_VALUES) {
2380
2664
  const [state, setState] = react.useState({ status: "loading" });
2381
2665
  const [attempt, setAttempt] = react.useState(0);
2382
2666
  react.useEffect(() => {
@@ -2386,6 +2670,7 @@ function useDetailData(definition, load, enabled, fallback) {
2386
2670
  Promise.resolve().then(
2387
2671
  () => load === void 0 ? void 0 : load(definition, {
2388
2672
  signal: controller.signal,
2673
+ options: optionValues,
2389
2674
  query: defaultDetailQuery()
2390
2675
  })
2391
2676
  ).then((raw) => {
@@ -2413,7 +2698,7 @@ function useDetailData(definition, load, enabled, fallback) {
2413
2698
  });
2414
2699
  });
2415
2700
  return () => controller.abort();
2416
- }, [enabled, load, fallback, definition, attempt]);
2701
+ }, [enabled, load, fallback, definition, optionValues, attempt]);
2417
2702
  return { state, reload: () => setAttempt((count) => count + 1) };
2418
2703
  }
2419
2704
  var NEW_PAGE_VALUE = "__new-page__";
@@ -2529,7 +2814,14 @@ function DetailHost({
2529
2814
  const { definition } = widget;
2530
2815
  const options = resolveDetailOptions(definition);
2531
2816
  const pageSize = options?.pageSize ?? 25;
2532
- const [query, setQuery] = react.useState(defaultDetailQuery(pageSize));
2817
+ const optionSort = sortFromOptions(
2818
+ definition,
2819
+ widget.options
2820
+ );
2821
+ const [query, setQuery] = react.useState({
2822
+ ...defaultDetailQuery(pageSize),
2823
+ ...optionSort === void 0 ? {} : { sort: optionSort }
2824
+ });
2533
2825
  const derived = react.useMemo(
2534
2826
  () => widget.status === "ok" ? deriveDetailData(widget.data, locale) : void 0,
2535
2827
  [widget, locale]
@@ -2538,7 +2830,8 @@ function DetailHost({
2538
2830
  definition,
2539
2831
  loadDetail,
2540
2832
  true,
2541
- derived
2833
+ derived,
2834
+ widget.options
2542
2835
  );
2543
2836
  const title = options?.title ?? definition.title;
2544
2837
  return /* @__PURE__ */ jsxRuntime.jsx(WidgetDetailDialog, { title, open: true, onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -3392,6 +3685,196 @@ function Dashboard({
3392
3685
  }
3393
3686
  );
3394
3687
  }
3688
+ function useStoredLayout(options) {
3689
+ const {
3690
+ persistence,
3691
+ scope,
3692
+ definitions,
3693
+ defaultLayout,
3694
+ saveDelayMs = 500,
3695
+ initialLayout,
3696
+ normalizeOptions
3697
+ } = options;
3698
+ const scopeKey = persistence.keyFor(scope);
3699
+ const [layout, setLayoutState] = react.useState(
3700
+ initialLayout ?? defaultLayout
3701
+ );
3702
+ const [backup, setBackupState] = react.useState(null);
3703
+ const [status, setStatus] = react.useState("loading");
3704
+ const [error, setError] = react.useState(null);
3705
+ const [conflict, setConflict] = react.useState(null);
3706
+ const [remoteLayout, setRemoteLayout] = react.useState(
3707
+ null
3708
+ );
3709
+ const latest = react.useRef(layout);
3710
+ const scopeRef = react.useRef(scope);
3711
+ scopeRef.current = scope;
3712
+ const normalizeRef = react.useRef(normalizeOptions);
3713
+ normalizeRef.current = normalizeOptions;
3714
+ const timer = react.useRef(void 0);
3715
+ const dirty = react.useRef(false);
3716
+ const apply = react.useCallback((next) => {
3717
+ latest.current = next;
3718
+ setLayoutState(next);
3719
+ }, []);
3720
+ const finishSave = react.useCallback(
3721
+ (result, mine) => {
3722
+ if (!result.ok) {
3723
+ setError(result.error);
3724
+ setStatus("error");
3725
+ return;
3726
+ }
3727
+ const saved = result.value;
3728
+ if (saved.outcome === "saved") {
3729
+ dirty.current = latest.current !== mine;
3730
+ setError(null);
3731
+ setStatus(dirty.current ? "saving" : "ready");
3732
+ return;
3733
+ }
3734
+ if (saved.outcome === "kept-theirs") {
3735
+ dirty.current = false;
3736
+ apply(saved.layout);
3737
+ setError(null);
3738
+ setStatus("ready");
3739
+ return;
3740
+ }
3741
+ setConflict({ mine, theirs: saved.layout });
3742
+ setStatus("conflict");
3743
+ },
3744
+ [apply]
3745
+ );
3746
+ const flush = react.useCallback(() => {
3747
+ if (timer.current !== void 0) {
3748
+ clearTimeout(timer.current);
3749
+ timer.current = void 0;
3750
+ }
3751
+ if (!dirty.current) return;
3752
+ const mine = latest.current;
3753
+ setStatus("saving");
3754
+ void persistence.save(scopeRef.current, mine).then((result) => finishSave(result, mine));
3755
+ }, [persistence, finishSave]);
3756
+ react.useEffect(() => {
3757
+ let current = true;
3758
+ setStatus("loading");
3759
+ setConflict(null);
3760
+ setRemoteLayout(null);
3761
+ void persistence.load(scopeRef.current, definitions, normalizeRef.current).then((result) => {
3762
+ if (!current) return;
3763
+ if (!result.ok) {
3764
+ setError(result.error);
3765
+ setStatus("error");
3766
+ return;
3767
+ }
3768
+ setError(null);
3769
+ if (!dirty.current && result.value.layout !== null) {
3770
+ apply(result.value.layout);
3771
+ }
3772
+ setStatus(dirty.current ? "saving" : "ready");
3773
+ });
3774
+ void persistence.loadBackup(scopeRef.current).then((result) => {
3775
+ if (current && result.ok) setBackupState(result.value);
3776
+ });
3777
+ return () => {
3778
+ current = false;
3779
+ };
3780
+ }, [persistence, scopeKey, definitions, apply]);
3781
+ react.useEffect(() => {
3782
+ return persistence.watch(
3783
+ scopeRef.current,
3784
+ definitions,
3785
+ (loaded) => {
3786
+ if (loaded.layout === null) return;
3787
+ if (dirty.current) {
3788
+ setRemoteLayout(loaded.layout);
3789
+ return;
3790
+ }
3791
+ apply(loaded.layout);
3792
+ },
3793
+ normalizeRef.current
3794
+ );
3795
+ }, [persistence, scopeKey, definitions, apply]);
3796
+ react.useEffect(() => {
3797
+ const onHide = () => {
3798
+ if (typeof document === "undefined" || document.visibilityState !== "hidden") {
3799
+ return;
3800
+ }
3801
+ flush();
3802
+ };
3803
+ if (typeof window !== "undefined") {
3804
+ window.addEventListener("pagehide", flush);
3805
+ document.addEventListener("visibilitychange", onHide);
3806
+ }
3807
+ return () => {
3808
+ if (typeof window !== "undefined") {
3809
+ window.removeEventListener("pagehide", flush);
3810
+ document.removeEventListener("visibilitychange", onHide);
3811
+ }
3812
+ flush();
3813
+ };
3814
+ }, [flush]);
3815
+ const setLayout = react.useCallback(
3816
+ (next) => {
3817
+ apply(next);
3818
+ dirty.current = true;
3819
+ if (timer.current !== void 0) clearTimeout(timer.current);
3820
+ timer.current = setTimeout(flush, Math.max(0, saveDelayMs));
3821
+ },
3822
+ [apply, flush, saveDelayMs]
3823
+ );
3824
+ const setBackup = react.useCallback(
3825
+ (next) => {
3826
+ setBackupState(next);
3827
+ if (next === null) return;
3828
+ void persistence.saveBackup(scopeRef.current, next).then((result) => {
3829
+ if (!result.ok) setError(result.error);
3830
+ });
3831
+ },
3832
+ [persistence]
3833
+ );
3834
+ const resolveConflict = react.useCallback(
3835
+ (choice) => {
3836
+ const pending = conflict;
3837
+ if (pending === null) return;
3838
+ setConflict(null);
3839
+ if (choice === "theirs") {
3840
+ dirty.current = false;
3841
+ apply(pending.theirs);
3842
+ setStatus("ready");
3843
+ return;
3844
+ }
3845
+ setStatus("saving");
3846
+ void persistence.save(scopeRef.current, pending.mine, { force: true }).then((result) => finishSave(result, pending.mine));
3847
+ },
3848
+ [conflict, persistence, apply, finishSave]
3849
+ );
3850
+ const resolveRemote = react.useCallback(
3851
+ (choice) => {
3852
+ const remote = remoteLayout;
3853
+ setRemoteLayout(null);
3854
+ if (choice === "use" && remote !== null) {
3855
+ if (timer.current !== void 0) {
3856
+ clearTimeout(timer.current);
3857
+ timer.current = void 0;
3858
+ }
3859
+ dirty.current = false;
3860
+ apply(remote);
3861
+ }
3862
+ },
3863
+ [remoteLayout, apply]
3864
+ );
3865
+ return {
3866
+ layout,
3867
+ setLayout,
3868
+ backup,
3869
+ setBackup,
3870
+ status,
3871
+ error,
3872
+ conflict,
3873
+ resolveConflict,
3874
+ remoteLayout,
3875
+ resolveRemote
3876
+ };
3877
+ }
3395
3878
 
3396
3879
  // src/core/payload.ts
3397
3880
  var WIDGET_KINDS = [
@@ -3735,12 +4218,33 @@ function failedWidget(definition, error) {
3735
4218
  function loadingWidgets(definitions) {
3736
4219
  return definitions.map((definition) => ({ definition, status: "loading" }));
3737
4220
  }
3738
- function cacheKeyFor(definition, options) {
3739
- return options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
4221
+ function cacheKeyFor(definition, options, values) {
4222
+ const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
4223
+ return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
4224
+ }
4225
+ function optionValuesFor(definition, options) {
4226
+ return resolveOptionValues(
4227
+ definition,
4228
+ options.optionValues?.[definition.key],
4229
+ options
4230
+ );
4231
+ }
4232
+ function finalizeWidget(widget, values) {
4233
+ if (Object.keys(values).length === 0) return widget;
4234
+ if (widget.status === "ok") {
4235
+ const data = applyClientOptions(
4236
+ widget.definition,
4237
+ widget.data,
4238
+ values
4239
+ );
4240
+ return { ...widget, data, options: values };
4241
+ }
4242
+ return { ...widget, options: values };
3740
4243
  }
3741
4244
  var LoadInterruption = class extends Error {
3742
4245
  };
3743
- async function resolveWidget(definition, provider, context, options = {}) {
4246
+ async function resolveWidgetUnapplied(definition, provider, context, options = {}) {
4247
+ const values = optionValuesFor(definition, options);
3744
4248
  const quotedKey = JSON.stringify(definition.key);
3745
4249
  if (provider === void 0) {
3746
4250
  return failedWidget(
@@ -3775,7 +4279,10 @@ async function resolveWidget(definition, provider, context, options = {}) {
3775
4279
  payload = await Promise.race([
3776
4280
  Promise.resolve().then(() => {
3777
4281
  if (controller.signal.aborted) throw new LoadInterruption();
3778
- return provider(context, definition, { signal: controller.signal });
4282
+ return provider(context, definition, {
4283
+ signal: controller.signal,
4284
+ options: values
4285
+ });
3779
4286
  }),
3780
4287
  interrupted
3781
4288
  ]);
@@ -3799,7 +4306,7 @@ async function resolveWidget(definition, provider, context, options = {}) {
3799
4306
  const storedAt = Date.now();
3800
4307
  if (options.cache !== void 0) {
3801
4308
  try {
3802
- await options.cache.set(cacheKeyFor(definition, options), {
4309
+ await options.cache.set(cacheKeyFor(definition, options, values), {
3803
4310
  payload: resolved.data,
3804
4311
  storedAt
3805
4312
  });
@@ -3816,7 +4323,17 @@ function widgetsFor(definitions, roles) {
3816
4323
 
3817
4324
  // src/core/loader.ts
3818
4325
  function createWidgetLoader(options) {
3819
- const { definitions, providers, context, ...resolveOptions } = options;
4326
+ const { definitions, providers, context, ...baseOptions } = options;
4327
+ const chosen = new Map(
4328
+ Object.entries(baseOptions.optionValues ?? {}).flatMap(
4329
+ ([key, values]) => values === void 0 ? [] : [[key, values]]
4330
+ )
4331
+ );
4332
+ const resolveOptions = baseOptions;
4333
+ function currentOptions() {
4334
+ return { ...resolveOptions, optionValues: Object.fromEntries(chosen) };
4335
+ }
4336
+ const unapplied = /* @__PURE__ */ new Map();
3820
4337
  const visible = widgetsFor(definitions, context.roles);
3821
4338
  let snapshot = loadingWidgets(visible);
3822
4339
  const listeners = /* @__PURE__ */ new Set();
@@ -3857,19 +4374,19 @@ function createWidgetLoader(options) {
3857
4374
  const controller = new AbortController();
3858
4375
  inFlight.set(key, controller);
3859
4376
  const isCurrent = () => !disposed && generations.get(key) === generation;
3860
- if (readCache && resolveOptions.cache !== void 0) {
4377
+ const runOptions = currentOptions();
4378
+ const values = optionValuesFor(definition, runOptions);
4379
+ if (readCache && runOptions.cache !== void 0) {
3861
4380
  try {
3862
- const entry = await resolveOptions.cache.get(
3863
- cacheKeyFor(definition, resolveOptions)
4381
+ const entry = await runOptions.cache.get(
4382
+ cacheKeyFor(definition, runOptions, values)
3864
4383
  );
3865
4384
  if (entry !== void 0 && isCurrent()) {
3866
- const cached = resolvePayload(
3867
- definition,
3868
- entry.payload,
3869
- resolveOptions
3870
- );
4385
+ const cached = resolvePayload(definition, entry.payload, runOptions);
3871
4386
  if (cached.status === "ok") {
3872
- replace(key, { ...cached, updatedAt: entry.storedAt, stale: true });
4387
+ const stale = { ...cached, updatedAt: entry.storedAt, stale: true };
4388
+ unapplied.set(key, stale);
4389
+ replace(key, finalizeWidget(stale, values));
3873
4390
  }
3874
4391
  }
3875
4392
  } catch {
@@ -3877,13 +4394,15 @@ function createWidgetLoader(options) {
3877
4394
  }
3878
4395
  if (!isCurrent()) return;
3879
4396
  const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
3880
- const result = await resolveWidget(definition, provider, context, {
3881
- ...resolveOptions,
4397
+ const result = await resolveWidgetUnapplied(definition, provider, context, {
4398
+ ...runOptions,
3882
4399
  signal: controller.signal
3883
4400
  });
3884
4401
  if (!isCurrent()) return;
3885
4402
  inFlight.delete(key);
3886
- replace(key, result);
4403
+ if (result.status === "ok") unapplied.set(key, result);
4404
+ else unapplied.delete(key);
4405
+ replace(key, finalizeWidget(result, values));
3887
4406
  }
3888
4407
  function start(definition, readCache) {
3889
4408
  started.add(definition.key);
@@ -3918,6 +4437,29 @@ function createWidgetLoader(options) {
3918
4437
  start(definition, false);
3919
4438
  }
3920
4439
  }
4440
+ function setOptions(key, values) {
4441
+ if (disposed) return;
4442
+ const definition = definitionFor(key);
4443
+ if (definition === void 0) return;
4444
+ const before = optionValuesFor(definition, currentOptions());
4445
+ chosen.set(key, { ...values });
4446
+ const runOptions = currentOptions();
4447
+ const after = optionValuesFor(definition, runOptions);
4448
+ if (sameOptionValues(before, after) || !started.has(key)) return;
4449
+ const kept = unapplied.get(key);
4450
+ if (kept !== void 0 && optionsCacheSuffix(definition, before, runOptions) === optionsCacheSuffix(definition, after, runOptions)) {
4451
+ replace(key, finalizeWidget(kept, after));
4452
+ return;
4453
+ }
4454
+ const current = snapshot.find(
4455
+ (widget) => widget.definition.key === key
4456
+ );
4457
+ if (current?.status === "ok") replace(key, { ...current, stale: true });
4458
+ else if (current?.status === "error") {
4459
+ replace(key, { definition, status: "loading" });
4460
+ }
4461
+ start(definition, true);
4462
+ }
3921
4463
  function dispose() {
3922
4464
  disposed = true;
3923
4465
  abortAll(new Error("The widget loader was disposed."));
@@ -3935,6 +4477,7 @@ function createWidgetLoader(options) {
3935
4477
  load,
3936
4478
  loadAll,
3937
4479
  refresh,
4480
+ setOptions,
3938
4481
  dispose
3939
4482
  };
3940
4483
  }
@@ -3949,7 +4492,8 @@ function useWidgets(definitions, providers, context, options = {}) {
3949
4492
  timeoutMs,
3950
4493
  signal,
3951
4494
  cache,
3952
- cacheKey
4495
+ cacheKey,
4496
+ optionValues
3953
4497
  } = options;
3954
4498
  const placeholders = react.useMemo(
3955
4499
  () => loadingWidgets(widgetsFor(definitions, context.roles)),
@@ -3959,6 +4503,8 @@ function useWidgets(definitions, providers, context, options = {}) {
3959
4503
  const [loader, setLoader] = react.useState(null);
3960
4504
  const loaderRef = react.useRef(null);
3961
4505
  const gridRef = react.useRef(null);
4506
+ const optionValuesRef = react.useRef(optionValues);
4507
+ optionValuesRef.current = optionValues;
3962
4508
  react.useEffect(() => {
3963
4509
  const created = createWidgetLoader({
3964
4510
  definitions,
@@ -3968,7 +4514,8 @@ function useWidgets(definitions, providers, context, options = {}) {
3968
4514
  ...timeoutMs === void 0 ? {} : { timeoutMs },
3969
4515
  ...signal === void 0 ? {} : { signal },
3970
4516
  ...cache === void 0 ? {} : { cache },
3971
- ...cacheKey === void 0 ? {} : { cacheKey }
4517
+ ...cacheKey === void 0 ? {} : { cacheKey },
4518
+ ...optionValuesRef.current === void 0 ? {} : { optionValues: optionValuesRef.current }
3972
4519
  });
3973
4520
  loaderRef.current = created;
3974
4521
  setWidgets(created.getSnapshot());
@@ -4040,10 +4587,23 @@ function useWidgets(definitions, providers, context, options = {}) {
4040
4587
  }, refreshMs);
4041
4588
  return () => clearInterval(timer);
4042
4589
  }, [loader, refreshMs]);
4590
+ const optionSignature = JSON.stringify(optionValues ?? {});
4591
+ react.useEffect(() => {
4592
+ if (loader === null) return;
4593
+ for (const [key, chosen] of Object.entries(optionValuesRef.current ?? {})) {
4594
+ if (chosen !== void 0) loader.setOptions(key, chosen);
4595
+ }
4596
+ }, [loader, optionSignature]);
4597
+ const setOptions = react.useCallback(
4598
+ (key, chosen) => {
4599
+ loaderRef.current?.setOptions(key, chosen);
4600
+ },
4601
+ []
4602
+ );
4043
4603
  const refresh = react.useCallback((key) => {
4044
4604
  loaderRef.current?.refresh(key);
4045
4605
  }, []);
4046
- return { widgets, refresh, gridRef };
4606
+ return { widgets, refresh, setOptions, gridRef };
4047
4607
  }
4048
4608
 
4049
4609
  exports.AlertListWidget = AlertListWidget;
@@ -4071,6 +4631,7 @@ exports.describeChart = describeChart;
4071
4631
  exports.joinClassNames = joinClassNames;
4072
4632
  exports.seriesColor = seriesColor;
4073
4633
  exports.useDetailData = useDetailData;
4634
+ exports.useStoredLayout = useStoredLayout;
4074
4635
  exports.useWidgetSettings = useWidgetSettings;
4075
4636
  exports.useWidgets = useWidgets;
4076
4637
  exports.widgetTitle = widgetTitle;