@richardmcquiston01/dashboard-widgets-toolkit 0.14.0 → 0.16.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
@@ -828,6 +828,14 @@ var DEFAULT_LABELS = {
828
828
  menuMoveLater: "Move later",
829
829
  menuHide: "Hide",
830
830
  menuOptions: "Options\u2026",
831
+ menuDuplicate: "Duplicate",
832
+ duplicate: (title) => `Duplicate ${title}`,
833
+ duplicateBlocked: (title, reason) => `Duplicate ${title} (unavailable: ${reason})`,
834
+ duplicated: (title) => `Duplicated ${title}`,
835
+ optionsDelete: "Delete this widget",
836
+ confirmDelete: "Delete this widget? Its settings are lost.",
837
+ deleted: (title) => `Deleted ${title}`,
838
+ suggestedViewTitle: (title, viewLabel) => `${title} ${viewLabel}`,
831
839
  optionsFor: (title) => `Options for ${title}`,
832
840
  optionsTitleField: "Title",
833
841
  titleInUse: "Another widget already uses this title.",
@@ -835,6 +843,8 @@ var DEFAULT_LABELS = {
835
843
  widthDefault: "Default",
836
844
  widthOption: (width, share) => `${width} of 12 (${share})`,
837
845
  optionsFlexible: "Grow to fill empty space in the row",
846
+ optionsViewField: "View",
847
+ viewDefault: (viewLabel) => `${viewLabel} (default)`,
838
848
  optionsDeclared: "Data",
839
849
  apply: "Apply",
840
850
  cancel: "Cancel",
@@ -1935,13 +1945,15 @@ var NOT_LOCKED = Object.freeze({
1935
1945
  move: false,
1936
1946
  hide: false,
1937
1947
  minimize: false,
1938
- options: false
1948
+ options: false,
1949
+ clone: false
1939
1950
  });
1940
1951
  var FULLY_LOCKED = Object.freeze({
1941
1952
  move: true,
1942
1953
  hide: true,
1943
1954
  minimize: true,
1944
- options: true
1955
+ options: true,
1956
+ clone: true
1945
1957
  });
1946
1958
  function resolveWidgetLock(definition) {
1947
1959
  const setting = definition.locked;
@@ -1951,7 +1963,8 @@ function resolveWidgetLock(definition) {
1951
1963
  move: setting.move === true,
1952
1964
  hide: setting.hide === true,
1953
1965
  minimize: setting.minimize === true,
1954
- options: setting.options === true
1966
+ options: setting.options === true,
1967
+ clone: setting.clone === true
1955
1968
  };
1956
1969
  }
1957
1970
  function sortDefinitions(definitions) {
@@ -1972,6 +1985,282 @@ function isWidgetVisibleTo(definition, roles) {
1972
1985
  return definition.roles.some((role) => roles.includes(role));
1973
1986
  }
1974
1987
 
1988
+ // src/core/payload.ts
1989
+ var WIDGET_KINDS = [
1990
+ "TEXT",
1991
+ "KPI",
1992
+ "GAUGE",
1993
+ "TABLE",
1994
+ "BAR_LIST",
1995
+ "ALERT_LIST",
1996
+ "GRAPH"
1997
+ ];
1998
+ function isWidgetKind(value) {
1999
+ return typeof value === "string" && WIDGET_KINDS.includes(value);
2000
+ }
2001
+ var VALUE_FORMATS = ["number", "currency", "percent"];
2002
+ var MAX_GRAPH_SERIES = 8;
2003
+ var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
2004
+ function isWidgetEmptyState(payload) {
2005
+ return "empty" in payload && payload.empty === true;
2006
+ }
2007
+ function emptyTextFor(payload) {
2008
+ if (isWidgetEmptyState(payload)) {
2009
+ return payload.text;
2010
+ }
2011
+ switch (payload.kind) {
2012
+ case "TABLE":
2013
+ return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
2014
+ case "BAR_LIST":
2015
+ return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
2016
+ case "ALERT_LIST":
2017
+ return payload.items.length === 0 ? payload.emptyText : null;
2018
+ case "GRAPH":
2019
+ return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
2020
+ case "TEXT":
2021
+ case "KPI":
2022
+ case "GAUGE":
2023
+ return null;
2024
+ }
2025
+ }
2026
+
2027
+ // src/core/views.ts
2028
+ var BASE_VIEW_KEY = "base";
2029
+ var VIEW_CONVERSIONS = {
2030
+ tableToBarList: { from: "TABLE", to: "BAR_LIST" },
2031
+ tableToGraph: { from: "TABLE", to: "GRAPH" },
2032
+ barListToTable: { from: "BAR_LIST", to: "TABLE" },
2033
+ graphToTable: { from: "GRAPH", to: "TABLE" },
2034
+ barListToGraph: { from: "BAR_LIST", to: "GRAPH" }
2035
+ };
2036
+ var KIND_LABELS = {
2037
+ TEXT: "Text",
2038
+ KPI: "Number",
2039
+ GAUGE: "Gauge",
2040
+ TABLE: "Table",
2041
+ BAR_LIST: "Bars",
2042
+ ALERT_LIST: "List",
2043
+ GRAPH: "Chart"
2044
+ };
2045
+ function kindLabel(kind) {
2046
+ return KIND_LABELS[kind];
2047
+ }
2048
+ function hasViews(item) {
2049
+ return item.views !== void 0 && item.views.length > 0;
2050
+ }
2051
+ function findView(item, key) {
2052
+ return item.views?.find((view) => view.key === key);
2053
+ }
2054
+ function viewChoices(item) {
2055
+ if (!hasViews(item)) return [];
2056
+ return [
2057
+ {
2058
+ key: BASE_VIEW_KEY,
2059
+ label: item.baseViewLabel ?? kindLabel(item.kind),
2060
+ kind: item.kind
2061
+ },
2062
+ ...(item.views ?? []).map(({ key, label, kind }) => ({
2063
+ key,
2064
+ label,
2065
+ kind
2066
+ }))
2067
+ ];
2068
+ }
2069
+ function isKnownView(item, key) {
2070
+ return key !== void 0 && (key === BASE_VIEW_KEY || findView(item, key) !== void 0);
2071
+ }
2072
+ function effectiveViewKey(item, chosen) {
2073
+ if (!hasViews(item)) return BASE_VIEW_KEY;
2074
+ if (isKnownView(item, chosen)) return chosen;
2075
+ if (isKnownView(item, item.defaultView)) return item.defaultView;
2076
+ return BASE_VIEW_KEY;
2077
+ }
2078
+ function cellValue2(cell) {
2079
+ const value = cell?.value;
2080
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2081
+ }
2082
+ function columnName(table, index) {
2083
+ return table.columns[index]?.label ?? `#${index + 1}`;
2084
+ }
2085
+ function checkColumn(table, index, field, context) {
2086
+ if (!Number.isInteger(index) || index < 0 || index >= table.columns.length) {
2087
+ return `${context.where}: ${field} column ${index} does not exist (the table has ${table.columns.length} columns, numbered from 0).`;
2088
+ }
2089
+ return void 0;
2090
+ }
2091
+ function numericColumn(table, index, context) {
2092
+ const numbers = [];
2093
+ for (const [rowIndex, row] of table.rows.entries()) {
2094
+ const value = cellValue2(row[index]);
2095
+ if (value === void 0) {
2096
+ return err(
2097
+ `${context.where}: row ${rowIndex + 1}, column ${index + 1} ("${columnName(table, index)}") has no numeric value. Add value to the cells.`
2098
+ );
2099
+ }
2100
+ numbers.push(value);
2101
+ }
2102
+ return ok(numbers);
2103
+ }
2104
+ function tableToBarList(table, conversion, context) {
2105
+ const bad = checkColumn(table, conversion.label, "label", context) ?? checkColumn(table, conversion.value, "value", context);
2106
+ if (bad !== void 0) return err(bad);
2107
+ const numbers = numericColumn(table, conversion.value, context);
2108
+ if (!numbers.ok) return numbers;
2109
+ const items = table.rows.map((row, index) => ({
2110
+ label: row[conversion.label]?.text ?? "",
2111
+ value: numbers.value[index],
2112
+ display: row[conversion.value]?.text ?? String(numbers.value[index])
2113
+ }));
2114
+ return ok({ kind: "BAR_LIST", items });
2115
+ }
2116
+ function tableToGraph(table, conversion, context) {
2117
+ if (conversion.values.length === 0 || conversion.values.length > MAX_GRAPH_SERIES) {
2118
+ return err(
2119
+ `${context.where}: a chart needs 1 to ${MAX_GRAPH_SERIES} series columns, got ${conversion.values.length}.`
2120
+ );
2121
+ }
2122
+ let bad = checkColumn(
2123
+ table,
2124
+ conversion.label,
2125
+ "label",
2126
+ context
2127
+ );
2128
+ for (const index of conversion.values) {
2129
+ bad ??= checkColumn(table, index, "series", context);
2130
+ }
2131
+ if (bad !== void 0) return err(bad);
2132
+ const series = [];
2133
+ for (const index of conversion.values) {
2134
+ const numbers = numericColumn(table, index, context);
2135
+ if (!numbers.ok) return numbers;
2136
+ series.push({
2137
+ name: columnName(table, index),
2138
+ points: table.rows.map((row, rowIndex) => ({
2139
+ label: row[conversion.label]?.text ?? "",
2140
+ value: numbers.value[rowIndex]
2141
+ }))
2142
+ });
2143
+ }
2144
+ return ok({
2145
+ kind: "GRAPH",
2146
+ series,
2147
+ chartType: conversion.chartType,
2148
+ valueFormat: conversion.valueFormat ?? "number",
2149
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency },
2150
+ xLabel: columnName(table, conversion.label)
2151
+ });
2152
+ }
2153
+ function barListToTable(list, conversion) {
2154
+ return ok({
2155
+ kind: "TABLE",
2156
+ columns: [
2157
+ { label: conversion.labelHeading ?? "Name" },
2158
+ { label: conversion.valueHeading ?? "Value", numeric: true }
2159
+ ],
2160
+ rows: list.items.map((item) => [
2161
+ { text: item.label },
2162
+ { text: item.display ?? String(item.value), value: item.value }
2163
+ ])
2164
+ });
2165
+ }
2166
+ function graphToTable(graph, context) {
2167
+ const first = graph.series[0];
2168
+ if (first === void 0)
2169
+ return err(`${context.where}: the chart has no series.`);
2170
+ const format = {
2171
+ ...graph.currency === void 0 ? {} : { currency: graph.currency },
2172
+ ...context.locale === void 0 ? {} : { locale: context.locale }
2173
+ };
2174
+ return ok({
2175
+ kind: "TABLE",
2176
+ columns: [
2177
+ { label: graph.xLabel ?? "Label" },
2178
+ ...graph.series.map((series) => ({ label: series.name, numeric: true }))
2179
+ ],
2180
+ rows: first.points.map((point, index) => [
2181
+ { text: point.label },
2182
+ ...graph.series.map((series) => {
2183
+ const value = series.points[index]?.value;
2184
+ return value === void 0 ? { text: "\u2014" } : { text: formatValue(value, graph.valueFormat, format), value };
2185
+ })
2186
+ ])
2187
+ });
2188
+ }
2189
+ function barListToGraph(list, conversion) {
2190
+ return ok({
2191
+ kind: "GRAPH",
2192
+ series: [
2193
+ {
2194
+ name: conversion.seriesName ?? "Value",
2195
+ points: list.items.map((item) => ({
2196
+ label: item.label,
2197
+ value: item.value
2198
+ }))
2199
+ }
2200
+ ],
2201
+ chartType: conversion.chartType,
2202
+ valueFormat: conversion.valueFormat ?? "number",
2203
+ ...conversion.currency === void 0 ? {} : { currency: conversion.currency }
2204
+ });
2205
+ }
2206
+ function convertView(widgetKey, view, data, options = {}) {
2207
+ const conversion = view.convert;
2208
+ const context = {
2209
+ where: `Widget ${JSON.stringify(widgetKey)} view ${JSON.stringify(view.key)}`,
2210
+ ...options.locale === void 0 ? {} : { locale: options.locale }
2211
+ };
2212
+ const expected = VIEW_CONVERSIONS[conversion.type]?.from;
2213
+ if (expected === void 0) {
2214
+ return err(
2215
+ `${context.where}: unknown conversion type ${JSON.stringify(conversion.type)}.`
2216
+ );
2217
+ }
2218
+ if (data.kind !== expected) {
2219
+ return err(
2220
+ `${context.where}: ${conversion.type} reads a ${expected} payload, but the widget returned ${data.kind}.`
2221
+ );
2222
+ }
2223
+ switch (conversion.type) {
2224
+ case "tableToBarList":
2225
+ return tableToBarList(data, conversion, context);
2226
+ case "tableToGraph":
2227
+ return tableToGraph(data, conversion, context);
2228
+ case "barListToTable":
2229
+ return barListToTable(data, conversion);
2230
+ case "graphToTable":
2231
+ return graphToTable(data, context);
2232
+ case "barListToGraph":
2233
+ return barListToGraph(data, conversion);
2234
+ }
2235
+ }
2236
+ function applyView(widget, chosen, options = {}) {
2237
+ const key = effectiveViewKey(widget.definition, chosen);
2238
+ const view = findView(widget.definition, key);
2239
+ if (view === void 0) return widget;
2240
+ const definition = {
2241
+ ...widget.definition,
2242
+ kind: view.kind
2243
+ };
2244
+ if (widget.status === "loading") return { ...widget, definition };
2245
+ if (widget.status === "empty") return { ...widget, definition };
2246
+ if (widget.status === "error") return { ...widget, definition };
2247
+ const converted = convertView(
2248
+ widget.definition.key,
2249
+ view,
2250
+ widget.data,
2251
+ options
2252
+ );
2253
+ if (!converted.ok) {
2254
+ return {
2255
+ definition,
2256
+ status: "error",
2257
+ error: converted.error,
2258
+ ...widget.options === void 0 ? {} : { options: widget.options }
2259
+ };
2260
+ }
2261
+ return { ...widget, definition, data: converted.value };
2262
+ }
2263
+
1975
2264
  // src/core/settings.ts
1976
2265
  var MAX_WIDGET_TITLE_LENGTH = 80;
1977
2266
  var MAX_COLUMN_LIST = 100;
@@ -1999,6 +2288,7 @@ function parseOne(raw) {
1999
2288
  const width = raw["width"];
2000
2289
  const flexible = raw["flexible"];
2001
2290
  const options = raw["options"];
2291
+ const view = raw["view"];
2002
2292
  const cleanTitle = typeof title === "string" ? title.trim().slice(0, MAX_WIDGET_TITLE_LENGTH) : "";
2003
2293
  const chosen = {};
2004
2294
  if (isRecord(options)) {
@@ -2011,7 +2301,8 @@ function parseOne(raw) {
2011
2301
  ...cleanTitle === "" ? {} : { title: cleanTitle },
2012
2302
  ...isWidgetWidth(width) ? { width } : {},
2013
2303
  ...typeof flexible === "boolean" ? { flexible } : {},
2014
- ...Object.keys(chosen).length === 0 ? {} : { options: chosen }
2304
+ ...Object.keys(chosen).length === 0 ? {} : { options: chosen },
2305
+ ...typeof view === "string" && view !== "" && view.length <= 80 ? { view } : {}
2015
2306
  };
2016
2307
  return Object.keys(settings).length === 0 ? void 0 : settings;
2017
2308
  }
@@ -2079,12 +2370,18 @@ function effectiveDefinitions(definitions, layout, options = {}) {
2079
2370
  }
2080
2371
  function sameSettings(a, b) {
2081
2372
  if (a === b) return true;
2082
- return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && sameOptionValues(a?.options ?? {}, b?.options ?? {});
2373
+ return a?.title === b?.title && a?.width === b?.width && a?.flexible === b?.flexible && a?.view === b?.view && sameOptionValues(a?.options ?? {}, b?.options ?? {});
2083
2374
  }
2084
2375
  function setWidgetSettings(layout, key, patch) {
2085
2376
  const before = layout.settings?.[key];
2086
2377
  const draft = { ...before };
2087
- for (const field of ["title", "width", "flexible", "options"]) {
2378
+ for (const field of [
2379
+ "title",
2380
+ "width",
2381
+ "flexible",
2382
+ "options",
2383
+ "view"
2384
+ ]) {
2088
2385
  if (!(field in patch)) continue;
2089
2386
  const value = patch[field];
2090
2387
  if (value === void 0) {
@@ -2117,6 +2414,62 @@ function isTitleInUse(definitions, layout, key, title) {
2117
2414
  );
2118
2415
  }
2119
2416
 
2417
+ // src/core/clones.ts
2418
+ function cloneNumber(key) {
2419
+ const match = /#(\d+)$/.exec(key);
2420
+ return match === null ? void 0 : Number(match[1]);
2421
+ }
2422
+ function cleanClones(definitions, clones, options = {}) {
2423
+ if (clones === void 0) return void 0;
2424
+ const byKey = new Map(definitions.map((d) => [d.key, d]));
2425
+ const seen = /* @__PURE__ */ new Set();
2426
+ const kept = [];
2427
+ for (const clone of clones) {
2428
+ if (seen.has(clone.key)) continue;
2429
+ const original = byKey.get(clone.from);
2430
+ if (original === void 0 || original.sourceKey !== void 0) continue;
2431
+ const existing = byKey.get(clone.key);
2432
+ if (existing !== void 0 && existing.sourceKey !== clone.from) continue;
2433
+ if (options.overrideLocks !== true && resolveWidgetLock(original).clone) {
2434
+ continue;
2435
+ }
2436
+ seen.add(clone.key);
2437
+ kept.push(clone);
2438
+ }
2439
+ if (kept.length === 0) return void 0;
2440
+ return kept.length === clones.length && kept.every((c, i) => c === clones[i]) ? clones : kept;
2441
+ }
2442
+ function defaultCloneTitle(original, clone, index) {
2443
+ const base = original.title ?? original.key;
2444
+ const suffix = ` (#${cloneNumber(clone.key) ?? index + 1})`;
2445
+ return `${base.slice(0, MAX_WIDGET_TITLE_LENGTH - suffix.length)}${suffix}`;
2446
+ }
2447
+ function withClones(definitions, layout, options = {}) {
2448
+ const valid = cleanClones(
2449
+ definitions,
2450
+ layout.clones,
2451
+ options
2452
+ );
2453
+ if (valid === void 0) return definitions;
2454
+ const present = new Set(definitions.map((d) => d.key));
2455
+ const toAdd = valid.filter((c) => !present.has(c.key));
2456
+ if (toAdd.length === 0) return definitions;
2457
+ const out = [];
2458
+ for (const definition of definitions) {
2459
+ out.push(definition);
2460
+ toAdd.forEach((clone, index) => {
2461
+ if (clone.from !== definition.key) return;
2462
+ const copy = { ...definition };
2463
+ copy.key = clone.key;
2464
+ copy.sourceKey = definition.key;
2465
+ copy.title = defaultCloneTitle(definition, clone, index);
2466
+ delete copy.locked;
2467
+ out.push(copy);
2468
+ });
2469
+ }
2470
+ return out;
2471
+ }
2472
+
2120
2473
  // src/core/layout.ts
2121
2474
  var MAX_PAGE_TITLE_LENGTH = 30;
2122
2475
  var MAX_PARSED_PAGES = 50;
@@ -2594,6 +2947,114 @@ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {
2594
2947
  }
2595
2948
  return ok(moved);
2596
2949
  }
2950
+
2951
+ // src/core/duplicate.ts
2952
+ function canDuplicate(definitions, key, options = {}) {
2953
+ const definition = definitions.find(
2954
+ (d) => d.key === key
2955
+ );
2956
+ if (definition === void 0) return false;
2957
+ if (options.overrideLocks === true) return true;
2958
+ const sourceKey = definition.sourceKey ?? definition.key;
2959
+ const original = definitions.find(
2960
+ (d) => d.key === sourceKey
2961
+ );
2962
+ return original !== void 0 && !resolveWidgetLock(original).clone;
2963
+ }
2964
+ function nextCloneKey(definitions, layout, fromKey) {
2965
+ const taken = /* @__PURE__ */ new Set([
2966
+ ...definitions.map((d) => d.key),
2967
+ ...(layout.clones ?? []).map((c) => c.key)
2968
+ ]);
2969
+ let highest = 0;
2970
+ for (const clone of layout.clones ?? []) {
2971
+ if (clone.from === fromKey) {
2972
+ highest = Math.max(highest, cloneNumber(clone.key) ?? 0);
2973
+ }
2974
+ }
2975
+ let n = highest + 1;
2976
+ while (taken.has(`${fromKey}#${n}`)) n += 1;
2977
+ return `${fromKey}#${n}`;
2978
+ }
2979
+ function duplicateWidget(definitions, layout, key, options = {}) {
2980
+ const widget = definitions.find((d) => d.key === key);
2981
+ if (widget === void 0) {
2982
+ return err(`Widget ${JSON.stringify(key)} isn't on this dashboard.`);
2983
+ }
2984
+ const originalKey = widget.sourceKey ?? widget.key;
2985
+ if (!canDuplicate(definitions, key, options)) {
2986
+ return err(
2987
+ `Widget ${JSON.stringify(originalKey)} is locked against duplicating.`
2988
+ );
2989
+ }
2990
+ const cloneKey = nextCloneKey(definitions, layout, originalKey);
2991
+ const copiedSettings = (() => {
2992
+ const mine = layout.settings?.[key];
2993
+ if (mine === void 0) return void 0;
2994
+ const { title: _title, ...rest } = mine;
2995
+ return Object.keys(rest).length === 0 ? void 0 : rest;
2996
+ })();
2997
+ const settings = copiedSettings === void 0 ? layout.settings : { ...layout.settings, [cloneKey]: copiedSettings };
2998
+ let next = {
2999
+ ...layout,
3000
+ clones: [...layout.clones ?? [], { key: cloneKey, from: originalKey }],
3001
+ ...settings === void 0 ? {} : { settings }
3002
+ };
3003
+ const all = withClones(definitions, next, options);
3004
+ const assigned = assignPages(all, next);
3005
+ const pageKey = [...assigned].find(
3006
+ ([, widgets]) => widgets.some((w) => w.key === key)
3007
+ )?.[0];
3008
+ if (pageKey !== void 0) {
3009
+ const view = pageLayout(next, pageKey);
3010
+ const onPage = assigned.get(pageKey) ?? [];
3011
+ if (view !== void 0) {
3012
+ const shown = visibleWidgets(onPage, view).filter((d) => !resolveWidgetLock(d).move && d.key !== cloneKey).map((d) => d.key);
3013
+ const at = shown.indexOf(key);
3014
+ const order = at === -1 ? [...shown, cloneKey] : [...shown.slice(0, at + 1), cloneKey, ...shown.slice(at + 1)];
3015
+ next = withPageLayout(next, pageKey, { ...view, order });
3016
+ }
3017
+ }
3018
+ if (hasPages(next) && pageKey !== void 0) {
3019
+ const effective = effectiveDefinitions(all, next, options);
3020
+ const room = pageRoom(effective, next, pageKey, options);
3021
+ if (room !== void 0 && room.rowsUsed > room.maxRows) {
3022
+ const page = next.pages?.find(
3023
+ (p) => p.key === pageKey
3024
+ );
3025
+ const width = effective.find(
3026
+ (d) => d.key === cloneKey
3027
+ )?.width;
3028
+ return err(
3029
+ `Page "${page?.title ?? pageKey}" has no room for a widget${width === void 0 ? "" : ` ${width} wide`} (it holds ${room.maxRows} rows).`
3030
+ );
3031
+ }
3032
+ }
3033
+ return ok({ layout: next, key: cloneKey });
3034
+ }
3035
+ function deleteClone(layout, key) {
3036
+ if (layout.clones?.some((clone) => clone.key === key) !== true) return layout;
3037
+ const without2 = (keys) => keys.includes(key) ? keys.filter((k) => k !== key) : keys;
3038
+ const { clones, settings, ...rest } = layout;
3039
+ const remaining = (clones ?? []).filter((c) => c.key !== key);
3040
+ const { [key]: _gone, ...otherSettings } = settings ?? {};
3041
+ return {
3042
+ ...rest,
3043
+ order: without2(layout.order),
3044
+ hidden: without2(layout.hidden),
3045
+ minimized: without2(layout.minimized),
3046
+ ...layout.pages === void 0 ? {} : {
3047
+ pages: layout.pages.map((page) => ({
3048
+ ...page,
3049
+ order: without2(page.order),
3050
+ hidden: without2(page.hidden),
3051
+ minimized: without2(page.minimized)
3052
+ }))
3053
+ },
3054
+ ...Object.keys(otherSettings).length === 0 ? {} : { settings: otherSettings },
3055
+ ...remaining.length === 0 ? {} : { clones: remaining }
3056
+ };
3057
+ }
2597
3058
  function InlineConfirm({
2598
3059
  message,
2599
3060
  onAnswer
@@ -2788,7 +3249,8 @@ function draftFrom(settings) {
2788
3249
  title: settings?.title ?? "",
2789
3250
  width: settings?.width,
2790
3251
  flexible: settings?.flexible,
2791
- options: settings?.options ?? {}
3252
+ options: settings?.options ?? {},
3253
+ view: settings?.view
2792
3254
  };
2793
3255
  }
2794
3256
  function storedDefault(option) {
@@ -2813,7 +3275,9 @@ function patchFrom(definition, draft) {
2813
3275
  title: title === "" ? void 0 : title,
2814
3276
  width: draft.width,
2815
3277
  flexible: draft.flexible === void 0 || draft.flexible === isFlexible(definition) ? void 0 : draft.flexible,
2816
- options: Object.keys(options).length === 0 ? void 0 : options
3278
+ options: Object.keys(options).length === 0 ? void 0 : options,
3279
+ // Choosing the author's own default view again clears the setting.
3280
+ view: draft.view === void 0 || draft.view === effectiveViewKey(definition, void 0) ? void 0 : draft.view
2817
3281
  };
2818
3282
  }
2819
3283
  function patchKey(patch) {
@@ -2821,19 +3285,28 @@ function patchKey(patch) {
2821
3285
  for (const key of Object.keys(patch.options ?? {}).sort()) {
2822
3286
  options[key] = patch.options[key];
2823
3287
  }
2824
- return JSON.stringify([patch.title, patch.width, patch.flexible, options]);
3288
+ return JSON.stringify([
3289
+ patch.title,
3290
+ patch.width,
3291
+ patch.flexible,
3292
+ options,
3293
+ patch.view
3294
+ ]);
2825
3295
  }
2826
3296
  var EMPTY_PATCH = {
2827
3297
  title: void 0,
2828
3298
  width: void 0,
2829
3299
  flexible: void 0,
2830
- options: void 0
3300
+ options: void 0,
3301
+ view: void 0
2831
3302
  };
2832
3303
  function WidgetOptionsDialog({
2833
3304
  definition,
2834
3305
  settings,
2835
3306
  baseline,
2836
3307
  isTitleInUse: isTitleInUse2,
3308
+ sourceTitle,
3309
+ onDelete,
2837
3310
  onApply,
2838
3311
  onClose
2839
3312
  }) {
@@ -2846,6 +3319,7 @@ function WidgetOptionsDialog({
2846
3319
  const [confirming, setConfirming] = useState(
2847
3320
  void 0
2848
3321
  );
3322
+ const autoTitle = useRef(void 0);
2849
3323
  useEffect(() => {
2850
3324
  const dialog = dialogRef.current;
2851
3325
  if (dialog === null) return void 0;
@@ -2865,6 +3339,7 @@ function WidgetOptionsDialog({
2865
3339
  const canRevert = baselineKey !== void 0 && baselineKey !== savedKey;
2866
3340
  const canReset = savedKey !== patchKey(EMPTY_PATCH);
2867
3341
  const range = widthRange(definition);
3342
+ const views = viewChoices(definition);
2868
3343
  const effectiveFlexible = draft.flexible ?? isFlexible(definition);
2869
3344
  function requestClose() {
2870
3345
  if (confirming !== void 0) {
@@ -2887,6 +3362,9 @@ function WidgetOptionsDialog({
2887
3362
  } else if (which === "reset") {
2888
3363
  onApply(EMPTY_PATCH);
2889
3364
  onClose();
3365
+ } else if (which === "delete") {
3366
+ onDelete?.();
3367
+ onClose();
2890
3368
  } else if (baseline !== void 0) {
2891
3369
  onApply(patchFrom(definition, draftFrom(baseline.settings)));
2892
3370
  onClose();
@@ -2902,7 +3380,8 @@ function WidgetOptionsDialog({
2902
3380
  apply: labels.confirmApply,
2903
3381
  discard: labels.confirmDiscard,
2904
3382
  reset: labels.confirmResetOptions,
2905
- revert: labels.confirmRevertOptions
3383
+ revert: labels.confirmRevertOptions,
3384
+ delete: labels.confirmDelete
2906
3385
  };
2907
3386
  const widths = [];
2908
3387
  for (let width = range.min; width <= range.max; width += 1)
@@ -2990,6 +3469,46 @@ function WidgetOptionsDialog({
2990
3469
  ),
2991
3470
  labels.optionsFlexible
2992
3471
  ] }),
3472
+ views.length > 0 && /* @__PURE__ */ jsxs("div", { className: "dwt-options-field", children: [
3473
+ /* @__PURE__ */ jsx("label", { htmlFor: `${baseId}-view`, children: labels.optionsViewField }),
3474
+ /* @__PURE__ */ jsx(
3475
+ "select",
3476
+ {
3477
+ id: `${baseId}-view`,
3478
+ value: effectiveViewKey(definition, draft.view),
3479
+ onChange: (event) => {
3480
+ const value = event.target.value;
3481
+ setDraft((current) => {
3482
+ const next = { ...current, view: value };
3483
+ if (sourceTitle === void 0 || current.title.trim() !== "" && current.title !== autoTitle.current) {
3484
+ return next;
3485
+ }
3486
+ const label = views.find(
3487
+ (view) => view.key === value
3488
+ )?.label;
3489
+ const startView = effectiveViewKey(
3490
+ definition,
3491
+ settings?.view
3492
+ );
3493
+ if (label === void 0 || value === startView) {
3494
+ autoTitle.current = void 0;
3495
+ return { ...next, title: "" };
3496
+ }
3497
+ let suggestion = labels.suggestedViewTitle(
3498
+ sourceTitle,
3499
+ label
3500
+ );
3501
+ for (let n = 2; isTitleInUse2?.(suggestion) === true && n < 50; n += 1) {
3502
+ suggestion = `${labels.suggestedViewTitle(sourceTitle, label)} (#${n})`;
3503
+ }
3504
+ autoTitle.current = suggestion;
3505
+ return { ...next, title: suggestion };
3506
+ });
3507
+ },
3508
+ 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))
3509
+ }
3510
+ )
3511
+ ] }),
2993
3512
  (definition.options?.length ?? 0) > 0 && /* @__PURE__ */ jsxs("fieldset", { className: "dwt-options-group", children: [
2994
3513
  /* @__PURE__ */ jsx("legend", { children: labels.optionsDeclared }),
2995
3514
  (definition.options ?? []).map((option) => /* @__PURE__ */ jsx(
@@ -3041,6 +3560,15 @@ function WidgetOptionsDialog({
3041
3560
  onClick: () => setConfirming("revert"),
3042
3561
  children: labels.revertToBeforeEditing
3043
3562
  }
3563
+ ),
3564
+ onDelete !== void 0 && /* @__PURE__ */ jsx(
3565
+ "button",
3566
+ {
3567
+ type: "button",
3568
+ className: slot("button", "dwt-button"),
3569
+ onClick: () => setConfirming("delete"),
3570
+ children: labels.optionsDelete
3571
+ }
3044
3572
  )
3045
3573
  ] }) : /* @__PURE__ */ jsx(
3046
3574
  InlineConfirm,
@@ -3969,10 +4497,13 @@ function DashboardInner({
3969
4497
  className
3970
4498
  }) {
3971
4499
  const slot = useSlotClassName();
3972
- const { labels } = useWidgetSettings();
4500
+ const { labels, locale } = useWidgetSettings();
3973
4501
  const effectiveWidgets = widgets.map((w) => {
3974
- const settings = savedLayout.settings?.[w.definition.key];
3975
- return settings === void 0 || !overrideLocks && isOptionsLocked(w.definition) ? w : { ...w, definition: applySettings(w.definition, settings) };
4502
+ const settings = !overrideLocks && isOptionsLocked(w.definition) ? void 0 : savedLayout.settings?.[w.definition.key];
4503
+ const styled = settings === void 0 ? w : { ...w, definition: applySettings(w.definition, settings) };
4504
+ return applyView(styled, settings?.view, {
4505
+ ...locale === void 0 ? {} : { locale }
4506
+ });
3976
4507
  });
3977
4508
  const definitions = effectiveWidgets.map(
3978
4509
  (w) => overrideLocks ? withoutLock(w.definition) : w.definition
@@ -4042,6 +4573,29 @@ function DashboardInner({
4042
4573
  setPageError(void 0);
4043
4574
  change(withPageLayout(layout, activeKey, next));
4044
4575
  }
4576
+ function duplicateItems(definition, key, title) {
4577
+ const sources = [...authorDefinitions.values()];
4578
+ if (!canDuplicate(sources, key, { overrideLocks })) return [];
4579
+ const trial = duplicateWidget(sources, layout, key, {
4580
+ overrideLocks,
4581
+ ...roomOptions
4582
+ });
4583
+ return [
4584
+ {
4585
+ kind: "action",
4586
+ key: "duplicate",
4587
+ label: labels.menuDuplicate,
4588
+ ariaLabel: trial.ok ? labels.duplicate(title) : labels.duplicateBlocked(title, trial.error),
4589
+ disabled: !trial.ok,
4590
+ onSelect: () => {
4591
+ if (!trial.ok) return;
4592
+ change(trial.value.layout);
4593
+ setOptionsKey(trial.value.key);
4594
+ setAnnouncement(labels.duplicated(title));
4595
+ }
4596
+ }
4597
+ ];
4598
+ }
4045
4599
  function selectPage(pageKey) {
4046
4600
  const index = pages.findIndex((page) => page.key === pageKey);
4047
4601
  if (index === -1) return;
@@ -4171,10 +4725,16 @@ function DashboardInner({
4171
4725
  definitions,
4172
4726
  defaultLayout ?? EMPTY_LAYOUT
4173
4727
  );
4174
- const { settings: _defaultSettings, ...arrangement } = arranged;
4175
- change(
4176
- layout.settings === void 0 ? arrangement : { ...arrangement, settings: layout.settings }
4177
- );
4728
+ const {
4729
+ settings: _defaultSettings,
4730
+ clones: _defaultClones,
4731
+ ...arrangement
4732
+ } = arranged;
4733
+ change({
4734
+ ...arrangement,
4735
+ ...layout.settings === void 0 ? {} : { settings: layout.settings },
4736
+ ...layout.clones === void 0 ? {} : { clones: layout.clones }
4737
+ });
4178
4738
  } else if (yes && which === "revert" && snapshot !== void 0) {
4179
4739
  change(snapshot);
4180
4740
  }
@@ -4480,6 +5040,7 @@ function DashboardInner({
4480
5040
  onSelect: () => setOptionsKey(key)
4481
5041
  }
4482
5042
  ],
5043
+ ...duplicateItems(definition, key, title),
4483
5044
  ...pageTargets,
4484
5045
  ...lock.hide ? [] : [
4485
5046
  {
@@ -4574,6 +5135,21 @@ function DashboardInner({
4574
5135
  definition: authorDefinitions.get(optionsKey),
4575
5136
  settings: layout.settings?.[optionsKey],
4576
5137
  ...snapshot === void 0 ? {} : { baseline: { settings: snapshot.settings?.[optionsKey] } },
5138
+ ...authorDefinitions.get(optionsKey)?.sourceKey === void 0 ? {} : {
5139
+ sourceTitle: effectiveWidgets.find(
5140
+ (w) => w.definition.key === authorDefinitions.get(optionsKey)?.sourceKey
5141
+ )?.definition.title ?? authorDefinitions.get(optionsKey)?.sourceKey ?? "",
5142
+ onDelete: () => {
5143
+ change(deleteClone(layout, optionsKey));
5144
+ setAnnouncement(
5145
+ labels.deleted(
5146
+ effectiveWidgets.find(
5147
+ (w) => w.definition.key === optionsKey
5148
+ )?.definition.title ?? optionsKey
5149
+ )
5150
+ );
5151
+ }
5152
+ },
4577
5153
  isTitleInUse: (title) => isTitleInUse(
4578
5154
  widgets.map((w) => w.definition),
4579
5155
  layout,
@@ -4817,45 +5393,6 @@ function useStoredLayout(options) {
4817
5393
  };
4818
5394
  }
4819
5395
 
4820
- // src/core/payload.ts
4821
- var WIDGET_KINDS = [
4822
- "TEXT",
4823
- "KPI",
4824
- "GAUGE",
4825
- "TABLE",
4826
- "BAR_LIST",
4827
- "ALERT_LIST",
4828
- "GRAPH"
4829
- ];
4830
- function isWidgetKind(value) {
4831
- return typeof value === "string" && WIDGET_KINDS.includes(value);
4832
- }
4833
- var VALUE_FORMATS = ["number", "currency", "percent"];
4834
- var MAX_GRAPH_SERIES = 8;
4835
- var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
4836
- function isWidgetEmptyState(payload) {
4837
- return "empty" in payload && payload.empty === true;
4838
- }
4839
- function emptyTextFor(payload) {
4840
- if (isWidgetEmptyState(payload)) {
4841
- return payload.text;
4842
- }
4843
- switch (payload.kind) {
4844
- case "TABLE":
4845
- return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
4846
- case "BAR_LIST":
4847
- return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
4848
- case "ALERT_LIST":
4849
- return payload.items.length === 0 ? payload.emptyText : null;
4850
- case "GRAPH":
4851
- return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
4852
- case "TEXT":
4853
- case "KPI":
4854
- case "GAUGE":
4855
- return null;
4856
- }
4857
- }
4858
-
4859
5396
  // src/core/validate.ts
4860
5397
  var MAX_REPORTED_PROBLEMS = 10;
4861
5398
  function describeType2(value) {
@@ -5160,7 +5697,7 @@ function loadingWidgets(definitions) {
5160
5697
  return definitions.map((definition) => ({ definition, status: "loading" }));
5161
5698
  }
5162
5699
  function cacheKeyFor(definition, options, values) {
5163
- const base = options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
5700
+ const base = options.cacheKey === void 0 ? definition.sourceKey ?? definition.key : options.cacheKey(definition);
5164
5701
  return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
5165
5702
  }
5166
5703
  function optionValuesFor(definition, options) {
@@ -5334,7 +5871,11 @@ function createWidgetLoader(options) {
5334
5871
  }
5335
5872
  }
5336
5873
  if (!isCurrent()) return;
5337
- const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
5874
+ const providerKey = definition.sourceKey ?? key;
5875
+ const provider = Object.prototype.hasOwnProperty.call(
5876
+ providers,
5877
+ providerKey
5878
+ ) ? providers[providerKey] : void 0;
5338
5879
  const result = await resolveWidgetUnapplied(definition, provider, context, {
5339
5880
  ...runOptions,
5340
5881
  signal: controller.signal