@pulsekit/react 0.0.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.
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+
3
+ interface PulseChartProps {
4
+ data: {
5
+ date: string;
6
+ totalViews: number;
7
+ uniqueVisitors: number;
8
+ }[];
9
+ }
10
+ declare function PulseChart({ data }: PulseChartProps): React.ReactElement | null;
11
+
12
+ export { PulseChart, type PulseChartProps };
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ AreaChart,
5
+ Area,
6
+ XAxis,
7
+ YAxis,
8
+ CartesianGrid,
9
+ Tooltip,
10
+ ResponsiveContainer
11
+ } from "recharts";
12
+ function formatDate(dateStr) {
13
+ const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
14
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
15
+ }
16
+ function PulseChart({ data }) {
17
+ if (data.length === 0) return null;
18
+ return /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: 300, children: /* @__PURE__ */ jsxs(AreaChart, { data, children: [
19
+ /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--border)" }),
20
+ /* @__PURE__ */ jsx(
21
+ XAxis,
22
+ {
23
+ dataKey: "date",
24
+ tickFormatter: formatDate,
25
+ fontSize: 12,
26
+ stroke: "var(--muted-foreground)"
27
+ }
28
+ ),
29
+ /* @__PURE__ */ jsx(
30
+ YAxis,
31
+ {
32
+ fontSize: 12,
33
+ stroke: "var(--muted-foreground)",
34
+ allowDecimals: false
35
+ }
36
+ ),
37
+ /* @__PURE__ */ jsx(
38
+ Tooltip,
39
+ {
40
+ labelFormatter: formatDate,
41
+ contentStyle: {
42
+ backgroundColor: "var(--card)",
43
+ border: "1px solid var(--border)",
44
+ borderRadius: "var(--radius)",
45
+ fontSize: 12
46
+ }
47
+ }
48
+ ),
49
+ /* @__PURE__ */ jsx(
50
+ Area,
51
+ {
52
+ type: "monotone",
53
+ dataKey: "totalViews",
54
+ name: "Views",
55
+ stroke: "var(--chart-1)",
56
+ fill: "var(--chart-1)",
57
+ fillOpacity: 0.2,
58
+ strokeWidth: 2
59
+ }
60
+ ),
61
+ /* @__PURE__ */ jsx(
62
+ Area,
63
+ {
64
+ type: "monotone",
65
+ dataKey: "uniqueVisitors",
66
+ name: "Unique visitors",
67
+ stroke: "var(--chart-2)",
68
+ fill: "var(--chart-2)",
69
+ fillOpacity: 0.2,
70
+ strokeWidth: 2
71
+ }
72
+ )
73
+ ] }) });
74
+ }
75
+ export {
76
+ PulseChart
77
+ };
@@ -0,0 +1,14 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { SupabaseClient } from '@supabase/supabase-js';
3
+ import { Timeframe } from '@pulsekit/core';
4
+
5
+ interface PulseDashboardProps {
6
+ supabase: SupabaseClient;
7
+ siteId: string;
8
+ timeframe?: Timeframe;
9
+ timezone?: string;
10
+ refreshEndpoint?: string;
11
+ }
12
+ declare function PulseDashboard({ supabase, siteId, timeframe, timezone, refreshEndpoint, }: PulseDashboardProps): Promise<react_jsx_runtime.JSX.Element>;
13
+
14
+ export { PulseDashboard, type PulseDashboardProps };
@@ -0,0 +1,179 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { getPulseStats, getPulseVitals } from "@pulsekit/core";
3
+ import { PulseChart } from "./PulseChart";
4
+ import { PulseMap } from "./PulseMap";
5
+ import { PulseVitals } from "./PulseVitals";
6
+ import { RefreshButton } from "./RefreshButton";
7
+ async function PulseDashboard({
8
+ supabase,
9
+ siteId,
10
+ timeframe = "7d",
11
+ timezone,
12
+ refreshEndpoint
13
+ }) {
14
+ const [stats, vitals] = await Promise.all([
15
+ getPulseStats({ supabase, siteId, timeframe, timezone }),
16
+ getPulseVitals({ supabase, siteId, timeframe }).catch((err) => {
17
+ console.error("getPulseVitals failed:", err);
18
+ return { overall: [], byPage: [] };
19
+ })
20
+ ]);
21
+ return /* @__PURE__ */ jsxs("div", { style: { maxWidth: 896, margin: "0 auto", padding: 24 }, children: [
22
+ /* @__PURE__ */ jsxs(
23
+ "div",
24
+ {
25
+ style: {
26
+ display: "flex",
27
+ alignItems: "center",
28
+ justifyContent: "space-between",
29
+ marginBottom: 24
30
+ },
31
+ children: [
32
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: 24, fontWeight: 600, margin: 0 }, children: "Pulse Analytics" }),
33
+ /* @__PURE__ */ jsx(RefreshButton, { endpoint: refreshEndpoint })
34
+ ]
35
+ }
36
+ ),
37
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 24 }, children: [
38
+ /* @__PURE__ */ jsxs(
39
+ "section",
40
+ {
41
+ style: {
42
+ border: "1px solid #e5e7eb",
43
+ borderRadius: 8,
44
+ overflow: "hidden"
45
+ },
46
+ children: [
47
+ /* @__PURE__ */ jsx("div", { style: { padding: "16px 20px", borderBottom: "1px solid #e5e7eb" }, children: /* @__PURE__ */ jsx("h2", { style: { fontSize: 16, fontWeight: 600, margin: 0 }, children: "Traffic over time" }) }),
48
+ /* @__PURE__ */ jsx("div", { style: { padding: 20 }, children: stats.daily.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 14, color: "#6b7280", margin: 0 }, children: "No analytics data yet. Visit your site and refresh aggregates." }) : /* @__PURE__ */ jsx(PulseChart, { data: stats.daily }) })
49
+ ]
50
+ }
51
+ ),
52
+ /* @__PURE__ */ jsxs(
53
+ "section",
54
+ {
55
+ style: {
56
+ border: "1px solid #e5e7eb",
57
+ borderRadius: 8,
58
+ overflow: "hidden"
59
+ },
60
+ children: [
61
+ /* @__PURE__ */ jsx("div", { style: { padding: "16px 20px", borderBottom: "1px solid #e5e7eb" }, children: /* @__PURE__ */ jsx("h2", { style: { fontSize: 16, fontWeight: 600, margin: 0 }, children: "Top pages" }) }),
62
+ /* @__PURE__ */ jsx("div", { style: { padding: 20 }, children: stats.topPages.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 14, color: "#6b7280", margin: 0 }, children: "No page data available for this timeframe." }) : /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
63
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
64
+ /* @__PURE__ */ jsx(
65
+ "th",
66
+ {
67
+ style: {
68
+ textAlign: "left",
69
+ padding: "8px 0",
70
+ fontSize: 14,
71
+ fontWeight: 500,
72
+ borderBottom: "1px solid #e5e7eb"
73
+ },
74
+ children: "Path"
75
+ }
76
+ ),
77
+ /* @__PURE__ */ jsx(
78
+ "th",
79
+ {
80
+ style: {
81
+ textAlign: "right",
82
+ padding: "8px 0",
83
+ fontSize: 14,
84
+ fontWeight: 500,
85
+ borderBottom: "1px solid #e5e7eb"
86
+ },
87
+ children: "Views"
88
+ }
89
+ ),
90
+ /* @__PURE__ */ jsx(
91
+ "th",
92
+ {
93
+ style: {
94
+ textAlign: "right",
95
+ padding: "8px 0",
96
+ fontSize: 14,
97
+ fontWeight: 500,
98
+ borderBottom: "1px solid #e5e7eb"
99
+ },
100
+ children: "Unique"
101
+ }
102
+ )
103
+ ] }) }),
104
+ /* @__PURE__ */ jsx("tbody", { children: stats.topPages.map((p) => /* @__PURE__ */ jsxs("tr", { children: [
105
+ /* @__PURE__ */ jsx(
106
+ "td",
107
+ {
108
+ style: {
109
+ padding: "8px 0",
110
+ fontSize: 12,
111
+ fontFamily: "monospace",
112
+ borderBottom: "1px solid #f3f4f6"
113
+ },
114
+ children: p.path
115
+ }
116
+ ),
117
+ /* @__PURE__ */ jsx(
118
+ "td",
119
+ {
120
+ style: {
121
+ textAlign: "right",
122
+ padding: "8px 0",
123
+ fontSize: 14,
124
+ borderBottom: "1px solid #f3f4f6"
125
+ },
126
+ children: p.totalViews
127
+ }
128
+ ),
129
+ /* @__PURE__ */ jsx(
130
+ "td",
131
+ {
132
+ style: {
133
+ textAlign: "right",
134
+ padding: "8px 0",
135
+ fontSize: 14,
136
+ borderBottom: "1px solid #f3f4f6"
137
+ },
138
+ children: p.uniqueVisitors
139
+ }
140
+ )
141
+ ] }, p.path)) })
142
+ ] }) })
143
+ ]
144
+ }
145
+ ),
146
+ vitals.overall.length > 0 && /* @__PURE__ */ jsxs(
147
+ "section",
148
+ {
149
+ style: {
150
+ border: "1px solid #e5e7eb",
151
+ borderRadius: 8,
152
+ overflow: "hidden"
153
+ },
154
+ children: [
155
+ /* @__PURE__ */ jsx("div", { style: { padding: "16px 20px", borderBottom: "1px solid #e5e7eb" }, children: /* @__PURE__ */ jsx("h2", { style: { fontSize: 16, fontWeight: 600, margin: 0 }, children: "Web Vitals" }) }),
156
+ /* @__PURE__ */ jsx("div", { style: { padding: 20 }, children: /* @__PURE__ */ jsx(PulseVitals, { data: vitals }) })
157
+ ]
158
+ }
159
+ ),
160
+ stats.locations.length > 0 && /* @__PURE__ */ jsxs(
161
+ "section",
162
+ {
163
+ style: {
164
+ border: "1px solid #e5e7eb",
165
+ borderRadius: 8,
166
+ overflow: "hidden"
167
+ },
168
+ children: [
169
+ /* @__PURE__ */ jsx("div", { style: { padding: "16px 20px", borderBottom: "1px solid #e5e7eb" }, children: /* @__PURE__ */ jsx("h2", { style: { fontSize: 16, fontWeight: 600, margin: 0 }, children: "Visitors by location" }) }),
170
+ /* @__PURE__ */ jsx("div", { style: { padding: 20 }, children: /* @__PURE__ */ jsx(PulseMap, { data: stats.locations }) })
171
+ ]
172
+ }
173
+ )
174
+ ] })
175
+ ] });
176
+ }
177
+ export {
178
+ PulseDashboard
179
+ };
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+
3
+ interface PulseMapProps {
4
+ data: {
5
+ country: string;
6
+ city: string | null;
7
+ latitude: number | null;
8
+ longitude: number | null;
9
+ totalViews: number;
10
+ uniqueVisitors: number;
11
+ }[];
12
+ }
13
+ declare function PulseMap({ data }: PulseMapProps): React.ReactElement;
14
+
15
+ export { PulseMap, type PulseMapProps };
@@ -0,0 +1,177 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import {
5
+ ComposableMap,
6
+ Geographies,
7
+ Geography,
8
+ Marker
9
+ } from "react-simple-maps";
10
+ const GEO_URL = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json";
11
+ function countryName(code) {
12
+ try {
13
+ return new Intl.DisplayNames(["en"], { type: "region" }).of(code) ?? code;
14
+ } catch {
15
+ return code;
16
+ }
17
+ }
18
+ function bubbleRadius(views, maxViews) {
19
+ if (maxViews === 0) return 4;
20
+ return 4 + 16 * Math.sqrt(views / maxViews);
21
+ }
22
+ function PulseMap({ data }) {
23
+ const [mounted, setMounted] = useState(false);
24
+ useEffect(() => setMounted(true), []);
25
+ const { markers, maxViews } = useMemo(() => {
26
+ const items = data.filter(
27
+ (r) => r.latitude != null && r.longitude != null
28
+ );
29
+ let max = 0;
30
+ for (const m of items) {
31
+ if (m.totalViews > max) max = m.totalViews;
32
+ }
33
+ return { markers: items, maxViews: max };
34
+ }, [data]);
35
+ const tableRows = useMemo(() => {
36
+ return data.map((row) => ({
37
+ country: row.country,
38
+ countryName: countryName(row.country),
39
+ city: row.city,
40
+ views: row.totalViews,
41
+ unique: row.uniqueVisitors
42
+ })).sort((a, b) => b.views - a.views);
43
+ }, [data]);
44
+ return /* @__PURE__ */ jsxs("div", { children: [
45
+ /* @__PURE__ */ jsx("div", { style: { width: "100%", height: "auto", overflow: "hidden" }, children: !mounted ? /* @__PURE__ */ jsx("div", { style: { height: 400 } }) : /* @__PURE__ */ jsxs(
46
+ ComposableMap,
47
+ {
48
+ projectionConfig: { scale: 147 },
49
+ width: 800,
50
+ height: 400,
51
+ style: { width: "100%", height: "auto", display: "block" },
52
+ children: [
53
+ /* @__PURE__ */ jsx(Geographies, { geography: GEO_URL, children: ({ geographies }) => geographies.map((geo) => /* @__PURE__ */ jsx(
54
+ Geography,
55
+ {
56
+ geography: geo,
57
+ fill: "#f0f0f0",
58
+ stroke: "#d1d5db",
59
+ strokeWidth: 0.5,
60
+ style: {
61
+ default: { outline: "none" },
62
+ hover: { outline: "none" },
63
+ pressed: { outline: "none" }
64
+ }
65
+ },
66
+ geo.rsmKey
67
+ )) }),
68
+ markers.map((m, i) => /* @__PURE__ */ jsx(
69
+ Marker,
70
+ {
71
+ coordinates: [m.longitude, m.latitude],
72
+ children: /* @__PURE__ */ jsx(
73
+ "circle",
74
+ {
75
+ r: bubbleRadius(m.totalViews, maxViews),
76
+ fill: "rgba(99, 102, 241, 0.6)",
77
+ stroke: "rgba(99, 102, 241, 0.9)",
78
+ strokeWidth: 1
79
+ }
80
+ )
81
+ },
82
+ `${m.country}-${m.city ?? "unknown"}-${i}`
83
+ ))
84
+ ]
85
+ }
86
+ ) }),
87
+ tableRows.length > 0 && /* @__PURE__ */ jsxs(
88
+ "table",
89
+ {
90
+ style: { width: "100%", borderCollapse: "collapse", marginTop: 16 },
91
+ children: [
92
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
93
+ /* @__PURE__ */ jsx(
94
+ "th",
95
+ {
96
+ style: {
97
+ textAlign: "left",
98
+ padding: "8px 0",
99
+ fontSize: 14,
100
+ fontWeight: 500,
101
+ borderBottom: "1px solid #e5e7eb"
102
+ },
103
+ children: "Location"
104
+ }
105
+ ),
106
+ /* @__PURE__ */ jsx(
107
+ "th",
108
+ {
109
+ style: {
110
+ textAlign: "right",
111
+ padding: "8px 0",
112
+ fontSize: 14,
113
+ fontWeight: 500,
114
+ borderBottom: "1px solid #e5e7eb"
115
+ },
116
+ children: "Views"
117
+ }
118
+ ),
119
+ /* @__PURE__ */ jsx(
120
+ "th",
121
+ {
122
+ style: {
123
+ textAlign: "right",
124
+ padding: "8px 0",
125
+ fontSize: 14,
126
+ fontWeight: 500,
127
+ borderBottom: "1px solid #e5e7eb"
128
+ },
129
+ children: "Unique"
130
+ }
131
+ )
132
+ ] }) }),
133
+ /* @__PURE__ */ jsx("tbody", { children: tableRows.map((row, i) => /* @__PURE__ */ jsxs("tr", { children: [
134
+ /* @__PURE__ */ jsx(
135
+ "td",
136
+ {
137
+ style: {
138
+ padding: "8px 0",
139
+ fontSize: 14,
140
+ borderBottom: "1px solid #f3f4f6"
141
+ },
142
+ children: row.city ? `${row.city}, ${row.countryName}` : row.countryName
143
+ }
144
+ ),
145
+ /* @__PURE__ */ jsx(
146
+ "td",
147
+ {
148
+ style: {
149
+ textAlign: "right",
150
+ padding: "8px 0",
151
+ fontSize: 14,
152
+ borderBottom: "1px solid #f3f4f6"
153
+ },
154
+ children: row.views
155
+ }
156
+ ),
157
+ /* @__PURE__ */ jsx(
158
+ "td",
159
+ {
160
+ style: {
161
+ textAlign: "right",
162
+ padding: "8px 0",
163
+ fontSize: 14,
164
+ borderBottom: "1px solid #f3f4f6"
165
+ },
166
+ children: row.unique
167
+ }
168
+ )
169
+ ] }, `${row.country}-${row.city ?? "unknown"}-${i}`)) })
170
+ ]
171
+ }
172
+ )
173
+ ] });
174
+ }
175
+ export {
176
+ PulseMap
177
+ };
@@ -0,0 +1,9 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { VitalsOverview } from '@pulsekit/core';
3
+
4
+ interface PulseVitalsProps {
5
+ data: VitalsOverview;
6
+ }
7
+ declare function PulseVitals({ data }: PulseVitalsProps): react_jsx_runtime.JSX.Element;
8
+
9
+ export { PulseVitals, type PulseVitalsProps };
@@ -0,0 +1,157 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ const METRIC_LABELS = {
4
+ lcp: { label: "LCP", unit: "ms", description: "Largest Contentful Paint \u2014 time until the biggest visible element loads" },
5
+ inp: { label: "INP", unit: "ms", description: "Interaction to Next Paint \u2014 responsiveness to user input" },
6
+ cls: { label: "CLS", unit: "", description: "Cumulative Layout Shift \u2014 how much the page layout moves around" },
7
+ fcp: { label: "FCP", unit: "ms", description: "First Contentful Paint \u2014 time until first text or image appears" },
8
+ ttfb: { label: "TTFB", unit: "ms", description: "Time to First Byte \u2014 server response time" }
9
+ };
10
+ const METRIC_ORDER = ["lcp", "inp", "cls", "fcp", "ttfb"];
11
+ const RATING_COLORS = {
12
+ good: { bg: "#f0fdf4", text: "#15803d", badge: "Good" },
13
+ "needs-improvement": { bg: "#fefce8", text: "#a16207", badge: "Needs work" },
14
+ poor: { bg: "#fef2f2", text: "#dc2626", badge: "Poor" }
15
+ };
16
+ function formatValue(metric, value) {
17
+ if (metric === "cls") return value.toFixed(3);
18
+ return Math.round(value).toString();
19
+ }
20
+ function RatingBadge({ rating }) {
21
+ const colors = RATING_COLORS[rating];
22
+ return /* @__PURE__ */ jsx(
23
+ "span",
24
+ {
25
+ style: {
26
+ display: "inline-block",
27
+ padding: "2px 8px",
28
+ borderRadius: 9999,
29
+ fontSize: 11,
30
+ fontWeight: 600,
31
+ backgroundColor: colors.bg,
32
+ color: colors.text
33
+ },
34
+ children: colors.badge
35
+ }
36
+ );
37
+ }
38
+ function VitalCard({ stat }) {
39
+ const info = METRIC_LABELS[stat.metric] ?? { label: stat.metric.toUpperCase(), unit: "" };
40
+ const colors = RATING_COLORS[stat.rating];
41
+ return /* @__PURE__ */ jsxs(
42
+ "div",
43
+ {
44
+ style: {
45
+ border: "1px solid #e5e7eb",
46
+ borderRadius: 8,
47
+ padding: 16,
48
+ flex: "1 1 0",
49
+ minWidth: 140,
50
+ backgroundColor: colors.bg
51
+ },
52
+ children: [
53
+ /* @__PURE__ */ jsx(
54
+ "div",
55
+ {
56
+ title: info.description,
57
+ style: { fontSize: 12, fontWeight: 500, color: "#6b7280", marginBottom: 4, cursor: "help" },
58
+ children: info.label
59
+ }
60
+ ),
61
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 24, fontWeight: 700, color: colors.text }, children: [
62
+ formatValue(stat.metric, stat.p75),
63
+ info.unit && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, fontWeight: 400, marginLeft: 2 }, children: info.unit })
64
+ ] }),
65
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 }, children: [
66
+ /* @__PURE__ */ jsx(RatingBadge, { rating: stat.rating }),
67
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 11, color: "#9ca3af" }, children: [
68
+ stat.sampleCount,
69
+ " sample",
70
+ stat.sampleCount !== 1 ? "s" : ""
71
+ ] })
72
+ ] })
73
+ ]
74
+ }
75
+ );
76
+ }
77
+ function CellValue({ stat }) {
78
+ if (!stat) {
79
+ return /* @__PURE__ */ jsx("span", { style: { color: "#d1d5db" }, children: "--" });
80
+ }
81
+ const colors = RATING_COLORS[stat.rating];
82
+ return /* @__PURE__ */ jsx("span", { style: { color: colors.text, fontWeight: 500 }, children: formatValue(stat.metric, stat.p75) });
83
+ }
84
+ function PulseVitals({ data }) {
85
+ const overallMap = /* @__PURE__ */ new Map();
86
+ for (const stat of data.overall) {
87
+ overallMap.set(stat.metric, stat);
88
+ }
89
+ return /* @__PURE__ */ jsxs("div", { children: [
90
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 12, flexWrap: "wrap", marginBottom: 20 }, children: METRIC_ORDER.map((metric) => {
91
+ const stat = overallMap.get(metric);
92
+ if (!stat) return null;
93
+ return /* @__PURE__ */ jsx(VitalCard, { stat }, metric);
94
+ }) }),
95
+ data.byPage.length > 0 && /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
96
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
97
+ /* @__PURE__ */ jsx(
98
+ "th",
99
+ {
100
+ style: {
101
+ textAlign: "left",
102
+ padding: "8px 0",
103
+ fontSize: 14,
104
+ fontWeight: 500,
105
+ borderBottom: "1px solid #e5e7eb"
106
+ },
107
+ children: "Page"
108
+ }
109
+ ),
110
+ METRIC_ORDER.map((metric) => /* @__PURE__ */ jsx(
111
+ "th",
112
+ {
113
+ style: {
114
+ textAlign: "right",
115
+ padding: "8px 8px",
116
+ fontSize: 14,
117
+ fontWeight: 500,
118
+ borderBottom: "1px solid #e5e7eb"
119
+ },
120
+ children: METRIC_LABELS[metric]?.label ?? metric.toUpperCase()
121
+ },
122
+ metric
123
+ ))
124
+ ] }) }),
125
+ /* @__PURE__ */ jsx("tbody", { children: data.byPage.map((page) => /* @__PURE__ */ jsxs("tr", { children: [
126
+ /* @__PURE__ */ jsx(
127
+ "td",
128
+ {
129
+ style: {
130
+ padding: "8px 0",
131
+ fontSize: 12,
132
+ fontFamily: "monospace",
133
+ borderBottom: "1px solid #f3f4f6"
134
+ },
135
+ children: page.path
136
+ }
137
+ ),
138
+ METRIC_ORDER.map((metric) => /* @__PURE__ */ jsx(
139
+ "td",
140
+ {
141
+ style: {
142
+ textAlign: "right",
143
+ padding: "8px 8px",
144
+ fontSize: 14,
145
+ borderBottom: "1px solid #f3f4f6"
146
+ },
147
+ children: /* @__PURE__ */ jsx(CellValue, { stat: page.vitals[metric] })
148
+ },
149
+ metric
150
+ ))
151
+ ] }, page.path)) })
152
+ ] })
153
+ ] });
154
+ }
155
+ export {
156
+ PulseVitals
157
+ };
@@ -0,0 +1,8 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface RefreshButtonProps {
4
+ endpoint?: string;
5
+ }
6
+ declare function RefreshButton({ endpoint, }: RefreshButtonProps): react_jsx_runtime.JSX.Element;
7
+
8
+ export { RefreshButton, type RefreshButtonProps };
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ function RefreshButton({
5
+ endpoint = "/api/pulse/refresh-aggregates"
6
+ }) {
7
+ const [loading, setLoading] = useState(false);
8
+ return /* @__PURE__ */ jsx(
9
+ "button",
10
+ {
11
+ disabled: loading,
12
+ onClick: async () => {
13
+ setLoading(true);
14
+ await fetch(endpoint, { method: "POST" });
15
+ setLoading(false);
16
+ window.location.reload();
17
+ },
18
+ style: {
19
+ padding: "6px 12px",
20
+ fontSize: "14px",
21
+ borderRadius: "6px",
22
+ border: "1px solid #d1d5db",
23
+ background: "transparent",
24
+ cursor: loading ? "not-allowed" : "pointer",
25
+ opacity: loading ? 0.6 : 1
26
+ },
27
+ children: loading ? "Refreshing..." : "Refresh data"
28
+ }
29
+ );
30
+ }
31
+ export {
32
+ RefreshButton
33
+ };
@@ -0,0 +1,9 @@
1
+ export { PulseChart, PulseChartProps } from './PulseChart.mjs';
2
+ export { PulseDashboard, PulseDashboardProps } from './PulseDashboard.mjs';
3
+ export { PulseMap, PulseMapProps } from './PulseMap.mjs';
4
+ export { PulseVitals, PulseVitalsProps } from './PulseVitals.mjs';
5
+ export { RefreshButton, RefreshButtonProps } from './RefreshButton.mjs';
6
+ import 'react';
7
+ import 'react/jsx-runtime';
8
+ import '@supabase/supabase-js';
9
+ import '@pulsekit/core';
package/dist/index.mjs ADDED
@@ -0,0 +1,12 @@
1
+ import { PulseChart } from "./PulseChart";
2
+ import { PulseDashboard } from "./PulseDashboard";
3
+ import { PulseMap } from "./PulseMap";
4
+ import { PulseVitals } from "./PulseVitals";
5
+ import { RefreshButton } from "./RefreshButton";
6
+ export {
7
+ PulseChart,
8
+ PulseDashboard,
9
+ PulseMap,
10
+ PulseVitals,
11
+ RefreshButton
12
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@pulsekit/react",
3
+ "version": "0.0.1",
4
+ "main": "./dist/index.mjs",
5
+ "types": "./dist/index.d.mts",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.mjs",
12
+ "types": "./dist/index.d.mts"
13
+ }
14
+ },
15
+ "dependencies": {
16
+ "react-simple-maps": "^3.0.0",
17
+ "recharts": "^2.15.0",
18
+ "@pulsekit/core": "0.0.1"
19
+ },
20
+ "devDependencies": {
21
+ "@supabase/supabase-js": "^2.49.0",
22
+ "@types/react": "^19.0.0",
23
+ "@types/react-simple-maps": "^3.0.6",
24
+ "react": "^19.0.0",
25
+ "tsup": "^8.0.0",
26
+ "typescript": "^5.7.0"
27
+ },
28
+ "peerDependencies": {
29
+ "@supabase/supabase-js": ">=2.0.0",
30
+ "react": ">=18.0.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "dev": "tsup --watch",
35
+ "clean": "rm -rf dist"
36
+ }
37
+ }