@richardmcquiston01/dashboard-widgets-toolkit 0.13.0 → 0.15.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
@@ -827,6 +827,32 @@ var DEFAULT_LABELS = {
827
827
  menuMoveEarlier: "Move earlier",
828
828
  menuMoveLater: "Move later",
829
829
  menuHide: "Hide",
830
+ menuOptions: "Options\u2026",
831
+ optionsFor: (title) => `Options for ${title}`,
832
+ optionsTitleField: "Title",
833
+ titleInUse: "Another widget already uses this title.",
834
+ optionsWidthField: "Width",
835
+ widthDefault: "Default",
836
+ widthOption: (width, share) => `${width} of 12 (${share})`,
837
+ optionsFlexible: "Grow to fill empty space in the row",
838
+ optionsViewField: "View",
839
+ viewDefault: (viewLabel) => `${viewLabel} (default)`,
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}`,
830
856
  moveToPage: (title) => `Move ${title} to another page`,
831
857
  moveToPagePlaceholder: "Move to page\u2026",
832
858
  moveToPageOption: (title, rowsFree) => `${title} (${rowsFree} ${rowsFree === 1 ? "row" : "rows"} free)`,
@@ -1910,12 +1936,14 @@ function ResolvedWidgetCard({
1910
1936
  var NOT_LOCKED = Object.freeze({
1911
1937
  move: false,
1912
1938
  hide: false,
1913
- minimize: false
1939
+ minimize: false,
1940
+ options: false
1914
1941
  });
1915
1942
  var FULLY_LOCKED = Object.freeze({
1916
1943
  move: true,
1917
1944
  hide: true,
1918
- minimize: true
1945
+ minimize: true,
1946
+ options: true
1919
1947
  });
1920
1948
  function resolveWidgetLock(definition) {
1921
1949
  const setting = definition.locked;
@@ -1924,7 +1952,8 @@ function resolveWidgetLock(definition) {
1924
1952
  return {
1925
1953
  move: setting.move === true,
1926
1954
  hide: setting.hide === true,
1927
- minimize: setting.minimize === true
1955
+ minimize: setting.minimize === true,
1956
+ options: setting.options === true
1928
1957
  };
1929
1958
  }
1930
1959
  function sortDefinitions(definitions) {
@@ -1945,6 +1974,435 @@ function isWidgetVisibleTo(definition, roles) {
1945
1974
  return definition.roles.some((role) => roles.includes(role));
1946
1975
  }
1947
1976
 
1977
+ // src/core/payload.ts
1978
+ var WIDGET_KINDS = [
1979
+ "TEXT",
1980
+ "KPI",
1981
+ "GAUGE",
1982
+ "TABLE",
1983
+ "BAR_LIST",
1984
+ "ALERT_LIST",
1985
+ "GRAPH"
1986
+ ];
1987
+ function isWidgetKind(value) {
1988
+ return typeof value === "string" && WIDGET_KINDS.includes(value);
1989
+ }
1990
+ var VALUE_FORMATS = ["number", "currency", "percent"];
1991
+ var MAX_GRAPH_SERIES = 8;
1992
+ var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
1993
+ function isWidgetEmptyState(payload) {
1994
+ return "empty" in payload && payload.empty === true;
1995
+ }
1996
+ function emptyTextFor(payload) {
1997
+ if (isWidgetEmptyState(payload)) {
1998
+ return payload.text;
1999
+ }
2000
+ switch (payload.kind) {
2001
+ case "TABLE":
2002
+ return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
2003
+ case "BAR_LIST":
2004
+ return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
2005
+ case "ALERT_LIST":
2006
+ return payload.items.length === 0 ? payload.emptyText : null;
2007
+ case "GRAPH":
2008
+ return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
2009
+ case "TEXT":
2010
+ case "KPI":
2011
+ case "GAUGE":
2012
+ return null;
2013
+ }
2014
+ }
2015
+
2016
+ // src/core/views.ts
2017
+ var BASE_VIEW_KEY = "base";
2018
+ var VIEW_CONVERSIONS = {
2019
+ tableToBarList: { from: "TABLE", to: "BAR_LIST" },
2020
+ tableToGraph: { from: "TABLE", to: "GRAPH" },
2021
+ barListToTable: { from: "BAR_LIST", to: "TABLE" },
2022
+ graphToTable: { from: "GRAPH", to: "TABLE" },
2023
+ barListToGraph: { from: "BAR_LIST", to: "GRAPH" }
2024
+ };
2025
+ var KIND_LABELS = {
2026
+ TEXT: "Text",
2027
+ KPI: "Number",
2028
+ GAUGE: "Gauge",
2029
+ TABLE: "Table",
2030
+ BAR_LIST: "Bars",
2031
+ ALERT_LIST: "List",
2032
+ GRAPH: "Chart"
2033
+ };
2034
+ function kindLabel(kind) {
2035
+ return KIND_LABELS[kind];
2036
+ }
2037
+ function hasViews(item) {
2038
+ return item.views !== void 0 && item.views.length > 0;
2039
+ }
2040
+ function findView(item, key) {
2041
+ return item.views?.find((view) => view.key === key);
2042
+ }
2043
+ function viewChoices(item) {
2044
+ if (!hasViews(item)) return [];
2045
+ return [
2046
+ {
2047
+ key: BASE_VIEW_KEY,
2048
+ label: item.baseViewLabel ?? kindLabel(item.kind),
2049
+ kind: item.kind
2050
+ },
2051
+ ...(item.views ?? []).map(({ key, label, kind }) => ({
2052
+ key,
2053
+ label,
2054
+ kind
2055
+ }))
2056
+ ];
2057
+ }
2058
+ function isKnownView(item, key) {
2059
+ return key !== void 0 && (key === BASE_VIEW_KEY || findView(item, key) !== void 0);
2060
+ }
2061
+ function effectiveViewKey(item, chosen) {
2062
+ if (!hasViews(item)) return BASE_VIEW_KEY;
2063
+ if (isKnownView(item, chosen)) return chosen;
2064
+ if (isKnownView(item, item.defaultView)) return item.defaultView;
2065
+ return BASE_VIEW_KEY;
2066
+ }
2067
+ function cellValue2(cell) {
2068
+ const value = cell?.value;
2069
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2070
+ }
2071
+ function columnName(table, index) {
2072
+ return table.columns[index]?.label ?? `#${index + 1}`;
2073
+ }
2074
+ function checkColumn(table, index, field, context) {
2075
+ if (!Number.isInteger(index) || index < 0 || index >= table.columns.length) {
2076
+ return `${context.where}: ${field} column ${index} does not exist (the table has ${table.columns.length} columns, numbered from 0).`;
2077
+ }
2078
+ return void 0;
2079
+ }
2080
+ function numericColumn(table, index, context) {
2081
+ const numbers = [];
2082
+ for (const [rowIndex, row] of table.rows.entries()) {
2083
+ const value = cellValue2(row[index]);
2084
+ if (value === void 0) {
2085
+ return err(
2086
+ `${context.where}: row ${rowIndex + 1}, column ${index + 1} ("${columnName(table, index)}") has no numeric value. Add value to the cells.`
2087
+ );
2088
+ }
2089
+ numbers.push(value);
2090
+ }
2091
+ return ok(numbers);
2092
+ }
2093
+ function tableToBarList(table, conversion, context) {
2094
+ const bad = checkColumn(table, conversion.label, "label", context) ?? checkColumn(table, conversion.value, "value", context);
2095
+ if (bad !== void 0) return err(bad);
2096
+ const numbers = numericColumn(table, conversion.value, context);
2097
+ if (!numbers.ok) return numbers;
2098
+ const items = table.rows.map((row, index) => ({
2099
+ label: row[conversion.label]?.text ?? "",
2100
+ value: numbers.value[index],
2101
+ display: row[conversion.value]?.text ?? String(numbers.value[index])
2102
+ }));
2103
+ return ok({ kind: "BAR_LIST", items });
2104
+ }
2105
+ function tableToGraph(table, conversion, context) {
2106
+ if (conversion.values.length === 0 || conversion.values.length > MAX_GRAPH_SERIES) {
2107
+ return err(
2108
+ `${context.where}: a chart needs 1 to ${MAX_GRAPH_SERIES} series columns, got ${conversion.values.length}.`
2109
+ );
2110
+ }
2111
+ let bad = checkColumn(
2112
+ table,
2113
+ conversion.label,
2114
+ "label",
2115
+ context
2116
+ );
2117
+ for (const index of conversion.values) {
2118
+ bad ??= checkColumn(table, index, "series", context);
2119
+ }
2120
+ if (bad !== void 0) return err(bad);
2121
+ const series = [];
2122
+ for (const index of conversion.values) {
2123
+ const numbers = numericColumn(table, index, context);
2124
+ if (!numbers.ok) return numbers;
2125
+ series.push({
2126
+ name: columnName(table, index),
2127
+ points: table.rows.map((row, rowIndex) => ({
2128
+ label: row[conversion.label]?.text ?? "",
2129
+ value: numbers.value[rowIndex]
2130
+ }))
2131
+ });
2132
+ }
2133
+ return ok({
2134
+ kind: "GRAPH",
2135
+ series,
2136
+ chartType: conversion.chartType,
2137
+ valueFormat: conversion.valueFormat ?? "number",
2138
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency },
2139
+ xLabel: columnName(table, conversion.label)
2140
+ });
2141
+ }
2142
+ function barListToTable(list, conversion) {
2143
+ return ok({
2144
+ kind: "TABLE",
2145
+ columns: [
2146
+ { label: conversion.labelHeading ?? "Name" },
2147
+ { label: conversion.valueHeading ?? "Value", numeric: true }
2148
+ ],
2149
+ rows: list.items.map((item) => [
2150
+ { text: item.label },
2151
+ { text: item.display ?? String(item.value), value: item.value }
2152
+ ])
2153
+ });
2154
+ }
2155
+ function graphToTable(graph, context) {
2156
+ const first = graph.series[0];
2157
+ if (first === void 0)
2158
+ return err(`${context.where}: the chart has no series.`);
2159
+ const format = {
2160
+ ...graph.currency === void 0 ? {} : { currency: graph.currency },
2161
+ ...context.locale === void 0 ? {} : { locale: context.locale }
2162
+ };
2163
+ return ok({
2164
+ kind: "TABLE",
2165
+ columns: [
2166
+ { label: graph.xLabel ?? "Label" },
2167
+ ...graph.series.map((series) => ({ label: series.name, numeric: true }))
2168
+ ],
2169
+ rows: first.points.map((point, index) => [
2170
+ { text: point.label },
2171
+ ...graph.series.map((series) => {
2172
+ const value = series.points[index]?.value;
2173
+ return value === void 0 ? { text: "\u2014" } : { text: formatValue(value, graph.valueFormat, format), value };
2174
+ })
2175
+ ])
2176
+ });
2177
+ }
2178
+ function barListToGraph(list, conversion) {
2179
+ return ok({
2180
+ kind: "GRAPH",
2181
+ series: [
2182
+ {
2183
+ name: conversion.seriesName ?? "Value",
2184
+ points: list.items.map((item) => ({
2185
+ label: item.label,
2186
+ value: item.value
2187
+ }))
2188
+ }
2189
+ ],
2190
+ chartType: conversion.chartType,
2191
+ valueFormat: conversion.valueFormat ?? "number",
2192
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency }
2193
+ });
2194
+ }
2195
+ function convertView(widgetKey, view, data, options = {}) {
2196
+ const conversion = view.convert;
2197
+ const context = {
2198
+ where: `Widget ${JSON.stringify(widgetKey)} view ${JSON.stringify(view.key)}`,
2199
+ ...options.locale === void 0 ? {} : { locale: options.locale }
2200
+ };
2201
+ const expected = VIEW_CONVERSIONS[conversion.type]?.from;
2202
+ if (expected === void 0) {
2203
+ return err(
2204
+ `${context.where}: unknown conversion type ${JSON.stringify(conversion.type)}.`
2205
+ );
2206
+ }
2207
+ if (data.kind !== expected) {
2208
+ return err(
2209
+ `${context.where}: ${conversion.type} reads a ${expected} payload, but the widget returned ${data.kind}.`
2210
+ );
2211
+ }
2212
+ switch (conversion.type) {
2213
+ case "tableToBarList":
2214
+ return tableToBarList(data, conversion, context);
2215
+ case "tableToGraph":
2216
+ return tableToGraph(data, conversion, context);
2217
+ case "barListToTable":
2218
+ return barListToTable(data, conversion);
2219
+ case "graphToTable":
2220
+ return graphToTable(data, context);
2221
+ case "barListToGraph":
2222
+ return barListToGraph(data, conversion);
2223
+ }
2224
+ }
2225
+ function applyView(widget, chosen, options = {}) {
2226
+ const key = effectiveViewKey(widget.definition, chosen);
2227
+ const view = findView(widget.definition, key);
2228
+ if (view === void 0) return widget;
2229
+ const definition = {
2230
+ ...widget.definition,
2231
+ kind: view.kind
2232
+ };
2233
+ if (widget.status === "loading") return { ...widget, definition };
2234
+ if (widget.status === "empty") return { ...widget, definition };
2235
+ if (widget.status === "error") return { ...widget, definition };
2236
+ const converted = convertView(
2237
+ widget.definition.key,
2238
+ view,
2239
+ widget.data,
2240
+ options
2241
+ );
2242
+ if (!converted.ok) {
2243
+ return {
2244
+ definition,
2245
+ status: "error",
2246
+ error: converted.error,
2247
+ ...widget.options === void 0 ? {} : { options: widget.options }
2248
+ };
2249
+ }
2250
+ return { ...widget, definition, data: converted.value };
2251
+ }
2252
+
2253
+ // src/core/settings.ts
2254
+ var MAX_WIDGET_TITLE_LENGTH = 80;
2255
+ var MAX_COLUMN_LIST = 100;
2256
+ var MAX_VALUE_LENGTH = 500;
2257
+ function isRecord(value) {
2258
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2259
+ }
2260
+ function parseOptionValue(value) {
2261
+ if (typeof value === "string") {
2262
+ return value.length <= MAX_VALUE_LENGTH ? value : void 0;
2263
+ }
2264
+ if (typeof value === "number")
2265
+ return Number.isFinite(value) ? value : void 0;
2266
+ if (typeof value === "boolean") return value;
2267
+ if (Array.isArray(value) && value.length <= MAX_COLUMN_LIST) {
2268
+ return value.every(
2269
+ (entry) => typeof entry === "string" && entry.length <= MAX_VALUE_LENGTH
2270
+ ) ? value : void 0;
2271
+ }
2272
+ return void 0;
2273
+ }
2274
+ function parseOne(raw) {
2275
+ if (!isRecord(raw)) return void 0;
2276
+ const title = raw["title"];
2277
+ const width = raw["width"];
2278
+ const flexible = raw["flexible"];
2279
+ const options = raw["options"];
2280
+ const view = raw["view"];
2281
+ const cleanTitle = typeof title === "string" ? title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH) : "";
2282
+ const chosen = {};
2283
+ if (isRecord(options)) {
2284
+ for (const [key, value] of Object.entries(options)) {
2285
+ const parsed = parseOptionValue(value);
2286
+ if (parsed !== void 0) chosen[key] = parsed;
2287
+ }
2288
+ }
2289
+ const settings = {
2290
+ ...cleanTitle === "" ? {} : { title: cleanTitle },
2291
+ ...isWidgetWidth(width) ? { width } : {},
2292
+ ...typeof flexible === "boolean" ? { flexible } : {},
2293
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
2294
+ ...typeof view === "string" && view !== "" && view.length <= 80 ? { view } : {}
2295
+ };
2296
+ return Object.keys(settings).length === 0 ? void 0 : settings;
2297
+ }
2298
+ function widthRange(item) {
2299
+ const min = isWidgetWidth(item.minWidth) ? item.minWidth : MIN_WIDGET_WIDTH;
2300
+ const max = isWidgetWidth(item.maxWidth) ? item.maxWidth : MAX_WIDGET_WIDTH;
2301
+ return { min, max: Math.max(min, max) };
2302
+ }
2303
+ var WIDTH_SHARES = {
2304
+ 2: "1/6",
2305
+ 3: "1/4",
2306
+ 4: "1/3",
2307
+ 5: "5/12",
2308
+ 6: "1/2",
2309
+ 7: "7/12",
2310
+ 8: "2/3",
2311
+ 9: "3/4",
2312
+ 10: "5/6",
2313
+ 11: "11/12",
2314
+ 12: "Full"
2315
+ };
2316
+ function widthShareLabel(width) {
2317
+ return WIDTH_SHARES[width] ?? `${width}/12`;
2318
+ }
2319
+ function isOptionsLocked(item) {
2320
+ return resolveWidgetLock(item).options;
2321
+ }
2322
+ function fillWith(fill, flexible) {
2323
+ const heightToo = fill === "height" || fill === "both";
2324
+ if (flexible) return heightToo ? "both" : "width";
2325
+ return heightToo ? "height" : void 0;
2326
+ }
2327
+ function isFlexible(item, settings) {
2328
+ return item.fill === "width" || item.fill === "both";
2329
+ }
2330
+ function applySettings(definition, settings) {
2331
+ if (settings === void 0) return definition;
2332
+ const range = widthRange(definition);
2333
+ const width = settings.width === void 0 ? void 0 : Math.min(range.max, Math.max(range.min, settings.width));
2334
+ const title = settings.title === void 0 || settings.title.trim() === "" ? void 0 : settings.title;
2335
+ const flexibleChanged = settings.flexible !== void 0 && settings.flexible !== isFlexible(definition);
2336
+ if (title === void 0 && width === void 0 && !flexibleChanged) {
2337
+ return definition;
2338
+ }
2339
+ const next = { ...definition };
2340
+ if (title !== void 0) next.title = title;
2341
+ if (width !== void 0) next.width = width;
2342
+ if (flexibleChanged) {
2343
+ const fill = fillWith(
2344
+ definition.fill,
2345
+ settings.flexible === true
2346
+ );
2347
+ if (fill === void 0) {
2348
+ delete next.fill;
2349
+ } else {
2350
+ next.fill = fill;
2351
+ }
2352
+ }
2353
+ return next;
2354
+ }
2355
+ function effectiveDefinitions(definitions, layout, options = {}) {
2356
+ return definitions.map(
2357
+ (definition) => options.overrideLocks !== true && isOptionsLocked(definition) ? definition : applySettings(definition, layout.settings?.[definition.key])
2358
+ );
2359
+ }
2360
+ function sameSettings(a, b) {
2361
+ if (a === b) return true;
2362
+ return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && a?.view === b?.view && sameOptionValues(a?.options ?? {}, b?.options ?? {});
2363
+ }
2364
+ function setWidgetSettings(layout, key, patch) {
2365
+ const before = layout.settings?.[key];
2366
+ const draft = { ...before };
2367
+ for (const field of [
2368
+ "title",
2369
+ "width",
2370
+ "flexible",
2371
+ "options",
2372
+ "view"
2373
+ ]) {
2374
+ if (!(field in patch)) continue;
2375
+ const value = patch[field];
2376
+ if (value === void 0) {
2377
+ delete draft[field];
2378
+ } else {
2379
+ draft[field] = value;
2380
+ }
2381
+ }
2382
+ const merged = parseOne(draft);
2383
+ if (sameSettings(before, merged)) return layout;
2384
+ const rest = { ...layout.settings };
2385
+ if (merged === void 0) {
2386
+ delete rest[key];
2387
+ } else {
2388
+ rest[key] = merged;
2389
+ }
2390
+ const { settings: _previous, ...others } = layout;
2391
+ return Object.keys(rest).length === 0 ? others : { ...others, settings: rest };
2392
+ }
2393
+ function normalizeTitle(title) {
2394
+ return title.normalize("NFKC").trim().toLowerCase();
2395
+ }
2396
+ function isTitleInUse(definitions, layout, key, title) {
2397
+ const wanted = normalizeTitle(title);
2398
+ if (wanted === "") return false;
2399
+ return effectiveDefinitions(definitions, layout, {
2400
+ overrideLocks: true
2401
+ }).some(
2402
+ (other) => other.key !== key && normalizeTitle(other.title ?? other.key) === wanted
2403
+ );
2404
+ }
2405
+
1948
2406
  // src/core/layout.ts
