@burdenoff/microfe-ecoimpacthub 2026.1030.3 → 2026.1031.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 +1 @@
1
- {"version":3,"file":"AiQuotaBanner.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/pages/carbon/AiQuotaBanner.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAE9D,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAE1D,UAAU,kBAAkB;IAC1B,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,EAAE,kBAAkB,2CA6BzD"}
1
+ {"version":3,"file":"AiQuotaBanner.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/pages/carbon/AiQuotaBanner.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAE9D,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAE1D,UAAU,kBAAkB;IAC1B,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,EAAE,kBAAkB,2CA0CzD"}
@@ -5,17 +5,20 @@ import { jsx as r } from "react/jsx-runtime";
5
5
  import { UpgradeInlineBanner as i, dispatchUpgradePrompt as a } from "@burdenoff/fe-libs/shared/components";
6
6
  function o({ wall: o }) {
7
7
  let s = e(), c = o.quotaName ?? "ecoimpacthub.ai.credits";
8
- return t(() => {
8
+ t(() => {
9
9
  a({ extensions: {
10
10
  code: "QUOTA_EXHAUSTED",
11
11
  quotaName: c
12
12
  } });
13
- }, [c]), /* @__PURE__ */ r("div", {
13
+ }, [c]);
14
+ let l = o.kind === "not-provisioned";
15
+ return /* @__PURE__ */ r("div", {
14
16
  "data-testid": "eco-ai-quota-banner",
17
+ "data-quota-block": o.kind,
15
18
  children: /* @__PURE__ */ r(i, {
16
19
  severity: "critical",
17
- title: s("ecoCarbon.ai.quota.title", "AI credits used up"),
18
- message: s("ecoCarbon.ai.quota.message", "Your plan has no AI credits left for EcoImpactHub this period. Upgrade to keep drafting ledger entries with AI, or record entries by hand."),
20
+ title: l ? s("ecoCarbon.ai.quota.notProvisionedTitle", "AI credits are not in your plan") : s("ecoCarbon.ai.quota.title", "AI credits used up"),
21
+ message: l ? s("ecoCarbon.ai.quota.notProvisionedMessage", "Your workspace's plan does not include EcoImpactHub AI credits. Choose a plan that includes them to use AI drafting and factor suggestions, or record entries by hand.") : s("ecoCarbon.ai.quota.message", "Your plan has no AI credits left for EcoImpactHub this period. Upgrade to keep drafting ledger entries with AI, or record entries by hand."),
19
22
  upgradeHref: "/billing/plans",
20
23
  renderCta: ({ href: e }) => /* @__PURE__ */ r(n, {
21
24
  to: e,
@@ -1 +1 @@
1
- {"version":3,"file":"AiQuotaBanner.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/carbon/AiQuotaBanner.tsx"],"sourcesContent":["/**\n * AI-credit quota wall for the carbon-ledger AI actions.\n * @module ecoimpacthub/pages/carbon/AiQuotaBanner\n *\n * Shown when `ecoAiDraftCarbonEntries` / `ecoAiSuggestEmissionFactor` come back\n * `QUOTA_EXHAUSTED` on `ecoimpacthub.ai.credits`. It renders the fe-libs\n * `UpgradeInlineBanner` (semantic tokens, upgrade CTA to `/billing/plans`) and\n * also dispatches the fe-libs global upgrade prompt once per wall, so a host\n * shell that mounts `UpgradePromptModal` opens its dialog as well. The global\n * backend-error toast deliberately skips QUOTA_EXHAUSTED, so without this the\n * refusal would be silent.\n */\n\nimport { useEffect } from 'react';\nimport { Link } from 'react-router';\nimport { UpgradeInlineBanner, dispatchUpgradePrompt } from '@burdenoff/fe-libs/shared/components';\nimport { useTr } from '../../hooks/useTr';\nimport type { EcoQuotaWall } from '../../utils/graphqlErrors';\n\nexport const AI_CREDITS_QUOTA = 'ecoimpacthub.ai.credits';\n\ninterface AiQuotaBannerProps {\n wall: EcoQuotaWall;\n}\n\nexport function AiQuotaBanner({ wall }: AiQuotaBannerProps) {\n const tr = useTr();\n const quotaName = wall.quotaName ?? AI_CREDITS_QUOTA;\n\n useEffect(() => {\n dispatchUpgradePrompt({ extensions: { code: 'QUOTA_EXHAUSTED', quotaName } });\n }, [quotaName]);\n\n return (\n <div data-testid=\"eco-ai-quota-banner\">\n <UpgradeInlineBanner\n severity=\"critical\"\n title={tr('ecoCarbon.ai.quota.title', 'AI credits used up')}\n message={tr(\n 'ecoCarbon.ai.quota.message',\n 'Your plan has no AI credits left for EcoImpactHub this period. Upgrade to keep drafting ledger entries with AI, or record entries by hand.'\n )}\n upgradeHref=\"/billing/plans\"\n renderCta={({ href }) => (\n <Link\n to={href}\n className=\"rounded-md bg-action-primary-bg px-2.5 py-1 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover\"\n >\n {tr('ecoCarbon.ai.quota.upgrade', 'View plans')}\n </Link>\n )}\n />\n </div>\n );\n}\n"],"mappings":";;;;;AAyBA,SAAgB,EAAc,EAAE,WAA4B;CAC1D,IAAM,IAAK,EAAM,GACX,IAAY,EAAK,aAAA;CAMvB,OAJA,QAAgB;EACd,EAAsB,EAAE,YAAY;GAAE,MAAM;GAAmB;EAAU,EAAE,CAAC;CAC9E,GAAG,CAAC,CAAS,CAAC,GAGZ,kBAAC,OAAD;EAAK,eAAY;YACf,kBAAC,GAAD;GACE,UAAS;GACT,OAAO,EAAG,4BAA4B,oBAAoB;GAC1D,SAAS,EACP,8BACA,4IACF;GACA,aAAY;GACZ,YAAY,EAAE,cACZ,kBAAC,GAAD;IACE,IAAI;IACJ,WAAU;cAET,EAAG,8BAA8B,YAAY;GAC1C,CAAA;EAET,CAAA;CACE,CAAA;AAET"}
1
+ {"version":3,"file":"AiQuotaBanner.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/carbon/AiQuotaBanner.tsx"],"sourcesContent":["/**\n * AI-credit quota wall for the carbon-ledger AI actions.\n * @module ecoimpacthub/pages/carbon/AiQuotaBanner\n *\n * Shown when `ecoAiDraftCarbonEntries` / `ecoAiSuggestEmissionFactor` are\n * refused by the gateway `@usage` gate on `ecoimpacthub.ai.credits`, either\n * because the credits are used up (`QUOTA_EXHAUSTED`) or because the\n * workspace's plan does not include them at all (`QUOTA_NOT_PROVISIONED`).\n * The copy differs between the two: telling someone with no plan that their\n * credits are \"used up\" is wrong. It renders the fe-libs\n * `UpgradeInlineBanner` (semantic tokens, upgrade CTA to `/billing/plans`) and\n * also dispatches the fe-libs global upgrade prompt once per wall, so a host\n * shell that mounts `UpgradePromptModal` opens its dialog as well. The global\n * backend-error toast deliberately skips QUOTA_EXHAUSTED, so without this the\n * refusal would be silent. The global prompt is dispatched as QUOTA_EXHAUSTED\n * for both kinds because that is the upgrade code the fe-libs modal accepts;\n * the inline banner carries the kind-specific wording.\n */\n\nimport { useEffect } from 'react';\nimport { Link } from 'react-router';\nimport { UpgradeInlineBanner, dispatchUpgradePrompt } from '@burdenoff/fe-libs/shared/components';\nimport { useTr } from '../../hooks/useTr';\nimport type { EcoQuotaWall } from '../../utils/graphqlErrors';\n\nexport const AI_CREDITS_QUOTA = 'ecoimpacthub.ai.credits';\n\ninterface AiQuotaBannerProps {\n wall: EcoQuotaWall;\n}\n\nexport function AiQuotaBanner({ wall }: AiQuotaBannerProps) {\n const tr = useTr();\n const quotaName = wall.quotaName ?? AI_CREDITS_QUOTA;\n\n useEffect(() => {\n dispatchUpgradePrompt({ extensions: { code: 'QUOTA_EXHAUSTED', quotaName } });\n }, [quotaName]);\n\n const notProvisioned = wall.kind === 'not-provisioned';\n\n return (\n <div data-testid=\"eco-ai-quota-banner\" data-quota-block={wall.kind}>\n <UpgradeInlineBanner\n severity=\"critical\"\n title={\n notProvisioned\n ? tr('ecoCarbon.ai.quota.notProvisionedTitle', 'AI credits are not in your plan')\n : tr('ecoCarbon.ai.quota.title', 'AI credits used up')\n }\n message={\n notProvisioned\n ? tr(\n 'ecoCarbon.ai.quota.notProvisionedMessage',\n \"Your workspace's plan does not include EcoImpactHub AI credits. Choose a plan that includes them to use AI drafting and factor suggestions, or record entries by hand.\"\n )\n : tr(\n 'ecoCarbon.ai.quota.message',\n 'Your plan has no AI credits left for EcoImpactHub this period. Upgrade to keep drafting ledger entries with AI, or record entries by hand.'\n )\n }\n upgradeHref=\"/billing/plans\"\n renderCta={({ href }) => (\n <Link\n to={href}\n className=\"rounded-md bg-action-primary-bg px-2.5 py-1 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover\"\n >\n {tr('ecoCarbon.ai.quota.upgrade', 'View plans')}\n </Link>\n )}\n />\n </div>\n );\n}\n"],"mappings":";;;;;AA+BA,SAAgB,EAAc,EAAE,WAA4B;CAC1D,IAAM,IAAK,EAAM,GACX,IAAY,EAAK,aAAA;CAEvB,QAAgB;EACd,EAAsB,EAAE,YAAY;GAAE,MAAM;GAAmB;EAAU,EAAE,CAAC;CAC9E,GAAG,CAAC,CAAS,CAAC;CAEd,IAAM,IAAiB,EAAK,SAAS;CAErC,OACE,kBAAC,OAAD;EAAK,eAAY;EAAsB,oBAAkB,EAAK;YAC5D,kBAAC,GAAD;GACE,UAAS;GACT,OACE,IACI,EAAG,0CAA0C,iCAAiC,IAC9E,EAAG,4BAA4B,oBAAoB;GAEzD,SACE,IACI,EACE,4CACA,wKACF,IACA,EACE,8BACA,4IACF;GAEN,aAAY;GACZ,YAAY,EAAE,cACZ,kBAAC,GAAD;IACE,IAAI;IACJ,WAAU;cAET,EAAG,8BAA8B,YAAY;GAC1C,CAAA;EAET,CAAA;CACE,CAAA;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"MobileShell.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/pages/mobile/MobileShell.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAwD3C,UAAU,gBAAgB;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CA2D5C,CAAC"}
1
+ {"version":3,"file":"MobileShell.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/pages/mobile/MobileShell.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAgEnE,UAAU,gBAAgB;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CA2E5C,CAAC"}
@@ -3,17 +3,18 @@ import "../../context/index.js";
3
3
  import { cn as t } from "../../utils/cn.js";
4
4
  import { useTr as n } from "../../hooks/useTr.js";
5
5
  import "../../hooks/index.js";
6
- import { Binoculars as r, Factory as i, House as a, Map as o, PawPrint as s } from "lucide-react";
7
- import { useLocation as c } from "react-router";
8
- import { jsx as l, jsxs as u } from "react/jsx-runtime";
6
+ import { useEffect as r, useRef as i } from "react";
7
+ import { Binoculars as a, Factory as o, House as s, Map as c, PawPrint as l } from "lucide-react";
8
+ import { useLocation as u } from "react-router";
9
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
9
10
  //#region src/ecoimpacthub/pages/mobile/MobileShell.tsx
10
- var d = [
11
+ var p = "--mobile-tabbar-height", m = [
11
12
  {
12
13
  id: "home",
13
14
  path: "mobile",
14
15
  labelKey: "ecoField.tabs.home",
15
16
  label: "Home",
16
- icon: /* @__PURE__ */ l(a, {
17
+ icon: /* @__PURE__ */ d(s, {
17
18
  className: "h-5 w-5",
18
19
  "aria-hidden": "true"
19
20
  })
@@ -23,7 +24,7 @@ var d = [
23
24
  path: "sites",
24
25
  labelKey: "ecoField.tabs.sites",
25
26
  label: "Sites",
26
- icon: /* @__PURE__ */ l(o, {
27
+ icon: /* @__PURE__ */ d(c, {
27
28
  className: "h-5 w-5",
28
29
  "aria-hidden": "true"
29
30
  })
@@ -33,7 +34,7 @@ var d = [
33
34
  path: "mobile/record-observation",
34
35
  labelKey: "ecoField.tabs.record",
35
36
  label: "Record",
36
- icon: /* @__PURE__ */ l(r, {
37
+ icon: /* @__PURE__ */ d(a, {
37
38
  className: "h-5 w-5",
38
39
  "aria-hidden": "true"
39
40
  }),
@@ -44,7 +45,7 @@ var d = [
44
45
  path: "species",
45
46
  labelKey: "ecoField.tabs.species",
46
47
  label: "Species",
47
- icon: /* @__PURE__ */ l(s, {
48
+ icon: /* @__PURE__ */ d(l, {
48
49
  className: "h-5 w-5",
49
50
  "aria-hidden": "true"
50
51
  })
@@ -54,47 +55,57 @@ var d = [
54
55
  path: "carbon",
55
56
  labelKey: "ecoField.tabs.carbon",
56
57
  label: "Carbon",
57
- icon: /* @__PURE__ */ l(i, {
58
+ icon: /* @__PURE__ */ d(o, {
58
59
  className: "h-5 w-5",
59
60
  "aria-hidden": "true"
60
61
  })
61
62
  }
62
- ], f = ({ children: r, pendingCount: i = 0 }) => {
63
- let a = n(), { getPath: o, navigateTo: s } = e(), { pathname: f } = c();
64
- return /* @__PURE__ */ u("div", {
63
+ ], h = ({ children: a, pendingCount: o = 0 }) => {
64
+ let s = n(), { getPath: c, navigateTo: l } = e(), { pathname: h } = u(), g = i(null);
65
+ return r(() => {
66
+ let e = g.current;
67
+ if (!e) return;
68
+ let t = document.documentElement, n = () => t.style.setProperty(p, `${e.offsetHeight}px`);
69
+ n();
70
+ let r = typeof ResizeObserver > "u" ? null : new ResizeObserver(n);
71
+ return r?.observe(e), () => {
72
+ r?.disconnect(), t.style.removeProperty(p);
73
+ };
74
+ }, []), /* @__PURE__ */ f("div", {
65
75
  className: "flex min-h-full flex-col bg-bg-canvas",
66
- children: [/* @__PURE__ */ l("div", {
76
+ children: [/* @__PURE__ */ d("div", {
67
77
  className: "mx-auto w-full max-w-xl flex-1 px-4 pb-28 pt-4",
68
- children: r
69
- }), /* @__PURE__ */ l("nav", {
70
- "aria-label": a("ecoField.tabs.label", "Field app"),
78
+ children: a
79
+ }), /* @__PURE__ */ d("nav", {
80
+ ref: g,
81
+ "aria-label": s("ecoField.tabs.label", "Field app"),
71
82
  className: "fixed inset-x-0 bottom-0 z-30 border-t border-border-seam bg-bg-surface/95 backdrop-blur",
72
83
  style: { paddingBottom: "env(safe-area-inset-bottom, 0px)" },
73
- children: /* @__PURE__ */ l("ul", {
84
+ children: /* @__PURE__ */ d("ul", {
74
85
  className: "mx-auto flex max-w-xl items-stretch justify-between px-2",
75
- children: d.map((e) => {
76
- let n = o(e.path), r = f === n || f === `${n}/`;
77
- return /* @__PURE__ */ l("li", {
86
+ children: m.map((e) => {
87
+ let n = c(e.path), r = h === n || h === `${n}/`;
88
+ return /* @__PURE__ */ d("li", {
78
89
  className: "flex-1",
79
- children: /* @__PURE__ */ u("button", {
90
+ children: /* @__PURE__ */ f("button", {
80
91
  type: "button",
81
- onClick: () => s(e.path),
92
+ onClick: () => l(e.path),
82
93
  "aria-current": r ? "page" : void 0,
83
94
  "data-testid": `field-tab-${e.id}`,
84
95
  className: t("relative flex w-full flex-col items-center gap-0.5 py-2 text-[11px] font-medium transition-colors", r ? "text-action-primary-bg" : "text-text-tertiary hover:text-text-primary"),
85
96
  children: [
86
- r && /* @__PURE__ */ l("span", {
97
+ r && /* @__PURE__ */ d("span", {
87
98
  "aria-hidden": "true",
88
99
  className: "absolute inset-x-4 top-0 h-0.5 rounded-full bg-action-primary-bg"
89
100
  }),
90
- /* @__PURE__ */ u("span", {
101
+ /* @__PURE__ */ f("span", {
91
102
  className: t("relative flex items-center justify-center", e.primary && "h-9 w-9 rounded-full bg-action-primary-bg text-action-primary-text shadow"),
92
- children: [e.icon, e.primary && i > 0 && /* @__PURE__ */ l("span", {
103
+ children: [e.icon, e.primary && o > 0 && /* @__PURE__ */ d("span", {
93
104
  className: "absolute -right-1.5 -top-1.5 min-w-[1.1rem] rounded-full bg-status-warning-text px-1 text-center text-[10px] font-semibold leading-[1.1rem] text-bg-surface",
94
- children: i
105
+ children: o
95
106
  })]
96
107
  }),
97
- a(e.labelKey, e.label)
108
+ s(e.labelKey, e.label)
98
109
  ]
99
110
  })
100
111
  }, e.id);
@@ -104,6 +115,6 @@ var d = [
104
115
  });
105
116
  };
106
117
  //#endregion
107
- export { f as MobileShell };
118
+ export { h as MobileShell };
108
119
 
109
120
  //# sourceMappingURL=MobileShell.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MobileShell.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/mobile/MobileShell.tsx"],"sourcesContent":["/**\n * MobileShell — the field app's frame: content plus a bottom tab bar.\n * @module ecoimpacthub/pages/mobile\n *\n * Mock 15's bottom navigation (Home · Sites · Species · Carbon) with the field\n * capture action in the middle, where a thumb reaches it. The bar honours the\n * device safe area so it clears the iOS home indicator inside the Capacitor\n * WebView. Every tab is a real route of this MFE.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useLocation } from 'react-router';\nimport { Binoculars, Factory, House, Map as MapIcon, PawPrint } from 'lucide-react';\nimport { useEcoImpactHub } from '../../context';\nimport { useTr } from '../../hooks';\nimport { cn } from '../../utils/cn';\n\ninterface TabDef {\n id: string;\n /** Path relative to the MFE base path. */\n path: string;\n labelKey: string;\n label: string;\n icon: ReactNode;\n primary?: boolean;\n}\n\nconst TABS: TabDef[] = [\n {\n id: 'home',\n path: 'mobile',\n labelKey: 'ecoField.tabs.home',\n label: 'Home',\n icon: <House className=\"h-5 w-5\" aria-hidden=\"true\" />,\n },\n {\n id: 'sites',\n path: 'sites',\n labelKey: 'ecoField.tabs.sites',\n label: 'Sites',\n icon: <MapIcon className=\"h-5 w-5\" aria-hidden=\"true\" />,\n },\n {\n id: 'record',\n path: 'mobile/record-observation',\n labelKey: 'ecoField.tabs.record',\n label: 'Record',\n icon: <Binoculars className=\"h-5 w-5\" aria-hidden=\"true\" />,\n primary: true,\n },\n {\n id: 'species',\n path: 'species',\n labelKey: 'ecoField.tabs.species',\n label: 'Species',\n icon: <PawPrint className=\"h-5 w-5\" aria-hidden=\"true\" />,\n },\n {\n id: 'carbon',\n path: 'carbon',\n labelKey: 'ecoField.tabs.carbon',\n label: 'Carbon',\n icon: <Factory className=\"h-5 w-5\" aria-hidden=\"true\" />,\n },\n];\n\ninterface MobileShellProps {\n children: ReactNode;\n /** Badge on the Record tab: observations still waiting to sync. */\n pendingCount?: number;\n}\n\nexport const MobileShell: FC<MobileShellProps> = ({ children, pendingCount = 0 }) => {\n const tr = useTr();\n const { getPath, navigateTo } = useEcoImpactHub();\n const { pathname } = useLocation();\n\n return (\n <div className=\"flex min-h-full flex-col bg-bg-canvas\">\n <div className=\"mx-auto w-full max-w-xl flex-1 px-4 pb-28 pt-4\">{children}</div>\n\n <nav\n aria-label={tr('ecoField.tabs.label', 'Field app')}\n className=\"fixed inset-x-0 bottom-0 z-30 border-t border-border-seam bg-bg-surface/95 backdrop-blur\"\n style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }}\n >\n <ul className=\"mx-auto flex max-w-xl items-stretch justify-between px-2\">\n {TABS.map((tab) => {\n const href = getPath(tab.path);\n const active = pathname === href || pathname === `${href}/`;\n return (\n <li key={tab.id} className=\"flex-1\">\n <button\n type=\"button\"\n onClick={() => navigateTo(tab.path)}\n aria-current={active ? 'page' : undefined}\n data-testid={`field-tab-${tab.id}`}\n className={cn(\n 'relative flex w-full flex-col items-center gap-0.5 py-2 text-[11px] font-medium transition-colors',\n active ? 'text-action-primary-bg' : 'text-text-tertiary hover:text-text-primary'\n )}\n >\n {active && (\n <span\n aria-hidden=\"true\"\n className=\"absolute inset-x-4 top-0 h-0.5 rounded-full bg-action-primary-bg\"\n />\n )}\n <span\n className={cn(\n 'relative flex items-center justify-center',\n tab.primary &&\n 'h-9 w-9 rounded-full bg-action-primary-bg text-action-primary-text shadow'\n )}\n >\n {tab.icon}\n {tab.primary && pendingCount > 0 && (\n <span className=\"absolute -right-1.5 -top-1.5 min-w-[1.1rem] rounded-full bg-status-warning-text px-1 text-center text-[10px] font-semibold leading-[1.1rem] text-bg-surface\">\n {pendingCount}\n </span>\n )}\n </span>\n {tr(tab.labelKey, tab.label)}\n </button>\n </li>\n );\n })}\n </ul>\n </nav>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;AA2BA,IAAM,IAAiB;CACrB;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAO,WAAU;GAAU,eAAY;EAAQ,CAAA;CACvD;CACA;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAS,WAAU;GAAU,eAAY;EAAQ,CAAA;CACzD;CACA;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAY,WAAU;GAAU,eAAY;EAAQ,CAAA;EAC1D,SAAS;CACX;CACA;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAU,WAAU;GAAU,eAAY;EAAQ,CAAA;CAC1D;CACA;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAS,WAAU;GAAU,eAAY;EAAQ,CAAA;CACzD;AACF,GAQa,KAAqC,EAAE,aAAU,kBAAe,QAAQ;CACnF,IAAM,IAAK,EAAM,GACX,EAAE,YAAS,kBAAe,EAAgB,GAC1C,EAAE,gBAAa,EAAY;CAEjC,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;GAAkD;EAAc,CAAA,GAE/E,kBAAC,OAAD;GACE,cAAY,EAAG,uBAAuB,WAAW;GACjD,WAAU;GACV,OAAO,EAAE,eAAe,mCAAmC;aAE3D,kBAAC,MAAD;IAAI,WAAU;cACX,EAAK,KAAK,MAAQ;KACjB,IAAM,IAAO,EAAQ,EAAI,IAAI,GACvB,IAAS,MAAa,KAAQ,MAAa,GAAG,EAAK;KACzD,OACE,kBAAC,MAAD;MAAiB,WAAU;gBACzB,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,EAAI,IAAI;OAClC,gBAAc,IAAS,SAAS,KAAA;OAChC,eAAa,aAAa,EAAI;OAC9B,WAAW,EACT,qGACA,IAAS,2BAA2B,4CACtC;iBARF;QAUG,KACC,kBAAC,QAAD;SACE,eAAY;SACZ,WAAU;QACX,CAAA;QAEH,kBAAC,QAAD;SACE,WAAW,EACT,6CACA,EAAI,WACF,2EACJ;mBALF,CAOG,EAAI,MACJ,EAAI,WAAW,IAAe,KAC7B,kBAAC,QAAD;UAAM,WAAU;oBACb;SACG,CAAA,CAEJ;;QACL,EAAG,EAAI,UAAU,EAAI,KAAK;OACrB;;KACN,GAjCK,EAAI,EAiCT;IAER,CAAC;GACC,CAAA;EACD,CAAA,CACF;;AAET"}
1
+ {"version":3,"file":"MobileShell.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/mobile/MobileShell.tsx"],"sourcesContent":["/**\n * MobileShell — the field app's frame: content plus a bottom tab bar.\n * @module ecoimpacthub/pages/mobile\n *\n * Mock 15's bottom navigation (Home · Sites · Species · Carbon) with the field\n * capture action in the middle, where a thumb reaches it. The bar honours the\n * device safe area so it clears the iOS home indicator inside the Capacitor\n * WebView. Every tab is a real route of this MFE.\n */\n\nimport { useEffect, useRef, type FC, type ReactNode } from 'react';\nimport { useLocation } from 'react-router';\nimport { Binoculars, Factory, House, Map as MapIcon, PawPrint } from 'lucide-react';\nimport { useEcoImpactHub } from '../../context';\nimport { useTr } from '../../hooks';\nimport { cn } from '../../utils/cn';\n\ninterface TabDef {\n id: string;\n /** Path relative to the MFE base path. */\n path: string;\n labelKey: string;\n label: string;\n icon: ReactNode;\n primary?: boolean;\n}\n\n/**\n * Same contract as the fe-libs shell `MobileTabBar`: publish the bar's height\n * as `--mobile-tabbar-height` on <html>, which floating chrome (the AI\n * assistant launcher, notification panel) already reads to sit above a\n * bottom tab bar instead of covering its right-most tab.\n */\nconst TABBAR_HEIGHT_VAR = '--mobile-tabbar-height';\n\nconst TABS: TabDef[] = [\n {\n id: 'home',\n path: 'mobile',\n labelKey: 'ecoField.tabs.home',\n label: 'Home',\n icon: <House className=\"h-5 w-5\" aria-hidden=\"true\" />,\n },\n {\n id: 'sites',\n path: 'sites',\n labelKey: 'ecoField.tabs.sites',\n label: 'Sites',\n icon: <MapIcon className=\"h-5 w-5\" aria-hidden=\"true\" />,\n },\n {\n id: 'record',\n path: 'mobile/record-observation',\n labelKey: 'ecoField.tabs.record',\n label: 'Record',\n icon: <Binoculars className=\"h-5 w-5\" aria-hidden=\"true\" />,\n primary: true,\n },\n {\n id: 'species',\n path: 'species',\n labelKey: 'ecoField.tabs.species',\n label: 'Species',\n icon: <PawPrint className=\"h-5 w-5\" aria-hidden=\"true\" />,\n },\n {\n id: 'carbon',\n path: 'carbon',\n labelKey: 'ecoField.tabs.carbon',\n label: 'Carbon',\n icon: <Factory className=\"h-5 w-5\" aria-hidden=\"true\" />,\n },\n];\n\ninterface MobileShellProps {\n children: ReactNode;\n /** Badge on the Record tab: observations still waiting to sync. */\n pendingCount?: number;\n}\n\nexport const MobileShell: FC<MobileShellProps> = ({ children, pendingCount = 0 }) => {\n const tr = useTr();\n const { getPath, navigateTo } = useEcoImpactHub();\n const { pathname } = useLocation();\n const navRef = useRef<HTMLElement>(null);\n\n useEffect(() => {\n const nav = navRef.current;\n if (!nav) return undefined;\n const root = document.documentElement;\n const publish = () => root.style.setProperty(TABBAR_HEIGHT_VAR, `${nav.offsetHeight}px`);\n publish();\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(publish);\n observer?.observe(nav);\n return () => {\n observer?.disconnect();\n root.style.removeProperty(TABBAR_HEIGHT_VAR);\n };\n }, []);\n\n return (\n <div className=\"flex min-h-full flex-col bg-bg-canvas\">\n <div className=\"mx-auto w-full max-w-xl flex-1 px-4 pb-28 pt-4\">{children}</div>\n\n <nav\n ref={navRef}\n aria-label={tr('ecoField.tabs.label', 'Field app')}\n className=\"fixed inset-x-0 bottom-0 z-30 border-t border-border-seam bg-bg-surface/95 backdrop-blur\"\n style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }}\n >\n <ul className=\"mx-auto flex max-w-xl items-stretch justify-between px-2\">\n {TABS.map((tab) => {\n const href = getPath(tab.path);\n const active = pathname === href || pathname === `${href}/`;\n return (\n <li key={tab.id} className=\"flex-1\">\n <button\n type=\"button\"\n onClick={() => navigateTo(tab.path)}\n aria-current={active ? 'page' : undefined}\n data-testid={`field-tab-${tab.id}`}\n className={cn(\n 'relative flex w-full flex-col items-center gap-0.5 py-2 text-[11px] font-medium transition-colors',\n active ? 'text-action-primary-bg' : 'text-text-tertiary hover:text-text-primary'\n )}\n >\n {active && (\n <span\n aria-hidden=\"true\"\n className=\"absolute inset-x-4 top-0 h-0.5 rounded-full bg-action-primary-bg\"\n />\n )}\n <span\n className={cn(\n 'relative flex items-center justify-center',\n tab.primary &&\n 'h-9 w-9 rounded-full bg-action-primary-bg text-action-primary-text shadow'\n )}\n >\n {tab.icon}\n {tab.primary && pendingCount > 0 && (\n <span className=\"absolute -right-1.5 -top-1.5 min-w-[1.1rem] rounded-full bg-status-warning-text px-1 text-center text-[10px] font-semibold leading-[1.1rem] text-bg-surface\">\n {pendingCount}\n </span>\n )}\n </span>\n {tr(tab.labelKey, tab.label)}\n </button>\n </li>\n );\n })}\n </ul>\n </nav>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;AAiCA,IAAM,IAAoB,0BAEpB,IAAiB;CACrB;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAO,WAAU;GAAU,eAAY;EAAQ,CAAA;CACvD;CACA;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAS,WAAU;GAAU,eAAY;EAAQ,CAAA;CACzD;CACA;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAY,WAAU;GAAU,eAAY;EAAQ,CAAA;EAC1D,SAAS;CACX;CACA;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAU,WAAU;GAAU,eAAY;EAAQ,CAAA;CAC1D;CACA;EACE,IAAI;EACJ,MAAM;EACN,UAAU;EACV,OAAO;EACP,MAAM,kBAAC,GAAD;GAAS,WAAU;GAAU,eAAY;EAAQ,CAAA;CACzD;AACF,GAQa,KAAqC,EAAE,aAAU,kBAAe,QAAQ;CACnF,IAAM,IAAK,EAAM,GACX,EAAE,YAAS,kBAAe,EAAgB,GAC1C,EAAE,gBAAa,EAAY,GAC3B,IAAS,EAAoB,IAAI;CAgBvC,OAdA,QAAgB;EACd,IAAM,IAAM,EAAO;EACnB,IAAI,CAAC,GAAK;EACV,IAAM,IAAO,SAAS,iBAChB,UAAgB,EAAK,MAAM,YAAY,GAAmB,GAAG,EAAI,aAAa,GAAG;EACvF,EAAQ;EACR,IAAM,IAAW,OAAO,iBAAmB,MAAc,OAAO,IAAI,eAAe,CAAO;EAE1F,OADA,GAAU,QAAQ,CAAG,SACR;GAEX,AADA,GAAU,WAAW,GACrB,EAAK,MAAM,eAAe,CAAiB;EAC7C;CACF,GAAG,CAAC,CAAC,GAGH,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;GAAkD;EAAc,CAAA,GAE/E,kBAAC,OAAD;GACE,KAAK;GACL,cAAY,EAAG,uBAAuB,WAAW;GACjD,WAAU;GACV,OAAO,EAAE,eAAe,mCAAmC;aAE3D,kBAAC,MAAD;IAAI,WAAU;cACX,EAAK,KAAK,MAAQ;KACjB,IAAM,IAAO,EAAQ,EAAI,IAAI,GACvB,IAAS,MAAa,KAAQ,MAAa,GAAG,EAAK;KACzD,OACE,kBAAC,MAAD;MAAiB,WAAU;gBACzB,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,EAAI,IAAI;OAClC,gBAAc,IAAS,SAAS,KAAA;OAChC,eAAa,aAAa,EAAI;OAC9B,WAAW,EACT,qGACA,IAAS,2BAA2B,4CACtC;iBARF;QAUG,KACC,kBAAC,QAAD;SACE,eAAY;SACZ,WAAU;QACX,CAAA;QAEH,kBAAC,QAAD;SACE,WAAW,EACT,6CACA,EAAI,WACF,2EACJ;mBALF,CAOG,EAAI,MACJ,EAAI,WAAW,IAAe,KAC7B,kBAAC,QAAD;UAAM,WAAU;oBACb;SACG,CAAA,CAEJ;;QACL,EAAG,EAAI,UAAU,EAAI,KAAK;OACrB;;KACN,GAjCK,EAAI,EAiCT;IAER,CAAC;GACC,CAAA;EACD,CAAA,CACF;;AAET"}
@@ -5,7 +5,8 @@
5
5
  * `graphqlFetch` hands back the raw GraphQL `errors` array (or rejects with an
6
6
  * Error that carries it on `.errors` when the request was batched). Throwing a
7
7
  * plain `Error(message)` from there loses `extensions.code`, which is the only
8
- * reliable way to tell a quota wall (`QUOTA_EXHAUSTED`) or a lifecycle conflict
8
+ * reliable way to tell a quota wall (`QUOTA_EXHAUSTED` /
9
+ * `QUOTA_NOT_PROVISIONED`) or a lifecycle conflict
9
10
  * (`CONFLICT`) apart from any other failure. `EcoGraphQLError` keeps the
10
11
  * original errors on `graphQLErrors` — the same property name Apollo uses — so
11
12
  * the fe-libs quota helpers (`extractQuotaExhaustedExtensions`,
@@ -34,7 +35,21 @@ export declare class EcoGraphQLError extends Error {
34
35
  export declare function graphQLErrorsOf(err: unknown): EcoGraphQLErrorEntry[];
35
36
  /** `extensions.code` of the first coded error, or undefined. */
36
37
  export declare function graphQLErrorCode(err: unknown): string | undefined;
38
+ /**
39
+ * Why the gateway's `@usage` gate refused the call.
40
+ *
41
+ * - `exhausted` (`QUOTA_EXHAUSTED`): the plan covers the quota and it is used
42
+ * up for this period.
43
+ * - `not-provisioned` (`QUOTA_NOT_PROVISIONED`): the workspace has no plan
44
+ * that covers the quota at all. Nothing was used up; the user has to pick a
45
+ * plan that includes it.
46
+ *
47
+ * Both are upgrade walls, so both render the upgrade banner rather than a
48
+ * raw error line.
49
+ */
50
+ export type EcoQuotaWallKind = 'exhausted' | 'not-provisioned';
37
51
  export interface EcoQuotaWall {
52
+ kind: EcoQuotaWallKind;
38
53
  quotaName?: string;
39
54
  }
40
55
  /** The quota wall an error describes, or null when it is not a quota error. */
@@ -1 +1 @@
1
- {"version":3,"file":"graphqlErrors.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/utils/graphqlErrors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,mDAAmD;AACnD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D;AAED,qBAAa,eAAgB,SAAQ,KAAK;IACxC,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,mDAAmD;IACnD,QAAQ,CAAC,aAAa,EAAE,oBAAoB,EAAE,CAAC;gBAEnC,MAAM,EAAE,oBAAoB,EAAE;CAY3C;AAQD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,EAAE,CASpE;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CAS7D;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIrE"}
1
+ {"version":3,"file":"graphqlErrors.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/utils/graphqlErrors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,mDAAmD;AACnD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D;AAED,qBAAa,eAAgB,SAAQ,KAAK;IACxC,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,mDAAmD;IACnD,QAAQ,CAAC,aAAa,EAAE,oBAAoB,EAAE,CAAC;gBAEnC,MAAM,EAAE,oBAAoB,EAAE;CAY3C;AAQD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,EAAE,CASpE;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,iBAAiB,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAOD,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CAgB7D;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIrE"}
@@ -18,19 +18,33 @@ function r(e) {
18
18
  if (typeof e == "string" && e) return e;
19
19
  }
20
20
  }
21
- function i(e) {
22
- for (let t of n(e)) if (t.extensions?.code === "QUOTA_EXHAUSTED") {
23
- let e = t.extensions.quotaName;
24
- return typeof e == "string" && e ? { quotaName: e } : {};
21
+ var i = {
22
+ QUOTA_EXHAUSTED: "exhausted",
23
+ QUOTA_NOT_PROVISIONED: "not-provisioned"
24
+ };
25
+ function a(e) {
26
+ for (let t of n(e)) {
27
+ let e = t.extensions?.code, n = typeof e == "string" ? i[e] : void 0;
28
+ if (n) {
29
+ let e = t.extensions?.quotaName;
30
+ return typeof e == "string" && e ? {
31
+ kind: n,
32
+ quotaName: e
33
+ } : { kind: n };
34
+ }
35
+ }
36
+ if (e instanceof Error) {
37
+ if (e.message.includes("QUOTA_NOT_PROVISIONED")) return { kind: "not-provisioned" };
38
+ if (e.message.includes("QUOTA_EXHAUSTED")) return { kind: "exhausted" };
25
39
  }
26
- return e instanceof Error && e.message.includes("QUOTA_EXHAUSTED") ? {} : null;
40
+ return null;
27
41
  }
28
- function a(e, t) {
42
+ function o(e, t) {
29
43
  if (e instanceof Error && e.message.trim()) return e.message;
30
44
  let r = n(e)[0];
31
45
  return r?.message?.trim() ? r.message : t;
32
46
  }
33
47
  //#endregion
34
- export { e as EcoGraphQLError, a as errorMessageOf, r as graphQLErrorCode, n as graphQLErrorsOf, i as quotaWallOf };
48
+ export { e as EcoGraphQLError, o as errorMessageOf, r as graphQLErrorCode, n as graphQLErrorsOf, a as quotaWallOf };
35
49
 
36
50
  //# sourceMappingURL=graphqlErrors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"graphqlErrors.js","names":[],"sources":["../../../src/ecoimpacthub/utils/graphqlErrors.ts"],"sourcesContent":["/**\n * Structured GraphQL errors for the EcoImpactHub transport.\n * @module ecoimpacthub/utils/graphqlErrors\n *\n * `graphqlFetch` hands back the raw GraphQL `errors` array (or rejects with an\n * Error that carries it on `.errors` when the request was batched). Throwing a\n * plain `Error(message)` from there loses `extensions.code`, which is the only\n * reliable way to tell a quota wall (`QUOTA_EXHAUSTED`) or a lifecycle conflict\n * (`CONFLICT`) apart from any other failure. `EcoGraphQLError` keeps the\n * original errors on `graphQLErrors` — the same property name Apollo uses — so\n * the fe-libs quota helpers (`extractQuotaExhaustedExtensions`,\n * `isUpgradeError`) recognise it without any adapter.\n */\n\n/** The subset of a GraphQL error the MFE reads. */\nexport interface EcoGraphQLErrorEntry {\n message: string;\n path?: ReadonlyArray<string | number>;\n extensions?: Record<string, unknown> & { code?: string };\n}\n\nexport class EcoGraphQLError extends Error {\n /** `extensions.code` of the first error, when the server set one. */\n readonly code: string | undefined;\n /** Every error the server returned, unmodified. */\n readonly graphQLErrors: EcoGraphQLErrorEntry[];\n\n constructor(errors: EcoGraphQLErrorEntry[]) {\n const first = errors[0];\n const userMessage = first?.extensions?.userMessage;\n super(\n typeof userMessage === 'string' && userMessage.trim()\n ? userMessage\n : (first?.message ?? 'GraphQL request failed')\n );\n this.name = 'EcoGraphQLError';\n this.code = typeof first?.extensions?.code === 'string' ? first.extensions.code : undefined;\n this.graphQLErrors = errors;\n }\n}\n\nconst isErrorEntry = (value: unknown): value is EcoGraphQLErrorEntry =>\n typeof value === 'object' &&\n value !== null &&\n 'message' in value &&\n typeof (value as { message: unknown }).message === 'string';\n\n/**\n * Read GraphQL errors off anything a transport may reject with: an\n * `EcoGraphQLError`, a batched-fetch Error carrying `.errors`, or an\n * Apollo-shaped error carrying `.graphQLErrors`. Returns [] for anything else.\n */\nexport function graphQLErrorsOf(err: unknown): EcoGraphQLErrorEntry[] {\n if (typeof err !== 'object' || err === null) return [];\n const candidate = err as { graphQLErrors?: unknown; errors?: unknown };\n const list = Array.isArray(candidate.graphQLErrors)\n ? candidate.graphQLErrors\n : Array.isArray(candidate.errors)\n ? candidate.errors\n : [];\n return list.filter(isErrorEntry);\n}\n\n/** `extensions.code` of the first coded error, or undefined. */\nexport function graphQLErrorCode(err: unknown): string | undefined {\n for (const entry of graphQLErrorsOf(err)) {\n const code = entry.extensions?.code;\n if (typeof code === 'string' && code) return code;\n }\n return undefined;\n}\n\nexport interface EcoQuotaWall {\n quotaName?: string;\n}\n\n/** The quota wall an error describes, or null when it is not a quota error. */\nexport function quotaWallOf(err: unknown): EcoQuotaWall | null {\n for (const entry of graphQLErrorsOf(err)) {\n if (entry.extensions?.code === 'QUOTA_EXHAUSTED') {\n const quotaName = entry.extensions.quotaName;\n return typeof quotaName === 'string' && quotaName ? { quotaName } : {};\n }\n }\n if (err instanceof Error && err.message.includes('QUOTA_EXHAUSTED')) return {};\n return null;\n}\n\n/** Human-readable message for any thrown value, with a caller fallback. */\nexport function errorMessageOf(err: unknown, fallback: string): string {\n if (err instanceof Error && err.message.trim()) return err.message;\n const first = graphQLErrorsOf(err)[0];\n return first?.message?.trim() ? first.message : fallback;\n}\n"],"mappings":";AAqBA,IAAa,IAAb,cAAqC,MAAM;CAEzC;CAEA;CAEA,YAAY,GAAgC;EAC1C,IAAM,IAAQ,EAAO,IACf,IAAc,GAAO,YAAY;EAQvC,AAPA,MACE,OAAO,KAAgB,YAAY,EAAY,KAAK,IAChD,IACC,GAAO,WAAW,wBACzB,GACA,KAAK,OAAO,mBACZ,KAAK,OAAO,OAAO,GAAO,YAAY,QAAS,WAAW,EAAM,WAAW,OAAO,KAAA,GAClF,KAAK,gBAAgB;CACvB;AACF,GAEM,KAAgB,MACpB,OAAO,KAAU,cACjB,KACA,aAAa,KACb,OAAQ,EAA+B,WAAY;AAOrD,SAAgB,EAAgB,GAAsC;CACpE,IAAI,OAAO,KAAQ,aAAY,GAAc,OAAO,CAAC;CACrD,IAAM,IAAY;CAMlB,QALa,MAAM,QAAQ,EAAU,aAAa,IAC9C,EAAU,gBACV,MAAM,QAAQ,EAAU,MAAM,IAC5B,EAAU,SACV,CAAC,GACK,OAAO,CAAY;AACjC;AAGA,SAAgB,EAAiB,GAAkC;CACjE,KAAK,IAAM,KAAS,EAAgB,CAAG,GAAG;EACxC,IAAM,IAAO,EAAM,YAAY;EAC/B,IAAI,OAAO,KAAS,YAAY,GAAM,OAAO;CAC/C;AAEF;AAOA,SAAgB,EAAY,GAAmC;CAC7D,KAAK,IAAM,KAAS,EAAgB,CAAG,GACrC,IAAI,EAAM,YAAY,SAAS,mBAAmB;EAChD,IAAM,IAAY,EAAM,WAAW;EACnC,OAAO,OAAO,KAAc,YAAY,IAAY,EAAE,aAAU,IAAI,CAAC;CACvE;CAGF,OADI,aAAe,SAAS,EAAI,QAAQ,SAAS,iBAAiB,IAAU,CAAC,IACtE;AACT;AAGA,SAAgB,EAAe,GAAc,GAA0B;CACrE,IAAI,aAAe,SAAS,EAAI,QAAQ,KAAK,GAAG,OAAO,EAAI;CAC3D,IAAM,IAAQ,EAAgB,CAAG,EAAE;CACnC,OAAO,GAAO,SAAS,KAAK,IAAI,EAAM,UAAU;AAClD"}
1
+ {"version":3,"file":"graphqlErrors.js","names":[],"sources":["../../../src/ecoimpacthub/utils/graphqlErrors.ts"],"sourcesContent":["/**\n * Structured GraphQL errors for the EcoImpactHub transport.\n * @module ecoimpacthub/utils/graphqlErrors\n *\n * `graphqlFetch` hands back the raw GraphQL `errors` array (or rejects with an\n * Error that carries it on `.errors` when the request was batched). Throwing a\n * plain `Error(message)` from there loses `extensions.code`, which is the only\n * reliable way to tell a quota wall (`QUOTA_EXHAUSTED` /\n * `QUOTA_NOT_PROVISIONED`) or a lifecycle conflict\n * (`CONFLICT`) apart from any other failure. `EcoGraphQLError` keeps the\n * original errors on `graphQLErrors` — the same property name Apollo uses — so\n * the fe-libs quota helpers (`extractQuotaExhaustedExtensions`,\n * `isUpgradeError`) recognise it without any adapter.\n */\n\n/** The subset of a GraphQL error the MFE reads. */\nexport interface EcoGraphQLErrorEntry {\n message: string;\n path?: ReadonlyArray<string | number>;\n extensions?: Record<string, unknown> & { code?: string };\n}\n\nexport class EcoGraphQLError extends Error {\n /** `extensions.code` of the first error, when the server set one. */\n readonly code: string | undefined;\n /** Every error the server returned, unmodified. */\n readonly graphQLErrors: EcoGraphQLErrorEntry[];\n\n constructor(errors: EcoGraphQLErrorEntry[]) {\n const first = errors[0];\n const userMessage = first?.extensions?.userMessage;\n super(\n typeof userMessage === 'string' && userMessage.trim()\n ? userMessage\n : (first?.message ?? 'GraphQL request failed')\n );\n this.name = 'EcoGraphQLError';\n this.code = typeof first?.extensions?.code === 'string' ? first.extensions.code : undefined;\n this.graphQLErrors = errors;\n }\n}\n\nconst isErrorEntry = (value: unknown): value is EcoGraphQLErrorEntry =>\n typeof value === 'object' &&\n value !== null &&\n 'message' in value &&\n typeof (value as { message: unknown }).message === 'string';\n\n/**\n * Read GraphQL errors off anything a transport may reject with: an\n * `EcoGraphQLError`, a batched-fetch Error carrying `.errors`, or an\n * Apollo-shaped error carrying `.graphQLErrors`. Returns [] for anything else.\n */\nexport function graphQLErrorsOf(err: unknown): EcoGraphQLErrorEntry[] {\n if (typeof err !== 'object' || err === null) return [];\n const candidate = err as { graphQLErrors?: unknown; errors?: unknown };\n const list = Array.isArray(candidate.graphQLErrors)\n ? candidate.graphQLErrors\n : Array.isArray(candidate.errors)\n ? candidate.errors\n : [];\n return list.filter(isErrorEntry);\n}\n\n/** `extensions.code` of the first coded error, or undefined. */\nexport function graphQLErrorCode(err: unknown): string | undefined {\n for (const entry of graphQLErrorsOf(err)) {\n const code = entry.extensions?.code;\n if (typeof code === 'string' && code) return code;\n }\n return undefined;\n}\n\n/**\n * Why the gateway's `@usage` gate refused the call.\n *\n * - `exhausted` (`QUOTA_EXHAUSTED`): the plan covers the quota and it is used\n * up for this period.\n * - `not-provisioned` (`QUOTA_NOT_PROVISIONED`): the workspace has no plan\n * that covers the quota at all. Nothing was used up; the user has to pick a\n * plan that includes it.\n *\n * Both are upgrade walls, so both render the upgrade banner rather than a\n * raw error line.\n */\nexport type EcoQuotaWallKind = 'exhausted' | 'not-provisioned';\n\nexport interface EcoQuotaWall {\n kind: EcoQuotaWallKind;\n quotaName?: string;\n}\n\nconst QUOTA_WALL_CODES: Readonly<Record<string, EcoQuotaWallKind>> = {\n QUOTA_EXHAUSTED: 'exhausted',\n QUOTA_NOT_PROVISIONED: 'not-provisioned',\n};\n\n/** The quota wall an error describes, or null when it is not a quota error. */\nexport function quotaWallOf(err: unknown): EcoQuotaWall | null {\n for (const entry of graphQLErrorsOf(err)) {\n const code = entry.extensions?.code;\n const kind = typeof code === 'string' ? QUOTA_WALL_CODES[code] : undefined;\n if (kind) {\n const quotaName = entry.extensions?.quotaName;\n return typeof quotaName === 'string' && quotaName ? { kind, quotaName } : { kind };\n }\n }\n if (err instanceof Error) {\n // Check the more specific code first: the not-provisioned message never\n // contains QUOTA_EXHAUSTED, but keep the order explicit anyway.\n if (err.message.includes('QUOTA_NOT_PROVISIONED')) return { kind: 'not-provisioned' };\n if (err.message.includes('QUOTA_EXHAUSTED')) return { kind: 'exhausted' };\n }\n return null;\n}\n\n/** Human-readable message for any thrown value, with a caller fallback. */\nexport function errorMessageOf(err: unknown, fallback: string): string {\n if (err instanceof Error && err.message.trim()) return err.message;\n const first = graphQLErrorsOf(err)[0];\n return first?.message?.trim() ? first.message : fallback;\n}\n"],"mappings":";AAsBA,IAAa,IAAb,cAAqC,MAAM;CAEzC;CAEA;CAEA,YAAY,GAAgC;EAC1C,IAAM,IAAQ,EAAO,IACf,IAAc,GAAO,YAAY;EAQvC,AAPA,MACE,OAAO,KAAgB,YAAY,EAAY,KAAK,IAChD,IACC,GAAO,WAAW,wBACzB,GACA,KAAK,OAAO,mBACZ,KAAK,OAAO,OAAO,GAAO,YAAY,QAAS,WAAW,EAAM,WAAW,OAAO,KAAA,GAClF,KAAK,gBAAgB;CACvB;AACF,GAEM,KAAgB,MACpB,OAAO,KAAU,cACjB,KACA,aAAa,KACb,OAAQ,EAA+B,WAAY;AAOrD,SAAgB,EAAgB,GAAsC;CACpE,IAAI,OAAO,KAAQ,aAAY,GAAc,OAAO,CAAC;CACrD,IAAM,IAAY;CAMlB,QALa,MAAM,QAAQ,EAAU,aAAa,IAC9C,EAAU,gBACV,MAAM,QAAQ,EAAU,MAAM,IAC5B,EAAU,SACV,CAAC,GACK,OAAO,CAAY;AACjC;AAGA,SAAgB,EAAiB,GAAkC;CACjE,KAAK,IAAM,KAAS,EAAgB,CAAG,GAAG;EACxC,IAAM,IAAO,EAAM,YAAY;EAC/B,IAAI,OAAO,KAAS,YAAY,GAAM,OAAO;CAC/C;AAEF;AAqBA,IAAM,IAA+D;CACnE,iBAAiB;CACjB,uBAAuB;AACzB;AAGA,SAAgB,EAAY,GAAmC;CAC7D,KAAK,IAAM,KAAS,EAAgB,CAAG,GAAG;EACxC,IAAM,IAAO,EAAM,YAAY,MACzB,IAAO,OAAO,KAAS,WAAW,EAAiB,KAAQ,KAAA;EACjE,IAAI,GAAM;GACR,IAAM,IAAY,EAAM,YAAY;GACpC,OAAO,OAAO,KAAc,YAAY,IAAY;IAAE;IAAM;GAAU,IAAI,EAAE,QAAK;EACnF;CACF;CACA,IAAI,aAAe,OAAO;EAGxB,IAAI,EAAI,QAAQ,SAAS,uBAAuB,GAAG,OAAO,EAAE,MAAM,kBAAkB;EACpF,IAAI,EAAI,QAAQ,SAAS,iBAAiB,GAAG,OAAO,EAAE,MAAM,YAAY;CAC1E;CACA,OAAO;AACT;AAGA,SAAgB,EAAe,GAAc,GAA0B;CACrE,IAAI,aAAe,SAAS,EAAI,QAAQ,KAAK,GAAG,OAAO,EAAI;CAC3D,IAAM,IAAQ,EAAgB,CAAG,EAAE;CACnC,OAAO,GAAO,SAAS,KAAK,IAAI,EAAM,UAAU;AAClD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-ecoimpacthub",
3
- "version": "2026.1030.3",
3
+ "version": "2026.1031.1",
4
4
  "description": "Burdenoff EcoImpactHub Microfrontend",
5
5
  "type": "module",
6
6
  "files": [