@motionstudies/web 0.1.0-alpha.21 → 0.1.0-alpha.22

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
@@ -259,3 +259,9 @@ The lab's **Fields** specimen renders a synthetic archipelago of seven instrumen
259
259
  ## Recorded power days
260
260
 
261
261
  `@motionstudies/core/domain/power-day` exports `readPowerDay`, `powerPeriod`, `unitPower`, `plannedPower`, `sumPower` and `fuelMixAt`, plus their data types. A complete civil day declares its timezone, UTC bounds, half-hour intervals, MWh settlement quantities and MW plans/aggregates. Missing values remain null; signed quantities remain signed. Interval energy becomes average power only through the explicit interval duration. Declarations are interpolated only within one unambiguous segment; fuel samples expire after five minutes. Callers choose aggregate membership and receive a missing-contributor count. The reader validates source links, settlement run labels, identities, placement coordinates and safe evidence-artifact paths without fetching data. Source adapters, placement policy and the composition stay outside core. See [power ownership](../docs/POWER.md#ownership-and-handoff).
262
+
263
+ ## Activity timeline
264
+
265
+ `@motionstudies/core/timeline` supplies validated interval geometry, lookup, time mapping and service-time formatting. `@motionstudies/web/components/StudyTimeline` renders a controlled activity chart and scrubber; import `@motionstudies/web/study-timeline.css`. Pass `bins: {start, end, value}[]`, the time window, current `time` and `onSeek`. A `null` value means missing evidence; zero is a measured zero. `variant="bars"` (default) or `"line"` shares the same time axis. `maximum` optionally fixes the scale for comparisons. Playback and loading remain consumer-owned. See [the full contract](https://github.com/emmettl/motionstudies/blob/main/docs/STUDY-TIMELINE.md) and the Timeline lab specimen.
266
+
267
+ Canvas editions can import `PausedVehicleFrame` from `@motionstudies/core/render-frame` without loading Three.js. The existing Three.js entry point re-exports the same class. Record only a frame actually submitted; change its visibility revision (or replace the tracker) when data, projection, size or selection changes. Time changes invalidate even while paused.
@@ -0,0 +1,25 @@
1
+ import { type TimelineBin } from '@motionstudies/core/timeline';
2
+ export interface StudyTimelineProps {
3
+ bins: readonly TimelineBin[];
4
+ windowStart: number;
5
+ windowEnd: number;
6
+ time: number;
7
+ onSeek: (time: number) => void;
8
+ label: string;
9
+ ariaLabel?: string;
10
+ description?: string;
11
+ variant?: 'bars' | 'line';
12
+ step?: number;
13
+ maximum?: number;
14
+ disabled?: boolean;
15
+ formatTime?: (time: number) => string;
16
+ formatValue?: (value: number) => string;
17
+ missingLabel?: string;
18
+ emptyLabel?: string;
19
+ scaleLabel?: string;
20
+ onScrubStart?: () => void;
21
+ onScrubEnd?: () => void;
22
+ className?: string;
23
+ }
24
+ /** Controlled chart and scrubber. Playback, loading and date/timezone policy belong to the study. */
25
+ export declare function StudyTimeline({ bins, windowStart, windowEnd, time, onSeek, label, ariaLabel, description, variant, step, maximum, disabled, formatTime, formatValue, missingLabel, emptyLabel, scaleLabel, onScrubStart, onScrubEnd, className }: StudyTimelineProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useMemo, useRef, useState } from 'react';
3
+ import { createTimelineModel, formatTimelineTime, timelineBinAt, timelinePosition, timelineTime } from '@motionstudies/core/timeline';
4
+ const formatCount = (value) => value.toLocaleString();
5
+ /** Controlled chart and scrubber. Playback, loading and date/timezone policy belong to the study. */
6
+ export function StudyTimeline({ bins, windowStart, windowEnd, time, onSeek, label, ariaLabel = label, description, variant = 'bars', step = 1, maximum, disabled = false, formatTime = formatTimelineTime, formatValue = formatCount, missingLabel = 'No observation', emptyLabel = 'No observations in this window', scaleLabel = 'Scale', onScrubStart, onScrubEnd, className = '' }) {
7
+ const id = useId(), [hover, setHover] = useState(), scrubbing = useRef(false);
8
+ const model = useMemo(() => createTimelineModel(bins, { start: windowStart, end: windowEnd }, maximum), [bins, windowStart, windowEnd, maximum]);
9
+ const position = timelinePosition(time, model.window), value = Number(timelineTime(position, model.window, step).toPrecision(12));
10
+ const active = timelineBinAt(model, value), inspected = timelineBinAt(model, hover ?? value);
11
+ const reading = (amount) => amount == null ? missingLabel : formatValue(amount);
12
+ const begin = () => { if (!disabled && !scrubbing.current) {
13
+ scrubbing.current = true;
14
+ onScrubStart?.();
15
+ } };
16
+ const finish = () => { if (scrubbing.current) {
17
+ scrubbing.current = false;
18
+ onScrubEnd?.();
19
+ } };
20
+ return _jsxs("div", { className: `ms-study-timeline ${className}`, "data-variant": variant, "data-disabled": disabled || undefined, children: [_jsxs("div", { className: "ms-study-timeline__heading", children: [_jsx("span", { id: `${id}-label`, children: label }), _jsxs("span", { className: "ms-study-timeline__reading", children: [formatTime(hover ?? value), " \u00B7 ", reading(inspected?.value)] })] }), _jsxs("div", { className: "ms-study-timeline__plot", children: [_jsxs("div", { className: "ms-study-timeline__graph", "aria-hidden": "true", children: [_jsx("svg", { viewBox: "0 0 1000 100", preserveAspectRatio: "none", focusable: "false", children: variant === 'bars' ? model.bars.filter(bar => bar.value !== null && bar.value > 0).map(bar => _jsx("rect", { x: bar.x * 1000, y: (1 - bar.height) * 100, width: bar.width * 1000 * .8, height: bar.height * 100 }, bar.start)) : model.lines.map((line, i) => line.length === 1 ? _jsx("line", { x1: line[0].x * 1000 - 1, x2: line[0].x * 1000 + 1, y1: line[0].y * 100, y2: line[0].y * 100 }, i) : _jsx("polyline", { points: line.map(p => `${p.x * 1000},${p.y * 100}`).join(' ') }, i)) }), _jsx("span", { className: "ms-study-timeline__cursor", style: { left: `${position * 100}%` } }), !model.bars.some(bar => bar.value !== null) && _jsx("span", { className: "ms-study-timeline__empty", children: emptyLabel })] }), _jsx("input", { type: "range", "aria-label": ariaLabel, "aria-describedby": description ? `${id}-description` : undefined, "aria-valuetext": `${formatTime(value)} · ${reading(active?.value)}`, min: windowStart, max: windowEnd, step: step, value: value, disabled: disabled, onChange: event => onSeek(Number(event.target.value)), onPointerDown: begin, onPointerUp: finish, onPointerCancel: finish, onBlur: () => { finish(); setHover(undefined); }, onKeyDown: event => { if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key))
21
+ begin(); }, onKeyUp: finish, onPointerMove: event => { if (disabled || event.pointerType === 'touch')
22
+ return; const box = event.currentTarget.getBoundingClientRect(); setHover(timelineTime((event.clientX - box.left - 9) / Math.max(1, box.width - 18), model.window, step)); }, onPointerLeave: () => setHover(undefined) })] }), _jsx("div", { className: "ms-study-timeline__ticks", "aria-hidden": "true", children: [0, .25, .5, .75, 1].map(fraction => _jsx("span", { children: formatTime(windowStart + fraction * (windowEnd - windowStart)) }, fraction)) }), _jsxs("div", { className: "ms-study-timeline__footnote", children: [description && _jsx("span", { id: `${id}-description`, children: description }), _jsxs("span", { children: [scaleLabel, " 0\u2013", formatValue(model.maximum)] })] })] });
23
+ }
@@ -0,0 +1,8 @@
1
+ .ms-study-timeline{--ms-timeline-ink:#dce7eb;--ms-timeline-muted:#8ba3af;--ms-timeline-accent:#e2bc7c;--ms-timeline-chart:#668b9e;--ms-timeline-height:48px;min-width:0;color:var(--ms-timeline-ink);font:10px/1.5 ui-monospace,monospace}
2
+ .ms-study-timeline__heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px;min-height:20px}.ms-study-timeline__reading{white-space:nowrap;color:var(--ms-timeline-accent);font-variant-numeric:tabular-nums}
3
+ .ms-study-timeline__plot{position:relative;height:calc(var(--ms-timeline-height) + 20px);margin-top:4px;isolation:isolate}.ms-study-timeline__graph{position:absolute;inset:4px 9px 12px;border-bottom:1px solid var(--ms-timeline-muted);pointer-events:none}.ms-study-timeline svg{display:block;width:100%;height:100%;overflow:visible}.ms-study-timeline rect{fill:var(--ms-timeline-chart)}.ms-study-timeline polyline,.ms-study-timeline line{fill:none;stroke:var(--ms-timeline-chart);stroke-width:1.5;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
4
+ .ms-study-timeline__cursor{position:absolute;top:0;bottom:-1px;width:1px;background:var(--ms-timeline-accent);transform:translateX(-.5px)}.ms-study-timeline__cursor::after{content:'';position:absolute;bottom:-4px;left:-3px;width:7px;height:7px;background:var(--ms-timeline-accent);transform:rotate(45deg)}
5
+ .ms-study-timeline__plot input[type=range]{position:absolute;inset:0;display:block;box-sizing:border-box;width:100%;height:100%;margin:0;padding:0;opacity:0;cursor:ew-resize;touch-action:pan-y;appearance:none;-webkit-appearance:none}.ms-study-timeline__plot input::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;width:18px;height:44px}.ms-study-timeline__plot input::-moz-range-thumb{width:18px;height:44px;border:0;border-radius:0}.ms-study-timeline__plot:focus-within{outline:2px solid var(--ms-timeline-accent);outline-offset:2px;border-radius:2px}
6
+ .ms-study-timeline__ticks{display:flex;justify-content:space-between;gap:8px;padding:0 9px;font-size:9px;font-variant-numeric:tabular-nums;color:var(--ms-timeline-muted)}.ms-study-timeline__footnote{display:flex;justify-content:space-between;flex-wrap:wrap;gap:4px 14px;margin-top:8px;font-size:9px;color:var(--ms-timeline-muted)}.ms-study-timeline__footnote>span:last-child{margin-left:auto}.ms-study-timeline__empty{position:absolute;inset:0;display:grid;place-items:center;color:var(--ms-timeline-muted);font-size:10px}.ms-study-timeline[data-disabled]{opacity:.55}.ms-study-timeline[data-disabled] input{cursor:default}
7
+ @media(max-width:480px){.ms-study-timeline__heading{gap:4px;flex-wrap:wrap;font-size:9px}.ms-study-timeline__ticks,.ms-study-timeline__footnote{font-size:8px}}
8
+ @media(forced-colors:active){.ms-study-timeline{--ms-timeline-ink:CanvasText;--ms-timeline-muted:CanvasText;--ms-timeline-accent:Highlight;--ms-timeline-chart:CanvasText}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motionstudies/web",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.22",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Browser controls and loaders for Motion Studies.",
@@ -154,7 +154,13 @@
154
154
  "default": "./components/NetworkVehicleHeroCard.js"
155
155
  },
156
156
  "./study-layout.css": "./study-layout.css",
157
- "./fonts.css": "./fonts.css"
157
+ "./fonts.css": "./fonts.css",
158
+ "./components/StudyTimeline": {
159
+ "types": "./components/StudyTimeline.d.ts",
160
+ "import": "./components/StudyTimeline.js",
161
+ "default": "./components/StudyTimeline.js"
162
+ },
163
+ "./study-timeline.css": "./components/study-timeline.css"
158
164
  },
159
165
  "files": [
160
166
  "**/*.js",
@@ -171,7 +177,7 @@
171
177
  "**/*.css"
172
178
  ],
173
179
  "dependencies": {
174
- "@motionstudies/core": "0.1.0-alpha.21"
180
+ "@motionstudies/core": "0.1.0-alpha.22"
175
181
  },
176
182
  "peerDependencies": {
177
183
  "react": "^19.2.8",