@parallelworks/ui 0.82.2 → 0.83.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.
@@ -18,11 +18,18 @@ export declare function arcRadius(minRise: number, colGap: number): number;
18
18
  * a connector spanning several columns runs level until it nears its target.
19
19
  */
20
20
  export declare function connectorPath(start: Point, end: Point, r: number, colGap: number): ConnectorPath;
21
+ /** Which way a graph runs: ranks as columns, left to right, or as rows, top to bottom. */
22
+ export type GraphDirection = 'right' | 'down';
23
+ /** A connector from a node's bottom to a later row's node's top: `connectorPath` turned a quarter,
24
+ * with the bends in the gap above `end`'s row. */
25
+ export declare function connectorPathDown(start: Point, end: Point, r: number, rowGap: number): ConnectorPath;
21
26
  /** A node's place in its graph level, as laid out. */
22
27
  export interface Box {
23
28
  x: number;
24
29
  y: number;
25
30
  width: number;
31
+ /** Where a graph runs down, its height, which places the connectors leaving its bottom. */
32
+ height?: number | undefined;
26
33
  }
27
34
  /** Whether two layouts put every node in the same place. */
28
35
  export declare function sameBoxes(a: Map<string, Box>, b: Map<string, Box>): boolean;
@@ -41,4 +48,6 @@ export interface DrawnEdge {
41
48
  */
42
49
  export declare function routeConnectors(cols: string[][][],
43
50
  /** Whether columns are left empty between those drawn, so gaps between them differ. */
44
- spaced: boolean, deps: Record<string, string[]>, boxes: Map<string, Box>, connectorY: number): DrawnEdge[];
51
+ spaced: boolean, deps: Record<string, string[]>, boxes: Map<string, Box>, connectorY: number,
52
+ /** `down` joins a node's bottom to the top of one in a later row, at their middles. */
53
+ direction?: GraphDirection): DrawnEdge[];
@@ -44,12 +44,44 @@ function connectorPath(start, end, r, colGap) {
44
44
  ].join(" ")
45
45
  };
46
46
  }
47
+ /** A connector from a node's bottom to a later row's node's top: `connectorPath` turned a quarter,
48
+ * with the bends in the gap above `end`'s row. */
49
+ function connectorPathDown(start, end, r, rowGap) {
50
+ const across = connectorPath({
51
+ x: start.y,
52
+ y: start.x
53
+ }, {
54
+ x: end.y,
55
+ y: end.x
56
+ }, r, rowGap);
57
+ return {
58
+ d: transposed(across.d),
59
+ reversed: transposed(across.reversed)
60
+ };
61
+ }
62
+ function transposed(d) {
63
+ const parts = d.split(" ");
64
+ const out = [];
65
+ for (let i = 0; i < parts.length;) {
66
+ const op = parts[i++] ?? "";
67
+ if (op === "A") {
68
+ const [rx, ry, rot, large, sweep, x, y] = parts.slice(i, i + 7);
69
+ out.push("A", ry ?? "", rx ?? "", rot ?? "", large ?? "", sweep === "1" ? "0" : "1", y ?? "", x ?? "");
70
+ i += 7;
71
+ } else {
72
+ const [x, y] = parts.slice(i, i + 2);
73
+ out.push(op, y ?? "", x ?? "");
74
+ i += 2;
75
+ }
76
+ }
77
+ return out.join(" ");
78
+ }
47
79
  /** Whether two layouts put every node in the same place. */
48
80
  function sameBoxes(a, b) {
49
81
  if (a.size !== b.size) return false;
50
82
  for (const [job, box] of a) {
51
83
  const other = b.get(job);
52
- if (!other || other.x !== box.x || other.y !== box.y || other.width !== box.width) return false;
84
+ if (!other || other.x !== box.x || other.y !== box.y || other.width !== box.width || other.height !== box.height) return false;
53
85
  }
54
86
  return true;
55
87
  }
