@richardmcquiston01/dashboard-widgets-toolkit 0.15.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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactNode, ReactElement, RefObject } from 'react';
2
- import { D as DashboardWidget, W as WidgetSize, a as WidgetFill, G as GraphWidgetData, N as NiceDomain, b as WidgetDefinition, c as DetailProviderOptions, d as DetailData, e as DetailQuery, O as OptionValues, L as Locale, f as DashboardLayout, g as WidgetViewerSettings, h as WidgetSettingsPatch, A as AlertListWidgetData, B as BarListWidgetData, i as GaugeWidgetData, K as KpiWidgetData, T as TableWidgetData, R as ResolvedTableControls, j as DetailSort, k as TextWidgetData, l as WidgetData, m as LayoutPersistence, S as StoreScope, P as PageItem, n as NormalizeOptions, o as ResolveOptions, p as WidgetContext, q as WidgetProviders } from './storage-DwJ-vGJg.cjs';
2
+ import { D as DashboardWidget, W as WidgetSize, a as WidgetFill, G as GraphWidgetData, N as NiceDomain, b as WidgetDefinition, c as DetailProviderOptions, d as DetailData, e as DetailQuery, O as OptionValues, L as Locale, f as DashboardLayout, g as WidgetViewerSettings, h as WidgetSettingsPatch, A as AlertListWidgetData, B as BarListWidgetData, i as GaugeWidgetData, K as KpiWidgetData, T as TableWidgetData, R as ResolvedTableControls, j as DetailSort, k as TextWidgetData, l as WidgetData, m as LayoutPersistence, S as StoreScope, P as PageItem, n as NormalizeOptions, o as ResolveOptions, p as WidgetContext, q as WidgetProviders } from './storage-oivwB7al.cjs';
3
3
 
4
4
  interface WidgetCardProps {
5
5
  readonly title: string;
@@ -216,6 +216,18 @@ interface DashboardLabels {
216
216
  readonly menuHide: string;
217
217
  /** The card menu's item that opens the Options dialog. */
218
218
  readonly menuOptions: string;
219
+ /** Arrange menu item that makes a clone of the widget. */
220
+ readonly menuDuplicate: string;
221
+ readonly duplicate: (title: string) => string;
222
+ /** Accessible name of Duplicate when the page has no room; `reason` says why. */
223
+ readonly duplicateBlocked: (title: string, reason: string) => string;
224
+ readonly duplicated: (title: string) => string;
225
+ /** Options dialog button that deletes a clone. */
226
+ readonly optionsDelete: string;
227
+ readonly confirmDelete: string;
228
+ readonly deleted: (title: string) => string;
229
+ /** The title suggested for a clone shown with another view. */
230
+ readonly suggestedViewTitle: (title: string, viewLabel: string) => string;
219
231
  /** The Options dialog: heading, fields and buttons. */
220
232
  readonly optionsFor: (title: string) => string;
221
233
  readonly optionsTitleField: string;
@@ -393,11 +405,18 @@ interface WidgetOptionsDialogProps {
393
405
  };
394
406
  /** Whether another widget already shows this title; the dialog refuses it. */
395
407
  readonly isTitleInUse?: (title: string) => boolean;
408
+ /**
409
+ * For a clone: its original's title, so choosing another view can suggest
410
+ * a title like "Recent Orders Line Chart".
411
+ */
412
+ readonly sourceTitle?: string;
413
+ /** For a clone: shows "Delete this widget"; called after the viewer confirms. */
414
+ readonly onDelete?: () => void;
396
415
  /** Called after the viewer confirms Apply, Reset or Revert. */
397
416
  readonly onApply: (patch: WidgetSettingsPatch) => void;
398
417
  readonly onClose: () => void;
399
418
  }
400
- declare function WidgetOptionsDialog({ definition, settings, baseline, isTitleInUse, onApply, onClose, }: WidgetOptionsDialogProps): ReactElement;
419
+ declare function WidgetOptionsDialog({ definition, settings, baseline, isTitleInUse, sourceTitle, onDelete, onApply, onClose, }: WidgetOptionsDialogProps): ReactElement;
401
420
 
402
421
  /**
403
422
  * Default colors, used as `var(--dwt-*, fallback)` so charts read correctly
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactNode, ReactElement, RefObject } from 'react';
2
- import { D as DashboardWidget, W as WidgetSize, a as WidgetFill, G as GraphWidgetData, N as NiceDomain, b as WidgetDefinition, c as DetailProviderOptions, d as DetailData, e as DetailQuery, O as OptionValues, L as Locale, f as DashboardLayout, g as WidgetViewerSettings, h as WidgetSettingsPatch, A as AlertListWidgetData, B as BarListWidgetData, i as GaugeWidgetData, K as KpiWidgetData, T as TableWidgetData, R as ResolvedTableControls, j as DetailSort, k as TextWidgetData, l as WidgetData, m as LayoutPersistence, S as StoreScope, P as PageItem, n as NormalizeOptions, o as ResolveOptions, p as WidgetContext, q as WidgetProviders } from './storage-DwJ-vGJg.js';
2
+ import { D as DashboardWidget, W as WidgetSize, a as WidgetFill, G as GraphWidgetData, N as NiceDomain, b as WidgetDefinition, c as DetailProviderOptions, d as DetailData, e as DetailQuery, O as OptionValues, L as Locale, f as DashboardLayout, g as WidgetViewerSettings, h as WidgetSettingsPatch, A as AlertListWidgetData, B as BarListWidgetData, i as GaugeWidgetData, K as KpiWidgetData, T as TableWidgetData, R as ResolvedTableControls, j as DetailSort, k as TextWidgetData, l as WidgetData, m as LayoutPersistence, S as StoreScope, P as PageItem, n as NormalizeOptions, o as ResolveOptions, p as WidgetContext, q as WidgetProviders } from './storage-oivwB7al.js';
3
3
 
4
4
  interface WidgetCardProps {
5
5
  readonly title: string;
@@ -216,6 +216,18 @@ interface DashboardLabels {
216
216
  readonly menuHide: string;
217
217
  /** The card menu's item that opens the Options dialog. */
218
218
  readonly menuOptions: string;
219
+ /** Arrange menu item that makes a clone of the widget. */
220
+ readonly menuDuplicate: string;
221
+ readonly duplicate: (title: string) => string;
222
+ /** Accessible name of Duplicate when the page has no room; `reason` says why. */
223
+ readonly duplicateBlocked: (title: string, reason: string) => string;
224
+ readonly duplicated: (title: string) => string;
225
+ /** Options dialog button that deletes a clone. */
226
+ readonly optionsDelete: string;
227
+ readonly confirmDelete: string;
228
+ readonly deleted: (title: string) => string;
229
+ /** The title suggested for a clone shown with another view. */
230
+ readonly suggestedViewTitle: (title: string, viewLabel: string) => string;
219
231
  /** The Options dialog: heading, fields and buttons. */
220
232
  readonly optionsFor: (title: string) => string;
221
233
  readonly optionsTitleField: string;
@@ -393,11 +405,18 @@ interface WidgetOptionsDialogProps {
393
405
  };
394
406
  /** Whether another widget already shows this title; the dialog refuses it. */
395
407
  readonly isTitleInUse?: (title: string) => boolean;
408
+ /**
409
+ * For a clone: its original's title, so choosing another view can suggest
410
+ * a title like "Recent Orders Line Chart".
411
+ */
412
+ readonly sourceTitle?: string;
413
+ /** For a clone: shows "Delete this widget"; called after the viewer confirms. */
414
+ readonly onDelete?: () => void;
396
415
  /** Called after the viewer confirms Apply, Reset or Revert. */
397
416
  readonly onApply: (patch: WidgetSettingsPatch) => void;
398
417
  readonly onClose: () => void;
399
418
  }
