@burdenoff/microfe-ecoimpacthub 2026.720.4 → 2026.720.5

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.
@@ -0,0 +1,11 @@
1
+ import { FC } from 'react';
2
+ import { DashboardWidgetId, UseDashboardLayoutResult } from '../hooks/useDashboardLayout';
3
+
4
+ interface DashboardCustomizerDialogProps {
5
+ open: boolean;
6
+ onOpenChange: (open: boolean) => void;
7
+ layout: UseDashboardLayoutResult;
8
+ }
9
+ export declare const DashboardCustomizerDialog: FC<DashboardCustomizerDialogProps>;
10
+ export type { DashboardWidgetId };
11
+ //# sourceMappingURL=DashboardCustomizerDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DashboardCustomizerDialog.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/components/DashboardCustomizerDialog.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,KAAK,EAAE,EAAY,MAAM,OAAO,CAAC;AAe1C,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC9B,MAAM,6BAA6B,CAAC;AAErC,UAAU,8BAA8B;IACtC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,MAAM,EAAE,wBAAwB,CAAC;CAClC;AAED,eAAO,MAAM,yBAAyB,EAAE,EAAE,CAAC,8BAA8B,CAwMxE,CAAC;AAEF,YAAY,EAAE,iBAAiB,EAAE,CAAC"}
@@ -0,0 +1,182 @@
1
+ import { DASHBOARD_WIDGET_DEFS as e } from "../hooks/useDashboardLayout.js";
2
+ import { useState as t } from "react";
3
+ import { ArrowDown as n, ArrowUp as r, Plus as i, StickyNote as a, Trash2 as o } from "lucide-react";
4
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
5
+ import { Button as l, Checkbox as u, Input as d, Label as f, ResponsiveDialog as p, ResponsiveDialogContent as m, ResponsiveDialogDescription as h, ResponsiveDialogFooter as g, ResponsiveDialogHeader as _, ResponsiveDialogTitle as v, Textarea as y } from "@burdenoff/fe-libs/ui";
6
+ //#region src/ecoimpacthub/components/DashboardCustomizerDialog.tsx
7
+ var b = ({ open: b, onOpenChange: x, layout: S }) => {
8
+ let { layout: C, isWidgetVisible: w, toggleWidget: T, moveWidget: E, addNote: D, removeNote: O, resetToDefault: k } = S, [A, j] = t(""), [M, N] = t(""), P = C.widgetOrder.map((t) => e.find((e) => e.id === t)).filter((e) => !!e), F = e.filter((e) => !w(e.id));
9
+ return /* @__PURE__ */ s(p, {
10
+ open: b,
11
+ onOpenChange: x,
12
+ children: /* @__PURE__ */ c(m, {
13
+ className: "sm:max-w-lg",
14
+ children: [
15
+ /* @__PURE__ */ c(_, { children: [/* @__PURE__ */ s(v, { children: "Customize dashboard" }), /* @__PURE__ */ s(h, { children: "Show, hide, and reorder widgets, or add a custom note. Changes save automatically for you on this workspace." })] }),
16
+ /* @__PURE__ */ c("div", {
17
+ className: "grid gap-5 py-2",
18
+ children: [
19
+ /* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("p", {
20
+ className: "mb-2 text-xs font-medium uppercase tracking-wide text-text-tertiary",
21
+ children: "Visible widgets"
22
+ }), /* @__PURE__ */ c("ul", {
23
+ className: "space-y-1.5",
24
+ children: [P.map((e, t) => /* @__PURE__ */ c("li", {
25
+ className: "flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2",
26
+ children: [
27
+ /* @__PURE__ */ s(u, {
28
+ checked: !0,
29
+ onCheckedChange: () => T(e.id),
30
+ "aria-label": `Hide ${e.label}`
31
+ }),
32
+ /* @__PURE__ */ c("div", {
33
+ className: "min-w-0 flex-1",
34
+ children: [/* @__PURE__ */ s("p", {
35
+ className: "text-sm font-medium text-text-primary",
36
+ children: e.label
37
+ }), /* @__PURE__ */ s("p", {
38
+ className: "truncate text-xs text-text-tertiary",
39
+ children: e.description
40
+ })]
41
+ }),
42
+ /* @__PURE__ */ c("div", {
43
+ className: "flex shrink-0 items-center gap-1",
44
+ children: [/* @__PURE__ */ s(l, {
45
+ type: "button",
46
+ variant: "ghost",
47
+ size: "icon",
48
+ className: "h-7 w-7",
49
+ disabled: t === 0,
50
+ onClick: () => E(e.id, "up"),
51
+ "aria-label": `Move ${e.label} up`,
52
+ children: /* @__PURE__ */ s(r, { className: "h-3.5 w-3.5" })
53
+ }), /* @__PURE__ */ s(l, {
54
+ type: "button",
55
+ variant: "ghost",
56
+ size: "icon",
57
+ className: "h-7 w-7",
58
+ disabled: t === P.length - 1,
59
+ onClick: () => E(e.id, "down"),
60
+ "aria-label": `Move ${e.label} down`,
61
+ children: /* @__PURE__ */ s(n, { className: "h-3.5 w-3.5" })
62
+ })]
63
+ })
64
+ ]
65
+ }, e.id)), P.length === 0 && /* @__PURE__ */ s("li", {
66
+ className: "rounded-lg border border-dashed border-border-seam px-3 py-4 text-center text-xs text-text-tertiary",
67
+ children: "All widgets hidden — add one back below."
68
+ })]
69
+ })] }),
70
+ F.length > 0 && /* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("p", {
71
+ className: "mb-2 text-xs font-medium uppercase tracking-wide text-text-tertiary",
72
+ children: "Hidden widgets"
73
+ }), /* @__PURE__ */ s("ul", {
74
+ className: "space-y-1.5",
75
+ children: F.map((e) => /* @__PURE__ */ c("li", {
76
+ className: "flex items-center gap-2.5 rounded-lg border border-dashed border-border-seam px-3 py-2 opacity-70",
77
+ children: [/* @__PURE__ */ s(u, {
78
+ checked: !1,
79
+ onCheckedChange: () => T(e.id),
80
+ "aria-label": `Show ${e.label}`
81
+ }), /* @__PURE__ */ c("div", {
82
+ className: "min-w-0 flex-1",
83
+ children: [/* @__PURE__ */ s("p", {
84
+ className: "text-sm font-medium text-text-primary",
85
+ children: e.label
86
+ }), /* @__PURE__ */ s("p", {
87
+ className: "truncate text-xs text-text-tertiary",
88
+ children: e.description
89
+ })]
90
+ })]
91
+ }, e.id))
92
+ })] }),
93
+ /* @__PURE__ */ c("div", { children: [
94
+ /* @__PURE__ */ s("p", {
95
+ className: "mb-2 text-xs font-medium uppercase tracking-wide text-text-tertiary",
96
+ children: "Custom notes"
97
+ }),
98
+ C.notes.length > 0 && /* @__PURE__ */ s("ul", {
99
+ className: "mb-2.5 space-y-1.5",
100
+ children: C.notes.map((e) => /* @__PURE__ */ c("li", {
101
+ className: "flex items-start gap-2.5 rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2",
102
+ children: [
103
+ /* @__PURE__ */ s(a, { className: "mt-0.5 h-4 w-4 shrink-0 text-accent" }),
104
+ /* @__PURE__ */ c("div", {
105
+ className: "min-w-0 flex-1",
106
+ children: [e.title && /* @__PURE__ */ s("p", {
107
+ className: "truncate text-sm font-medium text-text-primary",
108
+ children: e.title
109
+ }), e.body && /* @__PURE__ */ s("p", {
110
+ className: "line-clamp-2 text-xs text-text-tertiary",
111
+ children: e.body
112
+ })]
113
+ }),
114
+ /* @__PURE__ */ s(l, {
115
+ type: "button",
116
+ variant: "ghost",
117
+ size: "icon",
118
+ className: "h-7 w-7 shrink-0",
119
+ onClick: () => O(e.id),
120
+ "aria-label": "Remove note",
121
+ children: /* @__PURE__ */ s(o, { className: "h-3.5 w-3.5" })
122
+ })
123
+ ]
124
+ }, e.id))
125
+ }),
126
+ /* @__PURE__ */ c("div", {
127
+ className: "grid gap-1.5",
128
+ children: [/* @__PURE__ */ s(f, {
129
+ htmlFor: "dashboard-note-title",
130
+ children: "Note title"
131
+ }), /* @__PURE__ */ s(d, {
132
+ id: "dashboard-note-title",
133
+ value: A,
134
+ onChange: (e) => j(e.target.value),
135
+ placeholder: "e.g. Q3 audit reminder"
136
+ })]
137
+ }),
138
+ /* @__PURE__ */ c("div", {
139
+ className: "mt-2 grid gap-1.5",
140
+ children: [/* @__PURE__ */ s(f, {
141
+ htmlFor: "dashboard-note-body",
142
+ children: "Note"
143
+ }), /* @__PURE__ */ s(y, {
144
+ id: "dashboard-note-body",
145
+ value: M,
146
+ onChange: (e) => N(e.target.value),
147
+ placeholder: "Optional detail for this note.",
148
+ rows: 2
149
+ })]
150
+ }),
151
+ /* @__PURE__ */ c(l, {
152
+ type: "button",
153
+ variant: "outline",
154
+ size: "sm",
155
+ className: "mt-2",
156
+ onClick: () => {
157
+ !A.trim() && !M.trim() || (D(A, M), j(""), N(""));
158
+ },
159
+ disabled: !A.trim() && !M.trim(),
160
+ children: [/* @__PURE__ */ s(i, { className: "mr-1.5 h-3.5 w-3.5" }), "Add note"]
161
+ })
162
+ ] })
163
+ ]
164
+ }),
165
+ /* @__PURE__ */ c(g, { children: [/* @__PURE__ */ s(l, {
166
+ type: "button",
167
+ variant: "ghost",
168
+ onClick: k,
169
+ children: "Reset to default"
170
+ }), /* @__PURE__ */ s(l, {
171
+ type: "button",
172
+ onClick: () => x(!1),
173
+ children: "Done"
174
+ })] })
175
+ ]
176
+ })
177
+ });
178
+ };
179
+ //#endregion
180
+ export { b as DashboardCustomizerDialog };
181
+
182
+ //# sourceMappingURL=DashboardCustomizerDialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DashboardCustomizerDialog.js","names":[],"sources":["../../../src/ecoimpacthub/components/DashboardCustomizerDialog.tsx"],"sourcesContent":["/**\n * Dashboard customizer — widget palette for the \"Create dashboard\" CTA (BOFF-3943).\n * @module ecoimpacthub/components\n *\n * Lets a user show/hide/reorder the built-in dashboard widgets and add\n * freeform note widgets. Persisted per user/workspace via\n * {@link useDashboardLayout} (client-side; no backend model exists yet — see\n * that hook's doc comment for why).\n */\nimport { type FC, useState } from 'react';\nimport { ArrowDown, ArrowUp, Plus, StickyNote, Trash2 } from 'lucide-react';\nimport {\n Button,\n Checkbox,\n Input,\n Label,\n Textarea,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n DASHBOARD_WIDGET_DEFS,\n type DashboardWidgetId,\n type UseDashboardLayoutResult,\n} from '../hooks/useDashboardLayout';\n\ninterface DashboardCustomizerDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n layout: UseDashboardLayoutResult;\n}\n\nexport const DashboardCustomizerDialog: FC<DashboardCustomizerDialogProps> = ({\n open,\n onOpenChange,\n layout,\n}) => {\n const {\n layout: current,\n isWidgetVisible,\n toggleWidget,\n moveWidget,\n addNote,\n removeNote,\n resetToDefault,\n } = layout;\n const [noteTitle, setNoteTitle] = useState('');\n const [noteBody, setNoteBody] = useState('');\n\n const orderedWidgetDefs = current.widgetOrder\n .map((id) => DASHBOARD_WIDGET_DEFS.find((w) => w.id === id))\n .filter((w): w is (typeof DASHBOARD_WIDGET_DEFS)[number] => Boolean(w));\n const hiddenWidgetDefs = DASHBOARD_WIDGET_DEFS.filter((w) => !isWidgetVisible(w.id));\n\n const handleAddNote = () => {\n if (!noteTitle.trim() && !noteBody.trim()) return;\n addNote(noteTitle, noteBody);\n setNoteTitle('');\n setNoteBody('');\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Customize dashboard</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Show, hide, and reorder widgets, or add a custom note. Changes save automatically for\n you on this workspace.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-5 py-2\">\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n Visible widgets\n </p>\n <ul className=\"space-y-1.5\">\n {orderedWidgetDefs.map((widget, index) => (\n <li\n key={widget.id}\n className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\"\n >\n <Checkbox\n checked\n onCheckedChange={() => toggleWidget(widget.id)}\n aria-label={`Hide ${widget.label}`}\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{widget.label}</p>\n <p className=\"truncate text-xs text-text-tertiary\">{widget.description}</p>\n </div>\n <div className=\"flex shrink-0 items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-7 w-7\"\n disabled={index === 0}\n onClick={() => moveWidget(widget.id, 'up')}\n aria-label={`Move ${widget.label} up`}\n >\n <ArrowUp className=\"h-3.5 w-3.5\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-7 w-7\"\n disabled={index === orderedWidgetDefs.length - 1}\n onClick={() => moveWidget(widget.id, 'down')}\n aria-label={`Move ${widget.label} down`}\n >\n <ArrowDown className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </li>\n ))}\n {orderedWidgetDefs.length === 0 && (\n <li className=\"rounded-lg border border-dashed border-border-seam px-3 py-4 text-center text-xs text-text-tertiary\">\n All widgets hidden — add one back below.\n </li>\n )}\n </ul>\n </div>\n\n {hiddenWidgetDefs.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n Hidden widgets\n </p>\n <ul className=\"space-y-1.5\">\n {hiddenWidgetDefs.map((widget) => (\n <li\n key={widget.id}\n className=\"flex items-center gap-2.5 rounded-lg border border-dashed border-border-seam px-3 py-2 opacity-70\"\n >\n <Checkbox\n checked={false}\n onCheckedChange={() => toggleWidget(widget.id)}\n aria-label={`Show ${widget.label}`}\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{widget.label}</p>\n <p className=\"truncate text-xs text-text-tertiary\">{widget.description}</p>\n </div>\n </li>\n ))}\n </ul>\n </div>\n )}\n\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n Custom notes\n </p>\n {current.notes.length > 0 && (\n <ul className=\"mb-2.5 space-y-1.5\">\n {current.notes.map((note) => (\n <li\n key={note.id}\n className=\"flex items-start gap-2.5 rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\"\n >\n <StickyNote className=\"mt-0.5 h-4 w-4 shrink-0 text-accent\" />\n <div className=\"min-w-0 flex-1\">\n {note.title && (\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {note.title}\n </p>\n )}\n {note.body && (\n <p className=\"line-clamp-2 text-xs text-text-tertiary\">{note.body}</p>\n )}\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-7 w-7 shrink-0\"\n onClick={() => removeNote(note.id)}\n aria-label=\"Remove note\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </li>\n ))}\n </ul>\n )}\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"dashboard-note-title\">Note title</Label>\n <Input\n id=\"dashboard-note-title\"\n value={noteTitle}\n onChange={(e) => setNoteTitle(e.target.value)}\n placeholder=\"e.g. Q3 audit reminder\"\n />\n </div>\n <div className=\"mt-2 grid gap-1.5\">\n <Label htmlFor=\"dashboard-note-body\">Note</Label>\n <Textarea\n id=\"dashboard-note-body\"\n value={noteBody}\n onChange={(e) => setNoteBody(e.target.value)}\n placeholder=\"Optional detail for this note.\"\n rows={2}\n />\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-2\"\n onClick={handleAddNote}\n disabled={!noteTitle.trim() && !noteBody.trim()}\n >\n <Plus className=\"mr-1.5 h-3.5 w-3.5\" />\n Add note\n </Button>\n </div>\n </div>\n\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={resetToDefault}>\n Reset to default\n </Button>\n <Button type=\"button\" onClick={() => onOpenChange(false)}>\n Done\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n};\n\nexport type { DashboardWidgetId };\n"],"mappings":"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"}
@@ -7,4 +7,5 @@ export * from './useCursorPager';
7
7
  export * from './useEcoFiles';
