@thebase/ui 1.0.0 → 1.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/CHANGELOG.md +23 -0
- package/LICENSE +28 -0
- package/README.md +21 -6
- package/dist/baseui.analytics.esm.js +1 -0
- package/dist/baseui.analytics.min.js +1 -0
- package/dist/baseui.esm.js +9 -9
- package/dist/baseui.icons.esm.js +1 -1
- package/dist/baseui.min.js +9 -9
- package/dist/baseui.registry.json +8 -4
- package/dist/types/analytics.d.ts +76 -0
- package/dist/types/index.d.ts +4 -0
- package/docs/analytics.md +267 -0
- package/docs/blocks.md +1 -1
- package/docs/components/avatar.md +3 -3
- package/docs/components/block-viewer.md +2 -2
- package/docs/components/calendar.md +3 -3
- package/docs/components/carousel.md +3 -3
- package/docs/components/chart.md +3 -3
- package/docs/components/combobox.md +3 -3
- package/docs/components/command.md +3 -3
- package/docs/components/context-menu.md +3 -3
- package/docs/components/data-table.md +3 -3
- package/docs/components/date-picker.md +3 -3
- package/docs/components/icon.md +2 -2
- package/docs/components/input-otp.md +3 -3
- package/docs/components/input.md +2 -2
- package/docs/components/markdown.md +2 -2
- package/docs/components/message-scroller.md +3 -3
- package/docs/components/message.md +3 -3
- package/docs/components/popover.md +3 -3
- package/docs/components/questionnaire.md +3 -3
- package/docs/components/resizable.md +3 -3
- package/docs/components/scroll-area.md +3 -3
- package/docs/components/text-editor.md +1 -1
- package/docs/components/theme-button.md +2 -2
- package/docs/components/toggle-group.md +3 -3
- package/docs/components/toggle.md +3 -3
- package/docs/components/tooltip.md +3 -3
- package/docs/icons.md +5 -5
- package/docs/installation.md +4 -3
- package/docs/owl-components.md +2 -2
- package/docs/registry.md +2 -1
- package/docs/runtime-api.md +16 -12
- package/docs/theming.md +4 -4
- package/docs/usage-guide.md +15 -15
- package/package.json +8 -1
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"baseui.icons.esm.js",
|
|
19
19
|
"baseui.templates.xml",
|
|
20
20
|
"baseui.block-viewer.esm.js",
|
|
21
|
+
"baseui.analytics.esm.js",
|
|
22
|
+
"baseui.analytics.min.js",
|
|
21
23
|
"icons/lucide.json"
|
|
22
24
|
],
|
|
23
25
|
"icons": {
|
|
@@ -1660,11 +1662,13 @@
|
|
|
1660
1662
|
"integrity": {
|
|
1661
1663
|
"baseui.min.css": "sha384-hYFwlV6T/ONYzawtDGDFUoruqgNwYl9AVvLDHx24IWgKSMXqwACo/nX04I3gnyT9",
|
|
1662
1664
|
"bootstrap.bundle.min.js": "sha384-RMbllTJcplkCRnELNTOc5Kz/3AvFDE/rNlJqWqfgmRWruobClBdfwvl10akQZkR1",
|
|
1663
|
-
"baseui.min.js": "sha384-
|
|
1664
|
-
"baseui.esm.js": "sha384-
|
|
1665
|
-
"baseui.icons.esm.js": "sha384-
|
|
1665
|
+
"baseui.min.js": "sha384-m91LivkZ3FsaSmngFXfmaGHxfBokU/tEXFAuYz3PIjeXuKMcJjdRmOEk596O8S8u",
|
|
1666
|
+
"baseui.esm.js": "sha384-v3JjyzSIh67awBXIDScUTR40lyCHyuOue1zwObQHjH4OjOj9Cg3mPMkto3LVcVO7",
|
|
1667
|
+
"baseui.icons.esm.js": "sha384-hj39VMhzcFvmYnjob2hlioMOAqwAXCURGreF6s9RitXFp/iYi7rUdqtfCI+ICQeS",
|
|
1666
1668
|
"baseui.templates.xml": "sha384-XkqmfurKcEF6WuIQHWAbnZfW6oz19n7SDVRDU3g0hc3qOr4kPd/qfNGcTmcDDIgQ",
|
|
1667
|
-
"baseui.block-viewer.esm.js": "sha384-
|
|
1669
|
+
"baseui.block-viewer.esm.js": "sha384-XlIb3jDxt6Q75dbC4rLctTdPU6u0vJFcbxp9Gl6VKOkhjpz3wRBe0E3dWd80425L",
|
|
1670
|
+
"baseui.analytics.esm.js": "sha384-M6TVmchmceTvpSzgI+DNA6zAn9xmz9a+YEexCLslSo/Ngxyp2kmzs9fm+PxWy+Da",
|
|
1671
|
+
"baseui.analytics.min.js": "sha384-B0hBkVDbT7gmDmUMazlN4Nc1KnYGSpL+IcW2hd8840knx73HGImPgGs84MvS3hKE",
|
|
1668
1672
|
"icons/lucide.json": "sha384-mc5mzpmq9eOgZVa2IcjrL33Ty4nMXE8aoZMfy+lgkC/jMQ4aYnk2S58mFsSGFnW1",
|
|
1669
1673
|
"themes/baseui-light.css": "sha384-/TosvycgVJ5aZ7EOWd4cr8Zen4nayAC/iA9BgGdXKH0LuHSnZk4guDaWM5WPC7yy",
|
|
1670
1674
|
"themes/baseui-dark.css": "sha384-zF9Rhfjpv90sxuJ2WK7GBPCltN6mkebzK1w9EzQGY9DYHiX3eIX/fcE7fXzfIZVF"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Cookieless analytics service (ported from @plausible-analytics/tracker 0.4.6). */
|
|
2
|
+
export interface BaseUIAnalytics {
|
|
3
|
+
/** Sets up the tracker. Can be called once. */
|
|
4
|
+
init(config: BaseUIAnalyticsConfig): void;
|
|
5
|
+
/** Tracks an event; requires `init` to be called first. */
|
|
6
|
+
track(eventName: string, options?: BaseUIAnalyticsEventOptions): void;
|
|
7
|
+
/** File types tracked by default when `fileDownloads` is enabled. */
|
|
8
|
+
DEFAULT_FILE_TYPES: string[];
|
|
9
|
+
/** Coarse visitor setup (timezone, language, screen bucket); pass as `customProperties`. */
|
|
10
|
+
visitorContext(): BaseUIVisitorContext;
|
|
11
|
+
}
|
|
12
|
+
export interface BaseUIVisitorContext {
|
|
13
|
+
/** IANA timezone, e.g. "Europe/Paris" (browsers may report legacy aliases like "Asia/Saigon"). */
|
|
14
|
+
tz: string;
|
|
15
|
+
/** `navigator.language`, e.g. "vi-VN". */
|
|
16
|
+
lang: string;
|
|
17
|
+
/** Viewport width bucket: < 576, < 992, < 1440, otherwise Desktop. */
|
|
18
|
+
screen: "Mobile" | "Tablet" | "Laptop" | "Desktop";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BaseUIAnalyticsConfig {
|
|
22
|
+
/** Analytics backend origin, e.g. "https://stats.example.com" — events go to `<backendUrl><apiPath>`. Required unless `endpoint` is given. */
|
|
23
|
+
backendUrl?: string;
|
|
24
|
+
/** Path appended to `backendUrl`. Defaults to "/api/event". */
|
|
25
|
+
apiPath?: string;
|
|
26
|
+
/** Full event URL (http(s):// or a same-site /path); overrides `backendUrl` and `apiPath`. */
|
|
27
|
+
endpoint?: string;
|
|
28
|
+
/** Site name reported with every event. Defaults to `location.hostname`. */
|
|
29
|
+
domain?: string;
|
|
30
|
+
/** Capture pageviews automatically (incl. pushState/popstate). Defaults to true. */
|
|
31
|
+
autoCapturePageviews?: boolean;
|
|
32
|
+
/** Page uses hash-based routing. Defaults to false. */
|
|
33
|
+
hashBasedRouting?: boolean;
|
|
34
|
+
/** Track outbound link clicks. Defaults to false. */
|
|
35
|
+
outboundLinks?: boolean;
|
|
36
|
+
/** Track file downloads, optionally with a custom extension list. Defaults to false. */
|
|
37
|
+
fileDownloads?: boolean | { fileExtensions: string[] };
|
|
38
|
+
/** Track form submissions. Defaults to false. */
|
|
39
|
+
formSubmissions?: boolean;
|
|
40
|
+
/** Capture events on localhost. Defaults to false. */
|
|
41
|
+
captureOnLocalhost?: boolean;
|
|
42
|
+
/** Log ignored events to the console. Defaults to true. */
|
|
43
|
+
logging?: boolean;
|
|
44
|
+
/** Properties added to every event, or a function returning them per event name. */
|
|
45
|
+
customProperties?: Record<string, string> | ((eventName: string) => Record<string, string>);
|
|
46
|
+
/** Rewrite the payload before sending; return a falsy value to drop the event. */
|
|
47
|
+
transformRequest?: (payload: BaseUIAnalyticsPayload) => BaseUIAnalyticsPayload | null;
|
|
48
|
+
/** Bind the tracker to `window.thebase.track`. Defaults to true. */
|
|
49
|
+
bindToWindow?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BaseUIAnalyticsEventOptions {
|
|
53
|
+
props?: Record<string, string>;
|
|
54
|
+
/** Non-interactive events don't count towards bounce rate. Defaults to true. */
|
|
55
|
+
interactive?: boolean;
|
|
56
|
+
revenue?: { amount: number | string; currency: string };
|
|
57
|
+
/** Called with `{ status }`, `{ error }`, or undefined when the event was ignored. */
|
|
58
|
+
callback?: (result?: { status: number } | { error: unknown }) => void;
|
|
59
|
+
/** Overrides the page URL (defaults to `location.href`). */
|
|
60
|
+
url?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type BaseUIAnalyticsPayload = {
|
|
64
|
+
n: string;
|
|
65
|
+
u: string;
|
|
66
|
+
d: string;
|
|
67
|
+
r?: string | null;
|
|
68
|
+
p?: Record<string, string>;
|
|
69
|
+
$?: { amount: number | string; currency: string };
|
|
70
|
+
i?: boolean;
|
|
71
|
+
} & Record<string, unknown>;
|
|
72
|
+
|
|
73
|
+
export const DEFAULT_FILE_TYPES: string[];
|
|
74
|
+
export function init(config: BaseUIAnalyticsConfig): void;
|
|
75
|
+
export function track(eventName: string, options?: BaseUIAnalyticsEventOptions): void;
|
|
76
|
+
export function visitorContext(): BaseUIVisitorContext;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface BaseUIApi {
|
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
27
|
theme: { set(modeOrTokens: string | Record<string, string>): void };
|
|
28
|
+
/** Set by dist/baseui.analytics.min.js (the optional analytics service). */
|
|
29
|
+
analytics?: import("./analytics").BaseUIAnalytics;
|
|
30
|
+
/** Bound by `analytics.init()` (unless `bindToWindow: false`). */
|
|
31
|
+
track?: import("./analytics").BaseUIAnalytics["track"];
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export interface BaseUIThemeController {
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Analytics
|
|
2
|
+
|
|
3
|
+
BaseUI ships a cookieless web-analytics service, ported from [`@plausible-analytics/tracker`](https://www.npmjs.com/package/@plausible-analytics/tracker) 0.4.6 (MIT; notice in `LICENSE`). Nothing is collected until the page calls `init({ backendUrl })`. Available from `@thebase/ui` 1.1.0.
|
|
4
|
+
|
|
5
|
+
It is an optional, standalone feature: it is **not** part of `dist/baseui.min.js`/`dist/baseui.esm.js`, and it needs neither of them (no Owl, Bootstrap or component runtime). Load it on its own, with or without the rest of BaseUI (~5.6 KB minified):
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// npm / bundler
|
|
9
|
+
import { init, track } from "@thebase/ui/analytics";
|
|
10
|
+
init({ backendUrl: "https://stats.example.com" });
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<!-- ES module from the CDN (or map it as "@base/analytics" in an import map) -->
|
|
15
|
+
<script type="module">
|
|
16
|
+
import { init } from "https://cdn.jsdelivr.net/npm/@thebase/ui@1.1.1/dist/baseui.analytics.esm.js";
|
|
17
|
+
init({ backendUrl: "https://stats.example.com" });
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<!-- Classic script tag: sets window.thebase.analytics (load order vs dist/baseui.min.js doesn't matter) -->
|
|
21
|
+
<script src="https://cdn.jsdelivr.net/npm/@thebase/ui@1.1.1/dist/baseui.analytics.min.js"></script>
|
|
22
|
+
<script>
|
|
23
|
+
thebase.analytics.init({ backendUrl: "https://stats.example.com" });
|
|
24
|
+
</script>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Types ship as `dist/types/analytics.d.ts` (resolved automatically for `@thebase/ui/analytics`).
|
|
28
|
+
|
|
29
|
+
`init()` can be called once. It sends the first pageview, then follows `history.pushState`/`popstate` navigations (or `hashchange` with `hashBasedRouting: true`).
|
|
30
|
+
|
|
31
|
+
## Options
|
|
32
|
+
|
|
33
|
+
| Option | Default | Effect |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `backendUrl` | required¹ | Analytics backend origin, e.g. `https://stats.example.com`. Events are POSTed (`text/plain` JSON, no CORS preflight) to `<backendUrl><apiPath>`; a trailing `/` is fine. |
|
|
36
|
+
| `apiPath` | `/api/event` | Path appended to `backendUrl` (leading `/` optional), e.g. `/v1/collect`. |
|
|
37
|
+
| `endpoint` | — | Full event URL instead of `backendUrl` + `apiPath` (`http(s)://…` or a same-site `/path`, e.g. behind a proxy). |
|
|
38
|
+
| `domain` | `location.hostname` | Site name reported with every event. |
|
|
39
|
+
| `autoCapturePageviews` | `true` | Send pageviews automatically. |
|
|
40
|
+
| `hashBasedRouting` | `false` | Treat `#` changes as navigations. |
|
|
41
|
+
| `outboundLinks` | `false` | Track clicks on links to other hosts as `Outbound Link: Click`. |
|
|
42
|
+
| `fileDownloads` | `false` | Track download links as `File Download`; pass `{ fileExtensions: [...] }` to replace `analytics.DEFAULT_FILE_TYPES`. |
|
|
43
|
+
| `formSubmissions` | `false` | Track valid form submits as `Form: Submission`. |
|
|
44
|
+
| `captureOnLocalhost` | `false` | Also send from `localhost`/`127.x`/`file:`. |
|
|
45
|
+
| `logging` | `true` | `console.warn` when an event is ignored. |
|
|
46
|
+
| `customProperties` | — | Props added to every event (object, or `(eventName) => object`). |
|
|
47
|
+
| `transformRequest` | — | Rewrite the payload; return a falsy value to drop the event. |
|
|
48
|
+
| `bindToWindow` | `true` | Bind the tracker to `window.thebase.track`. |
|
|
49
|
+
|
|
50
|
+
¹ One of `backendUrl` or `endpoint` is required. `init()` throws if neither is given, or if the URL has no `http(s)://` scheme and doesn't start with `/` (e.g. `stats.example.com`), since that would silently post to a path on your own site.
|
|
51
|
+
|
|
52
|
+
Your backend should answer with `Access-Control-Allow-Origin` for your site. The event is delivered either way (there is no preflight), but without that header the browser hides the response and `callback` receives `{ error }`.
|
|
53
|
+
|
|
54
|
+
## Custom events
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
thebase.track("Signup", { props: { plan: "pro" }, revenue: { amount: 10, currency: "USD" } });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`track(name, options)` accepts `props`, `revenue`, `interactive: false` (excluded from bounce rate), `url` (override the page URL), and `callback(result)` — called with `{ status }`, `{ error }`, or `undefined` when the event was ignored.
|
|
61
|
+
|
|
62
|
+
Or tag markup with classes — no JavaScript needed:
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<button class="thebase-event-name=Signup thebase-event-plan=pro">Sign up</button>
|
|
66
|
+
<form class="thebase-event-name=Lead">...</form>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`+` in a class value becomes a space; `thebase-revenue-amount=10 thebase-revenue-currency=USD` attaches revenue.
|
|
70
|
+
|
|
71
|
+
## Visitor context
|
|
72
|
+
|
|
73
|
+
`visitorContext()` returns coarse facts about the visitor's setup. Pass it as `customProperties` to attach them to every event (available from 1.1.1):
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { init, visitorContext } from "@thebase/ui/analytics";
|
|
77
|
+
init({ backendUrl: "https://stats.example.com", customProperties: visitorContext });
|
|
78
|
+
// every event's props now include { tz: "Asia/Ho_Chi_Minh", lang: "vi-VN", screen: "Mobile" }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Prop | Value |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `tz` | `Intl.DateTimeFormat().resolvedOptions().timeZone`. Some browsers report legacy aliases (Chromium sends `Asia/Saigon`), so backends should canonicalize. |
|
|
84
|
+
| `lang` | `navigator.language`, e.g. `vi-VN`. |
|
|
85
|
+
| `screen` | Viewport width bucket: `Mobile` (< 576 px), `Tablet` (< 992), `Laptop` (< 1440), `Desktop`. |
|
|
86
|
+
|
|
87
|
+
The values are deliberately coarse: no exact pixels, nothing persistent. Backends can treat `tz`, `lang` and `screen` as visit dimensions instead of event props. To combine it with your own props, call it inside your function: `customProperties: (name) => ({ ...visitorContext(), plan: "pro" })`. The IP address and user agent never appear in the payload; the server reads them from the HTTP request (see [Receiving events](#receiving-events-backend)).
|
|
88
|
+
|
|
89
|
+
## Engagement
|
|
90
|
+
|
|
91
|
+
After each pageview the service measures time on screen and maximum scroll depth, and sends an `engagement` event (`e` = ms, `sd` = %) when the tab is hidden, loses focus, or navigates — only if the visitor scrolled further or spent at least 3 seconds since the last report.
|
|
92
|
+
|
|
93
|
+
## Ignored visits
|
|
94
|
+
|
|
95
|
+
Events are dropped (and `callback()` gets `undefined`) on localhost unless `captureOnLocalhost` is set, in automated browsers (`navigator.webdriver`, Cypress, PhantomJS, Nightmare — unless `window.__thebase` is set), or when `localStorage.thebase_ignore === "true"` (set that on your own devices to exclude them).
|
|
96
|
+
|
|
97
|
+
## Receiving events (backend)
|
|
98
|
+
|
|
99
|
+
Any HTTP server can collect events. It needs one route — `POST <backendUrl><apiPath>` (default `/api/event`) — that:
|
|
100
|
+
|
|
101
|
+
1. Reads the **raw body** as JSON. The tracker sends `Content-Type: text/plain` to avoid a CORS preflight, so framework helpers that only parse `application/json` bodies won't decode it.
|
|
102
|
+
2. Rejects bodies missing `n`, `u` or `d`, and caps the body size (events are well under 1 KB).
|
|
103
|
+
3. Answers quickly (`202 Accepted`, no body) with `Access-Control-Allow-Origin` for your site. Store or queue the event after you reply, or send it to a queue, so the visitor's browser never waits on the database.
|
|
104
|
+
|
|
105
|
+
### Payload
|
|
106
|
+
|
|
107
|
+
| Key | Type | Meaning |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| `n` | string | Event name: `pageview`, `engagement`, or a custom name (`Signup`, `Outbound Link: Click`, `File Download`, `Form: Submission`, …). |
|
|
110
|
+
| `u` | string | Page URL. |
|
|
111
|
+
| `d` | string | Site `domain`. |
|
|
112
|
+
| `r` | string \| null | `document.referrer` (pageviews and custom events). |
|
|
113
|
+
| `p` | object | Custom properties (`props` + `customProperties`); values are usually strings. |
|
|
114
|
+
| `$` | object | Revenue, e.g. `{ "amount": 10, "currency": "USD" }` (`amount` is a string when it comes from a class tag). |
|
|
115
|
+
| `i` | `false` | Present only for non-interactive events (excluded from bounce rate). |
|
|
116
|
+
| `h` | `1` | Present only with `hashBasedRouting`. |
|
|
117
|
+
| `sd` | number | `engagement` only: max scroll depth, %. |
|
|
118
|
+
| `e` | number | `engagement` only: engaged time, ms. |
|
|
119
|
+
| `v` | number | Tracker version (currently `36`). |
|
|
120
|
+
|
|
121
|
+
The payload carries no visitor ID or cookie. To count unique visitors, derive one on the server, e.g. `sha256(dailySalt + domain + clientIP + userAgent)`. Rotate the salt every day and never store the raw IP. If the server sits behind a proxy or load balancer, configure it to trust the proxy's `X-Forwarded-For` header so it sees the visitor's IP instead of the proxy's.
|
|
122
|
+
|
|
123
|
+
### Go — Fiber v3
|
|
124
|
+
|
|
125
|
+
Requires Go 1.25+ and `go get github.com/gofiber/fiber/v3` (tested with v3.5.0).
|
|
126
|
+
|
|
127
|
+
```go
|
|
128
|
+
package main
|
|
129
|
+
|
|
130
|
+
import (
|
|
131
|
+
"encoding/json"
|
|
132
|
+
"log"
|
|
133
|
+
|
|
134
|
+
"github.com/gofiber/fiber/v3"
|
|
135
|
+
"github.com/gofiber/fiber/v3/middleware/cors"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// Event is the compact payload POSTed by @thebase/ui/analytics.
|
|
139
|
+
type Event struct {
|
|
140
|
+
Name string `json:"n"`
|
|
141
|
+
URL string `json:"u"`
|
|
142
|
+
Domain string `json:"d"`
|
|
143
|
+
Referrer *string `json:"r"`
|
|
144
|
+
Props map[string]any `json:"p"`
|
|
145
|
+
Revenue map[string]any `json:"$"`
|
|
146
|
+
Interactive *bool `json:"i"`
|
|
147
|
+
HashRouting int `json:"h"`
|
|
148
|
+
ScrollDepth *int `json:"sd"`
|
|
149
|
+
EngagedMs *int64 `json:"e"`
|
|
150
|
+
Version int `json:"v"`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func main() {
|
|
154
|
+
app := fiber.New(fiber.Config{BodyLimit: 16 * 1024})
|
|
155
|
+
app.Use(cors.New(cors.Config{AllowOrigins: []string{"https://www.example.com"}}))
|
|
156
|
+
|
|
157
|
+
app.Post("/api/event", func(c fiber.Ctx) error {
|
|
158
|
+
// Body is text/plain, so decode it directly instead of c.Bind().JSON().
|
|
159
|
+
var ev Event
|
|
160
|
+
if err := json.Unmarshal(c.Body(), &ev); err != nil || ev.Name == "" || ev.URL == "" || ev.Domain == "" {
|
|
161
|
+
return c.SendStatus(fiber.StatusBadRequest)
|
|
162
|
+
}
|
|
163
|
+
log.Printf("%s %s %s ip=%s ua=%q", ev.Domain, ev.Name, ev.URL, c.IP(), c.Get(fiber.HeaderUserAgent))
|
|
164
|
+
// TODO: enqueue/store ev.
|
|
165
|
+
return c.SendStatus(fiber.StatusAccepted)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
log.Fatal(app.Listen(":8080"))
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Python — FastAPI
|
|
173
|
+
|
|
174
|
+
Requires Python 3.10+ and `pip install "fastapi[standard]"` (tested with FastAPI 0.142.2 / Pydantic 2.13). Run with `fastapi run main.py`.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from typing import Any
|
|
178
|
+
|
|
179
|
+
from fastapi import FastAPI, Request, Response
|
|
180
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
181
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
182
|
+
|
|
183
|
+
app = FastAPI()
|
|
184
|
+
app.add_middleware(CORSMiddleware, allow_origins=["https://www.example.com"], allow_methods=["POST"])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Event(BaseModel):
|
|
188
|
+
"""Compact payload POSTed by @thebase/ui/analytics."""
|
|
189
|
+
|
|
190
|
+
name: str = Field(alias="n", min_length=1)
|
|
191
|
+
url: str = Field(alias="u", min_length=1)
|
|
192
|
+
domain: str = Field(alias="d", min_length=1)
|
|
193
|
+
referrer: str | None = Field(None, alias="r")
|
|
194
|
+
props: dict[str, Any] | None = Field(None, alias="p")
|
|
195
|
+
revenue: dict[str, Any] | None = Field(None, alias="$")
|
|
196
|
+
interactive: bool | None = Field(None, alias="i")
|
|
197
|
+
hash_routing: int | None = Field(None, alias="h")
|
|
198
|
+
scroll_depth: int | None = Field(None, alias="sd")
|
|
199
|
+
engaged_ms: int | None = Field(None, alias="e")
|
|
200
|
+
version: int = Field(alias="v")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.post("/api/event", status_code=202)
|
|
204
|
+
async def collect(request: Request) -> Response:
|
|
205
|
+
# Body is text/plain, so parse the raw bytes instead of declaring `event: Event` as a parameter.
|
|
206
|
+
body = await request.body()
|
|
207
|
+
if len(body) > 16 * 1024:
|
|
208
|
+
return Response(status_code=413)
|
|
209
|
+
try:
|
|
210
|
+
event = Event.model_validate_json(body)
|
|
211
|
+
except ValidationError:
|
|
212
|
+
return Response(status_code=400)
|
|
213
|
+
ip = request.client.host if request.client else None
|
|
214
|
+
print(event.domain, event.name, event.url, ip, request.headers.get("user-agent"))
|
|
215
|
+
# TODO: enqueue/store event.
|
|
216
|
+
return Response(status_code=202)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Point the tracker at either server with `init({ backendUrl: "https://stats.example.com" })`. To smoke-test the server, run:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
curl -i -X POST http://localhost:8080/api/event -H 'Content-Type: text/plain' \
|
|
223
|
+
-d '{"n":"pageview","v":36,"u":"https://www.example.com/","d":"www.example.com","r":null}'
|
|
224
|
+
# HTTP/1.1 202 Accepted
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Sample dashboard
|
|
228
|
+
|
|
229
|
+
[`examples/analytics/`](../examples/analytics/dashboard/?backend=http://localhost:8080) contains a working dashboard plus two complete sample backends: [`server-go`](../examples/analytics/server-go/main.go) (Fiber v3) and [`server-fastapi`](../examples/analytics/server-fastapi/main.py). Each one extends the receivers above with an in-memory store, a daily-salted cookieless visitor hash, a `GET /api/stats` endpoint, and static hosting for the dashboard.
|
|
230
|
+
|
|
231
|
+
The dashboard ([`dashboard/`](../examples/analytics/dashboard/?backend=http://localhost:8080)) is a pure Owl app that loads `@thebase/ui@1.1.1` from jsDelivr: KPI `Card`s, an area `Chart`, top pages/sources/events `Table`s, site and period `NativeSelect`s, and a `ThemeButton`. It refreshes every 30 s and tracks its own visits through `@thebase/ui/analytics`, so a fresh backend has data straight away.
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
# Go (1.25+)
|
|
235
|
+
cd examples/analytics/server-go && DEMO_SEED=1 go run .
|
|
236
|
+
|
|
237
|
+
# or Python (3.10+)
|
|
238
|
+
cd examples/analytics/server-fastapi && pip install -r requirements.txt && DEMO_SEED=1 fastapi run main.py --port 8080
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Open <http://localhost:8080/>, the backend serves the dashboard itself. If you are browsing these docs from `npm run dev` (port 5173), the [dashboard link](../examples/analytics/dashboard/?backend=http://localhost:8080) also works once a backend is running on port 8080; until then it shows a "Can't load stats" message. `DEMO_SEED=1` preloads 30 days of synthetic traffic for `demo.example.com`. Other environment variables:
|
|
242
|
+
- `ADDR`: the Go listen address.
|
|
243
|
+
- `ALLOWED_ORIGINS`: a comma-separated CORS allow-list (default `*`).
|
|
244
|
+
- `DASHBOARD_DIR`: the folder served at `/`.
|
|
245
|
+
|
|
246
|
+
To serve the dashboard from somewhere else, open it with `?backend=https://stats.example.com`.
|
|
247
|
+
|
|
248
|
+
`GET /api/stats?domain=<site>&days=<1..90>` (default: the busiest site, 7 days, UTC) returns:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"domain": "demo.example.com",
|
|
253
|
+
"domains": ["demo.example.com", "www.example.com"],
|
|
254
|
+
"days": 7,
|
|
255
|
+
"totals": { "visitors": 643, "pageviews": 1582, "events": 124, "avgTimeSec": 62.7, "avgScrollDepth": 63.4 },
|
|
256
|
+
"timeseries": [{ "date": "2026-09-28", "visitors": 105, "pageviews": 261 }],
|
|
257
|
+
"topPages": [{ "name": "/pricing", "visitors": 296, "count": 373 }],
|
|
258
|
+
"topSources": [{ "name": "google.com", "visitors": 186, "count": 186 }],
|
|
259
|
+
"topEvents": [{ "name": "Signup", "visitors": 40, "count": 40 }]
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The sample servers are demos, not production stores:
|
|
264
|
+
- Events live in memory only and are lost on restart.
|
|
265
|
+
- `/api/stats` has no authentication.
|
|
266
|
+
|
|
267
|
+
Before going live, persist events (e.g. PostgreSQL or ClickHouse), put `/api/stats` behind auth, and restrict `ALLOWED_ORIGINS`.
|
package/docs/blocks.md
CHANGED
|
@@ -13,7 +13,7 @@ Blocks are not standalone runtime components. Paste the markup and load the norm
|
|
|
13
13
|
<script src="https://cdn.jsdelivr.net/npm/@thebase/ui@latest/dist/baseui.min.js" defer></script>
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`https://unpkg.com/@thebase/ui@latest/dist/...` mirrors the same files as an alternative CDN if jsDelivr is unreachable. Pin an exact version (e.g. `@1.
|
|
16
|
+
`https://unpkg.com/@thebase/ui@latest/dist/...` mirrors the same files as an alternative CDN if jsDelivr is unreachable. Pin an exact version (e.g. `@1.1.1`, or `@1` to take only non-breaking updates) instead of `@latest` for production. Blocks are static markup, so they only need the static `b-ui` runtime above — see [Usage Guide](usage-guide.md) if the surrounding page is itself an Owl app and you'd rather compose the equivalent layout from [Pure Owl Components](owl-components.md).
|
|
17
17
|
|
|
18
18
|
Use `b-block` to select the block style:
|
|
19
19
|
|
|
@@ -37,7 +37,7 @@ See [Pure Owl Components](/examples/blocks.html#/docs/guide/owl-components) for
|
|
|
37
37
|
|
|
38
38
|
## Enhanced usage
|
|
39
39
|
|
|
40
|
-
Call `
|
|
40
|
+
Call `thebase.mount(element)` or rely on `thebase.mountAll()` after the script loads.
|
|
41
41
|
|
|
42
42
|
## Options and attributes
|
|
43
43
|
|
|
@@ -45,7 +45,7 @@ See `dist/baseui.registry.json` for the supported attribute list.
|
|
|
45
45
|
|
|
46
46
|
## Methods
|
|
47
47
|
|
|
48
|
-
Use the global runtime methods: `
|
|
48
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
49
49
|
|
|
50
50
|
## Events
|
|
51
51
|
|
|
@@ -69,4 +69,4 @@ BaseUI targets modern evergreen browsers that support ES modules, CSS variables,
|
|
|
69
69
|
|
|
70
70
|
## Test checklist
|
|
71
71
|
|
|
72
|
-
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `
|
|
72
|
+
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
|
|
@@ -37,7 +37,7 @@ import { BlockViewer } from "@base/component";
|
|
|
37
37
|
</body>
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
`
|
|
40
|
+
`thebase.mountAll()` (called automatically on `DOMContentLoaded`) mounts the `b-ui="block-viewer"` element as a full Owl app, using the block's own inline template as its live preview and its serialized text as the "Code" panel.
|
|
41
41
|
|
|
42
42
|
## Options and attributes
|
|
43
43
|
|
|
@@ -49,4 +49,4 @@ See `dist/baseui.registry.json` for the full attribute list.
|
|
|
49
49
|
|
|
50
50
|
## Methods
|
|
51
51
|
|
|
52
|
-
Use the global runtime methods: `
|
|
52
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
@@ -36,7 +36,7 @@ See [Pure Owl Components](/examples/blocks.html#/docs/guide/owl-components) for
|
|
|
36
36
|
|
|
37
37
|
## Enhanced usage
|
|
38
38
|
|
|
39
|
-
Call `
|
|
39
|
+
Call `thebase.mount(element)` or rely on `thebase.mountAll()` after the script loads.
|
|
40
40
|
|
|
41
41
|
## Options and attributes
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ See `dist/baseui.registry.json` for the supported attribute list.
|
|
|
44
44
|
|
|
45
45
|
## Methods
|
|
46
46
|
|
|
47
|
-
Use the global runtime methods: `
|
|
47
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
48
48
|
|
|
49
49
|
## Events
|
|
50
50
|
|
|
@@ -68,4 +68,4 @@ BaseUI targets modern evergreen browsers that support ES modules, CSS variables,
|
|
|
68
68
|
|
|
69
69
|
## Test checklist
|
|
70
70
|
|
|
71
|
-
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `
|
|
71
|
+
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
|
|
@@ -86,7 +86,7 @@ The static adapter builds the `.carousel-inner`/`.carousel-item` wrapper structu
|
|
|
86
86
|
|
|
87
87
|
## Enhanced usage
|
|
88
88
|
|
|
89
|
-
Call `
|
|
89
|
+
Call `thebase.mount(element)` or rely on `thebase.mountAll()` after the script loads.
|
|
90
90
|
|
|
91
91
|
## Options and attributes
|
|
92
92
|
|
|
@@ -94,7 +94,7 @@ See `dist/baseui.registry.json` for the supported attribute list.
|
|
|
94
94
|
|
|
95
95
|
## Methods
|
|
96
96
|
|
|
97
|
-
Use the global runtime methods: `
|
|
97
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
98
98
|
|
|
99
99
|
## Events
|
|
100
100
|
|
|
@@ -118,4 +118,4 @@ BaseUI targets modern evergreen browsers that support ES modules, CSS variables,
|
|
|
118
118
|
|
|
119
119
|
## Test checklist
|
|
120
120
|
|
|
121
|
-
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `
|
|
121
|
+
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
|
package/docs/components/chart.md
CHANGED
|
@@ -94,7 +94,7 @@ See `examples/charts/area.html` for the local copy of the shadcn area chart gall
|
|
|
94
94
|
|
|
95
95
|
## Enhanced usage
|
|
96
96
|
|
|
97
|
-
Call `
|
|
97
|
+
Call `thebase.mount(element)` or rely on `thebase.mountAll()` after the script loads.
|
|
98
98
|
|
|
99
99
|
## Options and attributes
|
|
100
100
|
|
|
@@ -102,7 +102,7 @@ See `dist/baseui.registry.json` for the supported attribute list.
|
|
|
102
102
|
|
|
103
103
|
## Methods
|
|
104
104
|
|
|
105
|
-
Use the global runtime methods: `
|
|
105
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
106
106
|
|
|
107
107
|
## Events
|
|
108
108
|
|
|
@@ -126,4 +126,4 @@ BaseUI targets modern evergreen browsers that support ES modules, CSS variables,
|
|
|
126
126
|
|
|
127
127
|
## Test checklist
|
|
128
128
|
|
|
129
|
-
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `
|
|
129
|
+
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
|
|
@@ -43,7 +43,7 @@ See [Pure Owl Components](/examples/blocks.html#/docs/guide/owl-components) for
|
|
|
43
43
|
|
|
44
44
|
## Enhanced usage
|
|
45
45
|
|
|
46
|
-
Call `
|
|
46
|
+
Call `thebase.mount(element)` or rely on `thebase.mountAll()` after the script loads.
|
|
47
47
|
|
|
48
48
|
## Options and attributes
|
|
49
49
|
|
|
@@ -51,7 +51,7 @@ See `dist/baseui.registry.json` for the supported attribute list.
|
|
|
51
51
|
|
|
52
52
|
## Methods
|
|
53
53
|
|
|
54
|
-
Use the global runtime methods: `
|
|
54
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
55
55
|
|
|
56
56
|
## Events
|
|
57
57
|
|
|
@@ -75,4 +75,4 @@ BaseUI targets modern evergreen browsers that support ES modules, CSS variables,
|
|
|
75
75
|
|
|
76
76
|
## Test checklist
|
|
77
77
|
|
|
78
|
-
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `
|
|
78
|
+
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
|
|
@@ -46,7 +46,7 @@ See [Pure Owl Components](/examples/blocks.html#/docs/guide/owl-components) for
|
|
|
46
46
|
|
|
47
47
|
## Enhanced usage
|
|
48
48
|
|
|
49
|
-
Call `
|
|
49
|
+
Call `thebase.mount(element)` or rely on `thebase.mountAll()` after the script loads.
|
|
50
50
|
|
|
51
51
|
## Options and attributes
|
|
52
52
|
|
|
@@ -54,7 +54,7 @@ See `dist/baseui.registry.json` for the supported attribute list.
|
|
|
54
54
|
|
|
55
55
|
## Methods
|
|
56
56
|
|
|
57
|
-
Use the global runtime methods: `
|
|
57
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
58
58
|
|
|
59
59
|
## Events
|
|
60
60
|
|
|
@@ -78,4 +78,4 @@ BaseUI targets modern evergreen browsers that support ES modules, CSS variables,
|
|
|
78
78
|
|
|
79
79
|
## Test checklist
|
|
80
80
|
|
|
81
|
-
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `
|
|
81
|
+
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
|
|
@@ -48,7 +48,7 @@ See [Pure Owl Components](/examples/blocks.html#/docs/guide/owl-components) for
|
|
|
48
48
|
|
|
49
49
|
## Enhanced usage
|
|
50
50
|
|
|
51
|
-
Call `
|
|
51
|
+
Call `thebase.mount(element)` or rely on `thebase.mountAll()` after the script loads.
|
|
52
52
|
|
|
53
53
|
## Options and attributes
|
|
54
54
|
|
|
@@ -56,7 +56,7 @@ See `dist/baseui.registry.json` for the supported attribute list.
|
|
|
56
56
|
|
|
57
57
|
## Methods
|
|
58
58
|
|
|
59
|
-
Use the global runtime methods: `
|
|
59
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
60
60
|
|
|
61
61
|
## Events
|
|
62
62
|
|
|
@@ -80,4 +80,4 @@ BaseUI targets modern evergreen browsers that support ES modules, CSS variables,
|
|
|
80
80
|
|
|
81
81
|
## Test checklist
|
|
82
82
|
|
|
83
|
-
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `
|
|
83
|
+
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
|
|
@@ -74,7 +74,7 @@ See [Pure Owl Components](/examples/blocks.html#/docs/guide/owl-components) for
|
|
|
74
74
|
|
|
75
75
|
## Enhanced usage
|
|
76
76
|
|
|
77
|
-
Call `
|
|
77
|
+
Call `thebase.mount(element)` or rely on `thebase.mountAll()` after the script loads.
|
|
78
78
|
|
|
79
79
|
## Options and attributes
|
|
80
80
|
|
|
@@ -82,7 +82,7 @@ See `dist/baseui.registry.json` for the supported attribute list.
|
|
|
82
82
|
|
|
83
83
|
## Methods
|
|
84
84
|
|
|
85
|
-
Use the global runtime methods: `
|
|
85
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
86
86
|
|
|
87
87
|
## Events
|
|
88
88
|
|
|
@@ -154,4 +154,4 @@ BaseUI targets modern evergreen browsers that support ES modules, CSS variables,
|
|
|
154
154
|
|
|
155
155
|
## Test checklist
|
|
156
156
|
|
|
157
|
-
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `
|
|
157
|
+
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
|
|
@@ -36,7 +36,7 @@ See [Pure Owl Components](/examples/blocks.html#/docs/guide/owl-components) for
|
|
|
36
36
|
|
|
37
37
|
## Enhanced usage
|
|
38
38
|
|
|
39
|
-
Call `
|
|
39
|
+
Call `thebase.mount(element)` or rely on `thebase.mountAll()` after the script loads.
|
|
40
40
|
|
|
41
41
|
## Options and attributes
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ See `dist/baseui.registry.json` for the supported attribute list.
|
|
|
44
44
|
|
|
45
45
|
## Methods
|
|
46
46
|
|
|
47
|
-
Use the global runtime methods: `
|
|
47
|
+
Use the global runtime methods: `thebase.mount()`, `thebase.mountAll()`, and `thebase.destroy()`.
|
|
48
48
|
|
|
49
49
|
## Events
|
|
50
50
|
|
|
@@ -68,4 +68,4 @@ BaseUI targets modern evergreen browsers that support ES modules, CSS variables,
|
|
|
68
68
|
|
|
69
69
|
## Test checklist
|
|
70
70
|
|
|
71
|
-
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `
|
|
71
|
+
Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
|
package/docs/components/icon.md
CHANGED
|
@@ -46,8 +46,8 @@ For install patterns, accessibility guidance, self-hosting, and JavaScript examp
|
|
|
46
46
|
## JavaScript API
|
|
47
47
|
|
|
48
48
|
```js
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
thebase.icons.lucide.url("panel-left");
|
|
50
|
+
thebase.icons.lucide.render(document.querySelector("#icon"), "check", { size: "18px" });
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
By default, icon URLs point to `https://cdn.jsdelivr.net/npm/lucide-static@1.31.0/icons`. The build also publishes every SVG under `dist/icons/lucide/` plus a generated `dist/icons/lucide.json` manifest for self-hosted CDN deployments.
|