@burdenoff/microfe-ecoimpacthub 2026.704.1 → 2026.704.3

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.
Files changed (30) hide show
  1. package/dist/ecoimpacthub/components/EcoDiscussion.js +109 -0
  2. package/dist/ecoimpacthub/components/EcoDiscussion.js.map +1 -0
  3. package/dist/ecoimpacthub/components/EcoTags.js +149 -0
  4. package/dist/ecoimpacthub/components/EcoTags.js.map +1 -0
  5. package/dist/ecoimpacthub/components/EvidenceUpload.js +93 -0
  6. package/dist/ecoimpacthub/components/EvidenceUpload.js.map +1 -0
  7. package/dist/ecoimpacthub/components/FileLink.js +36 -0
  8. package/dist/ecoimpacthub/components/FileLink.js.map +1 -0
  9. package/dist/ecoimpacthub/components/PageLayout.js +2 -2
  10. package/dist/ecoimpacthub/components/PageLayout.js.map +1 -1
  11. package/dist/ecoimpacthub/hooks/index.js +3 -0
  12. package/dist/ecoimpacthub/hooks/useCursorPager.js +1 -1
  13. package/dist/ecoimpacthub/hooks/useCursorPager.js.map +1 -1
  14. package/dist/ecoimpacthub/hooks/useEcoComments.js +45 -0
  15. package/dist/ecoimpacthub/hooks/useEcoComments.js.map +1 -0
  16. package/dist/ecoimpacthub/hooks/useEcoFiles.js +81 -0
  17. package/dist/ecoimpacthub/hooks/useEcoFiles.js.map +1 -0
  18. package/dist/ecoimpacthub/hooks/useEcoImpactHubApi.js +169 -166
  19. package/dist/ecoimpacthub/hooks/useEcoImpactHubApi.js.map +1 -1
  20. package/dist/ecoimpacthub/hooks/useEcoTags.js +89 -0
  21. package/dist/ecoimpacthub/hooks/useEcoTags.js.map +1 -0
  22. package/dist/ecoimpacthub/pages/observations/ObservationsPage.js +124 -101
  23. package/dist/ecoimpacthub/pages/observations/ObservationsPage.js.map +1 -1
  24. package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js +153 -126
  25. package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js.map +1 -1
  26. package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js +241 -230
  27. package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js.map +1 -1
  28. package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js +260 -225
  29. package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js.map +1 -1
  30. package/package.json +1 -1
