@burdenoff/microfe-workspaces 2026.830.2 → 2026.917.1

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.
@@ -1,23 +1,24 @@
1
1
  import { tWithFallback as e } from "../../utils/i18n.js";
2
2
  import { DEFAULT_DASHBOARD_WIDGETS as t, DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT as n, WIDGET_DEFINITIONS as r, isWidgetAvailable as i, resolveWidgetCopy as a } from "./config.js";
3
3
  import { nativeImpact as o } from "../../utils/nativeBridge.js";
4
- import { useCallback as s, useEffect as c, useState as l } from "react";
5
- import { ArrowDown as u, ArrowUp as d, GripVertical as f, Loader2 as p, RotateCcw as m, Save as h } from "lucide-react";
6
- import { jsx as g, jsxs as _ } from "react/jsx-runtime";
7
- import { Badge as v, Button as y, Separator as b, Sheet as x, SheetContent as S, SheetDescription as C, SheetFooter as w, SheetHeader as T, SheetTitle as E, Switch as D } from "@burdenoff/fe-libs/ui";
8
- import { useI18n as O } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
4
+ import { dropPositionFromPointer as s, reorderByDropIndex as c } from "./reorder.js";
5
+ import { useCallback as l, useEffect as u, useRef as d, useState as f } from "react";
6
+ import { ArrowDown as p, ArrowUp as m, GripVertical as h, Loader2 as g, RotateCcw as _, Save as v } from "lucide-react";
7
+ import { jsx as y, jsxs as b } from "react/jsx-runtime";
8
+ import { Badge as x, Button as S, Separator as C, Sheet as w, SheetContent as T, SheetDescription as E, SheetFooter as D, SheetHeader as O, SheetTitle as ee, Switch as te } from "@burdenoff/fe-libs/ui";
9
+ import { useI18n as ne } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
9
10
  //#region src/pages/dashboard/DashboardCustomizer.tsx