400
- declare function WidgetOptionsDialog({ definition, settings, baseline, isTitleInUse, onApply, onClose, }: WidgetOptionsDialogProps): ReactElement;
419
+ declare function WidgetOptionsDialog({ definition, settings, baseline, isTitleInUse, sourceTitle, onDelete, onApply, onClose, }: WidgetOptionsDialogProps): ReactElement;
401
420
 
402
421
  /**
403
422
  * Default colors, used as `var(--dwt-*, fallback)` so charts read correctly
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.",
@@ -1937,13 +1945,15 @@ var NOT_LOCKED = Object.freeze({
1937
1945
  move: false,
1938
1946
  hide: false,
1939
1947
  minimize: false,
1940
- options: false
1948
+ options: false,
1949
+ clone: false
1941
1950
  });
1942
1951
  var FULLY_LOCKED = Object.freeze({
1943
1952
  move: true,
1944
1953
  hide: true,
1945
1954
  minimize: true,
1946
- options: true
1955
+ options: true,
1956
+ clone: true
1947
1957
  });
1948
1958
  function resolveWidgetLock(definition) {
1949
1959
  const setting = definition.locked;
@@ -1953,7 +1963,8 @@ function resolveWidgetLock(definition) {
1953
1963
  move: setting.move === true,
1954
1964
  hide: setting.hide === true,
1955
1965
  minimize: setting.minimize === true,
1956
- options: setting.options === true
1966
+ options: setting.options === true,
1967
+ clone: setting.clone === true
1957
1968
  };
1958
1969
  }
1959
1970
  function sortDefinitions(definitions) {
@@ -2403,6 +2414,62 @@ function isTitleInUse(definitions, layout, key, title) {
2403
2414
  );
2404
2415
  }
2405
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
+
2406
2473
  // src/core/layout.ts
2407
2474
  var MAX_PAGE_TITLE_LENGTH = 30;
2408
2475
  var MAX_PARSED_PAGES = 50;
@@ -2880,6 +2947,114 @@ function moveWidgetToPage(definitions, layout, widgetKey, targetKey, options = {
2880
2947
  }
2881
2948
  return ok(moved);
2882
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
+ }
2883
3058
  function InlineConfirm({
2884
3059
  message,
2885
3060
  onAnswer
@@ -3130,6 +3305,8 @@ function WidgetOptionsDialog({
3130
3305
  settings,
3131
3306
  baseline,
3132
3307
  isTitleInUse: isTitleInUse2,
3308
+ sourceTitle,
3309
+ onDelete,
3133
3310
  onApply,
3134
3311
  onClose
3135
3312
  }) {
@@ -3142,6 +3319,7 @@ function WidgetOptionsDialog({
3142
3319
  const [confirming, setConfirming] = useState(
3143
3320
  void 0
3144
3321
  );
3322
+ const autoTitle = useRef(void 0);
3145
3323
  useEffect(() => {
3146
3324
  const dialog = dialogRef.current;
3147
3325
  if (dialog === null) return void 0;
@@ -3184,6 +3362,9 @@ function WidgetOptionsDialog({
3184
3362
  } else if (which === "reset") {
3185
3363
  onApply(EMPTY_PATCH);
3186
3364
  onClose();
3365
+ } else if (which === "delete") {
3366
+ onDelete?.();
3367
+ onClose();
3187
3368
  } else if (baseline !== void 0) {
3188
3369
  onApply(patchFrom(definition, draftFrom(baseline.settings)));
3189
3370
  onClose();
@@ -3199,7 +3380,8 @@ function WidgetOptionsDialog({
3199
3380
  apply: labels.confirmApply,
3200
3381
  discard: labels.confirmDiscard,
3201
3382
  reset: labels.confirmResetOptions,
3202
- revert: labels.confirmRevertOptions
3383
+ revert: labels.confirmRevertOptions,
3384
+ delete: labels.confirmDelete
3203
3385
  };
3204
3386
  const widths = [];
3205
3387
  for (let width = range.min; width <= range.max; width += 1)
@@ -3296,7 +3478,32 @@ function WidgetOptionsDialog({
3296
3478
  value: effectiveViewKey(definition, draft.view),
3297
3479
  onChange: (event) => {
3298
3480
  const value = event.target.value;
3299
- setDraft((current) => ({ ...current, view: 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
+ });
3300
3507
  },
3301
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))
3302
3509
  }
@@ -3353,6 +3560,15 @@ function WidgetOptionsDialog({
3353
3560
  onClick: () => setConfirming("revert"),
3354
3561
  children: labels.revertToBeforeEditing
3355
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
+ }
3356
3572
  )
3357
3573
  ] }) : /* @__PURE__ */ jsx(
3358
3574
  InlineConfirm,
@@ -4357,6 +4573,29 @@ function DashboardInner({
4357
4573
  setPageError(void 0);
4358
4574
  change(withPageLayout(layout, activeKey, next));
4359
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
+ }
4360
4599
  function selectPage(pageKey) {
4361
4600
  const index = pages.findIndex((page) => page.key === pageKey);
4362
4601
  if (index === -1) return;
@@ -4486,10 +4725,16 @@ function DashboardInner({
4486
4725
  definitions,
4487
4726
  defaultLayout ?? EMPTY_LAYOUT
4488
4727
  );
4489
- const { settings: _defaultSettings, ...arrangement } = arranged;
4490
- change(
4491
- layout.settings === void 0 ? arrangement : { ...arrangement, settings: layout.settings }
4492
- );
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
+ });
4493
4738
  } else if (yes && which === "revert" && snapshot !== void 0) {
4494
4739
  change(snapshot);
4495
4740
  }
@@ -4795,6 +5040,7 @@ function DashboardInner({
4795
5040
  onSelect: () => setOptionsKey(key)
4796
5041
  }
4797
5042
  ],
5043
+ ...duplicateItems(definition, key, title),
4798
5044
  ...pageTargets,
4799
5045
  ...lock.hide ? [] : [
4800
5046
  {
@@ -4889,6 +5135,21 @@ function DashboardInner({
4889
5135
  definition: authorDefinitions.get(optionsKey),
4890
5136
  settings: layout.settings?.[optionsKey],
4891
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
+ },
4892
5153
  isTitleInUse: (title) => isTitleInUse(
4893
5154
  widgets.map((w) => w.definition),
4894
5155
  layout,
@@ -5436,7 +5697,7 @@ function loadingWidgets(definitions) {
5436
5697
  return definitions.map((definition) => ({ definition, status: "loading" }));
5437
5698
  }
5438
5699
  function cacheKeyFor(definition, options, values) {
5439
- 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);
5440
5701
  return values === void 0 ? base : `${base}${optionsCacheSuffix(definition, values, options)}`;
5441
5702
  }
5442
5703
  function optionValuesFor(definition, options) {
@@ -5610,7 +5871,11 @@ function createWidgetLoader(options) {
5610
5871
  }
5611
5872
  }
5612
5873
  if (!isCurrent()) return;
5613
- 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;
5614
5879
  const result = await resolveWidgetUnapplied(definition, provider, context, {
5615
5880
  ...runOptions,
5616
5881
  signal: controller.signal