@parallelworks/ui 0.40.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,94 @@
1
+ import { Store, useStore } from "./store.js";
2
+ import { createContext, useContext } from "react";
3
+ //#region src/graph/lineage.ts
4
+ /**
5
+ * The hovered job's lineage on one graph level: the jobs it leads to or follows from, by hops. Each
6
+ * node, connector and dot reads only its own part of it, so hovering re-renders those whose highlight
7
+ * changes and nothing else, however large the graph.
8
+ */
9
+ var Lineage = class extends Store {
10
+ /** Hops from the hovered job, or none while no job is. */
11
+ hovered = null;
12
+ /** The lineage shown, kept after unhover while its highlight draws back out. */
13
+ shown = null;
14
+ furthest = 0;
15
+ timer;
16
+ walk;
17
+ /** Seconds each hop of the highlight takes. */
18
+ animT;
19
+ /** How deeply the level nests, which names the classes that dim it. */
20
+ depth;
21
+ /** Whether its nodes fade as they dim, rather than dim at once. */
22
+ fade;
23
+ constructor(walk, { animT, depth = 0, fade = true }) {
24
+ super();
25
+ this.walk = walk;
26
+ this.animT = animT;
27
+ this.depth = depth;
28
+ this.fade = fade;
29
+ }
30
+ get hovering() {
31
+ return this.hovered !== null;
32
+ }
33
+ /** Whether a lineage shows, hovered or drawing out. */
34
+ get showing() {
35
+ return this.shown !== null;
36
+ }
37
+ hover = (job) => {
38
+ clearTimeout(this.timer);
39
+ this.hovered = job === null ? null : this.walk(job);
40
+ if (this.hovered) {
41
+ this.shown = this.hovered;
42
+ this.furthest = 0;
43
+ for (const hops of this.hovered.values()) this.furthest = Math.max(this.furthest, hops);
44
+ } else if (this.shown) this.timer = setTimeout(() => {
45
+ this.shown = null;
46
+ this.furthest = 0;
47
+ this.notify();
48
+ }, ((this.furthest + 1) * this.animT + .2) * 1e3);
49
+ this.notify();
50
+ };
51
+ /** Stops a draw-out still to finish, as the level goes. */
52
+ dispose() {
53
+ clearTimeout(this.timer);
54
+ }
55
+ /** A node's highlight: 2 in the hovered lineage, 1 drawing out of it, 0 neither. */
56
+ node(job) {
57
+ return this.hovered?.has(job) ? 2 : this.shown?.has(job) ? 1 : 0;
58
+ }
59
+ /**
60
+ * A dot's or connector's highlight between `from` and `to` (a dot's both the same job), as one
61
+ * value React compares: none, or lit or drawing out, its delay, and whether it draws from `to`.
62
+ */
63
+ between(from, to) {
64
+ const shown = this.shown;
65
+ const fromHops = shown?.get(from);
66
+ const toHops = shown?.get(to);
67
+ if (fromHops === void 0 || toHops === void 0) return "";
68
+ const lit = !!this.hovered?.has(from) && this.hovered.has(to);
69
+ const closer = Math.min(fromHops, toHops);
70
+ const delay = (lit ? closer : this.furthest - closer) * this.animT;
71
+ return `${lit ? "lit" : "out"} ${delay} ${toHops < fromHops ? "back" : "on"}`;
72
+ }
73
+ };
74
+ /** A highlight `between` describes, read back. */
75
+ function highlightOf(value) {
76
+ const [state = "", delay = "0", direction] = value.split(" ");
77
+ return {
78
+ lit: state === "lit",
79
+ out: state === "out",
80
+ delay: Number(delay),
81
+ back: direction === "back"
82
+ };
83
+ }
84
+ var LineageContext = createContext(null);
85
+ /** Part of the enclosing level's lineage, re-rendering only when that part changes. */
86
+ function useLineage(read, none) {
87
+ return useStore(useContext(LineageContext), read, none);
88
+ }
89
+ /** The enclosing level's lineage, for what hovers it. */
90
+ function useLineageStore() {
91
+ return useContext(LineageContext);
92
+ }
93
+ //#endregion
94
+ export { Lineage, LineageContext, highlightOf, useLineage, useLineageStore };
@@ -0,0 +1,10 @@
1
+ /** State components read part of, each re-rendering only when its part changes. */
2
+ export declare class Store {
3
+ /** Counts changes, so what follows the whole of a store can tell one happened. */
4
+ version: number;
5
+ private readonly listeners;
6
+ subscribe: (listener: () => void) => () => void;
7
+ protected notify(): void;
8
+ }
9
+ /** What `read` takes from `store`, re-rendering when it changes; `none` without a store. */
10
+ export declare function useStore<S extends Store, T extends string | number>(store: S | null, read: (store: S) => T, none: T): T;
@@ -0,0 +1,25 @@
1
+ import { useSyncExternalStore } from "react";
2
+ //#region src/graph/store.ts
3
+ /** State components read part of, each re-rendering only when its part changes. */
4
+ var Store = class {
5
+ /** Counts changes, so what follows the whole of a store can tell one happened. */
6
+ version = 0;
7
+ listeners = /* @__PURE__ */ new Set();
8
+ subscribe = (listener) => {
9
+ this.listeners.add(listener);
10
+ return () => {
11
+ this.listeners.delete(listener);
12
+ };
13
+ };
14
+ notify() {
15
+ this.version++;
16
+ for (const listener of this.listeners) listener();
17
+ }
18
+ };
19
+ var noSubscription = () => () => {};
20
+ /** What `read` takes from `store`, re-rendering when it changes; `none` without a store. */
21
+ function useStore(store, read, none) {
22
+ return useSyncExternalStore(store?.subscribe ?? noSubscription, () => store ? read(store) : none, () => none);
23
+ }
24
+ //#endregion
25
+ export { Store, useStore };
@@ -0,0 +1,40 @@
1
+ /** How far out the view zooms: far enough to show the shape of a graph of thousands of jobs. */
2
+ export declare const MIN_SCALE = 0.01;
3
+ /** How far in the view zooms, past where a fit stops, to read a node up close. */
4
+ export declare const MAX_SCALE = 2;
5
+ /** The closest a fit comes, where a small graph's nodes read at a comfortable size. */
6
+ export declare const FIT_MAX_SCALE = 0.6;
7
+ /** An exponent of the scale, so presses in and out change it by the same factor. */
8
+ export declare const ZOOM_STEP = 0.25;
9
+ export interface View {
10
+ x: number;
11
+ y: number;
12
+ scale: number;
13
+ }
14
+ export interface Bounds {
15
+ minX: number;
16
+ minY: number;
17
+ maxX: number;
18
+ maxY: number;
19
+ }
20
+ /** The way the view can zoom no further, if it is at a limit. */
21
+ export declare function zoomLimit(scale: number): 'in' | 'out' | null;
22
+ /** The view scaled by `factor` about `at`, a point in the panel, which stays where it is. */
23
+ export declare function zoomAround(view: View, at: {
24
+ x: number;
25
+ y: number;
26
+ }, factor: number): View;
27
+ /** The factor a wheel event scales the view by, the same at every zoom. */
28
+ export declare function wheelFactor(event: Pick<WheelEvent, 'deltaY' | 'deltaMode' | 'ctrlKey'>): number;
29
+ /** The space a fit leaves around a graph in a panel. */
30
+ export declare function fitMargin(panel: {
31
+ width: number;
32
+ height: number;
33
+ }): number;
34
+ /** The view that shows all of `bounds` in a panel from its top left, no closer than a fit comes. */
35
+ export declare function fitView(bounds: Bounds, panel: {
36
+ width: number;
37
+ height: number;
38
+ }): View | null;
39
+ /** The least shift that brings [start, end] inside [low, high]; none when it can't all fit, so the view stays. */
40
+ export declare function intoView(start: number, end: number, low: number, high: number): number;
@@ -0,0 +1,56 @@
1
+ //#region src/graph/viewport.ts
2
+ /** How far out the view zooms: far enough to show the shape of a graph of thousands of jobs. */
3
+ var MIN_SCALE = .01;
4
+ /** The closest a fit comes, where a small graph's nodes read at a comfortable size. */
5
+ var FIT_MAX_SCALE = .6;
6
+ /** An exponent of the scale, so presses in and out change it by the same factor. */
7
+ var ZOOM_STEP = .25;
8
+ var clampScale = (scale) => Math.min(2, Math.max(MIN_SCALE, scale));
9
+ /** The way the view can zoom no further, if it is at a limit. */
10
+ function zoomLimit(scale) {
11
+ return scale >= 1.998 ? "in" : scale <= .01001 ? "out" : null;
12
+ }
13
+ /** The view scaled by `factor` about `at`, a point in the panel, which stays where it is. */
14
+ function zoomAround(view, at, factor) {
15
+ const scale = clampScale(view.scale * factor);
16
+ return {
17
+ x: at.x - (at.x - view.x) / view.scale * scale,
18
+ y: at.y - (at.y - view.y) / view.scale * scale,
19
+ scale
20
+ };
21
+ }
22
+ var LINE_PX = 16;
23
+ var PAGE_PX = 800;
24
+ var WHEEL_RATE = .002;
25
+ var PINCH_RATE = .01;
26
+ var MAX_WHEEL_PX = 120;
27
+ /** The factor a wheel event scales the view by, the same at every zoom. */
28
+ function wheelFactor(event) {
29
+ const px = event.deltaY * (event.deltaMode === 1 ? LINE_PX : event.deltaMode === 2 ? PAGE_PX : 1);
30
+ return Math.exp(-Math.max(-120, Math.min(MAX_WHEEL_PX, px)) * (event.ctrlKey ? PINCH_RATE : WHEEL_RATE));
31
+ }
32
+ /** The space a fit leaves around a graph in a panel. */
33
+ function fitMargin(panel) {
34
+ return Math.min(panel.width, panel.height) * .05;
35
+ }
36
+ /** The view that shows all of `bounds` in a panel from its top left, no closer than a fit comes. */
37
+ function fitView(bounds, panel) {
38
+ const width = bounds.maxX - bounds.minX;
39
+ const height = bounds.maxY - bounds.minY;
40
+ if (width <= 0 || height <= 0) return null;
41
+ const margin = fitMargin(panel);
42
+ const scale = Math.max(MIN_SCALE, Math.min((panel.width - margin * 2) / width, (panel.height - margin * 2) / height, FIT_MAX_SCALE));
43
+ return {
44
+ x: margin - bounds.minX * scale,
45
+ y: margin - bounds.minY * scale,
46
+ scale
47
+ };
48
+ }
49
+ /** The least shift that brings [start, end] inside [low, high]; none when it can't all fit, so the view stays. */
50
+ function intoView(start, end, low, high) {
51
+ if (end - start > high - low) return 0;
52
+ if (start < low) return low - start;
53
+ return end > high ? high - end : 0;
54
+ }
55
+ //#endregion
56
+ export { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, fitMargin, fitView, intoView, wheelFactor, zoomAround, zoomLimit };