@richardmcquiston01/dashboard-widgets-toolkit 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -829,6 +829,30 @@ var DEFAULT_LABELS = {
829
829
  menuMoveEarlier: "Move earlier",
830
830
  menuMoveLater: "Move later",
831
831
  menuHide: "Hide",
832
+ menuOptions: "Options\u2026",
833
+ optionsFor: (title) => `Options for ${title}`,
834
+ optionsTitleField: "Title",
835
+ titleInUse: "Another widget already uses this title.",
836
+ optionsWidthField: "Width",
837
+ widthDefault: "Default",
838
+ widthOption: (width, share) => `${width} of 12 (${share})`,
839
+ optionsFlexible: "Grow to fill empty space in the row",
840
+ optionsDeclared: "Data",
841
+ apply: "Apply",
842
+ cancel: "Cancel",
843
+ resetToDefaults: "Reset to defaults",
844
+ revertToBeforeEditing: "Revert to before editing",
845
+ confirmApply: "Apply these changes?",
846
+ confirmDiscard: "Discard your changes?",
847
+ confirmResetOptions: "Reset these options to their defaults?",
848
+ confirmRevertOptions: "Revert these options to how they were before you started editing?",
849
+ optionsSaved: (title) => `Options saved for ${title}`,
850
+ sortDefault: "Default order",
851
+ sortAscending: "Ascending",
852
+ sortDescending: "Descending",
853
+ moveColumnUp: (column) => `Move ${column} up`,
854
+ moveColumnDown: (column) => `Move ${column} down`,
855
+ customRange: (value) => `Custom: ${value}`,
832
856
  moveToPage: (title) => `Move ${title} to another page`,
833
857
  moveToPagePlaceholder: "Move to page\u2026",
834
858
  moveToPageOption: (title, rowsFree) => `${title} (${rowsFree} ${rowsFree === 1 ? "row" : "rows"} free)`,
@@ -1912,12 +1936,14 @@ function ResolvedWidgetCard({
1912
1936
  var NOT_LOCKED = Object.freeze({
1913
1937
  move: false,
1914
1938
  hide: false,
1915
- minimize: false
1939
+ minimize: false,
1940
+ options: false
1916
1941
  });
1917
1942
  var FULLY_LOCKED = Object.freeze({
1918
1943
  move: true,
1919
1944
  hide: true,
1920
- minimize: true
1945
+ minimize: true,
1946
+ options: true
1921
1947
  });
1922
1948
  function resolveWidgetLock(definition) {
1923
1949
  const setting = definition.locked;
@@ -1926,7 +1952,8 @@ function resolveWidgetLock(definition) {
1926
1952
  return {
1927
1953
  move: setting.move === true,
1928
1954
  hide: setting.hide === true,
1929
- minimize: setting.minimize === true
1955
+ minimize: setting.minimize === true,
1956
+ options: setting.options === true
1930
1957
  };
1931
1958
  }
1932
1959
  function sortDefinitions(definitions) {
@@ -1947,6 +1974,151 @@ function isWidgetVisibleTo(definition, roles) {
1947
1974
  return definition.roles.some((role) => roles.includes(role));
1948
1975
  }
1949
1976
 
1977
+ // src/core/settings.ts
1978
+ var MAX_WIDGET_TITLE_LENGTH = 80;
1979
+ var MAX_COLUMN_LIST = 100;
1980
+ var MAX_VALUE_LENGTH = 500;
1981
+ function isRecord(value) {
1982
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1983
+ }
1984
+ function parseOptionValue(value) {
1985
+ if (typeof value === "string") {
1986
+ return value.length <= MAX_VALUE_LENGTH ? value : void 0;
1987
+ }
1988
+ if (typeof value === "number")
1989
+ return Number.isFinite(value) ? value : void 0;
1990
+ if (typeof value === "boolean") return value;
1991
+ if (Array.isArray(value) && value.length <= MAX_COLUMN_LIST) {
1992
+ return value.every(
1993
+ (entry) => typeof entry === "string" && entry.length <= MAX_VALUE_LENGTH
1994
+ ) ? value : void 0;
1995
+ }
1996
+ return void 0;
1997
+ }
1998
+ function parseOne(raw) {
1999
+ if (!isRecord(raw)) return void 0;
2000
+ const title = raw["title"];
2001
+ const width = raw["width"];
2002
+ const flexible = raw["flexible"];
2003
+ const options = raw["options"];
2004
+ const cleanTitle = typeof title === "string" ? title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH) : "";
2005
+ const chosen = {};
2006
+ if (isRecord(options)) {
2007
+ for (const [key, value] of Object.entries(options)) {
2008
+ const parsed = parseOptionValue(value);
2009
+ if (parsed !== void 0) chosen[key] = parsed;
2010
+ }
2011
+ }
2012
+ const settings = {
2013
+ ...cleanTitle === "" ? {} : { title: cleanTitle },
2014
+ ...isWidgetWidth(width) ? { width } : {},
2015
+ ...typeof flexible === "boolean" ? { flexible } : {},
2016
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen }
2017
+ };
2018
+ return Object.keys(settings).length === 0 ? void 0 : settings;
2019
+ }
2020
+ function widthRange(item) {
2021
+ const min = isWidgetWidth(item.minWidth) ? item.minWidth : MIN_WIDGET_WIDTH;
2022
+ const max = isWidgetWidth(item.maxWidth) ? item.maxWidth : MAX_WIDGET_WIDTH;
2023
+ return { min, max: Math.max(min, max) };
2024
+ }
2025
+ var WIDTH_SHARES = {
2026
+ 2: "1/6",
2027
+ 3: "1/4",
2028
+ 4: "1/3",
2029
+ 5: "5/12",
2030
+ 6: "1/2",
2031
+ 7: "7/12",
2032
+ 8: "2/3",
2033
+ 9: "3/4",
2034
+ 10: "5/6",
2035
+ 11: "11/12",
2036
+ 12: "Full"
2037
+ };
2038
+ function widthShareLabel(width) {
2039
+ return WIDTH_SHARES[width] ?? `${width}/12`;
2040
+ }
2041
+ function isOptionsLocked(item) {
2042
+ return resolveWidgetLock(item).options;
2043
+ }
2044
+ function fillWith(fill, flexible) {
2045
+ const heightToo = fill === "height" || fill === "both";
2046
+ if (flexible) return heightToo ? "both" : "width";
2047
+ return heightToo ? "height" : void 0;
2048
+ }
2049
+ function isFlexible(item, settings) {
2050
+ return item.fill === "width" || item.fill === "both";
2051
+ }
2052
+ function applySettings(definition, settings) {
2053
+ if (settings === void 0) return definition;
2054
+ const range = widthRange(definition);
2055
+ const width = settings.width === void 0 ? void 0 : Math.min(range.max, Math.max(range.min, settings.width));
2056
+ const title = settings.title === void 0 || settings.title.trim() === "" ? void 0 : settings.title;
2057
+ const flexibleChanged = settings.flexible !== void 0 && settings.flexible !== isFlexible(definition);
2058
+ if (title === void 0 && width === void 0 && !flexibleChanged) {
2059
+ return definition;
2060
+ }
2061
+ const next = { ...definition };
2062
+ if (title !== void 0) next.title = title;
2063
+ if (width !== void 0) next.width = width;
2064
+ if (flexibleChanged) {
2065
+ const fill = fillWith(
2066
+ definition.fill,
2067
+ settings.flexible === true
2068
+ );
2069
+ if (fill === void 0) {
2070
+ delete next.fill;
2071
+ } else {
2072
+ next.fill = fill;
2073
+ }
2074
+ }
2075
+ return next;
2076
+ }
2077
+ function effectiveDefinitions(definitions, layout, options = {}) {
2078
+ return definitions.map(
2079
+ (definition) => options.overrideLocks !== true && isOptionsLocked(definition) ? definition : applySettings(definition, layout.settings?.[definition.key])
2080
+ );
2081
+ }
2082
+ function sameSettings(a, b) {
2083
+ if (a === b) return true;
2084
+ return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && sameOptionValues(a?.options ?? {}, b?.options ?? {});
2085
+ }
2086
+ function setWidgetSettings(layout, key, patch) {
2087
+ const before = layout.settings?.[key];
2088
+ const draft = { ...before };
2089
+ for (const field of ["title", "width", "flexible", "options"]) {
2090
+ if (!(field in patch)) continue;
2091
+ const value = patch[field];
2092
+ if (value === void 0) {
2093
+ delete draft[field];
2094
+ } else {
2095
+ draft[field] = value;
2096
+ }
2097
+ }
2098
+ const merged = parseOne(draft);
2099
+ if (sameSettings(before, merged)) return layout;
2100
+ const rest = { ...layout.settings };
2101
+ if (merged === void 0) {
2102
+ delete rest[key];
2103
+ } else {
2104
+ rest[key] = merged;
2105
+ }
2106
+ const { settings: _previous, ...others } = layout;
2107
+ return Object.keys(rest).length === 0 ? others : { ...others, settings: rest };
2108
+ }
2109
+ function normalizeTitle(title) {
2110
+ return title.normalize("NFKC").trim().toLowerCase();
2111
+ }
2112
+ function isTitleInUse(definitions, layout, key, title) {
2113
+ const wanted = normalizeTitle(title);
2114
+ if (wanted === "") return false;
2115
+ return effectiveDefinitions(definitions, layout, {
2116
+ overrideLocks: true
2117
+ }).some(
2118
+ (other) => other.key !== key && normalizeTitle(other.title ?? other.key) === wanted
2119
+ );
2120
+ }
2121
+
1950
2122
  // src/core/layout.ts
1951
2123
  var MAX_PAGE_TITLE_LENGTH = 30;
1952
2124
  var MAX_PARSED_PAGES = 50;
@@ -2424,6 +2596,57 @@ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {
2424
2596
  }
2425
2597
  return ok(moved);
2426
2598
  }
2599
+ function InlineConfirm({
2600
+ message,
2601
+ onAnswer
2602
+ }) {
2603
+ const slot = useSlotClassName();
2604
+ const { labels } = useWidgetSettings();
2605
+ const noRef = react.useRef(null);
2606
+ react.useEffect(() => {
2607
+ noRef.current?.focus();
2608
+ }, []);
2609
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2610
+ "span",
2611
+ {
2612
+ role: "group",
2613
+ "aria-label": message,
2614
+ className: "dwt-confirm",
2615
+ onKeyDown: (event) => {
2616
+ if (event.key === "Escape") {
2617
+ event.stopPropagation();
2618
+ onAnswer(false);
2619
+ }
2620
+ },
2621
+ children: [
2622
+ /* @__PURE__ */ jsxRuntime.jsx("span", { role: "alert", className: "dwt-confirm-message", children: message }),
2623
+ /* @__PURE__ */ jsxRuntime.jsx(
2624
+ "button",
2625
+ {
2626
+ type: "button",
2627
+ className: slot("button", "dwt-button", "dwt-icon-button"),
2628
+ "aria-label": labels.confirmYes,
2629
+ title: labels.confirmYes,
2630
+ onClick: () => onAnswer(true),
2631
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2713" })
2632
+ }
2633
+ ),
2634
+ /* @__PURE__ */ jsxRuntime.jsx(
2635
+ "button",
2636
+ {
2637
+ ref: noRef,
2638
+ type: "button",
2639
+ className: slot("button", "dwt-button", "dwt-icon-button"),
2640
+ "aria-label": labels.confirmNo,
2641
+ title: labels.confirmNo,
2642
+ onClick: () => onAnswer(false),
2643
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" })
2644
+ }
2645
+ )
2646
+ ]
2647
+ }
2648
+ );
2649
+ }
2427
2650
  var EditMenuInitialOpenContext = react.createContext(false);
2428
2651
  var ITEM_SELECTOR = '[role="menuitem"]:not(:disabled)';
2429
2652
  function WidgetEditMenu({
@@ -2562,6 +2785,561 @@ function WidgetEditMenu({
2562
2785
  }
2563
2786
  );
2564
2787
  }
2788
+ function draftFrom(settings) {
2789
+ return {
2790
+ title: settings?.title ?? "",
2791
+ width: settings?.width,
2792
+ flexible: settings?.flexible,
2793
+ options: settings?.options ?? {}
2794
+ };
2795
+ }
2796
+ function storedDefault(option) {
2797
+ return option.type === "dateRange" ? option.default : defaultOptionValue(option);
2798
+ }
2799
+ function sameValue2(a, b) {
2800
+ if (Array.isArray(a) && Array.isArray(b)) {
2801
+ return a.length === b.length && a.every((item, i) => item === b[i]);
2802
+ }
2803
+ return a === b;
2804
+ }
2805
+ function patchFrom(definition, draft) {
2806
+ const title = draft.title.trim();
2807
+ const options = {};
2808
+ for (const option of definition.options ?? []) {
2809
+ const value = draft.options[option.key];
2810
+ if (value === void 0) continue;
2811
+ if (sameValue2(value, storedDefault(option))) continue;
2812
+ options[option.key] = value;
2813
+ }
2814
+ return {
2815
+ title: title === "" ? void 0 : title,
2816
+ width: draft.width,
2817
+ flexible: draft.flexible === void 0 || draft.flexible === isFlexible(definition) ? void 0 : draft.flexible,
2818
+ options: Object.keys(options).length === 0 ? void 0 : options
2819
+ };
2820
+ }
2821
+ function patchKey(patch) {
2822
+ const options = {};
2823
+ for (const key of Object.keys(patch.options ?? {}).sort()) {
2824
+ options[key] = patch.options[key];
2825
+ }
2826
+ return JSON.stringify([patch.title, patch.width, patch.flexible, options]);
2827
+ }
2828
+ var EMPTY_PATCH = {
2829
+ title: void 0,
2830
+ width: void 0,
2831
+ flexible: void 0,
2832
+ options: void 0
2833
+ };
2834
+ function WidgetOptionsDialog({
2835
+ definition,
2836
+ settings,
2837
+ baseline,
2838
+ isTitleInUse: isTitleInUse2,
2839
+ onApply,
2840
+ onClose
2841
+ }) {
2842
+ const slot = useSlotClassName();
2843
+ const { labels } = useWidgetSettings();
2844
+ const dialogRef = react.useRef(null);
2845
+ const headingId = react.useId();
2846
+ const baseId = react.useId();
2847
+ const [draft, setDraft] = react.useState(() => draftFrom(settings));
2848
+ const [confirming, setConfirming] = react.useState(
2849
+ void 0
2850
+ );
2851
+ react.useEffect(() => {
2852
+ const dialog = dialogRef.current;
2853
+ if (dialog === null) return void 0;
2854
+ const opener = document.activeElement;
2855
+ if (!dialog.open) dialog.showModal();
2856
+ return () => {
2857
+ if (dialog.open) dialog.close();
2858
+ if (opener instanceof HTMLElement) opener.focus();
2859
+ };
2860
+ }, []);
2861
+ const savedKey = patchKey(patchFrom(definition, draftFrom(settings)));
2862
+ const draftPatch = patchFrom(definition, draft);
2863
+ const dirty = patchKey(draftPatch) !== savedKey;
2864
+ const titleTaken = draft.title.trim() !== "" && isTitleInUse2?.(draft.title.trim()) === true;
2865
+ const canApply = dirty && !titleTaken;
2866
+ const baselineKey = baseline === void 0 ? void 0 : patchKey(patchFrom(definition, draftFrom(baseline.settings)));
2867
+ const canRevert = baselineKey !== void 0 && baselineKey !== savedKey;
2868
+ const canReset = savedKey !== patchKey(EMPTY_PATCH);
2869
+ const range = widthRange(definition);
2870
+ const effectiveFlexible = draft.flexible ?? isFlexible(definition);
2871
+ function requestClose() {
2872
+ if (confirming !== void 0) {
2873
+ setConfirming(void 0);
2874
+ } else if (dirty) {
2875
+ setConfirming("discard");
2876
+ } else {
2877
+ onClose();
2878
+ }
2879
+ }
2880
+ function answer(yes) {
2881
+ const which = confirming;
2882
+ setConfirming(void 0);
2883
+ if (!yes || which === void 0) return;
2884
+ if (which === "discard") {
2885
+ onClose();
2886
+ } else if (which === "apply") {
2887
+ onApply(draftPatch);
2888
+ onClose();
2889
+ } else if (which === "reset") {
2890
+ onApply(EMPTY_PATCH);
2891
+ onClose();
2892
+ } else if (baseline !== void 0) {
2893
+ onApply(patchFrom(definition, draftFrom(baseline.settings)));
2894
+ onClose();
2895
+ }
2896
+ }
2897
+ function setOption(key, value) {
2898
+ setDraft((current) => ({
2899
+ ...current,
2900
+ options: { ...current.options, [key]: value }
2901
+ }));
2902
+ }
2903
+ const confirmMessage = {
2904
+ apply: labels.confirmApply,
2905
+ discard: labels.confirmDiscard,
2906
+ reset: labels.confirmResetOptions,
2907
+ revert: labels.confirmRevertOptions
2908
+ };
2909
+ const widths = [];
2910
+ for (let width = range.min; width <= range.max; width += 1)
2911
+ widths.push(width);
2912
+ return /* @__PURE__ */ jsxRuntime.jsx(
2913
+ "dialog",
2914
+ {
2915
+ ref: dialogRef,
2916
+ className: slot("dialog", "dwt-options-dialog"),
2917
+ "aria-labelledby": headingId,
2918
+ onCancel: (event) => {
2919
+ event.preventDefault();
2920
+ requestClose();
2921
+ },
2922
+ onClick: (event) => {
2923
+ if (event.target === dialogRef.current) requestClose();
2924
+ },
2925
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2926
+ "form",
2927
+ {
2928
+ className: "dwt-options",
2929
+ onSubmit: (event) => {
2930
+ event.preventDefault();
2931
+ if (canApply && confirming === void 0) setConfirming("apply");
2932
+ },
2933
+ children: [
2934
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { id: headingId, className: "dwt-options-heading", children: labels.optionsFor(definition.title) }),
2935
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-options-field", children: [
2936
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `${baseId}-title`, children: labels.optionsTitleField }),
2937
+ /* @__PURE__ */ jsxRuntime.jsx(
2938
+ "input",
2939
+ {
2940
+ id: `${baseId}-title`,
2941
+ type: "text",
2942
+ value: draft.title,
2943
+ maxLength: MAX_WIDGET_TITLE_LENGTH,
2944
+ placeholder: definition.title,
2945
+ "aria-invalid": titleTaken,
2946
+ ...titleTaken ? { "aria-describedby": `${baseId}-title-error` } : {},
2947
+ onChange: (event) => setDraft((current) => ({ ...current, title: event.target.value }))
2948
+ }
2949
+ ),
2950
+ titleTaken && /* @__PURE__ */ jsxRuntime.jsx(
2951
+ "p",
2952
+ {
2953
+ id: `${baseId}-title-error`,
2954
+ role: "alert",
2955
+ className: "dwt-options-error",
2956
+ children: labels.titleInUse
2957
+ }
2958
+ )
2959
+ ] }),
2960
+ range.min < range.max && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-options-field", children: [
2961
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `${baseId}-width`, children: labels.optionsWidthField }),
2962
+ /* @__PURE__ */ jsxRuntime.jsxs(
2963
+ "select",
2964
+ {
2965
+ id: `${baseId}-width`,
2966
+ value: draft.width === void 0 ? "" : String(draft.width),
2967
+ onChange: (event) => {
2968
+ const value = event.target.value;
2969
+ setDraft((current) => ({
2970
+ ...current,
2971
+ width: value === "" ? void 0 : Number(value)
2972
+ }));
2973
+ },
2974
+ children: [
2975
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: labels.widthDefault }),
2976
+ widths.map((width) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: String(width), children: labels.widthOption(width, widthShareLabel(width)) }, width))
2977
+ ]
2978
+ }
2979
+ )
2980
+ ] }),
2981
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "dwt-options-check", children: [
2982
+ /* @__PURE__ */ jsxRuntime.jsx(
2983
+ "input",
2984
+ {
2985
+ type: "checkbox",
2986
+ checked: effectiveFlexible,
2987
+ onChange: (event) => setDraft((current) => ({
2988
+ ...current,
2989
+ flexible: event.target.checked
2990
+ }))
2991
+ }
2992
+ ),
2993
+ labels.optionsFlexible
2994
+ ] }),
2995
+ (definition.options?.length ?? 0) > 0 && /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { className: "dwt-options-group", children: [
2996
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: labels.optionsDeclared }),
2997
+ (definition.options ?? []).map((option) => /* @__PURE__ */ jsxRuntime.jsx(
2998
+ OptionField,
2999
+ {
3000
+ option,
3001
+ value: draft.options[option.key],
3002
+ id: `${baseId}-opt-${option.key}`,
3003
+ onChange: (value) => setOption(option.key, value)
3004
+ },
3005
+ option.key
3006
+ ))
3007
+ ] }),
3008
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "dwt-options-actions", children: confirming === void 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3009
+ /* @__PURE__ */ jsxRuntime.jsx(
3010
+ "button",
3011
+ {
3012
+ type: "submit",
3013
+ className: slot("button", "dwt-button", "dwt-button-primary"),
3014
+ disabled: !canApply,
3015
+ children: labels.apply
3016
+ }
3017
+ ),
3018
+ /* @__PURE__ */ jsxRuntime.jsx(
3019
+ "button",
3020
+ {
3021
+ type: "button",
3022
+ className: slot("button", "dwt-button"),
3023
+ onClick: requestClose,
3024
+ children: labels.cancel
3025
+ }
3026
+ ),
3027
+ /* @__PURE__ */ jsxRuntime.jsx(
3028
+ "button",
3029
+ {
3030
+ type: "button",
3031
+ className: slot("button", "dwt-button"),
3032
+ disabled: !canReset,
3033
+ onClick: () => setConfirming("reset"),
3034
+ children: labels.resetToDefaults
3035
+ }
3036
+ ),
3037
+ baseline !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
3038
+ "button",
3039
+ {
3040
+ type: "button",
3041
+ className: slot("button", "dwt-button"),
3042
+ disabled: !canRevert,
3043
+ onClick: () => setConfirming("revert"),
3044
+ children: labels.revertToBeforeEditing
3045
+ }
3046
+ )
3047
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
3048
+ InlineConfirm,
3049
+ {
3050
+ message: confirmMessage[confirming],
3051
+ onAnswer: answer
3052
+ }
3053
+ ) })
3054
+ ]
3055
+ }
3056
+ )
3057
+ }
3058
+ );
3059
+ }
3060
+ function OptionField({
3061
+ option,
3062
+ value,
3063
+ id,
3064
+ onChange
3065
+ }) {
3066
+ const current = value ?? storedDefault(option);
3067
+ switch (option.type) {
3068
+ case "choice":
3069
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-options-field", children: [
3070
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, children: option.label }),
3071
+ /* @__PURE__ */ jsxRuntime.jsx(
3072
+ "select",
3073
+ {
3074
+ id,
3075
+ value: String(current ?? option.default),
3076
+ onChange: (event) => onChange(event.target.value),
3077
+ children: option.choices.map((choice) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: choice.value, children: choice.label }, choice.value))
3078
+ }
3079
+ )
3080
+ ] });
3081
+ case "number":
3082
+ return /* @__PURE__ */ jsxRuntime.jsx(
3083
+ NumberField,
3084
+ {
3085
+ option,
3086
+ id,
3087
+ value: typeof current === "number" ? current : option.default,
3088
+ onChange
3089
+ }
3090
+ );
3091
+ case "boolean":
3092
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "dwt-options-check", children: [
3093
+ /* @__PURE__ */ jsxRuntime.jsx(
3094
+ "input",
3095
+ {
3096
+ type: "checkbox",
3097
+ checked: current === true,
3098
+ onChange: (event) => onChange(event.target.checked)
3099
+ }
3100
+ ),
3101
+ option.label
3102
+ ] });
3103
+ case "text":
3104
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-options-field", children: [
3105
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, children: option.label }),
3106
+ /* @__PURE__ */ jsxRuntime.jsx(
3107
+ "input",
3108
+ {
3109
+ id,
3110
+ type: "text",
3111
+ value: typeof current === "string" ? current : "",
3112
+ maxLength: option.maxLength,
3113
+ onChange: (event) => onChange(event.target.value)
3114
+ }
3115
+ )
3116
+ ] });
3117
+ case "color":
3118
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-options-field", children: [
3119
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, children: option.label }),
3120
+ /* @__PURE__ */ jsxRuntime.jsx(
3121
+ "input",
3122
+ {
3123
+ id,
3124
+ type: "color",
3125
+ value: typeof current === "string" ? current : option.default,
3126
+ onChange: (event) => onChange(event.target.value)
3127
+ }
3128
+ )
3129
+ ] });
3130
+ case "dateRange":
3131
+ return /* @__PURE__ */ jsxRuntime.jsx(
3132
+ DateRangeField,
3133
+ {
3134
+ option,
3135
+ id,
3136
+ value: typeof current === "string" ? current : option.default,
3137
+ onChange
3138
+ }
3139
+ );
3140
+ case "sort":
3141
+ return /* @__PURE__ */ jsxRuntime.jsx(
3142
+ SortField,
3143
+ {
3144
+ option,
3145
+ id,
3146
+ value: typeof current === "string" ? current : void 0,
3147
+ onChange
3148
+ }
3149
+ );
3150
+ case "columns":
3151
+ return /* @__PURE__ */ jsxRuntime.jsx(
3152
+ ColumnsField,
3153
+ {
3154
+ option,
3155
+ value: Array.isArray(current) ? current : option.default,
3156
+ onChange
3157
+ }
3158
+ );
3159
+ }
3160
+ }
3161
+ function NumberField({
3162
+ option,
3163
+ id,
3164
+ value,
3165
+ onChange
3166
+ }) {
3167
+ const [text, setText] = react.useState(String(value));
3168
+ react.useEffect(() => {
3169
+ setText((typed) => Number(typed) === value ? typed : String(value));
3170
+ }, [value]);
3171
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-options-field", children: [
3172
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, children: option.label }),
3173
+ /* @__PURE__ */ jsxRuntime.jsx(
3174
+ "input",
3175
+ {
3176
+ id,
3177
+ type: "number",
3178
+ value: text,
3179
+ min: option.min,
3180
+ max: option.max,
3181
+ step: option.step ?? 1,
3182
+ onChange: (event) => {
3183
+ setText(event.target.value);
3184
+ const parsed = Number(event.target.value);
3185
+ if (event.target.value.trim() !== "" && coerceOptionValue(option, parsed) !== void 0) {
3186
+ onChange(parsed);
3187
+ }
3188
+ }
3189
+ }
3190
+ )
3191
+ ] });
3192
+ }
3193
+ var PRESET_LABELS = {
3194
+ today: "Today",
3195
+ yesterday: "Yesterday",
3196
+ last7: "Last 7 days",
3197
+ last30: "Last 30 days",
3198
+ last90: "Last 90 days",
3199
+ thisMonth: "This month",
3200
+ lastMonth: "Last month",
3201
+ thisYear: "This year",
3202
+ lastYear: "Last year"
3203
+ };
3204
+ function DateRangeField({
3205
+ option,
3206
+ id,
3207
+ value,
3208
+ onChange
3209
+ }) {
3210
+ const { labels } = useWidgetSettings();
3211
+ const presets = option.presets ?? DATE_RANGE_PRESETS.map((preset) => ({
3212
+ value: preset,
3213
+ label: PRESET_LABELS[preset]
3214
+ }));
3215
+ const known = presets.some((preset) => preset.value === value);
3216
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-options-field", children: [
3217
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, children: option.label }),
3218
+ /* @__PURE__ */ jsxRuntime.jsxs(
3219
+ "select",
3220
+ {
3221
+ id,
3222
+ value,
3223
+ onChange: (event) => onChange(event.target.value),
3224
+ children: [
3225
+ !known && /* @__PURE__ */ jsxRuntime.jsx("option", { value, children: labels.customRange(value) }),
3226
+ presets.map((preset) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: preset.value, children: preset.label }, preset.value))
3227
+ ]
3228
+ }
3229
+ )
3230
+ ] });
3231
+ }
3232
+ function SortField({
3233
+ option,
3234
+ id,
3235
+ value,
3236
+ onChange
3237
+ }) {
3238
+ const { labels } = useWidgetSettings();
3239
+ const parsed = value === void 0 ? void 0 : parseSortValue(value);
3240
+ const column = parsed?.column ?? "";
3241
+ const direction = parsed?.direction ?? "desc";
3242
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-options-field", children: [
3243
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, children: option.label }),
3244
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "dwt-options-pair", children: [
3245
+ /* @__PURE__ */ jsxRuntime.jsxs(
3246
+ "select",
3247
+ {
3248
+ id,
3249
+ value: column,
3250
+ onChange: (event) => onChange(`${event.target.value}:${direction}`),
3251
+ children: [
3252
+ option.default === void 0 && column === "" && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: labels.sortDefault }),
3253
+ option.columns.map((entry) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: entry.key, children: entry.label }, entry.key))
3254
+ ]
3255
+ }
3256
+ ),
3257
+ /* @__PURE__ */ jsxRuntime.jsxs(
3258
+ "select",
3259
+ {
3260
+ "aria-label": option.label,
3261
+ value: direction,
3262
+ disabled: column === "",
3263
+ onChange: (event) => onChange(`${column}:${event.target.value}`),
3264
+ children: [
3265
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "asc", children: labels.sortAscending }),
3266
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "desc", children: labels.sortDescending })
3267
+ ]
3268
+ }
3269
+ )
3270
+ ] })
3271
+ ] });
3272
+ }
3273
+ function ColumnsField({
3274
+ option,
3275
+ value,
3276
+ onChange
3277
+ }) {
3278
+ const { labels } = useWidgetSettings();
3279
+ const slot = useSlotClassName();
3280
+ const ordered = [
3281
+ ...value,
3282
+ ...option.columns.map((c) => c.key).filter((key) => !value.includes(key))
3283
+ ];
3284
+ const labelOf = (key) => option.columns.find((c) => c.key === key)?.label ?? key;
3285
+ function move(key, by) {
3286
+ const next = [...value];
3287
+ const from = next.indexOf(key);
3288
+ const to = from + by;
3289
+ if (from === -1 || to < 0 || to >= next.length) return;
3290
+ [next[from], next[to]] = [next[to], next[from]];
3291
+ onChange(next);
3292
+ }
3293
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { className: "dwt-options-columns", children: [
3294
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: option.label }),
3295
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { children: ordered.map((key) => {
3296
+ const shown = value.includes(key);
3297
+ const name = labelOf(key);
3298
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
3299
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
3300
+ /* @__PURE__ */ jsxRuntime.jsx(
3301
+ "input",
3302
+ {
3303
+ type: "checkbox",
3304
+ checked: shown,
3305
+ disabled: shown && value.length === 1,
3306
+ onChange: (event) => onChange(
3307
+ event.target.checked ? [...value, key] : value.filter((k) => k !== key)
3308
+ )
3309
+ }
3310
+ ),
3311
+ name
3312
+ ] }),
3313
+ shown && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "dwt-options-move", children: [
3314
+ /* @__PURE__ */ jsxRuntime.jsx(
3315
+ "button",
3316
+ {
3317
+ type: "button",
3318
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3319
+ "aria-label": labels.moveColumnUp(name),
3320
+ title: labels.moveColumnUp(name),
3321
+ disabled: value.indexOf(key) === 0,
3322
+ onClick: () => move(key, -1),
3323
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2191" })
3324
+ }
3325
+ ),
3326
+ /* @__PURE__ */ jsxRuntime.jsx(
3327
+ "button",
3328
+ {
3329
+ type: "button",
3330
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3331
+ "aria-label": labels.moveColumnDown(name),
3332
+ title: labels.moveColumnDown(name),
3333
+ disabled: value.indexOf(key) === value.length - 1,
3334
+ onClick: () => move(key, 1),
3335
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2193" })
3336
+ }
3337
+ )
3338
+ ] })
3339
+ ] }, key);
3340
+ }) })
3341
+ ] });
3342
+ }
2565
3343
  function nextSort(current, column) {
2566
3344
  if (current?.column !== column) return { column, direction: "asc" };
2567
3345
  return current.direction === "asc" ? { column, direction: "desc" } : void 0;
@@ -2989,57 +3767,6 @@ function DetailHost({
2989
3767
  }
2990
3768
  ) });