1949
2407
  var MAX_PAGE_TITLE_LENGTH = 30;
1950
2408
  var MAX_PARSED_PAGES = 50;
@@ -2422,6 +2880,57 @@ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {
2422
2880
  }
2423
2881
  return ok(moved);
2424
2882
  }
2883
+ function InlineConfirm({
2884
+ message,
2885
+ onAnswer
2886
+ }) {
2887
+ const slot = useSlotClassName();
2888
+ const { labels } = useWidgetSettings();
2889
+ const noRef = useRef(null);
2890
+ useEffect(() => {
2891
+ noRef.current?.focus();
2892
+ }, []);
2893
+ return /* @__PURE__ */ jsxs(
2894
+ "span",
2895
+ {
2896
+ role: "group",
2897
+ "aria-label": message,
2898
+ className: "dwt-confirm",
2899
+ onKeyDown: (event) => {
2900
+ if (event.key === "Escape") {
2901
+ event.stopPropagation();
2902
+ onAnswer(false);
2903
+ }
2904
+ },
2905
+ children: [
2906
+ /* @__PURE__ */ jsx("span", { role: "alert", className: "dwt-confirm-message", children: message }),
2907
+ /* @__PURE__ */ jsx(
2908
+ "button",
2909
+ {
2910
+ type: "button",
2911
+ className: slot("button", "dwt-button", "dwt-icon-button"),
2912
+ "aria-label": labels.confirmYes,
2913
+ title: labels.confirmYes,
2914
+ onClick: () => onAnswer(true),
2915
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2713" })
2916
+ }
2917
+ ),
2918
+ /* @__PURE__ */ jsx(
2919
+ "button",
2920
+ {
2921
+ ref: noRef,
2922
+ type: "button",
2923
+ className: slot("button", "dwt-button", "dwt-icon-button"),
2924
+ "aria-label": labels.confirmNo,
2925
+ title: labels.confirmNo,
2926
+ onClick: () => onAnswer(false),
2927
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2715" })
2928
+ }
2929
+ )
2930
+ ]
2931
+ }
2932
+ );
2933
+ }
2425
2934
  var EditMenuInitialOpenContext = createContext(false);
