wiki-formant 0.40.0 → 0.41.0

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/README.md CHANGED
@@ -732,7 +732,8 @@ return <Stats digest={await digest(sql, q)} {...q} />;
732
732
  own subpath, because it imports the optional peer `@simplewebauthn/browser`.
733
733
  `Stats` (`wiki-formant/stats`) renders a `digest` as one page: the
734
734
  headline figures against the window before, one of them per day, every non-empty
735
- ranked list, and `?days=` links between windows, all time among them. Each
735
+ ranked list, and `?days=` links between windows, all time among them, pinned in
736
+ the chart's frame as its own buttons are (`TimeChart`'s `controls`). Each
736
737
  figure is a button that charts it: visitors, page views, bounce rate, visit
737
738
  length, visitors with agents and clicks to X. `digest`'s `by_day` carries every
738
739
  one of those per UTC day, a visit on the day it began, so the days add up to the
package/dist/base.css CHANGED
@@ -190,15 +190,14 @@ pre:hover > .code-copy-btn,
190
190
  gap: 0.5rem;
191
191
  }
192
192
  .chart-controls > div { display: flex; gap: 2px; }
193
- .chart-controls button { font: inherit; color: inherit; background: none; cursor: pointer; opacity: 0.6; }
194
- .chart-controls button[aria-pressed='true'] { opacity: 1; }
193
+ .chart-controls :is(button, a) { font: inherit; color: inherit; text-decoration: none; background: none; cursor: pointer; opacity: 0.6; }
194
+ /* A button is pressed; a link (a stats page's window) is the page shown. */
195
+ .chart-controls :is([aria-pressed='true'], [aria-current]) { opacity: 1; }
195
196
 
196
197
  /* ---- Stats ----------------------------------------------------------------
197
198
  `--share` is a row's value over the largest in its list, 0 to 1. Bars take
198
199
  `currentColor` so they show before a stylesheet colours them. */
199
200
  .stats { display: grid; gap: 2rem; }
200
- .stats-ranges { display: flex; flex-wrap: wrap; gap: 1rem; }
201
- .stats-ranges a[aria-current] { text-decoration: underline; text-underline-offset: 0.3em; }
202
201
  /* Each figure is a button that charts it; pressed is the one drawn. */
203
202
  .stats-figures {
204
203
  display: grid;
package/dist/chart.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type ReactNode } from 'react';
1
2
  /**
2
3
  * One value at a time in Unix seconds, UTC. A price may carry the rest of its
3
4
  * candle, `value` being the close, and the volume traded in it.
@@ -52,6 +53,11 @@ export interface TimeChartProps {
52
53
  candles?: boolean;
53
54
  /** In pixels. Otherwise the stylesheet's `--chart-height`, else 15rem. */
54
55
  height?: number;
56
+ /**
57
+ * Groups pinned before the chart's own, styled as its buttons: a stats page's
58
+ * window links, which load another digest rather than slice this one.
59
+ */
60
+ controls?: ReactNode;
55
61
  className?: string;
56
62
  }
57
63
  /**
@@ -59,4 +65,4 @@ export interface TimeChartProps {
59
65
  * `.chart-controls button[aria-pressed]`, and a site's stylesheet gives them
60
66
  * and the box their look; the line and fill are the canvas box's `color`.
61
67
  */