2991
3769
  }
2992
- function InlineConfirm({
2993
- message,
2994
- onAnswer
2995
- }) {
2996
- const slot = useSlotClassName();
2997
- const { labels } = useWidgetSettings();
2998
- const noRef = react.useRef(null);
2999
- react.useEffect(() => {
3000
- noRef.current?.focus();
3001
- }, []);
3002
- return /* @__PURE__ */ jsxRuntime.jsxs(
3003
- "span",
3004
- {
3005
- role: "group",
3006
- "aria-label": message,
3007
- className: "dwt-confirm",
3008
- onKeyDown: (event) => {
3009
- if (event.key === "Escape") {
3010
- event.stopPropagation();
3011
- onAnswer(false);
3012
- }
3013
- },
3014
- children: [
3015
- /* @__PURE__ */ jsxRuntime.jsx("span", { role: "alert", className: "dwt-confirm-message", children: message }),
3016
- /* @__PURE__ */ jsxRuntime.jsx(
3017
- "button",
3018
- {
3019
- type: "button",
3020
- className: slot("button", "dwt-button", "dwt-icon-button"),
3021
- "aria-label": labels.confirmYes,
3022
- title: labels.confirmYes,
3023
- onClick: () => onAnswer(true),
3024
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2713" })
3025
- }
3026
- ),
3027
- /* @__PURE__ */ jsxRuntime.jsx(
3028
- "button",
3029
- {
3030
- ref: noRef,
3031
- type: "button",
3032
- className: slot("button", "dwt-button", "dwt-icon-button"),
3033
- "aria-label": labels.confirmNo,
3034
- title: labels.confirmNo,
3035
- onClick: () => onAnswer(false),
3036
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" })
3037
- }
3038
- )
3039
- ]
3040
- }
3041
- );
3042
- }
3043
3770
  function PageTabs({
3044
3771
  pages,
3045
3772
  activeKey,
@@ -3245,12 +3972,19 @@ function DashboardInner({
3245
3972
  }) {
3246
3973
  const slot = useSlotClassName();
3247
3974
  const { labels } = useWidgetSettings();
3248
- const definitions = widgets.map(
3975
+ const effectiveWidgets = widgets.map((w) => {
3976
+ const settings = savedLayout.settings?.[w.definition.key];
3977
+ return settings === void 0 || !overrideLocks && isOptionsLocked(w.definition) ? w : { ...w, definition: applySettings(w.definition, settings) };
3978
+ });
3979
+ const definitions = effectiveWidgets.map(
3249
3980
  (w) => overrideLocks ? withoutLock(w.definition) : w.definition
3250
3981
  );
3251
3982
  const layout = enforceLocks(definitions, savedLayout);
3252
3983
  const byKey = new Map(
3253
- widgets.map((w) => [w.definition.key, w])
3984
+ effectiveWidgets.map((w) => [w.definition.key, w])
3985
+ );
3986
+ const authorDefinitions = new Map(
3987
+ widgets.map((w) => [w.definition.key, w.definition])
3254
3988
  );
3255
3989
  const pages = pageList(layout);
3256
3990
  const paged = pages.length >= 2;
@@ -3295,6 +4029,7 @@ function DashboardInner({
3295
4029
  const minimized = editable ? visible.filter((definition) => isMinimized(view, definition.key)) : [];
3296
4030
  const shown = editable ? visible.filter((definition) => !isMinimized(view, definition.key)) : visible;
3297
4031
  const [detailKey, setDetailKey] = react.useState(void 0);
4032
+ const [optionsKey, setOptionsKey] = react.useState(void 0);
3298
4033
  const gridRef = react.useRef(null);
3299
4034
  const twelve = usesWidthColumns(shown);
3300
4035
  const spans = useFillSpans(
@@ -3434,7 +4169,14 @@ function DashboardInner({
3434
4169
  const which = pending;
3435
4170
  setConfirming(void 0);
3436
4171
  if (yes && which === "reset") {
3437
- change(enforceLocks(definitions, defaultLayout ?? EMPTY_LAYOUT));
4172
+ const arranged = enforceLocks(
4173
+ definitions,
4174
+ defaultLayout ?? EMPTY_LAYOUT
4175
+ );
4176
+ const { settings: _defaultSettings, ...arrangement } = arranged;
4177
+ change(
4178
+ layout.settings === void 0 ? arrangement : { ...arrangement, settings: layout.settings }
4179
+ );
3438
4180
  } else if (yes && which === "revert" && snapshot !== void 0) {
3439
4181
  change(snapshot);
3440
4182
  }
@@ -3731,6 +4473,15 @@ function DashboardInner({
3731
4473
  onSelect: () => changePage(moveWidgetBy(pageDefs, view, key, 1))
3732
4474
  }
3733
4475
  ],
4476
+ ...lock.options ? [] : [
4477
+ {
4478
+ kind: "action",
4479
+ key: "options",
4480
+ label: labels.menuOptions,
4481
+ ariaLabel: labels.optionsFor(title),
4482
+ onSelect: () => setOptionsKey(key)
4483
+ }
4484
+ ],
3734
4485
  ...pageTargets,
3735
4486
  ...lock.hide ? [] : [
3736
4487
  {
@@ -3819,6 +4570,30 @@ function DashboardInner({
3819
4570
  ]
3820
4571
  }
3821
4572
  ),
4573
+ optionsKey !== void 0 && authorDefinitions.get(optionsKey) !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
4574
+ WidgetOptionsDialog,
4575
+ {
4576
+ definition: authorDefinitions.get(optionsKey),
4577
+ settings: layout.settings?.[optionsKey],
4578
+ ...snapshot === void 0 ? {} : { baseline: { settings: snapshot.settings?.[optionsKey] } },
4579
+ isTitleInUse: (title) => isTitleInUse(
4580
+ widgets.map((w) => w.definition),
4581
+ layout,
4582
+ optionsKey,
4583
+ title
4584
+ ),
4585
+ onApply: (patch) => {
4586
+ change(setWidgetSettings(layout, optionsKey, patch));
4587
+ setAnnouncement(
4588
+ labels.optionsSaved(
4589
+ authorDefinitions.get(optionsKey)?.title ?? optionsKey
4590
+ )
4591
+ );
4592
+ },
4593
+ onClose: () => setOptionsKey(void 0)
4594
+ },
4595
+ optionsKey
4596
+ ),
3822
4597
  detailKey !== void 0 && byKey.get(detailKey) !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
3823
4598
  DetailHost,
3824
4599
  {
@@ -4093,7 +4868,7 @@ function describeType2(value) {
4093
4868
  }
4094
4869
  return typeof value;
4095
4870
  }
4096
- function isRecord(value) {
4871
+ function isRecord2(value) {
4097
4872
  return typeof value === "object" && value !== null && !Array.isArray(value);
4098
4873
  }
4099
4874
  function join(path, field) {
@@ -4175,7 +4950,7 @@ function requireOneOf(record, field, options, path, problems) {
4175
4950
  function eachRecord(items, path, problems, check) {
4176
4951
  items.forEach((item, index) => {
4177
4952
  const itemPath = `${path}[${index}]`;
4178
- if (isRecord(item)) {
4953
+ if (isRecord2(item)) {
4179
4954
  check(item, itemPath);
4180
4955
  } else {
4181
4956
  problems.push(
@@ -4329,7 +5104,7 @@ function formatProblems(prefix, problems) {
4329
5104
  }
4330
5105
  function validateWidgetData(value, options = {}) {
4331
5106
  const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
4332
- if (!isRecord(value)) {
5107
+ if (!isRecord2(value)) {
4333
5108
  return err(`${subject}: must be an object, got ${describeType2(value)}.`);
4334
5109
  }
4335
5110
  if (value["empty"] === true) {
@@ -4792,6 +5567,7 @@ exports.WidgetDetail = WidgetDetail;
4792
5567
  exports.WidgetDetailDialog = WidgetDetailDialog;
4793
5568
  exports.WidgetGrid = WidgetGrid;
4794
5569
  exports.WidgetLink = WidgetLink;
5570
+ exports.WidgetOptionsDialog = WidgetOptionsDialog;
4795
5571
  exports.WidgetSettingsProvider = WidgetSettingsProvider;
4796
5572
  exports.barPath = barPath;
4797
5573
  exports.buildChartModel = buildChartModel;