@event-timeline/react 0.1.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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/index.cjs +234 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +220 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Karl Gorgoglione
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @event-timeline/react
|
|
2
|
+
|
|
3
|
+
React binding for [`@event-timeline/core`](https://www.npmjs.com/package/@event-timeline/core)
|
|
4
|
+
— a Canvas-based, domain-agnostic timeline that renders **elements** (horizontal
|
|
5
|
+
lanes) and **events** (directed arrows) and stays smooth at large scale.
|
|
6
|
+
|
|
7
|
+
A thin binding: it owns the two `<canvas>` layers, the `ResizeObserver`, and the
|
|
8
|
+
HTML tooltip overlay, and syncs props to the engine imperatively so React stays
|
|
9
|
+
out of the render loop. See
|
|
10
|
+
[`docs/DESIGN.md`](https://github.com/KarlGorgoglione/event-timeline/blob/main/docs/DESIGN.md)
|
|
11
|
+
§12 for the contract.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @event-timeline/react @event-timeline/core react react-dom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`react` and `react-dom` are **peer dependencies** (`>=18`, i.e. React 18 & 19).
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { useMemo, useRef } from 'react';
|
|
25
|
+
import { Timeline, type TimelineHandle } from '@event-timeline/react';
|
|
26
|
+
|
|
27
|
+
function MyTimeline({ elements, events }) {
|
|
28
|
+
const ref = useRef<TimelineHandle>(null);
|
|
29
|
+
|
|
30
|
+
// Memoize props by reference — the binding diffs them and re-applies only on
|
|
31
|
+
// change (passing fresh inline objects each render re-runs setData; §12).
|
|
32
|
+
const data = useMemo(() => ({ elements, events }), [elements, events]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Timeline
|
|
36
|
+
ref={ref}
|
|
37
|
+
data={data}
|
|
38
|
+
options={{ multiSelect: true }}
|
|
39
|
+
onHover={(hit) => {
|
|
40
|
+
/* typed discriminated union: element | event | cluster | null */
|
|
41
|
+
}}
|
|
42
|
+
onSelectionChange={(ids) => {}}
|
|
43
|
+
style={{ width: '100%', height: 480 }}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Imperative handle
|
|
50
|
+
|
|
51
|
+
The `ref` exposes camera and streaming controls (§12): `fit()`, `panToTime(t)`,
|
|
52
|
+
`zoomToRange(start, end)`, `select(ids)`, `getViewport()`, `resize(w, h, dpr)`,
|
|
53
|
+
and the live-update methods `setData` / `addEvents` / `removeEvents` /
|
|
54
|
+
`addElements` / `updateElement`. For high-frequency streams, call these directly
|
|
55
|
+
to bypass React reconciliation and get incremental index updates.
|
|
56
|
+
|
|
57
|
+
### Props
|
|
58
|
+
|
|
59
|
+
Every engine event has a matching `on*` callback prop (`onHover`, `onClick`,
|
|
60
|
+
`onSelectionChange`, `onViewportChange`, `onDataChange`, `onReady`). Customise
|
|
61
|
+
via `theme` (`Partial<Theme>`), `styleResolvers`, `formatters`, and `options`
|
|
62
|
+
(`layout`, `clustering`, `lod`, `multiSelect`, `streamingLayout`,
|
|
63
|
+
`onDiagnostic`, `onFrameStats`, …). The component is generic — `Timeline<TData>`
|
|
64
|
+
threads your opaque `data` payload back through every callback. It is
|
|
65
|
+
StrictMode-safe and SSR-safe (the engine is created in an effect).
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
[MIT](https://github.com/KarlGorgoglione/event-timeline/blob/main/LICENSE) ©
|
|
70
|
+
Karl Gorgoglione
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var core = require('@event-timeline/core');
|
|
5
|
+
var reactDom = require('react-dom');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/Timeline.tsx
|
|
9
|
+
function useTimeline(args) {
|
|
10
|
+
const { baseRef, overlayRef, containerRef, data, theme, options } = args;
|
|
11
|
+
const { styleResolvers, formatters } = args;
|
|
12
|
+
const engineRef = react.useRef(null);
|
|
13
|
+
const callbacks = react.useRef(args);
|
|
14
|
+
callbacks.current = args;
|
|
15
|
+
const dataRef = react.useRef(data);
|
|
16
|
+
dataRef.current = data;
|
|
17
|
+
const appliedData = react.useRef(void 0);
|
|
18
|
+
react.useEffect(() => {
|
|
19
|
+
const base = baseRef.current;
|
|
20
|
+
const overlay = overlayRef.current;
|
|
21
|
+
const container = containerRef.current;
|
|
22
|
+
if (base === null || overlay === null || container === null) return;
|
|
23
|
+
const engine = new core.TimelineEngine(base, overlay, {
|
|
24
|
+
theme,
|
|
25
|
+
onDiagnostic: options?.onDiagnostic,
|
|
26
|
+
layout: options?.layout,
|
|
27
|
+
layoutDebounceMs: options?.layoutDebounceMs,
|
|
28
|
+
streamingLayout: options?.streamingLayout,
|
|
29
|
+
multiSelect: options?.multiSelect,
|
|
30
|
+
clustering: options?.clustering,
|
|
31
|
+
lod: options?.lod,
|
|
32
|
+
onFrameStats: options?.onFrameStats,
|
|
33
|
+
styleResolvers,
|
|
34
|
+
formatters
|
|
35
|
+
});
|
|
36
|
+
engineRef.current = engine;
|
|
37
|
+
const off = [
|
|
38
|
+
engine.on("ready", () => callbacks.current.onReady?.()),
|
|
39
|
+
engine.on(
|
|
40
|
+
"viewportChange",
|
|
41
|
+
(v) => callbacks.current.onViewportChange?.(v)
|
|
42
|
+
),
|
|
43
|
+
engine.on("dataChange", (d) => callbacks.current.onDataChange?.(d)),
|
|
44
|
+
engine.on("hover", (hit) => callbacks.current.onHover?.(hit)),
|
|
45
|
+
engine.on("click", (e) => callbacks.current.onClick?.(e)),
|
|
46
|
+
engine.on(
|
|
47
|
+
"selectionChange",
|
|
48
|
+
(s) => callbacks.current.onSelectionChange?.(s.ids)
|
|
49
|
+
)
|
|
50
|
+
];
|
|
51
|
+
const applySize = (cssW, cssH) => engine.resize(cssW, cssH, window.devicePixelRatio || 1);
|
|
52
|
+
const observer = new ResizeObserver((entries) => {
|
|
53
|
+
const rect2 = entries[0]?.contentRect;
|
|
54
|
+
if (rect2) applySize(rect2.width, rect2.height);
|
|
55
|
+
});
|
|
56
|
+
observer.observe(container);
|
|
57
|
+
const rect = container.getBoundingClientRect();
|
|
58
|
+
applySize(rect.width, rect.height);
|
|
59
|
+
if (dataRef.current) {
|
|
60
|
+
engine.setData(dataRef.current);
|
|
61
|
+
appliedData.current = dataRef.current;
|
|
62
|
+
}
|
|
63
|
+
return () => {
|
|
64
|
+
for (const unsubscribe of off) unsubscribe();
|
|
65
|
+
observer.disconnect();
|
|
66
|
+
engine.dispose();
|
|
67
|
+
engineRef.current = null;
|
|
68
|
+
appliedData.current = void 0;
|
|
69
|
+
};
|
|
70
|
+
}, [
|
|
71
|
+
baseRef,
|
|
72
|
+
overlayRef,
|
|
73
|
+
containerRef,
|
|
74
|
+
theme,
|
|
75
|
+
options,
|
|
76
|
+
styleResolvers,
|
|
77
|
+
formatters
|
|
78
|
+
]);
|
|
79
|
+
react.useEffect(() => {
|
|
80
|
+
const engine = engineRef.current;
|
|
81
|
+
if (engine === null || data === appliedData.current) return;
|
|
82
|
+
if (data) engine.setData(data);
|
|
83
|
+
appliedData.current = data;
|
|
84
|
+
}, [data]);
|
|
85
|
+
return engineRef;
|
|
86
|
+
}
|
|
87
|
+
function defaultContent(hit) {
|
|
88
|
+
switch (hit.kind) {
|
|
89
|
+
case "event":
|
|
90
|
+
return hit.event.label ?? `${hit.event.sourceId} \u2192 ${hit.event.targetId}`;
|
|
91
|
+
case "element":
|
|
92
|
+
return hit.element.label;
|
|
93
|
+
case "cluster":
|
|
94
|
+
return `${hit.count} events`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var tooltipStyle = {
|
|
98
|
+
position: "fixed",
|
|
99
|
+
pointerEvents: "none",
|
|
100
|
+
zIndex: 2147483647,
|
|
101
|
+
padding: "4px 8px",
|
|
102
|
+
borderRadius: 4,
|
|
103
|
+
background: "rgba(20, 24, 31, 0.95)",
|
|
104
|
+
color: "#e6e6e6",
|
|
105
|
+
font: "12px system-ui, sans-serif",
|
|
106
|
+
whiteSpace: "nowrap",
|
|
107
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.4)"
|
|
108
|
+
};
|
|
109
|
+
function Tooltip({
|
|
110
|
+
hit,
|
|
111
|
+
x,
|
|
112
|
+
y,
|
|
113
|
+
render
|
|
114
|
+
}) {
|
|
115
|
+
if (hit === null || typeof document === "undefined") return null;
|
|
116
|
+
return reactDom.createPortal(
|
|
117
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
118
|
+
"div",
|
|
119
|
+
{
|
|
120
|
+
"data-testid": "timeline-tooltip",
|
|
121
|
+
role: "tooltip",
|
|
122
|
+
style: { ...tooltipStyle, left: x + 12, top: y + 12 },
|
|
123
|
+
children: render ? render(hit) : defaultContent(hit)
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
document.body
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
var containerStyle = {
|
|
130
|
+
position: "relative",
|
|
131
|
+
width: "100%",
|
|
132
|
+
height: "100%",
|
|
133
|
+
overflow: "hidden"
|
|
134
|
+
};
|
|
135
|
+
var canvasStyle = {
|
|
136
|
+
position: "absolute",
|
|
137
|
+
inset: 0,
|
|
138
|
+
width: "100%",
|
|
139
|
+
height: "100%",
|
|
140
|
+
display: "block"
|
|
141
|
+
};
|
|
142
|
+
var EMPTY_VIEWPORT = {
|
|
143
|
+
timeRange: [0, 0],
|
|
144
|
+
scrollY: 0,
|
|
145
|
+
pxPerMs: 0,
|
|
146
|
+
visibleRowRange: [0, 0]
|
|
147
|
+
};
|
|
148
|
+
function TimelineInner(props, ref) {
|
|
149
|
+
const { className, style, data, theme, options, styleResolvers, formatters } = props;
|
|
150
|
+
const containerRef = react.useRef(null);
|
|
151
|
+
const baseRef = react.useRef(null);
|
|
152
|
+
const overlayRef = react.useRef(null);
|
|
153
|
+
const [hover, setHover] = react.useState(null);
|
|
154
|
+
const [pointer, setPointer] = react.useState({ x: 0, y: 0 });
|
|
155
|
+
const engineRef = useTimeline({
|
|
156
|
+
baseRef,
|
|
157
|
+
overlayRef,
|
|
158
|
+
containerRef,
|
|
159
|
+
data,
|
|
160
|
+
theme,
|
|
161
|
+
options,
|
|
162
|
+
styleResolvers,
|
|
163
|
+
formatters,
|
|
164
|
+
onReady: props.onReady,
|
|
165
|
+
onViewportChange: props.onViewportChange,
|
|
166
|
+
onDataChange: props.onDataChange,
|
|
167
|
+
onHover: (hit) => {
|
|
168
|
+
setHover(hit);
|
|
169
|
+
props.onHover?.(hit);
|
|
170
|
+
},
|
|
171
|
+
onClick: props.onClick,
|
|
172
|
+
onSelectionChange: props.onSelectionChange
|
|
173
|
+
});
|
|
174
|
+
react.useImperativeHandle(
|
|
175
|
+
ref,
|
|
176
|
+
() => ({
|
|
177
|
+
version: core.VERSION,
|
|
178
|
+
fit: () => engineRef.current?.fit(),
|
|
179
|
+
panToTime: (t) => engineRef.current?.panToTime(t),
|
|
180
|
+
zoomToRange: (s, e) => engineRef.current?.zoomToRange(s, e),
|
|
181
|
+
select: (ids) => engineRef.current?.select(ids),
|
|
182
|
+
getViewport: () => engineRef.current?.getViewport() ?? EMPTY_VIEWPORT,
|
|
183
|
+
resize: (w, h, dpr) => engineRef.current?.resize(w, h, dpr),
|
|
184
|
+
setData: (d) => engineRef.current?.setData(d),
|
|
185
|
+
addEvents: (e) => engineRef.current?.addEvents(e),
|
|
186
|
+
removeEvents: (ids) => engineRef.current?.removeEvents(ids),
|
|
187
|
+
addElements: (el) => engineRef.current?.addElements(el),
|
|
188
|
+
updateElement: (id, patch) => engineRef.current?.updateElement(id, patch)
|
|
189
|
+
}),
|
|
190
|
+
[engineRef]
|
|
191
|
+
);
|
|
192
|
+
const onPointerMove = (e) => setPointer({ x: e.clientX, y: e.clientY });
|
|
193
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
194
|
+
"div",
|
|
195
|
+
{
|
|
196
|
+
ref: containerRef,
|
|
197
|
+
className,
|
|
198
|
+
style: { ...containerStyle, ...style },
|
|
199
|
+
"data-testid": "timeline-root",
|
|
200
|
+
onPointerMove,
|
|
201
|
+
children: [
|
|
202
|
+
/* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: baseRef, style: canvasStyle }),
|
|
203
|
+
/* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: overlayRef, style: canvasStyle }),
|
|
204
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
205
|
+
Tooltip,
|
|
206
|
+
{
|
|
207
|
+
hit: hover,
|
|
208
|
+
x: pointer.x,
|
|
209
|
+
y: pointer.y,
|
|
210
|
+
render: props.renderTooltip
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
var Timeline = react.forwardRef(TimelineInner);
|
|
218
|
+
|
|
219
|
+
Object.defineProperty(exports, "barycenter", {
|
|
220
|
+
enumerable: true,
|
|
221
|
+
get: function () { return core.barycenter; }
|
|
222
|
+
});
|
|
223
|
+
Object.defineProperty(exports, "byFirstEvent", {
|
|
224
|
+
enumerable: true,
|
|
225
|
+
get: function () { return core.byFirstEvent; }
|
|
226
|
+
});
|
|
227
|
+
Object.defineProperty(exports, "explicit", {
|
|
228
|
+
enumerable: true,
|
|
229
|
+
get: function () { return core.explicit; }
|
|
230
|
+
});
|
|
231
|
+
exports.Timeline = Timeline;
|
|
232
|
+
exports.Tooltip = Tooltip;
|
|
233
|
+
//# sourceMappingURL=index.cjs.map
|
|
234
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useTimeline.ts","../src/Tooltip.tsx","../src/Timeline.tsx"],"names":["useRef","useEffect","TimelineEngine","rect","createPortal","jsx","useState","useImperativeHandle","VERSION","jsxs","forwardRef"],"mappings":";;;;;;;;AA6DO,SAAS,YACd,IAAA,EACyC;AACzC,EAAA,MAAM,EAAE,OAAA,EAAS,UAAA,EAAY,cAAc,IAAA,EAAM,KAAA,EAAO,SAAQ,GAAI,IAAA;AACpE,EAAA,MAAM,EAAE,cAAA,EAAgB,UAAA,EAAW,GAAI,IAAA;AACvC,EAAA,MAAM,SAAA,GAAYA,aAAqC,IAAI,CAAA;AAI3D,EAAA,MAAM,SAAA,GAAYA,aAAO,IAAI,CAAA;AAC7B,EAAA,SAAA,CAAU,OAAA,GAAU,IAAA;AACpB,EAAA,MAAM,OAAA,GAAUA,aAAO,IAAI,CAAA;AAC3B,EAAA,OAAA,CAAQ,OAAA,GAAU,IAAA;AAClB,EAAA,MAAM,WAAA,GAAcA,aAAwC,MAAS,CAAA;AAIrE,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,OAAO,OAAA,CAAQ,OAAA;AACrB,IAAA,MAAM,UAAU,UAAA,CAAW,OAAA;AAC3B,IAAA,MAAM,YAAY,YAAA,CAAa,OAAA;AAC/B,IAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,OAAA,KAAY,IAAA,IAAQ,cAAc,IAAA,EAAM;AAE7D,IAAA,MAAM,MAAA,GAAS,IAAIC,mBAAA,CAAsB,IAAA,EAAM,OAAA,EAAS;AAAA,MACtD,KAAA;AAAA,MACA,cAAc,OAAA,EAAS,YAAA;AAAA,MACvB,QAAQ,OAAA,EAAS,MAAA;AAAA,MACjB,kBAAkB,OAAA,EAAS,gBAAA;AAAA,MAC3B,iBAAiB,OAAA,EAAS,eAAA;AAAA,MAC1B,aAAa,OAAA,EAAS,WAAA;AAAA,MACtB,YAAY,OAAA,EAAS,UAAA;AAAA,MACrB,KAAK,OAAA,EAAS,GAAA;AAAA,MACd,cAAc,OAAA,EAAS,YAAA;AAAA,MACvB,cAAA;AAAA,MACA;AAAA,KACD,CAAA;AACD,IAAA,SAAA,CAAU,OAAA,GAAU,MAAA;AAEpB,IAAA,MAAM,GAAA,GAAM;AAAA,MACV,OAAO,EAAA,CAAG,OAAA,EAAS,MAAM,SAAA,CAAU,OAAA,CAAQ,WAAW,CAAA;AAAA,MACtD,MAAA,CAAO,EAAA;AAAA,QAAG,gBAAA;AAAA,QAAkB,CAAC,CAAA,KAC3B,SAAA,CAAU,OAAA,CAAQ,mBAAmB,CAAC;AAAA,OACxC;AAAA,MACA,MAAA,CAAO,GAAG,YAAA,EAAc,CAAC,MAAM,SAAA,CAAU,OAAA,CAAQ,YAAA,GAAe,CAAC,CAAC,CAAA;AAAA,MAClE,MAAA,CAAO,GAAG,OAAA,EAAS,CAAC,QAAQ,SAAA,CAAU,OAAA,CAAQ,OAAA,GAAU,GAAG,CAAC,CAAA;AAAA,MAC5D,MAAA,CAAO,GAAG,OAAA,EAAS,CAAC,MAAM,SAAA,CAAU,OAAA,CAAQ,OAAA,GAAU,CAAC,CAAC,CAAA;AAAA,MACxD,MAAA,CAAO,EAAA;AAAA,QAAG,iBAAA;AAAA,QAAmB,CAAC,CAAA,KAC5B,SAAA,CAAU,OAAA,CAAQ,iBAAA,GAAoB,EAAE,GAAG;AAAA;AAC7C,KACF;AAEA,IAAA,MAAM,SAAA,GAAY,CAAC,IAAA,EAAc,IAAA,KAC/B,MAAA,CAAO,OAAO,IAAA,EAAM,IAAA,EAAM,MAAA,CAAO,gBAAA,IAAoB,CAAC,CAAA;AAExD,IAAA,MAAM,QAAA,GAAW,IAAI,cAAA,CAAe,CAAC,OAAA,KAAY;AAC/C,MAAA,MAAMC,KAAAA,GAAO,OAAA,CAAQ,CAAC,CAAA,EAAG,WAAA;AACzB,MAAA,IAAIA,KAAAA,EAAM,SAAA,CAAUA,KAAAA,CAAK,KAAA,EAAOA,MAAK,MAAM,CAAA;AAAA,IAC7C,CAAC,CAAA;AACD,IAAA,QAAA,CAAS,QAAQ,SAAS,CAAA;AAE1B,IAAA,MAAM,IAAA,GAAO,UAAU,qBAAA,EAAsB;AAC7C,IAAA,SAAA,CAAU,IAAA,CAAK,KAAA,EAAO,IAAA,CAAK,MAAM,CAAA;AAEjC,IAAA,IAAI,QAAQ,OAAA,EAAS;AACnB,MAAA,MAAA,CAAO,OAAA,CAAQ,QAAQ,OAAO,CAAA;AAC9B,MAAA,WAAA,CAAY,UAAU,OAAA,CAAQ,OAAA;AAAA,IAChC;AAEA,IAAA,OAAO,MAAM;AACX,MAAA,KAAA,MAAW,WAAA,IAAe,KAAK,WAAA,EAAY;AAC3C,MAAA,QAAA,CAAS,UAAA,EAAW;AACpB,MAAA,MAAA,CAAO,OAAA,EAAQ;AACf,MAAA,SAAA,CAAU,OAAA,GAAU,IAAA;AACpB,MAAA,WAAA,CAAY,OAAA,GAAU,MAAA;AAAA,IACxB,CAAA;AAAA,EACF,CAAA,EAAG;AAAA,IACD,OAAA;AAAA,IACA,UAAA;AAAA,IACA,YAAA;AAAA,IACA,KAAA;AAAA,IACA,OAAA;AAAA,IACA,cAAA;AAAA,IACA;AAAA,GACD,CAAA;AAGD,EAAAF,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,SAAS,SAAA,CAAU,OAAA;AACzB,IAAA,IAAI,MAAA,KAAW,IAAA,IAAQ,IAAA,KAAS,WAAA,CAAY,OAAA,EAAS;AACrD,IAAA,IAAI,IAAA,EAAM,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA;AAC7B,IAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AAAA,EACxB,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAET,EAAA,OAAO,SAAA;AACT;ACpIA,SAAS,eAAe,GAAA,EAAwB;AAC9C,EAAA,QAAQ,IAAI,IAAA;AAAM,IAChB,KAAK,OAAA;AACH,MAAA,OAAO,GAAA,CAAI,KAAA,CAAM,KAAA,IAAS,CAAA,EAAG,GAAA,CAAI,MAAM,QAAQ,CAAA,QAAA,EAAM,GAAA,CAAI,KAAA,CAAM,QAAQ,CAAA,CAAA;AAAA,IACzE,KAAK,SAAA;AACH,MAAA,OAAO,IAAI,OAAA,CAAQ,KAAA;AAAA,IACrB,KAAK,SAAA;AACH,MAAA,OAAO,CAAA,EAAG,IAAI,KAAK,CAAA,OAAA,CAAA;AAAA;AAEzB;AAEA,IAAM,YAAA,GAA8B;AAAA,EAClC,QAAA,EAAU,OAAA;AAAA,EACV,aAAA,EAAe,MAAA;AAAA,EACf,MAAA,EAAQ,UAAA;AAAA,EACR,OAAA,EAAS,SAAA;AAAA,EACT,YAAA,EAAc,CAAA;AAAA,EACd,UAAA,EAAY,wBAAA;AAAA,EACZ,KAAA,EAAO,SAAA;AAAA,EACP,IAAA,EAAM,4BAAA;AAAA,EACN,UAAA,EAAY,QAAA;AAAA,EACZ,SAAA,EAAW;AACb,CAAA;AAEO,SAAS,OAAA,CAAe;AAAA,EAC7B,GAAA;AAAA,EACA,CAAA;AAAA,EACA,CAAA;AAAA,EACA;AACF,CAAA,EAA4C;AAC1C,EAAA,IAAI,GAAA,KAAQ,IAAA,IAAQ,OAAO,QAAA,KAAa,aAAa,OAAO,IAAA;AAC5D,EAAA,OAAOG,qBAAA;AAAA,oBACLC,cAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,aAAA,EAAY,kBAAA;AAAA,QACZ,IAAA,EAAK,SAAA;AAAA,QACL,KAAA,EAAO,EAAE,GAAG,YAAA,EAAc,MAAM,CAAA,GAAI,EAAA,EAAI,GAAA,EAAK,CAAA,GAAI,EAAA,EAAG;AAAA,QAEnD,QAAA,EAAA,MAAA,GAAS,MAAA,CAAO,GAAG,CAAA,GAAI,eAAe,GAAG;AAAA;AAAA,KAC5C;AAAA,IACA,QAAA,CAAS;AAAA,GACX;AACF;ACOA,IAAM,cAAA,GAAgC;AAAA,EACpC,QAAA,EAAU,UAAA;AAAA,EACV,KAAA,EAAO,MAAA;AAAA,EACP,MAAA,EAAQ,MAAA;AAAA,EACR,QAAA,EAAU;AACZ,CAAA;AAEA,IAAM,WAAA,GAA6B;AAAA,EACjC,QAAA,EAAU,UAAA;AAAA,EACV,KAAA,EAAO,CAAA;AAAA,EACP,KAAA,EAAO,MAAA;AAAA,EACP,MAAA,EAAQ,MAAA;AAAA,EACR,OAAA,EAAS;AACX,CAAA;AAEA,IAAM,cAAA,GAAqD;AAAA,EACzD,SAAA,EAAW,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,EAChB,OAAA,EAAS,CAAA;AAAA,EACT,OAAA,EAAS,CAAA;AAAA,EACT,eAAA,EAAiB,CAAC,CAAA,EAAG,CAAC;AACxB,CAAA;AAEA,SAAS,aAAA,CACP,OACA,GAAA,EACA;AACA,EAAA,MAAM,EAAE,WAAW,KAAA,EAAO,IAAA,EAAM,OAAO,OAAA,EAAS,cAAA,EAAgB,YAAW,GACzE,KAAA;AACF,EAAA,MAAM,YAAA,GAAeL,aAAuB,IAAI,CAAA;AAChD,EAAA,MAAM,OAAA,GAAUA,aAA0B,IAAI,CAAA;AAC9C,EAAA,MAAM,UAAA,GAAaA,aAA0B,IAAI,CAAA;AAKjD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIM,eAAkC,IAAI,CAAA;AAChE,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAIA,cAAA,CAAS,EAAE,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA;AAErD,EAAA,MAAM,YAAY,WAAA,CAAmB;AAAA,IACnC,OAAA;AAAA,IACA,UAAA;AAAA,IACA,YAAA;AAAA,IACA,IAAA;AAAA,IACA,KAAA;AAAA,IACA,OAAA;AAAA,IACA,cAAA;AAAA,IACA,UAAA;AAAA,IACA,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,kBAAkB,KAAA,CAAM,gBAAA;AAAA,IACxB,cAAc,KAAA,CAAM,YAAA;AAAA,IACpB,OAAA,EAAS,CAAC,GAAA,KAAQ;AAChB,MAAA,QAAA,CAAS,GAAG,CAAA;AACZ,MAAA,KAAA,CAAM,UAAU,GAAG,CAAA;AAAA,IACrB,CAAA;AAAA,IACA,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,mBAAmB,KAAA,CAAM;AAAA,GAC1B,CAAA;AAED,EAAAC,yBAAA;AAAA,IACE,GAAA;AAAA,IACA,OAA8B;AAAA,MAC5B,OAAA,EAASC,YAAA;AAAA,MACT,GAAA,EAAK,MAAM,SAAA,CAAU,OAAA,EAAS,GAAA,EAAI;AAAA,MAClC,WAAW,CAAC,CAAA,KAAM,SAAA,CAAU,OAAA,EAAS,UAAU,CAAC,CAAA;AAAA,MAChD,WAAA,EAAa,CAAC,CAAA,EAAG,CAAA,KAAM,UAAU,OAAA,EAAS,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,MAC1D,QAAQ,CAAC,GAAA,KAAQ,SAAA,CAAU,OAAA,EAAS,OAAO,GAAG,CAAA;AAAA,MAC9C,WAAA,EAAa,MAAM,SAAA,CAAU,OAAA,EAAS,aAAY,IAAK,cAAA;AAAA,MACvD,MAAA,EAAQ,CAAC,CAAA,EAAG,CAAA,EAAG,GAAA,KAAQ,UAAU,OAAA,EAAS,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA;AAAA,MAC1D,SAAS,CAAC,CAAA,KAAM,SAAA,CAAU,OAAA,EAAS,QAAQ,CAAC,CAAA;AAAA,MAC5C,WAAW,CAAC,CAAA,KAAM,SAAA,CAAU,OAAA,EAAS,UAAU,CAAC,CAAA;AAAA,MAChD,cAAc,CAAC,GAAA,KAAQ,SAAA,CAAU,OAAA,EAAS,aAAa,GAAG,CAAA;AAAA,MAC1D,aAAa,CAAC,EAAA,KAAO,SAAA,CAAU,OAAA,EAAS,YAAY,EAAE,CAAA;AAAA,MACtD,aAAA,EAAe,CAAC,EAAA,EAAI,KAAA,KAAU,UAAU,OAAA,EAAS,aAAA,CAAc,IAAI,KAAK;AAAA,KAC1E,CAAA;AAAA,IACA,CAAC,SAAS;AAAA,GACZ;AAEA,EAAA,MAAM,aAAA,GAAgB,CAAC,CAAA,KACrB,UAAA,CAAW,EAAE,CAAA,EAAG,CAAA,CAAE,OAAA,EAAS,CAAA,EAAG,CAAA,CAAE,OAAA,EAAS,CAAA;AAE3C,EAAA,uBACEC,eAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAA,EAAK,YAAA;AAAA,MACL,SAAA;AAAA,MACA,KAAA,EAAO,EAAE,GAAG,cAAA,EAAgB,GAAG,KAAA,EAAM;AAAA,MACrC,aAAA,EAAY,eAAA;AAAA,MACZ,aAAA;AAAA,MAEA,QAAA,EAAA;AAAA,wBAAAJ,cAAAA,CAAC,QAAA,EAAA,EAAO,GAAA,EAAK,OAAA,EAAS,OAAO,WAAA,EAAa,CAAA;AAAA,wBAC1CA,cAAAA,CAAC,QAAA,EAAA,EAAO,GAAA,EAAK,UAAA,EAAY,OAAO,WAAA,EAAa,CAAA;AAAA,wBAC7CA,cAAAA;AAAA,UAAC,OAAA;AAAA,UAAA;AAAA,YACC,GAAA,EAAK,KAAA;AAAA,YACL,GAAG,OAAA,CAAQ,CAAA;AAAA,YACX,GAAG,OAAA,CAAQ,CAAA;AAAA,YACX,QAAQ,KAAA,CAAM;AAAA;AAAA;AAChB;AAAA;AAAA,GACF;AAEJ;AAMO,IAAM,QAAA,GAAWK,iBAAW,aAAa","file":"index.cjs","sourcesContent":["// React ↔ engine bridge (DESIGN.md §12).\n//\n// Instantiates the core engine once, owns the ResizeObserver, and syncs props\n// to the engine imperatively so React stays out of the render loop. All\n// browser APIs (ResizeObserver, devicePixelRatio) are touched inside effects,\n// never at import/render, keeping the binding SSR-safe; setup/teardown is\n// idempotent so React StrictMode's double-invoke is safe.\n\nimport { useEffect, useRef, type RefObject } from 'react';\nimport {\n TimelineEngine,\n type ClusteringOptions,\n type Formatters,\n type FrameStats,\n type HitResult,\n type LayoutStrategy,\n type LodOptions,\n type OnDiagnostic,\n type StyleResolvers,\n type Theme,\n type TimelineData,\n type TimelineEventMap,\n} from '@event-timeline/core';\n\nexport interface TimelineOptions {\n onDiagnostic?: OnDiagnostic;\n /** Vertical row-ordering strategy (DESIGN.md §7). Default `byFirstEvent`. */\n layout?: LayoutStrategy;\n /** Trailing-edge delay (ms) for coalesced incremental re-layout (DESIGN.md §7). */\n layoutDebounceMs?: number;\n /** Re-layout policy for streaming mutations (DESIGN.md §7, §10). Default stable-append. */\n streamingLayout?: 'stable-append' | 'full';\n /** Allow `ctrl`/`⌘`+click to extend the selection (DESIGN.md §9). */\n multiSelect?: boolean;\n /** Event collapsing/aggregation config (DESIGN.md §6). */\n clustering?: ClusteringOptions;\n /** Level-of-detail config, e.g. the event-label zoom threshold (DESIGN.md §6). */\n lod?: LodOptions;\n /** Optional per-frame timing sink for benchmarking (DESIGN.md §13). */\n onFrameStats?: (stats: FrameStats) => void;\n}\n\nexport interface UseTimelineArgs<TData> {\n baseRef: RefObject<HTMLCanvasElement | null>;\n overlayRef: RefObject<HTMLCanvasElement | null>;\n containerRef: RefObject<HTMLElement | null>;\n data?: TimelineData<TData>;\n theme?: Partial<Theme>;\n options?: TimelineOptions;\n /** Data-driven per-item style overrides (DESIGN.md §11, §12). */\n styleResolvers?: StyleResolvers<TData>;\n /** Label/tick text formatters (DESIGN.md §11, §12). */\n formatters?: Formatters<TData>;\n onReady?: () => void;\n onViewportChange?: (v: TimelineEventMap<TData>['viewportChange']) => void;\n onDataChange?: (d: TimelineEventMap<TData>['dataChange']) => void;\n onHover?: (hit: HitResult<TData> | null) => void;\n onClick?: (e: TimelineEventMap<TData>['click']) => void;\n onSelectionChange?: (ids: string[]) => void;\n}\n\nexport function useTimeline<TData>(\n args: UseTimelineArgs<TData>,\n): RefObject<TimelineEngine<TData> | null> {\n const { baseRef, overlayRef, containerRef, data, theme, options } = args;\n const { styleResolvers, formatters } = args;\n const engineRef = useRef<TimelineEngine<TData> | null>(null);\n\n // Keep the latest callbacks/data in refs so the setup effect doesn't re-run\n // when the host passes fresh inline functions each render.\n const callbacks = useRef(args);\n callbacks.current = args;\n const dataRef = useRef(data);\n dataRef.current = data;\n const appliedData = useRef<TimelineData<TData> | undefined>(undefined);\n\n // Engine lifecycle. Re-creates only when theme/options change by reference\n // (hosts memoize these — §12 referential-stability contract).\n useEffect(() => {\n const base = baseRef.current;\n const overlay = overlayRef.current;\n const container = containerRef.current;\n if (base === null || overlay === null || container === null) return;\n\n const engine = new TimelineEngine<TData>(base, overlay, {\n theme,\n onDiagnostic: options?.onDiagnostic,\n layout: options?.layout,\n layoutDebounceMs: options?.layoutDebounceMs,\n streamingLayout: options?.streamingLayout,\n multiSelect: options?.multiSelect,\n clustering: options?.clustering,\n lod: options?.lod,\n onFrameStats: options?.onFrameStats,\n styleResolvers,\n formatters,\n });\n engineRef.current = engine;\n\n const off = [\n engine.on('ready', () => callbacks.current.onReady?.()),\n engine.on('viewportChange', (v) =>\n callbacks.current.onViewportChange?.(v),\n ),\n engine.on('dataChange', (d) => callbacks.current.onDataChange?.(d)),\n engine.on('hover', (hit) => callbacks.current.onHover?.(hit)),\n engine.on('click', (e) => callbacks.current.onClick?.(e)),\n engine.on('selectionChange', (s) =>\n callbacks.current.onSelectionChange?.(s.ids),\n ),\n ];\n\n const applySize = (cssW: number, cssH: number) =>\n engine.resize(cssW, cssH, window.devicePixelRatio || 1);\n\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (rect) applySize(rect.width, rect.height);\n });\n observer.observe(container);\n\n const rect = container.getBoundingClientRect();\n applySize(rect.width, rect.height);\n\n if (dataRef.current) {\n engine.setData(dataRef.current);\n appliedData.current = dataRef.current;\n }\n\n return () => {\n for (const unsubscribe of off) unsubscribe();\n observer.disconnect();\n engine.dispose();\n engineRef.current = null;\n appliedData.current = undefined;\n };\n }, [\n baseRef,\n overlayRef,\n containerRef,\n theme,\n options,\n styleResolvers,\n formatters,\n ]);\n\n // Data sync: full setData whenever the `data` reference changes (§10, §12).\n useEffect(() => {\n const engine = engineRef.current;\n if (engine === null || data === appliedData.current) return;\n if (data) engine.setData(data);\n appliedData.current = data;\n }, [data]);\n\n return engineRef;\n}\n","// HTML hover tooltip (DESIGN.md §9, §12).\n//\n// Hover hit-testing happens on the canvas (core); the tooltip is plain HTML so\n// its text stays crisp (no canvas-text blur) and selectable. It is portalled to\n// `document.body` and positioned with `fixed` at the pointer's client\n// coordinates, so it is never clipped by the timeline container's `overflow:\n// hidden`. SSR-safe: renders nothing until a hover hit exists on the client.\n\nimport { createPortal } from 'react-dom';\nimport type { CSSProperties, ReactNode, ReactPortal } from 'react';\nimport type { HitResult } from '@event-timeline/core';\n\nexport interface TooltipProps<TData = unknown> {\n /** The current hover hit, or null when nothing is hovered. */\n hit: HitResult<TData> | null;\n /** Pointer position in client (viewport) pixels. */\n x: number;\n y: number;\n /** Optional host renderer for tooltip content; falls back to the default text. */\n render?: (hit: HitResult<TData>) => ReactNode;\n}\n\n/** Default tooltip text for a hit; host-customisable content arrives in Phase 6. */\nfunction defaultContent(hit: HitResult): string {\n switch (hit.kind) {\n case 'event':\n return hit.event.label ?? `${hit.event.sourceId} → ${hit.event.targetId}`;\n case 'element':\n return hit.element.label;\n case 'cluster':\n return `${hit.count} events`;\n }\n}\n\nconst tooltipStyle: CSSProperties = {\n position: 'fixed',\n pointerEvents: 'none',\n zIndex: 2147483647,\n padding: '4px 8px',\n borderRadius: 4,\n background: 'rgba(20, 24, 31, 0.95)',\n color: '#e6e6e6',\n font: '12px system-ui, sans-serif',\n whiteSpace: 'nowrap',\n boxShadow: '0 2px 8px rgba(0, 0, 0, 0.4)',\n};\n\nexport function Tooltip<TData>({\n hit,\n x,\n y,\n render,\n}: TooltipProps<TData>): ReactPortal | null {\n if (hit === null || typeof document === 'undefined') return null;\n return createPortal(\n <div\n data-testid=\"timeline-tooltip\"\n role=\"tooltip\"\n style={{ ...tooltipStyle, left: x + 12, top: y + 12 }}\n >\n {render ? render(hit) : defaultContent(hit)}\n </div>,\n document.body,\n );\n}\n","import {\n forwardRef,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ForwardedRef,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n VERSION,\n type Formatters,\n type HitResult,\n type StyleResolvers,\n type Theme,\n type Time,\n type TimelineData,\n type TimelineElement,\n type TimelineEvent,\n type TimelineEventMap,\n} from '@event-timeline/core';\nimport { useTimeline, type TimelineOptions } from './useTimeline';\nimport { Tooltip } from './Tooltip';\n\n/**\n * Props for the {@link Timeline} component (Phase 1 subset, DESIGN.md §12).\n * `data`, `theme`, and `options` should be memoized by the host: they are\n * diffed by reference and re-applied only when the reference changes.\n */\nexport interface TimelineProps<TData = unknown> {\n data?: TimelineData<TData>;\n theme?: Partial<Theme>;\n options?: TimelineOptions;\n /** Data-driven per-item style overrides (DESIGN.md §11, §12); memoize. */\n styleResolvers?: StyleResolvers<TData>;\n /** Label/tick text formatters (DESIGN.md §11, §12); memoize. */\n formatters?: Formatters<TData>;\n /** Optional host renderer for tooltip content; defaults to label-only text. */\n renderTooltip?: (hit: HitResult<TData>) => ReactNode;\n /** Class applied to the root container. */\n className?: string;\n /** Inline style merged over the default container style. */\n style?: CSSProperties;\n onReady?: () => void;\n onViewportChange?: (v: TimelineEventMap<TData>['viewportChange']) => void;\n onDataChange?: (d: TimelineEventMap<TData>['dataChange']) => void;\n onHover?: (hit: HitResult<TData> | null) => void;\n onClick?: (e: TimelineEventMap<TData>['click']) => void;\n onSelectionChange?: (ids: string[]) => void;\n}\n\n/** Imperative handle returned via `ref` (DESIGN.md §12). */\nexport interface TimelineHandle<TData = unknown> {\n readonly version: string;\n fit(): void;\n panToTime(t: Time): void;\n zoomToRange(start: Time, end: Time): void;\n select(ids: string[]): void;\n getViewport(): TimelineEventMap<TData>['viewportChange'];\n resize(cssW: number, cssH: number, dpr: number): void;\n setData(data: TimelineData<TData>): void;\n // Streaming methods (DESIGN.md §10): incremental updates that bypass the\n // prop-driven full `setData`, for high-frequency hosts.\n addEvents(events: readonly TimelineEvent<TData>[]): void;\n removeEvents(ids: readonly string[]): void;\n addElements(elements: readonly TimelineElement<TData>[]): void;\n updateElement(id: string, patch: Partial<TimelineElement<TData>>): void;\n}\n\nconst containerStyle: CSSProperties = {\n position: 'relative',\n width: '100%',\n height: '100%',\n overflow: 'hidden',\n};\n\nconst canvasStyle: CSSProperties = {\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n display: 'block',\n};\n\nconst EMPTY_VIEWPORT: TimelineEventMap['viewportChange'] = {\n timeRange: [0, 0],\n scrollY: 0,\n pxPerMs: 0,\n visibleRowRange: [0, 0],\n};\n\nfunction TimelineInner<TData>(\n props: TimelineProps<TData>,\n ref: ForwardedRef<TimelineHandle<TData>>,\n) {\n const { className, style, data, theme, options, styleResolvers, formatters } =\n props;\n const containerRef = useRef<HTMLDivElement>(null);\n const baseRef = useRef<HTMLCanvasElement>(null);\n const overlayRef = useRef<HTMLCanvasElement>(null);\n\n // Hover hit (from the engine) and the pointer position (from the DOM) drive\n // the HTML tooltip. Tracking position in React keeps canvas hit-testing in\n // the engine while text rendering stays crisp HTML (§9).\n const [hover, setHover] = useState<HitResult<TData> | null>(null);\n const [pointer, setPointer] = useState({ x: 0, y: 0 });\n\n const engineRef = useTimeline<TData>({\n baseRef,\n overlayRef,\n containerRef,\n data,\n theme,\n options,\n styleResolvers,\n formatters,\n onReady: props.onReady,\n onViewportChange: props.onViewportChange,\n onDataChange: props.onDataChange,\n onHover: (hit) => {\n setHover(hit);\n props.onHover?.(hit);\n },\n onClick: props.onClick,\n onSelectionChange: props.onSelectionChange,\n });\n\n useImperativeHandle(\n ref,\n (): TimelineHandle<TData> => ({\n version: VERSION,\n fit: () => engineRef.current?.fit(),\n panToTime: (t) => engineRef.current?.panToTime(t),\n zoomToRange: (s, e) => engineRef.current?.zoomToRange(s, e),\n select: (ids) => engineRef.current?.select(ids),\n getViewport: () => engineRef.current?.getViewport() ?? EMPTY_VIEWPORT,\n resize: (w, h, dpr) => engineRef.current?.resize(w, h, dpr),\n setData: (d) => engineRef.current?.setData(d),\n addEvents: (e) => engineRef.current?.addEvents(e),\n removeEvents: (ids) => engineRef.current?.removeEvents(ids),\n addElements: (el) => engineRef.current?.addElements(el),\n updateElement: (id, patch) => engineRef.current?.updateElement(id, patch),\n }),\n [engineRef],\n );\n\n const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) =>\n setPointer({ x: e.clientX, y: e.clientY });\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ ...containerStyle, ...style }}\n data-testid=\"timeline-root\"\n onPointerMove={onPointerMove}\n >\n <canvas ref={baseRef} style={canvasStyle} />\n <canvas ref={overlayRef} style={canvasStyle} />\n <Tooltip\n hit={hover}\n x={pointer.x}\n y={pointer.y}\n render={props.renderTooltip}\n />\n </div>\n );\n}\n\n/**\n * Canvas timeline component. Owns the two stacked `<canvas>` layers and the\n * ResizeObserver, and drives the core engine imperatively (DESIGN.md §4, §12).\n */\nexport const Timeline = forwardRef(TimelineInner) as <TData = unknown>(\n props: TimelineProps<TData> & { ref?: Ref<TimelineHandle<TData>> },\n) => ReturnType<typeof TimelineInner>;\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, CSSProperties, Ref, ForwardedRef, ReactPortal } from 'react';
|
|
3
|
+
import { OnDiagnostic, LayoutStrategy, ClusteringOptions, LodOptions, FrameStats, TimelineData, Theme, StyleResolvers, Formatters, HitResult, TimelineEventMap, Time, TimelineEvent, TimelineElement } from '@event-timeline/core';
|
|
4
|
+
export { ElementStyle, ElementStyleResolver, EventStyle, EventStyleResolver, Formatters, FrameStats, HitResult, LayoutStrategy, StyleResolvers, Theme, TickFormatter, Time, TimelineData, TimelineElement, TimelineEvent, barycenter, byFirstEvent, explicit } from '@event-timeline/core';
|
|
5
|
+
|
|
6
|
+
interface TimelineOptions {
|
|
7
|
+
onDiagnostic?: OnDiagnostic;
|
|
8
|
+
/** Vertical row-ordering strategy (DESIGN.md §7). Default `byFirstEvent`. */
|
|
9
|
+
layout?: LayoutStrategy;
|
|
10
|
+
/** Trailing-edge delay (ms) for coalesced incremental re-layout (DESIGN.md §7). */
|
|
11
|
+
layoutDebounceMs?: number;
|
|
12
|
+
/** Re-layout policy for streaming mutations (DESIGN.md §7, §10). Default stable-append. */
|
|
13
|
+
streamingLayout?: 'stable-append' | 'full';
|
|
14
|
+
/** Allow `ctrl`/`⌘`+click to extend the selection (DESIGN.md §9). */
|
|
15
|
+
multiSelect?: boolean;
|
|
16
|
+
/** Event collapsing/aggregation config (DESIGN.md §6). */
|
|
17
|
+
clustering?: ClusteringOptions;
|
|
18
|
+
/** Level-of-detail config, e.g. the event-label zoom threshold (DESIGN.md §6). */
|
|
19
|
+
lod?: LodOptions;
|
|
20
|
+
/** Optional per-frame timing sink for benchmarking (DESIGN.md §13). */
|
|
21
|
+
onFrameStats?: (stats: FrameStats) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Props for the {@link Timeline} component (Phase 1 subset, DESIGN.md §12).
|
|
26
|
+
* `data`, `theme`, and `options` should be memoized by the host: they are
|
|
27
|
+
* diffed by reference and re-applied only when the reference changes.
|
|
28
|
+
*/
|
|
29
|
+
interface TimelineProps<TData = unknown> {
|
|
30
|
+
data?: TimelineData<TData>;
|
|
31
|
+
theme?: Partial<Theme>;
|
|
32
|
+
options?: TimelineOptions;
|
|
33
|
+
/** Data-driven per-item style overrides (DESIGN.md §11, §12); memoize. */
|
|
34
|
+
styleResolvers?: StyleResolvers<TData>;
|
|
35
|
+
/** Label/tick text formatters (DESIGN.md §11, §12); memoize. */
|
|
36
|
+
formatters?: Formatters<TData>;
|
|
37
|
+
/** Optional host renderer for tooltip content; defaults to label-only text. */
|
|
38
|
+
renderTooltip?: (hit: HitResult<TData>) => ReactNode;
|
|
39
|
+
/** Class applied to the root container. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/** Inline style merged over the default container style. */
|
|
42
|
+
style?: CSSProperties;
|
|
43
|
+
onReady?: () => void;
|
|
44
|
+
onViewportChange?: (v: TimelineEventMap<TData>['viewportChange']) => void;
|
|
45
|
+
onDataChange?: (d: TimelineEventMap<TData>['dataChange']) => void;
|
|
46
|
+
onHover?: (hit: HitResult<TData> | null) => void;
|
|
47
|
+
onClick?: (e: TimelineEventMap<TData>['click']) => void;
|
|
48
|
+
onSelectionChange?: (ids: string[]) => void;
|
|
49
|
+
}
|
|
50
|
+
/** Imperative handle returned via `ref` (DESIGN.md §12). */
|
|
51
|
+
interface TimelineHandle<TData = unknown> {
|
|
52
|
+
readonly version: string;
|
|
53
|
+
fit(): void;
|
|
54
|
+
panToTime(t: Time): void;
|
|
55
|
+
zoomToRange(start: Time, end: Time): void;
|
|
56
|
+
select(ids: string[]): void;
|
|
57
|
+
getViewport(): TimelineEventMap<TData>['viewportChange'];
|
|
58
|
+
resize(cssW: number, cssH: number, dpr: number): void;
|
|
59
|
+
setData(data: TimelineData<TData>): void;
|
|
60
|
+
addEvents(events: readonly TimelineEvent<TData>[]): void;
|
|
61
|
+
removeEvents(ids: readonly string[]): void;
|
|
62
|
+
addElements(elements: readonly TimelineElement<TData>[]): void;
|
|
63
|
+
updateElement(id: string, patch: Partial<TimelineElement<TData>>): void;
|
|
64
|
+
}
|
|
65
|
+
declare function TimelineInner<TData>(props: TimelineProps<TData>, ref: ForwardedRef<TimelineHandle<TData>>): react.JSX.Element;
|
|
66
|
+
/**
|
|
67
|
+
* Canvas timeline component. Owns the two stacked `<canvas>` layers and the
|
|
68
|
+
* ResizeObserver, and drives the core engine imperatively (DESIGN.md §4, §12).
|
|
69
|
+
*/
|
|
70
|
+
declare const Timeline: <TData = unknown>(props: TimelineProps<TData> & {
|
|
71
|
+
ref?: Ref<TimelineHandle<TData>>;
|
|
72
|
+
}) => ReturnType<typeof TimelineInner>;
|
|
73
|
+
|
|
74
|
+
interface TooltipProps<TData = unknown> {
|
|
75
|
+
/** The current hover hit, or null when nothing is hovered. */
|
|
76
|
+
hit: HitResult<TData> | null;
|
|
77
|
+
/** Pointer position in client (viewport) pixels. */
|
|
78
|
+
x: number;
|
|
79
|
+
y: number;
|
|
80
|
+
/** Optional host renderer for tooltip content; falls back to the default text. */
|
|
81
|
+
render?: (hit: HitResult<TData>) => ReactNode;
|
|
82
|
+
}
|
|
83
|
+
declare function Tooltip<TData>({ hit, x, y, render, }: TooltipProps<TData>): ReactPortal | null;
|
|
84
|
+
|
|
85
|
+
export { Timeline, type TimelineHandle, type TimelineOptions, type TimelineProps, Tooltip, type TooltipProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, CSSProperties, Ref, ForwardedRef, ReactPortal } from 'react';
|
|
3
|
+
import { OnDiagnostic, LayoutStrategy, ClusteringOptions, LodOptions, FrameStats, TimelineData, Theme, StyleResolvers, Formatters, HitResult, TimelineEventMap, Time, TimelineEvent, TimelineElement } from '@event-timeline/core';
|
|
4
|
+
export { ElementStyle, ElementStyleResolver, EventStyle, EventStyleResolver, Formatters, FrameStats, HitResult, LayoutStrategy, StyleResolvers, Theme, TickFormatter, Time, TimelineData, TimelineElement, TimelineEvent, barycenter, byFirstEvent, explicit } from '@event-timeline/core';
|
|
5
|
+
|
|
6
|
+
interface TimelineOptions {
|
|
7
|
+
onDiagnostic?: OnDiagnostic;
|
|
8
|
+
/** Vertical row-ordering strategy (DESIGN.md §7). Default `byFirstEvent`. */
|
|
9
|
+
layout?: LayoutStrategy;
|
|
10
|
+
/** Trailing-edge delay (ms) for coalesced incremental re-layout (DESIGN.md §7). */
|
|
11
|
+
layoutDebounceMs?: number;
|
|
12
|
+
/** Re-layout policy for streaming mutations (DESIGN.md §7, §10). Default stable-append. */
|
|
13
|
+
streamingLayout?: 'stable-append' | 'full';
|
|
14
|
+
/** Allow `ctrl`/`⌘`+click to extend the selection (DESIGN.md §9). */
|
|
15
|
+
multiSelect?: boolean;
|
|
16
|
+
/** Event collapsing/aggregation config (DESIGN.md §6). */
|
|
17
|
+
clustering?: ClusteringOptions;
|
|
18
|
+
/** Level-of-detail config, e.g. the event-label zoom threshold (DESIGN.md §6). */
|
|
19
|
+
lod?: LodOptions;
|
|
20
|
+
/** Optional per-frame timing sink for benchmarking (DESIGN.md §13). */
|
|
21
|
+
onFrameStats?: (stats: FrameStats) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Props for the {@link Timeline} component (Phase 1 subset, DESIGN.md §12).
|
|
26
|
+
* `data`, `theme`, and `options` should be memoized by the host: they are
|
|
27
|
+
* diffed by reference and re-applied only when the reference changes.
|
|
28
|
+
*/
|
|
29
|
+
interface TimelineProps<TData = unknown> {
|
|
30
|
+
data?: TimelineData<TData>;
|
|
31
|
+
theme?: Partial<Theme>;
|
|
32
|
+
options?: TimelineOptions;
|
|
33
|
+
/** Data-driven per-item style overrides (DESIGN.md §11, §12); memoize. */
|
|
34
|
+
styleResolvers?: StyleResolvers<TData>;
|
|
35
|
+
/** Label/tick text formatters (DESIGN.md §11, §12); memoize. */
|
|
36
|
+
formatters?: Formatters<TData>;
|
|
37
|
+
/** Optional host renderer for tooltip content; defaults to label-only text. */
|
|
38
|
+
renderTooltip?: (hit: HitResult<TData>) => ReactNode;
|
|
39
|
+
/** Class applied to the root container. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/** Inline style merged over the default container style. */
|
|
42
|
+
style?: CSSProperties;
|
|
43
|
+
onReady?: () => void;
|
|
44
|
+
onViewportChange?: (v: TimelineEventMap<TData>['viewportChange']) => void;
|
|
45
|
+
onDataChange?: (d: TimelineEventMap<TData>['dataChange']) => void;
|
|
46
|
+
onHover?: (hit: HitResult<TData> | null) => void;
|
|
47
|
+
onClick?: (e: TimelineEventMap<TData>['click']) => void;
|
|
48
|
+
onSelectionChange?: (ids: string[]) => void;
|
|
49
|
+
}
|
|
50
|
+
/** Imperative handle returned via `ref` (DESIGN.md §12). */
|
|
51
|
+
interface TimelineHandle<TData = unknown> {
|
|
52
|
+
readonly version: string;
|
|
53
|
+
fit(): void;
|
|
54
|
+
panToTime(t: Time): void;
|
|
55
|
+
zoomToRange(start: Time, end: Time): void;
|
|
56
|
+
select(ids: string[]): void;
|
|
57
|
+
getViewport(): TimelineEventMap<TData>['viewportChange'];
|
|
58
|
+
resize(cssW: number, cssH: number, dpr: number): void;
|
|
59
|
+
setData(data: TimelineData<TData>): void;
|
|
60
|
+
addEvents(events: readonly TimelineEvent<TData>[]): void;
|
|
61
|
+
removeEvents(ids: readonly string[]): void;
|
|
62
|
+
addElements(elements: readonly TimelineElement<TData>[]): void;
|
|
63
|
+
updateElement(id: string, patch: Partial<TimelineElement<TData>>): void;
|
|
64
|
+
}
|
|
65
|
+
declare function TimelineInner<TData>(props: TimelineProps<TData>, ref: ForwardedRef<TimelineHandle<TData>>): react.JSX.Element;
|
|
66
|
+
/**
|
|
67
|
+
* Canvas timeline component. Owns the two stacked `<canvas>` layers and the
|
|
68
|
+
* ResizeObserver, and drives the core engine imperatively (DESIGN.md §4, §12).
|
|
69
|
+
*/
|
|
70
|
+
declare const Timeline: <TData = unknown>(props: TimelineProps<TData> & {
|
|
71
|
+
ref?: Ref<TimelineHandle<TData>>;
|
|
72
|
+
}) => ReturnType<typeof TimelineInner>;
|
|
73
|
+
|
|
74
|
+
interface TooltipProps<TData = unknown> {
|
|
75
|
+
/** The current hover hit, or null when nothing is hovered. */
|
|
76
|
+
hit: HitResult<TData> | null;
|
|
77
|
+
/** Pointer position in client (viewport) pixels. */
|
|
78
|
+
x: number;
|
|
79
|
+
y: number;
|
|
80
|
+
/** Optional host renderer for tooltip content; falls back to the default text. */
|
|
81
|
+
render?: (hit: HitResult<TData>) => ReactNode;
|
|
82
|
+
}
|
|
83
|
+
declare function Tooltip<TData>({ hit, x, y, render, }: TooltipProps<TData>): ReactPortal | null;
|
|
84
|
+
|
|
85
|
+
export { Timeline, type TimelineHandle, type TimelineOptions, type TimelineProps, Tooltip, type TooltipProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { forwardRef, useRef, useState, useImperativeHandle, useEffect } from 'react';
|
|
2
|
+
import { VERSION, TimelineEngine } from '@event-timeline/core';
|
|
3
|
+
export { barycenter, byFirstEvent, explicit } from '@event-timeline/core';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
// src/Timeline.tsx
|
|
8
|
+
function useTimeline(args) {
|
|
9
|
+
const { baseRef, overlayRef, containerRef, data, theme, options } = args;
|
|
10
|
+
const { styleResolvers, formatters } = args;
|
|
11
|
+
const engineRef = useRef(null);
|
|
12
|
+
const callbacks = useRef(args);
|
|
13
|
+
callbacks.current = args;
|
|
14
|
+
const dataRef = useRef(data);
|
|
15
|
+
dataRef.current = data;
|
|
16
|
+
const appliedData = useRef(void 0);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const base = baseRef.current;
|
|
19
|
+
const overlay = overlayRef.current;
|
|
20
|
+
const container = containerRef.current;
|
|
21
|
+
if (base === null || overlay === null || container === null) return;
|
|
22
|
+
const engine = new TimelineEngine(base, overlay, {
|
|
23
|
+
theme,
|
|
24
|
+
onDiagnostic: options?.onDiagnostic,
|
|
25
|
+
layout: options?.layout,
|
|
26
|
+
layoutDebounceMs: options?.layoutDebounceMs,
|
|
27
|
+
streamingLayout: options?.streamingLayout,
|
|
28
|
+
multiSelect: options?.multiSelect,
|
|
29
|
+
clustering: options?.clustering,
|
|
30
|
+
lod: options?.lod,
|
|
31
|
+
onFrameStats: options?.onFrameStats,
|
|
32
|
+
styleResolvers,
|
|
33
|
+
formatters
|
|
34
|
+
});
|
|
35
|
+
engineRef.current = engine;
|
|
36
|
+
const off = [
|
|
37
|
+
engine.on("ready", () => callbacks.current.onReady?.()),
|
|
38
|
+
engine.on(
|
|
39
|
+
"viewportChange",
|
|
40
|
+
(v) => callbacks.current.onViewportChange?.(v)
|
|
41
|
+
),
|
|
42
|
+
engine.on("dataChange", (d) => callbacks.current.onDataChange?.(d)),
|
|
43
|
+
engine.on("hover", (hit) => callbacks.current.onHover?.(hit)),
|
|
44
|
+
engine.on("click", (e) => callbacks.current.onClick?.(e)),
|
|
45
|
+
engine.on(
|
|
46
|
+
"selectionChange",
|
|
47
|
+
(s) => callbacks.current.onSelectionChange?.(s.ids)
|
|
48
|
+
)
|
|
49
|
+
];
|
|
50
|
+
const applySize = (cssW, cssH) => engine.resize(cssW, cssH, window.devicePixelRatio || 1);
|
|
51
|
+
const observer = new ResizeObserver((entries) => {
|
|
52
|
+
const rect2 = entries[0]?.contentRect;
|
|
53
|
+
if (rect2) applySize(rect2.width, rect2.height);
|
|
54
|
+
});
|
|
55
|
+
observer.observe(container);
|
|
56
|
+
const rect = container.getBoundingClientRect();
|
|
57
|
+
applySize(rect.width, rect.height);
|
|
58
|
+
if (dataRef.current) {
|
|
59
|
+
engine.setData(dataRef.current);
|
|
60
|
+
appliedData.current = dataRef.current;
|
|
61
|
+
}
|
|
62
|
+
return () => {
|
|
63
|
+
for (const unsubscribe of off) unsubscribe();
|
|
64
|
+
observer.disconnect();
|
|
65
|
+
engine.dispose();
|
|
66
|
+
engineRef.current = null;
|
|
67
|
+
appliedData.current = void 0;
|
|
68
|
+
};
|
|
69
|
+
}, [
|
|
70
|
+
baseRef,
|
|
71
|
+
overlayRef,
|
|
72
|
+
containerRef,
|
|
73
|
+
theme,
|
|
74
|
+
options,
|
|
75
|
+
styleResolvers,
|
|
76
|
+
formatters
|
|
77
|
+
]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const engine = engineRef.current;
|
|
80
|
+
if (engine === null || data === appliedData.current) return;
|
|
81
|
+
if (data) engine.setData(data);
|
|
82
|
+
appliedData.current = data;
|
|
83
|
+
}, [data]);
|
|
84
|
+
return engineRef;
|
|
85
|
+
}
|
|
86
|
+
function defaultContent(hit) {
|
|
87
|
+
switch (hit.kind) {
|
|
88
|
+
case "event":
|
|
89
|
+
return hit.event.label ?? `${hit.event.sourceId} \u2192 ${hit.event.targetId}`;
|
|
90
|
+
case "element":
|
|
91
|
+
return hit.element.label;
|
|
92
|
+
case "cluster":
|
|
93
|
+
return `${hit.count} events`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
var tooltipStyle = {
|
|
97
|
+
position: "fixed",
|
|
98
|
+
pointerEvents: "none",
|
|
99
|
+
zIndex: 2147483647,
|
|
100
|
+
padding: "4px 8px",
|
|
101
|
+
borderRadius: 4,
|
|
102
|
+
background: "rgba(20, 24, 31, 0.95)",
|
|
103
|
+
color: "#e6e6e6",
|
|
104
|
+
font: "12px system-ui, sans-serif",
|
|
105
|
+
whiteSpace: "nowrap",
|
|
106
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.4)"
|
|
107
|
+
};
|
|
108
|
+
function Tooltip({
|
|
109
|
+
hit,
|
|
110
|
+
x,
|
|
111
|
+
y,
|
|
112
|
+
render
|
|
113
|
+
}) {
|
|
114
|
+
if (hit === null || typeof document === "undefined") return null;
|
|
115
|
+
return createPortal(
|
|
116
|
+
/* @__PURE__ */ jsx(
|
|
117
|
+
"div",
|
|
118
|
+
{
|
|
119
|
+
"data-testid": "timeline-tooltip",
|
|
120
|
+
role: "tooltip",
|
|
121
|
+
style: { ...tooltipStyle, left: x + 12, top: y + 12 },
|
|
122
|
+
children: render ? render(hit) : defaultContent(hit)
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
document.body
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
var containerStyle = {
|
|
129
|
+
position: "relative",
|
|
130
|
+
width: "100%",
|
|
131
|
+
height: "100%",
|
|
132
|
+
overflow: "hidden"
|
|
133
|
+
};
|
|
134
|
+
var canvasStyle = {
|
|
135
|
+
position: "absolute",
|
|
136
|
+
inset: 0,
|
|
137
|
+
width: "100%",
|
|
138
|
+
height: "100%",
|
|
139
|
+
display: "block"
|
|
140
|
+
};
|
|
141
|
+
var EMPTY_VIEWPORT = {
|
|
142
|
+
timeRange: [0, 0],
|
|
143
|
+
scrollY: 0,
|
|
144
|
+
pxPerMs: 0,
|
|
145
|
+
visibleRowRange: [0, 0]
|
|
146
|
+
};
|
|
147
|
+
function TimelineInner(props, ref) {
|
|
148
|
+
const { className, style, data, theme, options, styleResolvers, formatters } = props;
|
|
149
|
+
const containerRef = useRef(null);
|
|
150
|
+
const baseRef = useRef(null);
|
|
151
|
+
const overlayRef = useRef(null);
|
|
152
|
+
const [hover, setHover] = useState(null);
|
|
153
|
+
const [pointer, setPointer] = useState({ x: 0, y: 0 });
|
|
154
|
+
const engineRef = useTimeline({
|
|
155
|
+
baseRef,
|
|
156
|
+
overlayRef,
|
|
157
|
+
containerRef,
|
|
158
|
+
data,
|
|
159
|
+
theme,
|
|
160
|
+
options,
|
|
161
|
+
styleResolvers,
|
|
162
|
+
formatters,
|
|
163
|
+
onReady: props.onReady,
|
|
164
|
+
onViewportChange: props.onViewportChange,
|
|
165
|
+
onDataChange: props.onDataChange,
|
|
166
|
+
onHover: (hit) => {
|
|
167
|
+
setHover(hit);
|
|
168
|
+
props.onHover?.(hit);
|
|
169
|
+
},
|
|
170
|
+
onClick: props.onClick,
|
|
171
|
+
onSelectionChange: props.onSelectionChange
|
|
172
|
+
});
|
|
173
|
+
useImperativeHandle(
|
|
174
|
+
ref,
|
|
175
|
+
() => ({
|
|
176
|
+
version: VERSION,
|
|
177
|
+
fit: () => engineRef.current?.fit(),
|
|
178
|
+
panToTime: (t) => engineRef.current?.panToTime(t),
|
|
179
|
+
zoomToRange: (s, e) => engineRef.current?.zoomToRange(s, e),
|
|
180
|
+
select: (ids) => engineRef.current?.select(ids),
|
|
181
|
+
getViewport: () => engineRef.current?.getViewport() ?? EMPTY_VIEWPORT,
|
|
182
|
+
resize: (w, h, dpr) => engineRef.current?.resize(w, h, dpr),
|
|
183
|
+
setData: (d) => engineRef.current?.setData(d),
|
|
184
|
+
addEvents: (e) => engineRef.current?.addEvents(e),
|
|
185
|
+
removeEvents: (ids) => engineRef.current?.removeEvents(ids),
|
|
186
|
+
addElements: (el) => engineRef.current?.addElements(el),
|
|
187
|
+
updateElement: (id, patch) => engineRef.current?.updateElement(id, patch)
|
|
188
|
+
}),
|
|
189
|
+
[engineRef]
|
|
190
|
+
);
|
|
191
|
+
const onPointerMove = (e) => setPointer({ x: e.clientX, y: e.clientY });
|
|
192
|
+
return /* @__PURE__ */ jsxs(
|
|
193
|
+
"div",
|
|
194
|
+
{
|
|
195
|
+
ref: containerRef,
|
|
196
|
+
className,
|
|
197
|
+
style: { ...containerStyle, ...style },
|
|
198
|
+
"data-testid": "timeline-root",
|
|
199
|
+
onPointerMove,
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ jsx("canvas", { ref: baseRef, style: canvasStyle }),
|
|
202
|
+
/* @__PURE__ */ jsx("canvas", { ref: overlayRef, style: canvasStyle }),
|
|
203
|
+
/* @__PURE__ */ jsx(
|
|
204
|
+
Tooltip,
|
|
205
|
+
{
|
|
206
|
+
hit: hover,
|
|
207
|
+
x: pointer.x,
|
|
208
|
+
y: pointer.y,
|
|
209
|
+
render: props.renderTooltip
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
var Timeline = forwardRef(TimelineInner);
|
|
217
|
+
|
|
218
|
+
export { Timeline, Tooltip };
|
|
219
|
+
//# sourceMappingURL=index.js.map
|
|
220
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useTimeline.ts","../src/Tooltip.tsx","../src/Timeline.tsx"],"names":["rect","useRef","jsx"],"mappings":"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","file":"index.js","sourcesContent":["// React ↔ engine bridge (DESIGN.md §12).\n//\n// Instantiates the core engine once, owns the ResizeObserver, and syncs props\n// to the engine imperatively so React stays out of the render loop. All\n// browser APIs (ResizeObserver, devicePixelRatio) are touched inside effects,\n// never at import/render, keeping the binding SSR-safe; setup/teardown is\n// idempotent so React StrictMode's double-invoke is safe.\n\nimport { useEffect, useRef, type RefObject } from 'react';\nimport {\n TimelineEngine,\n type ClusteringOptions,\n type Formatters,\n type FrameStats,\n type HitResult,\n type LayoutStrategy,\n type LodOptions,\n type OnDiagnostic,\n type StyleResolvers,\n type Theme,\n type TimelineData,\n type TimelineEventMap,\n} from '@event-timeline/core';\n\nexport interface TimelineOptions {\n onDiagnostic?: OnDiagnostic;\n /** Vertical row-ordering strategy (DESIGN.md §7). Default `byFirstEvent`. */\n layout?: LayoutStrategy;\n /** Trailing-edge delay (ms) for coalesced incremental re-layout (DESIGN.md §7). */\n layoutDebounceMs?: number;\n /** Re-layout policy for streaming mutations (DESIGN.md §7, §10). Default stable-append. */\n streamingLayout?: 'stable-append' | 'full';\n /** Allow `ctrl`/`⌘`+click to extend the selection (DESIGN.md §9). */\n multiSelect?: boolean;\n /** Event collapsing/aggregation config (DESIGN.md §6). */\n clustering?: ClusteringOptions;\n /** Level-of-detail config, e.g. the event-label zoom threshold (DESIGN.md §6). */\n lod?: LodOptions;\n /** Optional per-frame timing sink for benchmarking (DESIGN.md §13). */\n onFrameStats?: (stats: FrameStats) => void;\n}\n\nexport interface UseTimelineArgs<TData> {\n baseRef: RefObject<HTMLCanvasElement | null>;\n overlayRef: RefObject<HTMLCanvasElement | null>;\n containerRef: RefObject<HTMLElement | null>;\n data?: TimelineData<TData>;\n theme?: Partial<Theme>;\n options?: TimelineOptions;\n /** Data-driven per-item style overrides (DESIGN.md §11, §12). */\n styleResolvers?: StyleResolvers<TData>;\n /** Label/tick text formatters (DESIGN.md §11, §12). */\n formatters?: Formatters<TData>;\n onReady?: () => void;\n onViewportChange?: (v: TimelineEventMap<TData>['viewportChange']) => void;\n onDataChange?: (d: TimelineEventMap<TData>['dataChange']) => void;\n onHover?: (hit: HitResult<TData> | null) => void;\n onClick?: (e: TimelineEventMap<TData>['click']) => void;\n onSelectionChange?: (ids: string[]) => void;\n}\n\nexport function useTimeline<TData>(\n args: UseTimelineArgs<TData>,\n): RefObject<TimelineEngine<TData> | null> {\n const { baseRef, overlayRef, containerRef, data, theme, options } = args;\n const { styleResolvers, formatters } = args;\n const engineRef = useRef<TimelineEngine<TData> | null>(null);\n\n // Keep the latest callbacks/data in refs so the setup effect doesn't re-run\n // when the host passes fresh inline functions each render.\n const callbacks = useRef(args);\n callbacks.current = args;\n const dataRef = useRef(data);\n dataRef.current = data;\n const appliedData = useRef<TimelineData<TData> | undefined>(undefined);\n\n // Engine lifecycle. Re-creates only when theme/options change by reference\n // (hosts memoize these — §12 referential-stability contract).\n useEffect(() => {\n const base = baseRef.current;\n const overlay = overlayRef.current;\n const container = containerRef.current;\n if (base === null || overlay === null || container === null) return;\n\n const engine = new TimelineEngine<TData>(base, overlay, {\n theme,\n onDiagnostic: options?.onDiagnostic,\n layout: options?.layout,\n layoutDebounceMs: options?.layoutDebounceMs,\n streamingLayout: options?.streamingLayout,\n multiSelect: options?.multiSelect,\n clustering: options?.clustering,\n lod: options?.lod,\n onFrameStats: options?.onFrameStats,\n styleResolvers,\n formatters,\n });\n engineRef.current = engine;\n\n const off = [\n engine.on('ready', () => callbacks.current.onReady?.()),\n engine.on('viewportChange', (v) =>\n callbacks.current.onViewportChange?.(v),\n ),\n engine.on('dataChange', (d) => callbacks.current.onDataChange?.(d)),\n engine.on('hover', (hit) => callbacks.current.onHover?.(hit)),\n engine.on('click', (e) => callbacks.current.onClick?.(e)),\n engine.on('selectionChange', (s) =>\n callbacks.current.onSelectionChange?.(s.ids),\n ),\n ];\n\n const applySize = (cssW: number, cssH: number) =>\n engine.resize(cssW, cssH, window.devicePixelRatio || 1);\n\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (rect) applySize(rect.width, rect.height);\n });\n observer.observe(container);\n\n const rect = container.getBoundingClientRect();\n applySize(rect.width, rect.height);\n\n if (dataRef.current) {\n engine.setData(dataRef.current);\n appliedData.current = dataRef.current;\n }\n\n return () => {\n for (const unsubscribe of off) unsubscribe();\n observer.disconnect();\n engine.dispose();\n engineRef.current = null;\n appliedData.current = undefined;\n };\n }, [\n baseRef,\n overlayRef,\n containerRef,\n theme,\n options,\n styleResolvers,\n formatters,\n ]);\n\n // Data sync: full setData whenever the `data` reference changes (§10, §12).\n useEffect(() => {\n const engine = engineRef.current;\n if (engine === null || data === appliedData.current) return;\n if (data) engine.setData(data);\n appliedData.current = data;\n }, [data]);\n\n return engineRef;\n}\n","// HTML hover tooltip (DESIGN.md §9, §12).\n//\n// Hover hit-testing happens on the canvas (core); the tooltip is plain HTML so\n// its text stays crisp (no canvas-text blur) and selectable. It is portalled to\n// `document.body` and positioned with `fixed` at the pointer's client\n// coordinates, so it is never clipped by the timeline container's `overflow:\n// hidden`. SSR-safe: renders nothing until a hover hit exists on the client.\n\nimport { createPortal } from 'react-dom';\nimport type { CSSProperties, ReactNode, ReactPortal } from 'react';\nimport type { HitResult } from '@event-timeline/core';\n\nexport interface TooltipProps<TData = unknown> {\n /** The current hover hit, or null when nothing is hovered. */\n hit: HitResult<TData> | null;\n /** Pointer position in client (viewport) pixels. */\n x: number;\n y: number;\n /** Optional host renderer for tooltip content; falls back to the default text. */\n render?: (hit: HitResult<TData>) => ReactNode;\n}\n\n/** Default tooltip text for a hit; host-customisable content arrives in Phase 6. */\nfunction defaultContent(hit: HitResult): string {\n switch (hit.kind) {\n case 'event':\n return hit.event.label ?? `${hit.event.sourceId} → ${hit.event.targetId}`;\n case 'element':\n return hit.element.label;\n case 'cluster':\n return `${hit.count} events`;\n }\n}\n\nconst tooltipStyle: CSSProperties = {\n position: 'fixed',\n pointerEvents: 'none',\n zIndex: 2147483647,\n padding: '4px 8px',\n borderRadius: 4,\n background: 'rgba(20, 24, 31, 0.95)',\n color: '#e6e6e6',\n font: '12px system-ui, sans-serif',\n whiteSpace: 'nowrap',\n boxShadow: '0 2px 8px rgba(0, 0, 0, 0.4)',\n};\n\nexport function Tooltip<TData>({\n hit,\n x,\n y,\n render,\n}: TooltipProps<TData>): ReactPortal | null {\n if (hit === null || typeof document === 'undefined') return null;\n return createPortal(\n <div\n data-testid=\"timeline-tooltip\"\n role=\"tooltip\"\n style={{ ...tooltipStyle, left: x + 12, top: y + 12 }}\n >\n {render ? render(hit) : defaultContent(hit)}\n </div>,\n document.body,\n );\n}\n","import {\n forwardRef,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ForwardedRef,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n VERSION,\n type Formatters,\n type HitResult,\n type StyleResolvers,\n type Theme,\n type Time,\n type TimelineData,\n type TimelineElement,\n type TimelineEvent,\n type TimelineEventMap,\n} from '@event-timeline/core';\nimport { useTimeline, type TimelineOptions } from './useTimeline';\nimport { Tooltip } from './Tooltip';\n\n/**\n * Props for the {@link Timeline} component (Phase 1 subset, DESIGN.md §12).\n * `data`, `theme`, and `options` should be memoized by the host: they are\n * diffed by reference and re-applied only when the reference changes.\n */\nexport interface TimelineProps<TData = unknown> {\n data?: TimelineData<TData>;\n theme?: Partial<Theme>;\n options?: TimelineOptions;\n /** Data-driven per-item style overrides (DESIGN.md §11, §12); memoize. */\n styleResolvers?: StyleResolvers<TData>;\n /** Label/tick text formatters (DESIGN.md §11, §12); memoize. */\n formatters?: Formatters<TData>;\n /** Optional host renderer for tooltip content; defaults to label-only text. */\n renderTooltip?: (hit: HitResult<TData>) => ReactNode;\n /** Class applied to the root container. */\n className?: string;\n /** Inline style merged over the default container style. */\n style?: CSSProperties;\n onReady?: () => void;\n onViewportChange?: (v: TimelineEventMap<TData>['viewportChange']) => void;\n onDataChange?: (d: TimelineEventMap<TData>['dataChange']) => void;\n onHover?: (hit: HitResult<TData> | null) => void;\n onClick?: (e: TimelineEventMap<TData>['click']) => void;\n onSelectionChange?: (ids: string[]) => void;\n}\n\n/** Imperative handle returned via `ref` (DESIGN.md §12). */\nexport interface TimelineHandle<TData = unknown> {\n readonly version: string;\n fit(): void;\n panToTime(t: Time): void;\n zoomToRange(start: Time, end: Time): void;\n select(ids: string[]): void;\n getViewport(): TimelineEventMap<TData>['viewportChange'];\n resize(cssW: number, cssH: number, dpr: number): void;\n setData(data: TimelineData<TData>): void;\n // Streaming methods (DESIGN.md §10): incremental updates that bypass the\n // prop-driven full `setData`, for high-frequency hosts.\n addEvents(events: readonly TimelineEvent<TData>[]): void;\n removeEvents(ids: readonly string[]): void;\n addElements(elements: readonly TimelineElement<TData>[]): void;\n updateElement(id: string, patch: Partial<TimelineElement<TData>>): void;\n}\n\nconst containerStyle: CSSProperties = {\n position: 'relative',\n width: '100%',\n height: '100%',\n overflow: 'hidden',\n};\n\nconst canvasStyle: CSSProperties = {\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n display: 'block',\n};\n\nconst EMPTY_VIEWPORT: TimelineEventMap['viewportChange'] = {\n timeRange: [0, 0],\n scrollY: 0,\n pxPerMs: 0,\n visibleRowRange: [0, 0],\n};\n\nfunction TimelineInner<TData>(\n props: TimelineProps<TData>,\n ref: ForwardedRef<TimelineHandle<TData>>,\n) {\n const { className, style, data, theme, options, styleResolvers, formatters } =\n props;\n const containerRef = useRef<HTMLDivElement>(null);\n const baseRef = useRef<HTMLCanvasElement>(null);\n const overlayRef = useRef<HTMLCanvasElement>(null);\n\n // Hover hit (from the engine) and the pointer position (from the DOM) drive\n // the HTML tooltip. Tracking position in React keeps canvas hit-testing in\n // the engine while text rendering stays crisp HTML (§9).\n const [hover, setHover] = useState<HitResult<TData> | null>(null);\n const [pointer, setPointer] = useState({ x: 0, y: 0 });\n\n const engineRef = useTimeline<TData>({\n baseRef,\n overlayRef,\n containerRef,\n data,\n theme,\n options,\n styleResolvers,\n formatters,\n onReady: props.onReady,\n onViewportChange: props.onViewportChange,\n onDataChange: props.onDataChange,\n onHover: (hit) => {\n setHover(hit);\n props.onHover?.(hit);\n },\n onClick: props.onClick,\n onSelectionChange: props.onSelectionChange,\n });\n\n useImperativeHandle(\n ref,\n (): TimelineHandle<TData> => ({\n version: VERSION,\n fit: () => engineRef.current?.fit(),\n panToTime: (t) => engineRef.current?.panToTime(t),\n zoomToRange: (s, e) => engineRef.current?.zoomToRange(s, e),\n select: (ids) => engineRef.current?.select(ids),\n getViewport: () => engineRef.current?.getViewport() ?? EMPTY_VIEWPORT,\n resize: (w, h, dpr) => engineRef.current?.resize(w, h, dpr),\n setData: (d) => engineRef.current?.setData(d),\n addEvents: (e) => engineRef.current?.addEvents(e),\n removeEvents: (ids) => engineRef.current?.removeEvents(ids),\n addElements: (el) => engineRef.current?.addElements(el),\n updateElement: (id, patch) => engineRef.current?.updateElement(id, patch),\n }),\n [engineRef],\n );\n\n const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) =>\n setPointer({ x: e.clientX, y: e.clientY });\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ ...containerStyle, ...style }}\n data-testid=\"timeline-root\"\n onPointerMove={onPointerMove}\n >\n <canvas ref={baseRef} style={canvasStyle} />\n <canvas ref={overlayRef} style={canvasStyle} />\n <Tooltip\n hit={hover}\n x={pointer.x}\n y={pointer.y}\n render={props.renderTooltip}\n />\n </div>\n );\n}\n\n/**\n * Canvas timeline component. Owns the two stacked `<canvas>` layers and the\n * ResizeObserver, and drives the core engine imperatively (DESIGN.md §4, §12).\n */\nexport const Timeline = forwardRef(TimelineInner) as <TData = unknown>(\n props: TimelineProps<TData> & { ref?: Ref<TimelineHandle<TData>> },\n) => ReturnType<typeof TimelineInner>;\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@event-timeline/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React binding for the Canvas timeline engine.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Karl Gorgoglione",
|
|
7
|
+
"homepage": "https://github.com/KarlGorgoglione/event-timeline/tree/main/packages/react#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/KarlGorgoglione/event-timeline.git",
|
|
11
|
+
"directory": "packages/react"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/KarlGorgoglione/event-timeline/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"timeline",
|
|
18
|
+
"canvas",
|
|
19
|
+
"react",
|
|
20
|
+
"visualization",
|
|
21
|
+
"events",
|
|
22
|
+
"graph",
|
|
23
|
+
"typescript"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"main": "./dist/index.cjs",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"default": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/index.d.cts",
|
|
44
|
+
"default": "./dist/index.cjs"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">=18",
|
|
50
|
+
"react-dom": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@event-timeline/core": "0.1.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
57
|
+
"@testing-library/react": "^16.1.0",
|
|
58
|
+
"@types/react": "^19.0.2",
|
|
59
|
+
"@types/react-dom": "^19.0.2",
|
|
60
|
+
"jsdom": "^25.0.1",
|
|
61
|
+
"react": "^19.0.0",
|
|
62
|
+
"react-dom": "^19.0.0",
|
|
63
|
+
"tsup": "^8.3.5",
|
|
64
|
+
"typescript": "^5.7.2"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsup",
|
|
68
|
+
"dev": "tsup --watch",
|
|
69
|
+
"typecheck": "tsc --noEmit"
|
|
70
|
+
}
|
|
71
|
+
}
|