62
- export declare function TimeChart({ series, label, ranges, range: initial, aggregate, fromZero, format, candles: initialCandles, height, className, }: TimeChartProps): import("react").JSX.Element;
68
+ export declare function TimeChart({ series, label, ranges, range: initial, aggregate, fromZero, format, candles: initialCandles, height, controls, className, }: TimeChartProps): import("react").JSX.Element;
package/dist/chart.js CHANGED
@@ -101,7 +101,7 @@ const fromZeroScale = (base) => {
101
101
  * `.chart-controls button[aria-pressed]`, and a site's stylesheet gives them
102
102
  * and the box their look; the line and fill are the canvas box's `color`.
103
103
  */
104
- export function TimeChart({ series, label, ranges = typeof series === 'function' ? CHART_RANGES : DAILY_RANGES, range: initial = ranges.includes('30d') ? '30d' : ranges[0], aggregate = 'last', fromZero = false, format = formatChartValue, candles: initialCandles = false, height, className, }) {
104
+ export function TimeChart({ series, label, ranges = typeof series === 'function' ? CHART_RANGES : DAILY_RANGES, range: initial = ranges.includes('30d') ? '30d' : ranges[0], aggregate = 'last', fromZero = false, format = formatChartValue, candles: initialCandles = false, height, controls, className, }) {
105
105
  const box = useRef(null);
106
106
  const chart = useRef(null);
107
107
  const formatRef = useRef(format);
@@ -261,5 +261,5 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
261
261
  c.api.timeScale().fitContent();
262
262
  }, [ready, source, view, candles]);
263
263
  const status = source && 'error' in source ? source.error : points && !view?.shown.length ? 'No data in this range' : null;
264
- return (_jsxs("figure", { className: className ? `chart ${className}` : 'chart', children: [_jsx("div", { ref: box, className: "chart-canvas", role: "img", "aria-label": label, style: height ? { height } : undefined }), (!ready || (!points && !status)) && _jsx("div", { className: "chart-status skeleton", "aria-hidden": true }), status && _jsx("p", { className: "chart-status", children: status }), (ranges.length > 1 || (view && (view.steps.length > 1 || view.ohlc))) && (_jsxs("div", { className: "chart-controls", children: [ranges.length > 1 && (_jsx("div", { role: "group", "aria-label": "Range", children: ranges.map(r => (_jsx("button", { type: "button", "aria-pressed": r === range, onClick: () => { setRange(r); setStep(null); }, children: RANGE[r].label }, r))) })), view && view.steps.length > 1 && (_jsx("div", { role: "group", "aria-label": "Step", children: view.steps.map(s => (_jsx("button", { type: "button", "aria-pressed": s === view.step, onClick: () => setStep(s), title: `One point per ${s}`, children: STEP[s].label }, s))) })), view?.ohlc && (_jsxs("div", { role: "group", "aria-label": "Style", children: [_jsx("button", { type: "button", "aria-pressed": !candles, onClick: () => setCandles(false), children: "Line" }), _jsx("button", { type: "button", "aria-pressed": candles, onClick: () => setCandles(true), children: "Candles" })] }))] }))] }));
264
+ return (_jsxs("figure", { className: className ? `chart ${className}` : 'chart', children: [_jsx("div", { ref: box, className: "chart-canvas", role: "img", "aria-label": label, style: height ? { height } : undefined }), (!ready || (!points && !status)) && _jsx("div", { className: "chart-status skeleton", "aria-hidden": true }), status && _jsx("p", { className: "chart-status", children: status }), (controls || ranges.length > 1 || (view && (view.steps.length > 1 || view.ohlc))) && (_jsxs("div", { className: "chart-controls", children: [controls, ranges.length > 1 && (_jsx("div", { role: "group", "aria-label": "Range", children: ranges.map(r => (_jsx("button", { type: "button", "aria-pressed": r === range, onClick: () => { setRange(r); setStep(null); }, children: RANGE[r].label }, r))) })), view && view.steps.length > 1 && (_jsx("div", { role: "group", "aria-label": "Step", children: view.steps.map(s => (_jsx("button", { type: "button", "aria-pressed": s === view.step, onClick: () => setStep(s), title: `One point per ${s}`, children: STEP[s].label }, s))) })), view?.ohlc && (_jsxs("div", { role: "group", "aria-label": "Style", children: [_jsx("button", { type: "button", "aria-pressed": !candles, onClick: () => setCandles(false), children: "Line" }), _jsx("button", { type: "button", "aria-pressed": candles, onClick: () => setCandles(true), children: "Candles" })] }))] }))] }));
265
265
  }
@@ -1,10 +1,12 @@
1
+ import { type ReactNode } from 'react';
1
2
  import type { Digest } from './analytics.js';
2
3
  /**
3
4
  * The figures as `.stats-figures button[aria-pressed]` and the pressed one per
4
5
  * day from `first` (a UTC day number) to today. A count's empty day is a zero;
5
6
  * a rate's is left out, since no visits is not a bounce rate of nothing.
6
7
  */