@@ -58,7 +90,8 @@ function sameBoxes(a, b) {
58
90
  * jobs share their needs, so its first job's needs are the node's; a need naming a node not laid out
59
91
  * draws nothing. `connectorY` is how far below a node's top its connectors meet it.
60
92
  */
61
- function routeConnectors(cols, spaced, deps, boxes, connectorY) {
93
+ function routeConnectors(cols, spaced, deps, boxes, connectorY, direction = "right") {
94
+ if (direction === "down") return routeDown(cols, deps, boxes);
62
95
  const heads = (c) => cols[c]?.[0]?.[0];
63
96
  const gapAfter = (c) => {
64
97
  const left = boxes.get(heads(c) ?? "");
@@ -99,5 +132,50 @@ function routeConnectors(cols, spaced, deps, boxes, connectorY) {
99
132
  path: connectorPath(edge.start, edge.end, r, colGap)
100
133
  }));
101
134
  }
135
+ function routeDown(rows, deps, boxes) {
136
+ const top = (row) => Math.min(...row.map(([head]) => boxes.get(head ?? "")?.y ?? Number.POSITIVE_INFINITY));
137
+ const bottom = (row) => Math.max(...row.map(([head]) => {
138
+ const box = boxes.get(head ?? "");
139
+ return box ? box.y + (box.height ?? 0) : Number.NEGATIVE_INFINITY;
140
+ }));
141
+ let rowGap = Number.POSITIVE_INFINITY;
142
+ for (let r = 0; r + 1 < rows.length; r++) {
143
+ const gap = top(rows[r + 1] ?? []) - bottom(rows[r] ?? []);
144
+ if (Number.isFinite(gap)) rowGap = Math.min(rowGap, gap);
145
+ }
146
+ if (!Number.isFinite(rowGap)) rowGap = 32;
147
+ const ends = [];
148
+ let minShift = Number.POSITIVE_INFINITY;
149
+ for (const row of rows) for (const [to] of row) {
150
+ const target = to === void 0 ? void 0 : boxes.get(to);
151
+ if (to === void 0 || !target) continue;
152
+ for (const from of deps[to] ?? []) {
153
+ const source = boxes.get(from);
154
+ if (!source) continue;
155
+ const start = {
156
+ x: Math.round(source.x + source.width / 2),
157
+ y: source.y + (source.height ?? 0) - 2
158
+ };
159
+ const middle = Math.round(target.x + target.width / 2);
160
+ const end = {
161
+ x: Math.abs(middle - start.x) <= 8 ? start.x : middle,
162
+ y: target.y + 2
163
+ };
164
+ const shift = Math.abs(end.x - start.x);
165
+ if (shift > 0) minShift = Math.min(minShift, shift);
166
+ ends.push({
167
+ from,
168
+ to,
169
+ start,
170
+ end
171
+ });
172
+ }
173
+ }
174
+ const r = arcRadius(minShift, rowGap);
175
+ return ends.map((edge) => ({
176
+ ...edge,
177
+ path: connectorPathDown(edge.start, edge.end, r, rowGap)
178
+ }));
179
+ }
102
180
  //#endregion
103
- export { arcRadius, connectorPath, routeConnectors, sameBoxes };
181
+ export { arcRadius, connectorPath, connectorPathDown, routeConnectors, sameBoxes };
@@ -31,13 +31,37 @@ export declare function fitMargin(panel: {
31
31
  width: number;
32
32
  height: number;
33
33
  }): number;
34
+ /** How a fit may differ from showing everything. */
35
+ export interface FitOptions {
36
+ /** The farthest out it goes: a graph too big to read at this scale shows from its start,
37
+ * its top left, for the rest to be panned to. */
38
+ minScale?: number | undefined;
39
+ /** Room kept clear at the panel's top, such as under a bar over it, when the fit stops at
40
+ * `minScale`. */
41
+ top?: number | undefined;
42
+ /** Fits the graph's width alone, for one that runs down the panel to be panned along: centered
43
+ * across the panel, from its top below `top`. */
44
+ width?: boolean | undefined;
45
+ }
34
46
  /** The view that shows all of `bounds` in a panel from its top left, no closer than a fit comes. */
35
47
  export declare function fitView(bounds: Bounds, panel: {
36
48
  width: number;
37
49
  height: number;
38
- }): View | null;
50
+ }, { minScale, top, width: byWidth }?: FitOptions): View | null;
39
51
  /** The least shift that brings [start, end] inside [low, high]; none when it can't all fit, so the view stays. */
40
52
  export declare function intoView(start: number, end: number, low: number, high: number): number;
53
+ /** How much of a graph a pan or pinch leaves in its panel, at the least: half the panel's shorter
54
+ * side, or all of a graph smaller than that. */
55
+ export declare function reachKept(panel: {
56
+ width: number;
57
+ height: number;
58
+ }): number;
59
+ /** The least shift that brings at least `keep` of `graph` back inside `panel` on each axis, so a
60
+ * pan or a fling never leaves the graph out of reach; none while that much already shows. */
61
+ export declare function intoReach(graph: Rect, panel: Rect, keep: number): {
62
+ dx: number;
63
+ dy: number;
64
+ };
41
65
  /** A box on screen, as `getBoundingClientRect` gives it. */
