@litemetrics/ui 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1050 @@
1
+ import "./chunk-CBT6H723.js";
2
+
3
+ // src/theme/index.ts
4
+ var KEY_TO_VAR = {
5
+ bg: "--lm-bg",
6
+ bgSecondary: "--lm-bg-secondary",
7
+ bgTertiary: "--lm-bg-tertiary",
8
+ border: "--lm-border",
9
+ borderHover: "--lm-border-hover",
10
+ text: "--lm-text",
11
+ textSecondary: "--lm-text-secondary",
12
+ textTertiary: "--lm-text-tertiary",
13
+ textMuted: "--lm-text-muted",
14
+ accent: "--lm-accent",
15
+ accentLight: "--lm-accent-light",
16
+ accentText: "--lm-accent-text",
17
+ accentHover: "--lm-accent-hover",
18
+ positive: "--lm-positive",
19
+ negative: "--lm-negative",
20
+ chartStroke: "--lm-chart-stroke",
21
+ chartFill: "--lm-chart-fill",
22
+ chartGrid: "--lm-chart-grid",
23
+ chartAxis: "--lm-chart-axis",
24
+ tooltipBg: "--lm-tooltip-bg",
25
+ tooltipText: "--lm-tooltip-text",
26
+ tooltipMuted: "--lm-tooltip-muted",
27
+ bar: "--lm-bar",
28
+ barHover: "--lm-bar-hover",
29
+ pie1: "--lm-pie-1",
30
+ pie2: "--lm-pie-2",
31
+ pie3: "--lm-pie-3",
32
+ pie4: "--lm-pie-4",
33
+ pie5: "--lm-pie-5",
34
+ pie6: "--lm-pie-6",
35
+ pie7: "--lm-pie-7",
36
+ pie8: "--lm-pie-8",
37
+ mapEmpty: "--lm-map-empty",
38
+ mapStroke: "--lm-map-stroke",
39
+ mapHover: "--lm-map-hover"
40
+ };
41
+ var defaultTheme = {
42
+ bg: "255 255 255",
43
+ bgSecondary: "250 250 250",
44
+ bgTertiary: "244 244 245",
45
+ border: "228 228 231",
46
+ borderHover: "212 212 216",
47
+ text: "24 24 27",
48
+ textSecondary: "113 113 122",
49
+ textTertiary: "161 161 170",
50
+ textMuted: "212 212 216",
51
+ accent: "99 102 241",
52
+ accentLight: "238 242 255",
53
+ accentText: "67 56 202",
54
+ accentHover: "129 140 248",
55
+ positive: "5 150 105",
56
+ negative: "239 68 68",
57
+ chartStroke: "99 102 241",
58
+ chartFill: "99 102 241",
59
+ chartGrid: "244 244 245",
60
+ chartAxis: "161 161 170",
61
+ tooltipBg: "24 24 27",
62
+ tooltipText: "255 255 255",
63
+ tooltipMuted: "161 161 170",
64
+ bar: "238 242 255",
65
+ barHover: "224 231 255",
66
+ pie1: "99 102 241",
67
+ pie2: "139 92 246",
68
+ pie3: "59 130 246",
69
+ pie4: "20 184 166",
70
+ pie5: "16 185 129",
71
+ pie6: "245 158 11",
72
+ pie7: "239 68 68",
73
+ pie8: "236 72 153",
74
+ mapEmpty: "244 244 245",
75
+ mapStroke: "228 228 231",
76
+ mapHover: "129 140 248"
77
+ };
78
+ var darkTheme = {
79
+ bg: "24 24 27",
80
+ bgSecondary: "39 39 42",
81
+ bgTertiary: "63 63 70",
82
+ border: "63 63 70",
83
+ borderHover: "82 82 91",
84
+ text: "244 244 245",
85
+ textSecondary: "161 161 170",
86
+ textTertiary: "113 113 122",
87
+ textMuted: "82 82 91",
88
+ accent: "129 140 248",
89
+ accentLight: "30 27 75",
90
+ accentText: "165 180 252",
91
+ accentHover: "165 180 252",
92
+ positive: "52 211 153",
93
+ negative: "248 113 113",
94
+ chartStroke: "129 140 248",
95
+ chartFill: "129 140 248",
96
+ chartGrid: "63 63 70",
97
+ chartAxis: "113 113 122",
98
+ tooltipBg: "244 244 245",
99
+ tooltipText: "24 24 27",
100
+ tooltipMuted: "113 113 122",
101
+ bar: "30 27 75",
102
+ barHover: "49 46 129",
103
+ pie1: "129 140 248",
104
+ pie2: "167 139 250",
105
+ pie3: "96 165 250",
106
+ pie4: "45 212 191",
107
+ pie5: "52 211 153",
108
+ pie6: "251 191 36",
109
+ pie7: "248 113 113",
110
+ pie8: "244 114 182",
111
+ mapEmpty: "39 39 42",
112
+ mapStroke: "63 63 70",
113
+ mapHover: "165 180 252"
114
+ };
115
+ function themeToCSS(theme) {
116
+ return Object.entries(theme).filter(([, v]) => v !== void 0).map(([key, value]) => `${KEY_TO_VAR[key]}: ${value};`).join("\n ");
117
+ }
118
+ function buildStyleSheet(light, dark) {
119
+ const mergedLight = { ...defaultTheme, ...light };
120
+ const mergedDark = { ...darkTheme, ...dark };
121
+ return `:root, .litemetrics-light {
122
+ ${themeToCSS(mergedLight)}
123
+ }
124
+
125
+ .dark, .litemetrics-dark {
126
+ ${themeToCSS(mergedDark)}
127
+ }`;
128
+ }
129
+ var STYLE_ID = "litemetrics-ui-theme";
130
+
131
+ // src/context/LitemetricsUIContext.tsx
132
+ import { createContext } from "react";
133
+ var LitemetricsUIContext = createContext(null);
134
+
135
+ // src/context/LitemetricsProvider.tsx
136
+ import { useState, useMemo, useEffect } from "react";
137
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
138
+ import { createClient } from "@litemetrics/client";
139
+ import { jsx } from "react/jsx-runtime";
140
+ function LitemetricsProvider({
141
+ baseUrl,
142
+ siteId,
143
+ secretKey,
144
+ defaultPeriod = "7d",
145
+ queryClient: externalQueryClient,
146
+ staleTime = 3e4,
147
+ theme,
148
+ darkTheme: darkThemeProp,
149
+ children
150
+ }) {
151
+ const [period, setPeriod] = useState(defaultPeriod);
152
+ const [dateFrom, setDateFrom] = useState("");
153
+ const [dateTo, setDateTo] = useState("");
154
+ const client = useMemo(
155
+ () => createClient({ baseUrl, siteId, secretKey }),
156
+ [baseUrl, siteId, secretKey]
157
+ );
158
+ const internalQueryClient = useMemo(
159
+ () => new QueryClient({ defaultOptions: { queries: { staleTime } } }),
160
+ [staleTime]
161
+ );
162
+ const queryClient = externalQueryClient ?? internalQueryClient;
163
+ useEffect(() => {
164
+ const css = buildStyleSheet(theme, darkThemeProp);
165
+ let el = document.getElementById(STYLE_ID);
166
+ if (!el) {
167
+ el = document.createElement("style");
168
+ el.id = STYLE_ID;
169
+ document.head.appendChild(el);
170
+ }
171
+ el.textContent = css;
172
+ return () => {
173
+ el?.remove();
174
+ };
175
+ }, [theme, darkThemeProp]);
176
+ const value = useMemo(
177
+ () => ({ client, siteId, period, setPeriod, dateFrom, setDateFrom, dateTo, setDateTo, staleTime }),
178
+ [client, siteId, period, dateFrom, dateTo, staleTime]
179
+ );
180
+ const content = /* @__PURE__ */ jsx(LitemetricsUIContext.Provider, { value, children });
181
+ if (externalQueryClient) {
182
+ return content;
183
+ }
184
+ return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: content });
185
+ }
186
+
187
+ // src/hooks/queryKeys.ts
188
+ var PREFIX = "litemetrics-ui";
189
+ var queryKeys = {
190
+ stats: (siteId, metric, period, dateFrom, dateTo) => [PREFIX, "stats", siteId, metric, period, dateFrom, dateTo],
191
+ timeSeries: (siteId, metric, period) => [PREFIX, "timeSeries", siteId, metric, period],
192
+ overview: (siteId, period, dateFrom, dateTo) => [PREFIX, "overview", siteId, period, dateFrom, dateTo],
193
+ worldMap: (siteId, period) => [PREFIX, "worldMap", siteId, period]
194
+ };
195
+
196
+ // src/hooks/useLitemetricsUI.ts
197
+ import { useContext } from "react";
198
+ function useLitemetricsUI() {
199
+ const ctx = useContext(LitemetricsUIContext);
200
+ if (!ctx) {
201
+ throw new Error("useLitemetricsUI must be used within a <LitemetricsProvider>");
202
+ }
203
+ return ctx;
204
+ }
205
+
206
+ // src/hooks/useStats.ts
207
+ import { useQuery } from "@tanstack/react-query";
208
+ function useStats(metric, options) {
209
+ const { client, siteId, period: ctxPeriod, dateFrom, dateTo, staleTime } = useLitemetricsUI();
210
+ const period = options?.period ?? ctxPeriod;
211
+ const statsOptions = period === "custom" && dateFrom && dateTo ? { period, dateFrom: new Date(dateFrom).toISOString(), dateTo: (/* @__PURE__ */ new Date(dateTo + "T23:59:59")).toISOString(), limit: options?.limit, compare: true } : { period, limit: options?.limit, compare: true };
212
+ return useQuery({
213
+ queryKey: queryKeys.stats(siteId, metric, period, dateFrom, dateTo),
214
+ queryFn: async () => {
215
+ client.setSiteId(siteId);
216
+ return client.getStats(metric, statsOptions);
217
+ },
218
+ staleTime,
219
+ enabled: options?.enabled !== false && (period !== "custom" || !!dateFrom && !!dateTo)
220
+ });
221
+ }
222
+
223
+ // src/hooks/useTimeSeries.ts
224
+ import { useQuery as useQuery2 } from "@tanstack/react-query";
225
+ function useTimeSeries(metric, options) {
226
+ const { client, siteId, period: ctxPeriod, staleTime } = useLitemetricsUI();
227
+ const period = options?.period ?? ctxPeriod;
228
+ return useQuery2({
229
+ queryKey: queryKeys.timeSeries(siteId, metric, period),
230
+ queryFn: async () => {
231
+ client.setSiteId(siteId);
232
+ return client.getTimeSeries(metric, { period });
233
+ },
234
+ staleTime,
235
+ enabled: options?.enabled !== false
236
+ });
237
+ }
238
+
239
+ // src/hooks/useOverview.ts
240
+ import { useQuery as useQuery3 } from "@tanstack/react-query";
241
+ function useOverview(options) {
242
+ const { client, siteId, period: ctxPeriod, dateFrom, dateTo, staleTime } = useLitemetricsUI();
243
+ const period = options?.period ?? ctxPeriod;
244
+ const metrics2 = options?.metrics ?? ["pageviews", "visitors", "sessions", "events"];
245
+ const statsOptions = period === "custom" && dateFrom && dateTo ? { period, dateFrom: new Date(dateFrom).toISOString(), dateTo: (/* @__PURE__ */ new Date(dateTo + "T23:59:59")).toISOString(), compare: true } : { period, compare: true };
246
+ return useQuery3({
247
+ queryKey: queryKeys.overview(siteId, period, dateFrom, dateTo),
248
+ queryFn: async () => {
249
+ client.setSiteId(siteId);
250
+ return client.getOverview(metrics2, statsOptions);
251
+ },
252
+ staleTime,
253
+ enabled: options?.enabled !== false && (period !== "custom" || !!dateFrom && !!dateTo)
254
+ });
255
+ }
256
+
257
+ // src/hooks/useThemeColors.ts
258
+ import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
259
+
260
+ // src/utils/theme.ts
261
+ function cssVar(name, fallback) {
262
+ if (typeof document === "undefined") return fallback ? `rgb(${fallback})` : "";
263
+ const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
264
+ if (!raw) return fallback ? `rgb(${fallback})` : "";
265
+ if (/^\d+\s+\d+\s+\d+$/.test(raw)) return `rgb(${raw.replace(/ /g, ", ")})`;
266
+ return raw;
267
+ }
268
+ var DEFAULT_PIE = [
269
+ "99 102 241",
270
+ "139 92 246",
271
+ "59 130 246",
272
+ "20 184 166",
273
+ "16 185 129",
274
+ "245 158 11",
275
+ "239 68 68",
276
+ "236 72 153"
277
+ ];
278
+ function getPieColors() {
279
+ return Array.from(
280
+ { length: 8 },
281
+ (_, i) => cssVar(`--lm-pie-${i + 1}`, DEFAULT_PIE[i])
282
+ );
283
+ }
284
+
285
+ // src/hooks/useThemeColors.ts
286
+ function useThemeColors() {
287
+ const [, setTick] = useState2(0);
288
+ useEffect2(() => {
289
+ if (typeof document === "undefined") return;
290
+ const target = document.documentElement;
291
+ const observer = new MutationObserver((mutations) => {
292
+ for (const m of mutations) {
293
+ if (m.type === "attributes" && (m.attributeName === "class" || m.attributeName === "style")) {
294
+ setTick((t) => t + 1);
295
+ break;
296
+ }
297
+ }
298
+ });
299
+ observer.observe(target, { attributes: true, attributeFilter: ["class", "style"] });
300
+ return () => observer.disconnect();
301
+ }, []);
302
+ const get = useCallback((varName, fallback) => cssVar(varName, fallback), []);
303
+ return { get };
304
+ }
305
+
306
+ // src/components/StatCard.tsx
307
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
308
+ function StatCard({ title, value, loading, changePercent, className }) {
309
+ const hasComparison = changePercent !== void 0 && changePercent !== null;
310
+ const isPositive = hasComparison && changePercent > 0;
311
+ const isNegative = hasComparison && changePercent < 0;
312
+ return /* @__PURE__ */ jsxs("div", { className: `rounded-xl bg-[rgb(var(--lm-bg))] border border-[rgb(var(--lm-border))] p-6 hover:border-[rgb(var(--lm-border-hover))] hover:shadow-md hover:-translate-y-0.5 transition-all duration-200 ${className ?? ""}`, children: [
313
+ /* @__PURE__ */ jsx2("p", { className: "text-xs font-medium text-[rgb(var(--lm-text-tertiary))] uppercase tracking-wide mb-1", children: title }),
314
+ loading ? /* @__PURE__ */ jsx2("div", { className: "h-8 w-20 bg-[rgb(var(--lm-bg-tertiary))] rounded animate-pulse" }) : /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
315
+ /* @__PURE__ */ jsx2("p", { className: "text-3xl font-semibold tabular-nums text-[rgb(var(--lm-text))]", children: typeof value === "number" ? value.toLocaleString() : value }),
316
+ hasComparison && /* @__PURE__ */ jsxs(
317
+ "span",
318
+ {
319
+ className: `text-xs font-medium pb-0.5 ${isPositive ? "text-[rgb(var(--lm-positive))]" : isNegative ? "text-[rgb(var(--lm-negative))]" : "text-[rgb(var(--lm-text-tertiary))]"}`,
320
+ children: [
321
+ isPositive && /* @__PURE__ */ jsx2("svg", { className: "w-3 h-3 inline mr-0.5 -mt-0.5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 15l7-7 7 7" }) }),
322
+ isNegative && /* @__PURE__ */ jsx2("svg", { className: "w-3 h-3 inline mr-0.5 -mt-0.5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) }),
323
+ changePercent === 0 ? "0%" : `${changePercent > 0 ? "+" : ""}${changePercent.toFixed(1)}%`
324
+ ]
325
+ }
326
+ )
327
+ ] })
328
+ ] });
329
+ }
330
+
331
+ // src/components/TopList.tsx
332
+ import { useState as useState3 } from "react";
333
+
334
+ // src/utils/icons.tsx
335
+ import { SiGooglechrome, SiFirefox, SiSafari, SiOpera, SiApple, SiLinux, SiAndroid } from "react-icons/si";
336
+ import { SiGoogle, SiInstagram, SiX, SiFacebook, SiLinkedin, SiYoutube, SiGithub, SiReddit, SiPinterest, SiTiktok } from "react-icons/si";
337
+ import { FaEdge, FaInternetExplorer, FaGlobe, FaWindows } from "react-icons/fa6";
338
+ import { HiOutlineDesktopComputer, HiOutlineDeviceMobile, HiOutlineDeviceTablet } from "react-icons/hi";
339
+ import { jsx as jsx3 } from "react/jsx-runtime";
340
+ var iconClass = "w-4 h-4 flex-shrink-0";
341
+ function getBrowserIcon(browser) {
342
+ const b = browser.toLowerCase();
343
+ if (b.includes("chrome") && !b.includes("edge")) return /* @__PURE__ */ jsx3(SiGooglechrome, { className: `${iconClass} text-[#4285F4]` });
344
+ if (b.includes("firefox")) return /* @__PURE__ */ jsx3(SiFirefox, { className: `${iconClass} text-[#FF7139]` });
345
+ if (b.includes("safari") && !b.includes("chrome")) return /* @__PURE__ */ jsx3(SiSafari, { className: `${iconClass} text-[#006CFF]` });
346
+ if (b.includes("edge")) return /* @__PURE__ */ jsx3(FaEdge, { className: `${iconClass} text-[#0078D7]` });
347
+ if (b.includes("opera")) return /* @__PURE__ */ jsx3(SiOpera, { className: `${iconClass} text-[#FF1B2D]` });
348
+ if (b.includes("ie") || b.includes("internet explorer")) return /* @__PURE__ */ jsx3(FaInternetExplorer, { className: `${iconClass} text-blue-400` });
349
+ return /* @__PURE__ */ jsx3(FaGlobe, { className: `${iconClass} text-zinc-400` });
350
+ }
351
+ function getOSIcon(os) {
352
+ const o = os.toLowerCase();
353
+ if (o.includes("windows")) return /* @__PURE__ */ jsx3(FaWindows, { className: `${iconClass} text-[#0078D4]` });
354
+ if (o.includes("mac") || o.includes("ios")) return /* @__PURE__ */ jsx3(SiApple, { className: `${iconClass} text-zinc-700` });
355
+ if (o.includes("linux")) return /* @__PURE__ */ jsx3(SiLinux, { className: `${iconClass} text-[#FCC624]` });
356
+ if (o.includes("android")) return /* @__PURE__ */ jsx3(SiAndroid, { className: `${iconClass} text-[#34A853]` });
357
+ return /* @__PURE__ */ jsx3(HiOutlineDesktopComputer, { className: `${iconClass} text-zinc-400` });
358
+ }
359
+ function getDeviceIcon(type) {
360
+ const t = type.toLowerCase();
361
+ if (t.includes("mobile")) return /* @__PURE__ */ jsx3(HiOutlineDeviceMobile, { className: `${iconClass} text-zinc-500` });
362
+ if (t.includes("tablet")) return /* @__PURE__ */ jsx3(HiOutlineDeviceTablet, { className: `${iconClass} text-zinc-500` });
363
+ return /* @__PURE__ */ jsx3(HiOutlineDesktopComputer, { className: `${iconClass} text-zinc-500` });
364
+ }
365
+ function getReferrerIcon(referrer) {
366
+ const r = referrer.toLowerCase();
367
+ if (r.includes("google")) return /* @__PURE__ */ jsx3(SiGoogle, { className: `${iconClass} text-[#4285F4]` });
368
+ if (r.includes("instagram")) return /* @__PURE__ */ jsx3(SiInstagram, { className: `${iconClass} text-[#E4405F]` });
369
+ if (r.includes("twitter") || r.includes("t.co") || r.includes("x.com")) return /* @__PURE__ */ jsx3(SiX, { className: `${iconClass} text-zinc-800` });
370
+ if (r.includes("facebook") || r.includes("fb.com")) return /* @__PURE__ */ jsx3(SiFacebook, { className: `${iconClass} text-[#1877F2]` });
371
+ if (r.includes("linkedin")) return /* @__PURE__ */ jsx3(SiLinkedin, { className: `${iconClass} text-[#0A66C2]` });
372
+ if (r.includes("youtube")) return /* @__PURE__ */ jsx3(SiYoutube, { className: `${iconClass} text-[#FF0000]` });
373
+ if (r.includes("github")) return /* @__PURE__ */ jsx3(SiGithub, { className: `${iconClass} text-zinc-800` });
374
+ if (r.includes("reddit")) return /* @__PURE__ */ jsx3(SiReddit, { className: `${iconClass} text-[#FF4500]` });
375
+ if (r.includes("pinterest")) return /* @__PURE__ */ jsx3(SiPinterest, { className: `${iconClass} text-[#BD081C]` });
376
+ if (r.includes("tiktok")) return /* @__PURE__ */ jsx3(SiTiktok, { className: `${iconClass} text-zinc-800` });
377
+ return /* @__PURE__ */ jsx3(FaGlobe, { className: `${iconClass} text-zinc-400` });
378
+ }
379
+ function countryToFlag(code) {
380
+ if (!code || code.length !== 2) return "";
381
+ const upper = code.toUpperCase();
382
+ return String.fromCodePoint(
383
+ ...Array.from(upper).map((c) => 127462 + c.charCodeAt(0) - 65)
384
+ );
385
+ }
386
+
387
+ // src/components/TopList.tsx
388
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
389
+ function getIcon(type, key) {
390
+ if (!type) return null;
391
+ switch (type) {
392
+ case "pages":
393
+ return /* @__PURE__ */ jsx4("svg", { className: "w-4 h-4 flex-shrink-0 text-[rgb(var(--lm-text-tertiary))]", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) });
394
+ case "referrers":
395
+ return getReferrerIcon(key);
396
+ case "countries": {
397
+ const flag = countryToFlag(key);
398
+ return flag ? /* @__PURE__ */ jsx4("span", { className: "text-sm flex-shrink-0 leading-none", children: flag }) : null;
399
+ }
400
+ case "events":
401
+ return /* @__PURE__ */ jsx4("svg", { className: "w-4 h-4 flex-shrink-0 text-amber-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M13 10V3L4 14h7v7l9-11h-7z" }) });
402
+ case "browsers":
403
+ return getBrowserIcon(key);
404
+ case "devices":
405
+ return getDeviceIcon(key);
406
+ }
407
+ }
408
+ function TopList({ title, data, loading, type, className }) {
409
+ const [tooltip, setTooltip] = useState3(null);
410
+ const maxValue = data ? Math.max(...data.map((d) => d.value), 1) : 1;
411
+ const totalValue = data ? data.reduce((sum, d) => sum + d.value, 0) : 0;
412
+ return /* @__PURE__ */ jsxs2("div", { className: `rounded-xl bg-[rgb(var(--lm-bg))] border border-[rgb(var(--lm-border))] p-5 hover:shadow-sm transition-all duration-200 ${className ?? ""}`, children: [
413
+ /* @__PURE__ */ jsx4("h3", { className: "text-xs font-medium text-[rgb(var(--lm-text-tertiary))] uppercase tracking-wide mb-4", children: title }),
414
+ loading ? /* @__PURE__ */ jsx4("div", { className: "space-y-3", children: [...Array(5)].map((_, i) => /* @__PURE__ */ jsx4("div", { className: "h-7 bg-[rgb(var(--lm-bg-secondary))] rounded animate-pulse" }, i)) }) : !data || data.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "py-8 text-center", children: /* @__PURE__ */ jsx4("p", { className: "text-[rgb(var(--lm-text-muted))] text-sm", children: "No data yet" }) }) : /* @__PURE__ */ jsx4("div", { className: "space-y-1", children: data.map((item) => {
415
+ const pct = totalValue > 0 ? Math.round(item.value / totalValue * 100) : 0;
416
+ const icon = getIcon(type, item.key);
417
+ return /* @__PURE__ */ jsxs2(
418
+ "div",
419
+ {
420
+ className: "relative group",
421
+ onMouseEnter: (e) => {
422
+ const rect = e.currentTarget.getBoundingClientRect();
423
+ setTooltip({ key: item.key, value: item.value, pct, x: rect.left + rect.width / 2, y: rect.top });
424
+ },
425
+ onMouseLeave: () => setTooltip(null),
426
+ children: [
427
+ /* @__PURE__ */ jsx4(
428
+ "div",
429
+ {
430
+ className: "absolute inset-0 bg-[rgb(var(--lm-bar))] rounded transition-all group-hover:bg-[rgb(var(--lm-bar-hover))]",
431
+ style: { width: `${item.value / maxValue * 100}%` }
432
+ }
433
+ ),
434
+ /* @__PURE__ */ jsxs2("div", { className: "relative flex items-center justify-between px-2.5 py-1.5 text-sm", children: [
435
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 truncate mr-3", children: [
436
+ icon,
437
+ /* @__PURE__ */ jsx4("span", { className: "truncate text-[rgb(var(--lm-text-secondary))]", children: item.key || "(direct)" })
438
+ ] }),
439
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
440
+ /* @__PURE__ */ jsx4("span", { className: "text-[rgb(var(--lm-text-secondary))] tabular-nums text-xs font-medium", children: item.value.toLocaleString() }),
441
+ /* @__PURE__ */ jsxs2("span", { className: "text-[rgb(var(--lm-text-tertiary))] tabular-nums text-xs w-8 text-right", children: [
442
+ pct,
443
+ "%"
444
+ ] })
445
+ ] })
446
+ ] })
447
+ ]
448
+ },
449
+ item.key
450
+ );
451
+ }) }),
452
+ tooltip && /* @__PURE__ */ jsxs2(
453
+ "div",
454
+ {
455
+ className: "fixed z-50 bg-[rgb(var(--lm-tooltip-bg))] text-[rgb(var(--lm-tooltip-text))] text-xs rounded-lg px-3 py-2 shadow-lg pointer-events-none max-w-xs",
456
+ style: { left: tooltip.x, top: tooltip.y - 8, transform: "translate(-50%, -100%)" },
457
+ children: [
458
+ /* @__PURE__ */ jsx4("p", { className: "font-medium break-all", children: tooltip.key || "(direct)" }),
459
+ /* @__PURE__ */ jsxs2("p", { className: "text-[rgb(var(--lm-tooltip-muted))] mt-0.5", children: [
460
+ tooltip.value.toLocaleString(),
461
+ " \xB7 ",
462
+ tooltip.pct,
463
+ "%"
464
+ ] })
465
+ ]
466
+ }
467
+ )
468
+ ] });
469
+ }
470
+
471
+ // src/components/PieChartCard.tsx
472
+ import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts";
473
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
474
+ function PieChartCard({ title, data, loading, className }) {
475
+ const total = data.reduce((sum, d) => sum + d.value, 0);
476
+ const colors = getPieColors();
477
+ return /* @__PURE__ */ jsxs3("div", { className: `rounded-xl bg-[rgb(var(--lm-bg))] border border-[rgb(var(--lm-border))] p-5 ${className ?? ""}`, children: [
478
+ /* @__PURE__ */ jsx5("h3", { className: "text-xs font-medium text-[rgb(var(--lm-text-tertiary))] uppercase tracking-wide mb-4", children: title }),
479
+ loading ? /* @__PURE__ */ jsx5("div", { className: "h-48 bg-[rgb(var(--lm-bg-secondary))] rounded-lg animate-pulse" }) : data.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "h-48 flex items-center justify-center", children: /* @__PURE__ */ jsx5("p", { className: "text-[rgb(var(--lm-text-muted))] text-sm", children: "No data yet" }) }) : /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-4", children: [
480
+ /* @__PURE__ */ jsx5("div", { className: "w-36 h-36 flex-shrink-0", children: /* @__PURE__ */ jsx5(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs3(PieChart, { children: [
481
+ /* @__PURE__ */ jsx5(
482
+ Pie,
483
+ {
484
+ data,
485
+ cx: "50%",
486
+ cy: "50%",
487
+ innerRadius: 35,
488
+ outerRadius: 55,
489
+ dataKey: "value",
490
+ strokeWidth: 2,
491
+ stroke: cssVar("--lm-bg", "255 255 255"),
492
+ children: data.map((_, index) => /* @__PURE__ */ jsx5(Cell, { fill: colors[index % colors.length] }, index))
493
+ }
494
+ ),
495
+ /* @__PURE__ */ jsx5(
496
+ Tooltip,
497
+ {
498
+ content: ({ active, payload }) => {
499
+ if (!active || !payload?.[0]) return null;
500
+ const item = payload[0];
501
+ const pct = total > 0 ? Math.round(item.value / total * 100) : 0;
502
+ return /* @__PURE__ */ jsxs3("div", { className: "bg-[rgb(var(--lm-tooltip-bg))] text-[rgb(var(--lm-tooltip-text))] text-xs rounded-lg px-3 py-2 shadow-lg", children: [
503
+ /* @__PURE__ */ jsx5("p", { className: "font-medium", children: item.name }),
504
+ /* @__PURE__ */ jsxs3("p", { className: "text-[rgb(var(--lm-tooltip-muted))]", children: [
505
+ item.value.toLocaleString(),
506
+ " (",
507
+ pct,
508
+ "%)"
509
+ ] })
510
+ ] });
511
+ }
512
+ }
513
+ )
514
+ ] }) }) }),
515
+ /* @__PURE__ */ jsx5("div", { className: "flex-1 space-y-1.5 min-w-0", children: data.slice(0, 6).map((item, i) => {
516
+ const pct = total > 0 ? Math.round(item.value / total * 100) : 0;
517
+ return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-sm", children: [
518
+ /* @__PURE__ */ jsx5(
519
+ "span",
520
+ {
521
+ className: "w-2.5 h-2.5 rounded-full flex-shrink-0",
522
+ style: { backgroundColor: colors[i % colors.length] }
523
+ }
524
+ ),
525
+ /* @__PURE__ */ jsx5("span", { className: "text-[rgb(var(--lm-text-secondary))] truncate", children: item.name || "(unknown)" }),
526
+ /* @__PURE__ */ jsxs3("span", { className: "text-[rgb(var(--lm-text-tertiary))] ml-auto flex-shrink-0 tabular-nums text-xs", children: [
527
+ pct,
528
+ "%"
529
+ ] })
530
+ ] }, item.name);
531
+ }) })
532
+ ] })
533
+ ] });
534
+ }
535
+
536
+ // src/components/PeriodSelector.tsx
537
+ import { useState as useState4 } from "react";
538
+
539
+ // src/components/DateRangePicker.tsx
540
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
541
+ function DateRangePicker({ startDate, endDate, onStartChange, onEndChange, className }) {
542
+ return /* @__PURE__ */ jsxs4("div", { className: `flex items-center gap-2 ${className ?? ""}`, children: [
543
+ /* @__PURE__ */ jsx6(
544
+ "input",
545
+ {
546
+ type: "date",
547
+ value: startDate,
548
+ onChange: (e) => onStartChange(e.target.value),
549
+ max: endDate || void 0,
550
+ className: "bg-[rgb(var(--lm-bg))] border border-[rgb(var(--lm-border))] rounded-lg px-2.5 py-1.5 text-sm text-[rgb(var(--lm-text))] focus:outline-none focus:border-[rgb(var(--lm-accent))]"
551
+ }
552
+ ),
553
+ /* @__PURE__ */ jsx6("span", { className: "text-[rgb(var(--lm-text-tertiary))] text-sm", children: "to" }),
554
+ /* @__PURE__ */ jsx6(
555
+ "input",
556
+ {
557
+ type: "date",
558
+ value: endDate,
559
+ onChange: (e) => onEndChange(e.target.value),
560
+ min: startDate || void 0,
561
+ max: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
562
+ className: "bg-[rgb(var(--lm-bg))] border border-[rgb(var(--lm-border))] rounded-lg px-2.5 py-1.5 text-sm text-[rgb(var(--lm-text))] focus:outline-none focus:border-[rgb(var(--lm-accent))]"
563
+ }
564
+ )
565
+ ] });
566
+ }
567
+
568
+ // src/components/PeriodSelector.tsx
569
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
570
+ var periods = [
571
+ { value: "1h", label: "1H" },
572
+ { value: "24h", label: "24H" },
573
+ { value: "7d", label: "7D" },
574
+ { value: "30d", label: "30D" },
575
+ { value: "90d", label: "90D" },
576
+ { value: "custom", label: "Custom" }
577
+ ];
578
+ function PeriodSelector({ value, onChange, dateFrom, dateTo, onDateFromChange, onDateToChange, className }) {
579
+ const [showCustom, setShowCustom] = useState4(value === "custom");
580
+ return /* @__PURE__ */ jsxs5("div", { className: `flex items-center gap-3 ${className ?? ""}`, children: [
581
+ /* @__PURE__ */ jsx7("div", { className: "flex gap-1 bg-[rgb(var(--lm-bg-tertiary))] rounded-lg p-1 border border-[rgb(var(--lm-border))]", children: periods.map((p) => /* @__PURE__ */ jsx7(
582
+ "button",
583
+ {
584
+ onClick: () => {
585
+ onChange(p.value);
586
+ setShowCustom(p.value === "custom");
587
+ },
588
+ className: `px-3 py-1.5 text-sm rounded-md transition-colors ${value === p.value ? "bg-[rgb(var(--lm-bg))] text-[rgb(var(--lm-text))] shadow-sm" : "text-[rgb(var(--lm-text-secondary))] hover:text-[rgb(var(--lm-text))]"}`,
589
+ children: p.label
590
+ },
591
+ p.value
592
+ )) }),
593
+ showCustom && onDateFromChange && onDateToChange && /* @__PURE__ */ jsx7(
594
+ DateRangePicker,
595
+ {
596
+ startDate: dateFrom || "",
597
+ endDate: dateTo || "",
598
+ onStartChange: onDateFromChange,
599
+ onEndChange: onDateToChange
600
+ }
601
+ )
602
+ ] });
603
+ }
604
+
605
+ // src/components/ExportButton.tsx
606
+ import { useState as useState5, useRef, useEffect as useEffect3 } from "react";
607
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
608
+ function ExportButton({ data, filename, className }) {
609
+ const [open, setOpen] = useState5(false);
610
+ const ref = useRef(null);
611
+ useEffect3(() => {
612
+ const handler = (e) => {
613
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
614
+ };
615
+ document.addEventListener("mousedown", handler);
616
+ return () => document.removeEventListener("mousedown", handler);
617
+ }, []);
618
+ if (!data || data.length === 0) return null;
619
+ function download(format) {
620
+ let content;
621
+ let mimeType;
622
+ let ext;
623
+ const headers = Object.keys(data[0]);
624
+ switch (format) {
625
+ case "csv": {
626
+ const rows = [
627
+ headers.join(","),
628
+ ...data.map(
629
+ (row) => headers.map((h) => {
630
+ const val = row[h];
631
+ const str = val === null || val === void 0 ? "" : String(val);
632
+ return str.includes(",") || str.includes('"') || str.includes("\n") ? `"${str.replace(/"/g, '""')}"` : str;
633
+ }).join(",")
634
+ )
635
+ ];
636
+ content = rows.join("\n");
637
+ mimeType = "text/csv";
638
+ ext = "csv";
639
+ break;
640
+ }
641
+ case "json": {
642
+ content = JSON.stringify(data, null, 2);
643
+ mimeType = "application/json";
644
+ ext = "json";
645
+ break;
646
+ }
647
+ case "markdown": {
648
+ const headerRow = "| " + headers.join(" | ") + " |";
649
+ const separator = "| " + headers.map(() => "---").join(" | ") + " |";
650
+ const bodyRows = data.map(
651
+ (row) => "| " + headers.map((h) => String(row[h] ?? "")).join(" | ") + " |"
652
+ );
653
+ content = [headerRow, separator, ...bodyRows].join("\n");
654
+ mimeType = "text/markdown";
655
+ ext = "md";
656
+ break;
657
+ }
658
+ }
659
+ const blob = new Blob([content], { type: mimeType });
660
+ const url = URL.createObjectURL(blob);
661
+ const a = document.createElement("a");
662
+ a.href = url;
663
+ a.download = `${filename}.${ext}`;
664
+ document.body.appendChild(a);
665
+ a.click();
666
+ document.body.removeChild(a);
667
+ URL.revokeObjectURL(url);
668
+ setOpen(false);
669
+ }
670
+ return /* @__PURE__ */ jsxs6("div", { ref, className: `relative ${className ?? ""}`, children: [
671
+ /* @__PURE__ */ jsxs6(
672
+ "button",
673
+ {
674
+ onClick: () => setOpen(!open),
675
+ className: "flex items-center gap-1.5 px-3 py-1.5 text-sm border border-[rgb(var(--lm-border))] rounded-lg hover:border-[rgb(var(--lm-border-hover))] transition-colors text-[rgb(var(--lm-text-secondary))]",
676
+ children: [
677
+ /* @__PURE__ */ jsx8("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx8("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) }),
678
+ "Export"
679
+ ]
680
+ }
681
+ ),
682
+ open && /* @__PURE__ */ jsxs6("div", { className: "absolute top-full mt-1 right-0 w-36 bg-[rgb(var(--lm-bg))] border border-[rgb(var(--lm-border))] rounded-lg shadow-lg z-50 py-1", children: [
683
+ /* @__PURE__ */ jsx8("button", { onClick: () => download("csv"), className: "w-full text-left px-3 py-2 text-sm hover:bg-[rgb(var(--lm-bg-secondary))] text-[rgb(var(--lm-text-secondary))]", children: "CSV" }),
684
+ /* @__PURE__ */ jsx8("button", { onClick: () => download("json"), className: "w-full text-left px-3 py-2 text-sm hover:bg-[rgb(var(--lm-bg-secondary))] text-[rgb(var(--lm-text-secondary))]", children: "JSON" }),
685
+ /* @__PURE__ */ jsx8("button", { onClick: () => download("markdown"), className: "w-full text-left px-3 py-2 text-sm hover:bg-[rgb(var(--lm-bg-secondary))] text-[rgb(var(--lm-text-secondary))]", children: "Markdown" })
686
+ ] })
687
+ ] });
688
+ }
689
+
690
+ // src/widgets/StatCards.tsx
691
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
692
+ function StatCards({ period, className }) {
693
+ const { data: overview, isLoading: loading } = useOverview({ period });
694
+ return /* @__PURE__ */ jsxs7("div", { className: `grid grid-cols-2 md:grid-cols-4 gap-5 ${className ?? ""}`, children: [
695
+ /* @__PURE__ */ jsx9(
696
+ StatCard,
697
+ {
698
+ title: "Pageviews",
699
+ value: overview?.pageviews?.total ?? 0,
700
+ changePercent: overview?.pageviews?.changePercent,
701
+ loading
702
+ }
703
+ ),
704
+ /* @__PURE__ */ jsx9(
705
+ StatCard,
706
+ {
707
+ title: "Visitors",
708
+ value: overview?.visitors?.total ?? 0,
709
+ changePercent: overview?.visitors?.changePercent,
710
+ loading
711
+ }
712
+ ),
713
+ /* @__PURE__ */ jsx9(
714
+ StatCard,
715
+ {
716
+ title: "Sessions",
717
+ value: overview?.sessions?.total ?? 0,
718
+ changePercent: overview?.sessions?.changePercent,
719
+ loading
720
+ }
721
+ ),
722
+ /* @__PURE__ */ jsx9(
723
+ StatCard,
724
+ {
725
+ title: "Events",
726
+ value: overview?.events?.total ?? 0,
727
+ changePercent: overview?.events?.changePercent,
728
+ loading
729
+ }
730
+ )
731
+ ] });
732
+ }
733
+
734
+ // src/widgets/TimeSeriesChart.tsx
735
+ import { useState as useState6 } from "react";
736
+ import { AreaChart, Area, XAxis, YAxis, Tooltip as Tooltip2, ResponsiveContainer as ResponsiveContainer2, CartesianGrid } from "recharts";
737
+
738
+ // src/utils/formatters.ts
739
+ function formatDate(iso, period) {
740
+ const d = new Date(iso);
741
+ if (period === "1h" || period === "24h") {
742
+ return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
743
+ }
744
+ return d.toLocaleDateString([], { month: "short", day: "numeric" });
745
+ }
746
+ function formatTooltipDate(iso, period) {
747
+ const d = new Date(iso);
748
+ if (period === "1h" || period === "24h") {
749
+ return d.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
750
+ }
751
+ return d.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
752
+ }
753
+
754
+ // src/widgets/TimeSeriesChart.tsx
755
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
756
+ var metrics = [
757
+ { value: "pageviews", label: "Pageviews" },
758
+ { value: "visitors", label: "Visitors" },
759
+ { value: "sessions", label: "Sessions" }
760
+ ];
761
+ function TimeSeriesChart({ defaultMetric = "pageviews", period: periodProp, className }) {
762
+ const [metric, setMetric] = useState6(defaultMetric);
763
+ const { period: ctxPeriod } = useLitemetricsUI();
764
+ const period = periodProp ?? ctxPeriod;
765
+ const { get } = useThemeColors();
766
+ const { data: result, isLoading: loading } = useTimeSeries(metric, { period });
767
+ const data = result?.data ?? [];
768
+ const strokeColor = get("--lm-chart-stroke", "99 102 241");
769
+ const fillColor = get("--lm-chart-fill", "99 102 241");
770
+ const gridColor = get("--lm-chart-grid", "244 244 245");
771
+ const axisColor = get("--lm-chart-axis", "161 161 170");
772
+ return /* @__PURE__ */ jsxs8("div", { className: `rounded-xl bg-[rgb(var(--lm-bg))] border border-[rgb(var(--lm-border))] p-6 ${className ?? ""}`, children: [
773
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between mb-4", children: [
774
+ /* @__PURE__ */ jsx10("h3", { className: "text-sm font-medium text-[rgb(var(--lm-text-secondary))]", children: "Overview" }),
775
+ /* @__PURE__ */ jsx10("div", { className: "flex gap-1", children: metrics.map((m) => /* @__PURE__ */ jsx10(
776
+ "button",
777
+ {
778
+ onClick: () => setMetric(m.value),
779
+ className: `px-2.5 py-1 text-xs rounded-md transition-colors ${metric === m.value ? "bg-[rgb(var(--lm-accent-light))] text-[rgb(var(--lm-accent-text))] font-medium" : "text-[rgb(var(--lm-text-tertiary))] hover:text-[rgb(var(--lm-text-secondary))]"}`,
780
+ children: m.label
781
+ },
782
+ m.value
783
+ )) })
784
+ ] }),
785
+ /* @__PURE__ */ jsx10("div", { className: "h-64", children: loading ? /* @__PURE__ */ jsx10("div", { className: "h-full bg-[rgb(var(--lm-bg-secondary))] rounded-lg animate-pulse" }) : data.length === 0 ? /* @__PURE__ */ jsx10("div", { className: "h-full flex items-center justify-center text-[rgb(var(--lm-text-tertiary))] text-sm", children: "No data for this period" }) : /* @__PURE__ */ jsx10(ResponsiveContainer2, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs8(AreaChart, { data, margin: { top: 4, right: 4, bottom: 0, left: -20 }, children: [
786
+ /* @__PURE__ */ jsx10("defs", { children: /* @__PURE__ */ jsxs8("linearGradient", { id: "lm-chartGradient", x1: "0", y1: "0", x2: "0", y2: "1", children: [
787
+ /* @__PURE__ */ jsx10("stop", { offset: "0%", stopColor: fillColor, stopOpacity: 0.2 }),
788
+ /* @__PURE__ */ jsx10("stop", { offset: "100%", stopColor: fillColor, stopOpacity: 0 })
789
+ ] }) }),
790
+ /* @__PURE__ */ jsx10(CartesianGrid, { strokeDasharray: "3 3", stroke: gridColor, vertical: false }),
791
+ /* @__PURE__ */ jsx10(
792
+ XAxis,
793
+ {
794
+ dataKey: "date",
795
+ tickFormatter: (v) => formatDate(v, period),
796
+ tick: { fontSize: 11, fill: axisColor },
797
+ tickLine: false,
798
+ axisLine: { stroke: gridColor },
799
+ interval: "preserveStartEnd",
800
+ minTickGap: 40
801
+ }
802
+ ),
803
+ /* @__PURE__ */ jsx10(
804
+ YAxis,
805
+ {
806
+ tick: { fontSize: 11, fill: axisColor },
807
+ tickLine: false,
808
+ axisLine: false,
809
+ allowDecimals: false
810
+ }
811
+ ),
812
+ /* @__PURE__ */ jsx10(
813
+ Tooltip2,
814
+ {
815
+ content: ({ active, payload }) => {
816
+ if (!active || !payload?.[0]) return null;
817
+ const point = payload[0].payload;
818
+ return /* @__PURE__ */ jsxs8("div", { className: "bg-[rgb(var(--lm-tooltip-bg))] text-[rgb(var(--lm-tooltip-text))] text-xs rounded-lg px-3 py-2 shadow-lg", children: [
819
+ /* @__PURE__ */ jsx10("p", { className: "text-[rgb(var(--lm-tooltip-muted))] mb-0.5", children: formatTooltipDate(point.date, period) }),
820
+ /* @__PURE__ */ jsxs8("p", { className: "font-medium", children: [
821
+ point.value.toLocaleString(),
822
+ " ",
823
+ metric
824
+ ] })
825
+ ] });
826
+ }
827
+ }
828
+ ),
829
+ /* @__PURE__ */ jsx10(
830
+ Area,
831
+ {
832
+ type: "monotone",
833
+ dataKey: "value",
834
+ stroke: strokeColor,
835
+ strokeWidth: 2,
836
+ fill: "url(#lm-chartGradient)"
837
+ }
838
+ )
839
+ ] }) }) })
840
+ ] });
841
+ }
842
+
843
+ // src/widgets/TopPages.tsx
844
+ import { jsx as jsx11 } from "react/jsx-runtime";
845
+ function TopPages({ period, limit = 10, className }) {
846
+ const { data, isLoading } = useStats("top_pages", { period, limit });
847
+ return /* @__PURE__ */ jsx11(TopList, { title: "Pages", type: "pages", data: data?.data ?? null, loading: isLoading, className });
848
+ }
849
+
850
+ // src/widgets/TopReferrers.tsx
851
+ import { jsx as jsx12 } from "react/jsx-runtime";
852
+ function TopReferrers({ period, limit = 10, className }) {
853
+ const { data, isLoading } = useStats("top_referrers", { period, limit });
854
+ return /* @__PURE__ */ jsx12(TopList, { title: "Referrers", type: "referrers", data: data?.data ?? null, loading: isLoading, className });
855
+ }
856
+
857
+ // src/widgets/TopCountries.tsx
858
+ import { jsx as jsx13 } from "react/jsx-runtime";
859
+ function TopCountries({ period, limit = 10, className }) {
860
+ const { data, isLoading } = useStats("top_countries", { period, limit });
861
+ return /* @__PURE__ */ jsx13(TopList, { title: "Countries", type: "countries", data: data?.data ?? null, loading: isLoading, className });
862
+ }
863
+
864
+ // src/widgets/TopEvents.tsx
865
+ import { jsx as jsx14 } from "react/jsx-runtime";
866
+ function TopEvents({ period, limit = 10, className }) {
867
+ const { data, isLoading } = useStats("top_events", { period, limit });
868
+ return /* @__PURE__ */ jsx14(TopList, { title: "Events", type: "events", data: data?.data ?? null, loading: isLoading, className });
869
+ }
870
+
871
+ // src/widgets/TopBrowsers.tsx
872
+ import { jsx as jsx15 } from "react/jsx-runtime";
873
+ function TopBrowsers({ period, limit = 10, className }) {
874
+ const { data, isLoading } = useStats("top_browsers", { period, limit });
875
+ return /* @__PURE__ */ jsx15(TopList, { title: "Browsers", type: "browsers", data: data?.data ?? null, loading: isLoading, className });
876
+ }
877
+
878
+ // src/widgets/TopDevices.tsx
879
+ import { jsx as jsx16 } from "react/jsx-runtime";
880
+ function TopDevices({ period, limit = 10, className }) {
881
+ const { data, isLoading } = useStats("top_devices", { period, limit });
882
+ return /* @__PURE__ */ jsx16(TopList, { title: "Devices", type: "devices", data: data?.data ?? null, loading: isLoading, className });
883
+ }
884
+
885
+ // src/widgets/BrowsersChart.tsx
886
+ import { jsx as jsx17 } from "react/jsx-runtime";
887
+ function BrowsersChart({ period, className }) {
888
+ const { data, isLoading } = useStats("top_browsers", { period });
889
+ const pieData = (data?.data ?? []).map((d) => ({ name: d.key, value: d.value }));
890
+ return /* @__PURE__ */ jsx17(PieChartCard, { title: "Browsers", data: pieData, loading: isLoading, className });
891
+ }
892
+
893
+ // src/widgets/DevicesChart.tsx
894
+ import { jsx as jsx18 } from "react/jsx-runtime";
895
+ function DevicesChart({ period, className }) {
896
+ const { data, isLoading } = useStats("top_devices", { period });
897
+ const pieData = (data?.data ?? []).map((d) => ({ name: d.key, value: d.value }));
898
+ return /* @__PURE__ */ jsx18(PieChartCard, { title: "Devices", data: pieData, loading: isLoading, className });
899
+ }
900
+
901
+ // src/widgets/WorldMap.tsx
902
+ import { useState as useState7, memo, lazy, Suspense } from "react";
903
+ import { jsx as jsx19, jsxs as jsxs9 } from "react/jsx-runtime";
904
+ var geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json";
905
+ var WorldMapInner = lazy(() => import("./WorldMapInner-AJ3WDZR2.js"));
906
+ var WorldMap = memo(function WorldMap2({ period, className }) {
907
+ const [tooltip, setTooltip] = useState7(null);
908
+ const { get } = useThemeColors();
909
+ const { data: statsResult, isLoading: loading } = useStats("top_countries", { period, limit: 200 });
910
+ const countryData = {};
911
+ if (statsResult?.data) {
912
+ for (const d of statsResult.data) {
913
+ countryData[d.key] = d.value;
914
+ }
915
+ }
916
+ const maxValue = Math.max(...Object.values(countryData), 1);
917
+ function parseRGB(triplet) {
918
+ const parts = triplet.trim().split(/\s+/).map(Number);
919
+ return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
920
+ }
921
+ function getColor(iso) {
922
+ const value = countryData[iso] || 0;
923
+ const emptyColor = get("--lm-map-empty", "244 244 245");
924
+ if (value === 0) return emptyColor;
925
+ const intensity = Math.min(value / maxValue, 1);
926
+ const emptyRaw = getComputedStyle(document.documentElement).getPropertyValue("--lm-map-empty").trim() || "244 244 245";
927
+ const accentRaw = getComputedStyle(document.documentElement).getPropertyValue("--lm-accent").trim() || "99 102 241";
928
+ const [er, eg, eb] = parseRGB(emptyRaw);
929
+ const [ar, ag, ab] = parseRGB(accentRaw);
930
+ const r = Math.round(er + intensity * (ar - er));
931
+ const g = Math.round(eg + intensity * (ag - eg));
932
+ const b = Math.round(eb + intensity * (ab - eb));
933
+ return `rgb(${r}, ${g}, ${b})`;
934
+ }
935
+ const strokeColor = get("--lm-map-stroke", "228 228 231");
936
+ const hoverColor = get("--lm-map-hover", "129 140 248");
937
+ return /* @__PURE__ */ jsxs9("div", { className: `rounded-xl bg-[rgb(var(--lm-bg))] border border-[rgb(var(--lm-border))] p-5 ${className ?? ""}`, children: [
938
+ /* @__PURE__ */ jsx19("h3", { className: "text-xs font-medium text-[rgb(var(--lm-text-tertiary))] uppercase tracking-wide mb-3", children: "Visitors by Country" }),
939
+ loading ? /* @__PURE__ */ jsx19("div", { className: "h-64 bg-[rgb(var(--lm-bg-secondary))] rounded-lg animate-pulse" }) : /* @__PURE__ */ jsx19(Suspense, { fallback: /* @__PURE__ */ jsx19("div", { className: "h-64 bg-[rgb(var(--lm-bg-secondary))] rounded-lg flex items-center justify-center text-[rgb(var(--lm-text-tertiary))] text-sm", children: "Loading map..." }), children: /* @__PURE__ */ jsx19(
940
+ WorldMapInner,
941
+ {
942
+ geoUrl,
943
+ countryData,
944
+ getColor,
945
+ setTooltip,
946
+ strokeColor,
947
+ hoverColor
948
+ }
949
+ ) }),
950
+ tooltip && /* @__PURE__ */ jsxs9(
951
+ "div",
952
+ {
953
+ className: "fixed z-50 bg-[rgb(var(--lm-tooltip-bg))] text-[rgb(var(--lm-tooltip-text))] text-xs rounded-lg px-3 py-2 shadow-lg pointer-events-none",
954
+ style: { left: tooltip.x + 10, top: tooltip.y - 30 },
955
+ children: [
956
+ /* @__PURE__ */ jsx19("p", { className: "font-medium", children: tooltip.name }),
957
+ /* @__PURE__ */ jsxs9("p", { className: "text-[rgb(var(--lm-tooltip-muted))]", children: [
958
+ tooltip.value.toLocaleString(),
959
+ " visitor",
960
+ tooltip.value !== 1 ? "s" : ""
961
+ ] })
962
+ ]
963
+ }
964
+ )
965
+ ] });
966
+ });
967
+
968
+ // src/widgets/AnalyticsDashboard.tsx
969
+ import { jsx as jsx20, jsxs as jsxs10 } from "react/jsx-runtime";
970
+ function AnalyticsDashboard({
971
+ showPeriodSelector = true,
972
+ showExport = true,
973
+ showWorldMap = true,
974
+ showPieCharts = true,
975
+ period,
976
+ className
977
+ }) {
978
+ const { period: ctxPeriod, setPeriod, dateFrom, setDateFrom, dateTo, setDateTo } = useLitemetricsUI();
979
+ const effectivePeriod = period ?? ctxPeriod;
980
+ return /* @__PURE__ */ jsxs10("div", { className, children: [
981
+ (showPeriodSelector || showExport) && /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between mb-6 flex-wrap gap-3", children: [
982
+ showPeriodSelector && /* @__PURE__ */ jsx20(
983
+ PeriodSelector,
984
+ {
985
+ value: effectivePeriod,
986
+ onChange: setPeriod,
987
+ dateFrom,
988
+ dateTo,
989
+ onDateFromChange: setDateFrom,
990
+ onDateToChange: setDateTo
991
+ }
992
+ ),
993
+ showExport && /* @__PURE__ */ jsx20(ExportButton, { data: [], filename: `analytics-${effectivePeriod}` })
994
+ ] }),
995
+ /* @__PURE__ */ jsx20(TimeSeriesChart, { period: effectivePeriod, className: "mb-6" }),
996
+ /* @__PURE__ */ jsx20(StatCards, { period: effectivePeriod, className: "mb-6" }),
997
+ showWorldMap && /* @__PURE__ */ jsx20(WorldMap, { period: effectivePeriod, className: "mb-6" }),
998
+ showPieCharts && /* @__PURE__ */ jsxs10("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-5 mb-6", children: [
999
+ /* @__PURE__ */ jsx20(BrowsersChart, { period: effectivePeriod }),
1000
+ /* @__PURE__ */ jsx20(DevicesChart, { period: effectivePeriod })
1001
+ ] }),
1002
+ /* @__PURE__ */ jsxs10("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-5", children: [
1003
+ /* @__PURE__ */ jsx20(TopPages, { period: effectivePeriod }),
1004
+ /* @__PURE__ */ jsx20(TopReferrers, { period: effectivePeriod }),
1005
+ /* @__PURE__ */ jsx20(TopCountries, { period: effectivePeriod }),
1006
+ /* @__PURE__ */ jsx20(TopEvents, { period: effectivePeriod }),
1007
+ /* @__PURE__ */ jsx20(TopBrowsers, { period: effectivePeriod }),
1008
+ /* @__PURE__ */ jsx20(TopDevices, { period: effectivePeriod })
1009
+ ] })
1010
+ ] });
1011
+ }
1012
+ export {
1013
+ AnalyticsDashboard,
1014
+ BrowsersChart,
1015
+ DateRangePicker,
1016
+ DevicesChart,
1017
+ ExportButton,
1018
+ LitemetricsProvider,
1019
+ PeriodSelector,
1020
+ PieChartCard,
1021
+ StatCard,
1022
+ StatCards,
1023
+ TimeSeriesChart,
1024
+ TopBrowsers,
1025
+ TopCountries,
1026
+ TopDevices,
1027
+ TopEvents,
1028
+ TopList,
1029
+ TopPages,
1030
+ TopReferrers,
1031
+ WorldMap,
1032
+ countryToFlag,
1033
+ cssVar,
1034
+ darkTheme,
1035
+ defaultTheme,
1036
+ formatDate,
1037
+ formatTooltipDate,
1038
+ getBrowserIcon,
1039
+ getDeviceIcon,
1040
+ getOSIcon,
1041
+ getPieColors,
1042
+ getReferrerIcon,
1043
+ queryKeys,
1044
+ useLitemetricsUI,
1045
+ useOverview,
1046
+ useStats,
1047
+ useThemeColors,
1048
+ useTimeSeries
1049
+ };
1050
+ //# sourceMappingURL=index.js.map