2426
2935
  var ITEM_SELECTOR = '[role="menuitem"]:not(:disabled)';
2427
2936
  function WidgetEditMenu({
@@ -2532,34 +3041,615 @@ function WidgetEditMenu({
2532
3041
  (item) => item.kind === "heading" ? /* @__PURE__ */ jsx(
2533
3042
  "p",
2534
3043
  {
2535
- className: "dwt-edit-heading",
2536
- role: "presentation",
2537
- children: item.label
2538
- },
2539
- item.key
2540
- ) : /* @__PURE__ */ jsx(
3044
+ className: "dwt-edit-heading",
3045
+ role: "presentation",
3046
+ children: item.label
3047
+ },
3048
+ item.key
3049
+ ) : /* @__PURE__ */ jsx(
3050
+ "button",
3051
+ {
3052
+ type: "button",
3053
+ role: "menuitem",
3054
+ className: "dwt-edit-option",
3055
+ ...item.ariaLabel === void 0 ? {} : { "aria-label": item.ariaLabel },
3056
+ disabled: item.disabled === true,
3057
+ onClick: () => {
3058
+ close(false);
3059
+ item.onSelect();
3060
+ },
3061
+ children: item.label
3062
+ },
3063
+ item.key
3064
+ )
3065
+ )
3066
+ }
3067
+ )
3068
+ ]
3069
+ }
3070
+ );
3071
+ }
3072
+ function draftFrom(settings) {
3073
+ return {
3074
+ title: settings?.title ?? "",
3075
+ width: settings?.width,
3076
+ flexible: settings?.flexible,
3077
+ options: settings?.options ?? {},
3078
+ view: settings?.view
3079
+ };
3080
+ }
3081
+ function storedDefault(option) {
3082
+ return option.type === "dateRange" ? option.default : defaultOptionValue(option);
3083
+ }
3084
+ function sameValue2(a, b) {
3085
+ if (Array.isArray(a) && Array.isArray(b)) {
3086
+ return a.length === b.length && a.every((item, i) => item === b[i]);
3087
+ }
3088
+ return a === b;
3089
+ }
3090
+ function patchFrom(definition, draft) {
3091
+ const title = draft.title.trim();
3092
+ const options = {};
3093
+ for (const option of definition.options ?? []) {
3094
+ const value = draft.options[option.key];
3095
+ if (value === void 0) continue;
3096
+ if (sameValue2(value, storedDefault(option))) continue;
3097
+ options[option.key] = value;
3098
+ }
3099
+ return {
3100
+ title: title === "" ? void 0 : title,
3101
+ width: draft.width,
3102
+ flexible: draft.flexible === void 0 || draft.flexible === isFlexible(definition) ? void 0 : draft.flexible,
3103
+ options: Object.keys(options).length === 0 ? void 0 : options,
3104
+ // Choosing the author's own default view again clears the setting.
3105
+ view: draft.view === void 0 || draft.view === effectiveViewKey(definition, void 0) ? void 0 : draft.view
3106
+ };
3107
+ }
3108
+ function patchKey(patch) {
3109
+ const options = {};
3110
+ for (const key of Object.keys(patch.options ?? {}).sort()) {
3111
+ options[key] = patch.options[key];
3112
+ }
3113
+ return JSON.stringify([
3114
+ patch.title,
3115
+ patch.width,
3116
+ patch.flexible,
3117
+ options,
3118
+ patch.view
3119
+ ]);
3120
+ }
3121
+ var EMPTY_PATCH = {
3122
+ title: void 0,
3123
+ width: void 0,
3124
+ flexible: void 0,
3125
+ options: void 0,
3126
+ view: void 0
3127
+ };
3128
+ function WidgetOptionsDialog({
3129
+ definition,
3130
+ settings,
3131
+ baseline,
3132
+ isTitleInUse: isTitleInUse2,
3133
+ onApply,
3134
+ onClose
3135
+ }) {
3136
+ const slot = useSlotClassName();
3137
+ const { labels } = useWidgetSettings();
3138
+ const dialogRef = useRef(null);
3139
+ const headingId = useId();
3140
+ const baseId = useId();
3141
+ const [draft, setDraft] = useState(() => draftFrom(settings));
3142
+ const [confirming, setConfirming] = useState(
3143
+ void 0
3144
+ );
3145
+ useEffect(() => {
3146
+ const dialog = dialogRef.current;
3147
+ if (dialog === null) return void 0;
3148
+ const opener = document.activeElement;
3149
+ if (!dialog.open) dialog.showModal();
3150
+ return () => {
3151
+ if (dialog.open) dialog.close();
3152
+ if (opener instanceof HTMLElement) opener.focus();
3153
+ };
3154
+ }, []);
3155
+ const savedKey = patchKey(patchFrom(definition, draftFrom(settings)));
3156
+ const draftPatch = patchFrom(definition, draft);
3157
+ const dirty = patchKey(draftPatch) !== savedKey;
3158
+ const titleTaken = draft.title.trim() !== "" && isTitleInUse2?.(draft.title.trim()) === true;
3159
+ const canApply = dirty && !titleTaken;
3160
+ const baselineKey = baseline === void 0 ? void 0 : patchKey(patchFrom(definition, draftFrom(baseline.settings)));
3161
+ const canRevert = baselineKey !== void 0 && baselineKey !== savedKey;
3162
+ const canReset = savedKey !== patchKey(EMPTY_PATCH);
3163
+ const range = widthRange(definition);
3164
+ const views = viewChoices(definition);
3165
+ const effectiveFlexible = draft.flexible ?? isFlexible(definition);
3166
+ function requestClose() {
3167
+ if (confirming !== void 0) {
3168
+ setConfirming(void 0);
3169
+ } else if (dirty) {
3170
+ setConfirming("discard");
3171
+ } else {
3172
+ onClose();
3173
+ }
3174
+ }
3175
+ function answer(yes) {
3176
+ const which = confirming;
3177
+ setConfirming(void 0);
3178
+ if (!yes || which === void 0) return;
3179
+ if (which === "discard") {
3180
+ onClose();
3181
+ } else if (which === "apply") {
3182
+ onApply(draftPatch);
3183
+ onClose();
3184
+ } else if (which === "reset") {
3185
+ onApply(EMPTY_PATCH);
3186
+ onClose();
3187
+ } else if (baseline !== void 0) {
3188
+ onApply(patchFrom(definition, draftFrom(baseline.settings)));
3189
+ onClose();
3190
+ }
3191
+ }
3192
+ function setOption(key, value) {
3193
+ setDraft((current) => ({
3194
+ ...current,
3195
+ options: { ...current.options, [key]: value }
3196
+ }));
3197
+ }
3198
+ const confirmMessage = {
3199
+ apply: labels.confirmApply,
3200
+ discard: labels.confirmDiscard,
3201
+ reset: labels.confirmResetOptions,
3202
+ revert: labels.confirmRevertOptions
3203
+ };
3204
+ const widths = [];
3205
+ for (let width = range.min; width <= range.max; width += 1)
3206
+ widths.push(width);
3207
+ return /* @__PURE__ */ jsx(
3208
+ "dialog",
3209
+ {
3210
+ ref: dialogRef,
3211
+ className: slot("dialog", "dwt-options-dialog"),
3212
+ "aria-labelledby": headingId,
3213
+ onCancel: (event) => {
3214
+ event.preventDefault();
3215
+ requestClose();
3216
+ },
3217
+ onClick: (event) => {
3218
+ if (event.target === dialogRef.current) requestClose();
3219
+ },
3220
+ children: /* @__PURE__ */ jsxs(
3221
+ "form",
3222
+ {
3223
+ className: "dwt-options",
3224
+ onSubmit: (event) => {
3225
+ event.preventDefault();
3226
+ if (canApply && confirming === void 0) setConfirming("apply");
3227
+ },
3228
+ children: [
3229
+ /* @__PURE__ */ jsx("h2", { id: headingId, className: "dwt-options-heading", children: labels.optionsFor(definition.title) }),
3230
+ /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3231
+ /* @__PURE__ */ jsx("label", { htmlFor: `${baseId}-title`, children: labels.optionsTitleField }),
3232
+ /* @__PURE__ */ jsx(
3233
+ "input",
3234
+ {
3235
+ id: `${baseId}-title`,
3236
+ type: "text",
3237
+ value: draft.title,
3238
+ maxLength: MAX_WIDGET_TITLE_LENGTH,
3239
+ placeholder: definition.title,
3240
+ "aria-invalid": titleTaken,
3241
+ ...titleTaken ? { "aria-describedby": `${baseId}-title-error` } : {},
3242
+ onChange: (event) => setDraft((current) => ({ ...current, title: event.target.value }))
3243
+ }
3244
+ ),
3245
+ titleTaken && /* @__PURE__ */ jsx(
3246
+ "p",
3247
+ {
3248
+ id: `${baseId}-title-error`,
3249
+ role: "alert",
3250
+ className: "dwt-options-error",
3251
+ children: labels.titleInUse
3252
+ }
3253
+ )
3254
+ ] }),
3255
+ range.min < range.max && /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3256
+ /* @__PURE__ */ jsx("label", { htmlFor: `${baseId}-width`, children: labels.optionsWidthField }),
3257
+ /* @__PURE__ */ jsxs(
3258
+ "select",
3259
+ {
3260
+ id: `${baseId}-width`,
3261
+ value: draft.width === void 0 ? "" : String(draft.width),
3262
+ onChange: (event) => {
3263
+ const value = event.target.value;
3264
+ setDraft((current) => ({
3265
+ ...current,
3266
+ width: value === "" ? void 0 : Number(value)
3267
+ }));
3268
+ },
3269
+ children: [
3270
+ /* @__PURE__ */ jsx("option", { value: "", children: labels.widthDefault }),
3271
+ widths.map((width) => /* @__PURE__ */ jsx("option", { value: String(width), children: labels.widthOption(width, widthShareLabel(width)) }, width))
3272
+ ]
3273
+ }
3274
+ )
3275
+ ] }),
3276
+ /* @__PURE__ */ jsxs("label", { className: "dwt-options-check", children: [
3277
+ /* @__PURE__ */ jsx(
3278
+ "input",
3279
+ {
3280
+ type: "checkbox",
3281
+ checked: effectiveFlexible,
3282
+ onChange: (event) => setDraft((current) => ({
3283
+ ...current,
3284
+ flexible: event.target.checked
3285
+ }))
3286
+ }
3287
+ ),
3288
+ labels.optionsFlexible
3289
+ ] }),
3290
+ views.length > 0 && /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3291
+ /* @__PURE__ */ jsx("label", { htmlFor: `${baseId}-view`, children: labels.optionsViewField }),
3292
+ /* @__PURE__ */ jsx(
3293
+ "select",
3294
+ {
3295
+ id: `${baseId}-view`,
3296
+ value: effectiveViewKey(definition, draft.view),
3297
+ onChange: (event) => {
3298
+ const value = event.target.value;
3299
+ setDraft((current) => ({ ...current, view: value }));
3300
+ },
3301
+ children: views.map((view) => /* @__PURE__ */ jsx("option", { value: view.key, children: view.key === effectiveViewKey(definition, void 0) ? labels.viewDefault(view.label) : view.label }, view.key))
3302
+ }
3303
+ )
3304
+ ] }),
3305
+ (definition.options?.length ?? 0) > 0 && /* @__PURE__ */ jsxs("fieldset", { className: "dwt-options-group", children: [
3306
+ /* @__PURE__ */ jsx("legend", { children: labels.optionsDeclared }),
3307
+ (definition.options ?? []).map((option) => /* @__PURE__ */ jsx(
3308
+ OptionField,
3309
+ {
3310
+ option,
3311
+ value: draft.options[option.key],
3312
+ id: `${baseId}-opt-${option.key}`,
3313
+ onChange: (value) => setOption(option.key, value)
3314
+ },
3315
+ option.key
3316
+ ))
3317
+ ] }),
3318
+ /* @__PURE__ */ jsx("div", { className: "dwt-options-actions", children: confirming === void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
3319
+ /* @__PURE__ */ jsx(
3320
+ "button",
3321
+ {
3322
+ type: "submit",
3323
+ className: slot("button", "dwt-button", "dwt-button-primary"),
3324
+ disabled: !canApply,
3325
+ children: labels.apply
3326
+ }
3327
+ ),
3328
+ /* @__PURE__ */ jsx(
3329
+ "button",
3330
+ {
3331
+ type: "button",
3332
+ className: slot("button", "dwt-button"),
3333
+ onClick: requestClose,
3334
+ children: labels.cancel
3335
+ }
3336
+ ),
3337
+ /* @__PURE__ */ jsx(
3338
+ "button",
3339
+ {
3340
+ type: "button",
3341
+ className: slot("button", "dwt-button"),
3342
+ disabled: !canReset,
3343
+ onClick: () => setConfirming("reset"),
3344
+ children: labels.resetToDefaults
3345
+ }
3346
+ ),
3347
+ baseline !== void 0 && /* @__PURE__ */ jsx(
2541
3348
  "button",
2542
3349
  {
2543
3350
  type: "button",
2544
- role: "menuitem",
2545
- className: "dwt-edit-option",
2546
- ...item.ariaLabel === void 0 ? {} : { "aria-label": item.ariaLabel },
2547
- disabled: item.disabled === true,
2548
- onClick: () => {
2549
- close(false);
2550
- item.onSelect();
2551
- },
2552
- children: item.label
2553
- },
2554
- item.key
3351
+ className: slot("button", "dwt-button"),
3352
+ disabled: !canRevert,
3353
+ onClick: () => setConfirming("revert"),
3354
+ children: labels.revertToBeforeEditing
3355
+ }
2555
3356
  )
2556
- )
2557
- }
2558
- )
2559
- ]
3357
+ ] }) : /* @__PURE__ */ jsx(
3358
+ InlineConfirm,
3359
+ {
3360
+ message: confirmMessage[confirming],
3361
+ onAnswer: answer
3362
+ }
3363
+ ) })
3364
+ ]
3365
+ }
3366
+ )
2560
3367
  }