7
- export declare function StatsFigures({ digest, first }: {
8
+ export declare function StatsFigures({ digest, first, windows }: {
8
9
  digest: Digest;
9
10
  first: number;
11
+ windows: ReactNode;
10
12
  }): import("react").JSX.Element;
@@ -41,7 +41,7 @@ const EMPTY = { date: '', visitors: 0, pageviews: 0, visits: 0, bounces: 0, seco
41
41
  * day from `first` (a UTC day number) to today. A count's empty day is a zero;
42
42
  * a rate's is left out, since no visits is not a bounce rate of nothing.
43
43
  */
44
- export function StatsFigures({ digest, first }) {
44
+ export function StatsFigures({ digest, first, windows }) {
45
45
  const figures = useMemo(() => figuresOf(digest), [digest]);
46
46
  const [chosen, choose] = useState(0);
47
47
  const figure = figures[chosen] ?? figures[0];
@@ -54,5 +54,5 @@ export function StatsFigures({ digest, first }) {
54
54
  return value == null ? null : { time, value };
55
55
  }).filter(p => p !== null);
56
56
  }, [byDay, first, figure]);
57
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "stats-figures", role: "group", "aria-label": "Chart", children: figures.map((f, i) => (_jsxs("button", { type: "button", "aria-pressed": i === chosen, onClick: () => choose(i), children: [_jsx("span", { children: f.label }), _jsxs("strong", { children: [f.value, f.delta && _jsx("small", { children: ` ${f.delta}` })] })] }, f.label))) }), _jsx(TimeChart, { series: series, label: `${figure.label} per day`, ranges: ['all'], aggregate: "mean", fromZero: true, format: figure.format, className: "stats-chart" })] }));
57
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "stats-figures", role: "group", "aria-label": "Chart", children: figures.map((f, i) => (_jsxs("button", { type: "button", "aria-pressed": i === chosen, onClick: () => choose(i), children: [_jsx("span", { children: f.label }), _jsxs("strong", { children: [f.value, f.delta && _jsx("small", { children: ` ${f.delta}` })] })] }, f.label))) }), _jsx(TimeChart, { series: series, label: `${figure.label} per day`, ranges: ['all'], aggregate: "mean", fromZero: true, format: figure.format, controls: windows, className: "stats-chart" })] }));
58
58
  }
package/dist/stats.js CHANGED
@@ -5,6 +5,8 @@ import { StatsFigures } from './stats-figures.js';
5
5
  export const STATS_RANGES = [1, 7, 30, 90, 365, Infinity];
6
6
  /** A window as its `?days=` value. */
7
7
  const daysParam = (n) => (Number.isFinite(n) ? String(n) : 'all');
8
+ /** A window as `TimeChart` names its ranges, since its buttons are where the windows sit. */
9
+ const windowLabel = (n) => (n === 1 ? '24H' : n === 365 ? '1Y' : Number.isFinite(n) ? `${n}D` : 'All');
8
10
  /** A `?days=` value as one of `ranges`, else `fallback`. */
9
11
  export const statsDays = (param, ranges = STATS_RANGES, fallback = 30) => ranges.find(n => daysParam(n) === param) ?? fallback;
10
12
  /**
@@ -67,5 +69,8 @@ export function Stats({ digest: d, days, filter = {}, ranges = STATS_RANGES }) {
67
69
  : Number.isFinite(start) ? start : Math.min(today, ...d.by_day.map(r => Date.parse(r.date) / DAY_MS));
68
70
  const rows = (list, k, m) => list.map(r => [r[k], r[m]]);
69
71
  const to = (k) => (v) => href(days, { ...filter, [k]: v });
70
- return (_jsxs("div", { className: "stats", children: [_jsx("nav", { className: "stats-ranges", "aria-label": "Window", children: ranges.map(n => (_jsx("a", { href: href(n, filter), "aria-current": n === days ? 'page' : undefined, children: n === 1 ? '24 hours' : Number.isFinite(n) ? `${n} days` : 'All time' }, n))) }), narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs("a", { href: href(days, { ...filter, [k]: undefined }), title: "Remove", children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => href(days, { ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events') })] })] }));
72
+ // Each window is another digest, so a link rather than a button, pinned
73
+ // with the chart's own controls.
74
+ const windows = (_jsx("div", { role: "group", "aria-label": "Window", children: ranges.map(n => (_jsx("a", { href: href(n, filter), "aria-current": n === days ? 'page' : undefined, children: windowLabel(n) }, n))) }));
75
+ return (_jsxs("div", { className: "stats", children: [narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs("a", { href: href(days, { ...filter, [k]: undefined }), title: "Remove", children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first, windows: windows }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => href(days, { ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events') })] })] }));
71
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
5
5
  "type": "module",
6
6
  "license": "MIT",