10
11
  function k({ open: k, onOpenChange: A, widgets: j, onSave: M, isSaving: N, productId: P = "workspaces" }) {
11
- let { t: F } = O(), I = (t, n) => e(F, t, n), [L, R] = l(j), [z, B] = l(!1);
12
- c(() => {
12
+ let { t: F } = ne(), I = l((t, n) => e(F, t, n), [F]), [L, R] = f(j), [z, B] = f(!1), [V, H] = f(null), [U, W] = f(null), [G, K] = f(""), q = d(/* @__PURE__ */ new Map()), J = d(null), Y = d(0);
13
+ u(() => {
13
14
  k && (R([...j]), B(!1));
14
15
  }, [k, j]);
15
- let V = s((e) => {
16
+ let X = l((e) => {
16
17
  R((t) => t.map((t) => t.widgetId === e ? {
17
18
  ...t,
18
19
  enabled: !t.enabled
19
20
  } : t)), B(!0);
20
- }, []), H = s((e, t) => {
21
+ }, []), Z = l((e, t) => {
21
22
  R((n) => {
22
23
  let r = [...n].sort((e, t) => e.order - t.order), i = r.findIndex((t) => t.widgetId === e);
23
24
  if (i < 0) return n;
@@ -32,96 +33,155 @@ function k({ open: k, onOpenChange: A, widgets: j, onSave: M, isSaving: N, produ
32
33
  order: o
33
34
  }, r;
34
35
  }), B(!0);
35
- }, []), U = s(() => {
36
+ }, []), re = l(() => {
36
37
  R([...n[P] ?? t]), B(!0);
37
- }, [P]), W = s(async () => {
38
+ }, [P]), ie = l((e) => (t) => {
39
+ t ? q.current.set(e, t) : q.current.delete(e);
40
+ }, []), ae = l((e, t) => {
41
+ H(e), J.current = null, cancelAnimationFrame(Y.current), t.dataTransfer.effectAllowed = "move", t.dataTransfer.setData("text/plain", e);
42
+ }, []), oe = l((e, t) => {
43
+ !V || V === e || (t.preventDefault(), t.dataTransfer.dropEffect = "move", W(e), cancelAnimationFrame(Y.current), Y.current = requestAnimationFrame(() => {
44
+ let n = q.current.get(e);
45
+ if (!n) return;
46
+ let r = s(t.clientY, n.getBoundingClientRect()), i = J.current;
47
+ i && i.fromId === V && i.toId === e && i.position === r || (J.current = {
48
+ fromId: V,
49
+ toId: e,
50
+ position: r
51
+ }, R((t) => c(t, V, e, r)), B(!0));
52
+ }));
53
+ }, [V]), Q = l((e, t) => {
54
+ t.currentTarget.contains(t.relatedTarget) || U === e && W(null);
55
+ }, [U]), se = l((e, t) => {
56
+ t.preventDefault(), t.dataTransfer && (t.dataTransfer.dropEffect = "move"), cancelAnimationFrame(Y.current), H(null), W(null);
57
+ }, []), ce = l(() => {
58
+ if (cancelAnimationFrame(Y.current), V) {
59
+ let t = [...L].sort((e, t) => e.order - t.order), n = t.findIndex((e) => e.widgetId === V), i = r[V], o = i ? a(I, i).title : V;
60
+ n >= 0 && K(e(F, "dashboardCustomizer.reorderAnnouncement", `${o} moved to position ${n + 1} of ${t.length}`));
61
+ }
62
+ H(null), W(null), J.current = null;
63
+ }, [
64
+ V,
65
+ L,
66
+ I,
67
+ F
68
+ ]), le = l(async () => {
38
69
  await M(L), B(!1), A(!1);
39
70
  }, [
40
71
  L,
41
72
  M,
42
73
  A
43
- ]), G = [...L].sort((e, t) => e.order - t.order);
44
- return /* @__PURE__ */ g(x, {
74
+ ]), $ = [...L].sort((e, t) => e.order - t.order);
75
+ return /* @__PURE__ */ y(w, {
45
76
  open: k,
46
77
  onOpenChange: A,
47
- children: /* @__PURE__ */ _(S, {
78
+ children: /* @__PURE__ */ b(T, {
48
79
  className: "w-full sm:max-w-md overflow-y-auto",
49
80
  side: "right",
50
81
  children: [
51
- /* @__PURE__ */ _(T, { children: [/* @__PURE__ */ g(E, { children: e(F, "dashboardCustomizer.title", "Customize Dashboard") }), /* @__PURE__ */ g(C, { children: e(F, "dashboardCustomizer.description", "Toggle widgets on or off and reorder them to personalize your dashboard.") })] }),
52
- /* @__PURE__ */ g("div", {
82
+ /* @__PURE__ */ b(O, { children: [/* @__PURE__ */ y(ee, { children: e(F, "dashboardCustomizer.title", "Customize Dashboard") }), /* @__PURE__ */ y(E, { children: e(F, "dashboardCustomizer.description", "Toggle widgets on or off and reorder them to personalize your dashboard.") })] }),
83
+ /* @__PURE__ */ b("div", {
84
+ role: "listbox",
85
+ "aria-label": e(F, "dashboardCustomizer.listLabel", "Dashboard widgets"),
53
86
  className: "py-4 space-y-1",
54
- children: G.map((e, t) => {
55
- let n = r[e.widgetId];
56
- return !n || !i(n, P) ? null : /* @__PURE__ */ _("div", {
57
- className: `flex items-center gap-3 p-3 rounded-lg border transition-colors ${e.enabled ? "border-border-default bg-bg-surface" : "border-transparent bg-bg-sunken/30 opacity-60"}`,
87
+ children: [/* @__PURE__ */ y("div", {
88
+ "aria-live": "assertive",
89
+ className: "sr-only",
90
+ children: G
91
+ }), $.map((t, n) => {
92
+ let s = r[t.widgetId];
93
+ if (!s || !i(s, P)) return null;
94
+ let c = V === t.widgetId, l = U === t.widgetId && V !== null, u = a(I, s).title;
95
+ return /* @__PURE__ */ b("div", {
96
+ ref: ie(t.widgetId),
97
+ role: "option",
98
+ "aria-roledescription": e(F, "dashboardCustomizer.sortableItem", "sortable item"),
99
+ "aria-selected": c,
100
+ "aria-label": e(F, "dashboardCustomizer.widgetRow", `Widget: ${u}. Use the arrow buttons to reorder.`),
101
+ onDragOver: (e) => oe(t.widgetId, e),
102
+ onDragLeave: (e) => Q(t.widgetId, e),
103
+ onDrop: (e) => se(t.widgetId, e),
104
+ "data-widget-id": t.widgetId,
105
+ className: `flex items-center gap-3 p-3 rounded-lg border transition-colors ${t.enabled ? "border-border-default bg-bg-surface" : "border-transparent bg-bg-sunken/30 opacity-60"} ${c ? "opacity-50" : ""} ${l ? "ring-2 ring-border-strong border-transparent" : ""}`,
58
106
  children: [
59
- /* @__PURE__ */ g(f, { className: "size-4 text-text-secondary shrink-0" }),
60
- /* @__PURE__ */ _("div", {
107
+ /* @__PURE__ */ y("span", {
108
+ draggable: !0,
109
+ onDragStart: (e) => ae(t.widgetId, e),
110
+ onDragEnd: ce,
111
+ "aria-label": e(F, "dashboardCustomizer.dragHandle", `Drag handle for ${u}`),
112
+ className: "cursor-grab active:cursor-grabbing touch-none select-none",
113
+ children: /* @__PURE__ */ y(h, {
114
+ "aria-hidden": !0,
115
+ className: "size-4 text-text-secondary shrink-0"
116
+ })
117
+ }),
118
+ /* @__PURE__ */ b("div", {
61
119
  className: "flex-1 min-w-0",
62
- children: [/* @__PURE__ */ _("div", {
120
+ children: [/* @__PURE__ */ b("div", {
63
121
  className: "flex items-center gap-2",
64
- children: [/* @__PURE__ */ g("span", {
122
+ children: [/* @__PURE__ */ y("span", {
65
123
  className: "text-sm font-medium text-text-primary",
66
- children: a(I, n).title
67
- }), /* @__PURE__ */ g(v, {
124
+ children: u
125
+ }), /* @__PURE__ */ y(x, {
68
126
  variant: "outline",
69
127
  className: "text-[10px]",
70
- children: n.defaultSize
128
+ children: s.defaultSize
71
129
  })]
72
- }), /* @__PURE__ */ g("p", {
130
+ }), /* @__PURE__ */ y("p", {
73
131
  className: "text-xs text-text-secondary truncate",
74
- children: a(I, n).description
132
+ children: a(I, s).description
75
133
  })]
76
134
  }),
77
- /* @__PURE__ */ _("div", {
135
+ /* @__PURE__ */ b("div", {
78
136
  className: "flex items-center gap-1 shrink-0",
79
137
  children: [
80
- /* @__PURE__ */ g(y, {
138
+ /* @__PURE__ */ y(S, {
81
139
  variant: "ghost",
82
140
  size: "sm",
83
141
  className: "size-7 p-0",
84
- disabled: t === 0,
85
- onClick: () => H(e.widgetId, "up"),
86
- children: /* @__PURE__ */ g(d, { className: "size-3" })
142
+ disabled: n === 0,
143
+ onClick: () => Z(t.widgetId, "up"),
144
+ "aria-label": e(F, "dashboardCustomizer.moveUp", `Move ${u} up`),
145
+ children: /* @__PURE__ */ y(m, { className: "size-3" })
87
146
  }),
88
- /* @__PURE__ */ g(y, {
147
+ /* @__PURE__ */ y(S, {
89
148
  variant: "ghost",
90
149
  size: "sm",
91
150
  className: "size-7 p-0",
92
- disabled: t === G.length - 1,
93
- onClick: () => H(e.widgetId, "down"),
94
- children: /* @__PURE__ */ g(u, { className: "size-3" })
151
+ disabled: n === $.length - 1,
152
+ onClick: () => Z(t.widgetId, "down"),
153
+ "aria-label": e(F, "dashboardCustomizer.moveDown", `Move ${u} down`),
154
+ children: /* @__PURE__ */ y(p, { className: "size-3" })
95
155
  }),
96
- /* @__PURE__ */ g(D, {
97
- checked: e.enabled,
156
+ /* @__PURE__ */ y(te, {
157
+ checked: t.enabled,
98
158
  onCheckedChange: () => {
99
- o("medium"), V(e.widgetId);
159
+ o("medium"), X(t.widgetId);
100
160
  },
101
- disabled: !n.removable,
161
+ disabled: !s.removable,
102
162
  className: "ml-1"
103
163
  })
104
164
  ]
105
165
  })
106
166
  ]
107
- }, e.widgetId);
108
- })
167
+ }, t.widgetId);
168
+ })]
109
169
  }),
110
- /* @__PURE__ */ g(b, {}),
111
- /* @__PURE__ */ _(w, {
170
+ /* @__PURE__ */ y(C, {}),
171
+ /* @__PURE__ */ b(D, {
112
172
  className: "flex-row gap-2 pt-4",
113
- children: [/* @__PURE__ */ _(y, {
173
+ children: [/* @__PURE__ */ b(S, {
114
174
  variant: "outline",
115
175
  size: "sm",
116
- onClick: U,
176
+ onClick: re,
117
177
  className: "flex-1",
118
- children: [/* @__PURE__ */ g(m, { className: "size-3 mr-1.5" }), e(F, "dashboardCustomizer.resetToDefaults", "Reset to defaults")]
119
- }), /* @__PURE__ */ _(y, {
178
+ children: [/* @__PURE__ */ y(_, { className: "size-3 mr-1.5" }), e(F, "dashboardCustomizer.resetToDefaults", "Reset to defaults")]
179
+ }), /* @__PURE__ */ b(S, {
120
180
  size: "sm",
121
- onClick: W,
181
+ onClick: le,
122
182
  disabled: !z || N,
123
183
  className: "flex-1",
124
- children: [N ? /* @__PURE__ */ g(p, { className: "size-3 mr-1.5 animate-spin" }) : /* @__PURE__ */ g(h, { className: "size-3 mr-1.5" }), e(F, "dashboardCustomizer.saveLayout", "Save layout")]
184
+ children: [N ? /* @__PURE__ */ y(g, { className: "size-3 mr-1.5 animate-spin" }) : /* @__PURE__ */ y(v, { className: "size-3 mr-1.5" }), e(F, "dashboardCustomizer.saveLayout", "Save layout")]
125
185
  })]
126
186
  })
127
187
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardCustomizer.js","names":[],"sources":["../../../src/pages/dashboard/DashboardCustomizer.tsx"],"sourcesContent":["/**\n * DashboardCustomizer — Sheet overlay for customizing dashboard widgets.\n *\n * Allows users to toggle widgets on/off, reorder them, and save their layout.\n */\nimport { useState, useCallback, useEffect } from 'react';\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n SheetDescription,\n SheetFooter,\n Button,\n Switch,\n Badge,\n Separator,\n} from '@burdenoff/fe-libs/ui';\nimport { ArrowUp, ArrowDown, RotateCcw, Save, Loader2, GripVertical } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../../utils/i18n';\nimport type { WidgetConfig } from './types';\nimport {\n WIDGET_DEFINITIONS,\n DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT,\n DEFAULT_DASHBOARD_WIDGETS,\n isWidgetAvailable,\n resolveWidgetCopy,\n} from './config';\nimport { nativeImpact } from '../../utils/nativeBridge';\n\ninterface DashboardCustomizerProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n widgets: WidgetConfig[];\n onSave: (widgets: WidgetConfig[]) => Promise<void>;\n isSaving: boolean;\n productId?: string;\n}\n\nexport function DashboardCustomizer({\n open,\n onOpenChange,\n widgets,\n onSave,\n isSaving,\n productId = 'workspaces',\n}: DashboardCustomizerProps) {\n const { t } = useI18n();\n // Widget titles/descriptions live in module-scope WIDGET_DEFINITIONS, so they\n // are translated here at render time with literal keys. [BOFF-5997]\n const trLiteral = (key: string, fallback: string): string => tWithFallback(t, key, fallback);\n\n const [localWidgets, setLocalWidgets] = useState<WidgetConfig[]>(widgets);\n const [hasChanges, setHasChanges] = useState(false);\n\n // Sync local state when sheet opens or widgets prop changes while open\n useEffect(() => {\n if (open) {\n setLocalWidgets([...widgets]);\n setHasChanges(false);\n }\n }, [open, widgets]);\n\n const toggleWidget = useCallback((widgetId: string) => {\n setLocalWidgets((prev) =>\n prev.map((w) => (w.widgetId === widgetId ? { ...w, enabled: !w.enabled } : w))\n );\n setHasChanges(true);\n }, []);\n\n const moveWidget = useCallback((widgetId: string, direction: 'up' | 'down') => {\n setLocalWidgets((prev) => {\n const sorted = [...prev].sort((a, b) => a.order - b.order);\n const idx = sorted.findIndex((w) => w.widgetId === widgetId);\n if (idx < 0) return prev;\n\n const swapIdx = direction === 'up' ? idx - 1 : idx + 1;\n if (swapIdx < 0 || swapIdx >= sorted.length) return prev;\n\n // Swap orders\n const tempOrder = sorted[idx].order;\n sorted[idx] = { ...sorted[idx], order: sorted[swapIdx].order };\n sorted[swapIdx] = { ...sorted[swapIdx], order: tempOrder };\n\n return sorted;\n });\n setHasChanges(true);\n }, []);\n\n const resetToDefaults = useCallback(() => {\n const defaults = DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT[productId] ?? DEFAULT_DASHBOARD_WIDGETS;\n setLocalWidgets([...defaults]);\n setHasChanges(true);\n }, [productId]);\n\n const handleSave = useCallback(async () => {\n await onSave(localWidgets);\n setHasChanges(false);\n onOpenChange(false);\n }, [localWidgets, onSave, onOpenChange]);\n\n const sorted = [...localWidgets].sort((a, b) => a.order - b.order);\n\n return (\n <Sheet open={open} onOpenChange={onOpenChange}>\n <SheetContent className=\"w-full sm:max-w-md overflow-y-auto\" side=\"right\">\n <SheetHeader>\n <SheetTitle>\n {tWithFallback(t, 'dashboardCustomizer.title', 'Customize Dashboard')}\n </SheetTitle>\n <SheetDescription>\n {tWithFallback(\n t,\n 'dashboardCustomizer.description',\n 'Toggle widgets on or off and reorder them to personalize your dashboard.'\n )}\n </SheetDescription>\n </SheetHeader>\n\n <div className=\"py-4 space-y-1\">\n {sorted.map((widget, idx) => {\n const definition = WIDGET_DEFINITIONS[widget.widgetId];\n if (!definition) return null;\n if (!isWidgetAvailable(definition, productId)) return null;\n\n return (\n <div\n key={widget.widgetId}\n className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${\n widget.enabled\n ? 'border-border-default bg-bg-surface'\n : 'border-transparent bg-bg-sunken/30 opacity-60'\n }`}\n >\n <GripVertical className=\"size-4 text-text-secondary shrink-0\" />\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">\n {resolveWidgetCopy(trLiteral, definition).title}\n </span>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {definition.defaultSize}\n </Badge>\n </div>\n <p className=\"text-xs text-text-secondary truncate\">\n {resolveWidgetCopy(trLiteral, definition).description}\n </p>\n </div>\n\n <div className=\"flex items-center gap-1 shrink-0\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"size-7 p-0\"\n disabled={idx === 0}\n onClick={() => moveWidget(widget.widgetId, 'up')}\n >\n <ArrowUp className=\"size-3\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"size-7 p-0\"\n disabled={idx === sorted.length - 1}\n onClick={() => moveWidget(widget.widgetId, 'down')}\n >\n <ArrowDown className=\"size-3\" />\n </Button>\n <Switch\n checked={widget.enabled}\n onCheckedChange={() => {\n void nativeImpact('medium');\n toggleWidget(widget.widgetId);\n }}\n disabled={!definition.removable}\n className=\"ml-1\"\n />\n </div>\n </div>\n );\n })}\n </div>\n\n <Separator />\n\n <SheetFooter className=\"flex-row gap-2 pt-4\">\n <Button variant=\"outline\" size=\"sm\" onClick={resetToDefaults} className=\"flex-1\">\n <RotateCcw className=\"size-3 mr-1.5\" />\n {tWithFallback(t, 'dashboardCustomizer.resetToDefaults', 'Reset to defaults')}\n </Button>\n <Button\n size=\"sm\"\n onClick={handleSave}\n disabled={!hasChanges || isSaving}\n className=\"flex-1\"\n >\n {isSaving ? (\n <Loader2 className=\"size-3 mr-1.5 animate-spin\" />\n ) : (\n <Save className=\"size-3 mr-1.5\" />\n )}\n {tWithFallback(t, 'dashboardCustomizer.saveLayout', 'Save layout')}\n </Button>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardCustomizer.js","names":[],"sources":["../../../src/pages/dashboard/DashboardCustomizer.tsx"],"sourcesContent":["/**\n * DashboardCustomizer — Sheet overlay for customizing dashboard widgets.\n *\n * Allows users to toggle widgets on/off, reorder them, and save their layout.\n */\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n SheetDescription,\n SheetFooter,\n Button,\n Switch,\n Badge,\n Separator,\n} from '@burdenoff/fe-libs/ui';\nimport { ArrowUp, ArrowDown, RotateCcw, Save, Loader2, GripVertical } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../../utils/i18n';\nimport type { WidgetConfig } from './types';\nimport {\n WIDGET_DEFINITIONS,\n DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT,\n DEFAULT_DASHBOARD_WIDGETS,\n isWidgetAvailable,\n resolveWidgetCopy,\n} from './config';\nimport { nativeImpact } from '../../utils/nativeBridge';\nimport { reorderByDropIndex, dropPositionFromPointer, type DropPosition } from './reorder';\n\ninterface DashboardCustomizerProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n widgets: WidgetConfig[];\n onSave: (widgets: WidgetConfig[]) => Promise<void>;\n isSaving: boolean;\n productId?: string;\n}\n\nexport function DashboardCustomizer({\n open,\n onOpenChange,\n widgets,\n onSave,\n isSaving,\n productId = 'workspaces',\n}: DashboardCustomizerProps) {\n const { t } = useI18n();\n // Widget titles/descriptions live in module-scope WIDGET_DEFINITIONS, so they\n // are translated here at render time with literal keys. [BOFF-5997]\n const trLiteral = useCallback(\n (key: string, fallback: string): string => tWithFallback(t, key, fallback),\n [t]\n );\n\n const [localWidgets, setLocalWidgets] = useState<WidgetConfig[]>(widgets);\n const [hasChanges, setHasChanges] = useState(false);\n const [draggingId, setDraggingId] = useState<string | null>(null);\n const [dropTargetId, setDropTargetId] = useState<string | null>(null);\n /** Live-region announcement for screen readers after a successful reorder. */\n const [liveAnnouncement, setLiveAnnouncement] = useState('');\n const rowRefs = useRef(new Map<string, HTMLDivElement>());\n /** Last (from, to, position) applied during the current drag — used to skip\n * repeated `dragOver` firings that would otherwise churn renders. */\n const lastAppliedMoveRef = useRef<{\n fromId: string;\n toId: string;\n position: DropPosition;\n } | null>(null);\n /** Pending rAF id for coalescing `dragOver` reorders to one per frame. */\n const dragOverRafRef = useRef(0);\n\n // Sync local state when sheet opens or widgets prop changes while open\n useEffect(() => {\n if (open) {\n setLocalWidgets([...widgets]);\n setHasChanges(false);\n }\n }, [open, widgets]);\n\n const toggleWidget = useCallback((widgetId: string) => {\n setLocalWidgets((prev) =>\n prev.map((w) => (w.widgetId === widgetId ? { ...w, enabled: !w.enabled } : w))\n );\n setHasChanges(true);\n }, []);\n\n const moveWidget = useCallback((widgetId: string, direction: 'up' | 'down') => {\n setLocalWidgets((prev) => {\n const sorted = [...prev].sort((a, b) => a.order - b.order);\n const idx = sorted.findIndex((w) => w.widgetId === widgetId);\n if (idx < 0) return prev;\n\n const swapIdx = direction === 'up' ? idx - 1 : idx + 1;\n if (swapIdx < 0 || swapIdx >= sorted.length) return prev;\n\n // Swap orders\n const tempOrder = sorted[idx].order;\n sorted[idx] = { ...sorted[idx], order: sorted[swapIdx].order };\n sorted[swapIdx] = { ...sorted[swapIdx], order: tempOrder };\n\n return sorted;\n });\n setHasChanges(true);\n }, []);\n\n const resetToDefaults = useCallback(() => {\n const defaults = DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT[productId] ?? DEFAULT_DASHBOARD_WIDGETS;\n setLocalWidgets([...defaults]);\n setHasChanges(true);\n }, [productId]);\n\n // ─── Drag-and-drop (HTML5 native) ──────────────────────────────────────────\n // The 6-dot icon next to each widget is the drag handle. Up/down arrow\n // buttons remain the keyboard-accessible reorder path. [BOFF-7219]\n const setRowRef = useCallback(\n (widgetId: string) => (el: HTMLDivElement | null) => {\n if (el) rowRefs.current.set(widgetId, el);\n else rowRefs.current.delete(widgetId);\n },\n []\n );\n\n const handleDragStart = useCallback(\n (widgetId: string, event: React.DragEvent<HTMLSpanElement>) => {\n setDraggingId(widgetId);\n lastAppliedMoveRef.current = null;\n cancelAnimationFrame(dragOverRafRef.current);\n event.dataTransfer.effectAllowed = 'move';\n // Firefox refuses to fire drag events without a setData payload.\n event.dataTransfer.setData('text/plain', widgetId);\n },\n []\n );\n\n const handleDragOver = useCallback(\n (widgetId: string, event: React.DragEvent<HTMLDivElement>) => {\n if (!draggingId || draggingId === widgetId) return;\n event.preventDefault();\n event.dataTransfer.dropEffect = 'move';\n setDropTargetId(widgetId);\n\n // Coalesce the expensive reorder work to once per animation frame.\n // `dragOver` fires at ~60 FPS even when the pointer is stationary;\n // without this guard every firing triggers a re-sort + re-render.\n cancelAnimationFrame(dragOverRafRef.current);\n dragOverRafRef.current = requestAnimationFrame(() => {\n const row = rowRefs.current.get(widgetId);\n if (!row) return;\n const position = dropPositionFromPointer(event.clientY, row.getBoundingClientRect());\n const last = lastAppliedMoveRef.current;\n if (\n last &&\n last.fromId === draggingId &&\n last.toId === widgetId &&\n last.position === position\n ) {\n return; // same move as last firing — skip to avoid re-render churn\n }\n lastAppliedMoveRef.current = { fromId: draggingId, toId: widgetId, position };\n setLocalWidgets((prev) => reorderByDropIndex(prev, draggingId, widgetId, position));\n setHasChanges(true);\n });\n },\n [draggingId]\n );\n\n const handleDragLeave = useCallback(\n (widgetId: string, event: React.DragEvent<HTMLDivElement>) => {\n // Only clear the drop target when the pointer truly leaves the row\n // (entering a child element bubbles dragleave on the parent).\n if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n if (dropTargetId === widgetId) setDropTargetId(null);\n },\n [dropTargetId]\n );\n\n const handleDrop = useCallback((widgetId: string, event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';\n cancelAnimationFrame(dragOverRafRef.current);\n setDraggingId(null);\n setDropTargetId(null);\n }, []);\n\n const handleDragEnd = useCallback(() => {\n cancelAnimationFrame(dragOverRafRef.current);\n // Announce the final position of the dragged widget for screen-reader\n // users. aria-live=\"assertive\" reads the new text immediately.\n if (draggingId) {\n const finalWidgets = [...localWidgets].sort((a, b) => a.order - b.order);\n const finalIdx = finalWidgets.findIndex((w) => w.widgetId === draggingId);\n const definition = WIDGET_DEFINITIONS[draggingId];\n const title = definition ? resolveWidgetCopy(trLiteral, definition).title : draggingId;\n if (finalIdx >= 0) {\n setLiveAnnouncement(\n tWithFallback(\n t,\n 'dashboardCustomizer.reorderAnnouncement',\n `${title} moved to position ${finalIdx + 1} of ${finalWidgets.length}`\n )\n );\n }\n }\n setDraggingId(null);\n setDropTargetId(null);\n lastAppliedMoveRef.current = null;\n }, [draggingId, localWidgets, trLiteral, t]);\n\n const handleSave = useCallback(async () => {\n await onSave(localWidgets);\n setHasChanges(false);\n onOpenChange(false);\n }, [localWidgets, onSave, onOpenChange]);\n\n const sorted = [...localWidgets].sort((a, b) => a.order - b.order);\n\n return (\n <Sheet open={open} onOpenChange={onOpenChange}>\n <SheetContent className=\"w-full sm:max-w-md overflow-y-auto\" side=\"right\">\n <SheetHeader>\n <SheetTitle>\n {tWithFallback(t, 'dashboardCustomizer.title', 'Customize Dashboard')}\n </SheetTitle>\n <SheetDescription>\n {tWithFallback(\n t,\n 'dashboardCustomizer.description',\n 'Toggle widgets on or off and reorder them to personalize your dashboard.'\n )}\n </SheetDescription>\n </SheetHeader>\n\n <div\n role=\"listbox\"\n aria-label={tWithFallback(t, 'dashboardCustomizer.listLabel', 'Dashboard widgets')}\n className=\"py-4 space-y-1\"\n >\n {/* Live region for screen-reader announcements after a successful\n reorder. `aria-live=\"assertive\"` interrupts the user to make\n the new position obvious; the region itself is visually\n hidden via `sr-only`. */}\n <div aria-live=\"assertive\" className=\"sr-only\">\n {liveAnnouncement}\n </div>\n {sorted.map((widget, idx) => {\n const definition = WIDGET_DEFINITIONS[widget.widgetId];\n if (!definition) return null;\n if (!isWidgetAvailable(definition, productId)) return null;\n\n const isDragging = draggingId === widget.widgetId;\n const isDropTarget = dropTargetId === widget.widgetId && draggingId !== null;\n const widgetTitle = resolveWidgetCopy(trLiteral, definition).title;\n\n return (\n <div\n key={widget.widgetId}\n ref={setRowRef(widget.widgetId)}\n role=\"option\"\n aria-roledescription={tWithFallback(\n t,\n 'dashboardCustomizer.sortableItem',\n 'sortable item'\n )}\n aria-selected={isDragging}\n aria-label={tWithFallback(\n t,\n 'dashboardCustomizer.widgetRow',\n `Widget: ${widgetTitle}. Use the arrow buttons to reorder.`\n )}\n onDragOver={(e) => handleDragOver(widget.widgetId, e)}\n onDragLeave={(e) => handleDragLeave(widget.widgetId, e)}\n onDrop={(e) => handleDrop(widget.widgetId, e)}\n data-widget-id={widget.widgetId}\n className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${\n widget.enabled\n ? 'border-border-default bg-bg-surface'\n : 'border-transparent bg-bg-sunken/30 opacity-60'\n } ${isDragging ? 'opacity-50' : ''} ${\n isDropTarget ? 'ring-2 ring-border-strong border-transparent' : ''\n }`}\n >\n <span\n draggable\n onDragStart={(e) => handleDragStart(widget.widgetId, e)}\n onDragEnd={handleDragEnd}\n aria-label={tWithFallback(\n t,\n 'dashboardCustomizer.dragHandle',\n `Drag handle for ${widgetTitle}`\n )}\n className=\"cursor-grab active:cursor-grabbing touch-none select-none\"\n >\n <GripVertical aria-hidden className=\"size-4 text-text-secondary shrink-0\" />\n </span>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">{widgetTitle}</span>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {definition.defaultSize}\n </Badge>\n </div>\n <p className=\"text-xs text-text-secondary truncate\">\n {resolveWidgetCopy(trLiteral, definition).description}\n </p>\n </div>\n\n <div className=\"flex items-center gap-1 shrink-0\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"size-7 p-0\"\n disabled={idx === 0}\n onClick={() => moveWidget(widget.widgetId, 'up')}\n aria-label={tWithFallback(\n t,\n 'dashboardCustomizer.moveUp',\n `Move ${widgetTitle} up`\n )}\n >\n <ArrowUp className=\"size-3\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"size-7 p-0\"\n disabled={idx === sorted.length - 1}\n onClick={() => moveWidget(widget.widgetId, 'down')}\n aria-label={tWithFallback(\n t,\n 'dashboardCustomizer.moveDown',\n `Move ${widgetTitle} down`\n )}\n >\n <ArrowDown className=\"size-3\" />\n </Button>\n <Switch\n checked={widget.enabled}\n onCheckedChange={() => {\n void nativeImpact('medium');\n toggleWidget(widget.widgetId);\n }}\n disabled={!definition.removable}\n className=\"ml-1\"\n />\n </div>\n </div>\n );\n })}\n </div>\n\n <Separator />\n\n <SheetFooter className=\"flex-row gap-2 pt-4\">\n <Button variant=\"outline\" size=\"sm\" onClick={resetToDefaults} className=\"flex-1\">\n <RotateCcw className=\"size-3 mr-1.5\" />\n {tWithFallback(t, 'dashboardCustomizer.resetToDefaults', 'Reset to defaults')}\n </Button>\n <Button\n size=\"sm\"\n onClick={handleSave}\n disabled={!hasChanges || isSaving}\n className=\"flex-1\"\n >\n {isSaving ? (\n <Loader2 className=\"size-3 mr-1.5 animate-spin\" />\n ) : (\n <Save className=\"size-3 mr-1.5\" />\n )}\n {tWithFallback(t, 'dashboardCustomizer.saveLayout', 'Save layout')}\n </Button>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n );\n}\n"],"mappings":";;;;;;;;;;AAyCA,SAAgB,EAAoB,EAClC,SACA,iBACA,YACA,WACA,aACA,eAAY,gBACe;CAC3B,IAAM,EAAE,SAAM,IAAS,EAGjB,IAAY,GACf,GAAa,MAA6B,EAAc,GAAG,GAAK,EAAS,EAC1E,CAAC,EAAE,CACJ,EAEK,CAAC,GAAc,KAAmB,EAAyB,EAAQ,EACnE,CAAC,GAAY,KAAiB,EAAS,GAAM,EAC7C,CAAC,GAAY,KAAiB,EAAwB,KAAK,EAC3D,CAAC,GAAc,KAAmB,EAAwB,KAAK,EAE/D,CAAC,GAAkB,KAAuB,EAAS,GAAG,EACtD,IAAU,kBAAO,IAAI,KAA6B,CAAC,EAGnD,IAAqB,EAIjB,KAAK,EAET,IAAiB,EAAO,EAAE;AAGhC,SAAgB;AACd,EAAI,MACF,EAAgB,CAAC,GAAG,EAAQ,CAAC,EAC7B,EAAc,GAAM;IAErB,CAAC,GAAM,EAAQ,CAAC;CAEnB,IAAM,IAAe,GAAa,MAAqB;AAIrD,EAHA,GAAiB,MACf,EAAK,KAAK,MAAO,EAAE,aAAa,IAAW;GAAE,GAAG;GAAG,SAAS,CAAC,EAAE;GAAS,GAAG,EAAG,CAC/E,EACD,EAAc,GAAK;IAClB,EAAE,CAAC,EAEA,IAAa,GAAa,GAAkB,MAA6B;AAgB7E,EAfA,GAAiB,MAAS;GACxB,IAAM,IAAS,CAAC,GAAG,EAAK,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EACpD,IAAM,EAAO,WAAW,MAAM,EAAE,aAAa,EAAS;AAC5D,OAAI,IAAM,EAAG,QAAO;GAEpB,IAAM,IAAU,MAAc,OAAO,IAAM,IAAI,IAAM;AACrD,OAAI,IAAU,KAAK,KAAW,EAAO,OAAQ,QAAO;GAGpD,IAAM,IAAY,EAAO,GAAK;AAI9B,UAHA,EAAO,KAAO;IAAE,GAAG,EAAO;IAAM,OAAO,EAAO,GAAS;IAAO,EAC9D,EAAO,KAAW;IAAE,GAAG,EAAO;IAAU,OAAO;IAAW,EAEnD;IACP,EACF,EAAc,GAAK;IAClB,EAAE,CAAC,EAEA,KAAkB,QAAkB;AAGxC,EADA,EAAgB,CAAC,GADA,EAAqC,MAAc,EACvC,CAAC,EAC9B,EAAc,GAAK;IAClB,CAAC,EAAU,CAAC,EAKT,KAAY,GACf,OAAsB,MAA8B;AACnD,EAAI,IAAI,EAAQ,QAAQ,IAAI,GAAU,EAAG,GACpC,EAAQ,QAAQ,OAAO,EAAS;IAEvC,EAAE,CACH,EAEK,KAAkB,GACrB,GAAkB,MAA4C;AAM7D,EALA,EAAc,EAAS,EACvB,EAAmB,UAAU,MAC7B,qBAAqB,EAAe,QAAQ,EAC5C,EAAM,aAAa,gBAAgB,QAEnC,EAAM,aAAa,QAAQ,cAAc,EAAS;IAEpD,EAAE,CACH,EAEK,KAAiB,GACpB,GAAkB,MAA2C;AACxD,GAAC,KAAc,MAAe,MAClC,EAAM,gBAAgB,EACtB,EAAM,aAAa,aAAa,QAChC,EAAgB,EAAS,EAKzB,qBAAqB,EAAe,QAAQ,EAC5C,EAAe,UAAU,4BAA4B;GACnD,IAAM,IAAM,EAAQ,QAAQ,IAAI,EAAS;AACzC,OAAI,CAAC,EAAK;GACV,IAAM,IAAW,EAAwB,EAAM,SAAS,EAAI,uBAAuB,CAAC,EAC9E,IAAO,EAAmB;AAE9B,QACA,EAAK,WAAW,KAChB,EAAK,SAAS,KACd,EAAK,aAAa,MAIpB,EAAmB,UAAU;IAAE,QAAQ;IAAY,MAAM;IAAU;IAAU,EAC7E,GAAiB,MAAS,EAAmB,GAAM,GAAY,GAAU,EAAS,CAAC,EACnF,EAAc,GAAK;IACnB;IAEJ,CAAC,EAAW,CACb,EAEK,IAAkB,GACrB,GAAkB,MAA2C;AAGxD,IAAM,cAAc,SAAS,EAAM,cAA6B,IAChE,MAAiB,KAAU,EAAgB,KAAK;IAEtD,CAAC,EAAa,CACf,EAEK,KAAa,GAAa,GAAkB,MAA2C;AAK3F,EAJA,EAAM,gBAAgB,EAClB,EAAM,iBAAc,EAAM,aAAa,aAAa,SACxD,qBAAqB,EAAe,QAAQ,EAC5C,EAAc,KAAK,EACnB,EAAgB,KAAK;IACpB,EAAE,CAAC,EAEA,KAAgB,QAAkB;AAItC,MAHA,qBAAqB,EAAe,QAAQ,EAGxC,GAAY;GACd,IAAM,IAAe,CAAC,GAAG,EAAa,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EAClE,IAAW,EAAa,WAAW,MAAM,EAAE,aAAa,EAAW,EACnE,IAAa,EAAmB,IAChC,IAAQ,IAAa,EAAkB,GAAW,EAAW,CAAC,QAAQ;AAC5E,GAAI,KAAY,KACd,EACE,EACE,GACA,2CACA,GAAG,EAAM,qBAAqB,IAAW,EAAE,MAAM,EAAa,SAC/D,CACF;;AAKL,EAFA,EAAc,KAAK,EACnB,EAAgB,KAAK,EACrB,EAAmB,UAAU;IAC5B;EAAC;EAAY;EAAc;EAAW;EAAE,CAAC,EAEtC,KAAa,EAAY,YAAY;AAGzC,EAFA,MAAM,EAAO,EAAa,EAC1B,EAAc,GAAM,EACpB,EAAa,GAAM;IAClB;EAAC;EAAc;EAAQ;EAAa,CAAC,EAElC,IAAS,CAAC,GAAG,EAAa,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;AAElE,QACE,kBAAC,GAAD;EAAa;EAAoB;YAC/B,kBAAC,GAAD;GAAc,WAAU;GAAqC,MAAK;aAAlE;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,IAAD,EAAA,UACG,EAAc,GAAG,6BAA6B,sBAAsB,EAC1D,CAAA,EACb,kBAAC,GAAD,EAAA,UACG,EACC,GACA,mCACA,2EACD,EACgB,CAAA,CACP,EAAA,CAAA;IAEd,kBAAC,OAAD;KACE,MAAK;KACL,cAAY,EAAc,GAAG,iCAAiC,oBAAoB;KAClF,WAAU;eAHZ,CASE,kBAAC,OAAD;MAAK,aAAU;MAAY,WAAU;gBAClC;MACG,CAAA,EACL,EAAO,KAAK,GAAQ,MAAQ;MAC3B,IAAM,IAAa,EAAmB,EAAO;AAE7C,UADI,CAAC,KACD,CAAC,EAAkB,GAAY,EAAU,CAAE,QAAO;MAEtD,IAAM,IAAa,MAAe,EAAO,UACnC,IAAe,MAAiB,EAAO,YAAY,MAAe,MAClE,IAAc,EAAkB,GAAW,EAAW,CAAC;AAE7D,aACE,kBAAC,OAAD;OAEE,KAAK,GAAU,EAAO,SAAS;OAC/B,MAAK;OACL,wBAAsB,EACpB,GACA,oCACA,gBACD;OACD,iBAAe;OACf,cAAY,EACV,GACA,iCACA,WAAW,EAAY,qCACxB;OACD,aAAa,MAAM,GAAe,EAAO,UAAU,EAAE;OACrD,cAAc,MAAM,EAAgB,EAAO,UAAU,EAAE;OACvD,SAAS,MAAM,GAAW,EAAO,UAAU,EAAE;OAC7C,kBAAgB,EAAO;OACvB,WAAW,mEACT,EAAO,UACH,wCACA,gDACL,GAAG,IAAa,eAAe,GAAG,GACjC,IAAe,iDAAiD;iBAxBpE;QA2BE,kBAAC,QAAD;SACE,WAAA;SACA,cAAc,MAAM,GAAgB,EAAO,UAAU,EAAE;SACvD,WAAW;SACX,cAAY,EACV,GACA,kCACA,mBAAmB,IACpB;SACD,WAAU;mBAEV,kBAAC,GAAD;UAAc,eAAA;UAAY,WAAU;UAAwC,CAAA;SACvE,CAAA;QAEP,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,QAAD;WAAM,WAAU;qBAAyC;WAAmB,CAAA,EAC5E,kBAAC,GAAD;WAAO,SAAQ;WAAU,WAAU;qBAChC,EAAW;WACN,CAAA,CACJ;aACN,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAkB,GAAW,EAAW,CAAC;UACxC,CAAA,CACA;;QAEN,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,GAAD;WACE,SAAQ;WACR,MAAK;WACL,WAAU;WACV,UAAU,MAAQ;WAClB,eAAe,EAAW,EAAO,UAAU,KAAK;WAChD,cAAY,EACV,GACA,8BACA,QAAQ,EAAY,KACrB;qBAED,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;WACvB,CAAA;UACT,kBAAC,GAAD;WACE,SAAQ;WACR,MAAK;WACL,WAAU;WACV,UAAU,MAAQ,EAAO,SAAS;WAClC,eAAe,EAAW,EAAO,UAAU,OAAO;WAClD,cAAY,EACV,GACA,gCACA,QAAQ,EAAY,OACrB;qBAED,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;WACzB,CAAA;UACT,kBAAC,IAAD;WACE,SAAS,EAAO;WAChB,uBAAuB;AAErB,YADK,EAAa,SAAS,EAC3B,EAAa,EAAO,SAAS;;WAE/B,UAAU,CAAC,EAAW;WACtB,WAAU;WACV,CAAA;UACE;;QACF;SA3FC,EAAO,SA2FR;OAER,CACE;;IAEN,kBAAC,GAAD,EAAa,CAAA;IAEb,kBAAC,GAAD;KAAa,WAAU;eAAvB,CACE,kBAAC,GAAD;MAAQ,SAAQ;MAAU,MAAK;MAAK,SAAS;MAAiB,WAAU;gBAAxE,CACE,kBAAC,GAAD,EAAW,WAAU,iBAAkB,CAAA,EACtC,EAAc,GAAG,uCAAuC,oBAAoB,CACtE;SACT,kBAAC,GAAD;MACE,MAAK;MACL,SAAS;MACT,UAAU,CAAC,KAAc;MACzB,WAAU;gBAJZ,CAMG,IACC,kBAAC,GAAD,EAAS,WAAU,8BAA+B,CAAA,GAElD,kBAAC,GAAD,EAAM,WAAU,iBAAkB,CAAA,EAEnC,EAAc,GAAG,kCAAkC,cAAc,CAC3D;QACG;;IACD;;EACT,CAAA"}
@@ -0,0 +1,26 @@
1
+ //#region src/pages/dashboard/reorder.ts
2
+ var e = 10;
3
+ function t(t) {
4
+ return t.map((t, n) => ({
5
+ ...t,
6
+ order: (n + 1) * e
7
+ }));
8
+ }
9
+ function n(e, n, r, i) {
10
+ if (n === r) return e;
11
+ let a = [...e].sort((e, t) => e.order - t.order), o = a.findIndex((e) => e.widgetId === n), s = a.findIndex((e) => e.widgetId === r);
12
+ if (o < 0 || s < 0) return e;
13
+ let c = a[o], l = a.filter((e, t) => t !== o), u = i === "above" ? l.findIndex((e) => e.widgetId === r) : l.findIndex((e) => e.widgetId === r) + 1;
14
+ return t([
15
+ ...l.slice(0, u),
16
+ c,
17
+ ...l.slice(u)
18
+ ]);
19
+ }
20
+ function r(e, t) {
21
+ return e < t.top + t.height / 2 ? "above" : "below";
22
+ }
23
+ //#endregion
24
+ export { r as dropPositionFromPointer, n as reorderByDropIndex };
25
+
26
+ //# sourceMappingURL=reorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reorder.js","names":[],"sources":["../../../src/pages/dashboard/reorder.ts"],"sourcesContent":["/**\n * Pure reorder helpers for the dashboard customizer drag-and-drop.\n *\n * Extracted from `DashboardCustomizer.tsx` so the ordering logic can be unit-\n * tested without DOM/React Test Renderer. The caller owns the live `WidgetConfig[]`\n * state and is responsible for cloning before mutating.\n *\n * Invariants:\n * - Input widgets are expected to already be sorted by `order` ascending. The\n * helper re-sorts defensively so callers don't have to.\n * - `enabled` and `size` are preserved on every widget; only `order` changes.\n * - Drop positions are evaluated relative to the row the pointer is over: the\n * upper half means \"insert before\", the lower half means \"insert after\".\n */\nimport type { WidgetConfig } from './types';\n\n/** Side of the target row the pointer is on, relative to the row midpoint. */\nexport type DropPosition = 'above' | 'below';\n\nconst ORDER_STEP = 10;\n\n/**\n * Re-number `order` so it is strictly increasing by `ORDER_STEP` from the\n * given array's order. Returns a fresh array; input is not mutated.\n */\nexport function renumberOrders(widgets: WidgetConfig[]): WidgetConfig[] {\n return widgets.map((w, idx) => ({ ...w, order: (idx + 1) * ORDER_STEP }));\n}\n\n/**\n * Move `fromId` to a position adjacent to `toId`.\n *\n * - `position === 'above'` — `fromId` ends up immediately before `toId`.\n * - `position === 'below'` — `fromId` ends up immediately after `toId`.\n *\n * If either id is missing, or `fromId === toId`, the input is returned\n * unchanged (referential equality preserved). Adjacent swaps are valid\n * moves — flicker prevention is the caller's responsibility (see\n * `DashboardCustomizer.handleDragOver`, which tracks the last-applied\n * move via a ref). [BOFF-7219]\n */\nexport function reorderByDropIndex(\n widgets: WidgetConfig[],\n fromId: string,\n toId: string,\n position: DropPosition\n): WidgetConfig[] {\n if (fromId === toId) return widgets;\n\n const sorted = [...widgets].sort((a, b) => a.order - b.order);\n const fromIdx = sorted.findIndex((w) => w.widgetId === fromId);\n const toIdx = sorted.findIndex((w) => w.widgetId === toId);\n if (fromIdx < 0 || toIdx < 0) return widgets;\n\n const moving = sorted[fromIdx];\n const without = sorted.filter((_, i) => i !== fromIdx);\n const insertIdx =\n position === 'above'\n ? without.findIndex((w) => w.widgetId === toId)\n : without.findIndex((w) => w.widgetId === toId) + 1;\n\n const reordered = [...without.slice(0, insertIdx), moving, ...without.slice(insertIdx)];\n return renumberOrders(reordered);\n}\n\n/**\n * Resolve the drop position from a pointer event and a target row's bounding\n * rect. `clientY` above the midpoint yields 'above', otherwise 'below'.\n */\nexport function dropPositionFromPointer(clientY: number, rect: DOMRect): DropPosition {\n return clientY < rect.top + rect.height / 2 ? 'above' : 'below';\n}\n"],"mappings":";AAmBA,IAAM,IAAa;AAMnB,SAAgB,EAAe,GAAyC;AACtE,QAAO,EAAQ,KAAK,GAAG,OAAS;EAAE,GAAG;EAAG,QAAQ,IAAM,KAAK;EAAY,EAAE;;AAe3E,SAAgB,EACd,GACA,GACA,GACA,GACgB;AAChB,KAAI,MAAW,EAAM,QAAO;CAE5B,IAAM,IAAS,CAAC,GAAG,EAAQ,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EACvD,IAAU,EAAO,WAAW,MAAM,EAAE,aAAa,EAAO,EACxD,IAAQ,EAAO,WAAW,MAAM,EAAE,aAAa,EAAK;AAC1D,KAAI,IAAU,KAAK,IAAQ,EAAG,QAAO;CAErC,IAAM,IAAS,EAAO,IAChB,IAAU,EAAO,QAAQ,GAAG,MAAM,MAAM,EAAQ,EAChD,IACJ,MAAa,UACT,EAAQ,WAAW,MAAM,EAAE,aAAa,EAAK,GAC7C,EAAQ,WAAW,MAAM,EAAE,aAAa,EAAK,GAAG;AAGtD,QAAO,EADW;EAAC,GAAG,EAAQ,MAAM,GAAG,EAAU;EAAE;EAAQ,GAAG,EAAQ,MAAM,EAAU;EAAC,CACvD;;AAOlC,SAAgB,EAAwB,GAAiB,GAA6B;AACpF,QAAO,IAAU,EAAK,MAAM,EAAK,SAAS,IAAI,UAAU"}
@@ -5,10 +5,16 @@ import { jsx as s, jsxs as c } from "react/jsx-runtime";
5
5
  import { GlassCard as l, GradientText as u } from "@burdenoff/fe-libs/ui";
6
6
  import { useI18n as d } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
7
7
  //#region src/pages/dashboard/widgets/WelcomeWidget.tsx
8
- function f({ icon: e, label: t, value: n }) {
8
+ function f({ icon: e, label: t, value: n, onClick: r }) {
9
9
  return /* @__PURE__ */ s(l, {
10
10
  treatment: "metric",
11
- className: "p-3",
11
+ className: `p-3${r ? " cursor-pointer hover:shadow-[var(--shadow-pop-hover)] hover:border-border-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]" : ""}`,
12
+ onClick: r,
13
+ onKeyDown: r ? (e) => {
14
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), r?.());
15
+ } : void 0,
16
+ role: r ? "button" : void 0,
17
+ tabIndex: r ? 0 : void 0,
12
18
  children: /* @__PURE__ */ c("div", {
13
19
  className: "flex items-center gap-3",
14
20
  children: [/* @__PURE__ */ s("div", {
@@ -24,10 +30,10 @@ function f({ icon: e, label: t, value: n }) {
24
30
  })
25
31
  });
26
32
  }
27
- function p({ workspaceData: p, globalData: m, isLoading: h }) {
28
- let { t: g } = d(), _ = m?.currentUser, v = _?.firstName || _?.name?.split(" ")[0] || e(g, "dashboard.welcome.fallbackName", "there"), y = (/* @__PURE__ */ new Date()).getHours(), b = y < 12 ? e(g, "dashboard.welcome.greetingMorning", "Good morning") : y < 18 ? e(g, "dashboard.welcome.greetingAfternoon", "Good afternoon") : e(g, "dashboard.welcome.greetingEvening", "Good evening"), x = p?.workspaces.total ?? 0, S = p?.projects.total ?? 0, C = p?.groups.totalCount ?? 0, w = p?.recentActivities.length ?? 0;
33
+ function p({ workspaceData: p, globalData: m, isLoading: h, navigate: g }) {
34
+ let { t: _ } = d(), v = m?.currentUser, y = v?.firstName || v?.name?.split(" ")[0] || e(_, "dashboard.welcome.fallbackName", "there"), b = (/* @__PURE__ */ new Date()).getHours(), x = b < 12 ? e(_, "dashboard.welcome.greetingMorning", "Good morning") : b < 18 ? e(_, "dashboard.welcome.greetingAfternoon", "Good afternoon") : e(_, "dashboard.welcome.greetingEvening", "Good evening"), S = p?.workspaces.total ?? 0, C = p?.projects.total ?? 0, w = p?.groups.totalCount ?? 0, T = p?.recentActivities.length ?? 0;
29
35
  return /* @__PURE__ */ s(t, {
30
- title: e(g, "dashboard.welcome.title", "Dashboard"),
36
+ title: e(_, "dashboard.welcome.title", "Dashboard"),
31
37
  icon: /* @__PURE__ */ s(a, { className: "size-4" }),
32
38
  size: "large",
33
39
  isLoading: h,
@@ -40,37 +46,41 @@ function p({ workspaceData: p, globalData: m, isLoading: h }) {
40
46
  children: [/* @__PURE__ */ c("h2", {
41
47
  className: "text-xl font-bold text-text-primary",
42
48
  children: [
43
- b,
49
+ x,
44
50
  ", ",
45
- /* @__PURE__ */ s(u, { children: v }),
51
+ /* @__PURE__ */ s(u, { children: y }),
46
52
  "!"
47
53
  ]
48
54
  }), /* @__PURE__ */ s("p", {
49
55
  className: "text-sm text-text-secondary mt-1",
50
- children: e(g, "dashboard.welcome.subtitle", "Here's what's happening across your workspaces today.")
56
+ children: e(_, "dashboard.welcome.subtitle", "Here's what's happening across your workspaces today.")
51
57
  })]
52
58
  }), /* @__PURE__ */ c("div", {
53
59
  className: "grid grid-cols-2 sm:grid-cols-4 gap-3",
54
60
  children: [
55
61
  /* @__PURE__ */ s(f, {
56
62
  icon: /* @__PURE__ */ s(r, { className: "size-4" }),
57
- label: e(g, "dashboard.welcome.statWorkspaces", "Workspaces"),
58
- value: x
63
+ label: e(_, "dashboard.welcome.statWorkspaces", "Workspaces"),
64
+ value: S,
65
+ onClick: () => g("/workspaces/list")
59
66
  }),
60
67
  /* @__PURE__ */ s(f, {
61
68
  icon: /* @__PURE__ */ s(i, { className: "size-4" }),
62
- label: e(g, "dashboard.welcome.statProjects", "Projects"),
63
- value: S
69
+ label: e(_, "dashboard.welcome.statProjects", "Projects"),
70
+ value: C,
71
+ onClick: () => g("/workspaces/projects")
64
72
  }),
65
73
  /* @__PURE__ */ s(f, {
66
74
  icon: /* @__PURE__ */ s(o, { className: "size-4" }),
67
- label: e(g, "dashboard.welcome.statGroups", "Groups"),
68
- value: C
75
+ label: e(_, "dashboard.welcome.statGroups", "Groups"),
76
+ value: w,
77
+ onClick: () => g("/groups")
69
78
  }),
70
79
  /* @__PURE__ */ s(f, {
71
80
  icon: /* @__PURE__ */ s(n, { className: "size-4" }),
72
- label: e(g, "dashboard.welcome.statRecentActivity", "Recent Activity"),
73
- value: w
81
+ label: e(_, "dashboard.welcome.statRecentActivity", "Recent Activity"),
82
+ value: T,
83
+ onClick: () => g("/workspaces/activity")
74
84
  })
75
85
  ]
76
86
  })]
@@ -1 +1 @@
1
- {"version":3,"file":"WelcomeWidget.js","names":[],"sources":["../../../../src/pages/dashboard/widgets/WelcomeWidget.tsx"],"sourcesContent":["import { LayoutDashboard, Building2, FolderKanban, Users, Activity } from 'lucide-react';\nimport { GlassCard, GradientText } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { WidgetWrapper } from '../WidgetWrapper';\nimport { tWithFallback } from '../../../utils/i18n';\nimport type { WidgetProps } from '../types';\n\nfunction StatCard({\n icon,\n label,\n value,\n}: {\n icon: React.ReactNode;\n label: string;\n value: number | string;\n}) {\n return (\n <GlassCard treatment=\"metric\" className=\"p-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"rounded-md bg-action-primary-bg/10 p-2 text-primary\">{icon}</div>\n <div>\n <p className=\"text-2xl font-bold text-text-primary tabular-nums\">{value}</p>\n <p className=\"text-xs text-text-muted\">{label}</p>\n </div>\n </div>\n </GlassCard>\n );\n}\n\nexport function WelcomeWidget({ workspaceData, globalData, isLoading }: WidgetProps) {\n const { t } = useI18n();\n const user = globalData?.currentUser;\n const firstName =\n user?.firstName ||\n user?.name?.split(' ')[0] ||\n tWithFallback(t, 'dashboard.welcome.fallbackName', 'there');\n const hour = new Date().getHours();\n const greeting =\n hour < 12\n ? tWithFallback(t, 'dashboard.welcome.greetingMorning', 'Good morning')\n : hour < 18\n ? tWithFallback(t, 'dashboard.welcome.greetingAfternoon', 'Good afternoon')\n : tWithFallback(t, 'dashboard.welcome.greetingEvening', 'Good evening');\n\n const totalWorkspaces = workspaceData?.workspaces.total ?? 0;\n const totalProjects = workspaceData?.projects.total ?? 0;\n const totalGroups = workspaceData?.groups.totalCount ?? 0;\n const activityCount = workspaceData?.recentActivities.length ?? 0;\n\n return (\n <WidgetWrapper\n title={tWithFallback(t, 'dashboard.welcome.title', 'Dashboard')}\n icon={<LayoutDashboard className=\"size-4\" />}\n size=\"large\"\n isLoading={isLoading}\n >\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" glow className=\"relative overflow-hidden p-4\">\n <h2 className=\"text-xl font-bold text-text-primary\">\n {greeting}, <GradientText>{firstName}</GradientText>!\n </h2>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tWithFallback(\n t,\n 'dashboard.welcome.subtitle',\n \"Here's what's happening across your workspaces today.\"\n )}\n </p>\n </GlassCard>\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3\">\n <StatCard\n icon={<Building2 className=\"size-4\" />}\n label={tWithFallback(t, 'dashboard.welcome.statWorkspaces', 'Workspaces')}\n value={totalWorkspaces}\n />\n <StatCard\n icon={<FolderKanban className=\"size-4\" />}\n label={tWithFallback(t, 'dashboard.welcome.statProjects', 'Projects')}\n value={totalProjects}\n />\n <StatCard\n icon={<Users className=\"size-4\" />}\n label={tWithFallback(t, 'dashboard.welcome.statGroups', 'Groups')}\n value={totalGroups}\n />\n <StatCard\n icon={<Activity className=\"size-4\" />}\n label={tWithFallback(t, 'dashboard.welcome.statRecentActivity', 'Recent Activity')}\n value={activityCount}\n />\n </div>\n </div>\n </WidgetWrapper>\n );\n}\n"],"mappings":";;;;;;;AAOA,SAAS,EAAS,EAChB,SACA,UACA,YAKC;AACD,QACE,kBAAC,GAAD;EAAW,WAAU;EAAS,WAAU;YACtC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAuD;IAAW,CAAA,EACjF,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;IAAG,WAAU;cAAqD;IAAU,CAAA,EAC5E,kBAAC,KAAD;IAAG,WAAU;cAA2B;IAAU,CAAA,CAC9C,EAAA,CAAA,CACF;;EACI,CAAA;;AAIhB,SAAgB,EAAc,EAAE,kBAAe,eAAY,gBAA0B;CACnF,IAAM,EAAE,SAAM,GAAS,EACjB,IAAO,GAAY,aACnB,IACJ,GAAM,aACN,GAAM,MAAM,MAAM,IAAI,CAAC,MACvB,EAAc,GAAG,kCAAkC,QAAQ,EACvD,qBAAO,IAAI,MAAM,EAAC,UAAU,EAC5B,IACJ,IAAO,KACH,EAAc,GAAG,qCAAqC,eAAe,GACrE,IAAO,KACL,EAAc,GAAG,uCAAuC,iBAAiB,GACzE,EAAc,GAAG,qCAAqC,eAAe,EAEvE,IAAkB,GAAe,WAAW,SAAS,GACrD,IAAgB,GAAe,SAAS,SAAS,GACjD,IAAc,GAAe,OAAO,cAAc,GAClD,IAAgB,GAAe,iBAAiB,UAAU;AAEhE,QACE,kBAAC,GAAD;EACE,OAAO,EAAc,GAAG,2BAA2B,YAAY;EAC/D,MAAM,kBAAC,GAAD,EAAiB,WAAU,UAAW,CAAA;EAC5C,MAAK;EACM;YAEX,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAW,WAAU;IAAQ,MAAA;IAAK,WAAU;cAA5C,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd;MACG;MAAS;MAAE,kBAAC,GAAD,EAAA,UAAe,GAAyB,CAAA;;MACjD;QACL,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,GACA,8BACA,wDACD;KACC,CAAA,CACM;OACZ,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;MACtC,OAAO,EAAc,GAAG,oCAAoC,aAAa;MACzE,OAAO;MACP,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA;MACzC,OAAO,EAAc,GAAG,kCAAkC,WAAW;MACrE,OAAO;MACP,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;MAClC,OAAO,EAAc,GAAG,gCAAgC,SAAS;MACjE,OAAO;MACP,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;MACrC,OAAO,EAAc,GAAG,wCAAwC,kBAAkB;MAClF,OAAO;MACP,CAAA;KACE;MACF;;EACQ,CAAA"}
1
+ {"version":3,"file":"WelcomeWidget.js","names":[],"sources":["../../../../src/pages/dashboard/widgets/WelcomeWidget.tsx"],"sourcesContent":["import { LayoutDashboard, Building2, FolderKanban, Users, Activity } from 'lucide-react';\nimport { GlassCard, GradientText } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { WidgetWrapper } from '../WidgetWrapper';\nimport { tWithFallback } from '../../../utils/i18n';\nimport type { WidgetProps } from '../types';\n\nfunction StatCard({\n icon,\n label,\n value,\n onClick,\n}: {\n icon: React.ReactNode;\n label: string;\n value: number | string;\n onClick?: () => void;\n}) {\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n };\n\n return (\n <GlassCard\n treatment=\"metric\"\n className={`p-3${onClick ? ' cursor-pointer hover:shadow-[var(--shadow-pop-hover)] hover:border-border-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]' : ''}`}\n onClick={onClick}\n onKeyDown={onClick ? handleKeyDown : undefined}\n role={onClick ? 'button' : undefined}\n tabIndex={onClick ? 0 : undefined}\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"rounded-md bg-action-primary-bg/10 p-2 text-primary\">{icon}</div>\n <div>\n <p className=\"text-2xl font-bold text-text-primary tabular-nums\">{value}</p>\n <p className=\"text-xs text-text-muted\">{label}</p>\n </div>\n </div>\n </GlassCard>\n );\n}\n\nexport function WelcomeWidget({ workspaceData, globalData, isLoading, navigate }: WidgetProps) {\n const { t } = useI18n();\n const user = globalData?.currentUser;\n const firstName =\n user?.firstName ||\n user?.name?.split(' ')[0] ||\n tWithFallback(t, 'dashboard.welcome.fallbackName', 'there');\n const hour = new Date().getHours();\n const greeting =\n hour < 12\n ? tWithFallback(t, 'dashboard.welcome.greetingMorning', 'Good morning')\n : hour < 18\n ? tWithFallback(t, 'dashboard.welcome.greetingAfternoon', 'Good afternoon')\n : tWithFallback(t, 'dashboard.welcome.greetingEvening', 'Good evening');\n\n const totalWorkspaces = workspaceData?.workspaces.total ?? 0;\n const totalProjects = workspaceData?.projects.total ?? 0;\n const totalGroups = workspaceData?.groups.totalCount ?? 0;\n const activityCount = workspaceData?.recentActivities.length ?? 0;\n\n return (\n <WidgetWrapper\n title={tWithFallback(t, 'dashboard.welcome.title', 'Dashboard')}\n icon={<LayoutDashboard className=\"size-4\" />}\n size=\"large\"\n isLoading={isLoading}\n >\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" glow className=\"relative overflow-hidden p-4\">\n <h2 className=\"text-xl font-bold text-text-primary\">\n {greeting}, <GradientText>{firstName}</GradientText>!\n </h2>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tWithFallback(\n t,\n 'dashboard.welcome.subtitle',\n \"Here's what's happening across your workspaces today.\"\n )}\n </p>\n </GlassCard>\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3\">\n <StatCard\n icon={<Building2 className=\"size-4\" />}\n label={tWithFallback(t, 'dashboard.welcome.statWorkspaces', 'Workspaces')}\n value={totalWorkspaces}\n onClick={() => navigate('/workspaces/list')}\n />\n <StatCard\n icon={<FolderKanban className=\"size-4\" />}\n label={tWithFallback(t, 'dashboard.welcome.statProjects', 'Projects')}\n value={totalProjects}\n onClick={() => navigate('/workspaces/projects')}\n />\n <StatCard\n icon={<Users className=\"size-4\" />}\n label={tWithFallback(t, 'dashboard.welcome.statGroups', 'Groups')}\n value={totalGroups}\n onClick={() => navigate('/groups')}\n />\n <StatCard\n icon={<Activity className=\"size-4\" />}\n label={tWithFallback(t, 'dashboard.welcome.statRecentActivity', 'Recent Activity')}\n value={activityCount}\n onClick={() => navigate('/workspaces/activity')}\n />\n </div>\n </div>\n </WidgetWrapper>\n );\n}\n"],"mappings":";;;;;;;AAOA,SAAS,EAAS,EAChB,SACA,UACA,UACA,cAMC;AAQD,QACE,kBAAC,GAAD;EACE,WAAU;EACV,WAAW,MAAM,IAAU,mLAAmL;EACrM;EACT,WAAW,KAZQ,MAA2B;AAChD,IAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EAClB,KAAW;MAS0B,KAAA;EACrC,MAAM,IAAU,WAAW,KAAA;EAC3B,UAAU,IAAU,IAAI,KAAA;YAExB,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAuD;IAAW,CAAA,EACjF,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;IAAG,WAAU;cAAqD;IAAU,CAAA,EAC5E,kBAAC,KAAD;IAAG,WAAU;cAA2B;IAAU,CAAA,CAC9C,EAAA,CAAA,CACF;;EACI,CAAA;;AAIhB,SAAgB,EAAc,EAAE,kBAAe,eAAY,cAAW,eAAyB;CAC7F,IAAM,EAAE,SAAM,GAAS,EACjB,IAAO,GAAY,aACnB,IACJ,GAAM,aACN,GAAM,MAAM,MAAM,IAAI,CAAC,MACvB,EAAc,GAAG,kCAAkC,QAAQ,EACvD,qBAAO,IAAI,MAAM,EAAC,UAAU,EAC5B,IACJ,IAAO,KACH,EAAc,GAAG,qCAAqC,eAAe,GACrE,IAAO,KACL,EAAc,GAAG,uCAAuC,iBAAiB,GACzE,EAAc,GAAG,qCAAqC,eAAe,EAEvE,IAAkB,GAAe,WAAW,SAAS,GACrD,IAAgB,GAAe,SAAS,SAAS,GACjD,IAAc,GAAe,OAAO,cAAc,GAClD,IAAgB,GAAe,iBAAiB,UAAU;AAEhE,QACE,kBAAC,GAAD;EACE,OAAO,EAAc,GAAG,2BAA2B,YAAY;EAC/D,MAAM,kBAAC,GAAD,EAAiB,WAAU,UAAW,CAAA;EAC5C,MAAK;EACM;YAEX,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAW,WAAU;IAAQ,MAAA;IAAK,WAAU;cAA5C,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd;MACG;MAAS;MAAE,kBAAC,GAAD,EAAA,UAAe,GAAyB,CAAA;;MACjD;QACL,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,GACA,8BACA,wDACD;KACC,CAAA,CACM;OACZ,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;MACtC,OAAO,EAAc,GAAG,oCAAoC,aAAa;MACzE,OAAO;MACP,eAAe,EAAS,mBAAmB;MAC3C,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA;MACzC,OAAO,EAAc,GAAG,kCAAkC,WAAW;MACrE,OAAO;MACP,eAAe,EAAS,uBAAuB;MAC/C,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;MAClC,OAAO,EAAc,GAAG,gCAAgC,SAAS;MACjE,OAAO;MACP,eAAe,EAAS,UAAU;MAClC,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;MACrC,OAAO,EAAc,GAAG,wCAAwC,kBAAkB;MAClF,OAAO;MACP,eAAe,EAAS,uBAAuB;MAC/C,CAAA;KACE;MACF;;EACQ,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-workspaces",
3
- "version": "2026.830.2",
3
+ "version": "2026.917.1",
4
4
  "description": "Workspaces microfrontend for Burdenoff products",
5
5
  "type": "module",
6
6
  "files": [