2561
3368
  );
2562
3369
  }
3370
+ function OptionField({
3371
+ option,
3372
+ value,
3373
+ id,
3374
+ onChange
3375
+ }) {
3376
+ const current = value ?? storedDefault(option);
3377
+ switch (option.type) {
3378
+ case "choice":
3379
+ return /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3380
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: option.label }),
3381
+ /* @__PURE__ */ jsx(
3382
+ "select",
3383
+ {
3384
+ id,
3385
+ value: String(current ?? option.default),
3386
+ onChange: (event) => onChange(event.target.value),
3387
+ children: option.choices.map((choice) => /* @__PURE__ */ jsx("option", { value: choice.value, children: choice.label }, choice.value))
3388
+ }
3389
+ )
3390
+ ] });
3391
+ case "number":
3392
+ return /* @__PURE__ */ jsx(
3393
+ NumberField,
3394
+ {
3395
+ option,
3396
+ id,
3397
+ value: typeof current === "number" ? current : option.default,
3398
+ onChange
3399
+ }
3400
+ );
3401
+ case "boolean":
3402
+ return /* @__PURE__ */ jsxs("label", { className: "dwt-options-check", children: [
3403
+ /* @__PURE__ */ jsx(
3404
+ "input",
3405
+ {
3406
+ type: "checkbox",
3407
+ checked: current === true,
3408
+ onChange: (event) => onChange(event.target.checked)
3409
+ }
3410
+ ),
3411
+ option.label
3412
+ ] });
3413
+ case "text":
3414
+ return /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3415
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: option.label }),
3416
+ /* @__PURE__ */ jsx(
3417
+ "input",
3418
+ {
3419
+ id,
3420
+ type: "text",
3421
+ value: typeof current === "string" ? current : "",
3422
+ maxLength: option.maxLength,
3423
+ onChange: (event) => onChange(event.target.value)
3424
+ }
3425
+ )
3426
+ ] });
3427
+ case "color":
3428
+ return /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3429
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: option.label }),
3430
+ /* @__PURE__ */ jsx(
3431
+ "input",
3432
+ {
3433
+ id,
3434
+ type: "color",
3435
+ value: typeof current === "string" ? current : option.default,
3436
+ onChange: (event) => onChange(event.target.value)
3437
+ }
3438
+ )
3439
+ ] });
3440
+ case "dateRange":
3441
+ return /* @__PURE__ */ jsx(
3442
+ DateRangeField,
3443
+ {
3444
+ option,
3445
+ id,
3446
+ value: typeof current === "string" ? current : option.default,
3447
+ onChange
3448
+ }
3449
+ );
3450
+ case "sort":
3451
+ return /* @__PURE__ */ jsx(
3452
+ SortField,
3453
+ {
3454
+ option,
3455
+ id,
3456
+ value: typeof current === "string" ? current : void 0,
3457
+ onChange
3458
+ }
3459
+ );
3460
+ case "columns":
3461
+ return /* @__PURE__ */ jsx(
3462
+ ColumnsField,
3463
+ {
3464
+ option,
3465
+ value: Array.isArray(current) ? current : option.default,
3466
+ onChange
3467
+ }
3468
+ );
3469
+ }
3470
+ }
3471
+ function NumberField({
3472
+ option,
3473
+ id,
3474
+ value,
3475
+ onChange
3476
+ }) {
3477
+ const [text, setText] = useState(String(value));
3478
+ useEffect(() => {
3479
+ setText((typed) => Number(typed) === value ? typed : String(value));
3480
+ }, [value]);
3481
+ return /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3482
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: option.label }),
3483
+ /* @__PURE__ */ jsx(
3484
+ "input",
3485
+ {
3486
+ id,
3487
+ type: "number",
3488
+ value: text,
3489
+ min: option.min,
3490
+ max: option.max,
3491
+ step: option.step ?? 1,
3492
+ onChange: (event) => {
3493
+ setText(event.target.value);
3494
+ const parsed = Number(event.target.value);
3495
+ if (event.target.value.trim() !== "" && coerceOptionValue(option, parsed) !== void 0) {
3496
+ onChange(parsed);
3497
+ }
3498
+ }
3499
+ }
3500
+ )
3501
+ ] });
3502
+ }
3503
+ var PRESET_LABELS = {
3504
+ today: "Today",
3505
+ yesterday: "Yesterday",
3506
+ last7: "Last 7 days",
3507
+ last30: "Last 30 days",
3508
+ last90: "Last 90 days",
3509
+ thisMonth: "This month",
3510
+ lastMonth: "Last month",
3511
+ thisYear: "This year",
3512
+ lastYear: "Last year"
3513
+ };
3514
+ function DateRangeField({
3515
+ option,
3516
+ id,
3517
+ value,
3518
+ onChange
3519
+ }) {
3520
+ const { labels } = useWidgetSettings();
3521
+ const presets = option.presets ?? DATE_RANGE_PRESETS.map((preset) => ({
3522
+ value: preset,
3523
+ label: PRESET_LABELS[preset]
3524
+ }));
3525
+ const known = presets.some((preset) => preset.value === value);
3526
+ return /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3527
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: option.label }),
3528
+ /* @__PURE__ */ jsxs(
3529
+ "select",
3530
+ {
3531
+ id,
3532
+ value,
3533
+ onChange: (event) => onChange(event.target.value),
3534
+ children: [
3535
+ !known && /* @__PURE__ */ jsx("option", { value, children: labels.customRange(value) }),
3536
+ presets.map((preset) => /* @__PURE__ */ jsx("option", { value: preset.value, children: preset.label }, preset.value))
3537
+ ]
3538
+ }
3539
+ )
3540
+ ] });
3541
+ }
3542
+ function SortField({
3543
+ option,
3544
+ id,
3545
+ value,
3546
+ onChange
3547
+ }) {
3548
+ const { labels } = useWidgetSettings();
3549
+ const parsed = value === void 0 ? void 0 : parseSortValue(value);
3550
+ const column = parsed?.column ?? "";
3551
+ const direction = parsed?.direction ?? "desc";
3552
+ return /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3553
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: option.label }),
3554
+ /* @__PURE__ */ jsxs("span", { className: "dwt-options-pair", children: [
3555
+ /* @__PURE__ */ jsxs(
3556
+ "select",
3557
+ {
3558
+ id,
3559
+ value: column,
3560
+ onChange: (event) => onChange(`${event.target.value}:${direction}`),
3561
+ children: [
3562
+ option.default === void 0 && column === "" && /* @__PURE__ */ jsx("option", { value: "", children: labels.sortDefault }),
3563
+ option.columns.map((entry) => /* @__PURE__ */ jsx("option", { value: entry.key, children: entry.label }, entry.key))
3564
+ ]
3565
+ }
3566
+ ),
3567
+ /* @__PURE__ */ jsxs(
3568
+ "select",
3569
+ {
3570
+ "aria-label": option.label,
3571
+ value: direction,
3572
+ disabled: column === "",
3573
+ onChange: (event) => onChange(`${column}:${event.target.value}`),
3574
+ children: [
3575
+ /* @__PURE__ */ jsx("option", { value: "asc", children: labels.sortAscending }),
3576
+ /* @__PURE__ */ jsx("option", { value: "desc", children: labels.sortDescending })
3577
+ ]
3578
+ }
3579
+ )
3580
+ ] })
3581
+ ] });
3582
+ }
3583
+ function ColumnsField({
3584
+ option,
3585
+ value,
3586
+ onChange
3587
+ }) {
3588
+ const { labels } = useWidgetSettings();
3589
+ const slot = useSlotClassName();
3590
+ const ordered = [
3591
+ ...value,
3592
+ ...option.columns.map((c) => c.key).filter((key) => !value.includes(key))
3593
+ ];
3594
+ const labelOf = (key) => option.columns.find((c) => c.key === key)?.label ?? key;
3595
+ function move(key, by) {
3596
+ const next = [...value];
3597
+ const from = next.indexOf(key);
3598
+ const to = from + by;
3599
+ if (from === -1 || to < 0 || to >= next.length) return;
3600
+ [next[from], next[to]] = [next[to], next[from]];
3601
+ onChange(next);
3602
+ }
3603
+ return /* @__PURE__ */ jsxs("fieldset", { className: "dwt-options-columns", children: [
3604
+ /* @__PURE__ */ jsx("legend", { children: option.label }),
3605
+ /* @__PURE__ */ jsx("ul", { children: ordered.map((key) => {
3606
+ const shown = value.includes(key);
3607
+ const name = labelOf(key);
3608
+ return /* @__PURE__ */ jsxs("li", { children: [
3609
+ /* @__PURE__ */ jsxs("label", { children: [
3610
+ /* @__PURE__ */ jsx(
3611
+ "input",
3612
+ {
3613
+ type: "checkbox",
3614
+ checked: shown,
3615
+ disabled: shown && value.length === 1,
3616
+ onChange: (event) => onChange(
3617
+ event.target.checked ? [...value, key] : value.filter((k) => k !== key)
3618
+ )
3619
+ }
3620
+ ),
3621
+ name
3622
+ ] }),
3623
+ shown && /* @__PURE__ */ jsxs("span", { className: "dwt-options-move", children: [
3624
+ /* @__PURE__ */ jsx(
3625
+ "button",
3626
+ {
3627
+ type: "button",
3628
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3629
+ "aria-label": labels.moveColumnUp(name),
3630
+ title: labels.moveColumnUp(name),
3631
+ disabled: value.indexOf(key) === 0,
3632
+ onClick: () => move(key, -1),
3633
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2191" })
3634
+ }
3635
+ ),
3636
+ /* @__PURE__ */ jsx(
3637
+ "button",
3638
+ {
3639
+ type: "button",
3640
+ className: slot("button", "dwt-button", "dwt-icon-button"),
3641
+ "aria-label": labels.moveColumnDown(name),
3642
+ title: labels.moveColumnDown(name),
3643
+ disabled: value.indexOf(key) === value.length - 1,
3644
+ onClick: () => move(key, 1),
3645
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2193" })
3646
+ }
3647
+ )
3648
+ ] })
3649
+ ] }, key);
3650
+ }) })
3651
+ ] });
3652
+ }
2563
3653
  function nextSort(current, column) {
2564
3654
  if (current?.column !== column) return { column, direction: "asc" };
2565
3655
  return current.direction === "asc" ? { column, direction: "desc" } : void 0;
@@ -2987,57 +4077,6 @@ function DetailHost({
2987
4077
  }
2988
4078
  ) });
