@motionstudies/web 0.1.0-alpha.20 → 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
@@ -255,3 +255,13 @@ Render support as opacity or reach, not as confidence in the underlying measurem
255
255
  `@motionstudies/core/domain/daylight` computes `solarPosition` (altitude and azimuth in degrees, azimuth clockwise from north) from the Astronomy Answers low-precision algorithm, matching SunCalc to a thousandth of a degree without a dependency. `daylightAt` adds a unit `direction` towards the sun in a scene frame with +x east, +y up and +z south, and smooth `twilight`, `warmth` and `daylight` envelopes from `DaylightEnvelopes` (defaults: twilight −18° to −1°, warmth rising −12° to −1° and falling 3° to 15°, daylight −1° to 12°). The envelopes are artistic responses to sun altitude and encode no weather or sky observation. Sample several longitudes to place a terminator across a national field.
256
256
 
257
257
  The lab's **Fields** specimen renders a synthetic archipelago of seven instruments with a mid-study gap and one vectorless instrument, an invented cover series as haze, integrated trails, and a terminator crossing with the study clock. Brightness is the estimate and opacity is support; clicking reads the estimate, cover and sun altitude at a place.
258
+
259
+ ## Recorded power days
260
+
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.20",
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.20"
180
+ "@motionstudies/core": "0.1.0-alpha.22"
175
181
  },
176
182
  "peerDependencies": {
177
183
  "react": "^19.2.8",