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