@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +28 -0
  3. package/README.md +21 -6
  4. package/dist/baseui.analytics.esm.js +1 -0
  5. package/dist/baseui.analytics.min.js +1 -0
  6. package/dist/baseui.esm.js +9 -9
  7. package/dist/baseui.icons.esm.js +1 -1
  8. package/dist/baseui.min.js +9 -9
  9. package/dist/baseui.registry.json +8 -4
  10. package/dist/types/analytics.d.ts +76 -0
  11. package/dist/types/index.d.ts +4 -0
  12. package/docs/analytics.md +267 -0
  13. package/docs/blocks.md +1 -1
  14. package/docs/components/avatar.md +3 -3
  15. package/docs/components/block-viewer.md +2 -2
  16. package/docs/components/calendar.md +3 -3
  17. package/docs/components/carousel.md +3 -3
  18. package/docs/components/chart.md +3 -3
  19. package/docs/components/combobox.md +3 -3
  20. package/docs/components/command.md +3 -3
  21. package/docs/components/context-menu.md +3 -3
  22. package/docs/components/data-table.md +3 -3
  23. package/docs/components/date-picker.md +3 -3
  24. package/docs/components/icon.md +2 -2
  25. package/docs/components/input-otp.md +3 -3
  26. package/docs/components/input.md +2 -2
  27. package/docs/components/markdown.md +2 -2
  28. package/docs/components/message-scroller.md +3 -3
  29. package/docs/components/message.md +3 -3
  30. package/docs/components/popover.md +3 -3
  31. package/docs/components/questionnaire.md +3 -3
  32. package/docs/components/resizable.md +3 -3
  33. package/docs/components/scroll-area.md +3 -3
  34. package/docs/components/text-editor.md +1 -1
  35. package/docs/components/theme-button.md +2 -2
  36. package/docs/components/toggle-group.md +3 -3
  37. package/docs/components/toggle.md +3 -3
  38. package/docs/components/tooltip.md +3 -3
  39. package/docs/icons.md +5 -5
  40. package/docs/installation.md +4 -3
  41. package/docs/owl-components.md +2 -2
  42. package/docs/registry.md +2 -1
  43. package/docs/runtime-api.md +16 -12
  44. package/docs/theming.md +4 -4
  45. package/docs/usage-guide.md +15 -15
  46. 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-dgJ6Mbk5+apdFpV1iU/Lnp4CQWZXgBpsUn3r7oh8DHH2rNqIqNmsHKPVub5ZEq+D",
1664
- "baseui.esm.js": "sha384-AP0mGX4n1avSDWoAUakm71/JAR0vvd6n2qR39RI8nssDriB8lhN4wXZijA6XfVeJ",
1665
- "baseui.icons.esm.js": "sha384-M30zHGB1h8ys1b2YBuLR7J3VzcYqft9L+wVI/EM1P8h8fY5Y2h1woOQzyE5DZ+lp",
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-rQ7YMxLWDA+ccC/JwTcOV8DKUjZTZofqJwNX8xrKTpZbsBVRa4taXLOLpRQ6poR4",
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;
@@ -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.0.0`, 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).
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 `BaseUI.mount(element)` or rely on `BaseUI.mountAll()` after the script loads.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.destroy()`.
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
- `BaseUI.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.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.mount(element)` or rely on `BaseUI.mountAll()` after the script loads.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.destroy()`.
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 `BaseUI.mount(element)` or rely on `BaseUI.mountAll()` after the script loads.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.destroy()`.
121
+ Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
@@ -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 `BaseUI.mount(element)` or rely on `BaseUI.mountAll()` after the script loads.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.destroy()`.
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 `BaseUI.mount(element)` or rely on `BaseUI.mountAll()` after the script loads.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.destroy()`.
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 `BaseUI.mount(element)` or rely on `BaseUI.mountAll()` after the script loads.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.destroy()`.
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 `BaseUI.mount(element)` or rely on `BaseUI.mountAll()` after the script loads.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.destroy()`.
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 `BaseUI.mount(element)` or rely on `BaseUI.mountAll()` after the script loads.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.destroy()`.
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 `BaseUI.mount(element)` or rely on `BaseUI.mountAll()` after the script loads.
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: `BaseUI.mount()`, `BaseUI.mountAll()`, and `BaseUI.destroy()`.
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 `BaseUI.destroy()`.
71
+ Verify light and dark themes, keyboard access for focusable controls, disabled or readonly states when applicable, and cleanup through `thebase.destroy()`.
@@ -46,8 +46,8 @@ For install patterns, accessibility guidance, self-hosting, and JavaScript examp
46
46
  ## JavaScript API
47
47
 
48
48
  ```js
49
- BaseUI.icons.lucide.url("panel-left");
50
- BaseUI.icons.lucide.render(document.querySelector("#icon"), "check", { size: "18px" });
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.