8
8
  export * from './useEcoComments';
9
9
  export * from './useEcoTags';
10
+ export * from './useDashboardLayout';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/hooks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/hooks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC"}
@@ -1,3 +1,4 @@
1
+ import "./useDashboardLayout.js";
1
2
  import "./useEcoImpactHubApi.js";
2
3
  import "./useCursorPager.js";
3
4
  import "./useEcoFiles.js";
@@ -0,0 +1,29 @@
1
+ /** Every built-in dashboard widget a user can show/hide/reorder. */
2
+ export type DashboardWidgetId = 'kpis' | 'carbonByScope' | 'recentBreaches' | 'recentObservations' | 'conservationStatus';
3
+ export declare const DASHBOARD_WIDGET_DEFS: ReadonlyArray<{
4
+ id: DashboardWidgetId;
5
+ label: string;
6
+ description: string;
7
+ }>;
8
+ /** A user-authored freeform note widget — the one net-new widget type. */
9
+ export interface DashboardNote {
10
+ id: string;
11
+ title: string;
12
+ body: string;
13
+ }
14
+ export interface DashboardLayout {
15
+ /** Built-in widget ids, in display order. Hidden widgets are simply absent. */
16
+ widgetOrder: DashboardWidgetId[];
17
+ notes: DashboardNote[];
18
+ }
19
+ export interface UseDashboardLayoutResult {
20
+ layout: DashboardLayout;
21
+ isWidgetVisible: (id: DashboardWidgetId) => boolean;
22
+ toggleWidget: (id: DashboardWidgetId) => void;
23
+ moveWidget: (id: DashboardWidgetId, direction: 'up' | 'down') => void;
24
+ addNote: (title: string, body: string) => void;
25
+ removeNote: (id: string) => void;
26
+ resetToDefault: () => void;
27
+ }
28
+ export declare function useDashboardLayout(workspaceId: string | undefined, userId: string | undefined): UseDashboardLayoutResult;
29
+ //# sourceMappingURL=useDashboardLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDashboardLayout.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/hooks/useDashboardLayout.ts"],"names":[],"mappings":"AAmBA,oEAAoE;AACpE,MAAM,MAAM,iBAAiB,GAC3B,MAAM,GAAG,eAAe,GAAG,gBAAgB,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAE5F,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC;IAChD,EAAE,EAAE,iBAAiB,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB,CA0BA,CAAC;AAEF,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AAWD,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,eAAe,CAAC;IACxB,eAAe,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC;IACpD,YAAY,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC9C,UAAU,EAAE,CAAC,EAAE,EAAE,iBAAiB,EAAE,SAAS,EAAE,IAAI,GAAG,MAAM,KAAK,IAAI,CAAC;IACtE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,wBAAwB,CAiF1B"}
@@ -0,0 +1,95 @@
1
+ import { useCallback as e, useMemo as t } from "react";
2
+ import { useLocalStorage as n } from "@burdenoff/fe-libs/shared/hooks";
3
+ //#region src/ecoimpacthub/hooks/useDashboardLayout.ts
4
+ var r = [
5
+ {
6
+ id: "kpis",
7
+ label: "KPI summary",
8
+ description: "Sites, species, projects, footprint, sensors, breaches."
9
+ },
10
+ {
11
+ id: "carbonByScope",
12
+ label: "Carbon footprint by scope",
13
+ description: "Scope 1 / 2 / 3 emissions chart."
14
+ },
15
+ {
16
+ id: "recentBreaches",
17
+ label: "Recent breaches",
18
+ description: "Latest threshold-breach measurements."
19
+ },
20
+ {
21
+ id: "recentObservations",
22
+ label: "Recent field observations",
23
+ description: "Latest species sightings."
24
+ },
25
+ {
26
+ id: "conservationStatus",
27
+ label: "Conservation status",
28
+ description: "Tracked species by IUCN-style status."
29
+ }
30
+ ], i = {
31
+ widgetOrder: r.map((e) => e.id),
32
+ notes: []
33
+ };
34
+ function a(e, t) {
35
+ return `eih.dashboard.layout.v1.${e ?? "no-workspace"}.${t ?? "no-user"}`;
36
+ }
37
+ function o(r, o) {
38
+ let [s, c, l] = n(a(r, o), i), u = e((e) => s.widgetOrder.includes(e), [s.widgetOrder]), d = e((e) => {
39
+ c((t) => {
40
+ let n = t.widgetOrder.includes(e);
41
+ return {
42
+ ...t,
43
+ widgetOrder: n ? t.widgetOrder.filter((t) => t !== e) : [...t.widgetOrder, e]
44
+ };
45
+ });
46
+ }, [c]), f = e((e, t) => {
47
+ c((n) => {
48
+ let r = n.widgetOrder.indexOf(e);
49
+ if (r === -1) return n;
50
+ let i = t === "up" ? r - 1 : r + 1;
51
+ if (i < 0 || i >= n.widgetOrder.length) return n;
52
+ let a = [...n.widgetOrder], [o] = a.splice(r, 1);
53
+ return a.splice(i, 0, o), {
54
+ ...n,
55
+ widgetOrder: a
56
+ };
57
+ });
58
+ }, [c]), p = e((e, t) => {
59
+ let n = e.trim(), r = t.trim();
60
+ !n && !r || c((e) => ({
61
+ ...e,
62
+ notes: [...e.notes, {
63
+ id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
64
+ title: n,
65
+ body: r
66
+ }]
67
+ }));
68
+ }, [c]), m = e((e) => {
69
+ c((t) => ({
70
+ ...t,
71
+ notes: t.notes.filter((t) => t.id !== e)
72
+ }));
73
+ }, [c]);
74
+ return t(() => ({
75
+ layout: s,
76
+ isWidgetVisible: u,
77
+ toggleWidget: d,
78
+ moveWidget: f,
79
+ addNote: p,
80
+ removeNote: m,
81
+ resetToDefault: l
82
+ }), [
83
+ s,
84
+ u,
85
+ d,
86
+ f,
87
+ p,
88
+ m,
89
+ l
90
+ ]);
91
+ }
92
+ //#endregion
93
+ export { r as DASHBOARD_WIDGET_DEFS, o as useDashboardLayout };
94
+
95
+ //# sourceMappingURL=useDashboardLayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDashboardLayout.js","names":[],"sources":["../../../src/ecoimpacthub/hooks/useDashboardLayout.ts"],"sourcesContent":["/**\n * Dashboard layout customization (BOFF-3943).\n * @module ecoimpacthub/hooks\n *\n * Before this hook the dashboard was a fixed six-widget read-only layout with\n * no \"Create dashboard\" CTA, no widget palette, and no add/remove/reorder\n * state — the mock (01-dashboard.jpg) shows a customizable dashboard the\n * product never built.\n *\n * Persistence: no platform-wide dashboard-preference store exists yet (only\n * per-product Prisma models would), so this is a per-user, per-workspace\n * client preference stored via `useLocalStorage` — the same mechanism\n * `@burdenoff/fe-libs/shared` already ships for this class of UI state.\n * Uncustomized users see the exact same six widgets, in the same order, as\n * before this change (the stored layout is additive, not a migration).\n */\nimport { useCallback, useMemo } from 'react';\nimport { useLocalStorage } from '@burdenoff/fe-libs/shared/hooks';\n\n/** Every built-in dashboard widget a user can show/hide/reorder. */\nexport type DashboardWidgetId =\n 'kpis' | 'carbonByScope' | 'recentBreaches' | 'recentObservations' | 'conservationStatus';\n\nexport const DASHBOARD_WIDGET_DEFS: ReadonlyArray<{\n id: DashboardWidgetId;\n label: string;\n description: string;\n}> = [\n {\n id: 'kpis',\n label: 'KPI summary',\n description: 'Sites, species, projects, footprint, sensors, breaches.',\n },\n {\n id: 'carbonByScope',\n label: 'Carbon footprint by scope',\n description: 'Scope 1 / 2 / 3 emissions chart.',\n },\n {\n id: 'recentBreaches',\n label: 'Recent breaches',\n description: 'Latest threshold-breach measurements.',\n },\n {\n id: 'recentObservations',\n label: 'Recent field observations',\n description: 'Latest species sightings.',\n },\n {\n id: 'conservationStatus',\n label: 'Conservation status',\n description: 'Tracked species by IUCN-style status.',\n },\n];\n\n/** A user-authored freeform note widget — the one net-new widget type. */\nexport interface DashboardNote {\n id: string;\n title: string;\n body: string;\n}\n\nexport interface DashboardLayout {\n /** Built-in widget ids, in display order. Hidden widgets are simply absent. */\n widgetOrder: DashboardWidgetId[];\n notes: DashboardNote[];\n}\n\nconst DEFAULT_LAYOUT: DashboardLayout = {\n widgetOrder: DASHBOARD_WIDGET_DEFS.map((w) => w.id),\n notes: [],\n};\n\nfunction layoutStorageKey(workspaceId: string | undefined, userId: string | undefined): string {\n return `eih.dashboard.layout.v1.${workspaceId ?? 'no-workspace'}.${userId ?? 'no-user'}`;\n}\n\nexport interface UseDashboardLayoutResult {\n layout: DashboardLayout;\n isWidgetVisible: (id: DashboardWidgetId) => boolean;\n toggleWidget: (id: DashboardWidgetId) => void;\n moveWidget: (id: DashboardWidgetId, direction: 'up' | 'down') => void;\n addNote: (title: string, body: string) => void;\n removeNote: (id: string) => void;\n resetToDefault: () => void;\n}\n\nexport function useDashboardLayout(\n workspaceId: string | undefined,\n userId: string | undefined\n): UseDashboardLayoutResult {\n const [layout, setLayout, resetToDefault] = useLocalStorage<DashboardLayout>(\n layoutStorageKey(workspaceId, userId),\n DEFAULT_LAYOUT\n );\n\n const isWidgetVisible = useCallback(\n (id: DashboardWidgetId) => layout.widgetOrder.includes(id),\n [layout.widgetOrder]\n );\n\n const toggleWidget = useCallback(\n (id: DashboardWidgetId) => {\n setLayout((prev) => {\n const isVisible = prev.widgetOrder.includes(id);\n return {\n ...prev,\n widgetOrder: isVisible\n ? prev.widgetOrder.filter((w) => w !== id)\n : [...prev.widgetOrder, id],\n };\n });\n },\n [setLayout]\n );\n\n const moveWidget = useCallback(\n (id: DashboardWidgetId, direction: 'up' | 'down') => {\n setLayout((prev) => {\n const index = prev.widgetOrder.indexOf(id);\n if (index === -1) return prev;\n const targetIndex = direction === 'up' ? index - 1 : index + 1;\n if (targetIndex < 0 || targetIndex >= prev.widgetOrder.length) return prev;\n const next = [...prev.widgetOrder];\n const [moved] = next.splice(index, 1);\n next.splice(targetIndex, 0, moved);\n return { ...prev, widgetOrder: next };\n });\n },\n [setLayout]\n );\n\n const addNote = useCallback(\n (title: string, body: string) => {\n const trimmedTitle = title.trim();\n const trimmedBody = body.trim();\n if (!trimmedTitle && !trimmedBody) return;\n setLayout((prev) => ({\n ...prev,\n notes: [\n ...prev.notes,\n {\n id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n title: trimmedTitle,\n body: trimmedBody,\n },\n ],\n }));\n },\n [setLayout]\n );\n\n const removeNote = useCallback(\n (id: string) => {\n setLayout((prev) => ({ ...prev, notes: prev.notes.filter((n) => n.id !== id) }));\n },\n [setLayout]\n );\n\n return useMemo(\n () => ({\n layout,\n isWidgetVisible,\n toggleWidget,\n moveWidget,\n addNote,\n removeNote,\n resetToDefault,\n }),\n [layout, isWidgetVisible, toggleWidget, moveWidget, addNote, removeNote, resetToDefault]\n );\n}\n"],"mappings":";;;AAuBA,IAAa,IAIR;CACH;EACE,IAAI;EACJ,OAAO;EACP,aAAa;CACf;CACA;EACE,IAAI;EACJ,OAAO;EACP,aAAa;CACf;CACA;EACE,IAAI;EACJ,OAAO;EACP,aAAa;CACf;CACA;EACE,IAAI;EACJ,OAAO;EACP,aAAa;CACf;CACA;EACE,IAAI;EACJ,OAAO;EACP,aAAa;CACf;AACF,GAeM,IAAkC;CACtC,aAAa,EAAsB,KAAK,MAAM,EAAE,EAAE;CAClD,OAAO,CAAC;AACV;AAEA,SAAS,EAAiB,GAAiC,GAAoC;CAC7F,OAAO,2BAA2B,KAAe,eAAe,GAAG,KAAU;AAC/E;AAYA,SAAgB,EACd,GACA,GAC0B;CAC1B,IAAM,CAAC,GAAQ,GAAW,KAAkB,EAC1C,EAAiB,GAAa,CAAM,GACpC,CACF,GAEM,IAAkB,GACrB,MAA0B,EAAO,YAAY,SAAS,CAAE,GACzD,CAAC,EAAO,WAAW,CACrB,GAEM,IAAe,GAClB,MAA0B;EACzB,GAAW,MAAS;GAClB,IAAM,IAAY,EAAK,YAAY,SAAS,CAAE;GAC9C,OAAO;IACL,GAAG;IACH,aAAa,IACT,EAAK,YAAY,QAAQ,MAAM,MAAM,CAAE,IACvC,CAAC,GAAG,EAAK,aAAa,CAAE;GAC9B;EACF,CAAC;CACH,GACA,CAAC,CAAS,CACZ,GAEM,IAAa,GAChB,GAAuB,MAA6B;EACnD,GAAW,MAAS;GAClB,IAAM,IAAQ,EAAK,YAAY,QAAQ,CAAE;GACzC,IAAI,MAAU,IAAI,OAAO;GACzB,IAAM,IAAc,MAAc,OAAO,IAAQ,IAAI,IAAQ;GAC7D,IAAI,IAAc,KAAK,KAAe,EAAK,YAAY,QAAQ,OAAO;GACtE,IAAM,IAAO,CAAC,GAAG,EAAK,WAAW,GAC3B,CAAC,KAAS,EAAK,OAAO,GAAO,CAAC;GAEpC,OADA,EAAK,OAAO,GAAa,GAAG,CAAK,GAC1B;IAAE,GAAG;IAAM,aAAa;GAAK;EACtC,CAAC;CACH,GACA,CAAC,CAAS,CACZ,GAEM,IAAU,GACb,GAAe,MAAiB;EAC/B,IAAM,IAAe,EAAM,KAAK,GAC1B,IAAc,EAAK,KAAK;EAC1B,CAAC,KAAgB,CAAC,KACtB,GAAW,OAAU;GACnB,GAAG;GACH,OAAO,CACL,GAAG,EAAK,OACR;IACE,IAAI,QAAQ,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC;IAC/D,OAAO;IACP,MAAM;GACR,CACF;EACF,EAAE;CACJ,GACA,CAAC,CAAS,CACZ,GAEM,IAAa,GAChB,MAAe;EACd,GAAW,OAAU;GAAE,GAAG;GAAM,OAAO,EAAK,MAAM,QAAQ,MAAM,EAAE,OAAO,CAAE;EAAE,EAAE;CACjF,GACA,CAAC,CAAS,CACZ;CAEA,OAAO,SACE;EACL;EACA;EACA;EACA;EACA;EACA;EACA;CACF,IACA;EAAC;EAAQ;EAAiB;EAAc;EAAY;EAAS;EAAY;CAAc,CACzF;AACF"}
@@ -5,6 +5,13 @@
5
5
  * KPIs are aggregated client-side from the real `eco*` list endpoints plus the
6
6
  * `ecoCarbonTotalKg` aggregate (by scope). Widgets show recent field
7
7
  * observations and open threshold breaches. No mock data.
8
+ *
9
+ * Customizable (BOFF-3943): the "Customize dashboard" CTA opens a widget
10
+ * palette (show/hide/reorder + freeform notes), persisted per user/workspace
11
+ * via {@link useDashboardLayout}. Uncustomized users see the exact same
12
+ * layout as before this change — the widget order defaults to this file's
13
+ * original hardcoded order, and the carbon-by-scope / recent-breaches pair
14
+ * keeps its original side-by-side layout whenever they remain adjacent.
8
15
  */
9
16
  export declare function DashboardPage(): import("react/jsx-runtime").JSX.Element;
10
17
  //# sourceMappingURL=DashboardPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAkEH,wBAAgB,aAAa,4CAmd5B"}
1
+ {"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAuEH,wBAAgB,aAAa,4CAyhB5B"}