@burdenoff/microfe-ecoimpacthub 2026.704.4 → 2026.704.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/ecoimpacthub/pages/DashboardPage.js +345 -189
  2. package/dist/ecoimpacthub/pages/DashboardPage.js.map +1 -1
  3. package/dist/ecoimpacthub/pages/_shared/ConservationBadge.js +58 -0
  4. package/dist/ecoimpacthub/pages/_shared/ConservationBadge.js.map +1 -0
  5. package/dist/ecoimpacthub/pages/_shared/EvidenceThumb.js +70 -0
  6. package/dist/ecoimpacthub/pages/_shared/EvidenceThumb.js.map +1 -0
  7. package/dist/ecoimpacthub/pages/_shared/MethodBadge.js +51 -0
  8. package/dist/ecoimpacthub/pages/_shared/MethodBadge.js.map +1 -0
  9. package/dist/ecoimpacthub/pages/_shared/TaxonChip.js +29 -0
  10. package/dist/ecoimpacthub/pages/_shared/TaxonChip.js.map +1 -0
  11. package/dist/ecoimpacthub/pages/badges.js +71 -0
  12. package/dist/ecoimpacthub/pages/badges.js.map +1 -0
  13. package/dist/ecoimpacthub/pages/carbon/CarbonPage.js +337 -300
  14. package/dist/ecoimpacthub/pages/carbon/CarbonPage.js.map +1 -1
  15. package/dist/ecoimpacthub/pages/carbonVisuals.js +89 -0
  16. package/dist/ecoimpacthub/pages/carbonVisuals.js.map +1 -0
  17. package/dist/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.js +252 -218
  18. package/dist/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.js.map +1 -1
  19. package/dist/ecoimpacthub/pages/habitat/HabitatProjectsPage.js +15 -12
  20. package/dist/ecoimpacthub/pages/habitat/HabitatProjectsPage.js.map +1 -1
  21. package/dist/ecoimpacthub/pages/habitat/LifecycleProgress.js +168 -0
  22. package/dist/ecoimpacthub/pages/habitat/LifecycleProgress.js.map +1 -0
  23. package/dist/ecoimpacthub/pages/monitoring/MonitoringPage.js +30 -17
  24. package/dist/ecoimpacthub/pages/monitoring/MonitoringPage.js.map +1 -1
  25. package/dist/ecoimpacthub/pages/observations/ObservationsPage.js +236 -219
  26. package/dist/ecoimpacthub/pages/observations/ObservationsPage.js.map +1 -1
  27. package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js +248 -212
  28. package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js.map +1 -1
  29. package/dist/ecoimpacthub/pages/reports/ReportsPage.js +119 -118
  30. package/dist/ecoimpacthub/pages/reports/ReportsPage.js.map +1 -1
  31. package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js +498 -370
  32. package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js.map +1 -1
  33. package/dist/ecoimpacthub/pages/sites/SitesPage.js +109 -99
  34. package/dist/ecoimpacthub/pages/sites/SitesPage.js.map +1 -1
  35. package/dist/ecoimpacthub/pages/sites/siteTypeVisuals.js +64 -0
  36. package/dist/ecoimpacthub/pages/sites/siteTypeVisuals.js.map +1 -0
  37. package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js +275 -259
  38. package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js.map +1 -1
  39. package/dist/ecoimpacthub/pages/species/SpeciesPage.js +235 -177
  40. package/dist/ecoimpacthub/pages/species/SpeciesPage.js.map +1 -1
  41. package/dist/node_modules/recharts/es6/cartesian/CartesianGrid.js +305 -0
  42. package/dist/node_modules/recharts/es6/cartesian/CartesianGrid.js.map +1 -0
  43. package/dist/node_modules/recharts/es6/context/chartLayoutContext.js +44 -33
  44. package/dist/node_modules/recharts/es6/context/chartLayoutContext.js.map +1 -1
  45. package/dist/node_modules/recharts/es6/util/ChartUtils.js +31 -23
  46. package/dist/node_modules/recharts/es6/util/ChartUtils.js.map +1 -1
  47. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/ecoimpacthub/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard — EcoImpactHub environment, conservation & sustainability overview.\n * @module ecoimpacthub/pages\n *\n * KPIs are aggregated client-side from the real `eco*` list endpoints plus the\n * `ecoCarbonTotalKg` aggregate (by scope). Widgets show recent field\n * observations and open threshold breaches. No mock data.\n */\n\nimport { useMemo } from 'react';\nimport {\n MapPin,\n PawPrint,\n Trees,\n Factory,\n TriangleAlert,\n Binoculars,\n ShieldCheck,\n ArrowRight,\n Radio,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n BarChart,\n Bar,\n XAxis,\n YAxis,\n ResponsiveContainer,\n Tooltip,\n Cell,\n LabelList,\n} from 'recharts';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { MiniBar } from '../components/MiniBar';\nimport { LoadingView, Section } from '../components/StateViews';\nimport { EmptyState } from '../components/EmptyState';\nimport { useEcoImpactHub } from '../context';\nimport {\n useEcoSites,\n useEcoSpecies,\n useEcoHabitatProjects,\n useEcoCarbonTotals,\n useEcoMeasurements,\n useEcoObservations,\n useEcoSensors,\n} from '../hooks';\nimport { formatNumber, formatRelativeTime, formatCo2e, formatEnumLabel } from '../utils/formatters';\n\nconst ACTIVE_PROJECT_STATUSES = new Set(['PLANNED', 'IN_PROGRESS', 'MONITORING']);\n\nconst SCOPE_FILL: Record<string, string> = {\n SCOPE_1: 'var(--color-status-error-text)',\n SCOPE_2: 'var(--color-status-warning-text)',\n SCOPE_3: 'var(--color-accent)',\n};\n\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useEcoImpactHub();\n\n const sites = useEcoSites({ pagination: { limit: 100, offset: 0 } });\n const species = useEcoSpecies({ pagination: { limit: 100, offset: 0 } });\n const projects = useEcoHabitatProjects();\n const carbon = useEcoCarbonTotals();\n const breaches = useEcoMeasurements({ breachesOnly: true, pagination: { limit: 6, offset: 0 } });\n const observations = useEcoObservations({ pagination: { limit: 6, offset: 0 } });\n const sensors = useEcoSensors();\n\n const siteRows = sites.data?.items ?? [];\n const speciesRows = species.data?.items ?? [];\n const projectRows = projects.data ?? [];\n const sensorRows = sensors.data ?? [];\n\n const activeSites = siteRows.filter((s) => s.status === 'ACTIVE').length;\n const priorityCount = speciesRows.filter((s) => s.isPriority).length;\n const threatened = speciesRows.filter((s) =>\n ['VULNERABLE', 'ENDANGERED', 'CRITICALLY_ENDANGERED'].includes(s.conservationStatus)\n ).length;\n const activeProjects = projectRows.filter((p) => ACTIVE_PROJECT_STATUSES.has(p.status)).length;\n const onlineSensors = sensorRows.filter((s) => s.status === 'ONLINE').length;\n const offlineSensors = sensorRows.filter((s) =>\n ['OFFLINE', 'DEGRADED', 'MAINTENANCE'].includes(s.status)\n ).length;\n\n const carbonData = useMemo(() => {\n const c = carbon.data;\n if (!c) return [];\n return [\n { scope: 'SCOPE_1', label: 'Scope 1', kg: c.scope1 },\n { scope: 'SCOPE_2', label: 'Scope 2', kg: c.scope2 },\n { scope: 'SCOPE_3', label: 'Scope 3', kg: c.scope3 },\n ];\n }, [carbon.data]);\n\n const isInitialLoading =\n sites.isLoading && species.isLoading && projects.isLoading && carbon.isLoading;\n\n if (isInitialLoading) {\n return (\n <PageLayout title=\"EcoImpactHub\" titleGradient aurora>\n <LoadingView label=\"Loading your impact overview…\" />\n </PageLayout>\n );\n }\n\n const greetingName = currentUser?.firstName || currentUser?.name || 'there';\n const breachRows = breaches.data?.items ?? [];\n const observationRows = observations.data?.items ?? [];\n\n return (\n <PageLayout\n title=\"EcoImpactHub\"\n titleGradient\n aurora\n description={`Welcome back, ${greetingName} — your environment, conservation & sustainability control room.`}\n >\n {/* KPI grid */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6\">\n <MetricsCard\n icon={<MapPin className=\"h-5 w-5\" />}\n value={\n sites.data?.pageInfo?.hasMore ? `${siteRows.length}+` : formatNumber(siteRows.length)\n }\n label=\"Monitored sites\"\n footer={<p className=\"text-xs text-text-tertiary\">{activeSites} active</p>}\n />\n <MetricsCard\n icon={<PawPrint className=\"h-5 w-5\" />}\n value={\n species.data?.pageInfo?.hasMore\n ? `${speciesRows.length}+`\n : formatNumber(speciesRows.length)\n }\n label=\"Species tracked\"\n accent={threatened > 0 ? 'warning' : undefined}\n footer={\n <p className=\"text-xs text-text-tertiary\">\n {priorityCount} priority · {threatened} threatened\n </p>\n }\n />\n <MetricsCard\n icon={<Trees className=\"h-5 w-5\" />}\n value={formatNumber(activeProjects)}\n label=\"Active habitat projects\"\n footer={\n <p className=\"text-xs text-text-tertiary\">{formatNumber(projectRows.length)} total</p>\n }\n />\n <MetricsCard\n icon={<Factory className=\"h-5 w-5\" />}\n value={carbon.data ? formatCo2e(carbon.data.total) : '—'}\n label=\"Total footprint\"\n featured\n />\n <MetricsCard\n icon={<Radio className=\"h-5 w-5\" />}\n value={formatNumber(onlineSensors)}\n label=\"Sensors online\"\n accent={offlineSensors > 0 ? 'warning' : 'success'}\n footer={<p className=\"text-xs text-text-tertiary\">{offlineSensors} need attention</p>}\n />\n <MetricsCard\n icon={<TriangleAlert className=\"h-5 w-5\" />}\n value={\n breaches.data?.pageInfo?.hasMore\n ? `${breachRows.length}+`\n : formatNumber(breachRows.length)\n }\n label=\"Open breaches\"\n accent={breachRows.length > 0 ? 'error' : 'success'}\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Carbon by scope */}\n <GlassCard treatment=\"glass\" className=\"p-6 lg:col-span-2\">\n <div className=\"mb-4 flex items-center justify-between\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">\n Carbon footprint by scope\n </h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n GHG Protocol Scope 1 / 2 / 3 emissions (CO₂e).\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('carbon')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Ledger <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n {carbonData.every((d) => d.kg === 0) ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Factory className=\"h-10 w-10\" />}\n title=\"No carbon entries yet\"\n description=\"Record activity in the carbon ledger to see your Scope 1–3 footprint here.\"\n />\n ) : (\n <div className=\"h-56\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <BarChart data={carbonData} margin={{ top: 16, right: 8, left: 8, bottom: 0 }}>\n <XAxis\n dataKey=\"label\"\n tick={{ fill: 'var(--color-text-secondary)', fontSize: 12 }}\n axisLine={{ stroke: 'var(--color-border-seam)' }}\n tickLine={false}\n />\n <YAxis\n tick={{ fill: 'var(--color-text-tertiary)', fontSize: 11 }}\n axisLine={false}\n tickLine={false}\n width={48}\n tickFormatter={(v: number) => (v >= 1000 ? `${Math.round(v / 1000)}t` : `${v}`)}\n />\n <Tooltip\n cursor={{ fill: 'var(--color-bg-sunken)', opacity: 0.4 }}\n contentStyle={{\n background: 'var(--color-bg-surface)',\n border: '1px solid var(--color-border-seam)',\n borderRadius: 12,\n color: 'var(--color-text-primary)',\n }}\n formatter={(value: number) => [formatCo2e(value), 'Emissions']}\n />\n <Bar dataKey=\"kg\" radius={[6, 6, 0, 0]} maxBarSize={80}>\n {carbonData.map((d) => (\n <Cell key={d.scope} fill={SCOPE_FILL[d.scope]} />\n ))}\n <LabelList\n dataKey=\"kg\"\n position=\"top\"\n formatter={(v: number) => (v > 0 ? formatCo2e(v) : '')}\n style={{ fill: 'var(--color-text-secondary)', fontSize: 11 }}\n />\n </Bar>\n </BarChart>\n </ResponsiveContainer>\n </div>\n )}\n </GlassCard>\n\n {/* Recent breaches */}\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-base font-semibold text-text-primary\">Recent breaches</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('monitoring')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Monitoring <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n {breachRows.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <ShieldCheck className=\"h-10 w-10 text-status-success-text opacity-70\" />\n <p className=\"mt-3 text-sm text-text-secondary\">\n No threshold breaches — all parameters within range.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-3\">\n {breachRows.map((m) => (\n <li key={m.id} className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{m.parameter}</p>\n <p className=\"text-xs text-text-tertiary\">\n {formatNumber(m.value)} {m.unit} · {formatRelativeTime(m.recordedAt)}\n </p>\n </div>\n <StatusBadge status=\"BREACH\" />\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </div>\n\n {/* Recent observations */}\n <Section\n title=\"Recent field observations\"\n description=\"Latest wildlife & biodiversity sightings recorded across your sites.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('observations')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n All observations <ArrowRight className=\"h-4 w-4\" />\n </button>\n }\n >\n {observationRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Binoculars className=\"h-10 w-10\" />}\n title=\"No observations yet\"\n description=\"Record your first field observation to build your biodiversity dataset.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {observationRows.map((o) => (\n <GlassCard key={o.id} treatment=\"plain\" className=\"p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {o.species?.commonName ?? 'Unknown species'}\n </p>\n {o.species?.scientificName && (\n <p className=\"truncate text-xs italic text-text-tertiary\">\n {o.species.scientificName}\n </p>\n )}\n </div>\n <span className=\"shrink-0 rounded-full bg-bg-sunken px-2 py-0.5 text-xs tabular-nums text-text-secondary\">\n ×{formatNumber(o.count)}\n </span>\n </div>\n <div className=\"mt-3 flex items-center justify-between\">\n <span className=\"text-xs text-text-tertiary\">{formatEnumLabel(o.method)}</span>\n <span className=\"text-xs text-text-tertiary\">\n {formatRelativeTime(o.observedAt)}\n </span>\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </Section>\n\n {/* Conservation status snapshot */}\n {speciesRows.length > 0 && (\n <Section\n title=\"Conservation status\"\n description=\"Distribution of tracked species by IUCN-style status.\"\n >\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"space-y-3\">\n {(() => {\n const counts = new Map<string, number>();\n for (const s of speciesRows) {\n counts.set(s.conservationStatus, (counts.get(s.conservationStatus) ?? 0) + 1);\n }\n const total = speciesRows.length;\n return [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([status, count]) => (\n <div key={status} className=\"flex items-center gap-4\">\n <div className=\"w-48 shrink-0\">\n <StatusBadge status={status} />\n </div>\n <MiniBar value={(count / total) * 100} className=\"flex-1\" />\n <span className=\"w-10 shrink-0 text-right text-sm tabular-nums text-text-secondary\">\n {count}\n </span>\n </div>\n ));\n })()}\n </div>\n </GlassCard>\n </Section>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAM,IAA0B,IAAI,IAAI;CAAC;CAAW;CAAe;CAAa,CAAC,EAE3E,IAAqC;CACzC,SAAS;CACT,SAAS;CACT,SAAS;CACV;AAED,SAAgB,IAAgB;CAC9B,IAAM,EAAE,eAAY,mBAAgB,GAAiB,EAE/C,IAAQ,EAAY,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;EAAG,EAAE,CAAC,EAC9D,IAAU,EAAc,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;EAAG,EAAE,CAAC,EAClE,IAAW,GAAuB,EAClC,IAAS,GAAoB,EAC7B,IAAW,EAAmB;EAAE,cAAc;EAAM,YAAY;GAAE,OAAO;GAAG,QAAQ;GAAG;EAAE,CAAC,EAC1F,IAAe,EAAmB,EAAE,YAAY;EAAE,OAAO;EAAG,QAAQ;EAAG,EAAE,CAAC,EAC1E,IAAU,GAAe,EAEzB,IAAW,EAAM,MAAM,SAAS,EAAE,EAClC,IAAc,EAAQ,MAAM,SAAS,EAAE,EACvC,IAAc,EAAS,QAAQ,EAAE,EACjC,IAAa,EAAQ,QAAQ,EAAE,EAE/B,KAAc,EAAS,QAAQ,MAAM,EAAE,WAAW,SAAS,CAAC,QAC5D,KAAgB,EAAY,QAAQ,MAAM,EAAE,WAAW,CAAC,QACxD,IAAa,EAAY,QAAQ,MACrC;EAAC;EAAc;EAAc;EAAwB,CAAC,SAAS,EAAE,mBAAmB,CACrF,CAAC,QACI,KAAiB,EAAY,QAAQ,MAAM,EAAwB,IAAI,EAAE,OAAO,CAAC,CAAC,QAClF,KAAgB,EAAW,QAAQ,MAAM,EAAE,WAAW,SAAS,CAAC,QAChE,IAAiB,EAAW,QAAQ,MACxC;EAAC;EAAW;EAAY;EAAc,CAAC,SAAS,EAAE,OAAO,CAC1D,CAAC,QAEI,IAAa,SAAc;EAC/B,IAAM,IAAI,EAAO;AAEjB,SADK,IACE;GACL;IAAE,OAAO;IAAW,OAAO;IAAW,IAAI,EAAE;IAAQ;GACpD;IAAE,OAAO;IAAW,OAAO;IAAW,IAAI,EAAE;IAAQ;GACpD;IAAE,OAAO;IAAW,OAAO;IAAW,IAAI,EAAE;IAAQ;GACrD,GALc,EAAE;IAMhB,CAAC,EAAO,KAAK,CAAC;AAKjB,KAFE,EAAM,aAAa,EAAQ,aAAa,EAAS,aAAa,EAAO,UAGrE,QACE,kBAAC,GAAD;EAAY,OAAM;EAAe,eAAA;EAAc,QAAA;YAC7C,kBAAC,GAAD,EAAa,OAAM,iCAAkC,CAAA;EAC1C,CAAA;CAIjB,IAAM,KAAe,GAAa,aAAa,GAAa,QAAQ,SAC9D,IAAa,EAAS,MAAM,SAAS,EAAE,EACvC,IAAkB,EAAa,MAAM,SAAS,EAAE;AAEtD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,eAAA;EACA,QAAA;EACA,aAAa,iBAAiB,GAAa;YAJ7C;GAOE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,MAAM,kBAAC,IAAD,EAAQ,WAAU,WAAY,CAAA;MACpC,OACE,EAAM,MAAM,UAAU,UAAU,GAAG,EAAS,OAAO,KAAK,EAAa,EAAS,OAAO;MAEvF,OAAM;MACN,QAAQ,kBAAC,KAAD;OAAG,WAAU;iBAAb,CAA2C,IAAY,UAAW;;MAC1E,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,IAAD,EAAU,WAAU,WAAY,CAAA;MACtC,OACE,EAAQ,MAAM,UAAU,UACpB,GAAG,EAAY,OAAO,KACtB,EAAa,EAAY,OAAO;MAEtC,OAAM;MACN,QAAQ,IAAa,IAAI,YAAY,KAAA;MACrC,QACE,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG;QAAc;QAAa;QAAW;QACrC;;MAEN,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;MACnC,OAAO,EAAa,GAAe;MACnC,OAAM;MACN,QACE,kBAAC,KAAD;OAAG,WAAU;iBAAb,CAA2C,EAAa,EAAY,OAAO,EAAC,SAAU;;MAExF,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;MACrC,OAAO,EAAO,OAAO,EAAW,EAAO,KAAK,MAAM,GAAG;MACrD,OAAM;MACN,UAAA;MACA,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;MACnC,OAAO,EAAa,GAAc;MAClC,OAAM;MACN,QAAQ,IAAiB,IAAI,YAAY;MACzC,QAAQ,kBAAC,KAAD;OAAG,WAAU;iBAAb,CAA2C,GAAe,kBAAmB;;MACrF,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;MAC3C,OACE,EAAS,MAAM,UAAU,UACrB,GAAG,EAAW,OAAO,KACrB,EAAa,EAAW,OAAO;MAErC,OAAM;MACN,QAAQ,EAAW,SAAS,IAAI,UAAU;MAC1C,CAAA;KACE;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,GAAD;KAAW,WAAU;KAAQ,WAAU;eAAvC,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA4C;OAErD,CAAA,EACL,kBAAC,KAAD;OAAG,WAAU;iBAAqC;OAE9C,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,SAAS;OACnC,WAAU;iBAHZ,CAIC,WACQ,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA,CAClC;SACL;SACL,EAAW,OAAO,MAAM,EAAE,OAAO,EAAE,GAClC,kBAAC,GAAD;MACE,cAAa;MACb,MAAM,kBAAC,GAAD,EAAS,WAAU,aAAc,CAAA;MACvC,OAAM;MACN,aAAY;MACZ,CAAA,GAEF,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAqB,OAAM;OAAO,QAAO;iBACvC,kBAAC,GAAD;QAAU,MAAM;QAAY,QAAQ;SAAE,KAAK;SAAI,OAAO;SAAG,MAAM;SAAG,QAAQ;SAAG;kBAA7E;SACE,kBAAC,GAAD;UACE,SAAQ;UACR,MAAM;WAAE,MAAM;WAA+B,UAAU;WAAI;UAC3D,UAAU,EAAE,QAAQ,4BAA4B;UAChD,UAAU;UACV,CAAA;SACF,kBAAC,GAAD;UACE,MAAM;WAAE,MAAM;WAA8B,UAAU;WAAI;UAC1D,UAAU;UACV,UAAU;UACV,OAAO;UACP,gBAAgB,MAAe,KAAK,MAAO,GAAG,KAAK,MAAM,IAAI,IAAK,CAAC,KAAK,GAAG;UAC3E,CAAA;SACF,kBAAC,GAAD;UACE,QAAQ;WAAE,MAAM;WAA0B,SAAS;WAAK;UACxD,cAAc;WACZ,YAAY;WACZ,QAAQ;WACR,cAAc;WACd,OAAO;WACR;UACD,YAAY,MAAkB,CAAC,EAAW,EAAM,EAAE,YAAY;UAC9D,CAAA;SACF,kBAAC,IAAD;UAAK,SAAQ;UAAK,QAAQ;WAAC;WAAG;WAAG;WAAG;WAAE;UAAE,YAAY;oBAApD,CACG,EAAW,KAAK,MACf,kBAAC,GAAD,EAAoB,MAAM,EAAW,EAAE,QAAU,EAAtC,EAAE,MAAoC,CACjD,EACF,kBAAC,GAAD;WACE,SAAQ;WACR,UAAS;WACT,YAAY,MAAe,IAAI,IAAI,EAAW,EAAE,GAAG;WACnD,OAAO;YAAE,MAAM;YAA+B,UAAU;YAAI;WAC5D,CAAA,CACE;;SACG;;OACS,CAAA;MAClB,CAAA,CAEE;QAGZ,kBAAC,GAAD;KAAW,WAAU;KAAQ,WAAU;eAAvC,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA4C;OAAoB,CAAA,EAC9E,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,aAAa;OACvC,WAAU;iBAHZ,CAIC,eACY,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA,CACtC;SACL;SACL,EAAW,WAAW,IACrB,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,iDAAkD,CAAA,EACzE,kBAAC,KAAD;OAAG,WAAU;iBAAmC;OAE5C,CAAA,CACA;UAEN,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAW,KAAK,MACf,kBAAC,MAAD;OAAe,WAAU;iBAAzB,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAkD,EAAE;SAAc,CAAA,EAC/E,kBAAC,KAAD;SAAG,WAAU;mBAAb;UACG,EAAa,EAAE,MAAM;UAAC;UAAE,EAAE;UAAK;UAAI,EAAmB,EAAE,WAAW;UAClE;WACA;WACN,kBAAC,GAAD,EAAa,QAAO,UAAW,CAAA,CAC5B;SARI,EAAE,GAQN,CACL;MACC,CAAA,CAEG;OACR;;GAGN,kBAAC,GAAD;IACE,OAAM;IACN,aAAY;IACZ,QACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,eAAe;KACzC,WAAU;eAHZ,CAIC,qBACkB,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA,CAC5C;;cAGV,EAAgB,WAAW,IAC1B,kBAAC,GAAD;KACE,cAAa;KACb,MAAM,kBAAC,IAAD,EAAY,WAAU,aAAc,CAAA;KAC1C,OAAM;KACN,aAAY;KACZ,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAgB,KAAK,MACpB,kBAAC,GAAD;MAAsB,WAAU;MAAQ,WAAU;gBAAlD,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAE,SAAS,cAAc;SACxB,CAAA,EACH,EAAE,SAAS,kBACV,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAE,QAAQ;SACT,CAAA,CAEF;WACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CAA0G,KACtG,EAAa,EAAE,MAAM,CAClB;UACH;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAA8B,GAAgB,EAAE,OAAO;QAAQ,CAAA,EAC/E,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAmB,EAAE,WAAW;QAC5B,CAAA,CACH;SACI;QAtBI,EAAE,GAsBN,CACZ;KACE,CAAA;IAEA,CAAA;GAGT,EAAY,SAAS,KACpB,kBAAC,GAAD;IACE,OAAM;IACN,aAAY;cAEZ,kBAAC,GAAD;KAAW,WAAU;KAAQ,WAAU;eACrC,kBAAC,OAAD;MAAK,WAAU;uBACL;OACN,IAAM,oBAAS,IAAI,KAAqB;AACxC,YAAK,IAAM,KAAK,EACd,GAAO,IAAI,EAAE,qBAAqB,EAAO,IAAI,EAAE,mBAAmB,IAAI,KAAK,EAAE;OAE/E,IAAM,IAAQ,EAAY;AAC1B,cAAO,CAAC,GAAG,EAAO,SAAS,CAAC,CACzB,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG,CAC3B,KAAK,CAAC,GAAQ,OACb,kBAAC,OAAD;QAAkB,WAAU;kBAA5B;SACE,kBAAC,OAAD;UAAK,WAAU;oBACb,kBAAC,GAAD,EAAqB,WAAU,CAAA;UAC3B,CAAA;SACN,kBAAC,GAAD;UAAS,OAAQ,IAAQ,IAAS;UAAK,WAAU;UAAW,CAAA;SAC5D,kBAAC,QAAD;UAAM,WAAU;oBACb;UACI,CAAA;SACH;UARI,EAQJ,CACN;UACF;MACA,CAAA;KACI,CAAA;IACJ,CAAA;GAED"}
1
+ {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/ecoimpacthub/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard — EcoImpactHub environment, conservation & sustainability overview.\n * @module ecoimpacthub/pages\n *\n * KPIs are aggregated client-side from the real `eco*` list endpoints plus the\n * `ecoCarbonTotalKg` aggregate (by scope). Widgets show recent field\n * observations and open threshold breaches. No mock data.\n */\n\nimport { useMemo } from 'react';\nimport {\n MapPin,\n PawPrint,\n Trees,\n Factory,\n TriangleAlert,\n Binoculars,\n ShieldCheck,\n ArrowRight,\n Radio,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n BarChart,\n Bar,\n XAxis,\n YAxis,\n CartesianGrid,\n ResponsiveContainer,\n Tooltip,\n Cell,\n LabelList,\n} from 'recharts';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { MiniBar } from '../components/MiniBar';\nimport { LoadingView, Section } from '../components/StateViews';\nimport { EmptyState } from '../components/EmptyState';\nimport { ScopeSplitBar, ScopeLegend, Meter, SCOPE_HUE } from './carbonVisuals';\nimport { useEcoImpactHub } from '../context';\nimport {\n useEcoSites,\n useEcoSpecies,\n useEcoHabitatProjects,\n useEcoCarbonTotals,\n useEcoMeasurements,\n useEcoObservations,\n useEcoSensors,\n} from '../hooks';\nimport {\n formatNumber,\n formatRelativeTime,\n formatCo2e,\n formatEnumLabel,\n formatPercent,\n} from '../utils/formatters';\nimport type { EcoConservationStatus, EcoEmissionScope } from '../types';\n\nconst ACTIVE_PROJECT_STATUSES = new Set(['PLANNED', 'IN_PROGRESS', 'MONITORING']);\n\n/** Conservation-status → meter tint (semantic, IUCN-severity ordered). */\nconst CONSERVATION_TONE: Record<EcoConservationStatus, string> = {\n LEAST_CONCERN: 'var(--color-status-success-text)',\n NEAR_THREATENED: 'var(--color-accent-teal)',\n VULNERABLE: 'var(--color-status-warning-text)',\n ENDANGERED: 'var(--color-status-error-text)',\n CRITICALLY_ENDANGERED: 'var(--color-status-error-text)',\n EXTINCT_IN_WILD: 'var(--color-text-tertiary)',\n DATA_DEFICIENT: 'var(--color-text-tertiary)',\n NOT_EVALUATED: 'var(--color-text-tertiary)',\n};\n\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useEcoImpactHub();\n\n const sites = useEcoSites({ pagination: { limit: 100, offset: 0 } });\n const species = useEcoSpecies({ pagination: { limit: 100, offset: 0 } });\n const projects = useEcoHabitatProjects();\n const carbon = useEcoCarbonTotals();\n const breaches = useEcoMeasurements({ breachesOnly: true, pagination: { limit: 6, offset: 0 } });\n const observations = useEcoObservations({ pagination: { limit: 6, offset: 0 } });\n const sensors = useEcoSensors();\n\n const siteRows = sites.data?.items ?? [];\n const speciesRows = species.data?.items ?? [];\n const projectRows = projects.data ?? [];\n const sensorRows = sensors.data ?? [];\n\n const activeSites = siteRows.filter((s) => s.status === 'ACTIVE').length;\n const priorityCount = speciesRows.filter((s) => s.isPriority).length;\n const threatened = speciesRows.filter((s) =>\n ['VULNERABLE', 'ENDANGERED', 'CRITICALLY_ENDANGERED'].includes(s.conservationStatus)\n ).length;\n const activeProjects = projectRows.filter((p) => ACTIVE_PROJECT_STATUSES.has(p.status)).length;\n const onlineSensors = sensorRows.filter((s) => s.status === 'ONLINE').length;\n const offlineSensors = sensorRows.filter((s) =>\n ['OFFLINE', 'DEGRADED', 'MAINTENANCE'].includes(s.status)\n ).length;\n\n const carbonData = useMemo<{ scope: EcoEmissionScope; label: string; kg: number }[]>(() => {\n const c = carbon.data;\n if (!c) return [];\n return [\n { scope: 'SCOPE_1', label: 'Scope 1', kg: c.scope1 },\n { scope: 'SCOPE_2', label: 'Scope 2', kg: c.scope2 },\n { scope: 'SCOPE_3', label: 'Scope 3', kg: c.scope3 },\n ];\n }, [carbon.data]);\n\n const isInitialLoading =\n sites.isLoading && species.isLoading && projects.isLoading && carbon.isLoading;\n\n if (isInitialLoading) {\n return (\n <PageLayout title=\"EcoImpactHub\" titleGradient aurora>\n <LoadingView label=\"Loading your impact overview…\" />\n </PageLayout>\n );\n }\n\n const greetingName = currentUser?.firstName || currentUser?.name || 'there';\n const breachRows = breaches.data?.items ?? [];\n const observationRows = observations.data?.items ?? [];\n\n return (\n <PageLayout\n title=\"EcoImpactHub\"\n titleGradient\n aurora\n description={`Welcome back, ${greetingName} — your environment, conservation & sustainability control room.`}\n >\n {/* KPI grid */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6\">\n <MetricsCard\n icon={<MapPin className=\"h-5 w-5\" />}\n value={\n sites.data?.pageInfo?.hasMore ? `${siteRows.length}+` : formatNumber(siteRows.length)\n }\n label=\"Monitored sites\"\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={siteRows.length ? (activeSites / siteRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-action-primary-bg\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{activeSites}</span> active\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<PawPrint className=\"h-5 w-5\" />}\n value={\n species.data?.pageInfo?.hasMore\n ? `${speciesRows.length}+`\n : formatNumber(speciesRows.length)\n }\n label=\"Species tracked\"\n accent={threatened > 0 ? 'warning' : undefined}\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={speciesRows.length ? (threatened / speciesRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-status-warning-text\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{priorityCount}</span> priority ·{' '}\n <span className=\"tabular-nums text-text-secondary\">{threatened}</span> threatened\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<Trees className=\"h-5 w-5\" />}\n value={formatNumber(activeProjects)}\n label=\"Active habitat projects\"\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={projectRows.length ? (activeProjects / projectRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-action-primary-bg\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(projectRows.length)}\n </span>{' '}\n total\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<Factory className=\"h-5 w-5\" />}\n value={carbon.data ? formatCo2e(carbon.data.total) : '—'}\n label=\"Total footprint\"\n featured\n footer={\n carbon.data && carbon.data.total > 0 ? (\n <div className=\"space-y-2\">\n <ScopeSplitBar\n scope1={carbon.data.scope1}\n scope2={carbon.data.scope2}\n scope3={carbon.data.scope3}\n height={6}\n />\n <p className=\"text-xs text-text-tertiary\">Scope 1 · 2 · 3 split</p>\n </div>\n ) : undefined\n }\n />\n <MetricsCard\n icon={<Radio className=\"h-5 w-5\" />}\n value={formatNumber(onlineSensors)}\n label=\"Sensors online\"\n accent={offlineSensors > 0 ? 'warning' : 'success'}\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={sensorRows.length ? (onlineSensors / sensorRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-status-success-text\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{offlineSensors}</span> need\n attention\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<TriangleAlert className=\"h-5 w-5\" />}\n value={\n breaches.data?.pageInfo?.hasMore\n ? `${breachRows.length}+`\n : formatNumber(breachRows.length)\n }\n label=\"Open breaches\"\n accent={breachRows.length > 0 ? 'error' : 'success'}\n footer={\n <p\n className={\n breachRows.length > 0\n ? 'text-xs font-medium text-status-error-text'\n : 'text-xs text-text-tertiary'\n }\n >\n {breachRows.length > 0 ? 'Needs attention' : 'All parameters in range'}\n </p>\n }\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Carbon by scope */}\n <GlassCard treatment=\"glass\" className=\"p-6 lg:col-span-2\">\n <div className=\"mb-4 flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">\n Carbon footprint by scope\n </h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n GHG Protocol Scope 1 / 2 / 3 emissions (CO₂e).\n </p>\n </div>\n <div className=\"flex items-center gap-4\">\n {carbon.data && carbon.data.total > 0 && (\n <div className=\"text-right\">\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatCo2e(carbon.data.total)}\n </p>\n <p className=\"text-xs text-text-tertiary\">total</p>\n </div>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo('carbon')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Ledger <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n {carbonData.every((d) => d.kg === 0) ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Factory className=\"h-10 w-10\" />}\n title=\"No carbon entries yet\"\n description=\"Record activity in the carbon ledger to see your Scope 1–3 footprint here.\"\n />\n ) : (\n <>\n <div className=\"h-56\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <BarChart data={carbonData} margin={{ top: 20, right: 8, left: 8, bottom: 0 }}>\n <defs>\n {carbonData.map((d) => (\n <linearGradient\n key={d.scope}\n id={`ecoScopeGrad-${d.scope}`}\n x1=\"0\"\n y1=\"0\"\n x2=\"0\"\n y2=\"1\"\n >\n <stop\n offset=\"0%\"\n style={{ stopColor: SCOPE_HUE[d.scope], stopOpacity: 1 }}\n />\n <stop\n offset=\"100%\"\n style={{ stopColor: SCOPE_HUE[d.scope], stopOpacity: 0.55 }}\n />\n </linearGradient>\n ))}\n </defs>\n <CartesianGrid\n vertical={false}\n stroke=\"var(--color-border-seam)\"\n strokeOpacity={0.6}\n strokeDasharray=\"3 4\"\n />\n <XAxis\n dataKey=\"label\"\n tick={{ fill: 'var(--color-text-secondary)', fontSize: 12 }}\n axisLine={{ stroke: 'var(--color-border-seam)' }}\n tickLine={false}\n dy={4}\n />\n <YAxis\n tick={{ fill: 'var(--color-text-tertiary)', fontSize: 11 }}\n axisLine={false}\n tickLine={false}\n width={48}\n tickFormatter={(v: number) =>\n v >= 1000 ? `${Math.round(v / 1000)}t` : `${v}`\n }\n />\n <Tooltip\n cursor={{ fill: 'var(--color-bg-sunken)', opacity: 0.4 }}\n contentStyle={{\n background: 'var(--color-bg-surface)',\n border: '1px solid var(--color-border-seam)',\n borderRadius: 12,\n color: 'var(--color-text-primary)',\n }}\n formatter={(value: number) => [formatCo2e(value), 'Emissions']}\n />\n <Bar dataKey=\"kg\" radius={[8, 8, 2, 2]} maxBarSize={72}>\n {carbonData.map((d) => (\n <Cell key={d.scope} fill={`url(#ecoScopeGrad-${d.scope})`} />\n ))}\n <LabelList\n dataKey=\"kg\"\n position=\"top\"\n formatter={(v: number) => (v > 0 ? formatCo2e(v) : '')}\n style={{\n fill: 'var(--color-text-secondary)',\n fontSize: 11,\n fontWeight: 600,\n }}\n />\n </Bar>\n </BarChart>\n </ResponsiveContainer>\n </div>\n <ScopeLegend data={carbonData} className=\"mt-4 border-t border-border-seam pt-4\" />\n </>\n )}\n </GlassCard>\n\n {/* Recent breaches */}\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-base font-semibold text-text-primary\">Recent breaches</h2>\n {breachRows.length > 0 && (\n <span className=\"rounded-full bg-status-error-bg-subtle px-2 py-0.5 text-xs font-medium tabular-nums text-status-error-text\">\n {breaches.data?.pageInfo?.hasMore ? `${breachRows.length}+` : breachRows.length}\n </span>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('monitoring')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Monitoring <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n {breachRows.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center rounded-xl border border-dashed border-border-seam py-10 text-center\">\n <span className=\"flex size-12 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <ShieldCheck className=\"h-6 w-6 text-status-success-text\" />\n </span>\n <p className=\"mt-3 text-sm font-medium text-text-primary\">All parameters in range</p>\n <p className=\"mt-1 text-xs text-text-tertiary\">No open threshold breaches.</p>\n </div>\n ) : (\n <ul className=\"space-y-2.5\">\n {breachRows.map((m) => (\n <li\n key={m.id}\n className=\"flex items-start justify-between gap-3 rounded-xl bg-bg-sunken/50 p-3\"\n >\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <span\n aria-hidden=\"true\"\n className=\"mt-1.5 size-2 shrink-0 rounded-full bg-status-error-text motion-safe:animate-vc-glow-pulse\"\n />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {m.parameter}\n </p>\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums\">\n {formatNumber(m.value)} {m.unit}\n </span>{' '}\n · {formatRelativeTime(m.recordedAt)}\n </p>\n </div>\n </div>\n <StatusBadge status=\"BREACH\" />\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </div>\n\n {/* Recent observations */}\n <Section\n title=\"Recent field observations\"\n description=\"Latest wildlife & biodiversity sightings recorded across your sites.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('observations')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n All observations <ArrowRight className=\"h-4 w-4\" />\n </button>\n }\n >\n {observationRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Binoculars className=\"h-10 w-10\" />}\n title=\"No observations yet\"\n description=\"Record your first field observation to build your biodiversity dataset.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {observationRows.map((o) => (\n <GlassCard\n key={o.id}\n treatment=\"plain\"\n className=\"p-4 transition-shadow hover:shadow-float\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <span className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent\">\n <Binoculars className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {o.species?.commonName ?? 'Unknown species'}\n </p>\n {o.species?.scientificName && (\n <p className=\"truncate text-xs italic text-text-tertiary\">\n {o.species.scientificName}\n </p>\n )}\n </div>\n </div>\n <span className=\"shrink-0 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium tabular-nums text-text-primary\">\n ×{formatNumber(o.count)}\n </span>\n </div>\n <div className=\"mt-3 flex items-center justify-between border-t border-border-seam/70 pt-2.5\">\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {formatEnumLabel(o.method)}\n </span>\n <span className=\"text-xs text-text-tertiary\">\n {formatRelativeTime(o.observedAt)}\n </span>\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </Section>\n\n {/* Conservation status snapshot */}\n {speciesRows.length > 0 && (\n <Section\n title=\"Conservation status\"\n description=\"Distribution of tracked species by IUCN-style status.\"\n >\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"space-y-3.5\">\n {(() => {\n const counts = new Map<string, number>();\n for (const s of speciesRows) {\n counts.set(s.conservationStatus, (counts.get(s.conservationStatus) ?? 0) + 1);\n }\n const total = speciesRows.length;\n return [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([status, count]) => {\n const pct = (count / total) * 100;\n const tone =\n CONSERVATION_TONE[status as EcoConservationStatus] ??\n 'var(--color-text-tertiary)';\n return (\n <div key={status} className=\"flex items-center gap-3 sm:gap-4\">\n <div className=\"w-36 shrink-0 sm:w-48\">\n <StatusBadge status={status} />\n </div>\n <Meter value={pct} color={tone} className=\"flex-1\" />\n <span className=\"w-9 shrink-0 text-right text-sm font-medium tabular-nums text-text-primary\">\n {count}\n </span>\n <span className=\"hidden w-12 shrink-0 text-right text-xs tabular-nums text-text-tertiary sm:inline\">\n {formatPercent(pct)}\n </span>\n </div>\n );\n });\n })()}\n </div>\n </GlassCard>\n </Section>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { StatusBadge as t } from "../../components/StatusBadge.js";
3
+ import { CircleHelp as n, Eye as r, ShieldAlert as i, ShieldCheck as a, ShieldOff as o, TriangleAlert as s } from "lucide-react";
4
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
5
+ //#region src/ecoimpacthub/pages/_shared/ConservationBadge.tsx
6
+ var u = {
7
+ CRITICALLY_ENDANGERED: {
8
+ Icon: i,
9
+ iconClass: "text-status-error-text"
10
+ },
11
+ ENDANGERED: {
12
+ Icon: i,
13
+ iconClass: "text-status-error-text"
14
+ },
15
+ VULNERABLE: {
16
+ Icon: s,
17
+ iconClass: "text-status-warning-text"
18
+ },
19
+ NEAR_THREATENED: {
20
+ Icon: r,
21
+ iconClass: "text-accent"
22
+ },
23
+ LEAST_CONCERN: {
24
+ Icon: a,
25
+ iconClass: "text-status-success-text"
26
+ },
27
+ EXTINCT_IN_WILD: {
28
+ Icon: o,
29
+ iconClass: "text-text-secondary"
30
+ },
31
+ DATA_DEFICIENT: {
32
+ Icon: n,
33
+ iconClass: "text-text-tertiary"
34
+ },
35
+ NOT_EVALUATED: {
36
+ Icon: n,
37
+ iconClass: "text-text-tertiary"
38
+ }
39
+ }, d = {
40
+ Icon: n,
41
+ iconClass: "text-text-tertiary"
42
+ }, f = ({ status: n, className: r }) => {
43
+ let { Icon: i, iconClass: a } = u[n.toUpperCase()] ?? d;
44
+ return /* @__PURE__ */ l("span", {
45
+ className: e("inline-flex items-center gap-1.5", r),
46
+ children: [/* @__PURE__ */ c(i, {
47
+ className: e("h-4 w-4 shrink-0", a),
48
+ "aria-hidden": "true"
49
+ }), /* @__PURE__ */ c(t, {
50
+ status: n,
51
+ className: "font-semibold"
52
+ })]
53
+ });
54
+ };
55
+ //#endregion
56
+ export { f as ConservationBadge };
57
+
58
+ //# sourceMappingURL=ConservationBadge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConservationBadge.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/_shared/ConservationBadge.tsx"],"sourcesContent":["/**\n * ConservationBadge — a rich IUCN conservation-status indicator.\n * @module ecoimpacthub/pages/_shared\n *\n * Wraps the shared {@link StatusBadge} (which already maps every IUCN status to\n * the correct semantic tone — Endangered/Critically Endangered = destructive,\n * Vulnerable = warning, Near Threatened = info, Least Concern = success, Data\n * Deficient/Not Evaluated = neutral) and prefixes a tone-matched threat-tier\n * icon so the badge reads at a glance in a dense catalog. Tokens only.\n */\n\nimport type { FC } from 'react';\nimport {\n CircleHelp,\n Eye,\n ShieldAlert,\n ShieldCheck,\n ShieldOff,\n TriangleAlert,\n type LucideIcon,\n} from 'lucide-react';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { cn } from '../../utils/cn';\n\ninterface Tier {\n Icon: LucideIcon;\n iconClass: string;\n}\n\nconst TIER: Record<string, Tier> = {\n CRITICALLY_ENDANGERED: { Icon: ShieldAlert, iconClass: 'text-status-error-text' },\n ENDANGERED: { Icon: ShieldAlert, iconClass: 'text-status-error-text' },\n VULNERABLE: { Icon: TriangleAlert, iconClass: 'text-status-warning-text' },\n NEAR_THREATENED: { Icon: Eye, iconClass: 'text-accent' },\n LEAST_CONCERN: { Icon: ShieldCheck, iconClass: 'text-status-success-text' },\n EXTINCT_IN_WILD: { Icon: ShieldOff, iconClass: 'text-text-secondary' },\n DATA_DEFICIENT: { Icon: CircleHelp, iconClass: 'text-text-tertiary' },\n NOT_EVALUATED: { Icon: CircleHelp, iconClass: 'text-text-tertiary' },\n};\n\nconst FALLBACK: Tier = { Icon: CircleHelp, iconClass: 'text-text-tertiary' };\n\ninterface ConservationBadgeProps {\n status: string;\n className?: string;\n}\n\nexport const ConservationBadge: FC<ConservationBadgeProps> = ({ status, className }) => {\n const tier = TIER[status.toUpperCase()] ?? FALLBACK;\n const { Icon, iconClass } = tier;\n return (\n <span className={cn('inline-flex items-center gap-1.5', className)}>\n <Icon className={cn('h-4 w-4 shrink-0', iconClass)} aria-hidden=\"true\" />\n <StatusBadge status={status} className=\"font-semibold\" />\n </span>\n );\n};\n"],"mappings":";;;;;AA6BA,IAAM,IAA6B;CACjC,uBAAuB;EAAE,MAAM;EAAa,WAAW;EAA0B;CACjF,YAAY;EAAE,MAAM;EAAa,WAAW;EAA0B;CACtE,YAAY;EAAE,MAAM;EAAe,WAAW;EAA4B;CAC1E,iBAAiB;EAAE,MAAM;EAAK,WAAW;EAAe;CACxD,eAAe;EAAE,MAAM;EAAa,WAAW;EAA4B;CAC3E,iBAAiB;EAAE,MAAM;EAAW,WAAW;EAAuB;CACtE,gBAAgB;EAAE,MAAM;EAAY,WAAW;EAAsB;CACrE,eAAe;EAAE,MAAM;EAAY,WAAW;EAAsB;CACrE,EAEK,IAAiB;CAAE,MAAM;CAAY,WAAW;CAAsB,EAO/D,KAAiD,EAAE,WAAQ,mBAAgB;CAEtF,IAAM,EAAE,SAAM,iBADD,EAAK,EAAO,aAAa,KAAK;AAE3C,QACE,kBAAC,QAAD;EAAM,WAAW,EAAG,oCAAoC,EAAU;YAAlE,CACE,kBAAC,GAAD;GAAM,WAAW,EAAG,oBAAoB,EAAU;GAAE,eAAY;GAAS,CAAA,EACzE,kBAAC,GAAD;GAAqB;GAAQ,WAAU;GAAkB,CAAA,CACpD"}
@@ -0,0 +1,70 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { useEcoFile as t } from "../../hooks/useEcoFiles.js";
3
+ import "../../hooks/index.js";
4
+ import { useState as n } from "react";
5
+ import { AudioLines as r, FileText as i, ImageOff as a, Loader2 as o } from "lucide-react";
6
+ import { jsx as s } from "react/jsx-runtime";
7
+ //#region src/ecoimpacthub/pages/_shared/EvidenceThumb.tsx
8
+ var c = "flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border-seam bg-bg-sunken";
9
+ function l({ fileId: l, alt: u }) {
10
+ let { data: d, isLoading: f, isError: p } = t(l), [m, h] = n(!1);
11
+ if (!l) return /* @__PURE__ */ s("span", {
12
+ className: e(c, "text-text-tertiary"),
13
+ "aria-label": "No evidence",
14
+ title: "No evidence",
15
+ children: /* @__PURE__ */ s(a, {
16
+ className: "h-4 w-4",
17
+ "aria-hidden": "true"
18
+ })
19
+ });
20
+ if (f) return /* @__PURE__ */ s("span", {
21
+ className: e(c, "text-text-tertiary"),
22
+ "aria-label": "Loading evidence",
23
+ children: /* @__PURE__ */ s(o, {
24
+ className: "h-4 w-4 animate-spin",
25
+ "aria-hidden": "true"
26
+ })
27
+ });
28
+ if (p || !d || !d.downloadUrl) return /* @__PURE__ */ s("span", {
29
+ className: e(c, "text-text-tertiary"),
30
+ "aria-label": "Evidence unavailable",
31
+ title: d?.name ?? "Evidence unavailable",
32
+ children: /* @__PURE__ */ s(i, {
33
+ className: "h-4 w-4",
34
+ "aria-hidden": "true"
35
+ })
36
+ });
37
+ let g = d.mimeType ?? "", _ = g.startsWith("image/"), v = g.startsWith("audio/");
38
+ return _ && !m ? /* @__PURE__ */ s("a", {
39
+ href: d.downloadUrl,
40
+ target: "_blank",
41
+ rel: "noopener noreferrer",
42
+ onClick: (e) => e.stopPropagation(),
43
+ className: e(c, "p-0 transition-shadow hover:ring-2 hover:ring-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"),
44
+ title: d.name,
45
+ "aria-label": `Open evidence photo${u ? `: ${u}` : ""}`,
46
+ children: /* @__PURE__ */ s("img", {
47
+ src: d.downloadUrl,
48
+ alt: u ?? d.name,
49
+ loading: "lazy",
50
+ onError: () => h(!0),
51
+ className: "h-full w-full object-cover"
52
+ })
53
+ }) : /* @__PURE__ */ s("a", {
54
+ href: d.downloadUrl,
55
+ target: "_blank",
56
+ rel: "noopener noreferrer",
57
+ onClick: (e) => e.stopPropagation(),
58
+ className: e(c, "text-text-secondary transition-colors hover:border-accent hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"),
59
+ title: d.name,
60
+ "aria-label": `Open evidence: ${d.name}`,
61
+ children: s(v ? r : i, {
62
+ className: "h-4 w-4",
63
+ "aria-hidden": "true"
64
+ })
65
+ });
66
+ }
67
+ //#endregion
68
+ export { l as EvidenceThumb };
69
+
70
+ //# sourceMappingURL=EvidenceThumb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EvidenceThumb.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/_shared/EvidenceThumb.tsx"],"sourcesContent":["/**\n * EvidenceThumb — an inline evidence-photo thumbnail for a field observation.\n * @module ecoimpacthub/pages/_shared\n *\n * Observation queries only select `evidence { id }`, so this resolves the\n * platform File via `useEcoFile(id)` (wspace-files-svc `getFile`, Integration A)\n * to obtain a signed `downloadUrl` + `mimeType`, then renders:\n * - an actual <img> thumbnail (linked to the full asset) for image evidence,\n * - an audio / document glyph tile (linked) for non-image evidence,\n * - and graceful, tone-muted fallback tiles for: no evidence, still loading,\n * unresolvable file, or a broken image URL.\n *\n * The `useEcoFile` query is disabled when there is no `fileId`, so only rows\n * that actually carry evidence trigger a fetch (and React Query dedupes by id).\n */\n\nimport { useState } from 'react';\nimport { AudioLines, FileText, ImageOff, Loader2 } from 'lucide-react';\nimport { useEcoFile } from '../../hooks';\nimport { cn } from '../../utils/cn';\n\nconst TILE =\n 'flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border-seam bg-bg-sunken';\n\ninterface EvidenceThumbProps {\n fileId?: string | null;\n /** Descriptive alt text (e.g. the species name) for the rendered image. */\n alt?: string;\n}\n\nexport function EvidenceThumb({ fileId, alt }: EvidenceThumbProps) {\n const { data, isLoading, isError } = useEcoFile(fileId);\n const [imgBroken, setImgBroken] = useState(false);\n\n // No evidence attached to this observation.\n if (!fileId) {\n return (\n <span className={cn(TILE, 'text-text-tertiary')} aria-label=\"No evidence\" title=\"No evidence\">\n <ImageOff className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n );\n }\n\n // Resolving the file record.\n if (isLoading) {\n return (\n <span className={cn(TILE, 'text-text-tertiary')} aria-label=\"Loading evidence\">\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n </span>\n );\n }\n\n // File record missing / no usable URL.\n if (isError || !data || !data.downloadUrl) {\n return (\n <span\n className={cn(TILE, 'text-text-tertiary')}\n aria-label=\"Evidence unavailable\"\n title={data?.name ?? 'Evidence unavailable'}\n >\n <FileText className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n );\n }\n\n const mimeType = data.mimeType ?? '';\n const isImage = mimeType.startsWith('image/');\n const isAudio = mimeType.startsWith('audio/');\n\n // Image evidence → real thumbnail, linked to the full asset.\n if (isImage && !imgBroken) {\n return (\n <a\n href={data.downloadUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={(e) => e.stopPropagation()}\n className={cn(\n TILE,\n 'p-0 transition-shadow hover:ring-2 hover:ring-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent'\n )}\n title={data.name}\n aria-label={`Open evidence photo${alt ? `: ${alt}` : ''}`}\n >\n <img\n src={data.downloadUrl}\n alt={alt ?? data.name}\n loading=\"lazy\"\n onError={() => setImgBroken(true)}\n className=\"h-full w-full object-cover\"\n />\n </a>\n );\n }\n\n // Non-image (or broken image) evidence → a linked glyph tile.\n return (\n <a\n href={data.downloadUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={(e) => e.stopPropagation()}\n className={cn(\n TILE,\n 'text-text-secondary transition-colors hover:border-accent hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent'\n )}\n title={data.name}\n aria-label={`Open evidence: ${data.name}`}\n >\n {isAudio ? (\n <AudioLines className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <FileText className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n </a>\n );\n}\n"],"mappings":";;;;;;;AAqBA,IAAM,IACJ;AAQF,SAAgB,EAAc,EAAE,WAAQ,UAA2B;CACjE,IAAM,EAAE,SAAM,cAAW,eAAY,EAAW,EAAO,EACjD,CAAC,GAAW,KAAgB,EAAS,GAAM;AAGjD,KAAI,CAAC,EACH,QACE,kBAAC,QAAD;EAAM,WAAW,EAAG,GAAM,qBAAqB;EAAE,cAAW;EAAc,OAAM;YAC9E,kBAAC,GAAD;GAAU,WAAU;GAAU,eAAY;GAAS,CAAA;EAC9C,CAAA;AAKX,KAAI,EACF,QACE,kBAAC,QAAD;EAAM,WAAW,EAAG,GAAM,qBAAqB;EAAE,cAAW;YAC1D,kBAAC,GAAD;GAAS,WAAU;GAAuB,eAAY;GAAS,CAAA;EAC1D,CAAA;AAKX,KAAI,KAAW,CAAC,KAAQ,CAAC,EAAK,YAC5B,QACE,kBAAC,QAAD;EACE,WAAW,EAAG,GAAM,qBAAqB;EACzC,cAAW;EACX,OAAO,GAAM,QAAQ;YAErB,kBAAC,GAAD;GAAU,WAAU;GAAU,eAAY;GAAS,CAAA;EAC9C,CAAA;CAIX,IAAM,IAAW,EAAK,YAAY,IAC5B,IAAU,EAAS,WAAW,SAAS,EACvC,IAAU,EAAS,WAAW,SAAS;AA6B7C,QA1BI,KAAW,CAAC,IAEZ,kBAAC,KAAD;EACE,MAAM,EAAK;EACX,QAAO;EACP,KAAI;EACJ,UAAU,MAAM,EAAE,iBAAiB;EACnC,WAAW,EACT,GACA,iIACD;EACD,OAAO,EAAK;EACZ,cAAY,sBAAsB,IAAM,KAAK,MAAQ;YAErD,kBAAC,OAAD;GACE,KAAK,EAAK;GACV,KAAK,KAAO,EAAK;GACjB,SAAQ;GACR,eAAe,EAAa,GAAK;GACjC,WAAU;GACV,CAAA;EACA,CAAA,GAMN,kBAAC,KAAD;EACE,MAAM,EAAK;EACX,QAAO;EACP,KAAI;EACJ,UAAU,MAAM,EAAE,iBAAiB;EACnC,WAAW,EACT,GACA,wJACD;EACD,OAAO,EAAK;EACZ,cAAY,kBAAkB,EAAK;YAGjC,EADD,IACE,IAEA,GAFD;GAAY,WAAU;GAAU,eAAY;GAAS,CAEF;EAEnD,CAAA"}
@@ -0,0 +1,51 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { formatEnumLabel as t } from "../../utils/formatters.js";
3
+ import { AudioLines as n, Camera as r, Dna as i, Eye as a, Footprints as o, SatelliteDish as s, Users as c } from "lucide-react";
4
+ import { jsx as l, jsxs as u } from "react/jsx-runtime";
5
+ //#region src/ecoimpacthub/pages/_shared/MethodBadge.tsx
6
+ var d = {
7
+ Icon: a,
8
+ className: "bg-bg-sunken text-text-secondary"
9
+ }, f = {
10
+ DIRECT_SIGHTING: {
11
+ Icon: a,
12
+ className: "bg-status-info-bg-subtle text-status-info-text"
13
+ },
14
+ CAMERA_TRAP: {
15
+ Icon: r,
16
+ className: "bg-status-success-bg-subtle text-status-success-text"
17
+ },
18
+ COMMUNITY_REPORT: {
19
+ Icon: c,
20
+ className: "bg-status-warning-bg-subtle text-status-warning-text"
21
+ },
22
+ ACOUSTIC: {
23
+ Icon: n,
24
+ className: "bg-accent-indigo-subtle text-accent-indigo"
25
+ },
26
+ TRACKS_SIGNS: {
27
+ Icon: o,
28
+ className: "bg-accent-teal-subtle text-accent-teal"
29
+ },
30
+ EDNA: {
31
+ Icon: i,
32
+ className: "bg-accent-blue-subtle text-accent-blue"
33
+ },
34
+ TELEMETRY: {
35
+ Icon: s,
36
+ className: "bg-bg-sunken text-text-secondary"
37
+ }
38
+ }, p = ({ method: n, className: r }) => {
39
+ let i = f[n.toUpperCase()] ?? d, { Icon: a } = i;
40
+ return /* @__PURE__ */ u("span", {
41
+ className: e("inline-flex items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 py-1 text-xs font-medium", i.className, r),
42
+ children: [/* @__PURE__ */ l(a, {
43
+ className: "h-3.5 w-3.5 shrink-0",
44
+ "aria-hidden": "true"
45
+ }), t(n)]
46
+ });
47
+ };
48
+ //#endregion
49
+ export { p as MethodBadge };
50
+
51
+ //# sourceMappingURL=MethodBadge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MethodBadge.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/_shared/MethodBadge.tsx"],"sourcesContent":["/**\n * MethodBadge — an icon-led, tone-coded chip for an observation method.\n * @module ecoimpacthub/pages/_shared\n *\n * Each survey method gets a distinct semantic tone + lucide glyph so a mixed\n * field-observation feed is instantly scannable: Camera Trap (success), Direct\n * Sighting (info), Community Report (warning), Acoustic (indigo), Tracks/Signs\n * (teal), eDNA (blue), Telemetry (neutral). Semantic tokens only — no raw color.\n */\n\nimport type { FC } from 'react';\nimport {\n AudioLines,\n Camera,\n Dna,\n Eye,\n Footprints,\n SatelliteDish,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { formatEnumLabel } from '../../utils/formatters';\n\ninterface MethodStyle {\n Icon: LucideIcon;\n className: string;\n}\n\nconst NEUTRAL: MethodStyle = { Icon: Eye, className: 'bg-bg-sunken text-text-secondary' };\n\nconst METHOD: Record<string, MethodStyle> = {\n DIRECT_SIGHTING: { Icon: Eye, className: 'bg-status-info-bg-subtle text-status-info-text' },\n CAMERA_TRAP: { Icon: Camera, className: 'bg-status-success-bg-subtle text-status-success-text' },\n COMMUNITY_REPORT: {\n Icon: Users,\n className: 'bg-status-warning-bg-subtle text-status-warning-text',\n },\n ACOUSTIC: { Icon: AudioLines, className: 'bg-accent-indigo-subtle text-accent-indigo' },\n TRACKS_SIGNS: { Icon: Footprints, className: 'bg-accent-teal-subtle text-accent-teal' },\n EDNA: { Icon: Dna, className: 'bg-accent-blue-subtle text-accent-blue' },\n TELEMETRY: { Icon: SatelliteDish, className: 'bg-bg-sunken text-text-secondary' },\n};\n\ninterface MethodBadgeProps {\n method: string;\n className?: string;\n}\n\nexport const MethodBadge: FC<MethodBadgeProps> = ({ method, className }) => {\n const style = METHOD[method.toUpperCase()] ?? NEUTRAL;\n const { Icon } = style;\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 py-1 text-xs font-medium',\n style.className,\n className\n )}\n >\n <Icon className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" />\n {formatEnumLabel(method)}\n </span>\n );\n};\n"],"mappings":";;;;;AA6BA,IAAM,IAAuB;CAAE,MAAM;CAAK,WAAW;CAAoC,EAEnF,IAAsC;CAC1C,iBAAiB;EAAE,MAAM;EAAK,WAAW;EAAkD;CAC3F,aAAa;EAAE,MAAM;EAAQ,WAAW;EAAwD;CAChG,kBAAkB;EAChB,MAAM;EACN,WAAW;EACZ;CACD,UAAU;EAAE,MAAM;EAAY,WAAW;EAA8C;CACvF,cAAc;EAAE,MAAM;EAAY,WAAW;EAA0C;CACvF,MAAM;EAAE,MAAM;EAAK,WAAW;EAA0C;CACxE,WAAW;EAAE,MAAM;EAAe,WAAW;EAAoC;CAClF,EAOY,KAAqC,EAAE,WAAQ,mBAAgB;CAC1E,IAAM,IAAQ,EAAO,EAAO,aAAa,KAAK,GACxC,EAAE,YAAS;AACjB,QACE,kBAAC,QAAD;EACE,WAAW,EACT,mGACA,EAAM,WACN,EACD;YALH,CAOE,kBAAC,GAAD;GAAM,WAAU;GAAuB,eAAY;GAAS,CAAA,EAC3D,EAAgB,EAAO,CACnB"}
@@ -0,0 +1,29 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { formatEnumLabel as t } from "../../utils/formatters.js";
3
+ import { Bird as n, Bug as r, Fish as i, Leaf as a, PawPrint as o, Shell as s, Snail as c, Sprout as l, Turtle as u } from "lucide-react";
4
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
5
+ //#region src/ecoimpacthub/pages/_shared/TaxonChip.tsx
6
+ var p = {
7
+ MAMMAL: o,
8
+ BIRD: n,
9
+ REPTILE: u,
10
+ AMPHIBIAN: c,
11
+ FISH: i,
12
+ INSECT: r,
13
+ PLANT: l,
14
+ FUNGI: s,
15
+ OTHER: a
16
+ }, m = ({ group: n, className: r }) => {
17
+ let i = p[n.toUpperCase()] ?? a;
18
+ return /* @__PURE__ */ f("span", {
19
+ className: e("inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-bg-sunken/70 px-2.5 py-0.5 text-xs font-medium text-text-secondary", r),
20
+ children: [/* @__PURE__ */ d(i, {
21
+ className: "h-3.5 w-3.5 text-text-tertiary",
22
+ "aria-hidden": "true"
23
+ }), t(n)]
24
+ });
25
+ };
26
+ //#endregion
27
+ export { m as TaxonChip };
28
+
29
+ //# sourceMappingURL=TaxonChip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaxonChip.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/_shared/TaxonChip.tsx"],"sourcesContent":["/**\n * TaxonChip — a compact, icon-led chip for a species' taxon group.\n * @module ecoimpacthub/pages/_shared\n *\n * Presentational-only; semantic tokens exclusively. Shared by the Species\n * catalog + Species detail pages so taxon rendering stays consistent.\n */\n\nimport type { FC } from 'react';\nimport {\n Bird,\n Bug,\n Fish,\n Leaf,\n PawPrint,\n Shell,\n Snail,\n Sprout,\n Turtle,\n type LucideIcon,\n} from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { formatEnumLabel } from '../../utils/formatters';\n\nconst TAXON_ICON: Record<string, LucideIcon> = {\n MAMMAL: PawPrint,\n BIRD: Bird,\n REPTILE: Turtle,\n AMPHIBIAN: Snail,\n FISH: Fish,\n INSECT: Bug,\n PLANT: Sprout,\n FUNGI: Shell,\n OTHER: Leaf,\n};\n\ninterface TaxonChipProps {\n group: string;\n className?: string;\n}\n\nexport const TaxonChip: FC<TaxonChipProps> = ({ group, className }) => {\n const Icon = TAXON_ICON[group.toUpperCase()] ?? Leaf;\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-bg-sunken/70 px-2.5 py-0.5 text-xs font-medium text-text-secondary',\n className\n )}\n >\n <Icon className=\"h-3.5 w-3.5 text-text-tertiary\" aria-hidden=\"true\" />\n {formatEnumLabel(group)}\n </span>\n );\n};\n"],"mappings":";;;;;AAwBA,IAAM,IAAyC;CAC7C,QAAQ;CACR,MAAM;CACN,SAAS;CACT,WAAW;CACX,MAAM;CACN,QAAQ;CACR,OAAO;CACP,OAAO;CACP,OAAO;CACR,EAOY,KAAiC,EAAE,UAAO,mBAAgB;CACrE,IAAM,IAAO,EAAW,EAAM,aAAa,KAAK;AAChD,QACE,kBAAC,QAAD;EACE,WAAW,EACT,iJACA,EACD;YAJH,CAME,kBAAC,GAAD;GAAM,WAAU;GAAiC,eAAY;GAAS,CAAA,EACrE,EAAgB,EAAM,CAClB"}
@@ -0,0 +1,71 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { formatEnumLabel as t } from "../utils/formatters.js";
3
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
+ //#region src/ecoimpacthub/pages/badges.tsx
5
+ var i = {
6
+ success: "bg-status-success-bg-subtle text-status-success-text",
7
+ warning: "bg-status-warning-bg-subtle text-status-warning-text",
8
+ destructive: "bg-status-error-bg-subtle text-status-error-text",
9
+ secondary: "bg-bg-sunken text-text-secondary",
10
+ info: "bg-[var(--color-accent-soft)] text-text-primary",
11
+ outline: "border border-border-seam text-text-primary"
12
+ };
13
+ function a({ variant: t = "secondary", live: a = !1, className: o, children: s }) {
14
+ return /* @__PURE__ */ r("span", {
15
+ className: e("inline-flex items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 py-0.5 text-xs font-medium", i[t], o),
16
+ children: [a && /* @__PURE__ */ n("span", {
17
+ "aria-hidden": "true",
18
+ className: "size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse"
19
+ }), s]
20
+ });
21
+ }
22
+ var o = {
23
+ PLANNED: "secondary",
24
+ IN_PROGRESS: "warning",
25
+ MONITORING: "info",
26
+ COMPLETED: "success",
27
+ ON_HOLD: "warning",
28
+ CANCELLED: "destructive"
29
+ }, s = new Set(["IN_PROGRESS", "MONITORING"]);
30
+ function c({ status: e, className: r }) {
31
+ return /* @__PURE__ */ n(a, {
32
+ variant: o[e],
33
+ live: s.has(e),
34
+ className: r,
35
+ children: t(e)
36
+ });
37
+ }
38
+ var l = {
39
+ PLANTING: "success",
40
+ REWILDING: "success",
41
+ INVASIVE_REMOVAL: "warning",
42
+ SOIL_REMEDIATION: "warning",
43
+ WATER_MANAGEMENT: "info",
44
+ SURVEY: "info",
45
+ COMMUNITY_ENGAGEMENT: "outline"
46
+ };
47
+ function u({ type: e }) {
48
+ return /* @__PURE__ */ n(a, {
49
+ variant: l[e],
50
+ children: t(e)
51
+ });
52
+ }
53
+ var d = {
54
+ GHG_PROTOCOL: "GHG Protocol",
55
+ GRI: "GRI",
56
+ TCFD: "TCFD",
57
+ CDP: "CDP",
58
+ SASB: "SASB",
59
+ CUSTOM: "Custom"
60
+ };
61
+ function f({ framework: e, className: t }) {
62
+ return /* @__PURE__ */ n(a, {
63
+ variant: "outline",
64
+ className: t,
65
+ children: d[e]
66
+ });
67
+ }
68
+ //#endregion
69
+ export { u as ActivityTypeBadge, f as FrameworkBadge, c as LifecycleBadge };
70
+
71
+ //# sourceMappingURL=badges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"badges.js","names":[],"sources":["../../../src/ecoimpacthub/pages/badges.tsx"],"sourcesContent":["/**\n * Page-local badge primitives (Boff UI 2.0).\n * @module ecoimpacthub/pages/badges\n *\n * A small, presentational pill built from the exact same semantic-token visual\n * language as the shared {@link StatusBadge} — used where the shared badge can't\n * express what these screens need:\n * - `LifecycleBadge` colours the habitat lifecycle incl. `IN_PROGRESS`\n * (the shared map leaves it uncoloured — see the \"shared tweaks\" note).\n * - `ActivityTypeBadge` colours restoration-activity types (not in the shared map).\n * - `FrameworkBadge` renders neutral-outline framework pills with correct\n * acronym casing (GHG Protocol / TCFD / …) which `formatEnumLabel` can't do.\n *\n * Semantic tokens only — no raw colours.\n */\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel } from '../utils/formatters';\nimport type {\n EcoHabitatProjectStatus,\n EcoReportFramework,\n EcoRestorationActivityType,\n} from '../types';\n\nexport type PillVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\nconst pillStyles: Record<PillVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n outline: 'border border-border-seam text-text-primary',\n};\n\ninterface PillProps {\n variant?: PillVariant;\n /** Adds a subtle, reduced-motion-safe \"live\" pulse dot. */\n live?: boolean;\n className?: string;\n children: ReactNode;\n}\n\n/** Rounded status pill — visually identical to the shared StatusBadge. */\nexport function Pill({ variant = 'secondary', live = false, className, children }: PillProps) {\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 py-0.5 text-xs font-medium',\n pillStyles[variant],\n className\n )}\n >\n {live && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {children}\n </span>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Habitat lifecycle\n// ---------------------------------------------------------------------------\n\nconst LIFECYCLE_VARIANT: Record<EcoHabitatProjectStatus, PillVariant> = {\n PLANNED: 'secondary',\n IN_PROGRESS: 'warning',\n MONITORING: 'info',\n COMPLETED: 'success',\n ON_HOLD: 'warning',\n CANCELLED: 'destructive',\n};\n\nconst LIFECYCLE_LIVE = new Set<EcoHabitatProjectStatus>(['IN_PROGRESS', 'MONITORING']);\n\nexport function LifecycleBadge({\n status,\n className,\n}: {\n status: EcoHabitatProjectStatus;\n className?: string;\n}) {\n return (\n <Pill\n variant={LIFECYCLE_VARIANT[status]}\n live={LIFECYCLE_LIVE.has(status)}\n className={className}\n >\n {formatEnumLabel(status)}\n </Pill>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Restoration activity type\n// ---------------------------------------------------------------------------\n\nconst ACTIVITY_VARIANT: Record<EcoRestorationActivityType, PillVariant> = {\n PLANTING: 'success',\n REWILDING: 'success',\n INVASIVE_REMOVAL: 'warning',\n SOIL_REMEDIATION: 'warning',\n WATER_MANAGEMENT: 'info',\n SURVEY: 'info',\n COMMUNITY_ENGAGEMENT: 'outline',\n};\n\nexport function ActivityTypeBadge({ type }: { type: EcoRestorationActivityType }) {\n return <Pill variant={ACTIVITY_VARIANT[type]}>{formatEnumLabel(type)}</Pill>;\n}\n\n// ---------------------------------------------------------------------------\n// Report framework\n// ---------------------------------------------------------------------------\n\nexport const FRAMEWORK_LABELS: Record<EcoReportFramework, string> = {\n GHG_PROTOCOL: 'GHG Protocol',\n GRI: 'GRI',\n TCFD: 'TCFD',\n CDP: 'CDP',\n SASB: 'SASB',\n CUSTOM: 'Custom',\n};\n\nexport function FrameworkBadge({\n framework,\n className,\n}: {\n framework: EcoReportFramework;\n className?: string;\n}) {\n return (\n <Pill variant=\"outline\" className={className}>\n {FRAMEWORK_LABELS[framework]}\n </Pill>\n );\n}\n"],"mappings":";;;;AA2BA,IAAM,IAA0C;CAC9C,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;CACV;AAWD,SAAgB,EAAK,EAAE,aAAU,aAAa,UAAO,IAAO,cAAW,eAAuB;AAC5F,QACE,kBAAC,QAAD;EACE,WAAW,EACT,qGACA,EAAW,IACX,EACD;YALH,CAOG,KACC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;GACV,CAAA,EAEH,EACI;;;AAQX,IAAM,IAAkE;CACtE,SAAS;CACT,aAAa;CACb,YAAY;CACZ,WAAW;CACX,SAAS;CACT,WAAW;CACZ,EAEK,IAAiB,IAAI,IAA6B,CAAC,eAAe,aAAa,CAAC;AAEtF,SAAgB,EAAe,EAC7B,WACA,gBAIC;AACD,QACE,kBAAC,GAAD;EACE,SAAS,EAAkB;EAC3B,MAAM,EAAe,IAAI,EAAO;EACrB;YAEV,EAAgB,EAAO;EACnB,CAAA;;AAQX,IAAM,IAAoE;CACxE,UAAU;CACV,WAAW;CACX,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAClB,QAAQ;CACR,sBAAsB;CACvB;AAED,SAAgB,EAAkB,EAAE,WAA8C;AAChF,QAAO,kBAAC,GAAD;EAAM,SAAS,EAAiB;YAAQ,EAAgB,EAAK;EAAQ,CAAA;;AAO9E,IAAa,IAAuD;CAClE,cAAc;CACd,KAAK;CACL,MAAM;CACN,KAAK;CACL,MAAM;CACN,QAAQ;CACT;AAED,SAAgB,EAAe,EAC7B,cACA,gBAIC;AACD,QACE,kBAAC,GAAD;EAAM,SAAQ;EAAqB;YAChC,EAAiB;EACb,CAAA"}