42
66
  export interface Rect {
43
67
  left: number;
@@ -34,15 +34,18 @@ function fitMargin(panel) {
34
34
  return Math.min(panel.width, panel.height) * .05;
35
35
  }
36
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) {
37
+ function fitView(bounds, panel, { minScale = MIN_SCALE, top = 0, width: byWidth = false } = {}) {
38
38
  const width = bounds.maxX - bounds.minX;
39
39
  const height = bounds.maxY - bounds.minY;
40
40
  if (width <= 0 || height <= 0) return null;
41
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));
42
+ const fit = Math.min((panel.width - margin * 2) / width, byWidth ? FIT_MAX_SCALE : (panel.height - margin * 2) / height, FIT_MAX_SCALE);
43
+ const floor = Math.min(Math.max(MIN_SCALE, minScale), FIT_MAX_SCALE);
44
+ const scale = Math.max(floor, fit);
45
+ const y = fit < floor || byWidth ? Math.max(margin, top + margin) : margin;
43
46
  return {
44
- x: margin - bounds.minX * scale,
45
- y: margin - bounds.minY * scale,
47
+ x: (byWidth ? (panel.width - width * scale) / 2 : margin) - bounds.minX * scale,
48
+ y: y - bounds.minY * scale,
46
49
  scale
47
50
  };
48
51
  }
@@ -52,6 +55,25 @@ function intoView(start, end, low, high) {
52
55
  if (start < low) return low - start;
53
56
  return end > high ? high - end : 0;
54
57
  }
58
+ /** How much of a graph a pan or pinch leaves in its panel, at the least: half the panel's shorter
59
+ * side, or all of a graph smaller than that. */
60
+ function reachKept(panel) {
61
+ return Math.min(panel.width, panel.height) / 2;
62
+ }
63
+ /** The least shift that brings at least `keep` of `graph` back inside `panel` on each axis, so a
64
+ * pan or a fling never leaves the graph out of reach; none while that much already shows. */
65
+ function intoReach(graph, panel, keep) {
66
+ const axis = (start, size, low, span) => {
67
+ if (size <= 0) return 0;
68
+ const least = Math.min(keep, size, span);
69
+ if (start + size < low + least) return low + least - (start + size);
70
+ return start > low + span - least ? low + span - least - start : 0;
71
+ };
72
+ return {
73
+ dx: axis(graph.left, graph.width, panel.left, panel.width),
74
+ dy: axis(graph.top, graph.height, panel.top, panel.height)
75
+ };
76
+ }
55
77
  /** The smallest box around all of `rects`, which must not be empty. */
56
78
  function enclosing(rects) {
57
79
  const left = Math.min(...rects.map((r) => r.left));
@@ -114,4 +136,4 @@ function viewShowing(region, target, visible, origin, view, readable) {
114
136
  };
115
137
  }
116
138
  //#endregion
117
- export { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, enclosing, fitMargin, fitView, intoView, viewShowing, wheelFactor, zoomAround, zoomLimit };
139
+ export { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, enclosing, fitMargin, fitView, intoReach, intoView, reachKept, viewShowing, wheelFactor, zoomAround, zoomLimit };
@@ -36,7 +36,10 @@ export declare const STRETCHED_LINK_ROW = "relative [&_:is(a,button,input,label,
36
36
  export declare function NameCell({ icon, iconTile, name, subtitle, href, badge, title, children, }: {
37
37
  icon: ReactNode;
38
38
  /** Sets the icon in a small bordered tile, so rows whose icons differ in
39
- * weight, such as kinds of token or runner, still line up. */
39
+ * weight, such as kinds of token or runner, still line up. The 28px tile
40
+ * sits beside a name and its subtitle; a row with no subtitle keeps its
41
+ * icon bare, centered where the tile would be, so a one-line row stays
42
+ * as short as its text and its name still lines up with tiled rows. */
40
43
  iconTile?: boolean | undefined;
41
44
  name: string;
42
45
  subtitle?: string | undefined;
@@ -45,9 +45,12 @@ function NameCell({ icon, iconTile = false, name, subtitle, href, badge, title,
45
45
  const stretch = useContext(StretchRowLinkContext);
46
46
  const hasIcon = icon !== null && icon !== void 0 && icon !== false;
47
47
  const columns = hasIcon ? "col-span-2 grid grid-cols-subgrid items-center" : "flex items-center";
48
- const inner = /* @__PURE__ */ jsxs(Fragment$1, { children: [hasIcon && (iconTile ? /* @__PURE__ */ jsx("span", {
48
+ const inner = /* @__PURE__ */ jsxs(Fragment$1, { children: [hasIcon && (iconTile && subtitle ? /* @__PURE__ */ jsx("span", {
49
49
  className: "flex size-7 shrink-0 items-center justify-center rounded-md border border-(--theme-border) text-(--theme-muted-text-color) [&>svg]:size-3.5",
50
50
  children: icon
51
+ }) : iconTile ? /* @__PURE__ */ jsx("span", {
52
+ className: "flex w-7 shrink-0 items-center justify-center text-(--theme-muted-text-color) [&>svg]:size-3.5",
53
+ children: icon
51
54
  }) : icon), /* @__PURE__ */ jsxs("div", {
52
55
  className: "min-w-0",
53
56
  children: [/* @__PURE__ */ jsxs("div", {