2989
4079
  }
2990
- function InlineConfirm({
2991
- message,
2992
- onAnswer
2993
- }) {
2994
- const slot = useSlotClassName();
2995
- const { labels } = useWidgetSettings();
2996
- const noRef = useRef(null);
2997
- useEffect(() => {
2998
- noRef.current?.focus();
2999
- }, []);
3000
- return /* @__PURE__ */ jsxs(
3001
- "span",
3002
- {
3003
- role: "group",
3004
- "aria-label": message,
3005
- className: "dwt-confirm",
3006
- onKeyDown: (event) => {
3007
- if (event.key === "Escape") {
3008
- event.stopPropagation();
3009
- onAnswer(false);
3010
- }
3011
- },
3012
- children: [
3013
- /* @__PURE__ */ jsx("span", { role: "alert", className: "dwt-confirm-message", children: message }),
3014
- /* @__PURE__ */ jsx(
3015
- "button",
3016
- {
3017
- type: "button",
3018
- className: slot("button", "dwt-button", "dwt-icon-button"),
3019
- "aria-label": labels.confirmYes,
3020
- title: labels.confirmYes,
3021
- onClick: () => onAnswer(true),
3022
- children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2713" })
3023
- }
3024
- ),
3025
- /* @__PURE__ */ jsx(
3026
- "button",
3027
- {
3028
- ref: noRef,
3029
- type: "button",
3030
- className: slot("button", "dwt-button", "dwt-icon-button"),
3031
- "aria-label": labels.confirmNo,
3032
- title: labels.confirmNo,
3033
- onClick: () => onAnswer(false),
3034
- children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2715" })
3035
- }
3036
- )
3037
- ]
3038
- }
3039
- );
3040
- }
3041
4080
  function PageTabs({
3042
4081
  pages,
3043
4082
  activeKey,
@@ -3242,13 +4281,23 @@ function DashboardInner({
3242
4281
  className
3243
4282
  }) {
3244
4283
  const slot = useSlotClassName();
3245
- const { labels } = useWidgetSettings();
3246
- const definitions = widgets.map(
4284
+ const { labels, locale } = useWidgetSettings();
4285
+ const effectiveWidgets = widgets.map((w) => {
4286
+ const settings = !overrideLocks && isOptionsLocked(w.definition) ? void 0 : savedLayout.settings?.[w.definition.key];
4287
+ const styled = settings === void 0 ? w : { ...w, definition: applySettings(w.definition, settings) };
4288
+ return applyView(styled, settings?.view, {
4289
+ ...locale === void 0 ? {} : { locale }
4290
+ });
4291
+ });
4292
+ const definitions = effectiveWidgets.map(
3247
4293
  (w) => overrideLocks ? withoutLock(w.definition) : w.definition
3248
4294
  );
3249
4295
  const layout = enforceLocks(definitions, savedLayout);
3250
4296
  const byKey = new Map(
3251
- widgets.map((w) => [w.definition.key, w])
4297
+ effectiveWidgets.map((w) => [w.definition.key, w])
4298
+ );
4299
+ const authorDefinitions = new Map(
4300
+ widgets.map((w) => [w.definition.key, w.definition])
3252
4301
  );
3253
4302
  const pages = pageList(layout);
3254
4303
  const paged = pages.length >= 2;
@@ -3293,6 +4342,7 @@ function DashboardInner({
3293
4342
  const minimized = editable ? visible.filter((definition) => isMinimized(view, definition.key)) : [];
3294
4343
  const shown = editable ? visible.filter((definition) => !isMinimized(view, definition.key)) : visible;
3295
4344
  const [detailKey, setDetailKey] = useState(void 0);
4345
+ const [optionsKey, setOptionsKey] = useState(void 0);
3296
4346
  const gridRef = useRef(null);
3297
4347
  const twelve = usesWidthColumns(shown);
3298
4348
  const spans = useFillSpans(
@@ -3432,7 +4482,14 @@ function DashboardInner({
3432
4482
  const which = pending;
3433
4483
  setConfirming(void 0);
3434
4484
  if (yes && which === "reset") {
3435
- change(enforceLocks(definitions, defaultLayout ?? EMPTY_LAYOUT));
4485
+ const arranged = enforceLocks(
4486
+ definitions,
4487
+ defaultLayout ?? EMPTY_LAYOUT
4488
+ );
4489
+ const { settings: _defaultSettings, ...arrangement } = arranged;
4490
+ change(
4491
+ layout.settings === void 0 ? arrangement : { ...arrangement, settings: layout.settings }
4492
+ );
3436
4493
  } else if (yes && which === "revert" && snapshot !== void 0) {
3437
4494
  change(snapshot);
3438
4495
  }
@@ -3729,6 +4786,15 @@ function DashboardInner({
3729
4786
  onSelect: () => changePage(moveWidgetBy(pageDefs, view, key, 1))
3730
4787
  }
3731
4788
  ],
4789
+ ...lock.options ? [] : [
4790
+ {
4791
+ kind: "action",
4792
+ key: "options",
4793
+ label: labels.menuOptions,
4794
+ ariaLabel: labels.optionsFor(title),
4795
+ onSelect: () => setOptionsKey(key)
4796
+ }
4797
+ ],
3732
4798
  ...pageTargets,
3733
4799
  ...lock.hide ? [] : [
3734
4800
  {
@@ -3817,6 +4883,30 @@ function DashboardInner({
3817
4883
  ]
3818
4884
  }
3819
4885
  ),
4886
+ optionsKey !== void 0 && authorDefinitions.get(optionsKey) !== void 0 && /* @__PURE__ */ jsx(
4887
+ WidgetOptionsDialog,
4888
+ {
4889
+ definition: authorDefinitions.get(optionsKey),
4890
+ settings: layout.settings?.[optionsKey],
4891
+ ...snapshot === void 0 ? {} : { baseline: { settings: snapshot.settings?.[optionsKey] } },
4892
+ isTitleInUse: (title) => isTitleInUse(
4893
+ widgets.map((w) => w.definition),
4894
+ layout,
4895
+ optionsKey,
4896
+ title
4897
+ ),
4898
+ onApply: (patch) => {
4899
+ change(setWidgetSettings(layout, optionsKey, patch));
4900
+ setAnnouncement(
4901
+ labels.optionsSaved(
4902
+ authorDefinitions.get(optionsKey)?.title ?? optionsKey
4903
+ )
4904
+ );
4905
+ },
4906
+ onClose: () => setOptionsKey(void 0)
4907
+ },
4908
+ optionsKey
4909
+ ),
3820
4910
  detailKey !== void 0 && byKey.get(detailKey) !== void 0 && /* @__PURE__ */ jsx(
3821
4911
  DetailHost,
3822
4912
  {
@@ -4042,45 +5132,6 @@ function useStoredLayout(options) {
4042
5132
  };
4043
5133
  }
4044
5134
 
4045
- // src/core/payload.ts
4046
- var WIDGET_KINDS = [
4047
- "TEXT",
4048
- "KPI",
4049
- "GAUGE",
4050
- "TABLE",
4051
- "BAR_LIST",
4052
- "ALERT_LIST",
4053
- "GRAPH"
4054
- ];
4055
- function isWidgetKind(value) {
4056
- return typeof value === "string" && WIDGET_KINDS.includes(value);
4057
- }
4058
- var VALUE_FORMATS = ["number", "currency", "percent"];
4059
- var MAX_GRAPH_SERIES = 8;
4060
- var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
4061
- function isWidgetEmptyState(payload) {
4062
- return "empty" in payload && payload.empty === true;
4063
- }
4064
- function emptyTextFor(payload) {
4065
- if (isWidgetEmptyState(payload)) {
4066
- return payload.text;
4067
- }
4068
- switch (payload.kind) {
4069
- case "TABLE":
4070
- return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
4071
- case "BAR_LIST":
4072
- return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
4073
- case "ALERT_LIST":
4074
- return payload.items.length === 0 ? payload.emptyText : null;
4075
- case "GRAPH":
4076
- return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
4077
- case "TEXT":
4078
- case "KPI":
4079
- case "GAUGE":
4080
- return null;
4081
- }
4082
- }
4083
-
4084
5135
  // src/core/validate.ts
4085
5136
  var MAX_REPORTED_PROBLEMS = 10;
4086
5137
  function describeType2(value) {
@@ -4091,7 +5142,7 @@ function describeType2(value) {
4091
5142
  }
4092
5143
  return typeof value;
4093
5144
  }
4094
- function isRecord(value) {
5145
+ function isRecord2(value) {
4095
5146
  return typeof value === "object" && value !== null && !Array.isArray(value);
4096
5147
  }
4097
5148
  function join(path, field) {
@@ -4173,7 +5224,7 @@ function requireOneOf(record, field, options, path, problems) {
4173
5224
  function eachRecord(items, path, problems, check) {
4174
5225
  items.forEach((item, index) => {
4175
5226
  const itemPath = `${path}[${index}]`;
4176
- if (isRecord(item)) {
5227
+ if (isRecord2(item)) {
4177
5228
  check(item, itemPath);
4178
5229
  } else {
4179
5230
  problems.push(
@@ -4327,7 +5378,7 @@ function formatProblems(prefix, problems) {
4327
5378
  }
4328
5379
  function validateWidgetData(value, options = {}) {
4329
5380
  const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
4330
- if (!isRecord(value)) {
5381
+ if (!isRecord2(value)) {
4331
5382
  return err(`${subject}: must be an object, got ${describeType2(value)}.`);
4332
5383
  }
4333
5384
  if (value["empty"] === true) {
@@ -4772,6 +5823,6 @@ function useWidgets(definitions, providers, context, options = {}) {
4772
5823
  return { widgets, refresh, setOptions, gridRef };
4773
5824
  }
4774
5825
 
4775
- 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 };
5826
+ export { AlertListWidget, BarListWidget, DEFAULT_LABELS, DEFAULT_SERIES_COLORS, Dashboard, GaugeWidget, GraphWidget, KpiWidget, ResolvedWidgetCard, TableWidget, TextWidget, Thumbnail, WidgetCard, WidgetContent, WidgetDetail, WidgetDetailDialog, WidgetGrid, WidgetLink, WidgetOptionsDialog, WidgetSettingsProvider, barPath, buildChartModel, describeChart, joinClassNames, seriesColor, useDetailData, useStoredLayout, useWidgetSettings, useWidgets, widgetTitle };
4776
5827
  //# sourceMappingURL=react.js.map
4777
5828
  //# sourceMappingURL=react.js.map