@analytix/dashboard 0.2.2 → 0.2.4

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.
@@ -13,6 +13,10 @@ export interface AnalyticsDashboardProps {
13
13
  defaultWidgets?: DashboardWidgetId[];
14
14
  /** PATCH endpoint to persist widget layout as site default. */
15
15
  settingsEndpoint?: string;
16
+ /** Called after widget layout is saved as site default. */
17
+ onWidgetsSaved?: () => void;
18
+ /** Called when saving widget layout fails. */
19
+ onWidgetsSaveError?: (message: string) => void;
16
20
  }
17
- export declare function AnalyticsDashboard({ siteId, summaryEndpoint, exportEndpoint, loadingFallback, defaultTheme, defaultWidgets, settingsEndpoint, }: AnalyticsDashboardProps): import("react").JSX.Element;
21
+ export declare function AnalyticsDashboard({ siteId, summaryEndpoint, exportEndpoint, loadingFallback, defaultTheme, defaultWidgets, settingsEndpoint, onWidgetsSaved, onWidgetsSaveError, }: AnalyticsDashboardProps): import("react").JSX.Element;
18
22
  export {};
@@ -3,9 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { DEFAULT_DASHBOARD_WIDGETS, percentChange } from "@analytix/core";
5
5
  import { Area, CartesianGrid, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts";
6
- import { Download, Eye, Users, Clock, MousePointerClick, Repeat, FileText, UserPlus, UserCheck, Moon, Sun, SlidersHorizontal, } from "lucide-react";
6
+ import { Download, Eye, Users, Clock, MousePointerClick, Repeat, FileText, UserPlus, UserCheck, Sun, Moon, Monitor, SlidersHorizontal, } from "lucide-react";
7
7
  import { AnalyticsDashboardSkeleton } from "./AnalyticsDashboardSkeleton";
8
8
  import { useDashboardWidgets } from "./useDashboardWidgets";
9
+ import { DASHBOARD_THEME_LABELS, useDashboardTheme } from "./useDashboardTheme";
9
10
  import { WidgetCustomizePanel } from "./WidgetCustomizePanel";
10
11
  function formatBucketLabel(value, granularity) {
11
12
  const date = new Date(value);
@@ -32,22 +33,7 @@ function Delta({ current, previous }) {
32
33
  function BreakdownPanel({ title, emptyMessage, rows, }) {
33
34
  return (_jsxs("div", { className: "chartPanel", children: [_jsx("div", { className: "panelTitle", children: title }), rows.length === 0 ? (_jsx("p", { className: "emptyState", children: emptyMessage })) : (_jsx("ul", { className: "list", children: rows.map((row) => (_jsxs("li", { children: [_jsx("span", { className: "listLabel", children: row.label }), _jsx("span", { className: "listValue", children: row.value })] }, row.key))) }))] }));
34
35
  }
35
- function useResolvedTheme(mode) {
36
- const [resolved, setResolved] = useState("light");
37
- useEffect(() => {
38
- if (mode !== "system") {
39
- setResolved(mode);
40
- return;
41
- }
42
- const media = window.matchMedia("(prefers-color-scheme: dark)");
43
- const apply = () => setResolved(media.matches ? "dark" : "light");
44
- apply();
45
- media.addEventListener("change", apply);
46
- return () => media.removeEventListener("change", apply);
47
- }, [mode]);
48
- return resolved;
49
- }
50
- export function AnalyticsDashboard({ siteId, summaryEndpoint, exportEndpoint, loadingFallback, defaultTheme = "system", defaultWidgets = DEFAULT_DASHBOARD_WIDGETS, settingsEndpoint, }) {
36
+ export function AnalyticsDashboard({ siteId, summaryEndpoint, exportEndpoint, loadingFallback, defaultTheme = "system", defaultWidgets = DEFAULT_DASHBOARD_WIDGETS, settingsEndpoint, onWidgetsSaved, onWidgetsSaveError, }) {
51
37
  const summaryUrl = summaryEndpoint ?? `/api/v1/sites/${siteId}/summary`;
52
38
  const exportUrl = exportEndpoint ?? `/api/v1/sites/${siteId}/export`;
53
39
  const [range, setRange] = useState("7d");
@@ -63,11 +49,11 @@ export function AnalyticsDashboard({ siteId, summaryEndpoint, exportEndpoint, lo
63
49
  const [initialLoading, setInitialLoading] = useState(true);
64
50
  const [refreshing, setRefreshing] = useState(false);
65
51
  const [error, setError] = useState("");
66
- const [themeMode, setThemeMode] = useState(defaultTheme);
67
- const resolvedTheme = useResolvedTheme(themeMode);
52
+ const { themeMode, cycleTheme, resolvedTheme } = useDashboardTheme(siteId, defaultTheme);
68
53
  const hasLoadedRef = useRef(false);
69
54
  const [showWidgetCustomize, setShowWidgetCustomize] = useState(false);
70
55
  const [savingDefaultWidgets, setSavingDefaultWidgets] = useState(false);
56
+ const [retryNonce, setRetryNonce] = useState(0);
71
57
  const { widgets, toggleWidget, resetToDefault, isVisible } = useDashboardWidgets(siteId, defaultWidgets);
72
58
  const filterParams = useMemo(() => {
73
59
  const params = new URLSearchParams({
@@ -119,7 +105,7 @@ export function AnalyticsDashboard({ siteId, summaryEndpoint, exportEndpoint, lo
119
105
  setRefreshing(true);
120
106
  }
121
107
  setError("");
122
- fetch(`${summaryUrl}?${filterParams.toString()}`)
108
+ fetch(`${summaryUrl}?${filterParams.toString()}`, { credentials: "same-origin" })
123
109
  .then(async (res) => {
124
110
  if (!res.ok) {
125
111
  const payload = await res.json().catch(() => ({}));
@@ -146,7 +132,7 @@ export function AnalyticsDashboard({ siteId, summaryEndpoint, exportEndpoint, lo
146
132
  return () => {
147
133
  cancelled = true;
148
134
  };
149
- }, [summaryUrl, filterParams]);
135
+ }, [summaryUrl, filterParams, retryNonce]);
150
136
  const chartData = useMemo(() => (summary?.buckets ?? []).map((bucket) => ({
151
137
  label: formatBucketLabel(bucket.bucket_start, granularity),
152
138
  views: bucket.page_views,
@@ -157,7 +143,10 @@ export function AnalyticsDashboard({ siteId, summaryEndpoint, exportEndpoint, lo
157
143
  return (_jsx("div", { className: `analytix-dash ${themeClass}`, children: loadingFallback ?? _jsx(AnalyticsDashboardSkeleton, {}) }));
158
144
  }
159
145
  if (error) {
160
- return (_jsx("div", { className: `analytix-dash ${themeClass}`, children: _jsx("p", { className: "error", children: error }) }));
146
+ return (_jsxs("div", { className: `analytix-dash ${themeClass}`, children: [_jsx("p", { className: "error", children: error }), _jsx("button", { type: "button", className: "btnSecondary", style: { marginTop: 12 }, onClick: () => {
147
+ setError("");
148
+ setRetryNonce((n) => n + 1);
149
+ }, children: "Retry" })] }));
161
150
  }
162
151
  if (!summary) {
163
152
  return (_jsx("div", { className: `analytix-dash ${themeClass}`, children: _jsx("p", { className: "emptyState", children: "No analytics data yet." }) }));
@@ -172,27 +161,35 @@ export function AnalyticsDashboard({ siteId, summaryEndpoint, exportEndpoint, lo
172
161
  return;
173
162
  setSavingDefaultWidgets(true);
174
163
  try {
175
- await fetch(settingsEndpoint, {
164
+ const res = await fetch(settingsEndpoint, {
176
165
  method: "PATCH",
177
166
  headers: { "Content-Type": "application/json" },
178
167
  body: JSON.stringify({ analytics_config: { dashboard_widgets: widgets } }),
179
168
  });
169
+ if (!res.ok) {
170
+ const payload = await res.json().catch(() => ({}));
171
+ const message = typeof payload.error === "string" ? payload.error : "Failed to save widget layout";
172
+ onWidgetsSaveError?.(message);
173
+ return;
174
+ }
175
+ onWidgetsSaved?.();
176
+ }
177
+ catch {
178
+ onWidgetsSaveError?.("Failed to save widget layout");
180
179
  }
181
180
  finally {
182
181
  setSavingDefaultWidgets(false);
183
182
  }
184
183
  }
185
- return (_jsxs("div", { className: `analytix-dash ${themeClass}${refreshing ? " analytix-refreshing" : ""}`, children: [_jsxs("div", { className: "toolbar", children: [_jsxs("span", { className: "realtime", children: [summary.realtime_visitors, " visitors in the last 15 minutes"] }), _jsxs("div", { className: "toolbarActions", children: [_jsx("button", { type: "button", className: "btn btnIcon", onClick: () => setShowWidgetCustomize((open) => !open), "aria-label": "Customize widgets", children: _jsx(SlidersHorizontal, { size: 16 }) }), _jsx("button", { type: "button", className: "btn btnIcon", onClick: () => setThemeMode((current) => current === "dark" || (current === "system" && resolvedTheme === "dark")
186
- ? "light"
187
- : "dark"), "aria-label": "Toggle theme", children: resolvedTheme === "dark" ? _jsx(Sun, { size: 16 }) : _jsx(Moon, { size: 16 }) }), _jsxs("a", { className: "btn", href: `${exportUrl}?${filterParams.toString()}`, children: [_jsx(Download, { size: 14 }), "Export CSV"] })] })] }), showWidgetCustomize ? (_jsx(WidgetCustomizePanel, { widgets: widgets, onToggle: toggleWidget, onReset: resetToDefault, onSaveDefault: settingsEndpoint ? saveDefaultWidgets : undefined, savingDefault: savingDefaultWidgets })) : null, _jsxs("div", { className: "filters", children: [_jsxs("div", { className: "filterGroup filterGroupWide", children: [_jsx("span", { className: "filterLabel", children: "Range" }), _jsxs("div", { className: "chips", children: [["24h", "7d", "30d", "90d"].map((key) => (_jsx("button", { type: "button", className: !useCustomRange && range === key ? "chipActive" : "chip", onClick: () => {
184
+ return (_jsxs("div", { className: `analytix-dash ${themeClass}${refreshing ? " analytix-refreshing" : ""}`, children: [_jsxs("div", { className: "toolbar", children: [_jsxs("span", { className: "realtime", children: [summary.realtime_visitors, " visitors in the last 15 minutes"] }), _jsxs("div", { className: "toolbarActions", children: [_jsx("button", { type: "button", className: "btn btnIcon", onClick: () => setShowWidgetCustomize((open) => !open), "aria-label": "Customize widgets", children: _jsx(SlidersHorizontal, { size: 16 }) }), _jsx("button", { type: "button", className: "btn btnIcon", onClick: cycleTheme, "aria-label": `Theme: ${DASHBOARD_THEME_LABELS[themeMode]}. Click to change.`, title: `Theme: ${DASHBOARD_THEME_LABELS[themeMode]}`, children: themeMode === "system" ? (_jsx(Monitor, { size: 16 })) : resolvedTheme === "dark" ? (_jsx(Sun, { size: 16 })) : (_jsx(Moon, { size: 16 })) }), _jsxs("a", { className: "btn", href: `${exportUrl}?${filterParams.toString()}`, children: [_jsx(Download, { size: 14 }), "Export CSV"] })] })] }), showWidgetCustomize ? (_jsx(WidgetCustomizePanel, { widgets: widgets, onToggle: toggleWidget, onReset: resetToDefault, onSaveDefault: settingsEndpoint ? saveDefaultWidgets : undefined, savingDefault: savingDefaultWidgets })) : null, _jsxs("div", { className: "filters", children: [_jsxs("div", { className: "filterGroup filterGroupWide", children: [_jsx("span", { className: "filterLabel", children: "Range" }), _jsxs("div", { className: "chips", children: [["24h", "7d", "30d", "90d"].map((key) => (_jsx("button", { type: "button", className: !useCustomRange && range === key ? "chipActive" : "chip", onClick: () => {
188
185
  setUseCustomRange(false);
189
186
  setRange(key);
190
- }, children: key }, key))), _jsx("button", { type: "button", className: useCustomRange ? "chipActive" : "chip", onClick: () => setUseCustomRange(true), children: "Custom" })] }), useCustomRange && (_jsxs("div", { className: "dateRangeRow", children: [_jsxs("label", { children: [_jsx("span", { className: "filterLabel", children: "From" }), _jsx("input", { type: "date", value: customFrom, max: customTo, onChange: (e) => setCustomFrom(e.target.value) })] }), _jsxs("label", { children: [_jsx("span", { className: "filterLabel", children: "To" }), _jsx("input", { type: "date", value: customTo, min: customFrom, max: toDateInputValue(new Date()), onChange: (e) => setCustomTo(e.target.value) })] })] }))] }), _jsxs("div", { className: "filterGroup", children: [_jsx("span", { className: "filterLabel", children: "Granularity" }), _jsx("div", { className: "chips", children: ["hour", "day"].map((key) => (_jsx("button", { type: "button", className: granularity === key ? "chipActive" : "chip", onClick: () => setGranularity(key), children: key }, key))) })] }), _jsxs("div", { className: "filterGroup", children: [_jsx("span", { className: "filterLabel", children: "Scope" }), _jsx("div", { className: "chips", children: ["all", "page", "blog"].map((key) => (_jsx("button", { type: "button", className: trafficScope === key ? "chipActive" : "chip", onClick: () => setTrafficScope(key), children: key }, key))) })] }), trafficScope === "page" && (_jsxs("div", { className: "filterGroup filterGroupWide textField", children: [_jsx("span", { className: "filterLabel", children: "Path" }), _jsx("input", { value: path, onChange: (e) => setPath(e.target.value), placeholder: "/pricing" }), path.trim() === "/blog" && (_jsxs("label", { className: "checkboxRow", children: [_jsx("input", { type: "checkbox", checked: includeBlogArticles, onChange: (e) => setIncludeBlogArticles(e.target.checked) }), "Include blog articles"] }))] })), trafficScope === "blog" && (_jsxs("div", { className: "filterGroup filterGroupWide textField", children: [_jsx("span", { className: "filterLabel", children: "Content ID (optional)" }), _jsx("input", { value: contentId, onChange: (e) => setContentId(e.target.value), placeholder: "Filter to a single article" })] }))] }), isVisible("metrics") ? (_jsxs("div", { className: "metrics", children: [_jsxs("div", { className: "metricCard", children: [_jsx(Eye, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.total_page_views) }), _jsx("span", { className: "metricLabel", children: "Page views" }), _jsx(Delta, { current: summary.total_page_views, previous: prev?.total_page_views })] }), _jsxs("div", { className: "metricCard", children: [_jsx(Users, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.unique_visitors) }), _jsx("span", { className: "metricLabel", children: "Unique visitors" }), _jsx(Delta, { current: summary.unique_visitors, previous: prev?.unique_visitors })] }), _jsxs("div", { className: "metricCard", children: [_jsx(Repeat, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.total_sessions) }), _jsx("span", { className: "metricLabel", children: "Sessions" }), _jsx(Delta, { current: summary.total_sessions, previous: prev?.total_sessions })] }), _jsxs("div", { className: "metricCard", children: [_jsx(FileText, { size: 18 }), _jsx("span", { className: "metricValue", children: summary.pages_per_session.toFixed(2) }), _jsx("span", { className: "metricLabel", children: "Pages / session" })] }), _jsxs("div", { className: "metricCard", children: [_jsx(UserPlus, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.new_visitors) }), _jsx("span", { className: "metricLabel", children: "New visitors" })] }), _jsxs("div", { className: "metricCard", children: [_jsx(UserCheck, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.returning_visitors) }), _jsx("span", { className: "metricLabel", children: "Returning visitors" })] }), _jsxs("div", { className: "metricCard", children: [_jsx(MousePointerClick, { size: 18 }), _jsxs("span", { className: "metricValue", children: [summary.bounce_rate, "%"] }), _jsx("span", { className: "metricLabel", children: "Bounce rate" }), _jsx(Delta, { current: summary.bounce_rate, previous: prev?.bounce_rate })] }), _jsxs("div", { className: "metricCard", children: [_jsx(Clock, { size: 18 }), _jsxs("span", { className: "metricValue", children: [summary.avg_engagement_seconds, "s"] }), _jsx("span", { className: "metricLabel", children: "Avg engagement" }), _jsx(Delta, { current: summary.avg_engagement_seconds, previous: prev?.avg_engagement_seconds })] })] })) : null, isVisible("chart") ? (_jsxs("div", { className: "chartPanel", children: [_jsx("div", { className: "panelTitle", children: "Traffic over time" }), _jsx("div", { className: "chartContainer", children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(ComposedChart, { data: chartData, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), _jsx(XAxis, { dataKey: "label", tick: { fontSize: 12, fill: "var(--ax-muted)" } }), _jsx(YAxis, { tick: { fontSize: 12, fill: "var(--ax-muted)" } }), _jsx(Tooltip, { contentStyle: {
187
+ }, children: key }, key))), _jsx("button", { type: "button", className: useCustomRange ? "chipActive" : "chip", onClick: () => setUseCustomRange(true), children: "Custom" })] }), useCustomRange && (_jsxs("div", { className: "dateRangeRow", children: [_jsxs("label", { children: [_jsx("span", { className: "filterLabel", children: "From" }), _jsx("input", { type: "date", value: customFrom, max: customTo, onChange: (e) => setCustomFrom(e.target.value) })] }), _jsxs("label", { children: [_jsx("span", { className: "filterLabel", children: "To" }), _jsx("input", { type: "date", value: customTo, min: customFrom, max: toDateInputValue(new Date()), onChange: (e) => setCustomTo(e.target.value) })] })] }))] }), _jsxs("div", { className: "filterGroup", children: [_jsx("span", { className: "filterLabel", children: "Granularity" }), _jsx("div", { className: "chips", children: ["hour", "day"].map((key) => (_jsx("button", { type: "button", className: granularity === key ? "chipActive" : "chip", onClick: () => setGranularity(key), children: key }, key))) })] }), _jsxs("div", { className: "filterGroup", children: [_jsx("span", { className: "filterLabel", children: "Scope" }), _jsx("div", { className: "chips", children: ["all", "page", "blog"].map((key) => (_jsx("button", { type: "button", className: trafficScope === key ? "chipActive" : "chip", onClick: () => setTrafficScope(key), children: key }, key))) })] }), trafficScope === "page" && (_jsxs("div", { className: "filterGroup filterGroupWide textField", children: [_jsx("span", { className: "filterLabel", children: "Path" }), _jsx("input", { value: path, onChange: (e) => setPath(e.target.value), placeholder: "/pricing" }), path.trim() === "/blog" && (_jsxs("label", { className: "checkboxRow", children: [_jsx("input", { type: "checkbox", checked: includeBlogArticles, onChange: (e) => setIncludeBlogArticles(e.target.checked) }), "Include blog articles"] }))] })), trafficScope === "blog" && (_jsxs("div", { className: "filterGroup filterGroupWide textField", children: [_jsx("span", { className: "filterLabel", children: "Content ID (optional)" }), _jsx("input", { value: contentId, onChange: (e) => setContentId(e.target.value), placeholder: "Filter to a single article" })] }))] }), isVisible("metrics") ? (_jsxs("div", { className: "metrics", children: [_jsxs("div", { className: "metricCard", children: [_jsx(Eye, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.total_page_views) }), _jsx("span", { className: "metricLabel", children: "Page views" }), _jsx(Delta, { current: summary.total_page_views, previous: prev?.total_page_views })] }), _jsxs("div", { className: "metricCard", children: [_jsx(Users, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.unique_visitors) }), _jsx("span", { className: "metricLabel", children: "Unique visitors" }), _jsx(Delta, { current: summary.unique_visitors, previous: prev?.unique_visitors })] }), _jsxs("div", { className: "metricCard", children: [_jsx(Repeat, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.total_sessions) }), _jsx("span", { className: "metricLabel", children: "Sessions" }), _jsx(Delta, { current: summary.total_sessions, previous: prev?.total_sessions })] }), _jsxs("div", { className: "metricCard", children: [_jsx(FileText, { size: 18 }), _jsx("span", { className: "metricValue", children: summary.pages_per_session.toFixed(2) }), _jsx("span", { className: "metricLabel", children: "Pages / session" })] }), _jsxs("div", { className: "metricCard", children: [_jsx(UserPlus, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.new_visitors) }), _jsx("span", { className: "metricLabel", children: "New visitors" })] }), _jsxs("div", { className: "metricCard", children: [_jsx(UserCheck, { size: 18 }), _jsx("span", { className: "metricValue", children: formatNumber(summary.returning_visitors) }), _jsx("span", { className: "metricLabel", children: "Returning visitors" })] }), _jsxs("div", { className: "metricCard", children: [_jsx(MousePointerClick, { size: 18 }), _jsxs("span", { className: "metricValue", children: [summary.bounce_rate, "%"] }), _jsx("span", { className: "metricLabel", children: "Bounce rate" }), _jsx(Delta, { current: summary.bounce_rate, previous: prev?.bounce_rate })] }), _jsxs("div", { className: "metricCard", children: [_jsx(Clock, { size: 18 }), _jsxs("span", { className: "metricValue", children: [summary.avg_engagement_seconds, "s"] }), _jsx("span", { className: "metricLabel", children: "Avg engagement" }), _jsx(Delta, { current: summary.avg_engagement_seconds, previous: prev?.avg_engagement_seconds })] })] })) : null, isVisible("chart") ? (_jsxs("div", { className: "chartPanel", children: [_jsx("div", { className: "panelTitle", children: "Traffic over time" }), _jsx("div", { className: "chartContainer", children: chartData.length === 0 ? (_jsx("p", { className: "emptyState", children: "No traffic recorded for this period." })) : (_jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(ComposedChart, { data: chartData, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), _jsx(XAxis, { dataKey: "label", tick: { fontSize: 12, fill: "var(--ax-muted)" } }), _jsx(YAxis, { tick: { fontSize: 12, fill: "var(--ax-muted)" } }), _jsx(Tooltip, { contentStyle: {
191
188
  background: "var(--ax-surface)",
192
189
  border: "1px solid var(--ax-border)",
193
190
  borderRadius: 10,
194
191
  color: "var(--ax-primary)",
195
- } }), _jsx(Area, { type: "monotone", dataKey: "views", fill: areaFill, stroke: areaStroke, name: "Page views" }), _jsx(Line, { type: "monotone", dataKey: "uniques", stroke: lineStroke, name: "Uniques" })] }) }) })] })) : null, isVisible("top_paths") || isVisible("top_content") ? (_jsxs("div", { className: "splitPanels", children: [isVisible("top_paths") ? (_jsx(BreakdownPanel, { title: "Top paths", emptyMessage: "No path data for this period.", rows: summary.top_paths.map((row) => ({
192
+ } }), _jsx(Area, { type: "monotone", dataKey: "views", fill: areaFill, stroke: areaStroke, name: "Page views" }), _jsx(Line, { type: "monotone", dataKey: "uniques", stroke: lineStroke, name: "Uniques" })] }) })) })] })) : null, isVisible("top_paths") || isVisible("top_content") ? (_jsxs("div", { className: "splitPanels", children: [isVisible("top_paths") ? (_jsx(BreakdownPanel, { title: "Top paths", emptyMessage: "No path data for this period.", rows: summary.top_paths.map((row) => ({
196
193
  key: row.path,
197
194
  label: row.path,
198
195
  value: `${formatNumber(row.views)} views · ${formatNumber(row.uniques)} uniques`,
@@ -1,4 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  export function AnalyticsDashboardSkeleton() {
3
- return (_jsxs("div", { className: "analytix-dash analytix-skeleton", "aria-busy": "true", "aria-label": "Loading analytics", children: [_jsxs("div", { className: "toolbar", children: [_jsx("div", { className: "sk skRealtime" }), _jsx("div", { className: "sk skBtn" })] }), _jsx("div", { className: "filters", children: Array.from({ length: 4 }).map((_, i) => (_jsxs("div", { className: "filterGroup", children: [_jsx("div", { className: "sk skLabel" }), _jsxs("div", { className: "skRow", children: [_jsx("div", { className: "sk skChip" }), _jsx("div", { className: "sk skChip" }), _jsx("div", { className: "sk skChip" })] })] }, i))) }), _jsx("div", { className: "metrics", children: Array.from({ length: 7 }).map((_, i) => (_jsxs("div", { className: "metricCard", children: [_jsx("div", { className: "sk skIcon" }), _jsx("div", { className: "sk skMetricValue" }), _jsx("div", { className: "sk skMetricLabel" }), _jsx("div", { className: "sk skDelta" })] }, i))) }), _jsxs("div", { className: "chartPanel", children: [_jsx("div", { className: "sk skPanelTitle" }), _jsx("div", { className: "sk skChart" })] }), _jsx("div", { className: "splitPanels", children: Array.from({ length: 2 }).map((_, i) => (_jsxs("div", { className: "chartPanel", children: [_jsx("div", { className: "sk skPanelTitle" }), _jsx("div", { className: "skList", children: Array.from({ length: 5 }).map((__, j) => (_jsx("div", { className: "sk skListRow" }, j))) })] }, i))) })] }));
3
+ return (_jsxs("div", { className: "analytix-dash analytix-theme-light analytix-skeleton", "aria-busy": "true", "aria-label": "Loading analytics", children: [_jsxs("div", { className: "toolbar", children: [_jsx("div", { className: "sk skRealtime" }), _jsx("div", { className: "sk skBtn" })] }), _jsx("div", { className: "filters", children: Array.from({ length: 4 }).map((_, i) => (_jsxs("div", { className: "filterGroup", children: [_jsx("div", { className: "sk skLabel" }), _jsxs("div", { className: "skRow", children: [_jsx("div", { className: "sk skChip" }), _jsx("div", { className: "sk skChip" }), _jsx("div", { className: "sk skChip" })] })] }, i))) }), _jsx("div", { className: "metrics", children: Array.from({ length: 7 }).map((_, i) => (_jsxs("div", { className: "metricCard", children: [_jsx("div", { className: "sk skIcon" }), _jsx("div", { className: "sk skMetricValue" }), _jsx("div", { className: "sk skMetricLabel" }), _jsx("div", { className: "sk skDelta" })] }, i))) }), _jsxs("div", { className: "chartPanel", children: [_jsx("div", { className: "sk skPanelTitle" }), _jsx("div", { className: "sk skChart" })] }), _jsx("div", { className: "splitPanels", children: Array.from({ length: 2 }).map((_, i) => (_jsxs("div", { className: "chartPanel", children: [_jsx("div", { className: "sk skPanelTitle" }), _jsx("div", { className: "skList", children: Array.from({ length: 5 }).map((__, j) => (_jsx("div", { className: "sk skListRow" }, j))) })] }, i))) })] }));
4
4
  }
@@ -1,42 +1,55 @@
1
+ /**
2
+ * Embeddable dashboard styles.
3
+ * Host apps may override tokens via --analytix-dash-* on a parent element.
4
+ */
1
5
  .analytix-dash {
2
- --ax-primary: #0f172a;
3
- --ax-muted: #64748b;
4
- --ax-accent: #0052ff;
5
- --ax-surface: rgba(255, 255, 255, 0.72);
6
- --ax-surface-solid: #ffffff;
7
- --ax-border: rgba(15, 23, 42, 0.08);
8
- --ax-success: #047857;
9
- --ax-success-bg: rgba(5, 150, 105, 0.08);
10
- --ax-danger: #dc2626;
11
- --ax-up: #059669;
12
- --ax-down: #dc2626;
13
- --ax-chart-grid: rgba(15, 23, 42, 0.08);
6
+ --ax-primary: var(--analytix-dash-ink, #111111);
7
+ --ax-muted: var(--analytix-dash-muted, #787774);
8
+ --ax-accent: var(--analytix-dash-accent, #111111);
9
+ --ax-surface: var(--analytix-dash-surface, rgba(255, 255, 255, 0.82));
10
+ --ax-surface-solid: var(--analytix-dash-surface-solid, #ffffff);
11
+ --ax-border: var(--analytix-dash-border, rgba(17, 17, 17, 0.08));
12
+ --ax-success: var(--analytix-dash-success, #346538);
13
+ --ax-success-bg: var(--analytix-dash-success-bg, #edf3ec);
14
+ --ax-danger: var(--analytix-dash-danger, #9f2f2d);
15
+ --ax-up: #346538;
16
+ --ax-down: #9f2f2d;
17
+ --ax-chart-grid: rgba(17, 17, 17, 0.08);
14
18
  display: flex;
15
19
  flex-direction: column;
16
20
  gap: 24px;
17
21
  width: 100%;
22
+ min-width: 0;
18
23
  color: var(--ax-primary);
19
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
24
+ font-family: var(
25
+ --analytix-dash-font,
26
+ system-ui,
27
+ -apple-system,
28
+ BlinkMacSystemFont,
29
+ "Segoe UI",
30
+ sans-serif
31
+ );
20
32
  }
21
33
 
22
34
  .analytix-dash.analytix-theme-dark {
23
- --ax-primary: #e2e8f0;
24
- --ax-muted: #94a3b8;
25
- --ax-accent: #60a5fa;
26
- --ax-surface: rgba(15, 23, 42, 0.72);
27
- --ax-surface-solid: #0f172a;
28
- --ax-border: rgba(148, 163, 184, 0.18);
29
- --ax-success: #34d399;
30
- --ax-success-bg: rgba(52, 211, 153, 0.12);
31
- --ax-up: #34d399;
32
- --ax-down: #f87171;
33
- --ax-chart-grid: rgba(148, 163, 184, 0.15);
35
+ --ax-primary: var(--analytix-dash-ink, #f7f6f3);
36
+ --ax-muted: var(--analytix-dash-muted, #9b9a97);
37
+ --ax-accent: var(--analytix-dash-accent, #f7f6f3);
38
+ --ax-surface: var(--analytix-dash-surface, rgba(25, 25, 24, 0.88));
39
+ --ax-surface-solid: var(--analytix-dash-surface-solid, #191918);
40
+ --ax-border: var(--analytix-dash-border, rgba(255, 255, 255, 0.1));
41
+ --ax-success: #6ee7b7;
42
+ --ax-success-bg: rgba(52, 101, 56, 0.22);
43
+ --ax-danger: #fca5a5;
44
+ --ax-up: #6ee7b7;
45
+ --ax-down: #fca5a5;
46
+ --ax-chart-grid: rgba(255, 255, 255, 0.08);
34
47
  }
35
48
 
36
49
  .analytix-dash.analytix-refreshing {
37
50
  opacity: 0.72;
38
51
  pointer-events: none;
39
- transition: opacity 0.15s ease;
52
+ transition: opacity 150ms cubic-bezier(0.23, 1, 0.32, 1);
40
53
  }
41
54
 
42
55
  .analytix-dash .filters {
@@ -45,7 +58,7 @@
45
58
  gap: 14px;
46
59
  padding: 18px;
47
60
  border: 1px solid var(--ax-border);
48
- border-radius: 14px;
61
+ border-radius: 12px;
49
62
  background: var(--ax-surface);
50
63
  }
51
64
 
@@ -83,17 +96,33 @@
83
96
  font-weight: 600;
84
97
  cursor: pointer;
85
98
  color: var(--ax-muted);
99
+ transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1),
100
+ background 160ms cubic-bezier(0.23, 1, 0.32, 1),
101
+ border-color 160ms cubic-bezier(0.23, 1, 0.32, 1),
102
+ color 160ms cubic-bezier(0.23, 1, 0.32, 1);
103
+ }
104
+
105
+ @media (hover: hover) and (pointer: fine) {
106
+ .analytix-dash .chip:hover {
107
+ color: var(--ax-primary);
108
+ border-color: var(--ax-accent);
109
+ }
110
+ }
111
+
112
+ .analytix-dash .chip:active,
113
+ .analytix-dash .chipActive:active {
114
+ transform: scale(0.97);
86
115
  }
87
116
 
88
117
  .analytix-dash .chipActive {
89
- background: rgba(0, 82, 255, 0.08);
90
- border-color: rgba(0, 82, 255, 0.18);
118
+ background: rgba(17, 17, 17, 0.06);
119
+ border-color: rgba(17, 17, 17, 0.14);
91
120
  color: var(--ax-accent);
92
121
  }
93
122
 
94
123
  .analytix-dash.analytix-theme-dark .chipActive {
95
- background: rgba(96, 165, 250, 0.14);
96
- border-color: rgba(96, 165, 250, 0.28);
124
+ background: rgba(255, 255, 255, 0.08);
125
+ border-color: rgba(255, 255, 255, 0.16);
97
126
  }
98
127
 
99
128
  .analytix-dash .dateRangeRow {
@@ -115,9 +144,17 @@
115
144
  font: inherit;
116
145
  padding: 10px 12px;
117
146
  border: 1px solid var(--ax-border);
118
- border-radius: 10px;
147
+ border-radius: 8px;
119
148
  background: var(--ax-surface-solid);
120
149
  color: var(--ax-primary);
150
+ transition: border-color 160ms cubic-bezier(0.23, 1, 0.32, 1);
151
+ }
152
+
153
+ .analytix-dash .textField input:focus,
154
+ .analytix-dash .dateRangeRow input:focus,
155
+ .analytix-dash select:focus {
156
+ outline: none;
157
+ border-color: var(--ax-accent);
121
158
  }
122
159
 
123
160
  .analytix-dash .checkboxRow {
@@ -139,7 +176,7 @@
139
176
  flex-direction: column;
140
177
  gap: 8px;
141
178
  padding: 18px;
142
- border-radius: 14px;
179
+ border-radius: 12px;
143
180
  border: 1px solid var(--ax-border);
144
181
  background: var(--ax-surface);
145
182
  color: var(--ax-accent);
@@ -151,6 +188,7 @@
151
188
  font-weight: 600;
152
189
  color: var(--ax-primary);
153
190
  line-height: 1.2;
191
+ font-variant-numeric: tabular-nums;
154
192
  }
155
193
 
156
194
  .analytix-dash .metricLabel {
@@ -173,7 +211,7 @@
173
211
 
174
212
  .analytix-dash .chartPanel {
175
213
  padding: 18px;
176
- border-radius: 14px;
214
+ border-radius: 12px;
177
215
  border: 1px solid var(--ax-border);
178
216
  background: var(--ax-surface);
179
217
  min-width: 0;
@@ -183,11 +221,15 @@
183
221
  width: 100%;
184
222
  height: 280px;
185
223
  min-height: 280px;
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
186
227
  }
187
228
 
188
229
  .analytix-dash .panelTitle {
189
- font-size: 0.875rem;
230
+ font-size: 0.75rem;
190
231
  font-weight: 700;
232
+ letter-spacing: 0.06em;
191
233
  text-transform: uppercase;
192
234
  color: var(--ax-muted);
193
235
  margin-bottom: 14px;
@@ -250,6 +292,7 @@
250
292
  display: flex;
251
293
  gap: 8px;
252
294
  align-items: center;
295
+ flex-wrap: wrap;
253
296
  }
254
297
 
255
298
  .analytix-dash .btn {
@@ -265,6 +308,18 @@
265
308
  font-weight: 600;
266
309
  color: var(--ax-primary);
267
310
  text-decoration: none;
311
+ transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1),
312
+ background 160ms cubic-bezier(0.23, 1, 0.32, 1);
313
+ }
314
+
315
+ @media (hover: hover) and (pointer: fine) {
316
+ .analytix-dash .btn:hover {
317
+ background: var(--ax-surface);
318
+ }
319
+ }
320
+
321
+ .analytix-dash .btn:active {
322
+ transform: scale(0.97);
268
323
  }
269
324
 
270
325
  .analytix-dash .btnIcon {
@@ -272,27 +327,49 @@
272
327
  }
273
328
 
274
329
  .analytix-dash .realtime {
330
+ display: inline-flex;
331
+ align-items: center;
332
+ gap: 8px;
275
333
  padding: 10px 14px;
276
- border-radius: 10px;
334
+ border-radius: 9999px;
277
335
  background: var(--ax-success-bg);
278
336
  color: var(--ax-success);
279
337
  font-size: 0.875rem;
280
338
  font-weight: 600;
281
339
  }
282
340
 
341
+ .analytix-dash .realtime::before {
342
+ content: "";
343
+ width: 7px;
344
+ height: 7px;
345
+ border-radius: 50%;
346
+ background: currentColor;
347
+ opacity: 0.85;
348
+ }
349
+
283
350
  /* Skeleton */
284
351
  .analytix-dash.analytix-skeleton .sk {
285
352
  border-radius: 8px;
286
353
  background: linear-gradient(
287
354
  90deg,
288
- rgba(148, 163, 184, 0.12) 0%,
289
- rgba(148, 163, 184, 0.22) 50%,
290
- rgba(148, 163, 184, 0.12) 100%
355
+ rgba(120, 119, 116, 0.1) 0%,
356
+ rgba(120, 119, 116, 0.18) 50%,
357
+ rgba(120, 119, 116, 0.1) 100%
291
358
  );
292
359
  background-size: 200% 100%;
293
360
  animation: analytix-shimmer 1.4s ease-in-out infinite;
294
361
  }
295
362
 
363
+ .analytix-dash.analytix-theme-dark.analytix-skeleton .sk {
364
+ background: linear-gradient(
365
+ 90deg,
366
+ rgba(255, 255, 255, 0.06) 0%,
367
+ rgba(255, 255, 255, 0.12) 50%,
368
+ rgba(255, 255, 255, 0.06) 100%
369
+ );
370
+ background-size: 200% 100%;
371
+ }
372
+
296
373
  .analytix-dash.analytix-skeleton .skRow {
297
374
  display: flex;
298
375
  gap: 6px;
@@ -302,6 +379,7 @@
302
379
  .analytix-dash.analytix-skeleton .skRealtime {
303
380
  width: 260px;
304
381
  height: 40px;
382
+ border-radius: 9999px;
305
383
  }
306
384
 
307
385
  .analytix-dash.analytix-skeleton .skBtn {
@@ -372,6 +450,17 @@
372
450
  }
373
451
  }
374
452
 
453
+ @media (prefers-reduced-motion: reduce) {
454
+ .analytix-dash.analytix-skeleton .sk {
455
+ animation: none;
456
+ background: rgba(120, 119, 116, 0.12);
457
+ }
458
+
459
+ .analytix-dash.analytix-theme-dark.analytix-skeleton .sk {
460
+ background: rgba(255, 255, 255, 0.08);
461
+ }
462
+ }
463
+
375
464
  @media (max-width: 900px) {
376
465
  .analytix-dash .filters,
377
466
  .analytix-dash .splitPanels {
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export { AnalyticsDashboard, type AnalyticsDashboardProps } from "./AnalyticsDas
2
2
  export { AnalyticsDashboardSkeleton } from "./AnalyticsDashboardSkeleton";
3
3
  export { WidgetCustomizePanel } from "./WidgetCustomizePanel";
4
4
  export { useDashboardWidgets } from "./useDashboardWidgets";
5
+ export { useDashboardTheme, DASHBOARD_THEME_LABELS, type DashboardThemeMode } from "./useDashboardTheme";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { AnalyticsDashboard } from "./AnalyticsDashboard";
2
2
  export { AnalyticsDashboardSkeleton } from "./AnalyticsDashboardSkeleton";
3
3
  export { WidgetCustomizePanel } from "./WidgetCustomizePanel";
4
4
  export { useDashboardWidgets } from "./useDashboardWidgets";
5
+ export { useDashboardTheme, DASHBOARD_THEME_LABELS } from "./useDashboardTheme";
@@ -0,0 +1,8 @@
1
+ export type DashboardThemeMode = "light" | "dark" | "system";
2
+ export declare function useDashboardTheme(siteId: string, defaultTheme?: DashboardThemeMode): {
3
+ themeMode: DashboardThemeMode;
4
+ setThemeMode: (next: DashboardThemeMode) => void;
5
+ cycleTheme: () => void;
6
+ resolvedTheme: "light" | "dark";
7
+ };
8
+ export declare const DASHBOARD_THEME_LABELS: Record<DashboardThemeMode, string>;
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ const STORAGE_PREFIX = "analytix_dashboard_theme";
4
+ function storageKey(siteId) {
5
+ return `${STORAGE_PREFIX}_${siteId}`;
6
+ }
7
+ function readStoredTheme(siteId, fallback) {
8
+ if (typeof window === "undefined")
9
+ return fallback;
10
+ const stored = localStorage.getItem(storageKey(siteId));
11
+ if (stored === "light" || stored === "dark" || stored === "system")
12
+ return stored;
13
+ return fallback;
14
+ }
15
+ export function useDashboardTheme(siteId, defaultTheme = "system") {
16
+ const [themeMode, setThemeModeState] = useState(defaultTheme);
17
+ useEffect(() => {
18
+ setThemeModeState(readStoredTheme(siteId, defaultTheme));
19
+ }, [siteId, defaultTheme]);
20
+ useEffect(() => {
21
+ localStorage.setItem(storageKey(siteId), themeMode);
22
+ }, [siteId, themeMode]);
23
+ function setThemeMode(next) {
24
+ setThemeModeState(next);
25
+ }
26
+ function cycleTheme() {
27
+ setThemeModeState((current) => current === "light" ? "dark" : current === "dark" ? "system" : "light");
28
+ }
29
+ const resolvedTheme = useResolvedTheme(themeMode);
30
+ return { themeMode, setThemeMode, cycleTheme, resolvedTheme };
31
+ }
32
+ function useResolvedTheme(mode) {
33
+ const [resolved, setResolved] = useState("light");
34
+ useEffect(() => {
35
+ if (mode !== "system") {
36
+ setResolved(mode);
37
+ return;
38
+ }
39
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
40
+ const apply = () => setResolved(media.matches ? "dark" : "light");
41
+ apply();
42
+ media.addEventListener("change", apply);
43
+ return () => media.removeEventListener("change", apply);
44
+ }, [mode]);
45
+ return resolved;
46
+ }
47
+ export const DASHBOARD_THEME_LABELS = {
48
+ light: "Light",
49
+ dark: "Dark",
50
+ system: "System",
51
+ };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@analytix/dashboard",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Analytix embeddable analytics dashboard UI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "src/dashboard.css"
10
11
  ],
11
12
  "exports": {
12
13
  ".": {
@@ -14,7 +15,7 @@
14
15
  "import": "./dist/index.js",
15
16
  "default": "./dist/index.js"
16
17
  },
17
- "./styles.css": "./dist/dashboard.css"
18
+ "./styles.css": "./src/dashboard.css"
18
19
  },
19
20
  "publishConfig": {
20
21
  "access": "public",
@@ -35,7 +36,7 @@
35
36
  "lucide-react": ">=0.400"
36
37
  },
37
38
  "dependencies": {
38
- "@analytix/core": "^0.2.2"
39
+ "@analytix/core": "^0.3.1"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@types/react": "^19",
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Embeddable dashboard styles.
3
+ * Host apps may override tokens via --analytix-dash-* on a parent element.
4
+ */
5
+ .analytix-dash {
6
+ --ax-primary: var(--analytix-dash-ink, #111111);
7
+ --ax-muted: var(--analytix-dash-muted, #787774);
8
+ --ax-accent: var(--analytix-dash-accent, #111111);
9
+ --ax-surface: var(--analytix-dash-surface, rgba(255, 255, 255, 0.82));
10
+ --ax-surface-solid: var(--analytix-dash-surface-solid, #ffffff);
11
+ --ax-border: var(--analytix-dash-border, rgba(17, 17, 17, 0.08));
12
+ --ax-success: var(--analytix-dash-success, #346538);
13
+ --ax-success-bg: var(--analytix-dash-success-bg, #edf3ec);
14
+ --ax-danger: var(--analytix-dash-danger, #9f2f2d);
15
+ --ax-up: #346538;
16
+ --ax-down: #9f2f2d;
17
+ --ax-chart-grid: rgba(17, 17, 17, 0.08);
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: 24px;
21
+ width: 100%;
22
+ min-width: 0;
23
+ color: var(--ax-primary);
24
+ font-family: var(
25
+ --analytix-dash-font,
26
+ system-ui,
27
+ -apple-system,
28
+ BlinkMacSystemFont,
29
+ "Segoe UI",
30
+ sans-serif
31
+ );
32
+ }
33
+
34
+ .analytix-dash.analytix-theme-dark {
35
+ --ax-primary: var(--analytix-dash-ink, #f7f6f3);
36
+ --ax-muted: var(--analytix-dash-muted, #9b9a97);
37
+ --ax-accent: var(--analytix-dash-accent, #f7f6f3);
38
+ --ax-surface: var(--analytix-dash-surface, rgba(25, 25, 24, 0.88));
39
+ --ax-surface-solid: var(--analytix-dash-surface-solid, #191918);
40
+ --ax-border: var(--analytix-dash-border, rgba(255, 255, 255, 0.1));
41
+ --ax-success: #6ee7b7;
42
+ --ax-success-bg: rgba(52, 101, 56, 0.22);
43
+ --ax-danger: #fca5a5;
44
+ --ax-up: #6ee7b7;
45
+ --ax-down: #fca5a5;
46
+ --ax-chart-grid: rgba(255, 255, 255, 0.08);
47
+ }
48
+
49
+ .analytix-dash.analytix-refreshing {
50
+ opacity: 0.72;
51
+ pointer-events: none;
52
+ transition: opacity 150ms cubic-bezier(0.23, 1, 0.32, 1);
53
+ }
54
+
55
+ .analytix-dash .filters {
56
+ display: grid;
57
+ grid-template-columns: repeat(2, minmax(0, 1fr));
58
+ gap: 14px;
59
+ padding: 18px;
60
+ border: 1px solid var(--ax-border);
61
+ border-radius: 12px;
62
+ background: var(--ax-surface);
63
+ }
64
+
65
+ .analytix-dash .filterGroup {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 8px;
69
+ }
70
+
71
+ .analytix-dash .filterGroupWide {
72
+ grid-column: 1 / -1;
73
+ }
74
+
75
+ .analytix-dash .filterLabel {
76
+ font-size: 0.75rem;
77
+ font-weight: 600;
78
+ letter-spacing: 0.04em;
79
+ text-transform: uppercase;
80
+ color: var(--ax-muted);
81
+ }
82
+
83
+ .analytix-dash .chips {
84
+ display: flex;
85
+ flex-wrap: wrap;
86
+ gap: 6px;
87
+ }
88
+
89
+ .analytix-dash .chip,
90
+ .analytix-dash .chipActive {
91
+ padding: 7px 12px;
92
+ border-radius: 999px;
93
+ border: 1px solid var(--ax-border);
94
+ background: var(--ax-surface-solid);
95
+ font-size: 0.8125rem;
96
+ font-weight: 600;
97
+ cursor: pointer;
98
+ color: var(--ax-muted);
99
+ transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1),
100
+ background 160ms cubic-bezier(0.23, 1, 0.32, 1),
101
+ border-color 160ms cubic-bezier(0.23, 1, 0.32, 1),
102
+ color 160ms cubic-bezier(0.23, 1, 0.32, 1);
103
+ }
104
+
105
+ @media (hover: hover) and (pointer: fine) {
106
+ .analytix-dash .chip:hover {
107
+ color: var(--ax-primary);
108
+ border-color: var(--ax-accent);
109
+ }
110
+ }
111
+
112
+ .analytix-dash .chip:active,
113
+ .analytix-dash .chipActive:active {
114
+ transform: scale(0.97);
115
+ }
116
+
117
+ .analytix-dash .chipActive {
118
+ background: rgba(17, 17, 17, 0.06);
119
+ border-color: rgba(17, 17, 17, 0.14);
120
+ color: var(--ax-accent);
121
+ }
122
+
123
+ .analytix-dash.analytix-theme-dark .chipActive {
124
+ background: rgba(255, 255, 255, 0.08);
125
+ border-color: rgba(255, 255, 255, 0.16);
126
+ }
127
+
128
+ .analytix-dash .dateRangeRow {
129
+ display: flex;
130
+ flex-wrap: wrap;
131
+ gap: 12px;
132
+ margin-top: 4px;
133
+ }
134
+
135
+ .analytix-dash .dateRangeRow label {
136
+ display: flex;
137
+ flex-direction: column;
138
+ gap: 6px;
139
+ }
140
+
141
+ .analytix-dash .textField input,
142
+ .analytix-dash .dateRangeRow input,
143
+ .analytix-dash select {
144
+ font: inherit;
145
+ padding: 10px 12px;
146
+ border: 1px solid var(--ax-border);
147
+ border-radius: 8px;
148
+ background: var(--ax-surface-solid);
149
+ color: var(--ax-primary);
150
+ transition: border-color 160ms cubic-bezier(0.23, 1, 0.32, 1);
151
+ }
152
+
153
+ .analytix-dash .textField input:focus,
154
+ .analytix-dash .dateRangeRow input:focus,
155
+ .analytix-dash select:focus {
156
+ outline: none;
157
+ border-color: var(--ax-accent);
158
+ }
159
+
160
+ .analytix-dash .checkboxRow {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 8px;
164
+ font-size: 0.875rem;
165
+ color: var(--ax-muted);
166
+ }
167
+
168
+ .analytix-dash .metrics {
169
+ display: grid;
170
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
171
+ gap: 14px;
172
+ }
173
+
174
+ .analytix-dash .metricCard {
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 8px;
178
+ padding: 18px;
179
+ border-radius: 12px;
180
+ border: 1px solid var(--ax-border);
181
+ background: var(--ax-surface);
182
+ color: var(--ax-accent);
183
+ min-width: 0;
184
+ }
185
+
186
+ .analytix-dash .metricValue {
187
+ font-size: 1.5rem;
188
+ font-weight: 600;
189
+ color: var(--ax-primary);
190
+ line-height: 1.2;
191
+ font-variant-numeric: tabular-nums;
192
+ }
193
+
194
+ .analytix-dash .metricLabel {
195
+ font-size: 0.8125rem;
196
+ color: var(--ax-muted);
197
+ }
198
+
199
+ .analytix-dash .metricDelta {
200
+ font-size: 0.75rem;
201
+ color: var(--ax-muted);
202
+ }
203
+
204
+ .analytix-dash .metricDeltaUp {
205
+ color: var(--ax-up);
206
+ }
207
+
208
+ .analytix-dash .metricDeltaDown {
209
+ color: var(--ax-down);
210
+ }
211
+
212
+ .analytix-dash .chartPanel {
213
+ padding: 18px;
214
+ border-radius: 12px;
215
+ border: 1px solid var(--ax-border);
216
+ background: var(--ax-surface);
217
+ min-width: 0;
218
+ }
219
+
220
+ .analytix-dash .chartContainer {
221
+ width: 100%;
222
+ height: 280px;
223
+ min-height: 280px;
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ }
228
+
229
+ .analytix-dash .panelTitle {
230
+ font-size: 0.75rem;
231
+ font-weight: 700;
232
+ letter-spacing: 0.06em;
233
+ text-transform: uppercase;
234
+ color: var(--ax-muted);
235
+ margin-bottom: 14px;
236
+ }
237
+
238
+ .analytix-dash .splitPanels {
239
+ display: grid;
240
+ grid-template-columns: repeat(2, minmax(0, 1fr));
241
+ gap: 14px;
242
+ }
243
+
244
+ .analytix-dash .list {
245
+ list-style: none;
246
+ margin: 0;
247
+ padding: 0;
248
+ display: flex;
249
+ flex-direction: column;
250
+ gap: 8px;
251
+ }
252
+
253
+ .analytix-dash .list li {
254
+ display: flex;
255
+ justify-content: space-between;
256
+ gap: 12px;
257
+ font-size: 0.875rem;
258
+ }
259
+
260
+ .analytix-dash .listLabel {
261
+ min-width: 0;
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ white-space: nowrap;
265
+ }
266
+
267
+ .analytix-dash .listValue {
268
+ flex-shrink: 0;
269
+ color: var(--ax-muted);
270
+ font-variant-numeric: tabular-nums;
271
+ }
272
+
273
+ .analytix-dash .emptyState {
274
+ margin: 0;
275
+ font-size: 0.875rem;
276
+ color: var(--ax-muted);
277
+ }
278
+
279
+ .analytix-dash .error {
280
+ color: var(--ax-danger);
281
+ }
282
+
283
+ .analytix-dash .toolbar {
284
+ display: flex;
285
+ gap: 12px;
286
+ flex-wrap: wrap;
287
+ align-items: center;
288
+ justify-content: space-between;
289
+ }
290
+
291
+ .analytix-dash .toolbarActions {
292
+ display: flex;
293
+ gap: 8px;
294
+ align-items: center;
295
+ flex-wrap: wrap;
296
+ }
297
+
298
+ .analytix-dash .btn {
299
+ display: inline-flex;
300
+ align-items: center;
301
+ gap: 6px;
302
+ padding: 8px 14px;
303
+ border-radius: 8px;
304
+ border: 1px solid var(--ax-border);
305
+ background: var(--ax-surface-solid);
306
+ cursor: pointer;
307
+ font-size: 0.875rem;
308
+ font-weight: 600;
309
+ color: var(--ax-primary);
310
+ text-decoration: none;
311
+ transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1),
312
+ background 160ms cubic-bezier(0.23, 1, 0.32, 1);
313
+ }
314
+
315
+ @media (hover: hover) and (pointer: fine) {
316
+ .analytix-dash .btn:hover {
317
+ background: var(--ax-surface);
318
+ }
319
+ }
320
+
321
+ .analytix-dash .btn:active {
322
+ transform: scale(0.97);
323
+ }
324
+
325
+ .analytix-dash .btnIcon {
326
+ padding: 8px 10px;
327
+ }
328
+
329
+ .analytix-dash .realtime {
330
+ display: inline-flex;
331
+ align-items: center;
332
+ gap: 8px;
333
+ padding: 10px 14px;
334
+ border-radius: 9999px;
335
+ background: var(--ax-success-bg);
336
+ color: var(--ax-success);
337
+ font-size: 0.875rem;
338
+ font-weight: 600;
339
+ }
340
+
341
+ .analytix-dash .realtime::before {
342
+ content: "";
343
+ width: 7px;
344
+ height: 7px;
345
+ border-radius: 50%;
346
+ background: currentColor;
347
+ opacity: 0.85;
348
+ }
349
+
350
+ /* Skeleton */
351
+ .analytix-dash.analytix-skeleton .sk {
352
+ border-radius: 8px;
353
+ background: linear-gradient(
354
+ 90deg,
355
+ rgba(120, 119, 116, 0.1) 0%,
356
+ rgba(120, 119, 116, 0.18) 50%,
357
+ rgba(120, 119, 116, 0.1) 100%
358
+ );
359
+ background-size: 200% 100%;
360
+ animation: analytix-shimmer 1.4s ease-in-out infinite;
361
+ }
362
+
363
+ .analytix-dash.analytix-theme-dark.analytix-skeleton .sk {
364
+ background: linear-gradient(
365
+ 90deg,
366
+ rgba(255, 255, 255, 0.06) 0%,
367
+ rgba(255, 255, 255, 0.12) 50%,
368
+ rgba(255, 255, 255, 0.06) 100%
369
+ );
370
+ background-size: 200% 100%;
371
+ }
372
+
373
+ .analytix-dash.analytix-skeleton .skRow {
374
+ display: flex;
375
+ gap: 6px;
376
+ flex-wrap: wrap;
377
+ }
378
+
379
+ .analytix-dash.analytix-skeleton .skRealtime {
380
+ width: 260px;
381
+ height: 40px;
382
+ border-radius: 9999px;
383
+ }
384
+
385
+ .analytix-dash.analytix-skeleton .skBtn {
386
+ width: 120px;
387
+ height: 36px;
388
+ }
389
+
390
+ .analytix-dash.analytix-skeleton .skLabel {
391
+ width: 72px;
392
+ height: 12px;
393
+ }
394
+
395
+ .analytix-dash.analytix-skeleton .skChip {
396
+ width: 48px;
397
+ height: 32px;
398
+ border-radius: 999px;
399
+ }
400
+
401
+ .analytix-dash.analytix-skeleton .skIcon {
402
+ width: 18px;
403
+ height: 18px;
404
+ }
405
+
406
+ .analytix-dash.analytix-skeleton .skMetricValue {
407
+ width: 72px;
408
+ height: 28px;
409
+ }
410
+
411
+ .analytix-dash.analytix-skeleton .skMetricLabel {
412
+ width: 96px;
413
+ height: 14px;
414
+ }
415
+
416
+ .analytix-dash.analytix-skeleton .skDelta {
417
+ width: 120px;
418
+ height: 12px;
419
+ }
420
+
421
+ .analytix-dash.analytix-skeleton .skPanelTitle {
422
+ width: 120px;
423
+ height: 14px;
424
+ margin-bottom: 14px;
425
+ }
426
+
427
+ .analytix-dash.analytix-skeleton .skChart {
428
+ width: 100%;
429
+ height: 280px;
430
+ border-radius: 12px;
431
+ }
432
+
433
+ .analytix-dash.analytix-skeleton .skList {
434
+ display: flex;
435
+ flex-direction: column;
436
+ gap: 8px;
437
+ }
438
+
439
+ .analytix-dash.analytix-skeleton .skListRow {
440
+ width: 100%;
441
+ height: 18px;
442
+ }
443
+
444
+ @keyframes analytix-shimmer {
445
+ 0% {
446
+ background-position: 200% 0;
447
+ }
448
+ 100% {
449
+ background-position: -200% 0;
450
+ }
451
+ }
452
+
453
+ @media (prefers-reduced-motion: reduce) {
454
+ .analytix-dash.analytix-skeleton .sk {
455
+ animation: none;
456
+ background: rgba(120, 119, 116, 0.12);
457
+ }
458
+
459
+ .analytix-dash.analytix-theme-dark.analytix-skeleton .sk {
460
+ background: rgba(255, 255, 255, 0.08);
461
+ }
462
+ }
463
+
464
+ @media (max-width: 900px) {
465
+ .analytix-dash .filters,
466
+ .analytix-dash .splitPanels {
467
+ grid-template-columns: 1fr;
468
+ }
469
+ }
470
+
471
+ .analytix-dash .widgetCustomize .widgetGrid {
472
+ display: grid;
473
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
474
+ gap: 10px;
475
+ margin-bottom: 14px;
476
+ }
477
+
478
+ .analytix-dash .widgetToggle {
479
+ display: flex;
480
+ align-items: center;
481
+ gap: 8px;
482
+ font-size: 0.875rem;
483
+ color: var(--ax-muted);
484
+ }
485
+
486
+ .analytix-dash .widgetActions {
487
+ display: flex;
488
+ flex-wrap: wrap;
489
+ gap: 8px;
490
+ }