@@ -0,0 +1,109 @@
1
+ import { useEcoImpactHub as e } from "../context/EcoImpactHubContext.js";
2
+ import { formatDateTime as t } from "../utils/formatters.js";
3
+ import { ErrorView as n, LoadingView as r, Section as i } from "./StateViews.js";
4
+ import { EmptyState as a } from "./EmptyState.js";
5
+ import "../context/index.js";
6
+ import { useEcoComments as o, useEcoCreateComment as s } from "../hooks/useEcoComments.js";
7
+ import "../hooks/index.js";
8
+ import { useState as c } from "react";
9
+ import { MessageSquare as l, Send as u } from "lucide-react";
10
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
11
+ import { Button as p, Textarea as m } from "@burdenoff/fe-libs/ui";
12
+ //#region src/ecoimpacthub/components/EcoDiscussion.tsx
13
+ function h(e) {
14
+ if (!e) return "You";
15
+ let t = [e.firstName, e.lastName].filter(Boolean).join(" ").trim();
16
+ return e.name || t || e.username || e.email || "You";
17
+ }
18
+ function g(e) {
19
+ let t = e.trim();
20
+ return t ? t[0].toUpperCase() : "?";
21
+ }
22
+ function _({ entityType: _, entityId: v, title: y = "Discussion", description: b = "Coordinate review and verification with your team." }) {
23
+ let { currentUser: x } = e(), { data: S, isLoading: C, isError: w, refetch: T } = o(_, v), E = s(_, v), [D, O] = c(""), [k, A] = c(null), j = h(x), M = (e) => x?.id && e.authorId === x.id ? j : "Workspace member", N = async () => {
24
+ let e = D.trim();
25
+ if (e) {
26
+ A(null);
27
+ try {
28
+ await E.mutateAsync(e), O("");
29
+ } catch (e) {
30
+ A(e instanceof Error ? e.message : "Failed to post comment.");
31
+ }
32
+ }
33
+ }, P = S?.items ?? [];
34
+ return /* @__PURE__ */ d(i, {
35
+ title: y,
36
+ description: b,
37
+ children: /* @__PURE__ */ f("div", {
38
+ className: "rounded-xl border border-border-seam bg-bg-surface/40 p-4",
39
+ children: [/* @__PURE__ */ f("div", {
40
+ className: "flex gap-3",
41
+ children: [/* @__PURE__ */ d("div", {
42
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-sm font-semibold text-action-primary-bg",
43
+ children: g(j)
44
+ }), /* @__PURE__ */ f("div", {
45
+ className: "flex-1",
46
+ children: [/* @__PURE__ */ d(m, {
47
+ value: D,
48
+ onChange: (e) => O(e.target.value),
49
+ placeholder: "Add a comment for your team…",
50
+ rows: 2
51
+ }), /* @__PURE__ */ f("div", {
52
+ className: "mt-2 flex items-center justify-end gap-2",
53
+ children: [k && /* @__PURE__ */ d("span", {
54
+ className: "mr-auto text-xs text-status-error-text",
55
+ children: k
56
+ }), /* @__PURE__ */ f(p, {
57
+ size: "sm",
58
+ onClick: N,
59
+ disabled: !D.trim() || E.isPending,
60
+ children: [/* @__PURE__ */ d(u, { className: "mr-1.5 h-4 w-4" }), E.isPending ? "Posting…" : "Comment"]
61
+ })]
62
+ })]
63
+ })]
64
+ }), /* @__PURE__ */ d("div", {
65
+ className: "mt-4 border-t border-border-seam pt-4",
66
+ children: C ? /* @__PURE__ */ d(r, { label: "Loading discussion…" }) : w ? /* @__PURE__ */ d(n, {
67
+ message: "Couldn't load the discussion.",
68
+ onRetry: () => T()
69
+ }) : P.length === 0 ? /* @__PURE__ */ d(a, {
70
+ illustration: "empty-data",
71
+ icon: /* @__PURE__ */ d(l, { className: "h-9 w-9" }),
72
+ title: "No comments yet",
73
+ description: "Start the discussion by adding the first comment."
74
+ }) : /* @__PURE__ */ d("ul", {
75
+ className: "flex flex-col gap-4",
76
+ children: P.map((e) => {
77
+ let n = M(e), r = e.content.text ?? e.content.markdown ?? "";
78
+ return /* @__PURE__ */ f("li", {
79
+ className: "flex gap-3",
80
+ children: [/* @__PURE__ */ d("div", {
81
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-secondary",
82
+ children: g(n)
83
+ }), /* @__PURE__ */ f("div", {
84
+ className: "min-w-0 flex-1",
85
+ children: [/* @__PURE__ */ f("div", {
86
+ className: "flex flex-wrap items-baseline gap-x-2",
87
+ children: [/* @__PURE__ */ d("span", {
88
+ className: "text-sm font-semibold text-text-primary",
89
+ children: n
90
+ }), /* @__PURE__ */ f("span", {
91
+ className: "text-xs text-text-tertiary",
92
+ children: [t(e.createdAt), e.edited ? " · edited" : ""]
93
+ })]
94
+ }), /* @__PURE__ */ d("p", {
95
+ className: "mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary",
96
+ children: r
97
+ })]
98
+ })]
99
+ }, e.id);
100
+ })
101
+ })
102
+ })]
103
+ })
104
+ });
105
+ }
106
+ //#endregion
107
+ export { _ as EcoDiscussion };
108
+
109
+ //# sourceMappingURL=EcoDiscussion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EcoDiscussion.js","names":[],"sources":["../../../src/ecoimpacthub/components/EcoDiscussion.tsx"],"sourcesContent":["/**\n * EcoDiscussion — a comment thread on any eco entity (Integration B).\n * @module ecoimpacthub/components/EcoDiscussion\n *\n * Reuses the platform EntityComment API (`getEntityComments` /\n * `createEntityComment`) keyed by `(entityType, entityId)` — no eco-svc change.\n * Renders root comments (author, body, time) + a composer. Robust to empty\n * state and errors.\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, Send } from 'lucide-react';\nimport { Button, Textarea } from '@burdenoff/fe-libs/ui';\nimport { Section, LoadingView, ErrorView } from './StateViews';\nimport { EmptyState } from './EmptyState';\nimport { useEcoImpactHub } from '../context';\nimport { useEcoComments, useEcoCreateComment, type EcoComment } from '../hooks';\nimport { formatDateTime } from '../utils/formatters';\n\ninterface EcoDiscussionProps {\n entityType: string;\n entityId: string;\n title?: string;\n description?: string;\n}\n\nfunction displayName(user: ReturnType<typeof useEcoImpactHub>['currentUser']): string {\n if (!user) return 'You';\n const full = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();\n return user.name || full || user.username || user.email || 'You';\n}\n\nfunction initialFor(label: string): string {\n const trimmed = label.trim();\n return trimmed ? trimmed[0]!.toUpperCase() : '?';\n}\n\nexport function EcoDiscussion({\n entityType,\n entityId,\n title = 'Discussion',\n description = 'Coordinate review and verification with your team.',\n}: EcoDiscussionProps) {\n const { currentUser } = useEcoImpactHub();\n const { data, isLoading, isError, refetch } = useEcoComments(entityType, entityId);\n const createComment = useEcoCreateComment(entityType, entityId);\n\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const selfName = displayName(currentUser);\n\n const authorLabel = (comment: EcoComment): string =>\n currentUser?.id && comment.authorId === currentUser.id ? selfName : 'Workspace member';\n\n const handleSubmit = async () => {\n const text = draft.trim();\n if (!text) return;\n setError(null);\n try {\n await createComment.mutateAsync(text);\n setDraft('');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to post comment.');\n }\n };\n\n const comments = data?.items ?? [];\n\n return (\n <Section title={title} description={description}>\n <div className=\"rounded-xl border border-border-seam bg-bg-surface/40 p-4\">\n {/* Composer */}\n <div className=\"flex gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-sm font-semibold text-action-primary-bg\">\n {initialFor(selfName)}\n </div>\n <div className=\"flex-1\">\n <Textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n placeholder=\"Add a comment for your team…\"\n rows={2}\n />\n <div className=\"mt-2 flex items-center justify-end gap-2\">\n {error && <span className=\"mr-auto text-xs text-status-error-text\">{error}</span>}\n <Button\n size=\"sm\"\n onClick={handleSubmit}\n disabled={!draft.trim() || createComment.isPending}\n >\n <Send className=\"mr-1.5 h-4 w-4\" />\n {createComment.isPending ? 'Posting…' : 'Comment'}\n </Button>\n </div>\n </div>\n </div>\n\n {/* Thread */}\n <div className=\"mt-4 border-t border-border-seam pt-4\">\n {isLoading ? (\n <LoadingView label=\"Loading discussion…\" />\n ) : isError ? (\n <ErrorView message=\"Couldn't load the discussion.\" onRetry={() => refetch()} />\n ) : comments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<MessageSquare className=\"h-9 w-9\" />}\n title=\"No comments yet\"\n description=\"Start the discussion by adding the first comment.\"\n />\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {comments.map((comment) => {\n const label = authorLabel(comment);\n const body = comment.content.text ?? comment.content.markdown ?? '';\n return (\n <li key={comment.id} className=\"flex gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-secondary\">\n {initialFor(label)}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-baseline gap-x-2\">\n <span className=\"text-sm font-semibold text-text-primary\">{label}</span>\n <span className=\"text-xs text-text-tertiary\">\n {formatDateTime(comment.createdAt)}\n {comment.edited ? ' · edited' : ''}\n </span>\n </div>\n <p className=\"mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {body}\n </p>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </Section>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,149 @@
1
+ import { useEcoAssignTag as e, useEcoCreateAndAssignTag as t, useEcoEntityTags as n, useEcoRemoveTagAssignment as r, useEcoTagSearch as i } from "../hooks/useEcoTags.js";
2
+ import "../hooks/index.js";
3
+ import { useEffect as a, useState as o } from "react";
4
+ import { Loader2 as s, Plus as c, Tag as l, X as u } from "lucide-react";
5
+ import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
6
+ import { Button as m, Input as h, Popover as g, PopoverContent as _, PopoverTrigger as v, Spinner as y } from "@burdenoff/fe-libs/ui";
7
+ //#region src/ecoimpacthub/components/EcoTags.tsx
8
+ function b({ entityType: b, entityId: x, label: S = "Tags" }) {
9
+ let C = n(b, x), w = e(b, x), T = r(b, x), E = t(b, x), [D, O] = o(!1), [k, A] = o(""), [j, M] = o(""), [N, P] = o(null);
10
+ a(() => {
11
+ let e = setTimeout(() => M(k), 250);
12
+ return () => clearTimeout(e);
13
+ }, [k]);
14
+ let F = i(j), I = C.data ?? [], L = new Set(I.map((e) => e.tag.id)), R = j.trim(), z = (F.data ?? []).filter((e) => !L.has(e.id)), B = R.length > 0 && [...I.map((e) => e.tag.label), ...(F.data ?? []).map((e) => e.label)].some((e) => e.toLowerCase() === R.toLowerCase()), V = R.length > 0 && !B, H = w.isPending || E.isPending, U = () => {
15
+ A(""), M(""), P(null);
16
+ }, W = async (e) => {
17
+ P(null);
18
+ try {
19
+ await w.mutateAsync(e), U(), O(!1);
20
+ } catch (e) {
21
+ P(e instanceof Error ? e.message : "Failed to add tag.");
22
+ }
23
+ }, G = async () => {
24
+ if (R) {
25
+ P(null);
26
+ try {
27
+ await E.mutateAsync(R), U(), O(!1);
28
+ } catch (e) {
29
+ P(e instanceof Error ? e.message : "Failed to create tag.");
30
+ }
31
+ }
32
+ }, K = async (e) => {
33
+ P(null);
34
+ try {
35
+ await T.mutateAsync(e);
36
+ } catch (e) {
37
+ P(e instanceof Error ? e.message : "Failed to remove tag.");
38
+ }
39
+ };
40
+ return /* @__PURE__ */ p("div", { children: [
41
+ /* @__PURE__ */ f("p", {
42
+ className: "mb-1.5 text-xs text-text-tertiary",
43
+ children: S
44
+ }),
45
+ /* @__PURE__ */ p("div", {
46
+ className: "flex flex-wrap items-center gap-2",
47
+ children: [C.isLoading ? /* @__PURE__ */ p("span", {
48
+ className: "inline-flex items-center gap-1.5 text-sm text-text-tertiary",
49
+ children: [/* @__PURE__ */ f(s, { className: "h-3.5 w-3.5 animate-spin" }), " Loading tags…"]
50
+ }) : I.map((e) => /* @__PURE__ */ p("span", {
51
+ className: "inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-bg-sunken px-2.5 py-1 text-xs text-text-secondary",
52
+ children: [
53
+ e.tag.color ? /* @__PURE__ */ f("span", {
54
+ className: "h-2 w-2 shrink-0 rounded-full",
55
+ style: { backgroundColor: e.tag.color },
56
+ "aria-hidden": !0
57
+ }) : /* @__PURE__ */ f(l, { className: "h-3 w-3 text-text-tertiary" }),
58
+ /* @__PURE__ */ f("span", {
59
+ className: "max-w-[10rem] truncate",
60
+ children: e.tag.label
61
+ }),
62
+ /* @__PURE__ */ f("button", {
63
+ type: "button",
64
+ onClick: () => K(e.id),
65
+ disabled: T.isPending,
66
+ "aria-label": `Remove tag ${e.tag.label}`,
67
+ className: "rounded-full p-0.5 text-text-tertiary hover:bg-bg-canvas hover:text-text-primary",
68
+ children: /* @__PURE__ */ f(u, { className: "h-3 w-3" })
69
+ })
70
+ ]
71
+ }, e.id)), /* @__PURE__ */ p(g, {
72
+ open: D,
73
+ onOpenChange: (e) => {
74
+ O(e), e || U();
75
+ },
76
+ children: [/* @__PURE__ */ f(v, {
77
+ asChild: !0,
78
+ children: /* @__PURE__ */ p(m, {
79
+ size: "sm",
80
+ variant: "outline",
81
+ className: "h-7 gap-1 px-2 text-xs",
82
+ children: [/* @__PURE__ */ f(c, { className: "h-3.5 w-3.5" }), " Add tag"]
83
+ })
84
+ }), /* @__PURE__ */ p(_, {
85
+ align: "start",
86
+ className: "w-64 p-2",
87
+ children: [/* @__PURE__ */ f(h, {
88
+ autoFocus: !0,
89
+ value: k,
90
+ onChange: (e) => A(e.target.value),
91
+ placeholder: "Search or create a tag…",
92
+ className: "h-8"
93
+ }), /* @__PURE__ */ f("div", {
94
+ className: "mt-2 max-h-56 overflow-y-auto",
95
+ children: F.isFetching && R.length > 0 ? /* @__PURE__ */ p("div", {
96
+ className: "flex items-center gap-2 px-1 py-2 text-sm text-text-tertiary",
97
+ children: [/* @__PURE__ */ f(y, { size: "sm" }), " Searching…"]
98
+ }) : /* @__PURE__ */ p(d, { children: [
99
+ z.map((e) => /* @__PURE__ */ p("button", {
100
+ type: "button",
101
+ onClick: () => W(e.id),
102
+ disabled: H,
103
+ className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-text-secondary hover:bg-bg-sunken disabled:opacity-60",
104
+ children: [e.color ? /* @__PURE__ */ f("span", {
105
+ className: "h-2 w-2 shrink-0 rounded-full",
106
+ style: { backgroundColor: e.color },
107
+ "aria-hidden": !0
108
+ }) : /* @__PURE__ */ f(l, { className: "h-3.5 w-3.5 text-text-tertiary" }), /* @__PURE__ */ f("span", {
109
+ className: "truncate",
110
+ children: e.label
111
+ })]
112
+ }, e.id)),
113
+ V && /* @__PURE__ */ p("button", {
114
+ type: "button",
115
+ onClick: G,
116
+ disabled: H,
117
+ className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-action-primary-bg hover:bg-bg-sunken disabled:opacity-60",
118
+ children: [/* @__PURE__ */ f(c, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ p("span", {
119
+ className: "truncate",
120
+ children: [
121
+ "Create “",
122
+ R,
123
+ "”"
124
+ ]
125
+ })]
126
+ }),
127
+ !V && z.length === 0 && R.length > 0 && /* @__PURE__ */ f("p", {
128
+ className: "px-2 py-2 text-sm text-text-tertiary",
129
+ children: "Already added."
130
+ }),
131
+ R.length === 0 && /* @__PURE__ */ f("p", {
132
+ className: "px-2 py-2 text-sm text-text-tertiary",
133
+ children: "Type to search existing tags or create a new one."
134
+ })
135
+ ] })
136
+ })]
137
+ })]
138
+ })]
139
+ }),
140
+ N && /* @__PURE__ */ f("p", {
141
+ className: "mt-1.5 text-xs text-status-error-text",
142
+ children: N
143
+ })
144
+ ] });
145
+ }
146
+ //#endregion
147
+ export { b as EcoTags };
148
+
149
+ //# sourceMappingURL=EcoTags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EcoTags.js","names":[],"sources":["../../../src/ecoimpacthub/components/EcoTags.tsx"],"sourcesContent":["/**\n * EcoTags — a lightweight tag control for eco entities (Integration C).\n * @module ecoimpacthub/components/EcoTags\n *\n * The eco svc exposes no tags field, so this drives the platform tag store\n * directly: `getEntityTags` (display), `searchTags` (+ `createTag`) to pick or\n * create, `assignTag`/`removeTagAssignment` to link — all via the workspace\n * gateway. No eco-svc change.\n */\n\nimport { useEffect, useState } from 'react';\nimport { Loader2, Plus, Tag as TagIcon, X } from 'lucide-react';\nimport {\n Button,\n Input,\n Popover,\n PopoverContent,\n PopoverTrigger,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport {\n useEcoEntityTags,\n useEcoTagSearch,\n useEcoAssignTag,\n useEcoRemoveTagAssignment,\n useEcoCreateAndAssignTag,\n} from '../hooks';\n\ninterface EcoTagsProps {\n entityType: string;\n entityId: string;\n label?: string;\n}\n\nexport function EcoTags({ entityType, entityId, label = 'Tags' }: EcoTagsProps) {\n const tags = useEcoEntityTags(entityType, entityId);\n const assign = useEcoAssignTag(entityType, entityId);\n const remove = useEcoRemoveTagAssignment(entityType, entityId);\n const createAndAssign = useEcoCreateAndAssignTag(entityType, entityId);\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState('');\n const [debounced, setDebounced] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const timer = setTimeout(() => setDebounced(query), 250);\n return () => clearTimeout(timer);\n }, [query]);\n\n const search = useEcoTagSearch(debounced);\n\n const assignments = tags.data ?? [];\n const assignedIds = new Set(assignments.map((a) => a.tag.id));\n const q = debounced.trim();\n const searchResults = (search.data ?? []).filter((t) => !assignedIds.has(t.id));\n const hasExact =\n q.length > 0 &&\n [...assignments.map((a) => a.tag.label), ...(search.data ?? []).map((t) => t.label)].some(\n (l) => l.toLowerCase() === q.toLowerCase()\n );\n const showCreate = q.length > 0 && !hasExact;\n const busy = assign.isPending || createAndAssign.isPending;\n\n const resetInput = () => {\n setQuery('');\n setDebounced('');\n setError(null);\n };\n\n const handleAssign = async (tagId: string) => {\n setError(null);\n try {\n await assign.mutateAsync(tagId);\n resetInput();\n setOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add tag.');\n }\n };\n\n const handleCreate = async () => {\n if (!q) return;\n setError(null);\n try {\n await createAndAssign.mutateAsync(q);\n resetInput();\n setOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create tag.');\n }\n };\n\n const handleRemove = async (assignmentId: string) => {\n setError(null);\n try {\n await remove.mutateAsync(assignmentId);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove tag.');\n }\n };\n\n return (\n <div>\n <p className=\"mb-1.5 text-xs text-text-tertiary\">{label}</p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {tags.isLoading ? (\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> Loading tags…\n </span>\n ) : (\n assignments.map((assignment) => (\n <span\n key={assignment.id}\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-bg-sunken px-2.5 py-1 text-xs text-text-secondary\"\n >\n {assignment.tag.color ? (\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: assignment.tag.color }}\n aria-hidden\n />\n ) : (\n <TagIcon className=\"h-3 w-3 text-text-tertiary\" />\n )}\n <span className=\"max-w-[10rem] truncate\">{assignment.tag.label}</span>\n <button\n type=\"button\"\n onClick={() => handleRemove(assignment.id)}\n disabled={remove.isPending}\n aria-label={`Remove tag ${assignment.tag.label}`}\n className=\"rounded-full p-0.5 text-text-tertiary hover:bg-bg-canvas hover:text-text-primary\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))\n )}\n\n <Popover\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) resetInput();\n }}\n >\n <PopoverTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 gap-1 px-2 text-xs\">\n <Plus className=\"h-3.5 w-3.5\" /> Add tag\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search or create a tag…\"\n className=\"h-8\"\n />\n <div className=\"mt-2 max-h-56 overflow-y-auto\">\n {search.isFetching && q.length > 0 ? (\n <div className=\"flex items-center gap-2 px-1 py-2 text-sm text-text-tertiary\">\n <Spinner size=\"sm\" /> Searching…\n </div>\n ) : (\n <>\n {searchResults.map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n onClick={() => handleAssign(tag.id)}\n disabled={busy}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-text-secondary hover:bg-bg-sunken disabled:opacity-60\"\n >\n {tag.color ? (\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: tag.color }}\n aria-hidden\n />\n ) : (\n <TagIcon className=\"h-3.5 w-3.5 text-text-tertiary\" />\n )}\n <span className=\"truncate\">{tag.label}</span>\n </button>\n ))}\n\n {showCreate && (\n <button\n type=\"button\"\n onClick={handleCreate}\n disabled={busy}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-action-primary-bg hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Plus className=\"h-3.5 w-3.5 shrink-0\" />\n <span className=\"truncate\">Create “{q}”</span>\n </button>\n )}\n\n {!showCreate && searchResults.length === 0 && q.length > 0 && (\n <p className=\"px-2 py-2 text-sm text-text-tertiary\">Already added.</p>\n )}\n {q.length === 0 && (\n <p className=\"px-2 py-2 text-sm text-text-tertiary\">\n Type to search existing tags or create a new one.\n </p>\n )}\n </>\n )}\n </div>\n </PopoverContent>\n </Popover>\n </div>\n\n {error && <p className=\"mt-1.5 text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ import { useEcoFileUpload as e } from "../hooks/useEcoFiles.js";
2
+ import "../hooks/index.js";
3
+ import { useRef as t } from "react";
4
+ import { Paperclip as n, Upload as r, X as i } from "lucide-react";
5
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
6
+ import { Button as s, Label as c, Spinner as l } from "@burdenoff/fe-libs/ui";
7
+ //#region src/ecoimpacthub/components/EvidenceUpload.tsx
8
+ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,application/pdf,audio/*", disabled: h = !1, hint: g }) {
9
+ let _ = t(null), { upload: v, isUploading: y, progress: b, error: x, reset: S } = e();
10
+ return /* @__PURE__ */ o("div", {
11
+ className: "grid gap-1.5",
12
+ children: [
13
+ /* @__PURE__ */ a(c, {
14
+ htmlFor: u,
15
+ children: d
16
+ }),
17
+ /* @__PURE__ */ a("input", {
18
+ ref: _,
19
+ id: u,
20
+ type: "file",
21
+ accept: m,
22
+ className: "hidden",
23
+ disabled: h || y,
24
+ onChange: async (e) => {
25
+ let t = e.target.files?.[0];
26
+ if (e.target.value = "", t) try {
27
+ p(await v(t));
28
+ } catch {}
29
+ }
30
+ }),
31
+ f ? /* @__PURE__ */ o("div", {
32
+ className: "flex items-center gap-2 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2",
33
+ children: [
34
+ /* @__PURE__ */ a(n, { className: "h-4 w-4 shrink-0 text-text-tertiary" }),
35
+ /* @__PURE__ */ a("span", {
36
+ className: "min-w-0 flex-1 truncate text-sm text-text-primary",
37
+ title: f.fileName,
38
+ children: f.fileName
39
+ }),
40
+ /* @__PURE__ */ a("button", {
41
+ type: "button",
42
+ onClick: () => {
43
+ S(), p(null);
44
+ },
45
+ disabled: h,
46
+ "aria-label": "Remove attachment",
47
+ className: "rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary",
48
+ children: /* @__PURE__ */ a(i, { className: "h-4 w-4" })
49
+ })
50
+ ]
51
+ }) : y ? /* @__PURE__ */ o("div", {
52
+ className: "flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2",
53
+ children: [
54
+ /* @__PURE__ */ a(l, { size: "sm" }),
55
+ /* @__PURE__ */ o("span", {
56
+ className: "text-sm text-text-secondary",
57
+ children: [
58
+ "Uploading… ",
59
+ b,
60
+ "%"
61
+ ]
62
+ }),
63
+ /* @__PURE__ */ a("div", {
64
+ className: "ml-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken",
65
+ children: /* @__PURE__ */ a("div", {
66
+ className: "h-full rounded-full bg-action-primary-bg transition-[width] duration-150",
67
+ style: { width: `${b}%` }
68
+ })
69
+ })
70
+ ]
71
+ }) : /* @__PURE__ */ o(s, {
72
+ type: "button",
73
+ variant: "outline",
74
+ disabled: h,
75
+ onClick: () => _.current?.click(),
76
+ className: "justify-start",
77
+ children: [/* @__PURE__ */ a(r, { className: "mr-1.5 h-4 w-4" }), " Choose file"]
78
+ }),
79
+ g && !x && /* @__PURE__ */ a("p", {
80
+ className: "text-xs text-text-tertiary",
81
+ children: g
82
+ }),
83
+ x && /* @__PURE__ */ a("p", {
84
+ className: "text-xs text-status-error-text",
85
+ children: x
86
+ })
87
+ ]
88
+ });
89
+ }
90
+ //#endregion
91
+ export { u as EvidenceUpload };
92
+
93
+ //# sourceMappingURL=EvidenceUpload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EvidenceUpload.js","names":[],"sources":["../../../src/ecoimpacthub/components/EvidenceUpload.tsx"],"sourcesContent":["/**\n * EvidenceUpload — a controlled file-attach control (Integration A).\n * @module ecoimpacthub/components/EvidenceUpload\n *\n * Wraps `useEcoFileUpload` (the wspace-files 3-step flow). On file select it\n * uploads, shows byte-progress + the filename, and emits `{ fileId, fileName }`\n * via `onChange`. The parent stores `fileId` and passes it to the eco mutation\n * (`ecoRecordObservation.evidenceFileId`, `ecoPublishReport.fileId`).\n */\n\nimport { useRef, type ChangeEvent } from 'react';\nimport { Paperclip, Upload, X } from 'lucide-react';\nimport { Button, Label, Spinner } from '@burdenoff/fe-libs/ui';\nimport { useEcoFileUpload, type UploadedEcoFile } from '../hooks';\n\ninterface EvidenceUploadProps {\n id: string;\n label: string;\n value: UploadedEcoFile | null;\n onChange: (value: UploadedEcoFile | null) => void;\n accept?: string;\n disabled?: boolean;\n hint?: string;\n}\n\nexport function EvidenceUpload({\n id,\n label,\n value,\n onChange,\n accept = 'image/*,application/pdf,audio/*',\n disabled = false,\n hint,\n}: EvidenceUploadProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { upload, isUploading, progress, error, reset } = useEcoFileUpload();\n\n const handleSelect = async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Clear the native input so the same file can be re-selected after a remove.\n event.target.value = '';\n if (!file) return;\n try {\n const uploaded = await upload(file);\n onChange(uploaded);\n } catch {\n // Error surfaced inline via `error`; keep the form usable.\n }\n };\n\n const handleRemove = () => {\n reset();\n onChange(null);\n };\n\n return (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={id}>{label}</Label>\n\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n disabled={disabled || isUploading}\n onChange={handleSelect}\n />\n\n {value ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Paperclip className=\"h-4 w-4 shrink-0 text-text-tertiary\" />\n <span\n className=\"min-w-0 flex-1 truncate text-sm text-text-primary\"\n title={value.fileName}\n >\n {value.fileName}\n </span>\n <button\n type=\"button\"\n onClick={handleRemove}\n disabled={disabled}\n aria-label=\"Remove attachment\"\n className=\"rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ) : isUploading ? (\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Spinner size=\"sm\" />\n <span className=\"text-sm text-text-secondary\">Uploading… {progress}%</span>\n <div className=\"ml-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-150\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={disabled}\n onClick={() => inputRef.current?.click()}\n className=\"justify-start\"\n >\n <Upload className=\"mr-1.5 h-4 w-4\" /> Choose file\n </Button>\n )}\n\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAyBA,SAAgB,EAAe,EAC7B,OACA,UACA,UACA,aACA,YAAS,mCACT,cAAW,IACX,WACsB;CACtB,IAAM,IAAW,EAAyB,KAAK,EACzC,EAAE,WAAQ,gBAAa,aAAU,UAAO,aAAU,GAAkB;AAoB1E,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IAAO,SAAS;cAAK;IAAc,CAAA;GAEnC,kBAAC,SAAD;IACE,KAAK;IACD;IACJ,MAAK;IACG;IACR,WAAU;IACV,UAAU,KAAY;IACtB,UA7Be,OAAO,MAAyC;KACnE,IAAM,IAAO,EAAM,OAAO,QAAQ;AAElC,WAAM,OAAO,QAAQ,IAChB,EACL,KAAI;AAEF,QADiB,MAAM,EAAO,EAAK,CACjB;aACZ;;IAsBJ,CAAA;GAED,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAW,WAAU,uCAAwC,CAAA;KAC7D,kBAAC,QAAD;MACE,WAAU;MACV,OAAO,EAAM;gBAEZ,EAAM;MACF,CAAA;KACP,kBAAC,UAAD;MACE,MAAK;MACL,eA9BiB;AAEzB,OADA,GAAO,EACP,EAAS,KAAK;;MA6BI;MACV,cAAW;MACX,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA;KACL;QACJ,IACF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA;KACrB,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAA8C;OAAY;OAAS;OAAQ;;KAC3E,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAS,IAAI;OAChC,CAAA;MACE,CAAA;KACF;QAEN,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACE;IACV,eAAe,EAAS,SAAS,OAAO;IACxC,WAAU;cALZ,CAOE,kBAAC,GAAD,EAAQ,WAAU,kBAAmB,CAAA,EAAA,eAC9B;;GAGV,KAAQ,CAAC,KAAS,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAAS,CAAA;GACtE,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAkC;IAAU,CAAA;GAC/D"}
@@ -0,0 +1,36 @@
1
+ import { useEcoFile as e } from "../hooks/useEcoFiles.js";
2
+ import "../hooks/index.js";
3
+ import { FileText as t, Loader2 as n } from "lucide-react";
4
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
5
+ //#region src/ecoimpacthub/components/FileLink.tsx
6
+ function a({ fileId: a, className: o }) {
7
+ let { data: s, isLoading: c, isError: l } = e(a);
8
+ return a ? c ? /* @__PURE__ */ i("span", {
9
+ className: "inline-flex items-center gap-1.5 text-sm text-text-tertiary",
10
+ children: [/* @__PURE__ */ r(n, { className: "h-3.5 w-3.5 animate-spin" }), " Loading attachment…"]
11
+ }) : l || !s ? /* @__PURE__ */ i("span", {
12
+ className: "inline-flex items-center gap-1.5 text-sm text-text-tertiary",
13
+ children: [/* @__PURE__ */ r(t, { className: "h-3.5 w-3.5" }), " Attachment unavailable"]
14
+ }) : s.downloadUrl ? /* @__PURE__ */ i("a", {
15
+ href: s.downloadUrl,
16
+ target: "_blank",
17
+ rel: "noopener noreferrer",
18
+ className: o ?? "inline-flex items-center gap-1.5 text-sm font-medium text-action-primary-bg hover:underline",
19
+ children: [
20
+ /* @__PURE__ */ r(t, { className: "h-3.5 w-3.5" }),
21
+ " ",
22
+ s.name
23
+ ]
24
+ }) : /* @__PURE__ */ i("span", {
25
+ className: o ?? "inline-flex items-center gap-1.5 text-sm text-text-secondary",
26
+ children: [
27
+ /* @__PURE__ */ r(t, { className: "h-3.5 w-3.5" }),
28
+ " ",
29
+ s.name
30
+ ]
31
+ }) : null;
32
+ }
33
+ //#endregion
34
+ export { a as FileLink };
35
+
36
+ //# sourceMappingURL=FileLink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileLink.js","names":[],"sources":["../../../src/ecoimpacthub/components/FileLink.tsx"],"sourcesContent":["/**\n * FileLink — resolves a federated File id to a download link (Integration A).\n * @module ecoimpacthub/components/FileLink\n *\n * The eco queries select `evidence { id }` / `file { id }` (just the id). This\n * resolves the platform File via `getFile(id)` for its name + signed\n * `downloadUrl` so evidence/report artifacts render as a real link.\n */\n\nimport { FileText, Loader2 } from 'lucide-react';\nimport { useEcoFile } from '../hooks';\n\ninterface FileLinkProps {\n fileId: string | null | undefined;\n className?: string;\n}\n\nexport function FileLink({ fileId, className }: FileLinkProps) {\n const { data, isLoading, isError } = useEcoFile(fileId);\n\n if (!fileId) return null;\n\n if (isLoading) {\n return (\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> Loading attachment…\n </span>\n );\n }\n\n if (isError || !data) {\n return (\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <FileText className=\"h-3.5 w-3.5\" /> Attachment unavailable\n </span>\n );\n }\n\n const base =\n 'inline-flex items-center gap-1.5 text-sm font-medium text-action-primary-bg hover:underline';\n\n if (!data.downloadUrl) {\n return (\n <span className={className ?? 'inline-flex items-center gap-1.5 text-sm text-text-secondary'}>\n <FileText className=\"h-3.5 w-3.5\" /> {data.name}\n </span>\n );\n }\n\n return (\n <a\n href={data.downloadUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={className ?? base}\n >\n <FileText className=\"h-3.5 w-3.5\" /> {data.name}\n </a>\n );\n}\n"],"mappings":";;;;;AAiBA,SAAgB,EAAS,EAAE,WAAQ,gBAA4B;CAC7D,IAAM,EAAE,SAAM,cAAW,eAAY,EAAW,EAAO;AA+BvD,QA7BK,IAED,IAEA,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,GAAD,EAAS,WAAU,4BAA6B,CAAA,EAAA,uBAC3C;MAIP,KAAW,CAAC,IAEZ,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA,EAAA,0BAC/B;MAON,EAAK,cASR,kBAAC,KAAD;EACE,MAAM,EAAK;EACX,QAAO;EACP,KAAI;EACJ,WAAW,KAAa;YAJ1B;GAME,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA;;GAAE,EAAK;GACzC;MAdF,kBAAC,QAAD;EAAM,WAAW,KAAa;YAA9B;GACE,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA;;GAAE,EAAK;GACtC;MAzBS"}
@@ -5,7 +5,7 @@ import { AuroraBackground as r, GradientText as i } from "@burdenoff/fe-libs/ui"
5
5
  var a = ({ title: a, description: o, actions: s, children: c, className: l, titleGradient: u = !1, aurora: d = !1 }) => /* @__PURE__ */ n("div", {
6
6
  className: e("flex flex-col h-full", l),
7
7
  children: [/* @__PURE__ */ n("div", {
8
- className: e("relative flex items-center justify-between gap-4 px-6 py-4 border-b border-border-seam", d ? "overflow-hidden bg-gloss-card" : "bg-bg-surface"),
8
+ className: e("relative flex flex-col gap-3 px-6 py-4 border-b border-border-seam sm:flex-row sm:items-center sm:justify-between sm:gap-4", d ? "overflow-hidden bg-gloss-card" : "bg-bg-surface"),
9
9
  children: [
10
10
  d && /* @__PURE__ */ t(r, { intensity: "subtle" }),
11
11
  /* @__PURE__ */ n("div", {
@@ -23,7 +23,7 @@ var a = ({ title: a, description: o, actions: s, children: c, className: l, titl
23
23
  })]
24
24
  }),
25
25
  s && /* @__PURE__ */ t("div", {
26
- className: "flex items-center gap-3 shrink-0",
26
+ className: "flex flex-wrap items-center gap-3 shrink-0 max-sm:w-full",
27
27
  children: s
28
28
  })
29
29
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/ecoimpacthub/components/PageLayout.tsx"],"sourcesContent":["/**\n * Page Layout Component\n * @module ecoimpacthub/components\n *\n * Boff UI 2.0 page wrapper. Supports a living-glass {@link AuroraBackground}\n * hero backdrop and a gradient hero title for the MFE's primary page.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n /** Render the title as a gradient hero heading (reserve for the ONE primary page). */\n titleGradient?: boolean;\n /** Drop a subtle living-glass aurora behind the header (primary page only). */\n aurora?: boolean;\n}\n\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n actions,\n children,\n className,\n titleGradient = false,\n aurora = false,\n}) => {\n return (\n <div className={cn('flex flex-col h-full', className)}>\n <div\n className={cn(\n 'relative flex items-center justify-between gap-4 px-6 py-4 border-b border-border-seam',\n aurora ? 'overflow-hidden bg-gloss-card' : 'bg-bg-surface'\n )}\n >\n {aurora && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"min-w-0\">\n {titleGradient && typeof title === 'string' ? (\n <GradientText as=\"h1\" className=\"text-2xl font-semibold tracking-tight\">\n {title}\n </GradientText>\n ) : (\n <h1 className=\"text-2xl font-semibold text-text-primary\">{title}</h1>\n )}\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n {actions && <div className=\"flex items-center gap-3 shrink-0\">{actions}</div>}\n </div>\n\n <div className=\"flex-1 overflow-auto p-6\">{children}</div>\n </div>\n );\n};\n"],"mappings":";;;;AAwBA,IAAa,KAAmC,EAC9C,UACA,gBACA,YACA,aACA,cACA,mBAAgB,IAChB,YAAS,SAGP,kBAAC,OAAD;CAAK,WAAW,EAAG,wBAAwB,EAAU;WAArD,CACE,kBAAC,OAAD;EACE,WAAW,EACT,0FACA,IAAS,kCAAkC,gBAC5C;YAJH;GAMG,KAAU,kBAAC,GAAD,EAAkB,WAAU,UAAW,CAAA;GAClD,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KAAiB,OAAO,KAAU,WACjC,kBAAC,GAAD;KAAc,IAAG;KAAK,WAAU;eAC7B;KACY,CAAA,GAEf,kBAAC,MAAD;KAAI,WAAU;eAA4C;KAAW,CAAA,EAEtE,KAAe,kBAAC,KAAD;KAAG,WAAU;eAAoC;KAAgB,CAAA,CAC7E;;GACL,KAAW,kBAAC,OAAD;IAAK,WAAU;cAAoC;IAAc,CAAA;GACzE;KAEN,kBAAC,OAAD;EAAK,WAAU;EAA4B;EAAe,CAAA,CACtD"}
1
+ {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/ecoimpacthub/components/PageLayout.tsx"],"sourcesContent":["/**\n * Page Layout Component\n * @module ecoimpacthub/components\n *\n * Boff UI 2.0 page wrapper. Supports a living-glass {@link AuroraBackground}\n * hero backdrop and a gradient hero title for the MFE's primary page.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n /** Render the title as a gradient hero heading (reserve for the ONE primary page). */\n titleGradient?: boolean;\n /** Drop a subtle living-glass aurora behind the header (primary page only). */\n aurora?: boolean;\n}\n\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n actions,\n children,\n className,\n titleGradient = false,\n aurora = false,\n}) => {\n return (\n <div className={cn('flex flex-col h-full', className)}>\n <div\n className={cn(\n 'relative flex flex-col gap-3 px-6 py-4 border-b border-border-seam sm:flex-row sm:items-center sm:justify-between sm:gap-4',\n aurora ? 'overflow-hidden bg-gloss-card' : 'bg-bg-surface'\n )}\n >\n {aurora && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"min-w-0\">\n {titleGradient && typeof title === 'string' ? (\n <GradientText as=\"h1\" className=\"text-2xl font-semibold tracking-tight\">\n {title}\n </GradientText>\n ) : (\n <h1 className=\"text-2xl font-semibold text-text-primary\">{title}</h1>\n )}\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n {actions && (\n <div className=\"flex flex-wrap items-center gap-3 shrink-0 max-sm:w-full\">{actions}</div>\n )}\n </div>\n\n <div className=\"flex-1 overflow-auto p-6\">{children}</div>\n </div>\n );\n};\n"],"mappings":";;;;AAwBA,IAAa,KAAmC,EAC9C,UACA,gBACA,YACA,aACA,cACA,mBAAgB,IAChB,YAAS,SAGP,kBAAC,OAAD;CAAK,WAAW,EAAG,wBAAwB,EAAU;WAArD,CACE,kBAAC,OAAD;EACE,WAAW,EACT,8HACA,IAAS,kCAAkC,gBAC5C;YAJH;GAMG,KAAU,kBAAC,GAAD,EAAkB,WAAU,UAAW,CAAA;GAClD,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KAAiB,OAAO,KAAU,WACjC,kBAAC,GAAD;KAAc,IAAG;KAAK,WAAU;eAC7B;KACY,CAAA,GAEf,kBAAC,MAAD;KAAI,WAAU;eAA4C;KAAW,CAAA,EAEtE,KAAe,kBAAC,KAAD;KAAG,WAAU;eAAoC;KAAgB,CAAA,CAC7E;;GACL,KACC,kBAAC,OAAD;IAAK,WAAU;cAA4D;IAAc,CAAA;GAEvF;KAEN,kBAAC,OAAD;EAAK,WAAU;EAA4B;EAAe,CAAA,CACtD"}
@@ -1,2 +1,5 @@
1
1
  import "./useEcoImpactHubApi.js";
2
2
  import "./useCursorPager.js";
3
+ import "./useEcoFiles.js";
4
+ import "./useEcoComments.js";
5
+ import "./useEcoTags.js";
@@ -4,7 +4,7 @@ function n() {
4
4
  let [n, r] = t([null]), i = n.length - 1;
5
5
  return {
6
6
  page: i,
7
- cursor: n[i] ?? null,
7
+ cursor: n[i] ?? void 0,
8
8
  next: e((e) => {
9
9
  e && r((t) => [...t, e]);
10
10
  }, []),
@@ -1 +1 @@
1
- {"version":3,"file":"useCursorPager.js","names":[],"sources":["../../../src/ecoimpacthub/hooks/useCursorPager.ts"],"sourcesContent":["/**\n * useCursorPager — forward/back cursor pagination for Eco* list connections.\n * @module ecoimpacthub/hooks\n *\n * The svc paginates by cursor only (`PaginationInput.offset` is a no-op — the\n * resolver never sets Prisma `skip` from it). So a page must send\n * `pagination.cursor = the previous page's pageInfo.cursor` to advance. This\n * hook keeps a stack of the cursors that opened each visited page so Prev/Next\n * are exact, and exposes a 0-indexed `page` for display.\n */\n\nimport { useCallback, useState } from 'react';\n\nexport interface CursorPager {\n /** 0-indexed page number for display. */\n page: number;\n /** Cursor that opens the current page (null = first page). */\n cursor: string | null;\n /** Advance to the next page using the current page's end cursor. No-op if null. */\n next: (endCursor: string | null | undefined) => void;\n /** Go back one page. No-op on the first page. */\n prev: () => void;\n /** Reset to the first page (call when filters change). */\n reset: () => void;\n /**\n * Adapter for `<Pagination onPageChange>` — compares the requested page to the\n * current one and calls next()/prev() accordingly.\n */\n onPageChange: (targetPage: number, endCursor: string | null | undefined) => void;\n}\n\nexport function useCursorPager(): CursorPager {\n const [stack, setStack] = useState<(string | null)[]>([null]);\n const page = stack.length - 1;\n const cursor = stack[page] ?? null;\n\n const next = useCallback((endCursor: string | null | undefined) => {\n if (endCursor) setStack((s) => [...s, endCursor]);\n }, []);\n\n const prev = useCallback(() => {\n setStack((s) => (s.length > 1 ? s.slice(0, -1) : s));\n }, []);\n\n const reset = useCallback(() => {\n setStack([null]);\n }, []);\n\n const onPageChange = useCallback((targetPage: number, endCursor: string | null | undefined) => {\n setStack((s) => {\n const current = s.length - 1;\n if (targetPage > current) {\n return endCursor ? [...s, endCursor] : s;\n }\n return s.length > 1 ? s.slice(0, -1) : s;\n });\n }, []);\n\n return { page, cursor, next, prev, reset, onPageChange };\n}\n"],"mappings":";;AA+BA,SAAgB,IAA8B;CAC5C,IAAM,CAAC,GAAO,KAAY,EAA4B,CAAC,KAAK,CAAC,EACvD,IAAO,EAAM,SAAS;AAyB5B,QAAO;EAAE;EAAM,QAxBA,EAAM,MAAS;EAwBP,MAtBV,GAAa,MAAyC;AACjE,GAAI,KAAW,GAAU,MAAM,CAAC,GAAG,GAAG,EAAU,CAAC;KAChD,EAAE,CAAC;EAoBuB,MAlBhB,QAAkB;AAC7B,MAAU,MAAO,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,EAAG;KACnD,EAAE,CAAC;EAgB6B,OAdrB,QAAkB;AAC9B,KAAS,CAAC,KAAK,CAAC;KACf,EAAE,CAAC;EAYoC,cAVrB,GAAa,GAAoB,MAAyC;AAC7F,MAAU,MAEJ,IADY,EAAE,SAAS,IAElB,IAAY,CAAC,GAAG,GAAG,EAAU,GAAG,IAElC,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,EACvC;KACD,EAAE,CAAC;EAEkD"}
1
+ {"version":3,"file":"useCursorPager.js","names":[],"sources":["../../../src/ecoimpacthub/hooks/useCursorPager.ts"],"sourcesContent":["/**\n * useCursorPager — forward/back cursor pagination for Eco* list connections.\n * @module ecoimpacthub/hooks\n *\n * The svc paginates by cursor only (`PaginationInput.offset` is a no-op — the\n * resolver never sets Prisma `skip` from it). So a page must send\n * `pagination.cursor = the previous page's pageInfo.cursor` to advance. This\n * hook keeps a stack of the cursors that opened each visited page so Prev/Next\n * are exact, and exposes a 0-indexed `page` for display.\n */\n\nimport { useCallback, useState } from 'react';\n\nexport interface CursorPager {\n /** 0-indexed page number for display. */\n page: number;\n /**\n * Cursor that opens the current page — `undefined` on the first page so it is\n * OMITTED from the GraphQL variables (the svc rejects an explicit `cursor: null`;\n * a first page must send no cursor at all, per cursor-pagination convention).\n */\n cursor: string | undefined;\n /** Advance to the next page using the current page's end cursor. No-op if null. */\n next: (endCursor: string | null | undefined) => void;\n /** Go back one page. No-op on the first page. */\n prev: () => void;\n /** Reset to the first page (call when filters change). */\n reset: () => void;\n /**\n * Adapter for `<Pagination onPageChange>` — compares the requested page to the\n * current one and calls next()/prev() accordingly.\n */\n onPageChange: (targetPage: number, endCursor: string | null | undefined) => void;\n}\n\nexport function useCursorPager(): CursorPager {\n const [stack, setStack] = useState<(string | null)[]>([null]);\n const page = stack.length - 1;\n const cursor = stack[page] ?? undefined;\n\n const next = useCallback((endCursor: string | null | undefined) => {\n if (endCursor) setStack((s) => [...s, endCursor]);\n }, []);\n\n const prev = useCallback(() => {\n setStack((s) => (s.length > 1 ? s.slice(0, -1) : s));\n }, []);\n\n const reset = useCallback(() => {\n setStack([null]);\n }, []);\n\n const onPageChange = useCallback((targetPage: number, endCursor: string | null | undefined) => {\n setStack((s) => {\n const current = s.length - 1;\n if (targetPage > current) {\n return endCursor ? [...s, endCursor] : s;\n }\n return s.length > 1 ? s.slice(0, -1) : s;\n });\n }, []);\n\n return { page, cursor, next, prev, reset, onPageChange };\n}\n"],"mappings":";;AAmCA,SAAgB,IAA8B;CAC5C,IAAM,CAAC,GAAO,KAAY,EAA4B,CAAC,KAAK,CAAC,EACvD,IAAO,EAAM,SAAS;AAyB5B,QAAO;EAAE;EAAM,QAxBA,EAAM,MAAS,KAAA;EAwBP,MAtBV,GAAa,MAAyC;AACjE,GAAI,KAAW,GAAU,MAAM,CAAC,GAAG,GAAG,EAAU,CAAC;KAChD,EAAE,CAAC;EAoBuB,MAlBhB,QAAkB;AAC7B,MAAU,MAAO,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,EAAG;KACnD,EAAE,CAAC;EAgB6B,OAdrB,QAAkB;AAC9B,KAAS,CAAC,KAAK,CAAC;KACf,EAAE,CAAC;EAYoC,cAVrB,GAAa,GAAoB,MAAyC;AAC7F,MAAU,MAEJ,IADY,EAAE,SAAS,IAElB,IAAY,CAAC,GAAG,GAAG,EAAU,GAAG,IAElC,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,EACvC;KACD,EAAE,CAAC;EAEkD"}
@@ -0,0 +1,45 @@
1
+ import { useEcoImpactHub as e } from "../context/EcoImpactHubContext.js";
2
+ import "../context/index.js";
3
+ import { wspaceFetch as t } from "./useEcoFiles.js";
4
+ import { useMutation as n, useQuery as r, useQueryClient as i } from "@tanstack/react-query";
5
+ //#region src/ecoimpacthub/hooks/useEcoComments.ts
6
+ var a = (e, t) => [
7
+ "ecoimpacthub",
8
+ "comments",
9
+ e,
10
+ t
11
+ ], o = (n, i) => {
12
+ let { apiGatewayUrl: o } = e();
13
+ return r({
14
+ queryKey: a(n, i ?? ""),
15
+ enabled: !!i,
16
+ queryFn: async () => {
17
+ let e = await t("query EcoGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }", { input: {
18
+ entityType: n,
19
+ entityId: i,
20
+ take: 100,
21
+ parentCommentId: null
22
+ } }, o), r = e.getEntityComments.items.filter((e) => !e.parentCommentId);
23
+ return r.sort((e, t) => new Date(e.createdAt).getTime() - new Date(t.createdAt).getTime()), {
24
+ items: r,
25
+ total: e.getEntityComments.total
26
+ };
27
+ }
28
+ });
29
+ }, s = (r, o) => {
30
+ let { apiGatewayUrl: s } = e(), c = i();
31
+ return n({
32
+ mutationFn: async (e) => (await t("mutation EcoCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }", { input: {
33
+ entityType: r,
34
+ entityId: o,
35
+ content: { text: e }
36
+ } }, s)).createEntityComment,
37
+ onSuccess: () => {
38
+ c.invalidateQueries({ queryKey: a(r, o) });
39
+ }
40
+ });
41
+ };
42
+ //#endregion
43
+ export { o as useEcoComments, s as useEcoCreateComment };
44
+
45
+ //# sourceMappingURL=useEcoComments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEcoComments.js","names":[],"sources":["../../../src/ecoimpacthub/hooks/useEcoComments.ts"],"sourcesContent":["/**\n * EcoImpactHub — wspace-conversations-svc EntityComment REUSE hooks (Integration B).\n * @module ecoimpacthub/hooks/useEcoComments\n *\n * Adds a discussion surface to eco entities WITHOUT any eco-svc change by\n * reusing the generic EntityComment API composed into the WORKSPACE supergraph.\n * Comments attach to any entity by `(entityType, entityId)`:\n * - `getEntityComments(input): EntityCommentConnection!`\n * - `createEntityComment(input): EntityComment!`\n *\n * Transport reuses `wspaceFetch` from `useEcoFiles` (workspace gateway +\n * workspace token). Lives in a separate file; `useEcoImpactHubApi.ts` is\n * untouched.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useEcoImpactHub } from '../context';\nimport { wspaceFetch } from './useEcoFiles';\n\nexport interface EcoComment {\n id: string;\n authorId: string;\n authorType: string;\n content: {\n text: string | null;\n markdown: string | null;\n };\n createdAt: string;\n edited: boolean;\n parentCommentId: string | null;\n replyCount: number;\n}\n\nconst commentsKey = (entityType: string, entityId: string) =>\n ['ecoimpacthub', 'comments', entityType, entityId] as const;\n\n/** Top-level (root) comments for an eco entity, oldest first. */\nexport const useEcoComments = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useEcoImpactHub();\n return useQuery({\n queryKey: commentsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{\n getEntityComments: { items: EcoComment[]; total: number };\n }>(\n `query EcoGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }`,\n {\n input: {\n entityType,\n entityId,\n take: 100,\n parentCommentId: null,\n },\n },\n apiGatewayUrl\n );\n // Render root comments oldest-first (server order is not guaranteed).\n const roots = data.getEntityComments.items.filter((c) => !c.parentCommentId);\n roots.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());\n return { items: roots, total: data.getEntityComments.total };\n },\n });\n};\n\nexport const useEcoCreateComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useEcoImpactHub();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (text: string) => {\n const data = await wspaceFetch<{ createEntityComment: { id: string } }>(\n `mutation EcoCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }`,\n {\n input: {\n entityType,\n entityId,\n content: { text },\n },\n },\n apiGatewayUrl\n );\n return data.createEntityComment;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: commentsKey(entityType, entityId) });\n },\n });\n};\n"],"mappings":";;;;;AAkCA,IAAM,KAAe,GAAoB,MACvC;CAAC;CAAgB;CAAY;CAAY;CAAS,EAGvC,KAAkB,GAAoB,MAAiC;CAClF,IAAM,EAAE,qBAAkB,GAAiB;AAC3C,QAAO,EAAS;EACd,UAAU,EAAY,GAAY,KAAY,GAAG;EACjD,SAAS,EAAQ;EACjB,SAAS,YAAY;GACnB,IAAM,IAAO,MAAM,EAGjB,6YAeA,EACE,OAAO;IACL;IACA;IACA,MAAM;IACN,iBAAiB;IAClB,EACF,EACD,EACD,EAEK,IAAQ,EAAK,kBAAkB,MAAM,QAAQ,MAAM,CAAC,EAAE,gBAAgB;AAE5E,UADA,EAAM,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,EAChF;IAAE,OAAO;IAAO,OAAO,EAAK,kBAAkB;IAAO;;EAE/D,CAAC;GAGS,KAAuB,GAAoB,MAAqB;CAC3E,IAAM,EAAE,qBAAkB,GAAiB,EACrC,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,YAAY,OAAO,OACJ,MAAM,EACjB,wIAGA,EACE,OAAO;GACL;GACA;GACA,SAAS,EAAE,SAAM;GAClB,EACF,EACD,EACD,EACW;EAEd,iBAAiB;AACf,KAAG,kBAAkB,EAAE,UAAU,EAAY,GAAY,EAAS,EAAE,CAAC;;EAExE,CAAC"}